multi-agents-cli 1.1.82 → 1.1.84
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 +10 -4
- package/lib/data-config.js +39 -61
- package/lib/questions-flow.js +32 -18
- 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,
|
|
@@ -397,12 +396,19 @@ const main = async () => {
|
|
|
397
396
|
for (const blockKey of blockOrder) {
|
|
398
397
|
const block = BLOCK_CONFIG[blockKey];
|
|
399
398
|
block.isActive = true;
|
|
400
|
-
let
|
|
399
|
+
let isBlockEntry = true;
|
|
401
400
|
for (const [segKey, seg] of Object.entries(block)) {
|
|
402
|
-
if (segKey === 'isActive' || typeof seg !== 'object' || !('
|
|
401
|
+
if (segKey === 'isActive' || typeof seg !== 'object' || !('key' in seg)) continue;
|
|
403
402
|
seg.absoluteStep = absoluteStep;
|
|
404
|
-
if (
|
|
403
|
+
if (isBlockEntry) { blockEntries.push(absoluteStep); isBlockEntry = false; }
|
|
405
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
|
+
}
|
|
406
412
|
}
|
|
407
413
|
}
|
|
408
414
|
steps.blockEntries = blockEntries;
|
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,14 +58,14 @@ 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 {
|
|
99
65
|
const https = require('https');
|
|
100
|
-
const fetch = (url) => Promise.race([
|
|
66
|
+
const fetch = (url, headers = {}) => Promise.race([
|
|
101
67
|
new Promise((resolve, reject) => {
|
|
102
|
-
const req = https.get(url, (res) => {
|
|
68
|
+
const req = https.get(url, { headers }, (res) => {
|
|
103
69
|
let data = '';
|
|
104
70
|
res.on('data', chunk => data += chunk);
|
|
105
71
|
res.on('end', () => resolve(data));
|
|
@@ -110,7 +76,7 @@ const fetchLatestVersions = async (frameworkValue) => {
|
|
|
110
76
|
]);
|
|
111
77
|
|
|
112
78
|
if (entry.registry === 'npm') {
|
|
113
|
-
const raw = await fetch(`https://registry.npmjs.org/${entry.package}
|
|
79
|
+
const raw = await fetch(`https://registry.npmjs.org/${entry.package}`, { 'Accept': 'application/vnd.npm.install-v1+json' });
|
|
114
80
|
const json = JSON.parse(raw);
|
|
115
81
|
const versions = Object.keys(json.versions || {})
|
|
116
82
|
.filter(v => /^\d+\.\d+\.\d+$/.test(v) && !v.includes('-'))
|
|
@@ -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');
|
|
@@ -26,8 +26,9 @@ const buildStepDefs = (IDE_CANDIDATES, blockOrder = ['client', 'backend']) => {
|
|
|
26
26
|
name: 'clientFw',
|
|
27
27
|
run: async (machine, answers) => {
|
|
28
28
|
stepHeader(1, 12);
|
|
29
|
-
console.log(`\n${bold(
|
|
30
|
-
|
|
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);
|
|
31
32
|
},
|
|
32
33
|
},
|
|
33
34
|
|
|
@@ -43,7 +44,7 @@ const buildStepDefs = (IDE_CANDIDATES, blockOrder = ['client', 'backend']) => {
|
|
|
43
44
|
versions = cached;
|
|
44
45
|
} else {
|
|
45
46
|
const spinner = createSpinner('Fetching latest versions...');
|
|
46
|
-
versions = await fetchLatestVersions(fwValue(fw)) ||
|
|
47
|
+
versions = await fetchLatestVersions(fwValue(fw)) || CLIENT_FRAMEWORKS.find(f => f.value === fwValue(fw))?.versions?.filter(Boolean) || [];
|
|
47
48
|
spinner.stop();
|
|
48
49
|
}
|
|
49
50
|
if (!versions.length) return null; // skip silently
|
|
@@ -51,8 +52,10 @@ const buildStepDefs = (IDE_CANDIDATES, blockOrder = ['client', 'backend']) => {
|
|
|
51
52
|
label: i === 0 ? `v${v} ${dim('(latest)')}` : `v${v}`,
|
|
52
53
|
value: v,
|
|
53
54
|
}));
|
|
55
|
+
const desc = BLOCK_CONFIG.client.framework.properties.frameworkVersion.desc;
|
|
56
|
+
console.log(bold(cyan(typeof desc === 'function' ? desc(fwValue(fw)) : desc)));
|
|
54
57
|
const label = fwValue(fw) === 'Vite+React' ? '* Vite version:' : `* ${fwValue(fw)} version:`;
|
|
55
|
-
const navOpts = machine.navOptions(
|
|
58
|
+
const navOpts = machine.navOptions(BLOCK_CONFIG.client.framework.properties.frameworkVersion.absoluteStep);
|
|
56
59
|
const idx = await arrowSelect(label, [
|
|
57
60
|
...versionChoices,
|
|
58
61
|
...navOpts.map(n => ({ label: n.label })),
|
|
@@ -69,8 +72,9 @@ const buildStepDefs = (IDE_CANDIDATES, blockOrder = ['client', 'backend']) => {
|
|
|
69
72
|
name: 'clientState',
|
|
70
73
|
run: async (machine, answers) => {
|
|
71
74
|
stepHeader(3, 12);
|
|
75
|
+
console.log(bold(cyan(BLOCK_CONFIG.client.framework.properties.stateManagement.desc)));
|
|
72
76
|
const opts = STATE_OPTIONS[fwValue(answers.clientFw)] || [];
|
|
73
|
-
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);
|
|
74
78
|
},
|
|
75
79
|
},
|
|
76
80
|
|
|
@@ -79,8 +83,9 @@ const buildStepDefs = (IDE_CANDIDATES, blockOrder = ['client', 'backend']) => {
|
|
|
79
83
|
name: 'clientUi',
|
|
80
84
|
run: async (machine, answers) => {
|
|
81
85
|
stepHeader(4, 12);
|
|
86
|
+
console.log(bold(cyan(BLOCK_CONFIG.client.framework.properties.uiLibrary.desc)));
|
|
82
87
|
const opts = UI_OPTIONS[fwValue(answers.clientFw)] || [];
|
|
83
|
-
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);
|
|
84
89
|
},
|
|
85
90
|
},
|
|
86
91
|
|
|
@@ -89,7 +94,9 @@ const buildStepDefs = (IDE_CANDIDATES, blockOrder = ['client', 'backend']) => {
|
|
|
89
94
|
name: 'clientStyle',
|
|
90
95
|
run: async (machine, answers) => {
|
|
91
96
|
stepHeader(5, 12);
|
|
92
|
-
|
|
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);
|
|
93
100
|
},
|
|
94
101
|
},
|
|
95
102
|
|
|
@@ -102,7 +109,7 @@ const buildStepDefs = (IDE_CANDIDATES, blockOrder = ['client', 'backend']) => {
|
|
|
102
109
|
if (!fw || !fw.integratedBackend) return false; // skip — not applicable
|
|
103
110
|
|
|
104
111
|
separator();
|
|
105
|
-
console.log(`\n${bold(
|
|
112
|
+
console.log(`\n${bold('BACKEND CONFIGURATION')}`);
|
|
106
113
|
console.log(dim(` ${fwValue(fw)} supports server-side rendering and API routes.\n`));
|
|
107
114
|
const confirmed = await arrowConfirm(`Use integrated backend (${fwValue(fw)} API routes/SSR) instead of a separate backend?`);
|
|
108
115
|
if (confirmed) console.log(dim(`\n Using ${fwValue(fw)} integrated backend. No separate backend needed.\n`));
|
|
@@ -122,10 +129,11 @@ const buildStepDefs = (IDE_CANDIDATES, blockOrder = ['client', 'backend']) => {
|
|
|
122
129
|
stepHeader(7, 12);
|
|
123
130
|
|
|
124
131
|
separator();
|
|
125
|
-
console.log(`\n${bold(
|
|
132
|
+
console.log(`\n${bold('BACKEND CONFIGURATION')}`);
|
|
126
133
|
console.log(dim(' You can skip the backend framework and decide later.\n'));
|
|
134
|
+
console.log(bold(yellow(BLOCK_CONFIG.backend.framework.desc)));
|
|
127
135
|
|
|
128
|
-
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);
|
|
129
137
|
},
|
|
130
138
|
},
|
|
131
139
|
|
|
@@ -143,7 +151,7 @@ const buildStepDefs = (IDE_CANDIDATES, blockOrder = ['client', 'backend']) => {
|
|
|
143
151
|
versions = cached;
|
|
144
152
|
} else {
|
|
145
153
|
const spinner = createSpinner('Fetching latest versions...');
|
|
146
|
-
versions = await fetchLatestVersions(fwValue(fwObj)) ||
|
|
154
|
+
versions = await fetchLatestVersions(fwValue(fwObj)) || BACKEND_FRAMEWORKS.find(f => f.value === fwValue(fwObj))?.versions?.filter(Boolean) || [];
|
|
147
155
|
spinner.stop();
|
|
148
156
|
}
|
|
149
157
|
if (!versions.length) return null;
|
|
@@ -152,7 +160,9 @@ const buildStepDefs = (IDE_CANDIDATES, blockOrder = ['client', 'backend']) => {
|
|
|
152
160
|
label: i === 0 ? `v${v} ${dim('(latest)')}` : `v${v}`,
|
|
153
161
|
value: v,
|
|
154
162
|
}));
|
|
155
|
-
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);
|
|
156
166
|
const idx = await arrowSelect(`* ${fwValue(fwObj)} version:`, [
|
|
157
167
|
...vChoices,
|
|
158
168
|
...navOpts.map(n => ({ label: n.label })),
|
|
@@ -170,7 +180,8 @@ const buildStepDefs = (IDE_CANDIDATES, blockOrder = ['client', 'backend']) => {
|
|
|
170
180
|
run: async (machine, answers) => {
|
|
171
181
|
if (!answers.backendFw) return null;
|
|
172
182
|
stepHeader(9, 12);
|
|
173
|
-
|
|
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);
|
|
174
185
|
},
|
|
175
186
|
},
|
|
176
187
|
|
|
@@ -181,10 +192,12 @@ const buildStepDefs = (IDE_CANDIDATES, blockOrder = ['client', 'backend']) => {
|
|
|
181
192
|
const { backendFw, backendDb } = answers;
|
|
182
193
|
if (!backendFw || !backendDb) return null;
|
|
183
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)));
|
|
184
197
|
const byDb = ORM_OPTIONS_BY_DB[backendDb] || [];
|
|
185
198
|
const byFw = ORM_OPTIONS[fwValue(backendFw)] || [];
|
|
186
199
|
const ormChoices = byDb.length && byFw.length ? byDb.filter(o => byFw.includes(o)) : byDb.length ? byDb : byFw;
|
|
187
|
-
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);
|
|
188
201
|
},
|
|
189
202
|
},
|
|
190
203
|
|
|
@@ -195,7 +208,8 @@ const buildStepDefs = (IDE_CANDIDATES, blockOrder = ['client', 'backend']) => {
|
|
|
195
208
|
const { backendFw } = answers;
|
|
196
209
|
if (!backendFw) return null;
|
|
197
210
|
stepHeader(11, 12);
|
|
198
|
-
|
|
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);
|
|
199
213
|
},
|
|
200
214
|
},
|
|
201
215
|
|
|
@@ -209,7 +223,7 @@ const buildStepDefs = (IDE_CANDIDATES, blockOrder = ['client', 'backend']) => {
|
|
|
209
223
|
run: async (machine, answers) => {
|
|
210
224
|
stepHeader(12, 12);
|
|
211
225
|
separator();
|
|
212
|
-
console.log(`\n${bold(
|
|
226
|
+
console.log(`\n${bold(red('ENVIRONMENT'))}`);
|
|
213
227
|
|
|
214
228
|
const osName = { darwin: 'macOS', win32: 'Windows', linux: 'Linux' }[process.platform] || process.platform;
|
|
215
229
|
console.log(`\n ${dim('OS detected:')} ${bold(osName)}`);
|
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