@procedures/lib 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 (114) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/LICENSE +21 -0
  3. package/README.md +119 -2
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/cli-flags.d.ts +27 -0
  6. package/dist/cli-flags.d.ts.map +1 -0
  7. package/dist/cli-flags.js +33 -0
  8. package/dist/cli-flags.js.map +1 -0
  9. package/dist/dag/builder.d.ts +18 -0
  10. package/dist/dag/builder.d.ts.map +1 -0
  11. package/dist/dag/builder.js +35 -0
  12. package/dist/dag/builder.js.map +1 -0
  13. package/dist/dag/index.d.ts +12 -0
  14. package/dist/dag/index.d.ts.map +1 -0
  15. package/dist/dag/index.js +12 -0
  16. package/dist/dag/index.js.map +1 -0
  17. package/dist/git/index.d.ts +5 -0
  18. package/dist/git/index.d.ts.map +1 -0
  19. package/dist/git/index.js +5 -0
  20. package/dist/git/index.js.map +1 -0
  21. package/dist/git/ref-parser.d.ts +34 -0
  22. package/dist/git/ref-parser.d.ts.map +1 -0
  23. package/dist/git/ref-parser.js +70 -0
  24. package/dist/git/ref-parser.js.map +1 -0
  25. package/dist/index.d.ts +14 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +19 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/module.d.ts +22 -0
  30. package/dist/module.d.ts.map +1 -0
  31. package/dist/module.js +180 -0
  32. package/dist/module.js.map +1 -0
  33. package/dist/procedures/core/catch.d.ts +33 -0
  34. package/dist/procedures/core/catch.d.ts.map +1 -0
  35. package/dist/procedures/core/catch.js +118 -0
  36. package/dist/procedures/core/catch.js.map +1 -0
  37. package/dist/procedures/core/index.d.ts +9 -0
  38. package/dist/procedures/core/index.d.ts.map +1 -0
  39. package/dist/procedures/core/index.js +8 -0
  40. package/dist/procedures/core/index.js.map +1 -0
  41. package/dist/procedures/dag/index.d.ts +5 -0
  42. package/dist/procedures/dag/index.d.ts.map +1 -0
  43. package/dist/procedures/dag/index.js +5 -0
  44. package/dist/procedures/dag/index.js.map +1 -0
  45. package/dist/procedures/dag/traverse.d.ts +54 -0
  46. package/dist/procedures/dag/traverse.d.ts.map +1 -0
  47. package/dist/procedures/dag/traverse.js +252 -0
  48. package/dist/procedures/dag/traverse.js.map +1 -0
  49. package/dist/procedures/ecosystem/index.d.ts +7 -0
  50. package/dist/procedures/ecosystem/index.d.ts.map +1 -0
  51. package/dist/procedures/ecosystem/index.js +6 -0
  52. package/dist/procedures/ecosystem/index.js.map +1 -0
  53. package/dist/procedures/ecosystem/procedures.d.ts +63 -0
  54. package/dist/procedures/ecosystem/procedures.d.ts.map +1 -0
  55. package/dist/procedures/ecosystem/procedures.js +215 -0
  56. package/dist/procedures/ecosystem/procedures.js.map +1 -0
  57. package/dist/procedures/lib/audit.d.ts +29 -0
  58. package/dist/procedures/lib/audit.d.ts.map +1 -0
  59. package/dist/procedures/lib/audit.js +236 -0
  60. package/dist/procedures/lib/audit.js.map +1 -0
  61. package/dist/procedures/lib/index.d.ts +8 -0
  62. package/dist/procedures/lib/index.d.ts.map +1 -0
  63. package/dist/procedures/lib/index.js +8 -0
  64. package/dist/procedures/lib/index.js.map +1 -0
  65. package/dist/procedures/lib/new.d.ts +20 -0
  66. package/dist/procedures/lib/new.d.ts.map +1 -0
  67. package/dist/procedures/lib/new.js +205 -0
  68. package/dist/procedures/lib/new.js.map +1 -0
  69. package/dist/procedures/lib/rename.d.ts +28 -0
  70. package/dist/procedures/lib/rename.d.ts.map +1 -0
  71. package/dist/procedures/lib/rename.js +267 -0
  72. package/dist/procedures/lib/rename.js.map +1 -0
  73. package/dist/procedures/lib/scan.d.ts +14 -0
  74. package/dist/procedures/lib/scan.d.ts.map +1 -0
  75. package/dist/procedures/lib/scan.js +102 -0
  76. package/dist/procedures/lib/scan.js.map +1 -0
  77. package/dist/register.d.ts +7 -0
  78. package/dist/register.d.ts.map +1 -0
  79. package/dist/register.js +9 -0
  80. package/dist/register.js.map +1 -0
  81. package/dist/scoped-input.d.ts +17 -0
  82. package/dist/scoped-input.d.ts.map +1 -0
  83. package/dist/scoped-input.js +23 -0
  84. package/dist/scoped-input.js.map +1 -0
  85. package/dist/types.d.ts +248 -0
  86. package/dist/types.d.ts.map +1 -0
  87. package/dist/types.js +231 -0
  88. package/dist/types.js.map +1 -0
  89. package/dist/workspace.d.ts +46 -0
  90. package/dist/workspace.d.ts.map +1 -0
  91. package/dist/workspace.js +98 -0
  92. package/dist/workspace.js.map +1 -0
  93. package/package.json +87 -3
  94. package/src/cli-flags.ts +49 -0
  95. package/src/dag/builder.ts +44 -0
  96. package/src/dag/index.ts +23 -0
  97. package/src/git/index.ts +11 -0
  98. package/src/git/ref-parser.ts +83 -0
  99. package/src/index.ts +56 -0
  100. package/src/module.ts +232 -0
  101. package/src/procedures/core/catch.ts +142 -0
  102. package/src/procedures/core/index.ts +9 -0
  103. package/src/procedures/dag/index.ts +5 -0
  104. package/src/procedures/dag/traverse.ts +281 -0
  105. package/src/procedures/ecosystem/index.ts +12 -0
  106. package/src/procedures/ecosystem/procedures.ts +317 -0
  107. package/src/procedures/lib/audit.ts +307 -0
  108. package/src/procedures/lib/index.ts +8 -0
  109. package/src/procedures/lib/new.ts +253 -0
  110. package/src/procedures/lib/rename.ts +355 -0
  111. package/src/procedures/lib/scan.ts +143 -0
  112. package/src/register.ts +11 -0
  113. package/src/types.ts +526 -0
  114. package/src/workspace.ts +110 -0
