totopo 3.0.0-rc-8 → 3.0.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.
@@ -7,7 +7,7 @@ import { existsSync } from "node:fs";
7
7
  import { join, relative } from "node:path";
8
8
  import { cancel, isCancel, log, outro, select } from "@clack/prompts";
9
9
  import { buildAgentContextDocs, buildAgentMountArgs, injectAgentContext } from "../lib/agent-context.js";
10
- import { CONTAINER_POST_START, CONTAINER_WORKSPACE, LABEL_MANAGED, LABEL_PROFILE, LABEL_SHADOWS } from "../lib/constants.js";
10
+ import { CONTAINER_POST_START, CONTAINER_WORKSPACE, LABEL_MANAGED, LABEL_PROFILE, LABEL_SHADOWS, PROFILE } from "../lib/constants.js";
11
11
  import { buildDockerfile, buildImageWithTempfile } from "../lib/dockerfile-builder.js";
12
12
  import { buildShadowMountArgs, ensureShadowsInSync, expandShadowPatterns } from "../lib/shadows.js";
13
13
  import { readTotopoYaml } from "../lib/totopo-yaml.js";
@@ -35,9 +35,9 @@ async function promptWorkdir(workspaceDir, cwd) {
35
35
  async function selectProfile(ctx, profiles) {
36
36
  const profileNames = Object.keys(profiles);
37
37
  if (profileNames.length <= 1) {
38
- return profileNames[0] ?? "default";
38
+ return profileNames[0] ?? PROFILE.default;
39
39
  }
40
- const currentProfile = readActiveProfile(ctx.workspaceId) ?? "default";
40
+ const currentProfile = readActiveProfile(ctx.workspaceId) ?? PROFILE.default;
41
41
  const choice = await select({
42
42
  message: "Profile:",
43
43
  options: profileNames.map((name) => {
@@ -6,11 +6,12 @@ import { existsSync } from "node:fs";
6
6
  import { join } from "node:path";
7
7
  import { styleText } from "node:util";
8
8
  import { box, cancel, isCancel, select } from "@clack/prompts";
9
+ import { PROFILE } from "../lib/constants.js";
9
10
  import { readActiveProfile } from "../lib/workspace-identity.js";
10
11
  export async function run(args) {
11
12
  const { ctx, workspaceRunning } = args;
12
13
  // --- Read workspace config -----------------------------------------------------------------------------------------------------------
13
- const activeProfile = readActiveProfile(ctx.workspaceId) ?? "default";
14
+ const activeProfile = readActiveProfile(ctx.workspaceId) ?? PROFILE.default;
14
15
  const hasGit = existsSync(join(ctx.workspaceRoot, ".git"));
15
16
  // --- Status box ----------------------------------------------------------------------------------------------------------------------
16
17
  const containerStatus = workspaceRunning ? "running" : "stopped";
@@ -4,6 +4,7 @@
4
4
  import { spawnSync } from "node:child_process";
5
5
  import { relative } from "node:path";
6
6
  import { cancel, confirm, isCancel, log, multiselect, note, outro, path, select, text } from "@clack/prompts";
7
+ import { PROFILE } from "../lib/constants.js";
7
8
  import { countPatternHits } from "../lib/shadows.js";
8
9
  import { buildDefaultTotopoYaml, readTotopoYaml, writeTotopoYaml } from "../lib/totopo-yaml.js";
9
10
  import { readActiveProfile, writeActiveProfile } from "../lib/workspace-identity.js";
@@ -20,7 +21,7 @@ async function profileMenu(ctx) {
20
21
  log.info("No profiles defined in totopo.yaml.");
21
22
  return;
22
23
  }
23
- const currentProfile = readActiveProfile(ctx.workspaceId) ?? "default";
24
+ const currentProfile = readActiveProfile(ctx.workspaceId) ?? PROFILE.default;
24
25
  note(`Active profile: ${currentProfile}`, "Profiles");
25
26
  if (profileNames.length <= 1) {
26
27
  log.info("Only one profile defined. Add more profiles in totopo.yaml to switch between them.");
@@ -28,3 +28,9 @@ export const CONTAINER_NAME_PREFIX = "totopo-";
28
28
  export const LABEL_MANAGED = "totopo.managed";
29
29
  export const LABEL_SHADOWS = "totopo.shadows";
30
30
  export const LABEL_PROFILE = "totopo.profile";
31
+ // Built-in profile names (must match keys in buildDefaultTotopoYaml in totopo-yaml.ts)
32
+ export const PROFILE = {
33
+ default: "default",
34
+ slim: "slim",
35
+ custom: "custom",
36
+ };
@@ -24,10 +24,10 @@ import { homedir } from "node:os";
24
24
  import { join } from "node:path";
25
25
  import { log } from "@clack/prompts";
26
26
  import { load as loadYaml } from "js-yaml";
27
- import { AGENTS_DIR, CONTAINER_NAME_PREFIX, GLOBAL_ENV_FILE, LOCK_FILE, PROJECTS_DIR, SHADOWS_DIR, TOTOPO_DIR, TOTOPO_YAML, WORKSPACES_DIR, } from "./constants.js";
27
+ import { AGENTS_DIR, LOCK_FILE, PROFILE, SHADOWS_DIR, TOTOPO_DIR, TOTOPO_YAML, WORKSPACES_DIR } from "./constants.js";
28
28
  import { safeRmSync } from "./safe-rm.js";
29
29
  import { buildDefaultTotopoYaml, readTotopoYaml, slugifyForWorkspaceId, validateWorkspaceId, writeTotopoYaml, } from "./totopo-yaml.js";
30
- import { findTotopoYamlDir, getWorkspacesBaseDir, initWorkspaceDir } from "./workspace-identity.js";
30
+ import { findTotopoYamlDir, getWorkspacesBaseDir, initWorkspaceDir, LOCK_KEYS } from "./workspace-identity.js";
31
31
  function isV2ProjectDir(dirPath) {
32
32
  return existsSync(join(dirPath, "meta.json"));
33
33
  }
@@ -134,7 +134,7 @@ function migrateSingleV2Workspace(v2, existingIds) {
134
134
  }
135
135
  const newDir = join(getWorkspacesBaseDir(), workspaceId);
136
136
  initWorkspaceDir(workspaceId, v2.projectRoot);
137
- const oldAgents = join(getWorkspacesBaseDir(), v2.hashId, AGENTS_DIR);
137
+ const oldAgents = join(getWorkspacesBaseDir(), v2.hashId, "agents");
138
138
  const newAgents = join(newDir, AGENTS_DIR);
139
139
  if (existsSync(oldAgents)) {
140
140
  try {
@@ -144,7 +144,7 @@ function migrateSingleV2Workspace(v2, existingIds) {
144
144
  log.warn(`Could not copy agent memory for "${v2.displayName}"`);
145
145
  }
146
146
  }
147
- const oldShadows = join(getWorkspacesBaseDir(), v2.hashId, SHADOWS_DIR);
147
+ const oldShadows = join(getWorkspacesBaseDir(), v2.hashId, "shadows");
148
148
  const newShadows = join(newDir, SHADOWS_DIR);
149
149
  if (existsSync(oldShadows)) {
150
150
  try {
@@ -166,7 +166,7 @@ function migrateSingleV2Workspace(v2, existingIds) {
166
166
  * Stops running containers first because they have bind mounts into the old path.
167
167
  */
168
168
  function migrateProjectsDir() {
169
- const oldDir = join(homedir(), TOTOPO_DIR, PROJECTS_DIR);
169
+ const oldDir = join(homedir(), ".totopo", "projects");
170
170
  const newDir = join(homedir(), TOTOPO_DIR, WORKSPACES_DIR);
171
171
  if (!existsSync(oldDir))
172
172
  return;
@@ -175,11 +175,11 @@ function migrateProjectsDir() {
175
175
  safeRmSync(oldDir, { recursive: true });
176
176
  return;
177
177
  }
178
- const psResult = spawnSync("docker", ["ps", "--filter", `name=${CONTAINER_NAME_PREFIX}`, "--format", "{{.Names}}"], {
178
+ const psResult = spawnSync("docker", ["ps", "--filter", "name=totopo-", "--format", "{{.Names}}"], {
179
179
  encoding: "utf8",
180
180
  stdio: "pipe",
181
181
  });
182
- const projectContainerNames = new Set(entries.map((e) => `${CONTAINER_NAME_PREFIX}${e}`));
182
+ const projectContainerNames = new Set(entries.map((e) => `totopo-${e}`));
183
183
  const running = (psResult.stdout ?? "")
184
184
  .trim()
185
185
  .split("\n")
@@ -269,7 +269,7 @@ function migrateTotopoYaml(cwd) {
269
269
  * API keys are now declared per-workspace via env_file in totopo.yaml.
270
270
  */
271
271
  function migrateGlobalEnv() {
272
- const globalEnv = join(homedir(), TOTOPO_DIR, GLOBAL_ENV_FILE);
272
+ const globalEnv = join(homedir(), ".totopo", ".env");
273
273
  if (!existsSync(globalEnv))
274
274
  return;
275
275
  log.warn("Removed legacy ~/.totopo/.env - API keys are now declared per-workspace via env_file in totopo.yaml.\n" +
@@ -295,8 +295,30 @@ function migrateLockFileFormat() {
295
295
  const [firstLine, secondLine] = lines;
296
296
  if (!firstLine || firstLine.includes("="))
297
297
  continue; // empty or already new format
298
- const activeProfile = secondLine ?? "default";
299
- writeFileSync(lockPath, `yaml=${firstLine}\nprofile=${activeProfile}\nlast-cli-update=\n`);
298
+ const activeProfile = secondLine ?? PROFILE.default;
299
+ writeFileSync(lockPath, `${LOCK_KEYS.workspaceRoot}=${firstLine}\n${LOCK_KEYS.activeProfile}=${activeProfile}\n${LOCK_KEYS.lastCliUpdate}=\n`);
300
+ }
301
+ catch {
302
+ // unreadable -- skip, will surface as a broken workspace elsewhere
303
+ }
304
+ }
305
+ }
306
+ /**
307
+ * v3-rc-8 and earlier: Rename the "yaml" key to "root" in .lock files.
308
+ * The "yaml" key name was misleading — it holds the workspace root path, not YAML content.
309
+ * Detects old format by presence of a line starting with "yaml=". Idempotent.
310
+ */
311
+ function migrateLockKeyYamlToRoot() {
312
+ const baseDir = getWorkspacesBaseDir();
313
+ if (!existsSync(baseDir))
314
+ return;
315
+ for (const entry of readdirSync(baseDir)) {
316
+ const lockPath = join(baseDir, entry, LOCK_FILE);
317
+ try {
318
+ const content = readFileSync(lockPath, "utf8");
319
+ if (!content.includes("yaml="))
320
+ continue;
321
+ writeFileSync(lockPath, content.replace(/^yaml=/m, `${LOCK_KEYS.workspaceRoot}=`));
300
322
  }
301
323
  catch {
302
324
  // unreadable -- skip, will surface as a broken workspace elsewhere
@@ -306,13 +328,16 @@ function migrateLockFileFormat() {
306
328
  // Order matters: migrateProjectsDir must run before migrateV2Workspaces because
307
329
  // step 2 scans ~/.totopo/workspaces/ which only exists after step 1 renames projects/.
308
330
  // Steps 3 and 4 are independent of each other and of steps 1-2.
309
- // migrateLockFileFormat must run last so all workspace dirs are in their final location first.
331
+ // migrateLockFileFormat and migrateLockKeyYamlToRoot must run last so all workspace
332
+ // dirs are in their final location first. migrateLockKeyYamlToRoot runs after
333
+ // migrateLockFileFormat so the latter always writes "root=" for freshly upgraded files.
310
334
  const MIGRATIONS = [
311
335
  { from: "v3-rc-1/rc-2", description: "Rename ~/.totopo/projects/ to ~/.totopo/workspaces/", run: migrateProjectsDir },
312
336
  { from: "v2.x", description: "Hash-based dirs to workspace_id-based dirs + totopo.yaml", run: migrateV2Workspaces },
313
337
  { from: "v3-rc-1/rc-2", description: "Rename project_id to workspace_id in totopo.yaml", run: migrateTotopoYaml },
314
338
  { from: "v2.x", description: "Remove legacy ~/.totopo/.env global key file", run: migrateGlobalEnv },
315
339
  { from: "v3-rc-6", description: "Upgrade .lock files from positional to key=value format", run: migrateLockFileFormat },
340
+ { from: "v3-rc-8", description: "Rename 'yaml' key to 'root' in .lock files", run: migrateLockKeyYamlToRoot },
316
341
  ];
317
342
  /** Run all migrations in order. Called early in bin/totopo.js startup. */
318
343
  export function runMigration(cwd) {
@@ -5,11 +5,11 @@
5
5
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
6
6
  import { homedir } from "node:os";
7
7
  import { dirname, join } from "node:path";
8
- import { AGENTS_DIR, CONTAINER_NAME_PREFIX, LOCK_FILE, SHADOWS_DIR, TOTOPO_DIR, TOTOPO_YAML, WORKSPACES_DIR } from "./constants.js";
8
+ import { AGENTS_DIR, CONTAINER_NAME_PREFIX, LOCK_FILE, PROFILE, SHADOWS_DIR, TOTOPO_DIR, TOTOPO_YAML, WORKSPACES_DIR, } from "./constants.js";
9
9
  import { readTotopoYaml } from "./totopo-yaml.js";
10
10
  /** Maps LockFile field names to their corresponding keys written in the .lock file. */
11
- const LOCK_KEYS = {
12
- workspaceRoot: "yaml",
11
+ export const LOCK_KEYS = {
12
+ workspaceRoot: "root",
13
13
  activeProfile: "profile",
14
14
  lastCliUpdate: "last-cli-update",
15
15
  };
@@ -53,7 +53,7 @@ function parseLockFile(workspaceId) {
53
53
  return null;
54
54
  return {
55
55
  workspaceRoot: partial.workspaceRoot,
56
- activeProfile: partial.activeProfile ?? "default",
56
+ activeProfile: partial.activeProfile ?? PROFILE.default,
57
57
  lastCliUpdate: partial.lastCliUpdate ?? "",
58
58
  };
59
59
  }
@@ -77,7 +77,7 @@ export function writeLockFile(workspaceId, workspaceRoot) {
77
77
  const existing = parseLockFile(workspaceId);
78
78
  writeLockFileInternal(workspaceId, {
79
79
  workspaceRoot,
80
- activeProfile: existing?.activeProfile ?? "default",
80
+ activeProfile: existing?.activeProfile ?? PROFILE.default,
81
81
  lastCliUpdate: existing?.lastCliUpdate ?? "",
82
82
  });
83
83
  }
@@ -105,7 +105,7 @@ export function writeLastCliUpdate(workspaceId, timestamp) {
105
105
  }
106
106
  // --- Workspace directory initialization --------------------------------------------------------------------------------------------------
107
107
  /** Initialize ~/.totopo/workspaces/<workspace_id>/ with lock file and subdirs. */
108
- export function initWorkspaceDir(workspaceId, workspaceRoot, activeProfile = "default") {
108
+ export function initWorkspaceDir(workspaceId, workspaceRoot, activeProfile = PROFILE.default) {
109
109
  const dir = getWorkspaceDir(workspaceId);
110
110
  mkdirSync(join(dir, AGENTS_DIR), { recursive: true });
111
111
  mkdirSync(join(dir, SHADOWS_DIR), { recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "totopo",
3
- "version": "3.0.0-rc-8",
3
+ "version": "3.0.0",
4
4
  "description": "Run AI coding agents safely in your local codebase",
5
5
  "type": "module",
6
6
  "bin": {