dev-prism 0.2.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.
package/README.md CHANGED
@@ -17,10 +17,13 @@ All Docker configuration lives in `docker-compose.session.yml` in your project -
17
17
 
18
18
  ## Features
19
19
 
20
- - **Git worktrees** for isolated working directories
20
+ - **Git worktrees** for isolated working directories (or in-place mode)
21
21
  - **Docker Compose** handles all container orchestration
22
- - **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
23
25
  - **Two modes**: Docker (apps in containers) or Native (apps run locally)
26
+ - **Claude Code integration** built-in (`dev-prism claude`)
24
27
  - **Portable**: Works with any project
25
28
 
26
29
  ## Installation
@@ -36,17 +39,27 @@ pnpm add -D dev-prism
36
39
  ### Create a session
37
40
 
38
41
  ```bash
39
- # Docker mode (default) - apps run in containers
42
+ # Auto-assign session ID
43
+ dev-prism create
44
+
45
+ # Explicit session ID
40
46
  dev-prism create 001
41
47
 
48
+ # Custom branch name
49
+ dev-prism create --branch feature/my-feature
50
+
42
51
  # Native mode - only infrastructure in Docker, apps run via pnpm dev
43
- 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
44
56
 
45
57
  # In-place mode - use current directory instead of creating worktree
46
- dev-prism create 001 --in-place
47
- ```
58
+ dev-prism create --in-place
48
59
 
49
- **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
+ ```
50
63
 
51
64
  ### List sessions
52
65
 
@@ -74,11 +87,20 @@ dev-prism stop-all # Stop all sessions
74
87
  dev-prism logs 001
75
88
  ```
76
89
 
77
- ### Destroy a session
90
+ ### Cleanup
78
91
 
79
92
  ```bash
80
93
  dev-prism destroy 001 # Destroy specific session
81
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
82
104
  ```
83
105
 
84
106
  ## Port Allocation
@@ -171,9 +193,11 @@ services:
171
193
 
172
194
  ## How It Works
173
195
 
174
- 1. **Create session**: `dev-prism create 001`
175
- - 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`)
176
199
  - Generates `.env.session` with calculated ports
200
+ - Records session in local SQLite database
177
201
  - Runs `docker compose --env-file .env.session up -d`
178
202
  - Runs setup commands
179
203
 
@@ -182,6 +206,8 @@ services:
182
206
  3. **Docker mode** (`--profile apps`): All services including apps run in containers
183
207
  4. **Native mode**: Only infrastructure runs; apps use `pnpm dev` with `.env.session`
184
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
+
185
211
  ## Generated Files
186
212
 
187
213
  ```
@@ -207,4 +233,4 @@ To use in another project:
207
233
  1. Install: `pnpm add -D dev-prism`
208
234
  2. Create `session.config.mjs` with port offsets
209
235
  3. Create `docker-compose.session.yml` with `${VAR}` placeholders
