@objectstack/cli 1.0.11 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,313 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { printHeader, printSuccess, printError, printStep, printKV, printInfo } from '../utils/format.js';
6
+
7
+ const TEMPLATES: Record<string, {
8
+ description: string;
9
+ dependencies: Record<string, string>;
10
+ devDependencies: Record<string, string>;
11
+ scripts: Record<string, string>;
12
+ configContent: (name: string) => string;
13
+ srcFiles: Record<string, (name: string) => string>;
14
+ }> = {
15
+ app: {
16
+ description: 'Full application with objects, views, and actions',
17
+ dependencies: {
18
+ '@objectstack/spec': 'workspace:*',
19
+ '@objectstack/runtime': 'workspace:^',
20
+ '@objectstack/objectql': 'workspace:^',
21
+ '@objectstack/driver-memory': 'workspace:^',
22
+ },
23
+ devDependencies: {
24
+ '@objectstack/cli': 'workspace:*',
25
+ 'typescript': '^5.3.0',
26
+ },
27
+ scripts: {
28
+ dev: 'objectstack dev',
29
+ start: 'objectstack serve',
30
+ build: 'objectstack compile',
31
+ validate: 'objectstack validate',
32
+ typecheck: 'tsc --noEmit',
33
+ },
34
+ configContent: (name: string) => `import { defineStack } from '@objectstack/spec';
35
+ import * as objects from './src/objects';
36
+
37
+ export default defineStack({
38
+ manifest: {
39
+ id: 'com.example.${name}',
40
+ namespace: '${name}',
41
+ version: '0.1.0',
42
+ type: 'app',
43
+ name: '${toTitleCase(name)}',
44
+ description: '${toTitleCase(name)} application built with ObjectStack',
45
+ },
46
+
47
+ objects: Object.values(objects),
48
+ });
49
+ `,
50
+ srcFiles: {
51
+ 'src/objects/index.ts': (name) => `export { default as ${toCamelCase(name)} } from './${name}';
52
+ `,
53
+ 'src/objects/__name__.ts': (name) => `import { Data } from '@objectstack/spec';
54
+
55
+ const ${toCamelCase(name)}: Data.Object = {
56
+ name: '${name}',
57
+ label: '${toTitleCase(name)}',
58
+ ownership: 'own',
59
+ fields: {
60
+ name: {
61
+ type: 'text',
62
+ label: 'Name',
63
+ required: true,
64
+ },
65
+ description: {
66
+ type: 'textarea',
67
+ label: 'Description',
68
+ },
69
+ status: {
70
+ type: 'select',
71
+ label: 'Status',
72
+ options: [
73
+ { label: 'Draft', value: 'draft' },
74
+ { label: 'Active', value: 'active' },
75
+ { label: 'Archived', value: 'archived' },
76
+ ],
77
+ defaultValue: 'draft',
78
+ },
79
+ },
80
+ };
81
+
82
+ export default ${toCamelCase(name)};
83
+ `,
84
+ },
85
+ },
86
+
87
+ plugin: {
88
+ description: 'Reusable plugin with objects and extensions',
89
+ dependencies: {
90
+ '@objectstack/spec': 'workspace:*',
91
+ },
92
+ devDependencies: {
93
+ 'typescript': '^5.3.0',
94
+ 'vitest': '^4.0.18',
95
+ },
96
+ scripts: {
97
+ build: 'objectstack compile',
98
+ validate: 'objectstack validate',
99
+ test: 'vitest run',
100
+ typecheck: 'tsc --noEmit',
101
+ },
102
+ configContent: (name: string) => `import { defineStack } from '@objectstack/spec';
103
+ import * as objects from './src/objects';
104
+
105
+ export default defineStack({
106
+ manifest: {
107
+ id: 'com.objectstack.plugin-${name}',
108
+ namespace: 'plugin_${name}',
109
+ version: '0.1.0',
110
+ type: 'plugin',
111
+ name: '${toTitleCase(name)} Plugin',
112
+ description: 'ObjectStack Plugin: ${toTitleCase(name)}',
113
+ },
114
+
115
+ objects: Object.values(objects),
116
+ });
117
+ `,
118
+ srcFiles: {
119
+ 'src/objects/index.ts': (name) => `export { default as ${toCamelCase(name)} } from './${name}';
120
+ `,
121
+ 'src/objects/__name__.ts': (name) => `import { Data } from '@objectstack/spec';
122
+
123
+ const ${toCamelCase(name)}: Data.Object = {
124
+ name: '${name}',
125
+ label: '${toTitleCase(name)}',
126
+ ownership: 'own',
127
+ fields: {
128
+ name: {
129
+ type: 'text',
130
+ label: 'Name',
131
+ required: true,
132
+ },
133
+ },
134
+ };
135
+
136
+ export default ${toCamelCase(name)};
137
+ `,
138
+ },
139
+ },
140
+
141
+ empty: {
142
+ description: 'Minimal project with just a config file',
143
+ dependencies: {
144
+ '@objectstack/spec': 'workspace:*',
145
+ },
146
+ devDependencies: {
147
+ '@objectstack/cli': 'workspace:*',
148
+ 'typescript': '^5.3.0',
149
+ },
150
+ scripts: {
151
+ build: 'objectstack compile',
152
+ validate: 'objectstack validate',
153
+ typecheck: 'tsc --noEmit',
154
+ },
155
+ configContent: (name: string) => `import { defineStack } from '@objectstack/spec';
156
+
157
+ export default defineStack({
158
+ manifest: {
159
+ id: 'com.example.${name}',
160
+ namespace: '${name}',
161
+ version: '0.1.0',
162
+ type: 'app',
163
+ name: '${toTitleCase(name)}',
164
+ description: '',
165
+ },
166
+ });
167
+ `,
168
+ srcFiles: {},
169
+ },
170
+ };
171
+
172
+ function toCamelCase(str: string): string {
173
+ return str.replace(/[-_]([a-z])/g, (_, c) => c.toUpperCase());
174
+ }
175
+
176
+ function toTitleCase(str: string): string {
177
+ return str.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
178
+ }
179
+
180
+ export const initCommand = new Command('init')
181
+ .description('Initialize a new ObjectStack project in the current directory')
182
+ .argument('[name]', 'Project name (defaults to directory name)')
183
+ .option('-t, --template <template>', 'Template: app, plugin, empty', 'app')
184
+ .option('--no-install', 'Skip dependency installation')
185
+ .action(async (name, options) => {
186
+ printHeader('Init');
187
+
188
+ const cwd = process.cwd();
189
+ const projectName = name || path.basename(cwd);
190
+ const template = TEMPLATES[options.template];
191
+
192
+ if (!template) {
193
+ printError(`Unknown template: ${options.template}`);
194
+ console.log(chalk.dim(` Available: ${Object.keys(TEMPLATES).join(', ')}`));
195
+ process.exit(1);
196
+ }
197
+
198
+ // Check for existing config
199
+ if (fs.existsSync(path.join(cwd, 'objectstack.config.ts'))) {
200
+ printError('objectstack.config.ts already exists in this directory');
201
+ console.log(chalk.dim(' Use `objectstack generate` to add metadata to an existing project'));
202
+ process.exit(1);
203
+ }
204
+
205
+ printKV('Project', projectName);
206
+ printKV('Template', `${options.template} — ${template.description}`);
207
+ printKV('Directory', cwd);
208
+ console.log('');
209
+
210
+ const createdFiles: string[] = [];
211
+
212
+ try {
213
+ // 1. Create package.json if missing
214
+ const pkgPath = path.join(cwd, 'package.json');
215
+ if (!fs.existsSync(pkgPath)) {
216
+ const pkg = {
217
+ name: projectName,
218
+ version: '0.1.0',
219
+ private: true,
220
+ type: 'module',
221
+ scripts: template.scripts,
222
+ dependencies: template.dependencies,
223
+ devDependencies: template.devDependencies,
224
+ };
225
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
226
+ createdFiles.push('package.json');
227
+ } else {
228
+ printInfo('package.json already exists, skipping');
229
+ }
230
+
231
+ // 2. Create objectstack.config.ts
232
+ const configContent = template.configContent(projectName);
233
+ fs.writeFileSync(path.join(cwd, 'objectstack.config.ts'), configContent);
234
+ createdFiles.push('objectstack.config.ts');
235
+
236
+ // 3. Create tsconfig.json if missing
237
+ const tsconfigPath = path.join(cwd, 'tsconfig.json');
238
+ if (!fs.existsSync(tsconfigPath)) {
239
+ const tsconfig = {
240
+ compilerOptions: {
241
+ target: 'ES2022',
242
+ module: 'ESNext',
243
+ moduleResolution: 'bundler',
244
+ strict: true,
245
+ esModuleInterop: true,
246
+ skipLibCheck: true,
247
+ outDir: 'dist',
248
+ rootDir: '.',
249
+ declaration: true,
250
+ },
251
+ include: ['*.ts', 'src/**/*'],
252
+ exclude: ['dist', 'node_modules'],
253
+ };
254
+ fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2) + '\n');
255
+ createdFiles.push('tsconfig.json');
256
+ }
257
+
258
+ // 4. Create src files
259
+ for (const [filePath, contentFn] of Object.entries(template.srcFiles)) {
260
+ const resolvedPath = filePath.replace('__name__', projectName);
261
+ const fullPath = path.join(cwd, resolvedPath);
262
+ const dir = path.dirname(fullPath);
263
+
264
+ if (!fs.existsSync(dir)) {
265
+ fs.mkdirSync(dir, { recursive: true });
266
+ }
267
+
268
+ fs.writeFileSync(fullPath, contentFn(projectName));
269
+ createdFiles.push(resolvedPath);
270
+ }
271
+
272
+ // 5. Create .gitignore if missing
273
+ const gitignorePath = path.join(cwd, '.gitignore');
274
+ if (!fs.existsSync(gitignorePath)) {
275
+ fs.writeFileSync(gitignorePath, `node_modules/\ndist/\n*.tsbuildinfo\n`);
276
+ createdFiles.push('.gitignore');
277
+ }
278
+
279
+ // Summary
280
+ console.log(chalk.bold(' Created files:'));
281
+ for (const f of createdFiles) {
282
+ console.log(chalk.green(` + ${f}`));
283
+ }
284
+ console.log('');
285
+
286
+ // Install dependencies
287
+ if (options.install !== false) {
288
+ printStep('Installing dependencies...');
289
+ const { execSync } = await import('child_process');
290
+ try {
291
+ execSync('pnpm install', { stdio: 'inherit', cwd });
292
+ } catch {
293
+ printWarning('Dependency installation failed. Run `pnpm install` manually.');
294
+ }
295
+ }
296
+
297
+ printSuccess('Project initialized!');
298
+ console.log('');
299
+ console.log(chalk.bold(' Next steps:'));
300
+ console.log(chalk.dim(' objectstack validate # Check configuration'));
301
+ console.log(chalk.dim(' objectstack dev # Start development server'));
302
+ console.log(chalk.dim(' objectstack generate # Add objects, views, etc.'));
303
+ console.log('');
304
+
305
+ } catch (error: any) {
306
+ printError(error.message || String(error));
307
+ process.exit(1);
308
+ }
309
+ });
310
+
311
+ function printWarning(msg: string) {
312
+ console.log(chalk.yellow(` ⚠ ${msg}`));
313
+ }
@@ -4,6 +4,24 @@ import fs from 'fs';
4
4
  import net from 'net';
