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
@@ -0,0 +1,130 @@
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 { checkFiles } from '../files-check'
7
+ import { checkScripts } from '../scripts-check'
8
+ import { checkTurbo } from '../turbo-check'
9
+
10
+ const tmpDirs: string[] = []
11
+
12
+ const makeTmpDir = (): string => {
13
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'checks-'))
14
+
15
+ tmpDirs.push(dir)
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('checkScripts', () => {
31
+ it('passes a script defined with a runnable command', () => {
32
+ const checks = checkScripts({ build: 'tsc' }, ['build'])
33
+
34
+ expect(checks).toEqual([{ name: 'script:build', status: 'pass', message: 'defined' }])
35
+ })
36
+
37
+ it('fails a script that is absent from the scripts map', () => {
38
+ const checks = checkScripts({}, ['build'])
39
+
40
+ expect(checks[0]).toMatchObject({ name: 'script:build', status: 'fail' })
41
+ expect(checks[0]?.message).toContain('missing')
42
+ })
43
+
44
+ it('fails a script declared with an empty value', () => {
45
+ const checks = checkScripts({ build: '' }, ['build'])
46
+
47
+ expect(checks[0]).toMatchObject({ name: 'script:build', status: 'fail' })
48
+ expect(checks[0]?.message).toContain('empty')
49
+ })
50
+
51
+ it('fails a script declared with a whitespace-only value', () => {
52
+ const checks = checkScripts({ build: ' ' }, ['build'])
53
+
54
+ expect(checks[0]).toMatchObject({ name: 'script:build', status: 'fail' })
55
+ expect(checks[0]?.message).toContain('empty')
56
+ })
57
+ })
58
+
59
+ describe('checkFiles', () => {
60
+ it('passes a required path that is a regular file', async () => {
61
+ const dir = makeTmpDir()
62
+
63
+ fs.writeFileSync(path.join(dir, 'readme.md'), '# hi')
64
+
65
+ const checks = await checkFiles(dir, ['readme.md'])
66
+
67
+ expect(checks[0]).toMatchObject({ name: 'file:readme.md', status: 'pass' })
68
+ })
69
+
70
+ it('fails a required file that does not exist', async () => {
71
+ const dir = makeTmpDir()
72
+
73
+ const checks = await checkFiles(dir, ['readme.md'])
74
+
75
+ expect(checks[0]).toMatchObject({ name: 'file:readme.md', status: 'fail' })
76
+ expect(checks[0]?.message).toContain('missing file')
77
+ })
78
+
79
+ it('fails a required path that exists but is a directory', async () => {
80
+ const dir = makeTmpDir()
81
+
82
+ fs.mkdirSync(path.join(dir, 'readme.md'))
83
+
84
+ const checks = await checkFiles(dir, ['readme.md'])
85
+
86
+ expect(checks[0]).toMatchObject({ name: 'file:readme.md', status: 'fail' })
87
+ expect(checks[0]?.message).toContain('not a file')
88
+ })
89
+ })
90
+
91
+ describe('checkTurbo', () => {
92
+ it('returns no checks when no turbo tasks are required', async () => {
93
+ const dir = makeTmpDir()
94
+
95
+ expect(await checkTurbo(dir, [])).toEqual([])
96
+ })
97
+
98
+ it('fails with a single diagnostic when turbo.json cannot be read', async () => {
99
+ const dir = makeTmpDir()
100
+
101
+ const checks = await checkTurbo(dir, ['build'])
102
+
103
+ expect(checks).toHaveLength(1)
104
+ expect(checks[0]).toMatchObject({ name: 'turbo.json', status: 'fail' })
105
+ expect(checks[0]?.message).toContain('cannot read/parse')
106
+ })
107
+
108
+ it('fails with a single diagnostic when turbo.json has no tasks object', async () => {
109
+ const dir = makeTmpDir()
110
+
111
+ fs.writeFileSync(path.join(dir, 'turbo.json'), JSON.stringify({ $schema: 'x' }))
112
+
113
+ const checks = await checkTurbo(dir, ['build', 'test'])
114
+
115
+ expect(checks).toHaveLength(1)
116
+ expect(checks[0]).toMatchObject({ name: 'turbo.json', status: 'fail' })
117
+ expect(checks[0]?.message).toContain('no "tasks" object')
118
+ })
119
+
120
+ it('reports per-task results when a tasks object exists', async () => {
121
+ const dir = makeTmpDir()
122
+
123
+ fs.writeFileSync(path.join(dir, 'turbo.json'), JSON.stringify({ tasks: { build: {} } }))
124
+
125
+ const checks = await checkTurbo(dir, ['build', 'test'])
126
+
127
+ expect(checks).toContainEqual(expect.objectContaining({ name: 'turbo:build', status: 'pass' }))
128
+ expect(checks).toContainEqual(expect.objectContaining({ name: 'turbo:test', status: 'fail' }))
129
+ })
130
+ })
@@ -0,0 +1,30 @@
1
+ import { DEFAULT_RULES } from 'src/lib/package-config'
2
+ import type { ResolvedPackageRules } from 'src/lib/package-config'
3
+
4
+ import { PACKAGE_CONFIG_FILE, loadPackageConfig } from '../loader'
5
+ import type { PackageCheck } from '../types'
6
+
7
+ /**
8
+ * Build the "config present and valid" check, returning the resolved rules when
9
+ * the load succeeds so the caller can run the rule-based checks against them.
10
+ * When the config fails to load the rules are `null` and the caller skips the
11
+ * rule-based checks (the expectations are unknown).
12
+ */
13
+ export const checkConfig = async (
14
+ packageDir: string,
15
+ baseline: Readonly<ResolvedPackageRules> = DEFAULT_RULES,
16
+ ): Promise<{ check: PackageCheck; rules: ResolvedPackageRules | null }> => {
17
+ try {
18
+ const rules = await loadPackageConfig(packageDir, baseline)
19
+
20
+ return {
21
+ check: { name: PACKAGE_CONFIG_FILE, status: 'pass', message: 'present and valid' },
22
+ rules,
23
+ }
24
+ } catch (err) {
25
+ return {
26
+ check: { name: PACKAGE_CONFIG_FILE, status: 'fail', message: (err as Error).message },
27
+ rules: null,
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,29 @@
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
+
4
+ import type { PackageCheck } from '../types'
5
+
6
+ /**
7
+ * Check that every required file exists relative to the package root AND is a
8
+ * regular file. A directory that happens to share the required name fails — a
9
+ * required `readme.md` must be the file, not a folder.
10
+ */
11
+ export const checkFiles = async (packageDir: string, requiredFiles: string[]): Promise<PackageCheck[]> => {
12
+ return Promise.all(
13
+ requiredFiles.map(async (file) => {
14
+ const stat = await fs.stat(path.join(packageDir, file)).catch(() => {
15
+ return null
16
+ })
17
+
18
+ if (!stat) {
19
+ return { name: `file:${file}`, status: 'fail' as const, message: `missing file: ${file}` }
20
+ }
21
+
22
+ if (!stat.isFile()) {
23
+ return { name: `file:${file}`, status: 'fail' as const, message: `not a file: ${file} (found a directory)` }
24
+ }
25
+
26
+ return { name: `file:${file}`, status: 'pass' as const, message: 'exists' }
27
+ }),
28
+ )
29
+ }
@@ -0,0 +1,4 @@
1
+ export { checkConfig } from './config-check'
2
+ export { checkFiles } from './files-check'
3
+ export { checkScripts } from './scripts-check'
4
+ export { checkTurbo } from './turbo-check'
@@ -0,0 +1,23 @@
1
+ import type { PackageCheck } from '../types'
2
+
3
+ /**
4
+ * Check that every required script is present in the package.json `scripts` map
5
+ * and carries a runnable command. A key declared with an empty or whitespace-only
6
+ * value fails as well — an empty script silently no-ops in CI, so presence alone
7
+ * is not enough.
8
+ */
9
+ export const checkScripts = (scripts: Record<string, string>, requiredScripts: string[]): PackageCheck[] => {
10
+ return requiredScripts.map((script) => {
11
+ const value = scripts[script]
12
+
13
+ if (typeof value !== 'string') {
14
+ return { name: `script:${script}`, status: 'fail', message: `missing "${script}" in package.json scripts` }
15
+ }
16
+
17
+ if (value.trim().length === 0) {
18
+ return { name: `script:${script}`, status: 'fail', message: `"${script}" is empty in package.json scripts` }
19
+ }
20
+
21
+ return { name: `script:${script}`, status: 'pass', message: 'defined' }
22
+ })
23
+ }
@@ -0,0 +1,47 @@
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
+
4
+ import type { PackageCheck } from '../types'
5
+
6
+ const TURBO_FILE = 'turbo.json'
7
+
8
+ /**
9
+ * Check that every required turbo task is defined in turbo.json `tasks`. A root
10
+ * task may be keyed as either `name` or `//#name`, so both forms count as present.
11
+ * Runs only when the resolved rules ask for turbo tasks (the monorepo root).
12
+ *
13
+ * Reports a single clear diagnostic when turbo.json is unreadable or carries no
14
+ * `tasks` object, rather than emitting one identical "missing task" line per
15
+ * required task.
16
+ */
17
+ export const checkTurbo = async (packageDir: string, requiredTasks: string[]): Promise<PackageCheck[]> => {
18
+ if (requiredTasks.length === 0) {
19
+ return []
20
+ }
21
+
22
+ let parsed: { tasks?: Record<string, unknown> }
23
+
24
+ try {
25
+ const raw = await fs.readFile(path.join(packageDir, TURBO_FILE), 'utf-8')
26
+
27
+ parsed = JSON.parse(raw) as { tasks?: Record<string, unknown> }
28
+ } catch (err) {
29
+ return [{ name: TURBO_FILE, status: 'fail', message: `cannot read/parse ${TURBO_FILE}: ${(err as Error).message}` }]
30
+ }
31
+
32
+ const tasks = parsed.tasks
33
+
34
+ if (tasks === null || typeof tasks !== 'object') {
35
+ return [{ name: TURBO_FILE, status: 'fail', message: `no "tasks" object defined in ${TURBO_FILE}` }]
36
+ }
37
+
38
+ return requiredTasks.map((task) => {
39
+ const defined = task in tasks || `//#${task}` in tasks
40
+
41
+ return {
42
+ name: `turbo:${task}`,
43
+ status: defined ? 'pass' : 'fail',
44
+ message: defined ? 'defined' : `missing turbo task "${task}" in ${TURBO_FILE}`,
45
+ }
46
+ })
47
+ }
@@ -0,0 +1,18 @@
1
+ import fs from 'node:fs/promises'
2
+
3
+ /**
4
+ * Resolve whether a path is reachable, suppressing ENOENT into a boolean.
5
+ *
6
+ * @example
7
+ * await pathExists('/etc/hosts') // => true
8
+ * await pathExists('/nope') // => false
9
+ */
10
+ export const pathExists = async (target: string): Promise<boolean> => {
11
+ try {
12
+ await fs.access(target)
13
+
14
+ return true
15
+ } catch {
16
+ return false
17
+ }
18
+ }
@@ -0,0 +1,3 @@
1
+ export { pathExists } from './fs-utils'
2
+ export { discoverPackages, loadPackageConfig, validatePackage } from './package-validator'
3
+ export type { PackageCheck, PackageValidationResult } from './package-validator'
@@ -0,0 +1,77 @@
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
+ import { pathToFileURL } from 'node:url'
4
+ import { z } from 'zod'
5
+
6
+ import { DEFAULT_RULES, packageConfigSchema, resolvePackageConfig } from 'src/lib/package-config'
7
+ import type { ResolvedPackageRules } from 'src/lib/package-config'
8
+
9
+ import { pathExists } from '../fs-utils'
10
+
11
+ /** Per-package config filename every validated package must provide. */
12
+ export const PACKAGE_CONFIG_FILE = 'infra-kit.config.ts'
13
+
14
+ interface PackageJsonShape {
15
+ name?: string
16
+ scripts?: Record<string, string>
17
+ }
18
+
19
+ /**
20
+ * Read and JSON-parse a package.json, returning an empty object when it is
21
+ * missing or unreadable so callers can degrade into a clear "missing" check.
22
+ */
23
+ export const readPackageJson = async (packageDir: string): Promise<PackageJsonShape> => {
24
+ try {
25
+ const raw = await fs.readFile(path.join(packageDir, 'package.json'), 'utf-8')
26
+
27
+ return JSON.parse(raw) as PackageJsonShape
28
+ } catch {
29
+ return {}
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Load, resolve, and validate a package's `infra-kit.config.ts`.
35
+ *
36
+ * Dynamic-imports the file (ESM), resolves the Vite-style factory or object
37
+ * default export, validates the result against {@link packageConfigSchema}, and
38
+ * merges it over the defaults. Throws a descriptive error when the file is
39
+ * absent or the resolved config violates the schema.
40
+ *
41
+ * @example
42
+ * await loadPackageConfig('/repo/packages/serverless-config')
43
+ * // => { requiredScripts: [], requiredFiles: ['serverless.common.yml'], turboTasks: [] }
44
+ */
45
+ export const loadPackageConfig = async (
46
+ packageDir: string,
47
+ baseline: Readonly<ResolvedPackageRules> = DEFAULT_RULES,
48
+ ): Promise<ResolvedPackageRules> => {
49
+ const configPath = path.join(packageDir, PACKAGE_CONFIG_FILE)
50
+
51
+ if (!(await pathExists(configPath))) {
52
+ throw new Error(`${PACKAGE_CONFIG_FILE} not found at ${configPath}`)
53
+ }
54
+
55
+ // Cache-bust with the file mtime so repeated loads (long-running MCP server)
56
+ // pick up edits without a process restart. `.ts` configs load via Node's
57
+ // native type stripping (the repo requires Node >= 24).
58
+ const stat = await fs.stat(configPath)
59
+ const moduleUrl = `${pathToFileURL(configPath).href}?mtime=${Number(stat.mtimeMs)}`
60
+
61
+ const imported = (await import(moduleUrl)) as { default?: unknown }
62
+ const rawExport = imported.default
63
+
64
+ if (rawExport === undefined) {
65
+ throw new Error(`${PACKAGE_CONFIG_FILE} at ${configPath} has no default export`)
66
+ }
67
+
68
+ const resolvedExport = typeof rawExport === 'function' ? await (rawExport as () => unknown)() : rawExport
69
+
70
+ const parsed = packageConfigSchema.safeParse(resolvedExport)
71
+
72
+ if (!parsed.success) {
73
+ throw new Error(`Invalid ${PACKAGE_CONFIG_FILE} at ${configPath}: ${z.prettifyError(parsed.error)}`)
74
+ }
75
+
76
+ return resolvePackageConfig(parsed.data, baseline)
77
+ }
@@ -0,0 +1,2 @@
1
+ export { loadPackageConfig, PACKAGE_CONFIG_FILE, readPackageJson } from './config-loader'
2
+ export { discoverPackages } from './package-discovery'
@@ -0,0 +1,98 @@
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
+ import yaml from 'yaml'
4
+
5
+ import { pathExists } from '../fs-utils'
6
+
7
+ const WORKSPACE_FILE = 'pnpm-workspace.yaml'
8
+
9
+ /**
10
+ * List the immediate child directories of `dir`, returning `[]` when the path
11
+ * can't be read (e.g. the parent glob segment matched a non-existent dir).
12
+ */
13
+ const listChildDirs = async (dir: string): Promise<string[]> => {
14
+ const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => {
15
+ return []
16
+ })
17
+
18
+ return entries
19
+ .filter((entry) => {
20
+ return entry.isDirectory()
21
+ })
22
+ .map((entry) => {
23
+ return path.join(dir, entry.name)
24
+ })
25
+ }
26
+
27
+ /**
28
+ * Apply one glob segment to a set of directories: `*` fans out to every child
29
+ * directory, a literal segment keeps the dirs where that child path exists.
30
+ */
31
+ const expandSegment = async (dirs: string[], segment: string): Promise<string[]> => {
32
+ const next: string[] = []
33
+
34
+ for (const dir of dirs) {
35
+ if (segment === '*') {
36
+ next.push(...(await listChildDirs(dir)))
37
+
38
+ continue
39
+ }
40
+
41
+ const candidate = path.join(dir, segment)
42
+
43
+ if (await pathExists(candidate)) {
44
+ next.push(candidate)
45
+ }
46
+ }
47
+
48
+ return next
49
+ }
50
+
51
+ /**
52
+ * Expand a single pnpm-workspace glob (only the `*` segment wildcard is
53
+ * supported, which covers every pattern this monorepo uses) into directories.
54
+ */
55
+ const expandGlob = async (projectRoot: string, pattern: string): Promise<string[]> => {
56
+ let dirs = [projectRoot]
57
+
58
+ for (const segment of pattern.split('/')) {
59
+ dirs = await expandSegment(dirs, segment)
60
+ }
61
+
62
+ return dirs
63
+ }
64
+
65
+ /**
66
+ * Discover validatable workspace packages from `pnpm-workspace.yaml`.
67
+ *
68
+ * Negation patterns (`!…`) and everything under `vendor/` are excluded —
69
+ * vendor is mirrored from `starter-workspace` and is checksum-enforced by
70
+ * `pnpm vendor:check`, so its configs are owned upstream, not here. Only
71
+ * directories that actually contain a package.json are returned.
72
+ *
73
+ * @example
74
+ * await discoverPackages('/repo')
75
+ * // => ['/repo/apps/infra-kit/cli', '/repo/packages/serverless-config']
76
+ */
77
+ export const discoverPackages = async (projectRoot: string): Promise<string[]> => {
78
+ const raw = await fs.readFile(path.join(projectRoot, WORKSPACE_FILE), 'utf-8')
79
+ const parsed = (yaml.parse(raw) ?? {}) as { packages?: string[] }
80
+
81
+ const patterns = (parsed.packages ?? []).filter((pattern) => {
82
+ return !pattern.startsWith('!') && !pattern.startsWith('vendor')
83
+ })
84
+
85
+ const found = new Set<string>()
86
+
87
+ for (const pattern of patterns) {
88
+ const dirs = await expandGlob(projectRoot, pattern)
89
+
90
+ for (const dir of dirs) {
91
+ if (await pathExists(path.join(dir, 'package.json'))) {
92
+ found.add(dir)
93
+ }
94
+ }
95
+ }
96
+
97
+ return [...found].sort()
98
+ }
@@ -0,0 +1,48 @@
1
+ import path from 'node:path'
2
+
3
+ import { DEFAULT_RULES } from 'src/lib/package-config'
4
+ import type { ResolvedPackageRules } from 'src/lib/package-config'
5
+
6
+ import { checkConfig, checkFiles, checkScripts, checkTurbo } from './checks'
7
+ import { readPackageJson } from './loader'
8
+ import type { PackageCheck, PackageValidationResult } from './types'
9
+
10
+ // Re-exported on the historical import path so consumers and tests that reach
11
+ // for the loader through `package-validator` keep resolving after the split.
12
+ export { discoverPackages, loadPackageConfig } from './loader'
13
+ export type { PackageCheck, PackageValidationResult } from './types'
14
+
15
+ /**
16
+ * Validate a single directory against its `infra-kit.config.ts` rules: the config
17
+ * must be present and valid, every required script must be declared, every
18
+ * required file must exist, and (root only) every required turbo task must be
19
+ * defined. When the config fails to load, only that check is reported (the rules
20
+ * are unknown, so the rule-based checks are skipped). `baseline` selects which
21
+ * under-the-hood defaults apply — package defaults or {@link ROOT_DEFAULT_RULES}.
22
+ *
23
+ * @example
24
+ * const result = await validatePackage('/repo/packages/serverless-config')
25
+ * // result.passed reflects the package's conformance; result.checks lists each check
26
+ */
27
+ export const validatePackage = async (
28
+ packageDir: string,
29
+ baseline: Readonly<ResolvedPackageRules> = DEFAULT_RULES,
30
+ ): Promise<PackageValidationResult> => {
31
+ const pkgJson = await readPackageJson(packageDir)
32
+ const packageName = pkgJson.name ?? path.basename(packageDir)
33
+
34
+ const { check: configCheck, rules } = await checkConfig(packageDir, baseline)
35
+ const checks: PackageCheck[] = [configCheck]
36
+
37
+ if (rules) {
38
+ checks.push(...checkScripts(pkgJson.scripts ?? {}, rules.requiredScripts))
39
+ checks.push(...(await checkFiles(packageDir, rules.requiredFiles)))
40
+ checks.push(...(await checkTurbo(packageDir, rules.turboTasks)))
41
+ }
42
+
43
+ const passed = checks.every((check) => {
44
+ return check.status === 'pass'
45
+ })
46
+
47
+ return { packageDir, packageName, checks, passed }
48
+ }
@@ -0,0 +1,15 @@
1
+ /** Result of a single validation check against one package. */
2
+ export interface PackageCheck {
3
+ /** Stable identifier for the check, e.g. `infra-kit.config.ts`, `script:build`. */
4
+ name: string
5
+ status: 'pass' | 'fail'
6
+ message: string
7
+ }
8
+
9
+ /** Aggregate validation outcome for one package directory. */
10
+ export interface PackageValidationResult {
11
+ packageDir: string
12
+ packageName: string
13
+ checks: PackageCheck[]
14
+ passed: boolean
15
+ }