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
package/eslint.config.js CHANGED
@@ -1,3 +1,3 @@
1
- import config from '@pkg/eslint-config'
1
+ import config from '@wl/eslint-config'
2
2
 
3
3
  export default config()
@@ -0,0 +1,5 @@
1
+ import { defineConfig } from 'infra-kit'
2
+
3
+ export default defineConfig(() => {
4
+ return {}
5
+ })
package/package.json CHANGED
@@ -1,13 +1,19 @@
1
1
  {
2
2
  "name": "infra-kit",
3
3
  "type": "module",
4
- "version": "0.1.102",
4
+ "version": "0.1.107",
5
5
  "description": "infra-kit",
6
- "main": "dist/cli.js",
7
- "module": "dist/cli.js",
6
+ "main": "dist/index.js",
7
+ "module": "dist/index.js",
8
+ "types": "dist/entry/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/entry/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
8
15
  "bin": {
9
- "infra-kit": "dist/cli.js",
10
- "ik": "dist/cli.js"
16
+ "infra-kit": "dist/cli.js"
11
17
  },
12
18
  "engines": {
13
19
  "node": ">=24.x"
@@ -15,6 +21,7 @@
15
21
  "scripts": {
16
22
  "inspector": "npx @modelcontextprotocol/inspector node ./dist/mcp.js --debug",
17
23
  "build": "pnpm run clean-artifacts && node ./scripts/build.js",
24
+ "check": "node ./dist/cli.js check",
18
25
  "clean-artifacts": "rm -rf dist",
19
26
  "clean-cache": "rm -rf node_modules/.cache .eslintcache tsconfig.tsbuildinfo .turbo .swc",
20
27
  "prettier-fix": "pnpm exec prettier **/* --write --no-error-on-unmatched-pattern --log-level silent --ignore-path ../../../.prettierignore",
@@ -30,21 +37,21 @@
30
37
  "fix": "pnpm run prettier-fix && pnpm run eslint-fix && pnpm run qa"
31
38
  },
32
39
  "dependencies": {
33
- "@inquirer/checkbox": "^5.1.5",
34
- "@inquirer/confirm": "^6.0.13",
35
- "@inquirer/select": "^5.1.5",
40
+ "@inquirer/checkbox": "^5.2.1",
41
+ "@inquirer/confirm": "^6.1.1",
42
+ "@inquirer/select": "^5.2.1",
36
43
  "@modelcontextprotocol/sdk": "^1.29.0",
37
- "commander": "^14.0.3",
44
+ "commander": "^15.0.0",
38
45
  "pino": "^10.3.1",
39
46
  "pino-pretty": "^13.1.3",
40
47
  "yaml": "^2.9.0",
41
- "zod": "^3.25.76",
48
+ "zod": "^4.4.3",
42
49
  "zx": "^8.8.5"
43
50
  },
44
51
  "devDependencies": {
45
- "@pkg/eslint-config": "workspace:*",
46
- "@pkg/vitest-config": "workspace:*",
47
- "esbuild": "^0.28.0",
52
+ "@wl/eslint-config": "workspace:*",
53
+ "@wl/vitest-config": "workspace:*",
54
+ "esbuild": "^0.28.1",
48
55
  "typescript": "^6.0.3"
49
56
  }
50
57
  }
package/scripts/build.js CHANGED
@@ -1,4 +1,6 @@
1
+ /* eslint-disable sonarjs/no-os-command-from-path */
1
2
  import * as esbuild from 'esbuild'
3
+ import { execFileSync } from 'node:child_process'
2
4
  import fs from 'node:fs'
3
5
  import { dirname, resolve } from 'node:path'
4
6
  import { fileURLToPath } from 'node:url'
@@ -8,10 +10,14 @@ import packageJson from '../package.json' with { type: 'json' }
8
10
  const __filename = fileURLToPath(import.meta.url)
9
11
  const __dirname = dirname(__filename)
10
12
 
11
- const OUT_DIR = resolve(__dirname, '../dist')
12
- const ENTRY_DIR = resolve(__dirname, '../src/entry')
13
+ const PKG_DIR = resolve(__dirname, '..')
14
+ const OUT_DIR = resolve(PKG_DIR, 'dist')
15
+ const ENTRY_DIR = resolve(PKG_DIR, 'src/entry')
13
16
 
14
- const entryPoints = fs.readdirSync(ENTRY_DIR).map((file) => resolve(ENTRY_DIR, file))
17
+ // 1. Bundle the JavaScript with esbuild (fast, but cannot emit .d.ts files).
18
+ const entryPoints = fs.readdirSync(ENTRY_DIR).map((file) => {
19
+ return resolve(ENTRY_DIR, file)
20
+ })
15
21
 
16
22
  await esbuild.build({
17
23
  entryPoints,
@@ -34,3 +40,26 @@ for (const entryPoint of entryPoints) {
34
40
 
35
41
  console.log('✅ Build was completed successfully: ', fileName, '-', +(stat.size / 1024 / 1024).toPrecision(3), 'MB')
36
42
  }
43
+
44
+ // 2. Emit the public type declarations with tsc (esbuild does not generate them).
45
+ // The library entry (src/entry/index.ts) only re-exports the package-config
46
+ // API via relative imports, so tsc needs no project config — a direct CLI
47
+ // call replaces the separate tsconfig.build.json. `--ignoreConfig` is
48
+ // required because tsconfig.json is present alongside the input file.
49
+ execFileSync(
50
+ 'tsc',
51
+ [
52
+ 'src/entry/index.ts',
53
+ '--ignoreConfig',
54
+ '--declaration',
55
+ '--emitDeclarationOnly',
56
+ '--rootDir',
57
+ 'src',
58
+ '--outDir',
59
+ 'dist',
60
+ '--skipLibCheck',
61
+ ],
62
+ { cwd: PKG_DIR, stdio: 'inherit' },
63
+ )
64
+
65
+ console.log('✅ Type declarations emitted: dist/entry/index.d.ts')
@@ -0,0 +1,18 @@
1
+ {
2
+ "version": 1,
3
+ "entries": {
4
+ "79a93d4a2f8f50b95f852280616242fee1855dc99a3c75211917f55e72e95fae": {
5
+ "last_emitted_at_ms": 1781185365793,
6
+ "message": "Use parallel execution for independent tasks. Use run_in_background for long operations (npm install, builds, tests)."
7
+ },
8
+ "445ed27a3872b681d98190bae61ccb84954e1bc4e140df5370be958dee776b3a": {
9
+ "last_emitted_at_ms": 1781185292480,
10
+ "message": "Verify changes work after editing. Test functionality before marking complete."
11
+ },
12
+ "466399dafa2d20f60587180bad0a07358eb7f2bce724df0ff682f038ad33f5ad": {
13
+ "last_emitted_at_ms": 1781185283907,
14
+ "message": "Read multiple files in parallel when possible for faster analysis."
15
+ }
16
+ },
17
+ "updated_at": "2026-06-11T13:42:45.793Z"
18
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "version": 1,
3
+ "entries": {
4
+ "79a93d4a2f8f50b95f852280616242fee1855dc99a3c75211917f55e72e95fae": {
5
+ "last_emitted_at_ms": 1781510589151,
6
+ "message": "Use parallel execution for independent tasks. Use run_in_background for long operations (npm install, builds, tests)."
7
+ },
8
+ "466399dafa2d20f60587180bad0a07358eb7f2bce724df0ff682f038ad33f5ad": {
9
+ "last_emitted_at_ms": 1781510561764,
10
+ "message": "Read multiple files in parallel when possible for faster analysis."
11
+ }
12
+ },
13
+ "updated_at": "2026-06-15T08:03:09.151Z"
14
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "version": 1,
3
+ "entries": {
4
+ "79a93d4a2f8f50b95f852280616242fee1855dc99a3c75211917f55e72e95fae": {
5
+ "last_emitted_at_ms": 1780905267223,
6
+ "message": "Use parallel execution for independent tasks. Use run_in_background for long operations (npm install, builds, tests)."
7
+ },
8
+ "445ed27a3872b681d98190bae61ccb84954e1bc4e140df5370be958dee776b3a": {
9
+ "last_emitted_at_ms": 1780905232864,
10
+ "message": "Verify changes work after editing. Test functionality before marking complete."
11
+ },
12
+ "466399dafa2d20f60587180bad0a07358eb7f2bce724df0ff682f038ad33f5ad": {
13
+ "last_emitted_at_ms": 1780905206915,
14
+ "message": "Read multiple files in parallel when possible for faster analysis."
15
+ }
16
+ },
17
+ "updated_at": "2026-06-08T07:54:27.223Z"
18
+ }
@@ -0,0 +1,59 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { afterEach, describe, expect, it } from 'vitest'
5
+
6
+ import { audit, auditMcpTool } from '../audit'
7
+
8
+ const tmpDirs: string[] = []
9
+
10
+ const makeTmpPackage = (config: string, packageJson: Record<string, unknown>): string => {
11
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'audit-cmd-'))
12
+
13
+ tmpDirs.push(dir)
14
+ fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(packageJson))
15
+ fs.writeFileSync(path.join(dir, 'infra-kit.config.ts'), config)
16
+
17
+ return dir
18
+ }
19
+
20
+ afterEach(() => {
21
+ while (tmpDirs.length > 0) {
22
+ const dir = tmpDirs.pop()
23
+
24
+ if (dir) {
25
+ fs.rmSync(dir, { recursive: true, force: true })
26
+ }
27
+ }
28
+ })
29
+
30
+ describe('audit', () => {
31
+ it('passes a package that satisfies its resolved rules', async () => {
32
+ const dir = makeTmpPackage('export default { requiredScripts: [], requiredFiles: [] }', {
33
+ name: '@x/ok',
34
+ type: 'module',
35
+ })
36
+
37
+ const result = await audit({ cwd: dir })
38
+
39
+ expect(result.structuredContent.allPassed).toBe(true)
40
+ expect(result.structuredContent.packages[0]?.name).toBe('@x/ok')
41
+ })
42
+
43
+ it('fails a package missing infra-kit.config.ts', async () => {
44
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'audit-cmd-'))
45
+
46
+ tmpDirs.push(dir)
47
+ fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name: '@x/no-config', type: 'module' }))
48
+
49
+ const result = await audit({ cwd: dir })
50
+
51
+ expect(result.structuredContent.allPassed).toBe(false)
52
+ })
53
+ })
54
+
55
+ describe('mCP tool registration', () => {
56
+ it('exposes the canonical `audit` tool', () => {
57
+ expect(auditMcpTool.name).toBe('audit')
58
+ })
59
+ })
@@ -0,0 +1,177 @@
1
+ import path from 'node:path'
2
+ import process from 'node:process'
3
+ import { z } from 'zod'
4
+
5
+ import { getProjectRoot } from 'src/lib/git-utils'
6
+ import { logger } from 'src/lib/logger'
7
+ import { ROOT_DEFAULT_RULES } from 'src/lib/package-config'
8
+ import type { ResolvedPackageRules } from 'src/lib/package-config'
9
+ import { discoverPackages, pathExists, validatePackage } from 'src/lib/package-validator'
10
+ import type { PackageValidationResult } from 'src/lib/package-validator'
11
+ import { defineMcpTool, textContent } from 'src/types'
12
+
13
+ // TODO [DO]: extract `audit` into its own standalone CLI tool, decoupled from infra-kit.
14
+
15
+ interface AuditOptions {
16
+ /** Audit every non-vendor workspace package instead of just the current one. */
17
+ all?: boolean
18
+ /** Audit the monorepo root (turbo pipeline + root commands) instead of a package. */
19
+ root?: boolean
20
+ /** Directory to resolve the current package from. Defaults to `process.cwd()`. */
21
+ cwd?: string
22
+ }
23
+
24
+ /** A directory to audit plus the under-the-hood defaults that apply to it. */
25
+ interface AuditTarget {
26
+ dir: string
27
+ baseline?: Readonly<ResolvedPackageRules>
28
+ }
29
+
30
+ /**
31
+ * Walk upward from `start` to the nearest directory containing a package.json.
32
+ * Used so `infra-kit audit` (no `--all`) targets the package whose package.json
33
+ * script invoked it, regardless of the exact working directory.
34
+ *
35
+ * @example
36
+ * await findPackageRoot('/repo/packages/serverless-config/src')
37
+ * // => '/repo/packages/serverless-config'
38
+ */
39
+ const findPackageRoot = async (start: string): Promise<string> => {
40
+ let current = path.resolve(start)
41
+
42
+ while (current !== path.dirname(current)) {
43
+ if (await pathExists(path.join(current, 'package.json'))) {
44
+ return current
45
+ }
46
+
47
+ current = path.dirname(current)
48
+ }
49
+
50
+ if (await pathExists(path.join(current, 'package.json'))) {
51
+ return current
52
+ }
53
+
54
+ throw new Error(`No package.json found in or above ${start}`)
55
+ }
56
+
57
+ /**
58
+ * Resolve which directories to audit, and the baseline defaults each uses:
59
+ * `root` → the monorepo root with {@link ROOT_DEFAULT_RULES}; `all` → every
60
+ * discovered non-vendor package; otherwise the package walked up from cwd.
61
+ */
62
+ const resolveTargets = async (options: AuditOptions): Promise<AuditTarget[]> => {
63
+ if (options.root) {
64
+ return [{ dir: await getProjectRoot(), baseline: ROOT_DEFAULT_RULES }]
65
+ }
66
+
67
+ if (options.all) {
68
+ const dirs = await discoverPackages(await getProjectRoot())
69
+
70
+ return dirs.map((dir) => {
71
+ return { dir }
72
+ })
73
+ }
74
+
75
+ return [{ dir: await findPackageRoot(options.cwd ?? process.cwd()) }]
76
+ }
77
+
78
+ /**
79
+ * Print a package's audit result as doctor-style `[PASS]`/`[FAIL]` lines.
80
+ */
81
+ const logResult = (result: PackageValidationResult): void => {
82
+ const header = result.passed ? 'PASS' : 'FAIL'
83
+
84
+ logger.info(`\n${result.packageName} — ${header}`)
85
+
86
+ for (const check of result.checks) {
87
+ const icon = check.status === 'pass' ? '[PASS]' : '[FAIL]'
88
+
89
+ logger.info(` ${icon} ${check.name}: ${check.message}`)
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Audit the monorepo root (`root`), every non-vendor workspace package (`all`),
95
+ * or the package resolved by walking up from the working directory (default —
96
+ * the shape used by a package's `"check": "infra-kit audit"` script). The
97
+ * returned `structuredContent.allPassed` lets the CLI set a non-zero exit code so
98
+ * the audit fails CI; this function never calls `process.exit` so the MCP tool
99
+ * can reuse it.
100
+ *
101
+ * @example
102
+ * // CLI inside packages/serverless-config: `infra-kit audit`
103
+ * await audit() // audits the current package
104
+ * await audit({ all: true }) // audits every non-vendor workspace package
105
+ * await audit({ root: true }) // audits the monorepo root (turbo + root commands)
106
+ */
107
+ export const audit = async (options: AuditOptions = {}) => {
108
+ const targets = await resolveTargets(options)
109
+
110
+ const results: PackageValidationResult[] = []
111
+
112
+ for (const target of targets) {
113
+ results.push(await validatePackage(target.dir, target.baseline))
114
+ }
115
+
116
+ for (const result of results) {
117
+ logResult(result)
118
+ }
119
+
120
+ const allPassed = results.every((result) => {
121
+ return result.passed
122
+ })
123
+
124
+ logger.info(`\n${allPassed ? '✅ All valid' : '❌ Audit failed'} (${results.length} checked)`)
125
+
126
+ const structuredContent = {
127
+ allPassed,
128
+ packages: results.map((result) => {
129
+ return {
130
+ name: result.packageName,
131
+ passed: result.passed,
132
+ checks: result.checks,
133
+ }
134
+ }),
135
+ }
136
+
137
+ return {
138
+ content: textContent(JSON.stringify(structuredContent, null, 2)),
139
+ structuredContent,
140
+ }
141
+ }
142
+
143
+ const auditInputSchema = {
144
+ all: z.boolean().optional().describe('Audit every non-vendor workspace package'),
145
+ root: z.boolean().optional().describe('Audit the monorepo root (turbo pipeline + root commands)'),
146
+ }
147
+
148
+ const auditOutputSchema = {
149
+ allPassed: z.boolean().describe('Whether every audited package passed all checks'),
150
+ packages: z
151
+ .array(
152
+ z.object({
153
+ name: z.string(),
154
+ passed: z.boolean(),
155
+ checks: z.array(
156
+ z.object({
157
+ name: z.string(),
158
+ status: z.enum(['pass', 'fail']),
159
+ message: z.string(),
160
+ }),
161
+ ),
162
+ }),
163
+ )
164
+ .describe('Per-package check results'),
165
+ }
166
+
167
+ // MCP Tool Registration
168
+ export const auditMcpTool = defineMcpTool({
169
+ name: 'audit',
170
+ description:
171
+ 'Audit packages against infra-kit.config.ts rules (config present and valid, required scripts, required files, and turbo tasks for the root). Defaults to the current package; all=true audits every non-vendor workspace package; root=true audits the monorepo root.',
172
+ inputSchema: auditInputSchema,
173
+ outputSchema: auditOutputSchema,
174
+ handler: (params) => {
175
+ return audit({ all: params.all, root: params.root })
176
+ },
177
+ })
@@ -0,0 +1 @@
1
+ export { audit, auditMcpTool } from './audit'
@@ -31,7 +31,7 @@ const fileExists = async (filePath: string): Promise<boolean> => {
31
31
  *
32
32
  * @example
33
33
  * // os.homedir() === '/Users/arthur'
34
- * tildify('/Users/arthur/.infra-kit/config.yml') // => '~/.infra-kit/config.yml'
34
+ * tildify('/Users/arthur/.infra-kit/config.json') // => '~/.infra-kit/config.json'
35
35
  * tildify('/etc/hosts') // => '/etc/hosts'
36
36
  */
37
37
  const tildify = (filePath: string): string => {
@@ -49,9 +49,9 @@ const tildify = (filePath: string): string => {
49
49
  * // CLI: `infra-kit config path`
50
50
  * // INFO: Project name: api
51
51
  * // INFO: Config merge chain (later overrides earlier):
52
- * // INFO: [✓] project (committed) ~/projects/api/infra-kit.yml
53
- * // INFO: [ ] user global ~/.infra-kit/config.yml
54
- * // INFO: [✓] user project ~/.infra-kit/projects/api/infra-kit.yml
52
+ * // INFO: [✓] project (committed) ~/projects/api/infra-kit.json
53
+ * // INFO: [ ] user global ~/.infra-kit/config.json
54
+ * // INFO: [✓] user project ~/.infra-kit/projects/api/infra-kit.json
55
55
  */
56
56
  export const configPath = async (): Promise<ToolsExecutionResult> => {
57
57
  const paths = await getInfraKitConfigPaths()
@@ -95,7 +95,8 @@ export const configPath = async (): Promise<ToolsExecutionResult> => {
95
95
  *
96
96
  * @example
97
97
  * // CLI: `infra-kit config edit`
98
- * // first run — creates ~/.infra-kit/projects/api/infra-kit.yml from a stub, then $EDITOR opens it
98
+ * // first run — creates ~/.infra-kit/projects/api/infra-kit.json ({}) + a sibling
99
+ * // infra-kit.example.jsonc reference, then $EDITOR opens the .json
99
100
  * // subsequent runs — opens the existing file as-is
100
101
  */
101
102
  export const configEdit = async (): Promise<ToolsExecutionResult> => {
@@ -105,9 +106,15 @@ export const configEdit = async (): Promise<ToolsExecutionResult> => {
105
106
  await fs.mkdir(path.dirname(paths.userProject), { recursive: true })
106
107
 
107
108
  if (!(await fileExists(paths.userProject))) {
108
- const stub = `# infra-kit user override for ${paths.projectName} — ~/.infra-kit/projects/${paths.projectName}/infra-kit.yml\n#\n# Layer 3 (highest precedence) of the config merge chain. Shallow-merged on\n# top of <repo>/infra-kit.yml and ~/.infra-kit/config.yml — top-level keys\n# (environments, envManagement, ide, taskManager, worktrees) replace wholesale.\n`
109
+ // JSON can't carry comments, so seed an empty-but-valid config and drop the
110
+ // annotated guidance next to it in a non-loaded .example.jsonc the loader
111
+ // never reads (it only globs the three exact `infra-kit.json` filenames).
112
+ const examplePath = exampleSiblingPath(paths.userProject)
109
113
 
110
- await fs.writeFile(paths.userProject, stub, 'utf-8')
114
+ await fs.writeFile(paths.userProject, '{}\n', 'utf-8')
115
+ await fs.writeFile(examplePath, buildUserProjectExample(paths.projectName), 'utf-8')
116
+
117
+ logger.info(`Created ${tildify(paths.userProject)} — see ${tildify(examplePath)} for the annotated reference.`)
111
118
  }
112
119
 
113
120
  logger.info(`Opening ${tildify(paths.userProject)} in ${editor}`)
@@ -123,3 +130,38 @@ export const configEdit = async (): Promise<ToolsExecutionResult> => {
123
130
  structuredContent,
124
131
  }
125
132
  }
133
+
134
+ /**
135
+ * Derive the non-loaded `.example.jsonc` sibling for a config path.
136
+ *
137
+ * @example
138
+ * exampleSiblingPath('/u/.infra-kit/projects/api/infra-kit.json')
139
+ * // => '/u/.infra-kit/projects/api/infra-kit.example.jsonc'
140
+ */
141
+ const exampleSiblingPath = (jsonPath: string): string => {
142
+ return jsonPath.replace(/\.json$/, '.example.jsonc')
143
+ }
144
+
145
+ /**
146
+ * Annotated JSONC reference for the user-scope per-project override layer.
147
+ * Written alongside the real `{}` config so the guidance the old YAML stub
148
+ * carried in comments survives the move to JSON.
149
+ *
150
+ * @example
151
+ * buildUserProjectExample('api')
152
+ * // => '// infra-kit user override for api …\n{ … }\n'
153
+ */
154
+ const buildUserProjectExample = (projectName: string): string => {
155
+ return `// infra-kit user override for ${projectName} — ~/.infra-kit/projects/${projectName}/infra-kit.json
156
+ //
157
+ // Layer 3 (highest precedence) of the config merge chain. Shallow-merged on top
158
+ // of <repo>/infra-kit.json and ~/.infra-kit/config.json — top-level keys
159
+ // (environments, envManagement, ide, taskManager, worktrees) replace wholesale.
160
+ //
161
+ // This .example.jsonc is reference only — it is NOT loaded. Put real overrides
162
+ // in the sibling infra-kit.json (strict JSON: no comments, double-quoted keys).
163
+ {
164
+ // "worktrees": { "openInGithubDesktop": false, "openInCmux": true }
165
+ }
166
+ `
167
+ }
@@ -0,0 +1,110 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5
+
6
+ import { writeAgentFiles } from 'src/commands/init/agent-files'
7
+ import { getProjectRoot, getRepoName } from 'src/lib/git-utils'
8
+
9
+ import { checkAgentFiles } from '../doctor'
10
+
11
+ vi.mock('src/lib/git-utils', () => {
12
+ return {
13
+ getProjectRoot: vi.fn(),
14
+ getRepoName: vi.fn(),
15
+ }
16
+ })
17
+
18
+ const writeFile = (filePath: string, content: string): void => {
19
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
20
+ fs.writeFileSync(filePath, content, 'utf-8')
21
+ }
22
+
23
+ const withTmpRepo = async (fn: (tmp: string) => Promise<void>, opts: { repo?: boolean } = {}): Promise<void> => {
24
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'infra-kit-doctor-agents-test-'))
25
+
26
+ vi.mocked(getProjectRoot).mockResolvedValue(tmp)
27
+ vi.mocked(getRepoName).mockResolvedValue(path.basename(tmp))
28
+
29
+ if (opts.repo !== false) {
30
+ writeFile(path.join(tmp, 'infra-kit.json'), '{"environments":["dev"]}\n')
31
+ }
32
+
33
+ try {
34
+ await fn(tmp)
35
+ } finally {
36
+ fs.rmSync(tmp, { recursive: true, force: true })
37
+ }
38
+ }
39
+
40
+ const statusOf = (checks: { name: string; status: string }[], name: string): string | undefined => {
41
+ return checks.find((c) => {
42
+ return c.name === name
43
+ })?.status
44
+ }
45
+
46
+ describe('checkAgentFiles', () => {
47
+ beforeEach(() => {
48
+ vi.clearAllMocks()
49
+ })
50
+
51
+ afterEach(() => {
52
+ vi.clearAllMocks()
53
+ })
54
+
55
+ it('returns no checks outside an infra-kit repo (never crashes)', async () => {
56
+ await withTmpRepo(
57
+ async () => {
58
+ await expect(checkAgentFiles()).resolves.toEqual([])
59
+ },
60
+ { repo: false },
61
+ )
62
+ })
63
+
64
+ it('passes all three checks right after init runs the agent-files step', async () => {
65
+ await withTmpRepo(async () => {
66
+ await writeAgentFiles()
67
+
68
+ const checks = await checkAgentFiles()
69
+
70
+ expect(statusOf(checks, 'AGENTS.md block')).toBe('pass')
71
+ expect(statusOf(checks, 'CLAUDE.md import')).toBe('pass')
72
+ expect(statusOf(checks, '.cursor/rules block')).toBe('pass')
73
+ })
74
+ })
75
+
76
+ it('flags AGENTS.md present but CLAUDE.md import removed', async () => {
77
+ await withTmpRepo(async (tmp) => {
78
+ await writeAgentFiles()
79
+ // User deletes CLAUDE.md (drops the @AGENTS.md import) but keeps AGENTS.md.
80
+ fs.rmSync(path.join(tmp, 'CLAUDE.md'))
81
+
82
+ const checks = await checkAgentFiles()
83
+
84
+ expect(statusOf(checks, 'AGENTS.md block')).toBe('pass')
85
+ expect(statusOf(checks, 'CLAUDE.md import')).toBe('fail')
86
+ })
87
+ })
88
+
89
+ it('flags a missing .cursor/rules block', async () => {
90
+ await withTmpRepo(async (tmp) => {
91
+ await writeAgentFiles()
92
+ fs.rmSync(path.join(tmp, '.cursor', 'rules', 'infra-kit.mdc'))
93
+
94
+ const checks = await checkAgentFiles()
95
+
96
+ expect(statusOf(checks, '.cursor/rules block')).toBe('fail')
97
+ })
98
+ })
99
+
100
+ it('flags a missing AGENTS.md block', async () => {
101
+ await withTmpRepo(async (tmp) => {
102
+ await writeAgentFiles()
103
+ fs.rmSync(path.join(tmp, 'AGENTS.md'))
104
+
105
+ const checks = await checkAgentFiles()
106
+
107
+ expect(statusOf(checks, 'AGENTS.md block')).toBe('fail')
108
+ })
109
+ })
110
+ })