dev-prism 0.4.0 → 0.7.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.
Files changed (98) hide show
  1. package/README.md +180 -131
  2. package/bin/dev-prism.js +77 -97
  3. package/dist/{chunk-7YGOMAJG.js → chunk-3CIXBEXK.js} +22 -28
  4. package/dist/chunk-AHC6CD7F.js +92 -0
  5. package/dist/chunk-FQTS57VO.js +64 -0
  6. package/dist/chunk-GWQPK7MZ.js +50 -0
  7. package/dist/chunk-HDGBJGIH.js +55 -0
  8. package/dist/chunk-ILICQAU7.js +60 -0
  9. package/dist/chunk-IWZN6P6M.js +155 -0
  10. package/dist/chunk-KP56QH72.js +133 -0
  11. package/dist/{chunk-Y3GR6XK7.js → chunk-NJAITOCG.js} +3 -13
  12. package/dist/{chunk-25WQHUYW.js → chunk-TSNFAXVQ.js} +5 -12
  13. package/dist/chunk-VAPRJUC7.js +67 -0
  14. package/dist/chunk-VL56YPMK.js +45 -0
  15. package/dist/commands/claude.js +1 -1
  16. package/dist/commands/create.d.ts +1 -4
  17. package/dist/commands/create.js +5 -7
  18. package/dist/commands/destroy.d.ts +1 -1
  19. package/dist/commands/destroy.js +4 -5
  20. package/dist/commands/env.d.ts +7 -0
  21. package/dist/commands/env.js +9 -0
  22. package/dist/commands/info.js +4 -2
  23. package/dist/commands/list.d.ts +1 -3
  24. package/dist/commands/list.js +2 -5
  25. package/dist/commands/prune.d.ts +1 -1
  26. package/dist/commands/prune.js +2 -5
  27. package/dist/commands/with-env.d.ts +3 -0
  28. package/dist/commands/with-env.js +9 -0
  29. package/dist/index.d.ts +9 -12
  30. package/dist/index.js +58 -56
  31. package/dist/lib/config.d.ts +4 -7
  32. package/dist/lib/config.js +1 -3
  33. package/dist/lib/db.d.ts +26 -0
  34. package/dist/lib/db.js +28 -0
  35. package/dist/lib/env.d.ts +7 -5
  36. package/dist/lib/env.js +9 -9
  37. package/dist/lib/worktree.d.ts +2 -3
  38. package/dist/lib/worktree.js +1 -3
  39. package/package.json +8 -8
  40. package/dist/chunk-35SHBLIZ.js +0 -69
  41. package/dist/chunk-3ATDGV4Y.js +0 -22
  42. package/dist/chunk-3MSC3CGG.js +0 -78
  43. package/dist/chunk-3NW2OWIU.js +0 -78
  44. package/dist/chunk-3TRRZEFR.js +0 -38
  45. package/dist/chunk-4UNCSJRM.js +0 -70
  46. package/dist/chunk-5KDDYO6Y.js +0 -168
  47. package/dist/chunk-63II3EL4.js +0 -98
  48. package/dist/chunk-6YMQTISJ.js +0 -84
  49. package/dist/chunk-AOM6BONB.js +0 -98
  50. package/dist/chunk-AW2FJGXA.js +0 -38
  51. package/dist/chunk-C4WLIOBR.js +0 -67
  52. package/dist/chunk-D6QWWXZD.js +0 -49
  53. package/dist/chunk-FKTFCSU7.js +0 -78
  54. package/dist/chunk-GBN67HYD.js +0 -57
  55. package/dist/chunk-GKXXK2ZH.js +0 -203
  56. package/dist/chunk-GWDGC2OE.js +0 -116
  57. package/dist/chunk-H4HPDIY3.js +0 -95
  58. package/dist/chunk-HCCZKLC4.js +0 -64
  59. package/dist/chunk-HZUN6NRB.js +0 -70
  60. package/dist/chunk-J36LRUXM.js +0 -60
  61. package/dist/chunk-JHR4WADC.js +0 -200
  62. package/dist/chunk-JIU574KX.js +0 -41
  63. package/dist/chunk-KZJE62TK.js +0 -203
  64. package/dist/chunk-LDK6QMR6.js +0 -67
  65. package/dist/chunk-LEHA65A7.js +0 -59
  66. package/dist/chunk-LNIOSGC4.js +0 -78
  67. package/dist/chunk-LOVO4P3Y.js +0 -41
  68. package/dist/chunk-NNVP5F6I.js +0 -77
  69. package/dist/chunk-OL25TBYX.js +0 -67
  70. package/dist/chunk-P3ETW2KK.js +0 -166
  71. package/dist/chunk-PJKUD2N2.js +0 -22
  72. package/dist/chunk-PKC2ZED2.js +0 -168
  73. package/dist/chunk-PS76Q3HD.js +0 -168
  74. package/dist/chunk-QSG5CXPX.js +0 -171
  75. package/dist/chunk-QUMZI5KK.js +0 -98
  76. package/dist/chunk-RC2AUYZ7.js +0 -49
  77. package/dist/chunk-SMFAL2VP.js +0 -69
  78. package/dist/chunk-SSQ7XBY2.js +0 -30
  79. package/dist/chunk-SUMJLXT7.js +0 -30
  80. package/dist/chunk-UHI2QJFI.js +0 -200
  81. package/dist/chunk-VR3QWHHB.js +0 -57
  82. package/dist/chunk-X5A6H4Q7.js +0 -70
  83. package/dist/chunk-X6FHBEAS.js +0 -200
  84. package/dist/chunk-XUWQUDLT.js +0 -67
  85. package/dist/commands/logs.d.ts +0 -8
  86. package/dist/commands/logs.js +0 -8
  87. package/dist/commands/start.d.ts +0 -7
  88. package/dist/commands/start.js +0 -9
  89. package/dist/commands/stop-all.d.ts +0 -3
  90. package/dist/commands/stop-all.js +0 -9
  91. package/dist/commands/stop.d.ts +0 -3
  92. package/dist/commands/stop.js +0 -7
  93. package/dist/lib/docker.d.ts +0 -12
  94. package/dist/lib/docker.js +0 -14
  95. package/dist/lib/ports.d.ts +0 -6
  96. package/dist/lib/ports.js +0 -8
  97. package/dist/lib/store.d.ts +0 -46
  98. package/dist/lib/store.js +0 -6
