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.
Files changed (39) hide show
  1. package/README.md +3 -0
  2. package/dist/cli/commands/api/serve.js +73 -10
  3. package/dist/cli/commands/script-pack.js +3 -0
  4. package/dist/cli/i18n/en.js +37 -0
  5. package/dist/cli/i18n/es.js +37 -0
  6. package/dist/cli/i18n/fr.js +37 -0
  7. package/dist/cli/i18n/hi.js +37 -0
  8. package/dist/cli/i18n/ko.js +37 -0
  9. package/dist/cli/i18n/zh.js +37 -0
  10. package/dist/cli/lib/command-registry.js +3 -0
  11. package/dist/cli/lib/script-pack-registry.js +84 -0
  12. package/dist/cli/script-packs/repo-automation-surface.js +88 -0
  13. package/dist/cli/script-packs/repo-dependency-surface.js +87 -0
  14. package/dist/cli/script-packs/repo-toolchain-provenance.js +90 -0
  15. package/dist/core/public-json-contracts.js +27 -0
  16. package/dist/core/repo-automation-surface.js +376 -0
  17. package/dist/core/repo-dependency-surface.js +282 -0
  18. package/dist/core/repo-toolchain-provenance.js +421 -0
  19. package/dist/core/run-receipt-state.js +23 -2
  20. package/dist/core/script-pack-suggestions.js +33 -1
  21. package/dist/core/secret-redaction.js +6 -1
  22. package/package.json +1 -1
  23. package/schemas/README.md +10 -0
  24. package/schemas/api-serve-response.schema.json +1 -0
  25. package/schemas/repo-automation-surface-report.schema.json +148 -0
  26. package/schemas/repo-dependency-surface-report.schema.json +121 -0
  27. package/schemas/repo-toolchain-provenance-report.schema.json +124 -0
  28. package/templates/default/i18n.toml +9 -9
  29. package/templates/default/locales/en/.mustflow/skills/INDEX.md +17 -14
  30. package/templates/default/locales/en/.mustflow/skills/ci-pipeline-triage/SKILL.md +39 -11
  31. package/templates/default/locales/en/.mustflow/skills/cloud-cost-guardrail-review/SKILL.md +4 -1
  32. package/templates/default/locales/en/.mustflow/skills/go-code-change/SKILL.md +56 -17
  33. package/templates/default/locales/en/.mustflow/skills/python-code-change/SKILL.md +86 -27
  34. package/templates/default/locales/en/.mustflow/skills/routes.toml +4 -4
  35. package/templates/default/locales/en/.mustflow/skills/rust-code-change/SKILL.md +51 -32
  36. package/templates/default/locales/en/.mustflow/skills/tauri-code-change/SKILL.md +41 -3
  37. package/templates/default/locales/en/.mustflow/skills/typescript-code-change/SKILL.md +47 -29
  38. package/templates/default/locales/en/.mustflow/skills/wails-code-change/SKILL.md +34 -4
  39. package/templates/default/manifest.toml +1 -1
