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,145 @@
1
+ /**
2
+ * Generic "managed block" helpers — a marker-delimited region a tool owns and
3
+ * rewrites idempotently while never touching content outside the markers.
4
+ *
5
+ * This is the same mechanism `infra-kit init` already uses for the `~/.zshrc`
6
+ * shell block (`# -- infra-kit:begin -- … # -- infra-kit:end --`), lifted here so
7
+ * it can be reused for the repo agent-instruction files (`AGENTS.md`,
8
+ * `CLAUDE.md` import region, `.cursor/rules`). It mirrors the design of OMC's
9
+ * `<!-- OMC:START --> … <!-- OMC:END -->` CLAUDE.md installer.
10
+ */
11
+
12
+ /**
13
+ * Where a freshly upserted block lands relative to existing content.
14
+ *
15
+ * - `replace-in-place`: if the block already exists, rewrite it where it sits
16
+ * (surrounding text untouched, verbatim); if absent, append it at end-of-file.
17
+ * - `append-end`: strip any existing block from wherever it is, then append the
18
+ * fresh block at end-of-file (i.e. always relocate to the end). This preserves
19
+ * the historical `~/.zshrc` behavior of `removeExistingBlock` + append.
20
+ */
21
+ export type BlockPlacement = 'replace-in-place' | 'append-end'
22
+
23
+ export interface UpsertManagedBlockArgs {
24
+ /** Existing file content (`''` for a new file). */
25
+ content: string
26
+ /** Inner body to place between the markers (WITHOUT the markers). */
27
+ body: string
28
+ startMarker: string
29
+ endMarker: string
30
+ /** Defaults to `replace-in-place`. */
31
+ placement?: BlockPlacement
32
+ }
33
+
34
+ /**
35
+ * Whether `content` contains a well-formed `start … end` block. A reversed pair
36
+ * (end before start) is treated as absent — the same guard `doctor.ts` applies
37
+ * to the zshrc block, ported here so corrupted markers never match.
38
+ *
39
+ * @example
40
+ * hasManagedBlock('a<!--s-->x<!--e-->b', '<!--s-->', '<!--e-->') // => true
41
+ * hasManagedBlock('<!--e--><!--s-->', '<!--s-->', '<!--e-->') // => false (reversed)
42
+ */
43
+ export const hasManagedBlock = (content: string, startMarker: string, endMarker: string): boolean => {
44
+ const startIdx = content.indexOf(startMarker)
45
+ const endIdx = content.indexOf(endMarker)
46
+
47
+ return startIdx !== -1 && endIdx !== -1 && endIdx >= startIdx
48
+ }
49
+
50
+ /**
51
+ * Remove the first complete `start … end` block, preserving surrounding text.
52
+ * Returns `null` when no well-formed block is present (no markers, or reversed),
53
+ * so callers can fall through to legacy-format handling — this matches the
54
+ * `string | null` contract of `init.ts`'s original `removeBetween`, with the
55
+ * added reversed-marker guard.
56
+ *
57
+ * @example
58
+ * removeManagedBlock('top\n<!--s-->mid<!--e-->\nbot', '<!--s-->', '<!--e-->')
59
+ * // => 'top\nbot'
60
+ */
61
+ export const removeManagedBlock = (content: string, startMarker: string, endMarker: string): string | null => {
62
+ const startIdx = content.indexOf(startMarker)
63
+ const endIdx = content.indexOf(endMarker)
64
+
65
+ if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) return null
66
+
67
+ // eslint-disable-next-line sonarjs/slow-regex
68
+ const before = content.slice(0, startIdx).replace(/\n+$/, '')
69
+ const after = content.slice(endIdx + endMarker.length).replace(/^\n+/, '')
70
+
71
+ return before + (after ? `\n${after}` : '')
72
+ }
73
+
74
+ /**
75
+ * Read the version token that follows `versionPrefix` (e.g.
76
+ * `'<!-- infra-kit:version '`). Returns the token up to the next whitespace or
77
+ * `>`, or `null` if the prefix is absent. Mirrors OMC's `OMC:VERSION:` line.
78
+ *
79
+ * @example
80
+ * extractVersion('<!-- infra-kit:version 0.1.105 -->', '<!-- infra-kit:version ')
81
+ * // => '0.1.105'
82
+ */
83
+ export const extractVersion = (content: string, versionPrefix: string): string | null => {
84
+ const idx = content.indexOf(versionPrefix)
85
+
86
+ if (idx === -1) return null
87
+
88
+ const rest = content.slice(idx + versionPrefix.length)
89
+ const match = rest.match(/^([^\s>]+)/)
90
+
91
+ return match ? match[1]! : null
92
+ }
93
+
94
+ /**
95
+ * Compose a full block string: `start\n{body}\nend`. Kept identical in shape to
96
+ * the historical `buildShellBlock()` output so doctor's exact-match comparison
97
+ * stays valid.
98
+ */
99
+ export const buildManagedBlock = (startMarker: string, body: string, endMarker: string): string => {
100
+ return `${startMarker}\n${body}\n${endMarker}`
101
+ }
102
+
103
+ /**
104
+ * Insert or update a managed block in `content`, preserving everything outside
105
+ * the markers. Idempotent: re-running with the same body yields the same block
106
+ * and never nests duplicates.
107
+ *
108
+ * @example
109
+ * // fresh file
110
+ * upsertManagedBlock({ content: '', body: 'hi', startMarker: '<!--s-->', endMarker: '<!--e-->' })
111
+ * // => '<!--s-->\nhi\n<!--e-->\n'
112
+ *
113
+ * @example
114
+ * // existing block replaced in place, surrounding text kept
115
+ * upsertManagedBlock({
116
+ * content: 'top\n<!--s-->\nold\n<!--e-->\nbot',
117
+ * body: 'new', startMarker: '<!--s-->', endMarker: '<!--e-->',
118
+ * })
119
+ * // => 'top\n<!--s-->\nnew\n<!--e-->\nbot'
120
+ */
121
+ export const upsertManagedBlock = ({
122
+ content,
123
+ body,
124
+ startMarker,
125
+ endMarker,
126
+ placement = 'replace-in-place',
127
+ }: UpsertManagedBlockArgs): string => {
128
+ const block = buildManagedBlock(startMarker, body, endMarker)
129
+ const present = hasManagedBlock(content, startMarker, endMarker)
130
+
131
+ if (placement === 'replace-in-place' && present) {
132
+ const startIdx = content.indexOf(startMarker)
133
+ const endIdx = content.indexOf(endMarker) + endMarker.length
134
+
135
+ return content.slice(0, startIdx) + block + content.slice(endIdx)
136
+ }
137
+
138
+ // append-end, or replace-in-place with no existing block: drop any stale block
139
+ // then append the fresh one at end-of-file.
140
+ const stripped = present ? (removeManagedBlock(content, startMarker, endMarker) ?? content) : content
141
+ // eslint-disable-next-line sonarjs/slow-regex
142
+ const base = stripped.replace(/\n+$/, '')
143
+
144
+ return base.length > 0 ? `${base}\n${block}\n` : `${block}\n`
145
+ }
@@ -0,0 +1,95 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { DEFAULT_RULES, ROOT_DEFAULT_RULES, defineConfig, resolvePackageConfig } from '../package-config'
4
+ import { packageConfigSchema } from '../package-config-schema'
5
+
6
+ describe('defineConfig', () => {
7
+ it('returns an object input unchanged (identity)', () => {
8
+ const input = { requiredScripts: ['build'] }
9
+
10
+ expect(defineConfig(input)).toBe(input)
11
+ })
12
+
13
+ it('returns a factory input unchanged so the loader can resolve it', () => {
14
+ const factory = () => {
15
+ return { requiredFiles: ['a.txt'] }
16
+ }
17
+
18
+ expect(defineConfig(factory)).toBe(factory)
19
+ })
20
+ })
21
+
22
+ describe('resolvePackageConfig', () => {
23
+ it('falls back to defaults for every unset key', () => {
24
+ const rules = resolvePackageConfig({})
25
+
26
+ expect(rules.requiredScripts).toEqual(DEFAULT_RULES.requiredScripts)
27
+ expect(rules.requiredFiles).toEqual(DEFAULT_RULES.requiredFiles)
28
+ })
29
+
30
+ it('replaces a key wholesale when provided, including an empty array opt-out', () => {
31
+ const rules = resolvePackageConfig({ requiredScripts: [], requiredFiles: ['serverless.common.yml'] })
32
+
33
+ expect(rules.requiredScripts).toEqual([])
34
+ expect(rules.requiredFiles).toEqual(['serverless.common.yml'])
35
+ })
36
+
37
+ it('does not share the default array reference with the resolved result', () => {
38
+ const rules = resolvePackageConfig({})
39
+
40
+ rules.requiredScripts.push('mutated')
41
+
42
+ expect(DEFAULT_RULES.requiredScripts).not.toContain('mutated')
43
+ })
44
+
45
+ it('falls back to the supplied baseline (root) for unset keys, including turbo tasks', () => {
46
+ const rules = resolvePackageConfig({}, ROOT_DEFAULT_RULES)
47
+
48
+ expect(rules.requiredScripts).toEqual(ROOT_DEFAULT_RULES.requiredScripts)
49
+ expect(rules.turboTasks).toEqual(ROOT_DEFAULT_RULES.turboTasks)
50
+ })
51
+
52
+ it('lets a config override turbo.requiredTasks', () => {
53
+ const rules = resolvePackageConfig({ turbo: { requiredTasks: ['build'] } }, ROOT_DEFAULT_RULES)
54
+
55
+ expect(rules.turboTasks).toEqual(['build'])
56
+ })
57
+
58
+ it('defaults turboTasks to an empty array for packages', () => {
59
+ const rules = resolvePackageConfig({})
60
+
61
+ expect(rules.turboTasks).toEqual([])
62
+ })
63
+ })
64
+
65
+ describe('packageConfigSchema', () => {
66
+ it('rejects unknown keys so config typos surface as errors', () => {
67
+ const result = packageConfigSchema.safeParse({ requiredScript: ['build'] })
68
+
69
+ expect(result.success).toBe(false)
70
+ })
71
+
72
+ it('rejects a non-array requiredScripts', () => {
73
+ const result = packageConfigSchema.safeParse({ requiredScripts: 'build' })
74
+
75
+ expect(result.success).toBe(false)
76
+ })
77
+
78
+ it('accepts a well-formed config', () => {
79
+ const result = packageConfigSchema.safeParse({ requiredScripts: ['build'], requiredFiles: ['tsconfig.json'] })
80
+
81
+ expect(result.success).toBe(true)
82
+ })
83
+
84
+ it('accepts a turbo.requiredTasks block', () => {
85
+ const result = packageConfigSchema.safeParse({ turbo: { requiredTasks: ['build', 'validate'] } })
86
+
87
+ expect(result.success).toBe(true)
88
+ })
89
+
90
+ it('rejects an unknown key inside turbo', () => {
91
+ const result = packageConfigSchema.safeParse({ turbo: { tasks: ['build'] } })
92
+
93
+ expect(result.success).toBe(false)
94
+ })
95
+ })
@@ -0,0 +1,3 @@
1
+ export { DEFAULT_RULES, defineConfig, resolvePackageConfig, ROOT_DEFAULT_RULES } from './package-config'
2
+ export type { InfraKitPackageConfig, InfraKitPackageConfigInput, ResolvedPackageRules } from './package-config'
3
+ export { packageConfigSchema } from './package-config-schema'
@@ -0,0 +1,19 @@
1
+ import { z } from 'zod'
2
+
3
+ /**
4
+ * Schema for the resolved (post-factory) package config object. `strictObject`
5
+ * rejects unknown keys so typos in `infra-kit.config.ts` surface as validation
6
+ * errors instead of being silently ignored.
7
+ *
8
+ * Kept in its own module — separate from the public `defineConfig`/types entry —
9
+ * so the published `infra-kit` type surface stays free of a `zod` import.
10
+ */
11
+ export const packageConfigSchema = z.strictObject({
12
+ requiredScripts: z.array(z.string().min(1)).optional(),
13
+ requiredFiles: z.array(z.string().min(1)).optional(),
14
+ turbo: z
15
+ .strictObject({
16
+ requiredTasks: z.array(z.string().min(1)).optional(),
17
+ })
18
+ .optional(),
19
+ })
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Validation rules for a single workspace package, declared in its
3
+ * `infra-kit.config.js`. Every field is optional: a key left unset falls back to
4
+ * the active baseline ({@link DEFAULT_RULES} for packages, {@link ROOT_DEFAULT_RULES}
5
+ * for the monorepo root), and a key set replaces that default wholesale (per-key,
6
+ * no array concatenation) so a package can opt out with an explicit empty array.
7
+ *
8
+ * Most packages need none of these — the standard rules live in the baseline, so
9
+ * a typical config is just `defineConfig(() => ({}))`.
10
+ *
11
+ * @example
12
+ * // infra-kit.config.js
13
+ * import { defineConfig } from 'infra-kit'
14
+ *
15
+ * export default defineConfig(() => ({}))
16
+ */
17
+ export interface InfraKitPackageConfig {
18
+ /** Scripts that must be present in the package's package.json `scripts` map. */
19
+ requiredScripts?: string[]
20
+ /** Files (relative to the package root) that must exist on disk. */
21
+ requiredFiles?: string[]
22
+ /** Turborepo expectations — only meaningful where a turbo.json lives (the root). */
23
+ turbo?: {
24
+ /** Tasks that must be defined in turbo.json `tasks`. */
25
+ requiredTasks?: string[]
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Accepted shapes for a package config's default export — mirrors Vite's
31
+ * `defineConfig` input: a plain object, a sync factory, or an async factory.
32
+ */
33
+ export type InfraKitPackageConfigInput =
34
+ | InfraKitPackageConfig
35
+ | (() => InfraKitPackageConfig)
36
+ | (() => Promise<InfraKitPackageConfig>)
37
+
38
+ /**
39
+ * Identity helper that gives `infra-kit.config.js` authors full type inference
40
+ * and editor autocomplete without changing the value — exactly like Vite's
41
+ * `defineConfig`. Resolution of the factory form happens in the loader, not here.
42
+ *
43
+ * @example
44
+ * export default defineConfig(() => ({}))
45
+ *
46
+ * @example
47
+ * export default defineConfig(() => ({ requiredScripts: [] }))
48
+ */
49
+ export const defineConfig = (config: InfraKitPackageConfigInput): InfraKitPackageConfigInput => {
50
+ return config
51
+ }
52
+
53
+ /** Fully-resolved rules with every defaultable field present. */
54
+ export interface ResolvedPackageRules {
55
+ requiredScripts: string[]
56
+ requiredFiles: string[]
57
+ turboTasks: string[]
58
+ }
59
+
60
+ /**
61
+ * Baseline rules for a standard TypeScript workspace package, applied to any key
62
+ * a package leaves unset. These are the "under the hood" defaults so a conforming
63
+ * package's config can stay empty; non-standard packages override the relevant key.
64
+ */
65
+ export const DEFAULT_RULES: Readonly<ResolvedPackageRules> = {
66
+ requiredScripts: ['build', 'ts-check', 'eslint-check', 'prettier-check', 'test'],
67
+ requiredFiles: ['tsconfig.json', 'eslint.config.js', 'readme.md'],
68
+ turboTasks: [],
69
+ }
70
+
71
+ /**
72
+ * Baseline rules for the monorepo root (`infra-kit audit --root`). Checks the
73
+ * root commands, the workspace/turbo files, and that the turbo pipeline defines
74
+ * the expected tasks — so the root's own config can also stay empty.
75
+ */
76
+ export const ROOT_DEFAULT_RULES: Readonly<ResolvedPackageRules> = {
77
+ requiredScripts: ['build', 'dev', 'test', 'qa', 'check', 'fix'],
78
+ requiredFiles: ['turbo.json', 'pnpm-workspace.yaml'],
79
+ turboTasks: ['build', 'test', 'ts-check', 'eslint-check', 'prettier-check', 'check'],
80
+ }
81
+
82
+ /**
83
+ * Merge a parsed package config over a baseline. Each key is replaced wholesale
84
+ * when the package provides it, otherwise the baseline value is used.
85
+ *
86
+ * @example
87
+ * resolvePackageConfig({ requiredScripts: [] })
88
+ * // => { requiredScripts: [], requiredFiles: [...DEFAULT_RULES.requiredFiles], turboTasks: [] }
89
+ */
90
+ export const resolvePackageConfig = (
91
+ config: InfraKitPackageConfig,
92
+ baseline: Readonly<ResolvedPackageRules> = DEFAULT_RULES,
93
+ ): ResolvedPackageRules => {
94
+ return {
95
+ requiredScripts: config.requiredScripts ?? [...baseline.requiredScripts],
96
+ requiredFiles: config.requiredFiles ?? [...baseline.requiredFiles],
97
+ turboTasks: config.turbo?.requiredTasks ?? [...baseline.turboTasks],
98
+ }
99
+ }
@@ -0,0 +1,263 @@
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 { DEFAULT_RULES, ROOT_DEFAULT_RULES } from 'src/lib/package-config'
7
+
8
+ import { discoverPackages, loadPackageConfig, validatePackage } from '../package-validator'
9
+
10
+ const tmpDirs: string[] = []
11
+
12
+ const makeTmpDir = (): string => {
13
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pkg-validator-'))
14
+
15
+ tmpDirs.push(dir)
16
+
17
+ return dir
18
+ }
19
+
20
+ interface PackageFixture {
21
+ packageJson?: Record<string, unknown>
22
+ config?: string
23
+ files?: Record<string, string>
24
+ }
25
+
26
+ const writePackage = (dir: string, fixture: PackageFixture): void => {
27
+ const packageJson = fixture.packageJson ?? { name: '@x/pkg', type: 'module' }
28
+
29
+ fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(packageJson, null, 2))
30
+
31
+ if (fixture.config !== undefined) {
32
+ fs.writeFileSync(path.join(dir, 'infra-kit.config.ts'), fixture.config)
33
+ }
34
+
35
+ for (const [name, content] of Object.entries(fixture.files ?? {})) {
36
+ fs.writeFileSync(path.join(dir, name), content)
37
+ }
38
+ }
39
+
40
+ afterEach(() => {
41
+ while (tmpDirs.length > 0) {
42
+ const dir = tmpDirs.pop()
43
+
44
+ if (dir) {
45
+ fs.rmSync(dir, { recursive: true, force: true })
46
+ }
47
+ }
48
+ })
49
+
50
+ describe('loadPackageConfig', () => {
51
+ it('throws when infra-kit.config.js is missing', async () => {
52
+ const dir = makeTmpDir()
53
+
54
+ writePackage(dir, {})
55
+
56
+ await expect(loadPackageConfig(dir)).rejects.toThrow(/not found/)
57
+ })
58
+
59
+ it('loads an object default export and merges defaults', async () => {
60
+ const dir = makeTmpDir()
61
+
62
+ writePackage(dir, { config: 'export default { requiredScripts: [] }' })
63
+
64
+ const rules = await loadPackageConfig(dir)
65
+
66
+ expect(rules.requiredScripts).toEqual([])
67
+ expect(rules.requiredFiles).toEqual(DEFAULT_RULES.requiredFiles)
68
+ })
69
+
70
+ it('resolves a factory (function) default export', async () => {
71
+ const dir = makeTmpDir()
72
+
73
+ writePackage(dir, { config: "export default () => ({ requiredFiles: ['a.txt'] })" })
74
+
75
+ const rules = await loadPackageConfig(dir)
76
+
77
+ expect(rules.requiredFiles).toEqual(['a.txt'])
78
+ })
79
+
80
+ it('rejects an invalid config shape with a descriptive error', async () => {
81
+ const dir = makeTmpDir()
82
+
83
+ writePackage(dir, { config: "export default { requiredScripts: 'build' }" })
84
+
85
+ await expect(loadPackageConfig(dir)).rejects.toThrow(/Invalid/)
86
+ })
87
+
88
+ it('rejects an unknown key (typo protection)', async () => {
89
+ const dir = makeTmpDir()
90
+
91
+ writePackage(dir, { config: 'export default { requiredScript: [] }' })
92
+
93
+ await expect(loadPackageConfig(dir)).rejects.toThrow(/Invalid/)
94
+ })
95
+ })
96
+
97
+ describe('validatePackage', () => {
98
+ it('passes when config, required scripts, and required files are all satisfied', async () => {
99
+ const dir = makeTmpDir()
100
+
101
+ writePackage(dir, {
102
+ packageJson: { name: '@x/ok', type: 'module', scripts: { build: 'x' } },
103
+ config: "export default { requiredScripts: ['build'], requiredFiles: ['tsconfig.json'] }",
104
+ files: { 'tsconfig.json': '{}' },
105
+ })
106
+
107
+ const result = await validatePackage(dir)
108
+
109
+ expect(result.passed).toBe(true)
110
+ expect(result.packageName).toBe('@x/ok')
111
+ })
112
+
113
+ it('applies the under-the-hood defaults when the config is empty', async () => {
114
+ const dir = makeTmpDir()
115
+
116
+ writePackage(dir, {
117
+ packageJson: {
118
+ name: '@x/std',
119
+ type: 'module',
120
+ scripts: { build: 'x', 'ts-check': 'x', 'eslint-check': 'x', 'prettier-check': 'x', test: 'x' },
121
+ },
122
+ config: 'export default {}',
123
+ files: { 'tsconfig.json': '{}', 'eslint.config.js': '', 'readme.md': '' },
124
+ })
125
+
126
+ const result = await validatePackage(dir)
127
+
128
+ expect(result.passed).toBe(true)
129
+ })
130
+
131
+ it('fails when a required script is missing', async () => {
132
+ const dir = makeTmpDir()
133
+
134
+ writePackage(dir, {
135
+ packageJson: { name: '@x/no-script', type: 'module', scripts: { build: 'x' } },
136
+ config: "export default { requiredScripts: ['build', 'ts-check'], requiredFiles: [] }",
137
+ })
138
+
139
+ const result = await validatePackage(dir)
140
+
141
+ expect(result.passed).toBe(false)
142
+ expect(result.checks).toContainEqual(expect.objectContaining({ name: 'script:ts-check', status: 'fail' }))
143
+ })
144
+
145
+ it('fails when a required file is missing', async () => {
146
+ const dir = makeTmpDir()
147
+
148
+ writePackage(dir, {
149
+ packageJson: { name: '@x/no-file', type: 'module', scripts: {} },
150
+ config: "export default { requiredScripts: [], requiredFiles: ['tsconfig.json'] }",
151
+ })
152
+
153
+ const result = await validatePackage(dir)
154
+
155
+ expect(result.passed).toBe(false)
156
+ expect(result.checks).toContainEqual(expect.objectContaining({ name: 'file:tsconfig.json', status: 'fail' }))
157
+ })
158
+
159
+ it('fails with only the config check when infra-kit.config.ts is missing', async () => {
160
+ const dir = makeTmpDir()
161
+
162
+ writePackage(dir, { packageJson: { name: '@x/no-config', type: 'module' } })
163
+
164
+ const result = await validatePackage(dir)
165
+
166
+ expect(result.passed).toBe(false)
167
+ expect(result.checks).toHaveLength(1)
168
+ expect(result.checks[0]).toMatchObject({ name: 'infra-kit.config.ts', status: 'fail' })
169
+ })
170
+ })
171
+
172
+ describe('validatePackage — root / turbo', () => {
173
+ it('passes the root baseline when scripts, files, and turbo tasks are present', async () => {
174
+ const dir = makeTmpDir()
175
+
176
+ writePackage(dir, {
177
+ packageJson: {
178
+ name: 'monorepo',
179
+ type: 'module',
180
+ scripts: { build: 'x', dev: 'x', test: 'x', qa: 'x', check: 'x', fix: 'x' },
181
+ },
182
+ config: 'export default {}',
183
+ files: {
184
+ 'pnpm-workspace.yaml': 'packages: []\n',
185
+ 'turbo.json': JSON.stringify({
186
+ tasks: { build: {}, test: {}, 'ts-check': {}, 'eslint-check': {}, 'prettier-check': {}, check: {} },
187
+ }),
188
+ },
189
+ })
190
+
191
+ const result = await validatePackage(dir, ROOT_DEFAULT_RULES)
192
+
193
+ expect(result.passed).toBe(true)
194
+ })
195
+
196
+ it('fails when a required turbo task is missing from turbo.json', async () => {
197
+ const dir = makeTmpDir()
198
+
199
+ writePackage(dir, {
200
+ packageJson: { name: 'monorepo', type: 'module' },
201
+ config: "export default { requiredScripts: [], requiredFiles: [], turbo: { requiredTasks: ['build', 'lint'] } }",
202
+ files: { 'turbo.json': JSON.stringify({ tasks: { build: {} } }) },
203
+ })
204
+
205
+ const result = await validatePackage(dir)
206
+
207
+ expect(result.passed).toBe(false)
208
+ expect(result.checks).toContainEqual(expect.objectContaining({ name: 'turbo:lint', status: 'fail' }))
209
+ expect(result.checks).toContainEqual(expect.objectContaining({ name: 'turbo:build', status: 'pass' }))
210
+ })
211
+
212
+ it('accepts a root task keyed as //#name in turbo.json', async () => {
213
+ const dir = makeTmpDir()
214
+
215
+ writePackage(dir, {
216
+ packageJson: { name: 'monorepo', type: 'module' },
217
+ config: "export default { requiredScripts: [], requiredFiles: [], turbo: { requiredTasks: ['check-root'] } }",
218
+ files: { 'turbo.json': JSON.stringify({ tasks: { '//#check-root': {} } }) },
219
+ })
220
+
221
+ const result = await validatePackage(dir)
222
+
223
+ expect(result.checks).toContainEqual(expect.objectContaining({ name: 'turbo:check-root', status: 'pass' }))
224
+ })
225
+ })
226
+
227
+ describe('discoverPackages', () => {
228
+ it('expands non-vendor globs and excludes vendor and negations', async () => {
229
+ const root = makeTmpDir()
230
+
231
+ fs.writeFileSync(
232
+ path.join(root, 'pnpm-workspace.yaml'),
233
+ 'packages:\n - apps/*/*\n - packages/*\n - vendor/packages/*\n - "!**/test/**"\n',
234
+ )
235
+
236
+ const dirs = ['apps/infra-kit/cli', 'packages/p1', 'vendor/packages/v1']
237
+
238
+ for (const dir of dirs) {
239
+ const full = path.join(root, dir)
240
+
241
+ fs.mkdirSync(full, { recursive: true })
242
+ fs.writeFileSync(path.join(full, 'package.json'), '{}')
243
+ }
244
+
245
+ const found = await discoverPackages(root)
246
+
247
+ expect(found).toContain(path.join(root, 'apps/infra-kit/cli'))
248
+ expect(found).toContain(path.join(root, 'packages/p1'))
249
+ expect(found).not.toContain(path.join(root, 'vendor/packages/v1'))
250
+ })
251
+
252
+ it('omits directories that lack a package.json', async () => {
253
+ const root = makeTmpDir()
254
+
255
+ fs.writeFileSync(path.join(root, 'pnpm-workspace.yaml'), 'packages:\n - packages/*\n')
256
+
257
+ fs.mkdirSync(path.join(root, 'packages/empty'), { recursive: true })
258
+
259
+ const found = await discoverPackages(root)
260
+
261
+ expect(found).not.toContain(path.join(root, 'packages/empty'))
262
+ })
263
+ })