@procedures/procedure 0.0.0-stage → 0.1.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 (40) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/LICENSE +21 -0
  3. package/README.md +405 -2
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/index.d.ts +9 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +12 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/module.d.ts +17 -0
  10. package/dist/module.d.ts.map +1 -0
  11. package/dist/module.js +41 -0
  12. package/dist/module.js.map +1 -0
  13. package/dist/procedures/procedure/index.d.ts +5 -0
  14. package/dist/procedures/procedure/index.d.ts.map +1 -0
  15. package/dist/procedures/procedure/index.js +5 -0
  16. package/dist/procedures/procedure/index.js.map +1 -0
  17. package/dist/procedures/procedure/new.d.ts +54 -0
  18. package/dist/procedures/procedure/new.d.ts.map +1 -0
  19. package/dist/procedures/procedure/new.js +442 -0
  20. package/dist/procedures/procedure/new.js.map +1 -0
  21. package/dist/procedures/procedure/registry.d.ts +10 -0
  22. package/dist/procedures/procedure/registry.d.ts.map +1 -0
  23. package/dist/procedures/procedure/registry.js +67 -0
  24. package/dist/procedures/procedure/registry.js.map +1 -0
  25. package/dist/register.d.ts +8 -0
  26. package/dist/register.d.ts.map +1 -0
  27. package/dist/register.js +16 -0
  28. package/dist/register.js.map +1 -0
  29. package/dist/types.d.ts +29 -0
  30. package/dist/types.d.ts.map +1 -0
  31. package/dist/types.js +34 -0
  32. package/dist/types.js.map +1 -0
  33. package/package.json +72 -3
  34. package/src/index.ts +14 -0
  35. package/src/module.ts +50 -0
  36. package/src/procedures/procedure/index.ts +5 -0
  37. package/src/procedures/procedure/new.ts +493 -0
  38. package/src/procedures/procedure/registry.ts +92 -0
  39. package/src/register.ts +19 -0
  40. package/src/types.ts +63 -0
