@objectstack/cli 3.0.6 → 3.0.8
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/.turbo/turbo-build.log +2 -26
- package/CHANGELOG.md +27 -0
- package/README.md +98 -54
- package/bin/run-dev.js +5 -0
- package/bin/run.js +5 -0
- package/dist/bin.d.ts +11 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +12 -3767
- package/dist/bin.js.map +1 -0
- package/dist/commands/codemod/v2-to-v3.d.ts +10 -0
- package/dist/commands/codemod/v2-to-v3.d.ts.map +1 -0
- package/dist/commands/codemod/v2-to-v3.js +145 -0
- package/dist/commands/codemod/v2-to-v3.js.map +1 -0
- package/dist/commands/compile.d.ts +13 -0
- package/dist/commands/compile.d.ts.map +1 -0
- package/dist/commands/compile.js +91 -0
- package/dist/commands/compile.js.map +1 -0
- package/dist/commands/create.d.ts +91 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +259 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/dev.d.ts +14 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +67 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/diff.d.ts +16 -0
- package/dist/commands/diff.d.ts.map +1 -0
- package/dist/commands/diff.js +239 -0
- package/dist/commands/diff.js.map +1 -0
- package/dist/commands/doctor.d.ts +10 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +532 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/explain.d.ts +12 -0
- package/dist/commands/explain.d.ts.map +1 -0
- package/dist/commands/explain.js +368 -0
- package/dist/commands/explain.js.map +1 -0
- package/dist/commands/generate.d.ts +17 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +833 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/info.d.ts +12 -0
- package/dist/commands/info.d.ts.map +1 -0
- package/dist/commands/info.js +100 -0
- package/dist/commands/info.js.map +1 -0
- package/dist/commands/init.d.ts +22 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +295 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/lint.d.ts +13 -0
- package/dist/commands/lint.d.ts.map +1 -0
- package/dist/commands/lint.js +255 -0
- package/dist/commands/lint.js.map +1 -0
- package/dist/commands/plugin/add.d.ts +22 -0
- package/dist/commands/plugin/add.d.ts.map +1 -0
- package/dist/commands/plugin/add.js +93 -0
- package/dist/commands/plugin/add.js.map +1 -0
- package/dist/commands/plugin/info.d.ts +10 -0
- package/dist/commands/plugin/info.d.ts.map +1 -0
- package/dist/commands/plugin/info.js +65 -0
- package/dist/commands/plugin/info.js.map +1 -0
- package/dist/commands/plugin/list.d.ts +13 -0
- package/dist/commands/plugin/list.d.ts.map +1 -0
- package/dist/commands/plugin/list.js +78 -0
- package/dist/commands/plugin/list.js.map +1 -0
- package/dist/commands/plugin/remove.d.ts +20 -0
- package/dist/commands/plugin/remove.d.ts.map +1 -0
- package/dist/commands/plugin/remove.js +79 -0
- package/dist/commands/plugin/remove.js.map +1 -0
- package/dist/commands/serve.d.ts +15 -0
- package/dist/commands/serve.d.ts.map +1 -0
- package/dist/commands/serve.js +286 -0
- package/dist/commands/serve.js.map +1 -0
- package/dist/commands/studio.d.ts +19 -0
- package/dist/commands/studio.d.ts.map +1 -0
- package/dist/commands/studio.js +43 -0
- package/dist/commands/studio.js.map +1 -0
- package/dist/commands/test.d.ts +13 -0
- package/dist/commands/test.d.ts.map +1 -0
- package/dist/commands/test.js +120 -0
- package/dist/commands/test.js.map +1 -0
- package/dist/commands/validate.d.ts +13 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +115 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/index.d.ts +15 -114
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -2805
- package/dist/index.js.map +1 -0
- package/dist/utils/config.d.ts +21 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +66 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/format.d.ts +52 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/format.js +202 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/plugin-helpers.d.ts +14 -0
- package/dist/utils/plugin-helpers.d.ts.map +1 -0
- package/dist/utils/plugin-helpers.js +40 -0
- package/dist/utils/plugin-helpers.js.map +1 -0
- package/dist/utils/studio.d.ts +58 -0
- package/dist/utils/studio.d.ts.map +1 -0
- package/dist/utils/studio.js +288 -0
- package/dist/utils/studio.js.map +1 -0
- package/package.json +33 -15
- package/src/bin.ts +11 -104
- package/src/commands/{codemod.ts → codemod/v2-to-v3.ts} +21 -28
- package/src/commands/compile.ts +31 -22
- package/src/commands/create.ts +29 -19
- package/src/commands/dev.ts +21 -10
- package/src/commands/diff.ts +28 -19
- package/src/commands/doctor.ts +17 -10
- package/src/commands/explain.ts +20 -10
- package/src/commands/generate.ts +81 -90
- package/src/commands/info.ts +20 -11
- package/src/commands/init.ts +32 -20
- package/src/commands/lint.ts +24 -14
- package/src/commands/plugin/add.ts +112 -0
- package/src/commands/plugin/info.ts +79 -0
- package/src/commands/plugin/list.ts +93 -0
- package/src/commands/plugin/remove.ts +97 -0
- package/src/commands/serve.ts +30 -20
- package/src/commands/studio.ts +21 -11
- package/src/commands/test.ts +21 -10
- package/src/commands/validate.ts +32 -22
- package/src/index.ts +20 -12
- package/src/utils/plugin-helpers.ts +37 -0
- package/src/utils/studio.ts +0 -1
- package/test/commands.test.ts +76 -37
- package/test/plugin-commands.test.ts +42 -160
- package/test/plugin.test.ts +19 -23
- package/tsconfig.build.json +18 -0
- package/bin/objectstack.js +0 -2
- package/dist/chunk-T2YN4AB7.js +0 -249
- package/dist/chunk-XNACYTC5.js +0 -251
- package/dist/config-FOXDQ5F7.js +0 -10
- package/dist/config-GBR54FKL.js +0 -11
- package/src/commands/plugin.ts +0 -372
- package/src/utils/plugin-commands.ts +0 -163
package/dist/bin.js
CHANGED
|
@@ -1,3767 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
printServerReady,
|
|
14
|
-
printStep,
|
|
15
|
-
printSuccess,
|
|
16
|
-
printWarning,
|
|
17
|
-
resolveConfigPath
|
|
18
|
-
} from "./chunk-XNACYTC5.js";
|
|
19
|
-
|
|
20
|
-
// src/bin.ts
|
|
21
|
-
import { createRequire as createRequire2 } from "module";
|
|
22
|
-
import { Command as Command17 } from "commander";
|
|
23
|
-
import chalk17 from "chalk";
|
|
24
|
-
|
|
25
|
-
// src/commands/compile.ts
|
|
26
|
-
import { Command } from "commander";
|
|
27
|
-
import path from "path";
|
|
28
|
-
import fs from "fs";
|
|
29
|
-
import chalk from "chalk";
|
|
30
|
-
import { ObjectStackDefinitionSchema, normalizeStackInput } from "@objectstack/spec";
|
|
31
|
-
var compileCommand = new Command("compile").description("Compile ObjectStack configuration to JSON artifact").argument("[config]", "Source configuration file").option("-o, --output <path>", "Output JSON file", "dist/objectstack.json").option("--json", "Output compile result as JSON (for CI)").action(async (configPath, options) => {
|
|
32
|
-
const timer = createTimer();
|
|
33
|
-
if (!options.json) {
|
|
34
|
-
printHeader("Compile");
|
|
35
|
-
}
|
|
36
|
-
try {
|
|
37
|
-
if (!options.json) printStep("Loading configuration...");
|
|
38
|
-
const { config, absolutePath, duration } = await loadConfig(configPath);
|
|
39
|
-
if (!options.json) {
|
|
40
|
-
printKV("Config", path.relative(process.cwd(), absolutePath));
|
|
41
|
-
printKV("Load time", `${duration}ms`);
|
|
42
|
-
}
|
|
43
|
-
if (!options.json) printStep("Validating protocol compliance...");
|
|
44
|
-
const normalized = normalizeStackInput(config);
|
|
45
|
-
const result = ObjectStackDefinitionSchema.safeParse(normalized);
|
|
46
|
-
if (!result.success) {
|
|
47
|
-
if (options.json) {
|
|
48
|
-
console.log(JSON.stringify({ success: false, errors: result.error.issues }));
|
|
49
|
-
process.exit(1);
|
|
50
|
-
}
|
|
51
|
-
console.log("");
|
|
52
|
-
printError("Validation failed");
|
|
53
|
-
formatZodErrors(result.error);
|
|
54
|
-
process.exit(1);
|
|
55
|
-
}
|
|
56
|
-
if (!options.json) printStep("Writing artifact...");
|
|
57
|
-
const output = options.output;
|
|
58
|
-
const artifactPath = path.resolve(process.cwd(), output);
|
|
59
|
-
const artifactDir = path.dirname(artifactPath);
|
|
60
|
-
if (!fs.existsSync(artifactDir)) {
|
|
61
|
-
fs.mkdirSync(artifactDir, { recursive: true });
|
|
62
|
-
}
|
|
63
|
-
const jsonContent = JSON.stringify(result.data, null, 2);
|
|
64
|
-
fs.writeFileSync(artifactPath, jsonContent);
|
|
65
|
-
const sizeKB = (jsonContent.length / 1024).toFixed(1);
|
|
66
|
-
const stats = collectMetadataStats(config);
|
|
67
|
-
if (options.json) {
|
|
68
|
-
console.log(JSON.stringify({
|
|
69
|
-
success: true,
|
|
70
|
-
output: artifactPath,
|
|
71
|
-
size: jsonContent.length,
|
|
72
|
-
stats,
|
|
73
|
-
duration: timer.elapsed()
|
|
74
|
-
}));
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
console.log("");
|
|
78
|
-
printSuccess(`Build complete ${chalk.dim(`(${timer.display()})`)}`);
|
|
79
|
-
console.log("");
|
|
80
|
-
printMetadataStats(stats);
|
|
81
|
-
console.log("");
|
|
82
|
-
printKV("Artifact", `${output} ${chalk.dim(`(${sizeKB} KB`)})`);
|
|
83
|
-
console.log("");
|
|
84
|
-
} catch (error) {
|
|
85
|
-
if (options.json) {
|
|
86
|
-
console.log(JSON.stringify({ success: false, error: error.message }));
|
|
87
|
-
process.exit(1);
|
|
88
|
-
}
|
|
89
|
-
console.log("");
|
|
90
|
-
printError(error.message || String(error));
|
|
91
|
-
process.exit(1);
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
// src/commands/dev.ts
|
|
96
|
-
import { Command as Command2 } from "commander";
|
|
97
|
-
import chalk2 from "chalk";
|
|
98
|
-
import { execSync, spawn } from "child_process";
|
|
99
|
-
import fs2 from "fs";
|
|
100
|
-
import path2 from "path";
|
|
101
|
-
var devCommand = new Command2("dev").description("Start development mode with hot-reload").argument("[package]", "Package name or filter pattern", "all").option("-w, --watch", "Enable watch mode (default)", true).option("--ui", "Enable Studio UI at /_studio/").option("-v, --verbose", "Verbose output").action(async (packageName, options) => {
|
|
102
|
-
printHeader("Development Mode");
|
|
103
|
-
const configPath = path2.resolve(process.cwd(), "objectstack.config.ts");
|
|
104
|
-
if (packageName === "all" && fs2.existsSync(configPath)) {
|
|
105
|
-
printKV("Config", configPath, "\u{1F4C2}");
|
|
106
|
-
printStep("Starting dev server...");
|
|
107
|
-
const binPath = process.argv[1];
|
|
108
|
-
const child = spawn(process.execPath, [binPath, "serve", "--dev", ...options.ui ? ["--ui"] : [], ...options.verbose ? ["--verbose"] : []], {
|
|
109
|
-
stdio: "inherit",
|
|
110
|
-
env: { ...process.env, NODE_ENV: "development" }
|
|
111
|
-
});
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
try {
|
|
115
|
-
const cwd = process.cwd();
|
|
116
|
-
const workspaceConfigPath = path2.resolve(cwd, "pnpm-workspace.yaml");
|
|
117
|
-
const isWorkspaceRoot = fs2.existsSync(workspaceConfigPath);
|
|
118
|
-
if (packageName === "all" && !isWorkspaceRoot) {
|
|
119
|
-
printError(`Config file not found in ${cwd}`);
|
|
120
|
-
console.error(chalk2.yellow(" Run in a directory with objectstack.config.ts, or from the monorepo root."));
|
|
121
|
-
process.exit(1);
|
|
122
|
-
}
|
|
123
|
-
const filter = packageName === "all" ? "" : `--filter ${packageName}`;
|
|
124
|
-
printKV("Package", packageName === "all" ? "All packages" : packageName, "\u{1F4E6}");
|
|
125
|
-
printKV("Watch", "enabled", "\u{1F504}");
|
|
126
|
-
const command = `pnpm ${filter} dev`.trim();
|
|
127
|
-
console.log(chalk2.dim(`$ ${command}`));
|
|
128
|
-
console.log("");
|
|
129
|
-
execSync(command, {
|
|
130
|
-
stdio: "inherit",
|
|
131
|
-
cwd
|
|
132
|
-
});
|
|
133
|
-
} catch (error) {
|
|
134
|
-
printError(`Development mode failed: ${error.message || error}`);
|
|
135
|
-
process.exit(1);
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
// src/commands/doctor.ts
|
|
140
|
-
import { Command as Command3 } from "commander";
|
|
141
|
-
import chalk3 from "chalk";
|
|
142
|
-
import { execSync as execSync2 } from "child_process";
|
|
143
|
-
import fs3 from "fs";
|
|
144
|
-
import path3 from "path";
|
|
145
|
-
import { normalizeStackInput as normalizeStackInput2 } from "@objectstack/spec";
|
|
146
|
-
function detectCircularDependencies(objects) {
|
|
147
|
-
const issues = [];
|
|
148
|
-
const graph = /* @__PURE__ */ new Map();
|
|
149
|
-
for (const obj of objects) {
|
|
150
|
-
const deps = [];
|
|
151
|
-
if (obj.fields && typeof obj.fields === "object") {
|
|
152
|
-
for (const field of Object.values(obj.fields)) {
|
|
153
|
-
if (field?.type === "lookup" && field?.reference) {
|
|
154
|
-
deps.push(field.reference);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
graph.set(obj.name, deps);
|
|
159
|
-
}
|
|
160
|
-
const visited = /* @__PURE__ */ new Set();
|
|
161
|
-
const stack = /* @__PURE__ */ new Set();
|
|
162
|
-
function dfs(node, path12) {
|
|
163
|
-
if (stack.has(node)) {
|
|
164
|
-
const cycleStart = path12.indexOf(node);
|
|
165
|
-
const cycle = path12.slice(cycleStart).concat(node);
|
|
166
|
-
issues.push(`Circular dependency: ${cycle.join(" \u2192 ")}`);
|
|
167
|
-
return true;
|
|
168
|
-
}
|
|
169
|
-
if (visited.has(node)) return false;
|
|
170
|
-
visited.add(node);
|
|
171
|
-
stack.add(node);
|
|
172
|
-
for (const dep of graph.get(node) || []) {
|
|
173
|
-
if (graph.has(dep)) {
|
|
174
|
-
dfs(dep, [...path12, node]);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
stack.delete(node);
|
|
178
|
-
return false;
|
|
179
|
-
}
|
|
180
|
-
for (const name of graph.keys()) {
|
|
181
|
-
if (!visited.has(name)) {
|
|
182
|
-
dfs(name, []);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
return issues;
|
|
186
|
-
}
|
|
187
|
-
function findOrphanViews(config) {
|
|
188
|
-
const objectNames = /* @__PURE__ */ new Set();
|
|
189
|
-
if (Array.isArray(config.objects)) {
|
|
190
|
-
for (const obj of config.objects) {
|
|
191
|
-
if (obj.name) objectNames.add(obj.name);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
const orphans = [];
|
|
195
|
-
if (Array.isArray(config.views)) {
|
|
196
|
-
for (const view of config.views) {
|
|
197
|
-
if (view.object && !objectNames.has(view.object)) {
|
|
198
|
-
orphans.push(`View "${view.name || "?"}" references non-existent object "${view.object}"`);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
return orphans;
|
|
203
|
-
}
|
|
204
|
-
function findUnusedObjects(config) {
|
|
205
|
-
const objectNames = /* @__PURE__ */ new Set();
|
|
206
|
-
if (Array.isArray(config.objects)) {
|
|
207
|
-
for (const obj of config.objects) {
|
|
208
|
-
if (obj.name) objectNames.add(obj.name);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
const referencedObjects = /* @__PURE__ */ new Set();
|
|
212
|
-
if (Array.isArray(config.views)) {
|
|
213
|
-
for (const view of config.views) {
|
|
214
|
-
if (view.object) referencedObjects.add(view.object);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
if (Array.isArray(config.flows)) {
|
|
218
|
-
for (const flow of config.flows) {
|
|
219
|
-
if (flow.trigger?.object) referencedObjects.add(flow.trigger.object);
|
|
220
|
-
if (flow.object) referencedObjects.add(flow.object);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
if (Array.isArray(config.apps)) {
|
|
224
|
-
for (const app of config.apps) {
|
|
225
|
-
if (Array.isArray(app.navigation)) {
|
|
226
|
-
for (const nav of app.navigation) {
|
|
227
|
-
if (nav.object) referencedObjects.add(nav.object);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
if (Array.isArray(config.agents)) {
|
|
233
|
-
for (const agent of config.agents) {
|
|
234
|
-
if (Array.isArray(agent.objects)) {
|
|
235
|
-
for (const o of agent.objects) referencedObjects.add(o);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
if (Array.isArray(config.objects)) {
|
|
240
|
-
for (const obj of config.objects) {
|
|
241
|
-
if (obj.fields && typeof obj.fields === "object") {
|
|
242
|
-
for (const field of Object.values(obj.fields)) {
|
|
243
|
-
if (field?.type === "lookup" && field?.reference) {
|
|
244
|
-
referencedObjects.add(field.reference);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
const unused = [];
|
|
251
|
-
for (const name of objectNames) {
|
|
252
|
-
if (!referencedObjects.has(name)) {
|
|
253
|
-
unused.push(`Object "${name}" is defined but not referenced by any view, flow, app, or agent`);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
return unused;
|
|
257
|
-
}
|
|
258
|
-
function walkDir(dir, ext) {
|
|
259
|
-
const results = [];
|
|
260
|
-
if (!fs3.existsSync(dir)) return results;
|
|
261
|
-
const entries = fs3.readdirSync(dir, { withFileTypes: true });
|
|
262
|
-
for (const entry of entries) {
|
|
263
|
-
if (entry.name === "node_modules") continue;
|
|
264
|
-
const fullPath = path3.join(dir, entry.name);
|
|
265
|
-
if (entry.isDirectory()) {
|
|
266
|
-
results.push(...walkDir(fullPath, ext));
|
|
267
|
-
} else if (entry.name.endsWith(ext)) {
|
|
268
|
-
results.push(fullPath);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
return results;
|
|
272
|
-
}
|
|
273
|
-
function findMissingTests(cwd) {
|
|
274
|
-
const specSrcDir = path3.join(cwd, "packages/spec/src");
|
|
275
|
-
if (!fs3.existsSync(specSrcDir)) return [];
|
|
276
|
-
const missing = [];
|
|
277
|
-
const zodFiles = walkDir(specSrcDir, ".zod.ts");
|
|
278
|
-
for (const zodFile of zodFiles) {
|
|
279
|
-
const testFile = zodFile.replace(".zod.ts", ".test.ts");
|
|
280
|
-
if (!fs3.existsSync(testFile)) {
|
|
281
|
-
const relZod = path3.relative(specSrcDir, zodFile);
|
|
282
|
-
const relTest = path3.relative(specSrcDir, testFile);
|
|
283
|
-
missing.push(`Missing test: ${relTest} (for ${relZod})`);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
return missing;
|
|
287
|
-
}
|
|
288
|
-
function findDeprecatedUsages(cwd) {
|
|
289
|
-
const specSrcDir = path3.join(cwd, "packages/spec/src");
|
|
290
|
-
if (!fs3.existsSync(specSrcDir)) return [];
|
|
291
|
-
const deprecated = [];
|
|
292
|
-
const tsFiles = walkDir(specSrcDir, ".ts").filter((f) => !f.endsWith(".test.ts"));
|
|
293
|
-
for (const tsFile of tsFiles) {
|
|
294
|
-
try {
|
|
295
|
-
const content = fs3.readFileSync(tsFile, "utf-8");
|
|
296
|
-
const lines = content.split("\n");
|
|
297
|
-
const relPath = path3.relative(specSrcDir, tsFile);
|
|
298
|
-
for (let i = 0; i < lines.length; i++) {
|
|
299
|
-
if (lines[i].includes("@deprecated")) {
|
|
300
|
-
deprecated.push(`${relPath}:${i + 1} \u2014 @deprecated tag found`);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
} catch {
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
return deprecated;
|
|
307
|
-
}
|
|
308
|
-
var DEPRECATED_PATTERNS = [
|
|
309
|
-
{
|
|
310
|
-
pattern: /\bEnhancedObjectKernel\b/,
|
|
311
|
-
description: "EnhancedObjectKernel is deprecated in v3",
|
|
312
|
-
replacement: "Use ObjectKernel instead"
|
|
313
|
-
},
|
|
314
|
-
{
|
|
315
|
-
pattern: /\bmax_length\b/,
|
|
316
|
-
description: "snake_case config key: max_length",
|
|
317
|
-
replacement: "Use maxLength (camelCase)"
|
|
318
|
-
},
|
|
319
|
-
{
|
|
320
|
-
pattern: /\bdefault_value\b/,
|
|
321
|
-
description: "snake_case config key: default_value",
|
|
322
|
-
replacement: "Use defaultValue (camelCase)"
|
|
323
|
-
},
|
|
324
|
-
{
|
|
325
|
-
pattern: /\bmin_length\b/,
|
|
326
|
-
description: "snake_case config key: min_length",
|
|
327
|
-
replacement: "Use minLength (camelCase)"
|
|
328
|
-
},
|
|
329
|
-
{
|
|
330
|
-
pattern: /\breference_filters\b/,
|
|
331
|
-
description: "snake_case config key: reference_filters",
|
|
332
|
-
replacement: "Use referenceFilters (camelCase)"
|
|
333
|
-
},
|
|
334
|
-
{
|
|
335
|
-
pattern: /\bunique_name\b/,
|
|
336
|
-
description: "snake_case config key: unique_name",
|
|
337
|
-
replacement: "Use uniqueName (camelCase)"
|
|
338
|
-
},
|
|
339
|
-
{
|
|
340
|
-
pattern: /from\s+['"]@objectstack\/core\/enhanced['"]/,
|
|
341
|
-
description: "Import from deprecated @objectstack/core/enhanced path",
|
|
342
|
-
replacement: "Use import from '@objectstack/core'"
|
|
343
|
-
},
|
|
344
|
-
{
|
|
345
|
-
pattern: /from\s+['"]@objectstack\/spec\/dist\/[^'"]+['"]/,
|
|
346
|
-
description: "Import from deprecated @objectstack/spec/dist/ deep path",
|
|
347
|
-
replacement: "Use import from '@objectstack/spec'"
|
|
348
|
-
}
|
|
349
|
-
];
|
|
350
|
-
function scanDeprecatedPatterns(dir) {
|
|
351
|
-
const results = [];
|
|
352
|
-
if (!fs3.existsSync(dir)) return results;
|
|
353
|
-
const tsFiles = walkDir(dir, ".ts").filter((f) => !f.endsWith(".test.ts"));
|
|
354
|
-
for (const tsFile of tsFiles) {
|
|
355
|
-
try {
|
|
356
|
-
const content = fs3.readFileSync(tsFile, "utf-8");
|
|
357
|
-
const lines = content.split("\n");
|
|
358
|
-
const relPath = path3.relative(process.cwd(), tsFile);
|
|
359
|
-
for (let i = 0; i < lines.length; i++) {
|
|
360
|
-
for (const dp of DEPRECATED_PATTERNS) {
|
|
361
|
-
if (dp.pattern.test(lines[i])) {
|
|
362
|
-
results.push({
|
|
363
|
-
file: relPath,
|
|
364
|
-
line: i + 1,
|
|
365
|
-
description: dp.description,
|
|
366
|
-
replacement: dp.replacement
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
} catch {
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
return results;
|
|
375
|
-
}
|
|
376
|
-
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) => {
|
|
377
|
-
printHeader("Environment Health Check");
|
|
378
|
-
const results = [];
|
|
379
|
-
try {
|
|
380
|
-
const nodeVersion = process.version;
|
|
381
|
-
const majorVersion = parseInt(nodeVersion.slice(1).split(".")[0]);
|
|
382
|
-
if (majorVersion >= 18) {
|
|
383
|
-
results.push({
|
|
384
|
-
name: "Node.js",
|
|
385
|
-
status: "ok",
|
|
386
|
-
message: `Version ${nodeVersion}`
|
|
387
|
-
});
|
|
388
|
-
} else {
|
|
389
|
-
results.push({
|
|
390
|
-
name: "Node.js",
|
|
391
|
-
status: "error",
|
|
392
|
-
message: `Version ${nodeVersion} (requires >= 18.0.0)`,
|
|
393
|
-
fix: "Upgrade Node.js: https://nodejs.org"
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
} catch (error) {
|
|
397
|
-
results.push({
|
|
398
|
-
name: "Node.js",
|
|
399
|
-
status: "error",
|
|
400
|
-
message: "Not found",
|
|
401
|
-
fix: "Install Node.js: https://nodejs.org"
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
try {
|
|
405
|
-
const pnpmVersion = execSync2("pnpm -v", { encoding: "utf-8" }).trim();
|
|
406
|
-
results.push({
|
|
407
|
-
name: "pnpm",
|
|
408
|
-
status: "ok",
|
|
409
|
-
message: `Version ${pnpmVersion}`
|
|
410
|
-
});
|
|
411
|
-
} catch (error) {
|
|
412
|
-
results.push({
|
|
413
|
-
name: "pnpm",
|
|
414
|
-
status: "error",
|
|
415
|
-
message: "Not found",
|
|
416
|
-
fix: "Install pnpm: npm install -g pnpm@10.28.1"
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
try {
|
|
420
|
-
const tscVersion = execSync2("tsc -v", { encoding: "utf-8" }).trim();
|
|
421
|
-
results.push({
|
|
422
|
-
name: "TypeScript",
|
|
423
|
-
status: "ok",
|
|
424
|
-
message: tscVersion
|
|
425
|
-
});
|
|
426
|
-
} catch (error) {
|
|
427
|
-
results.push({
|
|
428
|
-
name: "TypeScript",
|
|
429
|
-
status: "warning",
|
|
430
|
-
message: "Not found in PATH",
|
|
431
|
-
fix: "Installed locally via pnpm"
|
|
432
|
-
});
|
|
433
|
-
}
|
|
434
|
-
const cwd = process.cwd();
|
|
435
|
-
const nodeModulesPath = path3.join(cwd, "node_modules");
|
|
436
|
-
if (fs3.existsSync(nodeModulesPath)) {
|
|
437
|
-
results.push({
|
|
438
|
-
name: "Dependencies",
|
|
439
|
-
status: "ok",
|
|
440
|
-
message: "Installed"
|
|
441
|
-
});
|
|
442
|
-
} else {
|
|
443
|
-
results.push({
|
|
444
|
-
name: "Dependencies",
|
|
445
|
-
status: "error",
|
|
446
|
-
message: "Not installed",
|
|
447
|
-
fix: "Run: pnpm install"
|
|
448
|
-
});
|
|
449
|
-
}
|
|
450
|
-
const specDistPath = path3.join(cwd, "packages/spec/dist");
|
|
451
|
-
if (fs3.existsSync(specDistPath)) {
|
|
452
|
-
results.push({
|
|
453
|
-
name: "@objectstack/spec",
|
|
454
|
-
status: "ok",
|
|
455
|
-
message: "Built"
|
|
456
|
-
});
|
|
457
|
-
} else {
|
|
458
|
-
results.push({
|
|
459
|
-
name: "@objectstack/spec",
|
|
460
|
-
status: "warning",
|
|
461
|
-
message: "Not built",
|
|
462
|
-
fix: "Run: pnpm --filter @objectstack/spec build"
|
|
463
|
-
});
|
|
464
|
-
}
|
|
465
|
-
try {
|
|
466
|
-
const gitVersion = execSync2("git --version", { encoding: "utf-8" }).trim();
|
|
467
|
-
results.push({
|
|
468
|
-
name: "Git",
|
|
469
|
-
status: "ok",
|
|
470
|
-
message: gitVersion
|
|
471
|
-
});
|
|
472
|
-
} catch (error) {
|
|
473
|
-
results.push({
|
|
474
|
-
name: "Git",
|
|
475
|
-
status: "warning",
|
|
476
|
-
message: "Not found",
|
|
477
|
-
fix: "Install Git for version control"
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
let hasErrors = false;
|
|
481
|
-
let hasWarnings = false;
|
|
482
|
-
console.log("");
|
|
483
|
-
results.forEach((result) => {
|
|
484
|
-
const padded = result.name.padEnd(20);
|
|
485
|
-
if (result.status === "ok") {
|
|
486
|
-
printSuccess(`${padded} ${result.message}`);
|
|
487
|
-
} else if (result.status === "warning") {
|
|
488
|
-
printWarning(`${padded} ${result.message}`);
|
|
489
|
-
} else {
|
|
490
|
-
printError(`${padded} ${result.message}`);
|
|
491
|
-
}
|
|
492
|
-
if (result.fix && (options.verbose || result.status === "error")) {
|
|
493
|
-
console.log(chalk3.dim(` \u2192 ${result.fix}`));
|
|
494
|
-
}
|
|
495
|
-
if (result.status === "error") hasErrors = true;
|
|
496
|
-
if (result.status === "warning") hasWarnings = true;
|
|
497
|
-
});
|
|
498
|
-
printStep("Checking for missing test files...");
|
|
499
|
-
const missingTests = findMissingTests(cwd);
|
|
500
|
-
if (missingTests.length > 0) {
|
|
501
|
-
hasWarnings = true;
|
|
502
|
-
for (const msg of missingTests) {
|
|
503
|
-
printWarning(msg);
|
|
504
|
-
}
|
|
505
|
-
} else {
|
|
506
|
-
printSuccess("Test coverage All *.zod.ts files have matching tests");
|
|
507
|
-
}
|
|
508
|
-
printStep("Scanning for @deprecated usage...");
|
|
509
|
-
const deprecatedUsages = findDeprecatedUsages(cwd);
|
|
510
|
-
if (deprecatedUsages.length > 0) {
|
|
511
|
-
hasWarnings = true;
|
|
512
|
-
for (const msg of deprecatedUsages) {
|
|
513
|
-
printWarning(`Deprecated: ${msg}`);
|
|
514
|
-
}
|
|
515
|
-
} else {
|
|
516
|
-
printSuccess("Deprecations No @deprecated tags found");
|
|
517
|
-
}
|
|
518
|
-
if (configExists()) {
|
|
519
|
-
printStep("Loading configuration for analysis...");
|
|
520
|
-
try {
|
|
521
|
-
const { config: rawConfig } = await loadConfig();
|
|
522
|
-
const config = normalizeStackInput2(rawConfig);
|
|
523
|
-
if (Array.isArray(config.objects) && config.objects.length > 0) {
|
|
524
|
-
printStep("Checking for circular dependencies...");
|
|
525
|
-
const cycles = detectCircularDependencies(config.objects);
|
|
526
|
-
if (cycles.length > 0) {
|
|
527
|
-
hasWarnings = true;
|
|
528
|
-
for (const msg of cycles) {
|
|
529
|
-
printWarning(msg);
|
|
530
|
-
}
|
|
531
|
-
} else {
|
|
532
|
-
printSuccess("Dependencies No circular references detected");
|
|
533
|
-
}
|
|
534
|
-
printStep("Checking for unused objects...");
|
|
535
|
-
const unused = findUnusedObjects(config);
|
|
536
|
-
if (unused.length > 0) {
|
|
537
|
-
hasWarnings = true;
|
|
538
|
-
for (const msg of unused) {
|
|
539
|
-
printWarning(msg);
|
|
540
|
-
}
|
|
541
|
-
} else {
|
|
542
|
-
printSuccess("Object usage All objects are referenced");
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
if (Array.isArray(config.views) && config.views.length > 0) {
|
|
546
|
-
printStep("Checking for orphan views...");
|
|
547
|
-
const orphans = findOrphanViews(config);
|
|
548
|
-
if (orphans.length > 0) {
|
|
549
|
-
hasWarnings = true;
|
|
550
|
-
for (const msg of orphans) {
|
|
551
|
-
printWarning(msg);
|
|
552
|
-
}
|
|
553
|
-
} else {
|
|
554
|
-
printSuccess("View integrity All views reference valid objects");
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
} catch {
|
|
558
|
-
printWarning("Could not load config for analysis (config checks skipped)");
|
|
559
|
-
hasWarnings = true;
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
if (options.scanDeprecations) {
|
|
563
|
-
printStep("Scanning for deprecated ObjectStack patterns...");
|
|
564
|
-
const scanDir = path3.join(cwd, "src");
|
|
565
|
-
const deprecations = scanDeprecatedPatterns(scanDir);
|
|
566
|
-
if (deprecations.length > 0) {
|
|
567
|
-
hasWarnings = true;
|
|
568
|
-
for (const dep of deprecations) {
|
|
569
|
-
printWarning(`${dep.file}:${dep.line} \u2014 ${dep.description}`);
|
|
570
|
-
if (options.verbose) {
|
|
571
|
-
console.log(chalk3.dim(` \u2192 ${dep.replacement}`));
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
console.log("");
|
|
575
|
-
printInfo(`Found ${deprecations.length} deprecated pattern(s). Run \`objectstack codemod v2-to-v3\` to auto-fix.`);
|
|
576
|
-
} else {
|
|
577
|
-
printSuccess("Deprecation scan No deprecated patterns found");
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
console.log("");
|
|
581
|
-
if (hasErrors) {
|
|
582
|
-
console.log(chalk3.red("\u274C Some critical issues found. Please fix them before continuing."));
|
|
583
|
-
results.filter((r) => r.status === "error" && r.fix).forEach((r) => console.log(chalk3.dim(` ${r.fix}`)));
|
|
584
|
-
process.exit(1);
|
|
585
|
-
} else if (hasWarnings) {
|
|
586
|
-
console.log(chalk3.yellow("\u26A0\uFE0F Environment is functional but has some warnings."));
|
|
587
|
-
console.log(chalk3.dim(" Run with --verbose to see fix suggestions."));
|
|
588
|
-
} else {
|
|
589
|
-
console.log(chalk3.green("\u2705 Environment is healthy and ready for development!"));
|
|
590
|
-
}
|
|
591
|
-
console.log("");
|
|
592
|
-
});
|
|
593
|
-
|
|
594
|
-
// src/commands/create.ts
|
|
595
|
-
import { Command as Command4 } from "commander";
|
|
596
|
-
import chalk4 from "chalk";
|
|
597
|
-
import fs4 from "fs";
|
|
598
|
-
import path4 from "path";
|
|
599
|
-
var templates = {
|
|
600
|
-
plugin: {
|
|
601
|
-
description: "Create a new ObjectStack plugin",
|
|
602
|
-
files: {
|
|
603
|
-
"package.json": (name) => ({
|
|
604
|
-
name: `@objectstack/plugin-${name}`,
|
|
605
|
-
version: "0.1.0",
|
|
606
|
-
description: `ObjectStack Plugin: ${name}`,
|
|
607
|
-
main: "dist/index.js",
|
|
608
|
-
types: "dist/index.d.ts",
|
|
609
|
-
scripts: {
|
|
610
|
-
build: "tsc",
|
|
611
|
-
dev: "tsc --watch",
|
|
612
|
-
test: "vitest"
|
|
613
|
-
},
|
|
614
|
-
keywords: ["objectstack", "plugin", name],
|
|
615
|
-
author: "",
|
|
616
|
-
license: "MIT",
|
|
617
|
-
dependencies: {
|
|
618
|
-
"@objectstack/spec": "workspace:*",
|
|
619
|
-
zod: "^4.3.6"
|
|
620
|
-
},
|
|
621
|
-
devDependencies: {
|
|
622
|
-
"@types/node": "^22.0.0",
|
|
623
|
-
typescript: "^5.8.0",
|
|
624
|
-
vitest: "^4.0.0"
|
|
625
|
-
}
|
|
626
|
-
}),
|
|
627
|
-
"tsconfig.json": () => ({
|
|
628
|
-
extends: "../../tsconfig.json",
|
|
629
|
-
compilerOptions: {
|
|
630
|
-
outDir: "dist",
|
|
631
|
-
rootDir: "src"
|
|
632
|
-
},
|
|
633
|
-
include: ["src/**/*"]
|
|
634
|
-
}),
|
|
635
|
-
"src/index.ts": (name) => `import type { Plugin } from '@objectstack/spec';
|
|
636
|
-
|
|
637
|
-
/**
|
|
638
|
-
* ${name} Plugin for ObjectStack
|
|
639
|
-
*/
|
|
640
|
-
export const ${toCamelCase(name)}Plugin: Plugin = {
|
|
641
|
-
name: '${name}',
|
|
642
|
-
version: '0.1.0',
|
|
643
|
-
|
|
644
|
-
async initialize(context) {
|
|
645
|
-
console.log('Initializing ${name} plugin...');
|
|
646
|
-
// Plugin initialization logic
|
|
647
|
-
},
|
|
648
|
-
|
|
649
|
-
async destroy() {
|
|
650
|
-
console.log('Destroying ${name} plugin...');
|
|
651
|
-
// Plugin cleanup logic
|
|
652
|
-
},
|
|
653
|
-
};
|
|
654
|
-
|
|
655
|
-
export default ${toCamelCase(name)}Plugin;
|
|
656
|
-
`,
|
|
657
|
-
"README.md": (name) => `# @objectstack/plugin-${name}
|
|
658
|
-
|
|
659
|
-
ObjectStack Plugin: ${name}
|
|
660
|
-
|
|
661
|
-
## Installation
|
|
662
|
-
|
|
663
|
-
\`\`\`bash
|
|
664
|
-
pnpm add @objectstack/plugin-${name}
|
|
665
|
-
\`\`\`
|
|
666
|
-
|
|
667
|
-
## Usage
|
|
668
|
-
|
|
669
|
-
\`\`\`typescript
|
|
670
|
-
import { ${toCamelCase(name)}Plugin } from '@objectstack/plugin-${name}';
|
|
671
|
-
|
|
672
|
-
// Use the plugin in your ObjectStack configuration
|
|
673
|
-
export default {
|
|
674
|
-
plugins: [
|
|
675
|
-
${toCamelCase(name)}Plugin,
|
|
676
|
-
],
|
|
677
|
-
};
|
|
678
|
-
\`\`\`
|
|
679
|
-
|
|
680
|
-
## License
|
|
681
|
-
|
|
682
|
-
MIT
|
|
683
|
-
`
|
|
684
|
-
}
|
|
685
|
-
},
|
|
686
|
-
example: {
|
|
687
|
-
description: "Create a new ObjectStack example application",
|
|
688
|
-
files: {
|
|
689
|
-
"package.json": (name) => ({
|
|
690
|
-
name: `@example/${name}`,
|
|
691
|
-
version: "0.1.0",
|
|
692
|
-
private: true,
|
|
693
|
-
description: `ObjectStack Example: ${name}`,
|
|
694
|
-
scripts: {
|
|
695
|
-
build: "objectstack compile",
|
|
696
|
-
dev: "objectstack dev",
|
|
697
|
-
test: "vitest"
|
|
698
|
-
},
|
|
699
|
-
dependencies: {
|
|
700
|
-
"@objectstack/spec": "workspace:*",
|
|
701
|
-
"@objectstack/cli": "workspace:*",
|
|
702
|
-
zod: "^4.3.6"
|
|
703
|
-
},
|
|
704
|
-
devDependencies: {
|
|
705
|
-
"@types/node": "^22.0.0",
|
|
706
|
-
tsx: "^4.21.0",
|
|
707
|
-
typescript: "^5.8.0",
|
|
708
|
-
vitest: "^4.0.0"
|
|
709
|
-
}
|
|
710
|
-
}),
|
|
711
|
-
"objectstack.config.ts": (name) => `import { defineStack } from '@objectstack/spec';
|
|
712
|
-
|
|
713
|
-
// Barrel imports \u2014 add more as you create new type folders
|
|
714
|
-
// import * as objects from './src/objects';
|
|
715
|
-
// import * as actions from './src/actions';
|
|
716
|
-
// import * as apps from './src/apps';
|
|
717
|
-
|
|
718
|
-
export default defineStack({
|
|
719
|
-
manifest: {
|
|
720
|
-
name: '${name}',
|
|
721
|
-
version: '0.1.0',
|
|
722
|
-
description: '${name} example application',
|
|
723
|
-
},
|
|
724
|
-
|
|
725
|
-
objects: [
|
|
726
|
-
// Object.values(objects), // Uncomment after creating src/objects/index.ts
|
|
727
|
-
],
|
|
728
|
-
|
|
729
|
-
apps: [
|
|
730
|
-
// Object.values(apps), // Uncomment after creating src/apps/index.ts
|
|
731
|
-
],
|
|
732
|
-
});
|
|
733
|
-
`,
|
|
734
|
-
"README.md": (name) => `# ${name} Example
|
|
735
|
-
|
|
736
|
-
ObjectStack example application: ${name}
|
|
737
|
-
|
|
738
|
-
## Quick Start
|
|
739
|
-
|
|
740
|
-
\`\`\`bash
|
|
741
|
-
# Build the configuration
|
|
742
|
-
pnpm build
|
|
743
|
-
|
|
744
|
-
# Run in development mode
|
|
745
|
-
pnpm dev
|
|
746
|
-
\`\`\`
|
|
747
|
-
|
|
748
|
-
## Structure
|
|
749
|
-
|
|
750
|
-
- \`objectstack.config.ts\` - Main configuration file
|
|
751
|
-
- \`dist/objectstack.json\` - Compiled artifact
|
|
752
|
-
|
|
753
|
-
## Learn More
|
|
754
|
-
|
|
755
|
-
- [ObjectStack Documentation](../../content/docs)
|
|
756
|
-
- [Examples](../)
|
|
757
|
-
`,
|
|
758
|
-
"tsconfig.json": () => ({
|
|
759
|
-
extends: "../../tsconfig.json",
|
|
760
|
-
compilerOptions: {
|
|
761
|
-
outDir: "dist",
|
|
762
|
-
rootDir: "."
|
|
763
|
-
},
|
|
764
|
-
include: ["*.ts", "src/**/*"]
|
|
765
|
-
})
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
};
|
|
769
|
-
function toCamelCase(str) {
|
|
770
|
-
return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
|
771
|
-
}
|
|
772
|
-
var createCommand = new Command4("create").description("Create a new package, plugin, or example from template").argument("<type>", "Type of project to create (plugin, example)").argument("[name]", "Name of the project").option("-d, --dir <directory>", "Target directory").action(async (type, name, options) => {
|
|
773
|
-
console.log(chalk4.bold(`
|
|
774
|
-
\u{1F4E6} ObjectStack Project Creator`));
|
|
775
|
-
console.log(chalk4.dim(`-------------------------------`));
|
|
776
|
-
if (!templates[type]) {
|
|
777
|
-
console.error(chalk4.red(`
|
|
778
|
-
\u274C Unknown type: ${type}`));
|
|
779
|
-
console.log(chalk4.dim("Available types: plugin, example"));
|
|
780
|
-
process.exit(1);
|
|
781
|
-
}
|
|
782
|
-
if (!name) {
|
|
783
|
-
console.error(chalk4.red("\n\u274C Project name is required"));
|
|
784
|
-
console.log(chalk4.dim(`Usage: objectstack create ${type} <name>`));
|
|
785
|
-
process.exit(1);
|
|
786
|
-
}
|
|
787
|
-
const template = templates[type];
|
|
788
|
-
const cwd = process.cwd();
|
|
789
|
-
let targetDir;
|
|
790
|
-
if (options?.dir) {
|
|
791
|
-
targetDir = path4.resolve(cwd, options.dir);
|
|
792
|
-
} else {
|
|
793
|
-
const baseDir = type === "plugin" ? "packages/plugins" : "examples";
|
|
794
|
-
const projectName = type === "plugin" ? `plugin-${name}` : name;
|
|
795
|
-
targetDir = path4.join(cwd, baseDir, projectName);
|
|
796
|
-
}
|
|
797
|
-
if (fs4.existsSync(targetDir)) {
|
|
798
|
-
console.error(chalk4.red(`
|
|
799
|
-
\u274C Directory already exists: ${targetDir}`));
|
|
800
|
-
process.exit(1);
|
|
801
|
-
}
|
|
802
|
-
console.log(`\u{1F4C1} Creating ${type}: ${chalk4.blue(name)}`);
|
|
803
|
-
console.log(`\u{1F4C2} Location: ${chalk4.dim(targetDir)}`);
|
|
804
|
-
console.log("");
|
|
805
|
-
try {
|
|
806
|
-
fs4.mkdirSync(targetDir, { recursive: true });
|
|
807
|
-
for (const [filePath, contentFn] of Object.entries(template.files)) {
|
|
808
|
-
const fullPath = path4.join(targetDir, filePath);
|
|
809
|
-
const dir = path4.dirname(fullPath);
|
|
810
|
-
if (!fs4.existsSync(dir)) {
|
|
811
|
-
fs4.mkdirSync(dir, { recursive: true });
|
|
812
|
-
}
|
|
813
|
-
const content = contentFn(name);
|
|
814
|
-
const fileContent = typeof content === "string" ? content : JSON.stringify(content, null, 2);
|
|
815
|
-
fs4.writeFileSync(fullPath, fileContent);
|
|
816
|
-
console.log(chalk4.green(`\u2713 Created ${filePath}`));
|
|
817
|
-
}
|
|
818
|
-
console.log("");
|
|
819
|
-
console.log(chalk4.green("\u2705 Project created successfully!"));
|
|
820
|
-
console.log("");
|
|
821
|
-
console.log(chalk4.bold("Next steps:"));
|
|
822
|
-
console.log(chalk4.dim(` cd ${path4.relative(cwd, targetDir)}`));
|
|
823
|
-
console.log(chalk4.dim(" pnpm install"));
|
|
824
|
-
console.log(chalk4.dim(" pnpm build"));
|
|
825
|
-
console.log("");
|
|
826
|
-
} catch (error) {
|
|
827
|
-
console.error(chalk4.red("\n\u274C Failed to create project:"));
|
|
828
|
-
console.error(error.message || error);
|
|
829
|
-
if (fs4.existsSync(targetDir)) {
|
|
830
|
-
fs4.rmSync(targetDir, { recursive: true });
|
|
831
|
-
}
|
|
832
|
-
process.exit(1);
|
|
833
|
-
}
|
|
834
|
-
});
|
|
835
|
-
|
|
836
|
-
// src/commands/serve.ts
|
|
837
|
-
import { Command as Command5 } from "commander";
|
|
838
|
-
import path6 from "path";
|
|
839
|
-
import fs6 from "fs";
|
|
840
|
-
import net from "net";
|
|
841
|
-
import chalk5 from "chalk";
|
|
842
|
-
import { bundleRequire } from "bundle-require";
|
|
843
|
-
|
|
844
|
-
// src/utils/studio.ts
|
|
845
|
-
import path5 from "path";
|
|
846
|
-
import fs5 from "fs";
|
|
847
|
-
import { createRequire } from "module";
|
|
848
|
-
import { pathToFileURL } from "url";
|
|
849
|
-
var STUDIO_PATH = "/_studio";
|
|
850
|
-
function resolveStudioPath() {
|
|
851
|
-
const cwd = process.cwd();
|
|
852
|
-
const candidates = [
|
|
853
|
-
path5.resolve(cwd, "apps/studio"),
|
|
854
|
-
path5.resolve(cwd, "../../apps/studio"),
|
|
855
|
-
path5.resolve(cwd, "../apps/studio")
|
|
856
|
-
];
|
|
857
|
-
for (const candidate of candidates) {
|
|
858
|
-
const pkgPath = path5.join(candidate, "package.json");
|
|
859
|
-
if (fs5.existsSync(pkgPath)) {
|
|
860
|
-
try {
|
|
861
|
-
const pkg2 = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
|
|
862
|
-
if (pkg2.name === "@objectstack/studio") return candidate;
|
|
863
|
-
} catch {
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
const resolutionBases = [
|
|
868
|
-
pathToFileURL(path5.join(cwd, "package.json")).href,
|
|
869
|
-
// consumer workspace
|
|
870
|
-
import.meta.url
|
|
871
|
-
// CLI package itself
|
|
872
|
-
];
|
|
873
|
-
for (const base of resolutionBases) {
|
|
874
|
-
try {
|
|
875
|
-
const req = createRequire(base);
|
|
876
|
-
const resolved = req.resolve("@objectstack/studio/package.json");
|
|
877
|
-
return path5.dirname(resolved);
|
|
878
|
-
} catch {
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
const directPath = path5.join(cwd, "node_modules", "@objectstack", "studio");
|
|
882
|
-
if (fs5.existsSync(path5.join(directPath, "package.json"))) {
|
|
883
|
-
return directPath;
|
|
884
|
-
}
|
|
885
|
-
return null;
|
|
886
|
-
}
|
|
887
|
-
function hasStudioDist(studioPath) {
|
|
888
|
-
return fs5.existsSync(path5.join(studioPath, "dist", "index.html"));
|
|
889
|
-
}
|
|
890
|
-
function createStudioStaticPlugin(distPath, options) {
|
|
891
|
-
return {
|
|
892
|
-
name: "com.objectstack.studio-static",
|
|
893
|
-
init: async () => {
|
|
894
|
-
},
|
|
895
|
-
start: async (ctx) => {
|
|
896
|
-
const httpServer = ctx.getService?.("http.server");
|
|
897
|
-
if (!httpServer?.getRawApp) {
|
|
898
|
-
ctx.logger?.warn?.("Studio static: http.server service not found \u2014 skipping");
|
|
899
|
-
return;
|
|
900
|
-
}
|
|
901
|
-
const app = httpServer.getRawApp();
|
|
902
|
-
const absoluteDist = path5.resolve(distPath);
|
|
903
|
-
const indexPath = path5.join(absoluteDist, "index.html");
|
|
904
|
-
if (!fs5.existsSync(indexPath)) {
|
|
905
|
-
ctx.logger?.warn?.(`Studio static: dist not found at ${absoluteDist}`);
|
|
906
|
-
return;
|
|
907
|
-
}
|
|
908
|
-
const rawHtml = fs5.readFileSync(indexPath, "utf-8");
|
|
909
|
-
const rewrittenHtml = rawHtml.replace(
|
|
910
|
-
/(\s(?:href|src))="\/(?!\/)/g,
|
|
911
|
-
`$1="${STUDIO_PATH}/`
|
|
912
|
-
);
|
|
913
|
-
if (options?.isDev) {
|
|
914
|
-
app.get("/", (c) => c.redirect(`${STUDIO_PATH}/`));
|
|
915
|
-
}
|
|
916
|
-
app.get(STUDIO_PATH, (c) => c.redirect(`${STUDIO_PATH}/`));
|
|
917
|
-
app.get(`${STUDIO_PATH}/*`, async (c) => {
|
|
918
|
-
const reqPath = c.req.path.substring(STUDIO_PATH.length) || "/";
|
|
919
|
-
const filePath = path5.join(absoluteDist, reqPath);
|
|
920
|
-
if (!filePath.startsWith(absoluteDist)) {
|
|
921
|
-
return c.text("Forbidden", 403);
|
|
922
|
-
}
|
|
923
|
-
if (fs5.existsSync(filePath) && fs5.statSync(filePath).isFile()) {
|
|
924
|
-
const content = fs5.readFileSync(filePath);
|
|
925
|
-
return new Response(content, {
|
|
926
|
-
headers: { "content-type": mimeType(filePath) }
|
|
927
|
-
});
|
|
928
|
-
}
|
|
929
|
-
return new Response(rewrittenHtml, {
|
|
930
|
-
headers: { "content-type": "text/html; charset=utf-8" }
|
|
931
|
-
});
|
|
932
|
-
});
|
|
933
|
-
}
|
|
934
|
-
};
|
|
935
|
-
}
|
|
936
|
-
var MIME_TYPES = {
|
|
937
|
-
".html": "text/html; charset=utf-8",
|
|
938
|
-
".js": "application/javascript; charset=utf-8",
|
|
939
|
-
".mjs": "application/javascript; charset=utf-8",
|
|
940
|
-
".css": "text/css; charset=utf-8",
|
|
941
|
-
".json": "application/json; charset=utf-8",
|
|
942
|
-
".svg": "image/svg+xml",
|
|
943
|
-
".png": "image/png",
|
|
944
|
-
".jpg": "image/jpeg",
|
|
945
|
-
".jpeg": "image/jpeg",
|
|
946
|
-
".gif": "image/gif",
|
|
947
|
-
".ico": "image/x-icon",
|
|
948
|
-
".woff": "font/woff",
|
|
949
|
-
".woff2": "font/woff2",
|
|
950
|
-
".ttf": "font/ttf",
|
|
951
|
-
".map": "application/json"
|
|
952
|
-
};
|
|
953
|
-
function mimeType(filePath) {
|
|
954
|
-
const ext = path5.extname(filePath).toLowerCase();
|
|
955
|
-
return MIME_TYPES[ext] || "application/octet-stream";
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
// src/commands/serve.ts
|
|
959
|
-
var getAvailablePort = async (startPort) => {
|
|
960
|
-
const isPortAvailable = (port2) => {
|
|
961
|
-
return new Promise((resolve) => {
|
|
962
|
-
const server = net.createServer();
|
|
963
|
-
server.once("error", (err) => {
|
|
964
|
-
resolve(false);
|
|
965
|
-
});
|
|
966
|
-
server.once("listening", () => {
|
|
967
|
-
server.close(() => resolve(true));
|
|
968
|
-
});
|
|
969
|
-
server.listen(port2);
|
|
970
|
-
});
|
|
971
|
-
};
|
|
972
|
-
let port = startPort;
|
|
973
|
-
while (!await isPortAvailable(port)) {
|
|
974
|
-
port++;
|
|
975
|
-
if (port > startPort + 100) {
|
|
976
|
-
throw new Error(`Could not find an available port starting from ${startPort}`);
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
return port;
|
|
980
|
-
};
|
|
981
|
-
var serveCommand = new Command5("serve").description("Start ObjectStack server with plugins from configuration").argument("[config]", "Configuration file path", "objectstack.config.ts").option("-p, --port <port>", "Server port", "3000").option("--dev", "Run in development mode (load devPlugins)").option("--ui", "Enable Studio UI at /_studio/ (default: true in dev mode)").option("--no-server", "Skip starting HTTP server plugin").action(async (configPath, options) => {
|
|
982
|
-
let port = parseInt(options.port);
|
|
983
|
-
try {
|
|
984
|
-
const availablePort = await getAvailablePort(port);
|
|
985
|
-
if (availablePort !== port) {
|
|
986
|
-
port = availablePort;
|
|
987
|
-
}
|
|
988
|
-
} catch (e) {
|
|
989
|
-
}
|
|
990
|
-
const isDev = options.dev || process.env.NODE_ENV === "development";
|
|
991
|
-
const absolutePath = path6.resolve(process.cwd(), configPath);
|
|
992
|
-
const relativeConfig = path6.relative(process.cwd(), absolutePath);
|
|
993
|
-
if (!fs6.existsSync(absolutePath)) {
|
|
994
|
-
printError(`Configuration file not found: ${absolutePath}`);
|
|
995
|
-
console.log(chalk5.dim(" Hint: Run `objectstack init` to create a new project"));
|
|
996
|
-
process.exit(1);
|
|
997
|
-
}
|
|
998
|
-
console.log("");
|
|
999
|
-
console.log(chalk5.dim(` Loading ${relativeConfig}...`));
|
|
1000
|
-
const loadedPlugins = [];
|
|
1001
|
-
const shortPluginName = (raw) => {
|
|
1002
|
-
if (raw.includes("objectql")) return "ObjectQL";
|
|
1003
|
-
if (raw.includes("driver") && raw.includes("memory")) return "MemoryDriver";
|
|
1004
|
-
if (raw.startsWith("plugin.app.")) return raw.replace("plugin.app.", "").split(".").pop() || raw;
|
|
1005
|
-
if (raw.includes("hono")) return "HonoServer";
|
|
1006
|
-
return raw;
|
|
1007
|
-
};
|
|
1008
|
-
const trackPlugin = (name) => {
|
|
1009
|
-
loadedPlugins.push(shortPluginName(name));
|
|
1010
|
-
};
|
|
1011
|
-
const originalConsoleLog = console.log;
|
|
1012
|
-
const originalConsoleDebug = console.debug;
|
|
1013
|
-
const origStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
1014
|
-
let bootQuiet = false;
|
|
1015
|
-
const restoreOutput = () => {
|
|
1016
|
-
bootQuiet = false;
|
|
1017
|
-
process.stdout.write = origStdoutWrite;
|
|
1018
|
-
console.log = originalConsoleLog;
|
|
1019
|
-
console.debug = originalConsoleDebug;
|
|
1020
|
-
};
|
|
1021
|
-
const portShifted = parseInt(options.port) !== port;
|
|
1022
|
-
try {
|
|
1023
|
-
bootQuiet = true;
|
|
1024
|
-
process.stdout.write = (chunk, ...rest) => {
|
|
1025
|
-
if (bootQuiet) return true;
|
|
1026
|
-
return origStdoutWrite(chunk, ...rest);
|
|
1027
|
-
};
|
|
1028
|
-
console.log = (...args) => {
|
|
1029
|
-
if (!bootQuiet) originalConsoleLog(...args);
|
|
1030
|
-
};
|
|
1031
|
-
console.debug = (...args) => {
|
|
1032
|
-
if (!bootQuiet) originalConsoleDebug(...args);
|
|
1033
|
-
};
|
|
1034
|
-
const { mod } = await bundleRequire({
|
|
1035
|
-
filepath: absolutePath
|
|
1036
|
-
});
|
|
1037
|
-
const config = mod.default || mod;
|
|
1038
|
-
if (!config) {
|
|
1039
|
-
throw new Error(`No default export found in ${configPath}`);
|
|
1040
|
-
}
|
|
1041
|
-
const { Runtime } = await import("@objectstack/runtime");
|
|
1042
|
-
const loggerConfig = { level: "silent" };
|
|
1043
|
-
const runtime = new Runtime({
|
|
1044
|
-
kernel: {
|
|
1045
|
-
logger: loggerConfig
|
|
1046
|
-
}
|
|
1047
|
-
});
|
|
1048
|
-
const kernel = runtime.getKernel();
|
|
1049
|
-
let plugins = config.plugins || [];
|
|
1050
|
-
if (options.dev && config.devPlugins) {
|
|
1051
|
-
plugins = [...plugins, ...config.devPlugins];
|
|
1052
|
-
}
|
|
1053
|
-
const hasObjectQL = plugins.some((p) => p.name?.includes("objectql") || p.constructor?.name?.includes("ObjectQL"));
|
|
1054
|
-
if (config.objects && !hasObjectQL) {
|
|
1055
|
-
try {
|
|
1056
|
-
const { ObjectQLPlugin } = await import("@objectstack/objectql");
|
|
1057
|
-
await kernel.use(new ObjectQLPlugin());
|
|
1058
|
-
trackPlugin("ObjectQL");
|
|
1059
|
-
} catch (e) {
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
const hasDriver = plugins.some((p) => p.name?.includes("driver") || p.constructor?.name?.includes("Driver"));
|
|
1063
|
-
if (isDev && !hasDriver && config.objects) {
|
|
1064
|
-
try {
|
|
1065
|
-
const { DriverPlugin } = await import("@objectstack/runtime");
|
|
1066
|
-
const { InMemoryDriver } = await import("@objectstack/driver-memory");
|
|
1067
|
-
await kernel.use(new DriverPlugin(new InMemoryDriver()));
|
|
1068
|
-
trackPlugin("MemoryDriver");
|
|
1069
|
-
} catch (e) {
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
if (config.objects || config.manifest || config.apps) {
|
|
1073
|
-
try {
|
|
1074
|
-
const { AppPlugin } = await import("@objectstack/runtime");
|
|
1075
|
-
await kernel.use(new AppPlugin(config));
|
|
1076
|
-
trackPlugin("App");
|
|
1077
|
-
} catch (e) {
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
if (plugins.length > 0) {
|
|
1081
|
-
for (const plugin of plugins) {
|
|
1082
|
-
try {
|
|
1083
|
-
let pluginToLoad = plugin;
|
|
1084
|
-
if (typeof plugin === "string") {
|
|
1085
|
-
try {
|
|
1086
|
-
const imported = await import(plugin);
|
|
1087
|
-
pluginToLoad = imported.default || imported;
|
|
1088
|
-
} catch (importError) {
|
|
1089
|
-
throw new Error(`Failed to import plugin '${plugin}': ${importError.message}`);
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
if (pluginToLoad && typeof pluginToLoad === "object" && !pluginToLoad.init) {
|
|
1093
|
-
try {
|
|
1094
|
-
const { AppPlugin } = await import("@objectstack/runtime");
|
|
1095
|
-
pluginToLoad = new AppPlugin(pluginToLoad);
|
|
1096
|
-
} catch (e) {
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
await kernel.use(pluginToLoad);
|
|
1100
|
-
const pluginName = plugin.name || plugin.constructor?.name || "unnamed";
|
|
1101
|
-
trackPlugin(pluginName);
|
|
1102
|
-
} catch (e) {
|
|
1103
|
-
console.error(chalk5.red(` \u2717 Failed to load plugin: ${e.message}`));
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
if (options.server !== false) {
|
|
1108
|
-
try {
|
|
1109
|
-
const { HonoServerPlugin } = await import("@objectstack/plugin-hono-server");
|
|
1110
|
-
const serverPlugin = new HonoServerPlugin({ port });
|
|
1111
|
-
await kernel.use(serverPlugin);
|
|
1112
|
-
trackPlugin("HonoServer");
|
|
1113
|
-
} catch (e) {
|
|
1114
|
-
console.warn(chalk5.yellow(` \u26A0 HTTP server plugin not available: ${e.message}`));
|
|
1115
|
-
}
|
|
1116
|
-
try {
|
|
1117
|
-
const { createRestApiPlugin } = await import("@objectstack/rest");
|
|
1118
|
-
await kernel.use(createRestApiPlugin());
|
|
1119
|
-
trackPlugin("RestAPI");
|
|
1120
|
-
} catch (e) {
|
|
1121
|
-
}
|
|
1122
|
-
try {
|
|
1123
|
-
const { createDispatcherPlugin } = await import("@objectstack/runtime");
|
|
1124
|
-
await kernel.use(createDispatcherPlugin());
|
|
1125
|
-
trackPlugin("Dispatcher");
|
|
1126
|
-
} catch (e) {
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
const enableUI = options.ui ?? isDev;
|
|
1130
|
-
if (enableUI) {
|
|
1131
|
-
const studioPath = resolveStudioPath();
|
|
1132
|
-
if (!studioPath) {
|
|
1133
|
-
console.warn(chalk5.yellow(` \u26A0 @objectstack/studio not found \u2014 skipping UI`));
|
|
1134
|
-
} else if (hasStudioDist(studioPath)) {
|
|
1135
|
-
const distPath = path6.join(studioPath, "dist");
|
|
1136
|
-
await kernel.use(createStudioStaticPlugin(distPath, { isDev }));
|
|
1137
|
-
trackPlugin("StudioUI");
|
|
1138
|
-
} else {
|
|
1139
|
-
console.warn(chalk5.yellow(` \u26A0 Studio dist not found \u2014 run "pnpm --filter @objectstack/studio build" first`));
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
await runtime.start();
|
|
1143
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
1144
|
-
restoreOutput();
|
|
1145
|
-
printServerReady({
|
|
1146
|
-
port,
|
|
1147
|
-
configFile: relativeConfig,
|
|
1148
|
-
isDev,
|
|
1149
|
-
pluginCount: loadedPlugins.length,
|
|
1150
|
-
pluginNames: loadedPlugins,
|
|
1151
|
-
uiEnabled: enableUI,
|
|
1152
|
-
studioPath: STUDIO_PATH
|
|
1153
|
-
});
|
|
1154
|
-
process.on("SIGINT", async () => {
|
|
1155
|
-
console.warn(chalk5.yellow(`
|
|
1156
|
-
|
|
1157
|
-
\u23F9 Stopping server...`));
|
|
1158
|
-
await runtime.getKernel().shutdown();
|
|
1159
|
-
console.log(chalk5.green(`\u2705 Server stopped`));
|
|
1160
|
-
process.exit(0);
|
|
1161
|
-
});
|
|
1162
|
-
} catch (error) {
|
|
1163
|
-
restoreOutput();
|
|
1164
|
-
console.log("");
|
|
1165
|
-
printError(error.message || String(error));
|
|
1166
|
-
if (process.env.DEBUG) console.error(chalk5.dim(error.stack));
|
|
1167
|
-
process.exit(1);
|
|
1168
|
-
}
|
|
1169
|
-
});
|
|
1170
|
-
|
|
1171
|
-
// src/commands/studio.ts
|
|
1172
|
-
import { Command as Command6 } from "commander";
|
|
1173
|
-
import { spawn as spawn2 } from "child_process";
|
|
1174
|
-
var studioCommand = new Command6("studio").description("Launch Studio UI with development server").argument("[config]", "Configuration file path", "objectstack.config.ts").option("-p, --port <port>", "Server port", "3000").action(async (configPath, options) => {
|
|
1175
|
-
printHeader("Studio");
|
|
1176
|
-
printKV("Mode", "dev + ui", "\u{1F3A8}");
|
|
1177
|
-
printStep("Delegating to serve --dev --ui \u2026");
|
|
1178
|
-
console.log("");
|
|
1179
|
-
const binPath = process.argv[1];
|
|
1180
|
-
const args = [
|
|
1181
|
-
binPath,
|
|
1182
|
-
"serve",
|
|
1183
|
-
configPath,
|
|
1184
|
-
"--dev",
|
|
1185
|
-
"--ui",
|
|
1186
|
-
"--port",
|
|
1187
|
-
options.port
|
|
1188
|
-
];
|
|
1189
|
-
const child = spawn2(process.execPath, args, {
|
|
1190
|
-
stdio: "inherit",
|
|
1191
|
-
env: { ...process.env, NODE_ENV: "development" }
|
|
1192
|
-
});
|
|
1193
|
-
child.on("exit", (code) => process.exit(code ?? 0));
|
|
1194
|
-
});
|
|
1195
|
-
|
|
1196
|
-
// src/commands/test.ts
|
|
1197
|
-
import { Command as Command7 } from "commander";
|
|
1198
|
-
import chalk6 from "chalk";
|
|
1199
|
-
import path7 from "path";
|
|
1200
|
-
import fs7 from "fs";
|
|
1201
|
-
import { QA as CoreQA } from "@objectstack/core";
|
|
1202
|
-
function resolveGlob(pattern) {
|
|
1203
|
-
if (!pattern.includes("*")) {
|
|
1204
|
-
return fs7.existsSync(pattern) ? [pattern] : [];
|
|
1205
|
-
}
|
|
1206
|
-
const parts = pattern.split(path7.sep.replace("\\", "/"));
|
|
1207
|
-
const segments = pattern.includes("/") ? pattern.split("/") : parts;
|
|
1208
|
-
let baseDir = ".";
|
|
1209
|
-
let globStart = 0;
|
|
1210
|
-
for (let i = 0; i < segments.length; i++) {
|
|
1211
|
-
if (segments[i].includes("*")) {
|
|
1212
|
-
globStart = i;
|
|
1213
|
-
break;
|
|
1214
|
-
}
|
|
1215
|
-
baseDir = i === 0 ? segments[i] : path7.join(baseDir, segments[i]);
|
|
1216
|
-
}
|
|
1217
|
-
if (!fs7.existsSync(baseDir)) return [];
|
|
1218
|
-
const globPortion = segments.slice(globStart).join("/");
|
|
1219
|
-
const regexStr = globPortion.replace(/\./g, "\\.").replace(/\*\*\//g, "(.+/)?").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*");
|
|
1220
|
-
const regex = new RegExp(`^${regexStr}$`);
|
|
1221
|
-
const entries = fs7.readdirSync(baseDir, { recursive: true, encoding: "utf-8" });
|
|
1222
|
-
return entries.filter((entry) => regex.test(entry.replace(/\\/g, "/"))).map((entry) => path7.join(baseDir, entry)).filter((fullPath) => fs7.statSync(fullPath).isFile());
|
|
1223
|
-
}
|
|
1224
|
-
var testCommand = new Command7("test").description("Run Quality Protocol test scenarios against a running server").argument("[files]", 'Glob pattern for test files (e.g. "qa/*.test.json")', "qa/*.test.json").option("--url <url>", "Target base URL", "http://localhost:3000").option("--token <token>", "Authentication token").action(async (filesPattern, options) => {
|
|
1225
|
-
console.log(chalk6.bold(`
|
|
1226
|
-
\u{1F9EA} ObjectStack Quality Protocol Runner`));
|
|
1227
|
-
console.log(chalk6.dim(`-------------------------------------`));
|
|
1228
|
-
console.log(`Target: ${chalk6.blue(options.url)}`);
|
|
1229
|
-
const adapter = new CoreQA.HttpTestAdapter(options.url, options.token);
|
|
1230
|
-
const runner = new CoreQA.TestRunner(adapter);
|
|
1231
|
-
const testFiles = resolveGlob(filesPattern);
|
|
1232
|
-
if (testFiles.length === 0) {
|
|
1233
|
-
console.warn(chalk6.yellow(`No test files found matching: ${filesPattern}`));
|
|
1234
|
-
return;
|
|
1235
|
-
}
|
|
1236
|
-
console.log(`Found ${testFiles.length} test suites.`);
|
|
1237
|
-
let totalPassed = 0;
|
|
1238
|
-
let totalFailed = 0;
|
|
1239
|
-
for (const file of testFiles) {
|
|
1240
|
-
console.log(`
|
|
1241
|
-
\u{1F4C4} Running suite: ${chalk6.bold(path7.basename(file))}`);
|
|
1242
|
-
try {
|
|
1243
|
-
const content = fs7.readFileSync(file, "utf-8");
|
|
1244
|
-
const suite = JSON.parse(content);
|
|
1245
|
-
const results = await runner.runSuite(suite);
|
|
1246
|
-
for (const result of results) {
|
|
1247
|
-
const icon = result.passed ? "\u2705" : "\u274C";
|
|
1248
|
-
console.log(` ${icon} Scenario: ${result.scenarioId} (${result.duration}ms)`);
|
|
1249
|
-
if (!result.passed) {
|
|
1250
|
-
console.error(chalk6.red(` Error: ${result.error}`));
|
|
1251
|
-
result.steps.forEach((step) => {
|
|
1252
|
-
if (!step.passed) {
|
|
1253
|
-
console.error(chalk6.red(` Step Failed: ${step.stepName}`));
|
|
1254
|
-
if (step.output) console.error(` Output:`, step.output);
|
|
1255
|
-
if (step.error) console.error(` Error:`, step.error);
|
|
1256
|
-
}
|
|
1257
|
-
});
|
|
1258
|
-
totalFailed++;
|
|
1259
|
-
} else {
|
|
1260
|
-
totalPassed++;
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
|
-
} catch (e) {
|
|
1264
|
-
console.error(chalk6.red(`Failed to load or run suite ${file}: ${e}`));
|
|
1265
|
-
totalFailed++;
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
console.log(chalk6.dim(`
|
|
1269
|
-
-------------------------------------`));
|
|
1270
|
-
if (totalFailed > 0) {
|
|
1271
|
-
console.log(chalk6.red(`FAILED: ${totalFailed} scenarios failed. ${totalPassed} passed.`));
|
|
1272
|
-
process.exit(1);
|
|
1273
|
-
} else {
|
|
1274
|
-
console.log(chalk6.green(`SUCCESS: All ${totalPassed} scenarios passed.`));
|
|
1275
|
-
process.exit(0);
|
|
1276
|
-
}
|
|
1277
|
-
});
|
|
1278
|
-
|
|
1279
|
-
// src/commands/validate.ts
|
|
1280
|
-
import { Command as Command8 } from "commander";
|
|
1281
|
-
import chalk7 from "chalk";
|
|
1282
|
-
import { ObjectStackDefinitionSchema as ObjectStackDefinitionSchema2, normalizeStackInput as normalizeStackInput3 } from "@objectstack/spec";
|
|
1283
|
-
var validateCommand = new Command8("validate").description("Validate ObjectStack configuration against the protocol schema").argument("[config]", "Configuration file path").option("--strict", "Treat warnings as errors").option("--json", "Output results as JSON").action(async (configPath, options) => {
|
|
1284
|
-
const timer = createTimer();
|
|
1285
|
-
if (!options.json) {
|
|
1286
|
-
printHeader("Validate");
|
|
1287
|
-
}
|
|
1288
|
-
try {
|
|
1289
|
-
if (!options.json) printStep("Loading configuration...");
|
|
1290
|
-
const { config, absolutePath, duration } = await loadConfig(configPath);
|
|
1291
|
-
if (!options.json) {
|
|
1292
|
-
printKV("Config", absolutePath);
|
|
1293
|
-
printKV("Load time", `${duration}ms`);
|
|
1294
|
-
}
|
|
1295
|
-
if (!options.json) printStep("Validating against ObjectStack Protocol...");
|
|
1296
|
-
const normalized = normalizeStackInput3(config);
|
|
1297
|
-
const result = ObjectStackDefinitionSchema2.safeParse(normalized);
|
|
1298
|
-
if (!result.success) {
|
|
1299
|
-
if (options.json) {
|
|
1300
|
-
console.log(JSON.stringify({
|
|
1301
|
-
valid: false,
|
|
1302
|
-
errors: result.error.issues,
|
|
1303
|
-
duration: timer.elapsed()
|
|
1304
|
-
}, null, 2));
|
|
1305
|
-
process.exit(1);
|
|
1306
|
-
}
|
|
1307
|
-
console.log("");
|
|
1308
|
-
printError("Validation failed");
|
|
1309
|
-
formatZodErrors(result.error);
|
|
1310
|
-
process.exit(1);
|
|
1311
|
-
}
|
|
1312
|
-
const stats = collectMetadataStats(config);
|
|
1313
|
-
if (options.json) {
|
|
1314
|
-
console.log(JSON.stringify({
|
|
1315
|
-
valid: true,
|
|
1316
|
-
manifest: config.manifest,
|
|
1317
|
-
stats,
|
|
1318
|
-
duration: timer.elapsed()
|
|
1319
|
-
}, null, 2));
|
|
1320
|
-
return;
|
|
1321
|
-
}
|
|
1322
|
-
const warnings = [];
|
|
1323
|
-
if (stats.objects === 0) {
|
|
1324
|
-
warnings.push("No objects defined \u2014 this stack has no data model");
|
|
1325
|
-
}
|
|
1326
|
-
if (stats.apps === 0 && stats.plugins === 0) {
|
|
1327
|
-
warnings.push("No apps or plugins defined \u2014 this stack may not do much");
|
|
1328
|
-
}
|
|
1329
|
-
if (!config.manifest?.id) {
|
|
1330
|
-
warnings.push("Missing manifest.id \u2014 required for deployment");
|
|
1331
|
-
}
|
|
1332
|
-
if (!config.manifest?.namespace) {
|
|
1333
|
-
warnings.push("Missing manifest.namespace \u2014 required for multi-app hosting");
|
|
1334
|
-
}
|
|
1335
|
-
console.log("");
|
|
1336
|
-
printSuccess(`Validation passed ${chalk7.dim(`(${timer.display()})`)}`);
|
|
1337
|
-
console.log("");
|
|
1338
|
-
if (config.manifest) {
|
|
1339
|
-
console.log(` ${chalk7.bold(config.manifest.name || config.manifest.id || "Unnamed")} ${chalk7.dim(`v${config.manifest.version || "0.0.0"}`)}`);
|
|
1340
|
-
if (config.manifest.description) {
|
|
1341
|
-
console.log(chalk7.dim(` ${config.manifest.description}`));
|
|
1342
|
-
}
|
|
1343
|
-
console.log("");
|
|
1344
|
-
}
|
|
1345
|
-
printMetadataStats(stats);
|
|
1346
|
-
if (warnings.length > 0) {
|
|
1347
|
-
console.log("");
|
|
1348
|
-
for (const w of warnings) {
|
|
1349
|
-
console.log(chalk7.yellow(` \u26A0 ${w}`));
|
|
1350
|
-
}
|
|
1351
|
-
if (options.strict) {
|
|
1352
|
-
console.log("");
|
|
1353
|
-
printError("Strict mode: warnings treated as errors");
|
|
1354
|
-
process.exit(1);
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
console.log("");
|
|
1358
|
-
} catch (error) {
|
|
1359
|
-
if (options.json) {
|
|
1360
|
-
console.log(JSON.stringify({
|
|
1361
|
-
valid: false,
|
|
1362
|
-
error: error.message,
|
|
1363
|
-
duration: timer.elapsed()
|
|
1364
|
-
}, null, 2));
|
|
1365
|
-
process.exit(1);
|
|
1366
|
-
}
|
|
1367
|
-
console.log("");
|
|
1368
|
-
printError(error.message || String(error));
|
|
1369
|
-
process.exit(1);
|
|
1370
|
-
}
|
|
1371
|
-
});
|
|
1372
|
-
|
|
1373
|
-
// src/commands/init.ts
|
|
1374
|
-
import { Command as Command9 } from "commander";
|
|
1375
|
-
import chalk8 from "chalk";
|
|
1376
|
-
import fs8 from "fs";
|
|
1377
|
-
import path8 from "path";
|
|
1378
|
-
var TEMPLATES = {
|
|
1379
|
-
app: {
|
|
1380
|
-
description: "Full application with objects, views, and actions",
|
|
1381
|
-
dependencies: {
|
|
1382
|
-
"@objectstack/spec": "workspace:*",
|
|
1383
|
-
"@objectstack/runtime": "workspace:^",
|
|
1384
|
-
"@objectstack/objectql": "workspace:^",
|
|
1385
|
-
"@objectstack/driver-memory": "workspace:^"
|
|
1386
|
-
},
|
|
1387
|
-
devDependencies: {
|
|
1388
|
-
"@objectstack/cli": "workspace:*",
|
|
1389
|
-
"typescript": "^5.3.0"
|
|
1390
|
-
},
|
|
1391
|
-
scripts: {
|
|
1392
|
-
dev: "objectstack dev",
|
|
1393
|
-
start: "objectstack serve",
|
|
1394
|
-
build: "objectstack compile",
|
|
1395
|
-
validate: "objectstack validate",
|
|
1396
|
-
typecheck: "tsc --noEmit"
|
|
1397
|
-
},
|
|
1398
|
-
configContent: (name) => `import { defineStack } from '@objectstack/spec';
|
|
1399
|
-
import * as objects from './src/objects';
|
|
1400
|
-
|
|
1401
|
-
export default defineStack({
|
|
1402
|
-
manifest: {
|
|
1403
|
-
id: 'com.example.${name}',
|
|
1404
|
-
namespace: '${name}',
|
|
1405
|
-
version: '0.1.0',
|
|
1406
|
-
type: 'app',
|
|
1407
|
-
name: '${toTitleCase(name)}',
|
|
1408
|
-
description: '${toTitleCase(name)} application built with ObjectStack',
|
|
1409
|
-
},
|
|
1410
|
-
|
|
1411
|
-
objects: Object.values(objects),
|
|
1412
|
-
});
|
|
1413
|
-
`,
|
|
1414
|
-
srcFiles: {
|
|
1415
|
-
"src/objects/index.ts": (name) => `export { default as ${toCamelCase2(name)} } from './${name}';
|
|
1416
|
-
`,
|
|
1417
|
-
"src/objects/__name__.ts": (name) => `import { Data } from '@objectstack/spec';
|
|
1418
|
-
|
|
1419
|
-
const ${toCamelCase2(name)}: Data.Object = {
|
|
1420
|
-
name: '${name}',
|
|
1421
|
-
label: '${toTitleCase(name)}',
|
|
1422
|
-
ownership: 'own',
|
|
1423
|
-
fields: {
|
|
1424
|
-
name: {
|
|
1425
|
-
type: 'text',
|
|
1426
|
-
label: 'Name',
|
|
1427
|
-
required: true,
|
|
1428
|
-
},
|
|
1429
|
-
description: {
|
|
1430
|
-
type: 'textarea',
|
|
1431
|
-
label: 'Description',
|
|
1432
|
-
},
|
|
1433
|
-
status: {
|
|
1434
|
-
type: 'select',
|
|
1435
|
-
label: 'Status',
|
|
1436
|
-
options: [
|
|
1437
|
-
{ label: 'Draft', value: 'draft' },
|
|
1438
|
-
{ label: 'Active', value: 'active' },
|
|
1439
|
-
{ label: 'Archived', value: 'archived' },
|
|
1440
|
-
],
|
|
1441
|
-
defaultValue: 'draft',
|
|
1442
|
-
},
|
|
1443
|
-
},
|
|
1444
|
-
};
|
|
1445
|
-
|
|
1446
|
-
export default ${toCamelCase2(name)};
|
|
1447
|
-
`
|
|
1448
|
-
}
|
|
1449
|
-
},
|
|
1450
|
-
plugin: {
|
|
1451
|
-
description: "Reusable plugin with objects and extensions",
|
|
1452
|
-
dependencies: {
|
|
1453
|
-
"@objectstack/spec": "workspace:*"
|
|
1454
|
-
},
|
|
1455
|
-
devDependencies: {
|
|
1456
|
-
"typescript": "^5.3.0",
|
|
1457
|
-
"vitest": "^4.0.18"
|
|
1458
|
-
},
|
|
1459
|
-
scripts: {
|
|
1460
|
-
build: "objectstack compile",
|
|
1461
|
-
validate: "objectstack validate",
|
|
1462
|
-
test: "vitest run",
|
|
1463
|
-
typecheck: "tsc --noEmit"
|
|
1464
|
-
},
|
|
1465
|
-
configContent: (name) => `import { defineStack } from '@objectstack/spec';
|
|
1466
|
-
import * as objects from './src/objects';
|
|
1467
|
-
|
|
1468
|
-
export default defineStack({
|
|
1469
|
-
manifest: {
|
|
1470
|
-
id: 'com.objectstack.plugin-${name}',
|
|
1471
|
-
namespace: 'plugin_${name}',
|
|
1472
|
-
version: '0.1.0',
|
|
1473
|
-
type: 'plugin',
|
|
1474
|
-
name: '${toTitleCase(name)} Plugin',
|
|
1475
|
-
description: 'ObjectStack Plugin: ${toTitleCase(name)}',
|
|
1476
|
-
},
|
|
1477
|
-
|
|
1478
|
-
objects: Object.values(objects),
|
|
1479
|
-
});
|
|
1480
|
-
`,
|
|
1481
|
-
srcFiles: {
|
|
1482
|
-
"src/objects/index.ts": (name) => `export { default as ${toCamelCase2(name)} } from './${name}';
|
|
1483
|
-
`,
|
|
1484
|
-
"src/objects/__name__.ts": (name) => `import { Data } from '@objectstack/spec';
|
|
1485
|
-
|
|
1486
|
-
const ${toCamelCase2(name)}: Data.Object = {
|
|
1487
|
-
name: '${name}',
|
|
1488
|
-
label: '${toTitleCase(name)}',
|
|
1489
|
-
ownership: 'own',
|
|
1490
|
-
fields: {
|
|
1491
|
-
name: {
|
|
1492
|
-
type: 'text',
|
|
1493
|
-
label: 'Name',
|
|
1494
|
-
required: true,
|
|
1495
|
-
},
|
|
1496
|
-
},
|
|
1497
|
-
};
|
|
1498
|
-
|
|
1499
|
-
export default ${toCamelCase2(name)};
|
|
1500
|
-
`
|
|
1501
|
-
}
|
|
1502
|
-
},
|
|
1503
|
-
empty: {
|
|
1504
|
-
description: "Minimal project with just a config file",
|
|
1505
|
-
dependencies: {
|
|
1506
|
-
"@objectstack/spec": "workspace:*"
|
|
1507
|
-
},
|
|
1508
|
-
devDependencies: {
|
|
1509
|
-
"@objectstack/cli": "workspace:*",
|
|
1510
|
-
"typescript": "^5.3.0"
|
|
1511
|
-
},
|
|
1512
|
-
scripts: {
|
|
1513
|
-
build: "objectstack compile",
|
|
1514
|
-
validate: "objectstack validate",
|
|
1515
|
-
typecheck: "tsc --noEmit"
|
|
1516
|
-
},
|
|
1517
|
-
configContent: (name) => `import { defineStack } from '@objectstack/spec';
|
|
1518
|
-
|
|
1519
|
-
export default defineStack({
|
|
1520
|
-
manifest: {
|
|
1521
|
-
id: 'com.example.${name}',
|
|
1522
|
-
namespace: '${name}',
|
|
1523
|
-
version: '0.1.0',
|
|
1524
|
-
type: 'app',
|
|
1525
|
-
name: '${toTitleCase(name)}',
|
|
1526
|
-
description: '',
|
|
1527
|
-
},
|
|
1528
|
-
});
|
|
1529
|
-
`,
|
|
1530
|
-
srcFiles: {}
|
|
1531
|
-
}
|
|
1532
|
-
};
|
|
1533
|
-
function toCamelCase2(str) {
|
|
1534
|
-
return str.replace(/[-_]([a-z])/g, (_, c) => c.toUpperCase());
|
|
1535
|
-
}
|
|
1536
|
-
function toTitleCase(str) {
|
|
1537
|
-
return str.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1538
|
-
}
|
|
1539
|
-
var initCommand = new Command9("init").description("Initialize a new ObjectStack project in the current directory").argument("[name]", "Project name (defaults to directory name)").option("-t, --template <template>", "Template: app, plugin, empty", "app").option("--no-install", "Skip dependency installation").action(async (name, options) => {
|
|
1540
|
-
printHeader("Init");
|
|
1541
|
-
const cwd = process.cwd();
|
|
1542
|
-
const projectName = name || path8.basename(cwd);
|
|
1543
|
-
const template = TEMPLATES[options.template];
|
|
1544
|
-
if (!template) {
|
|
1545
|
-
printError(`Unknown template: ${options.template}`);
|
|
1546
|
-
console.log(chalk8.dim(` Available: ${Object.keys(TEMPLATES).join(", ")}`));
|
|
1547
|
-
process.exit(1);
|
|
1548
|
-
}
|
|
1549
|
-
if (fs8.existsSync(path8.join(cwd, "objectstack.config.ts"))) {
|
|
1550
|
-
printError("objectstack.config.ts already exists in this directory");
|
|
1551
|
-
console.log(chalk8.dim(" Use `objectstack generate` to add metadata to an existing project"));
|
|
1552
|
-
process.exit(1);
|
|
1553
|
-
}
|
|
1554
|
-
printKV("Project", projectName);
|
|
1555
|
-
printKV("Template", `${options.template} \u2014 ${template.description}`);
|
|
1556
|
-
printKV("Directory", cwd);
|
|
1557
|
-
console.log("");
|
|
1558
|
-
const createdFiles = [];
|
|
1559
|
-
try {
|
|
1560
|
-
const pkgPath = path8.join(cwd, "package.json");
|
|
1561
|
-
if (!fs8.existsSync(pkgPath)) {
|
|
1562
|
-
const pkg2 = {
|
|
1563
|
-
name: projectName,
|
|
1564
|
-
version: "0.1.0",
|
|
1565
|
-
private: true,
|
|
1566
|
-
type: "module",
|
|
1567
|
-
scripts: template.scripts,
|
|
1568
|
-
dependencies: template.dependencies,
|
|
1569
|
-
devDependencies: template.devDependencies
|
|
1570
|
-
};
|
|
1571
|
-
fs8.writeFileSync(pkgPath, JSON.stringify(pkg2, null, 2) + "\n");
|
|
1572
|
-
createdFiles.push("package.json");
|
|
1573
|
-
} else {
|
|
1574
|
-
printInfo("package.json already exists, skipping");
|
|
1575
|
-
}
|
|
1576
|
-
const configContent = template.configContent(projectName);
|
|
1577
|
-
fs8.writeFileSync(path8.join(cwd, "objectstack.config.ts"), configContent);
|
|
1578
|
-
createdFiles.push("objectstack.config.ts");
|
|
1579
|
-
const tsconfigPath = path8.join(cwd, "tsconfig.json");
|
|
1580
|
-
if (!fs8.existsSync(tsconfigPath)) {
|
|
1581
|
-
const tsconfig = {
|
|
1582
|
-
compilerOptions: {
|
|
1583
|
-
target: "ES2022",
|
|
1584
|
-
module: "ESNext",
|
|
1585
|
-
moduleResolution: "bundler",
|
|
1586
|
-
strict: true,
|
|
1587
|
-
esModuleInterop: true,
|
|
1588
|
-
skipLibCheck: true,
|
|
1589
|
-
outDir: "dist",
|
|
1590
|
-
rootDir: ".",
|
|
1591
|
-
declaration: true
|
|
1592
|
-
},
|
|
1593
|
-
include: ["*.ts", "src/**/*"],
|
|
1594
|
-
exclude: ["dist", "node_modules"]
|
|
1595
|
-
};
|
|
1596
|
-
fs8.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n");
|
|
1597
|
-
createdFiles.push("tsconfig.json");
|
|
1598
|
-
}
|
|
1599
|
-
for (const [filePath, contentFn] of Object.entries(template.srcFiles)) {
|
|
1600
|
-
const resolvedPath = filePath.replace("__name__", projectName);
|
|
1601
|
-
const fullPath = path8.join(cwd, resolvedPath);
|
|
1602
|
-
const dir = path8.dirname(fullPath);
|
|
1603
|
-
if (!fs8.existsSync(dir)) {
|
|
1604
|
-
fs8.mkdirSync(dir, { recursive: true });
|
|
1605
|
-
}
|
|
1606
|
-
fs8.writeFileSync(fullPath, contentFn(projectName));
|
|
1607
|
-
createdFiles.push(resolvedPath);
|
|
1608
|
-
}
|
|
1609
|
-
const gitignorePath = path8.join(cwd, ".gitignore");
|
|
1610
|
-
if (!fs8.existsSync(gitignorePath)) {
|
|
1611
|
-
fs8.writeFileSync(gitignorePath, `node_modules/
|
|
1612
|
-
dist/
|
|
1613
|
-
*.tsbuildinfo
|
|
1614
|
-
`);
|
|
1615
|
-
createdFiles.push(".gitignore");
|
|
1616
|
-
}
|
|
1617
|
-
console.log(chalk8.bold(" Created files:"));
|
|
1618
|
-
for (const f of createdFiles) {
|
|
1619
|
-
console.log(chalk8.green(` + ${f}`));
|
|
1620
|
-
}
|
|
1621
|
-
console.log("");
|
|
1622
|
-
if (options.install !== false) {
|
|
1623
|
-
printStep("Installing dependencies...");
|
|
1624
|
-
const { execSync: execSync3 } = await import("child_process");
|
|
1625
|
-
try {
|
|
1626
|
-
execSync3("pnpm install", { stdio: "inherit", cwd });
|
|
1627
|
-
} catch {
|
|
1628
|
-
printWarning2("Dependency installation failed. Run `pnpm install` manually.");
|
|
1629
|
-
}
|
|
1630
|
-
}
|
|
1631
|
-
printSuccess("Project initialized!");
|
|
1632
|
-
console.log("");
|
|
1633
|
-
console.log(chalk8.bold(" Next steps:"));
|
|
1634
|
-
console.log(chalk8.dim(" objectstack validate # Check configuration"));
|
|
1635
|
-
console.log(chalk8.dim(" objectstack dev # Start development server"));
|
|
1636
|
-
console.log(chalk8.dim(" objectstack generate # Add objects, views, etc."));
|
|
1637
|
-
console.log("");
|
|
1638
|
-
} catch (error) {
|
|
1639
|
-
printError(error.message || String(error));
|
|
1640
|
-
process.exit(1);
|
|
1641
|
-
}
|
|
1642
|
-
});
|
|
1643
|
-
function printWarning2(msg) {
|
|
1644
|
-
console.log(chalk8.yellow(` \u26A0 ${msg}`));
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
// src/commands/info.ts
|
|
1648
|
-
import { Command as Command10 } from "commander";
|
|
1649
|
-
import chalk9 from "chalk";
|
|
1650
|
-
import { normalizeStackInput as normalizeStackInput4 } from "@objectstack/spec";
|
|
1651
|
-
var infoCommand = new Command10("info").description("Display metadata summary of an ObjectStack configuration").argument("[config]", "Configuration file path").option("--json", "Output as JSON").action(async (configPath, options) => {
|
|
1652
|
-
const timer = createTimer();
|
|
1653
|
-
if (!options.json) {
|
|
1654
|
-
printHeader("Info");
|
|
1655
|
-
}
|
|
1656
|
-
try {
|
|
1657
|
-
const { config: rawConfig, absolutePath, duration } = await loadConfig(configPath);
|
|
1658
|
-
const config = normalizeStackInput4(rawConfig);
|
|
1659
|
-
const stats = collectMetadataStats(config);
|
|
1660
|
-
if (options.json) {
|
|
1661
|
-
console.log(JSON.stringify({
|
|
1662
|
-
config: absolutePath,
|
|
1663
|
-
manifest: config.manifest || null,
|
|
1664
|
-
stats,
|
|
1665
|
-
objects: (config.objects || []).map((o) => ({
|
|
1666
|
-
name: o.name,
|
|
1667
|
-
label: o.label,
|
|
1668
|
-
fields: o.fields ? Object.keys(o.fields).length : 0
|
|
1669
|
-
})),
|
|
1670
|
-
loadTime: duration
|
|
1671
|
-
}, null, 2));
|
|
1672
|
-
return;
|
|
1673
|
-
}
|
|
1674
|
-
if (config.manifest) {
|
|
1675
|
-
const m = config.manifest;
|
|
1676
|
-
console.log("");
|
|
1677
|
-
console.log(` ${chalk9.bold(m.name || m.id || "Unnamed")} ${chalk9.dim(`v${m.version || "0.0.0"}`)}`);
|
|
1678
|
-
if (m.id) console.log(chalk9.dim(` ${m.id}`));
|
|
1679
|
-
if (m.description) console.log(chalk9.dim(` ${m.description}`));
|
|
1680
|
-
if (m.namespace) printKV(" Namespace", m.namespace);
|
|
1681
|
-
if (m.type) printKV(" Type", m.type);
|
|
1682
|
-
}
|
|
1683
|
-
console.log("");
|
|
1684
|
-
printMetadataStats(stats);
|
|
1685
|
-
if (config.objects && config.objects.length > 0) {
|
|
1686
|
-
console.log("");
|
|
1687
|
-
console.log(chalk9.bold(" Objects:"));
|
|
1688
|
-
for (const obj of config.objects) {
|
|
1689
|
-
const fieldCount = obj.fields ? Object.keys(obj.fields).length : 0;
|
|
1690
|
-
const ownership = obj.ownership || "own";
|
|
1691
|
-
console.log(
|
|
1692
|
-
` ${chalk9.cyan(obj.name || "?")}` + chalk9.dim(` (${fieldCount} fields, ${ownership})`) + (obj.label ? chalk9.dim(` \u2014 ${obj.label}`) : "")
|
|
1693
|
-
);
|
|
1694
|
-
}
|
|
1695
|
-
}
|
|
1696
|
-
if (config.agents && config.agents.length > 0) {
|
|
1697
|
-
console.log("");
|
|
1698
|
-
console.log(chalk9.bold(" Agents:"));
|
|
1699
|
-
for (const agent of config.agents) {
|
|
1700
|
-
console.log(
|
|
1701
|
-
` ${chalk9.magenta(agent.name || "?")}` + (agent.role ? chalk9.dim(` \u2014 ${agent.role}`) : "")
|
|
1702
|
-
);
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
if (config.apps && config.apps.length > 0) {
|
|
1706
|
-
console.log("");
|
|
1707
|
-
console.log(chalk9.bold(" Apps:"));
|
|
1708
|
-
for (const app of config.apps) {
|
|
1709
|
-
console.log(
|
|
1710
|
-
` ${chalk9.green(app.name || "?")}` + (app.label ? chalk9.dim(` \u2014 ${app.label}`) : "")
|
|
1711
|
-
);
|
|
1712
|
-
}
|
|
1713
|
-
}
|
|
1714
|
-
console.log("");
|
|
1715
|
-
console.log(chalk9.dim(` Loaded in ${duration}ms`));
|
|
1716
|
-
console.log("");
|
|
1717
|
-
} catch (error) {
|
|
1718
|
-
if (options.json) {
|
|
1719
|
-
console.log(JSON.stringify({ error: error.message }));
|
|
1720
|
-
process.exit(1);
|
|
1721
|
-
}
|
|
1722
|
-
console.log("");
|
|
1723
|
-
printError(error.message || String(error));
|
|
1724
|
-
process.exit(1);
|
|
1725
|
-
}
|
|
1726
|
-
});
|
|
1727
|
-
|
|
1728
|
-
// src/commands/generate.ts
|
|
1729
|
-
import { Command as Command11 } from "commander";
|
|
1730
|
-
import chalk10 from "chalk";
|
|
1731
|
-
import fs9 from "fs";
|
|
1732
|
-
import path9 from "path";
|
|
1733
|
-
var GENERATORS = {
|
|
1734
|
-
object: {
|
|
1735
|
-
description: "Business data object",
|
|
1736
|
-
defaultDir: "src/objects",
|
|
1737
|
-
generate: (name) => `import { Data } from '@objectstack/spec';
|
|
1738
|
-
|
|
1739
|
-
/**
|
|
1740
|
-
* ${toTitleCase2(name)} Object
|
|
1741
|
-
*/
|
|
1742
|
-
const ${toCamelCase3(name)}: Data.Object = {
|
|
1743
|
-
name: '${toSnakeCase(name)}',
|
|
1744
|
-
label: '${toTitleCase2(name)}',
|
|
1745
|
-
pluralLabel: '${toTitleCase2(name)}s',
|
|
1746
|
-
ownership: 'own',
|
|
1747
|
-
fields: {
|
|
1748
|
-
name: {
|
|
1749
|
-
type: 'text',
|
|
1750
|
-
label: 'Name',
|
|
1751
|
-
required: true,
|
|
1752
|
-
maxLength: 255,
|
|
1753
|
-
},
|
|
1754
|
-
description: {
|
|
1755
|
-
type: 'textarea',
|
|
1756
|
-
label: 'Description',
|
|
1757
|
-
},
|
|
1758
|
-
},
|
|
1759
|
-
};
|
|
1760
|
-
|
|
1761
|
-
export default ${toCamelCase3(name)};
|
|
1762
|
-
`
|
|
1763
|
-
},
|
|
1764
|
-
view: {
|
|
1765
|
-
description: "List or form view",
|
|
1766
|
-
defaultDir: "src/views",
|
|
1767
|
-
generate: (name) => `import { UI } from '@objectstack/spec';
|
|
1768
|
-
|
|
1769
|
-
/**
|
|
1770
|
-
* ${toTitleCase2(name)} List View
|
|
1771
|
-
*/
|
|
1772
|
-
const ${toCamelCase3(name)}ListView: UI.View = {
|
|
1773
|
-
name: '${toSnakeCase(name)}_list',
|
|
1774
|
-
label: '${toTitleCase2(name)} List',
|
|
1775
|
-
type: 'list',
|
|
1776
|
-
objectName: '${toSnakeCase(name)}',
|
|
1777
|
-
list: {
|
|
1778
|
-
type: 'grid',
|
|
1779
|
-
columns: [
|
|
1780
|
-
{ field: 'name', width: 200 },
|
|
1781
|
-
],
|
|
1782
|
-
defaultSort: { field: 'name', direction: 'asc' },
|
|
1783
|
-
pageSize: 25,
|
|
1784
|
-
},
|
|
1785
|
-
};
|
|
1786
|
-
|
|
1787
|
-
export default ${toCamelCase3(name)}ListView;
|
|
1788
|
-
`
|
|
1789
|
-
},
|
|
1790
|
-
action: {
|
|
1791
|
-
description: "Button or batch action",
|
|
1792
|
-
defaultDir: "src/actions",
|
|
1793
|
-
generate: (name) => `import { UI } from '@objectstack/spec';
|
|
1794
|
-
|
|
1795
|
-
/**
|
|
1796
|
-
* ${toTitleCase2(name)} Action
|
|
1797
|
-
*/
|
|
1798
|
-
const ${toCamelCase3(name)}Action: UI.Action = {
|
|
1799
|
-
name: '${toSnakeCase(name)}',
|
|
1800
|
-
label: '${toTitleCase2(name)}',
|
|
1801
|
-
type: 'custom',
|
|
1802
|
-
objectName: '${toSnakeCase(name)}',
|
|
1803
|
-
handler: {
|
|
1804
|
-
type: 'flow',
|
|
1805
|
-
target: '${toSnakeCase(name)}_flow',
|
|
1806
|
-
},
|
|
1807
|
-
};
|
|
1808
|
-
|
|
1809
|
-
export default ${toCamelCase3(name)}Action;
|
|
1810
|
-
`
|
|
1811
|
-
},
|
|
1812
|
-
flow: {
|
|
1813
|
-
description: "Automation flow",
|
|
1814
|
-
defaultDir: "src/flows",
|
|
1815
|
-
generate: (name) => `import { Automation } from '@objectstack/spec';
|
|
1816
|
-
|
|
1817
|
-
/**
|
|
1818
|
-
* ${toTitleCase2(name)} Flow
|
|
1819
|
-
*/
|
|
1820
|
-
const ${toCamelCase3(name)}Flow: Automation.Flow = {
|
|
1821
|
-
name: '${toSnakeCase(name)}_flow',
|
|
1822
|
-
label: '${toTitleCase2(name)} Flow',
|
|
1823
|
-
type: 'autolaunched',
|
|
1824
|
-
status: 'draft',
|
|
1825
|
-
trigger: {
|
|
1826
|
-
type: 'record_change',
|
|
1827
|
-
object: '${toSnakeCase(name)}',
|
|
1828
|
-
events: ['after_insert', 'after_update'],
|
|
1829
|
-
},
|
|
1830
|
-
nodes: [
|
|
1831
|
-
{
|
|
1832
|
-
id: 'start',
|
|
1833
|
-
type: 'start',
|
|
1834
|
-
name: 'Start',
|
|
1835
|
-
next: 'end',
|
|
1836
|
-
},
|
|
1837
|
-
],
|
|
1838
|
-
};
|
|
1839
|
-
|
|
1840
|
-
export default ${toCamelCase3(name)}Flow;
|
|
1841
|
-
`
|
|
1842
|
-
},
|
|
1843
|
-
agent: {
|
|
1844
|
-
description: "AI agent",
|
|
1845
|
-
defaultDir: "src/agents",
|
|
1846
|
-
generate: (name) => `import { AI } from '@objectstack/spec';
|
|
1847
|
-
|
|
1848
|
-
/**
|
|
1849
|
-
* ${toTitleCase2(name)} Agent
|
|
1850
|
-
*/
|
|
1851
|
-
const ${toCamelCase3(name)}Agent: AI.Agent = {
|
|
1852
|
-
name: '${toSnakeCase(name)}_agent',
|
|
1853
|
-
label: '${toTitleCase2(name)} Agent',
|
|
1854
|
-
role: '${toTitleCase2(name)} assistant',
|
|
1855
|
-
instructions: 'You are a helpful ${toTitleCase2(name).toLowerCase()} assistant.',
|
|
1856
|
-
model: {
|
|
1857
|
-
provider: 'openai',
|
|
1858
|
-
model: 'gpt-4o',
|
|
1859
|
-
},
|
|
1860
|
-
tools: [],
|
|
1861
|
-
};
|
|
1862
|
-
|
|
1863
|
-
export default ${toCamelCase3(name)}Agent;
|
|
1864
|
-
`
|
|
1865
|
-
},
|
|
1866
|
-
dashboard: {
|
|
1867
|
-
description: "Analytics dashboard",
|
|
1868
|
-
defaultDir: "src/dashboards",
|
|
1869
|
-
generate: (name) => `import { UI } from '@objectstack/spec';
|
|
1870
|
-
|
|
1871
|
-
/**
|
|
1872
|
-
* ${toTitleCase2(name)} Dashboard
|
|
1873
|
-
*/
|
|
1874
|
-
const ${toCamelCase3(name)}Dashboard: UI.Dashboard = {
|
|
1875
|
-
name: '${toSnakeCase(name)}_dashboard',
|
|
1876
|
-
label: '${toTitleCase2(name)} Dashboard',
|
|
1877
|
-
widgets: [],
|
|
1878
|
-
};
|
|
1879
|
-
|
|
1880
|
-
export default ${toCamelCase3(name)}Dashboard;
|
|
1881
|
-
`
|
|
1882
|
-
},
|
|
1883
|
-
app: {
|
|
1884
|
-
description: "Application navigation",
|
|
1885
|
-
defaultDir: "src/apps",
|
|
1886
|
-
generate: (name) => `import { UI } from '@objectstack/spec';
|
|
1887
|
-
|
|
1888
|
-
/**
|
|
1889
|
-
* ${toTitleCase2(name)} App
|
|
1890
|
-
*/
|
|
1891
|
-
const ${toCamelCase3(name)}App: UI.App = {
|
|
1892
|
-
name: '${toSnakeCase(name)}_app',
|
|
1893
|
-
label: '${toTitleCase2(name)}',
|
|
1894
|
-
navigation: {
|
|
1895
|
-
type: 'sidebar',
|
|
1896
|
-
items: [],
|
|
1897
|
-
},
|
|
1898
|
-
};
|
|
1899
|
-
|
|
1900
|
-
export default ${toCamelCase3(name)}App;
|
|
1901
|
-
`
|
|
1902
|
-
}
|
|
1903
|
-
};
|
|
1904
|
-
function toCamelCase3(str) {
|
|
1905
|
-
return str.replace(/[-_]([a-z])/g, (_, c) => c.toUpperCase());
|
|
1906
|
-
}
|
|
1907
|
-
function toTitleCase2(str) {
|
|
1908
|
-
return str.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1909
|
-
}
|
|
1910
|
-
function toSnakeCase(str) {
|
|
1911
|
-
return str.replace(/[-]/g, "_").replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`).replace(/^_/, "");
|
|
1912
|
-
}
|
|
1913
|
-
var FIELD_TYPE_MAP = {
|
|
1914
|
-
text: "string",
|
|
1915
|
-
textarea: "string",
|
|
1916
|
-
richtext: "string",
|
|
1917
|
-
html: "string",
|
|
1918
|
-
markdown: "string",
|
|
1919
|
-
number: "number",
|
|
1920
|
-
integer: "number",
|
|
1921
|
-
currency: "number",
|
|
1922
|
-
percent: "number",
|
|
1923
|
-
boolean: "boolean",
|
|
1924
|
-
date: "string",
|
|
1925
|
-
datetime: "string",
|
|
1926
|
-
time: "string",
|
|
1927
|
-
email: "string",
|
|
1928
|
-
phone: "string",
|
|
1929
|
-
url: "string",
|
|
1930
|
-
select: "string",
|
|
1931
|
-
multiselect: "string[]",
|
|
1932
|
-
lookup: "string",
|
|
1933
|
-
master_detail: "string",
|
|
1934
|
-
formula: "unknown",
|
|
1935
|
-
autonumber: "string",
|
|
1936
|
-
json: "Record<string, unknown>",
|
|
1937
|
-
file: "string",
|
|
1938
|
-
image: "string",
|
|
1939
|
-
password: "string",
|
|
1940
|
-
slug: "string",
|
|
1941
|
-
uuid: "string",
|
|
1942
|
-
ip_address: "string",
|
|
1943
|
-
color: "string",
|
|
1944
|
-
rating: "number",
|
|
1945
|
-
geo_point: "{ lat: number; lng: number }",
|
|
1946
|
-
vector: "number[]",
|
|
1947
|
-
encrypted: "string"
|
|
1948
|
-
};
|
|
1949
|
-
function fieldTypeToTs(fieldType, multiple) {
|
|
1950
|
-
const base = FIELD_TYPE_MAP[fieldType] || "unknown";
|
|
1951
|
-
return multiple ? `${base}[]` : base;
|
|
1952
|
-
}
|
|
1953
|
-
function generateTypesFromConfig(config) {
|
|
1954
|
-
const lines = [
|
|
1955
|
-
"// Auto-generated by ObjectStack CLI \u2014 do not edit manually",
|
|
1956
|
-
`// Generated at ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
1957
|
-
"",
|
|
1958
|
-
"import type { Data } from '@objectstack/spec';",
|
|
1959
|
-
""
|
|
1960
|
-
];
|
|
1961
|
-
const objects = [];
|
|
1962
|
-
const rawObjects = config.objects ?? config.data?.objects ?? {};
|
|
1963
|
-
if (Array.isArray(rawObjects)) {
|
|
1964
|
-
objects.push(...rawObjects);
|
|
1965
|
-
} else if (typeof rawObjects === "object") {
|
|
1966
|
-
for (const val of Object.values(rawObjects)) {
|
|
1967
|
-
if (val && typeof val === "object") objects.push(val);
|
|
1968
|
-
}
|
|
1969
|
-
}
|
|
1970
|
-
if (objects.length === 0) {
|
|
1971
|
-
lines.push("// No objects found in configuration");
|
|
1972
|
-
return lines.join("\n") + "\n";
|
|
1973
|
-
}
|
|
1974
|
-
for (const obj of objects) {
|
|
1975
|
-
const name = String(obj.name || "unknown");
|
|
1976
|
-
const typeName = name.split("_").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
1977
|
-
const fields = obj.fields ?? {};
|
|
1978
|
-
lines.push(`/** ${String(obj.label || typeName)} record type */`);
|
|
1979
|
-
lines.push(`export interface ${typeName}Record {`);
|
|
1980
|
-
lines.push(" id: string;");
|
|
1981
|
-
for (const [fieldName, fieldDef] of Object.entries(fields)) {
|
|
1982
|
-
const fType = String(fieldDef.type || "text");
|
|
1983
|
-
const tsType = fieldTypeToTs(fType, !!fieldDef.multiple);
|
|
1984
|
-
const required = fieldDef.required ? "" : "?";
|
|
1985
|
-
if (fieldDef.label) {
|
|
1986
|
-
lines.push(` /** ${fieldDef.label} */`);
|
|
1987
|
-
}
|
|
1988
|
-
lines.push(` ${fieldName}${required}: ${tsType};`);
|
|
1989
|
-
}
|
|
1990
|
-
lines.push("}");
|
|
1991
|
-
lines.push("");
|
|
1992
|
-
}
|
|
1993
|
-
return lines.join("\n") + "\n";
|
|
1994
|
-
}
|
|
1995
|
-
var generateMetadataCommand = new Command11("metadata").alias("m").description("Generate metadata scaffold (object, view, action, flow, agent, dashboard, app)").argument("<type>", "Metadata type to generate").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").action(async (type, name, options) => {
|
|
1996
|
-
printHeader("Generate");
|
|
1997
|
-
const generator = GENERATORS[type];
|
|
1998
|
-
if (!generator) {
|
|
1999
|
-
printError(`Unknown type: ${type}`);
|
|
2000
|
-
console.log("");
|
|
2001
|
-
console.log(chalk10.bold(" Available types:"));
|
|
2002
|
-
for (const [key, gen] of Object.entries(GENERATORS)) {
|
|
2003
|
-
console.log(` ${chalk10.cyan(key.padEnd(12))} ${chalk10.dim(gen.description)}`);
|
|
2004
|
-
}
|
|
2005
|
-
console.log("");
|
|
2006
|
-
console.log(chalk10.dim(" Usage: objectstack generate <type> <name>"));
|
|
2007
|
-
console.log(chalk10.dim(" Example: objectstack generate object project"));
|
|
2008
|
-
console.log(chalk10.dim(" Alias: os g object project"));
|
|
2009
|
-
process.exit(1);
|
|
2010
|
-
}
|
|
2011
|
-
const dir = options.dir || generator.defaultDir;
|
|
2012
|
-
const fileName = `${toSnakeCase(name)}.ts`;
|
|
2013
|
-
const filePath = path9.join(process.cwd(), dir, fileName);
|
|
2014
|
-
console.log(` ${chalk10.dim("Type:")} ${chalk10.cyan(type)} \u2014 ${generator.description}`);
|
|
2015
|
-
console.log(` ${chalk10.dim("Name:")} ${chalk10.white(name)}`);
|
|
2016
|
-
console.log(` ${chalk10.dim("File:")} ${chalk10.white(path9.join(dir, fileName))}`);
|
|
2017
|
-
console.log("");
|
|
2018
|
-
if (options.dryRun) {
|
|
2019
|
-
printInfo("Dry run \u2014 no files written");
|
|
2020
|
-
console.log("");
|
|
2021
|
-
console.log(chalk10.dim(" Content:"));
|
|
2022
|
-
console.log(chalk10.dim(" " + "-".repeat(38)));
|
|
2023
|
-
const content = generator.generate(name);
|
|
2024
|
-
for (const line of content.split("\n")) {
|
|
2025
|
-
console.log(chalk10.dim(` ${line}`));
|
|
2026
|
-
}
|
|
2027
|
-
console.log("");
|
|
2028
|
-
return;
|
|
2029
|
-
}
|
|
2030
|
-
if (fs9.existsSync(filePath)) {
|
|
2031
|
-
printError(`File already exists: ${filePath}`);
|
|
2032
|
-
process.exit(1);
|
|
2033
|
-
}
|
|
2034
|
-
try {
|
|
2035
|
-
const fullDir = path9.dirname(filePath);
|
|
2036
|
-
if (!fs9.existsSync(fullDir)) {
|
|
2037
|
-
fs9.mkdirSync(fullDir, { recursive: true });
|
|
2038
|
-
}
|
|
2039
|
-
const content = generator.generate(name);
|
|
2040
|
-
fs9.writeFileSync(filePath, content);
|
|
2041
|
-
printSuccess(`Created ${path9.join(dir, fileName)}`);
|
|
2042
|
-
const indexPath = path9.join(process.cwd(), dir, "index.ts");
|
|
2043
|
-
if (fs9.existsSync(indexPath)) {
|
|
2044
|
-
const indexContent = fs9.readFileSync(indexPath, "utf-8");
|
|
2045
|
-
const exportLine = `export { default as ${toCamelCase3(name)} } from './${toSnakeCase(name)}';`;
|
|
2046
|
-
if (!indexContent.includes(toCamelCase3(name))) {
|
|
2047
|
-
fs9.appendFileSync(indexPath, exportLine + "\n");
|
|
2048
|
-
printSuccess(`Updated ${dir}/index.ts with export`);
|
|
2049
|
-
}
|
|
2050
|
-
} else {
|
|
2051
|
-
const exportLine = `export { default as ${toCamelCase3(name)} } from './${toSnakeCase(name)}';
|
|
2052
|
-
`;
|
|
2053
|
-
fs9.writeFileSync(indexPath, exportLine);
|
|
2054
|
-
printSuccess(`Created ${dir}/index.ts`);
|
|
2055
|
-
}
|
|
2056
|
-
console.log("");
|
|
2057
|
-
console.log(chalk10.dim(` Tip: Run \`objectstack validate\` to check your config`));
|
|
2058
|
-
console.log("");
|
|
2059
|
-
} catch (error) {
|
|
2060
|
-
printError(error.message || String(error));
|
|
2061
|
-
process.exit(1);
|
|
2062
|
-
}
|
|
2063
|
-
});
|
|
2064
|
-
var generateTypesCommand = new Command11("types").description("Generate TypeScript type definitions from ObjectStack configuration").argument("[config]", "Configuration file path").option("-o, --output <file>", "Output file path", "src/types/objectstack.d.ts").option("--dry-run", "Show what would be generated without writing files").action(async (configPath, options) => {
|
|
2065
|
-
printHeader("Generate Types");
|
|
2066
|
-
try {
|
|
2067
|
-
const { loadConfig: loadConfig2 } = await import("./config-GBR54FKL.js");
|
|
2068
|
-
printInfo("Loading configuration...");
|
|
2069
|
-
const { config, absolutePath } = await loadConfig2(configPath);
|
|
2070
|
-
console.log(` ${chalk10.dim("Config:")} ${chalk10.white(absolutePath)}`);
|
|
2071
|
-
console.log(` ${chalk10.dim("Output:")} ${chalk10.white(options.output)}`);
|
|
2072
|
-
console.log("");
|
|
2073
|
-
const content = generateTypesFromConfig(config);
|
|
2074
|
-
if (options.dryRun) {
|
|
2075
|
-
printInfo("Dry run \u2014 no files written");
|
|
2076
|
-
console.log("");
|
|
2077
|
-
for (const line of content.split("\n")) {
|
|
2078
|
-
console.log(chalk10.dim(` ${line}`));
|
|
2079
|
-
}
|
|
2080
|
-
console.log("");
|
|
2081
|
-
return;
|
|
2082
|
-
}
|
|
2083
|
-
const outPath = path9.resolve(process.cwd(), options.output);
|
|
2084
|
-
const outDir = path9.dirname(outPath);
|
|
2085
|
-
if (!fs9.existsSync(outDir)) {
|
|
2086
|
-
fs9.mkdirSync(outDir, { recursive: true });
|
|
2087
|
-
}
|
|
2088
|
-
fs9.writeFileSync(outPath, content);
|
|
2089
|
-
printSuccess(`Generated types at ${options.output}`);
|
|
2090
|
-
console.log("");
|
|
2091
|
-
} catch (error) {
|
|
2092
|
-
printError(error.message || String(error));
|
|
2093
|
-
process.exit(1);
|
|
2094
|
-
}
|
|
2095
|
-
});
|
|
2096
|
-
function generateClientFromConfig(config) {
|
|
2097
|
-
const lines = [
|
|
2098
|
-
"// Auto-generated by ObjectStack CLI \u2014 do not edit manually",
|
|
2099
|
-
`// Generated at ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
2100
|
-
"",
|
|
2101
|
-
"import type { Data } from '@objectstack/spec';",
|
|
2102
|
-
""
|
|
2103
|
-
];
|
|
2104
|
-
const objects = [];
|
|
2105
|
-
const rawObjects = config.objects ?? config.data?.objects ?? {};
|
|
2106
|
-
if (Array.isArray(rawObjects)) {
|
|
2107
|
-
objects.push(...rawObjects);
|
|
2108
|
-
} else if (typeof rawObjects === "object") {
|
|
2109
|
-
for (const val of Object.values(rawObjects)) {
|
|
2110
|
-
if (val && typeof val === "object") objects.push(val);
|
|
2111
|
-
}
|
|
2112
|
-
}
|
|
2113
|
-
if (objects.length === 0) {
|
|
2114
|
-
lines.push("// No objects found in configuration");
|
|
2115
|
-
return lines.join("\n") + "\n";
|
|
2116
|
-
}
|
|
2117
|
-
for (const obj of objects) {
|
|
2118
|
-
const name = String(obj.name || "unknown");
|
|
2119
|
-
const typeName = name.split("_").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
2120
|
-
const fields = obj.fields ?? {};
|
|
2121
|
-
lines.push(`export interface ${typeName}Record {`);
|
|
2122
|
-
lines.push(" id: string;");
|
|
2123
|
-
for (const [fieldName, fieldDef] of Object.entries(fields)) {
|
|
2124
|
-
const fType = String(fieldDef.type || "text");
|
|
2125
|
-
const tsType = fieldTypeToTs(fType, !!fieldDef.multiple);
|
|
2126
|
-
const required = fieldDef.required ? "" : "?";
|
|
2127
|
-
lines.push(` ${fieldName}${required}: ${tsType};`);
|
|
2128
|
-
}
|
|
2129
|
-
lines.push("}");
|
|
2130
|
-
lines.push("");
|
|
2131
|
-
}
|
|
2132
|
-
lines.push("export class ObjectStackClient {");
|
|
2133
|
-
lines.push(" constructor(private baseUrl: string, private headers: Record<string, string> = {}) {}");
|
|
2134
|
-
lines.push("");
|
|
2135
|
-
lines.push(" private async request<T>(method: string, path: string, body?: unknown): Promise<T> {");
|
|
2136
|
-
lines.push(" const res = await fetch(`${this.baseUrl}${path}`, {");
|
|
2137
|
-
lines.push(" method,");
|
|
2138
|
-
lines.push(" headers: { 'Content-Type': 'application/json', ...this.headers },");
|
|
2139
|
-
lines.push(" body: body ? JSON.stringify(body) : undefined,");
|
|
2140
|
-
lines.push(" });");
|
|
2141
|
-
lines.push(" if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);");
|
|
2142
|
-
lines.push(" return res.json() as Promise<T>;");
|
|
2143
|
-
lines.push(" }");
|
|
2144
|
-
for (const obj of objects) {
|
|
2145
|
-
const name = String(obj.name || "unknown");
|
|
2146
|
-
const typeName = name.split("_").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
2147
|
-
const endpoint = `/api/${name}`;
|
|
2148
|
-
lines.push("");
|
|
2149
|
-
lines.push(` async list${typeName}(): Promise<${typeName}Record[]> {`);
|
|
2150
|
-
lines.push(` return this.request<${typeName}Record[]>('GET', '${endpoint}');`);
|
|
2151
|
-
lines.push(" }");
|
|
2152
|
-
lines.push("");
|
|
2153
|
-
lines.push(` async get${typeName}(id: string): Promise<${typeName}Record> {`);
|
|
2154
|
-
lines.push(` return this.request<${typeName}Record>('GET', '${endpoint}/\${id}');`);
|
|
2155
|
-
lines.push(" }");
|
|
2156
|
-
lines.push("");
|
|
2157
|
-
lines.push(` async create${typeName}(data: Omit<${typeName}Record, 'id'>): Promise<${typeName}Record> {`);
|
|
2158
|
-
lines.push(` return this.request<${typeName}Record>('POST', '${endpoint}', data);`);
|
|
2159
|
-
lines.push(" }");
|
|
2160
|
-
lines.push("");
|
|
2161
|
-
lines.push(` async update${typeName}(id: string, data: Partial<${typeName}Record>): Promise<${typeName}Record> {`);
|
|
2162
|
-
lines.push(` return this.request<${typeName}Record>('PATCH', '${endpoint}/\${id}', data);`);
|
|
2163
|
-
lines.push(" }");
|
|
2164
|
-
lines.push("");
|
|
2165
|
-
lines.push(` async delete${typeName}(id: string): Promise<void> {`);
|
|
2166
|
-
lines.push(` return this.request<void>('DELETE', '${endpoint}/\${id}');`);
|
|
2167
|
-
lines.push(" }");
|
|
2168
|
-
}
|
|
2169
|
-
lines.push("}");
|
|
2170
|
-
lines.push("");
|
|
2171
|
-
return lines.join("\n") + "\n";
|
|
2172
|
-
}
|
|
2173
|
-
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) => {
|
|
2174
|
-
printHeader("Generate Client SDK");
|
|
2175
|
-
try {
|
|
2176
|
-
const { loadConfig: loadConfig2 } = await import("./config-GBR54FKL.js");
|
|
2177
|
-
const timer = createTimer();
|
|
2178
|
-
printInfo("Loading configuration...");
|
|
2179
|
-
const { config, absolutePath } = await loadConfig2(configPath);
|
|
2180
|
-
console.log(` ${chalk10.dim("Config:")} ${chalk10.white(absolutePath)}`);
|
|
2181
|
-
console.log(` ${chalk10.dim("Output:")} ${chalk10.white(options.output)}`);
|
|
2182
|
-
console.log("");
|
|
2183
|
-
printStep("Generating client SDK...");
|
|
2184
|
-
const content = generateClientFromConfig(config);
|
|
2185
|
-
if (options.dryRun) {
|
|
2186
|
-
printInfo("Dry run \u2014 no files written");
|
|
2187
|
-
console.log("");
|
|
2188
|
-
for (const line of content.split("\n")) {
|
|
2189
|
-
console.log(chalk10.dim(` ${line}`));
|
|
2190
|
-
}
|
|
2191
|
-
console.log("");
|
|
2192
|
-
return;
|
|
2193
|
-
}
|
|
2194
|
-
const outPath = path9.resolve(process.cwd(), options.output);
|
|
2195
|
-
const outDir = path9.dirname(outPath);
|
|
2196
|
-
if (!fs9.existsSync(outDir)) {
|
|
2197
|
-
fs9.mkdirSync(outDir, { recursive: true });
|
|
2198
|
-
}
|
|
2199
|
-
fs9.writeFileSync(outPath, content);
|
|
2200
|
-
printSuccess(`Generated client SDK at ${options.output} (${timer.display()})`);
|
|
2201
|
-
console.log("");
|
|
2202
|
-
} catch (error) {
|
|
2203
|
-
printError(error.message || String(error));
|
|
2204
|
-
process.exit(1);
|
|
2205
|
-
}
|
|
2206
|
-
});
|
|
2207
|
-
var FIELD_TYPE_SQL_MAP = {
|
|
2208
|
-
text: "VARCHAR(255)",
|
|
2209
|
-
textarea: "TEXT",
|
|
2210
|
-
richtext: "TEXT",
|
|
2211
|
-
html: "TEXT",
|
|
2212
|
-
markdown: "TEXT",
|
|
2213
|
-
number: "DECIMAL(18,2)",
|
|
2214
|
-
integer: "INTEGER",
|
|
2215
|
-
currency: "DECIMAL(18,2)",
|
|
2216
|
-
percent: "DECIMAL(5,2)",
|
|
2217
|
-
boolean: "BOOLEAN",
|
|
2218
|
-
date: "DATE",
|
|
2219
|
-
datetime: "TIMESTAMP",
|
|
2220
|
-
time: "TIME",
|
|
2221
|
-
email: "VARCHAR(255)",
|
|
2222
|
-
phone: "VARCHAR(50)",
|
|
2223
|
-
url: "VARCHAR(2048)",
|
|
2224
|
-
select: "VARCHAR(255)",
|
|
2225
|
-
multiselect: "TEXT",
|
|
2226
|
-
lookup: "VARCHAR(36)",
|
|
2227
|
-
master_detail: "VARCHAR(36)",
|
|
2228
|
-
formula: "TEXT",
|
|
2229
|
-
autonumber: "SERIAL",
|
|
2230
|
-
json: "JSONB",
|
|
2231
|
-
file: "VARCHAR(2048)",
|
|
2232
|
-
image: "VARCHAR(2048)",
|
|
2233
|
-
password: "VARCHAR(255)",
|
|
2234
|
-
slug: "VARCHAR(255)",
|
|
2235
|
-
uuid: "UUID",
|
|
2236
|
-
ip_address: "VARCHAR(45)",
|
|
2237
|
-
color: "VARCHAR(7)",
|
|
2238
|
-
rating: "INTEGER",
|
|
2239
|
-
geo_point: "POINT",
|
|
2240
|
-
vector: "VECTOR",
|
|
2241
|
-
encrypted: "TEXT"
|
|
2242
|
-
};
|
|
2243
|
-
function fieldTypeToSql(fieldType) {
|
|
2244
|
-
return FIELD_TYPE_SQL_MAP[fieldType] || "TEXT";
|
|
2245
|
-
}
|
|
2246
|
-
function generateMigrationSql(config) {
|
|
2247
|
-
const lines = [
|
|
2248
|
-
"-- Auto-generated by ObjectStack CLI \u2014 do not edit manually",
|
|
2249
|
-
`-- Generated at ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
2250
|
-
""
|
|
2251
|
-
];
|
|
2252
|
-
const objects = [];
|
|
2253
|
-
const rawObjects = config.objects ?? config.data?.objects ?? {};
|
|
2254
|
-
if (Array.isArray(rawObjects)) {
|
|
2255
|
-
objects.push(...rawObjects);
|
|
2256
|
-
} else if (typeof rawObjects === "object") {
|
|
2257
|
-
for (const val of Object.values(rawObjects)) {
|
|
2258
|
-
if (val && typeof val === "object") objects.push(val);
|
|
2259
|
-
}
|
|
2260
|
-
}
|
|
2261
|
-
if (objects.length === 0) {
|
|
2262
|
-
lines.push("-- No objects found in configuration");
|
|
2263
|
-
return lines.join("\n") + "\n";
|
|
2264
|
-
}
|
|
2265
|
-
for (const obj of objects) {
|
|
2266
|
-
const tableName = String(obj.name || "unknown");
|
|
2267
|
-
const fields = obj.fields ?? {};
|
|
2268
|
-
lines.push(`CREATE TABLE IF NOT EXISTS "${tableName}" (`);
|
|
2269
|
-
lines.push(' "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),');
|
|
2270
|
-
const fieldLines = [];
|
|
2271
|
-
for (const [fieldName, fieldDef] of Object.entries(fields)) {
|
|
2272
|
-
const sqlType = fieldTypeToSql(String(fieldDef.type || "text"));
|
|
2273
|
-
const notNull = fieldDef.required ? " NOT NULL" : "";
|
|
2274
|
-
fieldLines.push(` "${fieldName}" ${sqlType}${notNull}`);
|
|
2275
|
-
}
|
|
2276
|
-
fieldLines.push(' "created_at" TIMESTAMP NOT NULL DEFAULT now()');
|
|
2277
|
-
fieldLines.push(' "updated_at" TIMESTAMP NOT NULL DEFAULT now()');
|
|
2278
|
-
lines.push(fieldLines.join(",\n"));
|
|
2279
|
-
lines.push(");");
|
|
2280
|
-
lines.push("");
|
|
2281
|
-
}
|
|
2282
|
-
return lines.join("\n") + "\n";
|
|
2283
|
-
}
|
|
2284
|
-
function generateMigrationTs(config) {
|
|
2285
|
-
const lines = [
|
|
2286
|
-
"// Auto-generated by ObjectStack CLI \u2014 do not edit manually",
|
|
2287
|
-
`// Generated at ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
2288
|
-
"",
|
|
2289
|
-
"export async function up(db: any): Promise<void> {"
|
|
2290
|
-
];
|
|
2291
|
-
const objects = [];
|
|
2292
|
-
const rawObjects = config.objects ?? config.data?.objects ?? {};
|
|
2293
|
-
if (Array.isArray(rawObjects)) {
|
|
2294
|
-
objects.push(...rawObjects);
|
|
2295
|
-
} else if (typeof rawObjects === "object") {
|
|
2296
|
-
for (const val of Object.values(rawObjects)) {
|
|
2297
|
-
if (val && typeof val === "object") objects.push(val);
|
|
2298
|
-
}
|
|
2299
|
-
}
|
|
2300
|
-
if (objects.length === 0) {
|
|
2301
|
-
lines.push(" // No objects found in configuration");
|
|
2302
|
-
lines.push("}");
|
|
2303
|
-
lines.push("");
|
|
2304
|
-
lines.push("export async function down(db: any): Promise<void> {");
|
|
2305
|
-
lines.push(" // No objects found in configuration");
|
|
2306
|
-
lines.push("}");
|
|
2307
|
-
return lines.join("\n") + "\n";
|
|
2308
|
-
}
|
|
2309
|
-
for (const obj of objects) {
|
|
2310
|
-
const tableName = String(obj.name || "unknown");
|
|
2311
|
-
const fields = obj.fields ?? {};
|
|
2312
|
-
lines.push(` await db.schema.createTable('${tableName}', (table: any) => {`);
|
|
2313
|
-
lines.push(" table.uuid('id').primary().defaultTo(db.fn.uuid());");
|
|
2314
|
-
for (const [fieldName, fieldDef] of Object.entries(fields)) {
|
|
2315
|
-
const fType = String(fieldDef.type || "text");
|
|
2316
|
-
const required = fieldDef.required ? ".notNullable()" : ".nullable()";
|
|
2317
|
-
let colMethod;
|
|
2318
|
-
switch (fType) {
|
|
2319
|
-
case "text":
|
|
2320
|
-
case "email":
|
|
2321
|
-
case "phone":
|
|
2322
|
-
case "url":
|
|
2323
|
-
case "select":
|
|
2324
|
-
case "slug":
|
|
2325
|
-
case "password":
|
|
2326
|
-
case "color":
|
|
2327
|
-
case "ip_address":
|
|
2328
|
-
colMethod = `table.string('${fieldName}')`;
|
|
2329
|
-
break;
|
|
2330
|
-
case "textarea":
|
|
2331
|
-
case "richtext":
|
|
2332
|
-
case "html":
|
|
2333
|
-
case "markdown":
|
|
2334
|
-
case "formula":
|
|
2335
|
-
case "encrypted":
|
|
2336
|
-
colMethod = `table.text('${fieldName}')`;
|
|
2337
|
-
break;
|
|
2338
|
-
case "number":
|
|
2339
|
-
case "currency":
|
|
2340
|
-
case "percent":
|
|
2341
|
-
colMethod = `table.decimal('${fieldName}')`;
|
|
2342
|
-
break;
|
|
2343
|
-
case "integer":
|
|
2344
|
-
case "rating":
|
|
2345
|
-
colMethod = `table.integer('${fieldName}')`;
|
|
2346
|
-
break;
|
|
2347
|
-
case "boolean":
|
|
2348
|
-
colMethod = `table.boolean('${fieldName}')`;
|
|
2349
|
-
break;
|
|
2350
|
-
case "date":
|
|
2351
|
-
colMethod = `table.date('${fieldName}')`;
|
|
2352
|
-
break;
|
|
2353
|
-
case "datetime":
|
|
2354
|
-
colMethod = `table.timestamp('${fieldName}')`;
|
|
2355
|
-
break;
|
|
2356
|
-
case "time":
|
|
2357
|
-
colMethod = `table.time('${fieldName}')`;
|
|
2358
|
-
break;
|
|
2359
|
-
case "json":
|
|
2360
|
-
case "multiselect":
|
|
2361
|
-
colMethod = `table.jsonb('${fieldName}')`;
|
|
2362
|
-
break;
|
|
2363
|
-
case "uuid":
|
|
2364
|
-
case "lookup":
|
|
2365
|
-
case "master_detail":
|
|
2366
|
-
colMethod = `table.uuid('${fieldName}')`;
|
|
2367
|
-
break;
|
|
2368
|
-
default:
|
|
2369
|
-
colMethod = `table.text('${fieldName}')`;
|
|
2370
|
-
}
|
|
2371
|
-
lines.push(` ${colMethod}${required};`);
|
|
2372
|
-
}
|
|
2373
|
-
lines.push(" table.timestamps(true, true);");
|
|
2374
|
-
lines.push(" });");
|
|
2375
|
-
}
|
|
2376
|
-
lines.push("}");
|
|
2377
|
-
lines.push("");
|
|
2378
|
-
lines.push("export async function down(db: any): Promise<void> {");
|
|
2379
|
-
const tableNames = objects.map((o) => String(o.name || "unknown")).reverse();
|
|
2380
|
-
for (const tableName of tableNames) {
|
|
2381
|
-
lines.push(` await db.schema.dropTableIfExists('${tableName}');`);
|
|
2382
|
-
}
|
|
2383
|
-
lines.push("}");
|
|
2384
|
-
return lines.join("\n") + "\n";
|
|
2385
|
-
}
|
|
2386
|
-
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) => {
|
|
2387
|
-
printHeader("Generate Migration");
|
|
2388
|
-
try {
|
|
2389
|
-
const { loadConfig: loadConfig2 } = await import("./config-GBR54FKL.js");
|
|
2390
|
-
const timer = createTimer();
|
|
2391
|
-
printInfo("Loading configuration...");
|
|
2392
|
-
const { config, absolutePath } = await loadConfig2(configPath);
|
|
2393
|
-
const ext = options.format === "sql" ? "sql" : "ts";
|
|
2394
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T]/g, "").slice(0, 14);
|
|
2395
|
-
const defaultOutput = `migrations/${timestamp}_migration.${ext}`;
|
|
2396
|
-
const output = options.output || defaultOutput;
|
|
2397
|
-
console.log(` ${chalk10.dim("Config:")} ${chalk10.white(absolutePath)}`);
|
|
2398
|
-
console.log(` ${chalk10.dim("Format:")} ${chalk10.white(options.format)}`);
|
|
2399
|
-
console.log(` ${chalk10.dim("Output:")} ${chalk10.white(output)}`);
|
|
2400
|
-
console.log("");
|
|
2401
|
-
printStep("Generating migration...");
|
|
2402
|
-
const content = options.format === "sql" ? generateMigrationSql(config) : generateMigrationTs(config);
|
|
2403
|
-
if (options.dryRun) {
|
|
2404
|
-
printInfo("Dry run \u2014 no files written");
|
|
2405
|
-
console.log("");
|
|
2406
|
-
for (const line of content.split("\n")) {
|
|
2407
|
-
console.log(chalk10.dim(` ${line}`));
|
|
2408
|
-
}
|
|
2409
|
-
console.log("");
|
|
2410
|
-
return;
|
|
2411
|
-
}
|
|
2412
|
-
const outPath = path9.resolve(process.cwd(), output);
|
|
2413
|
-
const outDir = path9.dirname(outPath);
|
|
2414
|
-
if (!fs9.existsSync(outDir)) {
|
|
2415
|
-
fs9.mkdirSync(outDir, { recursive: true });
|
|
2416
|
-
}
|
|
2417
|
-
fs9.writeFileSync(outPath, content);
|
|
2418
|
-
printSuccess(`Generated migration at ${output} (${timer.display()})`);
|
|
2419
|
-
console.log("");
|
|
2420
|
-
} catch (error) {
|
|
2421
|
-
printError(error.message || String(error));
|
|
2422
|
-
process.exit(1);
|
|
2423
|
-
}
|
|
2424
|
-
});
|
|
2425
|
-
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) => {
|
|
2426
|
-
printHeader("Generate Schema");
|
|
2427
|
-
try {
|
|
2428
|
-
const timer = createTimer();
|
|
2429
|
-
printStep("Loading ObjectStackDefinitionSchema...");
|
|
2430
|
-
const { z } = await import("zod");
|
|
2431
|
-
const { ObjectStackDefinitionSchema: ObjectStackDefinitionSchema3 } = await import("@objectstack/spec");
|
|
2432
|
-
printStep("Converting to JSON Schema...");
|
|
2433
|
-
const jsonSchema = z.toJSONSchema(ObjectStackDefinitionSchema3, {
|
|
2434
|
-
target: "draft-2020-12"
|
|
2435
|
-
});
|
|
2436
|
-
const schema = {
|
|
2437
|
-
...jsonSchema,
|
|
2438
|
-
$id: "https://schema.objectstack.io/objectstack.config.json",
|
|
2439
|
-
title: "ObjectStack Configuration",
|
|
2440
|
-
description: "JSON Schema for objectstack.config.ts \u2014 generated from ObjectStackDefinitionSchema"
|
|
2441
|
-
};
|
|
2442
|
-
const content = JSON.stringify(schema, null, 2) + "\n";
|
|
2443
|
-
if (options.dryRun) {
|
|
2444
|
-
printInfo("Dry run \u2014 no files written");
|
|
2445
|
-
console.log("");
|
|
2446
|
-
console.log(content);
|
|
2447
|
-
return;
|
|
2448
|
-
}
|
|
2449
|
-
const outPath = path9.resolve(process.cwd(), options.output);
|
|
2450
|
-
const outDir = path9.dirname(outPath);
|
|
2451
|
-
if (!fs9.existsSync(outDir)) {
|
|
2452
|
-
fs9.mkdirSync(outDir, { recursive: true });
|
|
2453
|
-
}
|
|
2454
|
-
fs9.writeFileSync(outPath, content);
|
|
2455
|
-
printSuccess(`Generated JSON Schema at ${options.output} (${timer.display()})`);
|
|
2456
|
-
console.log("");
|
|
2457
|
-
console.log(chalk10.dim(" Usage: Reference in your IDE or editor for autocomplete"));
|
|
2458
|
-
console.log(chalk10.dim(` Path: ${outPath}`));
|
|
2459
|
-
console.log("");
|
|
2460
|
-
} catch (error) {
|
|
2461
|
-
printError(error.message || String(error));
|
|
2462
|
-
process.exit(1);
|
|
2463
|
-
}
|
|
2464
|
-
});
|
|
2465
|
-
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) => {
|
|
2466
|
-
if (!type) {
|
|
2467
|
-
printHeader("Generate");
|
|
2468
|
-
console.log(chalk10.bold(" Sub-commands:"));
|
|
2469
|
-
console.log(` ${chalk10.cyan("types".padEnd(12))} Generate TypeScript type definitions from config`);
|
|
2470
|
-
console.log(` ${chalk10.cyan("client".padEnd(12))} Generate a type-safe client SDK from config`);
|
|
2471
|
-
console.log(` ${chalk10.cyan("migration".padEnd(12))} Generate database migration from schema`);
|
|
2472
|
-
console.log(` ${chalk10.cyan("schema".padEnd(12))} Generate JSON Schema for objectstack.config.ts (IDE autocomplete)`);
|
|
2473
|
-
console.log("");
|
|
2474
|
-
console.log(chalk10.bold(" Metadata types:"));
|
|
2475
|
-
for (const [key, gen] of Object.entries(GENERATORS)) {
|
|
2476
|
-
console.log(` ${chalk10.cyan(key.padEnd(12))} ${chalk10.dim(gen.description)}`);
|
|
2477
|
-
}
|
|
2478
|
-
console.log("");
|
|
2479
|
-
console.log(chalk10.dim(" Usage: objectstack generate <type> <name>"));
|
|
2480
|
-
console.log(chalk10.dim(" Usage: objectstack generate types [config]"));
|
|
2481
|
-
return;
|
|
2482
|
-
}
|
|
2483
|
-
if (!name) {
|
|
2484
|
-
printError("Missing required argument: <name>");
|
|
2485
|
-
console.log(chalk10.dim(" Usage: objectstack generate <type> <name>"));
|
|
2486
|
-
process.exit(1);
|
|
2487
|
-
}
|
|
2488
|
-
await generateMetadataCommand.parseAsync([type, name, ...process.argv.slice(4)], { from: "user" });
|
|
2489
|
-
});
|
|
2490
|
-
|
|
2491
|
-
// src/commands/plugin.ts
|
|
2492
|
-
import { Command as Command12 } from "commander";
|
|
2493
|
-
import chalk11 from "chalk";
|
|
2494
|
-
import fs10 from "fs";
|
|
2495
|
-
import path10 from "path";
|
|
2496
|
-
function resolvePluginName(plugin) {
|
|
2497
|
-
if (typeof plugin === "string") return plugin;
|
|
2498
|
-
if (plugin && typeof plugin === "object") {
|
|
2499
|
-
const p = plugin;
|
|
2500
|
-
if (typeof p.name === "string") return p.name;
|
|
2501
|
-
if (p.constructor && p.constructor.name !== "Object") return p.constructor.name;
|
|
2502
|
-
}
|
|
2503
|
-
return "unknown";
|
|
2504
|
-
}
|
|
2505
|
-
function resolvePluginVersion(plugin) {
|
|
2506
|
-
if (plugin && typeof plugin === "object") {
|
|
2507
|
-
const p = plugin;
|
|
2508
|
-
if (typeof p.version === "string") return p.version;
|
|
2509
|
-
}
|
|
2510
|
-
return "-";
|
|
2511
|
-
}
|
|
2512
|
-
function resolvePluginType(plugin) {
|
|
2513
|
-
if (plugin && typeof plugin === "object") {
|
|
2514
|
-
const p = plugin;
|
|
2515
|
-
if (typeof p.type === "string") return p.type;
|
|
2516
|
-
}
|
|
2517
|
-
return "standard";
|
|
2518
|
-
}
|
|
2519
|
-
function readConfigText(configPath) {
|
|
2520
|
-
return fs10.readFileSync(configPath, "utf-8");
|
|
2521
|
-
}
|
|
2522
|
-
function addPluginToConfig(configPath, packageName) {
|
|
2523
|
-
let content = readConfigText(configPath);
|
|
2524
|
-
const shortName = packageName.replace(/^@[^/]+\//, "").replace(/^plugin-/, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
2525
|
-
const varName = shortName.replace(/-([a-z])/g, (_, c) => c.toUpperCase()) + "Plugin";
|
|
2526
|
-
const importLine = `import ${varName} from '${packageName}';
|
|
2527
|
-
`;
|
|
2528
|
-
if (content.includes(packageName)) {
|
|
2529
|
-
throw new Error(`Plugin '${packageName}' is already referenced in the config`);
|
|
2530
|
-
}
|
|
2531
|
-
const importRegex = /^import .+$/gm;
|
|
2532
|
-
let lastImportEnd = 0;
|
|
2533
|
-
let match;
|
|
2534
|
-
while ((match = importRegex.exec(content)) !== null) {
|
|
2535
|
-
lastImportEnd = match.index + match[0].length;
|
|
2536
|
-
}
|
|
2537
|
-
if (lastImportEnd > 0) {
|
|
2538
|
-
content = content.slice(0, lastImportEnd) + "\n" + importLine + content.slice(lastImportEnd);
|
|
2539
|
-
} else {
|
|
2540
|
-
content = importLine + "\n" + content;
|
|
2541
|
-
}
|
|
2542
|
-
if (/plugins\s*:\s*\[/.test(content)) {
|
|
2543
|
-
let replaced = false;
|
|
2544
|
-
content = content.replace(
|
|
2545
|
-
/(plugins\s*:\s*\[)/,
|
|
2546
|
-
(match2) => {
|
|
2547
|
-
if (replaced) return match2;
|
|
2548
|
-
replaced = true;
|
|
2549
|
-
return `${match2}
|
|
2550
|
-
${varName},`;
|
|
2551
|
-
}
|
|
2552
|
-
);
|
|
2553
|
-
} else {
|
|
2554
|
-
content = content.replace(
|
|
2555
|
-
/(defineStack\(\{[\s\S]*?)(}\s*\))/,
|
|
2556
|
-
`$1 plugins: [
|
|
2557
|
-
${varName},
|
|
2558
|
-
],
|
|
2559
|
-
$2`
|
|
2560
|
-
);
|
|
2561
|
-
}
|
|
2562
|
-
fs10.writeFileSync(configPath, content);
|
|
2563
|
-
}
|
|
2564
|
-
function removePluginFromConfig(configPath, pluginName) {
|
|
2565
|
-
let content = readConfigText(configPath);
|
|
2566
|
-
const importRegex = new RegExp(`^import .+['"]${escapeRegex(pluginName)}['"]\\s*;?\\s*$\\n?`, "gm");
|
|
2567
|
-
const hadImport = importRegex.test(content);
|
|
2568
|
-
importRegex.lastIndex = 0;
|
|
2569
|
-
content = content.replace(importRegex, "");
|
|
2570
|
-
const shortName = pluginName.replace(/^@[^/]+\//, "").replace(/^plugin-/, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
2571
|
-
const varName = shortName.replace(/-([a-z])/g, (_, c) => c.toUpperCase()) + "Plugin";
|
|
2572
|
-
if (!hadImport) {
|
|
2573
|
-
const varImportRegex = new RegExp(`^import .* ${escapeRegex(varName)} .+$\\n?`, "gm");
|
|
2574
|
-
content = content.replace(varImportRegex, "");
|
|
2575
|
-
}
|
|
2576
|
-
const entryPatterns = [
|
|
2577
|
-
new RegExp(`\\s*${escapeRegex(varName)},?\\n?`, "g"),
|
|
2578
|
-
new RegExp(`\\s*['"]${escapeRegex(pluginName)}['"],?\\n?`, "g")
|
|
2579
|
-
];
|
|
2580
|
-
for (const pattern of entryPatterns) {
|
|
2581
|
-
content = content.replace(pattern, "\n");
|
|
2582
|
-
}
|
|
2583
|
-
content = content.replace(/plugins\s*:\s*\[\s*\],?\n?/g, "");
|
|
2584
|
-
fs10.writeFileSync(configPath, content);
|
|
2585
|
-
}
|
|
2586
|
-
function escapeRegex(str) {
|
|
2587
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2588
|
-
}
|
|
2589
|
-
var listCommand = new Command12("list").alias("ls").description("List plugins defined in the configuration").argument("[config]", "Configuration file path").option("--json", "Output as JSON").action(async (configSource, options) => {
|
|
2590
|
-
try {
|
|
2591
|
-
const { config } = await loadConfig(configSource);
|
|
2592
|
-
const plugins = config.plugins || [];
|
|
2593
|
-
const devPlugins = config.devPlugins || [];
|
|
2594
|
-
if (options?.json) {
|
|
2595
|
-
const data = {
|
|
2596
|
-
plugins: plugins.map((p) => ({
|
|
2597
|
-
name: resolvePluginName(p),
|
|
2598
|
-
version: resolvePluginVersion(p),
|
|
2599
|
-
type: resolvePluginType(p),
|
|
2600
|
-
dev: false
|
|
2601
|
-
})),
|
|
2602
|
-
devPlugins: devPlugins.map((p) => ({
|
|
2603
|
-
name: resolvePluginName(p),
|
|
2604
|
-
version: resolvePluginVersion(p),
|
|
2605
|
-
type: resolvePluginType(p),
|
|
2606
|
-
dev: true
|
|
2607
|
-
}))
|
|
2608
|
-
};
|
|
2609
|
-
console.log(JSON.stringify(data, null, 2));
|
|
2610
|
-
return;
|
|
2611
|
-
}
|
|
2612
|
-
printHeader("Plugins");
|
|
2613
|
-
if (plugins.length === 0 && devPlugins.length === 0) {
|
|
2614
|
-
printInfo("No plugins configured");
|
|
2615
|
-
console.log("");
|
|
2616
|
-
console.log(chalk11.dim(" Hint: Add plugins to your objectstack.config.ts"));
|
|
2617
|
-
console.log(chalk11.dim(" Or run: os plugin add <package-name>"));
|
|
2618
|
-
console.log("");
|
|
2619
|
-
return;
|
|
2620
|
-
}
|
|
2621
|
-
if (plugins.length > 0) {
|
|
2622
|
-
console.log(chalk11.bold(`
|
|
2623
|
-
Plugins (${plugins.length}):`));
|
|
2624
|
-
for (const plugin of plugins) {
|
|
2625
|
-
const name = resolvePluginName(plugin);
|
|
2626
|
-
const version = resolvePluginVersion(plugin);
|
|
2627
|
-
const type = resolvePluginType(plugin);
|
|
2628
|
-
console.log(
|
|
2629
|
-
` ${chalk11.cyan("\u25CF")} ${chalk11.white(name)}` + (version !== "-" ? chalk11.dim(` v${version}`) : "") + (type !== "standard" ? chalk11.dim(` [${type}]`) : "")
|
|
2630
|
-
);
|
|
2631
|
-
}
|
|
2632
|
-
}
|
|
2633
|
-
if (devPlugins.length > 0) {
|
|
2634
|
-
console.log(chalk11.bold(`
|
|
2635
|
-
Dev Plugins (${devPlugins.length}):`));
|
|
2636
|
-
for (const plugin of devPlugins) {
|
|
2637
|
-
const name = resolvePluginName(plugin);
|
|
2638
|
-
const version = resolvePluginVersion(plugin);
|
|
2639
|
-
console.log(
|
|
2640
|
-
` ${chalk11.yellow("\u25CF")} ${chalk11.white(name)}` + (version !== "-" ? chalk11.dim(` v${version}`) : "") + chalk11.dim(" [dev]")
|
|
2641
|
-
);
|
|
2642
|
-
}
|
|
2643
|
-
}
|
|
2644
|
-
console.log("");
|
|
2645
|
-
} catch (error) {
|
|
2646
|
-
printError(error.message || String(error));
|
|
2647
|
-
process.exit(1);
|
|
2648
|
-
}
|
|
2649
|
-
});
|
|
2650
|
-
var infoSubCommand = new Command12("info").description("Show detailed information about a plugin").argument("<name>", "Plugin name or package name").argument("[config]", "Configuration file path").action(async (name, configSource) => {
|
|
2651
|
-
try {
|
|
2652
|
-
const { config } = await loadConfig(configSource);
|
|
2653
|
-
const allPlugins = [
|
|
2654
|
-
...config.plugins || [],
|
|
2655
|
-
...config.devPlugins || []
|
|
2656
|
-
];
|
|
2657
|
-
const found = allPlugins.find((p) => {
|
|
2658
|
-
const pName = resolvePluginName(p);
|
|
2659
|
-
return pName === name || pName.includes(name);
|
|
2660
|
-
});
|
|
2661
|
-
if (!found) {
|
|
2662
|
-
printError(`Plugin '${name}' not found in configuration`);
|
|
2663
|
-
console.log("");
|
|
2664
|
-
console.log(chalk11.dim(" Available plugins:"));
|
|
2665
|
-
for (const p of allPlugins) {
|
|
2666
|
-
console.log(chalk11.dim(` - ${resolvePluginName(p)}`));
|
|
2667
|
-
}
|
|
2668
|
-
console.log("");
|
|
2669
|
-
process.exit(1);
|
|
2670
|
-
}
|
|
2671
|
-
printHeader(`Plugin: ${resolvePluginName(found)}`);
|
|
2672
|
-
printKV("Name", resolvePluginName(found));
|
|
2673
|
-
printKV("Version", resolvePluginVersion(found));
|
|
2674
|
-
printKV("Type", resolvePluginType(found));
|
|
2675
|
-
const isDev = (config.devPlugins || []).includes(found);
|
|
2676
|
-
printKV("Environment", isDev ? "development" : "production");
|
|
2677
|
-
if (found && typeof found === "object") {
|
|
2678
|
-
const p = found;
|
|
2679
|
-
if (typeof p.description === "string") {
|
|
2680
|
-
printKV("Description", p.description);
|
|
2681
|
-
}
|
|
2682
|
-
if (Array.isArray(p.dependencies) && p.dependencies.length > 0) {
|
|
2683
|
-
printKV("Dependencies", p.dependencies.join(", "));
|
|
2684
|
-
}
|
|
2685
|
-
if (typeof p.init === "function") {
|
|
2686
|
-
printInfo("This is a runtime plugin instance (has init function)");
|
|
2687
|
-
}
|
|
2688
|
-
}
|
|
2689
|
-
if (typeof found === "string") {
|
|
2690
|
-
printInfo("This is a string reference (will be imported at runtime)");
|
|
2691
|
-
}
|
|
2692
|
-
console.log("");
|
|
2693
|
-
} catch (error) {
|
|
2694
|
-
printError(error.message || String(error));
|
|
2695
|
-
process.exit(1);
|
|
2696
|
-
}
|
|
2697
|
-
});
|
|
2698
|
-
var addCommand = new Command12("add").description("Add a plugin to objectstack.config.ts").argument("<package>", "Plugin package name (e.g. @objectstack/plugin-auth)").option("-d, --dev", "Add as a dev-only plugin").option("-c, --config <path>", "Configuration file path").action(async (packageName, options) => {
|
|
2699
|
-
try {
|
|
2700
|
-
const configPath = resolveConfigPath(options?.config);
|
|
2701
|
-
printHeader("Add Plugin");
|
|
2702
|
-
console.log(` ${chalk11.dim("Package:")} ${chalk11.white(packageName)}`);
|
|
2703
|
-
console.log(` ${chalk11.dim("Config:")} ${chalk11.white(path10.relative(process.cwd(), configPath))}`);
|
|
2704
|
-
console.log("");
|
|
2705
|
-
addPluginToConfig(configPath, packageName);
|
|
2706
|
-
printSuccess(`Added ${chalk11.cyan(packageName)} to config`);
|
|
2707
|
-
console.log("");
|
|
2708
|
-
console.log(chalk11.dim(" Next steps:"));
|
|
2709
|
-
console.log(chalk11.dim(` 1. Install the package: pnpm add ${packageName}`));
|
|
2710
|
-
console.log(chalk11.dim(" 2. Run: os validate"));
|
|
2711
|
-
console.log("");
|
|
2712
|
-
} catch (error) {
|
|
2713
|
-
printError(error.message || String(error));
|
|
2714
|
-
process.exit(1);
|
|
2715
|
-
}
|
|
2716
|
-
});
|
|
2717
|
-
var removeCommand = new Command12("remove").alias("rm").description("Remove a plugin from objectstack.config.ts").argument("<name>", "Plugin name or package name to remove").option("-c, --config <path>", "Configuration file path").action(async (pluginName, options) => {
|
|
2718
|
-
try {
|
|
2719
|
-
const configPath = resolveConfigPath(options?.config);
|
|
2720
|
-
printHeader("Remove Plugin");
|
|
2721
|
-
console.log(` ${chalk11.dim("Plugin:")} ${chalk11.white(pluginName)}`);
|
|
2722
|
-
console.log(` ${chalk11.dim("Config:")} ${chalk11.white(path10.relative(process.cwd(), configPath))}`);
|
|
2723
|
-
console.log("");
|
|
2724
|
-
removePluginFromConfig(configPath, pluginName);
|
|
2725
|
-
printSuccess(`Removed ${chalk11.cyan(pluginName)} from config`);
|
|
2726
|
-
console.log("");
|
|
2727
|
-
console.log(chalk11.dim(" Tip: Run `pnpm remove " + pluginName + "` to uninstall the package"));
|
|
2728
|
-
console.log("");
|
|
2729
|
-
} catch (error) {
|
|
2730
|
-
printError(error.message || String(error));
|
|
2731
|
-
process.exit(1);
|
|
2732
|
-
}
|
|
2733
|
-
});
|
|
2734
|
-
var pluginCommand = new Command12("plugin").description("Manage plugins (list, info, add, remove)").addCommand(listCommand).addCommand(infoSubCommand).addCommand(addCommand).addCommand(removeCommand);
|
|
2735
|
-
|
|
2736
|
-
// src/commands/diff.ts
|
|
2737
|
-
import { Command as Command13 } from "commander";
|
|
2738
|
-
import chalk12 from "chalk";
|
|
2739
|
-
function getNames(items) {
|
|
2740
|
-
const map = /* @__PURE__ */ new Map();
|
|
2741
|
-
if (!Array.isArray(items)) return map;
|
|
2742
|
-
for (const item of items) {
|
|
2743
|
-
if (item?.name) map.set(item.name, item);
|
|
2744
|
-
}
|
|
2745
|
-
return map;
|
|
2746
|
-
}
|
|
2747
|
-
function getFieldNames(obj) {
|
|
2748
|
-
if (!obj?.fields || typeof obj.fields !== "object") return [];
|
|
2749
|
-
return Object.keys(obj.fields);
|
|
2750
|
-
}
|
|
2751
|
-
function getFieldType(obj, fieldName) {
|
|
2752
|
-
return obj?.fields?.[fieldName]?.type;
|
|
2753
|
-
}
|
|
2754
|
-
function isFieldRequired(obj, fieldName) {
|
|
2755
|
-
return obj?.fields?.[fieldName]?.required === true;
|
|
2756
|
-
}
|
|
2757
|
-
function diffNamedArrays(beforeItems, afterItems, category, detectFieldChanges) {
|
|
2758
|
-
const entries = [];
|
|
2759
|
-
const beforeMap = getNames(beforeItems);
|
|
2760
|
-
const afterMap = getNames(afterItems);
|
|
2761
|
-
for (const [name] of beforeMap) {
|
|
2762
|
-
if (!afterMap.has(name)) {
|
|
2763
|
-
entries.push({
|
|
2764
|
-
type: "removed",
|
|
2765
|
-
category,
|
|
2766
|
-
name,
|
|
2767
|
-
breaking: true
|
|
2768
|
-
});
|
|
2769
|
-
}
|
|
2770
|
-
}
|
|
2771
|
-
for (const [name] of afterMap) {
|
|
2772
|
-
if (!beforeMap.has(name)) {
|
|
2773
|
-
entries.push({
|
|
2774
|
-
type: "added",
|
|
2775
|
-
category,
|
|
2776
|
-
name,
|
|
2777
|
-
breaking: false
|
|
2778
|
-
});
|
|
2779
|
-
}
|
|
2780
|
-
}
|
|
2781
|
-
if (detectFieldChanges) {
|
|
2782
|
-
for (const [name, beforeObj] of beforeMap) {
|
|
2783
|
-
const afterObj = afterMap.get(name);
|
|
2784
|
-
if (!afterObj) continue;
|
|
2785
|
-
const beforeFields = getFieldNames(beforeObj);
|
|
2786
|
-
const afterFields = getFieldNames(afterObj);
|
|
2787
|
-
const beforeSet = new Set(beforeFields);
|
|
2788
|
-
const afterSet = new Set(afterFields);
|
|
2789
|
-
for (const f of beforeFields) {
|
|
2790
|
-
if (!afterSet.has(f)) {
|
|
2791
|
-
entries.push({
|
|
2792
|
-
type: "removed",
|
|
2793
|
-
category: `${category}.${name}.fields`,
|
|
2794
|
-
name: f,
|
|
2795
|
-
breaking: true,
|
|
2796
|
-
detail: "field removed"
|
|
2797
|
-
});
|
|
2798
|
-
}
|
|
2799
|
-
}
|
|
2800
|
-
for (const f of afterFields) {
|
|
2801
|
-
if (!beforeSet.has(f)) {
|
|
2802
|
-
const breaking = isFieldRequired(afterObj, f);
|
|
2803
|
-
entries.push({
|
|
2804
|
-
type: "added",
|
|
2805
|
-
category: `${category}.${name}.fields`,
|
|
2806
|
-
name: f,
|
|
2807
|
-
breaking,
|
|
2808
|
-
detail: breaking ? "required field added" : "optional field added"
|
|
2809
|
-
});
|
|
2810
|
-
}
|
|
2811
|
-
}
|
|
2812
|
-
for (const f of beforeFields) {
|
|
2813
|
-
if (!afterSet.has(f)) continue;
|
|
2814
|
-
const oldType = getFieldType(beforeObj, f);
|
|
2815
|
-
const newType = getFieldType(afterObj, f);
|
|
2816
|
-
if (oldType && newType && oldType !== newType) {
|
|
2817
|
-
entries.push({
|
|
2818
|
-
type: "modified",
|
|
2819
|
-
category: `${category}.${name}.fields`,
|
|
2820
|
-
name: f,
|
|
2821
|
-
breaking: true,
|
|
2822
|
-
detail: `type changed: ${oldType} \u2192 ${newType}`
|
|
2823
|
-
});
|
|
2824
|
-
}
|
|
2825
|
-
}
|
|
2826
|
-
if (beforeObj.label !== afterObj.label) {
|
|
2827
|
-
entries.push({
|
|
2828
|
-
type: "modified",
|
|
2829
|
-
category,
|
|
2830
|
-
name,
|
|
2831
|
-
breaking: false,
|
|
2832
|
-
detail: `label changed: "${beforeObj.label ?? "(none)"}" \u2192 "${afterObj.label ?? "(none)"}"`
|
|
2833
|
-
});
|
|
2834
|
-
}
|
|
2835
|
-
}
|
|
2836
|
-
}
|
|
2837
|
-
return entries;
|
|
2838
|
-
}
|
|
2839
|
-
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) => {
|
|
2840
|
-
const timer = createTimer();
|
|
2841
|
-
const beforePath = beforeArg || options.before;
|
|
2842
|
-
const afterPath = afterArg || options.after;
|
|
2843
|
-
if (!beforePath || !afterPath) {
|
|
2844
|
-
printError("Two config file paths are required.");
|
|
2845
|
-
console.log("");
|
|
2846
|
-
console.log(chalk12.dim(" Usage: objectstack diff <before> <after>"));
|
|
2847
|
-
console.log(chalk12.dim(" or: objectstack diff --before path1 --after path2"));
|
|
2848
|
-
process.exit(1);
|
|
2849
|
-
}
|
|
2850
|
-
if (!options.json) {
|
|
2851
|
-
printHeader("Diff");
|
|
2852
|
-
printStep("Loading configurations...");
|
|
2853
|
-
}
|
|
2854
|
-
try {
|
|
2855
|
-
const { config: beforeConfig } = await loadConfig(beforePath);
|
|
2856
|
-
const { config: afterConfig } = await loadConfig(afterPath);
|
|
2857
|
-
if (!options.json) {
|
|
2858
|
-
printInfo(`Before: ${chalk12.white(beforePath)}`);
|
|
2859
|
-
printInfo(`After: ${chalk12.white(afterPath)}`);
|
|
2860
|
-
}
|
|
2861
|
-
const allDiffs = [];
|
|
2862
|
-
allDiffs.push(...diffNamedArrays(beforeConfig.objects, afterConfig.objects, "objects", true));
|
|
2863
|
-
const simpleCats = [
|
|
2864
|
-
{ key: "views", label: "views" },
|
|
2865
|
-
{ key: "flows", label: "flows" },
|
|
2866
|
-
{ key: "agents", label: "agents" },
|
|
2867
|
-
{ key: "apps", label: "apps" },
|
|
2868
|
-
{ key: "dashboards", label: "dashboards" },
|
|
2869
|
-
{ key: "actions", label: "actions" },
|
|
2870
|
-
{ key: "workflows", label: "workflows" },
|
|
2871
|
-
{ key: "apis", label: "apis" },
|
|
2872
|
-
{ key: "roles", label: "roles" }
|
|
2873
|
-
];
|
|
2874
|
-
for (const cat of simpleCats) {
|
|
2875
|
-
allDiffs.push(
|
|
2876
|
-
...diffNamedArrays(beforeConfig[cat.key], afterConfig[cat.key], cat.label, false)
|
|
2877
|
-
);
|
|
2878
|
-
}
|
|
2879
|
-
const diffs = options.breakingOnly ? allDiffs.filter((d) => d.breaking) : allDiffs;
|
|
2880
|
-
const breakingCount = allDiffs.filter((d) => d.breaking).length;
|
|
2881
|
-
if (options.json) {
|
|
2882
|
-
console.log(JSON.stringify({
|
|
2883
|
-
before: beforePath,
|
|
2884
|
-
after: afterPath,
|
|
2885
|
-
total: diffs.length,
|
|
2886
|
-
breaking: breakingCount,
|
|
2887
|
-
changes: diffs,
|
|
2888
|
-
duration: timer.elapsed()
|
|
2889
|
-
}, null, 2));
|
|
2890
|
-
return;
|
|
2891
|
-
}
|
|
2892
|
-
console.log("");
|
|
2893
|
-
if (diffs.length === 0) {
|
|
2894
|
-
printSuccess(options.breakingOnly ? "No breaking changes detected." : "No changes detected.");
|
|
2895
|
-
console.log("");
|
|
2896
|
-
return;
|
|
2897
|
-
}
|
|
2898
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
2899
|
-
for (const d of diffs) {
|
|
2900
|
-
const key = d.category;
|
|
2901
|
-
if (!grouped.has(key)) grouped.set(key, []);
|
|
2902
|
-
grouped.get(key).push(d);
|
|
2903
|
-
}
|
|
2904
|
-
for (const [category, items] of grouped) {
|
|
2905
|
-
console.log(` ${chalk12.bold(category)}`);
|
|
2906
|
-
for (const item of items) {
|
|
2907
|
-
const icon = item.type === "added" ? "+" : item.type === "removed" ? "-" : "~";
|
|
2908
|
-
const color = item.type === "added" ? chalk12.green : item.type === "removed" ? chalk12.red : chalk12.yellow;
|
|
2909
|
-
const breakingTag = item.breaking ? chalk12.bgRed.white(" BREAKING ") + " " : "";
|
|
2910
|
-
const detail = item.detail ? chalk12.dim(` (${item.detail})`) : "";
|
|
2911
|
-
console.log(` ${color(icon)} ${breakingTag}${color(item.name)}${detail}`);
|
|
2912
|
-
}
|
|
2913
|
-
console.log("");
|
|
2914
|
-
}
|
|
2915
|
-
if (breakingCount > 0) {
|
|
2916
|
-
printError(`${breakingCount} breaking change(s) detected`);
|
|
2917
|
-
} else {
|
|
2918
|
-
printSuccess("No breaking changes");
|
|
2919
|
-
}
|
|
2920
|
-
console.log(chalk12.dim(` ${diffs.length} total change(s) in ${timer.display()}`));
|
|
2921
|
-
console.log("");
|
|
2922
|
-
} catch (error) {
|
|
2923
|
-
if (options.json) {
|
|
2924
|
-
console.log(JSON.stringify({ error: error.message }));
|
|
2925
|
-
process.exit(1);
|
|
2926
|
-
}
|
|
2927
|
-
console.log("");
|
|
2928
|
-
printError(error.message || String(error));
|
|
2929
|
-
process.exit(1);
|
|
2930
|
-
}
|
|
2931
|
-
});
|
|
2932
|
-
|
|
2933
|
-
// src/commands/lint.ts
|
|
2934
|
-
import { Command as Command14 } from "commander";
|
|
2935
|
-
import chalk13 from "chalk";
|
|
2936
|
-
import { normalizeStackInput as normalizeStackInput5 } from "@objectstack/spec";
|
|
2937
|
-
var SNAKE_CASE_RE = /^[a-z][a-z0-9_]*$/;
|
|
2938
|
-
function checkSnakeCase(value, path12, label) {
|
|
2939
|
-
if (!SNAKE_CASE_RE.test(value)) {
|
|
2940
|
-
return {
|
|
2941
|
-
severity: "error",
|
|
2942
|
-
rule: "naming/snake-case",
|
|
2943
|
-
message: `${label} "${value}" must be snake_case`,
|
|
2944
|
-
path: path12,
|
|
2945
|
-
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, "_")
|
|
2946
|
-
};
|
|
2947
|
-
}
|
|
2948
|
-
return null;
|
|
2949
|
-
}
|
|
2950
|
-
function checkLabelExists(item, path12, kind) {
|
|
2951
|
-
if (!item.label) {
|
|
2952
|
-
return {
|
|
2953
|
-
severity: "error",
|
|
2954
|
-
rule: "required/label",
|
|
2955
|
-
message: `${kind} "${item.name || "?"}" is missing a label`,
|
|
2956
|
-
path: path12
|
|
2957
|
-
};
|
|
2958
|
-
}
|
|
2959
|
-
return null;
|
|
2960
|
-
}
|
|
2961
|
-
function checkLabelCase(label, path12) {
|
|
2962
|
-
if (label && label[0] !== label[0].toUpperCase()) {
|
|
2963
|
-
return {
|
|
2964
|
-
severity: "warning",
|
|
2965
|
-
rule: "convention/label-case",
|
|
2966
|
-
message: `Label "${label}" should start with an uppercase letter`,
|
|
2967
|
-
path: path12,
|
|
2968
|
-
fix: label.charAt(0).toUpperCase() + label.slice(1)
|
|
2969
|
-
};
|
|
2970
|
-
}
|
|
2971
|
-
return null;
|
|
2972
|
-
}
|
|
2973
|
-
function lintConfig(config) {
|
|
2974
|
-
const issues = [];
|
|
2975
|
-
const push = (issue) => {
|
|
2976
|
-
if (issue) issues.push(issue);
|
|
2977
|
-
};
|
|
2978
|
-
const objects = Array.isArray(config.objects) ? config.objects : [];
|
|
2979
|
-
for (let i = 0; i < objects.length; i++) {
|
|
2980
|
-
const obj = objects[i];
|
|
2981
|
-
const objPath = `objects[${i}]`;
|
|
2982
|
-
if (obj.name) {
|
|
2983
|
-
push(checkSnakeCase(obj.name, `${objPath}.name`, "Object name"));
|
|
2984
|
-
}
|
|
2985
|
-
push(checkLabelExists(obj, `${objPath}.label`, "Object"));
|
|
2986
|
-
if (obj.label) {
|
|
2987
|
-
push(checkLabelCase(obj.label, `${objPath}.label`));
|
|
2988
|
-
}
|
|
2989
|
-
if (obj.fields && typeof obj.fields === "object") {
|
|
2990
|
-
const fieldNames = Object.keys(obj.fields);
|
|
2991
|
-
if (fieldNames.length === 0) {
|
|
2992
|
-
issues.push({
|
|
2993
|
-
severity: "warning",
|
|
2994
|
-
rule: "structure/empty-fields",
|
|
2995
|
-
message: `Object "${obj.name || "?"}" has an empty fields map`,
|
|
2996
|
-
path: `${objPath}.fields`
|
|
2997
|
-
});
|
|
2998
|
-
}
|
|
2999
|
-
for (const fieldName of fieldNames) {
|
|
3000
|
-
const field = obj.fields[fieldName];
|
|
3001
|
-
const fieldPath = `${objPath}.fields.${fieldName}`;
|
|
3002
|
-
push(checkSnakeCase(fieldName, fieldPath, "Field name"));
|
|
3003
|
-
if (field && typeof field === "object") {
|
|
3004
|
-
push(checkLabelExists({ ...field, name: fieldName }, `${fieldPath}.label`, "Field"));
|
|
3005
|
-
if (field.label) {
|
|
3006
|
-
push(checkLabelCase(field.label, `${fieldPath}.label`));
|
|
3007
|
-
}
|
|
3008
|
-
}
|
|
3009
|
-
}
|
|
3010
|
-
} else if (!obj.fields) {
|
|
3011
|
-
issues.push({
|
|
3012
|
-
severity: "error",
|
|
3013
|
-
rule: "structure/no-fields",
|
|
3014
|
-
message: `Object "${obj.name || "?"}" has no fields defined`,
|
|
3015
|
-
path: `${objPath}.fields`
|
|
3016
|
-
});
|
|
3017
|
-
}
|
|
3018
|
-
}
|
|
3019
|
-
const views = Array.isArray(config.views) ? config.views : [];
|
|
3020
|
-
for (let i = 0; i < views.length; i++) {
|
|
3021
|
-
const view = views[i];
|
|
3022
|
-
const viewPath = `views[${i}]`;
|
|
3023
|
-
if (view.name) {
|
|
3024
|
-
push(checkSnakeCase(view.name, `${viewPath}.name`, "View name"));
|
|
3025
|
-
}
|
|
3026
|
-
push(checkLabelExists(view, `${viewPath}.label`, "View"));
|
|
3027
|
-
if (view.label) {
|
|
3028
|
-
push(checkLabelCase(view.label, `${viewPath}.label`));
|
|
3029
|
-
}
|
|
3030
|
-
}
|
|
3031
|
-
const apps = Array.isArray(config.apps) ? config.apps : [];
|
|
3032
|
-
for (let i = 0; i < apps.length; i++) {
|
|
3033
|
-
const app = apps[i];
|
|
3034
|
-
const appPath = `apps[${i}]`;
|
|
3035
|
-
if (app.name) {
|
|
3036
|
-
push(checkSnakeCase(app.name, `${appPath}.name`, "App name"));
|
|
3037
|
-
}
|
|
3038
|
-
push(checkLabelExists(app, `${appPath}.label`, "App"));
|
|
3039
|
-
if (app.label) {
|
|
3040
|
-
push(checkLabelCase(app.label, `${appPath}.label`));
|
|
3041
|
-
}
|
|
3042
|
-
}
|
|
3043
|
-
const flows = Array.isArray(config.flows) ? config.flows : [];
|
|
3044
|
-
for (let i = 0; i < flows.length; i++) {
|
|
3045
|
-
const flow = flows[i];
|
|
3046
|
-
const flowPath = `flows[${i}]`;
|
|
3047
|
-
if (flow.name) {
|
|
3048
|
-
push(checkSnakeCase(flow.name, `${flowPath}.name`, "Flow name"));
|
|
3049
|
-
}
|
|
3050
|
-
}
|
|
3051
|
-
const agents = Array.isArray(config.agents) ? config.agents : [];
|
|
3052
|
-
for (let i = 0; i < agents.length; i++) {
|
|
3053
|
-
const agent = agents[i];
|
|
3054
|
-
const agentPath = `agents[${i}]`;
|
|
3055
|
-
if (agent.name) {
|
|
3056
|
-
push(checkSnakeCase(agent.name, `${agentPath}.name`, "Agent name"));
|
|
3057
|
-
}
|
|
3058
|
-
}
|
|
3059
|
-
return issues;
|
|
3060
|
-
}
|
|
3061
|
-
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) => {
|
|
3062
|
-
const timer = createTimer();
|
|
3063
|
-
if (!options.json) {
|
|
3064
|
-
printHeader("Lint");
|
|
3065
|
-
printStep("Loading configuration...");
|
|
3066
|
-
}
|
|
3067
|
-
try {
|
|
3068
|
-
const { config, absolutePath } = await loadConfig(configPath);
|
|
3069
|
-
if (!options.json) {
|
|
3070
|
-
printInfo(`Config: ${chalk13.white(absolutePath)}`);
|
|
3071
|
-
}
|
|
3072
|
-
const normalized = normalizeStackInput5(config);
|
|
3073
|
-
const issues = lintConfig(normalized);
|
|
3074
|
-
if (options.json) {
|
|
3075
|
-
const errors2 = issues.filter((i) => i.severity === "error");
|
|
3076
|
-
const warnings2 = issues.filter((i) => i.severity === "warning");
|
|
3077
|
-
const suggestions2 = issues.filter((i) => i.severity === "suggestion");
|
|
3078
|
-
console.log(JSON.stringify({
|
|
3079
|
-
passed: errors2.length === 0,
|
|
3080
|
-
total: issues.length,
|
|
3081
|
-
errors: errors2.length,
|
|
3082
|
-
warnings: warnings2.length,
|
|
3083
|
-
suggestions: suggestions2.length,
|
|
3084
|
-
issues,
|
|
3085
|
-
duration: timer.elapsed()
|
|
3086
|
-
}, null, 2));
|
|
3087
|
-
if (errors2.length > 0) process.exit(1);
|
|
3088
|
-
return;
|
|
3089
|
-
}
|
|
3090
|
-
console.log("");
|
|
3091
|
-
if (issues.length === 0) {
|
|
3092
|
-
printSuccess(`All checks passed ${chalk13.dim(`(${timer.display()})`)}`);
|
|
3093
|
-
console.log("");
|
|
3094
|
-
return;
|
|
3095
|
-
}
|
|
3096
|
-
const errors = issues.filter((i) => i.severity === "error");
|
|
3097
|
-
const warnings = issues.filter((i) => i.severity === "warning");
|
|
3098
|
-
const suggestions = issues.filter((i) => i.severity === "suggestion");
|
|
3099
|
-
const printIssue = (issue) => {
|
|
3100
|
-
const color = issue.severity === "error" ? chalk13.red : issue.severity === "warning" ? chalk13.yellow : chalk13.blue;
|
|
3101
|
-
const icon = issue.severity === "error" ? "\u2717" : issue.severity === "warning" ? "\u26A0" : "\u2139";
|
|
3102
|
-
console.log(` ${color(icon)} ${color(issue.message)}`);
|
|
3103
|
-
console.log(chalk13.dim(` ${issue.rule} at ${issue.path}`));
|
|
3104
|
-
if (options.fix && issue.fix) {
|
|
3105
|
-
console.log(chalk13.green(` \u2192 fix: ${issue.fix}`));
|
|
3106
|
-
}
|
|
3107
|
-
};
|
|
3108
|
-
if (errors.length > 0) {
|
|
3109
|
-
console.log(chalk13.bold.red(` Errors (${errors.length})`));
|
|
3110
|
-
errors.forEach(printIssue);
|
|
3111
|
-
console.log("");
|
|
3112
|
-
}
|
|
3113
|
-
if (warnings.length > 0) {
|
|
3114
|
-
console.log(chalk13.bold.yellow(` Warnings (${warnings.length})`));
|
|
3115
|
-
warnings.forEach(printIssue);
|
|
3116
|
-
console.log("");
|
|
3117
|
-
}
|
|
3118
|
-
if (suggestions.length > 0) {
|
|
3119
|
-
console.log(chalk13.bold.blue(` Suggestions (${suggestions.length})`));
|
|
3120
|
-
suggestions.forEach(printIssue);
|
|
3121
|
-
console.log("");
|
|
3122
|
-
}
|
|
3123
|
-
const parts = [];
|
|
3124
|
-
if (errors.length > 0) parts.push(chalk13.red(`${errors.length} error(s)`));
|
|
3125
|
-
if (warnings.length > 0) parts.push(chalk13.yellow(`${warnings.length} warning(s)`));
|
|
3126
|
-
if (suggestions.length > 0) parts.push(chalk13.blue(`${suggestions.length} suggestion(s)`));
|
|
3127
|
-
console.log(` ${parts.join(", ")} ${chalk13.dim(`(${timer.display()})`)}`);
|
|
3128
|
-
if (options.fix) {
|
|
3129
|
-
console.log("");
|
|
3130
|
-
printInfo("Dry-run mode: no files were modified.");
|
|
3131
|
-
}
|
|
3132
|
-
console.log("");
|
|
3133
|
-
if (errors.length > 0) process.exit(1);
|
|
3134
|
-
} catch (error) {
|
|
3135
|
-
if (options.json) {
|
|
3136
|
-
console.log(JSON.stringify({ error: error.message }));
|
|
3137
|
-
process.exit(1);
|
|
3138
|
-
}
|
|
3139
|
-
console.log("");
|
|
3140
|
-
printError(error.message || String(error));
|
|
3141
|
-
process.exit(1);
|
|
3142
|
-
}
|
|
3143
|
-
});
|
|
3144
|
-
|
|
3145
|
-
// src/commands/explain.ts
|
|
3146
|
-
import { Command as Command15 } from "commander";
|
|
3147
|
-
import chalk14 from "chalk";
|
|
3148
|
-
var SCHEMAS = {
|
|
3149
|
-
object: {
|
|
3150
|
-
name: "Object",
|
|
3151
|
-
description: "Defines a data entity in the ObjectStack data model. Objects contain fields, enable capabilities, and form the foundation of the metadata-driven platform.",
|
|
3152
|
-
required: [
|
|
3153
|
-
{ name: "name", type: "string (snake_case)", description: "Machine name identifier" },
|
|
3154
|
-
{ name: "fields", type: "Record<string, Field>", description: "Map of field definitions" }
|
|
3155
|
-
],
|
|
3156
|
-
optional: [
|
|
3157
|
-
{ name: "label", type: "string", description: "Human-readable display name" },
|
|
3158
|
-
{ name: "pluralLabel", type: "string", description: "Plural display name" },
|
|
3159
|
-
{ name: "description", type: "string", description: "Documentation for the object" },
|
|
3160
|
-
{ name: "ownership", type: '"own" | "extend"', description: "Whether this object is owned or extended" },
|
|
3161
|
-
{ name: "enable", type: "ObjectCapabilities", description: "Feature flags (trackHistory, apiEnabled, etc.)" },
|
|
3162
|
-
{ name: "icon", type: "string", description: "Icon identifier for UI display" }
|
|
3163
|
-
],
|
|
3164
|
-
example: `{
|
|
3165
|
-
name: 'project_task',
|
|
3166
|
-
label: 'Project Task',
|
|
3167
|
-
fields: {
|
|
3168
|
-
title: { type: 'text', label: 'Title', required: true },
|
|
3169
|
-
status: { type: 'select', label: 'Status', options: ['open', 'closed'] },
|
|
3170
|
-
assigned_to: { type: 'lookup', label: 'Assigned To', reference: 'user' },
|
|
3171
|
-
},
|
|
3172
|
-
enable: { trackHistory: true, apiEnabled: true },
|
|
3173
|
-
}`,
|
|
3174
|
-
related: ["field", "view", "flow", "query"],
|
|
3175
|
-
docsPath: "data/object"
|
|
3176
|
-
},
|
|
3177
|
-
field: {
|
|
3178
|
-
name: "Field",
|
|
3179
|
-
description: "Defines a property on an Object. Fields have types that control validation, storage, and UI rendering.",
|
|
3180
|
-
required: [
|
|
3181
|
-
{ name: "type", type: "FieldType", description: "Field data type (text, number, boolean, select, lookup, etc.)" }
|
|
3182
|
-
],
|
|
3183
|
-
optional: [
|
|
3184
|
-
{ name: "label", type: "string", description: "Human-readable display name" },
|
|
3185
|
-
{ name: "required", type: "boolean", description: "Whether the field is mandatory" },
|
|
3186
|
-
{ name: "multiple", type: "boolean", description: "Whether the field holds an array of values" },
|
|
3187
|
-
{ name: "defaultValue", type: "any", description: "Default value for new records" },
|
|
3188
|
-
{ name: "maxLength", type: "number", description: "Maximum character length (text fields)" },
|
|
3189
|
-
{ name: "reference", type: "string", description: "Target object name (lookup fields)" },
|
|
3190
|
-
{ name: "options", type: "string[]", description: "Available choices (select fields)" }
|
|
3191
|
-
],
|
|
3192
|
-
example: `{
|
|
3193
|
-
type: 'text',
|
|
3194
|
-
label: 'Email Address',
|
|
3195
|
-
required: true,
|
|
3196
|
-
maxLength: 255,
|
|
3197
|
-
}`,
|
|
3198
|
-
related: ["object", "view", "query"],
|
|
3199
|
-
docsPath: "data/field"
|
|
3200
|
-
},
|
|
3201
|
-
view: {
|
|
3202
|
-
name: "View",
|
|
3203
|
-
description: "Defines how data is displayed and interacted with. Views can be list views (grid, kanban, calendar) or form views (simple, tabbed, wizard).",
|
|
3204
|
-
required: [
|
|
3205
|
-
{ name: "name", type: "string (snake_case)", description: "Machine name identifier" },
|
|
3206
|
-
{ name: "object", type: "string", description: "Target object to display" },
|
|
3207
|
-
{ name: "type", type: '"list" | "form"', description: "View category" }
|
|
3208
|
-
],
|
|
3209
|
-
optional: [
|
|
3210
|
-
{ name: "label", type: "string", description: "Display name" },
|
|
3211
|
-
{ name: "layout", type: '"grid" | "kanban" | "calendar" | "gantt" | "simple" | "tabbed" | "wizard"', description: "Layout style" },
|
|
3212
|
-
{ name: "columns", type: "string[]", description: "Visible fields for list views" },
|
|
3213
|
-
{ name: "filters", type: "Filter[]", description: "Default query filters" },
|
|
3214
|
-
{ name: "sort", type: "SortConfig", description: "Default sort configuration" }
|
|
3215
|
-
],
|
|
3216
|
-
example: `{
|
|
3217
|
-
name: 'task_board',
|
|
3218
|
-
object: 'project_task',
|
|
3219
|
-
type: 'list',
|
|
3220
|
-
label: 'Task Board',
|
|
3221
|
-
layout: 'kanban',
|
|
3222
|
-
columns: ['title', 'status', 'assigned_to'],
|
|
3223
|
-
}`,
|
|
3224
|
-
related: ["object", "app", "action", "dashboard"],
|
|
3225
|
-
docsPath: "ui/view"
|
|
3226
|
-
},
|
|
3227
|
-
flow: {
|
|
3228
|
-
name: "Flow",
|
|
3229
|
-
description: "Visual logic orchestration for business processes. Flows can be auto-launched, screen-based, or scheduled.",
|
|
3230
|
-
required: [
|
|
3231
|
-
{ name: "name", type: "string (snake_case)", description: "Machine name identifier" },
|
|
3232
|
-
{ name: "type", type: '"autolaunched" | "screen" | "schedule"', description: "Trigger type" }
|
|
3233
|
-
],
|
|
3234
|
-
optional: [
|
|
3235
|
-
{ name: "label", type: "string", description: "Display name" },
|
|
3236
|
-
{ name: "description", type: "string", description: "Documentation for the flow" },
|
|
3237
|
-
{ name: "trigger", type: "TriggerConfig", description: "Event that starts the flow" },
|
|
3238
|
-
{ name: "steps", type: "FlowStep[]", description: "Sequence of actions" },
|
|
3239
|
-
{ name: "variables", type: "Variable[]", description: "Flow-scoped variables" }
|
|
3240
|
-
],
|
|
3241
|
-
example: `{
|
|
3242
|
-
name: 'assign_on_create',
|
|
3243
|
-
type: 'autolaunched',
|
|
3244
|
-
label: 'Auto-Assign on Create',
|
|
3245
|
-
trigger: { object: 'project_task', event: 'afterInsert' },
|
|
3246
|
-
steps: [
|
|
3247
|
-
{ type: 'assignment', field: 'assigned_to', value: '$currentUser' },
|
|
3248
|
-
],
|
|
3249
|
-
}`,
|
|
3250
|
-
related: ["object", "trigger", "workflow", "agent"],
|
|
3251
|
-
docsPath: "automation/flow"
|
|
3252
|
-
},
|
|
3253
|
-
agent: {
|
|
3254
|
-
name: "Agent",
|
|
3255
|
-
description: "Autonomous AI actor that can perform tasks using tools, instructions, and context from the ObjectStack data model.",
|
|
3256
|
-
required: [
|
|
3257
|
-
{ name: "name", type: "string (snake_case)", description: "Machine name identifier" },
|
|
3258
|
-
{ name: "role", type: "string", description: "Agent purpose description" }
|
|
3259
|
-
],
|
|
3260
|
-
optional: [
|
|
3261
|
-
{ name: "instructions", type: "string", description: "System prompt / behavioral instructions" },
|
|
3262
|
-
{ name: "tools", type: "ToolReference[]", description: "Tools the agent can invoke" },
|
|
3263
|
-
{ name: "model", type: "string", description: "LLM model to use" },
|
|
3264
|
-
{ name: "objects", type: "string[]", description: "Objects the agent can access" },
|
|
3265
|
-
{ name: "temperature", type: "number", description: "Creativity parameter (0-1)" }
|
|
3266
|
-
],
|
|
3267
|
-
example: `{
|
|
3268
|
-
name: 'support_agent',
|
|
3269
|
-
role: 'Customer Support Assistant',
|
|
3270
|
-
instructions: 'Help users resolve issues by searching the knowledge base.',
|
|
3271
|
-
tools: [{ name: 'query', object: 'knowledge_article' }],
|
|
3272
|
-
model: 'gpt-4o',
|
|
3273
|
-
}`,
|
|
3274
|
-
related: ["object", "flow", "query"],
|
|
3275
|
-
docsPath: "ai/agent"
|
|
3276
|
-
},
|
|
3277
|
-
app: {
|
|
3278
|
-
name: "App",
|
|
3279
|
-
description: "Application shell that groups navigation, branding, and views into a cohesive user experience.",
|
|
3280
|
-
required: [
|
|
3281
|
-
{ name: "name", type: "string (snake_case)", description: "Machine name identifier" }
|
|
3282
|
-
],
|
|
3283
|
-
optional: [
|
|
3284
|
-
{ name: "label", type: "string", description: "Display name" },
|
|
3285
|
-
{ name: "description", type: "string", description: "App description" },
|
|
3286
|
-
{ name: "navigation", type: "NavItem[]", description: "Menu tree structure" },
|
|
3287
|
-
{ name: "logo", type: "string", description: "Logo URL or asset path" },
|
|
3288
|
-
{ name: "theme", type: "string", description: "Theme reference" },
|
|
3289
|
-
{ name: "defaultRoute", type: "string", description: "Landing page route" }
|
|
3290
|
-
],
|
|
3291
|
-
example: `{
|
|
3292
|
-
name: 'project_manager',
|
|
3293
|
-
label: 'Project Manager',
|
|
3294
|
-
navigation: [
|
|
3295
|
-
{ type: 'object', object: 'project_task', label: 'Tasks' },
|
|
3296
|
-
{ type: 'dashboard', dashboard: 'project_overview', label: 'Overview' },
|
|
3297
|
-
],
|
|
3298
|
-
}`,
|
|
3299
|
-
related: ["view", "dashboard", "action", "object"],
|
|
3300
|
-
docsPath: "ui/app"
|
|
3301
|
-
},
|
|
3302
|
-
query: {
|
|
3303
|
-
name: "Query",
|
|
3304
|
-
description: "Declarative data retrieval definition used for fetching and filtering records from objects.",
|
|
3305
|
-
required: [
|
|
3306
|
-
{ name: "object", type: "string", description: "Target object to query" }
|
|
3307
|
-
],
|
|
3308
|
-
optional: [
|
|
3309
|
-
{ name: "fields", type: "string[]", description: "Fields to select" },
|
|
3310
|
-
{ name: "filters", type: "Filter[]", description: "Where conditions" },
|
|
3311
|
-
{ name: "sort", type: "SortConfig[]", description: "Order by configuration" },
|
|
3312
|
-
{ name: "limit", type: "number", description: "Maximum records to return" },
|
|
3313
|
-
{ name: "offset", type: "number", description: "Pagination offset" }
|
|
3314
|
-
],
|
|
3315
|
-
example: `{
|
|
3316
|
-
object: 'project_task',
|
|
3317
|
-
fields: ['title', 'status', 'assigned_to'],
|
|
3318
|
-
filters: [{ field: 'status', operator: 'eq', value: 'open' }],
|
|
3319
|
-
sort: [{ field: 'created_at', direction: 'desc' }],
|
|
3320
|
-
limit: 50,
|
|
3321
|
-
}`,
|
|
3322
|
-
related: ["object", "field", "view"],
|
|
3323
|
-
docsPath: "data/query"
|
|
3324
|
-
},
|
|
3325
|
-
dashboard: {
|
|
3326
|
-
name: "Dashboard",
|
|
3327
|
-
description: "Grid-layout container for widgets that display aggregated data, charts, and key metrics.",
|
|
3328
|
-
required: [
|
|
3329
|
-
{ name: "name", type: "string (snake_case)", description: "Machine name identifier" }
|
|
3330
|
-
],
|
|
3331
|
-
optional: [
|
|
3332
|
-
{ name: "label", type: "string", description: "Display name" },
|
|
3333
|
-
{ name: "widgets", type: "Widget[]", description: "Dashboard widget definitions" },
|
|
3334
|
-
{ name: "layout", type: "GridLayout", description: "Widget positioning" },
|
|
3335
|
-
{ name: "refreshInterval", type: "number", description: "Auto-refresh interval in seconds" }
|
|
3336
|
-
],
|
|
3337
|
-
example: `{
|
|
3338
|
-
name: 'project_overview',
|
|
3339
|
-
label: 'Project Overview',
|
|
3340
|
-
widgets: [
|
|
3341
|
-
{ type: 'chart', object: 'project_task', groupBy: 'status' },
|
|
3342
|
-
{ type: 'metric', object: 'project_task', aggregate: 'count' },
|
|
3343
|
-
],
|
|
3344
|
-
}`,
|
|
3345
|
-
related: ["app", "view", "object"],
|
|
3346
|
-
docsPath: "ui/dashboard"
|
|
3347
|
-
},
|
|
3348
|
-
action: {
|
|
3349
|
-
name: "Action",
|
|
3350
|
-
description: "User-triggered operation such as a button click, URL redirect, or screen flow launch.",
|
|
3351
|
-
required: [
|
|
3352
|
-
{ name: "name", type: "string (snake_case)", description: "Machine name identifier" },
|
|
3353
|
-
{ name: "type", type: '"button" | "url" | "flow" | "api"', description: "Action type" }
|
|
3354
|
-
],
|
|
3355
|
-
optional: [
|
|
3356
|
-
{ name: "label", type: "string", description: "Display text" },
|
|
3357
|
-
{ name: "icon", type: "string", description: "Button icon" },
|
|
3358
|
-
{ name: "object", type: "string", description: "Target object context" },
|
|
3359
|
-
{ name: "flow", type: "string", description: "Flow to launch (for flow actions)" },
|
|
3360
|
-
{ name: "url", type: "string", description: "Target URL (for url actions)" },
|
|
3361
|
-
{ name: "confirmation", type: "string", description: "Confirmation dialog message" }
|
|
3362
|
-
],
|
|
3363
|
-
example: `{
|
|
3364
|
-
name: 'close_task',
|
|
3365
|
-
type: 'flow',
|
|
3366
|
-
label: 'Close Task',
|
|
3367
|
-
object: 'project_task',
|
|
3368
|
-
flow: 'close_task_flow',
|
|
3369
|
-
confirmation: 'Are you sure you want to close this task?',
|
|
3370
|
-
}`,
|
|
3371
|
-
related: ["flow", "view", "app"],
|
|
3372
|
-
docsPath: "ui/action"
|
|
3373
|
-
},
|
|
3374
|
-
workflow: {
|
|
3375
|
-
name: "Workflow",
|
|
3376
|
-
description: "State machine and approval process that governs record lifecycle transitions.",
|
|
3377
|
-
required: [
|
|
3378
|
-
{ name: "name", type: "string (snake_case)", description: "Machine name identifier" },
|
|
3379
|
-
{ name: "object", type: "string", description: "Target object" }
|
|
3380
|
-
],
|
|
3381
|
-
optional: [
|
|
3382
|
-
{ name: "label", type: "string", description: "Display name" },
|
|
3383
|
-
{ name: "states", type: "State[]", description: "Defined workflow states" },
|
|
3384
|
-
{ name: "transitions", type: "Transition[]", description: "Allowed state transitions" },
|
|
3385
|
-
{ name: "approvers", type: "ApproverConfig", description: "Approval chain configuration" }
|
|
3386
|
-
],
|
|
3387
|
-
example: `{
|
|
3388
|
-
name: 'task_approval',
|
|
3389
|
-
object: 'project_task',
|
|
3390
|
-
label: 'Task Approval',
|
|
3391
|
-
states: ['draft', 'pending', 'approved', 'rejected'],
|
|
3392
|
-
transitions: [
|
|
3393
|
-
{ from: 'draft', to: 'pending', action: 'submit' },
|
|
3394
|
-
{ from: 'pending', to: 'approved', action: 'approve' },
|
|
3395
|
-
{ from: 'pending', to: 'rejected', action: 'reject' },
|
|
3396
|
-
],
|
|
3397
|
-
}`,
|
|
3398
|
-
related: ["object", "flow", "action"],
|
|
3399
|
-
docsPath: "automation/workflow"
|
|
3400
|
-
},
|
|
3401
|
-
trigger: {
|
|
3402
|
-
name: "Trigger",
|
|
3403
|
-
description: "Event-driven automation hook that fires when specific data events occur on an object.",
|
|
3404
|
-
required: [
|
|
3405
|
-
{ name: "name", type: "string (snake_case)", description: "Machine name identifier" },
|
|
3406
|
-
{ name: "object", type: "string", description: "Target object to monitor" },
|
|
3407
|
-
{ name: "event", type: '"beforeInsert" | "afterInsert" | "beforeUpdate" | "afterUpdate" | "beforeDelete" | "afterDelete"', description: "Event type" }
|
|
3408
|
-
],
|
|
3409
|
-
optional: [
|
|
3410
|
-
{ name: "label", type: "string", description: "Display name" },
|
|
3411
|
-
{ name: "condition", type: "FilterExpression", description: "Conditional guard" },
|
|
3412
|
-
{ name: "flow", type: "string", description: "Flow to execute" },
|
|
3413
|
-
{ name: "async", type: "boolean", description: "Whether to execute asynchronously" }
|
|
3414
|
-
],
|
|
3415
|
-
example: `{
|
|
3416
|
-
name: 'task_created_notify',
|
|
3417
|
-
object: 'project_task',
|
|
3418
|
-
event: 'afterInsert',
|
|
3419
|
-
label: 'Notify on Task Creation',
|
|
3420
|
-
flow: 'send_task_notification',
|
|
3421
|
-
async: true,
|
|
3422
|
-
}`,
|
|
3423
|
-
related: ["object", "flow", "workflow"],
|
|
3424
|
-
docsPath: "automation/trigger"
|
|
3425
|
-
}
|
|
3426
|
-
};
|
|
3427
|
-
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) => {
|
|
3428
|
-
if (!schemaName) {
|
|
3429
|
-
if (options.json) {
|
|
3430
|
-
console.log(JSON.stringify({
|
|
3431
|
-
schemas: Object.entries(SCHEMAS).map(([key, s]) => ({
|
|
3432
|
-
name: key,
|
|
3433
|
-
description: s.description
|
|
3434
|
-
}))
|
|
3435
|
-
}, null, 2));
|
|
3436
|
-
return;
|
|
3437
|
-
}
|
|
3438
|
-
printHeader("Available Schemas");
|
|
3439
|
-
console.log("");
|
|
3440
|
-
for (const [key, schema2] of Object.entries(SCHEMAS)) {
|
|
3441
|
-
const desc = schema2.description;
|
|
3442
|
-
console.log(` ${chalk14.bold.cyan(key.padEnd(12))} ${chalk14.dim(desc.length > 70 ? desc.slice(0, 70) + "..." : desc)}`);
|
|
3443
|
-
}
|
|
3444
|
-
console.log("");
|
|
3445
|
-
printInfo(`Run ${chalk14.white("objectstack explain <schema>")} for details.`);
|
|
3446
|
-
console.log("");
|
|
3447
|
-
return;
|
|
3448
|
-
}
|
|
3449
|
-
const schema = SCHEMAS[schemaName.toLowerCase()];
|
|
3450
|
-
if (!schema) {
|
|
3451
|
-
if (options.json) {
|
|
3452
|
-
console.log(JSON.stringify({ error: `Unknown schema: ${schemaName}` }));
|
|
3453
|
-
process.exit(1);
|
|
3454
|
-
}
|
|
3455
|
-
printError(`Unknown schema: "${schemaName}"`);
|
|
3456
|
-
console.log("");
|
|
3457
|
-
printInfo(`Available schemas: ${Object.keys(SCHEMAS).join(", ")}`);
|
|
3458
|
-
console.log("");
|
|
3459
|
-
process.exit(1);
|
|
3460
|
-
}
|
|
3461
|
-
if (options.json) {
|
|
3462
|
-
console.log(JSON.stringify(schema, null, 2));
|
|
3463
|
-
return;
|
|
3464
|
-
}
|
|
3465
|
-
printHeader(`Schema: ${schema.name}`);
|
|
3466
|
-
console.log("");
|
|
3467
|
-
console.log(` ${schema.description}`);
|
|
3468
|
-
console.log("");
|
|
3469
|
-
console.log(chalk14.bold(" Required Properties:"));
|
|
3470
|
-
for (const prop of schema.required) {
|
|
3471
|
-
console.log(` ${chalk14.green(prop.name.padEnd(18))} ${chalk14.dim(prop.type.padEnd(30))} ${prop.description}`);
|
|
3472
|
-
}
|
|
3473
|
-
if (schema.optional.length > 0) {
|
|
3474
|
-
console.log("");
|
|
3475
|
-
console.log(chalk14.bold(" Optional Properties:"));
|
|
3476
|
-
for (const prop of schema.optional) {
|
|
3477
|
-
console.log(` ${chalk14.yellow(prop.name.padEnd(18))} ${chalk14.dim(prop.type.padEnd(30))} ${prop.description}`);
|
|
3478
|
-
}
|
|
3479
|
-
}
|
|
3480
|
-
console.log("");
|
|
3481
|
-
console.log(chalk14.bold(" Example:"));
|
|
3482
|
-
for (const line of schema.example.split("\n")) {
|
|
3483
|
-
console.log(chalk14.dim(` ${line}`));
|
|
3484
|
-
}
|
|
3485
|
-
console.log("");
|
|
3486
|
-
printKV(" Related", schema.related.map((r) => chalk14.cyan(r)).join(", "));
|
|
3487
|
-
printKV(" Docs", `https://objectstack.dev/docs/${schema.docsPath}`);
|
|
3488
|
-
console.log("");
|
|
3489
|
-
});
|
|
3490
|
-
|
|
3491
|
-
// src/commands/codemod.ts
|
|
3492
|
-
import { Command as Command16 } from "commander";
|
|
3493
|
-
import chalk15 from "chalk";
|
|
3494
|
-
import fs11 from "fs";
|
|
3495
|
-
import path11 from "path";
|
|
3496
|
-
var V2_TO_V3_TRANSFORMS = [
|
|
3497
|
-
{
|
|
3498
|
-
pattern: /\bEnhancedObjectKernel\b/g,
|
|
3499
|
-
replacement: "ObjectKernel",
|
|
3500
|
-
description: "EnhancedObjectKernel \u2192 ObjectKernel"
|
|
3501
|
-
},
|
|
3502
|
-
{
|
|
3503
|
-
pattern: /\bmax_length\b/g,
|
|
3504
|
-
replacement: "maxLength",
|
|
3505
|
-
description: "max_length \u2192 maxLength"
|
|
3506
|
-
},
|
|
3507
|
-
{
|
|
3508
|
-
pattern: /\breference_filters\b/g,
|
|
3509
|
-
replacement: "referenceFilters",
|
|
3510
|
-
description: "reference_filters \u2192 referenceFilters"
|
|
3511
|
-
},
|
|
3512
|
-
{
|
|
3513
|
-
pattern: /\bdefault_value\b/g,
|
|
3514
|
-
replacement: "defaultValue",
|
|
3515
|
-
description: "default_value \u2192 defaultValue"
|
|
3516
|
-
},
|
|
3517
|
-
{
|
|
3518
|
-
pattern: /\bmin_length\b/g,
|
|
3519
|
-
replacement: "minLength",
|
|
3520
|
-
description: "min_length \u2192 minLength"
|
|
3521
|
-
},
|
|
3522
|
-
{
|
|
3523
|
-
pattern: /\bunique_name\b/g,
|
|
3524
|
-
replacement: "uniqueName",
|
|
3525
|
-
description: "unique_name \u2192 uniqueName"
|
|
3526
|
-
},
|
|
3527
|
-
{
|
|
3528
|
-
pattern: /from\s+['"]@objectstack\/core\/enhanced['"]/g,
|
|
3529
|
-
replacement: "from '@objectstack/core'",
|
|
3530
|
-
description: "Update import from @objectstack/core/enhanced"
|
|
3531
|
-
},
|
|
3532
|
-
{
|
|
3533
|
-
pattern: /from\s+['"]@objectstack\/spec\/dist\/[^'"]+['"]/g,
|
|
3534
|
-
replacement: "from '@objectstack/spec'",
|
|
3535
|
-
description: "Update deep import from @objectstack/spec/dist/"
|
|
3536
|
-
}
|
|
3537
|
-
];
|
|
3538
|
-
function walkDir2(dir, ext) {
|
|
3539
|
-
const results = [];
|
|
3540
|
-
if (!fs11.existsSync(dir)) return results;
|
|
3541
|
-
const entries = fs11.readdirSync(dir, { withFileTypes: true });
|
|
3542
|
-
for (const entry of entries) {
|
|
3543
|
-
if (entry.name === "node_modules") continue;
|
|
3544
|
-
const fullPath = path11.join(dir, entry.name);
|
|
3545
|
-
if (entry.isDirectory()) {
|
|
3546
|
-
results.push(...walkDir2(fullPath, ext));
|
|
3547
|
-
} else if (entry.name.endsWith(ext)) {
|
|
3548
|
-
results.push(fullPath);
|
|
3549
|
-
}
|
|
3550
|
-
}
|
|
3551
|
-
return results;
|
|
3552
|
-
}
|
|
3553
|
-
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) => {
|
|
3554
|
-
printHeader("Codemod: v2 \u2192 v3");
|
|
3555
|
-
const timer = createTimer();
|
|
3556
|
-
const dir = path11.resolve(process.cwd(), options.dir);
|
|
3557
|
-
if (!fs11.existsSync(dir)) {
|
|
3558
|
-
printError(`Directory not found: ${dir}`);
|
|
3559
|
-
process.exit(1);
|
|
3560
|
-
}
|
|
3561
|
-
console.log(` ${chalk15.dim("Directory:")} ${chalk15.white(options.dir)}`);
|
|
3562
|
-
console.log(` ${chalk15.dim("Dry run:")} ${chalk15.white(options.dryRun ? "yes" : "no")}`);
|
|
3563
|
-
console.log("");
|
|
3564
|
-
printStep("Scanning TypeScript files...");
|
|
3565
|
-
const files = walkDir2(dir, ".ts");
|
|
3566
|
-
if (files.length === 0) {
|
|
3567
|
-
printInfo("No .ts files found in directory");
|
|
3568
|
-
return;
|
|
3569
|
-
}
|
|
3570
|
-
printInfo(`Found ${files.length} TypeScript file(s)`);
|
|
3571
|
-
console.log("");
|
|
3572
|
-
let totalTransforms = 0;
|
|
3573
|
-
let filesModified = 0;
|
|
3574
|
-
const transformCounts = {};
|
|
3575
|
-
for (const file of files) {
|
|
3576
|
-
const original = fs11.readFileSync(file, "utf-8");
|
|
3577
|
-
let content = original;
|
|
3578
|
-
let fileTransforms = 0;
|
|
3579
|
-
for (const transform of V2_TO_V3_TRANSFORMS) {
|
|
3580
|
-
const matches = content.match(transform.pattern);
|
|
3581
|
-
if (matches) {
|
|
3582
|
-
const count = matches.length;
|
|
3583
|
-
content = content.replace(transform.pattern, transform.replacement);
|
|
3584
|
-
fileTransforms += count;
|
|
3585
|
-
transformCounts[transform.description] = (transformCounts[transform.description] || 0) + count;
|
|
3586
|
-
}
|
|
3587
|
-
}
|
|
3588
|
-
if (fileTransforms > 0) {
|
|
3589
|
-
const relPath = path11.relative(process.cwd(), file);
|
|
3590
|
-
filesModified++;
|
|
3591
|
-
totalTransforms += fileTransforms;
|
|
3592
|
-
if (options.dryRun) {
|
|
3593
|
-
printInfo(`${relPath} \u2014 ${fileTransforms} change(s)`);
|
|
3594
|
-
} else {
|
|
3595
|
-
fs11.writeFileSync(file, content);
|
|
3596
|
-
printSuccess(`${relPath} \u2014 ${fileTransforms} change(s)`);
|
|
3597
|
-
}
|
|
3598
|
-
}
|
|
3599
|
-
}
|
|
3600
|
-
console.log("");
|
|
3601
|
-
if (totalTransforms === 0) {
|
|
3602
|
-
printSuccess("No v2 patterns found \u2014 code is already v3 compatible");
|
|
3603
|
-
} else {
|
|
3604
|
-
console.log(chalk15.bold(" Summary:"));
|
|
3605
|
-
for (const [desc, count] of Object.entries(transformCounts)) {
|
|
3606
|
-
console.log(` ${chalk15.dim(desc)}: ${chalk15.white(count)}`);
|
|
3607
|
-
}
|
|
3608
|
-
console.log("");
|
|
3609
|
-
if (options.dryRun) {
|
|
3610
|
-
printInfo(`Would modify ${filesModified} file(s) with ${totalTransforms} total change(s)`);
|
|
3611
|
-
console.log(chalk15.dim(" Run without --dry-run to apply changes"));
|
|
3612
|
-
} else {
|
|
3613
|
-
printSuccess(`Modified ${filesModified} file(s) with ${totalTransforms} total change(s) (${timer.display()})`);
|
|
3614
|
-
}
|
|
3615
|
-
}
|
|
3616
|
-
console.log("");
|
|
3617
|
-
});
|
|
3618
|
-
var codemodCommand = new Command16("codemod").description("Run automated code transformations").addCommand(v2ToV3Command).action(() => {
|
|
3619
|
-
printHeader("Codemod");
|
|
3620
|
-
console.log(chalk15.bold(" Available codemods:"));
|
|
3621
|
-
console.log(` ${chalk15.cyan("v2-to-v3".padEnd(16))} Migrate ObjectStack v2 code patterns to v3`);
|
|
3622
|
-
console.log("");
|
|
3623
|
-
console.log(chalk15.dim(" Usage: objectstack codemod v2-to-v3 [--dir src/] [--dry-run]"));
|
|
3624
|
-
console.log("");
|
|
3625
|
-
});
|
|
3626
|
-
|
|
3627
|
-
// src/utils/plugin-commands.ts
|
|
3628
|
-
import chalk16 from "chalk";
|
|
3629
|
-
async function loadPluginCommands(program2) {
|
|
3630
|
-
let config;
|
|
3631
|
-
try {
|
|
3632
|
-
const loaded = await loadConfig();
|
|
3633
|
-
config = loaded.config;
|
|
3634
|
-
} catch {
|
|
3635
|
-
return;
|
|
3636
|
-
}
|
|
3637
|
-
const plugins = [
|
|
3638
|
-
...config.plugins || [],
|
|
3639
|
-
...config.devPlugins || []
|
|
3640
|
-
];
|
|
3641
|
-
const contributions = [];
|
|
3642
|
-
for (const plugin of plugins) {
|
|
3643
|
-
if (!plugin || typeof plugin !== "object") continue;
|
|
3644
|
-
const p = plugin;
|
|
3645
|
-
const manifest = p.manifest;
|
|
3646
|
-
const contributes = manifest?.contributes ?? p.contributes;
|
|
3647
|
-
if (!contributes) continue;
|
|
3648
|
-
const commands = contributes.commands;
|
|
3649
|
-
if (!Array.isArray(commands)) continue;
|
|
3650
|
-
const pluginName = resolvePluginName2(p);
|
|
3651
|
-
for (const cmd of commands) {
|
|
3652
|
-
if (!cmd || typeof cmd.name !== "string") continue;
|
|
3653
|
-
contributions.push({
|
|
3654
|
-
name: cmd.name,
|
|
3655
|
-
description: typeof cmd.description === "string" ? cmd.description : void 0,
|
|
3656
|
-
module: typeof cmd.module === "string" ? cmd.module : void 0,
|
|
3657
|
-
pluginName
|
|
3658
|
-
});
|
|
3659
|
-
}
|
|
3660
|
-
}
|
|
3661
|
-
if (contributions.length === 0) return;
|
|
3662
|
-
for (const contribution of contributions) {
|
|
3663
|
-
try {
|
|
3664
|
-
const commands = await importPluginCommands(contribution);
|
|
3665
|
-
for (const cmd of commands) {
|
|
3666
|
-
program2.addCommand(cmd);
|
|
3667
|
-
}
|
|
3668
|
-
} catch (error) {
|
|
3669
|
-
if (process.env.DEBUG) {
|
|
3670
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3671
|
-
console.error(
|
|
3672
|
-
chalk16.yellow(` \u26A0 Failed to load CLI command '${contribution.name}' from plugin '${contribution.pluginName}': ${message}`)
|
|
3673
|
-
);
|
|
3674
|
-
}
|
|
3675
|
-
}
|
|
3676
|
-
}
|
|
3677
|
-
}
|
|
3678
|
-
async function importPluginCommands(contribution) {
|
|
3679
|
-
const moduleId = contribution.module ? `${contribution.pluginName}/${contribution.module.replace(/^\.\//, "")}` : contribution.pluginName;
|
|
3680
|
-
const mod = await import(moduleId);
|
|
3681
|
-
if (Array.isArray(mod.commands)) {
|
|
3682
|
-
return mod.commands.filter(isCommandInstance);
|
|
3683
|
-
}
|
|
3684
|
-
const defaultExport = mod.default;
|
|
3685
|
-
if (defaultExport) {
|
|
3686
|
-
if (Array.isArray(defaultExport)) {
|
|
3687
|
-
return defaultExport.filter(isCommandInstance);
|
|
3688
|
-
}
|
|
3689
|
-
if (isCommandInstance(defaultExport)) {
|
|
3690
|
-
return [defaultExport];
|
|
3691
|
-
}
|
|
3692
|
-
}
|
|
3693
|
-
const commands = [];
|
|
3694
|
-
for (const key of Object.keys(mod)) {
|
|
3695
|
-
if (isCommandInstance(mod[key])) {
|
|
3696
|
-
commands.push(mod[key]);
|
|
3697
|
-
}
|
|
3698
|
-
}
|
|
3699
|
-
return commands;
|
|
3700
|
-
}
|
|
3701
|
-
function isCommandInstance(value) {
|
|
3702
|
-
if (value === null || typeof value !== "object") return false;
|
|
3703
|
-
const obj = value;
|
|
3704
|
-
return typeof obj.name === "function" && typeof obj.description === "function" && typeof obj.action === "function" && typeof obj.parse === "function";
|
|
3705
|
-
}
|
|
3706
|
-
function resolvePluginName2(plugin) {
|
|
3707
|
-
if (typeof plugin.name === "string") return plugin.name;
|
|
3708
|
-
const manifest = plugin.manifest;
|
|
3709
|
-
if (manifest && typeof manifest.name === "string") return manifest.name;
|
|
3710
|
-
if (plugin.constructor && plugin.constructor.name !== "Object") return plugin.constructor.name;
|
|
3711
|
-
return "unknown";
|
|
3712
|
-
}
|
|
3713
|
-
|
|
3714
|
-
// src/bin.ts
|
|
3715
|
-
var require2 = createRequire2(import.meta.url);
|
|
3716
|
-
var pkg = require2("../package.json");
|
|
3717
|
-
process.on("unhandledRejection", (err) => {
|
|
3718
|
-
console.error(chalk17.red(`
|
|
3719
|
-
\u2717 Unhandled error: ${err?.message || err}`));
|
|
3720
|
-
if (err?.stack && process.env.DEBUG) {
|
|
3721
|
-
console.error(chalk17.dim(err.stack));
|
|
3722
|
-
}
|
|
3723
|
-
process.exit(1);
|
|
3724
|
-
});
|
|
3725
|
-
var program = new Command17();
|
|
3726
|
-
program.name("objectstack").description("ObjectStack CLI \u2014 Build metadata-driven apps with the ObjectStack Protocol").version(pkg.version, "-v, --version").configureHelp({
|
|
3727
|
-
sortSubcommands: false
|
|
3728
|
-
}).addHelpText("before", `
|
|
3729
|
-
${chalk17.bold.cyan("\u25C6 ObjectStack CLI")} ${chalk17.dim(`v${pkg.version}`)}
|
|
3730
|
-
`).addHelpText("after", `
|
|
3731
|
-
${chalk17.bold("Workflow:")}
|
|
3732
|
-
${chalk17.dim("$")} os init ${chalk17.dim("# Create a new project")}
|
|
3733
|
-
${chalk17.dim("$")} os generate object task ${chalk17.dim("# Add metadata")}
|
|
3734
|
-
${chalk17.dim("$")} os plugin add <package> ${chalk17.dim("# Add a plugin")}
|
|
3735
|
-
${chalk17.dim("$")} os validate ${chalk17.dim("# Check configuration")}
|
|
3736
|
-
${chalk17.dim("$")} os dev ${chalk17.dim("# Start dev server")}
|
|
3737
|
-
${chalk17.dim("$")} os studio ${chalk17.dim("# Dev server + Studio UI")}
|
|
3738
|
-
${chalk17.dim("$")} os compile ${chalk17.dim("# Build for production")}
|
|
3739
|
-
|
|
3740
|
-
${chalk17.dim("Aliases: objectstack | os")}
|
|
3741
|
-
${chalk17.dim("Docs: https://objectstack.dev")}
|
|
3742
|
-
`);
|
|
3743
|
-
program.addCommand(initCommand);
|
|
3744
|
-
program.addCommand(devCommand);
|
|
3745
|
-
program.addCommand(serveCommand);
|
|
3746
|
-
program.addCommand(studioCommand);
|
|
3747
|
-
program.addCommand(compileCommand);
|
|
3748
|
-
program.addCommand(validateCommand);
|
|
3749
|
-
program.addCommand(infoCommand);
|
|
3750
|
-
program.addCommand(generateCommand);
|
|
3751
|
-
program.addCommand(createCommand);
|
|
3752
|
-
program.addCommand(pluginCommand);
|
|
3753
|
-
program.addCommand(testCommand);
|
|
3754
|
-
program.addCommand(doctorCommand);
|
|
3755
|
-
program.addCommand(lintCommand);
|
|
3756
|
-
program.addCommand(diffCommand);
|
|
3757
|
-
program.addCommand(explainCommand);
|
|
3758
|
-
program.addCommand(codemodCommand);
|
|
3759
|
-
loadPluginCommands(program).then(() => {
|
|
3760
|
-
program.parse(process.argv);
|
|
3761
|
-
}).catch((err) => {
|
|
3762
|
-
if (process.env.DEBUG) {
|
|
3763
|
-
console.error(chalk17.yellow(`
|
|
3764
|
-
\u26A0 Plugin command loading failed: ${err?.message || err}`));
|
|
3765
|
-
}
|
|
3766
|
-
program.parse(process.argv);
|
|
3767
|
-
});
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
/**
|
|
3
|
+
* ObjectStack CLI — oclif-based entry point.
|
|
4
|
+
*
|
|
5
|
+
* All commands are auto-discovered from `src/commands/` by oclif.
|
|
6
|
+
* Plugins extend the CLI via oclif's built-in plugin system
|
|
7
|
+
* (configured in package.json under "oclif.plugins").
|
|
8
|
+
*
|
|
9
|
+
* Run `os --help` for available commands.
|
|
10
|
+
*/
|
|
11
|
+
export { execute } from '@oclif/core';
|
|
12
|
+
//# sourceMappingURL=bin.js.map
|