@herbertgao/pi-extensions 2026.9.4 → 2026.9.6

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 (146) hide show
  1. package/README.md +3 -15
  2. package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
  3. package/node_modules/@dietrichgebert/ponytail/README.es.md +33 -2
  4. package/node_modules/@dietrichgebert/ponytail/README.ko.md +33 -2
  5. package/node_modules/@dietrichgebert/ponytail/README.md +49 -5
  6. package/node_modules/@dietrichgebert/ponytail/assets/retriever-icon.png +0 -0
  7. package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-dark.svg +4 -0
  8. package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-light.svg +4 -0
  9. package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +0 -3
  10. package/node_modules/@dietrichgebert/ponytail/hooks/cursor-hooks.json +17 -0
  11. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +22 -3
  12. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-mode-tracker.js +27 -2
  13. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +61 -2
  14. package/node_modules/@dietrichgebert/ponytail/package.json +2 -1
  15. package/node_modules/@dietrichgebert/ponytail/scripts/cursor-hooks.js +135 -0
  16. package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +21 -4
  17. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/index.ts +33 -15
  18. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +6 -2
  19. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/scroll.ts +23 -1
  20. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +69 -14
  21. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  22. package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +1 -1
  23. package/node_modules/@herbertgao/sol-pi/package.json +1 -1
  24. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/index.ts +0 -5
  25. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/economics.ts +4 -4
  26. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +41 -1
  27. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/state.ts +3 -1
  28. package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +5 -1
  29. package/node_modules/@juicesharp/rpiv-ask-user-question/events.ts +5 -1
  30. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +3 -2
  31. package/node_modules/@juicesharp/rpiv-ask-user-question/tool/normalize-params.ts +56 -0
  32. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/option-list-view.ts +1 -1
  33. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/tab-bar.ts +1 -2
  34. package/node_modules/@pi-plugins/fast-mode/README.md +31 -44
  35. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +20914 -30
  36. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
  37. package/node_modules/@pi-plugins/fast-mode/package.json +5 -7
  38. package/node_modules/@tifan/pi-copy-response/README.md +2 -2
  39. package/node_modules/@tifan/pi-copy-response/package.json +3 -2
  40. package/node_modules/@tifan/pi-inline-skills/README.md +2 -2
  41. package/node_modules/@tifan/pi-inline-skills/package.json +3 -2
  42. package/node_modules/@tifan/pi-mermaid-open/README.md +1 -1
  43. package/node_modules/@tifan/pi-mermaid-open/package.json +3 -2
  44. package/node_modules/@tifan/pi-preferred-thinking/package.json +3 -2
  45. package/node_modules/@tifan/pi-recap/package.json +3 -2
  46. package/node_modules/pi-mcp-adapter/CHANGELOG.md +78 -0
  47. package/node_modules/pi-mcp-adapter/README.md +125 -19
  48. package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +69 -30
  49. package/node_modules/pi-mcp-adapter/agent-plugin-provenance.ts +36 -0
  50. package/node_modules/pi-mcp-adapter/claude-plugin-loader.ts +365 -0
  51. package/node_modules/pi-mcp-adapter/commands.ts +13 -15
  52. package/node_modules/pi-mcp-adapter/config.ts +198 -30
  53. package/node_modules/pi-mcp-adapter/consent-manager.ts +40 -9
  54. package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +250 -0
  55. package/node_modules/pi-mcp-adapter/direct-tools.ts +9 -228
  56. package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js +74 -32
  57. package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js.map +1 -1
  58. package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.d.ts +1 -0
  59. package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js +31 -0
  60. package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js.map +1 -0
  61. package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.d.ts +9 -0
  62. package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js +362 -0
  63. package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js.map +1 -0
  64. package/node_modules/pi-mcp-adapter/dist/config.d.ts +3 -1
  65. package/node_modules/pi-mcp-adapter/dist/config.js +189 -27
  66. package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
  67. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +7 -16
  68. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
  69. package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js +5 -17
  70. package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js.map +1 -1
  71. package/node_modules/pi-mcp-adapter/dist/types.d.ts +30 -4
  72. package/node_modules/pi-mcp-adapter/dist/types.js +26 -0
  73. package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
  74. package/node_modules/pi-mcp-adapter/dist/utils.d.ts +6 -0
  75. package/node_modules/pi-mcp-adapter/dist/utils.js +65 -6
  76. package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
  77. package/node_modules/pi-mcp-adapter/host-html-template.ts +20 -19
  78. package/node_modules/pi-mcp-adapter/http-ca.ts +84 -0
  79. package/node_modules/pi-mcp-adapter/index.ts +827 -164
  80. package/node_modules/pi-mcp-adapter/init.ts +48 -10
  81. package/node_modules/pi-mcp-adapter/lazy-loader.ts +7 -0
  82. package/node_modules/pi-mcp-adapter/mcp-auth-fetch.ts +113 -0
  83. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +256 -77
  84. package/node_modules/pi-mcp-adapter/mcp-auth.ts +316 -54
  85. package/node_modules/pi-mcp-adapter/mcp-code.ts +45 -18
  86. package/node_modules/pi-mcp-adapter/mcp-install.ts +60 -0
  87. package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +70 -6
  88. package/node_modules/pi-mcp-adapter/mcp-output-guard.ts +53 -42
  89. package/node_modules/pi-mcp-adapter/mcp-panel-theme.ts +104 -0
  90. package/node_modules/pi-mcp-adapter/mcp-panel.ts +366 -332
  91. package/node_modules/pi-mcp-adapter/mcp-references.ts +9 -14
  92. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +3 -1
  93. package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +383 -341
  94. package/node_modules/pi-mcp-adapter/mcp-status.ts +5 -0
  95. package/node_modules/pi-mcp-adapter/metadata-cache.ts +7 -16
  96. package/node_modules/pi-mcp-adapter/namespace-tools.ts +61 -3
  97. package/node_modules/pi-mcp-adapter/oauth.ts +6 -2
  98. package/node_modules/pi-mcp-adapter/package-mcp-loader.ts +5 -17
  99. package/node_modules/pi-mcp-adapter/package.json +13 -3
  100. package/node_modules/pi-mcp-adapter/prompts.ts +35 -3
  101. package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -7
  102. package/node_modules/pi-mcp-adapter/runtime-owner.ts +16 -1
  103. package/node_modules/pi-mcp-adapter/sampling-handler.ts +9 -32
  104. package/node_modules/pi-mcp-adapter/sandbox-proxy-template.ts +20 -93
  105. package/node_modules/pi-mcp-adapter/server-manager.ts +227 -40
  106. package/node_modules/pi-mcp-adapter/session-approvals.ts +187 -0
  107. package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +9 -1
  108. package/node_modules/pi-mcp-adapter/state.ts +10 -1
  109. package/node_modules/pi-mcp-adapter/tool-approval.ts +10 -22
  110. package/node_modules/pi-mcp-adapter/tool-metadata.ts +14 -0
  111. package/node_modules/pi-mcp-adapter/types.ts +57 -4
  112. package/node_modules/pi-mcp-adapter/ui-server.ts +36 -28
  113. package/node_modules/pi-mcp-adapter/utils.ts +64 -6
  114. package/package.json +13 -21
  115. package/examples/pi-footer.json +0 -275
  116. package/node_modules/@czottmann/pi-automode/CHANGELOG.md +0 -70
  117. package/node_modules/@czottmann/pi-automode/LICENSE.md +0 -21
  118. package/node_modules/@czottmann/pi-automode/README.md +0 -215
  119. package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +0 -39
  120. package/node_modules/@czottmann/pi-automode/docs/adr/ADR-001-permission-precedence-and-trust-boundaries.md +0 -46
  121. package/node_modules/@czottmann/pi-automode/docs/adr/ADR-002-global-config-in-extension-data-directory.md +0 -60
  122. package/node_modules/@czottmann/pi-automode/docs/adr/INDEX.md +0 -6
  123. package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +0 -428
  124. package/node_modules/@czottmann/pi-automode/docs/configuration.md +0 -171
  125. package/node_modules/@czottmann/pi-automode/docs/defaults.md +0 -178
  126. package/node_modules/@czottmann/pi-automode/docs/diagnostics.md +0 -90
  127. package/node_modules/@czottmann/pi-automode/docs/observability-logging.md +0 -160
  128. package/node_modules/@czottmann/pi-automode/examples/automode.local.json +0 -44
  129. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/bash.ts +0 -692
  130. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +0 -852
  131. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +0 -904
  132. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +0 -228
  133. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +0 -954
  134. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/hard-deny.ts +0 -429
  135. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/log.ts +0 -173
  136. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/model-selector.ts +0 -113
  137. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/model.ts +0 -13
  138. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/paths.ts +0 -297
  139. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/permissions.ts +0 -623
  140. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/state.ts +0 -102
  141. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/transcript.ts +0 -236
  142. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/types.ts +0 -205
  143. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/utils.ts +0 -54
  144. package/node_modules/@czottmann/pi-automode/extensions/auto-mode.ts +0 -26
  145. package/node_modules/@czottmann/pi-automode/package.json +0 -61
  146. package/node_modules/@czottmann/pi-automode/skills/automode-diagnostics/SKILL.md +0 -63