package/dist/types.js ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Type definitions for client-procedure
3
+ */
4
+ import { z } from "zod";
5
+ // =============================================================================
6
+ // procedure.new Types
7
+ // =============================================================================
8
+ export const ProcedureNewInputSchema = z.object({
9
+ /** Procedure name (e.g., "greet", "user.create" or "fs.readJson") */
10
+ name: z
11
+ .string()
12
+ .regex(/^[a-z][a-zA-Z0-9]*(\.[a-z][a-zA-Z0-9]*)*$/, "Name must be dot-separated segments, each starting with a lowercase letter (camelCase is allowed)").describe("Procedure name (e.g., \"greet\", \"user.create\" or \"fs.readJson\")"),
13
+ /** Namespace override (defaults to first segment of name): the folder under src/procedures */
14
+ namespace: z
15
+ .string()
16
+ .regex(/^[a-z][a-zA-Z0-9]*$/, "Namespace must be one segment that starts with a lowercase letter")
17
+ .optional().describe("Namespace override (defaults to first segment of name): the folder under src/procedures"),
18
+ /** Procedure description for CLI help */
19
+ description: z.string().optional().describe("Procedure description for CLI help"),
20
+ /** Project path (defaults to cwd) */
21
+ path: z.string().optional().describe("Project path (defaults to cwd)"),
22
+ /** Preview changes without creating */
23
+ dryRun: z.boolean().default(false).describe("Preview changes without creating"),
24
+ });
25
+ /** The output schema of procedure.new (roadmap 2.1). Loose: a field that it does not name stays. */
26
+ export const ProcedureNewOutputSchema = z.looseObject({
27
+ success: z.boolean().describe("True when the procedure was made"),
28
+ procedurePath: z.array(z.string()).describe('The path of the new procedure, for example ["user", "create"]'),
29
+ created: z.array(z.string()).describe("The files that the call made"),
30
+ modified: z.array(z.string()).describe("The files that the call changed (types.ts, register.ts)"),
31
+ operations: z.array(z.string()).describe("The steps that the call ran"),
32
+ errors: z.array(z.string()).describe("The errors"),
33
+ });
34
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,gFAAgF;AAChF,sBAAsB;AACtB,gFAAgF;AAEhF,MAAM,CAAC,MAAM,uBAAuB,GAM/B,CAAC,CAAC,MAAM,CAAC;IACZ,qEAAqE;IACrE,IAAI,EAAE,CAAC;SACJ,MAAM,EAAE;SACR,KAAK,CACJ,2CAA2C,EAC3C,mGAAmG,CACpG,CAAC,QAAQ,CAAC,sEAAsE,CAAC;IACpF,8FAA8F;IAC9F,SAAS,EAAE,CAAC;SACT,MAAM,EAAE;SACR,KAAK,CAAC,qBAAqB,EAAE,mEAAmE,CAAC;SACjG,QAAQ,EAAE,CAAC,QAAQ,CAAC,yFAAyF,CAAC;IACjH,yCAAyC;IACzC,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,oCAAoC,CAAC;IACjF,qCAAqC;IACrC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,gCAAgC,CAAC;IACtE,uCAAuC;IACvC,MAAM,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,kCAAkC,CAAC;CAChF,CAAC,CAAC;AAmBH,oGAAoG;AACpG,MAAM,CAAC,MAAM,wBAAwB,GAAkC,CAAC,CAAC,WAAW,CAAC;IACnF,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,CAAC,kCAAkC,CAAC;IACjE,aAAa,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,CAAC,+DAA+D,CAAC;IAC5G,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,CAAC,8BAA8B,CAAC;IACrE,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,CAAC,yDAAyD,CAAC;IACjG,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,CAAC,6BAA6B,CAAC;IACvE,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,CAAC,YAAY,CAAC;CACnD,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,75 @@
1
1
  {
2
+ "$schema": "https://json.schemastore.org/package",
2
3
  "name": "@procedures/procedure",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
4
+ "version": "0.1.0",
5
+ "description": "Procedure scaffolding and management: procedure.new writes typed procedure files, plus get, list, define and the procedure store.",
6
+ "keywords": [
7
+ "scaffolding",
8
+ "codegen",
9
+ "generator",
10
+ "procedures"
11
+ ],
12
+ "homepage": "https://mark1russell7.github.io/client/",
13
+ "bugs": {
14
+ "url": "https://github.com/mark1russell7/client/issues"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/mark1russell7/client.git",
19
+ "directory": "packages/client-procedure"
20
+ },
21
+ "license": "MIT",
22
+ "author": "Mark Russell (https://github.com/mark1russell7)",
23
+ "type": "module",
24
+ "main": "./dist/index.js",
25
+ "types": "./dist/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.js"
30
+ },
31
+ "./module": {
32
+ "types": "./dist/module.d.ts",
33
+ "import": "./dist/module.js"
34
+ }
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "src",
39
+ "!src/**/*.test.ts",
40
+ "!src/**/*.test.tsx",
41
+ "!src/**/__tests__",
42
+ "CHANGELOG.md"
43
+ ],
44
+ "sideEffects": [
45
+ "./dist/index.js",
46
+ "./dist/register.js"
47
+ ],
48
+ "engines": {
49
+ "node": ">=22"
50
+ },
51
+ "publishConfig": {
52
+ "access": "public",
53
+ "provenance": true
54
+ },
55
+ "devDependencies": {
56
+ "typescript": "^7.0.2",
57
+ "@types/node": "^22.0.0",
58
+ "@mark1russell7/cue": "^1.0.0",
59
+ "vitest": "^5.0.3"
60
+ },
61
+ "dependencies": {
62
+ "zod": "^4.6.5",
63
+ "@procedures/client": "0.1.0"
64
+ },
65
+ "client": {
66
+ "procedures": "./dist/register.js"
67
+ },
68
+ "scripts": {
69
+ "build": "tsc -b",
70
+ "typecheck": "tsc --noEmit",
71
+ "clean": "rm -rf dist .tsbuildinfo",
72
+ "test": "vitest run --passWithNoTests",
73
+ "test:watch": "vitest"
74
+ }
6
75
  }
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @procedures/procedure
3
+ *
4
+ * Procedure scaffolding procedures - procedure.new
5
+ */
6
+
7
+ // Importing the package registers its procedures, as in the other client packages (BUGS-2026-07 H18)
8
+ import "./register.js";
9
+
10
+ // Re-export types
11
+ export * from "./types.js";
12
+
13
+ // Re-export procedures
14
+ export { procedureNew } from "./procedures/procedure/index.js";
package/src/module.ts ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Module (no side effects): the procedures of this package as data, for `createRegistry()`.
3
+ *
4
+ * Procedure Registration for procedure operations
5
+ *
6
+ * This is the canonical home for procedure.* procedures.
7
+ * client-cli no longer registers these to avoid duplicates.
8
+ */
9
+
10
+ import { createProcedure, zodAdapter, defineModule, type AnyProcedure, type ProcedureModule } from "@procedures/client";
11
+ import { procedureNew } from "./procedures/procedure/index.js";
12
+ import { procedureRegistryProcedures } from "./procedures/procedure/registry.js";
13
+ import {
14
+ ProcedureNewInputSchema,
15
+ type ProcedureNewInput,
16
+ type ProcedureNewOutput,
17
+ ProcedureNewOutputSchema,
18
+ } from "./types.js";
19
+ import type { ProcedureContext } from "@procedures/client";
20
+
21
+ // procedure.new procedure
22
+ const procedureNewProcedure = createProcedure()
23
+ .path(["procedure", "new"])
24
+ .input(zodAdapter<ProcedureNewInput>(ProcedureNewInputSchema))
25
+ .output(zodAdapter<ProcedureNewOutput>(ProcedureNewOutputSchema))
26
+ .meta({
27
+ description: "Scaffold a new procedure with types and registration boilerplate",
28
+ calls: ["fs.exists", "fs.read", "fs.write", "fs.mkdir"],
29
+ args: ["name"],
30
+ shorts: { namespace: "n", description: "d", path: "p", dryRun: "D" },
31
+ output: "text",
32
+ })
33
+ .handler(async (input: ProcedureNewInput, ctx: ProcedureContext): Promise<ProcedureNewOutput> => {
34
+ return procedureNew(input, ctx);
35
+ })
36
+ .build();
37
+
38
+ /**
39
+ * The module of this package, for `createRegistry()`: its procedures as data. Importing this
40
+ * file registers nothing. (`register.ts` registers the same procedures in the global registry.)
41
+ * `procedure.new` calls `fs.*`: the host gives them (this package does not depend on client-fs).
42
+ */
43
+ export const procedureModule: ProcedureModule = defineModule("@procedures/procedure", [
44
+ // procedure.new
45
+ procedureNewProcedure as AnyProcedure,
46
+ // procedure.export (from registry.ts)
47
+ ...procedureRegistryProcedures,
48
+ ]);
49
+
50
+ export default procedureModule;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * procedure namespace procedures
3
+ */
4
+
5
+ export { procedureNew } from "./new.js";
@@ -0,0 +1,493 @@
1
+ /**
2
+ * procedure.new - Scaffold a new procedure
3
+ *
4
+ * Creates the procedure file, its types and its registration. A dot-notation name such as
5
+ * "user.create" makes the procedure at ["user", "create"]: the file is
6
+ * procedures/user/create.ts, and "api.v2.users.create" gives procedures/api/v2/users/create.ts.
7
+ *
8
+ * The scaffolded code compiles with the strict options of the workspace (deep dive CLI-13):
9
+ * - the handler names its unused input `_input` (noUnusedParameters);
10
+ * - types.ts is created, or gets `import { z } from "zod"`, when it needs it;
11
+ * - the registration goes into src/register.ts.
12
+ * A name whose path, file, types or registration exists already is an error, and nothing is
13
+ * written. Before, "a.b.c" and "a.c" wrote the same file, and existing types were reused silently.
14
+ */
15
+
16
+ import { join } from "node:path";
17
+ import { PROCEDURE_REGISTRY, type ProcedureContext } from "@procedures/client";
18
+ import type { ProcedureNewInput, ProcedureNewOutput } from "../../types.js";
19
+
20
+ interface FsExistsOutput { exists: boolean; path: string; }
21
+ interface FsReadOutput { path: string; content: string; }
22
+ interface FsWriteOutput { path: string; bytesWritten: number; }
23
+ interface FsMkdirOutput { path: string; created: boolean; }
24
+
25
+ /**
26
+ * Check if path exists
27
+ */
28
+ async function pathExists(pathStr: string, ctx: ProcedureContext): Promise<boolean> {
29
+ try {
30
+ const result = await ctx.client.call<{ path: string }, FsExistsOutput>(
31
+ ["fs", "exists"],
32
+ { path: pathStr }
33
+ );
34
+ return result.exists;
35
+ } catch {
36
+ return false;
37
+ }
38
+ }
39
+
40
+ async function readText(pathStr: string, ctx: ProcedureContext): Promise<string | undefined> {
41
+ if (!(await pathExists(pathStr, ctx))) {
42
+ return undefined;
43
+ }
44
+ const result = await ctx.client.call<{ path: string }, FsReadOutput>(["fs", "read"], { path: pathStr });
45
+ return result.content;
46
+ }
47
+
48
+ async function writeText(pathStr: string, content: string, ctx: ProcedureContext): Promise<void> {
49
+ await ctx.client.call<{ path: string; content: string }, FsWriteOutput>(["fs", "write"], { path: pathStr, content });
50
+ }
51
+
52
+ /**
53
+ * Convert camelCase to PascalCase
54
+ */
55
+ function toPascalCase(str: string): string {
56
+ return str.charAt(0).toUpperCase() + str.slice(1);
57
+ }
58
+
59
+ /**
60
+ * Convert dot.notation to camelCase
61
+ */
62
+ function toCamelCase(segments: string[]): string {
63
+ return segments
64
+ .map((s, i) => (i === 0 ? s : toPascalCase(s)))
65
+ .join("");
66
+ }
67
+
68
+ /** The names and files of one procedure */
69
+ export interface ProcedureLayout {
70
+ segments: string[];
71
+ namespace: string;
72
+ /** The folders under procedures/<namespace>/ */
73
+ folders: string[];
74
+ /** The file name without ".ts" */
75
+ fileBase: string;
76
+ camelName: string;
77
+ pascalName: string;
78
+ }
79
+
80
+ /**
81
+ * The layout of a procedure: procedures/<namespace>/<middle segments>/<last segment>.ts
82
+ */
83
+ export function procedureLayout(name: string, namespace?: string): ProcedureLayout {
84
+ const segments = name.split(".");
85
+ return {
86
+ segments,
87
+ namespace: namespace ?? segments[0]!,
88
+ folders: segments.length > 2 ? segments.slice(1, -1) : [],
89
+ fileBase: segments[segments.length - 1]!,
90
+ camelName: toCamelCase(segments),
91
+ pascalName: segments.map(toPascalCase).join(""),
92
+ };
93
+ }
94
+
95
+ /**
96
+ * Generate procedure file content
97
+ */
98
+ function generateProcedureFile(layout: ProcedureLayout, description: string): string {
99
+ const { segments, camelName, pascalName, folders } = layout;
100
+ // procedures/<namespace>/<folders...>/<file>.ts -> src/types.ts
101
+ const typesPath = `${"../".repeat(2 + folders.length)}types.js`;
102
+
103
+ return `/**
104
+ * ${segments.join(".")} procedure
105
+ *
106
+ * ${description}
107
+ */
108
+
109
+ import type { ${pascalName}Input, ${pascalName}Output } from "${typesPath}";
110
+
111
+ /**
112
+ * ${description}
113
+ */
114
+ export async function ${camelName}(_input: ${pascalName}Input): Promise<${pascalName}Output> {
115
+ // TODO: Implement ${segments.join(".")} (rename _input to input when the code uses it)
116
+ return {
117
+ success: true,
118
+ message: "Hello from ${segments.join(".")}",
119
+ };
120
+ }
121
+ `;
122
+ }
123
+
124
+ /**
125
+ * Generate type definitions
126
+ */
127
+ function generateTypes(layout: ProcedureLayout, description: string): string {
128
+ const { segments, pascalName } = layout;
129
+ const schemaName = `${pascalName}InputSchema`;
130
+ const outputSchemaName = `${pascalName}OutputSchema`;
131
+
132
+ // The output has a real schema too: the registry validates each result with it. (Before, the
133
+ // output was an interface with a pass-through outputSchema(), so no result was checked.)
134
+ return `
135
+ // =============================================================================
136
+ // ${segments.join(".")} Types - ${description}
137
+ // =============================================================================
138
+
139
+ export const ${schemaName}: z.ZodObject<{
140
+ // TODO: Add input fields
141
+ }> = z.object({
142
+ // TODO: Add input fields
143
+ });
144
+
145
+ export type ${pascalName}Input = z.infer<typeof ${schemaName}>;
146
+
147
+ export const ${outputSchemaName}: z.ZodObject<{
148
+ success: z.ZodBoolean;
149
+ message: z.ZodString;
150
+ }> = z.object({
151
+ /** Whether the operation succeeded */
152
+ success: z.boolean().describe("Whether the operation succeeded"),
153
+ /** Response message */
154
+ message: z.string().describe("Response message"),
155
+ });
156
+
157
+ export type ${pascalName}Output = z.infer<typeof ${outputSchemaName}>;
158
+ `;
159
+ }
160
+
161
+ const TYPES_HEADER = `/**
162
+ * Type definitions of the procedures of this package
163
+ */
164
+
165
+ `;
166
+
167
+ const Z_IMPORT = `import { z } from "zod";\n`;
168
+
169
+ /** True when the source imports z from "zod" */
170
+ function importsZ(source: string): boolean {
171
+ return (
172
+ /import\s*\{[^}]*\bz\b[^}]*\}\s*from\s*["']zod["']/.test(source) ||
173
+ /import\s+\*\s+as\s+z\s+from\s*["']zod["']/.test(source) ||
174
+ /import\s+z\s+from\s*["']zod["']/.test(source)
175
+ );
176
+ }
177
+
178
+ /**
179
+ * Add import lines after the last import of a source (or after its leading comment)
180
+ */
181
+ export function addImports(source: string, lines: string[]): string {
182
+ if (lines.length === 0) {
183
+ return source;
184
+ }
185
+ const block = lines.join("");
186
+ const importPattern = /^import[\s\S]*?from\s*["'][^"']+["'];?[ \t]*\r?\n|^import\s*["'][^"']+["'];?[ \t]*\r?\n/gm;
187
+ let end = -1;
188
+ for (const match of source.matchAll(importPattern)) {
189
+ end = match.index + match[0].length;
190
+ }
191
+ if (end === -1) {
192
+ const comment = /^\s*\/\*[\s\S]*?\*\/[ \t]*\r?\n(\r?\n)?/.exec(source);
193
+ end = comment ? comment[0].length : 0;
194
+ }
195
+ return source.slice(0, end) + block + source.slice(end);
196
+ }
197
+
198
+ /**
199
+ * Add names to the value import from a module, or add the import
200
+ */
201
+ export function ensureNamedImports(source: string, moduleName: string, names: string[]): string {
202
+ const escaped = moduleName.replace(/[/\\^$*+?.()|[\]{}]/g, "\\$&");
203
+ const pattern = new RegExp(`import\\s*\\{([^}]*)\\}\\s*from\\s*["']${escaped}["'];?`);
204
+ const match = pattern.exec(source);
205
+ if (match && !/^import\s+type\b/.test(match[0])) {
206
+ const present = match[1]!
207
+ .split(",")
208
+ .map((part) => part.trim())
209
+ .filter((part) => part.length > 0);
210
+ const presentNames = new Set(present.map((part) => part.replace(/^type\s+/, "").split(/\s+as\s+/)[0]!.trim()));
211
+ const missing = names.filter((name) => !presentNames.has(name));
212
+ if (missing.length === 0) {
213
+ return source;
214
+ }
215
+ const replacement = `import { ${[...present, ...missing].join(", ")} } from "${moduleName}";`;
216
+ return source.slice(0, match.index) + replacement + source.slice(match.index + match[0].length);
217
+ }
218
+ return addImports(source, [`import { ${names.join(", ")} } from "${moduleName}";\n`]);
219
+ }
220
+
221
+ /** The index of the bracket that closes the bracket at `open` (strings are skipped) */
222
+ function closingBracket(source: string, open: number): number {
223
+ let depth = 0;
224
+ let quote: string | undefined;
225
+ for (let i = open; i < source.length; i++) {
226
+ const ch = source[i]!;
227
+ if (quote) {
228
+ if (ch === "\\") i++;
229
+ else if (ch === quote) quote = undefined;
230
+ continue;
231
+ }
232
+ if (ch === '"' || ch === "'" || ch === "`") quote = ch;
233
+ else if (ch === "[") depth++;
234
+ else if (ch === "]" && --depth === 0) return i;
235
+ }
236
+ return -1;
237
+ }
238
+
239
+ /**
240
+ * Add an item to the array of the first registerProcedures([...]) or registerAll([...]) call.
241
+ * Returns undefined when the source has no such call.
242
+ */
243
+ export function addToRegistration(source: string, item: string): string | undefined {
244
+ // The first call in code, not in a comment
245
+ const call = [...source.matchAll(/\b(registerProcedures|registerAll)\s*\(/g)].find((match) => {
246
+ const lineStart = source.lastIndexOf("\n", match.index) + 1;
247
+ const before = source.slice(lineStart, match.index).trim();
248
+ return !before.startsWith("*") && !before.startsWith("/*") && !before.includes("//");
249
+ });
250
+ if (!call) {
251
+ return undefined;
252
+ }
253
+ const open = source.indexOf("[", call.index);
254
+ const close = open === -1 ? -1 : closingBracket(source, open);
255
+ if (close === -1) {
256
+ return undefined;
257
+ }
258
+ const content = source.slice(open + 1, close);
259
+ let updated: string;
260
+ if (content.trim() === "") {
261
+ updated = item;
262
+ } else if (content.includes("\n")) {
263
+ // One item per line: add a line with the indentation of the last item
264
+ const trimmed = content.replace(/\s*$/, "");
265
+ const lastLine = trimmed.slice(trimmed.lastIndexOf("\n") + 1);
266
+ const indent = /^\s*/.exec(lastLine)?.[0] ?? " ";
267
+ const comma = trimmed.endsWith(",") ? "" : ",";
268
+ updated = `${trimmed}${comma}\n${indent}${item},${content.slice(trimmed.length)}`;
269
+ } else {
270
+ updated = `${content.replace(/,?\s*$/, "")}, ${item}`;
271
+ }
272
+ return source.slice(0, open + 1) + updated + source.slice(close);
273
+ }
274
+
275
+ /**
276
+ * The procedure definition that goes into register.ts
277
+ */
278
+ function generateRegistration(layout: ProcedureLayout, description: string): string {
279
+ const { segments, camelName, pascalName } = layout;
280
+ return `const ${camelName}Procedure = createProcedure()
281
+ .path(${JSON.stringify(segments)})
282
+ .input(zodAdapter<${pascalName}Input>(${pascalName}InputSchema))
283
+ .output(zodAdapter<${pascalName}Output>(${pascalName}OutputSchema))
284
+ .meta({
285
+ description: ${JSON.stringify(description)},
286
+ args: [],
287
+ shorts: {},
288
+ output: "text",
289
+ })
290
+ .handler(async (input: ${pascalName}Input): Promise<${pascalName}Output> => ${camelName}(input))
291
+ .build();
292
+
293
+ `;
294
+ }
295
+
296
+ const REGISTER_TEMPLATE = `/**
297
+ * Procedure registration of this package
298
+ *
299
+ * Scaffold a procedure with: mark procedure new <name> --path <this package>
300
+ */
301
+
302
+ import { registerProcedures } from "@procedures/client";
303
+
304
+ export function register(): void {
305
+ registerProcedures([]);
306
+ }
307
+
308
+ // Auto-register
309
+ register();
310
+ `;
311
+
312
+ /**
313
+ * Put the procedure into register.ts: its imports, its definition, and its place in the
314
+ * registration array. Returns undefined when register.ts has no registration call.
315
+ */
316
+ export function registerSource(source: string, layout: ProcedureLayout, description: string, importPath: string): string | undefined {
317
+ const { camelName, pascalName } = layout;
318
+ const withArray = addToRegistration(source, `${camelName}Procedure`);
319
+ if (withArray === undefined) {
320
+ return undefined;
321
+ }
322
+ let updated = ensureNamedImports(withArray, "@procedures/client", ["createProcedure", "zodAdapter"]);
323
+ updated = addImports(updated, [
324
+ `import { ${camelName} } from "${importPath}";\n`,
325
+ `import { ${pascalName}InputSchema, ${pascalName}OutputSchema, type ${pascalName}Input, type ${pascalName}Output } from "./types.js";\n`,
326
+ ]);
327
+ // The definition goes before the code that registers the procedures
328
+ const anchor = /^(export\s+)?(async\s+)?function\s+register\w*\s*\(|^\s*(registerProcedures|PROCEDURE_REGISTRY\.registerAll)\s*\(/m.exec(updated);
329
+ const at = anchor ? anchor.index : updated.length;
330
+ return updated.slice(0, at) + generateRegistration(layout, description) + updated.slice(at);
331
+ }
332
+
333
+ /**
334
+ * Generate index.ts export
335
+ */
336
+ function generateIndexExport(relativeFile: string, functionName: string): string {
337
+ return `export { ${functionName} } from "./${relativeFile}.js";\n`;
338
+ }
339
+
340
+ /**
341
+ * Scaffold a new procedure
342
+ */
343
+ export async function procedureNew(input: ProcedureNewInput, ctx: ProcedureContext): Promise<ProcedureNewOutput> {
344
+ const operations: string[] = [];
345
+ const created: string[] = [];
346
+ const modified: string[] = [];
347
+ const errors: string[] = [];
348
+
349
+ const layout = procedureLayout(input.name, input.namespace);
350
+ const { segments, namespace, folders, fileBase, camelName, pascalName } = layout;
351
+
352
+ // Resolve paths
353
+ const projectPath = input.path ?? process.cwd();
354
+ const srcPath = join(projectPath, "src");
355
+ const namespacePath = join(srcPath, "procedures", namespace);
356
+ const procedureDir = join(namespacePath, ...folders);
357
+ const procedureFile = join(procedureDir, `${fileBase}.ts`);
358
+ const namespaceIndex = join(namespacePath, "index.ts");
359
+ const typesFile = join(srcPath, "types.ts");
360
+ const registerFile = join(srcPath, "register.ts");
361
+ const relativeFile = [...folders, fileBase].join("/");
362
+ const importPath = `./procedures/${namespace}/${relativeFile}.js`;
363
+
364
+ const description = input.description ?? `${segments.join(".")} procedure`;
365
+ const fail = (message: string): ProcedureNewOutput => ({
366
+ success: false,
367
+ procedurePath: segments,
368
+ created,
369
+ modified,
370
+ operations,
371
+ errors: [...errors, message],
372
+ });
373
+
374
+ // Check every collision before anything is written
375
+ if (PROCEDURE_REGISTRY.has(segments)) {
376
+ return fail(`A procedure is registered at ${segments.join(".")} already. Choose another name.`);
377
+ }
378
+ if (await pathExists(procedureFile, ctx)) {
379
+ return fail(`Procedure file already exists: ${procedureFile}`);
380
+ }
381
+ const existingTypes = await readText(typesFile, ctx);
382
+ if (existingTypes !== undefined && new RegExp(`\\b${pascalName}(Input|Output|InputSchema|OutputSchema)\\b`).test(existingTypes)) {
383
+ return fail(
384
+ `types.ts already has types named ${pascalName}: the name ${segments.join(".")} collides with another procedure. Choose another name.`
385
+ );
386
+ }
387
+ const existingRegister = await readText(registerFile, ctx);
388
+ if (existingRegister !== undefined) {
389
+ if (existingRegister.includes(JSON.stringify(segments)) || new RegExp(`\\b${camelName}Procedure\\b`).test(existingRegister)) {
390
+ return fail(`register.ts already registers ${segments.join(".")} (or ${camelName}Procedure). Choose another name.`);
391
+ }
392
+ }
393
+ const registerBase = existingRegister ?? REGISTER_TEMPLATE;
394
+ const newRegister = registerSource(registerBase, layout, description, importPath);
395
+
396
+ if (input.dryRun) {
397
+ const [indexExists] = await Promise.all([pathExists(namespaceIndex, ctx)]);
398
+ return {
399
+ success: true,
400
+ procedurePath: segments,
401
+ created: [
402
+ procedureFile,
403
+ ...(indexExists ? [] : [namespaceIndex]),
404
+ ...(existingTypes === undefined ? [typesFile] : []),
405
+ ...(existingRegister === undefined ? [registerFile] : []),
406
+ ],
407
+ modified: [
408
+ ...(indexExists ? [namespaceIndex] : []),
409
+ ...(existingTypes !== undefined ? [typesFile] : []),
410
+ ...(existingRegister !== undefined && newRegister !== undefined ? [registerFile] : []),
411
+ ],
412
+ operations: [
413
+ `Would create procedure file: ${procedureFile}`,
414
+ `Would create/update index: ${namespaceIndex}`,
415
+ existingTypes === undefined ? `Would create ${typesFile}` : `Would append types to: ${typesFile}`,
416
+ newRegister === undefined
417
+ ? `Would not change ${registerFile}: it has no registerProcedures([...]) call`
418
+ : `Would register the procedure in: ${registerFile}`,
419
+ ],
420
+ errors: [],
421
+ };
422
+ }
423
+
424
+ try {
425
+ // Step 1: Create the folder and the procedure file
426
+ if (!(await pathExists(procedureDir, ctx))) {
427
+ operations.push(`Creating directory: ${procedureDir}`);
428
+ await ctx.client.call<{ path: string; recursive?: boolean }, FsMkdirOutput>(
429
+ ["fs", "mkdir"],
430
+ { path: procedureDir, recursive: true }
431
+ );
432
+ created.push(procedureDir);
433
+ }
434
+ operations.push(`Creating procedure file: ${procedureFile}`);
435
+ await writeText(procedureFile, generateProcedureFile(layout, description), ctx);
436
+ created.push(procedureFile);
437
+
438
+ // Step 2: Create/update namespace index.ts
439
+ const indexExport = generateIndexExport(relativeFile, camelName);
440
+ const existingIndex = await readText(namespaceIndex, ctx);
441
+ if (existingIndex !== undefined) {
442
+ if (!existingIndex.includes(`from "./${relativeFile}.js"`)) {
443
+ operations.push(`Updating index: ${namespaceIndex}`);
444
+ await writeText(namespaceIndex, existingIndex + indexExport, ctx);
445
+ modified.push(namespaceIndex);
446
+ }
447
+ } else {
448
+ operations.push(`Creating index: ${namespaceIndex}`);
449
+ await writeText(namespaceIndex, indexExport, ctx);
450
+ created.push(namespaceIndex);
451
+ }
452
+
453
+ // Step 3: Add the types (create types.ts, or add the z import that the types need)
454
+ const newTypes = generateTypes(layout, description);
455
+ if (existingTypes === undefined) {
456
+ operations.push(`Creating types: ${typesFile}`);
457
+ await writeText(typesFile, TYPES_HEADER + Z_IMPORT + newTypes, ctx);
458
+ created.push(typesFile);
459
+ } else {
460
+ operations.push(`Appending types to: ${typesFile}`);
461
+ const withImport = importsZ(existingTypes) ? existingTypes : addImports(existingTypes, [Z_IMPORT]);
462
+ await writeText(typesFile, withImport + newTypes, ctx);
463
+ modified.push(typesFile);
464
+ }
465
+
466
+ // Step 4: Register the procedure
467
+ if (newRegister === undefined) {
468
+ operations.push(
469
+ `register.ts has no registerProcedures([...]) call: add ${camelName}Procedure to the registration of ${registerFile}`
470
+ );
471
+ } else {
472
+ operations.push(`${existingRegister === undefined ? "Creating" : "Updating"} registration: ${registerFile}`);
473
+ await writeText(registerFile, newRegister, ctx);
474
+ (existingRegister === undefined ? created : modified).push(registerFile);
475
+ if (existingRegister === undefined) {
476
+ operations.push(`Import ./register.js from src/index.ts, so importing the package registers its procedures`);
477
+ }
478
+ }
479
+
480
+ operations.push(`Next: implement ${segments.join(".")} in ${procedureFile}, then run pnpm build`);
481
+
482
+ return {
483
+ success: errors.length === 0,
484
+ procedurePath: segments,
485
+ created,
486
+ modified,
487
+ operations,
488
+ errors,
489
+ };
490
+ } catch (error) {
491
+ return fail(error instanceof Error ? error.message : String(error));
492
+ }
493
+ }