@ssweens/pi-leash 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/LICENSE +21 -0
- package/README.md +221 -0
- package/package.json +83 -0
- package/src/config.ts +285 -0
- package/src/hooks/index.ts +9 -0
- package/src/hooks/permission-gate.ts +925 -0
- package/src/hooks/policies.ts +315 -0
- package/src/index.ts +38 -0
- package/src/lib/executor.ts +280 -0
- package/src/lib/index.ts +16 -0
- package/src/lib/model-resolver.ts +47 -0
- package/src/lib/timing.ts +42 -0
- package/src/lib/types.ts +115 -0
- package/src/utils/events.ts +32 -0
- package/src/utils/glob-expander.ts +128 -0
- package/src/utils/matching.ts +111 -0
- package/src/utils/shell-utils.ts +139 -0
- package/src/vendor/aliou-sh/NOTICE.md +13 -0
- package/src/vendor/aliou-sh/ast.d.ts +186 -0
- package/src/vendor/aliou-sh/index.d.ts +3 -0
- package/src/vendor/aliou-sh/index.js +1397 -0
- package/src/vendor/aliou-sh/parse.d.ts +3 -0
- package/src/vendor/aliou-sh/upstream.package.json +55 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared timing utilities for tool and subagent execution.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Minimal shape that supports timing fields. */
|
|
6
|
+
export interface TimedExecution {
|
|
7
|
+
startedAt?: number;
|
|
8
|
+
endedAt?: number;
|
|
9
|
+
durationMs?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Mark execution start time (epoch ms). */
|
|
13
|
+
export function markExecutionStart<T extends TimedExecution>(
|
|
14
|
+
target: T,
|
|
15
|
+
startedAt = Date.now(),
|
|
16
|
+
): T {
|
|
17
|
+
target.startedAt = startedAt;
|
|
18
|
+
return target;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Mark execution end time and compute duration (epoch ms / ms). */
|
|
22
|
+
export function markExecutionEnd<T extends TimedExecution>(
|
|
23
|
+
target: T,
|
|
24
|
+
endedAt = Date.now(),
|
|
25
|
+
): T {
|
|
26
|
+
target.endedAt = endedAt;
|
|
27
|
+
if (target.startedAt !== undefined) {
|
|
28
|
+
target.durationMs = Math.max(0, endedAt - target.startedAt);
|
|
29
|
+
}
|
|
30
|
+
return target;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Simple wall-clock timer for a full operation (e.g., subagent call). */
|
|
34
|
+
export function createExecutionTimer(startedAt = Date.now()): {
|
|
35
|
+
startedAt: number;
|
|
36
|
+
getDurationMs: (endedAt?: number) => number;
|
|
37
|
+
} {
|
|
38
|
+
return {
|
|
39
|
+
startedAt,
|
|
40
|
+
getDurationMs: (endedAt = Date.now()) => Math.max(0, endedAt - startedAt),
|
|
41
|
+
};
|
|
42
|
+
}
|
package/src/lib/types.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { AgentTool, ThinkingLevel } from "@mariozechner/pi-agent-core";
|
|
2
|
+
import type { Model } from "@mariozechner/pi-ai";
|
|
3
|
+
import type { Skill, ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Configuration for a subagent.
|
|
7
|
+
*/
|
|
8
|
+
export interface SubagentConfig {
|
|
9
|
+
/** Subagent name (for logging and run ID) */
|
|
10
|
+
name: string;
|
|
11
|
+
|
|
12
|
+
/** Model instance to use */
|
|
13
|
+
// biome-ignore lint/suspicious/noExplicitAny: Model type requires any for generic API
|
|
14
|
+
model: Model<any>;
|
|
15
|
+
|
|
16
|
+
/** System prompt for the subagent */
|
|
17
|
+
systemPrompt: string;
|
|
18
|
+
|
|
19
|
+
/** Built-in tools (AgentTool[]) - e.g., from createReadOnlyTools() */
|
|
20
|
+
tools?: AgentTool[];
|
|
21
|
+
|
|
22
|
+
/** Custom tools (ToolDefinition[]) - e.g., GitHub tools */
|
|
23
|
+
customTools?: ToolDefinition[];
|
|
24
|
+
|
|
25
|
+
/** Skills to load into system prompt */
|
|
26
|
+
skills?: Skill[];
|
|
27
|
+
|
|
28
|
+
/** Thinking level. Default: "low" */
|
|
29
|
+
thinkingLevel?: ThinkingLevel;
|
|
30
|
+
|
|
31
|
+
/** Logging options */
|
|
32
|
+
logging?: {
|
|
33
|
+
/** Enable logging. Default: false */
|
|
34
|
+
enabled: boolean;
|
|
35
|
+
/** Include raw events in debug.jsonl. Default: false */
|
|
36
|
+
debug?: boolean;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Tool call state for tracking subagent tool executions.
|
|
42
|
+
*/
|
|
43
|
+
export interface SubagentToolCall {
|
|
44
|
+
toolCallId: string;
|
|
45
|
+
toolName: string;
|
|
46
|
+
args: Record<string, unknown>;
|
|
47
|
+
status: "running" | "done" | "error";
|
|
48
|
+
/** Epoch ms when tool execution started */
|
|
49
|
+
startedAt?: number;
|
|
50
|
+
/** Epoch ms when tool execution ended */
|
|
51
|
+
endedAt?: number;
|
|
52
|
+
/** Duration in milliseconds (set when ended) */
|
|
53
|
+
durationMs?: number;
|
|
54
|
+
result?: unknown;
|
|
55
|
+
error?: string;
|
|
56
|
+
/** Partial result from tool updates (for progress display) */
|
|
57
|
+
partialResult?: {
|
|
58
|
+
content: Array<{ type: string; text?: string }>;
|
|
59
|
+
details?: unknown;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Usage/cost information from the model response.
|
|
65
|
+
*/
|
|
66
|
+
export interface SubagentUsage {
|
|
67
|
+
/** Input tokens from API (if available) */
|
|
68
|
+
inputTokens?: number;
|
|
69
|
+
/** Output tokens from API (if available) */
|
|
70
|
+
outputTokens?: number;
|
|
71
|
+
/** Cache read tokens (if available) */
|
|
72
|
+
cacheReadTokens?: number;
|
|
73
|
+
/** Cache write tokens (if available) */
|
|
74
|
+
cacheWriteTokens?: number;
|
|
75
|
+
/** Estimated tokens from response length (chars/4) */
|
|
76
|
+
estimatedTokens: number;
|
|
77
|
+
/** LLM cost in USD (if available) */
|
|
78
|
+
llmCost?: number;
|
|
79
|
+
/** Tool/API cost in USD (e.g., Exa, GitHub) */
|
|
80
|
+
toolCost?: number;
|
|
81
|
+
/** Total cost in USD (llmCost + toolCost) */
|
|
82
|
+
totalCost?: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Result from executing a subagent.
|
|
87
|
+
*/
|
|
88
|
+
export interface SubagentResult {
|
|
89
|
+
/** Final text content from the subagent */
|
|
90
|
+
content: string;
|
|
91
|
+
|
|
92
|
+
/** Whether the subagent was aborted */
|
|
93
|
+
aborted: boolean;
|
|
94
|
+
|
|
95
|
+
/** Final tool call states */
|
|
96
|
+
toolCalls: SubagentToolCall[];
|
|
97
|
+
|
|
98
|
+
/** Total subagent execution duration in milliseconds */
|
|
99
|
+
totalDurationMs: number;
|
|
100
|
+
|
|
101
|
+
/** Error message if the subagent failed */
|
|
102
|
+
error?: string;
|
|
103
|
+
|
|
104
|
+
/** Unique run identifier */
|
|
105
|
+
runId: string;
|
|
106
|
+
|
|
107
|
+
/** Usage/cost information */
|
|
108
|
+
usage: SubagentUsage;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Callback for text streaming updates */
|
|
112
|
+
export type OnTextUpdate = (delta: string, accumulated: string) => void;
|
|
113
|
+
|
|
114
|
+
/** Callback for tool execution updates */
|
|
115
|
+
export type OnToolUpdate = (toolCalls: SubagentToolCall[]) => void;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export const LEASH_BLOCKED_EVENT = "leash:blocked";
|
|
4
|
+
export const LEASH_DANGEROUS_EVENT = "leash:dangerous";
|
|
5
|
+
|
|
6
|
+
export interface LeashBlockedEvent {
|
|
7
|
+
feature: "policies" | "permissionGate";
|
|
8
|
+
toolName: string;
|
|
9
|
+
input: Record<string, unknown>;
|
|
10
|
+
reason: string;
|
|
11
|
+
userDenied?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface LeashDangerousEvent {
|
|
15
|
+
command: string;
|
|
16
|
+
description: string;
|
|
17
|
+
pattern: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function emitBlocked(
|
|
21
|
+
pi: ExtensionAPI,
|
|
22
|
+
event: LeashBlockedEvent,
|
|
23
|
+
): void {
|
|
24
|
+
pi.events.emit(LEASH_BLOCKED_EVENT, event);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function emitDangerous(
|
|
28
|
+
pi: ExtensionAPI,
|
|
29
|
+
event: LeashDangerousEvent,
|
|
30
|
+
): void {
|
|
31
|
+
pi.events.emit(LEASH_DANGEROUS_EVENT, event);
|
|
32
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Glob expansion using `fd` for env file protection.
|
|
3
|
+
*
|
|
4
|
+
* When a bash command contains shell globs referencing env files
|
|
5
|
+
* (e.g. `.env*`), we expand them against the filesystem to check
|
|
6
|
+
* if any expanded path matches a protected pattern.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execFile } from "node:child_process";
|
|
10
|
+
import { resolve } from "node:path";
|
|
11
|
+
|
|
12
|
+
interface ExpandGlobOptions {
|
|
13
|
+
cwd?: string;
|
|
14
|
+
maxDepth?: number;
|
|
15
|
+
maxResults?: number;
|
|
16
|
+
timeout?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Expand a glob pattern using `fd`.
|
|
21
|
+
* Returns matching file paths, or empty array on failure.
|
|
22
|
+
*
|
|
23
|
+
* fd is available at `~/.pi/agent/bin/fd` (in pi's PATH).
|
|
24
|
+
*/
|
|
25
|
+
export async function expandGlob(
|
|
26
|
+
pattern: string,
|
|
27
|
+
options: ExpandGlobOptions = {},
|
|
28
|
+
): Promise<string[]> {
|
|
29
|
+
const {
|
|
30
|
+
cwd = process.cwd(),
|
|
31
|
+
maxDepth = 3,
|
|
32
|
+
maxResults = 50,
|
|
33
|
+
timeout = 2000,
|
|
34
|
+
} = options;
|
|
35
|
+
|
|
36
|
+
// Convert glob to fd-compatible regex.
|
|
37
|
+
// fd uses regex by default, so we convert glob chars.
|
|
38
|
+
const fdPattern = globToFdRegex(pattern);
|
|
39
|
+
|
|
40
|
+
return new Promise((res) => {
|
|
41
|
+
const args = [
|
|
42
|
+
"--type",
|
|
43
|
+
"f",
|
|
44
|
+
"--max-depth",
|
|
45
|
+
String(maxDepth),
|
|
46
|
+
"--max-results",
|
|
47
|
+
String(maxResults),
|
|
48
|
+
"--no-ignore",
|
|
49
|
+
"--hidden",
|
|
50
|
+
fdPattern,
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const child = execFile("fd", args, { cwd, timeout }, (err, stdout) => {
|
|
54
|
+
if (err) {
|
|
55
|
+
res([]);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const files = stdout
|
|
60
|
+
.trim()
|
|
61
|
+
.split("\n")
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.map((f) => resolve(cwd, f));
|
|
64
|
+
|
|
65
|
+
res(files);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Safety net: kill if timeout isn't handled by execFile
|
|
69
|
+
setTimeout(() => {
|
|
70
|
+
child.kill();
|
|
71
|
+
res([]);
|
|
72
|
+
}, timeout + 500);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Convert a shell glob to an fd-compatible regex pattern.
|
|
78
|
+
* Handles `*`, `?`, and character classes `[...]`.
|
|
79
|
+
*/
|
|
80
|
+
function globToFdRegex(glob: string): string {
|
|
81
|
+
let regex = "";
|
|
82
|
+
let i = 0;
|
|
83
|
+
while (i < glob.length) {
|
|
84
|
+
const ch = glob[i] as string;
|
|
85
|
+
switch (ch) {
|
|
86
|
+
case "*":
|
|
87
|
+
regex += "[^/]*";
|
|
88
|
+
break;
|
|
89
|
+
case "?":
|
|
90
|
+
regex += "[^/]";
|
|
91
|
+
break;
|
|
92
|
+
case "[": {
|
|
93
|
+
// Pass character classes through
|
|
94
|
+
const end = glob.indexOf("]", i + 1);
|
|
95
|
+
if (end !== -1) {
|
|
96
|
+
regex += glob.slice(i, end + 1);
|
|
97
|
+
i = end;
|
|
98
|
+
} else {
|
|
99
|
+
regex += "\\[";
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
case ".":
|
|
104
|
+
case "(":
|
|
105
|
+
case ")":
|
|
106
|
+
case "+":
|
|
107
|
+
case "^":
|
|
108
|
+
case "$":
|
|
109
|
+
case "{":
|
|
110
|
+
case "}":
|
|
111
|
+
case "|":
|
|
112
|
+
case "\\":
|
|
113
|
+
regex += `\\${ch}`;
|
|
114
|
+
break;
|
|
115
|
+
default:
|
|
116
|
+
regex += ch;
|
|
117
|
+
}
|
|
118
|
+
i++;
|
|
119
|
+
}
|
|
120
|
+
return `^${regex}$`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Check if a string contains shell glob characters.
|
|
125
|
+
*/
|
|
126
|
+
export function hasGlobChars(s: string): boolean {
|
|
127
|
+
return /[*?[\]]/.test(s);
|
|
128
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern compilation for guardrails matching.
|
|
3
|
+
*
|
|
4
|
+
* Two contexts with different default semantics:
|
|
5
|
+
* - File context: default is glob matching against filename.
|
|
6
|
+
* - Command context: default is substring matching against raw command string.
|
|
7
|
+
*
|
|
8
|
+
* Both support `regex: true` for full regex matching.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { matchesGlob } from "node:path";
|
|
12
|
+
import type { PatternConfig } from "../config";
|
|
13
|
+
|
|
14
|
+
export interface CompiledPattern {
|
|
15
|
+
test: (input: string) => boolean;
|
|
16
|
+
source: PatternConfig;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Normalize file paths before matching.
|
|
21
|
+
* - Use forward slashes for cross-platform consistency.
|
|
22
|
+
* - Drop leading "./" segments.
|
|
23
|
+
* - Collapse duplicate slashes.
|
|
24
|
+
*/
|
|
25
|
+
export function normalizeFilePath(input: string): string {
|
|
26
|
+
const normalized = input
|
|
27
|
+
.replaceAll("\\", "/")
|
|
28
|
+
.replace(/^(?:\.\/)+/, "")
|
|
29
|
+
.replace(/\/{2,}/g, "/");
|
|
30
|
+
return normalized;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Compile a single pattern for file-context matching.
|
|
35
|
+
* Default: glob matching.
|
|
36
|
+
* - If pattern includes `/`, match full normalized relative path.
|
|
37
|
+
* - Otherwise, match basename only (backward compatible).
|
|
38
|
+
* regex: true -> full regex (case-insensitive) against normalized path.
|
|
39
|
+
*/
|
|
40
|
+
export function compileFilePattern(config: PatternConfig): CompiledPattern {
|
|
41
|
+
if (config.regex) {
|
|
42
|
+
try {
|
|
43
|
+
const re = new RegExp(config.pattern, "i");
|
|
44
|
+
return {
|
|
45
|
+
test: (input) => re.test(normalizeFilePath(input)),
|
|
46
|
+
source: config,
|
|
47
|
+
};
|
|
48
|
+
} catch {
|
|
49
|
+
console.warn(
|
|
50
|
+
`[pi-leash] Invalid regex in config: ${config.pattern}`,
|
|
51
|
+
);
|
|
52
|
+
return { test: () => false, source: config };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const matchFullPath = config.pattern.includes("/");
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
test: (input) => {
|
|
60
|
+
const normalized = normalizeFilePath(input);
|
|
61
|
+
const candidate = matchFullPath
|
|
62
|
+
? normalized
|
|
63
|
+
: (normalized.split("/").pop() ?? normalized);
|
|
64
|
+
|
|
65
|
+
return matchesGlob(candidate, config.pattern);
|
|
66
|
+
},
|
|
67
|
+
source: config,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Compile a single pattern for command-context matching.
|
|
73
|
+
* Default: substring match against raw command string.
|
|
74
|
+
* regex: true -> full regex against raw command string.
|
|
75
|
+
*/
|
|
76
|
+
export function compileCommandPattern(config: PatternConfig): CompiledPattern {
|
|
77
|
+
if (config.regex) {
|
|
78
|
+
try {
|
|
79
|
+
const re = new RegExp(config.pattern);
|
|
80
|
+
return { test: (input) => re.test(input), source: config };
|
|
81
|
+
} catch {
|
|
82
|
+
console.warn(
|
|
83
|
+
`[pi-leash] Invalid regex in config: ${config.pattern}`,
|
|
84
|
+
);
|
|
85
|
+
return { test: () => false, source: config };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
test: (input) => input.includes(config.pattern),
|
|
91
|
+
source: config,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Compile an array of patterns for file-context matching.
|
|
97
|
+
*/
|
|
98
|
+
export function compileFilePatterns(
|
|
99
|
+
configs: PatternConfig[],
|
|
100
|
+
): CompiledPattern[] {
|
|
101
|
+
return configs.map(compileFilePattern);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Compile an array of patterns for command-context matching.
|
|
106
|
+
*/
|
|
107
|
+
export function compileCommandPatterns(
|
|
108
|
+
configs: PatternConfig[],
|
|
109
|
+
): CompiledPattern[] {
|
|
110
|
+
return configs.map(compileCommandPattern);
|
|
111
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared shell AST helpers used by guardrails hooks.
|
|
3
|
+
*
|
|
4
|
+
* Each hook imports `parse` from the vendored shell parser and uses these
|
|
5
|
+
* for common AST operations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
Command,
|
|
10
|
+
Program,
|
|
11
|
+
SimpleCommand,
|
|
12
|
+
Statement,
|
|
13
|
+
Word,
|
|
14
|
+
WordPart,
|
|
15
|
+
} from "../vendor/aliou-sh/index.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a Word node to its literal string value.
|
|
19
|
+
* Concatenates Literal, SglQuoted, and simple DblQuoted parts.
|
|
20
|
+
* For parts containing parameter expansions, command substitutions, etc.,
|
|
21
|
+
* includes the raw text representation (e.g. `$VAR`).
|
|
22
|
+
*/
|
|
23
|
+
export function wordToString(word: Word): string {
|
|
24
|
+
return word.parts.map(partToString).join("");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function partToString(part: WordPart): string {
|
|
28
|
+
switch (part.type) {
|
|
29
|
+
case "Literal":
|
|
30
|
+
return part.value;
|
|
31
|
+
case "SglQuoted":
|
|
32
|
+
return part.value;
|
|
33
|
+
case "DblQuoted":
|
|
34
|
+
return part.parts.map(partToString).join("");
|
|
35
|
+
case "ParamExp":
|
|
36
|
+
return part.short
|
|
37
|
+
? `$${part.param.value}`
|
|
38
|
+
: `\${${part.param.value}${part.op ?? ""}${part.value ? wordToString(part.value) : ""}}`;
|
|
39
|
+
case "CmdSubst":
|
|
40
|
+
return "$(...)";
|
|
41
|
+
case "ArithExp":
|
|
42
|
+
return `$((${part.expr}))`;
|
|
43
|
+
case "ProcSubst":
|
|
44
|
+
return `${part.op}(...)`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Walk the AST and call `callback` for every SimpleCommand found at any
|
|
50
|
+
* nesting depth. Returns early if callback returns `true`.
|
|
51
|
+
*/
|
|
52
|
+
export function walkCommands(
|
|
53
|
+
node: Program,
|
|
54
|
+
callback: (cmd: SimpleCommand) => boolean | undefined,
|
|
55
|
+
): void {
|
|
56
|
+
for (const stmt of node.body) {
|
|
57
|
+
if (walkStatement(stmt, callback)) return;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function walkStatement(
|
|
62
|
+
stmt: Statement,
|
|
63
|
+
callback: (cmd: SimpleCommand) => boolean | undefined,
|
|
64
|
+
): boolean {
|
|
65
|
+
return walkCommand(stmt.command, callback);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function walkStatements(
|
|
69
|
+
stmts: Statement[],
|
|
70
|
+
callback: (cmd: SimpleCommand) => boolean | undefined,
|
|
71
|
+
): boolean {
|
|
72
|
+
for (const stmt of stmts) {
|
|
73
|
+
if (walkStatement(stmt, callback)) return true;
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function walkCommand(
|
|
79
|
+
cmd: Command,
|
|
80
|
+
callback: (cmd: SimpleCommand) => boolean | undefined,
|
|
81
|
+
): boolean {
|
|
82
|
+
switch (cmd.type) {
|
|
83
|
+
case "SimpleCommand":
|
|
84
|
+
return callback(cmd) === true;
|
|
85
|
+
|
|
86
|
+
case "Pipeline":
|
|
87
|
+
return walkStatements(cmd.commands, callback);
|
|
88
|
+
|
|
89
|
+
case "Logical":
|
|
90
|
+
return (
|
|
91
|
+
walkStatement(cmd.left, callback) || walkStatement(cmd.right, callback)
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
case "Subshell":
|
|
95
|
+
case "Block":
|
|
96
|
+
return walkStatements(cmd.body, callback);
|
|
97
|
+
|
|
98
|
+
case "IfClause":
|
|
99
|
+
return (
|
|
100
|
+
walkStatements(cmd.cond, callback) ||
|
|
101
|
+
walkStatements(cmd.then, callback) ||
|
|
102
|
+
(cmd.else ? walkStatements(cmd.else, callback) : false)
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
case "ForClause":
|
|
106
|
+
case "SelectClause":
|
|
107
|
+
case "WhileClause":
|
|
108
|
+
return (
|
|
109
|
+
("cond" in cmd && cmd.cond
|
|
110
|
+
? walkStatements(cmd.cond, callback)
|
|
111
|
+
: false) || walkStatements(cmd.body, callback)
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
case "CaseClause":
|
|
115
|
+
for (const item of cmd.items) {
|
|
116
|
+
if (walkStatements(item.body, callback)) return true;
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
|
|
120
|
+
case "FunctionDecl":
|
|
121
|
+
return walkStatements(cmd.body, callback);
|
|
122
|
+
|
|
123
|
+
case "TimeClause":
|
|
124
|
+
return walkStatement(cmd.command, callback);
|
|
125
|
+
|
|
126
|
+
case "CoprocClause":
|
|
127
|
+
return walkStatement(cmd.body, callback);
|
|
128
|
+
|
|
129
|
+
case "CStyleLoop":
|
|
130
|
+
return walkStatements(cmd.body, callback);
|
|
131
|
+
|
|
132
|
+
// These don't contain nested commands we need to walk
|
|
133
|
+
case "TestClause":
|
|
134
|
+
case "ArithCmd":
|
|
135
|
+
case "DeclClause":
|
|
136
|
+
case "LetClause":
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Vendored from `@aliou/sh@0.1.0` (MIT).
|
|
2
|
+
|
|
3
|
+
Source tarball:
|
|
4
|
+
https://registry.npmjs.org/@aliou/sh/-/sh-0.1.0.tgz
|
|
5
|
+
|
|
6
|
+
Files copied into this directory:
|
|
7
|
+
- index.js
|
|
8
|
+
- index.d.ts
|
|
9
|
+
- parse.d.ts
|
|
10
|
+
- ast.d.ts
|
|
11
|
+
- upstream.package.json
|
|
12
|
+
|
|
13
|
+
This vendoring removes runtime dependency on external module resolution for local path installs.
|