mustflow 1.30.0 → 2.11.0

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.
Files changed (82) hide show
  1. package/README.md +35 -11
  2. package/dist/cli/commands/classify.js +61 -6
  3. package/dist/cli/commands/contract-lint.js +13 -4
  4. package/dist/cli/commands/dashboard.js +6 -0
  5. package/dist/cli/commands/index.js +5 -0
  6. package/dist/cli/commands/run.js +224 -48
  7. package/dist/cli/commands/upgrade.js +65 -0
  8. package/dist/cli/commands/verify.js +550 -33
  9. package/dist/cli/i18n/en.js +73 -10
  10. package/dist/cli/i18n/es.js +73 -10
  11. package/dist/cli/i18n/fr.js +73 -10
  12. package/dist/cli/i18n/hi.js +73 -10
  13. package/dist/cli/i18n/ko.js +73 -10
  14. package/dist/cli/i18n/zh.js +73 -10
  15. package/dist/cli/index.js +27 -46
  16. package/dist/cli/lib/command-registry.js +5 -0
  17. package/dist/cli/lib/dashboard-export.js +62 -12
  18. package/dist/cli/lib/dashboard-html/client-script.js +1936 -0
  19. package/dist/cli/lib/dashboard-html/locale-bootstrap.js +8 -0
  20. package/dist/cli/lib/dashboard-html/styles.js +572 -0
  21. package/dist/cli/lib/dashboard-html/template.js +134 -0
  22. package/dist/cli/lib/dashboard-html/types.js +1 -0
  23. package/dist/cli/lib/dashboard-html.js +1 -1907
  24. package/dist/cli/lib/dashboard-locale.js +37 -0
  25. package/dist/cli/lib/local-index/constants.js +48 -0
  26. package/dist/cli/lib/local-index/index.js +2256 -0
  27. package/dist/cli/lib/local-index/sql.js +15 -0
  28. package/dist/cli/lib/local-index/types.js +1 -0
  29. package/dist/cli/lib/local-index.js +1 -1908
  30. package/dist/cli/lib/reporter.js +6 -0
  31. package/dist/cli/lib/run-plan.js +96 -4
  32. package/dist/cli/lib/templates.js +18 -1
  33. package/dist/cli/lib/validation/command-intents.js +11 -0
  34. package/dist/cli/lib/validation/constants.js +238 -0
  35. package/dist/cli/lib/validation/index.js +1384 -0
  36. package/dist/cli/lib/validation/primitives.js +198 -0
  37. package/dist/cli/lib/validation/test-selection.js +95 -0
  38. package/dist/cli/lib/validation/types.js +1 -0
  39. package/dist/cli/lib/validation.js +1 -1661
  40. package/dist/core/bounded-output.js +38 -0
  41. package/dist/core/change-classification.js +6 -2
  42. package/dist/core/change-verification.js +240 -6
  43. package/dist/core/check-issues.js +12 -0
  44. package/dist/core/command-contract-validation.js +20 -0
  45. package/dist/core/command-effects.js +13 -0
  46. package/dist/core/completion-verdict.js +209 -0
  47. package/dist/core/contract-lint.js +316 -7
  48. package/dist/core/dashboard-verification.js +8 -0
  49. package/dist/core/external-evidence.js +9 -0
  50. package/dist/core/public-json-contracts.js +28 -0
  51. package/dist/core/repeated-failure.js +17 -0
  52. package/dist/core/repro-evidence.js +53 -0
  53. package/dist/core/run-performance-history.js +307 -0
  54. package/dist/core/run-profile.js +87 -0
  55. package/dist/core/run-receipt.js +171 -4
  56. package/dist/core/run-write-drift.js +18 -2
  57. package/dist/core/scope-risk.js +64 -0
  58. package/dist/core/skill-route-alignment.js +110 -0
  59. package/dist/core/source-anchor-status.js +4 -1
  60. package/dist/core/test-selection.js +227 -0
  61. package/dist/core/validation-ratchet.js +52 -0
  62. package/dist/core/verification-decision-graph.js +67 -0
  63. package/dist/core/verification-evidence.js +249 -0
  64. package/dist/core/verification-scheduler.js +96 -2
  65. package/examples/README.md +12 -4
  66. package/package.json +1 -1
  67. package/schemas/README.md +18 -4
  68. package/schemas/change-verification-report.schema.json +169 -5
  69. package/schemas/commands.schema.json +51 -1
  70. package/schemas/contract-lint-report.schema.json +80 -0
  71. package/schemas/dashboard-export.schema.json +500 -0
  72. package/schemas/explain-report.schema.json +2 -0
  73. package/schemas/latest-run-pointer.schema.json +384 -0
  74. package/schemas/run-receipt.schema.json +113 -0
  75. package/schemas/test-selection.schema.json +81 -0
  76. package/schemas/verify-report.schema.json +361 -1
  77. package/schemas/verify-run-manifest.schema.json +410 -0
  78. package/templates/default/common/.mustflow/config/commands.toml +1 -1
  79. package/templates/default/i18n.toml +1 -1
  80. package/templates/default/locales/en/.mustflow/skills/INDEX.md +124 -29
  81. package/templates/default/locales/en/.mustflow/skills/routes.toml +289 -0
  82. package/templates/default/manifest.toml +29 -2
