Skip to content

fix: save org/app state on resume, enforce access checks, remove redundant prompts#569

Merged
WcaleNieWolny merged 5 commits intomainfrom
fix/save-org-on-resume
Mar 24, 2026
Merged

fix: save org/app state on resume, enforce access checks, remove redundant prompts#569
WcaleNieWolny merged 5 commits intomainfrom
fix/save-org-on-resume

Conversation

@WcaleNieWolny
Copy link
Copy Markdown
Contributor

@WcaleNieWolny WcaleNieWolny commented Mar 23, 2026

Summary

  • Save org and app state on resume: persist orgId, orgName, and appId to the temp file so resume doesn't fall back to defaults or re-ask org selection
  • Enforce role and 2FA checks on resume: validate saved org still has admin/super_admin role and passes 2FA guard before auto-resuming — permissions may change between sessions
  • Handle RPC errors: the resume path now checks for get_orgs_v7 errors instead of silently treating API failures as "org no longer available"
  • Remove redundant channel confirm: user already picked the channel name, no need to ask "Create channel X?" again
  • Cleanup temp file on "start over": when user declines to resume, delete the saved progress so they aren't asked again next run
  • Inform user about incompatible saved state: old temp files without orgId show a warning instead of silently starting fresh

Test plan

  • Run onboarding to step 2+, cancel, re-run — resume prompt should show org name and app ID
  • Pick "No, start over" — next run should NOT show resume prompt
  • Change user role to non-admin, resume — should warn and fall back to org selection
  • Channel step should go straight to creation after picking a name (no second confirm)

Summary by CodeRabbit

  • New Features

    • Resume now restores organization, organization name and application context so onboarding can continue with the previous app.
    • Resume happens before organization selection and can skip completed steps when you choose to continue.
    • Channel creation now proceeds automatically (no extra confirmation prompt).
  • Bug Fixes

    • If saved resume data is missing or invalid, the app falls back to interactive org selection and resets resume progress.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 23, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d09a8855-a0aa-4e50-a2f8-4eafe322c197

📥 Commits

Reviewing files that changed from the base of the PR and between 6c2797e and 315f22e.

📒 Files selected for processing (1)
  • src/init/command.ts

📝 Walkthrough

Walkthrough

Replaces the old resume mechanism with tryResumeOnboarding(apikey), persists orgId/orgName/appId in tmp onboarding state, adds process-wide globalAppId/globalOrgId/globalOrgName, resumes before org selection in initApp, validates saved org via Supabase, and updates globalAppId when apps are created. Removes readStepsDone(...).

Changes

Cohort / File(s) Summary
Onboarding resume & globals
src/init/command.ts
Introduces tryResumeOnboarding(apikey) that reads extended tmp state (step_done, orgId, orgName, optional appId), prompts user to continue or restart, returns structured resume data, and exposes globalAppId/globalOrgId/globalOrgName.
Init control flow & org validation
src/init/command.ts
Moves resume logic to before org selection in initApp, computes stepToSkip from resume, calls supabase.rpc('get_orgs_v7') to validate/restored org and required access, falls back to interactive selection if validation fails, and removes later readStepsDone(...) usage.
App & channel step handling
src/init/command.ts
Ensures addAppStep(...) writes the returned appId into globalAppId; addChannelStep(...) now always attempts channel creation via addChannelInternal(...) under a spinner (removes prior "Create channel?" confirmation branch).

Sequence Diagram

sequenceDiagram
    participant User as User
    participant Init as initApp
    participant Resume as tryResumeOnboarding
    participant Local as LocalTmpState
    participant DB as Supabase
    participant AddApp as addAppStep

    User->>Init: start init (apikey)
    Init->>Resume: tryResumeOnboarding(apikey)
    Resume->>Local: read tmp onboarding state
    Local-->>Resume: { step_done, orgId, orgName, appId? }
    alt valid state (orgId && step_done)
        Resume->>User: prompt ["Yes, continue","No, start over"]
        User-->>Resume: choice
        alt continue
            Resume->>DB: rpc('get_orgs_v7')
            DB-->>Resume: orgs
            Resume->>Resume: find org by gid === orgId
            alt org found and access ok
                Resume-->>Init: { stepDone, orgId, orgName, appId? }
            else
                Resume-->>Init: undefined
            end
        else start over
            Resume-->>Init: undefined
        end
    else
        Resume-->>Init: undefined
    end

    alt resume returned
        Init->>Init: set globalOrgId/globalOrgName
        Init->>Init: if appId present -> set globalAppId
        Init->>Init: stepToSkip = stepDone
    else fallback
        Init->>Init: interactive org selection, stepToSkip = 0
    end

    alt running add-app step
        Init->>AddApp: addAppStep(...)
        AddApp-->>Init: { appId }
        Init->>Init: set globalAppId = appId
        Init->>Local: markStepDone(... includes appId)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I found a tiny saved trail,
