totopo 3.3.1 → 3.3.2-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.
@@ -1,13 +1,14 @@
1
1
  // =========================================================================================================================================
2
2
  // src/commands/dev.ts - Start the dev container and connect via docker exec
3
- // In-memory Dockerfile build, profile selection, pattern-based shadows, env_file handling.
3
+ // In-memory Dockerfile build, profile selection, pattern-based shadows, env_file handling, runtime env injection.
4
4
  // =========================================================================================================================================
5
5
  import { spawnSync } from "node:child_process";
6
+ import { createHash } from "node:crypto";
6
7
  import { existsSync } from "node:fs";
7
8
  import { join, relative } from "node:path";
8
9
  import { cancel, confirm, isCancel, log, outro, select } from "@clack/prompts";
9
10
  import { buildAgentContextDocs, buildAgentMountArgs, injectAgentContext } from "../lib/agent-context.js";
10
- import { CONTAINER_STARTUP, CONTAINER_WORKSPACE, LABEL_MANAGED, LABEL_PROFILE, LABEL_SHADOWS, PROFILE } from "../lib/constants.js";
11
+ import { CONTAINER_STARTUP, CONTAINER_WORKSPACE, LABEL_MANAGED, LABEL_PROFILE, LABEL_RUNTIME_ENV, LABEL_SHADOWS, PROFILE, RUNTIME_ENV, } from "../lib/constants.js";
11
12
  import { buildDockerfile, buildImageWithTempfile } from "../lib/dockerfile-builder.js";
12
13
  import { isImageStale } from "../lib/migrate-to-latest.js";
13
14
  import { buildShadowMountArgs, ensureShadowsInSync, expandShadowPatterns } from "../lib/shadows.js";
@@ -63,13 +64,13 @@ async function selectProfile(ctx, profiles) {
63
64
  }
64
65
  // Returns null when the container does not exist (docker inspect exits non-zero).
65
66
  function inspectContainer(containerName) {
66
- const fmt = `{{.State.Status}}|{{index .Config.Labels "${LABEL_SHADOWS}"}}|{{index .Config.Labels "${LABEL_PROFILE}"}}`;
67
+ const fmt = `{{.State.Status}}|{{index .Config.Labels "${LABEL_SHADOWS}"}}|{{index .Config.Labels "${LABEL_PROFILE}"}}|{{index .Config.Labels "${LABEL_RUNTIME_ENV}"}}`;
67
68
  const result = spawnSync("docker", ["inspect", "--format", fmt, containerName], { encoding: "utf8", stdio: "pipe" });
68
69
  if (result.status !== 0)
69
70
  return null;
70
71
  const clean = (s) => (s === "<no value>" ? "" : s);
71
- const [status = "", shadows = "", profile = ""] = result.stdout.trim().split("|");
72
- return { status, shadowLabel: clean(shadows), profileLabel: clean(profile) };
72
+ const [status = "", shadows = "", profile = "", runtimeEnv = ""] = result.stdout.trim().split("|");
73
+ return { status, shadowLabel: clean(shadows), profileLabel: clean(profile), runtimeEnvLabel: clean(runtimeEnv) };
73
74
  }
74
75
  // --- Shadow label ------------------------------------------------------------------------------------------------------------------------