5
5
  import chalk from 'chalk';
6
6
  import { bundleRequire } from 'bundle-require';
7
+ import { loadConfig } from '../utils/config.js';
8
+ import {
9
+ printHeader,
10
+ printKV,
11
+ printSuccess,
12
+ printError,
13
+ printStep,
14
+ printInfo,
15
+ printServerReady,
16
+ } from '../utils/format.js';
17
+ import {
18
+ STUDIO_PATH,
19
+ resolveConsolePath,
20
+ hasConsoleDist,
21
+ spawnViteDevServer,
22
+ createConsoleProxyPlugin,
23
+ createConsoleStaticPlugin,
24
+ } from '../utils/console.js';
7
25
 
8
26
  // Helper to find available port
9
27
  const getAvailablePort = async (startPort: number): Promise<number> => {
@@ -35,6 +53,7 @@ export const serveCommand = new Command('serve')
35
53
  .argument('[config]', 'Configuration file path', 'objectstack.config.ts')
36
54
  .option('-p, --port <port>', 'Server port', '3000')
37
55
  .option('--dev', 'Run in development mode (load devPlugins)')
56
+ .option('--ui', 'Enable Console UI at /_studio/')
38
57
  .option('--no-server', 'Skip starting HTTP server plugin')
39
58
  .action(async (configPath, options) => {
40
59
  let port = parseInt(options.port);
@@ -44,30 +63,67 @@ export const serveCommand = new Command('serve')
44
63
  port = availablePort;
45
64
  }
46
65
  } catch (e) {
47
- // Ignore error and try with original port, or let it fail later
66
+ // Ignore error and try with original port
48
67
  }
49
68
 
50
- console.log(chalk.bold(`\n🚀 ObjectStack Server`));
51
- console.log(chalk.dim(`------------------------`));
52
- console.log(`📂 Config: ${chalk.blue(configPath)}`);
53
- if (parseInt(options.port) !== port) {
54
- console.log(`🌐 Port: ${chalk.blue(port)} ${chalk.yellow(`(requested: ${options.port} in use)`)}`);
55
- } else {
56
- console.log(`🌐 Port: ${chalk.blue(port)}`);
57
- }
58
- console.log('');
59
-
69
+ const isDev = options.dev || process.env.NODE_ENV === 'development';
60
70
 
61
71
  const absolutePath = path.resolve(process.cwd(), configPath);
72
+ const relativeConfig = path.relative(process.cwd(), absolutePath);
62
73
 
63
74
  if (!fs.existsSync(absolutePath)) {
64
- console.error(chalk.red(`\n❌ Configuration file not found: ${absolutePath}`));
75
+ printError(`Configuration file not found: ${absolutePath}`);
76
+ console.log(chalk.dim(' Hint: Run `objectstack init` to create a new project'));
65
77
  process.exit(1);
66
78
  }
67
79
 
80
+ // Quiet loading — only show a single spinner line
81
+ console.log('');
82
+ console.log(chalk.dim(` Loading ${relativeConfig}...`));
83
+
84
+ // Track loaded plugins for summary
85
+ const loadedPlugins: string[] = [];
86
+ const shortPluginName = (raw: string) => {
87
+ // Map verbose internal IDs to short display names
88
+ if (raw.includes('objectql')) return 'ObjectQL';
89
+ if (raw.includes('driver') && raw.includes('memory')) return 'MemoryDriver';
90
+ if (raw.startsWith('plugin.app.')) return raw.replace('plugin.app.', '').split('.').pop() || raw;
91
+ if (raw.includes('hono')) return 'HonoServer';
92
+ return raw;
93
+ };
94
+ const trackPlugin = (name: string) => { loadedPlugins.push(shortPluginName(name)); };
95
+
96
+ // Save original console/stdout methods — we'll suppress noise during boot
97
+ const originalConsoleLog = console.log;
98
+ const originalConsoleDebug = console.debug;
99
+ const origStdoutWrite = process.stdout.write.bind(process.stdout);
100
+ let bootQuiet = false;
101
+
102
+ const restoreOutput = () => {
103
+ bootQuiet = false;
104
+ process.stdout.write = origStdoutWrite;
105
+ console.log = originalConsoleLog;
106
+ console.debug = originalConsoleDebug;
107
+ };
108
+
109
+ const portShifted = parseInt(options.port) !== port;
110
+
68
111
  try {
112
+ // ── Suppress ALL runtime noise during boot ────────────────────
113
+ // Multiple sources write to stdout during startup:
114
+ // • Pino-pretty (direct process.stdout.write)
115
+ // • ObjectLogger browser fallback (console.log)
116
+ // • SchemaRegistry (console.log)
117
+ // We capture stdout entirely, then restore after runtime.start().
118
+ bootQuiet = true;
119
+ process.stdout.write = (chunk: any, ...rest: any[]) => {
120
+ if (bootQuiet) return true; // swallow
121
+ return (origStdoutWrite as any)(chunk, ...rest);
122
+ };
123
+ console.log = (...args: any[]) => { if (!bootQuiet) originalConsoleLog(...args); };
124
+ console.debug = (...args: any[]) => { if (!bootQuiet) originalConsoleDebug(...args); };
125
+
69
126
  // Load configuration
70
- console.log(chalk.yellow(`📦 Loading configuration...`));
71
127
  const { mod } = await bundleRequire({
72
128
  filepath: absolutePath,
73
129
  });
@@ -75,20 +131,14 @@ export const serveCommand = new Command('serve')
75
131
  const config = mod.default || mod;
76
132
 
77
133
  if (!config) {
78
- throw new Error(`Default export not found in ${configPath}`);
134
+ throw new Error(`No default export found in ${configPath}`);
79
135
  }
80
136
 
81
- console.log(chalk.green(`✓ Configuration loaded`));
82
-
83
137
  // Import ObjectStack runtime
84
138
  const { Runtime } = await import('@objectstack/runtime');
85
-
86
- // Create runtime instance
87
- console.log(chalk.yellow(`🔧 Initializing ObjectStack runtime...`));
88
-
89
- // Auto-configure pretty logging in development mode
90
- const isDev = options.dev || process.env.NODE_ENV === 'development';
91
- const loggerConfig = isDev ? { format: 'pretty' } : undefined;
139
+
140
+ // Set kernel logger to 'silent' — the CLI manages its own output
141
+ const loggerConfig = { level: 'silent' as const };
92
142
 
93
143
  const runtime = new Runtime({
94
144
  kernel: {
@@ -102,7 +152,6 @@ export const serveCommand = new Command('serve')
102
152
 
103
153
  // Merge devPlugins if in dev mode
104
154
  if (options.dev && config.devPlugins) {
105
- console.log(chalk.blue(`📦 Loading development plugins...`));
106
155
  plugins = [...plugins, ...config.devPlugins];
107
156
  }
108
157
 
@@ -110,12 +159,11 @@ export const serveCommand = new Command('serve')
110
159
  const hasObjectQL = plugins.some((p: any) => p.name?.includes('objectql') || p.constructor?.name?.includes('ObjectQL'));
111
160
  if (config.objects && !hasObjectQL) {
112
161
  try {
113
- console.log(chalk.dim(` Auto-injecting ObjectQL Engine...`));
114
162
  const { ObjectQLPlugin } = await import('@objectstack/objectql');
115
163
  await kernel.use(new ObjectQLPlugin());
116
- console.log(chalk.green(` ✓ Registered ObjectQL Plugin (auto-detected)`));
164
+ trackPlugin('ObjectQL');
117
165
  } catch (e: any) {
118
- console.warn(chalk.yellow(` ⚠ Could not auto-load ObjectQL: ${e.message}`));
166
+ // silent
119
167
  }
120
168
  }
121
169
 
@@ -123,14 +171,12 @@ export const serveCommand = new Command('serve')
123
171
  const hasDriver = plugins.some((p: any) => p.name?.includes('driver') || p.constructor?.name?.includes('Driver'));
124
172
  if (isDev && !hasDriver && config.objects) {
125
173
  try {
126
- console.log(chalk.dim(` Auto-injecting Memory Driver (Dev Mode)...`));
127
174
  const { DriverPlugin } = await import('@objectstack/runtime');
128
175
  const { InMemoryDriver } = await import('@objectstack/driver-memory');
129
176
  await kernel.use(new DriverPlugin(new InMemoryDriver()));
130
- console.log(chalk.green(` ✓ Registered Memory Driver (auto-detected)`));
177
+ trackPlugin('MemoryDriver');
131
178
  } catch (e: any) {
132
- // Silent fail - maybe they don't want a driver or don't have the package
133
- console.log(chalk.dim(` ℹ No default driver loaded: ${e.message}`));
179
+ // silent
134
180
  }
135
181
  }
136
182
 
@@ -139,38 +185,33 @@ export const serveCommand = new Command('serve')
139
185
  try {
140
186
  const { AppPlugin } = await import('@objectstack/runtime');
141
187
  await kernel.use(new AppPlugin(config));
142
- console.log(chalk.green(` ✓ Registered App Plugin (auto-detected)`));
188
+ trackPlugin('App');
143
189
  } catch (e: any) {
144
- console.warn(chalk.yellow(` ⚠ Could not auto-load AppPlugin: ${e.message}`));
190
+ // silent
145
191
  }
146
192
  }
147
193
 
148
194
 
149
195
  if (plugins.length > 0) {
150
- console.log(chalk.yellow(`📦 Loading ${plugins.length} plugin(s)...`));
151
-
152
196
  for (const plugin of plugins) {
153
197
  try {
154
198
  let pluginToLoad = plugin;
155
199
 
156
200
  // Resolve string references (package names)
157
201
  if (typeof plugin === 'string') {
158
- console.log(chalk.dim(` Trying to resolve plugin: ${plugin}`));
159
202
  try {
160
- // Try dynamic import for packages
161
203
  const imported = await import(plugin);
162
204
  pluginToLoad = imported.default || imported;
163
205
  } catch (importError: any) {
164
- // Fallback: try bundleRequire for local paths if needed, otherwise throw
165
206
  throw new Error(`Failed to import plugin '${plugin}': ${importError.message}`);
166
207
  }
167
208
  }
168
209
 
169
210
  await kernel.use(pluginToLoad);
170
211
  const pluginName = plugin.name || plugin.constructor?.name || 'unnamed';
171
- console.log(chalk.green(` ✓ Registered plugin: ${pluginName}`));
212
+ trackPlugin(pluginName);
172
213
  } catch (e: any) {
173
- console.error(chalk.red(` ✗ Failed to register plugin: ${e.message}`));
214
+ console.error(chalk.red(` ✗ Failed to load plugin: ${e.message}`));
174
215
  }
175
216
  }
176
217
  }
@@ -181,31 +222,76 @@ export const serveCommand = new Command('serve')
181
222
  const { HonoServerPlugin } = await import('@objectstack/plugin-hono-server');
182
223
  const serverPlugin = new HonoServerPlugin({ port });
183
224
  await kernel.use(serverPlugin);
184
- console.log(chalk.green(` ✓ Registered HTTP server plugin (port: ${port})`));
225
+ trackPlugin('HonoServer');
185
226
  } catch (e: any) {
186
227
  console.warn(chalk.yellow(` ⚠ HTTP server plugin not available: ${e.message}`));
187
228
  }
188
229
  }
189
230
 
231
+ // ── Console UI (--ui) ───────────────────────────────────────────
232
+ let viteProcess: import('child_process').ChildProcess | null = null;
233
+
234
+ if (options.ui) {
235
+ const consolePath = resolveConsolePath();
236
+ if (!consolePath) {
237
+ console.warn(chalk.yellow(` ⚠ @objectstack/console not found — skipping UI`));
238
+ } else if (isDev) {
239
+ // Dev mode → spawn Vite dev server & proxy through Hono
240
+ try {
241
+ const result = await spawnViteDevServer(consolePath, { serverPort: port });
242
+ viteProcess = result.process;
243
+ await kernel.use(createConsoleProxyPlugin(result.port));
244
+ trackPlugin('ConsoleUI');
245
+ } catch (e: any) {
246
+ console.warn(chalk.yellow(` ⚠ Console UI failed to start: ${e.message}`));
247
+ }
248
+ } else {
249
+ // Production mode → serve pre-built static files
250
+ const distPath = path.join(consolePath, 'dist');
251
+ if (hasConsoleDist(consolePath)) {
252
+ await kernel.use(createConsoleStaticPlugin(distPath));
253
+ trackPlugin('ConsoleUI');
254
+ } else {
255
+ console.warn(chalk.yellow(` ⚠ Console dist not found — run "pnpm --filter @objectstack/console build" first`));
256
+ }
257
+ }
258
+ }
259
+
190
260
  // Boot the runtime
191
- console.log(chalk.yellow(`\n🚀 Starting ObjectStack...`));
192
261
  await runtime.start();
193
262
 
194
- console.log(chalk.green(`\n✅ ObjectStack server is running!`));
195
- console.log(chalk.dim(` Press Ctrl+C to stop\n`));
263
+ // Wait briefly for pino worker thread buffers to flush, then restore
264
+ await new Promise(r => setTimeout(r, 100));
265
+ restoreOutput();
266
+
267
+ // ── Clean startup summary ──────────────────────────────────────
268
+ printServerReady({
269
+ port,
270
+ configFile: relativeConfig,
271
+ isDev,
272
+ pluginCount: loadedPlugins.length,
273
+ pluginNames: loadedPlugins,
274
+ uiEnabled: !!options.ui,
275
+ studioPath: STUDIO_PATH,
276
+ });
196
277
 
197
278
  // Keep process alive
198
279
  process.on('SIGINT', async () => {
199
- console.log(chalk.yellow(`\n\n⏹ Stopping server...`));
280
+ console.warn(chalk.yellow(`\n\n⏹ Stopping server...`));
281
+ if (viteProcess) {
282
+ viteProcess.kill();
283
+ viteProcess = null;
284
+ }
200
285
  await runtime.getKernel().shutdown();
201
286
  console.log(chalk.green(`✅ Server stopped`));
202
287
  process.exit(0);
203
288
  });
204
289
 
205
290
  } catch (error: any) {
206
- console.error(chalk.red(`\n❌ Server Error:`));
207
- console.error(error.message || error);
208
- console.error(error.stack);
291
+ restoreOutput();
292
+ console.log('');
293
+ printError(error.message || String(error));
294
+ if (process.env.DEBUG) console.error(chalk.dim(error.stack));
209
295
  process.exit(1);
210
296
  }
211
297
  });
