totopo 0.6.1 → 0.7.0-rc-2
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 +6 -0
- package/bin/totopo.js +64 -26
- package/package.json +1 -1
- package/src/core/commands/manage.ts +151 -0
- package/src/core/commands/menu.ts +7 -4
- package/src/core/commands/rebuild.ts +29 -0
- package/src/core/commands/stop.ts +19 -14
- package/src/core/lib/generate-dockerfile.ts +1 -1
- package/templates/Dockerfile +1 -1
- package/src/core/commands/reset.ts +0 -46
package/README.md
CHANGED
|
@@ -112,6 +112,12 @@ git push / pull / fetch
|
|
|
112
112
|
|
|
113
113
|
---
|
|
114
114
|
|
|
115
|
+
## Limitations
|
|
116
|
+
|
|
117
|
+
**No audio / microphone support** — the container has no access to host audio devices. Features that require microphone input (e.g. Claude Code's `/voice` mode) will not work inside the container.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
115
121
|
## Troubleshooting
|
|
116
122
|
|
|
117
123
|
**Container fails to start** — the startup check prints exactly which check failed and why.
|
package/bin/totopo.js
CHANGED
|
@@ -47,13 +47,14 @@ process.env.TOTOPO_REPO_ROOT = repoRoot;
|
|
|
47
47
|
// ─── Auto-install dependencies ────────────────────────────────────────────────
|
|
48
48
|
const tsx = join(packageDir, "node_modules/.bin/tsx");
|
|
49
49
|
if (!existsSync(tsx)) {
|
|
50
|
-
|
|
50
|
+
process.stdout.write(" Getting ready…");
|
|
51
51
|
let pm = "npm";
|
|
52
52
|
try {
|
|
53
53
|
execSync("which pnpm", { stdio: "ignore" });
|
|
54
54
|
pm = "pnpm";
|
|
55
55
|
} catch {}
|
|
56
56
|
execSync(`${pm} install --silent`, { cwd: packageDir, stdio: "inherit" });
|
|
57
|
+
process.stdout.write("\r\x1b[2K"); // clear the line
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
// ─── Helper ───────────────────────────────────────────────────────────────────
|
|
@@ -84,6 +85,20 @@ const dockerResult = spawnSync("docker", ["ps", "--filter", "name=totopo-managed
|
|
|
84
85
|
});
|
|
85
86
|
const activeCount = dockerResult.stdout ? dockerResult.stdout.trim().split("\n").filter(Boolean).length : 0;
|
|
86
87
|
|
|
88
|
+
// Is THIS project's container running?
|
|
89
|
+
const projectContainerResult = spawnSync("docker", ["ps", "--filter", `name=totopo-managed-${projectName}`, "--format", "{{.Names}}"], {
|
|
90
|
+
encoding: "utf8",
|
|
91
|
+
});
|
|
92
|
+
const projectRunning = (projectContainerResult.stdout ?? "")
|
|
93
|
+
.trim()
|
|
94
|
+
.split("\n")
|
|
95
|
+
.filter(Boolean)
|
|
96
|
+
.some((n) => n === `totopo-managed-${projectName}`);
|
|
97
|
+
|
|
98
|
+
// Does THIS project's image exist?
|
|
99
|
+
const projectImageResult = spawnSync("docker", ["images", "-q", `totopo-managed-${projectName}`], { encoding: "utf8" });
|
|
100
|
+
const projectImageExists = (projectImageResult.stdout ?? "").trim().length > 0;
|
|
101
|
+
|
|
87
102
|
let hasKey = false;
|
|
88
103
|
const envPath = join(repoRoot, ".totopo/.env");
|
|
89
104
|
if (existsSync(envPath)) {
|
|
@@ -102,29 +117,52 @@ if (existsSync(envPath)) {
|
|
|
102
117
|
// stdout → /dev/tty so the clack UI renders on the terminal
|
|
103
118
|
// stderr → pipe so the selected action string is captured
|
|
104
119
|
const ttyFd = openSync("/dev/tty", "w");
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
120
|
+
|
|
121
|
+
let showMenu = true;
|
|
122
|
+
while (showMenu) {
|
|
123
|
+
showMenu = false;
|
|
124
|
+
|
|
125
|
+
const menuResult = spawnSync(
|
|
126
|
+
tsx,
|
|
127
|
+
[
|
|
128
|
+
join(packageDir, "src/core/commands/menu.ts"),
|
|
129
|
+
projectName,
|
|
130
|
+
String(activeCount),
|
|
131
|
+
String(hasKey),
|
|
132
|
+
String(projectRunning),
|
|
133
|
+
String(projectImageExists),
|
|
134
|
+
],
|
|
135
|
+
{
|
|
136
|
+
stdio: ["inherit", ttyFd, "pipe"],
|
|
137
|
+
encoding: "utf8",
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
const action = (menuResult.stderr ?? "").trim();
|
|
141
|
+
|
|
142
|
+
// ─── Execute selection ────────────────────────────────────────────────────────
|
|
143
|
+
switch (action) {
|
|
144
|
+
case "dev":
|
|
145
|
+
run("dev.ts");
|
|
146
|
+
break;
|
|
147
|
+
case "stop":
|
|
148
|
+
run("stop.ts", [projectName]);
|
|
149
|
+
break;
|
|
150
|
+
case "rebuild":
|
|
151
|
+
run("rebuild.ts", [projectName]);
|
|
152
|
+
run("dev.ts");
|
|
153
|
+
break;
|
|
154
|
+
case "manage": {
|
|
155
|
+
const result = run("manage.ts", [projectName]);
|
|
156
|
+
if (result.status === 2) showMenu = true; // Back selected
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
case "doctor":
|
|
160
|
+
run("doctor.ts", ["--verbose"]);
|
|
161
|
+
break;
|
|
162
|
+
case "settings":
|
|
163
|
+
run("settings.ts");
|
|
164
|
+
break;
|
|
165
|
+
default:
|
|
166
|
+
break; // quit or cancelled
|
|
167
|
+
}
|
|
130
168
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// =========================================================================================================================================
|
|
3
|
+
// src/core/commands/manage.ts — Manage workspaces submenu
|
|
4
|
+
// Invoked by bin/totopo.js — do not run directly.
|
|
5
|
+
// =========================================================================================================================================
|
|
6
|
+
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { rmSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { cancel, confirm, isCancel, log, multiselect, outro, select } from "@clack/prompts";
|
|
11
|
+
|
|
12
|
+
const [projectName = "unknown"] = process.argv.slice(2);
|
|
13
|
+
|
|
14
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
15
|
+
function stopAndRemoveContainer(name: string) {
|
|
16
|
+
spawnSync("docker", ["stop", name], { stdio: "inherit" });
|
|
17
|
+
spawnSync("docker", ["rm", name], { stdio: "inherit" });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ─── Submenu ─────────────────────────────────────────────────────────────────
|
|
21
|
+
const action = await select({
|
|
22
|
+
message: "Manage workspaces:",
|
|
23
|
+
options: [
|
|
24
|
+
{ value: "stop-containers", label: "Stop running containers" },
|
|
25
|
+
{ value: "remove-images", label: "Remove images" },
|
|
26
|
+
{ value: "uninstall", label: "Uninstall from this project" },
|
|
27
|
+
{ value: "back", label: "← Back" },
|
|
28
|
+
],
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (isCancel(action) || action === "back") {
|
|
32
|
+
process.exit(2); // 2 = "back" signal to bin/totopo.js
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ─── A: Stop running containers ───────────────────────────────────────────────
|
|
36
|
+
if (action === "stop-containers") {
|
|
37
|
+
const listResult = spawnSync("docker", ["ps", "--filter", "name=totopo-managed-", "--format", "{{.Names}}"], { encoding: "utf8" });
|
|
38
|
+
const running = (listResult.stdout ?? "").trim().split("\n").filter(Boolean);
|
|
39
|
+
|
|
40
|
+
if (running.length === 0) {
|
|
41
|
+
log.info("No running containers.");
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let toStop: string[];
|
|
46
|
+
if (running.length === 1) {
|
|
47
|
+
toStop = running;
|
|
48
|
+
log.info(`Stopping ${running[0]}...`);
|
|
49
|
+
} else {
|
|
50
|
+
const selected = await multiselect({
|
|
51
|
+
message: "Select containers to stop:",
|
|
52
|
+
options: running.map((name) => ({ value: name, label: name })),
|
|
53
|
+
required: false,
|
|
54
|
+
});
|
|
55
|
+
if (isCancel(selected)) {
|
|
56
|
+
cancel();
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
toStop = selected as string[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const name of toStop) {
|
|
63
|
+
log.step(`Stopping ${name}...`);
|
|
64
|
+
stopAndRemoveContainer(name);
|
|
65
|
+
}
|
|
66
|
+
outro("Done.");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ─── B: Remove images ─────────────────────────────────────────────────────────
|
|
70
|
+
else if (action === "remove-images") {
|
|
71
|
+
const listResult = spawnSync("docker", ["images", "--filter", "label=totopo.managed=true", "--format", "{{.Repository}}\t{{.ID}}"], {
|
|
72
|
+
encoding: "utf8",
|
|
73
|
+
});
|
|
74
|
+
const lines = (listResult.stdout ?? "").trim().split("\n").filter(Boolean);
|
|
75
|
+
|
|
76
|
+
if (lines.length === 0) {
|
|
77
|
+
log.info("No images found.");
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const images = lines.map((line) => {
|
|
82
|
+
const [repo, id] = line.split("\t");
|
|
83
|
+
const workspace = (repo ?? "").replace(/^totopo-managed-/, "");
|
|
84
|
+
return { repo: repo ?? "", id: id ?? "", workspace };
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const selected = await multiselect({
|
|
88
|
+
message: "Select images to remove:",
|
|
89
|
+
options: images.map((img) => ({
|
|
90
|
+
value: img.repo,
|
|
91
|
+
label: `${img.workspace} (${img.repo})`,
|
|
92
|
+
})),
|
|
93
|
+
required: false,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (isCancel(selected)) {
|
|
97
|
+
cancel();
|
|
98
|
+
process.exit(0);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const toRemove = selected as string[];
|
|
102
|
+
|
|
103
|
+
for (const repo of toRemove) {
|
|
104
|
+
// Stop any running container that uses this image
|
|
105
|
+
const psResult = spawnSync("docker", ["ps", "--filter", `name=${repo}`, "--format", "{{.Names}}"], { encoding: "utf8" });
|
|
106
|
+
const containers = (psResult.stdout ?? "").trim().split("\n").filter(Boolean);
|
|
107
|
+
for (const c of containers) {
|
|
108
|
+
log.step(`Stopping container ${c} before removing image...`);
|
|
109
|
+
stopAndRemoveContainer(c);
|
|
110
|
+
}
|
|
111
|
+
log.step(`Removing image ${repo}...`);
|
|
112
|
+
spawnSync("docker", ["rmi", repo], { stdio: "inherit" });
|
|
113
|
+
}
|
|
114
|
+
outro("Done.");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─── C: Uninstall from this project ───────────────────────────────────────────
|
|
118
|
+
else if (action === "uninstall") {
|
|
119
|
+
const repoRoot = process.env.TOTOPO_REPO_ROOT ?? process.cwd();
|
|
120
|
+
const containerName = `totopo-managed-${projectName}`;
|
|
121
|
+
const imageName = `totopo-managed-${projectName}`;
|
|
122
|
+
|
|
123
|
+
const confirmed = await confirm({
|
|
124
|
+
message: `Remove .totopo/, stop containers, and delete the image for ${projectName}?`,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (isCancel(confirmed) || !confirmed) {
|
|
128
|
+
cancel();
|
|
129
|
+
process.exit(0);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Stop container if running
|
|
133
|
+
const inspectResult = spawnSync("docker", ["inspect", "--type", "container", containerName], { encoding: "utf8" });
|
|
134
|
+
if (inspectResult.status === 0) {
|
|
135
|
+
log.step(`Stopping container ${containerName}...`);
|
|
136
|
+
stopAndRemoveContainer(containerName);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Remove image if exists
|
|
140
|
+
const imageResult = spawnSync("docker", ["images", "-q", imageName], { encoding: "utf8" });
|
|
141
|
+
if ((imageResult.stdout ?? "").trim().length > 0) {
|
|
142
|
+
log.step(`Removing image ${imageName}...`);
|
|
143
|
+
spawnSync("docker", ["rmi", imageName], { stdio: "inherit" });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Delete .totopo/
|
|
147
|
+
log.step("Removing .totopo/...");
|
|
148
|
+
rmSync(join(repoRoot, ".totopo"), { recursive: true, force: true });
|
|
149
|
+
|
|
150
|
+
outro("Uninstalled. Re-run npx totopo to set up again.");
|
|
151
|
+
}
|
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
|
|
7
7
|
import { box, cancel, isCancel, select } from "@clack/prompts";
|
|
8
8
|
|
|
9
|
-
// Parse CLI args passed by
|
|
10
|
-
const [projectName = "unknown", activeCountStr, hasKeyStr] = process.argv.slice(2);
|
|
9
|
+
// Parse CLI args passed by bin/totopo.js: project name, active container count, API key presence, project state
|
|
10
|
+
const [projectName = "unknown", activeCountStr, hasKeyStr, projectRunningStr, projectImageExistsStr] = process.argv.slice(2);
|
|
11
11
|
const activeCount = Number.parseInt(activeCountStr ?? "0", 10);
|
|
12
12
|
const hasKey = hasKeyStr === "true";
|
|
13
|
+
const projectRunning = projectRunningStr === "true";
|
|
14
|
+
const projectImageExists = projectImageExistsStr === "true";
|
|
13
15
|
|
|
14
16
|
// ─── Status box ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
15
17
|
const sessionLabel = activeCount === 1 ? "1 container running" : `${activeCount} containers running`;
|
|
@@ -28,8 +30,9 @@ const action = await select({
|
|
|
28
30
|
message: "Menu:",
|
|
29
31
|
options: [
|
|
30
32
|
{ value: "dev", label: "Start session" },
|
|
31
|
-
{ value: "stop", label: "Stop
|
|
32
|
-
{ value: "
|
|
33
|
+
...(projectRunning ? [{ value: "stop", label: "Stop" }] : []),
|
|
34
|
+
...(projectImageExists ? [{ value: "rebuild", label: "Rebuild" }] : []),
|
|
35
|
+
{ value: "manage", label: "Manage workspaces" },
|
|
33
36
|
{ value: "doctor", label: "Doctor" },
|
|
34
37
|
{ value: "settings", label: "Settings" },
|
|
35
38
|
{ value: "quit", label: "Quit" },
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// =========================================================================================================================================
|
|
3
|
+
// src/core/commands/rebuild.ts — Stop this project's container and remove its image to force a fresh build
|
|
4
|
+
// Invoked by bin/totopo.js — do not run directly.
|
|
5
|
+
// =========================================================================================================================================
|
|
6
|
+
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { log } from "@clack/prompts";
|
|
9
|
+
|
|
10
|
+
const [projectName = "unknown"] = process.argv.slice(2);
|
|
11
|
+
const containerName = `totopo-managed-${projectName}`;
|
|
12
|
+
const imageName = `totopo-managed-${projectName}`;
|
|
13
|
+
|
|
14
|
+
// ─── Stop container if running ────────────────────────────────────────────────
|
|
15
|
+
const inspectResult = spawnSync("docker", ["inspect", "--type", "container", containerName], { encoding: "utf8" });
|
|
16
|
+
if (inspectResult.status === 0) {
|
|
17
|
+
log.step(`Stopping container ${containerName}...`);
|
|
18
|
+
spawnSync("docker", ["stop", containerName], { stdio: "inherit" });
|
|
19
|
+
spawnSync("docker", ["rm", containerName], { stdio: "inherit" });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ─── Remove image ─────────────────────────────────────────────────────────────
|
|
23
|
+
const imageResult = spawnSync("docker", ["images", "-q", imageName], { encoding: "utf8" });
|
|
24
|
+
if ((imageResult.stdout ?? "").trim().length > 0) {
|
|
25
|
+
log.step(`Removing image ${imageName}...`);
|
|
26
|
+
spawnSync("docker", ["rmi", imageName], { stdio: "inherit" });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
log.info("Image removed — starting fresh build…");
|
|
@@ -1,29 +1,34 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// =========================================================================================================================================
|
|
3
|
-
// src/core/commands/stop.ts — Stop and remove
|
|
3
|
+
// src/core/commands/stop.ts — Stop and remove THIS project's dev container
|
|
4
4
|
// Invoked by bin/totopo.js — do not run directly.
|
|
5
5
|
// =========================================================================================================================================
|
|
6
6
|
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
8
|
-
import { log, outro } from "@clack/prompts";
|
|
8
|
+
import { cancel, confirm, isCancel, log, outro } from "@clack/prompts";
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
const
|
|
10
|
+
const [projectName = "unknown"] = process.argv.slice(2);
|
|
11
|
+
const containerName = `totopo-managed-${projectName}`;
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
// ─── Check if container exists ────────────────────────────────────────────────
|
|
14
|
+
const inspectResult = spawnSync("docker", ["inspect", "--type", "container", containerName], { encoding: "utf8" });
|
|
14
15
|
|
|
15
|
-
if (
|
|
16
|
-
log.info(
|
|
16
|
+
if (inspectResult.status !== 0) {
|
|
17
|
+
log.info(`Container ${containerName} is not running.`);
|
|
17
18
|
process.exit(0);
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
// ───
|
|
21
|
-
|
|
21
|
+
// ─── Confirm ─────────────────────────────────────────────────────────────────
|
|
22
|
+
const confirmed = await confirm({ message: `Stop ${containerName}?` });
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
spawnSync("docker", ["rm", name], { stdio: "inherit" });
|
|
24
|
+
if (isCancel(confirmed) || !confirmed) {
|
|
25
|
+
cancel();
|
|
26
|
+
process.exit(0);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
// ─── Stop and remove ─────────────────────────────────────────────────────────
|
|
30
|
+
log.step(`Stopping ${containerName}...`);
|
|
31
|
+
spawnSync("docker", ["stop", containerName], { stdio: "inherit" });
|
|
32
|
+
spawnSync("docker", ["rm", containerName], { stdio: "inherit" });
|
|
33
|
+
|
|
34
|
+
outro(`${containerName} stopped and removed.`);
|
|
@@ -69,7 +69,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
|
69
69
|
# Build essentials (needed for Rust compilation, C extensions, etc.)
|
|
70
70
|
build-essential pkg-config libssl-dev \
|
|
71
71
|
# Utilities
|
|
72
|
-
jq unzip zip tree htop procps lsb-release gnupg ca-certificates \
|
|
72
|
+
jq unzip zip tree htop procps lsb-release gnupg ca-certificates sox \
|
|
73
73
|
# Modern search/navigation tools
|
|
74
74
|
ripgrep fzf \
|
|
75
75
|
# Database clients
|
package/templates/Dockerfile
CHANGED
|
@@ -17,7 +17,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
|
17
17
|
# Build essentials (needed for Rust compilation, C extensions, etc.)
|
|
18
18
|
build-essential pkg-config libssl-dev \
|
|
19
19
|
# Utilities
|
|
20
|
-
jq unzip zip tree htop procps lsb-release gnupg ca-certificates \
|
|
20
|
+
jq unzip zip tree htop procps lsb-release gnupg ca-certificates sox \
|
|
21
21
|
# Modern search/navigation tools
|
|
22
22
|
ripgrep fzf \
|
|
23
23
|
# Database clients
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// =========================================================================================================================================
|
|
3
|
-
// src/core/commands/reset.ts — Full reset: delete all totopo containers and Docker images
|
|
4
|
-
// Invoked by bin/totopo.js — do not run directly.
|
|
5
|
-
// Run 'npx totopo' → Start session after this to get a fresh build.
|
|
6
|
-
// =========================================================================================================================================
|
|
7
|
-
|
|
8
|
-
import { spawnSync } from "node:child_process";
|
|
9
|
-
import { log, outro } from "@clack/prompts";
|
|
10
|
-
|
|
11
|
-
// ─── Step 1: Find all totopo-managed-* containers ────────────────────────────
|
|
12
|
-
const listResult = spawnSync("docker", ["ps", "-a", "--filter", "name=totopo-managed-", "--format", "{{.Names}}"], { encoding: "utf8" });
|
|
13
|
-
|
|
14
|
-
const containers = (listResult.stdout ?? "").trim().split("\n").filter(Boolean);
|
|
15
|
-
|
|
16
|
-
// ─── Step 2: Stop and remove all totopo containers ───────────────────────────
|
|
17
|
-
if (containers.length === 0) {
|
|
18
|
-
log.info("No totopo containers found.");
|
|
19
|
-
} else {
|
|
20
|
-
log.step(`Stopping and removing ${containers.length} container(s)...`);
|
|
21
|
-
for (const name of containers) {
|
|
22
|
-
log.step(` Removing ${name}...`);
|
|
23
|
-
spawnSync("docker", ["stop", name], { stdio: "inherit" });
|
|
24
|
-
spawnSync("docker", ["rm", name], { stdio: "inherit" });
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// ─── Step 3: Remove cached Docker images ─────────────────────────────────────
|
|
29
|
-
// Images are identified via the LABEL totopo.managed=true baked into the
|
|
30
|
-
// Dockerfile template — works regardless of whether containers still exist.
|
|
31
|
-
log.step("Removing cached Docker images...");
|
|
32
|
-
|
|
33
|
-
const findImages = spawnSync("docker", ["images", "--filter", "label=totopo.managed=true", "--format", "{{.ID}}"], { encoding: "utf8" });
|
|
34
|
-
const imageIds = (findImages.stdout ?? "").trim().split("\n").filter(Boolean);
|
|
35
|
-
|
|
36
|
-
if (imageIds.length > 0) {
|
|
37
|
-
log.info(` Found ${imageIds.length} image(s) — removing...`);
|
|
38
|
-
spawnSync("docker", ["rmi", "--force", ...imageIds], { stdio: "inherit" });
|
|
39
|
-
} else {
|
|
40
|
-
log.info(" No cached images found.");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
spawnSync("docker", ["image", "prune", "--force"], { stdio: "inherit" });
|
|
44
|
-
|
|
45
|
-
// ─── Done ────────────────────────────────────────────────────────────────────
|
|
46
|
-
outro("Reset complete. Run 'npx totopo' and select 'Start session' to start fresh.");
|