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.
Files changed (127) hide show
  1. package/.eslintcache +1 -1
  2. package/.omc/state/agent-replay-0a58307d-2a37-4c69-851c-83a646502d62.jsonl +1 -0
  3. package/.omc/state/agent-replay-11c41aa0-51fa-49e1-a1dc-26dcd6ac26cc.jsonl +16 -0
  4. package/.omc/state/agent-replay-4cf1c186-81b2-497c-b002-d7f84e7839f3.jsonl +9 -0
  5. package/.omc/state/agent-replay-5c4ab554-64f1-42ae-83e3-21e0237e955c.jsonl +11 -0
  6. package/.omc/state/agent-replay-a60ac2ec-afbd-449f-a540-6df287392fc2.jsonl +1 -0
  7. package/.omc/state/agent-replay-be37e426-6fc8-47f4-8178-221c8494551c.jsonl +3 -0
  8. package/.omc/state/agent-replay-c967c819-3d1c-447b-ab48-56a8448ef9f8.jsonl +2 -0
  9. package/.omc/state/idle-notif-cooldown.json +3 -0
  10. package/.omc/state/last-tool-error.json +4 -4
  11. package/.omc/state/mission-state.json +53 -0
  12. package/.omc/state/sessions/0a58307d-2a37-4c69-851c-83a646502d62/pre-tool-advisory-throttle.json +18 -0
  13. package/.omc/state/sessions/0a58307d-2a37-4c69-851c-83a646502d62/subagent-tracking-state.json +7 -0
  14. package/.omc/state/sessions/11c41aa0-51fa-49e1-a1dc-26dcd6ac26cc/last-tool-error-state.json +7 -0
  15. package/.omc/state/sessions/11c41aa0-51fa-49e1-a1dc-26dcd6ac26cc/mission-state.json +117 -0
  16. package/.omc/state/sessions/11c41aa0-51fa-49e1-a1dc-26dcd6ac26cc/pre-tool-advisory-throttle.json +42 -0
  17. package/.omc/state/sessions/11c41aa0-51fa-49e1-a1dc-26dcd6ac26cc/subagent-tracking-state.json +53 -0
  18. package/.omc/state/sessions/4cf1c186-81b2-497c-b002-d7f84e7839f3/last-tool-error-state.json +7 -0
  19. package/.omc/state/sessions/4cf1c186-81b2-497c-b002-d7f84e7839f3/pre-tool-advisory-throttle.json +18 -0
  20. package/.omc/state/sessions/4cf1c186-81b2-497c-b002-d7f84e7839f3/subagent-tracking-state.json +7 -0
  21. package/.omc/state/sessions/5c4ab554-64f1-42ae-83e3-21e0237e955c/mission-state.json +117 -0
  22. package/.omc/state/sessions/5c4ab554-64f1-42ae-83e3-21e0237e955c/pre-tool-advisory-throttle.json +18 -0
  23. package/.omc/state/sessions/5c4ab554-64f1-42ae-83e3-21e0237e955c/subagent-tracking-state.json +17 -0
  24. package/.omc/state/sessions/a60ac2ec-afbd-449f-a540-6df287392fc2/pre-tool-advisory-throttle.json +18 -0
  25. package/.omc/state/sessions/a60ac2ec-afbd-449f-a540-6df287392fc2/subagent-tracking-state.json +7 -0
  26. package/.omc/state/sessions/c967c819-3d1c-447b-ab48-56a8448ef9f8/pre-tool-advisory-throttle.json +10 -0
  27. package/.omc/state/sessions/c967c819-3d1c-447b-ab48-56a8448ef9f8/subagent-tracking-state.json +7 -0
  28. package/.omc/state/subagent-tracking.json +14 -4
  29. package/.turbo/turbo-build.log +7 -0
  30. package/.turbo/turbo-check.log +14 -0
  31. package/.turbo/turbo-prettier-fix.log +2 -1
  32. package/.turbo/turbo-test.log +28 -5
  33. package/.turbo/turbo-validate.log +14 -0
  34. package/dist/cli.js +81 -74
  35. package/dist/cli.js.map +4 -4
  36. package/dist/entry/index.d.ts +2 -0
  37. package/dist/index.js +2 -0
  38. package/dist/index.js.map +7 -0
  39. package/dist/lib/package-config/package-config.d.ts +71 -0
  40. package/dist/mcp.js +43 -41
  41. package/dist/mcp.js.map +4 -4
  42. package/eslint.config.js +1 -1
  43. package/infra-kit.config.ts +5 -0
  44. package/package.json +20 -13
  45. package/scripts/build.js +32 -3
  46. package/src/.omc/state/sessions/0a58307d-2a37-4c69-851c-83a646502d62/pre-tool-advisory-throttle.json +18 -0
  47. package/src/commands/.omc/state/sessions/c967c819-3d1c-447b-ab48-56a8448ef9f8/pre-tool-advisory-throttle.json +18 -0
  48. package/src/commands/audit/__tests__/audit.test.ts +59 -0
  49. package/src/commands/audit/audit.ts +177 -0
  50. package/src/commands/audit/index.ts +1 -0
  51. package/src/commands/config/config.ts +49 -7
  52. package/src/commands/doctor/doctor.ts +3 -3
  53. package/src/commands/env-clear/env-clear.ts +1 -1
  54. package/src/commands/env-list/env-list.ts +3 -3
  55. package/src/commands/env-load/env-load.ts +1 -1
  56. package/src/commands/env-status/env-status.ts +1 -1
  57. package/src/commands/gh-merge-dev/gh-merge-dev.ts +3 -8
  58. package/src/commands/gh-release-deliver/gh-release-deliver.ts +47 -21
  59. package/src/commands/gh-release-deploy-all/gh-release-deploy-all.ts +13 -7
  60. package/src/commands/gh-release-deploy-selected/gh-release-deploy-selected.ts +12 -6
  61. package/src/commands/gh-release-list/gh-release-list.ts +19 -8
  62. package/src/commands/init/__tests__/migrate-config.test.ts +160 -0
  63. package/src/commands/init/init.ts +48 -35
  64. package/src/commands/init/migrate-config.ts +146 -0
  65. package/src/commands/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  66. package/src/commands/release-create/__tests__/release-create.test.ts +55 -0
  67. package/src/commands/release-create/release-create.ts +142 -38
  68. package/src/commands/release-desc-edit/release-desc-edit.ts +28 -8
  69. package/src/commands/version/version.ts +1 -1
  70. package/src/commands/worktrees-add/worktrees-add.ts +7 -12
  71. package/src/commands/worktrees-list/worktrees-list.ts +13 -5
  72. package/src/commands/worktrees-open/worktrees-open.ts +1 -1
  73. package/src/commands/worktrees-remove/worktrees-remove.ts +6 -10
  74. package/src/commands/worktrees-sync/worktrees-sync.ts +3 -5
  75. package/src/entry/cli.ts +49 -6
  76. package/src/entry/index.ts +5 -0
  77. package/src/integrations/cmux/open-workspace-with-layout.ts +4 -4
  78. package/src/integrations/cmux/workspace-title.ts +10 -4
  79. package/src/integrations/doppler/doppler-project.ts +1 -1
  80. package/src/integrations/gh/gh-release-prs/__tests__/gh-release-prs.test.ts +115 -0
  81. package/src/integrations/gh/gh-release-prs/gh-release-prs.ts +49 -32
  82. package/src/lib/.omc/state/sessions/a60ac2ec-afbd-449f-a540-6df287392fc2/pre-tool-advisory-throttle.json +14 -0
  83. package/src/lib/constants/index.ts +15 -0
  84. package/src/lib/git-utils/__tests__/git-utils.test.ts +49 -0
  85. package/src/lib/git-utils/git-utils.ts +3 -1
  86. package/src/lib/infra-kit-config/__tests__/infra-kit-config.test.ts +270 -0
  87. package/src/lib/infra-kit-config/index.ts +7 -1
  88. package/src/lib/infra-kit-config/infra-kit-config.ts +46 -28
  89. package/src/lib/package-config/__tests__/package-config.test.ts +95 -0
  90. package/src/lib/package-config/index.ts +3 -0
  91. package/src/lib/package-config/package-config-schema.ts +19 -0
  92. package/src/lib/package-config/package-config.ts +99 -0
  93. package/src/lib/package-validator/__tests__/package-validator.test.ts +263 -0
  94. package/src/lib/package-validator/checks/__tests__/checks.test.ts +130 -0
  95. package/src/lib/package-validator/checks/config-check.ts +30 -0
  96. package/src/lib/package-validator/checks/files-check.ts +29 -0
  97. package/src/lib/package-validator/checks/index.ts +4 -0
  98. package/src/lib/package-validator/checks/scripts-check.ts +23 -0
  99. package/src/lib/package-validator/checks/turbo-check.ts +47 -0
  100. package/src/lib/package-validator/fs-utils.ts +18 -0
  101. package/src/lib/package-validator/index.ts +3 -0
  102. package/src/lib/package-validator/loader/config-loader.ts +77 -0
  103. package/src/lib/package-validator/loader/index.ts +2 -0
  104. package/src/lib/package-validator/loader/package-discovery.ts +98 -0
  105. package/src/lib/package-validator/package-validator.ts +48 -0
  106. package/src/lib/package-validator/types.ts +15 -0
  107. package/src/lib/release-id/__tests__/release-id.test.ts +351 -0
  108. package/src/lib/release-id/__tests__/versioned-regression.test.ts +69 -0
  109. package/src/lib/release-id/index.ts +15 -0
  110. package/src/lib/release-id/release-id.ts +257 -0
  111. package/src/lib/release-utils/__tests__/release-utils.test.ts +122 -0
  112. package/src/lib/release-utils/index.ts +4 -0
  113. package/src/lib/release-utils/release-utils.ts +85 -17
  114. package/src/lib/version-utils/__tests__/load-existing-versions.test.ts +37 -0
  115. package/src/lib/version-utils/__tests__/next-version.test.ts +119 -13
  116. package/src/lib/version-utils/index.ts +3 -0
  117. package/src/lib/version-utils/load-existing-versions.ts +29 -10
  118. package/src/lib/version-utils/next-version.ts +67 -12
  119. package/src/lib/version-utils/version-utils.ts +13 -4
  120. package/src/mcp/tools/index.ts +2 -0
  121. package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  122. package/src/types.ts +1 -1
  123. package/tsconfig.tsbuildinfo +1 -1
  124. package/src/lib/__tests__/infra-kit-config.test.ts +0 -231
  125. /package/src/integrations/{clickup → linear}/.gitkeep +0 -0
  126. /package/src/lib/{__tests__ → constants/__tests__}/constants.test.ts +0 -0
  127. /package/src/lib/{constants.ts → constants/constants.ts} +0 -0
