@happyvertical/smrt-cli 0.30.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/index.js ADDED
@@ -0,0 +1,1809 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { createInterface } from "node:readline";
5
+ import { fileURLToPath } from "node:url";
6
+ import { ObjectRegistry } from "@happyvertical/smrt-core";
7
+ import { loadLocalTestManifestSync } from "@happyvertical/smrt-core/manifest";
8
+ import { parseCliArgs } from "@happyvertical/utils";
9
+ const __dirname$1 = dirname(fileURLToPath(import.meta.url));
10
+ const packageJson = JSON.parse(
11
+ readFileSync(join(__dirname$1, "../package.json"), "utf-8")
12
+ );
13
+ const CLI_VERSION = packageJson.version;
14
+ function countRequiredArgs(args) {
15
+ if (!args) return 0;
16
+ return args.filter((arg) => !arg.startsWith("[") || !arg.endsWith("]")).length;
17
+ }
18
+ let _gnodeCommands = null;
19
+ let _generateCommands = null;
20
+ let _gitCommands = null;
21
+ let _initCommands = null;
22
+ let _utilityCommands = null;
23
+ let _dispatchCommands = null;
24
+ let _docsCommands = null;
25
+ let _playgroundCommands = null;
26
+ async function getGnodeCommands() {
27
+ if (!_gnodeCommands) {
28
+ const { gnodeCommands } = await import("./index-psX--9zT.js");
29
+ _gnodeCommands = gnodeCommands;
30
+ }
31
+ return _gnodeCommands;
32
+ }
33
+ async function getGitCommands() {
34
+ if (!_gitCommands) {
35
+ const { gitCommands } = await import("./index-psX--9zT.js");
36
+ _gitCommands = gitCommands;
37
+ }
38
+ return _gitCommands;
39
+ }
40
+ async function getGenerateCommands() {
41
+ if (!_generateCommands) {
42
+ const { generateCommands } = await import("./index-psX--9zT.js");
43
+ _generateCommands = generateCommands;
44
+ }
45
+ return _generateCommands;
46
+ }
47
+ async function getInitCommands() {
48
+ if (!_initCommands) {
49
+ const { initCommands } = await import("./index-psX--9zT.js");
50
+ _initCommands = initCommands;
51
+ }
52
+ return _initCommands;
53
+ }
54
+ async function getUtilityCommands() {
55
+ if (!_utilityCommands) {
56
+ const { utilityCommands } = await import("./index-psX--9zT.js");
57
+ _utilityCommands = utilityCommands;
58
+ }
59
+ return _utilityCommands;
60
+ }
61
+ async function getDispatchCommands() {
62
+ if (!_dispatchCommands) {
63
+ const { dispatchCommands } = await import("./index-psX--9zT.js");
64
+ _dispatchCommands = dispatchCommands;
65
+ }
66
+ return _dispatchCommands;
67
+ }
68
+ async function getDocsCommands() {
69
+ if (!_docsCommands) {
70
+ const { docsCommands } = await import("./index-psX--9zT.js");
71
+ _docsCommands = docsCommands;
72
+ }
73
+ return _docsCommands;
74
+ }
75
+ async function getPlaygroundCommands() {
76
+ if (!_playgroundCommands) {
77
+ const { playgroundCommands } = await import("./index-psX--9zT.js");
78
+ _playgroundCommands = playgroundCommands;
79
+ }
80
+ return _playgroundCommands;
81
+ }
82
+ class CLIGenerator {
83
+ config;
84
+ context;
85
+ collections = /* @__PURE__ */ new Map();
86
+ commandCache = null;
87
+ /** Lazy-loaded cache for object commands (key: objectName lowercase) */
88
+ objectCommandsCache = /* @__PURE__ */ new Map();
89
+ /** Set of registered object names (lowercase) for quick lookup */
90
+ registeredObjectNames = null;
91
+ /** Whether manifest/classes have been loaded */
92
+ manifestLoaded = false;
93
+ constructor(config = {}, context = {}) {
94
+ this.config = {
95
+ name: "smrt",
96
+ version: CLI_VERSION,
97
+ description: "Admin CLI for smrt objects",
98
+ prompt: true,
99
+ colors: true,
100
+ ...config
101
+ };
102
+ this.context = context;
103
+ }
104
+ /**
105
+ * Check if running in test environment
106
+ */
107
+ isTestMode() {
108
+ return process.env.NODE_ENV === "test" || process.env.VITEST === "true" || typeof global.it === "function" || typeof global.describe === "function";
109
+ }
110
+ /**
111
+ * Check if a type string represents an inline object type parameter
112
+ * e.g., "{ meetingId?: string; limit?: number }"
113
+ *
114
+ * Note: Only supports single-level nested braces. Deeply nested types
115
+ * like "{ config: { nested: { deep: string } } }" are not fully supported.
116
+ */
117
+ isObjectTypeParameter(typeStr) {
118
+ return typeStr.includes("{") && typeStr.includes("}") && typeStr.includes(":");
119
+ }
120
+ /**
121
+ * Try to load user's compiled classes for runtime execution
122
+ *
123
+ * Loads both local classes (from project entry point) and external classes
124
+ * (from packages in .smrt/manifest.json) to enable full CLI functionality.
125
+ */
126
+ async tryLoadUserClasses() {
127
+ const verbose = process.env.SMRT_VERBOSE === "true" || process.env.DEBUG?.includes("smrt");
128
+ if (verbose) {
129
+ console.log("[CLI] tryLoadUserClasses() called");
130
+ }
131
+ try {
132
+ if (verbose) {
133
+ console.log("[CLI] Loading local classes...");
134
+ }
135
+ await this.loadLocalClasses();
136
+ } catch (localError) {
137
+ if (verbose) {
138
+ console.log(
139
+ "[CLI] Local class loading failed (this is OK if using external packages only):",
140
+ localError instanceof Error ? localError.message : "Unknown error"
141
+ );
142
+ }
143
+ }
144
+ if (verbose) {
145
+ console.log("[CLI] Loading external classes...");
146
+ }
147
+ await this.loadExternalClasses();
148
+ const { getPackageConfig } = await import("@happyvertical/smrt-config");
149
+ const { DEFAULT_CLI_CONFIG } = await import("./config-C8pQD-tk.js");
150
+ const config = getPackageConfig("cli", DEFAULT_CLI_CONFIG);
151
+ const registeredCount = ObjectRegistry.getAllClasses().size;
152
+ if (verbose || config.verbose) {
153
+ console.log(`[CLI] Successfully loaded ${registeredCount} SMRT objects`);
154
+ }
155
+ }
156
+ /**
157
+ * Load classes from local project entry point
158
+ *
159
+ * Entry point discovery order:
160
+ * 1. smrt.config.js: packages.cli.entryPoint (explicit override)
161
+ * 2. package.json: exports['.'] or main field
162
+ * 3. Fallback: ./dist/index.js
163
+ */
164
+ async loadLocalClasses() {
165
+ const { getPackageConfig } = await import("@happyvertical/smrt-config");
166
+ const { DEFAULT_CLI_CONFIG } = await import("./config-C8pQD-tk.js");
167
+ const fs = await import("node:fs");
168
+ const path = await import("node:path");
169
+ const config = getPackageConfig("cli", DEFAULT_CLI_CONFIG);
170
+ let entryPoint = config.entryPoint;
171
+ if (!entryPoint) {
172
+ try {
173
+ const packageJsonPath = path.resolve(process.cwd(), "package.json");
174
+ if (fs.existsSync(packageJsonPath)) {
175
+ const packageJson2 = JSON.parse(
176
+ fs.readFileSync(packageJsonPath, "utf-8")
177
+ );
178
+ entryPoint = packageJson2.exports?.["."]?.import || packageJson2.exports?.["."] || packageJson2.main || "./dist/index.js";
179
+ if (config.verbose) {
180
+ console.log(
181
+ `[CLI] Detected entry point from package.json: ${entryPoint}`
182
+ );
183
+ }
184
+ }
185
+ } catch {
186
+ entryPoint = "./dist/index.js";
187
+ }
188
+ }
189
+ if (!entryPoint) {
190
+ entryPoint = "./dist/index.js";
191
+ }
192
+ const fullPath = path.resolve(process.cwd(), entryPoint);
193
+ if (!fs.existsSync(fullPath)) {
194
+ if (config.verbose) {
195
+ console.log(`[CLI] Entry point not found: ${fullPath}`);
196
+ }
197
+ return;
198
+ }
199
+ if (config.verbose) {
200
+ console.log(`[CLI] Loading local SMRT classes from ${entryPoint}...`);
201
+ }
202
+ const fileUrl = `file://${fullPath}`;
203
+ const importedModule = await import(fileUrl);
204
+ for (const [exportName, exportValue] of Object.entries(importedModule)) {
205
+ if (exportValue && typeof exportValue === "function") {
206
+ const itemClass = exportValue._itemClass;
207
+ if (itemClass) {
208
+ const tableName = itemClass.SMRT_TABLE_NAME || itemClass.name.toLowerCase();
209
+ const existing = ObjectRegistry.getClass(tableName);
210
+ if (existing && !existing.collectionConstructor) {
211
+ ObjectRegistry.registerCollection(tableName, exportValue);
212
+ if (config.verbose) {
213
+ console.log(`[CLI] Registered local collection ${exportName}`);
214
+ }
215
+ }
216
+ }
217
+ }
218
+ }
219
+ }
220
+ /**
221
+ * Load classes from external packages
222
+ *
223
+ * Imports the auto-generated .smrt/register.js file which contains
224
+ * static imports and registrations for all external SMRT objects.
225
+ *
226
+ * This file is generated by the consumer plugin during build.
227
+ */
228
+ async loadExternalClasses() {
229
+ const { getPackageConfig } = await import("@happyvertical/smrt-config");
230
+ const { DEFAULT_CLI_CONFIG } = await import("./config-C8pQD-tk.js");
231
+ const { loadLocalTestManifestSync: loadLocalTestManifestSync2 } = await import("@happyvertical/smrt-core/manifest");
232
+ const fs = await import("node:fs");
233
+ const path = await import("node:path");
234
+ const config = getPackageConfig("cli", DEFAULT_CLI_CONFIG);
235
+ loadLocalTestManifestSync2();
236
+ const registerPath = path.join(process.cwd(), ".smrt", "register.js");
237
+ if (!fs.existsSync(registerPath)) {
238
+ console.log(
239
+ "[CLI] No .smrt/register.js found - custom commands may not work"
240
+ );
241
+ console.log(' Run "npm run build" to generate class registrations');
242
+ return;
243
+ }
244
+ try {
245
+ const fileUrl = `file://${registerPath}`;
246
+ await import(fileUrl);
247
+ if (config.verbose) {
248
+ const count = ObjectRegistry.getAllClasses().size;
249
+ console.log(`[CLI] Loaded ${count} objects from .smrt/register.js`);
250
+ }
251
+ } catch (error) {
252
+ const msg = error instanceof Error ? error.message : "Unknown error";
253
+ console.error(`
254
+ ❌ Failed to load .smrt/register.js: ${msg}`);
255
+ console.error(
256
+ "\nThis usually means an installed package has non-Node.js exports (e.g., .svelte files)."
257
+ );
258
+ console.error("Fix the offending package, then re-run.\n");
259
+ throw error;
260
+ }
261
+ }
262
+ /**
263
+ * Handle exits safely in test mode
264
+ */
265
+ exitWithError(message, code = 1) {
266
+ if (this.isTestMode()) {
267
+ throw new Error(message);
268
+ }
269
+ console.error(message);
270
+ process.exit(code);
271
+ }
272
+ /**
273
+ * Generate CLI handler function
274
+ */
275
+ generateHandler() {
276
+ return async (argv) => {
277
+ const commands = await this.generateCommands();
278
+ const processedArgv = this.preprocessObjectCommands(argv, commands);
279
+ const parsed = parseCliArgs(processedArgv, commands, {});
280
+ await this.executeCommand(parsed, commands, processedArgv);
281
+ };
282
+ }
283
+ /**
284
+ * Preprocess argv to support space-separated object commands
285
+ *
286
+ * Converts: ['council', 'list'] → ['council:list']
287
+ * Converts: ['council', 'get', 'abc-123'] → ['council:get', 'abc-123']
288
+ *
289
+ * This enables users to type:
290
+ * smrt council list
291
+ * smrt council get abc-123
292
+ * smrt council analyze abc-123 --depth 3
293
+ *
294
+ * Instead of:
295
+ * smrt council:list
296
+ * smrt council:get abc-123
297
+ */
298
+ preprocessObjectCommands(argv, commands) {
299
+ if (argv.length < 2) return argv;
300
+ const firstArg = argv[0];
301
+ const secondArg = argv[1];
302
+ if (firstArg.includes(":")) return argv;
303
+ if (firstArg.startsWith("-")) return argv;
304
+ if (secondArg.startsWith("-")) return argv;
305
+ const combinedCommand = `${firstArg}:${secondArg}`;
306
+ const matchesCommand = commands.some(
307
+ (cmd) => cmd.name === combinedCommand || cmd.aliases?.includes(combinedCommand)
308
+ );
309
+ if (matchesCommand) {
310
+ return [combinedCommand, ...argv.slice(2)];
311
+ }
312
+ const knownBuiltInNamespaces = /* @__PURE__ */ new Set([
313
+ "dispatch",
314
+ "docs",
315
+ "git",
316
+ "playground"
317
+ ]);
318
+ if (knownBuiltInNamespaces.has(firstArg)) {
319
+ return [combinedCommand, ...argv.slice(2)];
320
+ }
321
+ const registeredClasses = ObjectRegistry.getAllClasses();
322
+ const isKnownObject = Array.from(registeredClasses.values()).some(
323
+ (info) => (info.name || "").toLowerCase() === firstArg.toLowerCase()
324
+ );
325
+ if (isKnownObject) {
326
+ return [combinedCommand, ...argv.slice(2)];
327
+ }
328
+ return argv;
329
+ }
330
+ /**
331
+ * Ensure manifest and user classes are loaded.
332
+ * This is the minimum required work before any command can be executed.
333
+ * Separated from command generation to enable lazy command loading.
334
+ */
335
+ async ensureManifestLoaded() {
336
+ if (this.manifestLoaded) {
337
+ return;
338
+ }
339
+ const timing = this.context.timing;
340
+ const verbose = process.env.SMRT_VERBOSE === "true" || process.env.DEBUG?.includes("smrt");
341
+ const manifest = loadLocalTestManifestSync();
342
+ if (verbose) {
343
+ console.log(
344
+ "[CLI] Manifest loaded:",
345
+ manifest ? `${Object.keys(manifest.objects || {}).length} objects` : "null"
346
+ );
347
+ }
348
+ if (manifest?.objects) {
349
+ for (const [name, objectDef] of Object.entries(manifest.objects)) {
350
+ ObjectRegistry.registerFromManifest(
351
+ name,
352
+ objectDef,
353
+ manifest.packageName
354
+ );
355
+ }
356
+ }
357
+ const classLoadStart = timing ? performance.now() : 0;
358
+ await this.tryLoadUserClasses();
359
+ if (timing) {
360
+ timing.classLoading = performance.now() - classLoadStart;
361
+ }
362
+ const registeredClasses = ObjectRegistry.getAllClasses();
363
+ this.registeredObjectNames = new Set(
364
+ Array.from(registeredClasses.values()).map(
365
+ (info) => (info.name || "").toLowerCase()
366
+ )
367
+ );
368
+ this.manifestLoaded = true;
369
+ }
370
+ /**
371
+ * Get commands for a specific object (lazy generation with caching)
372
+ */
373
+ async getObjectCommandsLazy(objectName) {
374
+ const lowerName = objectName.toLowerCase();
375
+ const cachedCommands = this.objectCommandsCache.get(lowerName);
376
+ if (cachedCommands) {
377
+ return cachedCommands;
378
+ }
379
+ const registeredClasses = ObjectRegistry.getAllClasses();
380
+ let actualName;
381
+ let matchedKey;
382
+ for (const [key, info] of registeredClasses) {
383
+ if ((info.name || key).toLowerCase() === lowerName) {
384
+ actualName = info.name || key;
385
+ matchedKey = key;
386
+ break;
387
+ }
388
+ }
389
+ if (!actualName || !matchedKey) {
390
+ return [];
391
+ }
392
+ const classInfo = registeredClasses.get(matchedKey);
393
+ const commands = await this.generateObjectCommands(actualName, classInfo);
394
+ this.objectCommandsCache.set(lowerName, commands);
395
+ return commands;
396
+ }
397
+ /**
398
+ * Find an object command by name (lazy lookup)
399
+ */
400
+ async findObjectCommand(commandName) {
401
+ const colonIndex = commandName.indexOf(":");
402
+ if (colonIndex === -1) {
403
+ return void 0;
404
+ }
405
+ const objectName = commandName.slice(0, colonIndex);
406
+ await this.ensureManifestLoaded();
407
+ if (!this.registeredObjectNames?.has(objectName.toLowerCase())) {
408
+ return void 0;
409
+ }
410
+ const objectCommands = await this.getObjectCommandsLazy(objectName);
411
+ return objectCommands.find(
412
+ (cmd) => cmd.name === commandName || cmd.aliases?.includes(commandName)
413
+ );
414
+ }
415
+ /**
416
+ * Generate all CLI commands
417
+ *
418
+ * NOTE: Object commands are now loaded LAZILY for better startup performance.
419
+ * This method only generates utility commands upfront. Object commands are
420
+ * generated on-demand when executeCommand() looks for them.
421
+ *
422
+ * For full command list (e.g., help display), use generateAllCommands().
423
+ */
424
+ async generateCommands() {
425
+ const timing = this.context.timing;
426
+ const verbose = process.env.SMRT_VERBOSE === "true" || process.env.DEBUG?.includes("smrt");
427
+ if (this.commandCache) {
428
+ if (verbose) {
429
+ console.log("[CLI] generateCommands() returning cached commands");
430
+ }
431
+ return this.commandCache;
432
+ }
433
+ if (verbose) {
434
+ console.log("[CLI] generateCommands() starting (lazy mode)");
435
+ }
436
+ await this.ensureManifestLoaded();
437
+ const commandGenStart = timing ? performance.now() : 0;
438
+ const commands = [];
439
+ const commandNames = /* @__PURE__ */ new Set();
440
+ for (const cmd of this.generateUtilityCommands()) {
441
+ if (commandNames.has(cmd.name)) {
442
+ if (verbose) {
443
+ console.warn(`[CLI] Skipping duplicate utility command: ${cmd.name}`);
444
+ }
445
+ continue;
446
+ }
447
+ commandNames.add(cmd.name);
448
+ commands.push(cmd);
449
+ }
450
+ if (timing) {
451
+ timing.commandGen = performance.now() - commandGenStart;
452
+ }
453
+ this.commandCache = commands;
454
+ return commands;
455
+ }
456
+ /**
457
+ * Generate ALL commands including lazy-loaded object commands.
458
+ * Used for help display where we need the complete list.
459
+ */
460
+ async generateAllCommands() {
461
+ const verbose = process.env.SMRT_VERBOSE === "true" || process.env.DEBUG?.includes("smrt");
462
+ if (verbose) {
463
+ console.log("[CLI] generateAllCommands() - loading all object commands");
464
+ }
465
+ await this.ensureManifestLoaded();
466
+ const allCommands = [];
467
+ const commandNames = /* @__PURE__ */ new Set();
468
+ for (const cmd of this.generateUtilityCommands()) {
469
+ if (!commandNames.has(cmd.name)) {
470
+ commandNames.add(cmd.name);
471
+ allCommands.push(cmd);
472
+ }
473
+ }
474
+ const registeredClasses = ObjectRegistry.getAllClasses();
475
+ for (const [_key, classInfo] of registeredClasses) {
476
+ const objectCommands = await this.getObjectCommandsLazy(
477
+ classInfo.name || _key
478
+ );
479
+ for (const cmd of objectCommands) {
480
+ if (!commandNames.has(cmd.name)) {
481
+ commandNames.add(cmd.name);
482
+ allCommands.push(cmd);
483
+ }
484
+ }
485
+ }
486
+ return allCommands;
487
+ }
488
+ /**
489
+ * Generate CRUD commands for a specific object
490
+ */
491
+ async generateObjectCommands(objectName, _classInfo) {
492
+ const commands = [];
493
+ const lowerName = objectName.toLowerCase();
494
+ const config = ObjectRegistry.getConfig(objectName);
495
+ const cliConfig = config.cli;
496
+ if (cliConfig === false) return commands;
497
+ const excluded = (typeof cliConfig === "object" ? cliConfig.exclude : []) || [];
498
+ const included = typeof cliConfig === "object" ? cliConfig.include : null;
499
+ const shouldInclude = (command) => {
500
+ if (included && !included.includes(command)) return false;
501
+ if (excluded.includes(command)) return false;
502
+ return true;
503
+ };
504
+ if (shouldInclude("list")) {
505
+ commands.push({
506
+ name: `${lowerName}:list`,
507
+ description: `List ${objectName} objects`,
508
+ aliases: [`${lowerName}:ls`],
509
+ options: {
510
+ limit: {
511
+ type: "string",
512
+ description: "limit number of results",
513
+ default: "50",
514
+ short: "l"
515
+ },
516
+ offset: {
517
+ type: "string",
518
+ description: "offset for pagination",
519
+ default: "0",
520
+ short: "o"
521
+ },
522
+ "order-by": { type: "string", description: "field to order by" },
523
+ where: { type: "string", description: "filter conditions as JSON" },
524
+ format: {
525
+ type: "string",
526
+ description: "output format (table|json)",
527
+ default: "table"
528
+ }
529
+ },
530
+ handler: async (_args, options) => {
531
+ await this.handleList(objectName, options);
532
+ }
533
+ });
534
+ }
535
+ if (shouldInclude("get")) {
536
+ commands.push({
537
+ name: `${lowerName}:get`,
538
+ description: `Get ${objectName} by ID or slug`,
539
+ aliases: [`${lowerName}:show`],
540
+ args: ["id"],
541
+ options: {
542
+ format: {
543
+ type: "string",
544
+ description: "output format (json|yaml)",
545
+ default: "json"
546
+ }
547
+ },
548
+ handler: async (args, options) => {
549
+ await this.handleGet(objectName, args[0], options);
550
+ }
551
+ });
552
+ }
553
+ if (shouldInclude("create")) {
554
+ const options = {
555
+ interactive: {
556
+ type: "boolean",
557
+ description: "interactive mode with prompts"
558
+ },
559
+ "from-file": { type: "string", description: "create from JSON file" }
560
+ };
561
+ const fields = ObjectRegistry.getFields(objectName);
562
+ for (const [fieldName, field] of fields) {
563
+ const optionName = fieldName.replace(/_/g, "-");
564
+ const description = field.options?.description || `${objectName} ${fieldName}`;
565
+ options[optionName] = { type: "string", description };
566
+ }
567
+ commands.push({
568
+ name: `${lowerName}:create`,
569
+ description: `Create new ${objectName}`,
570
+ aliases: [`${lowerName}:new`],
571
+ options,
572
+ handler: async (_args, options2) => {
573
+ await this.handleCreate(objectName, options2);
574
+ }
575
+ });
576
+ }
577
+ if (shouldInclude("update")) {
578
+ const options = {
579
+ interactive: {
580
+ type: "boolean",
581
+ description: "interactive mode with prompts"
582
+ },
583
+ "from-file": { type: "string", description: "update from JSON file" }
584
+ };
585
+ const fields = ObjectRegistry.getFields(objectName);
586
+ for (const [fieldName, field] of fields) {
587
+ const optionName = fieldName.replace(/_/g, "-");
588
+ const description = field.options?.description || `${objectName} ${fieldName}`;
589
+ options[optionName] = { type: "string", description };
590
+ }
591
+ commands.push({
592
+ name: `${lowerName}:update`,
593
+ description: `Update ${objectName}`,
594
+ aliases: [`${lowerName}:edit`],
595
+ args: ["id"],
596
+ options,
597
+ handler: async (args, options2) => {
598
+ await this.handleUpdate(objectName, args[0], options2);
599
+ }
600
+ });
601
+ }
602
+ if (shouldInclude("delete")) {
603
+ commands.push({
604
+ name: `${lowerName}:delete`,
605
+ description: `Delete ${objectName}`,
606
+ aliases: [`${lowerName}:rm`],
607
+ args: ["id"],
608
+ options: {
609
+ force: { type: "boolean", description: "skip confirmation prompt" }
610
+ },
611
+ handler: async (args, options) => {
612
+ await this.handleDelete(objectName, args[0], options);
613
+ }
614
+ });
615
+ }
616
+ const methods = await ObjectRegistry.getAllMethods(objectName);
617
+ const crudOperations = ["list", "get", "create", "update", "delete"];
618
+ const hasCustomMethodsInInclude = included?.some(
619
+ (item) => !crudOperations.includes(item)
620
+ );
621
+ for (const [methodName, methodDef] of methods) {
622
+ const shouldIncludeMethod = () => {
623
+ if (!methodDef.isPublic) return false;
624
+ if (excluded.includes(methodName)) return false;
625
+ if (hasCustomMethodsInInclude && included && !included.includes(methodName)) {
626
+ return false;
627
+ }
628
+ return true;
629
+ };
630
+ if (!shouldIncludeMethod()) continue;
631
+ const methodOptions = {
632
+ json: {
633
+ type: "boolean",
634
+ description: "Output as JSON only (suppress other output)"
635
+ }
636
+ };
637
+ for (const param of methodDef.parameters || []) {
638
+ const typeStr = param.type || "";
639
+ const isObjectType = this.isObjectTypeParameter(typeStr);
640
+ if (isObjectType) {
641
+ const match = typeStr.match(/\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}/);
642
+ if (match) {
643
+ const propsStr = match[1];
644
+ const propMatches = propsStr.matchAll(/(\w+)(\?)?:\s*([^;]+)/g);
645
+ for (const propMatch of propMatches) {
646
+ const [, propName, isOptional, propType] = propMatch;
647
+ const optionName = propName.replace(/([A-Z])/g, "-$1").toLowerCase();
648
+ if (propType.includes("import(")) {
649
+ methodOptions[optionName] = {
650
+ type: "string",
651
+ description: `JSON object${isOptional ? " (optional)" : ""}`
652
+ };
653
+ } else {
654
+ const trimmedType = propType.trim();
655
+ methodOptions[optionName] = {
656
+ type: trimmedType === "boolean" ? "boolean" : "string",
657
+ description: `${trimmedType}${isOptional ? " (optional)" : ""}`
658
+ };
659
+ }
660
+ }
661
+ }
662
+ } else {
663
+ const optionName = param.name.replace(/([A-Z])/g, "-$1").toLowerCase();
664
+ const paramType = (param.type || "").trim();
665
+ methodOptions[optionName] = {
666
+ type: paramType === "boolean" ? "boolean" : "string",
667
+ description: `${param.type}${param.optional ? " (optional)" : ""}`,
668
+ ...param.default !== void 0 && {
669
+ default: String(param.default)
670
+ }
671
+ };
672
+ }
673
+ }
674
+ const firstParam = (methodDef.parameters || [])[0];
675
+ const needsInstance = firstParam?.name === "id";
676
+ commands.push({
677
+ name: `${lowerName}:${methodName}`,
678
+ description: methodDef.description || `Execute ${methodName} on ${objectName}`,
679
+ args: needsInstance ? ["id"] : [],
680
+ // Only require ID if method has required parameters
681
+ options: methodOptions,
682
+ handler: async (args, options) => {
683
+ if (needsInstance) {
684
+ await this.handleCustomMethod(
685
+ objectName,
686
+ args[0],
687
+ methodName,
688
+ options
689
+ );
690
+ } else {
691
+ await this.handleSingletonMethod(
692
+ objectName,
693
+ methodName,
694
+ options,
695
+ methodDef
696
+ );
697
+ }
698
+ }
699
+ });
700
+ }
701
+ return commands;
702
+ }
703
+ /**
704
+ * Execute a parsed command
705
+ */
706
+ async executeCommand(parsed, commands, processedArgv = process.argv.slice(2)) {
707
+ if (!parsed.command) {
708
+ const allCommands = await this.generateAllCommands();
709
+ await this.showHelp(allCommands);
710
+ return;
711
+ }
712
+ let command = commands.find(
713
+ (cmd) => cmd.name === parsed.command || parsed.command && cmd.aliases && cmd.aliases.includes(parsed.command)
714
+ );
715
+ if (!command && parsed.command) {
716
+ command = await this.findObjectCommand(parsed.command);
717
+ }
718
+ if (command) {
719
+ const requiredArgCount = countRequiredArgs(command.args);
720
+ if (parsed.args.length < requiredArgCount) {
721
+ const missingArgs = command.args?.slice(parsed.args.length).filter((arg) => !arg.startsWith("[") || !arg.endsWith("]"));
722
+ this.exitWithError(
723
+ `Missing required arguments: ${missingArgs?.join(", ") || ""}`
724
+ );
725
+ return;
726
+ }
727
+ if (!command.handler) {
728
+ this.exitWithError(
729
+ `Command '${parsed.command}' has no handler defined`
730
+ );
731
+ return;
732
+ }
733
+ try {
734
+ await command.handler(parsed.args, parsed.options);
735
+ return;
736
+ } catch (error) {
737
+ this.exitWithError(
738
+ `Error: ${error instanceof Error ? error.message : "Unknown error"}`
739
+ );
740
+ return;
741
+ }
742
+ }
743
+ const [
744
+ gnodeCommands,
745
+ generateCommands,
746
+ gitCommands,
747
+ initCommands,
748
+ utilityCommands,
749
+ dispatchCommands,
750
+ docsCommands,
751
+ playgroundCommands
752
+ ] = await Promise.all([
753
+ getGnodeCommands(),
754
+ getGenerateCommands(),
755
+ getGitCommands(),
756
+ getInitCommands(),
757
+ getUtilityCommands(),
758
+ getDispatchCommands(),
759
+ getDocsCommands(),
760
+ getPlaygroundCommands()
761
+ ]);
762
+ const builtInCommands = {
763
+ ...gnodeCommands,
764
+ ...generateCommands,
765
+ ...gitCommands,
766
+ ...initCommands,
767
+ ...utilityCommands,
768
+ ...dispatchCommands,
769
+ ...docsCommands,
770
+ ...playgroundCommands
771
+ };
772
+ const builtInCommand = builtInCommands[parsed.command] ?? Object.values(builtInCommands).find(
773
+ (cmd) => cmd.name === parsed.command || cmd.aliases?.includes(parsed.command ?? "")
774
+ );
775
+ if (builtInCommand) {
776
+ const requiredArgCount = countRequiredArgs(builtInCommand.args);
777
+ if (parsed.args.length < requiredArgCount) {
778
+ const missingArgs = builtInCommand.args?.slice(parsed.args.length).filter((arg) => !arg.startsWith("[") || !arg.endsWith("]"));
779
+ this.exitWithError(
780
+ `Missing required arguments: ${missingArgs?.join(", ") || ""}`
781
+ );
782
+ return;
783
+ }
784
+ if (!builtInCommand.handler) {
785
+ this.exitWithError(
786
+ `Command '${parsed.command}' has no handler defined`
787
+ );
788
+ return;
789
+ }
790
+ const reParsed = parseCliArgs(processedArgv, [builtInCommand], {});
791
+ try {
792
+ await builtInCommand.handler(reParsed.args, reParsed.options);
793
+ return;
794
+ } catch (error) {
795
+ this.exitWithError(
796
+ `Error: ${error instanceof Error ? error.message : "Unknown error"}`
797
+ );
798
+ return;
799
+ }
800
+ }
801
+ await this.ensureManifestLoaded();
802
+ const registeredClasses = ObjectRegistry.getAllClasses();
803
+ let matchingObject;
804
+ for (const [_key, info] of registeredClasses) {
805
+ if ((info.name || _key).toLowerCase() === parsed.command?.toLowerCase()) {
806
+ matchingObject = info.name || _key;
807
+ break;
808
+ }
809
+ }
810
+ if (matchingObject) {
811
+ const objectCommands = await this.getObjectCommandsLazy(matchingObject);
812
+ await this.showObjectHelp(matchingObject, objectCommands);
813
+ return;
814
+ }
815
+ this.exitWithError(`Unknown command '${parsed.command}'`);
816
+ }
817
+ /**
818
+ * Show help for a specific object and its available commands
819
+ */
820
+ async showObjectHelp(objectName, objectCommands) {
821
+ const classInfo = ObjectRegistry.getClass(objectName);
822
+ const lowerName = objectName.toLowerCase();
823
+ console.log(`
824
+ ${objectName}`);
825
+ console.log("=".repeat(objectName.length));
826
+ if (classInfo?.packageName) {
827
+ console.log(`Package: ${classInfo.packageName}`);
828
+ }
829
+ if (objectCommands.length === 0) {
830
+ console.log("\nNo CLI commands available for this object.");
831
+ console.log(
832
+ "Check that cli: true or cli: { include: [...] } is set in @smrt() decorator."
833
+ );
834
+ return;
835
+ }
836
+ console.log("\nAvailable commands:");
837
+ for (const cmd of objectCommands) {
838
+ const cmdName = cmd.name.replace(`${lowerName}:`, "");
839
+ const args = cmd.args ? ` ${cmd.args.map((arg) => `<${arg}>`).join(" ")}` : "";
840
+ console.log(` smrt ${lowerName} ${cmdName}${args}`);
841
+ console.log(` ${cmd.description}`);
842
+ if (cmd.options && Object.keys(cmd.options).length > 0) {
843
+ for (const [optName, opt] of Object.entries(cmd.options)) {
844
+ const short = opt.short ? `-${opt.short}, ` : "";
845
+ const def = opt.default ? ` (default: ${opt.default})` : "";
846
+ console.log(` ${short}--${optName}${def}`);
847
+ }
848
+ }
849
+ console.log();
850
+ }
851
+ const fields = ObjectRegistry.getFields(objectName);
852
+ if (fields.size > 0) {
853
+ console.log("Fields:");
854
+ for (const [fieldName, field] of fields) {
855
+ const required = field.options?.required ? " (required)" : "";
856
+ console.log(` ${fieldName}: ${field.type}${required}`);
857
+ }
858
+ }
859
+ }
860
+ /**
861
+ * Generate utility commands
862
+ */
863
+ generateUtilityCommands() {
864
+ const commands = [];
865
+ commands.push({
866
+ name: "objects",
867
+ description: "List all registered smrt objects",
868
+ aliases: ["ls"],
869
+ options: {
870
+ verbose: {
871
+ type: "boolean",
872
+ description: "Show detailed output including methods",
873
+ short: "v"
874
+ },
875
+ json: {
876
+ type: "boolean",
877
+ description: "Output as JSON"
878
+ }
879
+ },
880
+ handler: async (_args, options) => {
881
+ const registeredClasses = ObjectRegistry.getAllClasses();
882
+ if (registeredClasses.size === 0) {
883
+ console.log("No SMRT objects found.");
884
+ console.log("\nTo discover objects:");
885
+ console.log(" • Build your project: npm run build");
886
+ console.log(" • Ensure .smrt/register.js exists");
887
+ console.log(" • Run: smrt introspect for details");
888
+ return;
889
+ }
890
+ if (options.json) {
891
+ const output = {};
892
+ for (const [key, classInfo] of registeredClasses) {
893
+ const displayName = classInfo.name || key;
894
+ const config = ObjectRegistry.getConfig(key);
895
+ const fields = ObjectRegistry.getFields(key);
896
+ const methods = await ObjectRegistry.getAllMethods(key);
897
+ output[classInfo.qualifiedName || displayName] = {
898
+ name: displayName,
899
+ package: classInfo.packageName || "project",
900
+ hasConstructor: !!classInfo.constructor,
901
+ hasCollection: !!classInfo.collectionConstructor,
902
+ config,
903
+ fields: Object.fromEntries(fields),
904
+ methods: Object.fromEntries(methods)
905
+ };
906
+ }
907
+ console.log(JSON.stringify(output, null, 2));
908
+ return;
909
+ }
910
+ console.log("Registered SMRT objects:\n");
911
+ const byPackage = /* @__PURE__ */ new Map();
912
+ for (const [key, classInfo] of registeredClasses) {
913
+ const pkg = classInfo.packageName || "project";
914
+ if (!byPackage.has(pkg)) {
915
+ byPackage.set(pkg, []);
916
+ }
917
+ byPackage.get(pkg)?.push({ display: classInfo.name || key, key });
918
+ }
919
+ for (const [pkg, entries] of byPackage) {
920
+ console.log(` ${pkg}:`);
921
+ for (const { display: objName, key: objKey } of entries) {
922
+ const config = ObjectRegistry.getConfig(objKey);
923
+ const cliConfig = config.cli;
924
+ let cliMethods = [];
925
+ if (cliConfig) {
926
+ const methods = await ObjectRegistry.getAllMethods(objKey);
927
+ const methodNames = Array.from(methods.keys());
928
+ if (typeof cliConfig === "object" && cliConfig.include) {
929
+ cliMethods = methodNames.filter(
930
+ (m) => cliConfig.include?.includes(m) && methods.get(m)?.isPublic
931
+ );
932
+ } else if (cliConfig === true) {
933
+ cliMethods = methodNames.filter(
934
+ (m) => methods.get(m)?.isPublic
935
+ );
936
+ }
937
+ }
938
+ if (options.verbose) {
939
+ console.log(` • ${objName}`);
940
+ if (cliMethods.length > 0) {
941
+ console.log(` CLI: ${cliMethods.join(", ")}`);
942
+ }
943
+ const fields = ObjectRegistry.getFields(objKey);
944
+ const fieldNames = Array.from(fields.keys()).slice(0, 5);
945
+ if (fieldNames.length > 0) {
946
+ console.log(
947
+ ` Fields: ${fieldNames.join(", ")}${fields.size > 5 ? "..." : ""}`
948
+ );
949
+ }
950
+ } else {
951
+ const methodStr = cliMethods.length > 0 ? ` → ${cliMethods.join(", ")}` : "";
952
+ console.log(` • ${objName}${methodStr}`);
953
+ }
954
+ }
955
+ console.log();
956
+ }
957
+ console.log(
958
+ `Total: ${registeredClasses.size} objects from ${byPackage.size} source(s)`
959
+ );
960
+ }
961
+ });
962
+ commands.push({
963
+ name: "schema",
964
+ description: "Show schema for an object",
965
+ args: ["object"],
966
+ handler: this.createSchemaHandler()
967
+ });
968
+ commands.push({
969
+ name: "help",
970
+ description: "Show help information",
971
+ aliases: ["h"],
972
+ handler: async (_args, _options) => {
973
+ await this.showHelp(commands);
974
+ }
975
+ });
976
+ commands.push({
977
+ name: "version",
978
+ description: "Show version information",
979
+ aliases: ["v"],
980
+ handler: async (_args, _options) => {
981
+ console.log(`${this.config.name} v${this.config.version}`);
982
+ }
983
+ });
984
+ commands.push({
985
+ name: "status",
986
+ description: "Show system status",
987
+ handler: async (_args, _options) => {
988
+ console.log("System Status:");
989
+ console.log(`- CLI: ${this.config.name} v${this.config.version}`);
990
+ console.log(
991
+ `- Database: ${this.context.db ? "Connected" : "Not connected"}`
992
+ );
993
+ console.log(`- AI: ${this.context.ai ? "Available" : "Not available"}`);
994
+ console.log(`- User: ${this.context.user?.id || "Not authenticated"}`);
995
+ }
996
+ });
997
+ return commands;
998
+ }
999
+ /**
1000
+ * Create schema command handler
1001
+ */
1002
+ createSchemaHandler() {
1003
+ return async (args, _options) => {
1004
+ const objectName = args[0];
1005
+ const fields = ObjectRegistry.getFields(objectName);
1006
+ if (fields.size === 0) {
1007
+ this.exitWithError(`Object ${objectName} not found`);
1008
+ return;
1009
+ }
1010
+ console.log(`Schema for ${objectName}:`);
1011
+ for (const [fieldName, field] of fields) {
1012
+ console.log(
1013
+ ` ${fieldName}: ${field.type}${field.options?.required ? " (required)" : ""}`
1014
+ );
1015
+ if (field.options?.description) {
1016
+ console.log(` ${field.options.description}`);
1017
+ }
1018
+ }
1019
+ };
1020
+ }
1021
+ /**
1022
+ * Show help information
1023
+ */
1024
+ async showHelp(commands) {
1025
+ console.log(`${this.config.name} v${this.config.version}`);
1026
+ console.log(this.config.description);
1027
+ console.log();
1028
+ const [
1029
+ gnodeCommands,
1030
+ generateCommands,
1031
+ gitCommands,
1032
+ initCommands,
1033
+ utilityCommands,
1034
+ dispatchCommands,
1035
+ docsCommands,
1036
+ playgroundCommands
1037
+ ] = await Promise.all([
1038
+ getGnodeCommands(),
1039
+ getGenerateCommands(),
1040
+ getGitCommands(),
1041
+ getInitCommands(),
1042
+ getUtilityCommands(),
1043
+ getDispatchCommands(),
1044
+ getDocsCommands(),
1045
+ getPlaygroundCommands()
1046
+ ]);
1047
+ console.log("Project Setup:");
1048
+ for (const command of Object.values(initCommands)) {
1049
+ this.showCommandHelp(command);
1050
+ }
1051
+ console.log();
1052
+ console.log("Playground:");
1053
+ for (const command of Object.values(playgroundCommands)) {
1054
+ this.showCommandHelp(command);
1055
+ }
1056
+ console.log();
1057
+ console.log("Utility Commands:");
1058
+ for (const command of Object.values(utilityCommands)) {
1059
+ this.showCommandHelp(command);
1060
+ }
1061
+ console.log();
1062
+ console.log("Dispatch (Inter-Agent Communication):");
1063
+ for (const command of Object.values(dispatchCommands)) {
1064
+ this.showCommandHelp(command);
1065
+ }
1066
+ console.log();
1067
+ console.log("Git Integration:");
1068
+ for (const command of Object.values(gitCommands)) {
1069
+ this.showCommandHelp(command);
1070
+ }
1071
+ console.log();
1072
+ console.log("Gnode Commands:");
1073
+ for (const command of Object.values(gnodeCommands)) {
1074
+ this.showCommandHelp(command);
1075
+ }
1076
+ console.log("Code Generation:");
1077
+ for (const command of Object.values(generateCommands)) {
1078
+ this.showCommandHelp(command);
1079
+ }
1080
+ console.log("Documentation:");
1081
+ for (const command of Object.values(docsCommands)) {
1082
+ this.showCommandHelp(command);
1083
+ }
1084
+ console.log();
1085
+ const builtInUtilityCommands = commands.filter(
1086
+ (cmd) => cmd.name === "objects" || cmd.name === "schema" || cmd.name === "help" || cmd.name === "version" || cmd.name === "status"
1087
+ );
1088
+ if (builtInUtilityCommands.length > 0) {
1089
+ console.log("Object Utilities:");
1090
+ for (const command of builtInUtilityCommands) {
1091
+ this.showCommandHelp(command);
1092
+ }
1093
+ }
1094
+ const objectCommands = commands.filter(
1095
+ (cmd) => !builtInUtilityCommands.includes(cmd)
1096
+ );
1097
+ if (objectCommands.length > 0) {
1098
+ console.log("Object Commands (auto-generated):");
1099
+ for (const command of objectCommands) {
1100
+ this.showCommandHelp(command);
1101
+ }
1102
+ }
1103
+ }
1104
+ /**
1105
+ * Show help for a single command
1106
+ */
1107
+ showCommandHelp(command) {
1108
+ const aliases = command.aliases ? ` (${command.aliases.join(", ")})` : "";
1109
+ const args = command.args ? ` ${command.args.map((arg) => `<${arg}>`).join(" ")}` : "";
1110
+ console.log(` ${command.name}${args}${aliases}`);
1111
+ console.log(` ${command.description}`);
1112
+ if (command.options) {
1113
+ for (const [name, option] of Object.entries(command.options)) {
1114
+ const short = option.short ? `-${option.short}, ` : "";
1115
+ console.log(` ${short}--${name}: ${option.description}`);
1116
+ }
1117
+ }
1118
+ console.log();
1119
+ }
1120
+ /**
1121
+ * Create a simple spinner
1122
+ */
1123
+ createSpinner(text) {
1124
+ const isTTY = process.stdout.isTTY && typeof process.stdout.clearLine === "function" && typeof process.stdout.cursorTo === "function";
1125
+ if (this.config.colors && isTTY) {
1126
+ process.stdout.write(`⠋ ${text}`);
1127
+ return {
1128
+ succeed: (successText) => {
1129
+ process.stdout.clearLine(0);
1130
+ process.stdout.cursorTo(0);
1131
+ console.log(`✅ ${successText || text}`);
1132
+ },
1133
+ fail: (errorText) => {
1134
+ process.stdout.clearLine(0);
1135
+ process.stdout.cursorTo(0);
1136
+ console.log(`❌ ${errorText || text}`);
1137
+ }
1138
+ };
1139
+ }
1140
+ console.log(text);
1141
+ return {
1142
+ succeed: (successText) => console.log(successText || "Done"),
1143
+ fail: (errorText) => console.log(errorText || "Failed")
1144
+ };
1145
+ }
1146
+ /**
1147
+ * Prompt for input
1148
+ */
1149
+ async prompt(message) {
1150
+ const rl = createInterface({
1151
+ input: process.stdin,
1152
+ output: process.stdout
1153
+ });
1154
+ return new Promise((resolve) => {
1155
+ rl.question(`${message} `, (answer) => {
1156
+ rl.close();
1157
+ resolve(answer);
1158
+ });
1159
+ });
1160
+ }
1161
+ /**
1162
+ * Confirm prompt
1163
+ */
1164
+ async confirm(message) {
1165
+ const answer = await this.prompt(`${message} (y/n)`);
1166
+ return answer.toLowerCase().startsWith("y");
1167
+ }
1168
+ /**
1169
+ * Handle LIST command
1170
+ */
1171
+ async handleList(objectName, options) {
1172
+ const spinner = this.createSpinner(`Listing ${objectName} objects...`);
1173
+ try {
1174
+ const collection = await this.getCollection(objectName);
1175
+ const listOptions = {
1176
+ limit: Number.parseInt(options.limit, 10),
1177
+ offset: Number.parseInt(options.offset, 10)
1178
+ };
1179
+ const orderBy = options["order-by"] ?? options.orderBy;
1180
+ if (orderBy) {
1181
+ listOptions.orderBy = orderBy;
1182
+ }
1183
+ if (options.where) {
1184
+ listOptions.where = JSON.parse(options.where);
1185
+ }
1186
+ const results = await collection.list(listOptions);
1187
+ spinner.succeed(`Found ${results.length} ${objectName} objects`);
1188
+ if (options.format === "json") {
1189
+ console.log(JSON.stringify(results, null, 2));
1190
+ } else {
1191
+ this.displayTable(results, objectName);
1192
+ }
1193
+ } catch (error) {
1194
+ spinner.fail(`Failed to list ${objectName} objects`);
1195
+ this.exitWithError(
1196
+ error instanceof Error ? error.message : "Unknown error"
1197
+ );
1198
+ }
1199
+ }
1200
+ /**
1201
+ * Handle GET command
1202
+ */
1203
+ async handleGet(objectName, id, options) {
1204
+ const spinner = this.createSpinner(`Getting ${objectName}...`);
1205
+ try {
1206
+ const collection = await this.getCollection(objectName);
1207
+ const result = await collection.get(id);
1208
+ if (!result) {
1209
+ spinner.fail(`${objectName} not found`);
1210
+ this.exitWithError(`${objectName} not found`);
1211
+ return;
1212
+ }
1213
+ spinner.succeed(`Found ${objectName}`);
1214
+ if (options.format === "yaml") {
1215
+ console.log(this.toYamlString(result));
1216
+ } else {
1217
+ console.log(JSON.stringify(result, null, 2));
1218
+ }
1219
+ } catch (error) {
1220
+ spinner.fail(`Failed to get ${objectName}`);
1221
+ this.exitWithError(
1222
+ error instanceof Error ? error.message : "Unknown error"
1223
+ );
1224
+ }
1225
+ }
1226
+ /**
1227
+ * Handle CREATE command
1228
+ */
1229
+ async handleCreate(objectName, options) {
1230
+ try {
1231
+ let data = {};
1232
+ const fromFile = options["from-file"] ?? options.fromFile;
1233
+ if (fromFile) {
1234
+ const fs = await import("node:fs/promises");
1235
+ const content = await fs.readFile(fromFile, "utf-8");
1236
+ data = JSON.parse(content);
1237
+ } else if (options.interactive && this.config.prompt) {
1238
+ data = await this.promptForFields(objectName, {});
1239
+ } else {
1240
+ const fields = ObjectRegistry.getFields(objectName);
1241
+ for (const [fieldName] of fields) {
1242
+ const optionName = fieldName.replace(/_/g, "-");
1243
+ if (options[optionName] !== void 0) {
1244
+ data[fieldName] = this.parseFieldValue(options[optionName]);
1245
+ }
1246
+ }
1247
+ }
1248
+ const spinner = this.createSpinner(`Creating ${objectName}...`);
1249
+ const collection = await this.getCollection(objectName);
1250
+ const result = await collection.create(data);
1251
+ await result.save();
1252
+ spinner.succeed(`Created ${objectName} with ID: ${result.id}`);
1253
+ if (!options.quiet) {
1254
+ console.log(JSON.stringify(result, null, 2));
1255
+ }
1256
+ } catch (error) {
1257
+ this.exitWithError(
1258
+ error instanceof Error ? error.message : "Unknown error"
1259
+ );
1260
+ }
1261
+ }
1262
+ /**
1263
+ * Handle UPDATE command
1264
+ */
1265
+ async handleUpdate(objectName, id, options) {
1266
+ try {
1267
+ const collection = await this.getCollection(objectName);
1268
+ const existing = await collection.get(id);
1269
+ if (!existing) {
1270
+ this.exitWithError(`${objectName} not found`);
1271
+ return;
1272
+ }
1273
+ let data = {};
1274
+ const fromFile = options["from-file"] ?? options.fromFile;
1275
+ if (fromFile) {
1276
+ const fs = await import("node:fs/promises");
1277
+ const content = await fs.readFile(fromFile, "utf-8");
1278
+ data = JSON.parse(content);
1279
+ } else if (options.interactive && this.config.prompt) {
1280
+ data = await this.promptForFields(objectName, existing);
1281
+ } else {
1282
+ const fields = ObjectRegistry.getFields(objectName);
1283
+ for (const [fieldName] of fields) {
1284
+ const optionName = fieldName.replace(/_/g, "-");
1285
+ if (options[optionName] !== void 0) {
1286
+ data[fieldName] = this.parseFieldValue(options[optionName]);
1287
+ }
1288
+ }
1289
+ }
1290
+ const spinner = this.createSpinner(`Updating ${objectName}...`);
1291
+ Object.assign(existing, data);
1292
+ await existing.save();
1293
+ spinner.succeed(`Updated ${objectName}`);
1294
+ if (!options.quiet) {
1295
+ console.log(JSON.stringify(existing, null, 2));
1296
+ }
1297
+ } catch (error) {
1298
+ this.exitWithError(
1299
+ error instanceof Error ? error.message : "Unknown error"
1300
+ );
1301
+ }
1302
+ }
1303
+ /**
1304
+ * Handle DELETE command
1305
+ */
1306
+ async handleDelete(objectName, id, options) {
1307
+ try {
1308
+ const collection = await this.getCollection(objectName);
1309
+ const existing = await collection.get(id);
1310
+ if (!existing) {
1311
+ this.exitWithError(`${objectName} not found`);
1312
+ return;
1313
+ }
1314
+ if (!options.force && this.config.prompt) {
1315
+ const confirmed = await this.confirm(
1316
+ `Are you sure you want to delete ${objectName} "${existing.name || existing.slug || existing.id}"?`
1317
+ );
1318
+ if (!confirmed) {
1319
+ console.log("Cancelled");
1320
+ return;
1321
+ }
1322
+ }
1323
+ const spinner = this.createSpinner(`Deleting ${objectName}...`);
1324
+ await existing.delete();
1325
+ spinner.succeed(`Deleted ${objectName}`);
1326
+ } catch (error) {
1327
+ this.exitWithError(
1328
+ error instanceof Error ? error.message : "Unknown error"
1329
+ );
1330
+ }
1331
+ }
1332
+ /**
1333
+ * Handle custom method execution
1334
+ */
1335
+ async handleCustomMethod(objectName, id, methodName, options) {
1336
+ try {
1337
+ const collection = await this.getCollection(objectName);
1338
+ const obj = await collection.get(id);
1339
+ if (!obj) {
1340
+ this.exitWithError(`${objectName} not found`);
1341
+ return;
1342
+ }
1343
+ const methods = await ObjectRegistry.getAllMethods(objectName);
1344
+ const methodDef = methods.get(methodName);
1345
+ if (!methodDef) {
1346
+ this.exitWithError(`Method ${methodName} not found on ${objectName}`);
1347
+ return;
1348
+ }
1349
+ const jsonMode = options.json === true;
1350
+ const spinner = jsonMode ? {
1351
+ succeed: (_text) => {
1352
+ },
1353
+ fail: (msg) => {
1354
+ if (msg) {
1355
+ console.error(msg);
1356
+ }
1357
+ }
1358
+ } : this.createSpinner(`Executing ${methodName} on ${objectName}...`);
1359
+ const methodParams = methodDef.parameters || [];
1360
+ const methodCallArgs = [];
1361
+ for (const param of methodParams) {
1362
+ const typeStr = param.type || "";
1363
+ const isObjectType = this.isObjectTypeParameter(typeStr);
1364
+ if (isObjectType) {
1365
+ const objArg = {};
1366
+ const match = typeStr.match(/\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}/);
1367
+ if (match) {
1368
+ const propsStr = match[1];
1369
+ const propMatches = propsStr.matchAll(/(\w+)(\?)?:\s*([^;]+)/g);
1370
+ for (const propMatch of propMatches) {
1371
+ const [, propName] = propMatch;
1372
+ const optionName = propName.replace(/([A-Z])/g, "-$1").toLowerCase();
1373
+ if (options[optionName] !== void 0) {
1374
+ let value = options[optionName];
1375
+ if (typeof value === "string" && (value.startsWith("{") || value.startsWith("["))) {
1376
+ try {
1377
+ value = JSON.parse(value);
1378
+ } catch {
1379
+ }
1380
+ }
1381
+ objArg[propName] = value;
1382
+ }
1383
+ }
1384
+ }
1385
+ if (Object.keys(objArg).length > 0) {
1386
+ methodCallArgs.push(objArg);
1387
+ } else if (!param.optional) {
1388
+ methodCallArgs.push({});
1389
+ } else {
1390
+ methodCallArgs.push(void 0);
1391
+ }
1392
+ } else {
1393
+ const optionName = param.name.replace(/([A-Z])/g, "-$1").toLowerCase();
1394
+ if (options[optionName] !== void 0) {
1395
+ methodCallArgs.push(options[optionName]);
1396
+ } else if (param.default !== void 0) {
1397
+ methodCallArgs.push(param.default);
1398
+ } else {
1399
+ methodCallArgs.push(void 0);
1400
+ }
1401
+ }
1402
+ }
1403
+ const method = obj[methodName];
1404
+ if (typeof method !== "function") {
1405
+ spinner.fail(`Method ${methodName} is not a function`);
1406
+ this.exitWithError(`Method ${methodName} is not callable`);
1407
+ return;
1408
+ }
1409
+ const result = await method.call(obj, ...methodCallArgs);
1410
+ spinner.succeed(`Executed ${methodName}`);
1411
+ console.log(JSON.stringify(result, null, 2));
1412
+ } catch (error) {
1413
+ this.exitWithError(
1414
+ error instanceof Error ? error.message : "Unknown error"
1415
+ );
1416
+ }
1417
+ }
1418
+ /**
1419
+ * Handle singleton method (no parameters, no database lookup)
1420
+ * Creates a new instance with proper config and calls the method
1421
+ */
1422
+ async handleSingletonMethod(objectName, methodName, options, methodDef) {
1423
+ try {
1424
+ const classInfo = ObjectRegistry.getClass(objectName);
1425
+ if (!classInfo || !classInfo.constructor) {
1426
+ const availableObjects = Array.from(
1427
+ ObjectRegistry.getAllClasses().values()
1428
+ ).map((info) => info.name);
1429
+ const availableList = availableObjects.length > 0 ? `Available objects:
1430
+ ${availableObjects.join("\n ")}` : 'No objects registered. Run "npm run build" to generate registrations.';
1431
+ this.exitWithError(
1432
+ `Object class '${objectName}' not found.
1433
+
1434
+ ${availableList}
1435
+
1436
+ Troubleshooting:
1437
+ 1. Run "npm run build" to generate .smrt/register.js
1438
+ 2. Ensure package exports classes correctly
1439
+ 3. Run "smrt doctor" for diagnostics`
1440
+ );
1441
+ return;
1442
+ }
1443
+ const jsonMode = options.json === true;
1444
+ const spinner = jsonMode ? {
1445
+ succeed: (_text) => {
1446
+ },
1447
+ fail: (msg) => {
1448
+ if (msg) {
1449
+ console.error(msg);
1450
+ }
1451
+ }
1452
+ } : this.createSpinner(`Executing ${methodName} on ${objectName}...`);
1453
+ const { getConfig, getPackageConfig, getModuleConfig } = await import("@happyvertical/smrt-config");
1454
+ const smrtConfig = getConfig() || {};
1455
+ const moduleConfig = getModuleConfig(objectName.toLowerCase(), {});
1456
+ const { DEFAULT_CLI_CONFIG } = await import("./config-C8pQD-tk.js");
1457
+ const cliConfig = getPackageConfig("cli", DEFAULT_CLI_CONFIG);
1458
+ let db = this.context.db;
1459
+ if (!db && cliConfig?.database?.url) {
1460
+ const { getDatabase } = await import("@happyvertical/sql");
1461
+ db = await getDatabase({
1462
+ type: cliConfig.database.type || "sqlite",
1463
+ url: cliConfig.database.url
1464
+ });
1465
+ }
1466
+ const isManifestStub = classInfo.constructor?._isManifestStub === true;
1467
+ if (process.env.DEBUG) {
1468
+ console.log(`[DEBUG] ${objectName} constructor info:`);
1469
+ console.log(` - isManifestStub: ${isManifestStub}`);
1470
+ console.log(
1471
+ ` - constructor name: ${classInfo.constructor?.name || "undefined"}`
1472
+ );
1473
+ console.log(` - packageName: ${classInfo.packageName || "local"}`);
1474
+ }
1475
+ if (isManifestStub) {
1476
+ this.exitWithError(
1477
+ `${objectName} is registered from manifest but the real class wasn't loaded.
1478
+
1479
+ This usually means:
1480
+ 1. The .smrt/register.js file doesn't import the class
1481
+ 2. The package doesn't export the class properly
1482
+ 3. The class name in the package doesn't match the manifest
1483
+
1484
+ Try:
1485
+ - Run 'npm run build' to regenerate .smrt/register.js
1486
+ - Check that the package exports the ${objectName} class
1487
+ - Check .smrt/register.js imports match the package exports`
1488
+ );
1489
+ return;
1490
+ }
1491
+ if (typeof classInfo.constructor !== "function") {
1492
+ this.exitWithError(
1493
+ `${objectName} constructor is not available.
1494
+ This usually means the class wasn't properly exported or registered.
1495
+ Check that the package exports the class and .smrt/register.js imports it.`
1496
+ );
1497
+ return;
1498
+ }
1499
+ const useCliDb = db && !moduleConfig?.db;
1500
+ const instanceConfig = {
1501
+ ...smrtConfig,
1502
+ ...moduleConfig,
1503
+ ...useCliDb && { db },
1504
+ ...this.context.ai && { ai: this.context.ai },
1505
+ // In JSON mode, silence all log output to ensure clean JSON
1506
+ ...jsonMode && { silent: true }
1507
+ };
1508
+ const obj = new classInfo.constructor(instanceConfig);
1509
+ if (typeof obj.initialize === "function") {
1510
+ await obj.initialize();
1511
+ }
1512
+ const method = obj[methodName];
1513
+ if (typeof method !== "function") {
1514
+ spinner.fail(`Method ${methodName} is not a function`);
1515
+ this.exitWithError(`Method ${methodName} is not callable`);
1516
+ return;
1517
+ }
1518
+ const methodParams = methodDef?.parameters || [];
1519
+ const methodCallArgs = [];
1520
+ for (const param of methodParams) {
1521
+ const typeStr = param.type || "";
1522
+ const isObjectType = this.isObjectTypeParameter(typeStr);
1523
+ if (isObjectType) {
1524
+ const objArg = {};
1525
+ const match = typeStr.match(/\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}/);
1526
+ if (match) {
1527
+ const propsStr = match[1];
1528
+ const propMatches = propsStr.matchAll(/(\w+)(\?)?:\s*([^;]+)/g);
1529
+ for (const propMatch of propMatches) {
1530
+ const [, propName] = propMatch;
1531
+ const optionName = propName.replace(/([A-Z])/g, "-$1").toLowerCase();
1532
+ if (options[optionName] !== void 0) {
1533
+ let value = options[optionName];
1534
+ if (typeof value === "string" && (value.startsWith("{") || value.startsWith("["))) {
1535
+ try {
1536
+ value = JSON.parse(value);
1537
+ } catch {
1538
+ }
1539
+ }
1540
+ objArg[propName] = value;
1541
+ }
1542
+ }
1543
+ }
1544
+ if (Object.keys(objArg).length > 0) {
1545
+ methodCallArgs.push(objArg);
1546
+ } else if (!param.optional) {
1547
+ methodCallArgs.push({});
1548
+ } else {
1549
+ methodCallArgs.push(void 0);
1550
+ }
1551
+ } else {
1552
+ const optionName = param.name.replace(/([A-Z])/g, "-$1").toLowerCase();
1553
+ if (options[optionName] !== void 0) {
1554
+ methodCallArgs.push(options[optionName]);
1555
+ } else if (param.default !== void 0) {
1556
+ methodCallArgs.push(param.default);
1557
+ } else {
1558
+ methodCallArgs.push(void 0);
1559
+ }
1560
+ }
1561
+ }
1562
+ const result = await method.call(obj, ...methodCallArgs);
1563
+ spinner.succeed(`Executed ${methodName}`);
1564
+ if (result !== void 0) {
1565
+ console.log(JSON.stringify(result, null, 2));
1566
+ }
1567
+ } catch (error) {
1568
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
1569
+ const errorStack = error instanceof Error ? error.stack : void 0;
1570
+ console.error(`
1571
+ Error executing ${objectName}.${methodName}():`);
1572
+ console.error(` ${errorMessage}`);
1573
+ if (errorStack && process.env.DEBUG) {
1574
+ console.error("\nStack trace:");
1575
+ console.error(errorStack);
1576
+ }
1577
+ const schemaGuidance = errorMessage.includes("Run 'smrt db:migrate'");
1578
+ console.error(
1579
+ schemaGuidance ? "\nTip: Prepare the database schema before running generated CLI commands." : "\nTip: Set DEBUG=1 for full stack trace, or check the method implementation."
1580
+ );
1581
+ this.exitWithError(errorMessage);
1582
+ }
1583
+ }
1584
+ /**
1585
+ * Get or create collection for an object
1586
+ * Uses database configuration from smrt.config.js or defaults to :memory:
1587
+ */
1588
+ async getCollection(objectName) {
1589
+ if (!this.collections.has(objectName)) {
1590
+ const classInfo = ObjectRegistry.getClass(objectName);
1591
+ if (!classInfo || !classInfo.collectionConstructor) {
1592
+ const availableObjects = Array.from(
1593
+ ObjectRegistry.getAllClasses().values()
1594
+ ).map((info) => info.name);
1595
+ throw new Error(
1596
+ `Object '${objectName}' not found or has no collection constructor.
1597
+
1598
+ Available objects:
1599
+ ${availableObjects.join("\n ")}
1600
+
1601
+ Troubleshooting:
1602
+ 1. If from external package, ensure it's installed:
1603
+ npm install <package-name>
1604
+
1605
+ 2. Rebuild your project to regenerate manifest:
1606
+ npm run build
1607
+
1608
+ 3. Check .smrt/manifest.json contains the object
1609
+
1610
+ 4. Verify package exports classes correctly
1611
+ 5. Run with verbose mode for more details:
1612
+ SMRT_CLI_VERBOSE=true npx smrt ${objectName}:list
1613
+ `
1614
+ );
1615
+ }
1616
+ let db = this.context.db;
1617
+ if (!db) {
1618
+ const { getPackageConfig } = await import("@happyvertical/smrt-config");
1619
+ const { DEFAULT_CLI_CONFIG } = await import("./config-C8pQD-tk.js");
1620
+ const config = getPackageConfig("cli", DEFAULT_CLI_CONFIG);
1621
+ const { getDatabase } = await import("@happyvertical/sql");
1622
+ db = await getDatabase({
1623
+ type: config.database.type,
1624
+ url: config.database.url
1625
+ });
1626
+ if (config.verbose) {
1627
+ console.log(`[CLI] Using database: ${config.database.url}`);
1628
+ }
1629
+ }
1630
+ const collection2 = new classInfo.collectionConstructor({
1631
+ ai: this.context.ai,
1632
+ db
1633
+ });
1634
+ await collection2.initialize();
1635
+ this.collections.set(objectName, collection2);
1636
+ }
1637
+ const collection = this.collections.get(objectName);
1638
+ if (!collection) throw new Error(`Collection ${objectName} not found`);
1639
+ return collection;
1640
+ }
1641
+ /**
1642
+ * Interactive field prompts
1643
+ */
1644
+ async promptForFields(objectName, current) {
1645
+ const fields = ObjectRegistry.getFields(objectName);
1646
+ const result = {};
1647
+ for (const [fieldName, field] of fields) {
1648
+ const currentValue = current[fieldName];
1649
+ let message = `${fieldName}`;
1650
+ if (field.options?.description) {
1651
+ message += ` (${field.options.description})`;
1652
+ }
1653
+ if (currentValue !== void 0) {
1654
+ message += ` [${currentValue}]`;
1655
+ }
1656
+ message += ": ";
1657
+ if (field.type === "boolean") {
1658
+ result[fieldName] = await this.confirm(message);
1659
+ } else {
1660
+ const input = await this.prompt(message);
1661
+ if (input.trim()) {
1662
+ result[fieldName] = this.parseFieldValue(input);
1663
+ } else if (currentValue !== void 0) {
1664
+ result[fieldName] = currentValue;
1665
+ }
1666
+ }
1667
+ }
1668
+ return result;
1669
+ }
1670
+ /**
1671
+ * Parse field value from string
1672
+ */
1673
+ parseFieldValue(value) {
1674
+ try {
1675
+ return JSON.parse(value);
1676
+ } catch {
1677
+ return value;
1678
+ }
1679
+ }
1680
+ /**
1681
+ * Display results as table
1682
+ */
1683
+ displayTable(results, objectName) {
1684
+ if (results.length === 0) {
1685
+ console.log(`No ${objectName} objects found`);
1686
+ return;
1687
+ }
1688
+ const keys = ["id", "name", "slug", "created_at"];
1689
+ const rows = results.map(
1690
+ (item) => keys.map((key) => String(item[key] || "").substring(0, 30))
1691
+ );
1692
+ console.log();
1693
+ console.log(keys.join(" "));
1694
+ console.log("-".repeat(80));
1695
+ rows.forEach((row) => {
1696
+ console.log(row.join(" "));
1697
+ });
1698
+ console.log();
1699
+ }
1700
+ /**
1701
+ * Convert object to YAML-like string
1702
+ */
1703
+ toYamlString(obj, indent = 0) {
1704
+ const spaces = " ".repeat(indent);
1705
+ let result = "";
1706
+ for (const [key, value] of Object.entries(obj)) {
1707
+ if (value === null || value === void 0) {
1708
+ result += `${spaces}${key}: null
1709
+ `;
1710
+ } else if (typeof value === "object" && !Array.isArray(value)) {
1711
+ result += `${spaces}${key}:
1712
+ ${this.toYamlString(value, indent + 1)}`;
1713
+ } else if (Array.isArray(value)) {
1714
+ result += `${spaces}${key}:
1715
+ `;
1716
+ value.forEach((item) => {
1717
+ result += `${spaces} - ${item}
1718
+ `;
1719
+ });
1720
+ } else {
1721
+ result += `${spaces}${key}: ${value}
1722
+ `;
1723
+ }
1724
+ }
1725
+ return result;
1726
+ }
1727
+ }
1728
+ async function main() {
1729
+ const args = process.argv.slice(2);
1730
+ const timingEnabled = args.includes("--timing");
1731
+ const timing = {};
1732
+ const startTime = timingEnabled ? performance.now() : 0;
1733
+ {
1734
+ const fs = await import("node:fs");
1735
+ const path = await import("node:path");
1736
+ const configNames = [
1737
+ "smrt.config.js",
1738
+ "smrt.config.mjs",
1739
+ "smrt.config.cjs",
1740
+ "smrt.config.json"
1741
+ ];
1742
+ let dir = process.cwd();
1743
+ const { root } = path.parse(dir);
1744
+ while (dir !== root) {
1745
+ if (configNames.some((name) => fs.existsSync(path.join(dir, name)))) {
1746
+ try {
1747
+ const { loadEnvFile } = await import("node:process");
1748
+ loadEnvFile(path.join(dir, ".env"));
1749
+ } catch {
1750
+ }
1751
+ break;
1752
+ }
1753
+ dir = path.dirname(dir);
1754
+ }
1755
+ }
1756
+ const configStart = timingEnabled ? performance.now() : 0;
1757
+ const { loadConfig } = await import("@happyvertical/smrt-config");
1758
+ await loadConfig({ cache: true });
1759
+ if (timingEnabled) {
1760
+ timing.config = performance.now() - configStart;
1761
+ }
1762
+ const config = {
1763
+ name: "smrt",
1764
+ version: CLI_VERSION,
1765
+ description: "Admin CLI for smrt objects",
1766
+ prompt: !process.env.CI,
1767
+ // Disable prompts in CI
1768
+ colors: !process.env.NO_COLOR && process.stdout.isTTY
1769
+ };
1770
+ const context = {
1771
+ // db and ai can be configured via environment or initialized here
1772
+ timing: timingEnabled ? timing : void 0
1773
+ };
1774
+ const cli = new CLIGenerator(config, context);
1775
+ const handler = cli.generateHandler();
1776
+ const filteredArgs = args.filter((arg) => arg !== "--timing");
1777
+ try {
1778
+ await handler(filteredArgs);
1779
+ } catch (error) {
1780
+ console.error(
1781
+ "CLI Error:",
1782
+ error instanceof Error ? error.message : "Unknown error"
1783
+ );
1784
+ process.exit(1);
1785
+ }
1786
+ if (timingEnabled) {
1787
+ timing.total = performance.now() - startTime;
1788
+ console.log("\n⏱ Startup Timing:");
1789
+ console.log(` Config load: ${timing.config?.toFixed(0) ?? "N/A"}ms`);
1790
+ console.log(
1791
+ ` Class loading: ${timing.classLoading?.toFixed(0) ?? "N/A"}ms`
1792
+ );
1793
+ console.log(
1794
+ ` Command gen: ${timing.commandGen?.toFixed(0) ?? "N/A"}ms`
1795
+ );
1796
+ console.log(` Total startup: ${timing.total.toFixed(0)}ms`);
1797
+ }
1798
+ }
1799
+ const cliEntryGlobal = globalThis;
1800
+ if (!cliEntryGlobal.__SMRT_CLI_ENTRY_RUNNING__) {
1801
+ cliEntryGlobal.__SMRT_CLI_ENTRY_RUNNING__ = true;
1802
+ main().catch((error) => {
1803
+ console.error(
1804
+ "CLI Error:",
1805
+ error instanceof Error ? error.message : "Unknown error"
1806
+ );
1807
+ process.exit(1);
1808
+ });
1809
+ }