totopo 3.0.1 → 3.1.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
@@ -13,8 +13,8 @@ Local sandbox for AI agents.
13
13
  ## Motivation
14
14
 
15
15
  Two fundamental issues with AI agents:
16
- - They are non-deterministic they will occasionally get things wrong.
17
- - They are susceptible to prompt injection — they can get compromised and act against your interests without you knowing.
16
+ - They are non-deterministic and will occasionally make mistakes.
17
+ - Susceptibility to prompt injection — they can get compromised and act against your interests without you knowing.
18
18
 
19
19
  totopo addresses both with a dev container - when you run totopo in a given directory, the directory is mounted as a workspace where agents get a full, capable environment to work in — they just can't touch anything outside the workspace, and they can't reach remote git repositories.
20
20
 
@@ -68,7 +68,7 @@ On every run, totopo shows the workspace menu:
68
68
 
69
69
  - **Open session** — start or resume the dev container and connect
70
70
  - **Stop container** — stop the running container
71
- - **Manage Workspace** — profiles, shadow paths, rebuild, reset config
71
+ - **Manage Workspace** — shadow paths, rebuild, reset config
72
72
  - **Manage totopo** — multi-workspace management (stop containers, clear memory, uninstall)
73
73
 
74
74
  ### Working directory
@@ -101,22 +101,30 @@ Profiles let you define multiple container image variants for a workspace. Each
101
101
  # totopo.yaml
102
102
  profiles:
103
103
  default:
104
+ description: "Base image: Node.js, git, and AI CLIs"
104
105
  dockerfile_hook: |
