multi-agents-cli 1.1.78 → 1.1.80
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 +42 -2
- package/lib/data-config.js +89 -13
- package/lib/questions-flow.js +32 -27
- package/lib/steps.js +49 -2
- package/lib/ui.js +17 -1
- package/package.json +1 -1
package/init.js
CHANGED
|
@@ -29,7 +29,7 @@ const {
|
|
|
29
29
|
CLIENT_FRAMEWORKS,
|
|
30
30
|
BACKEND_FRAMEWORKS,
|
|
31
31
|
FRAMEWORK_VERSION_FALLBACK,
|
|
32
|
-
fetchLatestVersions,
|
|
32
|
+
fetchLatestVersions, prefetchVersions,
|
|
33
33
|
STATE_OPTIONS,
|
|
34
34
|
UI_OPTIONS,
|
|
35
35
|
STYLING_OPTIONS,
|
|
@@ -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 {
|
|
@@ -373,6 +375,44 @@ 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
|
+
// Prefetch versions for active blocks in background
|
|
412
|
+
if (BLOCK_CONFIG.client.isActive) prefetchVersions(CLIENT_FRAMEWORKS);
|
|
413
|
+
|
|
414
|
+
separator();
|
|
415
|
+
|
|
376
416
|
// Run all question steps with back-nav support
|
|
377
417
|
const answers = await runQuestions(stepDefs, steps);
|
|
378
418
|
if (answers === RESTART) {
|
|
@@ -389,7 +429,7 @@ const main = async () => {
|
|
|
389
429
|
const clientUi = answers.clientUi || null;
|
|
390
430
|
const clientStyle = answers.clientStyle || null;
|
|
391
431
|
const useIntegratedBackend = answers.useIntegratedBackend || false;
|
|
392
|
-
const backendFwObj = answers.
|
|
432
|
+
const backendFwObj = answers.backendFw || null;
|
|
393
433
|
const backendFw = backendFwObj ? fwValue(backendFwObj) : null;
|
|
394
434
|
const backendLang = backendFwObj ? backendFwObj.language : null;
|
|
395
435
|
const backendOrm = answers.backendOrm || null;
|
package/lib/data-config.js
CHANGED
|
@@ -59,8 +59,8 @@ const FRAMEWORK_REGISTRY = {
|
|
|
59
59
|
'Fastify': { registry: 'npm', package: 'fastify' },
|
|
60
60
|
'FastAPI': { registry: 'pypi', package: 'fastapi' },
|
|
61
61
|
'Django': { registry: 'pypi', package: 'django' },
|
|
62
|
-
'Laravel': { registry: '
|
|
63
|
-
'Rails': { registry: '
|
|
62
|
+
'Laravel': { registry: 'packagist', package: 'laravel/framework' },
|
|
63
|
+
'Rails': { registry: 'rubygems', package: 'rails' },
|
|
64
64
|
};
|
|
65
65
|
|
|
66
66
|
const FRAMEWORK_VERSION_FALLBACK = {
|
|
@@ -79,21 +79,35 @@ const FRAMEWORK_VERSION_FALLBACK = {
|
|
|
79
79
|
'Rails': ['7.2', '7.1', '7.0'],
|
|
80
80
|
};
|
|
81
81
|
|
|
82
|
+
const _versionCache = {};
|
|
83
|
+
|
|
84
|
+
const getVersionCache = (frameworkValue) => _versionCache[frameworkValue] || null;
|
|
85
|
+
|
|
86
|
+
const prefetchVersions = (frameworks) => {
|
|
87
|
+
frameworks.forEach(fw => {
|
|
88
|
+
if (_versionCache[fw.value]) return;
|
|
89
|
+
fetchLatestVersions(fw.value).catch(() => {});
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
|
|
82
93
|
const fetchLatestVersions = async (frameworkValue) => {
|
|
94
|
+
if (_versionCache[frameworkValue]) return _versionCache[frameworkValue];
|
|
83
95
|
const entry = FRAMEWORK_REGISTRY[frameworkValue];
|
|
84
96
|
if (!entry || !entry.package) return null;
|
|
85
97
|
|
|
86
98
|
try {
|
|
87
99
|
const https = require('https');
|
|
88
|
-
const fetch = (url) =>
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
100
|
+
const fetch = (url) => Promise.race([
|
|
101
|
+
new Promise((resolve, reject) => {
|
|
102
|
+
const req = https.get(url, (res) => {
|
|
103
|
+
let data = '';
|
|
104
|
+
res.on('data', chunk => data += chunk);
|
|
105
|
+
res.on('end', () => resolve(data));
|
|
106
|
+
});
|
|
107
|
+
req.on('error', reject);
|
|
108
|
+
}),
|
|
109
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 2500)),
|
|
110
|
+
]);
|
|
97
111
|
|
|
98
112
|
if (entry.registry === 'npm') {
|
|
99
113
|
const raw = await fetch(`https://registry.npmjs.org/${entry.package}`);
|
|
@@ -104,7 +118,9 @@ const fetchLatestVersions = async (frameworkValue) => {
|
|
|
104
118
|
.filter((v, i, arr) => arr.indexOf(v) === i)
|
|
105
119
|
.sort((a, b) => b - a)
|
|
106
120
|
.slice(0, 3);
|
|
107
|
-
|
|
121
|
+
const result = versions.length ? versions.map(String) : null;
|
|
122
|
+
if (result) _versionCache[frameworkValue] = result;
|
|
123
|
+
return result;
|
|
108
124
|
}
|
|
109
125
|
|
|
110
126
|
if (entry.registry === 'pypi') {
|
|
@@ -120,8 +136,37 @@ const fetchLatestVersions = async (frameworkValue) => {
|
|
|
120
136
|
.map(v => v.split('.').slice(0, 2).join('.'))
|
|
121
137
|
.filter((v, i, arr) => arr.indexOf(v) === i)
|
|
122
138
|
.slice(0, 3);
|
|
123
|
-
|
|
139
|
+
const result = versions.length ? versions : null;
|
|
140
|
+
if (result) _versionCache[frameworkValue] = result;
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
if (entry.registry === 'packagist') {
|
|
144
|
+
const raw = await fetch(`https://packagist.org/packages/${entry.package}.json`);
|
|
145
|
+
const json = JSON.parse(raw);
|
|
146
|
+
const versions = Object.keys(json.package?.versions || {})
|
|
147
|
+
.filter(v => /^v?\d+\.\d+\.\d+$/.test(v) && !v.includes('-'))
|
|
148
|
+
.map(v => parseInt(v.replace(/^v/, '').split('.')[0]))
|
|
149
|
+
.filter((v, i, arr) => arr.indexOf(v) === i)
|
|
150
|
+
.sort((a, b) => b - a)
|
|
151
|
+
.slice(0, 3);
|
|
152
|
+
const result = versions.length ? versions.map(String) : null;
|
|
153
|
+
if (result) _versionCache[frameworkValue] = result;
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (entry.registry === 'rubygems') {
|
|
158
|
+
const raw = await fetch(`https://rubygems.org/api/v1/versions/${entry.package}.json`);
|
|
159
|
+
const json = JSON.parse(raw);
|
|
160
|
+
const versions = json
|
|
161
|
+
.map(v => parseInt(v.number.split('.')[0]))
|
|
162
|
+
.filter((v, i, arr) => arr.indexOf(v) === i)
|
|
163
|
+
.sort((a, b) => b - a)
|
|
164
|
+
.slice(0, 3);
|
|
165
|
+
const result = versions.length ? versions.map(String) : null;
|
|
166
|
+
if (result) _versionCache[frameworkValue] = result;
|
|
167
|
+
return result;
|
|
124
168
|
}
|
|
169
|
+
|
|
125
170
|
} catch {
|
|
126
171
|
return null;
|
|
127
172
|
}
|
|
@@ -258,14 +303,45 @@ const BLOCKS = {
|
|
|
258
303
|
SHARED: 'shared',
|
|
259
304
|
};
|
|
260
305
|
|
|
306
|
+
// ── Block config ─────────────────────────────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
const BLOCK_CONFIG = {
|
|
309
|
+
client: {
|
|
310
|
+
isActive: true,
|
|
311
|
+
framework: { data: CLIENT_FRAMEWORKS, required: true, key: 'clientFw', step: 0 },
|
|
312
|
+
stateManagement: { data: STATE_OPTIONS, required: false, key: 'clientState', step: 1 },
|
|
313
|
+
uiLibrary: { data: UI_OPTIONS, required: false, key: 'clientUi', step: 2 },
|
|
314
|
+
styling: { data: STYLING_OPTIONS, required: false, key: 'clientStyle', step: 3 },
|
|
315
|
+
},
|
|
316
|
+
backend: {
|
|
317
|
+
isActive: false,
|
|
318
|
+
framework: { data: BACKEND_FRAMEWORKS, required: false, key: 'backendFw', step: 0 },
|
|
319
|
+
database: { data: DB_OPTIONS, required: false, key: 'backendDb', step: 1 },
|
|
320
|
+
orm: { data: ORM_OPTIONS, required: false, key: 'backendOrm', step: 2 },
|
|
321
|
+
auth: { data: AUTH_OPTIONS, required: false, key: 'backendAuth', step: 3 },
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// ── Intent map ────────────────────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
const INTENT_MAP = {
|
|
328
|
+
fullstack: { label: 'Full-stack', blocks: ['client', 'backend'], order: null },
|
|
329
|
+
frontend: { label: 'Frontend only', blocks: ['client'], order: null },
|
|
330
|
+
backend: { label: 'Backend only', blocks: ['backend'], order: null },
|
|
331
|
+
};
|
|
332
|
+
|
|
261
333
|
module.exports = {
|
|
262
334
|
BLOCKS,
|
|
335
|
+
BLOCK_CONFIG,
|
|
336
|
+
INTENT_MAP,
|
|
263
337
|
FRAMEWORK_CONVENTIONS,
|
|
264
338
|
CLIENT_FRAMEWORKS,
|
|
265
339
|
BACKEND_FRAMEWORKS,
|
|
266
340
|
FRAMEWORK_REGISTRY,
|
|
267
341
|
FRAMEWORK_VERSION_FALLBACK,
|
|
268
342
|
fetchLatestVersions,
|
|
343
|
+
getVersionCache,
|
|
344
|
+
prefetchVersions,
|
|
269
345
|
STATE_OPTIONS,
|
|
270
346
|
UI_OPTIONS,
|
|
271
347
|
STYLING_OPTIONS,
|
package/lib/questions-flow.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
// ── Imports ───────────────────────────────────────────────────────────────────
|
|
4
4
|
|
|
5
|
-
const { dim, bold, blue, green, cyan, separator, arrowSelect, arrowConfirm, selectRequired, selectOptional, stepHeader } = require('./ui');
|
|
5
|
+
const { dim, bold, blue, green, cyan, separator, arrowSelect, arrowConfirm, selectRequired, selectOptional, stepHeader, createSpinner } = require('./ui');
|
|
6
6
|
const { BLOCKS } = require('./data-config');
|
|
7
7
|
const { BACK, RESTART } = require('./steps');
|
|
8
8
|
const {
|
|
9
9
|
CLIENT_FRAMEWORKS, BACKEND_FRAMEWORKS, FRAMEWORK_VERSION_FALLBACK,
|
|
10
|
-
fetchLatestVersions, STATE_OPTIONS, UI_OPTIONS, STYLING_OPTIONS,
|
|
10
|
+
fetchLatestVersions, getVersionCache, prefetchVersions, STATE_OPTIONS, UI_OPTIONS, STYLING_OPTIONS,
|
|
11
11
|
DB_OPTIONS, ORM_OPTIONS_BY_DB, ORM_OPTIONS, AUTH_OPTIONS,
|
|
12
12
|
} = require('./data-config');
|
|
13
13
|
const { buildIDEOptions, verifyIDE, detectTerminal } = require('./detect');
|
|
@@ -36,10 +36,16 @@ const buildStepDefs = (IDE_CANDIDATES) => [
|
|
|
36
36
|
run: async (machine, answers) => {
|
|
37
37
|
stepHeader(2, 12);
|
|
38
38
|
const fw = answers.clientFw;
|
|
39
|
-
const
|
|
39
|
+
const cached = getVersionCache(fwValue(fw));
|
|
40
|
+
let versions;
|
|
41
|
+
if (cached) {
|
|
42
|
+
versions = cached;
|
|
43
|
+
} else {
|
|
44
|
+
const spinner = createSpinner('Fetching latest versions...');
|
|
45
|
+
versions = await fetchLatestVersions(fwValue(fw)) || FRAMEWORK_VERSION_FALLBACK[fwValue(fw)] || [];
|
|
46
|
+
spinner.stop();
|
|
47
|
+
}
|
|
40
48
|
if (!versions.length) return null; // skip silently
|
|
41
|
-
|
|
42
|
-
console.log(dim(' Fetching latest versions...'));
|
|
43
49
|
const versionChoices = versions.map((v, i) => ({
|
|
44
50
|
label: i === 0 ? `v${v} ${dim('(latest)')}` : `v${v}`,
|
|
45
51
|
value: v,
|
|
@@ -105,25 +111,16 @@ const buildStepDefs = (IDE_CANDIDATES) => [
|
|
|
105
111
|
|
|
106
112
|
// ── Step 6: Backend framework ────────────────────────────────────────────────
|
|
107
113
|
{
|
|
108
|
-
name: '
|
|
114
|
+
name: 'backendFw',
|
|
109
115
|
run: async (machine, answers) => {
|
|
110
|
-
stepHeader(7, 12);
|
|
111
116
|
if (answers.useIntegratedBackend) return null; // skip
|
|
117
|
+
stepHeader(7, 12);
|
|
112
118
|
|
|
113
119
|
separator();
|
|
114
120
|
console.log(`\n${bold(blue('Backend configuration'))}`);
|
|
115
121
|
console.log(dim(' You can skip the backend framework and decide later.\n'));
|
|
116
122
|
|
|
117
|
-
|
|
118
|
-
...BACKEND_FRAMEWORKS.map(f => ({ label: f.label || f.value })),
|
|
119
|
-
{ label: dim('Skip (decide later)') },
|
|
120
|
-
];
|
|
121
|
-
const idx = await arrowSelect('Backend framework:', choices);
|
|
122
|
-
|
|
123
|
-
// Back nav
|
|
124
|
-
if (idx >= BACKEND_FRAMEWORKS.length + 1) return BACK;
|
|
125
|
-
|
|
126
|
-
return idx === BACKEND_FRAMEWORKS.length ? null : BACKEND_FRAMEWORKS[idx];
|
|
123
|
+
return await selectOptional('Backend framework:', BACKEND_FRAMEWORKS, machine, 6, answers, BLOCKS.BACKEND);
|
|
127
124
|
},
|
|
128
125
|
},
|
|
129
126
|
|
|
@@ -131,11 +128,19 @@ const buildStepDefs = (IDE_CANDIDATES) => [
|
|
|
131
128
|
{
|
|
132
129
|
name: 'backendFwVersion',
|
|
133
130
|
run: async (machine, answers) => {
|
|
134
|
-
|
|
135
|
-
const fwObj = answers.backendFwObj;
|
|
131
|
+
const fwObj = answers.backendFw;
|
|
136
132
|
if (!fwObj) return null; // skip — no backend selected
|
|
133
|
+
stepHeader(8, 12);
|
|
137
134
|
|
|
138
|
-
const
|
|
135
|
+
const cached = getVersionCache(fwValue(fwObj));
|
|
136
|
+
let versions;
|
|
137
|
+
if (cached) {
|
|
138
|
+
versions = cached;
|
|
139
|
+
} else {
|
|
140
|
+
const spinner = createSpinner('Fetching latest versions...');
|
|
141
|
+
versions = await fetchLatestVersions(fwValue(fwObj)) || FRAMEWORK_VERSION_FALLBACK[fwValue(fwObj)] || [];
|
|
142
|
+
spinner.stop();
|
|
143
|
+
}
|
|
139
144
|
if (!versions.length) return null;
|
|
140
145
|
|
|
141
146
|
const vChoices = versions.map((v, i) => ({
|
|
@@ -158,8 +163,8 @@ const buildStepDefs = (IDE_CANDIDATES) => [
|
|
|
158
163
|
{
|
|
159
164
|
name: 'backendDb',
|
|
160
165
|
run: async (machine, answers) => {
|
|
166
|
+
if (!answers.backendFw) return null;
|
|
161
167
|
stepHeader(9, 12);
|
|
162
|
-
if (!answers.backendFwObj) return null;
|
|
163
168
|
return await selectOptional('Database type:', DB_OPTIONS, machine, 8, answers, BLOCKS.BACKEND);
|
|
164
169
|
},
|
|
165
170
|
},
|
|
@@ -168,11 +173,11 @@ const buildStepDefs = (IDE_CANDIDATES) => [
|
|
|
168
173
|
{
|
|
169
174
|
name: 'backendOrm',
|
|
170
175
|
run: async (machine, answers) => {
|
|
176
|
+
const { backendFw, backendDb } = answers;
|
|
177
|
+
if (!backendFw || !backendDb) return null;
|
|
171
178
|
stepHeader(10, 12);
|
|
172
|
-
const { backendFwObj, backendDb } = answers;
|
|
173
|
-
if (!backendFwObj || !backendDb) return null;
|
|
174
179
|
const byDb = ORM_OPTIONS_BY_DB[backendDb] || [];
|
|
175
|
-
const byFw = ORM_OPTIONS[fwValue(
|
|
180
|
+
const byFw = ORM_OPTIONS[fwValue(backendFw)] || [];
|
|
176
181
|
const ormChoices = byDb.length && byFw.length ? byDb.filter(o => byFw.includes(o)) : byDb.length ? byDb : byFw;
|
|
177
182
|
return await selectOptional('ORM / query layer:', ormChoices, machine, 9, answers, BLOCKS.BACKEND);
|
|
178
183
|
},
|
|
@@ -182,10 +187,10 @@ const buildStepDefs = (IDE_CANDIDATES) => [
|
|
|
182
187
|
{
|
|
183
188
|
name: 'backendAuth',
|
|
184
189
|
run: async (machine, answers) => {
|
|
190
|
+
const { backendFw } = answers;
|
|
191
|
+
if (!backendFw) return null;
|
|
185
192
|
stepHeader(11, 12);
|
|
186
|
-
|
|
187
|
-
if (!backendFwObj) return null;
|
|
188
|
-
return await selectOptional('Auth strategy:', AUTH_OPTIONS[fwValue(backendFwObj)] || [], machine, 10, answers, BLOCKS.BACKEND);
|
|
193
|
+
return await selectOptional('Auth strategy:', AUTH_OPTIONS[fwValue(backendFw)] || [], machine, 10, answers, BLOCKS.BACKEND);
|
|
189
194
|
},
|
|
190
195
|
},
|
|
191
196
|
|
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
|
@@ -38,6 +38,22 @@ const rl = readline.createInterface({
|
|
|
38
38
|
const ask = (question) =>
|
|
39
39
|
new Promise((resolve) => rl.question(question, (a) => resolve(a.trim())));
|
|
40
40
|
|
|
41
|
+
const createSpinner = (label) => {
|
|
42
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
43
|
+
let i = 0;
|
|
44
|
+
process.stdout.write(dim(' ' + label + ' '));
|
|
45
|
+
const timer = setInterval(() => {
|
|
46
|
+
process.stdout.write(String.fromCharCode(13) + dim(' ' + label + ' ' + frames[i % frames.length]));
|
|
47
|
+
i++;
|
|
48
|
+
}, 80);
|
|
49
|
+
return {
|
|
50
|
+
stop: () => {
|
|
51
|
+
clearInterval(timer);
|
|
52
|
+
process.stdout.write(String.fromCharCode(13) + dim(' ' + label + ' done') + String.fromCharCode(10));
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
41
57
|
const arrowSelect = async (message, choices, showBack = false, backLabel = '← Restart configuration') => {
|
|
42
58
|
const allChoices = showBack
|
|
43
59
|
? [...choices, { label: dim(backLabel) }]
|
|
@@ -241,7 +257,7 @@ module.exports = {
|
|
|
241
257
|
stepHeader,
|
|
242
258
|
c, bold, green, yellow, dim, cyan, blue, red,
|
|
243
259
|
rl, ask,
|
|
244
|
-
arrowSelect, arrowConfirm,
|
|
260
|
+
arrowSelect, arrowConfirm, createSpinner,
|
|
245
261
|
selectRequired, selectOptional,
|
|
246
262
|
separator, showList, summaryLine, renderTrajectoryLines,
|
|
247
263
|
};
|
package/package.json
CHANGED