@objectstack/cli 4.0.3 → 4.0.5
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/README.md +12 -25
- package/dist/commands/build.d.ts +5 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +6 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/compile.d.ts +3 -0
- package/dist/commands/compile.d.ts.map +1 -1
- package/dist/commands/compile.js +128 -6
- package/dist/commands/compile.js.map +1 -1
- package/dist/commands/create.js +1 -1
- package/dist/commands/data/create.js +2 -2
- package/dist/commands/data/create.js.map +1 -1
- package/dist/commands/data/delete.js +2 -2
- package/dist/commands/data/delete.js.map +1 -1
- package/dist/commands/data/get.js +2 -2
- package/dist/commands/data/get.js.map +1 -1
- package/dist/commands/data/query.js +2 -2
- package/dist/commands/data/query.js.map +1 -1
- package/dist/commands/data/update.js +2 -2
- package/dist/commands/data/update.js.map +1 -1
- package/dist/commands/dev.d.ts +3 -0
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +48 -19
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/generate.js +9 -9
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/i18n/check.d.ts +18 -0
- package/dist/commands/i18n/check.d.ts.map +1 -0
- package/dist/commands/i18n/check.js +153 -0
- package/dist/commands/i18n/check.js.map +1 -0
- package/dist/commands/init.js +2 -2
- package/dist/commands/lint.d.ts +3 -0
- package/dist/commands/lint.d.ts.map +1 -1
- package/dist/commands/lint.js +24 -0
- package/dist/commands/lint.js.map +1 -1
- package/dist/commands/login.d.ts +17 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +313 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/{auth/logout.js → logout.js} +14 -2
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/meta/delete.js +2 -2
- package/dist/commands/meta/delete.js.map +1 -1
- package/dist/commands/meta/get.js +2 -2
- package/dist/commands/meta/get.js.map +1 -1
- package/dist/commands/meta/list.js +2 -2
- package/dist/commands/meta/list.js.map +1 -1
- package/dist/commands/meta/register.js +2 -2
- package/dist/commands/meta/register.js.map +1 -1
- package/dist/commands/projects/bind.d.ts +30 -0
- package/dist/commands/projects/bind.d.ts.map +1 -0
- package/dist/commands/projects/bind.js +132 -0
- package/dist/commands/projects/bind.js.map +1 -0
- package/dist/commands/projects/create.d.ts +28 -0
- package/dist/commands/projects/create.d.ts.map +1 -0
- package/dist/commands/projects/create.js +120 -0
- package/dist/commands/projects/create.js.map +1 -0
- package/dist/commands/projects/list.d.ts +21 -0
- package/dist/commands/projects/list.d.ts.map +1 -0
- package/dist/commands/projects/list.js +79 -0
- package/dist/commands/projects/list.js.map +1 -0
- package/dist/commands/projects/projects.test.d.ts +2 -0
- package/dist/commands/projects/projects.test.d.ts.map +1 -0
- package/dist/commands/projects/projects.test.js +56 -0
- package/dist/commands/projects/projects.test.js.map +1 -0
- package/dist/commands/projects/show.d.ts +21 -0
- package/dist/commands/projects/show.d.ts.map +1 -0
- package/dist/commands/projects/show.js +72 -0
- package/dist/commands/projects/show.js.map +1 -0
- package/dist/commands/projects/switch.d.ts +24 -0
- package/dist/commands/projects/switch.d.ts.map +1 -0
- package/dist/commands/projects/switch.js +64 -0
- package/dist/commands/projects/switch.js.map +1 -0
- package/dist/commands/publish.d.ts +14 -0
- package/dist/commands/publish.d.ts.map +1 -0
- package/dist/commands/publish.js +91 -0
- package/dist/commands/publish.js.map +1 -0
- package/dist/commands/{auth/login.d.ts → register.d.ts} +3 -2
- package/dist/commands/register.d.ts.map +1 -0
- package/dist/commands/{auth/login.js → register.js} +44 -61
- package/dist/commands/register.js.map +1 -0
- package/dist/commands/serve.d.ts +8 -0
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +606 -44
- package/dist/commands/serve.js.map +1 -1
- package/dist/commands/start.d.ts +11 -0
- package/dist/commands/start.d.ts.map +1 -0
- package/dist/commands/start.js +43 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/commands/{auth/whoami.js → whoami.js} +5 -5
- package/dist/commands/whoami.js.map +1 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -5
- package/dist/index.js.map +1 -1
- package/dist/utils/account.d.ts +31 -0
- package/dist/utils/account.d.ts.map +1 -0
- package/dist/utils/account.js +154 -0
- package/dist/utils/account.js.map +1 -0
- package/dist/utils/api-client.d.ts +10 -4
- package/dist/utils/api-client.d.ts.map +1 -1
- package/dist/utils/api-client.js +13 -7
- package/dist/utils/api-client.js.map +1 -1
- package/dist/utils/auth-config.d.ts +6 -0
- package/dist/utils/auth-config.d.ts.map +1 -1
- package/dist/utils/auth-config.js.map +1 -1
- package/dist/utils/build-runtime.d.ts +45 -0
- package/dist/utils/build-runtime.d.ts.map +1 -0
- package/dist/utils/build-runtime.js +154 -0
- package/dist/utils/build-runtime.js.map +1 -0
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +17 -2
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/console.d.ts +32 -0
- package/dist/utils/console.d.ts.map +1 -0
- package/dist/utils/console.js +169 -0
- package/dist/utils/console.js.map +1 -0
- package/dist/utils/extract-hook-body.d.ts +13 -0
- package/dist/utils/extract-hook-body.d.ts.map +1 -0
- package/dist/utils/extract-hook-body.js +175 -0
- package/dist/utils/extract-hook-body.js.map +1 -0
- package/dist/utils/format.d.ts +8 -0
- package/dist/utils/format.d.ts.map +1 -1
- package/dist/utils/format.js +15 -2
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/i18n-coverage.d.ts +61 -0
- package/dist/utils/i18n-coverage.d.ts.map +1 -0
- package/dist/utils/i18n-coverage.js +176 -0
- package/dist/utils/i18n-coverage.js.map +1 -0
- package/dist/utils/lower-callables.d.ts +17 -0
- package/dist/utils/lower-callables.d.ts.map +1 -0
- package/dist/utils/lower-callables.js +181 -0
- package/dist/utils/lower-callables.js.map +1 -0
- package/dist/utils/plugin-detection.d.ts +1 -0
- package/dist/utils/plugin-detection.d.ts.map +1 -1
- package/dist/utils/plugin-detection.js +41 -0
- package/dist/utils/plugin-detection.js.map +1 -1
- package/dist/utils/studio.d.ts +1 -0
- package/dist/utils/studio.d.ts.map +1 -1
- package/dist/utils/studio.js +25 -9
- package/dist/utils/studio.js.map +1 -1
- package/package.json +55 -21
- package/.turbo/turbo-build.log +0 -4
- package/CHANGELOG.md +0 -805
- package/bin/run-dev.js +0 -5
- package/dist/commands/auth/login.d.ts.map +0 -1
- package/dist/commands/auth/login.js.map +0 -1
- package/dist/commands/auth/logout.d.ts.map +0 -1
- package/dist/commands/auth/logout.js.map +0 -1
- package/dist/commands/auth/whoami.d.ts.map +0 -1
- package/dist/commands/auth/whoami.js.map +0 -1
- package/dist/commands/codemod/v2-to-v3.d.ts +0 -10
- package/dist/commands/codemod/v2-to-v3.d.ts.map +0 -1
- package/dist/commands/codemod/v2-to-v3.js +0 -145
- package/dist/commands/codemod/v2-to-v3.js.map +0 -1
- package/dist/commands/plugin/add.d.ts +0 -22
- package/dist/commands/plugin/add.d.ts.map +0 -1
- package/dist/commands/plugin/add.js +0 -93
- package/dist/commands/plugin/add.js.map +0 -1
- package/dist/commands/plugin/build.d.ts +0 -29
- package/dist/commands/plugin/build.d.ts.map +0 -1
- package/dist/commands/plugin/build.js +0 -170
- package/dist/commands/plugin/build.js.map +0 -1
- package/dist/commands/plugin/info.d.ts +0 -10
- package/dist/commands/plugin/info.d.ts.map +0 -1
- package/dist/commands/plugin/info.js +0 -65
- package/dist/commands/plugin/info.js.map +0 -1
- package/dist/commands/plugin/list.d.ts +0 -13
- package/dist/commands/plugin/list.d.ts.map +0 -1
- package/dist/commands/plugin/list.js +0 -78
- package/dist/commands/plugin/list.js.map +0 -1
- package/dist/commands/plugin/publish.d.ts +0 -27
- package/dist/commands/plugin/publish.d.ts.map +0 -1
- package/dist/commands/plugin/publish.js +0 -152
- package/dist/commands/plugin/publish.js.map +0 -1
- package/dist/commands/plugin/remove.d.ts +0 -20
- package/dist/commands/plugin/remove.d.ts.map +0 -1
- package/dist/commands/plugin/remove.js +0 -79
- package/dist/commands/plugin/remove.js.map +0 -1
- package/dist/commands/plugin/validate.d.ts +0 -23
- package/dist/commands/plugin/validate.d.ts.map +0 -1
- package/dist/commands/plugin/validate.js +0 -251
- package/dist/commands/plugin/validate.js.map +0 -1
- package/src/bin.ts +0 -13
- package/src/commands/auth/login.ts +0 -188
- package/src/commands/auth/logout.ts +0 -51
- package/src/commands/auth/whoami.ts +0 -85
- package/src/commands/codemod/v2-to-v3.ts +0 -171
- package/src/commands/compile.ts +0 -114
- package/src/commands/create.ts +0 -281
- package/src/commands/data/create.ts +0 -110
- package/src/commands/data/delete.ts +0 -84
- package/src/commands/data/get.ts +0 -84
- package/src/commands/data/query.ts +0 -127
- package/src/commands/data/update.ts +0 -114
- package/src/commands/dev.ts +0 -83
- package/src/commands/diff.ts +0 -294
- package/src/commands/doctor.ts +0 -572
- package/src/commands/explain.ts +0 -412
- package/src/commands/generate.ts +0 -924
- package/src/commands/info.ts +0 -124
- package/src/commands/init.ts +0 -327
- package/src/commands/lint.ts +0 -315
- package/src/commands/meta/delete.ts +0 -79
- package/src/commands/meta/get.ts +0 -73
- package/src/commands/meta/list.ts +0 -105
- package/src/commands/meta/register.ts +0 -97
- package/src/commands/plugin/add.ts +0 -112
- package/src/commands/plugin/build.ts +0 -193
- package/src/commands/plugin/info.ts +0 -79
- package/src/commands/plugin/list.ts +0 -93
- package/src/commands/plugin/publish.ts +0 -176
- package/src/commands/plugin/remove.ts +0 -97
- package/src/commands/plugin/validate.ts +0 -268
- package/src/commands/serve.ts +0 -411
- package/src/commands/studio.ts +0 -52
- package/src/commands/test.ts +0 -135
- package/src/commands/validate.ts +0 -143
- package/src/index.ts +0 -22
- package/src/utils/api-client.ts +0 -88
- package/src/utils/auth-config.ts +0 -107
- package/src/utils/config.ts +0 -80
- package/src/utils/format.ts +0 -267
- package/src/utils/output-formatter.ts +0 -91
- package/src/utils/plugin-detection.ts +0 -16
- package/src/utils/plugin-helpers.ts +0 -37
- package/src/utils/studio.ts +0 -350
- package/test/commands.test.ts +0 -128
- package/test/create.test.ts +0 -25
- package/test/plugin-commands.test.ts +0 -44
- package/test/plugin.test.ts +0 -169
- package/test/remote-api-commands.test.ts +0 -188
- package/test/remote-api-utils.test.ts +0 -196
- package/test/serve-host-config.test.ts +0 -77
- package/tsconfig.build.json +0 -20
- package/tsconfig.json +0 -25
- package/tsup.config.ts +0 -23
- /package/dist/commands/{auth/logout.d.ts → logout.d.ts} +0 -0
- /package/dist/commands/{auth/whoami.d.ts → whoami.d.ts} +0 -0
package/dist/commands/serve.js
CHANGED
|
@@ -5,9 +5,11 @@ import fs from 'fs';
|
|
|
5
5
|
import net from 'net';
|
|
6
6
|
import chalk from 'chalk';
|
|
7
7
|
import { bundleRequire } from 'bundle-require';
|
|
8
|
-
import {
|
|
8
|
+
import { shouldBootWithLibrary } from '../utils/plugin-detection.js';
|
|
9
9
|
import { printError, printServerReady, } from '../utils/format.js';
|
|
10
10
|
import { STUDIO_PATH, resolveStudioPath, hasStudioDist, createStudioStaticPlugin, } from '../utils/studio.js';
|
|
11
|
+
import { ACCOUNT_PATH, resolveAccountPath, hasAccountDist, createAccountStaticPlugin, } from '../utils/account.js';
|
|
12
|
+
import { CONSOLE_PATH, resolveConsolePath, hasConsoleDist, createConsoleStaticPlugin, } from '../utils/console.js';
|
|
11
13
|
import dotenvFlow from 'dotenv-flow';
|
|
12
14
|
// Helper to find available port
|
|
13
15
|
const getAvailablePort = async (startPort) => {
|
|
@@ -38,10 +40,25 @@ export default class Serve extends Command {
|
|
|
38
40
|
config: Args.string({ description: 'Configuration file path', required: false, default: 'objectstack.config.ts' }),
|
|
39
41
|
};
|
|
40
42
|
static flags = {
|
|
41
|
-
port: Flags.string({ char: 'p', description: 'Server port', default: '3000' }),
|
|
43
|
+
port: Flags.string({ char: 'p', description: 'Server port', default: process.env.PORT ?? '3000' }),
|
|
42
44
|
dev: Flags.boolean({ description: 'Run in development mode (load devPlugins)' }),
|
|
43
|
-
ui: Flags.boolean({ description: 'Enable Studio UI at /_studio/ (default: true
|
|
45
|
+
ui: Flags.boolean({ description: 'Enable Studio UI at /_studio/ (default: true)', default: true, allowNo: true }),
|
|
44
46
|
server: Flags.boolean({ description: 'Start HTTP server plugin', default: true, allowNo: true }),
|
|
47
|
+
prebuilt: Flags.boolean({ description: 'Skip esbuild/bundle-require — load config as native ESM (production mode)', default: false }),
|
|
48
|
+
preset: Flags.string({
|
|
49
|
+
description: 'Plugin tier preset: minimal | default | full (overridden by config.tiers if set)',
|
|
50
|
+
options: ['minimal', 'default', 'full'],
|
|
51
|
+
}),
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Auto-registered plugin tiers. Plugins explicitly listed in
|
|
55
|
+
* `config.plugins` are always loaded — tiers only gate the optional
|
|
56
|
+
* auto-registration blocks below (AIService, I18n, Studio UI, etc.).
|
|
57
|
+
*/
|
|
58
|
+
static TIER_PRESETS = {
|
|
59
|
+
minimal: ['core'],
|
|
60
|
+
default: ['core', 'i18n', 'ui', 'auth'],
|
|
61
|
+
full: ['core', 'i18n', 'ui', 'ai', 'auth'],
|
|
45
62
|
};
|
|
46
63
|
async run() {
|
|
47
64
|
const { args, flags } = await this.parse(Serve);
|
|
@@ -86,6 +103,20 @@ export default class Serve extends Command {
|
|
|
86
103
|
return raw;
|
|
87
104
|
};
|
|
88
105
|
const trackPlugin = (name) => { loadedPlugins.push(shortPluginName(name)); };
|
|
106
|
+
// Track resolved storage driver + redacted URL for the startup banner.
|
|
107
|
+
let resolvedDriverLabel;
|
|
108
|
+
let resolvedDatabaseUrl;
|
|
109
|
+
const redactDbUrl = (url) => {
|
|
110
|
+
if (!url)
|
|
111
|
+
return undefined;
|
|
112
|
+
try {
|
|
113
|
+
// Redact passwords inside connection URLs: protocol://user:****@host/db
|
|
114
|
+
return url.replace(/(\/\/[^/@:]+):[^/@]+@/, '$1:****@');
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return url;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
89
120
|
// Save original console/stdout methods — we'll suppress noise during boot
|
|
90
121
|
const originalConsoleLog = console.log;
|
|
91
122
|
const originalConsoleDebug = console.debug;
|
|
@@ -116,13 +147,89 @@ export default class Serve extends Command {
|
|
|
116
147
|
console.debug = (...args) => { if (!bootQuiet)
|
|
117
148
|
originalConsoleDebug(...args); };
|
|
118
149
|
// Load configuration
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
150
|
+
// --prebuilt: load as native ESM (no esbuild, no bundle-require) —
|
|
151
|
+
// intended for production where the config has been compiled to dist/.
|
|
152
|
+
const { mod } = flags.prebuilt
|
|
153
|
+
? { mod: await import(absolutePath.startsWith('/') ? `file://${absolutePath}` : absolutePath) }
|
|
154
|
+
: await bundleRequire({ filepath: absolutePath });
|
|
155
|
+
let config = mod.default || mod;
|
|
123
156
|
if (!config) {
|
|
124
157
|
throw new Error(`No default export found in ${args.config}`);
|
|
125
158
|
}
|
|
159
|
+
// Preserve module-level named exports (e.g. `onEnable`, `onDisable`
|
|
160
|
+
// lifecycle hooks) that would otherwise be dropped when we unwrap
|
|
161
|
+
// `mod.default`. Without this AppPlugin can never invoke runtime hooks
|
|
162
|
+
// declared as `export const onEnable = ...` alongside the default
|
|
163
|
+
// `defineStack(...)` export.
|
|
164
|
+
if (mod.default != null && config !== mod) {
|
|
165
|
+
const merged = { ...config };
|
|
166
|
+
for (const key of Object.keys(mod)) {
|
|
167
|
+
if (key === 'default' || key in merged)
|
|
168
|
+
continue;
|
|
169
|
+
merged[key] = mod[key];
|
|
170
|
+
}
|
|
171
|
+
config = merged;
|
|
172
|
+
}
|
|
173
|
+
// Boot-mode dispatch: standalone goes directly through
|
|
174
|
+
// `@objectstack/runtime` (no cloud dependencies). runtime/cloud
|
|
175
|
+
// modes go through `@objectstack/service-cloud`.
|
|
176
|
+
if (shouldBootWithLibrary(config)) {
|
|
177
|
+
// The boot stack returns only `{plugins, api}` — preserve the
|
|
178
|
+
// original stack metadata (notably `requires`, `analyticsCubes`,
|
|
179
|
+
// `tiers`) so the capability resolver further down can read it.
|
|
180
|
+
const originalConfig = config;
|
|
181
|
+
const resolvedMode = config.bootMode ?? process.env.OS_MODE ?? 'standalone';
|
|
182
|
+
if (resolvedMode === 'standalone') {
|
|
183
|
+
const { createStandaloneStack } = await import('@objectstack/runtime');
|
|
184
|
+
const bootResult = await createStandaloneStack(config.standalone);
|
|
185
|
+
config = { ...originalConfig, ...bootResult };
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
const { createBootStack } = await import('@objectstack/service-cloud');
|
|
189
|
+
const bootResult = await createBootStack({
|
|
190
|
+
mode: config.bootMode,
|
|
191
|
+
runtime: config.runtime ?? config.project,
|
|
192
|
+
cloud: config.cloud,
|
|
193
|
+
});
|
|
194
|
+
config = { ...originalConfig, ...bootResult };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// ── Resolve plugin tiers ──────────────────────────────────────
|
|
198
|
+
// Precedence: config.requires (capability declarations) >
|
|
199
|
+
// config.tiers > --preset > built-in default.
|
|
200
|
+
//
|
|
201
|
+
// `requires: ['ai', 'automation', ...]` is the recommended
|
|
202
|
+
// app-level way to declare platform dependencies. The CLI
|
|
203
|
+
// expands each capability name into the matching tier so the
|
|
204
|
+
// optional auto-registration blocks below light up without
|
|
205
|
+
// extra flags. Explicitly-listed `config.plugins` always load
|
|
206
|
+
// and shadow any capability resolution (i.e. an explicit
|
|
207
|
+
// instance wins over the auto-loader).
|
|
208
|
+
const presetName = flags.preset ?? (isDev ? 'default' : 'default');
|
|
209
|
+
const presetTiers = Serve.TIER_PRESETS[presetName] ?? Serve.TIER_PRESETS.default;
|
|
210
|
+
const requires = Array.isArray(config.requires)
|
|
211
|
+
? config.requires.filter((c) => typeof c === 'string')
|
|
212
|
+
: [];
|
|
213
|
+
// Capability → tier: any capability that is gated by a tier
|
|
214
|
+
// here automatically opens that tier when listed in `requires`.
|
|
215
|
+
// Capabilities NOT in this map (e.g. `automation`, `analytics`,
|
|
216
|
+
// `audit`) bypass tier gating and are loaded directly by the
|
|
217
|
+
// capability-resolver block further down.
|
|
218
|
+
const CAPABILITY_TO_TIER = {
|
|
219
|
+
ai: 'ai',
|
|
220
|
+
i18n: 'i18n',
|
|
221
|
+
ui: 'ui',
|
|
222
|
+
auth: 'auth',
|
|
223
|
+
};
|
|
224
|
+
const requiredTiers = requires
|
|
225
|
+
.map((c) => CAPABILITY_TO_TIER[c])
|
|
226
|
+
.filter((t) => typeof t === 'string');
|
|
227
|
+
const baseTiers = Array.isArray(config.tiers) && config.tiers.length > 0
|
|
228
|
+
? config.tiers
|
|
229
|
+
: presetTiers;
|
|
230
|
+
const tiers = new Set([...baseTiers, ...requiredTiers]);
|
|
231
|
+
const tierEnabled = (t) => tiers.has(t);
|
|
232
|
+
const requiresCapability = (c) => requires.includes(c);
|
|
126
233
|
// Import ObjectStack runtime
|
|
127
234
|
const { Runtime } = await import('@objectstack/runtime');
|
|
128
235
|
// Set kernel logger to 'silent' — the CLI manages its own output
|
|
@@ -151,24 +258,120 @@ export default class Serve extends Command {
|
|
|
151
258
|
// silent
|
|
152
259
|
}
|
|
153
260
|
}
|
|
154
|
-
// 2. Auto-register
|
|
261
|
+
// 2. Auto-register storage driver
|
|
262
|
+
// Priority:
|
|
263
|
+
// 1. OS_DATABASE_DRIVER env var (explicit override)
|
|
264
|
+
// 2. URL scheme inferred from OS_DATABASE_URL
|
|
265
|
+
// mongodb://, mongodb+srv:// → mongodb
|
|
266
|
+
// postgres://, postgresql:// → postgres
|
|
267
|
+
// mysql://, mysql2:// → mysql
|
|
268
|
+
// libsql://, http(s):// + .turso. → turso
|
|
269
|
+
// file:, sqlite:, *.db, :memory: → sqlite
|
|
270
|
+
// 3. Default: InMemoryDriver in dev mode
|
|
155
271
|
const hasDriver = plugins.some((p) => p.name?.includes('driver') || p.constructor?.name?.includes('Driver'));
|
|
156
|
-
if (
|
|
272
|
+
if (!hasDriver && config.objects) {
|
|
273
|
+
const explicitDriver = (process.env.OS_DATABASE_DRIVER ?? '').toLowerCase().trim();
|
|
274
|
+
const databaseUrl = process.env.OS_DATABASE_URL;
|
|
275
|
+
const inferDriverFromUrl = (url) => {
|
|
276
|
+
if (!url)
|
|
277
|
+
return '';
|
|
278
|
+
const u = url.trim();
|
|
279
|
+
if (/^mongodb(\+srv)?:\/\//i.test(u))
|
|
280
|
+
return 'mongodb';
|
|
281
|
+
if (/^postgres(ql)?:\/\//i.test(u))
|
|
282
|
+
return 'postgres';
|
|
283
|
+
if (/^mysql2?:\/\//i.test(u))
|
|
284
|
+
return 'mysql';
|
|
285
|
+
if (/^libsql:\/\//i.test(u))
|
|
286
|
+
return 'turso';
|
|
287
|
+
if (/^https?:\/\//i.test(u) && /\.turso\./i.test(u))
|
|
288
|
+
return 'turso';
|
|
289
|
+
if (/^file:/i.test(u) || /^sqlite:/i.test(u) || u === ':memory:' || /\.(db|sqlite|sqlite3)$/i.test(u))
|
|
290
|
+
return 'sqlite';
|
|
291
|
+
return '';
|
|
292
|
+
};
|
|
293
|
+
const driverType = explicitDriver || inferDriverFromUrl(databaseUrl);
|
|
157
294
|
try {
|
|
158
295
|
const { DriverPlugin } = await import('@objectstack/runtime');
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
296
|
+
if (driverType === 'mongodb' || driverType === 'mongo') {
|
|
297
|
+
const { MongoDBDriver } = await import('@objectstack/driver-mongodb');
|
|
298
|
+
await kernel.use(new DriverPlugin(new MongoDBDriver({
|
|
299
|
+
url: databaseUrl ?? 'mongodb://localhost:27017/objectstack',
|
|
300
|
+
})));
|
|
301
|
+
trackPlugin('MongoDBDriver');
|
|
302
|
+
resolvedDriverLabel = 'MongoDBDriver';
|
|
303
|
+
resolvedDatabaseUrl = databaseUrl ?? 'mongodb://localhost:27017/objectstack';
|
|
304
|
+
}
|
|
305
|
+
else if (driverType === 'sqlite' || driverType === 'sql') {
|
|
306
|
+
const { SqlDriver } = await import('@objectstack/driver-sql');
|
|
307
|
+
const filePath = (databaseUrl ?? ':memory:').replace(/^file:/, '').replace(/^sqlite:/, '').replace(/^sql:\/\//, '');
|
|
308
|
+
await kernel.use(new DriverPlugin(new SqlDriver({
|
|
309
|
+
client: 'better-sqlite3',
|
|
310
|
+
connection: { filename: filePath },
|
|
311
|
+
useNullAsDefault: true,
|
|
312
|
+
})));
|
|
313
|
+
trackPlugin('SqlDriver');
|
|
314
|
+
resolvedDriverLabel = 'SqlDriver(sqlite)';
|
|
315
|
+
resolvedDatabaseUrl = databaseUrl ?? ':memory:';
|
|
316
|
+
}
|
|
317
|
+
else if (driverType === 'postgres' || driverType === 'postgresql' || driverType === 'pg') {
|
|
318
|
+
const { SqlDriver } = await import('@objectstack/driver-sql');
|
|
319
|
+
await kernel.use(new DriverPlugin(new SqlDriver({
|
|
320
|
+
client: 'pg',
|
|
321
|
+
connection: databaseUrl,
|
|
322
|
+
pool: { min: 0, max: 5 },
|
|
323
|
+
})));
|
|
324
|
+
trackPlugin('PostgresDriver');
|
|
325
|
+
resolvedDriverLabel = 'SqlDriver(pg)';
|
|
326
|
+
resolvedDatabaseUrl = databaseUrl;
|
|
327
|
+
}
|
|
328
|
+
else if (driverType === 'mysql' || driverType === 'mysql2') {
|
|
329
|
+
const { SqlDriver } = await import('@objectstack/driver-sql');
|
|
330
|
+
await kernel.use(new DriverPlugin(new SqlDriver({
|
|
331
|
+
client: 'mysql2',
|
|
332
|
+
connection: databaseUrl,
|
|
333
|
+
pool: { min: 0, max: 5 },
|
|
334
|
+
})));
|
|
335
|
+
trackPlugin('MySQLDriver');
|
|
336
|
+
resolvedDriverLabel = 'SqlDriver(mysql2)';
|
|
337
|
+
resolvedDatabaseUrl = databaseUrl;
|
|
338
|
+
}
|
|
339
|
+
else if (driverType === 'turso' || driverType === 'libsql') {
|
|
340
|
+
const { TursoDriver } = await import('@objectstack/driver-turso');
|
|
341
|
+
await kernel.use(new DriverPlugin(new TursoDriver({
|
|
342
|
+
url: databaseUrl ?? 'file:./local.db',
|
|
343
|
+
authToken: process.env.OS_DATABASE_AUTH_TOKEN,
|
|
344
|
+
})));
|
|
345
|
+
trackPlugin('TursoDriver');
|
|
346
|
+
resolvedDriverLabel = 'TursoDriver';
|
|
347
|
+
resolvedDatabaseUrl = databaseUrl ?? 'file:./local.db';
|
|
348
|
+
}
|
|
349
|
+
else if (isDev) {
|
|
350
|
+
// Default in dev: in-memory driver
|
|
351
|
+
const { InMemoryDriver } = await import('@objectstack/driver-memory');
|
|
352
|
+
await kernel.use(new DriverPlugin(new InMemoryDriver()));
|
|
353
|
+
trackPlugin('MemoryDriver');
|
|
354
|
+
resolvedDriverLabel = 'InMemoryDriver';
|
|
355
|
+
resolvedDatabaseUrl = '(in-memory)';
|
|
356
|
+
}
|
|
162
357
|
}
|
|
163
358
|
catch (e) {
|
|
164
359
|
// silent
|
|
165
360
|
}
|
|
166
361
|
}
|
|
167
362
|
// 3. Auto-register AppPlugin if config contains app definitions
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
|
|
363
|
+
// (objects / manifest / apps / flows / apis). Even host/aggregator
|
|
364
|
+
// configs (those whose `plugins` array contains instantiated plugins)
|
|
365
|
+
// need this wrap when they ALSO carry top-level metadata — otherwise
|
|
366
|
+
// top-level `flows`, `objects`, etc. never reach the ObjectQL registry
|
|
367
|
+
// and downstream services like AutomationServicePlugin start with 0 flows.
|
|
368
|
+
//
|
|
369
|
+
// To avoid double-registration when the host already wraps itself with
|
|
370
|
+
// an AppPlugin (e.g. apps/objectos's dev-workspace stack), we skip if
|
|
371
|
+
// any plugin in `plugins[]` is already an AppPlugin instance.
|
|
372
|
+
const hasAppPluginAlready = plugins.some((p) => p && (p.type === 'app' || p.constructor?.name === 'AppPlugin' || (p.name && typeof p.name === 'string' && p.name.startsWith('plugin.app.'))));
|
|
373
|
+
const configHasMetadata = !!(config.objects || config.manifest || config.apps || config.flows || config.apis);
|
|
374
|
+
if (!hasAppPluginAlready && configHasMetadata) {
|
|
172
375
|
try {
|
|
173
376
|
const { AppPlugin } = await import('@objectstack/runtime');
|
|
174
377
|
await kernel.use(new AppPlugin(config));
|
|
@@ -182,11 +385,33 @@ export default class Serve extends Command {
|
|
|
182
385
|
// This ensures i18n REST routes work out of the box without manual plugin registration.
|
|
183
386
|
const hasI18nPlugin = plugins.some((p) => p.name === 'com.objectstack.service.i18n'
|
|
184
387
|
|| p.constructor?.name === 'I18nServicePlugin');
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
388
|
+
// Check the top-level config AND any nested AppPlugin bundles in the
|
|
389
|
+
// `plugins` array — host/aggregator configs (e.g. apps/objectos) don't
|
|
390
|
+
// define translations themselves but compose multiple `new AppPlugin(...)`
|
|
391
|
+
// entries, each carrying its own translations.
|
|
392
|
+
const pluginBundleHasTranslations = (bundle) => {
|
|
393
|
+
if (!bundle || typeof bundle !== 'object')
|
|
394
|
+
return false;
|
|
395
|
+
if (Array.isArray(bundle.translations) && bundle.translations.length > 0)
|
|
396
|
+
return true;
|
|
397
|
+
if (bundle.i18n)
|
|
398
|
+
return true;
|
|
399
|
+
if (bundle.manifest && ((Array.isArray(bundle.manifest.translations) && bundle.manifest.translations.length > 0)
|
|
400
|
+
|| bundle.manifest.i18n))
|
|
401
|
+
return true;
|
|
402
|
+
return false;
|
|
403
|
+
};
|
|
404
|
+
const anyAppPluginHasTranslations = plugins.some((p) => {
|
|
405
|
+
if (!p)
|
|
406
|
+
return false;
|
|
407
|
+
// AppPlugin instances expose their bundle on `.bundle`
|
|
408
|
+
if (p.bundle && pluginBundleHasTranslations(p.bundle))
|
|
409
|
+
return true;
|
|
410
|
+
return false;
|
|
411
|
+
});
|
|
412
|
+
const configHasTranslations = (pluginBundleHasTranslations(config)
|
|
413
|
+
|| anyAppPluginHasTranslations);
|
|
414
|
+
if (!hasI18nPlugin && configHasTranslations && tierEnabled('i18n')) {
|
|
190
415
|
try {
|
|
191
416
|
// Dynamic import with variable to prevent tsc from resolving the optional package
|
|
192
417
|
const i18nPkg = '@objectstack/service-i18n';
|
|
@@ -222,18 +447,108 @@ export default class Serve extends Command {
|
|
|
222
447
|
console.warn(chalk.yellow(` ⚠ HTTP server plugin not available: ${e.message}`));
|
|
223
448
|
}
|
|
224
449
|
}
|
|
225
|
-
// 5. Auto-register
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
|
|
450
|
+
// 5. Auto-register Studio single-project signal in dev mode.
|
|
451
|
+
//
|
|
452
|
+
// `objectstack dev` runs a vanilla user stack (e.g. examples/app-crm)
|
|
453
|
+
// as a single project — there is no apps/cloud control plane and no
|
|
454
|
+
// org/project picker is meaningful. Without this plugin Studio would
|
|
455
|
+
// fall back to its multi-project default and ask the user to "Create
|
|
456
|
+
// organization" before showing any platform metadata.
|
|
457
|
+
//
|
|
458
|
+
// The plugin only registers `GET /api/v1/studio/runtime-config`
|
|
459
|
+
// (returning `{ singleProject: true, defaultOrgId, defaultProjectId }`)
|
|
460
|
+
// — no identity seed, since CLI dev mode has no sys_organization /
|
|
461
|
+
// sys_project tables to write into. Skipped when the user config
|
|
462
|
+
// already carries a single-project / multi-project plugin.
|
|
463
|
+
const hasProjectModePlugin = plugins.some((p) => {
|
|
464
|
+
const n = p?.name ?? p?.constructor?.name ?? '';
|
|
465
|
+
return n === 'com.objectstack.studio.single-project'
|
|
466
|
+
|| n === 'com.objectstack.multi-project'
|
|
467
|
+
|| n === 'com.objectstack.studio.runtime-config';
|
|
468
|
+
});
|
|
469
|
+
if (isDev && !hasProjectModePlugin) {
|
|
229
470
|
try {
|
|
230
|
-
const
|
|
231
|
-
const {
|
|
232
|
-
await kernel.use(
|
|
233
|
-
|
|
471
|
+
const cloudPkg = '@objectstack/service-cloud';
|
|
472
|
+
const { createSingleProjectPlugin } = await import(/* webpackIgnore: true */ cloudPkg);
|
|
473
|
+
await kernel.use(createSingleProjectPlugin({
|
|
474
|
+
projectId: process.env.OS_PROJECT_ID ?? 'proj_local',
|
|
475
|
+
orgId: process.env.OS_ORG_ID ?? 'org_local',
|
|
476
|
+
orgName: 'Local',
|
|
477
|
+
}));
|
|
478
|
+
trackPlugin('SingleProject');
|
|
234
479
|
}
|
|
235
480
|
catch {
|
|
236
|
-
// @objectstack/
|
|
481
|
+
// @objectstack/service-cloud not installed — Studio falls back
|
|
482
|
+
// to multi-project mode (org/project picker visible).
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
// 5b. Auto-register AuthPlugin (and paired Security/Audit) when the
|
|
486
|
+
// 'auth' tier is enabled and no auth plugin is already configured.
|
|
487
|
+
// The Studio + Account portals expect /api/v1/auth/* to be served by
|
|
488
|
+
// better-auth via @objectstack/plugin-auth. Without this block,
|
|
489
|
+
// running `objectstack dev` on a vanilla user stack would 404 on
|
|
490
|
+
// login/register flows.
|
|
491
|
+
const hasAuthPlugin = plugins.some((p) => p?.name === 'com.objectstack.auth' || p?.constructor?.name === 'AuthPlugin');
|
|
492
|
+
if (!hasAuthPlugin && tierEnabled('auth')) {
|
|
493
|
+
try {
|
|
494
|
+
const authPkg = '@objectstack/plugin-auth';
|
|
495
|
+
const { AuthPlugin } = await import(/* webpackIgnore: true */ authPkg);
|
|
496
|
+
// In dev, fall back to a stable local secret so users don't have
|
|
497
|
+
// to set AUTH_SECRET just to try the login/register flow.
|
|
498
|
+
const secret = process.env.AUTH_SECRET
|
|
499
|
+
?? process.env.OS_AUTH_SECRET
|
|
500
|
+
?? (isDev ? 'dev-only-insecure-secret-change-me-in-production' : undefined);
|
|
501
|
+
if (!secret) {
|
|
502
|
+
console.warn(chalk.yellow(' ⚠ AuthPlugin skipped — set AUTH_SECRET to enable authentication in production'));
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
const baseUrl = process.env.AUTH_BASE_URL
|
|
506
|
+
?? process.env.OS_BASE_URL
|
|
507
|
+
?? `http://localhost:${port}`;
|
|
508
|
+
const socialProviders = {};
|
|
509
|
+
if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET)
|
|
510
|
+
socialProviders.google = { clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET };
|
|
511
|
+
if (process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET)
|
|
512
|
+
socialProviders.github = { clientId: process.env.GITHUB_CLIENT_ID, clientSecret: process.env.GITHUB_CLIENT_SECRET };
|
|
513
|
+
await kernel.use(new AuthPlugin({
|
|
514
|
+
secret,
|
|
515
|
+
baseUrl,
|
|
516
|
+
socialProviders: Object.keys(socialProviders).length > 0 ? socialProviders : undefined,
|
|
517
|
+
}));
|
|
518
|
+
trackPlugin('Auth');
|
|
519
|
+
// Pair: SecurityPlugin (RBAC) — optional
|
|
520
|
+
try {
|
|
521
|
+
const securityPkg = '@objectstack/plugin-security';
|
|
522
|
+
const { SecurityPlugin } = await import(/* webpackIgnore: true */ securityPkg);
|
|
523
|
+
// `OS_MULTI_TENANT=false` disables wildcard tenant_isolation
|
|
524
|
+
// RLS policies and the `organization_id` auto-injection on
|
|
525
|
+
// insert. Keep multi-tenant on by default — most ObjectStack
|
|
526
|
+
// deployments are multi-org.
|
|
527
|
+
const multiTenant = String(process.env.OS_MULTI_TENANT ?? 'true').toLowerCase() !== 'false';
|
|
528
|
+
await kernel.use(new SecurityPlugin({ multiTenant }));
|
|
529
|
+
trackPlugin('Security');
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
// optional
|
|
533
|
+
}
|
|
534
|
+
// Pair: AuditPlugin — optional
|
|
535
|
+
try {
|
|
536
|
+
const auditPkg = '@objectstack/plugin-audit';
|
|
537
|
+
const { AuditPlugin } = await import(/* webpackIgnore: true */ auditPkg);
|
|
538
|
+
await kernel.use(new AuditPlugin());
|
|
539
|
+
trackPlugin('Audit');
|
|
540
|
+
}
|
|
541
|
+
catch {
|
|
542
|
+
// optional
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
catch (err) {
|
|
547
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
548
|
+
if (!msg.includes('Cannot find module') && !msg.includes('ERR_MODULE_NOT_FOUND')) {
|
|
549
|
+
console.warn(chalk.yellow(` ⚠ AuthPlugin failed to load: ${msg}`));
|
|
550
|
+
}
|
|
551
|
+
// @objectstack/plugin-auth not installed — login/register endpoints unavailable
|
|
237
552
|
}
|
|
238
553
|
}
|
|
239
554
|
if (plugins.length > 0) {
|
|
@@ -272,9 +587,16 @@ export default class Serve extends Command {
|
|
|
272
587
|
}
|
|
273
588
|
// Register REST API and Dispatcher plugins (consume http.server + protocol services)
|
|
274
589
|
if (flags.server) {
|
|
590
|
+
// Read project-scoping config from the stack's top-level `api` field
|
|
591
|
+
// (e.g. { api: { enableProjectScoping: true, projectResolution: 'auto' } }).
|
|
592
|
+
// Forwarded to both REST and Dispatcher plugins so they mount scoped
|
|
593
|
+
// routes consistently.
|
|
594
|
+
const apiConfig = config.api ?? {};
|
|
595
|
+
const enableProjectScoping = apiConfig.enableProjectScoping ?? false;
|
|
596
|
+
const projectResolution = apiConfig.projectResolution ?? 'auto';
|
|
275
597
|
try {
|
|
276
598
|
const { createRestApiPlugin } = await import('@objectstack/rest');
|
|
277
|
-
await kernel.use(createRestApiPlugin());
|
|
599
|
+
await kernel.use(createRestApiPlugin({ api: { api: { enableProjectScoping, projectResolution } } }));
|
|
278
600
|
trackPlugin('RestAPI');
|
|
279
601
|
}
|
|
280
602
|
catch (e) {
|
|
@@ -283,7 +605,7 @@ export default class Serve extends Command {
|
|
|
283
605
|
// Register Dispatcher plugin (auth, graphql, analytics, packages, hub, storage, automation)
|
|
284
606
|
try {
|
|
285
607
|
const { createDispatcherPlugin } = await import('@objectstack/runtime');
|
|
286
|
-
await kernel.use(createDispatcherPlugin());
|
|
608
|
+
await kernel.use(createDispatcherPlugin({ scoping: { enableProjectScoping, projectResolution } }));
|
|
287
609
|
trackPlugin('Dispatcher');
|
|
288
610
|
}
|
|
289
611
|
catch (e) {
|
|
@@ -295,7 +617,7 @@ export default class Serve extends Command {
|
|
|
295
617
|
// already in place when AIServicePlugin.start() fires the hook.
|
|
296
618
|
const hasAIPlugin = plugins.some((p) => p.name === 'com.objectstack.service-ai'
|
|
297
619
|
|| p.constructor?.name === 'AIServicePlugin');
|
|
298
|
-
if (!hasAIPlugin) {
|
|
620
|
+
if (!hasAIPlugin && tierEnabled('ai')) {
|
|
299
621
|
try {
|
|
300
622
|
const aiPkg = '@objectstack/service-ai';
|
|
301
623
|
const { AIServicePlugin } = await import(/* webpackIgnore: true */ aiPkg);
|
|
@@ -305,33 +627,216 @@ export default class Serve extends Command {
|
|
|
305
627
|
await kernel.use(new AIServicePlugin());
|
|
306
628
|
trackPlugin('AIService');
|
|
307
629
|
}
|
|
308
|
-
catch {
|
|
630
|
+
catch (err) {
|
|
631
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
632
|
+
if (!msg.includes('Cannot find module') && !msg.includes('ERR_MODULE_NOT_FOUND')) {
|
|
633
|
+
console.error('[AI] AIServicePlugin failed to start:', msg);
|
|
634
|
+
}
|
|
309
635
|
// @objectstack/service-ai not installed — AI features unavailable
|
|
310
636
|
}
|
|
311
637
|
}
|
|
638
|
+
const CAPABILITY_PROVIDERS = {
|
|
639
|
+
automation: {
|
|
640
|
+
pkg: '@objectstack/service-automation',
|
|
641
|
+
export: 'AutomationServicePlugin',
|
|
642
|
+
nameMatch: ['service-automation', 'AutomationServicePlugin'],
|
|
643
|
+
// The default node packs ship from the same package; auto-register them
|
|
644
|
+
// so flows actually have executors. Users can opt out by listing
|
|
645
|
+
// their own subset explicitly in `plugins: []` (which sets
|
|
646
|
+
// `nameMatch` to skip these auto-loads).
|
|
647
|
+
extras: [
|
|
648
|
+
{ pkg: '@objectstack/service-automation', export: 'CrudNodesPlugin', nameMatch: ['crud-nodes', 'CrudNodesPlugin'] },
|
|
649
|
+
{ pkg: '@objectstack/service-automation', export: 'LogicNodesPlugin', nameMatch: ['logic-nodes', 'LogicNodesPlugin'] },
|
|
650
|
+
{ pkg: '@objectstack/service-automation', export: 'HttpConnectorPlugin', nameMatch: ['http-connector', 'HttpConnectorPlugin'] },
|
|
651
|
+
{ pkg: '@objectstack/service-automation', export: 'ScreenNodesPlugin', nameMatch: ['screen-nodes', 'ScreenNodesPlugin'] },
|
|
652
|
+
],
|
|
653
|
+
},
|
|
654
|
+
analytics: {
|
|
655
|
+
pkg: '@objectstack/service-analytics',
|
|
656
|
+
export: 'AnalyticsServicePlugin',
|
|
657
|
+
nameMatch: ['service-analytics', 'AnalyticsServicePlugin'],
|
|
658
|
+
configKey: 'analyticsCubes',
|
|
659
|
+
},
|
|
660
|
+
audit: {
|
|
661
|
+
pkg: '@objectstack/plugin-audit',
|
|
662
|
+
export: 'AuditPlugin',
|
|
663
|
+
nameMatch: ['audit', 'AuditPlugin'],
|
|
664
|
+
},
|
|
665
|
+
cache: {
|
|
666
|
+
pkg: '@objectstack/service-cache',
|
|
667
|
+
export: 'CacheServicePlugin',
|
|
668
|
+
nameMatch: ['service-cache', 'CacheServicePlugin'],
|
|
669
|
+
},
|
|
670
|
+
storage: {
|
|
671
|
+
pkg: '@objectstack/service-storage',
|
|
672
|
+
export: 'StorageServicePlugin',
|
|
673
|
+
nameMatch: ['service-storage', 'StorageServicePlugin'],
|
|
674
|
+
},
|
|
675
|
+
queue: {
|
|
676
|
+
pkg: '@objectstack/service-queue',
|
|
677
|
+
export: 'QueueServicePlugin',
|
|
678
|
+
nameMatch: ['service-queue', 'QueueServicePlugin'],
|
|
679
|
+
},
|
|
680
|
+
job: {
|
|
681
|
+
pkg: '@objectstack/service-job',
|
|
682
|
+
export: 'JobServicePlugin',
|
|
683
|
+
nameMatch: ['service-job', 'JobServicePlugin'],
|
|
684
|
+
},
|
|
685
|
+
realtime: {
|
|
686
|
+
pkg: '@objectstack/service-realtime',
|
|
687
|
+
export: 'RealtimeServicePlugin',
|
|
688
|
+
nameMatch: ['service-realtime', 'RealtimeServicePlugin'],
|
|
689
|
+
},
|
|
690
|
+
feed: {
|
|
691
|
+
pkg: '@objectstack/service-feed',
|
|
692
|
+
export: 'FeedServicePlugin',
|
|
693
|
+
nameMatch: ['service-feed', 'FeedServicePlugin'],
|
|
694
|
+
},
|
|
695
|
+
mcp: {
|
|
696
|
+
pkg: '@objectstack/plugin-mcp-server',
|
|
697
|
+
export: 'MCPServerPlugin',
|
|
698
|
+
nameMatch: ['mcp-server', 'MCPServerPlugin'],
|
|
699
|
+
},
|
|
700
|
+
marketplace: {
|
|
701
|
+
pkg: '@objectstack/service-package',
|
|
702
|
+
export: 'PackageServicePlugin',
|
|
703
|
+
nameMatch: ['service-package', 'PackageServicePlugin'],
|
|
704
|
+
},
|
|
705
|
+
};
|
|
706
|
+
const hasPluginMatching = (fragments) => plugins.some((p) => {
|
|
707
|
+
const n = String(p?.name ?? '');
|
|
708
|
+
const c = String(p?.constructor?.name ?? '');
|
|
709
|
+
return fragments.some((f) => n.includes(f) || c.includes(f));
|
|
710
|
+
});
|
|
711
|
+
for (const cap of requires) {
|
|
712
|
+
const spec = CAPABILITY_PROVIDERS[cap];
|
|
713
|
+
if (!spec)
|
|
714
|
+
continue; // tier-gated capabilities (ai/i18n/ui/auth) handled above
|
|
715
|
+
if (hasPluginMatching(spec.nameMatch))
|
|
716
|
+
continue;
|
|
717
|
+
try {
|
|
718
|
+
const mod = await import(/* webpackIgnore: true */ spec.pkg);
|
|
719
|
+
const Ctor = mod[spec.export];
|
|
720
|
+
if (!Ctor) {
|
|
721
|
+
console.warn(chalk.yellow(` ⚠ Capability "${cap}": ${spec.pkg} did not export ${spec.export}`));
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
// analytics needs cubes from config, others take no args
|
|
725
|
+
let arg;
|
|
726
|
+
if (spec.configKey === 'analyticsCubes') {
|
|
727
|
+
const cubes = config.analyticsCubes ?? config.cubes ?? [];
|
|
728
|
+
arg = { cubes };
|
|
729
|
+
}
|
|
730
|
+
await kernel.use(arg !== undefined ? new Ctor(arg) : new Ctor());
|
|
731
|
+
trackPlugin(spec.export);
|
|
732
|
+
if (spec.extras) {
|
|
733
|
+
for (const ex of spec.extras) {
|
|
734
|
+
if (hasPluginMatching(ex.nameMatch))
|
|
735
|
+
continue;
|
|
736
|
+
try {
|
|
737
|
+
const exMod = await import(/* webpackIgnore: true */ ex.pkg);
|
|
738
|
+
const ExCtor = exMod[ex.export];
|
|
739
|
+
if (ExCtor) {
|
|
740
|
+
await kernel.use(new ExCtor());
|
|
741
|
+
trackPlugin(ex.export);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
catch {
|
|
745
|
+
// optional extra — silently skip
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
catch (err) {
|
|
751
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
752
|
+
if (!msg.includes('Cannot find module') && !msg.includes('ERR_MODULE_NOT_FOUND')) {
|
|
753
|
+
console.error(`[Capability:${cap}] failed to load ${spec.pkg}: ${msg}`);
|
|
754
|
+
}
|
|
755
|
+
else {
|
|
756
|
+
console.warn(chalk.yellow(` ⚠ Capability "${cap}" required but ${spec.pkg} is not installed`));
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
312
760
|
// ── Studio UI ─────────────────────────────────────────────────
|
|
313
761
|
// In dev mode, Studio UI is enabled by default (use --no-ui to disable).
|
|
314
762
|
// Always serves the pre-built dist/ — no Vite dev server, no extra port.
|
|
315
|
-
const enableUI = flags.ui
|
|
763
|
+
const enableUI = flags.ui && tierEnabled('ui');
|
|
316
764
|
if (enableUI) {
|
|
765
|
+
// Pre-detect Console availability so we can demote Studio's root
|
|
766
|
+
// redirect when the Console is going to claim `/`.
|
|
767
|
+
const consolePath = resolveConsolePath();
|
|
768
|
+
const consoleWillMount = !!(consolePath && hasConsoleDist(consolePath));
|
|
317
769
|
const studioPath = resolveStudioPath();
|
|
318
770
|
if (!studioPath) {
|
|
319
771
|
console.warn(chalk.yellow(` ⚠ @objectstack/studio not found — skipping UI`));
|
|
320
772
|
}
|
|
321
773
|
else if (hasStudioDist(studioPath)) {
|
|
322
774
|
const distPath = path.join(studioPath, 'dist');
|
|
323
|
-
await kernel.use(createStudioStaticPlugin(distPath, {
|
|
775
|
+
await kernel.use(createStudioStaticPlugin(distPath, {
|
|
776
|
+
isDev,
|
|
777
|
+
rootRedirect: !consoleWillMount,
|
|
778
|
+
}));
|
|
324
779
|
trackPlugin('StudioUI');
|
|
325
780
|
}
|
|
326
781
|
else {
|
|
327
782
|
console.warn(chalk.yellow(` ⚠ Studio dist not found — run "pnpm --filter @objectstack/studio build" first`));
|
|
328
783
|
}
|
|
784
|
+
// ── Account portal ─────────────────────────────────────────
|
|
785
|
+
// The account portal sits next to Studio under `/_account/` and
|
|
786
|
+
// follows the same enable rules — it's a self-service surface
|
|
787
|
+
// for end-users (login, organizations, profile, sessions).
|
|
788
|
+
const accountPath = resolveAccountPath();
|
|
789
|
+
if (!accountPath) {
|
|
790
|
+
console.warn(chalk.yellow(` ⚠ @objectstack/account not found — skipping Account UI`));
|
|
791
|
+
}
|
|
792
|
+
else if (hasAccountDist(accountPath)) {
|
|
793
|
+
const accountDistPath = path.join(accountPath, 'dist');
|
|
794
|
+
await kernel.use(createAccountStaticPlugin(accountDistPath, { isDev }));
|
|
795
|
+
trackPlugin('AccountUI');
|
|
796
|
+
}
|
|
797
|
+
else {
|
|
798
|
+
console.warn(chalk.yellow(` ⚠ Account dist not found — run "pnpm --filter @objectstack/account build" first`));
|
|
799
|
+
}
|
|
800
|
+
// ── Console portal ──────────────────────────────────────────
|
|
801
|
+
// The opinionated, fork-ready runtime console (`@objectstack/console`)
|
|
802
|
+
// mounts under `/_console/` exactly like Studio/Account. When
|
|
803
|
+
// present, it owns root `/` redirect (preferred default UI). It
|
|
804
|
+
// is optional — we only mount it when the package resolves and
|
|
805
|
+
// a pre-built `dist/` is present.
|
|
806
|
+
if (consolePath) {
|
|
807
|
+
if (consoleWillMount) {
|
|
808
|
+
const consoleDistPath = path.join(consolePath, 'dist');
|
|
809
|
+
await kernel.use(createConsoleStaticPlugin(consoleDistPath, { isDev }));
|
|
810
|
+
trackPlugin('ConsoleUI');
|
|
811
|
+
}
|
|
812
|
+
else {
|
|
813
|
+
console.warn(chalk.yellow(` ⚠ Console dist not found — run "pnpm --filter @objectstack/console build" first`));
|
|
814
|
+
}
|
|
815
|
+
}
|
|
329
816
|
}
|
|
330
817
|
// Boot the runtime
|
|
331
818
|
await runtime.start();
|
|
332
|
-
//
|
|
819
|
+
// Brief delay to allow logger writes to flush before restoring stdout
|
|
333
820
|
await new Promise(r => setTimeout(r, 100));
|
|
334
821
|
restoreOutput();
|
|
822
|
+
// ── Driver introspection ──────────────────────────────────────
|
|
823
|
+
// When the driver was registered by an app preset / per-project
|
|
824
|
+
// factory (ProjectKernelFactory) instead of serve.ts's own
|
|
825
|
+
// OS_DATABASE_URL fallback, `resolvedDriverLabel` is still
|
|
826
|
+
// unset. Probe well-known service names so the banner can show
|
|
827
|
+
// *something* useful regardless of who wired the driver.
|
|
828
|
+
if (!resolvedDriverLabel) {
|
|
829
|
+
try {
|
|
830
|
+
const probe = describeRegisteredDriver(kernel);
|
|
831
|
+
if (probe) {
|
|
832
|
+
resolvedDriverLabel = probe.label;
|
|
833
|
+
resolvedDatabaseUrl = probe.url;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
catch {
|
|
837
|
+
// best-effort only
|
|
838
|
+
}
|
|
839
|
+
}
|
|
335
840
|
// ── Clean startup summary ──────────────────────────────────────
|
|
336
841
|
printServerReady({
|
|
337
842
|
port,
|
|
@@ -341,14 +846,14 @@ export default class Serve extends Command {
|
|
|
341
846
|
pluginNames: loadedPlugins,
|
|
342
847
|
uiEnabled: enableUI,
|
|
343
848
|
studioPath: STUDIO_PATH,
|
|
849
|
+
accountPath: ACCOUNT_PATH,
|
|
850
|
+
consolePath: loadedPlugins.includes('ConsoleUI') ? CONSOLE_PATH : undefined,
|
|
851
|
+
driverLabel: resolvedDriverLabel,
|
|
852
|
+
databaseUrl: redactDbUrl(resolvedDatabaseUrl),
|
|
853
|
+
multiTenant: String(process.env.OS_MULTI_TENANT ?? 'true').toLowerCase() !== 'false',
|
|
344
854
|
});
|
|
345
|
-
//
|
|
346
|
-
|
|
347
|
-
console.warn(chalk.yellow(`\n\n⏹ Stopping server...`));
|
|
348
|
-
await runtime.getKernel().shutdown();
|
|
349
|
-
console.log(chalk.green(`✅ Server stopped`));
|
|
350
|
-
process.exit(0);
|
|
351
|
-
});
|
|
855
|
+
// Kernel already registers SIGINT/SIGTERM handlers during bootstrap.
|
|
856
|
+
// No duplicate handler needed here — just keep the process alive.
|
|
352
857
|
}
|
|
353
858
|
catch (error) {
|
|
354
859
|
restoreOutput();
|
|
@@ -360,4 +865,61 @@ export default class Serve extends Command {
|
|
|
360
865
|
}
|
|
361
866
|
}
|
|
362
867
|
}
|
|
868
|
+
/**
|
|
869
|
+
* Best-effort driver introspection.
|
|
870
|
+
*
|
|
871
|
+
* Drivers register themselves under the kernel service name
|
|
872
|
+
* `driver.{driver.name}` (see `DriverPlugin.init`). We probe a list of
|
|
873
|
+
* well-known names and return a single-line label + redacted URL so the
|
|
874
|
+
* startup banner can show *something* even when the driver wasn't
|
|
875
|
+
* registered through this command's own `OS_DATABASE_URL` fallback
|
|
876
|
+
* (e.g. when the example app's preset or `ProjectKernelFactory` wired
|
|
877
|
+
* it). Returns `null` when nothing matches; the caller treats that as
|
|
878
|
+
* "no driver info available" and skips the line.
|
|
879
|
+
*/
|
|
880
|
+
function describeRegisteredDriver(kernel) {
|
|
881
|
+
const candidates = [
|
|
882
|
+
'driver.com.objectstack.driver.sql',
|
|
883
|
+
'driver.com.objectstack.driver.mongodb',
|
|
884
|
+
'driver.com.objectstack.driver.turso',
|
|
885
|
+
'driver.com.objectstack.driver.memory',
|
|
886
|
+
'driver.sql', 'driver.mongodb', 'driver.turso', 'driver.memory',
|
|
887
|
+
];
|
|
888
|
+
for (const name of candidates) {
|
|
889
|
+
let driver;
|
|
890
|
+
try {
|
|
891
|
+
driver = kernel?.getService?.(name);
|
|
892
|
+
}
|
|
893
|
+
catch { /* not registered */ }
|
|
894
|
+
if (!driver)
|
|
895
|
+
continue;
|
|
896
|
+
// SqlDriver: `{ client, connection: string | { filename, host, ... } }`
|
|
897
|
+
const cfg = driver.config;
|
|
898
|
+
if (cfg) {
|
|
899
|
+
const client = cfg.client;
|
|
900
|
+
const conn = cfg.connection;
|
|
901
|
+
let url = '';
|
|
902
|
+
if (typeof conn === 'string') {
|
|
903
|
+
url = conn;
|
|
904
|
+
}
|
|
905
|
+
else if (conn && typeof conn === 'object') {
|
|
906
|
+
url = conn.filename
|
|
907
|
+
?? (conn.host ? `${conn.host}${conn.port ? `:${conn.port}` : ''}${conn.database ? `/${conn.database}` : ''}` : '');
|
|
908
|
+
}
|
|
909
|
+
const label = client ? `SqlDriver(${client})` : (driver.name ?? 'SqlDriver');
|
|
910
|
+
return { label, url: url || '(unknown)' };
|
|
911
|
+
}
|
|
912
|
+
// MongoDB / Turso drivers expose the URL on the instance itself.
|
|
913
|
+
if (driver.url) {
|
|
914
|
+
const label = driver.constructor?.name ?? driver.name ?? 'Driver';
|
|
915
|
+
return { label, url: String(driver.url) };
|
|
916
|
+
}
|
|
917
|
+
// InMemoryDriver — no URL.
|
|
918
|
+
return {
|
|
919
|
+
label: driver.constructor?.name ?? driver.name ?? 'Driver',
|
|
920
|
+
url: '(in-memory)',
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
return null;
|
|
924
|
+
}
|
|
363
925
|
//# sourceMappingURL=serve.js.map
|