totopo 3.2.1 → 3.3.0-rc-1
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 +5 -6
- package/dist/commands/dev.js +1 -1
- package/dist/commands/global.js +6 -6
- package/dist/commands/menu.js +1 -1
- package/dist/commands/onboard.js +12 -17
- package/dist/commands/workspace.js +2 -2
- package/dist/lib/migrate-to-latest.js +56 -15
- package/dist/lib/totopo-yaml.js +7 -24
- package/dist/lib/workspace-identity.js +0 -2
- package/package.json +1 -1
- package/schema/totopo.schema.json +1 -10
package/README.md
CHANGED
|
@@ -12,15 +12,14 @@ Local sandbox for AI agents.
|
|
|
12
12
|
|
|
13
13
|
## Motivation
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Two fundamental risks when running AI agents locally:
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
1. Agents are unpredictable — they will make mistakes that may be hard to detect or undo.
|
|
18
|
+
2. Agents are vulnerable to prompt injection and can be subtly manipulated to leak sensitive data or execute unauthorized operations.
|
|
19
19
|
|
|
20
|
-
Totopo mitigates both risks
|
|
20
|
+
Totopo mitigates both risks by letting you run agents in a dev container — when you run totopo in a given directory, that directory is mounted as a workspace where agents can work freely, without access to the rest of your filesystem or your git remote.
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
If an agent gets compromised, it can't reach your host files — blast radius is limited to the workspace you chose to share.
|
|
22
|
+
In practice, this means any mistake can be reverted from your git remote, and even a compromised agent can't access sensitive files on your machine — SSH keys, credentials, browser data — things a locally-running agent could otherwise read without you ever noticing.
|
|
24
23
|
|
|
25
24
|
> totopo's security approach is basic — it is about the minimal precautions I believe anyone running AI agents should have. If you need more robust protections, look somewhere else.
|
|
26
25
|
|
package/dist/commands/dev.js
CHANGED
|
@@ -251,7 +251,7 @@ export async function run(packageDir, ctx, options) {
|
|
|
251
251
|
envFilePath,
|
|
252
252
|
hasGit,
|
|
253
253
|
shadowPatterns,
|
|
254
|
-
workspaceName: ctx.
|
|
254
|
+
workspaceName: ctx.workspaceId,
|
|
255
255
|
...(options?.noCache !== undefined && { noCache: options.noCache }),
|
|
256
256
|
};
|
|
257
257
|
startContainer(containerOpts);
|
package/dist/commands/global.js
CHANGED
|
@@ -70,12 +70,12 @@ async function clearAgentMemory() {
|
|
|
70
70
|
if (w === undefined)
|
|
71
71
|
return;
|
|
72
72
|
toClear = [w.workspaceId];
|
|
73
|
-
log.info(`Clearing agent memory for ${w.
|
|
73
|
+
log.info(`Clearing agent memory for ${w.workspaceId}...`);
|
|
74
74
|
}
|
|
75
75
|
else {
|
|
76
76
|
const selected = await multiselect({
|
|
77
77
|
message: "Select workspaces to clear agent memory for: (space to toggle, enter to confirm)",
|
|
78
|
-
options: workspaces.map((w) => ({ value: w.workspaceId, label: w.
|
|
78
|
+
options: workspaces.map((w) => ({ value: w.workspaceId, label: w.workspaceId, hint: w.workspaceRoot })),
|
|
79
79
|
required: false,
|
|
80
80
|
});
|
|
81
81
|
if (isCancel(selected)) {
|
|
@@ -95,7 +95,7 @@ async function clearAgentMemory() {
|
|
|
95
95
|
const isRunning = inspectResult.status === 0 && inspectResult.stdout.trim() === "running";
|
|
96
96
|
if (isRunning) {
|
|
97
97
|
const confirmed = await confirm({
|
|
98
|
-
message: `Container for ${w.
|
|
98
|
+
message: `Container for ${w.workspaceId} is running. Stop it to clear memory?`,
|
|
99
99
|
});
|
|
100
100
|
if (isCancel(confirmed) || !confirmed)
|
|
101
101
|
continue;
|
|
@@ -104,7 +104,7 @@ async function clearAgentMemory() {
|
|
|
104
104
|
}
|
|
105
105
|
const agentsDir = join(w.workspaceDir, AGENTS_DIR);
|
|
106
106
|
safeRmSync(agentsDir, { recursive: true, force: true });
|
|
107
|
-
log.success(`Cleared agent memory for ${w.
|
|
107
|
+
log.success(`Cleared agent memory for ${w.workspaceId}.`);
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
// --- Remove images (multi-select across all workspaces) ----------------------------------------------------------------------------------
|
|
@@ -163,7 +163,7 @@ async function uninstallWorkspaces(currentWorkspaceId) {
|
|
|
163
163
|
message: "Select workspaces to uninstall: (space to toggle, enter to confirm)",
|
|
164
164
|
options: sorted.map((w) => ({
|
|
165
165
|
value: w.workspaceId,
|
|
166
|
-
label: w.
|
|
166
|
+
label: w.workspaceId,
|
|
167
167
|
hint: w.workspaceRoot + (w.workspaceId === currentWorkspaceId ? " (current)" : ""),
|
|
168
168
|
})),
|
|
169
169
|
required: false,
|
|
@@ -199,7 +199,7 @@ async function uninstallWorkspaces(currentWorkspaceId) {
|
|
|
199
199
|
removeTotopoYaml = !isCancel(ans) && ans;
|
|
200
200
|
}
|
|
201
201
|
removeWorkspaceFiles(w.workspaceRoot, w.workspaceDir, removeTotopoYaml);
|
|
202
|
-
log.success(`Uninstalled workspace ${w.
|
|
202
|
+
log.success(`Uninstalled workspace ${w.workspaceId}.`);
|
|
203
203
|
}
|
|
204
204
|
return currentWorkspaceId !== undefined && selectedIds.includes(currentWorkspaceId);
|
|
205
205
|
}
|
package/dist/commands/menu.js
CHANGED
|
@@ -16,7 +16,7 @@ export async function run(args) {
|
|
|
16
16
|
// --- Status box ----------------------------------------------------------------------------------------------------------------------
|
|
17
17
|
const containerStatus = workspaceRunning ? "running" : "stopped";
|
|
18
18
|
const gitNotice = hasGit ? "" : `\n${styleText("yellow", "●")} no git — agent changes are not tracked`;
|
|
19
|
-
box(`workspace: ${ctx.
|
|
19
|
+
box(`workspace: ${ctx.workspaceId}\nprofile: ${activeProfile}\ncontainer: ${containerStatus}${gitNotice}`, ` totopo v${version} `, {
|
|
20
20
|
contentAlign: "left",
|
|
21
21
|
titleAlign: "center",
|
|
22
22
|
width: "auto",
|
package/dist/commands/onboard.js
CHANGED
|
@@ -60,11 +60,6 @@ export async function run(cwd) {
|
|
|
60
60
|
}
|
|
61
61
|
workspaceRoot = yamlDir;
|
|
62
62
|
yaml = existing;
|
|
63
|
-
// Show welcome message
|
|
64
|
-
if (yaml.name) {
|
|
65
|
-
log.info(yaml.name);
|
|
66
|
-
process.stdout.write("\n");
|
|
67
|
-
}
|
|
68
63
|
const ok = await confirm({ message: `Set up totopo for: ${toTildePath(workspaceRoot)}?` });
|
|
69
64
|
if (isCancel(ok) || !ok) {
|
|
70
65
|
cancel("Setup cancelled.");
|
|
@@ -109,22 +104,23 @@ export async function run(cwd) {
|
|
|
109
104
|
else {
|
|
110
105
|
workspaceRoot = rootChoice;
|
|
111
106
|
}
|
|
112
|
-
// Ask for workspace
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
message: "Workspace
|
|
116
|
-
placeholder:
|
|
117
|
-
defaultValue:
|
|
107
|
+
// Ask for workspace ID
|
|
108
|
+
const defaultId = slugifyForWorkspaceId(basename(workspaceRoot));
|
|
109
|
+
const idInput = await text({
|
|
110
|
+
message: "Workspace ID:",
|
|
111
|
+
placeholder: defaultId,
|
|
112
|
+
defaultValue: defaultId,
|
|
113
|
+
validate: (v) => validateWorkspaceId((v ?? "").trim() || defaultId),
|
|
118
114
|
});
|
|
119
|
-
if (isCancel(
|
|
115
|
+
if (isCancel(idInput)) {
|
|
120
116
|
cancel("Setup cancelled.");
|
|
121
117
|
return null;
|
|
122
118
|
}
|
|
123
|
-
const
|
|
124
|
-
//
|
|
125
|
-
const workspaceId = deriveUniqueWorkspaceId(
|
|
119
|
+
const inputId = idInput.trim() || defaultId;
|
|
120
|
+
// Auto-resolve collisions with numeric suffix
|
|
121
|
+
const workspaceId = deriveUniqueWorkspaceId(inputId, workspaceRoot);
|
|
126
122
|
// Build and write totopo.yaml
|
|
127
|
-
yaml = buildDefaultTotopoYaml(workspaceId
|
|
123
|
+
yaml = buildDefaultTotopoYaml(workspaceId);
|
|
128
124
|
writeTotopoYaml(workspaceRoot, yaml);
|
|
129
125
|
log.success(`Created ${toTildePath(join(workspaceRoot, TOTOPO_YAML))}`);
|
|
130
126
|
}
|
|
@@ -205,6 +201,5 @@ export async function run(cwd) {
|
|
|
205
201
|
workspaceRoot,
|
|
206
202
|
containerName: deriveContainerName(finalId),
|
|
207
203
|
workspaceDir: getWorkspaceDir(finalId),
|
|
208
|
-
displayName: yaml.name || finalId,
|
|
209
204
|
};
|
|
210
205
|
}
|
|
@@ -147,12 +147,12 @@ async function resetTotopoYaml(ctx) {
|
|
|
147
147
|
return;
|
|
148
148
|
}
|
|
149
149
|
note("This will reset totopo.yaml to factory defaults.\n" +
|
|
150
|
-
"Your workspace_id
|
|
150
|
+
"Your workspace_id will be preserved.\n" +
|
|
151
151
|
"Shadow paths, profiles, and env_file will be reset to defaults.", "Reset totopo.yaml");
|
|
152
152
|
const confirmed = await confirm({ message: "Reset totopo.yaml to defaults?" });
|
|
153
153
|
if (isCancel(confirmed) || !confirmed)
|
|
154
154
|
return;
|
|
155
|
-
const freshYaml = buildDefaultTotopoYaml(yaml.workspace_id
|
|
155
|
+
const freshYaml = buildDefaultTotopoYaml(yaml.workspace_id);
|
|
156
156
|
writeTotopoYaml(ctx.workspaceRoot, freshYaml);
|
|
157
157
|
log.success("totopo.yaml reset to defaults.");
|
|
158
158
|
await promptStopContainer(ctx);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// v2.x (~/.totopo/projects/<sha256-hash>/)
|
|
7
7
|
// Each workspace stored as: meta.json, settings.json, agents/, shadows/
|
|
8
8
|
// Global API keys in ~/.totopo/.env
|
|
9
|
-
// Optional totopo.yaml with name field (
|
|
9
|
+
// Optional totopo.yaml with name field (removed in v3.3)
|
|
10
10
|
//
|
|
11
11
|
// v3-rc-1/rc-2 (~/.totopo/workspaces/<workspace_id>/)
|
|
12
12
|
// Renamed projects/ to workspaces/, hash dirs to workspace_id dirs
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
// v3-rc-3+ (latest)
|
|
17
17
|
// project_id renamed to workspace_id in totopo.yaml
|
|
18
18
|
//
|
|
19
|
+
// v3.2.1 and earlier
|
|
20
|
+
// totopo.yaml had schema_version field and yaml-language-server header (both redundant)
|
|
21
|
+
//
|
|
19
22
|
// All migrations are idempotent - each checks if needed and skips if not.
|
|
20
23
|
// =========================================================================================================================================
|
|
21
24
|
import { spawnSync } from "node:child_process";
|
|
@@ -54,18 +57,6 @@ function readV2ShadowPaths(dirPath) {
|
|
|
54
57
|
return [];
|
|
55
58
|
}
|
|
56
59
|
}
|
|
57
|
-
function readV2YamlName(workspaceRoot) {
|
|
58
|
-
try {
|
|
59
|
-
const raw = loadYaml(readFileSync(join(workspaceRoot, TOTOPO_YAML), "utf8"));
|
|
60
|
-
if (typeof raw !== "object" || raw === null)
|
|
61
|
-
return null;
|
|
62
|
-
const obj = raw;
|
|
63
|
-
return typeof obj.name === "string" ? obj.name : null;
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
60
|
function detectV2Projects() {
|
|
70
61
|
const baseDir = getWorkspacesBaseDir();
|
|
71
62
|
if (!existsSync(baseDir))
|
|
@@ -123,9 +114,8 @@ function migrateSingleV2Workspace(v2, existingIds) {
|
|
|
123
114
|
workspaceId = yaml.workspace_id;
|
|
124
115
|
}
|
|
125
116
|
else {
|
|
126
|
-
const v2Name = readV2YamlName(v2.projectRoot);
|
|
127
117
|
workspaceId = generateUniqueWorkspaceId(v2.displayName, existingIds);
|
|
128
|
-
yaml = buildDefaultTotopoYaml(workspaceId
|
|
118
|
+
yaml = buildDefaultTotopoYaml(workspaceId);
|
|
129
119
|
if (v2.shadowPaths.length > 0) {
|
|
130
120
|
yaml.shadow_paths = [...new Set([...(yaml.shadow_paths ?? []), ...v2.shadowPaths])];
|
|
131
121
|
}
|
|
@@ -351,6 +341,52 @@ function migrateRemoveLastCliUpdate() {
|
|
|
351
341
|
}
|
|
352
342
|
}
|
|
353
343
|
}
|
|
344
|
+
/**
|
|
345
|
+
* v3.2.1 and earlier: Remove deprecated fields from totopo.yaml.
|
|
346
|
+
* - schema_version: redundant, totopo validates with the bundled JSON schema at runtime
|
|
347
|
+
* - yaml-language-server header: created stale versioned URLs
|
|
348
|
+
* - name: redundant, workspace_id serves as both identifier and display name
|
|
349
|
+
* Only migrates the current workspace (found by walking up from cwd).
|
|
350
|
+
*/
|
|
351
|
+
function migrateRemoveDeprecatedYamlFields(cwd) {
|
|
352
|
+
const dir = findTotopoYamlDir(cwd);
|
|
353
|
+
if (!dir)
|
|
354
|
+
return;
|
|
355
|
+
const filePath = join(dir, TOTOPO_YAML);
|
|
356
|
+
try {
|
|
357
|
+
const content = readFileSync(filePath, "utf8");
|
|
358
|
+
const hasSchemaVersion = /^schema_version:\s/m.test(content);
|
|
359
|
+
const hasYamlLsHeader = content.includes("# yaml-language-server:");
|
|
360
|
+
const hasName = /^name:\s/m.test(content);
|
|
361
|
+
if (!hasSchemaVersion && !hasYamlLsHeader && !hasName)
|
|
362
|
+
return;
|
|
363
|
+
const raw = loadYaml(content);
|
|
364
|
+
if (typeof raw !== "object" || raw === null)
|
|
365
|
+
return;
|
|
366
|
+
const obj = raw;
|
|
367
|
+
delete obj.schema_version;
|
|
368
|
+
delete obj.name;
|
|
369
|
+
try {
|
|
370
|
+
writeTotopoYaml(dir, obj);
|
|
371
|
+
readTotopoYaml(dir);
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
writeFileSync(filePath, content);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const removed = [];
|
|
378
|
+
if (hasSchemaVersion)
|
|
379
|
+
removed.push("schema_version");
|
|
380
|
+
if (hasYamlLsHeader)
|
|
381
|
+
removed.push("yaml-language-server header");
|
|
382
|
+
if (hasName)
|
|
383
|
+
removed.push("name");
|
|
384
|
+
log.success(`Migrated totopo.yaml: removed ${removed.join(", ")}`);
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
// Unreadable or invalid yaml - skip
|
|
388
|
+
}
|
|
389
|
+
}
|
|
354
390
|
// Order matters: migrateProjectsDir must run before migrateV2Workspaces because
|
|
355
391
|
// step 2 scans ~/.totopo/workspaces/ which only exists after step 1 renames projects/.
|
|
356
392
|
// Steps 3 and 4 are independent of each other and of steps 1-2.
|
|
@@ -365,6 +401,11 @@ const MIGRATIONS = [
|
|
|
365
401
|
{ from: "v3-rc-6", description: "Upgrade .lock files from positional to key=value format", run: migrateLockFileFormat },
|
|
366
402
|
{ from: "v3-rc-8", description: "Rename 'yaml' key to 'root' in .lock files", run: migrateLockKeyYamlToRoot },
|
|
367
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
|
+
},
|
|
368
409
|
];
|
|
369
410
|
/** Run all migrations in order. Called early in bin/totopo.js startup. */
|
|
370
411
|
export function runMigration(cwd) {
|
package/dist/lib/totopo-yaml.js
CHANGED
|
@@ -78,18 +78,14 @@ export function readTotopoYaml(dir) {
|
|
|
78
78
|
// Every published version (rc or release) has a corresponding git tag created by pnpm rc / pnpm rc:promote.
|
|
79
79
|
// We rely on that tag existing so these URLs resolve correctly for every installed version.
|
|
80
80
|
const { version } = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
|
|
81
|
-
const GITHUB_RAW_BASE = `https://raw.githubusercontent.com/asafratzon/totopo/v${version}`;
|
|
82
81
|
export const GITHUB_README_URL = `https://github.com/asafratzon/totopo/blob/v${version}/README.md`;
|
|
83
|
-
const YAML_HEADER = `# yaml-language-server: $schema=${GITHUB_RAW_BASE}/schema/totopo.schema.json
|
|
84
|
-
`;
|
|
85
82
|
// Inline comments injected before specific YAML keys (preceded by a blank line)
|
|
86
83
|
const YAML_COMMENTS = {
|
|
87
|
-
workspace_id: "# totopo workspace config
|
|
84
|
+
workspace_id: "# totopo workspace config - run 'npx totopo' from anywhere under this directory tree to start your dev container.\n" +
|
|
88
85
|
"# Ask the AI agent inside the container to help you edit this file if needed.\n" +
|
|
89
86
|
"# This file may be rewritten by totopo (repair, reset, settings changes). Custom comments will not be preserved.",
|
|
90
|
-
shadow_paths: "# .gitignore-style patterns
|
|
91
|
-
profiles: "# Dockerfile profiles
|
|
92
|
-
`# Base Dockerfile: ${GITHUB_RAW_BASE}/templates/Dockerfile\n` +
|
|
87
|
+
shadow_paths: "# .gitignore-style patterns - agents see an empty, isolated copy instead of the real host data.",
|
|
88
|
+
profiles: "# Dockerfile profiles - each adds on top of the totopo base image (Debian + Node.js + git + AI CLIs).\n" +
|
|
93
89
|
"# Switch profiles in the totopo settings menu, or ask the agent inside the container to help you add a new one.",
|
|
94
90
|
};
|
|
95
91
|
/** Write totopo.yaml to a directory with schema header and inline comments. */
|
|
@@ -113,7 +109,7 @@ export function writeTotopoYaml(dir, config) {
|
|
|
113
109
|
output.push(line);
|
|
114
110
|
}
|
|
115
111
|
const body = output.join("\n").trimEnd();
|
|
116
|
-
writeFileSync(filePath, `${
|
|
112
|
+
writeFileSync(filePath, `${body}\n${PROFILES_FOOTER_COMMENT}\n`);
|
|
117
113
|
}
|
|
118
114
|
// --- Defaults ----------------------------------------------------------------------------------------------------------------------------
|
|
119
115
|
const DEFAULT_PROFILE_HOOK = `# No extras — uses the totopo base image as-is (Node.js + git + AI CLIs).
|
|
@@ -138,9 +134,8 @@ RUN curl -fsSL https://bun.sh/install | bash
|
|
|
138
134
|
// Appended after the last profile to hint at adding more
|
|
139
135
|
const PROFILES_FOOTER_COMMENT = " # Add more profiles here — or ask the agent inside the container to set one up for you.";
|
|
140
136
|
/** Create a default TotopoYamlConfig with sane defaults. */
|
|
141
|
-
export function buildDefaultTotopoYaml(workspaceId
|
|
142
|
-
|
|
143
|
-
schema_version: 3,
|
|
137
|
+
export function buildDefaultTotopoYaml(workspaceId) {
|
|
138
|
+
return {
|
|
144
139
|
workspace_id: workspaceId,
|
|
145
140
|
shadow_paths: [...DEFAULT_SHADOW_PATHS],
|
|
146
141
|
profiles: {
|
|
@@ -154,18 +149,10 @@ export function buildDefaultTotopoYaml(workspaceId, name) {
|
|
|
154
149
|
},
|
|
155
150
|
},
|
|
156
151
|
};
|
|
157
|
-
if (name)
|
|
158
|
-
config.name = name;
|
|
159
|
-
// Reorder keys so name appears after workspace_id
|
|
160
|
-
const { schema_version, workspace_id, name: n, ...rest } = config;
|
|
161
|
-
const ordered = { schema_version, workspace_id };
|
|
162
|
-
if (n !== undefined)
|
|
163
|
-
ordered.name = n;
|
|
164
|
-
return Object.assign(ordered, rest);
|
|
165
152
|
}
|
|
166
153
|
// --- Repair -------------------------------------------------------------------------------------------------------------------------------
|
|
167
154
|
/** Set of keys that TotopoYamlConfig allows (used to strip unknown fields). */
|
|
168
|
-
const KNOWN_KEYS = new Set(["
|
|
155
|
+
const KNOWN_KEYS = new Set(["workspace_id", "env_file", "shadow_paths", "profiles"]);
|
|
169
156
|
/**
|
|
170
157
|
* Attempt to repair an invalid totopo.yaml on disk.
|
|
171
158
|
* Strips unknown fields, fills missing required/optional fields from defaults,
|
|
@@ -192,10 +179,6 @@ export function repairTotopoYaml(dir) {
|
|
|
192
179
|
const fallbackId = slugifyForWorkspaceId(basename(dir));
|
|
193
180
|
const defaults = buildDefaultTotopoYaml(obj.workspace_id || fallbackId);
|
|
194
181
|
// Fill missing required fields
|
|
195
|
-
if (!("schema_version" in obj)) {
|
|
196
|
-
obj.schema_version = defaults.schema_version;
|
|
197
|
-
fixes.push("added missing schema_version");
|
|
198
|
-
}
|
|
199
182
|
if (!("workspace_id" in obj)) {
|
|
200
183
|
obj.workspace_id = defaults.workspace_id;
|
|
201
184
|
fixes.push(`added missing workspace_id ("${defaults.workspace_id}")`);
|
|
@@ -126,7 +126,6 @@ export function listWorkspaces() {
|
|
|
126
126
|
workspaceRoot: lockPath,
|
|
127
127
|
containerName: deriveContainerName(workspaceId),
|
|
128
128
|
workspaceDir: getWorkspaceDir(workspaceId),
|
|
129
|
-
displayName: yaml.name || workspaceId,
|
|
130
129
|
};
|
|
131
130
|
}
|
|
132
131
|
catch {
|
|
@@ -154,7 +153,6 @@ export function resolveWorkspace(fromPath) {
|
|
|
154
153
|
workspaceRoot: current,
|
|
155
154
|
containerName: deriveContainerName(yaml.workspace_id),
|
|
156
155
|
workspaceDir: getWorkspaceDir(yaml.workspace_id),
|
|
157
|
-
displayName: yaml.name || yaml.workspace_id,
|
|
158
156
|
};
|
|
159
157
|
}
|
|
160
158
|
// totopo.yaml found but workspace not initialized or lock mismatch - return null
|
package/package.json
CHANGED
|
@@ -3,14 +3,9 @@
|
|
|
3
3
|
"title": "totopo.yaml",
|
|
4
4
|
"description": "Configuration file for totopo — secure AI dev containers",
|
|
5
5
|
"type": "object",
|
|
6
|
-
"required": ["
|
|
6
|
+
"required": ["workspace_id"],
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"properties": {
|
|
9
|
-
"schema_version": {
|
|
10
|
-
"type": "integer",
|
|
11
|
-
"const": 3,
|
|
12
|
-
"description": "Schema version — must be 3"
|
|
13
|
-
},
|
|
14
9
|
"workspace_id": {
|
|
15
10
|
"type": "string",
|
|
16
11
|
"pattern": "^[a-z0-9][a-z0-9-]*[a-z0-9]$",
|
|
@@ -18,10 +13,6 @@
|
|
|
18
13
|
"maxLength": 48,
|
|
19
14
|
"description": "Unique workspace identifier. Used for container naming and cache directory. Lowercase alphanumeric and hyphens only."
|
|
20
15
|
},
|
|
21
|
-
"name": {
|
|
22
|
-
"type": "string",
|
|
23
|
-
"description": "Human-readable workspace name (shown in menus and welcome message)"
|
|
24
|
-
},
|
|
25
16
|
"env_file": {
|
|
26
17
|
"type": "string",
|
|
27
18
|
"description": "Path to env file relative to totopo.yaml (e.g. '.env'). Injected into container at runtime via --env-file."
|