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.
- package/.eslintcache +1 -1
- 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-fe1057b4-9a25-4e8e-8c3f-3e11652ee697.jsonl +5 -0
- package/.omc/state/idle-notif-cooldown.json +1 -1
- 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/{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
- 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/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-eslint-check.log +2 -0
- package/.turbo/turbo-test.log +90 -42
- package/dist/boot-K4QUAFSP.js +2 -0
- package/dist/boot-K4QUAFSP.js.map +7 -0
- package/dist/chunk-FEGOOVZE.js +166 -0
- package/dist/chunk-FEGOOVZE.js.map +7 -0
- package/dist/chunk-WA4BQRDC.js +2 -0
- package/dist/chunk-WA4BQRDC.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/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 +104 -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/open-cursor-workspace.ts +6 -11
- 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 +49 -10
- 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/package-config/package-config.ts +1 -3
- 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/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
|
@@ -2,9 +2,10 @@ import path from 'node:path'
|
|
|
2
2
|
import process from 'node:process'
|
|
3
3
|
import { z } from 'zod'
|
|
4
4
|
|
|
5
|
-
import { validateDopplerCliAndAuth } from 'src/integrations/doppler'
|
|
6
5
|
import {
|
|
7
6
|
ENV_LOAD_FILE,
|
|
7
|
+
INFRA_KIT_ENV_AUTOLOADED_VAR,
|
|
8
|
+
INFRA_KIT_ENV_CLEARED_VAR,
|
|
8
9
|
INFRA_KIT_ENV_CONFIG_VAR,
|
|
9
10
|
INFRA_KIT_ENV_LOADED_AT_VAR,
|
|
10
11
|
INFRA_KIT_ENV_PROJECT_VAR,
|
|
@@ -16,11 +17,13 @@ import { logger } from 'src/lib/logger'
|
|
|
16
17
|
import { defineMcpTool, textContent } from 'src/types'
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
|
-
*
|
|
20
|
+
* Report which env is currently loaded in the terminal session. Pure local
|
|
21
|
+
* introspection: reads only `process.env` + the cached env-load.sh — it makes NO
|
|
22
|
+
* Doppler call (use `doctor` for auth/CLI checks), so it works offline and never
|
|
23
|
+
* hangs. Surfaces whether the env was auto-loaded vs manually loaded and whether a
|
|
24
|
+
* clear is currently suppressing auto-load.
|
|
20
25
|
*/
|
|
21
26
|
export const envStatus = async () => {
|
|
22
|
-
await validateDopplerCliAndAuth()
|
|
23
|
-
|
|
24
27
|
logger.info('Environment session status:')
|
|
25
28
|
|
|
26
29
|
// Check session-loaded vars — getSessionCacheDir() throws if INFRA_KIT_SESSION is unset
|
|
@@ -35,6 +38,8 @@ export const envStatus = async () => {
|
|
|
35
38
|
const sessionConfig = process.env[INFRA_KIT_ENV_CONFIG_VAR] ?? null
|
|
36
39
|
const sessionProject = process.env[INFRA_KIT_ENV_PROJECT_VAR] ?? null
|
|
37
40
|
const sessionLoadedAt = process.env[INFRA_KIT_ENV_LOADED_AT_VAR] ?? null
|
|
41
|
+
const autoLoaded = process.env[INFRA_KIT_ENV_AUTOLOADED_VAR] === '1'
|
|
42
|
+
const cleared = Boolean(process.env[INFRA_KIT_ENV_CLEARED_VAR])
|
|
38
43
|
|
|
39
44
|
if (sessionConfig) {
|
|
40
45
|
const varNames = parseVarNamesFromEnvFile(envLoadPath)
|
|
@@ -47,9 +52,10 @@ export const envStatus = async () => {
|
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
const loadedAtDisplay = sessionLoadedAt?.replace(/\.\d{3}Z$/, '') ?? null
|
|
55
|
+
const origin = autoLoaded ? 'auto-loaded' : 'manually loaded'
|
|
50
56
|
|
|
51
57
|
logger.info(
|
|
52
|
-
` ${sessionConfig}: ${sessionLoadedCount} of ${sessionTotalCount} vars loaded (project: ${sessionProject}, loadedAt: ${loadedAtDisplay}, session: ${sessionId})\n`,
|
|
58
|
+
` ${sessionConfig}: ${sessionLoadedCount} of ${sessionTotalCount} vars loaded (${origin}, project: ${sessionProject}, loadedAt: ${loadedAtDisplay}, session: ${sessionId})\n`,
|
|
53
59
|
)
|
|
54
60
|
|
|
55
61
|
if (sessionTotalCount > 0 && sessionLoadedCount < sessionTotalCount) {
|
|
@@ -60,7 +66,9 @@ export const envStatus = async () => {
|
|
|
60
66
|
)
|
|
61
67
|
}
|
|
62
68
|
} else {
|
|
63
|
-
|
|
69
|
+
const clearedNote = cleared ? ' (cleared — auto-load suppressed until a new shell or explicit env-load)' : ''
|
|
70
|
+
|
|
71
|
+
logger.info(` Session ${sessionId}: no env loaded${clearedNote}\n`)
|
|
64
72
|
}
|
|
65
73
|
|
|
66
74
|
const structuredContent = {
|
|
@@ -70,6 +78,8 @@ export const envStatus = async () => {
|
|
|
70
78
|
sessionConfig,
|
|
71
79
|
sessionProject,
|
|
72
80
|
sessionLoadedAt,
|
|
81
|
+
autoLoaded,
|
|
82
|
+
cleared,
|
|
73
83
|
}
|
|
74
84
|
|
|
75
85
|
return {
|
|
@@ -82,7 +92,7 @@ export const envStatus = async () => {
|
|
|
82
92
|
export const envStatusMcpTool = defineMcpTool({
|
|
83
93
|
name: 'env-status',
|
|
84
94
|
description:
|
|
85
|
-
'Report which Doppler project/config is currently loaded in the terminal session, when it was loaded,
|
|
95
|
+
'Report which Doppler project/config is currently loaded in the terminal session, when it was loaded, how many variables are cached, whether it was auto-loaded, and whether a clear is suppressing auto-load. Pure local introspection — makes NO Doppler call (use doctor for auth). Read-only — use env-load / env-clear to change the terminal session.',
|
|
86
96
|
inputSchema: {},
|
|
87
97
|
outputSchema: {
|
|
88
98
|
sessionId: z.string().describe('Current terminal session ID'),
|
|
@@ -91,6 +101,8 @@ export const envStatusMcpTool = defineMcpTool({
|
|
|
91
101
|
sessionConfig: z.string().nullable().describe('Doppler config name of the loaded session (environment name)'),
|
|
92
102
|
sessionProject: z.string().nullable().describe('Doppler project name of the loaded session'),
|
|
93
103
|
sessionLoadedAt: z.string().nullable().describe('ISO 8601 timestamp of when the env was loaded'),
|
|
104
|
+
autoLoaded: z.boolean().describe('True when the loaded env was applied by env auto-load (not a manual env-load)'),
|
|
105
|
+
cleared: z.boolean().describe('True when env-clear is currently suppressing auto-load in this shell'),
|
|
94
106
|
},
|
|
95
107
|
handler: envStatus,
|
|
96
108
|
})
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import confirm from '@inquirer/confirm'
|
|
1
2
|
import select from '@inquirer/select'
|
|
2
3
|
import { z } from 'zod'
|
|
3
4
|
import { $ } from 'zx'
|
|
@@ -21,13 +22,39 @@ interface GhReleaseDeployAllArgs {
|
|
|
21
22
|
version: string
|
|
22
23
|
env: string
|
|
23
24
|
skipTerraform?: boolean
|
|
25
|
+
confirmedCommand?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ConfirmDeployArgs {
|
|
29
|
+
confirmedCommand?: boolean
|
|
30
|
+
branch: string
|
|
31
|
+
env: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Gate the workflow dispatch behind an interactive confirmation. Returns true to
|
|
36
|
+
* proceed; `confirmedCommand` (CLI `--yes`) skips the prompt. Mirrors the
|
|
37
|
+
* confirm+commandEcho pattern used by worktrees-remove.
|
|
38
|
+
*/
|
|
39
|
+
const confirmDeploy = async (args: ConfirmDeployArgs): Promise<boolean> => {
|
|
40
|
+
const { confirmedCommand, branch, env } = args
|
|
41
|
+
|
|
42
|
+
if (confirmedCommand) return true
|
|
43
|
+
|
|
44
|
+
commandEcho.setInteractive()
|
|
45
|
+
|
|
46
|
+
const answer = await confirm({ message: `Deploy ${branch} → ${env}?`, default: false })
|
|
47
|
+
|
|
48
|
+
if (answer) commandEcho.addOption('--yes', true)
|
|
49
|
+
|
|
50
|
+
return answer
|
|
24
51
|
}
|
|
25
52
|
|
|
26
53
|
/**
|
|
27
54
|
* Deploy a release branch to an environment
|
|
28
55
|
*/
|
|
29
56
|
export const ghReleaseDeployAll = async (args: GhReleaseDeployAllArgs) => {
|
|
30
|
-
const { version, env, skipTerraform } = args
|
|
57
|
+
const { version, env, skipTerraform, confirmedCommand } = args
|
|
31
58
|
|
|
32
59
|
commandEcho.start('release-deploy-all')
|
|
33
60
|
|
|
@@ -98,6 +125,23 @@ export const ghReleaseDeployAll = async (args: GhReleaseDeployAllArgs) => {
|
|
|
98
125
|
commandEcho.addOption('--skip-terraform', true)
|
|
99
126
|
}
|
|
100
127
|
|
|
128
|
+
if (!(await confirmDeploy({ confirmedCommand, branch: selectedReleaseBranch, env: selectedEnv }))) {
|
|
129
|
+
logger.info('Deployment cancelled')
|
|
130
|
+
|
|
131
|
+
const structuredContent = {
|
|
132
|
+
releaseBranch: selectedReleaseBranch,
|
|
133
|
+
version: selectedVersion,
|
|
134
|
+
environment: selectedEnv,
|
|
135
|
+
skipTerraformDeploy: shouldSkipTerraform,
|
|
136
|
+
success: false,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
content: textContent(JSON.stringify(structuredContent, null, 2)),
|
|
141
|
+
structuredContent,
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
101
145
|
try {
|
|
102
146
|
$.quiet = true
|
|
103
147
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import checkbox from '@inquirer/checkbox'
|
|
2
|
+
import confirm from '@inquirer/confirm'
|
|
2
3
|
import select from '@inquirer/select'
|
|
3
4
|
import fs from 'node:fs/promises'
|
|
4
5
|
import { resolve } from 'node:path'
|
|
@@ -27,6 +28,32 @@ interface GhReleaseDeploySelectedArgs {
|
|
|
27
28
|
env: string
|
|
28
29
|
services: string[]
|
|
29
30
|
skipTerraform?: boolean
|
|
31
|
+
confirmedCommand?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface ConfirmDeployArgs {
|
|
35
|
+
confirmedCommand?: boolean
|
|
36
|
+
branch: string
|
|
37
|
+
env: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Gate the workflow dispatch behind an interactive confirmation. Returns true to
|
|
42
|
+
* proceed; `confirmedCommand` (CLI `--yes`) skips the prompt. Mirrors the
|
|
43
|
+
* confirm+commandEcho pattern used by worktrees-remove.
|
|
44
|
+
*/
|
|
45
|
+
const confirmDeploy = async (args: ConfirmDeployArgs): Promise<boolean> => {
|
|
46
|
+
const { confirmedCommand, branch, env } = args
|
|
47
|
+
|
|
48
|
+
if (confirmedCommand) return true
|
|
49
|
+
|
|
50
|
+
commandEcho.setInteractive()
|
|
51
|
+
|
|
52
|
+
const answer = await confirm({ message: `Deploy ${branch} → ${env}?`, default: false })
|
|
53
|
+
|
|
54
|
+
if (answer) commandEcho.addOption('--yes', true)
|
|
55
|
+
|
|
56
|
+
return answer
|
|
30
57
|
}
|
|
31
58
|
|
|
32
59
|
/**
|
|
@@ -34,7 +61,7 @@ interface GhReleaseDeploySelectedArgs {
|
|
|
34
61
|
*/
|
|
35
62
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
36
63
|
export const ghReleaseDeploySelected = async (args: GhReleaseDeploySelectedArgs) => {
|
|
37
|
-
const { version, env, services, skipTerraform } = args
|
|
64
|
+
const { version, env, services, skipTerraform, confirmedCommand } = args
|
|
38
65
|
|
|
39
66
|
commandEcho.start('release-deploy-selected')
|
|
40
67
|
|
|
@@ -157,6 +184,24 @@ export const ghReleaseDeploySelected = async (args: GhReleaseDeploySelectedArgs)
|
|
|
157
184
|
commandEcho.addOption('--skip-terraform', true)
|
|
158
185
|
}
|
|
159
186
|
|
|
187
|
+
if (!(await confirmDeploy({ confirmedCommand, branch: selectedReleaseBranch, env: selectedEnv }))) {
|
|
188
|
+
logger.info('Deployment cancelled')
|
|
189
|
+
|
|
190
|
+
const structuredContent = {
|
|
191
|
+
releaseBranch: selectedReleaseBranch,
|
|
192
|
+
version: selectedVersion,
|
|
193
|
+
environment: selectedEnv,
|
|
194
|
+
services: selectedServices,
|
|
195
|
+
skipTerraformDeploy: shouldSkipTerraform,
|
|
196
|
+
success: false,
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
content: textContent(JSON.stringify(structuredContent, null, 2)),
|
|
201
|
+
structuredContent,
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
160
205
|
try {
|
|
161
206
|
$.quiet = true
|
|
162
207
|
|
|
@@ -5,7 +5,13 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
5
5
|
|
|
6
6
|
import { getProjectRoot, getRepoName } from 'src/lib/git-utils'
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
AGENTS_IMPORT_END,
|
|
10
|
+
AGENTS_IMPORT_START,
|
|
11
|
+
AGENTS_MARKER_END,
|
|
12
|
+
AGENTS_MARKER_START,
|
|
13
|
+
writeAgentFiles,
|
|
14
|
+
} from '../agent-files'
|
|
9
15
|
|
|
10
16
|
vi.mock('src/lib/git-utils', () => {
|
|
11
17
|
return {
|
|
@@ -46,32 +52,30 @@ describe('writeAgentFiles', () => {
|
|
|
46
52
|
vi.clearAllMocks()
|
|
47
53
|
})
|
|
48
54
|
|
|
49
|
-
it('
|
|
55
|
+
it('writes the full guidance block only to CLAUDE.md, and creates no other files, on a fresh repo', async () => {
|
|
50
56
|
await withTmpRepo(async (tmp) => {
|
|
51
57
|
const result = await writeAgentFiles()
|
|
52
58
|
|
|
53
59
|
expect(result.skipped).toBe(false)
|
|
54
60
|
expect(result.root).toBe(tmp)
|
|
55
61
|
|
|
56
|
-
const agents = fs.readFileSync(path.join(tmp, 'AGENTS.md'), 'utf-8')
|
|
57
|
-
|
|
58
|
-
expect(agents).toContain(AGENTS_MARKER_START)
|
|
59
|
-
expect(agents).toContain(AGENTS_MARKER_END)
|
|
60
|
-
expect(agents).toContain('<!-- infra-kit:version ')
|
|
61
|
-
|
|
62
62
|
const claude = fs.readFileSync(path.join(tmp, 'CLAUDE.md'), 'utf-8')
|
|
63
63
|
|
|
64
|
-
expect(claude).toContain(
|
|
65
|
-
expect(claude).toContain(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
expect(
|
|
70
|
-
expect(
|
|
64
|
+
expect(claude).toContain(AGENTS_MARKER_START)
|
|
65
|
+
expect(claude).toContain(AGENTS_MARKER_END)
|
|
66
|
+
expect(claude).toContain('<!-- infra-kit:version ')
|
|
67
|
+
expect(claude).toContain('# infra-kit')
|
|
68
|
+
// The migrated `@AGENTS.md` import region must never appear.
|
|
69
|
+
expect(claude).not.toContain(AGENTS_IMPORT_START)
|
|
70
|
+
expect(claude).not.toContain('@AGENTS.md')
|
|
71
|
+
|
|
72
|
+
// CLAUDE.md is the only output — infra-kit no longer touches AGENTS.md or .cursor.
|
|
73
|
+
expect(fs.existsSync(path.join(tmp, 'AGENTS.md'))).toBe(false)
|
|
74
|
+
expect(fs.existsSync(path.join(tmp, '.cursor'))).toBe(false)
|
|
71
75
|
})
|
|
72
76
|
})
|
|
73
77
|
|
|
74
|
-
it('preserves existing hand-authored CLAUDE.md content
|
|
78
|
+
it('preserves existing hand-authored CLAUDE.md content and appends the block after it', async () => {
|
|
75
79
|
await withTmpRepo(async (tmp) => {
|
|
76
80
|
const claudePath = path.join(tmp, 'CLAUDE.md')
|
|
77
81
|
const userContent = '# CLAUDE.md\n\n## Ticket Naming Convention\n\n- `[FE]` — frontend\n- `[BE]` — backend\n'
|
|
@@ -84,17 +88,19 @@ describe('writeAgentFiles', () => {
|
|
|
84
88
|
|
|
85
89
|
expect(updated).toContain('## Ticket Naming Convention')
|
|
86
90
|
expect(updated).toContain('`[FE]` — frontend')
|
|
87
|
-
expect(updated).toContain(
|
|
91
|
+
expect(updated).toContain(AGENTS_MARKER_START)
|
|
92
|
+
// The managed block lands AFTER the hand-authored content.
|
|
93
|
+
expect(updated.indexOf('## Ticket Naming Convention')).toBeLessThan(updated.indexOf(AGENTS_MARKER_START))
|
|
88
94
|
})
|
|
89
95
|
})
|
|
90
96
|
|
|
91
|
-
it('is idempotent — a second run does not duplicate the block', async () => {
|
|
97
|
+
it('is idempotent — a second run does not duplicate the block and reports unchanged (same CLI version)', async () => {
|
|
92
98
|
await withTmpRepo(async (tmp) => {
|
|
93
99
|
await writeAgentFiles()
|
|
94
|
-
const first = fs.readFileSync(path.join(tmp, '
|
|
100
|
+
const first = fs.readFileSync(path.join(tmp, 'CLAUDE.md'), 'utf-8')
|
|
95
101
|
|
|
96
102
|
const second = await writeAgentFiles()
|
|
97
|
-
const after = fs.readFileSync(path.join(tmp, '
|
|
103
|
+
const after = fs.readFileSync(path.join(tmp, 'CLAUDE.md'), 'utf-8')
|
|
98
104
|
|
|
99
105
|
expect(after).toBe(first)
|
|
100
106
|
expect(after.match(new RegExp(AGENTS_MARKER_START, 'g'))?.length).toBe(1)
|
|
@@ -113,35 +119,147 @@ describe('writeAgentFiles', () => {
|
|
|
113
119
|
|
|
114
120
|
expect(result.skipped).toBe(true)
|
|
115
121
|
expect(result.root).toBeNull()
|
|
116
|
-
expect(fs.existsSync(path.join(tmp, '
|
|
122
|
+
expect(fs.existsSync(path.join(tmp, 'CLAUDE.md'))).toBe(false)
|
|
117
123
|
},
|
|
118
124
|
{ repo: false },
|
|
119
125
|
)
|
|
120
126
|
})
|
|
121
127
|
|
|
122
|
-
it('refuses to write through a symlinked
|
|
128
|
+
it('refuses to write through a symlinked CLAUDE.md', async () => {
|
|
123
129
|
await withTmpRepo(async (tmp) => {
|
|
124
130
|
const realTarget = path.join(tmp, 'elsewhere.md')
|
|
125
131
|
|
|
126
132
|
writeFile(realTarget, 'sensitive')
|
|
127
|
-
fs.symlinkSync(realTarget, path.join(tmp, '
|
|
133
|
+
fs.symlinkSync(realTarget, path.join(tmp, 'CLAUDE.md'))
|
|
128
134
|
|
|
129
135
|
await expect(writeAgentFiles()).rejects.toThrow(/symlink/)
|
|
130
136
|
})
|
|
131
137
|
})
|
|
132
138
|
|
|
133
|
-
it('backs up an existing
|
|
139
|
+
it('backs up an existing CLAUDE.md before overwriting', async () => {
|
|
134
140
|
await withTmpRepo(async (tmp) => {
|
|
135
|
-
writeFile(path.join(tmp, '
|
|
141
|
+
writeFile(path.join(tmp, 'CLAUDE.md'), 'previous content without markers\n')
|
|
136
142
|
|
|
137
143
|
await writeAgentFiles()
|
|
138
144
|
|
|
139
145
|
const backups = fs.readdirSync(tmp).filter((f) => {
|
|
140
|
-
return f.startsWith('
|
|
146
|
+
return f.startsWith('CLAUDE.md.backup.')
|
|
141
147
|
})
|
|
142
148
|
|
|
143
|
-
expect(backups
|
|
149
|
+
expect(backups).toHaveLength(1)
|
|
144
150
|
expect(fs.readFileSync(path.join(tmp, backups[0]!), 'utf-8')).toContain('previous content without markers')
|
|
145
151
|
})
|
|
146
152
|
})
|
|
153
|
+
|
|
154
|
+
describe('legacy AGENTS.md migration', () => {
|
|
155
|
+
/** Seed a repo in the pre-migration state: AGENTS.md (full block) + CLAUDE.md (@AGENTS.md import). */
|
|
156
|
+
const seedLegacy = async (
|
|
157
|
+
tmp: string,
|
|
158
|
+
opts: { agentsExtra?: string; claudeExtra?: string } = {},
|
|
159
|
+
): Promise<void> => {
|
|
160
|
+
const agentsBlock = `${AGENTS_MARKER_START}\n# infra-kit\nlegacy body\n${AGENTS_MARKER_END}\n`
|
|
161
|
+
const importBlock = `${AGENTS_IMPORT_START}\n@AGENTS.md\n${AGENTS_IMPORT_END}\n`
|
|
162
|
+
|
|
163
|
+
writeFile(path.join(tmp, 'AGENTS.md'), `${opts.agentsExtra ?? ''}${agentsBlock}`)
|
|
164
|
+
writeFile(path.join(tmp, 'CLAUDE.md'), `${opts.claudeExtra ?? ''}${importBlock}`)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
it('removes a purely-generated AGENTS.md (with a backup) and strips the @AGENTS.md import from CLAUDE.md', async () => {
|
|
168
|
+
await withTmpRepo(async (tmp) => {
|
|
169
|
+
await seedLegacy(tmp)
|
|
170
|
+
|
|
171
|
+
await writeAgentFiles()
|
|
172
|
+
|
|
173
|
+
// AGENTS.md is gone, but a backup survives.
|
|
174
|
+
expect(fs.existsSync(path.join(tmp, 'AGENTS.md'))).toBe(false)
|
|
175
|
+
const backups = fs.readdirSync(tmp).filter((f) => {
|
|
176
|
+
return f.startsWith('AGENTS.md.backup.')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
expect(backups).toHaveLength(1)
|
|
180
|
+
|
|
181
|
+
// CLAUDE.md now carries the full block and no longer imports @AGENTS.md.
|
|
182
|
+
const claude = fs.readFileSync(path.join(tmp, 'CLAUDE.md'), 'utf-8')
|
|
183
|
+
|
|
184
|
+
expect(claude).toContain(AGENTS_MARKER_START)
|
|
185
|
+
expect(claude).not.toContain(AGENTS_IMPORT_START)
|
|
186
|
+
expect(claude).not.toContain('@AGENTS.md')
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('retains an AGENTS.md that has hand-authored content (block removed, file kept, backup made)', async () => {
|
|
191
|
+
await withTmpRepo(async (tmp) => {
|
|
192
|
+
await seedLegacy(tmp, { agentsExtra: '# My own notes\n\nkeep me\n\n' })
|
|
193
|
+
|
|
194
|
+
await writeAgentFiles()
|
|
195
|
+
|
|
196
|
+
// File retained with hand content; the infra-kit block is stripped.
|
|
197
|
+
expect(fs.existsSync(path.join(tmp, 'AGENTS.md'))).toBe(true)
|
|
198
|
+
const agents = fs.readFileSync(path.join(tmp, 'AGENTS.md'), 'utf-8')
|
|
199
|
+
|
|
200
|
+
expect(agents).toContain('# My own notes')
|
|
201
|
+
expect(agents).toContain('keep me')
|
|
202
|
+
expect(agents).not.toContain(AGENTS_MARKER_START)
|
|
203
|
+
|
|
204
|
+
// A backup of the pre-strip file exists.
|
|
205
|
+
const backups = fs.readdirSync(tmp).filter((f) => {
|
|
206
|
+
return f.startsWith('AGENTS.md.backup.')
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
expect(backups).toHaveLength(1)
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('leaves an AGENTS.md with no infra-kit block untouched (not ours)', async () => {
|
|
214
|
+
await withTmpRepo(async (tmp) => {
|
|
215
|
+
const handAuthored = '# AGENTS\n\nentirely hand-written, no markers\n'
|
|
216
|
+
|
|
217
|
+
writeFile(path.join(tmp, 'AGENTS.md'), handAuthored)
|
|
218
|
+
|
|
219
|
+
await writeAgentFiles()
|
|
220
|
+
|
|
221
|
+
expect(fs.readFileSync(path.join(tmp, 'AGENTS.md'), 'utf-8')).toBe(handAuthored)
|
|
222
|
+
const backups = fs.readdirSync(tmp).filter((f) => {
|
|
223
|
+
return f.startsWith('AGENTS.md.backup.')
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
expect(backups).toHaveLength(0)
|
|
227
|
+
})
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('is idempotent after migration — a second run finds nothing left to migrate', async () => {
|
|
231
|
+
await withTmpRepo(async (tmp) => {
|
|
232
|
+
await seedLegacy(tmp)
|
|
233
|
+
await writeAgentFiles()
|
|
234
|
+
|
|
235
|
+
const second = await writeAgentFiles()
|
|
236
|
+
|
|
237
|
+
expect(
|
|
238
|
+
second.written.every((w) => {
|
|
239
|
+
return w.action === 'unchanged'
|
|
240
|
+
}),
|
|
241
|
+
).toBe(true)
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('does not touch a pre-existing .cursor/rules file — infra-kit no longer supports Cursor rules', async () => {
|
|
247
|
+
await withTmpRepo(async (tmp) => {
|
|
248
|
+
const cursorPath = path.join(tmp, '.cursor', 'rules', 'infra-kit.mdc')
|
|
249
|
+
// Even a file that carries our managed block is left completely alone now.
|
|
250
|
+
const existing = `---\ndescription: x\n---\n${AGENTS_MARKER_START}\n# infra-kit\nold body\n${AGENTS_MARKER_END}\n`
|
|
251
|
+
|
|
252
|
+
writeFile(cursorPath, existing)
|
|
253
|
+
|
|
254
|
+
await writeAgentFiles()
|
|
255
|
+
|
|
256
|
+
// Untouched: same content, no backup, no deletion.
|
|
257
|
+
expect(fs.readFileSync(cursorPath, 'utf-8')).toBe(existing)
|
|
258
|
+
const backups = fs.readdirSync(path.join(tmp, '.cursor', 'rules')).filter((f) => {
|
|
259
|
+
return f.startsWith('infra-kit.mdc.backup.')
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
expect(backups).toHaveLength(0)
|
|
263
|
+
})
|
|
264
|
+
})
|
|
147
265
|
})
|
|
@@ -6,7 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
6
6
|
// Import AFTER the mock is declared so the module picks up the mocked dep.
|
|
7
7
|
import { getProjectRoot, getRepoName } from 'src/lib/git-utils'
|
|
8
8
|
|
|
9
|
-
import { migrateLegacyConfig, normalizeLegacyIdeStructures } from '../migrate-config'
|
|
9
|
+
import { migrateLegacyConfig, migrateUserGlobalConfigFilename, normalizeLegacyIdeStructures } from '../migrate-config'
|
|
10
10
|
|
|
11
11
|
vi.mock('src/lib/git-utils', () => {
|
|
12
12
|
return {
|
|
@@ -105,8 +105,10 @@ describe('migrateLegacyConfig', () => {
|
|
|
105
105
|
await withTmpRepo(async (tmp) => {
|
|
106
106
|
const mainYml = path.join(tmp, 'infra-kit.yml')
|
|
107
107
|
const mainJson = path.join(tmp, 'infra-kit.json')
|
|
108
|
+
// The user-global legacy YAML keeps the old `config.yml` name; its JSON
|
|
109
|
+
// target is the renamed canonical `infra-kit.json`.
|
|
108
110
|
const userGlobalYml = path.join(tmp, '.infra-kit', 'config.yml')
|
|
109
|
-
const userGlobalJson = path.join(tmp, '.infra-kit', '
|
|
111
|
+
const userGlobalJson = path.join(tmp, '.infra-kit', 'infra-kit.json')
|
|
110
112
|
|
|
111
113
|
writeFile(mainYml, MAIN_YML)
|
|
112
114
|
// Invalid override: environments present but empty (min(1) fails).
|
|
@@ -151,9 +153,17 @@ describe('migrateLegacyConfig', () => {
|
|
|
151
153
|
|
|
152
154
|
await migrateLegacyConfig()
|
|
153
155
|
|
|
154
|
-
|
|
156
|
+
// The user-global legacy `config.yml` migrates to the renamed canonical
|
|
157
|
+
// `infra-kit.json`; main/userProject keep their `.yml`→`.json` sibling name.
|
|
158
|
+
const pairs: [yml: string, json: string][] = [
|
|
159
|
+
[mainYml, path.join(tmp, 'infra-kit.json')],
|
|
160
|
+
[userGlobalYml, path.join(tmp, '.infra-kit', 'infra-kit.json')],
|
|
161
|
+
[userProjectYml, path.join(tmp, '.infra-kit', 'projects', projectName, 'infra-kit.json')],
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
for (const [yml, json] of pairs) {
|
|
155
165
|
expect(fs.existsSync(yml)).toBe(false)
|
|
156
|
-
expect(fs.existsSync(
|
|
166
|
+
expect(fs.existsSync(json)).toBe(true)
|
|
157
167
|
}
|
|
158
168
|
})
|
|
159
169
|
})
|
|
@@ -221,7 +231,7 @@ describe('normalizeLegacyIdeStructures', () => {
|
|
|
221
231
|
|
|
222
232
|
it('normalizes the user-global config layer too', async () => {
|
|
223
233
|
await withTmpRepo(async (tmp) => {
|
|
224
|
-
const userGlobalJson = path.join(tmp, '.infra-kit', '
|
|
234
|
+
const userGlobalJson = path.join(tmp, '.infra-kit', 'infra-kit.json')
|
|
225
235
|
|
|
226
236
|
writeFile(userGlobalJson, JSON.stringify({ ide: { provider: 'zed', config: { mode: 'windows' } } }))
|
|
227
237
|
|
|
@@ -289,3 +299,124 @@ describe('normalizeLegacyIdeStructures', () => {
|
|
|
289
299
|
})
|
|
290
300
|
})
|
|
291
301
|
})
|
|
302
|
+
|
|
303
|
+
describe('migrateUserGlobalConfigFilename', () => {
|
|
304
|
+
beforeEach(() => {
|
|
305
|
+
vi.clearAllMocks()
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
afterEach(() => {
|
|
309
|
+
vi.clearAllMocks()
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
const userGlobalPaths = (tmp: string) => {
|
|
313
|
+
const dir = path.join(tmp, '.infra-kit')
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
legacyConfig: path.join(dir, 'config.json'),
|
|
317
|
+
newConfig: path.join(dir, 'infra-kit.json'),
|
|
318
|
+
legacyExample: path.join(dir, 'config.example.jsonc'),
|
|
319
|
+
newExample: path.join(dir, 'infra-kit.example.jsonc'),
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
it('renames config.json → infra-kit.json (and the example) preserving content', async () => {
|
|
324
|
+
await withTmpRepo(async (tmp) => {
|
|
325
|
+
const p = userGlobalPaths(tmp)
|
|
326
|
+
const configBody = JSON.stringify({ worktrees: { openInCmux: true } })
|
|
327
|
+
const exampleBody = '// example\n{}\n'
|
|
328
|
+
|
|
329
|
+
writeFile(p.legacyConfig, configBody)
|
|
330
|
+
writeFile(p.legacyExample, exampleBody)
|
|
331
|
+
|
|
332
|
+
await migrateUserGlobalConfigFilename()
|
|
333
|
+
|
|
334
|
+
expect(fs.existsSync(p.legacyConfig)).toBe(false)
|
|
335
|
+
expect(fs.existsSync(p.legacyExample)).toBe(false)
|
|
336
|
+
expect(fs.readFileSync(p.newConfig, 'utf-8')).toBe(configBody)
|
|
337
|
+
expect(fs.readFileSync(p.newExample, 'utf-8')).toBe(exampleBody)
|
|
338
|
+
})
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
it('is a no-op when there is no legacy config.json (idempotent)', async () => {
|
|
342
|
+
await withTmpRepo(async (tmp) => {
|
|
343
|
+
const p = userGlobalPaths(tmp)
|
|
344
|
+
|
|
345
|
+
await expect(migrateUserGlobalConfigFilename()).resolves.toBeUndefined()
|
|
346
|
+
|
|
347
|
+
expect(fs.existsSync(p.legacyConfig)).toBe(false)
|
|
348
|
+
expect(fs.existsSync(p.newConfig)).toBe(false)
|
|
349
|
+
})
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
it('never overwrites an existing infra-kit.json; leaves the stale config.json in place', async () => {
|
|
353
|
+
await withTmpRepo(async (tmp) => {
|
|
354
|
+
const p = userGlobalPaths(tmp)
|
|
355
|
+
const legacyBody = JSON.stringify({ worktrees: { openInCmux: false } })
|
|
356
|
+
const activeBody = JSON.stringify({ worktrees: { openInCmux: true } })
|
|
357
|
+
|
|
358
|
+
writeFile(p.legacyConfig, legacyBody)
|
|
359
|
+
writeFile(p.newConfig, activeBody)
|
|
360
|
+
|
|
361
|
+
await migrateUserGlobalConfigFilename()
|
|
362
|
+
|
|
363
|
+
// Active config untouched (NOT clobbered); legacy file preserved for the user.
|
|
364
|
+
expect(fs.readFileSync(p.newConfig, 'utf-8')).toBe(activeBody)
|
|
365
|
+
expect(fs.readFileSync(p.legacyConfig, 'utf-8')).toBe(legacyBody)
|
|
366
|
+
})
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
it('migrates each file independently (example present, config absent)', async () => {
|
|
370
|
+
await withTmpRepo(async (tmp) => {
|
|
371
|
+
const p = userGlobalPaths(tmp)
|
|
372
|
+
const exampleBody = '// example only\n{}\n'
|
|
373
|
+
|
|
374
|
+
writeFile(p.legacyExample, exampleBody)
|
|
375
|
+
|
|
376
|
+
await migrateUserGlobalConfigFilename()
|
|
377
|
+
|
|
378
|
+
expect(fs.existsSync(p.legacyExample)).toBe(false)
|
|
379
|
+
expect(fs.readFileSync(p.newExample, 'utf-8')).toBe(exampleBody)
|
|
380
|
+
// No config.json existed → nothing created on that side.
|
|
381
|
+
expect(fs.existsSync(p.newConfig)).toBe(false)
|
|
382
|
+
})
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
it('yAML-legacy upgrade: migrateLegacyConfig + rename land config at infra-kit.json', async () => {
|
|
386
|
+
await withTmpRepo(async (tmp) => {
|
|
387
|
+
const p = userGlobalPaths(tmp)
|
|
388
|
+
const legacyYml = path.join(tmp, '.infra-kit', 'config.yml')
|
|
389
|
+
|
|
390
|
+
writeFile(legacyYml, 'worktrees:\n openInCmux: true\n')
|
|
391
|
+
|
|
392
|
+
// migrateLegacyConfig converts the pinned config.yml source → infra-kit.json;
|
|
393
|
+
// the rename then has nothing left to do for the user-global config.
|
|
394
|
+
await migrateLegacyConfig()
|
|
395
|
+
await migrateUserGlobalConfigFilename()
|
|
396
|
+
|
|
397
|
+
expect(fs.existsSync(legacyYml)).toBe(false)
|
|
398
|
+
expect(fs.existsSync(p.legacyConfig)).toBe(false)
|
|
399
|
+
expect(JSON.parse(fs.readFileSync(p.newConfig, 'utf-8')).worktrees.openInCmux).toBe(true)
|
|
400
|
+
})
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
it('both-legacy files: YAML wins via migrateLegacyConfig, config.json is left untouched', async () => {
|
|
404
|
+
await withTmpRepo(async (tmp) => {
|
|
405
|
+
const p = userGlobalPaths(tmp)
|
|
406
|
+
const legacyYml = path.join(tmp, '.infra-kit', 'config.yml')
|
|
407
|
+
const jsonBody = JSON.stringify({ worktrees: { openInCmux: false } })
|
|
408
|
+
|
|
409
|
+
writeFile(legacyYml, 'worktrees:\n openInCmux: true\n')
|
|
410
|
+
writeFile(p.legacyConfig, jsonBody)
|
|
411
|
+
|
|
412
|
+
// init() order: migrateLegacyConfig (yml→infra-kit.json) then the rename.
|
|
413
|
+
await migrateLegacyConfig()
|
|
414
|
+
await migrateUserGlobalConfigFilename()
|
|
415
|
+
|
|
416
|
+
// YAML content won at infra-kit.json; the rename's no-overwrite guard then
|
|
417
|
+
// skipped config.json, leaving it untouched (no data loss).
|
|
418
|
+
expect(JSON.parse(fs.readFileSync(p.newConfig, 'utf-8')).worktrees.openInCmux).toBe(true)
|
|
419
|
+
expect(fs.readFileSync(p.legacyConfig, 'utf-8')).toBe(jsonBody)
|
|
420
|
+
})
|
|
421
|
+
})
|
|
422
|
+
})
|