infra-kit 0.1.114 → 0.1.117

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.
Files changed (204) hide show
  1. package/.eslintcache +1 -1
  2. package/.omc/state/agent-replay-1fcfe0ec-7567-41c3-9bfd-501e99e86819.jsonl +19 -0
  3. package/.omc/state/agent-replay-38382db5-3088-4b5a-aff7-2561a60827c7.jsonl +17 -0
  4. package/.omc/state/agent-replay-50c36be9-a3a2-42b9-9c85-5a06d3e22f6a.jsonl +5 -0
  5. package/.omc/state/agent-replay-8198b3e5-fad6-4ee0-9c59-ef773ad89af4.jsonl +1 -0
  6. package/.omc/state/agent-replay-8cd7652a-b8b5-49cf-9b1f-bee8e5972458.jsonl +3 -0
  7. package/.omc/state/agent-replay-9bea518f-a9ee-4d14-bb53-a936ebaf647c.jsonl +7 -0
  8. package/.omc/state/agent-replay-b52d89fb-0e24-4901-bc66-27e3c1697126.jsonl +2 -0
  9. package/.omc/state/agent-replay-f581a1ae-0ee4-48a3-96f3-ce31a0633bb0.jsonl +1 -0
  10. package/.omc/state/agent-replay-fe1057b4-9a25-4e8e-8c3f-3e11652ee697.jsonl +5 -0
  11. package/.omc/state/idle-notif-cooldown.json +1 -1
  12. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/last-tool-error-state.json +7 -0
  13. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/mission-state.json +109 -0
  14. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/pre-tool-advisory-throttle.json +42 -0
  15. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/ralph-state.json +13 -0
  16. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/skill-active-state.json +15 -0
  17. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/subagent-tracking-state.json +62 -0
  18. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/ultrawork-state.json +11 -0
  19. package/.omc/state/sessions/23d0ff82-17e5-428b-8743-38f7979bb6f3/last-tool-error-state.json +7 -0
  20. package/.omc/state/sessions/23d0ff82-17e5-428b-8743-38f7979bb6f3/pre-tool-advisory-throttle.json +18 -0
  21. package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/last-tool-error-state.json +7 -0
  22. package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/mission-state.json +145 -0
  23. package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/pre-tool-advisory-throttle.json +38 -0
  24. package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/ralplan-state.json +11 -0
  25. package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/subagent-tracking-state.json +98 -0
  26. package/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/last-tool-error-state.json +7 -0
  27. package/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/pre-tool-advisory-throttle.json +18 -0
  28. package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/mission-state.json +61 -0
  29. package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/pre-tool-advisory-throttle.json +22 -0
  30. package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/skill-active-state.json +15 -0
  31. package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/subagent-tracking-state.json +26 -0
  32. package/{src/lib/.omc/state/sessions/a60ac2ec-afbd-449f-a540-6df287392fc2 → .omc/state/sessions/8198b3e5-fad6-4ee0-9c59-ef773ad89af4}/pre-tool-advisory-throttle.json +3 -3
  33. package/.omc/state/sessions/8198b3e5-fad6-4ee0-9c59-ef773ad89af4/subagent-tracking-state.json +17 -0
  34. package/.omc/state/sessions/8cd7652a-b8b5-49cf-9b1f-bee8e5972458/pre-tool-advisory-throttle.json +18 -0
  35. package/.omc/state/sessions/8cd7652a-b8b5-49cf-9b1f-bee8e5972458/ralplan-state.json +11 -0
  36. package/.omc/state/sessions/8cd7652a-b8b5-49cf-9b1f-bee8e5972458/subagent-tracking-state.json +35 -0
  37. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/last-tool-error-state.json +7 -0
  38. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/mission-state.json +79 -0
  39. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/pre-tool-advisory-throttle.json +22 -0
  40. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/progress.txt +7 -0
  41. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/skill-active-state.json +15 -0
  42. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/subagent-tracking-state.json +26 -0
  43. package/.omc/state/sessions/b52d89fb-0e24-4901-bc66-27e3c1697126/pre-tool-advisory-throttle.json +14 -0
  44. package/.omc/state/sessions/b52d89fb-0e24-4901-bc66-27e3c1697126/subagent-tracking-state.json +17 -0
  45. package/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/pre-tool-advisory-throttle.json +18 -0
  46. package/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/pre-tool-advisory-throttle.json +18 -0
  47. package/.omc/state/sessions/f581a1ae-0ee4-48a3-96f3-ce31a0633bb0/pre-tool-advisory-throttle.json +18 -0
  48. package/.omc/state/sessions/f581a1ae-0ee4-48a3-96f3-ce31a0633bb0/subagent-tracking-state.json +17 -0
  49. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/last-tool-error-state.json +7 -0
  50. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/mission-state.json +61 -0
  51. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/prd.json +22 -0
  52. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/pre-tool-advisory-throttle.json +22 -0
  53. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/progress.txt +22 -0
  54. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/skill-active-state.json +15 -0
  55. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/subagent-tracking-state.json +26 -0
  56. package/.turbo/turbo-build.log +3 -3
  57. package/.turbo/turbo-eslint-check.log +2 -0
  58. package/.turbo/turbo-test.log +90 -42
  59. package/dist/boot-K4QUAFSP.js +2 -0
  60. package/dist/boot-K4QUAFSP.js.map +7 -0
  61. package/dist/chunk-FEGOOVZE.js +166 -0
  62. package/dist/chunk-FEGOOVZE.js.map +7 -0
  63. package/dist/chunk-WA4BQRDC.js +2 -0
  64. package/dist/chunk-WA4BQRDC.js.map +7 -0
  65. package/dist/cli.js +11 -107
  66. package/dist/cli.js.map +4 -4
  67. package/dist/entry/index.d.ts +2 -0
  68. package/dist/index.js +1 -1
  69. package/dist/index.js.map +4 -4
  70. package/dist/lib/vendor/config-schema.d.ts +58 -0
  71. package/dist/mcp.js +1 -54
  72. package/dist/mcp.js.map +4 -4
  73. package/eslint.config.js +33 -1
  74. package/package.json +5 -1
  75. package/scripts/build.js +11 -1
  76. package/src/.omc/state/agent-replay-c513ef38-cc3e-4a29-9c4c-1e0137245dd5.jsonl +4 -0
  77. package/src/.omc/state/agent-replay-ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a.jsonl +2 -0
  78. package/src/.omc/state/idle-notif-cooldown.json +1 -1
  79. package/src/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/mission-state.json +79 -0
  80. package/src/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/pre-tool-advisory-throttle.json +22 -0
  81. package/src/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/subagent-tracking-state.json +26 -0
  82. package/src/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/pre-tool-advisory-throttle.json +18 -0
  83. package/src/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/ralph-state.json +13 -0
  84. package/src/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/ultrawork-state.json +11 -0
  85. package/src/commands/config/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/last-tool-error-state.json +7 -0
  86. package/src/commands/config/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/pre-tool-advisory-throttle.json +18 -0
  87. package/src/commands/config/config.ts +7 -37
  88. package/src/commands/doctor/__tests__/agent-files.test.ts +8 -30
  89. package/src/commands/doctor/__tests__/check-legacy-user-global-config.test.ts +110 -0
  90. package/src/commands/doctor/doctor.ts +104 -44
  91. package/src/commands/env-autoload/env-autoload.ts +12 -0
  92. package/src/commands/env-autoload/index.ts +1 -0
  93. package/src/commands/env-clear/__tests__/env-clear.test.ts +27 -0
  94. package/src/commands/env-clear/env-clear.ts +22 -8
  95. package/src/commands/env-load/__tests__/env-load.test.ts +36 -1
  96. package/src/commands/env-load/env-load.ts +152 -35
  97. package/src/commands/env-load/index.ts +2 -1
  98. package/src/commands/env-status/__tests__/env-status.test.ts +78 -0
  99. package/src/commands/env-status/env-status.ts +19 -7
  100. package/src/commands/gh-release-deploy-all/gh-release-deploy-all.ts +45 -1
  101. package/src/commands/gh-release-deploy-selected/gh-release-deploy-selected.ts +46 -1
  102. package/src/commands/init/__tests__/agent-files.test.ts +145 -27
  103. package/src/commands/init/__tests__/migrate-config.test.ts +136 -5
  104. package/src/commands/init/__tests__/migrate-factory-config.test.ts +93 -0
  105. package/src/commands/init/__tests__/seed-user-global-config.test.ts +64 -0
  106. package/src/commands/init/__tests__/shell-body.test.ts +30 -0
  107. package/src/commands/init/agent-files.ts +65 -45
  108. package/src/commands/init/init.ts +140 -31
  109. package/src/commands/init/migrate-config.ts +160 -32
  110. package/src/commands/vendor-check/__tests__/runtime-isolation.test.ts +71 -0
  111. package/src/commands/vendor-check/__tests__/vendor-check.test.ts +87 -0
  112. package/src/commands/vendor-check/index.ts +1 -0
  113. package/src/commands/vendor-check/vendor-check.ts +127 -0
  114. package/src/commands/vendor-config/__tests__/vendor-config.test.ts +103 -0
  115. package/src/commands/vendor-config/index.ts +1 -0
  116. package/src/commands/vendor-config/vendor-config.ts +162 -0
  117. package/src/commands/vendor-diff/__tests__/vendor-diff.test.ts +74 -0
  118. package/src/commands/vendor-diff/index.ts +1 -0
  119. package/src/commands/vendor-diff/vendor-diff.ts +77 -0
  120. package/src/commands/vendor-manifest/index.ts +1 -0
  121. package/src/commands/vendor-manifest/vendor-manifest.ts +60 -0
  122. package/src/commands/vendor-sync/__tests__/vendor-write.test.ts +86 -0
  123. package/src/commands/vendor-sync/index.ts +1 -0
  124. package/src/commands/vendor-sync/vendor-sync.ts +90 -0
  125. package/src/commands/worktrees-add/worktrees-add.ts +1 -1
  126. package/src/commands/worktrees-reload/worktrees-reload.ts +6 -5
  127. package/src/commands/worktrees-remove/worktrees-remove.ts +10 -1
  128. package/src/commands/worktrees-sync/worktrees-sync.ts +9 -1
  129. package/src/entry/cli.ts +417 -210
  130. package/src/entry/index.ts +7 -1
  131. package/src/integrations/cmux/__tests__/open-workspace-with-layout.test.ts +127 -0
  132. package/src/integrations/cmux/close-workspace-by-title.ts +2 -2
  133. package/src/integrations/cmux/list-workspace-titles.ts +1 -1
  134. package/src/integrations/cmux/open-workspace-with-layout.ts +15 -4
  135. package/src/integrations/cursor/open-cursor-workspace.ts +6 -11
  136. package/src/integrations/doppler/__tests__/doppler-errors.test.ts +76 -0
  137. package/src/integrations/doppler/doppler-cli-auth.ts +12 -4
  138. package/src/integrations/doppler/doppler-errors.ts +88 -0
  139. package/src/integrations/doppler/doppler-project.ts +80 -0
  140. package/src/integrations/doppler/index.ts +3 -1
  141. package/src/integrations/gh/gh-cli-auth/gh-cli-auth.ts +9 -5
  142. package/src/integrations/gh/gh-release-prs/gh-release-prs.ts +16 -8
  143. package/src/integrations/ide/__tests__/ide-facade.test.ts +49 -10
  144. package/src/integrations/ide/open-ide-workspace.ts +6 -6
  145. package/src/integrations/ide/remove-ide-worktree-folders.ts +95 -30
  146. package/src/integrations/ide/types.ts +10 -1
  147. package/src/integrations/zed/__tests__/open-zed-workspace.test.ts +1 -17
  148. package/src/integrations/zed/__tests__/reuse-zed-workspace.test.ts +86 -0
  149. package/src/integrations/zed/index.ts +1 -0
  150. package/src/integrations/zed/open-zed-workspace.ts +4 -10
  151. package/src/integrations/zed/reuse-zed-workspace.ts +57 -0
  152. package/src/lib/command-catalog/__tests__/__snapshots__/command-catalog.test.ts.snap +212 -0
  153. package/src/lib/command-catalog/__tests__/command-catalog.test.ts +193 -0
  154. package/src/lib/command-catalog/command-catalog.ts +141 -0
  155. package/src/lib/command-catalog/index.ts +2 -0
  156. package/src/lib/command-echo/__tests__/command-echo.test.ts +127 -0
  157. package/src/lib/constants/constants.ts +13 -0
  158. package/src/lib/constants/index.ts +2 -0
  159. package/src/lib/env-autoload/__tests__/env-autoload.test.ts +288 -0
  160. package/src/lib/env-autoload/env-autoload.ts +294 -0
  161. package/src/lib/env-autoload/index.ts +9 -0
  162. package/src/lib/errors/operation-error.ts +1 -1
  163. package/src/lib/infra-kit-config/__tests__/env-autoload-schema.test.ts +71 -0
  164. package/src/lib/infra-kit-config/__tests__/infra-kit-config.test.ts +77 -6
  165. package/src/lib/infra-kit-config/index.ts +9 -1
  166. package/src/lib/infra-kit-config/infra-kit-config.ts +71 -11
  167. package/src/lib/json-output/__tests__/json-output.test.ts +139 -0
  168. package/src/lib/json-output/index.ts +1 -0
  169. package/src/lib/json-output/json-output.ts +47 -0
  170. package/src/lib/managed-block/managed-block.ts +4 -4
  171. package/src/lib/package-config/package-config.ts +1 -3
  172. package/src/lib/path-display/index.ts +1 -0
  173. package/src/lib/path-display/path-display.ts +42 -0
  174. package/src/lib/render/__tests__/render.test.ts +48 -0
  175. package/src/lib/render/index.ts +1 -0
  176. package/src/lib/render/render.ts +24 -0
  177. package/src/lib/vendor/__tests__/config-schema.test.ts +31 -0
  178. package/src/lib/vendor/__tests__/config.test.ts +63 -0
  179. package/src/lib/vendor/__tests__/factory-config.test.ts +92 -0
  180. package/src/lib/vendor/__tests__/vendor-lib.test.ts +158 -0
  181. package/src/lib/vendor/__tests__/zx-mock.ts +32 -0
  182. package/src/lib/vendor/config-schema.ts +78 -0
  183. package/src/lib/vendor/config.ts +58 -0
  184. package/src/lib/vendor/factory-config-schema.ts +35 -0
  185. package/src/lib/vendor/factory-config.ts +91 -0
  186. package/src/lib/vendor/hash.ts +16 -0
  187. package/src/lib/vendor/index.ts +23 -0
  188. package/src/lib/vendor/manifest.ts +151 -0
  189. package/src/lib/vendor/skip-sets.ts +48 -0
  190. package/src/lib/vendor/sync-ops.ts +290 -0
  191. package/src/lib/vendor/walk.ts +56 -0
  192. package/src/mcp/tools/index.ts +5 -42
  193. package/src/sanity.test.tsx +1 -0
  194. package/src/tui/__tests__/no-react-boundary.test.ts +51 -0
  195. package/src/tui/boot.tsx +45 -0
  196. package/src/tui/screens/__tests__/command-palette.test.tsx +136 -0
  197. package/src/tui/screens/command-palette.tsx +159 -0
  198. package/src/tui/types.ts +8 -0
  199. package/tsconfig.json +1 -0
  200. package/tsconfig.tsbuildinfo +1 -1
  201. package/vitest.config.ts +8 -0
  202. package/src/commands/worktrees-open/__tests__/open-cmux.test.ts +0 -96
  203. package/src/commands/worktrees-open/index.ts +0 -1
  204. package/src/commands/worktrees-open/worktrees-open.ts +0 -195
