Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
9412b45
Integrate & migrate commands
eandreeva-twr Jul 24, 2026
408802d
Disclose telemetry after agent runs and add --no-telemetry flag
eandreeva-twr Jul 24, 2026
a163dea
Support Gemini, Cursor, and Copilot agents in integrate & migrate
eandreeva-twr Jul 24, 2026
d58bbaa
Tune agent guidance and UX from test-case review
eandreeva-twr Jul 24, 2026
79724e6
Cover tier-1 test cases: project scan, billing detection, prompts, RC…
eandreeva-twr Jul 24, 2026
1fca467
Make resolveCliCommand test platform-agnostic
eandreeva-twr Jul 24, 2026
8c3f263
Collect store product IDs interactively for integrate & migrate
eandreeva-twr Jul 27, 2026
b08e1aa
Disclose telemetry right after sign-in, and stop printing "as undefined"
eandreeva-twr Jul 29, 2026
7b491f6
Pick where the paywall belongs before creating the placement
eandreeva-twr Jul 29, 2026
1fc8366
Take the migration playbook from the skill instead of inlining it
eandreeva-twr Aug 4, 2026
5ffccb7
Merge remote-tracking branch 'origin/main' into integrate-command-pro…
eandreeva-twr Aug 4, 2026
8cde9c0
Offer the prompt and the skill when no agent is on PATH
eandreeva-twr Aug 5, 2026
3e19c8e
Create no placement on the Flow Builder path
eandreeva-twr Aug 5, 2026
541c231
Put every run on its own branch
eandreeva-twr Aug 5, 2026
4ba961e
Detect an already-populated app and offer a code-only run
eandreeva-twr Aug 14, 2026
dcc8727
Point the skill fetch at the renamed adapty-integration directory
eandreeva-twr Aug 20, 2026
3e341b5
Point the skill fetch at adaptyteam/adapty-skills
eandreeva-twr Aug 20, 2026
a2a11df
Point the skill fetch at the renamed adapty-integration directory
eandreeva-twr Aug 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"url": "https://github.com/adaptyteam/adapty-cli/issues"
},
"dependencies": {
"@clack/prompts": "^1.7.0",
"@oclif/core": "^4",
"@oclif/plugin-help": "^6",
"open": "^11.0.0"
Expand Down
47 changes: 47 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions src/commands/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
access_token: string
expires_in: number
token_type: string
user: {email: string; name: string}
// The response has carried different user shapes; treat every field as optional
// so a missing one degrades to a plain "Authenticated" instead of "undefined".
user?: {email?: string; name?: string}
}

