infra-kit 0.1.98 → 0.1.100

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 (56) hide show
  1. package/.eslintcache +1 -1
  2. package/.omc/state/agent-replay-d367c3be-9c2a-48e7-bcea-b45861af568c.jsonl +2 -0
  3. package/.omc/state/agent-replay-f2846d8f-974c-486c-b16f-4bdaa28ca45f.jsonl +1 -0
  4. package/.omc/state/last-tool-error.json +7 -0
  5. package/.omc/state/subagent-tracking.json +7 -0
  6. package/.turbo/turbo-eslint-check.log +2 -4
  7. package/.turbo/turbo-eslint-fix.log +1 -0
  8. package/.turbo/turbo-prettier-check.log +2 -4
  9. package/.turbo/turbo-prettier-fix.log +1 -10
  10. package/.turbo/turbo-test.log +12 -191
  11. package/.turbo/turbo-ts-check.log +2 -9
  12. package/dist/cli.js +69 -44
  13. package/dist/cli.js.map +4 -4
  14. package/dist/mcp.js +45 -34
  15. package/dist/mcp.js.map +4 -4
  16. package/package.json +11 -11
  17. package/src/commands/config/config.ts +1 -1
  18. package/src/commands/doctor/doctor.ts +62 -12
  19. package/src/commands/env-clear/env-clear.ts +5 -10
  20. package/src/commands/env-list/env-list.ts +5 -10
  21. package/src/commands/env-load/env-load.ts +5 -10
  22. package/src/commands/env-status/env-status.ts +5 -10
  23. package/src/commands/gh-merge-dev/gh-merge-dev.ts +17 -18
  24. package/src/commands/gh-release-deliver/gh-release-deliver.ts +290 -89
  25. package/src/commands/gh-release-deploy-all/gh-release-deploy-all.ts +15 -14
  26. package/src/commands/gh-release-deploy-selected/gh-release-deploy-selected.ts +30 -23
  27. package/src/commands/gh-release-list/gh-release-list.ts +5 -10
  28. package/src/commands/init/init.ts +17 -6
  29. package/src/commands/release-create/release-create.ts +223 -139
  30. package/src/commands/release-desc-edit/index.ts +1 -0
  31. package/src/commands/release-desc-edit/release-desc-edit.ts +207 -0
  32. package/src/commands/version/version.ts +5 -10
  33. package/src/commands/worktrees-add/worktrees-add.ts +34 -26
  34. package/src/commands/worktrees-list/worktrees-list.ts +6 -11
  35. package/src/commands/worktrees-open/worktrees-open.ts +10 -6
  36. package/src/commands/worktrees-remove/worktrees-remove.ts +18 -14
  37. package/src/commands/worktrees-sync/worktrees-sync.ts +17 -12
  38. package/src/entry/cli.ts +24 -21
  39. package/src/integrations/gh/gh-release-prs/gh-release-prs.ts +21 -0
  40. package/src/integrations/gh/gh-release-prs/index.ts +1 -1
  41. package/src/integrations/gh/index.ts +1 -1
  42. package/src/integrations/jira/api.ts +8 -17
  43. package/src/integrations/jira/index.ts +2 -0
  44. package/src/lib/__tests__/infra-kit-config.test.ts +50 -0
  45. package/src/lib/errors/__tests__/operation-error.test.ts +62 -0
  46. package/src/lib/errors/format-zx-error.ts +54 -0
  47. package/src/lib/errors/operation-error.ts +80 -0
  48. package/src/lib/infra-kit-config/infra-kit-config.ts +7 -0
  49. package/src/lib/version-utils/__tests__/next-version.test.ts +128 -23
  50. package/src/lib/version-utils/index.ts +4 -2
  51. package/src/lib/version-utils/next-version.ts +64 -25
  52. package/src/mcp/tools/index.ts +2 -2
  53. package/src/types.ts +56 -2
  54. package/tsconfig.tsbuildinfo +1 -1
  55. package/src/commands/release-create-batch/index.ts +0 -1
  56. package/src/commands/release-create-batch/release-create-batch.ts +0 -222
@@ -4,8 +4,8 @@ import {
4
4
  NoPriorVersionsError,
5
5
  collectKnownVersions,
6
6
  computeNextVersion,
7
- resolveVersionTokens,
8
- splitVersionInput,
7
+ parseReleaseSpec,
8
+ resolveReleaseEntries,
9
9
  } from '../next-version'
