Skip to content

Commit f63b559

Browse files
juanmaguitarclaude
andauthored
Run the initial setup steps as one chain (#246) (#276)
> **Stacked on #274.** Review that one first — it is the kill path this PR's Stop button depends on. The diff below is this branch against #274, not against trunk. ## Why After the clone finishes, the setup checklist stops and waits. **Install npm dependencies**, then **Run full build** — two clicks, with no decision to make between them. The contributor's only job is to notice a step ended and press the next button. That is the wrong shape for a wizard. Someone who walks away during the clone — which is what you do during a ten-minute clone at a Contributor Day — comes back to a checklist waiting on them rather than to an environment they can work in. #246 makes the case at length. ## What changes Setup becomes the third chain of the shape the app already runs twice: `planUpdateSteps` (fetch → install → build) for a trunk update, `planApplySteps` (apply → install → build) for a patch, both sharing `updateStepStatuses` and `updateOutcome`. `planSetupSteps` joins them, and **the chain starts on its own when the clone finishes** — no button. What makes running unattended reasonable rather than presumptuous, and each of these is the reason the issue gives: - **It is visible.** A banner names the running step and counts it (`step 2 of 3`), and every line of npm output still goes to the Terminal (#41). - **It stops.** **Stop setup**, and Ctrl+C in the Terminal, end the child — install included, which is what #274 is for. - **It stops at the first failure.** A build on a half-installed tree cannot work, and its failure would bury the one that mattered (#42). - **It starts once, on an edge.** The clone going from running to finished, once per row. A site already cloned when its row appears never triggers, so reopening the app on a half-finished site lands on the manual checklist rather than launching a half-hour build nobody asked for. **Deliberately not in it:** starting the dev server. #246 lists it as the fourth link, and I left it out — that step also calls `markSkipWizard()` and hands the contributor to a WordPress setup wizard in a browser, so running it unattended would end the checklist on their behalf and leave a server listening that nobody asked for. Also not in it: resuming the chain after a stop. Retry is a plain button on the failed step; a cancel followed by a retry must not silently restart a half-hour build. **It also closes the open half of #44.** The checklist ladder moves out of `index.jsx` into `setupStepStatuses` and gains a `failed` state, so a step whose last attempt lost says **Failed**, keeps its retry, and does not hand `current` to the next step. #258 fixed the label half ("Ready" until an action runs); the explicit failed/retryable state was the acceptance criterion still open, and a chain that can stop is what made it necessary rather than nice. ## How to test this Platforms: **any** for 1 and 4–6; do **2 and 3 on both macOS and Windows** — process-tree killing is where they differ. **Starting state:** the app, with no site for the WordPress repo you are about to create. This is a real clone plus a real install plus a real build, so budget for it — the point of steps 2 and 3 is that you do not have to sit through the build. 1. **Create a WordPress Core site.** Watch the *Download WordPress development version* step. It should say the install and build start on their own when it finishes. 2. **When the clone finishes, click nothing.** The blue banner appears, *Install npm dependencies* turns **In progress**, and npm output streams into the Terminal. When install ends, the build follows on its own and the banner reads *step 3 of 3*. - *Start dev server & finish wizard* must still read **Ready** and must **not** have started. 3. **Press Stop setup** while npm install is running. The install actually dies — check no `node`/`npm` child of the app survives. The build never starts, the banner is replaced by *Setup stopped*, and the install step offers **Retry npm install**. 4. **Press Stop setup during the build** instead (create a second site, let install finish). The whole Grunt tree dies, not just the runner (#83, #146). 5. **A failing install.** Easiest is to pull the network mid-install. The step reads **Failed** in red with its retry live, the build stays **Locked**, and the chain does not advance. 6. **Reopen mid-setup.** Quit during the build, relaunch, open the site. The checklist shows the real state from disk and the chain does **not** restart by itself. 7. **Skip initialization wizard** during a running chain. The checklist goes away, the chain keeps running, the Terminal still shows it. **What must not have happened:** - **The dev server must not have started, and the wizard must not be marked skipped.** The chain ends at the build. If you land on a running server or a post-init view without clicking, that is the regression. - **No orphaned `npm`/`node` process after a Stop**, on either platform. - **A stop must not be reported as a failure.** A killed npm exits non-zero — on Windows without even a signal — so the step must read *Setup stopped*, not *npm install failed*. - **A stop must not respawn itself.** Watch for a second install starting a moment after you stopped the first (that is #274's `cancelled` flag doing its job). - **Reopening the app must not start anything.** Step 6 is the one that would be easy not to notice: it only misbehaves on a site you left half-finished, and the symptom is your laptop quietly building for half an hour. ## Risks and limitations **The end-to-end chain has not been driven by hand.** I could not: this Electron build does not expose its accessibility tree to the automation available here, and creating a site needs a native folder picker. What I did verify is that the renderer bundles and mounts with these changes, lint is clean, and both Node runtimes are green (840 tests). **Steps 1–7 above are unverified and want a human**, and step 3 in particular is the one that justifies the whole feature. `buildFailed` is session-local. Only the install outcome is persisted (`src/main.js` records it on the site's meta), so after a restart a failed build reads **Ready** again rather than **Failed**. That is the honest fallback — the app still knows there is no build on disk, just not that the last attempt lost — and persisting it is a separate change. Auto-start is once per row **mount**, not once per site. Switching away from a site and back during a clone remounts the row; if the clone finishes after that remount the chain still fires, which is the wanted behaviour, but it does mean the guard is memory, not state on disk. The `hasNodeModules` check is what stops that mattering. The banner says the build can take up to half an hour on Windows (#72). It runs a production build where a dev build would do — that is #92, untouched here, and auto-starting makes it more worth fixing, not less. ## Related Closes #246. Closes the remaining acceptance criterion of #44. Depends on #274. Makes #57 (the redundant nested `npm install` between the two steps) easier to fix now the two are one run. --- <details> <summary>Design decisions and alternatives considered</summary> **The checklist rows are not driven by `updateStepStatuses`.** #246 suggests they could be — "it already returns the complete/current/pending/skipped states the checklist needs". They cannot, and this is the one place I departed from the issue. Those states are derived from disk (`hasNodeModules`, `hasBuilt`, `installFailed`) precisely so a site reopened days later, with nothing running, still shows the truth; driving them from chain position would show every step `pending` on a reopened half-finished site. So the reuse is real but narrower: `planSetupSteps` + `updateStepStatuses` + `setupOutcome` drive the **banner** — progress counter, Stop, how it ended — exactly as they drive the update panel, and the rows keep their own ladder. **`planSetupSteps` lives in `update-plan.cjs`** rather than in `setup-steps.cjs`, following the `planApplySteps` precedent: the module is where the chains and the machinery they share live, and splitting the third one off would have meant importing `updateStepStatuses` across modules to save a rename. Its docblock now says it owns three chains. **`setupOutcome` separates `stopped` from `failed-install`/`failed-build`** even though the exit codes are identical. Telling a contributor their install "failed" when they pressed Stop is how a tool loses their trust, and the exit code genuinely cannot tell the two apart — so `stopped` comes from the fact that we asked for the kill, not from what the process did. **Auto-start is a tri-state decision, not a chain of `if`s in an effect.** `setupAutoStartDecision` returns `skip` / `probe` / `start`, because the decision is taken in two halves: is this the clone-finished edge (so reading status off disk is worth it), and then does that status say this is a fresh clone. Being able to test that is worth more than the shape being slightly unusual — it is the riskiest behaviour in the PR. </details> <details> <summary>Review outcome (required — see AGENTS.md)</summary> 2 [fix here] · 0 [follow-up] — both fixed. **architecture 🟡 [fix here]** — the auto-start decision lived inline in a `useEffect` in `index.jsx`, which the suite cannot reach. Per §1, the finding is the missing module, not the missing test — and this was the riskiest decision in the change (too eager and it launches a half-hour build unasked, too shy and the wizard never finishes itself). Extracted to `setupAutoStartDecision` with nine tests covering the edge, the arming, the two refusals and a failed status probe. **architecture 🟡 [fix here]** — four branchy user-facing strings (install and build labels and descriptions) were derived inline in `index.jsx`. §1 names "a string the user reads" explicitly. Extracted to `setupStepCopy`, which takes the same flags as `computeSetupStepState` so the words and the button state cannot disagree; four tests, including one pinning that pairing for the #42 case. The judgement pass was run in this session rather than dispatched to a subagent — the tooling in use blocked spawning one. Flagging it because the standard prefers fresh context for that pass, and self-review is the weaker version. Worth a second pair of eyes on the effect in `index.jsx` in particular. </details> <details> <summary>Implementation notes</summary> The chain reuses the wizard's own `runInstall` / `runScript` wrappers, the same way `runUpdateInstallAndBuild` does, so exit codes, retries, terminal streaming and the post-run status reload all behave identically to every other path. Nothing new crosses IPC. `runScript` gained one line: it clears `buildFailed` when a build starts and records it on exit. Clearing on start rather than on the next exit is what stops a step reading **Failed** while its own retry is streaming output — there is a test for that shape on the install side. `deriveNextAction` is unchanged. The call site now passes a failed step as the current one, since retrying it is what the contributor should do next and a `failed` row consumes no `current` — without that, a stopped chain would leave the view with no cue at all. </details> <details> <summary>Screenshots or recording</summary> None — see the limitation above: I could not drive the app's UI from this environment, so there is no shot of the banner or the failed row. This is the part of the PR that most wants a human with the Buildkite artifact. </details> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 8f36065 commit f63b559

5 files changed

Lines changed: 666 additions & 39 deletions

File tree

‎src/renderer/index.jsx‎

Lines changed: 196 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ import { plus, chevronLeft, chevronRight, chevronDown, copy as copyIcon, check a
2121
import '@wordpress/components/build-style/style.css';
2222
import { Terminal } from 'xterm';
2323
import 'xterm/css/xterm.css';
24-
import { computeSetupStepState, setupStepLabel } from './setup-steps.cjs';
24+
import { computeSetupStepState, setupStepStatuses, setupStepCopy, setupAutoStartDecision, setupStepLabel } from './setup-steps.cjs';
2525
import { deriveNextAction } from './next-action.cjs';
2626
import { shouldShowTerminalHints, computeTerminalBusy } from './terminal-hints.cjs';
2727
import { planDevServerStart, formatElapsed, watchTabLabel } from './dev-server-command.cjs';
2828
import { appendBounded, countLines } from './debug-log.cjs';
2929
import { pathBasename } from './path-basename.cjs';
3030
import { sanitizeSiteFolder, resolveTargetDir, directoryFromFileEntry } from './site-folder.cjs';
3131
import { noticeForOpenResult } from './open-failure.cjs';
32-
import { trunkAgeInfo, planUpdateSteps, updateStepStatuses, SKIP_INSTALL_MESSAGE, planApplySteps, planWatchImpact, APPLY_STATE_TO_STEP } from './update-plan.cjs';
32+
import { trunkAgeInfo, planUpdateSteps, updateStepStatuses, SKIP_INSTALL_MESSAGE, planApplySteps, planWatchImpact, APPLY_STATE_TO_STEP, planSetupSteps, SETUP_STATE_TO_STEP, setupOutcome } from './update-plan.cjs';
3333
import { pickLatest } from '../latest-patch.cjs';
3434
import { beginSetup, adoptSetupPath, discardSetup, rowPathAfterStatus } from './pending-setup.cjs';
3535
import { parsePrRef } from '../patch-sources.cjs';
@@ -1254,6 +1254,11 @@ function SiteRow({ sitePath, initialized, createdAt, label, onInitialized, onSit
12541254
const [building, setBuilding] = useState(false);
12551255
const [hasNodeModules, setHasNodeModules] = useState(false);
12561256
const [installFailed, setInstallFailed] = useState(false);
1257+
// The build's counterpart to installFailed — session-local, because only the
1258+
// install outcome is persisted (main.js records it on the site's meta). After
1259+
// a restart a failed build reads "Ready" again, which is the honest fallback:
1260+
// the app knows there is no build on disk, just not that the last attempt lost.
1261+
const [buildFailed, setBuildFailed] = useState(false);
12571262
const [hasBuilt, setHasBuilt] = useState(false);
12581263
const [skipInit, setSkipInit] = useState(false);
12591264
const [statusLoading, setStatusLoading] = useState(true);
@@ -1289,6 +1294,11 @@ function SiteRow({ sitePath, initialized, createdAt, label, onInitialized, onSit
12891294
const [trunkDate, setTrunkDate] = useState(null);
12901295
const [updateIncomplete, setUpdateIncomplete] = useState(false);
12911296
const [updateState, setUpdateState] = useState('idle'); // idle | fetching | installing | building
1297+
// Initial setup chain (#246): install then build, started by the clone
1298+
// finishing rather than by a click. Same shape as the two chains below.
1299+
const [setupChainState, setSetupChainState] = useState('idle'); // idle | installing | building
1300+
// How the last chain ended, or null while one is running or none has run.
1301+
const [setupChainEnd, setSetupChainEnd] = useState(null);
12921302
// Applying someone else's patch (#11)
12931303
const [applyState, setApplyState] = useState('idle'); // idle | applying | installing | building
12941304
const [applyPreview, setApplyPreview] = useState(null);
@@ -1600,8 +1610,13 @@ function SiteRow({ sitePath, initialized, createdAt, label, onInitialized, onSit
16001610
if (s?.trunkDate) patch.trunkDate = s.trunkDate;
16011611
metaPatchRef.current(sitePath, patch);
16021612
}
1613+
// Returned as well as stored: the setup chain (#246) re-probes when the
1614+
// clone finishes and has to decide from that read, not from state React
1615+
// has not committed yet.
1616+
return s;
16031617
} catch {}
16041618
finally { setStatusLoading(false); }
1619+
return null;
16051620
}, [sitePath]);
16061621
useEffect(()=>{ loadStatus(); }, [loadStatus]);
16071622

@@ -1873,7 +1888,9 @@ function SiteRow({ sitePath, initialized, createdAt, label, onInitialized, onSit
18731888
const runScript = useCallback((name, options = {}) => {
18741889
const { onLog, onDone, args = [], track = true, mirrorToNpm = true, onStart } = options;
18751890
ensureStick('npm');
1876-
if (name === 'build') setBuilding(true);
1891+
// Clearing the failure here rather than on the next exit is what stops the
1892+
// step reading "Failed" while its own retry is streaming to the terminal.
1893+
if (name === 'build') { setBuilding(true); setBuildFailed(false); }
18771894
if (track) currentRunIdRef.current = null;
18781895
return window.api.runNpmScript(sitePath, name, args, ({ data }) => {
18791896
if (mirrorToNpm) appendNpm(data);
@@ -1882,6 +1899,7 @@ function SiteRow({ sitePath, initialized, createdAt, label, onInitialized, onSit
18821899
if (mirrorToNpm) appendNpm(`\n${name} exited with code ${code}\n`);
18831900
if (name === 'build') {
18841901
setBuilding(false);
1902+
setBuildFailed(code !== 0);
18851903
try { await loadStatus(); } catch {}
18861904
}
18871905
if (track) currentRunIdRef.current = null;
@@ -1964,7 +1982,10 @@ function SiteRow({ sitePath, initialized, createdAt, label, onInitialized, onSit
19641982
if (terminalStickRef.current) term.scrollToBottom();
19651983
}, [normalizeForTerminal]);
19661984

1985+
// Taking a step back by hand is the answer to "Setup stopped." — so the
1986+
// notice goes away here rather than lingering over work already resumed.
19671987
const runInstallWithTerminal = useCallback(() => {
1988+
setSetupChainEnd(null);
19681989
writeToTerminal('Running npm install…\n');
19691990
runInstall({
19701991
onLog: (chunk) => writeToTerminal(chunk),
@@ -1975,6 +1996,7 @@ function SiteRow({ sitePath, initialized, createdAt, label, onInitialized, onSit
19751996
}, [runInstall, writeToTerminal]);
19761997

19771998
const runBuildWithTerminal = useCallback(() => {
1999+
setSetupChainEnd(null);
19782000
writeToTerminal('Running npm run build…\n');
19792001
runScript('build', {
19802002
onLog: (chunk) => writeToTerminal(chunk),
@@ -2690,6 +2712,125 @@ function SiteRow({ sitePath, initialized, createdAt, label, onInitialized, onSit
26902712
const applySteps = planApplySteps({ needsInstall: applyNeedsInstall, buildByWatcher: applyBuildByWatcher });
26912713
const applyStepStates = updateStepStatuses(applySteps, applyState, APPLY_STATE_TO_STEP);
26922714

2715+
// --- Initial setup, as one chain (#246) ---
2716+
// The third chain, and the only one nobody starts: between the clone, the
2717+
// install and the build there is no decision to make, so making the
2718+
// contributor notice each one end and click the next was work the app was
2719+
// handing back for nothing. It runs on its own once the clone finishes.
2720+
//
2721+
// What makes that reasonable rather than presumptuous is that it is visible
2722+
// and it stops: the checklist names the running step, and Stop actually ends
2723+
// the child (an install included, since the kill path learned to reach one).
2724+
const isSettingUp = setupChainState !== 'idle';
2725+
const setupSteps = planSetupSteps();
2726+
const setupStepStates = updateStepStatuses(setupSteps, setupChainState, SETUP_STATE_TO_STEP);
2727+
// Whether this chain is ending because the contributor asked it to. A killed
2728+
// npm exits non-zero — on Windows without even a signal — so the exit code
2729+
// cannot tell a stop from a failure; only the fact that we asked for the kill
2730+
// can.
2731+
const setupStoppedRef = useRef(false);
2732+
2733+
// One sentence per way the chain can end, and `setupOutcome` is what picks
2734+
// between them. Every one of them ends by naming where the rest of the work
2735+
// now lives, because the chain going quiet is otherwise indistinguishable
2736+
// from the app having forgotten about the site.
2737+
const SETUP_END_MESSAGES = {
2738+
done: '\nSetup complete — start the dev server when you are ready.\n',
2739+
stopped: '\nSetup stopped. The remaining steps are in the checklist above — run them whenever you are ready.\n',
2740+
'failed-install': '\nnpm install failed — setup stopped here. Its output is above; retry the install from the checklist.\n',
2741+
'failed-build': '\nThe build failed — dependencies are installed. Its output is above; retry the build from the checklist.\n'
2742+
};
2743+
2744+
const finishSetupChain = (outcome) => {
2745+
markTerminalRunning(false);
2746+
terminalKillRef.current = null;
2747+
setSetupChainState('idle');
2748+
setSetupChainEnd(outcome);
2749+
writeToTerminal(SETUP_END_MESSAGES[outcome] || '');
2750+
if (outcome === 'done') confirm('This site is ready to work on');
2751+
};
2752+
2753+
const stopSetupChain = () => {
2754+
setupStoppedRef.current = true;
2755+
writeToTerminal('\nStopping setup…\n');
2756+
killCurrent().catch(() => {});
2757+
};
2758+
2759+
const startSetupChain = () => {
2760+
const state = terminalStateRef.current;
2761+
if (state.running) return;
2762+
setupStoppedRef.current = false;
2763+
setSetupChainEnd(null);
2764+
markTerminalRunning(true);
2765+
terminalKillRef.current = () => { stopSetupChain(); };
2766+
setSetupChainState('installing');
2767+
writeToTerminal('\nSetting this site up — running npm install…\n');
2768+
runInstall({
2769+
onLog: (chunk) => writeToTerminal(chunk),
2770+
onDone: ({ code }) => {
2771+
const stopped = setupStoppedRef.current;
2772+
// Stopping at the first failure is not politeness — a build on a
2773+
// half-installed tree cannot work, and its failure would bury the one
2774+
// that mattered (#42).
2775+
if (stopped || code !== 0) {
2776+
finishSetupChain(setupOutcome({ stopped, installCode: code }));
2777+
return;
2778+
}
2779+
// Checked again here: Stop is reachable in the moment between the
2780+
// install ending and the build being spawned, and a stop that quietly
2781+
// started a half-hour build would be the worst possible answer to it.
2782+
if (setupStoppedRef.current) {
2783+
finishSetupChain(setupOutcome({ stopped: true }));
2784+
return;
2785+
}
2786+
setSetupChainState('building');
2787+
writeToTerminal('\nRunning npm run build…\n');
2788+
runScript('build', {
2789+
onLog: (chunk) => writeToTerminal(chunk),
2790+
onDone: ({ code: buildCode }) => {
2791+
finishSetupChain(setupOutcome({
2792+
stopped: setupStoppedRef.current,
2793+
installCode: 0,
2794+
buildCode
2795+
}));
2796+
}
2797+
});
2798+
}
2799+
});
2800+
};
2801+
2802+
// The chain is started by an edge, not by a state: the clone finishing. The
2803+
// ref keeps that edge reachable from an effect that must not re-run whenever
2804+
// the chain's own callbacks are rebuilt on a render.
2805+
const startSetupChainRef = useRef(startSetupChain);
2806+
useEffect(() => { startSetupChainRef.current = startSetupChain; });
2807+
2808+
const wasPendingRef = useRef(isPending);
2809+
const setupChainArmedRef = useRef(false);
2810+
useEffect(() => {
2811+
const gate = {
2812+
wasPending: wasPendingRef.current,
2813+
isPending,
2814+
alreadyArmed: setupChainArmedRef.current
2815+
};
2816+
wasPendingRef.current = isPending;
2817+
// Two calls, because the decision is: is this the clone-finished edge (so
2818+
// reading the site's state off disk is worth it), and then does that state
2819+
// say this is a fresh clone we should drive. See setupAutoStartDecision.
2820+
if (setupAutoStartDecision(gate) !== 'probe') return undefined;
2821+
setupChainArmedRef.current = true;
2822+
let cancelled = false;
2823+
(async () => {
2824+
// The status this row is holding was probed while the clone was still
2825+
// running, so it is re-read here rather than trusted.
2826+
const status = (await loadStatus()) || null;
2827+
if (cancelled) return;
2828+
if (setupAutoStartDecision({ ...gate, status }) !== 'start') return;
2829+
startSetupChainRef.current();
2830+
})();
2831+
return () => { cancelled = true; };
2832+
}, [isPending, loadStatus]);
2833+
26932834
// The single most recent patch across whatever is loaded — PRs always, Trac
26942835
// attachments once the contributor has opened them (#11). Drives the "Latest"
26952836
// pill and the "latest is a patch file" note.
@@ -3665,6 +3806,15 @@ function SiteRow({ sitePath, initialized, createdAt, label, onInitialized, onSit
36653806
indicatorBorder: 'none',
36663807
indicatorContent: '•'
36673808
},
3809+
failed: {
3810+
color: '#8a1f21',
3811+
background: '#fcf0f1',
3812+
border: '#d63638',
3813+
indicatorBg: '#d63638',
3814+
indicatorColor: '#fff',
3815+
indicatorBorder: 'none',
3816+
indicatorContent: '✕'
3817+
},
36683818
pending: {
36693819
color: '#6c6f72',
36703820
background: '#f8f9f9',
@@ -3685,7 +3835,7 @@ function SiteRow({ sitePath, initialized, createdAt, label, onInitialized, onSit
36853835
}
36863836
};
36873837

3688-
const stepState = computeSetupStepState({
3838+
const setupFlags = {
36893839
isPending,
36903840
statusLoading,
36913841
hasNodeModules,
@@ -3694,31 +3844,28 @@ function SiteRow({ sitePath, initialized, createdAt, label, onInitialized, onSit
36943844
building,
36953845
starting,
36963846
installFailed,
3847+
buildFailed,
36973848
isUpdating
3698-
});
3699-
3700-
let installLabel = 'Install npm dependencies';
3701-
if (stepState.install.done) installLabel = 'Dependencies installed';
3702-
else if (installFailed) installLabel = 'Retry npm install';
3849+
};
3850+
const stepState = computeSetupStepState(setupFlags);
3851+
const { installLabel, installDescription, buildLabel, buildDescription } = setupStepCopy(setupFlags);
37033852

37043853
const baseSteps = [
37053854
{
37063855
key: 'download',
37073856
label: 'Download WordPress development version',
37083857
description: isPending
3709-
? 'Cloning the WordPress develop repository… the next step unlocks when it finishes.'
3858+
// The clone is also the trigger for everything after it (#246), so the
3859+
// step says what happens next rather than implying a click is coming.
3860+
? 'Cloning the WordPress develop repository… install and build start on their own when it finishes.'
37103861
: 'Clone the WordPress develop repository.',
37113862
...stepState.download,
37123863
running: isPending
37133864
},
37143865
{
37153866
key: 'install',
37163867
label: 'Install npm dependencies',
3717-
// Once done, this button never re-enables (#182), so the step says where
3718-
// a later install lives rather than leaving a dead control unexplained.
3719-
description: stepState.install.done
3720-
? 'Installed. Added a dependency to package.json since? Run npm install in the Terminal below.'
3721-
: 'Install npm packages so commands can run.',
3868+
description: installDescription,
37223869
...stepState.install,
37233870
running: installing,
37243871
action: (
@@ -3733,9 +3880,7 @@ function SiteRow({ sitePath, initialized, createdAt, label, onInitialized, onSit
37333880
{
37343881
key: 'build',
37353882
label: 'Run full build',
3736-
description: hasBuilt
3737-
? 'Built. Edited files in src/ since? Run npm run build in the Terminal below so the site picks them up — updates and applied patches rebuild on their own.'
3738-
: 'Compile WordPress Core to generate the dist files. Later updates rebuild automatically.',
3883+
description: buildDescription,
37393884
...stepState.build,
37403885
running: building,
37413886
action: (
@@ -3744,7 +3889,7 @@ function SiteRow({ sitePath, initialized, createdAt, label, onInitialized, onSit
37443889
variant={hasBuilt ? 'secondary' : 'primary'}
37453890
onClick={runBuildWithTerminal}
37463891
disabled={stepState.build.disabled}
3747-
>{hasBuilt ? 'Build complete' : 'Run full build'}</Button>
3892+
>{buildLabel}</Button>
37483893
)
37493894
},
37503895
{
@@ -3781,28 +3926,17 @@ function SiteRow({ sitePath, initialized, createdAt, label, onInitialized, onSit
37813926
}
37823927
];
37833928

3784-
let currentStepCaptured = false;
3785-
const stepItems = baseSteps.map((step) => {
3786-
let status;
3787-
if (step.done) {
3788-
status = 'complete';
3789-
} else if (!currentStepCaptured && step.ready) {
3790-
status = 'current';
3791-
currentStepCaptured = true;
3792-
} else if (step.ready) {
3793-
status = 'pending';
3794-
} else {
3795-
status = 'locked';
3796-
}
3797-
return { ...step, status };
3798-
});
3929+
const stepItems = setupStepStatuses(baseSteps);
37993930

38003931
// The one block to point the contributor at next (#252). The checklist has
38013932
// already worked out its own current step; the resolver folds that together
38023933
// with the post-init state into a single id, which the render tags onto the
38033934
// matching block (`data-next-action` + the `.next-action-cue` class) and the
38043935
// cue hook scrolls into view.
3805-
const currentSetupStep = stepItems.find((s) => s.status === 'current')?.key || null;
3936+
// A failed step counts as the one to point at: retrying it is exactly what
3937+
// the contributor should do next, and it consumes no `current` of its own, so
3938+
// without this a chain that stopped would leave the view with no cue at all.
3939+
const currentSetupStep = stepItems.find((s) => s.status === 'current' || s.status === 'failed')?.key || null;
38063940
const nextAction = deriveNextAction({
38073941
skipInit,
38083942
currentSetupStep,
@@ -4048,6 +4182,33 @@ function SiteRow({ sitePath, initialized, createdAt, label, onInitialized, onSit
40484182
{!skipInit ? (
40494183
<div style={{ padding: 20, border: '1px solid #dcdcde', borderRadius: 12, background: '#fff' }}>
40504184
<div style={{ fontWeight: 600, fontSize: 16, color: '#1d2327' }}>Initial setup checklist</div>
4185+
{/*
4186+
Nobody pressed a button to start this, so the banner has to say what
4187+
is happening, how far along it is and how to stop it — that is the
4188+
whole licence for running unattended. The step counter comes from
4189+
the same `updateStepStatuses` the update panel uses.
4190+
*/}
4191+
{isSettingUp ? (
4192+
<div role="status" style={{ marginTop: 12, display: 'flex', alignItems: 'flex-start', gap: 12, flexWrap: 'wrap', padding: '12px 16px', background: '#e8f3ff', border: '1px solid #66afe9', borderRadius: 8, fontSize: 13, color: '#0b5d95' }}>
4193+
<div style={{ flex: '1 1 320px', display: 'flex', flexDirection: 'column', gap: 4 }}>
4194+
<strong style={{ color: '#0b5d95' }}>
4195+
Setting this site up for you — step {setupStepStates.filter((s) => s.status === 'complete').length + 1} of {setupSteps.length}
4196+
</strong>
4197+
<span>
4198+
{setupChainState === 'installing'
4199+
? 'Installing dependencies. You can leave this running — the build follows on its own.'
4200+
: 'Running the full build. This can take up to half an hour on Windows; the Terminal below shows what it is doing.'}
4201+
</span>
4202+
</div>
4203+
<Button variant="secondary" onClick={stopSetupChain}>Stop setup</Button>
4204+
</div>
4205+
) : null}
4206+
{!isSettingUp && setupChainEnd === 'stopped' ? (
4207+
<div style={{ marginTop: 12, padding: '12px 16px', background: '#fcf9e8', border: '1px solid #dba617', borderRadius: 8, fontSize: 13, color: '#6e5406' }}>
4208+
<strong style={{ color: '#5c4400' }}>Setup stopped.</strong>{' '}
4209+
Nothing was lost — pick it back up with the buttons below whenever you want.
4210+
</div>
4211+
) : null}
40514212
<div style={{ marginTop: 4, fontSize: 13, color: '#3c434a' }}>Complete each step to prepare this site for development.</div>
40524213
<div style={{ marginTop: 16, display: 'flex', flexDirection: 'column', gap: 12 }}>
40534214
{stepItems.map((step) => {

0 commit comments

Comments
 (0)