@herbertgao/pi-extensions 2026.9.5 → 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 (38) hide show
  1. package/README.md +3 -15
  2. package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +1 -1
  3. package/node_modules/@herbertgao/sol-pi/package.json +1 -1
  4. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/index.ts +0 -5
  5. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/state.ts +3 -1
  6. package/package.json +2 -7
  7. package/examples/pi-footer.json +0 -275
  8. package/node_modules/@czottmann/pi-automode/CHANGELOG.md +0 -70
  9. package/node_modules/@czottmann/pi-automode/LICENSE.md +0 -21
  10. package/node_modules/@czottmann/pi-automode/README.md +0 -215
  11. package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +0 -39
  12. package/node_modules/@czottmann/pi-automode/docs/adr/ADR-001-permission-precedence-and-trust-boundaries.md +0 -46
  13. package/node_modules/@czottmann/pi-automode/docs/adr/ADR-002-global-config-in-extension-data-directory.md +0 -60
  14. package/node_modules/@czottmann/pi-automode/docs/adr/INDEX.md +0 -6
  15. package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +0 -428
  16. package/node_modules/@czottmann/pi-automode/docs/configuration.md +0 -171
  17. package/node_modules/@czottmann/pi-automode/docs/defaults.md +0 -178
  18. package/node_modules/@czottmann/pi-automode/docs/diagnostics.md +0 -90
  19. package/node_modules/@czottmann/pi-automode/docs/observability-logging.md +0 -160
  20. package/node_modules/@czottmann/pi-automode/examples/automode.local.json +0 -44
  21. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/bash.ts +0 -692
  22. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +0 -852
  23. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +0 -904
  24. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +0 -228
  25. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +0 -954
  26. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/hard-deny.ts +0 -429
  27. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/log.ts +0 -173
  28. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/model-selector.ts +0 -113
  29. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/model.ts +0 -13
  30. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/paths.ts +0 -297
  31. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/permissions.ts +0 -623
  32. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/state.ts +0 -102
  33. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/transcript.ts +0 -236
  34. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/types.ts +0 -205
  35. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/utils.ts +0 -54
  36. package/node_modules/@czottmann/pi-automode/extensions/auto-mode.ts +0 -26
  37. package/node_modules/@czottmann/pi-automode/package.json +0 -61
  38. 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
- }