@@ -1,17 +1,8 @@
1
- import { spawnSync } from 'node:child_process';
2
- import { runCheck } from './check.js';
3
- import { runClassify } from './classify.js';
4
- import { runContext } from './context.js';
5
- import { runDoctor } from './doctor.js';
6
- import { runHelp } from './help.js';
7
- import { runImpact } from './impact.js';
8
- import { runLineEndings } from './line-endings.js';
9
- import { runMap } from './map.js';
10
- import { runStatus } from './status.js';
11
- import { runUpdate } from './update.js';
12
- import { runVersionSources } from './version-sources.js';
1
+ import { spawn, spawnSync } from 'node:child_process';
2
+ import { performance } from 'node:perf_hooks';
13
3
  import { canRunMustflowBuiltinInProcess, isMustflowBinName } from '../../core/command-classification.js';
14
4
  import { createCommandEnv } from '../../core/command-env.js';
5
+ import { BoundedOutputBuffer } from '../../core/bounded-output.js';
15
6
  import { printUsageError, renderCliError, renderHelp } from '../lib/cli-output.js';
16
7
  import { readCommandContract, readMustflowConfigIfExists } from '../../core/config-loading.js';
17
8
  import { resolveRunReceiptRetentionPolicy } from '../../core/retention-policy.js';
@@ -20,12 +11,14 @@ import { getPackageVersion } from '../lib/package-info.js';
20
11
  import { resolveMustflowRoot } from '../lib/project-root.js';
21
12
  import { createRunPlan, createRunPreview, isMustflowBuiltinIntent, renderRunPreviewText, } from '../lib/run-plan.js';
22
13
  import { createRunReceipt, writeRunReceipt } from '../../core/run-receipt.js';
14
+ import { recordRunPerformanceHistory } from '../../core/run-performance-history.js';
15
+ import { RunProfiler } from '../../core/run-profile.js';
23
16
  import { finishRunWriteTracking, startRunWriteTracking } from '../../core/run-write-drift.js';
