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.
- package/README.md +65 -21
- package/bin/dev-prism.js +20 -233
- package/dist/chunk-35SHBLIZ.js +69 -0
- package/dist/chunk-3ATDGV4Y.js +22 -0
- package/dist/chunk-3MSC3CGG.js +78 -0
- package/dist/chunk-5KDDYO6Y.js +168 -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-GBN67HYD.js +57 -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-LDK6QMR6.js +67 -0
- package/dist/chunk-LOVO4P3Y.js +41 -0
- package/dist/chunk-P3ETW2KK.js +166 -0
- package/dist/chunk-PKC2ZED2.js +168 -0
- package/dist/chunk-PS76Q3HD.js +168 -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/config.d.ts +1 -0
- package/dist/lib/docker.js +1 -1
- 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
|
@@ -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
|
-
#
|
|
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
|
|
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
|
|
43
|
-
```
|
|
58
|
+
dev-prism create --in-place
|
|
44
59
|
|
|
45
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
|
89
|
-
|
|
|
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
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
//
|
|
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
|
|
157
|
-
-
|
|
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
|
-
|
|
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
|
|
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 ?? ['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
|
-
|
|
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 ?? ['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
|
-
|
|
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
|
-
|
|
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
|
+
};
|