33import { Command , Flags } from '@oclif/core' ;
44import { spawn } from 'child_process' ;
55import fs from 'fs' ;
6+ import os from 'os' ;
67import path from 'path' ;
78import { printHeader , printKV , printStep , printError } from '../utils/format.js' ;
89
10+ /**
11+ * `objectstack start` — zero-config quick boot.
12+ *
13+ * Three escalating modes, picked automatically:
14+ *
15+ * 1. **Empty boot** (no artifact, no config in cwd)
16+ * Boots a bare kernel with Studio mounted at `/_studio/`. The user
17+ * can then browse the marketplace and install apps into the home
18+ * directory at runtime. Perfect for "I just want to try it".
19+ *
20+ * 2. **Artifact boot** (an `objectstack.json` is reachable)
21+ * Boots from the compiled artifact, same as today.
22+ *
23+ * 3. **Explicit overrides** (`--artifact`, `--database`, ...)
24+ * Highest precedence — the user is in control.
25+ *
26+ * The HOME directory (default `~/.objectstack`) is where the default
27+ * sqlite database, downloaded marketplace apps and plugin cache all live
28+ * — independent of the current working directory, so the same `os start`
29+ * gives you the same instance no matter where you run it.
30+ */
931export default class Start extends Command {
10- static override description = 'Serve the pre-compiled artifact in production mode ( no objectstack.config.ts required )' ;
32+ static override description = 'Quick-start an ObjectStack server (auto-falls back to an empty kernel with Studio + marketplace when no artifact is present )' ;
1133
1234 static override examples = [
1335 '<%= config.bin %> start' ,
36+ '<%= config.bin %> start --home ~/my-objectstack' ,
1437 '<%= config.bin %> start --artifact ./build/myapp.json' ,
1538 '<%= config.bin %> start --artifact https://cdn.example.com/app.json --port 8080' ,
1639 '<%= config.bin %> start --database file:./data/prod.db' ,
1740 '<%= config.bin %> start --database postgres://user:pass@host:5432/mydb' ,
1841 '<%= config.bin %> start --database libsql://my-db.turso.io --database-auth-token $TURSO_TOKEN' ,
19- '<%= config.bin %> start --auth-secret $(openssl rand -hex 32) - -ui' ,
42+ '<%= config.bin %> start --no -ui' ,
2043 ] ;
2144
2245 static override flags = {
2346 // Server
2447 port : Flags . integer ( { char : 'p' , description : 'Port to listen on (overrides $PORT, default 3000)' } ) ,
2548 ui : Flags . boolean ( {
26- description : 'Mount Studio / Account / Console portals at /_studio/, /_account/, /_console/ (off by default in production)' ,
49+ description : 'Mount Studio / Account / Console portals at /_studio/, /_account/, /_console/ (default: true so you can install marketplace apps)' ,
50+ default : true ,
51+ allowNo : true ,
2752 } ) ,
2853 verbose : Flags . boolean ( { char : 'v' , description : 'Verbose output' } ) ,
2954
55+ // Home directory — where persistent runtime state lives.
56+ home : Flags . string ( {
57+ description : 'Home directory for persistent state (default ~/.objectstack; overrides $OS_HOME)' ,
58+ } ) ,
59+
3060 // Artifact source
3161 artifact : Flags . string ( {
3262 char : 'a' ,
33- description : 'Path or http(s):// URL to the compiled objectstack.json (overrides $OS_ARTIFACT_PATH; defaults to . /dist/objectstack.json)' ,
63+ description : 'Path or http(s):// URL to the compiled objectstack.json (overrides $OS_ARTIFACT_PATH; auto-detected from ./dist/objectstack.json or <home> /dist/objectstack.json)' ,
3464 } ) ,
3565
3666 // Project identity
37- 'project -id' : Flags . string ( {
38- description : 'Project identifier (overrides $OS_ENVIRONMENT_ID, default proj_local )' ,
67+ 'environment -id' : Flags . string ( {
68+ description : 'Environment identifier (overrides $OS_ENVIRONMENT_ID, default env_local )' ,
3969 } ) ,
4070
4171 // Storage
4272 database : Flags . string ( {
4373 char : 'd' ,
44- description : 'Database URL: file:./db.sqlite | libsql://... | postgres://... | mongodb://... | memory:// (overrides $OS_DATABASE_URL)' ,
74+ description : 'Database URL: file:./db.sqlite | libsql://... | postgres://... | mongodb://... | memory:// (overrides $OS_DATABASE_URL; defaults to file:<home>/data/objectstack.db )' ,
4575 } ) ,
4676 'database-driver' : Flags . string ( {
4777 description : 'Force driver kind when URL is ambiguous: sqlite | turso | postgres | mongodb | memory (overrides $OS_DATABASE_DRIVER)' ,
@@ -60,69 +90,132 @@ export default class Start extends Command {
6090 async run ( ) : Promise < void > {
6191 const { flags } = await this . parse ( Start ) ;
6292
63- printHeader ( 'Production Mode ' ) ;
93+ printHeader ( 'ObjectStack ' ) ;
6494
65- // ── Artifact resolution ────────────────────────────────────────
66- // Priority: --artifact flag > $OS_ARTIFACT_PATH > ./dist/objectstack.json
67- const artifactPathInput = flags . artifact
68- ?? process . env . OS_ARTIFACT_PATH
69- ?? path . resolve ( process . cwd ( ) , 'dist/objectstack.json' ) ;
70-
71- // `--artifact` / `OS_ARTIFACT_PATH` is allowed to be an
72- // `http(s)://` URL — the runtime's loadArtifactBundle() can fetch
73- // JSON over the network. Skip the existence check for URLs
74- // (validated lazily by the loader).
75- const isUrl = / ^ h t t p s ? : \/ \/ / i. test ( artifactPathInput ) ;
76- const artifactPath = isUrl ? artifactPathInput : path . resolve ( process . cwd ( ) , artifactPathInput ) ;
77-
78- if ( ! isUrl && ! fs . existsSync ( artifactPath ) ) {
79- printError ( `Artifact not found: ${ path . relative ( process . cwd ( ) , artifactPath ) } ` ) ;
80- console . error ( ' Run \x1b[33mobjectstack build\x1b[0m to compile your configuration first,' ) ;
81- console . error ( ' pass \x1b[33m--artifact <path|url>\x1b[0m, or set \x1b[33mOS_ARTIFACT_PATH\x1b[0m.' ) ;
95+ // ── Home directory ──────────────────────────────────────────────
96+ // Priority: --home > $OS_HOME > ~/.objectstack
97+ const homeDir = resolveHome ( flags . home ) ;
98+ try {
99+ fs . mkdirSync ( path . join ( homeDir , 'data' ) , { recursive : true } ) ;
100+ } catch ( err : any ) {
101+ printError ( `Cannot create home directory at ${ homeDir } : ${ err ?. message ?? err } ` ) ;
82102 process . exit ( 1 ) ;
83103 }
84104
85- const displayPath = isUrl ? artifactPath : path . relative ( process . cwd ( ) , artifactPath ) ;
86- printKV ( 'Artifact' , displayPath , '📦' ) ;
87- printStep ( 'Starting server (production mode)...' ) ;
105+ // ── Artifact resolution ────────────────────────────────────────
106+ // Priority: --artifact > $OS_ARTIFACT_PATH > ./dist/objectstack.json
107+ // > <home>/dist/objectstack.json > none (empty boot)
108+ const artifactSource = resolveArtifactSource ( flags . artifact , homeDir ) ;
109+
110+ // ── Database resolution ─────────────────────────────────────────
111+ // Priority: --database > $OS_DATABASE_URL > file:<home>/data/objectstack.db
112+ const databaseUrl = flags . database
113+ ?? process . env . OS_DATABASE_URL
114+ ?? `file:${ path . join ( homeDir , 'data' , 'objectstack.db' ) } ` ;
115+
116+ const environmentId = flags [ 'environment-id' ]
117+ ?? process . env . OS_ENVIRONMENT_ID
118+ ?? 'env_local' ;
119+
120+ // ── Banner ──────────────────────────────────────────────────────
121+ printKV ( 'Home' , homeDir , '🏠' ) ;
122+ if ( artifactSource ) {
123+ printKV ( 'Artifact' , artifactSource . display , '📦' ) ;
124+ } else {
125+ printKV ( 'Artifact' , 'none (empty kernel — install apps via Studio marketplace)' , '📦' ) ;
126+ }
127+ printKV ( 'Database' , redactDbUrl ( databaseUrl ) , '🗄️' ) ;
128+ printKV ( 'Environment' , environmentId , '🎯' ) ;
129+ if ( flags . ui ) printKV ( 'Studio' , `http://localhost:${ flags . port ?? 3000 } /_studio/` , '🎨' ) ;
88130
89- const environmentId = flags [ 'project-id' ] ?? process . env . OS_ENVIRONMENT_ID ?? 'proj_local' ;
131+ printStep ( 'Starting server...' ) ;
90132
91- // Build the env handed to `serve`. Flags win over inherited env so
92- // `--database file:foo.db` overrides any pre-existing OS_DATABASE_URL.
133+ // ── Child env ───────────────────────────────────────────────────
134+ // Flags win over inherited env. When no artifact was located, signal
135+ // serve.ts to boot an empty kernel via OS_BOOT_EMPTY=1.
93136 const localEnv : NodeJS . ProcessEnv = {
94137 ...process . env ,
95- NODE_ENV : 'production' ,
138+ OS_HOME : homeDir ,
96139 OS_ENVIRONMENT_ID : environmentId ,
97- OS_ARTIFACT_PATH : artifactPath ,
140+ OS_DATABASE_URL : databaseUrl ,
98141 ...( flags . port ? { PORT : String ( flags . port ) } : { } ) ,
99- ...( flags . database ? { OS_DATABASE_URL : flags . database } : { } ) ,
100142 ...( flags [ 'database-driver' ] ? { OS_DATABASE_DRIVER : flags [ 'database-driver' ] } : { } ) ,
101143 ...( flags [ 'database-auth-token' ] ? { OS_DATABASE_AUTH_TOKEN : flags [ 'database-auth-token' ] } : { } ) ,
102144 ...( flags [ 'auth-secret' ] ? { AUTH_SECRET : flags [ 'auth-secret' ] } : { } ) ,
145+ ...( artifactSource ? { OS_ARTIFACT_PATH : artifactSource . path } : { OS_BOOT_EMPTY : '1' } ) ,
103146 } ;
104-
105- printKV ( 'Project ID' , environmentId , '🎯' ) ;
106- if ( flags . database ) printKV ( 'Database' , redactDbUrl ( flags . database ) , '🗄️' ) ;
147+ // NODE_ENV is only forced to production when the user has not set it.
148+ // Allows `NODE_ENV=development objectstack start` to work for debugging.
149+ if ( ! localEnv . NODE_ENV ) localEnv . NODE_ENV = 'production' ;
107150
108151 const binPath = process . argv [ 1 ] ;
109- spawn (
152+ const child = spawn (
110153 process . execPath ,
111154 [
112155 binPath ,
113156 'serve' ,
114- // Production runtime: Studio is opt-in. The `serve` command
115- // defaults `--ui` to true (because it's also driven by `os dev`
116- // where Studio is the expected DX), so we have to explicitly
117- // pass `--no-ui` when the user hasn't asked for `--ui`.
118- ...( flags . ui ? [ '--ui' ] : [ '--no-ui' ] ) ,
157+ flags . ui ? '--ui' : '--no-ui' ,
119158 ...( flags . verbose ? [ '--verbose' ] : [ ] ) ,
120159 ] ,
121160 { stdio : 'inherit' , env : localEnv } ,
122161 ) ;
162+ child . on ( 'exit' , ( code ) => process . exit ( code ?? 0 ) ) ;
123163 }
124164}
125165
166+ function resolveHome ( flagValue ?: string ) : string {
167+ const raw = flagValue ?? process . env . OS_HOME ;
168+ if ( raw && raw . trim ( ) . length > 0 ) {
169+ const v = raw . trim ( ) ;
170+ if ( v . startsWith ( '~' ) ) return path . resolve ( os . homedir ( ) , v . slice ( 1 ) . replace ( / ^ [ / \\ ] / , '' ) ) ;
171+ return path . resolve ( v ) ;
172+ }
173+ return path . resolve ( os . homedir ( ) , '.objectstack' ) ;
174+ }
175+
176+ interface ResolvedArtifact {
177+ /** Absolute path or URL passed to OS_ARTIFACT_PATH. */
178+ path : string ;
179+ /** Human-friendly form for the banner. */
180+ display : string ;
181+ }
182+
183+ function resolveArtifactSource ( flagValue : string | undefined , homeDir : string ) : ResolvedArtifact | undefined {
184+ const cwd = process . cwd ( ) ;
185+
186+ // Explicit flag wins, including URLs.
187+ if ( flagValue ) {
188+ if ( / ^ h t t p s ? : \/ \/ / i. test ( flagValue ) ) return { path : flagValue , display : flagValue } ;
189+ const abs = path . resolve ( cwd , flagValue ) ;
190+ if ( ! fs . existsSync ( abs ) ) {
191+ // We don't exit here — the user asked for this file. Defer to
192+ // serve.ts which already prints a precise error.
193+ return { path : abs , display : path . relative ( cwd , abs ) } ;
194+ }
195+ return { path : abs , display : path . relative ( cwd , abs ) } ;
196+ }
197+
198+ // Explicit env var wins next.
199+ const envPath = process . env . OS_ARTIFACT_PATH ;
200+ if ( envPath ) {
201+ if ( / ^ h t t p s ? : \/ \/ / i. test ( envPath ) ) return { path : envPath , display : envPath } ;
202+ const abs = path . resolve ( cwd , envPath ) ;
203+ return { path : abs , display : path . relative ( cwd , abs ) } ;
204+ }
205+
206+ // Auto-detect — cwd first, then home.
207+ const cwdCandidate = path . resolve ( cwd , 'dist/objectstack.json' ) ;
208+ if ( fs . existsSync ( cwdCandidate ) ) {
209+ return { path : cwdCandidate , display : path . relative ( cwd , cwdCandidate ) } ;
210+ }
211+ const homeCandidate = path . resolve ( homeDir , 'dist/objectstack.json' ) ;
212+ if ( fs . existsSync ( homeCandidate ) ) {
213+ return { path : homeCandidate , display : homeCandidate } ;
214+ }
215+
216+ return undefined ;
217+ }
218+
126219function redactDbUrl ( url : string ) : string {
127220 try {
128221 return url . replace ( / ( \/ \/ [ ^ / @ : ] + ) : [ ^ / @ ] + @ / , '$1:****@' ) ;
0 commit comments