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 +37 -11
- package/bin/dev-prism.js +20 -233
- package/dist/chunk-3ATDGV4Y.js +22 -0
- package/dist/chunk-3MSC3CGG.js +78 -0
- package/dist/chunk-6YMQTISJ.js +84 -0
- package/dist/chunk-7YGOMAJG.js +106 -0
- package/dist/chunk-AW2FJGXA.js +38 -0
- package/dist/chunk-C4WLIOBR.js +67 -0
- package/dist/chunk-D6QWWXZD.js +49 -0
- package/dist/chunk-HCCZKLC4.js +64 -0
- package/dist/chunk-HZUN6NRB.js +70 -0
- package/dist/chunk-J36LRUXM.js +60 -0
- package/dist/chunk-JHR4WADC.js +200 -0
- package/dist/chunk-JIU574KX.js +41 -0
- package/dist/chunk-LOVO4P3Y.js +41 -0
- package/dist/chunk-P3ETW2KK.js +166 -0
- package/dist/chunk-QUMZI5KK.js +98 -0
- package/dist/chunk-SSQ7XBY2.js +30 -0
- package/dist/chunk-UHI2QJFI.js +200 -0
- package/dist/chunk-X5A6H4Q7.js +70 -0
- package/dist/chunk-Y3GR6XK7.js +71 -0
- package/dist/commands/claude.js +3 -102
- package/dist/commands/create.js +6 -5
- package/dist/commands/destroy.js +4 -3
- package/dist/commands/info.js +7 -0
- package/dist/commands/list.js +4 -4
- package/dist/commands/logs.js +8 -0
- package/dist/commands/prune.js +10 -0
- package/dist/commands/start.js +9 -0
- package/dist/commands/stop-all.js +9 -0
- package/dist/commands/stop.js +7 -0
- package/dist/index.js +60 -10
- package/dist/lib/env.js +3 -1
- package/dist/lib/ports.js +1 -1
- package/dist/lib/store.js +6 -0
- package/dist/lib/worktree.js +1 -5
- package/package.json +14 -4
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
|
-
#
|
|
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
|
|
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
|
|
47
|
-
```
|
|
58
|
+
dev-prism create --in-place
|
|
48
59
|
|
|
49
|
-
|
|
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
|
-
###
|
|
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
|
|
175
|
-
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
};
|