mustflow 2.58.0 → 2.58.1
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.
|
@@ -3,6 +3,7 @@ import { printUsageError, renderCliError, renderHelp } from '../lib/cli-output.j
|
|
|
3
3
|
import { writeUtf8FileInsideWithoutSymlinks } from '../lib/filesystem.js';
|
|
4
4
|
import { isRecord } from '../lib/command-contract.js';
|
|
5
5
|
import { t } from '../lib/i18n.js';
|
|
6
|
+
import { checkNpmPackageExists } from '../lib/npm-version-check.js';
|
|
6
7
|
import { formatCliOptionParseError, getParsedCliStringOption, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
|
|
7
8
|
import { resolveMustflowRoot } from '../lib/project-root.js';
|
|
8
9
|
import { readMustflowTomlFile } from '../lib/toml.js';
|
|
@@ -14,6 +15,7 @@ const TECH_OPTIONS = [
|
|
|
14
15
|
{ name: '--status', kind: 'string' },
|
|
15
16
|
{ name: '--ecosystem', kind: 'string' },
|
|
16
17
|
{ name: '--package', kind: 'string' },
|
|
18
|
+
{ name: '--verify', kind: 'boolean' },
|
|
17
19
|
{ name: '--why', kind: 'string' },
|
|
18
20
|
{ name: '--constraint', kind: 'string' },
|
|
19
21
|
];
|
|
@@ -40,13 +42,14 @@ export function getTechHelp(lang = 'en') {
|
|
|
40
42
|
{ label: '--status <status>', description: `Preference status: ${TECHNOLOGY_STATUSES.join(', ')}` },
|
|
41
43
|
{ label: '--ecosystem <ecosystem>', description: 'Package ecosystem or platform, such as npm, cargo, pip, go, or deno' },
|
|
42
44
|
{ label: '--package <package>', description: 'Package name to associate with the preference. Repeatable.' },
|
|
45
|
+
{ label: '--verify', description: 'Verify listed npm packages exist before writing the preference' },
|
|
43
46
|
{ label: '--why <text>', description: 'Short rationale for the preference' },
|
|
44
47
|
{ label: '--constraint <text>', description: 'Guardrail agents must keep in mind. Repeatable.' },
|
|
45
48
|
{ label: '-h, --help', description: t(lang, 'cli.option.help') },
|
|
46
49
|
],
|
|
47
50
|
examples: [
|
|
48
51
|
'mf tech list',
|
|
49
|
-
'mf tech add framework nextjs --scope frontend --ecosystem npm --package next --package react --why "Preferred React app framework"',
|
|
52
|
+
'mf tech add framework nextjs --scope frontend --ecosystem npm --package next --package react --verify --why "Preferred React app framework"',
|
|
50
53
|
'mf tech add language rust --scope backend --status preferred --why "Use for correctness-critical engines"',
|
|
51
54
|
'mf tech add library jquery --scope frontend --status avoid --why "Avoid new usage"',
|
|
52
55
|
'mf tech suggest --scope frontend',
|
|
@@ -71,6 +74,7 @@ function parseTechOptions(args, lang) {
|
|
|
71
74
|
status: null,
|
|
72
75
|
ecosystem: null,
|
|
73
76
|
packages: [],
|
|
77
|
+
verify: false,
|
|
74
78
|
why: null,
|
|
75
79
|
constraints: [],
|
|
76
80
|
error: actionToken ? `Unknown tech action: ${actionToken}` : 'Specify a tech action: list, add, remove, or suggest',
|
|
@@ -87,6 +91,7 @@ function parseTechOptions(args, lang) {
|
|
|
87
91
|
status: null,
|
|
88
92
|
ecosystem: null,
|
|
89
93
|
packages: [],
|
|
94
|
+
verify: hasParsedCliOption(parsed, '--verify'),
|
|
90
95
|
why: null,
|
|
91
96
|
constraints: [],
|
|
92
97
|
error: formatCliOptionParseError(parsed.error, lang),
|
|
@@ -109,6 +114,7 @@ function parseTechOptions(args, lang) {
|
|
|
109
114
|
status,
|
|
110
115
|
ecosystem: getParsedCliStringOption(parsed, '--ecosystem'),
|
|
111
116
|
packages: getRepeatedStringOptions(parsed.occurrences, '--package'),
|
|
117
|
+
verify: hasParsedCliOption(parsed, '--verify'),
|
|
112
118
|
why: getParsedCliStringOption(parsed, '--why'),
|
|
113
119
|
constraints: getRepeatedStringOptions(parsed.occurrences, '--constraint'),
|
|
114
120
|
};
|
|
@@ -123,6 +129,7 @@ function invalidParsed(action, positionals, json, error) {
|
|
|
123
129
|
status: null,
|
|
124
130
|
ecosystem: null,
|
|
125
131
|
packages: [],
|
|
132
|
+
verify: false,
|
|
126
133
|
why: null,
|
|
127
134
|
constraints: [],
|
|
128
135
|
error,
|
|
@@ -239,7 +246,44 @@ function findExistingIndex(preferences, candidate) {
|
|
|
239
246
|
return preference.kind === candidate.kind && normalizeTechnologyKey(preference.name) === normalizeTechnologyKey(candidate.name);
|
|
240
247
|
});
|
|
241
248
|
}
|
|
242
|
-
function
|
|
249
|
+
async function verifyNpmPackages(options, reporter) {
|
|
250
|
+
if (!options.verify) {
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
if (options.action !== 'add') {
|
|
254
|
+
reporter.stderr(renderCliError('--verify is only supported with mf tech add', 'mf tech --help'));
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
if (options.packages.length === 0) {
|
|
258
|
+
reporter.stderr(renderCliError('--verify requires at least one --package value', 'mf tech --help'));
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
if (options.ecosystem !== 'npm') {
|
|
262
|
+
reporter.stderr(renderCliError('--verify currently supports --ecosystem npm only', 'mf tech --help'));
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
const checks = [];
|
|
266
|
+
for (const packageName of options.packages) {
|
|
267
|
+
try {
|
|
268
|
+
const check = await checkNpmPackageExists(packageName);
|
|
269
|
+
if (!check.exists) {
|
|
270
|
+
reporter.stderr(renderCliError(`npm package not found: ${packageName}`, 'mf tech --help'));
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
checks.push(check);
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
277
|
+
reporter.stderr(renderCliError(`Could not verify npm package ${packageName}: ${message}`, 'mf tech --help'));
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return checks;
|
|
282
|
+
}
|
|
283
|
+
function renderVerifiedPackageNames(checks) {
|
|
284
|
+
return checks.map((check) => check.resolvedName ?? check.packageName).join(', ');
|
|
285
|
+
}
|
|
286
|
+
async function runAdd(projectRoot, options, reporter) {
|
|
243
287
|
const [kindToken, nameToken] = options.positionals;
|
|
244
288
|
if (!isTechnologyKind(kindToken ?? null)) {
|
|
245
289
|
reporter.stderr(renderCliError('Missing or unsupported technology kind', 'mf tech --help'));
|
|
@@ -277,6 +321,10 @@ function runAdd(projectRoot, options, reporter) {
|
|
|
277
321
|
const preferences = [...file.preferences];
|
|
278
322
|
const existingIndex = findExistingIndex(preferences, candidate);
|
|
279
323
|
const action = existingIndex === -1 ? 'created' : 'updated';
|
|
324
|
+
const verifiedPackages = await verifyNpmPackages(options, reporter);
|
|
325
|
+
if (verifiedPackages === null) {
|
|
326
|
+
return 1;
|
|
327
|
+
}
|
|
280
328
|
if (existingIndex === -1) {
|
|
281
329
|
preferences.push(candidate);
|
|
282
330
|
}
|
|
@@ -285,9 +333,17 @@ function runAdd(projectRoot, options, reporter) {
|
|
|
285
333
|
}
|
|
286
334
|
writeTechnologyPreferences(projectRoot, preferences);
|
|
287
335
|
if (options.json) {
|
|
288
|
-
reporter.stdout(JSON.stringify({
|
|
336
|
+
reporter.stdout(JSON.stringify({
|
|
337
|
+
action,
|
|
338
|
+
preference: candidate,
|
|
339
|
+
path: TECHNOLOGY_CONFIG_RELATIVE_PATH,
|
|
340
|
+
verified_packages: verifiedPackages,
|
|
341
|
+
}, null, 2));
|
|
289
342
|
return 0;
|
|
290
343
|
}
|
|
344
|
+
if (verifiedPackages.length > 0) {
|
|
345
|
+
reporter.stdout(`Verified npm packages: ${renderVerifiedPackageNames(verifiedPackages)}`);
|
|
346
|
+
}
|
|
291
347
|
reporter.stdout(`${action === 'created' ? 'Created' : 'Updated'} ${candidate.id} in ${TECHNOLOGY_CONFIG_RELATIVE_PATH}`);
|
|
292
348
|
return 0;
|
|
293
349
|
}
|
|
@@ -322,7 +378,7 @@ function runRemove(projectRoot, options, reporter) {
|
|
|
322
378
|
reporter.stdout(`Removed ${removed.id} from ${TECHNOLOGY_CONFIG_RELATIVE_PATH}`);
|
|
323
379
|
return 0;
|
|
324
380
|
}
|
|
325
|
-
export function runTech(args, reporter, lang = 'en') {
|
|
381
|
+
export async function runTech(args, reporter, lang = 'en') {
|
|
326
382
|
if (hasCliOptionToken(args, '--help', ['-h'])) {
|
|
327
383
|
reporter.stdout(getTechHelp(lang));
|
|
328
384
|
return 0;
|
|
@@ -150,6 +150,42 @@ function buildLatestPackageUrl(registryUrl, packageName) {
|
|
|
150
150
|
: encodeURIComponent(packageName);
|
|
151
151
|
return `${trimmedRegistryUrl}/${encodedPackageName}/latest`;
|
|
152
152
|
}
|
|
153
|
+
function buildPackageMetadataUrl(registryUrl, packageName) {
|
|
154
|
+
const trimmedRegistryUrl = registryUrl.replace(/\/+$/u, '');
|
|
155
|
+
const encodedPackageName = packageName.startsWith('@')
|
|
156
|
+
? `@${encodeURIComponent(packageName.slice(1))}`
|
|
157
|
+
: encodeURIComponent(packageName);
|
|
158
|
+
return `${trimmedRegistryUrl}/${encodedPackageName}`;
|
|
159
|
+
}
|
|
160
|
+
export async function checkNpmPackageExists(packageName) {
|
|
161
|
+
const registryUrl = getRegistryUrl();
|
|
162
|
+
const response = await fetch(buildPackageMetadataUrl(registryUrl, packageName), {
|
|
163
|
+
headers: { accept: 'application/json' },
|
|
164
|
+
signal: AbortSignal.timeout(getTimeoutMs()),
|
|
165
|
+
});
|
|
166
|
+
if (response.status === 404) {
|
|
167
|
+
return {
|
|
168
|
+
packageName,
|
|
169
|
+
registryUrl,
|
|
170
|
+
exists: false,
|
|
171
|
+
resolvedName: null,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (!response.ok) {
|
|
175
|
+
throw new Error(`npm registry returned HTTP ${response.status}`);
|
|
176
|
+
}
|
|
177
|
+
const body = await response.json();
|
|
178
|
+
const resolvedName = isRecord(body) && typeof body.name === 'string' ? body.name : null;
|
|
179
|
+
if (!resolvedName) {
|
|
180
|
+
throw new Error('npm registry response did not include a package name');
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
packageName,
|
|
184
|
+
registryUrl,
|
|
185
|
+
exists: true,
|
|
186
|
+
resolvedName,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
153
189
|
export async function checkNpmLatestVersion(metadata) {
|
|
154
190
|
const registryUrl = getRegistryUrl();
|
|
155
191
|
const response = await fetch(buildLatestPackageUrl(registryUrl, metadata.name), {
|
package/package.json
CHANGED