dev-prism 0.4.0 → 0.6.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 +262 -70
- package/bin/dev-prism.js +46 -44
- package/dist/chunk-24OM3LGM.js +35 -0
- package/dist/chunk-3FYEGH2G.js +217 -0
- package/dist/chunk-3Q454U3I.js +71 -0
- package/dist/chunk-76LSQIZI.js +31 -0
- package/dist/chunk-7OSTLJLO.js +219 -0
- package/dist/chunk-7YEZWM6Y.js +97 -0
- package/dist/chunk-AEVARZQ4.js +203 -0
- package/dist/chunk-AIVPJ467.js +70 -0
- package/dist/chunk-BXYXWNGH.js +30 -0
- package/dist/chunk-CUQVGZBX.js +44 -0
- package/dist/chunk-DQER5GNG.js +72 -0
- package/dist/chunk-DTE5YQMI.js +41 -0
- package/dist/chunk-EXRHG5KQ.js +60 -0
- package/dist/chunk-H3L73URT.js +65 -0
- package/dist/chunk-HZBJF67X.js +60 -0
- package/dist/chunk-I3U6JK77.js +66 -0
- package/dist/chunk-JMKE3ZKI.js +61 -0
- package/dist/chunk-JZ2VPQXP.js +132 -0
- package/dist/chunk-KDYKLH6P.js +40 -0
- package/dist/chunk-LAOWFCQL.js +21 -0
- package/dist/chunk-MRBTH5PL.js +66 -0
- package/dist/chunk-OOJ6YOGS.js +53 -0
- package/dist/chunk-OPEZFBBI.js +219 -0
- package/dist/chunk-Q5DPX4WL.js +219 -0
- package/dist/chunk-RQ245R7T.js +67 -0
- package/dist/chunk-SD3TON6N.js +32 -0
- package/dist/chunk-SEKH4ZV6.js +60 -0
- package/dist/chunk-UKYQN4A3.js +38 -0
- package/dist/chunk-URGGS3XM.js +95 -0
- package/dist/chunk-VUNPVDSO.js +74 -0
- package/dist/chunk-VXP2SPRI.js +51 -0
- package/dist/chunk-W54CPPSK.js +217 -0
- package/dist/chunk-X2PXZRYU.js +41 -0
- package/dist/chunk-YSO3IDZZ.js +40 -0
- package/dist/chunk-YY5DA35Z.js +40 -0
- package/dist/chunk-Z2ISJMLW.js +92 -0
- package/dist/chunk-ZKHNUDSL.js +119 -0
- package/dist/commands/create.d.ts +2 -1
- package/dist/commands/create.js +6 -5
- package/dist/commands/destroy.d.ts +1 -1
- package/dist/commands/destroy.js +4 -3
- package/dist/commands/info.js +3 -2
- package/dist/commands/list.d.ts +1 -3
- package/dist/commands/list.js +3 -5
- package/dist/commands/logs.d.ts +1 -1
- package/dist/commands/logs.js +3 -2
- package/dist/commands/prune.d.ts +1 -1
- package/dist/commands/prune.js +4 -3
- package/dist/commands/start.d.ts +1 -1
- package/dist/commands/start.js +3 -2
- package/dist/commands/stop-all.d.ts +1 -1
- package/dist/commands/stop-all.js +3 -4
- package/dist/commands/stop.d.ts +1 -1
- package/dist/commands/stop.js +3 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +36 -18
- package/dist/lib/compose.d.ts +12 -0
- package/dist/lib/compose.js +12 -0
- package/dist/lib/config.d.ts +4 -0
- package/dist/lib/config.test.d.ts +1 -0
- package/dist/lib/config.test.js +32 -0
- package/dist/lib/docker-inspect.d.ts +24 -0
- package/dist/lib/docker-inspect.js +16 -0
- package/dist/lib/env.d.ts +3 -3
- package/dist/lib/env.js +1 -1
- package/dist/lib/env.test.d.ts +1 -0
- package/dist/lib/env.test.js +68 -0
- package/dist/lib/ports.d.ts +3 -3
- package/dist/lib/ports.js +3 -3
- package/dist/lib/ports.test.d.ts +1 -0
- package/dist/lib/ports.test.js +61 -0
- package/dist/lib/session.d.ts +16 -0
- package/dist/lib/session.js +13 -0
- package/dist/lib/store.d.ts +5 -5
- package/dist/lib/store.js +1 -1
- package/dist/lib/store.test.d.ts +1 -0
- package/dist/lib/store.test.js +205 -0
- package/dist/lib/worktree.d.ts +1 -1
- package/dist/lib/worktree.js +1 -1
- package/dist/lib/worktree.test.d.ts +1 -0
- package/dist/lib/worktree.test.js +41 -0
- package/package.json +2 -5
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadConfig
|
|
3
|
+
} from "./chunk-25WQHUYW.js";
|
|
4
|
+
import {
|
|
5
|
+
isRunning
|
|
6
|
+
} from "./chunk-GBN67HYD.js";
|
|
7
|
+
import {
|
|
8
|
+
SessionStore
|
|
9
|
+
} from "./chunk-ZKHNUDSL.js";
|
|
10
|
+
|
|
11
|
+
// src/commands/stop-all.ts
|
|
12
|
+
import { existsSync } from "fs";
|
|
13
|
+
import { resolve } from "path";
|
|
14
|
+
import chalk from "chalk";
|
|
15
|
+
import { execa } from "execa";
|
|
16
|
+
async function stopAllSessions() {
|
|
17
|
+
const store = new SessionStore();
|
|
18
|
+
let sessions;
|
|
19
|
+
try {
|
|
20
|
+
sessions = store.listAll();
|
|
21
|
+
} finally {
|
|
22
|
+
store.close();
|
|
23
|
+
}
|
|
24
|
+
if (sessions.length === 0) {
|
|
25
|
+
console.log(chalk.gray("No sessions found."));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const runningSessions = [];
|
|
29
|
+
for (const session of sessions) {
|
|
30
|
+
const envFile = resolve(session.session_dir, ".env.session");
|
|
31
|
+
if (existsSync(envFile)) {
|
|
32
|
+
const running = await isRunning({ cwd: session.session_dir });
|
|
33
|
+
if (running) {
|
|
34
|
+
runningSessions.push({ sessionId: session.session_id, path: session.session_dir, projectRoot: session.project_root });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (runningSessions.length === 0) {
|
|
39
|
+
console.log(chalk.gray("No running sessions found."));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
console.log(chalk.blue(`Stopping ${runningSessions.length} running session(s)...
|
|
43
|
+
`));
|
|
44
|
+
for (const session of runningSessions) {
|
|
45
|
+
console.log(chalk.gray(` Stopping session ${session.sessionId}...`));
|
|
46
|
+
try {
|
|
47
|
+
const config = await loadConfig(session.projectRoot);
|
|
48
|
+
const allApps = config.apps ?? [];
|
|
49
|
+
const profileFlags = allApps.flatMap((p) => ["--profile", p]);
|
|
50
|
+
await execa(
|
|
51
|
+
"docker",
|
|
52
|
+
["compose", "-f", "docker-compose.session.yml", "--env-file", ".env.session", ...profileFlags, "stop"],
|
|
53
|
+
{ cwd: session.path, stdio: "inherit" }
|
|
54
|
+
);
|
|
55
|
+
console.log(chalk.green(` Session ${session.sessionId} stopped.`));
|
|
56
|
+
} catch {
|
|
57
|
+
console.log(chalk.yellow(` Warning: Could not stop session ${session.sessionId}`));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
console.log(chalk.green(`
|
|
61
|
+
Stopped ${runningSessions.length} session(s).`));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export {
|
|
65
|
+
stopAllSessions
|
|
66
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// src/lib/compose.ts
|
|
2
|
+
import { writeFileSync } from "fs";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
import { createHash } from "crypto";
|
|
5
|
+
function generateComposeFile(workingDir, projectName, services) {
|
|
6
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7
|
+
const dirHash = createHash("md5").update(workingDir).digest("hex").substring(0, 8);
|
|
8
|
+
const composeProjectName = `${projectName}-${dirHash}`;
|
|
9
|
+
const lines = [
|
|
10
|
+
"# Auto-generated by dev-prism - DO NOT EDIT",
|
|
11
|
+
'version: "3.8"',
|
|
12
|
+
"",
|
|
13
|
+
"x-dev-prism-labels: &dev-prism-labels",
|
|
14
|
+
' dev-prism.managed: "true"',
|
|
15
|
+
` dev-prism.working_dir: "${workingDir}"`,
|
|
16
|
+
` dev-prism.session_id: "${workingDir}"`,
|
|
17
|
+
` dev-prism.created_at: "${createdAt}"`,
|
|
18
|
+
"",
|
|
19
|
+
"services:"
|
|
20
|
+
];
|
|
21
|
+
for (const service of services) {
|
|
22
|
+
lines.push(` ${service.name}:`);
|
|
23
|
+
lines.push(` extends:`);
|
|
24
|
+
lines.push(` file: docker-compose.yml`);
|
|
25
|
+
lines.push(` service: ${service.name}`);
|
|
26
|
+
lines.push(` ports:`);
|
|
27
|
+
lines.push(` - "0:${service.internalPort}" # Random host port`);
|
|
28
|
+
lines.push(` labels:`);
|
|
29
|
+
lines.push(` <<: *dev-prism-labels`);
|
|
30
|
+
lines.push(` dev-prism.service: "${service.name}"`);
|
|
31
|
+
lines.push(` dev-prism.internal_port: "${service.internalPort}"`);
|
|
32
|
+
lines.push(``);
|
|
33
|
+
}
|
|
34
|
+
lines.unshift(`# Compose project name: ${composeProjectName}`);
|
|
35
|
+
lines.unshift("");
|
|
36
|
+
return lines.join("\n");
|
|
37
|
+
}
|
|
38
|
+
function writeComposeFile(workingDir, projectName, services) {
|
|
39
|
+
const content = generateComposeFile(workingDir, projectName, services);
|
|
40
|
+
const filePath = resolve(workingDir, "docker-compose.session.yml");
|
|
41
|
+
writeFileSync(filePath, content, "utf-8");
|
|
42
|
+
return filePath;
|
|
43
|
+
}
|
|
44
|
+
function generateEnvStub(workingDir, projectName) {
|
|
45
|
+
const dirHash = createHash("md5").update(workingDir).digest("hex").substring(0, 8);
|
|
46
|
+
const composeProjectName = `${projectName}-${dirHash}`;
|
|
47
|
+
const lines = [
|
|
48
|
+
"# Auto-generated by dev-prism",
|
|
49
|
+
`COMPOSE_PROJECT_NAME=${composeProjectName}`,
|
|
50
|
+
`SESSION_DIR=${workingDir}`,
|
|
51
|
+
"",
|
|
52
|
+
"# Ports will be added after containers start"
|
|
53
|
+
];
|
|
54
|
+
return lines.join("\n") + "\n";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export {
|
|
58
|
+
generateComposeFile,
|
|
59
|
+
writeComposeFile,
|
|
60
|
+
generateEnvStub
|
|
61
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// src/lib/docker-inspect.ts
|
|
2
|
+
import { execa } from "execa";
|
|
3
|
+
var DEV_PRISM_LABEL = "dev-prism.managed=true";
|
|
4
|
+
async function listManagedContainers() {
|
|
5
|
+
try {
|
|
6
|
+
const result = await execa("docker", [
|
|
7
|
+
"ps",
|
|
8
|
+
"--filter",
|
|
9
|
+
`label=${DEV_PRISM_LABEL}`,
|
|
10
|
+
"--format",
|
|
11
|
+
"{{json .}}"
|
|
12
|
+
]);
|
|
13
|
+
if (!result.stdout.trim()) {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
return result.stdout.trim().split("\n").map((line) => {
|
|
17
|
+
const raw = JSON.parse(line);
|
|
18
|
+
return {
|
|
19
|
+
id: raw.ID,
|
|
20
|
+
name: raw.Names,
|
|
21
|
+
state: raw.State,
|
|
22
|
+
labels: parseLabels(raw.Labels),
|
|
23
|
+
ports: []
|
|
24
|
+
// Will be filled in by inspectContainer if needed
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error("Failed to list containers:", error);
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function parseLabels(labelsStr) {
|
|
33
|
+
if (!labelsStr) return {};
|
|
34
|
+
const labels = {};
|
|
35
|
+
const pairs = labelsStr.split(",");
|
|
36
|
+
for (const pair of pairs) {
|
|
37
|
+
const [key, ...valueParts] = pair.split("=");
|
|
38
|
+
if (key) {
|
|
39
|
+
labels[key] = valueParts.join("=");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return labels;
|
|
43
|
+
}
|
|
44
|
+
async function inspectContainer(containerId) {
|
|
45
|
+
try {
|
|
46
|
+
const result = await execa("docker", ["inspect", containerId]);
|
|
47
|
+
const data = JSON.parse(result.stdout);
|
|
48
|
+
if (!data || data.length === 0) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const container = data[0];
|
|
52
|
+
const ports = [];
|
|
53
|
+
const portBindings = container.NetworkSettings?.Ports || {};
|
|
54
|
+
for (const [portSpec, bindings] of Object.entries(portBindings)) {
|
|
55
|
+
const [portStr, protocol] = portSpec.split("/");
|
|
56
|
+
const privatePort = parseInt(portStr, 10);
|
|
57
|
+
if (Array.isArray(bindings) && bindings.length > 0) {
|
|
58
|
+
const binding = bindings[0];
|
|
59
|
+
const publicPort = parseInt(binding.HostPort, 10);
|
|
60
|
+
ports.push({
|
|
61
|
+
privatePort,
|
|
62
|
+
publicPort,
|
|
63
|
+
type: protocol
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
id: container.Id,
|
|
69
|
+
name: container.Name.replace(/^\//, ""),
|
|
70
|
+
// Remove leading slash
|
|
71
|
+
state: container.State.Status,
|
|
72
|
+
labels: container.Config.Labels || {},
|
|
73
|
+
ports
|
|
74
|
+
};
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error(`Failed to inspect container ${containerId}:`, error);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function getPortMappings(workingDir) {
|
|
81
|
+
const containers = await listManagedContainers();
|
|
82
|
+
const sessionContainers = containers.filter(
|
|
83
|
+
(c) => c.labels["dev-prism.working_dir"] === workingDir
|
|
84
|
+
);
|
|
85
|
+
const mappings = [];
|
|
86
|
+
for (const container of sessionContainers) {
|
|
87
|
+
const detailed = await inspectContainer(container.id);
|
|
88
|
+
if (!detailed) continue;
|
|
89
|
+
const serviceName = detailed.labels["dev-prism.service"];
|
|
90
|
+
if (!serviceName) continue;
|
|
91
|
+
for (const port of detailed.ports) {
|
|
92
|
+
if (port.publicPort) {
|
|
93
|
+
mappings.push({
|
|
94
|
+
service: serviceName,
|
|
95
|
+
internalPort: port.privatePort,
|
|
96
|
+
externalPort: port.publicPort
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return mappings;
|
|
102
|
+
}
|
|
103
|
+
function groupContainersByWorkingDir(containers) {
|
|
104
|
+
const groups = /* @__PURE__ */ new Map();
|
|
105
|
+
for (const container of containers) {
|
|
106
|
+
const workingDir = container.labels["dev-prism.working_dir"];
|
|
107
|
+
if (!workingDir) continue;
|
|
108
|
+
if (!groups.has(workingDir)) {
|
|
109
|
+
groups.set(workingDir, []);
|
|
110
|
+
}
|
|
111
|
+
groups.get(workingDir).push(container);
|
|
112
|
+
}
|
|
113
|
+
return groups;
|
|
114
|
+
}
|
|
115
|
+
async function sessionExists(workingDir) {
|
|
116
|
+
const containers = await listManagedContainers();
|
|
117
|
+
return containers.some((c) => c.labels["dev-prism.working_dir"] === workingDir);
|
|
118
|
+
}
|
|
119
|
+
async function getComposeProject(workingDir) {
|
|
120
|
+
const containers = await listManagedContainers();
|
|
121
|
+
const container = containers.find((c) => c.labels["dev-prism.working_dir"] === workingDir);
|
|
122
|
+
return container?.labels["com.docker.compose.project"] || null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export {
|
|
126
|
+
listManagedContainers,
|
|
127
|
+
inspectContainer,
|
|
128
|
+
getPortMappings,
|
|
129
|
+
groupContainersByWorkingDir,
|
|
130
|
+
sessionExists,
|
|
131
|
+
getComposeProject
|
|
132
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {
|
|
2
|
+
listActiveSessions
|
|
3
|
+
} from "./chunk-DQER5GNG.js";
|
|
4
|
+
|
|
5
|
+
// src/commands/stop-all.ts
|
|
6
|
+
import { existsSync, unlinkSync } from "fs";
|
|
7
|
+
import { resolve } from "path";
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import { execa } from "execa";
|
|
10
|
+
async function stopAllSessions() {
|
|
11
|
+
const sessions = await listActiveSessions();
|
|
12
|
+
if (sessions.length === 0) {
|
|
13
|
+
console.log(chalk.gray("No active sessions found."));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
console.log(chalk.blue(`Stopping ${sessions.length} session(s)...
|
|
17
|
+
`));
|
|
18
|
+
for (const session of sessions) {
|
|
19
|
+
console.log(chalk.gray(` Stopping session in ${session.workingDir}...`));
|
|
20
|
+
try {
|
|
21
|
+
if (session.containers.length > 0) {
|
|
22
|
+
const containerIds = session.containers.map((c) => c.id);
|
|
23
|
+
await execa("docker", ["stop", ...containerIds], { stdio: "pipe" });
|
|
24
|
+
}
|
|
25
|
+
const envPath = resolve(session.workingDir, ".env.session");
|
|
26
|
+
if (existsSync(envPath)) {
|
|
27
|
+
unlinkSync(envPath);
|
|
28
|
+
}
|
|
29
|
+
console.log(chalk.green(` Stopped: ${session.workingDir}`));
|
|
30
|
+
} catch {
|
|
31
|
+
console.log(chalk.yellow(` Warning: Could not stop session in ${session.workingDir}`));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
console.log(chalk.green(`
|
|
35
|
+
Stopped ${sessions.length} session(s).`));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export {
|
|
39
|
+
stopAllSessions
|
|
40
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// src/lib/ports.ts
|
|
2
|
+
function extractPorts(portMappings) {
|
|
3
|
+
const ports = {};
|
|
4
|
+
for (const mapping of portMappings) {
|
|
5
|
+
const envVarName = `${mapping.service.toUpperCase()}_PORT`;
|
|
6
|
+
ports[envVarName] = mapping.externalPort;
|
|
7
|
+
}
|
|
8
|
+
return ports;
|
|
9
|
+
}
|
|
10
|
+
function formatPortsTable(ports) {
|
|
11
|
+
const lines = [];
|
|
12
|
+
for (const [name, port] of Object.entries(ports)) {
|
|
13
|
+
lines.push(` ${name}: http://localhost:${port}`);
|
|
14
|
+
}
|
|
15
|
+
return lines.join("\n");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
extractPorts,
|
|
20
|
+
formatPortsTable
|
|
21
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// src/lib/compose.ts
|
|
2
|
+
import { writeFileSync } from "fs";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
import { createHash } from "crypto";
|
|
5
|
+
function generateComposeFile(workingDir, projectName, services) {
|
|
6
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7
|
+
const dirHash = createHash("md5").update(workingDir).digest("hex").substring(0, 8);
|
|
8
|
+
const composeProjectName = `${projectName}-${dirHash}`;
|
|
9
|
+
const lines = [
|
|
10
|
+
"# Auto-generated by dev-prism - DO NOT EDIT",
|
|
11
|
+
"",
|
|
12
|
+
"x-dev-prism-labels: &dev-prism-labels",
|
|
13
|
+
' dev-prism.managed: "true"',
|
|
14
|
+
` dev-prism.working_dir: "${workingDir}"`,
|
|
15
|
+
` dev-prism.session_id: "${workingDir}"`,
|
|
16
|
+
` dev-prism.created_at: "${createdAt}"`,
|
|
17
|
+
"",
|
|
18
|
+
"services:"
|
|
19
|
+
];
|
|
20
|
+
for (const service of services) {
|
|
21
|
+
lines.push(` ${service.name}:`);
|
|
22
|
+
lines.push(` extends:`);
|
|
23
|
+
lines.push(` file: docker-compose.yml`);
|
|
24
|
+
lines.push(` service: ${service.name}`);
|
|
25
|
+
lines.push(` ports:`);
|
|
26
|
+
lines.push(` - "0:${service.internalPort}" # Random host port`);
|
|
27
|
+
lines.push(` labels:`);
|
|
28
|
+
lines.push(` <<: *dev-prism-labels`);
|
|
29
|
+
lines.push(` dev-prism.service: "${service.name}"`);
|
|
30
|
+
lines.push(` dev-prism.internal_port: "${service.internalPort}"`);
|
|
31
|
+
lines.push(``);
|
|
32
|
+
}
|
|
33
|
+
lines.unshift(`# Compose project name: ${composeProjectName}`);
|
|
34
|
+
lines.unshift("");
|
|
35
|
+
lines.push("volumes:");
|
|
36
|
+
lines.push(" postgres-data:");
|
|
37
|
+
return lines.join("\n");
|
|
38
|
+
}
|
|
39
|
+
function writeComposeFile(workingDir, projectName, services) {
|
|
40
|
+
const content = generateComposeFile(workingDir, projectName, services);
|
|
41
|
+
const filePath = resolve(workingDir, "docker-compose.session.yml");
|
|
42
|
+
writeFileSync(filePath, content, "utf-8");
|
|
43
|
+
return filePath;
|
|
44
|
+
}
|
|
45
|
+
function generateEnvStub(workingDir, projectName) {
|
|
46
|
+
const composeProjectName = getComposeProjectName(workingDir, projectName);
|
|
47
|
+
const lines = [
|
|
48
|
+
"# Auto-generated by dev-prism",
|
|
49
|
+
`COMPOSE_PROJECT_NAME=${composeProjectName}`,
|
|
50
|
+
`SESSION_DIR=${workingDir}`,
|
|
51
|
+
"",
|
|
52
|
+
"# Ports will be added after containers start"
|
|
53
|
+
];
|
|
54
|
+
return lines.join("\n") + "\n";
|
|
55
|
+
}
|
|
56
|
+
function getComposeProjectName(workingDir, projectName) {
|
|
57
|
+
const dirHash = createHash("md5").update(workingDir).digest("hex").substring(0, 8);
|
|
58
|
+
return `${projectName}-${dirHash}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export {
|
|
62
|
+
generateComposeFile,
|
|
63
|
+
writeComposeFile,
|
|
64
|
+
generateEnvStub,
|
|
65
|
+
getComposeProjectName
|
|
66
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadConfig
|
|
3
|
+
} from "./chunk-25WQHUYW.js";
|
|
4
|
+
import {
|
|
5
|
+
getSession
|
|
6
|
+
} from "./chunk-DQER5GNG.js";
|
|
7
|
+
|
|
8
|
+
// src/commands/logs.ts
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
import { execa } from "execa";
|
|
11
|
+
import { resolve } from "path";
|
|
12
|
+
import { existsSync } from "fs";
|
|
13
|
+
async function streamLogs(workingDir, options) {
|
|
14
|
+
const session = await getSession(workingDir);
|
|
15
|
+
if (!session) {
|
|
16
|
+
console.error(chalk.red(`Error: No session found in ${workingDir}`));
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
let projectRoot = workingDir;
|
|
20
|
+
for (let i = 0; i < 5; i++) {
|
|
21
|
+
const configPath = resolve(projectRoot, "session.config.mjs");
|
|
22
|
+
const altConfigPath = resolve(projectRoot, "session.config.js");
|
|
23
|
+
if (existsSync(configPath) || existsSync(altConfigPath)) {
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
projectRoot = resolve(projectRoot, "..");
|
|
27
|
+
}
|
|
28
|
+
const config = await loadConfig(projectRoot);
|
|
29
|
+
let profileFlags = [];
|
|
30
|
+
if (options.mode === "docker") {
|
|
31
|
+
const allApps = config.apps ?? [];
|
|
32
|
+
const excludeApps = options.without ?? [];
|
|
33
|
+
const profiles = allApps.filter((app) => !excludeApps.includes(app));
|
|
34
|
+
profileFlags = profiles.flatMap((p) => ["--profile", p]);
|
|
35
|
+
}
|
|
36
|
+
const args = [
|
|
37
|
+
"compose",
|
|
38
|
+
"-f",
|
|
39
|
+
"docker-compose.session.yml",
|
|
40
|
+
"--env-file",
|
|
41
|
+
".env.session",
|
|
42
|
+
...profileFlags,
|
|
43
|
+
"logs",
|
|
44
|
+
"-f",
|
|
45
|
+
"--tail",
|
|
46
|
+
options.tail ?? "50"
|
|
47
|
+
];
|
|
48
|
+
await execa("docker", args, { cwd: workingDir, stdio: "inherit" });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export {
|
|
52
|
+
streamLogs
|
|
53
|
+
};
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import {
|
|
2
|
+
extractPorts,
|
|
3
|
+
formatPortsTable
|
|
4
|
+
} from "./chunk-LAOWFCQL.js";
|
|
5
|
+
import {
|
|
6
|
+
generateEnvStub,
|
|
7
|
+
getComposeProjectName,
|
|
8
|
+
writeComposeFile
|
|
9
|
+
} from "./chunk-H3L73URT.js";
|
|
10
|
+
import {
|
|
11
|
+
writeAppEnvFiles,
|
|
12
|
+
writeEnvFile
|
|
13
|
+
} from "./chunk-HZBJF67X.js";
|
|
14
|
+
import {
|
|
15
|
+
createWorktree,
|
|
16
|
+
generateDefaultBranchName,
|
|
17
|
+
removeWorktree
|
|
18
|
+
} from "./chunk-3Q454U3I.js";
|
|
19
|
+
import {
|
|
20
|
+
down,
|
|
21
|
+
logs,
|
|
22
|
+
up
|
|
23
|
+
} from "./chunk-GBN67HYD.js";
|
|
24
|
+
import {
|
|
25
|
+
getSessionsDir,
|
|
26
|
+
loadConfig
|
|
27
|
+
} from "./chunk-25WQHUYW.js";
|
|
28
|
+
import {
|
|
29
|
+
getPortMappings,
|
|
30
|
+
sessionExists
|
|
31
|
+
} from "./chunk-JZ2VPQXP.js";
|
|
32
|
+
|
|
33
|
+
// src/commands/create.ts
|
|
34
|
+
import { existsSync, mkdirSync, copyFileSync, readFileSync, writeFileSync } from "fs";
|
|
35
|
+
import { basename, join, resolve } from "path";
|
|
36
|
+
import chalk from "chalk";
|
|
37
|
+
import { execa } from "execa";
|
|
38
|
+
function updateEnvDatabaseUrl(envPath, newDbUrl) {
|
|
39
|
+
if (!existsSync(envPath)) return;
|
|
40
|
+
let content = readFileSync(envPath, "utf-8");
|
|
41
|
+
if (content.includes("DATABASE_URL=")) {
|
|
42
|
+
content = content.replace(/^DATABASE_URL=.*/m, `DATABASE_URL=${newDbUrl}`);
|
|
43
|
+
} else {
|
|
44
|
+
content += `
|
|
45
|
+
DATABASE_URL=${newDbUrl}
|
|
46
|
+
`;
|
|
47
|
+
}
|
|
48
|
+
writeFileSync(envPath, content);
|
|
49
|
+
}
|
|
50
|
+
async function createSession(projectRoot, _sessionId, options) {
|
|
51
|
+
const config = await loadConfig(projectRoot);
|
|
52
|
+
const sessionsDir = getSessionsDir(config, projectRoot);
|
|
53
|
+
const inPlace = options.inPlace ?? false;
|
|
54
|
+
const mode = options.mode || "docker";
|
|
55
|
+
let workingDir;
|
|
56
|
+
let branchName = "";
|
|
57
|
+
if (inPlace) {
|
|
58
|
+
workingDir = projectRoot;
|
|
59
|
+
console.log(chalk.blue(`Creating session in current directory (${mode} mode)...`));
|
|
60
|
+
} else {
|
|
61
|
+
branchName = options.branch || generateDefaultBranchName();
|
|
62
|
+
workingDir = resolve(sessionsDir, branchName);
|
|
63
|
+
console.log(chalk.blue(`Creating session (${mode} mode)...`));
|
|
64
|
+
console.log(chalk.gray(`Branch: ${branchName}`));
|
|
65
|
+
console.log(chalk.gray(`Directory: ${workingDir}`));
|
|
66
|
+
}
|
|
67
|
+
if (await sessionExists(workingDir)) {
|
|
68
|
+
console.error(chalk.red(`
|
|
69
|
+
Error: Session already running in this directory.`));
|
|
70
|
+
console.error(chalk.gray(`Stop it first with: dev-prism stop`));
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
let profiles;
|
|
74
|
+
try {
|
|
75
|
+
if (!inPlace) {
|
|
76
|
+
if (!existsSync(sessionsDir)) {
|
|
77
|
+
mkdirSync(sessionsDir, { recursive: true });
|
|
78
|
+
}
|
|
79
|
+
console.log(chalk.blue("\nCreating git worktree..."));
|
|
80
|
+
await createWorktree(projectRoot, workingDir, branchName);
|
|
81
|
+
console.log(chalk.green(` Created: ${workingDir}`));
|
|
82
|
+
}
|
|
83
|
+
const projectName = config.projectName ?? basename(projectRoot);
|
|
84
|
+
const services = config.services ?? [
|
|
85
|
+
{ name: "postgres", internalPort: 5432 },
|
|
86
|
+
{ name: "app", internalPort: 3e3 }
|
|
87
|
+
];
|
|
88
|
+
console.log(chalk.blue("\nGenerating docker-compose.session.yml..."));
|
|
89
|
+
const composePath = writeComposeFile(workingDir, projectName, services);
|
|
90
|
+
console.log(chalk.green(` Written: ${composePath}`));
|
|
91
|
+
console.log(chalk.blue("\nGenerating .env.session stub..."));
|
|
92
|
+
const envStub = generateEnvStub(workingDir, projectName);
|
|
93
|
+
const envPath = resolve(workingDir, ".env.session");
|
|
94
|
+
writeFileSync(envPath, envStub, "utf-8");
|
|
95
|
+
console.log(chalk.green(` Written: ${envPath}`));
|
|
96
|
+
if (!inPlace) {
|
|
97
|
+
const envFilesToCopy = config.envFiles ?? [];
|
|
98
|
+
for (const envFile of envFilesToCopy) {
|
|
99
|
+
const srcPath = join(projectRoot, envFile);
|
|
100
|
+
const destPath = join(workingDir, envFile);
|
|
101
|
+
if (existsSync(srcPath)) {
|
|
102
|
+
copyFileSync(srcPath, destPath);
|
|
103
|
+
console.log(chalk.green(` Copied: ${envFile}`));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
console.log(chalk.blue("\nStarting Docker services..."));
|
|
108
|
+
if (mode === "docker") {
|
|
109
|
+
const allApps = config.apps ?? [];
|
|
110
|
+
const excludeApps = options.without ?? [];
|
|
111
|
+
profiles = allApps.filter((app) => !excludeApps.includes(app));
|
|
112
|
+
if (excludeApps.length > 0) {
|
|
113
|
+
console.log(chalk.gray(` Excluding apps: ${excludeApps.join(", ")}`));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
await up({ cwd: workingDir, profiles, detach: true });
|
|
117
|
+
console.log(chalk.blue("Waiting for services to be ready..."));
|
|
118
|
+
await new Promise((resolve2) => setTimeout(resolve2, 3e3));
|
|
119
|
+
console.log(chalk.blue("\nDiscovering ports from containers..."));
|
|
120
|
+
const portMappings = await getPortMappings(workingDir);
|
|
121
|
+
const ports = extractPorts(portMappings);
|
|
122
|
+
console.log(chalk.gray("Discovered ports:"));
|
|
123
|
+
console.log(chalk.gray(formatPortsTable(ports)));
|
|
124
|
+
const composeProjectName = getComposeProjectName(workingDir, projectName);
|
|
125
|
+
const finalEnvContent = writeEnvFile(workingDir, ports, composeProjectName);
|
|
126
|
+
console.log(chalk.green(` Updated: ${finalEnvContent}`));
|
|
127
|
+
const appEnvFiles = writeAppEnvFiles(config, workingDir, ports);
|
|
128
|
+
for (const file of appEnvFiles) {
|
|
129
|
+
console.log(chalk.green(` Written: ${file}`));
|
|
130
|
+
}
|
|
131
|
+
if (!inPlace && ports.POSTGRES_PORT) {
|
|
132
|
+
const sessionDbUrl = `postgresql://postgres:postgres@localhost:${ports.POSTGRES_PORT}/postgres`;
|
|
133
|
+
const envFilesToCopy = config.envFiles ?? [];
|
|
134
|
+
for (const envFile of envFilesToCopy) {
|
|
135
|
+
const destPath = join(workingDir, envFile);
|
|
136
|
+
if (existsSync(destPath)) {
|
|
137
|
+
updateEnvDatabaseUrl(destPath, sessionDbUrl);
|
|
138
|
+
console.log(chalk.green(` Updated DATABASE_URL in: ${envFile}`));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (config.setup.length > 0) {
|
|
143
|
+
console.log(chalk.blue("\nRunning setup commands..."));
|
|
144
|
+
const setupEnv = {
|
|
145
|
+
...process.env,
|
|
146
|
+
SESSION_DIR: workingDir
|
|
147
|
+
};
|
|
148
|
+
for (const [name, port] of Object.entries(ports)) {
|
|
149
|
+
setupEnv[name] = String(port);
|
|
150
|
+
}
|
|
151
|
+
if (ports.POSTGRES_PORT) {
|
|
152
|
+
setupEnv.DATABASE_URL = `postgresql://postgres:postgres@localhost:${ports.POSTGRES_PORT}/postgres`;
|
|
153
|
+
}
|
|
154
|
+
for (const cmd of config.setup) {
|
|
155
|
+
console.log(chalk.gray(` Running: ${cmd}`));
|
|
156
|
+
const [command, ...args] = cmd.split(" ");
|
|
157
|
+
try {
|
|
158
|
+
await execa(command, args, {
|
|
159
|
+
cwd: workingDir,
|
|
160
|
+
stdio: "inherit",
|
|
161
|
+
env: setupEnv
|
|
162
|
+
});
|
|
163
|
+
} catch {
|
|
164
|
+
console.warn(chalk.yellow(` Warning: Command failed: ${cmd}`));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
console.log(chalk.green(`
|
|
169
|
+
Session ready!`));
|
|
170
|
+
console.log(chalk.gray(`Directory: ${workingDir}`));
|
|
171
|
+
if (mode === "docker") {
|
|
172
|
+
console.log(chalk.gray("\nDocker mode - all services in containers."));
|
|
173
|
+
console.log(chalk.gray("View logs: docker compose -f docker-compose.session.yml logs -f"));
|
|
174
|
+
} else {
|
|
175
|
+
console.log(chalk.gray("\nNative mode - run apps with: pnpm dev"));
|
|
176
|
+
}
|
|
177
|
+
console.log(chalk.gray("\nPorts:"));
|
|
178
|
+
for (const [name, port] of Object.entries(ports)) {
|
|
179
|
+
console.log(chalk.cyan(` ${name}: http://localhost:${port}`));
|
|
180
|
+
}
|
|
181
|
+
if (options.detach === false) {
|
|
182
|
+
console.log(chalk.blue("\nStreaming logs (Ctrl+C to stop)..."));
|
|
183
|
+
console.log(chalk.gray("\u2500".repeat(60)));
|
|
184
|
+
try {
|
|
185
|
+
await logs({ cwd: workingDir, profiles });
|
|
186
|
+
} catch (error) {
|
|
187
|
+
const execaError = error;
|
|
188
|
+
if (execaError.signal === "SIGINT") {
|
|
189
|
+
console.log(chalk.gray("\n\u2500".repeat(60)));
|
|
190
|
+
console.log(chalk.yellow("\nLog streaming stopped. Services are still running."));
|
|
191
|
+
console.log(
|
|
192
|
+
chalk.gray(
|
|
193
|
+
`Resume logs: cd ${workingDir} && docker compose -f docker-compose.session.yml --env-file .env.session logs -f`
|
|
194
|
+
)
|
|
195
|
+
);
|
|
196
|
+
} else {
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
} catch (error) {
|
|
202
|
+
console.error(chalk.red("Session creation failed. Cleaning up..."));
|
|
203
|
+
try {
|
|
204
|
+
await down({ cwd: workingDir });
|
|
205
|
+
} catch {
|
|
206
|
+
}
|
|
207
|
+
if (!inPlace && branchName) {
|
|
208
|
+
try {
|
|
209
|
+
await removeWorktree(projectRoot, workingDir, branchName);
|
|
210
|
+
} catch {
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export {
|
|
218
|
+
createSession
|
|
219
|
+
};
|