@guilz-dev/belay 0.9.1 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/adapters/codex/runtime-entry.d.ts +3 -0
- package/dist/adapters/codex/runtime-entry.js +27 -4
- package/dist/adapters/cursor/cwd-resolution.d.ts +10 -0
- package/dist/adapters/cursor/cwd-resolution.js +58 -0
- package/dist/adapters/cursor/hooks.d.ts +5 -3
- package/dist/adapters/cursor/hooks.js +29 -20
- package/dist/adapters/cursor/runtime-entry.d.ts +1 -0
- package/dist/adapters/cursor/runtime-entry.js +107 -6
- package/dist/adapters/shared/gate-runtime.js +26 -3
- package/dist/adapters/shared/repo-root.js +20 -1
- package/dist/bundle/claude-runtime.mjs +1225 -287
- package/dist/bundle/codex-runtime.mjs +1247 -291
- package/dist/bundle/cursor-runtime.mjs +4320 -3154
- package/dist/cli.js +33 -3
- package/dist/commands/doctor.js +38 -9
- package/dist/commands/health-snapshot.d.ts +3 -0
- package/dist/commands/health-snapshot.js +56 -0
- package/dist/commands/report.js +14 -0
- package/dist/commands/status.js +15 -0
- package/dist/commands/where.d.ts +4 -0
- package/dist/commands/where.js +52 -0
- package/dist/core/approval-repo-lookup.d.ts +16 -0
- package/dist/core/approval-repo-lookup.js +48 -0
- package/dist/core/audit-io.d.ts +1 -1
- package/dist/core/audit-io.js +1 -1
- package/dist/core/audit-legacy-archive.d.ts +1 -0
- package/dist/core/audit-legacy-archive.js +5 -0
- package/dist/core/audit-query.d.ts +1 -0
- package/dist/core/audit-query.js +7 -0
- package/dist/core/audit-serialize.d.ts +3 -0
- package/dist/core/audit-serialize.js +39 -3
- package/dist/core/audit-summary.d.ts +9 -0
- package/dist/core/audit-summary.js +58 -1
- package/dist/core/audit-types.d.ts +4 -0
- package/dist/core/effect-ir/shell-lower.js +283 -27
- package/dist/core/replay-scrub.d.ts +1 -0
- package/dist/core/replay-scrub.js +22 -3
- package/dist/core/shell-tokenizer.d.ts +28 -0
- package/dist/core/shell-tokenizer.js +111 -29
- package/dist/core/verdict/docker-compose-run.d.ts +18 -0
- package/dist/core/verdict/docker-compose-run.js +136 -0
- package/dist/core/verdict/launcher-resolve.js +66 -25
- package/dist/core/verdict/makefile-expand.d.ts +4 -0
- package/dist/core/verdict/makefile-expand.js +151 -0
- package/dist/core/verdict/parser.d.ts +4 -0
- package/dist/core/verdict/parser.js +25 -30
- package/dist/core/verdict/recursive-invocation.d.ts +20 -0
- package/dist/core/verdict/recursive-invocation.js +224 -0
- package/dist/corpus/benign-probe-cores.d.ts +1 -1
- package/dist/corpus/benign-probe-cores.js +2 -0
- package/dist/defaults.js +16 -0
- package/dist/installer/scope-config.d.ts +2 -2
- package/dist/installer.d.ts +10 -1
- package/dist/installer.js +43 -2
- package/dist/types.d.ts +34 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -2
- package/skills/belay/SKILL.md +5 -0
- package/skills/belay/belay-report.md +4 -1
|
@@ -67,61 +67,143 @@ export function isRedirectOperator(token) {
|
|
|
67
67
|
export function isFdDuplication(token) {
|
|
68
68
|
return FD_DUPLICATION_PATTERN.test(token);
|
|
69
69
|
}
|
|
70
|
-
export function
|
|
70
|
+
export function lexShell(input) {
|
|
71
71
|
const tokens = [];
|
|
72
|
-
let
|
|
72
|
+
let value = '';
|
|
73
|
+
let wordStart = null;
|
|
74
|
+
let parts = [];
|
|
73
75
|
let quote = null;
|
|
74
|
-
let
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
let quoteStart = -1;
|
|
77
|
+
let quoteHadContent = false;
|
|
78
|
+
let complete = true;
|
|
79
|
+
const startWord = (index) => {
|
|
80
|
+
wordStart ??= index;
|
|
81
|
+
};
|
|
82
|
+
const append = (decoded, start, end, mode, hasExpansion) => {
|
|
83
|
+
startWord(start);
|
|
84
|
+
value += decoded;
|
|
85
|
+
const previous = parts.at(-1);
|
|
86
|
+
if (previous &&
|
|
87
|
+
previous.quote === mode &&
|
|
88
|
+
previous.hasExpansion === hasExpansion &&
|
|
89
|
+
previous.end === start) {
|
|
90
|
+
previous.value += decoded;
|
|
91
|
+
previous.raw += input.slice(start, end);
|
|
92
|
+
previous.end = end;
|
|
93
|
+
return;
|
|
79
94
|
}
|
|
95
|
+
parts.push({
|
|
96
|
+
value: decoded,
|
|
97
|
+
raw: input.slice(start, end),
|
|
98
|
+
start,
|
|
99
|
+
end,
|
|
100
|
+
quote: mode,
|
|
101
|
+
hasExpansion,
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
const flushWord = (end) => {
|
|
105
|
+
if (wordStart === null)
|
|
106
|
+
return;
|
|
107
|
+
tokens.push({
|
|
108
|
+
kind: 'word',
|
|
109
|
+
value,
|
|
110
|
+
raw: input.slice(wordStart, end),
|
|
111
|
+
start: wordStart,
|
|
112
|
+
end,
|
|
113
|
+
parts,
|
|
114
|
+
});
|
|
115
|
+
value = '';
|
|
116
|
+
wordStart = null;
|
|
117
|
+
parts = [];
|
|
118
|
+
};
|
|
119
|
+
const pushOperator = (token, start, end) => {
|
|
120
|
+
tokens.push({ kind: 'operator', value: token, raw: input.slice(start, end), start, end });
|
|
80
121
|
};
|
|
81
122
|
for (let index = 0; index < input.length; index += 1) {
|
|
82
|
-
const char = input[index];
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
123
|
+
const char = input[index] ?? '';
|
|
124
|
+
if (quote === 'single') {
|
|
125
|
+
if (char === "'") {
|
|
126
|
+
if (!quoteHadContent)
|
|
127
|
+
append('', quoteStart, index + 1, 'single', false);
|
|
128
|
+
quote = null;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
append(char, index, index + 1, 'single', false);
|
|
132
|
+
quoteHadContent = true;
|
|
133
|
+
}
|
|
90
134
|
continue;
|
|
91
135
|
}
|
|
92
|
-
if (quote) {
|
|
93
|
-
if (char ===
|
|
136
|
+
if (quote === 'double') {
|
|
137
|
+
if (char === '"') {
|
|
138
|
+
if (!quoteHadContent)
|
|
139
|
+
append('', quoteStart, index + 1, 'double', false);
|
|
94
140
|
quote = null;
|
|
141
|
+
continue;
|
|
95
142
|
}
|
|
96
|
-
|
|
97
|
-
|
|
143
|
+
if (char === '\\') {
|
|
144
|
+
const next = input[index + 1];
|
|
145
|
+
if (next === undefined) {
|
|
146
|
+
append('\\', index, index + 1, 'double', false);
|
|
147
|
+
complete = false;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (next === '$' || next === '`' || next === '"' || next === '\\' || next === '\n') {
|
|
151
|
+
append(next === '\n' ? '' : next, index, index + 2, 'double', false);
|
|
152
|
+
quoteHadContent = true;
|
|
153
|
+
index += 1;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
append('\\', index, index + 1, 'double', false);
|
|
157
|
+
quoteHadContent = true;
|
|
158
|
+
continue;
|
|
98
159
|
}
|
|
160
|
+
append(char, index, index + 1, 'double', char === '$' || char === '`');
|
|
161
|
+
quoteHadContent = true;
|
|
99
162
|
continue;
|
|
100
163
|
}
|
|
101
|
-
if (char === '"
|
|
102
|
-
|
|
164
|
+
if (char === "'" || char === '"') {
|
|
165
|
+
startWord(index);
|
|
166
|
+
quote = char === "'" ? 'single' : 'double';
|
|
167
|
+
quoteStart = index;
|
|
168
|
+
quoteHadContent = false;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (char === '\\') {
|
|
172
|
+
const next = input[index + 1];
|
|
173
|
+
if (next === undefined) {
|
|
174
|
+
append('\\', index, index + 1, 'unquoted', false);
|
|
175
|
+
complete = false;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
append(next, index, index + 2, 'unquoted', false);
|
|
179
|
+
index += 1;
|
|
103
180
|
continue;
|
|
104
181
|
}
|
|
105
182
|
const operator = readShellOperator(input, index);
|
|
106
183
|
if (operator) {
|
|
107
|
-
|
|
108
|
-
|
|
184
|
+
flushWord(index);
|
|
185
|
+
pushOperator(operator.token, index, index + operator.length);
|
|
109
186
|
index += operator.length - 1;
|
|
110
187
|
continue;
|
|
111
188
|
}
|
|
112
189
|
if (char === '\n' || char === '\r') {
|
|
113
|
-
|
|
114
|
-
|
|
190
|
+
flushWord(index);
|
|
191
|
+
pushOperator(';', index, index + 1);
|
|
115
192
|
continue;
|
|
116
193
|
}
|
|
117
194
|
if (/\s/.test(char)) {
|
|
118
|
-
|
|
195
|
+
flushWord(index);
|
|
119
196
|
continue;
|
|
120
197
|
}
|
|
121
|
-
|
|
198
|
+
append(char, index, index + 1, 'unquoted', char === '$' || char === '`');
|
|
122
199
|
}
|
|
123
|
-
|
|
124
|
-
|
|
200
|
+
if (quote !== null)
|
|
201
|
+
complete = false;
|
|
202
|
+
flushWord(input.length);
|
|
203
|
+
return { tokens, complete };
|
|
204
|
+
}
|
|
205
|
+
export function tokenizeShell(input) {
|
|
206
|
+
return lexShell(input).tokens.map((token) => token.value);
|
|
125
207
|
}
|
|
126
208
|
export function normalizeShellCommand(command, repoRoot, normalizeToken) {
|
|
127
209
|
const tokens = tokenizeShell(command);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ShellToken } from '../shell-tokenizer.js';
|
|
2
|
+
export type DockerComposeRunInvocation = {
|
|
3
|
+
kind: 'recursive';
|
|
4
|
+
service: string;
|
|
5
|
+
interpreter: string;
|
|
6
|
+
script: string;
|
|
7
|
+
} | {
|
|
8
|
+
kind: 'dynamic';
|
|
9
|
+
service: string;
|
|
10
|
+
signal: string;
|
|
11
|
+
} | {
|
|
12
|
+
kind: 'none';
|
|
13
|
+
} | {
|
|
14
|
+
kind: 'indeterminate';
|
|
15
|
+
signal: 'shell.compose_argv_indeterminate';
|
|
16
|
+
};
|
|
17
|
+
export declare function decodeDockerComposeRun(tokens: readonly ShellToken[]): DockerComposeRunInvocation;
|
|
18
|
+
export declare function decodeDockerComposeRunValues(values: readonly string[]): DockerComposeRunInvocation;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { decodeRecursiveInvocation, shellTokensFromValues } from './recursive-invocation.js';
|
|
3
|
+
const COMPOSE_GLOBAL_OPTIONS = new Map([
|
|
4
|
+
['--all-resources', 0],
|
|
5
|
+
['--ansi', 1],
|
|
6
|
+
['--compatibility', 0],
|
|
7
|
+
['--dry-run', 0],
|
|
8
|
+
['--env-file', 1],
|
|
9
|
+
['-f', 1],
|
|
10
|
+
['--file', 1],
|
|
11
|
+
['--parallel', 1],
|
|
12
|
+
['--profile', 1],
|
|
13
|
+
['--progress', 1],
|
|
14
|
+
['--project-directory', 1],
|
|
15
|
+
['-p', 1],
|
|
16
|
+
['--project-name', 1],
|
|
17
|
+
]);
|
|
18
|
+
const COMPOSE_RUN_OPTIONS = new Map([
|
|
19
|
+
['--build', 0],
|
|
20
|
+
['--cap-add', 1],
|
|
21
|
+
['--cap-drop', 1],
|
|
22
|
+
['-d', 0],
|
|
23
|
+
['--detach', 0],
|
|
24
|
+
['--entrypoint', 1],
|
|
25
|
+
['-e', 1],
|
|
26
|
+
['--env', 1],
|
|
27
|
+
['--env-from-file', 1],
|
|
28
|
+
['-i', 0],
|
|
29
|
+
['--interactive', 0],
|
|
30
|
+
['-l', 1],
|
|
31
|
+
['--label', 1],
|
|
32
|
+
['--name', 1],
|
|
33
|
+
['--no-deps', 0],
|
|
34
|
+
['-T', 0],
|
|
35
|
+
['--no-tty', 0],
|
|
36
|
+
['-p', 1],
|
|
37
|
+
['--publish', 1],
|
|
38
|
+
['--pull', 1],
|
|
39
|
+
['-q', 0],
|
|
40
|
+
['--quiet', 0],
|
|
41
|
+
['--quiet-build', 0],
|
|
42
|
+
['--quiet-pull', 0],
|
|
43
|
+
['--remove-orphans', 0],
|
|
44
|
+
['--rm', 0],
|
|
45
|
+
['-P', 0],
|
|
46
|
+
['--service-ports', 0],
|
|
47
|
+
['--use-aliases', 0],
|
|
48
|
+
['-u', 1],
|
|
49
|
+
['--user', 1],
|
|
50
|
+
['-v', 1],
|
|
51
|
+
['--volume', 1],
|
|
52
|
+
['-w', 1],
|
|
53
|
+
['--workdir', 1],
|
|
54
|
+
]);
|
|
55
|
+
function parseOptions(words, start, options) {
|
|
56
|
+
let index = start;
|
|
57
|
+
while (index < words.length) {
|
|
58
|
+
const value = words[index]?.value ?? '';
|
|
59
|
+
if (value === '--')
|
|
60
|
+
return { kind: 'ok', index: index + 1 };
|
|
61
|
+
if (!value.startsWith('-') || value === '-')
|
|
62
|
+
return { kind: 'ok', index };
|
|
63
|
+
const equalsIndex = value.indexOf('=');
|
|
64
|
+
const name = equalsIndex === -1 ? value : value.slice(0, equalsIndex);
|
|
65
|
+
const arity = options.get(name);
|
|
66
|
+
if (arity === undefined)
|
|
67
|
+
return { kind: 'indeterminate' };
|
|
68
|
+
if (equalsIndex !== -1) {
|
|
69
|
+
if (!value.startsWith('--') || arity !== 1 || equalsIndex === value.length - 1) {
|
|
70
|
+
return { kind: 'indeterminate' };
|
|
71
|
+
}
|
|
72
|
+
index += 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (arity === 1) {
|
|
76
|
+
if (!words[index + 1])
|
|
77
|
+
return { kind: 'indeterminate' };
|
|
78
|
+
index += 2;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
index += 1;
|
|
82
|
+
}
|
|
83
|
+
return { kind: 'ok', index };
|
|
84
|
+
}
|
|
85
|
+
export function decodeDockerComposeRun(tokens) {
|
|
86
|
+
if (tokens.some((token) => token.kind === 'operator'))
|
|
87
|
+
return { kind: 'none' };
|
|
88
|
+
const words = tokens.filter((token) => token.kind === 'word');
|
|
89
|
+
const head = path.basename(words[0]?.value ?? '');
|
|
90
|
+
let index;
|
|
91
|
+
if (head === 'docker-compose') {
|
|
92
|
+
index = 1;
|
|
93
|
+
}
|
|
94
|
+
else if (head === 'docker' && words[1]?.value === 'compose') {
|
|
95
|
+
index = 2;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
return { kind: 'none' };
|
|
99
|
+
}
|
|
100
|
+
const globalOptions = parseOptions(words, index, COMPOSE_GLOBAL_OPTIONS);
|
|
101
|
+
if (globalOptions.kind === 'indeterminate') {
|
|
102
|
+
return { kind: 'indeterminate', signal: 'shell.compose_argv_indeterminate' };
|
|
103
|
+
}
|
|
104
|
+
index = globalOptions.index;
|
|
105
|
+
if (words[index]?.value !== 'run')
|
|
106
|
+
return { kind: 'none' };
|
|
107
|
+
const runOptions = parseOptions(words, index + 1, COMPOSE_RUN_OPTIONS);
|
|
108
|
+
if (runOptions.kind === 'indeterminate') {
|
|
109
|
+
return { kind: 'indeterminate', signal: 'shell.compose_argv_indeterminate' };
|
|
110
|
+
}
|
|
111
|
+
index = runOptions.index;
|
|
112
|
+
const service = words[index]?.value;
|
|
113
|
+
if (!service)
|
|
114
|
+
return { kind: 'indeterminate', signal: 'shell.compose_argv_indeterminate' };
|
|
115
|
+
const command = words.slice(index + 1);
|
|
116
|
+
if (command.length === 0)
|
|
117
|
+
return { kind: 'none' };
|
|
118
|
+
const recursive = decodeRecursiveInvocation(command);
|
|
119
|
+
if (recursive.kind === 'static') {
|
|
120
|
+
return {
|
|
121
|
+
kind: 'recursive',
|
|
122
|
+
service,
|
|
123
|
+
interpreter: recursive.interpreter,
|
|
124
|
+
script: recursive.script,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (recursive.kind === 'dynamic')
|
|
128
|
+
return { kind: 'dynamic', service, signal: recursive.signal };
|
|
129
|
+
if (recursive.kind === 'indeterminate') {
|
|
130
|
+
return { kind: 'indeterminate', signal: 'shell.compose_argv_indeterminate' };
|
|
131
|
+
}
|
|
132
|
+
return { kind: 'none' };
|
|
133
|
+
}
|
|
134
|
+
export function decodeDockerComposeRunValues(values) {
|
|
135
|
+
return decodeDockerComposeRun(shellTokensFromValues(values, { detectExpansion: false }));
|
|
136
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { expandMakeExpression, normalizeMakeRecipeLine, parseMakefileVariables, } from './makefile-expand.js';
|
|
3
4
|
const MAX_RESOLVE_DEPTH = 8;
|
|
4
5
|
const PNPM_BUILTIN_COMMANDS = new Set([
|
|
5
6
|
'add',
|
|
@@ -145,10 +146,9 @@ function resolveNpmRecipe(cwd, repoRoot, scriptName, extraArgs) {
|
|
|
145
146
|
reason: 'npm_script_resolved',
|
|
146
147
|
};
|
|
147
148
|
}
|
|
148
|
-
function
|
|
149
|
+
function parseMakefileRecipeContent(content) {
|
|
149
150
|
const targets = new Map();
|
|
150
151
|
try {
|
|
151
|
-
const content = readFileSync(makefilePath, 'utf8');
|
|
152
152
|
const lines = content.split('\n');
|
|
153
153
|
let currentTarget = null;
|
|
154
154
|
let recipeLines = [];
|
|
@@ -206,7 +206,7 @@ function parseMakefileRecipes(makefilePath) {
|
|
|
206
206
|
}
|
|
207
207
|
return targets;
|
|
208
208
|
}
|
|
209
|
-
function resolveMakeRecipe(cwd, repoRoot, target) {
|
|
209
|
+
function resolveMakeRecipe(cwd, repoRoot, target, cliVars = {}) {
|
|
210
210
|
const candidates = ['Makefile', 'makefile', 'GNUmakefile'];
|
|
211
211
|
let makefilePath = null;
|
|
212
212
|
let searchDir = path.resolve(cwd);
|
|
@@ -227,57 +227,75 @@ function resolveMakeRecipe(cwd, repoRoot, target) {
|
|
|
227
227
|
if (!makefilePath) {
|
|
228
228
|
return { recipes: [], opaque: true, reason: 'unknown_local_effect' };
|
|
229
229
|
}
|
|
230
|
-
const
|
|
230
|
+
const makefileContent = readFileSync(makefilePath, 'utf8');
|
|
231
|
+
const makefileVars = parseMakefileVariables(makefileContent);
|
|
232
|
+
const targets = parseMakefileRecipeContent(makefileContent);
|
|
231
233
|
if (!targets.has(target)) {
|
|
232
234
|
return { recipes: [], opaque: true, reason: 'make_target_undefined' };
|
|
233
235
|
}
|
|
234
236
|
const recipeLines = [];
|
|
235
237
|
const visiting = new Set();
|
|
236
238
|
const visited = new Set();
|
|
237
|
-
let
|
|
239
|
+
let hasDynamicPrerequisite = false;
|
|
240
|
+
let hasUndefinedPrerequisite = false;
|
|
241
|
+
let hasDependencyCycle = false;
|
|
238
242
|
const collect = (name) => {
|
|
239
243
|
if (visited.has(name)) {
|
|
240
|
-
return
|
|
244
|
+
return;
|
|
241
245
|
}
|
|
242
246
|
if (visiting.has(name)) {
|
|
243
|
-
|
|
247
|
+
hasDependencyCycle = true;
|
|
248
|
+
return;
|
|
244
249
|
}
|
|
245
250
|
const entry = targets.get(name);
|
|
246
251
|
if (!entry) {
|
|
247
|
-
|
|
252
|
+
if (!existsSync(path.resolve(path.dirname(makefilePath), name))) {
|
|
253
|
+
hasUndefinedPrerequisite = true;
|
|
254
|
+
}
|
|
255
|
+
return;
|
|
248
256
|
}
|
|
249
257
|
visiting.add(name);
|
|
250
|
-
|
|
258
|
+
hasDynamicPrerequisite ||= entry.opaquePrerequisites;
|
|
251
259
|
for (const prerequisite of entry.prerequisites) {
|
|
252
|
-
|
|
253
|
-
return false;
|
|
254
|
-
}
|
|
260
|
+
collect(prerequisite);
|
|
255
261
|
}
|
|
256
262
|
recipeLines.push(...entry.recipes);
|
|
257
263
|
visiting.delete(name);
|
|
258
264
|
visited.add(name);
|
|
259
|
-
return true;
|
|
260
265
|
};
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
266
|
+
collect(target);
|
|
267
|
+
const expandedRecipes = [];
|
|
264
268
|
for (const line of recipeLines) {
|
|
265
|
-
|
|
269
|
+
const normalized = normalizeMakeRecipeLine(line);
|
|
270
|
+
const expanded = expandMakeExpression(normalized, cliVars, makefileVars);
|
|
271
|
+
if (expanded === null) {
|
|
266
272
|
return { recipes: recipeLines, opaque: true, reason: 'make_recipe_dynamic' };
|
|
267
273
|
}
|
|
274
|
+
expandedRecipes.push(expanded);
|
|
275
|
+
}
|
|
276
|
+
for (const line of expandedRecipes) {
|
|
277
|
+
if (/\$\(/.test(line) || /\$\{/.test(line)) {
|
|
278
|
+
return { recipes: expandedRecipes, opaque: true, reason: 'make_recipe_dynamic' };
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (hasDependencyCycle) {
|
|
282
|
+
return { recipes: expandedRecipes, opaque: true, reason: 'make_dependency_cycle' };
|
|
268
283
|
}
|
|
269
|
-
if (
|
|
270
|
-
return { recipes:
|
|
284
|
+
if (hasDynamicPrerequisite) {
|
|
285
|
+
return { recipes: expandedRecipes, opaque: true, reason: 'make_prerequisite_dynamic' };
|
|
271
286
|
}
|
|
272
|
-
|
|
287
|
+
if (hasUndefinedPrerequisite) {
|
|
288
|
+
return { recipes: expandedRecipes, opaque: true, reason: 'make_prerequisite_undefined' };
|
|
289
|
+
}
|
|
290
|
+
return { recipes: expandedRecipes, opaque: false, reason: 'make_recipe_resolved' };
|
|
273
291
|
}
|
|
274
292
|
export function resolveLauncherRecipe(params) {
|
|
275
|
-
if (params.depth >= MAX_RESOLVE_DEPTH) {
|
|
276
|
-
return { recipes: [], opaque: true, reason: 'launcher_depth_exceeded' };
|
|
277
|
-
}
|
|
278
293
|
const tokens = params.tokens;
|
|
279
294
|
const scriptName = npmScriptName(tokens);
|
|
280
295
|
if (scriptName) {
|
|
296
|
+
if (params.depth >= MAX_RESOLVE_DEPTH) {
|
|
297
|
+
return { recipes: [], opaque: true, reason: 'launcher_depth_exceeded' };
|
|
298
|
+
}
|
|
281
299
|
const resolution = resolveNpmRecipe(params.cwd, params.repoRoot, scriptName, forwardedArgs(tokens));
|
|
282
300
|
if (tokens[0] === 'pnpm' &&
|
|
283
301
|
tokens[1] &&
|
|
@@ -291,8 +309,31 @@ export function resolveLauncherRecipe(params) {
|
|
|
291
309
|
}
|
|
292
310
|
return resolution;
|
|
293
311
|
}
|
|
294
|
-
if (tokens[0] === 'make'
|
|
295
|
-
|
|
312
|
+
if (tokens[0] === 'make') {
|
|
313
|
+
if (params.depth >= MAX_RESOLVE_DEPTH) {
|
|
314
|
+
return { recipes: [], opaque: true, reason: 'launcher_depth_exceeded' };
|
|
315
|
+
}
|
|
316
|
+
if (tokens.includes('-n') || tokens.includes('--dry-run')) {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
let target = null;
|
|
320
|
+
const cliVars = {};
|
|
321
|
+
for (const token of tokens.slice(1)) {
|
|
322
|
+
if (token.startsWith('-')) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const assignment = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(token);
|
|
326
|
+
if (assignment) {
|
|
327
|
+
cliVars[assignment[1] ?? ''] = assignment[2] ?? '';
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (!target) {
|
|
331
|
+
target = token;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (target) {
|
|
335
|
+
return resolveMakeRecipe(params.cwd, params.repoRoot, target, cliVars);
|
|
336
|
+
}
|
|
296
337
|
}
|
|
297
338
|
if (tokens[0] === 'pnpm' && tokens[1] === 'exec' && tokens[2]) {
|
|
298
339
|
return {
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function parseMakefileVariables(content: string): Map<string, string>;
|
|
2
|
+
export declare function parsePhonyTargets(content: string): Set<string>;
|
|
3
|
+
export declare function normalizeMakeRecipeLine(line: string): string;
|
|
4
|
+
export declare function expandMakeExpression(expression: string, cliVars: Readonly<Record<string, string>>, makefileVars: ReadonlyMap<string, string>): string | null;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
const MAX_EXPAND_DEPTH = 16;
|
|
2
|
+
export function parseMakefileVariables(content) {
|
|
3
|
+
const variables = new Map();
|
|
4
|
+
for (const line of content.split('\n')) {
|
|
5
|
+
const trimmed = line.trim();
|
|
6
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
7
|
+
continue;
|
|
8
|
+
}
|
|
9
|
+
const match = /^([A-Za-z_][A-Za-z0-9_]*)\s*[:?]?=\s*(.+)$/.exec(trimmed);
|
|
10
|
+
if (!match) {
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
variables.set(match[1] ?? '', (match[2] ?? '').trim());
|
|
14
|
+
}
|
|
15
|
+
return variables;
|
|
16
|
+
}
|
|
17
|
+
export function parsePhonyTargets(content) {
|
|
18
|
+
const phony = new Set();
|
|
19
|
+
for (const line of content.split('\n')) {
|
|
20
|
+
const trimmed = line.trim();
|
|
21
|
+
const match = /^\.PHONY:\s*(.+)$/.exec(trimmed);
|
|
22
|
+
if (!match) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
for (const token of (match[1] ?? '').split(/\s+/)) {
|
|
26
|
+
if (token) {
|
|
27
|
+
phony.add(token);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return phony;
|
|
32
|
+
}
|
|
33
|
+
export function normalizeMakeRecipeLine(line) {
|
|
34
|
+
let normalized = line.trim();
|
|
35
|
+
while (normalized.startsWith('@') || normalized.startsWith('-') || normalized.startsWith('+')) {
|
|
36
|
+
normalized = normalized.slice(1).trimStart();
|
|
37
|
+
}
|
|
38
|
+
return normalized;
|
|
39
|
+
}
|
|
40
|
+
export function expandMakeExpression(expression, cliVars, makefileVars) {
|
|
41
|
+
if (/\$\(\s*shell\b/i.test(expression) || expression.includes('$$')) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const expanded = expandMakeValue(expression, cliVars, makefileVars, 0);
|
|
46
|
+
if (expanded === null || /\$\(/.test(expanded) || /\$\{/.test(expanded)) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return expanded;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function expandMakeValue(expression, cliVars, makefileVars, depth) {
|
|
56
|
+
if (depth > MAX_EXPAND_DEPTH) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
let value = expression.trim();
|
|
60
|
+
let changed = true;
|
|
61
|
+
let iterations = 0;
|
|
62
|
+
while (changed && iterations < MAX_EXPAND_DEPTH) {
|
|
63
|
+
changed = false;
|
|
64
|
+
iterations += 1;
|
|
65
|
+
const orMatch = value.match(/\$\(\s*or\s+([^()]*(?:\([^)]*\)[^()]*)*)\)/);
|
|
66
|
+
if (orMatch) {
|
|
67
|
+
const [fullMatch, inner] = orMatch;
|
|
68
|
+
const parts = splitMakeFunctionArgs(inner ?? '');
|
|
69
|
+
let selected = null;
|
|
70
|
+
for (const part of parts) {
|
|
71
|
+
const expanded = expandMakeValue(part.trim(), cliVars, makefileVars, depth + 1);
|
|
72
|
+
if (expanded !== null && expanded.trim() !== '') {
|
|
73
|
+
selected = expanded;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (selected === null) {
|
|
78
|
+
const fallback = parts.at(-1)?.trim();
|
|
79
|
+
selected =
|
|
80
|
+
fallback === undefined ? '' : expandMakeValue(fallback, cliVars, makefileVars, depth + 1);
|
|
81
|
+
}
|
|
82
|
+
if (selected === null) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
value = value.replace(fullMatch, selected);
|
|
86
|
+
changed = true;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const varMatch = value.match(/\$\(([A-Za-z_][A-Za-z0-9_]*)\)/);
|
|
90
|
+
if (varMatch) {
|
|
91
|
+
const [fullMatch, name] = varMatch;
|
|
92
|
+
const resolved = resolveMakeVariable(name ?? '', cliVars, makefileVars, depth + 1);
|
|
93
|
+
if (resolved === null) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
value = value.replace(fullMatch, resolved);
|
|
97
|
+
changed = true;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const bracedMatch = value.match(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/);
|
|
101
|
+
if (bracedMatch) {
|
|
102
|
+
const [fullMatch, name] = bracedMatch;
|
|
103
|
+
const resolved = name === 'PWD' ? '.' : resolveMakeVariable(name ?? '', cliVars, makefileVars, depth + 1);
|
|
104
|
+
if (resolved === null) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
value = value.replace(fullMatch, resolved);
|
|
108
|
+
changed = true;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
function resolveMakeVariable(name, cliVars, makefileVars, depth) {
|
|
116
|
+
if (Object.hasOwn(cliVars, name)) {
|
|
117
|
+
return cliVars[name] ?? '';
|
|
118
|
+
}
|
|
119
|
+
const definition = makefileVars.get(name);
|
|
120
|
+
if (definition === undefined) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
return expandMakeValue(definition, cliVars, makefileVars, depth);
|
|
124
|
+
}
|
|
125
|
+
function splitMakeFunctionArgs(input) {
|
|
126
|
+
const parts = [];
|
|
127
|
+
let current = '';
|
|
128
|
+
let depth = 0;
|
|
129
|
+
for (const char of input) {
|
|
130
|
+
if (char === '(') {
|
|
131
|
+
depth += 1;
|
|
132
|
+
current += char;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (char === ')') {
|
|
136
|
+
depth -= 1;
|
|
137
|
+
current += char;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (char === ',' && depth === 0) {
|
|
141
|
+
parts.push(current.trim());
|
|
142
|
+
current = '';
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
current += char;
|
|
146
|
+
}
|
|
147
|
+
if (current.trim()) {
|
|
148
|
+
parts.push(current.trim());
|
|
149
|
+
}
|
|
150
|
+
return parts;
|
|
151
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type ShellToken } from '../shell-tokenizer.js';
|
|
2
|
+
import { type RecursiveInvocation } from './recursive-invocation.js';
|
|
1
3
|
import type { VerdictOpacity } from './types.js';
|
|
2
4
|
export interface ParsedSegment {
|
|
3
5
|
tokens: string[];
|
|
@@ -16,6 +18,8 @@ export declare function peelTransparentWrappers(tokens: string[]): {
|
|
|
16
18
|
export declare function isVariableIndirectHead(head: string): boolean;
|
|
17
19
|
export declare function extractEvalBody(tokens: string[]): string | null;
|
|
18
20
|
export declare function extractRecursiveScript(tokens: string[]): string | null;
|
|
21
|
+
export declare function decodeRecursiveInvocationTokens(tokens: readonly ShellToken[]): RecursiveInvocation;
|
|
22
|
+
export declare function extractDockerComposeRunScript(tokens: string[]): string | null;
|
|
19
23
|
/**
|
|
20
24
|
* True when a recursive script is evaluated from a command argument rather
|
|
21
25
|
* than expanded from a static launcher recipe. Callers use this semantic fact
|