infra-kit 0.1.98 → 0.1.99
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/.turbo/turbo-eslint-check.log +5 -4
- package/.turbo/turbo-prettier-check.log +5 -4
- package/.turbo/turbo-test.log +18 -191
- package/.turbo/turbo-ts-check.log +5 -9
- package/dist/cli.js +59 -43
- package/dist/cli.js.map +4 -4
- package/dist/mcp.js +34 -31
- package/dist/mcp.js.map +4 -4
- package/package.json +1 -1
- package/src/commands/config/config.ts +1 -1
- package/src/commands/init/init.ts +17 -6
- package/src/commands/release-create/release-create.ts +210 -130
- package/src/commands/worktrees-add/worktrees-add.ts +16 -12
- package/src/entry/cli.ts +15 -28
- package/src/lib/__tests__/infra-kit-config.test.ts +50 -0
- package/src/lib/infra-kit-config/infra-kit-config.ts +7 -0
- package/src/lib/version-utils/__tests__/next-version.test.ts +128 -23
- package/src/lib/version-utils/index.ts +4 -2
- package/src/lib/version-utils/next-version.ts +64 -25
- package/src/mcp/tools/index.ts +0 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/src/commands/release-create-batch/index.ts +0 -1
- package/src/commands/release-create-batch/release-create-batch.ts +0 -222
|
@@ -37,11 +37,17 @@ const withTmpRepo = async (fn: (tmp: string) => Promise<void>): Promise<void> =>
|
|
|
37
37
|
|
|
38
38
|
vi.mocked(getProjectRoot).mockResolvedValue(tmp)
|
|
39
39
|
vi.mocked(getRepoName).mockResolvedValue(path.basename(tmp))
|
|
40
|
+
// Point os.homedir() at the tmp dir so user-scope override layers
|
|
41
|
+
// (~/.infra-kit/config.yml, ~/.infra-kit/projects/<repo>/infra-kit.yml)
|
|
42
|
+
// can't leak the developer's real config into the test.
|
|
43
|
+
const homedirSpy = vi.spyOn(os, 'homedir').mockReturnValue(tmp)
|
|
44
|
+
|
|
40
45
|
resetInfraKitConfigCache()
|
|
41
46
|
|
|
42
47
|
try {
|
|
43
48
|
await fn(tmp)
|
|
44
49
|
} finally {
|
|
50
|
+
homedirSpy.mockRestore()
|
|
45
51
|
resetInfraKitConfigCache()
|
|
46
52
|
fs.rmSync(tmp, { recursive: true, force: true })
|
|
47
53
|
}
|
|
@@ -105,6 +111,50 @@ taskManager:
|
|
|
105
111
|
})
|
|
106
112
|
})
|
|
107
113
|
|
|
114
|
+
it('accepts a worktrees prompt-defaults block', async () => {
|
|
115
|
+
await withTmpRepo(async (tmp) => {
|
|
116
|
+
fs.writeFileSync(
|
|
117
|
+
path.join(tmp, 'infra-kit.yml'),
|
|
118
|
+
`environments: [dev]
|
|
119
|
+
envManagement:
|
|
120
|
+
provider: doppler
|
|
121
|
+
config:
|
|
122
|
+
name: p
|
|
123
|
+
worktrees:
|
|
124
|
+
openInGithubDesktop: false
|
|
125
|
+
openInCmux: true
|
|
126
|
+
`,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
const cfg = await getInfraKitConfig()
|
|
130
|
+
|
|
131
|
+
expect(cfg.worktrees?.openInGithubDesktop).toBe(false)
|
|
132
|
+
expect(cfg.worktrees?.openInCmux).toBe(true)
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('lets the user-global config layer supply a worktrees block when the project omits it', async () => {
|
|
137
|
+
await withTmpRepo(async (tmp) => {
|
|
138
|
+
fs.writeFileSync(path.join(tmp, 'infra-kit.yml'), VALID_YML)
|
|
139
|
+
|
|
140
|
+
const userGlobalDir = path.join(tmp, '.infra-kit')
|
|
141
|
+
|
|
142
|
+
fs.mkdirSync(userGlobalDir, { recursive: true })
|
|
143
|
+
fs.writeFileSync(
|
|
144
|
+
path.join(userGlobalDir, 'config.yml'),
|
|
145
|
+
`worktrees:
|
|
146
|
+
openInGithubDesktop: false
|
|
147
|
+
openInCmux: true
|
|
148
|
+
`,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
const cfg = await getInfraKitConfig()
|
|
152
|
+
|
|
153
|
+
expect(cfg.worktrees?.openInGithubDesktop).toBe(false)
|
|
154
|
+
expect(cfg.worktrees?.openInCmux).toBe(true)
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
108
158
|
it('rejects ide.cursor mode=workspace without workspaceConfigPath', async () => {
|
|
109
159
|
await withTmpRepo(async (tmp) => {
|
|
110
160
|
fs.writeFileSync(
|
|
@@ -56,11 +56,18 @@ const jiraTaskManagerSchema = z.object({
|
|
|
56
56
|
|
|
57
57
|
const taskManagerSchema = z.discriminatedUnion('provider', [jiraTaskManagerSchema])
|
|
58
58
|
|
|
59
|
+
// worktrees prompt defaults
|
|
60
|
+
const worktreesConfigSchema = z.object({
|
|
61
|
+
openInGithubDesktop: z.boolean().optional(),
|
|
62
|
+
openInCmux: z.boolean().optional(),
|
|
63
|
+
})
|
|
64
|
+
|
|
59
65
|
const infraKitConfigSchema = z.object({
|
|
60
66
|
environments: z.array(z.string().min(1)).min(1),
|
|
61
67
|
envManagement: envManagementSchema,
|
|
62
68
|
ide: ideSchema.optional(),
|
|
63
69
|
taskManager: taskManagerSchema.optional(),
|
|
70
|
+
worktrees: worktreesConfigSchema.optional(),
|
|
64
71
|
})
|
|
65
72
|
|
|
66
73
|
const infraKitOverrideConfigSchema = infraKitConfigSchema.partial()
|
|
@@ -4,8 +4,8 @@ import {
|
|
|
4
4
|
NoPriorVersionsError,
|
|
5
5
|
collectKnownVersions,
|
|
6
6
|
computeNextVersion,
|
|
7
|
-
|
|
8
|
-
|
|
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('
|
|
74
|
-
|
|
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('
|
|
77
|
-
expect(
|
|
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('
|
|
81
|
-
expect(
|
|
98
|
+
it('accepts the literal "next" token', () => {
|
|
99
|
+
expect(parseReleaseSpec('next:hotfix')).toEqual({ version: 'next', type: 'hotfix' })
|
|
82
100
|
})
|
|
83
101
|
|
|
84
|
-
it('
|
|
85
|
-
expect(
|
|
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('
|
|
89
|
-
expect(
|
|
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
|
|
110
|
+
it('throws on unknown type', () => {
|
|
93
111
|
expect(() => {
|
|
94
|
-
return
|
|
95
|
-
}).toThrow(/Invalid
|
|
112
|
+
return parseReleaseSpec('1.2.5:major')
|
|
113
|
+
}).toThrow(/Invalid release type/)
|
|
96
114
|
})
|
|
97
115
|
|
|
98
|
-
it('
|
|
99
|
-
expect(
|
|
116
|
+
it('throws on empty spec', () => {
|
|
117
|
+
expect(() => {
|
|
118
|
+
return parseReleaseSpec(' ')
|
|
119
|
+
}).toThrow(/empty/)
|
|
100
120
|
})
|
|
101
121
|
})
|
|
102
122
|
|
|
103
|
-
describe('
|
|
104
|
-
|
|
105
|
-
|
|
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('
|
|
109
|
-
expect(
|
|
110
|
-
|
|
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
|
-
|
|
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
|
|
102
|
-
* into concrete
|
|
103
|
-
* max so "next
|
|
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
|
|
151
|
+
export const resolveReleaseEntries = (entries: ReleaseEntry[], known: SemVer[]): ReleaseEntry[] => {
|
|
106
152
|
const running: SemVer[] = [...known]
|
|
107
|
-
const resolved: string[] = []
|
|
108
153
|
|
|
109
|
-
|
|
110
|
-
const trimmed =
|
|
154
|
+
return entries.map((entry) => {
|
|
155
|
+
const trimmed = entry.version.trim()
|
|
111
156
|
|
|
112
|
-
if (trimmed === '')
|
|
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
|
-
|
|
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
|
-
|
|
179
|
+
return { ...entry, version: explicit }
|
|
180
|
+
})
|
|
135
181
|
}
|
|
136
182
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
}
|
package/src/mcp/tools/index.ts
CHANGED
|
@@ -10,7 +10,6 @@ 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'
|
|
14
13
|
import { versionMcpTool } from 'src/commands/version'
|
|
15
14
|
import { worktreesAddMcpTool } from 'src/commands/worktrees-add'
|
|
16
15
|
import { worktreesListMcpTool } from 'src/commands/worktrees-list'
|
|
@@ -26,7 +25,6 @@ const tools = [
|
|
|
26
25
|
envClearMcpTool,
|
|
27
26
|
ghMergeDevMcpTool,
|
|
28
27
|
releaseCreateMcpTool,
|
|
29
|
-
releaseCreateBatchMcpTool,
|
|
30
28
|
ghReleaseDeliverMcpTool,
|
|
31
29
|
ghReleaseDeployAllMcpTool,
|
|
32
30
|
ghReleaseDeploySelectedMcpTool,
|