infra-kit 0.1.71 → 0.1.74
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 +29 -15
- package/dist/cli.js.map +4 -4
- package/dist/mcp.js +32 -19
- package/dist/mcp.js.map +4 -4
- package/package.json +7 -7
- package/src/commands/doctor/doctor.ts +105 -0
- package/src/commands/doctor/index.ts +1 -0
- package/src/commands/env-clear/env-clear.ts +99 -0
- package/src/commands/env-clear/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 +94 -0
- package/src/commands/env-load/index.ts +1 -0
- package/src/commands/env-status/env-status.ts +97 -0
- package/src/commands/env-status/index.ts +1 -0
- package/src/commands/gh-release-deploy-all/gh-release-deploy-all.ts +1 -13
- package/src/commands/gh-release-deploy-selected/gh-release-deploy-selected.ts +1 -13
- package/src/commands/gh-release-deploy-service/gh-release-deploy-service.ts +1 -14
- package/src/entry/cli.ts +40 -3
- 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 +18 -0
- package/src/lib/load-env/load-env.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/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.74",
|
|
5
5
|
"description": "infra-kit",
|
|
6
6
|
"main": "dist/cli.js",
|
|
7
7
|
"module": "dist/cli.js",
|
|
@@ -30,12 +30,12 @@
|
|
|
30
30
|
"fix": "pnpm run prettier-fix && pnpm run eslint-fix && pnpm run qa"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@inquirer/checkbox": "^5.
|
|
34
|
-
"@inquirer/confirm": "^6.0.
|
|
35
|
-
"@inquirer/select": "^5.
|
|
36
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
33
|
+
"@inquirer/checkbox": "^5.1.2",
|
|
34
|
+
"@inquirer/confirm": "^6.0.10",
|
|
35
|
+
"@inquirer/select": "^5.1.2",
|
|
36
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
37
37
|
"commander": "^14.0.3",
|
|
38
|
-
"dotenv": "^17.
|
|
38
|
+
"dotenv": "^17.3.1",
|
|
39
39
|
"pino": "^10.3.1",
|
|
40
40
|
"pino-pretty": "^13.1.3",
|
|
41
41
|
"yaml": "^2.8.2",
|
|
@@ -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,99 @@
|
|
|
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 type { EnvCache } from 'src/lib/constants'
|
|
7
|
+
import {
|
|
8
|
+
ENV_CACHE_DIR,
|
|
9
|
+
ENV_CACHE_FILE,
|
|
10
|
+
INFRA_KIT_ENV_CONFIG_VAR,
|
|
11
|
+
INFRA_KIT_ENV_LOADED_AT_VAR,
|
|
12
|
+
INFRA_KIT_ENV_PROJECT_VAR,
|
|
13
|
+
} from 'src/lib/constants'
|
|
14
|
+
import { logger } from 'src/lib/logger'
|
|
15
|
+
import type { ToolsExecutionResult } from 'src/types'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Clear previously loaded environment variables. Usage: source <(infra-kit env-clear)
|
|
19
|
+
*/
|
|
20
|
+
export const envClear = async (): Promise<ToolsExecutionResult> => {
|
|
21
|
+
const cachePath = path.join(ENV_CACHE_DIR, ENV_CACHE_FILE)
|
|
22
|
+
|
|
23
|
+
if (!process.env[INFRA_KIT_ENV_CONFIG_VAR]) {
|
|
24
|
+
logger.error('No loaded environment found. Run `env-load` first.')
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
content: [
|
|
28
|
+
{
|
|
29
|
+
type: 'text',
|
|
30
|
+
text: 'No loaded environment found. Run `env-load` first.',
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const cache: EnvCache = JSON.parse(fs.readFileSync(cachePath, 'utf-8'))
|
|
37
|
+
const varNames = cache.varNames
|
|
38
|
+
|
|
39
|
+
// Only unset vars that are actually set in the current session
|
|
40
|
+
const activeVars = varNames.filter((v) => {
|
|
41
|
+
return v in process.env
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
if (activeVars.length === 0) {
|
|
45
|
+
fs.unlinkSync(cachePath)
|
|
46
|
+
logger.info('Current session is clear, no environment variables to unset.')
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
content: [
|
|
50
|
+
{
|
|
51
|
+
type: 'text',
|
|
52
|
+
text: 'Current session is clear, no environment variables to unset.',
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Output unset statements for shell sourcing
|
|
59
|
+
for (const varName of activeVars) {
|
|
60
|
+
process.stdout.write(`unset ${varName}\n`)
|
|
61
|
+
}
|
|
62
|
+
process.stdout.write(`unset ${INFRA_KIT_ENV_CONFIG_VAR}\n`)
|
|
63
|
+
process.stdout.write(`unset ${INFRA_KIT_ENV_PROJECT_VAR}\n`)
|
|
64
|
+
process.stdout.write(`unset ${INFRA_KIT_ENV_LOADED_AT_VAR}\n`)
|
|
65
|
+
|
|
66
|
+
// Remove cache file
|
|
67
|
+
fs.unlinkSync(cachePath)
|
|
68
|
+
|
|
69
|
+
logger.info(`Cleared ${activeVars.length} environment variables`)
|
|
70
|
+
|
|
71
|
+
const structuredContent = {
|
|
72
|
+
variableCount: activeVars.length,
|
|
73
|
+
unsetStatements: activeVars.map((v) => {
|
|
74
|
+
return `unset ${v}`
|
|
75
|
+
}),
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
content: [
|
|
80
|
+
{
|
|
81
|
+
type: 'text',
|
|
82
|
+
text: JSON.stringify(structuredContent, null, 2),
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
structuredContent,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// MCP Tool Registration
|
|
90
|
+
export const envClearMcpTool = {
|
|
91
|
+
name: 'env-clear',
|
|
92
|
+
description: 'Clear previously loaded environment variables. Usage: source <(infra-kit env-clear)',
|
|
93
|
+
inputSchema: {},
|
|
94
|
+
outputSchema: {
|
|
95
|
+
variableCount: z.number().describe('Number of variables cleared'),
|
|
96
|
+
unsetStatements: z.array(z.string()).describe('Unset statements generated'),
|
|
97
|
+
},
|
|
98
|
+
handler: envClear,
|
|
99
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { envClear, envClearMcpTool } from './env-clear'
|
|
@@ -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,94 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import process from 'node:process'
|
|
4
|
+
import { z } from 'zod'
|
|
5
|
+
import { $ } from 'zx'
|
|
6
|
+
|
|
7
|
+
import { validateDopplerCliAndAuth } from 'src/integrations/doppler'
|
|
8
|
+
import { getDopplerProject } from 'src/integrations/doppler/doppler-project'
|
|
9
|
+
import type { EnvCache } from 'src/lib/constants'
|
|
10
|
+
import {
|
|
11
|
+
ENV_CACHE_DIR,
|
|
12
|
+
ENV_CACHE_FILE,
|
|
13
|
+
INFRA_KIT_ENV_CONFIG_VAR,
|
|
14
|
+
INFRA_KIT_ENV_LOADED_AT_VAR,
|
|
15
|
+
INFRA_KIT_ENV_PROJECT_VAR,
|
|
16
|
+
} from 'src/lib/constants'
|
|
17
|
+
import { logger } from 'src/lib/logger'
|
|
18
|
+
import type { ToolsExecutionResult } from 'src/types'
|
|
19
|
+
|
|
20
|
+
interface EnvLoadArgs {
|
|
21
|
+
config: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Load environment variables from Doppler for the given config
|
|
26
|
+
*/
|
|
27
|
+
export const envLoad = async (args: EnvLoadArgs): Promise<ToolsExecutionResult> => {
|
|
28
|
+
await validateDopplerCliAndAuth()
|
|
29
|
+
|
|
30
|
+
const { config } = args
|
|
31
|
+
|
|
32
|
+
const project = await getDopplerProject()
|
|
33
|
+
|
|
34
|
+
const result = await $`doppler secrets download --no-file --format env --project ${project} --config ${config}`
|
|
35
|
+
const envContent = result.stdout.trim()
|
|
36
|
+
|
|
37
|
+
// Output shell-sourceable format
|
|
38
|
+
process.stdout.write('set -a\n')
|
|
39
|
+
process.stdout.write(`${envContent}\n`)
|
|
40
|
+
process.stdout.write('set +a\n')
|
|
41
|
+
process.stdout.write(`export ${INFRA_KIT_ENV_CONFIG_VAR}=${config}\n`)
|
|
42
|
+
process.stdout.write(`export ${INFRA_KIT_ENV_PROJECT_VAR}=${project}\n`)
|
|
43
|
+
process.stdout.write(`export ${INFRA_KIT_ENV_LOADED_AT_VAR}=${new Date().toISOString()}\n`)
|
|
44
|
+
|
|
45
|
+
// Extract variable names and save to cache
|
|
46
|
+
const varNames = envContent
|
|
47
|
+
.split('\n')
|
|
48
|
+
.filter((line) => {
|
|
49
|
+
return line.includes('=')
|
|
50
|
+
})
|
|
51
|
+
.map((line) => {
|
|
52
|
+
return line.split('=')[0]!
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const cachePath = path.join(ENV_CACHE_DIR, ENV_CACHE_FILE)
|
|
56
|
+
|
|
57
|
+
const cache: EnvCache = { config, loadedAt: new Date().toISOString(), varNames }
|
|
58
|
+
|
|
59
|
+
fs.mkdirSync(ENV_CACHE_DIR, { recursive: true })
|
|
60
|
+
fs.writeFileSync(cachePath, JSON.stringify(cache, null, 2))
|
|
61
|
+
|
|
62
|
+
logger.info(`Loaded ${varNames.length} variables from ${project}/${config}`)
|
|
63
|
+
|
|
64
|
+
const structuredContent = {
|
|
65
|
+
variableCount: varNames.length,
|
|
66
|
+
project,
|
|
67
|
+
config,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
content: [
|
|
72
|
+
{
|
|
73
|
+
type: 'text',
|
|
74
|
+
text: JSON.stringify(structuredContent, null, 2),
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
structuredContent,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// MCP Tool Registration
|
|
82
|
+
export const envLoadMcpTool = {
|
|
83
|
+
name: 'env-load',
|
|
84
|
+
description: 'Load environment variables from Doppler for a given config. Usage: source <(infra-kit env-load -c dev)',
|
|
85
|
+
inputSchema: {
|
|
86
|
+
config: z.string().describe('Environment config name to load (e.g. dev, arthur, renana)'),
|
|
87
|
+
},
|
|
88
|
+
outputSchema: {
|
|
89
|
+
variableCount: z.number().describe('Number of variables loaded'),
|
|
90
|
+
project: z.string().describe('Doppler project name'),
|
|
91
|
+
config: z.string().describe('Doppler config name'),
|
|
92
|
+
},
|
|
93
|
+
handler: envLoad,
|
|
94
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { envLoad, envLoadMcpTool } from './env-load'
|
|
@@ -0,0 +1,97 @@
|
|
|
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 { validateDopplerCliAndAuth } from 'src/integrations/doppler'
|
|
7
|
+
import { getDopplerProject } from 'src/integrations/doppler/doppler-project'
|
|
8
|
+
import type { EnvCache } from 'src/lib/constants'
|
|
9
|
+
import {
|
|
10
|
+
ENV_CACHE_DIR,
|
|
11
|
+
ENV_CACHE_FILE,
|
|
12
|
+
ENVs,
|
|
13
|
+
INFRA_KIT_ENV_CONFIG_VAR,
|
|
14
|
+
INFRA_KIT_ENV_LOADED_AT_VAR,
|
|
15
|
+
INFRA_KIT_ENV_PROJECT_VAR,
|
|
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
|
|
34
|
+
const cachePath = path.join(ENV_CACHE_DIR, ENV_CACHE_FILE)
|
|
35
|
+
|
|
36
|
+
let sessionLoadedCount = 0
|
|
37
|
+
let sessionTotalCount = 0
|
|
38
|
+
const sessionConfig = process.env[INFRA_KIT_ENV_CONFIG_VAR] ?? null
|
|
39
|
+
const sessionProject = process.env[INFRA_KIT_ENV_PROJECT_VAR] ?? null
|
|
40
|
+
const sessionLoadedAt = process.env[INFRA_KIT_ENV_LOADED_AT_VAR] ?? null
|
|
41
|
+
|
|
42
|
+
if (sessionConfig) {
|
|
43
|
+
if (fs.existsSync(cachePath)) {
|
|
44
|
+
const cache: EnvCache = JSON.parse(fs.readFileSync(cachePath, 'utf-8'))
|
|
45
|
+
|
|
46
|
+
sessionTotalCount = cache.varNames.length
|
|
47
|
+
sessionLoadedCount = cache.varNames.filter((v) => {
|
|
48
|
+
return v in process.env
|
|
49
|
+
}).length
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
logger.info(
|
|
53
|
+
` Session: ${sessionLoadedCount} of ${sessionTotalCount} vars loaded (project: ${sessionProject}, config: ${sessionConfig}, loaded at: ${sessionLoadedAt})`,
|
|
54
|
+
)
|
|
55
|
+
} else {
|
|
56
|
+
logger.info(' Session: no env loaded')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const structuredContent = {
|
|
60
|
+
authenticated: true,
|
|
61
|
+
project,
|
|
62
|
+
configs: ENVs,
|
|
63
|
+
sessionLoadedCount,
|
|
64
|
+
sessionTotalCount,
|
|
65
|
+
sessionConfig,
|
|
66
|
+
sessionProject,
|
|
67
|
+
sessionLoadedAt,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
content: [
|
|
72
|
+
{
|
|
73
|
+
type: 'text',
|
|
74
|
+
text: JSON.stringify(structuredContent, null, 2),
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
structuredContent,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// MCP Tool Registration
|
|
82
|
+
export const envStatusMcpTool = {
|
|
83
|
+
name: 'env-status',
|
|
84
|
+
description: 'Show Doppler authentication status and detected project info',
|
|
85
|
+
inputSchema: {},
|
|
86
|
+
outputSchema: {
|
|
87
|
+
authenticated: z.boolean().describe('Whether the user is authenticated'),
|
|
88
|
+
project: z.string().describe('Detected Doppler project name'),
|
|
89
|
+
configs: z.array(z.string()).describe('Available environment configs'),
|
|
90
|
+
sessionLoadedCount: z.number().describe('Number of cached vars active in the current session'),
|
|
91
|
+
sessionTotalCount: z.number().describe('Total number of cached var names'),
|
|
92
|
+
sessionConfig: z.string().nullable().describe('Doppler config name of the loaded session'),
|
|
93
|
+
sessionProject: z.string().nullable().describe('Doppler project name of the loaded session'),
|
|
94
|
+
sessionLoadedAt: z.string().nullable().describe('ISO 8601 timestamp of when the env was loaded'),
|
|
95
|
+
},
|
|
96
|
+
handler: envStatus,
|
|
97
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { envStatus, envStatusMcpTool } from './env-status'
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import confirm from '@inquirer/confirm'
|
|
2
1
|
import select from '@inquirer/select'
|
|
3
2
|
import process from 'node:process'
|
|
4
3
|
import { z } from 'zod'
|
|
@@ -85,18 +84,7 @@ export const ghReleaseDeployAll = async (args: GhReleaseDeployAllArgs): Promise<
|
|
|
85
84
|
process.exit(1)
|
|
86
85
|
}
|
|
87
86
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (typeof skipTerraform !== 'undefined') {
|
|
91
|
-
shouldSkipTerraform = skipTerraform
|
|
92
|
-
} else {
|
|
93
|
-
commandEcho.setInteractive()
|
|
94
|
-
|
|
95
|
-
shouldSkipTerraform = await confirm({
|
|
96
|
-
message: '🏗️ Skip terraform deployment?',
|
|
97
|
-
default: false,
|
|
98
|
-
})
|
|
99
|
-
}
|
|
87
|
+
const shouldSkipTerraform = skipTerraform ?? false
|
|
100
88
|
|
|
101
89
|
if (shouldSkipTerraform) {
|
|
102
90
|
commandEcho.addOption('--skip-terraform', true)
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import checkbox from '@inquirer/checkbox'
|
|
2
|
-
import confirm from '@inquirer/confirm'
|
|
3
2
|
import select from '@inquirer/select'
|
|
4
3
|
import fs from 'node:fs/promises'
|
|
5
4
|
import { resolve } from 'node:path'
|
|
@@ -136,18 +135,7 @@ export const ghReleaseDeploySelected = async (args: GhReleaseDeploySelectedArgs)
|
|
|
136
135
|
process.exit(1)
|
|
137
136
|
}
|
|
138
137
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (typeof skipTerraform !== 'undefined') {
|
|
142
|
-
shouldSkipTerraform = skipTerraform
|
|
143
|
-
} else {
|
|
144
|
-
commandEcho.setInteractive()
|
|
145
|
-
|
|
146
|
-
shouldSkipTerraform = await confirm({
|
|
147
|
-
message: '🏗️ Skip terraform deployment?',
|
|
148
|
-
default: false,
|
|
149
|
-
})
|
|
150
|
-
}
|
|
138
|
+
const shouldSkipTerraform = skipTerraform ?? false
|
|
151
139
|
|
|
152
140
|
if (shouldSkipTerraform) {
|
|
153
141
|
commandEcho.addOption('--skip-terraform', true)
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import confirm from '@inquirer/confirm'
|
|
2
1
|
import select from '@inquirer/select'
|
|
3
2
|
import fs from 'node:fs/promises'
|
|
4
3
|
import { resolve } from 'node:path'
|
|
@@ -24,7 +23,6 @@ interface GhReleaseDeployServiceArgs {
|
|
|
24
23
|
/**
|
|
25
24
|
* Deploy a specific service in a release branch to an environment
|
|
26
25
|
*/
|
|
27
|
-
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
28
26
|
export const ghReleaseDeployService = async (args: GhReleaseDeployServiceArgs): Promise<ToolsExecutionResult> => {
|
|
29
27
|
const { version, env, service, skipTerraform } = args
|
|
30
28
|
|
|
@@ -119,18 +117,7 @@ export const ghReleaseDeployService = async (args: GhReleaseDeployServiceArgs):
|
|
|
119
117
|
process.exit(1)
|
|
120
118
|
}
|
|
121
119
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (typeof skipTerraform !== 'undefined') {
|
|
125
|
-
shouldSkipTerraform = skipTerraform
|
|
126
|
-
} else {
|
|
127
|
-
commandEcho.setInteractive()
|
|
128
|
-
|
|
129
|
-
shouldSkipTerraform = await confirm({
|
|
130
|
-
message: '🏗️ Skip terraform deployment?',
|
|
131
|
-
default: false,
|
|
132
|
-
})
|
|
133
|
-
}
|
|
120
|
+
const shouldSkipTerraform = skipTerraform ?? false
|
|
134
121
|
|
|
135
122
|
if (shouldSkipTerraform) {
|
|
136
123
|
commandEcho.addOption('--skip-terraform', true)
|
package/src/entry/cli.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
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 { envList } from 'src/commands/env-list'
|
|
6
|
+
import { envLoad } from 'src/commands/env-load'
|
|
7
|
+
import { envStatus } from 'src/commands/env-status'
|
|
3
8
|
// Commands
|
|
4
9
|
import { ghMergeDev } from 'src/commands/gh-merge-dev'
|
|
5
10
|
import { ghReleaseDeliver } from 'src/commands/gh-release-deliver'
|
|
@@ -13,8 +18,6 @@ import { worktreesAdd } from 'src/commands/worktrees-add'
|
|
|
13
18
|
import { worktreesList } from 'src/commands/worktrees-list'
|
|
14
19
|
import { worktreesRemove } from 'src/commands/worktrees-remove'
|
|
15
20
|
import { worktreesSync } from 'src/commands/worktrees-sync'
|
|
16
|
-
// Integrations
|
|
17
|
-
import { validateGitHubCliAndAuth } from 'src/integrations/gh'
|
|
18
21
|
import { loadEnvFromGitRoot } from 'src/lib/load-env'
|
|
19
22
|
|
|
20
23
|
// Load .env before anything else
|
|
@@ -153,6 +156,40 @@ program
|
|
|
153
156
|
await worktreesRemove({ confirmedCommand: options.yes, all: options.all })
|
|
154
157
|
})
|
|
155
158
|
|
|
156
|
-
|
|
159
|
+
program
|
|
160
|
+
.command('doctor')
|
|
161
|
+
.description('Check installation and authentication status of gh and doppler CLIs')
|
|
162
|
+
.action(async () => {
|
|
163
|
+
await doctor()
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
program
|
|
167
|
+
.command('env-status')
|
|
168
|
+
.description('Show Doppler authentication status and detected project info')
|
|
169
|
+
.action(async () => {
|
|
170
|
+
await envStatus()
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
program
|
|
174
|
+
.command('env-list')
|
|
175
|
+
.description('List available Doppler configs for the detected project')
|
|
176
|
+
.action(async () => {
|
|
177
|
+
await envList()
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
program
|
|
181
|
+
.command('env-load')
|
|
182
|
+
.description('Load environment variables from Doppler. Usage: source <(infra-kit env-load -c dev)')
|
|
183
|
+
.requiredOption('-c, --config <config>', 'Environment config name to load (e.g. dev, arthur)')
|
|
184
|
+
.action(async (options) => {
|
|
185
|
+
await envLoad({ config: options.config })
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
program
|
|
189
|
+
.command('env-clear')
|
|
190
|
+
.description('Clear previously loaded environment variables. Usage: source <(infra-kit env-clear)')
|
|
191
|
+
.action(async () => {
|
|
192
|
+
await envClear()
|
|
193
|
+
})
|
|
157
194
|
|
|
158
195
|
program.parse()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import process from 'node:process'
|
|
2
|
+
import { $ } from 'zx'
|
|
3
|
+
|
|
4
|
+
import { logger } from 'src/lib/logger'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Validate Doppler CLI installation and authentication status and throw an error if not valid
|
|
8
|
+
*/
|
|
9
|
+
export const validateDopplerCliAndAuth = async () => {
|
|
10
|
+
try {
|
|
11
|
+
await $`doppler --version`
|
|
12
|
+
} catch (error: unknown) {
|
|
13
|
+
logger.error({ error }, 'Error: Doppler CLI is not installed.')
|
|
14
|
+
logger.error('Please install it from: https://docs.doppler.com/docs/install-cli')
|
|
15
|
+
process.exit(1)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
await $`doppler me`
|
|
20
|
+
} catch (error: unknown) {
|
|
21
|
+
logger.error({ error }, 'Error: Doppler CLI is not authenticated.')
|
|
22
|
+
logger.error('Please authenticate by running: doppler login')
|
|
23
|
+
process.exit(1)
|
|
24
|
+
}
|
|
25
|
+
}
|