infra-kit 0.1.102 → 0.1.105
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-be37e426-6fc8-47f4-8178-221c8494551c.jsonl +3 -0
- package/.omc/state/agent-replay-c967c819-3d1c-447b-ab48-56a8448ef9f8.jsonl +2 -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/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/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 +81 -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/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/doctor.ts +3 -3
- 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__/migrate-config.test.ts +160 -0
- package/src/commands/init/init.ts +48 -35
- 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 +49 -6
- 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/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,146 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import yaml from 'yaml'
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
getInfraKitConfigPaths,
|
|
9
|
+
infraKitConfigSchema,
|
|
10
|
+
infraKitOverrideConfigSchema,
|
|
11
|
+
resetInfraKitConfigCache,
|
|
12
|
+
} from 'src/lib/infra-kit-config'
|
|
13
|
+
import { logger } from 'src/lib/logger'
|
|
14
|
+
|
|
15
|
+
interface MigrateLayer {
|
|
16
|
+
label: string
|
|
17
|
+
/** Legacy YAML source path. */
|
|
18
|
+
yml: string
|
|
19
|
+
/** Target JSON path. */
|
|
20
|
+
json: string
|
|
21
|
+
/** Validates the parsed object before any write (main vs override schema). */
|
|
22
|
+
schema: z.ZodType
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Replace the user's home prefix with `~` so logged paths stay short.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* // os.homedir() === '/Users/arthur'
|
|
30
|
+
* tildify('/Users/arthur/.infra-kit/config.yml') // => '~/.infra-kit/config.yml'
|
|
31
|
+
*/
|
|
32
|
+
const tildify = (filePath: string): string => {
|
|
33
|
+
const home = os.homedir()
|
|
34
|
+
|
|
35
|
+
return filePath.startsWith(home) ? `~${filePath.slice(home.length)}` : filePath
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* `fs.access` reduced to a boolean, swallowing ENOENT.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* await fileExists('/etc/hosts') // => true
|
|
43
|
+
*/
|
|
44
|
+
const fileExists = async (filePath: string): Promise<boolean> => {
|
|
45
|
+
try {
|
|
46
|
+
await fs.access(filePath)
|
|
47
|
+
|
|
48
|
+
return true
|
|
49
|
+
} catch {
|
|
50
|
+
return false
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Swap a resolved `.json` config path back to its legacy `.yml` sibling.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* legacyYmlPath('/repo/infra-kit.json') // => '/repo/infra-kit.yml'
|
|
59
|
+
*/
|
|
60
|
+
const legacyYmlPath = (jsonPath: string): string => {
|
|
61
|
+
return jsonPath.replace(/\.json$/, '.yml')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Convert any legacy `infra-kit.yml` config layers to `infra-kit.json` as part
|
|
66
|
+
* of `infra-kit init`. Best-effort and non-fatal: each merge-chain layer
|
|
67
|
+
* (project, user-global, user-project) is migrated independently, and a
|
|
68
|
+
* conflict (both `.yml` and `.json` present) or an invalid `.yml` warns and
|
|
69
|
+
* skips that layer rather than aborting init or touching the other layers.
|
|
70
|
+
* Idempotent — already-JSON layers are left untouched.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* await migrateLegacyConfig()
|
|
74
|
+
* // INFO: ✓ Migrated infra-kit.yml → infra-kit.json
|
|
75
|
+
* // (no output when there is nothing legacy to convert)
|
|
76
|
+
*/
|
|
77
|
+
export const migrateLegacyConfig = async (): Promise<void> => {
|
|
78
|
+
let paths: Awaited<ReturnType<typeof getInfraKitConfigPaths>>
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
paths = await getInfraKitConfigPaths()
|
|
82
|
+
} catch {
|
|
83
|
+
// No resolvable project (e.g. init run outside a repo) — nothing to migrate.
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const layers: MigrateLayer[] = [
|
|
88
|
+
{ label: 'infra-kit.json', yml: legacyYmlPath(paths.main), json: paths.main, schema: infraKitConfigSchema },
|
|
89
|
+
{
|
|
90
|
+
label: '~/.infra-kit/config.json',
|
|
91
|
+
yml: legacyYmlPath(paths.userGlobal),
|
|
92
|
+
json: paths.userGlobal,
|
|
93
|
+
schema: infraKitOverrideConfigSchema,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
label: `~/.infra-kit/projects/${paths.projectName}/infra-kit.json`,
|
|
97
|
+
yml: legacyYmlPath(paths.userProject),
|
|
98
|
+
json: paths.userProject,
|
|
99
|
+
schema: infraKitOverrideConfigSchema,
|
|
100
|
+
},
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
let migrated = 0
|
|
104
|
+
|
|
105
|
+
for (const layer of layers) {
|
|
106
|
+
const [ymlExists, jsonExists] = await Promise.all([fileExists(layer.yml), fileExists(layer.json)])
|
|
107
|
+
|
|
108
|
+
if (!ymlExists) continue
|
|
109
|
+
|
|
110
|
+
if (jsonExists) {
|
|
111
|
+
logger.info(
|
|
112
|
+
`⚠ Skipped ${tildify(layer.yml)} — ${tildify(layer.json)} already exists (remove the stale .yml manually)`,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Keep per-layer migration non-fatal even for malformed YAML or I/O errors
|
|
119
|
+
// (TOCTOU after the existence probe, EACCES, read-only FS): warn and skip
|
|
120
|
+
// so one bad layer never aborts `init` or the other layers.
|
|
121
|
+
try {
|
|
122
|
+
const raw = await fs.readFile(layer.yml, 'utf-8')
|
|
123
|
+
const parsed = (yaml.parse(raw) ?? {}) as unknown
|
|
124
|
+
const result = layer.schema.safeParse(parsed)
|
|
125
|
+
|
|
126
|
+
if (!result.success) {
|
|
127
|
+
logger.info(`⚠ Skipped ${tildify(layer.yml)} — invalid config: ${z.prettifyError(result.error)}`)
|
|
128
|
+
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
await fs.mkdir(path.dirname(layer.json), { recursive: true })
|
|
133
|
+
await fs.writeFile(layer.json, `${JSON.stringify(result.data, null, 2)}\n`, 'utf-8')
|
|
134
|
+
await fs.rm(layer.yml, { force: true })
|
|
135
|
+
|
|
136
|
+
logger.info(`✓ Migrated ${tildify(layer.yml)} → ${tildify(layer.json)}`)
|
|
137
|
+
migrated++
|
|
138
|
+
} catch (err) {
|
|
139
|
+
logger.info(`⚠ Skipped ${tildify(layer.yml)} — ${(err as Error).message}`)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (migrated > 0) {
|
|
144
|
+
resetInfraKitConfigCache()
|
|
145
|
+
}
|
|
146
|
+
}
|
package/src/commands/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":"4.1.8","results":[[":audit/__tests__/audit.test.ts",{"duration":0,"failed":true}]]}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import type { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
import { releaseCreateMcpTool } from '../release-create'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The MCP `releases[]` entry schema enforces that exactly one of `version` or
|
|
8
|
+
* `name` is supplied, and transforms each entry into the internal ReleaseInput
|
|
9
|
+
* shape. We exercise the schema directly (the array element) so we do not need
|
|
10
|
+
* to run the full handler (which performs git/Jira side effects).
|
|
11
|
+
*/
|
|
12
|
+
const entrySchema = (releaseCreateMcpTool.inputSchema.releases as z.ZodArray<z.ZodTypeAny>).element
|
|
13
|
+
|
|
14
|
+
describe('release-create MCP releases[] entry schema', () => {
|
|
15
|
+
it('accepts a versioned entry and transforms it into a version ReleaseInput', () => {
|
|
16
|
+
expect(entrySchema.parse({ version: '1.2.5', type: 'hotfix' })).toEqual({
|
|
17
|
+
version: '1.2.5',
|
|
18
|
+
type: 'hotfix',
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('defaults type to regular', () => {
|
|
23
|
+
expect(entrySchema.parse({ version: '1.2.5' })).toEqual({
|
|
24
|
+
version: '1.2.5',
|
|
25
|
+
type: 'regular',
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('accepts a named entry and transforms it into a name ReleaseInput', () => {
|
|
30
|
+
expect(entrySchema.parse({ name: 'checkout-redesign', type: 'regular' })).toEqual({
|
|
31
|
+
name: 'checkout-redesign',
|
|
32
|
+
type: 'regular',
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('carries description through for named entries', () => {
|
|
37
|
+
expect(entrySchema.parse({ name: 'checkout-redesign', description: 'Q3' })).toEqual({
|
|
38
|
+
name: 'checkout-redesign',
|
|
39
|
+
type: 'regular',
|
|
40
|
+
description: 'Q3',
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('rejects an entry with both version and name (mutually exclusive)', () => {
|
|
45
|
+
expect(() => {
|
|
46
|
+
return entrySchema.parse({ version: '1.2.5', name: 'checkout-redesign' })
|
|
47
|
+
}).toThrow(/exactly one of/)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('rejects an entry with neither version nor name', () => {
|
|
51
|
+
expect(() => {
|
|
52
|
+
return entrySchema.parse({ type: 'regular' })
|
|
53
|
+
}).toThrow(/exactly one of/)
|
|
54
|
+
})
|
|
55
|
+
})
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import confirm from '@inquirer/confirm'
|
|
2
2
|
import select from '@inquirer/select'
|
|
3
3
|
import process from 'node:process'
|
|
4
|
-
import { z } from 'zod
|
|
4
|
+
import { z } from 'zod'
|
|
5
5
|
import { question } from 'zx'
|
|
6
6
|
|
|
7
7
|
import { loadJiraConfig } from 'src/integrations/jira'
|
|
8
8
|
import { commandEcho } from 'src/lib/command-echo'
|
|
9
9
|
import { OperationError } from 'src/lib/errors/operation-error'
|
|
10
10
|
import { logger } from 'src/lib/logger'
|
|
11
|
+
import { InvalidReleaseNameError, displayLabel, validateName } from 'src/lib/release-id'
|
|
11
12
|
import { createSingleRelease, prepareGitForRelease } from 'src/lib/release-utils'
|
|
12
13
|
import type { ReleaseCreationResult, ReleaseType } from 'src/lib/release-utils'
|
|
13
14
|
import {
|
|
@@ -18,12 +19,12 @@ import {
|
|
|
18
19
|
parseVersion,
|
|
19
20
|
resolveReleaseEntries,
|
|
20
21
|
} from 'src/lib/version-utils'
|
|
21
|
-
import type { ReleaseEntry, SemVer } from 'src/lib/version-utils'
|
|
22
|
+
import type { ReleaseEntry, ReleaseInput, SemVer } from 'src/lib/version-utils'
|
|
22
23
|
import { defineMcpTool, textContent } from 'src/types'
|
|
23
24
|
import type { RequiredConfirmedOptionArg } from 'src/types'
|
|
24
25
|
|
|
25
26
|
interface ReleaseCreateArgs extends RequiredConfirmedOptionArg {
|
|
26
|
-
releases?:
|
|
27
|
+
releases?: ReleaseInput[]
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const VERSION_PROMPT_HINT = '"1.2.5" or "next"'
|
|
@@ -38,7 +39,7 @@ const trySuggestNext = (known: SemVer[], type: ReleaseType): string | null => {
|
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
const resolveOrExit = (entries:
|
|
42
|
+
const resolveOrExit = (entries: ReleaseInput[], known: SemVer[]): ReleaseEntry[] => {
|
|
42
43
|
try {
|
|
43
44
|
return resolveReleaseEntries(entries, known)
|
|
44
45
|
} catch (err) {
|
|
@@ -49,20 +50,80 @@ const resolveOrExit = (entries: ReleaseEntry[], known: SemVer[]): ReleaseEntry[]
|
|
|
49
50
|
})
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
if (err instanceof InvalidReleaseNameError) {
|
|
54
|
+
throw new OperationError(err, {
|
|
55
|
+
operation: 'validate release name',
|
|
56
|
+
remediation:
|
|
57
|
+
'use a kebab-case name like "checkout-redesign" (lowercase, digits, single hyphens, not a reserved word)',
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
52
61
|
throw err
|
|
53
62
|
}
|
|
54
63
|
}
|
|
55
64
|
|
|
65
|
+
const promptForVersionInput = async (running: SemVer[], type: ReleaseType): Promise<string> => {
|
|
66
|
+
const suggestion = trySuggestNext(running, type)
|
|
67
|
+
const defaultHint = suggestion ? ` [${suggestion}]` : ''
|
|
68
|
+
const versionAnswer = (await question(` Version (e.g. ${VERSION_PROMPT_HINT})${defaultHint}: `)).trim()
|
|
69
|
+
const versionInput = versionAnswer === '' ? (suggestion ?? '') : versionAnswer
|
|
70
|
+
|
|
71
|
+
if (versionInput === '') {
|
|
72
|
+
logger.error('No version provided. Exiting...')
|
|
73
|
+
process.exit(1)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return versionInput
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const promptForNameInput = async (): Promise<string> => {
|
|
80
|
+
const name = (await question(' Name (kebab-case, e.g. "checkout-redesign"): ')).trim()
|
|
81
|
+
|
|
82
|
+
if (name === '') {
|
|
83
|
+
logger.error('No name provided. Exiting...')
|
|
84
|
+
process.exit(1)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
validateName(name)
|
|
89
|
+
} catch (err) {
|
|
90
|
+
const reason = err instanceof Error ? err.message : String(err)
|
|
91
|
+
|
|
92
|
+
logger.error(`${reason} Exiting...`)
|
|
93
|
+
process.exit(1)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return name
|
|
97
|
+
}
|
|
98
|
+
|
|
56
99
|
const promptForReleasesInteractive = async (ensureKnown: () => Promise<SemVer[]>): Promise<ReleaseEntry[]> => {
|
|
57
100
|
commandEcho.setInteractive()
|
|
58
101
|
|
|
59
|
-
|
|
60
|
-
const running: SemVer[] = [
|
|
102
|
+
let baseKnown: SemVer[] | null = null
|
|
103
|
+
const running: SemVer[] = []
|
|
104
|
+
const ensureRunning = async (): Promise<SemVer[]> => {
|
|
105
|
+
if (baseKnown === null) {
|
|
106
|
+
baseKnown = await ensureKnown()
|
|
107
|
+
running.push(...baseKnown)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return running
|
|
111
|
+
}
|
|
112
|
+
|
|
61
113
|
const entries: ReleaseEntry[] = []
|
|
62
114
|
let addAnother = true
|
|
63
115
|
|
|
64
116
|
while (addAnother) {
|
|
65
117
|
const ordinal = entries.length + 1
|
|
118
|
+
const kind = await select<'version' | 'name'>({
|
|
119
|
+
message: `Release #${ordinal} — version or name?`,
|
|
120
|
+
choices: [
|
|
121
|
+
{ name: 'version (semver / next)', value: 'version' },
|
|
122
|
+
{ name: 'name (free-form)', value: 'name' },
|
|
123
|
+
],
|
|
124
|
+
default: 'version',
|
|
125
|
+
})
|
|
126
|
+
|
|
66
127
|
const type = await select<ReleaseType>({
|
|
67
128
|
message: `Release #${ordinal} — select type:`,
|
|
68
129
|
choices: [
|
|
@@ -72,19 +133,22 @@ const promptForReleasesInteractive = async (ensureKnown: () => Promise<SemVer[]>
|
|
|
72
133
|
default: 'regular',
|
|
73
134
|
})
|
|
74
135
|
|
|
75
|
-
|
|
76
|
-
const defaultHint = suggestion ? ` [${suggestion}]` : ''
|
|
77
|
-
const versionAnswer = (await question(` Version (e.g. ${VERSION_PROMPT_HINT})${defaultHint}: `)).trim()
|
|
78
|
-
const versionInput = versionAnswer === '' ? (suggestion ?? '') : versionAnswer
|
|
136
|
+
let resolved: ReleaseEntry
|
|
79
137
|
|
|
80
|
-
if (
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
138
|
+
if (kind === 'name') {
|
|
139
|
+
const name = await promptForNameInput()
|
|
140
|
+
|
|
141
|
+
resolved = resolveOrExit([{ name, type }], [])[0] as ReleaseEntry
|
|
142
|
+
} else {
|
|
143
|
+
// Versions may need prior versions for "next"; load lazily.
|
|
144
|
+
const versionInput = await promptForVersionInput(await ensureRunning(), type)
|
|
84
145
|
|
|
85
|
-
|
|
146
|
+
resolved = resolveOrExit([{ version: versionInput, type }], running)[0] as ReleaseEntry
|
|
86
147
|
|
|
87
|
-
|
|
148
|
+
if (resolved.id.kind === 'version') {
|
|
149
|
+
running.push(parseVersion(`v${resolved.id.raw}`))
|
|
150
|
+
}
|
|
151
|
+
}
|
|
88
152
|
|
|
89
153
|
const description = (await question(' Description (optional, press Enter to skip): ')).trim()
|
|
90
154
|
|
|
@@ -97,7 +161,8 @@ const promptForReleasesInteractive = async (ensureKnown: () => Promise<SemVer[]>
|
|
|
97
161
|
}
|
|
98
162
|
|
|
99
163
|
const formatReleaseSummary = (entry: ReleaseEntry): string => {
|
|
100
|
-
const
|
|
164
|
+
const label = entry.id.kind === 'version' ? `v${entry.id.raw}` : entry.id.name
|
|
165
|
+
const parts = [label, entry.type]
|
|
101
166
|
|
|
102
167
|
if (entry.description) parts.push(entry.description)
|
|
103
168
|
|
|
@@ -106,9 +171,17 @@ const formatReleaseSummary = (entry: ReleaseEntry): string => {
|
|
|
106
171
|
|
|
107
172
|
const echoReleases = (entries: ReleaseEntry[]): void => {
|
|
108
173
|
for (const entry of entries) {
|
|
174
|
+
if (entry.id.kind === 'name') {
|
|
175
|
+
// Named releases echo the --name form; description is not part of the
|
|
176
|
+
// flag and is set later via release-desc-edit / interactively.
|
|
177
|
+
commandEcho.addOption('--name', entry.id.name)
|
|
178
|
+
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
|
|
109
182
|
const spec = entry.description
|
|
110
|
-
? `${entry.
|
|
111
|
-
: `${entry.
|
|
183
|
+
? `${entry.id.raw}:${entry.type}:${entry.description}`
|
|
184
|
+
: `${entry.id.raw}:${entry.type}`
|
|
112
185
|
|
|
113
186
|
commandEcho.addOption('--release', spec)
|
|
114
187
|
}
|
|
@@ -120,7 +193,7 @@ interface FailedRelease {
|
|
|
120
193
|
}
|
|
121
194
|
|
|
122
195
|
const collectEntries = async (
|
|
123
|
-
inputReleases:
|
|
196
|
+
inputReleases: ReleaseInput[] | undefined,
|
|
124
197
|
ensureKnown: () => Promise<SemVer[]>,
|
|
125
198
|
): Promise<ReleaseEntry[]> => {
|
|
126
199
|
if (inputReleases && inputReleases.length > 0) {
|
|
@@ -168,31 +241,33 @@ const executeOne = async (
|
|
|
168
241
|
args: ExecuteOneArgs,
|
|
169
242
|
): Promise<{ result?: ReleaseCreationResult; failure?: FailedRelease }> => {
|
|
170
243
|
const { entry, jiraConfig } = args
|
|
244
|
+
const label = displayLabel(entry.id)
|
|
245
|
+
const prTitleLabel = entry.id.kind === 'version' ? `v${entry.id.raw}` : entry.id.name
|
|
171
246
|
|
|
172
247
|
try {
|
|
173
248
|
await prepareGitForRelease(entry.type)
|
|
174
249
|
|
|
175
250
|
const result = await createSingleRelease({
|
|
176
|
-
|
|
251
|
+
id: entry.id,
|
|
177
252
|
jiraConfig,
|
|
178
253
|
description: entry.description,
|
|
179
254
|
type: entry.type,
|
|
180
255
|
})
|
|
181
256
|
|
|
182
|
-
logger.info(`✅ Successfully created release:
|
|
257
|
+
logger.info(`✅ Successfully created release: ${prTitleLabel} (${entry.type})`)
|
|
183
258
|
logger.info(`🔗 GitHub PR: ${result.prUrl}`)
|
|
184
259
|
logger.info(`🔗 Jira Version: ${result.jiraVersionUrl}\n`)
|
|
185
260
|
|
|
186
261
|
return { result }
|
|
187
262
|
} catch (error) {
|
|
188
263
|
const err = new OperationError(error, {
|
|
189
|
-
operation: `create release
|
|
190
|
-
remediation: 'verify the version is unique and the base branch is clean',
|
|
264
|
+
operation: `create release ${prTitleLabel} (${entry.type})`,
|
|
265
|
+
remediation: 'verify the version or name is unique and the base branch is clean',
|
|
191
266
|
})
|
|
192
267
|
|
|
193
268
|
logger.error(`❌ ${err.message}\n`)
|
|
194
269
|
|
|
195
|
-
return { failure: { version:
|
|
270
|
+
return { failure: { version: label, error: err.message } }
|
|
196
271
|
}
|
|
197
272
|
}
|
|
198
273
|
|
|
@@ -272,24 +347,53 @@ export const releaseCreate = async (args: ReleaseCreateArgs) => {
|
|
|
272
347
|
export const releaseCreateMcpTool = defineMcpTool({
|
|
273
348
|
name: 'release-create',
|
|
274
349
|
description:
|
|
275
|
-
'Create one or more releases in a single call. Each entry in "releases" carries its own
|
|
350
|
+
'Create one or more releases in a single call. Each entry in "releases" carries EITHER a "version" (semver or the literal token "next") OR a "name" (free-form kebab-case identifier) — exactly one is required and they are mutually exclusive. Each entry also has its own type (regular|hotfix, default regular) and optional description, so regular and hotfix releases can be mixed in the same invocation. For each release this tool switches to the appropriate base branch (dev for regular, main for hotfix), cuts the release branch (release/v<semver> for versions, release/n/<name> for names), opens a GitHub release PR, and creates the matching Jira fix version (v<semver> for versions, <name> for names). The literal token "next" auto-increments from the union of remote release branches and Jira fix versions (regular bumps minor + resets patch; hotfix bumps patch on the highest minor); multiple "next" tokens advance sequentially across mixed types. Named releases never auto-bump and "next" is version-only. Confirmation is auto-skipped for MCP calls, so the caller is responsible for gating. Continues on per-release failure and reports successes/failures.',
|
|
276
351
|
inputSchema: {
|
|
277
352
|
releases: z
|
|
278
353
|
.array(
|
|
279
|
-
z
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
354
|
+
z
|
|
355
|
+
.object({
|
|
356
|
+
version: z
|
|
357
|
+
.string()
|
|
358
|
+
.optional()
|
|
359
|
+
.describe(
|
|
360
|
+
'Version to create (e.g., "1.2.5") or the literal token "next" for auto-increment. Mutually exclusive with "name".',
|
|
361
|
+
),
|
|
362
|
+
name: z
|
|
363
|
+
.string()
|
|
364
|
+
.optional()
|
|
365
|
+
.describe(
|
|
366
|
+
'Free-form kebab-case release name (e.g., "checkout-redesign"). Mutually exclusive with "version". Named releases never auto-bump.',
|
|
367
|
+
),
|
|
368
|
+
type: z
|
|
369
|
+
.enum(['regular', 'hotfix'])
|
|
370
|
+
.optional()
|
|
371
|
+
.default('regular')
|
|
372
|
+
.describe('Release type: "regular" (branches off dev) or "hotfix" (branches off main).'),
|
|
373
|
+
description: z.string().optional().describe('Optional description for the Jira version.'),
|
|
374
|
+
})
|
|
375
|
+
.refine(
|
|
376
|
+
(entry) => {
|
|
377
|
+
return (entry.version === undefined) !== (entry.name === undefined)
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
message: 'Each release entry must have exactly one of "version" or "name" (they are mutually exclusive).',
|
|
381
|
+
},
|
|
382
|
+
)
|
|
383
|
+
.transform((entry): ReleaseInput => {
|
|
384
|
+
return entry.name !== undefined
|
|
385
|
+
? { name: entry.name, type: entry.type, ...(entry.description ? { description: entry.description } : {}) }
|
|
386
|
+
: {
|
|
387
|
+
version: entry.version as string,
|
|
388
|
+
type: entry.type,
|
|
389
|
+
...(entry.description ? { description: entry.description } : {}),
|
|
390
|
+
}
|
|
391
|
+
}),
|
|
290
392
|
)
|
|
291
393
|
.min(1)
|
|
292
|
-
.describe(
|
|
394
|
+
.describe(
|
|
395
|
+
'One or more releases to create. Each entry has exactly one of "version" or "name", plus its own type and optional description.',
|
|
396
|
+
),
|
|
293
397
|
},
|
|
294
398
|
outputSchema: {
|
|
295
399
|
createdBranches: z.array(z.string()).describe('List of created release branch names'),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import confirm from '@inquirer/confirm'
|
|
2
2
|
import select from '@inquirer/select'
|
|
3
3
|
import process from 'node:process'
|
|
4
|
-
import { z } from 'zod
|
|
4
|
+
import { z } from 'zod'
|
|
5
5
|
import { question } from 'zx'
|
|
6
6
|
|
|
7
7
|
import { getReleasePRsWithInfo, updateReleasePRBody } from 'src/integrations/gh'
|
|
@@ -10,7 +10,13 @@ import type { JiraConfig, JiraVersion } from 'src/integrations/jira'
|
|
|
10
10
|
import { commandEcho } from 'src/lib/command-echo'
|
|
11
11
|
import { OperationError } from 'src/lib/errors/operation-error'
|
|
12
12
|
import { logger } from 'src/lib/logger'
|
|
13
|
-
import {
|
|
13
|
+
import { displayLabel, formatJiraName, parseBranchName } from 'src/lib/release-id'
|
|
14
|
+
import {
|
|
15
|
+
detectReleaseType,
|
|
16
|
+
formatBranchChoices,
|
|
17
|
+
getJiraDescriptions,
|
|
18
|
+
resolveReleaseBranch,
|
|
19
|
+
} from 'src/lib/release-utils'
|
|
14
20
|
import type { ReleaseType } from 'src/lib/release-utils'
|
|
15
21
|
import { defineMcpTool, textContent } from 'src/types'
|
|
16
22
|
import type { RequiredConfirmedOptionArg } from 'src/types'
|
|
@@ -87,7 +93,7 @@ export const releaseDescEdit = async (args: ReleaseDescEditArgs) => {
|
|
|
87
93
|
let selectedBranch: string
|
|
88
94
|
|
|
89
95
|
if (versionArg) {
|
|
90
|
-
selectedBranch =
|
|
96
|
+
selectedBranch = resolveReleaseBranch(versionArg)
|
|
91
97
|
await verifyReleasePRExists(selectedBranch)
|
|
92
98
|
} else {
|
|
93
99
|
commandEcho.setInteractive()
|
|
@@ -96,11 +102,23 @@ export const releaseDescEdit = async (args: ReleaseDescEditArgs) => {
|
|
|
96
102
|
selectedBranch = picked.branch
|
|
97
103
|
}
|
|
98
104
|
|
|
99
|
-
|
|
105
|
+
// selectedBranch is always a release branch here (operator ref strictly parsed,
|
|
106
|
+
// or picked from discovery-filtered choices), so parseBranchName cannot be null.
|
|
107
|
+
const releaseId = parseBranchName(selectedBranch)
|
|
108
|
+
|
|
109
|
+
if (!releaseId) {
|
|
110
|
+
throw new OperationError(undefined, {
|
|
111
|
+
operation: `edit description for ${selectedBranch}`,
|
|
112
|
+
remediation: 'pass a version (e.g. "1.2.5") or a release name (e.g. "checkout-redesign")',
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const selectedVersion = displayLabel(releaseId)
|
|
100
117
|
|
|
101
118
|
commandEcho.addOption('--version', selectedVersion)
|
|
102
119
|
|
|
103
|
-
|
|
120
|
+
// Jira fix version is named by the Jira convention: `v1.2.3` | `<name>`.
|
|
121
|
+
const versionName = formatJiraName(releaseId)
|
|
104
122
|
const jiraVersion = await findVersionByName(versionName, jiraConfig)
|
|
105
123
|
|
|
106
124
|
if (!jiraVersion) {
|
|
@@ -190,14 +208,16 @@ export const releaseDescEdit = async (args: ReleaseDescEditArgs) => {
|
|
|
190
208
|
export const releaseDescEditMcpTool = defineMcpTool({
|
|
191
209
|
name: 'release-desc-edit',
|
|
192
210
|
description:
|
|
193
|
-
"Edit a release's description in Jira and in the matching GitHub release PR body.
|
|
211
|
+
"Edit a release's description in Jira and in the matching GitHub release PR body. Accepts a release version or a release name: targets the Jira fix version named `v<version>` (versioned) or `<name>` (named) and the open PR on branch `release/v<version>` or `release/n/<name>`. The PR body is rewritten canonically to `<jiraVersionUrl>\\n\\n<description>` — any prior manual edits to the body are overwritten. Both `version` and `description` are required for MCP calls (the picker/prompt are unreachable without a TTY). Empty `description` clears the description on both sides. Confirmation is auto-skipped for MCP, so the caller is responsible for gating.",
|
|
194
212
|
inputSchema: {
|
|
195
|
-
version: z
|
|
213
|
+
version: z
|
|
214
|
+
.string()
|
|
215
|
+
.describe('Accepts a release version (e.g. "1.2.5") OR a release name (e.g. "checkout-redesign").'),
|
|
196
216
|
description: z.string().describe('New description. Empty string clears the description.'),
|
|
197
217
|
},
|
|
198
218
|
outputSchema: {
|
|
199
219
|
version: z.string().describe('Release version'),
|
|
200
|
-
branch: z.string().describe('Release branch name (e.g. "release/v1.2.5")'),
|
|
220
|
+
branch: z.string().describe('Release branch name (e.g. "release/v1.2.5" or "release/n/checkout-redesign")'),
|
|
201
221
|
jiraVersionUrl: z.string().describe('Jira fix version URL'),
|
|
202
222
|
previousDescription: z.string().describe('The description before the update'),
|
|
203
223
|
newDescription: z.string().describe('The description after the update'),
|
|
@@ -3,7 +3,7 @@ import checkbox from '@inquirer/checkbox'
|
|
|
3
3
|
import confirm from '@inquirer/confirm'
|
|
4
4
|
import select from '@inquirer/select'
|
|
5
5
|
import process from 'node:process'
|
|
6
|
-
import { z } from 'zod
|
|
6
|
+
import { z } from 'zod'
|
|
7
7
|
import { $ } from 'zx'
|
|
8
8
|
|
|
9
9
|
import { buildCmuxWorkspaceTitle, openCmuxWorkspaceWithLayout } from 'src/integrations/cmux'
|
|
@@ -15,7 +15,8 @@ import { OperationError } from 'src/lib/errors/operation-error'
|
|
|
15
15
|
import { getCurrentWorktrees, getProjectRoot, getRepoName } from 'src/lib/git-utils'
|
|
16
16
|
import { getInfraKitConfig } from 'src/lib/infra-kit-config'
|
|
17
17
|
import { logger } from 'src/lib/logger'
|
|
18
|
-
import {
|
|
18
|
+
import { formatBranchName, isReleaseBranch, parseReleaseRef } from 'src/lib/release-id'
|
|
19
|
+
import { detectReleaseType, formatBranchChoices, getJiraDescriptions, releaseBranchLabels } from 'src/lib/release-utils'
|
|
19
20
|
import type { ReleaseType } from 'src/lib/release-utils'
|
|
20
21
|
import { defineMcpTool, textContent } from 'src/types'
|
|
21
22
|
import type { RequiredConfirmedOptionArg } from 'src/types'
|
|
@@ -23,7 +24,6 @@ import type { RequiredConfirmedOptionArg } from 'src/types'
|
|
|
23
24
|
// Constants
|
|
24
25
|
const FEATURE_DIR = 'feature'
|
|
25
26
|
const RELEASE_DIR = 'release'
|
|
26
|
-
const RELEASE_BRANCH_PREFIX = 'release/v'
|
|
27
27
|
|
|
28
28
|
export const CURSOR_MODES = ['workspace', 'windows', 'none'] as const
|
|
29
29
|
export type CursorMode = (typeof CURSOR_MODES)[number]
|
|
@@ -58,7 +58,7 @@ export const worktreesAdd = async (options: WorktreeManagementArgs) => {
|
|
|
58
58
|
|
|
59
59
|
if (versions) {
|
|
60
60
|
selectedReleaseBranches = versions.split(',').map((v) => {
|
|
61
|
-
return
|
|
61
|
+
return formatBranchName(parseReleaseRef(v.trim()))
|
|
62
62
|
})
|
|
63
63
|
} else {
|
|
64
64
|
const releasePRsInfo = await getReleasePRsWithInfo()
|
|
@@ -103,12 +103,7 @@ export const worktreesAdd = async (options: WorktreeManagementArgs) => {
|
|
|
103
103
|
if (all) {
|
|
104
104
|
commandEcho.addOption('--all', true)
|
|
105
105
|
} else {
|
|
106
|
-
commandEcho.addOption(
|
|
107
|
-
'--versions',
|
|
108
|
-
selectedReleaseBranches.map((branch) => {
|
|
109
|
-
return branch.replace('release/v', '')
|
|
110
|
-
}),
|
|
111
|
-
)
|
|
106
|
+
commandEcho.addOption('--versions', releaseBranchLabels(selectedReleaseBranches))
|
|
112
107
|
}
|
|
113
108
|
|
|
114
109
|
// Ask for confirmation
|
|
@@ -282,7 +277,7 @@ const categorizeWorktrees = (args: CategorizeWorktreesArgs): { branchesToCreate:
|
|
|
282
277
|
const { selectedReleaseBranches, currentWorktrees } = args
|
|
283
278
|
|
|
284
279
|
const currentBranchNames = currentWorktrees.filter((branch) => {
|
|
285
|
-
return branch
|
|
280
|
+
return isReleaseBranch(branch)
|
|
286
281
|
})
|
|
287
282
|
|
|
288
283
|
const branchesToCreate = selectedReleaseBranches.filter((branch) => {
|
|
@@ -357,7 +352,7 @@ export const worktreesAddMcpTool = defineMcpTool({
|
|
|
357
352
|
.string()
|
|
358
353
|
.optional()
|
|
359
354
|
.describe(
|
|
360
|
-
'Comma-separated release versions to target (e.g. "1.2.5, 1.2.6"). Either "versions" or all=true must be provided for MCP calls. Overrides "all" when set.',
|
|
355
|
+
'Comma-separated release versions or names to target (e.g. "1.2.5, 1.2.6" or "checkout-redesign, 1.2.5"). Either "versions" or all=true must be provided for MCP calls. Overrides "all" when set.',
|
|
361
356
|
),
|
|
362
357
|
cursor: z
|
|
363
358
|
.enum(CURSOR_MODES)
|