multi-agents-cli 1.1.81 → 1.1.83

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
@@ -28,7 +28,6 @@ const {
28
28
  FRAMEWORK_CONVENTIONS,
29
29
  CLIENT_FRAMEWORKS,
30
30
  BACKEND_FRAMEWORKS,
31
- FRAMEWORK_VERSION_FALLBACK,
32
31
  fetchLatestVersions, prefetchVersions,
33
32
  STATE_OPTIONS,
34
33
  UI_OPTIONS,
@@ -364,7 +363,6 @@ const main = async () => {
364
363
  // ── Step machine + question runner ──────────────────────────────────────────
365
364
 
366
365
  const steps = new StepMachine();
367
- const stepDefs = buildStepDefs(IDE_CANDIDATES);
368
366
 
369
367
  // Project name (free-text, outside the runner)
370
368
  let projectName = '';
@@ -398,15 +396,23 @@ const main = async () => {
398
396
  for (const blockKey of blockOrder) {
399
397
  const block = BLOCK_CONFIG[blockKey];
400
398
  block.isActive = true;
401
- let isFirstSeg = true;
399
+ let isBlockEntry = true;
402
400
  for (const [segKey, seg] of Object.entries(block)) {
403
- if (segKey === 'isActive' || typeof seg !== 'object' || !('step' in seg)) continue;
401
+ if (segKey === 'isActive' || typeof seg !== 'object' || !('key' in seg)) continue;
404
402
  seg.absoluteStep = absoluteStep;
405
- if (isFirstSeg) { blockEntries.push(absoluteStep); isFirstSeg = false; }
403
+ if (isBlockEntry) { blockEntries.push(absoluteStep); isBlockEntry = false; }
406
404
  absoluteStep++;
405
+ // Walk nested properties (framework sub-steps)
406
+ if (seg.properties) {
407
+ for (const prop of Object.values(seg.properties)) {
408
+ prop.absoluteStep = absoluteStep;
409
+ absoluteStep++;
410
+ }
411
+ }
407
412
  }
408
413
  }
409
414
  steps.blockEntries = blockEntries;
415
+ const stepDefs = buildStepDefs(IDE_CANDIDATES, blockOrder);
410
416
 
411
417
  // Prefetch versions for active blocks in background
412
418
  if (BLOCK_CONFIG.client.isActive) prefetchVersions(CLIENT_FRAMEWORKS);
@@ -25,60 +25,26 @@ const FRAMEWORK_CONVENTIONS = {
25
25
  // ── Client frameworks ─────────────────────────────────────────────────────────
26
26
 
27
27
  const CLIENT_FRAMEWORKS = [
28
- { label: 'Next.js', value: 'Next.js', language: 'TypeScript', integratedBackend: true },
29
- { label: 'Angular', value: 'Angular', language: 'TypeScript', integratedBackend: false },
30
- { label: 'Vue / Nuxt', value: 'Nuxt', language: 'TypeScript', integratedBackend: true },
31
- { label: 'SvelteKit', value: 'SvelteKit', language: 'TypeScript', integratedBackend: true },
32
- { label: 'Remix', value: 'Remix', language: 'TypeScript', integratedBackend: true },
33
- { label: 'Vite + React', value: 'Vite+React', language: 'TypeScript', integratedBackend: false },
28
+ { label: 'Next.js', value: 'Next.js', language: 'TypeScript', integratedBackend: true, registry: 'npm', package: 'next', versions: ['15','14','13'] },
29
+ { label: 'Angular', value: 'Angular', language: 'TypeScript', integratedBackend: false, registry: 'npm', package: '@angular/core', versions: ['22','21','20'] },
30
+ { label: 'Vue / Nuxt', value: 'Nuxt', language: 'TypeScript', integratedBackend: true, registry: 'npm', package: 'nuxt', versions: ['3','2',null] },
31
+ { label: 'SvelteKit', value: 'SvelteKit', language: 'TypeScript', integratedBackend: true, registry: 'npm', package: '@sveltejs/kit', versions: ['2','1',null] },
32
+ { label: 'Remix', value: 'Remix', language: 'TypeScript', integratedBackend: true, registry: 'npm', package: '@remix-run/react', versions: ['2','1',null] },
33
+ { label: 'Vite + React', value: 'Vite+React', language: 'TypeScript', integratedBackend: false, registry: 'npm', package: 'vite', versions: ['8','7','6'] },
34
34
  ];
35
35
 
36
36
  // ── Backend frameworks ────────────────────────────────────────────────────────
37
37
 
38
38
  const BACKEND_FRAMEWORKS = [
39
- { label: 'NestJS', value: 'NestJS', language: 'TypeScript' },
40
- { label: 'Express', value: 'Express', language: 'TypeScript' },
41
- { label: 'Fastify', value: 'Fastify', language: 'TypeScript' },
42
- { label: 'Django', value: 'Django', language: 'Python' },
43
- { label: 'FastAPI', value: 'FastAPI', language: 'Python' },
44
- { label: 'Laravel', value: 'Laravel', language: 'PHP' },
45
- { label: 'Ruby on Rails', value: 'Rails', language: 'Ruby' },
39
+ { label: 'NestJS', value: 'NestJS', language: 'TypeScript', registry: 'npm', package: '@nestjs/core', versions: ['11','10','9'] },
40
+ { label: 'Express', value: 'Express', language: 'TypeScript', registry: 'npm', package: 'express', versions: ['5','4'] },
41
+ { label: 'Fastify', value: 'Fastify', language: 'TypeScript', registry: 'npm', package: 'fastify', versions: ['5','4',null] },
42
+ { label: 'Django', value: 'Django', language: 'Python', registry: 'pypi', package: 'django', versions: ['5.1','4.2','3.2'] },
43
+ { label: 'FastAPI', value: 'FastAPI', language: 'Python', registry: 'pypi', package: 'fastapi', versions: ['0.115','0.111','0.104'] },
44
+ { label: 'Laravel', value: 'Laravel', language: 'PHP', registry: 'packagist', package: 'laravel/framework', versions: ['11','10','9'] },
45
+ { label: 'Ruby on Rails', value: 'Rails', language: 'Ruby', registry: 'rubygems', package: 'rails', versions: ['7.2','7.1','7.0'] },
46
46
  ];
47
47
 
48
- // ── Framework version registry ────────────────────────────────────────────────
49
-
50
- const FRAMEWORK_REGISTRY = {
51
- 'Next.js': { registry: 'npm', package: 'next' },
52
- 'Angular': { registry: 'npm', package: '@angular/core' },
53
- 'Nuxt': { registry: 'npm', package: 'nuxt' },
54
- 'SvelteKit': { registry: 'npm', package: '@sveltejs/kit' },
55
- 'Remix': { registry: 'npm', package: '@remix-run/react' },
56
- 'Vite+React': { registry: 'npm', package: 'vite' },
57
- 'NestJS': { registry: 'npm', package: '@nestjs/core' },
58
- 'Express': { registry: 'npm', package: 'express' },
59
- 'Fastify': { registry: 'npm', package: 'fastify' },
60
- 'FastAPI': { registry: 'pypi', package: 'fastapi' },
61
- 'Django': { registry: 'pypi', package: 'django' },
62
- 'Laravel': { registry: 'packagist', package: 'laravel/framework' },
63
- 'Rails': { registry: 'rubygems', package: 'rails' },
64
- };
65
-
66
- const FRAMEWORK_VERSION_FALLBACK = {
67
- 'Next.js': ['15', '14', '13'],
68
- 'Angular': ['22', '21', '20'],
69
- 'Nuxt': ['3', '2', null],
70
- 'SvelteKit': ['2', '1', null],
71
- 'Remix': ['2', '1', null],
72
- 'Vite+React': ['8', '7', '6'],
73
- 'NestJS': ['11', '10', '9' ],
74
- 'Express': ['5', '4'],
75
- 'Fastify': ['5', '4', null],
76
- 'FastAPI': ['0.115', '0.111', '0.104'],
77
- 'Django': ['5.1', '4.2', '3.2'],
78
- 'Laravel': ['11', '10', '9'],
79
- 'Rails': ['7.2', '7.1', '7.0'],
80
- };
81
-
82
48
  const _versionCache = {};
83
49
 
84
50
  const getVersionCache = (frameworkValue) => _versionCache[frameworkValue] || null;
@@ -92,7 +58,7 @@ const prefetchVersions = (frameworks) => {
92
58
 
93
59
  const fetchLatestVersions = async (frameworkValue) => {
94
60
  if (_versionCache[frameworkValue]) return _versionCache[frameworkValue];
95
- const entry = FRAMEWORK_REGISTRY[frameworkValue];
61
+ const entry = [...CLIENT_FRAMEWORKS, ...BACKEND_FRAMEWORKS].find(fw => fw.value === frameworkValue);
96
62
  if (!entry || !entry.package) return null;
97
63
 
98
64
  try {
@@ -308,17 +274,30 @@ const BLOCKS = {
308
274
  const BLOCK_CONFIG = {
309
275
  client: {
310
276
  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 },
277
+ framework: {
278
+ data: CLIENT_FRAMEWORKS, required: true, key: 'clientFw', step: 0,
279
+ desc: 'Which client framework do you want to use?',
280
+ properties: {
281
+ frameworkVersion: { data: CLIENT_FRAMEWORKS, required: false, key: 'clientFwVersion', step: 0, desc: (fw) => `Which version of ${fw} will you use?` },
282
+ stateManagement: { data: STATE_OPTIONS, required: false, key: 'clientState', step: 1, desc: 'How will your app manage shared data (state management)?' },
283
+ uiLibrary: { data: UI_OPTIONS, required: false, key: 'clientUi', step: 2, desc: 'Which UI library do you want to use?' },
284
+ styling: { data: STYLING_OPTIONS, required: false, key: 'clientStyle', step: 3, desc: (fw) => `How do you want to style your ${fw} app?` },
285
+ },
286
+ },
287
+ integratedBackend: { data: null, required: false, key: 'useIntegratedBackend', step: 1, desc: (fw) => `Will you use ${fw}'s built-in backend instead of a separate server?` },
315
288
  },
316
289
  backend: {
317
290
  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 },
291
+ framework: {
292
+ data: BACKEND_FRAMEWORKS, required: false, key: 'backendFw', step: 0,
293
+ desc: 'Which backend framework do you want to use?',
294
+ properties: {
295
+ frameworkVersion: { data: BACKEND_FRAMEWORKS, required: false, key: 'backendFwVersion', step: 0, desc: (fw) => `Which version of ${fw} will you use?` },
296
+ database: { data: DB_OPTIONS, required: false, key: 'backendDb', step: 1, desc: 'Which database do you wish to use to store the data?' },
297
+ orm: { data: ORM_OPTIONS, required: false, key: 'backendOrm', step: 2, desc: (db) => `How will your app communicate with ${db}?` },
298
+ auth: { data: AUTH_OPTIONS, required: false, key: 'backendAuth', step: 3, desc: 'How will your app handle user authentication?' },
299
+ },
300
+ },
322
301
  },
323
302
  };
324
303
 
@@ -337,8 +316,7 @@ module.exports = {
337
316
  FRAMEWORK_CONVENTIONS,
338
317
  CLIENT_FRAMEWORKS,
339
318
  BACKEND_FRAMEWORKS,
340
- FRAMEWORK_REGISTRY,
341
- FRAMEWORK_VERSION_FALLBACK,
319
+
342
320
  fetchLatestVersions,
343
321
  getVersionCache,
344
322
  prefetchVersions,
@@ -2,11 +2,11 @@
2
2
 
3
3
  // ── Imports ───────────────────────────────────────────────────────────────────
4
4
 
5
- const { dim, bold, blue, green, cyan, separator, arrowSelect, arrowConfirm, selectRequired, selectOptional, stepHeader, createSpinner } = require('./ui');
5
+ const { dim, bold, blue, yellow, red, 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
- CLIENT_FRAMEWORKS, BACKEND_FRAMEWORKS, FRAMEWORK_VERSION_FALLBACK,
9
+ BLOCK_CONFIG, CLIENT_FRAMEWORKS, BACKEND_FRAMEWORKS,
10
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');
@@ -18,15 +18,17 @@ const { buildIDEOptions, verifyIDE, detectTerminal } = require('./detect');
18
18
 
19
19
  const fwValue = (fw) => fw && typeof fw === 'object' ? fw.value : (typeof fw === 'string' && fw.length > 0 ? fw : '__custom__');
20
20
 
21
- const buildStepDefs = (IDE_CANDIDATES) => [
21
+ const buildStepDefs = (IDE_CANDIDATES, blockOrder = ['client', 'backend']) => {
22
+ const clientSteps = [
22
23
 
23
24
  // ── Step 0: Client framework ────────────────────────────────────────────────
24
25
  {
25
26
  name: 'clientFw',
26
27
  run: async (machine, answers) => {
27
28
  stepHeader(1, 12);
28
- console.log(`\n${bold(blue('Client configuration'))}`);
29
- return await selectRequired('* Client framework (required):', CLIENT_FRAMEWORKS, machine, 0, answers, BLOCKS.CLIENT);
29
+ console.log(`\n${bold('CLIENT CONFIGURATION')}`);
30
+ console.log(bold(cyan(BLOCK_CONFIG.client.framework.desc)));
31
+ return await selectRequired('* Client framework (required):', CLIENT_FRAMEWORKS, machine, BLOCK_CONFIG.client.framework.absoluteStep, answers, BLOCKS.CLIENT);
30
32
  },
31
33
  },
32
34
 
@@ -42,7 +44,7 @@ const buildStepDefs = (IDE_CANDIDATES) => [
42
44
  versions = cached;
43
45
  } else {
44
46
  const spinner = createSpinner('Fetching latest versions...');
45
- versions = await fetchLatestVersions(fwValue(fw)) || FRAMEWORK_VERSION_FALLBACK[fwValue(fw)] || [];
47
+ versions = await fetchLatestVersions(fwValue(fw)) || CLIENT_FRAMEWORKS.find(f => f.value === fwValue(fw))?.versions?.filter(Boolean) || [];
46
48
  spinner.stop();
47
49
  }
48
50
  if (!versions.length) return null; // skip silently
@@ -50,8 +52,10 @@ const buildStepDefs = (IDE_CANDIDATES) => [
50
52
  label: i === 0 ? `v${v} ${dim('(latest)')}` : `v${v}`,
51
53
  value: v,
52
54
  }));
55
+ const desc = BLOCK_CONFIG.client.framework.properties.frameworkVersion.desc;
56
+ console.log(bold(cyan(typeof desc === 'function' ? desc(fwValue(fw)) : desc)));
53
57
  const label = fwValue(fw) === 'Vite+React' ? '* Vite version:' : `* ${fwValue(fw)} version:`;
54
- const navOpts = machine.navOptions(1, false);
58
+ const navOpts = machine.navOptions(BLOCK_CONFIG.client.framework.properties.frameworkVersion.absoluteStep);
55
59
  const idx = await arrowSelect(label, [
56
60
  ...versionChoices,
57
61
  ...navOpts.map(n => ({ label: n.label })),
@@ -68,8 +72,9 @@ const buildStepDefs = (IDE_CANDIDATES) => [
68
72
  name: 'clientState',
69
73
  run: async (machine, answers) => {
70
74
  stepHeader(3, 12);
75
+ console.log(bold(cyan(BLOCK_CONFIG.client.framework.properties.stateManagement.desc)));
71
76
  const opts = STATE_OPTIONS[fwValue(answers.clientFw)] || [];
72
- return await selectOptional('State management:', opts, machine, 2, answers, BLOCKS.CLIENT);
77
+ return await selectOptional('State management:', opts, machine, BLOCK_CONFIG.client.framework.properties.stateManagement.absoluteStep, answers, BLOCKS.CLIENT);
73
78
  },
74
79
  },
75
80
 
@@ -78,8 +83,9 @@ const buildStepDefs = (IDE_CANDIDATES) => [
78
83
  name: 'clientUi',
79
84
  run: async (machine, answers) => {
80
85
  stepHeader(4, 12);
86
+ console.log(bold(cyan(BLOCK_CONFIG.client.framework.properties.uiLibrary.desc)));
81
87
  const opts = UI_OPTIONS[fwValue(answers.clientFw)] || [];
82
- return await selectOptional('UI library:', opts, machine, 3, answers, BLOCKS.CLIENT);
88
+ return await selectOptional('UI library:', opts, machine, BLOCK_CONFIG.client.framework.properties.uiLibrary.absoluteStep, answers, BLOCKS.CLIENT);
83
89
  },
84
90
  },
85
91
 
@@ -88,7 +94,9 @@ const buildStepDefs = (IDE_CANDIDATES) => [
88
94
  name: 'clientStyle',
89
95
  run: async (machine, answers) => {
90
96
  stepHeader(5, 12);
91
- return await selectOptional('Styling:', STYLING_OPTIONS, machine, 4, answers, BLOCKS.CLIENT);
97
+ const styleDesc = BLOCK_CONFIG.client.framework.properties.styling.desc;
98
+ console.log(bold(cyan(typeof styleDesc === 'function' ? styleDesc(fwValue(answers.clientFw)) : styleDesc)));
99
+ return await selectOptional('Styling:', STYLING_OPTIONS, machine, BLOCK_CONFIG.client.framework.properties.styling.absoluteStep, answers, BLOCKS.CLIENT);
92
100
  },
93
101
  },
94
102
 
@@ -101,7 +109,7 @@ const buildStepDefs = (IDE_CANDIDATES) => [
101
109
  if (!fw || !fw.integratedBackend) return false; // skip — not applicable
102
110
 
103
111
  separator();
104
- console.log(`\n${bold(blue('Backend configuration'))}`);
112
+ console.log(`\n${bold('BACKEND CONFIGURATION')}`);
105
113
  console.log(dim(` ${fwValue(fw)} supports server-side rendering and API routes.\n`));
106
114
  const confirmed = await arrowConfirm(`Use integrated backend (${fwValue(fw)} API routes/SSR) instead of a separate backend?`);
107
115
  if (confirmed) console.log(dim(`\n Using ${fwValue(fw)} integrated backend. No separate backend needed.\n`));
@@ -109,6 +117,10 @@ const buildStepDefs = (IDE_CANDIDATES) => [
109
117
  },
110
118
  },
111
119
 
120
+ ];
121
+
122
+ const backendSteps = [
123
+
112
124
  // ── Step 6: Backend framework ────────────────────────────────────────────────
113
125
  {
114
126
  name: 'backendFw',
@@ -117,10 +129,11 @@ const buildStepDefs = (IDE_CANDIDATES) => [
117
129
  stepHeader(7, 12);
118
130
 
119
131
  separator();
120
- console.log(`\n${bold(blue('Backend configuration'))}`);
132
+ console.log(`\n${bold('BACKEND CONFIGURATION')}`);
121
133
  console.log(dim(' You can skip the backend framework and decide later.\n'));
134
+ console.log(bold(yellow(BLOCK_CONFIG.backend.framework.desc)));
122
135
 
123
- return await selectOptional('Backend framework:', BACKEND_FRAMEWORKS, machine, 6, answers, BLOCKS.BACKEND);
136
+ return await selectOptional('Backend framework:', BACKEND_FRAMEWORKS, machine, BLOCK_CONFIG.backend.framework.absoluteStep, answers, BLOCKS.BACKEND);
124
137
  },
125
138
  },
126
139
 
@@ -138,7 +151,7 @@ const buildStepDefs = (IDE_CANDIDATES) => [
138
151
  versions = cached;
139
152
  } else {
140
153
  const spinner = createSpinner('Fetching latest versions...');
141
- versions = await fetchLatestVersions(fwValue(fwObj)) || FRAMEWORK_VERSION_FALLBACK[fwValue(fwObj)] || [];
154
+ versions = await fetchLatestVersions(fwValue(fwObj)) || BACKEND_FRAMEWORKS.find(f => f.value === fwValue(fwObj))?.versions?.filter(Boolean) || [];
142
155
  spinner.stop();
143
156
  }
144
157
  if (!versions.length) return null;
@@ -147,7 +160,9 @@ const buildStepDefs = (IDE_CANDIDATES) => [
147
160
  label: i === 0 ? `v${v} ${dim('(latest)')}` : `v${v}`,
148
161
  value: v,
149
162
  }));
150
- const navOpts = machine.navOptions(7, false);
163
+ const bvDesc = BLOCK_CONFIG.backend.framework.properties.frameworkVersion.desc;
164
+ console.log(bold(yellow(typeof bvDesc === 'function' ? bvDesc(fwValue(fwObj)) : bvDesc)));
165
+ const navOpts = machine.navOptions(BLOCK_CONFIG.backend.framework.properties.frameworkVersion.absoluteStep);
151
166
  const idx = await arrowSelect(`* ${fwValue(fwObj)} version:`, [
152
167
  ...vChoices,
153
168
  ...navOpts.map(n => ({ label: n.label })),
@@ -165,7 +180,8 @@ const buildStepDefs = (IDE_CANDIDATES) => [
165
180
  run: async (machine, answers) => {
166
181
  if (!answers.backendFw) return null;
167
182
  stepHeader(9, 12);
168
- return await selectOptional('Database type:', DB_OPTIONS, machine, 8, answers, BLOCKS.BACKEND);
183
+ console.log(bold(yellow(BLOCK_CONFIG.backend.framework.properties.database.desc)));
184
+ return await selectOptional('Database type:', DB_OPTIONS, machine, BLOCK_CONFIG.backend.framework.properties.database.absoluteStep, answers, BLOCKS.BACKEND);
169
185
  },
170
186
  },
171
187
 
@@ -176,10 +192,12 @@ const buildStepDefs = (IDE_CANDIDATES) => [
176
192
  const { backendFw, backendDb } = answers;
177
193
  if (!backendFw || !backendDb) return null;
178
194
  stepHeader(10, 12);
195
+ const ormDesc = BLOCK_CONFIG.backend.framework.properties.orm.desc;
196
+ console.log(bold(yellow(typeof ormDesc === 'function' ? ormDesc(fwValue(answers.backendDb)) : ormDesc)));
179
197
  const byDb = ORM_OPTIONS_BY_DB[backendDb] || [];
180
198
  const byFw = ORM_OPTIONS[fwValue(backendFw)] || [];
181
199
  const ormChoices = byDb.length && byFw.length ? byDb.filter(o => byFw.includes(o)) : byDb.length ? byDb : byFw;
182
- return await selectOptional('ORM / query layer:', ormChoices, machine, 9, answers, BLOCKS.BACKEND);
200
+ return await selectOptional('ORM / query layer:', ormChoices, machine, BLOCK_CONFIG.backend.framework.properties.orm.absoluteStep, answers, BLOCKS.BACKEND);
183
201
  },
184
202
  },
185
203
 
@@ -190,17 +208,22 @@ const buildStepDefs = (IDE_CANDIDATES) => [
190
208
  const { backendFw } = answers;
191
209
  if (!backendFw) return null;
192
210
  stepHeader(11, 12);
193
- return await selectOptional('Auth strategy:', AUTH_OPTIONS[fwValue(backendFw)] || [], machine, 10, answers, BLOCKS.BACKEND);
211
+ console.log(bold(yellow(BLOCK_CONFIG.backend.framework.properties.auth.desc)));
212
+ return await selectOptional('Auth strategy:', AUTH_OPTIONS[fwValue(backendFw)] || [], machine, BLOCK_CONFIG.backend.framework.properties.auth.absoluteStep, answers, BLOCKS.BACKEND);
194
213
  },
195
214
  },
196
215
 
216
+ ];
217
+
218
+ const ideSteps = [
219
+
197
220
  // ── Step 11: IDE ─────────────────────────────────────────────────────────────
198
221
  {
199
222
  name: 'ideChoice',
200
223
  run: async (machine, answers) => {
201
224
  stepHeader(12, 12);
202
225
  separator();
203
- console.log(`\n${bold(blue('Environment'))}`);
226
+ console.log(`\n${bold(red('ENVIRONMENT'))}`);
204
227
 
205
228
  const osName = { darwin: 'macOS', win32: 'Windows', linux: 'Linux' }[process.platform] || process.platform;
206
229
  console.log(`\n ${dim('OS detected:')} ${bold(osName)}`);
@@ -241,7 +264,13 @@ const buildStepDefs = (IDE_CANDIDATES) => [
241
264
  }
242
265
  },
243
266
  },
244
- ];
267
+ ];
268
+
269
+ return [
270
+ ...blockOrder.flatMap(b => b === 'client' ? clientSteps : backendSteps),
271
+ ...ideSteps,
272
+ ];
273
+ };
245
274
 
246
275
  // ── Exports ───────────────────────────────────────────────────────────────────
247
276
 
package/lib/steps.js CHANGED
@@ -47,11 +47,11 @@ class StepMachine {
47
47
 
48
48
  // Build the navigation options to append to a step's choices
49
49
  // stepIndex: current step (1-based)
50
- // isFirstStep: true if this is step 2 (first real choice after project name)
51
- navOptions(stepIndex, isFirstStep = false) {
50
+ // Build navigation options back is suppressed at block entry points
51
+ navOptions(stepIndex) {
52
52
  const opts = [];
53
53
 
54
- if (!isFirstStep) {
54
+ if (stepIndex !== this.blockEntries[0]) {
55
55
  opts.push({ label: '← Back to previous step', value: BACK });
56
56
  }
57
57
 
@@ -135,7 +135,7 @@ const runQuestions = async (stepDefs, machine) => {
135
135
  const popped = machine.pop();
136
136
  if (popped) delete answers[popped.stepName];
137
137
  // Block boundary checkpoint — fires when back-nav reaches a block entry point
138
- if (machine.isBlockEntry(i)) {
138
+ if (machine.isBlockEntry(i) && i !== machine.blockEntries[0]) {
139
139
  const { arrowSelect } = require('./ui');
140
140
  const boundaryChoice = await arrowSelect('You\'ve reached the start of this block.', [
141
141
  { label: '→ Continue from here (re-answer this block)' },
@@ -162,11 +162,24 @@ const runQuestions = async (stepDefs, machine) => {
162
162
  if (result === CONTINUE) {
163
163
  // User chose to resume — skip forward to where they first pressed Back
164
164
  const resumeTarget = machine.resumeIndex ?? i + 1;
165
+ machine.restoreSnapshot();
166
+ Object.keys(answers).forEach(k => delete answers[k]);
167
+ machine.history.forEach(h => { answers[h.stepName] = h.answer; });
165
168
  machine.exitBackNav();
166
169
  i = resumeTarget;
167
170
  continue;
168
171
  }
169
172
 
173
+ // If in back-nav and step auto-skipped (null), keep going back
174
+ if (result === null && machine.inBackNav) {
175
+ if (i === 0) return RESTART;
176
+ i--;
177
+ while (i > 0 && !machine.history.find(h => h.stepIndex === i) && !machine.isBlockEntry(i)) i--;
178
+ const popped = machine.pop();
179
+ if (popped) delete answers[popped.stepName];
180
+ continue;
181
+ }
182
+
170
183
  // Normal answer — if re-answering during back-nav, exit back-nav mode
171
184
  if (machine.inBackNav) machine.exitBackNav();
172
185
 
package/lib/ui.js CHANGED
@@ -73,7 +73,11 @@ const arrowSelect = async (message, choices, showBack = false, backLabel = '←
73
73
  return new Promise(resolve => {
74
74
  rl.question(`\n Select (1-${allChoices.length}): `, ans => {
75
75
  const n = parseInt(ans) - 1;
76
- resolve(!isNaN(n) && n >= 0 && n < allChoices.length ? n : 0);
76
+ const idx = !isNaN(n) && n >= 0 && n < allChoices.length ? n : 0;
77
+ const picked = allChoices[idx];
78
+ const pickedLabel = typeof picked === 'string' ? picked : picked.label;
79
+ process.stdout.write(`\r Select (1-${allChoices.length}): ${pickedLabel}\n`);
80
+ resolve(idx);
77
81
  });
78
82
  });
79
83
  };
@@ -92,7 +96,18 @@ const arrowConfirm = async (message, initial = true) => {
92
96
  }, { onCancel: () => process.exit(0) });
93
97
  return res.value ?? initial;
94
98
  }
95
- return initial;
99
+ // readline fallback for non-TTY
100
+ console.log(` ${message}`);
101
+ console.log(` ${dim('1.')} Yes`);
102
+ console.log(` ${dim('2.')} No`);
103
+ return new Promise(resolve => {
104
+ rl.question(`\n Select (1-2): `, ans => {
105
+ const n = parseInt(ans);
106
+ const result = n === 1 ? true : n === 2 ? false : initial;
107
+ console.log(dim(` ${result ? 'Yes' : 'No'}`));
108
+ resolve(result);
109
+ });
110
+ });
96
111
  };
97
112
 
98
113
  // ── Layout helpers ────────────────────────────────────────────────────────────
@@ -201,8 +216,7 @@ const handleFreeText = async (prompt, items, stepMachine, stepIndex, context, is
201
216
  };
202
217
 
203
218
  const selectRequired = async (prompt, items, stepMachine, stepIndex, context = {}, block = null) => {
204
- const isFirstStep = stepIndex <= 1;
205
- const navOpts = stepMachine ? stepMachine.navOptions(stepIndex, isFirstStep) : [];
219
+ const navOpts = stepMachine ? stepMachine.navOptions(stepIndex) : [];
206
220
  const allItems = block !== null ? [...items, { label: NONE_LABEL }, ...navOpts] : [...items, ...navOpts];
207
221
 
208
222
  const idx = await arrowSelect(prompt, allItems.map(i => ({ label: typeof i === 'string' ? i : i.label })));
@@ -223,8 +237,7 @@ const selectRequired = async (prompt, items, stepMachine, stepIndex, context = {
223
237
 
224
238
  const selectOptional = async (prompt, items, stepMachine, stepIndex, context = {}, block = null) => {
225
239
  if (!items || items.length === 0) return null;
226
- const isFirstStep = stepIndex <= 1;
227
- const navOpts = stepMachine ? stepMachine.navOptions(stepIndex, isFirstStep) : [];
240
+ const navOpts = stepMachine ? stepMachine.navOptions(stepIndex) : [];
228
241
  const choices = [
229
242
  ...items.map(i => ({ label: typeof i === 'string' ? i : i.label })),
230
243
  { label: dim('Skip (agent will propose when needed)') },
package/lib/validate.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { FRAMEWORK_REGISTRY, CLIENT_FRAMEWORKS, BACKEND_FRAMEWORKS, BLOCKS } = require('./data-config');
3
+ const { CLIENT_FRAMEWORKS, BACKEND_FRAMEWORKS, BLOCKS } = require('./data-config');
4
4
 
5
5
  const ALL_FRAMEWORKS = [...CLIENT_FRAMEWORKS, ...BACKEND_FRAMEWORKS];
6
6
 
@@ -22,12 +22,11 @@ const fuzzyMatchFramework = (typed, block = null) => {
22
22
  : block === BLOCKS.BACKEND ? BACKEND_FRAMEWORKS
23
23
  : ALL_FRAMEWORKS;
24
24
  const lower = typed.toLowerCase().replace(/[^a-z0-9]/g, '');
25
- const matchedNames = Object.keys(FRAMEWORK_REGISTRY).filter(name => {
26
- const n = name.toLowerCase().replace(/[^a-z0-9]/g, '');
27
- return pool.some(f => f.value === name) &&
28
- (n.includes(lower) || lower.includes(n) || (lower.length >= 3 && n.startsWith(lower.slice(0, 3))));
25
+ const matchedNames = pool.filter(f => {
26
+ const n = f.value.toLowerCase().replace(/[^a-z0-9]/g, '');
27
+ return n.includes(lower) || lower.includes(n) || (lower.length >= 3 && n.startsWith(lower.slice(0, 3)));
29
28
  }).slice(0, 3);
30
- return matchedNames.map(name => pool.find(f => f.value === name) || { label: name, value: name });
29
+ return matchedNames;
31
30
  };
32
31
 
33
32
  // ── checkFrameworkExists ──────────────────────────────────────────────────────
@@ -35,9 +34,8 @@ const fuzzyMatchFramework = (typed, block = null) => {
35
34
  // Returns { exists: boolean, registry: string|null }
36
35
 
37
36
  const checkFrameworkExists = async (name) => {
38
- const known = FRAMEWORK_REGISTRY[name];
39
- if (known && known.package) return { exists: true, registry: known.registry };
40
- if (known && !known.package) return { exists: true, registry: known.registry }; // Laravel/Rails - known but no package
37
+ const known = [...CLIENT_FRAMEWORKS, ...BACKEND_FRAMEWORKS].find(f => f.value === name);
38
+ if (known) return { exists: true, registry: known.registry };
41
39
 
42
40
  const https = require('https');
43
41
  const fetch = (url) => new Promise((resolve, reject) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multi-agents-cli",
3
- "version": "1.1.81",
3
+ "version": "1.1.83",
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",