@devcraft-ts/diadem 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,815 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readFileSync, mkdirSync, writeFileSync, readdirSync } from 'fs';
3
+ import { resolve, dirname, join, relative } from 'path';
4
+ import ts from 'typescript';
5
+
6
+ var DEFAULTS = {
7
+ scanDirs: ["src"],
8
+ // Default: every .ts file. The AST pass keeps only DI-decorated classes, so
9
+ // `include` is purely an optional performance narrowing (e.g. ['Service\\.ts$']).
10
+ include: ["\\.ts$"],
11
+ exclude: [
12
+ "\\.test\\.ts$",
13
+ "\\.spec\\.ts$",
14
+ "\\.d\\.ts$",
15
+ "node_modules",
16
+ "/dist/",
17
+ "/build/",
18
+ "/generated/"
19
+ ],
20
+ outFile: "src/generated/service-manifest.ts",
21
+ environments: ["development", "production", "test"],
22
+ emit: "manifest",
23
+ targetEnv: void 0
24
+ };
25
+ function stripUndefined(obj) {
26
+ return Object.fromEntries(
27
+ Object.entries(obj).filter(([, v]) => v !== void 0)
28
+ );
29
+ }
30
+ function loadConfig(rootDir, overrides = {}) {
31
+ let fileConfig = {};
32
+ const configPath = resolve(rootDir, "diadem.config.json");
33
+ if (existsSync(configPath)) {
34
+ fileConfig = JSON.parse(
35
+ readFileSync(configPath, "utf8")
36
+ );
37
+ }
38
+ const merged = {
39
+ ...DEFAULTS,
40
+ ...stripUndefined(fileConfig),
41
+ ...stripUndefined(overrides)
42
+ };
43
+ return {
44
+ rootDir,
45
+ scanDirs: merged.scanDirs,
46
+ include: merged.include.map((p) => new RegExp(p)),
47
+ exclude: merged.exclude.map((p) => new RegExp(p)),
48
+ outFile: merged.outFile,
49
+ environments: merged.environments,
50
+ emit: merged.emit,
51
+ targetEnv: merged.targetEnv
52
+ };
53
+ }
54
+ var PACKAGE_NAME = "@devcraft-ts/diadem";
55
+ var LIFECYCLE_BY_DECORATOR = {
56
+ singleton: "singleton",
57
+ factory: "factory",
58
+ lazy: "lazy",
59
+ lazySingleton: "lazySingleton",
60
+ injectable: "dependency"
61
+ };
62
+ var PRIMITIVE_TYPES = /* @__PURE__ */ new Set([
63
+ "string",
64
+ "number",
65
+ "boolean",
66
+ "Date",
67
+ "Array",
68
+ "Object",
69
+ "any",
70
+ "unknown",
71
+ "void",
72
+ "null",
73
+ "undefined",
74
+ "Promise",
75
+ "RegExp",
76
+ "Error",
77
+ "Map",
78
+ "Set"
79
+ ]);
80
+ function generateManifest(config) {
81
+ const files = collectFiles(config);
82
+ const services = [];
83
+ for (const file of files) {
84
+ services.push(...analyzeFile(file.fullPath, file.relPath));
85
+ }
86
+ const { sorted, cycles, duplicateTokens } = resolveAndSort(services);
87
+ sorted.forEach((service, index) => {
88
+ service.registrationOrder = index;
89
+ });
90
+ const outFile = resolve(config.rootDir, config.outFile);
91
+ const content = config.emit === "compiled" ? renderCompiled(sorted, config, outFile) : renderManifest(sorted, config, outFile);
92
+ mkdirSync(dirname(outFile), { recursive: true });
93
+ writeFileSync(outFile, content, "utf8");
94
+ const externalDependencies = sorted.reduce(
95
+ (sum, s) => sum + s.resolvedDependencies.filter((d) => d.external).length,
96
+ 0
97
+ );
98
+ const unresolved = [];
99
+ for (const service of sorted) {
100
+ for (const dep of service.resolvedDependencies) {
101
+ if (dep.external && !dep.isOptional) {
102
+ unresolved.push({
103
+ service: service.className,
104
+ paramName: dep.paramName,
105
+ typeName: dep.typeName
106
+ });
107
+ }
108
+ }
109
+ }
110
+ return {
111
+ outFile,
112
+ serviceCount: sorted.length,
113
+ cycles,
114
+ externalDependencies,
115
+ unresolved,
116
+ duplicateTokens
117
+ };
118
+ }
119
+ function collectFiles(config) {
120
+ const files = [];
121
+ for (const dir of config.scanDirs) {
122
+ const abs = resolve(config.rootDir, dir);
123
+ if (!existsSync(abs)) {
124
+ continue;
125
+ }
126
+ walk(abs, dir, config, files);
127
+ }
128
+ return files;
129
+ }
130
+ function walk(absDir, relDir, config, out) {
131
+ for (const entry of readdirSync(absDir, { withFileTypes: true })) {
132
+ const fullPath = join(absDir, entry.name);
133
+ const relPath = join(relDir, entry.name);
134
+ if (entry.isDirectory()) {
135
+ walk(fullPath, relPath, config, out);
136
+ } else if (entry.isFile() && shouldScan(relPath, config)) {
137
+ out.push({ fullPath, relPath });
138
+ }
139
+ }
140
+ }
141
+ function shouldScan(relPath, config) {
142
+ const normalized = relPath.replace(/\\/g, "/");
143
+ if (config.exclude.some((re) => re.test(normalized))) {
144
+ return false;
145
+ }
146
+ return config.include.some((re) => re.test(normalized));
147
+ }
148
+ function analyzeFile(fullPath, relPath) {
149
+ const source = ts.createSourceFile(
150
+ fullPath,
151
+ readFileSync(fullPath, "utf8"),
152
+ ts.ScriptTarget.Latest,
153
+ /* setParentNodes */
154
+ true
155
+ );
156
+ const tokenSources = /* @__PURE__ */ new Map();
157
+ const fileDir = dirname(fullPath);
158
+ const exportedClasses = /* @__PURE__ */ new Set();
159
+ ts.forEachChild(source, function collect(node) {
160
+ if (ts.isImportDeclaration(node) && node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings) && ts.isStringLiteral(node.moduleSpecifier)) {
161
+ const specifier = node.moduleSpecifier.text;
162
+ const module = specifier.startsWith(".") ? { kind: "file", fullPath: resolve(fileDir, specifier) } : { kind: "bare", specifier };
163
+ for (const element of node.importClause.namedBindings.elements) {
164
+ tokenSources.set(element.name.text, module);
165
+ }
166
+ }
167
+ if (ts.isClassDeclaration(node) && node.name && isExported(node)) {
168
+ exportedClasses.add(node.name.text);
169
+ tokenSources.set(node.name.text, { kind: "file", fullPath });
170
+ }
171
+ ts.forEachChild(node, collect);
172
+ });
173
+ const services = [];
174
+ ts.forEachChild(source, function visit(node) {
175
+ if (ts.isClassDeclaration(node)) {
176
+ const info = analyzeClass(node, fullPath, relPath, exportedClasses, tokenSources);
177
+ if (info) {
178
+ services.push(info);
179
+ }
180
+ }
181
+ ts.forEachChild(node, visit);
182
+ });
183
+ return services;
184
+ }
185
+ function analyzeClass(node, fullPath, relPath, exportedClasses, tokenSources) {
186
+ if (!node.name) {
187
+ return null;
188
+ }
189
+ const decoratorInfo = findDIDecorator(node);
190
+ if (!decoratorInfo) {
191
+ return null;
192
+ }
193
+ const dependencies = analyzeConstructor(node);
194
+ const token = decoratorInfo.token;
195
+ return {
196
+ className: node.name.text,
197
+ lifecycle: decoratorInfo.lifecycle,
198
+ environment: decoratorInfo.environment,
199
+ token,
200
+ tokenExported: !!token && exportedClasses.has(token),
201
+ tokenModule: token ? tokenSources.get(token) : void 0,
202
+ fullPath,
203
+ filePath: relPath.replace(/\\/g, "/"),
204
+ exported: isExported(node),
205
+ dependencies,
206
+ resolvedDependencies: [],
207
+ registrationOrder: 0
208
+ };
209
+ }
210
+ function findDIDecorator(node) {
211
+ const decorators = ts.canHaveDecorators(node) ? ts.getDecorators(node) ?? [] : [];
212
+ for (const decorator of decorators) {
213
+ const expr = decorator.expression;
214
+ let name = "";
215
+ let args;
216
+ if (ts.isCallExpression(expr)) {
217
+ name = expr.expression.getText();
218
+ args = expr.arguments;
219
+ } else if (ts.isIdentifier(expr)) {
220
+ name = expr.getText();
221
+ }
222
+ const lifecycle = LIFECYCLE_BY_DECORATOR[name];
223
+ if (!lifecycle) {
224
+ continue;
225
+ }
226
+ let token;
227
+ let environment;
228
+ if (args && args.length > 0 && ts.isIdentifier(args[0])) {
229
+ token = args[0].getText();
230
+ }
231
+ if (args && args.length > 1 && ts.isStringLiteral(args[1])) {
232
+ environment = args[1].text;
233
+ }
234
+ return { lifecycle, token, environment };
235
+ }
236
+ return null;
237
+ }
238
+ function analyzeConstructor(node) {
239
+ const ctor = node.members.find((m) => ts.isConstructorDeclaration(m));
240
+ if (!ctor) {
241
+ return [];
242
+ }
243
+ const deps = [];
244
+ ctor.parameters.forEach((param, index) => {
245
+ if (!param.type) {
246
+ return;
247
+ }
248
+ const typeName = extractTypeName(param.type);
249
+ if (isPrimitive(typeName)) {
250
+ return;
251
+ }
252
+ const modifiers = ts.canHaveModifiers(param) ? ts.getModifiers(param) ?? [] : [];
253
+ deps.push({
254
+ paramName: param.name.getText(),
255
+ paramIndex: index,
256
+ typeName,
257
+ isOptional: !!param.questionToken || !!param.initializer,
258
+ isReadonly: modifiers.some(
259
+ (m) => m.kind === ts.SyntaxKind.ReadonlyKeyword
260
+ ),
261
+ isPrivate: modifiers.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword)
262
+ });
263
+ });
264
+ return deps;
265
+ }
266
+ function extractTypeName(typeNode) {
267
+ const raw = ts.isTypeReferenceNode(typeNode) ? typeNode.typeName.getText() : typeNode.getText();
268
+ return raw.replace(/<[^>]*>/g, "").replace(/\[\]/g, "").trim();
269
+ }
270
+ function isPrimitive(typeName) {
271
+ return PRIMITIVE_TYPES.has(typeName) || typeName.toLowerCase() === typeName || typeName.includes("<");
272
+ }
273
+ function isExported(node) {
274
+ const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : void 0;
275
+ return modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
276
+ }
277
+ function resolveAndSort(services) {
278
+ const serviceByName = /* @__PURE__ */ new Map();
279
+ const tokenToImpl = /* @__PURE__ */ new Map();
280
+ const duplicateTokens = /* @__PURE__ */ new Set();
281
+ for (const service of services) {
282
+ serviceByName.set(service.className, service);
283
+ if (service.token) {
284
+ if (tokenToImpl.has(service.token)) {
285
+ duplicateTokens.add(service.token);
286
+ } else {
287
+ tokenToImpl.set(service.token, service.className);
288
+ }
289
+ }
290
+ }
291
+ const resolveByHeuristic = (typeName) => {
292
+ const direct = serviceByName.get(typeName);
293
+ if (direct) {
294
+ return direct.className;
295
+ }
296
+ if (typeName.startsWith("I")) {
297
+ const stripped = typeName.slice(1);
298
+ if (serviceByName.has(stripped)) {
299
+ return stripped;
300
+ }
301
+ const suffixed = services.find(
302
+ (s) => (s.className.endsWith("Service") || s.className.endsWith("Repository")) && s.className.includes(stripped)
303
+ );
304
+ if (suffixed) {
305
+ return suffixed.className;
306
+ }
307
+ }
308
+ return void 0;
309
+ };
310
+ for (const service of services) {
311
+ service.resolvedDependencies = service.dependencies.map((dep) => {
312
+ const implementingService = tokenToImpl.get(dep.typeName) ?? resolveByHeuristic(dep.typeName);
313
+ return implementingService ? { ...dep, implementingService } : { ...dep, external: true };
314
+ });
315
+ }
316
+ const { sorted, cycles } = topologicalSort(services, serviceByName);
317
+ return { sorted, cycles, duplicateTokens: [...duplicateTokens] };
318
+ }
319
+ function topologicalSort(services, serviceByName) {
320
+ const visited = /* @__PURE__ */ new Set();
321
+ const visiting = /* @__PURE__ */ new Set();
322
+ const sorted = [];
323
+ const cycles = [];
324
+ const visit = (name) => {
325
+ if (visited.has(name)) {
326
+ return;
327
+ }
328
+ if (visiting.has(name)) {
329
+ cycles.push(name);
330
+ return;
331
+ }
332
+ const service = serviceByName.get(name);
333
+ if (!service) {
334
+ return;
335
+ }
336
+ visiting.add(name);
337
+ for (const dep of service.resolvedDependencies) {
338
+ if (dep.implementingService && !dep.external) {
339
+ visit(dep.implementingService);
340
+ }
341
+ }
342
+ visiting.delete(name);
343
+ visited.add(name);
344
+ sorted.push(service);
345
+ };
346
+ for (const service of services) {
347
+ visit(service.className);
348
+ }
349
+ return { sorted, cycles };
350
+ }
351
+ function importPathFor(outFile, serviceFullPath) {
352
+ let rel = relative(dirname(outFile), serviceFullPath).replace(/\\/g, "/").replace(/\.ts$/, "");
353
+ if (!rel.startsWith(".")) {
354
+ rel = `./${rel}`;
355
+ }
356
+ return rel;
357
+ }
358
+ function groupByEnvironment(services, environments) {
359
+ const groups = { all: [] };
360
+ for (const env of environments) {
361
+ groups[env] = [];
362
+ }
363
+ for (const service of services) {
364
+ groups.all.push(service);
365
+ if (service.environment) {
366
+ groups[service.environment]?.push(service);
367
+ } else {
368
+ for (const env of environments) {
369
+ groups[env].push(service);
370
+ }
371
+ }
372
+ }
373
+ return groups;
374
+ }
375
+ function toEntry(service, outFile) {
376
+ const entry = {
377
+ className: service.className,
378
+ importPath: importPathFor(outFile, service.fullPath),
379
+ lifecycle: service.lifecycle,
380
+ exported: service.exported,
381
+ filePath: service.filePath,
382
+ registrationOrder: service.registrationOrder,
383
+ dependencies: service.dependencies,
384
+ resolvedDependencies: service.resolvedDependencies
385
+ };
386
+ if (service.environment) {
387
+ entry.environment = service.environment;
388
+ }
389
+ return entry;
390
+ }
391
+ function renderManifest(services, config, outFile) {
392
+ const byEnv = groupByEnvironment(services, config.environments);
393
+ const importsByPath = /* @__PURE__ */ new Map();
394
+ for (const service of services) {
395
+ const path = importPathFor(outFile, service.fullPath);
396
+ const names = importsByPath.get(path) ?? /* @__PURE__ */ new Set();
397
+ names.add(service.className);
398
+ if (service.token && service.token !== service.className && service.tokenExported) {
399
+ names.add(service.token);
400
+ }
401
+ importsByPath.set(path, names);
402
+ }
403
+ const staticImports = [...importsByPath.keys()].sort().map((path) => {
404
+ const names = [...importsByPath.get(path) ?? []].sort();
405
+ return names.length === 1 ? `import { ${names[0]} } from '${path}'` : `import {
406
+ ${names.join(",\n ")}
407
+ } from '${path}'`;
408
+ }).join("\n");
409
+ const serviceClassMapping = services.map((s) => ` ${s.className}`).join(",\n");
410
+ const envEntries = ["all", ...config.environments].map(
411
+ (env) => ` ${env}: ${JSON.stringify(byEnv[env].map((s) => toEntry(s, outFile)), null, 2)} as ServiceManifestEntry[]`
412
+ ).join(",\n");
413
+ const lifecycleCounts = ["dependency", "singleton", "factory", "lazy", "lazySingleton"].map((lc) => ` ${lc}: ${services.filter((s) => s.lifecycle === lc).length}`).join(",\n");
414
+ const totalDependencies = services.reduce(
415
+ (sum, s) => sum + s.dependencies.length,
416
+ 0
417
+ );
418
+ const externalDependencies = services.reduce(
419
+ (sum, s) => sum + s.resolvedDependencies.filter((d) => d.external).length,
420
+ 0
421
+ );
422
+ const maxDepth = services.reduce(
423
+ (max, s) => Math.max(max, s.dependencies.length),
424
+ 0
425
+ );
426
+ return `/**
427
+ * Auto-generated by \`diadem build\`. DO NOT EDIT MANUALLY.
428
+ *
429
+ * Total services: ${services.length}
430
+ */
431
+
432
+ /* eslint-disable */
433
+
434
+ import type {
435
+ ImportedService,
436
+ ServiceManifestEntry
437
+ } from '${PACKAGE_NAME}'
438
+
439
+ ${staticImports}
440
+
441
+ export const SERVICE_MANIFEST: ServiceManifestEntry[] = ${JSON.stringify(
442
+ services.map((s) => toEntry(s, outFile)),
443
+ null,
444
+ 2
445
+ )} as ServiceManifestEntry[]
446
+
447
+ export const SERVICES_BY_ENVIRONMENT = {
448
+ ${envEntries}
449
+ }
450
+
451
+ export const SERVICE_CLASSES = {
452
+ ${serviceClassMapping}
453
+ } as const
454
+
455
+ export function getServicesForEnvironment(
456
+ environment?: string
457
+ ): ServiceManifestEntry[] {
458
+ if (!environment || environment === 'all') {
459
+ return SERVICE_MANIFEST
460
+ }
461
+ return (
462
+ SERVICES_BY_ENVIRONMENT[
463
+ environment as keyof typeof SERVICES_BY_ENVIRONMENT
464
+ ] ?? []
465
+ )
466
+ }
467
+
468
+ export async function importService(entry: ServiceManifestEntry) {
469
+ const serviceClass =
470
+ SERVICE_CLASSES[entry.className as keyof typeof SERVICE_CLASSES]
471
+ if (!serviceClass) {
472
+ throw new Error(\`Service \${entry.className} not found in manifest.\`)
473
+ }
474
+ return serviceClass
475
+ }
476
+
477
+ export async function importAllServices(
478
+ entries: ServiceManifestEntry[]
479
+ ): Promise<ImportedService[]> {
480
+ const ordered = [...entries].sort(
481
+ (a, b) => a.registrationOrder - b.registrationOrder
482
+ )
483
+ const results: ImportedService[] = []
484
+ for (const entry of ordered) {
485
+ const serviceClass =
486
+ SERVICE_CLASSES[entry.className as keyof typeof SERVICE_CLASSES]
487
+ if (serviceClass) {
488
+ results.push({ entry, serviceClass })
489
+ }
490
+ }
491
+ return results
492
+ }
493
+
494
+ export const MANIFEST_STATS = {
495
+ totalServices: ${services.length},
496
+ environments: [${config.environments.map((e) => `'${e}'`).join(", ")}],
497
+ lifecycles: {
498
+ ${lifecycleCounts}
499
+ },
500
+ dependencyAnalysis: {
501
+ servicesWithDependencies: ${services.filter((s) => s.dependencies.length > 0).length},
502
+ totalDependencies: ${totalDependencies},
503
+ externalDependencies: ${externalDependencies},
504
+ maxDependencyDepth: ${maxDepth}
505
+ }
506
+ }
507
+ `;
508
+ }
509
+ var EAGER_LIFECYCLES = /* @__PURE__ */ new Set([
510
+ "singleton",
511
+ "dependency",
512
+ "lazySingleton"
513
+ ]);
514
+ function localName(className) {
515
+ return `_${className}`;
516
+ }
517
+ function externalDefault(typeName) {
518
+ switch (typeName) {
519
+ case "string":
520
+ return "''";
521
+ case "number":
522
+ return "0";
523
+ case "boolean":
524
+ return "false";
525
+ default:
526
+ return "undefined";
527
+ }
528
+ }
529
+ function renderCompiled(allServices, config, outFile) {
530
+ const target = config.targetEnv;
531
+ const services = allServices.filter(
532
+ (s) => !target || !s.environment || s.environment === target
533
+ );
534
+ const selected = new Set(services.map((s) => s.className));
535
+ const eager = /* @__PURE__ */ new Map();
536
+ for (const s of services) {
537
+ eager.set(s.className, EAGER_LIFECYCLES.has(s.lifecycle));
538
+ }
539
+ const classNames = new Set(services.map((s) => s.className));
540
+ const importsByPath = /* @__PURE__ */ new Map();
541
+ const addImport = (path, name) => {
542
+ const names = importsByPath.get(path) ?? /* @__PURE__ */ new Set();
543
+ names.add(name);
544
+ importsByPath.set(path, names);
545
+ };
546
+ for (const service of services) {
547
+ addImport(importPathFor(outFile, service.fullPath), service.className);
548
+ }
549
+ const tokenCount = /* @__PURE__ */ new Map();
550
+ for (const s of services) {
551
+ if (s.token) {
552
+ tokenCount.set(s.token, (tokenCount.get(s.token) ?? 0) + 1);
553
+ }
554
+ }
555
+ const tokenPath = (m) => m.kind === "file" ? importPathFor(outFile, m.fullPath) : m.specifier;
556
+ const typed = services.filter(
557
+ (s) => !!s.token && !!s.tokenModule && tokenCount.get(s.token) === 1 && (s.token === s.className || !classNames.has(s.token))
558
+ );
559
+ for (const s of typed) {
560
+ addImport(tokenPath(s.tokenModule), s.token);
561
+ }
562
+ const serviceImports = [...importsByPath.keys()].sort().map((path) => {
563
+ const names = [...importsByPath.get(path) ?? []].sort();
564
+ return names.length === 1 ? `import { ${names[0]} } from '${path}'` : `import {
565
+ ${names.join(",\n ")}
566
+ } from '${path}'`;
567
+ }).join("\n");
568
+ const argExpr = (service) => {
569
+ const arity = service.dependencies.reduce(
570
+ (max, d) => Math.max(max, d.paramIndex + 1),
571
+ 0
572
+ );
573
+ const args = Array.from({ length: arity }, () => "undefined");
574
+ for (const dep of service.resolvedDependencies) {
575
+ if (dep.external) {
576
+ args[dep.paramIndex] = dep.isOptional ? "undefined" : externalDefault(dep.typeName);
577
+ continue;
578
+ }
579
+ const impl = dep.implementingService;
580
+ if (impl && selected.has(impl)) {
581
+ args[dep.paramIndex] = eager.get(impl) ? localName(impl) : `c.resolve(token(${impl}))`;
582
+ } else {
583
+ args[dep.paramIndex] = "undefined";
584
+ }
585
+ }
586
+ return args.join(", ");
587
+ };
588
+ const lines = [];
589
+ for (const service of services) {
590
+ const cls = service.className;
591
+ if (eager.get(cls)) {
592
+ lines.push(` const ${localName(cls)} = new ${cls}(${argExpr(service)})`);
593
+ lines.push(` c.register(token(${cls}), ${localName(cls)})`);
594
+ } else {
595
+ lines.push(
596
+ ` c.registerFactory(token(${cls}), () => new ${cls}(${argExpr(service)}))`
597
+ );
598
+ }
599
+ }
600
+ const targetComment = target ? `environment: ${target}` : "environment: all";
601
+ const accessorBlock = typed.length === 0 ? "" : `
602
+ /**
603
+ * Type-safe accessor surface. Only registered tokens are present, each typed to
604
+ * its token \u2014 resolving an unregistered token is a compile error.
605
+ */
606
+ export interface DiademServices {
607
+ ${typed.map((s) => ` ${s.token}: ${s.token}`).join("\n")}
608
+ }
609
+
610
+ export function createServices(): DiademServices & {
611
+ readonly container: DiademContainer
612
+ dispose: () => Promise<void>
613
+ } {
614
+ const container = createContainer()
615
+ return {
616
+ container,
617
+ dispose: () => container.dispose(),
618
+ ${typed.map(
619
+ (s) => ` get ${s.token}(): ${s.token} {
620
+ return container.resolve(${s.token})
621
+ }`
622
+ ).join(",\n")}
623
+ }
624
+ }
625
+ `;
626
+ return `/**
627
+ * Auto-generated by \`diadem build --emit=compiled\`. DO NOT EDIT MANUALLY.
628
+ *
629
+ * Straight-line wiring (${targetComment}). Total services: ${services.length}.
630
+ */
631
+
632
+ /* eslint-disable */
633
+
634
+ import { DiademContainer, getDIMetadata } from '${PACKAGE_NAME}'
635
+
636
+ ${serviceImports}
637
+
638
+ function token(cls: any): any {
639
+ const meta = getDIMetadata(cls)
640
+ if (!meta) {
641
+ throw new Error('diadem: missing DI metadata for ' + cls.name)
642
+ }
643
+ return meta.token
644
+ }
645
+
646
+ /** Build a fully-wired, ready container. */
647
+ export function createContainer(): DiademContainer {
648
+ const c = new DiademContainer()
649
+ ${lines.join("\n")}
650
+ c.setReady()
651
+ return c
652
+ }
653
+ ${accessorBlock}`;
654
+ }
655
+
656
+ // src/cli/index.ts
657
+ function parseArgs(argv) {
658
+ const parsed = {
659
+ command: "build",
660
+ cwd: process.cwd(),
661
+ failOnCycle: false,
662
+ strict: false,
663
+ help: false,
664
+ overrides: {}
665
+ };
666
+ const scanDirs = [];
667
+ const include = [];
668
+ const exclude = [];
669
+ const environments = [];
670
+ let i = 0;
671
+ if (argv[i] && !argv[i].startsWith("-")) {
672
+ parsed.command = argv[i];
673
+ i++;
674
+ }
675
+ for (; i < argv.length; i++) {
676
+ const arg = argv[i];
677
+ const next = () => {
678
+ const value = argv[++i];
679
+ if (value === void 0) {
680
+ throw new Error(`Missing value for ${arg}`);
681
+ }
682
+ return value;
683
+ };
684
+ switch (arg) {
685
+ case "--scan-dir":
686
+ scanDirs.push(next());
687
+ break;
688
+ case "--out":
689
+ parsed.overrides.outFile = next();
690
+ break;
691
+ case "--include":
692
+ include.push(next());
693
+ break;
694
+ case "--exclude":
695
+ exclude.push(next());
696
+ break;
697
+ case "--env":
698
+ environments.push(next());
699
+ break;
700
+ case "--cwd":
701
+ parsed.cwd = next();
702
+ break;
703
+ case "--emit": {
704
+ const mode = next();
705
+ if (mode !== "manifest" && mode !== "compiled") {
706
+ throw new Error(`Invalid --emit value: ${mode} (expected manifest|compiled)`);
707
+ }
708
+ parsed.overrides.emit = mode;
709
+ break;
710
+ }
711
+ case "--target-env":
712
+ parsed.overrides.targetEnv = next();
713
+ break;
714
+ case "--fail-on-cycle":
715
+ parsed.failOnCycle = true;
716
+ break;
717
+ case "--strict":
718
+ parsed.strict = true;
719
+ break;
720
+ case "-h":
721
+ case "--help":
722
+ parsed.help = true;
723
+ break;
724
+ default:
725
+ throw new Error(`Unknown option: ${arg}`);
726
+ }
727
+ }
728
+ if (scanDirs.length) parsed.overrides.scanDirs = scanDirs;
729
+ if (include.length) parsed.overrides.include = include;
730
+ if (exclude.length) parsed.overrides.exclude = exclude;
731
+ if (environments.length) parsed.overrides.environments = environments;
732
+ return parsed;
733
+ }
734
+ var HELP = `diadem \u2014 build-time DI manifest generator
735
+
736
+ Usage:
737
+ diadem build [options]
738
+
739
+ Options:
740
+ --scan-dir <dir> Directory to scan (repeatable). Default: src
741
+ --out <file> Output manifest path. Default: src/generated/service-manifest.ts
742
+ --include <regex> Filename include pattern (repeatable). Default: \\.ts$
743
+ --exclude <regex> Filename exclude pattern (repeatable).
744
+ --env <name> Environment to group by (repeatable). Default: development, production, test
745
+ --emit <mode> Output mode: manifest (default) or compiled (straight-line wiring)
746
+ --target-env <name> For --emit=compiled, bake in a single environment
747
+ --cwd <dir> Project root. Default: current directory
748
+ --fail-on-cycle Exit non-zero if a dependency cycle is detected
749
+ --strict Exit non-zero on cycles, ambiguous tokens, or required
750
+ dependencies with no implementing service
751
+ -h, --help Show this help
752
+
753
+ A diadem.config.json in the project root is merged under CLI flags.
754
+ `;
755
+ function main() {
756
+ const args = parseArgs(process.argv.slice(2));
757
+ if (args.help) {
758
+ process.stdout.write(HELP);
759
+ return;
760
+ }
761
+ if (args.command !== "build") {
762
+ process.stderr.write(`Unknown command: ${args.command}
763
+
764
+ ${HELP}`);
765
+ process.exit(1);
766
+ }
767
+ const config = loadConfig(args.cwd, args.overrides);
768
+ const result = generateManifest(config);
769
+ process.stdout.write(
770
+ `diadem: wrote ${result.serviceCount} services to ${result.outFile}
771
+ `
772
+ );
773
+ if (result.externalDependencies > 0) {
774
+ process.stdout.write(
775
+ `diadem: ${result.externalDependencies} external dependencies (not container-managed)
776
+ `
777
+ );
778
+ }
779
+ for (const token of result.duplicateTokens) {
780
+ process.stderr.write(
781
+ `diadem: warning \u2014 token ${token} is declared by more than one service (ambiguous)
782
+ `
783
+ );
784
+ }
785
+ if (result.cycles.length > 0) {
786
+ process.stderr.write(
787
+ `diadem: warning \u2014 dependency cycle(s) detected: ${result.cycles.join(", ")}
788
+ `
789
+ );
790
+ }
791
+ const cycleViolation = result.cycles.length > 0 && (args.strict || args.failOnCycle);
792
+ const strictViolation = args.strict && (result.unresolved.length > 0 || result.duplicateTokens.length > 0);
793
+ if (args.strict) {
794
+ for (const dep of result.unresolved) {
795
+ process.stderr.write(
796
+ `diadem: error \u2014 ${dep.service} requires ${dep.typeName} (${dep.paramName}), but no service implements it
797
+ `
798
+ );
799
+ }
800
+ }
801
+ if (cycleViolation || strictViolation) {
802
+ process.exit(1);
803
+ }
804
+ }
805
+ try {
806
+ main();
807
+ } catch (error) {
808
+ process.stderr.write(
809
+ `diadem: ${error instanceof Error ? error.message : String(error)}
810
+ `
811
+ );
812
+ process.exit(1);
813
+ }
814
+ //# sourceMappingURL=cli.js.map
815
+ //# sourceMappingURL=cli.js.map