@@ -1,8 +1,9 @@
1
- import { z } from 'zod/v4'
1
+ import { z } from 'zod'
2
2
 
3
3
  import { getReleasePRsWithInfo } from 'src/integrations/gh'
4
4
  import { getCurrentWorktrees } from 'src/lib/git-utils'
5
5
  import { logger } from 'src/lib/logger'
6
+ import { displayLabel, formatJiraName, parseBranchName } from 'src/lib/release-id'
6
7
  import { detectReleaseType, formatVersionLabel, getJiraDescriptions } from 'src/lib/release-utils'
7
8
  import type { ReleaseType } from 'src/lib/release-utils'
8
9
  import { defineMcpTool, textContent } from 'src/types'
@@ -36,12 +37,19 @@ export const worktreesList = async () => {
36
37
  }),
37
38
  )
38
39
 
39
- const worktrees: WorktreeInfo[] = currentWorktrees.map((branch) => {
40
- const version = branch.replace('release/', '')
40
+ // Skip worktrees whose branch does not parse as a release id (lenient source).
41
+ const worktrees: WorktreeInfo[] = currentWorktrees.flatMap((branch) => {
42
+ const id = parseBranchName(branch)
43
+
44
+ if (!id) return []
45
+
46
+ // Human label `1.2.3` | `<name>`; Jira-descriptions map is keyed by the
47
+ // Jira version NAME (`v1.2.3` | `<name>`) — same split as formatBranchChoices.
48
+ const version = displayLabel(id)
41
49
  const type = releaseTypes.get(branch) || 'regular'
42
- const description = jiraDescriptions.get(version) || null
50
+ const description = jiraDescriptions.get(formatJiraName(id)) || null
43
51
 
44
- return { version, type, description }
52
+ return [{ version, type, description }]
45
53
  })
46
54
 
47
55
  // Log formatted output
@@ -1,4 +1,4 @@
1
- import { z } from 'zod/v4'
1
+ import { z } from 'zod'
2
2
  import { $ } from 'zx'
3
3
 
4
4
  import { buildCmuxWorkspaceTitle, listCmuxWorkspaceTitles, openCmuxWorkspaceWithLayout } from 'src/integrations/cmux'
@@ -1,7 +1,7 @@
1
1
  import checkbox from '@inquirer/checkbox'
2
2
  import confirm from '@inquirer/confirm'
3
3
  import process from 'node:process'
4
- import { z } from 'zod/v4'
4
+ import { z } from 'zod'
5
5
 
6
6
  import { removeFoldersFromCursorWorkspace, resolveCursorWorkspacePath } from 'src/integrations/cursor'
7
7
  import { getReleasePRsWithInfo } from 'src/integrations/gh'
@@ -11,7 +11,8 @@ import { OperationError } from 'src/lib/errors/operation-error'
11
11
  import { getCurrentWorktrees, getProjectRoot, getRepoName } from 'src/lib/git-utils'
12
12
  import { getInfraKitConfig } from 'src/lib/infra-kit-config'
13
13
  import { logger } from 'src/lib/logger'
14
- import { detectReleaseType, formatBranchChoices, getJiraDescriptions } from 'src/lib/release-utils'
14
+ import { formatBranchName, parseReleaseRef } from 'src/lib/release-id'
15
+ import { detectReleaseType, formatBranchChoices, getJiraDescriptions, releaseBranchLabels } from 'src/lib/release-utils'
15
16
  import type { ReleaseType } from 'src/lib/release-utils'
16
17
  import { removeWorktrees } from 'src/lib/worktrees'
17
18
  import { defineMcpTool, textContent } from 'src/types'
@@ -56,7 +57,7 @@ export const worktreesRemove = async (options: WorktreeManagementArgs) => {
56
57
  selectedReleaseBranches = currentWorktrees
57
58
  } else if (versions) {
58
59
  selectedReleaseBranches = versions.split(',').map((v) => {
59
- return `release/v${v.trim()}`
60
+ return formatBranchName(parseReleaseRef(v.trim()))
60
61
  })
61
62
  } else {
62
63
  commandEcho.setInteractive()
@@ -82,12 +83,7 @@ export const worktreesRemove = async (options: WorktreeManagementArgs) => {
82
83
  if (allSelected) {
83
84
  commandEcho.addOption('--all', true)
84
85
  } else {
85
- commandEcho.addOption(
86
- '--versions',
87
- selectedReleaseBranches.map((branch) => {
88
- return branch.replace('release/v', '')
89
- }),
90
- )
86
+ commandEcho.addOption('--versions', releaseBranchLabels(selectedReleaseBranches))
91
87
  }
92
88
 
93
89
  // Ask for confirmation
@@ -216,7 +212,7 @@ export const worktreesRemoveMcpTool = defineMcpTool({
216
212
  .string()
217
213
  .optional()
218
214
  .describe(
219
- '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.',
215
+ '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.',
220
216
  ),
221
217
  },
222
218
  outputSchema: {
@@ -1,6 +1,6 @@
1
1
  import confirm from '@inquirer/confirm'
2
2
  import process from 'node:process'
3
- import { z } from 'zod/v4'
3
+ import { z } from 'zod'
4
4
  import { $ } from 'zx'
5
5
 
6
6
  import { buildCmuxWorkspaceTitle, closeCmuxWorkspaceByTitle } from 'src/integrations/cmux'
@@ -12,12 +12,10 @@ import { OperationError } from 'src/lib/errors/operation-error'
12
12
  import { getCurrentWorktrees, getProjectRoot, getRepoName } from 'src/lib/git-utils'
13
13
  import { getInfraKitConfig } from 'src/lib/infra-kit-config'
14
14
  import { logger } from 'src/lib/logger'
15
+ import { isReleaseBranch } from 'src/lib/release-id'
15
16
  import { defineMcpTool, textContent } from 'src/types'
16
17
  import type { RequiredConfirmedOptionArg } from 'src/types'
17
18
 
18
- // Constants
19
- const RELEASE_BRANCH_PREFIX = 'release/v'
20
-
21
19
  interface WorktreeSyncArgs extends RequiredConfirmedOptionArg {}
22
20
 
23
21
  /**
@@ -108,7 +106,7 @@ const categorizeWorktrees = (args: CategorizeWorktreesArgs): { branchesToRemove:
108
106
  const { releasePRsList, currentWorktrees } = args
109
107
 
110
108
  const currentBranchNames = currentWorktrees.filter((branch) => {
111
- return branch.startsWith(RELEASE_BRANCH_PREFIX)
109
+ return isReleaseBranch(branch)
112
110
  })
113
111
 
114
112
  const branchesToRemove = currentBranchNames.filter((branch) => {
package/src/entry/cli.ts CHANGED
@@ -2,6 +2,7 @@ import select, { Separator } from '@inquirer/select'
2
2
  import { Command } from 'commander'
3
3
  import process from 'node:process'
4
4
 
5
+ import { audit } from 'src/commands/audit'
5
6
  import { configEdit, configPath } from 'src/commands/config'
6
7
  import { doctor } from 'src/commands/doctor'
7
8
  import { envClear } from 'src/commands/env-clear'
@@ -25,6 +26,7 @@ import { worktreesRemove } from 'src/commands/worktrees-remove'
25
26
  import { worktreesSync } from 'src/commands/worktrees-sync'
26
27
  import { logger } from 'src/lib/logger'
27
28
  import { parseReleaseSpec } from 'src/lib/version-utils'
29
+ import type { ReleaseInput } from 'src/lib/version-utils'
28
30
 
29
31
  const program = new Command()
30
32
 
@@ -92,10 +94,22 @@ program
92
94
  collectReleaseSpec,
93
95
  [],
94
96
  )
97
+ .option(
98
+ '-n, --name <name>',
99
+ 'Named release (repeatable). Bare kebab-case name, e.g. "checkout-redesign". Creates a regular release; set a description later via release-desc-edit. Can be combined with --release.',
100
+ collectReleaseSpec,
101
+ [],
102
+ )
95
103
  .option('-y, --yes', 'Skip confirmation prompt')
96
104
  .action(async (options) => {
97
105
  const specs = options.release as string[]
98
- const releases = specs.length > 0 ? specs.map(parseReleaseSpec) : undefined
106
+ const names = options.name as string[]
107
+ const versionInputs: ReleaseInput[] = specs.map(parseReleaseSpec)
108
+ const nameInputs: ReleaseInput[] = names.map((name) => {
109
+ return { name, type: 'regular' as const }
110
+ })
111
+ const combined = [...versionInputs, ...nameInputs]
112
+ const releases = combined.length > 0 ? combined : undefined
99
113
 
100
114
  await releaseCreate({
101
115
  releases,
@@ -106,7 +120,7 @@ program
106
120
  program
107
121
  .command('release-desc-edit')
108
122
  .description("Edit a release's description in Jira and in the matching GitHub PR body")
109
- .option('-v, --version <version>', 'Release version, e.g. 1.2.5')
123
+ .option('-v, --version <version>', 'Release version (e.g. 1.2.5) or release name (e.g. checkout-redesign)')
110
124
  .option('-d, --description <description>', 'New description (use "" to clear)')
111
125
  .option('-y, --yes', 'Skip confirmation prompt')
112
126
  .action(async (options) => {
@@ -120,7 +134,10 @@ program
120
134
  program
121
135
  .command('release-deploy-all')
122
136
  .description('Deploy any release branch to any environment')
123
- .option('-v, --version <version>', 'Specify the version to deploy, e.g. 1.2.5')
137
+ .option(
138
+ '-v, --version <version>',
139
+ 'Version (e.g. 1.2.5) or release name (e.g. checkout-redesign) to deploy; "dev" deploys from the dev branch',
140
+ )
124
141
  .option('-e, --env <env>', 'Specify the environment to deploy to, e.g. dev')
125
142
  .option('--skip-terraform', 'Skip terraform deployment step')
126
143
  .action(async (options) => {
@@ -130,7 +147,10 @@ program
130
147
  program
131
148
  .command('release-deploy-selected')
132
149
  .description('Deploy selected services from release branch to any environment')
133
- .option('-v, --version <version>', 'Specify the version to deploy, e.g. 1.2.5')
150
+ .option(
151
+ '-v, --version <version>',
152
+ 'Version (e.g. 1.2.5) or release name (e.g. checkout-redesign) to deploy; "dev" deploys from the dev branch',
153
+ )
134
154
  .option('-e, --env <env>', 'Specify the environment to deploy to, e.g. dev')
135
155
  .option('-s, --services <services...>', 'Specify services to deploy, e.g. client-be client-fe')
136
156
  .option('--skip-terraform', 'Skip terraform deployment step')
@@ -146,7 +166,7 @@ program
146
166
  program
147
167
  .command('release-deliver')
148
168
  .description('Release a new version to production')
149
- .option('-v, --version <version>', 'Specify the version to release, e.g. 1.2.5')
169
+ .option('-v, --version <version>', 'Version (e.g. 1.2.5) or release name (e.g. checkout-redesign) to deliver')
150
170
  .option('-y, --yes', 'Skip confirmation prompt')
151
171
  .action(async (options) => {
152
172
  await ghReleaseDeliver({ version: options.version, confirmedCommand: options.yes })
@@ -223,6 +243,19 @@ configCmd
223
243
  await configEdit()
224
244
  })
225
245
 
246
+ program
247
+ .command('audit')
248
+ .description('Audit against infra-kit.config.ts rules (--all for every package, --root for the monorepo root)')
249
+ .option('-a, --all', 'Audit every non-vendor workspace package')
250
+ .option('-r, --root', 'Audit the monorepo root (turbo pipeline + root commands)')
251
+ .action(async (options) => {
252
+ const result = await audit({ all: options.all, root: options.root })
253
+
254
+ if (!result.structuredContent.allPassed) {
255
+ process.exitCode = 1
256
+ }
257
+ })
258
+
226
259
  program
227
260
  .command('doctor')
228
261
  .description('Check installation and authentication status of gh and doppler CLIs')
@@ -284,7 +317,17 @@ if (process.argv.length <= 2) {
284
317
  'release-deliver',
285
318
  ]
286
319
  const worktreeCommands = ['worktrees-add', 'worktrees-list', 'worktrees-open', 'worktrees-remove', 'worktrees-sync']
287
- const envCommands = ['doctor', 'init', 'version', 'config', 'env-status', 'env-list', 'env-load', 'env-clear']
320
+ const envCommands = [
321
+ 'audit',
322
+ 'doctor',
323
+ 'init',
324
+ 'version',
325
+ 'config',
326
+ 'env-status',
327
+ 'env-list',
328
+ 'env-load',
329
+ 'env-clear',
330
+ ]
288
331
 
289
332
  const commandMap = new Map(
290
333
  program.commands.map((cmd) => {
@@ -0,0 +1,5 @@
1
+ // Public library entry for `import { defineConfig } from 'infra-kit'`. Uses
2
+ // relative imports (no `src/*` alias) so the emitted .d.ts stays portable for
3
+ // external consumers. Keep this surface minimal — only the package-config API.
4
+ export { defineConfig } from '../lib/package-config/package-config'
5
+ export type { InfraKitPackageConfig, InfraKitPackageConfigInput } from '../lib/package-config/package-config'
@@ -14,7 +14,7 @@ interface OpenCmuxWorkspaceArgs {
14
14
  export const openCmuxWorkspaceWithLayout = async (args: OpenCmuxWorkspaceArgs): Promise<void> => {
15
15
  const { cwd, title } = args
16
16
 
17
- const newWorkspaceOutput = (await $`cmux new-workspace --cwd ${cwd}`).stdout
17
+ const newWorkspaceOutput = (await $`cmux workspace create --cwd ${cwd}`).stdout
18
18
 
19
19
  const workspaceRef = parseWorkspaceRef(newWorkspaceOutput)
20
20
 
@@ -26,7 +26,7 @@ export const openCmuxWorkspaceWithLayout = async (args: OpenCmuxWorkspaceArgs):
26
26
  await $`cmux new-split down --workspace ${workspaceRef} --surface ${leftTopRef}`
27
27
 
28
28
  if (title) {
29
- await $`cmux rename-workspace --workspace ${workspaceRef} ${title}`
29
+ await $`cmux workspace rename --workspace ${workspaceRef} ${title}`
30
30
  }
31
31
  }
32
32
 
@@ -51,7 +51,7 @@ const parseFirstSurfaceRef = (output: string): string => {
51
51
 
52
52
  /**
53
53
  * Extracts the `workspace:<id>` reference from the output of
54
- * `cmux new-workspace`. The returned ref is used to target the newly
54
+ * `cmux workspace create`. The returned ref is used to target the newly
55
55
  * created workspace in follow-up `cmux` commands (splits, rename, etc.).
56
56
  *
57
57
  * @example
@@ -62,7 +62,7 @@ const parseWorkspaceRef = (output: string): string => {
62
62
  const match = output.match(/workspace:\d+/)
63
63
 
64
64
  if (!match) {
65
- throw new Error('cmux: could not locate workspace ref in new-workspace output')
65
+ throw new Error('cmux: could not locate workspace ref in workspace create output')
66
66
  }
67
67
 
68
68
  return match[0]
@@ -1,3 +1,5 @@
1
+ import { displayLabel, parseBranchName } from 'src/lib/release-id'
2
+
1
3
  interface BuildCmuxWorkspaceTitleArgs {
2
4
  repoName: string
3
5
  branch: string
@@ -5,13 +7,17 @@ interface BuildCmuxWorkspaceTitleArgs {
5
7
 
6
8
  /**
7
9
  * Builds the cmux workspace title used by `worktrees-add` and looked up by
8
- * `worktrees-remove`. The `release/` prefix is stripped so the title reads
9
- * e.g. `"hulyo-monorepo v1.48.0"` for branch `"release/v1.48.0"`.
10
+ * `worktrees-remove`. Release branches are rendered via their release-id
11
+ * display label so the title reads e.g. `"hulyo-monorepo 1.48.0"` for
12
+ * `"release/v1.48.0"` and `"hulyo-monorepo checkout-redesign"` for
13
+ * `"release/n/checkout-redesign"`. Non-release branches (cmux titles them too)
14
+ * fall back to the raw branch string.
10
15
  */
11
16
  export const buildCmuxWorkspaceTitle = (args: BuildCmuxWorkspaceTitleArgs): string => {
12
17
  const { repoName, branch } = args
13
18
 
14
- const version = branch.replace('release/', '')
19
+ const id = parseBranchName(branch)
20
+ const label = id ? displayLabel(id) : branch
15
21
 
16
- return `${repoName} ${version}`
22
+ return `${repoName} ${label}`
17
23
  }
@@ -1,7 +1,7 @@
1
1
  import { getInfraKitConfig } from 'src/lib/infra-kit-config'
2
2
 
3
3
  /**
4
- * Resolve Doppler project name from infra-kit.yml at the project root
4
+ * Resolve Doppler project name from infra-kit.json at the project root
5
5
  */
6
6
  export const getDopplerProject = async (): Promise<string> => {
7
7
  const { envManagement } = await getInfraKitConfig()
@@ -0,0 +1,115 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ import { getReleasePRs, getReleasePRsWithInfo } from '../gh-release-prs'
4
+
5
+ interface FakePR {
6
+ number: number
7
+ title: string
8
+ headRefName: string
9
+ state: string
10
+ baseRefName: string
11
+ createdAt: string
12
+ }
13
+
14
+ const responses = vi.hoisted(() => {
15
+ return { release: [] as FakePR[], hotfix: [] as FakePR[] }
16
+ })
17
+
18
+ // 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', () => {
22
+ return {
23
+ $: vi.fn((strings: TemplateStringsArray) => {
24
+ const command = strings.join('')
25
+
26
+ if (command.includes('--base main')) {
27
+ return Promise.resolve({ stdout: JSON.stringify(responses.hotfix) })
28
+ }
29
+
30
+ return Promise.resolve({ stdout: JSON.stringify(responses.release) })
31
+ }),
32
+ }
33
+ })
34
+
35
+ const pr = (overrides: Partial<FakePR> & Pick<FakePR, 'headRefName' | 'createdAt'>): FakePR => {
36
+ return {
37
+ number: 1,
38
+ title: 'Release',
39
+ state: 'OPEN',
40
+ baseRefName: 'dev',
41
+ ...overrides,
42
+ }
43
+ }
44
+
45
+ describe('getReleasePRs (discovery + sort)', () => {
46
+ beforeEach(() => {
47
+ responses.release = []
48
+ responses.hotfix = []
49
+ })
50
+
51
+ it('sorts version branches first by semver ascending (numeric, 1.9.0 < 1.10.0), then names by createdAt', async () => {
52
+ responses.release = [
53
+ pr({ headRefName: 'release/n/zeta-feature', createdAt: '2026-01-10T00:00:00Z', title: 'Release zeta-feature' }),
54
+ pr({ headRefName: 'release/v1.10.0', createdAt: '2026-01-02T00:00:00Z' }),
55
+ pr({ headRefName: 'release/n/alpha-feature', createdAt: '2026-01-05T00:00:00Z', title: 'Release alpha-feature' }),
56
+ pr({ headRefName: 'release/v1.9.0', createdAt: '2026-01-01T00:00:00Z' }),
57
+ ]
58
+
59
+ await expect(getReleasePRs()).resolves.toEqual([
60
+ 'release/v1.9.0',
61
+ 'release/v1.10.0',
62
+ 'release/n/alpha-feature',
63
+ 'release/n/zeta-feature',
64
+ ])
65
+ })
66
+
67
+ it('filters out unparseable junk branches instead of throwing or NaN-sorting', async () => {
68
+ responses.release = [
69
+ pr({ headRefName: 'release/garbage', createdAt: '2026-01-01T00:00:00Z' }),
70
+ pr({ headRefName: 'release/v1.2.3', createdAt: '2026-01-02T00:00:00Z' }),
71
+ pr({ headRefName: 'totally-not-a-release', createdAt: '2026-01-03T00:00:00Z' }),
72
+ pr({
73
+ headRefName: 'release/n/checkout-redesign',
74
+ createdAt: '2026-01-04T00:00:00Z',
75
+ title: 'Release checkout-redesign',
76
+ }),
77
+ ]
78
+
79
+ await expect(getReleasePRs()).resolves.toEqual(['release/v1.2.3', 'release/n/checkout-redesign'])
80
+ })
81
+
82
+ it('merges hotfix (base main) and release (base dev) sets', async () => {
83
+ responses.release = [pr({ headRefName: 'release/v2.0.0', createdAt: '2026-01-01T00:00:00Z' })]
84
+ responses.hotfix = [
85
+ pr({
86
+ headRefName: 'release/v1.9.9',
87
+ createdAt: '2026-01-02T00:00:00Z',
88
+ baseRefName: 'main',
89
+ title: 'Hotfix v1.9.9',
90
+ }),
91
+ ]
92
+
93
+ await expect(getReleasePRs()).resolves.toEqual(['release/v1.9.9', 'release/v2.0.0'])
94
+ })
95
+ })
96
+
97
+ describe('getReleasePRsWithInfo (discovery + sort)', () => {
98
+ beforeEach(() => {
99
+ responses.release = []
100
+ responses.hotfix = []
101
+ })
102
+
103
+ it('returns branch/title/createdAt in the locked order with junk filtered', async () => {
104
+ responses.release = [
105
+ pr({ headRefName: 'release/n/beta-feature', createdAt: '2026-02-02T00:00:00Z', title: 'Release beta-feature' }),
106
+ pr({ headRefName: 'release/garbage', createdAt: '2026-02-03T00:00:00Z', title: 'Release garbage' }),
107
+ pr({ headRefName: 'release/v3.1.0', createdAt: '2026-02-01T00:00:00Z', title: 'Release v3.1.0' }),
108
+ ]
109
+
110
+ await expect(getReleasePRsWithInfo()).resolves.toEqual([
111
+ { branch: 'release/v3.1.0', title: 'Release v3.1.0', createdAt: '2026-02-01T00:00:00Z' },
112
+ { branch: 'release/n/beta-feature', title: 'Release beta-feature', createdAt: '2026-02-02T00:00:00Z' },
113
+ ])
114
+ })
115
+ })
@@ -2,9 +2,10 @@ import process from 'node:process'
2
2
  import { $ } from 'zx'
3
3
 
4
4
  import { logger } from 'src/lib/logger'
5
+ import { compareReleaseIds, formatBranchName, formatPrTitle, parseBranchName } from 'src/lib/release-id'
6
+ import type { ReleaseId } from 'src/lib/release-id'
5
7
  import { getBaseBranch } from 'src/lib/release-utils'
6
8
  import type { ReleaseType } from 'src/lib/release-utils'
7
- import { sortVersions } from 'src/lib/version-utils'
8
9
 
9
10
  interface ReleasePR {
10
11
  headRefName: string
@@ -12,11 +13,36 @@ interface ReleasePR {
12
13
  state: string
13
14
  title: string
14
15
  baseRefName: string
16
+ createdAt: string
15
17
  }
16
18
 
17
19
  export interface ReleasePRInfo {
18
20
  branch: string
19
21
  title: string
22
+ createdAt: string
23
+ }
24
+
25
+ /**
26
+ * Sort release head refs in the locked deterministic order (versions block
27
+ * first by semver ascending, then names by PR creation date). Head refs that
28
+ * are not valid release branches (parseBranchName → null) are filtered out
29
+ * rather than throwing or NaN-sorting, so a stray junk branch can never break
30
+ * discovery. Carries each PR's createdAt for name ordering.
31
+ */
32
+ const sortReleasePRs = (prs: ReleasePR[]): ReleasePR[] => {
33
+ return prs
34
+ .map((pr) => {
35
+ return { pr, id: parseBranchName(pr.headRefName) }
36
+ })
37
+ .filter((entry): entry is { pr: ReleasePR; id: NonNullable<typeof entry.id> } => {
38
+ return entry.id !== null
39
+ })
40
+ .sort((a, b) => {
41
+ return compareReleaseIds(a.id, b.id, { a: a.pr.createdAt, b: b.pr.createdAt })
42
+ })
43
+ .map((entry) => {
44
+ return entry.pr
45
+ })
20
46
  }
21
47
 
22
48
  /**
@@ -26,10 +52,10 @@ export interface ReleasePRInfo {
26
52
  */
27
53
  const fetchAllReleasePRs = async (): Promise<ReleasePR[]> => {
28
54
  const releasePRs =
29
- await $`gh pr list --search "Release in:title" --base dev --json number,title,headRefName,state,baseRefName`
55
+ await $`gh pr list --search "Release in:title" --base dev --json number,title,headRefName,state,baseRefName,createdAt`
30
56
 
31
57
  const hotfixPRs =
32
- await $`gh pr list --search "Hotfix in:title" --base main --json number,title,headRefName,state,baseRefName`
58
+ await $`gh pr list --search "Hotfix in:title" --base main --json number,title,headRefName,state,baseRefName,createdAt`
33
59
 
34
60
  const all: ReleasePR[] = [...JSON.parse(releasePRs.stdout), ...JSON.parse(hotfixPRs.stdout)]
35
61
 
@@ -46,10 +72,12 @@ const fetchAllReleasePRs = async (): Promise<ReleasePR[]> => {
46
72
  }
47
73
 
48
74
  /**
49
- * Fetch open release PRs from GitHub with 'Release' or 'Hotfix' in the title and base 'dev'.
50
- * Returns an array of headRefName strings sorted by semver in ascending order.
75
+ * Fetch open release PRs from GitHub with 'Release' or 'Hotfix' in the title.
76
+ * Returns an array of headRefName strings in the locked deterministic order
77
+ * (version branches first by semver ascending, then named branches by PR
78
+ * creation date). Unparseable head refs are filtered out.
51
79
  *
52
- * @returns [release/v1.18.22, release/v1.18.23, release/v1.18.24] (sorted by semver)
80
+ * @returns [release/v1.18.22, release/v1.18.23, release/n/checkout-redesign]
53
81
  */
54
82
  export const getReleasePRs = async (): Promise<string[]> => {
55
83
  try {
@@ -61,11 +89,9 @@ export const getReleasePRs = async (): Promise<string[]> => {
61
89
  process.exit(1)
62
90
  }
63
91
 
64
- return sortVersions(
65
- prs.map((pr) => {
66
- return pr.headRefName
67
- }),
68
- )
92
+ return sortReleasePRs(prs).map((pr) => {
93
+ return pr.headRefName
94
+ })
69
95
  } catch (error) {
70
96
  logger.error({ error }, '❌ Error fetching release PRs')
71
97
 
@@ -75,7 +101,9 @@ export const getReleasePRs = async (): Promise<string[]> => {
75
101
 
76
102
  /**
77
103
  * Fetch open release PRs with title info (for detecting release type).
78
- * Returns ReleasePRInfo objects sorted by semver.
104
+ * Returns ReleasePRInfo objects in the locked deterministic order (version
105
+ * branches first by semver ascending, then named branches by PR creation
106
+ * date). Unparseable head refs are filtered out.
79
107
  */
80
108
  export const getReleasePRsWithInfo = async (): Promise<ReleasePRInfo[]> => {
81
109
  try {
@@ -86,21 +114,11 @@ export const getReleasePRsWithInfo = async (): Promise<ReleasePRInfo[]> => {
86
114
  process.exit(1)
87
115
  }
88
116
 
89
- const sortedBranches = sortVersions(
90
- prs.map((pr) => {
91
- return pr.headRefName
92
- }),
93
- )
94
- const prByBranch = new Map(
95
- prs.map((pr) => {
96
- return [pr.headRefName, pr]
97
- }),
98
- )
99
-
100
- return sortedBranches.map((branch) => {
117
+ return sortReleasePRs(prs).map((pr) => {
101
118
  return {
102
- branch,
103
- title: prByBranch.get(branch)!.title,
119
+ branch: pr.headRefName,
120
+ title: pr.title,
121
+ createdAt: pr.createdAt,
104
122
  }
105
123
  })
106
124
  } catch (error) {
@@ -131,7 +149,7 @@ export const updateReleasePRBody = async (args: UpdateReleasePRBodyArgs): Promis
131
149
  }
132
150
 
133
151
  interface CreateReleaseBranchArgs {
134
- version: string
152
+ id: ReleaseId
135
153
  jiraVersionUrl: string
136
154
  type: ReleaseType
137
155
  description?: string
@@ -141,11 +159,11 @@ interface CreateReleaseBranchArgs {
141
159
  export const createReleaseBranch = async (
142
160
  args: CreateReleaseBranchArgs,
143
161
  ): Promise<{ branchName: string; prUrl: string }> => {
144
- const { version, jiraVersionUrl, type, description } = args
145
- const titlePrefix = type === 'hotfix' ? 'Hotfix' : 'Release'
162
+ const { id, jiraVersionUrl, type, description } = args
163
+ const prTitle = formatPrTitle(id, type)
146
164
  const baseBranch = getBaseBranch(type)
147
165
 
148
- const branchName = `release/v${version}`
166
+ const branchName = formatBranchName(id)
149
167
 
150
168
  const body = description && description.trim() !== '' ? `${jiraVersionUrl}\n\n${description}` : `${jiraVersionUrl} \n`
151
169
 
@@ -160,8 +178,7 @@ export const createReleaseBranch = async (
160
178
  await $`git push origin ${branchName}`
161
179
 
162
180
  // Create PR and capture URL
163
- const prResult =
164
- await $`gh pr create --title "${titlePrefix} v${version}" --body ${body} --base ${baseBranch} --head ${branchName}`
181
+ const prResult = await $`gh pr create --title "${prTitle}" --body ${body} --base ${baseBranch} --head ${branchName}`
165
182
 
166
183
  const prLink = prResult.stdout.trim()
167
184
 
@@ -0,0 +1,14 @@
1
+ {
2
+ "version": 1,
3
+ "entries": {
4
+ "466399dafa2d20f60587180bad0a07358eb7f2bce724df0ff682f038ad33f5ad": {
5
+ "last_emitted_at_ms": 1780863529171,
6
+ "message": "Read multiple files in parallel when possible for faster analysis."
7
+ },
8
+ "79a93d4a2f8f50b95f852280616242fee1855dc99a3c75211917f55e72e95fae": {
9
+ "last_emitted_at_ms": 1780863517870,
10
+ "message": "Use parallel execution for independent tasks. Use run_in_background for long operations (npm install, builds, tests)."
11
+ }
12
+ },
13
+ "updated_at": "2026-06-07T20:18:49.171Z"
14
+ }
@@ -0,0 +1,15 @@
1
+ export {
2
+ atomicWriteFileSync,
3
+ ENV_CLEAR_FILE,
4
+ ENV_LOAD_FILE,
5
+ ENV_VAR_LINE_PATTERN,
6
+ getCacheRoot,
7
+ getSessionCacheDir,
8
+ INFRA_KIT_ENV_CONFIG_VAR,
9
+ INFRA_KIT_ENV_LOADED_AT_VAR,
10
+ INFRA_KIT_ENV_PROJECT_VAR,
11
+ INFRA_KIT_SESSION_VAR,
12
+ LOG_FILE_PATH,
13
+ parseVarNamesFromEnvFile,
14
+ WORKTREES_DIR_SUFFIX,
15
+ } from './constants'