@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,20 @@
|
|
|
1
|
+
export type AgentsMdAction = 'created' | 'merged' | 'unchanged';
|
|
2
|
+
/**
|
|
3
|
+
* The envseal imperative: never read .env, use ensure/run instead of a paste.
|
|
4
|
+
* Doctor and init both use this so "instructions exist" is not just a filename.
|
|
5
|
+
*/
|
|
6
|
+
export declare function hasEnvsealImperative(text: string): boolean;
|
|
7
|
+
export declare function inspectAgentsMd(root: string): {
|
|
8
|
+
path: string;
|
|
9
|
+
exists: boolean;
|
|
10
|
+
instructions: 'ok' | 'missing';
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Merge plugins/generic/AGENTS.md into project-root AGENTS.md.
|
|
14
|
+
* Creates the file, or appends an envseal section; never clobbers unrelated content.
|
|
15
|
+
*/
|
|
16
|
+
export declare function mergeAgentsMd(root: string): {
|
|
17
|
+
action: AgentsMdAction;
|
|
18
|
+
path: string;
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=agents-md.d.ts.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { AGENTS_MD_CONTENT } from './agents-md-content.js';
|
|
4
|
+
/**
|
|
5
|
+
* The envseal imperative: never read .env, use ensure/run instead of a paste.
|
|
6
|
+
* Doctor and init both use this so "instructions exist" is not just a filename.
|
|
7
|
+
*/
|
|
8
|
+
export function hasEnvsealImperative(text) {
|
|
9
|
+
const neverEnv = /never/i.test(text) && /\.env/i.test(text);
|
|
10
|
+
const useEnsure = /envseal\s+ensure/i.test(text);
|
|
11
|
+
const useRun = /envseal\s+run/i.test(text);
|
|
12
|
+
return neverEnv && useEnsure && useRun;
|
|
13
|
+
}
|
|
14
|
+
export function inspectAgentsMd(root) {
|
|
15
|
+
const path = join(root, 'AGENTS.md');
|
|
16
|
+
if (!existsSync(path)) {
|
|
17
|
+
return { path, exists: false, instructions: 'missing' };
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const text = readFileSync(path, 'utf8');
|
|
21
|
+
return {
|
|
22
|
+
path,
|
|
23
|
+
exists: true,
|
|
24
|
+
instructions: hasEnvsealImperative(text) ? 'ok' : 'missing',
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return { path, exists: true, instructions: 'missing' };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Merge plugins/generic/AGENTS.md into project-root AGENTS.md.
|
|
33
|
+
* Creates the file, or appends an envseal section; never clobbers unrelated content.
|
|
34
|
+
*/
|
|
35
|
+
export function mergeAgentsMd(root) {
|
|
36
|
+
const path = join(root, 'AGENTS.md');
|
|
37
|
+
if (!existsSync(path)) {
|
|
38
|
+
writeFileSync(path, AGENTS_MD_CONTENT, 'utf8');
|
|
39
|
+
return { action: 'created', path };
|
|
40
|
+
}
|
|
41
|
+
const existing = readFileSync(path, 'utf8');
|
|
42
|
+
if (hasEnvsealImperative(existing)) {
|
|
43
|
+
return { action: 'unchanged', path };
|
|
44
|
+
}
|
|
45
|
+
const trimmed = existing.replace(/\s+$/u, '');
|
|
46
|
+
const next = `${trimmed}\n\n${AGENTS_MD_CONTENT}`;
|
|
47
|
+
writeFileSync(path, next, 'utf8');
|
|
48
|
+
return { action: 'merged', path };
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=agents-md.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aider config shipped by `envseal init --host aider`.
|
|
3
|
+
*
|
|
4
|
+
* Keep this string identical to `plugins/aider/.aider.conf.yml`. The host-wiring
|
|
5
|
+
* test fails if they drift.
|
|
6
|
+
*/
|
|
7
|
+
export declare const AIDER_CONF_YML = "# .aider.conf.yml \u2014 envseal for Aider (Tier C host, Tier-4 CLI binding)\n#\n# Aider renders every file it reads into the chat context. NEVER add `.env`\n# or `.env.*` to the `read` list \u2014 that is exactly the leak path envseal exists\n# to prevent. `env.schema.jsonc` and `.env.example` contain declarations and\n# placeholders only and are safe to read.\n\nmodel: gpt-4o\nedit-format: editor-diff\n\nread:\n - env.schema.jsonc\n - .env.example\n\n# Optional: run a command after every edit with secrets injected.\n# auto-test:\n# command: \"../../.../envseal run -- pnpm test\"\n\n# --- Tier-4 shell recipe (run from Aider's REPL) --------------------------\n#\n# /run envseal status # which declared keys are present\n# /run envseal ensure # prompt the user for every missing key\n# /run envseal run -- pytest # run tests with secrets injected\n# /run envseal verify # probe the keys end-to-end\n# /run envseal doctor # report host + tier + config health\n#\n# `envseal ensure` and `envseal run --` are the only ways to obtain or use\n# secret values inside Aider. Never ask the user to paste a key into the chat;\n# never read `.env`; never `echo $KEY`.\n";
|
|
8
|
+
//# sourceMappingURL=aider-conf.d.ts.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aider config shipped by `envseal init --host aider`.
|
|
3
|
+
*
|
|
4
|
+
* Keep this string identical to `plugins/aider/.aider.conf.yml`. The host-wiring
|
|
5
|
+
* test fails if they drift.
|
|
6
|
+
*/
|
|
7
|
+
export const AIDER_CONF_YML = `# .aider.conf.yml — envseal for Aider (Tier C host, Tier-4 CLI binding)
|
|
8
|
+
#
|
|
9
|
+
# Aider renders every file it reads into the chat context. NEVER add \`.env\`
|
|
10
|
+
# or \`.env.*\` to the \`read\` list — that is exactly the leak path envseal exists
|
|
11
|
+
# to prevent. \`env.schema.jsonc\` and \`.env.example\` contain declarations and
|
|
12
|
+
# placeholders only and are safe to read.
|
|
13
|
+
|
|
14
|
+
model: gpt-4o
|
|
15
|
+
edit-format: editor-diff
|
|
16
|
+
|
|
17
|
+
read:
|
|
18
|
+
- env.schema.jsonc
|
|
19
|
+
- .env.example
|
|
20
|
+
|
|
21
|
+
# Optional: run a command after every edit with secrets injected.
|
|
22
|
+
# auto-test:
|
|
23
|
+
# command: "../../.../envseal run -- pnpm test"
|
|
24
|
+
|
|
25
|
+
# --- Tier-4 shell recipe (run from Aider's REPL) --------------------------
|
|
26
|
+
#
|
|
27
|
+
# /run envseal status # which declared keys are present
|
|
28
|
+
# /run envseal ensure # prompt the user for every missing key
|
|
29
|
+
# /run envseal run -- pytest # run tests with secrets injected
|
|
30
|
+
# /run envseal verify # probe the keys end-to-end
|
|
31
|
+
# /run envseal doctor # report host + tier + config health
|
|
32
|
+
#
|
|
33
|
+
# \`envseal ensure\` and \`envseal run --\` are the only ways to obtain or use
|
|
34
|
+
# secret values inside Aider. Never ask the user to paste a key into the chat;
|
|
35
|
+
# never read \`.env\`; never \`echo $KEY\`.
|
|
36
|
+
`;
|
|
37
|
+
//# sourceMappingURL=aider-conf.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { McpWriteAction } from './mcp.js';
|
|
2
|
+
export declare function aiderConfPath(root: string): string | undefined;
|
|
3
|
+
export declare function aiderReadListIncludesEnv(text: string): boolean;
|
|
4
|
+
export declare function inspectAiderConf(root: string): {
|
|
5
|
+
path: string | undefined;
|
|
6
|
+
envOnRead: boolean;
|
|
7
|
+
wired: boolean;
|
|
8
|
+
message: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function mergeAiderConf(root: string): {
|
|
11
|
+
action: McpWriteAction;
|
|
12
|
+
path: string;
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=aider.d.ts.map
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { AIDER_CONF_YML } from './aider-conf.js';
|
|
4
|
+
const AIDER_FILENAMES = [
|
|
5
|
+
'.aider.conf.yml',
|
|
6
|
+
'.aider.conf.yaml',
|
|
7
|
+
'aider.conf.yml',
|
|
8
|
+
'aider.conf.yaml',
|
|
9
|
+
'.aider.conf.json',
|
|
10
|
+
'aider.conf.json',
|
|
11
|
+
];
|
|
12
|
+
/** A `read:` list item that would dump `.env` into Aider's chat context. */
|
|
13
|
+
const ENV_READ_ITEM = /^\s*-\s+['"]?\.env(?:\..*?)?['"]?\s*$/;
|
|
14
|
+
export function aiderConfPath(root) {
|
|
15
|
+
for (const name of AIDER_FILENAMES) {
|
|
16
|
+
const path = join(root, name);
|
|
17
|
+
if (existsSync(path))
|
|
18
|
+
return path;
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
export function aiderReadListIncludesEnv(text) {
|
|
23
|
+
return text.split(/\r?\n/).some((line) => ENV_READ_ITEM.test(line) && !/example/i.test(line));
|
|
24
|
+
}
|
|
25
|
+
function stripEnvFromReadList(text) {
|
|
26
|
+
return text
|
|
27
|
+
.split(/\r?\n/)
|
|
28
|
+
.filter((line) => !(ENV_READ_ITEM.test(line) && !/example/i.test(line)))
|
|
29
|
+
.join('\n');
|
|
30
|
+
}
|
|
31
|
+
function ensureSafeReadEntries(text) {
|
|
32
|
+
let next = stripEnvFromReadList(text);
|
|
33
|
+
if (!/^read:/m.test(next)) {
|
|
34
|
+
const trimmed = next.replace(/\s+$/u, '');
|
|
35
|
+
return `${trimmed}\n\nread:\n - env.schema.jsonc\n - .env.example\n`;
|
|
36
|
+
}
|
|
37
|
+
if (!/env\.schema\.jsonc/.test(next)) {
|
|
38
|
+
next = next.replace(/^read:\s*$/m, 'read:\n - env.schema.jsonc');
|
|
39
|
+
if (!/env\.schema\.jsonc/.test(next)) {
|
|
40
|
+
next = next.replace(/^read:/m, 'read:\n - env.schema.jsonc');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (!/\.env\.example/.test(next)) {
|
|
44
|
+
next = next.replace(/(env\.schema\.jsonc[^\n]*)/, '$1\n - .env.example');
|
|
45
|
+
}
|
|
46
|
+
return next;
|
|
47
|
+
}
|
|
48
|
+
export function inspectAiderConf(root) {
|
|
49
|
+
const path = aiderConfPath(root);
|
|
50
|
+
if (path === undefined) {
|
|
51
|
+
return {
|
|
52
|
+
path,
|
|
53
|
+
envOnRead: false,
|
|
54
|
+
wired: false,
|
|
55
|
+
message: 'No Aider config found. Run `envseal init --host aider` to write .aider.conf.yml.',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const text = readFileSync(path, 'utf8');
|
|
59
|
+
const envOnRead = aiderReadListIncludesEnv(text);
|
|
60
|
+
if (envOnRead) {
|
|
61
|
+
return {
|
|
62
|
+
path,
|
|
63
|
+
envOnRead: true,
|
|
64
|
+
wired: false,
|
|
65
|
+
message: `${path} lists .env under read: — Aider would paste secrets into chat. Re-run envseal init --host aider.`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
path,
|
|
70
|
+
envOnRead: false,
|
|
71
|
+
wired: true,
|
|
72
|
+
message: 'Aider config does not put .env on the read list.',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export function mergeAiderConf(root) {
|
|
76
|
+
const existing = aiderConfPath(root);
|
|
77
|
+
const path = existing ?? join(root, '.aider.conf.yml');
|
|
78
|
+
if (!existsSync(path)) {
|
|
79
|
+
writeFileSync(path, AIDER_CONF_YML, 'utf8');
|
|
80
|
+
return { action: 'created', path };
|
|
81
|
+
}
|
|
82
|
+
const text = readFileSync(path, 'utf8');
|
|
83
|
+
const next = ensureSafeReadEntries(text);
|
|
84
|
+
if (next === text) {
|
|
85
|
+
return { action: 'unchanged', path };
|
|
86
|
+
}
|
|
87
|
+
writeFileSync(path, next.endsWith('\n') ? next : `${next}\n`, 'utf8');
|
|
88
|
+
return { action: 'merged', path };
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=aider.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type AgentsMdAction } from './agents-md.js';
|
|
2
|
+
import { type CursorWiringResult } from './cursor.js';
|
|
3
|
+
import { type McpWriteAction } from './mcp.js';
|
|
4
|
+
export type HostWiringEntry = {
|
|
5
|
+
id: string;
|
|
6
|
+
action: McpWriteAction | 'printed';
|
|
7
|
+
path?: string;
|
|
8
|
+
hint: string;
|
|
9
|
+
extra?: Record<string, unknown>;
|
|
10
|
+
};
|
|
11
|
+
export type ApplyWiringResult = {
|
|
12
|
+
agentsMd: {
|
|
13
|
+
action: AgentsMdAction;
|
|
14
|
+
path: string;
|
|
15
|
+
};
|
|
16
|
+
hosts: HostWiringEntry[];
|
|
17
|
+
cursor?: CursorWiringResult;
|
|
18
|
+
/** True when init wrote nothing host-specific besides AGENTS.md. */
|
|
19
|
+
bareTerminal: boolean;
|
|
20
|
+
};
|
|
21
|
+
export declare function applyHostWiring(root: string, hostIds: string[], platform?: NodeJS.Platform): ApplyWiringResult;
|
|
22
|
+
//# sourceMappingURL=apply.d.ts.map
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { mkdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { mergeAiderConf } from './aider.js';
|
|
4
|
+
import { mergeAgentsMd } from './agents-md.js';
|
|
5
|
+
import { writeCodexProjectConfig } from './codex.js';
|
|
6
|
+
import { writeContinueProjectFiles, continueSnippetYaml } from './continue.js';
|
|
7
|
+
import { mergeCopilotSettings } from './copilot.js';
|
|
8
|
+
import { writeCursorHostFiles } from './cursor.js';
|
|
9
|
+
import { writeGooseMarker, gooseYamlSnippet } from './goose.js';
|
|
10
|
+
import { mergeMcpServersFile } from './mcp.js';
|
|
11
|
+
import { mergeZedSettings } from './zed.js';
|
|
12
|
+
const RELOAD = 'Reload MCP / restart the host, then run `envseal doctor`.';
|
|
13
|
+
function mcpServersWrite(id, path, relative, platform, extraHint) {
|
|
14
|
+
const result = mergeMcpServersFile(path, platform);
|
|
15
|
+
const skipped = result.action === 'skipped'
|
|
16
|
+
? ` Could not merge ${relative} (invalid JSON). Fix the file and re-run envseal init.`
|
|
17
|
+
: '';
|
|
18
|
+
return {
|
|
19
|
+
id,
|
|
20
|
+
action: result.action,
|
|
21
|
+
path: result.path,
|
|
22
|
+
hint: skipped || `${extraHint ?? ''} ${RELOAD}`.trim(),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function applyHostWiring(root, hostIds, platform = process.platform) {
|
|
26
|
+
const agentsMd = mergeAgentsMd(root);
|
|
27
|
+
const hosts = [];
|
|
28
|
+
let cursor;
|
|
29
|
+
const unique = [...new Set(hostIds)];
|
|
30
|
+
const writesMcpHost = unique.some((id) => [
|
|
31
|
+
'cursor',
|
|
32
|
+
'claude-code',
|
|
33
|
+
'windsurf',
|
|
34
|
+
'cline',
|
|
35
|
+
'zed',
|
|
36
|
+
'jetbrains',
|
|
37
|
+
'copilot',
|
|
38
|
+
'continue',
|
|
39
|
+
'codex',
|
|
40
|
+
'goose',
|
|
41
|
+
'aider',
|
|
42
|
+
].includes(id));
|
|
43
|
+
for (const id of unique) {
|
|
44
|
+
switch (id) {
|
|
45
|
+
case 'cursor': {
|
|
46
|
+
cursor = writeCursorHostFiles(root, platform);
|
|
47
|
+
hosts.push({
|
|
48
|
+
id,
|
|
49
|
+
action: cursor.mcp,
|
|
50
|
+
path: cursor.mcpPath,
|
|
51
|
+
hint: cursor.mcp === 'skipped'
|
|
52
|
+
? 'Could not merge .cursor/mcp.json (invalid JSON). Fix the file and re-run envseal init.'
|
|
53
|
+
: cursor.reloadHint,
|
|
54
|
+
extra: { mcp: cursor.mcp, rules: cursor.rules, rulesPath: cursor.rulesPath },
|
|
55
|
+
});
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
case 'claude-code': {
|
|
59
|
+
mkdirSync(join(root, '.claude'), { recursive: true });
|
|
60
|
+
hosts.push(mcpServersWrite(id, join(root, '.mcp.json'), '.mcp.json', platform, 'Wrote project .mcp.json (Tier B protocol). Restart Claude Code so it picks up envseal-mcp. Install plugins/claude-code for Tier A hooks — doctor reports A only when hooks are visible.'));
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
case 'windsurf': {
|
|
64
|
+
hosts.push(mcpServersWrite(id, join(root, '.windsurf', 'mcp_config.json'), '.windsurf/mcp_config.json', platform, '[VERIFY] Windsurf MCP schema has shifted across releases.'));
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
case 'cline': {
|
|
68
|
+
hosts.push(mcpServersWrite(id, join(root, '.cline', 'mcp_settings.json'), '.cline/mcp_settings.json', platform, '[VERIFY] Cline MCP schema.'));
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
case 'zed': {
|
|
72
|
+
const result = mergeZedSettings(root, platform);
|
|
73
|
+
hosts.push({
|
|
74
|
+
id,
|
|
75
|
+
action: result.action,
|
|
76
|
+
path: result.path,
|
|
77
|
+
hint: result.action === 'skipped'
|
|
78
|
+
? 'Could not merge .zed/settings.json (invalid JSON). Fix the file and re-run envseal init. [VERIFY]'
|
|
79
|
+
: `[VERIFY] Zed MCP key shape. ${RELOAD}`,
|
|
80
|
+
});
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
case 'jetbrains': {
|
|
84
|
+
hosts.push(mcpServersWrite(id, join(root, '.idea', 'mcp.json'), '.idea/mcp.json', platform, '[VERIFY] JetBrains MCP location varies by product.'));
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
case 'copilot': {
|
|
88
|
+
const result = mergeCopilotSettings(root, platform);
|
|
89
|
+
hosts.push({
|
|
90
|
+
id,
|
|
91
|
+
action: result.action,
|
|
92
|
+
path: result.path,
|
|
93
|
+
hint: result.action === 'skipped'
|
|
94
|
+
? 'Could not merge .vscode/settings.json (invalid JSON). Fix the file and re-run envseal init. [VERIFY]'
|
|
95
|
+
: `[VERIFY] github.copilot.mcp schema. ${RELOAD}`,
|
|
96
|
+
});
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
case 'continue': {
|
|
100
|
+
const result = writeContinueProjectFiles(root, platform);
|
|
101
|
+
hosts.push({
|
|
102
|
+
id,
|
|
103
|
+
action: result.action,
|
|
104
|
+
path: result.path,
|
|
105
|
+
hint: `${result.printOnlyHint}\n\nMerge into ~/.continue/config.yaml:\n${continueSnippetYaml(platform)}`,
|
|
106
|
+
});
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
case 'codex': {
|
|
110
|
+
const result = writeCodexProjectConfig(root, platform);
|
|
111
|
+
hosts.push({
|
|
112
|
+
id,
|
|
113
|
+
action: result.action,
|
|
114
|
+
path: result.path,
|
|
115
|
+
hint: `[VERIFY] Codex may still read ~/.codex/config.toml; envseal does not write $HOME. ${RELOAD}`,
|
|
116
|
+
});
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
case 'goose': {
|
|
120
|
+
const result = writeGooseMarker(root);
|
|
121
|
+
hosts.push({
|
|
122
|
+
id,
|
|
123
|
+
action: 'printed',
|
|
124
|
+
path: result.path,
|
|
125
|
+
hint: `Goose is not OOTB. ${result.addHint}\n\nOr merge (unverified yaml):\n${gooseYamlSnippet(platform)}`,
|
|
126
|
+
});
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case 'aider': {
|
|
130
|
+
const result = mergeAiderConf(root);
|
|
131
|
+
hosts.push({
|
|
132
|
+
id,
|
|
133
|
+
action: result.action,
|
|
134
|
+
path: result.path,
|
|
135
|
+
hint: 'Merged Aider config so .env is not on the read list. Use /run envseal ensure.',
|
|
136
|
+
});
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
case 'generic':
|
|
140
|
+
case 'unknown':
|
|
141
|
+
case 'openhands':
|
|
142
|
+
hosts.push({
|
|
143
|
+
id,
|
|
144
|
+
action: 'printed',
|
|
145
|
+
hint: id === 'openhands'
|
|
146
|
+
? 'OpenHands is deployment-dependent (binary inside sandbox, TTY for ensure). Layer 1 AGENTS.md is the working path.'
|
|
147
|
+
: 'Layer 1 only (AGENTS.md). Any agent that reads instruction files can run envseal ensure / envseal run --.',
|
|
148
|
+
});
|
|
149
|
+
break;
|
|
150
|
+
default:
|
|
151
|
+
hosts.push({
|
|
152
|
+
id,
|
|
153
|
+
action: 'printed',
|
|
154
|
+
hint: 'Unknown host: Layer 1 AGENTS.md only. Re-run from the IDE or `envseal init --host <name>`.',
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
agentsMd,
|
|
160
|
+
hosts,
|
|
161
|
+
cursor,
|
|
162
|
+
bareTerminal: !writesMcpHost,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=apply.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type McpInspection, type McpWriteAction } from './mcp.js';
|
|
2
|
+
export declare function codexTomlBlock(platform?: NodeJS.Platform): string;
|
|
3
|
+
export declare function writeCodexProjectConfig(root: string, platform?: NodeJS.Platform): {
|
|
4
|
+
action: McpWriteAction;
|
|
5
|
+
path: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function inspectCodexProject(root: string): McpInspection;
|
|
8
|
+
//# sourceMappingURL=codex.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { mcpLaunch } from './mcp.js';
|
|
4
|
+
export function codexTomlBlock(platform = process.platform) {
|
|
5
|
+
const launch = mcpLaunch(platform);
|
|
6
|
+
const args = launch.args.map((a) => `"${a}"`).join(', ');
|
|
7
|
+
return `[mcp_servers.envseal-mcp]
|
|
8
|
+
command = "${launch.command}"
|
|
9
|
+
args = [${args}]
|
|
10
|
+
`;
|
|
11
|
+
}
|
|
12
|
+
export function writeCodexProjectConfig(root, platform = process.platform) {
|
|
13
|
+
const path = join(root, '.codex', 'config.toml');
|
|
14
|
+
const block = `# envseal MCP — [VERIFY] Codex schema is still moving. Prefer project
|
|
15
|
+
# .codex/ over ~/.codex/config.toml (global cwd is $HOME).
|
|
16
|
+
${codexTomlBlock(platform)}`;
|
|
17
|
+
if (!existsSync(path)) {
|
|
18
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
19
|
+
writeFileSync(path, block, 'utf8');
|
|
20
|
+
return { action: 'created', path };
|
|
21
|
+
}
|
|
22
|
+
const existing = readFileSync(path, 'utf8');
|
|
23
|
+
if (/mcp_servers\.envseal-mcp|envseal-mcp/.test(existing)) {
|
|
24
|
+
return { action: 'unchanged', path };
|
|
25
|
+
}
|
|
26
|
+
const trimmed = existing.replace(/\s+$/u, '');
|
|
27
|
+
writeFileSync(path, `${trimmed}\n\n${block}`, 'utf8');
|
|
28
|
+
return { action: 'merged', path };
|
|
29
|
+
}
|
|
30
|
+
export function inspectCodexProject(root) {
|
|
31
|
+
const path = join(root, '.codex', 'config.toml');
|
|
32
|
+
const hint = 'Run `envseal init` to write project .codex/config.toml. [VERIFY] Codex may still only read ~/.codex/config.toml — envseal does not write $HOME.';
|
|
33
|
+
if (!existsSync(path)) {
|
|
34
|
+
return {
|
|
35
|
+
wired: false,
|
|
36
|
+
status: 'absent',
|
|
37
|
+
commandOk: null,
|
|
38
|
+
message: `.codex/config.toml is missing. ${hint}`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const text = readFileSync(path, 'utf8');
|
|
42
|
+
if (!/envseal-mcp/.test(text)) {
|
|
43
|
+
return {
|
|
44
|
+
wired: false,
|
|
45
|
+
status: 'missing',
|
|
46
|
+
commandOk: null,
|
|
47
|
+
message: `.codex/config.toml has no envseal-mcp. ${hint}`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
wired: true,
|
|
52
|
+
status: 'wired',
|
|
53
|
+
commandOk: null,
|
|
54
|
+
message: 'Project .codex/config.toml names envseal-mcp. [VERIFY: Codex may ignore project files.]',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=codex.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type McpInspection, type McpWriteAction } from './mcp.js';
|
|
2
|
+
export declare function continueSnippetYaml(platform?: NodeJS.Platform): string;
|
|
3
|
+
export declare function writeContinueProjectFiles(root: string, platform?: NodeJS.Platform): {
|
|
4
|
+
action: McpWriteAction;
|
|
5
|
+
path: string;
|
|
6
|
+
printOnlyHint: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function inspectContinueProject(root: string): McpInspection;
|
|
9
|
+
//# sourceMappingURL=continue.d.ts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { mcpLaunch } from './mcp.js';
|
|
4
|
+
export function continueSnippetYaml(platform = process.platform) {
|
|
5
|
+
const launch = mcpLaunch(platform);
|
|
6
|
+
const args = launch.args.map((a) => `"${a}"`).join(', ');
|
|
7
|
+
return `mcpServers:
|
|
8
|
+
- name: envseal-mcp
|
|
9
|
+
command: ${launch.command}
|
|
10
|
+
args: [${args}]
|
|
11
|
+
`;
|
|
12
|
+
}
|
|
13
|
+
export function writeContinueProjectFiles(root, platform = process.platform) {
|
|
14
|
+
const dir = join(root, '.continue');
|
|
15
|
+
mkdirSync(dir, { recursive: true });
|
|
16
|
+
const path = join(dir, 'config.yaml');
|
|
17
|
+
const snippet = continueSnippetYaml(platform);
|
|
18
|
+
const printOnlyHint = 'Continue loads MCP from ~/.continue/config.yaml (global), not this project file. Merge the printed block into the global config; envseal does not write $HOME files. [VERIFY]';
|
|
19
|
+
if (!existsSync(path)) {
|
|
20
|
+
const body = `# Project copy of the envseal MCP snippet. Continue currently reads
|
|
21
|
+
# ~/.continue/config.yaml — merge the mcpServers block there. Do not copy this
|
|
22
|
+
# into $HOME from envseal init; this file exists so doctor can see the host.
|
|
23
|
+
#
|
|
24
|
+
# [VERIFY: recent Continue builds moved to a new HUB config schema; if a
|
|
25
|
+
# top-level mcpServers list is not accepted, use experimental.mcpServers.]
|
|
26
|
+
|
|
27
|
+
${snippet}`;
|
|
28
|
+
writeFileSync(path, body, 'utf8');
|
|
29
|
+
return { action: 'created', path, printOnlyHint };
|
|
30
|
+
}
|
|
31
|
+
const existing = readFileSync(path, 'utf8');
|
|
32
|
+
if (/envseal-mcp/.test(existing) && /@envseal\/mcp-server|npx/.test(existing)) {
|
|
33
|
+
return { action: 'unchanged', path, printOnlyHint };
|
|
34
|
+
}
|
|
35
|
+
if (/envseal-mcp/.test(existing)) {
|
|
36
|
+
return { action: 'unchanged', path, printOnlyHint };
|
|
37
|
+
}
|
|
38
|
+
const trimmed = existing.replace(/\s+$/u, '');
|
|
39
|
+
writeFileSync(path, `${trimmed}\n\n${snippet}`, 'utf8');
|
|
40
|
+
return { action: 'merged', path, printOnlyHint };
|
|
41
|
+
}
|
|
42
|
+
export function inspectContinueProject(root) {
|
|
43
|
+
const path = join(root, '.continue', 'config.yaml');
|
|
44
|
+
const hasSnippet = existsSync(path) && /envseal-mcp/.test(readFileSync(path, 'utf8'));
|
|
45
|
+
// Continue's documented MCP file is ~/.continue/config.yaml. envseal never
|
|
46
|
+
// writes $HOME, so project MCP is not OOTB regardless of the snippet file.
|
|
47
|
+
return {
|
|
48
|
+
wired: false,
|
|
49
|
+
status: hasSnippet ? 'missing' : 'absent',
|
|
50
|
+
commandOk: null,
|
|
51
|
+
message: 'Continue is not OOTB: merge the envseal mcpServers block into ~/.continue/config.yaml (envseal does not write $HOME). Project .continue/ is a detection marker + copy-paste snippet. [VERIFY]',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=continue.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type McpInspection, type McpWriteAction } from './mcp.js';
|
|
2
|
+
export declare function mergeCopilotSettings(root: string, platform?: NodeJS.Platform): {
|
|
3
|
+
action: McpWriteAction;
|
|
4
|
+
path: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function inspectCopilotSettings(root: string, options?: {
|
|
7
|
+
probe?: boolean;
|
|
8
|
+
platform?: NodeJS.Platform;
|
|
9
|
+
}): McpInspection;
|
|
10
|
+
//# sourceMappingURL=copilot.d.ts.map
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { ENVSEAL_MCP_SERVER_NAME, isRecord, mcpLaunch, parseJsonObject, writeJson, isEmptyEnvsealStub, isStockNpxLaunch, looksLikeEnvsealServer, probeVersion, } from './mcp.js';
|
|
4
|
+
function asServerList(value) {
|
|
5
|
+
if (value === undefined)
|
|
6
|
+
return undefined;
|
|
7
|
+
if (!Array.isArray(value))
|
|
8
|
+
return undefined;
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
function entryName(entry) {
|
|
12
|
+
if (!isRecord(entry))
|
|
13
|
+
return undefined;
|
|
14
|
+
if (typeof entry.name === 'string')
|
|
15
|
+
return entry.name;
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
export function mergeCopilotSettings(root, platform = process.platform) {
|
|
19
|
+
const path = join(root, '.vscode', 'settings.json');
|
|
20
|
+
const launch = { name: ENVSEAL_MCP_SERVER_NAME, ...mcpLaunch(platform) };
|
|
21
|
+
if (!existsSync(path)) {
|
|
22
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
23
|
+
writeJson(path, { 'github.copilot.mcp': [launch] });
|
|
24
|
+
return { action: 'created', path };
|
|
25
|
+
}
|
|
26
|
+
const parsed = parseJsonObject(readFileSync(path, 'utf8'));
|
|
27
|
+
if (parsed === null) {
|
|
28
|
+
return { action: 'skipped', path };
|
|
29
|
+
}
|
|
30
|
+
const existing = asServerList(parsed['github.copilot.mcp']);
|
|
31
|
+
if (parsed['github.copilot.mcp'] !== undefined && existing === undefined) {
|
|
32
|
+
return { action: 'skipped', path };
|
|
33
|
+
}
|
|
34
|
+
const list = existing === undefined ? [] : [...existing];
|
|
35
|
+
const index = list.findIndex((item) => entryName(item) === ENVSEAL_MCP_SERVER_NAME);
|
|
36
|
+
if (index === -1) {
|
|
37
|
+
list.push(launch);
|
|
38
|
+
writeJson(path, { ...parsed, 'github.copilot.mcp': list });
|
|
39
|
+
return { action: 'merged', path };
|
|
40
|
+
}
|
|
41
|
+
const current = list[index];
|
|
42
|
+
if (isEmptyEnvsealStub(current) || isStockNpxLaunch(current)) {
|
|
43
|
+
if (isStockNpxLaunch(current) && isRecord(current) && current.command === launch.command) {
|
|
44
|
+
return { action: 'unchanged', path };
|
|
45
|
+
}
|
|
46
|
+
list[index] = isRecord(current) ? { ...current, ...launch } : launch;
|
|
47
|
+
writeJson(path, { ...parsed, 'github.copilot.mcp': list });
|
|
48
|
+
return { action: 'merged', path };
|
|
49
|
+
}
|
|
50
|
+
return { action: 'unchanged', path };
|
|
51
|
+
}
|
|
52
|
+
export function inspectCopilotSettings(root, options = {}) {
|
|
53
|
+
const path = join(root, '.vscode', 'settings.json');
|
|
54
|
+
const hint = 'Run `envseal init` to merge github.copilot.mcp with envseal-mcp (npx @envseal/mcp-server). [VERIFY]';
|
|
55
|
+
if (!existsSync(path)) {
|
|
56
|
+
return {
|
|
57
|
+
wired: false,
|
|
58
|
+
status: 'absent',
|
|
59
|
+
commandOk: null,
|
|
60
|
+
message: `.vscode/settings.json is missing. ${hint}`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const parsed = parseJsonObject(readFileSync(path, 'utf8'));
|
|
64
|
+
if (parsed === null) {
|
|
65
|
+
return {
|
|
66
|
+
wired: false,
|
|
67
|
+
status: 'unreadable',
|
|
68
|
+
commandOk: null,
|
|
69
|
+
message: `.vscode/settings.json is not valid JSON. ${hint}`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const list = asServerList(parsed['github.copilot.mcp']);
|
|
73
|
+
if (list === undefined || list.length === 0) {
|
|
74
|
+
return {
|
|
75
|
+
wired: false,
|
|
76
|
+
status: 'missing',
|
|
77
|
+
commandOk: null,
|
|
78
|
+
message: `.vscode/settings.json has no github.copilot.mcp envseal entry. ${hint}`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const entry = list.find((item) => entryName(item) === ENVSEAL_MCP_SERVER_NAME);
|
|
82
|
+
if (entry === undefined || isEmptyEnvsealStub(entry) || !looksLikeEnvsealServer(entry)) {
|
|
83
|
+
return {
|
|
84
|
+
wired: false,
|
|
85
|
+
status: entry === undefined ? 'missing' : 'stub',
|
|
86
|
+
commandOk: null,
|
|
87
|
+
message: `.vscode/settings.json github.copilot.mcp has no working envseal-mcp. ${hint}`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const rec = entry;
|
|
91
|
+
const commandOk = options.probe === true ? probeVersion(rec) : null;
|
|
92
|
+
let message = 'Copilot MCP is wired (project .vscode/settings.json github.copilot.mcp). [VERIFY]';
|
|
93
|
+
if (commandOk === false) {
|
|
94
|
+
message =
|
|
95
|
+
'Copilot MCP is configured, but the launch command did not report a version. Run `envseal init`. [VERIFY]';
|
|
96
|
+
}
|
|
97
|
+
return { wired: true, status: 'wired', commandOk, message };
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=copilot.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
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 declare const CURSOR_RULES_MDC = "---\ndescription: envseal \u2014 never read .env, never echo secrets; use env_describe/env_request instead.\nalwaysApply: true\n---\n\nCursor rules are advisory, not enforced. Treat these as hard rules regardless.\n\n- Never read, write, or print the contents of `.env` or any `.env.*` file (`.env.example` is the only exception \u2014 it contains placeholders, never values).\n- 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.\n- Never `cat`, `head`, `tail`, `less`, `grep`, `xxd`, or `base64` a secrets file (`*.pem`, `*.key`, `credentials.json`, `secrets.json`, `.envseal/*`).\n- Never ask the user to paste an API key into the chat.\n\nInstead, use the envseal broker via MCP:\n\n- `env_describe` \u2014 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.\n- `env_request` \u2014 collect the missing keys from the user through a secure prompt. It returns a ticket, never a value; follow up with `env_await`.\n- `env_declare` \u2014 declare a key this project needs, with format and provider metadata.\n- `env_verify` \u2014 test that a key actually works (classified result only).\n- `env_use` \u2014 run a command with secrets injected into the child process; output is redacted.\n- `env_revoke` \u2014 remove a key and get the rotation URL.\n\nIf a required key is missing, `env_request` it. Never read `.env` to find it, and never print it back.\n";
|
|
9
|
+
//# sourceMappingURL=cursor-rules.d.ts.map
|