@guilz-dev/belay 0.9.2 → 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 +1 -0
- package/dist/adapters/cursor/hooks.js +21 -0
- 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 +855 -338
- package/dist/bundle/codex-runtime.mjs +877 -342
- package/dist/bundle/cursor-runtime.mjs +3886 -3141
- package/dist/cli.js +33 -3
- package/dist/commands/doctor.js +24 -1
- 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-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 +93 -40
- 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 +28 -28
- package/dist/core/verdict/parser.d.ts +3 -0
- package/dist/core/verdict/parser.js +23 -40
- package/dist/core/verdict/recursive-invocation.d.ts +20 -0
- package/dist/core/verdict/recursive-invocation.js +224 -0
- package/dist/defaults.js +7 -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
|
@@ -2,11 +2,12 @@ import { lstatSync, realpathSync } from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { inspectGitResourceIdentity } from '../git-resource-identity.js';
|
|
4
4
|
import { canonicalPath, pathWithinRoot } from '../path-utils.js';
|
|
5
|
-
import { isFdDuplication, isRedirectOperator, tokenizeShell } from '../shell-tokenizer.js';
|
|
5
|
+
import { isFdDuplication, isRedirectOperator, lexShell, tokenizeShell, } from '../shell-tokenizer.js';
|
|
6
|
+
import { decodeDockerComposeRun as decodeStructuredDockerComposeRun } from '../verdict/docker-compose-run.js';
|
|
6
7
|
import { decodeEgressEffects } from '../verdict/egress-classify.js';
|
|
7
8
|
import { decodeGitEffects } from '../verdict/git-classifier.js';
|
|
8
9
|
import { resolveLauncherRecipe } from '../verdict/launcher-resolve.js';
|
|
9
|
-
import {
|
|
10
|
+
import { decodeRecursiveInvocationTokens, isCommandInspection, parseSegment, redactCommand, segmentOpacity, splitStructuralShellSegments, structuralSubstitutionInners, } from '../verdict/parser.js';
|
|
10
11
|
import { joinEffectOpacity } from './normalize.js';
|
|
11
12
|
import { classifyPackageAcquisitionSpec, innerRecipeFromPeel, peelPackageExecArgv, resolveLocalBin, } from './package-exec.js';
|
|
12
13
|
import { buildShellEffectPlan, } from './shell-build.js';
|
|
@@ -127,7 +128,8 @@ function joinNestedOpacity(outer, nested) {
|
|
|
127
128
|
}
|
|
128
129
|
function lowerSegment(command, context) {
|
|
129
130
|
const commandRedacted = redactCommand(command);
|
|
130
|
-
const
|
|
131
|
+
const lexed = lexShell(command);
|
|
132
|
+
const rawTokens = lexed.tokens.map((token) => token.value);
|
|
131
133
|
const environment = extractEnvironment(rawTokens, context.env);
|
|
132
134
|
const env = environment.env;
|
|
133
135
|
const parsed = parseSegment(command);
|
|
@@ -137,10 +139,18 @@ function lowerSegment(command, context) {
|
|
|
137
139
|
rawTokens.every((token) => ENV_PREFIX_PATTERN.test(token))
|
|
138
140
|
? rawTokens
|
|
139
141
|
: parsedTokens).map((token) => expandKnownVariables(token, env));
|
|
142
|
+
const decoderTokens = alignStructuredTokens(stripStructuredRedirects(lexed.tokens), stripRedirects(parsedTokens));
|
|
140
143
|
const head = path.basename(tokens[0] ?? parsed.head);
|
|
141
144
|
let opacity = segmentOpacity(command);
|
|
142
145
|
const signals = new Set();
|
|
143
146
|
const requirements = [];
|
|
147
|
+
if (!lexed.complete) {
|
|
148
|
+
requirements.push(requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, commandRedacted, [
|
|
149
|
+
'shell.grammar_incomplete',
|
|
150
|
+
]));
|
|
151
|
+
signals.add('shell.grammar_incomplete');
|
|
152
|
+
opacity = joinEffectOpacity(opacity, 'unparseable');
|
|
153
|
+
}
|
|
144
154
|
addRedirectEffects(requirements, rawTokens, env, context, commandRedacted);
|
|
145
155
|
addSubstitutionEffects(requirements, command, context, commandRedacted, signals);
|
|
146
156
|
if (environment.malformed) {
|
|
@@ -156,7 +166,7 @@ function lowerSegment(command, context) {
|
|
|
156
166
|
signals.add('shell.xargs_stdin_dynamic');
|
|
157
167
|
opacity = joinEffectOpacity(opacity, 'opaque');
|
|
158
168
|
}
|
|
159
|
-
if (context.depth
|
|
169
|
+
if (context.depth > MAX_LOWER_DEPTH) {
|
|
160
170
|
requirements.push(requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, commandRedacted, [
|
|
161
171
|
'shell.lower_depth_exceeded',
|
|
162
172
|
]));
|
|
@@ -191,50 +201,69 @@ function lowerSegment(command, context) {
|
|
|
191
201
|
}
|
|
192
202
|
return shellSegment(commandRedacted, head, requirements, opacity, signals);
|
|
193
203
|
}
|
|
194
|
-
const
|
|
195
|
-
if (
|
|
196
|
-
|
|
197
|
-
requirements.push(processRequirement(head || 'sh', 'spawn', commandRedacted, [
|
|
204
|
+
const recursive = decodeRecursiveInvocationTokens(decoderTokens);
|
|
205
|
+
if (recursive.kind === 'static' && opacity !== 'opaque' && opacity !== 'unparseable') {
|
|
206
|
+
requirements.push(processRequirement(recursive.interpreter, 'spawn', commandRedacted, [
|
|
198
207
|
'shell.recursive_wrapper',
|
|
199
|
-
|
|
208
|
+
'dynamic_shell_evaluation',
|
|
200
209
|
]));
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
210
|
+
if (recursive.script !== '') {
|
|
211
|
+
const nested = lowerTopLevelSegments(recursive.script, {
|
|
212
|
+
...context,
|
|
213
|
+
command: recursive.script,
|
|
214
|
+
env,
|
|
215
|
+
depth: context.depth + 1,
|
|
216
|
+
});
|
|
217
|
+
for (const nestedSegment of nested) {
|
|
218
|
+
requirements.push(...nestedSegment.requirements.map((entry) => withInnerProvenance(entry, recursive.script, head, commandRedacted)));
|
|
219
|
+
for (const signal of nestedSegment.signals)
|
|
220
|
+
signals.add(signal);
|
|
211
221
|
}
|
|
212
222
|
}
|
|
213
223
|
signals.add('shell.recursive_wrapper');
|
|
214
|
-
|
|
215
|
-
signals.add('dynamic_shell_evaluation');
|
|
216
|
-
}
|
|
224
|
+
signals.add('dynamic_shell_evaluation');
|
|
217
225
|
return shellSegment(commandRedacted, head, requirements, 'recursive', signals);
|
|
218
226
|
}
|
|
219
|
-
|
|
220
|
-
|
|
227
|
+
if (recursive.kind === 'dynamic' || recursive.kind === 'indeterminate') {
|
|
228
|
+
const recursiveSignals = [
|
|
229
|
+
recursive.signal,
|
|
230
|
+
...(recursive.kind === 'dynamic' ? ['dynamic_shell_evaluation'] : []),
|
|
231
|
+
];
|
|
232
|
+
requirements.push(processRequirement(recursive.interpreter, 'spawn', commandRedacted, recursiveSignals), requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, commandRedacted, [
|
|
233
|
+
...recursiveSignals,
|
|
234
|
+
]));
|
|
235
|
+
for (const signal of recursiveSignals)
|
|
236
|
+
signals.add(signal);
|
|
237
|
+
return shellSegment(commandRedacted, head, requirements, joinEffectOpacity(opacity, 'opaque'), signals);
|
|
238
|
+
}
|
|
239
|
+
const compose = decodeStructuredDockerComposeRun(decoderTokens);
|
|
240
|
+
if (compose.kind === 'recursive' && opacity !== 'opaque' && opacity !== 'unparseable') {
|
|
221
241
|
requirements.push(processRequirement(head, 'spawn', commandRedacted, ['process.docker_compose_run']));
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
242
|
+
if (compose.script !== '') {
|
|
243
|
+
const nested = lowerTopLevelSegments(compose.script, {
|
|
244
|
+
...context,
|
|
245
|
+
command: compose.script,
|
|
246
|
+
env,
|
|
247
|
+
depth: context.depth + 1,
|
|
248
|
+
});
|
|
249
|
+
for (const nestedSegment of nested) {
|
|
250
|
+
requirements.push(...nestedSegment.requirements.map((entry) => withInnerProvenance(entry, compose.script, head, commandRedacted)));
|
|
251
|
+
for (const signal of nestedSegment.signals)
|
|
252
|
+
signals.add(signal);
|
|
253
|
+
opacity = joinNestedOpacity(opacity, nestedSegment);
|
|
232
254
|
}
|
|
233
|
-
opacity = joinNestedOpacity(opacity, nestedSegment);
|
|
234
255
|
}
|
|
235
256
|
signals.add('process.docker_compose_run');
|
|
236
257
|
return shellSegment(commandRedacted, head, requirements, 'recursive', signals);
|
|
237
258
|
}
|
|
259
|
+
if (compose.kind === 'dynamic' || compose.kind === 'indeterminate') {
|
|
260
|
+
requirements.push(processRequirement(head, 'spawn', commandRedacted, ['process.docker_compose_run']), requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, commandRedacted, [
|
|
261
|
+
compose.signal,
|
|
262
|
+
]));
|
|
263
|
+
signals.add('process.docker_compose_run');
|
|
264
|
+
signals.add(compose.signal);
|
|
265
|
+
return shellSegment(commandRedacted, head, requirements, joinEffectOpacity(opacity, 'opaque'), signals);
|
|
266
|
+
}
|
|
238
267
|
const launcher = resolveLauncherRecipe({
|
|
239
268
|
tokens,
|
|
240
269
|
cwd: context.cwd,
|
|
@@ -1337,15 +1366,39 @@ function stripRedirects(tokens) {
|
|
|
1337
1366
|
stripped.push(token);
|
|
1338
1367
|
continue;
|
|
1339
1368
|
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1369
|
+
index += 1;
|
|
1370
|
+
}
|
|
1371
|
+
return stripped;
|
|
1372
|
+
}
|
|
1373
|
+
function stripStructuredRedirects(tokens) {
|
|
1374
|
+
const stripped = [];
|
|
1375
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
1376
|
+
const token = tokens[index];
|
|
1377
|
+
if (!token)
|
|
1378
|
+
continue;
|
|
1379
|
+
if (isFdDuplication(token.value)) {
|
|
1380
|
+
continue;
|
|
1345
1381
|
}
|
|
1382
|
+
if (!isRedirectOperator(token.value)) {
|
|
1383
|
+
stripped.push(token);
|
|
1384
|
+
continue;
|
|
1385
|
+
}
|
|
1386
|
+
index += 1;
|
|
1346
1387
|
}
|
|
1347
1388
|
return stripped;
|
|
1348
1389
|
}
|
|
1390
|
+
function alignStructuredTokens(tokens, values) {
|
|
1391
|
+
if (values.length === 0)
|
|
1392
|
+
return [];
|
|
1393
|
+
for (let start = tokens.length - values.length; start >= 0; start -= 1) {
|
|
1394
|
+
const candidate = tokens.slice(start);
|
|
1395
|
+
if (candidate.length === values.length &&
|
|
1396
|
+
candidate.every((token, index) => token.value === values[index])) {
|
|
1397
|
+
return candidate;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
return [];
|
|
1401
|
+
}
|
|
1349
1402
|
function shellSegment(commandRedacted, segmentHead, requirements, opacity, signals) {
|
|
1350
1403
|
const normalizedRequirements = requirements.flatMap((entry) => {
|
|
1351
1404
|
const dynamicSignal = dynamicResourceSignal(entry.resource);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { GatedActionKind } from './gate-contract.js';
|
|
2
2
|
import type { ScrubOptions } from './types.js';
|
|
3
|
+
export declare function redactToolInvocationId(value: unknown, rawToolUseId?: string): unknown;
|
|
3
4
|
/** Subagent fingerprint input — must match classify-subagent `fingerprintSource`. */
|
|
4
5
|
export declare function subagentFingerprintSource(payload: Record<string, unknown>, scrubOptions: ScrubOptions): unknown;
|
|
5
6
|
/**
|
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
import { scrubValue } from './scrub.js';
|
|
2
|
+
export function redactToolInvocationId(value, rawToolUseId) {
|
|
3
|
+
if (typeof value === 'string') {
|
|
4
|
+
return rawToolUseId ? value.replaceAll(rawToolUseId, '<tool-use-id>') : value;
|
|
5
|
+
}
|
|
6
|
+
if (Array.isArray(value)) {
|
|
7
|
+
return value.map((item) => redactToolInvocationId(item, rawToolUseId));
|
|
8
|
+
}
|
|
9
|
+
if (value && typeof value === 'object') {
|
|
10
|
+
const result = {};
|
|
11
|
+
for (const [key, child] of Object.entries(value)) {
|
|
12
|
+
if (key !== 'tool_use_id') {
|
|
13
|
+
result[key] = redactToolInvocationId(child, rawToolUseId);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
2
20
|
/** Subagent fingerprint input — must match classify-subagent `fingerprintSource`. */
|
|
3
21
|
export function subagentFingerprintSource(payload, scrubOptions) {
|
|
4
22
|
const toolInput = payload.tool_input;
|
|
@@ -30,14 +48,15 @@ export function fingerprintReplayPayload(kind, payload, scrubOptions) {
|
|
|
30
48
|
if (!payload) {
|
|
31
49
|
return undefined;
|
|
32
50
|
}
|
|
51
|
+
const replayPayload = redactToolInvocationId(payload, typeof payload.tool_use_id === 'string' ? payload.tool_use_id : undefined);
|
|
33
52
|
if (kind === 'tool') {
|
|
34
|
-
const toolInput =
|
|
53
|
+
const toolInput = replayPayload.tool_input;
|
|
35
54
|
if (toolInput && typeof toolInput === 'object') {
|
|
36
55
|
return scrubValue(toolInput, scrubOptions);
|
|
37
56
|
}
|
|
38
57
|
}
|
|
39
58
|
if (kind === 'subagent') {
|
|
40
|
-
return subagentFingerprintSource(
|
|
59
|
+
return subagentFingerprintSource(replayPayload, scrubOptions);
|
|
41
60
|
}
|
|
42
|
-
return scrubValue(
|
|
61
|
+
return scrubValue(replayPayload, scrubOptions);
|
|
43
62
|
}
|
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
export declare function isRedirectOperator(token: string): boolean;
|
|
2
2
|
export declare function isFdDuplication(token: string): boolean;
|
|
3
|
+
export type ShellQuoteMode = 'unquoted' | 'single' | 'double';
|
|
4
|
+
export interface ShellWordPart {
|
|
5
|
+
value: string;
|
|
6
|
+
raw: string;
|
|
7
|
+
start: number;
|
|
8
|
+
end: number;
|
|
9
|
+
quote: ShellQuoteMode;
|
|
10
|
+
hasExpansion: boolean;
|
|
11
|
+
}
|
|
12
|
+
export type ShellToken = {
|
|
13
|
+
kind: 'word';
|
|
14
|
+
value: string;
|
|
15
|
+
raw: string;
|
|
16
|
+
start: number;
|
|
17
|
+
end: number;
|
|
18
|
+
parts: ShellWordPart[];
|
|
19
|
+
} | {
|
|
20
|
+
kind: 'operator';
|
|
21
|
+
value: string;
|
|
22
|
+
raw: string;
|
|
23
|
+
start: number;
|
|
24
|
+
end: number;
|
|
25
|
+
};
|
|
26
|
+
export interface ShellLexResult {
|
|
27
|
+
tokens: ShellToken[];
|
|
28
|
+
complete: boolean;
|
|
29
|
+
}
|
|
30
|
+
export declare function lexShell(input: string): ShellLexResult;
|
|
3
31
|
export declare function tokenizeShell(input: string): string[];
|
|
4
32
|
export declare function normalizeShellCommand(command: string, repoRoot: string, normalizeToken: (t: string, r: string) => string): string;
|
|
5
33
|
export declare function splitTopLevelSegments(tokens: string[]): string[][];
|
|
@@ -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
|
+
}
|