infra-kit 0.1.72 → 0.1.75
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/dist/cli.js +33 -16
- package/dist/cli.js.map +4 -4
- package/dist/mcp.js +29 -19
- package/dist/mcp.js.map +4 -4
- package/package.json +5 -5
- package/src/commands/doctor/doctor.ts +105 -0
- package/src/commands/doctor/index.ts +1 -0
- package/src/commands/env-clear/env-clear.ts +91 -0
- package/src/commands/env-clear/index.ts +1 -0
- package/src/commands/env-init/env-init.ts +83 -0
- package/src/commands/env-init/index.ts +1 -0
- package/src/commands/env-list/env-list.ts +50 -0
- package/src/commands/env-list/index.ts +1 -0
- package/src/commands/env-load/env-load.ts +120 -0
- package/src/commands/env-load/index.ts +1 -0
- package/src/commands/env-status/env-status.ts +103 -0
- package/src/commands/env-status/index.ts +1 -0
- package/src/entry/cli.ts +49 -7
- package/src/entry/mcp.ts +0 -5
- package/src/integrations/doppler/doppler-cli-auth.ts +25 -0
- package/src/integrations/doppler/doppler-project.ts +23 -0
- package/src/integrations/doppler/index.ts +2 -0
- package/src/lib/constants.ts +39 -0
- package/src/lib/logger/index.ts +1 -0
- package/src/mcp/tools/index.ts +8 -0
- package/src/integrations/acli/acli-auth/acli-auth.ts +0 -25
- package/src/integrations/acli/acli-auth/index.ts +0 -1
- package/src/integrations/acli/index.ts +0 -1
- package/src/lib/load-env/index.ts +0 -1
- package/src/lib/load-env/load-env.ts +0 -27
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infra-kit",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.75",
|
|
5
5
|
"description": "infra-kit",
|
|
6
6
|
"main": "dist/cli.js",
|
|
7
7
|
"module": "dist/cli.js",
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
"fix": "pnpm run prettier-fix && pnpm run eslint-fix && pnpm run qa"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@inquirer/checkbox": "^5.1.
|
|
34
|
-
"@inquirer/confirm": "^6.0.
|
|
35
|
-
"@inquirer/select": "^5.1.
|
|
33
|
+
"@inquirer/checkbox": "^5.1.2",
|
|
34
|
+
"@inquirer/confirm": "^6.0.10",
|
|
35
|
+
"@inquirer/select": "^5.1.2",
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
37
37
|
"commander": "^14.0.3",
|
|
38
38
|
"dotenv": "^17.3.1",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@pkg/eslint-config": "workspace:*",
|
|
47
47
|
"@pkg/vitest-config": "workspace:*",
|
|
48
|
-
"esbuild": "^0.27.
|
|
48
|
+
"esbuild": "^0.27.4",
|
|
49
49
|
"typescript": "^5.9.3"
|
|
50
50
|
}
|
|
51
51
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { $ } from 'zx'
|
|
3
|
+
|
|
4
|
+
import { logger } from 'src/lib/logger'
|
|
5
|
+
import type { ToolsExecutionResult } from 'src/types'
|
|
6
|
+
|
|
7
|
+
interface CheckResult {
|
|
8
|
+
name: string
|
|
9
|
+
status: 'pass' | 'fail'
|
|
10
|
+
message: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const checkCommand = async (
|
|
14
|
+
name: string,
|
|
15
|
+
command: string[],
|
|
16
|
+
successMsg: string,
|
|
17
|
+
failMsg: string,
|
|
18
|
+
): Promise<CheckResult> => {
|
|
19
|
+
try {
|
|
20
|
+
await $`${command}`
|
|
21
|
+
|
|
22
|
+
return { name, status: 'pass', message: successMsg }
|
|
23
|
+
} catch {
|
|
24
|
+
return { name, status: 'fail', message: failMsg }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check installation and authentication status of gh and doppler CLIs
|
|
30
|
+
*/
|
|
31
|
+
export const doctor = async (): Promise<ToolsExecutionResult> => {
|
|
32
|
+
const checks: CheckResult[] = await Promise.all([
|
|
33
|
+
checkCommand(
|
|
34
|
+
'gh installed',
|
|
35
|
+
['gh', '--version'],
|
|
36
|
+
'GitHub CLI is installed',
|
|
37
|
+
'GitHub CLI is not installed. Install from: https://cli.github.com/',
|
|
38
|
+
),
|
|
39
|
+
checkCommand(
|
|
40
|
+
'gh authenticated',
|
|
41
|
+
['gh', 'auth', 'status'],
|
|
42
|
+
'GitHub CLI is authenticated',
|
|
43
|
+
'GitHub CLI is not authenticated. Run: gh auth login',
|
|
44
|
+
),
|
|
45
|
+
checkCommand(
|
|
46
|
+
'doppler installed',
|
|
47
|
+
['doppler', '--version'],
|
|
48
|
+
'Doppler CLI is installed',
|
|
49
|
+
'Doppler CLI is not installed. Install from: https://docs.doppler.com/docs/install-cli',
|
|
50
|
+
),
|
|
51
|
+
checkCommand(
|
|
52
|
+
'doppler authenticated',
|
|
53
|
+
['doppler', 'me'],
|
|
54
|
+
'Doppler CLI is authenticated',
|
|
55
|
+
'Doppler CLI is not authenticated. Run: doppler login',
|
|
56
|
+
),
|
|
57
|
+
])
|
|
58
|
+
|
|
59
|
+
logger.info('Doctor check results:\n')
|
|
60
|
+
|
|
61
|
+
for (const check of checks) {
|
|
62
|
+
const icon = check.status === 'pass' ? '[PASS]' : '[FAIL]'
|
|
63
|
+
|
|
64
|
+
logger.info(` ${icon} ${check.name}: ${check.message}`)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const structuredContent = {
|
|
68
|
+
checks: checks.map((c) => {
|
|
69
|
+
return { name: c.name, status: c.status, message: c.message }
|
|
70
|
+
}),
|
|
71
|
+
allPassed: checks.every((c) => {
|
|
72
|
+
return c.status === 'pass'
|
|
73
|
+
}),
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
content: [
|
|
78
|
+
{
|
|
79
|
+
type: 'text',
|
|
80
|
+
text: JSON.stringify(structuredContent, null, 2),
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
structuredContent,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// MCP Tool Registration
|
|
88
|
+
export const doctorMcpTool = {
|
|
89
|
+
name: 'doctor',
|
|
90
|
+
description: 'Check installation and authentication status of gh and doppler CLIs',
|
|
91
|
+
inputSchema: {},
|
|
92
|
+
outputSchema: {
|
|
93
|
+
checks: z
|
|
94
|
+
.array(
|
|
95
|
+
z.object({
|
|
96
|
+
name: z.string().describe('Name of the check'),
|
|
97
|
+
status: z.enum(['pass', 'fail']).describe('Check result'),
|
|
98
|
+
message: z.string().describe('Details about the check result'),
|
|
99
|
+
}),
|
|
100
|
+
)
|
|
101
|
+
.describe('List of all check results'),
|
|
102
|
+
allPassed: z.boolean().describe('Whether all checks passed'),
|
|
103
|
+
},
|
|
104
|
+
handler: doctor,
|
|
105
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { doctor, doctorMcpTool } from './doctor'
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import process from 'node:process'
|
|
4
|
+
import { z } from 'zod'
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
ENV_CLEAR_FILE,
|
|
8
|
+
ENV_LOAD_FILE,
|
|
9
|
+
getSessionCacheDir,
|
|
10
|
+
INFRA_KIT_ENV_CONFIG_VAR,
|
|
11
|
+
INFRA_KIT_ENV_LOADED_AT_VAR,
|
|
12
|
+
INFRA_KIT_ENV_PROJECT_VAR,
|
|
13
|
+
parseVarNamesFromEnvFile,
|
|
14
|
+
} from 'src/lib/constants'
|
|
15
|
+
import { logger } from 'src/lib/logger'
|
|
16
|
+
import type { ToolsExecutionResult } from 'src/types'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Clear previously loaded environment variables. Usage: env-clear (after running eval "$(infra-kit env-init)" in shell)
|
|
20
|
+
*/
|
|
21
|
+
export const envClear = async (): Promise<ToolsExecutionResult> => {
|
|
22
|
+
const cacheDir = getSessionCacheDir()
|
|
23
|
+
const envLoadPath = path.join(cacheDir, ENV_LOAD_FILE)
|
|
24
|
+
|
|
25
|
+
if (!fs.existsSync(envLoadPath)) {
|
|
26
|
+
logger.error('No loaded environment found. Run `env-load` first.')
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
content: [
|
|
30
|
+
{
|
|
31
|
+
type: 'text',
|
|
32
|
+
text: 'No loaded environment found. Run `env-load` first.',
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const varNames = parseVarNamesFromEnvFile(envLoadPath)
|
|
39
|
+
|
|
40
|
+
// Build unset script
|
|
41
|
+
const unsetLines = [
|
|
42
|
+
...varNames.map((v) => {
|
|
43
|
+
return `unset ${v}`
|
|
44
|
+
}),
|
|
45
|
+
`unset ${INFRA_KIT_ENV_CONFIG_VAR}`,
|
|
46
|
+
`unset ${INFRA_KIT_ENV_PROJECT_VAR}`,
|
|
47
|
+
`unset ${INFRA_KIT_ENV_LOADED_AT_VAR}`,
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
// Write unset script to cache
|
|
51
|
+
const clearFilePath = path.resolve(cacheDir, ENV_CLEAR_FILE)
|
|
52
|
+
|
|
53
|
+
fs.mkdirSync(cacheDir, { recursive: true })
|
|
54
|
+
fs.writeFileSync(clearFilePath, `${unsetLines.join('\n')}\n`)
|
|
55
|
+
|
|
56
|
+
// REQUIRED
|
|
57
|
+
process.stdout.write(`${clearFilePath}\n`)
|
|
58
|
+
|
|
59
|
+
// Remove env load file so env-clear can detect "no env loaded" next time
|
|
60
|
+
fs.unlinkSync(envLoadPath)
|
|
61
|
+
|
|
62
|
+
logger.info(`Cleared ${varNames.length} environment variables`)
|
|
63
|
+
|
|
64
|
+
const structuredContent = {
|
|
65
|
+
variableCount: varNames.length,
|
|
66
|
+
unsetStatements: unsetLines,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
content: [
|
|
71
|
+
{
|
|
72
|
+
type: 'text',
|
|
73
|
+
text: JSON.stringify(structuredContent, null, 2),
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
structuredContent,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// MCP Tool Registration
|
|
81
|
+
export const envClearMcpTool = {
|
|
82
|
+
name: 'env-clear',
|
|
83
|
+
description:
|
|
84
|
+
'Clear previously loaded environment variables. Usage: env-clear (after running eval "$(infra-kit env-init)" in shell)',
|
|
85
|
+
inputSchema: {},
|
|
86
|
+
outputSchema: {
|
|
87
|
+
variableCount: z.number().describe('Number of variables cleared'),
|
|
88
|
+
unsetStatements: z.array(z.string()).describe('Unset statements generated'),
|
|
89
|
+
},
|
|
90
|
+
handler: envClear,
|
|
91
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { envClear, envClearMcpTool } from './env-clear'
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import process from 'node:process'
|
|
5
|
+
|
|
6
|
+
import { logger } from 'src/lib/logger'
|
|
7
|
+
|
|
8
|
+
const MARKER_COMMENT = '# infra-kit shell functions'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Append infra-kit shell functions directly to .zshrc.
|
|
12
|
+
*/
|
|
13
|
+
export const envInit = async (): Promise<void> => {
|
|
14
|
+
const zshrcPath = path.join(os.homedir(), '.zshrc')
|
|
15
|
+
const binPath = getBinPath()
|
|
16
|
+
|
|
17
|
+
if (!fs.existsSync(binPath)) {
|
|
18
|
+
logger.error(`Could not find infra-kit binary at ${binPath}`)
|
|
19
|
+
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const shellBlock = buildShellBlock(binPath)
|
|
24
|
+
|
|
25
|
+
if (fs.existsSync(zshrcPath)) {
|
|
26
|
+
const content = fs.readFileSync(zshrcPath, 'utf-8')
|
|
27
|
+
const cleaned = removeExistingBlock(content)
|
|
28
|
+
|
|
29
|
+
fs.writeFileSync(zshrcPath, cleaned)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fs.appendFileSync(zshrcPath, `\n${shellBlock}\n`)
|
|
33
|
+
logger.info(`Added infra-kit shell functions to ${zshrcPath}`)
|
|
34
|
+
logger.info('Run `source ~/.zshrc` or open a new terminal to activate.')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const getBinPath = (): string => {
|
|
38
|
+
// resolve the absolute path to the infra-kit binary
|
|
39
|
+
return path.resolve(path.join(path.dirname(process.argv[1]!), 'cli.js'))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const isBlockLine = (line: string): boolean => {
|
|
43
|
+
return (
|
|
44
|
+
line.startsWith('#') ||
|
|
45
|
+
line.startsWith('env-load') ||
|
|
46
|
+
line.startsWith('env-clear') ||
|
|
47
|
+
line.startsWith('if ') ||
|
|
48
|
+
line.startsWith(' export INFRA_KIT_SESSION') ||
|
|
49
|
+
line.startsWith('fi')
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const removeExistingBlock = (content: string): string => {
|
|
54
|
+
const markerIdx = content.indexOf(MARKER_COMMENT)
|
|
55
|
+
|
|
56
|
+
if (markerIdx === -1) return content
|
|
57
|
+
|
|
58
|
+
const before = content.slice(0, markerIdx).replace(/\n+$/, '')
|
|
59
|
+
const afterLines = content.slice(markerIdx).split('\n')
|
|
60
|
+
|
|
61
|
+
let i = 0
|
|
62
|
+
|
|
63
|
+
while (i < afterLines.length && isBlockLine(afterLines[i]!)) {
|
|
64
|
+
i++
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const remaining = afterLines.slice(i).join('\n')
|
|
68
|
+
|
|
69
|
+
return before + (remaining ? `\n${remaining}` : '')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const buildShellBlock = (binPath: string): string => {
|
|
73
|
+
const runCmd = `node ${binPath}`
|
|
74
|
+
|
|
75
|
+
return [
|
|
76
|
+
MARKER_COMMENT,
|
|
77
|
+
'if [[ -z "${INFRA_KIT_SESSION}" ]]; then',
|
|
78
|
+
' export INFRA_KIT_SESSION=$(head -c 4 /dev/urandom | xxd -p)',
|
|
79
|
+
'fi',
|
|
80
|
+
`env-load() { local f; f=$(${runCmd} env-load "$@") && source "$f"; }`,
|
|
81
|
+
`env-clear() { local f; f=$(${runCmd} env-clear) && source "$f"; }`,
|
|
82
|
+
].join('\n')
|
|
83
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { envInit } from './env-init'
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
import { validateDopplerCliAndAuth } from 'src/integrations/doppler'
|
|
4
|
+
import { getDopplerProject } from 'src/integrations/doppler/doppler-project'
|
|
5
|
+
import { ENVs } from 'src/lib/constants'
|
|
6
|
+
import { logger } from 'src/lib/logger'
|
|
7
|
+
import type { ToolsExecutionResult } from 'src/types'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* List available Doppler configs for the detected project
|
|
11
|
+
*/
|
|
12
|
+
export const envList = async (): Promise<ToolsExecutionResult> => {
|
|
13
|
+
await validateDopplerCliAndAuth()
|
|
14
|
+
|
|
15
|
+
const project = await getDopplerProject()
|
|
16
|
+
|
|
17
|
+
logger.info(`Doppler Project: ${project}\n`)
|
|
18
|
+
logger.info('Available Configs:')
|
|
19
|
+
|
|
20
|
+
for (const env of ENVs) {
|
|
21
|
+
logger.info(` - ${env}`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const structuredContent = {
|
|
25
|
+
project,
|
|
26
|
+
configs: ENVs,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
content: [
|
|
31
|
+
{
|
|
32
|
+
type: 'text',
|
|
33
|
+
text: JSON.stringify(structuredContent, null, 2),
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
structuredContent,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// MCP Tool Registration
|
|
41
|
+
export const envListMcpTool = {
|
|
42
|
+
name: 'env-list',
|
|
43
|
+
description: 'List available Doppler configs for the detected project',
|
|
44
|
+
inputSchema: {},
|
|
45
|
+
outputSchema: {
|
|
46
|
+
project: z.string().describe('Detected Doppler project name'),
|
|
47
|
+
configs: z.array(z.string()).describe('Available environment configs'),
|
|
48
|
+
},
|
|
49
|
+
handler: envList,
|
|
50
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { envList, envListMcpTool } from './env-list'
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import select from '@inquirer/select'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import process from 'node:process'
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
import { $ } from 'zx'
|
|
7
|
+
|
|
8
|
+
import { validateDopplerCliAndAuth } from 'src/integrations/doppler'
|
|
9
|
+
import { getDopplerProject } from 'src/integrations/doppler/doppler-project'
|
|
10
|
+
import { commandEcho } from 'src/lib/command-echo'
|
|
11
|
+
import {
|
|
12
|
+
ENV_LOAD_FILE,
|
|
13
|
+
ENVs,
|
|
14
|
+
INFRA_KIT_ENV_CONFIG_VAR,
|
|
15
|
+
INFRA_KIT_ENV_LOADED_AT_VAR,
|
|
16
|
+
INFRA_KIT_ENV_PROJECT_VAR,
|
|
17
|
+
getSessionCacheDir,
|
|
18
|
+
} from 'src/lib/constants'
|
|
19
|
+
import { logger } from 'src/lib/logger'
|
|
20
|
+
import type { ToolsExecutionResult } from 'src/types'
|
|
21
|
+
|
|
22
|
+
interface EnvLoadArgs {
|
|
23
|
+
config?: string
|
|
24
|
+
quiet?: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Load environment variables from Doppler for the given config
|
|
29
|
+
*/
|
|
30
|
+
export const envLoad = async (args: EnvLoadArgs): Promise<ToolsExecutionResult> => {
|
|
31
|
+
await validateDopplerCliAndAuth()
|
|
32
|
+
|
|
33
|
+
const { config, quiet } = args
|
|
34
|
+
|
|
35
|
+
commandEcho.start('env-load')
|
|
36
|
+
|
|
37
|
+
let selectedConfig = ''
|
|
38
|
+
|
|
39
|
+
if (config) {
|
|
40
|
+
selectedConfig = config
|
|
41
|
+
} else {
|
|
42
|
+
commandEcho.setInteractive()
|
|
43
|
+
selectedConfig = await select({
|
|
44
|
+
message: 'Select environment config',
|
|
45
|
+
choices: ENVs.map((env) => {
|
|
46
|
+
return { name: env, value: env }
|
|
47
|
+
}),
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
commandEcho.addOption('--config', selectedConfig)
|
|
52
|
+
|
|
53
|
+
const project = await getDopplerProject()
|
|
54
|
+
|
|
55
|
+
$.quiet = true
|
|
56
|
+
const result =
|
|
57
|
+
await $`doppler secrets download --no-file --format env --project ${project} --config ${selectedConfig}`
|
|
58
|
+
|
|
59
|
+
$.quiet = false
|
|
60
|
+
const envContent = result.stdout.trim()
|
|
61
|
+
|
|
62
|
+
// Build env file content in dotenv format
|
|
63
|
+
const loadedAt = new Date().toISOString()
|
|
64
|
+
const envFileLines = [
|
|
65
|
+
'set -a',
|
|
66
|
+
envContent,
|
|
67
|
+
`${INFRA_KIT_ENV_CONFIG_VAR}=${selectedConfig}`,
|
|
68
|
+
`${INFRA_KIT_ENV_PROJECT_VAR}=${project}`,
|
|
69
|
+
`${INFRA_KIT_ENV_LOADED_AT_VAR}=${loadedAt}`,
|
|
70
|
+
'set +a',
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
// Write env file to cache
|
|
74
|
+
const cacheDir = getSessionCacheDir()
|
|
75
|
+
const envFilePath = path.resolve(cacheDir, ENV_LOAD_FILE)
|
|
76
|
+
|
|
77
|
+
fs.mkdirSync(cacheDir, { recursive: true })
|
|
78
|
+
fs.writeFileSync(envFilePath, `${envFileLines.join('\n')}\n`)
|
|
79
|
+
|
|
80
|
+
// REQUIRED
|
|
81
|
+
process.stdout.write(`${envFilePath}\n`)
|
|
82
|
+
|
|
83
|
+
const varCount = envContent.split('\n').filter((line) => line.includes('=')).length
|
|
84
|
+
|
|
85
|
+
if (!quiet) {
|
|
86
|
+
logger.info(`Loaded ${varCount} variables from ${project}/${selectedConfig}`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const structuredContent = {
|
|
90
|
+
variableCount: varCount,
|
|
91
|
+
project,
|
|
92
|
+
config: selectedConfig,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
content: [
|
|
97
|
+
{
|
|
98
|
+
type: 'text',
|
|
99
|
+
text: JSON.stringify(structuredContent, null, 2),
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
structuredContent,
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// MCP Tool Registration
|
|
107
|
+
export const envLoadMcpTool = {
|
|
108
|
+
name: 'env-load',
|
|
109
|
+
description:
|
|
110
|
+
'Load environment variables from Doppler for a given config. Usage: env-load -c dev (after running eval "$(infra-kit env-init)" in shell)',
|
|
111
|
+
inputSchema: {
|
|
112
|
+
config: z.string().describe('Environment config name to load (e.g. dev, arthur, renana)'),
|
|
113
|
+
},
|
|
114
|
+
outputSchema: {
|
|
115
|
+
variableCount: z.number().describe('Number of variables loaded'),
|
|
116
|
+
project: z.string().describe('Doppler project name'),
|
|
117
|
+
config: z.string().describe('Doppler config name'),
|
|
118
|
+
},
|
|
119
|
+
handler: envLoad,
|
|
120
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { envLoad, envLoadMcpTool } from './env-load'
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import process from 'node:process'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
|
|
5
|
+
import { validateDopplerCliAndAuth } from 'src/integrations/doppler'
|
|
6
|
+
import { getDopplerProject } from 'src/integrations/doppler/doppler-project'
|
|
7
|
+
import {
|
|
8
|
+
ENV_LOAD_FILE,
|
|
9
|
+
ENVs,
|
|
10
|
+
INFRA_KIT_ENV_CONFIG_VAR,
|
|
11
|
+
INFRA_KIT_ENV_LOADED_AT_VAR,
|
|
12
|
+
INFRA_KIT_ENV_PROJECT_VAR,
|
|
13
|
+
INFRA_KIT_SESSION_VAR,
|
|
14
|
+
getSessionCacheDir,
|
|
15
|
+
parseVarNamesFromEnvFile,
|
|
16
|
+
} from 'src/lib/constants'
|
|
17
|
+
import { logger } from 'src/lib/logger'
|
|
18
|
+
import type { ToolsExecutionResult } from 'src/types'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Show Doppler authentication status and detected project info
|
|
22
|
+
*/
|
|
23
|
+
export const envStatus = async (): Promise<ToolsExecutionResult> => {
|
|
24
|
+
await validateDopplerCliAndAuth()
|
|
25
|
+
|
|
26
|
+
const project = await getDopplerProject()
|
|
27
|
+
|
|
28
|
+
logger.info('Doppler Environment Status:\n')
|
|
29
|
+
logger.info(` Authenticated: yes`)
|
|
30
|
+
logger.info(` Detected Project: ${project}`)
|
|
31
|
+
logger.info(` Available Configs: ${ENVs.join(', ')}`)
|
|
32
|
+
|
|
33
|
+
// Check session-loaded vars — getSessionCacheDir() throws if INFRA_KIT_SESSION is unset
|
|
34
|
+
const cacheDir = getSessionCacheDir()
|
|
35
|
+
|
|
36
|
+
const sessionId = process.env[INFRA_KIT_SESSION_VAR]!
|
|
37
|
+
const envLoadPath = path.join(cacheDir, ENV_LOAD_FILE)
|
|
38
|
+
|
|
39
|
+
let sessionLoadedCount = 0
|
|
40
|
+
let sessionTotalCount = 0
|
|
41
|
+
const sessionConfig = process.env[INFRA_KIT_ENV_CONFIG_VAR] ?? null
|
|
42
|
+
const sessionProject = process.env[INFRA_KIT_ENV_PROJECT_VAR] ?? null
|
|
43
|
+
const sessionLoadedAt = process.env[INFRA_KIT_ENV_LOADED_AT_VAR] ?? null
|
|
44
|
+
|
|
45
|
+
if (sessionConfig) {
|
|
46
|
+
const varNames = parseVarNamesFromEnvFile(envLoadPath)
|
|
47
|
+
|
|
48
|
+
if (varNames.length > 0) {
|
|
49
|
+
sessionTotalCount = varNames.length
|
|
50
|
+
sessionLoadedCount = varNames.filter((v) => {
|
|
51
|
+
return v in process.env
|
|
52
|
+
}).length
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
logger.info(
|
|
56
|
+
` Session: ${sessionLoadedCount} of ${sessionTotalCount} vars loaded (project: ${sessionProject}, config: ${sessionConfig}, loaded at: ${sessionLoadedAt})`,
|
|
57
|
+
)
|
|
58
|
+
logger.info(` Session ID: ${sessionId}`)
|
|
59
|
+
} else {
|
|
60
|
+
logger.info(' Session: no env loaded')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const structuredContent = {
|
|
64
|
+
authenticated: true,
|
|
65
|
+
project,
|
|
66
|
+
configs: ENVs,
|
|
67
|
+
sessionId,
|
|
68
|
+
sessionLoadedCount,
|
|
69
|
+
sessionTotalCount,
|
|
70
|
+
sessionConfig,
|
|
71
|
+
sessionProject,
|
|
72
|
+
sessionLoadedAt,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
content: [
|
|
77
|
+
{
|
|
78
|
+
type: 'text',
|
|
79
|
+
text: JSON.stringify(structuredContent, null, 2),
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
structuredContent,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// MCP Tool Registration
|
|
87
|
+
export const envStatusMcpTool = {
|
|
88
|
+
name: 'env-status',
|
|
89
|
+
description: 'Show Doppler authentication status and detected project info',
|
|
90
|
+
inputSchema: {},
|
|
91
|
+
outputSchema: {
|
|
92
|
+
authenticated: z.boolean().describe('Whether the user is authenticated'),
|
|
93
|
+
project: z.string().describe('Detected Doppler project name'),
|
|
94
|
+
configs: z.array(z.string()).describe('Available environment configs'),
|
|
95
|
+
sessionId: z.string().describe('Current terminal session ID'),
|
|
96
|
+
sessionLoadedCount: z.number().describe('Number of cached vars active in the current session'),
|
|
97
|
+
sessionTotalCount: z.number().describe('Total number of cached var names'),
|
|
98
|
+
sessionConfig: z.string().nullable().describe('Doppler config name of the loaded session'),
|
|
99
|
+
sessionProject: z.string().nullable().describe('Doppler project name of the loaded session'),
|
|
100
|
+
sessionLoadedAt: z.string().nullable().describe('ISO 8601 timestamp of when the env was loaded'),
|
|
101
|
+
},
|
|
102
|
+
handler: envStatus,
|
|
103
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { envStatus, envStatusMcpTool } from './env-status'
|
package/src/entry/cli.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
|
|
3
|
+
import { doctor } from 'src/commands/doctor'
|
|
4
|
+
import { envClear } from 'src/commands/env-clear'
|
|
5
|
+
import { envInit } from 'src/commands/env-init'
|
|
6
|
+
import { envList } from 'src/commands/env-list'
|
|
7
|
+
import { envLoad } from 'src/commands/env-load'
|
|
8
|
+
import { envStatus } from 'src/commands/env-status'
|
|
3
9
|
// Commands
|
|
4
10
|
import { ghMergeDev } from 'src/commands/gh-merge-dev'
|
|
5
11
|
import { ghReleaseDeliver } from 'src/commands/gh-release-deliver'
|
|
@@ -13,12 +19,6 @@ import { worktreesAdd } from 'src/commands/worktrees-add'
|
|
|
13
19
|
import { worktreesList } from 'src/commands/worktrees-list'
|
|
14
20
|
import { worktreesRemove } from 'src/commands/worktrees-remove'
|
|
15
21
|
import { worktreesSync } from 'src/commands/worktrees-sync'
|
|
16
|
-
// Integrations
|
|
17
|
-
import { validateGitHubCliAndAuth } from 'src/integrations/gh'
|
|
18
|
-
import { loadEnvFromGitRoot } from 'src/lib/load-env'
|
|
19
|
-
|
|
20
|
-
// Load .env before anything else
|
|
21
|
-
await loadEnvFromGitRoot()
|
|
22
22
|
|
|
23
23
|
const program = new Command()
|
|
24
24
|
|
|
@@ -153,6 +153,48 @@ program
|
|
|
153
153
|
await worktreesRemove({ confirmedCommand: options.yes, all: options.all })
|
|
154
154
|
})
|
|
155
155
|
|
|
156
|
-
|
|
156
|
+
program
|
|
157
|
+
.command('doctor')
|
|
158
|
+
.description('Check installation and authentication status of gh and doppler CLIs')
|
|
159
|
+
.action(async () => {
|
|
160
|
+
await doctor()
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
program
|
|
164
|
+
.command('env-status')
|
|
165
|
+
.description('Show Doppler authentication status and detected project info')
|
|
166
|
+
.action(async () => {
|
|
167
|
+
await envStatus()
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
program
|
|
171
|
+
.command('env-list')
|
|
172
|
+
.description('List available Doppler configs for the detected project')
|
|
173
|
+
.action(async () => {
|
|
174
|
+
await envList()
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
program
|
|
178
|
+
.command('env-init')
|
|
179
|
+
.description('Set up shell functions for env-load/env-clear in .zshrc')
|
|
180
|
+
.action(async () => {
|
|
181
|
+
await envInit()
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
program
|
|
185
|
+
.command('env-load')
|
|
186
|
+
.description('Load environment variables from Doppler. Usage: env-load -c dev (after shell init)')
|
|
187
|
+
.option('-c, --config <config>', 'Environment config name to load (e.g. dev, arthur)')
|
|
188
|
+
.option('-q, --quiet', 'Suppress info logging')
|
|
189
|
+
.action(async (options) => {
|
|
190
|
+
await envLoad({ config: options.config, quiet: options.quiet })
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
program
|
|
194
|
+
.command('env-clear')
|
|
195
|
+
.description('Clear previously loaded environment variables. Usage: env-clear (after shell init)')
|
|
196
|
+
.action(async () => {
|
|
197
|
+
await envClear()
|
|
198
|
+
})
|
|
157
199
|
|
|
158
200
|
program.parse()
|
package/src/entry/mcp.ts
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
|
-
/* eslint-disable antfu/no-top-level-await */
|
|
2
1
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
3
2
|
import process from 'node:process'
|
|
4
3
|
|
|
5
4
|
import { setupErrorHandlers } from 'src/lib/error-handlers'
|
|
6
|
-
import { loadEnvFromGitRoot } from 'src/lib/load-env'
|
|
7
5
|
import { initLoggerMcp } from 'src/lib/logger'
|
|
8
6
|
|
|
9
7
|
import { createMcpServer } from '../mcp/server'
|
|
10
8
|
|
|
11
|
-
// Load .env before anything else
|
|
12
|
-
await loadEnvFromGitRoot()
|
|
13
|
-
|
|
14
9
|
const logger = initLoggerMcp()
|
|
15
10
|
|
|
16
11
|
const startServer = async () => {
|