dev-prism 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Emil Bryggare
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,192 @@
1
+ # dev-prism
2
+
3
+ A minimal CLI tool for managing isolated parallel development sessions. Enables multiple Claude Code (or human developer) sessions to work on the same repo simultaneously with complete isolation.
4
+
5
+ ## Philosophy
6
+
7
+ **Minimal orchestration, maximal Docker Compose.** This tool does the bare minimum:
8
+ 1. Creates git worktrees for isolated working directories
9
+ 2. Generates `.env.session` with calculated ports
10
+ 3. Runs `docker compose` commands
11
+
12
+ All Docker configuration lives in `docker-compose.session.yml` in your project - a standard file you control.
13
+
14
+ ## Features
15
+
16
+ - **Git worktrees** for isolated working directories
17
+ - **Docker Compose** handles all container orchestration
18
+ - **Unique ports** per session (calculated from session ID)
19
+ - **Two modes**: Docker (apps in containers) or Native (apps run locally)
20
+ - **Portable**: Works with any project
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ npm install -g dev-prism
26
+ # or
27
+ pnpm add -D dev-prism
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ ### Create a session
33
+
34
+ ```bash
35
+ # Docker mode (default) - apps run in containers
36
+ dev-prism create 001
37
+
38
+ # Native mode - only infrastructure in Docker, apps run via pnpm dev
39
+ dev-prism create 001 --mode=native
40
+
41
+ # In-place mode - use current directory instead of creating worktree
42
+ dev-prism create 001 --in-place
43
+ ```
44
+
45
+ **Note:** In-place sessions are not shown in `dev-prism list` (which only lists worktree-based sessions). Use `dev-prism info` from within an in-place session directory to see its details.
46
+
47
+ ### List sessions
48
+
49
+ ```bash
50
+ dev-prism list
51
+ ```
52
+
53
+ ### Session info (for current directory)
54
+
55
+ ```bash
56
+ dev-prism info
57
+ ```
58
+
59
+ ### Start/Stop services
60
+
61
+ ```bash
62
+ dev-prism stop 001 # Stop without destroying
63
+ dev-prism start 001 # Start again
64
+ dev-prism stop-all # Stop all sessions
65
+ ```
66
+
67
+ ### View logs
68
+
69
+ ```bash
70
+ dev-prism logs 001
71
+ ```
72
+
73
+ ### Destroy a session
74
+
75
+ ```bash
76
+ dev-prism destroy 001 # Destroy specific session
77
+ dev-prism destroy --all # Destroy all sessions
78
+ ```
79
+
80
+ ## Port Allocation
81
+
82
+ Formula: `port = portBase + (sessionId * 100) + offset`
83
+
84
+ With base port 47000:
85
+
86
+ | Service | Session 001 | Session 002 | Session 003 |
87
+ |----------------|-------------|-------------|-------------|
88
+ | CONVAS_APP_PORT| 47100 | 47200 | 47300 |
89
+ | CONVAS_WEB_PORT| 47101 | 47201 | 47301 |
90
+ | POSTGRES_PORT | 47110 | 47210 | 47310 |
91
+ | MAILPIT_SMTP | 47111 | 47211 | 47311 |
92
+ | MAILPIT_WEB | 47112 | 47212 | 47312 |
93
+
94
+ ## Configuration
95
+
96
+ ### session.config.mjs (minimal)
97
+
98
+ ```javascript
99
+ export default {
100
+ portBase: 47000,
101
+ sessionsDir: '../my-project-sessions',
102
+
103
+ // Port offsets - become env vars for docker-compose
104
+ ports: {
105
+ POSTGRES_PORT: 10,
106
+ REDIS_PORT: 11,
107
+ APP_PORT: 0,
108
+ },
109
+
110
+ // Optional: app-specific env for CLI commands from host
111
+ appEnv: {
112
+ 'apps/my-app': {
113
+ DATABASE_URL: 'postgresql://postgres:postgres@localhost:${POSTGRES_PORT}/postgres',
114
+ },
115
+ },
116
+
117
+ setup: ['pnpm install', 'pnpm db:push'],
118
+ };
119
+ ```
120
+
121
+ ### docker-compose.session.yml (standard Docker Compose)
122
+
123
+ ```yaml
124
+ services:
125
+ postgres:
126
+ image: postgres:16
127
+ container_name: postgres-${SESSION_ID}
128
+ ports:
129
+ - "${POSTGRES_PORT}:5432"
130
+ environment:
131
+ POSTGRES_USER: postgres
132
+ POSTGRES_PASSWORD: postgres
133
+ healthcheck:
134
+ test: ["CMD-SHELL", "pg_isready -U postgres"]
135
+ interval: 5s
136
+ timeout: 5s
137
+ retries: 5
138
+
139
+ my-app:
140
+ profiles: ["apps"] # Only runs in docker mode
141
+ build:
142
+ context: .
143
+ dockerfile: apps/my-app/Dockerfile.dev
144
+ container_name: my-app-${SESSION_ID}
145
+ ports:
146
+ - "${APP_PORT}:3000"
147
+ environment:
148
+ DATABASE_URL: postgresql://postgres:postgres@postgres:5432/postgres
149
+ depends_on:
150
+ postgres:
151
+ condition: service_healthy
152
+ ```
153
+
154
+ ## How It Works
155
+
156
+ 1. **Create session**: `dev-prism create 001`
157
+ - Creates git worktree at `../project-sessions/session-001`
158
+ - Generates `.env.session` with calculated ports
159
+ - Runs `docker compose --env-file .env.session up -d`
160
+ - Runs setup commands
161
+
162
+ 2. **Docker Compose** reads `.env.session` and substitutes `${VAR}` placeholders
163
+
164
+ 3. **Docker mode** (`--profile apps`): All services including apps run in containers
165
+ 4. **Native mode**: Only infrastructure runs; apps use `pnpm dev` with `.env.session`
166
+
167
+ ## Generated Files
168
+
169
+ ```
170
+ session-001/
171
+ ├── .env.session # Port variables for docker-compose
172
+ ├── docker-compose.session.yml # (from git, not generated)
173
+ └── apps/my-app/.env.session # App-specific env for host CLI
174
+ ```
175
+
176
+ Example `.env.session`:
177
+ ```bash
178
+ SESSION_ID=001
179
+ POSTGRES_PORT=47110
180
+ MAILPIT_SMTP_PORT=47111
181
+ MAILPIT_WEB_PORT=47112
182
+ CONVAS_APP_PORT=47100
183
+ ```
184
+
185
+ ## Portability
186
+
187
+ To use in another project:
188
+
189
+ 1. Install: `pnpm add -D dev-prism`
190
+ 2. Create `session.config.mjs` with port offsets
191
+ 3. Create `docker-compose.session.yml` with `${VAR}` placeholders
192
+ 4. Run `dev-prism create 001`
@@ -0,0 +1,399 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { createSession } from '../dist/commands/create.js';
5
+ import { destroySession } from '../dist/commands/destroy.js';
6
+ import { listSessions } from '../dist/commands/list.js';
7
+ import { installClaude } from '../dist/commands/claude.js';
8
+
9
+ const program = new Command();
10
+
11
+ program
12
+ .name('dev-prism')
13
+ .description('CLI tool for managing isolated parallel development sessions')
14
+ .version('0.1.0');
15
+
16
+ program
17
+ .command('create [sessionId]')
18
+ .description('Create a new isolated development session')
19
+ .option('-m, --mode <mode>', 'App mode: docker (default) or native', 'docker')
20
+ .option('-b, --branch <branch>', 'Git branch name (default: session/YYYY-MM-DD/XXX)')
21
+ .option('-W, --without <apps>', 'Exclude apps (comma-separated: app,web,widget)', (val) => val.split(','))
22
+ .option('--no-detach', 'Stream container logs after starting (default: detach)')
23
+ .option('--in-place', 'Run in current directory instead of creating a worktree')
24
+ .action(async (sessionId, options) => {
25
+ const projectRoot = process.cwd();
26
+ await createSession(projectRoot, sessionId, {
27
+ mode: options.mode,
28
+ branch: options.branch,
29
+ detach: options.detach,
30
+ without: options.without,
31
+ inPlace: options.inPlace,
32
+ });
33
+ });
34
+
35
+ program
36
+ .command('destroy [sessionId]')
37
+ .description('Destroy a development session')
38
+ .option('-a, --all', 'Destroy all sessions')
39
+ .action(async (sessionId, options) => {
40
+ const projectRoot = process.cwd();
41
+ await destroySession(projectRoot, sessionId, { all: options.all });
42
+ });
43
+
44
+ program
45
+ .command('list')
46
+ .description('List all active development sessions')
47
+ .action(async () => {
48
+ const projectRoot = process.cwd();
49
+ await listSessions(projectRoot);
50
+ });
51
+
52
+ program
53
+ .command('info')
54
+ .description('Show session info for current directory (useful for --in-place sessions)')
55
+ .action(async () => {
56
+ const cwd = process.cwd();
57
+ const chalk = (await import('chalk')).default;
58
+ const { existsSync, readFileSync } = await import('node:fs');
59
+ const { resolve } = await import('node:path');
60
+ const docker = await import('../dist/lib/docker.js');
61
+
62
+ const envFile = resolve(cwd, '.env.session');
63
+ if (!existsSync(envFile)) {
64
+ console.log(chalk.yellow('No .env.session found in current directory.'));
65
+ console.log(chalk.gray('Run `dev-prism create --in-place` to create a session here.'));
66
+ process.exit(1);
67
+ }
68
+
69
+ // Parse .env.session
70
+ const envContent = readFileSync(envFile, 'utf-8');
71
+ const env = {};
72
+ for (const line of envContent.split('\n')) {
73
+ const match = line.match(/^([^=]+)=(.*)$/);
74
+ if (match) {
75
+ env[match[1]] = match[2];
76
+ }
77
+ }
78
+
79
+ const sessionId = env.SESSION_ID || 'unknown';
80
+ const running = await docker.isRunning({ cwd });
81
+
82
+ console.log(chalk.blue(`\nSession ${sessionId}`));
83
+ console.log(chalk.gray(`Directory: ${cwd}`));
84
+ console.log(running ? chalk.green('Status: running') : chalk.yellow('Status: stopped'));
85
+
86
+ console.log(chalk.gray('\nPorts:'));
87
+ for (const [key, value] of Object.entries(env)) {
88
+ if (key.includes('PORT')) {
89
+ console.log(chalk.gray(` ${key}: ${value}`));
90
+ }
91
+ }
92
+
93
+ console.log(chalk.gray('\nURLs:'));
94
+ for (const [key, value] of Object.entries(env)) {
95
+ if (key.includes('APP') || key.includes('WEB') || key.includes('WIDGET')) {
96
+ if (key.includes('PORT')) {
97
+ console.log(chalk.cyan(` ${key.replace('_PORT', '')}: http://localhost:${value}`));
98
+ }
99
+ }
100
+ }
101
+ console.log('');
102
+ });
103
+
104
+ program
105
+ .command('start <sessionId>')
106
+ .description('Start Docker services for a session')
107
+ .option('-m, --mode <mode>', 'App mode: docker or native', 'docker')
108
+ .option('-W, --without <apps>', 'Exclude apps (comma-separated: app,web,widget)', (val) => val.split(','))
109
+ .action(async (sessionId, options) => {
110
+ const projectRoot = process.cwd();
111
+ const { loadConfig, getSessionDir } = await import('../dist/lib/config.js');
112
+ const docker = await import('../dist/lib/docker.js');
113
+
114
+ const config = await loadConfig(projectRoot);
115
+ const sessionDir = getSessionDir(config, projectRoot, sessionId);
116
+ let profiles;
117
+ if (options.mode === 'docker') {
118
+ const allApps = config.apps ?? ['app', 'web', 'widget'];
119
+ const excludeApps = options.without ?? [];
120
+ profiles = allApps.filter((app) => !excludeApps.includes(app));
121
+ }
122
+
123
+ await docker.up({ cwd: sessionDir, profiles });
124
+ });
125
+
126
+ program
127
+ .command('stop <sessionId>')
128
+ .description('Stop Docker services for a session (without destroying)')
129
+ .action(async (sessionId) => {
130
+ const projectRoot = process.cwd();
131
+ const { loadConfig, getSessionDir } = await import('../dist/lib/config.js');
132
+ const { execa } = await import('execa');
133
+
134
+ const config = await loadConfig(projectRoot);
135
+ const sessionDir = getSessionDir(config, projectRoot, sessionId);
136
+
137
+ // Use stop instead of down to preserve volumes
138
+ await execa(
139
+ 'docker',
140
+ ['compose', '-f', 'docker-compose.session.yml', '--env-file', '.env.session', 'stop'],
141
+ { cwd: sessionDir, stdio: 'inherit' }
142
+ );
143
+ });
144
+
145
+ program
146
+ .command('logs <sessionId>')
147
+ .description('Stream logs from a session\'s Docker services')
148
+ .option('-m, --mode <mode>', 'App mode: docker or native', 'docker')
149
+ .option('-W, --without <apps>', 'Exclude apps (comma-separated: app,web,widget)', (val) => val.split(','))
150
+ .option('-n, --tail <lines>', 'Number of lines to show from the end', '50')
151
+ .action(async (sessionId, options) => {
152
+ const projectRoot = process.cwd();
153
+ const { loadConfig, getSessionDir } = await import('../dist/lib/config.js');
154
+ const { execa } = await import('execa');
155
+
156
+ const config = await loadConfig(projectRoot);
157
+ const sessionDir = getSessionDir(config, projectRoot, sessionId);
158
+ let profileFlags = [];
159
+ if (options.mode === 'docker') {
160
+ const allApps = config.apps ?? ['app', 'web', 'widget'];
161
+ const excludeApps = options.without ?? [];
162
+ const profiles = allApps.filter((app) => !excludeApps.includes(app));
163
+ profileFlags = profiles.flatMap((p) => ['--profile', p]);
164
+ }
165
+
166
+ const args = [
167
+ 'compose',
168
+ '-f', 'docker-compose.session.yml',
169
+ '--env-file', '.env.session',
170
+ ...profileFlags,
171
+ 'logs',
172
+ '-f',
173
+ '--tail', options.tail,
174
+ ];
175
+
176
+ await execa('docker', args, { cwd: sessionDir, stdio: 'inherit' });
177
+ });
178
+
179
+ program
180
+ .command('stop-all')
181
+ .description('Stop all running sessions (preserves data)')
182
+ .action(async () => {
183
+ const projectRoot = process.cwd();
184
+ const chalk = (await import('chalk')).default;
185
+ const { loadConfig, getSessionDir } = await import('../dist/lib/config.js');
186
+ const { getSessionWorktrees } = await import('../dist/lib/worktree.js');
187
+ const docker = await import('../dist/lib/docker.js');
188
+ const { existsSync } = await import('node:fs');
189
+ const { resolve } = await import('node:path');
190
+
191
+ const config = await loadConfig(projectRoot);
192
+ const sessions = await getSessionWorktrees(projectRoot);
193
+
194
+ if (sessions.length === 0) {
195
+ console.log(chalk.gray('No sessions found.'));
196
+ return;
197
+ }
198
+
199
+ // Find running sessions
200
+ const runningSessions = [];
201
+ for (const session of sessions) {
202
+ const envFile = resolve(session.path, '.env.session');
203
+ if (existsSync(envFile)) {
204
+ const running = await docker.isRunning({ cwd: session.path });
205
+ if (running) {
206
+ runningSessions.push(session);
207
+ }
208
+ }
209
+ }
210
+
211
+ if (runningSessions.length === 0) {
212
+ console.log(chalk.gray('No running sessions found.'));
213
+ return;
214
+ }
215
+
216
+ console.log(chalk.blue(`Stopping ${runningSessions.length} running session(s)...\n`));
217
+
218
+ // Get all app profiles and service names to ensure we stop everything
219
+ const allApps = config.apps ?? ['app', 'web', 'widget'];
220
+ const profileFlags = allApps.flatMap((p) => ['--profile', p]);
221
+ // Explicitly list all services to stop (infrastructure + apps)
222
+ const allServices = ['postgres', 'mailpit', 'convas-app', 'convas-web', 'convas-widget'];
223
+
224
+ const { execa } = await import('execa');
225
+ for (const session of runningSessions) {
226
+ console.log(chalk.gray(` Stopping session ${session.sessionId}...`));
227
+ try {
228
+ await execa(
229
+ 'docker',
230
+ ['compose', '-f', 'docker-compose.session.yml', '--env-file', '.env.session', ...profileFlags, 'stop', ...allServices],
231
+ { cwd: session.path, stdio: 'pipe' }
232
+ );
233
+ console.log(chalk.green(` Session ${session.sessionId} stopped.`));
234
+ } catch (error) {
235
+ console.log(chalk.yellow(` Warning: Could not stop session ${session.sessionId}`));
236
+ }
237
+ }
238
+
239
+ console.log(chalk.green(`\nStopped ${runningSessions.length} session(s).`));
240
+ });
241
+
242
+ program
243
+ .command('prune')
244
+ .description('Remove all stopped sessions (destroys data)')
245
+ .option('-y, --yes', 'Skip confirmation prompt')
246
+ .action(async (options) => {
247
+ const projectRoot = process.cwd();
248
+ const chalk = (await import('chalk')).default;
249
+ const { loadConfig } = await import('../dist/lib/config.js');
250
+ const { getSessionWorktrees, removeWorktree } = await import('../dist/lib/worktree.js');
251
+ const docker = await import('../dist/lib/docker.js');
252
+ const { existsSync } = await import('node:fs');
253
+ const { resolve } = await import('node:path');
254
+ const readline = await import('node:readline');
255
+
256
+ const config = await loadConfig(projectRoot);
257
+ const sessions = await getSessionWorktrees(projectRoot);
258
+
259
+ if (sessions.length === 0) {
260
+ console.log(chalk.gray('No sessions found.'));
261
+ return;
262
+ }
263
+
264
+ // Find stopped sessions
265
+ const stoppedSessions = [];
266
+ for (const session of sessions) {
267
+ const envFile = resolve(session.path, '.env.session');
268
+ let running = false;
269
+ if (existsSync(envFile)) {
270
+ running = await docker.isRunning({ cwd: session.path });
271
+ }
272
+ if (!running) {
273
+ stoppedSessions.push(session);
274
+ }
275
+ }
276
+
277
+ if (stoppedSessions.length === 0) {
278
+ console.log(chalk.gray('No stopped sessions to prune.'));
279
+ return;
280
+ }
281
+
282
+ console.log(chalk.yellow(`\nFound ${stoppedSessions.length} stopped session(s) to prune:`));
283
+ for (const session of stoppedSessions) {
284
+ console.log(chalk.gray(` - Session ${session.sessionId} (${session.branch})`));
285
+ }
286
+ console.log('');
287
+
288
+ // Confirm unless --yes flag provided
289
+ if (!options.yes) {
290
+ const rl = readline.createInterface({
291
+ input: process.stdin,
292
+ output: process.stdout,
293
+ });
294
+
295
+ const answer = await new Promise((resolve) => {
296
+ rl.question(chalk.red('Are you sure you want to delete these sessions? This cannot be undone. [y/N] '), resolve);
297
+ });
298
+ rl.close();
299
+
300
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
301
+ console.log(chalk.gray('Cancelled.'));
302
+ return;
303
+ }
304
+ }
305
+
306
+ console.log(chalk.blue('\nPruning stopped sessions...\n'));
307
+
308
+ for (const session of stoppedSessions) {
309
+ console.log(chalk.gray(` Removing session ${session.sessionId}...`));
310
+ try {
311
+ // Clean up any docker resources
312
+ const envFile = resolve(session.path, '.env.session');
313
+ if (existsSync(envFile)) {
314
+ try {
315
+ await docker.down({ cwd: session.path });
316
+ } catch {
317
+ // Ignore errors - containers might already be removed
318
+ }
319
+ }
320
+ // Remove worktree and branch
321
+ await removeWorktree(projectRoot, session.path, session.branch);
322
+ console.log(chalk.green(` Session ${session.sessionId} removed.`));
323
+ } catch (error) {
324
+ console.log(chalk.yellow(` Warning: Could not fully remove session ${session.sessionId}`));
325
+ }
326
+ }
327
+
328
+ console.log(chalk.green(`\nPruned ${stoppedSessions.length} session(s).`));
329
+ });
330
+
331
+ program
332
+ .command('claude')
333
+ .description('Install Claude Code integration (skill + CLAUDE.md)')
334
+ .option('-f, --force', 'Overwrite existing files')
335
+ .action(async (options) => {
336
+ await installClaude(process.cwd(), { force: options.force });
337
+ });
338
+
339
+ program
340
+ .command('help')
341
+ .description('Show detailed help and examples')
342
+ .action(async () => {
343
+ const chalk = (await import('chalk')).default;
344
+
345
+ console.log(`
346
+ ${chalk.bold('dev-prism')} - Manage isolated parallel development sessions
347
+
348
+ ${chalk.bold('USAGE')}
349
+ dev-prism <command> [options]
350
+
351
+ ${chalk.bold('COMMANDS')}
352
+ ${chalk.cyan('create')} [id] Create a new session (auto-assigns ID if not provided)
353
+ ${chalk.cyan('destroy')} <id> Destroy a specific session
354
+ ${chalk.cyan('list')} List all sessions and their status
355
+ ${chalk.cyan('info')} Show session info for current directory
356
+ ${chalk.cyan('start')} <id> Start Docker services for a stopped session
357
+ ${chalk.cyan('stop')} <id> Stop Docker services (preserves data)
358
+ ${chalk.cyan('stop-all')} Stop all running sessions
359
+ ${chalk.cyan('logs')} <id> Stream logs from a session
360
+ ${chalk.cyan('prune')} Remove all stopped sessions
361
+
362
+ ${chalk.bold('EXAMPLES')}
363
+ ${chalk.gray('# Create a new session (auto-assigns next available ID)')}
364
+ $ dev-prism create
365
+
366
+ ${chalk.gray('# Create session with specific branch')}
367
+ $ dev-prism create --branch feature/my-feature
368
+
369
+ ${chalk.gray('# Create session in native mode (apps run on host)')}
370
+ $ dev-prism create --mode native
371
+
372
+ ${chalk.gray('# Create session without web app')}
373
+ $ dev-prism create --without web
374
+
375
+ ${chalk.gray('# Create session in current directory (no worktree)')}
376
+ $ dev-prism create --in-place
377
+
378
+ ${chalk.gray('# Check session status in current directory')}
379
+ $ dev-prism info
380
+
381
+ ${chalk.gray('# Stop all running sessions before switching context')}
382
+ $ dev-prism stop-all
383
+
384
+ ${chalk.gray('# Clean up old stopped sessions')}
385
+ $ dev-prism prune
386
+
387
+ ${chalk.gray('# Destroy all sessions')}
388
+ $ dev-prism destroy --all
389
+
390
+ ${chalk.bold('SESSION MODES')}
391
+ ${chalk.cyan('docker')} (default) All apps run in containers
392
+ ${chalk.cyan('native')} Only infrastructure in Docker, apps on host
393
+
394
+ ${chalk.bold('MORE INFO')}
395
+ Run ${chalk.cyan('dev-prism <command> --help')} for command-specific options
396
+ `);
397
+ });
398
+
399
+ program.parse();
@@ -0,0 +1,46 @@
1
+ // src/lib/config.ts
2
+ import { existsSync } from "fs";
3
+ import { resolve } from "path";
4
+ import { pathToFileURL } from "url";
5
+ var DEFAULT_CONFIG = {
6
+ portBase: 47e3,
7
+ sessionsDir: "../sessions",
8
+ ports: {
9
+ POSTGRES_PORT: 10
10
+ },
11
+ setup: ["pnpm install"]
12
+ };
13
+ async function loadConfig(projectRoot) {
14
+ let configPath = resolve(projectRoot, "session.config.mjs");
15
+ if (!existsSync(configPath)) {
16
+ configPath = resolve(projectRoot, "session.config.js");
17
+ }
18
+ if (!existsSync(configPath)) {
19
+ console.warn("No session.config.mjs found, using defaults");
20
+ return DEFAULT_CONFIG;
21
+ }
22
+ try {
23
+ const configUrl = pathToFileURL(configPath).href;
24
+ const module = await import(configUrl);
25
+ const userConfig = module.default || module;
26
+ return {
27
+ ...DEFAULT_CONFIG,
28
+ ...userConfig
29
+ };
30
+ } catch (error) {
31
+ console.error(`Failed to load config from ${configPath}:`, error);
32
+ return DEFAULT_CONFIG;
33
+ }
34
+ }
35
+ function getSessionsDir(config, projectRoot) {
36
+ return resolve(projectRoot, config.sessionsDir);
37
+ }
38
+ function getSessionDir(config, projectRoot, sessionId) {
39
+ return resolve(getSessionsDir(config, projectRoot), `session-${sessionId}`);
40
+ }
41
+
42
+ export {
43
+ loadConfig,
44
+ getSessionsDir,
45
+ getSessionDir
46
+ };