Skip to content

feat(cli): auto-update RDE dashboard via npx re-exec on stack dev#1521

Open
BilalG1 wants to merge 66 commits into
devfrom
rde-cli-auto-update
Open

feat(cli): auto-update RDE dashboard via npx re-exec on stack dev#1521
BilalG1 wants to merge 66 commits into
devfrom
rde-cli-auto-update

Conversation

@BilalG1
Copy link
Copy Markdown
Collaborator

@BilalG1 BilalG1 commented May 29, 2026

What & why

Re-running stack dev / hexclave dev now picks up the latest published dashboard without reinstalling the CLI.

In the RDE, the dashboard is a Next.js standalone build bundled into the @hexclave/cli npm tarball — so a dashboard change only reaches a user when they get a newer CLI version. This PR closes that gap for the recommended stack dev flow.

How it works

  1. npx self-re-exec — at the top of the dev action, the CLI checks npm for a newer @hexclave/cli. If found, it re-execs npx --yes -p @hexclave/cli@<latest> stack dev <your args> (with a loop guard) and exits with the child's code. The running code — and the dashboard bundled in that tarball — is now the latest; the user's installed devDependency is untouched. npx caches per version, so steady-state runs are fast.

  2. Dashboard version handshake (the necessary second half) — stack dev keeps a detached background dashboard alive across runs and reuses it by default, which would otherwise silently defeat the update. The now-latest process compares the running dashboard's version (persisted in dev-env state) against its own and kills + restarts the stale one (SIGTERM → wait → SIGKILL) so the new dashboard actually binds :26700. Equal/older/unknown versions are reused exactly as before.

Safety / opt-outs

  • Skipped for the re-exec'd child (STACK_CLI_SKIP_AUTO_UPDATE, loop guard), when the user opts out (STACK_CLI_NO_AUTO_UPDATE / --no-auto-update), and in CI (CI).
  • Registry lookup is TTL-cached in dev-env state with a short timeout and is offline-safe — any failure (no network, no npx) falls through to the installed CLI.
  • isVersionNewer never downgrades and returns false for unparseable versions.

Changes

  • packages/stack-cli/src/lib/self-update.ts (new) — maybeReexecToLatest(), resolveLatestVersion(), isVersionNewer(), buildNpxInvocation().
  • packages/stack-cli/src/commands/dev.ts — re-exec wiring, killLocalDashboard(), version handshake, --no-auto-update flag, version stamp on the recorded dashboard process.
  • packages/stack-cli/src/lib/dev-env-state.tslocalDashboard.version + cliUpdateCheck cache helpers.
  • Tests: new self-update.test.ts + additions to dev-env-state.test.ts.

Verification

  • pnpm --filter @hexclave/cli run lint
  • pnpm --filter @hexclave/cli run typecheck
  • pnpm --filter @hexclave/cli run test ✅ (132 passed)

Prerequisite

Relies on @hexclave/cli being published to npm with the latest dist-tag tracking releases — otherwise the check is a no-op (which is safe).


Summary by cubic

stack dev now re-execs via npx to use the latest published @hexclave/cli, keeping the bundled RDE dashboard up to date without reinstalling. It also restarts a stale background dashboard so the new version binds port 26700.

  • New Features

    • Auto-update: checks npm for a newer @hexclave/cli and re-runs npx --yes --min-release-age=0 -p <pkg>@<latest> stack dev .... Cached lookup, short timeout, offline-safe. Skips in CI and when --no-auto-update or STACK_CLI_NO_AUTO_UPDATE=1 is set.
    • Dashboard version handshake: records the CLI version that started the dashboard and restarts it if the running one is older; otherwise reuses it. Graceful SIGTERM with a timeout, then SIGKILL if needed.
  • Bug Fixes

    • Cooldown handling: npx re-exec now passes npm’s real --min-release-age=0 so just-published versions install reliably, avoiding ETARGET.
    • Safer dashboard restarts: after SIGTERM, wait for the port to free instead of gating on the pid to avoid recycled-pid false positives; bail early on ESRCH/EPERM; only SIGKILL when the port is still held.
    • Robust cache: validate cliUpdateCheck shape on read; drop malformed entries to avoid version-parse errors.
    • Fail-open: wrap the auto-update path so corrupt/unreadable state files don’t crash stack dev; continue with the installed CLI.

