Skip to content

feat: scaffold a Next.js app when installing in an empty directory#173

Merged
nicknisi merged 9 commits into
mainfrom
nicknisi/scaffold-app
Jun 8, 2026
Merged

feat: scaffold a Next.js app when installing in an empty directory#173
nicknisi merged 9 commits into
mainfrom
nicknisi/scaffold-app

Conversation

@nicknisi

@nicknisi nicknisi commented Jun 7, 2026

Copy link
Copy Markdown
Member

What & why

Running workos install in an empty directory was a dead end: detection found no framework and the state machine errored at the hasIntegration guard. This adds a delegate-don't-template scaffold path — on an empty dir the CLI scaffolds a blank Next.js app with create-next-app, then the existing install pipeline detects and wires AuthKit, unchanged.

How

  • New compound scaffold XState state before preparing (checking → prompting → running → done), mirroring the existing gitCheck/branchCheck sub-machines. Both transitions that fed preparing now route through scaffold first.
  • New src/lib/scaffold module: isScaffoldableEmptyDir, resolvePackageManager, buildCreateNextAppArgs, runCreateNextApp. Pinned create-next-app@16 (major float), --yes defaults, spawned via child_process. The agent Bash allowlist is untouched — the scaffold never goes through the LLM.
  • scaffold:* event family + wiring across all three adapters: CLI confirm (default yes) + a "next steps" hint, headless NDJSON with scaffolded: true, dashboard auto-proceed.
  • --scaffold and --pm flags; package manager resolved from npm_config_user_agent (npm fallback).
  • scaffolded recorded as a session telemetry tag.

Empty-dir gate

