infra-kit 0.1.105 → 0.1.108

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 (50) hide show
  1. package/.eslintcache +1 -1
  2. package/.omc/state/agent-replay-36d91435-50b4-442f-9f4e-3cc68d776236.jsonl +2 -0
  3. package/.omc/state/agent-replay-afc6290b-40d3-4bef-b3b6-14484c034ab9.jsonl +14 -0
  4. package/.omc/state/agent-replay-e947a3c6-989d-4a60-91dd-6b0ddd827b2d.jsonl +3 -0
  5. package/.omc/state/idle-notif-cooldown.json +1 -1
  6. package/.omc/state/sessions/36d91435-50b4-442f-9f4e-3cc68d776236/pre-tool-advisory-throttle.json +18 -0
  7. package/.omc/state/sessions/36d91435-50b4-442f-9f4e-3cc68d776236/subagent-tracking-state.json +17 -0
  8. package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/last-tool-error-state.json +7 -0
  9. package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/mission-state.json +89 -0
  10. package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/pre-tool-advisory-throttle.json +34 -0
  11. package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/ralph-state.json +13 -0
  12. package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/skill-active-state.json +15 -0
  13. package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/subagent-tracking-state.json +35 -0
  14. package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/ultrawork-state.json +11 -0
  15. package/.omc/state/sessions/e947a3c6-989d-4a60-91dd-6b0ddd827b2d/last-tool-error-state.json +7 -0
  16. package/.omc/state/sessions/e947a3c6-989d-4a60-91dd-6b0ddd827b2d/pre-tool-advisory-throttle.json +10 -0
  17. package/.omc/state/sessions/e947a3c6-989d-4a60-91dd-6b0ddd827b2d/subagent-tracking-state.json +26 -0
  18. package/.turbo/turbo-build.log +2 -2
  19. package/.turbo/turbo-test.log +43 -6
  20. package/dist/cli.js +61 -54
  21. package/dist/cli.js.map +4 -4
  22. package/dist/mcp.js +31 -31
  23. package/dist/mcp.js.map +4 -4
  24. package/package.json +1 -1
  25. package/src/commands/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/pre-tool-advisory-throttle.json +14 -0
  26. package/src/commands/doctor/__tests__/agent-files.test.ts +110 -0
  27. package/src/commands/doctor/doctor.ts +66 -1
  28. package/src/commands/init/__tests__/agent-files.test.ts +147 -0
  29. package/src/commands/init/agent-files.ts +199 -0
  30. package/src/commands/init/index.ts +7 -0
  31. package/src/commands/init/init.ts +34 -25
  32. package/src/commands/worktrees-add/worktrees-add.ts +13 -1
  33. package/src/commands/worktrees-open/__tests__/open-cmux.test.ts +96 -0
  34. package/src/commands/worktrees-open/worktrees-open.ts +10 -3
  35. package/src/entry/cli.ts +1 -1
  36. package/src/integrations/cmux/__tests__/canonicalize-cmux-title.test.ts +56 -0
  37. package/src/integrations/cmux/__tests__/close-workspace-by-title.test.ts +63 -0
  38. package/src/integrations/cmux/__tests__/list-workspace-titles.test.ts +65 -0
  39. package/src/integrations/cmux/canonicalize-cmux-title.ts +31 -0
  40. package/src/integrations/cmux/close-workspace-by-title.ts +12 -4
  41. package/src/integrations/cmux/index.ts +1 -0
  42. package/src/integrations/cmux/list-workspace-titles.ts +11 -6
  43. package/src/integrations/cmux/open-workspace-with-layout.ts +1 -1
  44. package/src/integrations/gh/gh-release-prs/.omc/state/sessions/c6ed6186-1aac-48e0-aa2a-edc6da8e0410/pre-tool-advisory-throttle.json +10 -0
  45. package/src/integrations/gh/gh-release-prs/__tests__/gh-release-prs.test.ts +89 -6
  46. package/src/integrations/gh/gh-release-prs/gh-release-prs.ts +1 -1
  47. package/src/lib/managed-block/__tests__/managed-block.test.ts +121 -0
  48. package/src/lib/managed-block/index.ts +8 -0
  49. package/src/lib/managed-block/managed-block.ts +145 -0
  50. package/tsconfig.tsbuildinfo +1 -1
