infra-kit 0.1.105 → 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-afc6290b-40d3-4bef-b3b6-14484c034ab9.jsonl +14 -0
- package/.omc/state/agent-replay-e947a3c6-989d-4a60-91dd-6b0ddd827b2d.jsonl +3 -0
- package/.omc/state/idle-notif-cooldown.json +1 -1
- 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/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/dist/cli.js +61 -54
- package/dist/cli.js.map +4 -4
- package/dist/mcp.js +2 -2
- package/dist/mcp.js.map +2 -2
- package/package.json +1 -1
- package/src/commands/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/pre-tool-advisory-throttle.json +14 -0
- package/src/commands/doctor/__tests__/agent-files.test.ts +110 -0
- package/src/commands/doctor/doctor.ts +66 -1
- package/src/commands/init/__tests__/agent-files.test.ts +147 -0
- package/src/commands/init/agent-files.ts +199 -0
- package/src/commands/init/index.ts +7 -0
- package/src/commands/init/init.ts +34 -25
- package/src/entry/cli.ts +1 -1
- package/src/integrations/cmux/open-workspace-with-layout.ts +1 -1
- 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/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"entries": {
|
|
4
|
+
"79a93d4a2f8f50b95f852280616242fee1855dc99a3c75211917f55e72e95fae": {
|
|
5
|
+
"last_emitted_at_ms": 1781510589151,
|
|
6
|
+
"message": "Use parallel execution for independent tasks. Use run_in_background for long operations (npm install, builds, tests)."
|
|
7
|
+
},
|
|
8
|
+
"466399dafa2d20f60587180bad0a07358eb7f2bce724df0ff682f038ad33f5ad": {
|
|
9
|
+
"last_emitted_at_ms": 1781510561764,
|
|
10
|
+
"message": "Read multiple files in parallel when possible for faster analysis."
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"updated_at": "2026-06-15T08:03:09.151Z"
|
|
14
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
5
|
+
|
|
6
|
+
import { writeAgentFiles } from 'src/commands/init/agent-files'
|
|
7
|
+
import { getProjectRoot, getRepoName } from 'src/lib/git-utils'
|
|
8
|
+
|
|
9
|
+
import { checkAgentFiles } from '../doctor'
|
|
10
|
+
|
|
11
|
+
vi.mock('src/lib/git-utils', () => {
|
|
12
|
+
return {
|
|
13
|
+
getProjectRoot: vi.fn(),
|
|
14
|
+
getRepoName: vi.fn(),
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const writeFile = (filePath: string, content: string): void => {
|
|
19
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
20
|
+
fs.writeFileSync(filePath, content, 'utf-8')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const withTmpRepo = async (fn: (tmp: string) => Promise<void>, opts: { repo?: boolean } = {}): Promise<void> => {
|
|
24
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'infra-kit-doctor-agents-test-'))
|
|
25
|
+
|
|
26
|
+
vi.mocked(getProjectRoot).mockResolvedValue(tmp)
|
|
27
|
+
vi.mocked(getRepoName).mockResolvedValue(path.basename(tmp))
|
|
28
|
+
|
|
29
|
+
if (opts.repo !== false) {
|
|
30
|
+
writeFile(path.join(tmp, 'infra-kit.json'), '{"environments":["dev"]}\n')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
await fn(tmp)
|
|
35
|
+
} finally {
|
|
36
|
+
fs.rmSync(tmp, { recursive: true, force: true })
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const statusOf = (checks: { name: string; status: string }[], name: string): string | undefined => {
|
|
41
|
+
return checks.find((c) => {
|
|
42
|
+
return c.name === name
|
|
43
|
+
})?.status
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('checkAgentFiles', () => {
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
vi.clearAllMocks()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
vi.clearAllMocks()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('returns no checks outside an infra-kit repo (never crashes)', async () => {
|
|
56
|
+
await withTmpRepo(
|
|
57
|
+
async () => {
|
|
58
|
+
await expect(checkAgentFiles()).resolves.toEqual([])
|
|
59
|
+
},
|
|
60
|
+
{ repo: false },
|
|
61
|
+
)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('passes all three checks right after init runs the agent-files step', async () => {
|
|
65
|
+
await withTmpRepo(async () => {
|
|
66
|
+
await writeAgentFiles()
|
|
67
|
+
|
|
68
|
+
const checks = await checkAgentFiles()
|
|
69
|
+
|
|
70
|
+
expect(statusOf(checks, 'AGENTS.md block')).toBe('pass')
|
|
71
|
+
expect(statusOf(checks, 'CLAUDE.md import')).toBe('pass')
|
|
72
|
+
expect(statusOf(checks, '.cursor/rules block')).toBe('pass')
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('flags AGENTS.md present but CLAUDE.md import removed', async () => {
|
|
77
|
+
await withTmpRepo(async (tmp) => {
|
|
78
|
+
await writeAgentFiles()
|
|
79
|
+
// User deletes CLAUDE.md (drops the @AGENTS.md import) but keeps AGENTS.md.
|
|
80
|
+
fs.rmSync(path.join(tmp, 'CLAUDE.md'))
|
|
81
|
+
|
|
82
|
+
const checks = await checkAgentFiles()
|
|
83
|
+
|
|
84
|
+
expect(statusOf(checks, 'AGENTS.md block')).toBe('pass')
|
|
85
|
+
expect(statusOf(checks, 'CLAUDE.md import')).toBe('fail')
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('flags a missing .cursor/rules block', async () => {
|
|
90
|
+
await withTmpRepo(async (tmp) => {
|
|
91
|
+
await writeAgentFiles()
|
|
92
|
+
fs.rmSync(path.join(tmp, '.cursor', 'rules', 'infra-kit.mdc'))
|
|
93
|
+
|
|
94
|
+
const checks = await checkAgentFiles()
|
|
95
|
+
|
|
96
|
+
expect(statusOf(checks, '.cursor/rules block')).toBe('fail')
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('flags a missing AGENTS.md block', async () => {
|
|
101
|
+
await withTmpRepo(async (tmp) => {
|
|
102
|
+
await writeAgentFiles()
|
|
103
|
+
fs.rmSync(path.join(tmp, 'AGENTS.md'))
|
|
104
|
+
|
|
105
|
+
const checks = await checkAgentFiles()
|
|
106
|
+
|
|
107
|
+
expect(statusOf(checks, 'AGENTS.md block')).toBe('fail')
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
})
|
|
@@ -4,10 +4,17 @@ import path from 'node:path'
|
|
|
4
4
|
import { z } from 'zod'
|
|
5
5
|
import { $ } from 'zx'
|
|
6
6
|
|
|
7
|
+
import {
|
|
8
|
+
AGENTS_IMPORT_END,
|
|
9
|
+
AGENTS_IMPORT_START,
|
|
10
|
+
AGENTS_MARKER_END,
|
|
11
|
+
AGENTS_MARKER_START,
|
|
12
|
+
} from 'src/commands/init/agent-files'
|
|
7
13
|
import { MARKER_END, MARKER_START, buildShellBlock } from 'src/commands/init/init'
|
|
8
14
|
import { getProjectRoot } from 'src/lib/git-utils/git-utils'
|
|
9
15
|
import { getInfraKitConfig, getInfraKitConfigPaths, resetInfraKitConfigCache } from 'src/lib/infra-kit-config'
|
|
10
16
|
import { logger } from 'src/lib/logger'
|
|
17
|
+
import { hasManagedBlock } from 'src/lib/managed-block'
|
|
11
18
|
import { defineMcpTool, textContent } from 'src/types'
|
|
12
19
|
|
|
13
20
|
interface CheckResult {
|
|
@@ -196,11 +203,67 @@ const checkRtkConfigured = async (): Promise<CheckResult> => {
|
|
|
196
203
|
}
|
|
197
204
|
}
|
|
198
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Check that the repo agent-instruction files managed by `infra-kit init` exist:
|
|
208
|
+
* the `AGENTS.md` block, the `@AGENTS.md` import region in `CLAUDE.md`, and the
|
|
209
|
+
* `.cursor/rules` block. Presence only. Repo-gated: returns no checks when run
|
|
210
|
+
* outside an infra-kit repo so doctor never crashes there.
|
|
211
|
+
*/
|
|
212
|
+
export const checkAgentFiles = async (): Promise<CheckResult[]> => {
|
|
213
|
+
let mainConfigPath: string
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
mainConfigPath = (await getInfraKitConfigPaths()).main
|
|
217
|
+
} catch {
|
|
218
|
+
return []
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!fs.existsSync(mainConfigPath)) return []
|
|
222
|
+
|
|
223
|
+
const root = path.dirname(mainConfigPath)
|
|
224
|
+
|
|
225
|
+
const blockPresent = (relPath: string, start: string, end: string): boolean => {
|
|
226
|
+
const filePath = path.join(root, relPath)
|
|
227
|
+
const content = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : ''
|
|
228
|
+
|
|
229
|
+
return hasManagedBlock(content, start, end)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const entries = [
|
|
233
|
+
{
|
|
234
|
+
name: 'AGENTS.md block',
|
|
235
|
+
present: blockPresent('AGENTS.md', AGENTS_MARKER_START, AGENTS_MARKER_END),
|
|
236
|
+
okMessage: 'AGENTS.md block present',
|
|
237
|
+
missingMessage: 'infra-kit block missing from AGENTS.md. Run: infra-kit init',
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
name: 'CLAUDE.md import',
|
|
241
|
+
present: blockPresent('CLAUDE.md', AGENTS_IMPORT_START, AGENTS_IMPORT_END),
|
|
242
|
+
okMessage: 'CLAUDE.md imports @AGENTS.md',
|
|
243
|
+
missingMessage: '@AGENTS.md import block missing from CLAUDE.md. Run: infra-kit init',
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
name: '.cursor/rules block',
|
|
247
|
+
present: blockPresent(path.join('.cursor', 'rules', 'infra-kit.mdc'), AGENTS_MARKER_START, AGENTS_MARKER_END),
|
|
248
|
+
okMessage: '.cursor/rules/infra-kit.mdc block present',
|
|
249
|
+
missingMessage: '.cursor/rules/infra-kit.mdc block missing. Run: infra-kit init',
|
|
250
|
+
},
|
|
251
|
+
]
|
|
252
|
+
|
|
253
|
+
return entries.map((entry): CheckResult => {
|
|
254
|
+
return {
|
|
255
|
+
name: entry.name,
|
|
256
|
+
status: entry.present ? 'pass' : 'fail',
|
|
257
|
+
message: entry.present ? entry.okMessage : entry.missingMessage,
|
|
258
|
+
}
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
|
|
199
262
|
/**
|
|
200
263
|
* Check installation and authentication status of gh, doppler, aws, and rtk CLIs
|
|
201
264
|
*/
|
|
202
265
|
export const doctor = async () => {
|
|
203
|
-
const
|
|
266
|
+
const baseChecks: CheckResult[] = await Promise.all([
|
|
204
267
|
checkCommand(
|
|
205
268
|
'gh installed',
|
|
206
269
|
['gh', '--version'],
|
|
@@ -251,6 +314,8 @@ export const doctor = async () => {
|
|
|
251
314
|
checkUserOverridePath(),
|
|
252
315
|
])
|
|
253
316
|
|
|
317
|
+
const checks: CheckResult[] = [...baseChecks, ...(await checkAgentFiles())]
|
|
318
|
+
|
|
254
319
|
logger.info('Doctor check results:\n')
|
|
255
320
|
|
|
256
321
|
for (const check of checks) {
|
|
@@ -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,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
|
+
}
|