@@ -4,31 +4,29 @@ import { join } from "path";
4
4
  import chalk from "chalk";
5
5
  var SKILL_CONTENT = `---
6
6
  allowed-tools: Bash(dev-prism *)
7
- description: Manage isolated development sessions (create, list, start, stop, destroy)
7
+ description: Manage isolated development sessions (create, destroy, list, env, with-env)
8
8
  ---
9
9
 
10
10
  # Dev Session Manager
11
11
 
12
- Manage isolated parallel development sessions using git worktrees and Docker.
12
+ Manage isolated parallel development sessions with port allocation and env injection.
13
13
 
14
14
  ## Parse Intent from: $ARGUMENTS
15
15
 
16
16
  - "create" / "new" -> dev-prism create
17
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
18
+ - "info" -> dev-prism info
19
+ - "destroy" -> dev-prism destroy
20
+ - "env" -> dev-prism env
23
21
  - "prune" -> dev-prism prune
24
22
 
25
23
  ## Commands
26
24
 
27
- Run from the project root (where session.config.mjs exists).
25
+ Run from the project root (where prism.config.mjs exists).
28
26
 
29
27
  After running commands, explain:
30
28
  1. What happened
31
- 2. Relevant ports/paths
29
+ 2. Relevant ports/env vars
32
30
  3. Next steps
33
31
 
34
32
  Warn before destructive operations (destroy, prune).
@@ -36,33 +34,29 @@ Warn before destructive operations (destroy, prune).
36
34
  var CLAUDE_MD_SECTION = `
37
35
  ## Dev Sessions
38
36
 
39
- Isolated parallel development sessions using git worktrees and Docker.
37
+ Port allocator, env injector, and worktree manager for parallel development.
40
38
 
