infra-kit 0.1.102 → 0.1.107

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 (147) hide show
  1. package/.eslintcache +1 -1
  2. package/.omc/state/agent-replay-0a58307d-2a37-4c69-851c-83a646502d62.jsonl +1 -0
  3. package/.omc/state/agent-replay-11c41aa0-51fa-49e1-a1dc-26dcd6ac26cc.jsonl +16 -0
  4. package/.omc/state/agent-replay-4cf1c186-81b2-497c-b002-d7f84e7839f3.jsonl +9 -0
  5. package/.omc/state/agent-replay-5c4ab554-64f1-42ae-83e3-21e0237e955c.jsonl +11 -0
  6. package/.omc/state/agent-replay-a60ac2ec-afbd-449f-a540-6df287392fc2.jsonl +1 -0
  7. package/.omc/state/agent-replay-afc6290b-40d3-4bef-b3b6-14484c034ab9.jsonl +14 -0
  8. package/.omc/state/agent-replay-be37e426-6fc8-47f4-8178-221c8494551c.jsonl +3 -0
  9. package/.omc/state/agent-replay-c967c819-3d1c-447b-ab48-56a8448ef9f8.jsonl +2 -0
  10. package/.omc/state/agent-replay-e947a3c6-989d-4a60-91dd-6b0ddd827b2d.jsonl +3 -0
  11. package/.omc/state/idle-notif-cooldown.json +3 -0
  12. package/.omc/state/last-tool-error.json +4 -4
  13. package/.omc/state/mission-state.json +53 -0
  14. package/.omc/state/sessions/0a58307d-2a37-4c69-851c-83a646502d62/pre-tool-advisory-throttle.json +18 -0
  15. package/.omc/state/sessions/0a58307d-2a37-4c69-851c-83a646502d62/subagent-tracking-state.json +7 -0
  16. package/.omc/state/sessions/11c41aa0-51fa-49e1-a1dc-26dcd6ac26cc/last-tool-error-state.json +7 -0
  17. package/.omc/state/sessions/11c41aa0-51fa-49e1-a1dc-26dcd6ac26cc/mission-state.json +117 -0
  18. package/.omc/state/sessions/11c41aa0-51fa-49e1-a1dc-26dcd6ac26cc/pre-tool-advisory-throttle.json +42 -0
  19. package/.omc/state/sessions/11c41aa0-51fa-49e1-a1dc-26dcd6ac26cc/subagent-tracking-state.json +53 -0
  20. package/.omc/state/sessions/4cf1c186-81b2-497c-b002-d7f84e7839f3/last-tool-error-state.json +7 -0
  21. package/.omc/state/sessions/4cf1c186-81b2-497c-b002-d7f84e7839f3/pre-tool-advisory-throttle.json +18 -0
  22. package/.omc/state/sessions/4cf1c186-81b2-497c-b002-d7f84e7839f3/subagent-tracking-state.json +7 -0
  23. package/.omc/state/sessions/5c4ab554-64f1-42ae-83e3-21e0237e955c/mission-state.json +117 -0
  24. package/.omc/state/sessions/5c4ab554-64f1-42ae-83e3-21e0237e955c/pre-tool-advisory-throttle.json +18 -0
  25. package/.omc/state/sessions/5c4ab554-64f1-42ae-83e3-21e0237e955c/subagent-tracking-state.json +17 -0
  26. package/.omc/state/sessions/a60ac2ec-afbd-449f-a540-6df287392fc2/pre-tool-advisory-throttle.json +18 -0
  27. package/.omc/state/sessions/a60ac2ec-afbd-449f-a540-6df287392fc2/subagent-tracking-state.json +7 -0
  28. package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/last-tool-error-state.json +7 -0
  29. package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/mission-state.json +89 -0
  30. package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/pre-tool-advisory-throttle.json +34 -0
  31. package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/ralph-state.json +13 -0
  32. package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/skill-active-state.json +15 -0
  33. package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/subagent-tracking-state.json +35 -0
  34. package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/ultrawork-state.json +11 -0
  35. package/.omc/state/sessions/c967c819-3d1c-447b-ab48-56a8448ef9f8/pre-tool-advisory-throttle.json +10 -0
  36. package/.omc/state/sessions/c967c819-3d1c-447b-ab48-56a8448ef9f8/subagent-tracking-state.json +7 -0
  37. package/.omc/state/sessions/e947a3c6-989d-4a60-91dd-6b0ddd827b2d/last-tool-error-state.json +7 -0
  38. package/.omc/state/sessions/e947a3c6-989d-4a60-91dd-6b0ddd827b2d/pre-tool-advisory-throttle.json +10 -0
  39. package/.omc/state/sessions/e947a3c6-989d-4a60-91dd-6b0ddd827b2d/subagent-tracking-state.json +26 -0
  40. package/.omc/state/subagent-tracking.json +14 -4
  41. package/.turbo/turbo-build.log +7 -0
  42. package/.turbo/turbo-check.log +14 -0
  43. package/.turbo/turbo-prettier-fix.log +2 -1
  44. package/.turbo/turbo-test.log +28 -5
  45. package/.turbo/turbo-validate.log +14 -0
  46. package/dist/cli.js +88 -74
  47. package/dist/cli.js.map +4 -4
  48. package/dist/entry/index.d.ts +2 -0
  49. package/dist/index.js +2 -0
  50. package/dist/index.js.map +7 -0
  51. package/dist/lib/package-config/package-config.d.ts +71 -0
  52. package/dist/mcp.js +43 -41
  53. package/dist/mcp.js.map +4 -4
  54. package/eslint.config.js +1 -1
  55. package/infra-kit.config.ts +5 -0
  56. package/package.json +20 -13
  57. package/scripts/build.js +32 -3
  58. package/src/.omc/state/sessions/0a58307d-2a37-4c69-851c-83a646502d62/pre-tool-advisory-throttle.json +18 -0
  59. package/src/commands/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/pre-tool-advisory-throttle.json +14 -0
  60. package/src/commands/.omc/state/sessions/c967c819-3d1c-447b-ab48-56a8448ef9f8/pre-tool-advisory-throttle.json +18 -0
  61. package/src/commands/audit/__tests__/audit.test.ts +59 -0
  62. package/src/commands/audit/audit.ts +177 -0
  63. package/src/commands/audit/index.ts +1 -0
  64. package/src/commands/config/config.ts +49 -7
  65. package/src/commands/doctor/__tests__/agent-files.test.ts +110 -0
  66. package/src/commands/doctor/doctor.ts +69 -4
  67. package/src/commands/env-clear/env-clear.ts +1 -1
  68. package/src/commands/env-list/env-list.ts +3 -3
  69. package/src/commands/env-load/env-load.ts +1 -1
  70. package/src/commands/env-status/env-status.ts +1 -1
  71. package/src/commands/gh-merge-dev/gh-merge-dev.ts +3 -8
  72. package/src/commands/gh-release-deliver/gh-release-deliver.ts +47 -21
  73. package/src/commands/gh-release-deploy-all/gh-release-deploy-all.ts +13 -7
  74. package/src/commands/gh-release-deploy-selected/gh-release-deploy-selected.ts +12 -6
  75. package/src/commands/gh-release-list/gh-release-list.ts +19 -8
  76. package/src/commands/init/__tests__/agent-files.test.ts +147 -0
  77. package/src/commands/init/__tests__/migrate-config.test.ts +160 -0
  78. package/src/commands/init/agent-files.ts +199 -0
  79. package/src/commands/init/index.ts +7 -0
  80. package/src/commands/init/init.ts +82 -60
  81. package/src/commands/init/migrate-config.ts +146 -0
  82. package/src/commands/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  83. package/src/commands/release-create/__tests__/release-create.test.ts +55 -0
  84. package/src/commands/release-create/release-create.ts +142 -38
  85. package/src/commands/release-desc-edit/release-desc-edit.ts +28 -8
  86. package/src/commands/version/version.ts +1 -1
  87. package/src/commands/worktrees-add/worktrees-add.ts +7 -12
  88. package/src/commands/worktrees-list/worktrees-list.ts +13 -5
  89. package/src/commands/worktrees-open/worktrees-open.ts +1 -1
  90. package/src/commands/worktrees-remove/worktrees-remove.ts +6 -10
  91. package/src/commands/worktrees-sync/worktrees-sync.ts +3 -5
  92. package/src/entry/cli.ts +50 -7
  93. package/src/entry/index.ts +5 -0
  94. package/src/integrations/cmux/open-workspace-with-layout.ts +4 -4
  95. package/src/integrations/cmux/workspace-title.ts +10 -4
  96. package/src/integrations/doppler/doppler-project.ts +1 -1
  97. package/src/integrations/gh/gh-release-prs/__tests__/gh-release-prs.test.ts +115 -0
  98. package/src/integrations/gh/gh-release-prs/gh-release-prs.ts +49 -32
  99. package/src/lib/.omc/state/sessions/a60ac2ec-afbd-449f-a540-6df287392fc2/pre-tool-advisory-throttle.json +14 -0
  100. package/src/lib/constants/index.ts +15 -0
  101. package/src/lib/git-utils/__tests__/git-utils.test.ts +49 -0
  102. package/src/lib/git-utils/git-utils.ts +3 -1
  103. package/src/lib/infra-kit-config/__tests__/infra-kit-config.test.ts +270 -0
  104. package/src/lib/infra-kit-config/index.ts +7 -1
  105. package/src/lib/infra-kit-config/infra-kit-config.ts +46 -28
  106. package/src/lib/managed-block/__tests__/managed-block.test.ts +121 -0
  107. package/src/lib/managed-block/index.ts +8 -0
  108. package/src/lib/managed-block/managed-block.ts +145 -0
  109. package/src/lib/package-config/__tests__/package-config.test.ts +95 -0
  110. package/src/lib/package-config/index.ts +3 -0
  111. package/src/lib/package-config/package-config-schema.ts +19 -0
  112. package/src/lib/package-config/package-config.ts +99 -0
  113. package/src/lib/package-validator/__tests__/package-validator.test.ts +263 -0
  114. package/src/lib/package-validator/checks/__tests__/checks.test.ts +130 -0
  115. package/src/lib/package-validator/checks/config-check.ts +30 -0
  116. package/src/lib/package-validator/checks/files-check.ts +29 -0
  117. package/src/lib/package-validator/checks/index.ts +4 -0
  118. package/src/lib/package-validator/checks/scripts-check.ts +23 -0
  119. package/src/lib/package-validator/checks/turbo-check.ts +47 -0
  120. package/src/lib/package-validator/fs-utils.ts +18 -0
  121. package/src/lib/package-validator/index.ts +3 -0
  122. package/src/lib/package-validator/loader/config-loader.ts +77 -0
  123. package/src/lib/package-validator/loader/index.ts +2 -0
  124. package/src/lib/package-validator/loader/package-discovery.ts +98 -0
  125. package/src/lib/package-validator/package-validator.ts +48 -0
  126. package/src/lib/package-validator/types.ts +15 -0
  127. package/src/lib/release-id/__tests__/release-id.test.ts +351 -0
  128. package/src/lib/release-id/__tests__/versioned-regression.test.ts +69 -0
  129. package/src/lib/release-id/index.ts +15 -0
  130. package/src/lib/release-id/release-id.ts +257 -0
  131. package/src/lib/release-utils/__tests__/release-utils.test.ts +122 -0
  132. package/src/lib/release-utils/index.ts +4 -0
  133. package/src/lib/release-utils/release-utils.ts +85 -17
  134. package/src/lib/version-utils/__tests__/load-existing-versions.test.ts +37 -0
  135. package/src/lib/version-utils/__tests__/next-version.test.ts +119 -13
  136. package/src/lib/version-utils/index.ts +3 -0
  137. package/src/lib/version-utils/load-existing-versions.ts +29 -10
  138. package/src/lib/version-utils/next-version.ts +67 -12
  139. package/src/lib/version-utils/version-utils.ts +13 -4
  140. package/src/mcp/tools/index.ts +2 -0
  141. package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  142. package/src/types.ts +1 -1
  143. package/tsconfig.tsbuildinfo +1 -1
  144. package/src/lib/__tests__/infra-kit-config.test.ts +0 -231
  145. /package/src/integrations/{clickup → linear}/.gitkeep +0 -0
  146. /package/src/lib/{__tests__ → constants/__tests__}/constants.test.ts +0 -0
  147. /package/src/lib/{constants.ts → constants/constants.ts} +0 -0
