Skip to content

fix(cli): recover from missing notifyAppReady / public key / appId - #3184

Open
riderx wants to merge 5 commits into
mainfrom
cursor/cli-recovery-missing-config-b135
Open

fix(cli): recover from missing notifyAppReady / public key / appId#3184
riderx wants to merge 5 commits into
mainfrom
cursor/cli-recovery-missing-config-b135

Conversation

@riderx

@riderx riderx commented Aug 23, 2026

Copy link
Copy Markdown
Member

Summary (AI generated)

  • Replaces the silencing approach from fix(cli): CliUserError for missing notifyAppReady / public key / appId #3181 with real interactive recovery paths for three high-volume CLI configuration failures.
  • notifyAppReady: bundle zip / bundle upload now offer patch-build, patch-source, manual snippet, or explicit skip-with-warning when notifyAppReady() is missing in the build folder.
  • public key: bundle encrypt can generate/import RSA keys and write CapacitorUpdater.publicKey into capacitor config, then continue.
  • appId: upload/zip resolve missing app IDs from capacitor config, package.json, Gradle applicationId, Capgo app list selection, manual entry, or app add create flow.
  • Adds --ignore-notify-app-ready (alias escape hatch alongside --no-code-check) and test:cli-recovery unit tests.

Motivation (AI generated)

PostHog error tracking shows recurring CLI failures that are fixable in-place, but users currently hit a hard stop:

Martin's direction: do not convert these to CliUserError just to hide PostHog tracking. Keep tracking until a success path actually runs (or emit explicit recovery analytics events).

Business Impact (AI generated)

  • More users complete their first zip/upload/encrypt without leaving the CLI.
  • PostHog keeps measuring unresolved failures so we can verify the recovery flows reduce issue volume over time.
  • Recovery events (CLI Recovered Missing *) give a success funnel alongside existing $exception issues.

Test Plan (AI generated)

  • bunx tsc --noEmit in cli/
  • bun run test:cli-recovery
  • bun run test:posthog-exception (confirms unresolved failures remain capturable)
  • CI cli test suite
  • Manual: run bundle zip in a project missing notifyAppReady() and choose patch-build
  • Manual: run bundle encrypt without publicKey in config interactively
  • Manual: run bundle upload without appId arg and pick/create an app

Generated with AI

Open in Web Open in Cursor 

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Review in cubic

@riderx
riderx deployed to deepsec-pr August 23, 2026 23:19 — with GitHub Actions Active
Comment thread cli/src/recovery/app-id.ts Fixed
@codspeed-hq

codspeed-hq Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 43 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing cursor/cli-recovery-missing-config-b135 (545ce1a) with main (808b7d4)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

cursoragent and others added 2 commits August 23, 2026 23:23
Add interactive recovery flows for the three most common CLI configuration
failures tracked in PostHog error tracking, instead of silencing them as
CliUserError.

- notifyAppReady: patch build output or source entry, manual snippet, or
  explicit skip with warning; keep PostHog capture on unresolved failures
- public key: generate or import keys into capacitor config before encrypt
- appId: auto-detect from project files, select from Capgo apps, or create app

Includes unit tests and --ignore-notify-app-ready flag alias.

Co-authored-by: Martin DONADIEU <martindonadieu@gmail.com>
Co-authored-by: Martin DONADIEU <martindonadieu@gmail.com>
@cursor
cursor Bot force-pushed the cursor/cli-recovery-missing-config-b135 branch from 06d0826 to 7ce75f6 Compare August 23, 2026 23:23
@cursor
cursor Bot deployed to deepsec-pr August 23, 2026 23:23 Active
@coderabbitai

coderabbitai Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

On-demand reviews are free for the next 28 days. After that, they cost $0.25 per reviewed file.

Or wait 6 minutes for your next included review.

View limit details

Limit details: You’ve used all 2 included reviews currently available. Your 51 included PR review attempts over the past 7 days set your current allowance at 2 reviews per hour.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 506a5e95-2e90-45cc-a9d2-b8d2369574ed

📥 Commits

Reviewing files that changed from the base of the PR and between 808b7d4 and 545ce1a.