41
39
  ### Commands
42
40
  \`\`\`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
41
+ dev-prism create [--branch <name>] [--in-place] # Allocate ports + optional worktree
42
+ dev-prism destroy [--all] # Deallocate ports + remove worktree
43
+ dev-prism list # List sessions from SQLite
44
+ dev-prism info # Show session ports/env for cwd
45
+ dev-prism with-env -- <command> # Inject env + exec command
46
+ dev-prism with-env <app> -- <command> # Inject app-specific env + exec
47
+ dev-prism env [--write <path>] [--app <name>] # Print/write env vars
48
+ dev-prism prune [-y] # Remove orphaned sessions
51
49
  \`\`\`
52
50
 
53
51
  ### 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 |
52
+ Ports are allocated dynamically using \`get-port\` and stored in SQLite.
53
+ Each service gets a unique port. Use \`dev-prism info\` to see allocated ports.
61
54
 
62
55
  ### 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
56
+ - Use \`dev-prism with-env -- <cmd>\` to run commands with session env injected
57
+ - \`with-env\` is a no-op outside a session \u2014 safe to use unconditionally
58
+ - Use \`dev-prism env\` to see all env vars for the current session
59
+ - Docker is not managed by dev-prism \u2014 users manage their own compose files
66
60
  `;
67
61
  async function installClaude(projectRoot, options) {
68
62
  const skillDir = join(projectRoot, ".claude", "commands");
@@ -0,0 +1,92 @@
1
+ import {
2
+ removeWorktree
3
+ } from "./chunk-NJAITOCG.js";
4
+ import {
5
+ getSessionsDir,
6
+ loadConfig
7
+ } from "./chunk-TSNFAXVQ.js";
8
+ import {
9
+ deleteDbSession,
10
+ findProjectRoot,
11
+ getDbSession,
12
+ listDbSessions,
13
+ openDatabase
14
+ } from "./chunk-IWZN6P6M.js";
15
+
16
+ // src/commands/destroy.ts
17
+ import { existsSync } from "fs";
18
+ import chalk from "chalk";
19
+ async function destroySession(workingDir, options) {
20
+ let projectRoot;
21
+ try {
22
+ projectRoot = findProjectRoot(workingDir);
23
+ } catch {
24
+ console.error(chalk.red("Error: Could not find prism.config.mjs"));
25
+ process.exit(1);
26
+ }
27
+ const db = openDatabase();
28
+ try {
29
+ if (options.all) {
30
+ console.log(chalk.blue("Destroying all sessions..."));
31
+ const sessions = listDbSessions(db);
32
+ if (sessions.length === 0) {
33
+ console.log(chalk.gray("No sessions found."));
34
+ return;
35
+ }
36
+ const config2 = await loadConfig(projectRoot);
37
+ const sessionsDir2 = getSessionsDir(config2, projectRoot);
38
+ for (const session2 of sessions) {
39
+ await destroySingleSession(
40
+ db,
41
+ projectRoot,
42
+ sessionsDir2,
43
+ session2.id,
44
+ session2.branch
45
+ );
46
+ }
47
+ console.log(chalk.green(`
48
+ Destroyed ${sessions.length} session(s).`));
49
+ return;
50
+ }
51
+ const session = getDbSession(db, workingDir);
52
+ if (!session) {
53
+ console.error(
54
+ chalk.red(`Error: No session found for ${workingDir}`)
55
+ );
56
+ process.exit(1);
57
+ }
58
+ const config = await loadConfig(projectRoot);
59
+ const sessionsDir = getSessionsDir(config, projectRoot);
60
+ await destroySingleSession(
61
+ db,
62
+ projectRoot,
63
+ sessionsDir,
64
+ session.id,
65
+ session.branch
66
+ );
67
+ console.log(chalk.green("\nSession destroyed."));
68
+ } finally {
69
+ db.close();
70
+ }
71
+ }
72
+ async function destroySingleSession(db, projectRoot, sessionsDir, sessionId, branch) {
73
+ console.log(chalk.blue(`
74
+ Destroying session in ${sessionId}...`));
75
+ deleteDbSession(db, sessionId);
76
+ console.log(chalk.gray(" Removed from database."));
77
+ if (branch && sessionId.startsWith(sessionsDir) && existsSync(sessionId)) {
78
+ console.log(chalk.gray(" Removing git worktree..."));
79
+ try {
80
+ await removeWorktree(projectRoot, sessionId, branch);
81
+ } catch (error) {
82
+ console.warn(
83
+ chalk.yellow(` Warning: Could not remove worktree: ${error}`)
84
+ );
85
+ }
86
+ }
87
+ console.log(chalk.green(" Session destroyed."));
88
+ }
89
+
90
+ export {
91
+ destroySession
92
+ };
@@ -0,0 +1,64 @@
1
+ import {
2
+ deleteDbSession,
3
+ listDbSessions,
4
+ openDatabase
5
+ } from "./chunk-IWZN6P6M.js";
6
+
7
+ // src/commands/prune.ts
8
+ import { existsSync } from "fs";
9
+ import { createInterface } from "readline";
10
+ import chalk from "chalk";
11
+ async function pruneSessions(options) {
12
+ const db = openDatabase();
13
+ try {
14
+ const sessions = listDbSessions(db);
15
+ const orphaned = sessions.filter((s) => !existsSync(s.id));
16
+ if (orphaned.length === 0) {
17
+ console.log(chalk.gray("No orphaned sessions to prune."));
18
+ return;
19
+ }
20
+ console.log(
21
+ chalk.yellow(
22
+ `
23
+ Found ${orphaned.length} orphaned session(s) to prune:`
24
+ )
25
+ );
26
+ for (const session of orphaned) {
27
+ console.log(chalk.gray(` - ${session.id}`));
28
+ }
29
+ console.log("");
30
+ if (!options.yes) {
31
+ const rl = createInterface({
32
+ input: process.stdin,
33
+ output: process.stdout
34
+ });
35
+ const answer = await new Promise((resolve) => {
36
+ rl.question(
37
+ chalk.red("Remove these orphaned sessions from the database? [y/N] "),
38
+ resolve
39
+ );
40
+ });
41
+ rl.close();
42
+ if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
43
+ console.log(chalk.gray("Cancelled."));
44
+ return;
45
+ }
46
+ }
47
+ console.log(chalk.blue("\nPruning orphaned sessions...\n"));
48
+ for (const session of orphaned) {
49
+ console.log(chalk.gray(` Removing ${session.id}...`));
50
+ deleteDbSession(db, session.id);
51
+ console.log(chalk.green(` Removed.`));
52
+ }
53
+ console.log(
54
+ chalk.green(`
55
+ Pruned ${orphaned.length} orphaned session(s).`)
56
+ );
57
+ } finally {
58
+ db.close();
59
+ }
60
+ }
61
+
62
+ export {
63
+ pruneSessions
64
+ };
@@ -0,0 +1,50 @@
1
+ // src/lib/env.ts
2
+ import { createHash } from "crypto";
3
+ import { basename } from "path";
4
+ function renderTemplate(template, ports) {
5
+ const result = {};
6
+ for (const [key, value] of Object.entries(template)) {
7
+ let rendered = value;
8
+ for (const [serviceName, portValue] of Object.entries(ports)) {
9
+ rendered = rendered.replace(
10
+ new RegExp(`\\$\\{${serviceName}\\}`, "g"),
11
+ String(portValue)
12
+ );
13
+ }
14
+ result[key] = rendered;
15
+ }
16
+ return result;
17
+ }
18
+ function buildSessionEnv(config, workingDir, allocations, appName) {
19
+ const ports = {};
20
+ for (const alloc of allocations) {
21
+ ports[alloc.service] = alloc.port;
22
+ }
23
+ const env = {};
24
+ env.COMPOSE_PROJECT_NAME = getComposeProjectName(
25
+ workingDir,
26
+ config.projectName
27
+ );
28
+ if (config.env) {
29
+ Object.assign(env, renderTemplate(config.env, ports));
30
+ }
31
+ if (appName && config.apps?.[appName]) {
32
+ Object.assign(env, renderTemplate(config.apps[appName], ports));
33
+ }
34
+ return env;
35
+ }
36
+ function formatEnvFile(env) {
37
+ return Object.entries(env).map(([key, value]) => `${key}=${value}`).join("\n") + "\n";
38
+ }
39
+ function getComposeProjectName(workingDir, projectName) {
40
+ const name = projectName ?? basename(workingDir);
41
+ const dirHash = createHash("md5").update(workingDir).digest("hex").substring(0, 8);
42
+ return `${name}-${dirHash}`;
43
+ }
44
+
45
+ export {
46
+ renderTemplate,
47
+ buildSessionEnv,
48
+ formatEnvFile,
49
+ getComposeProjectName
50
+ };
@@ -0,0 +1,55 @@
1
+ import {
2
+ buildSessionEnv,
3
+ formatEnvFile
4
+ } from "./chunk-GWQPK7MZ.js";
5
+ import {
6
+ loadConfig
7
+ } from "./chunk-TSNFAXVQ.js";
8
+ import {
9
+ findProjectRoot,
10
+ getDbSession,
11
+ getPortAllocations,
12
+ openDatabase
13
+ } from "./chunk-IWZN6P6M.js";
14
+
15
+ // src/commands/env.ts
16
+ import { writeFileSync } from "fs";
17
+ import { resolve } from "path";
18
+ import chalk from "chalk";
19
+ async function showEnv(options) {
20
+ const cwd = process.cwd();
21
+ let projectRoot;
22
+ try {
23
+ projectRoot = findProjectRoot(cwd);
24
+ } catch {
25
+ console.error(chalk.red("Error: Could not find prism.config.mjs"));
26
+ process.exit(1);
27
+ }
28
+ const db = openDatabase();
29
+ try {
30
+ const session = getDbSession(db, cwd);
31
+ if (!session) {
32
+ console.error(chalk.red("Error: No session found for this directory."));
33
+ console.error(
34
+ chalk.gray("Run `dev-prism create --in-place` to create a session here.")
35
+ );
36
+ process.exit(1);
37
+ }
38
+ const ports = getPortAllocations(db, session.id);
39
+ const config = await loadConfig(projectRoot);
40
+ const env = buildSessionEnv(config, cwd, ports, options.app);
41
+ if (options.write) {
42
+ const filePath = resolve(cwd, options.write);
43
+ writeFileSync(filePath, formatEnvFile(env), "utf-8");
44
+ console.error(chalk.green(`Written: ${filePath}`));
45
+ } else {
46
+ process.stdout.write(formatEnvFile(env));
47
+ }
48
+ } finally {
49
+ db.close();
50
+ }
51
+ }
52
+
53
+ export {
54
+ showEnv
55
+ };
@@ -0,0 +1,60 @@
1
+ import {
2
+ buildSessionEnv
3
+ } from "./chunk-GWQPK7MZ.js";
4
+ import {
5
+ loadConfig
6
+ } from "./chunk-TSNFAXVQ.js";
7
+ import {
8
+ findProjectRoot,
9
+ getDbSession,
10
+ getPortAllocations,
11
+ openDatabase
12
+ } from "./chunk-IWZN6P6M.js";
13
+
14
+ // src/commands/with-env.ts
15
+ import { execa } from "execa";
16
+ async function withEnv(command, appName) {
17
+ if (command.length === 0) {
18
+ console.error("Usage: dev-prism with-env [app] -- <command>");
19
+ process.exit(1);
20
+ }
21
+ const cwd = process.cwd();
22
+ let sessionEnv = {};
23
+ try {
24
+ const projectRoot = findProjectRoot(cwd);
25
+ const db = openDatabase();
26
+ try {
27
+ const session = getDbSession(db, cwd);
28
+ if (session) {
29
+ const ports = getPortAllocations(db, session.id);
30
+ const config = await loadConfig(projectRoot);
31
+ sessionEnv = buildSessionEnv(config, cwd, ports, appName);
32
+ }
33
+ } finally {
34
+ db.close();
35
+ }
36
+ } catch {
37
+ }
38
+ const [cmd, ...args] = command;
39
+ const merged = {
40
+ ...process.env,
41
+ ...sessionEnv
42
+ };
43
+ try {
44
+ const result = await execa(cmd, args, {
45
+ stdio: "inherit",
46
+ env: merged,
47
+ reject: false
48
+ });
49
+ if (result.exitCode !== void 0 && result.exitCode !== 0) {
50
+ process.exit(result.exitCode);
51
+ }
52
+ } catch (error) {
53
+ console.error(error.message);
54
+ process.exit(1);
55
+ }
56
+ }
57
+
58
+ export {
59
+ withEnv
60
+ };
@@ -0,0 +1,155 @@
1
+ // src/lib/db.ts
2
+ import Database from "better-sqlite3";
3
+ import getPort from "get-port";
4
+ import { existsSync, mkdirSync } from "fs";
5
+ import { homedir } from "os";
6
+ import { dirname, join, resolve } from "path";
7
+ var SCHEMA = `
8
+ CREATE TABLE IF NOT EXISTS sessions (
9
+ id TEXT PRIMARY KEY,
10
+ branch TEXT,
11
+ created_at TEXT NOT NULL
12
+ );
13
+
14
+ CREATE TABLE IF NOT EXISTS port_allocations (
15
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
16
+ service TEXT NOT NULL,
17
+ port INTEGER NOT NULL UNIQUE,
18
+ PRIMARY KEY (session_id, service)
19
+ );
20
+
21
+ CREATE TABLE IF NOT EXISTS reservations (
22
+ port INTEGER PRIMARY KEY,
23
+ reason TEXT,
24
+ created_at TEXT NOT NULL
25
+ );
26
+ `;
27
+ function openDatabase() {
28
+ const dbDir = join(homedir(), ".dev-prism");
29
+ if (!existsSync(dbDir)) {
30
+ mkdirSync(dbDir, { recursive: true });
31
+ }
32
+ const db = new Database(join(dbDir, "sessions.db"));
33
+ db.pragma("journal_mode = WAL");
34
+ db.pragma("busy_timeout = 5000");
35
+ db.pragma("foreign_keys = ON");
36
+ db.exec(SCHEMA);
37
+ return db;
38
+ }
39
+ function findProjectRoot(startDir) {
40
+ let dir = resolve(startDir);
41
+ while (true) {
42
+ if (existsSync(join(dir, "prism.config.mjs")) || existsSync(join(dir, "prism.config.js"))) {
43
+ return dir;
44
+ }
45
+ const parent = dirname(dir);
46
+ if (parent === dir) {
47
+ throw new Error(
48
+ "Could not find prism.config.mjs in any parent directory"
49
+ );
50
+ }
51
+ dir = parent;
52
+ }
53
+ }
54
+ function createDbSession(db, session) {
55
+ db.prepare(
56
+ "INSERT INTO sessions (id, branch, created_at) VALUES (?, ?, ?)"
57
+ ).run(session.id, session.branch, session.created_at);
58
+ }
59
+ function deleteDbSession(db, sessionId) {
60
+ db.prepare("DELETE FROM sessions WHERE id = ?").run(sessionId);
61
+ }
62
+ function getDbSession(db, sessionId) {
63
+ return db.prepare("SELECT id, branch, created_at FROM sessions WHERE id = ?").get(sessionId);
64
+ }
65
+ function listDbSessions(db) {
66
+ return db.prepare("SELECT id, branch, created_at FROM sessions ORDER BY created_at").all();
67
+ }
68
+ function getPortAllocations(db, sessionId) {
69
+ return db.prepare(
70
+ "SELECT session_id, service, port FROM port_allocations WHERE session_id = ?"
71
+ ).all(sessionId);
72
+ }
73
+ function getAllocatedPorts(db) {
74
+ return db.prepare("SELECT port FROM port_allocations").all().map((r) => r.port);
75
+ }
76
+ function getReservedPorts(db) {
77
+ return db.prepare("SELECT port FROM reservations").all().map((r) => r.port);
78
+ }
79
+ function reservePort(db, port, reason) {
80
+ db.prepare(
81
+ "INSERT INTO reservations (port, reason, created_at) VALUES (?, ?, ?)"
82
+ ).run(port, reason ?? null, (/* @__PURE__ */ new Date()).toISOString());
83
+ }
84
+ function unreservePort(db, port) {
85
+ db.prepare("DELETE FROM reservations WHERE port = ?").run(port);
86
+ }
87
+ async function allocatePorts(db, sessionId, services) {
88
+ if (services.length === 0) return [];
89
+ const excludePorts = /* @__PURE__ */ new Set([
90
+ ...getAllocatedPorts(db),
91
+ ...getReservedPorts(db)
92
+ ]);
93
+ const allocations = [];
94
+ for (const name of services) {
95
+ const port = await getPort({ exclude: [...excludePorts] });
96
+ excludePorts.add(port);
97
+ allocations.push({
98
+ session_id: sessionId,
99
+ service: name,
100
+ port
101
+ });
102
+ }
103
+ const insert = db.prepare(
104
+ "INSERT INTO port_allocations (session_id, service, port) VALUES (?, ?, ?)"
105
+ );
106
+ try {
107
+ const insertAll = db.transaction(() => {
108
+ for (const alloc of allocations) {
109
+ insert.run(alloc.session_id, alloc.service, alloc.port);
110
+ }
111
+ });
112
+ insertAll();
113
+ } catch (err) {
114
+ if (err.code === "SQLITE_CONSTRAINT_UNIQUE") {
115
+ const retryExclude = /* @__PURE__ */ new Set([
116
+ ...getAllocatedPorts(db),
117
+ ...getReservedPorts(db)
118
+ ]);
119
+ const retryAllocations = [];
120
+ for (const name of services) {
121
+ const port = await getPort({ exclude: [...retryExclude] });
122
+ retryExclude.add(port);
123
+ retryAllocations.push({
124
+ session_id: sessionId,
125
+ service: name,
126
+ port
127
+ });
128
+ }
129
+ const insertAllRetry = db.transaction(() => {
130
+ for (const alloc of retryAllocations) {
131
+ insert.run(alloc.session_id, alloc.service, alloc.port);
132
+ }
133
+ });
134
+ insertAllRetry();
135
+ return retryAllocations;
136
+ }
137
+ throw err;
138
+ }
139
+ return allocations;
140
+ }
141
+
142
+ export {
143
+ openDatabase,
144
+ findProjectRoot,
145
+ createDbSession,
146
+ deleteDbSession,
147
+ getDbSession,
148
+ listDbSessions,
149
+ getPortAllocations,
150
+ getAllocatedPorts,
151
+ getReservedPorts,
152
+ reservePort,
153
+ unreservePort,
154
+ allocatePorts
155
+ };
@@ -0,0 +1,133 @@
1
+ import {
2
+ createWorktree,
3
+ generateDefaultBranchName,
4
+ removeWorktree
5
+ } from "./chunk-NJAITOCG.js";
6
+ import {
7
+ buildSessionEnv
8
+ } from "./chunk-GWQPK7MZ.js";
9
+ import {
10
+ getSessionsDir,
11
+ loadConfig
12
+ } from "./chunk-TSNFAXVQ.js";
13
+ import {
14
+ allocatePorts,
15
+ createDbSession,
16
+ getDbSession,
17
+ openDatabase
18
+ } from "./chunk-IWZN6P6M.js";
19
+
20
+ // src/commands/create.ts
21
+ import { existsSync, mkdirSync } from "fs";
22
+ import { resolve } from "path";
23
+ import chalk from "chalk";
24
+ import { execa } from "execa";
25
+ async function createSession(projectRoot, options) {
26
+ const config = await loadConfig(projectRoot);
27
+ const sessionsDir = getSessionsDir(config, projectRoot);
28
+ const inPlace = options.inPlace ?? false;
29
+ let workingDir;
30
+ let branchName = "";
31
+ if (inPlace) {
32
+ workingDir = projectRoot;
33
+ console.log(chalk.blue("Creating session in current directory..."));
34
+ } else {
35
+ branchName = options.branch || generateDefaultBranchName();
36
+ workingDir = resolve(sessionsDir, branchName);
37
+ console.log(chalk.blue("Creating session..."));
38
+ console.log(chalk.gray(`Branch: ${branchName}`));
39
+ console.log(chalk.gray(`Directory: ${workingDir}`));
40
+ }
41
+ const db = openDatabase();
42
+ try {
43
+ if (getDbSession(db, workingDir)) {
44
+ console.error(chalk.red("Error: Session already exists for this directory."));
45
+ console.error(chalk.gray("Destroy it first with: dev-prism destroy"));
46
+ process.exit(1);
47
+ }
48
+ if (!inPlace) {
49
+ if (!existsSync(sessionsDir)) {
50
+ mkdirSync(sessionsDir, { recursive: true });
51
+ }
52
+ console.log(chalk.blue("\nCreating git worktree..."));
53
+ await createWorktree(projectRoot, workingDir, branchName);
54
+ console.log(chalk.green(` Created: ${workingDir}`));
55
+ }
56
+ createDbSession(db, {
57
+ id: workingDir,
58
+ branch: branchName || null,
59
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
60
+ });
61
+ console.log(chalk.blue("\nAllocating ports..."));
62
+ const allocations = await allocatePorts(db, workingDir, config.ports);
63
+ if (allocations.length > 0) {
64
+ console.log(chalk.gray("Allocated ports:"));
65
+ for (const alloc of allocations) {
66
+ console.log(
67
+ chalk.cyan(` ${alloc.service}: ${alloc.port}`)
68
+ );
69
+ }
70
+ }
71
+ if (config.setup.length > 0) {
72
+ console.log(chalk.blue("\nRunning setup commands..."));
73
+ const sessionEnv = buildSessionEnv(config, workingDir, allocations);
74
+ const setupEnv = {
75
+ ...process.env,
76
+ ...sessionEnv
77
+ };
78
+ for (const cmd of config.setup) {
79
+ console.log(chalk.gray(` Running: ${cmd}`));
80
+ const [command, ...args] = cmd.split(" ");
81
+ try {
82
+ await execa(command, args, {
83
+ cwd: workingDir,
84
+ stdio: "inherit",
85
+ env: setupEnv
86
+ });
87
+ } catch {
88
+ console.warn(chalk.yellow(` Warning: Command failed: ${cmd}`));
89
+ }
90
+ }
91
+ }
92
+ console.log(chalk.green("\nSession ready!"));
93
+ console.log(chalk.gray(`Directory: ${workingDir}`));
94
+ if (allocations.length > 0) {
95
+ console.log(chalk.gray("\nPorts:"));
96
+ for (const alloc of allocations) {
97
+ console.log(
98
+ chalk.cyan(` ${alloc.service}: http://localhost:${alloc.port}`)
99
+ );
100
+ }
101
+ }
102
+ console.log(chalk.gray("\nUsage:"));
103
+ console.log(
104
+ chalk.gray(" dev-prism with-env -- docker compose up -d")
105
+ );
106
+ console.log(
107
+ chalk.gray(" dev-prism with-env -- pnpm dev")
108
+ );
109
+ console.log(
110
+ chalk.gray(" dev-prism env # show env vars")
111
+ );
112
+ } catch (error) {
113
+ console.error(chalk.red("Session creation failed. Cleaning up..."));
114
+ try {
115
+ const { deleteDbSession } = await import("./lib/db.js");
116
+ deleteDbSession(db, workingDir);
117
+ } catch {
118
+ }
119
+ if (!inPlace && branchName) {
120
+ try {
121
+ await removeWorktree(projectRoot, workingDir, branchName);
122
+ } catch {
123
+ }
124
+ }
125
+ throw error;
126
+ } finally {
127
+ db.close();
128
+ }
129
+ }
130
+
131
+ export {
132
+ createSession
133
+ };