@procedures/cli-run 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.
@@ -0,0 +1,142 @@
1
+ /**
2
+ * cli.run procedure
3
+ *
4
+ * Wraps the mark CLI as a procedure using client-shell.
5
+ * This allows calling any mark CLI command programmatically. The mark CLI itself uses a running
6
+ * CLI server when that server is safe to use (same folder, same build, its token).
7
+ */
8
+
9
+ import { existsSync } from "node:fs";
10
+ import { dirname, join } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import type { ProcedureContext } from "@procedures/client";
13
+ import type { CliRunInput, CliRunOutput } from "../../types.js";
14
+
15
+ /**
16
+ * Find the mark CLI of the workspace that contains this package (packages/mark/dist/cli.js)
17
+ */
18
+ function resolveMarkCli(): string {
19
+ let dir = dirname(fileURLToPath(import.meta.url));
20
+ for (;;) {
21
+ if (existsSync(join(dir, "pnpm-workspace.yaml"))) {
22
+ const cli = join(dir, "packages", "mark", "dist", "cli.js");
23
+ if (existsSync(cli)) {
24
+ return cli;
25
+ }
26
+ break;
27
+ }
28
+ const parent = dirname(dir);
29
+ if (parent === dir) {
30
+ break;
31
+ }
32
+ dir = parent;
33
+ }
34
+ throw new Error("Could not find the mark CLI (packages/mark/dist/cli.js in the workspace). Run pnpm build.");
35
+ }
36
+
37
+ /**
38
+ * The argv of `mark` for an input: the command path, then each named argument as
39
+ * `--key=<text>`, then `--` and the positional arguments.
40
+ *
41
+ * A string value goes as it is. Another value goes as JSON, which `mark` converts by the type of
42
+ * the field: `--flag=false` for a boolean, `--files=["a","b"]` for an array, `--env={"A":"1"}`
43
+ * for a record. Before, the procedure sent `--key value`, so a value that starts with "-" became
44
+ * a flag, and it dropped a `false` boolean, so a default of `true` could not be turned off.
45
+ * The `--` keeps a positional argument that starts with "-" from becoming a flag.
46
+ */
47
+ export function buildMarkArgs(input: Pick<CliRunInput, "path" | "positional" | "args">): string[] {
48
+ const args: string[] = [...input.path];
49
+ for (const [key, value] of Object.entries(input.args ?? {})) {
50
+ if (value === undefined || value === null) continue;
51
+ args.push(`--${key}=${typeof value === "string" ? value : JSON.stringify(value)}`);
52
+ }
53
+ if (input.positional && input.positional.length > 0) {
54
+ args.push("--", ...input.positional.map(String));
55
+ }
56
+ return args;
57
+ }
58
+
59
+ /**
60
+ * Execute via shell (spawn node process)
61
+ */
62
+ async function shellExecution(
63
+ input: CliRunInput,
64
+ ctx: ProcedureContext,
65
+ startTime: number
66
+ ): Promise<CliRunOutput> {
67
+ const args = buildMarkArgs(input);
68
+
69
+ // Build shell input
70
+ const shellInput: {
71
+ command: string;
72
+ args: string[];
73
+ cwd?: string | undefined;
74
+ timeout?: number | undefined;
75
+ } = {
76
+ command: process.execPath,
77
+ args: [resolveMarkCli(), ...args],
78
+ };
79
+
80
+ if (input.cwd !== undefined) shellInput.cwd = input.cwd;
81
+ if (input.timeout !== undefined) shellInput.timeout = input.timeout;
82
+
83
+ // Call shell.run
84
+ const result = await ctx.client.call<
85
+ typeof shellInput,
86
+ {
87
+ exitCode: number;
88
+ stdout: string;
89
+ stderr: string;
90
+ signal?: string | undefined;
91
+ }
92
+ >(["shell", "run"], shellInput);
93
+
94
+ return {
95
+ exitCode: result.exitCode,
96
+ stdout: result.stdout,
97
+ stderr: result.stderr,
98
+ success: result.exitCode === 0,
99
+ duration: Date.now() - startTime,
100
+ };
101
+ }
102
+
103
+ /**
104
+ * Run a mark CLI command
105
+ *
106
+ *
107
+ * @example
108
+ * // Equivalent to: mark lib new my-package
109
+ * await client.call(["cli", "run"], {
110
+ * path: ["lib", "new"],
111
+ * positional: ["my-package"],
112
+ * });
113
+ *
114
+ * @example
115
+ * // Equivalent to: mark procedure new fs.read --description "Read a file"
116
+ * await client.call(["cli", "run"], {
117
+ * path: ["procedure", "new"],
118
+ * positional: ["fs.read"],
119
+ * args: { description: "Read a file" },
120
+ * });
121
+ */
122
+ export async function cliRun(
123
+ input: CliRunInput,
124
+ ctx: ProcedureContext
125
+ ): Promise<CliRunOutput> {
126
+ const startTime = Date.now();
127
+
128
+ try {
129
+ // Always through the mark CLI. Before, a second path sent the input straight to a running
130
+ // CLI server, without mark's argument mapping, so the two paths gave different results
131
+ // (a "dry run" wrote files), and it had its own copy of the lockfile logic (deep dive CLI-12).
132
+ return await shellExecution(input, ctx, startTime);
133
+ } catch (error) {
134
+ return {
135
+ exitCode: 1,
136
+ stdout: "",
137
+ stderr: error instanceof Error ? error.message : String(error),
138
+ success: false,
139
+ duration: Date.now() - startTime,
140
+ };
141
+ }
142
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Importing this file registers the procedures of @procedures/cli-run, and of the modules that it
3
+ * requires, in the global registry: `import "@procedures/cli-run/register"`. The package root and the
4
+ * `./module` entry register nothing (ARCHITECTURE-PROPOSALS-2026-10, P9).
5
+ */
6
+
7
+ import { registerCliProcedures } from "./module.js";
8
+
9
+ export * from "./module.js";
10
+
11
+ registerCliProcedures();
package/src/types.ts ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Type definitions for client-cli procedures
3
+ *
4
+ * client-cli wraps the mark CLI as procedures using client-shell.
5
+ */
6
+
7
+ import { z } from "zod";
8
+
9
+ // =============================================================================
10
+ // cli.run Types
11
+ // =============================================================================
12
+
13
+ export const CliRunInputSchema: z.ZodObject<{
14
+ path: z.ZodArray<z.ZodString>;
15
+ args: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>>;
16
+ positional: z.ZodOptional<z.ZodArray<z.ZodString>>;
17
+ cwd: z.ZodOptional<z.ZodString>;
18
+ timeout: z.ZodOptional<z.ZodNumber>;
19
+ }> = z.object({
20
+ /** Procedure path to call, e.g. ["lib", "new"] for "mark lib new" */
21
+ path: z.array(z.string()).describe("Procedure path to call, e.g. [\"lib\", \"new\"] for \"mark lib new\""),
22
+ /** Named arguments as key-value pairs, e.g. { name: "foo" } -> "--name foo" */
23
+ args: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional().describe("Named arguments as key-value pairs, e.g. { name: \"foo\" } -> \"--name foo\""),
24
+ /** Positional arguments after the path */
25
+ positional: z.array(z.string()).optional().describe("Positional arguments after the path"),
26
+ /** Working directory */
27
+ cwd: z.string().optional().describe("Working directory"),
28
+ /** Timeout in milliseconds */
29
+ timeout: z.number().optional().describe("Timeout in milliseconds"),
30
+ });
31
+
32
+ export type CliRunInput = z.infer<typeof CliRunInputSchema>;
33
+
34
+ export interface CliRunOutput {
35
+ /** Exit code of the command */
36
+ exitCode: number;
37
+ /** Standard output */
38
+ stdout: string;
39
+ /** Standard error */
40
+ stderr: string;
41
+ /** Whether command succeeded (exit code 0) */
42
+ success: boolean;
43
+ /** Duration in milliseconds */
44
+ duration: number;
45
+ }
46
+
47
+ /** The output schema of cli.run (roadmap 2.1). Loose: a field that it does not name stays. */
48
+ export const CliRunOutputSchema: z.ZodType<CliRunOutput> = z.looseObject({
49
+ exitCode: z.number().describe("The exit code of mark"),
50
+ stdout: z.string().describe("The standard output of mark"),
51
+ stderr: z.string().describe("The standard error of mark"),
52
+ success: z.boolean().describe("True when the exit code is 0"),
53
+ duration: z.number().describe("The time of the command, in milliseconds"),
54
+ });