@objectstack/cli 1.0.11 → 1.0.12
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 +14 -9
- package/CHANGELOG.md +13 -0
- package/README.md +132 -13
- package/dist/bin.js +1488 -178
- package/dist/index.d.ts +97 -1
- package/dist/index.js +1938 -5
- package/package.json +9 -9
- package/src/bin.ts +53 -6
- package/src/commands/compile.ts +66 -39
- package/src/commands/create.ts +15 -11
- package/src/commands/dev.ts +12 -16
- package/src/commands/doctor.ts +13 -9
- package/src/commands/generate.ts +297 -0
- package/src/commands/info.ts +111 -0
- package/src/commands/init.ts +313 -0
- package/src/commands/serve.ts +134 -48
- package/src/commands/studio.ts +40 -0
- package/src/commands/test.ts +2 -2
- package/src/commands/validate.ts +130 -0
- package/src/index.ts +9 -0
- package/src/utils/config.ts +78 -0
- package/src/utils/console.ts +319 -0
- package/src/utils/format.ts +261 -0
- package/test/commands.test.ts +26 -1
- package/tsup.config.ts +18 -9
- package/dist/bin.d.ts +0 -2
- package/dist/chunk-2YXVEYO7.js +0 -64
package/dist/index.js
CHANGED
|
@@ -1,7 +1,1940 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// src/commands/compile.ts
|
|
9
|
+
import { Command } from "commander";
|
|
10
|
+
import path2 from "path";
|
|
11
|
+
import fs2 from "fs";
|
|
12
|
+
import chalk3 from "chalk";
|
|
13
|
+
import { ObjectStackDefinitionSchema } from "@objectstack/spec";
|
|
14
|
+
|
|
15
|
+
// src/utils/config.ts
|
|
16
|
+
import path from "path";
|
|
17
|
+
import fs from "fs";
|
|
18
|
+
import chalk2 from "chalk";
|
|
19
|
+
import { bundleRequire } from "bundle-require";
|
|
20
|
+
|
|
21
|
+
// src/utils/format.ts
|
|
22
|
+
import chalk from "chalk";
|
|
23
|
+
function printHeader(title) {
|
|
24
|
+
console.log(chalk.bold(`
|
|
25
|
+
\u25C6 ${title}`));
|
|
26
|
+
console.log(chalk.dim("\u2500".repeat(40)));
|
|
27
|
+
}
|
|
28
|
+
function printKV(key, value, icon) {
|
|
29
|
+
const prefix = icon ? `${icon} ` : " ";
|
|
30
|
+
console.log(`${prefix}${chalk.dim(key + ":")} ${chalk.white(String(value))}`);
|
|
31
|
+
}
|
|
32
|
+
function printSuccess(msg) {
|
|
33
|
+
console.log(chalk.green(` \u2713 ${msg}`));
|
|
34
|
+
}
|
|
35
|
+
function printWarning(msg) {
|
|
36
|
+
console.log(chalk.yellow(` \u26A0 ${msg}`));
|
|
37
|
+
}
|
|
38
|
+
function printError(msg) {
|
|
39
|
+
console.log(chalk.red(` \u2717 ${msg}`));
|
|
40
|
+
}
|
|
41
|
+
function printInfo(msg) {
|
|
42
|
+
console.log(chalk.blue(` \u2139 ${msg}`));
|
|
43
|
+
}
|
|
44
|
+
function printStep(msg) {
|
|
45
|
+
console.log(chalk.yellow(` \u2192 ${msg}`));
|
|
46
|
+
}
|
|
47
|
+
function createTimer() {
|
|
48
|
+
const start = Date.now();
|
|
49
|
+
return {
|
|
50
|
+
elapsed: () => Date.now() - start,
|
|
51
|
+
display: () => `${Date.now() - start}ms`
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function formatZodErrors(error) {
|
|
55
|
+
const issues = error.issues || error.errors || [];
|
|
56
|
+
if (issues.length === 0) {
|
|
57
|
+
console.log(chalk.red(" Unknown validation error"));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
61
|
+
for (const issue of issues) {
|
|
62
|
+
const topPath = issue.path?.[0] || "_root";
|
|
63
|
+
if (!grouped.has(String(topPath))) {
|
|
64
|
+
grouped.set(String(topPath), []);
|
|
65
|
+
}
|
|
66
|
+
grouped.get(String(topPath)).push(issue);
|
|
67
|
+
}
|
|
68
|
+
for (const [section, sectionIssues] of grouped) {
|
|
69
|
+
console.log(chalk.bold.red(`
|
|
70
|
+
${section}:`));
|
|
71
|
+
for (const issue of sectionIssues) {
|
|
72
|
+
const path11 = issue.path?.join(".") || "";
|
|
73
|
+
const code = issue.code || "";
|
|
74
|
+
const msg = issue.message || "";
|
|
75
|
+
console.log(chalk.red(` \u2717 ${path11}`));
|
|
76
|
+
console.log(chalk.dim(` ${code}: ${msg}`));
|
|
77
|
+
if (issue.expected) {
|
|
78
|
+
console.log(chalk.dim(` expected: ${chalk.green(issue.expected)}`));
|
|
79
|
+
}
|
|
80
|
+
if (issue.received) {
|
|
81
|
+
console.log(chalk.dim(` received: ${chalk.red(issue.received)}`));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
console.log("");
|
|
86
|
+
console.log(chalk.dim(` ${issues.length} validation error(s) total`));
|
|
87
|
+
}
|
|
88
|
+
function collectMetadataStats(config) {
|
|
89
|
+
const count = (arr) => Array.isArray(arr) ? arr.length : 0;
|
|
90
|
+
let fields = 0;
|
|
91
|
+
if (Array.isArray(config.objects)) {
|
|
92
|
+
for (const obj of config.objects) {
|
|
93
|
+
if (obj.fields && typeof obj.fields === "object") {
|
|
94
|
+
fields += Object.keys(obj.fields).length;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
objects: count(config.objects),
|
|
100
|
+
objectExtensions: count(config.objectExtensions),
|
|
101
|
+
fields,
|
|
102
|
+
views: count(config.views),
|
|
103
|
+
pages: count(config.pages),
|
|
104
|
+
apps: count(config.apps),
|
|
105
|
+
dashboards: count(config.dashboards),
|
|
106
|
+
reports: count(config.reports),
|
|
107
|
+
actions: count(config.actions),
|
|
108
|
+
flows: count(config.flows),
|
|
109
|
+
workflows: count(config.workflows),
|
|
110
|
+
approvals: count(config.approvals),
|
|
111
|
+
agents: count(config.agents),
|
|
112
|
+
apis: count(config.apis),
|
|
113
|
+
roles: count(config.roles),
|
|
114
|
+
permissions: count(config.permissions),
|
|
115
|
+
themes: count(config.themes),
|
|
116
|
+
datasources: count(config.datasources),
|
|
117
|
+
translations: count(config.translations),
|
|
118
|
+
plugins: count(config.plugins),
|
|
119
|
+
devPlugins: count(config.devPlugins)
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function printServerReady(opts) {
|
|
123
|
+
const base = `http://localhost:${opts.port}`;
|
|
124
|
+
console.log("");
|
|
125
|
+
console.log(chalk.bold.green(" \u2713 Server is ready"));
|
|
126
|
+
console.log("");
|
|
127
|
+
console.log(chalk.cyan(" \u279C") + chalk.bold(" API: ") + chalk.cyan(base + "/"));
|
|
128
|
+
if (opts.uiEnabled && opts.studioPath) {
|
|
129
|
+
console.log(chalk.cyan(" \u279C") + chalk.bold(" Console: ") + chalk.cyan(base + opts.studioPath + "/"));
|
|
130
|
+
}
|
|
131
|
+
console.log("");
|
|
132
|
+
console.log(chalk.dim(` Config: ${opts.configFile}`));
|
|
133
|
+
console.log(chalk.dim(` Mode: ${opts.isDev ? "development" : "production"}`));
|
|
134
|
+
console.log(chalk.dim(` Plugins: ${opts.pluginCount} loaded`));
|
|
135
|
+
if (opts.pluginNames && opts.pluginNames.length > 0) {
|
|
136
|
+
console.log(chalk.dim(` ${opts.pluginNames.join(", ")}`));
|
|
137
|
+
}
|
|
138
|
+
console.log("");
|
|
139
|
+
console.log(chalk.dim(" Press Ctrl+C to stop"));
|
|
140
|
+
console.log("");
|
|
141
|
+
}
|
|
142
|
+
function printMetadataStats(stats) {
|
|
143
|
+
const sections = [
|
|
144
|
+
{
|
|
145
|
+
label: "Data",
|
|
146
|
+
items: [
|
|
147
|
+
["Objects", stats.objects],
|
|
148
|
+
["Fields", stats.fields],
|
|
149
|
+
["Extensions", stats.objectExtensions],
|
|
150
|
+
["Datasources", stats.datasources]
|
|
151
|
+
]
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
label: "UI",
|
|
155
|
+
items: [
|
|
156
|
+
["Apps", stats.apps],
|
|
157
|
+
["Views", stats.views],
|
|
158
|
+
["Pages", stats.pages],
|
|
159
|
+
["Dashboards", stats.dashboards],
|
|
160
|
+
["Reports", stats.reports],
|
|
161
|
+
["Actions", stats.actions],
|
|
162
|
+
["Themes", stats.themes]
|
|
163
|
+
]
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
label: "Logic",
|
|
167
|
+
items: [
|
|
168
|
+
["Flows", stats.flows],
|
|
169
|
+
["Workflows", stats.workflows],
|
|
170
|
+
["Approvals", stats.approvals],
|
|
171
|
+
["Agents", stats.agents],
|
|
172
|
+
["APIs", stats.apis]
|
|
173
|
+
]
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
label: "Security",
|
|
177
|
+
items: [
|
|
178
|
+
["Roles", stats.roles],
|
|
179
|
+
["Permissions", stats.permissions]
|
|
180
|
+
]
|
|
181
|
+
}
|
|
182
|
+
];
|
|
183
|
+
for (const section of sections) {
|
|
184
|
+
const nonZero = section.items.filter(([, v]) => v > 0);
|
|
185
|
+
if (nonZero.length === 0) continue;
|
|
186
|
+
const line = nonZero.map(([k, v]) => `${chalk.white(v)} ${chalk.dim(k)}`).join(" ");
|
|
187
|
+
console.log(` ${chalk.bold(section.label + ":")} ${line}`);
|
|
188
|
+
}
|
|
189
|
+
if (stats.plugins > 0 || stats.devPlugins > 0) {
|
|
190
|
+
const parts = [];
|
|
191
|
+
if (stats.plugins > 0) parts.push(`${stats.plugins} plugins`);
|
|
192
|
+
if (stats.devPlugins > 0) parts.push(`${stats.devPlugins} devPlugins`);
|
|
193
|
+
console.log(` ${chalk.bold("Runtime:")} ${chalk.dim(parts.join(", "))}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/utils/config.ts
|
|
198
|
+
function resolveConfigPath(source) {
|
|
199
|
+
if (source) {
|
|
200
|
+
const abs = path.resolve(process.cwd(), source);
|
|
201
|
+
if (!fs.existsSync(abs)) {
|
|
202
|
+
printError(`Config file not found: ${chalk2.white(abs)}`);
|
|
203
|
+
console.log("");
|
|
204
|
+
console.log(chalk2.dim(" Hint: Run this command from a directory with objectstack.config.ts"));
|
|
205
|
+
console.log(chalk2.dim(" Or specify the path: objectstack <command> path/to/config.ts"));
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
return abs;
|
|
209
|
+
}
|
|
210
|
+
const candidates = [
|
|
211
|
+
"objectstack.config.ts",
|
|
212
|
+
"objectstack.config.js",
|
|
213
|
+
"objectstack.config.mjs"
|
|
214
|
+
];
|
|
215
|
+
for (const candidate of candidates) {
|
|
216
|
+
const abs = path.resolve(process.cwd(), candidate);
|
|
217
|
+
if (fs.existsSync(abs)) return abs;
|
|
218
|
+
}
|
|
219
|
+
printError("No objectstack.config.{ts,js,mjs} found in current directory");
|
|
220
|
+
console.log("");
|
|
221
|
+
console.log(chalk2.dim(" Hint: Run `objectstack init` to create a new project"));
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
async function loadConfig(source) {
|
|
225
|
+
const absolutePath = resolveConfigPath(source);
|
|
226
|
+
const start = Date.now();
|
|
227
|
+
const { mod } = await bundleRequire({
|
|
228
|
+
filepath: absolutePath
|
|
229
|
+
});
|
|
230
|
+
const config = mod.default || mod;
|
|
231
|
+
if (!config) {
|
|
232
|
+
throw new Error(`No default export found in ${path.basename(absolutePath)}`);
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
config,
|
|
236
|
+
absolutePath,
|
|
237
|
+
duration: Date.now() - start
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/commands/compile.ts
|
|
242
|
+
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) => {
|
|
243
|
+
const timer = createTimer();
|
|
244
|
+
if (!options.json) {
|
|
245
|
+
printHeader("Compile");
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
if (!options.json) printStep("Loading configuration...");
|
|
249
|
+
const { config, absolutePath, duration } = await loadConfig(configPath);
|
|
250
|
+
if (!options.json) {
|
|
251
|
+
printKV("Config", path2.relative(process.cwd(), absolutePath));
|
|
252
|
+
printKV("Load time", `${duration}ms`);
|
|
253
|
+
}
|
|
254
|
+
if (!options.json) printStep("Validating protocol compliance...");
|
|
255
|
+
const result = ObjectStackDefinitionSchema.safeParse(config);
|
|
256
|
+
if (!result.success) {
|
|
257
|
+
if (options.json) {
|
|
258
|
+
console.log(JSON.stringify({ success: false, errors: result.error.issues }));
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
console.log("");
|
|
262
|
+
printError("Validation failed");
|
|
263
|
+
formatZodErrors(result.error);
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
if (!options.json) printStep("Writing artifact...");
|
|
267
|
+
const output = options.output;
|
|
268
|
+
const artifactPath = path2.resolve(process.cwd(), output);
|
|
269
|
+
const artifactDir = path2.dirname(artifactPath);
|
|
270
|
+
if (!fs2.existsSync(artifactDir)) {
|
|
271
|
+
fs2.mkdirSync(artifactDir, { recursive: true });
|
|
272
|
+
}
|
|
273
|
+
const jsonContent = JSON.stringify(result.data, null, 2);
|
|
274
|
+
fs2.writeFileSync(artifactPath, jsonContent);
|
|
275
|
+
const sizeKB = (jsonContent.length / 1024).toFixed(1);
|
|
276
|
+
const stats = collectMetadataStats(config);
|
|
277
|
+
if (options.json) {
|
|
278
|
+
console.log(JSON.stringify({
|
|
279
|
+
success: true,
|
|
280
|
+
output: artifactPath,
|
|
281
|
+
size: jsonContent.length,
|
|
282
|
+
stats,
|
|
283
|
+
duration: timer.elapsed()
|
|
284
|
+
}));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
console.log("");
|
|
288
|
+
printSuccess(`Build complete ${chalk3.dim(`(${timer.display()})`)}`);
|
|
289
|
+
console.log("");
|
|
290
|
+
printMetadataStats(stats);
|
|
291
|
+
console.log("");
|
|
292
|
+
printKV("Artifact", `${output} ${chalk3.dim(`(${sizeKB} KB`)})`);
|
|
293
|
+
console.log("");
|
|
294
|
+
} catch (error) {
|
|
295
|
+
if (options.json) {
|
|
296
|
+
console.log(JSON.stringify({ success: false, error: error.message }));
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
console.log("");
|
|
300
|
+
printError(error.message || String(error));
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// src/commands/validate.ts
|
|
306
|
+
import { Command as Command2 } from "commander";
|
|
307
|
+
import chalk4 from "chalk";
|
|
308
|
+
import { ObjectStackDefinitionSchema as ObjectStackDefinitionSchema2 } from "@objectstack/spec";
|
|
309
|
+
var validateCommand = new Command2("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) => {
|
|
310
|
+
const timer = createTimer();
|
|
311
|
+
if (!options.json) {
|
|
312
|
+
printHeader("Validate");
|
|
313
|
+
}
|
|
314
|
+
try {
|
|
315
|
+
if (!options.json) printStep("Loading configuration...");
|
|
316
|
+
const { config, absolutePath, duration } = await loadConfig(configPath);
|
|
317
|
+
if (!options.json) {
|
|
318
|
+
printKV("Config", absolutePath);
|
|
319
|
+
printKV("Load time", `${duration}ms`);
|
|
320
|
+
}
|
|
321
|
+
if (!options.json) printStep("Validating against ObjectStack Protocol...");
|
|
322
|
+
const result = ObjectStackDefinitionSchema2.safeParse(config);
|
|
323
|
+
if (!result.success) {
|
|
324
|
+
if (options.json) {
|
|
325
|
+
console.log(JSON.stringify({
|
|
326
|
+
valid: false,
|
|
327
|
+
errors: result.error.issues,
|
|
328
|
+
duration: timer.elapsed()
|
|
329
|
+
}, null, 2));
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
console.log("");
|
|
333
|
+
printError("Validation failed");
|
|
334
|
+
formatZodErrors(result.error);
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
const stats = collectMetadataStats(config);
|
|
338
|
+
if (options.json) {
|
|
339
|
+
console.log(JSON.stringify({
|
|
340
|
+
valid: true,
|
|
341
|
+
manifest: config.manifest,
|
|
342
|
+
stats,
|
|
343
|
+
duration: timer.elapsed()
|
|
344
|
+
}, null, 2));
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const warnings = [];
|
|
348
|
+
if (stats.objects === 0) {
|
|
349
|
+
warnings.push("No objects defined \u2014 this stack has no data model");
|
|
350
|
+
}
|
|
351
|
+
if (stats.apps === 0 && stats.plugins === 0) {
|
|
352
|
+
warnings.push("No apps or plugins defined \u2014 this stack may not do much");
|
|
353
|
+
}
|
|
354
|
+
if (!config.manifest?.id) {
|
|
355
|
+
warnings.push("Missing manifest.id \u2014 required for deployment");
|
|
356
|
+
}
|
|
357
|
+
if (!config.manifest?.namespace) {
|
|
358
|
+
warnings.push("Missing manifest.namespace \u2014 required for multi-app hosting");
|
|
359
|
+
}
|
|
360
|
+
console.log("");
|
|
361
|
+
printSuccess(`Validation passed ${chalk4.dim(`(${timer.display()})`)}`);
|
|
362
|
+
console.log("");
|
|
363
|
+
if (config.manifest) {
|
|
364
|
+
console.log(` ${chalk4.bold(config.manifest.name || config.manifest.id || "Unnamed")} ${chalk4.dim(`v${config.manifest.version || "0.0.0"}`)}`);
|
|
365
|
+
if (config.manifest.description) {
|
|
366
|
+
console.log(chalk4.dim(` ${config.manifest.description}`));
|
|
367
|
+
}
|
|
368
|
+
console.log("");
|
|
369
|
+
}
|
|
370
|
+
printMetadataStats(stats);
|
|
371
|
+
if (warnings.length > 0) {
|
|
372
|
+
console.log("");
|
|
373
|
+
for (const w of warnings) {
|
|
374
|
+
console.log(chalk4.yellow(` \u26A0 ${w}`));
|
|
375
|
+
}
|
|
376
|
+
if (options.strict) {
|
|
377
|
+
console.log("");
|
|
378
|
+
printError("Strict mode: warnings treated as errors");
|
|
379
|
+
process.exit(1);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
console.log("");
|
|
383
|
+
} catch (error) {
|
|
384
|
+
if (options.json) {
|
|
385
|
+
console.log(JSON.stringify({
|
|
386
|
+
valid: false,
|
|
387
|
+
error: error.message,
|
|
388
|
+
duration: timer.elapsed()
|
|
389
|
+
}, null, 2));
|
|
390
|
+
process.exit(1);
|
|
391
|
+
}
|
|
392
|
+
console.log("");
|
|
393
|
+
printError(error.message || String(error));
|
|
394
|
+
process.exit(1);
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// src/commands/info.ts
|
|
399
|
+
import { Command as Command3 } from "commander";
|
|
400
|
+
import chalk5 from "chalk";
|
|
401
|
+
var infoCommand = new Command3("info").description("Display metadata summary of an ObjectStack configuration").argument("[config]", "Configuration file path").option("--json", "Output as JSON").action(async (configPath, options) => {
|
|
402
|
+
const timer = createTimer();
|
|
403
|
+
if (!options.json) {
|
|
404
|
+
printHeader("Info");
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
const { config, absolutePath, duration } = await loadConfig(configPath);
|
|
408
|
+
const stats = collectMetadataStats(config);
|
|
409
|
+
if (options.json) {
|
|
410
|
+
console.log(JSON.stringify({
|
|
411
|
+
config: absolutePath,
|
|
412
|
+
manifest: config.manifest || null,
|
|
413
|
+
stats,
|
|
414
|
+
objects: (config.objects || []).map((o) => ({
|
|
415
|
+
name: o.name,
|
|
416
|
+
label: o.label,
|
|
417
|
+
fields: o.fields ? Object.keys(o.fields).length : 0
|
|
418
|
+
})),
|
|
419
|
+
loadTime: duration
|
|
420
|
+
}, null, 2));
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (config.manifest) {
|
|
424
|
+
const m = config.manifest;
|
|
425
|
+
console.log("");
|
|
426
|
+
console.log(` ${chalk5.bold(m.name || m.id || "Unnamed")} ${chalk5.dim(`v${m.version || "0.0.0"}`)}`);
|
|
427
|
+
if (m.id) console.log(chalk5.dim(` ${m.id}`));
|
|
428
|
+
if (m.description) console.log(chalk5.dim(` ${m.description}`));
|
|
429
|
+
if (m.namespace) printKV(" Namespace", m.namespace);
|
|
430
|
+
if (m.type) printKV(" Type", m.type);
|
|
431
|
+
}
|
|
432
|
+
console.log("");
|
|
433
|
+
printMetadataStats(stats);
|
|
434
|
+
if (config.objects && config.objects.length > 0) {
|
|
435
|
+
console.log("");
|
|
436
|
+
console.log(chalk5.bold(" Objects:"));
|
|
437
|
+
for (const obj of config.objects) {
|
|
438
|
+
const fieldCount = obj.fields ? Object.keys(obj.fields).length : 0;
|
|
439
|
+
const ownership = obj.ownership || "own";
|
|
440
|
+
console.log(
|
|
441
|
+
` ${chalk5.cyan(obj.name || "?")}` + chalk5.dim(` (${fieldCount} fields, ${ownership})`) + (obj.label ? chalk5.dim(` \u2014 ${obj.label}`) : "")
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (config.agents && config.agents.length > 0) {
|
|
446
|
+
console.log("");
|
|
447
|
+
console.log(chalk5.bold(" Agents:"));
|
|
448
|
+
for (const agent of config.agents) {
|
|
449
|
+
console.log(
|
|
450
|
+
` ${chalk5.magenta(agent.name || "?")}` + (agent.role ? chalk5.dim(` \u2014 ${agent.role}`) : "")
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (config.apps && config.apps.length > 0) {
|
|
455
|
+
console.log("");
|
|
456
|
+
console.log(chalk5.bold(" Apps:"));
|
|
457
|
+
for (const app of config.apps) {
|
|
458
|
+
console.log(
|
|
459
|
+
` ${chalk5.green(app.name || "?")}` + (app.label ? chalk5.dim(` \u2014 ${app.label}`) : "")
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
console.log("");
|
|
464
|
+
console.log(chalk5.dim(` Loaded in ${duration}ms`));
|
|
465
|
+
console.log("");
|
|
466
|
+
} catch (error) {
|
|
467
|
+
if (options.json) {
|
|
468
|
+
console.log(JSON.stringify({ error: error.message }));
|
|
469
|
+
process.exit(1);
|
|
470
|
+
}
|
|
471
|
+
console.log("");
|
|
472
|
+
printError(error.message || String(error));
|
|
473
|
+
process.exit(1);
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// src/commands/init.ts
|
|
478
|
+
import { Command as Command4 } from "commander";
|
|
479
|
+
import chalk6 from "chalk";
|
|
480
|
+
import fs3 from "fs";
|
|
481
|
+
import path3 from "path";
|
|
482
|
+
var TEMPLATES = {
|
|
483
|
+
app: {
|
|
484
|
+
description: "Full application with objects, views, and actions",
|
|
485
|
+
dependencies: {
|
|
486
|
+
"@objectstack/spec": "workspace:*",
|
|
487
|
+
"@objectstack/runtime": "workspace:^",
|
|
488
|
+
"@objectstack/objectql": "workspace:^",
|
|
489
|
+
"@objectstack/driver-memory": "workspace:^"
|
|
490
|
+
},
|
|
491
|
+
devDependencies: {
|
|
492
|
+
"@objectstack/cli": "workspace:*",
|
|
493
|
+
"typescript": "^5.3.0"
|
|
494
|
+
},
|
|
495
|
+
scripts: {
|
|
496
|
+
dev: "objectstack dev",
|
|
497
|
+
start: "objectstack serve",
|
|
498
|
+
build: "objectstack compile",
|
|
499
|
+
validate: "objectstack validate",
|
|
500
|
+
typecheck: "tsc --noEmit"
|
|
501
|
+
},
|
|
502
|
+
configContent: (name) => `import { defineStack } from '@objectstack/spec';
|
|
503
|
+
import * as objects from './src/objects';
|
|
504
|
+
|
|
505
|
+
export default defineStack({
|
|
506
|
+
manifest: {
|
|
507
|
+
id: 'com.example.${name}',
|
|
508
|
+
namespace: '${name}',
|
|
509
|
+
version: '0.1.0',
|
|
510
|
+
type: 'app',
|
|
511
|
+
name: '${toTitleCase(name)}',
|
|
512
|
+
description: '${toTitleCase(name)} application built with ObjectStack',
|
|
513
|
+
},
|
|
514
|
+
|
|
515
|
+
objects: Object.values(objects),
|
|
516
|
+
});
|
|
517
|
+
`,
|
|
518
|
+
srcFiles: {
|
|
519
|
+
"src/objects/index.ts": (name) => `export { default as ${toCamelCase(name)} } from './${name}';
|
|
520
|
+
`,
|
|
521
|
+
"src/objects/__name__.ts": (name) => `import { Data } from '@objectstack/spec';
|
|
522
|
+
|
|
523
|
+
const ${toCamelCase(name)}: Data.Object = {
|
|
524
|
+
name: '${name}',
|
|
525
|
+
label: '${toTitleCase(name)}',
|
|
526
|
+
ownership: 'own',
|
|
527
|
+
fields: {
|
|
528
|
+
name: {
|
|
529
|
+
type: 'text',
|
|
530
|
+
label: 'Name',
|
|
531
|
+
required: true,
|
|
532
|
+
},
|
|
533
|
+
description: {
|
|
534
|
+
type: 'textarea',
|
|
535
|
+
label: 'Description',
|
|
536
|
+
},
|
|
537
|
+
status: {
|
|
538
|
+
type: 'select',
|
|
539
|
+
label: 'Status',
|
|
540
|
+
options: [
|
|
541
|
+
{ label: 'Draft', value: 'draft' },
|
|
542
|
+
{ label: 'Active', value: 'active' },
|
|
543
|
+
{ label: 'Archived', value: 'archived' },
|
|
544
|
+
],
|
|
545
|
+
defaultValue: 'draft',
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
export default ${toCamelCase(name)};
|
|
551
|
+
`
|
|
552
|
+
}
|
|
553
|
+
},
|
|
554
|
+
plugin: {
|
|
555
|
+
description: "Reusable plugin with objects and extensions",
|
|
556
|
+
dependencies: {
|
|
557
|
+
"@objectstack/spec": "workspace:*"
|
|
558
|
+
},
|
|
559
|
+
devDependencies: {
|
|
560
|
+
"typescript": "^5.3.0",
|
|
561
|
+
"vitest": "^4.0.18"
|
|
562
|
+
},
|
|
563
|
+
scripts: {
|
|
564
|
+
build: "objectstack compile",
|
|
565
|
+
validate: "objectstack validate",
|
|
566
|
+
test: "vitest run",
|
|
567
|
+
typecheck: "tsc --noEmit"
|
|
568
|
+
},
|
|
569
|
+
configContent: (name) => `import { defineStack } from '@objectstack/spec';
|
|
570
|
+
import * as objects from './src/objects';
|
|
571
|
+
|
|
572
|
+
export default defineStack({
|
|
573
|
+
manifest: {
|
|
574
|
+
id: 'com.objectstack.plugin-${name}',
|
|
575
|
+
namespace: 'plugin_${name}',
|
|
576
|
+
version: '0.1.0',
|
|
577
|
+
type: 'plugin',
|
|
578
|
+
name: '${toTitleCase(name)} Plugin',
|
|
579
|
+
description: 'ObjectStack Plugin: ${toTitleCase(name)}',
|
|
580
|
+
},
|
|
581
|
+
|
|
582
|
+
objects: Object.values(objects),
|
|
583
|
+
});
|
|
584
|
+
`,
|
|
585
|
+
srcFiles: {
|
|
586
|
+
"src/objects/index.ts": (name) => `export { default as ${toCamelCase(name)} } from './${name}';
|
|
587
|
+
`,
|
|
588
|
+
"src/objects/__name__.ts": (name) => `import { Data } from '@objectstack/spec';
|
|
589
|
+
|
|
590
|
+
const ${toCamelCase(name)}: Data.Object = {
|
|
591
|
+
name: '${name}',
|
|
592
|
+
label: '${toTitleCase(name)}',
|
|
593
|
+
ownership: 'own',
|
|
594
|
+
fields: {
|
|
595
|
+
name: {
|
|
596
|
+
type: 'text',
|
|
597
|
+
label: 'Name',
|
|
598
|
+
required: true,
|
|
599
|
+
},
|
|
600
|
+
},
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
export default ${toCamelCase(name)};
|
|
604
|
+
`
|
|
605
|
+
}
|
|
606
|
+
},
|
|
607
|
+
empty: {
|
|
608
|
+
description: "Minimal project with just a config file",
|
|
609
|
+
dependencies: {
|
|
610
|
+
"@objectstack/spec": "workspace:*"
|
|
611
|
+
},
|
|
612
|
+
devDependencies: {
|
|
613
|
+
"@objectstack/cli": "workspace:*",
|
|
614
|
+
"typescript": "^5.3.0"
|
|
615
|
+
},
|
|
616
|
+
scripts: {
|
|
617
|
+
build: "objectstack compile",
|
|
618
|
+
validate: "objectstack validate",
|
|
619
|
+
typecheck: "tsc --noEmit"
|
|
620
|
+
},
|
|
621
|
+
configContent: (name) => `import { defineStack } from '@objectstack/spec';
|
|
622
|
+
|
|
623
|
+
export default defineStack({
|
|
624
|
+
manifest: {
|
|
625
|
+
id: 'com.example.${name}',
|
|
626
|
+
namespace: '${name}',
|
|
627
|
+
version: '0.1.0',
|
|
628
|
+
type: 'app',
|
|
629
|
+
name: '${toTitleCase(name)}',
|
|
630
|
+
description: '',
|
|
631
|
+
},
|
|
632
|
+
});
|
|
633
|
+
`,
|
|
634
|
+
srcFiles: {}
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
function toCamelCase(str) {
|
|
638
|
+
return str.replace(/[-_]([a-z])/g, (_, c) => c.toUpperCase());
|
|
639
|
+
}
|
|
640
|
+
function toTitleCase(str) {
|
|
641
|
+
return str.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
642
|
+
}
|
|
643
|
+
var initCommand = new Command4("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) => {
|
|
644
|
+
printHeader("Init");
|
|
645
|
+
const cwd = process.cwd();
|
|
646
|
+
const projectName = name || path3.basename(cwd);
|
|
647
|
+
const template = TEMPLATES[options.template];
|
|
648
|
+
if (!template) {
|
|
649
|
+
printError(`Unknown template: ${options.template}`);
|
|
650
|
+
console.log(chalk6.dim(` Available: ${Object.keys(TEMPLATES).join(", ")}`));
|
|
651
|
+
process.exit(1);
|
|
652
|
+
}
|
|
653
|
+
if (fs3.existsSync(path3.join(cwd, "objectstack.config.ts"))) {
|
|
654
|
+
printError("objectstack.config.ts already exists in this directory");
|
|
655
|
+
console.log(chalk6.dim(" Use `objectstack generate` to add metadata to an existing project"));
|
|
656
|
+
process.exit(1);
|
|
657
|
+
}
|
|
658
|
+
printKV("Project", projectName);
|
|
659
|
+
printKV("Template", `${options.template} \u2014 ${template.description}`);
|
|
660
|
+
printKV("Directory", cwd);
|
|
661
|
+
console.log("");
|
|
662
|
+
const createdFiles = [];
|
|
663
|
+
try {
|
|
664
|
+
const pkgPath = path3.join(cwd, "package.json");
|
|
665
|
+
if (!fs3.existsSync(pkgPath)) {
|
|
666
|
+
const pkg = {
|
|
667
|
+
name: projectName,
|
|
668
|
+
version: "0.1.0",
|
|
669
|
+
private: true,
|
|
670
|
+
type: "module",
|
|
671
|
+
scripts: template.scripts,
|
|
672
|
+
dependencies: template.dependencies,
|
|
673
|
+
devDependencies: template.devDependencies
|
|
674
|
+
};
|
|
675
|
+
fs3.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
676
|
+
createdFiles.push("package.json");
|
|
677
|
+
} else {
|
|
678
|
+
printInfo("package.json already exists, skipping");
|
|
679
|
+
}
|
|
680
|
+
const configContent = template.configContent(projectName);
|
|
681
|
+
fs3.writeFileSync(path3.join(cwd, "objectstack.config.ts"), configContent);
|
|
682
|
+
createdFiles.push("objectstack.config.ts");
|
|
683
|
+
const tsconfigPath = path3.join(cwd, "tsconfig.json");
|
|
684
|
+
if (!fs3.existsSync(tsconfigPath)) {
|
|
685
|
+
const tsconfig = {
|
|
686
|
+
compilerOptions: {
|
|
687
|
+
target: "ES2022",
|
|
688
|
+
module: "ESNext",
|
|
689
|
+
moduleResolution: "bundler",
|
|
690
|
+
strict: true,
|
|
691
|
+
esModuleInterop: true,
|
|
692
|
+
skipLibCheck: true,
|
|
693
|
+
outDir: "dist",
|
|
694
|
+
rootDir: ".",
|
|
695
|
+
declaration: true
|
|
696
|
+
},
|
|
697
|
+
include: ["*.ts", "src/**/*"],
|
|
698
|
+
exclude: ["dist", "node_modules"]
|
|
699
|
+
};
|
|
700
|
+
fs3.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n");
|
|
701
|
+
createdFiles.push("tsconfig.json");
|
|
702
|
+
}
|
|
703
|
+
for (const [filePath, contentFn] of Object.entries(template.srcFiles)) {
|
|
704
|
+
const resolvedPath = filePath.replace("__name__", projectName);
|
|
705
|
+
const fullPath = path3.join(cwd, resolvedPath);
|
|
706
|
+
const dir = path3.dirname(fullPath);
|
|
707
|
+
if (!fs3.existsSync(dir)) {
|
|
708
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
709
|
+
}
|
|
710
|
+
fs3.writeFileSync(fullPath, contentFn(projectName));
|
|
711
|
+
createdFiles.push(resolvedPath);
|
|
712
|
+
}
|
|
713
|
+
const gitignorePath = path3.join(cwd, ".gitignore");
|
|
714
|
+
if (!fs3.existsSync(gitignorePath)) {
|
|
715
|
+
fs3.writeFileSync(gitignorePath, `node_modules/
|
|
716
|
+
dist/
|
|
717
|
+
*.tsbuildinfo
|
|
718
|
+
`);
|
|
719
|
+
createdFiles.push(".gitignore");
|
|
720
|
+
}
|
|
721
|
+
console.log(chalk6.bold(" Created files:"));
|
|
722
|
+
for (const f of createdFiles) {
|
|
723
|
+
console.log(chalk6.green(` + ${f}`));
|
|
724
|
+
}
|
|
725
|
+
console.log("");
|
|
726
|
+
if (options.install !== false) {
|
|
727
|
+
printStep("Installing dependencies...");
|
|
728
|
+
const { execSync: execSync3 } = await import("child_process");
|
|
729
|
+
try {
|
|
730
|
+
execSync3("pnpm install", { stdio: "inherit", cwd });
|
|
731
|
+
} catch {
|
|
732
|
+
printWarning2("Dependency installation failed. Run `pnpm install` manually.");
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
printSuccess("Project initialized!");
|
|
736
|
+
console.log("");
|
|
737
|
+
console.log(chalk6.bold(" Next steps:"));
|
|
738
|
+
console.log(chalk6.dim(" objectstack validate # Check configuration"));
|
|
739
|
+
console.log(chalk6.dim(" objectstack dev # Start development server"));
|
|
740
|
+
console.log(chalk6.dim(" objectstack generate # Add objects, views, etc."));
|
|
741
|
+
console.log("");
|
|
742
|
+
} catch (error) {
|
|
743
|
+
printError(error.message || String(error));
|
|
744
|
+
process.exit(1);
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
function printWarning2(msg) {
|
|
748
|
+
console.log(chalk6.yellow(` \u26A0 ${msg}`));
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// src/commands/generate.ts
|
|
752
|
+
import { Command as Command5 } from "commander";
|
|
753
|
+
import chalk7 from "chalk";
|
|
754
|
+
import fs4 from "fs";
|
|
755
|
+
import path4 from "path";
|
|
756
|
+
var GENERATORS = {
|
|
757
|
+
object: {
|
|
758
|
+
description: "Business data object",
|
|
759
|
+
defaultDir: "src/objects",
|
|
760
|
+
generate: (name) => `import { Data } from '@objectstack/spec';
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* ${toTitleCase2(name)} Object
|
|
764
|
+
*/
|
|
765
|
+
const ${toCamelCase2(name)}: Data.Object = {
|
|
766
|
+
name: '${toSnakeCase(name)}',
|
|
767
|
+
label: '${toTitleCase2(name)}',
|
|
768
|
+
pluralLabel: '${toTitleCase2(name)}s',
|
|
769
|
+
ownership: 'own',
|
|
770
|
+
fields: {
|
|
771
|
+
name: {
|
|
772
|
+
type: 'text',
|
|
773
|
+
label: 'Name',
|
|
774
|
+
required: true,
|
|
775
|
+
maxLength: 255,
|
|
776
|
+
},
|
|
777
|
+
description: {
|
|
778
|
+
type: 'textarea',
|
|
779
|
+
label: 'Description',
|
|
780
|
+
},
|
|
781
|
+
},
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
export default ${toCamelCase2(name)};
|
|
785
|
+
`
|
|
786
|
+
},
|
|
787
|
+
view: {
|
|
788
|
+
description: "List or form view",
|
|
789
|
+
defaultDir: "src/views",
|
|
790
|
+
generate: (name) => `import { UI } from '@objectstack/spec';
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* ${toTitleCase2(name)} List View
|
|
794
|
+
*/
|
|
795
|
+
const ${toCamelCase2(name)}ListView: UI.View = {
|
|
796
|
+
name: '${toSnakeCase(name)}_list',
|
|
797
|
+
label: '${toTitleCase2(name)} List',
|
|
798
|
+
type: 'list',
|
|
799
|
+
objectName: '${toSnakeCase(name)}',
|
|
800
|
+
list: {
|
|
801
|
+
type: 'grid',
|
|
802
|
+
columns: [
|
|
803
|
+
{ field: 'name', width: 200 },
|
|
804
|
+
],
|
|
805
|
+
defaultSort: { field: 'name', direction: 'asc' },
|
|
806
|
+
pageSize: 25,
|
|
807
|
+
},
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
export default ${toCamelCase2(name)}ListView;
|
|
811
|
+
`
|
|
812
|
+
},
|
|
813
|
+
action: {
|
|
814
|
+
description: "Button or batch action",
|
|
815
|
+
defaultDir: "src/actions",
|
|
816
|
+
generate: (name) => `import { UI } from '@objectstack/spec';
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* ${toTitleCase2(name)} Action
|
|
820
|
+
*/
|
|
821
|
+
const ${toCamelCase2(name)}Action: UI.Action = {
|
|
822
|
+
name: '${toSnakeCase(name)}',
|
|
823
|
+
label: '${toTitleCase2(name)}',
|
|
824
|
+
type: 'custom',
|
|
825
|
+
objectName: '${toSnakeCase(name)}',
|
|
826
|
+
handler: {
|
|
827
|
+
type: 'flow',
|
|
828
|
+
target: '${toSnakeCase(name)}_flow',
|
|
829
|
+
},
|
|
830
|
+
};
|
|
831
|
+
|
|
832
|
+
export default ${toCamelCase2(name)}Action;
|
|
833
|
+
`
|
|
834
|
+
},
|
|
835
|
+
flow: {
|
|
836
|
+
description: "Automation flow",
|
|
837
|
+
defaultDir: "src/flows",
|
|
838
|
+
generate: (name) => `import { Automation } from '@objectstack/spec';
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* ${toTitleCase2(name)} Flow
|
|
842
|
+
*/
|
|
843
|
+
const ${toCamelCase2(name)}Flow: Automation.Flow = {
|
|
844
|
+
name: '${toSnakeCase(name)}_flow',
|
|
845
|
+
label: '${toTitleCase2(name)} Flow',
|
|
846
|
+
type: 'autolaunched',
|
|
847
|
+
status: 'draft',
|
|
848
|
+
trigger: {
|
|
849
|
+
type: 'record_change',
|
|
850
|
+
object: '${toSnakeCase(name)}',
|
|
851
|
+
events: ['after_insert', 'after_update'],
|
|
852
|
+
},
|
|
853
|
+
nodes: [
|
|
854
|
+
{
|
|
855
|
+
id: 'start',
|
|
856
|
+
type: 'start',
|
|
857
|
+
name: 'Start',
|
|
858
|
+
next: 'end',
|
|
859
|
+
},
|
|
860
|
+
],
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
export default ${toCamelCase2(name)}Flow;
|
|
864
|
+
`
|
|
865
|
+
},
|
|
866
|
+
agent: {
|
|
867
|
+
description: "AI agent",
|
|
868
|
+
defaultDir: "src/agents",
|
|
869
|
+
generate: (name) => `import { AI } from '@objectstack/spec';
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* ${toTitleCase2(name)} Agent
|
|
873
|
+
*/
|
|
874
|
+
const ${toCamelCase2(name)}Agent: AI.Agent = {
|
|
875
|
+
name: '${toSnakeCase(name)}_agent',
|
|
876
|
+
label: '${toTitleCase2(name)} Agent',
|
|
877
|
+
role: '${toTitleCase2(name)} assistant',
|
|
878
|
+
instructions: 'You are a helpful ${toTitleCase2(name).toLowerCase()} assistant.',
|
|
879
|
+
model: {
|
|
880
|
+
provider: 'openai',
|
|
881
|
+
model: 'gpt-4o',
|
|
882
|
+
},
|
|
883
|
+
tools: [],
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
export default ${toCamelCase2(name)}Agent;
|
|
887
|
+
`
|
|
888
|
+
},
|
|
889
|
+
dashboard: {
|
|
890
|
+
description: "Analytics dashboard",
|
|
891
|
+
defaultDir: "src/dashboards",
|
|
892
|
+
generate: (name) => `import { UI } from '@objectstack/spec';
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* ${toTitleCase2(name)} Dashboard
|
|
896
|
+
*/
|
|
897
|
+
const ${toCamelCase2(name)}Dashboard: UI.Dashboard = {
|
|
898
|
+
name: '${toSnakeCase(name)}_dashboard',
|
|
899
|
+
label: '${toTitleCase2(name)} Dashboard',
|
|
900
|
+
widgets: [],
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
export default ${toCamelCase2(name)}Dashboard;
|
|
904
|
+
`
|
|
905
|
+
},
|
|
906
|
+
app: {
|
|
907
|
+
description: "Application navigation",
|
|
908
|
+
defaultDir: "src/apps",
|
|
909
|
+
generate: (name) => `import { UI } from '@objectstack/spec';
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* ${toTitleCase2(name)} App
|
|
913
|
+
*/
|
|
914
|
+
const ${toCamelCase2(name)}App: UI.App = {
|
|
915
|
+
name: '${toSnakeCase(name)}_app',
|
|
916
|
+
label: '${toTitleCase2(name)}',
|
|
917
|
+
navigation: {
|
|
918
|
+
type: 'sidebar',
|
|
919
|
+
items: [],
|
|
920
|
+
},
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
export default ${toCamelCase2(name)}App;
|
|
924
|
+
`
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
function toCamelCase2(str) {
|
|
928
|
+
return str.replace(/[-_]([a-z])/g, (_, c) => c.toUpperCase());
|
|
929
|
+
}
|
|
930
|
+
function toTitleCase2(str) {
|
|
931
|
+
return str.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
932
|
+
}
|
|
933
|
+
function toSnakeCase(str) {
|
|
934
|
+
return str.replace(/[-]/g, "_").replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`).replace(/^_/, "");
|
|
935
|
+
}
|
|
936
|
+
var generateCommand = new Command5("generate").alias("g").description("Generate metadata files (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) => {
|
|
937
|
+
printHeader("Generate");
|
|
938
|
+
const generator = GENERATORS[type];
|
|
939
|
+
if (!generator) {
|
|
940
|
+
printError(`Unknown type: ${type}`);
|
|
941
|
+
console.log("");
|
|
942
|
+
console.log(chalk7.bold(" Available types:"));
|
|
943
|
+
for (const [key, gen] of Object.entries(GENERATORS)) {
|
|
944
|
+
console.log(` ${chalk7.cyan(key.padEnd(12))} ${chalk7.dim(gen.description)}`);
|
|
945
|
+
}
|
|
946
|
+
console.log("");
|
|
947
|
+
console.log(chalk7.dim(" Usage: objectstack generate <type> <name>"));
|
|
948
|
+
console.log(chalk7.dim(" Example: objectstack generate object project"));
|
|
949
|
+
console.log(chalk7.dim(" Alias: os g object project"));
|
|
950
|
+
process.exit(1);
|
|
951
|
+
}
|
|
952
|
+
const dir = options.dir || generator.defaultDir;
|
|
953
|
+
const fileName = `${toSnakeCase(name)}.ts`;
|
|
954
|
+
const filePath = path4.join(process.cwd(), dir, fileName);
|
|
955
|
+
console.log(` ${chalk7.dim("Type:")} ${chalk7.cyan(type)} \u2014 ${generator.description}`);
|
|
956
|
+
console.log(` ${chalk7.dim("Name:")} ${chalk7.white(name)}`);
|
|
957
|
+
console.log(` ${chalk7.dim("File:")} ${chalk7.white(path4.join(dir, fileName))}`);
|
|
958
|
+
console.log("");
|
|
959
|
+
if (options.dryRun) {
|
|
960
|
+
printInfo("Dry run \u2014 no files written");
|
|
961
|
+
console.log("");
|
|
962
|
+
console.log(chalk7.dim(" Content:"));
|
|
963
|
+
console.log(chalk7.dim(" " + "-".repeat(38)));
|
|
964
|
+
const content = generator.generate(name);
|
|
965
|
+
for (const line of content.split("\n")) {
|
|
966
|
+
console.log(chalk7.dim(` ${line}`));
|
|
967
|
+
}
|
|
968
|
+
console.log("");
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
if (fs4.existsSync(filePath)) {
|
|
972
|
+
printError(`File already exists: ${filePath}`);
|
|
973
|
+
process.exit(1);
|
|
974
|
+
}
|
|
975
|
+
try {
|
|
976
|
+
const fullDir = path4.dirname(filePath);
|
|
977
|
+
if (!fs4.existsSync(fullDir)) {
|
|
978
|
+
fs4.mkdirSync(fullDir, { recursive: true });
|
|
979
|
+
}
|
|
980
|
+
const content = generator.generate(name);
|
|
981
|
+
fs4.writeFileSync(filePath, content);
|
|
982
|
+
printSuccess(`Created ${path4.join(dir, fileName)}`);
|
|
983
|
+
const indexPath = path4.join(process.cwd(), dir, "index.ts");
|
|
984
|
+
if (fs4.existsSync(indexPath)) {
|
|
985
|
+
const indexContent = fs4.readFileSync(indexPath, "utf-8");
|
|
986
|
+
const exportLine = `export { default as ${toCamelCase2(name)} } from './${toSnakeCase(name)}';`;
|
|
987
|
+
if (!indexContent.includes(toCamelCase2(name))) {
|
|
988
|
+
fs4.appendFileSync(indexPath, exportLine + "\n");
|
|
989
|
+
printSuccess(`Updated ${dir}/index.ts with export`);
|
|
990
|
+
}
|
|
991
|
+
} else {
|
|
992
|
+
const exportLine = `export { default as ${toCamelCase2(name)} } from './${toSnakeCase(name)}';
|
|
993
|
+
`;
|
|
994
|
+
fs4.writeFileSync(indexPath, exportLine);
|
|
995
|
+
printSuccess(`Created ${dir}/index.ts`);
|
|
996
|
+
}
|
|
997
|
+
console.log("");
|
|
998
|
+
console.log(chalk7.dim(` Tip: Run \`objectstack validate\` to check your config`));
|
|
999
|
+
console.log("");
|
|
1000
|
+
} catch (error) {
|
|
1001
|
+
printError(error.message || String(error));
|
|
1002
|
+
process.exit(1);
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
// src/commands/create.ts
|
|
1007
|
+
import { Command as Command6 } from "commander";
|
|
1008
|
+
import chalk8 from "chalk";
|
|
1009
|
+
import fs5 from "fs";
|
|
1010
|
+
import path5 from "path";
|
|
1011
|
+
var templates = {
|
|
1012
|
+
plugin: {
|
|
1013
|
+
description: "Create a new ObjectStack plugin",
|
|
1014
|
+
files: {
|
|
1015
|
+
"package.json": (name) => ({
|
|
1016
|
+
name: `@objectstack/plugin-${name}`,
|
|
1017
|
+
version: "0.1.0",
|
|
1018
|
+
description: `ObjectStack Plugin: ${name}`,
|
|
1019
|
+
main: "dist/index.js",
|
|
1020
|
+
types: "dist/index.d.ts",
|
|
1021
|
+
scripts: {
|
|
1022
|
+
build: "tsc",
|
|
1023
|
+
dev: "tsc --watch",
|
|
1024
|
+
test: "vitest"
|
|
1025
|
+
},
|
|
1026
|
+
keywords: ["objectstack", "plugin", name],
|
|
1027
|
+
author: "",
|
|
1028
|
+
license: "MIT",
|
|
1029
|
+
dependencies: {
|
|
1030
|
+
"@objectstack/spec": "workspace:*",
|
|
1031
|
+
zod: "^4.3.6"
|
|
1032
|
+
},
|
|
1033
|
+
devDependencies: {
|
|
1034
|
+
"@types/node": "^22.0.0",
|
|
1035
|
+
typescript: "^5.8.0",
|
|
1036
|
+
vitest: "^4.0.0"
|
|
1037
|
+
}
|
|
1038
|
+
}),
|
|
1039
|
+
"tsconfig.json": () => ({
|
|
1040
|
+
extends: "../../tsconfig.json",
|
|
1041
|
+
compilerOptions: {
|
|
1042
|
+
outDir: "dist",
|
|
1043
|
+
rootDir: "src"
|
|
1044
|
+
},
|
|
1045
|
+
include: ["src/**/*"]
|
|
1046
|
+
}),
|
|
1047
|
+
"src/index.ts": (name) => `import type { Plugin } from '@objectstack/spec';
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* ${name} Plugin for ObjectStack
|
|
1051
|
+
*/
|
|
1052
|
+
export const ${toCamelCase3(name)}Plugin: Plugin = {
|
|
1053
|
+
name: '${name}',
|
|
1054
|
+
version: '0.1.0',
|
|
1055
|
+
|
|
1056
|
+
async initialize(context) {
|
|
1057
|
+
console.log('Initializing ${name} plugin...');
|
|
1058
|
+
// Plugin initialization logic
|
|
1059
|
+
},
|
|
1060
|
+
|
|
1061
|
+
async destroy() {
|
|
1062
|
+
console.log('Destroying ${name} plugin...');
|
|
1063
|
+
// Plugin cleanup logic
|
|
1064
|
+
},
|
|
1065
|
+
};
|
|
1066
|
+
|
|
1067
|
+
export default ${toCamelCase3(name)}Plugin;
|
|
1068
|
+
`,
|
|
1069
|
+
"README.md": (name) => `# @objectstack/plugin-${name}
|
|
1070
|
+
|
|
1071
|
+
ObjectStack Plugin: ${name}
|
|
1072
|
+
|
|
1073
|
+
## Installation
|
|
1074
|
+
|
|
1075
|
+
\`\`\`bash
|
|
1076
|
+
pnpm add @objectstack/plugin-${name}
|
|
1077
|
+
\`\`\`
|
|
1078
|
+
|
|
1079
|
+
## Usage
|
|
1080
|
+
|
|
1081
|
+
\`\`\`typescript
|
|
1082
|
+
import { ${toCamelCase3(name)}Plugin } from '@objectstack/plugin-${name}';
|
|
1083
|
+
|
|
1084
|
+
// Use the plugin in your ObjectStack configuration
|
|
1085
|
+
export default {
|
|
1086
|
+
plugins: [
|
|
1087
|
+
${toCamelCase3(name)}Plugin,
|
|
1088
|
+
],
|
|
1089
|
+
};
|
|
1090
|
+
\`\`\`
|
|
1091
|
+
|
|
1092
|
+
## License
|
|
1093
|
+
|
|
1094
|
+
MIT
|
|
1095
|
+
`
|
|
1096
|
+
}
|
|
1097
|
+
},
|
|
1098
|
+
example: {
|
|
1099
|
+
description: "Create a new ObjectStack example application",
|
|
1100
|
+
files: {
|
|
1101
|
+
"package.json": (name) => ({
|
|
1102
|
+
name: `@example/${name}`,
|
|
1103
|
+
version: "0.1.0",
|
|
1104
|
+
private: true,
|
|
1105
|
+
description: `ObjectStack Example: ${name}`,
|
|
1106
|
+
scripts: {
|
|
1107
|
+
build: "objectstack compile",
|
|
1108
|
+
dev: "objectstack dev",
|
|
1109
|
+
test: "vitest"
|
|
1110
|
+
},
|
|
1111
|
+
dependencies: {
|
|
1112
|
+
"@objectstack/spec": "workspace:*",
|
|
1113
|
+
"@objectstack/cli": "workspace:*",
|
|
1114
|
+
zod: "^4.3.6"
|
|
1115
|
+
},
|
|
1116
|
+
devDependencies: {
|
|
1117
|
+
"@types/node": "^22.0.0",
|
|
1118
|
+
tsx: "^4.21.0",
|
|
1119
|
+
typescript: "^5.8.0",
|
|
1120
|
+
vitest: "^4.0.0"
|
|
1121
|
+
}
|
|
1122
|
+
}),
|
|
1123
|
+
"objectstack.config.ts": (name) => `import { defineStack } from '@objectstack/spec';
|
|
1124
|
+
|
|
1125
|
+
// Barrel imports \u2014 add more as you create new type folders
|
|
1126
|
+
// import * as objects from './src/objects';
|
|
1127
|
+
// import * as actions from './src/actions';
|
|
1128
|
+
// import * as apps from './src/apps';
|
|
1129
|
+
|
|
1130
|
+
export default defineStack({
|
|
1131
|
+
manifest: {
|
|
1132
|
+
name: '${name}',
|
|
1133
|
+
version: '0.1.0',
|
|
1134
|
+
description: '${name} example application',
|
|
1135
|
+
},
|
|
1136
|
+
|
|
1137
|
+
objects: [
|
|
1138
|
+
// Object.values(objects), // Uncomment after creating src/objects/index.ts
|
|
1139
|
+
],
|
|
1140
|
+
|
|
1141
|
+
apps: [
|
|
1142
|
+
// Object.values(apps), // Uncomment after creating src/apps/index.ts
|
|
1143
|
+
],
|
|
1144
|
+
});
|
|
1145
|
+
`,
|
|
1146
|
+
"README.md": (name) => `# ${name} Example
|
|
1147
|
+
|
|
1148
|
+
ObjectStack example application: ${name}
|
|
1149
|
+
|
|
1150
|
+
## Quick Start
|
|
1151
|
+
|
|
1152
|
+
\`\`\`bash
|
|
1153
|
+
# Build the configuration
|
|
1154
|
+
pnpm build
|
|
1155
|
+
|
|
1156
|
+
# Run in development mode
|
|
1157
|
+
pnpm dev
|
|
1158
|
+
\`\`\`
|
|
1159
|
+
|
|
1160
|
+
## Structure
|
|
1161
|
+
|
|
1162
|
+
- \`objectstack.config.ts\` - Main configuration file
|
|
1163
|
+
- \`dist/objectstack.json\` - Compiled artifact
|
|
1164
|
+
|
|
1165
|
+
## Learn More
|
|
1166
|
+
|
|
1167
|
+
- [ObjectStack Documentation](../../content/docs)
|
|
1168
|
+
- [Examples](../)
|
|
1169
|
+
`,
|
|
1170
|
+
"tsconfig.json": () => ({
|
|
1171
|
+
extends: "../../tsconfig.json",
|
|
1172
|
+
compilerOptions: {
|
|
1173
|
+
outDir: "dist",
|
|
1174
|
+
rootDir: "."
|
|
1175
|
+
},
|
|
1176
|
+
include: ["*.ts", "src/**/*"]
|
|
1177
|
+
})
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
};
|
|
1181
|
+
function toCamelCase3(str) {
|
|
1182
|
+
return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
|
1183
|
+
}
|
|
1184
|
+
var createCommand = new Command6("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) => {
|
|
1185
|
+
console.log(chalk8.bold(`
|
|
1186
|
+
\u{1F4E6} ObjectStack Project Creator`));
|
|
1187
|
+
console.log(chalk8.dim(`-------------------------------`));
|
|
1188
|
+
if (!templates[type]) {
|
|
1189
|
+
console.error(chalk8.red(`
|
|
1190
|
+
\u274C Unknown type: ${type}`));
|
|
1191
|
+
console.log(chalk8.dim("Available types: plugin, example"));
|
|
1192
|
+
process.exit(1);
|
|
1193
|
+
}
|
|
1194
|
+
if (!name) {
|
|
1195
|
+
console.error(chalk8.red("\n\u274C Project name is required"));
|
|
1196
|
+
console.log(chalk8.dim(`Usage: objectstack create ${type} <name>`));
|
|
1197
|
+
process.exit(1);
|
|
1198
|
+
}
|
|
1199
|
+
const template = templates[type];
|
|
1200
|
+
const cwd = process.cwd();
|
|
1201
|
+
let targetDir;
|
|
1202
|
+
if (options?.dir) {
|
|
1203
|
+
targetDir = path5.resolve(cwd, options.dir);
|
|
1204
|
+
} else {
|
|
1205
|
+
const baseDir = type === "plugin" ? "packages/plugins" : "examples";
|
|
1206
|
+
const projectName = type === "plugin" ? `plugin-${name}` : name;
|
|
1207
|
+
targetDir = path5.join(cwd, baseDir, projectName);
|
|
1208
|
+
}
|
|
1209
|
+
if (fs5.existsSync(targetDir)) {
|
|
1210
|
+
console.error(chalk8.red(`
|
|
1211
|
+
\u274C Directory already exists: ${targetDir}`));
|
|
1212
|
+
process.exit(1);
|
|
1213
|
+
}
|
|
1214
|
+
console.log(`\u{1F4C1} Creating ${type}: ${chalk8.blue(name)}`);
|
|
1215
|
+
console.log(`\u{1F4C2} Location: ${chalk8.dim(targetDir)}`);
|
|
1216
|
+
console.log("");
|
|
1217
|
+
try {
|
|
1218
|
+
fs5.mkdirSync(targetDir, { recursive: true });
|
|
1219
|
+
for (const [filePath, contentFn] of Object.entries(template.files)) {
|
|
1220
|
+
const fullPath = path5.join(targetDir, filePath);
|
|
1221
|
+
const dir = path5.dirname(fullPath);
|
|
1222
|
+
if (!fs5.existsSync(dir)) {
|
|
1223
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
1224
|
+
}
|
|
1225
|
+
const content = contentFn(name);
|
|
1226
|
+
const fileContent = typeof content === "string" ? content : JSON.stringify(content, null, 2);
|
|
1227
|
+
fs5.writeFileSync(fullPath, fileContent);
|
|
1228
|
+
console.log(chalk8.green(`\u2713 Created ${filePath}`));
|
|
1229
|
+
}
|
|
1230
|
+
console.log("");
|
|
1231
|
+
console.log(chalk8.green("\u2705 Project created successfully!"));
|
|
1232
|
+
console.log("");
|
|
1233
|
+
console.log(chalk8.bold("Next steps:"));
|
|
1234
|
+
console.log(chalk8.dim(` cd ${path5.relative(cwd, targetDir)}`));
|
|
1235
|
+
console.log(chalk8.dim(" pnpm install"));
|
|
1236
|
+
console.log(chalk8.dim(" pnpm build"));
|
|
1237
|
+
console.log("");
|
|
1238
|
+
} catch (error) {
|
|
1239
|
+
console.error(chalk8.red("\n\u274C Failed to create project:"));
|
|
1240
|
+
console.error(error.message || error);
|
|
1241
|
+
if (fs5.existsSync(targetDir)) {
|
|
1242
|
+
fs5.rmSync(targetDir, { recursive: true });
|
|
1243
|
+
}
|
|
1244
|
+
process.exit(1);
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
// src/commands/dev.ts
|
|
1249
|
+
import { Command as Command7 } from "commander";
|
|
1250
|
+
import chalk9 from "chalk";
|
|
1251
|
+
import { execSync, spawn } from "child_process";
|
|
1252
|
+
import fs6 from "fs";
|
|
1253
|
+
import path6 from "path";
|
|
1254
|
+
var devCommand = new Command7("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 Console UI at /_studio/").option("-v, --verbose", "Verbose output").action(async (packageName, options) => {
|
|
1255
|
+
printHeader("Development Mode");
|
|
1256
|
+
const configPath = path6.resolve(process.cwd(), "objectstack.config.ts");
|
|
1257
|
+
if (packageName === "all" && fs6.existsSync(configPath)) {
|
|
1258
|
+
printKV("Config", configPath, "\u{1F4C2}");
|
|
1259
|
+
printStep("Starting dev server...");
|
|
1260
|
+
const binPath = process.argv[1];
|
|
1261
|
+
const child = spawn(process.execPath, [binPath, "serve", "--dev", ...options.ui ? ["--ui"] : [], ...options.verbose ? ["--verbose"] : []], {
|
|
1262
|
+
stdio: "inherit",
|
|
1263
|
+
env: { ...process.env, NODE_ENV: "development" }
|
|
1264
|
+
});
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
try {
|
|
1268
|
+
const cwd = process.cwd();
|
|
1269
|
+
const workspaceConfigPath = path6.resolve(cwd, "pnpm-workspace.yaml");
|
|
1270
|
+
const isWorkspaceRoot = fs6.existsSync(workspaceConfigPath);
|
|
1271
|
+
if (packageName === "all" && !isWorkspaceRoot) {
|
|
1272
|
+
printError(`Config file not found in ${cwd}`);
|
|
1273
|
+
console.error(chalk9.yellow(" Run in a directory with objectstack.config.ts, or from the monorepo root."));
|
|
1274
|
+
process.exit(1);
|
|
1275
|
+
}
|
|
1276
|
+
const filter = packageName === "all" ? "" : `--filter ${packageName}`;
|
|
1277
|
+
printKV("Package", packageName === "all" ? "All packages" : packageName, "\u{1F4E6}");
|
|
1278
|
+
printKV("Watch", "enabled", "\u{1F504}");
|
|
1279
|
+
const command = `pnpm ${filter} dev`.trim();
|
|
1280
|
+
console.log(chalk9.dim(`$ ${command}`));
|
|
1281
|
+
console.log("");
|
|
1282
|
+
execSync(command, {
|
|
1283
|
+
stdio: "inherit",
|
|
1284
|
+
cwd
|
|
1285
|
+
});
|
|
1286
|
+
} catch (error) {
|
|
1287
|
+
printError(`Development mode failed: ${error.message || error}`);
|
|
1288
|
+
process.exit(1);
|
|
1289
|
+
}
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
// src/commands/serve.ts
|
|
1293
|
+
import { Command as Command8 } from "commander";
|
|
1294
|
+
import path8 from "path";
|
|
1295
|
+
import fs8 from "fs";
|
|
1296
|
+
import net2 from "net";
|
|
1297
|
+
import chalk10 from "chalk";
|
|
1298
|
+
import { bundleRequire as bundleRequire2 } from "bundle-require";
|
|
1299
|
+
|
|
1300
|
+
// src/utils/console.ts
|
|
1301
|
+
import path7 from "path";
|
|
1302
|
+
import fs7 from "fs";
|
|
1303
|
+
import net from "net";
|
|
1304
|
+
import { spawn as spawn2 } from "child_process";
|
|
1305
|
+
var STUDIO_PATH = "/_studio";
|
|
1306
|
+
var VITE_PORT_START = 24678;
|
|
1307
|
+
function resolveConsolePath() {
|
|
1308
|
+
const cwd = process.cwd();
|
|
1309
|
+
const candidates = [
|
|
1310
|
+
path7.resolve(cwd, "apps/console"),
|
|
1311
|
+
path7.resolve(cwd, "../../apps/console"),
|
|
1312
|
+
path7.resolve(cwd, "../apps/console")
|
|
1313
|
+
];
|
|
1314
|
+
for (const candidate of candidates) {
|
|
1315
|
+
const pkgPath = path7.join(candidate, "package.json");
|
|
1316
|
+
if (fs7.existsSync(pkgPath)) {
|
|
1317
|
+
try {
|
|
1318
|
+
const pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
|
|
1319
|
+
if (pkg.name === "@objectstack/console") return candidate;
|
|
1320
|
+
} catch {
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
try {
|
|
1325
|
+
const { createRequire } = __require("module");
|
|
1326
|
+
const req = createRequire(import.meta.url);
|
|
1327
|
+
const resolved = req.resolve("@objectstack/console/package.json");
|
|
1328
|
+
return path7.dirname(resolved);
|
|
1329
|
+
} catch {
|
|
1330
|
+
return null;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
function hasConsoleDist(consolePath) {
|
|
1334
|
+
return fs7.existsSync(path7.join(consolePath, "dist", "index.html"));
|
|
1335
|
+
}
|
|
1336
|
+
function findAvailablePort(start = VITE_PORT_START) {
|
|
1337
|
+
return new Promise((resolve, reject) => {
|
|
1338
|
+
const server = net.createServer();
|
|
1339
|
+
server.once("error", () => {
|
|
1340
|
+
findAvailablePort(start + 1).then(resolve, reject);
|
|
1341
|
+
});
|
|
1342
|
+
server.once("listening", () => {
|
|
1343
|
+
server.close(() => resolve(start));
|
|
1344
|
+
});
|
|
1345
|
+
server.listen(start);
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
async function spawnViteDevServer(consolePath, options = {}) {
|
|
1349
|
+
const vitePort = await findAvailablePort(VITE_PORT_START);
|
|
1350
|
+
const viteBinCandidates = [
|
|
1351
|
+
path7.join(consolePath, "node_modules", ".bin", "vite"),
|
|
1352
|
+
path7.join(consolePath, "..", "..", "node_modules", ".bin", "vite")
|
|
1353
|
+
];
|
|
1354
|
+
let viteBin = null;
|
|
1355
|
+
for (const candidate of viteBinCandidates) {
|
|
1356
|
+
if (fs7.existsSync(candidate)) {
|
|
1357
|
+
viteBin = candidate;
|
|
1358
|
+
break;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
const command = viteBin || "npx";
|
|
1362
|
+
const args = viteBin ? ["--port", String(vitePort), "--strictPort"] : ["vite", "--port", String(vitePort), "--strictPort"];
|
|
1363
|
+
const child = spawn2(command, args, {
|
|
1364
|
+
cwd: consolePath,
|
|
1365
|
+
env: {
|
|
1366
|
+
...process.env,
|
|
1367
|
+
VITE_BASE: `${STUDIO_PATH}/`,
|
|
1368
|
+
VITE_PORT: String(vitePort),
|
|
1369
|
+
VITE_HMR_PORT: String(vitePort),
|
|
1370
|
+
VITE_RUNTIME_MODE: "server",
|
|
1371
|
+
VITE_SERVER_URL: "",
|
|
1372
|
+
// Same-origin API
|
|
1373
|
+
NODE_ENV: "development"
|
|
1374
|
+
},
|
|
1375
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1376
|
+
});
|
|
1377
|
+
let stderr = "";
|
|
1378
|
+
child.stderr?.on("data", (data) => {
|
|
1379
|
+
stderr += data.toString();
|
|
1380
|
+
});
|
|
1381
|
+
await new Promise((resolve, reject) => {
|
|
1382
|
+
const timeout = setTimeout(() => {
|
|
1383
|
+
child.kill();
|
|
1384
|
+
reject(new Error(`Vite dev server timed out after 30 s.
|
|
1385
|
+
${stderr}`));
|
|
1386
|
+
}, 3e4);
|
|
1387
|
+
child.stdout?.on("data", (data) => {
|
|
1388
|
+
const output = data.toString();
|
|
1389
|
+
if (output.includes("Local:") || output.includes("ready in")) {
|
|
1390
|
+
clearTimeout(timeout);
|
|
1391
|
+
resolve();
|
|
1392
|
+
}
|
|
1393
|
+
});
|
|
1394
|
+
child.on("error", (err) => {
|
|
1395
|
+
clearTimeout(timeout);
|
|
1396
|
+
reject(err);
|
|
1397
|
+
});
|
|
1398
|
+
child.on("exit", (code) => {
|
|
1399
|
+
if (code !== 0 && code !== null) {
|
|
1400
|
+
clearTimeout(timeout);
|
|
1401
|
+
reject(new Error(`Vite exited with code ${code}.
|
|
1402
|
+
${stderr}`));
|
|
1403
|
+
}
|
|
1404
|
+
});
|
|
1405
|
+
});
|
|
1406
|
+
return { port: vitePort, process: child };
|
|
1407
|
+
}
|
|
1408
|
+
function createConsoleProxyPlugin(vitePort) {
|
|
1409
|
+
return {
|
|
1410
|
+
name: "com.objectstack.console-proxy",
|
|
1411
|
+
init: async () => {
|
|
1412
|
+
},
|
|
1413
|
+
start: async (ctx) => {
|
|
1414
|
+
const httpServer = ctx.getService?.("http.server");
|
|
1415
|
+
if (!httpServer?.getRawApp) {
|
|
1416
|
+
ctx.logger?.warn?.("Console proxy: http.server service not found \u2014 skipping");
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
const app = httpServer.getRawApp();
|
|
1420
|
+
app.get(STUDIO_PATH, (c) => c.redirect(`${STUDIO_PATH}/`));
|
|
1421
|
+
app.all(`${STUDIO_PATH}/*`, async (c) => {
|
|
1422
|
+
const targetUrl = `http://localhost:${vitePort}${c.req.path}`;
|
|
1423
|
+
try {
|
|
1424
|
+
const headers = new Headers(c.req.raw.headers);
|
|
1425
|
+
headers.delete("host");
|
|
1426
|
+
const isBodyAllowed = !["GET", "HEAD"].includes(c.req.method);
|
|
1427
|
+
const resp = await fetch(targetUrl, {
|
|
1428
|
+
method: c.req.method,
|
|
1429
|
+
headers,
|
|
1430
|
+
body: isBodyAllowed ? c.req.raw.body : void 0,
|
|
1431
|
+
// @ts-expect-error — duplex required for streaming request body
|
|
1432
|
+
duplex: isBodyAllowed ? "half" : void 0
|
|
1433
|
+
});
|
|
1434
|
+
return new Response(resp.body, {
|
|
1435
|
+
status: resp.status,
|
|
1436
|
+
headers: resp.headers
|
|
1437
|
+
});
|
|
1438
|
+
} catch {
|
|
1439
|
+
return c.text("Console dev server is starting\u2026", 502);
|
|
1440
|
+
}
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
function createConsoleStaticPlugin(distPath) {
|
|
1446
|
+
return {
|
|
1447
|
+
name: "com.objectstack.console-static",
|
|
1448
|
+
init: async () => {
|
|
1449
|
+
},
|
|
1450
|
+
start: async (ctx) => {
|
|
1451
|
+
const httpServer = ctx.getService?.("http.server");
|
|
1452
|
+
if (!httpServer?.getRawApp) {
|
|
1453
|
+
ctx.logger?.warn?.("Console static: http.server service not found \u2014 skipping");
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
const app = httpServer.getRawApp();
|
|
1457
|
+
const absoluteDist = path7.resolve(distPath);
|
|
1458
|
+
const indexPath = path7.join(absoluteDist, "index.html");
|
|
1459
|
+
if (!fs7.existsSync(indexPath)) {
|
|
1460
|
+
ctx.logger?.warn?.(`Console static: dist not found at ${absoluteDist}`);
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
app.get(STUDIO_PATH, (c) => c.redirect(`${STUDIO_PATH}/`));
|
|
1464
|
+
app.get(`${STUDIO_PATH}/*`, async (c) => {
|
|
1465
|
+
const reqPath = c.req.path.substring(STUDIO_PATH.length) || "/";
|
|
1466
|
+
const filePath = path7.join(absoluteDist, reqPath);
|
|
1467
|
+
if (!filePath.startsWith(absoluteDist)) {
|
|
1468
|
+
return c.text("Forbidden", 403);
|
|
1469
|
+
}
|
|
1470
|
+
if (fs7.existsSync(filePath) && fs7.statSync(filePath).isFile()) {
|
|
1471
|
+
const content = fs7.readFileSync(filePath);
|
|
1472
|
+
return new Response(content, {
|
|
1473
|
+
headers: { "content-type": mimeType(filePath) }
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
const html = fs7.readFileSync(indexPath);
|
|
1477
|
+
return new Response(html, {
|
|
1478
|
+
headers: { "content-type": "text/html; charset=utf-8" }
|
|
1479
|
+
});
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
var MIME_TYPES = {
|
|
1485
|
+
".html": "text/html; charset=utf-8",
|
|
1486
|
+
".js": "application/javascript; charset=utf-8",
|
|
1487
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
1488
|
+
".css": "text/css; charset=utf-8",
|
|
1489
|
+
".json": "application/json; charset=utf-8",
|
|
1490
|
+
".svg": "image/svg+xml",
|
|
1491
|
+
".png": "image/png",
|
|
1492
|
+
".jpg": "image/jpeg",
|
|
1493
|
+
".jpeg": "image/jpeg",
|
|
1494
|
+
".gif": "image/gif",
|
|
1495
|
+
".ico": "image/x-icon",
|
|
1496
|
+
".woff": "font/woff",
|
|
1497
|
+
".woff2": "font/woff2",
|
|
1498
|
+
".ttf": "font/ttf",
|
|
1499
|
+
".map": "application/json"
|
|
1500
|
+
};
|
|
1501
|
+
function mimeType(filePath) {
|
|
1502
|
+
const ext = path7.extname(filePath).toLowerCase();
|
|
1503
|
+
return MIME_TYPES[ext] || "application/octet-stream";
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// src/commands/serve.ts
|
|
1507
|
+
var getAvailablePort = async (startPort) => {
|
|
1508
|
+
const isPortAvailable = (port2) => {
|
|
1509
|
+
return new Promise((resolve) => {
|
|
1510
|
+
const server = net2.createServer();
|
|
1511
|
+
server.once("error", (err) => {
|
|
1512
|
+
resolve(false);
|
|
1513
|
+
});
|
|
1514
|
+
server.once("listening", () => {
|
|
1515
|
+
server.close(() => resolve(true));
|
|
1516
|
+
});
|
|
1517
|
+
server.listen(port2);
|
|
1518
|
+
});
|
|
1519
|
+
};
|
|
1520
|
+
let port = startPort;
|
|
1521
|
+
while (!await isPortAvailable(port)) {
|
|
1522
|
+
port++;
|
|
1523
|
+
if (port > startPort + 100) {
|
|
1524
|
+
throw new Error(`Could not find an available port starting from ${startPort}`);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
return port;
|
|
1528
|
+
};
|
|
1529
|
+
var serveCommand = new Command8("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 Console UI at /_studio/").option("--no-server", "Skip starting HTTP server plugin").action(async (configPath, options) => {
|
|
1530
|
+
let port = parseInt(options.port);
|
|
1531
|
+
try {
|
|
1532
|
+
const availablePort = await getAvailablePort(port);
|
|
1533
|
+
if (availablePort !== port) {
|
|
1534
|
+
port = availablePort;
|
|
1535
|
+
}
|
|
1536
|
+
} catch (e) {
|
|
1537
|
+
}
|
|
1538
|
+
const isDev = options.dev || process.env.NODE_ENV === "development";
|
|
1539
|
+
const absolutePath = path8.resolve(process.cwd(), configPath);
|
|
1540
|
+
const relativeConfig = path8.relative(process.cwd(), absolutePath);
|
|
1541
|
+
if (!fs8.existsSync(absolutePath)) {
|
|
1542
|
+
printError(`Configuration file not found: ${absolutePath}`);
|
|
1543
|
+
console.log(chalk10.dim(" Hint: Run `objectstack init` to create a new project"));
|
|
1544
|
+
process.exit(1);
|
|
1545
|
+
}
|
|
1546
|
+
console.log("");
|
|
1547
|
+
console.log(chalk10.dim(` Loading ${relativeConfig}...`));
|
|
1548
|
+
const loadedPlugins = [];
|
|
1549
|
+
const shortPluginName = (raw) => {
|
|
1550
|
+
if (raw.includes("objectql")) return "ObjectQL";
|
|
1551
|
+
if (raw.includes("driver") && raw.includes("memory")) return "MemoryDriver";
|
|
1552
|
+
if (raw.startsWith("plugin.app.")) return raw.replace("plugin.app.", "").split(".").pop() || raw;
|
|
1553
|
+
if (raw.includes("hono")) return "HonoServer";
|
|
1554
|
+
return raw;
|
|
1555
|
+
};
|
|
1556
|
+
const trackPlugin = (name) => {
|
|
1557
|
+
loadedPlugins.push(shortPluginName(name));
|
|
1558
|
+
};
|
|
1559
|
+
const originalConsoleLog = console.log;
|
|
1560
|
+
const originalConsoleDebug = console.debug;
|
|
1561
|
+
const origStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
1562
|
+
let bootQuiet = false;
|
|
1563
|
+
const restoreOutput = () => {
|
|
1564
|
+
bootQuiet = false;
|
|
1565
|
+
process.stdout.write = origStdoutWrite;
|
|
1566
|
+
console.log = originalConsoleLog;
|
|
1567
|
+
console.debug = originalConsoleDebug;
|
|
1568
|
+
};
|
|
1569
|
+
const portShifted = parseInt(options.port) !== port;
|
|
1570
|
+
try {
|
|
1571
|
+
bootQuiet = true;
|
|
1572
|
+
process.stdout.write = (chunk, ...rest) => {
|
|
1573
|
+
if (bootQuiet) return true;
|
|
1574
|
+
return origStdoutWrite(chunk, ...rest);
|
|
1575
|
+
};
|
|
1576
|
+
console.log = (...args) => {
|
|
1577
|
+
if (!bootQuiet) originalConsoleLog(...args);
|
|
1578
|
+
};
|
|
1579
|
+
console.debug = (...args) => {
|
|
1580
|
+
if (!bootQuiet) originalConsoleDebug(...args);
|
|
1581
|
+
};
|
|
1582
|
+
const { mod } = await bundleRequire2({
|
|
1583
|
+
filepath: absolutePath
|
|
1584
|
+
});
|
|
1585
|
+
const config = mod.default || mod;
|
|
1586
|
+
if (!config) {
|
|
1587
|
+
throw new Error(`No default export found in ${configPath}`);
|
|
1588
|
+
}
|
|
1589
|
+
const { Runtime } = await import("@objectstack/runtime");
|
|
1590
|
+
const loggerConfig = { level: "silent" };
|
|
1591
|
+
const runtime = new Runtime({
|
|
1592
|
+
kernel: {
|
|
1593
|
+
logger: loggerConfig
|
|
1594
|
+
}
|
|
1595
|
+
});
|
|
1596
|
+
const kernel = runtime.getKernel();
|
|
1597
|
+
let plugins = config.plugins || [];
|
|
1598
|
+
if (options.dev && config.devPlugins) {
|
|
1599
|
+
plugins = [...plugins, ...config.devPlugins];
|
|
1600
|
+
}
|
|
1601
|
+
const hasObjectQL = plugins.some((p) => p.name?.includes("objectql") || p.constructor?.name?.includes("ObjectQL"));
|
|
1602
|
+
if (config.objects && !hasObjectQL) {
|
|
1603
|
+
try {
|
|
1604
|
+
const { ObjectQLPlugin } = await import("@objectstack/objectql");
|
|
1605
|
+
await kernel.use(new ObjectQLPlugin());
|
|
1606
|
+
trackPlugin("ObjectQL");
|
|
1607
|
+
} catch (e) {
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
const hasDriver = plugins.some((p) => p.name?.includes("driver") || p.constructor?.name?.includes("Driver"));
|
|
1611
|
+
if (isDev && !hasDriver && config.objects) {
|
|
1612
|
+
try {
|
|
1613
|
+
const { DriverPlugin } = await import("@objectstack/runtime");
|
|
1614
|
+
const { InMemoryDriver } = await import("@objectstack/driver-memory");
|
|
1615
|
+
await kernel.use(new DriverPlugin(new InMemoryDriver()));
|
|
1616
|
+
trackPlugin("MemoryDriver");
|
|
1617
|
+
} catch (e) {
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
if (config.objects || config.manifest || config.apps) {
|
|
1621
|
+
try {
|
|
1622
|
+
const { AppPlugin } = await import("@objectstack/runtime");
|
|
1623
|
+
await kernel.use(new AppPlugin(config));
|
|
1624
|
+
trackPlugin("App");
|
|
1625
|
+
} catch (e) {
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
if (plugins.length > 0) {
|
|
1629
|
+
for (const plugin of plugins) {
|
|
1630
|
+
try {
|
|
1631
|
+
let pluginToLoad = plugin;
|
|
1632
|
+
if (typeof plugin === "string") {
|
|
1633
|
+
try {
|
|
1634
|
+
const imported = await import(plugin);
|
|
1635
|
+
pluginToLoad = imported.default || imported;
|
|
1636
|
+
} catch (importError) {
|
|
1637
|
+
throw new Error(`Failed to import plugin '${plugin}': ${importError.message}`);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
await kernel.use(pluginToLoad);
|
|
1641
|
+
const pluginName = plugin.name || plugin.constructor?.name || "unnamed";
|
|
1642
|
+
trackPlugin(pluginName);
|
|
1643
|
+
} catch (e) {
|
|
1644
|
+
console.error(chalk10.red(` \u2717 Failed to load plugin: ${e.message}`));
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
if (options.server !== false) {
|
|
1649
|
+
try {
|
|
1650
|
+
const { HonoServerPlugin } = await import("@objectstack/plugin-hono-server");
|
|
1651
|
+
const serverPlugin = new HonoServerPlugin({ port });
|
|
1652
|
+
await kernel.use(serverPlugin);
|
|
1653
|
+
trackPlugin("HonoServer");
|
|
1654
|
+
} catch (e) {
|
|
1655
|
+
console.warn(chalk10.yellow(` \u26A0 HTTP server plugin not available: ${e.message}`));
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
let viteProcess = null;
|
|
1659
|
+
if (options.ui) {
|
|
1660
|
+
const consolePath = resolveConsolePath();
|
|
1661
|
+
if (!consolePath) {
|
|
1662
|
+
console.warn(chalk10.yellow(` \u26A0 @objectstack/console not found \u2014 skipping UI`));
|
|
1663
|
+
} else if (isDev) {
|
|
1664
|
+
try {
|
|
1665
|
+
const result = await spawnViteDevServer(consolePath, { serverPort: port });
|
|
1666
|
+
viteProcess = result.process;
|
|
1667
|
+
await kernel.use(createConsoleProxyPlugin(result.port));
|
|
1668
|
+
trackPlugin("ConsoleUI");
|
|
1669
|
+
} catch (e) {
|
|
1670
|
+
console.warn(chalk10.yellow(` \u26A0 Console UI failed to start: ${e.message}`));
|
|
1671
|
+
}
|
|
1672
|
+
} else {
|
|
1673
|
+
const distPath = path8.join(consolePath, "dist");
|
|
1674
|
+
if (hasConsoleDist(consolePath)) {
|
|
1675
|
+
await kernel.use(createConsoleStaticPlugin(distPath));
|
|
1676
|
+
trackPlugin("ConsoleUI");
|
|
1677
|
+
} else {
|
|
1678
|
+
console.warn(chalk10.yellow(` \u26A0 Console dist not found \u2014 run "pnpm --filter @objectstack/console build" first`));
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
await runtime.start();
|
|
1683
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1684
|
+
restoreOutput();
|
|
1685
|
+
printServerReady({
|
|
1686
|
+
port,
|
|
1687
|
+
configFile: relativeConfig,
|
|
1688
|
+
isDev,
|
|
1689
|
+
pluginCount: loadedPlugins.length,
|
|
1690
|
+
pluginNames: loadedPlugins,
|
|
1691
|
+
uiEnabled: !!options.ui,
|
|
1692
|
+
studioPath: STUDIO_PATH
|
|
1693
|
+
});
|
|
1694
|
+
process.on("SIGINT", async () => {
|
|
1695
|
+
console.warn(chalk10.yellow(`
|
|
1696
|
+
|
|
1697
|
+
\u23F9 Stopping server...`));
|
|
1698
|
+
if (viteProcess) {
|
|
1699
|
+
viteProcess.kill();
|
|
1700
|
+
viteProcess = null;
|
|
1701
|
+
}
|
|
1702
|
+
await runtime.getKernel().shutdown();
|
|
1703
|
+
console.log(chalk10.green(`\u2705 Server stopped`));
|
|
1704
|
+
process.exit(0);
|
|
1705
|
+
});
|
|
1706
|
+
} catch (error) {
|
|
1707
|
+
restoreOutput();
|
|
1708
|
+
console.log("");
|
|
1709
|
+
printError(error.message || String(error));
|
|
1710
|
+
if (process.env.DEBUG) console.error(chalk10.dim(error.stack));
|
|
1711
|
+
process.exit(1);
|
|
1712
|
+
}
|
|
1713
|
+
});
|
|
1714
|
+
|
|
1715
|
+
// src/commands/test.ts
|
|
1716
|
+
import { Command as Command9 } from "commander";
|
|
1717
|
+
import chalk11 from "chalk";
|
|
1718
|
+
import path9 from "path";
|
|
1719
|
+
import fs9 from "fs";
|
|
1720
|
+
import { QA as CoreQA } from "@objectstack/core";
|
|
1721
|
+
var testCommand = new Command9("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) => {
|
|
1722
|
+
console.log(chalk11.bold(`
|
|
1723
|
+
\u{1F9EA} ObjectStack Quality Protocol Runner`));
|
|
1724
|
+
console.log(chalk11.dim(`-------------------------------------`));
|
|
1725
|
+
console.log(`Target: ${chalk11.blue(options.url)}`);
|
|
1726
|
+
const adapter = new CoreQA.HttpTestAdapter(options.url, options.token);
|
|
1727
|
+
const runner = new CoreQA.TestRunner(adapter);
|
|
1728
|
+
const cwd = process.cwd();
|
|
1729
|
+
const testFiles = [];
|
|
1730
|
+
if (fs9.existsSync(filesPattern)) {
|
|
1731
|
+
testFiles.push(filesPattern);
|
|
1732
|
+
} else {
|
|
1733
|
+
const dir = path9.dirname(filesPattern);
|
|
1734
|
+
const ext = path9.extname(filesPattern);
|
|
1735
|
+
if (fs9.existsSync(dir)) {
|
|
1736
|
+
const files = fs9.readdirSync(dir).filter((f) => f.endsWith(ext) || f.endsWith(".json"));
|
|
1737
|
+
files.forEach((f) => testFiles.push(path9.join(dir, f)));
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
if (testFiles.length === 0) {
|
|
1741
|
+
console.warn(chalk11.yellow(`No test files found matching: ${filesPattern}`));
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1744
|
+
console.log(`Found ${testFiles.length} test suites.`);
|
|
1745
|
+
let totalPassed = 0;
|
|
1746
|
+
let totalFailed = 0;
|
|
1747
|
+
for (const file of testFiles) {
|
|
1748
|
+
console.log(`
|
|
1749
|
+
\u{1F4C4} Running suite: ${chalk11.bold(path9.basename(file))}`);
|
|
1750
|
+
try {
|
|
1751
|
+
const content = fs9.readFileSync(file, "utf-8");
|
|
1752
|
+
const suite = JSON.parse(content);
|
|
1753
|
+
const results = await runner.runSuite(suite);
|
|
1754
|
+
for (const result of results) {
|
|
1755
|
+
const icon = result.passed ? "\u2705" : "\u274C";
|
|
1756
|
+
console.log(` ${icon} Scenario: ${result.scenarioId} (${result.duration}ms)`);
|
|
1757
|
+
if (!result.passed) {
|
|
1758
|
+
console.error(chalk11.red(` Error: ${result.error}`));
|
|
1759
|
+
result.steps.forEach((step) => {
|
|
1760
|
+
if (!step.passed) {
|
|
1761
|
+
console.error(chalk11.red(` Step Failed: ${step.stepName}`));
|
|
1762
|
+
if (step.output) console.error(` Output:`, step.output);
|
|
1763
|
+
if (step.error) console.error(` Error:`, step.error);
|
|
1764
|
+
}
|
|
1765
|
+
});
|
|
1766
|
+
totalFailed++;
|
|
1767
|
+
} else {
|
|
1768
|
+
totalPassed++;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
} catch (e) {
|
|
1772
|
+
console.error(chalk11.red(`Failed to load or run suite ${file}: ${e}`));
|
|
1773
|
+
totalFailed++;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
console.log(chalk11.dim(`
|
|
1777
|
+
-------------------------------------`));
|
|
1778
|
+
if (totalFailed > 0) {
|
|
1779
|
+
console.log(chalk11.red(`FAILED: ${totalFailed} scenarios failed. ${totalPassed} passed.`));
|
|
1780
|
+
process.exit(1);
|
|
1781
|
+
} else {
|
|
1782
|
+
console.log(chalk11.green(`SUCCESS: All ${totalPassed} scenarios passed.`));
|
|
1783
|
+
process.exit(0);
|
|
1784
|
+
}
|
|
1785
|
+
});
|
|
1786
|
+
|
|
1787
|
+
// src/commands/doctor.ts
|
|
1788
|
+
import { Command as Command10 } from "commander";
|
|
1789
|
+
import chalk12 from "chalk";
|
|
1790
|
+
import { execSync as execSync2 } from "child_process";
|
|
1791
|
+
import fs10 from "fs";
|
|
1792
|
+
import path10 from "path";
|
|
1793
|
+
var doctorCommand = new Command10("doctor").description("Check development environment health").option("-v, --verbose", "Show detailed information").action(async (options) => {
|
|
1794
|
+
printHeader("Environment Health Check");
|
|
1795
|
+
const results = [];
|
|
1796
|
+
try {
|
|
1797
|
+
const nodeVersion = process.version;
|
|
1798
|
+
const majorVersion = parseInt(nodeVersion.slice(1).split(".")[0]);
|
|
1799
|
+
if (majorVersion >= 18) {
|
|
1800
|
+
results.push({
|
|
1801
|
+
name: "Node.js",
|
|
1802
|
+
status: "ok",
|
|
1803
|
+
message: `Version ${nodeVersion}`
|
|
1804
|
+
});
|
|
1805
|
+
} else {
|
|
1806
|
+
results.push({
|
|
1807
|
+
name: "Node.js",
|
|
1808
|
+
status: "error",
|
|
1809
|
+
message: `Version ${nodeVersion} (requires >= 18.0.0)`,
|
|
1810
|
+
fix: "Upgrade Node.js: https://nodejs.org"
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
} catch (error) {
|
|
1814
|
+
results.push({
|
|
1815
|
+
name: "Node.js",
|
|
1816
|
+
status: "error",
|
|
1817
|
+
message: "Not found",
|
|
1818
|
+
fix: "Install Node.js: https://nodejs.org"
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
try {
|
|
1822
|
+
const pnpmVersion = execSync2("pnpm -v", { encoding: "utf-8" }).trim();
|
|
1823
|
+
results.push({
|
|
1824
|
+
name: "pnpm",
|
|
1825
|
+
status: "ok",
|
|
1826
|
+
message: `Version ${pnpmVersion}`
|
|
1827
|
+
});
|
|
1828
|
+
} catch (error) {
|
|
1829
|
+
results.push({
|
|
1830
|
+
name: "pnpm",
|
|
1831
|
+
status: "error",
|
|
1832
|
+
message: "Not found",
|
|
1833
|
+
fix: "Install pnpm: npm install -g pnpm@10.28.1"
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1836
|
+
try {
|
|
1837
|
+
const tscVersion = execSync2("tsc -v", { encoding: "utf-8" }).trim();
|
|
1838
|
+
results.push({
|
|
1839
|
+
name: "TypeScript",
|
|
1840
|
+
status: "ok",
|
|
1841
|
+
message: tscVersion
|
|
1842
|
+
});
|
|
1843
|
+
} catch (error) {
|
|
1844
|
+
results.push({
|
|
1845
|
+
name: "TypeScript",
|
|
1846
|
+
status: "warning",
|
|
1847
|
+
message: "Not found in PATH",
|
|
1848
|
+
fix: "Installed locally via pnpm"
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
const cwd = process.cwd();
|
|
1852
|
+
const nodeModulesPath = path10.join(cwd, "node_modules");
|
|
1853
|
+
if (fs10.existsSync(nodeModulesPath)) {
|
|
1854
|
+
results.push({
|
|
1855
|
+
name: "Dependencies",
|
|
1856
|
+
status: "ok",
|
|
1857
|
+
message: "Installed"
|
|
1858
|
+
});
|
|
1859
|
+
} else {
|
|
1860
|
+
results.push({
|
|
1861
|
+
name: "Dependencies",
|
|
1862
|
+
status: "error",
|
|
1863
|
+
message: "Not installed",
|
|
1864
|
+
fix: "Run: pnpm install"
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
const specDistPath = path10.join(cwd, "packages/spec/dist");
|
|
1868
|
+
if (fs10.existsSync(specDistPath)) {
|
|
1869
|
+
results.push({
|
|
1870
|
+
name: "@objectstack/spec",
|
|
1871
|
+
status: "ok",
|
|
1872
|
+
message: "Built"
|
|
1873
|
+
});
|
|
1874
|
+
} else {
|
|
1875
|
+
results.push({
|
|
1876
|
+
name: "@objectstack/spec",
|
|
1877
|
+
status: "warning",
|
|
1878
|
+
message: "Not built",
|
|
1879
|
+
fix: "Run: pnpm --filter @objectstack/spec build"
|
|
1880
|
+
});
|
|
1881
|
+
}
|
|
1882
|
+
try {
|
|
1883
|
+
const gitVersion = execSync2("git --version", { encoding: "utf-8" }).trim();
|
|
1884
|
+
results.push({
|
|
1885
|
+
name: "Git",
|
|
1886
|
+
status: "ok",
|
|
1887
|
+
message: gitVersion
|
|
1888
|
+
});
|
|
1889
|
+
} catch (error) {
|
|
1890
|
+
results.push({
|
|
1891
|
+
name: "Git",
|
|
1892
|
+
status: "warning",
|
|
1893
|
+
message: "Not found",
|
|
1894
|
+
fix: "Install Git for version control"
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
let hasErrors = false;
|
|
1898
|
+
let hasWarnings = false;
|
|
1899
|
+
console.log("");
|
|
1900
|
+
results.forEach((result) => {
|
|
1901
|
+
const padded = result.name.padEnd(20);
|
|
1902
|
+
if (result.status === "ok") {
|
|
1903
|
+
printSuccess(`${padded} ${result.message}`);
|
|
1904
|
+
} else if (result.status === "warning") {
|
|
1905
|
+
printWarning(`${padded} ${result.message}`);
|
|
1906
|
+
} else {
|
|
1907
|
+
printError(`${padded} ${result.message}`);
|
|
1908
|
+
}
|
|
1909
|
+
if (result.fix && (options.verbose || result.status === "error")) {
|
|
1910
|
+
console.log(chalk12.dim(` \u2192 ${result.fix}`));
|
|
1911
|
+
}
|
|
1912
|
+
if (result.status === "error") hasErrors = true;
|
|
1913
|
+
if (result.status === "warning") hasWarnings = true;
|
|
1914
|
+
});
|
|
1915
|
+
console.log("");
|
|
1916
|
+
if (hasErrors) {
|
|
1917
|
+
console.log(chalk12.red("\u274C Some critical issues found. Please fix them before continuing."));
|
|
1918
|
+
results.filter((r) => r.status === "error" && r.fix).forEach((r) => console.log(chalk12.dim(` ${r.fix}`)));
|
|
1919
|
+
process.exit(1);
|
|
1920
|
+
} else if (hasWarnings) {
|
|
1921
|
+
console.log(chalk12.yellow("\u26A0\uFE0F Environment is functional but has some warnings."));
|
|
1922
|
+
console.log(chalk12.dim(" Run with --verbose to see fix suggestions."));
|
|
1923
|
+
} else {
|
|
1924
|
+
console.log(chalk12.green("\u2705 Environment is healthy and ready for development!"));
|
|
1925
|
+
}
|
|
1926
|
+
console.log("");
|
|
1927
|
+
});
|
|
5
1928
|
export {
|
|
6
|
-
compileCommand
|
|
1929
|
+
compileCommand,
|
|
1930
|
+
createCommand,
|
|
1931
|
+
devCommand,
|
|
1932
|
+
doctorCommand,
|
|
1933
|
+
generateCommand,
|
|
1934
|
+
infoCommand,
|
|
1935
|
+
initCommand,
|
|
1936
|
+
serveCommand,
|
|
1937
|
+
templates,
|
|
1938
|
+
testCommand,
|
|
1939
|
+
validateCommand
|
|
7
1940
|
};
|