interface TokenErrorResponse {
Expand All @@ -36,7 +38,7 @@
static description = 'Authenticate with Adapty via browser'
static examples = ['<%= config.bin %> auth login']

async run(): Promise<void> {

Check warning on line 41 in src/commands/auth/login.ts

View workflow job for this annotation

GitHub Actions / unit-tests (ubuntu-latest, lts/*)

Async method 'run' has a complexity of 22. Maximum allowed is 20

Check warning on line 41 in src/commands/auth/login.ts

View workflow job for this annotation

GitHub Actions / unit-tests (windows-latest, lts/*)

Async method 'run' has a complexity of 22. Maximum allowed is 20

Check warning on line 41 in src/commands/auth/login.ts

View workflow job for this annotation

GitHub Actions / unit-tests (windows-latest, lts/-1)

Async method 'run' has a complexity of 22. Maximum allowed is 20

Check warning on line 41 in src/commands/auth/login.ts

View workflow job for this annotation

GitHub Actions / unit-tests (ubuntu-latest, lts/-1)

Async method 'run' has a complexity of 22. Maximum allowed is 20
const config = await readConfig(this.config.configDir)
if (config.access_token && config.user) {
this.log(`Already authenticated as ${config.user.email}. Re-authenticating...`)
Expand Down Expand Up @@ -89,7 +91,7 @@
})
} catch (error) {
if (error instanceof ApiError) {
switch (error.errorCode) {

Check warning on line 94 in src/commands/auth/login.ts

View workflow job for this annotation

GitHub Actions / unit-tests (ubuntu-latest, lts/*)

Blocks are nested too deeply (5). Maximum allowed is 4

Check warning on line 94 in src/commands/auth/login.ts

View workflow job for this annotation

GitHub Actions / unit-tests (windows-latest, lts/*)

Blocks are nested too deeply (5). Maximum allowed is 4

Check warning on line 94 in src/commands/auth/login.ts

View workflow job for this annotation

GitHub Actions / unit-tests (windows-latest, lts/-1)

Blocks are nested too deeply (5). Maximum allowed is 4

Check warning on line 94 in src/commands/auth/login.ts

View workflow job for this annotation

GitHub Actions / unit-tests (ubuntu-latest, lts/-1)

Blocks are nested too deeply (5). Maximum allowed is 4
case 'authorization_pending': {
continue
}
Expand Down Expand Up @@ -137,7 +139,8 @@
this.config.configDir,
)

this.log(`\nAuthenticated as ${result.user.email}`)
const who = result.user?.email ?? result.user?.name
this.log(who ? `\nAuthenticated as ${who}` : '\nAuthenticated')
this.log(`Token saved to ${this.config.configDir}/config.json`)
return
}
Expand Down
125 changes: 125 additions & 0 deletions src/commands/integrate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import {Command, Flags} from '@oclif/core'
import {resolve} from 'node:path'

import {integrateAction} from '../lib/agent/actions/integrate.js'
import {DRIVER_IDS, DRIVERS} from '../lib/agent/drivers/index.js'
import {collectStoreProducts} from '../lib/agent/products.js'
import {emitCopyPrompt, prepareWorkBranch, reportActionFailure, runActionWithFollowUp} from '../lib/agent/run.js'
import {preparePromptContext, prepareWizard, resolvePlacementDeveloperId} from '../lib/agent/wizard.js'
import {billingLabel, detectBilling} from '../lib/project/billing.js'
import {confirm, isInteractive, select} from '../lib/ui/ask.js'

/** Every integrate flag re-expressed as migrate argv, so the switch loses nothing the user typed. */
function migratePassthrough(
path: string,
flags: {app?: string; 'code-only'?: boolean; copy?: boolean; driver?: string; 'no-telemetry'?: boolean},
): string[] {
const passthrough = ['--path', path]
if (flags.app) passthrough.push('--app', flags.app)
if (flags.driver) passthrough.push('--driver', flags.driver)
if (flags.copy) passthrough.push('--copy')
if (flags['code-only'] !== undefined) passthrough.push(flags['code-only'] ? '--code-only' : '--no-code-only')
if (flags['no-telemetry']) passthrough.push('--no-telemetry')
return passthrough
}

export default class Integrate extends Command {
static description = `Set up the Adapty SDK in your app using your coding agent (${DRIVERS.map(
(d) => d.displayName,
).join(', ')})`
static examples = [
'<%= config.bin %> integrate',
'<%= config.bin %> integrate --path ./apps/mobile',
'<%= config.bin %> integrate --copy',
]
static flags = {
app: Flags.string({description: 'Adapty app ID (UUID) to connect; skips the app picker'}),
'code-only': Flags.boolean({
allowNo: true,
description:
'The dashboard is already set up - wire the code to the existing entities and create nothing (--no-code-only forces entity creation; without either, the CLI asks when the app is not empty)',
}),
copy: Flags.boolean({
description: 'Print the integration prompt instead of running an agent (paste it into any coding agent)',
}),
driver: Flags.string({description: 'Force a specific coding agent', options: DRIVER_IDS}),
'no-telemetry': Flags.boolean({
description: 'Do not send anonymous usage stats (also honored: ADAPTY_TELEMETRY_DISABLED=1, DO_NOT_TRACK=1)',
}),
path: Flags.string({description: 'App directory (defaults to the current directory)'}),
}

async run(): Promise<void> {
const {flags} = await this.parse(Integrate)
const path = resolve(flags.path ?? process.cwd())

// A project that already has a billing SDK is a migration, not a fresh
// integration - offer the switch BEFORE the wizard so no question runs twice.
const billing = await detectBilling(path)
if (billing) {
if (isInteractive()) {
const wantsMigrate = await confirm(
`Found ${billingLabel(billing)} in this project - \`adapty migrate\` replaces it with Adapty end-to-end. Switch to migrate?`,
)
if (wantsMigrate === null) return this.log('Cancelled.')
if (wantsMigrate) return this.config.runCommand('migrate', migratePassthrough(path, flags))
} else {
this.log(
`Found ${billingLabel(billing)} in this project - \`adapty migrate\` is built for replacing it. Continuing with a fresh integration.`,
)
}
}

const setup = await prepareWizard(this, {...flags, path})
if (!setup) return
const {copyOnly, driver, installSkill, interactive, project, token} = setup

// Paywall approach - the one product question the skill needs answered upfront.
const approach = await select(
'How do you want to build paywalls?',
[
{hint: 'no-code visual editor, recommended', label: 'Flow Builder', value: 'flow_builder'},
{hint: 'you build the UI, Adapty handles products & purchases', label: 'Custom paywall', value: 'custom'},
{hint: 'keep existing purchase code, Adapty only tracks', label: 'Observer mode', value: 'observer'},
],
'flow_builder',
)
if (!approach) return this.log('Cancelled.')

// The go/no-go gate comes BEFORE the product interview - never collect
// answers that a declined confirm would throw away.
if (!copyOnly && interactive && !(await confirm(`Integrate the Adapty SDK into "${project.name}" now?`))) {
return this.log('No problem - run `adapty integrate` again anytime, or use --copy to drive your own agent.')
}

// In code-only mode the placement's developer ID is settled here, by the
// user, not later by the agent - a wrong guess fails silently at runtime.
const placementDeveloperId = await resolvePlacementDeveloperId(this, setup, approach)
if (placementDeveloperId === null) return this.log('Cancelled.')

// Real store IDs turn "defer everything to ADAPTY_SETUP.md" into a full dashboard
// setup - pointless in code-only mode, where the products already exist in Adapty.
const products = setup.dashboardMode === 'code-only' ? [] : await collectStoreProducts(project.platform)
if (products === null) return this.log('Cancelled.')
const promptCtx = await preparePromptContext(setup, approach, products, {placementDeveloperId})

if (copyOnly) {
return emitCopyPrompt(this, integrateAction, promptCtx, {installSkill})
}

// Every run gets its own branch; no git at all is the one case that stops us.
const branch = await prepareWorkBranch(this, project.path, 'integrate', interactive)
if (branch === null) return this.log('Run `git init` and commit what you have, then re-run `adapty integrate`.')

const result = await runActionWithFollowUp(this, {
action: integrateAction,
branch,
ctx: promptCtx,
driver: driver!,
env: token ? {ADAPTY_TOKEN: token} : undefined,
interactive,
noTelemetry: flags['no-telemetry'],
})
if (!result.ok) reportActionFailure(this, driver!, result)
}
}
Loading
Loading