multi-agents-cli 1.1.67 → 1.1.69
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/lib/questions-flow.js +3 -1
- package/lib/ui.js +71 -48
- package/lib/validate.js +58 -5
- package/package.json +1 -1
package/lib/questions-flow.js
CHANGED
|
@@ -168,7 +168,9 @@ const buildStepDefs = (IDE_CANDIDATES) => [
|
|
|
168
168
|
stepHeader(10, 12);
|
|
169
169
|
const { backendFwObj, backendDb } = answers;
|
|
170
170
|
if (!backendFwObj || !backendDb) return null;
|
|
171
|
-
const
|
|
171
|
+
const byDb = ORM_OPTIONS_BY_DB[backendDb] || [];
|
|
172
|
+
const byFw = ORM_OPTIONS[backendFwObj.value] || [];
|
|
173
|
+
const ormChoices = byDb.length && byFw.length ? byDb.filter(o => byFw.includes(o)) : byDb.length ? byDb : byFw;
|
|
172
174
|
return await selectOptional('ORM / query layer:', ormChoices, machine, 9, answers);
|
|
173
175
|
},
|
|
174
176
|
},
|
package/lib/ui.js
CHANGED
|
@@ -121,10 +121,78 @@ const stepHeader = (stepIndex, totalSteps) => {
|
|
|
121
121
|
// ── Step selection helpers ────────────────────────────────────────────────────
|
|
122
122
|
|
|
123
123
|
const { BACK, RESTART } = require('./steps');
|
|
124
|
-
const { validateCombination } = require('./validate');
|
|
124
|
+
const { validateCombination, checkFrameworkExists, fuzzyMatchFramework } = require('./validate');
|
|
125
125
|
|
|
126
126
|
const NONE_LABEL = '→ None of these - I\'ll specify my own';
|
|
127
127
|
|
|
128
|
+
// ── Free-text handler (shared by selectRequired + selectOptional) ────────────
|
|
129
|
+
|
|
130
|
+
const handleFreeText = async (prompt, items, stepMachine, stepIndex, context, isOptional) => {
|
|
131
|
+
const _res = await prompts({ type: 'text', name: 'value', message: 'Type your own value:' }, { onCancel: () => process.exit(0) });
|
|
132
|
+
const typed = (_res.value || '').trim();
|
|
133
|
+
if (!typed) return isOptional ? null : handleFreeText(prompt, items, stepMachine, stepIndex, context, isOptional);
|
|
134
|
+
|
|
135
|
+
// Fuzzy match first - before registry check
|
|
136
|
+
const matches = fuzzyMatchFramework(typed);
|
|
137
|
+
if (matches.length > 0) {
|
|
138
|
+
const didYouMeanOpts = [
|
|
139
|
+
...matches.map(m => `→ ${m.label || m}`),
|
|
140
|
+
`→ Type a different value`,
|
|
141
|
+
];
|
|
142
|
+
const choice = await arrowSelect(`"${typed}" couldn't be verified. Did you mean:`, didYouMeanOpts.map(o => ({ label: o })));
|
|
143
|
+
if (choice < matches.length) return matches[choice]; // returns full framework object
|
|
144
|
+
return handleFreeText(prompt, items, stepMachine, stepIndex, context, isOptional);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Live registry check - only when no fuzzy match
|
|
148
|
+
console.log(dim(' Verifying...'));
|
|
149
|
+
const existence = await checkFrameworkExists(typed);
|
|
150
|
+
|
|
151
|
+
if (!existence.exists) {
|
|
152
|
+
console.log('');
|
|
153
|
+
const riskOpts = [
|
|
154
|
+
`→ Proceed with "${typed}" - I accept the risks`,
|
|
155
|
+
`→ Type a different value`,
|
|
156
|
+
];
|
|
157
|
+
const choice = await arrowSelect(`"${typed}" couldn't be verified.`, riskOpts.map(o => ({ label: o })));
|
|
158
|
+
if (choice === 1) return handleFreeText(prompt, items, stepMachine, stepIndex, context, isOptional);
|
|
159
|
+
// Show risk consequence block
|
|
160
|
+
console.log('');
|
|
161
|
+
console.log(` ${yellow('⚠ Proceeding with an unverified framework may result in:')}`);
|
|
162
|
+
console.log(dim(' · Agent spending excessive tokens resolving unknown setup'));
|
|
163
|
+
console.log(dim(' · Incorrect scaffold structure for this framework'));
|
|
164
|
+
console.log(dim(' · Missing contracts and integration points'));
|
|
165
|
+
console.log(dim(' · Potential mid-flow failures requiring full restart'));
|
|
166
|
+
console.log('');
|
|
167
|
+
const confirm = await arrowSelect('Confirm you understand and want to continue?', [
|
|
168
|
+
{ label: `→ Yes - proceed with "${typed}"` },
|
|
169
|
+
{ label: `→ No - let me pick something else` },
|
|
170
|
+
]);
|
|
171
|
+
if (confirm === 1) return handleFreeText(prompt, items, stepMachine, stepIndex, context, isOptional);
|
|
172
|
+
return typed;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Valid + verified - check compatibility
|
|
176
|
+
const check = validateCombination(typed, context);
|
|
177
|
+
if (!check.valid) {
|
|
178
|
+
console.log(`
|
|
179
|
+
${yellow('⚠')} ${check.reason}
|
|
180
|
+
`);
|
|
181
|
+
const opts = [
|
|
182
|
+
'→ Continue - I understand the tradeoff',
|
|
183
|
+
...check.alternatives.map(a => `→ ${a}`),
|
|
184
|
+
'→ Type a different value',
|
|
185
|
+
];
|
|
186
|
+
const choice = await arrowSelect('How would you like to proceed?', opts.map(o => ({ label: o })));
|
|
187
|
+
if (choice === 0) return typed;
|
|
188
|
+
if (choice === opts.length - 1) return handleFreeText(prompt, items, stepMachine, stepIndex, context, isOptional);
|
|
189
|
+
return check.alternatives[choice - 1];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
console.log(dim(' Custom value - the agent will handle compatibility at runtime.'));
|
|
193
|
+
return typed;
|
|
194
|
+
};
|
|
195
|
+
|
|
128
196
|
const selectRequired = async (prompt, items, stepMachine, stepIndex, context = {}) => {
|
|
129
197
|
const isFirstStep = stepIndex <= 1;
|
|
130
198
|
const navOpts = stepMachine ? stepMachine.navOptions(stepIndex, isFirstStep) : [];
|
|
@@ -133,29 +201,7 @@ const selectRequired = async (prompt, items, stepMachine, stepIndex, context = {
|
|
|
133
201
|
|
|
134
202
|
const idx = await arrowSelect(prompt, allItems.map(i => ({ label: typeof i === 'string' ? i : i.label })));
|
|
135
203
|
|
|
136
|
-
|
|
137
|
-
if (idx === items.length) {
|
|
138
|
-
const _res = await prompts({ type: 'text', name: 'value', message: 'Type your own value:' }, { onCancel: () => process.exit(0) });
|
|
139
|
-
const typed = (_res.value || '').trim();
|
|
140
|
-
if (!typed) return null;
|
|
141
|
-
const check = validateCombination(typed, context);
|
|
142
|
-
if (!check.valid) {
|
|
143
|
-
console.log(`
|
|
144
|
-
${yellow('⚠')} ${check.reason}
|
|
145
|
-
`);
|
|
146
|
-
const opts = [
|
|
147
|
-
'→ Continue - I understand the tradeoff',
|
|
148
|
-
...check.alternatives.map(a => `→ ${a}`),
|
|
149
|
-
'→ Type a different value',
|
|
150
|
-
];
|
|
151
|
-
const choice = await arrowSelect('How would you like to proceed?', opts.map(o => ({ label: o })));
|
|
152
|
-
if (choice === 0) return typed;
|
|
153
|
-
if (choice === opts.length - 1) return selectRequired(prompt, items, stepMachine, stepIndex, context);
|
|
154
|
-
return check.alternatives[choice - 1];
|
|
155
|
-
}
|
|
156
|
-
console.log(dim(' Custom value - the agent will handle compatibility at runtime.'));
|
|
157
|
-
return typed;
|
|
158
|
-
}
|
|
204
|
+
if (idx === items.length) return handleFreeText(prompt, items, stepMachine, stepIndex, context, false);
|
|
159
205
|
|
|
160
206
|
if (idx > items.length) {
|
|
161
207
|
const nav = navOpts[idx - items.length - 1];
|
|
@@ -178,30 +224,7 @@ const selectOptional = async (prompt, items, stepMachine, stepIndex, context = {
|
|
|
178
224
|
const idx = await arrowSelect(prompt, choices);
|
|
179
225
|
|
|
180
226
|
if (idx === items.length) return null; // skip
|
|
181
|
-
|
|
182
|
-
// None of these - free text input
|
|
183
|
-
if (idx === items.length + 1) {
|
|
184
|
-
const _res = await prompts({ type: 'text', name: 'value', message: 'Type your own value:' }, { onCancel: () => process.exit(0) });
|
|
185
|
-
const typed = (_res.value || '').trim();
|
|
186
|
-
if (!typed) return null;
|
|
187
|
-
const check = validateCombination(typed, context);
|
|
188
|
-
if (!check.valid) {
|
|
189
|
-
console.log(`
|
|
190
|
-
${yellow('⚠')} ${check.reason}
|
|
191
|
-
`);
|
|
192
|
-
const opts = [
|
|
193
|
-
'→ Continue - I understand the tradeoff',
|
|
194
|
-
...check.alternatives.map(a => `→ ${a}`),
|
|
195
|
-
'→ Type a different value',
|
|
196
|
-
];
|
|
197
|
-
const choice = await arrowSelect('How would you like to proceed?', opts.map(o => ({ label: o })));
|
|
198
|
-
if (choice === 0) return typed;
|
|
199
|
-
if (choice === opts.length - 1) return selectOptional(prompt, items, stepMachine, stepIndex, context);
|
|
200
|
-
return check.alternatives[choice - 1];
|
|
201
|
-
}
|
|
202
|
-
console.log(dim(' Custom value - the agent will handle compatibility at runtime.'));
|
|
203
|
-
return typed;
|
|
204
|
-
}
|
|
227
|
+
if (idx === items.length + 1) return handleFreeText(prompt, items, stepMachine, stepIndex, context, true);
|
|
205
228
|
|
|
206
229
|
if (idx > items.length + 1) {
|
|
207
230
|
const nav = navOpts[idx - items.length - 2];
|
package/lib/validate.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { FRAMEWORK_REGISTRY, CLIENT_FRAMEWORKS, BACKEND_FRAMEWORKS } = require('./data-config');
|
|
4
|
+
|
|
5
|
+
const ALL_FRAMEWORKS = [...CLIENT_FRAMEWORKS, ...BACKEND_FRAMEWORKS];
|
|
6
|
+
|
|
3
7
|
// ── Compatibility matrix ──────────────────────────────────────────────────────
|
|
4
8
|
|
|
5
9
|
const PYTHON_BACKENDS = ['FastAPI', 'Django'];
|
|
@@ -10,15 +14,64 @@ const MONGO_ONLY_ORMS = ['Beanie (MongoDB)'];
|
|
|
10
14
|
const NEXTJS_ONLY_LIBS = ['shadcn/ui', 'Radix UI'];
|
|
11
15
|
const MONGO_DBS = ['MongoDB'];
|
|
12
16
|
|
|
17
|
+
// ── fuzzyMatchFramework ───────────────────────────────────────────────────────
|
|
18
|
+
// Returns up to 3 known framework names that loosely match the typed value
|
|
19
|
+
|
|
20
|
+
const fuzzyMatchFramework = (typed) => {
|
|
21
|
+
const lower = typed.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
22
|
+
const matchedNames = Object.keys(FRAMEWORK_REGISTRY).filter(name => {
|
|
23
|
+
const n = name.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
24
|
+
return n.includes(lower) || lower.includes(n) || (lower.length >= 3 && n.startsWith(lower.slice(0, 3)));
|
|
25
|
+
}).slice(0, 3);
|
|
26
|
+
return matchedNames.map(name => ALL_FRAMEWORKS.find(f => f.value === name) || { label: name, value: name });
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// ── checkFrameworkExists ──────────────────────────────────────────────────────
|
|
30
|
+
// Checks npm then pypi for the typed framework name
|
|
31
|
+
// Returns { exists: boolean, registry: string|null }
|
|
32
|
+
|
|
33
|
+
const checkFrameworkExists = async (name) => {
|
|
34
|
+
const known = FRAMEWORK_REGISTRY[name];
|
|
35
|
+
if (known && known.package) return { exists: true, registry: known.registry };
|
|
36
|
+
if (known && !known.package) return { exists: true, registry: known.registry }; // Laravel/Rails - known but no package
|
|
37
|
+
|
|
38
|
+
const https = require('https');
|
|
39
|
+
const fetch = (url) => new Promise((resolve, reject) => {
|
|
40
|
+
const req = https.get(url, { timeout: 4000 }, (res) => {
|
|
41
|
+
let data = '';
|
|
42
|
+
res.on('data', chunk => data += chunk);
|
|
43
|
+
res.on('end', () => resolve({ status: res.statusCode, data }));
|
|
44
|
+
});
|
|
45
|
+
req.on('error', reject);
|
|
46
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const slug = name.toLowerCase().replace(/\s+/g, '-');
|
|
50
|
+
|
|
51
|
+
// Try npm
|
|
52
|
+
try {
|
|
53
|
+
const res = await fetch(`https://registry.npmjs.org/${slug}`);
|
|
54
|
+
if (res.status === 200) return { exists: true, registry: 'npm' };
|
|
55
|
+
} catch {}
|
|
56
|
+
|
|
57
|
+
// Try pypi
|
|
58
|
+
try {
|
|
59
|
+
const res = await fetch(`https://pypi.org/pypi/${slug}/json`);
|
|
60
|
+
if (res.status === 200) return { exists: true, registry: 'pypi' };
|
|
61
|
+
} catch {}
|
|
62
|
+
|
|
63
|
+
return { exists: false, registry: null };
|
|
64
|
+
};
|
|
65
|
+
|
|
13
66
|
// ── validateCombination ───────────────────────────────────────────────────────
|
|
14
67
|
// chosen — the value just entered (string)
|
|
15
68
|
// context — answers accumulated so far { clientFw, backendFwObj, backendDb, ... }
|
|
16
69
|
// returns { valid, reason, alternatives }
|
|
17
70
|
|
|
18
71
|
const validateCombination = (chosen, context = {}) => {
|
|
19
|
-
const clientFw
|
|
20
|
-
const backendFw
|
|
21
|
-
const backendDb
|
|
72
|
+
const clientFw = context.clientFw?.value || context.clientFw || null;
|
|
73
|
+
const backendFw = context.backendFwObj?.value || context.backendFwObj || null;
|
|
74
|
+
const backendDb = context.backendDb || null;
|
|
22
75
|
|
|
23
76
|
// Node.js ORM + Python backend
|
|
24
77
|
if (NODE_ONLY_ORMS.includes(chosen) && PYTHON_BACKENDS.includes(backendFw)) {
|
|
@@ -58,7 +111,7 @@ const validateCombination = (chosen, context = {}) => {
|
|
|
58
111
|
};
|
|
59
112
|
}
|
|
60
113
|
|
|
61
|
-
// Ambiguous cross-boundary
|
|
114
|
+
// Ambiguous cross-boundary
|
|
62
115
|
if (chosen && backendFw && chosen.toLowerCase() === backendFw.toLowerCase()) {
|
|
63
116
|
return {
|
|
64
117
|
valid: false,
|
|
@@ -70,4 +123,4 @@ const validateCombination = (chosen, context = {}) => {
|
|
|
70
123
|
return { valid: true, reason: null, alternatives: [] };
|
|
71
124
|
};
|
|
72
125
|
|
|
73
|
-
module.exports = { validateCombination };
|
|
126
|
+
module.exports = { validateCombination, checkFrameworkExists, fuzzyMatchFramework };
|
package/package.json
CHANGED