@aliou/pi-processes 0.4.7 → 0.6.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.
Files changed (51) hide show
  1. package/README.md +83 -13
  2. package/package.json +10 -4
  3. package/src/commands/clear/command.ts +14 -0
  4. package/src/commands/clear/index.ts +1 -0
  5. package/src/commands/completions.ts +38 -0
  6. package/src/commands/dock/command.ts +29 -0
  7. package/src/commands/dock/index.ts +1 -0
  8. package/src/commands/index.ts +26 -327
  9. package/src/commands/kill/command.ts +69 -0
  10. package/src/commands/kill/index.ts +1 -0
  11. package/src/commands/logs/command.ts +47 -0
  12. package/src/commands/logs/index.ts +1 -0
  13. package/src/commands/pick-process.ts +34 -0
  14. package/src/commands/pin/command.ts +33 -0
  15. package/src/commands/pin/index.ts +1 -0
  16. package/src/commands/processes/command.ts +39 -0
  17. package/src/commands/processes/index.ts +1 -0
  18. package/src/commands/settings/apply-setting-change.ts +72 -0
  19. package/src/commands/settings/build-sections.ts +160 -0
  20. package/src/commands/settings/command.ts +20 -0
  21. package/src/commands/settings/index.ts +1 -0
  22. package/src/components/log-dock-component.ts +238 -0
  23. package/src/components/log-file-viewer.ts +317 -0
  24. package/src/components/log-overlay-component.ts +555 -0
  25. package/src/components/process-picker-component.ts +14 -2
  26. package/src/components/processes-component.ts +41 -21
  27. package/src/config.ts +38 -1
  28. package/src/constants/index.ts +1 -0
  29. package/src/constants/types.ts +7 -1
  30. package/src/hooks/background-blocker.ts +66 -0
  31. package/src/hooks/index.ts +15 -3
  32. package/src/hooks/process-end.ts +3 -30
  33. package/src/hooks/widget/index.ts +2 -0
  34. package/src/hooks/widget/setup.ts +168 -0
  35. package/src/hooks/{widget.ts → widget/status-widget.ts} +27 -68
  36. package/src/hooks/widget/types.ts +21 -0
  37. package/src/index.ts +9 -3
  38. package/src/manager.ts +61 -9
  39. package/src/tools/actions/index.ts +5 -0
  40. package/src/tools/actions/write.ts +87 -0
  41. package/src/tools/index.ts +82 -14
  42. package/src/utils/command-executor.test.ts +2 -1
  43. package/src/utils/command-executor.ts +1 -1
  44. package/src/utils/keybindings.ts +76 -0
  45. package/src/utils/shell-utils.ts +133 -0
  46. package/src/commands/settings-command.ts +0 -157
  47. package/src/components/log-stream-component.ts +0 -149
  48. package/src/test/test-exit-crash.sh +0 -19
  49. package/src/test/test-exit-failure.sh +0 -17
  50. package/src/test/test-exit-success.sh +0 -16
  51. package/src/test/test-output.sh +0 -28
package/src/manager.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  } from "node:fs";
10
10
  import { tmpdir } from "node:os";
11
11
  import { join } from "node:path";
12
+ import type { Writable } from "node:stream";
12
13
 
