mustflow 2.18.21 → 2.21.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/classify.js +2 -3
- package/dist/cli/commands/doctor.js +46 -6
- package/dist/cli/commands/run/output.js +1 -1
- package/dist/cli/commands/run/receipt.js +1 -0
- package/dist/cli/commands/run.js +3 -1
- package/dist/cli/commands/verify.js +15 -10
- package/dist/cli/i18n/en.js +1 -0
- package/dist/cli/i18n/es.js +1 -0
- package/dist/cli/i18n/fr.js +1 -0
- package/dist/cli/i18n/hi.js +1 -0
- package/dist/cli/i18n/ko.js +1 -0
- package/dist/cli/i18n/zh.js +1 -0
- package/dist/cli/lib/filesystem.js +3 -96
- package/dist/cli/lib/local-index/index.js +4 -4
- package/dist/cli/lib/repo-map.js +3 -2
- package/dist/cli/lib/run-plan.js +8 -4
- package/dist/core/check-issues.js +1 -1
- package/dist/core/command-contract-validation.js +24 -10
- package/dist/core/command-effects.js +3 -4
- package/dist/core/command-output-limits.js +2 -1
- package/dist/core/line-endings.js +12 -4
- package/dist/core/repeated-failure.js +3 -3
- package/dist/core/run-performance-history.js +4 -4
- package/dist/core/run-profile.js +2 -3
- package/dist/core/run-receipt.js +11 -3
- package/dist/core/run-write-drift.js +67 -15
- package/dist/core/safe-filesystem.js +158 -0
- package/package.json +1 -1
- package/schemas/commands.schema.json +1 -0
- package/schemas/doctor-report.schema.json +23 -1
- package/schemas/run-receipt.schema.json +6 -2
- package/templates/default/i18n.toml +13 -13
- package/templates/default/locales/en/.mustflow/skills/INDEX.md +13 -13
- package/templates/default/locales/en/.mustflow/skills/adapter-boundary/SKILL.md +72 -4
- package/templates/default/locales/en/.mustflow/skills/command-contract-authoring/SKILL.md +16 -10
- package/templates/default/locales/en/.mustflow/skills/command-pattern/SKILL.md +64 -7
- package/templates/default/locales/en/.mustflow/skills/database-change-safety/SKILL.md +249 -16
- package/templates/default/locales/en/.mustflow/skills/dependency-reality-check/SKILL.md +37 -7
- package/templates/default/locales/en/.mustflow/skills/migration-safety-check/SKILL.md +74 -10
- package/templates/default/locales/en/.mustflow/skills/performance-budget-check/SKILL.md +132 -5
- package/templates/default/locales/en/.mustflow/skills/pure-core-imperative-shell/SKILL.md +12 -5
- package/templates/default/locales/en/.mustflow/skills/result-option/SKILL.md +4 -2
- package/templates/default/locales/en/.mustflow/skills/security-privacy-review/SKILL.md +112 -29
- package/templates/default/locales/en/.mustflow/skills/state-machine-pattern/SKILL.md +17 -4
- package/templates/default/locales/en/.mustflow/skills/structure-discovery-gate/SKILL.md +193 -2
- package/templates/default/manifest.toml +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
1
|
import path from 'node:path';
|
|
3
2
|
import { createChangeClassificationReport, } from '../../core/change-classification.js';
|
|
3
|
+
import { writeJsonFileInsideWithoutSymlinks } from '../../core/safe-filesystem.js';
|
|
4
4
|
import { printUsageError, renderHelp } from '../lib/cli-output.js';
|
|
5
5
|
import { requireGitChangedFiles } from '../lib/git-changes.js';
|
|
6
6
|
import { t } from '../lib/i18n.js';
|
|
@@ -111,8 +111,7 @@ function resolveWritePath(projectRoot, inputPath) {
|
|
|
111
111
|
}
|
|
112
112
|
function writeClassifyOutput(projectRoot, inputPath, output) {
|
|
113
113
|
const outputPath = resolveWritePath(projectRoot, inputPath);
|
|
114
|
-
|
|
115
|
-
writeFileSync(outputPath, `${JSON.stringify(output, null, 2)}\n`, 'utf8');
|
|
114
|
+
writeJsonFileInsideWithoutSymlinks(projectRoot, outputPath, output);
|
|
116
115
|
}
|
|
117
116
|
export function runClassify(args, reporter, lang = 'en') {
|
|
118
117
|
if (args.includes('--help') || args.includes('-h')) {
|
|
@@ -5,7 +5,9 @@ import { getAgentContext, } from '../lib/agent-context.js';
|
|
|
5
5
|
import { t } from '../lib/i18n.js';
|
|
6
6
|
import { getLocalIndexDatabasePath } from '../lib/local-index.js';
|
|
7
7
|
import { resolveMustflowRoot } from '../lib/project-root.js';
|
|
8
|
-
import {
|
|
8
|
+
import { checkMustflowProjectReport } from '../lib/validation.js';
|
|
9
|
+
import { findCommandEnvInheritanceWarnings } from '../../core/command-contract-validation.js';
|
|
10
|
+
import { readCommandContract } from '../../core/config-loading.js';
|
|
9
11
|
import { summarizeSkillRouteAlignment } from '../../core/skill-route-alignment.js';
|
|
10
12
|
const DOCTOR_SCHEMA_VERSION = '1';
|
|
11
13
|
export function getDoctorHelp(lang = 'en') {
|
|
@@ -46,6 +48,7 @@ function createDiagnostics(output) {
|
|
|
46
48
|
const localIndexExists = existsSync(getLocalIndexDatabasePath(output.mustflow_root));
|
|
47
49
|
const runnableIntentCount = output.context.runnable_intents.length;
|
|
48
50
|
const skillRouteAlignment = summarizeSkillRouteAlignment(output.check.issues);
|
|
51
|
+
const inheritedEnvIntentCount = output.command_environment.inherited_intents.length;
|
|
49
52
|
diagnostics.push({
|
|
50
53
|
id: 'install',
|
|
51
54
|
status: output.installed ? 'ok' : 'fail',
|
|
@@ -55,7 +58,9 @@ function createDiagnostics(output) {
|
|
|
55
58
|
diagnostics.push({
|
|
56
59
|
id: 'validation',
|
|
57
60
|
status: output.check.ok ? 'ok' : 'fail',
|
|
58
|
-
summary:
|
|
61
|
+
summary: output.check.warning_count === 0
|
|
62
|
+
? `${output.check.issue_count} ${pluralize(output.check.issue_count, 'issue', 'issues')}`
|
|
63
|
+
: `${output.check.issue_count} ${pluralize(output.check.issue_count, 'issue', 'issues')}, ${output.check.warning_count} ${pluralize(output.check.warning_count, 'warning', 'warnings')}`,
|
|
59
64
|
action: output.check.ok ? null : checkCommand,
|
|
60
65
|
});
|
|
61
66
|
diagnostics.push({
|
|
@@ -72,6 +77,16 @@ function createDiagnostics(output) {
|
|
|
72
77
|
: 'missing',
|
|
73
78
|
action: output.context.command_contract_exists ? 'mf help commands' : 'mf init --dry-run',
|
|
74
79
|
});
|
|
80
|
+
diagnostics.push({
|
|
81
|
+
id: 'environment',
|
|
82
|
+
status: output.context.command_contract_exists ? (inheritedEnvIntentCount > 0 ? 'warn' : 'ok') : 'info',
|
|
83
|
+
summary: output.context.command_contract_exists
|
|
84
|
+
? inheritedEnvIntentCount === 0
|
|
85
|
+
? 'no inherited host-environment intents'
|
|
86
|
+
: `${inheritedEnvIntentCount} inherited host-environment ${pluralize(inheritedEnvIntentCount, 'intent', 'intents')}: ${output.command_environment.inherited_intents.join(', ')}`
|
|
87
|
+
: 'command contract missing',
|
|
88
|
+
action: inheritedEnvIntentCount > 0 ? 'mf check --strict --json' : null,
|
|
89
|
+
});
|
|
75
90
|
diagnostics.push({
|
|
76
91
|
id: 'read_order',
|
|
77
92
|
status: output.context.missing_read_order.length === 0 ? 'ok' : 'fail',
|
|
@@ -124,15 +139,37 @@ function getNextSteps(output) {
|
|
|
124
139
|
}
|
|
125
140
|
return nextSteps;
|
|
126
141
|
}
|
|
142
|
+
function readCommandEnvironmentSummary(projectRoot) {
|
|
143
|
+
try {
|
|
144
|
+
const contract = readCommandContract(projectRoot);
|
|
145
|
+
const warnings = findCommandEnvInheritanceWarnings(contract);
|
|
146
|
+
return {
|
|
147
|
+
inherited_intents: warnings.map((warning) => warning.intentName).sort((left, right) => left.localeCompare(right)),
|
|
148
|
+
inherited_network_intents: warnings
|
|
149
|
+
.filter((warning) => warning.network)
|
|
150
|
+
.map((warning) => warning.intentName)
|
|
151
|
+
.sort((left, right) => left.localeCompare(right)),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return {
|
|
156
|
+
inherited_intents: [],
|
|
157
|
+
inherited_network_intents: [],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
127
161
|
function createDoctorOutput(strict) {
|
|
128
162
|
const mustflowRoot = resolveMustflowRoot();
|
|
129
163
|
const context = getAgentContext(mustflowRoot);
|
|
130
|
-
const
|
|
164
|
+
const checkReport = checkMustflowProjectReport(mustflowRoot, { strict });
|
|
131
165
|
const check = {
|
|
132
|
-
ok: issues.length === 0,
|
|
133
|
-
issue_count: issues.length,
|
|
134
|
-
issues,
|
|
166
|
+
ok: checkReport.issues.length === 0,
|
|
167
|
+
issue_count: checkReport.issues.length,
|
|
168
|
+
issues: checkReport.issues,
|
|
169
|
+
warning_count: checkReport.warnings.length,
|
|
170
|
+
warnings: checkReport.warnings,
|
|
135
171
|
};
|
|
172
|
+
const commandEnvironment = readCommandEnvironmentSummary(mustflowRoot);
|
|
136
173
|
const baseOutput = {
|
|
137
174
|
schema_version: DOCTOR_SCHEMA_VERSION,
|
|
138
175
|
command: 'doctor',
|
|
@@ -150,6 +187,7 @@ function createDoctorOutput(strict) {
|
|
|
150
187
|
missing_optional_read_order: context.optional_read_order.filter((entry) => !entry.exists).map((entry) => entry.path),
|
|
151
188
|
latest_run_exists: context.latest_run.exists,
|
|
152
189
|
},
|
|
190
|
+
command_environment: commandEnvironment,
|
|
153
191
|
effective_policy: context.effective_policy,
|
|
154
192
|
state_policy: context.state_policy,
|
|
155
193
|
blocked_actions: context.blocked_actions,
|
|
@@ -171,6 +209,8 @@ function getDiagnosticLabel(id, lang) {
|
|
|
171
209
|
return t(lang, 'doctor.diagnostic.skillRoutes');
|
|
172
210
|
case 'commands':
|
|
173
211
|
return t(lang, 'doctor.diagnostic.commands');
|
|
212
|
+
case 'environment':
|
|
213
|
+
return t(lang, 'doctor.diagnostic.environment');
|
|
174
214
|
case 'read_order':
|
|
175
215
|
return t(lang, 'doctor.diagnostic.readOrder');
|
|
176
216
|
case 'optional_read_order':
|
|
@@ -46,7 +46,7 @@ export function writeStreamChunk(reporter, stream, chunk) {
|
|
|
46
46
|
reporter.stderr(chunk.toString());
|
|
47
47
|
}
|
|
48
48
|
export function createOutputLimitError(stream, maxOutputBytes) {
|
|
49
|
-
return Object.assign(new Error(`${stream} exceeded max_output_bytes (${maxOutputBytes})`), {
|
|
49
|
+
return Object.assign(new Error(`${stream} exceeded per-stream max_output_bytes (${maxOutputBytes})`), {
|
|
50
50
|
code: OUTPUT_LIMIT_ERROR_CODE,
|
|
51
51
|
});
|
|
52
52
|
}
|
|
@@ -16,6 +16,7 @@ export function assembleRunReceipt(input) {
|
|
|
16
16
|
envPolicy: input.plan.envPolicy,
|
|
17
17
|
envAllowlist: input.plan.envAllowlist,
|
|
18
18
|
timeoutSeconds: input.plan.timeoutSeconds,
|
|
19
|
+
killAfterSeconds: input.plan.killAfterSeconds,
|
|
19
20
|
maxOutputBytes: input.plan.maxOutputBytes,
|
|
20
21
|
successExitCodes: input.plan.successExitCodes,
|
|
21
22
|
exitCode: input.exitCode,
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -171,7 +171,9 @@ export async function runRun(args, reporter, lang = 'en', options = {}) {
|
|
|
171
171
|
}
|
|
172
172
|
const runReceiptPolicy = profiler.measure('retention_policy', () => resolveRunReceiptRetentionPolicy(readMustflowConfigIfExists(projectRoot)));
|
|
173
173
|
const env = profiler.measure('environment', () => createCommandEnv(projectRoot, { policy: plan.envPolicy, allowlist: plan.envAllowlist }));
|
|
174
|
-
const writeTracker = profiler.measure('write_drift_before', () => startRunWriteTracking(projectRoot, contract, intentName
|
|
174
|
+
const writeTracker = profiler.measure('write_drift_before', () => startRunWriteTracking(projectRoot, contract, intentName, {
|
|
175
|
+
additionalDeclaredPaths: options.additionalDeclaredWritePaths,
|
|
176
|
+
}));
|
|
175
177
|
const stdoutTailBytes = Math.min(runReceiptPolicy.stdoutTailBytes, plan.maxOutputBytes);
|
|
176
178
|
const stderrTailBytes = Math.min(runReceiptPolicy.stderrTailBytes, plan.maxOutputBytes);
|
|
177
179
|
let streamedOutput = false;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { createClassifyOutput } from './classify.js';
|
|
5
5
|
import { runRun } from './run.js';
|
|
6
6
|
import { createChangeVerificationReport, } from '../../core/change-verification.js';
|
|
7
|
+
import { writeJsonFileInsideWithoutSymlinks } from '../../core/safe-filesystem.js';
|
|
7
8
|
import { createVerifyCompletionVerdict, } from '../../core/completion-verdict.js';
|
|
8
|
-
import {
|
|
9
|
+
import { createStateRunId } from '../../core/atomic-state-write.js';
|
|
9
10
|
import { createExternalEvidenceRisks, } from '../../core/external-evidence.js';
|
|
10
11
|
import { createRepeatedFailureRisks, createVerificationFailureFingerprint, updateRepeatedFailureState, } from '../../core/repeated-failure.js';
|
|
11
12
|
import { countReproEvidenceVerdictEffects, createReproEvidenceRisks, } from '../../core/repro-evidence.js';
|
|
@@ -545,8 +546,7 @@ function createInputFromChanged(projectRoot) {
|
|
|
545
546
|
}
|
|
546
547
|
function writeChangedPlan(projectRoot, inputPath, plan) {
|
|
547
548
|
const planPath = resolvePlanPath(projectRoot, inputPath);
|
|
548
|
-
|
|
549
|
-
writeFileSync(planPath, `${JSON.stringify(plan, null, 2)}\n`, 'utf8');
|
|
549
|
+
writeJsonFileInsideWithoutSymlinks(projectRoot, planPath, plan);
|
|
550
550
|
}
|
|
551
551
|
export function planErrorMessageKey(code) {
|
|
552
552
|
switch (code) {
|
|
@@ -628,11 +628,12 @@ function testTargetsByScheduledIntent(report) {
|
|
|
628
628
|
candidate.appliedTestTargets.length > 0)
|
|
629
629
|
.map((candidate) => [candidate.intent, candidate.appliedTestTargets]));
|
|
630
630
|
}
|
|
631
|
-
async function runVerificationIntent(intent, lang, verificationPlanId, testTargets = []) {
|
|
631
|
+
async function runVerificationIntent(intent, lang, verificationPlanId, testTargets = [], additionalDeclaredWritePaths = []) {
|
|
632
632
|
const output = createBufferedOutput();
|
|
633
633
|
const exitCode = await runRun([intent, '--json'], output.reporter, lang, {
|
|
634
634
|
writeLatestReceipt: false,
|
|
635
635
|
testTargets,
|
|
636
|
+
additionalDeclaredWritePaths,
|
|
636
637
|
});
|
|
637
638
|
const rawStdout = output.stdout().trim();
|
|
638
639
|
let receipt = null;
|
|
@@ -680,7 +681,12 @@ async function runVerificationEntriesInParallelChunks(entries, parallelism, lang
|
|
|
680
681
|
const results = [];
|
|
681
682
|
for (let index = 0; index < entries.length; index += parallelism) {
|
|
682
683
|
const chunk = entries.slice(index, index + parallelism);
|
|
683
|
-
|
|
684
|
+
const batchDeclaredWritePaths = [
|
|
685
|
+
...new Set(chunk.flatMap((entry) => entry.effects
|
|
686
|
+
.filter((effect) => effect.access === 'write' && typeof effect.path === 'string')
|
|
687
|
+
.map((effect) => effect.path))),
|
|
688
|
+
].sort((left, right) => left.localeCompare(right));
|
|
689
|
+
results.push(...(await Promise.all(chunk.map((entry) => runVerificationIntent(entry.intent, lang, verificationPlanId, scheduledTestTargets.get(entry.intent) ?? [], batchDeclaredWritePaths)))));
|
|
684
690
|
}
|
|
685
691
|
return results;
|
|
686
692
|
}
|
|
@@ -1059,7 +1065,6 @@ function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks,
|
|
|
1059
1065
|
const statePaths = createVerifyRunStatePaths(projectRoot);
|
|
1060
1066
|
const receipts = [];
|
|
1061
1067
|
const results = [];
|
|
1062
|
-
mkdirSync(statePaths.absoluteIntentDir, { recursive: true });
|
|
1063
1068
|
for (const [index, result] of output.results.entries()) {
|
|
1064
1069
|
let receiptPath = null;
|
|
1065
1070
|
let receiptSha256 = null;
|
|
@@ -1075,7 +1080,7 @@ function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks,
|
|
|
1075
1080
|
};
|
|
1076
1081
|
const receiptContent = `${JSON.stringify(receipt, null, 2)}\n`;
|
|
1077
1082
|
receiptSha256 = hashTextSha256(receiptContent);
|
|
1078
|
-
|
|
1083
|
+
writeJsonFileInsideWithoutSymlinks(projectRoot, absoluteReceiptPath, receipt);
|
|
1079
1084
|
}
|
|
1080
1085
|
receipts.push({
|
|
1081
1086
|
intent: result.intent,
|
|
@@ -1181,7 +1186,7 @@ function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks,
|
|
|
1181
1186
|
...(outputWithReceiptPaths.external_checks ? { external_checks: outputWithReceiptPaths.external_checks } : {}),
|
|
1182
1187
|
receipts,
|
|
1183
1188
|
};
|
|
1184
|
-
|
|
1189
|
+
writeJsonFileInsideWithoutSymlinks(projectRoot, statePaths.absoluteManifestPath, manifest);
|
|
1185
1190
|
const latest = {
|
|
1186
1191
|
schema_version: '1',
|
|
1187
1192
|
command: 'verify',
|
|
@@ -1202,7 +1207,7 @@ function writeVerifyRunReceipts(projectRoot, output, report, sourceAnchorRisks,
|
|
|
1202
1207
|
run_dir: statePaths.runDir,
|
|
1203
1208
|
manifest_path: statePaths.manifestPath,
|
|
1204
1209
|
};
|
|
1205
|
-
|
|
1210
|
+
writeJsonFileInsideWithoutSymlinks(projectRoot, path.join(projectRoot, LATEST_RUN_RECEIPT_PATH), latest);
|
|
1206
1211
|
return outputWithReceiptPaths;
|
|
1207
1212
|
}
|
|
1208
1213
|
async function createVerifyOutput(input, planSource, projectRoot, lang, reproEvidence = null, externalChecks = [], parallelism = DEFAULT_VERIFY_PARALLELISM) {
|
package/dist/cli/i18n/en.js
CHANGED
|
@@ -488,6 +488,7 @@ export const enMessages = {
|
|
|
488
488
|
"doctor.diagnostic.validation": "Validation",
|
|
489
489
|
"doctor.diagnostic.skillRoutes": "Skill routes",
|
|
490
490
|
"doctor.diagnostic.commands": "Command specification",
|
|
491
|
+
"doctor.diagnostic.environment": "Environment",
|
|
491
492
|
"doctor.diagnostic.readOrder": "Read order",
|
|
492
493
|
"doctor.diagnostic.optionalReadOrder": "Optional read order",
|
|
493
494
|
"doctor.diagnostic.repoMap": "REPO_MAP.md",
|
package/dist/cli/i18n/es.js
CHANGED
|
@@ -488,6 +488,7 @@ export const esMessages = {
|
|
|
488
488
|
"doctor.diagnostic.validation": "Validación",
|
|
489
489
|
"doctor.diagnostic.skillRoutes": "Rutas de skills",
|
|
490
490
|
"doctor.diagnostic.commands": "Especificación de comandos",
|
|
491
|
+
"doctor.diagnostic.environment": "Entorno",
|
|
491
492
|
"doctor.diagnostic.readOrder": "Orden de lectura",
|
|
492
493
|
"doctor.diagnostic.optionalReadOrder": "Orden de lectura opcional",
|
|
493
494
|
"doctor.diagnostic.repoMap": "REPO_MAP.md",
|
package/dist/cli/i18n/fr.js
CHANGED
|
@@ -488,6 +488,7 @@ export const frMessages = {
|
|
|
488
488
|
"doctor.diagnostic.validation": "Validation",
|
|
489
489
|
"doctor.diagnostic.skillRoutes": "Routage des skills",
|
|
490
490
|
"doctor.diagnostic.commands": "Spécification des commandes",
|
|
491
|
+
"doctor.diagnostic.environment": "Environnement",
|
|
491
492
|
"doctor.diagnostic.readOrder": "Ordre de lecture",
|
|
492
493
|
"doctor.diagnostic.optionalReadOrder": "Ordre de lecture optionnel",
|
|
493
494
|
"doctor.diagnostic.repoMap": "REPO_MAP.md",
|
package/dist/cli/i18n/hi.js
CHANGED
|
@@ -488,6 +488,7 @@ export const hiMessages = {
|
|
|
488
488
|
"doctor.diagnostic.validation": "सत्यापन",
|
|
489
489
|
"doctor.diagnostic.skillRoutes": "स्किल रूट",
|
|
490
490
|
"doctor.diagnostic.commands": "कमांड विनिर्देश",
|
|
491
|
+
"doctor.diagnostic.environment": "एनवायरनमेंट",
|
|
491
492
|
"doctor.diagnostic.readOrder": "पढ़ने का क्रम",
|
|
492
493
|
"doctor.diagnostic.optionalReadOrder": "वैकल्पिक पढ़ने का क्रम",
|
|
493
494
|
"doctor.diagnostic.repoMap": "REPO_MAP.md",
|
package/dist/cli/i18n/ko.js
CHANGED
|
@@ -488,6 +488,7 @@ export const koMessages = {
|
|
|
488
488
|
"doctor.diagnostic.validation": "검증",
|
|
489
489
|
"doctor.diagnostic.skillRoutes": "스킬 라우팅",
|
|
490
490
|
"doctor.diagnostic.commands": "명령",
|
|
491
|
+
"doctor.diagnostic.environment": "실행 환경",
|
|
491
492
|
"doctor.diagnostic.readOrder": "읽기 순서",
|
|
492
493
|
"doctor.diagnostic.optionalReadOrder": "선택적 읽기 순서",
|
|
493
494
|
"doctor.diagnostic.repoMap": "REPO_MAP.md",
|
package/dist/cli/i18n/zh.js
CHANGED
|
@@ -488,6 +488,7 @@ export const zhMessages = {
|
|
|
488
488
|
"doctor.diagnostic.validation": "验证",
|
|
489
489
|
"doctor.diagnostic.skillRoutes": "技能路由",
|
|
490
490
|
"doctor.diagnostic.commands": "命令规范",
|
|
491
|
+
"doctor.diagnostic.environment": "环境",
|
|
491
492
|
"doctor.diagnostic.readOrder": "读取顺序",
|
|
492
493
|
"doctor.diagnostic.optionalReadOrder": "可选读取顺序",
|
|
493
494
|
"doctor.diagnostic.repoMap": "REPO_MAP.md",
|
|
@@ -1,103 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
|
|
3
|
+
export { ensureFileTargetInsideWithoutSymlinks, ensureInside, ensureInsideWithoutSymlinks, readFileInsideWithoutSymlinks, readUtf8FileInsideWithoutSymlinks, writeFileInsideWithoutSymlinks, writeUtf8FileInsideWithoutSymlinks, } from '../../core/safe-filesystem.js';
|
|
4
|
+
import { readFileInsideWithoutSymlinks, writeFileInsideWithoutSymlinks, } from '../../core/safe-filesystem.js';
|
|
4
5
|
export function toPosixPath(value) {
|
|
5
6
|
return value.split(path.sep).join('/');
|
|
6
7
|
}
|
|
7
|
-
export function ensureInside(parentPath, childPath) {
|
|
8
|
-
const parent = path.resolve(parentPath);
|
|
9
|
-
const child = path.resolve(childPath);
|
|
10
|
-
const relative = path.relative(parent, child);
|
|
11
|
-
if (relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))) {
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
throw new Error(`Path escapes allowed directory: ${childPath}`);
|
|
15
|
-
}
|
|
16
|
-
function isMissingPathError(error) {
|
|
17
|
-
return error instanceof Error && 'code' in error && error.code === 'ENOENT';
|
|
18
|
-
}
|
|
19
|
-
export function ensureInsideWithoutSymlinks(parentPath, childPath, options = {}) {
|
|
20
|
-
ensureInside(parentPath, childPath);
|
|
21
|
-
const parent = path.resolve(parentPath);
|
|
22
|
-
const child = path.resolve(childPath);
|
|
23
|
-
const relative = path.relative(parent, child);
|
|
24
|
-
const segments = relative === '' ? [] : relative.split(path.sep).filter((segment) => segment.length > 0);
|
|
25
|
-
let currentPath = parent;
|
|
26
|
-
const parentStats = lstatSync(parent);
|
|
27
|
-
if (parentStats.isSymbolicLink()) {
|
|
28
|
-
throw new Error(`Path must not contain symlinks: ${childPath}`);
|
|
29
|
-
}
|
|
30
|
-
for (const [index, segment] of segments.entries()) {
|
|
31
|
-
currentPath = path.join(currentPath, segment);
|
|
32
|
-
const isLeaf = index === segments.length - 1;
|
|
33
|
-
try {
|
|
34
|
-
const stats = lstatSync(currentPath);
|
|
35
|
-
if (stats.isSymbolicLink()) {
|
|
36
|
-
throw new Error(`Path must not contain symlinks: ${childPath}`);
|
|
37
|
-
}
|
|
38
|
-
if (!isLeaf && !stats.isDirectory()) {
|
|
39
|
-
throw new Error(`Path component is not a directory: ${currentPath}`);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
catch (error) {
|
|
43
|
-
if (isMissingPathError(error) && options.allowMissingLeaf) {
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
throw error;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
export function readUtf8FileInsideWithoutSymlinks(parentPath, childPath) {
|
|
51
|
-
return readFileInsideWithoutSymlinks(parentPath, childPath).toString('utf8');
|
|
52
|
-
}
|
|
53
|
-
export function readFileInsideWithoutSymlinks(parentPath, childPath) {
|
|
54
|
-
const absoluteChildPath = path.resolve(childPath);
|
|
55
|
-
ensureInsideWithoutSymlinks(parentPath, absoluteChildPath);
|
|
56
|
-
const fileDescriptor = openSync(absoluteChildPath, constants.O_RDONLY | NOFOLLOW_FLAG);
|
|
57
|
-
try {
|
|
58
|
-
return readFileSync(fileDescriptor);
|
|
59
|
-
}
|
|
60
|
-
finally {
|
|
61
|
-
closeSync(fileDescriptor);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
export function ensureFileTargetInsideWithoutSymlinks(parentPath, childPath, options = {}) {
|
|
65
|
-
const absoluteChildPath = path.resolve(childPath);
|
|
66
|
-
ensureInside(parentPath, absoluteChildPath);
|
|
67
|
-
ensureInsideWithoutSymlinks(parentPath, path.dirname(absoluteChildPath), { allowMissingLeaf: true });
|
|
68
|
-
try {
|
|
69
|
-
const stats = lstatSync(absoluteChildPath);
|
|
70
|
-
if (stats.isSymbolicLink()) {
|
|
71
|
-
throw new Error(`Path must not contain symlinks: ${childPath}`);
|
|
72
|
-
}
|
|
73
|
-
if (!stats.isFile()) {
|
|
74
|
-
throw new Error(`Path must be a regular file: ${childPath}`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
catch (error) {
|
|
78
|
-
if (isMissingPathError(error) && options.allowMissingLeaf) {
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
throw error;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
export function writeUtf8FileInsideWithoutSymlinks(parentPath, childPath, content) {
|
|
85
|
-
writeFileInsideWithoutSymlinks(parentPath, childPath, content);
|
|
86
|
-
}
|
|
87
|
-
export function writeFileInsideWithoutSymlinks(parentPath, childPath, content) {
|
|
88
|
-
const absoluteChildPath = path.resolve(childPath);
|
|
89
|
-
const directoryPath = path.dirname(absoluteChildPath);
|
|
90
|
-
ensureInsideWithoutSymlinks(parentPath, directoryPath, { allowMissingLeaf: true });
|
|
91
|
-
mkdirSync(directoryPath, { recursive: true });
|
|
92
|
-
ensureFileTargetInsideWithoutSymlinks(parentPath, absoluteChildPath, { allowMissingLeaf: true });
|
|
93
|
-
const fileDescriptor = openSync(absoluteChildPath, constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | NOFOLLOW_FLAG);
|
|
94
|
-
try {
|
|
95
|
-
writeFileSync(fileDescriptor, content);
|
|
96
|
-
}
|
|
97
|
-
finally {
|
|
98
|
-
closeSync(fileDescriptor);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
8
|
export function copyFileInsideWithoutSymlinks(sourceParentPath, sourcePath, targetParentPath, targetPath) {
|
|
102
9
|
const content = readFileInsideWithoutSymlinks(sourceParentPath, sourcePath);
|
|
103
10
|
writeFileInsideWithoutSymlinks(targetParentPath, targetPath, content);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { isRecord, readCommandContract, readString, readStringArray } from '../command-contract.js';
|
|
@@ -8,6 +8,7 @@ import { collectSourceAnchorIndexRecords, hasHighRiskSourceAnchorRiskTags, } fro
|
|
|
8
8
|
import { listSourceAnchorFiles } from '../../../core/source-anchors.js';
|
|
9
9
|
import { normalizeCommandEffects } from '../../../core/command-effects.js';
|
|
10
10
|
import { listChangeClassificationRuleDescriptors } from '../../../core/change-classification.js';
|
|
11
|
+
import { writeFileInsideWithoutSymlinks } from '../../../core/safe-filesystem.js';
|
|
11
12
|
import { DEFAULT_DATABASE_RELATIVE_PATH, DEFAULT_PROMPT_CACHE_STABLE_READ, DEFAULT_PROMPT_CACHE_TASK_SOURCES, DEFAULT_PROMPT_CACHE_VOLATILE_SOURCES, INDEX_CONFIG_RELATIVE_PATH, LOCAL_INDEX_CONTENT_MODE, LOCAL_INDEX_EXCLUDED_RAW_DATA_KINDS, LOCAL_INDEX_PARSER_VERSION, LOCAL_INDEX_SCHEMA_VERSION, LOCAL_INDEX_STORE_FULL_CONTENT, LATEST_RUN_STATE_RELATIVE_PATH, MAX_SEARCH_MATCH_SNIPPET_CHARS, MAX_SNIPPET_BYTES_PER_DOCUMENT, MUSTFLOW_RELATIVE_PATH, SEARCH_BACKEND_FTS5, SEARCH_BACKEND_TABLE_SCAN, SEARCH_MATCH_CONTEXT_AFTER_CHARS, SEARCH_MATCH_CONTEXT_BEFORE_CHARS, SEARCH_MATCH_TRUNCATION_MARKER, SEARCH_NGRAM_MAX_GRAMS_PER_TARGET, SEARCH_NGRAM_MAX_LENGTH, SEARCH_NGRAM_MAX_TOKEN_CHARS, SEARCH_NGRAM_MIN_LENGTH, SOURCE_INDEX_MAX_FILE_BYTES, TEST_DISABLE_FTS5_ENV, } from './constants.js';
|
|
12
13
|
import { loadSqlJs } from './sql.js';
|
|
13
14
|
export function getLocalIndexDatabasePath(projectRoot) {
|
|
@@ -2202,8 +2203,7 @@ export async function createLocalIndex(projectRoot, options = {}) {
|
|
|
2202
2203
|
const database = new SQL.Database();
|
|
2203
2204
|
createSchema(database, capabilities);
|
|
2204
2205
|
populateDatabase(database, capabilities, documents, skills, skillRoutes, commandIntents, sourceAnchors, indexedFiles, verificationEvidence, indexMode, sourceScopeHash, includeSource, new Date().toISOString());
|
|
2205
|
-
|
|
2206
|
-
writeFileSync(databasePath, database.export());
|
|
2206
|
+
writeFileInsideWithoutSymlinks(projectRoot, databasePath, database.export());
|
|
2207
2207
|
database.close();
|
|
2208
2208
|
}
|
|
2209
2209
|
return {
|
|
@@ -2245,7 +2245,7 @@ export async function createLocalIndex(projectRoot, options = {}) {
|
|
|
2245
2245
|
max_snippet_bytes_per_document: MAX_SNIPPET_BYTES_PER_DOCUMENT,
|
|
2246
2246
|
excluded_raw_data_kinds: [...LOCAL_INDEX_EXCLUDED_RAW_DATA_KINDS],
|
|
2247
2247
|
indexed_file_count: indexedFiles.length,
|
|
2248
|
-
indexed_paths:
|
|
2248
|
+
indexed_paths: indexedFiles.map((file) => file.path),
|
|
2249
2249
|
};
|
|
2250
2250
|
}
|
|
2251
2251
|
function readStoredSchemaVersion(database) {
|
package/dist/cli/lib/repo-map.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
-
import { existsSync, lstatSync, readdirSync, realpathSync, statSync
|
|
3
|
+
import { existsSync, lstatSync, readdirSync, realpathSync, statSync } from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { toPosixPath } from './filesystem.js';
|
|
6
|
+
import { writeUtf8FileInsideWithoutSymlinks } from '../../core/safe-filesystem.js';
|
|
6
7
|
import { readTomlFile } from './toml.js';
|
|
7
8
|
const DEFAULT_DEPTH = 3;
|
|
8
9
|
const REPO_MAP_DOC_ID = 'repo-map';
|
|
@@ -691,5 +692,5 @@ export function generateRepoMap(projectRoot, options = {}) {
|
|
|
691
692
|
].join('\n');
|
|
692
693
|
}
|
|
693
694
|
export function writeRepoMap(projectRoot, content) {
|
|
694
|
-
|
|
695
|
+
writeUtf8FileInsideWithoutSymlinks(projectRoot, path.join(projectRoot, 'REPO_MAP.md'), content);
|
|
695
696
|
}
|
package/dist/cli/lib/run-plan.js
CHANGED
|
@@ -4,7 +4,7 @@ import { resolveSafeProjectCwd } from '../../core/command-cwd.js';
|
|
|
4
4
|
import { resolveCommandEnv } from '../../core/command-env.js';
|
|
5
5
|
import { evaluateCommandIntentEligibility, } from '../../core/command-intent-eligibility.js';
|
|
6
6
|
import { isRecord, readPositiveInteger, readString, readStringArray, } from '../../core/config-loading.js';
|
|
7
|
-
import { DEFAULT_COMMAND_MAX_OUTPUT_BYTES, MAX_COMMAND_OUTPUT_BYTES, commandMaxOutputBytesLimitMessage, } from '../../core/command-output-limits.js';
|
|
7
|
+
import { DEFAULT_COMMAND_MAX_OUTPUT_BYTES, COMMAND_OUTPUT_LIMIT_SCOPE, MAX_COMMAND_OUTPUT_BYTES, commandMaxOutputBytesLimitMessage, } from '../../core/command-output-limits.js';
|
|
8
8
|
import { t } from './i18n.js';
|
|
9
9
|
function getSuccessExitCodes(intent) {
|
|
10
10
|
const value = intent.success_exit_codes;
|
|
@@ -78,8 +78,10 @@ function readEffectiveMaxOutputBytes(contract, intent) {
|
|
|
78
78
|
readPositiveInteger(contract.defaults, 'max_output_bytes') ??
|
|
79
79
|
DEFAULT_COMMAND_MAX_OUTPUT_BYTES;
|
|
80
80
|
}
|
|
81
|
-
function readEffectiveKillAfterSeconds(contract) {
|
|
82
|
-
return readPositiveInteger(
|
|
81
|
+
function readEffectiveKillAfterSeconds(contract, intent) {
|
|
82
|
+
return readPositiveInteger(intent, 'kill_after_seconds') ??
|
|
83
|
+
readPositiveInteger(contract.defaults, 'kill_after_seconds') ??
|
|
84
|
+
5;
|
|
83
85
|
}
|
|
84
86
|
function getMaxOutputBytesLimitDetail(contract, intent) {
|
|
85
87
|
const intentValue = readPositiveInteger(intent, 'max_output_bytes');
|
|
@@ -106,7 +108,7 @@ function readRunIntentMetadata(contract, intent) {
|
|
|
106
108
|
kind: readString(intent, 'kind') ?? null,
|
|
107
109
|
configuredCwd,
|
|
108
110
|
timeoutSeconds: readPositiveInteger(intent, 'timeout_seconds') ?? null,
|
|
109
|
-
killAfterSeconds: readEffectiveKillAfterSeconds(contract),
|
|
111
|
+
killAfterSeconds: readEffectiveKillAfterSeconds(contract, intent),
|
|
110
112
|
maxOutputBytes: readEffectiveMaxOutputBytes(contract, intent),
|
|
111
113
|
successExitCodes: getSuccessExitCodes(intent),
|
|
112
114
|
commandArgv,
|
|
@@ -284,7 +286,9 @@ export function createRunPreview(plan, previewMode) {
|
|
|
284
286
|
cwd: plan.relativeCwd,
|
|
285
287
|
resolved_cwd: plan.cwd,
|
|
286
288
|
timeout_seconds: plan.timeoutSeconds,
|
|
289
|
+
kill_after_seconds: plan.killAfterSeconds,
|
|
287
290
|
max_output_bytes: plan.maxOutputBytes,
|
|
291
|
+
max_output_bytes_scope: plan.maxOutputBytes === null ? null : COMMAND_OUTPUT_LIMIT_SCOPE,
|
|
288
292
|
mode: plan.mode,
|
|
289
293
|
argv: plan.commandArgv,
|
|
290
294
|
resolved_argv: plan.argvCommand,
|
|
@@ -4,7 +4,7 @@ const CHECK_ISSUE_ID_RULES = [
|
|
|
4
4
|
['mustflow.command_contract.configured_missing_lifecycle', /^Configured intent [^\s]+ must define lifecycle$/u],
|
|
5
5
|
['mustflow.command_contract.configured_missing_run_policy', /^Configured intent [^\s]+ must define run_policy$/u],
|
|
6
6
|
['mustflow.command_contract.oneshot_missing_timeout', /^Oneshot intent [^\s]+ must define timeout_seconds$/u],
|
|
7
|
-
['mustflow.command_contract.max_output_bytes_exceeds_limit', /^\[commands\.(?:defaults|intents\.[^\]]+)\]\.max_output_bytes must be less than or equal to \d
|
|
7
|
+
['mustflow.command_contract.max_output_bytes_exceeds_limit', /^\[commands\.(?:defaults|intents\.[^\]]+)\]\.max_output_bytes must be less than or equal to \d+ per output stream$/u],
|
|
8
8
|
['mustflow.command_contract.oneshot_stdin_not_closed', /^Oneshot intent [^\s]+ must set stdin = "closed"$/u],
|
|
9
9
|
['mustflow.command_contract.long_running_agent_allowed', /^Long-running intent [^\s]+ must not use run_policy = "agent_allowed"$/u],
|
|
10
10
|
['mustflow.command_contract.executable_source_missing', /^Configured intent [^\s]+ must define argv or mode = "shell" with cmd$/u],
|
|
@@ -161,6 +161,7 @@ function validateCommandIntent(intentName, intent, issues) {
|
|
|
161
161
|
validateAllowedStringField(intent, 'env_policy', `[commands.intents.${intentName}].env_policy`, COMMAND_ENV_POLICIES, issues);
|
|
162
162
|
validateStringArrayField(intent, 'env_allowlist', `[commands.intents.${intentName}].env_allowlist`, issues);
|
|
163
163
|
validateMaxOutputBytesField(intent, 'max_output_bytes', `[commands.intents.${intentName}].max_output_bytes`, issues);
|
|
164
|
+
validatePositiveIntegerField(intent, 'kill_after_seconds', `[commands.intents.${intentName}].kill_after_seconds`, issues);
|
|
164
165
|
validateCommandIntentSelection(intentName, intent, issues);
|
|
165
166
|
if (intent.status !== 'configured') {
|
|
166
167
|
return;
|
|
@@ -223,10 +224,10 @@ function getEffectiveCommandEnvPolicy(defaults, intent) {
|
|
|
223
224
|
}
|
|
224
225
|
return { policy: DEFAULT_COMMAND_ENV_POLICY, source: 'implicit' };
|
|
225
226
|
}
|
|
226
|
-
function
|
|
227
|
-
const
|
|
227
|
+
export function findCommandEnvInheritanceWarnings(commandsToml) {
|
|
228
|
+
const warnings = [];
|
|
228
229
|
if (!commandsToml || !isRecord(commandsToml.intents)) {
|
|
229
|
-
return
|
|
230
|
+
return warnings;
|
|
230
231
|
}
|
|
231
232
|
const defaults = isRecord(commandsToml.defaults) ? commandsToml.defaults : undefined;
|
|
232
233
|
for (const [intentName, intent] of Object.entries(commandsToml.intents)) {
|
|
@@ -237,13 +238,26 @@ function validateCommandEnvInheritanceWarnings(commandsToml) {
|
|
|
237
238
|
if (envPolicy.policy !== 'inherit') {
|
|
238
239
|
continue;
|
|
239
240
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
241
|
+
warnings.push({
|
|
242
|
+
intentName,
|
|
243
|
+
source: envPolicy.source,
|
|
244
|
+
network: intent.network === true,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
return warnings;
|
|
248
|
+
}
|
|
249
|
+
function formatCommandEnvInheritanceWarning(warning) {
|
|
250
|
+
const networkScope = warning.network ? ' with network = true' : '';
|
|
251
|
+
const migration = 'set env_policy = "minimal" or "allowlist" unless broad host state is required';
|
|
252
|
+
if (warning.source === 'implicit') {
|
|
253
|
+
return `configured agent-runnable intent ${warning.intentName} implicitly inherits the host environment${networkScope}; ${migration}`;
|
|
254
|
+
}
|
|
255
|
+
return `configured agent-runnable intent ${warning.intentName} uses env_policy = "inherit"${networkScope}; ${migration}`;
|
|
256
|
+
}
|
|
257
|
+
function validateCommandEnvInheritanceWarnings(commandsToml) {
|
|
258
|
+
const issues = [];
|
|
259
|
+
for (const warning of findCommandEnvInheritanceWarnings(commandsToml)) {
|
|
260
|
+
issues.push(commandContractWarning(formatCommandEnvInheritanceWarning(warning)));
|
|
247
261
|
}
|
|
248
262
|
return issues;
|
|
249
263
|
}
|
|
@@ -17,12 +17,11 @@ function validateEffectPath(projectRoot, intent, rawPath) {
|
|
|
17
17
|
const cwd = resolveSafeProjectCwd(projectRoot, readString(intent, 'cwd'));
|
|
18
18
|
const resolved = path.resolve(cwd, rawPath);
|
|
19
19
|
const root = path.resolve(projectRoot);
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
if (resolvedLower !== rootLower && !resolvedLower.startsWith(`${rootLower}${path.sep}`)) {
|
|
20
|
+
const relative = path.relative(root, resolved);
|
|
21
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
23
22
|
throw new Error(`Command effect path must stay inside the current root: ${rawPath}`);
|
|
24
23
|
}
|
|
25
|
-
return normalizeRelativePath(
|
|
24
|
+
return normalizeRelativePath(relative);
|
|
26
25
|
}
|
|
27
26
|
function readResourcePaths(commandContract, lock) {
|
|
28
27
|
const resource = commandContract.resources[lock];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export const DEFAULT_COMMAND_MAX_OUTPUT_BYTES = 1_048_576;
|
|
2
2
|
export const MAX_COMMAND_OUTPUT_BYTES = 16 * 1024 * 1024;
|
|
3
|
+
export const COMMAND_OUTPUT_LIMIT_SCOPE = 'per_stream';
|
|
3
4
|
export function commandMaxOutputBytesLimitMessage(label) {
|
|
4
|
-
return `${label} must be less than or equal to ${MAX_COMMAND_OUTPUT_BYTES}`;
|
|
5
|
+
return `${label} must be less than or equal to ${MAX_COMMAND_OUTPUT_BYTES} per output stream`;
|
|
5
6
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import { existsSync
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { readFileInsideWithoutSymlinks, writeFileInsideWithoutSymlinks } from './safe-filesystem.js';
|
|
4
5
|
const GITATTRIBUTES_PATH = '.gitattributes';
|
|
5
6
|
function toPosixPath(value) {
|
|
6
7
|
return value.split(path.sep).join('/');
|
|
@@ -10,7 +11,7 @@ function hasLfPolicy(projectRoot) {
|
|
|
10
11
|
if (!existsSync(attributesPath)) {
|
|
11
12
|
return false;
|
|
12
13
|
}
|
|
13
|
-
const content =
|
|
14
|
+
const content = readFileInsideWithoutSymlinks(projectRoot, attributesPath).toString('utf8');
|
|
14
15
|
return /^\*\s+.*(?:^|\s)eol=lf(?:\s|$)/imu.test(content);
|
|
15
16
|
}
|
|
16
17
|
function gitList(projectRoot, args) {
|
|
@@ -107,14 +108,21 @@ export function inspectLineEndings(projectRoot, mode, options = {}) {
|
|
|
107
108
|
if (!existsSync(absolutePath)) {
|
|
108
109
|
continue;
|
|
109
110
|
}
|
|
110
|
-
|
|
111
|
+
let buffer;
|
|
112
|
+
try {
|
|
113
|
+
buffer = readFileInsideWithoutSymlinks(root, absolutePath);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
issues.push(error instanceof Error ? error.message : String(error));
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
111
119
|
const lineEnding = detectLineEnding(buffer);
|
|
112
120
|
const wouldChange = policy === 'lf' && (lineEnding === 'crlf' || lineEnding === 'mixed');
|
|
113
121
|
if (!wouldChange) {
|
|
114
122
|
continue;
|
|
115
123
|
}
|
|
116
124
|
if (canApply) {
|
|
117
|
-
|
|
125
|
+
writeFileInsideWithoutSymlinks(root, absolutePath, normalizeLf(buffer));
|
|
118
126
|
changedFiles.push(toPosixPath(relativePath));
|
|
119
127
|
}
|
|
120
128
|
nonCompliantFiles.push({
|