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.
@@ -0,0 +1,166 @@
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-GBN67HYD.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 = config.envFiles ?? [];
77
+ for (const envFile of envFilesToCopy) {
78
+ const srcPath = join(projectRoot, envFile);
79
+ const destPath = join(sessionDir, envFile);
80
+ if (existsSync(srcPath)) {
81
+ copyFileSync(srcPath, destPath);
82
+ updateEnvDatabaseUrl(destPath, sessionDbUrl);
83
+ console.log(chalk.green(` Copied: ${envFile} (updated DATABASE_URL)`));
84
+ }
85
+ }
86
+ }
87
+ console.log(chalk.blue("\nGenerating .env.session..."));
88
+ const projectName = config.projectName ?? basename(projectRoot);
89
+ const envPath = writeEnvFile(sessionDir, sessionId, ports, projectName);
90
+ console.log(chalk.green(` Written: ${envPath}`));
91
+ const appEnvFiles = writeAppEnvFiles(config, sessionDir, sessionId, ports);
92
+ for (const file of appEnvFiles) {
93
+ console.log(chalk.green(` Written: ${file}`));
94
+ }
95
+ console.log(chalk.blue("\nStarting Docker services..."));
96
+ let profiles;
97
+ if (mode === "docker") {
98
+ const allApps = config.apps ?? [];
99
+ const excludeApps = options.without ?? [];
100
+ profiles = allApps.filter((app) => !excludeApps.includes(app));
101
+ if (excludeApps.length > 0) {
102
+ console.log(chalk.gray(` Excluding apps: ${excludeApps.join(", ")}`));
103
+ }
104
+ }
105
+ await up({ cwd: sessionDir, profiles });
106
+ console.log(chalk.blue("Waiting for services to be ready..."));
107
+ await new Promise((resolve) => setTimeout(resolve, 3e3));
108
+ if (config.setup.length > 0) {
109
+ console.log(chalk.blue("\nRunning setup commands..."));
110
+ const setupEnv = {
111
+ ...process.env,
112
+ SESSION_ID: sessionId,
113
+ // Add DATABASE_URL for db commands
114
+ DATABASE_URL: `postgresql://postgres:postgres@localhost:${ports.POSTGRES_PORT}/postgres`
115
+ };
116
+ for (const [name, port] of Object.entries(ports)) {
117
+ setupEnv[name] = String(port);
118
+ }
119
+ for (const cmd of config.setup) {
120
+ console.log(chalk.gray(` Running: ${cmd}`));
121
+ const [command, ...args] = cmd.split(" ");
122
+ try {
123
+ await execa(command, args, {
124
+ cwd: sessionDir,
125
+ stdio: "inherit",
126
+ env: setupEnv
127
+ });
128
+ } catch {
129
+ console.warn(chalk.yellow(` Warning: Command failed: ${cmd}`));
130
+ }
131
+ }
132
+ }
133
+ console.log(chalk.green(`
134
+ Session ${sessionId} ready!`));
135
+ console.log(chalk.gray(`Directory: ${sessionDir}`));
136
+ if (mode === "docker") {
137
+ console.log(chalk.gray("\nDocker mode - all services in containers."));
138
+ console.log(chalk.gray("View logs: docker compose -f docker-compose.session.yml logs -f"));
139
+ } else {
140
+ console.log(chalk.gray("\nNative mode - run apps with: pnpm dev"));
141
+ }
142
+ console.log(chalk.gray("\nPorts:"));
143
+ for (const [name, port] of Object.entries(ports)) {
144
+ console.log(chalk.cyan(` ${name}: ${port}`));
145
+ }
146
+ if (options.detach === false) {
147
+ console.log(chalk.blue("\nStreaming logs (Ctrl+C to stop)..."));
148
+ console.log(chalk.gray("\u2500".repeat(60)));
149
+ try {
150
+ await logs({ cwd: sessionDir, profiles });
151
+ } catch (error) {
152
+ const execaError = error;
153
+ if (execaError.signal === "SIGINT") {
154
+ console.log(chalk.gray("\n\u2500".repeat(60)));
155
+ console.log(chalk.yellow("\nLog streaming stopped. Services are still running."));
156
+ console.log(chalk.gray(`Resume logs: cd ${sessionDir} && docker compose -f docker-compose.session.yml --env-file .env.session logs -f`));
157
+ } else {
158
+ throw error;
159
+ }
160
+ }
161
+ }
162
+ }
163
+
164
+ export {
165
+ createSession
166
+ };
@@ -0,0 +1,98 @@
1
+ import {
2
+ removeWorktree
3
+ } from "./chunk-Y3GR6XK7.js";
4
+ import {
5
+ down,
6
+ isRunning
7
+ } from "./chunk-GBN67HYD.js";
8
+ import {
9
+ loadConfig
10
+ } from "./chunk-25WQHUYW.js";
11
+ import {
12
+ SessionStore
13
+ } from "./chunk-6YMQTISJ.js";
14
+
15
+ // src/commands/prune.ts
16
+ import { existsSync } from "fs";
17
+ import { resolve } from "path";
18
+ import { createInterface } from "readline";
19
+ import chalk from "chalk";
20
+ async function pruneSessions(projectRoot, options) {
21
+ const config = await loadConfig(projectRoot);
22
+ const store = new SessionStore();
23
+ try {
24
+ const sessions = store.listByProject(projectRoot);
25
+ if (sessions.length === 0) {
26
+ console.log(chalk.gray("No sessions found."));
27
+ return;
28
+ }
29
+ const stoppedSessions = [];
30
+ for (const session of sessions) {
31
+ const envFile = resolve(session.session_dir, ".env.session");
32
+ let running = false;
33
+ if (existsSync(envFile)) {
34
+ running = await isRunning({ cwd: session.session_dir });
35
+ }
36
+ if (!running) {
37
+ stoppedSessions.push({
38
+ sessionId: session.session_id,
39
+ path: session.session_dir,
40
+ branch: session.branch,
41
+ inPlace: session.in_place === 1
42
+ });
43
+ }
44
+ }
45
+ if (stoppedSessions.length === 0) {
46
+ console.log(chalk.gray("No stopped sessions to prune."));
47
+ return;
48
+ }
49
+ console.log(chalk.yellow(`
50
+ Found ${stoppedSessions.length} stopped session(s) to prune:`));
51
+ for (const session of stoppedSessions) {
52
+ console.log(chalk.gray(` - Session ${session.sessionId} (${session.branch})`));
53
+ }
54
+ console.log("");
55
+ if (!options.yes) {
56
+ const rl = createInterface({
57
+ input: process.stdin,
58
+ output: process.stdout
59
+ });
60
+ const answer = await new Promise((resolve2) => {
61
+ rl.question(chalk.red("Are you sure you want to delete these sessions? This cannot be undone. [y/N] "), resolve2);
62
+ });
63
+ rl.close();
64
+ if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
65
+ console.log(chalk.gray("Cancelled."));
66
+ return;
67
+ }
68
+ }
69
+ console.log(chalk.blue("\nPruning stopped sessions...\n"));
70
+ for (const session of stoppedSessions) {
71
+ console.log(chalk.gray(` Removing session ${session.sessionId}...`));
72
+ try {
73
+ const envFile = resolve(session.path, ".env.session");
74
+ if (existsSync(envFile)) {
75
+ try {
76
+ await down({ cwd: session.path });
77
+ } catch {
78
+ }
79
+ }
80
+ if (!session.inPlace) {
81
+ await removeWorktree(projectRoot, session.path, session.branch);
82
+ }
83
+ store.markDestroyed(projectRoot, session.sessionId);
84
+ console.log(chalk.green(` Session ${session.sessionId} removed.`));
85
+ } catch {
86
+ console.log(chalk.yellow(` Warning: Could not fully remove session ${session.sessionId}`));
87
+ }
88
+ }
89
+ console.log(chalk.green(`
90
+ Pruned ${stoppedSessions.length} session(s).`));
91
+ } finally {
92
+ store.close();
93
+ }
94
+ }
95
+
96
+ export {
97
+ pruneSessions
98
+ };
@@ -0,0 +1,30 @@
1
+ import {
2
+ SessionStore
3
+ } from "./chunk-6YMQTISJ.js";
4
+
5
+ // src/commands/stop.ts
6
+ import chalk from "chalk";
7
+ import { execa } from "execa";
8
+ async function stopSession(projectRoot, sessionId) {
9
+ const store = new SessionStore();
10
+ let sessionDir;
11
+ try {
12
+ const session = store.findSession(projectRoot, sessionId);
13
+ if (!session) {
14
+ console.error(chalk.red(`Error: Session ${sessionId} not found.`));
15
+ process.exit(1);
16
+ }
17
+ sessionDir = session.session_dir;
18
+ } finally {
19
+ store.close();
20
+ }
21
+ await execa(
22
+ "docker",
23
+ ["compose", "-f", "docker-compose.session.yml", "--env-file", ".env.session", "stop"],
24
+ { cwd: sessionDir, stdio: "inherit" }
25
+ );
26
+ }
27
+
28
+ export {
29
+ stopSession
30
+ };
@@ -0,0 +1,200 @@
1
+ import {
2
+ writeAppEnvFiles,
3
+ writeEnvFile
4
+ } from "./chunk-J36LRUXM.js";
5
+ import {
6
+ calculatePorts,
7
+ formatPortsTable
8
+ } from "./chunk-PJKUD2N2.js";
9
+ import {
10
+ createWorktree,
11
+ findNextSessionId,
12
+ generateDefaultBranchName,
13
+ removeWorktree
14
+ } from "./chunk-Y3GR6XK7.js";
15
+ import {
16
+ down,
17
+ logs,
18
+ up
19
+ } from "./chunk-GBN67HYD.js";
20
+ import {
21
+ getSessionDir,
22
+ getSessionsDir,
23
+ loadConfig
24
+ } from "./chunk-25WQHUYW.js";
25
+ import {
26
+ SessionStore
27
+ } from "./chunk-6YMQTISJ.js";
28
+
29
+ // src/commands/create.ts
30
+ import { existsSync, mkdirSync, copyFileSync, readFileSync, writeFileSync } from "fs";
31
+ import { basename, join } from "path";
32
+ import chalk from "chalk";
33
+ import { execa } from "execa";
34
+ function updateEnvDatabaseUrl(envPath, newDbUrl) {
35
+ if (!existsSync(envPath)) return;
36
+ let content = readFileSync(envPath, "utf-8");
37
+ if (content.includes("DATABASE_URL=")) {
38
+ content = content.replace(/^DATABASE_URL=.*/m, `DATABASE_URL=${newDbUrl}`);
39
+ } else {
40
+ content += `
41
+ DATABASE_URL=${newDbUrl}
42
+ `;
43
+ }
44
+ writeFileSync(envPath, content);
45
+ }
46
+ async function createSession(projectRoot, sessionId, options) {
47
+ const config = await loadConfig(projectRoot);
48
+ const sessionsDir = getSessionsDir(config, projectRoot);
49
+ const store = new SessionStore();
50
+ try {
51
+ if (!sessionId) {
52
+ sessionId = findNextSessionId(store.getUsedSessionIds(projectRoot));
53
+ console.log(chalk.gray(`Auto-assigned session ID: ${sessionId}`));
54
+ }
55
+ if (!/^\d{3}$/.test(sessionId)) {
56
+ console.error(chalk.red("Error: Session ID must be exactly 3 digits (001-999)"));
57
+ process.exit(1);
58
+ }
59
+ const inPlace = options.inPlace ?? false;
60
+ const branchName = options.branch || generateDefaultBranchName(sessionId);
61
+ const mode = options.mode || "docker";
62
+ console.log(chalk.blue(`Creating session ${sessionId} (${mode} mode${inPlace ? ", in-place" : ""})...`));
63
+ if (!inPlace) {
64
+ console.log(chalk.gray(`Branch: ${branchName}`));
65
+ }
66
+ const ports = calculatePorts(config, sessionId);
67
+ console.log(chalk.gray("\nPorts:"));
68
+ console.log(chalk.gray(formatPortsTable(ports)));
69
+ let sessionDir;
70
+ if (inPlace) {
71
+ sessionDir = projectRoot;
72
+ console.log(chalk.blue("\nUsing current directory (in-place mode)..."));
73
+ console.log(chalk.green(` Directory: ${sessionDir}`));
74
+ } else {
75
+ if (!existsSync(sessionsDir)) {
76
+ mkdirSync(sessionsDir, { recursive: true });
77
+ }
78
+ sessionDir = getSessionDir(config, projectRoot, sessionId);
79
+ console.log(chalk.blue("\nCreating git worktree..."));
80
+ await createWorktree(projectRoot, sessionDir, branchName);
81
+ console.log(chalk.green(` Created: ${sessionDir}`));
82
+ const sessionDbUrl = `postgresql://postgres:postgres@localhost:${ports.POSTGRES_PORT}/postgres`;
83
+ const envFilesToCopy = config.envFiles ?? [];
84
+ for (const envFile of envFilesToCopy) {
85
+ const srcPath = join(projectRoot, envFile);
86
+ const destPath = join(sessionDir, envFile);
87
+ if (existsSync(srcPath)) {
88
+ copyFileSync(srcPath, destPath);
89
+ updateEnvDatabaseUrl(destPath, sessionDbUrl);
90
+ console.log(chalk.green(` Copied: ${envFile} (updated DATABASE_URL)`));
91
+ }
92
+ }
93
+ }
94
+ console.log(chalk.blue("\nGenerating .env.session..."));
95
+ const projectName = config.projectName ?? basename(projectRoot);
96
+ const envPath = writeEnvFile(sessionDir, sessionId, ports, projectName);
97
+ console.log(chalk.green(` Written: ${envPath}`));
98
+ const appEnvFiles = writeAppEnvFiles(config, sessionDir, sessionId, ports);
99
+ for (const file of appEnvFiles) {
100
+ console.log(chalk.green(` Written: ${file}`));
101
+ }
102
+ console.log(chalk.blue("\nStarting Docker services..."));
103
+ let profiles;
104
+ if (mode === "docker") {
105
+ const allApps = config.apps ?? [];
106
+ const excludeApps = options.without ?? [];
107
+ profiles = allApps.filter((app) => !excludeApps.includes(app));
108
+ if (excludeApps.length > 0) {
109
+ console.log(chalk.gray(` Excluding apps: ${excludeApps.join(", ")}`));
110
+ }
111
+ }
112
+ await up({ cwd: sessionDir, profiles });
113
+ console.log(chalk.blue("Waiting for services to be ready..."));
114
+ await new Promise((resolve) => setTimeout(resolve, 3e3));
115
+ if (config.setup.length > 0) {
116
+ console.log(chalk.blue("\nRunning setup commands..."));
117
+ const setupEnv = {
118
+ ...process.env,
119
+ SESSION_ID: sessionId,
120
+ // Add DATABASE_URL for db commands
121
+ DATABASE_URL: `postgresql://postgres:postgres@localhost:${ports.POSTGRES_PORT}/postgres`
122
+ };
123
+ for (const [name, port] of Object.entries(ports)) {
124
+ setupEnv[name] = String(port);
125
+ }
126
+ for (const cmd of config.setup) {
127
+ console.log(chalk.gray(` Running: ${cmd}`));
128
+ const [command, ...args] = cmd.split(" ");
129
+ try {
130
+ await execa(command, args, {
131
+ cwd: sessionDir,
132
+ stdio: "inherit",
133
+ env: setupEnv
134
+ });
135
+ } catch {
136
+ console.warn(chalk.yellow(` Warning: Command failed: ${cmd}`));
137
+ }
138
+ }
139
+ }
140
+ store.remove(projectRoot, sessionId);
141
+ try {
142
+ store.insert({
143
+ sessionId,
144
+ projectRoot,
145
+ sessionDir,
146
+ branch: inPlace ? "" : branchName,
147
+ mode,
148
+ inPlace
149
+ });
150
+ } catch (dbErr) {
151
+ console.error(chalk.red("Failed to record session in database. Cleaning up..."));
152
+ try {
153
+ await down({ cwd: sessionDir });
154
+ } catch {
155
+ }
156
+ if (!inPlace) {
157
+ try {
158
+ await removeWorktree(projectRoot, sessionDir, branchName);
159
+ } catch {
160
+ }
161
+ }
162
+ throw dbErr;
163
+ }
164
+ console.log(chalk.green(`
165
+ Session ${sessionId} ready!`));
166
+ console.log(chalk.gray(`Directory: ${sessionDir}`));
167
+ if (mode === "docker") {
168
+ console.log(chalk.gray("\nDocker mode - all services in containers."));
169
+ console.log(chalk.gray("View logs: docker compose -f docker-compose.session.yml logs -f"));
170
+ } else {
171
+ console.log(chalk.gray("\nNative mode - run apps with: pnpm dev"));
172
+ }
173
+ console.log(chalk.gray("\nPorts:"));
174
+ for (const [name, port] of Object.entries(ports)) {
175
+ console.log(chalk.cyan(` ${name}: ${port}`));
176
+ }
177
+ if (options.detach === false) {
178
+ console.log(chalk.blue("\nStreaming logs (Ctrl+C to stop)..."));
179
+ console.log(chalk.gray("\u2500".repeat(60)));
180
+ try {
181
+ await logs({ cwd: sessionDir, profiles });
182
+ } catch (error) {
183
+ const execaError = error;
184
+ if (execaError.signal === "SIGINT") {
185
+ console.log(chalk.gray("\n\u2500".repeat(60)));
186
+ console.log(chalk.yellow("\nLog streaming stopped. Services are still running."));
187
+ console.log(chalk.gray(`Resume logs: cd ${sessionDir} && docker compose -f docker-compose.session.yml --env-file .env.session logs -f`));
188
+ } else {
189
+ throw error;
190
+ }
191
+ }
192
+ }
193
+ } finally {
194
+ store.close();
195
+ }
196
+ }
197
+
198
+ export {
199
+ createSession
200
+ };
@@ -0,0 +1,70 @@
1
+ import {
2
+ calculatePorts
3
+ } from "./chunk-PJKUD2N2.js";
4
+ import {
5
+ isRunning
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/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 store = new SessionStore();
21
+ let sessions;
22
+ try {
23
+ sessions = store.listByProject(projectRoot);
24
+ } finally {
25
+ store.close();
26
+ }
27
+ if (sessions.length === 0) {
28
+ console.log(chalk.gray("No active sessions found."));
29
+ console.log(chalk.gray("\nTo create a session:"));
30
+ console.log(chalk.cyan(" dev-prism create"));
31
+ return;
32
+ }
33
+ console.log(chalk.blue("Active Sessions:"));
34
+ console.log(chalk.gray("================\n"));
35
+ for (const session of sessions) {
36
+ const status = await getSessionStatus(session.session_id, session.session_dir, session.branch, config);
37
+ printSessionStatus(status);
38
+ }
39
+ }
40
+ async function getSessionStatus(sessionId, path, branch, config) {
41
+ const ports = calculatePorts(config, sessionId);
42
+ let running = false;
43
+ const envFile = resolve(path, ".env.session");
44
+ if (existsSync(envFile)) {
45
+ running = await isRunning({ cwd: path });
46
+ }
47
+ return {
48
+ sessionId,
49
+ path,
50
+ branch,
51
+ running,
52
+ ports
53
+ };
54
+ }
55
+ function printSessionStatus(status) {
56
+ const statusIcon = status.running ? chalk.green("\u25CF") : chalk.red("\u25CB");
57
+ const statusText = status.running ? chalk.green("running") : chalk.gray("stopped");
58
+ console.log(`${statusIcon} Session ${chalk.bold(status.sessionId)} ${statusText}`);
59
+ console.log(chalk.gray(` Path: ${status.path}`));
60
+ console.log(chalk.gray(` Branch: ${status.branch}`));
61
+ console.log(chalk.gray(" Ports:"));
62
+ for (const [name, port] of Object.entries(status.ports)) {
63
+ console.log(chalk.gray(` ${name}: ${port}`));
64
+ }
65
+ console.log("");
66
+ }
67
+
68
+ export {
69
+ listSessions
70
+ };
@@ -0,0 +1,71 @@
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
+ function findNextSessionId(usedIds) {
16
+ for (let i = 1; i <= 999; i++) {
17
+ const sessionId = String(i).padStart(3, "0");
18
+ if (!usedIds.has(sessionId)) {
19
+ return sessionId;
20
+ }
21
+ }
22
+ throw new Error("No available session IDs (001-999 all in use)");
23
+ }
24
+ function generateDefaultBranchName(sessionId) {
25
+ const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
26
+ return `session/${today}/${sessionId}`;
27
+ }
28
+ async function createWorktree(projectRoot, sessionDir, branchName) {
29
+ if (existsSync(sessionDir)) {
30
+ throw new Error(`Session directory already exists: ${sessionDir}`);
31
+ }
32
+ const exists = await branchExists(projectRoot, branchName);
33
+ if (exists) {
34
+ await execa("git", ["worktree", "add", sessionDir, branchName], {
35
+ cwd: projectRoot,
36
+ stdio: "inherit"
37
+ });
38
+ } else {
39
+ await execa("git", ["worktree", "add", sessionDir, "-b", branchName, "HEAD"], {
40
+ cwd: projectRoot,
41
+ stdio: "inherit"
42
+ });
43
+ }
44
+ }
45
+ async function removeWorktree(projectRoot, sessionDir, branchName) {
46
+ if (existsSync(sessionDir)) {
47
+ try {
48
+ await execa("git", ["worktree", "remove", "--force", sessionDir], {
49
+ cwd: projectRoot,
50
+ stdio: "inherit"
51
+ });
52
+ } catch {
53
+ rmSync(sessionDir, { recursive: true, force: true });
54
+ }
55
+ }
56
+ try {
57
+ await execa("git", ["branch", "-D", branchName], {
58
+ cwd: projectRoot,
59
+ stdio: "pipe"
60
+ // Don't show output, branch might not exist
61
+ });
62
+ } catch {
63
+ }
64
+ }
65
+
66
+ export {
67
+ findNextSessionId,
68
+ generateDefaultBranchName,
69
+ createWorktree,
70
+ removeWorktree
71
+ };