13
14
  import {
14
15
  type KillResult,
@@ -17,12 +18,15 @@ import {
17
18
  type ProcessInfo,
18
19
  type ProcessStatus,
19
20
  type StartOptions,
21
+ type WriteResult,
20
22
  } from "./constants";
21
23
  import { isProcessGroupAlive, killProcessGroup } from "./utils";
22
24
  import { spawnCommand } from "./utils/command-executor";
23
25
 
24
26
  interface ManagedProcess extends ProcessInfo {
25
27
  process: ChildProcess;
28
+ stdin: Writable | null;
29
+ stdinClosed: boolean;
26
30
  lastSignalSent: NodeJS.Signals | null;
27
31
  combinedFile: string;
28
32
  }
@@ -57,15 +61,8 @@ export class ProcessManager {
57
61
 
58
62
  private transition(managed: ManagedProcess, next: ProcessStatus): void {
59
63
  if (managed.status === next) return;
60
- const prev = managed.status;
61
64
  managed.status = next;
62
65
 
63
- this.emit({
64
- type: "process_status_changed",
65
- info: this.toProcessInfo(managed),
66
- prev,
67
- });
68
-
69
66
  if (next === "exited" || next === "killed") {
70
67
  this.emit({ type: "process_ended", info: this.toProcessInfo(managed) });
71
68
  }
@@ -159,6 +156,8 @@ export class ProcessManager {
159
156
  alertOnFailure: options?.alertOnFailure ?? true,
160
157
  alertOnKill: options?.alertOnKill ?? false,
161
158
  process: child,
159
+ stdin: child.stdin,
160
+ stdinClosed: false,
162
161
  lastSignalSent: null,
163
162
  };
164
163
 
@@ -319,12 +318,15 @@ export class ProcessManager {
319
318
  }
320
319
  }
321
320
 
322
- getLogFiles(id: string): { stdoutFile: string; stderrFile: string } | null {
321
+ getLogFiles(
322
+ id: string,
323
+ ): { stdoutFile: string; stderrFile: string; combinedFile: string } | null {
323
324
  const managed = this.processes.get(id);
324
325
  if (!managed) return null;
325
326
  return {
326
327
  stdoutFile: managed.stdoutFile,
327
328
  stderrFile: managed.stderrFile,
329
+ combinedFile: managed.combinedFile,
328
330
  };
329
331
  }
330
332
 
@@ -407,6 +409,50 @@ export class ProcessManager {
407
409
  return { ok: true, info: this.toProcessInfo(managed) };
408
410
  }
409
411
 
412
+ writeToStdin(
413
+ id: string,
414
+ data: string,
415
+ opts?: { end?: boolean },
416
+ ): WriteResult {
417
+ const managed = this.processes.get(id);
418
+ if (!managed) {
419
+ return {
420
+ ok: false,
421
+ reason: "not_found",
422
+ };
423
+ }
424
+
425
+ if (!LIVE_STATUSES.has(managed.status)) {
426
+ return {
427
+ ok: false,
428
+ reason: "process_exited",
429
+ };
430
+ }
431
+
432
+ if (managed.stdinClosed || !managed.stdin) {
433
+ return {
434
+ ok: false,
435
+ reason: "stdin_closed",
436
+ };
437
+ }
438
+
439
+ try {
440
+ managed.stdin.write(data);
441
+
442
+ if (opts?.end) {
443
+ managed.stdin.end();
444
+ managed.stdinClosed = true;
445
+ }
446
+
447
+ return { ok: true };
448
+ } catch {
449
+ return {
450
+ ok: false,
451
+ reason: "write_error",
452
+ };
453
+ }
454
+ }
455
+
410
456
  clearFinished(): number {
411
457
  let cleared = 0;
412
458
  for (const [id, managed] of this.processes) {
@@ -519,4 +565,10 @@ export class ProcessManager {
519
565
  }
520
566
  }
521
567
 
522
- export type { ProcessInfo, ProcessStatus, ManagerEvent, KillResult };
568
+ export type {
569
+ ProcessInfo,
570
+ ProcessStatus,
571
+ ManagerEvent,
572
+ KillResult,
573
+ WriteResult,
574
+ };
@@ -7,12 +7,15 @@ import { executeList } from "./list";
7
7
  import { executeLogs } from "./logs";
8
8
  import { executeOutput } from "./output";
9
9
  import { executeStart } from "./start";
10
+ import { executeWrite } from "./write";
10
11
 
11
12
  interface ActionParams {
12
13
  action: string;
13
14
  command?: string;
14
15
  name?: string;
15
16
  id?: string;
17
+ input?: string;
18
+ end?: boolean;
16
19
  alertOnSuccess?: boolean;
17
20
  alertOnFailure?: boolean;
18
21
  alertOnKill?: boolean;
@@ -36,6 +39,8 @@ export async function executeAction(
36
39
  return executeKill(params, manager);
37
40
  case "clear":
38
41
  return executeClear(manager);
42
+ case "write":
43
+ return executeWrite(params, manager);
39
44
  default:
40
45
  return {
41
46
  content: [{ type: "text", text: `Unknown action: ${params.action}` }],
@@ -0,0 +1,87 @@
1
+ import type { ExecuteResult } from "../../constants";
2
+ import type { ProcessManager } from "../../manager";
3
+
4
+ interface WriteParams {
5
+ id?: string;
6
+ input?: string;
7
+ end?: boolean;
8
+ }
9
+
10
+ export function executeWrite(
11
+ params: WriteParams,
12
+ manager: ProcessManager,
13
+ ): ExecuteResult {
14
+ const { id, input, end } = params;
15
+
16
+ if (!id) {
17
+ return {
18
+ content: [{ type: "text", text: "Missing required parameter: id" }],
19
+ details: {
20
+ action: "write",
21
+ success: false,
22
+ message: "Missing required parameter: id",
23
+ },
24
+ };
25
+ }
26
+
27
+ if (input === undefined) {
28
+ return {
29
+ content: [{ type: "text", text: "Missing required parameter: input" }],
30
+ details: {
31
+ action: "write",
32
+ success: false,
33
+ message: "Missing required parameter: input",
34
+ },
35
+ };
36
+ }
37
+
38
+ const process = manager.find(id);
39
+ if (!process) {
40
+ return {
41
+ content: [{ type: "text", text: `Process not found: ${id}` }],
42
+ details: {
43
+ action: "write",
44
+ success: false,
45
+ message: `Process not found: ${id}`,
46
+ },
47
+ };
48
+ }
49
+
50
+ const result = manager.writeToStdin(process.id, input, { end });
51
+
52
+ if (!result.ok) {
53
+ const messages: Record<string, string> = {
54
+ not_found: `Process not found: ${process.id}`,
55
+ process_exited: `Process has already exited: ${process.id}`,
56
+ stdin_closed: `Stdin already closed for process: ${process.id}`,
57
+ write_error: `Failed to write to stdin for process: ${process.id}`,
58
+ };
59
+
60
+ const message =
61
+ messages[result.reason] || `Unknown error: ${result.reason}`;
62
+
63
+ return {
64
+ content: [{ type: "text", text: message }],
65
+ details: {
66
+ action: "write",
67
+ success: false,
68
+ message,
69
+ },
70
+ };
71
+ }
72
+
73
+ const suffix = end ? " (stdin closed)" : "";
74
+ return {
75
+ content: [
76
+ {
77
+ type: "text",
78
+ text: `Wrote ${input.length} bytes to ${process.id}${suffix}`,
79
+ },
80
+ ],
81
+ details: {
82
+ action: "write",
83
+ success: true,
84
+ message: `Wrote ${input.length} bytes to process stdin${suffix}`,
85
+ },
86
+ };
87
+ }
@@ -15,10 +15,10 @@ import { executeAction } from "./actions";
15
15
 
16
16
  const ProcessesParams = Type.Object({
17
17
  action: StringEnum(
18
- ["start", "list", "output", "logs", "kill", "clear"] as const,
18
+ ["start", "list", "output", "logs", "kill", "clear", "write"] as const,
19
19
  {
20
20
  description:
21
- "Action: start (run command), list (show all), output (get recent output), logs (get log file paths), kill (terminate), clear (remove finished)",
21
+ "Action: start (run command), list (show all), output (get recent output), logs (get log file paths), kill (terminate), clear (remove finished), write (write to stdin)",
22
22
  },
23
23
  ),
24
24
  command: Type.Optional(
@@ -33,7 +33,18 @@ const ProcessesParams = Type.Object({
33
33
  id: Type.Optional(
34
34
  Type.String({
35
35
  description:
36
- "Process ID or name to match (required for output/kill/logs). Can be proc_N or friendly name.",
36
+ "Process ID or name to match (required for output/kill/logs/write). Can be proc_N or friendly name.",
37
+ }),
38
+ ),
39
+ input: Type.Optional(
40
+ Type.String({
41
+ description: "Data to write to process stdin (required for write action)",
42
+ }),
43
+ ),
44
+ end: Type.Optional(
45
+ Type.Boolean({
46
+ description:
47
+ "Close stdin after writing (optional for write action, use for programs reading until EOF)",
37
48
  }),
38
49
  ),
39
50
  alertOnSuccess: Type.Optional(
@@ -72,6 +83,7 @@ export function setupProcessesTools(pi: ExtensionAPI, manager: ProcessManager) {
72
83
  - logs: Get log file paths to inspect with read tool (requires 'id')
73
84
  - kill: Terminate a process (requires 'id' - can be proc_N or name match like "backend")
74
85
  - clear: Remove all finished processes from the list
86
+ - write: Write to process stdin (requires 'id' and 'input', optional 'end' to close stdin)
75
87
 
76
88
  Important: You DON'T need to poll or wait for processes. Notifications arrive automatically based on your preferences. Start processes and continue with other work - you'll be informed if something requires attention.
77
89
 
@@ -107,12 +119,20 @@ Note: User always sees process updates in the UI. The notify flags control wheth
107
119
  if (
108
120
  (args.action === "output" ||
109
121
  args.action === "kill" ||
110
- args.action === "logs") &&
122
+ args.action === "logs" ||
123
+ args.action === "write") &&
111
124
  args.id
112
125
  ) {
113
126
  mainArg = args.id;
114
127
  }
115
128
 
129
+ if (args.action === "write" && args.input) {
130
+ optionArgs.push({ label: "input", value: args.input });
131
+ if (args.end) {
132
+ optionArgs.push({ label: "end", value: "true" });
133
+ }
134
+ }
135
+
116
136
  return new ToolCallHeader(
117
137
  {
118
138
  toolName: "Process",
@@ -204,6 +224,23 @@ Note: User always sees process updates in the UI. The notify flags control wheth
204
224
  }
205
225
 
206
226
  fields.push(new Text(lines.join("\n"), 0, 0));
227
+
228
+ // Collapsed summary
229
+ const previewSource =
230
+ details.output.stdout.length > 0
231
+ ? details.output.stdout
232
+ : details.output.stderr;
233
+ const preview = previewSource
234
+ .slice(-2)
235
+ .map((l) => stripAnsi(l))
236
+ .join("\n");
237
+ fields.push({
238
+ label: "Output",
239
+ value: preview
240
+ ? `${theme.fg("muted", preview)}`
241
+ : theme.fg("muted", "(empty)"),
242
+ showCollapsed: true,
243
+ });
207
244
  } else if (
208
245
  details.action === "list" &&
209
246
  details.processes &&
@@ -243,6 +280,31 @@ Note: User always sees process updates in the UI. The notify flags control wheth
243
280
  }
244
281
 
245
282
  fields.push(new Text(lines.join("\n"), 0, 0));
283
+
284
+ // Collapsed summary: first 3 processes
285
+ const summary = details.processes
286
+ .slice(0, 3)
287
+ .map((p) => {
288
+ const s =
289
+ p.status === "running"
290
+ ? theme.fg("accent", "running")
291
+ : p.status === "exited" && p.success
292
+ ? theme.fg("success", "exit(0)")
293
+ : p.status === "exited"
294
+ ? theme.fg("error", `exit(${p.exitCode ?? "?"})`)
295
+ : theme.fg("muted", p.status);
296
+ return `${theme.fg("accent", `"${p.name}"`)} [${s}]`;
297
+ })
298
+ .join(", ");
299
+ const more =
300
+ details.processes.length > 3
301
+ ? theme.fg("muted", ` +${details.processes.length - 3} more`)
302
+ : "";
303
+ fields.push({
304
+ label: "Processes",
305
+ value: summary + more,
306
+ showCollapsed: true,
307
+ });
246
308
  } else if (details.action === "logs" && details.logFiles) {
247
309
  fields.push(
248
310
  new Text(
@@ -263,16 +325,22 @@ Note: User always sees process updates in the UI. The notify flags control wheth
263
325
  });
264
326
  }
265
327
 
266
- const footer = new ToolFooter(theme, {
267
- items: [
268
- { label: "action", value: details.action, tone: "accent" },
269
- {
270
- label: "status",
271
- value: details.success ? "ok" : "error",
272
- tone: details.success ? "success" : "error",
273
- },
274
- ],
275
- });
328
+ const footerItems: Array<{
329
+ label: string;
330
+ value: string;
331
+ tone: "accent" | "success" | "error" | "warning" | "muted";
332
+ }> = [];
333
+ if (!details.success) {
334
+ footerItems.push({
335
+ label: "status",
336
+ value: "error",
337
+ tone: "error",
338
+ });
339
+ }
340
+ const footer =
341
+ footerItems.length > 0
342
+ ? new ToolFooter(theme, { items: footerItems })
343
+ : undefined;
276
344
 
277
345
  return new ToolBody({ fields, footer }, options, theme);
278
346
  },
@@ -1,9 +1,10 @@
1
+ import type * as nodeFs from "node:fs";
1
2
  import { existsSync } from "node:fs";
2
3
  import { describe, expect, it, vi } from "vitest";
3
4
  import { resolveShellExecutable } from "./command-executor";
4
5
 
5
6
  vi.mock("node:fs", async (importOriginal) => {
6
- const actual = await importOriginal<typeof import("node:fs")>();
7
+ const actual = await importOriginal<typeof nodeFs>();
7
8
  return { ...actual, existsSync: vi.fn() };
8
9
  });
9
10
 
@@ -50,7 +50,7 @@ export function spawnCommand(
50
50
  return spawn(shellExecutable, ["-lc", command], {
51
51
  cwd,
52
52
  env: process.env,
53
- stdio: ["ignore", "pipe", "pipe"],
53
+ stdio: ["pipe", "pipe", "pipe"],
54
54
  detached: true,
55
55
  });
56
56
  }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Keyboard shortcuts configuration for the Process Dock.
3
+ */
4
+
5
+ export interface ProcessesKeybindings {
6
+ /** Toggle dock visibility (global) */
7
+ toggleDock: string;
8
+ /** Scroll logs up */
9
+ scrollUp: string;
10
+ /** Scroll logs down */
11
+ scrollDown: string;
12
+ /** Focus previous process */
13
+ prevProcess: string;
14
+ /** Focus next process */
15
+ nextProcess: string;
16
+ /** Toggle focus mode */
17
+ toggleFocus: string;
18
+ /** Toggle follow mode */
19
+ toggleFollow: string;
20
+ /** Kill focused process */
21
+ killProcess: string;
22
+ /** Clear finished processes */
23
+ clearFinished: string;
24
+ /** Collapse/close dock */
25
+ closeDock: string;
26
+ }
27
+
28
+ export const DEFAULT_KEYBINDINGS: ProcessesKeybindings = {
29
+ toggleDock: "", // Disabled - conflicts with editor shortcuts
30
+ scrollUp: "k",
31
+ scrollDown: "j",
32
+ prevProcess: "h",
33
+ nextProcess: "l",
34
+ toggleFocus: "f",
35
+ toggleFollow: "Shift+F",
36
+ killProcess: "x",
37
+ clearFinished: "c",
38
+ closeDock: "q",
39
+ };
40
+
41
+ /**
42
+ * Interface for config that may contain keybindings overrides
43
+ */
44
+ export interface ProcessesConfigKeybindings {
45
+ toggleDock?: string;
46
+ scrollUp?: string;
47
+ scrollDown?: string;
48
+ prevProcess?: string;
49
+ nextProcess?: string;
50
+ toggleFocus?: string;
51
+ toggleFollow?: string;
52
+ killProcess?: string;
53
+ clearFinished?: string;
54
+ closeDock?: string;
55
+ }
56
+
57
+ /**
58
+ * Load keybindings from config, falling back to defaults.
59
+ */
60
+ export function loadKeybindings(config: {
61
+ keybindings?: ProcessesConfigKeybindings;
62
+ }): ProcessesKeybindings {
63
+ const user = config.keybindings ?? {};
64
+ return {
65
+ toggleDock: user.toggleDock ?? DEFAULT_KEYBINDINGS.toggleDock,
66
+ scrollUp: user.scrollUp ?? DEFAULT_KEYBINDINGS.scrollUp,
67
+ scrollDown: user.scrollDown ?? DEFAULT_KEYBINDINGS.scrollDown,
68
+ prevProcess: user.prevProcess ?? DEFAULT_KEYBINDINGS.prevProcess,
69
+ nextProcess: user.nextProcess ?? DEFAULT_KEYBINDINGS.nextProcess,
70
+ toggleFocus: user.toggleFocus ?? DEFAULT_KEYBINDINGS.toggleFocus,
71
+ toggleFollow: user.toggleFollow ?? DEFAULT_KEYBINDINGS.toggleFollow,
72
+ killProcess: user.killProcess ?? DEFAULT_KEYBINDINGS.killProcess,
73
+ clearFinished: user.clearFinished ?? DEFAULT_KEYBINDINGS.clearFinished,
74
+ closeDock: user.closeDock ?? DEFAULT_KEYBINDINGS.closeDock,
75
+ };
76
+ }
@@ -0,0 +1,133 @@
1
+ // Shell AST helpers. Duplicated from pi-toolchain since cross-extension imports are not allowed.
2
+
3
+ import type {
4
+ Command,
5
+ Program,
6
+ SimpleCommand,
7
+ Statement,
8
+ Word,
9
+ WordPart,
10
+ } from "@aliou/sh";
11
+
12
+ /**
13
+ * Resolve a Word node to its literal string value.
14
+ * Concatenates Literal, SglQuoted, and simple DblQuoted parts.
15
+ * For parts containing parameter expansions, command substitutions, etc.,
16
+ * includes the raw text representation (e.g. `$VAR`).
17
+ */
18
+ export function wordToString(word: Word): string {
19
+ return word.parts.map(partToString).join("");
20
+ }
21
+
22
+ function partToString(part: WordPart): string {
23
+ switch (part.type) {
24
+ case "Literal":
25
+ return part.value;
26
+ case "SglQuoted":
27
+ return part.value;
28
+ case "DblQuoted":
29
+ return part.parts.map(partToString).join("");
30
+ case "ParamExp":
31
+ return part.short
32
+ ? `$${part.param.value}`
33
+ : `\${${part.param.value}${part.op ?? ""}${part.value ? wordToString(part.value) : ""}}`;
34
+ case "CmdSubst":
35
+ return "$(...)";
36
+ case "ArithExp":
37
+ return `$((${part.expr}))`;
38
+ case "ProcSubst":
39
+ return `${part.op}(...)`;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Walk the AST and call `callback` for every SimpleCommand found at any
45
+ * nesting depth. Returns early if callback returns `true`.
46
+ */
47
+ export function walkCommands(
48
+ node: Program,
49
+ callback: (cmd: SimpleCommand) => boolean | undefined,
50
+ ): void {
51
+ for (const stmt of node.body) {
52
+ if (walkStatement(stmt, callback)) return;
53
+ }
54
+ }
55
+
56
+ function walkStatement(
57
+ stmt: Statement,
58
+ callback: (cmd: SimpleCommand) => boolean | undefined,
59
+ ): boolean {
60
+ return walkCommand(stmt.command, callback);
61
+ }
62
+
63
+ function walkStatements(
64
+ stmts: Statement[],
65
+ callback: (cmd: SimpleCommand) => boolean | undefined,
66
+ ): boolean {
67
+ for (const stmt of stmts) {
68
+ if (walkStatement(stmt, callback)) return true;
69
+ }
70
+ return false;
71
+ }
72
+
73
+ function walkCommand(
74
+ cmd: Command,
75
+ callback: (cmd: SimpleCommand) => boolean | undefined,
76
+ ): boolean {
77
+ switch (cmd.type) {
78
+ case "SimpleCommand":
79
+ return callback(cmd) === true;
80
+
81
+ case "Pipeline":
82
+ return walkStatements(cmd.commands, callback);
83
+
84
+ case "Logical":
85
+ return (
86
+ walkStatement(cmd.left, callback) || walkStatement(cmd.right, callback)
87
+ );
88
+
89
+ case "Subshell":
90
+ case "Block":
91
+ return walkStatements(cmd.body, callback);
92
+
93
+ case "IfClause":
94
+ return (
95
+ walkStatements(cmd.cond, callback) ||
96
+ walkStatements(cmd.then, callback) ||
97
+ (cmd.else ? walkStatements(cmd.else, callback) : false)
98
+ );
99
+
100
+ case "ForClause":
101
+ case "SelectClause":
102
+ case "WhileClause":
103
+ return (
104
+ ("cond" in cmd && cmd.cond
105
+ ? walkStatements(cmd.cond, callback)
106
+ : false) || walkStatements(cmd.body, callback)
107
+ );
108
+
109
+ case "CaseClause":
110
+ for (const item of cmd.items) {
111
+ if (walkStatements(item.body, callback)) return true;
112
+ }
113
+ return false;
114
+
115
+ case "FunctionDecl":
116
+ return walkStatements(cmd.body, callback);
117
+
118
+ case "TimeClause":
119
+ return walkStatement(cmd.command, callback);
120
+
121
+ case "CoprocClause":
122
+ return walkStatement(cmd.body, callback);
123
+
124
+ case "CStyleLoop":
125
+ return walkStatements(cmd.body, callback);
126
+
127
+ case "TestClause":
128
+ case "ArithCmd":
129
+ case "DeclClause":
130
+ case "LetClause":
131
+ return false;
132
+ }
133
+ }