@@ -0,0 +1,40 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { spawn } from 'child_process';
4
+ import { printHeader, printKV, printStep } from '../utils/format.js';
5
+
6
+ /**
7
+ * `objectstack studio` — Launch the ObjectStack Console UI.
8
+ *
9
+ * Alias for `objectstack serve --dev --ui`.
10
+ * Starts the ObjectStack server in development mode with the Console
11
+ * UI available at http://localhost:<port>/_studio/
12
+ */
13
+ export const studioCommand = new Command('studio')
14
+ .description('Launch Console UI with development server')
15
+ .argument('[config]', 'Configuration file path', 'objectstack.config.ts')
16
+ .option('-p, --port <port>', 'Server port', '3000')
17
+ .action(async (configPath, options) => {
18
+ printHeader('Studio');
19
+ printKV('Mode', 'dev + ui', '🎨');
20
+ printStep('Delegating to serve --dev --ui …');
21
+ console.log('');
22
+
23
+ // Delegate to the serve command with --dev --ui flags
24
+ const binPath = process.argv[1];
25
+ const args = [
26
+ binPath,
27
+ 'serve',
28
+ configPath,
29
+ '--dev',
30
+ '--ui',
31
+ '--port', options.port,
32
+ ];
33
+
34
+ const child = spawn(process.execPath, args, {
35
+ stdio: 'inherit',
36
+ env: { ...process.env, NODE_ENV: 'development' },
37
+ });
38
+
39
+ child.on('exit', (code) => process.exit(code ?? 0));
40
+ });
@@ -5,8 +5,8 @@ import fs from 'fs';
5
5
  import { QA as CoreQA } from '@objectstack/core';
6
6
  import { QA } from '@objectstack/spec';
7
7
 
8
- export const testCommand = new Command('test:run')
9
- .description('Run Quality Protocol test scenarios')
8
+ export const testCommand = new Command('test')
9
+ .description('Run Quality Protocol test scenarios against a running server')
10
10
  .argument('[files]', 'Glob pattern for test files (e.g. "qa/*.test.json")', 'qa/*.test.json')
11
11
  .option('--url <url>', 'Target base URL', 'http://localhost:3000')
12
12
  .option('--token <token>', 'Authentication token')