mustflow 2.23.0 → 2.25.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 (80) 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/i18n.toml +1 -1
  76. package/templates/default/locales/en/.mustflow/skills/INDEX.md +2 -1
  77. package/templates/default/locales/en/.mustflow/skills/clarifying-question-gate/SKILL.md +183 -0
  78. package/templates/default/locales/en/.mustflow/skills/routes.toml +7 -1
  79. package/templates/default/locales/en/.mustflow/skills/structure-discovery-gate/SKILL.md +63 -20
  80. package/templates/default/manifest.toml +8 -1
@@ -0,0 +1,136 @@
1
+ import { existsSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { lintCommandContract, } from '../../core/contract-lint.js';
4
+ import { isRecord, readCommandContract, } from '../../core/config-loading.js';
5
+ import { releaseVersioningIsEnabled } from '../../core/version-sources.js';
6
+ import { printUsageError, renderHelp } from '../lib/cli-output.js';
7
+ import { t } from '../lib/i18n.js';
8
+ import { formatCliOptionParseError, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
9
+ import { resolveMustflowRoot } from '../lib/project-root.js';
10
+ import { readMustflowTomlFile } from '../lib/toml.js';
11
+ const ONBOARD_COMMANDS_SCHEMA_VERSION = '1';
12
+ const COMMAND_CONTRACT_PATH = '.mustflow/config/commands.toml';
13
+ const ONBOARD_COMMANDS_OPTIONS = [
14
+ { name: '--json', kind: 'boolean' },
15
+ ];
16
+ export function getOnboardHelp(lang = 'en') {
17
+ return renderHelp({
18
+ usage: 'mf onboard commands [options]',
19
+ summary: t(lang, 'onboard.help.summary'),
20
+ options: [
21
+ { label: '--json', description: t(lang, 'cli.option.json') },
22
+ { label: '-h, --help', description: t(lang, 'cli.option.help') },
23
+ ],
24
+ examples: ['mf onboard commands', 'mf onboard commands --json'],
25
+ exitCodes: [
26
+ { label: '0', description: t(lang, 'onboard.help.exit.ok') },
27
+ { label: '1', description: t(lang, 'onboard.help.exit.fail') },
28
+ ],
29
+ }, lang);
30
+ }
31
+ function readPreferences(projectRoot) {
32
+ const preferencesPath = path.join(projectRoot, '.mustflow', 'config', 'preferences.toml');
33
+ if (!existsSync(preferencesPath)) {
34
+ return undefined;
35
+ }
36
+ const preferences = readMustflowTomlFile(projectRoot, '.mustflow/config/preferences.toml');
37
+ return isRecord(preferences) ? preferences : undefined;
38
+ }
39
+ function countSuggestionsByKind(suggestions, sourceKind) {
40
+ return suggestions.filter((suggestion) => suggestion.sourceKind === sourceKind).length;
41
+ }
42
+ function createNextSteps(suggestions) {
43
+ if (suggestions.length === 0) {
44
+ return [
45
+ 'No package.json scripts, Makefile targets, or justfile recipes need review-only command-intent suggestions.',
46
+ 'Keep command execution authority in .mustflow/config/commands.toml.',
47
+ ];
48
+ }
49
+ return [
50
+ 'Review each suggested unknown intent before copying it into .mustflow/config/commands.toml.',
51
+ 'Do not add argv, lifecycle, run_policy, stdin, timeout_seconds, writes, network, or destructive fields until a maintainer has reviewed the command behavior.',
52
+ 'After accepting any snippet, run command-contract validation through the configured mustflow check intent.',
53
+ ];
54
+ }
55
+ function createOnboardCommandsOutput(projectRoot) {
56
+ const report = lintCommandContract(readCommandContract(projectRoot), {
57
+ suggest: true,
58
+ projectRoot,
59
+ releaseVersioningEnabled: releaseVersioningIsEnabled(readPreferences(projectRoot)),
60
+ });
61
+ const suggestions = report.suggestions ?? [];
62
+ return {
63
+ schema_version: ONBOARD_COMMANDS_SCHEMA_VERSION,
64
+ command: 'onboard commands',
65
+ mustflow_root: projectRoot,
66
+ command_contract_path: COMMAND_CONTRACT_PATH,
67
+ policy: {
68
+ command_authority: COMMAND_CONTRACT_PATH,
69
+ suggestions_grant_command_authority: false,
70
+ suggestions_are_review_only: true,
71
+ writes_files: false,
72
+ },
73
+ summary: {
74
+ total_intents: report.summary.totalIntents,
75
+ runnable_intents: report.summary.runnable,
76
+ manual_only_intents: report.summary.manualOnly,
77
+ unknown_intents: report.summary.unknown,
78
+ suggestions: suggestions.length,
79
+ package_scripts: countSuggestionsByKind(suggestions, 'package_script'),
80
+ make_targets: countSuggestionsByKind(suggestions, 'make_target'),
81
+ just_recipes: countSuggestionsByKind(suggestions, 'just_recipe'),
82
+ contract_errors: report.summary.errors,
83
+ contract_warnings: report.summary.warnings,
84
+ },
85
+ suggestions,
86
+ next_steps: createNextSteps(suggestions),
87
+ };
88
+ }
89
+ function renderOnboardCommandsOutput(output, lang) {
90
+ const reviewOnlyValue = output.policy.suggestions_are_review_only ? t(lang, 'value.yes') : t(lang, 'value.no');
91
+ const writesFilesValue = output.policy.writes_files ? t(lang, 'value.yes') : t(lang, 'value.no');
92
+ const lines = [
93
+ t(lang, 'onboard.title'),
94
+ `${t(lang, 'label.mustflowRoot')}: ${output.mustflow_root}`,
95
+ `${t(lang, 'label.commandContract')}: ${output.command_contract_path}`,
96
+ `${t(lang, 'onboard.label.suggestions')}: ${output.summary.suggestions}`,
97
+ `${t(lang, 'onboard.label.reviewOnly')}: ${reviewOnlyValue}`,
98
+ `${t(lang, 'onboard.label.writesFiles')}: ${writesFilesValue}`,
99
+ ];
100
+ if (output.suggestions.length > 0) {
101
+ lines.push('', t(lang, 'onboard.label.reviewSnippets'));
102
+ for (const suggestion of output.suggestions) {
103
+ lines.push(`- ${suggestion.sourceFile}:${suggestion.sourceName} -> ${suggestion.suggestedIntent}`);
104
+ lines.push(...suggestion.snippet.split('\n').map((line) => ` ${line}`));
105
+ }
106
+ }
107
+ lines.push('', t(lang, 'onboard.label.nextSteps'));
108
+ for (const step of output.next_steps) {
109
+ lines.push(`- ${step}`);
110
+ }
111
+ return lines.join('\n');
112
+ }
113
+ export function runOnboard(args, reporter, lang = 'en') {
114
+ if (hasCliOptionToken(args, '--help', ['-h'])) {
115
+ reporter.stdout(getOnboardHelp(lang));
116
+ return 0;
117
+ }
118
+ const [action, ...rest] = args;
119
+ if (action !== 'commands') {
120
+ printUsageError(reporter, t(lang, action ? 'onboard.error.unknownAction' : 'onboard.error.missingAction', { action: action ?? '' }), 'mf onboard --help', getOnboardHelp(lang), lang);
121
+ return 1;
122
+ }
123
+ const options = parseCliOptions(rest, ONBOARD_COMMANDS_OPTIONS);
124
+ if (options.error) {
125
+ printUsageError(reporter, formatCliOptionParseError(options.error, lang), 'mf onboard --help', getOnboardHelp(lang), lang);
126
+ return 1;
127
+ }
128
+ const output = createOnboardCommandsOutput(resolveMustflowRoot());
129
+ if (hasParsedCliOption(options, '--json')) {
130
+ reporter.stdout(JSON.stringify(output, null, 2));
131
+ }
132
+ else {
133
+ reporter.stdout(renderOnboardCommandsOutput(output, lang));
134
+ }
135
+ return 0;
136
+ }
@@ -7,6 +7,7 @@ import { printUsageError, renderCliError, renderHelp } from '../lib/cli-output.j
7
7
  import { readCommandContract, readMustflowConfigIfExists } from '../../core/config-loading.js';
8
8
  import { resolveRunReceiptRetentionPolicy } from '../../core/retention-policy.js';
9
9
  import { t } from '../lib/i18n.js';
10
+ import { formatCliOptionParseError, getParsedCliStringOption, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
10
11
  import { resolveMustflowRoot } from '../lib/project-root.js';
11
12
  import { ALLOW_UNTRUSTED_ROOT_OPTION, assessRunRootTrust } from '../lib/run-root-trust.js';
12
13
  import { createRunPlan, createRunPreview, renderRunPreviewText, } from '../lib/run-plan.js';
@@ -20,6 +21,14 @@ import { createPendingTimeoutTermination, getKillMethod, terminateProcessTree }
20
21
  import { assembleRunReceipt } from './run/receipt.js';
21
22
  const DEFAULT_ACTIVE_LOCK_WAIT_TIMEOUT_SECONDS = 300;
22
23
  const ACTIVE_LOCK_WAIT_POLL_MS = 1_000;
24
+ const RUN_OPTIONS = [
25
+ { name: '--json', kind: 'boolean' },
26
+ { name: '--dry-run', kind: 'boolean' },
27
+ { name: '--plan-only', kind: 'boolean' },
28
+ { name: '--wait', kind: 'boolean' },
29
+ { name: ALLOW_UNTRUSTED_ROOT_OPTION, kind: 'boolean' },
30
+ { name: '--wait-timeout', kind: 'string' },
31
+ ];
23
32
  function delay(milliseconds) {
24
33
  return new Promise((resolve) => {
25
34
  setTimeout(resolve, milliseconds);
@@ -125,51 +134,44 @@ function renderActiveLockConflictMessage(intentName, conflicts, lang) {
125
134
  return t(lang, 'run.error.activeLockConflict', { intent: intentName, detail });
126
135
  }
127
136
  function parseRunArguments(args) {
128
- const supportedBooleanOptions = new Set(['--json', '--dry-run', '--plan-only', '--wait', ALLOW_UNTRUSTED_ROOT_OPTION]);
129
- const supportedValueOptions = new Set(['--wait-timeout']);
130
- const positional = [];
131
- const unsupported = [];
137
+ const parsed = parseCliOptions(args, RUN_OPTIONS, { allowPositionals: true });
132
138
  let waitTimeoutSeconds = DEFAULT_ACTIVE_LOCK_WAIT_TIMEOUT_SECONDS;
133
- let invalidWaitTimeout = false;
134
- for (let index = 0; index < args.length; index += 1) {
135
- const arg = args[index];
136
- if (supportedBooleanOptions.has(arg)) {
137
- continue;
138
- }
139
- if (supportedValueOptions.has(arg)) {
140
- const value = args[index + 1];
141
- if (!value || value.startsWith('-')) {
142
- invalidWaitTimeout = true;
143
- continue;
144
- }
145
- const parsed = Number(value);
146
- if (!Number.isInteger(parsed) || parsed <= 0) {
147
- invalidWaitTimeout = true;
148
- }
149
- else {
150
- waitTimeoutSeconds = parsed;
151
- }
152
- index += 1;
153
- continue;
139
+ if (parsed.error) {
140
+ const [intentName, ...extra] = parsed.positionals;
141
+ return {
142
+ json: hasParsedCliOption(parsed, '--json'),
143
+ dryRun: hasParsedCliOption(parsed, '--dry-run'),
144
+ planOnly: hasParsedCliOption(parsed, '--plan-only'),
145
+ allowUntrustedRoot: hasParsedCliOption(parsed, ALLOW_UNTRUSTED_ROOT_OPTION),
146
+ wait: hasParsedCliOption(parsed, '--wait'),
147
+ waitTimeoutSeconds,
148
+ intentName: intentName ?? null,
149
+ extra,
150
+ error: parsed.error,
151
+ };
152
+ }
153
+ const waitTimeoutValue = getParsedCliStringOption(parsed, '--wait-timeout');
154
+ let error = null;
155
+ if (waitTimeoutValue !== null) {
156
+ const parsedWaitTimeout = Number(waitTimeoutValue);
157
+ if (!Number.isInteger(parsedWaitTimeout) || parsedWaitTimeout <= 0) {
158
+ error = 'invalid_wait_timeout';
154
159
  }
155
- if (arg.startsWith('-')) {
156
- unsupported.push(arg);
157
- continue;
160
+ else {
161
+ waitTimeoutSeconds = parsedWaitTimeout;
158
162
  }
159
- positional.push(arg);
160
163
  }
161
- const [intentName, ...extra] = positional;
164
+ const [intentName, ...extra] = parsed.positionals;
162
165
  return {
163
- json: args.includes('--json'),
164
- dryRun: args.includes('--dry-run'),
165
- planOnly: args.includes('--plan-only'),
166
- allowUntrustedRoot: args.includes(ALLOW_UNTRUSTED_ROOT_OPTION),
167
- wait: args.includes('--wait'),
166
+ json: hasParsedCliOption(parsed, '--json'),
167
+ dryRun: hasParsedCliOption(parsed, '--dry-run'),
168
+ planOnly: hasParsedCliOption(parsed, '--plan-only'),
169
+ allowUntrustedRoot: hasParsedCliOption(parsed, ALLOW_UNTRUSTED_ROOT_OPTION),
170
+ wait: hasParsedCliOption(parsed, '--wait'),
168
171
  waitTimeoutSeconds,
169
172
  intentName: intentName ?? null,
170
173
  extra,
171
- unsupported,
172
- invalidWaitTimeout,
174
+ error,
173
175
  };
174
176
  }
175
177
  async function acquireActiveRunLockWithOptionalWait(input) {
@@ -256,17 +258,20 @@ export function getRunHelp(lang = 'en') {
256
258
  export async function runRun(args, reporter, lang = 'en', options = {}) {
257
259
  const executorStartedAtMs = performance.now();
258
260
  const profiler = new RunProfiler();
259
- if (args.includes('--help') || args.includes('-h')) {
261
+ if (hasCliOptionToken(args, '--help', ['-h'])) {
260
262
  reporter.stdout(getRunHelp(lang));
261
263
  return 0;
262
264
  }
263
265
  const parsedArgs = parseRunArguments(args);
264
- const unsupported = parsedArgs.unsupported;
265
- if (unsupported.length > 0) {
266
- printUsageError(reporter, t(lang, 'cli.error.unknownOption', { option: unsupported[0] }), 'mf run --help', getRunHelp(lang), lang);
266
+ if (parsedArgs.error && parsedArgs.error !== 'invalid_wait_timeout') {
267
+ if (parsedArgs.error.kind === 'missing_value' && parsedArgs.error.option === '--wait-timeout') {
268
+ printUsageError(reporter, t(lang, 'run.error.invalidWaitTimeout'), 'mf run --help', getRunHelp(lang), lang);
269
+ return 1;
270
+ }
271
+ printUsageError(reporter, formatCliOptionParseError(parsedArgs.error, lang), 'mf run --help', getRunHelp(lang), lang);
267
272
  return 1;
268
273
  }
269
- if (parsedArgs.invalidWaitTimeout) {
274
+ if (parsedArgs.error === 'invalid_wait_timeout') {
270
275
  printUsageError(reporter, t(lang, 'run.error.invalidWaitTimeout'), 'mf run --help', getRunHelp(lang), lang);
271
276
  return 1;
272
277
  }
@@ -1,7 +1,13 @@
1
1
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
2
2
  import { t } from '../lib/i18n.js';
3
3
  import { searchLocalIndex } from '../lib/local-index.js';
4
+ import { getParsedCliStringOption, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
4
5
  import { resolveMustflowRoot } from '../lib/project-root.js';
6
+ const SEARCH_OPTIONS = [
7
+ { name: '--limit', kind: 'string' },
8
+ { name: '--scope', kind: 'string' },
9
+ { name: '--json', kind: 'boolean' },
10
+ ];
5
11
  export function getSearchHelp(lang = 'en') {
6
12
  return renderHelp({
7
13
  usage: 'mf search <query> [options]',
@@ -37,80 +43,48 @@ export function getSearchHelp(lang = 'en') {
37
43
  ],
38
44
  }, lang);
39
45
  }
46
+ function toSearchOptionError(error) {
47
+ if (error.kind === 'missing_value' && error.option === '--limit') {
48
+ return { key: 'search.error.missingLimit' };
49
+ }
50
+ if (error.kind === 'missing_value' && error.option === '--scope') {
51
+ return { key: 'search.error.missingScope' };
52
+ }
53
+ if (error.kind === 'missing_value') {
54
+ return { key: 'cli.error.missingValue', params: { option: error.option } };
55
+ }
56
+ return { key: 'cli.error.unknownOption', params: { option: error.option } };
57
+ }
40
58
  function parseSearchOptions(args) {
41
- const queryParts = [];
59
+ const parsed = parseCliOptions(args, SEARCH_OPTIONS, { allowPositionals: true });
42
60
  let limit = 10;
43
61
  let scope = 'workflow';
44
- let json = false;
45
- for (let index = 0; index < args.length; index += 1) {
46
- const arg = args[index];
47
- if (!arg) {
48
- continue;
49
- }
50
- if (arg === '--json') {
51
- json = true;
52
- continue;
53
- }
54
- if (arg === '--limit') {
55
- const rawLimit = args[index + 1];
56
- if (!rawLimit || rawLimit.startsWith('-')) {
57
- return { query: '', limit, scope, json, error: { key: 'search.error.missingLimit' } };
58
- }
59
- const parsedLimit = Number.parseInt(rawLimit, 10);
60
- if (!Number.isInteger(parsedLimit) || parsedLimit < 1 || parsedLimit > 50) {
61
- return { query: '', limit, scope, json, error: { key: 'search.error.invalidLimit' } };
62
- }
63
- limit = parsedLimit;
64
- index += 1;
65
- continue;
66
- }
67
- if (arg.startsWith('--limit=')) {
68
- const rawLimit = arg.slice('--limit='.length);
69
- const parsedLimit = Number.parseInt(rawLimit, 10);
70
- if (!Number.isInteger(parsedLimit) || parsedLimit < 1 || parsedLimit > 50) {
71
- return { query: '', limit, scope, json, error: { key: 'search.error.invalidLimit' } };
72
- }
73
- limit = parsedLimit;
74
- continue;
75
- }
76
- if (arg === '--scope') {
77
- const rawScope = args[index + 1];
78
- if (!rawScope || rawScope.startsWith('-')) {
79
- return { query: '', limit, scope, json, error: { key: 'search.error.missingScope' } };
80
- }
81
- if (!isSearchScope(rawScope)) {
82
- return {
83
- query: '',
84
- limit,
85
- scope,
86
- json,
87
- error: { key: 'search.error.invalidScope', params: { scope: rawScope } },
88
- };
89
- }
90
- scope = rawScope;
91
- index += 1;
92
- continue;
93
- }
94
- if (arg.startsWith('--scope=')) {
95
- const rawScope = arg.slice('--scope='.length);
96
- if (!isSearchScope(rawScope)) {
97
- return {
98
- query: '',
99
- limit,
100
- scope,
101
- json,
102
- error: { key: 'search.error.invalidScope', params: { scope: rawScope } },
103
- };
104
- }
105
- scope = rawScope;
106
- continue;
62
+ const json = hasParsedCliOption(parsed, '--json');
63
+ if (parsed.error) {
64
+ return { query: '', limit, scope, json, error: toSearchOptionError(parsed.error) };
65
+ }
66
+ const rawLimit = getParsedCliStringOption(parsed, '--limit');
67
+ if (rawLimit !== null) {
68
+ const parsedLimit = Number.parseInt(rawLimit, 10);
69
+ if (!Number.isInteger(parsedLimit) || parsedLimit < 1 || parsedLimit > 50) {
70
+ return { query: '', limit, scope, json, error: { key: 'search.error.invalidLimit' } };
107
71
  }
108
- if (arg.startsWith('-')) {
109
- return { query: '', limit, scope, json, error: { key: 'cli.error.unknownOption', params: { option: arg } } };
72
+ limit = parsedLimit;
73
+ }
74
+ const rawScope = getParsedCliStringOption(parsed, '--scope');
75
+ if (rawScope !== null) {
76
+ if (!isSearchScope(rawScope)) {
77
+ return {
78
+ query: '',
79
+ limit,
80
+ scope,
81
+ json,
82
+ error: { key: 'search.error.invalidScope', params: { scope: rawScope } },
83
+ };
110
84
  }
111
- queryParts.push(arg);
85
+ scope = rawScope;
112
86
  }
113
- const query = queryParts.join(' ').trim();
87
+ const query = parsed.positionals.join(' ').trim();
114
88
  if (query.length === 0) {
115
89
  return { query, limit, scope, json, error: { key: 'search.error.missingQuery' } };
116
90
  }
@@ -140,7 +114,7 @@ function renderSearchSummary(result, lang) {
140
114
  return lines.join('\n');
141
115
  }
142
116
  export async function runSearch(args, reporter, lang = 'en') {
143
- if (args.includes('--help') || args.includes('-h')) {
117
+ if (hasCliOptionToken(args, '--help', ['-h'])) {
144
118
  reporter.stdout(getSearchHelp(lang));
145
119
  return 0;
146
120
  }
@@ -3,7 +3,11 @@ 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 { inspectManifestLock } from '../lib/manifest-lock.js';
6
+ import { formatCliOptionParseError, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
6
7
  import { resolveMustflowRoot } from '../lib/project-root.js';
8
+ const STATUS_OPTIONS = [
9
+ { name: '--json', kind: 'boolean' },
10
+ ];
7
11
  export function getStatusHelp(lang = 'en') {
8
12
  return renderHelp({
9
13
  usage: 'mf status [options]',
@@ -36,19 +40,18 @@ function getStatusSnapshot(projectRoot) {
36
40
  };
37
41
  }
38
42
  export function runStatus(args, reporter, lang = 'en') {
39
- if (args.includes('--help') || args.includes('-h')) {
43
+ if (hasCliOptionToken(args, '--help', ['-h'])) {
40
44
  reporter.stdout(getStatusHelp(lang));
41
45
  return 0;
42
46
  }
43
- const supported = new Set(['--json']);
44
- const unsupported = args.filter((arg) => !supported.has(arg));
45
- if (unsupported.length > 0) {
46
- printUsageError(reporter, t(lang, 'cli.error.unknownOption', { option: unsupported[0] }), 'mf status --help', getStatusHelp(lang), lang);
47
+ const options = parseCliOptions(args, STATUS_OPTIONS);
48
+ if (options.error) {
49
+ printUsageError(reporter, formatCliOptionParseError(options.error, lang), 'mf status --help', getStatusHelp(lang), lang);
47
50
  return 1;
48
51
  }
49
52
  const projectRoot = resolveMustflowRoot();
50
53
  const status = getStatusSnapshot(projectRoot);
51
- if (args.includes('--json')) {
54
+ if (hasParsedCliOption(options, '--json')) {
52
55
  reporter.stdout(JSON.stringify(status, null, 2));
53
56
  return 0;
54
57
  }
@@ -5,12 +5,19 @@ import { copyFileInsideWithoutSymlinks, ensureFileTargetInsideWithoutSymlinks, e
5
5
  import { MANIFEST_LOCK_RELATIVE_PATH, readManifestLock, sha256File } from '../lib/manifest-lock.js';
6
6
  import { printUsageError, renderHelp } from '../lib/cli-output.js';
7
7
  import { t } from '../lib/i18n.js';
8
+ import { formatCliOptionParseError, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
8
9
  import { resolveMustflowRoot } from '../lib/project-root.js';
9
10
  import { getDefaultTemplate, getTemplateFiles, skillNameForTemplatePath } from '../lib/templates.js';
10
11
  import { readMustflowTomlFile, stringifyToml } from '../lib/toml.js';
11
12
  import { createUpdateDiffPreview, shouldPreviewUpdateDiff } from '../lib/update-diff-preview.js';
12
13
  const UPDATE_SCHEMA_VERSION = '1';
13
14
  const CUSTOMIZED_LOCK_ACTION = 'customized';
15
+ const UPDATE_OPTIONS = [
16
+ { name: '--dry-run', kind: 'boolean' },
17
+ { name: '--apply', kind: 'boolean' },
18
+ { name: '--json', kind: 'boolean' },
19
+ { name: '--diff', kind: 'boolean' },
20
+ ];
14
21
  const UPDATE_POLICY = {
15
22
  baseline: 'manifest_lock_content_hash',
16
23
  allowed_apply_actions: ['update', 'create'],
@@ -381,21 +388,20 @@ function printDiffPreviews(items, reporter, lang) {
381
388
  }
382
389
  }
383
390
  export function runUpdate(args, reporter, lang = 'en') {
384
- if (args.includes('--help') || args.includes('-h')) {
391
+ if (hasCliOptionToken(args, '--help', ['-h'])) {
385
392
  reporter.stdout(getUpdateHelp(lang));
386
393
  return 0;
387
394
  }
388
- const supported = new Set(['--dry-run', '--apply', '--json', '--diff']);
389
- const unsupported = args.filter((arg) => !supported.has(arg));
390
- const wantsJson = args.includes('--json');
391
- const wantsDryRun = args.includes('--dry-run');
392
- const wantsApply = args.includes('--apply');
393
- const wantsDiff = args.includes('--diff');
394
- const requestedMode = getRequestedMode(wantsDryRun, wantsApply);
395
- if (unsupported.length > 0) {
396
- printUsageError(reporter, t(lang, 'cli.error.unknownOption', { option: unsupported[0] }), 'mf update --help', getUpdateHelp(lang), lang);
395
+ const parsed = parseCliOptions(args, UPDATE_OPTIONS);
396
+ if (parsed.error) {
397
+ printUsageError(reporter, formatCliOptionParseError(parsed.error, lang), 'mf update --help', getUpdateHelp(lang), lang);
397
398
  return 1;
398
399
  }
400
+ const wantsJson = hasParsedCliOption(parsed, '--json');
401
+ const wantsDryRun = hasParsedCliOption(parsed, '--dry-run');
402
+ const wantsApply = hasParsedCliOption(parsed, '--apply');
403
+ const wantsDiff = hasParsedCliOption(parsed, '--diff');
404
+ const requestedMode = getRequestedMode(wantsDryRun, wantsApply);
399
405
  if (wantsDryRun && wantsApply) {
400
406
  const error = t(lang, 'update.error.cannotCombineModes');
401
407
  if (wantsJson) {
@@ -1,8 +1,12 @@
1
1
  import { printUsageError, renderCliError, renderHelp } from '../lib/cli-output.js';
2
2
  import { t } from '../lib/i18n.js';
3
3
  import { checkNpmLatestVersion } from '../lib/npm-version-check.js';
4
+ import { formatCliOptionParseError, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
4
5
  import { readPackageMetadata } from '../lib/package-info.js';
5
6
  import { runUpdate } from './update.js';
7
+ const UPGRADE_OPTIONS = [
8
+ { name: '--dry-run', kind: 'boolean' },
9
+ ];
6
10
  export function getUpgradeHelp(lang = 'en') {
7
11
  return renderHelp({
8
12
  usage: 'mf upgrade [options]',
@@ -32,17 +36,16 @@ function printPackageCheck(check, reporter, lang) {
32
36
  }
33
37
  }
34
38
  export async function runUpgrade(args, reporter, lang = 'en') {
35
- if (args.includes('--help') || args.includes('-h')) {
39
+ if (hasCliOptionToken(args, '--help', ['-h'])) {
36
40
  reporter.stdout(getUpgradeHelp(lang));
37
41
  return 0;
38
42
  }
39
- const supported = new Set(['--dry-run']);
40
- const unsupported = args.filter((arg) => !supported.has(arg));
41
- if (unsupported.length > 0) {
42
- printUsageError(reporter, t(lang, 'cli.error.unknownOption', { option: unsupported[0] }), 'mf upgrade --help', getUpgradeHelp(lang), lang);
43
+ const options = parseCliOptions(args, UPGRADE_OPTIONS);
44
+ if (options.error) {
45
+ printUsageError(reporter, formatCliOptionParseError(options.error, lang), 'mf upgrade --help', getUpgradeHelp(lang), lang);
43
46
  return 1;
44
47
  }
45
- const dryRun = args.includes('--dry-run');
48
+ const dryRun = hasParsedCliOption(options, '--dry-run');
46
49
  reporter.stdout(t(lang, 'upgrade.title'));
47
50
  reporter.stdout('');
48
51
  reporter.stdout(t(lang, 'upgrade.packageSection'));