dev-prism 0.1.0 → 0.3.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.
Files changed (45) hide show
  1. package/README.md +65 -21
  2. package/bin/dev-prism.js +20 -233
  3. package/dist/chunk-35SHBLIZ.js +69 -0
  4. package/dist/chunk-3ATDGV4Y.js +22 -0
  5. package/dist/chunk-3MSC3CGG.js +78 -0
  6. package/dist/chunk-5KDDYO6Y.js +168 -0
  7. package/dist/chunk-6YMQTISJ.js +84 -0
  8. package/dist/chunk-7YGOMAJG.js +106 -0
  9. package/dist/chunk-AW2FJGXA.js +38 -0
  10. package/dist/chunk-C4WLIOBR.js +67 -0
  11. package/dist/chunk-D6QWWXZD.js +49 -0
  12. package/dist/chunk-GBN67HYD.js +57 -0
  13. package/dist/chunk-HCCZKLC4.js +64 -0
  14. package/dist/chunk-HZUN6NRB.js +70 -0
  15. package/dist/chunk-J36LRUXM.js +60 -0
  16. package/dist/chunk-JHR4WADC.js +200 -0
  17. package/dist/chunk-JIU574KX.js +41 -0
  18. package/dist/chunk-LDK6QMR6.js +67 -0
  19. package/dist/chunk-LOVO4P3Y.js +41 -0
  20. package/dist/chunk-P3ETW2KK.js +166 -0
  21. package/dist/chunk-PKC2ZED2.js +168 -0
  22. package/dist/chunk-PS76Q3HD.js +168 -0
  23. package/dist/chunk-QUMZI5KK.js +98 -0
  24. package/dist/chunk-SSQ7XBY2.js +30 -0
  25. package/dist/chunk-UHI2QJFI.js +200 -0
  26. package/dist/chunk-X5A6H4Q7.js +70 -0
  27. package/dist/chunk-Y3GR6XK7.js +71 -0
  28. package/dist/commands/claude.js +3 -102
  29. package/dist/commands/create.js +6 -5
  30. package/dist/commands/destroy.js +4 -3
  31. package/dist/commands/info.js +7 -0
  32. package/dist/commands/list.js +4 -4
  33. package/dist/commands/logs.js +8 -0
  34. package/dist/commands/prune.js +10 -0
  35. package/dist/commands/start.js +9 -0
  36. package/dist/commands/stop-all.js +9 -0
  37. package/dist/commands/stop.js +7 -0
  38. package/dist/index.js +60 -10
  39. package/dist/lib/config.d.ts +1 -0
  40. package/dist/lib/docker.js +1 -1
  41. package/dist/lib/env.js +3 -1
  42. package/dist/lib/ports.js +1 -1
  43. package/dist/lib/store.js +6 -0
  44. package/dist/lib/worktree.js +1 -5
  45. package/package.json +14 -4
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # dev-prism
2
2
 
3
+ <p align="center">
4
+ <img src="banner.png" alt="dev-prism - One codebase, many parallel sessions" width="600">
5
+ </p>
6
+
3
7
  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
8
 
5
9
  ## Philosophy
@@ -13,10 +17,13 @@ All Docker configuration lives in `docker-compose.session.yml` in your project -
13
17
 
14
18
  ## Features
15
19
 
16
- - **Git worktrees** for isolated working directories
20
+ - **Git worktrees** for isolated working directories (or in-place mode)
17
21
  - **Docker Compose** handles all container orchestration
18
- - **Unique ports** per session (calculated from session ID)
22
+ - **Unique ports** per session (calculated from session ID, displayed as clickable URLs)
23
+ - **Auto-assign** session IDs or choose your own
24
+ - **SQLite tracking** — all sessions stored in a local database
19
25
  - **Two modes**: Docker (apps in containers) or Native (apps run locally)
26
+ - **Claude Code integration** built-in (`dev-prism claude`)
20
27
  - **Portable**: Works with any project
21
28
 
22
29
  ## Installation
@@ -32,17 +39,27 @@ pnpm add -D dev-prism
32
39
  ### Create a session
33
40
 
