mustflow 2.108.0 → 2.108.3
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/README.md +3 -0
- package/dist/cli/commands/api/serve.js +73 -10
- package/dist/cli/commands/script-pack.js +3 -0
- package/dist/cli/i18n/en.js +37 -0
- package/dist/cli/i18n/es.js +37 -0
- package/dist/cli/i18n/fr.js +37 -0
- package/dist/cli/i18n/hi.js +37 -0
- package/dist/cli/i18n/ko.js +37 -0
- package/dist/cli/i18n/zh.js +37 -0
- package/dist/cli/lib/command-registry.js +3 -0
- package/dist/cli/lib/script-pack-registry.js +84 -0
- package/dist/cli/script-packs/repo-automation-surface.js +88 -0
- package/dist/cli/script-packs/repo-dependency-surface.js +87 -0
- package/dist/cli/script-packs/repo-toolchain-provenance.js +90 -0
- package/dist/core/public-json-contracts.js +27 -0
- package/dist/core/repo-automation-surface.js +376 -0
- package/dist/core/repo-dependency-surface.js +282 -0
- package/dist/core/repo-toolchain-provenance.js +421 -0
- package/dist/core/run-receipt-state.js +23 -2
- package/dist/core/script-pack-suggestions.js +33 -1
- package/dist/core/secret-redaction.js +6 -1
- package/package.json +1 -1
- package/schemas/README.md +10 -0
- package/schemas/api-serve-response.schema.json +1 -0
- package/schemas/repo-automation-surface-report.schema.json +148 -0
- package/schemas/repo-dependency-surface-report.schema.json +121 -0
- package/schemas/repo-toolchain-provenance-report.schema.json +124 -0
- package/templates/default/i18n.toml +9 -9
- package/templates/default/locales/en/.mustflow/skills/INDEX.md +17 -14
- package/templates/default/locales/en/.mustflow/skills/ci-pipeline-triage/SKILL.md +39 -11
- package/templates/default/locales/en/.mustflow/skills/cloud-cost-guardrail-review/SKILL.md +4 -1
- package/templates/default/locales/en/.mustflow/skills/go-code-change/SKILL.md +56 -17
- package/templates/default/locales/en/.mustflow/skills/python-code-change/SKILL.md +86 -27
- package/templates/default/locales/en/.mustflow/skills/routes.toml +4 -4
- package/templates/default/locales/en/.mustflow/skills/rust-code-change/SKILL.md +51 -32
- package/templates/default/locales/en/.mustflow/skills/tauri-code-change/SKILL.md +41 -3
- package/templates/default/locales/en/.mustflow/skills/typescript-code-change/SKILL.md +47 -29
- package/templates/default/locales/en/.mustflow/skills/wails-code-change/SKILL.md +34 -4
- package/templates/default/manifest.toml +1 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { printUsageError, renderHelp } from '../lib/cli-output.js';
|
|
2
|
+
import { t } from '../lib/i18n.js';
|
|
3
|
+
import { formatCliOptionParseError, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
|
|
4
|
+
import { resolveMustflowRoot } from '../lib/project-root.js';
|
|
5
|
+
import { inspectRepoAutomationSurface, REPO_AUTOMATION_SURFACE_SCRIPT_REF, } from '../../core/repo-automation-surface.js';
|
|
6
|
+
const REPO_AUTOMATION_SURFACE_OPTIONS = [{ name: '--json', kind: 'boolean' }];
|
|
7
|
+
export function getRepoAutomationSurfaceHelp(lang = 'en') {
|
|
8
|
+
return renderHelp({
|
|
9
|
+
usage: 'mf script-pack run repo/automation-surface inspect [options]',
|
|
10
|
+
summary: t(lang, 'automationSurface.help.summary'),
|
|
11
|
+
options: [
|
|
12
|
+
{ label: '--json', description: t(lang, 'cli.option.json') },
|
|
13
|
+
{ label: '-h, --help', description: t(lang, 'cli.option.help') },
|
|
14
|
+
],
|
|
15
|
+
examples: [
|
|
16
|
+
'mf script-pack run repo/automation-surface inspect',
|
|
17
|
+
'mf script-pack run repo/automation-surface inspect --json',
|
|
18
|
+
],
|
|
19
|
+
exitCodes: [
|
|
20
|
+
{ label: '0', description: t(lang, 'automationSurface.help.exit.ok') },
|
|
21
|
+
{ label: '1', description: t(lang, 'automationSurface.help.exit.fail') },
|
|
22
|
+
],
|
|
23
|
+
}, lang);
|
|
24
|
+
}
|
|
25
|
+
function parseRepoAutomationSurfaceOptions(args, lang) {
|
|
26
|
+
const [action, ...rest] = args;
|
|
27
|
+
const parsed = parseCliOptions(rest, REPO_AUTOMATION_SURFACE_OPTIONS, { allowPositionals: false });
|
|
28
|
+
const json = hasParsedCliOption(parsed, '--json');
|
|
29
|
+
if (action !== 'inspect') {
|
|
30
|
+
return {
|
|
31
|
+
action: 'inspect',
|
|
32
|
+
json,
|
|
33
|
+
error: action
|
|
34
|
+
? t(lang, 'automationSurface.error.unknownAction', { action })
|
|
35
|
+
: t(lang, 'automationSurface.error.missingAction'),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (parsed.error) {
|
|
39
|
+
return { action, json, error: formatCliOptionParseError(parsed.error, lang) };
|
|
40
|
+
}
|
|
41
|
+
return { action, json };
|
|
42
|
+
}
|
|
43
|
+
function renderRepoAutomationSurfaceSummary(report, lang) {
|
|
44
|
+
const lines = [
|
|
45
|
+
t(lang, 'automationSurface.title'),
|
|
46
|
+
`${t(lang, 'scriptPack.label.script')}: ${REPO_AUTOMATION_SURFACE_SCRIPT_REF}`,
|
|
47
|
+
`${t(lang, 'label.status')}: ${report.status}`,
|
|
48
|
+
`${t(lang, 'automationSurface.label.surfaces')}: ${report.summary.surface_count}`,
|
|
49
|
+
`${t(lang, 'automationSurface.label.risky')}: ${report.summary.risky_surface_count}`,
|
|
50
|
+
`${t(lang, 'automationSurface.label.findings')}: ${report.findings.length}`,
|
|
51
|
+
];
|
|
52
|
+
if (report.surfaces.length > 0) {
|
|
53
|
+
lines.push(t(lang, 'automationSurface.label.surfaceDetails'));
|
|
54
|
+
for (const surface of report.surfaces.slice(0, 30)) {
|
|
55
|
+
const location = surface.line === null ? surface.path : `${surface.path}:${surface.line}`;
|
|
56
|
+
const mapped = surface.mapped_intent === null ? 'unmapped' : `intent ${surface.mapped_intent}`;
|
|
57
|
+
lines.push(`- ${surface.name} (${surface.category}, ${surface.kind}, ${mapped}) at ${location}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (report.findings.length > 0) {
|
|
61
|
+
lines.push(t(lang, 'automationSurface.label.findings'));
|
|
62
|
+
for (const finding of report.findings) {
|
|
63
|
+
lines.push(`- ${finding.path}: ${finding.code} (${finding.message})`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (report.issues.length > 0) {
|
|
67
|
+
lines.push(t(lang, 'automationSurface.label.issues'), ...report.issues.map((issue) => `- ${issue}`));
|
|
68
|
+
}
|
|
69
|
+
return lines.join('\n');
|
|
70
|
+
}
|
|
71
|
+
export function runRepoAutomationSurfaceScript(args, reporter, lang = 'en') {
|
|
72
|
+
if (hasCliOptionToken(args, '--help', ['-h'])) {
|
|
73
|
+
reporter.stdout(getRepoAutomationSurfaceHelp(lang));
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
const options = parseRepoAutomationSurfaceOptions(args, lang);
|
|
77
|
+
if (options.error) {
|
|
78
|
+
printUsageError(reporter, options.error, 'mf script-pack run repo/automation-surface --help', getRepoAutomationSurfaceHelp(lang), lang);
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
const report = inspectRepoAutomationSurface(resolveMustflowRoot());
|
|
82
|
+
if (options.json) {
|
|
83
|
+
reporter.stdout(JSON.stringify(report, null, 2));
|
|
84
|
+
return report.ok ? 0 : 1;
|
|
85
|
+
}
|
|
86
|
+
reporter.stdout(renderRepoAutomationSurfaceSummary(report, lang));
|
|
87
|
+
return report.ok ? 0 : 1;
|
|
88
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { printUsageError, renderHelp } from '../lib/cli-output.js';
|
|
2
|
+
import { t } from '../lib/i18n.js';
|
|
3
|
+
import { formatCliOptionParseError, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
|
|
4
|
+
import { resolveMustflowRoot } from '../lib/project-root.js';
|
|
5
|
+
import { inspectRepoDependencySurface, REPO_DEPENDENCY_SURFACE_SCRIPT_REF, } from '../../core/repo-dependency-surface.js';
|
|
6
|
+
const REPO_DEPENDENCY_SURFACE_OPTIONS = [{ name: '--json', kind: 'boolean' }];
|
|
7
|
+
export function getRepoDependencySurfaceHelp(lang = 'en') {
|
|
8
|
+
return renderHelp({
|
|
9
|
+
usage: 'mf script-pack run repo/dependency-surface inspect [options]',
|
|
10
|
+
summary: t(lang, 'dependencySurface.help.summary'),
|
|
11
|
+
options: [
|
|
12
|
+
{ label: '--json', description: t(lang, 'cli.option.json') },
|
|
13
|
+
{ label: '-h, --help', description: t(lang, 'cli.option.help') },
|
|
14
|
+
],
|
|
15
|
+
examples: [
|
|
16
|
+
'mf script-pack run repo/dependency-surface inspect',
|
|
17
|
+
'mf script-pack run repo/dependency-surface inspect --json',
|
|
18
|
+
],
|
|
19
|
+
exitCodes: [
|
|
20
|
+
{ label: '0', description: t(lang, 'dependencySurface.help.exit.ok') },
|
|
21
|
+
{ label: '1', description: t(lang, 'dependencySurface.help.exit.fail') },
|
|
22
|
+
],
|
|
23
|
+
}, lang);
|
|
24
|
+
}
|
|
25
|
+
function parseRepoDependencySurfaceOptions(args, lang) {
|
|
26
|
+
const [action, ...rest] = args;
|
|
27
|
+
const parsed = parseCliOptions(rest, REPO_DEPENDENCY_SURFACE_OPTIONS, { allowPositionals: false });
|
|
28
|
+
const json = hasParsedCliOption(parsed, '--json');
|
|
29
|
+
if (action !== 'inspect') {
|
|
30
|
+
return {
|
|
31
|
+
action: 'inspect',
|
|
32
|
+
json,
|
|
33
|
+
error: action
|
|
34
|
+
? t(lang, 'dependencySurface.error.unknownAction', { action })
|
|
35
|
+
: t(lang, 'dependencySurface.error.missingAction'),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (parsed.error) {
|
|
39
|
+
return { action, json, error: formatCliOptionParseError(parsed.error, lang) };
|
|
40
|
+
}
|
|
41
|
+
return { action, json };
|
|
42
|
+
}
|
|
43
|
+
function renderRepoDependencySurfaceSummary(report, lang) {
|
|
44
|
+
const lines = [
|
|
45
|
+
t(lang, 'dependencySurface.title'),
|
|
46
|
+
`${t(lang, 'scriptPack.label.script')}: ${REPO_DEPENDENCY_SURFACE_SCRIPT_REF}`,
|
|
47
|
+
`${t(lang, 'label.status')}: ${report.status}`,
|
|
48
|
+
`${t(lang, 'dependencySurface.label.surfaces')}: ${report.summary.surface_count}`,
|
|
49
|
+
`${t(lang, 'dependencySurface.label.ecosystems')}: ${report.summary.ecosystem_count}`,
|
|
50
|
+
`${t(lang, 'dependencySurface.label.findings')}: ${report.summary.finding_count}`,
|
|
51
|
+
];
|
|
52
|
+
if (report.surfaces.length > 0) {
|
|
53
|
+
lines.push(t(lang, 'dependencySurface.label.surfaceDetails'));
|
|
54
|
+
for (const surface of report.surfaces) {
|
|
55
|
+
const location = surface.line === null ? surface.path : `${surface.path}:${surface.line}`;
|
|
56
|
+
lines.push(`- ${surface.ecosystem} ${surface.kind} ${surface.name} at ${location}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (report.findings.length > 0) {
|
|
60
|
+
lines.push(t(lang, 'dependencySurface.label.findings'));
|
|
61
|
+
for (const finding of report.findings) {
|
|
62
|
+
lines.push(`- ${finding.path}: ${finding.code} (${finding.message})`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (report.issues.length > 0) {
|
|
66
|
+
lines.push(t(lang, 'dependencySurface.label.issues'), ...report.issues.map((issue) => `- ${issue}`));
|
|
67
|
+
}
|
|
68
|
+
return lines.join('\n');
|
|
69
|
+
}
|
|
70
|
+
export function runRepoDependencySurfaceScript(args, reporter, lang = 'en') {
|
|
71
|
+
if (hasCliOptionToken(args, '--help', ['-h'])) {
|
|
72
|
+
reporter.stdout(getRepoDependencySurfaceHelp(lang));
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
const options = parseRepoDependencySurfaceOptions(args, lang);
|
|
76
|
+
if (options.error) {
|
|
77
|
+
printUsageError(reporter, options.error, 'mf script-pack run repo/dependency-surface --help', getRepoDependencySurfaceHelp(lang), lang);
|
|
78
|
+
return 1;
|
|
79
|
+
}
|
|
80
|
+
const report = inspectRepoDependencySurface(resolveMustflowRoot());
|
|
81
|
+
if (options.json) {
|
|
82
|
+
reporter.stdout(JSON.stringify(report, null, 2));
|
|
83
|
+
return report.ok ? 0 : 1;
|
|
84
|
+
}
|
|
85
|
+
reporter.stdout(renderRepoDependencySurfaceSummary(report, lang));
|
|
86
|
+
return report.ok ? 0 : 1;
|
|
87
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { printUsageError, renderHelp } from '../lib/cli-output.js';
|
|
2
|
+
import { t } from '../lib/i18n.js';
|
|
3
|
+
import { formatCliOptionParseError, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
|
|
4
|
+
import { resolveMustflowRoot } from '../lib/project-root.js';
|
|
5
|
+
import { inspectRepoToolchainProvenance, REPO_TOOLCHAIN_PROVENANCE_SCRIPT_REF, } from '../../core/repo-toolchain-provenance.js';
|
|
6
|
+
const REPO_TOOLCHAIN_PROVENANCE_OPTIONS = [{ name: '--json', kind: 'boolean' }];
|
|
7
|
+
export function getRepoToolchainProvenanceHelp(lang = 'en') {
|
|
8
|
+
return renderHelp({
|
|
9
|
+
usage: 'mf script-pack run repo/toolchain-provenance inspect [options]',
|
|
10
|
+
summary: t(lang, 'toolchainProvenance.help.summary'),
|
|
11
|
+
options: [
|
|
12
|
+
{ label: '--json', description: t(lang, 'cli.option.json') },
|
|
13
|
+
{ label: '-h, --help', description: t(lang, 'cli.option.help') },
|
|
14
|
+
],
|
|
15
|
+
examples: [
|
|
16
|
+
'mf script-pack run repo/toolchain-provenance inspect',
|
|
17
|
+
'mf script-pack run repo/toolchain-provenance inspect --json',
|
|
18
|
+
],
|
|
19
|
+
exitCodes: [
|
|
20
|
+
{ label: '0', description: t(lang, 'toolchainProvenance.help.exit.ok') },
|
|
21
|
+
{ label: '1', description: t(lang, 'toolchainProvenance.help.exit.fail') },
|
|
22
|
+
],
|
|
23
|
+
}, lang);
|
|
24
|
+
}
|
|
25
|
+
function parseRepoToolchainProvenanceOptions(args, lang) {
|
|
26
|
+
const [action, ...rest] = args;
|
|
27
|
+
const parsed = parseCliOptions(rest, REPO_TOOLCHAIN_PROVENANCE_OPTIONS, { allowPositionals: false });
|
|
28
|
+
const json = hasParsedCliOption(parsed, '--json');
|
|
29
|
+
if (action !== 'inspect') {
|
|
30
|
+
return {
|
|
31
|
+
action: 'inspect',
|
|
32
|
+
json,
|
|
33
|
+
error: action
|
|
34
|
+
? t(lang, 'toolchainProvenance.error.unknownAction', { action })
|
|
35
|
+
: t(lang, 'toolchainProvenance.error.missingAction'),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (parsed.error) {
|
|
39
|
+
return { action, json, error: formatCliOptionParseError(parsed.error, lang) };
|
|
40
|
+
}
|
|
41
|
+
return { action, json };
|
|
42
|
+
}
|
|
43
|
+
function renderRepoToolchainProvenanceSummary(report, lang) {
|
|
44
|
+
const lines = [
|
|
45
|
+
t(lang, 'toolchainProvenance.title'),
|
|
46
|
+
`${t(lang, 'scriptPack.label.script')}: ${REPO_TOOLCHAIN_PROVENANCE_SCRIPT_REF}`,
|
|
47
|
+
`${t(lang, 'label.status')}: ${report.status}`,
|
|
48
|
+
`${t(lang, 'toolchainProvenance.label.sources')}: ${report.summary.source_count}`,
|
|
49
|
+
`${t(lang, 'toolchainProvenance.label.lockfiles')}: ${report.summary.lockfile_count}`,
|
|
50
|
+
`${t(lang, 'toolchainProvenance.label.findings')}: ${report.summary.finding_count}`,
|
|
51
|
+
];
|
|
52
|
+
if (report.sources.length > 0) {
|
|
53
|
+
lines.push(t(lang, 'toolchainProvenance.label.sourceDetails'));
|
|
54
|
+
for (const source of report.sources) {
|
|
55
|
+
const location = source.line === null ? source.path : `${source.path}:${source.line}`;
|
|
56
|
+
lines.push(`- ${source.kind} ${source.key}=${source.value} (${source.source_kind}, ${location})`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (report.findings.length > 0) {
|
|
60
|
+
lines.push(t(lang, 'toolchainProvenance.label.findings'));
|
|
61
|
+
for (const finding of report.findings) {
|
|
62
|
+
lines.push(`- ${finding.path}: ${finding.code} (${finding.message})`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (report.issues.length > 0) {
|
|
66
|
+
lines.push(t(lang, 'toolchainProvenance.label.issues'), ...report.issues.map((issue) => `- ${issue}`));
|
|
67
|
+
}
|
|
68
|
+
if (report.sources.length === 0 && report.findings.length === 0 && report.issues.length === 0) {
|
|
69
|
+
lines.push(t(lang, 'toolchainProvenance.clean'));
|
|
70
|
+
}
|
|
71
|
+
return lines.join('\n');
|
|
72
|
+
}
|
|
73
|
+
export function runRepoToolchainProvenanceScript(args, reporter, lang = 'en') {
|
|
74
|
+
if (hasCliOptionToken(args, '--help', ['-h'])) {
|
|
75
|
+
reporter.stdout(getRepoToolchainProvenanceHelp(lang));
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
const options = parseRepoToolchainProvenanceOptions(args, lang);
|
|
79
|
+
if (options.error) {
|
|
80
|
+
printUsageError(reporter, options.error, 'mf script-pack run repo/toolchain-provenance --help', getRepoToolchainProvenanceHelp(lang), lang);
|
|
81
|
+
return 1;
|
|
82
|
+
}
|
|
83
|
+
const report = inspectRepoToolchainProvenance(resolveMustflowRoot());
|
|
84
|
+
if (options.json) {
|
|
85
|
+
reporter.stdout(JSON.stringify(report, null, 2));
|
|
86
|
+
return report.ok ? 0 : 1;
|
|
87
|
+
}
|
|
88
|
+
reporter.stdout(renderRepoToolchainProvenanceSummary(report, lang));
|
|
89
|
+
return report.ok ? 0 : 1;
|
|
90
|
+
}
|
|
@@ -527,6 +527,33 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
|
|
|
527
527
|
installedCommand: ['mf', 'script-pack', 'run', 'repo/version-source', 'inspect', '--json'],
|
|
528
528
|
expectedExitCodes: [0, 1],
|
|
529
529
|
},
|
|
530
|
+
{
|
|
531
|
+
id: 'repo-toolchain-provenance-report',
|
|
532
|
+
schemaFile: 'repo-toolchain-provenance-report.schema.json',
|
|
533
|
+
producer: 'mf script-pack run repo/toolchain-provenance inspect --json',
|
|
534
|
+
packaged: true,
|
|
535
|
+
documented: true,
|
|
536
|
+
installedCommand: ['mf', 'script-pack', 'run', 'repo/toolchain-provenance', 'inspect', '--json'],
|
|
537
|
+
expectedExitCodes: [0, 1],
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
id: 'repo-automation-surface-report',
|
|
541
|
+
schemaFile: 'repo-automation-surface-report.schema.json',
|
|
542
|
+
producer: 'mf script-pack run repo/automation-surface inspect --json',
|
|
543
|
+
packaged: true,
|
|
544
|
+
documented: true,
|
|
545
|
+
installedCommand: ['mf', 'script-pack', 'run', 'repo/automation-surface', 'inspect', '--json'],
|
|
546
|
+
expectedExitCodes: [0, 1],
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
id: 'repo-dependency-surface-report',
|
|
550
|
+
schemaFile: 'repo-dependency-surface-report.schema.json',
|
|
551
|
+
producer: 'mf script-pack run repo/dependency-surface inspect --json',
|
|
552
|
+
packaged: true,
|
|
553
|
+
documented: true,
|
|
554
|
+
installedCommand: ['mf', 'script-pack', 'run', 'repo/dependency-surface', 'inspect', '--json'],
|
|
555
|
+
expectedExitCodes: [0, 1],
|
|
556
|
+
},
|
|
530
557
|
{
|
|
531
558
|
id: 'repo-approval-gate-report',
|
|
532
559
|
schemaFile: 'repo-approval-gate-report.schema.json',
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { isRecord, readMustflowOwnedTomlFile } from './config-loading.js';
|
|
5
|
+
export const REPO_AUTOMATION_SURFACE_PACK_ID = 'repo';
|
|
6
|
+
export const REPO_AUTOMATION_SURFACE_SCRIPT_ID = 'automation-surface';
|
|
7
|
+
export const REPO_AUTOMATION_SURFACE_SCRIPT_REF = `${REPO_AUTOMATION_SURFACE_PACK_ID}/${REPO_AUTOMATION_SURFACE_SCRIPT_ID}`;
|
|
8
|
+
const DEFAULT_MAX_FILE_BYTES = 256 * 1024;
|
|
9
|
+
const COMMANDS_PATH = '.mustflow/config/commands.toml';
|
|
10
|
+
const WORKFLOW_DIR = '.github/workflows';
|
|
11
|
+
function sha256(value) {
|
|
12
|
+
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
13
|
+
}
|
|
14
|
+
function normalizeRelativePath(value) {
|
|
15
|
+
return value.replace(/\\/gu, '/').replace(/^\.\/+/u, '');
|
|
16
|
+
}
|
|
17
|
+
function lineForOffset(content, offset) {
|
|
18
|
+
let line = 1;
|
|
19
|
+
for (let index = 0; index < offset; index += 1) {
|
|
20
|
+
if (content.charCodeAt(index) === 10) {
|
|
21
|
+
line += 1;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return line;
|
|
25
|
+
}
|
|
26
|
+
function safeReadText(root, relativePath, scannedPaths, issues) {
|
|
27
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
28
|
+
scannedPaths.add(normalized);
|
|
29
|
+
const absolute = path.join(root, ...normalized.split('/'));
|
|
30
|
+
try {
|
|
31
|
+
const stats = statSync(absolute);
|
|
32
|
+
if (!stats.isFile()) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (stats.size > DEFAULT_MAX_FILE_BYTES) {
|
|
36
|
+
issues.push(`${normalized} exceeds max_file_bytes (${stats.size} > ${DEFAULT_MAX_FILE_BYTES}).`);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return readFileSync(absolute, 'utf8');
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (!existsSync(absolute)) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
46
|
+
issues.push(`Could not read ${normalized}: ${message}`);
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function firstLine(content, pattern) {
|
|
51
|
+
const match = pattern.exec(content);
|
|
52
|
+
return match && match.index >= 0 ? lineForOffset(content, match.index) : null;
|
|
53
|
+
}
|
|
54
|
+
function uniqueStrings(values) {
|
|
55
|
+
return [...new Set([...values].filter((value) => value.trim().length > 0))].sort((left, right) => left.localeCompare(right));
|
|
56
|
+
}
|
|
57
|
+
function classifyCategory(name, command) {
|
|
58
|
+
const value = `${name} ${command ?? ''}`.toLowerCase();
|
|
59
|
+
if (/\bdoctor|health|diagnose/u.test(value))
|
|
60
|
+
return 'doctor';
|
|
61
|
+
if (/\bbootstrap|setup|install|init\b/u.test(value))
|
|
62
|
+
return 'bootstrap';
|
|
63
|
+
if (/\bcheck|verify|lint|typecheck|fmt --check|format-check/u.test(value))
|
|
64
|
+
return 'check';
|
|
65
|
+
if (/\btest|vitest|jest|pytest|cargo test|go test/u.test(value))
|
|
66
|
+
return 'test';
|
|
67
|
+
if (/\bfix|format|prettier --write|eslint .*--fix|ruff .*--fix/u.test(value))
|
|
68
|
+
return 'fix';
|
|
69
|
+
if (/\bclean|rm -rf|rimraf/u.test(value))
|
|
70
|
+
return 'clean';
|
|
71
|
+
if (/\bsmoke|healthcheck/u.test(value))
|
|
72
|
+
return 'smoke';
|
|
73
|
+
if (/\bdeps?|dependency|renovate|dependabot|update\b/u.test(value))
|
|
74
|
+
return 'deps';
|
|
75
|
+
if (/\bdb:|database|migrate|seed|backup|restore|reset-dev/u.test(value))
|
|
76
|
+
return 'db';
|
|
77
|
+
if (/\brelease|publish|version|tag\b/u.test(value))
|
|
78
|
+
return 'release';
|
|
79
|
+
if (/\bdeploy|wrangler|vercel|netlify|pages\b/u.test(value))
|
|
80
|
+
return 'deploy';
|
|
81
|
+
if (/\bdev|serve|start|preview\b/u.test(value))
|
|
82
|
+
return 'dev_server';
|
|
83
|
+
if (/\bwatch\b/u.test(value))
|
|
84
|
+
return 'watch';
|
|
85
|
+
if (/\bworkflow|mustflow|docs|map|flow/u.test(value))
|
|
86
|
+
return 'workflow';
|
|
87
|
+
return 'unknown';
|
|
88
|
+
}
|
|
89
|
+
function classifyRisks(name, command, category) {
|
|
90
|
+
const value = `${name} ${command ?? ''}`.toLowerCase();
|
|
91
|
+
const risks = [];
|
|
92
|
+
if (category === 'dev_server' || category === 'watch' || /\b(?:serve|preview|--watch|watch)\b/u.test(value))
|
|
93
|
+
risks.push('long_running');
|
|
94
|
+
if (category === 'release' || category === 'deploy' || /\bpublish|gh release|git tag|npm version\b/u.test(value))
|
|
95
|
+
risks.push('release');
|
|
96
|
+
if (/\b(?:install|update|upgrade|audit|curl|wget|docker pull|git fetch|git push|npm publish|bun publish)\b/u.test(value))
|
|
97
|
+
risks.push('network');
|
|
98
|
+
if (/\b(?:rm -rf|rimraf|drop database|migrate reset|docker compose down -v|git reset|git clean)\b/u.test(value))
|
|
99
|
+
risks.push('destructive');
|
|
100
|
+
if (/\b(?:git commit|git push|git tag|gh release)\b/u.test(value))
|
|
101
|
+
risks.push('git_state');
|
|
102
|
+
if (/\b(?:secret|token|key|login|auth)\b/u.test(value))
|
|
103
|
+
risks.push('secret');
|
|
104
|
+
if (/\b(?:--yes|-y|write|build|dist|codegen|generate|migrate|seed|format|fix)\b/u.test(value))
|
|
105
|
+
risks.push('writes');
|
|
106
|
+
if (/\b(?:read -p|select |prompt|confirm|pause)\b/u.test(value))
|
|
107
|
+
risks.push('interactive');
|
|
108
|
+
return uniqueStrings(risks);
|
|
109
|
+
}
|
|
110
|
+
function candidateIntentName(name, category) {
|
|
111
|
+
const normalized = name
|
|
112
|
+
.toLowerCase()
|
|
113
|
+
.replace(/^scripts?:/u, '')
|
|
114
|
+
.replace(/[^a-z0-9]+/gu, '_')
|
|
115
|
+
.replace(/^_+|_+$/gu, '');
|
|
116
|
+
return normalized || (category === 'unknown' ? null : category);
|
|
117
|
+
}
|
|
118
|
+
function addSurface(surfaces, input, intents) {
|
|
119
|
+
const category = classifyCategory(input.name, input.command_hint);
|
|
120
|
+
const risks = classifyRisks(input.name, input.command_hint, category);
|
|
121
|
+
const mappedName = candidateIntentName(input.name, category);
|
|
122
|
+
const intent = mappedName ? intents.get(mappedName) : undefined;
|
|
123
|
+
const agentAllowed = intent ? intent.status === 'configured' && intent.runPolicy === 'agent_allowed' : null;
|
|
124
|
+
surfaces.push({
|
|
125
|
+
id: `${input.kind}:${input.path}:${input.line ?? 0}:${input.name}`,
|
|
126
|
+
...input,
|
|
127
|
+
category,
|
|
128
|
+
risks,
|
|
129
|
+
mapped_intent: intent ? mappedName : null,
|
|
130
|
+
agent_allowed: agentAllowed,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function readCommandIntents(root, scannedPaths, issues) {
|
|
134
|
+
scannedPaths.add(COMMANDS_PATH);
|
|
135
|
+
const intents = new Map();
|
|
136
|
+
if (!existsSync(path.join(root, ...COMMANDS_PATH.split('/')))) {
|
|
137
|
+
return intents;
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
const parsed = readMustflowOwnedTomlFile(root, COMMANDS_PATH);
|
|
141
|
+
const table = isRecord(parsed) ? parsed : {};
|
|
142
|
+
const intentTable = isRecord(table.intents) ? table.intents : {};
|
|
143
|
+
for (const [name, value] of Object.entries(intentTable)) {
|
|
144
|
+
if (!isRecord(value)) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
intents.set(name, {
|
|
148
|
+
status: typeof value.status === 'string' ? value.status : null,
|
|
149
|
+
runPolicy: typeof value.run_policy === 'string' ? value.run_policy : null,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
155
|
+
issues.push(`Could not read ${COMMANDS_PATH}: ${message}`);
|
|
156
|
+
}
|
|
157
|
+
return intents;
|
|
158
|
+
}
|
|
159
|
+
function scanPackageJson(root, scannedPaths, surfaces, intents, issues) {
|
|
160
|
+
const content = safeReadText(root, 'package.json', scannedPaths, issues);
|
|
161
|
+
if (content === null) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
let parsed;
|
|
165
|
+
try {
|
|
166
|
+
parsed = JSON.parse(content);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
170
|
+
issues.push(`Could not parse package.json: ${message}`);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const scripts = isRecord(parsed) && isRecord(parsed.scripts) ? parsed.scripts : {};
|
|
174
|
+
for (const [name, value] of Object.entries(scripts)) {
|
|
175
|
+
if (typeof value !== 'string') {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
addSurface(surfaces, {
|
|
179
|
+
kind: 'package_script',
|
|
180
|
+
path: 'package.json',
|
|
181
|
+
line: firstLine(content, new RegExp(`"${name.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&')}"\\s*:`, 'u')),
|
|
182
|
+
name,
|
|
183
|
+
command_hint: value,
|
|
184
|
+
}, intents);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function scanMakefile(root, scannedPaths, surfaces, intents, issues) {
|
|
188
|
+
for (const relativePath of ['Makefile', 'makefile']) {
|
|
189
|
+
const content = safeReadText(root, relativePath, scannedPaths, issues);
|
|
190
|
+
if (content === null)
|
|
191
|
+
continue;
|
|
192
|
+
for (const [index, line] of content.split(/\r?\n/u).entries()) {
|
|
193
|
+
const match = /^([A-Za-z0-9_.:-]+)\s*:(?:\s|$)/u.exec(line);
|
|
194
|
+
if (!match || match[1].startsWith('.'))
|
|
195
|
+
continue;
|
|
196
|
+
addSurface(surfaces, { kind: 'make_target', path: relativePath, line: index + 1, name: match[1], command_hint: null }, intents);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function scanTaskLikeFile(root, scannedPaths, surfaces, intents, issues) {
|
|
201
|
+
for (const relativePath of ['Taskfile.yml', 'Taskfile.yaml']) {
|
|
202
|
+
const content = safeReadText(root, relativePath, scannedPaths, issues);
|
|
203
|
+
if (content === null)
|
|
204
|
+
continue;
|
|
205
|
+
for (const [index, line] of content.split(/\r?\n/u).entries()) {
|
|
206
|
+
const match = /^\s{2}([A-Za-z0-9_.:-]+):\s*$/u.exec(line);
|
|
207
|
+
if (!match)
|
|
208
|
+
continue;
|
|
209
|
+
addSurface(surfaces, { kind: 'taskfile_task', path: relativePath, line: index + 1, name: match[1], command_hint: null }, intents);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function scanMiseTasks(root, scannedPaths, surfaces, intents, issues) {
|
|
214
|
+
for (const relativePath of ['mise.toml', '.mise.toml']) {
|
|
215
|
+
const content = safeReadText(root, relativePath, scannedPaths, issues);
|
|
216
|
+
if (content === null)
|
|
217
|
+
continue;
|
|
218
|
+
for (const [index, line] of content.split(/\r?\n/u).entries()) {
|
|
219
|
+
const match = /^\s*\[tasks\.([^\]]+)\]\s*$/u.exec(line) ?? /^\s*([A-Za-z0-9_.:-]+)\s*=\s*["'][^"']+["']\s*$/u.exec(line);
|
|
220
|
+
if (!match)
|
|
221
|
+
continue;
|
|
222
|
+
addSurface(surfaces, { kind: 'mise_task', path: relativePath, line: index + 1, name: match[1], command_hint: line.trim() }, intents);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function scanWorkflows(root, scannedPaths, surfaces, intents, issues) {
|
|
227
|
+
const workflowRoot = path.join(root, ...WORKFLOW_DIR.split('/'));
|
|
228
|
+
if (!existsSync(workflowRoot)) {
|
|
229
|
+
scannedPaths.add(WORKFLOW_DIR);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
let entries = [];
|
|
233
|
+
try {
|
|
234
|
+
entries = readdirSync(workflowRoot)
|
|
235
|
+
.filter((name) => /\.(?:yml|yaml)$/u.test(name))
|
|
236
|
+
.map((name) => `${WORKFLOW_DIR}/${name}`)
|
|
237
|
+
.sort((left, right) => left.localeCompare(right));
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
241
|
+
issues.push(`Could not list ${WORKFLOW_DIR}: ${message}`);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
for (const relativePath of entries) {
|
|
245
|
+
const content = safeReadText(root, relativePath, scannedPaths, issues);
|
|
246
|
+
if (content === null)
|
|
247
|
+
continue;
|
|
248
|
+
const name = /name:\s*([^\n]+)/u.exec(content)?.[1]?.trim() ?? path.basename(relativePath);
|
|
249
|
+
addSurface(surfaces, {
|
|
250
|
+
kind: 'ci_workflow',
|
|
251
|
+
path: relativePath,
|
|
252
|
+
line: firstLine(content, /^name:/mu),
|
|
253
|
+
name,
|
|
254
|
+
command_hint: content.includes('workflow_dispatch') ? 'workflow_dispatch' : content.includes('push:') ? 'push' : null,
|
|
255
|
+
}, intents);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function addMustflowIntentSurfaces(surfaces, intents) {
|
|
259
|
+
for (const [name, intent] of intents.entries()) {
|
|
260
|
+
const category = classifyCategory(name, null);
|
|
261
|
+
surfaces.push({
|
|
262
|
+
id: `mustflow_intent:${name}`,
|
|
263
|
+
kind: 'mustflow_intent',
|
|
264
|
+
path: COMMANDS_PATH,
|
|
265
|
+
line: null,
|
|
266
|
+
name,
|
|
267
|
+
command_hint: null,
|
|
268
|
+
category,
|
|
269
|
+
risks: classifyRisks(name, null, category),
|
|
270
|
+
mapped_intent: name,
|
|
271
|
+
agent_allowed: intent.status === 'configured' && intent.runPolicy === 'agent_allowed',
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function createFindings(surfaces) {
|
|
276
|
+
const findings = [];
|
|
277
|
+
for (const surface of surfaces) {
|
|
278
|
+
if (surface.kind !== 'mustflow_intent' && surface.mapped_intent === null && surface.category !== 'unknown') {
|
|
279
|
+
findings.push({
|
|
280
|
+
code: 'raw_command_without_mustflow_intent',
|
|
281
|
+
severity: surface.risks.length > 0 ? 'medium' : 'low',
|
|
282
|
+
path: surface.path,
|
|
283
|
+
message: `${surface.name} appears to be a ${surface.category} automation surface without a mapped mustflow intent.`,
|
|
284
|
+
json_pointer: null,
|
|
285
|
+
metric: null,
|
|
286
|
+
actual: null,
|
|
287
|
+
expected: null,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
if (surface.risks.includes('long_running')) {
|
|
291
|
+
findings.push({
|
|
292
|
+
code: 'long_running_automation_surface',
|
|
293
|
+
severity: 'medium',
|
|
294
|
+
path: surface.path,
|
|
295
|
+
message: `${surface.name} looks long-running and should remain manual or explicitly bounded.`,
|
|
296
|
+
json_pointer: null,
|
|
297
|
+
metric: null,
|
|
298
|
+
actual: null,
|
|
299
|
+
expected: null,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
if (surface.risks.some((risk) => ['destructive', 'release', 'secret', 'git_state'].includes(risk))) {
|
|
303
|
+
findings.push({
|
|
304
|
+
code: 'dangerous_automation_surface',
|
|
305
|
+
severity: 'high',
|
|
306
|
+
path: surface.path,
|
|
307
|
+
message: `${surface.name} touches a high-risk automation category: ${surface.risks.join(', ')}.`,
|
|
308
|
+
json_pointer: null,
|
|
309
|
+
metric: null,
|
|
310
|
+
actual: null,
|
|
311
|
+
expected: null,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
if (surface.kind === 'mustflow_intent' && surface.agent_allowed === false) {
|
|
315
|
+
findings.push({
|
|
316
|
+
code: 'mustflow_intent_manual_boundary',
|
|
317
|
+
severity: 'low',
|
|
318
|
+
path: surface.path,
|
|
319
|
+
message: `${surface.name} is present in the command contract but is not agent-allowed.`,
|
|
320
|
+
json_pointer: null,
|
|
321
|
+
metric: null,
|
|
322
|
+
actual: null,
|
|
323
|
+
expected: null,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return findings;
|
|
328
|
+
}
|
|
329
|
+
function createSummary(surfaces) {
|
|
330
|
+
return {
|
|
331
|
+
surface_count: surfaces.length,
|
|
332
|
+
mustflow_intent_count: surfaces.filter((surface) => surface.kind === 'mustflow_intent').length,
|
|
333
|
+
raw_surface_count: surfaces.filter((surface) => surface.kind !== 'mustflow_intent').length,
|
|
334
|
+
agent_allowed_intent_count: surfaces.filter((surface) => surface.kind === 'mustflow_intent' && surface.agent_allowed === true).length,
|
|
335
|
+
manual_only_intent_count: surfaces.filter((surface) => surface.kind === 'mustflow_intent' && surface.agent_allowed === false).length,
|
|
336
|
+
risky_surface_count: surfaces.filter((surface) => surface.risks.length > 0).length,
|
|
337
|
+
long_running_surface_count: surfaces.filter((surface) => surface.risks.includes('long_running')).length,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
export function inspectRepoAutomationSurface(projectRoot) {
|
|
341
|
+
const root = path.resolve(projectRoot);
|
|
342
|
+
const issues = [];
|
|
343
|
+
const scannedPaths = new Set();
|
|
344
|
+
const intents = readCommandIntents(root, scannedPaths, issues);
|
|
345
|
+
const surfaces = [];
|
|
346
|
+
scanPackageJson(root, scannedPaths, surfaces, intents, issues);
|
|
347
|
+
scanMakefile(root, scannedPaths, surfaces, intents, issues);
|
|
348
|
+
scanTaskLikeFile(root, scannedPaths, surfaces, intents, issues);
|
|
349
|
+
scanMiseTasks(root, scannedPaths, surfaces, intents, issues);
|
|
350
|
+
scanWorkflows(root, scannedPaths, surfaces, intents, issues);
|
|
351
|
+
addMustflowIntentSurfaces(surfaces, intents);
|
|
352
|
+
const sortedSurfaces = surfaces.sort((left, right) => left.path.localeCompare(right.path) || left.name.localeCompare(right.name));
|
|
353
|
+
const findings = createFindings(sortedSurfaces);
|
|
354
|
+
const summary = createSummary(sortedSurfaces);
|
|
355
|
+
const status = issues.length > 0 ? 'error' : findings.some((finding) => finding.severity === 'high') ? 'failed' : 'passed';
|
|
356
|
+
return {
|
|
357
|
+
schema_version: '1',
|
|
358
|
+
command: 'script-pack',
|
|
359
|
+
pack_id: REPO_AUTOMATION_SURFACE_PACK_ID,
|
|
360
|
+
script_id: REPO_AUTOMATION_SURFACE_SCRIPT_ID,
|
|
361
|
+
script_ref: REPO_AUTOMATION_SURFACE_SCRIPT_REF,
|
|
362
|
+
action: 'inspect',
|
|
363
|
+
status,
|
|
364
|
+
ok: status === 'passed',
|
|
365
|
+
mustflow_root: root,
|
|
366
|
+
input: {
|
|
367
|
+
scanned_paths: uniqueStrings(scannedPaths),
|
|
368
|
+
max_file_bytes: DEFAULT_MAX_FILE_BYTES,
|
|
369
|
+
},
|
|
370
|
+
input_hash: sha256(JSON.stringify({ summary, surfaces: sortedSurfaces, findings, issues })),
|
|
371
|
+
summary,
|
|
372
|
+
surfaces: sortedSurfaces,
|
|
373
|
+
findings,
|
|
374
|
+
issues,
|
|
375
|
+
};
|
|
376
|
+
}
|