a note that said, "You may prevail."
I hopped back in, restored the place,
org and app snug in my pace.
Onward we go — a bouncy grace. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: saving org data in onboarding state to enable resume functionality that skips org selection, which aligns with the PR's core objective.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/save-org-on-resume

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/init/command.ts (2)

2347-2350: Minor: orgName could be undefined for legacy state files.

If a user resumes from a state file created before this PR (which wouldn't have orgName saved), the warning message will display "undefined" for the org name. Consider using a fallback.

🔧 Suggested improvement
-      pLog.warn(`Previously used organization "${resumed.orgName}" is no longer available. Please select a new one.`)
+      pLog.warn(`Previously used organization "${resumed.orgName || resumed.orgId}" is no longer available. Please select a new one.`)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/init/command.ts` around lines 2347 - 2350, The warning message uses
resumed.orgName which may be undefined for legacy state files; update the
pLog.warn call in the else branch (where resumed and organization selection
happens) to use a safe fallback (e.g., resumed.orgName || '<previous
organization>' or similar) so the log never prints "undefined"; keep the rest of
the flow (calling selectOrganizationForInit and resetting stepToSkip) unchanged.

434-436: Minor: Falsy check on step_done could reject step 0.

The condition !step_done will be true if step_done is 0. While the current code never writes step_done: 0 to the state file (steps start at 1), this could be confusing if the behavior changes later. Consider using an explicit type check.

🔧 Suggested improvement
-    if (!orgId || !step_done)
+    if (!orgId || typeof step_done !== 'number' || step_done < 1)
       return undefined
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/init/command.ts` around lines 434 - 436, The current destructure and
guard uses a falsy check (!step_done) which will incorrectly reject a valid
numeric 0; change the guard to explicitly check for null/undefined instead
(e.g., test step_done == null or typeof step_done === 'undefined') while keeping
the same orgId null/undefined check for orgId; update the conditional that
follows the JSON.parse(rawData) destructure (referencing step_done, orgId,
orgName, pathToPackageJson, channelName, platform, delta, currentVersion) so it
only returns undefined when step_done or orgId are actually missing, not merely
falsy.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/init/command.ts`:
- Around line 2339-2351: The RPC call supabase.rpc('get_orgs_v7') in the resumed
branch doesn't check the returned error, so allOrganizations can be undefined on
API failure and the code wrongly warns the org is missing; update the resumed
path to inspect the RPC response error (like selectOrganizationForInit does),
log pLog.error with the RPC error when present, and only proceed to find
savedOrg when data exists—if an error occurred, surface the error and prompt
retry or fall back to selectOrganizationForInit (affecting variables
allOrganizations, savedOrg, organization, and stepToSkip).

---

Nitpick comments:
In `@src/init/command.ts`:
- Around line 2347-2350: The warning message uses resumed.orgName which may be
undefined for legacy state files; update the pLog.warn call in the else branch
(where resumed and organization selection happens) to use a safe fallback (e.g.,
resumed.orgName || '<previous organization>' or similar) so the log never prints
"undefined"; keep the rest of the flow (calling selectOrganizationForInit and
resetting stepToSkip) unchanged.
- Around line 434-436: The current destructure and guard uses a falsy check
(!step_done) which will incorrectly reject a valid numeric 0; change the guard
to explicitly check for null/undefined instead (e.g., test step_done == null or
typeof step_done === 'undefined') while keeping the same orgId null/undefined
check for orgId; update the conditional that follows the JSON.parse(rawData)
destructure (referencing step_done, orgId, orgName, pathToPackageJson,
channelName, platform, delta, currentVersion) so it only returns undefined when
step_done or orgId are actually missing, not merely falsy.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1eaeb05a-8bb9-43b5-9738-be916b94f870

📥 Commits

Reviewing files that changed from the base of the PR and between cb465fd and 7a6d850.

📒 Files selected for processing (1)
  • src/init/command.ts

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7a6d850bf4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2342 to +2345
const savedOrg = allOrganizations?.find(org => org.gid === resumed.orgId)
if (savedOrg) {
organization = savedOrg
pLog.info(`Using organization "${savedOrg.name}"`)
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 Badge Reapply org access checks before auto-resuming org

This branch accepts savedOrg from get_orgs_v7 and assigns it directly, which skips the admin-role and 2FA checks enforced in selectOrganizationForInit (normalizeRole filtering and enforcing_2fa guard). If a user’s permissions changed since the saved state (e.g., no longer admin, or 2FA required but not enabled), resume will continue with an org they can’t onboard and fail later instead of falling back to org selection as intended.

Useful? React with 👍 / 👎.

@WcaleNieWolny WcaleNieWolny force-pushed the fix/save-org-on-resume branch from 7a6d850 to e391b04 Compare March 23, 2026 16:44
Previously the org selection prompt appeared every time, even when
resuming. Now markStepDone persists orgId/orgName in the temp file,
and tryResumeOnboarding reads it back — asking "resume?" before org
selection. If the saved org is no longer available, falls back to
the normal org picker and resets progress.
Old temp files without orgId are now explicitly flagged with a warning
instead of silently discarding the saved state.
@WcaleNieWolny WcaleNieWolny force-pushed the fix/save-org-on-resume branch from e391b04 to 6d47ff2 Compare March 23, 2026 16:46
Without this, resuming onboarding uses the appId from capacitor.config
instead of the one created during step 1, causing subsequent steps to
reference the wrong app.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/init/command.ts (1)

453-469: ⚠️ Potential issue | 🟠 Major

Validate the saved org before hydrating the resume state.

Lines 453-468 restore globalPathToPackageJson / globalChannelName / related state before the org has been validated, while Lines 2348-2356 only check whether the saved gid exists. That leaves three bad paths here: an RPC failure gets reported as “org no longer available”, a downgraded or non-2FA-eligible org bypasses the same guards as selectOrganizationForInit(), and the fallback stepToSkip = 0 path can still carry stale resume state into the fresh flow. Only apply the saved globals after get_orgs_v7 succeeds and the org passes the normal admin/2FA checks; otherwise clear or rewrite the tmp state before falling back.

Also applies to: 2341-2358

🧹 Nitpick comments (1)
src/init/command.ts (1)

474-476: Use formatError(err) in this CLI error path.

Interpolating err directly here will often degrade to [object Object] and makes the resume failure harder to understand.

Proposed fix
   catch (err) {
-    pLog.error(`Cannot read which steps have been completed, error:\n${err}`)
+    pLog.error(`Cannot read which steps have been completed, error:\n${formatError(err)}`)
     pLog.warn('Onboarding will continue but please report it to the capgo team!')
     return undefined
   }

As per coding guidelines, "For user-visible error messages, format errors with formatError(...) instead of dumping raw exceptions when possible".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/init/command.ts` around lines 474 - 476, Replace the raw error
interpolation in the catch block that calls pLog.error with a formatted error
using formatError(err); specifically update the pLog.error invocation (the one
currently doing pLog.error(`Cannot read which steps have been completed,
error:\n${err}`)) to pass formatError(err) so the message becomes the same
descriptive text combined with formatError(err), ensuring you import or
reference formatError where used.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/init/command.ts`:
- Around line 474-476: Replace the raw error interpolation in the catch block
that calls pLog.error with a formatted error using formatError(err);
specifically update the pLog.error invocation (the one currently doing
pLog.error(`Cannot read which steps have been completed, error:\n${err}`)) to
pass formatError(err) so the message becomes the same descriptive text combined
with formatError(err), ensuring you import or reference formatError where used.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 189198f1-d81b-474a-acee-56ba01c099ba

📥 Commits

Reviewing files that changed from the base of the PR and between e391b04 and 6d47ff2.

📒 Files selected for processing (1)
  • src/init/command.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/init/command.ts (1)

431-485: Consider refactoring to reduce cognitive complexity.

SonarCloud flags this function with cognitive complexity 18 (limit 15). While the logic is correct, extracting the state restoration (lines 457-474) into a helper would improve readability and comply with complexity thresholds.

♻️ Suggested refactor to extract state restoration
+function restoreGlobalStateFromResume(
+  pathToPackageJson?: string,
+  channelName?: string,
+  platform?: string,
+  delta?: boolean,
+  currentVersion?: string,
+  savedAppId?: string,
+) {
+  if (pathToPackageJson)
+    globalPathToPackageJson = pathToPackageJson
+  if (channelName)
+    globalChannelName = channelName
+  if (platform === 'ios' || platform === 'android')
+    globalPlatform = platform
+  if (typeof delta === 'boolean')
+    globalDelta = delta
+  if (typeof currentVersion === 'string' && currentVersion.length > 0)
+    globalCurrentVersion = currentVersion
+  if (savedAppId)
+    globalAppId = savedAppId
+}
+
 async function tryResumeOnboarding(apikey: string): Promise<ResumeResult | undefined> {
   try {
     const rawData = readFileSync(getTmpObjectPath(), 'utf-8')
     if (!rawData || rawData.length === 0)
       return undefined

     const { step_done, orgId, orgName, appId: savedAppId, pathToPackageJson, channelName, platform, delta, currentVersion } = JSON.parse(rawData)
     if (!orgId || !step_done) {
       pLog.warn('⚠️  Found previous onboarding progress, but it was saved in an older format.')
       pLog.info('   Starting fresh. Your previous progress cannot be resumed.')
       return undefined
     }

     pLog.info(formatInitResumeMessage(step_done, initOnboardingSteps.length))
     if (orgName) {
       pLog.info(`   Organization: ${orgName}`)
     }
     const resumeChoice = await pSelect({
       message: 'Would you like to continue from where you left off?',
       options: [
         { value: 'yes', label: '✅ Yes, continue' },
         { value: 'no', label: '❌ No, start over' },
       ],
     })
     await cancelCommand(resumeChoice, orgId, apikey)
     if (resumeChoice === 'yes') {
-      if (pathToPackageJson) {
-        globalPathToPackageJson = pathToPackageJson
-      }
-      if (channelName) {
-        globalChannelName = channelName
-      }
-      if (platform === 'ios' || platform === 'android') {
-        globalPlatform = platform
-      }
-      if (typeof delta === 'boolean') {
-        globalDelta = delta
-      }
-      if (typeof currentVersion === 'string' && currentVersion.length > 0) {
-        globalCurrentVersion = currentVersion
-      }
-      if (savedAppId) {
-        globalAppId = savedAppId
-      }
+      restoreGlobalStateFromResume(pathToPackageJson, channelName, platform, delta, currentVersion, savedAppId)
       return { stepDone: step_done, orgId, orgName, appId: savedAppId }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/init/command.ts` around lines 431 - 485, The tryResumeOnboarding function
has a dense state-restoration block (setting globalPathToPackageJson,
globalChannelName, globalPlatform, globalDelta, globalCurrentVersion,
globalAppId from pathToPackageJson, channelName, platform, delta,
currentVersion, savedAppId) that increases cognitive complexity; extract that
logic into a small helper function (e.g., restoreOnboardingState(parsed:
{pathToPackageJson?, channelName?, platform?, delta?, currentVersion?, appId?})
or restoreOnboardingStateFromSaved) which performs the conditional assignments
and returns any needed values, then call this helper from tryResumeOnboarding
(replace the inline assignments with a single call) so tryResumeOnboarding
becomes simpler and under the complexity threshold.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/init/command.ts`:
- Around line 431-485: The tryResumeOnboarding function has a dense
state-restoration block (setting globalPathToPackageJson, globalChannelName,
globalPlatform, globalDelta, globalCurrentVersion, globalAppId from
pathToPackageJson, channelName, platform, delta, currentVersion, savedAppId)
that increases cognitive complexity; extract that logic into a small helper
function (e.g., restoreOnboardingState(parsed: {pathToPackageJson?,
channelName?, platform?, delta?, currentVersion?, appId?}) or
restoreOnboardingStateFromSaved) which performs the conditional assignments and
returns any needed values, then call this helper from tryResumeOnboarding
(replace the inline assignments with a single call) so tryResumeOnboarding
becomes simpler and under the complexity threshold.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f9be5ac4-4149-4aaf-acfb-4044a5c7d5cd

📥 Commits

Reviewing files that changed from the base of the PR and between 6d47ff2 and 6c2797e.

📒 Files selected for processing (1)
  • src/init/command.ts

- Remove "Create channel X for Y in Capgo?" confirmation — user already
  picked the channel name, no need to ask again
- Delete temp file when user chooses "No, start over" on the resume prompt
  so they aren't asked again on the next run
- Fix "Done" → "done" in channel success message
- Validate saved org has admin/super_admin role before auto-resuming
  (permissions may have changed since state was saved)
- Check enforcing_2fa guard on resume just like selectOrganizationForInit does
- Handle RPC error from get_orgs_v7 in the resume path instead of silently
  treating API failures as "org no longer available"
- Fall back to org selection with stepToSkip=0 in all failure cases
@WcaleNieWolny WcaleNieWolny changed the title fix: save org in onboarding state so resume skips org selection fix: save org/app state on resume, enforce access checks, remove redundant prompts Mar 24, 2026
@WcaleNieWolny WcaleNieWolny merged commit 844643f into main Mar 24, 2026
13 checks passed
@sonarqubecloud
Copy link
Copy Markdown

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.

1 participant