Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 (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 (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 (ubuntu-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 (windows-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 (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 (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 (ubuntu-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 (windows-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
102 changes: 102 additions & 0 deletions src/commands/integrate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
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, reportActionFailure, runActionWithFollowUp} from '../lib/agent/run.js'
import {preparePromptContext, prepareWizard} from '../lib/agent/wizard.js'
import {billingLabel, detectBilling} from '../lib/project/billing.js'
import {confirm, isInteractive, select} from '../lib/ui/ask.js'

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'}),
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) {
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['no-telemetry']) passthrough.push('--no-telemetry')
return this.config.runCommand('migrate', passthrough)
}
} 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 {driver, 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 (!flags.copy && 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.')
}

// Real store IDs turn "defer everything to ADAPTY_SETUP.md" into a full dashboard setup.
const products = await collectStoreProducts(project.platform)
if (products === null) return this.log('Cancelled.')
const promptCtx = await preparePromptContext(setup, approach, products)

if (flags.copy) {
return emitCopyPrompt(this, integrateAction, promptCtx)
}

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