📒 Files selected for processing (10)
  • cli/package.json
  • cli/src/bundle/encrypt.ts
  • cli/src/bundle/upload.ts
  • cli/src/bundle/zip.ts
  • cli/src/index.ts
  • cli/src/recovery/app-id.ts
  • cli/src/recovery/notify-app-ready.ts
  • cli/src/recovery/public-key.ts
  • cli/src/schemas/bundle.ts
  • cli/test/test-cli-recovery.mjs

Comment @coderabbitai help to get the list of available commands.

Importing init/command pulled Ink into the bundle build and broke CI.
Keep notifyAppReady injection self-contained in the recovery module.

Co-authored-by: Martin DONADIEU <martindonadieu@gmail.com>
@cursor
cursor Bot deployed to deepsec-pr August 23, 2026 23:28 Active
Co-authored-by: Martin DONADIEU <martindonadieu@gmail.com>
@cursor
cursor Bot deployed to deepsec-pr August 23, 2026 23:34 Active
Comment thread cli/src/recovery/app-id.ts Fixed
Co-authored-by: Martin DONADIEU <martindonadieu@gmail.com>
@cursor
cursor Bot deployed to deepsec-pr August 23, 2026 23:49 Active
@sonarqubecloud

Copy link
Copy Markdown

@riderx
riderx marked this pull request as ready for review August 23, 2026 23:56

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

10 issues found across 10 files

Confidence score: 2/5

  • cli/src/bundle/encrypt.ts can pair a supplied private key with an unrelated default public key, producing bundles that cannot be recovered; ensure the public key is derived or validated against the supplied key before encryption.
  • cli/src/recovery/notify-app-ready.ts may patch an unresolved import or select a runtime/polyfill script instead of the application entry, then report success without a working update hook; restrict patching to valid entries and verify the rebuilt output contains the call.
  • cli/src/recovery/public-key.ts and cli/src/bundle/encrypt.ts can write key files or rewrite configuration before later validation fails, leaving mismatched or partially mutated recovery state; validate inputs and existing key pairs before any writes.
  • cli/src/recovery/app-id.ts can block manual recovery, accept invalid remote app_id values, or ignore IDs in a selected package.json, causing incorrect or missing Capacitor configuration; validate candidates and pass the configured package paths through recovery.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="cli/src/bundle/encrypt.ts">

<violation number="1" location="cli/src/bundle/encrypt.ts:40">
P1: When `bundle encrypt` supplies `--key` or `--key-data` while `publicKey` is missing, recovery can install an unrelated default public key, but encryption uses the supplied private key. The resulting bundle cannot be consumed; skip automatic recovery for explicit private keys or derive and save their matching public key.</violation>

<violation number="2" location="cli/src/bundle/encrypt.ts:40">
P2: When the zip path is missing, this recovery can generate key files and rewrite capacitor config before the existing zip guard runs. Check the zip path before invoking recovery so an invalid input does not mutate encryption state.</violation>

<violation number="3" location="cli/src/bundle/encrypt.ts:42">
P1: With `--capacitor-config` selecting a source file, recovery writes that file but this refresh reads the active root config. The command can report a missing public key after a successful write; refresh with `getConfigForWrite()`.</violation>
</file>

<file name="cli/src/recovery/public-key.ts">

<violation number="1" location="cli/src/recovery/public-key.ts:47">
P1: When `.capgo_key_v2` exists but `.capgo_key_v2.pub` is missing, this recovery generates and writes a new public key before `createKeyInternal` rejects the existing private key. A retry then sees the mismatched public file and saves it to the config, so bundles encrypted with the old private key cannot be decrypted; refuse generation when the private key already exists or make keypair creation atomic.</violation>
</file>

<file name="cli/src/recovery/app-id.ts">

<violation number="1" location="cli/src/recovery/app-id.ts:31">
P2: When the app ID exists only in a package.json selected with `--package-json`, recovery ignores that file and falls through to the missing-ID prompt or error. Pass the configured package-json path(s) into candidate collection instead of always reading the workspace-root package.json.</violation>

<violation number="2" location="cli/src/recovery/app-id.ts:153">
P1: When `bundle zip` runs without a saved/provided API key and no local candidate is detected, this guard aborts before the manual recovery option appears. Require an API key only for remote listing or app creation, while allowing detected/manual choices for local zip.</violation>

<violation number="3" location="cli/src/recovery/app-id.ts:173">
P2: When the Capgo app-list response contains an invalid or missing `app_id`, the prompt still offers it and selecting it writes the invalid value into Capacitor config. Filter remote rows with `isValidAppId` before constructing selection options.

(Based on your team's feedback about validating filtered app results.)</violation>
</file>

<file name="cli/src/recovery/notify-app-ready.ts">

<violation number="1" location="cli/src/recovery/notify-app-ready.ts:35">
P1: When `index.html` lists runtime or polyfill scripts before the app bundle, this returns the wrong file and `patch-build` reports recovery without changing the application entry. Identify the actual app entry or patch the relevant bundled scripts rather than taking the first script.</violation>

<violation number="2" location="cli/src/recovery/notify-app-ready.ts:63">
P1: When the built bundle does not already contain the updater binding, `patch-build` adds an unresolved bare package import to the static output, then treats the text match as success. Restrict build patching to an already-bundled binding and require source patch plus rebuild otherwise.</violation>

<violation number="3" location="cli/src/recovery/notify-app-ready.ts:200">
P2: When `patch-source` is selected but the rebuild is cancelled or still lacks the call, this emits a successful-recovery event anyway. Move the event after the rebuilt folder passes `searchInDirectory(webDir, 'notifyAppReady')`.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread cli/src/bundle/encrypt.ts

const hasPrivateKeyInConfig = !!extConfig.config.plugins?.CapacitorUpdater?.privateKey
const hasPublicKeyInConfig = !!extConfig.config.plugins?.CapacitorUpdater?.publicKey
await ensurePublicKeyInConfig({ interactive, silent: silent || json, json })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When bundle encrypt supplies --key or --key-data while publicKey is missing, recovery can install an unrelated default public key, but encryption uses the supplied private key. The resulting bundle cannot be consumed; skip automatic recovery for explicit private keys or derive and save their matching public key.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cli/src/bundle/encrypt.ts, line 40:

<comment>When `bundle encrypt` supplies `--key` or `--key-data` while `publicKey` is missing, recovery can install an unrelated default public key, but encryption uses the supplied private key. The resulting bundle cannot be consumed; skip automatic recovery for explicit private keys or derive and save their matching public key.</comment>

<file context>
@@ -34,10 +35,13 @@ export async function encryptZipInternal(
 
-    const hasPrivateKeyInConfig = !!extConfig.config.plugins?.CapacitorUpdater?.privateKey
-    const hasPublicKeyInConfig = !!extConfig.config.plugins?.CapacitorUpdater?.publicKey
+    await ensurePublicKeyInConfig({ interactive, silent: silent || json, json })
+
+    const refreshedConfig = await getConfig()
</file context>
Suggested change
await ensurePublicKeyInConfig({ interactive, silent: silent || json, json })
if (!options.key && !options.keyData)
await ensurePublicKeyInConfig({ interactive, silent: silent || json, json })

Comment thread cli/src/bundle/encrypt.ts
const hasPublicKeyInConfig = !!extConfig.config.plugins?.CapacitorUpdater?.publicKey
await ensurePublicKeyInConfig({ interactive, silent: silent || json, json })

const refreshedConfig = await getConfig()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: With --capacitor-config selecting a source file, recovery writes that file but this refresh reads the active root config. The command can report a missing public key after a successful write; refresh with getConfigForWrite().

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cli/src/bundle/encrypt.ts, line 42:

<comment>With `--capacitor-config` selecting a source file, recovery writes that file but this refresh reads the active root config. The command can report a missing public key after a successful write; refresh with `getConfigForWrite()`.</comment>

<file context>
@@ -34,10 +35,13 @@ export async function encryptZipInternal(
-    const hasPublicKeyInConfig = !!extConfig.config.plugins?.CapacitorUpdater?.publicKey
+    await ensurePublicKeyInConfig({ interactive, silent: silent || json, json })
+
+    const refreshedConfig = await getConfig()
+    const hasPrivateKeyInConfig = !!refreshedConfig.config.plugins?.CapacitorUpdater?.privateKey
+    const hasPublicKeyInConfig = !!refreshedConfig.config.plugins?.CapacitorUpdater?.publicKey
</file context>

}
else {
log.info('Capgo can generate an RSA keypair, save the public key in capacitor config, and keep the private key in .capgo_key_v2.')
await createKeyInternal({ force: false, setupChannel: false }, true, extConfig)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When .capgo_key_v2 exists but .capgo_key_v2.pub is missing, this recovery generates and writes a new public key before createKeyInternal rejects the existing private key. A retry then sees the mismatched public file and saves it to the config, so bundles encrypted with the old private key cannot be decrypted; refuse generation when the private key already exists or make keypair creation atomic.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cli/src/recovery/public-key.ts, line 47:

<comment>When `.capgo_key_v2` exists but `.capgo_key_v2.pub` is missing, this recovery generates and writes a new public key before `createKeyInternal` rejects the existing private key. A retry then sees the mismatched public file and saves it to the config, so bundles encrypted with the old private key cannot be decrypted; refuse generation when the private key already exists or make keypair creation atomic.</comment>

<file context>
@@ -0,0 +1,63 @@
+  }
+  else {
+    log.info('Capgo can generate an RSA keypair, save the public key in capacitor config, and keep the private key in .capgo_key_v2.')
+    await createKeyInternal({ force: false, setupChannel: false }, true, extConfig)
+  }
+
</file context>


const resolvedApikey = options.apikey || findSavedKey()
if (!resolvedApikey)
throw new Error('Missing API key. Run `npx @capgo/cli@latest login` first.')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When bundle zip runs without a saved/provided API key and no local candidate is detected, this guard aborts before the manual recovery option appears. Require an API key only for remote listing or app creation, while allowing detected/manual choices for local zip.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cli/src/recovery/app-id.ts, line 153:

<comment>When `bundle zip` runs without a saved/provided API key and no local candidate is detected, this guard aborts before the manual recovery option appears. Require an API key only for remote listing or app creation, while allowing detected/manual choices for local zip.</comment>

<file context>
@@ -0,0 +1,268 @@
+
+  const resolvedApikey = options.apikey || findSavedKey()
+  if (!resolvedApikey)
+    throw new Error('Missing API key. Run `npx @capgo/cli@latest login` first.')
+
+  while (true) {
</file context>

continue
const candidate = join(webDir, src.replace(/^\//, ''))
if (existsSync(candidate) && extname(candidate) === '.js')
return candidate

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When index.html lists runtime or polyfill scripts before the app bundle, this returns the wrong file and patch-build reports recovery without changing the application entry. Identify the actual app entry or patch the relevant bundled scripts rather than taking the first script.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cli/src/recovery/notify-app-ready.ts, line 35:

<comment>When `index.html` lists runtime or polyfill scripts before the app bundle, this returns the wrong file and `patch-build` reports recovery without changing the application entry. Identify the actual app entry or patch the relevant bundled scripts rather than taking the first script.</comment>

<file context>
@@ -0,0 +1,213 @@
+        continue
+      const candidate = join(webDir, src.replace(/^\//, ''))
+      if (existsSync(candidate) && extname(candidate) === '.js')
+        return candidate
+    }
+  }
</file context>


const updaterImport = extname(filePath) === '.cjs'
? 'const { CapacitorUpdater } = require(\'@capgo/capacitor-updater\')'
: 'import { CapacitorUpdater } from \'@capgo/capacitor-updater\''

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When the built bundle does not already contain the updater binding, patch-build adds an unresolved bare package import to the static output, then treats the text match as success. Restrict build patching to an already-bundled binding and require source patch plus rebuild otherwise.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cli/src/recovery/notify-app-ready.ts, line 63:

<comment>When the built bundle does not already contain the updater binding, `patch-build` adds an unresolved bare package import to the static output, then treats the text match as success. Restrict build patching to an already-bundled binding and require source patch plus rebuild otherwise.</comment>

<file context>
@@ -0,0 +1,213 @@
+
+  const updaterImport = extname(filePath) === '.cjs'
+    ? 'const { CapacitorUpdater } = require(\'@capgo/capacitor-updater\')'
+    : 'import { CapacitorUpdater } from \'@capgo/capacitor-updater\''
+  return `${updaterImport};\n\n${NOTIFY_CALL};\n${content}`
+}
</file context>

Comment thread cli/src/bundle/encrypt.ts

const hasPrivateKeyInConfig = !!extConfig.config.plugins?.CapacitorUpdater?.privateKey
const hasPublicKeyInConfig = !!extConfig.config.plugins?.CapacitorUpdater?.publicKey
await ensurePublicKeyInConfig({ interactive, silent: silent || json, json })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When the zip path is missing, this recovery can generate key files and rewrite capacitor config before the existing zip guard runs. Check the zip path before invoking recovery so an invalid input does not mutate encryption state.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cli/src/bundle/encrypt.ts, line 40:

<comment>When the zip path is missing, this recovery can generate key files and rewrite capacitor config before the existing zip guard runs. Check the zip path before invoking recovery so an invalid input does not mutate encryption state.</comment>

<file context>
@@ -34,10 +35,13 @@ export async function encryptZipInternal(
 
-    const hasPrivateKeyInConfig = !!extConfig.config.plugins?.CapacitorUpdater?.privateKey
-    const hasPublicKeyInConfig = !!extConfig.config.plugins?.CapacitorUpdater?.publicKey
+    await ensurePublicKeyInConfig({ interactive, silent: silent || json, json })
+
+    const refreshedConfig = await getConfig()
</file context>

push(config?.appId)
push(config?.plugins?.CapacitorUpdater?.appId)

const packageJsonPath = join(projectRoot, PACKNAME)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When the app ID exists only in a package.json selected with --package-json, recovery ignores that file and falls through to the missing-ID prompt or error. Pass the configured package-json path(s) into candidate collection instead of always reading the workspace-root package.json.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cli/src/recovery/app-id.ts, line 31:

<comment>When the app ID exists only in a package.json selected with `--package-json`, recovery ignores that file and falls through to the missing-ID prompt or error. Pass the configured package-json path(s) into candidate collection instead of always reading the workspace-root package.json.</comment>

<file context>
@@ -0,0 +1,268 @@
+  push(config?.appId)
+  push(config?.plugins?.CapacitorUpdater?.appId)
+
+  const packageJsonPath = join(projectRoot, PACKNAME)
+  if (existsSync(packageJsonPath)) {
+    try {
</file context>

})
}
}
for (const app of remoteApps) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When the Capgo app-list response contains an invalid or missing app_id, the prompt still offers it and selecting it writes the invalid value into Capacitor config. Filter remote rows with isValidAppId before constructing selection options.

(Based on your team's feedback about validating filtered app results.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cli/src/recovery/app-id.ts, line 173:

<comment>When the Capgo app-list response contains an invalid or missing `app_id`, the prompt still offers it and selecting it writes the invalid value into Capacitor config. Filter remote rows with `isValidAppId` before constructing selection options.

(Based on your team's feedback about validating filtered app results.) </comment>

<file context>
@@ -0,0 +1,268 @@
+        })
+      }
+    }
+    for (const app of remoteApps) {
+      selectOptions.push({
+        value: `remote:${app.app_id}`,
</file context>

}
log.success(`Added notifyAppReady() to ${patchedPath}`)
log.info('Rebuild your web assets, then retry zip/upload.')
void trackEvent({ channel: 'bundle', event: 'CLI Recovered Missing NotifyAppReady', tags: { recovery: 'patch-source' } })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When patch-source is selected but the rebuild is cancelled or still lacks the call, this emits a successful-recovery event anyway. Move the event after the rebuilt folder passes searchInDirectory(webDir, 'notifyAppReady').

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cli/src/recovery/notify-app-ready.ts, line 200:

<comment>When `patch-source` is selected but the rebuild is cancelled or still lacks the call, this emits a successful-recovery event anyway. Move the event after the rebuilt folder passes `searchInDirectory(webDir, 'notifyAppReady')`.</comment>

<file context>
@@ -0,0 +1,213 @@
+        }
+        log.success(`Added notifyAppReady() to ${patchedPath}`)
+        log.info('Rebuild your web assets, then retry zip/upload.')
+        void trackEvent({ channel: 'bundle', event: 'CLI Recovered Missing NotifyAppReady', tags: { recovery: 'patch-source' } })
+        const rebuilt = await pConfirm({ message: 'Have you rebuilt your app into the build folder?' })
+        if (pIsCancel(rebuilt) || !rebuilt)
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants