From 539f597727804e6565bb3ab84defc2cd19a110eb Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 14:02:17 +0000 Subject: [PATCH] fix(create-objectstack): a failed install lists the install step in "Next steps" "Next steps" printed ` install` only under `--skip-install`, so a run whose install was attempted and failed printed the same list as a successful one, and the next command it named ran against an empty node_modules. The condition is now `!installed`: skipped or failed. Pins: a stub `pnpm` whose `install` exits 1 lists the install step before the run commands; CONTROL legs for a succeeding install (no install step) and `--skip-install` (install step, as before). Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT Co-authored-by: Claude --- .../23865-next-steps-after-failed-install.md | 12 ++ packages/create-objectstack/src/index.ts | 6 +- .../src/scaffold-next-steps-pm.test.ts | 109 +++++++++++++++++- 3 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 .changeset/23865-next-steps-after-failed-install.md diff --git a/.changeset/23865-next-steps-after-failed-install.md b/.changeset/23865-next-steps-after-failed-install.md new file mode 100644 index 00000000000..3a22094d5f7 --- /dev/null +++ b/.changeset/23865-next-steps-after-failed-install.md @@ -0,0 +1,12 @@ +--- +'create-objectstack': patch +--- + +Clause-②: no + +When the dependency install fails (a network error, a registry outage, an engine mismatch, an npm crash), the +scaffolder's closing "Next steps" now lists `npm install` (or `pnpm install`, whichever package manager the run +detected) before `npm run dev` and `npm run validate`, the same as it does under `--skip-install`. Before, a +failed install printed the same list as a successful one, so the next command it named ran against an empty +`node_modules`. A successful install still prints no install step. No flag, exit code, prompt or scaffolded file +changes. diff --git a/packages/create-objectstack/src/index.ts b/packages/create-objectstack/src/index.ts index 8beec56fba1..a76503cfe4e 100644 --- a/packages/create-objectstack/src/index.ts +++ b/packages/create-objectstack/src/index.ts @@ -631,7 +631,11 @@ const program = new Command() if (!isCurrentDir) { console.log(chalk.dim(` cd ${name}`)); } - if (options.skipInstall) { + // `!installed`, not `options.skipInstall`: an install that was attempted + // and FAILED leaves the same empty node_modules as a skipped one, so the + // list a newcomer follows must name the install before the run commands + // in both cases — the warning line above is easy to scroll past. + if (!installed) { console.log(chalk.dim(` ${pm} install`)); } // Same `${pm} run …` shape for both commands, padded to the longer of diff --git a/packages/create-objectstack/src/scaffold-next-steps-pm.test.ts b/packages/create-objectstack/src/scaffold-next-steps-pm.test.ts index 9942c6621fe..3b1769af014 100644 --- a/packages/create-objectstack/src/scaffold-next-steps-pm.test.ts +++ b/packages/create-objectstack/src/scaffold-next-steps-pm.test.ts @@ -15,6 +15,8 @@ // `--skip-install` keeps every run here fast and offline: `detectPackageManager()` // is a read-only ` --version` probe, so its result — and therefore what // "Next steps" prints — does not depend on an install actually following it. +// The install-outcome legs at the foot of the file run without it, against a +// stub `pnpm` that answers `install` itself, so they stay offline too. // // ─── WHY EVERY LEG STUBS pnpm, INCLUDING THE ONE WHERE pnpm WORKS ─────────── // @@ -146,13 +148,56 @@ function nextStepsSection(stdout: string): string { return stdout.split('Next steps:')[1] ?? ''; } -/** Run the real CLI with --skip-install --skip-skills under `bin`'s PATH. */ -function runScaffold(bin: string): string { +/** + * Only the "Next steps:" command list — the lines up to the blank line that + * ends it. Narrower again than `nextStepsSection`: the "AI Skills" block that + * follows says "After ` install`:" under `--skip-install`, so an install + * assertion against the whole section would pass without the list naming it. + */ +function nextStepsList(stdout: string): string { + return nextStepsSection(stdout).split(/\n\s*\n/)[0] ?? ''; +} + +/** + * `node` + `npm` reachable and a stub `pnpm` whose `--version` succeeds and + * whose `install` exits with `installExit`. Every call's argv is appended to + * `calls.log` beside it, so a leg can prove the install was (or was not) + * attempted from the record rather than from the wording of a warning. + */ +function makeInstallStubPnpmBin(installExit: number): string { + const dir = makeBin(); + const script = + '#!/bin/sh\n' + + `echo "$*" >> "${path.join(dir, 'calls.log')}"\n` + + 'case "$1" in\n' + + ' --version) echo "10.31.0"; exit 0 ;;\n' + + ` install) exit ${installExit} ;;\n` + + 'esac\n' + + 'exit 64\n'; + fs.writeFileSync(path.join(dir, 'pnpm'), script, { mode: 0o755 }); + return dir; +} + +/** The argv of every stub `pnpm` call a leg made, one entry per call. */ +function stubPnpmCalls(bin: string): string[] { + const log = path.join(bin, 'calls.log'); + return fs.existsSync(log) ? fs.readFileSync(log, 'utf8').split('\n').filter(Boolean) : []; +} + +/** Run the real CLI with --skip-skills (and, by default, --skip-install) under `bin`'s PATH. */ +function runScaffold(bin: string, { skipInstall = true }: { skipInstall?: boolean } = {}): string { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'create-objectstack-nextsteps-')); try { return execFileSync( TSX, - [INDEX_TS, 'my-app', '--template', 'blank', '--skip-install', '--skip-skills'], + [ + INDEX_TS, + 'my-app', + '--template', + 'blank', + ...(skipInstall ? ['--skip-install'] : []), + '--skip-skills', + ], { cwd: tmp, env: { ...process.env, PATH: childPath(bin) }, encoding: 'utf8' }, ); } finally { @@ -259,3 +304,61 @@ describe('a failed pnpm probe is reported as a fallback, not as a choice', () => }); }, 20_000); }); + +// ─── A failed install leaves the same empty node_modules as a skipped one ─── +// +// "Next steps" used to print ` install` only under `--skip-install`, so a +// run whose install was ATTEMPTED and FAILED (a network error, a registry +// outage, an engine mismatch, an npm crash) printed the same list as a +// successful one, and the next command it named ran against an empty +// node_modules. The condition is now "the install did not complete". The +// install's outcome is the stub's exit status, never the network, for the +// reason the header gives; the stub's call log proves each leg took the path +// it claims. + +/** The index of the first line of `list` matching `re`, or -1. */ +function lineIndex(list: string, re: RegExp): number { + return list.split('\n').findIndex((line) => re.test(line)); +} + +describe('a failed install still lists the install step in "Next steps" (#23865)', () => { + it('install attempted and failed: the list names `pnpm install`, before the run commands', () => { + withBin(() => makeInstallStubPnpmBin(1), (bin) => { + expectPnpmResolvesToStub(bin); + + const list = nextStepsList(runScaffold(bin, { skipInstall: false })); + + // Attempted, and the stub answered it with exit 1. + expect(stubPnpmCalls(bin)).toContain('install'); + const install = lineIndex(list, /\bpnpm install\b/); + expect(install).toBeGreaterThanOrEqual(0); + expect(lineIndex(list, /\bpnpm run dev\b/)).toBeGreaterThan(install); + }); + }, 20_000); + + it('CONTROL: install attempted and succeeded, so no install step in the list', () => { + withBin(() => makeInstallStubPnpmBin(0), (bin) => { + expectPnpmResolvesToStub(bin); + + const list = nextStepsList(runScaffold(bin, { skipInstall: false })); + + expect(stubPnpmCalls(bin)).toContain('install'); + expect(list).toMatch(/\bpnpm run dev\b/); + expect(list).not.toMatch(/\bpnpm install\b/); + }); + }, 20_000); + + it('CONTROL: `--skip-install` still names `pnpm install` before the run commands', () => { + withBin(() => makeInstallStubPnpmBin(0), (bin) => { + expectPnpmResolvesToStub(bin); + + const list = nextStepsList(runScaffold(bin)); + + // Skipped, not attempted: the only call the stub saw is the probe. + expect(stubPnpmCalls(bin)).toEqual(['--version']); + const install = lineIndex(list, /\bpnpm install\b/); + expect(install).toBeGreaterThanOrEqual(0); + expect(lineIndex(list, /\bpnpm run dev\b/)).toBeGreaterThan(install); + }); + }, 20_000); +});