@@ -0,0 +1,282 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { isRecord } from './config-loading.js';
5
+ export const REPO_DEPENDENCY_SURFACE_PACK_ID = 'repo';
6
+ export const REPO_DEPENDENCY_SURFACE_SCRIPT_ID = 'dependency-surface';
7
+ export const REPO_DEPENDENCY_SURFACE_SCRIPT_REF = `${REPO_DEPENDENCY_SURFACE_PACK_ID}/${REPO_DEPENDENCY_SURFACE_SCRIPT_ID}`;
8
+ const DEFAULT_MAX_FILE_BYTES = 256 * 1024;
9
+ const JAVASCRIPT_LOCKFILES = ['package-lock.json', 'npm-shrinkwrap.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lock', 'bun.lockb'];
10
+ const CANDIDATE_PATHS = [
11
+ 'package.json',
12
+ 'pnpm-workspace.yaml',
13
+ '.npmrc',
14
+ 'pyproject.toml',
15
+ 'requirements.txt',
16
+ 'uv.lock',
17
+ 'poetry.lock',
18
+ 'Pipfile.lock',
19
+ 'go.mod',
20
+ 'go.sum',
21
+ 'Cargo.toml',
22
+ 'Cargo.lock',
23
+ 'deny.toml',
24
+ 'renovate.json',
25
+ '.github/dependabot.yml',
26
+ '.github/dependabot.yaml',
27
+ '.github/workflows',
28
+ ...JAVASCRIPT_LOCKFILES,
29
+ ];
30
+ function sha256(value) {
31
+ return `sha256:${createHash('sha256').update(value).digest('hex')}`;
32
+ }
33
+ function normalizeRelativePath(value) {
34
+ return value.replace(/\\/gu, '/').replace(/^\.\/+/u, '');
35
+ }
36
+ function lineForOffset(content, offset) {
37
+ let line = 1;
38
+ for (let index = 0; index < offset; index += 1) {
39
+ if (content.charCodeAt(index) === 10)
40
+ line += 1;
41
+ }
42
+ return line;
43
+ }
44
+ function safeReadText(root, relativePath, scannedPaths, issues) {
45
+ const normalized = normalizeRelativePath(relativePath);
46
+ scannedPaths.add(normalized);
47
+ const absolute = path.join(root, ...normalized.split('/'));
48
+ try {
49
+ const stats = statSync(absolute);
50
+ if (!stats.isFile())
51
+ return null;
52
+ if (stats.size > DEFAULT_MAX_FILE_BYTES) {
53
+ issues.push(`${normalized} exceeds max_file_bytes (${stats.size} > ${DEFAULT_MAX_FILE_BYTES}).`);
54
+ return null;
55
+ }
56
+ return readFileSync(absolute, 'utf8');
57
+ }
58
+ catch (error) {
59
+ if (!existsSync(absolute))
60
+ return null;
61
+ const message = error instanceof Error ? error.message : String(error);
62
+ issues.push(`Could not read ${normalized}: ${message}`);
63
+ return null;
64
+ }
65
+ }
66
+ function uniqueStrings(values) {
67
+ return [...new Set([...values].filter((value) => value.trim().length > 0))].sort((left, right) => left.localeCompare(right));
68
+ }
69
+ function addSurface(surfaces, ecosystem, kind, pathValue, name, evidence, line = null) {
70
+ surfaces.push({
71
+ id: `${ecosystem}:${kind}:${pathValue}:${name}`,
72
+ ecosystem,
73
+ kind,
74
+ path: pathValue,
75
+ line,
76
+ name,
77
+ evidence,
78
+ });
79
+ }
80
+ function scanStaticFiles(root, scannedPaths, surfaces, issues) {
81
+ for (const candidate of CANDIDATE_PATHS) {
82
+ if (candidate === '.github/workflows') {
83
+ continue;
84
+ }
85
+ const absolute = path.join(root, ...candidate.split('/'));
86
+ scannedPaths.add(candidate);
87
+ if (!existsSync(absolute)) {
88
+ continue;
89
+ }
90
+ const ecosystem = candidate.includes('package') || candidate.includes('pnpm') || candidate.includes('yarn') || candidate.includes('bun') || candidate === '.npmrc'
91
+ ? 'javascript'
92
+ : candidate.includes('pyproject') || candidate.includes('requirements') || candidate.includes('uv.lock') || candidate.includes('poetry') || candidate.includes('Pipfile')
93
+ ? 'python'
94
+ : candidate.startsWith('go.')
95
+ ? 'go'
96
+ : candidate.includes('Cargo') || candidate === 'deny.toml'
97
+ ? 'rust'
98
+ : null;
99
+ if (ecosystem === null) {
100
+ const updateKind = candidate.includes('dependabot') || candidate.includes('renovate') ? 'dependency_update_config' : 'audit_config';
101
+ addSurface(surfaces, 'javascript', updateKind, candidate, path.basename(candidate), candidate);
102
+ continue;
103
+ }
104
+ const kind = JAVASCRIPT_LOCKFILES.includes(candidate) || /(?:\.lock|go\.sum)$/u.test(candidate)
105
+ ? 'lockfile'
106
+ : candidate.includes('workspace')
107
+ ? 'workspace_config'
108
+ : candidate === '.npmrc'
109
+ ? 'package_manager_config'
110
+ : candidate === 'deny.toml'
111
+ ? 'audit_config'
112
+ : 'manifest';
113
+ addSurface(surfaces, ecosystem, kind, candidate, path.basename(candidate), candidate);
114
+ if (candidate === 'package.json') {
115
+ const content = safeReadText(root, candidate, scannedPaths, issues);
116
+ if (content === null)
117
+ continue;
118
+ try {
119
+ const parsed = JSON.parse(content);
120
+ if (isRecord(parsed) && typeof parsed.packageManager === 'string') {
121
+ addSurface(surfaces, 'javascript', 'package_manager_config', candidate, 'packageManager', parsed.packageManager, null);
122
+ }
123
+ }
124
+ catch {
125
+ issues.push('Could not parse package.json while inspecting dependency surface.');
126
+ }
127
+ }
128
+ }
129
+ }
130
+ function scanWorkflowDependencyAutomation(root, scannedPaths, surfaces, issues) {
131
+ const workflowRoot = path.join(root, '.github', 'workflows');
132
+ scannedPaths.add('.github/workflows');
133
+ if (!existsSync(workflowRoot)) {
134
+ return;
135
+ }
136
+ let names = [];
137
+ try {
138
+ names = readdirSync(workflowRoot).filter((name) => /\.(?:yml|yaml)$/u.test(name));
139
+ }
140
+ catch (error) {
141
+ const message = error instanceof Error ? error.message : String(error);
142
+ issues.push(`Could not list .github/workflows: ${message}`);
143
+ return;
144
+ }
145
+ for (const name of names) {
146
+ const relativePath = `.github/workflows/${name}`;
147
+ const content = safeReadText(root, relativePath, scannedPaths, issues);
148
+ if (content === null)
149
+ continue;
150
+ for (const [patternName, pattern] of [
151
+ ['npm audit', /\bnpm\s+audit\b/u],
152
+ ['pnpm audit', /\bpnpm\s+audit\b/u],
153
+ ['bun audit', /\bbun\s+audit\b/u],
154
+ ['pip-audit', /\bpip-audit\b/u],
155
+ ['govulncheck', /\bgovulncheck\b/u],
156
+ ['cargo audit', /\bcargo\s+audit\b/u],
157
+ ['osv-scanner', /\bosv-scanner\b/u],
158
+ ['trivy', /\btrivy\b/u],
159
+ ['syft', /\bsyft\b/u],
160
+ ]) {
161
+ const match = pattern.exec(content);
162
+ if (!match || match.index < 0)
163
+ continue;
164
+ const ecosystem = patternName.includes('cargo') ? 'rust' : patternName.includes('go') ? 'go' : patternName.includes('pip') ? 'python' : 'javascript';
165
+ addSurface(surfaces, ecosystem, 'audit_config', relativePath, patternName, match[0], lineForOffset(content, match.index));
166
+ }
167
+ }
168
+ }
169
+ function createFindings(surfaces) {
170
+ const findings = [];
171
+ const jsLockfiles = surfaces.filter((surface) => surface.ecosystem === 'javascript' && surface.kind === 'lockfile');
172
+ if (jsLockfiles.length > 1) {
173
+ findings.push({
174
+ code: 'conflicting_javascript_lockfiles',
175
+ severity: 'high',
176
+ path: jsLockfiles[0]?.path ?? 'package.json',
177
+ message: 'Multiple JavaScript lockfiles were detected.',
178
+ json_pointer: null,
179
+ metric: 'javascript_lockfile_count',
180
+ actual: jsLockfiles.length,
181
+ expected: 1,
182
+ });
183
+ }
184
+ for (const ecosystem of uniqueStrings(surfaces.map((surface) => surface.ecosystem))) {
185
+ const manifests = surfaces.filter((surface) => surface.ecosystem === ecosystem && surface.kind === 'manifest');
186
+ const lockfiles = surfaces.filter((surface) => surface.ecosystem === ecosystem && surface.kind === 'lockfile');
187
+ if (manifests.length > 0 && lockfiles.length === 0 && ecosystem !== 'go') {
188
+ findings.push({
189
+ code: 'manifest_without_lockfile',
190
+ severity: 'medium',
191
+ path: manifests[0]?.path ?? '',
192
+ message: `${ecosystem} dependency manifest was detected without a lockfile.`,
193
+ json_pointer: null,
194
+ metric: `${ecosystem}_lockfile_count`,
195
+ actual: 0,
196
+ expected: 1,
197
+ });
198
+ }
199
+ if (lockfiles.length > 0 && manifests.length === 0) {
200
+ findings.push({
201
+ code: 'lockfile_without_manifest',
202
+ severity: 'medium',
203
+ path: lockfiles[0]?.path ?? '',
204
+ message: `${ecosystem} lockfile was detected without a matching manifest.`,
205
+ json_pointer: null,
206
+ metric: `${ecosystem}_manifest_count`,
207
+ actual: 0,
208
+ expected: 1,
209
+ });
210
+ }
211
+ }
212
+ const updateConfigs = surfaces.filter((surface) => surface.kind === 'dependency_update_config');
213
+ const auditConfigs = surfaces.filter((surface) => surface.kind === 'audit_config');
214
+ if (updateConfigs.length > 0 && auditConfigs.length === 0) {
215
+ findings.push({
216
+ code: 'update_automation_without_policy',
217
+ severity: 'medium',
218
+ path: updateConfigs[0]?.path ?? '.github/dependabot.yml',
219
+ message: 'Dependency update automation was detected without an audit, license, SBOM, or policy surface.',
220
+ json_pointer: null,
221
+ metric: 'audit_config_count',
222
+ actual: 0,
223
+ expected: 1,
224
+ });
225
+ }
226
+ if (surfaces.some((surface) => surface.kind === 'manifest' || surface.kind === 'lockfile') && updateConfigs.length === 0) {
227
+ findings.push({
228
+ code: 'dependency_surface_without_update_automation',
229
+ severity: 'low',
230
+ path: surfaces.find((surface) => surface.kind === 'manifest' || surface.kind === 'lockfile')?.path ?? 'package.json',
231
+ message: 'Dependency surfaces were detected without Dependabot or Renovate configuration.',
232
+ json_pointer: null,
233
+ metric: 'dependency_update_config_count',
234
+ actual: 0,
235
+ expected: 1,
236
+ });
237
+ }
238
+ return findings;
239
+ }
240
+ function createSummary(surfaces, findings) {
241
+ return {
242
+ surface_count: surfaces.length,
243
+ manifest_count: surfaces.filter((surface) => surface.kind === 'manifest').length,
244
+ lockfile_count: surfaces.filter((surface) => surface.kind === 'lockfile').length,
245
+ update_config_count: surfaces.filter((surface) => surface.kind === 'dependency_update_config').length,
246
+ audit_config_count: surfaces.filter((surface) => surface.kind === 'audit_config').length,
247
+ ecosystem_count: uniqueStrings(surfaces.map((surface) => surface.ecosystem)).length,
248
+ finding_count: findings.length,
249
+ };
250
+ }
251
+ export function inspectRepoDependencySurface(projectRoot) {
252
+ const root = path.resolve(projectRoot);
253
+ const scannedPaths = new Set(CANDIDATE_PATHS);
254
+ const issues = [];
255
+ const surfaces = [];
256
+ scanStaticFiles(root, scannedPaths, surfaces, issues);
257
+ scanWorkflowDependencyAutomation(root, scannedPaths, surfaces, issues);
258
+ const sortedSurfaces = surfaces.sort((left, right) => left.path.localeCompare(right.path) || left.name.localeCompare(right.name));
259
+ const findings = createFindings(sortedSurfaces);
260
+ const summary = createSummary(sortedSurfaces, findings);
261
+ const status = issues.length > 0 ? 'error' : findings.some((finding) => finding.severity === 'high') ? 'failed' : 'passed';
262
+ return {
263
+ schema_version: '1',
264
+ command: 'script-pack',
265
+ pack_id: REPO_DEPENDENCY_SURFACE_PACK_ID,
266
+ script_id: REPO_DEPENDENCY_SURFACE_SCRIPT_ID,
267
+ script_ref: REPO_DEPENDENCY_SURFACE_SCRIPT_REF,
268
+ action: 'inspect',
269
+ status,
270
+ ok: status === 'passed',
271
+ mustflow_root: root,
272
+ input: {
273
+ scanned_paths: uniqueStrings(scannedPaths),
274
+ max_file_bytes: DEFAULT_MAX_FILE_BYTES,
275
+ },
276
+ input_hash: sha256(JSON.stringify({ summary, surfaces: sortedSurfaces, findings, issues })),
277
+ summary,
278
+ surfaces: sortedSurfaces,
279
+ findings,
280
+ issues,
281
+ };
282
+ }