Skip to content

feat(cli): add experimental stack restart - #6512

Open
jgoux wants to merge 8 commits into
feat/experimental-stack-preparefrom
feat/experimental-stack-restart
Open

feat(cli): add experimental stack restart#6512
jgoux wants to merge 8 commits into
feat/experimental-stack-preparefrom
feat/experimental-stack-restart

Conversation

@jgoux

@jgoux jgoux commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

supabase experimental stack restart stops and starts an existing managed stack with the selected project’s configuration while preserving its identity and data. Validate configuration and prepare artifacts before stopping; if startup fails afterward, the same stack remains available for recovery.

This PR builds on experimental stack prepare (#6511).

@jgoux
jgoux requested a review from a team as a code owner September 7, 2026 23:52

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Superseded by a newer AI review

🤖 AI Review

Only Claude's independent review was available; the Codex review did not complete. Seven minor/nit findings are confirmed, covering duplicated helpers, required branch coverage, consistency, and incomplete side-effect documentation. The runtime-mismatch finding is refuted by the stack API and implementation. No critical or major defects were found.

Findings

Severity Location Category Sources Claim
🟡 MINOR apps/cli/src/commands/experimental/stack/restart/restart.handler.ts:83 code-duplication claude The restart handler duplicates status serialization and rendering helpers already used by the start command, contrary to the trusted repository convention requiring shared family helpers to be hoisted.
🟡 MINOR apps/cli/src/commands/experimental/stack/restart/restart.handler.ts:129 test-coverage claude The malformed --stack-id rejection branch is not exercised by the restart integration tests, violating the repository's 100% handler branch-coverage requirement.
🟡 MINOR apps/cli/src/commands/experimental/stack/restart/SIDE_EFFECTS.md:1 documentation claude The new SIDE_EFFECTS.md does not provide the required structured coverage of files, API activity, environment variables, exit codes, telemetry, and output modes.
🟡 MINOR apps/cli/src/commands/experimental/stack/restart/restart.integration.test.ts:36 test-coverage claude The restart fixture always uses empty endpoints and capabilities, leaving the renderer's endpoint and dormant-capability branches uncovered; the fixture also exposes unused missing-config and projectRoot affordances.
⚪ NIT apps/cli/src/commands/experimental/stack/restart/restart.handler.ts:117 consistency claude The legacy output-flag error omits the suggestion field consistently supplied by sibling experimental stack commands.
⚪ NIT apps/cli/src/commands/experimental/stack/restart/restart.handler.ts:21 style claude StackError is referenced through repeated inline import types despite an existing top-level import from the same module.
⚪ NIT apps/cli/src/commands/experimental/stack/restart/restart.handler.ts:144 error-handling claude A missing explicitly named stack receives the generic current-project not-found message rather than identifying the requested name.
Refuted findings (kept for transparency, not posted as review comments)
  • apps/cli/src/commands/experimental/stack/restart/restart.handler.ts:40 (error-handling): StackRuntimeMismatchError cannot arise during restart and is therefore incorrectly classified as a flags error with inapplicable advice.
    Refuted: Concrete package code contradicts both premises: runtime mismatch is part of openStack's public failure contract and is constructed outside createStack, while choosing another --stack name is an available restart action. Sibling start and prepare mappings also classify this error as flags-related.

Stats

Claude findings: 8 · Codex findings: 0 · Confirmed: 7 · Refuted: 1 · Uncertain: 0


Models: claude-opus-5 + gpt-5.6-sol · Trigger: auto · Workflow run

This review runs once per PR. A maintainer can request another with a /ai-review comment.

Comment thread apps/cli/src/commands/experimental/stack/restart/restart.handler.ts Outdated
Comment thread apps/cli/src/commands/experimental/stack/restart/restart.handler.ts
Comment thread apps/cli/src/commands/experimental/stack/restart/restart.handler.ts
Comment thread apps/cli/src/commands/experimental/stack/restart/SIDE_EFFECTS.md Outdated
Comment thread apps/cli/src/commands/experimental/stack/restart/restart.handler.ts Outdated
Comment thread apps/cli/src/commands/experimental/stack/restart/restart.handler.ts
Comment thread apps/cli/src/commands/experimental/stack/restart/restart.integration.test.ts Outdated
@jgoux
jgoux force-pushed the feat/experimental-stack-restart branch from 0f58457 to 37240ee Compare September 8, 2026 05:31
@jgoux
jgoux force-pushed the feat/experimental-stack-restart branch from 37240ee to c98065a Compare September 8, 2026 06:02
@jgoux
jgoux force-pushed the feat/experimental-stack-restart branch from c98065a to c7e2e96 Compare September 8, 2026 12:32
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Supabase CLI preview

npx --yes https://pkg.pr.new/supabase/cli/supabase@c7e2e96f89c6ad02a57ff7161a5923d2c18385c1

Preview package for commit c7e2e96.

@jgoux

jgoux commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

/ai-review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 AI Review

Only Claude's independent review was available; the Codex pass did not complete. All eight reported concerns were verified in the checked-out code. None is critical or major: three are minor consistency/UX/duplication issues and five are nits involving interruption cleanup, maintainability, style, documentation, and test coverage.

Findings

Severity Location Category Sources Claim
🟡 MINOR apps/cli/src/commands/experimental/stack/restart/restart.handler.ts:49 error-handling claude Restart classifies StackRuntimeError and StackCleanupError as user-actionable lifecycle/configuration failures, while start classifies the same errors from stack.start() as unknown failures with debug guidance, producing inconsistent telemetry and remediation.
🟡 MINOR apps/cli/src/commands/experimental/stack/restart/restart.handler.ts:101 duplication claude Restart duplicates stack target validation and resolution logic already implemented across sibling handlers instead of sharing it, contrary to the trusted Hoist Before You Duplicate convention.
🟡 MINOR apps/cli/src/commands/experimental/stack/restart/restart.handler.ts:149 user-experience claude If stop succeeds and start fails, the stack remains stopped, but the surfaced error does not disclose that state change or provide explicit recovery guidance.
⚪ NIT apps/cli/src/commands/experimental/stack/restart/restart.handler.ts:144 user-experience claude Interrupting restart during prepare, stop, or start leaves its progress task unsettled because only ordinary success and typed-error paths finalize it.
⚪ NIT apps/cli/src/commands/experimental/stack/restart/restart.errors.ts:25 maintainability claude The actionability getter uses a non-exhaustive if chain with a redundant port branch, so a future reason can silently fall through to unknown.
⚪ NIT apps/cli/src/commands/experimental/stack/stack.shared.ts:12 style claude StackStatus is imported through a separate third import declaration from @supabase/stack/effect instead of being included in the existing import block.
⚪ NIT apps/cli/src/commands/experimental/stack/restart/restart.command.ts:17 documentation claude The new restart command supplies no Command.withExamples metadata, so its generated help and reference material lack examples for current, named, or ID-based target selection.
⚪ NIT apps/cli/src/commands/experimental/stack/restart/restart.integration.test.ts:305 test-coverage claude The tests omit the unnamed missing-target branch, inspectStack failure handling for --stack-id, and verification of the projectRoot/name query passed to findStack.

Stats

Claude findings: 8 · Codex findings: 0 · Confirmed: 8 · Refuted: 0 · Uncertain: 0


Models: claude-opus-5 + gpt-5.6-sol · Trigger: manual · Workflow run

This review runs once per PR. A maintainer can request another with a /ai-review comment.

Comment on lines +49 to +61
Match.tag(
"StackLifecycleConflictError",
"StackNotRunningError",
"StackMustBeStoppedError",
"StackOwnershipConflictError",
"StackUpgradeRequiredError",
"StackRuntimeError",
"StackCleanupError",
() => ({
reason: "lifecycle" as const,
suggestion: "Run supabase experimental stack status to inspect the stack state.",
}),
),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 MINOR · error-handling · source: claude

Restart classifies StackRuntimeError and StackCleanupError as user-actionable lifecycle/configuration failures, while start classifies the same errors from stack.start() as unknown failures with debug guidance, producing inconsistent telemetry and remediation.

Evidence: restart.handler.ts:49-61 maps both tags to lifecycle; restart.errors.ts:25-34 maps lifecycle to actionability.invalidConfig. start.handler.ts:193-200 maps them to unknown, and packages/stack/src/public/Errors.ts:276-297 confirms both can be returned by start().

Suggested fix: Classify these start-phase failures consistently with start.handler.ts, using unknown and phase-appropriate debug guidance, then update the restart test.

Comment on lines +101 to +132
const target = yield* stackId !== undefined
? Effect.gen(function* () {
const id = stackId;
if (!isStackId(id))
return yield* new LegacyExperimentalStackRestartError({
reason: "flags",
message: "--stack-id must be a lowercase SHA-256 stack id",
});
const inspection = yield* catchStackError(api.inspectStack(id));
return { id, projectRoot: inspection.descriptor.projectRoot };
})
: Effect.gen(function* () {
const found = yield* catchStackError(
api.findStack({
projectRoot: settings.workdir,
...(stackName === undefined ? {} : { name: stackName }),
}),
);
if (Option.isNone(found))
return yield* new LegacyExperimentalStackRestartError({
reason: "not-found",
message:
stackName === undefined
? "No managed stack exists for the selected project."
: `No managed stack named "${stackName}" was found for this project.`,
suggestion:
stackName === undefined
? "Run supabase experimental stack start first."
: "Choose an existing --stack name or omit --stack for the current project.",
});
return { id: found.value.id, projectRoot: found.value.projectRoot };
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 MINOR · duplication · source: claude

Restart duplicates stack target validation and resolution logic already implemented across sibling handlers instead of sharing it, contrary to the trusted Hoist Before You Duplicate convention.

Evidence: restart.handler.ts:15-23 repeats the mutual-exclusion validation found in start, stop, prepare, logs, and status. restart.handler.ts:101-132 closely repeats status.handler.ts:133-160. trusted/apps/cli/CLAUDE.md:233-250 requires overlapping same-family handler logic to be hoisted.

Suggested fix: Hoist common target validation/resolution into stack.shared.ts and refactor existing callers, or reuse LegacyExperimentalStackTargetResolver for its applicable validation and inspection behavior while sharing the existing-stack lookup separately.

Comment on lines +149 to +153
yield* catchStackError(stack.stop()).pipe(Effect.tapError((error) => task.fail(error.message)));
const status = yield* catchStackError(stack.start({ config })).pipe(
Effect.tapError((error) => task.fail(error.message)),
Effect.tap(() => task.clear()),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 MINOR · user-experience · source: claude

If stop succeeds and start fails, the stack remains stopped, but the surfaced error does not disclose that state change or provide explicit recovery guidance.

Evidence: restart.handler.ts:149-153 performs stop before start and applies the same phase-agnostic mapper to start failures. SIDE_EFFECTS.md:61-66 documents that a start failure leaves the stack stopped, and restart.integration.test.ts:242-257 verifies that resulting lifecycle.

Suggested fix: Map start-phase failures separately and append guidance explaining that the stack is stopped and can be recovered after fixing the reported problem.

Comment on lines +144 to +153
const task = yield* output.task("Preparing local Supabase stack...");
yield* catchStackError(stack.prepare({ config })).pipe(
Effect.tapError((error) => task.fail(error.message)),
);
yield* task.message("Restarting local Supabase stack...");
yield* catchStackError(stack.stop()).pipe(Effect.tapError((error) => task.fail(error.message)));
const status = yield* catchStackError(stack.start({ config })).pipe(
Effect.tapError((error) => task.fail(error.message)),
Effect.tap(() => task.clear()),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚪ NIT · user-experience · source: claude

Interrupting restart during prepare, stop, or start leaves its progress task unsettled because only ordinary success and typed-error paths finalize it.

Evidence: restart.handler.ts:144-153 only calls task.fail through Effect.tapError and task.clear through Effect.tap. output.service.ts:7-14 exposes cancel and clear, while shared/cli/run.ts:831-849 interrupts the command fiber on a signal; no interruption finalizer settles this task.

Suggested fix: Attach Effect.onInterrupt to cancel or clear the task around the lifecycle sequence, and consider applying the same cleanup to sibling stack handlers.

Comment on lines +25 to +34
get [ErrorActionabilityId](): CliErrorActionabilityDeclaration {
if (this.reason === "flags" || this.reason === "not-found") return actionability.provideFlags;
if (this.reason === "invalid-config" || this.reason === "lifecycle")
return actionability.invalidConfig;
if (this.reason === "port") return actionability.invalidConfig;
if (this.reason === "docker") return actionability.dockerNotRunning;
if (this.reason === "registry" || this.reason === "artifact")
return actionability.externalNetwork;
return actionability.unknown;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚪ NIT · maintainability · source: claude

The actionability getter uses a non-exhaustive if chain with a redundant port branch, so a future reason can silently fall through to unknown.

Evidence: restart.errors.ts:25-34 returns invalidConfig separately for port and for invalid-config/lifecycle, then defaults to unknown. start.errors.ts:33-50 uses an exhaustive switch over its closed reason union.

Suggested fix: Use an exhaustive switch and group reasons that share the same actionability.

type StackRuntimePreference,
} from "@supabase/stack/effect";
import type { StackId } from "@supabase/stack";
import type { StackStatus } from "@supabase/stack/effect";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚪ NIT · style · source: claude

StackStatus is imported through a separate third import declaration from @supabase/stack/effect instead of being included in the existing import block.

Evidence: stack.shared.ts:2-10 imports values and StackRuntimePreference from @supabase/stack/effect, line 12 separately imports StackStatus, and line 13 separately imports StackNotFoundError from the same module.

Suggested fix: Combine the imports from @supabase/stack/effect into one declaration.

Comment on lines +17 to +25
export const legacyExperimentalStackRestartCommand = Command.make("restart", config).pipe(
Command.withDescription("Restart an existing managed local Supabase stack."),
Command.withShortDescription("Restart a managed local stack"),
Command.withHandler((flags) =>
legacyExperimentalStackRestart(flags).pipe(
withLegacyCommandInstrumentation({ flags, config }),
withJsonErrorHandling,
),
),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚪ NIT · documentation · source: claude

The new restart command supplies no Command.withExamples metadata, so its generated help and reference material lack examples for current, named, or ID-based target selection.

Evidence: restart.command.ts:17-25 includes descriptions and a handler but no examples. start.command.ts:32-41, stop.command.ts:20-25, prepare.command.ts:29-38, and logs.command.ts:44-53 provide examples; trusted/docs/adr/0003-self-documenting-cli.md identifies command examples as generated reference metadata.

Suggested fix: Add examples for restarting the current project stack and a named or ID-selected stack using the full experimental command path.

Comment on lines +305 to +317
it.effect("fails a missing named target without lifecycle calls", () => {
const fixture = makeFixture({ target: "name", missingTarget: true });
return fixture.effect.pipe(
Effect.flip,
Effect.tap((error) =>
Effect.sync(() => {
expect(error.message).toContain('No managed stack named "feature-a"');
expect(error.suggestion).toContain("existing --stack name");
expect(fixture.calls).toEqual([]);
}),
),
);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚪ NIT · test-coverage · source: claude

The tests omit the unnamed missing-target branch, inspectStack failure handling for --stack-id, and verification of the projectRoot/name query passed to findStack.

Evidence: restart.integration.test.ts:305-317 tests only a missing named target. The fixture at lines 131-141 makes inspectStack always succeed and makes findStack ignore its argument, leaving restart.handler.ts:109 and the distinct unnamed branch at lines 120-129 unverified.

Suggested fix: Add unnamed missing-target and failing inspectStack cases, and capture the findStack input to assert projectRoot and name.

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