@@ -1,692 +0,0 @@
1
- import { basename } from "node:path";
2
- import {
3
- parse,
4
- type ArithmeticExpression,
5
- type Node,
6
- type ParseError,
7
- type ParsedScript,
8
- type Redirect,
9
- type TestExpression,
10
- type Word,
11
- type WordPart,
12
- } from "unbash";
13
-
14
- export type BashAnalysisError = {
15
- message: string;
16
- pos?: number;
17
- };
18
-
19
- export type BashRedirectAnalysis = {
20
- operator: Redirect["operator"];
21
- target?: string;
22
- targetDynamic: boolean;
23
- fileDescriptor?: number;
24
- variableName?: string;
25
- heredoc: boolean;
26
- };
27
-
28
- export type EffectiveCommand = {
29
- name?: string;
30
- args: string[];
31
- argTexts: string[];
32
- argTildeExpansions: boolean[];
33
- unresolvedTransparentDispatch: boolean;
34
- };
35
-
36
- export type BashCommandAnalysis = {
37
- raw: string;
38
- text: string;
39
- name?: string;
40
- words: string[];
41
- args: string[];
42
- argTexts: string[];
43
- effectiveCommand: EffectiveCommand;
44
- redirects: BashRedirectAnalysis[];
45
- redirectTargets: string[];
46
- dynamic: boolean;
47
- dynamicName: boolean;
48
- dynamicShellScript: boolean;
49
- pos: number;
50
- end: number;
51
- };
52
-
53
- export type BashAnalysis = {
54
- source: string;
55
- commands: BashCommandAnalysis[];
56
- redirects: BashRedirectAnalysis[];
57
- redirectTargets: string[];
58
- structure: string[];
59
- allowStructureSafe: boolean;
60
- errors: BashAnalysisError[];
61
- };
62
-
63
- export type BashParser = (source: string) => ParsedScript;
64
-
65
- export const MAX_BASH_SOURCE_LENGTH = 1024 * 1024;
66
- const MAX_NESTED_SHELL_DEPTH = 16;
67
-
68
- function parseError(error: ParseError): BashAnalysisError {
69
- return { message: error.message, pos: error.pos };
70
- }
71
-
72
- function parserException(error: unknown): BashAnalysisError {
73
- return {
74
- message: `Bash parser failed: ${
75
- error instanceof Error ? error.message : String(error)
76
- }`,
77
- };
78
- }
79
-
80
- function wordParts(word: Word): WordPart[] {
81
- return word.parts ?? [];
82
- }
83
-
84
- function wordIsStatic(word: Word): boolean {
85
- const parts = wordParts(word);
86
- if (parts.length === 0) return true;
87
- return parts.every(partIsStatic);
88
- }
89
-
90
- function wordHasTildeExpansion(word: Word): boolean {
91
- if (!word.text.startsWith("~")) return false;
92
- for (const character of word.text.slice(1)) {
93
- if (character === "/") return true;
94
- if (["\\", "'", '"', "$", "`"].includes(character)) return false;
95
- }
96
- return true;
97
- }
98
-
99
- function partIsStatic(part: WordPart): boolean {
100
- switch (part.type) {
101
- case "Literal":
102
- case "SingleQuoted":
103
- case "AnsiCQuoted":
104
- return true;
105
- case "DoubleQuoted":
106
- return part.parts.every((child) => child.type === "Literal");
107
- case "LocaleString":
108
- case "SimpleExpansion":
109
- case "ParameterExpansion":
110
- case "CommandExpansion":
111
- case "ArithmeticExpansion":
112
- case "ProcessSubstitution":
113
- case "ExtendedGlob":
114
- case "BraceExpansion":
115
- return false;
116
- }
117
- }
118
-
119
- function commandName(value: string | undefined): string | undefined {
120
- if (!value) return undefined;
121
- return basename(value).toLowerCase();
122
- }
123
-
124
- type CommandInvocation = {
125
- name?: string;
126
- argumentWords: Word[];
127
- unresolvedTransparentDispatch: boolean;
128
- };
129
-
130
- function unwrapTransparentCommandOnce(
131
- name: string | undefined,
132
- argumentWords: Word[],
133
- ): CommandInvocation {
134
- if (name !== "command" && name !== "exec" && name !== "env") {
135
- return { name, argumentWords, unresolvedTransparentDispatch: false };
136
- }
137
-
138
- let index = 0;
139
- let optionsEnded = false;
140
- while (index < argumentWords.length) {
141
- const word = argumentWords[index];
142
- if (!word || !wordIsStatic(word)) {
143
- return {
144
- argumentWords: [],
145
- unresolvedTransparentDispatch: true,
146
- };
147
- }
148
- const value = word.value;
149
- if (!optionsEnded && value === "--") {
150
- optionsEnded = true;
151
- index += 1;
152
- continue;
153
- }
154
-
155
- if (name === "command" && !optionsEnded) {
156
- if (value === "-p") {
157
- index += 1;
158
- continue;
159
- }
160
- if (value === "-v" || value === "-V" || value.startsWith("-")) {
161
- return { argumentWords: [], unresolvedTransparentDispatch: true };
162
- }
163
- }
164
-
165
- if (name === "exec" && !optionsEnded) {
166
- if (value === "-a") {
167
- const optionValue = argumentWords[index + 1];
168
- if (!optionValue || !wordIsStatic(optionValue)) {
169
- return { argumentWords: [], unresolvedTransparentDispatch: true };
170
- }
171
- index += 2;
172
- continue;
173
- }
174
- if (/^-[cl]+$/.test(value)) {
175
- index += 1;
176
- continue;
177
- }
178
- if (value.startsWith("-")) {
179
- return { argumentWords: [], unresolvedTransparentDispatch: true };
180
- }
181
- }
182
-
183
- if (name === "env") {
184
- if (!optionsEnded && value.startsWith("-")) {
185
- if (
186
- value === "-i" ||
187
- value === "--ignore-environment" ||
188
- value === "-0" ||
189
- value === "--null" ||
190
- value === "-v" ||
191
- value === "--debug"
192
- ) {
193
- index += 1;
194
- continue;
195
- }
196
- if (["-u", "--unset", "-C", "--chdir"].includes(value)) {
197
- const optionValue = argumentWords[index + 1];
198
- if (!optionValue || !wordIsStatic(optionValue)) {
199
- return { argumentWords: [], unresolvedTransparentDispatch: true };
200
- }
201
- index += 2;
202
- continue;
203
- }
204
- if (value.startsWith("--unset=") || value.startsWith("--chdir=")) {
205
- index += 1;
206
- continue;
207
- }
208
- return { argumentWords: [], unresolvedTransparentDispatch: true };
209
- }
210
- if (/^[^=]+=/.test(value)) {
211
- index += 1;
212
- continue;
213
- }
214
- }
215
-
216
- return {
217
- name: commandName(value),
218
- argumentWords: argumentWords.slice(index + 1),
219
- unresolvedTransparentDispatch: false,
220
- };
221
- }
222
-
223
- return { argumentWords: [], unresolvedTransparentDispatch: true };
224
- }
225
-
226
- function effectiveCommandInvocation(
227
- name: string | undefined,
228
- argumentWords: Word[],
229
- ): CommandInvocation {
230
- let invocation: CommandInvocation = {
231
- name,
232
- argumentWords,
233
- unresolvedTransparentDispatch: false,
234
- };
235
- for (let depth = 0; depth < MAX_NESTED_SHELL_DEPTH; depth += 1) {
236
- const next = unwrapTransparentCommandOnce(
237
- invocation.name,
238
- invocation.argumentWords,
239
- );
240
- if (next.unresolvedTransparentDispatch) return next;
241
- if (
242
- next.name === invocation.name &&
243
- next.argumentWords === invocation.argumentWords
244
- ) {
245
- return next;
246
- }
247
- invocation = next;
248
- }
249
- return { argumentWords: [], unresolvedTransparentDispatch: true };
250
- }
251
-
252
- type NestedShell = {
253
- name?: string;
254
- source?: string;
255
- hasScriptArgument: boolean;
256
- };
257
-
258
- function nestedShell(invocation: CommandInvocation): NestedShell | undefined {
259
- if (invocation.unresolvedTransparentDispatch) {
260
- return { hasScriptArgument: true };
261
- }
262
- if (invocation.name === "eval") {
263
- return {
264
- name: "eval",
265
- source:
266
- invocation.argumentWords.length > 0 &&
267
- invocation.argumentWords.every(wordIsStatic)
268
- ? invocation.argumentWords.map((word) => word.value).join(" ")
269
- : undefined,
270
- hasScriptArgument: invocation.argumentWords.length > 0,
271
- };
272
- }
273
- if (invocation.name !== "bash" && invocation.name !== "sh") {
274
- return undefined;
275
- }
276
- const commandOptionIndex = invocation.argumentWords.findIndex((word) =>
277
- /^-[^-]*c/.test(word.value)
278
- );
279
- if (commandOptionIndex < 0) return undefined;
280
- const scriptWord = invocation.argumentWords[commandOptionIndex + 1];
281
- return {
282
- name: invocation.name,
283
- source: scriptWord && wordIsStatic(scriptWord) ? scriptWord.value : undefined,
284
- hasScriptArgument: true,
285
- };
286
- }
287
-
288
- function exhaustiveNode(_node: never): never {
289
- throw new Error(`Unsupported unbash AST node: ${JSON.stringify(_node)}`);
290
- }
291
-
292
- /** Parse one Pi `bash` tool input into normalized executable command views. */
293
- export function analyzeBash(
294
- source: string,
295
- parser: BashParser = parse,
296
- ): BashAnalysis {
297
- const analysis: BashAnalysis = {
298
- source,
299
- commands: [],
300
- redirects: [],
301
- redirectTargets: [],
302
- structure: [],
303
- allowStructureSafe: true,
304
- errors: [],
305
- };
306
- if (source.length > MAX_BASH_SOURCE_LENGTH) {
307
- analysis.errors.push({
308
- message:
309
- `Bash input length ${source.length} exceeds ${MAX_BASH_SOURCE_LENGTH}`,
310
- });
311
- return analysis;
312
- }
313
-
314
- function analyzeRedirect(redirect: Redirect): BashRedirectAnalysis {
315
- return {
316
- operator: redirect.operator,
317
- target: redirect.target?.value,
318
- targetDynamic: !!redirect.target && !wordIsStatic(redirect.target),
319
- fileDescriptor: redirect.fileDescriptor,
320
- variableName: redirect.variableName,
321
- heredoc: redirect.operator === "<<" || redirect.operator === "<<-",
322
- };
323
- }
324
-
325
- function visitRedirect(
326
- redirect: Redirect,
327
- currentSource: string,
328
- depth: number,
329
- ): string | undefined {
330
- const redirectAnalysis = analyzeRedirect(redirect);
331
- analysis.redirects.push(redirectAnalysis);
332
- analysis.structure.push(
333
- `redirect:${redirect.fileDescriptor ?? ""}:${redirect.variableName ?? ""}:${redirect.operator}:${redirectAnalysis.heredoc ? "heredoc" : "file"}`,
334
- );
335
- if (redirect.target) visitWord(redirect.target, currentSource, depth);
336
- if (redirect.body) visitWord(redirect.body, currentSource, depth);
337
- const target = redirect.target?.value;
338
- if (target) analysis.redirectTargets.push(target);
339
- return target;
340
- }
341
-
342
- function visitPart(
343
- part: WordPart,
344
- currentSource: string,
345
- depth: number,
346
- ): void {
347
- switch (part.type) {
348
- case "Literal":
349
- case "SingleQuoted":
350
- case "AnsiCQuoted":
351
- case "SimpleExpansion":
352
- return;
353
- case "DoubleQuoted":
354
- case "LocaleString":
355
- for (const child of part.parts) visitPart(child, currentSource, depth);
356
- return;
357
- case "ParameterExpansion":
358
- for (const indexPart of part.indexParts ?? []) {
359
- visitPart(indexPart, currentSource, depth);
360
- }
361
- if (part.operand) visitWord(part.operand, currentSource, depth);
362
- if (part.slice) {
363
- visitWord(part.slice.offset, currentSource, depth);
364
- if (part.slice.length) {
365
- visitWord(part.slice.length, currentSource, depth);
366
- }
367
- }
368
- if (part.replace) {
369
- visitWord(part.replace.pattern, currentSource, depth);
370
- visitWord(part.replace.replacement, currentSource, depth);
371
- }
372
- return;
373
- case "CommandExpansion":
374
- case "ProcessSubstitution":
375
- analysis.structure.push(
376
- `part:${part.type}:${part.type === "ProcessSubstitution" ? part.operator : ""}`,
377
- );
378
- if (part.script) {
379
- visitScript(part.script, part.script.source ?? currentSource, depth);
380
- }
381
- return;
382
- case "ArithmeticExpansion":
383
- if (part.expression) {
384
- visitArithmetic(part.expression, currentSource, depth);
385
- }
386
- return;
387
- case "ExtendedGlob":
388
- case "BraceExpansion":
389
- for (const nestedPart of part.parts ?? []) {
390
- visitPart(nestedPart, currentSource, depth);
391
- }
392
- return;
393
- }
394
- }
395
-
396
- function visitWord(word: Word, currentSource: string, depth: number): void {
397
- for (const part of wordParts(word)) {
398
- visitPart(part, currentSource, depth);
399
- }
400
- }
401
-
402
- function visitArithmetic(
403
- expression: ArithmeticExpression,
404
- currentSource: string,
405
- depth: number,
406
- ): void {
407
- switch (expression.type) {
408
- case "ArithmeticBinary":
409
- visitArithmetic(expression.left, currentSource, depth);
410
- visitArithmetic(expression.right, currentSource, depth);
411
- return;
412
- case "ArithmeticUnary":
413
- visitArithmetic(expression.operand, currentSource, depth);
414
- return;
415
- case "ArithmeticTernary":
416
- visitArithmetic(expression.test, currentSource, depth);
417
- visitArithmetic(expression.consequent, currentSource, depth);
418
- visitArithmetic(expression.alternate, currentSource, depth);
419
- return;
420
- case "ArithmeticGroup":
421
- visitArithmetic(expression.expression, currentSource, depth);
422
- return;
423
- case "ArithmeticWord":
424
- for (const part of expression.parts ?? []) {
425
- visitPart(part, currentSource, depth);
426
- }
427
- return;
428
- case "ArithmeticCommandExpansion":
429
- analysis.structure.push("part:ArithmeticCommandExpansion");
430
- if (expression.script) {
431
- visitScript(
432
- expression.script,
433
- expression.script.source ?? currentSource,
434
- depth,
435
- );
436
- }
437
- return;
438
- }
439
- }
440
-
441
- function visitTest(
442
- expression: TestExpression,
443
- currentSource: string,
444
- depth: number,
445
- ): void {
446
- switch (expression.type) {
447
- case "TestUnary":
448
- visitWord(expression.operand, currentSource, depth);
449
- return;
450
- case "TestBinary":
451
- visitWord(expression.left, currentSource, depth);
452
- visitWord(expression.right, currentSource, depth);
453
- return;
454
- case "TestLogical":
455
- visitTest(expression.left, currentSource, depth);
456
- visitTest(expression.right, currentSource, depth);
457
- return;
458
- case "TestNot":
459
- visitTest(expression.operand, currentSource, depth);
460
- return;
461
- case "TestGroup":
462
- visitTest(expression.expression, currentSource, depth);
463
- return;
464
- }
465
- }
466
-
467
- function nodeStructureToken(node: Node): string {
468
- switch (node.type) {
469
- case "Command":
470
- return `node:Command:${node.prefix.length}:${node.redirects.length}`;
471
- case "Pipeline":
472
- return `node:Pipeline:${node.negated ? "negated" : "plain"}:${node.time ? "time" : "plain"}:${node.operators.join(",")}`;
473
- case "AndOr":
474
- return `node:AndOr:${node.operators.join(",")}`;
475
- case "If":
476
- return `node:If:${node.else?.type ?? "none"}`;
477
- case "For":
478
- case "Select":
479
- return `node:${node.type}:${node.wordlist.length}`;
480
- case "ArithmeticFor":
481
- return `node:ArithmeticFor:${node.initialize ? 1 : 0}:${node.test ? 1 : 0}:${node.update ? 1 : 0}`;
482
- case "While":
483
- return `node:While:${node.kind}`;
484
- case "Function":
485
- return `node:Function:${node.redirects.length}`;
486
- case "Subshell":
487
- case "BraceGroup":
488
- case "TestCommand":
489
- case "ArithmeticCommand":
490
- return `node:${node.type}`;
491
- case "CompoundList":
492
- return `node:CompoundList:${node.commands.length}`;
493
- case "Case":
494
- return `node:Case:${node.items.map((item) => item.terminator ?? "none").join(",")}`;
495
- case "Coproc":
496
- return `node:Coproc:${node.redirects.length}`;
497
- case "Statement":
498
- return `node:Statement:${node.background ? "background" : "foreground"}:${node.redirects.length}`;
499
- default:
500
- return exhaustiveNode(node);
501
- }
502
- }
503
-
504
- function visitNode(node: Node, nodeSource: string, depth: number): void {
505
- analysis.structure.push(nodeStructureToken(node));
506
- if (
507
- node.type === "For" ||
508
- node.type === "Select" ||
509
- node.type === "ArithmeticFor" ||
510
- node.type === "Function" ||
511
- node.type === "Case" ||
512
- node.type === "Coproc" ||
513
- node.type === "TestCommand" ||
514
- node.type === "ArithmeticCommand"
515
- ) {
516
- analysis.allowStructureSafe = false;
517
- }
518
- switch (node.type) {
519
- case "Command": {
520
- const prefixWords = node.prefix.map((prefix) => prefix.text);
521
- const commandWords = node.name ? [node.name, ...node.suffix] : node.suffix;
522
- const renderedWords = [
523
- ...prefixWords,
524
- ...commandWords.map((word) => word.text),
525
- ];
526
- const values = [
527
- ...prefixWords,
528
- ...commandWords.map((word) => word.value),
529
- ];
530
- const redirects = node.redirects.map(analyzeRedirect);
531
- const redirectTargets = redirects
532
- .map((redirect) => redirect.target)
533
- .filter((target): target is string => !!target);
534
- const normalizedName = commandName(node.name?.value);
535
- const invocation = effectiveCommandInvocation(normalizedName, node.suffix);
536
- const wrapper = nestedShell(invocation);
537
- const wrapperSource = wrapper?.source;
538
- analysis.commands.push({
539
- raw: nodeSource.slice(node.pos, node.end),
540
- text: renderedWords.join(" "),
541
- name: normalizedName,
542
- words: values,
543
- args: node.suffix.map((word) => word.value),
544
- argTexts: node.suffix.map((word) => word.text),
545
- effectiveCommand: {
546
- name: invocation.name,
547
- args: invocation.argumentWords.map((word) => word.value),
548
- argTexts: invocation.argumentWords.map((word) => word.text),
549
- argTildeExpansions:
550
- invocation.argumentWords.map(wordHasTildeExpansion),
551
- unresolvedTransparentDispatch:
552
- invocation.unresolvedTransparentDispatch,
553
- },
554
- redirects,
555
- redirectTargets,
556
- dynamic: commandWords.some((word) => !wordIsStatic(word)),
557
- dynamicName: !!node.name && !wordIsStatic(node.name),
558
- dynamicShellScript:
559
- !!wrapper?.hasScriptArgument && wrapperSource === undefined,
560
- pos: node.pos,
561
- end: node.end,
562
- });
563
-
564
- for (const redirect of node.redirects) {
565
- visitRedirect(redirect, nodeSource, depth);
566
- }
567
- if (node.name) visitWord(node.name, nodeSource, depth);
568
- for (const prefix of node.prefix) {
569
- if (prefix.value) visitWord(prefix.value, nodeSource, depth);
570
- for (const word of prefix.array ?? []) {
571
- visitWord(word, nodeSource, depth);
572
- }
573
- for (const part of prefix.indexParts ?? []) {
574
- visitPart(part, nodeSource, depth);
575
- }
576
- }
577
- for (const word of node.suffix) visitWord(word, nodeSource, depth);
578
-
579
- if (wrapperSource !== undefined) {
580
- analysis.structure.push(`wrapper:${wrapper?.name ?? normalizedName}`);
581
- if (depth >= MAX_NESTED_SHELL_DEPTH) {
582
- analysis.errors.push({
583
- message: `Nested shell depth exceeds ${MAX_NESTED_SHELL_DEPTH}`,
584
- });
585
- } else {
586
- try {
587
- visitScript(parser(wrapperSource), wrapperSource, depth + 1);
588
- } catch (error) {
589
- analysis.errors.push(parserException(error));
590
- }
591
- }
592
- }
593
- return;
594
- }
595
- case "Pipeline":
596
- case "AndOr":
597
- for (const command of node.commands) visitNode(command, nodeSource, depth);
598
- return;
599
- case "If":
600
- visitNode(node.clause, nodeSource, depth);
601
- visitNode(node.then, nodeSource, depth);
602
- if (node.else) visitNode(node.else, nodeSource, depth);
603
- return;
604
- case "For":
605
- case "Select":
606
- visitWord(node.name, nodeSource, depth);
607
- for (const word of node.wordlist) {
608
- visitWord(word, nodeSource, depth);
609
- }
610
- visitNode(node.body, nodeSource, depth);
611
- return;
612
- case "ArithmeticFor":
613
- if (node.initialize) visitArithmetic(node.initialize, nodeSource, depth);
614
- if (node.test) visitArithmetic(node.test, nodeSource, depth);
615
- if (node.update) visitArithmetic(node.update, nodeSource, depth);
616
- visitNode(node.body, nodeSource, depth);
617
- return;
618
- case "While":
619
- visitNode(node.clause, nodeSource, depth);
620
- visitNode(node.body, nodeSource, depth);
621
- return;
622
- case "Function":
623
- visitWord(node.name, nodeSource, depth);
624
- for (const redirect of node.redirects) {
625
- visitRedirect(redirect, nodeSource, depth);
626
- }
627
- visitNode(node.body, nodeSource, depth);
628
- return;
629
- case "Subshell":
630
- case "BraceGroup":
631
- visitNode(node.body, nodeSource, depth);
632
- return;
633
- case "CompoundList":
634
- for (const statement of node.commands) {
635
- visitNode(statement, nodeSource, depth);
636
- }
637
- return;
638
- case "Case":
639
- visitWord(node.word, nodeSource, depth);
640
- for (const item of node.items) {
641
- for (const pattern of item.pattern) {
642
- visitWord(pattern, nodeSource, depth);
643
- }
644
- visitNode(item.body, nodeSource, depth);
645
- }
646
- return;
647
- case "Coproc":
648
- if (node.name) visitWord(node.name, nodeSource, depth);
649
- for (const redirect of node.redirects) {
650
- visitRedirect(redirect, nodeSource, depth);
651
- }
652
- visitNode(node.body, nodeSource, depth);
653
- return;
654
- case "TestCommand":
655
- visitTest(node.expression, nodeSource, depth);
656
- return;
657
- case "ArithmeticCommand":
658
- if (node.expression) {
659
- visitArithmetic(node.expression, nodeSource, depth);
660
- }
661
- return;
662
- case "Statement":
663
- for (const redirect of node.redirects) {
664
- visitRedirect(redirect, nodeSource, depth);
665
- }
666
- visitNode(node.command, nodeSource, depth);
667
- return;
668
- default:
669
- exhaustiveNode(node);
670
- }
671
- }
672
-
673
- function visitScript(
674
- script: ParsedScript,
675
- scriptSource: string,
676
- depth: number,
677
- ): void {
678
- analysis.structure.push(`script:${script.commands.length}`);
679
- for (const error of script.errors ?? []) analysis.errors.push(parseError(error));
680
- for (const statement of script.commands) {
681
- visitNode(statement, scriptSource, depth);
682
- }
683
- }
684
-
685
- try {
686
- visitScript(parser(source), source, 0);
687
- } catch (error) {
688
- analysis.errors.push(parserException(error));
689
- }
690
-
691
- return analysis;
692
- }