210
- 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 ?? [];
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 ?? [];
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 ?? [];
220
- const profileFlags = allApps.flatMap((p) => ['--profile', p]);
221
- // Explicitly list all services to stop (infrastructure + apps)
222
- const allServices = ['postgres', 'mailpit', ...allApps];
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,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
+ };
@@ -0,0 +1,84 @@
1
+ // src/lib/store.ts
2
+ import Database from "better-sqlite3";
3
+ import { existsSync, mkdirSync } from "fs";
4
+ import { dirname, join } from "path";
5
+ import { homedir } from "os";
6
+ var SCHEMA = `
7
+ CREATE TABLE IF NOT EXISTS sessions (
8
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
9
+ session_id TEXT NOT NULL,
10
+ project_root TEXT NOT NULL,
11
+ session_dir TEXT NOT NULL,
12
+ branch TEXT NOT NULL DEFAULT '',
13
+ mode TEXT NOT NULL DEFAULT 'docker',
14
+ in_place INTEGER NOT NULL DEFAULT 0,
15
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
16
+ destroyed_at TEXT,
17
+ UNIQUE(session_id, project_root)
18
+ );
19
+ `;
20
+ function defaultDbPath() {
21
+ return join(homedir(), ".dev-prism", "sessions.db");
22
+ }
23
+ var SessionStore = class {
24
+ db;
25
+ constructor(dbPath) {
26
+ const path = dbPath ?? defaultDbPath();
27
+ if (path !== ":memory:") {
28
+ const dir = dirname(path);
29
+ if (!existsSync(dir)) {
30
+ mkdirSync(dir, { recursive: true });
31
+ }
32
+ }
33
+ this.db = new Database(path);
34
+ this.db.pragma("journal_mode = WAL");
35
+ this.db.pragma("busy_timeout = 3000");
36
+ this.db.exec(SCHEMA);
37
+ }
38
+ insert(row) {
39
+ const stmt = this.db.prepare(`
40
+ INSERT INTO sessions (session_id, project_root, session_dir, branch, mode, in_place)
41
+ VALUES (@session_id, @project_root, @session_dir, @branch, @mode, @in_place)
42
+ `);
43
+ const info = stmt.run({
44
+ session_id: row.sessionId,
45
+ project_root: row.projectRoot,
46
+ session_dir: row.sessionDir,
47
+ branch: row.branch ?? "",
48
+ mode: row.mode ?? "docker",
49
+ in_place: row.inPlace ? 1 : 0
50
+ });
51
+ return this.db.prepare("SELECT * FROM sessions WHERE id = ?").get(info.lastInsertRowid);
52
+ }
53
+ listByProject(projectRoot) {
54
+ return this.db.prepare("SELECT * FROM sessions WHERE project_root = ? AND destroyed_at IS NULL ORDER BY session_id").all(projectRoot);
55
+ }
56
+ listAll() {
57
+ return this.db.prepare("SELECT * FROM sessions WHERE destroyed_at IS NULL ORDER BY project_root, session_id").all();
58
+ }
59
+ findSession(projectRoot, sessionId) {
60
+ return this.db.prepare("SELECT * FROM sessions WHERE project_root = ? AND session_id = ? AND destroyed_at IS NULL").get(projectRoot, sessionId);
61
+ }
62
+ findByDir(sessionDir) {
63
+ return this.db.prepare("SELECT * FROM sessions WHERE session_dir = ? AND destroyed_at IS NULL").get(sessionDir);
64
+ }
65
+ getUsedSessionIds(projectRoot) {
66
+ const rows = this.db.prepare("SELECT session_id FROM sessions WHERE project_root = ? AND destroyed_at IS NULL").all(projectRoot);
67
+ return new Set(rows.map((r) => r.session_id));
68
+ }
69
+ markDestroyed(projectRoot, sessionId) {
70
+ const info = this.db.prepare("UPDATE sessions SET destroyed_at = datetime('now') WHERE project_root = ? AND session_id = ? AND destroyed_at IS NULL").run(projectRoot, sessionId);
71
+ return info.changes > 0;
72
+ }
73
+ remove(projectRoot, sessionId) {
74
+ const info = this.db.prepare("DELETE FROM sessions WHERE project_root = ? AND session_id = ?").run(projectRoot, sessionId);
75
+ return info.changes > 0;
76
+ }
77
+ close() {
78
+ this.db.close();
79
+ }
80
+ };
81
+
82
+ export {
83
+ SessionStore
84
+ };
@@ -0,0 +1,106 @@
1
+ // src/commands/claude.ts
2
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, appendFileSync } from "fs";
3
+ import { join } from "path";
4
+ import chalk from "chalk";
5
+ var SKILL_CONTENT = `---
6
+ allowed-tools: Bash(dev-prism *)
7
+ description: Manage isolated development sessions (create, list, start, stop, destroy)
8
+ ---
9
+
10
+ # Dev Session Manager
11
+
12
+ Manage isolated parallel development sessions using git worktrees and Docker.
13
+
14
+ ## Parse Intent from: $ARGUMENTS
15
+
16
+ - "create" / "new" -> dev-prism create
17
+ - "list" / "status" -> dev-prism list
18
+ - "start <id>" -> dev-prism start <id>
19
+ - "stop <id>" -> dev-prism stop <id>
20
+ - "destroy <id>" -> dev-prism destroy <id>
21
+ - "logs <id>" -> dev-prism logs <id>
22
+ - "stop all" -> dev-prism stop-all
23
+ - "prune" -> dev-prism prune
24
+
25
+ ## Commands
26
+
27
+ Run from the project root (where session.config.mjs exists).
28
+
29
+ After running commands, explain:
30
+ 1. What happened
31
+ 2. Relevant ports/paths
32
+ 3. Next steps
33
+
34
+ Warn before destructive operations (destroy, prune).
35
+ `;
36
+ var CLAUDE_MD_SECTION = `
37
+ ## Dev Sessions
38
+
39
+ Isolated parallel development sessions using git worktrees and Docker.
40
+
41
+ ### Commands
42
+ \`\`\`bash
43
+ dev-prism create [id] # Create session (auto-assigns ID)
44
+ dev-prism list # Show all sessions with status
45
+ dev-prism start <id> # Start stopped session
46
+ dev-prism stop <id> # Stop session (preserves data)
47
+ dev-prism stop-all # Stop all running sessions
48
+ dev-prism destroy <id> # Remove session completely
49
+ dev-prism logs <id> # Stream Docker logs
50
+ dev-prism prune # Remove stopped sessions
51
+ \`\`\`
52
+
53
+ ### Port Allocation
54
+ Port = 47000 + (sessionId \xD7 100) + offset
55
+
56
+ | Service | Offset | Session 001 |
57
+ |---------|--------|-------------|
58
+ | APP | 0 | 47100 |
59
+ | WEB | 1 | 47101 |
60
+ | POSTGRES| 10 | 47110 |
61
+
62
+ ### AI Notes
63
+ - In sessions, use DATABASE_URL from \`.env.session\`
64
+ - Run \`dev-prism list\` to discover ports
65
+ - Commands run from project root, not session worktrees
66
+ `;
67
+ async function installClaude(projectRoot, options) {
68
+ const skillDir = join(projectRoot, ".claude", "commands");
69
+ const skillPath = join(skillDir, "session.md");
70
+ const claudeMdPath = join(projectRoot, "CLAUDE.md");
71
+ if (existsSync(skillPath) && !options.force) {
72
+ console.log(chalk.yellow(`Skill already exists: ${skillPath}`));
73
+ console.log(chalk.gray("Use --force to overwrite"));
74
+ } else {
75
+ mkdirSync(skillDir, { recursive: true });
76
+ writeFileSync(skillPath, SKILL_CONTENT);
77
+ console.log(chalk.green(`Created: ${skillPath}`));
78
+ }
79
+ const marker = "## Dev Sessions";
80
+ if (existsSync(claudeMdPath)) {
81
+ const content = readFileSync(claudeMdPath, "utf-8");
82
+ if (content.includes(marker)) {
83
+ if (options.force) {
84
+ const beforeSection = content.split(marker)[0];
85
+ writeFileSync(claudeMdPath, beforeSection.trimEnd() + CLAUDE_MD_SECTION);
86
+ console.log(chalk.green(`Updated: ${claudeMdPath}`));
87
+ } else {
88
+ console.log(chalk.yellow("CLAUDE.md already has Dev Sessions section"));
89
+ console.log(chalk.gray("Use --force to overwrite"));
90
+ }
91
+ } else {
92
+ appendFileSync(claudeMdPath, CLAUDE_MD_SECTION);
93
+ console.log(chalk.green(`Updated: ${claudeMdPath}`));
94
+ }
95
+ } else {
96
+ writeFileSync(claudeMdPath, `# Project
97
+ ${CLAUDE_MD_SECTION}`);
98
+ console.log(chalk.green(`Created: ${claudeMdPath}`));
99
+ }
100
+ console.log(chalk.blue("\nClaude Code integration installed!"));
101
+ console.log(chalk.gray("Use /session in Claude Code to manage sessions."));
102
+ }
103
+
104
+ export {
105
+ installClaude
106
+ };