infra-kit 0.1.94 → 0.1.95
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 +4 -5
- package/.turbo/turbo-prettier-check.log +7 -5
- package/.turbo/turbo-test.log +82 -18
- package/.turbo/turbo-ts-check.log +10 -5
- package/dist/cli.js +36 -34
- package/dist/cli.js.map +4 -4
- package/dist/mcp.js +28 -26
- package/dist/mcp.js.map +4 -4
- package/package.json +2 -2
- 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-remove/worktrees-remove.ts +46 -1
- package/src/commands/worktrees-sync/worktrees-sync.ts +69 -5
- package/src/entry/cli.ts +25 -4
- package/src/integrations/clickup/.gitkeep +0 -0
- package/src/integrations/cursor/add-folders-to-workspace.ts +84 -0
- package/src/integrations/cursor/index.ts +3 -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/infra-kit-config/index.ts +2 -0
- package/src/lib/infra-kit-config/infra-kit-config.ts +143 -0
- package/tsconfig.json +3 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/src/lib/infra-kit-config.ts +0 -69
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
interface AddFoldersToCursorWorkspaceArgs {
|
|
5
|
+
workspacePath: string
|
|
6
|
+
folderPaths: string[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface AddFoldersToCursorWorkspaceResult {
|
|
10
|
+
added: string[]
|
|
11
|
+
skipped: string[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface WorkspaceFolderEntry {
|
|
15
|
+
path: string
|
|
16
|
+
name?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface WorkspaceFile {
|
|
20
|
+
folders?: WorkspaceFolderEntry[]
|
|
21
|
+
[key: string]: unknown
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Adds folders to a Cursor (`.code-workspace`) file's `folders` array, skipping
|
|
26
|
+
* entries that already point to the same absolute path. Folder paths are written
|
|
27
|
+
* as relative to the workspace file's directory to match Cursor's default style.
|
|
28
|
+
*/
|
|
29
|
+
export const addFoldersToCursorWorkspace = async (
|
|
30
|
+
args: AddFoldersToCursorWorkspaceArgs,
|
|
31
|
+
): Promise<AddFoldersToCursorWorkspaceResult> => {
|
|
32
|
+
const { workspacePath, folderPaths } = args
|
|
33
|
+
|
|
34
|
+
const workspaceDir = path.dirname(workspacePath)
|
|
35
|
+
|
|
36
|
+
let raw: string
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
raw = await fs.readFile(workspacePath, 'utf-8')
|
|
40
|
+
} catch (error) {
|
|
41
|
+
throw new Error(`Cursor workspace file not found at ${workspacePath}: ${(error as Error).message}`)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let parsed: WorkspaceFile
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
parsed = JSON.parse(raw) as WorkspaceFile
|
|
48
|
+
} catch (error) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Failed to parse ${workspacePath} as JSON. Comments (JSONC) are not supported. ${(error as Error).message}`,
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const existingFolders = parsed.folders ?? []
|
|
55
|
+
const existingAbsolutePaths = new Set(
|
|
56
|
+
existingFolders.map((entry) => {
|
|
57
|
+
return path.resolve(workspaceDir, entry.path)
|
|
58
|
+
}),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
const added: string[] = []
|
|
62
|
+
const skipped: string[] = []
|
|
63
|
+
|
|
64
|
+
for (const folderPath of folderPaths) {
|
|
65
|
+
const absolutePath = path.resolve(folderPath)
|
|
66
|
+
|
|
67
|
+
if (existingAbsolutePaths.has(absolutePath)) {
|
|
68
|
+
skipped.push(folderPath)
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const relativePath = path.relative(workspaceDir, absolutePath)
|
|
73
|
+
|
|
74
|
+
existingFolders.push({ path: relativePath })
|
|
75
|
+
existingAbsolutePaths.add(absolutePath)
|
|
76
|
+
added.push(folderPath)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
parsed.folders = existingFolders
|
|
80
|
+
|
|
81
|
+
await fs.writeFile(workspacePath, `${JSON.stringify(parsed, null, 2)}\n`, 'utf-8')
|
|
82
|
+
|
|
83
|
+
return { added, skipped }
|
|
84
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
interface RemoveFoldersFromCursorWorkspaceArgs {
|
|
5
|
+
workspacePath: string
|
|
6
|
+
folderPaths: string[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface RemoveFoldersFromCursorWorkspaceResult {
|
|
10
|
+
removed: string[]
|
|
11
|
+
notFound: string[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface WorkspaceFolderEntry {
|
|
15
|
+
path: string
|
|
16
|
+
name?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface WorkspaceFile {
|
|
20
|
+
folders?: WorkspaceFolderEntry[]
|
|
21
|
+
[key: string]: unknown
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Removes folders from a Cursor (`.code-workspace`) file's `folders` array. Entries
|
|
26
|
+
* are matched by resolved absolute path, so relative and absolute entries pointing
|
|
27
|
+
* at the same folder are both removed.
|
|
28
|
+
*/
|
|
29
|
+
export const removeFoldersFromCursorWorkspace = async (
|
|
30
|
+
args: RemoveFoldersFromCursorWorkspaceArgs,
|
|
31
|
+
): Promise<RemoveFoldersFromCursorWorkspaceResult> => {
|
|
32
|
+
const { workspacePath, folderPaths } = args
|
|
33
|
+
|
|
34
|
+
const workspaceDir = path.dirname(workspacePath)
|
|
35
|
+
|
|
36
|
+
let raw: string
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
raw = await fs.readFile(workspacePath, 'utf-8')
|
|
40
|
+
} catch (error) {
|
|
41
|
+
throw new Error(`Cursor workspace file not found at ${workspacePath}: ${(error as Error).message}`)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let parsed: WorkspaceFile
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
parsed = JSON.parse(raw) as WorkspaceFile
|
|
48
|
+
} catch (error) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Failed to parse ${workspacePath} as JSON. Comments (JSONC) are not supported. ${(error as Error).message}`,
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const existingFolders = parsed.folders ?? []
|
|
55
|
+
const targetAbsolutePaths = new Set(
|
|
56
|
+
folderPaths.map((folderPath) => {
|
|
57
|
+
return path.resolve(folderPath)
|
|
58
|
+
}),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
const removedAbsolutePaths = new Set<string>()
|
|
62
|
+
|
|
63
|
+
const filteredFolders = existingFolders.filter((entry) => {
|
|
64
|
+
const entryAbsolutePath = path.resolve(workspaceDir, entry.path)
|
|
65
|
+
|
|
66
|
+
if (targetAbsolutePaths.has(entryAbsolutePath)) {
|
|
67
|
+
removedAbsolutePaths.add(entryAbsolutePath)
|
|
68
|
+
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return true
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
parsed.folders = filteredFolders
|
|
76
|
+
|
|
77
|
+
await fs.writeFile(workspacePath, `${JSON.stringify(parsed, null, 2)}\n`, 'utf-8')
|
|
78
|
+
|
|
79
|
+
const removed: string[] = []
|
|
80
|
+
const notFound: string[] = []
|
|
81
|
+
|
|
82
|
+
for (const folderPath of folderPaths) {
|
|
83
|
+
const absolutePath = path.resolve(folderPath)
|
|
84
|
+
|
|
85
|
+
if (removedAbsolutePaths.has(absolutePath)) {
|
|
86
|
+
removed.push(folderPath)
|
|
87
|
+
} else {
|
|
88
|
+
notFound.push(folderPath)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { removed, notFound }
|
|
93
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolves the configured Cursor workspace path against the project root.
|
|
5
|
+
* Absolute paths are returned unchanged.
|
|
6
|
+
*/
|
|
7
|
+
export const resolveCursorWorkspacePath = (configValue: string, projectRoot: string): string => {
|
|
8
|
+
if (path.isAbsolute(configValue)) {
|
|
9
|
+
return configValue
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return path.resolve(projectRoot, configValue)
|
|
13
|
+
}
|
|
@@ -4,7 +4,7 @@ import { getInfraKitConfig } from 'src/lib/infra-kit-config'
|
|
|
4
4
|
* Resolve Doppler project name from infra-kit.yml at the project root
|
|
5
5
|
*/
|
|
6
6
|
export const getDopplerProject = async (): Promise<string> => {
|
|
7
|
-
const {
|
|
7
|
+
const { envManagement } = await getInfraKitConfig()
|
|
8
8
|
|
|
9
|
-
return
|
|
9
|
+
return envManagement.config.name
|
|
10
10
|
}
|
|
@@ -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
|
})
|
|
@@ -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/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,
|