dev-prism 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,116 @@
1
+ // src/lib/worktree.ts
2
+ import { existsSync, rmSync } from "fs";
3
+ import { execa } from "execa";
4
+ async function branchExists(projectRoot, branchName) {
5
+ try {
6
+ await execa("git", ["rev-parse", "--verify", branchName], {
7
+ cwd: projectRoot,
8
+ stdio: "pipe"
9
+ });
10
+ return true;
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
15
+ async function findNextSessionId(projectRoot, sessionsDir) {
16
+ const sessions = await getSessionWorktrees(projectRoot);
17
+ const usedIds = new Set(sessions.map((s) => parseInt(s.sessionId, 10)));
18
+ for (let i = 1; i <= 999; i++) {
19
+ if (!usedIds.has(i)) {
20
+ const sessionId = String(i).padStart(3, "0");
21
+ if (sessionsDir) {
22
+ const sessionDir = `${sessionsDir}/session-${sessionId}`;
23
+ if (existsSync(sessionDir)) {
24
+ continue;
25
+ }
26
+ }
27
+ return sessionId;
28
+ }
29
+ }
30
+ throw new Error("No available session IDs (001-999 all in use)");
31
+ }
32
+ function generateDefaultBranchName(sessionId) {
33
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
34
+ return `session/${today}/${sessionId}`;
35
+ }
36
+ async function createWorktree(projectRoot, sessionDir, branchName) {
37
+ if (existsSync(sessionDir)) {
38
+ throw new Error(`Session directory already exists: ${sessionDir}`);
39
+ }
40
+ const exists = await branchExists(projectRoot, branchName);
41
+ if (exists) {
42
+ await execa("git", ["worktree", "add", sessionDir, branchName], {
43
+ cwd: projectRoot,
44
+ stdio: "inherit"
45
+ });
46
+ } else {
47
+ await execa("git", ["worktree", "add", sessionDir, "-b", branchName, "HEAD"], {
48
+ cwd: projectRoot,
49
+ stdio: "inherit"
50
+ });
51
+ }
52
+ }
53
+ async function removeWorktree(projectRoot, sessionDir, branchName) {
54
+ if (existsSync(sessionDir)) {
55
+ try {
56
+ await execa("git", ["worktree", "remove", "--force", sessionDir], {
57
+ cwd: projectRoot,
58
+ stdio: "inherit"
59
+ });
60
+ } catch {
61
+ rmSync(sessionDir, { recursive: true, force: true });
62
+ }
63
+ }
64
+ try {
65
+ await execa("git", ["branch", "-D", branchName], {
66
+ cwd: projectRoot,
67
+ stdio: "pipe"
68
+ // Don't show output, branch might not exist
69
+ });
70
+ } catch {
71
+ }
72
+ }
73
+ async function listWorktrees(projectRoot) {
74
+ const { stdout } = await execa("git", ["worktree", "list", "--porcelain"], {
75
+ cwd: projectRoot
76
+ });
77
+ const worktrees = [];
78
+ let current = null;
79
+ for (const line of stdout.split("\n")) {
80
+ if (line.startsWith("worktree ")) {
81
+ if (current) {
82
+ worktrees.push(current);
83
+ }
84
+ current = { path: line.replace("worktree ", ""), branch: "", commit: "" };
85
+ } else if (line.startsWith("HEAD ") && current) {
86
+ current.commit = line.replace("HEAD ", "");
87
+ } else if (line.startsWith("branch ") && current) {
88
+ current.branch = line.replace("branch refs/heads/", "");
89
+ }
90
+ }
91
+ if (current) {
92
+ worktrees.push(current);
93
+ }
94
+ return worktrees;
95
+ }
96
+ async function getSessionWorktrees(projectRoot) {
97
+ const worktrees = await listWorktrees(projectRoot);
98
+ const sessionPattern = /\/session-(\d{3})$/;
99
+ return worktrees.filter((wt) => sessionPattern.test(wt.path)).map((wt) => {
100
+ const match = wt.path.match(sessionPattern);
101
+ return {
102
+ sessionId: match[1],
103
+ path: wt.path,
104
+ branch: wt.branch
105
+ };
106
+ });
107
+ }
108
+
109
+ export {
110
+ findNextSessionId,
111
+ generateDefaultBranchName,
112
+ createWorktree,
113
+ removeWorktree,
114
+ listWorktrees,
115
+ getSessionWorktrees
116
+ };
@@ -0,0 +1,59 @@
1
+ // src/lib/env.ts
2
+ import { writeFileSync, mkdirSync } from "fs";
3
+ import { resolve, dirname } from "path";
4
+ function generateEnvContent(sessionId, ports, projectName) {
5
+ const lines = [
6
+ `# Auto-generated session environment`,
7
+ `SESSION_ID=${sessionId}`,
8
+ `COMPOSE_PROJECT_NAME=${projectName}-${sessionId}`,
9
+ "",
10
+ "# Ports (used by docker-compose.session.yml)"
11
+ ];
12
+ for (const [name, port] of Object.entries(ports)) {
13
+ lines.push(`${name}=${port}`);
14
+ }
15
+ return lines.join("\n") + "\n";
16
+ }
17
+ function writeEnvFile(sessionDir, sessionId, ports, projectName) {
18
+ const content = generateEnvContent(sessionId, ports, projectName);
19
+ const filePath = resolve(sessionDir, ".env.session");
20
+ writeFileSync(filePath, content, "utf-8");
21
+ return filePath;
22
+ }
23
+ function renderAppEnv(template, ports) {
24
+ const result = {};
25
+ for (const [key, value] of Object.entries(template)) {
26
+ let rendered = value;
27
+ for (const [portName, portValue] of Object.entries(ports)) {
28
+ rendered = rendered.replace(
29
+ new RegExp(`\\$\\{${portName}\\}`, "g"),
30
+ String(portValue)
31
+ );
32
+ }
33
+ result[key] = rendered;
34
+ }
35
+ return result;
36
+ }
37
+ function writeAppEnvFiles(config, sessionDir, sessionId, ports) {
38
+ if (!config.appEnv) return [];
39
+ const writtenFiles = [];
40
+ for (const [appPath, template] of Object.entries(config.appEnv)) {
41
+ const env = renderAppEnv(template, ports);
42
+ const lines = [`# Auto-generated for session ${sessionId}`, `SESSION_ID=${sessionId}`];
43
+ for (const [key, value] of Object.entries(env)) {
44
+ lines.push(`${key}=${value}`);
45
+ }
46
+ const content = lines.join("\n") + "\n";
47
+ const envFilePath = resolve(sessionDir, appPath, ".env.session");
48
+ mkdirSync(dirname(envFilePath), { recursive: true });
49
+ writeFileSync(envFilePath, content, "utf-8");
50
+ writtenFiles.push(envFilePath);
51
+ }
52
+ return writtenFiles;
53
+ }
54
+
55
+ export {
56
+ generateEnvContent,
57
+ writeEnvFile,
58
+ writeAppEnvFiles
59
+ };
@@ -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}: ${port}`);
15
+ }
16
+ return lines.join("\n");
17
+ }
18
+
19
+ export {
20
+ calculatePorts,
21
+ formatPortsTable
22
+ };
@@ -0,0 +1,171 @@
1
+ import {
2
+ calculatePorts,
3
+ formatPortsTable
4
+ } from "./chunk-PJKUD2N2.js";
5
+ import {
6
+ createWorktree,
7
+ findNextSessionId,
8
+ generateDefaultBranchName
9
+ } from "./chunk-GWDGC2OE.js";
10
+ import {
11
+ getSessionDir,
12
+ getSessionsDir,
13
+ loadConfig
14
+ } from "./chunk-25WQHUYW.js";
15
+ import {
16
+ logs,
17
+ up
18
+ } from "./chunk-VR3QWHHB.js";
19
+ import {
20
+ writeAppEnvFiles,
21
+ writeEnvFile
22
+ } from "./chunk-LEHA65A7.js";
23
+
24
+ // src/commands/create.ts
25
+ import { existsSync, mkdirSync, copyFileSync, readFileSync, writeFileSync } from "fs";
26
+ import { basename, join } from "path";
27
+ import chalk from "chalk";
28
+ import { execa } from "execa";
29
+ function updateEnvDatabaseUrl(envPath, newDbUrl) {
30
+ if (!existsSync(envPath)) return;
31
+ let content = readFileSync(envPath, "utf-8");
32
+ if (content.includes("DATABASE_URL=")) {
33
+ content = content.replace(/^DATABASE_URL=.*/m, `DATABASE_URL=${newDbUrl}`);
34
+ } else {
35
+ content += `
36
+ DATABASE_URL=${newDbUrl}
37
+ `;
38
+ }
39
+ writeFileSync(envPath, content);
40
+ }
41
+ async function createSession(projectRoot, sessionId, options) {
42
+ const config = await loadConfig(projectRoot);
43
+ const sessionsDir = getSessionsDir(config, projectRoot);
44
+ if (!sessionId) {
45
+ sessionId = await findNextSessionId(projectRoot, sessionsDir);
46
+ console.log(chalk.gray(`Auto-assigned session ID: ${sessionId}`));
47
+ }
48
+ if (!/^\d{3}$/.test(sessionId)) {
49
+ console.error(chalk.red("Error: Session ID must be exactly 3 digits (001-999)"));
50
+ process.exit(1);
51
+ }
52
+ const inPlace = options.inPlace ?? false;
53
+ const branchName = options.branch || generateDefaultBranchName(sessionId);
54
+ const mode = options.mode || "docker";
55
+ console.log(chalk.blue(`Creating session ${sessionId} (${mode} mode${inPlace ? ", in-place" : ""})...`));
56
+ if (!inPlace) {
57
+ console.log(chalk.gray(`Branch: ${branchName}`));
58
+ }
59
+ const ports = calculatePorts(config, sessionId);
60
+ console.log(chalk.gray("\nPorts:"));
61
+ console.log(chalk.gray(formatPortsTable(ports)));
62
+ let sessionDir;
63
+ if (inPlace) {
64
+ sessionDir = projectRoot;
65
+ console.log(chalk.blue("\nUsing current directory (in-place mode)..."));
66
+ console.log(chalk.green(` Directory: ${sessionDir}`));
67
+ } else {
68
+ if (!existsSync(sessionsDir)) {
69
+ mkdirSync(sessionsDir, { recursive: true });
70
+ }
71
+ sessionDir = getSessionDir(config, projectRoot, sessionId);
72
+ console.log(chalk.blue("\nCreating git worktree..."));
73
+ await createWorktree(projectRoot, sessionDir, branchName);
74
+ console.log(chalk.green(` Created: ${sessionDir}`));
75
+ const sessionDbUrl = `postgresql://postgres:postgres@localhost:${ports.POSTGRES_PORT}/postgres`;
76
+ const envFilesToCopy = [
77
+ "apps/convas-app/.env",
78
+ "packages/convas-db/.env"
79
+ ];
80
+ for (const envFile of envFilesToCopy) {
81
+ const srcPath = join(projectRoot, envFile);
82
+ const destPath = join(sessionDir, envFile);
83
+ if (existsSync(srcPath)) {
84
+ copyFileSync(srcPath, destPath);
85
+ updateEnvDatabaseUrl(destPath, sessionDbUrl);
86
+ console.log(chalk.green(` Copied: ${envFile} (updated DATABASE_URL)`));
87
+ }
88
+ }
89
+ }
90
+ console.log(chalk.blue("\nGenerating .env.session..."));
91
+ const projectName = config.projectName ?? basename(projectRoot);
92
+ const envPath = writeEnvFile(sessionDir, sessionId, ports, projectName);
93
+ console.log(chalk.green(` Written: ${envPath}`));
94
+ const appEnvFiles = writeAppEnvFiles(config, sessionDir, sessionId, ports);
95
+ for (const file of appEnvFiles) {
96
+ console.log(chalk.green(` Written: ${file}`));
97
+ }
98
+ console.log(chalk.blue("\nStarting Docker services..."));
99
+ let profiles;
100
+ if (mode === "docker") {
101
+ const allApps = config.apps ?? ["app", "web", "widget"];
102
+ const excludeApps = options.without ?? [];
103
+ profiles = allApps.filter((app) => !excludeApps.includes(app));
104
+ if (excludeApps.length > 0) {
105
+ console.log(chalk.gray(` Excluding apps: ${excludeApps.join(", ")}`));
106
+ }
107
+ }
108
+ await up({ cwd: sessionDir, profiles });
109
+ console.log(chalk.blue("Waiting for services to be ready..."));
110
+ await new Promise((resolve) => setTimeout(resolve, 3e3));
111
+ if (config.setup.length > 0) {
112
+ console.log(chalk.blue("\nRunning setup commands..."));
113
+ const setupEnv = {
114
+ ...process.env,
115
+ SESSION_ID: sessionId,
116
+ // Add DATABASE_URL for db commands
117
+ DATABASE_URL: `postgresql://postgres:postgres@localhost:${ports.POSTGRES_PORT}/postgres`
118
+ };
119
+ for (const [name, port] of Object.entries(ports)) {
120
+ setupEnv[name] = String(port);
121
+ }
122
+ for (const cmd of config.setup) {
123
+ console.log(chalk.gray(` Running: ${cmd}`));
124
+ const [command, ...args] = cmd.split(" ");
125
+ try {
126
+ await execa(command, args, {
127
+ cwd: sessionDir,
128
+ stdio: "inherit",
129
+ env: setupEnv
130
+ });
131
+ } catch {
132
+ console.warn(chalk.yellow(` Warning: Command failed: ${cmd}`));
133
+ }
134
+ }
135
+ }
136
+ console.log(chalk.green(`
137
+ Session ${sessionId} ready!`));
138
+ console.log(chalk.gray(`Directory: ${sessionDir}`));
139
+ if (mode === "docker") {
140
+ console.log(chalk.gray("\nDocker mode - all services in containers."));
141
+ console.log(chalk.gray("View logs: docker compose -f docker-compose.session.yml logs -f"));
142
+ } else {
143
+ console.log(chalk.gray("\nNative mode - run apps with: pnpm dev"));
144
+ }
145
+ console.log(chalk.gray("\nURLs:"));
146
+ for (const [name, port] of Object.entries(ports)) {
147
+ if (name.includes("APP") || name.includes("WEB") || name.includes("WIDGET")) {
148
+ console.log(chalk.cyan(` ${name}: http://localhost:${port}`));
149
+ }
150
+ }
151
+ if (options.detach === false) {
152
+ console.log(chalk.blue("\nStreaming logs (Ctrl+C to stop)..."));
153
+ console.log(chalk.gray("\u2500".repeat(60)));
154
+ try {
155
+ await logs({ cwd: sessionDir, profiles });
156
+ } catch (error) {
157
+ const execaError = error;
158
+ if (execaError.signal === "SIGINT") {
159
+ console.log(chalk.gray("\n\u2500".repeat(60)));
160
+ console.log(chalk.yellow("\nLog streaming stopped. Services are still running."));
161
+ console.log(chalk.gray(`Resume logs: cd ${sessionDir} && docker compose -f docker-compose.session.yml --env-file .env.session logs -f`));
162
+ } else {
163
+ throw error;
164
+ }
165
+ }
166
+ }
167
+ }
168
+
169
+ export {
170
+ createSession
171
+ };
@@ -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-VR3QWHHB.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,57 @@
1
+ // src/lib/docker.ts
2
+ import { execa } from "execa";
3
+ var COMPOSE_FILE = "docker-compose.session.yml";
4
+ var ENV_FILE = ".env.session";
5
+ async function compose(args, options) {
6
+ const profileFlags = options.profiles?.flatMap((p) => ["--profile", p]) ?? [];
7
+ const fullArgs = [
8
+ "compose",
9
+ "-f",
10
+ COMPOSE_FILE,
11
+ "--env-file",
12
+ ENV_FILE,
13
+ ...profileFlags,
14
+ ...args
15
+ ];
16
+ return execa("docker", fullArgs, {
17
+ cwd: options.cwd,
18
+ stdio: "inherit"
19
+ });
20
+ }
21
+ async function up(options) {
22
+ const detach = options.detach !== false;
23
+ const args = detach ? ["up", "-d"] : ["up"];
24
+ await compose(args, options);
25
+ }
26
+ async function logs(options) {
27
+ await compose(["logs", "-f", "--tail=50"], options);
28
+ }
29
+ async function down(options) {
30
+ await compose(["down", "-v"], options);
31
+ }
32
+ async function ps(options) {
33
+ const result = await execa(
34
+ "docker",
35
+ ["compose", "-f", COMPOSE_FILE, "--env-file", ENV_FILE, "ps", "--format", "json"],
36
+ { cwd: options.cwd, reject: false }
37
+ );
38
+ return result.stdout;
39
+ }
40
+ async function isRunning(options) {
41
+ try {
42
+ const output = await ps(options);
43
+ if (!output.trim()) return false;
44
+ const services = output.trim().split("\n").map((line) => JSON.parse(line));
45
+ return services.some((s) => s.State === "running");
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
50
+
51
+ export {
52
+ up,
53
+ logs,
54
+ down,
55
+ ps,
56
+ isRunning
57
+ };
@@ -0,0 +1,67 @@
1
+ import {
2
+ getSessionWorktrees,
3
+ removeWorktree
4
+ } from "./chunk-GWDGC2OE.js";
5
+ import {
6
+ loadConfig
7
+ } from "./chunk-25WQHUYW.js";
8
+ import {
9
+ down
10
+ } from "./chunk-VR3QWHHB.js";
11
+
12
+ // src/commands/destroy.ts
13
+ import { existsSync } from "fs";
14
+ import { resolve } from "path";
15
+ import chalk from "chalk";
16
+ async function destroySession(projectRoot, sessionId, options) {
17
+ const config = await loadConfig(projectRoot);
18
+ const sessions = await getSessionWorktrees(projectRoot);
19
+ if (options.all) {
20
+ console.log(chalk.blue("Destroying all sessions..."));
21
+ if (sessions.length === 0) {
22
+ console.log(chalk.gray("No sessions found."));
23
+ return;
24
+ }
25
+ for (const session2 of sessions) {
26
+ await destroySingleSession(projectRoot, session2.sessionId, session2.path, session2.branch);
27
+ }
28
+ console.log(chalk.green(`
29
+ Destroyed ${sessions.length} session(s).`));
30
+ return;
31
+ }
32
+ if (!sessionId) {
33
+ console.error(chalk.red("Error: Session ID required. Use --all to destroy all sessions."));
34
+ process.exit(1);
35
+ }
36
+ if (!/^\d{3}$/.test(sessionId)) {
37
+ console.error(chalk.red("Error: Session ID must be exactly 3 digits (001-999)"));
38
+ process.exit(1);
39
+ }
40
+ const session = sessions.find((s) => s.sessionId === sessionId);
41
+ if (!session) {
42
+ console.error(chalk.red(`Error: Session ${sessionId} not found.`));
43
+ process.exit(1);
44
+ }
45
+ await destroySingleSession(projectRoot, sessionId, session.path, session.branch);
46
+ console.log(chalk.green(`
47
+ Session ${sessionId} destroyed.`));
48
+ }
49
+ async function destroySingleSession(projectRoot, sessionId, sessionDir, branchName) {
50
+ console.log(chalk.blue(`
51
+ Destroying session ${sessionId}...`));
52
+ const envFile = resolve(sessionDir, ".env.session");
53
+ if (existsSync(envFile)) {
54
+ console.log(chalk.gray(" Stopping Docker containers..."));
55
+ try {
56
+ await down({ cwd: sessionDir });
57
+ } catch {
58
+ }
59
+ }
60
+ console.log(chalk.gray(" Removing git worktree..."));
61
+ await removeWorktree(projectRoot, sessionDir, branchName);
62
+ console.log(chalk.green(` Session ${sessionId} destroyed.`));
63
+ }
64
+
65
+ export {
66
+ destroySession
67
+ };
@@ -0,0 +1,6 @@
1
+ interface ClaudeOptions {
2
+ force?: boolean;
3
+ }
4
+ declare function installClaude(projectRoot: string, options: ClaudeOptions): Promise<void>;
5
+
6
+ export { type ClaudeOptions, installClaude };
@@ -0,0 +1,105 @@
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
+ export {
104
+ installClaude
105
+ };
@@ -0,0 +1,10 @@
1
+ interface CreateOptions {
2
+ mode?: 'docker' | 'native';
3
+ branch?: string;
4
+ detach?: boolean;
5
+ without?: string[];
6
+ inPlace?: boolean;
7
+ }
8
+ declare function createSession(projectRoot: string, sessionId: string | undefined, options: CreateOptions): Promise<void>;
9
+
10
+ export { type CreateOptions, createSession };