@envseal/cli 0.1.4 → 0.1.5

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 CHANGED
@@ -13,7 +13,7 @@ import { doctor } from './commands/doctor.js';
13
13
  import { revoke } from './commands/revoke.js';
14
14
  import { mcp } from './commands/mcp.js';
15
15
  import { init } from './commands/init.js';
16
- const VERSION = '0.1.4';
16
+ const VERSION = '0.1.5';
17
17
  async function main() {
18
18
  const argv = process.argv.slice(2);
19
19
  if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
@@ -137,7 +137,7 @@ function showHelp() {
137
137
  Usage: envseal <command> [options]
138
138
 
139
139
  Commands:
140
- init [--host <name>] Initialize env.schema.jsonc
140
+ init [--host <name>] Initialize manifest, AGENTS.md, and host MCP
141
141
  ensure [--check] Prompt for all missing required keys
142
142
  (--check: report only, exit 0/1, never prompt)
143
143
  set <KEY> Prompt for a single key
package/dist/cli-utils.js CHANGED
@@ -161,9 +161,9 @@ export function parseArgs(argv) {
161
161
  const COMMAND_USAGE = {
162
162
  init: `Usage: envseal init [--host <name>] [--json] [--project <path>]
163
163
 
164
- Initialize env.schema.jsonc, declaring every environment-variable reference found by scanning the project.
164
+ Initialize env.schema.jsonc, merge AGENTS.md (Layer 1), and write project host MCP/config for every matching marker.
165
165
 
166
- --host <name> Override host detection. Valid values: claude-code, cursor, continue, aider, windsurf, cline, zed, codex, jetbrains, goose, copilot, generic, unknown.
166
+ --host <name> Write this host's project config (comma-separated ok). Valid values: claude-code, cursor, continue, aider, windsurf, cline, zed, codex, jetbrains, goose, copilot, generic, unknown, openhands.
167
167
  --json Output as JSON.
168
168
  --project <path> Project root (default: auto-detect).`,
169
169
  ensure: `Usage: envseal ensure [--check] [--json] [--project <path>]
@@ -202,8 +202,8 @@ Asks for confirmation first; --yes (or ENVSEAL_ASSUME_YES=1) pre-approves it.
202
202
  --project <path> Project root (default: auto-detect).`,
203
203
  doctor: `Usage: envseal doctor [--json] [--project <path>]
204
204
 
205
- Audit the project configuration: detected host and tier, gitignore coverage,
206
- file permissions, missing required keys.
205
+ Audit the project configuration: detected host and tier, agent wiring (MCP +
206
+ AGENTS.md), gitignore coverage, file permissions, missing required keys.
207
207
 
208
208
  --json Output as JSON.
209
209
  --project <path> Project root (default: auto-detect).`,
@@ -7,6 +7,7 @@ import { EXIT } from '../exit-codes.js';
7
7
  import { detectHost } from '../host.js';
8
8
  import { createBroker } from '../cli-utils.js';
9
9
  import { finish } from '../exit.js';
10
+ import { inspectPrimaryHostWiring, wiringFailsDoctor } from '../host-wiring/inspect.js';
10
11
  export async function doctor(root, json) {
11
12
  try {
12
13
  // An audit of a project with no configuration would report an empty,
@@ -35,6 +36,7 @@ export async function doctor(root, json) {
35
36
  envFileOk = (stats.mode & 0o077) === 0;
36
37
  }
37
38
  const host = detectHost(root);
39
+ const inspection = inspectPrimaryHostWiring(root, host.id, { probe: true });
38
40
  const output = {
39
41
  projectRoot: root,
40
42
  manifestPath,
@@ -45,6 +47,7 @@ export async function doctor(root, json) {
45
47
  reason: host.reason,
46
48
  recommendation: host.recommendation,
47
49
  },
50
+ agentWiring: inspection.wiring,
48
51
  gitignore: {
49
52
  exists: existsSync(gitignorePath),
50
53
  covers: gitignoreCovers,
@@ -57,12 +60,27 @@ export async function doctor(root, json) {
57
60
  hookFailClosed,
58
61
  missingRequiredCount: status.missingRequired.length,
59
62
  missingRequired: status.missingRequired,
63
+ ...(inspection.mcp === undefined
64
+ ? {}
65
+ : {
66
+ mcp: {
67
+ wired: inspection.mcp.wired,
68
+ status: inspection.mcp.status,
69
+ message: inspection.mcp.message,
70
+ commandOk: inspection.mcp.commandOk,
71
+ },
72
+ }),
60
73
  };
61
74
  if (!json) {
62
75
  console.log(`Project root: ${root}`);
63
76
  console.log(`Host: ${host.name} (Tier ${host.tier})`);
64
77
  console.log(` ${host.reason}`);
65
78
  console.log(` ${host.recommendation}`);
79
+ console.log(`Agent wiring: MCP ${inspection.wiring.mcp}, instructions ${inspection.wiring.instructions}`);
80
+ if (inspection.notOotb) {
81
+ console.log(' This host is not OOTB (print-only MCP). Layer 1 AGENTS.md is the working path.');
82
+ }
83
+ console.log(` ${inspection.message}`);
66
84
  console.log(`Gitignore covers .env: ${gitignoreCovers ? 'yes' : 'no'}`);
67
85
  console.log(`Hook on internal error: ${hookFailClosed ? 'fail-closed' : 'fail-open (default)'}`);
68
86
  console.log(`Missing required keys: ${status.missingRequired.length}`);
@@ -75,8 +93,7 @@ export async function doctor(root, json) {
75
93
  else {
76
94
  emit(json, '', output);
77
95
  }
78
- // Exit with UNSATISFIED if required keys are missing
79
- if (status.missingRequired.length > 0) {
96
+ if (status.missingRequired.length > 0 || wiringFailsDoctor(inspection)) {
80
97
  finish(EXIT.UNSATISFIED);
81
98
  return;
82
99
  }
@@ -1,32 +1,16 @@
1
1
  import { projectPaths, loadManifest, declareEntries, scanManifestEntry } from '@envseal/core';
2
2
  import { SepError } from '@envseal/protocol';
3
3
  import { emit, fail } from '../output.js';
4
- import { detectHost } from '../host.js';
4
+ import { detectHost, resolveInitHostIds } from '../host.js';
5
5
  import { scanForEnvKeys, entryForKey } from '../scan.js';
6
6
  import { EXIT } from '../exit-codes.js';
7
7
  import { finish } from '../exit.js';
8
- // The ids detectHost can ever return. --host used to accept any string
9
- // silently, recording a host detection would never report and printing a tier
10
- // computed for a fiction.
11
- const KNOWN_HOST_IDS = [
12
- 'claude-code',
13
- 'cursor',
14
- 'continue',
15
- 'aider',
16
- 'windsurf',
17
- 'cline',
18
- 'zed',
19
- 'codex',
20
- 'jetbrains',
21
- 'goose',
22
- 'copilot',
23
- 'generic',
24
- 'unknown',
25
- ];
8
+ import { applyHostWiring } from '../host-wiring/apply.js';
26
9
  export async function init(root, json, hostOverride) {
27
10
  try {
28
- if (hostOverride !== undefined && !KNOWN_HOST_IDS.includes(hostOverride)) {
29
- console.error(`Error: unknown --host '${hostOverride}'. Valid values: ${KNOWN_HOST_IDS.join(', ')}.`);
11
+ const resolved = resolveInitHostIds(root, hostOverride);
12
+ if (resolved.error !== undefined) {
13
+ console.error(`Error: ${resolved.error}`);
30
14
  finish(EXIT.USAGE);
31
15
  return;
32
16
  }
@@ -60,13 +44,19 @@ export async function init(root, json, hostOverride) {
60
44
  // below must be true, not aspirational.
61
45
  const result = declareEntries(paths, entries);
62
46
  const manifest = loadManifest(paths);
63
- const host = hostOverride
64
- ? { id: hostOverride, name: hostOverride, tier: 'C', reason: 'specified with --host', recommendation: '' }
65
- : detectHost(root);
47
+ const wiring = applyHostWiring(root, resolved.ids);
48
+ // Evidence after write: --host cursor on a bare tree now has `.cursor/`.
49
+ // Never invent a fake tier from the flag alone.
50
+ const detected = detectHost(root);
51
+ const cursorEntry = wiring.hosts.find((h) => h.id === 'cursor');
52
+ const cursorWiring = wiring.cursor;
66
53
  const output = {
67
54
  manifestPath: paths.manifest,
68
- host: host.id,
69
- protectionTier: host.tier,
55
+ host: detected.id,
56
+ protectionTier: detected.tier,
57
+ requestedHosts: resolved.source === 'flag' ? resolved.ids : undefined,
58
+ wiredHosts: resolved.ids,
59
+ wiringSource: resolved.source,
70
60
  scanned: discovered.length,
71
61
  added: result.added,
72
62
  updated: result.updated,
@@ -74,6 +64,25 @@ export async function init(root, json, hostOverride) {
74
64
  secretKeys: discovered.filter((d) => d.secret).map((d) => d.key),
75
65
  configKeys: discovered.filter((d) => !d.secret).map((d) => d.key),
76
66
  entries: manifest?.entries.length ?? 0,
67
+ agentsMd: {
68
+ action: wiring.agentsMd.action,
69
+ path: wiring.agentsMd.path,
70
+ },
71
+ hostWiring: wiring.hosts.map((h) => ({
72
+ id: h.id,
73
+ action: h.action,
74
+ path: h.path,
75
+ })),
76
+ ...(cursorWiring === undefined
77
+ ? {}
78
+ : {
79
+ cursorWiring: {
80
+ mcp: cursorWiring.mcp,
81
+ rules: cursorWiring.rules,
82
+ mcpPath: cursorWiring.mcpPath,
83
+ rulesPath: cursorWiring.rulesPath,
84
+ },
85
+ }),
77
86
  };
78
87
  if (json) {
79
88
  emit(json, '', output);
@@ -96,23 +105,42 @@ export async function init(root, json, hostOverride) {
96
105
  console.log(` Config (not prompted): ${config.map((s) => s.key).join(', ')}`);
97
106
  }
98
107
  }
99
- console.log(` Host: ${host.name} (protection tier ${host.tier})`);
100
- if (host.recommendation)
101
- console.log(` ${host.recommendation}`);
102
- if (hostOverride) {
103
- // The override line above is what was ASKED for, not what is here. An
104
- // auto-detected init on the same project can print a different tier, and
105
- // doctor is the one that reports evidence.
106
- console.log(' Override recorded; envseal doctor reports what is actually detected.');
108
+ console.log(` AGENTS.md: ${wiring.agentsMd.action} (Layer 1 — envseal ensure / envseal run --)`);
109
+ console.log(` Detected host: ${detected.name} (protection tier ${detected.tier})`);
110
+ console.log(` ${detected.reason}`);
111
+ if (detected.recommendation)
112
+ console.log(` ${detected.recommendation}`);
113
+ if (resolved.source === 'flag') {
114
+ console.log(` Requested host(s): ${resolved.ids.join(', ')}. Override recorded; envseal doctor reports what is actually detected.`);
107
115
  }
108
- if (host.id === 'claude-code') {
109
- // Without this the first run ends at a manifest and no connection: init
110
- // writes env.schema.jsonc but nothing tells the user the agent still has
111
- // to be pointed at the broker.
112
- console.log('');
113
- console.log('Connect your agent: create .mcp.json in the project root containing');
114
- console.log(' {"mcpServers":{"envseal-mcp":{"command":"envseal-mcp","args":[]}}}');
115
- console.log('then restart Claude Code — or install plugins/claude-code for Tier A hooks.');
116
+ if (resolved.source === 'none') {
117
+ console.log(' No project host markers and this process is not an IDE.');
118
+ console.log(' Wrote AGENTS.md only. Re-run from the IDE, or `envseal init --host cursor`.');
119
+ }
120
+ else if (resolved.ids.length > 0) {
121
+ console.log(` Wired host(s): ${resolved.ids.join(', ')} (${resolved.source})`);
122
+ }
123
+ for (const entry of wiring.hosts) {
124
+ if (entry.hint) {
125
+ for (const line of entry.hint.split('\n')) {
126
+ console.log(` ${line}`);
127
+ }
128
+ }
129
+ }
130
+ if (cursorEntry === undefined && !wiring.bareTerminal) {
131
+ console.log(' Reload MCP / restart the host, then run `envseal doctor`.');
132
+ }
133
+ else if (cursorWiring !== undefined && cursorWiring.mcp !== 'skipped' && wiring.hosts.length === 1) {
134
+ // Cursor entry already printed reloadHint.
135
+ }
136
+ else if (wiring.bareTerminal && resolved.source === 'none') {
137
+ // Already printed the re-run hint.
138
+ }
139
+ else if (wiring.hosts.some((h) => h.id !== 'cursor')) {
140
+ // Per-host hints already cover reload; keep a single closer.
141
+ }
142
+ if (resolved.ids.includes('claude-code')) {
143
+ console.log(' Claude Code: protocol connected (Tier B) via .mcp.json. Plugin = Tier A.');
116
144
  }
117
145
  }
118
146
  catch (error) {
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Layer 1 instruction file shipped by `envseal init`.
3
+ *
4
+ * Keep this string identical to `plugins/generic/AGENTS.md`. The host-wiring
5
+ * test fails if they drift. Embedding (not reading plugins/ at runtime) is
6
+ * what makes published `@envseal/cli` able to write the file.
7
+ */
8
+ export declare const AGENTS_MD_CONTENT = "# envseal \u2014 instructions for any coding agent\n\nThis repository uses envseal to provision secrets. The secret values must never\nappear in this chat, in your tool output, in files you write, or in commands you\nrun. Follow these rules without exception.\n\n## You must never\n\n- Read, write, or copy the contents of any `.env` or `.env.*` file. (`.env.example`\n is the only exception: it holds placeholders, never values.)\n- Run `printenv`, bare `env`, `export -p`, `set`, or `echo $VAR` / `echo $KEY...`\n to inspect or display environment variables.\n- Use `cat`, `head`, `tail`, `less`, `grep`, `xxd`, `base64`, `sed`, or any other\n reader on a secrets file: `*.pem`, `*.key`, `id_rsa*`, `credentials.json`,\n `secrets.json` / `secrets.yaml` / `secrets.toml`, `.envseal/*`.\n- Ask the user to paste an API key, token, or connection string into the chat.\n- Echo an environment variable into a transcript, log, diff, or tool result.\n\n## How to check which keys exist\n\n```sh\nenvseal status # human-readable\nenvseal status --json # machine-readable; never contains values\n```\n\nStatuses are presence + metadata only (format-valid, length bucket, fingerprint,\nlast verified). There is no way to make envseal print a value.\n\n## How to provision a missing key\n\n```sh\nenvseal ensure # prompts the user for every missing required key\nenvseal set KEY # prompts for a single key\n```\n\nNever ask the user to add a key to `.env` and tell you about it. Run\n`envseal ensure` instead, and wait for its exit code.\n\n## How to run a command that needs secrets\n\n```sh\nenvseal run -- <command...>\n```\n\nSecrets are injected only into that child process and its stdout/stderr are\nredacted. Never run the command plain and never try to read the key yourself.\n\n## How to verify a key actually works\n\n```sh\nenvseal verify # classified results: ok, auth_failed, rate_limited, ...\n```\n\n## How to ask the user what is wrong\n\n```sh\nenvseal doctor # project root, detected host + tier, gitignore, missing keys\n```\n\n## Exit codes (for scripts)\n\n`0` ok \u00B7 `1` required keys missing \u00B7 `2` usage error \u00B7 `3` cancelled \u00B7\n`4` no interactive surface \u00B7 `5` sink failure \u00B7 `6` verification failed.\nUse them; never parse human text for secrets.\n";
9
+ //# sourceMappingURL=agents-md-content.d.ts.map
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Layer 1 instruction file shipped by `envseal init`.
3
+ *
4
+ * Keep this string identical to `plugins/generic/AGENTS.md`. The host-wiring
5
+ * test fails if they drift. Embedding (not reading plugins/ at runtime) is
6
+ * what makes published `@envseal/cli` able to write the file.
7
+ */
8
+ export const AGENTS_MD_CONTENT = `# envseal — instructions for any coding agent
9
+
10
+ This repository uses envseal to provision secrets. The secret values must never
11
+ appear in this chat, in your tool output, in files you write, or in commands you
12
+ run. Follow these rules without exception.
13
+
14
+ ## You must never
15
+
16
+ - Read, write, or copy the contents of any \`.env\` or \`.env.*\` file. (\`.env.example\`
17
+ is the only exception: it holds placeholders, never values.)
18
+ - Run \`printenv\`, bare \`env\`, \`export -p\`, \`set\`, or \`echo $VAR\` / \`echo $KEY...\`
19
+ to inspect or display environment variables.
20
+ - Use \`cat\`, \`head\`, \`tail\`, \`less\`, \`grep\`, \`xxd\`, \`base64\`, \`sed\`, or any other
21
+ reader on a secrets file: \`*.pem\`, \`*.key\`, \`id_rsa*\`, \`credentials.json\`,
22
+ \`secrets.json\` / \`secrets.yaml\` / \`secrets.toml\`, \`.envseal/*\`.
23
+ - Ask the user to paste an API key, token, or connection string into the chat.
24
+ - Echo an environment variable into a transcript, log, diff, or tool result.
25
+
26
+ ## How to check which keys exist
27
+
28
+ \`\`\`sh
29
+ envseal status # human-readable
30
+ envseal status --json # machine-readable; never contains values
31
+ \`\`\`
32
+
33
+ Statuses are presence + metadata only (format-valid, length bucket, fingerprint,
34
+ last verified). There is no way to make envseal print a value.
35
+
36
+ ## How to provision a missing key
37
+
38
+ \`\`\`sh
39
+ envseal ensure # prompts the user for every missing required key
40
+ envseal set KEY # prompts for a single key
41
+ \`\`\`
42
+
43
+ Never ask the user to add a key to \`.env\` and tell you about it. Run
44
+ \`envseal ensure\` instead, and wait for its exit code.
45
+
46
+ ## How to run a command that needs secrets
47
+
48
+ \`\`\`sh
49
+ envseal run -- <command...>
50
+ \`\`\`
51
+
52
+ Secrets are injected only into that child process and its stdout/stderr are
53
+ redacted. Never run the command plain and never try to read the key yourself.
54
+
55
+ ## How to verify a key actually works
56
+
57
+ \`\`\`sh
58
+ envseal verify # classified results: ok, auth_failed, rate_limited, ...
59
+ \`\`\`
60
+
61
+ ## How to ask the user what is wrong
62
+
63
+ \`\`\`sh
64
+ envseal doctor # project root, detected host + tier, gitignore, missing keys
65
+ \`\`\`
66
+
67
+ ## Exit codes (for scripts)
68
+
69
+ \`0\` ok · \`1\` required keys missing · \`2\` usage error · \`3\` cancelled ·
70
+ \`4\` no interactive surface · \`5\` sink failure · \`6\` verification failed.
71
+ Use them; never parse human text for secrets.
72
+ `;
73
+ //# sourceMappingURL=agents-md-content.js.map
@@ -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