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.
- package/.eslintcache +1 -1
- package/.omc/state/agent-replay-0d1515ce-4ebe-4ce8-a865-2897f68de168.jsonl +1 -0
- package/.omc/state/agent-replay-1fcfe0ec-7567-41c3-9bfd-501e99e86819.jsonl +19 -0
- package/.omc/state/agent-replay-38382db5-3088-4b5a-aff7-2561a60827c7.jsonl +17 -0
- package/.omc/state/agent-replay-50c36be9-a3a2-42b9-9c85-5a06d3e22f6a.jsonl +5 -0
- package/.omc/state/agent-replay-8198b3e5-fad6-4ee0-9c59-ef773ad89af4.jsonl +1 -0
- package/.omc/state/agent-replay-8cd7652a-b8b5-49cf-9b1f-bee8e5972458.jsonl +3 -0
- package/.omc/state/agent-replay-9bea518f-a9ee-4d14-bb53-a936ebaf647c.jsonl +7 -0
- package/.omc/state/agent-replay-b52d89fb-0e24-4901-bc66-27e3c1697126.jsonl +2 -0
- package/.omc/state/agent-replay-f581a1ae-0ee4-48a3-96f3-ce31a0633bb0.jsonl +1 -0
- package/.omc/state/agent-replay-f99d8dad-81d1-4987-8183-62edf0843d0c.jsonl +2 -0
- package/.omc/state/agent-replay-fe1057b4-9a25-4e8e-8c3f-3e11652ee697.jsonl +5 -0
- package/.omc/state/idle-notif-cooldown.json +1 -1
- package/.omc/state/sessions/0d1515ce-4ebe-4ce8-a865-2897f68de168/pre-tool-advisory-throttle.json +10 -0
- package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/last-tool-error-state.json +7 -0
- package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/mission-state.json +109 -0
- package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/pre-tool-advisory-throttle.json +42 -0
- package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/ralph-state.json +13 -0
- package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/skill-active-state.json +15 -0
- package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/subagent-tracking-state.json +62 -0
- package/.omc/state/sessions/1fcfe0ec-7567-41c3-9bfd-501e99e86819/ultrawork-state.json +11 -0
- package/.omc/state/sessions/23d0ff82-17e5-428b-8743-38f7979bb6f3/last-tool-error-state.json +7 -0
- package/.omc/state/sessions/23d0ff82-17e5-428b-8743-38f7979bb6f3/pre-tool-advisory-throttle.json +18 -0
- package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/last-tool-error-state.json +7 -0
- package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/mission-state.json +145 -0
- package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/pre-tool-advisory-throttle.json +38 -0
- package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/ralplan-state.json +11 -0
- package/.omc/state/sessions/38382db5-3088-4b5a-aff7-2561a60827c7/subagent-tracking-state.json +98 -0
- package/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/last-tool-error-state.json +7 -0
- package/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/pre-tool-advisory-throttle.json +18 -0
- package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/mission-state.json +61 -0
- package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/pre-tool-advisory-throttle.json +22 -0
- package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/skill-active-state.json +15 -0
- package/.omc/state/sessions/50c36be9-a3a2-42b9-9c85-5a06d3e22f6a/subagent-tracking-state.json +26 -0
- package/.omc/state/sessions/560ea519-574a-4713-b134-5bf6cfaf3c33/pre-tool-advisory-throttle.json +18 -0
- package/.omc/state/sessions/8198b3e5-fad6-4ee0-9c59-ef773ad89af4/pre-tool-advisory-throttle.json +14 -0
- package/.omc/state/sessions/8198b3e5-fad6-4ee0-9c59-ef773ad89af4/subagent-tracking-state.json +17 -0
- package/.omc/state/sessions/8cd7652a-b8b5-49cf-9b1f-bee8e5972458/pre-tool-advisory-throttle.json +18 -0
- package/.omc/state/sessions/8cd7652a-b8b5-49cf-9b1f-bee8e5972458/ralplan-state.json +11 -0
- package/.omc/state/sessions/8cd7652a-b8b5-49cf-9b1f-bee8e5972458/subagent-tracking-state.json +35 -0
- package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/last-tool-error-state.json +7 -0
- package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/mission-state.json +79 -0
- package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/pre-tool-advisory-throttle.json +22 -0
- package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/progress.txt +7 -0
- package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/skill-active-state.json +15 -0
- package/.omc/state/sessions/9bea518f-a9ee-4d14-bb53-a936ebaf647c/subagent-tracking-state.json +26 -0
- package/.omc/state/sessions/b52d89fb-0e24-4901-bc66-27e3c1697126/pre-tool-advisory-throttle.json +14 -0
- package/.omc/state/sessions/b52d89fb-0e24-4901-bc66-27e3c1697126/subagent-tracking-state.json +17 -0
- package/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/pre-tool-advisory-throttle.json +18 -0
- package/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/pre-tool-advisory-throttle.json +18 -0
- package/.omc/state/sessions/f581a1ae-0ee4-48a3-96f3-ce31a0633bb0/pre-tool-advisory-throttle.json +18 -0
- package/.omc/state/sessions/f581a1ae-0ee4-48a3-96f3-ce31a0633bb0/subagent-tracking-state.json +17 -0
- package/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/pre-tool-advisory-throttle.json +14 -0
- package/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/ralph-state.json +13 -0
- package/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/ultrawork-state.json +11 -0
- package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/last-tool-error-state.json +7 -0
- package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/mission-state.json +61 -0
- package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/prd.json +22 -0
- package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/pre-tool-advisory-throttle.json +22 -0
- package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/progress.txt +22 -0
- package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/skill-active-state.json +15 -0
- package/.omc/state/sessions/fe1057b4-9a25-4e8e-8c3f-3e11652ee697/subagent-tracking-state.json +26 -0
- package/.turbo/turbo-build.log +3 -3
- package/.turbo/turbo-test.log +92 -44
- package/dist/boot-K4QUAFSP.js +2 -0
- package/dist/boot-K4QUAFSP.js.map +7 -0
- package/dist/chunk-RTSP2FCA.js +2 -0
- package/dist/chunk-RTSP2FCA.js.map +7 -0
- package/dist/chunk-VPBZR2PY.js +166 -0
- package/dist/chunk-VPBZR2PY.js.map +7 -0
- package/dist/cli.js +11 -107
- package/dist/cli.js.map +4 -4
- package/dist/entry/index.d.ts +2 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +4 -4
- package/dist/lib/vendor/config-schema.d.ts +58 -0
- package/dist/mcp.js +1 -54
- package/dist/mcp.js.map +4 -4
- package/eslint.config.js +33 -1
- package/package.json +5 -1
- package/scripts/build.js +11 -1
- package/src/.omc/state/agent-replay-c513ef38-cc3e-4a29-9c4c-1e0137245dd5.jsonl +4 -0
- package/src/.omc/state/agent-replay-ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a.jsonl +2 -0
- package/src/.omc/state/idle-notif-cooldown.json +1 -1
- package/src/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/mission-state.json +79 -0
- package/src/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/pre-tool-advisory-throttle.json +22 -0
- package/src/.omc/state/sessions/c513ef38-cc3e-4a29-9c4c-1e0137245dd5/subagent-tracking-state.json +26 -0
- package/src/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/pre-tool-advisory-throttle.json +18 -0
- package/src/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/ralph-state.json +13 -0
- package/src/.omc/state/sessions/ed3e2cfb-a666-4bb1-bcc1-569d5d0b837a/ultrawork-state.json +11 -0
- package/src/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/pre-tool-advisory-throttle.json +14 -0
- package/src/commands/config/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/last-tool-error-state.json +7 -0
- package/src/commands/config/.omc/state/sessions/41336850-028c-476b-be1a-2164e1e46cac/pre-tool-advisory-throttle.json +18 -0
- package/src/commands/config/config.ts +7 -37
- package/src/commands/doctor/__tests__/agent-files.test.ts +8 -30
- package/src/commands/doctor/__tests__/check-legacy-user-global-config.test.ts +110 -0
- package/src/commands/doctor/doctor.ts +110 -44
- package/src/commands/env-autoload/env-autoload.ts +12 -0
- package/src/commands/env-autoload/index.ts +1 -0
- package/src/commands/env-clear/__tests__/env-clear.test.ts +27 -0
- package/src/commands/env-clear/env-clear.ts +22 -8
- package/src/commands/env-load/__tests__/env-load.test.ts +36 -1
- package/src/commands/env-load/env-load.ts +152 -35
- package/src/commands/env-load/index.ts +2 -1
- package/src/commands/env-status/__tests__/env-status.test.ts +78 -0
- package/src/commands/env-status/env-status.ts +19 -7
- package/src/commands/gh-release-deploy-all/gh-release-deploy-all.ts +45 -1
- package/src/commands/gh-release-deploy-selected/gh-release-deploy-selected.ts +46 -1
- package/src/commands/init/__tests__/agent-files.test.ts +145 -27
- package/src/commands/init/__tests__/migrate-config.test.ts +136 -5
- package/src/commands/init/__tests__/migrate-factory-config.test.ts +93 -0
- package/src/commands/init/__tests__/seed-user-global-config.test.ts +64 -0
- package/src/commands/init/__tests__/shell-body.test.ts +30 -0
- package/src/commands/init/agent-files.ts +65 -45
- package/src/commands/init/init.ts +140 -31
- package/src/commands/init/migrate-config.ts +160 -32
- package/src/commands/vendor-check/__tests__/runtime-isolation.test.ts +71 -0
- package/src/commands/vendor-check/__tests__/vendor-check.test.ts +87 -0
- package/src/commands/vendor-check/index.ts +1 -0
- package/src/commands/vendor-check/vendor-check.ts +127 -0
- package/src/commands/vendor-config/__tests__/vendor-config.test.ts +103 -0
- package/src/commands/vendor-config/index.ts +1 -0
- package/src/commands/vendor-config/vendor-config.ts +162 -0
- package/src/commands/vendor-diff/__tests__/vendor-diff.test.ts +74 -0
- package/src/commands/vendor-diff/index.ts +1 -0
- package/src/commands/vendor-diff/vendor-diff.ts +77 -0
- package/src/commands/vendor-manifest/index.ts +1 -0
- package/src/commands/vendor-manifest/vendor-manifest.ts +60 -0
- package/src/commands/vendor-sync/__tests__/vendor-write.test.ts +86 -0
- package/src/commands/vendor-sync/index.ts +1 -0
- package/src/commands/vendor-sync/vendor-sync.ts +90 -0
- package/src/commands/worktrees-add/worktrees-add.ts +1 -1
- package/src/commands/worktrees-reload/worktrees-reload.ts +6 -5
- package/src/commands/worktrees-remove/worktrees-remove.ts +10 -1
- package/src/commands/worktrees-sync/worktrees-sync.ts +9 -1
- package/src/entry/cli.ts +417 -210
- package/src/entry/index.ts +7 -1
- package/src/integrations/cmux/__tests__/open-workspace-with-layout.test.ts +127 -0
- package/src/integrations/cmux/close-workspace-by-title.ts +2 -2
- package/src/integrations/cmux/list-workspace-titles.ts +1 -1
- package/src/integrations/cmux/open-workspace-with-layout.ts +15 -4
- package/src/integrations/cursor/.omc/state/agent-replay-f99d8dad-81d1-4987-8183-62edf0843d0c.jsonl +2 -0
- package/src/integrations/cursor/.omc/state/idle-notif-cooldown.json +3 -0
- package/src/integrations/cursor/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/pre-tool-advisory-throttle.json +18 -0
- package/src/integrations/cursor/.omc/state/sessions/f99d8dad-81d1-4987-8183-62edf0843d0c/subagent-tracking-state.json +17 -0
- package/src/integrations/cursor/index.ts +1 -0
- package/src/integrations/cursor/launch-cursor.ts +12 -0
- package/src/integrations/cursor/open-cursor-workspace.ts +8 -14
- package/src/integrations/doppler/__tests__/doppler-errors.test.ts +76 -0
- package/src/integrations/doppler/doppler-cli-auth.ts +12 -4
- package/src/integrations/doppler/doppler-errors.ts +88 -0
- package/src/integrations/doppler/doppler-project.ts +80 -0
- package/src/integrations/doppler/index.ts +3 -1
- package/src/integrations/gh/gh-cli-auth/gh-cli-auth.ts +9 -5
- package/src/integrations/gh/gh-release-prs/gh-release-prs.ts +16 -8
- package/src/integrations/ide/__tests__/ide-facade.test.ts +57 -10
- package/src/integrations/ide/add-ide-worktree-folders.ts +2 -4
- package/src/integrations/ide/open-ide-workspace.ts +6 -6
- package/src/integrations/ide/remove-ide-worktree-folders.ts +95 -30
- package/src/integrations/ide/types.ts +10 -1
- package/src/integrations/zed/__tests__/open-zed-workspace.test.ts +1 -17
- package/src/integrations/zed/__tests__/reuse-zed-workspace.test.ts +86 -0
- package/src/integrations/zed/index.ts +1 -0
- package/src/integrations/zed/open-zed-workspace.ts +4 -10
- package/src/integrations/zed/reuse-zed-workspace.ts +57 -0
- package/src/lib/command-catalog/__tests__/__snapshots__/command-catalog.test.ts.snap +212 -0
- package/src/lib/command-catalog/__tests__/command-catalog.test.ts +193 -0
- package/src/lib/command-catalog/command-catalog.ts +141 -0
- package/src/lib/command-catalog/index.ts +2 -0
- package/src/lib/command-echo/__tests__/command-echo.test.ts +127 -0
- package/src/lib/constants/constants.ts +13 -0
- package/src/lib/constants/index.ts +2 -0
- package/src/lib/env-autoload/__tests__/env-autoload.test.ts +288 -0
- package/src/lib/env-autoload/env-autoload.ts +294 -0
- package/src/lib/env-autoload/index.ts +9 -0
- package/src/lib/errors/operation-error.ts +1 -1
- package/src/lib/infra-kit-config/__tests__/env-autoload-schema.test.ts +71 -0
- package/src/lib/infra-kit-config/__tests__/infra-kit-config.test.ts +77 -6
- package/src/lib/infra-kit-config/index.ts +9 -1
- package/src/lib/infra-kit-config/infra-kit-config.ts +71 -11
- package/src/lib/json-output/__tests__/json-output.test.ts +139 -0
- package/src/lib/json-output/index.ts +1 -0
- package/src/lib/json-output/json-output.ts +47 -0
- package/src/lib/managed-block/managed-block.ts +4 -4
- package/src/lib/path-display/index.ts +1 -0
- package/src/lib/path-display/path-display.ts +42 -0
- package/src/lib/render/__tests__/render.test.ts +48 -0
- package/src/lib/render/index.ts +1 -0
- package/src/lib/render/render.ts +24 -0
- package/src/lib/vendor/__tests__/config-schema.test.ts +31 -0
- package/src/lib/vendor/__tests__/config.test.ts +63 -0
- package/src/lib/vendor/__tests__/factory-config.test.ts +92 -0
- package/src/lib/vendor/__tests__/vendor-lib.test.ts +158 -0
- package/src/lib/vendor/__tests__/zx-mock.ts +32 -0
- package/src/lib/vendor/config-schema.ts +78 -0
- package/src/lib/vendor/config.ts +58 -0
- package/src/lib/vendor/factory-config-schema.ts +35 -0
- package/src/lib/vendor/factory-config.ts +91 -0
- package/src/lib/vendor/hash.ts +16 -0
- package/src/lib/vendor/index.ts +23 -0
- package/src/lib/vendor/manifest.ts +151 -0
- package/src/lib/vendor/skip-sets.ts +48 -0
- package/src/lib/vendor/sync-ops.ts +290 -0
- package/src/lib/vendor/walk.ts +56 -0
- package/src/lib/worktrees/remove-worktrees.ts +15 -0
- package/src/mcp/tools/index.ts +5 -42
- package/src/sanity.test.tsx +1 -0
- package/src/tui/__tests__/no-react-boundary.test.ts +51 -0
- package/src/tui/boot.tsx +45 -0
- package/src/tui/screens/__tests__/command-palette.test.tsx +136 -0
- package/src/tui/screens/command-palette.tsx +159 -0
- package/src/tui/types.ts +8 -0
- package/tsconfig.json +1 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/vitest.config.ts +8 -0
- package/src/commands/worktrees-open/__tests__/open-cmux.test.ts +0 -96
- package/src/commands/worktrees-open/index.ts +0 -1
- package/src/commands/worktrees-open/worktrees-open.ts +0 -195
|
@@ -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
|
+
}
|
|
@@ -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
|
+
})
|