@procedures/shell 0.0.0-stage → 0.2.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 (85) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/LICENSE +21 -0
  3. package/README.md +421 -2
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/command/index.d.ts +18 -0
  6. package/dist/command/index.d.ts.map +1 -0
  7. package/dist/command/index.js +17 -0
  8. package/dist/command/index.js.map +1 -0
  9. package/dist/command/output.d.ts +22 -0
  10. package/dist/command/output.d.ts.map +1 -0
  11. package/dist/command/output.js +67 -0
  12. package/dist/command/output.js.map +1 -0
  13. package/dist/command/processes.d.ts +89 -0
  14. package/dist/command/processes.d.ts.map +1 -0
  15. package/dist/command/processes.js +223 -0
  16. package/dist/command/processes.js.map +1 -0
  17. package/dist/command/run.d.ts +48 -0
  18. package/dist/command/run.d.ts.map +1 -0
  19. package/dist/command/run.js +89 -0
  20. package/dist/command/run.js.map +1 -0
  21. package/dist/command/spawn.d.ts +26 -0
  22. package/dist/command/spawn.d.ts.map +1 -0
  23. package/dist/command/spawn.js +28 -0
  24. package/dist/command/spawn.js.map +1 -0
  25. package/dist/command/stream.d.ts +42 -0
  26. package/dist/command/stream.d.ts.map +1 -0
  27. package/dist/command/stream.js +158 -0
  28. package/dist/command/stream.js.map +1 -0
  29. package/dist/command/tree-kill.d.ts +26 -0
  30. package/dist/command/tree-kill.d.ts.map +1 -0
  31. package/dist/command/tree-kill.js +122 -0
  32. package/dist/command/tree-kill.js.map +1 -0
  33. package/dist/index.d.ts +31 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +33 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/module.d.ts +19 -0
  38. package/dist/module.d.ts.map +1 -0
  39. package/dist/module.js +98 -0
  40. package/dist/module.js.map +1 -0
  41. package/dist/procedures/shell/exec.d.ts +21 -0
  42. package/dist/procedures/shell/exec.d.ts.map +1 -0
  43. package/dist/procedures/shell/exec.js +45 -0
  44. package/dist/procedures/shell/exec.js.map +1 -0
  45. package/dist/procedures/shell/index.d.ts +5 -0
  46. package/dist/procedures/shell/index.d.ts.map +1 -0
  47. package/dist/procedures/shell/index.js +5 -0
  48. package/dist/procedures/shell/index.js.map +1 -0
  49. package/dist/procedures/shell/run.d.ts +14 -0
  50. package/dist/procedures/shell/run.d.ts.map +1 -0
  51. package/dist/procedures/shell/run.js +40 -0
  52. package/dist/procedures/shell/run.js.map +1 -0
  53. package/dist/procedures/shell/stream.d.ts +20 -0
  54. package/dist/procedures/shell/stream.d.ts.map +1 -0
  55. package/dist/procedures/shell/stream.js +39 -0
  56. package/dist/procedures/shell/stream.js.map +1 -0
  57. package/dist/procedures/shell/which.d.ts +10 -0
  58. package/dist/procedures/shell/which.d.ts.map +1 -0
  59. package/dist/procedures/shell/which.js +21 -0
  60. package/dist/procedures/shell/which.js.map +1 -0
  61. package/dist/register.d.ts +7 -0
  62. package/dist/register.d.ts.map +1 -0
  63. package/dist/register.js +9 -0
  64. package/dist/register.js.map +1 -0
  65. package/dist/types.d.ts +79 -0
  66. package/dist/types.d.ts.map +1 -0
  67. package/dist/types.js +89 -0
  68. package/dist/types.js.map +1 -0
  69. package/package.json +81 -3
  70. package/src/command/index.ts +24 -0
  71. package/src/command/output.ts +67 -0
  72. package/src/command/processes.ts +271 -0
  73. package/src/command/run.ts +127 -0
  74. package/src/command/spawn.ts +44 -0
  75. package/src/command/stream.ts +186 -0
  76. package/src/command/tree-kill.ts +121 -0
  77. package/src/index.ts +53 -0
  78. package/src/module.ts +123 -0
  79. package/src/procedures/shell/exec.ts +55 -0
  80. package/src/procedures/shell/index.ts +4 -0
  81. package/src/procedures/shell/run.ts +47 -0
  82. package/src/procedures/shell/stream.ts +43 -0
  83. package/src/procedures/shell/which.ts +26 -0
  84. package/src/register.ts +11 -0
  85. package/src/types.ts +172 -0
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Kill a child process and all the processes that it started (deep dive WRP-10).
3
+ *
4
+ * `child.kill()` reaches only the direct child. On Windows it calls `TerminateProcess`, so the
5
+ * grandchildren keep running (a `pnpm` that starts `node`, a `vite` that starts `esbuild`).
6
+ * - On Windows, `taskkill /T /F` ends the tree of the process id.
7
+ * - Elsewhere, the child starts in its own process group (`detached`), and a signal to the
8
+ * negative pid reaches every process of the group.
9
+ *
10
+ * The module also records each live child. When the host process ends (a normal exit, or
11
+ * SIGINT, SIGTERM or SIGHUP), it kills each recorded tree, so no child keeps running alone.
12
+ */
13
+
14
+ import { execFile, spawnSync, type ChildProcess } from "node:child_process";
15
+
16
+ const WINDOWS = process.platform === "win32";
17
+
18
+ /**
19
+ * True when a child must start with `detached: true`. On POSIX, `detached` puts the child in a
20
+ * new process group, which `killTree` needs. On Windows, `detached` opens a new console.
21
+ */
22
+ export const OWN_PROCESS_GROUP: boolean = !WINDOWS;
23
+
24
+ function running(child: ChildProcess): boolean {
25
+ return child.pid !== undefined && child.exitCode === null && child.signalCode === null;
26
+ }
27
+
28
+ /** This function kills the process tree of `child`. It does nothing when the child has ended. */
29
+ export function killTree(child: ChildProcess, signal: NodeJS.Signals = "SIGTERM"): void {
30
+ if (!running(child)) return;
31
+ const pid = child.pid!;
32
+ if (WINDOWS) {
33
+ execFile("taskkill", ["/T", "/F", "/PID", String(pid)], { windowsHide: true }, (error) => {
34
+ // taskkill fails when the process ended in the meantime, or when taskkill is missing
35
+ if (error && running(child)) child.kill(signal);
36
+ });
37
+ return;
38
+ }
39
+ try {
40
+ process.kill(-pid, signal);
41
+ } catch {
42
+ // The child is not a group leader (it did not start detached): kill only the child
43
+ child.kill(signal);
44
+ }
45
+ }
46
+
47
+ /** The synchronous form, for the exit of the host, when callbacks do not run any more. */
48
+ function killTreeSync(child: ChildProcess): void {
49
+ if (!running(child)) return;
50
+ const pid = child.pid!;
51
+ try {
52
+ if (WINDOWS) {
53
+ spawnSync("taskkill", ["/T", "/F", "/PID", String(pid)], { windowsHide: true, stdio: "ignore" });
54
+ } else {
55
+ process.kill(-pid, "SIGKILL");
56
+ }
57
+ } catch {
58
+ try {
59
+ child.kill("SIGKILL");
60
+ } catch {
61
+ // The process ended
62
+ }
63
+ }
64
+ }
65
+
66
+ // =============================================================================
67
+ // The live children, and the hooks that kill them when the host ends
68
+ // =============================================================================
69
+
70
+ const live = new Set<ChildProcess>();
71
+ const SIGNALS: readonly NodeJS.Signals[] = ["SIGINT", "SIGTERM", "SIGHUP"];
72
+ let hooked = false;
73
+
74
+ function killAll(): void {
75
+ for (const child of live) killTreeSync(child);
76
+ live.clear();
77
+ }
78
+
79
+ function onExit(): void {
80
+ killAll();
81
+ }
82
+
83
+ function onSignal(signal: NodeJS.Signals): void {
84
+ killAll();
85
+ // Without this handler, Node ends the process on the signal. When this handler is the only one,
86
+ // do the same: remove the hooks and send the signal again.
87
+ if (process.listenerCount(signal) === 1) {
88
+ unhook();
89
+ process.kill(process.pid, signal);
90
+ }
91
+ }
92
+
93
+ function hook(): void {
94
+ if (hooked) return;
95
+ hooked = true;
96
+ process.on("exit", onExit);
97
+ for (const signal of SIGNALS) process.on(signal, onSignal);
98
+ }
99
+
100
+ function unhook(): void {
101
+ if (!hooked) return;
102
+ hooked = false;
103
+ process.off("exit", onExit);
104
+ for (const signal of SIGNALS) process.off(signal, onSignal);
105
+ }
106
+
107
+ /**
108
+ * This function records a child until it ends. While at least one child is recorded, the hooks
109
+ * kill the recorded trees when the host ends. With no recorded child, the host has no hooks.
110
+ */
111
+ export function track(child: ChildProcess): void {
112
+ if (!running(child)) return;
113
+ live.add(child);
114
+ hook();
115
+ const forget = (): void => {
116
+ live.delete(child);
117
+ if (live.size === 0) unhook();
118
+ };
119
+ child.once("exit", forget);
120
+ child.once("error", forget);
121
+ }
package/src/index.ts ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @procedures/shell
3
+ *
4
+ * Generic shell command execution procedures.
5
+ *
6
+ * Provides procedures for running CLI commands:
7
+ * - shell.run - Run command with args
8
+ * - shell.exec - Execute command string via shell
9
+ * - shell.which - Find command path
10
+ * - shell.stream - Run command with args and stream each output line
11
+ *
12
+ * The command helpers (`runCommand`, `streamCommand`, `processes`) are also available without
13
+ * the registration of the procedures, from `@procedures/shell/command`.
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import { Client } from "@procedures/client";
18
+ *
19
+ * const result = await client.call(["shell", "run"], {
20
+ * command: "git",
21
+ * args: ["status"],
22
+ * cwd: "/path/to/repo"
23
+ * });
24
+ * ```
25
+ */
26
+
27
+ // Types
28
+ export type {
29
+ ShellRunInput,
30
+ ShellRunOutput,
31
+ ShellExecInput,
32
+ ShellExecOutput,
33
+ ShellWhichInput,
34
+ ShellWhichOutput,
35
+ ShellStreamInput,
36
+ ShellStreamItem,
37
+ } from "./types.js";
38
+
39
+ export {
40
+ ShellRunInputSchema,
41
+ ShellExecInputSchema,
42
+ ShellWhichInputSchema,
43
+ ShellStreamInputSchema,
44
+ } from "./types.js";
45
+
46
+ // Procedures
47
+ export { shellRun, shellExec, shellWhich, shellStream } from "./procedures/shell/index.js";
48
+
49
+ // Command helpers
50
+ export * from "./command/index.js";
51
+
52
+ // Registration
53
+ export { registerShellProcedures } from "./module.js";
package/src/module.ts ADDED
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Module (no side effects): the procedures of this package as data, for `createRegistry()`.
3
+ *
4
+ * Procedure Registration for shell operations
5
+ */
6
+
7
+ import { createProcedure, zodAdapter, defineModule, type Effects, type ProcedureModule, PROCEDURE_REGISTRY, registerModules, type ProcedureRegistry } from "@procedures/client";
8
+ import { shellRun } from "./procedures/shell/run.js";
9
+ import { shellExec } from "./procedures/shell/exec.js";
10
+ import { shellWhich } from "./procedures/shell/which.js";
11
+ import { shellStream } from "./procedures/shell/stream.js";
12
+ import {
13
+ ShellRunInputSchema,
14
+ ShellExecInputSchema,
15
+ ShellWhichInputSchema,
16
+ ShellStreamInputSchema,
17
+ type ShellRunInput,
18
+ type ShellRunOutput,
19
+ type ShellExecInput,
20
+ type ShellExecOutput,
21
+ type ShellWhichInput,
22
+ type ShellWhichOutput,
23
+ type ShellStreamInput,
24
+ type ShellStreamItem,
25
+ ShellRunOutputSchema,
26
+ ShellExecOutputSchema,
27
+ ShellWhichOutputSchema,
28
+ ShellStreamItemSchema,
29
+ } from "./types.js";
30
+
31
+ /**
32
+ * The effects of a procedure that runs a program that its caller chooses. The program can read
33
+ * and change anything, so "process" names both. A caller of these procedures declares what its
34
+ * program does.
35
+ */
36
+ const RUNS_PROGRAM: Effects = { reads: ["process"], writes: ["process"] };
37
+
38
+ // Procedure definitions
39
+ const shellRunProcedure = createProcedure()
40
+ .path(["shell", "run"])
41
+ .input(zodAdapter<ShellRunInput>(ShellRunInputSchema))
42
+ .output(zodAdapter<ShellRunOutput>(ShellRunOutputSchema))
43
+ .meta({
44
+ description: "Run a command with arguments",
45
+ args: ["command"],
46
+ shorts: { cwd: "C", timeout: "t" },
47
+ output: "json",
48
+ effects: RUNS_PROGRAM,
49
+ })
50
+ .handler(async (input: ShellRunInput, ctx): Promise<ShellRunOutput> => {
51
+ return shellRun(input, ctx);
52
+ })
53
+ .build();
54
+
55
+ const shellExecProcedure = createProcedure()
56
+ .path(["shell", "exec"])
57
+ .input(zodAdapter<ShellExecInput>(ShellExecInputSchema))
58
+ .output(zodAdapter<ShellExecOutput>(ShellExecOutputSchema))
59
+ .meta({
60
+ description: "Execute command string via shell",
61
+ args: ["command"],
62
+ shorts: { cwd: "C", timeout: "t" },
63
+ output: "json",
64
+ effects: RUNS_PROGRAM,
65
+ })
66
+ .handler(async (input: ShellExecInput, ctx): Promise<ShellExecOutput> => {
67
+ return shellExec(input, ctx);
68
+ })
69
+ .build();
70
+
71
+ const shellWhichProcedure = createProcedure()
72
+ .path(["shell", "which"])
73
+ .input(zodAdapter<ShellWhichInput>(ShellWhichInputSchema))
74
+ .output(zodAdapter<ShellWhichOutput>(ShellWhichOutputSchema))
75
+ .meta({
76
+ description: "Find the path to a command",
77
+ args: ["command"],
78
+ shorts: {},
79
+ output: "json",
80
+ // A fixed program (`where` or `which`) reads the folders of PATH and changes nothing
81
+ effects: { reads: ["fs"], writes: [] },
82
+ })
83
+ .handler(async (input: ShellWhichInput, ctx): Promise<ShellWhichOutput> => {
84
+ return shellWhich(input, ctx);
85
+ })
86
+ .build();
87
+
88
+ const shellStreamProcedure = createProcedure()
89
+ .path(["shell", "stream"])
90
+ .input(zodAdapter<ShellStreamInput>(ShellStreamInputSchema))
91
+ .output(zodAdapter<ShellStreamItem>(ShellStreamItemSchema))
92
+ .meta({
93
+ description: "Run a command and stream each output line as it arrives",
94
+ args: ["command"],
95
+ shorts: { cwd: "C", timeout: "t" },
96
+ output: "json",
97
+ streaming: true,
98
+ effects: RUNS_PROGRAM,
99
+ })
100
+ .handler(shellStream)
101
+ .build();
102
+
103
+ /**
104
+ * The module of this package, for `createRegistry()`: its procedures as data. Importing this
105
+ * file registers nothing. (`register.ts` registers the same procedures in the global registry.)
106
+ */
107
+ export const shellModule: ProcedureModule = defineModule("@procedures/shell", [
108
+ shellRunProcedure,
109
+ shellExecProcedure,
110
+ shellWhichProcedure,
111
+ shellStreamProcedure,
112
+ ]);
113
+
114
+ export default shellModule;
115
+
116
+ /**
117
+ * This function registers the procedures of @procedures/shell, and of the modules that it requires, in
118
+ * a registry. The default is the global registry. Importing this file registers nothing: the
119
+ * `./register` entry starts this function (ARCHITECTURE-PROPOSALS-2026-10, P9).
120
+ */
121
+ export function registerShellProcedures(registry: ProcedureRegistry = PROCEDURE_REGISTRY): void {
122
+ registerModules(registry, [shellModule]);
123
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * shell.exec procedure
3
+ *
4
+ * Execute a command string via shell.
5
+ *
6
+ * This is the one procedure that runs a shell: the command is a shell command line. It is not an
7
+ * MCP tool, and a data-driven procedure cannot call it (BUGS-2026-07 H18). The shell runs through
8
+ * the command helpers, so the timeout and `ctx.signal` kill the shell and every program that it
9
+ * started (deep dive WRP-10), and the `stdin` input reaches the command.
10
+ */
11
+
12
+ import { runCommand } from "../../command/run.js";
13
+ import type { ShellExecInput, ShellExecOutput } from "../../types.js";
14
+
15
+ /** The program and the arguments that run `command` in a shell, as Node's `exec` does. */
16
+ export function shellCommandLine(
17
+ command: string,
18
+ shell: boolean | string,
19
+ ): { file: string; args: string[]; verbatim: boolean } {
20
+ if (process.platform === "win32") {
21
+ const file = typeof shell === "string" ? shell : process.env["ComSpec"] ?? "cmd.exe";
22
+ if (/(?:^|[\\/])cmd(?:\.exe)?$/i.test(file)) {
23
+ // cmd.exe reads the rest of its command line as it is, so the arguments get no quotes
24
+ return { file, args: ["/d", "/s", "/c", `"${command}"`], verbatim: true };
25
+ }
26
+ return { file, args: ["-c", command], verbatim: false };
27
+ }
28
+ return { file: typeof shell === "string" ? shell : "/bin/sh", args: ["-c", command], verbatim: false };
29
+ }
30
+
31
+ export async function shellExec(
32
+ input: ShellExecInput,
33
+ ctx?: { signal?: AbortSignal | undefined }
34
+ ): Promise<ShellExecOutput> {
35
+ const { file, args, verbatim } = shellCommandLine(input.command, input.shell);
36
+ const result = await runCommand(file, {
37
+ args,
38
+ windowsVerbatimArguments: verbatim,
39
+ cwd: input.cwd,
40
+ env: input.env,
41
+ timeout: input.timeout,
42
+ signal: ctx?.signal,
43
+ input: input.stdin,
44
+ maxOutputBytes: input.maxBuffer,
45
+ });
46
+
47
+ return {
48
+ exitCode: result.exitCode,
49
+ stdout: result.stdout,
50
+ stderr: result.stderr,
51
+ success: result.success,
52
+ duration: result.duration,
53
+ signal: result.signal,
54
+ };
55
+ }
@@ -0,0 +1,4 @@
1
+ export { shellRun } from "./run.js";
2
+ export { shellExec } from "./exec.js";
3
+ export { shellWhich } from "./which.js";
4
+ export { shellStream } from "./stream.js";
@@ -0,0 +1,47 @@
1
+ /**
2
+ * shell.run procedure
3
+ *
4
+ * Run a command with arguments and return output.
5
+ */
6
+
7
+ import { runCommand } from "../../command/run.js";
8
+ import type { ShellRunInput, ShellRunOutput } from "../../types.js";
9
+
10
+ /**
11
+ * Most output kept per stream. Output beyond it is dropped (and reported), so a command
12
+ * that prints without end cannot exhaust memory. The process is not killed.
13
+ */
14
+ const MAX_OUTPUT_BYTES = 64 * 1024 * 1024;
15
+
16
+ /**
17
+ * Run a command with its arguments and no shell. The timeout and `ctx.signal` kill the whole
18
+ * process tree (deep dive WRP-10).
19
+ */
20
+ export async function shellRun(
21
+ input: ShellRunInput,
22
+ ctx?: { signal?: AbortSignal | undefined }
23
+ ): Promise<ShellRunOutput> {
24
+ const result = await runCommand(input.command, {
25
+ args: input.args,
26
+ cwd: input.cwd,
27
+ env: input.env,
28
+ timeout: input.timeout,
29
+ signal: ctx?.signal,
30
+ maxOutputBytes: MAX_OUTPUT_BYTES,
31
+ encoding: input.encoding as BufferEncoding,
32
+ });
33
+
34
+ const note = `\n[output truncated at ${MAX_OUTPUT_BYTES} bytes]`;
35
+ const output: ShellRunOutput = {
36
+ exitCode: result.exitCode,
37
+ stdout: result.truncated?.stdout ? result.stdout + note : result.stdout,
38
+ stderr: result.truncated?.stderr ? result.stderr + note : result.stderr,
39
+ success: result.success,
40
+ duration: result.duration,
41
+ };
42
+ // A signal means the process was killed (for example by the timeout)
43
+ if (result.signal) {
44
+ output.signal = result.signal;
45
+ }
46
+ return output;
47
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * shell.stream procedure
3
+ *
4
+ * Run a command and stream each output line as it arrives. The procedure is a generator: the
5
+ * caller reads `{ type: "stdout" | "stderr", line }` items while the command runs, then one
6
+ * `{ type: "exit" }` item. When the caller stops reading, or its signal aborts, the command and
7
+ * the programs that it started end.
8
+ *
9
+ * The queue to the reader has a limit: when the reader is slow, the command waits in its next
10
+ * write (deep dive WRP-9). A character whose bytes arrive in two chunks stays whole (deep dive
11
+ * WRP-2). The work is done by `streamCommand`.
12
+ */
13
+
14
+ import { streamCommand } from "../../command/stream.js";
15
+ import type { ShellStreamInput, ShellStreamItem } from "../../types.js";
16
+
17
+ /**
18
+ * Run a command and stream each output line as it arrives
19
+ */
20
+ export async function* shellStream(
21
+ input: ShellStreamInput,
22
+ ctx?: { signal?: AbortSignal | undefined }
23
+ ): AsyncGenerator<ShellStreamItem, void, undefined> {
24
+ // No shell: the arguments cannot inject commands
25
+ for await (const item of streamCommand(input.command, {
26
+ args: input.args,
27
+ cwd: input.cwd,
28
+ env: input.env,
29
+ timeout: input.timeout,
30
+ signal: ctx?.signal,
31
+ })) {
32
+ if (item.type === "exit") {
33
+ yield {
34
+ type: "exit",
35
+ exitCode: item.exitCode,
36
+ duration: item.duration,
37
+ ...(item.signal ? { signal: item.signal } : {}),
38
+ };
39
+ } else {
40
+ yield item;
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * shell.which procedure
3
+ *
4
+ * Find the full path to a command.
5
+ */
6
+
7
+ import { runCommand } from "../../command/run.js";
8
+ import type { ShellWhichInput, ShellWhichOutput } from "../../types.js";
9
+
10
+ export async function shellWhich(
11
+ input: ShellWhichInput,
12
+ ctx?: { signal?: AbortSignal | undefined }
13
+ ): Promise<ShellWhichOutput> {
14
+ // The name is one argument of where/which, with no shell. (Before, `where ${command}` ran in a
15
+ // shell, so a name such as `x & calc` ran a second command: deep dive WRP-6.)
16
+ if (input.command.startsWith("-")) {
17
+ return { path: null, found: false };
18
+ }
19
+ const program = process.platform === "win32" ? "where" : "which";
20
+ const result = await runCommand(program, { args: [input.command], signal: ctx?.signal, timeout: 30_000 });
21
+ if (!result.success) {
22
+ return { path: null, found: false };
23
+ }
24
+ const path = result.stdout.trim().split(/\r?\n/)[0]?.trim() ?? "";
25
+ return path.length > 0 ? { path, found: true } : { path: null, found: false };
26
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Importing this file registers the procedures of @procedures/shell, and of the modules that it
3
+ * requires, in the global registry: `import "@procedures/shell/register"`. The package root and the
4
+ * `./module` entry register nothing (ARCHITECTURE-PROPOSALS-2026-10, P9).
5
+ */
6
+
7
+ import { registerShellProcedures } from "./module.js";
8
+
9
+ export * from "./module.js";
10
+
11
+ registerShellProcedures();
package/src/types.ts ADDED
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Shell command types
3
+ */
4
+
5
+ import { z } from "zod";
6
+
7
+ // =============================================================================
8
+ // shell.run Types
9
+ // =============================================================================
10
+
11
+ export const ShellRunInputSchema: z.ZodObject<{
12
+ command: z.ZodString;
13
+ args: z.ZodDefault<z.ZodArray<z.ZodString>>;
14
+ cwd: z.ZodOptional<z.ZodString>;
15
+ env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
16
+ timeout: z.ZodOptional<z.ZodNumber>;
17
+ encoding: z.ZodDefault<z.ZodString>;
18
+ }> = z.object({
19
+ /** Command to run */
20
+ command: z.string().describe("Command to run"),
21
+ /** Arguments to pass */
22
+ args: z.array(z.string()).default([]).describe("Arguments to pass"),
23
+ /** Working directory */
24
+ cwd: z.string().optional().describe("Working directory"),
25
+ /** Environment variables to set */
26
+ env: z.record(z.string(), z.string()).optional().describe("Environment variables to set"),
27
+ /** Timeout in milliseconds */
28
+ timeout: z.number().optional().describe("Timeout in milliseconds"),
29
+ /** Encoding for output (default: utf8) */
30
+ encoding: z.string().default("utf8").describe("Encoding for output (default: utf8)"),
31
+ });
32
+
33
+ export type ShellRunInput = z.infer<typeof ShellRunInputSchema>;
34
+
35
+ export interface ShellRunOutput {
36
+ /** Exit code */
37
+ exitCode: number;
38
+ /** Standard output */
39
+ stdout: string;
40
+ /** Standard error */
41
+ stderr: string;
42
+ /** Whether command succeeded (exit code 0) */
43
+ success: boolean;
44
+ /** Duration in milliseconds */
45
+ duration: number;
46
+ /** The signal that ended the process, if it was killed (for example by the timeout) */
47
+ signal?: string | undefined;
48
+ }
49
+
50
+ // =============================================================================
51
+ // shell.exec Types (more options)
52
+ // =============================================================================
53
+
54
+ export const ShellExecInputSchema: z.ZodObject<{
55
+ command: z.ZodString;
56
+ cwd: z.ZodOptional<z.ZodString>;
57
+ env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
58
+ timeout: z.ZodOptional<z.ZodNumber>;
59
+ shell: z.ZodDefault<z.ZodUnion<readonly [z.ZodBoolean, z.ZodString]>>;
60
+ maxBuffer: z.ZodOptional<z.ZodNumber>;
61
+ stdin: z.ZodOptional<z.ZodString>;
62
+ }> = z.object({
63
+ /** Full command string to execute via shell */
64
+ command: z.string().describe("Full command string to execute via shell"),
65
+ /** Working directory */
66
+ cwd: z.string().optional().describe("Working directory"),
67
+ /** Environment variables */
68
+ env: z.record(z.string(), z.string()).optional().describe("Environment variables"),
69
+ /** Timeout in milliseconds. At the timeout, the shell and the programs that it started are killed. */
70
+ timeout: z.number().optional().describe("Timeout in milliseconds. At the timeout, the shell and the programs that it started are killed."),
71
+ /** Shell to use: a path, or `true` for the system shell (`cmd.exe` or `/bin/sh`). `false` is the same as `true`. */
72
+ shell: z.union([z.boolean(), z.string()]).default(true).describe("Shell to use: a path, or `true` for the system shell (`cmd.exe` or `/bin/sh`). `false` is the same as `true`."),
73
+ /** The most bytes kept of each output stream (default: 64 MiB). The procedure drops the rest. */
74
+ maxBuffer: z.number().optional().describe("The most bytes kept of each output stream (default: 64 MiB). The procedure drops the rest."),
75
+ /** Text to write to stdin. Without it, stdin is empty. */
76
+ stdin: z.string().optional().describe("Text to write to stdin. Without it, stdin is empty."),
77
+ });
78
+
79
+ export type ShellExecInput = z.infer<typeof ShellExecInputSchema>;
80
+
81
+ export interface ShellExecOutput {
82
+ exitCode: number;
83
+ stdout: string;
84
+ stderr: string;
85
+ success: boolean;
86
+ duration: number;
87
+ /** Signal that terminated the process (if any) */
88
+ signal?: string | undefined;
89
+ }
90
+
91
+ // =============================================================================
92
+ // shell.which Types
93
+ // =============================================================================
94
+
95
+ export const ShellWhichInputSchema: z.ZodObject<{
96
+ command: z.ZodString;
97
+ }> = z.object({
98
+ /** Command to find */
99
+ command: z.string().describe("Command to find"),
100
+ });
101
+
102
+ export type ShellWhichInput = z.infer<typeof ShellWhichInputSchema>;
103
+
104
+ export interface ShellWhichOutput {
105
+ /** Full path to command, or null if not found */
106
+ path: string | null;
107
+ /** Whether command was found */
108
+ found: boolean;
109
+ }
110
+
111
+ // =============================================================================
112
+ // shell.stream Types - Run a command and stream each output line as it arrives
113
+ // =============================================================================
114
+
115
+ export const ShellStreamInputSchema: z.ZodObject<{
116
+ command: z.ZodString;
117
+ args: z.ZodDefault<z.ZodArray<z.ZodString>>;
118
+ cwd: z.ZodOptional<z.ZodString>;
119
+ env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
120
+ timeout: z.ZodOptional<z.ZodNumber>;
121
+ }> = z.object({
122
+ command: z.string().describe("The program to start (no shell: arguments go in args)"),
123
+ args: z.array(z.string()).default([]).describe("The arguments of the program"),
124
+ cwd: z.string().optional().describe("Working directory"),
125
+ env: z.record(z.string(), z.string()).optional().describe("Environment variables (added to the environment of the host)"),
126
+ timeout: z.number().optional().describe("Timeout in milliseconds. At the timeout, the program and its children are killed."),
127
+ });
128
+
129
+ export type ShellStreamInput = z.infer<typeof ShellStreamInputSchema>;
130
+
131
+ /** One item of shell.stream: an output line, or the exit of the command (the last item). */
132
+ export type ShellStreamItem =
133
+ | { type: "stdout" | "stderr"; line: string }
134
+ | { type: "exit"; exitCode: number; duration: number; signal?: string | undefined };
135
+
136
+ // =============================================================================
137
+ // Output schemas (roadmap 2.1)
138
+ // =============================================================================
139
+
140
+ // Loose objects: a field that a schema does not name stays in the result (a strict schema would
141
+ // remove it when the output is validated).
142
+
143
+ const commandResult = {
144
+ exitCode: z.number().describe("The exit code (0 is success, -1 when a signal ended the program)"),
145
+ stdout: z.string().describe("The standard output"),
146
+ stderr: z.string().describe("The standard error"),
147
+ success: z.boolean().describe("True when the exit code is 0"),
148
+ duration: z.number().describe("The time of the command, in milliseconds"),
149
+ signal: z.string().optional().describe("The signal that ended the program (for example at the timeout)"),
150
+ };
151
+
152
+ export const ShellRunOutputSchema: z.ZodType<ShellRunOutput> = z.looseObject(commandResult);
153
+
154
+ export const ShellExecOutputSchema: z.ZodType<ShellExecOutput> = z.looseObject(commandResult);
155
+
156
+ export const ShellWhichOutputSchema: z.ZodType<ShellWhichOutput> = z.looseObject({
157
+ path: z.string().nullable().describe("The full path of the program, or null when it is not found"),
158
+ found: z.boolean().describe("True when the program is found"),
159
+ });
160
+
161
+ export const ShellStreamItemSchema: z.ZodType<ShellStreamItem> = z.union([
162
+ z.looseObject({
163
+ type: z.enum(["stdout", "stderr"]).describe("The stream of the line"),
164
+ line: z.string().describe("One line of output, without its line end"),
165
+ }),
166
+ z.looseObject({
167
+ type: z.literal("exit").describe("The end of the stream (the last item)"),
168
+ exitCode: z.number().describe("The exit code (-1 when a signal ended the program)"),
169
+ duration: z.number().describe("The time of the command, in milliseconds"),
170
+ signal: z.string().optional().describe("The signal that ended the program"),
171
+ }),
172
+ ]);