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 +11 -5
- package/lib/data-config.js +36 -58
- package/lib/questions-flow.js +49 -20
- package/lib/steps.js +17 -4
- package/lib/ui.js +19 -6
- package/lib/validate.js +7 -9
- package/package.json +1 -1
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
|
|
399
|
+
let isBlockEntry = true;
|
|
402
400
|
for (const [segKey, seg] of Object.entries(block)) {
|
|
403
|
-
if (segKey === 'isActive' || typeof seg !== 'object' || !('
|
|
401
|
+
if (segKey === 'isActive' || typeof seg !== 'object' || !('key' in seg)) continue;
|
|
404
402
|
seg.absoluteStep = absoluteStep;
|
|
405
|
-
if (
|
|
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);
|
package/lib/data-config.js
CHANGED
|
@@ -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',
|
|
40
|
-
{ label: 'Express',
|
|
41
|
-
{ label: 'Fastify',
|
|
42
|
-
{ label: 'Django',
|
|
43
|
-
{ label: 'FastAPI',
|
|
44
|
-
{ label: 'Laravel',
|
|
45
|
-
{ label: 'Ruby on Rails', value: 'Rails',
|
|
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 =
|
|
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:
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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:
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
341
|
-
FRAMEWORK_VERSION_FALLBACK,
|
|
319
|
+
|
|
342
320
|
fetchLatestVersions,
|
|
343
321
|
getVersionCache,
|
|
344
322
|
prefetchVersions,
|
package/lib/questions-flow.js
CHANGED
|
@@ -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,
|
|
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(
|
|
29
|
-
|
|
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)) ||
|
|
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(
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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,
|
|
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)) ||
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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
|
-
//
|
|
51
|
-
navOptions(stepIndex
|
|
50
|
+
// Build navigation options — back is suppressed at block entry points
|
|
51
|
+
navOptions(stepIndex) {
|
|
52
52
|
const opts = [];
|
|
53
53
|
|
|
54
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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 =
|
|
26
|
-
const n =
|
|
27
|
-
return
|
|
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
|
|
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 =
|
|
39
|
-
if (known
|
|
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