34
41
  ```bash
35
- # Docker mode (default) - apps run in containers
42
+ # Auto-assign session ID
43
+ dev-prism create
44
+
45
+ # Explicit session ID
36
46
  dev-prism create 001
37
47
 
48
+ # Custom branch name
49
+ dev-prism create --branch feature/my-feature
50
+
38
51
  # Native mode - only infrastructure in Docker, apps run via pnpm dev
39
- dev-prism create 001 --mode=native
52
+ dev-prism create --mode native
53
+
54
+ # Exclude specific apps from Docker
55
+ dev-prism create --without web,widget
40
56
 
41
57
  # In-place mode - use current directory instead of creating worktree
42
- dev-prism create 001 --in-place
43
- ```
58
+ dev-prism create --in-place
44
59
 
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.
60
+ # Stream logs after creation instead of detaching
61
+ dev-prism create --no-detach
62
+ ```
46
63
 
47
64
  ### List sessions
48
65
 
@@ -70,11 +87,20 @@ dev-prism stop-all # Stop all sessions
70
87
  dev-prism logs 001
71
88
  ```
72
89
 
73
- ### Destroy a session
90
+ ### Cleanup
74
91
 
75
92
  ```bash
76
93
  dev-prism destroy 001 # Destroy specific session
77
94
  dev-prism destroy --all # Destroy all sessions
95
+ dev-prism prune # Remove all stopped sessions
96
+ dev-prism prune -y # Skip confirmation
97
+ ```
98
+
99
+ ### Claude Code integration
100
+
101
+ ```bash
102
+ dev-prism claude # Install Claude Code skill + CLAUDE.md
103
+ dev-prism claude --force # Overwrite existing files
78
104
  ```
79
105
 
80
106
  ## Port Allocation
@@ -85,36 +111,50 @@ With base port 47000:
85
111
 
86
112
  | Service | Session 001 | Session 002 | Session 003 |
87
113
  |----------------|-------------|-------------|-------------|
88
- | CONVAS_APP_PORT| 47100 | 47200 | 47300 |
89
- | CONVAS_WEB_PORT| 47101 | 47201 | 47301 |
114
+ | APP_PORT | 47100 | 47200 | 47300 |
115
+ | WEB_PORT | 47101 | 47201 | 47301 |
90
116
  | POSTGRES_PORT | 47110 | 47210 | 47310 |
91
117
  | MAILPIT_SMTP | 47111 | 47211 | 47311 |
92
118
  | MAILPIT_WEB | 47112 | 47212 | 47312 |
93
119
 
94
120
  ## Configuration
95
121
 
96
- ### session.config.mjs (minimal)
122
+ ### session.config.mjs
97
123
 
98
124
  ```javascript
99
125
  export default {
126
+ // Required
100
127
  portBase: 47000,
101
128
  sessionsDir: '../my-project-sessions',
102
129
 
103
130
  // Port offsets - become env vars for docker-compose
131
+ // Formula: portBase + (sessionId * 100) + offset
104
132
  ports: {
105
- POSTGRES_PORT: 10,
106
- REDIS_PORT: 11,
107
- APP_PORT: 0,
133
+ APP_PORT: 0, // 47100, 47200, 47300...
134
+ WEB_PORT: 1, // 47101, 47201, 47301...
135
+ POSTGRES_PORT: 10, // 47110, 47210, 47310...
136
+ REDIS_PORT: 11, // 47111, 47211, 47311...
108
137
  },
109
138
 
110
- // Optional: app-specific env for CLI commands from host
139
+ // Docker Compose profiles for app containers (used in docker mode)
140
+ // These match service names with `profiles: ["app-name"]` in docker-compose
141
+ apps: ['app', 'web'],
142
+
143
+ // .env files to copy to session worktree (DATABASE_URL auto-updated)
144
+ envFiles: [
145
+ 'apps/my-app/.env',
146
+ 'packages/db/.env',
147
+ ],
148
+
149
+ // Commands to run after session creation
150
+ setup: ['pnpm install', 'pnpm db:push'],
151
+
152
+ // Optional: app-specific env for CLI commands from host (native mode)
111
153
  appEnv: {
112
154
  'apps/my-app': {
113
155
  DATABASE_URL: 'postgresql://postgres:postgres@localhost:${POSTGRES_PORT}/postgres',
114
156
  },
115
157
  },
116
-
117
- setup: ['pnpm install', 'pnpm db:push'],
118
158
  };
119
159
  ```
