infra-kit 0.1.113 → 0.1.115

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 (218) hide show
  1. package/.eslintcache +1 -1
  2. package/.omc/state/agent-replay-0d1515ce-4ebe-4ce8-a865-2897f68de168.jsonl +1 -0
  3. package/.omc/state/agent-replay-1fcfe0ec-7567-41c3-9bfd-501e99e86819.jsonl +19 -0
  4. package/.omc/state/agent-replay-38382db5-3088-4b5a-aff7-2561a60827c7.jsonl +17 -0
  5. package/.omc/state/agent-replay-50c36be9-a3a2-42b9-9c85-5a06d3e22f6a.jsonl +5 -0
  6. package/.omc/state/agent-replay-8198b3e5-fad6-4ee0-9c59-ef773ad89af4.jsonl +1 -0
  7. package/.omc/state/agent-replay-8cd7652a-b8b5-49cf-9b1f-bee8e5972458.jsonl +3 -0
  8. package/.omc/state/agent-replay-9bea518f-a9ee-4d14-bb53-a936ebaf647c.jsonl +7 -0
  9. package/.omc/state/agent-replay-b52d89fb-0e24-4901-bc66-27e3c1697126.jsonl +2 -0
  10. package/.omc/state/agent-replay-f581a1ae-0ee4-48a3-96f3-ce31a0633bb0.jsonl +1 -0
  11. package/.omc/state/agent-replay-f99d8dad-81d1-4987-8183-62edf0843d0c.jsonl +2 -0
  12. package/.omc/state/agent-replay-fe1057b4-9a25-4e8e-8c3f-3e11652ee697.jsonl +5 -0
  13. package/.omc/state/idle-notif-cooldown.json +1 -1
  14. package/.omc/state/sessions/0d1515ce-4ebe-4ce8-a865-2897f68de168/pre-tool-advisory-throttle.json +10 -0
  15. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/last-tool-error-state.json +7 -0
  16. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/mission-state.json +109 -0
  17. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/pre-tool-advisory-throttle.json +42 -0
  18. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/ralph-state.json +13 -0
  19. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/skill-active-state.json +15 -0
  20. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/subagent-tracking-state.json +62 -0
  21. package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/ultrawork-state.json +11 -0
  22. package/.omc/state/sessions/23d0ff82-17e5-428b-8743-38f7979bb6f3/last-tool-error-state.json +7 -0
  23. package/.omc/state/sessions/23d0ff82-17e5-428b-8743-38f7979bb6f3/pre-tool-advisory-throttle.json +18 -0
  24. package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/last-tool-error-state.json +7 -0
  25. package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/mission-state.json +145 -0
  26. package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/pre-tool-advisory-throttle.json +38 -0
  27. package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/ralplan-state.json +11 -0
  28. package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/subagent-tracking-state.json +98 -0
  29. package/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/last-tool-error-state.json +7 -0
  30. package/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/pre-tool-advisory-throttle.json +18 -0
  31. package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/mission-state.json +61 -0
  32. package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/pre-tool-advisory-throttle.json +22 -0
  33. package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/skill-active-state.json +15 -0
  34. package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/subagent-tracking-state.json +26 -0
  35. package/.omc/state/sessions/560ea519-574a-4713-b134-5bf6cfaf3c33/pre-tool-advisory-throttle.json +18 -0
  36. package/.omc/state/sessions/8198b3e5-fad6-4ee0-9c59-ef773ad89af4/pre-tool-advisory-throttle.json +14 -0
  37. package/.omc/state/sessions/8198b3e5-fad6-4ee0-9c59-ef773ad89af4/subagent-tracking-state.json +17 -0
  38. package/.omc/state/sessions/8cd7652a-b8b5-49cf-9b1f-bee8e5972458/pre-tool-advisory-throttle.json +18 -0
  39. package/.omc/state/sessions/8cd7652a-b8b5-49cf-9b1f-bee8e5972458/ralplan-state.json +11 -0
  40. package/.omc/state/sessions/8cd7652a-b8b5-49cf-9b1f-bee8e5972458/subagent-tracking-state.json +35 -0
  41. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/last-tool-error-state.json +7 -0
  42. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/mission-state.json +79 -0
  43. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/pre-tool-advisory-throttle.json +22 -0
  44. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/progress.txt +7 -0
  45. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/skill-active-state.json +15 -0
  46. package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/subagent-tracking-state.json +26 -0
  47. package/.omc/state/sessions/b52d89fb-0e24-4901-bc66-27e3c1697126/pre-tool-advisory-throttle.json +14 -0
  48. package/.omc/state/sessions/b52d89fb-0e24-4901-bc66-27e3c1697126/subagent-tracking-state.json +17 -0
  49. package/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/pre-tool-advisory-throttle.json +18 -0
  50. package/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/pre-tool-advisory-throttle.json +18 -0
  51. package/.omc/state/sessions/f581a1ae-0ee4-48a3-96f3-ce31a0633bb0/pre-tool-advisory-throttle.json +18 -0
  52. package/.omc/state/sessions/f581a1ae-0ee4-48a3-96f3-ce31a0633bb0/subagent-tracking-state.json +17 -0
  53. package/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/pre-tool-advisory-throttle.json +14 -0
  54. package/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/ralph-state.json +13 -0
  55. package/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/ultrawork-state.json +11 -0
  56. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/last-tool-error-state.json +7 -0
  57. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/mission-state.json +61 -0
  58. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/prd.json +22 -0
  59. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/pre-tool-advisory-throttle.json +22 -0
  60. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/progress.txt +22 -0
  61. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/skill-active-state.json +15 -0
  62. package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/subagent-tracking-state.json +26 -0
  63. package/.turbo/turbo-build.log +3 -3
  64. package/.turbo/turbo-test.log +92 -44
  65. package/dist/boot-K4QUAFSP.js +2 -0
  66. package/dist/boot-K4QUAFSP.js.map +7 -0
  67. package/dist/chunk-RTSP2FCA.js +2 -0
  68. package/dist/chunk-RTSP2FCA.js.map +7 -0
  69. package/dist/chunk-VPBZR2PY.js +166 -0
  70. package/dist/chunk-VPBZR2PY.js.map +7 -0
  71. package/dist/cli.js +11 -107
  72. package/dist/cli.js.map +4 -4
  73. package/dist/entry/index.d.ts +2 -0
  74. package/dist/index.js +1 -1
  75. package/dist/index.js.map +4 -4
  76. package/dist/lib/vendor/config-schema.d.ts +58 -0
  77. package/dist/mcp.js +1 -54
  78. package/dist/mcp.js.map +4 -4
  79. package/eslint.config.js +33 -1
  80. package/package.json +5 -1
  81. package/scripts/build.js +11 -1
  82. package/src/.omc/state/agent-replay-c513ef38-cc3e-4a29-9c4c-1e0137245dd5.jsonl +4 -0
  83. package/src/.omc/state/agent-replay-ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a.jsonl +2 -0
  84. package/src/.omc/state/idle-notif-cooldown.json +1 -1
  85. package/src/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/mission-state.json +79 -0
  86. package/src/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/pre-tool-advisory-throttle.json +22 -0
  87. package/src/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/subagent-tracking-state.json +26 -0
  88. package/src/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/pre-tool-advisory-throttle.json +18 -0
  89. package/src/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/ralph-state.json +13 -0
  90. package/src/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/ultrawork-state.json +11 -0
  91. package/src/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/pre-tool-advisory-throttle.json +14 -0
  92. package/src/commands/config/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/last-tool-error-state.json +7 -0
  93. package/src/commands/config/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/pre-tool-advisory-throttle.json +18 -0
  94. package/src/commands/config/config.ts +7 -37
  95. package/src/commands/doctor/__tests__/agent-files.test.ts +8 -30
  96. package/src/commands/doctor/__tests__/check-legacy-user-global-config.test.ts +110 -0
  97. package/src/commands/doctor/doctor.ts +110 -44
  98. package/src/commands/env-autoload/env-autoload.ts +12 -0
  99. package/src/commands/env-autoload/index.ts +1 -0
  100. package/src/commands/env-clear/__tests__/env-clear.test.ts +27 -0
  101. package/src/commands/env-clear/env-clear.ts +22 -8
  102. package/src/commands/env-load/__tests__/env-load.test.ts +36 -1
  103. package/src/commands/env-load/env-load.ts +152 -35
  104. package/src/commands/env-load/index.ts +2 -1
  105. package/src/commands/env-status/__tests__/env-status.test.ts +78 -0
  106. package/src/commands/env-status/env-status.ts +19 -7
  107. package/src/commands/gh-release-deploy-all/gh-release-deploy-all.ts +45 -1
  108. package/src/commands/gh-release-deploy-selected/gh-release-deploy-selected.ts +46 -1
  109. package/src/commands/init/__tests__/agent-files.test.ts +145 -27
  110. package/src/commands/init/__tests__/migrate-config.test.ts +136 -5
  111. package/src/commands/init/__tests__/migrate-factory-config.test.ts +93 -0
  112. package/src/commands/init/__tests__/seed-user-global-config.test.ts +64 -0
  113. package/src/commands/init/__tests__/shell-body.test.ts +30 -0
  114. package/src/commands/init/agent-files.ts +65 -45
  115. package/src/commands/init/init.ts +140 -31
  116. package/src/commands/init/migrate-config.ts +160 -32
  117. package/src/commands/vendor-check/__tests__/runtime-isolation.test.ts +71 -0
  118. package/src/commands/vendor-check/__tests__/vendor-check.test.ts +87 -0
  119. package/src/commands/vendor-check/index.ts +1 -0
  120. package/src/commands/vendor-check/vendor-check.ts +127 -0
  121. package/src/commands/vendor-config/__tests__/vendor-config.test.ts +103 -0
  122. package/src/commands/vendor-config/index.ts +1 -0
  123. package/src/commands/vendor-config/vendor-config.ts +162 -0
  124. package/src/commands/vendor-diff/__tests__/vendor-diff.test.ts +74 -0
  125. package/src/commands/vendor-diff/index.ts +1 -0
  126. package/src/commands/vendor-diff/vendor-diff.ts +77 -0
  127. package/src/commands/vendor-manifest/index.ts +1 -0
  128. package/src/commands/vendor-manifest/vendor-manifest.ts +60 -0
  129. package/src/commands/vendor-sync/__tests__/vendor-write.test.ts +86 -0
  130. package/src/commands/vendor-sync/index.ts +1 -0
  131. package/src/commands/vendor-sync/vendor-sync.ts +90 -0
  132. package/src/commands/worktrees-add/worktrees-add.ts +1 -1
  133. package/src/commands/worktrees-reload/worktrees-reload.ts +6 -5
  134. package/src/commands/worktrees-remove/worktrees-remove.ts +10 -1
  135. package/src/commands/worktrees-sync/worktrees-sync.ts +9 -1
  136. package/src/entry/cli.ts +417 -210
  137. package/src/entry/index.ts +7 -1
  138. package/src/integrations/cmux/__tests__/open-workspace-with-layout.test.ts +127 -0
  139. package/src/integrations/cmux/close-workspace-by-title.ts +2 -2
  140. package/src/integrations/cmux/list-workspace-titles.ts +1 -1
  141. package/src/integrations/cmux/open-workspace-with-layout.ts +15 -4
  142. package/src/integrations/cursor/.omc/state/agent-replay-f99d8dad-81d1-4987-8183-62edf0843d0c.jsonl +2 -0
  143. package/src/integrations/cursor/.omc/state/idle-notif-cooldown.json +3 -0
  144. package/src/integrations/cursor/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/pre-tool-advisory-throttle.json +18 -0
  145. package/src/integrations/cursor/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/subagent-tracking-state.json +17 -0
  146. package/src/integrations/cursor/index.ts +1 -0
  147. package/src/integrations/cursor/launch-cursor.ts +12 -0
  148. package/src/integrations/cursor/open-cursor-workspace.ts +8 -14
  149. package/src/integrations/doppler/__tests__/doppler-errors.test.ts +76 -0
  150. package/src/integrations/doppler/doppler-cli-auth.ts +12 -4
  151. package/src/integrations/doppler/doppler-errors.ts +88 -0
  152. package/src/integrations/doppler/doppler-project.ts +80 -0
  153. package/src/integrations/doppler/index.ts +3 -1
  154. package/src/integrations/gh/gh-cli-auth/gh-cli-auth.ts +9 -5
  155. package/src/integrations/gh/gh-release-prs/gh-release-prs.ts +16 -8
  156. package/src/integrations/ide/__tests__/ide-facade.test.ts +57 -10
  157. package/src/integrations/ide/add-ide-worktree-folders.ts +2 -4
  158. package/src/integrations/ide/open-ide-workspace.ts +6 -6
  159. package/src/integrations/ide/remove-ide-worktree-folders.ts +95 -30
  160. package/src/integrations/ide/types.ts +10 -1
  161. package/src/integrations/zed/__tests__/open-zed-workspace.test.ts +1 -17
  162. package/src/integrations/zed/__tests__/reuse-zed-workspace.test.ts +86 -0
  163. package/src/integrations/zed/index.ts +1 -0
  164. package/src/integrations/zed/open-zed-workspace.ts +4 -10
  165. package/src/integrations/zed/reuse-zed-workspace.ts +57 -0
  166. package/src/lib/command-catalog/__tests__/__snapshots__/command-catalog.test.ts.snap +212 -0
  167. package/src/lib/command-catalog/__tests__/command-catalog.test.ts +193 -0
  168. package/src/lib/command-catalog/command-catalog.ts +141 -0
  169. package/src/lib/command-catalog/index.ts +2 -0
  170. package/src/lib/command-echo/__tests__/command-echo.test.ts +127 -0
  171. package/src/lib/constants/constants.ts +13 -0
  172. package/src/lib/constants/index.ts +2 -0
  173. package/src/lib/env-autoload/__tests__/env-autoload.test.ts +288 -0
  174. package/src/lib/env-autoload/env-autoload.ts +294 -0
  175. package/src/lib/env-autoload/index.ts +9 -0
  176. package/src/lib/errors/operation-error.ts +1 -1
  177. package/src/lib/infra-kit-config/__tests__/env-autoload-schema.test.ts +71 -0
  178. package/src/lib/infra-kit-config/__tests__/infra-kit-config.test.ts +77 -6
  179. package/src/lib/infra-kit-config/index.ts +9 -1
  180. package/src/lib/infra-kit-config/infra-kit-config.ts +71 -11
  181. package/src/lib/json-output/__tests__/json-output.test.ts +139 -0
  182. package/src/lib/json-output/index.ts +1 -0
  183. package/src/lib/json-output/json-output.ts +47 -0
  184. package/src/lib/managed-block/managed-block.ts +4 -4
  185. package/src/lib/path-display/index.ts +1 -0
  186. package/src/lib/path-display/path-display.ts +42 -0
  187. package/src/lib/render/__tests__/render.test.ts +48 -0
  188. package/src/lib/render/index.ts +1 -0
  189. package/src/lib/render/render.ts +24 -0
  190. package/src/lib/vendor/__tests__/config-schema.test.ts +31 -0
  191. package/src/lib/vendor/__tests__/config.test.ts +63 -0
  192. package/src/lib/vendor/__tests__/factory-config.test.ts +92 -0
  193. package/src/lib/vendor/__tests__/vendor-lib.test.ts +158 -0
  194. package/src/lib/vendor/__tests__/zx-mock.ts +32 -0
  195. package/src/lib/vendor/config-schema.ts +78 -0
  196. package/src/lib/vendor/config.ts +58 -0
  197. package/src/lib/vendor/factory-config-schema.ts +35 -0
  198. package/src/lib/vendor/factory-config.ts +91 -0
  199. package/src/lib/vendor/hash.ts +16 -0
  200. package/src/lib/vendor/index.ts +23 -0
  201. package/src/lib/vendor/manifest.ts +151 -0
  202. package/src/lib/vendor/skip-sets.ts +48 -0
  203. package/src/lib/vendor/sync-ops.ts +290 -0
  204. package/src/lib/vendor/walk.ts +56 -0
  205. package/src/lib/worktrees/remove-worktrees.ts +15 -0
  206. package/src/mcp/tools/index.ts +5 -42
  207. package/src/sanity.test.tsx +1 -0
  208. package/src/tui/__tests__/no-react-boundary.test.ts +51 -0
  209. package/src/tui/boot.tsx +45 -0
  210. package/src/tui/screens/__tests__/command-palette.test.tsx +136 -0
  211. package/src/tui/screens/command-palette.tsx +159 -0
  212. package/src/tui/types.ts +8 -0
  213. package/tsconfig.json +1 -0
  214. package/tsconfig.tsbuildinfo +1 -1
  215. package/vitest.config.ts +8 -0
  216. package/src/commands/worktrees-open/__tests__/open-cmux.test.ts +0 -96
  217. package/src/commands/worktrees-open/index.ts +0 -1
  218. package/src/commands/worktrees-open/worktrees-open.ts +0 -195
