@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.
- 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
|
@@ -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
|
+
}
|
package/src/commands/serve.ts
CHANGED
|
@@ -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
|
|
66
|
+
// Ignore error and try with original port
|
|
48
67
|
}
|
|
49
68
|
|
|
50
|
-
|
|
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
|
-
|
|
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(`
|
|
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
|
-
//
|
|
87
|
-
|
|
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
|
-
|
|
164
|
+
trackPlugin('ObjectQL');
|
|
117
165
|
} catch (e: any) {
|
|
118
|
-
|
|
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
|
-
|
|
177
|
+
trackPlugin('MemoryDriver');
|
|
131
178
|
} catch (e: any) {
|
|
132
|
-
//
|
|
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
|
-
|
|
188
|
+
trackPlugin('App');
|
|
143
189
|
} catch (e: any) {
|
|
144
|
-
|
|
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
|
-
|
|
212
|
+
trackPlugin(pluginName);
|
|
172
213
|
} catch (e: any) {
|
|
173
|
-
console.error(chalk.red(` ✗ Failed to
|
|
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
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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.
|
|
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
|
-
|
|
207
|
-
console.
|
|
208
|
-
|
|
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
|
+
});
|
package/src/commands/test.ts
CHANGED
|
@@ -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
|
|
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')
|