10
10
 
11
11
  describe('collectKnownVersions', () => {
@@ -70,43 +70,148 @@ describe('computeNextVersion', () => {
70
70
  })
71
71
  })
72
72
 
73
- describe('resolveVersionTokens', () => {
74
- const known = collectKnownVersions({ remoteBranches: ['release/v1.63.0'] })
73
+ describe('parseReleaseSpec', () => {
74
+ it('parses bare version as regular with no description', () => {
75
+ expect(parseReleaseSpec('1.2.5')).toEqual({ version: '1.2.5', type: 'regular' })
76
+ })
77
+
78
+ it('parses version:type', () => {
79
+ expect(parseReleaseSpec('1.2.5:hotfix')).toEqual({ version: '1.2.5', type: 'hotfix' })
80
+ })
81
+
82
+ it('parses version:type:description', () => {
83
+ expect(parseReleaseSpec('1.2.5:regular:Holiday backend')).toEqual({
84
+ version: '1.2.5',
85
+ type: 'regular',
86
+ description: 'Holiday backend',
87
+ })
88
+ })
75
89
 
76
- it('resolves "next,next" sequentially for regular', () => {
77
- expect(resolveVersionTokens(['next', 'next'], 'regular', known)).toEqual(['1.64.0', '1.65.0'])
90
+ it('preserves colons inside the description', () => {
91
+ expect(parseReleaseSpec('1.2.5:regular:Fixes: A and B')).toEqual({
92
+ version: '1.2.5',
93
+ type: 'regular',
94
+ description: 'Fixes: A and B',
95
+ })
78
96
  })
79
97
 
80
- it('mixes literals and next, advancing running max', () => {
81
- expect(resolveVersionTokens(['next', '1.70.0', 'next'], 'regular', known)).toEqual(['1.64.0', '1.70.0', '1.71.0'])
98
+ it('accepts the literal "next" token', () => {
99
+ expect(parseReleaseSpec('next:hotfix')).toEqual({ version: 'next', type: 'hotfix' })
82
100
  })
83
101
 
84
- it('accepts NEXT and " next " (case + whitespace insensitive)', () => {
85
- expect(resolveVersionTokens(['NEXT', ' next '], 'regular', known)).toEqual(['1.64.0', '1.65.0'])
102
+ it('lowercases the type', () => {
103
+ expect(parseReleaseSpec('1.2.5:HOTFIX')).toEqual({ version: '1.2.5', type: 'hotfix' })
86
104
  })
87
105
 
88
- it('strips leading v on explicit versions', () => {
89
- expect(resolveVersionTokens(['v1.70.0'], 'regular', known)).toEqual(['1.70.0'])
106
+ it('drops empty description', () => {
107
+ expect(parseReleaseSpec('1.2.5:regular:')).toEqual({ version: '1.2.5', type: 'regular' })
90
108
  })
91
109
 
92
- it('throws on invalid token', () => {
110
+ it('throws on unknown type', () => {
93
111
  expect(() => {
94
- return resolveVersionTokens(['nope'], 'regular', known)
95
- }).toThrow(/Invalid version/)
112
+ return parseReleaseSpec('1.2.5:major')
113
+ }).toThrow(/Invalid release type/)
96
114
  })
97
115
 
98
- it('hotfix sequence advances patch each step', () => {
99
- expect(resolveVersionTokens(['next', 'next'], 'hotfix', known)).toEqual(['1.63.1', '1.63.2'])
116
+ it('throws on empty spec', () => {
117
+ expect(() => {
118
+ return parseReleaseSpec(' ')
119
+ }).toThrow(/empty/)
100
120
  })
101
121
  })
102
122
 
103
- describe('splitVersionInput', () => {
104
- it('splits comma-separated input and trims', () => {
105
- expect(splitVersionInput(' 1.2.3 , next, ,1.2.4 ')).toEqual(['1.2.3', 'next', '1.2.4'])
123
+ describe('resolveReleaseEntries', () => {
124
+ const known = collectKnownVersions({ remoteBranches: ['release/v1.63.0'] })
125
+
126
+ it('passes through explicit semver entries unchanged', () => {
127
+ expect(
128
+ resolveReleaseEntries(
129
+ [
130
+ { version: '1.70.0', type: 'regular' },
131
+ { version: 'v1.70.1', type: 'hotfix' },
132
+ ],
133
+ known,
134
+ ),
135
+ ).toEqual([
136
+ { version: '1.70.0', type: 'regular' },
137
+ { version: '1.70.1', type: 'hotfix' },
138
+ ])
139
+ })
140
+
141
+ it('resolves a single "next" using the entry type', () => {
142
+ expect(resolveReleaseEntries([{ version: 'next', type: 'regular' }], known)).toEqual([
143
+ { version: '1.64.0', type: 'regular' },
144
+ ])
145
+ })
146
+
147
+ it('advances sequential "next" tokens of the same type', () => {
148
+ expect(
149
+ resolveReleaseEntries(
150
+ [
151
+ { version: 'next', type: 'regular' },
152
+ { version: 'next', type: 'regular' },
153
+ ],
154
+ known,
155
+ ),
156
+ ).toEqual([
157
+ { version: '1.64.0', type: 'regular' },
158
+ { version: '1.65.0', type: 'regular' },
159
+ ])
106
160
  })
107
161
 
108
- it('returns empty array for empty input', () => {
109
- expect(splitVersionInput('')).toEqual([])
110
- expect(splitVersionInput(' ')).toEqual([])
162
+ it('advances sequential "next" tokens across mixed types', () => {
163
+ expect(
164
+ resolveReleaseEntries(
165
+ [
166
+ { version: 'next', type: 'regular' },
167
+ { version: 'next', type: 'hotfix' },
168
+ ],
169
+ known,
170
+ ),
171
+ ).toEqual([
172
+ { version: '1.64.0', type: 'regular' },
173
+ { version: '1.64.1', type: 'hotfix' },
174
+ ])
175
+ })
176
+
177
+ it('mixes literals and "next", advancing the running max', () => {
178
+ expect(
179
+ resolveReleaseEntries(
180
+ [
181
+ { version: 'next', type: 'regular' },
182
+ { version: '1.70.0', type: 'regular' },
183
+ { version: 'next', type: 'regular' },
184
+ ],
185
+ known,
186
+ ),
187
+ ).toEqual([
188
+ { version: '1.64.0', type: 'regular' },
189
+ { version: '1.70.0', type: 'regular' },
190
+ { version: '1.71.0', type: 'regular' },
191
+ ])
192
+ })
193
+
194
+ it('preserves description through resolution', () => {
195
+ expect(resolveReleaseEntries([{ version: 'next', type: 'regular', description: 'Holiday' }], known)).toEqual([
196
+ { version: '1.64.0', type: 'regular', description: 'Holiday' },
197
+ ])
198
+ })
199
+
200
+ it('accepts case-insensitive "next"', () => {
201
+ expect(resolveReleaseEntries([{ version: 'NEXT', type: 'regular' }], known)).toEqual([
202
+ { version: '1.64.0', type: 'regular' },
203
+ ])
204
+ })
205
+
206
+ it('throws on invalid version', () => {
207
+ expect(() => {
208
+ return resolveReleaseEntries([{ version: 'nope', type: 'regular' }], known)
209
+ }).toThrow(/Invalid version/)
210
+ })
211
+
212
+ it('throws NoPriorVersionsError when "next" with no known versions', () => {
213
+ expect(() => {
214
+ return resolveReleaseEntries([{ version: 'next', type: 'regular' }], [])
215
+ }).toThrow(NoPriorVersionsError)
111
216
  })
112
217
  })
@@ -3,10 +3,12 @@ export {
3
3
  collectKnownVersions,
4
4
  computeNextVersion,
5
5
  type ExistingVersionsSources,
6
+ hasNextToken,
6
7
  NEXT_TOKEN,
7
8
  NoPriorVersionsError,
8
- resolveVersionTokens,
9
+ parseReleaseSpec,
10
+ type ReleaseEntry,
11
+ resolveReleaseEntries,
9
12
  type SemVer,
10
- splitVersionInput,
11
13
  } from './next-version'
12
14
  export { parseVersion, sortVersions } from './version-utils'
@@ -97,26 +97,73 @@ const isNextToken = (token: string): boolean => {
97
97
  return token.trim().toLowerCase() === NEXT_TOKEN
98
98
  }
99
99
 
100
+ export interface ReleaseEntry {
101
+ version: string
102
+ type: ReleaseType
103
+ description?: string
104
+ }
105
+
106
+ const isReleaseType = (value: string): value is ReleaseType => {
107
+ return value === 'regular' || value === 'hotfix'
108
+ }
109
+
110
+ /**
111
+ * Parse a CLI release spec of the form `version[:type[:description]]`.
112
+ * Type defaults to "regular". Description is everything after the second
113
+ * colon, so colons inside descriptions are preserved.
114
+ */
115
+ export const parseReleaseSpec = (raw: string): ReleaseEntry => {
116
+ const spec = raw.trim()
117
+
118
+ if (spec === '') throw new Error('Release spec is empty')
119
+
120
+ const firstColon = spec.indexOf(':')
121
+
122
+ if (firstColon === -1) {
123
+ return { version: spec, type: 'regular' }
124
+ }
125
+
126
+ const version = spec.slice(0, firstColon).trim()
127
+ const rest = spec.slice(firstColon + 1)
128
+ const secondColon = rest.indexOf(':')
129
+
130
+ const typeRaw = secondColon === -1 ? rest.trim() : rest.slice(0, secondColon).trim()
131
+ const description = secondColon === -1 ? '' : rest.slice(secondColon + 1).trim()
132
+ const typeLower = typeRaw.toLowerCase()
133
+
134
+ if (!isReleaseType(typeLower)) {
135
+ throw new Error(`Invalid release type "${typeRaw}". Expected "regular" or "hotfix".`)
136
+ }
137
+
138
+ const entry: ReleaseEntry = { version, type: typeLower }
139
+
140
+ if (description !== '') entry.description = description
141
+
142
+ return entry
143
+ }
144
+
100
145
  /**
101
- * Resolve a list of input tokens (mix of "next" and explicit semver strings)
102
- * into concrete version strings. Each "next" advances based on the running
103
- * max so "next,next" produces sequential versions.
146
+ * Resolve a list of release entries (each with its own type and optional
147
+ * "next" version token) into entries with concrete versions. Each "next"
148
+ * advances based on the running max so successive "next" tokens produce
149
+ * sequential versions, even across mixed types.
104
150
  */
105
- export const resolveVersionTokens = (tokens: string[], type: ReleaseType, known: SemVer[]): string[] => {
151
+ export const resolveReleaseEntries = (entries: ReleaseEntry[], known: SemVer[]): ReleaseEntry[] => {
106
152
  const running: SemVer[] = [...known]
107
- const resolved: string[] = []
108
153
 
109
- for (const token of tokens) {
110
- const trimmed = token.trim()
154
+ return entries.map((entry) => {
155
+ const trimmed = entry.version.trim()
111
156
 
112
- if (trimmed === '') continue
157
+ if (trimmed === '') {
158
+ throw new Error('Release entry has an empty version')
159
+ }
113
160
 
114
161
  if (isNextToken(trimmed)) {
115
- const next = computeNextVersion(running, type)
162
+ const next = computeNextVersion(running, entry.type)
116
163
 
117
- resolved.push(next)
118
164
  running.push(parseVersion(`v${next}`))
119
- continue
165
+
166
+ return { ...entry, version: next }
120
167
  }
121
168
 
122
169
  const parsed = tryParse(trimmed)
@@ -127,22 +174,14 @@ export const resolveVersionTokens = (tokens: string[], type: ReleaseType, known:
127
174
 
128
175
  const explicit = `${parsed[0]}.${parsed[1]}.${parsed[2]}`
129
176
 
130
- resolved.push(explicit)
131
177
  running.push(parsed)
132
- }
133
178
 
134
- return resolved
179
+ return { ...entry, version: explicit }
180
+ })
135
181
  }
136
182
 
137
- /**
138
- * Split a raw user input into tokens, trimming and removing empties.
139
- * Accepts both whitespace-separated and comma-separated lists.
140
- */
141
- export const splitVersionInput = (input: string): string[] => {
142
- return input
143
- .split(',')
144
- .map((t) => {
145
- return t.trim()
146
- })
147
- .filter(Boolean)
183
+ export const hasNextToken = (entries: ReleaseEntry[]): boolean => {
184
+ return entries.some((e) => {
185
+ return isNextToken(e.version)
186
+ })
148
187
  }
@@ -10,7 +10,7 @@ import { ghReleaseDeployAllMcpTool } from 'src/commands/gh-release-deploy-all'
10
10
  import { ghReleaseDeploySelectedMcpTool } from 'src/commands/gh-release-deploy-selected'
11
11
  import { ghReleaseListMcpTool } from 'src/commands/gh-release-list'
12
12
  import { releaseCreateMcpTool } from 'src/commands/release-create'
13
- import { releaseCreateBatchMcpTool } from 'src/commands/release-create-batch'
13
+ import { releaseDescEditMcpTool } from 'src/commands/release-desc-edit'
14
14
  import { versionMcpTool } from 'src/commands/version'
15
15
  import { worktreesAddMcpTool } from 'src/commands/worktrees-add'
16
16
  import { worktreesListMcpTool } from 'src/commands/worktrees-list'
@@ -26,7 +26,7 @@ const tools = [
26
26
  envClearMcpTool,
27
27
  ghMergeDevMcpTool,
28
28
  releaseCreateMcpTool,
29
- releaseCreateBatchMcpTool,
29
+ releaseDescEditMcpTool,
30
30
  ghReleaseDeliverMcpTool,
31
31
  ghReleaseDeployAllMcpTool,
32
32
  ghReleaseDeploySelectedMcpTool,
package/src/types.ts CHANGED
@@ -1,12 +1,66 @@
1
- export interface ToolsExecutionResult {
1
+ import type { z } from 'zod/v4'
2
+
3
+ export interface ToolsExecutionResult<TStructured = Record<string, unknown>> {
2
4
  [x: string]: unknown
3
5
  content: {
4
6
  type: 'text'
5
7
  text: string
6
8
  }[]
7
- structuredContent?: { [x: string]: unknown }
9
+ structuredContent?: TStructured
8
10
  }
9
11
 
10
12
  export interface RequiredConfirmedOptionArg {
11
13
  confirmedCommand: boolean
12
14
  }
15
+
16
+ export interface McpTool<TIn extends z.ZodRawShape = z.ZodRawShape, TOut extends z.ZodRawShape = z.ZodRawShape> {
17
+ name: string
18
+ description: string
19
+ inputSchema: TIn
20
+ outputSchema: TOut
21
+ handler: (
22
+ params: z.infer<z.ZodObject<TIn>> & RequiredConfirmedOptionArg,
23
+ ) => Promise<ToolsExecutionResult<z.infer<z.ZodObject<TOut>>>>
24
+ }
25
+
26
+ /**
27
+ * Build the dual-channel content array shared by every MCP tool. Narrows the
28
+ * literal `type: 'text'` so handlers can use inferred return types without TS
29
+ * widening `type` to `string` — which would otherwise break assignability
30
+ * against the MCP SDK's content union.
31
+ *
32
+ * @example
33
+ * return {
34
+ * content: textContent(JSON.stringify(structuredContent, null, 2)),
35
+ * structuredContent,
36
+ * }
37
+ */
38
+ export const textContent = (text: string): ToolsExecutionResult['content'] => {
39
+ return [{ type: 'text', text }]
40
+ }
41
+
42
+ /**
43
+ * Factory that ties the handler's return type to the declared `outputSchema`
44
+ * so `structuredContent` is checked against the schema at compile time. If a
45
+ * handler accidentally drops or renames a field, TS errors at the registration
46
+ * site rather than at runtime in an MCP client.
47
+ *
48
+ * @example
49
+ * export const envLoadMcpTool = defineMcpTool({
50
+ * name: 'env-load',
51
+ * description: '...',
52
+ * inputSchema: { config: z.string() },
53
+ * outputSchema: {
54
+ * filePath: z.string(),
55
+ * variableCount: z.number(),
56
+ * project: z.string(),
57
+ * config: z.string(),
58
+ * },
59
+ * handler: envLoad,
60
+ * })
61
+ */
62
+ export const defineMcpTool = <TIn extends z.ZodRawShape, TOut extends z.ZodRawShape>(
63
+ tool: McpTool<TIn, TOut>,
64
+ ): McpTool<TIn, TOut> => {
65
+ return tool
66
+ }