@@ -1,6 +1,8 @@
1
+ import { execFileSync } from 'node:child_process'
1
2
  import { beforeEach, describe, expect, it, vi } from 'vitest'
3
+ import { quote } from 'zx'
2
4
 
3
- import { getReleasePRs, getReleasePRsWithInfo } from '../gh-release-prs'
5
+ import { createReleaseBranch, getReleasePRs, getReleasePRsWithInfo } from '../gh-release-prs'
4
6
 
5
7
  interface FakePR {
6
8
  number: number
@@ -12,17 +14,32 @@ interface FakePR {
12
14
  }
13
15
 
14
16
  const responses = vi.hoisted(() => {
15
- return { release: [] as FakePR[], hotfix: [] as FakePR[] }
17
+ return {
18
+ release: [] as FakePR[],
19
+ hotfix: [] as FakePR[],
20
+ calls: [] as { strings: string[]; values: unknown[] }[],
21
+ }
16
22
  })
17
23
 
18
24
  // Mock zx's tagged-template `$`: the gh pr list call for `--base dev` returns
19
- // the "release" set, `--base main` returns the "hotfix" set. The command is
20
- // reconstructed from the template strings so we can branch on the base flag.
21
- vi.mock('zx', () => {
25
+ // the "release" set, `--base main` returns the "hotfix" set, and `gh pr create`
26
+ // returns a fake PR URL. Every invocation is captured (template strings +
27
+ // interpolated values) so tests can reconstruct the exact command zx would run.
28
+ // Spreading `...actual` keeps zx's real `quote` available for that reconstruction.
29
+ vi.mock('zx', async (importOriginal) => {
30
+ const actual = await importOriginal<typeof import('zx')>()
31
+
22
32
  return {
23
- $: vi.fn((strings: TemplateStringsArray) => {
33
+ ...actual,
34
+ $: vi.fn((strings: TemplateStringsArray, ...values: unknown[]) => {
24
35
  const command = strings.join('')
25
36
 
37
+ responses.calls.push({ strings: [...strings], values })
38
+
39
+ if (command.includes('gh pr create')) {
40
+ return Promise.resolve({ stdout: 'https://github.com/acme/repo/pull/123' })
41
+ }
42
+
26
43
  if (command.includes('--base main')) {
27
44
  return Promise.resolve({ stdout: JSON.stringify(responses.hotfix) })
28
45
  }
@@ -113,3 +130,69 @@ describe('getReleasePRsWithInfo (discovery + sort)', () => {
113
130
  ])
114
131
  })
115
132
  })
133
+
134
+ describe('createReleaseBranch (gh pr create title quoting)', () => {
135
+ beforeEach(() => {
136
+ responses.calls = []
137
+ })
138
+
139
+ // Rebuild the exact command zx would have run: interleave each template
140
+ // string with the zx-quoted form of its interpolated value. This reflects
141
+ // what the shell actually receives, including zx's ANSI-C `$'...'` escaping
142
+ // for values containing spaces.
143
+ const reconstruct = (call: { strings: string[]; values: unknown[] }): string => {
144
+ return call.strings
145
+ .map((part, i) => {
146
+ return i < call.values.length ? part + quote(String(call.values[i])) : part
147
+ })
148
+ .join('')
149
+ }
150
+
151
+ const findCreateCommand = (): string => {
152
+ const call = responses.calls.find((c) => {
153
+ return c.strings.join('').includes('gh pr create')
154
+ })
155
+
156
+ if (!call) throw new Error('gh pr create was not invoked')
157
+
158
+ return reconstruct(call)
159
+ }
160
+
161
+ // Run the reconstructed command through a real shell with `gh pr create`
162
+ // replaced by a function that prints back the value the shell parsed for
163
+ // `--title`. This is the behavioral proof of what gh would receive.
164
+ const shellTitleArg = (command: string): string => {
165
+ const probe = command.replace(
166
+ 'gh pr create',
167
+ 'f(){ while [ "$#" -gt 0 ]; do if [ "$1" = "--title" ]; then printf %s "$2"; return 0; fi; shift; done; }; f',
168
+ )
169
+
170
+ // Absolute path (not PATH-resolved) keeps the lint's command-injection guard happy.
171
+ return execFileSync('/bin/sh', ['-c', probe]).toString()
172
+ }
173
+
174
+ it("delivers a spaced title to gh as the literal string, with no $'...' corruption", async () => {
175
+ const id = { kind: 'version', semver: { major: 1, minor: 89, patch: 1 }, raw: '1.89.1' } as const
176
+
177
+ await createReleaseBranch({ id, jiraVersionUrl: 'https://jira.example/v1.89.1', type: 'hotfix' })
178
+
179
+ const command = findCreateCommand()
180
+
181
+ // Behavioral: the shell parses --title to exactly the intended title.
182
+ expect(shellTitleArg(command)).toBe('Hotfix v1.89.1')
183
+ // The pre-fix bug wrapped zx's `$'...'` token in literal double quotes, so
184
+ // the shell passed the literal `$'Hotfix v1.89.1'` to gh. That marker must be absent.
185
+ expect(command).not.toContain('"$\'')
186
+ })
187
+
188
+ it('delivers a named release title to gh as the literal string', async () => {
189
+ const id = { kind: 'name', name: 'checkout-redesign', raw: 'checkout-redesign' } as const
190
+
191
+ await createReleaseBranch({ id, jiraVersionUrl: 'https://jira.example/checkout-redesign', type: 'regular' })
192
+
193
+ const command = findCreateCommand()
194
+
195
+ expect(shellTitleArg(command)).toBe('Release checkout-redesign')
196
+ expect(command).not.toContain('"$\'')
197
+ })
198
+ })
@@ -178,7 +178,7 @@ export const createReleaseBranch = async (
178
178
  await $`git push origin ${branchName}`
179
179
 
180
180
  // Create PR and capture URL
181
- const prResult = await $`gh pr create --title "${prTitle}" --body ${body} --base ${baseBranch} --head ${branchName}`
181
+ const prResult = await $`gh pr create --title ${prTitle} --body ${body} --base ${baseBranch} --head ${branchName}`
182
182
 
183
183
  const prLink = prResult.stdout.trim()
184
184
 
@@ -0,0 +1,121 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { extractVersion, hasManagedBlock, removeManagedBlock, upsertManagedBlock } from '../managed-block'
4
+
5
+ const START = '<!-- ik:begin -->'
6
+ const END = '<!-- ik:end -->'
7
+
8
+ describe('hasManagedBlock', () => {
9
+ it('detects a well-formed block', () => {
10
+ expect(hasManagedBlock(`x ${START} y ${END} z`, START, END)).toBe(true)
11
+ })
12
+
13
+ it('returns false when a marker is missing', () => {
14
+ expect(hasManagedBlock(`only ${START} here`, START, END)).toBe(false)
15
+ })
16
+
17
+ it('treats reversed markers (end before start) as absent', () => {
18
+ expect(hasManagedBlock(`${END} middle ${START}`, START, END)).toBe(false)
19
+ })
20
+ })
21
+
22
+ describe('removeManagedBlock', () => {
23
+ it('removes the block and preserves surrounding text', () => {
24
+ const content = `top\n${START}\nmid\n${END}\nbot`
25
+
26
+ expect(removeManagedBlock(content, START, END)).toBe('top\nbot')
27
+ })
28
+
29
+ it('returns null when no block is present', () => {
30
+ expect(removeManagedBlock('nothing here', START, END)).toBeNull()
31
+ })
32
+
33
+ it('returns null for reversed markers (guard)', () => {
34
+ expect(removeManagedBlock(`${END}\nx\n${START}`, START, END)).toBeNull()
35
+ })
36
+ })
37
+
38
+ describe('extractVersion', () => {
39
+ const PREFIX = '<!-- ik:version '
40
+
41
+ it('reads the version token after the prefix', () => {
42
+ expect(extractVersion('<!-- ik:version 0.1.105 -->', PREFIX)).toBe('0.1.105')
43
+ })
44
+
45
+ it('returns null when the prefix is absent', () => {
46
+ expect(extractVersion('no version here', PREFIX)).toBeNull()
47
+ })
48
+ })
49
+
50
+ describe('upsertManagedBlock', () => {
51
+ it('inserts into an empty file (replace-in-place default)', () => {
52
+ const result = upsertManagedBlock({ content: '', body: 'hello', startMarker: START, endMarker: END })
53
+
54
+ expect(result).toBe(`${START}\nhello\n${END}\n`)
55
+ })
56
+
57
+ it('inserts into an empty file (append-end)', () => {
58
+ const result = upsertManagedBlock({
59
+ content: '',
60
+ body: 'hello',
61
+ startMarker: START,
62
+ endMarker: END,
63
+ placement: 'append-end',
64
+ })
65
+
66
+ expect(result).toBe(`${START}\nhello\n${END}\n`)
67
+ })
68
+
69
+ it('replaces an existing block in place, keeping surrounding content verbatim', () => {
70
+ const content = `# top heading\n\n${START}\nold body\n${END}\n\n# bottom heading\n`
71
+ const result = upsertManagedBlock({ content, body: 'new body', startMarker: START, endMarker: END })
72
+
73
+ expect(result).toBe(`# top heading\n\n${START}\nnew body\n${END}\n\n# bottom heading\n`)
74
+ })
75
+
76
+ it('append-end relocates an existing mid-file block to end-of-file', () => {
77
+ const content = `intro\n\n${START}\nold\n${END}\n\nuser tail`
78
+ const result = upsertManagedBlock({
79
+ content,
80
+ body: 'fresh',
81
+ startMarker: START,
82
+ endMarker: END,
83
+ placement: 'append-end',
84
+ })
85
+
86
+ expect(result).toBe(`intro\nuser tail\n${START}\nfresh\n${END}\n`)
87
+ })
88
+
89
+ it('append-end lands the block at end-of-file when absent', () => {
90
+ const content = 'existing user content'
91
+ const result = upsertManagedBlock({
92
+ content,
93
+ body: 'b',
94
+ startMarker: START,
95
+ endMarker: END,
96
+ placement: 'append-end',
97
+ })
98
+
99
+ expect(result.endsWith(`${START}\nb\n${END}\n`)).toBe(true)
100
+ expect(result.startsWith('existing user content')).toBe(true)
101
+ })
102
+
103
+ it('is idempotent — re-running does not nest or duplicate blocks', () => {
104
+ const once = upsertManagedBlock({ content: 'pre\n', body: 'b', startMarker: START, endMarker: END })
105
+ const twice = upsertManagedBlock({ content: once, body: 'b', startMarker: START, endMarker: END })
106
+
107
+ expect(twice).toBe(once)
108
+ expect(twice.match(new RegExp(START, 'g'))?.length).toBe(1)
109
+ })
110
+
111
+ it('preserves content above and below across an update', () => {
112
+ const first = upsertManagedBlock({ content: 'ABOVE\n', body: 'v1', startMarker: START, endMarker: END })
113
+ const withTail = `${first}BELOW\n`
114
+ const updated = upsertManagedBlock({ content: withTail, body: 'v2', startMarker: START, endMarker: END })
115
+
116
+ expect(updated).toContain('ABOVE')
117
+ expect(updated).toContain('BELOW')
118
+ expect(updated).toContain('v2')
119
+ expect(updated).not.toContain('v1')
120
+ })
121
+ })
@@ -0,0 +1,8 @@
1
+ export {
2
+ buildManagedBlock,
3
+ extractVersion,
4
+ hasManagedBlock,
5
+ removeManagedBlock,
6
+ upsertManagedBlock,
7
+ } from './managed-block'
8
+ export type { BlockPlacement, UpsertManagedBlockArgs } from './managed-block'
@@ -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
+ }