mustflow 2.23.0 → 2.24.2

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 (75) hide show
  1. package/README.md +12 -2
  2. package/dist/cli/commands/adapters.js +11 -9
  3. package/dist/cli/commands/api.js +263 -113
  4. package/dist/cli/commands/check.js +11 -7
  5. package/dist/cli/commands/classify.js +16 -42
  6. package/dist/cli/commands/context.js +18 -31
  7. package/dist/cli/commands/contract-lint.js +12 -7
  8. package/dist/cli/commands/dashboard.js +65 -114
  9. package/dist/cli/commands/docs.js +43 -26
  10. package/dist/cli/commands/doctor.js +11 -7
  11. package/dist/cli/commands/evidence.js +642 -0
  12. package/dist/cli/commands/explain-verify.js +1 -59
  13. package/dist/cli/commands/explain.js +84 -36
  14. package/dist/cli/commands/handoff.js +13 -17
  15. package/dist/cli/commands/impact.js +14 -20
  16. package/dist/cli/commands/index.js +15 -9
  17. package/dist/cli/commands/init.js +56 -70
  18. package/dist/cli/commands/line-endings.js +15 -9
  19. package/dist/cli/commands/map.js +30 -42
  20. package/dist/cli/commands/next.js +300 -0
  21. package/dist/cli/commands/onboard.js +136 -0
  22. package/dist/cli/commands/run.js +47 -42
  23. package/dist/cli/commands/search.js +43 -69
  24. package/dist/cli/commands/status.js +9 -6
  25. package/dist/cli/commands/update.js +16 -10
  26. package/dist/cli/commands/upgrade.js +9 -6
  27. package/dist/cli/commands/verify/args.js +55 -249
  28. package/dist/cli/commands/verify.js +2 -1
  29. package/dist/cli/commands/version-sources.js +9 -6
  30. package/dist/cli/commands/version.js +9 -6
  31. package/dist/cli/commands/workspace.js +564 -0
  32. package/dist/cli/i18n/en.js +60 -1
  33. package/dist/cli/i18n/es.js +60 -1
  34. package/dist/cli/i18n/fr.js +60 -1
  35. package/dist/cli/i18n/hi.js +60 -1
  36. package/dist/cli/i18n/ko.js +60 -1
  37. package/dist/cli/i18n/zh.js +60 -1
  38. package/dist/cli/index.js +28 -25
  39. package/dist/cli/lib/agent-context.js +8 -9
  40. package/dist/cli/lib/command-registry.js +24 -0
  41. package/dist/cli/lib/dashboard-html/client-script.js +1 -1
  42. package/dist/cli/lib/local-index/database-path.js +5 -0
  43. package/dist/cli/lib/local-index/database-read.js +88 -0
  44. package/dist/cli/lib/local-index/effect-graph-read-model.js +112 -0
  45. package/dist/cli/lib/local-index/freshness.js +60 -0
  46. package/dist/cli/lib/local-index/index.js +12 -1866
  47. package/dist/cli/lib/local-index/path-surface-read-model.js +134 -0
  48. package/dist/cli/lib/local-index/populate.js +474 -0
  49. package/dist/cli/lib/local-index/schema.js +413 -0
  50. package/dist/cli/lib/local-index/search-read-model.js +533 -0
  51. package/dist/cli/lib/local-index/search-text.js +79 -0
  52. package/dist/cli/lib/option-parser.js +93 -0
  53. package/dist/cli/lib/repo-map.js +2 -2
  54. package/dist/cli/lib/run-plan.js +5 -22
  55. package/dist/core/change-verification.js +11 -5
  56. package/dist/core/command-effects.js +1 -3
  57. package/dist/core/command-intent-eligibility.js +14 -0
  58. package/dist/core/command-preconditions.js +8 -4
  59. package/dist/core/command-run-constraints.js +43 -0
  60. package/dist/core/public-json-contracts.js +57 -0
  61. package/dist/core/test-selection.js +8 -2
  62. package/dist/core/verification-plan.js +32 -4
  63. package/package.json +1 -1
  64. package/schemas/README.md +16 -0
  65. package/schemas/api-serve-response.schema.json +89 -0
  66. package/schemas/change-verification-report.schema.json +4 -1
  67. package/schemas/contract-lint-report.schema.json +1 -0
  68. package/schemas/evidence-report.schema.json +287 -0
  69. package/schemas/explain-report.schema.json +4 -0
  70. package/schemas/next-report.schema.json +121 -0
  71. package/schemas/onboard-commands-report.schema.json +100 -0
  72. package/schemas/workspace-command-catalog.schema.json +172 -0
  73. package/schemas/workspace-status.schema.json +141 -0
  74. package/schemas/workspace-verification-plan.schema.json +195 -0
  75. package/templates/default/manifest.toml +1 -1
