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 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
- console.log(" Installing totopo dependencies...");
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
- const menuResult = spawnSync(tsx, [join(packageDir, "src/core/commands/menu.ts"), projectName, String(activeCount), String(hasKey)], {
106
- stdio: ["inherit", ttyFd, "pipe"],
107
- encoding: "utf8",
108
- });
109
- const action = (menuResult.stderr ?? "").trim();
110
-
111
- // ─── Execute selection ────────────────────────────────────────────────────────
112
- switch (action) {
113
- case "dev":
114
- run("dev.ts");
115
- break;
116
- case "stop":
117
- run("stop.ts");
118
- break;
119
- case "reset":
120
- run("reset.ts");
121
- break;
122
- case "doctor":
123
- run("doctor.ts", ["--verbose"]);
124
- break;
125
- case "settings":
126
- run("settings.ts");
127
- break;
128
- default:
129
- break; // quit or cancelled
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "totopo",
3
- "version": "0.6.1",
3
+ "version": "0.7.0-rc-2",
4
4
  "description": "Secure AI Box — isolated dev environments for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 ai.sh: project name, active container count, and API key presence flag
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 all" },
32
- { value: "reset", label: "Reset (wipe workspaces + images)" },
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 all totopo dev containers
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
- // ─── Find all totopo-managed-* containers ────────────────────────────────────
11
- const listResult = spawnSync("docker", ["ps", "-a", "--filter", "name=totopo-managed-", "--format", "{{.Names}}"], { encoding: "utf8" });
10
+ const [projectName = "unknown"] = process.argv.slice(2);
11
+ const containerName = `totopo-managed-${projectName}`;
12
12
 
13
- const containers = (listResult.stdout ?? "").trim().split("\n").filter(Boolean);
13
+ // ─── Check if container exists ────────────────────────────────────────────────
14
+ const inspectResult = spawnSync("docker", ["inspect", "--type", "container", containerName], { encoding: "utf8" });
14
15
 
15
- if (containers.length === 0) {
16
- log.info("No totopo containers found.");
16
+ if (inspectResult.status !== 0) {
17
+ log.info(`Container ${containerName} is not running.`);
17
18
  process.exit(0);
18
19
  }
19
20
 
20
- // ─── Stop and remove each container ──────────────────────────────────────────
21
- log.step("Stopping all totopo containers...");
21
+ // ─── Confirm ─────────────────────────────────────────────────────────────────
22
+ const confirmed = await confirm({ message: `Stop ${containerName}?` });
22
23
 
23
- for (const name of containers) {
24
- log.step(`Stopping ${name}...`);
25
- spawnSync("docker", ["stop", name], { stdio: "inherit" });
26
- spawnSync("docker", ["rm", name], { stdio: "inherit" });
24
+ if (isCancel(confirmed) || !confirmed) {
25
+ cancel();
26
+ process.exit(0);
27
27
  }
28
28
 
29
- outro("All totopo containers stopped and removed.");
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
@@ -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.");