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.
- package/.eslintcache +1 -1
- package/.omc/state/agent-replay-0a58307d-2a37-4c69-851c-83a646502d62.jsonl +1 -0
- package/.omc/state/agent-replay-11c41aa0-51fa-49e1-a1dc-26dcd6ac26cc.jsonl +16 -0
- package/.omc/state/agent-replay-4cf1c186-81b2-497c-b002-d7f84e7839f3.jsonl +9 -0
- package/.omc/state/agent-replay-5c4ab554-64f1-42ae-83e3-21e0237e955c.jsonl +11 -0
- package/.omc/state/agent-replay-a60ac2ec-afbd-449f-a540-6df287392fc2.jsonl +1 -0
- package/.omc/state/agent-replay-afc6290b-40d3-4bef-b3b6-14484c034ab9.jsonl +14 -0
- package/.omc/state/agent-replay-be37e426-6fc8-47f4-8178-221c8494551c.jsonl +3 -0
- package/.omc/state/agent-replay-c967c819-3d1c-447b-ab48-56a8448ef9f8.jsonl +2 -0
- package/.omc/state/agent-replay-e947a3c6-989d-4a60-91dd-6b0ddd827b2d.jsonl +3 -0
- package/.omc/state/idle-notif-cooldown.json +3 -0
- package/.omc/state/last-tool-error.json +4 -4
- package/.omc/state/mission-state.json +53 -0
- package/.omc/state/sessions/0a58307d-2a37-4c69-851c-83a646502d62/pre-tool-advisory-throttle.json +18 -0
- package/.omc/state/sessions/0a58307d-2a37-4c69-851c-83a646502d62/subagent-tracking-state.json +7 -0
- package/.omc/state/sessions/11c41aa0-51fa-49e1-a1dc-26dcd6ac26cc/last-tool-error-state.json +7 -0
- package/.omc/state/sessions/11c41aa0-51fa-49e1-a1dc-26dcd6ac26cc/mission-state.json +117 -0
- package/.omc/state/sessions/11c41aa0-51fa-49e1-a1dc-26dcd6ac26cc/pre-tool-advisory-throttle.json +42 -0
- package/.omc/state/sessions/11c41aa0-51fa-49e1-a1dc-26dcd6ac26cc/subagent-tracking-state.json +53 -0
- package/.omc/state/sessions/4cf1c186-81b2-497c-b002-d7f84e7839f3/last-tool-error-state.json +7 -0
- package/.omc/state/sessions/4cf1c186-81b2-497c-b002-d7f84e7839f3/pre-tool-advisory-throttle.json +18 -0
- package/.omc/state/sessions/4cf1c186-81b2-497c-b002-d7f84e7839f3/subagent-tracking-state.json +7 -0
- package/.omc/state/sessions/5c4ab554-64f1-42ae-83e3-21e0237e955c/mission-state.json +117 -0
- package/.omc/state/sessions/5c4ab554-64f1-42ae-83e3-21e0237e955c/pre-tool-advisory-throttle.json +18 -0
- package/.omc/state/sessions/5c4ab554-64f1-42ae-83e3-21e0237e955c/subagent-tracking-state.json +17 -0
- package/.omc/state/sessions/a60ac2ec-afbd-449f-a540-6df287392fc2/pre-tool-advisory-throttle.json +18 -0
- package/.omc/state/sessions/a60ac2ec-afbd-449f-a540-6df287392fc2/subagent-tracking-state.json +7 -0
- package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/last-tool-error-state.json +7 -0
- package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/mission-state.json +89 -0
- package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/pre-tool-advisory-throttle.json +34 -0
- package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/ralph-state.json +13 -0
- package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/skill-active-state.json +15 -0
- package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/subagent-tracking-state.json +35 -0
- package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/ultrawork-state.json +11 -0
- package/.omc/state/sessions/c967c819-3d1c-447b-ab48-56a8448ef9f8/pre-tool-advisory-throttle.json +10 -0
- package/.omc/state/sessions/c967c819-3d1c-447b-ab48-56a8448ef9f8/subagent-tracking-state.json +7 -0
- package/.omc/state/sessions/e947a3c6-989d-4a60-91dd-6b0ddd827b2d/last-tool-error-state.json +7 -0
- package/.omc/state/sessions/e947a3c6-989d-4a60-91dd-6b0ddd827b2d/pre-tool-advisory-throttle.json +10 -0
- package/.omc/state/sessions/e947a3c6-989d-4a60-91dd-6b0ddd827b2d/subagent-tracking-state.json +26 -0
- package/.omc/state/subagent-tracking.json +14 -4
- package/.turbo/turbo-build.log +7 -0
- package/.turbo/turbo-check.log +14 -0
- package/.turbo/turbo-prettier-fix.log +2 -1
- package/.turbo/turbo-test.log +28 -5
- package/.turbo/turbo-validate.log +14 -0
- package/dist/cli.js +88 -74
- package/dist/cli.js.map +4 -4
- package/dist/entry/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +7 -0
- package/dist/lib/package-config/package-config.d.ts +71 -0
- package/dist/mcp.js +43 -41
- package/dist/mcp.js.map +4 -4
- package/eslint.config.js +1 -1
- package/infra-kit.config.ts +5 -0
- package/package.json +20 -13
- package/scripts/build.js +32 -3
- package/src/.omc/state/sessions/0a58307d-2a37-4c69-851c-83a646502d62/pre-tool-advisory-throttle.json +18 -0
- package/src/commands/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/pre-tool-advisory-throttle.json +14 -0
- package/src/commands/.omc/state/sessions/c967c819-3d1c-447b-ab48-56a8448ef9f8/pre-tool-advisory-throttle.json +18 -0
- package/src/commands/audit/__tests__/audit.test.ts +59 -0
- package/src/commands/audit/audit.ts +177 -0
- package/src/commands/audit/index.ts +1 -0
- package/src/commands/config/config.ts +49 -7
- package/src/commands/doctor/__tests__/agent-files.test.ts +110 -0
- package/src/commands/doctor/doctor.ts +69 -4
- package/src/commands/env-clear/env-clear.ts +1 -1
- package/src/commands/env-list/env-list.ts +3 -3
- package/src/commands/env-load/env-load.ts +1 -1
- package/src/commands/env-status/env-status.ts +1 -1
- package/src/commands/gh-merge-dev/gh-merge-dev.ts +3 -8
- package/src/commands/gh-release-deliver/gh-release-deliver.ts +47 -21
- package/src/commands/gh-release-deploy-all/gh-release-deploy-all.ts +13 -7
- package/src/commands/gh-release-deploy-selected/gh-release-deploy-selected.ts +12 -6
- package/src/commands/gh-release-list/gh-release-list.ts +19 -8
- package/src/commands/init/__tests__/agent-files.test.ts +147 -0
- package/src/commands/init/__tests__/migrate-config.test.ts +160 -0
- package/src/commands/init/agent-files.ts +199 -0
- package/src/commands/init/index.ts +7 -0
- package/src/commands/init/init.ts +82 -60
- package/src/commands/init/migrate-config.ts +146 -0
- package/src/commands/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/src/commands/release-create/__tests__/release-create.test.ts +55 -0
- package/src/commands/release-create/release-create.ts +142 -38
- package/src/commands/release-desc-edit/release-desc-edit.ts +28 -8
- package/src/commands/version/version.ts +1 -1
- package/src/commands/worktrees-add/worktrees-add.ts +7 -12
- package/src/commands/worktrees-list/worktrees-list.ts +13 -5
- package/src/commands/worktrees-open/worktrees-open.ts +1 -1
- package/src/commands/worktrees-remove/worktrees-remove.ts +6 -10
- package/src/commands/worktrees-sync/worktrees-sync.ts +3 -5
- package/src/entry/cli.ts +50 -7
- package/src/entry/index.ts +5 -0
- package/src/integrations/cmux/open-workspace-with-layout.ts +4 -4
- package/src/integrations/cmux/workspace-title.ts +10 -4
- package/src/integrations/doppler/doppler-project.ts +1 -1
- package/src/integrations/gh/gh-release-prs/__tests__/gh-release-prs.test.ts +115 -0
- package/src/integrations/gh/gh-release-prs/gh-release-prs.ts +49 -32
- package/src/lib/.omc/state/sessions/a60ac2ec-afbd-449f-a540-6df287392fc2/pre-tool-advisory-throttle.json +14 -0
- package/src/lib/constants/index.ts +15 -0
- package/src/lib/git-utils/__tests__/git-utils.test.ts +49 -0
- package/src/lib/git-utils/git-utils.ts +3 -1
- package/src/lib/infra-kit-config/__tests__/infra-kit-config.test.ts +270 -0
- package/src/lib/infra-kit-config/index.ts +7 -1
- package/src/lib/infra-kit-config/infra-kit-config.ts +46 -28
- package/src/lib/managed-block/__tests__/managed-block.test.ts +121 -0
- package/src/lib/managed-block/index.ts +8 -0
- package/src/lib/managed-block/managed-block.ts +145 -0
- package/src/lib/package-config/__tests__/package-config.test.ts +95 -0
- package/src/lib/package-config/index.ts +3 -0
- package/src/lib/package-config/package-config-schema.ts +19 -0
- package/src/lib/package-config/package-config.ts +99 -0
- package/src/lib/package-validator/__tests__/package-validator.test.ts +263 -0
- package/src/lib/package-validator/checks/__tests__/checks.test.ts +130 -0
- package/src/lib/package-validator/checks/config-check.ts +30 -0
- package/src/lib/package-validator/checks/files-check.ts +29 -0
- package/src/lib/package-validator/checks/index.ts +4 -0
- package/src/lib/package-validator/checks/scripts-check.ts +23 -0
- package/src/lib/package-validator/checks/turbo-check.ts +47 -0
- package/src/lib/package-validator/fs-utils.ts +18 -0
- package/src/lib/package-validator/index.ts +3 -0
- package/src/lib/package-validator/loader/config-loader.ts +77 -0
- package/src/lib/package-validator/loader/index.ts +2 -0
- package/src/lib/package-validator/loader/package-discovery.ts +98 -0
- package/src/lib/package-validator/package-validator.ts +48 -0
- package/src/lib/package-validator/types.ts +15 -0
- package/src/lib/release-id/__tests__/release-id.test.ts +351 -0
- package/src/lib/release-id/__tests__/versioned-regression.test.ts +69 -0
- package/src/lib/release-id/index.ts +15 -0
- package/src/lib/release-id/release-id.ts +257 -0
- package/src/lib/release-utils/__tests__/release-utils.test.ts +122 -0
- package/src/lib/release-utils/index.ts +4 -0
- package/src/lib/release-utils/release-utils.ts +85 -17
- package/src/lib/version-utils/__tests__/load-existing-versions.test.ts +37 -0
- package/src/lib/version-utils/__tests__/next-version.test.ts +119 -13
- package/src/lib/version-utils/index.ts +3 -0
- package/src/lib/version-utils/load-existing-versions.ts +29 -10
- package/src/lib/version-utils/next-version.ts +67 -12
- package/src/lib/version-utils/version-utils.ts +13 -4
- package/src/mcp/tools/index.ts +2 -0
- package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/src/types.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/src/lib/__tests__/infra-kit-config.test.ts +0 -231
- /package/src/integrations/{clickup → linear}/.gitkeep +0 -0
- /package/src/lib/{__tests__ → constants/__tests__}/constants.test.ts +0 -0
- /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,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,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,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
|
+
}
|