@@ -6,64 +6,6 @@ import { createVerificationSchedule } from '../../core/verification-scheduler.js
6
6
  import { t } from '../lib/i18n.js';
7
7
  import { readLatestLocalVerificationReadModelQueries, readLocalCommandEffectGraphs, } from '../lib/local-index.js';
8
8
  import { planErrorMessageKey, readInputFromClassificationReport } from './verify.js';
9
- export function parseExplainVerifyArgs(args) {
10
- let reason;
11
- let fromPlan;
12
- for (let index = 0; index < args.length; index += 1) {
13
- const arg = args[index];
14
- if (arg === '--reason') {
15
- const value = args[index + 1];
16
- if (!value || value.startsWith('-')) {
17
- return { reason, fromPlan, error: 'missing_reason_value' };
18
- }
19
- reason = value;
20
- index += 1;
21
- continue;
22
- }
23
- if (arg === '--from-plan') {
24
- const value = args[index + 1];
25
- if (!value || value.startsWith('-')) {
26
- return { reason, fromPlan, error: 'missing_from_plan_value' };
27
- }
28
- fromPlan = value;
29
- index += 1;
30
- continue;
31
- }
32
- if (arg.startsWith('--reason=')) {
33
- const value = arg.slice('--reason='.length);
34
- if (value.length === 0) {
35
- return { reason, fromPlan, error: 'missing_reason_value' };
36
- }
37
- reason = value;
38
- continue;
39
- }
40
- if (arg.startsWith('--from-plan=')) {
41
- const value = arg.slice('--from-plan='.length);
42
- if (value.length === 0) {
43
- return { reason, fromPlan, error: 'missing_from_plan_value' };
44
- }
45
- fromPlan = value;
46
- continue;
47
- }
48
- if (arg.startsWith('-')) {
49
- return { reason, fromPlan, error: arg };
50
- }
51
- return { reason, fromPlan, error: `unexpected:${arg}` };
52
- }
53
- return { reason, fromPlan };
54
- }
55
- export function explainVerifyArgErrorMessage(error, lang) {
56
- if (error === 'missing_reason_value') {
57
- return t(lang, 'cli.error.missingValue', { option: '--reason' });
58
- }
59
- if (error === 'missing_from_plan_value') {
60
- return t(lang, 'cli.error.missingValue', { option: '--from-plan' });
61
- }
62
- if (error.startsWith('unexpected:')) {
63
- return t(lang, 'cli.error.unexpectedArgument', { argument: error.slice('unexpected:'.length) });
64
- }
65
- return t(lang, 'cli.error.unknownOption', { option: error });
66
- }
67
9
  export function explainVerifyPlanErrorMessage(error, lang) {
68
10
  const code = error instanceof Error ? error.message : 'invalid_plan_file';
69
11
  return t(lang, planErrorMessageKey(code));
@@ -73,7 +15,7 @@ export function readExplainVerifyPlanReasons(projectRoot, planPath) {
73
15
  }
74
16
  export async function getVerifyExplainOutput(schemaVersion, projectRoot, reasons, inputReason, planSource) {
75
17
  const contract = readCommandContract(projectRoot);
76
- const plans = reasons.map((reason) => createVerificationPlan(contract, reason));
18
+ const plans = reasons.map((reason) => createVerificationPlan(contract, reason, projectRoot));
77
19
  const graphRequirements = reasons.map((reason) => ({
78
20
  reason,
79
21
  files: [],
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
4
4
  import { t } from '../lib/i18n.js';
5
5
  import { MUSTFLOW_JSON_MAX_BYTES, readMustflowTextFile } from '../lib/mustflow-read.js';
6
+ import { formatCliOptionParseError, getParsedCliStringOption, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
6
7
  import { resolveMustflowRoot } from '../lib/project-root.js';
7
8
  import { explainAssetOptimization, explainCommandIntent, } from '../../core/command-explanation.js';
8
9
  import { readCommandContract, readMustflowConfigIfExists } from '../../core/config-loading.js';
@@ -14,10 +15,16 @@ import { explainPublicSurface } from '../../core/public-surface-explanation.js';
14
15
  import { explainSourceAnchor } from '../../core/source-anchor-explanation.js';
15
16
  import { checkMustflowProject } from '../lib/validation.js';
16
17
  import { readLocalCommandEffectGraph, readLocalPathSurface, } from '../lib/local-index.js';
17
- import { explainVerifyArgErrorMessage, explainVerifyPlanErrorMessage, getVerifyExplainOutput, parseExplainVerifyArgs, readExplainVerifyPlanReasons, renderVerifyExplainDecision, } from './explain-verify.js';
18
+ import { explainVerifyPlanErrorMessage, getVerifyExplainOutput, readExplainVerifyPlanReasons, renderVerifyExplainDecision, } from './explain-verify.js';
18
19
  import { createRunPlan, } from '../lib/run-plan.js';
19
20
  const EXPLAIN_SCHEMA_VERSION = '1';
20
21
  const LATEST_RUN_RECEIPT_RELATIVE_PATH = '.mustflow/state/runs/latest.json';
22
+ const EXPLAIN_OPTIONS = [
23
+ { name: '--json', kind: 'boolean' },
24
+ { name: '--why-blocked', kind: 'boolean' },
25
+ { name: '--reason', kind: 'string' },
26
+ { name: '--from-plan', kind: 'string' },
27
+ ];
21
28
  export function getExplainHelp(lang = 'en') {
22
29
  return renderHelp({
23
30
  usage: 'mf explain <topic> [target] [options] | mf explain verify --reason <event> [options] | mf explain why <target> [options] | mf explain --why-blocked <intent> [options]',
@@ -322,10 +329,30 @@ function getLatestFailureExplainOutput(projectRoot) {
322
329
  function withWhyTopic(output) {
323
330
  return { ...output, topic: 'why' };
324
331
  }
325
- async function getWhyExplainOutput(projectRoot, targetArg, rest, lang, reporter) {
332
+ function firstVerifyOnlyOption(options) {
333
+ if (options.reason !== null) {
334
+ return '--reason';
335
+ }
336
+ if (options.fromPlan !== null) {
337
+ return '--from-plan';
338
+ }
339
+ return null;
340
+ }
341
+ function rejectVerifyOnlyOption(options, reporter, lang) {
342
+ const option = firstVerifyOnlyOption(options);
343
+ if (!option) {
344
+ return false;
345
+ }
346
+ printUsageError(reporter, t(lang, 'cli.error.unknownOption', { option }), 'mf explain --help', getExplainHelp(lang), lang);
347
+ return true;
348
+ }
349
+ async function getWhyExplainOutput(projectRoot, targetArg, rest, options, lang, reporter) {
326
350
  switch (targetArg) {
327
351
  case 'command':
328
352
  case 'intent': {
353
+ if (rejectVerifyOnlyOption(options, reporter, lang)) {
354
+ return null;
355
+ }
329
356
  const [commandName, ...extra] = rest;
330
357
  if (!commandName) {
331
358
  printUsageError(reporter, t(lang, 'explain.error.missingCommand'), 'mf explain --help', getExplainHelp(lang), lang);
@@ -338,12 +365,11 @@ async function getWhyExplainOutput(projectRoot, targetArg, rest, lang, reporter)
338
365
  return withWhyTopic(await getCommandExplainOutput(projectRoot, commandName));
339
366
  }
340
367
  case 'verify': {
341
- const parsed = parseExplainVerifyArgs([...rest]);
342
- if (parsed.error) {
343
- printUsageError(reporter, explainVerifyArgErrorMessage(parsed.error, lang), 'mf explain --help', getExplainHelp(lang), lang);
368
+ if (rest.length > 0) {
369
+ printUsageError(reporter, t(lang, 'cli.error.unexpectedArgument', { argument: rest[0] }), 'mf explain --help', getExplainHelp(lang), lang);
344
370
  return null;
345
371
  }
346
- const selectedInputCount = [parsed.reason, parsed.fromPlan].filter(Boolean).length;
372
+ const selectedInputCount = [options.reason, options.fromPlan].filter(Boolean).length;
347
373
  if (selectedInputCount > 1) {
348
374
  printUsageError(reporter, t(lang, 'verify.error.conflictingInputs'), 'mf explain --help', getExplainHelp(lang), lang);
349
375
  return null;
@@ -353,11 +379,11 @@ async function getWhyExplainOutput(projectRoot, targetArg, rest, lang, reporter)
353
379
  return null;
354
380
  }
355
381
  try {
356
- if (parsed.fromPlan) {
357
- const reasons = readExplainVerifyPlanReasons(projectRoot, parsed.fromPlan);
358
- return withWhyTopic(await getVerifyExplainOutput(EXPLAIN_SCHEMA_VERSION, projectRoot, reasons, null, parsed.fromPlan));
382
+ if (options.fromPlan) {
383
+ const reasons = readExplainVerifyPlanReasons(projectRoot, options.fromPlan);
384
+ return withWhyTopic(await getVerifyExplainOutput(EXPLAIN_SCHEMA_VERSION, projectRoot, reasons, null, options.fromPlan));
359
385
  }
360
- return withWhyTopic(await getVerifyExplainOutput(EXPLAIN_SCHEMA_VERSION, projectRoot, [parsed.reason], parsed.reason, null));
386
+ return withWhyTopic(await getVerifyExplainOutput(EXPLAIN_SCHEMA_VERSION, projectRoot, [options.reason], options.reason, null));
361
387
  }
362
388
  catch (error) {
363
389
  printUsageError(reporter, explainVerifyPlanErrorMessage(error, lang), 'mf explain --help', getExplainHelp(lang), lang);
@@ -365,6 +391,9 @@ async function getWhyExplainOutput(projectRoot, targetArg, rest, lang, reporter)
365
391
  }
366
392
  }
367
393
  case 'skill': {
394
+ if (rejectVerifyOnlyOption(options, reporter, lang)) {
395
+ return null;
396
+ }
368
397
  const [skillName, ...extra] = rest;
369
398
  if (!skillName) {
370
399
  printUsageError(reporter, t(lang, 'explain.error.missingSkill'), 'mf explain --help', getExplainHelp(lang), lang);
@@ -377,12 +406,18 @@ async function getWhyExplainOutput(projectRoot, targetArg, rest, lang, reporter)
377
406
  return withWhyTopic(getSkillExplainOutput(projectRoot, skillName));
378
407
  }
379
408
  case 'skills':
409
+ if (rejectVerifyOnlyOption(options, reporter, lang)) {
410
+ return null;
411
+ }
380
412
  if (rest.length > 0) {
381
413
  printUsageError(reporter, t(lang, 'cli.error.unexpectedArgument', { argument: rest[0] }), 'mf explain --help', getExplainHelp(lang), lang);
382
414
  return null;
383
415
  }
384
416
  return withWhyTopic(getSkillsExplainOutput(projectRoot));
385
417
  case 'surface': {
418
+ if (rejectVerifyOnlyOption(options, reporter, lang)) {
419
+ return null;
420
+ }
386
421
  const [pathArg, ...extra] = rest;
387
422
  if (extra.length > 0) {
388
423
  printUsageError(reporter, t(lang, 'cli.error.unexpectedArgument', { argument: extra[0] }), 'mf explain --help', getExplainHelp(lang), lang);
@@ -392,6 +427,9 @@ async function getWhyExplainOutput(projectRoot, targetArg, rest, lang, reporter)
392
427
  }
393
428
  case 'latest-failure':
394
429
  case 'latest-run':
430
+ if (rejectVerifyOnlyOption(options, reporter, lang)) {
431
+ return null;
432
+ }
395
433
  if (rest.length > 0) {
396
434
  printUsageError(reporter, t(lang, 'cli.error.unexpectedArgument', { argument: rest[0] }), 'mf explain --help', getExplainHelp(lang), lang);
397
435
  return null;
@@ -549,24 +587,38 @@ function renderExplainDecision(output, lang) {
549
587
  return lines.join('\n');
550
588
  }
551
589
  export async function runExplain(args, reporter, lang = 'en') {
552
- if (args.includes('--help') || args.includes('-h')) {
590
+ if (hasCliOptionToken(args, '--help', ['-h'])) {
553
591
  reporter.stdout(getExplainHelp(lang));
554
592
  return 0;
555
593
  }
556
- const json = args.includes('--json');
557
- const positional = args.filter((arg) => arg !== '--json');
558
- const [topic, targetArg, ...rest] = positional;
559
- if (topic === '--why-blocked') {
560
- if (!targetArg) {
594
+ const parsed = parseCliOptions(args, EXPLAIN_OPTIONS, { allowPositionals: true });
595
+ if (parsed.error) {
596
+ printUsageError(reporter, formatCliOptionParseError(parsed.error, lang), 'mf explain --help', getExplainHelp(lang), lang);
597
+ return 1;
598
+ }
599
+ const options = {
600
+ json: hasParsedCliOption(parsed, '--json'),
601
+ whyBlocked: hasParsedCliOption(parsed, '--why-blocked'),
602
+ reason: getParsedCliStringOption(parsed, '--reason'),
603
+ fromPlan: getParsedCliStringOption(parsed, '--from-plan'),
604
+ positionals: parsed.positionals,
605
+ };
606
+ const [topic, targetArg, ...rest] = options.positionals;
607
+ if (options.whyBlocked) {
608
+ const [commandName, ...extra] = options.positionals;
609
+ if (rejectVerifyOnlyOption(options, reporter, lang)) {
610
+ return 1;
611
+ }
612
+ if (!commandName) {
561
613
  printUsageError(reporter, t(lang, 'explain.error.missingCommand'), 'mf explain --help', getExplainHelp(lang), lang);
562
614
  return 1;
563
615
  }
564
- if (rest.length > 0) {
565
- printUsageError(reporter, t(lang, 'cli.error.unexpectedArgument', { argument: rest[0] }), 'mf explain --help', getExplainHelp(lang), lang);
616
+ if (extra.length > 0) {
617
+ printUsageError(reporter, t(lang, 'cli.error.unexpectedArgument', { argument: extra[0] }), 'mf explain --help', getExplainHelp(lang), lang);
566
618
  return 1;
567
619
  }
568
- const output = getWhyBlockedExplainOutput(resolveMustflowRoot(), targetArg);
569
- if (json) {
620
+ const output = getWhyBlockedExplainOutput(resolveMustflowRoot(), commandName);
621
+ if (options.json) {
570
622
  reporter.stdout(JSON.stringify(output, null, 2));
571
623
  return 0;
572
624
  }
@@ -575,11 +627,11 @@ export async function runExplain(args, reporter, lang = 'en') {
575
627
  }
576
628
  if (topic === 'why') {
577
629
  const projectRoot = resolveMustflowRoot();
578
- const output = await getWhyExplainOutput(projectRoot, targetArg, rest, lang, reporter);
630
+ const output = await getWhyExplainOutput(projectRoot, targetArg, rest, options, lang, reporter);
579
631
  if (!output) {
580
632
  return 1;
581
633
  }
582
- if (json) {
634
+ if (options.json) {
583
635
  reporter.stdout(JSON.stringify(output, null, 2));
584
636
  return 0;
585
637
  }
@@ -587,13 +639,11 @@ export async function runExplain(args, reporter, lang = 'en') {
587
639
  return 0;
588
640
  }
589
641
  if (topic === 'verify') {
590
- const verifyArgs = targetArg === undefined ? rest : [targetArg, ...rest];
591
- const parsed = parseExplainVerifyArgs(verifyArgs);
592
- if (parsed.error) {
593
- printUsageError(reporter, explainVerifyArgErrorMessage(parsed.error, lang), 'mf explain --help', getExplainHelp(lang), lang);
642
+ if (targetArg) {
643
+ printUsageError(reporter, t(lang, 'cli.error.unexpectedArgument', { argument: targetArg }), 'mf explain --help', getExplainHelp(lang), lang);
594
644
  return 1;
595
645
  }
596
- const selectedInputCount = [parsed.reason, parsed.fromPlan].filter(Boolean).length;
646
+ const selectedInputCount = [options.reason, options.fromPlan].filter(Boolean).length;
597
647
  if (selectedInputCount > 1) {
598
648
  printUsageError(reporter, t(lang, 'verify.error.conflictingInputs'), 'mf explain --help', getExplainHelp(lang), lang);
599
649
  return 1;
@@ -605,28 +655,26 @@ export async function runExplain(args, reporter, lang = 'en') {
605
655
  const projectRoot = resolveMustflowRoot();
606
656
  let output;
607
657
  try {
608
- if (parsed.fromPlan) {
609
- const reasons = readExplainVerifyPlanReasons(projectRoot, parsed.fromPlan);
610
- output = await getVerifyExplainOutput(EXPLAIN_SCHEMA_VERSION, projectRoot, reasons, null, parsed.fromPlan);
658
+ if (options.fromPlan) {
659
+ const reasons = readExplainVerifyPlanReasons(projectRoot, options.fromPlan);
660
+ output = await getVerifyExplainOutput(EXPLAIN_SCHEMA_VERSION, projectRoot, reasons, null, options.fromPlan);
611
661
  }
612
662
  else {
613
- output = await getVerifyExplainOutput(EXPLAIN_SCHEMA_VERSION, projectRoot, [parsed.reason], parsed.reason, null);
663
+ output = await getVerifyExplainOutput(EXPLAIN_SCHEMA_VERSION, projectRoot, [options.reason], options.reason, null);
614
664
  }
615
665
  }
616
666
  catch (error) {
617
667
  printUsageError(reporter, explainVerifyPlanErrorMessage(error, lang), 'mf explain --help', getExplainHelp(lang), lang);
618
668
  return 1;
619
669
  }
620
- if (json) {
670
+ if (options.json) {
621
671
  reporter.stdout(JSON.stringify(output, null, 2));
622
672
  return 0;
623
673
  }
624
674
  reporter.stdout(renderExplainDecision(output, lang));
625
675
  return 0;
626
676
  }
627
- const unsupported = args.filter((arg) => arg.startsWith('-') && arg !== '--json');
628
- if (unsupported.length > 0) {
629
- printUsageError(reporter, t(lang, 'cli.error.unknownOption', { option: unsupported[0] }), 'mf explain --help', getExplainHelp(lang), lang);
677
+ if (rejectVerifyOnlyOption(options, reporter, lang)) {
630
678
  return 1;
631
679
  }
632
680
  if (topic !== 'asset-optimization' &&
@@ -700,7 +748,7 @@ export async function runExplain(args, reporter, lang = 'en') {
700
748
  output = getSkillsExplainOutput(projectRoot);
701
749
  break;
702
750
  }
703
- if (json) {
751
+ if (options.json) {
704
752
  reporter.stdout(JSON.stringify(output, null, 2));
705
753
  return 0;
706
754
  }
@@ -4,7 +4,9 @@ import { MAX_HANDOFF_RECORD_BYTES, validateHandoffRecordJson, } from '../../core
4
4
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
5
5
  import { ensureInsideWithoutSymlinks, readUtf8FileInsideWithoutSymlinks } from '../lib/filesystem.js';
6
6
  import { t } from '../lib/i18n.js';
7
+ import { formatCliOptionParseError, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
7
8
  import { resolveMustflowRoot } from '../lib/project-root.js';
9
+ const HANDOFF_OPTIONS = [{ name: '--json', kind: 'boolean' }];
8
10
  export function getHandoffHelp(lang = 'en') {
9
11
  return renderHelp({
10
12
  usage: 'mf handoff validate <path> [options]',
@@ -25,26 +27,20 @@ export function getHandoffHelp(lang = 'en') {
25
27
  }
26
28
  function parseHandoffArgs(args, lang) {
27
29
  const [action, ...rest] = args;
28
- let json = false;
29
- let recordPath;
30
30
  if (!action) {
31
- return { action: 'validate', json, error: t(lang, 'handoff.error.missingAction') };
31
+ return { action: 'validate', json: false, error: t(lang, 'handoff.error.missingAction') };
32
32
  }
33
33
  if (action !== 'validate') {
34
- return { action: 'validate', json, error: t(lang, 'handoff.error.unknownAction', { action }) };
34
+ return { action: 'validate', json: false, error: t(lang, 'handoff.error.unknownAction', { action }) };
35
35
  }
36
- for (const arg of rest) {
37
- if (arg === '--json') {
38
- json = true;
39
- continue;
40
- }
41
- if (arg.startsWith('-')) {
42
- return { action, path: recordPath, json, error: t(lang, 'cli.error.unknownOption', { option: arg }) };
43
- }
44
- if (recordPath) {
45
- return { action, path: recordPath, json, error: t(lang, 'cli.error.unexpectedArgument', { argument: arg }) };
46
- }
47
- recordPath = arg;
36
+ const parsed = parseCliOptions(rest, HANDOFF_OPTIONS, { allowPositionals: true });
37
+ const json = hasParsedCliOption(parsed, '--json');
38
+ const [recordPath, unexpectedArgument] = parsed.positionals;
39
+ if (unexpectedArgument) {
40
+ return { action, path: recordPath, json, error: t(lang, 'cli.error.unexpectedArgument', { argument: unexpectedArgument }) };
41
+ }
42
+ if (parsed.error) {
43
+ return { action, path: recordPath, json, error: formatCliOptionParseError(parsed.error, lang) };
48
44
  }
49
45
  if (!recordPath) {
50
46
  return { action, json, error: t(lang, 'handoff.error.missingPath') };
@@ -94,7 +90,7 @@ function renderHandoffReport(report, lang) {
94
90
  }
95
91
  export function runHandoff(args, reporter, lang = 'en') {
96
92
  const helpText = getHandoffHelp(lang);
97
- if (args.includes('--help') || args.includes('-h')) {
93
+ if (hasCliOptionToken(args, '--help', ['-h'])) {
98
94
  reporter.stdout(helpText);
99
95
  return 0;
100
96
  }
@@ -4,10 +4,15 @@ import { printUsageError, renderHelp } from '../lib/cli-output.js';
4
4
  import { isRecord } from '../lib/command-contract.js';
5
5
  import { requireGitChangedFiles } from '../lib/git-changes.js';
6
6
  import { t } from '../lib/i18n.js';
7
+ import { formatCliOptionParseError, 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';
9
10
  import { detectVersionSources, releaseVersioningIsEnabled, } from '../../core/version-sources.js';
10
11
  const IMPACT_SCHEMA_VERSION = '1';
12
+ const IMPACT_OPTIONS = [
13
+ { name: '--json', kind: 'boolean' },
14
+ { name: '--changed', kind: 'boolean' },
15
+ ];
11
16
  export function getImpactHelp(lang = 'en') {
12
17
  return renderHelp({
13
18
  usage: 'mf impact --changed [options] | mf impact <path...> [options]',
@@ -25,24 +30,13 @@ export function getImpactHelp(lang = 'en') {
25
30
  }, lang);
26
31
  }
27
32
  function parseImpactArgs(args) {
28
- const paths = [];
29
- let json = false;
30
- let changed = false;
31
- for (const arg of args) {
32
- if (arg === '--json') {
33
- json = true;
34
- continue;
35
- }
36
- if (arg === '--changed') {
37
- changed = true;
38
- continue;
39
- }
40
- if (arg.startsWith('-')) {
41
- return { json, changed, paths, error: arg };
42
- }
43
- paths.push(arg);
44
- }
45
- return { json, changed, paths };
33
+ const parsed = parseCliOptions(args, IMPACT_OPTIONS, { allowPositionals: true });
34
+ return {
35
+ json: hasParsedCliOption(parsed, '--json'),
36
+ changed: hasParsedCliOption(parsed, '--changed'),
37
+ paths: parsed.positionals,
38
+ error: parsed.error,
39
+ };
46
40
  }
47
41
  function readPreferences(projectRoot) {
48
42
  try {
@@ -92,13 +86,13 @@ function renderImpactOutput(output, lang) {
92
86
  return lines.join('\n');
93
87
  }
94
88
  export function runImpact(args, reporter, lang = 'en') {
95
- if (args.includes('--help') || args.includes('-h')) {
89
+ if (hasCliOptionToken(args, '--help', ['-h'])) {
96
90
  reporter.stdout(getImpactHelp(lang));
97
91
  return 0;
98
92
  }
99
93
  const parsed = parseImpactArgs(args);
100
94
  if (parsed.error) {
101
- printUsageError(reporter, t(lang, 'cli.error.unknownOption', { option: parsed.error }), 'mf impact --help', getImpactHelp(lang), lang);
95
+ printUsageError(reporter, formatCliOptionParseError(parsed.error, lang), 'mf impact --help', getImpactHelp(lang), lang);
102
96
  return 1;
103
97
  }
104
98
  if (parsed.changed && parsed.paths.length > 0) {
@@ -1,7 +1,14 @@
1
1
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
2
2
  import { t } from '../lib/i18n.js';
3
3
  import { createLocalIndex } from '../lib/local-index.js';
4
+ import { formatCliOptionParseError, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
4
5
  import { resolveMustflowRoot } from '../lib/project-root.js';
6
+ const INDEX_OPTIONS = [
7
+ { name: '--dry-run', kind: 'boolean' },
8
+ { name: '--json', kind: 'boolean' },
9
+ { name: '--source', kind: 'boolean' },
10
+ { name: '--incremental', kind: 'boolean' },
11
+ ];
5
12
  export function getIndexHelp(lang = 'en') {
6
13
  return renderHelp({
7
14
  usage: 'mf index [options]',
@@ -67,22 +74,21 @@ function renderIndexSummary(result, lang) {
67
74
  return lines.join('\n');
68
75
  }
69
76
  export async function runIndex(args, reporter, lang = 'en') {
70
- if (args.includes('--help') || args.includes('-h')) {
77
+ if (hasCliOptionToken(args, '--help', ['-h'])) {
71
78
  reporter.stdout(getIndexHelp(lang));
72
79
  return 0;
73
80
  }
74
- const supported = new Set(['--dry-run', '--json', '--source', '--incremental']);
75
- const unsupported = args.filter((arg) => !supported.has(arg));
76
- if (unsupported.length > 0) {
77
- printUsageError(reporter, t(lang, 'cli.error.unknownOption', { option: unsupported[0] }), 'mf index --help', getIndexHelp(lang), lang);
81
+ const parsed = parseCliOptions(args, INDEX_OPTIONS);
82
+ if (parsed.error) {
83
+ printUsageError(reporter, formatCliOptionParseError(parsed.error, lang), 'mf index --help', getIndexHelp(lang), lang);
78
84
  return 1;
79
85
  }
80
86
  const result = await createLocalIndex(resolveMustflowRoot(), {
81
- dryRun: args.includes('--dry-run'),
82
- includeSource: args.includes('--source'),
83
- incremental: args.includes('--incremental'),
87
+ dryRun: hasParsedCliOption(parsed, '--dry-run'),
88
+ includeSource: hasParsedCliOption(parsed, '--source'),
89
+ incremental: hasParsedCliOption(parsed, '--incremental'),
84
90
  });
85
- if (args.includes('--json')) {
91
+ if (hasParsedCliOption(parsed, '--json')) {
86
92
  reporter.stdout(JSON.stringify(result, null, 2));
87
93
  return 0;
88
94
  }