infra-kit 0.1.94 → 0.1.97

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 (49) hide show
  1. package/.eslintcache +1 -1
  2. package/.turbo/turbo-eslint-check.log +1 -1
  3. package/.turbo/turbo-prettier-check.log +1 -1
  4. package/.turbo/turbo-test.log +7 -7
  5. package/.turbo/turbo-ts-check.log +1 -1
  6. package/dist/cli.js +37 -34
  7. package/dist/cli.js.map +4 -4
  8. package/dist/mcp.js +28 -25
  9. package/dist/mcp.js.map +4 -4
  10. package/package.json +3 -3
  11. package/src/commands/doctor/doctor.ts +119 -1
  12. package/src/commands/env-clear/env-clear.ts +1 -1
  13. package/src/commands/env-list/env-list.ts +1 -1
  14. package/src/commands/env-load/env-load.ts +1 -1
  15. package/src/commands/env-status/env-status.ts +1 -1
  16. package/src/commands/gh-merge-dev/gh-merge-dev.ts +1 -1
  17. package/src/commands/gh-release-deliver/gh-release-deliver.ts +1 -1
  18. package/src/commands/gh-release-deploy-all/gh-release-deploy-all.ts +1 -1
  19. package/src/commands/gh-release-deploy-selected/gh-release-deploy-selected.ts +1 -1
  20. package/src/commands/gh-release-list/gh-release-list.ts +1 -1
  21. package/src/commands/init/init.ts +3 -3
  22. package/src/commands/release-create/release-create.ts +1 -1
  23. package/src/commands/release-create-batch/release-create-batch.ts +1 -1
  24. package/src/commands/version/version.ts +1 -1
  25. package/src/commands/worktrees-add/index.ts +2 -1
  26. package/src/commands/worktrees-add/worktrees-add.ts +52 -11
  27. package/src/commands/worktrees-list/worktrees-list.ts +1 -1
  28. package/src/commands/worktrees-open/index.ts +1 -0
  29. package/src/commands/worktrees-open/worktrees-open.ts +197 -0
  30. package/src/commands/worktrees-remove/worktrees-remove.ts +50 -3
  31. package/src/commands/worktrees-sync/worktrees-sync.ts +69 -5
  32. package/src/entry/cli.ts +34 -5
  33. package/src/integrations/clickup/.gitkeep +0 -0
  34. package/src/integrations/cmux/index.ts +1 -0
  35. package/src/integrations/cmux/list-workspace-titles.ts +42 -0
  36. package/src/integrations/cursor/add-folders-to-workspace.ts +84 -0
  37. package/src/integrations/cursor/index.ts +4 -0
  38. package/src/integrations/cursor/reconcile-workspace-folders.ts +90 -0
  39. package/src/integrations/cursor/remove-folders-from-workspace.ts +93 -0
  40. package/src/integrations/cursor/resolve-workspace-path.ts +13 -0
  41. package/src/integrations/doppler/doppler-project.ts +2 -2
  42. package/src/lib/__tests__/infra-kit-config.test.ts +64 -14
  43. package/src/lib/git-utils/git-utils.ts +27 -8
  44. package/src/lib/infra-kit-config/index.ts +2 -0
  45. package/src/lib/infra-kit-config/infra-kit-config.ts +143 -0
  46. package/src/mcp/tools/index.ts +2 -0
  47. package/tsconfig.json +3 -2
  48. package/tsconfig.tsbuildinfo +1 -1
  49. package/src/lib/infra-kit-config.ts +0 -69
@@ -14,17 +14,21 @@ vi.mock('src/lib/git-utils', () => {
14
14
  }
15
15
  })
16
16
 
17
- const VALID_YML = `dopplerProjectName: my-project
18
- environments:
17
+ const VALID_YML = `environments:
19
18
  - dev
20
19
  - staging
21
- taskManagerProvider: linear
20
+ envManagement:
21
+ provider: doppler
22
+ config:
23
+ name: my-project
22
24
  `
23
25
 
24
- const ALTERNATE_YML = `dopplerProjectName: other-project
25
- environments:
26
+ const ALTERNATE_YML = `environments:
26
27
  - dev
27
- taskManagerProvider: false
28
+ envManagement:
29
+ provider: doppler
30
+ config:
31
+ name: other-project
28
32
  `
29
33
 
30
34
  const withTmpRepo = async (fn: (tmp: string) => Promise<void>): Promise<void> => {
@@ -57,19 +61,65 @@ describe('getInfraKitConfig', () => {
57
61
 
58
62
  const cfg = await getInfraKitConfig()
59
63
 
60
- expect(cfg.dopplerProjectName).toBe('my-project')
64
+ expect(cfg.envManagement.config.name).toBe('my-project')
61
65
  expect(cfg.environments).toEqual(['dev', 'staging'])
62
- expect(cfg.taskManagerProvider).toBe('linear')
66
+ expect(cfg.taskManager).toBeUndefined()
67
+ expect(cfg.ide).toBeUndefined()
63
68
  })
64
69
  })
