@objectstack/cli 4.0.4 → 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.
Files changed (241) hide show
  1. package/README.md +12 -25
  2. package/dist/commands/build.d.ts +5 -0
  3. package/dist/commands/build.d.ts.map +1 -0
  4. package/dist/commands/build.js +6 -0
  5. package/dist/commands/build.js.map +1 -0
  6. package/dist/commands/compile.d.ts +3 -0
  7. package/dist/commands/compile.d.ts.map +1 -1
  8. package/dist/commands/compile.js +128 -6
  9. package/dist/commands/compile.js.map +1 -1
  10. package/dist/commands/create.js +1 -1
  11. package/dist/commands/data/create.js +2 -2
  12. package/dist/commands/data/create.js.map +1 -1
  13. package/dist/commands/data/delete.js +2 -2
  14. package/dist/commands/data/delete.js.map +1 -1
  15. package/dist/commands/data/get.js +2 -2
  16. package/dist/commands/data/get.js.map +1 -1
  17. package/dist/commands/data/query.js +2 -2
  18. package/dist/commands/data/query.js.map +1 -1
  19. package/dist/commands/data/update.js +2 -2
  20. package/dist/commands/data/update.js.map +1 -1
  21. package/dist/commands/dev.d.ts +3 -0
  22. package/dist/commands/dev.d.ts.map +1 -1
  23. package/dist/commands/dev.js +48 -19
  24. package/dist/commands/dev.js.map +1 -1
  25. package/dist/commands/generate.js +9 -9
  26. package/dist/commands/generate.js.map +1 -1
  27. package/dist/commands/i18n/check.d.ts +18 -0
  28. package/dist/commands/i18n/check.d.ts.map +1 -0
  29. package/dist/commands/i18n/check.js +153 -0
  30. package/dist/commands/i18n/check.js.map +1 -0
  31. package/dist/commands/init.js +2 -2
  32. package/dist/commands/lint.d.ts +3 -0
  33. package/dist/commands/lint.d.ts.map +1 -1
  34. package/dist/commands/lint.js +24 -0
  35. package/dist/commands/lint.js.map +1 -1
  36. package/dist/commands/login.d.ts +17 -0
  37. package/dist/commands/login.d.ts.map +1 -0
  38. package/dist/commands/login.js +313 -0
  39. package/dist/commands/login.js.map +1 -0
  40. package/dist/commands/logout.d.ts.map +1 -0
  41. package/dist/commands/{auth/logout.js → logout.js} +14 -2
  42. package/dist/commands/logout.js.map +1 -0
  43. package/dist/commands/meta/delete.js +2 -2
  44. package/dist/commands/meta/delete.js.map +1 -1
  45. package/dist/commands/meta/get.js +2 -2
  46. package/dist/commands/meta/get.js.map +1 -1
  47. package/dist/commands/meta/list.js +2 -2
  48. package/dist/commands/meta/list.js.map +1 -1
  49. package/dist/commands/meta/register.js +2 -2
  50. package/dist/commands/meta/register.js.map +1 -1
  51. package/dist/commands/projects/bind.d.ts +30 -0
  52. package/dist/commands/projects/bind.d.ts.map +1 -0
  53. package/dist/commands/projects/bind.js +132 -0
  54. package/dist/commands/projects/bind.js.map +1 -0
  55. package/dist/commands/projects/create.d.ts +28 -0
  56. package/dist/commands/projects/create.d.ts.map +1 -0
  57. package/dist/commands/projects/create.js +120 -0
  58. package/dist/commands/projects/create.js.map +1 -0
  59. package/dist/commands/projects/list.d.ts +21 -0
  60. package/dist/commands/projects/list.d.ts.map +1 -0
  61. package/dist/commands/projects/list.js +79 -0
  62. package/dist/commands/projects/list.js.map +1 -0
  63. package/dist/commands/projects/projects.test.d.ts +2 -0
  64. package/dist/commands/projects/projects.test.d.ts.map +1 -0
  65. package/dist/commands/projects/projects.test.js +56 -0
  66. package/dist/commands/projects/projects.test.js.map +1 -0
  67. package/dist/commands/projects/show.d.ts +21 -0
  68. package/dist/commands/projects/show.d.ts.map +1 -0
  69. package/dist/commands/projects/show.js +72 -0
  70. package/dist/commands/projects/show.js.map +1 -0
  71. package/dist/commands/projects/switch.d.ts +24 -0
  72. package/dist/commands/projects/switch.d.ts.map +1 -0
  73. package/dist/commands/projects/switch.js +64 -0
  74. package/dist/commands/projects/switch.js.map +1 -0
  75. package/dist/commands/publish.d.ts +14 -0
  76. package/dist/commands/publish.d.ts.map +1 -0
  77. package/dist/commands/publish.js +91 -0
  78. package/dist/commands/publish.js.map +1 -0
  79. package/dist/commands/{auth/login.d.ts → register.d.ts} +3 -2
  80. package/dist/commands/register.d.ts.map +1 -0
  81. package/dist/commands/{auth/login.js → register.js} +44 -61
  82. package/dist/commands/register.js.map +1 -0
  83. package/dist/commands/serve.d.ts +8 -0
  84. package/dist/commands/serve.d.ts.map +1 -1
  85. package/dist/commands/serve.js +606 -44
  86. package/dist/commands/serve.js.map +1 -1
  87. package/dist/commands/start.d.ts +11 -0
  88. package/dist/commands/start.d.ts.map +1 -0
  89. package/dist/commands/start.js +43 -0
  90. package/dist/commands/start.js.map +1 -0
  91. package/dist/commands/whoami.d.ts.map +1 -0
  92. package/dist/commands/{auth/whoami.js → whoami.js} +5 -5
  93. package/dist/commands/whoami.js.map +1 -0
  94. package/dist/index.d.ts +7 -4
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +8 -5
  97. package/dist/index.js.map +1 -1
  98. package/dist/utils/account.d.ts +31 -0
  99. package/dist/utils/account.d.ts.map +1 -0
  100. package/dist/utils/account.js +154 -0
  101. package/dist/utils/account.js.map +1 -0
  102. package/dist/utils/api-client.d.ts +10 -4
  103. package/dist/utils/api-client.d.ts.map +1 -1
  104. package/dist/utils/api-client.js +13 -7
  105. package/dist/utils/api-client.js.map +1 -1
  106. package/dist/utils/auth-config.d.ts +6 -0
  107. package/dist/utils/auth-config.d.ts.map +1 -1
  108. package/dist/utils/auth-config.js.map +1 -1
  109. package/dist/utils/build-runtime.d.ts +45 -0
  110. package/dist/utils/build-runtime.d.ts.map +1 -0
  111. package/dist/utils/build-runtime.js +154 -0
  112. package/dist/utils/build-runtime.js.map +1 -0
  113. package/dist/utils/config.d.ts.map +1 -1
  114. package/dist/utils/config.js +17 -2
  115. package/dist/utils/config.js.map +1 -1
  116. package/dist/utils/console.d.ts +32 -0
  117. package/dist/utils/console.d.ts.map +1 -0
  118. package/dist/utils/console.js +169 -0
  119. package/dist/utils/console.js.map +1 -0
  120. package/dist/utils/extract-hook-body.d.ts +13 -0
  121. package/dist/utils/extract-hook-body.d.ts.map +1 -0
  122. package/dist/utils/extract-hook-body.js +175 -0
  123. package/dist/utils/extract-hook-body.js.map +1 -0
  124. package/dist/utils/format.d.ts +8 -0
  125. package/dist/utils/format.d.ts.map +1 -1
  126. package/dist/utils/format.js +15 -2
  127. package/dist/utils/format.js.map +1 -1
  128. package/dist/utils/i18n-coverage.d.ts +61 -0
  129. package/dist/utils/i18n-coverage.d.ts.map +1 -0
  130. package/dist/utils/i18n-coverage.js +176 -0
  131. package/dist/utils/i18n-coverage.js.map +1 -0
  132. package/dist/utils/lower-callables.d.ts +17 -0
  133. package/dist/utils/lower-callables.d.ts.map +1 -0
  134. package/dist/utils/lower-callables.js +181 -0
  135. package/dist/utils/lower-callables.js.map +1 -0
  136. package/dist/utils/plugin-detection.d.ts +1 -0
  137. package/dist/utils/plugin-detection.d.ts.map +1 -1
  138. package/dist/utils/plugin-detection.js +41 -0
  139. package/dist/utils/plugin-detection.js.map +1 -1
  140. package/dist/utils/studio.d.ts +1 -0
  141. package/dist/utils/studio.d.ts.map +1 -1
  142. package/dist/utils/studio.js +25 -9
  143. package/dist/utils/studio.js.map +1 -1
  144. package/package.json +55 -21
  145. package/.turbo/turbo-build.log +0 -4
  146. package/CHANGELOG.md +0 -821
  147. package/bin/run-dev.js +0 -5
  148. package/dist/commands/auth/login.d.ts.map +0 -1
  149. package/dist/commands/auth/login.js.map +0 -1
  150. package/dist/commands/auth/logout.d.ts.map +0 -1
  151. package/dist/commands/auth/logout.js.map +0 -1
  152. package/dist/commands/auth/whoami.d.ts.map +0 -1
  153. package/dist/commands/auth/whoami.js.map +0 -1
  154. package/dist/commands/codemod/v2-to-v3.d.ts +0 -10
  155. package/dist/commands/codemod/v2-to-v3.d.ts.map +0 -1
  156. package/dist/commands/codemod/v2-to-v3.js +0 -145
  157. package/dist/commands/codemod/v2-to-v3.js.map +0 -1
  158. package/dist/commands/plugin/add.d.ts +0 -22
  159. package/dist/commands/plugin/add.d.ts.map +0 -1
  160. package/dist/commands/plugin/add.js +0 -93
  161. package/dist/commands/plugin/add.js.map +0 -1
  162. package/dist/commands/plugin/build.d.ts +0 -29
  163. package/dist/commands/plugin/build.d.ts.map +0 -1
  164. package/dist/commands/plugin/build.js +0 -170
  165. package/dist/commands/plugin/build.js.map +0 -1
  166. package/dist/commands/plugin/info.d.ts +0 -10
  167. package/dist/commands/plugin/info.d.ts.map +0 -1
  168. package/dist/commands/plugin/info.js +0 -65
  169. package/dist/commands/plugin/info.js.map +0 -1
  170. package/dist/commands/plugin/list.d.ts +0 -13
  171. package/dist/commands/plugin/list.d.ts.map +0 -1
  172. package/dist/commands/plugin/list.js +0 -78
  173. package/dist/commands/plugin/list.js.map +0 -1
  174. package/dist/commands/plugin/publish.d.ts +0 -27
  175. package/dist/commands/plugin/publish.d.ts.map +0 -1
  176. package/dist/commands/plugin/publish.js +0 -152
  177. package/dist/commands/plugin/publish.js.map +0 -1
  178. package/dist/commands/plugin/remove.d.ts +0 -20
  179. package/dist/commands/plugin/remove.d.ts.map +0 -1
  180. package/dist/commands/plugin/remove.js +0 -79
  181. package/dist/commands/plugin/remove.js.map +0 -1
  182. package/dist/commands/plugin/validate.d.ts +0 -23
  183. package/dist/commands/plugin/validate.d.ts.map +0 -1
  184. package/dist/commands/plugin/validate.js +0 -251
  185. package/dist/commands/plugin/validate.js.map +0 -1
  186. package/src/bin.ts +0 -13
  187. package/src/commands/auth/login.ts +0 -188
  188. package/src/commands/auth/logout.ts +0 -51
  189. package/src/commands/auth/whoami.ts +0 -85
  190. package/src/commands/codemod/v2-to-v3.ts +0 -171
  191. package/src/commands/compile.ts +0 -114
  192. package/src/commands/create.ts +0 -281
  193. package/src/commands/data/create.ts +0 -110
  194. package/src/commands/data/delete.ts +0 -84
  195. package/src/commands/data/get.ts +0 -84
  196. package/src/commands/data/query.ts +0 -127
  197. package/src/commands/data/update.ts +0 -114
  198. package/src/commands/dev.ts +0 -83
  199. package/src/commands/diff.ts +0 -294
  200. package/src/commands/doctor.ts +0 -572
  201. package/src/commands/explain.ts +0 -412
  202. package/src/commands/generate.ts +0 -924
  203. package/src/commands/info.ts +0 -124
  204. package/src/commands/init.ts +0 -327
  205. package/src/commands/lint.ts +0 -315
  206. package/src/commands/meta/delete.ts +0 -79
  207. package/src/commands/meta/get.ts +0 -73
  208. package/src/commands/meta/list.ts +0 -105
  209. package/src/commands/meta/register.ts +0 -97
  210. package/src/commands/plugin/add.ts +0 -112
  211. package/src/commands/plugin/build.ts +0 -193
  212. package/src/commands/plugin/info.ts +0 -79
  213. package/src/commands/plugin/list.ts +0 -93
  214. package/src/commands/plugin/publish.ts +0 -176
  215. package/src/commands/plugin/remove.ts +0 -97
  216. package/src/commands/plugin/validate.ts +0 -268
  217. package/src/commands/serve.ts +0 -411
  218. package/src/commands/studio.ts +0 -52
  219. package/src/commands/test.ts +0 -135
  220. package/src/commands/validate.ts +0 -143
  221. package/src/index.ts +0 -22
  222. package/src/utils/api-client.ts +0 -88
  223. package/src/utils/auth-config.ts +0 -107
  224. package/src/utils/config.ts +0 -80
  225. package/src/utils/format.ts +0 -267
  226. package/src/utils/output-formatter.ts +0 -91
  227. package/src/utils/plugin-detection.ts +0 -16
  228. package/src/utils/plugin-helpers.ts +0 -37
  229. package/src/utils/studio.ts +0 -350
  230. package/test/commands.test.ts +0 -128
  231. package/test/create.test.ts +0 -25
  232. package/test/plugin-commands.test.ts +0 -44
  233. package/test/plugin.test.ts +0 -169
  234. package/test/remote-api-commands.test.ts +0 -188
  235. package/test/remote-api-utils.test.ts +0 -196
  236. package/test/serve-host-config.test.ts +0 -77
  237. package/tsconfig.build.json +0 -20
  238. package/tsconfig.json +0 -25
  239. package/tsup.config.ts +0 -23
  240. /package/dist/commands/{auth/logout.d.ts → logout.d.ts} +0 -0
  241. /package/dist/commands/{auth/whoami.d.ts → whoami.d.ts} +0 -0