120
160
 
@@ -153,9 +193,11 @@ services:
153
193
 
154
194
  ## How It Works
155
195
 
156
- 1. **Create session**: `dev-prism create 001`
157
- - Creates git worktree at `../project-sessions/session-001`
196
+ 1. **Create session**: `dev-prism create`
197
+ - Auto-assigns next available session ID (or use explicit ID)
198
+ - Creates git worktree at `../project-sessions/session-001` (or uses current dir with `--in-place`)
158
199
  - Generates `.env.session` with calculated ports
200
+ - Records session in local SQLite database
159
201
  - Runs `docker compose --env-file .env.session up -d`
160
202
  - Runs setup commands
161
203
 
@@ -164,6 +206,8 @@ services:
164
206
  3. **Docker mode** (`--profile apps`): All services including apps run in containers
165
207
  4. **Native mode**: Only infrastructure runs; apps use `pnpm dev` with `.env.session`
166
208
 
209
+ All session state is tracked in a SQLite database (`~/.dev-prism/sessions.db`), making both worktree and in-place sessions first-class citizens across all commands.
210
+
167
211
  ## Generated Files
168
212
 
169
213
  ```
@@ -179,7 +223,7 @@ SESSION_ID=001
179
223
  POSTGRES_PORT=47110
180
224
  MAILPIT_SMTP_PORT=47111
181
225
  MAILPIT_WEB_PORT=47112
182
- CONVAS_APP_PORT=47100
226
+ APP_PORT=47100
183
227
  ```
184
228
 
185
229
  ## Portability
@@ -189,4 +233,4 @@ To use in another project:
189
233
  1. Install: `pnpm add -D dev-prism`
190
234
  2. Create `session.config.mjs` with port offsets
191
235
  3. Create `docker-compose.session.yml` with `${VAR}` placeholders
192
- 4. Run `dev-prism create 001`
236
+ 4. Run `dev-prism create`
package/bin/dev-prism.js CHANGED
@@ -5,13 +5,19 @@ import { createSession } from '../dist/commands/create.js';
5
5
  import { destroySession } from '../dist/commands/destroy.js';
6
6
  import { listSessions } from '../dist/commands/list.js';
7
7
  import { installClaude } from '../dist/commands/claude.js';
8
+ import { showInfo } from '../dist/commands/info.js';
9
+ import { startSession } from '../dist/commands/start.js';
10
+ import { stopSession } from '../dist/commands/stop.js';
11
+ import { stopAllSessions } from '../dist/commands/stop-all.js';
12
+ import { pruneSessions } from '../dist/commands/prune.js';
13
+ import { streamLogs } from '../dist/commands/logs.js';
8
14
 
9
15
  const program = new Command();
10
16
 
11
17
  program
12
18
  .name('dev-prism')
13
19
  .description('CLI tool for managing isolated parallel development sessions')
14
- .version('0.1.0');
20
+ .version('0.3.0');
15
21
 
16
22
  program
17
23
  .command('create [sessionId]')
@@ -53,52 +59,7 @@ program
53
59
  .command('info')
54
60
  .description('Show session info for current directory (useful for --in-place sessions)')
55
61
  .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('');
62
+ await showInfo(process.cwd());
102
63
  });
103
64
 
104
65
  program
@@ -108,19 +69,10 @@ program
108
69
  .option('-W, --without <apps>', 'Exclude apps (comma-separated: app,web,widget)', (val) => val.split(','))
109
70
  .action(async (sessionId, options) => {
110
71
  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 });
72
+ await startSession(projectRoot, sessionId, {
73
+ mode: options.mode,
74
+ without: options.without,
75
+ });
124
76
  });
125
77
 
126
78
  program
@@ -128,18 +80,7 @@ program
128
80
  .description('Stop Docker services for a session (without destroying)')
