totopo 0.1.4 → 0.1.5

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/ai.sh CHANGED
@@ -79,9 +79,9 @@ fi
79
79
 
80
80
  # ─── Gather state for menu ──────────────────────────────────────────────────
81
81
  PROJECT_NAME="$(basename "$REPO_ROOT")"
82
- WORKSPACE_NAME="totopo-$PROJECT_NAME"
82
+ WORKSPACE_NAME="totopo-managed-$PROJECT_NAME"
83
83
 
84
- ACTIVE_COUNT=$(docker ps --filter "name=totopo-" --format "{{.Names}}" 2>/dev/null | wc -l | tr -d '[:space:]')
84
+ ACTIVE_COUNT=$(docker ps --filter "name=totopo-managed-" --format "{{.Names}}" 2>/dev/null | wc -l | tr -d '[:space:]')
85
85
 
86
86
  HAS_KEY=false
87
87
  if [ -f "$REPO_ROOT/.totopo/.env" ]; then
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "totopo",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Secure AI Box — isolated dev environments for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {
package/src/core/dev.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- // =============================================================================
2
+ // =========================================================================================================================================
3
3
  // scripts/dev.ts — Start the dev container and SSH in
4
4
  // Called by ai.sh — do not run directly.
5
- // =============================================================================
5
+ // =========================================================================================================================================
6
6
 
7
7
  import { spawnSync } from "node:child_process";
8
8
  import { basename } from "node:path";
@@ -10,40 +10,28 @@ import { log, outro } from "@clack/prompts";
10
10
 
11
11
  const workspaceDir = process.env.TOTOPO_REPO_ROOT;
12
12
  if (!workspaceDir) {
13
- log.error("TOTOPO_REPO_ROOT not set — run via ai.sh");
14
- process.exit(1);
13
+ log.error("TOTOPO_REPO_ROOT not set — run via ai.sh");
14
+ process.exit(1);
15
15
  }
16
16
 
17
- const workspaceName = `totopo-${basename(workspaceDir)}`;
17
+ // Derive a stable workspace ID — "totopo-managed-" prefix lets reset/stop scripts identify managed workspaces
18
+ const workspaceName = `totopo-managed-${basename(workspaceDir)}`;
18
19
 
19
20
  // Always run devpod up — it's idempotent (starts if stopped, no-op if running)
20
21
  log.step("Starting dev container...");
21
22
  const up = spawnSync(
22
- "devpod",
23
- [
24
- "up",
25
- workspaceDir,
26
- "--devcontainer-path",
27
- ".totopo/devcontainer.json",
28
- "--ide",
29
- "none",
30
- "--id",
31
- workspaceName,
32
- ],
33
- { stdio: "inherit" },
23
+ "devpod",
24
+ ["up", workspaceDir, "--devcontainer-path", ".totopo/devcontainer.json", "--ide", "none", "--id", workspaceName],
25
+ { stdio: "inherit" },
34
26
  );
35
27
  if (up.status !== 0) {
36
- outro("Failed to start dev container.");
37
- process.exit(up.status ?? 1);
28
+ outro("Failed to start dev container.");
29
+ process.exit(up.status ?? 1);
38
30
  }
39
31
  log.step("Connecting via SSH...");
40
32
 
41
- const ssh = spawnSync(
42
- "devpod",
43
- ["ssh", workspaceName, "--workdir", "/workspace"],
44
- {
45
- stdio: "inherit",
46
- },
47
- );
33
+ const ssh = spawnSync("devpod", ["ssh", workspaceName, "--workdir", "/workspace"], {
34
+ stdio: "inherit",
35
+ });
48
36
 
49
37
  process.exit(ssh.status ?? 0);
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- // =============================================================================
2
+ // =========================================================================================================================================
3
3
  // scripts/doctor.ts — Host readiness check for totopo
4
4
  // Runs silently on success; exits non-zero on failure.
5
5
  // Pass --verbose for a full report.
6
- // =============================================================================
6
+ // =========================================================================================================================================
7
7
 
8
8
  import { spawnSync } from "node:child_process";
9
9
  import { existsSync } from "node:fs";
@@ -12,109 +12,84 @@ import { log, outro } from "@clack/prompts";
12
12
  const verbose = process.argv.includes("--verbose");
13
13
  const repoRoot = process.env.TOTOPO_REPO_ROOT;
14
14
  if (!repoRoot) {
15
- log.error("TOTOPO_REPO_ROOT not set — run via ai.sh");
16
- process.exit(1);
15
+ log.error("TOTOPO_REPO_ROOT not set — run via ai.sh");
16
+ process.exit(1);
17
17
  }
18
18
 
19
19
  const errors: string[] = [];
20
20
 
21
+ // Logs the result of a single health check; accumulates failures into the errors array for the final report
21
22
  function check(label: string, ok: boolean, detail?: string): void {
22
- if (ok) {
23
- if (verbose)
24
- log.success(`${label}${detail ? ` \x1b[2m${detail}\x1b[0m` : ""}`);
25
- } else {
26
- errors.push(`${label}${detail ? `: ${detail}` : ""}`);
27
- if (verbose) log.error(`${label}${detail ? ` ${detail}` : ""}`);
28
- }
23
+ if (ok) {
24
+ if (verbose) log.success(`${label}${detail ? ` \x1b[2m${detail}\x1b[0m` : ""}`);
25
+ } else {
26
+ errors.push(`${label}${detail ? `: ${detail}` : ""}`);
27
+ if (verbose) log.error(`${label}${detail ? ` ${detail}` : ""}`);
28
+ }
29
29
  }
30
30
 
31
+ // Returns true if the given CLI tool is resolvable in the system PATH
31
32
  function commandExists(cmd: string): boolean {
32
- const r = spawnSync("command", ["-v", cmd], {
33
- shell: true,
34
- encoding: "utf8",
35
- });
36
- return r.status === 0;
33
+ const r = spawnSync("command", ["-v", cmd], {
34
+ shell: true,
35
+ encoding: "utf8",
36
+ });
37
+ return r.status === 0;
37
38
  }
38
39
 
39
40
  if (verbose) console.log("");
40
41
 
41
- // --- Docker installed ---
42
- check(
43
- "Docker installed",
44
- commandExists("docker"),
45
- commandExists("docker") ? undefined : "'docker' not found in PATH",
46
- );
42
+ // ─── Docker installed ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
43
+ check("Docker installed", commandExists("docker"), commandExists("docker") ? undefined : "'docker' not found in PATH");
47
44
 
48
- // --- Docker running ---
45
+ // ─── Docker running ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
49
46
  const dockerInfo = spawnSync("docker", ["info"], {
50
- encoding: "utf8",
51
- stdio: "pipe",
47
+ encoding: "utf8",
48
+ stdio: "pipe",
52
49
  });
53
- check(
54
- "Docker running",
55
- dockerInfo.status === 0,
56
- dockerInfo.status === 0 ? undefined : "Docker daemon not responding",
57
- );
50
+ check("Docker running", dockerInfo.status === 0, dockerInfo.status === 0 ? undefined : "Docker daemon not responding");
58
51
 
59
- // --- DevPod installed ---
52
+ // ─── DevPod installed ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
60
53
  const devpodInstalled = commandExists("devpod");
61
- check(
62
- "DevPod installed",
63
- devpodInstalled,
64
- devpodInstalled ? undefined : "'devpod' not found in PATH",
65
- );
54
+ check("DevPod installed", devpodInstalled, devpodInstalled ? undefined : "'devpod' not found in PATH");
66
55
 
67
- // --- DevPod provider configured ---
56
+ // ─── DevPod provider configured ──────────────────────────────────────────────────────────────────────────────────────────────────────────
68
57
  if (devpodInstalled) {
69
- let providerOk = false;
70
- for (let attempt = 1; attempt <= 5; attempt++) {
71
- const r = spawnSync("devpod", ["provider", "list"], {
72
- encoding: "utf8",
73
- stdio: "pipe",
74
- });
75
- if (r.stdout?.toLowerCase().includes("docker")) {
76
- providerOk = true;
77
- break;
78
- }
79
- if (attempt < 5) {
80
- spawnSync("sleep", ["1"]);
81
- }
82
- }
83
- check(
84
- "DevPod provider configured",
85
- providerOk,
86
- providerOk
87
- ? undefined
88
- : "no provider found — run: devpod provider add docker",
89
- );
58
+ let providerOk = false;
59
+ for (let attempt = 1; attempt <= 5; attempt++) {
60
+ const r = spawnSync("devpod", ["provider", "list"], {
61
+ encoding: "utf8",
62
+ stdio: "pipe",
63
+ });
64
+ if (r.stdout?.toLowerCase().includes("docker")) {
65
+ providerOk = true;
66
+ break;
67
+ }
68
+ if (attempt < 5) {
69
+ spawnSync("sleep", ["1"]);
70
+ }
71
+ }
72
+ check("DevPod provider configured", providerOk, providerOk ? undefined : "no provider found — run: devpod provider add docker");
90
73
  }
91
74
 
92
- // --- .totopo/ config present ---
93
- const configOk =
94
- existsSync(`${repoRoot}/.totopo/devcontainer.json`) &&
95
- existsSync(`${repoRoot}/.totopo/Dockerfile`);
96
- check(
97
- ".totopo/ config present",
98
- configOk,
99
- configOk
100
- ? undefined
101
- : "missing .totopo/devcontainer.json or .totopo/Dockerfile",
102
- );
75
+ // ─── .totopo/ config present ─────────────────────────────────────────────────────────────────────────────────────────────────────────────
76
+ const configOk = existsSync(`${repoRoot}/.totopo/devcontainer.json`) && existsSync(`${repoRoot}/.totopo/Dockerfile`);
77
+ check(".totopo/ config present", configOk, configOk ? undefined : "missing .totopo/devcontainer.json or .totopo/Dockerfile");
103
78
 
104
- // --- Report ---
79
+ // ─── Report ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
105
80
  if (errors.length > 0) {
106
- if (verbose) {
107
- console.log("");
108
- log.error("totopo doctor found problems:");
109
- for (const err of errors) {
110
- console.log(` \x1b[2m•\x1b[0m ${err}`);
111
- }
112
- console.log("");
113
- }
114
- process.exit(1);
81
+ if (verbose) {
82
+ console.log("");
83
+ log.error("totopo doctor found problems:");
84
+ for (const err of errors) {
85
+ console.log(` \x1b[2m•\x1b[0m ${err}`);
86
+ }
87
+ console.log("");
88
+ }
89
+ process.exit(1);
115
90
  }
116
91
 
117
92
  if (verbose) {
118
- console.log("");
119
- outro("All checks passed.");
93
+ console.log("");
94
+ outro("All checks passed.");
120
95
  }
package/src/core/menu.ts CHANGED
@@ -1,46 +1,43 @@
1
1
  #!/usr/bin/env node
2
- // =============================================================================
2
+ // =========================================================================================================================================
3
3
  // scripts/menu.ts — totopo interactive menu (powered by @clack/prompts)
4
4
  // Called by ai.sh — outputs selected action to stderr.
5
- // =============================================================================
5
+ // =========================================================================================================================================
6
6
 
7
7
  import { box, cancel, isCancel, select } from "@clack/prompts";
8
8
 
9
- const [projectName = "unknown", activeCountStr, hasKeyStr] =
10
- process.argv.slice(2);
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);
11
11
  const activeCount = Number.parseInt(activeCountStr ?? "0", 10);
12
12
  const hasKey = hasKeyStr === "true";
13
13
 
14
- // ─── Status box ──────────────────────────────────────────────────────────────
15
- const sessionLabel =
16
- activeCount === 1
17
- ? "1 container running"
18
- : `${activeCount} containers running`;
14
+ // ─── Status box ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
15
+ const sessionLabel = activeCount === 1 ? "1 container running" : `${activeCount} containers running`;
19
16
  const lines = [];
20
17
  lines.push(`status: ${sessionLabel}`);
21
18
  lines.push(`api keys: ${hasKey ? "configured" : "none"} (.totopo/.env)`);
22
19
  box(lines.join("\n"), ` totopo · ${projectName} `, {
23
- contentAlign: "center",
24
- titleAlign: "center",
25
- width: "auto",
26
- rounded: true,
20
+ contentAlign: "center",
21
+ titleAlign: "center",
22
+ width: "auto",
23
+ rounded: true,
27
24
  });
28
25
 
29
- // ─── Menu ───────────────────────────────────────────────────────────────────
26
+ // ─── Menu ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
30
27
  const action = await select({
31
- message: "Menu:",
32
- options: [
33
- { value: "dev", label: "Start session" },
34
- { value: "stop", label: "Stop all" },
35
- { value: "reset", label: "Reset (wipe workspaces + images)" },
36
- { value: "doctor", label: "Doctor" },
37
- { value: "quit", label: "Quit" },
38
- ],
28
+ message: "Menu:",
29
+ options: [
30
+ { value: "dev", label: "Start session" },
31
+ { value: "stop", label: "Stop all" },
32
+ { value: "reset", label: "Reset (wipe workspaces + images)" },
33
+ { value: "doctor", label: "Doctor" },
34
+ { value: "quit", label: "Quit" },
35
+ ],
39
36
  });
40
37
 
41
38
  if (isCancel(action)) {
42
- cancel();
43
- process.exit(0);
39
+ cancel();
40
+ process.exit(0);
44
41
  }
45
42
 
46
43
  // Output action to stderr — ai.sh captures it via redirection
@@ -1,104 +1,74 @@
1
1
  #!/usr/bin/env node
2
- // =============================================================================
2
+ // =========================================================================================================================================
3
3
  // scripts/onboard.ts — First-time setup for a project using totopo
4
4
  // Called by ai.sh when no .totopo/ config is found in the project.
5
- // =============================================================================
6
-
7
- import {
8
- cpSync,
9
- existsSync,
10
- mkdirSync,
11
- readFileSync,
12
- writeFileSync,
13
- } from "node:fs";
5
+ // =========================================================================================================================================
6
+
7
+ import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
14
8
  import { basename, join } from "node:path";
15
- import {
16
- box,
17
- cancel,
18
- confirm,
19
- intro,
20
- isCancel,
21
- log,
22
- outro,
23
- } from "@clack/prompts";
9
+ import { box, cancel, confirm, intro, isCancel, log, outro } from "@clack/prompts";
24
10
 
25
11
  const packageDir = process.env.TOTOPO_PACKAGE_DIR;
26
12
  const repoRoot = process.env.TOTOPO_REPO_ROOT;
27
13
 
28
14
  if (!packageDir || !repoRoot) {
29
- log.error("TOTOPO_PACKAGE_DIR / TOTOPO_REPO_ROOT not set — run via ai.sh");
30
- process.exit(1);
15
+ log.error("TOTOPO_PACKAGE_DIR / TOTOPO_REPO_ROOT not set — run via ai.sh");
16
+ process.exit(1);
31
17
  }
32
18
 
33
19
  const templatesDir = join(packageDir, "templates");
34
20
  const totopoDir = join(repoRoot, ".totopo");
35
21
  const projectName = basename(repoRoot);
36
22
 
37
- // ─── Intro ────────────────────────────────────────────────────────────────────
23
+ // ─── Intro ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
38
24
  intro("totopo — First-time setup");
39
25
 
40
- box(
41
- `project : ${projectName}\nlocation : ${totopoDir}`,
42
- "No .totopo/ config found — totopo will create it now.",
43
- {
44
- contentAlign: "center",
45
- titleAlign: "center",
46
- width: "auto",
47
- rounded: true,
48
- },
49
- );
26
+ box(`project : ${projectName}\nlocation : ${totopoDir}`, "No .totopo/ config found — totopo will create it now.", {
27
+ contentAlign: "center",
28
+ titleAlign: "center",
29
+ width: "auto",
30
+ rounded: true,
31
+ });
50
32
 
51
33
  const ok = await confirm({ message: "Continue?" });
52
34
 
53
35
  if (isCancel(ok) || !ok) {
54
- cancel("Setup cancelled.");
55
- process.exit(0);
36
+ cancel("Setup cancelled.");
37
+ process.exit(0);
56
38
  }
57
39
 
58
- // ─── Copy templates ───────────────────────────────────────────────────────────
40
+ // ─── Copy templates ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
59
41
  mkdirSync(totopoDir, { recursive: true });
60
42
 
61
43
  cpSync(join(templatesDir, "Dockerfile"), join(totopoDir, "Dockerfile"));
62
44
  cpSync(join(templatesDir, "post-start.mjs"), join(totopoDir, "post-start.mjs"));
63
45
 
64
46
  // Substitute project name in devcontainer.json (plain string replace — file has // comments)
65
- const dcTemplate = readFileSync(
66
- join(templatesDir, "devcontainer.json"),
67
- "utf8",
68
- );
69
- writeFileSync(
70
- join(totopoDir, "devcontainer.json"),
71
- dcTemplate.replace(/TOTOPO_PROJECT_NAME/g, projectName),
72
- );
47
+ const dcTemplate = readFileSync(join(templatesDir, "devcontainer.json"), "utf8");
48
+ writeFileSync(join(totopoDir, "devcontainer.json"), dcTemplate.replace(/TOTOPO_PROJECT_NAME/g, projectName));
73
49
 
74
50
  log.success("Copied config templates to .totopo/");
75
51
 
76
- // ─── Create .env ──────────────────────────────────────────────────────────────
52
+ // ─── Create .env ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
77
53
  const envPath = join(totopoDir, ".env");
78
54
  if (existsSync(envPath)) {
79
- log.info(".totopo/.env already exists — leaving it untouched");
55
+ log.info(".totopo/.env already exists — leaving it untouched");
80
56
  } else {
81
- cpSync(join(templatesDir, "env"), envPath);
82
- log.success("Created .totopo/.env");
57
+ cpSync(join(templatesDir, "env"), envPath);
58
+ log.success("Created .totopo/.env");
83
59
  }
84
60
 
85
- // ─── Ensure .totopo/.env is gitignored ─────────────────────────────────────────
61
+ // ─── Ensure .totopo/.env is gitignored ───────────────────────────────────────────────────────────────────────────────────────────────────
86
62
  const gitignorePath = join(repoRoot, ".gitignore");
87
63
  const gitignoreEntry = ".totopo/.env";
88
64
 
89
- if (
90
- existsSync(gitignorePath) &&
91
- readFileSync(gitignorePath, "utf8").includes(gitignoreEntry)
92
- ) {
93
- log.info(".totopo/.env already in .gitignore");
65
+ if (existsSync(gitignorePath) && readFileSync(gitignorePath, "utf8").includes(gitignoreEntry)) {
66
+ log.info(".totopo/.env already in .gitignore");
94
67
  } else {
95
- const addition =
96
- "\n# totopo API keys must never be committed\n.totopo/.env\n";
97
- const existing = existsSync(gitignorePath)
98
- ? readFileSync(gitignorePath, "utf8")
99
- : "";
100
- writeFileSync(gitignorePath, existing + addition);
101
- log.success("Added .totopo/.env to .gitignore");
68
+ const addition = "\n# totopo — API keys must never be committed\n.totopo/.env\n";
69
+ const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf8") : "";
70
+ writeFileSync(gitignorePath, existing + addition);
71
+ log.success("Added .totopo/.env to .gitignore");
102
72
  }
103
73
 
104
74
  log.warn("Add your API keys to .totopo/.env before starting the container.");
package/src/core/reset.ts CHANGED
@@ -1,70 +1,54 @@
1
1
  #!/usr/bin/env node
2
- // =============================================================================
2
+ // =========================================================================================================================================
3
3
  // scripts/reset.ts — Full reset: delete all totopo workspaces and Docker images
4
4
  // Called by ai.sh — do not run directly.
5
5
  // Run 'ai.sh' → Start session after this to get a fresh build.
6
- // =============================================================================
6
+ // =========================================================================================================================================
7
7
 
8
8
  import { spawnSync } from "node:child_process";
9
9
  import { log, outro } from "@clack/prompts";
10
10
 
11
- // ─── Step 1: Find all totopo-* DevPod workspaces ───────────────────────────────
11
+ // ─── Step 1: Find all totopo-managed-* DevPod workspaces ─────────────────────────────────────────────────────────────────────────────────
12
12
  const listResult = spawnSync("devpod", ["list", "--output", "json"], {
13
- encoding: "utf8",
13
+ encoding: "utf8",
14
14
  });
15
15
 
16
16
  const workspaces: string[] = [];
17
17
  if (listResult.stdout) {
18
- const matches = listResult.stdout.matchAll(/"id":"(totopo-[^"]+)"/g);
19
- for (const match of matches) {
20
- if (match[1]) workspaces.push(match[1]);
21
- }
18
+ const matches = listResult.stdout.matchAll(/"id":"(totopo-managed-[^"]+)"/g);
19
+ for (const match of matches) {
20
+ if (match[1]) workspaces.push(match[1]);
21
+ }
22
22
  }
23
23
 
24
- // ─── Step 2: Stop and delete all totopo workspaces ─────────────────────────────
24
+ // ─── Step 2: Stop and delete all totopo workspaces ───────────────────────────────────────────────────────────────────────────────────────
25
25
  if (workspaces.length === 0) {
26
- log.info("No totopo workspaces found.");
26
+ log.info("No totopo workspaces found.");
27
27
  } else {
28
- log.step(`Stopping and deleting ${workspaces.length} workspace(s)...`);
29
- for (const ws of workspaces) {
30
- log.step(` Removing ${ws}...`);
31
- spawnSync("devpod", ["stop", ws], { stdio: "inherit" });
32
- spawnSync("devpod", ["delete", ws, "--force"], { stdio: "inherit" });
33
- }
28
+ log.step(`Stopping and deleting ${workspaces.length} workspace(s)...`);
29
+ for (const ws of workspaces) {
30
+ log.step(` Removing ${ws}...`);
31
+ spawnSync("devpod", ["stop", ws], { stdio: "inherit" });
32
+ spawnSync("devpod", ["delete", ws, "--force"], { stdio: "inherit" });
33
+ }
34
34
  }
35
35
 
36
- // ─── Step 3: Remove cached Docker images ─────────────────────────────────────
37
- // DevPod images are named vsc-<project>-<hash> (based on the folder name, not
38
- // the workspace --id). For each totopo-<project> workspace, strip the "totopo-"
39
- // prefix to get the image reference filter.
36
+ // ─── Step 3: Remove cached Docker images ─────────────────────────────────────────────────────────────────────────────────────────────────
37
+ // Images are identified via the LABEL totopo.managed=true baked into the
38
+ // Dockerfile template works regardless of whether workspaces still exist.
40
39
  log.step("Removing cached Docker images...");
41
40
 
42
- const allImageIds = new Set<string>();
43
- for (const ws of workspaces) {
44
- const projectName = ws.replace(/^totopo-/, "");
45
- const findImages = spawnSync(
46
- "docker",
47
- [
48
- "images",
49
- "--filter",
50
- `reference=vsc-${projectName}-*`,
51
- "--format",
52
- "{{.ID}}",
53
- ],
54
- { encoding: "utf8" },
55
- );
56
- const ids = (findImages.stdout ?? "").trim().split("\n").filter(Boolean);
57
- for (const id of ids) allImageIds.add(id);
58
- }
41
+ const findImages = spawnSync("docker", ["images", "--filter", "label=totopo.managed=true", "--format", "{{.ID}}"], { encoding: "utf8" });
42
+ const imageIds = (findImages.stdout ?? "").trim().split("\n").filter(Boolean);
59
43
 
60
- if (allImageIds.size > 0) {
61
- log.info(` Found ${allImageIds.size} image(s) — removing...`);
62
- spawnSync("docker", ["rmi", "--force", ...allImageIds], { stdio: "inherit" });
44
+ if (imageIds.length > 0) {
45
+ log.info(` Found ${imageIds.length} image(s) — removing...`);
46
+ spawnSync("docker", ["rmi", "--force", ...imageIds], { stdio: "inherit" });
63
47
  } else {
64
- log.info(" No cached images found.");
48
+ log.info(" No cached images found.");
65
49
  }
66
50
 
67
51
  spawnSync("docker", ["image", "prune", "--force"], { stdio: "inherit" });
68
52
 
69
- // ─── Done ─────────────────────────────────────────────────────────────────────
53
+ // ─── Done ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
70
54
  outro("Reset complete. Run 'ai.sh' and select 'Start session' to start fresh.");
package/src/core/stop.ts CHANGED
@@ -1,35 +1,37 @@
1
1
  #!/usr/bin/env node
2
- // =============================================================================
2
+ // =========================================================================================================================================
3
3
  // scripts/stop.ts — Stop and remove all totopo dev container workspaces
4
4
  // Called by ai.sh — do not run directly.
5
- // =============================================================================
5
+ // =========================================================================================================================================
6
6
 
7
7
  import { spawnSync } from "node:child_process";
8
8
  import { log, outro } from "@clack/prompts";
9
9
 
10
+ // ─── Find all running totopo workspaces ──────────────────────────────────────────────────────────────────────────────────────────────────
10
11
  const listResult = spawnSync("devpod", ["list", "--output", "json"], {
11
- encoding: "utf8",
12
+ encoding: "utf8",
12
13
  });
13
14
 
14
15
  const workspaces: string[] = [];
15
16
  if (listResult.stdout) {
16
- const matches = listResult.stdout.matchAll(/"id":"(totopo-[^"]+)"/g);
17
- for (const match of matches) {
18
- if (match[1]) workspaces.push(match[1]);
19
- }
17
+ const matches = listResult.stdout.matchAll(/"id":"(totopo-managed-[^"]+)"/g);
18
+ for (const match of matches) {
19
+ if (match[1]) workspaces.push(match[1]);
20
+ }
20
21
  }
21
22
 
22
23
  if (workspaces.length === 0) {
23
- log.info("No totopo workspaces found.");
24
- process.exit(0);
24
+ log.info("No totopo workspaces found.");
25
+ process.exit(0);
25
26
  }
26
27
 
28
+ // ─── Stop and remove each workspace ──────────────────────────────────────────────────────────────────────────────────────────────────────
27
29
  log.step("Stopping all totopo workspaces...");
28
30
 
29
31
  for (const ws of workspaces) {
30
- log.step(`Stopping ${ws}...`);
31
- spawnSync("devpod", ["stop", ws], { stdio: "inherit" });
32
- spawnSync("devpod", ["delete", ws, "--force"], { stdio: "inherit" });
32
+ log.step(`Stopping ${ws}...`);
33
+ spawnSync("devpod", ["stop", ws], { stdio: "inherit" });
34
+ spawnSync("devpod", ["delete", ws, "--force"], { stdio: "inherit" });
33
35
  }
34
36
 
35
37
  outro("All totopo workspaces stopped and removed.");
@@ -5,6 +5,7 @@
5
5
  # =============================================================================
6
6
 
7
7
  FROM node:22-bookworm-slim
8
+ LABEL totopo.managed=true
8
9
 
9
10
  # ---------------------------------------------------------------------------
10
11
  # System packages
@@ -1,40 +1,35 @@
1
1
  {
2
- "name": "TOTOPO_PROJECT_NAME",
2
+ "name": "TOTOPO_PROJECT_NAME",
3
3
 
4
- // Build from local Dockerfile
5
- "build": {
6
- "dockerfile": "Dockerfile",
7
- "context": ".."
8
- },
4
+ // Build from local Dockerfile
5
+ "build": {
6
+ "dockerfile": "Dockerfile",
7
+ "context": ".."
8
+ },
9
9
 
10
- // Mount the repo as the workspace — nothing else is visible
11
- "workspaceFolder": "/workspace",
12
- "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=cached",
10
+ // Mount the repo as the workspace — nothing else is visible
11
+ "workspaceFolder": "/workspace",
12
+ "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=cached",
13
13
 
14
- // Pass API keys from a local .env file — never baked into the image
15
- // Create .totopo/.env on your host (see README.md)
16
- "runArgs": [
17
- "--name",
18
- "totopo-${localWorkspaceFolderBasename}",
19
- "--env-file",
20
- "${localWorkspaceFolder}/.totopo/.env"
21
- ],
14
+ // Pass API keys from a local .env file — never baked into the image
15
+ // Create .totopo/.env on your host (see README.md)
16
+ "runArgs": ["--name", "totopo-${localWorkspaceFolderBasename}", "--env-file", "${localWorkspaceFolder}/.totopo/.env"],
22
17
 
23
- // Run security + readiness checks every time container starts
24
- "postStartCommand": "node /workspace/.totopo/post-start.mjs",
18
+ // Run security + readiness checks every time container starts
19
+ "postStartCommand": "node /workspace/.totopo/post-start.mjs",
25
20
 
26
- // Terminal-only — no extensions installed
27
- "customizations": {
28
- "vscode": {
29
- "extensions": [],
30
- "settings": {
31
- // Do not forward host git credentials into the container
32
- "remote.containers.gitCredentialHelperConfigLocation": "none"
33
- }
34
- }
35
- },
21
+ // Terminal-only — no extensions installed
22
+ "customizations": {
23
+ "vscode": {
24
+ "extensions": [],
25
+ "settings": {
26
+ // Do not forward host git credentials into the container
27
+ "remote.containers.gitCredentialHelperConfigLocation": "none"
28
+ }
29
+ }
30
+ },
36
31
 
37
- // Drop capabilities — container cannot gain new privileges
38
- "capAdd": [],
39
- "securityOpt": ["no-new-privileges:true"]
32
+ // Drop capabilities — container cannot gain new privileges
33
+ "capAdd": [],
34
+ "securityOpt": ["no-new-privileges:true"]
40
35
  }
@@ -8,14 +8,14 @@
8
8
  import { execSync } from "node:child_process";
9
9
 
10
10
  const run = (cmd) => {
11
- try {
12
- return execSync(cmd, {
13
- encoding: "utf8",
14
- stdio: ["pipe", "pipe", "pipe"],
15
- }).trim();
16
- } catch {
17
- return null;
18
- }
11
+ try {
12
+ return execSync(cmd, {
13
+ encoding: "utf8",
14
+ stdio: ["pipe", "pipe", "pipe"],
15
+ }).trim();
16
+ } catch {
17
+ return null;
18
+ }
19
19
  };
20
20
 
21
21
  // ─── ANSI helpers ─────────────────────────────────────────────────────────────
@@ -27,13 +27,11 @@ const bold = (s) => `\x1b[1m${s}\x1b[0m`;
27
27
 
28
28
  let errors = 0;
29
29
 
30
- const ok = (label, detail) =>
31
- console.log(`${green("")} ${label.padEnd(24)}${detail ? dim(detail) : ""}`);
32
- const warn = (label, detail) =>
33
- console.log(`${yellow("▲")} ${label.padEnd(24)}${detail ? dim(detail) : ""}`);
30
+ const ok = (label, detail) => console.log(`${green("✓")} ${label.padEnd(24)}${detail ? dim(detail) : ""}`);
31
+ const warn = (label, detail) => console.log(`${yellow("")} ${label.padEnd(24)}${detail ? dim(detail) : ""}`);
34
32
  const fail = (label, detail) => {
35
- console.log(`${red("✗")} ${label.padEnd(24)}${detail || ""}`);
36
- errors++;
33
+ console.log(`${red("✗")} ${label.padEnd(24)}${detail || ""}`);
34
+ errors++;
37
35
  };
38
36
  const section = (title) => console.log(`\n${bold(title)}`);
39
37
 
@@ -45,35 +43,35 @@ section("Security");
45
43
 
46
44
  const whoami = run("whoami");
47
45
  if (whoami !== "root") {
48
- ok("non-root user", whoami ?? "unknown");
46
+ ok("non-root user", whoami ?? "unknown");
49
47
  } else {
50
- fail("non-root user", "running as root — container is misconfigured");
48
+ fail("non-root user", "running as root — container is misconfigured");
51
49
  }
52
50
 
53
51
  const gitProtocol = run("git config --system protocol.allow");
54
52
  if (gitProtocol === "never") {
55
- ok("git remote block", "protocol.allow = never");
53
+ ok("git remote block", "protocol.allow = never");
56
54
  } else {
57
- fail("git remote block", "not set — rebuild the container");
55
+ fail("git remote block", "not set — rebuild the container");
58
56
  }
59
57
 
60
58
  try {
61
- execSync("/usr/bin/git -C /workspace push", { stdio: "pipe" });
62
- fail("push blocked", "git push succeeded — remote access is NOT blocked");
59
+ execSync("/usr/bin/git -C /workspace push", { stdio: "pipe" });
60
+ fail("push blocked", "git push succeeded — remote access is NOT blocked");
63
61
  } catch {
64
- ok("push blocked", "remote push not possible");
62
+ ok("push blocked", "remote push not possible");
65
63
  }
66
64
 
67
65
  // ─── AI tools ────────────────────────────────────────────────────────────────
68
66
  section("AI tools");
69
67
 
70
68
  const checkTool = (cmd) => {
71
- const out = run(`${cmd} --version`);
72
- if (out !== null) {
73
- ok(cmd, out.split("\n")[0]);
74
- } else {
75
- fail(cmd, "not found — rebuild container");
76
- }
69
+ const out = run(`${cmd} --version`);
70
+ if (out !== null) {
71
+ ok(cmd, out.split("\n")[0]);
72
+ } else {
73
+ fail(cmd, "not found — rebuild container");
74
+ }
77
75
  };
78
76
 
79
77
  checkTool("claude");
@@ -91,12 +89,12 @@ ok("pnpm", run("pnpm --version") ? `v${run("pnpm --version")}` : "not found");
91
89
  section("API keys");
92
90
 
93
91
  const checkKey = (varName) => {
94
- const val = process.env[varName];
95
- if (val) {
96
- ok(varName, `${val.substring(0, 12)}...`);
97
- } else {
98
- warn(varName, "not set — add to .totopo/.env");
99
- }
92
+ const val = process.env[varName];
93
+ if (val) {
94
+ ok(varName, `${val.substring(0, 12)}...`);
95
+ } else {
96
+ warn(varName, "not set — add to .totopo/.env");
97
+ }
100
98
  };
101
99
 
102
100
  checkKey("ANTHROPIC_API_KEY");
@@ -104,10 +102,8 @@ checkKey("KILO_API_KEY");
104
102
 
105
103
  // ─── Summary ─────────────────────────────────────────────────────────────────
106
104
  if (errors === 0) {
107
- console.log(`\n${green("●")} ${bold("Ready.")}\n`);
105
+ console.log(`\n${green("●")} ${bold("Ready.")}\n`);
108
106
  } else {
109
- console.log(
110
- `\n${red("●")} ${bold(`${errors} error(s) — see above. Rebuild the container to fix.`)}\n`,
111
- );
112
- process.exit(1);
107
+ console.log(`\n${red("●")} ${bold(`${errors} error(s) — see above. Rebuild the container to fix.`)}\n`);
108
+ process.exit(1);
113
109
  }
package/tsconfig.json CHANGED
@@ -1,27 +1,27 @@
1
1
  {
2
- // Visit https://aka.ms/tsconfig to read more about this file
3
- "compilerOptions": {
4
- // Environment Settings
5
- // See also https://aka.ms/tsconfig/module
6
- "module": "nodenext",
7
- "target": "esnext",
8
- "lib": ["esnext"],
9
- "types": ["node"],
2
+ // Visit https://aka.ms/tsconfig to read more about this file
3
+ "compilerOptions": {
4
+ // Environment Settings
5
+ // See also https://aka.ms/tsconfig/module
6
+ "module": "nodenext",
7
+ "target": "esnext",
8
+ "lib": ["esnext"],
9
+ "types": ["node"],
10
10
 
11
- // Other Outputs
12
- "noEmit": true,
11
+ // Other Outputs
12
+ "noEmit": true,
13
13
 
14
- // Stricter Typechecking Options
15
- "noUncheckedIndexedAccess": true,
16
- "exactOptionalPropertyTypes": true,
14
+ // Stricter Typechecking Options
15
+ "noUncheckedIndexedAccess": true,
16
+ "exactOptionalPropertyTypes": true,
17
17
 
18
- // Recommended Options
19
- "strict": true,
20
- "verbatimModuleSyntax": true,
21
- "isolatedModules": true,
22
- "noUncheckedSideEffectImports": true,
23
- "moduleDetection": "force",
24
- "skipLibCheck": true
25
- },
26
- "include": ["src/**/*.ts"]
18
+ // Recommended Options
19
+ "strict": true,
20
+ "verbatimModuleSyntax": true,
21
+ "isolatedModules": true,
22
+ "noUncheckedSideEffectImports": true,
23
+ "moduleDetection": "force",
24
+ "skipLibCheck": true
25
+ },
26
+ "include": ["src/**/*.ts"]
27
27
  }