Scaffolds only when every entry in the dir is in SAFE_EMPTY_FILES (a verified subset of create-next-app v16's validFiles): empty, or only VCS/editor/cruft metadata (.git, .gitignore, LICENSE, .idea, …). Any project file — including README.md or package.json — opts the directory out and it's treated as an existing project. Interactive = single confirm; headless / --scaffold = auto.

Note: this excludes README.md/.vscode from the original spec's success criterion, deliberately, because they aren't in create-next-app's validFiles — including them would trip the "offer then create-next-app refuses" failure mode.

Also in this PR

  • fix: the empty-dir error is now actionable (names the directory, points to the empty-dir-scaffold vs existing-project paths) instead of the bare "Could not detect framework integration".
  • refactor: removed the inert --integration flag. It was read in only two places and was unconditionally overwritten by detection before it could affect anything — vestigial surface that predated the XState detection step. Detection still selects the integration; removal is non-breaking (the install command is non-strict, so a leftover --integration is ignored). True multi-framework scaffolding is the future home for a flag like this.
  • docs: README Installer Options + a Features bullet + an empty-dir behavior note + a greenfield example.

Known follow-ups (not in this PR)

  • Greenfield-with-README.md doesn't scaffold (by the gate above). If that's an important entry point, it needs a relocate-then-scaffold follow-up.
  • True multi-framework scaffolding (e.g. a React/Vite app via its own create-* tool), at which point a framework-selection flag becomes meaningful again.
  • Manual e2e still owed (real create-next-app run, pnpm dlx PM resolution, headless scaffolded: true).

Validation

pnpm typecheck ✓ · pnpm lint ✓ (0/0) · pnpm test ✓ (2079) · pnpm build ✓.

nicknisi added 3 commits June 6, 2026 10:51
Running `workos install` in an empty dir was a dead end: detection found no
framework and the state machine errored at the hasIntegration guard. Now a new
compound `scaffold` state runs before `preparing`. It checks whether the dir is
scaffoldable (empty or only create-next-app-safe files), confirms once
interactively (auto in headless / --scaffold), runs a pinned create-next-app@16
deterministically via child_process, then converges into the existing
detect -> credentials -> agent pipeline that wires AuthKit unchanged.

- New src/lib/scaffold module: isScaffoldableEmptyDir, resolvePackageManager,
  buildCreateNextAppArgs, runCreateNextApp. The agent Bash allowlist is
  untouched (the scaffold spawns directly, never through the LLM).
- scaffold:* event family plus adapter wiring (CLI confirm + next-steps hint,
  headless NDJSON with scaffolded:true, dashboard auto-proceed).
- --scaffold and --pm flags; package manager resolved from
  npm_config_user_agent (npm fallback).
- scaffolded recorded as a session telemetry tag in run-with-core.

SAFE_EMPTY_FILES is a verified subset of create-next-app v16 validFiles
(README.md/.vscode excluded since upstream does not allow them). Review
cycle: 1 (PASS).
The empty-dir error was a bare "Could not detect framework integration",
which gave no hint about the empty-dir-vs-existing-project fork. Replace it
with an actionable message that names the directory and points to both paths:
run in an empty directory to scaffold, or run from a project root / pass
--install-dir for an existing project.
Add a Features bullet, the --scaffold and --pm options to the Installer
Options list, a note explaining the empty-dir scaffold behavior (and that a
README.md / package.json opts the directory out), and a greenfield example.
@greptile-apps

greptile-apps Bot commented Jun 7, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds greenfield scaffolding support: running workos install in an empty directory now invokes create-next-app@16 before handing off to the existing AuthKit wiring pipeline. It also removes the vestigial --integration flag and improves the empty-directory error message.

  • New scaffold XState compound state (checking → prompting → running → done) inserted before preparing, with checkWorkspace and runScaffold actors wired through run-with-core.ts. Guards read from the actor's output field (not context) to avoid the pre-action evaluation timing issue.
  • New src/lib/scaffold/scaffold.ts module with isScaffoldableEmptyDir, resolvePackageManager, buildCreateNextAppArgs, and runCreateNextApp; per-PM runners (npx/pnpm dlx/yarn dlx/bunx), pinned create-next-app@16, stderr capped at 2 KB, and a thorough unit-test suite.
  • All three adapters (CLI, headless, dashboard) handle the scaffold:* event family; headless mode auto-proceeds, interactive mode prompts with default yes, dashboard auto-proceeds with status messages.

Confidence Score: 5/5

Safe to merge — the scaffold path is well-gated, all three adapters handle the new event family, and the two previously flagged issues (per-PM runner and stderr cap) are already fixed.

The core scaffold module is correct and thoroughly tested. The XState compound state follows existing machine patterns precisely — guards read from event output before context assignment, the onError fallback silently passes through to the existing install path, and the headless/interactive split is cleanly separated. No functional regressions were found in the changed files.

No files require special attention. The only observations are a scaffold:skipped event with no consumers and timing-based test delays that could be fragile under CI load — neither affects production behavior.

Important Files Changed

Filename Overview
src/lib/scaffold/scaffold.ts Core scaffold module: isScaffoldableEmptyDir, resolvePackageManager, buildCreateNextAppArgs, runCreateNextApp — previously flagged issues (npx hardcoding, unbounded stderr) are fixed; implementation is clean.
src/lib/installer-core.ts New scaffold compound state inserted before preparing; guards read event.output correctly (before assignWorkspaceResult runs); onError falls through silently preserving existing behavior.
src/lib/run-with-core.ts checkWorkspace and runScaffold actors wired correctly; headlessMode closure captured at the right scope; scaffolded telemetry tag added.
src/lib/adapters/headless-adapter.ts scaffold:* events streamed as NDJSON; scaffolded flag set on complete; scaffold:prompt correctly omitted since headlessMode forces autoScaffold=true.
src/lib/adapters/cli-adapter.ts Interactive scaffold prompt with default-yes; next steps hint on success; progress gated behind --debug to keep spinner clean.
src/lib/scaffold/scaffold.spec.ts Comprehensive unit tests for all scaffold helpers including per-PM runner, stderr cap, signal kill, and spawn error paths.
src/lib/installer-core.spec.ts New scaffold flow tests cover all branches; slowPreparingActors pattern ensures parallel-state snapshot is observable — timing-dependent delays could be flaky under load.
src/lib/events.ts scaffold:* event family added cleanly; scaffold:skipped is defined but consumed by no adapter (the machine routes to cancelled state instead).

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    START([START event]) --> AUTH{shouldSkipAuth?}
    AUTH -- yes --> SCAFFOLD
    AUTH -- no --> AUTHENTICATING[authenticating]
    AUTHENTICATING --> SCAFFOLD

    subgraph SCAFFOLD [scaffold]
        S_CHECK[checking\ncheckWorkspace actor]
        S_CHECK --> S_NOTSCAFFOLDABLE{notScaffoldable?}
        S_NOTSCAFFOLDABLE -- yes --> S_DONE[done/final]
        S_NOTSCAFFOLDABLE -- no --> S_AUTO{shouldAutoScaffold?}
        S_AUTO -- yes --> S_RUNNING[running\nrunScaffold actor]
        S_AUTO -- no --> S_PROMPT[prompting\nemit scaffold:prompt]
        S_PROMPT -- SCAFFOLD_CONFIRMED --> S_RUNNING
        S_PROMPT -- SCAFFOLD_CANCELLED --> CANCELLED([cancelled])
        S_RUNNING -- success --> S_DONE
        S_RUNNING -- failure --> ERROR([error])
    end

    S_DONE --> PREPARING

    subgraph PREPARING [preparing - parallel]
        DETECT[detection]
        GITCHECK[gitCheck]
        BRANCHCHECK[branchCheck]
    end

    PREPARING --> AGENT[agent running]
    AGENT --> POSTINSTALL[postInstall]
    POSTINSTALL --> COMPLETE([complete])
Loading

Reviews (6): Last reviewed commit: "chore: formatting" | Re-trigger Greptile

Comment thread src/lib/scaffold/scaffold.ts Outdated
Comment thread src/lib/scaffold/scaffold.ts

@devin-ai-integration devin-ai-integration 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.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 6 additional findings.

Open in Devin Review

nicknisi added 6 commits June 7, 2026 14:27
v1 scaffolding only supports Next.js, so passing e.g. `--integration react`
in an empty directory silently produced a Next.js app. Emit a one-shot
`scaffold:notice` before prompting/auto-scaffolding when a non-Next
integration was requested, surfaced by all three adapters (CLI warn, headless
NDJSON, dashboard status). True multi-framework scaffolding remains a tracked
follow-up (noted in scaffold.ts).
--integration was read in only two places: it seeded context.integration at
startup, which the detection actor then unconditionally overwrote. So it never
affected which installer ran — vestigial surface that predated the XState
detection step. Remove the flag plus its option/type/buildOptions plumbing and
README docs.

This also drops the scaffold:notice machinery added earlier in this branch,
which existed solely to warn that --integration couldn't steer the Next.js-only
scaffold. With the flag gone, there's nothing to warn about.

Detection is unchanged and still selects the integration. The install command
is non-strict, so a leftover --integration is silently ignored rather than
erroring — behavior is unchanged (the flag was already a no-op). True
multi-framework scaffolding remains a tracked follow-up.
Addresses Greptile review on PR #173.

- runCreateNextApp hardcoded `npx`, which ENOENTs on a bun-only machine (no
  npm/npx on PATH) even when --pm bun was passed. Route through each package
  manager's own runner: npm -> `npx --yes`, pnpm -> `pnpm dlx`,
  yarn -> `yarn dlx`, bun -> `bunx`. (`yarn dlx` assumes Yarn >= 2; Yarn 1
  users can fall back to --pm npm.)
- The thrown error appended the full unbounded create-next-app stderr; cap it
  at 2000 chars so a long dependency-resolution trace stays actionable.

Tests: runner-per-PM matrix + stderr cap.
Addresses the two P2s from the greptile --agent review on PR #173.

- stderr was accumulated unbounded and only sliced when building the error
  message; cap it at collection time so a pathological create-next-app failure
  can't buffer hundreds of KB. The message-time slice stays for the
  single-large-chunk case.
- When create-next-app is killed by a signal, the close handler received
  code=null and `code ?? 1` masked it as "exited with code 1". Capture the
  signal and report "was killed by signal <SIG>".

The review's critical finding (empty-CWD scaffolding "fails silently" on an
undefined installDir) is a false positive: installDir is typed `string` and
buildOptions always resolves it to process.cwd() via resolveInstallDir, so it
is never undefined. No change.

Tests: kill-signal rejection path.
@nicknisi nicknisi merged commit d96ad16 into main Jun 8, 2026
6 checks passed
@nicknisi nicknisi deleted the nicknisi/scaffold-app branch June 8, 2026 15:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant