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.
- package/README.md +180 -131
- package/bin/dev-prism.js +77 -97
- package/dist/{chunk-7YGOMAJG.js → chunk-3CIXBEXK.js} +22 -28
- package/dist/chunk-AHC6CD7F.js +92 -0
- package/dist/chunk-FQTS57VO.js +64 -0
- package/dist/chunk-GWQPK7MZ.js +50 -0
- package/dist/chunk-HDGBJGIH.js +55 -0
- package/dist/chunk-ILICQAU7.js +60 -0
- package/dist/chunk-IWZN6P6M.js +155 -0
- package/dist/chunk-KP56QH72.js +133 -0
- package/dist/{chunk-Y3GR6XK7.js → chunk-NJAITOCG.js} +3 -13
- package/dist/{chunk-25WQHUYW.js → chunk-TSNFAXVQ.js} +5 -12
- package/dist/chunk-VAPRJUC7.js +67 -0
- package/dist/chunk-VL56YPMK.js +45 -0
- package/dist/commands/claude.js +1 -1
- package/dist/commands/create.d.ts +1 -4
- package/dist/commands/create.js +5 -7
- package/dist/commands/destroy.d.ts +1 -1
- package/dist/commands/destroy.js +4 -5
- package/dist/commands/env.d.ts +7 -0
- package/dist/commands/env.js +9 -0
- package/dist/commands/info.js +4 -2
- package/dist/commands/list.d.ts +1 -3
- package/dist/commands/list.js +2 -5
- package/dist/commands/prune.d.ts +1 -1
- package/dist/commands/prune.js +2 -5
- package/dist/commands/with-env.d.ts +3 -0
- package/dist/commands/with-env.js +9 -0
- package/dist/index.d.ts +9 -12
- package/dist/index.js +58 -56
- package/dist/lib/config.d.ts +4 -7
- package/dist/lib/config.js +1 -3
- package/dist/lib/db.d.ts +26 -0
- package/dist/lib/db.js +28 -0
- package/dist/lib/env.d.ts +7 -5
- package/dist/lib/env.js +9 -9
- package/dist/lib/worktree.d.ts +2 -3
- package/dist/lib/worktree.js +1 -3
- package/package.json +8 -8
- package/dist/chunk-35SHBLIZ.js +0 -69
- package/dist/chunk-3ATDGV4Y.js +0 -22
- package/dist/chunk-3MSC3CGG.js +0 -78
- package/dist/chunk-3NW2OWIU.js +0 -78
- package/dist/chunk-3TRRZEFR.js +0 -38
- package/dist/chunk-4UNCSJRM.js +0 -70
- package/dist/chunk-5KDDYO6Y.js +0 -168
- package/dist/chunk-63II3EL4.js +0 -98
- package/dist/chunk-6YMQTISJ.js +0 -84
- package/dist/chunk-AOM6BONB.js +0 -98
- package/dist/chunk-AW2FJGXA.js +0 -38
- package/dist/chunk-C4WLIOBR.js +0 -67
- package/dist/chunk-D6QWWXZD.js +0 -49
- package/dist/chunk-FKTFCSU7.js +0 -78
- package/dist/chunk-GBN67HYD.js +0 -57
- package/dist/chunk-GKXXK2ZH.js +0 -203
- package/dist/chunk-GWDGC2OE.js +0 -116
- package/dist/chunk-H4HPDIY3.js +0 -95
- package/dist/chunk-HCCZKLC4.js +0 -64
- package/dist/chunk-HZUN6NRB.js +0 -70
- package/dist/chunk-J36LRUXM.js +0 -60
- package/dist/chunk-JHR4WADC.js +0 -200
- package/dist/chunk-JIU574KX.js +0 -41
- package/dist/chunk-KZJE62TK.js +0 -203
- package/dist/chunk-LDK6QMR6.js +0 -67
- package/dist/chunk-LEHA65A7.js +0 -59
- package/dist/chunk-LNIOSGC4.js +0 -78
- package/dist/chunk-LOVO4P3Y.js +0 -41
- package/dist/chunk-NNVP5F6I.js +0 -77
- package/dist/chunk-OL25TBYX.js +0 -67
- package/dist/chunk-P3ETW2KK.js +0 -166
- package/dist/chunk-PJKUD2N2.js +0 -22
- package/dist/chunk-PKC2ZED2.js +0 -168
- package/dist/chunk-PS76Q3HD.js +0 -168
- package/dist/chunk-QSG5CXPX.js +0 -171
- package/dist/chunk-QUMZI5KK.js +0 -98
- package/dist/chunk-RC2AUYZ7.js +0 -49
- package/dist/chunk-SMFAL2VP.js +0 -69
- package/dist/chunk-SSQ7XBY2.js +0 -30
- package/dist/chunk-SUMJLXT7.js +0 -30
- package/dist/chunk-UHI2QJFI.js +0 -200
- package/dist/chunk-VR3QWHHB.js +0 -57
- package/dist/chunk-X5A6H4Q7.js +0 -70
- package/dist/chunk-X6FHBEAS.js +0 -200
- package/dist/chunk-XUWQUDLT.js +0 -67
- package/dist/commands/logs.d.ts +0 -8
- package/dist/commands/logs.js +0 -8
- package/dist/commands/start.d.ts +0 -7
- package/dist/commands/start.js +0 -9
- package/dist/commands/stop-all.d.ts +0 -3
- package/dist/commands/stop-all.js +0 -9
- package/dist/commands/stop.d.ts +0 -3
- package/dist/commands/stop.js +0 -7
- package/dist/lib/docker.d.ts +0 -12
- package/dist/lib/docker.js +0 -14
- package/dist/lib/ports.d.ts +0 -6
- package/dist/lib/ports.js +0 -8
- package/dist/lib/store.d.ts +0 -46
- 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,
|
|
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
|
|
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
|
-
- "
|
|
19
|
-
- "
|
|
20
|
-
- "
|
|
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
|
|
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/
|
|
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
|
-
|
|
37
|
+
Port allocator, env injector, and worktree manager for parallel development.
|
|
40
38
|
|
|
41
39
|
### Commands
|
|
42
40
|
\`\`\`bash
|
|
43
|
-
dev-prism create [
|
|
44
|
-
dev-prism
|
|
45
|
-
dev-prism
|
|
46
|
-
dev-prism
|
|
47
|
-
dev-prism
|
|
48
|
-
dev-prism
|
|
49
|
-
dev-prism
|
|
50
|
-
dev-prism prune
|
|
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
|
-
|
|
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
|
-
-
|
|
64
|
-
-
|
|
65
|
-
-
|
|
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
|
+
};
|