@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.
Files changed (55) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/codex/runtime-entry.d.ts +3 -0
  3. package/dist/adapters/codex/runtime-entry.js +27 -4
  4. package/dist/adapters/cursor/cwd-resolution.d.ts +10 -0
  5. package/dist/adapters/cursor/cwd-resolution.js +58 -0
  6. package/dist/adapters/cursor/hooks.d.ts +1 -0
  7. package/dist/adapters/cursor/hooks.js +21 -0
  8. package/dist/adapters/cursor/runtime-entry.d.ts +1 -0
  9. package/dist/adapters/cursor/runtime-entry.js +107 -6
  10. package/dist/adapters/shared/gate-runtime.js +26 -3
  11. package/dist/adapters/shared/repo-root.js +20 -1
  12. package/dist/bundle/claude-runtime.mjs +855 -338
  13. package/dist/bundle/codex-runtime.mjs +877 -342
  14. package/dist/bundle/cursor-runtime.mjs +3886 -3141
  15. package/dist/cli.js +33 -3
  16. package/dist/commands/doctor.js +24 -1
  17. package/dist/commands/health-snapshot.d.ts +3 -0
  18. package/dist/commands/health-snapshot.js +56 -0
  19. package/dist/commands/report.js +14 -0
  20. package/dist/commands/status.js +15 -0
  21. package/dist/commands/where.d.ts +4 -0
  22. package/dist/commands/where.js +52 -0
  23. package/dist/core/approval-repo-lookup.d.ts +16 -0
  24. package/dist/core/approval-repo-lookup.js +48 -0
  25. package/dist/core/audit-io.d.ts +1 -1
  26. package/dist/core/audit-io.js +1 -1
  27. package/dist/core/audit-query.d.ts +1 -0
  28. package/dist/core/audit-query.js +7 -0
  29. package/dist/core/audit-serialize.d.ts +3 -0
  30. package/dist/core/audit-serialize.js +39 -3
  31. package/dist/core/audit-summary.d.ts +9 -0
  32. package/dist/core/audit-summary.js +58 -1
  33. package/dist/core/audit-types.d.ts +4 -0
  34. package/dist/core/effect-ir/shell-lower.js +93 -40
  35. package/dist/core/replay-scrub.d.ts +1 -0
  36. package/dist/core/replay-scrub.js +22 -3
  37. package/dist/core/shell-tokenizer.d.ts +28 -0
  38. package/dist/core/shell-tokenizer.js +111 -29
  39. package/dist/core/verdict/docker-compose-run.d.ts +18 -0
  40. package/dist/core/verdict/docker-compose-run.js +136 -0
  41. package/dist/core/verdict/launcher-resolve.js +28 -28
  42. package/dist/core/verdict/parser.d.ts +3 -0
  43. package/dist/core/verdict/parser.js +23 -40
  44. package/dist/core/verdict/recursive-invocation.d.ts +20 -0
  45. package/dist/core/verdict/recursive-invocation.js +224 -0
  46. package/dist/defaults.js +7 -0
  47. package/dist/installer/scope-config.d.ts +2 -2
  48. package/dist/installer.d.ts +10 -1
  49. package/dist/installer.js +43 -2
  50. package/dist/types.d.ts +34 -1
  51. package/dist/version.d.ts +1 -1
  52. package/dist/version.js +1 -1
  53. package/package.json +5 -2
  54. package/skills/belay/SKILL.md +5 -0
  55. 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 { extractDockerComposeRunScript, extractRecursiveScript, isCommandInspection, isDynamicRecursiveEvaluation, parseSegment, redactCommand, segmentOpacity, splitStructuralShellSegments, structuralSubstitutionInners, } from '../verdict/parser.js';
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 rawTokens = tokenizeShell(command);
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 >= MAX_LOWER_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 recursiveScript = extractRecursiveScript(tokens);
195
- if (recursiveScript && opacity !== 'opaque' && opacity !== 'unparseable') {
196
- const dynamicEvaluation = isDynamicRecursiveEvaluation(tokens);
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
- ...(dynamicEvaluation ? ['dynamic_shell_evaluation'] : []),
208
+ 'dynamic_shell_evaluation',
200
209
  ]));
201
- const nested = lowerTopLevelSegments(recursiveScript, {
202
- ...context,
203
- command: recursiveScript,
204
- env,
205
- depth: context.depth + 1,
206
- });
207
- for (const nestedSegment of nested) {
208
- requirements.push(...nestedSegment.requirements.map((entry) => withInnerProvenance(entry, recursiveScript, head, commandRedacted)));
209
- for (const signal of nestedSegment.signals) {
210
- signals.add(signal);
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
- if (dynamicEvaluation) {
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
- const dockerComposeScript = extractDockerComposeRunScript(tokens);
220
- if (dockerComposeScript && opacity !== 'opaque' && opacity !== 'unparseable') {
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
- const nested = lowerTopLevelSegments(dockerComposeScript, {
223
- ...context,
224
- command: dockerComposeScript,
225
- env,
226
- depth: context.depth + 1,
227
- });
228
- for (const nestedSegment of nested) {
229
- requirements.push(...nestedSegment.requirements.map((entry) => withInnerProvenance(entry, dockerComposeScript, head, commandRedacted)));
230
- for (const signal of nestedSegment.signals) {
231
- signals.add(signal);
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
- if (token.includes('>') || token.includes('<')) {
1341
- const inline = token.replace(/^\d*(?:>>?|<<?|<>|>\|)/, '');
1342
- if (!inline) {
1343
- index += 1;
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 = payload.tool_input;
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(payload, scrubOptions);
59
+ return subagentFingerprintSource(replayPayload, scrubOptions);
41
60
  }
42
- return scrubValue(payload, scrubOptions);
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 tokenizeShell(input) {
70
+ export function lexShell(input) {
71
71
  const tokens = [];
72
- let buffer = '';
72
+ let value = '';
73
+ let wordStart = null;
74
+ let parts = [];
73
75
  let quote = null;
74
- let escaping = false;
75
- const flush = () => {
76
- if (buffer.length > 0) {
77
- tokens.push(buffer);
78
- buffer = '';
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 (escaping) {
84
- buffer += char;
85
- escaping = false;
86
- continue;
87
- }
88
- if (char === '\\') {
89
- escaping = true;
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 === quote) {
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
- else {
97
- buffer += char;
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 === '"' || char === "'") {
102
- quote = char;
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
- flush();
108
- tokens.push(operator.token);
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
- flush();
114
- tokens.push(';');
190
+ flushWord(index);
191
+ pushOperator(';', index, index + 1);
115
192
  continue;
116
193
  }
117
194
  if (/\s/.test(char)) {
118
- flush();
195
+ flushWord(index);
119
196
  continue;
120
197
  }
121
- buffer += char;
198
+ append(char, index, index + 1, 'unquoted', char === '$' || char === '`');
122
199
  }
123
- flush();
124
- return tokens;
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
+ }