@@ -0,0 +1,253 @@
1
+ /**
2
+ * lib.new procedure
3
+ *
4
+ * Creates a new client package in the workspace, at packages/<name>, with the
5
+ * structure the other client packages have: cue-config files, src/module.ts (the procedures as
6
+ * data), src/register.ts (the only file with a side effect), src/index.ts, src/types.ts, the
7
+ * "./module" and "./register" exports and the "client.procedures" field. Importing the package
8
+ * root registers nothing (ARCHITECTURE-PROPOSALS-2026-10, P9). `mark procedure new` then adds
9
+ * procedures to the module, and they compile.
10
+ * Uses fs.* and shell.* procedures via ctx.client.call() for all operations.
11
+ *
12
+ * After it, run `pnpm install` and `pnpm build` in the workspace root.
13
+ */
14
+
15
+ import { join } from "node:path";
16
+ import type { ProcedureContext } from "@procedures/client";
17
+ import type { LibNewInput, LibNewOutput } from "../../types.js";
18
+ import { packagesDir, resolveWorkspaceRoot } from "../../workspace.js";
19
+
20
+ interface ShellRunOutput {
21
+ exitCode: number;
22
+ stdout: string;
23
+ stderr: string;
24
+ success: boolean;
25
+ }
26
+
27
+ // The root registers nothing: `import "<package>/register"` or `createRegistry()` registers the procedures
28
+ const INDEX_TS = `// The root registers nothing: import "<package>/register", or give the module to createRegistry()
29
+ export * from "./module.js";
30
+ export * from "./types.js";
31
+ `;
32
+
33
+ // \`mark procedure new\` appends the types of each procedure, and adds the zod import then.
34
+ // An unused import here would fail noUnusedLocals (deep dive CLI-13).
35
+ const TYPES_TS = `/**
36
+ * Type definitions of the procedures of this package
37
+ *
38
+ * mark procedure new adds the types of each new procedure here.
39
+ */
40
+
41
+ export {};
42
+ `;
43
+
44
+ /** The names of a package folder in code: "my-package" gives "myPackage" and "MyPackage". */
45
+ function codeNames(name: string): { camel: string; pascal: string } {
46
+ const pascal = name
47
+ .split(/[^A-Za-z0-9]+/)
48
+ .filter(Boolean)
49
+ .map((part) => part[0]!.toUpperCase() + part.slice(1))
50
+ .join("");
51
+ return { camel: pascal[0]!.toLowerCase() + pascal.slice(1), pascal };
52
+ }
53
+
54
+ function moduleTs(name: string, packageName: string): string {
55
+ const { camel, pascal } = codeNames(name);
56
+ return `/**
57
+ * The procedures of ${packageName} as data. Importing this file registers nothing.
58
+ *
59
+ * Scaffold a procedure with: mark procedure new <name> --path packages/${name}
60
+ */
61
+
62
+ import { defineModule, registerModules, PROCEDURE_REGISTRY, type ProcedureModule, type ProcedureRegistry } from "@procedures/client";
63
+
64
+ export const ${camel}Module: ProcedureModule = defineModule("${packageName}", []);
65
+
66
+ export default ${camel}Module;
67
+
68
+ /**
69
+ * This function registers the procedures of ${packageName} in a registry. The default is the
70
+ * global registry.
71
+ */
72
+ export function register${pascal}Procedures(registry: ProcedureRegistry = PROCEDURE_REGISTRY): void {
73
+ registerModules(registry, [${camel}Module]);
74
+ }
75
+ `;
76
+ }
77
+
78
+ function registerTs(name: string, packageName: string): string {
79
+ const { pascal } = codeNames(name);
80
+ return `/**
81
+ * Importing this file registers the procedures of ${packageName} in the global registry:
82
+ * \`import "${packageName}/register"\`. The package root registers nothing.
83
+ */
84
+
85
+ import { register${pascal}Procedures } from "./module.js";
86
+
87
+ export * from "./module.js";
88
+
89
+ register${pascal}Procedures();
90
+ `;
91
+ }
92
+
93
+ /**
94
+ * Run the workspace's cue-config CLI (not npx: a new package has no node_modules,
95
+ * and npx would download an unrelated package from the registry).
96
+ */
97
+ async function runCueConfig(
98
+ cueCli: string,
99
+ args: string[],
100
+ cwd: string,
101
+ ctx: ProcedureContext
102
+ ): Promise<void> {
103
+ const result = await ctx.client.call<
104
+ { command: string; args: string[]; cwd: string },
105
+ ShellRunOutput
106
+ >(["shell", "run"], { command: process.execPath, args: [cueCli, ...args], cwd });
107
+ if (!result.success) {
108
+ throw new Error(`cue-config ${args.join(" ")} failed (exit ${result.exitCode}): ${result.stderr || result.stdout}`);
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Create a new client package in the workspace
114
+ */
115
+ export async function libNew(input: LibNewInput, ctx: ProcedureContext): Promise<LibNewOutput> {
116
+ const operations: string[] = [];
117
+ const created: string[] = [];
118
+ const errors: string[] = [];
119
+ const packageName = `@procedures/${input.name}`;
120
+
121
+ let rootPath: string;
122
+ try {
123
+ rootPath = resolveWorkspaceRoot(input.rootPath);
124
+ } catch (error) {
125
+ return {
126
+ success: false,
127
+ packageName,
128
+ packagePath: "",
129
+ created: [],
130
+ operations: [],
131
+ errors: [error instanceof Error ? error.message : String(error)],
132
+ };
133
+ }
134
+
135
+ const packagePath = join(packagesDir(rootPath), input.name);
136
+ const cueCli = join(rootPath, "node_modules", "@mark1russell7", "cue", "dist", "cli.js");
137
+
138
+ // Check if package already exists
139
+ const existsResult = await ctx.client.call<{ path: string }, { exists: boolean; path: string }>(
140
+ ["fs", "exists"],
141
+ { path: packagePath }
142
+ );
143
+ if (existsResult.exists) {
144
+ return {
145
+ success: false,
146
+ packageName,
147
+ packagePath,
148
+ created: [],
149
+ operations: [],
150
+ errors: [`Package directory already exists: ${packagePath}`],
151
+ };
152
+ }
153
+
154
+ const files = [
155
+ join(packagePath, "src", "index.ts"),
156
+ join(packagePath, "src", "module.ts"),
157
+ join(packagePath, "src", "register.ts"),
158
+ join(packagePath, "src", "types.ts"),
159
+ join(packagePath, "dependencies.json"),
160
+ join(packagePath, "package.json"),
161
+ join(packagePath, "tsconfig.json"),
162
+ join(packagePath, ".gitignore"),
163
+ ];
164
+
165
+ if (input.dryRun) {
166
+ return {
167
+ success: true,
168
+ packageName,
169
+ packagePath,
170
+ created: [`${packagePath}/`, `${join(packagePath, "src")}/`, ...files],
171
+ operations: [
172
+ "Would create packages/" + input.name + " with src/index.ts, src/module.ts, src/register.ts and src/types.ts",
173
+ `Would run cue-config init --preset ${input.preset}`,
174
+ "Would run cue-config generate",
175
+ `Would set the package name to ${packageName}, the exports, client.procedures and the client dependency`,
176
+ "Then run pnpm install and pnpm build in the workspace root",
177
+ ],
178
+ errors: [],
179
+ };
180
+ }
181
+
182
+ try {
183
+ // Step 1: Create the folders and the source files
184
+ operations.push("Creating directory structure");
185
+ await ctx.client.call<{ path: string; recursive?: boolean }, { path: string; created: boolean }>(
186
+ ["fs", "mkdir"],
187
+ { path: join(packagePath, "src"), recursive: true }
188
+ );
189
+ created.push(`${packagePath}/`, `${join(packagePath, "src")}/`);
190
+
191
+ for (const [file, content] of [
192
+ [join(packagePath, "src", "index.ts"), INDEX_TS],
193
+ [join(packagePath, "src", "module.ts"), moduleTs(input.name, packageName)],
194
+ [join(packagePath, "src", "register.ts"), registerTs(input.name, packageName)],
195
+ [join(packagePath, "src", "types.ts"), TYPES_TS],
196
+ ] as const) {
197
+ await ctx.client.call<{ path: string; content: string }, { path: string; bytesWritten: number }>(
198
+ ["fs", "write"],
199
+ { path: file, content }
200
+ );
201
+ created.push(file);
202
+ }
203
+
204
+ // Step 2: cue-config init and generate (dependencies.json, package.json, tsconfig.json, .gitignore)
205
+ operations.push(`Running cue-config init --preset ${input.preset}`);
206
+ await runCueConfig(cueCli, ["init", "--preset", input.preset, "--force"], packagePath, ctx);
207
+ created.push(join(packagePath, "dependencies.json"));
208
+
209
+ operations.push("Running cue-config generate");
210
+ await runCueConfig(cueCli, ["generate"], packagePath, ctx);
211
+ created.push(join(packagePath, "package.json"), join(packagePath, "tsconfig.json"), join(packagePath, ".gitignore"));
212
+
213
+ // Step 3: cue-config writes the CUE default name ("unnamed") and no client fields.
214
+ // Set what a client package needs (see documentation/BUGS-2026-07.md, C5).
215
+ operations.push("Setting the package fields");
216
+ const pkgJsonPath = join(packagePath, "package.json");
217
+ const pkgReadResult = await ctx.client.call<{ path: string }, { path: string; data: unknown }>(
218
+ ["fs", "read.json"],
219
+ { path: pkgJsonPath }
220
+ );
221
+ const pkgJson = (pkgReadResult.data ?? {}) as Record<string, unknown>;
222
+ pkgJson["name"] = packageName;
223
+ pkgJson["exports"] = {
224
+ ".": { types: "./dist/index.d.ts", import: "./dist/index.js" },
225
+ "./module": { types: "./dist/module.d.ts", import: "./dist/module.js" },
226
+ "./register": { types: "./dist/register.d.ts", import: "./dist/register.js" },
227
+ };
228
+ // Only the register entry has a side effect, so a bundler can drop the rest
229
+ pkgJson["sideEffects"] = ["./dist/register.js"];
230
+ pkgJson["client"] = { procedures: "./dist/register.js" };
231
+ pkgJson["dependencies"] = {
232
+ ...((pkgJson["dependencies"] as Record<string, string> | undefined) ?? {}),
233
+ "@procedures/client": "workspace:*",
234
+ zod: "^4.6.5",
235
+ };
236
+ await ctx.client.call<{ path: string; content: string }, { path: string; bytesWritten: number }>(
237
+ ["fs", "write"],
238
+ { path: pkgJsonPath, content: JSON.stringify(pkgJson, null, 2) + "\n" }
239
+ );
240
+ operations.push(`Created ${packageName}. Next: run pnpm install and pnpm build in ${rootPath}`);
241
+ } catch (error) {
242
+ errors.push(error instanceof Error ? error.message : String(error));
243
+ }
244
+
245
+ return {
246
+ success: errors.length === 0,
247
+ packageName,
248
+ packagePath,
249
+ created,
250
+ operations,
251
+ errors,
252
+ };
253
+ }
@@ -0,0 +1,355 @@
1
+ /**
2
+ * lib.rename Procedure
3
+ *
4
+ * Renames a package across the entire codebase using ts-morph for AST-based
5
+ * import updates and direct file manipulation for package.json updates.
6
+ *
7
+ * Uses:
8
+ * - ts-morph: AST manipulation for TypeScript imports
9
+ * - File system operations for package.json updates
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * await client.call(["lib", "rename"], {
14
+ * oldName: "client",
15
+ * newName: "@procedures/client",
16
+ * dryRun: true, // Preview changes without applying
17
+ * });
18
+ * ```
19
+ *
20
+ * The search root is `rootPath`, or else the workspace that contains client-lib.
21
+ */
22
+
23
+ import { Project, SyntaxKind } from "ts-morph";
24
+ import type { ProcedureContext } from "@procedures/client";
25
+ import type { LibRenameInput, LibRenameOutput, RenameChange } from "../../types.js";
26
+ import { resolveWorkspaceRoot } from "../../workspace.js";
27
+
28
+ interface FsGlobOutput { pattern: string; matches: string[]; }
29
+ interface FsGlobInput { pattern: string; cwd: string; absolute: boolean; ignore: string[]; }
30
+ interface FsExistsOutput { exists: boolean; path: string; }
31
+ interface FsReadJsonOutput { path: string; data: unknown; }
32
+ interface FsWriteOutput { path: string; bytesWritten: number; }
33
+
34
+ // The folders that hold no source of the workspace
35
+ const IGNORE = ["**/node_modules/**", "**/dist/**", "**/.git/**"];
36
+
37
+ /**
38
+ * Find all package.json files under a root path (excluding node_modules)
39
+ */
40
+ async function findPackageJsonFiles(rootPath: string, ctx: ProcedureContext): Promise<string[]> {
41
+ const result = await ctx.client.call<FsGlobInput, FsGlobOutput>(
42
+ ["fs", "glob"],
43
+ { pattern: "**/package.json", cwd: rootPath, absolute: true, ignore: IGNORE }
44
+ );
45
+ // fs.glob returns `matches`. (Before, this read `files`, which does not exist: deep dive DATA-7.)
46
+ return result.matches;
47
+ }
48
+
49
+ /**
50
+ * Find all TypeScript files under a root path (excluding node_modules, dist)
51
+ */
52
+ async function findTypeScriptFiles(rootPath: string, ctx: ProcedureContext): Promise<string[]> {
53
+ const result = await ctx.client.call<FsGlobInput, FsGlobOutput>(
54
+ ["fs", "glob"],
55
+ { pattern: "**/*.{ts,tsx}", cwd: rootPath, absolute: true, ignore: IGNORE }
56
+ );
57
+ return result.matches;
58
+ }
59
+
60
+ /**
61
+ * Update package.json name field
62
+ */
63
+ async function updatePackageJsonName(
64
+ pkgPath: string,
65
+ oldName: string,
66
+ newName: string,
67
+ dryRun: boolean,
68
+ ctx: ProcedureContext
69
+ ): Promise<RenameChange | null> {
70
+ try {
71
+ const result = await ctx.client.call<{ path: string }, FsReadJsonOutput>(
72
+ ["fs", "read.json"],
73
+ { path: pkgPath }
74
+ );
75
+ const pkg = result.data as { name?: string };
76
+
77
+ if (pkg.name === oldName) {
78
+ if (!dryRun) {
79
+ pkg.name = newName;
80
+ await ctx.client.call<{ path: string; content: string }, FsWriteOutput>(
81
+ ["fs", "write"],
82
+ { path: pkgPath, content: JSON.stringify(pkg, null, 2) + "\n" }
83
+ );
84
+ }
85
+
86
+ return {
87
+ type: "package-name",
88
+ file: pkgPath,
89
+ oldValue: oldName,
90
+ newValue: newName,
91
+ };
92
+ }
93
+ } catch {
94
+ // Skip files we can't parse
95
+ }
96
+
97
+ return null;
98
+ }
99
+
100
+ /**
101
+ * Update package.json dependencies
102
+ */
103
+ async function updatePackageJsonDependencies(
104
+ pkgPath: string,
105
+ oldName: string,
106
+ newName: string,
107
+ dryRun: boolean,
108
+ ctx: ProcedureContext
109
+ ): Promise<RenameChange[]> {
110
+ const changes: RenameChange[] = [];
111
+
112
+ try {
113
+ const result = await ctx.client.call<{ path: string }, FsReadJsonOutput>(
114
+ ["fs", "read.json"],
115
+ { path: pkgPath }
116
+ );
117
+ const pkg = result.data as Record<string, unknown>;
118
+ let modified = false;
119
+
120
+ const depFields = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
121
+
122
+ for (const field of depFields) {
123
+ const deps = pkg[field] as Record<string, string> | undefined;
124
+ if (deps && oldName in deps) {
125
+ const oldValue = deps[oldName]!;
126
+ let newValue = oldValue;
127
+ // The repository of a git dependency has the name of the package without its scope
128
+ const bare = (name: string): string => name.replace(/^@[^/]+\//, "");
129
+ if (oldValue.includes(`github:mark1russell7/${bare(oldName)}`)) {
130
+ newValue = oldValue.replace(`github:mark1russell7/${bare(oldName)}`, `github:mark1russell7/${bare(newName)}`);
131
+ }
132
+
133
+ if (!dryRun) {
134
+ delete deps[oldName];
135
+ deps[newName] = newValue;
136
+ }
137
+
138
+ changes.push({
139
+ type: "dependency",
140
+ file: pkgPath,
141
+ field,
142
+ oldValue: `"${oldName}": "${oldValue}"`,
143
+ newValue: `"${newName}": "${newValue}"`,
144
+ });
145
+ modified = true;
146
+ }
147
+ }
148
+
149
+ if (modified && !dryRun) {
150
+ await ctx.client.call<{ path: string; content: string }, FsWriteOutput>(
151
+ ["fs", "write"],
152
+ { path: pkgPath, content: JSON.stringify(pkg, null, 2) + "\n" }
153
+ );
154
+ }
155
+ } catch {
156
+ // Skip files we can't parse
157
+ }
158
+
159
+ return changes;
160
+ }
161
+
162
+ /**
163
+ * Update TypeScript imports using ts-morph
164
+ */
165
+ function updateTypeScriptImports(
166
+ tsFiles: string[],
167
+ oldName: string,
168
+ newName: string,
169
+ dryRun: boolean
170
+ ): RenameChange[] {
171
+ const changes: RenameChange[] = [];
172
+
173
+ // Create ts-morph project
174
+ const project = new Project({
175
+ skipAddingFilesFromTsConfig: true,
176
+ skipFileDependencyResolution: true,
177
+ });
178
+
179
+ // Add files to project
180
+ for (const filePath of tsFiles) {
181
+ try {
182
+ project.addSourceFileAtPath(filePath);
183
+ } catch {
184
+ // Skip files we can't add
185
+ }
186
+ }
187
+
188
+ // Process each source file
189
+ for (const sourceFile of project.getSourceFiles()) {
190
+ let fileModified = false;
191
+
192
+ // Find all import declarations
193
+ const importDecls = sourceFile.getImportDeclarations();
194
+
195
+ for (const importDecl of importDecls) {
196
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
197
+
198
+ // Check if this import matches the old package name
199
+ // Match exact name or subpath imports (e.g., "client" or "client/something")
200
+ if (moduleSpecifier === oldName || moduleSpecifier.startsWith(`${oldName}/`)) {
201
+ const newSpecifier = moduleSpecifier.replace(oldName, newName);
202
+
203
+ changes.push({
204
+ type: "import",
205
+ file: sourceFile.getFilePath(),
206
+ line: importDecl.getStartLineNumber(),
207
+ oldValue: `from "${moduleSpecifier}"`,
208
+ newValue: `from "${newSpecifier}"`,
209
+ });
210
+
211
+ if (!dryRun) {
212
+ importDecl.setModuleSpecifier(newSpecifier);
213
+ fileModified = true;
214
+ }
215
+ }
216
+ }
217
+
218
+ // Also check dynamic imports (import())
219
+ const callExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
220
+
221
+ for (const call of callExpressions) {
222
+ const expression = call.getExpression();
223
+ if (expression.getKind() === SyntaxKind.ImportKeyword) {
224
+ const args = call.getArguments();
225
+ if (args.length > 0) {
226
+ const arg = args[0]!;
227
+ if (arg.getKind() === SyntaxKind.StringLiteral) {
228
+ const value = arg.getText().slice(1, -1); // Remove quotes
229
+ if (value === oldName || value.startsWith(`${oldName}/`)) {
230
+ const newValue = value.replace(oldName, newName);
231
+
232
+ changes.push({
233
+ type: "dynamic-import",
234
+ file: sourceFile.getFilePath(),
235
+ line: call.getStartLineNumber(),
236
+ oldValue: `import("${value}")`,
237
+ newValue: `import("${newValue}")`,
238
+ });
239
+
240
+ if (!dryRun) {
241
+ arg.replaceWithText(`"${newValue}"`);
242
+ fileModified = true;
243
+ }
244
+ }
245
+ }
246
+ }
247
+ }
248
+ }
249
+
250
+ // Save if modified
251
+ if (fileModified && !dryRun) {
252
+ sourceFile.saveSync();
253
+ }
254
+ }
255
+
256
+ return changes;
257
+ }
258
+
259
+ /**
260
+ * Execute the lib.rename procedure
261
+ */
262
+ export async function libRename(input: LibRenameInput, ctx: ProcedureContext): Promise<LibRenameOutput> {
263
+ const { oldName, newName, dryRun = false } = input;
264
+
265
+ const changes: RenameChange[] = [];
266
+ const errors: string[] = [];
267
+
268
+ // Resolve path: the given root, else the workspace that contains client-lib
269
+ let resolvedRoot: string;
270
+ try {
271
+ resolvedRoot = resolveWorkspaceRoot(input.rootPath);
272
+ } catch (error) {
273
+ return {
274
+ success: false,
275
+ changes: [],
276
+ errors: [error instanceof Error ? error.message : String(error)],
277
+ summary: { packageNames: 0, dependencies: 0, imports: 0, total: 0 },
278
+ };
279
+ }
280
+
281
+ const existsResult = await ctx.client.call<{ path: string }, FsExistsOutput>(
282
+ ["fs", "exists"],
283
+ { path: resolvedRoot }
284
+ );
285
+ if (!existsResult.exists) {
286
+ return {
287
+ success: false,
288
+ changes: [],
289
+ errors: [`Root path does not exist: ${resolvedRoot}`],
290
+ summary: { packageNames: 0, dependencies: 0, imports: 0, total: 0 },
291
+ };
292
+ }
293
+
294
+ console.error(`[lib.rename] ${dryRun ? "[DRY RUN] " : ""}Renaming "${oldName}" → "${newName}"`);
295
+ console.error(`[lib.rename] Scanning ${resolvedRoot}...`);
296
+
297
+ // Find all package.json files
298
+ const packageJsonFiles = await findPackageJsonFiles(resolvedRoot, ctx);
299
+ console.error(`[lib.rename] Found ${packageJsonFiles.length} package.json files`);
300
+
301
+ // Find all TypeScript files
302
+ const tsFiles = await findTypeScriptFiles(resolvedRoot, ctx);
303
+ console.error(`[lib.rename] Found ${tsFiles.length} TypeScript files`);
304
+
305
+ // 1. Update package.json name field (find the package being renamed)
306
+ for (const pkgPath of packageJsonFiles) {
307
+ const change = await updatePackageJsonName(pkgPath, oldName, newName, dryRun, ctx);
308
+ if (change) {
309
+ changes.push(change);
310
+ console.error(`[lib.rename] ${dryRun ? "Would update" : "Updated"} package name: ${pkgPath}`);
311
+ }
312
+ }
313
+
314
+ // 2. Update package.json dependencies
315
+ for (const pkgPath of packageJsonFiles) {
316
+ const depChanges = await updatePackageJsonDependencies(pkgPath, oldName, newName, dryRun, ctx);
317
+ changes.push(...depChanges);
318
+ if (depChanges.length > 0) {
319
+ console.error(
320
+ `[lib.rename] ${dryRun ? "Would update" : "Updated"} ${depChanges.length} dependencies in: ${pkgPath}`
321
+ );
322
+ }
323
+ }
324
+
325
+ // 3. Update TypeScript imports using ts-morph
326
+ try {
327
+ const importChanges = updateTypeScriptImports(tsFiles, oldName, newName, dryRun);
328
+ changes.push(...importChanges);
329
+ if (importChanges.length > 0) {
330
+ console.error(
331
+ `[lib.rename] ${dryRun ? "Would update" : "Updated"} ${importChanges.length} TypeScript imports`
332
+ );
333
+ }
334
+ } catch (error) {
335
+ const errorMessage = error instanceof Error ? error.message : String(error);
336
+ errors.push(`Error updating TypeScript imports: ${errorMessage}`);
337
+ }
338
+
339
+ // Build summary
340
+ const summary = {
341
+ packageNames: changes.filter((c) => c.type === "package-name").length,
342
+ dependencies: changes.filter((c) => c.type === "dependency").length,
343
+ imports: changes.filter((c) => c.type === "import" || c.type === "dynamic-import").length,
344
+ total: changes.length,
345
+ };
346
+
347
+ console.error(`[lib.rename] Summary: ${summary.packageNames} package names, ${summary.dependencies} dependencies, ${summary.imports} imports`);
348
+
349
+ return {
350
+ success: errors.length === 0,
351
+ changes,
352
+ errors,
353
+ summary,
354
+ };
355
+ }