@@ -3,6 +3,10 @@ import os from 'node:os'
3
3
  import path from 'node:path'
4
4
 
5
5
  import { logger } from 'src/lib/logger'
6
+ import { removeManagedBlock, upsertManagedBlock } from 'src/lib/managed-block'
7
+
8
+ import { writeAgentFiles } from './agent-files'
9
+ import { migrateLegacyConfig } from './migrate-config'
6
10
 
7
11
  export const MARKER_START = '# -- infra-kit:begin --'
8
12
  export const MARKER_END = '# -- infra-kit:end --'
@@ -10,76 +14,94 @@ export const MARKER_END = '# -- infra-kit:end --'
10
14
  const LEGACY_PAIRED: [start: string, end: string][] = [['# region infra-kit', '# endregion infra-kit']]
11
15
  const LEGACY_SINGLE = '# infra-kit shell functions'
12
16
 
13
- const USER_GLOBAL_CONFIG_STUB = `# infra-kit user-global config ~/.infra-kit/config.yml
14
- #
15
- # Merge chain (later layers override earlier ones at top-level keys):
16
- # 1. <repo>/infra-kit.yml — committed project config (required)
17
- # 2. ~/.infra-kit/config.yml — this file (user-global)
18
- # 3. ~/.infra-kit/projects/<repo-name>/infra-kit.yml — user-scope per-project override
19
- #
20
- # Merge is shallow: setting a top-level key here replaces that whole section
21
- # from layer 1. Arrays do not concatenate. Top-level keys recognized:
22
- # environments, envManagement, ide, taskManager, worktrees.
23
- #
24
- # Uncomment the blocks you want to apply globally across every project on this
25
- # machine. Per-project tweaks belong in layer 3 run \`infra-kit config edit\`.
26
-
27
- # Per-developer IDE config
28
- # ide:
29
- # provider: cursor
30
- # config:
31
- # mode: workspace
32
- # workspaceConfigPath: /path/to/your.code-workspace
33
-
34
- # Worktree prompt defaults — silences the follow-up prompts in \`worktrees-add\`
35
- # worktrees:
36
- # openInGithubDesktop: false
37
- # openInCmux: true
17
+ // JSON can't carry comments, so the real config is an empty-but-valid object…
18
+ const USER_GLOBAL_CONFIG_STUB = '{}\n'
19
+
20
+ // …and the annotated guidance lives next to it in a non-loaded .example.jsonc
21
+ // (the loader only reads the three exact `infra-kit.json` / `config.json` files).
22
+ const USER_GLOBAL_CONFIG_EXAMPLE = `// infra-kit user-global config — ~/.infra-kit/config.json
23
+ //
24
+ // Merge chain (later layers override earlier ones at top-level keys):
25
+ // 1. <repo>/infra-kit.json — committed project config (required)
26
+ // 2. ~/.infra-kit/config.json — user-global (the sibling of this file)
27
+ // 3. ~/.infra-kit/projects/<repo-name>/infra-kit.json — user-scope per-project override
28
+ //
29
+ // Merge is shallow: setting a top-level key replaces that whole section from
30
+ // layer 1. Arrays do not concatenate. Top-level keys recognized:
31
+ // environments, envManagement, ide, taskManager, worktrees.
32
+ //
33
+ // This .example.jsonc is reference only — it is NOT loaded. Put real global
34
+ // overrides in the sibling config.json (strict JSON: no comments, double-quoted
35
+ // keys). Per-project tweaks belong in layer 3 — run \`infra-kit config edit\`.
36
+ {
37
+ // "ide": {
38
+ // "provider": "cursor",
39
+ // "config": { "mode": "workspace", "workspaceConfigPath": "/path/to/your.code-workspace" }
40
+ // },
41
+ // "worktrees": { "openInGithubDesktop": false, "openInCmux": true }
42
+ }
38
43
  `
39
44
 
40
45
  /**
41
- * Append infra-kit shell functions to .zshrc and seed the user-global
42
- * config stub at ~/.infra-kit/config.yml on first run. Idempotent: a
43
- * subsequent run replaces the existing zshrc block in place and leaves
46
+ * Append infra-kit shell functions to .zshrc, migrate any legacy
47
+ * `infra-kit.yml` config layers to JSON, and seed the user-global config at
48
+ * ~/.infra-kit/config.json on first run. Idempotent: a subsequent run replaces
49
+ * the existing zshrc block in place, has nothing left to migrate, and leaves
44
50
  * the user-global config untouched.
45
51
  *
46
52
  * @example
47
53
  * // CLI: `infra-kit init` (or via the `pnpm dx-init` alias)
48
54
  * // INFO: Added infra-kit shell functions to /Users/me/.zshrc
49
- * // INFO: Wrote user-global config stub to /Users/me/.infra-kit/config.yml
55
+ * // INFO: Wrote user-global config to /Users/me/.infra-kit/config.json (see …/config.example.jsonc …)
50
56
  * // INFO: Run `source ~/.zshrc` or open a new terminal to activate.
51
57
  */
52
58
  export const init = async (): Promise<void> => {
53
59
  const zshrcPath = path.join(os.homedir(), '.zshrc')
54
- const shellBlock = buildShellBlock()
55
60
 
56
- if (fs.existsSync(zshrcPath)) {
57
- const content = fs.readFileSync(zshrcPath, 'utf-8')
58
- const cleaned = removeExistingBlock(content)
61
+ // Strip any prior block (current or legacy markers) anywhere in the file, then
62
+ // append a fresh block at end-of-file via the shared managed-block utility —
63
+ // the historical `removeExistingBlock` + append behavior, now centralized.
64
+ const existing = fs.existsSync(zshrcPath) ? removeExistingBlock(fs.readFileSync(zshrcPath, 'utf-8')) : ''
59
65
 
60
- fs.writeFileSync(zshrcPath, cleaned)
61
- }
66
+ const updated = upsertManagedBlock({
67
+ content: existing,
68
+ body: buildShellBody(),
69
+ startMarker: MARKER_START,
70
+ endMarker: MARKER_END,
71
+ placement: 'append-end',
72
+ })
73
+
74
+ fs.writeFileSync(zshrcPath, updated)
62
75
 
63
- fs.appendFileSync(zshrcPath, `\n${shellBlock}\n`)
64
76
  logger.info(`Added infra-kit shell functions to ${zshrcPath}`)
65
77
 
78
+ // Convert any legacy infra-kit.yml config layers to JSON before seeding, so a
79
+ // migrated config.json is not re-seeded as an empty stub.
80
+ await migrateLegacyConfig()
81
+
66
82
  seedUserGlobalConfig()
67
83
 
84
+ // Best-effort, non-fatal, repo-gated: keep the agent-instruction files in sync
85
+ // with the CLI surface. A no-op outside an infra-kit repo.
86
+ await writeAgentFiles()
87
+
68
88
  logger.info('Run `source ~/.zshrc` or open a new terminal to activate.')
69
89
  }
70
90
 
71
91
  /**
72
- * Create `~/.infra-kit/config.yml` with the documented stub when absent.
73
- * Skips silently if the file already exists so user edits are preserved.
92
+ * Create `~/.infra-kit/config.json` (empty `{}`) plus an annotated
93
+ * `~/.infra-kit/config.example.jsonc` reference when absent. Skips silently if
94
+ * the config already exists so user edits are preserved.
74
95
  *
75
96
  * @example
76
97
  * seedUserGlobalConfig()
77
- * // first call: writes ~/.infra-kit/config.yml from USER_GLOBAL_CONFIG_STUB
78
- * // later calls: leaves the file alone, logs that it is already present
98
+ * // first call: writes ~/.infra-kit/config.json ({}) + config.example.jsonc
99
+ * // later calls: leaves the config alone, logs that it is already present
79
100
  */
80
101
  const seedUserGlobalConfig = (): void => {
81
102
  const userConfigDir = path.join(os.homedir(), '.infra-kit')
82
- const userConfigPath = path.join(userConfigDir, 'config.yml')
103
+ const userConfigPath = path.join(userConfigDir, 'config.json')
104
+ const userConfigExamplePath = path.join(userConfigDir, 'config.example.jsonc')
83
105
 
84
106
  if (fs.existsSync(userConfigPath)) {
85
107
  logger.info(`User-global config already present at ${userConfigPath}`)
@@ -89,8 +111,11 @@ const seedUserGlobalConfig = (): void => {
89
111
 
90
112
  fs.mkdirSync(userConfigDir, { recursive: true })
91
113
  fs.writeFileSync(userConfigPath, USER_GLOBAL_CONFIG_STUB, 'utf-8')
114
+ fs.writeFileSync(userConfigExamplePath, USER_GLOBAL_CONFIG_EXAMPLE, 'utf-8')
92
115
 
93
- logger.info(`Wrote user-global config stub to ${userConfigPath}`)
116
+ logger.info(
117
+ `Wrote user-global config to ${userConfigPath} (see ${userConfigExamplePath} for the annotated reference)`,
118
+ )
94
119
  }
95
120
 
96
121
  const isBlockLine = (line: string): boolean => {
@@ -112,28 +137,15 @@ const isBlockLine = (line: string): boolean => {
112
137
  )
113
138
  }
114
139
 
115
- const removeBetween = (content: string, start: string, end: string): string | null => {
116
- const startIdx = content.indexOf(start)
117
- const endIdx = content.indexOf(end)
118
-
119
- if (startIdx === -1 || endIdx === -1) return null
120
-
121
- // eslint-disable-next-line sonarjs/slow-regex
122
- const before = content.slice(0, startIdx).replace(/\n+$/, '')
123
- const after = content.slice(endIdx + end.length).replace(/^\n+/, '')
124
-
125
- return before + (after ? `\n${after}` : '')
126
- }
127
-
128
140
  const removeExistingBlock = (content: string): string => {
129
141
  // 1. Current markers
130
- const result = removeBetween(content, MARKER_START, MARKER_END)
142
+ const result = removeManagedBlock(content, MARKER_START, MARKER_END)
131
143
 
132
144
  if (result !== null) return result
133
145
 
134
146
  // 2. Legacy paired markers (# region / # endregion)
135
147
  for (const [start, end] of LEGACY_PAIRED) {
136
- const legacyResult = removeBetween(content, start, end)
148
+ const legacyResult = removeManagedBlock(content, start, end)
137
149
 
138
150
  if (legacyResult !== null) return legacyResult
139
151
  }
@@ -158,11 +170,14 @@ const removeExistingBlock = (content: string): string => {
158
170
  return before + (remaining ? `\n${remaining}` : '')
159
171
  }
160
172
 
161
- export const buildShellBlock = (): string => {
173
+ /**
174
+ * The inner shell-function lines (no markers). Composed into the full marked
175
+ * block by {@link buildShellBlock} and fed to `upsertManagedBlock` by `init()`.
176
+ */
177
+ export const buildShellBody = (): string => {
162
178
  const runCmd = 'pnpm exec infra-kit'
163
179
 
164
180
  return [
165
- MARKER_START,
166
181
  'zmodload zsh/stat 2>/dev/null',
167
182
  'zmodload zsh/datetime 2>/dev/null',
168
183
  // eslint-disable-next-line no-template-curly-in-string
@@ -210,6 +225,13 @@ export const buildShellBlock = (): string => {
210
225
  'if (( _INFRA_KIT_SHELL_STARTED > 0 )); then',
211
226
  ' add-zsh-hook precmd _infra_kit_autoload',
212
227
  'fi',
213
- MARKER_END,
214
228
  ].join('\n')
215
229
  }
230
+
231
+ /**
232
+ * The full marker-delimited shell block (`MARKER_START … MARKER_END`). Kept as
233
+ * a single composed string so `doctor`'s exact-match freshness check stays valid.
234
+ */
235
+ export const buildShellBlock = (): string => {
236
+ return `${MARKER_START}\n${buildShellBody()}\n${MARKER_END}`
237
+ }
@@ -0,0 +1,146 @@
1
+ import fs from 'node:fs/promises'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import yaml from 'yaml'
5
+ import { z } from 'zod'
6
+
7
+ import {
8
+ getInfraKitConfigPaths,
9
+ infraKitConfigSchema,
10
+ infraKitOverrideConfigSchema,
11
+ resetInfraKitConfigCache,
12
+ } from 'src/lib/infra-kit-config'
13
+ import { logger } from 'src/lib/logger'
14
+
15
+ interface MigrateLayer {
16
+ label: string
17
+ /** Legacy YAML source path. */
18
+ yml: string
19
+ /** Target JSON path. */
20
+ json: string
21
+ /** Validates the parsed object before any write (main vs override schema). */
22
+ schema: z.ZodType
23
+ }
24
+
25
+ /**
26
+ * Replace the user's home prefix with `~` so logged paths stay short.
27
+ *
28
+ * @example
29
+ * // os.homedir() === '/Users/arthur'
30
+ * tildify('/Users/arthur/.infra-kit/config.yml') // => '~/.infra-kit/config.yml'
31
+ */
32
+ const tildify = (filePath: string): string => {
33
+ const home = os.homedir()
34
+
35
+ return filePath.startsWith(home) ? `~${filePath.slice(home.length)}` : filePath
36
+ }
37
+
38
+ /**
39
+ * `fs.access` reduced to a boolean, swallowing ENOENT.
40
+ *
41
+ * @example
42
+ * await fileExists('/etc/hosts') // => true
43
+ */
44
+ const fileExists = async (filePath: string): Promise<boolean> => {
45
+ try {
46
+ await fs.access(filePath)
47
+
48
+ return true
49
+ } catch {
50
+ return false
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Swap a resolved `.json` config path back to its legacy `.yml` sibling.
56
+ *
57
+ * @example
58
+ * legacyYmlPath('/repo/infra-kit.json') // => '/repo/infra-kit.yml'
59
+ */
60
+ const legacyYmlPath = (jsonPath: string): string => {
61
+ return jsonPath.replace(/\.json$/, '.yml')
62
+ }
63
+
64
+ /**
65
+ * Convert any legacy `infra-kit.yml` config layers to `infra-kit.json` as part
66
+ * of `infra-kit init`. Best-effort and non-fatal: each merge-chain layer
67
+ * (project, user-global, user-project) is migrated independently, and a
68
+ * conflict (both `.yml` and `.json` present) or an invalid `.yml` warns and
69
+ * skips that layer rather than aborting init or touching the other layers.
70
+ * Idempotent — already-JSON layers are left untouched.
71
+ *
72
+ * @example
73
+ * await migrateLegacyConfig()
74
+ * // INFO: ✓ Migrated infra-kit.yml → infra-kit.json
75
+ * // (no output when there is nothing legacy to convert)
76
+ */
77
+ export const migrateLegacyConfig = async (): Promise<void> => {
78
+ let paths: Awaited<ReturnType<typeof getInfraKitConfigPaths>>
79
+
80
+ try {
81
+ paths = await getInfraKitConfigPaths()
82
+ } catch {
83
+ // No resolvable project (e.g. init run outside a repo) — nothing to migrate.
84
+ return
85
+ }
86
+
87
+ const layers: MigrateLayer[] = [
88
+ { label: 'infra-kit.json', yml: legacyYmlPath(paths.main), json: paths.main, schema: infraKitConfigSchema },
89
+ {
90
+ label: '~/.infra-kit/config.json',
91
+ yml: legacyYmlPath(paths.userGlobal),
92
+ json: paths.userGlobal,
93
+ schema: infraKitOverrideConfigSchema,
94
+ },
95
+ {
96
+ label: `~/.infra-kit/projects/${paths.projectName}/infra-kit.json`,
97
+ yml: legacyYmlPath(paths.userProject),
98
+ json: paths.userProject,
99
+ schema: infraKitOverrideConfigSchema,
100
+ },
101
+ ]
102
+
103
+ let migrated = 0
104
+
105
+ for (const layer of layers) {
106
+ const [ymlExists, jsonExists] = await Promise.all([fileExists(layer.yml), fileExists(layer.json)])
107
+
108
+ if (!ymlExists) continue
109
+
110
+ if (jsonExists) {
111
+ logger.info(
112
+ `⚠ Skipped ${tildify(layer.yml)} — ${tildify(layer.json)} already exists (remove the stale .yml manually)`,
113
+ )
114
+
115
+ continue
116
+ }
117
+
118
+ // Keep per-layer migration non-fatal even for malformed YAML or I/O errors
119
+ // (TOCTOU after the existence probe, EACCES, read-only FS): warn and skip
120
+ // so one bad layer never aborts `init` or the other layers.
121
+ try {
122
+ const raw = await fs.readFile(layer.yml, 'utf-8')
123
+ const parsed = (yaml.parse(raw) ?? {}) as unknown
124
+ const result = layer.schema.safeParse(parsed)
125
+
126
+ if (!result.success) {
127
+ logger.info(`⚠ Skipped ${tildify(layer.yml)} — invalid config: ${z.prettifyError(result.error)}`)
128
+
129
+ continue
130
+ }
131
+
132
+ await fs.mkdir(path.dirname(layer.json), { recursive: true })
133
+ await fs.writeFile(layer.json, `${JSON.stringify(result.data, null, 2)}\n`, 'utf-8')
134
+ await fs.rm(layer.yml, { force: true })
135
+
136
+ logger.info(`✓ Migrated ${tildify(layer.yml)} → ${tildify(layer.json)}`)
137
+ migrated++
138
+ } catch (err) {
139
+ logger.info(`⚠ Skipped ${tildify(layer.yml)} — ${(err as Error).message}`)
140
+ }
141
+ }
142
+
143
+ if (migrated > 0) {
144
+ resetInfraKitConfigCache()
145
+ }
146
+ }
@@ -0,0 +1 @@
1
+ {"version":"4.1.8","results":[[":audit/__tests__/audit.test.ts",{"duration":0,"failed":true}]]}
@@ -0,0 +1,55 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type { z } from 'zod'
3
+
4
+ import { releaseCreateMcpTool } from '../release-create'
5
+
6
+ /**
7
+ * The MCP `releases[]` entry schema enforces that exactly one of `version` or
8
+ * `name` is supplied, and transforms each entry into the internal ReleaseInput
9
+ * shape. We exercise the schema directly (the array element) so we do not need
10
+ * to run the full handler (which performs git/Jira side effects).
11
+ */
12
+ const entrySchema = (releaseCreateMcpTool.inputSchema.releases as z.ZodArray<z.ZodTypeAny>).element
13
+
14
+ describe('release-create MCP releases[] entry schema', () => {
15
+ it('accepts a versioned entry and transforms it into a version ReleaseInput', () => {
16
+ expect(entrySchema.parse({ version: '1.2.5', type: 'hotfix' })).toEqual({
17
+ version: '1.2.5',
18
+ type: 'hotfix',
19
+ })
20
+ })
21
+
22
+ it('defaults type to regular', () => {
23
+ expect(entrySchema.parse({ version: '1.2.5' })).toEqual({
24
+ version: '1.2.5',
25
+ type: 'regular',
26
+ })
27
+ })
28
+
29
+ it('accepts a named entry and transforms it into a name ReleaseInput', () => {
30
+ expect(entrySchema.parse({ name: 'checkout-redesign', type: 'regular' })).toEqual({
31
+ name: 'checkout-redesign',
32
+ type: 'regular',
33
+ })
34
+ })
35
+
36
+ it('carries description through for named entries', () => {
37
+ expect(entrySchema.parse({ name: 'checkout-redesign', description: 'Q3' })).toEqual({
38
+ name: 'checkout-redesign',
39
+ type: 'regular',
40
+ description: 'Q3',
41
+ })
42
+ })
43
+
44
+ it('rejects an entry with both version and name (mutually exclusive)', () => {
45
+ expect(() => {
46
+ return entrySchema.parse({ version: '1.2.5', name: 'checkout-redesign' })
47
+ }).toThrow(/exactly one of/)
48
+ })
49
+
50
+ it('rejects an entry with neither version nor name', () => {
51
+ expect(() => {
52
+ return entrySchema.parse({ type: 'regular' })
53
+ }).toThrow(/exactly one of/)
54
+ })
55
+ })