Written for commit d3f0581. Summary will update on new commits.

Review in cubic

BilalG1 and others added 30 commits May 23, 2026 17:35
Rebased onto dev after PR 1475 (cl/hexclave-pr1) was squash-merged.
Squashes the original 46-commit branch (including PR1-duplicate commits
that arrived via cherry-picks/merges) into a single commit containing
only PR2's net delta over dev.

Original PR 1481 head: 94872de
(kept locally at backup/cl-romantic-mendel-5a2c25-pre-rebase)
Source rename across the monorepo. Every publishable package now ships
under its @hexclave/* name natively, no rewrite-at-publish indirection.

Workflow + tooling:
- Delete scripts/rewrite-packages-to-hexclave.ts (one-shot mirror).
- Remove the mirror-publish block from .github/workflows/npm-publish.yaml.
  The remaining `pnpm publish -r` step publishes @hexclave/* natively.
- Flip the auto-bump changeset target from @stackframe/stack to
  @hexclave/next so 'Update package versions on dev' keeps working.
- Delete packages/template/src/internal/deprecation-warning.ts and its
  imports — @hexclave/* never warns about itself, and after PR 3 no
  @stackframe/* artifact is ever built from source again.

Package renames (publishable):
  @stackframe/react              → @hexclave/react
  @stackframe/stack              → @hexclave/next
  @stackframe/js                 → @hexclave/js
  @stackframe/stack-shared       → @hexclave/shared
  @stackframe/stack-ui           → @hexclave/ui
  @stackframe/stack-sc           → @hexclave/sc
  @stackframe/stack-cli          → @hexclave/cli
  @stackframe/tanstack-start     → @hexclave/tanstack-start
  @stackframe/dashboard-ui-components → @hexclave/dashboard-ui-components

Internal monorepo packages (private, never published) also renamed for
brand consistency: backend, dashboard, docs, mcp, skills, e2e-tests,
example apps, the swift-sdk, the monorepo root, etc. Cost is mechanical;
payoff is no stray @stackframe/* names left under apps/, examples/, sdks/.

Carve-outs intentionally kept under their legacy names:
- @stackframe/emails — virtual module imported by customer-stored email
  templates; the renderer in apps/backend/src/lib/email-rendering.tsx
  dual-aliases both names to the same backing module indefinitely.
- @stackframe/template — internal codegen source, never published; per
  docs-mintlify/migration.mdx 'internal packages keep names'.
- @stackframe/init-stack — deprecated; now marked private: true so the
  last published version on npm continues to serve old install commands
  but the workspace stops publishing it.

Backward-compat detection (so projects still on the last @stackframe/*
release keep working):
- packages/stack-shared/src/config-rendering.ts — CONFIG_IMPORT_PACKAGES
  table includes both @hexclave/* (canonical, first match wins) and
  legacy @stackframe/* names. Function renamed
  detectStackframeImportPackage → detectConfigImportPackage.
- apps/dashboard/src/lib/github-config-push.ts — import detection regex
  now matches both @hexclave/<name> and @stackframe/<name>, hexclave
  preferred.

Versions: every renamed package reset to 1.0.0 in source. The repo's
existing 'bump versions before merging to main' flow will move them to
1.0.1 on the first publish run, so the dual-publish 1.0.0 from PR 2 is
not overwritten.

Other touch-ups discovered during sweep:
- Root package.json: 'fern' script filter was @stackframe/docs (legacy
  typo, never resolved) → @hexclave/docs.
- README.md contributor note: @stackframe/XYZ → @hexclave/XYZ.
- packages/stack-cli/package.json: register `hexclave` bin alongside
  the legacy `stack` bin so `npx @hexclave/cli init` works on the
  natively-published artifact (PR 1481's rewrite script did this at
  publish time; now it's in source).
- packages/template/package-template.json: per-platform names + version
  flipped to hexclave + 1.0.0 to stay in sync with generated package.json.
- docs/package.json (legacy fumadocs folder, otherwise carved out of the
  brand sweep): workspace deps and name updated minimally so `pnpm
  install` resolves — content (MDX) intentionally untouched per the
  PR 2 scoping decision.

Carve-out files (skipped entirely by the sweep, intentional history):
- docs-mintlify/migration.mdx — teaches the rename, references both.
- RENAME-TO-HEXCLAVE.md — planning doc, references both indefinitely.
- legacy docs/ folder — content untouched per PR 2 carve-out.

generate-sdks regenerated packages/{react,stack,js} from template.
pnpm-lock.yaml regenerated. Typecheck green on stack-shared, stack, js,
react. Dashboard typecheck has pre-existing 'X is of type unknown'
errors that need to be investigated separately (likely a local
node_modules build state issue, not source).
Six fixes from the four parallel reviewers on the rename PR:

1. **Backend .well-known/ routes** — the sweep's directory walker had a
   bug that skipped every dot-prefixed dir (intended to exclude .git /
   .turbo / etc.), which also caught Next.js .well-known/ route folders.
   Two route handlers under apps/backend/.well-known/ still imported
   @stackframe/stack-shared/dist/* — flipped to @hexclave/shared/dist/*.

2. **Legacy docs/ folder excluded from workspace** — docs/ is the legacy
   fumadocs site, no longer maintained (replaced by docs-mintlify/).
   Per user direction, kept on disk for migration reference but dropped
   from pnpm-workspace.yaml so it no longer gates install / typecheck /
   lint. This is the right call given the typecheck failures in
   docs/src/ from the sweep carve-out were never going to be fixed.

3. **Root package.json scripts** — removed every `--filter=@hexclave/docs`
   reference now that docs/ isn't in the workspace: build:docs (rerouted
   to @hexclave/docs-mintlify), dev / dev:tui / dev:docs (dropped the
   filter), and the dead 'fern' script (was @hexclave/docs-only).

4. **build:demo filter** — fixed pre-existing bug where the script
   filtered package name 'demo-app', but the package is
   '@hexclave/example-demo-app'. Never resolved before, fixed now.

5. **github-config-push.test.ts legacy fallback** — the sweep flipped the
   test 'preserves the existing @stackframe/* import package…' from
   @stackframe/react to @hexclave/react, which made it a duplicate of
   the test above it and eliminated all coverage of the legacy regex
   branch in detectImportPackage. Renamed the modified test to reflect
   what it now tests, and added a new parallel test that feeds an
   @stackframe/react import and asserts the legacy import is preserved
   on output. Both branches of the dual-name regex are now covered.

6. **examples/react-example version** — the only package the sweep
   missed for the 1.0.0 version reset (unscoped name 'react-example'
   wasn't in the rename map). Bumped 2.8.103 → 1.0.0 for consistency.

Verification on a clean install:
- `pnpm install --frozen-lockfile` — clean (only pre-existing
  @vercel/mcp-adapter bin warnings).
- `pnpm typecheck` — 28/28 tasks green across the whole workspace.
- `pnpm lint` — 28/28 tasks green.

Reviewers flagged but I did NOT change (out of scope or non-actionable):
- npm-publish.yaml GH Environment name still says 'hexclave/stack-auth'
  — env names are managed in repo settings, not in YAML; cosmetic.
- RENAME-TO-HEXCLAVE.md references the deleted rewrite script — it's
  a planning doc / historical record, leaving as-is.
- code-examples and migration.mdx user-facing references to
  @stackframe/* — these are documentation that teaches the rename, by
  design they mention both names.
Three small fixes from the parallel reviewers on PR 3:

1. **tanstack-start-demo vite SSR regex** — examples/tanstack-start-demo/
   vite.config.ts:76 had `noExternal: [/^@StackFrame\//, ...]`. The
   regex was missed by the source rename sweep because it's a regex
   pattern, not a string literal. After the rename no @stackframe/*
   package exists in the workspace, so the regex matched nothing and
   workspace deps (`@hexclave/tanstack-start`, `@hexclave/shared`,
   `@hexclave/ui`) stopped being inlined for SSR. Without inlining,
   the Nitro server hits ERR_REQUIRE_ESM on first request because the
   CJS bundles import ESM-only transitive deps (jose, oauth4webapi).
   Flipped to /^@hexclave\// to match the renamed packages.

2. **docs.json footer GitHub link** — pointed at hexclave/stack (404,
   no such repo). The navbar at line 34 already uses hexclave/hexclave
   per the plan, so aligned the footer to match.

3. **README cleanups**:
   - Alt text 'Stack Logo' → 'Hexclave Logo' on the header image.
   - Removed broken /docs/next prefix on the setup-guide link (the
     actual docs structure is /getting-started/setup, no /docs/next).
   - contrib.rocks image now points at hexclave/hexclave (was the old
     stack-auth/stack URL).

What I did NOT touch (out of PR 3 scope, surfaced separately for a
follow-up doc-pedagogy PR):

- 14+ docs-mintlify pages teaching STACK_* env vars instead of
  HEXCLAVE_* (works via dual-read, but contradicts migration.mdx's
  'new code should use HEXCLAVE_*' recommendation).
- REST API code samples teaching X-Stack-* headers instead of
  X-Hexclave-* (works via dual-accept proxy).
- docs-mintlify/sdk/objects/stack-app.mdx broken in-page anchors
  (#stackclientapp etc. — body headings renamed in PR 2 but
  anchor IDs weren't updated).
- MCP server name inconsistency (`stack-auth` in init-prompt.ts vs
  `hexclave` in dashboard setup-page).
- The `ask_stack_auth` MCP tool was removed despite
  RENAME-TO-HEXCLAVE.md saying it should stay registered as a
  compat alias indefinitely.
- AI prompts (apps/backend/src/lib/ai/prompts.ts, apps/skills/) still
  teach legacy header / env-var names in generated code examples.

All of those are PR 2 pedagogy carry-over, not introduced by this PR,
and work functionally via the dual-read/dual-accept compat layers PR
1 + PR 2 put in place. They're documentation polish, not bugs that
block PR 3 shipping.
apps/e2e/tests/js/auth-like.test.ts:68 asserts clientApp.version matches
`^js @stackframe/js@…`, but the build-time sentinel is stamped from
packages/js/package.json's `name` field (configs/tsdown/plugins.ts:10),
which PR 3 renamed @stackframe/js → @hexclave/js. Post-rename the
sentinel is `js @hexclave/js@1.0.0` (or 1.0.1 after auto-bump), so the
old regex fails.

Caught by a follow-up reviewer pass — the published-artifact sentinel
is the most direct observable that's affected by a source-name rename,
and this is the one e2e test that asserts on it directly.
…ertionError suffix

The HexclaveAssertionError disclaimer was simplified from
"...error in Hexclave (formerly Stack Auth)." to "...error in Hexclave."
but the inline snapshots in url-targets and redirect-urls tests still
expected the longer text. Updates the template source-of-truth; SDK
mirrors regenerate via the preinstall generate-sdks hook.
# Conflicts:
#	apps/backend/package.json
#	apps/dashboard/package.json
#	apps/dev-launchpad/package.json
#	apps/e2e/package.json
#	apps/mcp/package.json
#	apps/mock-oauth-server/package.json
#	apps/skills/package.json
#	docs-mintlify/index.mdx
#	examples/cjs-test/package.json
#	examples/convex/package.json
#	examples/demo/package.json
#	examples/docs-examples/package.json
#	examples/e-commerce/package.json
#	examples/js-example/package.json
#	examples/lovable-react-18-example/package.json
#	examples/middleware/package.json
#	examples/react-example/package.json
#	examples/supabase/package.json
#	examples/tanstack-start-demo/package.json
#	packages/dashboard-ui-components/package.json
#	packages/init-stack/package.json
#	packages/js/package.json
#	packages/react/package.json
#	packages/stack-cli/package.json
#	packages/stack-sc/package.json
#	packages/stack-shared/package.json
#	packages/stack-ui/package.json
#	packages/stack/package.json
#	packages/tanstack-start/package.json
#	packages/template/package-template.json
#	packages/template/package.json
#	skills/stack-auth/SKILL.md
# Conflicts:
#	apps/backend/package.json
#	apps/dashboard/package.json
#	apps/dev-launchpad/package.json
#	apps/e2e/package.json
#	apps/hosted-components/package.json
#	apps/internal-tool/package.json
#	apps/mcp/package.json
#	apps/mock-oauth-server/package.json
#	apps/skills/package.json
#	docs-mintlify/package.json
#	docs/package.json
#	examples/cjs-test/package.json
#	examples/convex/package.json
#	examples/demo/package.json
#	examples/docs-examples/package.json
#	examples/e-commerce/package.json
#	examples/js-example/package.json
#	examples/lovable-react-18-example/package.json
#	examples/middleware/package.json
#	examples/react-example/package.json
#	examples/supabase/package.json
#	examples/tanstack-start-demo/package.json
#	packages/dashboard-ui-components/package.json
#	packages/init-stack/package.json
#	packages/js/package.json
#	packages/react/package.json
#	packages/stack-cli/package.json
#	packages/stack-sc/package.json
#	packages/stack-shared/package.json
#	packages/stack-ui/package.json
#	packages/stack/package.json
#	packages/tanstack-start/package.json
#	packages/template/package-template.json
#	packages/template/package.json
#	sdks/implementations/swift/package.json
#	sdks/spec/package.json
Filter `pnpm publish -r` to only the rewritten @hexclave/* packages in
the mirror step, removing the reliance on pnpm's skip-existing-versions
behavior for the unchanged @stackframe/* packages still in the workspace
at that point.

Addresses greptile P1 finding on PR #1481.
# Conflicts:
#	.github/workflows/npm-publish.yaml
…sertionError suffix

Companion to ff44d4e — that commit shortened the disclaimer from
'...error in Hexclave (formerly Stack Auth).' to '...error in Hexclave.'
and updated the url-targets test snapshots, but missed the matching
inline snapshot in apps/backend/src/lib/redirect-urls.test.tsx. The test
suite passes after the update (33/33).
The rebrand-execution plan is now obsolete: PRs 1, 2, and 3 have shipped
the work it described. The doc was a one-shot working document and never
intended as long-lived reference material.

Note: 9 source-code comments still reference this file (idp.ts:172,
tokens.tsx:66, proxy.tsx in backend + dashboard, template/src/index.ts,
template/src/lib/stack-app/index.ts). They are intentionally left in
place as historical breadcrumbs; they don't affect runtime behavior and
can be scrubbed in a follow-up cleanup if desired.
9 source comments pointed at the planning doc removed in the prior commit.
Drop the 'See RENAME-TO-HEXCLAVE.md' sentence/parenthetical from each;
keep the surrounding substantive context intact (CORS allowlist derivation,
JWT issuer dual-acceptance rationale, OIDC opaque-identifier carve-out
explanation, Hexclave/Stack alias rationale).
The next.layout-provider check's import regex only matched the legacy
@stackframe/stack scope. After the PR-3 source rename, fixtures (and
real projects) import StackProvider from @hexclave/next, so the regex
returned false, the layout check failed, and the doctor exited 1.

Widen the regex to accept both scopes, mirroring the dual-scope detection
in packages/stack-shared/src/config-rendering.ts and apps/dashboard/src/lib/github-config-push.ts.

Fixes 9 Stack CLI - Doctor failures in apps/e2e/tests/general/cli.test.ts.
# Conflicts:
#	docs-mintlify/guides/getting-started/ai-integration.mdx
#	docs-mintlify/guides/going-further/local-development.mdx
#	docs-mintlify/guides/going-further/local-emulator.mdx
#	docs-mintlify/guides/other/showcase.mdx
# Conflicts:
#	apps/skills/src/app/route.ts
#	packages/stack-shared/src/ai/unified-prompts/skill-site-prompt-parts/ai-setup-prompt.ts
#	packages/stack-shared/src/interface/page-component-versions.ts
# Conflicts:
#	docs-mintlify/guides/getting-started/setup.mdx
#	docs-mintlify/snippets/home-prompt-island.jsx
The bare-name sweep was mutating the sentinel string
('js @stackframe/js@2.8.105') into 'js @hexclave/js@2.8.105' before
the sentinel-specific regex (built from oldName) had a chance to
match, silently leaving the version stuck at the old @StackFrame
version on published @hexclave/* artifacts. Reorder so the sentinel
rewrite runs first; the name sweep that follows can't touch the
rewritten sentinel because it no longer contains any @stackframe/*
substrings.
# Conflicts:
#	package.json
#	packages/stack-shared/src/interface/page-component-versions.ts
…cl/hexclave-pr3

# Conflicts:
#	scripts/rewrite-packages-to-hexclave.ts
Resolve conflicts in init-prompt.ts (take dev's unified-prompt structure,
keep Hexclave branding), and rebrand the auto-merged ai-setup-prompt.ts
sweep (CLI Python template names, MCP server name/URL/tool, StackConfig
type, x-hexclave-* header teach). Regenerated docs-mintlify setup files.
The legacy docs/ folder still referenced noreply@stackframe.co for the
shared email provider — flip to match the new sender domain set up
on Resend as the dedicated transactional-sender domain. Aligns with
the dashboard + docs-mintlify references that were already flipped.
The previous commit passed only `--minimum-release-age=0`, but npm's actual
config key is `min-release-age` (days, npm >=11.10.0) — CLI flag
`--min-release-age`. Pass that, plus the camelCase `--minimum-release-age=0`
defensively (the spelling pnpm/bun use, in case npx is shimmed). npm ignores
unrecognized config flags, so the extra one is harmless. Without the correct
flag, npx of a version newer than a user's global cooldown window fails with
ETARGET and would kill `stack dev`.
We only ever invoke `npx` (the npm binary), whose cooldown config is
`min-release-age`. The camelCase `--minimum-release-age` is pnpm/bun's
spelling, and those aren't reached via npx (they use `pnpm dlx` / `bunx`),
so the extra flag was dead weight npm silently ignored. Keep only
`--min-release-age=0`.
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 29, 2026

Greptile Summary

This PR adds an auto-update mechanism to stack dev that re-execs via npx @hexclave/cli@<latest> when a newer version is published, and adds a dashboard version handshake that kills and restarts a stale background dashboard process so users always get the latest bundled dashboard without reinstalling the CLI.

  • self-update.ts (new): registry lookup with TTL caching, isVersionNewer comparison, decideReexec I/O-free decision layer, and maybeReexecToLatest entry point — all with thorough unit test coverage.
  • dev.ts: wires in the re-exec call, adds killLocalDashboard (SIGTERM → wait → SIGKILL) and a version handshake in startDashboardIfNeeded to restart a stale background dashboard before launching a fresh one.
  • Shared helpers (own-package.ts, child-process.ts): extract repeated package.json reading and signal-forwarding boilerplate, consumed by both dev.ts and emulator.ts.

Confidence Score: 3/5

The auto-update and registry-cache paths are safe and well-tested, but killLocalDashboard can send SIGKILL to an unrelated process if the original dashboard's pid is recycled to a user-owned process before the 10-second timeout expires.

After SIGTERM succeeds, a recycled pid owned by the same user keeps processExists returning true for the full polling window, causing SIGKILL to be sent unconditionally to whatever process now holds that pid.

packages/stack-cli/src/commands/dev.ts — specifically the killLocalDashboard wait loop and SIGKILL escalation logic

Important Files Changed

Filename Overview
packages/stack-cli/src/commands/dev.ts Adds version handshake to restart stale background dashboard; killLocalDashboard has a correctness hole where a recycled pid owned by the same user can receive SIGKILL unintentionally.
packages/stack-cli/src/lib/self-update.ts New module implementing npx re-exec logic with version comparison, registry caching, and loop guard; well-structured with a clear I/O-free decision layer and comprehensive tests.
packages/stack-cli/src/lib/dev-env-state.ts Adds localDashboard.version and cliUpdateCheck to the persisted state; cliUpdateCheck is passed through without a structural type guard, though the downstream behaviour degrades gracefully.
packages/stack-cli/src/lib/own-package.ts Clean extraction of package.json reading into a tested, I/O-isolated helper; resolveBinName correctly prefers the stack alias for cross-version compatibility.
packages/stack-cli/src/lib/child-process.ts New shared forwardSignals helper deduplicates identical signal-forwarding boilerplate from both dev.ts and emulator.ts; correct and safe.

Sequence Diagram

sequenceDiagram
    participant User
    participant CLI as stack dev (installed)
    participant Registry as npm registry
    participant npx as npx @latest
    participant Dashboard as bg dashboard

    User->>CLI: stack dev --config-file
    CLI->>Registry: GET /latest (TTL-cached, 1.5s timeout)
    Registry-->>CLI: version 2.8.110
    CLI->>CLI: isVersionNewer check
    CLI->>npx: "spawn npx -p @hexclave/cli@2.8.110 stack dev"
    npx-->>CLI: exits with child code
    npx->>Dashboard: isDashboardReachable port 26700?
    alt stale dashboard running
        npx->>Dashboard: shouldRestartDashboard check
        npx->>Dashboard: SIGTERM then wait then SIGKILL
        npx->>Dashboard: spawn new dashboard detached
    else up-to-date or no dashboard
        npx->>Dashboard: reuse or start normally
    end
    Dashboard-->>User: http://127.0.0.1:26700
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
packages/stack-cli/src/commands/dev.ts:286-301
**SIGKILL can reach a recycled pid owned by the same user**

After a successful `SIGTERM`, the wait loop calls `processExists(pid)`, which probes with `kill(pid, 0)`. If the dashboard exits quickly and the pid is immediately reused by a process also owned by the current user, `kill(pid, 0)` succeeds (no exception), `processExists` returns `true`, the loop spins for the full 10-second timeout, and the subsequent `process.kill(pid, "SIGKILL")` kills an unrelated process. The EPERM short-circuit in the SIGTERM block only defends against recycling onto *another user's* process; it has no effect once SIGTERM is already sent and we're in the polling loop.

The port availability check is the property that actually matters for letting the new dashboard bind — gate the early-return on that alone, or additionally accept `!processExists(pid)` only when the result also matches the EPERM/ESRCH path.

### Issue 2 of 2
packages/stack-cli/src/lib/dev-env-state.ts:59
**`cliUpdateCheck` is passed through without type validation**

`parsed.cliUpdateCheck` is assigned as-is without verifying that it has the expected shape (`{ packageName: string, latestVersion: string, checkedAtMillis: number }`). If the state file is hand-edited or written by a different version, `readCliUpdateCheckCache` returns a structurally wrong object and `resolveLatestVersion` could see `NaN` from `now - cache.checkedAtMillis` or skip the cache packageName check unexpectedly. Adding a type guard matching `CliUpdateCheckCache` would keep the existing safe-fallback behaviour explicit rather than accidental.

Reviews (1): Last reviewed commit: "fix(cli): drop redundant --minimum-relea..." | Re-trigger Greptile

Comment thread packages/stack-cli/src/commands/dev.ts
Comment thread packages/stack-cli/src/lib/dev-env-state.ts
Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

1 issue found across 12 files

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

Fix all with cubic | Re-trigger cubic

Comment thread packages/stack-cli/src/lib/self-update.ts Outdated
Three unresolved review findings on the auto-update feature:

- killLocalDashboard (greptile P1): after SIGTERM, the wait loop gated on
  processExists(pid), which false-positives if the dashboard exits and its pid
  is recycled onto another same-user process — spinning the full timeout and
  then SIGKILLing the unrelated process. Gate purely on the port being freed
  (the property that actually lets the replacement bind). SIGKILL is now only
  reached when the port is still occupied, i.e. the process is still up and the
  pid is necessarily valid.
- dev-env-state (greptile P2): validate the on-disk cliUpdateCheck cache shape
  on read; a non-string latestVersion would otherwise reach version parsing and
  throw. Malformed entries are treated as "no cache".
- maybeReexecToLatest (cubic P2): documented "fail open" but readDevEnvState
  could throw (corrupt/bad-permission state file) and crash `stack dev`. Wrap
  the body in try/catch so any unexpected error falls through to the installed
  CLI.

Tests: +4 — recycled-pid early-return without SIGKILL, malformed/well-formed
cache read, and fail-open on a corrupt state file. 170 pass.
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.

2 participants