@@ -5,6 +5,8 @@ export {
5
5
  ENV_VAR_LINE_PATTERN,
6
6
  getCacheRoot,
7
7
  getSessionCacheDir,
8
+ INFRA_KIT_ENV_AUTOLOADED_VAR,
9
+ INFRA_KIT_ENV_CLEARED_VAR,
8
10
  INFRA_KIT_ENV_CONFIG_VAR,
9
11
  INFRA_KIT_ENV_LOADED_AT_VAR,
10
12
  INFRA_KIT_ENV_PROJECT_VAR,
@@ -0,0 +1,288 @@
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 { writeEnvLoadFile } from 'src/commands/env-load'
8
+ import {
9
+ INFRA_KIT_ENV_AUTOLOADED_VAR,
10
+ INFRA_KIT_ENV_CLEARED_VAR,
11
+ INFRA_KIT_ENV_CONFIG_VAR,
12
+ INFRA_KIT_ENV_PROJECT_VAR,
13
+ INFRA_KIT_SESSION_VAR,
14
+ } from 'src/lib/constants'
15
+ import { getInfraKitConfig } from 'src/lib/infra-kit-config'
16
+ import { logger } from 'src/lib/logger'
17
+
18
+ import { decideAutoLoad, resolveEnvAutoLoad, runEnvAutoLoad } from '../env-autoload'
19
+ import type { AutoLoadDecisionInput, AutoLoadEnvSnapshot } from '../env-autoload'
20
+
21
+ vi.mock('src/lib/infra-kit-config', () => {
22
+ return { getInfraKitConfig: vi.fn() }
23
+ })
24
+
25
+ vi.mock('src/commands/env-load', () => {
26
+ return { writeEnvLoadFile: vi.fn() }
27
+ })
28
+
29
+ vi.mock('src/lib/logger', () => {
30
+ return { logger: { warn: vi.fn(), debug: vi.fn() } }
31
+ })
32
+
33
+ const baseConfig = {
34
+ environments: ['dev', 'prod'],
35
+ envManagement: { provider: 'doppler', config: { name: 'my-project' } },
36
+ }
37
+
38
+ const buildInput = (overrides: Partial<AutoLoadDecisionInput> = {}): AutoLoadDecisionInput => {
39
+ return {
40
+ trigger: 'cli-invocation',
41
+ expectedTrigger: 'cli-invocation',
42
+ targetConfig: 'dev',
43
+ targetProject: 'my-project',
44
+ env: { session: 'sess-1' },
45
+ ...overrides,
46
+ }
47
+ }
48
+
49
+ const loadedEnv = (overrides: Partial<AutoLoadEnvSnapshot> = {}): AutoLoadEnvSnapshot => {
50
+ return {
51
+ session: 'sess-1',
52
+ currentConfig: 'dev',
53
+ currentProject: 'my-project',
54
+ autoLoadedMarker: '1',
55
+ ...overrides,
56
+ }
57
+ }
58
+
59
+ describe('decideAutoLoad — trigger gating', () => {
60
+ it('loads when the configured trigger matches this callsite', () => {
61
+ expect(decideAutoLoad(buildInput({ trigger: 'cli-invocation', expectedTrigger: 'cli-invocation' }))).toBe('load')
62
+ expect(decideAutoLoad(buildInput({ trigger: 'shell-startup', expectedTrigger: 'shell-startup' }))).toBe('load')
63
+ })
64
+
65
+ it('skips when the configured trigger is for the other callsite', () => {
66
+ expect(decideAutoLoad(buildInput({ trigger: 'shell-startup', expectedTrigger: 'cli-invocation' }))).toBe('skip')
67
+ expect(decideAutoLoad(buildInput({ trigger: 'cli-invocation', expectedTrigger: 'shell-startup' }))).toBe('skip')
68
+ })
69
+ })
70
+
71
+ describe('decideAutoLoad — session + clear guards', () => {
72
+ it('skips when no session is set (cache dir is session-scoped)', () => {
73
+ expect(decideAutoLoad(buildInput({ env: { session: undefined } }))).toBe('skip')
74
+ })
75
+
76
+ // Regression M2: a deliberate `env-clear` must keep cli-invocation auto-load
77
+ // from immediately re-loading in the same shell.
78
+ it('skips when the clear sentinel is set (M2: stay cleared)', () => {
79
+ expect(decideAutoLoad(buildInput({ env: { session: 'sess-1', cleared: '1' } }))).toBe('skip')
80
+ })
81
+ })
82
+
83
+ describe('decideAutoLoad — manual-load protection (C1)', () => {
84
+ // Regression C1: env loaded WITHOUT our auto marker means a manual `env-load`;
85
+ // auto-load must never overwrite it, even when the target config differs.
86
+ it('skips a manually-loaded env even when target config differs', () => {
87
+ expect(
88
+ decideAutoLoad(
89
+ buildInput({
90
+ targetConfig: 'dev',
91
+ env: { session: 'sess-1', currentConfig: 'prod', currentProject: 'my-project', autoLoadedMarker: undefined },
92
+ }),
93
+ ),
94
+ ).toBe('skip')
95
+ })
96
+ })
97
+
98
+ describe('decideAutoLoad — project-aware freshness', () => {
99
+ it('skips when our auto-load already matches config AND project', () => {
100
+ expect(decideAutoLoad(buildInput({ env: loadedEnv() }))).toBe('skip')
101
+ })
102
+
103
+ it('loads when the auto-loaded config differs from target', () => {
104
+ expect(decideAutoLoad(buildInput({ targetConfig: 'prod', env: loadedEnv({ currentConfig: 'dev' }) }))).toBe('load')
105
+ })
106
+
107
+ it('loads when the project differs even though the config name matches (worktree leak guard)', () => {
108
+ expect(
109
+ decideAutoLoad(buildInput({ targetProject: 'other-project', env: loadedEnv({ currentProject: 'my-project' }) })),
110
+ ).toBe('load')
111
+ })
112
+
113
+ it('loads into a clean shell (nothing currently loaded)', () => {
114
+ expect(decideAutoLoad(buildInput({ env: { session: 'sess-1' } }))).toBe('load')
115
+ })
116
+ })
117
+
118
+ describe('resolveEnvAutoLoad', () => {
119
+ const ORIGINAL_ENV = { ...process.env }
120
+ let cacheRoot: string
121
+
122
+ beforeEach(() => {
123
+ vi.mocked(getInfraKitConfig).mockReset()
124
+ vi.mocked(logger.warn).mockClear()
125
+ // Isolate the per-session warn-dedup flag in a fresh cache dir so the warning
126
+ // is not suppressed by a flag left over from the dev's real session.
127
+ cacheRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ik-autoload-resolve-'))
128
+ process.env.XDG_CACHE_HOME = cacheRoot
129
+ process.env[INFRA_KIT_SESSION_VAR] = 'sess-resolve'
130
+ })
131
+
132
+ afterEach(() => {
133
+ process.env = { ...ORIGINAL_ENV }
134
+ fs.rmSync(cacheRoot, { recursive: true, force: true })
135
+ })
136
+
137
+ it('returns null when not inside a project (getInfraKitConfig throws)', async () => {
138
+ vi.mocked(getInfraKitConfig).mockRejectedValue(new Error('infra-kit.json not found'))
139
+
140
+ expect(await resolveEnvAutoLoad()).toBeNull()
141
+ })
142
+
143
+ it('returns null when envAutoLoad is absent (feature off)', async () => {
144
+ vi.mocked(getInfraKitConfig).mockResolvedValue(baseConfig as never)
145
+
146
+ expect(await resolveEnvAutoLoad()).toBeNull()
147
+ })
148
+
149
+ it('resolves trigger + config + project from one config read', async () => {
150
+ vi.mocked(getInfraKitConfig).mockResolvedValue({
151
+ ...baseConfig,
152
+ envAutoLoad: { trigger: 'cli-invocation', config: 'dev' },
153
+ } as never)
154
+
155
+ expect(await resolveEnvAutoLoad()).toEqual({ trigger: 'cli-invocation', config: 'dev', project: 'my-project' })
156
+ })
157
+
158
+ it('warns and disables when config is not one of environments', async () => {
159
+ vi.mocked(getInfraKitConfig).mockResolvedValue({
160
+ ...baseConfig,
161
+ envAutoLoad: { trigger: 'shell-startup', config: 'nope' },
162
+ } as never)
163
+
164
+ expect(await resolveEnvAutoLoad()).toBeNull()
165
+ expect(logger.warn).toHaveBeenCalledOnce()
166
+ expect(vi.mocked(logger.warn).mock.calls[0]![0]).toContain('nope')
167
+ })
168
+
169
+ // Gap 1 regression: the shell-startup callsite discards stderr, so it must NOT
170
+ // warn (and must not write the dedup flag that would poison the interactive channel).
171
+ it('does NOT warn on bad config when canWarn is false (shell-startup channel)', async () => {
172
+ vi.mocked(getInfraKitConfig).mockResolvedValue({
173
+ ...baseConfig,
174
+ envAutoLoad: { trigger: 'shell-startup', config: 'nope' },
175
+ } as never)
176
+
177
+ expect(await resolveEnvAutoLoad(false)).toBeNull()
178
+ expect(logger.warn).not.toHaveBeenCalled()
179
+ })
180
+ })
181
+
182
+ describe('runEnvAutoLoad', () => {
183
+ const ORIGINAL_ENV = { ...process.env }
184
+ let cacheRoot: string
185
+
186
+ beforeEach(() => {
187
+ vi.mocked(getInfraKitConfig).mockReset()
188
+ vi.mocked(writeEnvLoadFile).mockReset()
189
+ cacheRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ik-autoload-'))
190
+ process.env.XDG_CACHE_HOME = cacheRoot
191
+ process.env[INFRA_KIT_SESSION_VAR] = 'sess-run'
192
+ delete process.env[INFRA_KIT_ENV_CONFIG_VAR]
193
+ delete process.env[INFRA_KIT_ENV_PROJECT_VAR]
194
+ delete process.env[INFRA_KIT_ENV_AUTOLOADED_VAR]
195
+ delete process.env[INFRA_KIT_ENV_CLEARED_VAR]
196
+ })
197
+
198
+ afterEach(() => {
199
+ process.env = { ...ORIGINAL_ENV }
200
+ fs.rmSync(cacheRoot, { recursive: true, force: true })
201
+ })
202
+
203
+ it('writes an auto-loaded file and returns its path on load', async () => {
204
+ vi.mocked(getInfraKitConfig).mockResolvedValue({
205
+ ...baseConfig,
206
+ envAutoLoad: { trigger: 'shell-startup', config: 'dev' },
207
+ } as never)
208
+ vi.mocked(writeEnvLoadFile).mockResolvedValue({
209
+ filePath: '/cache/env-load.sh',
210
+ variableCount: 3,
211
+ project: 'my-project',
212
+ config: 'dev',
213
+ })
214
+
215
+ const result = await runEnvAutoLoad({ expectedTrigger: 'shell-startup' })
216
+
217
+ expect(result).toBe('/cache/env-load.sh')
218
+ expect(writeEnvLoadFile).toHaveBeenCalledWith({ config: 'dev', autoLoaded: true })
219
+ })
220
+
221
+ it('returns null without fetching when the configured trigger is for the other callsite', async () => {
222
+ vi.mocked(getInfraKitConfig).mockResolvedValue({
223
+ ...baseConfig,
224
+ envAutoLoad: { trigger: 'shell-startup', config: 'dev' },
225
+ } as never)
226
+
227
+ const result = await runEnvAutoLoad({ expectedTrigger: 'cli-invocation' })
228
+
229
+ expect(result).toBeNull()
230
+ expect(writeEnvLoadFile).not.toHaveBeenCalled()
231
+ })
232
+
233
+ it('returns null and never fetches when env is already fresh', async () => {
234
+ vi.mocked(getInfraKitConfig).mockResolvedValue({
235
+ ...baseConfig,
236
+ envAutoLoad: { trigger: 'cli-invocation', config: 'dev' },
237
+ } as never)
238
+ process.env[INFRA_KIT_ENV_CONFIG_VAR] = 'dev'
239
+ process.env[INFRA_KIT_ENV_PROJECT_VAR] = 'my-project'
240
+ process.env[INFRA_KIT_ENV_AUTOLOADED_VAR] = '1'
241
+
242
+ const result = await runEnvAutoLoad({ expectedTrigger: 'cli-invocation' })
243
+
244
+ expect(result).toBeNull()
245
+ expect(writeEnvLoadFile).not.toHaveBeenCalled()
246
+ })
247
+
248
+ it('swallows transient failures to a debug log (shell-startup) and returns null', async () => {
249
+ vi.mocked(getInfraKitConfig).mockResolvedValue({
250
+ ...baseConfig,
251
+ envAutoLoad: { trigger: 'shell-startup', config: 'dev' },
252
+ } as never)
253
+ vi.mocked(writeEnvLoadFile).mockRejectedValue(new Error('doppler not authenticated'))
254
+
255
+ const result = await runEnvAutoLoad({ expectedTrigger: 'shell-startup' })
256
+
257
+ expect(result).toBeNull()
258
+ expect(logger.debug).toHaveBeenCalled()
259
+ expect(logger.warn).not.toHaveBeenCalled()
260
+ })
261
+
262
+ it('surfaces a transient failure once on the cli-invocation channel', async () => {
263
+ vi.mocked(getInfraKitConfig).mockResolvedValue({
264
+ ...baseConfig,
265
+ envAutoLoad: { trigger: 'cli-invocation', config: 'dev' },
266
+ } as never)
267
+ vi.mocked(writeEnvLoadFile).mockRejectedValue(new Error('doppler not authenticated'))
268
+
269
+ const result = await runEnvAutoLoad({ expectedTrigger: 'cli-invocation' })
270
+
271
+ expect(result).toBeNull()
272
+ expect(logger.warn).toHaveBeenCalledOnce()
273
+ expect(vi.mocked(logger.warn).mock.calls[0]![0]).toContain('failed')
274
+ })
275
+
276
+ it('backs off: after a failure the next run skips Doppler (writeEnvLoadFile called once)', async () => {
277
+ vi.mocked(getInfraKitConfig).mockResolvedValue({
278
+ ...baseConfig,
279
+ envAutoLoad: { trigger: 'cli-invocation', config: 'dev' },
280
+ } as never)
281
+ vi.mocked(writeEnvLoadFile).mockRejectedValue(new Error('network down'))
282
+
283
+ expect(await runEnvAutoLoad({ expectedTrigger: 'cli-invocation' })).toBeNull()
284
+ expect(await runEnvAutoLoad({ expectedTrigger: 'cli-invocation' })).toBeNull()
285
+
286
+ expect(writeEnvLoadFile).toHaveBeenCalledOnce()
287
+ })
288
+ })
@@ -0,0 +1,294 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import process from 'node:process'
4
+
5
+ import { writeEnvLoadFile } from 'src/commands/env-load'
6
+ import {
7
+ ENV_CLEAR_FILE,
8
+ ENV_LOAD_FILE,
9
+ INFRA_KIT_ENV_AUTOLOADED_VAR,
10
+ INFRA_KIT_ENV_CLEARED_VAR,
11
+ INFRA_KIT_ENV_CONFIG_VAR,
12
+ INFRA_KIT_ENV_PROJECT_VAR,
13
+ INFRA_KIT_SESSION_VAR,
14
+ getSessionCacheDir,
15
+ } from 'src/lib/constants'
16
+ import type { EnvAutoLoadConfig } from 'src/lib/infra-kit-config'
17
+ import { getInfraKitConfig } from 'src/lib/infra-kit-config'
18
+ import { logger } from 'src/lib/logger'
19
+
20
+ /** Which moment a concrete callsite represents. Matches the config `trigger`. */
21
+ export type AutoLoadTrigger = EnvAutoLoadConfig['trigger']
22
+
23
+ /** Per-session flag file that de-dups the auto-load warning (misconfig or failure). */
24
+ const WARN_SENTINEL_FILE = 'autoload-warn.flag'
25
+
26
+ /** Per-session marker recording the last auto-load failure (mtime = when). */
27
+ const FAIL_SENTINEL_FILE = 'autoload-fail.flag'
28
+
29
+ /**
30
+ * After a failed auto-load, suppress retries for this long so a down/unauthenticated
31
+ * Doppler isn't re-probed on every cli-invocation. A new shell starts a fresh
32
+ * session cache dir, so this only throttles within one session.
33
+ */
34
+ const FAIL_BACKOFF_MS = 30_000
35
+
36
+ /** Resolved auto-load inputs: the chosen trigger + the env config and Doppler project to load. */
37
+ export interface ResolvedEnvAutoLoad {
38
+ trigger: AutoLoadTrigger
39
+ config: string
40
+ project: string
41
+ }
42
+
43
+ /**
44
+ * Read the resolved + validated env auto-load inputs, or `null` when auto-load
45
+ * should not run. Returns `null` (never throws) when:
46
+ * - we're not inside an infra-kit project (getInfraKitConfig throws), or config is unreadable;
47
+ * - `envAutoLoad` is absent (feature off);
48
+ * - `envAutoLoad.config` is not one of `environments` — warns once per session, then disables.
49
+ *
50
+ * Validation lives here (not in the schema) so a typo only disables this optional
51
+ * feature instead of throwing inside the merged-config parse and breaking every command.
52
+ * Resolves the Doppler project from the SAME config read (no second getInfraKitConfig),
53
+ * so the skip path stays cheap.
54
+ *
55
+ * `canWarn` gates the misconfig warning: the shell-startup callsite runs backgrounded
56
+ * with stderr discarded, so warning there is invisible AND would write the dedup flag,
57
+ * poisoning the only channel (cli-invocation / interactive) that can actually surface
58
+ * it. So shell-startup passes `false`; interactive callsites pass `true`.
59
+ */
60
+ export const resolveEnvAutoLoad = async (canWarn = true): Promise<ResolvedEnvAutoLoad | null> => {
61
+ let config
62
+
63
+ try {
64
+ config = await getInfraKitConfig()
65
+ } catch {
66
+ return null
67
+ }
68
+
69
+ const autoLoad = config.envAutoLoad
70
+
71
+ if (!autoLoad) return null
72
+
73
+ if (!config.environments.includes(autoLoad.config)) {
74
+ if (canWarn) {
75
+ warnOnce(
76
+ `infra-kit: envAutoLoad.config "${autoLoad.config}" is not one of environments [${config.environments.join(
77
+ ', ',
78
+ )}] — env auto-load disabled.`,
79
+ )
80
+ }
81
+
82
+ return null
83
+ }
84
+
85
+ return {
86
+ trigger: autoLoad.trigger,
87
+ config: autoLoad.config,
88
+ project: config.envManagement.config.name,
89
+ }
90
+ }
91
+
92
+ /** The env-var snapshot the freshness/suppression guards read. */
93
+ export interface AutoLoadEnvSnapshot {
94
+ session?: string
95
+ cleared?: string
96
+ currentConfig?: string
97
+ currentProject?: string
98
+ autoLoadedMarker?: string
99
+ }
100
+
101
+ export interface AutoLoadDecisionInput {
102
+ /** The configured trigger. */
103
+ trigger: AutoLoadTrigger
104
+ /** Which trigger this callsite represents. */
105
+ expectedTrigger: AutoLoadTrigger
106
+ targetConfig: string
107
+ targetProject: string
108
+ env: AutoLoadEnvSnapshot
109
+ }
110
+
111
+ export type AutoLoadDecision = 'load' | 'skip'
112
+
113
+ /**
114
+ * Pure decision: should this callsite (auto-)load env right now? Encodes the full
115
+ * guard matrix so it is exhaustively unit-testable without Doppler or a shell:
116
+ * - the configured trigger must match this callsite;
117
+ * - a session must exist (the cache dir is session-scoped);
118
+ * - an explicit clear suppresses auto-load (M2);
119
+ * - a MANUAL load (config set, no auto marker) is never clobbered (C1);
120
+ * - an auto-load already fresh for the same config AND project is a no-op
121
+ * (project-aware so two same-named configs across different-repo worktrees
122
+ * sharing one session don't leak each other's secrets).
123
+ */
124
+ export const decideAutoLoad = (input: AutoLoadDecisionInput): AutoLoadDecision => {
125
+ const { trigger, expectedTrigger, targetConfig, targetProject, env } = input
126
+
127
+ if (trigger !== expectedTrigger) return 'skip'
128
+
129
+ if (!env.session) return 'skip'
130
+
131
+ if (env.cleared) return 'skip'
132
+
133
+ // Manual load present (a config is loaded but it wasn't auto-loaded) — leave it.
134
+ if (env.currentConfig && !env.autoLoadedMarker) return 'skip'
135
+
136
+ // Our own auto-load already matches the target config+project — nothing to do.
137
+ if (env.autoLoadedMarker && env.currentConfig === targetConfig && env.currentProject === targetProject) {
138
+ return 'skip'
139
+ }
140
+
141
+ return 'load'
142
+ }
143
+
144
+ export interface RunEnvAutoLoadArgs {
145
+ expectedTrigger: AutoLoadTrigger
146
+ }
147
+
148
+ /**
149
+ * Resolve config, evaluate the guards, and (if it should) produce env-load.sh with
150
+ * the auto-load marker. Returns the written file path, or `null` when auto-load was
151
+ * skipped or failed. NEVER throws. Transient failures (Doppler offline / not
152
+ * authenticated / network) record a backoff marker so they aren't re-probed on every
153
+ * command, and are surfaced once per session on the interactive (cli-invocation)
154
+ * channel. No producer-side lock — a rare cold-shell double-fetch is tolerated (the
155
+ * second write is atomic and idempotent).
156
+ */
157
+ export const runEnvAutoLoad = async ({ expectedTrigger }: RunEnvAutoLoadArgs): Promise<string | null> => {
158
+ // Only the cli-invocation / interactive callsite reaches a TTY; the shell-startup
159
+ // spawn discards stderr, so warning there is invisible and would poison the dedup.
160
+ const canWarn = expectedTrigger === 'cli-invocation'
161
+
162
+ try {
163
+ const resolved = await resolveEnvAutoLoad(canWarn)
164
+
165
+ if (!resolved) return null
166
+
167
+ const decision = decideAutoLoad({
168
+ trigger: resolved.trigger,
169
+ expectedTrigger,
170
+ targetConfig: resolved.config,
171
+ targetProject: resolved.project,
172
+ env: readAutoLoadEnvSnapshot(),
173
+ })
174
+
175
+ if (decision === 'skip') return null
176
+
177
+ // Disk-level clear signal: a clear that hasn't yet been sourced into this
178
+ // process's env still suppresses auto-load (clear file newer than load file).
179
+ if (isClearedOnDisk()) return null
180
+
181
+ // Back off after a recent failure so a down/unauthenticated Doppler isn't
182
+ // re-probed on every command in the same session.
183
+ if (recentlyFailed()) return null
184
+
185
+ const result = await writeEnvLoadFile({ config: resolved.config, autoLoaded: true })
186
+
187
+ clearFailure()
188
+
189
+ return result.filePath
190
+ } catch (error) {
191
+ const reason = (error as Error).message
192
+
193
+ recordFailure()
194
+
195
+ // Surface the failure once per session on the interactive channel; stay silent
196
+ // (debug only) on the backgrounded shell-startup path.
197
+ if (canWarn) {
198
+ warnOnce(`infra-kit: env auto-load failed — ${reason} (will retry later)`)
199
+ } else {
200
+ logger.debug(`env auto-load skipped: ${reason}`)
201
+ }
202
+
203
+ return null
204
+ }
205
+ }
206
+
207
+ /** Snapshot the env vars the guards depend on. */
208
+ const readAutoLoadEnvSnapshot = (): AutoLoadEnvSnapshot => {
209
+ return {
210
+ session: process.env[INFRA_KIT_SESSION_VAR],
211
+ cleared: process.env[INFRA_KIT_ENV_CLEARED_VAR],
212
+ currentConfig: process.env[INFRA_KIT_ENV_CONFIG_VAR],
213
+ currentProject: process.env[INFRA_KIT_ENV_PROJECT_VAR],
214
+ autoLoadedMarker: process.env[INFRA_KIT_ENV_AUTOLOADED_VAR],
215
+ }
216
+ }
217
+
218
+ /**
219
+ * True when a clear is pending on disk: an env-clear.sh exists and is at least as
220
+ * new as env-load.sh (or no load file remains). Belt-and-suspenders next to the
221
+ * INFRA_KIT_ENV_CLEARED env guard for the window before the shell sources the clear.
222
+ */
223
+ const isClearedOnDisk = (): boolean => {
224
+ try {
225
+ const dir = getSessionCacheDir()
226
+ const clearPath = path.join(dir, ENV_CLEAR_FILE)
227
+
228
+ if (!fs.existsSync(clearPath)) return false
229
+
230
+ const loadPath = path.join(dir, ENV_LOAD_FILE)
231
+
232
+ if (!fs.existsSync(loadPath)) return true
233
+
234
+ return fs.statSync(clearPath).mtimeMs >= fs.statSync(loadPath).mtimeMs
235
+ } catch {
236
+ return false
237
+ }
238
+ }
239
+
240
+ /** True when an auto-load failed within the backoff window (suppress retries). */
241
+ const recentlyFailed = (): boolean => {
242
+ try {
243
+ const flagPath = path.join(getSessionCacheDir(), FAIL_SENTINEL_FILE)
244
+
245
+ if (!fs.existsSync(flagPath)) return false
246
+
247
+ return Date.now() - fs.statSync(flagPath).mtimeMs < FAIL_BACKOFF_MS
248
+ } catch {
249
+ return false
250
+ }
251
+ }
252
+
253
+ /** Record an auto-load failure (refreshes the backoff window). */
254
+ const recordFailure = (): void => {
255
+ try {
256
+ const dir = getSessionCacheDir()
257
+
258
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
259
+ fs.writeFileSync(path.join(dir, FAIL_SENTINEL_FILE), '', { mode: 0o600 })
260
+ } catch {
261
+ // No session cache dir — nothing to back off against.
262
+ }
263
+ }
264
+
265
+ /** Clear the failure marker after a successful load. */
266
+ const clearFailure = (): void => {
267
+ try {
268
+ fs.rmSync(path.join(getSessionCacheDir(), FAIL_SENTINEL_FILE), { force: true })
269
+ } catch {
270
+ // No session cache dir — nothing to clear.
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Emit a warning at most once per shell session. Keyed on a flag file in the
276
+ * session cache dir so a misconfigured `envAutoLoad.config` (or a repeated failure)
277
+ * does not spam a warning on every cli-invocation. Falls back to a plain warn when
278
+ * no session cache dir is available.
279
+ */
280
+ const warnOnce = (message: string): void => {
281
+ try {
282
+ const dir = getSessionCacheDir()
283
+ const flagPath = path.join(dir, WARN_SENTINEL_FILE)
284
+
285
+ if (fs.existsSync(flagPath)) return
286
+
287
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
288
+ fs.writeFileSync(flagPath, '', { mode: 0o600 })
289
+ } catch {
290
+ // No session cache dir (e.g. INFRA_KIT_SESSION unset) — warn without de-dup.
291
+ }
292
+
293
+ logger.warn(message)
294
+ }
@@ -0,0 +1,9 @@
1
+ export { decideAutoLoad, resolveEnvAutoLoad, runEnvAutoLoad } from './env-autoload'
2
+ export type {
3
+ AutoLoadDecision,
4
+ AutoLoadDecisionInput,
5
+ AutoLoadEnvSnapshot,
6
+ AutoLoadTrigger,
7
+ ResolvedEnvAutoLoad,
8
+ RunEnvAutoLoadArgs,
9
+ } from './env-autoload'
@@ -15,7 +15,7 @@ export interface OperationErrorContext {
15
15
  * extractStderr({ stderr: 'fatal: ...' }) // 'fatal: ...'
16
16
  * extractStderr({ stderr: '' }) // undefined (empty treated as missing)
17
17
  */
18
- const extractStderr = (cause: unknown): string | undefined => {
18
+ export const extractStderr = (cause: unknown): string | undefined => {
19
19
  if (cause === null || typeof cause !== 'object') return undefined
20
20
  const stderr = (cause as { stderr?: unknown }).stderr
21
21
 
@@ -0,0 +1,71 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { infraKitConfigSchema, infraKitOverrideConfigSchema } from '../infra-kit-config'
4
+
5
+ const baseConfig = {
6
+ environments: ['dev', 'prod'],
7
+ envManagement: { provider: 'doppler', config: { name: 'my-project' } },
8
+ }
9
+
10
+ describe('envAutoLoad schema', () => {
11
+ it('is optional — a config without it parses', () => {
12
+ const result = infraKitConfigSchema.safeParse(baseConfig)
13
+
14
+ expect(result.success).toBe(true)
15
+ expect(result.success && result.data.envAutoLoad).toBeUndefined()
16
+ })
17
+
18
+ it.each(['shell-startup', 'cli-invocation'] as const)('accepts trigger "%s"', (trigger) => {
19
+ const result = infraKitConfigSchema.safeParse({ ...baseConfig, envAutoLoad: { trigger, config: 'dev' } })
20
+
21
+ expect(result.success).toBe(true)
22
+ expect(result.success && result.data.envAutoLoad).toEqual({ trigger, config: 'dev' })
23
+ })
24
+
25
+ it('rejects an unknown trigger value (e.g. "both")', () => {
26
+ const result = infraKitConfigSchema.safeParse({ ...baseConfig, envAutoLoad: { trigger: 'both', config: 'dev' } })
27
+
28
+ expect(result.success).toBe(false)
29
+ })
30
+
31
+ it('rejects a missing trigger', () => {
32
+ const result = infraKitConfigSchema.safeParse({ ...baseConfig, envAutoLoad: { config: 'dev' } })
33
+
34
+ expect(result.success).toBe(false)
35
+ })
36
+
37
+ it('rejects an empty config name', () => {
38
+ const result = infraKitConfigSchema.safeParse({
39
+ ...baseConfig,
40
+ envAutoLoad: { trigger: 'shell-startup', config: '' },
41
+ })
42
+
43
+ expect(result.success).toBe(false)
44
+ })
45
+
46
+ it('rejects unknown keys (strict object)', () => {
47
+ const result = infraKitConfigSchema.safeParse({
48
+ ...baseConfig,
49
+ envAutoLoad: { trigger: 'shell-startup', config: 'dev', extra: true },
50
+ })
51
+
52
+ expect(result.success).toBe(false)
53
+ })
54
+
55
+ // Important: an env name not in `environments` must NOT fail the schema — it is
56
+ // validated (and disabled) at resolve time so a typo never bricks every command.
57
+ it('does NOT reject a config name absent from environments (resolve-time concern)', () => {
58
+ const result = infraKitConfigSchema.safeParse({
59
+ ...baseConfig,
60
+ envAutoLoad: { trigger: 'shell-startup', config: 'not-listed' },
61
+ })
62
+
63
+ expect(result.success).toBe(true)
64
+ })
65
+
66
+ it('is accepted as a partial override layer', () => {
67
+ const result = infraKitOverrideConfigSchema.safeParse({ envAutoLoad: { trigger: 'cli-invocation', config: 'dev' } })
68
+
69
+ expect(result.success).toBe(true)
70
+ })
71
+ })