75
76
  function shadowLabel(paths) {
@@ -77,6 +78,17 @@ function shadowLabel(paths) {
77
78
  return "";
78
79
  return [...paths].sort().join(",");
79
80
  }
81
+ // --- Runtime env fingerprint -------------------------------------------------------------------------------------------------------------
82
+ function runtimeEnvLabel() {
83
+ const entries = Object.entries(RUNTIME_ENV);
84
+ if (entries.length === 0)
85
+ return "";
86
+ const sorted = entries
87
+ .map(([k, v]) => `${k}=${v}`)
88
+ .sort()
89
+ .join(",");
90
+ return createHash("sha256").update(sorted).digest("hex").slice(0, 12);
91
+ }
80
92
  // --- Stop and remove container -----------------------------------------------------------------------------------------------------------
81
93
  function stopAndRemoveContainer(containerName) {
82
94
  spawnSync("docker", ["stop", containerName], { stdio: "pipe" });
@@ -114,18 +126,25 @@ export function startContainer(opts) {
114
126
  `${LABEL_SHADOWS}=${shadowLabel(expandedShadows)}`,
115
127
  "--label",
116
128
  `${LABEL_PROFILE}=${activeProfile}`,
129
+ "--label",
130
+ `${LABEL_RUNTIME_ENV}=${runtimeEnvLabel()}`,
131
+ ];
132
+ // --- Runtime env vars -----------------------------------------------------------------------------------------------------------------
133
+ const runtimeEnvArgs = [
134
+ ...Object.entries(RUNTIME_ENV).flatMap(([k, v]) => ["-e", `${k}=${v}`]),
135
+ "-e",
136
+ `TOTOPO_WORKSPACE=${workspaceName}`,
117
137
  ];
118
- // --- Workspace identity env var ------------------------------------------------------------------------------------------------------
119
- const workspaceEnvArgs = ["-e", `TOTOPO_WORKSPACE=${workspaceName}`];
120
138
  // --- Inspect container state ---------------------------------------------------------------------------------------------------------
121
139
  const info = inspectContainer(containerName);
122
140
  let containerStatus = info?.status ?? null;
123
- // --- Check for shadow or profile mismatch --------------------------------------------------------------------------------------------
141
+ // --- Check for shadow, profile, or runtime env mismatch ------------------------------------------------------------------------------
124
142
  if (info !== null) {
125
143
  const expectedShadowLabel = shadowLabel(expandedShadows);
126
144
  const shadowChanged = info.shadowLabel !== expectedShadowLabel;
127
145
  const profileChanged = info.profileLabel !== activeProfile;
128
- if (shadowChanged || profileChanged) {
146
+ const runtimeEnvChanged = info.runtimeEnvLabel !== runtimeEnvLabel();
147
+ if (shadowChanged || profileChanged || runtimeEnvChanged) {
129
148
  stopAndRemoveContainer(containerName);
130
149
  containerStatus = null;
131
150
  if (profileChanged) {
@@ -134,10 +153,14 @@ export function startContainer(opts) {
134
153
  log.info(`Profile changed (${info.profileLabel} -> ${activeProfile}) — rebuilding...`);
135
154
  spawnSync("docker", ["rmi", containerName], { stdio: "pipe" });
136
155
  }
137
- else {
156
+ else if (shadowChanged) {
138
157
  if (!quiet)
139
158
  log.info("Shadow paths changed — recreating container...");
140
159
  }
160
+ else {
161
+ if (!quiet)
162
+ log.info("Runtime environment updated — recreating container...");
163
+ }
141
164
  }
142
165
  }
143
166
  if (containerStatus === null) {
@@ -163,7 +186,7 @@ export function startContainer(opts) {
163
186
  containerName,
164
187
  ...mountArgs,
165
188
  ...envFileArgs,
166
- ...workspaceEnvArgs,
189
+ ...runtimeEnvArgs,
167
190
  "--security-opt",
168
191
  "no-new-privileges:true",
169
192
  ...labelArgs,
@@ -28,8 +28,13 @@ 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
+ export const LABEL_RUNTIME_ENV = "totopo.runtime-env";
31
32
  // Built-in profile names (must match keys in buildDefaultTotopoYaml in totopo-yaml.ts)
32
33
  export const PROFILE = {
33
34
  default: "default",
34
35
  extended: "extended",
35
36
  };
37
+ // Runtime env vars injected into every container via docker run -e
38
+ export const RUNTIME_ENV = {
39
+ CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY: "1",
40
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "totopo",
3
- "version": "3.3.1",
3
+ "version": "3.3.2-rc-1",
4
4
  "description": "Run AI coding agents safely in your local codebase",
5
5
  "type": "module",
6
6
  "bin": {