@@ -4,13 +4,9 @@ import path from 'node:path'
4
4
  import { z } from 'zod'
5
5
  import { $ } from 'zx'
6
6
 
7
- import {
8
- AGENTS_IMPORT_END,
9
- AGENTS_IMPORT_START,
10
- AGENTS_MARKER_END,
11
- AGENTS_MARKER_START,
12
- } from 'src/commands/init/agent-files'
7
+ import { AGENTS_MARKER_END, AGENTS_MARKER_START } from 'src/commands/init/agent-files'
13
8
  import { MARKER_END, MARKER_START, buildShellBlock } from 'src/commands/init/init'
9
+ import { getDopplerProject, listDopplerProjects } from 'src/integrations/doppler'
14
10
  import { getProjectRoot } from 'src/lib/git-utils/git-utils'
15
11
  import {
16
12
  getInfraKitConfig,
@@ -20,6 +16,7 @@ import {
20
16
  } from 'src/lib/infra-kit-config'
21
17
  import { logger } from 'src/lib/logger'
22
18
  import { hasManagedBlock } from 'src/lib/managed-block'
19
+ import { tildify } from 'src/lib/path-display'
23
20
  import { defineMcpTool, textContent } from 'src/types'
24
21
 
25
22
  interface CheckResult {
@@ -89,7 +86,7 @@ const checkPnpmWorkspaceVirtualStore = async (): Promise<CheckResult> => {
89
86
  }
90
87
 
91
88
  const content = fs.readFileSync(yamlPath, 'utf-8')
92
- // eslint-disable-next-line sonarjs/slow-regex
89
+ // eslint-disable-next-line sonarjs/super-linear-regex
93
90
  const enabled = /^\s*enableGlobalVirtualStore\s*:\s*true\s*$/m.test(content)
94
91
 
95
92
  if (!enabled) {
@@ -127,6 +124,44 @@ const checkInfraKitConfigValid = async (): Promise<CheckResult> => {
127
124
  }
128
125
  }
129
126
 
127
+ /**
128
+ * Verify the Doppler project configured in infra-kit.json (`envManagement.config.name`)
129
+ * actually exists in the authenticated account — the proactive catch for the
130
+ * mismatch that otherwise only surfaces as a cryptic `env-load` failure. Skips
131
+ * (informational pass) when the config can't be read or the project list can't be
132
+ * fetched (auth/network), so a logged-out user isn't misdiagnosed as
133
+ * missing-project — the adjacent `doppler authenticated` check owns that failure.
134
+ */
135
+ export const checkDopplerProjectExists = async (): Promise<CheckResult> => {
136
+ const name = 'doppler project exists'
137
+
138
+ let project: string
139
+
140
+ try {
141
+ project = await getDopplerProject()
142
+ } catch {
143
+ return { name, status: 'pass', message: 'Skipped — infra-kit config could not be read (see config check)' }
144
+ }
145
+
146
+ const projects = await listDopplerProjects()
147
+
148
+ if (projects === null) {
149
+ return { name, status: 'pass', message: 'Skipped — could not list Doppler projects (see doppler auth check)' }
150
+ }
151
+
152
+ if (projects.includes(project)) {
153
+ return { name, status: 'pass', message: `Doppler project "${project}" exists` }
154
+ }
155
+
156
+ return {
157
+ name,
158
+ status: 'fail',
159
+ message: `Doppler project "${project}" not found (set in infra-kit.json → envManagement.config.name). Available: ${
160
+ projects.length > 0 ? projects.join(', ') : 'none'
161
+ }`,
162
+ }
163
+ }
164
+
130
165
  /**
131
166
  * Surface where this developer's user-scope override file would live and
132
167
  * whether it has been created. Always passes — informational only — so the
@@ -160,6 +195,54 @@ const checkUserOverridePath = async (): Promise<CheckResult> => {
160
195
  }
161
196
  }
162
197
 
198
+ /**
199
+ * Surface a lingering legacy `~/.infra-kit/config.json` left behind after the
200
+ * user-global config was renamed to `infra-kit.json`. The on-`init` auto-rename
201
+ * is the migration path, but a user who edits the file and never re-runs `init`
202
+ * would silently have their overrides stop applying — this makes that visible.
203
+ *
204
+ * Branches on whether the canonical `infra-kit.json` already exists so the
205
+ * message is accurate: only the "no infra-kit.json yet" case means overrides are
206
+ * not being applied; when both exist the legacy file is merely stale.
207
+ * Informational pass (no warning) when the paths can't resolve — mirrors
208
+ * {@link checkIdeInstalled}: nothing to surface if there's no resolvable target.
209
+ *
210
+ * @example
211
+ * await checkLegacyUserGlobalConfig()
212
+ * // { name: 'legacy user-global config', status: 'fail', message: 'Legacy user-global config.json found …' }
213
+ */
214
+ export const checkLegacyUserGlobalConfig = async (): Promise<CheckResult> => {
215
+ const name = 'legacy user-global config'
216
+
217
+ let paths: Awaited<ReturnType<typeof getInfraKitConfigPaths>>
218
+
219
+ try {
220
+ paths = await getInfraKitConfigPaths()
221
+ } catch {
222
+ return { name, status: 'pass', message: 'Skipped — infra-kit config paths could not be resolved' }
223
+ }
224
+
225
+ const legacyPath = path.join(path.dirname(paths.userGlobal), 'config.json')
226
+
227
+ if (!fs.existsSync(legacyPath)) {
228
+ return { name, status: 'pass', message: 'No legacy user-global config.json' }
229
+ }
230
+
231
+ if (!fs.existsSync(paths.userGlobal)) {
232
+ return {
233
+ name,
234
+ status: 'fail',
235
+ message: `Legacy user-global config.json found at ${tildify(legacyPath)} — run \`infra-kit init\` to migrate it (your overrides are not being applied).`,
236
+ }
237
+ }
238
+
239
+ return {
240
+ name,
241
+ status: 'fail',
242
+ message: `Stale legacy config.json found at ${tildify(legacyPath)} — ${tildify(paths.userGlobal)} is active; remove the old file.`,
243
+ }
244
+ }
245
+
163
246
  interface IdeProbe {
164
247
  ok: boolean
165
248
  label: string
@@ -300,10 +383,11 @@ const checkRtkConfigured = async (): Promise<CheckResult> => {
300
383
  }
301
384
 
302
385
  /**
303
- * Check that the repo agent-instruction files managed by `infra-kit init` exist:
304
- * the `AGENTS.md` block, the `@AGENTS.md` import region in `CLAUDE.md`, and the
305
- * `.cursor/rules` block. Presence only. Repo-gated: returns no checks when run
306
- * outside an infra-kit repo so doctor never crashes there.
386
+ * Check that the repo agent-instruction guidance managed by `infra-kit init` exists:
387
+ * the guidance block in `CLAUDE.md`. Presence only. Repo-gated: returns no checks
388
+ * when run outside an infra-kit repo so doctor never crashes there. A repo that
389
+ * predates the AGENTS.md→CLAUDE.md migration will report this check as failing until
390
+ * `infra-kit init` is re-run.
307
391
  */
308
392
  export const checkAgentFiles = async (): Promise<CheckResult[]> => {
309
393
  let mainConfigPath: string
@@ -316,43 +400,17 @@ export const checkAgentFiles = async (): Promise<CheckResult[]> => {
316
400
 
317
401
  if (!fs.existsSync(mainConfigPath)) return []
318
402
 
319
- const root = path.dirname(mainConfigPath)
320
-
321
- const blockPresent = (relPath: string, start: string, end: string): boolean => {
322
- const filePath = path.join(root, relPath)
323
- const content = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : ''
403
+ const claudePath = path.join(path.dirname(mainConfigPath), 'CLAUDE.md')
404
+ const content = fs.existsSync(claudePath) ? fs.readFileSync(claudePath, 'utf-8') : ''
405
+ const present = hasManagedBlock(content, AGENTS_MARKER_START, AGENTS_MARKER_END)
324
406
 
325
- return hasManagedBlock(content, start, end)
326
- }
327
-
328
- const entries = [
329
- {
330
- name: 'AGENTS.md block',
331
- present: blockPresent('AGENTS.md', AGENTS_MARKER_START, AGENTS_MARKER_END),
332
- okMessage: 'AGENTS.md block present',
333
- missingMessage: 'infra-kit block missing from AGENTS.md. Run: infra-kit init',
334
- },
335
- {
336
- name: 'CLAUDE.md import',
337
- present: blockPresent('CLAUDE.md', AGENTS_IMPORT_START, AGENTS_IMPORT_END),
338
- okMessage: 'CLAUDE.md imports @AGENTS.md',
339
- missingMessage: '@AGENTS.md import block missing from CLAUDE.md. Run: infra-kit init',
340
- },
407
+ return [
341
408
  {
342
- name: '.cursor/rules block',
343
- present: blockPresent(path.join('.cursor', 'rules', 'infra-kit.mdc'), AGENTS_MARKER_START, AGENTS_MARKER_END),
344
- okMessage: '.cursor/rules/infra-kit.mdc block present',
345
- missingMessage: '.cursor/rules/infra-kit.mdc block missing. Run: infra-kit init',
409
+ name: 'CLAUDE.md block',
410
+ status: present ? 'pass' : 'fail',
411
+ message: present ? 'CLAUDE.md block present' : 'infra-kit block missing from CLAUDE.md. Run: infra-kit init',
346
412
  },
347
413
  ]
348
-
349
- return entries.map((entry): CheckResult => {
350
- return {
351
- name: entry.name,
352
- status: entry.present ? 'pass' : 'fail',
353
- message: entry.present ? entry.okMessage : entry.missingMessage,
354
- }
355
- })
356
414
  }
357
415
 
358
416
  /**
@@ -413,7 +471,9 @@ export const doctor = async () => {
413
471
  Promise.resolve(checkZshrcInitialized()),
414
472
  checkPnpmWorkspaceVirtualStore(),
415
473
  checkInfraKitConfigValid(),
474
+ checkDopplerProjectExists(),
416
475
  checkUserOverridePath(),
476
+ checkLegacyUserGlobalConfig(),
417
477
  checkIdeInstalled(),
418
478
  ])
419
479
 
@@ -0,0 +1,12 @@
1
+ import { runEnvAutoLoad } from 'src/lib/env-autoload'
2
+
3
+ /**
4
+ * Internal command invoked (backgrounded) by the `infra-kit init` shell-startup
5
+ * integration. Runs the 'shell-startup' trigger, writing env-load.sh when
6
+ * envAutoLoad is configured for it + eligible; the shell precmd hook sources it on
7
+ * a subsequent prompt. Intentionally writes NOTHING to stdout and never throws —
8
+ * auto-load must never disrupt shell startup.
9
+ */
10
+ export const envAutoload = async (): Promise<void> => {
11
+ await runEnvAutoLoad({ expectedTrigger: 'shell-startup' })
12
+ }
@@ -0,0 +1 @@
1
+ export { envAutoload } from './env-autoload'
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { buildEnvClearLines } from '../env-clear'
4
+
5
+ describe('buildEnvClearLines', () => {
6
+ it('unsets every loaded var name passed in', () => {
7
+ const lines = buildEnvClearLines(['FOO', 'BAR'])
8
+
9
+ expect(lines).toContain('unset FOO')
10
+ expect(lines).toContain('unset BAR')
11
+ })
12
+
13
+ it('unsets the session metadata vars and the auto-load marker', () => {
14
+ const lines = buildEnvClearLines([])
15
+
16
+ expect(lines).toContain('unset INFRA_KIT_ENV_CONFIG')
17
+ expect(lines).toContain('unset INFRA_KIT_ENV_PROJECT')
18
+ expect(lines).toContain('unset INFRA_KIT_ENV_LOADED_AT')
19
+ expect(lines).toContain('unset INFRA_KIT_ENV_AUTOLOADED')
20
+ })
21
+
22
+ it('exports the clear sentinel so cli-invocation auto-load stays suppressed', () => {
23
+ const lines = buildEnvClearLines([])
24
+
25
+ expect(lines).toContain("export INFRA_KIT_ENV_CLEARED='1'")
26
+ })
27
+ })
@@ -6,6 +6,8 @@ import { z } from 'zod'
6
6
  import {
7
7
  ENV_CLEAR_FILE,
8
8
  ENV_LOAD_FILE,
9
+ INFRA_KIT_ENV_AUTOLOADED_VAR,
10
+ INFRA_KIT_ENV_CLEARED_VAR,
9
11
  INFRA_KIT_ENV_CONFIG_VAR,
10
12
  INFRA_KIT_ENV_LOADED_AT_VAR,
11
13
  INFRA_KIT_ENV_PROJECT_VAR,
@@ -15,6 +17,25 @@ import {
15
17
  } from 'src/lib/constants'
16
18
  import { defineMcpTool, textContent } from 'src/types'
17
19
 
20
+ /**
21
+ * Build the lines for env-clear.sh: `unset` every loaded var plus the session
22
+ * metadata (config/project/loadedAt) and the auto-load marker, then EXPORT the
23
+ * clear sentinel so cli-invocation auto-load stays suppressed in this shell until
24
+ * an explicit `env-load` (which unsets it) or a new shell. Pure for testability.
25
+ */
26
+ export const buildEnvClearLines = (varNames: string[]): string[] => {
27
+ return [
28
+ ...varNames.map((v) => {
29
+ return `unset ${v}`
30
+ }),
31
+ `unset ${INFRA_KIT_ENV_CONFIG_VAR}`,
32
+ `unset ${INFRA_KIT_ENV_PROJECT_VAR}`,
33
+ `unset ${INFRA_KIT_ENV_LOADED_AT_VAR}`,
34
+ `unset ${INFRA_KIT_ENV_AUTOLOADED_VAR}`,
35
+ `export ${INFRA_KIT_ENV_CLEARED_VAR}='1'`,
36
+ ]
37
+ }
38
+
18
39
  /**
19
40
  * Clear loaded env vars. Prints a file path to stdout that must be sourced to apply.
20
41
  * The env-clear shell alias does this automatically. Throws when no env is loaded
@@ -30,14 +51,7 @@ export const envClear = async () => {
30
51
 
31
52
  const varNames = parseVarNamesFromEnvFile(envLoadPath)
32
53
 
33
- const unsetLines = [
34
- ...varNames.map((v) => {
35
- return `unset ${v}`
36
- }),
37
- `unset ${INFRA_KIT_ENV_CONFIG_VAR}`,
38
- `unset ${INFRA_KIT_ENV_PROJECT_VAR}`,
39
- `unset ${INFRA_KIT_ENV_LOADED_AT_VAR}`,
40
- ]
54
+ const unsetLines = buildEnvClearLines(varNames)
41
55
 
42
56
  const clearFilePath = path.resolve(cacheDir, ENV_CLEAR_FILE)
43
57
 
@@ -1,6 +1,12 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
 
3
- import { DOPPLER_MAX_OUTPUT_BYTES, assertDopplerOutputSize, assertValidEnvContent, shellSingleQuote } from '../env-load'
3
+ import {
4
+ DOPPLER_MAX_OUTPUT_BYTES,
5
+ assertDopplerOutputSize,
6
+ assertValidEnvContent,
7
+ buildEnvLoadFileLines,
8
+ shellSingleQuote,
9
+ } from '../env-load'
4
10
 
5
11
  describe('shellSingleQuote', () => {
6
12
  it('wraps plain values in single quotes', () => {
@@ -130,3 +136,32 @@ describe('assertDopplerOutputSize', () => {
130
136
  }).toThrow(/unexpectedly large output/)
131
137
  })
132
138
  })
139
+
140
+ describe('buildEnvLoadFileLines', () => {
141
+ const args = { envContent: 'A=1\nB=2', config: 'dev', project: 'proj', loadedAt: '2026-01-01T00:00:00.000Z' }
142
+
143
+ it('wraps the body in set -a / set +a and records config/project/loadedAt', () => {
144
+ const lines = buildEnvLoadFileLines({ ...args, autoLoaded: false })
145
+
146
+ expect(lines[0]).toBe('set -a')
147
+ expect(lines.at(-1)).toBe('set +a')
148
+ expect(lines).toContain("INFRA_KIT_ENV_CONFIG='dev'")
149
+ expect(lines).toContain("INFRA_KIT_ENV_PROJECT='proj'")
150
+ expect(lines).toContain("INFRA_KIT_ENV_LOADED_AT='2026-01-01T00:00:00.000Z'")
151
+ })
152
+
153
+ it('writes the AUTOLOADED marker only when autoLoaded is true', () => {
154
+ const lines = buildEnvLoadFileLines({ ...args, autoLoaded: true })
155
+
156
+ expect(lines).toContain("INFRA_KIT_ENV_AUTOLOADED='1'")
157
+ expect(lines).not.toContain('unset INFRA_KIT_ENV_AUTOLOADED')
158
+ })
159
+
160
+ it('drops the marker and lifts the clear sentinel on a manual load (autoLoaded false)', () => {
161
+ const lines = buildEnvLoadFileLines({ ...args, autoLoaded: false })
162
+
163
+ expect(lines).toContain('unset INFRA_KIT_ENV_AUTOLOADED')
164
+ expect(lines).toContain('unset INFRA_KIT_ENV_CLEARED')
165
+ expect(lines).not.toContain("INFRA_KIT_ENV_AUTOLOADED='1'")
166
+ })
167
+ })
@@ -6,18 +6,27 @@ import process from 'node:process'
6
6
  import { z } from 'zod'
7
7
  import { $ } from 'zx'
8
8
 
9
- import { validateDopplerCliAndAuth } from 'src/integrations/doppler'
10
- import { getDopplerProject } from 'src/integrations/doppler/doppler-project'
9
+ import {
10
+ buildDopplerNotFoundMessage,
11
+ classifyDopplerDownloadError,
12
+ getDopplerProject,
13
+ listDopplerConfigs,
14
+ listDopplerProjects,
15
+ validateDopplerCliAndAuth,
16
+ } from 'src/integrations/doppler'
11
17
  import { commandEcho } from 'src/lib/command-echo'
12
18
  import {
13
19
  ENV_LOAD_FILE,
14
20
  ENV_VAR_LINE_PATTERN,
21
+ INFRA_KIT_ENV_AUTOLOADED_VAR,
22
+ INFRA_KIT_ENV_CLEARED_VAR,
15
23
  INFRA_KIT_ENV_CONFIG_VAR,
16
24
  INFRA_KIT_ENV_LOADED_AT_VAR,
17
25
  INFRA_KIT_ENV_PROJECT_VAR,
18
26
  atomicWriteFileSync,
19
27
  getSessionCacheDir,
20
28
  } from 'src/lib/constants'
29
+ import { extractStderr } from 'src/lib/errors/operation-error'
21
30
  import { getInfraKitConfig } from 'src/lib/infra-kit-config'
22
31
  import { defineMcpTool, textContent } from 'src/types'
23
32
 
@@ -25,12 +34,112 @@ interface EnvLoadArgs {
25
34
  config?: string
26
35
  }
27
36
 
37
+ interface WriteEnvLoadFileArgs {
38
+ /** Resolved Doppler config / environment name (no interactive picker here). */
39
+ config: string
40
+ /**
41
+ * Marks the produced file as auto-loaded. `true` writes the
42
+ * INFRA_KIT_ENV_AUTOLOADED marker; `false` (a manual load) instead unsets the
43
+ * marker and lifts any clear suppression so a deliberate load wins.
44
+ */
45
+ autoLoaded?: boolean
46
+ }
47
+
48
+ export interface EnvLoadFileResult {
49
+ filePath: string
50
+ variableCount: number
51
+ project: string
52
+ config: string
53
+ }
54
+
55
+ interface EnvLoadFileLinesArgs {
56
+ envContent: string
57
+ config: string
58
+ project: string
59
+ loadedAt: string
60
+ autoLoaded: boolean
61
+ }
62
+
28
63
  /**
29
- * Load environment variables from Doppler for the given config
64
+ * The auto-load marker lines appended to env-load.sh. Pure so the marker policy
65
+ * is unit-testable without touching Doppler or the filesystem.
66
+ *
67
+ * @example
68
+ * buildAutoLoadMarkerLines(true) // => ["INFRA_KIT_ENV_AUTOLOADED='1'"]
69
+ * buildAutoLoadMarkerLines(false) // => ['unset INFRA_KIT_ENV_AUTOLOADED', 'unset INFRA_KIT_ENV_CLEARED']
30
70
  */
31
- export const envLoad = async (args: EnvLoadArgs) => {
71
+ const buildAutoLoadMarkerLines = (autoLoaded: boolean): string[] => {
72
+ if (autoLoaded) {
73
+ return [`${INFRA_KIT_ENV_AUTOLOADED_VAR}=${shellSingleQuote('1')}`]
74
+ }
75
+
76
+ // Manual load: drop any auto marker so auto-load never re-clobbers a deliberate
77
+ // choice, and lift a prior clear suppression so the manual load takes effect.
78
+ return [`unset ${INFRA_KIT_ENV_AUTOLOADED_VAR}`, `unset ${INFRA_KIT_ENV_CLEARED_VAR}`]
79
+ }
80
+
81
+ /**
82
+ * Build the dotenv-format shell lines for env-load.sh. Pure (no I/O) so callers
83
+ * can assert the exact marker behavior. `set -a`/`set +a` auto-export every
84
+ * assignment when the file is sourced.
85
+ */
86
+ export const buildEnvLoadFileLines = ({
87
+ envContent,
88
+ config,
89
+ project,
90
+ loadedAt,
91
+ autoLoaded,
92
+ }: EnvLoadFileLinesArgs): string[] => {
93
+ return [
94
+ 'set -a',
95
+ envContent,
96
+ `${INFRA_KIT_ENV_CONFIG_VAR}=${shellSingleQuote(config)}`,
97
+ `${INFRA_KIT_ENV_PROJECT_VAR}=${shellSingleQuote(project)}`,
98
+ `${INFRA_KIT_ENV_LOADED_AT_VAR}=${shellSingleQuote(loadedAt)}`,
99
+ ...buildAutoLoadMarkerLines(autoLoaded),
100
+ 'set +a',
101
+ ]
102
+ }
103
+
104
+ /**
105
+ * Download Doppler secrets for a resolved config and atomically write env-load.sh
106
+ * to the session cache dir. Does NOT print to stdout — shared by the CLI/MCP
107
+ * `envLoad` entry (which prints the path) and the auto-load path (which lets the
108
+ * shell precmd hook source the file).
109
+ */
110
+ export const writeEnvLoadFile = async ({
111
+ config,
112
+ autoLoaded = false,
113
+ }: WriteEnvLoadFileArgs): Promise<EnvLoadFileResult> => {
32
114
  await validateDopplerCliAndAuth()
33
115
 
116
+ const project = await getDopplerProject()
117
+
118
+ const envContent = await downloadDopplerSecrets(project, config)
119
+
120
+ assertValidEnvContent(envContent)
121
+
122
+ const loadedAt = new Date().toISOString()
123
+ const envFileLines = buildEnvLoadFileLines({ envContent, config, project, loadedAt, autoLoaded })
124
+
125
+ const cacheDir = getSessionCacheDir()
126
+ const envFilePath = path.resolve(cacheDir, ENV_LOAD_FILE)
127
+
128
+ fs.mkdirSync(cacheDir, { recursive: true, mode: 0o700 })
129
+ atomicWriteFileSync(envFilePath, `${envFileLines.join('\n')}\n`, 0o600)
130
+
131
+ return {
132
+ filePath: envFilePath,
133
+ variableCount: countEnvVarLines(envContent),
134
+ project,
135
+ config,
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Load environment variables from Doppler for the given config
141
+ */
142
+ export const envLoad = async (args: EnvLoadArgs) => {
34
143
  const { config } = args
35
144
 
36
145
  commandEcho.start('env-load')
@@ -40,6 +149,11 @@ export const envLoad = async (args: EnvLoadArgs) => {
40
149
  if (config) {
41
150
  selectedConfig = config
42
151
  } else {
152
+ // Validate auth before the interactive picker so an unauthenticated user fails
153
+ // fast instead of choosing an env first. writeEnvLoadFile re-checks (cheap) for
154
+ // the non-interactive path where this branch is skipped.
155
+ await validateDopplerCliAndAuth()
156
+
43
157
  const { environments } = await getInfraKitConfig()
44
158
 
45
159
  commandEcho.setInteractive()
@@ -58,43 +172,21 @@ export const envLoad = async (args: EnvLoadArgs) => {
58
172
 
59
173
  commandEcho.addOption('--config', selectedConfig)
60
174
 
61
- const project = await getDopplerProject()
62
-
63
- const envContent = await downloadDopplerSecrets(project, selectedConfig)
64
-
65
- assertValidEnvContent(envContent)
66
-
67
- // Build env file content in dotenv format
68
- const loadedAt = new Date().toISOString()
69
- const envFileLines = [
70
- 'set -a',
71
- envContent,
72
- `${INFRA_KIT_ENV_CONFIG_VAR}=${shellSingleQuote(selectedConfig)}`,
73
- `${INFRA_KIT_ENV_PROJECT_VAR}=${shellSingleQuote(project)}`,
74
- `${INFRA_KIT_ENV_LOADED_AT_VAR}=${shellSingleQuote(loadedAt)}`,
75
- 'set +a',
76
- ]
77
-
78
- const cacheDir = getSessionCacheDir()
79
- const envFilePath = path.resolve(cacheDir, ENV_LOAD_FILE)
80
-
81
- fs.mkdirSync(cacheDir, { recursive: true, mode: 0o700 })
82
- atomicWriteFileSync(envFilePath, `${envFileLines.join('\n')}\n`, 0o600)
175
+ // A manual load is authoritative: autoLoaded=false drops the auto marker.
176
+ const result = await writeEnvLoadFile({ config: selectedConfig, autoLoaded: false })
83
177
 
84
178
  // REQUIRED
85
- process.stdout.write(`${envFilePath}\n`)
179
+ process.stdout.write(`${result.filePath}\n`)
86
180
 
87
181
  // Logs to stderr (pino → pretty-print), so it doesn't pollute the captured
88
182
  // file path that the shell wrapper reads from stdout.
89
183
  commandEcho.print()
90
184
 
91
- const varCount = countEnvVarLines(envContent)
92
-
93
185
  const structuredContent = {
94
- filePath: envFilePath,
95
- variableCount: varCount,
96
- project,
97
- config: selectedConfig,
186
+ filePath: result.filePath,
187
+ variableCount: result.variableCount,
188
+ project: result.project,
189
+ config: result.config,
98
190
  }
99
191
 
100
192
  return {
@@ -122,10 +214,15 @@ const downloadDopplerSecrets = async (project: string, config: string): Promise<
122
214
 
123
215
  $.quiet = true
124
216
  try {
125
- const result =
126
- await $`doppler secrets download --no-file --format env --project ${project} --config ${config}`.timeout(
217
+ let result
218
+
219
+ try {
220
+ result = await $`doppler secrets download --no-file --format env --project ${project} --config ${config}`.timeout(
127
221
  DOPPLER_DOWNLOAD_TIMEOUT_MS,
128
222
  )
223
+ } catch (error: unknown) {
224
+ throw await translateDopplerDownloadError(error, project, config)
225
+ }
129
226
 
130
227
  assertDopplerOutputSize(result.stdout)
131
228
 
@@ -135,6 +232,26 @@ const downloadDopplerSecrets = async (project: string, config: string): Promise<
135
232
  }
136
233
  }
137
234
 
235
+ /**
236
+ * Turn a raw `doppler secrets download` failure into an actionable error. A
237
+ * recognized project/config not-found is enriched (on this error path only) with
238
+ * the list of available names and rethrown as a clean `Error` pointing at the
239
+ * exact infra-kit.json field. Anything else — auth, network, timeout — is
240
+ * rethrown untouched so it degrades to the existing behavior, never worse.
241
+ */
242
+ const translateDopplerDownloadError = async (error: unknown, project: string, config: string): Promise<Error> => {
243
+ const stderr = extractStderr(error) ?? (error instanceof Error ? error.message : String(error))
244
+ const kind = classifyDopplerDownloadError(stderr)
245
+
246
+ if (kind === 'unknown') {
247
+ return error instanceof Error ? error : new Error(String(error))
248
+ }
249
+
250
+ const available = kind === 'project' ? await listDopplerProjects() : await listDopplerConfigs(project)
251
+
252
+ return new Error(buildDopplerNotFoundMessage({ kind, project, config, available }))
253
+ }
254
+
138
255
  export const assertDopplerOutputSize = (stdout: string): void => {
139
256
  const bytes = Buffer.byteLength(stdout, 'utf-8')
140
257
 
@@ -1 +1,2 @@
1
- export { envLoad, envLoadMcpTool } from './env-load'
1
+ export { buildEnvLoadFileLines, envLoad, envLoadMcpTool, writeEnvLoadFile } from './env-load'
2
+ export type { EnvLoadFileResult } from './env-load'
@@ -0,0 +1,78 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import process from 'node:process'
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
6
+
7
+ import {
8
+ INFRA_KIT_ENV_AUTOLOADED_VAR,
9
+ INFRA_KIT_ENV_CLEARED_VAR,
10
+ INFRA_KIT_ENV_CONFIG_VAR,
11
+ INFRA_KIT_ENV_PROJECT_VAR,
12
+ INFRA_KIT_SESSION_VAR,
13
+ } from 'src/lib/constants'
14
+
15
+ import { envStatus } from '../env-status'
16
+
17
+ vi.mock('src/lib/logger', () => {
18
+ return { logger: { info: vi.fn(), warn: vi.fn() } }
19
+ })
20
+
21
+ // Pure local introspection: env-status must NOT call Doppler. If it did, importing
22
+ // this mock-free test (no doppler mock) and running offline would hang/throw — the
23
+ // fact these tests pass is itself the regression guard for the de-Doppler change.
24
+
25
+ const ORIGINAL_ENV = { ...process.env }
26
+ let cacheRoot: string
27
+
28
+ beforeEach(() => {
29
+ cacheRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ik-envstatus-'))
30
+ process.env.XDG_CACHE_HOME = cacheRoot
31
+ process.env[INFRA_KIT_SESSION_VAR] = 'sess-status'
32
+ delete process.env[INFRA_KIT_ENV_CONFIG_VAR]
33
+ delete process.env[INFRA_KIT_ENV_PROJECT_VAR]
34
+ delete process.env[INFRA_KIT_ENV_AUTOLOADED_VAR]
35
+ delete process.env[INFRA_KIT_ENV_CLEARED_VAR]
36
+ })
37
+
38
+ afterEach(() => {
39
+ process.env = { ...ORIGINAL_ENV }
40
+ fs.rmSync(cacheRoot, { recursive: true, force: true })
41
+ })
42
+
43
+ describe('envStatus', () => {
44
+ it('reports nothing loaded with autoLoaded=false, cleared=false', async () => {
45
+ const { structuredContent } = await envStatus()
46
+
47
+ expect(structuredContent.sessionConfig).toBeNull()
48
+ expect(structuredContent.autoLoaded).toBe(false)
49
+ expect(structuredContent.cleared).toBe(false)
50
+ })
51
+
52
+ it('reports cleared=true when the clear sentinel is set', async () => {
53
+ process.env[INFRA_KIT_ENV_CLEARED_VAR] = '1'
54
+
55
+ const { structuredContent } = await envStatus()
56
+
57
+ expect(structuredContent.cleared).toBe(true)
58
+ })
59
+
60
+ it('reports autoLoaded=true when the auto-load marker is present', async () => {
61
+ process.env[INFRA_KIT_ENV_CONFIG_VAR] = 'dev'
62
+ process.env[INFRA_KIT_ENV_PROJECT_VAR] = 'my-project'
63
+ process.env[INFRA_KIT_ENV_AUTOLOADED_VAR] = '1'
64
+
65
+ const { structuredContent } = await envStatus()
66
+
67
+ expect(structuredContent.sessionConfig).toBe('dev')
68
+ expect(structuredContent.autoLoaded).toBe(true)
69
+ })
70
+
71
+ it('reports autoLoaded=false for a manual load (config set, no marker)', async () => {
72
+ process.env[INFRA_KIT_ENV_CONFIG_VAR] = 'dev'
73
+
74
+ const { structuredContent } = await envStatus()
75
+
76
+ expect(structuredContent.autoLoaded).toBe(false)
77
+ })
78
+ })