@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.
- package/CHANGELOG.md +30 -0
- package/LICENSE +21 -0
- package/README.md +119 -2
- package/dist/.tsbuildinfo +1 -0
- package/dist/cli-flags.d.ts +27 -0
- package/dist/cli-flags.d.ts.map +1 -0
- package/dist/cli-flags.js +33 -0
- package/dist/cli-flags.js.map +1 -0
- package/dist/dag/builder.d.ts +18 -0
- package/dist/dag/builder.d.ts.map +1 -0
- package/dist/dag/builder.js +35 -0
- package/dist/dag/builder.js.map +1 -0
- package/dist/dag/index.d.ts +12 -0
- package/dist/dag/index.d.ts.map +1 -0
- package/dist/dag/index.js +12 -0
- package/dist/dag/index.js.map +1 -0
- package/dist/git/index.d.ts +5 -0
- package/dist/git/index.d.ts.map +1 -0
- package/dist/git/index.js +5 -0
- package/dist/git/index.js.map +1 -0
- package/dist/git/ref-parser.d.ts +34 -0
- package/dist/git/ref-parser.d.ts.map +1 -0
- package/dist/git/ref-parser.js +70 -0
- package/dist/git/ref-parser.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/module.d.ts +22 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +180 -0
- package/dist/module.js.map +1 -0
- package/dist/procedures/core/catch.d.ts +33 -0
- package/dist/procedures/core/catch.d.ts.map +1 -0
- package/dist/procedures/core/catch.js +118 -0
- package/dist/procedures/core/catch.js.map +1 -0
- package/dist/procedures/core/index.d.ts +9 -0
- package/dist/procedures/core/index.d.ts.map +1 -0
- package/dist/procedures/core/index.js +8 -0
- package/dist/procedures/core/index.js.map +1 -0
- package/dist/procedures/dag/index.d.ts +5 -0
- package/dist/procedures/dag/index.d.ts.map +1 -0
- package/dist/procedures/dag/index.js +5 -0
- package/dist/procedures/dag/index.js.map +1 -0
- package/dist/procedures/dag/traverse.d.ts +54 -0
- package/dist/procedures/dag/traverse.d.ts.map +1 -0
- package/dist/procedures/dag/traverse.js +252 -0
- package/dist/procedures/dag/traverse.js.map +1 -0
- package/dist/procedures/ecosystem/index.d.ts +7 -0
- package/dist/procedures/ecosystem/index.d.ts.map +1 -0
- package/dist/procedures/ecosystem/index.js +6 -0
- package/dist/procedures/ecosystem/index.js.map +1 -0
- package/dist/procedures/ecosystem/procedures.d.ts +63 -0
- package/dist/procedures/ecosystem/procedures.d.ts.map +1 -0
- package/dist/procedures/ecosystem/procedures.js +215 -0
- package/dist/procedures/ecosystem/procedures.js.map +1 -0
- package/dist/procedures/lib/audit.d.ts +29 -0
- package/dist/procedures/lib/audit.d.ts.map +1 -0
- package/dist/procedures/lib/audit.js +236 -0
- package/dist/procedures/lib/audit.js.map +1 -0
- package/dist/procedures/lib/index.d.ts +8 -0
- package/dist/procedures/lib/index.d.ts.map +1 -0
- package/dist/procedures/lib/index.js +8 -0
- package/dist/procedures/lib/index.js.map +1 -0
- package/dist/procedures/lib/new.d.ts +20 -0
- package/dist/procedures/lib/new.d.ts.map +1 -0
- package/dist/procedures/lib/new.js +205 -0
- package/dist/procedures/lib/new.js.map +1 -0
- package/dist/procedures/lib/rename.d.ts +28 -0
- package/dist/procedures/lib/rename.d.ts.map +1 -0
- package/dist/procedures/lib/rename.js +267 -0
- package/dist/procedures/lib/rename.js.map +1 -0
- package/dist/procedures/lib/scan.d.ts +14 -0
- package/dist/procedures/lib/scan.d.ts.map +1 -0
- package/dist/procedures/lib/scan.js +102 -0
- package/dist/procedures/lib/scan.js.map +1 -0
- package/dist/register.d.ts +7 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +9 -0
- package/dist/register.js.map +1 -0
- package/dist/scoped-input.d.ts +17 -0
- package/dist/scoped-input.d.ts.map +1 -0
- package/dist/scoped-input.js +23 -0
- package/dist/scoped-input.js.map +1 -0
- package/dist/types.d.ts +248 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +231 -0
- package/dist/types.js.map +1 -0
- package/dist/workspace.d.ts +46 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +98 -0
- package/dist/workspace.js.map +1 -0
- package/package.json +87 -3
- package/src/cli-flags.ts +49 -0
- package/src/dag/builder.ts +44 -0
- package/src/dag/index.ts +23 -0
- package/src/git/index.ts +11 -0
- package/src/git/ref-parser.ts +83 -0
- package/src/index.ts +56 -0
- package/src/module.ts +232 -0
- package/src/procedures/core/catch.ts +142 -0
- package/src/procedures/core/index.ts +9 -0
- package/src/procedures/dag/index.ts +5 -0
- package/src/procedures/dag/traverse.ts +281 -0
- package/src/procedures/ecosystem/index.ts +12 -0
- package/src/procedures/ecosystem/procedures.ts +317 -0
- package/src/procedures/lib/audit.ts +307 -0
- package/src/procedures/lib/index.ts +8 -0
- package/src/procedures/lib/new.ts +253 -0
- package/src/procedures/lib/rename.ts +355 -0
- package/src/procedures/lib/scan.ts +143 -0
- package/src/register.ts +11 -0
- package/src/types.ts +526 -0
- 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
|
+
}
|