@guilz-dev/belay 0.9.2 → 0.9.4
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 +36 -2
- package/dist/adapters/codex/hooks.d.ts +1 -0
- package/dist/adapters/codex/hooks.js +3 -0
- 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/dispatcher-generation.d.ts +3 -0
- package/dist/adapters/cursor/dispatcher-generation.js +5 -0
- package/dist/adapters/cursor/hook-dispatch-entry.d.ts +7 -0
- package/dist/adapters/cursor/hook-dispatch-entry.js +126 -0
- package/dist/adapters/cursor/hook-router.d.ts +23 -0
- package/dist/adapters/cursor/hook-router.js +266 -0
- package/dist/adapters/cursor/hooks.d.ts +5 -0
- package/dist/adapters/cursor/hooks.js +115 -3
- package/dist/adapters/cursor/routing-layout.d.ts +5 -0
- package/dist/adapters/cursor/routing-layout.js +29 -0
- package/dist/adapters/cursor/runtime-entry.d.ts +18 -0
- package/dist/adapters/cursor/runtime-entry.js +135 -34
- package/dist/adapters/layouts/scope.d.ts +5 -0
- package/dist/adapters/layouts/scope.js +71 -0
- package/dist/adapters/shared/gate-runtime.js +27 -3
- package/dist/adapters/shared/repo-root.js +20 -1
- package/dist/bundle/claude-runtime.mjs +2660 -1951
- package/dist/bundle/codex-runtime.mjs +2680 -1953
- package/dist/bundle/cursor-dispatcher.mjs +443 -0
- package/dist/bundle/cursor-runtime.mjs +5638 -4697
- package/dist/cli.js +33 -3
- package/dist/commands/doctor.js +215 -16
- package/dist/commands/health-snapshot.d.ts +3 -0
- package/dist/commands/health-snapshot.js +61 -0
- package/dist/commands/recovery-checkpoints.d.ts +3 -3
- 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/config-io.js +11 -2
- 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.js +43 -25
- 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/classify-tool.js +16 -8
- package/dist/core/effect-ir/argv-delegate.d.ts +9 -0
- package/dist/core/effect-ir/argv-delegate.js +42 -0
- package/dist/core/effect-ir/shell-lower/argv-delegate-gate.d.ts +3 -0
- package/dist/core/effect-ir/shell-lower/argv-delegate-gate.js +40 -0
- package/dist/core/effect-ir/shell-lower/augment.d.ts +4 -0
- package/dist/core/effect-ir/shell-lower/augment.js +97 -0
- package/dist/core/effect-ir/shell-lower/context.d.ts +10 -0
- package/dist/core/effect-ir/shell-lower/context.js +1 -0
- package/dist/core/effect-ir/shell-lower/decode-process.d.ts +9 -0
- package/dist/core/effect-ir/shell-lower/decode-process.js +201 -0
- package/dist/core/effect-ir/shell-lower/decoders/belay.d.ts +2 -0
- package/dist/core/effect-ir/shell-lower/decoders/belay.js +29 -0
- package/dist/core/effect-ir/shell-lower/decoders/builtins.d.ts +7 -0
- package/dist/core/effect-ir/shell-lower/decoders/builtins.js +194 -0
- package/dist/core/effect-ir/shell-lower/decoders/decode-package-exec.d.ts +4 -0
- package/dist/core/effect-ir/shell-lower/decoders/decode-package-exec.js +68 -0
- package/dist/core/effect-ir/shell-lower/decoders/docker.d.ts +3 -0
- package/dist/core/effect-ir/shell-lower/decoders/docker.js +43 -0
- package/dist/core/effect-ir/shell-lower/decoders/filesystem.d.ts +9 -0
- package/dist/core/effect-ir/shell-lower/decoders/filesystem.js +217 -0
- package/dist/core/effect-ir/shell-lower/decoders/prisma.d.ts +2 -0
- package/dist/core/effect-ir/shell-lower/decoders/prisma.js +54 -0
- package/dist/core/effect-ir/shell-lower/decoders/ruby.d.ts +11 -0
- package/dist/core/effect-ir/shell-lower/decoders/ruby.js +147 -0
- package/dist/core/effect-ir/shell-lower/decoders/toolchain.d.ts +7 -0
- package/dist/core/effect-ir/shell-lower/decoders/toolchain.js +155 -0
- package/dist/core/effect-ir/shell-lower/requirement.d.ts +12 -0
- package/dist/core/effect-ir/shell-lower/requirement.js +65 -0
- package/dist/core/effect-ir/shell-lower/segment.d.ts +11 -0
- package/dist/core/effect-ir/shell-lower/segment.js +82 -0
- package/dist/core/effect-ir/shell-lower/tokens.d.ts +26 -0
- package/dist/core/effect-ir/shell-lower/tokens.js +183 -0
- package/dist/core/effect-ir/shell-lower.d.ts +2 -7
- package/dist/core/effect-ir/shell-lower.js +122 -1511
- package/dist/core/glob.js +32 -16
- package/dist/core/integrity.d.ts +2 -2
- package/dist/core/integrity.js +52 -12
- package/dist/core/replay-scrub.d.ts +3 -0
- package/dist/core/replay-scrub.js +30 -3
- package/dist/core/shell-substitution.js +2 -2
- package/dist/core/shell-tokenizer.d.ts +28 -0
- package/dist/core/shell-tokenizer.js +111 -29
- package/dist/core/shell-unparseable.js +3 -1
- 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.d.ts +7 -3
- package/dist/defaults.js +35 -21
- package/dist/installer/bootstrap.d.ts +1 -0
- package/dist/installer/bootstrap.js +2 -2
- package/dist/installer/runtime-artifacts.js +43 -13
- package/dist/installer/scope-config.d.ts +2 -2
- package/dist/installer.d.ts +10 -1
- package/dist/installer.js +83 -7
- package/dist/node-resolution.d.ts +1 -0
- package/dist/node-resolution.js +61 -0
- package/dist/templates.d.ts +7 -4
- package/dist/templates.js +42 -4
- package/dist/types.d.ts +35 -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
package/dist/core/glob.js
CHANGED
|
@@ -1,26 +1,42 @@
|
|
|
1
|
+
import { escapeRegex } from './approval.js';
|
|
2
|
+
function globPatternToRegexSource(pattern) {
|
|
3
|
+
let result = '';
|
|
4
|
+
let index = 0;
|
|
5
|
+
while (index < pattern.length) {
|
|
6
|
+
const char = pattern[index];
|
|
7
|
+
if (char === '*' && pattern[index + 1] === '*') {
|
|
8
|
+
index += 2;
|
|
9
|
+
if (pattern[index] === '/') {
|
|
10
|
+
result += '(?:.*/)?';
|
|
11
|
+
index += 1;
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
result += '.*';
|
|
15
|
+
}
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (char === '*') {
|
|
19
|
+
result += '[^/]*';
|
|
20
|
+
index += 1;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
result += escapeRegex(char);
|
|
24
|
+
index += 1;
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
function globPatternMatches(pattern, normalized, baseName) {
|
|
29
|
+
const regex = new RegExp(`^${globPatternToRegexSource(pattern)}$`);
|
|
30
|
+
return regex.test(normalized) || regex.test(baseName);
|
|
31
|
+
}
|
|
1
32
|
export function matchesSensitivePath(filePath, patterns) {
|
|
2
33
|
const normalized = filePath.replaceAll('\\', '/');
|
|
3
34
|
const segments = normalized.split('/');
|
|
4
35
|
const baseName = segments.at(-1) ?? normalized;
|
|
5
36
|
for (const pattern of patterns) {
|
|
6
37
|
const normalizedPattern = pattern.replaceAll('\\', '/');
|
|
7
|
-
if (normalizedPattern.includes('**')) {
|
|
8
|
-
const parts = normalizedPattern.split('**').map((part) => part.replace(/^\/+|\/+$/g, ''));
|
|
9
|
-
const prefix = parts[0]?.replace(/\/+$/, '') ?? '';
|
|
10
|
-
const suffix = parts[1]?.replace(/^\/+/, '') ?? '';
|
|
11
|
-
if (prefix && !normalized.startsWith(prefix)) {
|
|
12
|
-
continue;
|
|
13
|
-
}
|
|
14
|
-
if (suffix && !normalized.includes(suffix)) {
|
|
15
|
-
continue;
|
|
16
|
-
}
|
|
17
|
-
if (prefix || suffix) {
|
|
18
|
-
return true;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
38
|
if (normalizedPattern.includes('*')) {
|
|
22
|
-
|
|
23
|
-
if (regex.test(normalized) || regex.test(baseName)) {
|
|
39
|
+
if (globPatternMatches(normalizedPattern, normalized, baseName)) {
|
|
24
40
|
return true;
|
|
25
41
|
}
|
|
26
42
|
continue;
|
package/dist/core/integrity.d.ts
CHANGED
|
@@ -8,9 +8,9 @@ export interface IntegrityManifest {
|
|
|
8
8
|
}
|
|
9
9
|
export declare function sha256File(filePath: string): Promise<string>;
|
|
10
10
|
export declare function integrityManifestPath(layout: AdapterLayout, repoRoot: string): string;
|
|
11
|
-
export declare function runtimeIntegrityFiles(
|
|
11
|
+
export declare function runtimeIntegrityFiles(layout: AdapterLayout, paths: ScopedPaths): string[];
|
|
12
12
|
export declare function writeIntegrityManifest(repoRoot: string, layout: AdapterLayout, filePaths: string[]): Promise<void>;
|
|
13
|
-
export declare function verifyIntegrityManifest(repoRoot: string, layout: AdapterLayout): Promise<{
|
|
13
|
+
export declare function verifyIntegrityManifest(repoRoot: string, layout: AdapterLayout, requiredFilePaths?: string[]): Promise<{
|
|
14
14
|
ok: boolean;
|
|
15
15
|
mismatches: string[];
|
|
16
16
|
}>;
|
package/dist/core/integrity.js
CHANGED
|
@@ -3,8 +3,24 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { getAdapterLayout } from '../adapters/layouts/index.js';
|
|
6
|
-
import { resolveScopedPaths } from '../adapters/layouts/scope.js';
|
|
6
|
+
import { isPathInside, resolveScopedPaths } from '../adapters/layouts/scope.js';
|
|
7
7
|
import { resolveAdapterName } from '../config-io.js';
|
|
8
|
+
const GLOBAL_INTEGRITY_PREFIX = '@global/';
|
|
9
|
+
function portableRelativePath(from, to) {
|
|
10
|
+
return path.relative(from, to).split(path.sep).join('/');
|
|
11
|
+
}
|
|
12
|
+
function integrityManifestFileKey(repoRoot, layout, filePath) {
|
|
13
|
+
const absolutePath = path.resolve(filePath);
|
|
14
|
+
if (isPathInside(absolutePath, repoRoot)) {
|
|
15
|
+
return portableRelativePath(repoRoot, absolutePath);
|
|
16
|
+
}
|
|
17
|
+
const globalPaths = resolveScopedPaths(layout, 'global', repoRoot);
|
|
18
|
+
const globalAgentDir = path.dirname(globalPaths.hooksSettingsPath);
|
|
19
|
+
if (isPathInside(absolutePath, globalAgentDir)) {
|
|
20
|
+
return `${GLOBAL_INTEGRITY_PREFIX}${portableRelativePath(globalAgentDir, absolutePath)}`;
|
|
21
|
+
}
|
|
22
|
+
throw new Error(`Integrity file is outside the repository and global adapter root: ${filePath}`);
|
|
23
|
+
}
|
|
8
24
|
export async function sha256File(filePath) {
|
|
9
25
|
const content = await readFile(filePath);
|
|
10
26
|
return createHash('sha256').update(content).digest('hex');
|
|
@@ -12,11 +28,8 @@ export async function sha256File(filePath) {
|
|
|
12
28
|
export function integrityManifestPath(layout, repoRoot) {
|
|
13
29
|
return path.join(layout.repoLocalStateDir(repoRoot), 'integrity-manifest.json');
|
|
14
30
|
}
|
|
15
|
-
export function runtimeIntegrityFiles(
|
|
31
|
+
export function runtimeIntegrityFiles(layout, paths) {
|
|
16
32
|
const files = [paths.configPath];
|
|
17
|
-
if (paths.scope !== 'project') {
|
|
18
|
-
return files;
|
|
19
|
-
}
|
|
20
33
|
const hooksDir = paths.hooksDir;
|
|
21
34
|
const runtimeDir = paths.runtimeDir;
|
|
22
35
|
return [
|
|
@@ -28,7 +41,9 @@ export function runtimeIntegrityFiles(_layout, paths) {
|
|
|
28
41
|
path.join(hooksDir, 'belay-audit.mjs'),
|
|
29
42
|
path.join(hooksDir, 'belay-runner'),
|
|
30
43
|
path.join(hooksDir, 'belay-runner.cmd'),
|
|
44
|
+
...(layout.name === 'cursor' ? [path.join(hooksDir, 'belay-runner.ps1')] : []),
|
|
31
45
|
path.join(runtimeDir, 'core.mjs'),
|
|
46
|
+
...(layout.name === 'cursor' ? [path.join(runtimeDir, 'dispatcher.mjs')] : []),
|
|
32
47
|
];
|
|
33
48
|
}
|
|
34
49
|
export async function writeIntegrityManifest(repoRoot, layout, filePaths) {
|
|
@@ -37,8 +52,8 @@ export async function writeIntegrityManifest(repoRoot, layout, filePaths) {
|
|
|
37
52
|
if (!existsSync(filePath)) {
|
|
38
53
|
continue;
|
|
39
54
|
}
|
|
40
|
-
const
|
|
41
|
-
files[
|
|
55
|
+
const manifestKey = integrityManifestFileKey(repoRoot, layout, filePath);
|
|
56
|
+
files[manifestKey] = await sha256File(filePath);
|
|
42
57
|
}
|
|
43
58
|
const manifest = {
|
|
44
59
|
version: 1,
|
|
@@ -49,22 +64,47 @@ export async function writeIntegrityManifest(repoRoot, layout, filePaths) {
|
|
|
49
64
|
await mkdir(path.dirname(manifestPath), { recursive: true });
|
|
50
65
|
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
51
66
|
}
|
|
52
|
-
export async function verifyIntegrityManifest(repoRoot, layout) {
|
|
67
|
+
export async function verifyIntegrityManifest(repoRoot, layout, requiredFilePaths = []) {
|
|
53
68
|
const manifestPath = integrityManifestPath(layout, repoRoot);
|
|
54
69
|
if (!existsSync(manifestPath)) {
|
|
55
70
|
return { ok: false, mismatches: ['missing integrity-manifest.json'] };
|
|
56
71
|
}
|
|
57
72
|
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
58
73
|
const mismatches = [];
|
|
59
|
-
|
|
60
|
-
|
|
74
|
+
const requiredPathsByKey = new Map(requiredFilePaths.map((requiredPath) => [
|
|
75
|
+
integrityManifestFileKey(repoRoot, layout, requiredPath),
|
|
76
|
+
path.resolve(requiredPath),
|
|
77
|
+
]));
|
|
78
|
+
for (const requiredPath of requiredFilePaths) {
|
|
79
|
+
const requiredKey = integrityManifestFileKey(repoRoot, layout, requiredPath);
|
|
80
|
+
if (!Object.hasOwn(manifest.files ?? {}, requiredKey)) {
|
|
81
|
+
mismatches.push(`missing integrity pin ${requiredKey}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for (const [manifestKey, expectedHash] of Object.entries(manifest.files ?? {})) {
|
|
85
|
+
let absolutePath;
|
|
86
|
+
if (manifestKey.startsWith(GLOBAL_INTEGRITY_PREFIX)) {
|
|
87
|
+
const requiredPath = requiredPathsByKey.get(manifestKey);
|
|
88
|
+
if (!requiredPath) {
|
|
89
|
+
mismatches.push(`unrecognized global integrity pin ${manifestKey}`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
absolutePath = requiredPath;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
absolutePath = path.resolve(repoRoot, manifestKey);
|
|
96
|
+
if (!isPathInside(absolutePath, repoRoot)) {
|
|
97
|
+
mismatches.push(`integrity pin escapes repository ${manifestKey}`);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
61
101
|
if (!existsSync(absolutePath)) {
|
|
62
|
-
mismatches.push(`missing ${
|
|
102
|
+
mismatches.push(`missing ${manifestKey}`);
|
|
63
103
|
continue;
|
|
64
104
|
}
|
|
65
105
|
const actualHash = await sha256File(absolutePath);
|
|
66
106
|
if (actualHash !== expectedHash) {
|
|
67
|
-
mismatches.push(`hash mismatch ${
|
|
107
|
+
mismatches.push(`hash mismatch ${manifestKey}`);
|
|
68
108
|
}
|
|
69
109
|
}
|
|
70
110
|
return { ok: mismatches.length === 0, mismatches };
|
|
@@ -1,7 +1,10 @@
|
|
|
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;
|
|
6
|
+
/** Stable tool-input source for fingerprints — redacts volatile ids only, not secret values. */
|
|
7
|
+
export declare function fingerprintToolInputSource(payload: Record<string, unknown>): Record<string, unknown>;
|
|
5
8
|
/**
|
|
6
9
|
* Scrubbed payload used for replay envelope hashing — aligned with classifier fingerprints.
|
|
7
10
|
* Tool: scrubbed `tool_input`. Subagent: description/prompt subset. Other: full payload.
|
|
@@ -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;
|
|
@@ -22,6 +40,14 @@ export function subagentFingerprintSource(payload, scrubOptions) {
|
|
|
22
40
|
}
|
|
23
41
|
return scrubValue(payload, scrubOptions);
|
|
24
42
|
}
|
|
43
|
+
/** Stable tool-input source for fingerprints — redacts volatile ids only, not secret values. */
|
|
44
|
+
export function fingerprintToolInputSource(payload) {
|
|
45
|
+
const toolInput = payload.tool_input;
|
|
46
|
+
if (!toolInput || typeof toolInput !== 'object') {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
return redactToolInvocationId(toolInput, typeof payload.tool_use_id === 'string' ? payload.tool_use_id : undefined);
|
|
50
|
+
}
|
|
25
51
|
/**
|
|
26
52
|
* Scrubbed payload used for replay envelope hashing — aligned with classifier fingerprints.
|
|
27
53
|
* Tool: scrubbed `tool_input`. Subagent: description/prompt subset. Other: full payload.
|
|
@@ -30,14 +56,15 @@ export function fingerprintReplayPayload(kind, payload, scrubOptions) {
|
|
|
30
56
|
if (!payload) {
|
|
31
57
|
return undefined;
|
|
32
58
|
}
|
|
59
|
+
const replayPayload = redactToolInvocationId(payload, typeof payload.tool_use_id === 'string' ? payload.tool_use_id : undefined);
|
|
33
60
|
if (kind === 'tool') {
|
|
34
|
-
const toolInput =
|
|
61
|
+
const toolInput = replayPayload.tool_input;
|
|
35
62
|
if (toolInput && typeof toolInput === 'object') {
|
|
36
63
|
return scrubValue(toolInput, scrubOptions);
|
|
37
64
|
}
|
|
38
65
|
}
|
|
39
66
|
if (kind === 'subagent') {
|
|
40
|
-
return subagentFingerprintSource(
|
|
67
|
+
return subagentFingerprintSource(replayPayload, scrubOptions);
|
|
41
68
|
}
|
|
42
|
-
return scrubValue(
|
|
69
|
+
return scrubValue(replayPayload, scrubOptions);
|
|
43
70
|
}
|
|
@@ -16,7 +16,7 @@ export function findCommandSubstitutions(command) {
|
|
|
16
16
|
index += 1;
|
|
17
17
|
continue;
|
|
18
18
|
}
|
|
19
|
-
if (char === '\\' &&
|
|
19
|
+
if (char === '\\' && inDouble) {
|
|
20
20
|
escaping = true;
|
|
21
21
|
index += 1;
|
|
22
22
|
continue;
|
|
@@ -156,7 +156,7 @@ function extractBalancedParenContent(command, start) {
|
|
|
156
156
|
index += 1;
|
|
157
157
|
continue;
|
|
158
158
|
}
|
|
159
|
-
if (char === '\\' &&
|
|
159
|
+
if (char === '\\' && inDouble) {
|
|
160
160
|
escaping = true;
|
|
161
161
|
index += 1;
|
|
162
162
|
continue;
|
|
@@ -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
|
+
}
|