105
- # Installs Go and Java.
106
- RUN apt-get update && apt-get install -y --no-install-recommends golang-go default-jdk-headless && rm -rf /var/lib/apt/lists/*
107
- slim:
106
+ # No extras uses the totopo base image as-is (Node.js + git + AI CLIs).
107
+ extended:
108
+ description: Base image + Go, Java, Rust, and Bun
108
109
  dockerfile_hook: |
109
- # No extras — uses the base image only (Node.js + git + AI CLIs).
110
- custom:
111
- dockerfile_hook: |
112
- # Add your own Dockerfile instructions below, or ask the agent inside the container to help.
113
- # e.g. Install Rust:
114
- # RUN curl -sSf https://sh.rustup.rs | sh -s -- -y
110
+ # Go
111
+ RUN apt-get update && apt-get install -y --no-install-recommends golang-go && rm -rf /var/lib/apt/lists/*
112
+ # Java (headless JDK — includes javac; needed for Kotlin, Scala, Android tooling)
113
+ RUN apt-get update && apt-get install -y --no-install-recommends default-jdk-headless && rm -rf /var/lib/apt/lists/*
114
+ # Rust (system-wide install — devuser can use cargo and rustc)
115
+ ENV RUSTUP_HOME=/usr/local/rustup
116
+ ENV CARGO_HOME=/usr/local/cargo
117
+ ENV PATH=/usr/local/cargo/bin:$PATH
118
+ RUN curl -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path && chmod -R a+rx /usr/local/cargo /usr/local/rustup
119
+ # Bun (fast JS runtime, bundler, and package manager)
120
+ ENV BUN_INSTALL=/usr/local/bun
121
+ ENV PATH=/usr/local/bun/bin:$PATH
122
+ RUN curl -fsSL https://bun.sh/install | bash
115
123
  # Add more profiles here — or ask the agent inside the container to set one up for you.
116
124
 
117
125
  ```
118
126
 
119
- Three profiles are set by default. When multiple profiles are defined, totopo prompts you to pick one at session start (the choice is remembered). Switch any time in **Manage Workspace > Profiles** — a profile change triggers a container rebuild.
127
+ Two profiles are set by default. When multiple profiles are defined, totopo prompts you to pick one at session start (the choice is remembered). A profile change triggers a container rebuild on the next session.
120
128
 
121
129
  The base image is defined in [`templates/Dockerfile`](templates/Dockerfile) — inspect it to see what's already included before adding your own layers. To force a fully fresh build (no Docker layer cache), use **Manage Workspace > Clean rebuild**.
122
130
 
@@ -41,9 +41,12 @@ async function selectProfile(ctx, profiles) {
41
41
  const choice = await select({
42
42
  message: "Profile:",
43
43
  options: profileNames.map((name) => {
44
+ const description = profiles[name]?.description;
45
+ const isCurrent = name === currentProfile;
46
+ const hint = description && isCurrent ? `${description} · current` : (description ?? (isCurrent ? "current" : undefined));
44
47
  const opt = { value: name, label: name };
45
- if (name === currentProfile)
46
- opt.hint = "current";
48
+ if (hint)
49
+ opt.hint = hint;
47
50
  return opt;
48
51
  }),
49
52
  initialValue: currentProfile,
@@ -25,7 +25,7 @@ export async function run(args) {
25
25
  const options = [
26
26
  { value: "dev", label: "Open session", hint: "start or resume the dev container" },
27
27
  ...(workspaceRunning ? [{ value: "stop", label: "Stop container", hint: "stops this workspace's container" }] : []),
28
- { value: "settings", label: "Manage Workspace", hint: "profiles, shadow paths, rebuild" },
28
+ { value: "settings", label: "Manage Workspace", hint: "shadow paths, rebuild, reset config" },
29
29
  { value: "manage-totopo", label: "Manage totopo →", hint: "stop, clear, remove, uninstall" },
30
30
  { value: "help", label: "Help", hint: "official docs" },
31
31
  { value: "quit", label: "Quit" },
@@ -1,55 +1,11 @@
1
1
  // =========================================================================================================================================
2
- // src/commands/workspace.ts - Manage Workspace submenu: profiles, shadow paths, rebuild, reset, stop, reset-image
2
+ // src/commands/workspace.ts - Manage Workspace submenu: shadow paths, rebuild, reset, stop, reset-image
3
3
  // =========================================================================================================================================
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";
8
7
  import { countPatternHits } from "../lib/shadows.js";
9
8
  import { buildDefaultTotopoYaml, readTotopoYaml, writeTotopoYaml } from "../lib/totopo-yaml.js";
10
- import { readActiveProfile, writeActiveProfile } from "../lib/workspace-identity.js";
11
- // --- Profile menu ------------------------------------------------------------------------------------------------------------------------
12
- async function profileMenu(ctx) {
13
- const yaml = readTotopoYaml(ctx.workspaceRoot);
14
- if (!yaml) {
15
- log.error("totopo.yaml not found or invalid.");
16
- return;
17
- }
18
- const profiles = yaml.profiles ?? {};
19
- const profileNames = Object.keys(profiles);
20
- if (profileNames.length === 0) {
21
- log.info("No profiles defined in totopo.yaml.");
22
- return;
23
- }
24
- const currentProfile = readActiveProfile(ctx.workspaceId) ?? PROFILE.default;
25
- note(`Active profile: ${currentProfile}`, "Profiles");
26
- if (profileNames.length <= 1) {
27
- log.info("Only one profile defined. Add more profiles in totopo.yaml to switch between them.");
28
- return;
29
- }
30
- const profileOptions = profileNames.map((name) => {
31
- const opt = { value: name, label: name };
32
- if (name === currentProfile)
33
- opt.hint = "current";
34
- return opt;
35
- });
36
- profileOptions.push({ value: "back", label: "← Back" });
37
- const choice = await select({
38
- message: "Switch active profile:",
39
- options: profileOptions,
40
- });
41
- if (isCancel(choice) || choice === "back")
42
- return;
43
- const selected = choice;
44
- if (selected === currentProfile) {
45
- log.info("Already on that profile.");
46
- return;
47
- }
48
- writeActiveProfile(ctx.workspaceId, selected);
49
- log.success(`Switched to profile "${selected}"`);
50
- log.info("Profile change requires a container rebuild. Stop and rebuild to apply.");
51
- await promptStopContainer(ctx);
52
- }
53
9
  // --- Shadow paths menu -------------------------------------------------------------------------------------------------------------------
54
10
  async function shadowPathsMenu(ctx) {
55
11
  const yaml = readTotopoYaml(ctx.workspaceRoot);
@@ -205,7 +161,6 @@ async function resetTotopoYaml(ctx) {
205
161
  export async function run(ctx) {
206
162
  while (true) {
207
163
  const options = [
208
- { value: "profiles", label: "Profiles", hint: "switch active Dockerfile profile" },
209
164
  { value: "shadow-paths", label: "Shadow paths", hint: "manage shadow patterns" },
210
165
  { value: "rebuild", label: "Rebuild container", hint: "force a fresh image build" },
211
166
  { value: "clean-rebuild", label: "Clean rebuild", hint: "fresh build, no cache" },
@@ -217,9 +172,6 @@ export async function run(ctx) {
217
172
  return "back";
218
173
  }
219
174
  switch (action) {
220
- case "profiles":
221
- await profileMenu(ctx);
222
- break;
223
175
  case "shadow-paths":
224
176
  await shadowPathsMenu(ctx);
225
177
  break;
@@ -31,6 +31,5 @@ export const LABEL_PROFILE = "totopo.profile";
31
31
  // Built-in profile names (must match keys in buildDefaultTotopoYaml in totopo-yaml.ts)
32
32
  export const PROFILE = {
33
33
  default: "default",
34
- slim: "slim",
35
- custom: "custom",
34
+ extended: "extended",
36
35
  };
@@ -116,18 +116,24 @@ export function writeTotopoYaml(dir, config) {
116
116
  writeFileSync(filePath, `${YAML_HEADER}${body}\n${PROFILES_FOOTER_COMMENT}\n`);
117
117
  }
118
118
  // --- Defaults ----------------------------------------------------------------------------------------------------------------------------
119
- const DEFAULT_PROFILE_HOOK = `# Installs Go and Java.
120
- RUN apt-get update && apt-get install -y --no-install-recommends golang-go default-jdk-headless && rm -rf /var/lib/apt/lists/*
119
+ const DEFAULT_PROFILE_HOOK = `# No extras uses the totopo base image as-is (Node.js + git + AI CLIs).
121
120
  `;
122
- const SLIM_PROFILE_HOOK = `# No extras — uses the base image only (Node.js + git + AI CLIs).
123
- `;
124
- const CUSTOM_PROFILE_HOOK = `# Add your own Dockerfile instructions below, or ask the agent inside the container to help.
125
- # Install Bun:
126
- # RUN curl -fsSL https://bun.sh/install | bash
127
- # Install Rust:
128
- # RUN curl -sSf https://sh.rustup.rs | sh -s -- -y
129
- # Copy a local script into the image:
130
- # COPY my-tool.sh /usr/local/bin/my-tool.sh
121
+ const EXTENDED_PROFILE_HOOK = `# Go
122
+ RUN apt-get update && apt-get install -y --no-install-recommends golang-go && rm -rf /var/lib/apt/lists/*
123
+
124
+ # Java (headless JDK — includes javac; needed for Kotlin, Scala, Android tooling)
125
+ RUN apt-get update && apt-get install -y --no-install-recommends default-jdk-headless && rm -rf /var/lib/apt/lists/*
126
+
127
+ # Rust (system-wide install devuser can use cargo and rustc)
128
+ ENV RUSTUP_HOME=/usr/local/rustup
129
+ ENV CARGO_HOME=/usr/local/cargo
130
+ ENV PATH=/usr/local/cargo/bin:$PATH
131
+ RUN curl -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path && chmod -R a+rx /usr/local/cargo /usr/local/rustup
132
+
133
+ # Bun (fast JS runtime, bundler, and package manager)
134
+ ENV BUN_INSTALL=/usr/local/bun
135
+ ENV PATH=/usr/local/bun/bin:$PATH
136
+ RUN curl -fsSL https://bun.sh/install | bash
131
137
  `;
132
138
  // Appended after the last profile to hint at adding more
133
139
  const PROFILES_FOOTER_COMMENT = " # Add more profiles here — or ask the agent inside the container to set one up for you.";
@@ -139,13 +145,12 @@ export function buildDefaultTotopoYaml(workspaceId, name) {
139
145
  shadow_paths: [...DEFAULT_SHADOW_PATHS],
140
146
  profiles: {
141
147
  default: {
148
+ description: "Base image: Node.js, git, and AI CLIs",
142
149
  dockerfile_hook: DEFAULT_PROFILE_HOOK,
143
150
  },
144
- slim: {
145
- dockerfile_hook: SLIM_PROFILE_HOOK,
146
- },
147
- custom: {
148
- dockerfile_hook: CUSTOM_PROFILE_HOOK,
151
+ extended: {
152
+ description: "Base image + Go, Java, Rust, and Bun",
153
+ dockerfile_hook: EXTENDED_PROFILE_HOOK,
149
154
  },
150
155
  },
151
156
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "totopo",
3
- "version": "3.0.1",
3
+ "version": "3.1.0-rc-2",
4
4
  "description": "Run AI coding agents safely in your local codebase",
5
5
  "type": "module",
6
6
  "bin": {
@@ -37,6 +37,10 @@
37
37
  "additionalProperties": {
38
38
  "type": "object",
39
39
  "properties": {
40
+ "description": {
41
+ "type": "string",
42
+ "description": "Human-readable description of this profile shown as a hint during profile selection."
43
+ },
40
44
  "dockerfile_hook": {
41
45
  "type": "string",
42
46
  "description": "Raw Dockerfile instructions appended after the base image layers. Use for installing additional runtimes, tools, or dependencies."
@@ -106,11 +106,11 @@ ok("npm", `v${run("npm --version") ?? "not found"}`);
106
106
  const pnpmVer = run("pnpm --version");
107
107
  ok("pnpm", pnpmVer ? `v${pnpmVer}` : "not found");
108
108
  ok("python3", run("python3 --version") ?? "not found");
109
+ ok("pipx", run("pipx --version") ?? "not found");
109
110
 
110
111
  // Optional (installed via profile hooks)
111
112
  const bunVer = run("bun --version");
112
113
  checkRuntime("bun", bunVer ? `v${bunVer}` : null);
113
- checkRuntime("uv", run("uv --version"));
114
114
  checkRuntime("go", run("go version"));
115
115
  checkRuntime("cargo", run("cargo --version"));
116
116
  checkRuntime("java", run("java --version")?.split("\n")[0] ?? null);
@@ -125,6 +125,14 @@ ok("fzf", run("fzf --version") ?? "not found");
125
125
  ok("jq", run("jq --version") ?? "not found");
126
126
  ok("yq", run("yq --version") ?? "not found");
127
127
 
128
+ // ─── Database tools ──────────────────────────────────────────────────────────
129
+ section("Database tools");
130
+
131
+ ok("sqlite3", run("sqlite3 --version")?.split(" ").slice(0, 2).join(" ") ?? "not found");
132
+ ok("psql", run("psql --version") ?? "not found");
133
+ ok("mysql", run("mysql --version") ?? "not found");
134
+ ok("redis-cli", run("redis-cli --version") ?? "not found");
135
+
128
136
  // ─── API keys ────────────────────────────────────────────────────────────────
129
137
  section("API keys");
130
138