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 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.backendFwObj || null;
393
- const backendFw = backendFwObj ? backendFwObj.value : null;
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.value);
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.value} integrated` : backendFw || '(skipped)');
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.value,
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.value,
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.value,
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.value} integrated (API routes/SSR)`
677
+ ? `${fwValue(clientFw)} integrated (API routes/SSR)`
641
678
  : backendFw || 'Not configured';
642
679
 
643
- const clientStack = [clientFw.value, clientLang, clientStyle, clientUi, clientState].filter(Boolean).join(' + ');
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.value} integrated backend (API routes / SSR)
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`
@@ -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,
@@ -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.value) || FRAMEWORK_VERSION_FALLBACK[fw.value] || [];
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.value === 'Vite+React' ? '* Vite version:' : `* ${fw.value} version:`;
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.value] || [];
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.value] || [];
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.value} supports server-side rendering and API routes.\n`));
98
- const confirmed = await arrowConfirm(`Use integrated backend (${fw.value} API routes/SSR) instead of a separate backend?`);
99
- if (confirmed) console.log(dim(`\n Using ${fw.value} integrated backend. No separate backend needed.\n`));
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: 'backendFwObj',
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
- const choices = [
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.backendFwObj;
126
+ const fwObj = answers.backendFw;
134
127
  if (!fwObj) return null; // skip — no backend selected
135
128
 
136
- const versions = await fetchLatestVersions(fwObj.value) || FRAMEWORK_VERSION_FALLBACK[fwObj.value] || [];
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.value} version:`, [
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.backendFwObj) return null;
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 { backendFwObj, backendDb } = answers;
171
- if (!backendFwObj || !backendDb) return null;
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[backendFwObj.value] || [];
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 { backendFwObj } = answers;
185
- if (!backendFwObj) return null;
186
- return await selectOptional('Auth strategy:', AUTH_OPTIONS[backendFwObj.value] || [], machine, 10, answers, BLOCKS.BACKEND);
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
- i--;
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: block ? `Type a ${block} framework:` : 'Type your own value:' }, { onCancel: () => process.exit(0) });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multi-agents-cli",
3
- "version": "1.1.77",
3
+ "version": "1.1.79",
4
4
  "description": "Multi-agent workflow orchestration for Claude Code — isolated git worktrees, structured state tracking, autonomous task chaining",
5
5
  "keywords": [
6
6
  "claude-code",