@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,317 @@
1
+ /**
2
+ * ecosystem.procedures
3
+ *
4
+ * Enumerates all procedures across the ecosystem by:
5
+ * 1. Scanning all packages (lib.scan)
6
+ * 2. Loading each package's procedure registry
7
+ * 3. Aggregating using the collector pattern (pure)
8
+ *
9
+ * This is a composition of dag traversal + procedure discovery.
10
+ */
11
+
12
+ import { z } from "zod";
13
+ import type { ProcedureContext } from "@procedures/client";
14
+ import { libScan } from "../lib/scan.js";
15
+ import { join } from "node:path";
16
+ import { pathToFileURL } from "node:url";
17
+ import { readFile } from "node:fs/promises";
18
+
19
+ // =============================================================================
20
+ // Types
21
+ // =============================================================================
22
+
23
+ export const EcosystemProceduresInputSchema: z.ZodObject<{
24
+ rootPath: z.ZodOptional<z.ZodString>;
25
+ namespace: z.ZodOptional<z.ZodString>;
26
+ includeMetadata: z.ZodDefault<z.ZodBoolean>;
27
+ }> = z.object({
28
+ /** Workspace root (defaults to the pnpm workspace that contains client-lib) */
29
+ rootPath: z.string().optional().describe("Workspace root (defaults to the pnpm workspace that contains client-lib)"),
30
+ /** Filter by namespace prefix */
31
+ namespace: z.string().optional().describe("Filter by namespace prefix"),
32
+ /** Include full metadata */
33
+ includeMetadata: z.boolean().default(false).describe("Include full metadata"),
34
+ });
35
+
36
+ export type EcosystemProceduresInput = z.infer<typeof EcosystemProceduresInputSchema>;
37
+
38
+ export interface ProcedureInfo {
39
+ /** Procedure path as array */
40
+ path: string[];
41
+ /** Procedure path as dot-separated key */
42
+ key: string;
43
+ /** Source package */
44
+ package: string;
45
+ /** Package path on disk */
46
+ packagePath: string;
47
+ /** Description from metadata */
48
+ description?: string | undefined;
49
+ /** Tags from metadata */
50
+ tags?: string[] | undefined;
51
+ }
52
+
53
+ export interface PackageProcedures {
54
+ /** Package name */
55
+ name: string;
56
+ /** Package path */
57
+ path: string;
58
+ /** Procedures defined in this package */
59
+ procedures: ProcedureInfo[];
60
+ /** Whether loading succeeded */
61
+ success: boolean;
62
+ /** Error if loading failed */
63
+ error?: string | undefined;
64
+ }
65
+
66
+ export interface EcosystemProceduresOutput {
67
+ /** All procedures aggregated */
68
+ procedures: ProcedureInfo[];
69
+ /** Procedures grouped by package */
70
+ byPackage: PackageProcedures[];
71
+ /** Procedures grouped by namespace */
72
+ byNamespace: Record<string, ProcedureInfo[]>;
73
+ /** Summary counts */
74
+ summary: {
75
+ totalPackages: number;
76
+ packagesWithProcedures: number;
77
+ totalProcedures: number;
78
+ namespaces: string[];
79
+ };
80
+ }
81
+
82
+ const procedureInfoSchema = z.looseObject({
83
+ path: z.array(z.string()).describe("The procedure path"),
84
+ key: z.string().describe("The dotted key of the path"),
85
+ package: z.string().describe("The package that registers the procedure"),
86
+ packagePath: z.string().describe("The folder of the package"),
87
+ description: z.string().optional().describe("The description of the procedure"),
88
+ tags: z.array(z.string()).optional().describe("The tags of the procedure"),
89
+ });
90
+
91
+ /** The output schema of ecosystem.procedures (roadmap 2.1). Loose: other fields stay. */
92
+ export const EcosystemProceduresOutputSchema: z.ZodType<EcosystemProceduresOutput> = z.looseObject({
93
+ procedures: z.array(procedureInfoSchema).describe("Every procedure of the workspace packages"),
94
+ byPackage: z
95
+ .array(
96
+ z.looseObject({
97
+ name: z.string().describe("The package name"),
98
+ path: z.string().describe("The package folder"),
99
+ procedures: z.array(procedureInfoSchema).describe("The procedures of the package"),
100
+ success: z.boolean().describe("False when the package did not load"),
101
+ error: z.string().optional().describe("Why the package did not load"),
102
+ })
103
+ )
104
+ .describe("The procedures of each package"),
105
+ byNamespace: z.record(z.string(), z.array(procedureInfoSchema)).describe("The procedures of each namespace"),
106
+ summary: z
107
+ .looseObject({
108
+ totalPackages: z.number().describe("The number of packages that were examined"),
109
+ packagesWithProcedures: z.number().describe("The number of packages that have procedures"),
110
+ totalProcedures: z.number().describe("The number of procedures"),
111
+ namespaces: z.array(z.string()).describe("The namespaces (first path segments)"),
112
+ })
113
+ .describe("The counts"),
114
+ });
115
+
116
+ // =============================================================================
117
+ // Collector Pattern Implementation
118
+ // =============================================================================
119
+
120
+ interface Collector<T, A, R> {
121
+ supplier: () => A;
122
+ accumulator: (acc: A, element: T) => void;
123
+ finisher: (acc: A) => R;
124
+ }
125
+
126
+ function collect<T, A, R>(items: Iterable<T>, collector: Collector<T, A, R>): R {
127
+ const acc = collector.supplier();
128
+ for (const item of items) {
129
+ collector.accumulator(acc, item);
130
+ }
131
+ return collector.finisher(acc);
132
+ }
133
+
134
+ // Collector that aggregates PackageProcedures into EcosystemProceduresOutput
135
+ function ecosystemProceduresCollector(namespace?: string): Collector<
136
+ PackageProcedures,
137
+ {
138
+ procedures: ProcedureInfo[];
139
+ byPackage: PackageProcedures[];
140
+ byNamespace: Map<string, ProcedureInfo[]>;
141
+ },
142
+ EcosystemProceduresOutput
143
+ > {
144
+ return {
145
+ supplier: () => ({
146
+ procedures: [],
147
+ byPackage: [],
148
+ byNamespace: new Map<string, ProcedureInfo[]>(),
149
+ }),
150
+ accumulator: (acc, pkg) => {
151
+ acc.byPackage.push(pkg);
152
+
153
+ for (const proc of pkg.procedures) {
154
+ // Filter by namespace if specified
155
+ if (namespace && proc.path[0] !== namespace) {
156
+ continue;
157
+ }
158
+
159
+ acc.procedures.push(proc);
160
+
161
+ // Group by namespace
162
+ const ns = proc.path[0] ?? "root";
163
+ if (!acc.byNamespace.has(ns)) {
164
+ acc.byNamespace.set(ns, []);
165
+ }
166
+ acc.byNamespace.get(ns)!.push(proc);
167
+ }
168
+ },
169
+ finisher: (acc) => {
170
+ const namespaces = [...acc.byNamespace.keys()].sort();
171
+ return {
172
+ procedures: acc.procedures,
173
+ byPackage: acc.byPackage,
174
+ byNamespace: Object.fromEntries(acc.byNamespace),
175
+ summary: {
176
+ totalPackages: acc.byPackage.length,
177
+ packagesWithProcedures: acc.byPackage.filter(p => p.procedures.length > 0).length,
178
+ totalProcedures: acc.procedures.length,
179
+ namespaces,
180
+ },
181
+ };
182
+ },
183
+ };
184
+ }
185
+
186
+ // =============================================================================
187
+ // Package Procedure Discovery
188
+ // =============================================================================
189
+
190
+ async function discoverPackageProcedures(
191
+ packageName: string,
192
+ packagePath: string,
193
+ _ctx: ProcedureContext
194
+ ): Promise<PackageProcedures> {
195
+ try {
196
+ // Read package.json to find client.procedures entry
197
+ const pkgJsonPath = join(packagePath, "package.json");
198
+ const pkgJsonContent = await readFile(pkgJsonPath, "utf-8");
199
+ const pkgJson = JSON.parse(pkgJsonContent) as {
200
+ client?: { procedures?: string };
201
+ };
202
+
203
+ if (!pkgJson.client?.procedures) {
204
+ return {
205
+ name: packageName,
206
+ path: packagePath,
207
+ procedures: [],
208
+ success: true,
209
+ };
210
+ }
211
+
212
+ // Dynamically import the procedures register file
213
+ const proceduresPath = join(packagePath, pkgJson.client.procedures);
214
+
215
+ // Import to trigger registration. import() needs a file:// URL: a Windows path such as
216
+ // C:\... is not a valid module specifier (BUGS-2026-07 M33)
217
+ try {
218
+ await import(pathToFileURL(proceduresPath).href);
219
+ } catch (importError) {
220
+ // May already be imported or path issue - continue anyway
221
+ }
222
+
223
+ // Query the global registry for procedures from this package
224
+ // We identify procedures by checking if they're in a namespace that
225
+ // matches the package pattern (e.g., client-fs -> fs.*)
226
+ const { PROCEDURE_REGISTRY } = await import("@procedures/client");
227
+ const allProcs = PROCEDURE_REGISTRY.getAll();
228
+
229
+ // Extract namespace from package name (e.g., @procedures/fs -> fs)
230
+ const match = packageName.match(/^@procedures\/(\w+)/) ?? packageName.match(/@mark1russell7\/client-(\w+)/);
231
+ const expectedNamespace = match ? match[1] : null;
232
+
233
+ const procedures: ProcedureInfo[] = [];
234
+
235
+ for (const proc of allProcs) {
236
+ // For now, include all procedures but mark their source
237
+ // A more sophisticated approach would track which package registered which procedure
238
+ const meta = proc.metadata as Record<string, unknown> | undefined;
239
+ const description = meta?.["description"] as string | undefined;
240
+ const tags = meta?.["tags"] as string[] | undefined;
241
+
242
+ // Heuristic: if first path segment matches expected namespace, it's from this package
243
+ if (expectedNamespace && proc.path[0] === expectedNamespace) {
244
+ procedures.push({
245
+ path: proc.path,
246
+ key: proc.path.join("."),
247
+ package: packageName,
248
+ packagePath,
249
+ description,
250
+ tags,
251
+ });
252
+ } else if (packageName === "@procedures/lib" && proc.path[0] === "lib") {
253
+ procedures.push({
254
+ path: proc.path,
255
+ key: proc.path.join("."),
256
+ package: packageName,
257
+ packagePath,
258
+ description,
259
+ tags,
260
+ });
261
+ } else if (packageName === "@procedures/cli-run" &&
262
+ (proc.path[0] === "procedure" || proc.path[0] === "dag")) {
263
+ procedures.push({
264
+ path: proc.path,
265
+ key: proc.path.join("."),
266
+ package: packageName,
267
+ packagePath,
268
+ description,
269
+ tags,
270
+ });
271
+ }
272
+ }
273
+
274
+ return {
275
+ name: packageName,
276
+ path: packagePath,
277
+ procedures,
278
+ success: true,
279
+ };
280
+ } catch (error) {
281
+ return {
282
+ name: packageName,
283
+ path: packagePath,
284
+ procedures: [],
285
+ success: false,
286
+ error: error instanceof Error ? error.message : String(error),
287
+ };
288
+ }
289
+ }
290
+
291
+ // =============================================================================
292
+ // Main Procedure
293
+ // =============================================================================
294
+
295
+ export async function ecosystemProcedures(
296
+ input: EcosystemProceduresInput,
297
+ ctx: ProcedureContext
298
+ ): Promise<EcosystemProceduresOutput> {
299
+ // Step 1: Scan all packages
300
+ const scanResult = await libScan({ rootPath: input.rootPath }, ctx);
301
+
302
+ // Step 2: Discover procedures in each package
303
+ const packageResults: PackageProcedures[] = [];
304
+
305
+ for (const [name, info] of Object.entries(scanResult.packages)) {
306
+ // Only process client-* packages (they have procedures)
307
+ if (name.includes("/client-") || name === "@procedures/client") {
308
+ const result = await discoverPackageProcedures(name, info.repoPath, ctx);
309
+ packageResults.push(result);
310
+ }
311
+ }
312
+
313
+ // Step 3: Aggregate using collector pattern
314
+ const output = collect(packageResults, ecosystemProceduresCollector(input.namespace));
315
+
316
+ return output;
317
+ }
@@ -0,0 +1,307 @@
1
+ /**
2
+ * lib.audit procedure
3
+ *
4
+ * Validates the workspace packages against the package template, and checks for
5
+ * pnpm settings that do not work in a workspace.
6
+ */
7
+
8
+ import { basename, join } from "node:path";
9
+ import { PROCEDURE_REGISTRY, type AnyProcedure, type ProcedureContext } from "@procedures/client";
10
+ import type {
11
+ FlagNotice,
12
+ LibAuditInput,
13
+ LibAuditOutput,
14
+ PackageAuditResult,
15
+ PnpmIssue,
16
+ } from "../../types.js";
17
+ import { libScan } from "./scan.js";
18
+ import { resolveWorkspaceRoot } from "../../workspace.js";
19
+ import { CLI_GLOBAL_FLAGS } from "../../cli-flags.js";
20
+
21
+ interface FsExistsOutput { exists: boolean; path: string; }
22
+ interface FsReadJsonOutput { path: string; data: unknown; }
23
+ interface FsWriteOutput { path: string; bytesWritten: number; }
24
+ interface FsMkdirOutput { path: string; created: boolean; }
25
+
26
+ /**
27
+ * The files and folders every client package has. dist/ is not in the list:
28
+ * it is build output and does not exist before `pnpm build`.
29
+ */
30
+ export const PACKAGE_TEMPLATE: { files: string[]; dirs: string[] } = {
31
+ files: ["package.json", "tsconfig.json", "dependencies.json", ".gitignore"],
32
+ dirs: ["src"],
33
+ };
34
+
35
+ /**
36
+ * Folders under packages/ that are not client packages.
37
+ * packages/cli holds the repository tool from the template, which does not use cue-config.
38
+ */
39
+ const NOT_CLIENT_PACKAGES = new Set(["cli"]);
40
+
41
+ /**
42
+ * Check if path exists
43
+ */
44
+ async function pathExists(pathStr: string, ctx: ProcedureContext): Promise<boolean> {
45
+ try {
46
+ const result = await ctx.client.call<{ path: string }, FsExistsOutput>(
47
+ ["fs", "exists"],
48
+ { path: pathStr }
49
+ );
50
+ return result.exists;
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Package.json structure for pnpm validation
58
+ */
59
+ interface PackageJson {
60
+ name?: string;
61
+ dependencies?: Record<string, string>;
62
+ devDependencies?: Record<string, string>;
63
+ peerDependencies?: Record<string, string>;
64
+ pnpm?: unknown;
65
+ }
66
+
67
+ /**
68
+ * Check for pnpm configuration that does not work in a workspace
69
+ */
70
+ async function checkPnpmIssues(
71
+ pkgPath: string,
72
+ workspacePackages: Set<string>,
73
+ ctx: ProcedureContext
74
+ ): Promise<PnpmIssue[]> {
75
+ const issues: PnpmIssue[] = [];
76
+
77
+ if (await pathExists(join(pkgPath, "package-lock.json"), ctx)) {
78
+ issues.push({
79
+ type: "npm-lockfile",
80
+ message: "Found package-lock.json - the workspace uses the root pnpm-lock.yaml",
81
+ });
82
+ }
83
+ if (await pathExists(join(pkgPath, "pnpm-lock.yaml"), ctx)) {
84
+ issues.push({
85
+ type: "package-lockfile",
86
+ message: "Found a per-package pnpm-lock.yaml - the workspace uses the root pnpm-lock.yaml",
87
+ });
88
+ }
89
+
90
+ let pkgJson: PackageJson;
91
+ try {
92
+ const result = await ctx.client.call<{ path: string }, FsReadJsonOutput>(
93
+ ["fs", "read.json"],
94
+ { path: join(pkgPath, "package.json") }
95
+ );
96
+ pkgJson = result.data as PackageJson;
97
+ } catch {
98
+ return issues;
99
+ }
100
+
101
+ if (pkgJson.pnpm !== undefined) {
102
+ issues.push({
103
+ type: "ignored-pnpm-field",
104
+ message: 'The "pnpm" field of a workspace package is ignored - use the root pnpm-workspace.yaml',
105
+ });
106
+ }
107
+
108
+ // A dependency on another workspace package must use the workspace link, not a git reference
109
+ for (const deps of [pkgJson.dependencies, pkgJson.devDependencies, pkgJson.peerDependencies]) {
110
+ for (const [name, spec] of Object.entries(deps ?? {})) {
111
+ if (workspacePackages.has(name) && !spec.startsWith("workspace:")) {
112
+ issues.push({
113
+ type: "not-workspace-link",
114
+ message: `"${name}" is a workspace package but the dependency is "${spec}" - use "workspace:*"`,
115
+ package: name,
116
+ });
117
+ }
118
+ }
119
+ }
120
+
121
+ return issues;
122
+ }
123
+
124
+ /**
125
+ * Audit a single package against the template
126
+ */
127
+ async function auditPackage(
128
+ pkgPath: string,
129
+ pkgName: string,
130
+ template: { files: string[]; dirs: string[] },
131
+ workspacePackages: Set<string>,
132
+ fix: boolean,
133
+ ctx: ProcedureContext
134
+ ): Promise<PackageAuditResult> {
135
+ const missingFiles: string[] = [];
136
+ const missingDirs: string[] = [];
137
+ const fixedFiles: string[] = [];
138
+ const fixedDirs: string[] = [];
139
+
140
+ // Check required directories
141
+ for (const dir of template.dirs) {
142
+ const dirPath = join(pkgPath, dir);
143
+ if (!(await pathExists(dirPath, ctx))) {
144
+ missingDirs.push(dir);
145
+ if (fix) {
146
+ try {
147
+ await ctx.client.call<{ path: string; recursive?: boolean }, FsMkdirOutput>(
148
+ ["fs", "mkdir"],
149
+ { path: dirPath, recursive: true }
150
+ );
151
+ fixedDirs.push(dir);
152
+ } catch {
153
+ // Could not fix
154
+ }
155
+ }
156
+ }
157
+ }
158
+
159
+ // Check required files
160
+ for (const file of template.files) {
161
+ const filePath = join(pkgPath, file);
162
+ if (!(await pathExists(filePath, ctx))) {
163
+ missingFiles.push(file);
164
+ if (fix) {
165
+ // Only fix certain files with sensible defaults
166
+ try {
167
+ if (file === "dependencies.json") {
168
+ await ctx.client.call<{ path: string; content: string }, FsWriteOutput>(
169
+ ["fs", "write"],
170
+ {
171
+ path: filePath,
172
+ content: JSON.stringify(
173
+ { $schema: "./node_modules/@mark1russell7/cue/dependencies/schema.json", dependencies: ["ts", "node"] },
174
+ null,
175
+ 2
176
+ ) + "\n"
177
+ }
178
+ );
179
+ fixedFiles.push(file);
180
+ } else if (file === ".gitignore") {
181
+ await ctx.client.call<{ path: string; content: string }, FsWriteOutput>(
182
+ ["fs", "write"],
183
+ { path: filePath, content: "node_modules/\ndist/\n.tsbuildinfo\n" }
184
+ );
185
+ fixedFiles.push(file);
186
+ }
187
+ // Don't auto-create package.json or tsconfig.json - those need cue-config generate
188
+ } catch {
189
+ // Could not fix
190
+ }
191
+ }
192
+ }
193
+ }
194
+
195
+ // Check pnpm configuration
196
+ const pnpmIssues = await checkPnpmIssues(pkgPath, workspacePackages, ctx);
197
+
198
+ // Remove fixed items from missing lists
199
+ const stillMissingFiles = missingFiles.filter((f) => !fixedFiles.includes(f));
200
+ const stillMissingDirs = missingDirs.filter((d) => !fixedDirs.includes(d));
201
+
202
+ // Package is valid only if no missing files/dirs AND no pnpm issues
203
+ const isValid = stillMissingFiles.length === 0 && stillMissingDirs.length === 0 && pnpmIssues.length === 0;
204
+
205
+ return {
206
+ name: pkgName,
207
+ path: pkgPath,
208
+ valid: isValid,
209
+ missingFiles: stillMissingFiles,
210
+ missingDirs: stillMissingDirs,
211
+ pnpmIssues,
212
+ ...(fix && fixedFiles.length > 0 ? { fixedFiles } : {}),
213
+ ...(fix && fixedDirs.length > 0 ? { fixedDirs } : {}),
214
+ };
215
+ }
216
+
217
+ /**
218
+ * The field names of a procedure's input schema (a Zod object, also through zodAdapter)
219
+ */
220
+ function inputFieldNames(schema: unknown): string[] {
221
+ const node = schema as { shape?: unknown; _def?: { shape?: () => Record<string, unknown> } } | undefined;
222
+ if (node?.shape && typeof node.shape === "object") {
223
+ return Object.keys(node.shape);
224
+ }
225
+ if (typeof node?._def?.shape === "function") {
226
+ return Object.keys(node._def.shape());
227
+ }
228
+ return [];
229
+ }
230
+
231
+ /**
232
+ * The procedure flags that hide a global flag of `mark` after the command path. `mark` gives a
233
+ * flag after the path to the procedure when the procedure declares it, so the user must give
234
+ * the global flag before the command. The server options (`--port`, `--host`, `--transport`)
235
+ * are not reported: they are global only with `--server`, which takes no command.
236
+ */
237
+ export function findFlagNotices(procedures: readonly AnyProcedure[]): FlagNotice[] {
238
+ const notices: FlagNotice[] = [];
239
+ const globals = CLI_GLOBAL_FLAGS.filter((flag) => !flag.serverOption);
240
+ for (const procedure of procedures) {
241
+ const name = procedure.path.join(" ");
242
+ const shorts = ((procedure.metadata ?? {}) as { shorts?: Record<string, string> }).shorts ?? {};
243
+ for (const [field, letter] of Object.entries(shorts)) {
244
+ const global = globals.find((flag) => flag.short === letter);
245
+ if (global) {
246
+ notices.push({ procedure: name, flag: `-${letter} (${field})`, global: `-${letter} (--${global.long})` });
247
+ }
248
+ }
249
+ for (const field of inputFieldNames(procedure.input)) {
250
+ const kebab = field.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
251
+ const global = globals.find((flag) => flag.long === field || flag.long === kebab);
252
+ if (global) {
253
+ notices.push({ procedure: name, flag: `--${kebab}`, global: `--${global.long}` });
254
+ }
255
+ }
256
+ }
257
+ return notices;
258
+ }
259
+
260
+ /**
261
+ * Audit all client packages of the workspace against the package template. The result also
262
+ * lists the procedure flags that hide a global flag of `mark` (information only).
263
+ */
264
+ export async function libAudit(input: LibAuditInput, ctx: ProcedureContext): Promise<LibAuditOutput> {
265
+ const template = PACKAGE_TEMPLATE;
266
+ const flagNotices = findFlagNotices(PROCEDURE_REGISTRY.getAll());
267
+ const empty: LibAuditOutput = {
268
+ success: false,
269
+ flagNotices,
270
+ template,
271
+ results: [],
272
+ summary: { total: 0, valid: 0, invalid: 0 },
273
+ };
274
+
275
+ let rootPath: string;
276
+ try {
277
+ rootPath = resolveWorkspaceRoot(input.rootPath);
278
+ } catch {
279
+ return empty;
280
+ }
281
+
282
+ const scan = await libScan({ rootPath }, ctx);
283
+ const workspacePackages = new Set(Object.keys(scan.packages));
284
+ const results: PackageAuditResult[] = [];
285
+
286
+ for (const info of Object.values(scan.packages)) {
287
+ if (NOT_CLIENT_PACKAGES.has(basename(info.repoPath))) {
288
+ continue;
289
+ }
290
+ results.push(await auditPackage(info.repoPath, info.name, template, workspacePackages, input.fix, ctx));
291
+ }
292
+
293
+ const validCount = results.filter((r) => r.valid).length;
294
+ const invalidCount = results.filter((r) => !r.valid).length;
295
+
296
+ return {
297
+ success: invalidCount === 0,
298
+ flagNotices,
299
+ template,
300
+ results,
301
+ summary: {
302
+ total: results.length,
303
+ valid: validCount,
304
+ invalid: invalidCount,
305
+ },
306
+ };
307
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * lib procedures
3
+ */
4
+
5
+ export { libScan } from "./scan.js";
6
+ export { libRename } from "./rename.js";
7
+ export { libNew } from "./new.js";
8
+ export { libAudit } from "./audit.js";