multi-agents-cli 1.1.65 → 1.1.67
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 +1 -1
- package/lib/questions-flow.js +9 -9
- package/lib/ui.js +61 -7
- package/lib/validate.js +73 -0
- package/package.json +1 -1
- /package/lib/{questions.js → data-config.js} +0 -0
package/init.js
CHANGED
package/lib/questions-flow.js
CHANGED
|
@@ -8,7 +8,7 @@ const {
|
|
|
8
8
|
CLIENT_FRAMEWORKS, BACKEND_FRAMEWORKS, FRAMEWORK_VERSION_FALLBACK,
|
|
9
9
|
fetchLatestVersions, STATE_OPTIONS, UI_OPTIONS, STYLING_OPTIONS,
|
|
10
10
|
DB_OPTIONS, ORM_OPTIONS_BY_DB, ORM_OPTIONS, AUTH_OPTIONS,
|
|
11
|
-
} = require('./
|
|
11
|
+
} = require('./data-config');
|
|
12
12
|
const { buildIDEOptions, verifyIDE, detectTerminal } = require('./detect');
|
|
13
13
|
|
|
14
14
|
// ── Step definitions ──────────────────────────────────────────────────────────
|
|
@@ -23,7 +23,7 @@ const buildStepDefs = (IDE_CANDIDATES) => [
|
|
|
23
23
|
run: async (machine, answers) => {
|
|
24
24
|
stepHeader(1, 12);
|
|
25
25
|
console.log(`\n${bold(blue('Client configuration'))}`);
|
|
26
|
-
return await selectRequired('* Client framework (required):', CLIENT_FRAMEWORKS, machine, 0);
|
|
26
|
+
return await selectRequired('* Client framework (required):', CLIENT_FRAMEWORKS, machine, 0, answers);
|
|
27
27
|
},
|
|
28
28
|
},
|
|
29
29
|
|
|
@@ -60,7 +60,7 @@ const buildStepDefs = (IDE_CANDIDATES) => [
|
|
|
60
60
|
run: async (machine, answers) => {
|
|
61
61
|
stepHeader(3, 12);
|
|
62
62
|
const opts = STATE_OPTIONS[answers.clientFw.value] || [];
|
|
63
|
-
return await selectOptional('State management:', opts, machine, 2);
|
|
63
|
+
return await selectOptional('State management:', opts, machine, 2, answers);
|
|
64
64
|
},
|
|
65
65
|
},
|
|
66
66
|
|
|
@@ -70,7 +70,7 @@ const buildStepDefs = (IDE_CANDIDATES) => [
|
|
|
70
70
|
run: async (machine, answers) => {
|
|
71
71
|
stepHeader(4, 12);
|
|
72
72
|
const opts = UI_OPTIONS[answers.clientFw.value] || [];
|
|
73
|
-
return await selectOptional('UI library:', opts, machine, 3);
|
|
73
|
+
return await selectOptional('UI library:', opts, machine, 3, answers);
|
|
74
74
|
},
|
|
75
75
|
},
|
|
76
76
|
|
|
@@ -79,7 +79,7 @@ const buildStepDefs = (IDE_CANDIDATES) => [
|
|
|
79
79
|
name: 'clientStyle',
|
|
80
80
|
run: async (machine, answers) => {
|
|
81
81
|
stepHeader(5, 12);
|
|
82
|
-
return await selectOptional('Styling:', STYLING_OPTIONS, machine, 4);
|
|
82
|
+
return await selectOptional('Styling:', STYLING_OPTIONS, machine, 4, answers);
|
|
83
83
|
},
|
|
84
84
|
},
|
|
85
85
|
|
|
@@ -157,7 +157,7 @@ const buildStepDefs = (IDE_CANDIDATES) => [
|
|
|
157
157
|
run: async (machine, answers) => {
|
|
158
158
|
stepHeader(9, 12);
|
|
159
159
|
if (!answers.backendFwObj) return null;
|
|
160
|
-
return await selectOptional('Database type:', DB_OPTIONS, machine, 8);
|
|
160
|
+
return await selectOptional('Database type:', DB_OPTIONS, machine, 8, answers);
|
|
161
161
|
},
|
|
162
162
|
},
|
|
163
163
|
|
|
@@ -169,7 +169,7 @@ const buildStepDefs = (IDE_CANDIDATES) => [
|
|
|
169
169
|
const { backendFwObj, backendDb } = answers;
|
|
170
170
|
if (!backendFwObj || !backendDb) return null;
|
|
171
171
|
const ormChoices = ORM_OPTIONS_BY_DB[backendDb] || ORM_OPTIONS[backendFwObj.value] || [];
|
|
172
|
-
return await selectOptional('ORM / query layer:', ormChoices, machine, 9);
|
|
172
|
+
return await selectOptional('ORM / query layer:', ormChoices, machine, 9, answers);
|
|
173
173
|
},
|
|
174
174
|
},
|
|
175
175
|
|
|
@@ -180,7 +180,7 @@ const buildStepDefs = (IDE_CANDIDATES) => [
|
|
|
180
180
|
stepHeader(11, 12);
|
|
181
181
|
const { backendFwObj } = answers;
|
|
182
182
|
if (!backendFwObj) return null;
|
|
183
|
-
return await selectOptional('Auth strategy:', AUTH_OPTIONS[backendFwObj.value] || [], machine, 10);
|
|
183
|
+
return await selectOptional('Auth strategy:', AUTH_OPTIONS[backendFwObj.value] || [], machine, 10, answers);
|
|
184
184
|
},
|
|
185
185
|
},
|
|
186
186
|
|
|
@@ -206,7 +206,7 @@ const buildStepDefs = (IDE_CANDIDATES) => [
|
|
|
206
206
|
else console.log(`\n ${'\x1b[33m'}No IDEs detected.${'\x1b[0m'}\n`);
|
|
207
207
|
|
|
208
208
|
while (true) {
|
|
209
|
-
const ideChoice = await selectRequired('* IDE / editor (required):', sorted, machine, 11);
|
|
209
|
+
const ideChoice = await selectRequired('* IDE / editor (required):', sorted, machine, 11, answers);
|
|
210
210
|
if (ideChoice === RESTART || ideChoice === BACK) return ideChoice;
|
|
211
211
|
|
|
212
212
|
if (ideChoice.cmd && !ideChoice.detected) {
|
package/lib/ui.js
CHANGED
|
@@ -121,36 +121,90 @@ const stepHeader = (stepIndex, totalSteps) => {
|
|
|
121
121
|
// ── Step selection helpers ────────────────────────────────────────────────────
|
|
122
122
|
|
|
123
123
|
const { BACK, RESTART } = require('./steps');
|
|
124
|
+
const { validateCombination } = require('./validate');
|
|
124
125
|
|
|
125
|
-
const
|
|
126
|
+
const NONE_LABEL = '→ None of these - I\'ll specify my own';
|
|
127
|
+
|
|
128
|
+
const selectRequired = async (prompt, items, stepMachine, stepIndex, context = {}) => {
|
|
126
129
|
const isFirstStep = stepIndex <= 1;
|
|
127
130
|
const navOpts = stepMachine ? stepMachine.navOptions(stepIndex, isFirstStep) : [];
|
|
128
|
-
const
|
|
131
|
+
const noneOpt = { label: NONE_LABEL };
|
|
132
|
+
const allItems = [...items, noneOpt, ...navOpts];
|
|
129
133
|
|
|
130
134
|
const idx = await arrowSelect(prompt, allItems.map(i => ({ label: typeof i === 'string' ? i : i.label })));
|
|
131
135
|
|
|
132
|
-
|
|
133
|
-
|
|
136
|
+
// None of these - free text input
|
|
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
|
+
}
|
|
159
|
+
|
|
160
|
+
if (idx > items.length) {
|
|
161
|
+
const nav = navOpts[idx - items.length - 1];
|
|
134
162
|
return nav ? nav.value : RESTART;
|
|
135
163
|
}
|
|
136
164
|
return items[idx];
|
|
137
165
|
};
|
|
138
166
|
|
|
139
|
-
const selectOptional = async (prompt, items, stepMachine, stepIndex) => {
|
|
167
|
+
const selectOptional = async (prompt, items, stepMachine, stepIndex, context = {}) => {
|
|
140
168
|
if (!items || items.length === 0) return null;
|
|
141
169
|
const isFirstStep = stepIndex <= 1;
|
|
142
170
|
const navOpts = stepMachine ? stepMachine.navOptions(stepIndex, isFirstStep) : [];
|
|
143
171
|
const choices = [
|
|
144
172
|
...items.map(i => ({ label: typeof i === 'string' ? i : i.label })),
|
|
145
173
|
{ label: dim('Skip (agent will propose when needed)') },
|
|
174
|
+
{ label: NONE_LABEL },
|
|
146
175
|
...navOpts.map(n => ({ label: n.label })),
|
|
147
176
|
];
|
|
148
177
|
|
|
149
178
|
const idx = await arrowSelect(prompt, choices);
|
|
150
179
|
|
|
151
180
|
if (idx === items.length) return null; // skip
|
|
152
|
-
|
|
153
|
-
|
|
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
|
+
}
|
|
205
|
+
|
|
206
|
+
if (idx > items.length + 1) {
|
|
207
|
+
const nav = navOpts[idx - items.length - 2];
|
|
154
208
|
return nav ? nav.value : RESTART;
|
|
155
209
|
}
|
|
156
210
|
return typeof items[idx] === 'string' ? items[idx] : items[idx].value;
|
package/lib/validate.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ── Compatibility matrix ──────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
const PYTHON_BACKENDS = ['FastAPI', 'Django'];
|
|
6
|
+
const NODE_BACKENDS = ['Express', 'NestJS', 'Fastify'];
|
|
7
|
+
const NODE_ONLY_ORMS = ['Prisma', 'TypeORM', 'Drizzle', 'Sequelize', 'MikroORM', 'raw pg driver', 'raw mysql2 driver', 'better-sqlite3'];
|
|
8
|
+
const PYTHON_ONLY_ORMS = ['SQLAlchemy', 'Tortoise ORM', 'Django ORM (built-in)'];
|
|
9
|
+
const MONGO_ONLY_ORMS = ['Beanie (MongoDB)'];
|
|
10
|
+
const NEXTJS_ONLY_LIBS = ['shadcn/ui', 'Radix UI'];
|
|
11
|
+
const MONGO_DBS = ['MongoDB'];
|
|
12
|
+
|
|
13
|
+
// ── validateCombination ───────────────────────────────────────────────────────
|
|
14
|
+
// chosen — the value just entered (string)
|
|
15
|
+
// context — answers accumulated so far { clientFw, backendFwObj, backendDb, ... }
|
|
16
|
+
// returns { valid, reason, alternatives }
|
|
17
|
+
|
|
18
|
+
const validateCombination = (chosen, context = {}) => {
|
|
19
|
+
const clientFw = context.clientFw?.value || context.clientFw || null;
|
|
20
|
+
const backendFw = context.backendFwObj?.value || context.backendFwObj || null;
|
|
21
|
+
const backendDb = context.backendDb || null;
|
|
22
|
+
|
|
23
|
+
// Node.js ORM + Python backend
|
|
24
|
+
if (NODE_ONLY_ORMS.includes(chosen) && PYTHON_BACKENDS.includes(backendFw)) {
|
|
25
|
+
return {
|
|
26
|
+
valid: false,
|
|
27
|
+
reason: `${chosen} is a Node.js ORM and does not support ${backendFw} (Python).`,
|
|
28
|
+
alternatives: backendFw === 'Django'
|
|
29
|
+
? ['Django ORM (built-in)', 'SQLAlchemy']
|
|
30
|
+
: ['SQLAlchemy', 'Tortoise ORM', 'raw asyncpg'],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Python ORM + Node.js backend
|
|
35
|
+
if (PYTHON_ONLY_ORMS.includes(chosen) && NODE_BACKENDS.includes(backendFw)) {
|
|
36
|
+
return {
|
|
37
|
+
valid: false,
|
|
38
|
+
reason: `${chosen} is a Python ORM and does not support ${backendFw} (Node.js).`,
|
|
39
|
+
alternatives: ['Prisma', 'TypeORM', 'Drizzle'],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Beanie + non-MongoDB
|
|
44
|
+
if (MONGO_ONLY_ORMS.includes(chosen) && backendDb && !MONGO_DBS.includes(backendDb)) {
|
|
45
|
+
return {
|
|
46
|
+
valid: false,
|
|
47
|
+
reason: `Beanie is a MongoDB ODM and does not support ${backendDb}.`,
|
|
48
|
+
alternatives: ['Prisma', 'TypeORM', 'Drizzle'],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Next.js-only UI libs + Vite+React
|
|
53
|
+
if (NEXTJS_ONLY_LIBS.includes(chosen) && clientFw === 'Vite+React') {
|
|
54
|
+
return {
|
|
55
|
+
valid: false,
|
|
56
|
+
reason: `${chosen} is optimized for Next.js and may not work correctly with Vite+React.`,
|
|
57
|
+
alternatives: ['MUI', 'Chakra UI', 'Ant Design'],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Ambiguous cross-boundary (e.g. dotnet as both client and backend)
|
|
62
|
+
if (chosen && backendFw && chosen.toLowerCase() === backendFw.toLowerCase()) {
|
|
63
|
+
return {
|
|
64
|
+
valid: false,
|
|
65
|
+
reason: `${chosen} appears in both client and backend selections - this may cause scope ambiguity.`,
|
|
66
|
+
alternatives: [],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { valid: true, reason: null, alternatives: [] };
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
module.exports = { validateCombination };
|
package/package.json
CHANGED
|
File without changes
|