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.
- package/.eslintcache +1 -1
- package/.turbo/turbo-eslint-check.log +1 -1
- package/.turbo/turbo-prettier-check.log +1 -1
- package/.turbo/turbo-test.log +7 -7
- package/.turbo/turbo-ts-check.log +1 -1
- package/dist/cli.js +37 -34
- package/dist/cli.js.map +4 -4
- package/dist/mcp.js +28 -25
- package/dist/mcp.js.map +4 -4
- package/package.json +3 -3
- package/src/commands/doctor/doctor.ts +119 -1
- package/src/commands/env-clear/env-clear.ts +1 -1
- package/src/commands/env-list/env-list.ts +1 -1
- 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 +1 -1
- package/src/commands/gh-release-deliver/gh-release-deliver.ts +1 -1
- package/src/commands/gh-release-deploy-all/gh-release-deploy-all.ts +1 -1
- package/src/commands/gh-release-deploy-selected/gh-release-deploy-selected.ts +1 -1
- package/src/commands/gh-release-list/gh-release-list.ts +1 -1
- package/src/commands/init/init.ts +3 -3
- package/src/commands/release-create/release-create.ts +1 -1
- package/src/commands/release-create-batch/release-create-batch.ts +1 -1
- package/src/commands/version/version.ts +1 -1
- package/src/commands/worktrees-add/index.ts +2 -1
- package/src/commands/worktrees-add/worktrees-add.ts +52 -11
- package/src/commands/worktrees-list/worktrees-list.ts +1 -1
- package/src/commands/worktrees-open/index.ts +1 -0
- package/src/commands/worktrees-open/worktrees-open.ts +197 -0
- package/src/commands/worktrees-remove/worktrees-remove.ts +50 -3
- package/src/commands/worktrees-sync/worktrees-sync.ts +69 -5
- package/src/entry/cli.ts +34 -5
- package/src/integrations/clickup/.gitkeep +0 -0
- package/src/integrations/cmux/index.ts +1 -0
- package/src/integrations/cmux/list-workspace-titles.ts +42 -0
- package/src/integrations/cursor/add-folders-to-workspace.ts +84 -0
- package/src/integrations/cursor/index.ts +4 -0
- package/src/integrations/cursor/reconcile-workspace-folders.ts +90 -0
- package/src/integrations/cursor/remove-folders-from-workspace.ts +93 -0
- package/src/integrations/cursor/resolve-workspace-path.ts +13 -0
- package/src/integrations/doppler/doppler-project.ts +2 -2
- package/src/lib/__tests__/infra-kit-config.test.ts +64 -14
- package/src/lib/git-utils/git-utils.ts +27 -8
- package/src/lib/infra-kit-config/index.ts +2 -0
- package/src/lib/infra-kit-config/infra-kit-config.ts +143 -0
- package/src/mcp/tools/index.ts +2 -0
- package/tsconfig.json +3 -2
- package/tsconfig.tsbuildinfo +1 -1
- 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 = `
|
|
18
|
-
environments:
|
|
17
|
+
const VALID_YML = `environments:
|
|
19
18
|
- dev
|
|
20
19
|
- staging
|
|
21
|
-
|
|
20
|
+
envManagement:
|
|
21
|
+
provider: doppler
|
|
22
|
+
config:
|
|
23
|
+
name: my-project
|
|
22
24
|
`
|
|
23
25
|
|
|
24
|
-
const ALTERNATE_YML = `
|
|
25
|
-
environments:
|
|
26
|
+
const ALTERNATE_YML = `environments:
|
|
26
27
|
- dev
|
|
27
|
-
|
|
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.
|
|
64
|
+
expect(cfg.envManagement.config.name).toBe('my-project')
|
|
61
65
|
expect(cfg.environments).toEqual(['dev', 'staging'])
|
|
62
|
-
expect(cfg.
|
|
66
|
+
expect(cfg.taskManager).toBeUndefined()
|
|
67
|
+
expect(cfg.ide).toBeUndefined()
|
|
63
68
|
})
|
|
64
69
|
})
|
|
65
70
|
|
|
66
|
-
it('accepts
|
|
71
|
+
it('accepts ide and taskManager when provided', async () => {
|
|
67
72
|
await withTmpRepo(async (tmp) => {
|
|
68
|
-
fs.writeFileSync(
|
|
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.
|
|
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
|
-
'
|
|
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.
|
|
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.
|
|
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
|
|
39
|
-
|
|
40
|
-
if (parts.length < 3 || !parts[0]?.includes('release/v')) return null
|
|
61
|
+
const branch = parseWorktreeBranch(line)
|
|
41
62
|
|
|
42
|
-
return
|
|
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
|
|
60
|
-
|
|
61
|
-
if (parts.length < 3 || !parts[0]?.includes('feature/')) return null
|
|
80
|
+
const branch = parseWorktreeBranch(line)
|
|
62
81
|
|
|
63
|
-
return
|
|
82
|
+
return branch?.startsWith('feature/') ? branch : null
|
|
64
83
|
}
|
|
65
84
|
|
|
66
85
|
/**
|
|
@@ -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
|
+
}
|
package/src/mcp/tools/index.ts
CHANGED
|
@@ -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,
|