24
17
  function emitOutput(reporter, output, stream) {
25
18
  if (!output) {
26
19
  return;
27
20
  }
28
- const text = output.toString().trimEnd();
21
+ const text = (typeof output === 'object' && 'tail' in output ? output.tail : output.toString()).trimEnd();
29
22
  if (text.length === 0) {
30
23
  return;
31
24
  }
@@ -94,37 +87,37 @@ async function runKnownBuiltinCommand(args, reporter, lang) {
94
87
  return undefined;
95
88
  }
96
89
  if (command === 'check') {
97
- return runCheck(commandArgs, reporter, lang);
90
+ return (await import('./check.js')).runCheck(commandArgs, reporter, lang);
98
91
  }
99
92
  if (command === 'classify') {
100
- return runClassify(commandArgs, reporter, lang);
93
+ return (await import('./classify.js')).runClassify(commandArgs, reporter, lang);
101
94
  }
102
95
  if (command === 'context') {
103
- return runContext(commandArgs, reporter, lang);
96
+ return (await import('./context.js')).runContext(commandArgs, reporter, lang);
104
97
  }
105
98
  if (command === 'doctor') {
106
- return runDoctor(commandArgs, reporter, lang);
99
+ return (await import('./doctor.js')).runDoctor(commandArgs, reporter, lang);
107
100
  }
108
101
  if (command === 'help') {
109
- return runHelp(commandArgs, reporter, lang);
102
+ return (await import('./help.js')).runHelp(commandArgs, reporter, lang);
110
103
  }
111
104
  if (command === 'impact') {
112
- return runImpact(commandArgs, reporter, lang);
105
+ return (await import('./impact.js')).runImpact(commandArgs, reporter, lang);
113
106
  }
114
107
  if (command === 'line-endings') {
115
- return runLineEndings(commandArgs, reporter, lang);
108
+ return (await import('./line-endings.js')).runLineEndings(commandArgs, reporter, lang);
116
109
  }
117
110
  if (command === 'map') {
118
- return runMap(commandArgs, reporter, lang);
111
+ return (await import('./map.js')).runMap(commandArgs, reporter, lang);
119
112
  }
120
113
  if (command === 'status') {
121
- return runStatus(commandArgs, reporter, lang);
114
+ return (await import('./status.js')).runStatus(commandArgs, reporter, lang);
122
115
  }
123
116
  if (command === 'update') {
124
- return runUpdate(commandArgs, reporter, lang);
117
+ return (await import('./update.js')).runUpdate(commandArgs, reporter, lang);
125
118
  }
126
119
  if (command === 'version-sources') {
127
- return runVersionSources(commandArgs, reporter, lang);
120
+ return (await import('./version-sources.js')).runVersionSources(commandArgs, reporter, lang);
128
121
  }
129
122
  return undefined;
130
123
  }
@@ -179,6 +172,76 @@ function runArgvCommand(command, cwd, maxOutputBytes, env, timeoutSeconds) {
179
172
  windowsHide: true,
180
173
  });
181
174
  }
