@envseal/cli 0.1.4 → 0.1.6
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/bin.js +9 -2
- package/dist/cli-utils.js +4 -4
- package/dist/commands/audit.d.ts +16 -0
- package/dist/commands/audit.js +117 -0
- package/dist/commands/doctor.js +84 -7
- package/dist/commands/init.js +70 -42
- package/dist/commands/status.js +20 -1
- package/dist/exit-codes.d.ts +1 -0
- package/dist/exit-codes.js +6 -0
- package/dist/host-wiring/agents-md-content.d.ts +9 -0
- package/dist/host-wiring/agents-md-content.js +73 -0
- package/dist/host-wiring/agents-md.d.ts +20 -0
- package/dist/host-wiring/agents-md.js +50 -0
- package/dist/host-wiring/aider-conf.d.ts +8 -0
- package/dist/host-wiring/aider-conf.js +37 -0
- package/dist/host-wiring/aider.d.ts +14 -0
- package/dist/host-wiring/aider.js +90 -0
- package/dist/host-wiring/apply.d.ts +22 -0
- package/dist/host-wiring/apply.js +165 -0
- package/dist/host-wiring/codex.d.ts +8 -0
- package/dist/host-wiring/codex.js +57 -0
- package/dist/host-wiring/continue.d.ts +9 -0
- package/dist/host-wiring/continue.js +54 -0
- package/dist/host-wiring/copilot.d.ts +10 -0
- package/dist/host-wiring/copilot.js +99 -0
- package/dist/host-wiring/cursor-rules.d.ts +9 -0
- package/dist/host-wiring/cursor-rules.js +31 -0
- package/dist/host-wiring/cursor.d.ts +31 -0
- package/dist/host-wiring/cursor.js +39 -0
- package/dist/host-wiring/goose.d.ts +16 -0
- package/dist/host-wiring/goose.js +57 -0
- package/dist/host-wiring/inspect.d.ts +24 -0
- package/dist/host-wiring/inspect.js +102 -0
- package/dist/host-wiring/mcp.d.ts +55 -0
- package/dist/host-wiring/mcp.js +238 -0
- package/dist/host-wiring/zed.d.ts +10 -0
- package/dist/host-wiring/zed.js +97 -0
- package/dist/host.d.ts +28 -0
- package/dist/host.js +131 -16
- package/package.json +8 -8
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advisory Cursor rule file shipped by `envseal init`.
|
|
3
|
+
*
|
|
4
|
+
* Keep this string identical to `plugins/cursor/rules/envseal.mdc`. The
|
|
5
|
+
* cursor-wiring test fails if they drift. Embedding (not reading plugins/ at
|
|
6
|
+
* runtime) is what makes published `@envseal/cli` able to write the file.
|
|
7
|
+
*/
|
|
8
|
+
export const CURSOR_RULES_MDC = `---
|
|
9
|
+
description: envseal — never read .env, never echo secrets; use env_describe/env_request instead.
|
|
10
|
+
alwaysApply: true
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
Cursor rules are advisory, not enforced. Treat these as hard rules regardless.
|
|
14
|
+
|
|
15
|
+
- Never read, write, or print the contents of \`.env\` or any \`.env.*\` file (\`.env.example\` is the only exception — it contains placeholders, never values).
|
|
16
|
+
- Never run \`printenv\`, bare \`env\`, \`export -p\`, \`set\`, \`env | grep ...\`, or \`echo $VAR\` / \`echo $KEY...\` to inspect or display environment variables. Every one of these puts secret values into the transcript.
|
|
17
|
+
- Never \`cat\`, \`head\`, \`tail\`, \`less\`, \`grep\`, \`xxd\`, or \`base64\` a secrets file (\`*.pem\`, \`*.key\`, \`credentials.json\`, \`secrets.json\`, \`.envseal/*\`).
|
|
18
|
+
- Never ask the user to paste an API key into the chat.
|
|
19
|
+
|
|
20
|
+
Instead, use the envseal broker via MCP:
|
|
21
|
+
|
|
22
|
+
- \`env_describe\` — read-only status of the declared keys (present, format-valid, last verified). It never returns values and there is no way to make it do so.
|
|
23
|
+
- \`env_request\` — collect the missing keys from the user through a secure prompt. It returns a ticket, never a value; follow up with \`env_await\`.
|
|
24
|
+
- \`env_declare\` — declare a key this project needs, with format and provider metadata.
|
|
25
|
+
- \`env_verify\` — test that a key actually works (classified result only).
|
|
26
|
+
- \`env_use\` — run a command with secrets injected into the child process; output is redacted.
|
|
27
|
+
- \`env_revoke\` — remove a key and get the rotation URL.
|
|
28
|
+
|
|
29
|
+
If a required key is missing, \`env_request\` it. Never read \`.env\` to find it, and never print it back.
|
|
30
|
+
`;
|
|
31
|
+
//# sourceMappingURL=cursor-rules.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type McpInspection, type McpLaunch, type McpWriteAction } from './mcp.js';
|
|
2
|
+
export { ENVSEAL_MCP_PACKAGE, ENVSEAL_MCP_SERVER_NAME, isEmptyEnvsealStub, isStockNpxLaunch, } from './mcp.js';
|
|
3
|
+
export type CursorMcpLaunch = McpLaunch;
|
|
4
|
+
export type CursorMcpWriteAction = McpWriteAction;
|
|
5
|
+
export type CursorRulesWriteAction = 'created' | 'unchanged';
|
|
6
|
+
export type CursorMcpStatus = McpInspection['status'];
|
|
7
|
+
export type CursorMcpInspection = McpInspection;
|
|
8
|
+
export type CursorWiringResult = {
|
|
9
|
+
mcp: CursorMcpWriteAction;
|
|
10
|
+
rules: CursorRulesWriteAction;
|
|
11
|
+
mcpPath: string;
|
|
12
|
+
rulesPath: string;
|
|
13
|
+
/** Human one-liner: reload MCP. Empty when mcp write was skipped. */
|
|
14
|
+
reloadHint: string;
|
|
15
|
+
};
|
|
16
|
+
export declare function cursorMcpLaunch(platform?: NodeJS.Platform): CursorMcpLaunch;
|
|
17
|
+
export declare function cursorMcpSnippetJson(platform?: NodeJS.Platform): string;
|
|
18
|
+
export declare function writeCursorMcpJson(root: string, platform?: NodeJS.Platform): {
|
|
19
|
+
action: CursorMcpWriteAction;
|
|
20
|
+
path: string;
|
|
21
|
+
};
|
|
22
|
+
export declare function writeCursorRules(root: string): {
|
|
23
|
+
action: CursorRulesWriteAction;
|
|
24
|
+
path: string;
|
|
25
|
+
};
|
|
26
|
+
export declare function writeCursorHostFiles(root: string, platform?: NodeJS.Platform): CursorWiringResult;
|
|
27
|
+
export declare function inspectCursorMcp(root: string, options?: {
|
|
28
|
+
probe?: boolean;
|
|
29
|
+
platform?: NodeJS.Platform;
|
|
30
|
+
}): CursorMcpInspection;
|
|
31
|
+
//# sourceMappingURL=cursor.d.ts.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { CURSOR_RULES_MDC } from './cursor-rules.js';
|
|
4
|
+
import { inspectMcpServersFile, mcpLaunch, mcpSnippetJson, mergeMcpServersFile, } from './mcp.js';
|
|
5
|
+
export { ENVSEAL_MCP_PACKAGE, ENVSEAL_MCP_SERVER_NAME, isEmptyEnvsealStub, isStockNpxLaunch, } from './mcp.js';
|
|
6
|
+
const RELOAD_HINT = 'Reload MCP in Settings → MCP so Cursor picks up envseal-mcp.';
|
|
7
|
+
export function cursorMcpLaunch(platform = process.platform) {
|
|
8
|
+
return mcpLaunch(platform);
|
|
9
|
+
}
|
|
10
|
+
export function cursorMcpSnippetJson(platform = process.platform) {
|
|
11
|
+
return mcpSnippetJson(platform);
|
|
12
|
+
}
|
|
13
|
+
export function writeCursorMcpJson(root, platform = process.platform) {
|
|
14
|
+
return mergeMcpServersFile(join(root, '.cursor', 'mcp.json'), platform);
|
|
15
|
+
}
|
|
16
|
+
export function writeCursorRules(root) {
|
|
17
|
+
const path = join(root, '.cursor', 'rules', 'envseal.mdc');
|
|
18
|
+
if (existsSync(path)) {
|
|
19
|
+
return { action: 'unchanged', path };
|
|
20
|
+
}
|
|
21
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
22
|
+
writeFileSync(path, CURSOR_RULES_MDC, 'utf8');
|
|
23
|
+
return { action: 'created', path };
|
|
24
|
+
}
|
|
25
|
+
export function writeCursorHostFiles(root, platform = process.platform) {
|
|
26
|
+
const mcp = writeCursorMcpJson(root, platform);
|
|
27
|
+
const rules = writeCursorRules(root);
|
|
28
|
+
return {
|
|
29
|
+
mcp: mcp.action,
|
|
30
|
+
rules: rules.action,
|
|
31
|
+
mcpPath: mcp.path,
|
|
32
|
+
rulesPath: rules.path,
|
|
33
|
+
reloadHint: mcp.action === 'skipped' ? '' : RELOAD_HINT,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function inspectCursorMcp(root, options = {}) {
|
|
37
|
+
return inspectMcpServersFile(join(root, '.cursor', 'mcp.json'), '.cursor/mcp.json', options);
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=cursor.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type McpInspection, type McpWriteAction } from './mcp.js';
|
|
2
|
+
export declare function gooseYamlSnippet(platform?: NodeJS.Platform): string;
|
|
3
|
+
export declare function gooseAddHint(platform?: NodeJS.Platform): string;
|
|
4
|
+
/**
|
|
5
|
+
* Goose is print-only: do not invent a verified project schema.
|
|
6
|
+
* Create `.goose/` so doctor can label the host; MCP stays unwired until the
|
|
7
|
+
* user runs the printed CLI / yaml themselves.
|
|
8
|
+
*/
|
|
9
|
+
export declare function writeGooseMarker(root: string): {
|
|
10
|
+
action: McpWriteAction;
|
|
11
|
+
path: string;
|
|
12
|
+
addHint: string;
|
|
13
|
+
yamlSnippet: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function inspectGooseProject(root: string): McpInspection;
|
|
16
|
+
//# sourceMappingURL=goose.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { mcpLaunch } from './mcp.js';
|
|
4
|
+
export function gooseYamlSnippet(platform = process.platform) {
|
|
5
|
+
const launch = mcpLaunch(platform);
|
|
6
|
+
const args = launch.args.map((a) => `"${a}"`).join(', ');
|
|
7
|
+
return `mcp:
|
|
8
|
+
servers:
|
|
9
|
+
envseal-mcp:
|
|
10
|
+
cmd: ${launch.command}
|
|
11
|
+
args: [${args}]
|
|
12
|
+
`;
|
|
13
|
+
}
|
|
14
|
+
export function gooseAddHint(platform = process.platform) {
|
|
15
|
+
const launch = mcpLaunch(platform);
|
|
16
|
+
const args = launch.args.join(' ');
|
|
17
|
+
return `goose mcp add envseal-mcp -- ${launch.command} ${args} # [VERIFY: exact flags for your build]`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Goose is print-only: do not invent a verified project schema.
|
|
21
|
+
* Create `.goose/` so doctor can label the host; MCP stays unwired until the
|
|
22
|
+
* user runs the printed CLI / yaml themselves.
|
|
23
|
+
*/
|
|
24
|
+
export function writeGooseMarker(root) {
|
|
25
|
+
const dir = join(root, '.goose');
|
|
26
|
+
const existed = existsSync(dir);
|
|
27
|
+
mkdirSync(dir, { recursive: true });
|
|
28
|
+
return {
|
|
29
|
+
action: existed ? 'unchanged' : 'created',
|
|
30
|
+
path: dir,
|
|
31
|
+
addHint: gooseAddHint(),
|
|
32
|
+
yamlSnippet: gooseYamlSnippet(),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function inspectGooseProject(root) {
|
|
36
|
+
const files = [join(root, 'goose.config.yaml'), join(root, '.goose', 'config.yaml')];
|
|
37
|
+
for (const path of files) {
|
|
38
|
+
if (!existsSync(path))
|
|
39
|
+
continue;
|
|
40
|
+
const text = readFileSync(path, 'utf8');
|
|
41
|
+
if (/envseal-mcp/.test(text)) {
|
|
42
|
+
return {
|
|
43
|
+
wired: true,
|
|
44
|
+
status: 'wired',
|
|
45
|
+
commandOk: null,
|
|
46
|
+
message: `Goose config names envseal-mcp (${path}). [VERIFY]`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
wired: false,
|
|
52
|
+
status: 'missing',
|
|
53
|
+
commandOk: null,
|
|
54
|
+
message: 'Goose is not OOTB: run `goose mcp add` (see envseal init output) or merge the yaml snippet. envseal does not write ~/.config/goose/. [VERIFY]',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=goose.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type McpInspection } from './mcp.js';
|
|
2
|
+
export type AgentWiringMcp = 'ok' | 'missing' | 'spawn_failed';
|
|
3
|
+
export type AgentWiringInstructions = 'ok' | 'missing';
|
|
4
|
+
export type AgentWiring = {
|
|
5
|
+
mcp: AgentWiringMcp;
|
|
6
|
+
instructions: AgentWiringInstructions;
|
|
7
|
+
};
|
|
8
|
+
export type PrimaryHostInspection = {
|
|
9
|
+
wiring: AgentWiring;
|
|
10
|
+
mcp?: McpInspection;
|
|
11
|
+
/** Hosts where MCP is expected in a project file. */
|
|
12
|
+
mcpRequired: boolean;
|
|
13
|
+
/** Documented as print-only / not OOTB even after init. */
|
|
14
|
+
notOotb: boolean;
|
|
15
|
+
/** Aider: `.env` appears on the `read:` list. */
|
|
16
|
+
aiderUnsafe: boolean;
|
|
17
|
+
message: string;
|
|
18
|
+
};
|
|
19
|
+
export declare function inspectPrimaryHostWiring(root: string, hostId: string, options?: {
|
|
20
|
+
probe?: boolean;
|
|
21
|
+
platform?: NodeJS.Platform;
|
|
22
|
+
}): PrimaryHostInspection;
|
|
23
|
+
export declare function wiringFailsDoctor(inspection: PrimaryHostInspection): boolean;
|
|
24
|
+
//# sourceMappingURL=inspect.d.ts.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { inspectAiderConf } from './aider.js';
|
|
3
|
+
import { inspectAgentsMd } from './agents-md.js';
|
|
4
|
+
import { inspectCodexProject } from './codex.js';
|
|
5
|
+
import { inspectContinueProject } from './continue.js';
|
|
6
|
+
import { inspectCopilotSettings } from './copilot.js';
|
|
7
|
+
import { inspectCursorMcp } from './cursor.js';
|
|
8
|
+
import { inspectGooseProject } from './goose.js';
|
|
9
|
+
import { inspectMcpServersFile, mcpWiringState } from './mcp.js';
|
|
10
|
+
import { inspectZedSettings } from './zed.js';
|
|
11
|
+
const MCP_REQUIRED = new Set([
|
|
12
|
+
'cursor',
|
|
13
|
+
'claude-code',
|
|
14
|
+
'windsurf',
|
|
15
|
+
'cline',
|
|
16
|
+
'zed',
|
|
17
|
+
'jetbrains',
|
|
18
|
+
'copilot',
|
|
19
|
+
'continue',
|
|
20
|
+
'codex',
|
|
21
|
+
'goose',
|
|
22
|
+
]);
|
|
23
|
+
const NOT_OOTB = new Set(['continue', 'goose']);
|
|
24
|
+
function inspectPrimaryMcp(root, hostId, options) {
|
|
25
|
+
switch (hostId) {
|
|
26
|
+
case 'cursor':
|
|
27
|
+
return inspectCursorMcp(root, options);
|
|
28
|
+
case 'claude-code':
|
|
29
|
+
return inspectMcpServersFile(join(root, '.mcp.json'), '.mcp.json', options);
|
|
30
|
+
case 'windsurf':
|
|
31
|
+
return inspectMcpServersFile(join(root, '.windsurf', 'mcp_config.json'), '.windsurf/mcp_config.json', options);
|
|
32
|
+
case 'cline':
|
|
33
|
+
return inspectMcpServersFile(join(root, '.cline', 'mcp_settings.json'), '.cline/mcp_settings.json', options);
|
|
34
|
+
case 'zed':
|
|
35
|
+
return inspectZedSettings(root, options);
|
|
36
|
+
case 'jetbrains':
|
|
37
|
+
return inspectMcpServersFile(join(root, '.idea', 'mcp.json'), '.idea/mcp.json', options);
|
|
38
|
+
case 'copilot':
|
|
39
|
+
return inspectCopilotSettings(root, options);
|
|
40
|
+
case 'continue':
|
|
41
|
+
return inspectContinueProject(root);
|
|
42
|
+
case 'codex':
|
|
43
|
+
return inspectCodexProject(root);
|
|
44
|
+
case 'goose':
|
|
45
|
+
return inspectGooseProject(root);
|
|
46
|
+
default:
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function inspectPrimaryHostWiring(root, hostId, options = {}) {
|
|
51
|
+
const instructions = inspectAgentsMd(root).instructions;
|
|
52
|
+
const mcpRequired = MCP_REQUIRED.has(hostId);
|
|
53
|
+
const notOotb = NOT_OOTB.has(hostId);
|
|
54
|
+
if (hostId === 'aider') {
|
|
55
|
+
const aider = inspectAiderConf(root);
|
|
56
|
+
return {
|
|
57
|
+
wiring: { mcp: 'ok', instructions },
|
|
58
|
+
mcpRequired: false,
|
|
59
|
+
notOotb: false,
|
|
60
|
+
aiderUnsafe: !aider.wired,
|
|
61
|
+
message: aider.message,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (!mcpRequired) {
|
|
65
|
+
return {
|
|
66
|
+
wiring: { mcp: 'ok', instructions },
|
|
67
|
+
mcpRequired: false,
|
|
68
|
+
notOotb: false,
|
|
69
|
+
aiderUnsafe: false,
|
|
70
|
+
message: instructions === 'ok'
|
|
71
|
+
? 'Layer 1 instructions present (AGENTS.md).'
|
|
72
|
+
: 'AGENTS.md is missing the envseal imperative (never read .env; use envseal ensure / envseal run). Run `envseal init`.',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const inspection = inspectPrimaryMcp(root, hostId, options) ?? {
|
|
76
|
+
wired: false,
|
|
77
|
+
status: 'missing',
|
|
78
|
+
commandOk: null,
|
|
79
|
+
message: `No project MCP config for host ${hostId}. Run \`envseal init\`.`,
|
|
80
|
+
};
|
|
81
|
+
return {
|
|
82
|
+
wiring: {
|
|
83
|
+
mcp: mcpWiringState(inspection),
|
|
84
|
+
instructions,
|
|
85
|
+
},
|
|
86
|
+
mcp: inspection,
|
|
87
|
+
mcpRequired: true,
|
|
88
|
+
notOotb,
|
|
89
|
+
aiderUnsafe: false,
|
|
90
|
+
message: inspection.message,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
export function wiringFailsDoctor(inspection) {
|
|
94
|
+
if (inspection.mcpRequired && inspection.wiring.mcp !== 'ok')
|
|
95
|
+
return true;
|
|
96
|
+
if (inspection.wiring.instructions === 'missing')
|
|
97
|
+
return true;
|
|
98
|
+
if (inspection.aiderUnsafe)
|
|
99
|
+
return true;
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=inspect.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export declare const ENVSEAL_MCP_PACKAGE = "@envseal/mcp-server";
|
|
2
|
+
export declare const ENVSEAL_MCP_SERVER_NAME = "envseal-mcp";
|
|
3
|
+
export declare const NPX_ARGS: readonly ["-y", "@envseal/mcp-server"];
|
|
4
|
+
export type McpLaunch = {
|
|
5
|
+
command: string;
|
|
6
|
+
args: string[];
|
|
7
|
+
};
|
|
8
|
+
export type McpWriteAction = 'created' | 'merged' | 'unchanged' | 'skipped';
|
|
9
|
+
export type McpStatus = 'absent' | 'unreadable' | 'missing' | 'stub' | 'wired';
|
|
10
|
+
export type McpInspection = {
|
|
11
|
+
wired: boolean;
|
|
12
|
+
status: McpStatus;
|
|
13
|
+
message: string;
|
|
14
|
+
/** null when the launch command was not probed (npx is not side-effect free). */
|
|
15
|
+
commandOk: boolean | null;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Launch argv a host can spawn without a global `envseal-mcp` on PATH.
|
|
19
|
+
* Project MCP uses the workspace as cwd — never bake `--project` in.
|
|
20
|
+
*/
|
|
21
|
+
export declare function mcpLaunch(platform?: NodeJS.Platform): McpLaunch;
|
|
22
|
+
export declare function mcpSnippetJson(platform?: NodeJS.Platform): string;
|
|
23
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
24
|
+
export declare function argsList(value: unknown): string[] | undefined;
|
|
25
|
+
/** Old shipped stub: `envseal-mcp` with no args. Host launchers do not search node_modules/.bin. */
|
|
26
|
+
export declare function isEmptyEnvsealStub(entry: unknown): boolean;
|
|
27
|
+
/** Our npx snippet, either platform variant, and nothing else in command/args. */
|
|
28
|
+
export declare function isStockNpxLaunch(entry: unknown): boolean;
|
|
29
|
+
/** True when an entry would actually start @envseal/mcp-server (npx or a non-empty command). */
|
|
30
|
+
export declare function looksLikeEnvsealServer(entry: unknown): boolean;
|
|
31
|
+
export declare function nextEnvsealEntry(current: unknown, launch: McpLaunch): McpLaunch | Record<string, unknown> | null;
|
|
32
|
+
export declare function parseJsonObject(text: string): Record<string, unknown> | null;
|
|
33
|
+
export declare function writeJson(path: string, value: unknown): void;
|
|
34
|
+
/**
|
|
35
|
+
* Merge envseal-mcp into a file whose top-level key is `mcpServers`.
|
|
36
|
+
* Never writes user-global configs; the caller passes a project path.
|
|
37
|
+
*/
|
|
38
|
+
export declare function mergeMcpServersFile(path: string, platform?: NodeJS.Platform): {
|
|
39
|
+
action: McpWriteAction;
|
|
40
|
+
path: string;
|
|
41
|
+
};
|
|
42
|
+
export declare function classifyEntry(entry: unknown): 'missing' | 'stub' | 'wired';
|
|
43
|
+
/**
|
|
44
|
+
* `--version` is side-effect free on the envseal-mcp binary. `npx -y` is not
|
|
45
|
+
* (it may hit the network), so the default snippet is not probed.
|
|
46
|
+
*/
|
|
47
|
+
export declare function shouldProbeLaunch(entry: Record<string, unknown>): boolean;
|
|
48
|
+
export declare function probeVersion(entry: Record<string, unknown>): boolean | null;
|
|
49
|
+
export declare function inspectMcpServersFile(path: string, label: string, options?: {
|
|
50
|
+
probe?: boolean;
|
|
51
|
+
platform?: NodeJS.Platform;
|
|
52
|
+
}): McpInspection;
|
|
53
|
+
/** Map an inspection to the doctor `agentWiring.mcp` field. */
|
|
54
|
+
export declare function mcpWiringState(inspection: McpInspection): 'ok' | 'missing' | 'spawn_failed';
|
|
55
|
+
//# sourceMappingURL=mcp.d.ts.map
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
export const ENVSEAL_MCP_PACKAGE = '@envseal/mcp-server';
|
|
5
|
+
export const ENVSEAL_MCP_SERVER_NAME = 'envseal-mcp';
|
|
6
|
+
export const NPX_ARGS = ['-y', ENVSEAL_MCP_PACKAGE];
|
|
7
|
+
/**
|
|
8
|
+
* Launch argv a host can spawn without a global `envseal-mcp` on PATH.
|
|
9
|
+
* Project MCP uses the workspace as cwd — never bake `--project` in.
|
|
10
|
+
*/
|
|
11
|
+
export function mcpLaunch(platform = process.platform) {
|
|
12
|
+
return {
|
|
13
|
+
command: platform === 'win32' ? 'npx.cmd' : 'npx',
|
|
14
|
+
args: [...NPX_ARGS],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function mcpSnippetJson(platform = process.platform) {
|
|
18
|
+
const launch = mcpLaunch(platform);
|
|
19
|
+
return JSON.stringify({
|
|
20
|
+
mcpServers: { [ENVSEAL_MCP_SERVER_NAME]: launch },
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export function isRecord(value) {
|
|
24
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
25
|
+
}
|
|
26
|
+
export function argsList(value) {
|
|
27
|
+
if (value === undefined)
|
|
28
|
+
return undefined;
|
|
29
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== 'string'))
|
|
30
|
+
return undefined;
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
/** Old shipped stub: `envseal-mcp` with no args. Host launchers do not search node_modules/.bin. */
|
|
34
|
+
export function isEmptyEnvsealStub(entry) {
|
|
35
|
+
if (!isRecord(entry))
|
|
36
|
+
return true;
|
|
37
|
+
const command = entry.command;
|
|
38
|
+
if (typeof command !== 'string' || command.trim() === '')
|
|
39
|
+
return true;
|
|
40
|
+
if (command !== 'envseal-mcp')
|
|
41
|
+
return false;
|
|
42
|
+
const args = argsList(entry.args);
|
|
43
|
+
return args === undefined || args.length === 0;
|
|
44
|
+
}
|
|
45
|
+
/** Our npx snippet, either platform variant, and nothing else in command/args. */
|
|
46
|
+
export function isStockNpxLaunch(entry) {
|
|
47
|
+
if (!isRecord(entry))
|
|
48
|
+
return false;
|
|
49
|
+
const command = entry.command;
|
|
50
|
+
if (command !== 'npx' && command !== 'npx.cmd')
|
|
51
|
+
return false;
|
|
52
|
+
const args = argsList(entry.args);
|
|
53
|
+
return (args !== undefined && args.length === NPX_ARGS.length && NPX_ARGS.every((a, i) => a === args[i]));
|
|
54
|
+
}
|
|
55
|
+
/** True when an entry would actually start @envseal/mcp-server (npx or a non-empty command). */
|
|
56
|
+
export function looksLikeEnvsealServer(entry) {
|
|
57
|
+
if (entry === undefined || isEmptyEnvsealStub(entry))
|
|
58
|
+
return false;
|
|
59
|
+
if (!isRecord(entry))
|
|
60
|
+
return false;
|
|
61
|
+
const command = entry.command;
|
|
62
|
+
if (typeof command !== 'string' || command.trim() === '')
|
|
63
|
+
return false;
|
|
64
|
+
if (isStockNpxLaunch(entry))
|
|
65
|
+
return true;
|
|
66
|
+
const joined = `${command} ${(argsList(entry.args) ?? []).join(' ')}`;
|
|
67
|
+
return /@envseal\/mcp-server|envseal-mcp/i.test(joined);
|
|
68
|
+
}
|
|
69
|
+
export function nextEnvsealEntry(current, launch) {
|
|
70
|
+
if (current === undefined || isEmptyEnvsealStub(current)) {
|
|
71
|
+
return launch;
|
|
72
|
+
}
|
|
73
|
+
if (isStockNpxLaunch(current) && isRecord(current)) {
|
|
74
|
+
if (current.command === launch.command)
|
|
75
|
+
return null;
|
|
76
|
+
return { ...current, command: launch.command, args: launch.args };
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
export function parseJsonObject(text) {
|
|
81
|
+
try {
|
|
82
|
+
const parsed = JSON.parse(text);
|
|
83
|
+
return isRecord(parsed) ? parsed : null;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export function writeJson(path, value) {
|
|
90
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
91
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Merge envseal-mcp into a file whose top-level key is `mcpServers`.
|
|
95
|
+
* Never writes user-global configs; the caller passes a project path.
|
|
96
|
+
*/
|
|
97
|
+
export function mergeMcpServersFile(path, platform = process.platform) {
|
|
98
|
+
const launch = mcpLaunch(platform);
|
|
99
|
+
if (!existsSync(path)) {
|
|
100
|
+
writeJson(path, { mcpServers: { [ENVSEAL_MCP_SERVER_NAME]: launch } });
|
|
101
|
+
return { action: 'created', path };
|
|
102
|
+
}
|
|
103
|
+
const parsed = parseJsonObject(readFileSync(path, 'utf8'));
|
|
104
|
+
if (parsed === null) {
|
|
105
|
+
return { action: 'skipped', path };
|
|
106
|
+
}
|
|
107
|
+
const existingServers = parsed.mcpServers;
|
|
108
|
+
if (existingServers !== undefined && !isRecord(existingServers)) {
|
|
109
|
+
return { action: 'skipped', path };
|
|
110
|
+
}
|
|
111
|
+
const servers = existingServers === undefined ? {} : { ...existingServers };
|
|
112
|
+
const next = nextEnvsealEntry(servers[ENVSEAL_MCP_SERVER_NAME], launch);
|
|
113
|
+
if (next === null) {
|
|
114
|
+
return { action: 'unchanged', path };
|
|
115
|
+
}
|
|
116
|
+
servers[ENVSEAL_MCP_SERVER_NAME] = next;
|
|
117
|
+
writeJson(path, { ...parsed, mcpServers: servers });
|
|
118
|
+
return { action: 'merged', path };
|
|
119
|
+
}
|
|
120
|
+
export function classifyEntry(entry) {
|
|
121
|
+
if (entry === undefined)
|
|
122
|
+
return 'missing';
|
|
123
|
+
if (isEmptyEnvsealStub(entry))
|
|
124
|
+
return 'stub';
|
|
125
|
+
if (!isRecord(entry))
|
|
126
|
+
return 'stub';
|
|
127
|
+
const command = entry.command;
|
|
128
|
+
if (typeof command !== 'string' || command.trim() === '')
|
|
129
|
+
return 'stub';
|
|
130
|
+
return 'wired';
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* `--version` is side-effect free on the envseal-mcp binary. `npx -y` is not
|
|
134
|
+
* (it may hit the network), so the default snippet is not probed.
|
|
135
|
+
*/
|
|
136
|
+
export function shouldProbeLaunch(entry) {
|
|
137
|
+
const command = entry.command;
|
|
138
|
+
if (typeof command !== 'string')
|
|
139
|
+
return false;
|
|
140
|
+
if (command === 'npx' || command === 'npx.cmd')
|
|
141
|
+
return false;
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
export function probeVersion(entry) {
|
|
145
|
+
if (!shouldProbeLaunch(entry))
|
|
146
|
+
return null;
|
|
147
|
+
const command = entry.command;
|
|
148
|
+
if (typeof command !== 'string')
|
|
149
|
+
return null;
|
|
150
|
+
const args = argsList(entry.args) ?? [];
|
|
151
|
+
const result = spawnSync(command, [...args, '--version'], {
|
|
152
|
+
encoding: 'utf8',
|
|
153
|
+
timeout: 5000,
|
|
154
|
+
windowsHide: true,
|
|
155
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
156
|
+
});
|
|
157
|
+
if (result.error)
|
|
158
|
+
return false;
|
|
159
|
+
if (result.status !== 0)
|
|
160
|
+
return false;
|
|
161
|
+
const out = `${result.stdout ?? ''}${result.stderr ?? ''}`;
|
|
162
|
+
return /envseal-mcp/i.test(out);
|
|
163
|
+
}
|
|
164
|
+
function initHint(platform, label) {
|
|
165
|
+
return `Run \`envseal init\` to write it, or merge ${mcpSnippetJson(platform)} into ${label}.`;
|
|
166
|
+
}
|
|
167
|
+
export function inspectMcpServersFile(path, label, options = {}) {
|
|
168
|
+
const platform = options.platform ?? process.platform;
|
|
169
|
+
const hint = initHint(platform, label);
|
|
170
|
+
if (!existsSync(path)) {
|
|
171
|
+
return {
|
|
172
|
+
wired: false,
|
|
173
|
+
status: 'absent',
|
|
174
|
+
commandOk: null,
|
|
175
|
+
message: `${label} is missing. ${hint}`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const parsed = parseJsonObject(readFileSync(path, 'utf8'));
|
|
179
|
+
if (parsed === null) {
|
|
180
|
+
return {
|
|
181
|
+
wired: false,
|
|
182
|
+
status: 'unreadable',
|
|
183
|
+
commandOk: null,
|
|
184
|
+
message: `${label} is not valid JSON. ${hint}`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const servers = parsed.mcpServers;
|
|
188
|
+
if (servers === undefined || (isRecord(servers) && Object.keys(servers).length === 0)) {
|
|
189
|
+
return {
|
|
190
|
+
wired: false,
|
|
191
|
+
status: 'missing',
|
|
192
|
+
commandOk: null,
|
|
193
|
+
message: `${label} has no envseal-mcp (empty mcpServers). ${hint}`,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
if (!isRecord(servers)) {
|
|
197
|
+
return {
|
|
198
|
+
wired: false,
|
|
199
|
+
status: 'unreadable',
|
|
200
|
+
commandOk: null,
|
|
201
|
+
message: `${label} mcpServers is not an object. ${hint}`,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
const entry = servers[ENVSEAL_MCP_SERVER_NAME];
|
|
205
|
+
const kind = classifyEntry(entry);
|
|
206
|
+
if (kind === 'missing') {
|
|
207
|
+
return {
|
|
208
|
+
wired: false,
|
|
209
|
+
status: 'missing',
|
|
210
|
+
commandOk: null,
|
|
211
|
+
message: `${label} has no envseal-mcp. ${hint}`,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
if (kind === 'stub') {
|
|
215
|
+
return {
|
|
216
|
+
wired: false,
|
|
217
|
+
status: 'stub',
|
|
218
|
+
commandOk: null,
|
|
219
|
+
message: `${label} envseal-mcp is the empty envseal-mcp stub (not on PATH for the host). ${hint}`,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const rec = entry;
|
|
223
|
+
const commandOk = options.probe === true ? probeVersion(rec) : null;
|
|
224
|
+
let message = `MCP is wired (${label}).`;
|
|
225
|
+
if (commandOk === false) {
|
|
226
|
+
message = `MCP is configured in ${label}, but the launch command did not report a version. Run \`envseal init\` if the host cannot connect.`;
|
|
227
|
+
}
|
|
228
|
+
return { wired: true, status: 'wired', commandOk, message };
|
|
229
|
+
}
|
|
230
|
+
/** Map an inspection to the doctor `agentWiring.mcp` field. */
|
|
231
|
+
export function mcpWiringState(inspection) {
|
|
232
|
+
if (inspection.commandOk === false)
|
|
233
|
+
return 'spawn_failed';
|
|
234
|
+
if (inspection.wired)
|
|
235
|
+
return 'ok';
|
|
236
|
+
return 'missing';
|
|
237
|
+
}
|
|
238
|
+
//# sourceMappingURL=mcp.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type McpInspection, type McpWriteAction } from './mcp.js';
|
|
2
|
+
export declare function mergeZedSettings(root: string, platform?: NodeJS.Platform): {
|
|
3
|
+
action: McpWriteAction;
|
|
4
|
+
path: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function inspectZedSettings(root: string, options?: {
|
|
7
|
+
probe?: boolean;
|
|
8
|
+
platform?: NodeJS.Platform;
|
|
9
|
+
}): McpInspection;
|
|
10
|
+
//# sourceMappingURL=zed.d.ts.map
|