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,147 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
5
|
+
|
|
6
|
+
import { getProjectRoot, getRepoName } from 'src/lib/git-utils'
|
|
7
|
+
|
|
8
|
+
import { AGENTS_IMPORT_START, AGENTS_MARKER_END, AGENTS_MARKER_START, writeAgentFiles } from '../agent-files'
|
|
9
|
+
|
|
10
|
+
vi.mock('src/lib/git-utils', () => {
|
|
11
|
+
return {
|
|
12
|
+
getProjectRoot: vi.fn(),
|
|
13
|
+
getRepoName: vi.fn(),
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const writeFile = (filePath: string, content: string): void => {
|
|
18
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
19
|
+
fs.writeFileSync(filePath, content, 'utf-8')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A tmp dir that is a "valid repo" (has infra-kit.json) unless `repo: false`. */
|
|
23
|
+
const withTmpRepo = async (fn: (tmp: string) => Promise<void>, opts: { repo?: boolean } = {}): Promise<void> => {
|
|
24
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'infra-kit-agents-test-'))
|
|
25
|
+
|
|
26
|
+
vi.mocked(getProjectRoot).mockResolvedValue(tmp)
|
|
27
|
+
vi.mocked(getRepoName).mockResolvedValue(path.basename(tmp))
|
|
28
|
+
|
|
29
|
+
if (opts.repo !== false) {
|
|
30
|
+
writeFile(path.join(tmp, 'infra-kit.json'), '{"environments":["dev"]}\n')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
await fn(tmp)
|
|
35
|
+
} finally {
|
|
36
|
+
fs.rmSync(tmp, { recursive: true, force: true })
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('writeAgentFiles', () => {
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
vi.clearAllMocks()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
vi.clearAllMocks()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('creates AGENTS.md, the CLAUDE.md import, and the .cursor rule on a fresh repo', async () => {
|
|
50
|
+
await withTmpRepo(async (tmp) => {
|
|
51
|
+
const result = await writeAgentFiles()
|
|
52
|
+
|
|
53
|
+
expect(result.skipped).toBe(false)
|
|
54
|
+
expect(result.root).toBe(tmp)
|
|
55
|
+
|
|
56
|
+
const agents = fs.readFileSync(path.join(tmp, 'AGENTS.md'), 'utf-8')
|
|
57
|
+
|
|
58
|
+
expect(agents).toContain(AGENTS_MARKER_START)
|
|
59
|
+
expect(agents).toContain(AGENTS_MARKER_END)
|
|
60
|
+
expect(agents).toContain('<!-- infra-kit:version ')
|
|
61
|
+
|
|
62
|
+
const claude = fs.readFileSync(path.join(tmp, 'CLAUDE.md'), 'utf-8')
|
|
63
|
+
|
|
64
|
+
expect(claude).toContain(AGENTS_IMPORT_START)
|
|
65
|
+
expect(claude).toContain('@AGENTS.md')
|
|
66
|
+
|
|
67
|
+
const cursor = fs.readFileSync(path.join(tmp, '.cursor', 'rules', 'infra-kit.mdc'), 'utf-8')
|
|
68
|
+
|
|
69
|
+
expect(cursor).toContain('alwaysApply: true')
|
|
70
|
+
expect(cursor).toContain(AGENTS_MARKER_START)
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('preserves existing hand-authored CLAUDE.md content verbatim (ticket conventions)', async () => {
|
|
75
|
+
await withTmpRepo(async (tmp) => {
|
|
76
|
+
const claudePath = path.join(tmp, 'CLAUDE.md')
|
|
77
|
+
const userContent = '# CLAUDE.md\n\n## Ticket Naming Convention\n\n- `[FE]` — frontend\n- `[BE]` — backend\n'
|
|
78
|
+
|
|
79
|
+
writeFile(claudePath, userContent)
|
|
80
|
+
|
|
81
|
+
await writeAgentFiles()
|
|
82
|
+
|
|
83
|
+
const updated = fs.readFileSync(claudePath, 'utf-8')
|
|
84
|
+
|
|
85
|
+
expect(updated).toContain('## Ticket Naming Convention')
|
|
86
|
+
expect(updated).toContain('`[FE]` — frontend')
|
|
87
|
+
expect(updated).toContain(AGENTS_IMPORT_START)
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('is idempotent — a second run does not duplicate the block', async () => {
|
|
92
|
+
await withTmpRepo(async (tmp) => {
|
|
93
|
+
await writeAgentFiles()
|
|
94
|
+
const first = fs.readFileSync(path.join(tmp, 'AGENTS.md'), 'utf-8')
|
|
95
|
+
|
|
96
|
+
const second = await writeAgentFiles()
|
|
97
|
+
const after = fs.readFileSync(path.join(tmp, 'AGENTS.md'), 'utf-8')
|
|
98
|
+
|
|
99
|
+
expect(after).toBe(first)
|
|
100
|
+
expect(after.match(new RegExp(AGENTS_MARKER_START, 'g'))?.length).toBe(1)
|
|
101
|
+
expect(
|
|
102
|
+
second.written.every((w) => {
|
|
103
|
+
return w.action === 'unchanged'
|
|
104
|
+
}),
|
|
105
|
+
).toBe(true)
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('is a no-op outside an infra-kit repo (no infra-kit.json)', async () => {
|
|
110
|
+
await withTmpRepo(
|
|
111
|
+
async (tmp) => {
|
|
112
|
+
const result = await writeAgentFiles()
|
|
113
|
+
|
|
114
|
+
expect(result.skipped).toBe(true)
|
|
115
|
+
expect(result.root).toBeNull()
|
|
116
|
+
expect(fs.existsSync(path.join(tmp, 'AGENTS.md'))).toBe(false)
|
|
117
|
+
},
|
|
118
|
+
{ repo: false },
|
|
119
|
+
)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('refuses to write through a symlinked AGENTS.md', async () => {
|
|
123
|
+
await withTmpRepo(async (tmp) => {
|
|
124
|
+
const realTarget = path.join(tmp, 'elsewhere.md')
|
|
125
|
+
|
|
126
|
+
writeFile(realTarget, 'sensitive')
|
|
127
|
+
fs.symlinkSync(realTarget, path.join(tmp, 'AGENTS.md'))
|
|
128
|
+
|
|
129
|
+
await expect(writeAgentFiles()).rejects.toThrow(/symlink/)
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('backs up an existing AGENTS.md before overwriting', async () => {
|
|
134
|
+
await withTmpRepo(async (tmp) => {
|
|
135
|
+
writeFile(path.join(tmp, 'AGENTS.md'), 'previous content without markers\n')
|
|
136
|
+
|
|
137
|
+
await writeAgentFiles()
|
|
138
|
+
|
|
139
|
+
const backups = fs.readdirSync(tmp).filter((f) => {
|
|
140
|
+
return f.startsWith('AGENTS.md.backup.')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
expect(backups.length).toBe(1)
|
|
144
|
+
expect(fs.readFileSync(path.join(tmp, backups[0]!), 'utf-8')).toContain('previous content without markers')
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
})
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
5
|
+
|
|
6
|
+
// Import AFTER the mock is declared so the module picks up the mocked dep.
|
|
7
|
+
import { getProjectRoot, getRepoName } from 'src/lib/git-utils'
|
|
8
|
+
|
|
9
|
+
import { migrateLegacyConfig } from '../migrate-config'
|
|
10
|
+
|
|
11
|
+
vi.mock('src/lib/git-utils', () => {
|
|
12
|
+
return {
|
|
13
|
+
getProjectRoot: vi.fn(),
|
|
14
|
+
getRepoName: vi.fn(),
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const MAIN_YML = `environments:
|
|
19
|
+
- dev
|
|
20
|
+
- staging
|
|
21
|
+
envManagement:
|
|
22
|
+
provider: doppler
|
|
23
|
+
config:
|
|
24
|
+
name: my-project
|
|
25
|
+
`
|
|
26
|
+
|
|
27
|
+
const writeFile = (filePath: string, content: string): void => {
|
|
28
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
29
|
+
fs.writeFileSync(filePath, content, 'utf-8')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const withTmpRepo = async (fn: (tmp: string) => Promise<void>): Promise<void> => {
|
|
33
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'infra-kit-init-migrate-test-'))
|
|
34
|
+
|
|
35
|
+
vi.mocked(getProjectRoot).mockResolvedValue(tmp)
|
|
36
|
+
vi.mocked(getRepoName).mockResolvedValue(path.basename(tmp))
|
|
37
|
+
const homedirSpy = vi.spyOn(os, 'homedir').mockReturnValue(tmp)
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
await fn(tmp)
|
|
41
|
+
} finally {
|
|
42
|
+
homedirSpy.mockRestore()
|
|
43
|
+
fs.rmSync(tmp, { recursive: true, force: true })
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('migrateLegacyConfig', () => {
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
vi.clearAllMocks()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
vi.clearAllMocks()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('converts a legacy infra-kit.yml to infra-kit.json and removes the .yml', async () => {
|
|
57
|
+
await withTmpRepo(async (tmp) => {
|
|
58
|
+
const ymlPath = path.join(tmp, 'infra-kit.yml')
|
|
59
|
+
const jsonPath = path.join(tmp, 'infra-kit.json')
|
|
60
|
+
|
|
61
|
+
writeFile(ymlPath, MAIN_YML)
|
|
62
|
+
|
|
63
|
+
await migrateLegacyConfig()
|
|
64
|
+
|
|
65
|
+
expect(fs.existsSync(ymlPath)).toBe(false)
|
|
66
|
+
expect(fs.existsSync(jsonPath)).toBe(true)
|
|
67
|
+
expect(JSON.parse(fs.readFileSync(jsonPath, 'utf-8'))).toEqual({
|
|
68
|
+
environments: ['dev', 'staging'],
|
|
69
|
+
envManagement: { provider: 'doppler', config: { name: 'my-project' } },
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('is an idempotent no-op when the config is already JSON', async () => {
|
|
75
|
+
await withTmpRepo(async (tmp) => {
|
|
76
|
+
const jsonPath = path.join(tmp, 'infra-kit.json')
|
|
77
|
+
const json = '{"environments":["dev"],"envManagement":{"provider":"doppler","config":{"name":"p"}}}'
|
|
78
|
+
|
|
79
|
+
writeFile(jsonPath, json)
|
|
80
|
+
|
|
81
|
+
await expect(migrateLegacyConfig()).resolves.toBeUndefined()
|
|
82
|
+
|
|
83
|
+
expect(fs.readFileSync(jsonPath, 'utf-8')).toBe(json)
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('warns and skips (does not throw or overwrite) when both .yml and .json exist', async () => {
|
|
88
|
+
await withTmpRepo(async (tmp) => {
|
|
89
|
+
const ymlPath = path.join(tmp, 'infra-kit.yml')
|
|
90
|
+
const jsonPath = path.join(tmp, 'infra-kit.json')
|
|
91
|
+
const existingJson = '{"environments":["keep"],"envManagement":{"provider":"doppler","config":{"name":"keep"}}}'
|
|
92
|
+
|
|
93
|
+
writeFile(ymlPath, MAIN_YML)
|
|
94
|
+
writeFile(jsonPath, existingJson)
|
|
95
|
+
|
|
96
|
+
await expect(migrateLegacyConfig()).resolves.toBeUndefined()
|
|
97
|
+
|
|
98
|
+
// Conflict left untouched — no overwrite, .yml preserved.
|
|
99
|
+
expect(fs.existsSync(ymlPath)).toBe(true)
|
|
100
|
+
expect(fs.readFileSync(jsonPath, 'utf-8')).toBe(existingJson)
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('skips an invalid layer but still converts a valid sibling layer (non-fatal, per-layer)', async () => {
|
|
105
|
+
await withTmpRepo(async (tmp) => {
|
|
106
|
+
const mainYml = path.join(tmp, 'infra-kit.yml')
|
|
107
|
+
const mainJson = path.join(tmp, 'infra-kit.json')
|
|
108
|
+
const userGlobalYml = path.join(tmp, '.infra-kit', 'config.yml')
|
|
109
|
+
const userGlobalJson = path.join(tmp, '.infra-kit', 'config.json')
|
|
110
|
+
|
|
111
|
+
writeFile(mainYml, MAIN_YML)
|
|
112
|
+
// Invalid override: environments present but empty (min(1) fails).
|
|
113
|
+
writeFile(userGlobalYml, 'environments: []\n')
|
|
114
|
+
|
|
115
|
+
await expect(migrateLegacyConfig()).resolves.toBeUndefined()
|
|
116
|
+
|
|
117
|
+
// Valid main layer converted…
|
|
118
|
+
expect(fs.existsSync(mainYml)).toBe(false)
|
|
119
|
+
expect(fs.existsSync(mainJson)).toBe(true)
|
|
120
|
+
// …invalid user-global layer left as-is (no JSON written).
|
|
121
|
+
expect(fs.existsSync(userGlobalYml)).toBe(true)
|
|
122
|
+
expect(fs.existsSync(userGlobalJson)).toBe(false)
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('warns and skips a malformed .yml without throwing', async () => {
|
|
127
|
+
await withTmpRepo(async (tmp) => {
|
|
128
|
+
const ymlPath = path.join(tmp, 'infra-kit.yml')
|
|
129
|
+
const jsonPath = path.join(tmp, 'infra-kit.json')
|
|
130
|
+
|
|
131
|
+
// Unparseable YAML (bad indentation / flow) — yaml.parse throws.
|
|
132
|
+
writeFile(ymlPath, 'environments: [dev\n : : :\n')
|
|
133
|
+
|
|
134
|
+
await expect(migrateLegacyConfig()).resolves.toBeUndefined()
|
|
135
|
+
|
|
136
|
+
expect(fs.existsSync(ymlPath)).toBe(true)
|
|
137
|
+
expect(fs.existsSync(jsonPath)).toBe(false)
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('converts all three merge-chain layers in one run', async () => {
|
|
142
|
+
await withTmpRepo(async (tmp) => {
|
|
143
|
+
const projectName = path.basename(tmp)
|
|
144
|
+
const mainYml = path.join(tmp, 'infra-kit.yml')
|
|
145
|
+
const userGlobalYml = path.join(tmp, '.infra-kit', 'config.yml')
|
|
146
|
+
const userProjectYml = path.join(tmp, '.infra-kit', 'projects', projectName, 'infra-kit.yml')
|
|
147
|
+
|
|
148
|
+
writeFile(mainYml, MAIN_YML)
|
|
149
|
+
writeFile(userGlobalYml, 'worktrees:\n openInCmux: true\n')
|
|
150
|
+
writeFile(userProjectYml, 'worktrees:\n openInGithubDesktop: false\n')
|
|
151
|
+
|
|
152
|
+
await migrateLegacyConfig()
|
|
153
|
+
|
|
154
|
+
for (const yml of [mainYml, userGlobalYml, userProjectYml]) {
|
|
155
|
+
expect(fs.existsSync(yml)).toBe(false)
|
|
156
|
+
expect(fs.existsSync(yml.replace(/\.yml$/, '.json'))).toBe(true)
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
})
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { getInfraKitConfigPaths } from 'src/lib/infra-kit-config'
|
|
5
|
+
import { logger } from 'src/lib/logger'
|
|
6
|
+
import { hasManagedBlock, upsertManagedBlock } from 'src/lib/managed-block'
|
|
7
|
+
|
|
8
|
+
import packageJson from '../../../package.json' with { type: 'json' }
|
|
9
|
+
|
|
10
|
+
/** Markers for the generated guidance block (HTML comments — invisible in rendered markdown). */
|
|
11
|
+
export const AGENTS_MARKER_START = '<!-- infra-kit:begin -->'
|
|
12
|
+
export const AGENTS_MARKER_END = '<!-- infra-kit:end -->'
|
|
13
|
+
/** The version line lives on its own line (OMC `OMC:VERSION:` precedent) so the markers stay constant-matchable. */
|
|
14
|
+
const AGENTS_VERSION_PREFIX = '<!-- infra-kit:version '
|
|
15
|
+
|
|
16
|
+
/** Separate marker pair for the small `@AGENTS.md` import region injected into CLAUDE.md. */
|
|
17
|
+
export const AGENTS_IMPORT_START = '<!-- infra-kit:import:begin -->'
|
|
18
|
+
export const AGENTS_IMPORT_END = '<!-- infra-kit:import:end -->'
|
|
19
|
+
|
|
20
|
+
const AGENTS_FILE = 'AGENTS.md'
|
|
21
|
+
const CLAUDE_FILE = 'CLAUDE.md'
|
|
22
|
+
const CURSOR_RULE_REL = path.join('.cursor', 'rules', 'infra-kit.mdc')
|
|
23
|
+
|
|
24
|
+
const CURSOR_FRONTMATTER = [
|
|
25
|
+
'---',
|
|
26
|
+
'description: infra-kit CLI usage and conventions',
|
|
27
|
+
'alwaysApply: true',
|
|
28
|
+
'---',
|
|
29
|
+
'',
|
|
30
|
+
].join('\n')
|
|
31
|
+
|
|
32
|
+
export type WriteAction = 'created' | 'updated' | 'unchanged'
|
|
33
|
+
|
|
34
|
+
export interface AgentFileWrite {
|
|
35
|
+
path: string
|
|
36
|
+
action: WriteAction
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface WriteAgentFilesResult {
|
|
40
|
+
/** True when run outside an infra-kit repo (no `infra-kit.json` at the git root). */
|
|
41
|
+
skipped: boolean
|
|
42
|
+
/** Repo root the files were written under, or null when skipped. */
|
|
43
|
+
root: string | null
|
|
44
|
+
written: AgentFileWrite[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The version comment line, e.g. `<!-- infra-kit:version 0.1.105 -->`. */
|
|
48
|
+
const buildVersionLine = (version: string): string => {
|
|
49
|
+
return `${AGENTS_VERSION_PREFIX}${version} -->`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The generated guidance body (without the surrounding markers). Describes the
|
|
54
|
+
* infra-kit CLI surface so any AI agent working in the repo learns it. Curated
|
|
55
|
+
* from the real command set; the version line records which CLI version generated it.
|
|
56
|
+
*/
|
|
57
|
+
const buildAgentsBody = (version: string): string => {
|
|
58
|
+
return [
|
|
59
|
+
buildVersionLine(version),
|
|
60
|
+
'',
|
|
61
|
+
'# infra-kit',
|
|
62
|
+
'',
|
|
63
|
+
'This repository uses the **infra-kit** CLI for environment, worktree, and release workflows.',
|
|
64
|
+
'This block is generated by `infra-kit init` — edit text *outside* the markers, never inside.',
|
|
65
|
+
'',
|
|
66
|
+
'## Commands (`ik` = `pnpm exec infra-kit`)',
|
|
67
|
+
'',
|
|
68
|
+
'- `ik env-load -c <config>` / `ik env-clear` / `ik env-status` — load, clear, or inspect Doppler env vars for a config (e.g. `dev`). Source the returned file to apply.',
|
|
69
|
+
'- `ik worktrees-add` / `worktrees-list` / `worktrees-open` / `worktrees-remove` / `worktrees-sync` — manage release and feature git worktrees.',
|
|
70
|
+
'- `ik release-create` / `release-list` / `release-deploy-all` / `release-deploy-selected` / `release-deliver` / `release-desc-edit` — release-branch and deploy flow.',
|
|
71
|
+
'- `ik merge-dev` — merge the dev branch into every release branch.',
|
|
72
|
+
'- `ik audit` — audit packages against `infra-kit.config.ts` rules.',
|
|
73
|
+
'- `ik doctor` — check gh / doppler / aws / rtk install + auth and repo setup.',
|
|
74
|
+
'- `ik init` — (re)install shell integration and regenerate these agent-instruction files.',
|
|
75
|
+
'',
|
|
76
|
+
'## Conventions',
|
|
77
|
+
'',
|
|
78
|
+
'- Tickets are prefixed by area: `[FE]` frontend, `[BE]` backend, `[DO]` DevOps/infra, `[APP]` mobile app, `[IDEA]` proposal, `[ROOT]` cross-cutting. Pick the single dominant area.',
|
|
79
|
+
'- Environment variables are managed through Doppler via `ik env-load` — never commit secrets.',
|
|
80
|
+
].join('\n')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const assertNotSymlink = (filePath: string): void => {
|
|
84
|
+
if (fs.existsSync(filePath) && fs.lstatSync(filePath).isSymbolicLink()) {
|
|
85
|
+
throw new Error(`Refusing to write ${filePath} because the destination is a symlink`)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Write `nextContent` to `filePath` with OMC-style safety: refuse symlinks,
|
|
91
|
+
* back up the prior file with a timestamp before overwriting, and skip the
|
|
92
|
+
* write entirely when nothing changed (keeps re-runs churn-free).
|
|
93
|
+
*/
|
|
94
|
+
const writeManaged = (filePath: string, nextContent: string): WriteAction => {
|
|
95
|
+
assertNotSymlink(filePath)
|
|
96
|
+
|
|
97
|
+
const existed = fs.existsSync(filePath)
|
|
98
|
+
const previous = existed ? fs.readFileSync(filePath, 'utf-8') : null
|
|
99
|
+
|
|
100
|
+
if (previous === nextContent) return 'unchanged'
|
|
101
|
+
|
|
102
|
+
if (existed) {
|
|
103
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-')
|
|
104
|
+
|
|
105
|
+
fs.copyFileSync(filePath, `${filePath}.backup.${stamp}`)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
109
|
+
fs.writeFileSync(filePath, nextContent, 'utf-8')
|
|
110
|
+
|
|
111
|
+
return existed ? 'updated' : 'created'
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const readOr = (filePath: string, fallback: string): string => {
|
|
115
|
+
return fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : fallback
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const assertBlockPresent = (filePath: string, start: string, end: string): void => {
|
|
119
|
+
const content = fs.readFileSync(filePath, 'utf-8')
|
|
120
|
+
|
|
121
|
+
if (!hasManagedBlock(content, start, end)) {
|
|
122
|
+
throw new Error(`Post-write validation failed: managed block missing from ${filePath}`)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Generate (or refresh) the repo agent-instruction files: the `AGENTS.md`
|
|
128
|
+
* guidance block, a managed `@AGENTS.md` import region in `CLAUDE.md`
|
|
129
|
+
* (preserving existing content), and a `.cursor/rules` rule file. Repo-gated:
|
|
130
|
+
* a no-op outside an infra-kit repo. Idempotent and non-destructive.
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* await writeAgentFiles()
|
|
134
|
+
* // INFO: Agent-instruction files synced (infra-kit 0.1.105)
|
|
135
|
+
*/
|
|
136
|
+
export const writeAgentFiles = async (): Promise<WriteAgentFilesResult> => {
|
|
137
|
+
let mainConfigPath: string
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
mainConfigPath = (await getInfraKitConfigPaths()).main
|
|
141
|
+
} catch {
|
|
142
|
+
logger.info('Skipped agent-instruction files — not inside an infra-kit repo')
|
|
143
|
+
|
|
144
|
+
return { skipped: true, root: null, written: [] }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!fs.existsSync(mainConfigPath)) {
|
|
148
|
+
logger.info('Skipped agent-instruction files — no infra-kit.json at the repo root')
|
|
149
|
+
|
|
150
|
+
return { skipped: true, root: null, written: [] }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const root = path.dirname(mainConfigPath)
|
|
154
|
+
const version = packageJson.version
|
|
155
|
+
const body = buildAgentsBody(version)
|
|
156
|
+
|
|
157
|
+
const agentsPath = path.join(root, AGENTS_FILE)
|
|
158
|
+
const claudePath = path.join(root, CLAUDE_FILE)
|
|
159
|
+
const cursorPath = path.join(root, CURSOR_RULE_REL)
|
|
160
|
+
|
|
161
|
+
const agentsContent = upsertManagedBlock({
|
|
162
|
+
content: readOr(agentsPath, ''),
|
|
163
|
+
body,
|
|
164
|
+
startMarker: AGENTS_MARKER_START,
|
|
165
|
+
endMarker: AGENTS_MARKER_END,
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
const claudeContent = upsertManagedBlock({
|
|
169
|
+
content: readOr(claudePath, ''),
|
|
170
|
+
body: `@${AGENTS_FILE}`,
|
|
171
|
+
startMarker: AGENTS_IMPORT_START,
|
|
172
|
+
endMarker: AGENTS_IMPORT_END,
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
const cursorContent = upsertManagedBlock({
|
|
176
|
+
content: readOr(cursorPath, CURSOR_FRONTMATTER),
|
|
177
|
+
body,
|
|
178
|
+
startMarker: AGENTS_MARKER_START,
|
|
179
|
+
endMarker: AGENTS_MARKER_END,
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
const written: AgentFileWrite[] = [
|
|
183
|
+
{ path: agentsPath, action: writeManaged(agentsPath, agentsContent) },
|
|
184
|
+
{ path: claudePath, action: writeManaged(claudePath, claudeContent) },
|
|
185
|
+
{ path: cursorPath, action: writeManaged(cursorPath, cursorContent) },
|
|
186
|
+
]
|
|
187
|
+
|
|
188
|
+
assertBlockPresent(agentsPath, AGENTS_MARKER_START, AGENTS_MARKER_END)
|
|
189
|
+
assertBlockPresent(claudePath, AGENTS_IMPORT_START, AGENTS_IMPORT_END)
|
|
190
|
+
assertBlockPresent(cursorPath, AGENTS_MARKER_START, AGENTS_MARKER_END)
|
|
191
|
+
|
|
192
|
+
for (const file of written) {
|
|
193
|
+
logger.info(` ${file.action.padEnd(9)} ${path.relative(root, file.path)}`)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
logger.info(`Agent-instruction files synced (infra-kit ${version})`)
|
|
197
|
+
|
|
198
|
+
return { skipped: false, root, written }
|
|
199
|
+
}
|