175
+ function writeStreamChunk(reporter, stream, chunk) {
176
+ if (stream === 'stdout') {
177
+ if (reporter.writeStdout) {
178
+ reporter.writeStdout(chunk);
179
+ return;
180
+ }
181
+ reporter.stdout(chunk.toString().trimEnd());
182
+ return;
183
+ }
184
+ if (reporter.writeStderr) {
185
+ reporter.writeStderr(chunk);
186
+ return;
187
+ }
188
+ reporter.stderr(chunk.toString().trimEnd());
189
+ }
190
+ function runArgvCommandStreaming(command, cwd, env, timeoutSeconds, stdoutTailBytes, stderrTailBytes, reporter) {
191
+ return new Promise((resolve) => {
192
+ const stdout = new BoundedOutputBuffer(stdoutTailBytes);
193
+ const stderr = new BoundedOutputBuffer(stderrTailBytes);
194
+ let settled = false;
195
+ let timedOut = false;
196
+ let childError;
197
+ let childPid;
198
+ let timeout;
199
+ const child = spawn(command?.executable ?? '', command?.args ?? [], {
200
+ cwd,
201
+ env,
202
+ shell: command?.shell ?? false,
203
+ stdio: ['ignore', 'pipe', 'pipe'],
204
+ windowsHide: true,
205
+ detached: process.platform !== 'win32',
206
+ });
207
+ childPid = child.pid;
208
+ const finish = (status, signal) => {
209
+ if (settled) {
210
+ return;
211
+ }
212
+ settled = true;
213
+ if (timeout) {
214
+ clearTimeout(timeout);
215
+ }
216
+ resolve({
217
+ status,
218
+ signal,
219
+ error: timedOut ? Object.assign(new Error('Command timed out'), { code: 'ETIMEDOUT' }) : childError,
220
+ stdout: stdout.toSnapshot(),
221
+ stderr: stderr.toSnapshot(),
222
+ pid: childPid,
223
+ });
224
+ };
225
+ child.stdout?.on('data', (chunk) => {
226
+ stdout.append(chunk);
227
+ writeStreamChunk(reporter, 'stdout', chunk);
228
+ });
229
+ child.stderr?.on('data', (chunk) => {
230
+ stderr.append(chunk);
231
+ writeStreamChunk(reporter, 'stderr', chunk);
232
+ });
233
+ child.once('error', (error) => {
234
+ childError = error;
235
+ });
236
+ child.once('close', (status, signal) => {
237
+ finish(status, signal);
238
+ });
239
+ timeout = setTimeout(() => {
240
+ timedOut = true;
241
+ terminateProcessTree(childPid);
242
+ }, timeoutSeconds * 1000);
243
+ });
244
+ }
182
245
  function runShellCommand(command, cwd, maxOutputBytes, env, timeoutSeconds) {
183
246
  return spawnSync(command ?? '', {
184
247
  cwd,
@@ -192,6 +255,61 @@ function runShellCommand(command, cwd, maxOutputBytes, env, timeoutSeconds) {
192
255
  windowsHide: true,
193
256
  });
194
257
  }
258
+ function runShellCommandStreaming(command, cwd, env, timeoutSeconds, stdoutTailBytes, stderrTailBytes, reporter) {
259
+ return new Promise((resolve) => {
260
+ const stdout = new BoundedOutputBuffer(stdoutTailBytes);
261
+ const stderr = new BoundedOutputBuffer(stderrTailBytes);
262
+ let settled = false;
263
+ let timedOut = false;
264
+ let childError;
265
+ let childPid;
266
+ let timeout;
267
+ const child = spawn(command ?? '', {
268
+ cwd,
269
+ env,
270
+ shell: true,
271
+ stdio: ['ignore', 'pipe', 'pipe'],
272
+ windowsHide: true,
273
+ detached: process.platform !== 'win32',
274
+ });
275
+ childPid = child.pid;
276
+ const finish = (status, signal) => {
277
+ if (settled) {
278
+ return;
279
+ }
280
+ settled = true;
281
+ if (timeout) {
282
+ clearTimeout(timeout);
283
+ }
284
+ resolve({
285
+ status,
286
+ signal,
287
+ error: timedOut ? Object.assign(new Error('Command timed out'), { code: 'ETIMEDOUT' }) : childError,
288
+ stdout: stdout.toSnapshot(),
289
+ stderr: stderr.toSnapshot(),
290
+ pid: childPid,
291
+ });
292
+ };
293
+ child.stdout?.on('data', (chunk) => {
294
+ stdout.append(chunk);
295
+ writeStreamChunk(reporter, 'stdout', chunk);
296
+ });
297
+ child.stderr?.on('data', (chunk) => {
298
+ stderr.append(chunk);
299
+ writeStreamChunk(reporter, 'stderr', chunk);
300
+ });
301
+ child.once('error', (error) => {
302
+ childError = error;
303
+ });
304
+ child.once('close', (status, signal) => {
305
+ finish(status, signal);
306
+ });
307
+ timeout = setTimeout(() => {
308
+ timedOut = true;
309
+ terminateProcessTree(childPid);
310
+ }, timeoutSeconds * 1000);
311
+ });
312
+ }
195
313
  function getRunStatus(error, exitCode, successExitCodes) {
196
314
  const errorWithCode = error;
197
315
  if (errorWithCode?.code === 'ETIMEDOUT') {
@@ -249,6 +367,9 @@ function reportRunPlanFailure(plan, reporter, lang) {
249
367
  message = t(lang, 'run.error.unknownIntent', { intent: plan.intentName });
250
368
  break;
251
369
  }
370
+ if (plan.suggestedIntentSnippet) {
371
+ message = `${message}\n\n${t(lang, 'run.label.suggestedIntentSnippet')}:\n${plan.suggestedIntentSnippet}`;
372
+ }
252
373
  reporter.stderr(renderCliError(message, 'mf help commands', lang));
253
374
  }
254
375
  export function getRunHelp(lang = 'en') {
@@ -281,7 +402,9 @@ export function getRunHelp(lang = 'en') {
281
402
  * invariant: Execution requires configured status, oneshot lifecycle, agent_allowed policy, and closed stdin.
282
403
  * risk: config, security, state
283
404
  */
284
- export async function runRun(args, reporter, lang = 'en') {
405
+ export async function runRun(args, reporter, lang = 'en', options = {}) {
406
+ const executorStartedAtMs = performance.now();
407
+ const profiler = new RunProfiler();
285
408
  if (args.includes('--help') || args.includes('-h')) {
286
409
  reporter.stdout(getRunHelp(lang));
287
410
  return 0;
@@ -310,36 +433,69 @@ export async function runRun(args, reporter, lang = 'en') {
310
433
  printUsageError(reporter, t(lang, 'cli.error.unexpectedArgument', { argument: extra[0] }), 'mf run --help', getRunHelp(lang), lang);
311
434
  return 1;
312
435
  }
313
- const projectRoot = resolveMustflowRoot();
314
- const contract = readCommandContract(projectRoot);
315
- const plan = createRunPlan(projectRoot, contract, intentName);
436
+ const projectRoot = profiler.measure('root_detection', () => resolveMustflowRoot());
437
+ const contract = profiler.measure('command_contract', () => readCommandContract(projectRoot));
438
+ const plan = profiler.measure('plan_creation', () => createRunPlan(projectRoot, contract, intentName, { testTargets: options.testTargets }));
316
439
  if (previewMode) {
317
- if (json) {
318
- reporter.stdout(JSON.stringify(createRunPreview(plan, previewMode), null, 2));
319
- }
320
- else {
321
- reporter.stdout(renderRunPreviewText(plan, previewMode));
322
- }
440
+ profiler.measure('preview_render', () => {
441
+ if (json) {
442
+ reporter.stdout(JSON.stringify(createRunPreview(plan, previewMode), null, 2));
443
+ }
444
+ else {
445
+ reporter.stdout(renderRunPreviewText(plan, previewMode, lang));
446
+ }
447
+ });
448
+ profiler.writeLatest({
449
+ projectRoot,
450
+ intent: intentName,
451
+ status: plan.ok ? 'previewed' : 'blocked',
452
+ previewMode,
453
+ });
323
454
  return plan.ok ? 0 : 1;
324
455
  }
325
456
  if (!plan.ok) {
326
457
  reportRunPlanFailure(plan, reporter, lang);
458
+ profiler.writeLatest({
459
+ projectRoot,
460
+ intent: intentName,
461
+ status: 'blocked',
462
+ previewMode: null,
463
+ });
327
464
  return 1;
328
465
  }
329
- const runReceiptPolicy = resolveRunReceiptRetentionPolicy(readMustflowConfigIfExists(projectRoot));
330
- const env = createCommandEnv(projectRoot, { policy: plan.envPolicy, allowlist: plan.envAllowlist });
466
+ const runReceiptPolicy = profiler.measure('retention_policy', () => resolveRunReceiptRetentionPolicy(readMustflowConfigIfExists(projectRoot)));
467
+ const env = profiler.measure('environment', () => createCommandEnv(projectRoot, { policy: plan.envPolicy, allowlist: plan.envAllowlist }));
331
468
  const lifecycleValue = plan.lifecycle ?? 'oneshot';
332
469
  const runPolicyValue = plan.runPolicy ?? 'agent_allowed';
333
- const writeTracker = startRunWriteTracking(projectRoot, contract, intentName);
470
+ const writeTracker = profiler.measure('write_drift_before', () => startRunWriteTracking(projectRoot, contract, intentName));
471
+ const stdoutTailBytes = Math.min(runReceiptPolicy.stdoutTailBytes, plan.maxOutputBytes);
472
+ const stderrTailBytes = Math.min(runReceiptPolicy.stderrTailBytes, plan.maxOutputBytes);
473
+ let streamedOutput = false;
474
+ const childStartedAtMs = performance.now();
334
475
  const startedAt = new Date();
335
- const result = plan.commandArgv && isMustflowBuiltinIntent(plan.intent)
336
- ? ((await runBuiltinArgvInProcess(plan.commandArgv, plan.cwd, lang)) ??
337
- runArgvCommand(plan.argvCommand, plan.cwd, plan.maxOutputBytes, env, plan.timeoutSeconds))
338
- : plan.commandArgv
339
- ? runArgvCommand(plan.argvCommand, plan.cwd, plan.maxOutputBytes, env, plan.timeoutSeconds)
340
- : runShellCommand(plan.shellCommand, plan.cwd, plan.maxOutputBytes, env, plan.timeoutSeconds);
476
+ const result = await profiler.measureAsync('child_command', async () => {
477
+ if (plan.commandArgv && isMustflowBuiltinIntent(plan.intent)) {
478
+ const builtinResult = await runBuiltinArgvInProcess(plan.commandArgv, plan.cwd, lang);
479
+ if (builtinResult) {
480
+ return builtinResult;
481
+ }
482
+ }
483
+ if (plan.commandArgv) {
484
+ if (!json) {
485
+ streamedOutput = true;
486
+ return runArgvCommandStreaming(plan.argvCommand, plan.cwd, env, plan.timeoutSeconds, stdoutTailBytes, stderrTailBytes, reporter);
487
+ }
488
+ return runArgvCommand(plan.argvCommand, plan.cwd, plan.maxOutputBytes, env, plan.timeoutSeconds);
489
+ }
490
+ if (!json) {
491
+ streamedOutput = true;
492
+ return runShellCommandStreaming(plan.shellCommand, plan.cwd, env, plan.timeoutSeconds, stdoutTailBytes, stderrTailBytes, reporter);
493
+ }
494
+ return runShellCommand(plan.shellCommand, plan.cwd, plan.maxOutputBytes, env, plan.timeoutSeconds);
495
+ });
496
+ const childDurationMs = performance.now() - childStartedAtMs;
341
497
  const finishedAt = new Date();
342
- const writeDrift = finishRunWriteTracking(writeTracker);
498
+ const writeDrift = profiler.measure('write_drift_after', () => finishRunWriteTracking(writeTracker));
343
499
  const exitCode = typeof result.status === 'number' ? result.status : null;
344
500
  const runStatus = getRunStatus(result.error, exitCode, plan.successExitCodes);
345
501
  let killMethod = null;
@@ -347,7 +503,7 @@ export async function runRun(args, reporter, lang = 'en') {
347
503
  killMethod = getKillMethod();
348
504
  terminateProcessTree(result.pid);
349
505
  }
350
- const receipt = createRunReceipt({
506
+ const receipt = profiler.measure('receipt_create', () => createRunReceipt({
351
507
  intent: intentName,
352
508
  status: runStatus,
353
509
  timedOut: runStatus === 'timed_out',
@@ -372,16 +528,36 @@ export async function runRun(args, reporter, lang = 'en') {
372
528
  stdout: result.stdout,
373
529
  stderr: result.stderr,
374
530
  writeDrift,
531
+ executorOverheadMs: Math.max(0, Math.round((performance.now() - executorStartedAtMs - childDurationMs) * 1000) / 1000),
532
+ phaseTimings: profiler.getReceiptPhases(),
533
+ selectionSummary: {
534
+ strategy: plan.testTargets.length > 0 ? 'project_test_selection' : 'direct_intent',
535
+ changed_file_count: 0,
536
+ changed_surface_counts: {},
537
+ selected_target_count: Math.max(1, plan.testTargets.length),
538
+ fallback_used: false,
539
+ },
375
540
  stdoutTailBytes: runReceiptPolicy.stdoutTailBytes,
376
541
  stderrTailBytes: runReceiptPolicy.stderrTailBytes,
542
+ }));
543
+ if (options.writeLatestReceipt !== false) {
544
+ profiler.measure('receipt_write', () => writeRunReceipt(projectRoot, receipt));
545
+ }
546
+ profiler.measure('performance_history_write', () => recordRunPerformanceHistory(projectRoot, receipt));
547
+ profiler.writeLatest({
548
+ projectRoot,
549
+ intent: intentName,
550
+ status: runStatus,
551
+ previewMode: null,
377
552
  });
378
- writeRunReceipt(projectRoot, receipt);
379
553
  if (json) {
380
554
  reporter.stdout(JSON.stringify(receipt, null, 2));
381
555
  return runStatus === 'passed' ? 0 : 1;
382
556
  }
383
- emitOutput(reporter, result.stdout, 'stdout');
384
- emitOutput(reporter, result.stderr, 'stderr');
557
+ if (!streamedOutput) {
558
+ emitOutput(reporter, result.stdout, 'stdout');
559
+ emitOutput(reporter, result.stderr, 'stderr');
560
+ }
385
561
  if (result.error) {
386
562
  const errorWithCode = result.error;
387
563
  if (errorWithCode.code === 'ETIMEDOUT') {
@@ -0,0 +1,65 @@
1
+ import { printUsageError, renderCliError, renderHelp } from '../lib/cli-output.js';
2
+ import { t } from '../lib/i18n.js';
3
+ import { checkNpmLatestVersion } from '../lib/npm-version-check.js';
4
+ import { readPackageMetadata } from '../lib/package-info.js';
5
+ import { runUpdate } from './update.js';
6
+ export function getUpgradeHelp(lang = 'en') {
7
+ return renderHelp({
8
+ usage: 'mf upgrade [options]',
9
+ summary: t(lang, 'upgrade.help.summary'),
10
+ options: [
11
+ { label: '--dry-run', description: t(lang, 'upgrade.help.option.dryRun') },
12
+ { label: '-h, --help', description: t(lang, 'cli.option.help') },
13
+ ],
14
+ examples: ['mf upgrade', 'mf upgrade --dry-run'],
15
+ exitCodes: [
16
+ { label: '0', description: t(lang, 'upgrade.help.exit.ok') },
17
+ { label: '1', description: t(lang, 'upgrade.help.exit.fail') },
18
+ ],
19
+ }, lang);
20
+ }
21
+ function printPackageCheck(check, reporter, lang) {
22
+ reporter.stdout(`${check.packageName} ${check.currentVersion}`);
23
+ reporter.stdout(check.updateAvailable
24
+ ? t(lang, 'version.check.latestAvailable', { version: check.latestVersion })
25
+ : t(lang, 'version.check.upToDate', { version: check.latestVersion }));
26
+ if (check.updateAvailable) {
27
+ reporter.stdout('');
28
+ reporter.stdout(t(lang, 'version.check.updateCommand'));
29
+ reporter.stdout(check.updateCommand);
30
+ }
31
+ }
32
+ export async function runUpgrade(args, reporter, lang = 'en') {
33
+ if (args.includes('--help') || args.includes('-h')) {
34
+ reporter.stdout(getUpgradeHelp(lang));
35
+ return 0;
36
+ }
37
+ const supported = new Set(['--dry-run']);
38
+ const unsupported = args.filter((arg) => !supported.has(arg));
39
+ if (unsupported.length > 0) {
40
+ printUsageError(reporter, t(lang, 'cli.error.unknownOption', { option: unsupported[0] }), 'mf upgrade --help', getUpgradeHelp(lang), lang);
41
+ return 1;
42
+ }
43
+ const dryRun = args.includes('--dry-run');
44
+ reporter.stdout(t(lang, 'upgrade.title'));
45
+ reporter.stdout('');
46
+ reporter.stdout(t(lang, 'upgrade.packageSection'));
47
+ try {
48
+ const packageCheck = await checkNpmLatestVersion(readPackageMetadata());
49
+ printPackageCheck(packageCheck, reporter, lang);
50
+ if (packageCheck.updateAvailable) {
51
+ reporter.stdout('');
52
+ reporter.stdout(t(lang, 'upgrade.packageUpdateRequired'));
53
+ reporter.stdout(t(lang, 'upgrade.noFilesWritten'));
54
+ return 1;
55
+ }
56
+ }
57
+ catch (error) {
58
+ const message = error instanceof Error ? error.message : String(error);
59
+ reporter.stderr(renderCliError(t(lang, 'upgrade.warning.versionCheckFailed', { message }), 'mf version --check', lang));
60
+ reporter.stderr(t(lang, 'upgrade.warning.continueWithBundledTemplate'));
61
+ }
62
+ reporter.stdout('');
63
+ reporter.stdout(t(lang, 'upgrade.projectSection'));
64
+ return runUpdate([dryRun ? '--dry-run' : '--apply'], reporter, lang);
65
+ }