mustflow 2.16.0 → 2.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/cli/commands/classify.js +13 -3
- package/dist/cli/commands/dashboard.js +2 -1
- package/dist/cli/commands/impact.js +13 -3
- package/dist/cli/commands/run.js +86 -11
- package/dist/cli/commands/upgrade.js +3 -1
- package/dist/cli/commands/verify.js +9 -1
- package/dist/cli/commands/version.js +1 -1
- package/dist/cli/i18n/en.js +8 -1
- package/dist/cli/i18n/es.js +8 -1
- package/dist/cli/i18n/fr.js +8 -1
- package/dist/cli/i18n/hi.js +8 -1
- package/dist/cli/i18n/ko.js +7 -0
- package/dist/cli/i18n/zh.js +7 -0
- package/dist/cli/lib/git-changes.js +25 -2
- package/dist/cli/lib/local-index/constants.js +4 -1
- package/dist/cli/lib/local-index/index.js +22 -5
- package/dist/cli/lib/npm-version-check.js +71 -1
- package/dist/cli/lib/repo-map.js +81 -28
- package/dist/cli/lib/run-plan.js +25 -2
- package/dist/cli/lib/validation/index.js +2 -1
- package/dist/core/check-issues.js +2 -0
- package/dist/core/command-contract-rules.js +104 -2
- package/dist/core/command-contract-validation.js +14 -2
- package/dist/core/command-intent-eligibility.js +9 -1
- package/dist/core/command-output-limits.js +5 -0
- package/dist/core/contract-lint.js +10 -1
- package/package.json +1 -1
- package/schemas/README.md +3 -3
- package/schemas/change-verification-report.schema.json +2 -1
- package/schemas/contract-lint-report.schema.json +2 -1
- package/schemas/explain-report.schema.json +1 -0
- package/schemas/latest-run-pointer.schema.json +1 -0
- package/schemas/verify-report.schema.json +1 -0
- package/schemas/verify-run-manifest.schema.json +1 -0
- package/templates/default/manifest.toml +1 -1
package/dist/cli/i18n/zh.js
CHANGED
|
@@ -664,8 +664,12 @@ export const zhMessages = {
|
|
|
664
664
|
"run.error.unsafeIntentDetail": "请使用 shell 安全的意图名称。",
|
|
665
665
|
"run.error.blockedShellBackground": '意图 "{intent}" 已被阻止。{detail}',
|
|
666
666
|
"run.error.blockedShellBackgroundDetail": "Shell 命令不得启动后台工作。",
|
|
667
|
+
"run.error.blockedLongRunningCommand": '意图 "{intent}" 已被阻止。{detail}',
|
|
668
|
+
"run.error.blockedLongRunningCommandDetail": "argv 必须描述会结束的单次命令,而不是开发服务器、监听命令、shell 包装器、解释器循环或后台进程。",
|
|
667
669
|
"run.error.cwdOutsideProject": '命令 "{intent}" 的 cwd 无效:{detail}',
|
|
668
670
|
"run.error.cwdOutsideProjectDetail": "意图 cwd 必须位于当前根目录内。",
|
|
671
|
+
"run.error.maxOutputBytes": '命令 "{intent}" 的 max_output_bytes 无效。{detail}',
|
|
672
|
+
"run.error.maxOutputBytesDetail": "输出限制必须保持在允许的最大值内。",
|
|
669
673
|
"run.error.conflictingPreviewModes": "只能使用 --dry-run 或 --plan-only,不能同时使用",
|
|
670
674
|
"run.error.timedOut": '命令 "{intent}" 在 {seconds} 秒后超时',
|
|
671
675
|
"run.error.startFailed": '命令 "{intent}" 启动失败:{message}',
|
|
@@ -727,6 +731,7 @@ export const zhMessages = {
|
|
|
727
731
|
"classify.source.changed": "变更文件",
|
|
728
732
|
"classify.source.paths": "指定路径",
|
|
729
733
|
"classify.error.missingInput": "请指定 --changed 或至少一个路径",
|
|
734
|
+
"classify.error.changed_files_unavailable": "无法通过 git status 检查变更文件",
|
|
730
735
|
"classify.error.write_path_outside_root": "分类报告路径必须位于 mustflow 根目录内",
|
|
731
736
|
"impact.help.summary": "在不修改文件的情况下报告变更路径是否需要包或模板版本决策。",
|
|
732
737
|
"impact.help.option.changed": "从 git status --short --untracked-files=all 读取路径",
|
|
@@ -741,6 +746,7 @@ export const zhMessages = {
|
|
|
741
746
|
"impact.label.affectedVersionSources": "受影响的版本来源",
|
|
742
747
|
"impact.label.affectedSurfaces": "受影响的公开表面",
|
|
743
748
|
"impact.error.missingInput": "请指定 --changed 或至少一个路径",
|
|
749
|
+
"impact.error.changed_files_unavailable": "无法通过 git status 检查变更文件",
|
|
744
750
|
"verify.help.summary": "运行由 required_after 元数据选出的已配置验证意图。",
|
|
745
751
|
"verify.help.option.reason": "选择要验证的 required_after 原因",
|
|
746
752
|
"verify.help.option.fromClassification": "从此仓库内的 mf classify 报告读取验证原因",
|
|
@@ -768,6 +774,7 @@ export const zhMessages = {
|
|
|
768
774
|
"verify.error.plan_root_mismatch": "分类报告必须来自当前 mustflow 根目录",
|
|
769
775
|
"verify.error.missing_plan_reasons": "分类报告必须包含 summary.validationReasons",
|
|
770
776
|
"verify.error.plan_path_outside_root": "分类报告路径必须位于 mustflow 根目录内",
|
|
777
|
+
"verify.error.changed_files_unavailable": "无法通过 git status 检查变更文件",
|
|
771
778
|
"verify.error.invalid_repro_evidence_file": "复现证据必须是包含结构化证据字段的可读取 JSON 摘要",
|
|
772
779
|
"verify.error.unsupported_repro_evidence_source": "复现证据输入必须使用 command repro-evidence",
|
|
773
780
|
"verify.error.invalid_external_evidence_file": "外部证据必须是包含 checks 的可读取 JSON 摘要",
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import { parseGitStatusOutput } from '../../core/change-classification.js';
|
|
3
|
+
export class GitChangedFilesError extends Error {
|
|
4
|
+
result;
|
|
5
|
+
constructor(result) {
|
|
6
|
+
super('git_changed_files_unavailable');
|
|
7
|
+
this.name = 'GitChangedFilesError';
|
|
8
|
+
this.result = result;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
3
11
|
export function readGitChangedFiles(projectRoot) {
|
|
4
12
|
const result = spawnSync('git', ['status', '--short', '--untracked-files=all'], {
|
|
5
13
|
cwd: projectRoot,
|
|
@@ -7,7 +15,22 @@ export function readGitChangedFiles(projectRoot) {
|
|
|
7
15
|
windowsHide: true,
|
|
8
16
|
});
|
|
9
17
|
if (result.status !== 0 || typeof result.stdout !== 'string') {
|
|
10
|
-
|
|
18
|
+
const stderr = typeof result.stderr === 'string' ? result.stderr.trim() : '';
|
|
19
|
+
const message = result.error?.message ??
|
|
20
|
+
(stderr || (result.status === null ? 'git status did not complete' : `git status exited with code ${result.status}`));
|
|
21
|
+
return {
|
|
22
|
+
ok: false,
|
|
23
|
+
message,
|
|
24
|
+
status: result.status,
|
|
25
|
+
stderr,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return { ok: true, files: parseGitStatusOutput(result.stdout) };
|
|
29
|
+
}
|
|
30
|
+
export function requireGitChangedFiles(projectRoot) {
|
|
31
|
+
const result = readGitChangedFiles(projectRoot);
|
|
32
|
+
if (!result.ok) {
|
|
33
|
+
throw new GitChangedFilesError(result);
|
|
11
34
|
}
|
|
12
|
-
return
|
|
35
|
+
return result.files;
|
|
13
36
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const LOCAL_INDEX_SCHEMA_VERSION = '
|
|
1
|
+
export const LOCAL_INDEX_SCHEMA_VERSION = '20';
|
|
2
2
|
export const LOCAL_INDEX_PARSER_VERSION = '1';
|
|
3
3
|
export const DEFAULT_DATABASE_RELATIVE_PATH = '.mustflow/cache/mustflow.sqlite';
|
|
4
4
|
export const LATEST_RUN_STATE_RELATIVE_PATH = '.mustflow/state/runs/latest.json';
|
|
@@ -22,6 +22,9 @@ export const SEARCH_MATCH_CONTEXT_AFTER_CHARS = 96;
|
|
|
22
22
|
export const SEARCH_MATCH_TRUNCATION_MARKER = '...';
|
|
23
23
|
export const SEARCH_NGRAM_MIN_LENGTH = 2;
|
|
24
24
|
export const SEARCH_NGRAM_MAX_LENGTH = 3;
|
|
25
|
+
export const SEARCH_NGRAM_MAX_TOKEN_CHARS = 64;
|
|
26
|
+
export const SEARCH_NGRAM_MAX_GRAMS_PER_TARGET = 512;
|
|
27
|
+
export const SOURCE_INDEX_MAX_FILE_BYTES = 262144;
|
|
25
28
|
export const SEARCH_BACKEND_FTS5 = 'fts5';
|
|
26
29
|
export const SEARCH_BACKEND_TABLE_SCAN = 'table_scan';
|
|
27
30
|
export const TEST_DISABLE_FTS5_ENV = 'MUSTFLOW_TEST_DISABLE_FTS5';
|
|
@@ -7,7 +7,7 @@ import { readTomlFile } from '../toml.js';
|
|
|
7
7
|
import { collectSourceAnchorIndexRecords, hasHighRiskSourceAnchorRiskTags, } from '../../../core/source-anchor-status.js';
|
|
8
8
|
import { normalizeCommandEffects } from '../../../core/command-effects.js';
|
|
9
9
|
import { listChangeClassificationRuleDescriptors } from '../../../core/change-classification.js';
|
|
10
|
-
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_LENGTH, SEARCH_NGRAM_MIN_LENGTH, TEST_DISABLE_FTS5_ENV, } from './constants.js';
|
|
10
|
+
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';
|
|
11
11
|
import { loadSqlJs } from './sql.js';
|
|
12
12
|
export function getLocalIndexDatabasePath(projectRoot) {
|
|
13
13
|
return path.join(projectRoot, ...DEFAULT_DATABASE_RELATIVE_PATH.split('/'));
|
|
@@ -83,11 +83,12 @@ function readPositiveInteger(table, key) {
|
|
|
83
83
|
}
|
|
84
84
|
function readLocalIndexSourceConfig(projectRoot) {
|
|
85
85
|
const sourceIndexTable = readNestedTable(readIndexToml(projectRoot), 'source_index');
|
|
86
|
+
const configuredMaxFileBytes = readPositiveInteger(sourceIndexTable, 'max_file_bytes');
|
|
86
87
|
return {
|
|
87
88
|
enabledByDefault: readBoolean(sourceIndexTable, 'enabled_by_default') === true,
|
|
88
89
|
include: readOptionalStringArray(sourceIndexTable, 'include') ?? [],
|
|
89
90
|
exclude: readOptionalStringArray(sourceIndexTable, 'exclude') ?? [],
|
|
90
|
-
maxFileBytes:
|
|
91
|
+
maxFileBytes: Math.min(configuredMaxFileBytes ?? SOURCE_INDEX_MAX_FILE_BYTES, SOURCE_INDEX_MAX_FILE_BYTES),
|
|
91
92
|
allowedExtensions: readOptionalStringArray(sourceIndexTable, 'allowed_extensions') ?? [],
|
|
92
93
|
};
|
|
93
94
|
}
|
|
@@ -321,10 +322,14 @@ function buildSearchNgrams(values) {
|
|
|
321
322
|
const grams = new Set();
|
|
322
323
|
for (const value of values) {
|
|
323
324
|
for (const token of extractSearchTokens(value)) {
|
|
324
|
-
const
|
|
325
|
+
const boundedToken = token.slice(0, SEARCH_NGRAM_MAX_TOKEN_CHARS);
|
|
326
|
+
const maxLength = Math.min(SEARCH_NGRAM_MAX_LENGTH, boundedToken.length);
|
|
325
327
|
for (let length = SEARCH_NGRAM_MIN_LENGTH; length <= maxLength; length += 1) {
|
|
326
|
-
for (let index = 0; index <=
|
|
327
|
-
grams.add(
|
|
328
|
+
for (let index = 0; index <= boundedToken.length - length; index += 1) {
|
|
329
|
+
grams.add(boundedToken.slice(index, index + length));
|
|
330
|
+
if (grams.size >= SEARCH_NGRAM_MAX_GRAMS_PER_TARGET) {
|
|
331
|
+
return [...grams].sort((left, right) => left.localeCompare(right));
|
|
332
|
+
}
|
|
328
333
|
}
|
|
329
334
|
}
|
|
330
335
|
}
|
|
@@ -1650,6 +1655,18 @@ function populateDatabase(database, capabilities, documents, skills, skillRoutes
|
|
|
1650
1655
|
'max_snippet_bytes_per_document',
|
|
1651
1656
|
String(MAX_SNIPPET_BYTES_PER_DOCUMENT),
|
|
1652
1657
|
]);
|
|
1658
|
+
database.run('INSERT INTO metadata (key, value) VALUES (?, ?)', [
|
|
1659
|
+
'search_ngram_max_token_chars',
|
|
1660
|
+
String(SEARCH_NGRAM_MAX_TOKEN_CHARS),
|
|
1661
|
+
]);
|
|
1662
|
+
database.run('INSERT INTO metadata (key, value) VALUES (?, ?)', [
|
|
1663
|
+
'search_ngram_max_grams_per_target',
|
|
1664
|
+
String(SEARCH_NGRAM_MAX_GRAMS_PER_TARGET),
|
|
1665
|
+
]);
|
|
1666
|
+
database.run('INSERT INTO metadata (key, value) VALUES (?, ?)', [
|
|
1667
|
+
'source_index_max_file_bytes',
|
|
1668
|
+
String(SOURCE_INDEX_MAX_FILE_BYTES),
|
|
1669
|
+
]);
|
|
1653
1670
|
database.run('INSERT INTO metadata (key, value) VALUES (?, ?)', [
|
|
1654
1671
|
'excluded_raw_data_kinds',
|
|
1655
1672
|
LOCAL_INDEX_EXCLUDED_RAW_DATA_KINDS.join(','),
|
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
const DEFAULT_NPM_REGISTRY_URL = 'https://registry.npmjs.org';
|
|
2
2
|
const DEFAULT_VERSION_CHECK_TIMEOUT_MS = 3_000;
|
|
3
|
+
const PACKAGE_MANAGER_COMMANDS = [
|
|
4
|
+
{
|
|
5
|
+
id: 'npm',
|
|
6
|
+
label: 'npm',
|
|
7
|
+
command(packageName) {
|
|
8
|
+
return `npm install -g ${packageName}@latest`;
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
id: 'bun',
|
|
13
|
+
label: 'bun',
|
|
14
|
+
command(packageName) {
|
|
15
|
+
return `bun add -g ${packageName}@latest`;
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'pnpm',
|
|
20
|
+
label: 'pnpm',
|
|
21
|
+
command(packageName) {
|
|
22
|
+
return `pnpm add -g ${packageName}@latest`;
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'yarn',
|
|
27
|
+
label: 'yarn',
|
|
28
|
+
command(packageName) {
|
|
29
|
+
return `yarn global add ${packageName}@latest`;
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'deno',
|
|
34
|
+
label: 'deno',
|
|
35
|
+
command(packageName) {
|
|
36
|
+
return `deno install -g -A -n mf npm:${packageName}@latest`;
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
];
|
|
3
40
|
function isRecord(value) {
|
|
4
41
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
5
42
|
}
|
|
@@ -65,6 +102,37 @@ function getTimeoutMs() {
|
|
|
65
102
|
const parsed = rawValue ? Number(rawValue) : DEFAULT_VERSION_CHECK_TIMEOUT_MS;
|
|
66
103
|
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : DEFAULT_VERSION_CHECK_TIMEOUT_MS;
|
|
67
104
|
}
|
|
105
|
+
function detectPackageManagerId() {
|
|
106
|
+
const signals = [
|
|
107
|
+
process.env.npm_config_user_agent,
|
|
108
|
+
process.env.npm_execpath,
|
|
109
|
+
process.execPath,
|
|
110
|
+
process.argv[1],
|
|
111
|
+
import.meta.url,
|
|
112
|
+
]
|
|
113
|
+
.filter((signal) => typeof signal === 'string' && signal.length > 0)
|
|
114
|
+
.map((signal) => signal.toLowerCase());
|
|
115
|
+
for (const id of ['bun', 'pnpm', 'yarn', 'deno', 'npm']) {
|
|
116
|
+
if (signals.some((signal) => signal.includes(id))) {
|
|
117
|
+
return id;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
function getPackageInstallCommands(packageName) {
|
|
123
|
+
const detectedId = detectPackageManagerId();
|
|
124
|
+
const commands = [...PACKAGE_MANAGER_COMMANDS];
|
|
125
|
+
const recommendedIndex = detectedId ? commands.findIndex((entry) => entry.id === detectedId) : -1;
|
|
126
|
+
if (recommendedIndex > 0) {
|
|
127
|
+
const [recommended] = commands.splice(recommendedIndex, 1);
|
|
128
|
+
commands.unshift(recommended);
|
|
129
|
+
}
|
|
130
|
+
return commands.map((entry, index) => ({
|
|
131
|
+
manager: entry.label,
|
|
132
|
+
command: entry.command(packageName),
|
|
133
|
+
recommended: index === 0 && detectedId === entry.id,
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
68
136
|
function buildLatestPackageUrl(registryUrl, packageName) {
|
|
69
137
|
const trimmedRegistryUrl = registryUrl.replace(/\/+$/u, '');
|
|
70
138
|
const encodedPackageName = packageName.startsWith('@')
|
|
@@ -86,12 +154,14 @@ export async function checkNpmLatestVersion(metadata) {
|
|
|
86
154
|
if (!latestVersion) {
|
|
87
155
|
throw new Error('npm registry response did not include a version');
|
|
88
156
|
}
|
|
157
|
+
const updateCommands = getPackageInstallCommands(metadata.name);
|
|
89
158
|
return {
|
|
90
159
|
packageName: metadata.name,
|
|
91
160
|
currentVersion: metadata.version,
|
|
92
161
|
latestVersion,
|
|
93
162
|
updateAvailable: comparePackageVersions(metadata.version, latestVersion) < 0,
|
|
94
163
|
registryUrl,
|
|
95
|
-
updateCommand: `npm install -g ${metadata.name}@latest`,
|
|
164
|
+
updateCommand: updateCommands[0]?.command ?? `npm install -g ${metadata.name}@latest`,
|
|
165
|
+
updateCommands,
|
|
96
166
|
};
|
|
97
167
|
}
|
package/dist/cli/lib/repo-map.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
-
import { existsSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { existsSync, lstatSync, readdirSync, realpathSync, statSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import {
|
|
5
|
+
import { toPosixPath } from './filesystem.js';
|
|
6
6
|
import { readTomlFile } from './toml.js';
|
|
7
7
|
const DEFAULT_DEPTH = 3;
|
|
8
8
|
const REPO_MAP_DOC_ID = 'repo-map';
|
|
@@ -241,7 +241,7 @@ function getRepoMapConfig(projectRoot) {
|
|
|
241
241
|
};
|
|
242
242
|
}
|
|
243
243
|
function getGitFiles(projectRoot) {
|
|
244
|
-
const result = spawnSync('git', ['ls-files'], {
|
|
244
|
+
const result = spawnSync('git', ['ls-files', '-z'], {
|
|
245
245
|
cwd: projectRoot,
|
|
246
246
|
encoding: 'utf8',
|
|
247
247
|
});
|
|
@@ -249,16 +249,43 @@ function getGitFiles(projectRoot) {
|
|
|
249
249
|
return [];
|
|
250
250
|
}
|
|
251
251
|
return result.stdout
|
|
252
|
-
.split(
|
|
253
|
-
.map((line) => line
|
|
252
|
+
.split('\0')
|
|
253
|
+
.map((line) => toPosixPath(line))
|
|
254
254
|
.filter(Boolean);
|
|
255
255
|
}
|
|
256
|
-
function
|
|
256
|
+
function isAnchorCandidatePath(relativePath, priorityPaths) {
|
|
257
|
+
return priorityPaths.has(relativePath) || Boolean(getAnchorDescription(relativePath));
|
|
258
|
+
}
|
|
259
|
+
function listAnchorCandidateFilesRecursive(rootPath, depth, priorityPaths) {
|
|
260
|
+
const results = [];
|
|
261
|
+
function visit(currentPath, directoryDepth) {
|
|
262
|
+
for (const entry of readdirSync(currentPath, { withFileTypes: true })) {
|
|
263
|
+
const entryPath = path.join(currentPath, entry.name);
|
|
264
|
+
const relativePath = toPosixPath(path.relative(rootPath, entryPath));
|
|
265
|
+
if (entry.isDirectory()) {
|
|
266
|
+
if (EXCLUDED_SEGMENTS.has(entry.name) || directoryDepth >= depth) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
visit(entryPath, directoryDepth + 1);
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (entry.isFile() && isAnchorCandidatePath(relativePath, priorityPaths)) {
|
|
273
|
+
results.push(relativePath);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (!existsSync(rootPath) || !statSync(rootPath).isDirectory()) {
|
|
278
|
+
return [];
|
|
279
|
+
}
|
|
280
|
+
visit(rootPath, 0);
|
|
281
|
+
return results.sort();
|
|
282
|
+
}
|
|
283
|
+
function getRepositoryFiles(projectRoot, depth, priorityPaths) {
|
|
257
284
|
const files = new Set();
|
|
258
285
|
for (const relativePath of getGitFiles(projectRoot)) {
|
|
259
286
|
files.add(relativePath);
|
|
260
287
|
}
|
|
261
|
-
for (const relativePath of
|
|
288
|
+
for (const relativePath of listAnchorCandidateFilesRecursive(projectRoot, depth, priorityPaths)) {
|
|
262
289
|
files.add(relativePath);
|
|
263
290
|
}
|
|
264
291
|
return Array.from(files);
|
|
@@ -306,7 +333,7 @@ function isUnderExcludedPrefix(relativePath, excludedPrefixes) {
|
|
|
306
333
|
return excludedPrefixes.some((prefix) => relativePath === prefix.slice(0, -1) || relativePath.startsWith(prefix));
|
|
307
334
|
}
|
|
308
335
|
function discoverAnchors(projectRoot, depth, priorityPaths, nestedRepositories, excludedPrefixes) {
|
|
309
|
-
return getRepositoryFiles(projectRoot)
|
|
336
|
+
return getRepositoryFiles(projectRoot, depth, priorityPaths)
|
|
310
337
|
.filter(shouldIncludePath)
|
|
311
338
|
.filter((relativePath) => !isUnderNestedRepository(relativePath, nestedRepositories))
|
|
312
339
|
.filter((relativePath) => !isUnderExcludedPrefix(relativePath, excludedPrefixes))
|
|
@@ -350,13 +377,9 @@ function renderDirectoryAnchors(anchors) {
|
|
|
350
377
|
function hasGitMarker(directoryPath) {
|
|
351
378
|
return existsSync(path.join(directoryPath, '.git'));
|
|
352
379
|
}
|
|
353
|
-
function
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
357
|
-
catch {
|
|
358
|
-
return false;
|
|
359
|
-
}
|
|
380
|
+
function isRealPathInside(parentRealPath, childRealPath) {
|
|
381
|
+
const relative = path.relative(parentRealPath, childRealPath);
|
|
382
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
360
383
|
}
|
|
361
384
|
function isSafeWorkspaceRoot(projectRoot, workspaceRoot) {
|
|
362
385
|
const absoluteRoot = path.resolve(projectRoot, workspaceRoot);
|
|
@@ -371,6 +394,32 @@ function getWorkspaceRootPrefixes(projectRoot, workspaceConfig) {
|
|
|
371
394
|
.filter((workspaceRoot) => isSafeWorkspaceRoot(projectRoot, workspaceRoot))
|
|
372
395
|
.map((workspaceRoot) => `${toPosixPath(workspaceRoot).replace(/\/+$/, '')}/`);
|
|
373
396
|
}
|
|
397
|
+
function resolveSafeDirectoryTarget(projectRootRealPath, logicalPath, followSymlinks) {
|
|
398
|
+
try {
|
|
399
|
+
const stats = lstatSync(logicalPath);
|
|
400
|
+
if (stats.isSymbolicLink()) {
|
|
401
|
+
if (!followSymlinks) {
|
|
402
|
+
return undefined;
|
|
403
|
+
}
|
|
404
|
+
const realPath = realpathSync(logicalPath);
|
|
405
|
+
if (!isRealPathInside(projectRootRealPath, realPath) || !statSync(realPath).isDirectory()) {
|
|
406
|
+
return undefined;
|
|
407
|
+
}
|
|
408
|
+
return { logicalPath, realPath };
|
|
409
|
+
}
|
|
410
|
+
if (!stats.isDirectory()) {
|
|
411
|
+
return undefined;
|
|
412
|
+
}
|
|
413
|
+
const realPath = realpathSync(logicalPath);
|
|
414
|
+
if (!isRealPathInside(projectRootRealPath, realPath)) {
|
|
415
|
+
return undefined;
|
|
416
|
+
}
|
|
417
|
+
return { logicalPath, realPath };
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
return undefined;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
374
423
|
function collectNestedRepository(projectRoot, repositoryPath, anchorFiles) {
|
|
375
424
|
const relativeRoot = `${toPosixPath(path.relative(projectRoot, repositoryPath))}/`;
|
|
376
425
|
const existingAnchors = new Set();
|
|
@@ -422,31 +471,34 @@ function discoverNestedRepositories(projectRoot, mapConfig, workspaceConfig) {
|
|
|
422
471
|
}
|
|
423
472
|
const repositories = [];
|
|
424
473
|
const seenRepositoryPaths = new Set();
|
|
425
|
-
|
|
474
|
+
const seenDirectoryPaths = new Set();
|
|
475
|
+
const projectRootRealPath = realpathSync(projectRoot);
|
|
476
|
+
function visit(directoryTarget, depth) {
|
|
426
477
|
if (repositories.length >= workspaceConfig.maxRepositories || depth > workspaceConfig.maxDepth) {
|
|
427
478
|
return;
|
|
428
479
|
}
|
|
429
|
-
if (
|
|
430
|
-
|
|
480
|
+
if (seenDirectoryPaths.has(directoryTarget.realPath)) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
seenDirectoryPaths.add(directoryTarget.realPath);
|
|
484
|
+
if (hasGitMarker(directoryTarget.logicalPath)) {
|
|
485
|
+
const resolvedRepositoryPath = directoryTarget.realPath;
|
|
431
486
|
if (!seenRepositoryPaths.has(resolvedRepositoryPath)) {
|
|
432
487
|
seenRepositoryPaths.add(resolvedRepositoryPath);
|
|
433
|
-
repositories.push(collectNestedRepository(projectRoot,
|
|
488
|
+
repositories.push(collectNestedRepository(projectRoot, directoryTarget.logicalPath, mapConfig.anchorFiles));
|
|
434
489
|
}
|
|
435
490
|
if (workspaceConfig.stopAtRepositoryRoot) {
|
|
436
491
|
return;
|
|
437
492
|
}
|
|
438
493
|
}
|
|
439
|
-
for (const entry of readdirSync(
|
|
440
|
-
if (!entry.isDirectory()) {
|
|
441
|
-
continue;
|
|
442
|
-
}
|
|
494
|
+
for (const entry of readdirSync(directoryTarget.logicalPath, { withFileTypes: true })) {
|
|
443
495
|
if (EXCLUDED_SEGMENTS.has(entry.name)) {
|
|
444
496
|
continue;
|
|
445
497
|
}
|
|
446
|
-
|
|
447
|
-
|
|
498
|
+
const childDirectoryTarget = resolveSafeDirectoryTarget(projectRootRealPath, path.join(directoryTarget.logicalPath, entry.name), workspaceConfig.followSymlinks);
|
|
499
|
+
if (childDirectoryTarget) {
|
|
500
|
+
visit(childDirectoryTarget, depth + 1);
|
|
448
501
|
}
|
|
449
|
-
visit(path.join(directoryPath, entry.name), depth + 1);
|
|
450
502
|
}
|
|
451
503
|
}
|
|
452
504
|
for (const workspaceRoot of workspaceConfig.roots) {
|
|
@@ -454,10 +506,11 @@ function discoverNestedRepositories(projectRoot, mapConfig, workspaceConfig) {
|
|
|
454
506
|
continue;
|
|
455
507
|
}
|
|
456
508
|
const absoluteWorkspaceRoot = path.resolve(projectRoot, workspaceRoot);
|
|
457
|
-
|
|
509
|
+
const workspaceTarget = resolveSafeDirectoryTarget(projectRootRealPath, absoluteWorkspaceRoot, workspaceConfig.followSymlinks);
|
|
510
|
+
if (!workspaceTarget) {
|
|
458
511
|
continue;
|
|
459
512
|
}
|
|
460
|
-
visit(
|
|
513
|
+
visit(workspaceTarget, 0);
|
|
461
514
|
}
|
|
462
515
|
return repositories.sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
463
516
|
}
|
package/dist/cli/lib/run-plan.js
CHANGED
|
@@ -4,6 +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
8
|
import { t } from './i18n.js';
|
|
8
9
|
function getSuccessExitCodes(intent) {
|
|
9
10
|
const value = intent.success_exit_codes;
|
|
@@ -72,6 +73,24 @@ function getRunPlanMode(commandArgv, intent) {
|
|
|
72
73
|
}
|
|
73
74
|
return intent.mode === 'shell' ? 'shell' : null;
|
|
74
75
|
}
|
|
76
|
+
function readEffectiveMaxOutputBytes(contract, intent) {
|
|
77
|
+
return readPositiveInteger(intent, 'max_output_bytes') ??
|
|
78
|
+
readPositiveInteger(contract.defaults, 'max_output_bytes') ??
|
|
79
|
+
DEFAULT_COMMAND_MAX_OUTPUT_BYTES;
|
|
80
|
+
}
|
|
81
|
+
function getMaxOutputBytesLimitDetail(contract, intent) {
|
|
82
|
+
const intentValue = readPositiveInteger(intent, 'max_output_bytes');
|
|
83
|
+
if (intentValue !== undefined) {
|
|
84
|
+
return intentValue > MAX_COMMAND_OUTPUT_BYTES ?
|
|
85
|
+
commandMaxOutputBytesLimitMessage('[commands.intents.<intent>].max_output_bytes') :
|
|
86
|
+
null;
|
|
87
|
+
}
|
|
88
|
+
const defaultValue = readPositiveInteger(contract.defaults, 'max_output_bytes');
|
|
89
|
+
if (defaultValue !== undefined && defaultValue > MAX_COMMAND_OUTPUT_BYTES) {
|
|
90
|
+
return commandMaxOutputBytesLimitMessage('[commands.defaults].max_output_bytes');
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
75
94
|
function readRunIntentMetadata(contract, intent) {
|
|
76
95
|
const configuredCwd = readString(intent, 'cwd') ?? readString(contract.defaults, 'default_cwd') ?? '.';
|
|
77
96
|
const commandArgv = readStringArray(intent, 'argv');
|
|
@@ -84,7 +103,7 @@ function readRunIntentMetadata(contract, intent) {
|
|
|
84
103
|
kind: readString(intent, 'kind') ?? null,
|
|
85
104
|
configuredCwd,
|
|
86
105
|
timeoutSeconds: readPositiveInteger(intent, 'timeout_seconds') ?? null,
|
|
87
|
-
maxOutputBytes:
|
|
106
|
+
maxOutputBytes: readEffectiveMaxOutputBytes(contract, intent),
|
|
88
107
|
successExitCodes: getSuccessExitCodes(intent),
|
|
89
108
|
commandArgv,
|
|
90
109
|
shellCommand,
|
|
@@ -149,6 +168,10 @@ export function createRunPlan(projectRoot, contract, intentName, options = {}) {
|
|
|
149
168
|
return createBlockedRunPlan(contract, intentName, rawIntent, eligibility, eligibility.code, eligibility.detail);
|
|
150
169
|
}
|
|
151
170
|
const metadata = readRunIntentMetadata(contract, rawIntent);
|
|
171
|
+
const maxOutputBytesLimitDetail = getMaxOutputBytesLimitDetail(contract, rawIntent);
|
|
172
|
+
if (maxOutputBytesLimitDetail) {
|
|
173
|
+
return createBlockedRunPlan(contract, intentName, rawIntent, eligibility, 'max_output_bytes_exceeds_limit', maxOutputBytesLimitDetail);
|
|
174
|
+
}
|
|
152
175
|
let cwd;
|
|
153
176
|
try {
|
|
154
177
|
cwd = resolveSafeProjectCwd(projectRoot, metadata.configuredCwd);
|
|
@@ -207,7 +230,7 @@ function createSuggestedIntentSnippet(intentName, metadata, reasonCode) {
|
|
|
207
230
|
return null;
|
|
208
231
|
}
|
|
209
232
|
let commandLines;
|
|
210
|
-
if (reasonCode === 'blocked_shell_background_pattern') {
|
|
233
|
+
if (reasonCode === 'blocked_shell_background_pattern' || reasonCode === 'blocked_long_running_command_pattern') {
|
|
211
234
|
commandLines = [`argv = ${formatTomlStringArray(['TODO_REPLACE_WITH_FINITE_COMMAND'])}`];
|
|
212
235
|
}
|
|
213
236
|
else if (metadata?.shellCommand) {
|
|
@@ -605,7 +605,8 @@ function validateStrictVersionSources(projectRoot, preferencesToml, versioningTo
|
|
|
605
605
|
pushStrictIssue(issues, '[release.versioning] is enabled but no version source was detected; add .mustflow/config/versioning.toml or a package/template version source');
|
|
606
606
|
}
|
|
607
607
|
function validateStrictTemplateVersionSync(projectRoot, preferencesToml, issues) {
|
|
608
|
-
const
|
|
608
|
+
const changedPathResult = existsSync(path.join(projectRoot, '.git')) ? readGitChangedFiles(projectRoot) : undefined;
|
|
609
|
+
const changedPaths = changedPathResult?.ok ? changedPathResult.files : undefined;
|
|
609
610
|
for (const issue of validateTemplateVersionSync(projectRoot, preferencesToml, changedPaths)) {
|
|
610
611
|
if (issue.severity === 'warning') {
|
|
611
612
|
pushStrictWarning(issues, issue.message);
|
|
@@ -4,10 +4,12 @@ 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+$/u],
|
|
7
8
|
['mustflow.command_contract.oneshot_stdin_not_closed', /^Oneshot intent [^\s]+ must set stdin = "closed"$/u],
|
|
8
9
|
['mustflow.command_contract.long_running_agent_allowed', /^Long-running intent [^\s]+ must not use run_policy = "agent_allowed"$/u],
|
|
9
10
|
['mustflow.command_contract.executable_source_missing', /^Configured intent [^\s]+ must define argv or mode = "shell" with cmd$/u],
|
|
10
11
|
['mustflow.command_contract.shell_background_pattern', /^Shell intent [^\s]+ contains a blocked long-running or background pattern$/u],
|
|
12
|
+
['mustflow.command_contract.long_running_command_pattern', /^Intent [^\s]+ contains a blocked long-running or background command pattern$/u],
|
|
11
13
|
['mustflow.command_contract.success_exit_codes_invalid', /^\[commands\.intents\.[^\]]+\]\.success_exit_codes must be an integer array$/u],
|
|
12
14
|
['mustflow.command_contract.effects_invalid', /^(?:Strict: )?(?:\[commands\.(?:resources|intents\.[^\]]+\.effects)[^\]]*\]|Command effect for intent [^\s]+ must define path, paths, or lock)/u],
|
|
13
15
|
['mustflow.command_contract.effect_path_escape', /^Strict: Command effect path must stay inside the current root:/u],
|
|
@@ -1,16 +1,39 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
1
2
|
import { readString, readStringArray } from './config-loading.js';
|
|
2
3
|
const SAFE_COMMAND_INTENT_NAME_PATTERN = /^[A-Za-z0-9_-]+$/u;
|
|
4
|
+
const SHELL_WRAPPER_COMMANDS = new Set(['sh', 'bash', 'zsh', 'dash', 'ksh', 'cmd', 'powershell', 'pwsh']);
|
|
5
|
+
const SHELL_EVALUATION_FLAGS = new Set(['-c', '/c', '-command', '-commandwithargs']);
|
|
6
|
+
const INTERPRETER_EVALUATION_FLAGS = new Map([
|
|
7
|
+
['node', new Set(['-e', '--eval'])],
|
|
8
|
+
['python', new Set(['-c'])],
|
|
9
|
+
['python3', new Set(['-c'])],
|
|
10
|
+
['py', new Set(['-c'])],
|
|
11
|
+
['ruby', new Set(['-e'])],
|
|
12
|
+
['perl', new Set(['-e'])],
|
|
13
|
+
]);
|
|
14
|
+
const PACKAGE_SCRIPT_RUNNERS = new Set(['bun', 'npm', 'pnpm', 'yarn']);
|
|
15
|
+
const LONG_RUNNING_PACKAGE_SCRIPTS = new Set(['dev', 'start', 'serve', 'watch', 'preview']);
|
|
16
|
+
const LONG_RUNNING_EXECUTABLES = new Set(['nodemon', 'pm2', 'serve', 'http-server', 'live-server', 'webpack-dev-server']);
|
|
3
17
|
export const BACKGROUND_SHELL_PATTERNS = [
|
|
4
|
-
|
|
18
|
+
/(?:^|[^&])&(?!&)\s*$/u,
|
|
5
19
|
/\bnohup\b/iu,
|
|
6
20
|
/\bdisown\b/iu,
|
|
7
21
|
/\bStart-Process\b/iu,
|
|
8
|
-
|
|
22
|
+
/(?:^|[;&|]\s*)start\s+/iu,
|
|
9
23
|
/\bxdg-open\b/iu,
|
|
10
24
|
/\bopen\s+/iu,
|
|
11
25
|
/\bchrome(?:\.exe)?\b/iu,
|
|
12
26
|
/\bchromium(?:\.exe)?\b/iu,
|
|
13
27
|
];
|
|
28
|
+
export const LONG_RUNNING_COMMAND_TEXT_PATTERNS = [
|
|
29
|
+
/\b(?:npm|pnpm|bun|yarn)\s+(?:run\s+)?(?:dev|start|serve|watch|preview)\b/iu,
|
|
30
|
+
/\b(?:nohup|disown)\b/iu,
|
|
31
|
+
/(?:^|[^&])&(?!&)\s*$/u,
|
|
32
|
+
/\bsetInterval\s*\(/u,
|
|
33
|
+
/\bwhile\s*(?:\(\s*true\s*\)|true)\b/iu,
|
|
34
|
+
/\bserve_forever\s*\(/iu,
|
|
35
|
+
/\bcreateServer\s*\([^)]*\)\s*\.listen\s*\(/u,
|
|
36
|
+
];
|
|
14
37
|
export function commandIntentNameIsSafe(intentName) {
|
|
15
38
|
return SAFE_COMMAND_INTENT_NAME_PATTERN.test(intentName);
|
|
16
39
|
}
|
|
@@ -25,3 +48,82 @@ export function shellCommandHasBlockedBackgroundPattern(command) {
|
|
|
25
48
|
export function commandIntentHasBlockedShellBackgroundPattern(intent) {
|
|
26
49
|
return intent.mode === 'shell' && typeof intent.cmd === 'string' && shellCommandHasBlockedBackgroundPattern(intent.cmd);
|
|
27
50
|
}
|
|
51
|
+
function normalizeExecutableName(value) {
|
|
52
|
+
return path.basename(value).replace(/\.(?:cmd|exe|ps1)$/iu, '').toLowerCase();
|
|
53
|
+
}
|
|
54
|
+
function findFlagPayload(argv, flags) {
|
|
55
|
+
for (let index = 1; index < argv.length - 1; index += 1) {
|
|
56
|
+
if (flags.has(argv[index].toLowerCase())) {
|
|
57
|
+
return argv[index + 1];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
function commandTextHasLongRunningPattern(command) {
|
|
63
|
+
return LONG_RUNNING_COMMAND_TEXT_PATTERNS.some((pattern) => pattern.test(command));
|
|
64
|
+
}
|
|
65
|
+
function readPackageScriptName(command, args) {
|
|
66
|
+
if (!PACKAGE_SCRIPT_RUNNERS.has(command)) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
if (args[0] === 'run' && args[1] && !args[1].startsWith('-')) {
|
|
70
|
+
return args[1];
|
|
71
|
+
}
|
|
72
|
+
if (args[0] && LONG_RUNNING_PACKAGE_SCRIPTS.has(args[0])) {
|
|
73
|
+
return args[0];
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
function argvHasBlockedLongRunningPattern(argv) {
|
|
78
|
+
const [rawCommand = '', ...args] = argv;
|
|
79
|
+
const command = normalizeExecutableName(rawCommand);
|
|
80
|
+
const shellPayload = SHELL_WRAPPER_COMMANDS.has(command) ? findFlagPayload(argv, SHELL_EVALUATION_FLAGS) : null;
|
|
81
|
+
if (shellPayload && (shellCommandHasBlockedBackgroundPattern(shellPayload) || commandTextHasLongRunningPattern(shellPayload))) {
|
|
82
|
+
return `shell wrapper payload contains a blocked long-running or background pattern: ${shellPayload}`;
|
|
83
|
+
}
|
|
84
|
+
const interpreterFlags = INTERPRETER_EVALUATION_FLAGS.get(command);
|
|
85
|
+
const interpreterPayload = interpreterFlags ? findFlagPayload(argv, interpreterFlags) : null;
|
|
86
|
+
if (interpreterPayload && commandTextHasLongRunningPattern(interpreterPayload)) {
|
|
87
|
+
return `interpreter evaluation payload contains a blocked long-running pattern: ${interpreterPayload}`;
|
|
88
|
+
}
|
|
89
|
+
const packageScriptName = readPackageScriptName(command, args);
|
|
90
|
+
if (packageScriptName && LONG_RUNNING_PACKAGE_SCRIPTS.has(packageScriptName)) {
|
|
91
|
+
return `package-manager script "${packageScriptName}" is commonly long-running`;
|
|
92
|
+
}
|
|
93
|
+
if (LONG_RUNNING_EXECUTABLES.has(command)) {
|
|
94
|
+
return `executable "${command}" is commonly long-running`;
|
|
95
|
+
}
|
|
96
|
+
if (command === 'vite' && !args.includes('build')) {
|
|
97
|
+
return 'vite without build is commonly a development server';
|
|
98
|
+
}
|
|
99
|
+
if (command === 'next' && ['dev', 'start'].includes(args[0] ?? '')) {
|
|
100
|
+
return `next ${args[0]} is commonly long-running`;
|
|
101
|
+
}
|
|
102
|
+
if (command === 'webpack' && (args.includes('--watch') || args.includes('-w') || args.includes('serve'))) {
|
|
103
|
+
return 'webpack watch or serve mode is commonly long-running';
|
|
104
|
+
}
|
|
105
|
+
if (command === 'tsc' && (args.includes('--watch') || args.includes('-w'))) {
|
|
106
|
+
return 'tsc watch mode is long-running';
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
export function commandIntentBlockedCommandPattern(intent) {
|
|
111
|
+
if (intent.mode === 'shell' && typeof intent.cmd === 'string' && shellCommandHasBlockedBackgroundPattern(intent.cmd)) {
|
|
112
|
+
return {
|
|
113
|
+
code: 'shell_background_pattern',
|
|
114
|
+
detail: 'Shell command contains a blocked long-running or background pattern.',
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const argv = readStringArray(intent, 'argv');
|
|
118
|
+
if (!argv) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
const detail = argvHasBlockedLongRunningPattern(argv);
|
|
122
|
+
if (!detail) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
code: 'long_running_command_pattern',
|
|
127
|
+
detail: `Argv command contains a blocked long-running or background pattern: ${detail}.`,
|
|
128
|
+
};
|
|
129
|
+
}
|