totopo 3.3.0-rc-1 → 3.3.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/bin/totopo.js CHANGED
@@ -50,10 +50,10 @@ if (!existsSync(new URL("../dist/commands/dev.js", import.meta.url))) {
50
50
  process.exit(1);
51
51
  }
52
52
 
53
- // --- v2 migration check ------------------------------------------------------------------------------------------------------------------
53
+ // --- migrations check --------------------------------------------------------------------------------------------------------------------
54
54
  try {
55
55
  const { runMigration } = await import("../dist/lib/migrate-to-latest.js");
56
- runMigration(process.cwd());
56
+ await runMigration(process.cwd(), false);
57
57
  } catch {
58
58
  // Non-fatal - migration failure should not block startup
59
59
  }
@@ -22,10 +22,10 @@
22
22
  // All migrations are idempotent - each checks if needed and skips if not.
23
23
  // =========================================================================================================================================
24
24
  import { spawnSync } from "node:child_process";
25
- import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
25
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
26
26
  import { homedir } from "node:os";
27
27
  import { join } from "node:path";
28
- import { log } from "@clack/prompts";
28
+ import { confirm, isCancel, log } from "@clack/prompts";
29
29
  import { load as loadYaml } from "js-yaml";
30
30
  import { AGENTS_DIR, CONTAINER_STARTUP, LOCK_FILE, PROFILE, SHADOWS_DIR, TOTOPO_DIR, TOTOPO_YAML, WORKSPACES_DIR } from "./constants.js";
31
31
  import { safeRmSync } from "./safe-rm.js";
