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 +21 -13
- package/dist/commands/dev.js +5 -2
- package/dist/commands/menu.js +1 -1
- package/dist/commands/workspace.js +1 -49
- package/dist/lib/constants.js +1 -2
- package/dist/lib/totopo-yaml.js +21 -16
- package/package.json +1 -1
- package/schema/totopo.schema.json +4 -0
- package/templates/post-start.mjs +9 -1
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
|
|
17
|
-
-
|
|
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** —
|
|
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
|
-
#
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
#
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
#
|
|
114
|
-
|
|
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
|
-
|
|
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
|
|
package/dist/commands/dev.js
CHANGED
|
@@ -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 (
|
|
46
|
-
opt.hint =
|
|
48
|
+
if (hint)
|
|
49
|
+
opt.hint = hint;
|
|
47
50
|
return opt;
|
|
48
51
|
}),
|
|
49
52
|
initialValue: currentProfile,
|
package/dist/commands/menu.js
CHANGED
|
@@ -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: "
|
|
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:
|
|
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;
|
package/dist/lib/constants.js
CHANGED
package/dist/lib/totopo-yaml.js
CHANGED
|
@@ -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 = `#
|
|
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
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
#
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
#
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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
|
@@ -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."
|
package/templates/post-start.mjs
CHANGED
|
@@ -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
|
|