129
81
  .action(async (sessionId) => {
130
82
  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
- );
83
+ await stopSession(projectRoot, sessionId);
143
84
  });
144
85
 
145
86
  program
@@ -150,30 +91,11 @@ program
150
91
  .option('-n, --tail <lines>', 'Number of lines to show from the end', '50')
151
92
  .action(async (sessionId, options) => {
152
93
  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' });
94
+ await streamLogs(projectRoot, sessionId, {
95
+ mode: options.mode,
96
+ without: options.without,
97
+ tail: options.tail,
98
+ });
177
99
  });
178
100
 
179
101
  program
@@ -181,62 +103,7 @@ program
181
103
  .description('Stop all running sessions (preserves data)')
182
104
  .action(async () => {
183
105
  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).`));
106
+ await stopAllSessions(projectRoot);
240
107
  });
241
108
 
242
109
  program
@@ -245,87 +112,7 @@ program
245
112
  .option('-y, --yes', 'Skip confirmation prompt')
246
113
  .action(async (options) => {
247
114
  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).`));
115
+ await pruneSessions(projectRoot, { yes: options.yes });
329
116
  });
330
117
 
331
118
  program
@@ -0,0 +1,69 @@
1
+ import {
2
+ calculatePorts
3
+ } from "./chunk-PJKUD2N2.js";
4
+ import {
5
+ getSessionWorktrees
6
+ } from "./chunk-GWDGC2OE.js";
7
+ import {
8
+ loadConfig
9
+ } from "./chunk-25WQHUYW.js";
10
+ import {
11
+ isRunning
12
+ } from "./chunk-GBN67HYD.js";
13
+
14
+ // src/commands/list.ts
15
+ import { existsSync } from "fs";
16
+ import { resolve } from "path";
17
+ import chalk from "chalk";
18
+ async function listSessions(projectRoot) {
19
+ const config = await loadConfig(projectRoot);
20
+ const sessions = await getSessionWorktrees(projectRoot);
21
+ if (sessions.length === 0) {
22
+ console.log(chalk.gray("No active sessions found."));
23
+ console.log(chalk.gray("\nTo create a session:"));
24
+ console.log(chalk.cyan(" dev-prism create"));
25
+ return;
26
+ }
27
+ console.log(chalk.blue("Active Sessions:"));
28
+ console.log(chalk.gray("================\n"));
29
+ for (const session of sessions) {
30
+ const status = await getSessionStatus(session.sessionId, session.path, session.branch, config);
31
+ printSessionStatus(status);
32
+ }
33
+ }
34
+ async function getSessionStatus(sessionId, path, branch, config) {
35
+ const ports = calculatePorts(config, sessionId);
36
+ let running = false;
37
+ const envFile = resolve(path, ".env.session");
38
+ if (existsSync(envFile)) {
39
+ running = await isRunning({ cwd: path });
40
+ }
41
+ return {
42
+ sessionId,
43
+ path,
44
+ branch,
45
+ running,
46
+ ports
47
+ };
48
+ }
49
+ function printSessionStatus(status) {
50
+ const statusIcon = status.running ? chalk.green("\u25CF") : chalk.red("\u25CB");
51
+ const statusText = status.running ? chalk.green("running") : chalk.gray("stopped");
52
+ console.log(`${statusIcon} Session ${chalk.bold(status.sessionId)} ${statusText}`);
53
+ console.log(chalk.gray(` Path: ${status.path}`));
54
+ console.log(chalk.gray(` Branch: ${status.branch}`));
55
+ console.log(chalk.gray(" Ports:"));
56
+ for (const [name, port] of Object.entries(status.ports)) {
57
+ const isApp = name.includes("APP") || name.includes("WEB") || name.includes("WIDGET");
58
+ if (isApp) {
59
+ console.log(chalk.gray(` ${name}: http://localhost:${port}`));
60
+ } else {
61
+ console.log(chalk.gray(` ${name}: ${port}`));
62
+ }
63
+ }
64
+ console.log("");
65
+ }
66
+
67
+ export {
68
+ listSessions
69
+ };
@@ -0,0 +1,22 @@
1
+ // src/lib/ports.ts
2
+ function calculatePorts(config, sessionId) {
3
+ const sessionNum = parseInt(sessionId, 10);
4
+ const basePort = config.portBase + sessionNum * 100;
5
+ const ports = {};
6
+ for (const [name, offset] of Object.entries(config.ports)) {
7
+ ports[name] = basePort + offset;
8
+ }
9
+ return ports;
10
+ }
11
+ function formatPortsTable(ports) {
12
+ const lines = [];
13
+ for (const [name, port] of Object.entries(ports)) {
14
+ lines.push(` ${name}: http://localhost:${port}`);
15
+ }
16
+ return lines.join("\n");
17
+ }
18
+
19
+ export {
20
+ calculatePorts,
21
+ formatPortsTable
22
+ };
@@ -0,0 +1,78 @@
1
+ import {
2
+ removeWorktree
3
+ } from "./chunk-Y3GR6XK7.js";
4
+ import {
5
+ down
6
+ } from "./chunk-GBN67HYD.js";
7
+ import {
8
+ loadConfig
9
+ } from "./chunk-25WQHUYW.js";
10
+ import {
11
+ SessionStore
12
+ } from "./chunk-6YMQTISJ.js";
13
+
14
+ // src/commands/destroy.ts
15
+ import { existsSync } from "fs";
16
+ import { resolve } from "path";
17
+ import chalk from "chalk";
18
+ async function destroySession(projectRoot, sessionId, options) {
19
+ const config = await loadConfig(projectRoot);
20
+ const store = new SessionStore();
21
+ try {
22
+ if (options.all) {
23
+ console.log(chalk.blue("Destroying all sessions..."));
24
+ const sessions = store.listByProject(projectRoot);
25
+ if (sessions.length === 0) {
26
+ console.log(chalk.gray("No sessions found."));
27
+ return;
28
+ }
29
+ for (const session2 of sessions) {
30
+ await destroySingleSession(projectRoot, session2.session_id, session2.session_dir, session2.branch, session2.in_place === 1);
31
+ store.markDestroyed(projectRoot, session2.session_id);
32
+ }
33
+ console.log(chalk.green(`
34
+ Destroyed ${sessions.length} session(s).`));
35
+ return;
36
+ }
37
+ if (!sessionId) {
38
+ console.error(chalk.red("Error: Session ID required. Use --all to destroy all sessions."));
39
+ process.exit(1);
40
+ }
41
+ if (!/^\d{3}$/.test(sessionId)) {
42
+ console.error(chalk.red("Error: Session ID must be exactly 3 digits (001-999)"));
43
+ process.exit(1);
44
+ }
45
+ const session = store.findSession(projectRoot, sessionId);
46
+ if (!session) {
47
+ console.error(chalk.red(`Error: Session ${sessionId} not found.`));
48
+ process.exit(1);
49
+ }
50
+ await destroySingleSession(projectRoot, sessionId, session.session_dir, session.branch, session.in_place === 1);
51
+ store.markDestroyed(projectRoot, sessionId);
52
+ console.log(chalk.green(`
53
+ Session ${sessionId} destroyed.`));
54
+ } finally {
55
+ store.close();
56
+ }
57
+ }
58
+ async function destroySingleSession(projectRoot, sessionId, sessionDir, branchName, inPlace) {
59
+ console.log(chalk.blue(`
60
+ Destroying session ${sessionId}...`));
61
+ const envFile = resolve(sessionDir, ".env.session");
62
+ if (existsSync(envFile)) {
63
+ console.log(chalk.gray(" Stopping Docker containers..."));
64
+ try {
65
+ await down({ cwd: sessionDir });
66
+ } catch {
67
+ }
68
+ }
69
+ if (!inPlace) {
70
+ console.log(chalk.gray(" Removing git worktree..."));
71
+ await removeWorktree(projectRoot, sessionDir, branchName);
72
+ }
73
+ console.log(chalk.green(` Session ${sessionId} destroyed.`));
74
+ }
75
+
76
+ export {
77
+ destroySession
78
+ };