@zucker-framework/cli 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,516 @@
1
+ import {
2
+ __name
3
+ } from "./chunk-O6YSETKJ.mjs";
4
+
5
+ // src/tooling/module-wiring.ts
6
+ import { readdirSync, readFileSync, statSync } from "fs";
7
+ import { relative, resolve } from "path";
8
+ import ts from "typescript";
9
+ var LIFECYCLE_INTERFACES = /* @__PURE__ */ new Set([
10
+ "BeforeApplicationShutdown",
11
+ "OnApplicationBootstrap",
12
+ "OnApplicationShutdown",
13
+ "OnModuleDestroy",
14
+ "OnModuleInit"
15
+ ]);
16
+ var LIFECYCLE_METHODS = /* @__PURE__ */ new Set([
17
+ "beforeApplicationShutdown",
18
+ "onApplicationBootstrap",
19
+ "onApplicationShutdown",
20
+ "onModuleDestroy",
21
+ "onModuleInit"
22
+ ]);
23
+ function listTypeScriptFiles(directory) {
24
+ const files = [];
25
+ for (const entry of readdirSync(directory, {
26
+ withFileTypes: true
27
+ })) {
28
+ const path = resolve(directory, entry.name);
29
+ if (entry.isDirectory()) {
30
+ files.push(...listTypeScriptFiles(path));
31
+ } else if (entry.isFile() && entry.name.endsWith(".ts") && !entry.name.endsWith(".spec.ts") && !entry.name.endsWith(".test.ts")) {
32
+ files.push(path);
33
+ }
34
+ }
35
+ return files;
36
+ }
37
+ __name(listTypeScriptFiles, "listTypeScriptFiles");
38
+ function getImportAliases(sourceFile) {
39
+ const aliases = /* @__PURE__ */ new Map();
40
+ for (const statement of sourceFile.statements) {
41
+ if (!ts.isImportDeclaration(statement)) continue;
42
+ const bindings = statement.importClause?.namedBindings;
43
+ if (!bindings || !ts.isNamedImports(bindings)) continue;
44
+ for (const element of bindings.elements) {
45
+ aliases.set(element.name.text, element.propertyName?.text ?? element.name.text);
46
+ }
47
+ }
48
+ return aliases;
49
+ }
50
+ __name(getImportAliases, "getImportAliases");
51
+ function getScheduleModuleImports(sourceFile) {
52
+ const cron = /* @__PURE__ */ new Set();
53
+ const modules = /* @__PURE__ */ new Set();
54
+ const namespaces = /* @__PURE__ */ new Set();
55
+ for (const statement of sourceFile.statements) {
56
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== "@nestjs/schedule") {
57
+ continue;
58
+ }
59
+ const bindings = statement.importClause?.namedBindings;
60
+ if (!bindings) continue;
61
+ if (ts.isNamespaceImport(bindings)) {
62
+ namespaces.add(bindings.name.text);
63
+ continue;
64
+ }
65
+ for (const element of bindings.elements) {
66
+ const importedName = element.propertyName?.text ?? element.name.text;
67
+ if (importedName === "ScheduleModule") {
68
+ modules.add(element.name.text);
69
+ } else if (importedName === "Cron") {
70
+ cron.add(element.name.text);
71
+ }
72
+ }
73
+ }
74
+ return {
75
+ cron,
76
+ modules,
77
+ namespaces
78
+ };
79
+ }
80
+ __name(getScheduleModuleImports, "getScheduleModuleImports");
81
+ function isImportedScheduleModule(expression, imports) {
82
+ if (ts.isIdentifier(expression)) {
83
+ return imports.modules.has(expression.text);
84
+ }
85
+ return ts.isPropertyAccessExpression(expression) && ts.isIdentifier(expression.expression) && imports.namespaces.has(expression.expression.text) && expression.name.text === "ScheduleModule";
86
+ }
87
+ __name(isImportedScheduleModule, "isImportedScheduleModule");
88
+ function isImportedCron(expression, imports) {
89
+ if (ts.isIdentifier(expression)) {
90
+ return imports.cron.has(expression.text);
91
+ }
92
+ return ts.isPropertyAccessExpression(expression) && ts.isIdentifier(expression.expression) && imports.namespaces.has(expression.expression.text) && expression.name.text === "Cron";
93
+ }
94
+ __name(isImportedCron, "isImportedCron");
95
+ function waitsForCompletion(call) {
96
+ const options = call.arguments[1];
97
+ if (!options || !ts.isObjectLiteralExpression(options)) return false;
98
+ const property = findProperty(options, "waitForCompletion");
99
+ return property?.initializer.kind === ts.SyntaxKind.TrueKeyword;
100
+ }
101
+ __name(waitsForCompletion, "waitsForCompletion");
102
+ function enclosingClassName(node) {
103
+ let current = node.parent;
104
+ while (current) {
105
+ if (ts.isClassDeclaration(current)) return current.name?.text;
106
+ current = current.parent;
107
+ }
108
+ return void 0;
109
+ }
110
+ __name(enclosingClassName, "enclosingClassName");
111
+ function getCronClassNames(sourceFile, scheduleImports) {
112
+ const names = [];
113
+ const visit = /* @__PURE__ */ __name((node) => {
114
+ if (ts.isCallExpression(node) && ts.isDecorator(node.parent) && isImportedCron(node.expression, scheduleImports)) {
115
+ const className = enclosingClassName(node);
116
+ if (className) names.push(className);
117
+ }
118
+ ts.forEachChild(node, visit);
119
+ }, "visit");
120
+ visit(sourceFile);
121
+ return names;
122
+ }
123
+ __name(getCronClassNames, "getCronClassNames");
124
+ function expressionName(expression, aliases) {
125
+ if (!expression) return void 0;
126
+ if (ts.isIdentifier(expression)) {
127
+ return aliases.get(expression.text) ?? expression.text;
128
+ }
129
+ if (ts.isParenthesizedExpression(expression)) {
130
+ return expressionName(expression.expression, aliases);
131
+ }
132
+ if (ts.isAsExpression(expression) || ts.isTypeAssertionExpression(expression)) {
133
+ return expressionName(expression.expression, aliases);
134
+ }
135
+ return void 0;
136
+ }
137
+ __name(expressionName, "expressionName");
138
+ function propertyName(property) {
139
+ const name = property.name;
140
+ if (!name) return void 0;
141
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text;
142
+ return void 0;
143
+ }
144
+ __name(propertyName, "propertyName");
145
+ function findProperty(object, name) {
146
+ return object.properties.find((property) => ts.isPropertyAssignment(property) && propertyName(property) === name);
147
+ }
148
+ __name(findProperty, "findProperty");
149
+ function getModuleMetadata(sourceFile, aliases) {
150
+ const metadata = [];
151
+ const visit = /* @__PURE__ */ __name((node) => {
152
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && expressionName(node.expression, aliases) === "Module" && node.arguments.length === 1 && ts.isObjectLiteralExpression(node.arguments[0])) {
153
+ metadata.push(node.arguments[0]);
154
+ }
155
+ ts.forEachChild(node, visit);
156
+ }, "visit");
157
+ visit(sourceFile);
158
+ return metadata;
159
+ }
160
+ __name(getModuleMetadata, "getModuleMetadata");
161
+ function getProviders(metadata) {
162
+ const providers = findProperty(metadata, "providers");
163
+ if (!providers || !ts.isArrayLiteralExpression(providers.initializer)) return [];
164
+ return providers.initializer.elements.filter((element) => !ts.isSpreadElement(element));
165
+ }
166
+ __name(getProviders, "getProviders");
167
+ function getImplementedLifecycleNames(sourceFile, aliases) {
168
+ const names = [];
169
+ const visit = /* @__PURE__ */ __name((node) => {
170
+ if (ts.isClassDeclaration(node) && node.name) {
171
+ const implementsLifecycle = (node.heritageClauses ?? []).some((clause) => clause.token === ts.SyntaxKind.ImplementsKeyword && clause.types.some((type) => {
172
+ const localName = type.expression.getText(sourceFile).split(".").at(-1);
173
+ const name = localName ? aliases.get(localName) ?? localName : void 0;
174
+ return name && LIFECYCLE_INTERFACES.has(name);
175
+ }));
176
+ const declaresLifecycleHook = node.members.some((member) => {
177
+ const name = propertyName(member);
178
+ return name ? LIFECYCLE_METHODS.has(name) : false;
179
+ });
180
+ if (implementsLifecycle || declaresLifecycleHook) names.push(node.name.text);
181
+ }
182
+ ts.forEachChild(node, visit);
183
+ }, "visit");
184
+ visit(sourceFile);
185
+ return names;
186
+ }
187
+ __name(getImplementedLifecycleNames, "getImplementedLifecycleNames");
188
+ function factoryReturnsInjectedInstance(factory, injectedParameterNames) {
189
+ const returnsInjected = /* @__PURE__ */ __name((expression) => {
190
+ if (ts.isIdentifier(expression)) {
191
+ return injectedParameterNames.has(expression.text);
192
+ }
193
+ if (ts.isParenthesizedExpression(expression)) {
194
+ return returnsInjected(expression.expression);
195
+ }
196
+ if (ts.isAsExpression(expression) || ts.isTypeAssertionExpression(expression)) {
197
+ return returnsInjected(expression.expression);
198
+ }
199
+ if (ts.isConditionalExpression(expression)) {
200
+ return returnsInjected(expression.whenTrue) || returnsInjected(expression.whenFalse);
201
+ }
202
+ return false;
203
+ }, "returnsInjected");
204
+ if (!ts.isArrowFunction(factory) && !ts.isFunctionExpression(factory)) {
205
+ return false;
206
+ }
207
+ if (!ts.isBlock(factory.body)) return returnsInjected(factory.body);
208
+ let unsafe = false;
209
+ const visit = /* @__PURE__ */ __name((node) => {
210
+ if (ts.isReturnStatement(node) && node.expression) {
211
+ unsafe ||= returnsInjected(node.expression);
212
+ }
213
+ ts.forEachChild(node, visit);
214
+ }, "visit");
215
+ visit(factory.body);
216
+ return unsafe;
217
+ }
218
+ __name(factoryReturnsInjectedInstance, "factoryReturnsInjectedInstance");
219
+ function location(sourceRoot, file, node, sourceFile) {
220
+ const start = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
221
+ return `${relative(sourceRoot, file)}:${start.line + 1}`;
222
+ }
223
+ __name(location, "location");
224
+ function finding(code, message, at) {
225
+ return {
226
+ code,
227
+ message,
228
+ location: at
229
+ };
230
+ }
231
+ __name(finding, "finding");
232
+ function auditModuleWiring({ sourceRoot, schedulerRoot, globalGuards, infrastructureProviders = {} }) {
233
+ const expectedGlobalGuards = /* @__PURE__ */ new Set([
234
+ ...(globalGuards?.classes ?? []).map((name) => `useClass:${name}`),
235
+ ...(globalGuards?.factories ?? []).map((name) => `useFactory:${name}`)
236
+ ]);
237
+ if (!statSync(sourceRoot).isDirectory()) {
238
+ throw new Error(`Module source root is not a directory: ${sourceRoot}`);
239
+ }
240
+ const files = listTypeScriptFiles(sourceRoot);
241
+ const parsed = files.map((file) => {
242
+ const source = readFileSync(file, "utf8");
243
+ const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
244
+ return {
245
+ aliases: getImportAliases(sourceFile),
246
+ file,
247
+ scheduleImports: getScheduleModuleImports(sourceFile),
248
+ sourceFile
249
+ };
250
+ });
251
+ const lifecycleClasses = new Set(parsed.flatMap(({ aliases, sourceFile }) => getImplementedLifecycleNames(sourceFile, aliases)));
252
+ const cronClasses = new Set(parsed.flatMap(({ scheduleImports, sourceFile }) => getCronClassNames(sourceFile, scheduleImports)));
253
+ const findings = [];
254
+ const schedules = [];
255
+ const cronJobs = [];
256
+ const guards = [];
257
+ const coreProviders = Object.fromEntries(Object.keys(infrastructureProviders).map((name) => [
258
+ name,
259
+ []
260
+ ]));
261
+ const lifecycleProviders = /* @__PURE__ */ new Map();
262
+ const cronProviders = /* @__PURE__ */ new Map();
263
+ for (const { aliases, file, scheduleImports, sourceFile } of parsed) {
264
+ const relativeFile = relative(sourceRoot, file);
265
+ const visitSchedule = /* @__PURE__ */ __name((node) => {
266
+ if (ts.isCallExpression(node) && ts.isDecorator(node.parent) && isImportedCron(node.expression, scheduleImports)) {
267
+ const at = location(sourceRoot, file, node, sourceFile);
268
+ cronJobs.push(at);
269
+ if (!waitsForCompletion(node)) {
270
+ findings.push(finding("cron-overlap-guard", "@Cron jobs must set waitForCompletion: true to prevent overlapping runs in one process", at));
271
+ }
272
+ }
273
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "forRoot" && isImportedScheduleModule(node.expression.expression, scheduleImports)) {
274
+ schedules.push(location(sourceRoot, file, node, sourceFile));
275
+ }
276
+ ts.forEachChild(node, visitSchedule);
277
+ }, "visitSchedule");
278
+ visitSchedule(sourceFile);
279
+ for (const metadata of getModuleMetadata(sourceFile, aliases)) {
280
+ for (const provider of getProviders(metadata)) {
281
+ const directName = expressionName(provider, aliases);
282
+ if (directName && Object.hasOwn(coreProviders, directName)) {
283
+ coreProviders[directName].push(location(sourceRoot, file, provider, sourceFile));
284
+ }
285
+ if (directName && lifecycleClasses.has(directName)) {
286
+ const locations = lifecycleProviders.get(directName) ?? [];
287
+ locations.push(location(sourceRoot, file, provider, sourceFile));
288
+ lifecycleProviders.set(directName, locations);
289
+ }
290
+ if (directName && cronClasses.has(directName)) {
291
+ const locations = cronProviders.get(directName) ?? [];
292
+ locations.push(location(sourceRoot, file, provider, sourceFile));
293
+ cronProviders.set(directName, locations);
294
+ }
295
+ if (!ts.isObjectLiteralExpression(provider)) continue;
296
+ const provide = findProperty(provider, "provide");
297
+ const useClass = findProperty(provider, "useClass");
298
+ const factory = findProperty(provider, "useFactory");
299
+ const useClassName = useClass ? expressionName(useClass.initializer, aliases) : void 0;
300
+ if (useClassName && Object.hasOwn(coreProviders, useClassName)) {
301
+ coreProviders[useClassName].push(location(sourceRoot, file, provider, sourceFile));
302
+ }
303
+ if (useClassName && lifecycleClasses.has(useClassName)) {
304
+ const locations = lifecycleProviders.get(useClassName) ?? [];
305
+ locations.push(location(sourceRoot, file, provider, sourceFile));
306
+ lifecycleProviders.set(useClassName, locations);
307
+ }
308
+ if (useClassName && cronClasses.has(useClassName)) {
309
+ const locations = cronProviders.get(useClassName) ?? [];
310
+ locations.push(location(sourceRoot, file, provider, sourceFile));
311
+ cronProviders.set(useClassName, locations);
312
+ }
313
+ if (provide && expressionName(provide.initializer, aliases) === "APP_GUARD") {
314
+ const factoryName = factory ? expressionName(factory.initializer, aliases) : void 0;
315
+ guards.push({
316
+ identity: useClassName && !factory ? `useClass:${useClassName}` : factoryName && !useClass ? `useFactory:${factoryName}` : void 0,
317
+ location: location(sourceRoot, file, provider, sourceFile),
318
+ relativeFile
319
+ });
320
+ }
321
+ const useExisting = findProperty(provider, "useExisting");
322
+ const existingName = useExisting ? expressionName(useExisting.initializer, aliases) : void 0;
323
+ if (existingName && lifecycleClasses.has(existingName)) {
324
+ findings.push(finding("lifecycle-provider-alias", `${existingName} implements a Nest lifecycle hook and must not be exposed through useExisting`, location(sourceRoot, file, provider, sourceFile)));
325
+ }
326
+ const inject = findProperty(provider, "inject");
327
+ if (!factory || !inject || !ts.isArrayLiteralExpression(inject.initializer) || !ts.isArrowFunction(factory.initializer) && !ts.isFunctionExpression(factory.initializer)) {
328
+ continue;
329
+ }
330
+ const factoryFunction = factory.initializer;
331
+ const injectedParameterNames = /* @__PURE__ */ new Set();
332
+ inject.initializer.elements.forEach((element, index) => {
333
+ const injectedName = expressionName(element, aliases);
334
+ const parameter = factoryFunction.parameters[index];
335
+ if (injectedName && lifecycleClasses.has(injectedName) && parameter && ts.isIdentifier(parameter.name)) {
336
+ injectedParameterNames.add(parameter.name.text);
337
+ }
338
+ });
339
+ if (injectedParameterNames.size > 0 && factoryReturnsInjectedInstance(factory.initializer, injectedParameterNames)) {
340
+ findings.push(finding("lifecycle-provider-factory-alias", "factory returns an injected lifecycle provider directly; Nest can invoke its hooks through both provider wrappers", location(sourceRoot, file, provider, sourceFile)));
341
+ }
342
+ }
343
+ }
344
+ }
345
+ if (schedulerRoot && (schedules.length !== 1 || !schedules[0]?.startsWith(`${schedulerRoot}:`))) {
346
+ findings.push(finding("schedule-root-count", `ScheduleModule.forRoot() must appear exactly once in ${schedulerRoot}; found ${schedules.length}`, schedules.join(", ") || "not found"));
347
+ }
348
+ for (const [provider, expectedFile] of Object.entries(infrastructureProviders)) {
349
+ const locations = coreProviders[provider];
350
+ if (locations.length !== 1 || !locations[0].startsWith(`${expectedFile}:`)) {
351
+ findings.push(finding("infrastructure-provider-owner", `${provider} must be provided exactly once by ${expectedFile}; found ${locations.length}`, locations.join(", ") || "not found"));
352
+ }
353
+ }
354
+ const guardNames = guards.map(({ identity }) => identity).filter((name) => Boolean(name));
355
+ const duplicateGuards = guardNames.filter((name, index) => guardNames.indexOf(name) !== index);
356
+ const actualGuardSet = new Set(guardNames);
357
+ if (globalGuards && (guards.some(({ relativeFile }) => relativeFile !== globalGuards.owner) || guards.some(({ identity }) => !identity) || duplicateGuards.length > 0 || actualGuardSet.size !== expectedGlobalGuards.size || [
358
+ ...expectedGlobalGuards
359
+ ].some((name) => !actualGuardSet.has(name)))) {
360
+ findings.push(finding("global-guard-composition", `APP_GUARD must be registered once per expected guard in ${globalGuards.owner}; found ${guardNames.join(", ") || "none"}`, guards.map(({ location: at }) => at).join(", ") || "not found"));
361
+ }
362
+ for (const [className, locations] of lifecycleProviders) {
363
+ if (locations.length > 1) {
364
+ findings.push(finding("duplicate-lifecycle-provider", `${className} is provided by multiple modules and would have multiple lifecycle owners`, locations.join(", ")));
365
+ }
366
+ }
367
+ for (const className of cronClasses) {
368
+ const locations = cronProviders.get(className) ?? [];
369
+ if (locations.length !== 1) {
370
+ findings.push(finding("cron-provider-owner", `${className} contains @Cron jobs and must be provided exactly once; found ${locations.length}`, locations.join(", ") || "not found"));
371
+ }
372
+ }
373
+ return {
374
+ ok: findings.length === 0,
375
+ findings,
376
+ stats: {
377
+ files: files.length,
378
+ cronJobs: cronJobs.length,
379
+ cronProviders: cronProviders.size,
380
+ globalGuards: guards.length,
381
+ lifecycleClasses: lifecycleClasses.size,
382
+ scheduleRoots: schedules.length
383
+ }
384
+ };
385
+ }
386
+ __name(auditModuleWiring, "auditModuleWiring");
387
+
388
+ // src/tooling/transaction-boundaries.ts
389
+ import { readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
390
+ import { join } from "path";
391
+ import ts2 from "typescript";
392
+ function findTransactionBoundaryViolations(source, fileName, policy) {
393
+ const sourceFile = ts2.createSourceFile(fileName, source, ts2.ScriptTarget.Latest, true);
394
+ const violations = [];
395
+ const addViolation = /* @__PURE__ */ __name((node, message) => {
396
+ violations.push({
397
+ file: fileName,
398
+ line: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1,
399
+ message
400
+ });
401
+ }, "addViolation");
402
+ const inspectTransaction = /* @__PURE__ */ __name((callback) => {
403
+ const transactionClient = callback.parameters[0]?.name.getText(sourceFile);
404
+ if (!transactionClient) return;
405
+ const inspectBody = /* @__PURE__ */ __name((node) => {
406
+ if (ts2.isCallExpression(node)) {
407
+ const callee = node.expression.getText(sourceFile);
408
+ if (policy.rootClientNames.some((name) => callee.startsWith(`${name}.`) && transactionClient !== name)) {
409
+ addViolation(node, "interactive transaction uses the root Prisma client");
410
+ }
411
+ if (callee === "Promise.all" || callee === "Promise.allSettled") {
412
+ addViolation(node, "interactive transaction runs parallel work");
413
+ }
414
+ if (policy.externalCallPattern.test(callee)) {
415
+ addViolation(node, `interactive transaction performs external work via ${callee}`);
416
+ }
417
+ if (policy.transactionClientRequiredPattern.test(callee) && !node.arguments.some((argument) => argument.getText(sourceFile) === transactionClient)) {
418
+ addViolation(node, `service call ${callee} does not receive the transaction client`);
419
+ }
420
+ }
421
+ ts2.forEachChild(node, inspectBody);
422
+ }, "inspectBody");
423
+ inspectBody(callback.body);
424
+ }, "inspectTransaction");
425
+ const visit = /* @__PURE__ */ __name((node) => {
426
+ if (ts2.isCallExpression(node) && node.expression.getText(sourceFile).endsWith(".$transaction")) {
427
+ const callback = node.arguments[0];
428
+ if (callback && (ts2.isArrowFunction(callback) || ts2.isFunctionExpression(callback))) inspectTransaction(callback);
429
+ }
430
+ ts2.forEachChild(node, visit);
431
+ }, "visit");
432
+ visit(sourceFile);
433
+ return violations;
434
+ }
435
+ __name(findTransactionBoundaryViolations, "findTransactionBoundaryViolations");
436
+ function checkTransactionBoundaries(rootDirectory, policy) {
437
+ const scan = /* @__PURE__ */ __name((directory) => readdirSync2(directory, {
438
+ withFileTypes: true
439
+ }).flatMap((entry) => {
440
+ const entryPath = join(directory, entry.name);
441
+ if (entry.isDirectory()) return scan(entryPath);
442
+ if (!entry.name.endsWith(".ts") || entry.name.endsWith(".spec.ts")) return [];
443
+ return findTransactionBoundaryViolations(readFileSync2(entryPath, "utf8"), entryPath, policy);
444
+ }), "scan");
445
+ return scan(rootDirectory);
446
+ }
447
+ __name(checkTransactionBoundaries, "checkTransactionBoundaries");
448
+
449
+ // src/tooling/framework-boundaries.ts
450
+ var PUBLIC_FRAMEWORK_SUBPATHS = /* @__PURE__ */ new Set([
451
+ "@zucker-framework/cli/agent-instructions",
452
+ "@zucker-framework/ai/langchain",
453
+ "@zucker-framework/core/privacy",
454
+ "@zucker-framework/core/user-agent"
455
+ ]);
456
+ function auditFrameworkSourceText(file, source, rules = []) {
457
+ const violations = [];
458
+ const normalizedFile = file.replaceAll("\\", "/");
459
+ for (const specifier of importedSpecifiers(source)) {
460
+ if (specifier.startsWith("@zucker/")) {
461
+ violations.push(`${file}: replace retired @zucker/ imports with @zucker-framework/`);
462
+ }
463
+ if (specifier.startsWith("@zucker-framework/")) {
464
+ const segments = specifier.split("/");
465
+ if (segments.length !== 2 && !PUBLIC_FRAMEWORK_SUBPATHS.has(specifier)) {
466
+ violations.push(`${file}: import ${specifier} through a documented package public API`);
467
+ }
468
+ }
469
+ if (specifier.includes("zucker-framework/packages/") || specifier.includes("zucker-framework/src/")) {
470
+ violations.push(`${file}: do not import Zucker Framework source directly`);
471
+ }
472
+ for (const rule of rules) {
473
+ if (specifier === rule.specifier && rule.matchesFile(normalizedFile)) {
474
+ violations.push(`${file}: ${rule.message}`);
475
+ }
476
+ }
477
+ }
478
+ return violations;
479
+ }
480
+ __name(auditFrameworkSourceText, "auditFrameworkSourceText");
481
+ function auditPnpmWorkflowVersions(packageManager, workflowSources) {
482
+ const violations = [];
483
+ if (!/^pnpm@\d+\.\d+\.\d+$/.test(packageManager ?? "")) {
484
+ violations.push("package.json: packageManager must pin an exact pnpm version");
485
+ return violations;
486
+ }
487
+ const expectedPnpm = packageManager.slice("pnpm@".length);
488
+ for (const [file, source] of workflowSources) {
489
+ const setupPattern = /uses:\s*pnpm\/action-setup@[^\n]+/g;
490
+ let match;
491
+ while (match = setupPattern.exec(source)) {
492
+ const setupBlock = source.slice(match.index, match.index + 260);
493
+ const configuredPnpm = setupBlock.match(/\n\s*version:\s*([^\s#]+)/)?.[1];
494
+ if (configuredPnpm !== expectedPnpm) {
495
+ violations.push(`${file}: pnpm ${configuredPnpm ?? "missing"} does not match packageManager ${expectedPnpm}`);
496
+ }
497
+ }
498
+ }
499
+ return violations;
500
+ }
501
+ __name(auditPnpmWorkflowVersions, "auditPnpmWorkflowVersions");
502
+ function importedSpecifiers(source) {
503
+ const specifiers = [];
504
+ const importPattern = /(?:\bfrom\s*|\bimport\s*(?:\(\s*)?|\brequire\s*\(\s*)["']([^"']+)["']/g;
505
+ let match;
506
+ while (match = importPattern.exec(source)) specifiers.push(match[1]);
507
+ return specifiers;
508
+ }
509
+ __name(importedSpecifiers, "importedSpecifiers");
510
+ export {
511
+ auditFrameworkSourceText,
512
+ auditModuleWiring,
513
+ auditPnpmWorkflowVersions,
514
+ checkTransactionBoundaries,
515
+ findTransactionBoundaryViolations
516
+ };
package/package.json CHANGED
@@ -1,25 +1,28 @@
1
1
  {
2
2
  "name": "@zucker-framework/cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "CLI tool for Zucker Framework — scaffolding, diagnostics, and module management",
5
5
  "bin": {
6
6
  "zucker": "./dist/index.js"
7
7
  },
8
- "main": "dist/index.js",
9
- "types": "dist/index.d.ts",
8
+ "main": "dist/tooling.js",
9
+ "types": "dist/tooling.d.ts",
10
10
  "files": [
11
11
  "dist",
12
12
  "LICENSE",
13
13
  "NOTICE",
14
- "THIRD_PARTY_NOTICES"
14
+ "THIRD_PARTY_NOTICES",
15
+ "tooling/agent-instructions.mjs",
16
+ "tooling/agent-instructions.d.mts"
15
17
  ],
16
18
  "dependencies": {
17
- "@zucker-framework/ai-access": "1.0.0",
19
+ "@zucker-framework/ai-access": "1.0.2",
18
20
  "chalk": "^6.0.0",
19
21
  "commander": "^15.0.0",
20
22
  "dotenv": "^17.4.2",
21
23
  "openai": "7.8.0",
22
- "pg": "8.23.0"
24
+ "pg": "8.23.0",
25
+ "typescript": "^6.0.3"
23
26
  },
24
27
  "devDependencies": {
25
28
  "@types/node": "^20.19.43",
@@ -39,8 +42,26 @@
39
42
  "access": "public",
40
43
  "registry": "https://registry.npmjs.org/"
41
44
  },
45
+ "module": "dist/tooling.mjs",
46
+ "exports": {
47
+ ".": {
48
+ "import": {
49
+ "types": "./dist/tooling.d.mts",
50
+ "default": "./dist/tooling.mjs"
51
+ },
52
+ "require": {
53
+ "types": "./dist/tooling.d.ts",
54
+ "default": "./dist/tooling.js"
55
+ }
56
+ },
57
+ "./agent-instructions": {
58
+ "types": "./tooling/agent-instructions.d.mts",
59
+ "import": "./tooling/agent-instructions.mjs",
60
+ "default": "./tooling/agent-instructions.mjs"
61
+ }
62
+ },
42
63
  "scripts": {
43
- "build": "tsup src/index.ts --format cjs --dts && node ../../scripts/prepare-cli-catalog.mjs",
64
+ "build": "tsup --config tsup.config.ts && node ../../scripts/prepare-cli-catalog.mjs",
44
65
  "clean": "rm -rf dist",
45
66
  "lint": "eslint src/"
46
67
  }
@@ -0,0 +1,25 @@
1
+ export interface AgentInstructionPolicy {
2
+ skillsDirectory: string;
3
+ documents: readonly string[];
4
+ rootDocument: string;
5
+ compatibilityLinks: readonly {
6
+ path: string;
7
+ target?: string;
8
+ optional?: boolean;
9
+ }[];
10
+ skillAliasDirectories: readonly string[];
11
+ allowSkillReferenceAliases?: boolean;
12
+ checkGit?: boolean;
13
+ }
14
+
15
+ export interface AgentInstructionAudit {
16
+ errors: string[];
17
+ skills: number;
18
+ aliases: number;
19
+ }
20
+
21
+ /** Read the selected local instruction documents and optionally inspect Git ignore visibility. */
22
+ export declare function auditAgentInstructions(
23
+ repository: string,
24
+ policy: AgentInstructionPolicy,
25
+ ): AgentInstructionAudit;