mustflow 2.103.3 → 2.103.12
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/dist/cli/commands/run.js +11 -0
- package/dist/cli/commands/script-pack.js +2 -0
- package/dist/cli/i18n/en.js +35 -0
- package/dist/cli/i18n/es.js +35 -0
- package/dist/cli/i18n/fr.js +35 -0
- package/dist/cli/i18n/hi.js +35 -0
- package/dist/cli/i18n/ko.js +35 -0
- package/dist/cli/i18n/zh.js +35 -0
- package/dist/cli/lib/external-skill-import.js +78 -14
- package/dist/cli/lib/local-index/sql.js +9 -1
- package/dist/cli/lib/run-plan.js +37 -0
- package/dist/cli/lib/script-pack-registry.js +57 -0
- package/dist/cli/script-packs/repo-deploy-surface.js +98 -0
- package/dist/cli/script-packs/repo-security-pattern-scan.js +150 -0
- package/dist/core/change-impact.js +16 -0
- package/dist/core/code-outline.js +3 -13
- package/dist/core/command-env.js +26 -8
- package/dist/core/config-chain.js +3 -13
- package/dist/core/dependency-graph.js +3 -13
- package/dist/core/docs-link-integrity.js +23 -4
- package/dist/core/env-contract.js +3 -13
- package/dist/core/export-diff.js +3 -3
- package/dist/core/ignored-directories.js +40 -0
- package/dist/core/public-json-contracts.js +18 -0
- package/dist/core/reference-drift.js +4 -2
- package/dist/core/related-files.js +3 -13
- package/dist/core/repo-deploy-surface.js +428 -0
- package/dist/core/repo-merge-conflict-scan.js +3 -9
- package/dist/core/route-outline.js +3 -13
- package/dist/core/script-pack-suggestions.js +52 -14
- package/dist/core/secret-risk-scan.js +3 -13
- package/dist/core/security-pattern-scan.js +518 -0
- package/dist/core/skill-route-resolution.js +21 -1
- package/package.json +2 -2
- package/schemas/README.md +7 -0
- package/schemas/link-integrity-report.schema.json +1 -0
- package/schemas/reference-drift-report.schema.json +1 -0
- package/schemas/repo-deploy-surface-report.schema.json +190 -0
- package/schemas/security-pattern-scan-report.schema.json +196 -0
- package/templates/default/i18n.toml +20 -8
- package/templates/default/locales/en/.mustflow/skills/ai-generated-code-hardening/SKILL.md +30 -7
- package/templates/default/locales/en/.mustflow/skills/api-contract-change/SKILL.md +18 -9
- package/templates/default/locales/en/.mustflow/skills/api-request-performance-review/SKILL.md +12 -6
- package/templates/default/locales/en/.mustflow/skills/completion-evidence-gate/SKILL.md +20 -9
- package/templates/default/locales/en/.mustflow/skills/hot-path-performance-review/SKILL.md +20 -15
- package/templates/default/locales/en/.mustflow/skills/next-action-menu/SKILL.md +22 -7
- package/templates/default/locales/en/.mustflow/skills/quadratic-scan-review/SKILL.md +21 -19
- package/templates/default/locales/en/.mustflow/skills/react-code-change/SKILL.md +54 -8
- package/templates/default/locales/en/.mustflow/skills/vertical-slice-tdd/SKILL.md +22 -8
- package/templates/default/manifest.toml +1 -1
|
@@ -453,6 +453,35 @@ export const SCRIPT_PACKS = [
|
|
|
453
453
|
reportSchemaFile: 'secret-risk-scan-report.schema.json',
|
|
454
454
|
loadRunner: async () => (await import('../script-packs/repo-secret-risk-scan.js')).runRepoSecretRiskScanScript,
|
|
455
455
|
},
|
|
456
|
+
{
|
|
457
|
+
packId: 'repo',
|
|
458
|
+
id: 'security-pattern-scan',
|
|
459
|
+
ref: scriptRef('repo', 'security-pattern-scan'),
|
|
460
|
+
usage: 'mf script-pack run repo/security-pattern-scan scan [path...] [options]',
|
|
461
|
+
summaryKey: 'scriptPack.script.securityPatternScan.summary',
|
|
462
|
+
actions: ['scan'],
|
|
463
|
+
useWhen: [
|
|
464
|
+
'Scan source, CI, and config files for high-signal security code patterns without printing matched source lines or secret values.',
|
|
465
|
+
'Review filesystem, command, injection, browser, token, session, parser, logging, access-control, and transport-security pattern leads before source-to-sink review.',
|
|
466
|
+
],
|
|
467
|
+
phases: ['before_change', 'after_change', 'review'],
|
|
468
|
+
readOnly: true,
|
|
469
|
+
mutates: false,
|
|
470
|
+
network: false,
|
|
471
|
+
inputs: ['path', 'max_files', 'max_file_bytes', 'max_findings'],
|
|
472
|
+
outputs: ['human_summary', 'json_report', 'security_pattern_findings', 'review_focus'],
|
|
473
|
+
relatedSkills: [
|
|
474
|
+
'api-access-control-review',
|
|
475
|
+
'file-upload-security-review',
|
|
476
|
+
'security-flow-review',
|
|
477
|
+
'security-privacy-review',
|
|
478
|
+
'security-regression-tests',
|
|
479
|
+
],
|
|
480
|
+
riskLevel: 'medium',
|
|
481
|
+
cost: 'low',
|
|
482
|
+
reportSchemaFile: 'security-pattern-scan-report.schema.json',
|
|
483
|
+
loadRunner: async () => (await import('../script-packs/repo-security-pattern-scan.js')).runRepoSecurityPatternScanScript,
|
|
484
|
+
},
|
|
456
485
|
{
|
|
457
486
|
packId: 'repo',
|
|
458
487
|
id: 'generated-boundary',
|
|
@@ -655,6 +684,34 @@ export const SCRIPT_PACKS = [
|
|
|
655
684
|
reportSchemaFile: 'repo-approval-gate-report.schema.json',
|
|
656
685
|
loadRunner: async () => (await import('../script-packs/repo-approval-gate.js')).runRepoApprovalGateScript,
|
|
657
686
|
},
|
|
687
|
+
{
|
|
688
|
+
packId: 'repo',
|
|
689
|
+
id: 'deploy-surface',
|
|
690
|
+
ref: scriptRef('repo', 'deploy-surface'),
|
|
691
|
+
usage: 'mf script-pack run repo/deploy-surface inspect [options]',
|
|
692
|
+
summaryKey: 'scriptPack.script.deploySurface.summary',
|
|
693
|
+
actions: ['inspect'],
|
|
694
|
+
useWhen: [
|
|
695
|
+
'Inspect local workflows, package metadata, package scripts, and deploy config files before claiming push, tag, release, or deploy follow-up is available.',
|
|
696
|
+
'Review detected deploy surfaces with trigger, required verification, and manual gate evidence after repository changes.',
|
|
697
|
+
],
|
|
698
|
+
phases: ['before_change', 'after_change', 'review'],
|
|
699
|
+
readOnly: true,
|
|
700
|
+
mutates: false,
|
|
701
|
+
network: false,
|
|
702
|
+
inputs: [],
|
|
703
|
+
outputs: ['human_summary', 'json_report', 'deploy_surfaces', 'required_verification', 'manual_gates'],
|
|
704
|
+
relatedSkills: [
|
|
705
|
+
'completion-evidence-gate',
|
|
706
|
+
'deployment-rollout-safety-review',
|
|
707
|
+
'public-json-contract-change',
|
|
708
|
+
'release-publish-change',
|
|
709
|
+
],
|
|
710
|
+
riskLevel: 'low',
|
|
711
|
+
cost: 'low',
|
|
712
|
+
reportSchemaFile: 'repo-deploy-surface-report.schema.json',
|
|
713
|
+
loadRunner: async () => (await import('../script-packs/repo-deploy-surface.js')).runRepoDeploySurfaceScript,
|
|
714
|
+
},
|
|
658
715
|
{
|
|
659
716
|
packId: 'repo',
|
|
660
717
|
id: 'related-files',
|
|
@@ -0,0 +1,98 @@
|
|
|
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 { inspectRepoDeploySurface, REPO_DEPLOY_SURFACE_SCRIPT_REF, } from '../../core/repo-deploy-surface.js';
|
|
6
|
+
const REPO_DEPLOY_SURFACE_OPTIONS = [{ name: '--json', kind: 'boolean' }];
|
|
7
|
+
export function getRepoDeploySurfaceHelp(lang = 'en') {
|
|
8
|
+
return renderHelp({
|
|
9
|
+
usage: 'mf script-pack run repo/deploy-surface inspect [options]',
|
|
10
|
+
summary: t(lang, 'deploySurface.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/deploy-surface inspect',
|
|
17
|
+
'mf script-pack run repo/deploy-surface inspect --json',
|
|
18
|
+
],
|
|
19
|
+
exitCodes: [
|
|
20
|
+
{ label: '0', description: t(lang, 'deploySurface.help.exit.ok') },
|
|
21
|
+
{ label: '1', description: t(lang, 'deploySurface.help.exit.fail') },
|
|
22
|
+
],
|
|
23
|
+
}, lang);
|
|
24
|
+
}
|
|
25
|
+
function parseRepoDeploySurfaceOptions(args, lang) {
|
|
26
|
+
const [action, ...rest] = args;
|
|
27
|
+
const parsed = parseCliOptions(rest, REPO_DEPLOY_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, 'deploySurface.error.unknownAction', { action })
|
|
35
|
+
: t(lang, 'deploySurface.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 renderRepoDeploySurfaceSummary(report, lang) {
|
|
44
|
+
const lines = [
|
|
45
|
+
t(lang, 'deploySurface.title'),
|
|
46
|
+
`${t(lang, 'scriptPack.label.script')}: ${REPO_DEPLOY_SURFACE_SCRIPT_REF}`,
|
|
47
|
+
`${t(lang, 'label.status')}: ${report.status}`,
|
|
48
|
+
`${t(lang, 'deploySurface.label.deploySurface')}: ${report.has_deploy_surface ? t(lang, 'value.yes') : t(lang, 'value.no')}`,
|
|
49
|
+
`${t(lang, 'deploySurface.label.surfaces')}: ${report.summary.surface_count}`,
|
|
50
|
+
`${t(lang, 'deploySurface.label.requiredVerification')}: ${report.summary.required_verification_count}`,
|
|
51
|
+
`${t(lang, 'deploySurface.label.manualGates')}: ${report.summary.manual_gate_count}`,
|
|
52
|
+
];
|
|
53
|
+
if (report.surfaces.length > 0) {
|
|
54
|
+
lines.push(t(lang, 'deploySurface.label.surfaceDetails'));
|
|
55
|
+
for (const surface of report.surfaces) {
|
|
56
|
+
const location = surface.line === null ? surface.path : `${surface.path}:${surface.line}`;
|
|
57
|
+
const trigger = surface.trigger === null ? '' : `, trigger ${surface.trigger}`;
|
|
58
|
+
lines.push(`- ${surface.surface_type} (${surface.kind}) at ${location}${trigger}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (report.required_verification.length > 0) {
|
|
62
|
+
lines.push(t(lang, 'deploySurface.label.requiredVerification'));
|
|
63
|
+
for (const verification of report.required_verification) {
|
|
64
|
+
lines.push(`- ${verification}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (report.manual_gates.length > 0) {
|
|
68
|
+
lines.push(t(lang, 'deploySurface.label.manualGates'));
|
|
69
|
+
for (const gate of report.manual_gates) {
|
|
70
|
+
lines.push(`- ${gate}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (report.issues.length > 0) {
|
|
74
|
+
lines.push(t(lang, 'deploySurface.label.issues'), ...report.issues.map((issue) => `- ${issue}`));
|
|
75
|
+
}
|
|
76
|
+
if (!report.has_deploy_surface && report.issues.length === 0) {
|
|
77
|
+
lines.push(t(lang, 'deploySurface.noSurfaces'));
|
|
78
|
+
}
|
|
79
|
+
return lines.join('\n');
|
|
80
|
+
}
|
|
81
|
+
export function runRepoDeploySurfaceScript(args, reporter, lang = 'en') {
|
|
82
|
+
if (hasCliOptionToken(args, '--help', ['-h'])) {
|
|
83
|
+
reporter.stdout(getRepoDeploySurfaceHelp(lang));
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
const options = parseRepoDeploySurfaceOptions(args, lang);
|
|
87
|
+
if (options.error) {
|
|
88
|
+
printUsageError(reporter, options.error, 'mf script-pack run repo/deploy-surface --help', getRepoDeploySurfaceHelp(lang), lang);
|
|
89
|
+
return 1;
|
|
90
|
+
}
|
|
91
|
+
const report = inspectRepoDeploySurface(resolveMustflowRoot());
|
|
92
|
+
if (options.json) {
|
|
93
|
+
reporter.stdout(JSON.stringify(report, null, 2));
|
|
94
|
+
return report.ok ? 0 : 1;
|
|
95
|
+
}
|
|
96
|
+
reporter.stdout(renderRepoDeploySurfaceSummary(report, lang));
|
|
97
|
+
return report.ok ? 0 : 1;
|
|
98
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { printUsageError, renderHelp } from '../lib/cli-output.js';
|
|
2
|
+
import { t } from '../lib/i18n.js';
|
|
3
|
+
import { formatCliOptionParseError, getParsedCliStringOption, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
|
|
4
|
+
import { resolveMustflowRoot } from '../lib/project-root.js';
|
|
5
|
+
import { inspectSecurityPatternScan, SECURITY_PATTERN_SCAN_SCRIPT_REF, } from '../../core/security-pattern-scan.js';
|
|
6
|
+
const SECURITY_PATTERN_SCAN_OPTIONS = [
|
|
7
|
+
{ name: '--json', kind: 'boolean' },
|
|
8
|
+
{ name: '--max-files', kind: 'string' },
|
|
9
|
+
{ name: '--max-file-bytes', kind: 'string' },
|
|
10
|
+
{ name: '--max-findings', kind: 'string' },
|
|
11
|
+
];
|
|
12
|
+
function parsePositiveInteger(value, option, lang) {
|
|
13
|
+
if (value === null) {
|
|
14
|
+
return { value: null };
|
|
15
|
+
}
|
|
16
|
+
if (!/^[1-9]\d*$/u.test(value)) {
|
|
17
|
+
return { value: null, error: t(lang, 'securityPatternScan.error.invalidPositiveInteger', { option, value }) };
|
|
18
|
+
}
|
|
19
|
+
const parsed = Number(value);
|
|
20
|
+
if (!Number.isSafeInteger(parsed)) {
|
|
21
|
+
return { value: null, error: t(lang, 'securityPatternScan.error.invalidPositiveInteger', { option, value }) };
|
|
22
|
+
}
|
|
23
|
+
return { value: parsed };
|
|
24
|
+
}
|
|
25
|
+
export function getRepoSecurityPatternScanHelp(lang = 'en') {
|
|
26
|
+
return renderHelp({
|
|
27
|
+
usage: 'mf script-pack run repo/security-pattern-scan scan [path...] [options]',
|
|
28
|
+
summary: t(lang, 'securityPatternScan.help.summary'),
|
|
29
|
+
options: [
|
|
30
|
+
{ label: '--max-files <count>', description: t(lang, 'securityPatternScan.help.option.maxFiles') },
|
|
31
|
+
{ label: '--max-file-bytes <bytes>', description: t(lang, 'securityPatternScan.help.option.maxFileBytes') },
|
|
32
|
+
{ label: '--max-findings <count>', description: t(lang, 'securityPatternScan.help.option.maxFindings') },
|
|
33
|
+
{ label: '--json', description: t(lang, 'cli.option.json') },
|
|
34
|
+
{ label: '-h, --help', description: t(lang, 'cli.option.help') },
|
|
35
|
+
],
|
|
36
|
+
examples: [
|
|
37
|
+
'mf script-pack run repo/security-pattern-scan scan --json',
|
|
38
|
+
'mf script-pack run repo/security-pattern-scan scan src .github/workflows --json',
|
|
39
|
+
'mf script-pack run repo/security-pattern-scan scan src/server.ts --max-findings 50 --json',
|
|
40
|
+
],
|
|
41
|
+
exitCodes: [
|
|
42
|
+
{ label: '0', description: t(lang, 'securityPatternScan.help.exit.ok') },
|
|
43
|
+
{ label: '1', description: t(lang, 'securityPatternScan.help.exit.fail') },
|
|
44
|
+
],
|
|
45
|
+
}, lang);
|
|
46
|
+
}
|
|
47
|
+
function parseSecurityPatternScanOptions(args, lang) {
|
|
48
|
+
const [action, ...rest] = args;
|
|
49
|
+
const parsed = parseCliOptions(rest, SECURITY_PATTERN_SCAN_OPTIONS, { allowPositionals: true });
|
|
50
|
+
const json = hasParsedCliOption(parsed, '--json');
|
|
51
|
+
const maxFiles = parsePositiveInteger(getParsedCliStringOption(parsed, '--max-files'), '--max-files', lang);
|
|
52
|
+
const maxFileBytes = parsePositiveInteger(getParsedCliStringOption(parsed, '--max-file-bytes'), '--max-file-bytes', lang);
|
|
53
|
+
const maxFindings = parsePositiveInteger(getParsedCliStringOption(parsed, '--max-findings'), '--max-findings', lang);
|
|
54
|
+
if (action !== 'scan') {
|
|
55
|
+
return {
|
|
56
|
+
action: 'scan',
|
|
57
|
+
json,
|
|
58
|
+
paths: parsed.positionals,
|
|
59
|
+
maxFiles: maxFiles.value,
|
|
60
|
+
maxFileBytes: maxFileBytes.value,
|
|
61
|
+
maxFindings: maxFindings.value,
|
|
62
|
+
error: action
|
|
63
|
+
? t(lang, 'securityPatternScan.error.unknownAction', { action })
|
|
64
|
+
: t(lang, 'securityPatternScan.error.missingAction'),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (parsed.error) {
|
|
68
|
+
return {
|
|
69
|
+
action,
|
|
70
|
+
json,
|
|
71
|
+
paths: parsed.positionals,
|
|
72
|
+
maxFiles: maxFiles.value,
|
|
73
|
+
maxFileBytes: maxFileBytes.value,
|
|
74
|
+
maxFindings: maxFindings.value,
|
|
75
|
+
error: formatCliOptionParseError(parsed.error, lang),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
for (const candidate of [maxFiles, maxFileBytes, maxFindings]) {
|
|
79
|
+
if (candidate.error) {
|
|
80
|
+
return {
|
|
81
|
+
action,
|
|
82
|
+
json,
|
|
83
|
+
paths: parsed.positionals,
|
|
84
|
+
maxFiles: maxFiles.value,
|
|
85
|
+
maxFileBytes: maxFileBytes.value,
|
|
86
|
+
maxFindings: maxFindings.value,
|
|
87
|
+
error: candidate.error,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
action,
|
|
93
|
+
json,
|
|
94
|
+
paths: parsed.positionals,
|
|
95
|
+
maxFiles: maxFiles.value,
|
|
96
|
+
maxFileBytes: maxFileBytes.value,
|
|
97
|
+
maxFindings: maxFindings.value,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function renderSecurityPatternScanSummary(report, lang) {
|
|
101
|
+
const lines = [
|
|
102
|
+
t(lang, 'securityPatternScan.title'),
|
|
103
|
+
`${t(lang, 'scriptPack.label.script')}: ${SECURITY_PATTERN_SCAN_SCRIPT_REF}`,
|
|
104
|
+
`${t(lang, 'label.status')}: ${report.status}`,
|
|
105
|
+
`${t(lang, 'securityPatternScan.label.files')}: ${report.summary.file_count}`,
|
|
106
|
+
`${t(lang, 'securityPatternScan.label.findings')}: ${report.summary.finding_count}`,
|
|
107
|
+
`${t(lang, 'securityPatternScan.label.categories')}: ${report.summary.category_count}`,
|
|
108
|
+
`${t(lang, 'securityPatternScan.label.highOrCritical')}: ${report.summary.high_or_critical_count}`,
|
|
109
|
+
`${t(lang, 'securityPatternScan.label.truncated')}: ${report.truncated ? t(lang, 'value.yes') : t(lang, 'value.no')}`,
|
|
110
|
+
];
|
|
111
|
+
if (report.findings.length > 0) {
|
|
112
|
+
lines.push(t(lang, 'securityPatternScan.label.findings'));
|
|
113
|
+
for (const finding of report.findings.slice(0, 40)) {
|
|
114
|
+
const line = finding.line ? `:${finding.line}` : '';
|
|
115
|
+
const detector = finding.detector ? ` ${finding.detector}` : '';
|
|
116
|
+
const focus = finding.review_focus ? ` ${t(lang, 'securityPatternScan.label.reviewFocus')}: ${finding.review_focus}` : '';
|
|
117
|
+
lines.push(`- ${finding.path}${line}: ${finding.code}${detector} (${finding.message})${focus}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (report.issues.length > 0) {
|
|
121
|
+
lines.push(t(lang, 'securityPatternScan.label.issues'), ...report.issues.map((issue) => `- ${issue}`));
|
|
122
|
+
}
|
|
123
|
+
if (report.findings.length === 0 && report.issues.length === 0) {
|
|
124
|
+
lines.push(t(lang, 'securityPatternScan.clean'));
|
|
125
|
+
}
|
|
126
|
+
return lines.join('\n');
|
|
127
|
+
}
|
|
128
|
+
export function runRepoSecurityPatternScanScript(args, reporter, lang = 'en') {
|
|
129
|
+
if (hasCliOptionToken(args, '--help', ['-h'])) {
|
|
130
|
+
reporter.stdout(getRepoSecurityPatternScanHelp(lang));
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
const options = parseSecurityPatternScanOptions(args, lang);
|
|
134
|
+
if (options.error) {
|
|
135
|
+
printUsageError(reporter, options.error, 'mf script-pack run repo/security-pattern-scan --help', getRepoSecurityPatternScanHelp(lang), lang);
|
|
136
|
+
return 1;
|
|
137
|
+
}
|
|
138
|
+
const report = inspectSecurityPatternScan(resolveMustflowRoot(), {
|
|
139
|
+
paths: options.paths,
|
|
140
|
+
maxFiles: options.maxFiles ?? undefined,
|
|
141
|
+
maxFileBytes: options.maxFileBytes ?? undefined,
|
|
142
|
+
maxFindings: options.maxFindings ?? undefined,
|
|
143
|
+
});
|
|
144
|
+
if (options.json) {
|
|
145
|
+
reporter.stdout(JSON.stringify(report, null, 2));
|
|
146
|
+
return report.ok ? 0 : 1;
|
|
147
|
+
}
|
|
148
|
+
reporter.stdout(renderSecurityPatternScanSummary(report, lang));
|
|
149
|
+
return report.ok ? 0 : 1;
|
|
150
|
+
}
|
|
@@ -195,6 +195,13 @@ function addDependencyImpacts(root, changedFiles, impacts, policy, findings, iss
|
|
|
195
195
|
issues.push(`dependency-graph: ${issue}`);
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
|
+
for (const dependencyFinding of dependencyReport.findings) {
|
|
199
|
+
const mappedFinding = mapDependencyGraphTruncationFinding(dependencyFinding);
|
|
200
|
+
if (mappedFinding === null) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
findings.push(mappedFinding);
|
|
204
|
+
}
|
|
198
205
|
const changedPathSet = new Set(sourcePaths);
|
|
199
206
|
for (const edge of dependencyReport.edges) {
|
|
200
207
|
if (!changedPathSet.has(edge.target_path) || changedPathSet.has(edge.source_path)) {
|
|
@@ -209,6 +216,15 @@ function addDependencyImpacts(root, changedFiles, impacts, policy, findings, iss
|
|
|
209
216
|
}, policy, findings, issues);
|
|
210
217
|
}
|
|
211
218
|
}
|
|
219
|
+
function mapDependencyGraphTruncationFinding(finding) {
|
|
220
|
+
if (finding.code === 'dependency_graph_max_files_exceeded') {
|
|
221
|
+
return makeFinding('change_impact_max_files_exceeded', 'high', finding.path, `Dependency graph input was truncated while computing change impact: ${finding.message}`);
|
|
222
|
+
}
|
|
223
|
+
if (finding.code === 'dependency_graph_max_nodes_exceeded' || finding.code === 'dependency_graph_max_edges_exceeded') {
|
|
224
|
+
return makeFinding('change_impact_max_impacts_exceeded', 'high', finding.path, `Dependency graph impact expansion was truncated while computing change impact: ${finding.message}`);
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
212
228
|
function createScriptHints(changedFiles) {
|
|
213
229
|
const sourcePaths = changedFiles.filter((file) => file.surface === 'source').map((file) => file.path);
|
|
214
230
|
const selectorRelevantFiles = changedFiles.filter((file) => ['source', 'test', 'schema', 'config', 'package', 'template', 'workflow', 'unknown'].includes(file.surface));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { existsSync, lstatSync, readdirSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { DEFAULT_IGNORED_DIRECTORIES, isIgnoredDirectoryPath } from './ignored-directories.js';
|
|
4
5
|
import { ensureInside, ensureInsideWithoutSymlinks, readFileInsideWithoutSymlinks } from './safe-filesystem.js';
|
|
5
6
|
import { listSourceAnchorFiles, parseSourceAnchorsInContent, sourceAnchorTextContainsSecretLike, splitSourceAnchorList, } from './source-anchors.js';
|
|
6
7
|
export const CODE_PACK_ID = 'code';
|
|
@@ -19,17 +20,7 @@ const SVELTE_EXTENSIONS = ['.svelte'];
|
|
|
19
20
|
const GO_EXTENSIONS = ['.go'];
|
|
20
21
|
const RUST_EXTENSIONS = ['.rs'];
|
|
21
22
|
const PYTHON_EXTENSIONS = ['.py'];
|
|
22
|
-
const IGNORED_DIRECTORIES =
|
|
23
|
-
'.git',
|
|
24
|
-
'.mustflow/cache',
|
|
25
|
-
'.mustflow/state',
|
|
26
|
-
'node_modules',
|
|
27
|
-
'dist',
|
|
28
|
-
'build',
|
|
29
|
-
'coverage',
|
|
30
|
-
'.next',
|
|
31
|
-
'.turbo',
|
|
32
|
-
];
|
|
23
|
+
const IGNORED_DIRECTORIES = DEFAULT_IGNORED_DIRECTORIES;
|
|
33
24
|
const ERROR_OUTLINE_CODES = new Set([
|
|
34
25
|
'code_outline_path_outside_root',
|
|
35
26
|
'code_outline_unreadable_path',
|
|
@@ -97,8 +88,7 @@ export function languageForPath(filePath) {
|
|
|
97
88
|
return languageAdapterForPath(filePath)?.languageForPath(filePath) ?? null;
|
|
98
89
|
}
|
|
99
90
|
function isIgnoredDirectory(relativePath) {
|
|
100
|
-
|
|
101
|
-
return IGNORED_DIRECTORIES.some((directory) => normalized === directory || normalized.startsWith(`${directory}/`));
|
|
91
|
+
return isIgnoredDirectoryPath(normalizeRelativePath(relativePath), IGNORED_DIRECTORIES);
|
|
102
92
|
}
|
|
103
93
|
function makeOutlineFinding(code, severity, pathValue, message) {
|
|
104
94
|
return { code, severity, path: pathValue, message };
|
package/dist/core/command-env.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
1
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { readString, readStringArray } from './config-loading.js';
|
|
4
4
|
export const COMMAND_ENV_POLICIES = new Set(['inherit', 'minimal', 'allowlist']);
|
|
@@ -31,12 +31,30 @@ export const DEFAULT_PROJECT_LOCAL_BIN_BARE_EXECUTABLE_ALLOWLIST = new Set(['mf'
|
|
|
31
31
|
function getPathEnvKey(env) {
|
|
32
32
|
return Object.keys(env).find((key) => key.toLowerCase() === 'path') ?? 'PATH';
|
|
33
33
|
}
|
|
34
|
-
function
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
function normalizePathForComparison(value) {
|
|
35
|
+
const resolved = path.resolve(value);
|
|
36
|
+
if (!existsSync(resolved)) {
|
|
37
|
+
return resolved;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
return realpathSync.native(resolved);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return resolved;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function pathsEqual(left, right) {
|
|
47
|
+
return process.platform === 'win32' ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
48
|
+
}
|
|
49
|
+
function pathEntryCandidates(entry, projectRoot) {
|
|
50
|
+
if (path.isAbsolute(entry)) {
|
|
51
|
+
return [entry];
|
|
52
|
+
}
|
|
53
|
+
return [path.resolve(projectRoot, entry), path.resolve(entry)];
|
|
54
|
+
}
|
|
55
|
+
function sameResolvedPath(left, right, projectRoot) {
|
|
56
|
+
const resolvedRight = normalizePathForComparison(right);
|
|
57
|
+
return pathEntryCandidates(left, projectRoot).some((candidate) => pathsEqual(normalizePathForComparison(candidate), resolvedRight));
|
|
40
58
|
}
|
|
41
59
|
function uniqueEnvNames(values) {
|
|
42
60
|
return [...new Set(values.map((value) => value.trim()).filter((value) => value.length > 0))].sort((left, right) => left.localeCompare(right));
|
|
@@ -112,7 +130,7 @@ function removeProjectLocalBinFromPath(env, projectRoot) {
|
|
|
112
130
|
...env,
|
|
113
131
|
[pathKey]: currentPath
|
|
114
132
|
.split(path.delimiter)
|
|
115
|
-
.filter((entry) => entry.length > 0 && !sameResolvedPath(entry, localBinPath))
|
|
133
|
+
.filter((entry) => entry.length > 0 && !sameResolvedPath(entry, localBinPath, projectRoot))
|
|
116
134
|
.join(path.delimiter),
|
|
117
135
|
};
|
|
118
136
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { existsSync, lstatSync, readdirSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { DEFAULT_IGNORED_DIRECTORIES, isIgnoredDirectoryPath } from './ignored-directories.js';
|
|
4
5
|
import { ensureInside, ensureInsideWithoutSymlinks, readFileInsideWithoutSymlinks } from './safe-filesystem.js';
|
|
5
6
|
export const CONFIG_CHAIN_PACK_ID = 'repo';
|
|
6
7
|
export const CONFIG_CHAIN_SCRIPT_ID = 'config-chain';
|
|
@@ -37,17 +38,7 @@ const CONFIG_FILE_NAMES = [
|
|
|
37
38
|
'.mustflow/config/commands.toml',
|
|
38
39
|
'.mustflow/config/mustflow.toml',
|
|
39
40
|
];
|
|
40
|
-
const IGNORED_DIRECTORIES =
|
|
41
|
-
'.git',
|
|
42
|
-
'.mustflow/cache',
|
|
43
|
-
'.mustflow/state',
|
|
44
|
-
'node_modules',
|
|
45
|
-
'dist',
|
|
46
|
-
'build',
|
|
47
|
-
'coverage',
|
|
48
|
-
'.next',
|
|
49
|
-
'.turbo',
|
|
50
|
-
];
|
|
41
|
+
const IGNORED_DIRECTORIES = DEFAULT_IGNORED_DIRECTORIES;
|
|
51
42
|
const ERROR_CODES = new Set([
|
|
52
43
|
'config_chain_path_outside_root',
|
|
53
44
|
'config_chain_unreadable_path',
|
|
@@ -133,8 +124,7 @@ function isConfigFile(relativePath) {
|
|
|
133
124
|
/^tsconfig(?:\..*)?\.json$/u.test(name));
|
|
134
125
|
}
|
|
135
126
|
function isIgnoredDirectory(relativePath) {
|
|
136
|
-
|
|
137
|
-
return IGNORED_DIRECTORIES.some((directory) => normalized === directory || normalized.startsWith(`${directory}/`));
|
|
127
|
+
return isIgnoredDirectoryPath(normalizeRelativePath(relativePath), IGNORED_DIRECTORIES);
|
|
138
128
|
}
|
|
139
129
|
function normalizeTargetPath(projectRoot, targetPath) {
|
|
140
130
|
const absolutePath = path.resolve(process.cwd(), targetPath);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { existsSync, lstatSync, readdirSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { DEFAULT_IGNORED_DIRECTORIES, isIgnoredDirectoryPath } from './ignored-directories.js';
|
|
4
5
|
import { ensureInside, ensureInsideWithoutSymlinks, readFileInsideWithoutSymlinks } from './safe-filesystem.js';
|
|
5
6
|
export const DEPENDENCY_GRAPH_PACK_ID = 'code';
|
|
6
7
|
export const DEPENDENCY_GRAPH_SCRIPT_ID = 'dependency-graph';
|
|
@@ -14,17 +15,7 @@ const MAX_ISSUES = 50;
|
|
|
14
15
|
const MAX_CYCLES = 20;
|
|
15
16
|
const SOURCE_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'];
|
|
16
17
|
const RESOLVE_EXTENSIONS = [...SOURCE_EXTENSIONS, '.json'];
|
|
17
|
-
const IGNORED_DIRECTORIES =
|
|
18
|
-
'.git',
|
|
19
|
-
'.mustflow/cache',
|
|
20
|
-
'.mustflow/state',
|
|
21
|
-
'node_modules',
|
|
22
|
-
'dist',
|
|
23
|
-
'build',
|
|
24
|
-
'coverage',
|
|
25
|
-
'.next',
|
|
26
|
-
'.turbo',
|
|
27
|
-
];
|
|
18
|
+
const IGNORED_DIRECTORIES = DEFAULT_IGNORED_DIRECTORIES;
|
|
28
19
|
const ERROR_CODES = new Set([
|
|
29
20
|
'dependency_graph_path_outside_root',
|
|
30
21
|
'dependency_graph_unreadable_path',
|
|
@@ -64,8 +55,7 @@ function isSourceLanguage(language) {
|
|
|
64
55
|
return language !== 'json' && language !== 'other';
|
|
65
56
|
}
|
|
66
57
|
function isIgnoredDirectory(relativePath) {
|
|
67
|
-
|
|
68
|
-
return IGNORED_DIRECTORIES.some((directory) => normalized === directory || normalized.startsWith(`${directory}/`));
|
|
58
|
+
return isIgnoredDirectoryPath(normalizeRelativePath(relativePath), IGNORED_DIRECTORIES);
|
|
69
59
|
}
|
|
70
60
|
function makeFinding(code, severity, pathValue, message) {
|
|
71
61
|
return { code, severity, path: pathValue, message };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { existsSync, lstatSync, readdirSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { DEFAULT_IGNORED_DIRECTORIES, isIgnoredDirectoryPath } from './ignored-directories.js';
|
|
4
5
|
import { ensureInside, ensureInsideWithoutSymlinks, readFileInsideWithoutSymlinks } from './safe-filesystem.js';
|
|
5
6
|
export const LINK_INTEGRITY_PACK_ID = 'docs';
|
|
6
7
|
export const LINK_INTEGRITY_SCRIPT_ID = 'link-integrity';
|
|
@@ -11,7 +12,7 @@ const MAX_ISSUES = 50;
|
|
|
11
12
|
const DEFAULT_PATHS = ['README.md', 'schemas/README.md', 'docs-site/src/content/docs'];
|
|
12
13
|
const PATH_FILTERS = ['*.md', '*.mdx'];
|
|
13
14
|
const CHECKED_LINK_KINDS = ['local_file', 'local_anchor'];
|
|
14
|
-
const IGNORED_DIRECTORIES =
|
|
15
|
+
const IGNORED_DIRECTORIES = DEFAULT_IGNORED_DIRECTORIES;
|
|
15
16
|
const ERROR_CODES = new Set([
|
|
16
17
|
'link_integrity_path_outside_root',
|
|
17
18
|
'link_integrity_unreadable_path',
|
|
@@ -59,7 +60,7 @@ function addCandidate(candidates, findings, issues, policy, candidate) {
|
|
|
59
60
|
}
|
|
60
61
|
function collectDocumentsFromDirectory(projectRoot, absoluteDirectory, candidates, findings, issues, policy) {
|
|
61
62
|
const relativeDirectory = normalizeRelativePath(path.relative(projectRoot, absoluteDirectory));
|
|
62
|
-
if (
|
|
63
|
+
if (isIgnoredDirectoryPath(relativeDirectory, IGNORED_DIRECTORIES)) {
|
|
63
64
|
return;
|
|
64
65
|
}
|
|
65
66
|
let entries;
|
|
@@ -173,9 +174,26 @@ function splitTarget(target) {
|
|
|
173
174
|
const anchor = rawAnchor === undefined ? null : decodeUriComponentSafe(rawAnchor);
|
|
174
175
|
return { pathPart, anchor };
|
|
175
176
|
}
|
|
177
|
+
function stripHtmlTagText(value) {
|
|
178
|
+
let result = '';
|
|
179
|
+
let tagDepth = 0;
|
|
180
|
+
for (const char of value) {
|
|
181
|
+
if (char === '<') {
|
|
182
|
+
tagDepth += 1;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (char === '>') {
|
|
186
|
+
tagDepth = Math.max(0, tagDepth - 1);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (tagDepth === 0) {
|
|
190
|
+
result += char;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
176
195
|
function slugHeading(value) {
|
|
177
|
-
return value
|
|
178
|
-
.replace(/<[^>]+>/gu, '')
|
|
196
|
+
return stripHtmlTagText(value)
|
|
179
197
|
.replace(/[`*_~]/gu, '')
|
|
180
198
|
.replace(/\[([^\]]+)\]\([^)]+\)/gu, '$1')
|
|
181
199
|
.toLocaleLowerCase()
|
|
@@ -369,6 +387,7 @@ export function checkLinkIntegrity(projectRoot, options) {
|
|
|
369
387
|
max_file_bytes: options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES,
|
|
370
388
|
default_paths: [...DEFAULT_PATHS],
|
|
371
389
|
path_filters: [...PATH_FILTERS],
|
|
390
|
+
ignored_directories: [...IGNORED_DIRECTORIES],
|
|
372
391
|
checked_link_kinds: [...CHECKED_LINK_KINDS],
|
|
373
392
|
};
|
|
374
393
|
const findings = [];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { existsSync, lstatSync, readdirSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { DEFAULT_IGNORED_DIRECTORIES, isIgnoredDirectoryPath } from './ignored-directories.js';
|
|
4
5
|
import { ensureInside, ensureInsideWithoutSymlinks, readFileInsideWithoutSymlinks } from './safe-filesystem.js';
|
|
5
6
|
export const ENV_CONTRACT_PACK_ID = 'repo';
|
|
6
7
|
export const ENV_CONTRACT_SCRIPT_ID = 'env-contract';
|
|
@@ -36,17 +37,7 @@ const ENV_EXAMPLE_NAMES = [
|
|
|
36
37
|
'.dev.vars.example',
|
|
37
38
|
];
|
|
38
39
|
const SECRET_ENV_NAMES = ['.env', '.env.local', '.env.production', '.env.development', '.dev.vars'];
|
|
39
|
-
const IGNORED_DIRECTORIES =
|
|
40
|
-
'.git',
|
|
41
|
-
'.mustflow/cache',
|
|
42
|
-
'.mustflow/state',
|
|
43
|
-
'node_modules',
|
|
44
|
-
'dist',
|
|
45
|
-
'build',
|
|
46
|
-
'coverage',
|
|
47
|
-
'.next',
|
|
48
|
-
'.turbo',
|
|
49
|
-
];
|
|
40
|
+
const IGNORED_DIRECTORIES = DEFAULT_IGNORED_DIRECTORIES;
|
|
50
41
|
const ERROR_CODES = new Set([
|
|
51
42
|
'env_contract_path_outside_root',
|
|
52
43
|
'env_contract_unreadable_path',
|
|
@@ -66,8 +57,7 @@ function pushIssue(issues, issue) {
|
|
|
66
57
|
}
|
|
67
58
|
}
|
|
68
59
|
function isIgnoredDirectory(relativePath) {
|
|
69
|
-
|
|
70
|
-
return IGNORED_DIRECTORIES.some((directory) => normalized === directory || normalized.startsWith(`${directory}/`));
|
|
60
|
+
return isIgnoredDirectoryPath(normalizeRelativePath(relativePath), IGNORED_DIRECTORIES);
|
|
71
61
|
}
|
|
72
62
|
function isEnvExampleFile(relativePath) {
|
|
73
63
|
const name = path.basename(relativePath).toLowerCase();
|
package/dist/core/export-diff.js
CHANGED
|
@@ -3,6 +3,7 @@ import { createHash } from 'node:crypto';
|
|
|
3
3
|
import { existsSync, readFileSync } from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { extractSymbols, languageForPath } from './code-outline.js';
|
|
6
|
+
import { DEFAULT_IGNORED_DIRECTORIES, isIgnoredDirectoryPath } from './ignored-directories.js';
|
|
6
7
|
import { ensureInsideWithoutSymlinks, readFileInsideWithoutSymlinks } from './safe-filesystem.js';
|
|
7
8
|
export const CODE_EXPORT_DIFF_SCRIPT_ID = 'export-diff';
|
|
8
9
|
export const CODE_EXPORT_DIFF_SCRIPT_REF = `code/${CODE_EXPORT_DIFF_SCRIPT_ID}`;
|
|
@@ -10,7 +11,7 @@ const DEFAULT_BASE_REF = 'HEAD';
|
|
|
10
11
|
const DEFAULT_MAX_FILES = 100;
|
|
11
12
|
const DEFAULT_MAX_FILE_BYTES = 1024 * 1024;
|
|
12
13
|
const SUPPORTED_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs'];
|
|
13
|
-
const IGNORED_DIRECTORIES =
|
|
14
|
+
const IGNORED_DIRECTORIES = DEFAULT_IGNORED_DIRECTORIES;
|
|
14
15
|
const ERROR_CODES = new Set([
|
|
15
16
|
'export_diff_git_unavailable',
|
|
16
17
|
'export_diff_invalid_ref',
|
|
@@ -46,8 +47,7 @@ function makeFinding(code, severity, pathValue, message) {
|
|
|
46
47
|
return { code, severity, path: pathValue, message };
|
|
47
48
|
}
|
|
48
49
|
function isIgnoredPath(relativePath) {
|
|
49
|
-
|
|
50
|
-
return IGNORED_DIRECTORIES.some((directory) => normalized === directory || normalized.startsWith(`${directory}/`));
|
|
50
|
+
return isIgnoredDirectoryPath(normalizeRelativePath(relativePath), IGNORED_DIRECTORIES);
|
|
51
51
|
}
|
|
52
52
|
function isSupportedPath(relativePath) {
|
|
53
53
|
return SUPPORTED_EXTENSIONS.includes(path.extname(relativePath).toLowerCase());
|