@@ -151,6 +151,55 @@ function migrateSingleV2Workspace(v2, existingIds) {
151
151
  // =========================================================================================================================================
152
152
  // Migration steps - each is idempotent and checks if needed before acting
153
153
  // =========================================================================================================================================
154
+ const V1_WORKSPACE_FILES = ["Dockerfile", "README.md", "post-start.mjs", "settings.json"];
155
+ function getCandidateWorkspaceRoots(cwd) {
156
+ const roots = [cwd];
157
+ const gitRoot = spawnSync("git", ["rev-parse", "--show-toplevel"], {
158
+ cwd,
159
+ encoding: "utf8",
160
+ stdio: "pipe",
161
+ });
162
+ const root = (gitRoot.stdout ?? "").trim();
163
+ if (gitRoot.status === 0 && root.length > 0 && root !== cwd)
164
+ roots.push(root);
165
+ return roots;
166
+ }
167
+ function detectLegacyV1WorkspaceDir(cwd) {
168
+ for (const root of getCandidateWorkspaceRoots(cwd)) {
169
+ const legacyDir = join(root, TOTOPO_DIR);
170
+ try {
171
+ if (!statSync(legacyDir).isDirectory())
172
+ continue;
173
+ }
174
+ catch {
175
+ continue;
176
+ }
177
+ const hasLegacyFile = V1_WORKSPACE_FILES.some((file) => existsSync(join(legacyDir, file)));
178
+ if (hasLegacyFile)
179
+ return legacyDir;
180
+ }
181
+ return null;
182
+ }
183
+ /**
184
+ * v1.0.3 -> latest: Remove workspace-local .totopo/ artifacts.
185
+ * These files are now bundled in the totopo CLI package.
186
+ */
187
+ async function migrateLegacyV1WorkspaceArtifacts(cwd, requireConfirmation = true) {
188
+ const legacyDir = detectLegacyV1WorkspaceDir(cwd);
189
+ if (!legacyDir)
190
+ return;
191
+ log.warn(`Found legacy v1 totopo artifacts at ${legacyDir}.\n` +
192
+ " Latest totopo bundles these files in the binary, so this directory can be safely removed.");
193
+ if (requireConfirmation) {
194
+ const shouldRemove = await confirm({ message: "Remove legacy .totopo/ directory?", initialValue: true });
195
+ if (isCancel(shouldRemove) || !shouldRemove) {
196
+ log.info("Kept legacy .totopo/ directory.");
197
+ return;
198
+ }
199
+ }
200
+ safeRmSync(legacyDir, { recursive: true, force: true });
201
+ log.success("Removed legacy .totopo/ directory.");
202
+ }
154
203
  /**
155
204
  * v3-rc-1/rc-2 → latest: Rename ~/.totopo/projects/ → ~/.totopo/workspaces/.
156
205
  * Stops running containers first because they have bind mounts into the old path.
@@ -393,24 +442,31 @@ function migrateRemoveDeprecatedYamlFields(cwd) {
393
442
  // migrateLockFileFormat and migrateLockKeyYamlToRoot must run last so all workspace
394
443
  // dirs are in their final location first. migrateLockKeyYamlToRoot runs after
395
444
  // migrateLockFileFormat so the latter always writes "root=" for freshly upgraded files.
396
- const MIGRATIONS = [
397
- { from: "v3-rc-1/rc-2", description: "Rename ~/.totopo/projects/ to ~/.totopo/workspaces/", run: migrateProjectsDir },
398
- { from: "v2.x", description: "Hash-based dirs to workspace_id-based dirs + totopo.yaml", run: migrateV2Workspaces },
399
- { from: "v3-rc-1/rc-2", description: "Rename project_id to workspace_id in totopo.yaml", run: migrateTotopoYaml },
400
- { from: "v2.x", description: "Remove legacy ~/.totopo/.env global key file", run: migrateGlobalEnv },
401
- { from: "v3-rc-6", description: "Upgrade .lock files from positional to key=value format", run: migrateLockFileFormat },
402
- { from: "v3-rc-8", description: "Rename 'yaml' key to 'root' in .lock files", run: migrateLockKeyYamlToRoot },
403
- { from: "v3.1.0", description: "Remove last-cli-update key from .lock files", run: migrateRemoveLastCliUpdate },
404
- {
405
- from: "v3.2.1",
406
- description: "Remove deprecated fields (schema_version, name, yaml-language-server) from totopo.yaml",
407
- run: migrateRemoveDeprecatedYamlFields,
408
- },
409
- ];
445
+ function buildMigrations(cwd, skipAnyConfirmations) {
446
+ return [
447
+ {
448
+ from: "v1.0.3",
449
+ description: "Remove workspace-local .totopo/ artifacts",
450
+ run: () => migrateLegacyV1WorkspaceArtifacts(cwd, !skipAnyConfirmations),
451
+ },
452
+ { from: "v3-rc-1/rc-2", description: "Rename ~/.totopo/projects/ to ~/.totopo/workspaces/", run: migrateProjectsDir },
453
+ { from: "v2.x", description: "Hash-based dirs to workspace_id-based dirs + totopo.yaml", run: migrateV2Workspaces },
454
+ { from: "v3-rc-1/rc-2", description: "Rename project_id to workspace_id in totopo.yaml", run: () => migrateTotopoYaml(cwd) },
455
+ { from: "v2.x", description: "Remove legacy ~/.totopo/.env global key file", run: migrateGlobalEnv },
456
+ { from: "v3-rc-6", description: "Upgrade .lock files from positional to key=value format", run: migrateLockFileFormat },
457
+ { from: "v3-rc-8", description: "Rename 'yaml' key to 'root' in .lock files", run: migrateLockKeyYamlToRoot },
458
+ { from: "v3.1.0", description: "Remove last-cli-update key from .lock files", run: migrateRemoveLastCliUpdate },
459
+ {
460
+ from: "v3.2.1",
461
+ description: "Remove deprecated fields (schema_version, name, yaml-language-server) from totopo.yaml",
462
+ run: () => migrateRemoveDeprecatedYamlFields(cwd),
463
+ },
464
+ ];
465
+ }
410
466
  /** Run all migrations in order. Called early in bin/totopo.js startup. */
411
- export function runMigration(cwd) {
412
- for (const migration of MIGRATIONS) {
413
- migration.run(cwd);
467
+ export async function runMigration(cwd, skipAnyConfirmations = true) {
468
+ for (const migration of buildMigrations(cwd, skipAnyConfirmations)) {
469
+ await migration.run();
414
470
  }
415
471
  }
416
472
  // =========================================================================================================================================
@@ -421,8 +477,16 @@ export function runMigration(cwd) {
421
477
  /** Check if a running container's image is stale (missing expected files/features). */
422
478
  export function isImageStale(containerName) {
423
479
  // v3.2.0: startup.mjs replaced post-start.mjs + update-ai-clis.mjs
424
- const check = spawnSync("docker", ["exec", containerName, "test", "-f", CONTAINER_STARTUP], { stdio: "pipe" });
425
- if (check.status !== 0)
480
+ const startupCheck = spawnSync("docker", ["exec", containerName, "test", "-f", CONTAINER_STARTUP], { stdio: "pipe" });
481
+ if (startupCheck.status !== 0)
482
+ return true;
483
+ // v3.3.0: file added to the base image for artifact inspection
484
+ const fileCheck = spawnSync("docker", ["exec", containerName, "test", "-x", "/usr/bin/file"], { stdio: "pipe" });
485
+ if (fileCheck.status !== 0)
486
+ return true;
487
+ // v3.3.0: bubblewrap added for Codex sandboxing prerequisites
488
+ const bubblewrapCheck = spawnSync("docker", ["exec", containerName, "test", "-x", "/usr/bin/bwrap"], { stdio: "pipe" });
489
+ if (bubblewrapCheck.status !== 0)
426
490
  return true;
427
491
  return false;
428
492
  }
@@ -13,14 +13,19 @@ const TEST_TMP_PREFIX = join(tmpdir(), `${CONTAINER_NAME_PREFIX}test-`);
13
13
  /**
14
14
  * Safe wrapper around rmSync. Throws if the path is outside a totopo-owned location:
15
15
  * - ~/.totopo/ (workspace caches, agents, shadows, global config)
16
+ * - A directory named .totopo (legacy v1 workspace-local artifacts)
16
17
  * - A file named totopo.yaml (workspace config file in any user workspace root)
17
18
  * - <tmpdir>/totopo-test-* (test temp directories)
18
19
  */
19
20
  export function safeRmSync(path, options) {
20
21
  const r = resolve(path);
21
- const ok = r === TOTOPO_HOME || r.startsWith(TOTOPO_HOME + sep) || basename(r) === TOTOPO_YAML || r.startsWith(TEST_TMP_PREFIX);
22
+ const ok = r === TOTOPO_HOME ||
23
+ r.startsWith(TOTOPO_HOME + sep) ||
24
+ basename(r) === TOTOPO_DIR ||
25
+ basename(r) === TOTOPO_YAML ||
26
+ r.startsWith(TEST_TMP_PREFIX);
22
27
  if (!ok) {
23
- throw new Error(`safeRmSync: refusing to delete '${r}' must be under ~/.totopo/, named totopo.yaml, or a test temp dir`);
28
+ throw new Error(`safeRmSync: refusing to delete '${r}' - must be under ~/.totopo/, named .totopo, named totopo.yaml, or a test temp dir`);
24
29
  }
25
30
  rmSync(r, options);
26
31
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "totopo",
3
- "version": "3.3.0-rc-1",
3
+ "version": "3.3.0",
4
4
  "description": "Run AI coding agents safely in your local codebase",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,7 +23,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
23
23
  # Build essentials
24
24
  build-essential pkg-config libssl-dev \
25
25
  # Utilities
26
- jq unzip zip tree htop procps lsb-release gnupg ca-certificates sox \
26
+ jq unzip zip tree htop procps lsb-release gnupg ca-certificates sox file bubblewrap \
27
27
  # Modern search/navigation tools
28
28
  ripgrep fzf \
29
29
  # Database clients