multi-agents-cli 1.1.77 → 1.1.79
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/init.js +48 -11
- package/lib/data-config.js +29 -0
- package/lib/questions-flow.js +23 -30
- package/lib/steps.js +49 -2
- package/lib/ui.js +1 -1
- package/package.json +1 -1
package/init.js
CHANGED
|
@@ -38,6 +38,8 @@ const {
|
|
|
38
38
|
ORM_OPTIONS,
|
|
39
39
|
AUTH_OPTIONS,
|
|
40
40
|
IDE_CANDIDATES,
|
|
41
|
+
BLOCK_CONFIG,
|
|
42
|
+
INTENT_MAP,
|
|
41
43
|
} = require('./lib/data-config');
|
|
42
44
|
|
|
43
45
|
const {
|
|
@@ -62,7 +64,7 @@ const {
|
|
|
62
64
|
} = require('./lib/summary');
|
|
63
65
|
|
|
64
66
|
const { StepMachine, BACK, CONTINUE, RESTART, runQuestions } = require('./lib/steps');
|
|
65
|
-
const { buildStepDefs } = require('./lib/questions-flow');
|
|
67
|
+
const { buildStepDefs, fwValue } = require('./lib/questions-flow');
|
|
66
68
|
|
|
67
69
|
// ── Prompts ───────────────────────────────────────────────────────────────────
|
|
68
70
|
|
|
@@ -373,6 +375,41 @@ const main = async () => {
|
|
|
373
375
|
steps.push(0, 'Project name', projectName);
|
|
374
376
|
separator();
|
|
375
377
|
|
|
378
|
+
// ── Intent selection ────────────────────────────────────────────────────────
|
|
379
|
+
rl.pause();
|
|
380
|
+
const intentChoices = Object.entries(INTENT_MAP).map(([k, v]) => ({ label: v.label, value: k }));
|
|
381
|
+
const intentIdx = await arrowSelect('What would you like to build?', intentChoices);
|
|
382
|
+
const intentKey = intentChoices[intentIdx].value;
|
|
383
|
+
const intent = INTENT_MAP[intentKey];
|
|
384
|
+
|
|
385
|
+
// Secondary prompt for fullstack order
|
|
386
|
+
let blockOrder = intent.blocks;
|
|
387
|
+
if (intentKey === 'fullstack') {
|
|
388
|
+
const orderIdx = await arrowSelect('Full-stack order?', [
|
|
389
|
+
{ label: 'Client first, then backend' },
|
|
390
|
+
{ label: 'Backend first, then client' },
|
|
391
|
+
]);
|
|
392
|
+
blockOrder = orderIdx === 0 ? ['client', 'backend'] : ['backend', 'client'];
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Activate blocks and resolve absolute step indices
|
|
396
|
+
let absoluteStep = 0;
|
|
397
|
+
const blockEntries = [];
|
|
398
|
+
for (const blockKey of blockOrder) {
|
|
399
|
+
const block = BLOCK_CONFIG[blockKey];
|
|
400
|
+
block.isActive = true;
|
|
401
|
+
let isFirstSeg = true;
|
|
402
|
+
for (const [segKey, seg] of Object.entries(block)) {
|
|
403
|
+
if (segKey === 'isActive' || typeof seg !== 'object' || !('step' in seg)) continue;
|
|
404
|
+
seg.absoluteStep = absoluteStep;
|
|
405
|
+
if (isFirstSeg) { blockEntries.push(absoluteStep); isFirstSeg = false; }
|
|
406
|
+
absoluteStep++;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
steps.blockEntries = blockEntries;
|
|
410
|
+
|
|
411
|
+
separator();
|
|
412
|
+
|
|
376
413
|
// Run all question steps with back-nav support
|
|
377
414
|
const answers = await runQuestions(stepDefs, steps);
|
|
378
415
|
if (answers === RESTART) {
|
|
@@ -389,8 +426,8 @@ const main = async () => {
|
|
|
389
426
|
const clientUi = answers.clientUi || null;
|
|
390
427
|
const clientStyle = answers.clientStyle || null;
|
|
391
428
|
const useIntegratedBackend = answers.useIntegratedBackend || false;
|
|
392
|
-
const backendFwObj = answers.
|
|
393
|
-
const backendFw = backendFwObj ? backendFwObj
|
|
429
|
+
const backendFwObj = answers.backendFw || null;
|
|
430
|
+
const backendFw = backendFwObj ? fwValue(backendFwObj) : null;
|
|
394
431
|
const backendLang = backendFwObj ? backendFwObj.language : null;
|
|
395
432
|
const backendOrm = answers.backendOrm || null;
|
|
396
433
|
const backendAuth = answers.backendAuth || null;
|
|
@@ -410,12 +447,12 @@ const main = async () => {
|
|
|
410
447
|
|
|
411
448
|
console.log(`\n${bold('Review your configuration:')}\n`);
|
|
412
449
|
summaryLine('Project', projectName);
|
|
413
|
-
summaryLine('Client framework', clientFw
|
|
450
|
+
summaryLine('Client framework', fwValue(clientFw));
|
|
414
451
|
summaryLine('Client language', clientLang);
|
|
415
452
|
summaryLine('State management', clientState);
|
|
416
453
|
summaryLine('UI library', clientUi);
|
|
417
454
|
summaryLine('Styling', clientStyle);
|
|
418
|
-
summaryLine('Backend type', backendType === 'integrated' ? `${clientFw
|
|
455
|
+
summaryLine('Backend type', backendType === 'integrated' ? `${fwValue(clientFw)} integrated` : backendFw || '(skipped)');
|
|
419
456
|
if (backendType !== 'integrated') {
|
|
420
457
|
summaryLine('Backend language', backendLang);
|
|
421
458
|
summaryLine('ORM', backendOrm);
|
|
@@ -494,7 +531,7 @@ const main = async () => {
|
|
|
494
531
|
|
|
495
532
|
writeConfig(path.join(ROOT, 'client', 'CLAUDE.md'), {
|
|
496
533
|
PROJECT_NAME: projectName,
|
|
497
|
-
FRAMEWORK: clientFw
|
|
534
|
+
FRAMEWORK: fwValue(clientFw),
|
|
498
535
|
FRAMEWORK_VERSION: clientFwVersion || '',
|
|
499
536
|
LANGUAGE: clientLang,
|
|
500
537
|
STATE: clientState,
|
|
@@ -508,7 +545,7 @@ const main = async () => {
|
|
|
508
545
|
const scaffoldConfig = {
|
|
509
546
|
projectName,
|
|
510
547
|
client: {
|
|
511
|
-
framework: clientFw
|
|
548
|
+
framework: fwValue(clientFw),
|
|
512
549
|
frameworkVersion: clientFwVersion || '',
|
|
513
550
|
language: clientLang,
|
|
514
551
|
state: clientState,
|
|
@@ -574,7 +611,7 @@ const main = async () => {
|
|
|
574
611
|
linuxPaths: ideChoice.linux?.paths || [],
|
|
575
612
|
},
|
|
576
613
|
client: {
|
|
577
|
-
framework: clientFw
|
|
614
|
+
framework: fwValue(clientFw),
|
|
578
615
|
language: clientLang,
|
|
579
616
|
state: clientState,
|
|
580
617
|
uiLibrary: clientUi,
|
|
@@ -637,10 +674,10 @@ const main = async () => {
|
|
|
637
674
|
// ── BUILD_STATE.md ────────────────────────────────────────────────────────────
|
|
638
675
|
|
|
639
676
|
const backendDisplay = backendType === 'integrated'
|
|
640
|
-
? `${clientFw
|
|
677
|
+
? `${fwValue(clientFw)} integrated (API routes/SSR)`
|
|
641
678
|
: backendFw || 'Not configured';
|
|
642
679
|
|
|
643
|
-
const clientStack = [clientFw
|
|
680
|
+
const clientStack = [fwValue(clientFw), clientLang, clientStyle, clientUi, clientState].filter(Boolean).join(' + ');
|
|
644
681
|
const backendStack = backendType === 'separate'
|
|
645
682
|
? [backendFw, backendLang, backendOrm, backendAuth].filter(Boolean).join(' + ')
|
|
646
683
|
: backendDisplay;
|
|
@@ -668,7 +705,7 @@ Backend : ${backendStack}
|
|
|
668
705
|
|
|
669
706
|
## Backend State
|
|
670
707
|
${backendType === 'integrated'
|
|
671
|
-
? `Type: ${clientFw
|
|
708
|
+
? `Type: ${fwValue(clientFw)} integrated backend (API routes / SSR)
|
|
672
709
|
- [ ] API routes - server-side endpoints
|
|
673
710
|
- [ ] Auth - authentication strategy
|
|
674
711
|
- [ ] DB - data layer if needed`
|
package/lib/data-config.js
CHANGED
|
@@ -258,8 +258,37 @@ const BLOCKS = {
|
|
|
258
258
|
SHARED: 'shared',
|
|
259
259
|
};
|
|
260
260
|
|
|
261
|
+
// ── Block config ─────────────────────────────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
const BLOCK_CONFIG = {
|
|
264
|
+
client: {
|
|
265
|
+
isActive: true,
|
|
266
|
+
framework: { data: CLIENT_FRAMEWORKS, required: true, key: 'clientFw', step: 0 },
|
|
267
|
+
stateManagement: { data: STATE_OPTIONS, required: false, key: 'clientState', step: 1 },
|
|
268
|
+
uiLibrary: { data: UI_OPTIONS, required: false, key: 'clientUi', step: 2 },
|
|
269
|
+
styling: { data: STYLING_OPTIONS, required: false, key: 'clientStyle', step: 3 },
|
|
270
|
+
},
|
|
271
|
+
backend: {
|
|
272
|
+
isActive: false,
|
|
273
|
+
framework: { data: BACKEND_FRAMEWORKS, required: false, key: 'backendFw', step: 0 },
|
|
274
|
+
database: { data: DB_OPTIONS, required: false, key: 'backendDb', step: 1 },
|
|
275
|
+
orm: { data: ORM_OPTIONS, required: false, key: 'backendOrm', step: 2 },
|
|
276
|
+
auth: { data: AUTH_OPTIONS, required: false, key: 'backendAuth', step: 3 },
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// ── Intent map ────────────────────────────────────────────────────────────────
|
|
281
|
+
|
|
282
|
+
const INTENT_MAP = {
|
|
283
|
+
fullstack: { label: 'Full-stack', blocks: ['client', 'backend'], order: null },
|
|
284
|
+
frontend: { label: 'Frontend only', blocks: ['client'], order: null },
|
|
285
|
+
backend: { label: 'Backend only', blocks: ['backend'], order: null },
|
|
286
|
+
};
|
|
287
|
+
|
|
261
288
|
module.exports = {
|
|
262
289
|
BLOCKS,
|
|
290
|
+
BLOCK_CONFIG,
|
|
291
|
+
INTENT_MAP,
|
|
263
292
|
FRAMEWORK_CONVENTIONS,
|
|
264
293
|
CLIENT_FRAMEWORKS,
|
|
265
294
|
BACKEND_FRAMEWORKS,
|
package/lib/questions-flow.js
CHANGED
|
@@ -16,6 +16,8 @@ const { buildIDEOptions, verifyIDE, detectTerminal } = require('./detect');
|
|
|
16
16
|
// Each def: { name: string, run: async (machine, answers) => value | BACK | RESTART }
|
|
17
17
|
// Conditional steps return their skip-value immediately when not applicable.
|
|
18
18
|
|
|
19
|
+
const fwValue = (fw) => fw && typeof fw === 'object' ? fw.value : (typeof fw === 'string' && fw.length > 0 ? fw : '__custom__');
|
|
20
|
+
|
|
19
21
|
const buildStepDefs = (IDE_CANDIDATES) => [
|
|
20
22
|
|
|
21
23
|
// ── Step 0: Client framework ────────────────────────────────────────────────
|
|
@@ -34,7 +36,7 @@ const buildStepDefs = (IDE_CANDIDATES) => [
|
|
|
34
36
|
run: async (machine, answers) => {
|
|
35
37
|
stepHeader(2, 12);
|
|
36
38
|
const fw = answers.clientFw;
|
|
37
|
-
const versions = await fetchLatestVersions(fw
|
|
39
|
+
const versions = await fetchLatestVersions(fwValue(fw)) || FRAMEWORK_VERSION_FALLBACK[fwValue(fw)] || [];
|
|
38
40
|
if (!versions.length) return null; // skip silently
|
|
39
41
|
|
|
40
42
|
console.log(dim(' Fetching latest versions...'));
|
|
@@ -42,7 +44,7 @@ const buildStepDefs = (IDE_CANDIDATES) => [
|
|
|
42
44
|
label: i === 0 ? `v${v} ${dim('(latest)')}` : `v${v}`,
|
|
43
45
|
value: v,
|
|
44
46
|
}));
|
|
45
|
-
const label = fw
|
|
47
|
+
const label = fwValue(fw) === 'Vite+React' ? '* Vite version:' : `* ${fwValue(fw)} version:`;
|
|
46
48
|
const navOpts = machine.navOptions(1, false);
|
|
47
49
|
const idx = await arrowSelect(label, [
|
|
48
50
|
...versionChoices,
|
|
@@ -60,7 +62,7 @@ const buildStepDefs = (IDE_CANDIDATES) => [
|
|
|
60
62
|
name: 'clientState',
|
|
61
63
|
run: async (machine, answers) => {
|
|
62
64
|
stepHeader(3, 12);
|
|
63
|
-
const opts = STATE_OPTIONS[answers.clientFw
|
|
65
|
+
const opts = STATE_OPTIONS[fwValue(answers.clientFw)] || [];
|
|
64
66
|
return await selectOptional('State management:', opts, machine, 2, answers, BLOCKS.CLIENT);
|
|
65
67
|
},
|
|
66
68
|
},
|
|
@@ -70,7 +72,7 @@ const buildStepDefs = (IDE_CANDIDATES) => [
|
|
|
70
72
|
name: 'clientUi',
|
|
71
73
|
run: async (machine, answers) => {
|
|
72
74
|
stepHeader(4, 12);
|
|
73
|
-
const opts = UI_OPTIONS[answers.clientFw
|
|
75
|
+
const opts = UI_OPTIONS[fwValue(answers.clientFw)] || [];
|
|
74
76
|
return await selectOptional('UI library:', opts, machine, 3, answers, BLOCKS.CLIENT);
|
|
75
77
|
},
|
|
76
78
|
},
|
|
@@ -90,20 +92,20 @@ const buildStepDefs = (IDE_CANDIDATES) => [
|
|
|
90
92
|
run: async (machine, answers) => {
|
|
91
93
|
stepHeader(6, 12);
|
|
92
94
|
const fw = answers.clientFw;
|
|
93
|
-
if (!fw.integratedBackend) return false; // skip — not applicable
|
|
95
|
+
if (!fw || !fw.integratedBackend) return false; // skip — not applicable
|
|
94
96
|
|
|
95
97
|
separator();
|
|
96
98
|
console.log(`\n${bold(blue('Backend configuration'))}`);
|
|
97
|
-
console.log(dim(` ${fw
|
|
98
|
-
const confirmed = await arrowConfirm(`Use integrated backend (${fw
|
|
99
|
-
if (confirmed) console.log(dim(`\n Using ${fw
|
|
99
|
+
console.log(dim(` ${fwValue(fw)} supports server-side rendering and API routes.\n`));
|
|
100
|
+
const confirmed = await arrowConfirm(`Use integrated backend (${fwValue(fw)} API routes/SSR) instead of a separate backend?`);
|
|
101
|
+
if (confirmed) console.log(dim(`\n Using ${fwValue(fw)} integrated backend. No separate backend needed.\n`));
|
|
100
102
|
return confirmed;
|
|
101
103
|
},
|
|
102
104
|
},
|
|
103
105
|
|
|
104
106
|
// ── Step 6: Backend framework ────────────────────────────────────────────────
|
|
105
107
|
{
|
|
106
|
-
name: '
|
|
108
|
+
name: 'backendFw',
|
|
107
109
|
run: async (machine, answers) => {
|
|
108
110
|
stepHeader(7, 12);
|
|
109
111
|
if (answers.useIntegratedBackend) return null; // skip
|
|
@@ -112,16 +114,7 @@ const buildStepDefs = (IDE_CANDIDATES) => [
|
|
|
112
114
|
console.log(`\n${bold(blue('Backend configuration'))}`);
|
|
113
115
|
console.log(dim(' You can skip the backend framework and decide later.\n'));
|
|
114
116
|
|
|
115
|
-
|
|
116
|
-
...BACKEND_FRAMEWORKS.map(f => ({ label: f.label || f.value })),
|
|
117
|
-
{ label: dim('Skip (decide later)') },
|
|
118
|
-
];
|
|
119
|
-
const idx = await arrowSelect('Backend framework:', choices);
|
|
120
|
-
|
|
121
|
-
// Back nav
|
|
122
|
-
if (idx >= BACKEND_FRAMEWORKS.length + 1) return BACK;
|
|
123
|
-
|
|
124
|
-
return idx === BACKEND_FRAMEWORKS.length ? null : BACKEND_FRAMEWORKS[idx];
|
|
117
|
+
return await selectOptional('Backend framework:', BACKEND_FRAMEWORKS, machine, 6, answers, BLOCKS.BACKEND);
|
|
125
118
|
},
|
|
126
119
|
},
|
|
127
120
|
|
|
@@ -130,10 +123,10 @@ const buildStepDefs = (IDE_CANDIDATES) => [
|
|
|
130
123
|
name: 'backendFwVersion',
|
|
131
124
|
run: async (machine, answers) => {
|
|
132
125
|
stepHeader(8, 12);
|
|
133
|
-
const fwObj = answers.
|
|
126
|
+
const fwObj = answers.backendFw;
|
|
134
127
|
if (!fwObj) return null; // skip — no backend selected
|
|
135
128
|
|
|
136
|
-
const versions = await fetchLatestVersions(fwObj
|
|
129
|
+
const versions = await fetchLatestVersions(fwValue(fwObj)) || FRAMEWORK_VERSION_FALLBACK[fwValue(fwObj)] || [];
|
|
137
130
|
if (!versions.length) return null;
|
|
138
131
|
|
|
139
132
|
const vChoices = versions.map((v, i) => ({
|
|
@@ -141,7 +134,7 @@ const buildStepDefs = (IDE_CANDIDATES) => [
|
|
|
141
134
|
value: v,
|
|
142
135
|
}));
|
|
143
136
|
const navOpts = machine.navOptions(7, false);
|
|
144
|
-
const idx = await arrowSelect(`* ${fwObj
|
|
137
|
+
const idx = await arrowSelect(`* ${fwValue(fwObj)} version:`, [
|
|
145
138
|
...vChoices,
|
|
146
139
|
...navOpts.map(n => ({ label: n.label })),
|
|
147
140
|
]);
|
|
@@ -157,7 +150,7 @@ const buildStepDefs = (IDE_CANDIDATES) => [
|
|
|
157
150
|
name: 'backendDb',
|
|
158
151
|
run: async (machine, answers) => {
|
|
159
152
|
stepHeader(9, 12);
|
|
160
|
-
if (!answers.
|
|
153
|
+
if (!answers.backendFw) return null;
|
|
161
154
|
return await selectOptional('Database type:', DB_OPTIONS, machine, 8, answers, BLOCKS.BACKEND);
|
|
162
155
|
},
|
|
163
156
|
},
|
|
@@ -167,10 +160,10 @@ const buildStepDefs = (IDE_CANDIDATES) => [
|
|
|
167
160
|
name: 'backendOrm',
|
|
168
161
|
run: async (machine, answers) => {
|
|
169
162
|
stepHeader(10, 12);
|
|
170
|
-
const {
|
|
171
|
-
if (!
|
|
163
|
+
const { backendFw, backendDb } = answers;
|
|
164
|
+
if (!backendFw || !backendDb) return null;
|
|
172
165
|
const byDb = ORM_OPTIONS_BY_DB[backendDb] || [];
|
|
173
|
-
const byFw = ORM_OPTIONS[
|
|
166
|
+
const byFw = ORM_OPTIONS[fwValue(backendFw)] || [];
|
|
174
167
|
const ormChoices = byDb.length && byFw.length ? byDb.filter(o => byFw.includes(o)) : byDb.length ? byDb : byFw;
|
|
175
168
|
return await selectOptional('ORM / query layer:', ormChoices, machine, 9, answers, BLOCKS.BACKEND);
|
|
176
169
|
},
|
|
@@ -181,9 +174,9 @@ const buildStepDefs = (IDE_CANDIDATES) => [
|
|
|
181
174
|
name: 'backendAuth',
|
|
182
175
|
run: async (machine, answers) => {
|
|
183
176
|
stepHeader(11, 12);
|
|
184
|
-
const {
|
|
185
|
-
if (!
|
|
186
|
-
return await selectOptional('Auth strategy:', AUTH_OPTIONS[
|
|
177
|
+
const { backendFw } = answers;
|
|
178
|
+
if (!backendFw) return null;
|
|
179
|
+
return await selectOptional('Auth strategy:', AUTH_OPTIONS[fwValue(backendFw)] || [], machine, 10, answers, BLOCKS.BACKEND);
|
|
187
180
|
},
|
|
188
181
|
},
|
|
189
182
|
|
|
@@ -238,4 +231,4 @@ const buildStepDefs = (IDE_CANDIDATES) => [
|
|
|
238
231
|
|
|
239
232
|
// ── Exports ───────────────────────────────────────────────────────────────────
|
|
240
233
|
|
|
241
|
-
module.exports = { buildStepDefs };
|
|
234
|
+
module.exports = { buildStepDefs, fwValue };
|
package/lib/steps.js
CHANGED
|
@@ -10,10 +10,12 @@ const SHOW_LIST = Symbol('SHOW_LIST');
|
|
|
10
10
|
// ── Step machine ──────────────────────────────────────────────────────────────
|
|
11
11
|
|
|
12
12
|
class StepMachine {
|
|
13
|
-
constructor() {
|
|
13
|
+
constructor(blockEntries = []) {
|
|
14
14
|
this.history = []; // [{ stepIndex, stepName, answer }]
|
|
15
15
|
this.inBackNav = false;
|
|
16
16
|
this.resumeIndex = null; // step index where back-nav started
|
|
17
|
+
this.blockEntries = blockEntries; // absolute step indices that are block entry points
|
|
18
|
+
this.snapshot = null; // history snapshot taken at back-nav start
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
// Push a completed step answer onto the history stack
|
|
@@ -63,11 +65,30 @@ class StepMachine {
|
|
|
63
65
|
return opts;
|
|
64
66
|
}
|
|
65
67
|
|
|
68
|
+
// Take a snapshot of current history + answers (for ignore-changes restore)
|
|
69
|
+
snapshotHistory() {
|
|
70
|
+
this.snapshot = this.history.map(h => ({ ...h }));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Restore history from snapshot
|
|
74
|
+
restoreSnapshot() {
|
|
75
|
+
if (this.snapshot) {
|
|
76
|
+
this.history = this.snapshot.map(h => ({ ...h }));
|
|
77
|
+
this.snapshot = null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check if a step index is a block entry point
|
|
82
|
+
isBlockEntry(stepIndex) {
|
|
83
|
+
return this.blockEntries.includes(stepIndex);
|
|
84
|
+
}
|
|
85
|
+
|
|
66
86
|
// Enter back-nav mode — record where the user first pressed Back
|
|
67
87
|
enterBackNav(currentStepIndex) {
|
|
68
88
|
if (!this.inBackNav) {
|
|
69
89
|
this.inBackNav = true;
|
|
70
90
|
this.resumeIndex = currentStepIndex;
|
|
91
|
+
this.snapshotHistory();
|
|
71
92
|
}
|
|
72
93
|
}
|
|
73
94
|
|
|
@@ -82,6 +103,7 @@ class StepMachine {
|
|
|
82
103
|
this.history = [];
|
|
83
104
|
this.inBackNav = false;
|
|
84
105
|
this.resumeIndex = null;
|
|
106
|
+
this.snapshot = null;
|
|
85
107
|
}
|
|
86
108
|
}
|
|
87
109
|
|
|
@@ -106,9 +128,34 @@ const runQuestions = async (stepDefs, machine) => {
|
|
|
106
128
|
if (result === BACK) {
|
|
107
129
|
if (i === 0) return RESTART; // nowhere to go back from step 0
|
|
108
130
|
machine.enterBackNav(i); // record where back-nav started
|
|
131
|
+
i--;
|
|
132
|
+
// Skip back over auto-skipped steps (not in history), stop at block entry
|
|
133
|
+
while (i > 0 && !machine.history.find(h => h.stepIndex === i) && !machine.isBlockEntry(i)) i--;
|
|
134
|
+
// Pop the step we landed on
|
|
109
135
|
const popped = machine.pop();
|
|
110
136
|
if (popped) delete answers[popped.stepName];
|
|
111
|
-
|
|
137
|
+
// Block boundary checkpoint — fires when back-nav reaches a block entry point
|
|
138
|
+
if (machine.isBlockEntry(i)) {
|
|
139
|
+
const { arrowSelect } = require('./ui');
|
|
140
|
+
const boundaryChoice = await arrowSelect('You\'ve reached the start of this block.', [
|
|
141
|
+
{ label: '→ Continue from here (re-answer this block)' },
|
|
142
|
+
{ label: '→ Ignore changes (restore to where you started)' },
|
|
143
|
+
{ label: '← Restart configuration' },
|
|
144
|
+
]);
|
|
145
|
+
if (boundaryChoice === 1) {
|
|
146
|
+
const resumeTarget = machine.resumeIndex;
|
|
147
|
+
machine.restoreSnapshot();
|
|
148
|
+
Object.keys(answers).forEach(k => delete answers[k]);
|
|
149
|
+
machine.history.forEach(h => { answers[h.stepName] = h.answer; });
|
|
150
|
+
machine.exitBackNav();
|
|
151
|
+
const entryStep = machine.blockEntries.find(e => e <= i) ?? 0;
|
|
152
|
+
i = resumeTarget != null ? resumeTarget : entryStep + 1;
|
|
153
|
+
|
|
154
|
+
} else if (boundaryChoice === 2) {
|
|
155
|
+
return RESTART;
|
|
156
|
+
}
|
|
157
|
+
// 'continue' — just proceed from current block entry step
|
|
158
|
+
}
|
|
112
159
|
continue;
|
|
113
160
|
}
|
|
114
161
|
|
package/lib/ui.js
CHANGED
|
@@ -131,7 +131,7 @@ const NONE_LABEL = '→ None of these - I\'ll specify my own';
|
|
|
131
131
|
const handleFreeText = async (prompt, items, stepMachine, stepIndex, context, isOptional, block = null) => {
|
|
132
132
|
let typed = '';
|
|
133
133
|
while (!typed) {
|
|
134
|
-
const _res = await prompts({ type: 'text', name: 'value', message:
|
|
134
|
+
const _res = await prompts({ type: 'text', name: 'value', message: prompt.replace(':', '').replace('*', '').trim() + ':' }, { onCancel: () => process.exit(0) });
|
|
135
135
|
typed = (_res.value || '').trim();
|
|
136
136
|
if (!typed && isOptional) return null;
|
|
137
137
|
if (!typed) console.log(dim(' Please enter a value.'));
|
package/package.json
CHANGED