@@ -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 { isHostConfig } from '../utils/plugin-detection.js';
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 in dev mode)' }),
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
- const { mod } = await bundleRequire({
120
- filepath: absolutePath,
121
- });
122
- const config = mod.default || mod;
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 Memory Driver if in Dev and no driver configured
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 (isDev && !hasDriver && config.objects) {
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
- const { InMemoryDriver } = await import('@objectstack/driver-memory');
160
- await kernel.use(new DriverPlugin(new InMemoryDriver()));
161
- trackPlugin('MemoryDriver');
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
- // Skip if config is a host/aggregator config that already contains
169
- // instantiated plugins wrapping it would cause duplicate registration
170
- // and startup failures (e.g. plugin.app.dev-workspace).
171
- if (!isHostConfig(config) && (config.objects || config.manifest || config.apps)) {
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
- const configHasTranslations = ((Array.isArray(config.translations) && config.translations.length > 0)
186
- || config.i18n
187
- || (config.manifest && ((Array.isArray(config.manifest.translations) && config.manifest.translations.length > 0)
188
- || config.manifest.i18n)));
189
- if (!hasI18nPlugin && configHasTranslations) {
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 SetupPlugin BEFORE config plugins so that other
226
- // plugins (e.g. AuthPlugin) can call setupNav.contribute() during init.
227
- const hasSetupPlugin = plugins.some((p) => p.name === 'com.objectstack.setup' || p.constructor?.name === 'SetupPlugin');
228
- if (!hasSetupPlugin) {
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 setupPkg = '@objectstack/plugin-setup';
231
- const { SetupPlugin } = await import(/* webpackIgnore: true */ setupPkg);
232
- await kernel.use(new SetupPlugin());
233
- trackPlugin('Setup');
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/plugin-setup not installed — setup app unavailable
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 || isDev;
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, { isDev }));
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
- // Wait briefly for pino worker thread buffers to flush, then restore
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
- // Keep process alive
346
- process.on('SIGINT', async () => {
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