65
70
 
66
- it('accepts taskManagerProvider: false as a literal boolean', async () => {
71
+ it('accepts ide and taskManager when provided', async () => {
67
72
  await withTmpRepo(async (tmp) => {
68
- fs.writeFileSync(path.join(tmp, 'infra-kit.yml'), ALTERNATE_YML)
73
+ fs.writeFileSync(
74
+ path.join(tmp, 'infra-kit.yml'),
75
+ `environments: [dev]
76
+ envManagement:
77
+ provider: doppler
78
+ config:
79
+ name: p
80
+ ide:
81
+ provider: cursor
82
+ config:
83
+ mode: workspace
84
+ workspaceConfigPath: ./ws.code-workspace
85
+ taskManager:
86
+ provider: jira
87
+ config:
88
+ baseUrl: https://example.atlassian.net
89
+ projectId: 123
90
+ `,
91
+ )
69
92
 
70
93
  const cfg = await getInfraKitConfig()
71
94
 
72
- expect(cfg.taskManagerProvider).toBe(false)
95
+ expect(cfg.ide?.provider).toBe('cursor')
96
+
97
+ if (cfg.ide?.provider === 'cursor') {
98
+ expect(cfg.ide.config.mode).toBe('workspace')
99
+ expect(cfg.ide.config.workspaceConfigPath).toBe('./ws.code-workspace')
100
+ }
101
+
102
+ expect(cfg.taskManager?.provider).toBe('jira')
103
+ })
104
+ })
105
+
106
+ it('rejects ide.cursor mode=workspace without workspaceConfigPath', async () => {
107
+ await withTmpRepo(async (tmp) => {
108
+ fs.writeFileSync(
109
+ path.join(tmp, 'infra-kit.yml'),
110
+ `environments: [dev]
111
+ envManagement:
112
+ provider: doppler
113
+ config:
114
+ name: p
115
+ ide:
116
+ provider: cursor
117
+ config:
118
+ mode: workspace
119
+ `,
120
+ )
121
+
122
+ await expect(getInfraKitConfig()).rejects.toThrow(/workspaceConfigPath/)
73
123
  })
74
124
  })
75
125
 
@@ -83,7 +133,7 @@ describe('getInfraKitConfig', () => {
83
133
  await withTmpRepo(async (tmp) => {
84
134
  fs.writeFileSync(
85
135
  path.join(tmp, 'infra-kit.yml'),
86
- 'dopplerProjectName: ""\nenvironments: []\ntaskManagerProvider: false\n',
136
+ 'environments: []\nenvManagement:\n provider: doppler\n config:\n name: ""\n',
87
137
  )
88
138
 
89
139
  await expect(getInfraKitConfig()).rejects.toThrow(/Invalid infra-kit.yml/)
@@ -98,7 +148,7 @@ describe('getInfraKitConfig', () => {
98
148
 
99
149
  const first = await getInfraKitConfig()
100
150
 
101
- expect(first.dopplerProjectName).toBe('my-project')
151
+ expect(first.envManagement.config.name).toBe('my-project')
102
152
 
103
153
  // Advance mtime past the previous stat to simulate an edit; write new content.
104
154
  const future = new Date(Date.now() + 2_000)
@@ -108,7 +158,7 @@ describe('getInfraKitConfig', () => {
108
158
 
109
159
  const second = await getInfraKitConfig()
110
160
 
111
- expect(second.dopplerProjectName).toBe('other-project')
161
+ expect(second.envManagement.config.name).toBe('other-project')
112
162
  expect(second.environments).toEqual(['dev'])
113
163
  })
114
164
  })
@@ -21,6 +21,29 @@ export const getCurrentWorktrees = async (type: 'release' | 'feature'): Promise<
21
21
  })
22
22
  }
23
23
 
24
+ /**
25
+ * Extract the branch name from a `git worktree list` output line.
26
+ *
27
+ * `git worktree list` formats each line as:
28
+ * <path> <hash> [<branch>]
29
+ *
30
+ * Reads the branch from the trailing `[branch]` token so it works for the
31
+ * main checkout too (whose path does not encode the branch name).
32
+ */
33
+ const parseWorktreeBranch = (line: string): string | null => {
34
+ const trimmed = line.trimEnd()
35
+
36
+ if (!trimmed.endsWith(']')) return null
37
+
38
+ const open = trimmed.lastIndexOf('[')
39
+
40
+ if (open === -1) return null
41
+
42
+ const branch = trimmed.slice(open + 1, -1)
43
+
44
+ return branch.length > 0 ? branch : null
45
+ }
46
+
24
47
  /**
25
48
  * Extract a release branch name from a `git worktree list` output line.
26
49
  *
@@ -35,11 +58,9 @@ export const getCurrentWorktrees = async (type: 'release' | 'feature'): Promise<
35
58
  * // => null
36
59
  */
37
60
  const releaseWorktreePredicate = (line: string): string | null => {
38
- const parts = line.split(' ').filter(Boolean)
39
-
40
- if (parts.length < 3 || !parts[0]?.includes('release/v')) return null
61
+ const branch = parseWorktreeBranch(line)
41
62
 
42
- return `release/${parts[0]?.split('/').pop() || ''}`
63
+ return branch?.startsWith('release/v') ? branch : null
43
64
  }
44
65
 
45
66
  /**
@@ -56,11 +77,9 @@ const releaseWorktreePredicate = (line: string): string | null => {
56
77
  * // => null
57
78
  */
58
79
  const featureWorktreePredicate = (line: string): string | null => {
59
- const parts = line.split(' ').filter(Boolean)
60
-
61
- if (parts.length < 3 || !parts[0]?.includes('feature/')) return null
80
+ const branch = parseWorktreeBranch(line)
62
81
 
63
- return `feature/${parts[0]?.split('/').pop() || ''}`
82
+ return branch?.startsWith('feature/') ? branch : null
64
83
  }
65
84
 
66
85
  /**
@@ -0,0 +1,2 @@
1
+ export { getInfraKitConfig, resetInfraKitConfigCache } from './infra-kit-config'
2
+ export type { InfraKitConfig } from './infra-kit-config'
@@ -0,0 +1,143 @@
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
+ import yaml from 'yaml'
4
+ import { z } from 'zod/v4'
5
+
6
+ import { getProjectRoot } from 'src/lib/git-utils'
7
+
8
+ const INFRA_KIT_CONFIG_FILE = 'infra-kit.yml'
9
+ const INFRA_KIT_LOCAL_CONFIG_FILE = 'infra-kit.local.yml'
10
+
11
+ // envManagement
12
+ const dopplerEnvManagementSchema = z.object({
13
+ provider: z.literal('doppler'),
14
+ config: z.object({
15
+ name: z.string().min(1),
16
+ }),
17
+ })
18
+
19
+ const envManagementSchema = z.discriminatedUnion('provider', [dopplerEnvManagementSchema])
20
+
21
+ // ide
22
+ const cursorIdeConfigSchema = z
23
+ .object({
24
+ mode: z.enum(['workspace', 'windows']).default('workspace'),
25
+ workspaceConfigPath: z.string().min(1).optional(),
26
+ })
27
+ .refine(
28
+ (v) => {
29
+ return v.mode !== 'workspace' || !!v.workspaceConfigPath
30
+ },
31
+ {
32
+ message: 'workspaceConfigPath is required when mode is "workspace"',
33
+ path: ['workspaceConfigPath'],
34
+ },
35
+ )
36
+
37
+ const cursorIdeSchema = z.object({
38
+ provider: z.literal('cursor'),
39
+ config: cursorIdeConfigSchema,
40
+ })
41
+
42
+ const ideSchema = z.discriminatedUnion('provider', [cursorIdeSchema])
43
+
44
+ // taskManager
45
+ const jiraTaskManagerSchema = z.object({
46
+ provider: z.literal('jira'),
47
+ config: z.object({
48
+ baseUrl: z.string().url(),
49
+ projectId: z.number().int().positive(),
50
+ }),
51
+ })
52
+
53
+ const taskManagerSchema = z.discriminatedUnion('provider', [jiraTaskManagerSchema])
54
+
55
+ const infraKitConfigSchema = z.object({
56
+ environments: z.array(z.string().min(1)).min(1),
57
+ envManagement: envManagementSchema,
58
+ ide: ideSchema.optional(),
59
+ taskManager: taskManagerSchema.optional(),
60
+ })
61
+
62
+ const infraKitLocalConfigSchema = infraKitConfigSchema.partial()
63
+
64
+ export type InfraKitConfig = z.infer<typeof infraKitConfigSchema>
65
+
66
+ interface CacheEntry {
67
+ mainMtimeMs: number
68
+ localMtimeMs: number | null
69
+ value: InfraKitConfig
70
+ }
71
+
72
+ let cached: CacheEntry | null = null
73
+
74
+ /**
75
+ * Read and validate `infra-kit.yml`, with optional `infra-kit.local.yml` overrides
76
+ * shallow-merged on top (per-developer, gitignored). Top-level keys (entire
77
+ * capability sections like `ide`, `envManagement`) replace wholesale. Results are
78
+ * cached per file mtimes so the long-running MCP server picks up edits without a
79
+ * restart.
80
+ */
81
+ export const getInfraKitConfig = async (): Promise<InfraKitConfig> => {
82
+ const projectRoot = await getProjectRoot()
83
+ const mainPath = path.join(projectRoot, INFRA_KIT_CONFIG_FILE)
84
+ const localPath = path.join(projectRoot, INFRA_KIT_LOCAL_CONFIG_FILE)
85
+
86
+ let mainStat: Awaited<ReturnType<typeof fs.stat>>
87
+
88
+ try {
89
+ mainStat = await fs.stat(mainPath)
90
+ } catch {
91
+ cached = null
92
+ throw new Error(`infra-kit.yml not found at ${mainPath}`)
93
+ }
94
+
95
+ const localStat = await statIfExists(localPath)
96
+ const mainMtimeMs = Number(mainStat.mtimeMs)
97
+ const localMtimeMs = localStat ? Number(localStat.mtimeMs) : null
98
+
99
+ if (cached && cached.mainMtimeMs === mainMtimeMs && cached.localMtimeMs === localMtimeMs) {
100
+ return cached.value
101
+ }
102
+
103
+ const mainRaw = await fs.readFile(mainPath, 'utf-8')
104
+ const mainParsed = yaml.parse(mainRaw)
105
+
106
+ let merged: unknown = mainParsed
107
+
108
+ if (localStat) {
109
+ const localRaw = await fs.readFile(localPath, 'utf-8')
110
+ const localParsedRaw = yaml.parse(localRaw) ?? {}
111
+
112
+ const localResult = infraKitLocalConfigSchema.safeParse(localParsedRaw)
113
+
114
+ if (!localResult.success) {
115
+ throw new Error(`Invalid infra-kit.local.yml at ${localPath}: ${z.prettifyError(localResult.error)}`)
116
+ }
117
+
118
+ merged = { ...(mainParsed as object), ...localResult.data }
119
+ }
120
+
121
+ const result = infraKitConfigSchema.safeParse(merged)
122
+
123
+ if (!result.success) {
124
+ throw new Error(`Invalid infra-kit.yml at ${mainPath}: ${z.prettifyError(result.error)}`)
125
+ }
126
+
127
+ cached = { mainMtimeMs, localMtimeMs, value: result.data }
128
+
129
+ return result.data
130
+ }
131
+
132
+ /** For tests — drops the in-memory cache. */
133
+ export const resetInfraKitConfigCache = (): void => {
134
+ cached = null
135
+ }
136
+
137
+ const statIfExists = async (filePath: string): Promise<Awaited<ReturnType<typeof fs.stat>> | null> => {
138
+ try {
139
+ return await fs.stat(filePath)
140
+ } catch {
141
+ return null
142
+ }
143
+ }
@@ -14,6 +14,7 @@ import { releaseCreateBatchMcpTool } from 'src/commands/release-create-batch'
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'
17
+ import { worktreesOpenMcpTool } from 'src/commands/worktrees-open'
17
18
  import { worktreesRemoveMcpTool } from 'src/commands/worktrees-remove'
18
19
  import { worktreesSyncMcpTool } from 'src/commands/worktrees-sync'
19
20
  import { createToolHandler } from 'src/lib/tool-handler'
@@ -33,6 +34,7 @@ const tools = [
33
34
  versionMcpTool,
34
35
  worktreesAddMcpTool,
35
36
  worktreesListMcpTool,
37
+ worktreesOpenMcpTool,
36
38
  worktreesRemoveMcpTool,
37
39
  worktreesSyncMcpTool,
38
40
  ]
package/tsconfig.json CHANGED
@@ -6,9 +6,11 @@
6
6
  "composite": true,
7
7
  "target": "ESNext",
8
8
  "lib": ["ESNext"],
9
- "baseUrl": ".",
10
9
  "module": "ESNext",
11
10
  "moduleResolution": "bundler",
11
+ "paths": {
12
+ "src/*": ["./src/*"]
13
+ },
12
14
  "resolveJsonModule": true,
13
15
  "types": ["node"],
14
16
  "allowJs": true,
@@ -31,7 +33,6 @@
31
33
  "noStrictGenericChecks": false,
32
34
  "suppressExcessPropertyErrors": false,
33
35
  "suppressImplicitAnyIndexErrors": false,
34
- "downlevelIteration": true,
35
36
  "noEmit": true,
36
37
  "allowSyntheticDefaultImports": true,
37
38
  "esModuleInterop": true,