@objectstack/cli 3.0.0 → 3.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.
package/dist/bin.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  collectMetadataStats,
4
+ configExists,
4
5
  createTimer,
5
6
  formatZodErrors,
6
7
  loadConfig,
@@ -18,8 +19,8 @@ import {
18
19
 
19
20
  // src/bin.ts
20
21
  import { createRequire as createRequire2 } from "module";
21
- import { Command as Command13 } from "commander";
22
- import chalk13 from "chalk";
22
+ import { Command as Command17 } from "commander";
23
+ import chalk17 from "chalk";
23
24
 
24
25
  // src/commands/compile.ts
25
26
  import { Command } from "commander";
@@ -140,7 +141,237 @@ import chalk3 from "chalk";
140
141
  import { execSync as execSync2 } from "child_process";
141
142
  import fs3 from "fs";
142
143
  import path3 from "path";
143
- var doctorCommand = new Command3("doctor").description("Check development environment health").option("-v, --verbose", "Show detailed information").action(async (options) => {
144
+ function detectCircularDependencies(objects) {
145
+ const issues = [];
146
+ const graph = /* @__PURE__ */ new Map();
147
+ for (const obj of objects) {
148
+ const deps = [];
149
+ if (obj.fields && typeof obj.fields === "object") {
150
+ for (const field of Object.values(obj.fields)) {
151
+ if (field?.type === "lookup" && field?.reference) {
152
+ deps.push(field.reference);
153
+ }
154
+ }
155
+ }
156
+ graph.set(obj.name, deps);
157
+ }
158
+ const visited = /* @__PURE__ */ new Set();
159
+ const stack = /* @__PURE__ */ new Set();
160
+ function dfs(node, path12) {
161
+ if (stack.has(node)) {
162
+ const cycleStart = path12.indexOf(node);
163
+ const cycle = path12.slice(cycleStart).concat(node);
164
+ issues.push(`Circular dependency: ${cycle.join(" \u2192 ")}`);
165
+ return true;
166
+ }
167
+ if (visited.has(node)) return false;
168
+ visited.add(node);
169
+ stack.add(node);
170
+ for (const dep of graph.get(node) || []) {
171
+ if (graph.has(dep)) {
172
+ dfs(dep, [...path12, node]);
173
+ }
174
+ }
175
+ stack.delete(node);
176
+ return false;
177
+ }
178
+ for (const name of graph.keys()) {
179
+ if (!visited.has(name)) {
180
+ dfs(name, []);
181
+ }
182
+ }
183
+ return issues;
184
+ }
185
+ function findOrphanViews(config) {
186
+ const objectNames = /* @__PURE__ */ new Set();
187
+ if (Array.isArray(config.objects)) {
188
+ for (const obj of config.objects) {
189
+ if (obj.name) objectNames.add(obj.name);
190
+ }
191
+ }
192
+ const orphans = [];
193
+ if (Array.isArray(config.views)) {
194
+ for (const view of config.views) {
195
+ if (view.object && !objectNames.has(view.object)) {
196
+ orphans.push(`View "${view.name || "?"}" references non-existent object "${view.object}"`);
197
+ }
198
+ }
199
+ }
200
+ return orphans;
201
+ }
202
+ function findUnusedObjects(config) {
203
+ const objectNames = /* @__PURE__ */ new Set();
204
+ if (Array.isArray(config.objects)) {
205
+ for (const obj of config.objects) {
206
+ if (obj.name) objectNames.add(obj.name);
207
+ }
208
+ }
209
+ const referencedObjects = /* @__PURE__ */ new Set();
210
+ if (Array.isArray(config.views)) {
211
+ for (const view of config.views) {
212
+ if (view.object) referencedObjects.add(view.object);
213
+ }
214
+ }
215
+ if (Array.isArray(config.flows)) {
216
+ for (const flow of config.flows) {
217
+ if (flow.trigger?.object) referencedObjects.add(flow.trigger.object);
218
+ if (flow.object) referencedObjects.add(flow.object);
219
+ }
220
+ }
221
+ if (Array.isArray(config.apps)) {
222
+ for (const app of config.apps) {
223
+ if (Array.isArray(app.navigation)) {
224
+ for (const nav of app.navigation) {
225
+ if (nav.object) referencedObjects.add(nav.object);
226
+ }
227
+ }
228
+ }
229
+ }
230
+ if (Array.isArray(config.agents)) {
231
+ for (const agent of config.agents) {
232
+ if (Array.isArray(agent.objects)) {
233
+ for (const o of agent.objects) referencedObjects.add(o);
234
+ }
235
+ }
236
+ }
237
+ if (Array.isArray(config.objects)) {
238
+ for (const obj of config.objects) {
239
+ if (obj.fields && typeof obj.fields === "object") {
240
+ for (const field of Object.values(obj.fields)) {
241
+ if (field?.type === "lookup" && field?.reference) {
242
+ referencedObjects.add(field.reference);
243
+ }
244
+ }
245
+ }
246
+ }
247
+ }
248
+ const unused = [];
249
+ for (const name of objectNames) {
250
+ if (!referencedObjects.has(name)) {
251
+ unused.push(`Object "${name}" is defined but not referenced by any view, flow, app, or agent`);
252
+ }
253
+ }
254
+ return unused;
255
+ }
256
+ function walkDir(dir, ext) {
257
+ const results = [];
258
+ if (!fs3.existsSync(dir)) return results;
259
+ const entries = fs3.readdirSync(dir, { withFileTypes: true });
260
+ for (const entry of entries) {
261
+ if (entry.name === "node_modules") continue;
262
+ const fullPath = path3.join(dir, entry.name);
263
+ if (entry.isDirectory()) {
264
+ results.push(...walkDir(fullPath, ext));
265
+ } else if (entry.name.endsWith(ext)) {
266
+ results.push(fullPath);
267
+ }
268
+ }
269
+ return results;
270
+ }
271
+ function findMissingTests(cwd) {
272
+ const specSrcDir = path3.join(cwd, "packages/spec/src");
273
+ if (!fs3.existsSync(specSrcDir)) return [];
274
+ const missing = [];
275
+ const zodFiles = walkDir(specSrcDir, ".zod.ts");
276
+ for (const zodFile of zodFiles) {
277
+ const testFile = zodFile.replace(".zod.ts", ".test.ts");
278
+ if (!fs3.existsSync(testFile)) {
279
+ const relZod = path3.relative(specSrcDir, zodFile);
280
+ const relTest = path3.relative(specSrcDir, testFile);
281
+ missing.push(`Missing test: ${relTest} (for ${relZod})`);
282
+ }
283
+ }
284
+ return missing;
285
+ }
286
+ function findDeprecatedUsages(cwd) {
287
+ const specSrcDir = path3.join(cwd, "packages/spec/src");
288
+ if (!fs3.existsSync(specSrcDir)) return [];
289
+ const deprecated = [];
290
+ const tsFiles = walkDir(specSrcDir, ".ts").filter((f) => !f.endsWith(".test.ts"));
291
+ for (const tsFile of tsFiles) {
292
+ try {
293
+ const content = fs3.readFileSync(tsFile, "utf-8");
294
+ const lines = content.split("\n");
295
+ const relPath = path3.relative(specSrcDir, tsFile);
296
+ for (let i = 0; i < lines.length; i++) {
297
+ if (lines[i].includes("@deprecated")) {
298
+ deprecated.push(`${relPath}:${i + 1} \u2014 @deprecated tag found`);
299
+ }
300
+ }
301
+ } catch {
302
+ }
303
+ }
304
+ return deprecated;
305
+ }
306
+ var DEPRECATED_PATTERNS = [
307
+ {
308
+ pattern: /\bEnhancedObjectKernel\b/,
309
+ description: "EnhancedObjectKernel is deprecated in v3",
310
+ replacement: "Use ObjectKernel instead"
311
+ },
312
+ {
313
+ pattern: /\bmax_length\b/,
314
+ description: "snake_case config key: max_length",
315
+ replacement: "Use maxLength (camelCase)"
316
+ },
317
+ {
318
+ pattern: /\bdefault_value\b/,
319
+ description: "snake_case config key: default_value",
320
+ replacement: "Use defaultValue (camelCase)"
321
+ },
322
+ {
323
+ pattern: /\bmin_length\b/,
324
+ description: "snake_case config key: min_length",
325
+ replacement: "Use minLength (camelCase)"
326
+ },
327
+ {
328
+ pattern: /\breference_filters\b/,
329
+ description: "snake_case config key: reference_filters",
330
+ replacement: "Use referenceFilters (camelCase)"
331
+ },
332
+ {
333
+ pattern: /\bunique_name\b/,
334
+ description: "snake_case config key: unique_name",
335
+ replacement: "Use uniqueName (camelCase)"
336
+ },
337
+ {
338
+ pattern: /from\s+['"]@objectstack\/core\/enhanced['"]/,
339
+ description: "Import from deprecated @objectstack/core/enhanced path",
340
+ replacement: "Use import from '@objectstack/core'"
341
+ },
342
+ {
343
+ pattern: /from\s+['"]@objectstack\/spec\/dist\/[^'"]+['"]/,
344
+ description: "Import from deprecated @objectstack/spec/dist/ deep path",
345
+ replacement: "Use import from '@objectstack/spec'"
346
+ }
347
+ ];
348
+ function scanDeprecatedPatterns(dir) {
349
+ const results = [];
350
+ if (!fs3.existsSync(dir)) return results;
351
+ const tsFiles = walkDir(dir, ".ts").filter((f) => !f.endsWith(".test.ts"));
352
+ for (const tsFile of tsFiles) {
353
+ try {
354
+ const content = fs3.readFileSync(tsFile, "utf-8");
355
+ const lines = content.split("\n");
356
+ const relPath = path3.relative(process.cwd(), tsFile);
357
+ for (let i = 0; i < lines.length; i++) {
358
+ for (const dp of DEPRECATED_PATTERNS) {
359
+ if (dp.pattern.test(lines[i])) {
360
+ results.push({
361
+ file: relPath,
362
+ line: i + 1,
363
+ description: dp.description,
364
+ replacement: dp.replacement
365
+ });
366
+ }
367
+ }
368
+ }
369
+ } catch {
370
+ }
371
+ }
372
+ return results;
373
+ }
374
+ var doctorCommand = new Command3("doctor").description("Check development environment and configuration health").option("-v, --verbose", "Show detailed information").option("--scan-deprecations", "Scan for deprecated ObjectStack patterns").action(async (options) => {
144
375
  printHeader("Environment Health Check");
145
376
  const results = [];
146
377
  try {
@@ -262,6 +493,87 @@ var doctorCommand = new Command3("doctor").description("Check development enviro
262
493
  if (result.status === "error") hasErrors = true;
263
494
  if (result.status === "warning") hasWarnings = true;
264
495
  });
496
+ printStep("Checking for missing test files...");
497
+ const missingTests = findMissingTests(cwd);
498
+ if (missingTests.length > 0) {
499
+ hasWarnings = true;
500
+ for (const msg of missingTests) {
501
+ printWarning(msg);
502
+ }
503
+ } else {
504
+ printSuccess("Test coverage All *.zod.ts files have matching tests");
505
+ }
506
+ printStep("Scanning for @deprecated usage...");
507
+ const deprecatedUsages = findDeprecatedUsages(cwd);
508
+ if (deprecatedUsages.length > 0) {
509
+ hasWarnings = true;
510
+ for (const msg of deprecatedUsages) {
511
+ printWarning(`Deprecated: ${msg}`);
512
+ }
513
+ } else {
514
+ printSuccess("Deprecations No @deprecated tags found");
515
+ }
516
+ if (configExists()) {
517
+ printStep("Loading configuration for analysis...");
518
+ try {
519
+ const { config } = await loadConfig();
520
+ if (Array.isArray(config.objects) && config.objects.length > 0) {
521
+ printStep("Checking for circular dependencies...");
522
+ const cycles = detectCircularDependencies(config.objects);
523
+ if (cycles.length > 0) {
524
+ hasWarnings = true;
525
+ for (const msg of cycles) {
526
+ printWarning(msg);
527
+ }
528
+ } else {
529
+ printSuccess("Dependencies No circular references detected");
530
+ }
531
+ printStep("Checking for unused objects...");
532
+ const unused = findUnusedObjects(config);
533
+ if (unused.length > 0) {
534
+ hasWarnings = true;
535
+ for (const msg of unused) {
536
+ printWarning(msg);
537
+ }
538
+ } else {
539
+ printSuccess("Object usage All objects are referenced");
540
+ }
541
+ }
542
+ if (Array.isArray(config.views) && config.views.length > 0) {
543
+ printStep("Checking for orphan views...");
544
+ const orphans = findOrphanViews(config);
545
+ if (orphans.length > 0) {
546
+ hasWarnings = true;
547
+ for (const msg of orphans) {
548
+ printWarning(msg);
549
+ }
550
+ } else {
551
+ printSuccess("View integrity All views reference valid objects");
552
+ }
553
+ }
554
+ } catch {
555
+ printWarning("Could not load config for analysis (config checks skipped)");
556
+ hasWarnings = true;
557
+ }
558
+ }
559
+ if (options.scanDeprecations) {
560
+ printStep("Scanning for deprecated ObjectStack patterns...");
561
+ const scanDir = path3.join(cwd, "src");
562
+ const deprecations = scanDeprecatedPatterns(scanDir);
563
+ if (deprecations.length > 0) {
564
+ hasWarnings = true;
565
+ for (const dep of deprecations) {
566
+ printWarning(`${dep.file}:${dep.line} \u2014 ${dep.description}`);
567
+ if (options.verbose) {
568
+ console.log(chalk3.dim(` \u2192 ${dep.replacement}`));
569
+ }
570
+ }
571
+ console.log("");
572
+ printInfo(`Found ${deprecations.length} deprecated pattern(s). Run \`objectstack codemod v2-to-v3\` to auto-fix.`);
573
+ } else {
574
+ printSuccess("Deprecation scan No deprecated patterns found");
575
+ }
576
+ }
265
577
  console.log("");
266
578
  if (hasErrors) {
267
579
  console.log(chalk3.red("\u274C Some critical issues found. Please fix them before continuing."));
@@ -1775,79 +2087,451 @@ var generateTypesCommand = new Command11("types").description("Generate TypeScri
1775
2087
  process.exit(1);
1776
2088
  }
1777
2089
  });
1778
- var generateCommand = new Command11("generate").alias("g").description("Generate metadata files or TypeScript types").argument("[type]", "Metadata type to generate (object, view, action, flow, agent, dashboard, app)").argument("[name]", "Name for the metadata (use kebab-case)").option("-d, --dir <directory>", "Target directory (overrides default)").option("--dry-run", "Show what would be created without writing files").addCommand(generateTypesCommand).action(async (type, name, options) => {
1779
- if (!type) {
1780
- printHeader("Generate");
1781
- console.log(chalk10.bold(" Sub-commands:"));
1782
- console.log(` ${chalk10.cyan("types".padEnd(12))} Generate TypeScript type definitions from config`);
2090
+ function generateClientFromConfig(config) {
2091
+ const lines = [
2092
+ "// Auto-generated by ObjectStack CLI \u2014 do not edit manually",
2093
+ `// Generated at ${(/* @__PURE__ */ new Date()).toISOString()}`,
2094
+ "",
2095
+ "import type { Data } from '@objectstack/spec';",
2096
+ ""
2097
+ ];
2098
+ const objects = [];
2099
+ const rawObjects = config.objects ?? config.data?.objects ?? {};
2100
+ if (Array.isArray(rawObjects)) {
2101
+ objects.push(...rawObjects);
2102
+ } else if (typeof rawObjects === "object") {
2103
+ for (const val of Object.values(rawObjects)) {
2104
+ if (val && typeof val === "object") objects.push(val);
2105
+ }
2106
+ }
2107
+ if (objects.length === 0) {
2108
+ lines.push("// No objects found in configuration");
2109
+ return lines.join("\n") + "\n";
2110
+ }
2111
+ for (const obj of objects) {
2112
+ const name = String(obj.name || "unknown");
2113
+ const typeName = name.split("_").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
2114
+ const fields = obj.fields ?? {};
2115
+ lines.push(`export interface ${typeName}Record {`);
2116
+ lines.push(" id: string;");
2117
+ for (const [fieldName, fieldDef] of Object.entries(fields)) {
2118
+ const fType = String(fieldDef.type || "text");
2119
+ const tsType = fieldTypeToTs(fType, !!fieldDef.multiple);
2120
+ const required = fieldDef.required ? "" : "?";
2121
+ lines.push(` ${fieldName}${required}: ${tsType};`);
2122
+ }
2123
+ lines.push("}");
2124
+ lines.push("");
2125
+ }
2126
+ lines.push("export class ObjectStackClient {");
2127
+ lines.push(" constructor(private baseUrl: string, private headers: Record<string, string> = {}) {}");
2128
+ lines.push("");
2129
+ lines.push(" private async request<T>(method: string, path: string, body?: unknown): Promise<T> {");
2130
+ lines.push(" const res = await fetch(`${this.baseUrl}${path}`, {");
2131
+ lines.push(" method,");
2132
+ lines.push(" headers: { 'Content-Type': 'application/json', ...this.headers },");
2133
+ lines.push(" body: body ? JSON.stringify(body) : undefined,");
2134
+ lines.push(" });");
2135
+ lines.push(" if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);");
2136
+ lines.push(" return res.json() as Promise<T>;");
2137
+ lines.push(" }");
2138
+ for (const obj of objects) {
2139
+ const name = String(obj.name || "unknown");
2140
+ const typeName = name.split("_").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
2141
+ const endpoint = `/api/${name}`;
2142
+ lines.push("");
2143
+ lines.push(` async list${typeName}(): Promise<${typeName}Record[]> {`);
2144
+ lines.push(` return this.request<${typeName}Record[]>('GET', '${endpoint}');`);
2145
+ lines.push(" }");
2146
+ lines.push("");
2147
+ lines.push(` async get${typeName}(id: string): Promise<${typeName}Record> {`);
2148
+ lines.push(` return this.request<${typeName}Record>('GET', '${endpoint}/\${id}');`);
2149
+ lines.push(" }");
2150
+ lines.push("");
2151
+ lines.push(` async create${typeName}(data: Omit<${typeName}Record, 'id'>): Promise<${typeName}Record> {`);
2152
+ lines.push(` return this.request<${typeName}Record>('POST', '${endpoint}', data);`);
2153
+ lines.push(" }");
2154
+ lines.push("");
2155
+ lines.push(` async update${typeName}(id: string, data: Partial<${typeName}Record>): Promise<${typeName}Record> {`);
2156
+ lines.push(` return this.request<${typeName}Record>('PATCH', '${endpoint}/\${id}', data);`);
2157
+ lines.push(" }");
2158
+ lines.push("");
2159
+ lines.push(` async delete${typeName}(id: string): Promise<void> {`);
2160
+ lines.push(` return this.request<void>('DELETE', '${endpoint}/\${id}');`);
2161
+ lines.push(" }");
2162
+ }
2163
+ lines.push("}");
2164
+ lines.push("");
2165
+ return lines.join("\n") + "\n";
2166
+ }
2167
+ var generateClientCommand = new Command11("client").description("Generate a type-safe client SDK from ObjectStack configuration").argument("[config]", "Configuration file path").option("-o, --output <file>", "Output file path", "src/client/objectstack-client.ts").option("--dry-run", "Show output without writing").action(async (configPath, options) => {
2168
+ printHeader("Generate Client SDK");
2169
+ try {
2170
+ const { loadConfig: loadConfig2 } = await import("./config-A7BN6UIT.js");
2171
+ const timer = createTimer();
2172
+ printInfo("Loading configuration...");
2173
+ const { config, absolutePath } = await loadConfig2(configPath);
2174
+ console.log(` ${chalk10.dim("Config:")} ${chalk10.white(absolutePath)}`);
2175
+ console.log(` ${chalk10.dim("Output:")} ${chalk10.white(options.output)}`);
1783
2176
  console.log("");
1784
- console.log(chalk10.bold(" Metadata types:"));
1785
- for (const [key, gen] of Object.entries(GENERATORS)) {
1786
- console.log(` ${chalk10.cyan(key.padEnd(12))} ${chalk10.dim(gen.description)}`);
2177
+ printStep("Generating client SDK...");
2178
+ const content = generateClientFromConfig(config);
2179
+ if (options.dryRun) {
2180
+ printInfo("Dry run \u2014 no files written");
2181
+ console.log("");
2182
+ for (const line of content.split("\n")) {
2183
+ console.log(chalk10.dim(` ${line}`));
2184
+ }
2185
+ console.log("");
2186
+ return;
2187
+ }
2188
+ const outPath = path9.resolve(process.cwd(), options.output);
2189
+ const outDir = path9.dirname(outPath);
2190
+ if (!fs9.existsSync(outDir)) {
2191
+ fs9.mkdirSync(outDir, { recursive: true });
1787
2192
  }
2193
+ fs9.writeFileSync(outPath, content);
2194
+ printSuccess(`Generated client SDK at ${options.output} (${timer.display()})`);
1788
2195
  console.log("");
1789
- console.log(chalk10.dim(" Usage: objectstack generate <type> <name>"));
1790
- console.log(chalk10.dim(" Usage: objectstack generate types [config]"));
1791
- return;
1792
- }
1793
- if (!name) {
1794
- printError("Missing required argument: <name>");
1795
- console.log(chalk10.dim(" Usage: objectstack generate <type> <name>"));
2196
+ } catch (error) {
2197
+ printError(error.message || String(error));
1796
2198
  process.exit(1);
1797
2199
  }
1798
- await generateMetadataCommand.parseAsync([type, name, ...process.argv.slice(4)], { from: "user" });
1799
2200
  });
1800
-
1801
- // src/commands/plugin.ts
1802
- import { Command as Command12 } from "commander";
1803
- import chalk11 from "chalk";
1804
- import fs10 from "fs";
1805
- import path10 from "path";
1806
- function resolvePluginName(plugin) {
1807
- if (typeof plugin === "string") return plugin;
1808
- if (plugin && typeof plugin === "object") {
1809
- const p = plugin;
1810
- if (typeof p.name === "string") return p.name;
1811
- if (p.constructor && p.constructor.name !== "Object") return p.constructor.name;
1812
- }
1813
- return "unknown";
2201
+ var FIELD_TYPE_SQL_MAP = {
2202
+ text: "VARCHAR(255)",
2203
+ textarea: "TEXT",
2204
+ richtext: "TEXT",
2205
+ html: "TEXT",
2206
+ markdown: "TEXT",
2207
+ number: "DECIMAL(18,2)",
2208
+ integer: "INTEGER",
2209
+ currency: "DECIMAL(18,2)",
2210
+ percent: "DECIMAL(5,2)",
2211
+ boolean: "BOOLEAN",
2212
+ date: "DATE",
2213
+ datetime: "TIMESTAMP",
2214
+ time: "TIME",
2215
+ email: "VARCHAR(255)",
2216
+ phone: "VARCHAR(50)",
2217
+ url: "VARCHAR(2048)",
2218
+ select: "VARCHAR(255)",
2219
+ multiselect: "TEXT",
2220
+ lookup: "VARCHAR(36)",
2221
+ master_detail: "VARCHAR(36)",
2222
+ formula: "TEXT",
2223
+ autonumber: "SERIAL",
2224
+ json: "JSONB",
2225
+ file: "VARCHAR(2048)",
2226
+ image: "VARCHAR(2048)",
2227
+ password: "VARCHAR(255)",
2228
+ slug: "VARCHAR(255)",
2229
+ uuid: "UUID",
2230
+ ip_address: "VARCHAR(45)",
2231
+ color: "VARCHAR(7)",
2232
+ rating: "INTEGER",
2233
+ geo_point: "POINT",
2234
+ vector: "VECTOR",
2235
+ encrypted: "TEXT"
2236
+ };
2237
+ function fieldTypeToSql(fieldType) {
2238
+ return FIELD_TYPE_SQL_MAP[fieldType] || "TEXT";
1814
2239
  }
1815
- function resolvePluginVersion(plugin) {
1816
- if (plugin && typeof plugin === "object") {
1817
- const p = plugin;
1818
- if (typeof p.version === "string") return p.version;
2240
+ function generateMigrationSql(config) {
2241
+ const lines = [
2242
+ "-- Auto-generated by ObjectStack CLI \u2014 do not edit manually",
2243
+ `-- Generated at ${(/* @__PURE__ */ new Date()).toISOString()}`,
2244
+ ""
2245
+ ];
2246
+ const objects = [];
2247
+ const rawObjects = config.objects ?? config.data?.objects ?? {};
2248
+ if (Array.isArray(rawObjects)) {
2249
+ objects.push(...rawObjects);
2250
+ } else if (typeof rawObjects === "object") {
2251
+ for (const val of Object.values(rawObjects)) {
2252
+ if (val && typeof val === "object") objects.push(val);
2253
+ }
1819
2254
  }
1820
- return "-";
1821
- }
1822
- function resolvePluginType(plugin) {
1823
- if (plugin && typeof plugin === "object") {
1824
- const p = plugin;
1825
- if (typeof p.type === "string") return p.type;
2255
+ if (objects.length === 0) {
2256
+ lines.push("-- No objects found in configuration");
2257
+ return lines.join("\n") + "\n";
1826
2258
  }
1827
- return "standard";
1828
- }
1829
- function readConfigText(configPath) {
1830
- return fs10.readFileSync(configPath, "utf-8");
1831
- }
1832
- function addPluginToConfig(configPath, packageName) {
1833
- let content = readConfigText(configPath);
1834
- const shortName = packageName.replace(/^@[^/]+\//, "").replace(/^plugin-/, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
1835
- const varName = shortName.replace(/-([a-z])/g, (_, c) => c.toUpperCase()) + "Plugin";
1836
- const importLine = `import ${varName} from '${packageName}';
1837
- `;
1838
- if (content.includes(packageName)) {
1839
- throw new Error(`Plugin '${packageName}' is already referenced in the config`);
2259
+ for (const obj of objects) {
2260
+ const tableName = String(obj.name || "unknown");
2261
+ const fields = obj.fields ?? {};
2262
+ lines.push(`CREATE TABLE IF NOT EXISTS "${tableName}" (`);
2263
+ lines.push(' "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),');
2264
+ const fieldLines = [];
2265
+ for (const [fieldName, fieldDef] of Object.entries(fields)) {
2266
+ const sqlType = fieldTypeToSql(String(fieldDef.type || "text"));
2267
+ const notNull = fieldDef.required ? " NOT NULL" : "";
2268
+ fieldLines.push(` "${fieldName}" ${sqlType}${notNull}`);
2269
+ }
2270
+ fieldLines.push(' "created_at" TIMESTAMP NOT NULL DEFAULT now()');
2271
+ fieldLines.push(' "updated_at" TIMESTAMP NOT NULL DEFAULT now()');
2272
+ lines.push(fieldLines.join(",\n"));
2273
+ lines.push(");");
2274
+ lines.push("");
1840
2275
  }
1841
- const importRegex = /^import .+$/gm;
1842
- let lastImportEnd = 0;
1843
- let match;
1844
- while ((match = importRegex.exec(content)) !== null) {
1845
- lastImportEnd = match.index + match[0].length;
2276
+ return lines.join("\n") + "\n";
2277
+ }
2278
+ function generateMigrationTs(config) {
2279
+ const lines = [
2280
+ "// Auto-generated by ObjectStack CLI \u2014 do not edit manually",
2281
+ `// Generated at ${(/* @__PURE__ */ new Date()).toISOString()}`,
2282
+ "",
2283
+ "export async function up(db: any): Promise<void> {"
2284
+ ];
2285
+ const objects = [];
2286
+ const rawObjects = config.objects ?? config.data?.objects ?? {};
2287
+ if (Array.isArray(rawObjects)) {
2288
+ objects.push(...rawObjects);
2289
+ } else if (typeof rawObjects === "object") {
2290
+ for (const val of Object.values(rawObjects)) {
2291
+ if (val && typeof val === "object") objects.push(val);
2292
+ }
1846
2293
  }
1847
- if (lastImportEnd > 0) {
1848
- content = content.slice(0, lastImportEnd) + "\n" + importLine + content.slice(lastImportEnd);
1849
- } else {
1850
- content = importLine + "\n" + content;
2294
+ if (objects.length === 0) {
2295
+ lines.push(" // No objects found in configuration");
2296
+ lines.push("}");
2297
+ lines.push("");
2298
+ lines.push("export async function down(db: any): Promise<void> {");
2299
+ lines.push(" // No objects found in configuration");
2300
+ lines.push("}");
2301
+ return lines.join("\n") + "\n";
2302
+ }
2303
+ for (const obj of objects) {
2304
+ const tableName = String(obj.name || "unknown");
2305
+ const fields = obj.fields ?? {};
2306
+ lines.push(` await db.schema.createTable('${tableName}', (table: any) => {`);
2307
+ lines.push(" table.uuid('id').primary().defaultTo(db.fn.uuid());");
2308
+ for (const [fieldName, fieldDef] of Object.entries(fields)) {
2309
+ const fType = String(fieldDef.type || "text");
2310
+ const required = fieldDef.required ? ".notNullable()" : ".nullable()";
2311
+ let colMethod;
2312
+ switch (fType) {
2313
+ case "text":
2314
+ case "email":
2315
+ case "phone":
2316
+ case "url":
2317
+ case "select":
2318
+ case "slug":
2319
+ case "password":
2320
+ case "color":
2321
+ case "ip_address":
2322
+ colMethod = `table.string('${fieldName}')`;
2323
+ break;
2324
+ case "textarea":
2325
+ case "richtext":
2326
+ case "html":
2327
+ case "markdown":
2328
+ case "formula":
2329
+ case "encrypted":
2330
+ colMethod = `table.text('${fieldName}')`;
2331
+ break;
2332
+ case "number":
2333
+ case "currency":
2334
+ case "percent":
2335
+ colMethod = `table.decimal('${fieldName}')`;
2336
+ break;
2337
+ case "integer":
2338
+ case "rating":
2339
+ colMethod = `table.integer('${fieldName}')`;
2340
+ break;
2341
+ case "boolean":
2342
+ colMethod = `table.boolean('${fieldName}')`;
2343
+ break;
2344
+ case "date":
2345
+ colMethod = `table.date('${fieldName}')`;
2346
+ break;
2347
+ case "datetime":
2348
+ colMethod = `table.timestamp('${fieldName}')`;
2349
+ break;
2350
+ case "time":
2351
+ colMethod = `table.time('${fieldName}')`;
2352
+ break;
2353
+ case "json":
2354
+ case "multiselect":
2355
+ colMethod = `table.jsonb('${fieldName}')`;
2356
+ break;
2357
+ case "uuid":
2358
+ case "lookup":
2359
+ case "master_detail":
2360
+ colMethod = `table.uuid('${fieldName}')`;
2361
+ break;
2362
+ default:
2363
+ colMethod = `table.text('${fieldName}')`;
2364
+ }
2365
+ lines.push(` ${colMethod}${required};`);
2366
+ }
2367
+ lines.push(" table.timestamps(true, true);");
2368
+ lines.push(" });");
2369
+ }
2370
+ lines.push("}");
2371
+ lines.push("");
2372
+ lines.push("export async function down(db: any): Promise<void> {");
2373
+ const tableNames = objects.map((o) => String(o.name || "unknown")).reverse();
2374
+ for (const tableName of tableNames) {
2375
+ lines.push(` await db.schema.dropTableIfExists('${tableName}');`);
2376
+ }
2377
+ lines.push("}");
2378
+ return lines.join("\n") + "\n";
2379
+ }
2380
+ var generateMigrationCommand = new Command11("migration").description("Generate database migration from ObjectStack schema").argument("[config]", "Configuration file path").option("-o, --output <file>", "Output file path").option("--format <format>", "Output format: sql or typescript", "typescript").option("--dry-run", "Show output without writing").action(async (configPath, options) => {
2381
+ printHeader("Generate Migration");
2382
+ try {
2383
+ const { loadConfig: loadConfig2 } = await import("./config-A7BN6UIT.js");
2384
+ const timer = createTimer();
2385
+ printInfo("Loading configuration...");
2386
+ const { config, absolutePath } = await loadConfig2(configPath);
2387
+ const ext = options.format === "sql" ? "sql" : "ts";
2388
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T]/g, "").slice(0, 14);
2389
+ const defaultOutput = `migrations/${timestamp}_migration.${ext}`;
2390
+ const output = options.output || defaultOutput;
2391
+ console.log(` ${chalk10.dim("Config:")} ${chalk10.white(absolutePath)}`);
2392
+ console.log(` ${chalk10.dim("Format:")} ${chalk10.white(options.format)}`);
2393
+ console.log(` ${chalk10.dim("Output:")} ${chalk10.white(output)}`);
2394
+ console.log("");
2395
+ printStep("Generating migration...");
2396
+ const content = options.format === "sql" ? generateMigrationSql(config) : generateMigrationTs(config);
2397
+ if (options.dryRun) {
2398
+ printInfo("Dry run \u2014 no files written");
2399
+ console.log("");
2400
+ for (const line of content.split("\n")) {
2401
+ console.log(chalk10.dim(` ${line}`));
2402
+ }
2403
+ console.log("");
2404
+ return;
2405
+ }
2406
+ const outPath = path9.resolve(process.cwd(), output);
2407
+ const outDir = path9.dirname(outPath);
2408
+ if (!fs9.existsSync(outDir)) {
2409
+ fs9.mkdirSync(outDir, { recursive: true });
2410
+ }
2411
+ fs9.writeFileSync(outPath, content);
2412
+ printSuccess(`Generated migration at ${output} (${timer.display()})`);
2413
+ console.log("");
2414
+ } catch (error) {
2415
+ printError(error.message || String(error));
2416
+ process.exit(1);
2417
+ }
2418
+ });
2419
+ var generateSchemaCommand = new Command11("schema").description("Generate JSON Schema for objectstack.config.ts (for IDE autocomplete)").option("-o, --output <file>", "Output file path", "objectstack.schema.json").option("--dry-run", "Show output without writing").action(async (options) => {
2420
+ printHeader("Generate Schema");
2421
+ try {
2422
+ const timer = createTimer();
2423
+ printStep("Loading ObjectStackDefinitionSchema...");
2424
+ const { z } = await import("zod");
2425
+ const { ObjectStackDefinitionSchema: ObjectStackDefinitionSchema3 } = await import("@objectstack/spec");
2426
+ printStep("Converting to JSON Schema...");
2427
+ const jsonSchema = z.toJSONSchema(ObjectStackDefinitionSchema3, {
2428
+ target: "draft-2020-12"
2429
+ });
2430
+ const schema = {
2431
+ ...jsonSchema,
2432
+ $id: "https://schema.objectstack.io/objectstack.config.json",
2433
+ title: "ObjectStack Configuration",
2434
+ description: "JSON Schema for objectstack.config.ts \u2014 generated from ObjectStackDefinitionSchema"
2435
+ };
2436
+ const content = JSON.stringify(schema, null, 2) + "\n";
2437
+ if (options.dryRun) {
2438
+ printInfo("Dry run \u2014 no files written");
2439
+ console.log("");
2440
+ console.log(content);
2441
+ return;
2442
+ }
2443
+ const outPath = path9.resolve(process.cwd(), options.output);
2444
+ const outDir = path9.dirname(outPath);
2445
+ if (!fs9.existsSync(outDir)) {
2446
+ fs9.mkdirSync(outDir, { recursive: true });
2447
+ }
2448
+ fs9.writeFileSync(outPath, content);
2449
+ printSuccess(`Generated JSON Schema at ${options.output} (${timer.display()})`);
2450
+ console.log("");
2451
+ console.log(chalk10.dim(" Usage: Reference in your IDE or editor for autocomplete"));
2452
+ console.log(chalk10.dim(` Path: ${outPath}`));
2453
+ console.log("");
2454
+ } catch (error) {
2455
+ printError(error.message || String(error));
2456
+ process.exit(1);
2457
+ }
2458
+ });
2459
+ var generateCommand = new Command11("generate").alias("g").description("Generate metadata files or TypeScript types").argument("[type]", "Metadata type to generate (object, view, action, flow, agent, dashboard, app)").argument("[name]", "Name for the metadata (use kebab-case)").option("-d, --dir <directory>", "Target directory (overrides default)").option("--dry-run", "Show what would be created without writing files").addCommand(generateTypesCommand).addCommand(generateClientCommand).addCommand(generateMigrationCommand).addCommand(generateSchemaCommand).action(async (type, name, options) => {
2460
+ if (!type) {
2461
+ printHeader("Generate");
2462
+ console.log(chalk10.bold(" Sub-commands:"));
2463
+ console.log(` ${chalk10.cyan("types".padEnd(12))} Generate TypeScript type definitions from config`);
2464
+ console.log(` ${chalk10.cyan("client".padEnd(12))} Generate a type-safe client SDK from config`);
2465
+ console.log(` ${chalk10.cyan("migration".padEnd(12))} Generate database migration from schema`);
2466
+ console.log(` ${chalk10.cyan("schema".padEnd(12))} Generate JSON Schema for objectstack.config.ts (IDE autocomplete)`);
2467
+ console.log("");
2468
+ console.log(chalk10.bold(" Metadata types:"));
2469
+ for (const [key, gen] of Object.entries(GENERATORS)) {
2470
+ console.log(` ${chalk10.cyan(key.padEnd(12))} ${chalk10.dim(gen.description)}`);
2471
+ }
2472
+ console.log("");
2473
+ console.log(chalk10.dim(" Usage: objectstack generate <type> <name>"));
2474
+ console.log(chalk10.dim(" Usage: objectstack generate types [config]"));
2475
+ return;
2476
+ }
2477
+ if (!name) {
2478
+ printError("Missing required argument: <name>");
2479
+ console.log(chalk10.dim(" Usage: objectstack generate <type> <name>"));
2480
+ process.exit(1);
2481
+ }
2482
+ await generateMetadataCommand.parseAsync([type, name, ...process.argv.slice(4)], { from: "user" });
2483
+ });
2484
+
2485
+ // src/commands/plugin.ts
2486
+ import { Command as Command12 } from "commander";
2487
+ import chalk11 from "chalk";
2488
+ import fs10 from "fs";
2489
+ import path10 from "path";
2490
+ function resolvePluginName(plugin) {
2491
+ if (typeof plugin === "string") return plugin;
2492
+ if (plugin && typeof plugin === "object") {
2493
+ const p = plugin;
2494
+ if (typeof p.name === "string") return p.name;
2495
+ if (p.constructor && p.constructor.name !== "Object") return p.constructor.name;
2496
+ }
2497
+ return "unknown";
2498
+ }
2499
+ function resolvePluginVersion(plugin) {
2500
+ if (plugin && typeof plugin === "object") {
2501
+ const p = plugin;
2502
+ if (typeof p.version === "string") return p.version;
2503
+ }
2504
+ return "-";
2505
+ }
2506
+ function resolvePluginType(plugin) {
2507
+ if (plugin && typeof plugin === "object") {
2508
+ const p = plugin;
2509
+ if (typeof p.type === "string") return p.type;
2510
+ }
2511
+ return "standard";
2512
+ }
2513
+ function readConfigText(configPath) {
2514
+ return fs10.readFileSync(configPath, "utf-8");
2515
+ }
2516
+ function addPluginToConfig(configPath, packageName) {
2517
+ let content = readConfigText(configPath);
2518
+ const shortName = packageName.replace(/^@[^/]+\//, "").replace(/^plugin-/, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
2519
+ const varName = shortName.replace(/-([a-z])/g, (_, c) => c.toUpperCase()) + "Plugin";
2520
+ const importLine = `import ${varName} from '${packageName}';
2521
+ `;
2522
+ if (content.includes(packageName)) {
2523
+ throw new Error(`Plugin '${packageName}' is already referenced in the config`);
2524
+ }
2525
+ const importRegex = /^import .+$/gm;
2526
+ let lastImportEnd = 0;
2527
+ let match;
2528
+ while ((match = importRegex.exec(content)) !== null) {
2529
+ lastImportEnd = match.index + match[0].length;
2530
+ }
2531
+ if (lastImportEnd > 0) {
2532
+ content = content.slice(0, lastImportEnd) + "\n" + importLine + content.slice(lastImportEnd);
2533
+ } else {
2534
+ content = importLine + "\n" + content;
1851
2535
  }
1852
2536
  if (/plugins\s*:\s*\[/.test(content)) {
1853
2537
  let replaced = false;
@@ -2043,8 +2727,897 @@ var removeCommand = new Command12("remove").alias("rm").description("Remove a pl
2043
2727
  });
2044
2728
  var pluginCommand = new Command12("plugin").description("Manage plugins (list, info, add, remove)").addCommand(listCommand).addCommand(infoSubCommand).addCommand(addCommand).addCommand(removeCommand);
2045
2729
 
2046
- // src/utils/plugin-commands.ts
2730
+ // src/commands/diff.ts
2731
+ import { Command as Command13 } from "commander";
2047
2732
  import chalk12 from "chalk";
2733
+ function getNames(items) {
2734
+ const map = /* @__PURE__ */ new Map();
2735
+ if (!Array.isArray(items)) return map;
2736
+ for (const item of items) {
2737
+ if (item?.name) map.set(item.name, item);
2738
+ }
2739
+ return map;
2740
+ }
2741
+ function getFieldNames(obj) {
2742
+ if (!obj?.fields || typeof obj.fields !== "object") return [];
2743
+ return Object.keys(obj.fields);
2744
+ }
2745
+ function getFieldType(obj, fieldName) {
2746
+ return obj?.fields?.[fieldName]?.type;
2747
+ }
2748
+ function isFieldRequired(obj, fieldName) {
2749
+ return obj?.fields?.[fieldName]?.required === true;
2750
+ }
2751
+ function diffNamedArrays(beforeItems, afterItems, category, detectFieldChanges) {
2752
+ const entries = [];
2753
+ const beforeMap = getNames(beforeItems);
2754
+ const afterMap = getNames(afterItems);
2755
+ for (const [name] of beforeMap) {
2756
+ if (!afterMap.has(name)) {
2757
+ entries.push({
2758
+ type: "removed",
2759
+ category,
2760
+ name,
2761
+ breaking: true
2762
+ });
2763
+ }
2764
+ }
2765
+ for (const [name] of afterMap) {
2766
+ if (!beforeMap.has(name)) {
2767
+ entries.push({
2768
+ type: "added",
2769
+ category,
2770
+ name,
2771
+ breaking: false
2772
+ });
2773
+ }
2774
+ }
2775
+ if (detectFieldChanges) {
2776
+ for (const [name, beforeObj] of beforeMap) {
2777
+ const afterObj = afterMap.get(name);
2778
+ if (!afterObj) continue;
2779
+ const beforeFields = getFieldNames(beforeObj);
2780
+ const afterFields = getFieldNames(afterObj);
2781
+ const beforeSet = new Set(beforeFields);
2782
+ const afterSet = new Set(afterFields);
2783
+ for (const f of beforeFields) {
2784
+ if (!afterSet.has(f)) {
2785
+ entries.push({
2786
+ type: "removed",
2787
+ category: `${category}.${name}.fields`,
2788
+ name: f,
2789
+ breaking: true,
2790
+ detail: "field removed"
2791
+ });
2792
+ }
2793
+ }
2794
+ for (const f of afterFields) {
2795
+ if (!beforeSet.has(f)) {
2796
+ const breaking = isFieldRequired(afterObj, f);
2797
+ entries.push({
2798
+ type: "added",
2799
+ category: `${category}.${name}.fields`,
2800
+ name: f,
2801
+ breaking,
2802
+ detail: breaking ? "required field added" : "optional field added"
2803
+ });
2804
+ }
2805
+ }
2806
+ for (const f of beforeFields) {
2807
+ if (!afterSet.has(f)) continue;
2808
+ const oldType = getFieldType(beforeObj, f);
2809
+ const newType = getFieldType(afterObj, f);
2810
+ if (oldType && newType && oldType !== newType) {
2811
+ entries.push({
2812
+ type: "modified",
2813
+ category: `${category}.${name}.fields`,
2814
+ name: f,
2815
+ breaking: true,
2816
+ detail: `type changed: ${oldType} \u2192 ${newType}`
2817
+ });
2818
+ }
2819
+ }
2820
+ if (beforeObj.label !== afterObj.label) {
2821
+ entries.push({
2822
+ type: "modified",
2823
+ category,
2824
+ name,
2825
+ breaking: false,
2826
+ detail: `label changed: "${beforeObj.label ?? "(none)"}" \u2192 "${afterObj.label ?? "(none)"}"`
2827
+ });
2828
+ }
2829
+ }
2830
+ }
2831
+ return entries;
2832
+ }
2833
+ var diffCommand = new Command13("diff").description("Compare two ObjectStack configurations and detect breaking changes").argument("[before]", 'Path to the "before" config file').argument("[after]", 'Path to the "after" config file').option("--before <path>", 'Path to the "before" config (alternative)').option("--after <path>", 'Path to the "after" config (alternative)').option("--json", "Output as JSON").option("--breaking-only", "Show only breaking changes").action(async (beforeArg, afterArg, options) => {
2834
+ const timer = createTimer();
2835
+ const beforePath = beforeArg || options.before;
2836
+ const afterPath = afterArg || options.after;
2837
+ if (!beforePath || !afterPath) {
2838
+ printError("Two config file paths are required.");
2839
+ console.log("");
2840
+ console.log(chalk12.dim(" Usage: objectstack diff <before> <after>"));
2841
+ console.log(chalk12.dim(" or: objectstack diff --before path1 --after path2"));
2842
+ process.exit(1);
2843
+ }
2844
+ if (!options.json) {
2845
+ printHeader("Diff");
2846
+ printStep("Loading configurations...");
2847
+ }
2848
+ try {
2849
+ const { config: beforeConfig } = await loadConfig(beforePath);
2850
+ const { config: afterConfig } = await loadConfig(afterPath);
2851
+ if (!options.json) {
2852
+ printInfo(`Before: ${chalk12.white(beforePath)}`);
2853
+ printInfo(`After: ${chalk12.white(afterPath)}`);
2854
+ }
2855
+ const allDiffs = [];
2856
+ allDiffs.push(...diffNamedArrays(beforeConfig.objects, afterConfig.objects, "objects", true));
2857
+ const simpleCats = [
2858
+ { key: "views", label: "views" },
2859
+ { key: "flows", label: "flows" },
2860
+ { key: "agents", label: "agents" },
2861
+ { key: "apps", label: "apps" },
2862
+ { key: "dashboards", label: "dashboards" },
2863
+ { key: "actions", label: "actions" },
2864
+ { key: "workflows", label: "workflows" },
2865
+ { key: "apis", label: "apis" },
2866
+ { key: "roles", label: "roles" }
2867
+ ];
2868
+ for (const cat of simpleCats) {
2869
+ allDiffs.push(
2870
+ ...diffNamedArrays(beforeConfig[cat.key], afterConfig[cat.key], cat.label, false)
2871
+ );
2872
+ }
2873
+ const diffs = options.breakingOnly ? allDiffs.filter((d) => d.breaking) : allDiffs;
2874
+ const breakingCount = allDiffs.filter((d) => d.breaking).length;
2875
+ if (options.json) {
2876
+ console.log(JSON.stringify({
2877
+ before: beforePath,
2878
+ after: afterPath,
2879
+ total: diffs.length,
2880
+ breaking: breakingCount,
2881
+ changes: diffs,
2882
+ duration: timer.elapsed()
2883
+ }, null, 2));
2884
+ return;
2885
+ }
2886
+ console.log("");
2887
+ if (diffs.length === 0) {
2888
+ printSuccess(options.breakingOnly ? "No breaking changes detected." : "No changes detected.");
2889
+ console.log("");
2890
+ return;
2891
+ }
2892
+ const grouped = /* @__PURE__ */ new Map();
2893
+ for (const d of diffs) {
2894
+ const key = d.category;
2895
+ if (!grouped.has(key)) grouped.set(key, []);
2896
+ grouped.get(key).push(d);
2897
+ }
2898
+ for (const [category, items] of grouped) {
2899
+ console.log(` ${chalk12.bold(category)}`);
2900
+ for (const item of items) {
2901
+ const icon = item.type === "added" ? "+" : item.type === "removed" ? "-" : "~";
2902
+ const color = item.type === "added" ? chalk12.green : item.type === "removed" ? chalk12.red : chalk12.yellow;
2903
+ const breakingTag = item.breaking ? chalk12.bgRed.white(" BREAKING ") + " " : "";
2904
+ const detail = item.detail ? chalk12.dim(` (${item.detail})`) : "";
2905
+ console.log(` ${color(icon)} ${breakingTag}${color(item.name)}${detail}`);
2906
+ }
2907
+ console.log("");
2908
+ }
2909
+ if (breakingCount > 0) {
2910
+ printError(`${breakingCount} breaking change(s) detected`);
2911
+ } else {
2912
+ printSuccess("No breaking changes");
2913
+ }
2914
+ console.log(chalk12.dim(` ${diffs.length} total change(s) in ${timer.display()}`));
2915
+ console.log("");
2916
+ } catch (error) {
2917
+ if (options.json) {
2918
+ console.log(JSON.stringify({ error: error.message }));
2919
+ process.exit(1);
2920
+ }
2921
+ console.log("");
2922
+ printError(error.message || String(error));
2923
+ process.exit(1);
2924
+ }
2925
+ });
2926
+
2927
+ // src/commands/lint.ts
2928
+ import { Command as Command14 } from "commander";
2929
+ import chalk13 from "chalk";
2930
+ var SNAKE_CASE_RE = /^[a-z][a-z0-9_]*$/;
2931
+ function checkSnakeCase(value, path12, label) {
2932
+ if (!SNAKE_CASE_RE.test(value)) {
2933
+ return {
2934
+ severity: "error",
2935
+ rule: "naming/snake-case",
2936
+ message: `${label} "${value}" must be snake_case`,
2937
+ path: path12,
2938
+ fix: value.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z\d])([A-Z])/g, "$1_$2").toLowerCase().replace(/^_/, "").replace(/-/g, "_")
2939
+ };
2940
+ }
2941
+ return null;
2942
+ }
2943
+ function checkLabelExists(item, path12, kind) {
2944
+ if (!item.label) {
2945
+ return {
2946
+ severity: "error",
2947
+ rule: "required/label",
2948
+ message: `${kind} "${item.name || "?"}" is missing a label`,
2949
+ path: path12
2950
+ };
2951
+ }
2952
+ return null;
2953
+ }
2954
+ function checkLabelCase(label, path12) {
2955
+ if (label && label[0] !== label[0].toUpperCase()) {
2956
+ return {
2957
+ severity: "warning",
2958
+ rule: "convention/label-case",
2959
+ message: `Label "${label}" should start with an uppercase letter`,
2960
+ path: path12,
2961
+ fix: label.charAt(0).toUpperCase() + label.slice(1)
2962
+ };
2963
+ }
2964
+ return null;
2965
+ }
2966
+ function lintConfig(config) {
2967
+ const issues = [];
2968
+ const push = (issue) => {
2969
+ if (issue) issues.push(issue);
2970
+ };
2971
+ const objects = Array.isArray(config.objects) ? config.objects : [];
2972
+ for (let i = 0; i < objects.length; i++) {
2973
+ const obj = objects[i];
2974
+ const objPath = `objects[${i}]`;
2975
+ if (obj.name) {
2976
+ push(checkSnakeCase(obj.name, `${objPath}.name`, "Object name"));
2977
+ }
2978
+ push(checkLabelExists(obj, `${objPath}.label`, "Object"));
2979
+ if (obj.label) {
2980
+ push(checkLabelCase(obj.label, `${objPath}.label`));
2981
+ }
2982
+ if (obj.fields && typeof obj.fields === "object") {
2983
+ const fieldNames = Object.keys(obj.fields);
2984
+ if (fieldNames.length === 0) {
2985
+ issues.push({
2986
+ severity: "warning",
2987
+ rule: "structure/empty-fields",
2988
+ message: `Object "${obj.name || "?"}" has an empty fields map`,
2989
+ path: `${objPath}.fields`
2990
+ });
2991
+ }
2992
+ for (const fieldName of fieldNames) {
2993
+ const field = obj.fields[fieldName];
2994
+ const fieldPath = `${objPath}.fields.${fieldName}`;
2995
+ push(checkSnakeCase(fieldName, fieldPath, "Field name"));
2996
+ if (field && typeof field === "object") {
2997
+ push(checkLabelExists({ ...field, name: fieldName }, `${fieldPath}.label`, "Field"));
2998
+ if (field.label) {
2999
+ push(checkLabelCase(field.label, `${fieldPath}.label`));
3000
+ }
3001
+ }
3002
+ }
3003
+ } else if (!obj.fields) {
3004
+ issues.push({
3005
+ severity: "error",
3006
+ rule: "structure/no-fields",
3007
+ message: `Object "${obj.name || "?"}" has no fields defined`,
3008
+ path: `${objPath}.fields`
3009
+ });
3010
+ }
3011
+ }
3012
+ const views = Array.isArray(config.views) ? config.views : [];
3013
+ for (let i = 0; i < views.length; i++) {
3014
+ const view = views[i];
3015
+ const viewPath = `views[${i}]`;
3016
+ if (view.name) {
3017
+ push(checkSnakeCase(view.name, `${viewPath}.name`, "View name"));
3018
+ }
3019
+ push(checkLabelExists(view, `${viewPath}.label`, "View"));
3020
+ if (view.label) {
3021
+ push(checkLabelCase(view.label, `${viewPath}.label`));
3022
+ }
3023
+ }
3024
+ const apps = Array.isArray(config.apps) ? config.apps : [];
3025
+ for (let i = 0; i < apps.length; i++) {
3026
+ const app = apps[i];
3027
+ const appPath = `apps[${i}]`;
3028
+ if (app.name) {
3029
+ push(checkSnakeCase(app.name, `${appPath}.name`, "App name"));
3030
+ }
3031
+ push(checkLabelExists(app, `${appPath}.label`, "App"));
3032
+ if (app.label) {
3033
+ push(checkLabelCase(app.label, `${appPath}.label`));
3034
+ }
3035
+ }
3036
+ const flows = Array.isArray(config.flows) ? config.flows : [];
3037
+ for (let i = 0; i < flows.length; i++) {
3038
+ const flow = flows[i];
3039
+ const flowPath = `flows[${i}]`;
3040
+ if (flow.name) {
3041
+ push(checkSnakeCase(flow.name, `${flowPath}.name`, "Flow name"));
3042
+ }
3043
+ }
3044
+ const agents = Array.isArray(config.agents) ? config.agents : [];
3045
+ for (let i = 0; i < agents.length; i++) {
3046
+ const agent = agents[i];
3047
+ const agentPath = `agents[${i}]`;
3048
+ if (agent.name) {
3049
+ push(checkSnakeCase(agent.name, `${agentPath}.name`, "Agent name"));
3050
+ }
3051
+ }
3052
+ return issues;
3053
+ }
3054
+ var lintCommand = new Command14("lint").description("Check ObjectStack configuration for style and convention issues").argument("[config]", "Configuration file path").option("--json", "Output as JSON").option("--fix", "Show what would be fixed (dry-run)").action(async (configPath, options) => {
3055
+ const timer = createTimer();
3056
+ if (!options.json) {
3057
+ printHeader("Lint");
3058
+ printStep("Loading configuration...");
3059
+ }
3060
+ try {
3061
+ const { config, absolutePath } = await loadConfig(configPath);
3062
+ if (!options.json) {
3063
+ printInfo(`Config: ${chalk13.white(absolutePath)}`);
3064
+ }
3065
+ const issues = lintConfig(config);
3066
+ if (options.json) {
3067
+ const errors2 = issues.filter((i) => i.severity === "error");
3068
+ const warnings2 = issues.filter((i) => i.severity === "warning");
3069
+ const suggestions2 = issues.filter((i) => i.severity === "suggestion");
3070
+ console.log(JSON.stringify({
3071
+ passed: errors2.length === 0,
3072
+ total: issues.length,
3073
+ errors: errors2.length,
3074
+ warnings: warnings2.length,
3075
+ suggestions: suggestions2.length,
3076
+ issues,
3077
+ duration: timer.elapsed()
3078
+ }, null, 2));
3079
+ if (errors2.length > 0) process.exit(1);
3080
+ return;
3081
+ }
3082
+ console.log("");
3083
+ if (issues.length === 0) {
3084
+ printSuccess(`All checks passed ${chalk13.dim(`(${timer.display()})`)}`);
3085
+ console.log("");
3086
+ return;
3087
+ }
3088
+ const errors = issues.filter((i) => i.severity === "error");
3089
+ const warnings = issues.filter((i) => i.severity === "warning");
3090
+ const suggestions = issues.filter((i) => i.severity === "suggestion");
3091
+ const printIssue = (issue) => {
3092
+ const color = issue.severity === "error" ? chalk13.red : issue.severity === "warning" ? chalk13.yellow : chalk13.blue;
3093
+ const icon = issue.severity === "error" ? "\u2717" : issue.severity === "warning" ? "\u26A0" : "\u2139";
3094
+ console.log(` ${color(icon)} ${color(issue.message)}`);
3095
+ console.log(chalk13.dim(` ${issue.rule} at ${issue.path}`));
3096
+ if (options.fix && issue.fix) {
3097
+ console.log(chalk13.green(` \u2192 fix: ${issue.fix}`));
3098
+ }
3099
+ };
3100
+ if (errors.length > 0) {
3101
+ console.log(chalk13.bold.red(` Errors (${errors.length})`));
3102
+ errors.forEach(printIssue);
3103
+ console.log("");
3104
+ }
3105
+ if (warnings.length > 0) {
3106
+ console.log(chalk13.bold.yellow(` Warnings (${warnings.length})`));
3107
+ warnings.forEach(printIssue);
3108
+ console.log("");
3109
+ }
3110
+ if (suggestions.length > 0) {
3111
+ console.log(chalk13.bold.blue(` Suggestions (${suggestions.length})`));
3112
+ suggestions.forEach(printIssue);
3113
+ console.log("");
3114
+ }
3115
+ const parts = [];
3116
+ if (errors.length > 0) parts.push(chalk13.red(`${errors.length} error(s)`));
3117
+ if (warnings.length > 0) parts.push(chalk13.yellow(`${warnings.length} warning(s)`));
3118
+ if (suggestions.length > 0) parts.push(chalk13.blue(`${suggestions.length} suggestion(s)`));
3119
+ console.log(` ${parts.join(", ")} ${chalk13.dim(`(${timer.display()})`)}`);
3120
+ if (options.fix) {
3121
+ console.log("");
3122
+ printInfo("Dry-run mode: no files were modified.");
3123
+ }
3124
+ console.log("");
3125
+ if (errors.length > 0) process.exit(1);
3126
+ } catch (error) {
3127
+ if (options.json) {
3128
+ console.log(JSON.stringify({ error: error.message }));
3129
+ process.exit(1);
3130
+ }
3131
+ console.log("");
3132
+ printError(error.message || String(error));
3133
+ process.exit(1);
3134
+ }
3135
+ });
3136
+
3137
+ // src/commands/explain.ts
3138
+ import { Command as Command15 } from "commander";
3139
+ import chalk14 from "chalk";
3140
+ var SCHEMAS = {
3141
+ object: {
3142
+ name: "Object",
3143
+ description: "Defines a data entity in the ObjectStack data model. Objects contain fields, enable capabilities, and form the foundation of the metadata-driven platform.",
3144
+ required: [
3145
+ { name: "name", type: "string (snake_case)", description: "Machine name identifier" },
3146
+ { name: "fields", type: "Record<string, Field>", description: "Map of field definitions" }
3147
+ ],
3148
+ optional: [
3149
+ { name: "label", type: "string", description: "Human-readable display name" },
3150
+ { name: "pluralLabel", type: "string", description: "Plural display name" },
3151
+ { name: "description", type: "string", description: "Documentation for the object" },
3152
+ { name: "ownership", type: '"own" | "extend"', description: "Whether this object is owned or extended" },
3153
+ { name: "enable", type: "ObjectCapabilities", description: "Feature flags (trackHistory, apiEnabled, etc.)" },
3154
+ { name: "icon", type: "string", description: "Icon identifier for UI display" }
3155
+ ],
3156
+ example: `{
3157
+ name: 'project_task',
3158
+ label: 'Project Task',
3159
+ fields: {
3160
+ title: { type: 'text', label: 'Title', required: true },
3161
+ status: { type: 'select', label: 'Status', options: ['open', 'closed'] },
3162
+ assigned_to: { type: 'lookup', label: 'Assigned To', reference: 'user' },
3163
+ },
3164
+ enable: { trackHistory: true, apiEnabled: true },
3165
+ }`,
3166
+ related: ["field", "view", "flow", "query"],
3167
+ docsPath: "data/object"
3168
+ },
3169
+ field: {
3170
+ name: "Field",
3171
+ description: "Defines a property on an Object. Fields have types that control validation, storage, and UI rendering.",
3172
+ required: [
3173
+ { name: "type", type: "FieldType", description: "Field data type (text, number, boolean, select, lookup, etc.)" }
3174
+ ],
3175
+ optional: [
3176
+ { name: "label", type: "string", description: "Human-readable display name" },
3177
+ { name: "required", type: "boolean", description: "Whether the field is mandatory" },
3178
+ { name: "multiple", type: "boolean", description: "Whether the field holds an array of values" },
3179
+ { name: "defaultValue", type: "any", description: "Default value for new records" },
3180
+ { name: "maxLength", type: "number", description: "Maximum character length (text fields)" },
3181
+ { name: "reference", type: "string", description: "Target object name (lookup fields)" },
3182
+ { name: "options", type: "string[]", description: "Available choices (select fields)" }
3183
+ ],
3184
+ example: `{
3185
+ type: 'text',
3186
+ label: 'Email Address',
3187
+ required: true,
3188
+ maxLength: 255,
3189
+ }`,
3190
+ related: ["object", "view", "query"],
3191
+ docsPath: "data/field"
3192
+ },
3193
+ view: {
3194
+ name: "View",
3195
+ description: "Defines how data is displayed and interacted with. Views can be list views (grid, kanban, calendar) or form views (simple, tabbed, wizard).",
3196
+ required: [
3197
+ { name: "name", type: "string (snake_case)", description: "Machine name identifier" },
3198
+ { name: "object", type: "string", description: "Target object to display" },
3199
+ { name: "type", type: '"list" | "form"', description: "View category" }
3200
+ ],
3201
+ optional: [
3202
+ { name: "label", type: "string", description: "Display name" },
3203
+ { name: "layout", type: '"grid" | "kanban" | "calendar" | "gantt" | "simple" | "tabbed" | "wizard"', description: "Layout style" },
3204
+ { name: "columns", type: "string[]", description: "Visible fields for list views" },
3205
+ { name: "filters", type: "Filter[]", description: "Default query filters" },
3206
+ { name: "sort", type: "SortConfig", description: "Default sort configuration" }
3207
+ ],
3208
+ example: `{
3209
+ name: 'task_board',
3210
+ object: 'project_task',
3211
+ type: 'list',
3212
+ label: 'Task Board',
3213
+ layout: 'kanban',
3214
+ columns: ['title', 'status', 'assigned_to'],
3215
+ }`,
3216
+ related: ["object", "app", "action", "dashboard"],
3217
+ docsPath: "ui/view"
3218
+ },
3219
+ flow: {
3220
+ name: "Flow",
3221
+ description: "Visual logic orchestration for business processes. Flows can be auto-launched, screen-based, or scheduled.",
3222
+ required: [
3223
+ { name: "name", type: "string (snake_case)", description: "Machine name identifier" },
3224
+ { name: "type", type: '"autolaunched" | "screen" | "schedule"', description: "Trigger type" }
3225
+ ],
3226
+ optional: [
3227
+ { name: "label", type: "string", description: "Display name" },
3228
+ { name: "description", type: "string", description: "Documentation for the flow" },
3229
+ { name: "trigger", type: "TriggerConfig", description: "Event that starts the flow" },
3230
+ { name: "steps", type: "FlowStep[]", description: "Sequence of actions" },
3231
+ { name: "variables", type: "Variable[]", description: "Flow-scoped variables" }
3232
+ ],
3233
+ example: `{
3234
+ name: 'assign_on_create',
3235
+ type: 'autolaunched',
3236
+ label: 'Auto-Assign on Create',
3237
+ trigger: { object: 'project_task', event: 'afterInsert' },
3238
+ steps: [
3239
+ { type: 'assignment', field: 'assigned_to', value: '$currentUser' },
3240
+ ],
3241
+ }`,
3242
+ related: ["object", "trigger", "workflow", "agent"],
3243
+ docsPath: "automation/flow"
3244
+ },
3245
+ agent: {
3246
+ name: "Agent",
3247
+ description: "Autonomous AI actor that can perform tasks using tools, instructions, and context from the ObjectStack data model.",
3248
+ required: [
3249
+ { name: "name", type: "string (snake_case)", description: "Machine name identifier" },
3250
+ { name: "role", type: "string", description: "Agent purpose description" }
3251
+ ],
3252
+ optional: [
3253
+ { name: "instructions", type: "string", description: "System prompt / behavioral instructions" },
3254
+ { name: "tools", type: "ToolReference[]", description: "Tools the agent can invoke" },
3255
+ { name: "model", type: "string", description: "LLM model to use" },
3256
+ { name: "objects", type: "string[]", description: "Objects the agent can access" },
3257
+ { name: "temperature", type: "number", description: "Creativity parameter (0-1)" }
3258
+ ],
3259
+ example: `{
3260
+ name: 'support_agent',
3261
+ role: 'Customer Support Assistant',
3262
+ instructions: 'Help users resolve issues by searching the knowledge base.',
3263
+ tools: [{ name: 'query', object: 'knowledge_article' }],
3264
+ model: 'gpt-4o',
3265
+ }`,
3266
+ related: ["object", "flow", "query"],
3267
+ docsPath: "ai/agent"
3268
+ },
3269
+ app: {
3270
+ name: "App",
3271
+ description: "Application shell that groups navigation, branding, and views into a cohesive user experience.",
3272
+ required: [
3273
+ { name: "name", type: "string (snake_case)", description: "Machine name identifier" }
3274
+ ],
3275
+ optional: [
3276
+ { name: "label", type: "string", description: "Display name" },
3277
+ { name: "description", type: "string", description: "App description" },
3278
+ { name: "navigation", type: "NavItem[]", description: "Menu tree structure" },
3279
+ { name: "logo", type: "string", description: "Logo URL or asset path" },
3280
+ { name: "theme", type: "string", description: "Theme reference" },
3281
+ { name: "defaultRoute", type: "string", description: "Landing page route" }
3282
+ ],
3283
+ example: `{
3284
+ name: 'project_manager',
3285
+ label: 'Project Manager',
3286
+ navigation: [
3287
+ { type: 'object', object: 'project_task', label: 'Tasks' },
3288
+ { type: 'dashboard', dashboard: 'project_overview', label: 'Overview' },
3289
+ ],
3290
+ }`,
3291
+ related: ["view", "dashboard", "action", "object"],
3292
+ docsPath: "ui/app"
3293
+ },
3294
+ query: {
3295
+ name: "Query",
3296
+ description: "Declarative data retrieval definition used for fetching and filtering records from objects.",
3297
+ required: [
3298
+ { name: "object", type: "string", description: "Target object to query" }
3299
+ ],
3300
+ optional: [
3301
+ { name: "fields", type: "string[]", description: "Fields to select" },
3302
+ { name: "filters", type: "Filter[]", description: "Where conditions" },
3303
+ { name: "sort", type: "SortConfig[]", description: "Order by configuration" },
3304
+ { name: "limit", type: "number", description: "Maximum records to return" },
3305
+ { name: "offset", type: "number", description: "Pagination offset" }
3306
+ ],
3307
+ example: `{
3308
+ object: 'project_task',
3309
+ fields: ['title', 'status', 'assigned_to'],
3310
+ filters: [{ field: 'status', operator: 'eq', value: 'open' }],
3311
+ sort: [{ field: 'created_at', direction: 'desc' }],
3312
+ limit: 50,
3313
+ }`,
3314
+ related: ["object", "field", "view"],
3315
+ docsPath: "data/query"
3316
+ },
3317
+ dashboard: {
3318
+ name: "Dashboard",
3319
+ description: "Grid-layout container for widgets that display aggregated data, charts, and key metrics.",
3320
+ required: [
3321
+ { name: "name", type: "string (snake_case)", description: "Machine name identifier" }
3322
+ ],
3323
+ optional: [
3324
+ { name: "label", type: "string", description: "Display name" },
3325
+ { name: "widgets", type: "Widget[]", description: "Dashboard widget definitions" },
3326
+ { name: "layout", type: "GridLayout", description: "Widget positioning" },
3327
+ { name: "refreshInterval", type: "number", description: "Auto-refresh interval in seconds" }
3328
+ ],
3329
+ example: `{
3330
+ name: 'project_overview',
3331
+ label: 'Project Overview',
3332
+ widgets: [
3333
+ { type: 'chart', object: 'project_task', groupBy: 'status' },
3334
+ { type: 'metric', object: 'project_task', aggregate: 'count' },
3335
+ ],
3336
+ }`,
3337
+ related: ["app", "view", "object"],
3338
+ docsPath: "ui/dashboard"
3339
+ },
3340
+ action: {
3341
+ name: "Action",
3342
+ description: "User-triggered operation such as a button click, URL redirect, or screen flow launch.",
3343
+ required: [
3344
+ { name: "name", type: "string (snake_case)", description: "Machine name identifier" },
3345
+ { name: "type", type: '"button" | "url" | "flow" | "api"', description: "Action type" }
3346
+ ],
3347
+ optional: [
3348
+ { name: "label", type: "string", description: "Display text" },
3349
+ { name: "icon", type: "string", description: "Button icon" },
3350
+ { name: "object", type: "string", description: "Target object context" },
3351
+ { name: "flow", type: "string", description: "Flow to launch (for flow actions)" },
3352
+ { name: "url", type: "string", description: "Target URL (for url actions)" },
3353
+ { name: "confirmation", type: "string", description: "Confirmation dialog message" }
3354
+ ],
3355
+ example: `{
3356
+ name: 'close_task',
3357
+ type: 'flow',
3358
+ label: 'Close Task',
3359
+ object: 'project_task',
3360
+ flow: 'close_task_flow',
3361
+ confirmation: 'Are you sure you want to close this task?',
3362
+ }`,
3363
+ related: ["flow", "view", "app"],
3364
+ docsPath: "ui/action"
3365
+ },
3366
+ workflow: {
3367
+ name: "Workflow",
3368
+ description: "State machine and approval process that governs record lifecycle transitions.",
3369
+ required: [
3370
+ { name: "name", type: "string (snake_case)", description: "Machine name identifier" },
3371
+ { name: "object", type: "string", description: "Target object" }
3372
+ ],
3373
+ optional: [
3374
+ { name: "label", type: "string", description: "Display name" },
3375
+ { name: "states", type: "State[]", description: "Defined workflow states" },
3376
+ { name: "transitions", type: "Transition[]", description: "Allowed state transitions" },
3377
+ { name: "approvers", type: "ApproverConfig", description: "Approval chain configuration" }
3378
+ ],
3379
+ example: `{
3380
+ name: 'task_approval',
3381
+ object: 'project_task',
3382
+ label: 'Task Approval',
3383
+ states: ['draft', 'pending', 'approved', 'rejected'],
3384
+ transitions: [
3385
+ { from: 'draft', to: 'pending', action: 'submit' },
3386
+ { from: 'pending', to: 'approved', action: 'approve' },
3387
+ { from: 'pending', to: 'rejected', action: 'reject' },
3388
+ ],
3389
+ }`,
3390
+ related: ["object", "flow", "action"],
3391
+ docsPath: "automation/workflow"
3392
+ },
3393
+ trigger: {
3394
+ name: "Trigger",
3395
+ description: "Event-driven automation hook that fires when specific data events occur on an object.",
3396
+ required: [
3397
+ { name: "name", type: "string (snake_case)", description: "Machine name identifier" },
3398
+ { name: "object", type: "string", description: "Target object to monitor" },
3399
+ { name: "event", type: '"beforeInsert" | "afterInsert" | "beforeUpdate" | "afterUpdate" | "beforeDelete" | "afterDelete"', description: "Event type" }
3400
+ ],
3401
+ optional: [
3402
+ { name: "label", type: "string", description: "Display name" },
3403
+ { name: "condition", type: "FilterExpression", description: "Conditional guard" },
3404
+ { name: "flow", type: "string", description: "Flow to execute" },
3405
+ { name: "async", type: "boolean", description: "Whether to execute asynchronously" }
3406
+ ],
3407
+ example: `{
3408
+ name: 'task_created_notify',
3409
+ object: 'project_task',
3410
+ event: 'afterInsert',
3411
+ label: 'Notify on Task Creation',
3412
+ flow: 'send_task_notification',
3413
+ async: true,
3414
+ }`,
3415
+ related: ["object", "flow", "workflow"],
3416
+ docsPath: "automation/trigger"
3417
+ }
3418
+ };
3419
+ var explainCommand = new Command15("explain").description("Display human-readable explanation of an ObjectStack schema").argument("[schema]", "Schema name (e.g., object, field, view, flow, agent, app)").option("--json", "Output as JSON").action(async (schemaName, options) => {
3420
+ if (!schemaName) {
3421
+ if (options.json) {
3422
+ console.log(JSON.stringify({
3423
+ schemas: Object.entries(SCHEMAS).map(([key, s]) => ({
3424
+ name: key,
3425
+ description: s.description
3426
+ }))
3427
+ }, null, 2));
3428
+ return;
3429
+ }
3430
+ printHeader("Available Schemas");
3431
+ console.log("");
3432
+ for (const [key, schema2] of Object.entries(SCHEMAS)) {
3433
+ const desc = schema2.description;
3434
+ console.log(` ${chalk14.bold.cyan(key.padEnd(12))} ${chalk14.dim(desc.length > 70 ? desc.slice(0, 70) + "..." : desc)}`);
3435
+ }
3436
+ console.log("");
3437
+ printInfo(`Run ${chalk14.white("objectstack explain <schema>")} for details.`);
3438
+ console.log("");
3439
+ return;
3440
+ }
3441
+ const schema = SCHEMAS[schemaName.toLowerCase()];
3442
+ if (!schema) {
3443
+ if (options.json) {
3444
+ console.log(JSON.stringify({ error: `Unknown schema: ${schemaName}` }));
3445
+ process.exit(1);
3446
+ }
3447
+ printError(`Unknown schema: "${schemaName}"`);
3448
+ console.log("");
3449
+ printInfo(`Available schemas: ${Object.keys(SCHEMAS).join(", ")}`);
3450
+ console.log("");
3451
+ process.exit(1);
3452
+ }
3453
+ if (options.json) {
3454
+ console.log(JSON.stringify(schema, null, 2));
3455
+ return;
3456
+ }
3457
+ printHeader(`Schema: ${schema.name}`);
3458
+ console.log("");
3459
+ console.log(` ${schema.description}`);
3460
+ console.log("");
3461
+ console.log(chalk14.bold(" Required Properties:"));
3462
+ for (const prop of schema.required) {
3463
+ console.log(` ${chalk14.green(prop.name.padEnd(18))} ${chalk14.dim(prop.type.padEnd(30))} ${prop.description}`);
3464
+ }
3465
+ if (schema.optional.length > 0) {
3466
+ console.log("");
3467
+ console.log(chalk14.bold(" Optional Properties:"));
3468
+ for (const prop of schema.optional) {
3469
+ console.log(` ${chalk14.yellow(prop.name.padEnd(18))} ${chalk14.dim(prop.type.padEnd(30))} ${prop.description}`);
3470
+ }
3471
+ }
3472
+ console.log("");
3473
+ console.log(chalk14.bold(" Example:"));
3474
+ for (const line of schema.example.split("\n")) {
3475
+ console.log(chalk14.dim(` ${line}`));
3476
+ }
3477
+ console.log("");
3478
+ printKV(" Related", schema.related.map((r) => chalk14.cyan(r)).join(", "));
3479
+ printKV(" Docs", `https://objectstack.dev/docs/${schema.docsPath}`);
3480
+ console.log("");
3481
+ });
3482
+
3483
+ // src/commands/codemod.ts
3484
+ import { Command as Command16 } from "commander";
3485
+ import chalk15 from "chalk";
3486
+ import fs11 from "fs";
3487
+ import path11 from "path";
3488
+ var V2_TO_V3_TRANSFORMS = [
3489
+ {
3490
+ pattern: /\bEnhancedObjectKernel\b/g,
3491
+ replacement: "ObjectKernel",
3492
+ description: "EnhancedObjectKernel \u2192 ObjectKernel"
3493
+ },
3494
+ {
3495
+ pattern: /\bmax_length\b/g,
3496
+ replacement: "maxLength",
3497
+ description: "max_length \u2192 maxLength"
3498
+ },
3499
+ {
3500
+ pattern: /\breference_filters\b/g,
3501
+ replacement: "referenceFilters",
3502
+ description: "reference_filters \u2192 referenceFilters"
3503
+ },
3504
+ {
3505
+ pattern: /\bdefault_value\b/g,
3506
+ replacement: "defaultValue",
3507
+ description: "default_value \u2192 defaultValue"
3508
+ },
3509
+ {
3510
+ pattern: /\bmin_length\b/g,
3511
+ replacement: "minLength",
3512
+ description: "min_length \u2192 minLength"
3513
+ },
3514
+ {
3515
+ pattern: /\bunique_name\b/g,
3516
+ replacement: "uniqueName",
3517
+ description: "unique_name \u2192 uniqueName"
3518
+ },
3519
+ {
3520
+ pattern: /from\s+['"]@objectstack\/core\/enhanced['"]/g,
3521
+ replacement: "from '@objectstack/core'",
3522
+ description: "Update import from @objectstack/core/enhanced"
3523
+ },
3524
+ {
3525
+ pattern: /from\s+['"]@objectstack\/spec\/dist\/[^'"]+['"]/g,
3526
+ replacement: "from '@objectstack/spec'",
3527
+ description: "Update deep import from @objectstack/spec/dist/"
3528
+ }
3529
+ ];
3530
+ function walkDir2(dir, ext) {
3531
+ const results = [];
3532
+ if (!fs11.existsSync(dir)) return results;
3533
+ const entries = fs11.readdirSync(dir, { withFileTypes: true });
3534
+ for (const entry of entries) {
3535
+ if (entry.name === "node_modules") continue;
3536
+ const fullPath = path11.join(dir, entry.name);
3537
+ if (entry.isDirectory()) {
3538
+ results.push(...walkDir2(fullPath, ext));
3539
+ } else if (entry.name.endsWith(ext)) {
3540
+ results.push(fullPath);
3541
+ }
3542
+ }
3543
+ return results;
3544
+ }
3545
+ var v2ToV3Command = new Command16("v2-to-v3").description("Migrate ObjectStack v2 code patterns to v3").option("--dir <directory>", "Directory to scan", "src/").option("--dry-run", "Show changes without writing files").action(async (options) => {
3546
+ printHeader("Codemod: v2 \u2192 v3");
3547
+ const timer = createTimer();
3548
+ const dir = path11.resolve(process.cwd(), options.dir);
3549
+ if (!fs11.existsSync(dir)) {
3550
+ printError(`Directory not found: ${dir}`);
3551
+ process.exit(1);
3552
+ }
3553
+ console.log(` ${chalk15.dim("Directory:")} ${chalk15.white(options.dir)}`);
3554
+ console.log(` ${chalk15.dim("Dry run:")} ${chalk15.white(options.dryRun ? "yes" : "no")}`);
3555
+ console.log("");
3556
+ printStep("Scanning TypeScript files...");
3557
+ const files = walkDir2(dir, ".ts");
3558
+ if (files.length === 0) {
3559
+ printInfo("No .ts files found in directory");
3560
+ return;
3561
+ }
3562
+ printInfo(`Found ${files.length} TypeScript file(s)`);
3563
+ console.log("");
3564
+ let totalTransforms = 0;
3565
+ let filesModified = 0;
3566
+ const transformCounts = {};
3567
+ for (const file of files) {
3568
+ const original = fs11.readFileSync(file, "utf-8");
3569
+ let content = original;
3570
+ let fileTransforms = 0;
3571
+ for (const transform of V2_TO_V3_TRANSFORMS) {
3572
+ const matches = content.match(transform.pattern);
3573
+ if (matches) {
3574
+ const count = matches.length;
3575
+ content = content.replace(transform.pattern, transform.replacement);
3576
+ fileTransforms += count;
3577
+ transformCounts[transform.description] = (transformCounts[transform.description] || 0) + count;
3578
+ }
3579
+ }
3580
+ if (fileTransforms > 0) {
3581
+ const relPath = path11.relative(process.cwd(), file);
3582
+ filesModified++;
3583
+ totalTransforms += fileTransforms;
3584
+ if (options.dryRun) {
3585
+ printInfo(`${relPath} \u2014 ${fileTransforms} change(s)`);
3586
+ } else {
3587
+ fs11.writeFileSync(file, content);
3588
+ printSuccess(`${relPath} \u2014 ${fileTransforms} change(s)`);
3589
+ }
3590
+ }
3591
+ }
3592
+ console.log("");
3593
+ if (totalTransforms === 0) {
3594
+ printSuccess("No v2 patterns found \u2014 code is already v3 compatible");
3595
+ } else {
3596
+ console.log(chalk15.bold(" Summary:"));
3597
+ for (const [desc, count] of Object.entries(transformCounts)) {
3598
+ console.log(` ${chalk15.dim(desc)}: ${chalk15.white(count)}`);
3599
+ }
3600
+ console.log("");
3601
+ if (options.dryRun) {
3602
+ printInfo(`Would modify ${filesModified} file(s) with ${totalTransforms} total change(s)`);
3603
+ console.log(chalk15.dim(" Run without --dry-run to apply changes"));
3604
+ } else {
3605
+ printSuccess(`Modified ${filesModified} file(s) with ${totalTransforms} total change(s) (${timer.display()})`);
3606
+ }
3607
+ }
3608
+ console.log("");
3609
+ });
3610
+ var codemodCommand = new Command16("codemod").description("Run automated code transformations").addCommand(v2ToV3Command).action(() => {
3611
+ printHeader("Codemod");
3612
+ console.log(chalk15.bold(" Available codemods:"));
3613
+ console.log(` ${chalk15.cyan("v2-to-v3".padEnd(16))} Migrate ObjectStack v2 code patterns to v3`);
3614
+ console.log("");
3615
+ console.log(chalk15.dim(" Usage: objectstack codemod v2-to-v3 [--dir src/] [--dry-run]"));
3616
+ console.log("");
3617
+ });
3618
+
3619
+ // src/utils/plugin-commands.ts
3620
+ import chalk16 from "chalk";
2048
3621
  async function loadPluginCommands(program2) {
2049
3622
  let config;
2050
3623
  try {
@@ -2088,7 +3661,7 @@ async function loadPluginCommands(program2) {
2088
3661
  if (process.env.DEBUG) {
2089
3662
  const message = error instanceof Error ? error.message : String(error);
2090
3663
  console.error(
2091
- chalk12.yellow(` \u26A0 Failed to load CLI command '${contribution.name}' from plugin '${contribution.pluginName}': ${message}`)
3664
+ chalk16.yellow(` \u26A0 Failed to load CLI command '${contribution.name}' from plugin '${contribution.pluginName}': ${message}`)
2092
3665
  );
2093
3666
  }
2094
3667
  }
@@ -2134,30 +3707,30 @@ function resolvePluginName2(plugin) {
2134
3707
  var require2 = createRequire2(import.meta.url);
2135
3708
  var pkg = require2("../package.json");
2136
3709
  process.on("unhandledRejection", (err) => {
2137
- console.error(chalk13.red(`
3710
+ console.error(chalk17.red(`
2138
3711
  \u2717 Unhandled error: ${err?.message || err}`));
2139
3712
  if (err?.stack && process.env.DEBUG) {
2140
- console.error(chalk13.dim(err.stack));
3713
+ console.error(chalk17.dim(err.stack));
2141
3714
  }
2142
3715
  process.exit(1);
2143
3716
  });
2144
- var program = new Command13();
3717
+ var program = new Command17();
2145
3718
  program.name("objectstack").description("ObjectStack CLI \u2014 Build metadata-driven apps with the ObjectStack Protocol").version(pkg.version, "-v, --version").configureHelp({
2146
3719
  sortSubcommands: false
2147
3720
  }).addHelpText("before", `
2148
- ${chalk13.bold.cyan("\u25C6 ObjectStack CLI")} ${chalk13.dim(`v${pkg.version}`)}
3721
+ ${chalk17.bold.cyan("\u25C6 ObjectStack CLI")} ${chalk17.dim(`v${pkg.version}`)}
2149
3722
  `).addHelpText("after", `
2150
- ${chalk13.bold("Workflow:")}
2151
- ${chalk13.dim("$")} os init ${chalk13.dim("# Create a new project")}
2152
- ${chalk13.dim("$")} os generate object task ${chalk13.dim("# Add metadata")}
2153
- ${chalk13.dim("$")} os plugin add <package> ${chalk13.dim("# Add a plugin")}
2154
- ${chalk13.dim("$")} os validate ${chalk13.dim("# Check configuration")}
2155
- ${chalk13.dim("$")} os dev ${chalk13.dim("# Start dev server")}
2156
- ${chalk13.dim("$")} os studio ${chalk13.dim("# Dev server + Studio UI")}
2157
- ${chalk13.dim("$")} os compile ${chalk13.dim("# Build for production")}
2158
-
2159
- ${chalk13.dim("Aliases: objectstack | os")}
2160
- ${chalk13.dim("Docs: https://objectstack.dev")}
3723
+ ${chalk17.bold("Workflow:")}
3724
+ ${chalk17.dim("$")} os init ${chalk17.dim("# Create a new project")}
3725
+ ${chalk17.dim("$")} os generate object task ${chalk17.dim("# Add metadata")}
3726
+ ${chalk17.dim("$")} os plugin add <package> ${chalk17.dim("# Add a plugin")}
3727
+ ${chalk17.dim("$")} os validate ${chalk17.dim("# Check configuration")}
3728
+ ${chalk17.dim("$")} os dev ${chalk17.dim("# Start dev server")}
3729
+ ${chalk17.dim("$")} os studio ${chalk17.dim("# Dev server + Studio UI")}
3730
+ ${chalk17.dim("$")} os compile ${chalk17.dim("# Build for production")}
3731
+
3732
+ ${chalk17.dim("Aliases: objectstack | os")}
3733
+ ${chalk17.dim("Docs: https://objectstack.dev")}
2161
3734
  `);
2162
3735
  program.addCommand(initCommand);
2163
3736
  program.addCommand(devCommand);
@@ -2171,11 +3744,15 @@ program.addCommand(createCommand);
2171
3744
  program.addCommand(pluginCommand);
2172
3745
  program.addCommand(testCommand);
2173
3746
  program.addCommand(doctorCommand);
3747
+ program.addCommand(lintCommand);
3748
+ program.addCommand(diffCommand);
3749
+ program.addCommand(explainCommand);
3750
+ program.addCommand(codemodCommand);
2174
3751
  loadPluginCommands(program).then(() => {
2175
3752
  program.parse(process.argv);
2176
3753
  }).catch((err) => {
2177
3754
  if (process.env.DEBUG) {
2178
- console.error(chalk13.yellow(`
3755
+ console.error(chalk17.yellow(`
2179
3756
  \u26A0 Plugin command loading failed: ${err?.message || err}`));
2180
3757
  }
2181
3758
  program.parse(process.argv);