0595794385
- Default export is { id, setup, server }: OpenCode 2 calls setup(ctx), OpenCode 1 (1.18.29+) calls server()
- Tools defined once in lib/tools.ts and registered for both versions (V2 with codemode: false)
- Blocking covers path args of any tool, patch/apply_patch envelopes, shell commands naming env files, grep output, and V2 permission checks
- Secret-looking values from the project's env files are replaced in every non-envsitter tool's output
- Protected files come from envsitter's path policy; example/template files stay readable
- npm run test:e2e drives the real OpenCode 1 and 2 CLIs with a scripted model
- @opencode-ai/plugin ^1.19.3, @opencode/plugin ^2.1.0 (peer >=2.0.32), envsitter ^0.1.0, TypeScript 6
BREAKING CHANGE: the named export EnvSitterGuard is now the plugin definition object; call EnvSitterGuard.server(input) instead of EnvSitterGuard(input).
174 lines
5.8 KiB
TypeScript
174 lines
5.8 KiB
TypeScript
export const HELP_TOPICS = ["overview", "reading", "matching", "mutations", "file_ops", "all"] as const;
|
|
|
|
export type HelpTopic = (typeof HELP_TOPICS)[number];
|
|
|
|
const overview = `
|
|
## EnvSitter Tools Overview
|
|
|
|
EnvSitter provides safe .env file operations that NEVER expose secret values.
|
|
All tools return keys, booleans, line numbers, and operation plans only.
|
|
|
|
### Why Use EnvSitter?
|
|
- Direct access to sensitive env files (\`.env\`, \`.env.*\`, \`*.env\`) is BLOCKED to prevent secret leaks:
|
|
reads, edits/writes/patches, shell commands that name them, and grep matches inside them
|
|
- Secret-looking values from those files are replaced with \`[envsitter: KEY redacted]\` in other tools' output
|
|
- Example/template files (\`.env.example\`, \`.env.sample\`, \`.env.template\`, \`.env.dist\`, \`.env.default(s)\`) stay readable
|
|
- These tools let you inspect, validate, and modify env files safely
|
|
- File modifications are dry-run by default; use \`write: true\` to apply
|
|
|
|
### Tool Categories
|
|
- **Reading**: envsitter_keys, envsitter_fingerprint, envsitter_scan
|
|
- **Matching**: envsitter_match, envsitter_match_by_key
|
|
- **Mutations**: envsitter_add, envsitter_set, envsitter_unset, envsitter_delete
|
|
- **File Ops**: envsitter_validate, envsitter_copy, envsitter_format, envsitter_annotate
|
|
`;
|
|
|
|
const reading = `
|
|
## Reading Tools (never return values)
|
|
|
|
### envsitter_keys
|
|
List all keys in a .env file.
|
|
\`\`\`json
|
|
{ "filePath": ".env", "filterRegex": "/^API_/" }
|
|
\`\`\`
|
|
Returns: \`{ file, keys: string[] }\`
|
|
|
|
### envsitter_fingerprint
|
|
Get a deterministic fingerprint of a key's value (for comparison/auditing).
|
|
\`\`\`json
|
|
{ "filePath": ".env", "key": "DATABASE_URL" }
|
|
\`\`\`
|
|
Returns: \`{ file, key, result: { algorithm, fingerprint, length } }\`
|
|
|
|
### envsitter_scan
|
|
Detect value shapes (JWT, URL, base64) without revealing values.
|
|
\`\`\`json
|
|
{ "filePath": ".env", "detect": ["jwt", "url", "base64"], "keysFilterRegex": "/TOKEN/" }
|
|
\`\`\`
|
|
Returns: \`{ file, findings: [{ key, detections }] }\`
|
|
`;
|
|
|
|
const matching = `
|
|
## Matching Tools (return booleans only)
|
|
|
|
### envsitter_match
|
|
Check if a key's value matches criteria without seeing the value.
|
|
|
|
**Operations** (op parameter):
|
|
- \`exists\`: key is present
|
|
- \`is_empty\`: value is empty string
|
|
- \`is_equal\`: matches candidate exactly (provide \`candidate\` or \`candidateEnvVar\`)
|
|
- \`partial_match_prefix\`: value starts with candidate
|
|
- \`partial_match_suffix\`: value ends with candidate
|
|
- \`partial_match_regex\`: value matches regex pattern
|
|
- \`is_number\`: value is numeric
|
|
- \`is_boolean\`: value is true/false
|
|
- \`is_string\`: value is neither number nor boolean
|
|
|
|
**Selectors** (provide exactly one):
|
|
- \`key\`: single key
|
|
- \`keys\`: array of keys
|
|
- \`allKeys: true\`: all keys in file
|
|
|
|
\`\`\`json
|
|
{ "filePath": ".env", "key": "NODE_ENV", "op": "is_equal", "candidate": "production" }
|
|
{ "filePath": ".env", "keys": ["API_KEY", "SECRET"], "op": "exists" }
|
|
{ "filePath": ".env", "allKeys": true, "op": "is_empty" }
|
|
\`\`\`
|
|
|
|
### envsitter_match_by_key
|
|
Bulk match different candidates against different keys.
|
|
\`\`\`json
|
|
{ "filePath": ".env", "candidatesByKey": { "API_KEY": "sk-xxx", "DB_PASS": "secret123" } }
|
|
\`\`\`
|
|
Returns: \`{ file, matches: [{ key, match: boolean }] }\`
|
|
`;
|
|
|
|
const mutations = `
|
|
## Mutation Tools (modify .env files safely)
|
|
|
|
All mutation tools are DRY-RUN by default. Set \`write: true\` to apply changes.
|
|
Output includes operation plan with line numbers, never values.
|
|
|
|
### envsitter_add
|
|
Add a NEW key (fails if key already exists).
|
|
\`\`\`json
|
|
{ "filePath": ".env", "key": "NEW_KEY", "value": "some-value", "write": true }
|
|
\`\`\`
|
|
Returns: \`{ file, key, hasChanges, plan: { action: "added"|"key_exists" } }\`
|
|
|
|
### envsitter_set
|
|
Create or update a key (upsert behavior).
|
|
\`\`\`json
|
|
{ "filePath": ".env", "key": "API_KEY", "value": "new-value", "write": true }
|
|
\`\`\`
|
|
Returns: \`{ file, key, hasChanges, plan: { action: "added"|"updated"|"no_change" } }\`
|
|
|
|
### envsitter_unset
|
|
Set a key to empty string (keeps the key line).
|
|
\`\`\`json
|
|
{ "filePath": ".env", "key": "OLD_KEY", "write": true }
|
|
\`\`\`
|
|
Returns: \`{ file, key, hasChanges, plan: { action: "unset"|"not_found" } }\`
|
|
|
|
### envsitter_delete
|
|
Remove key(s) entirely from the file.
|
|
\`\`\`json
|
|
{ "filePath": ".env", "keys": ["OLD_KEY", "UNUSED_KEY"], "write": true }
|
|
\`\`\`
|
|
Returns: \`{ file, keys, hasChanges, plan: [{ key, action: "deleted"|"not_found" }] }\`
|
|
`;
|
|
|
|
const fileOps = `
|
|
## File Operation Tools
|
|
|
|
### envsitter_validate
|
|
Check .env file syntax for errors.
|
|
\`\`\`json
|
|
{ "filePath": ".env" }
|
|
\`\`\`
|
|
Returns: \`{ file, ok: boolean, issues: [{ line, column, message }] }\`
|
|
|
|
### envsitter_copy
|
|
Copy keys between .env files. Dry-run unless \`write: true\`.
|
|
\`\`\`json
|
|
{
|
|
"from": ".env.production",
|
|
"to": ".env.staging",
|
|
"keys": ["API_URL", "REDIS_URL"],
|
|
"onConflict": "overwrite",
|
|
"write": true
|
|
}
|
|
\`\`\`
|
|
Options: \`includeRegex\`, \`excludeRegex\`, \`rename\` (e.g., "OLD=NEW,A=B")
|
|
Returns: \`{ from, to, hasChanges, plan: [{ fromKey, toKey, action }] }\`
|
|
|
|
### envsitter_format / envsitter_reorder
|
|
Format/reorder a .env file. Dry-run unless \`write: true\`.
|
|
\`\`\`json
|
|
{ "filePath": ".env", "mode": "sections", "sort": "alpha", "write": true }
|
|
\`\`\`
|
|
Modes: \`sections\` (preserve section groupings), \`global\` (treat as flat list)
|
|
Sort: \`alpha\` (alphabetical), \`none\` (preserve order within sections)
|
|
|
|
### envsitter_annotate
|
|
Add a comment above a key. Dry-run unless \`write: true\`.
|
|
\`\`\`json
|
|
{ "filePath": ".env", "key": "DATABASE_URL", "comment": "Production DB only", "write": true }
|
|
\`\`\`
|
|
Returns: \`{ file, key, hasChanges, plan: { action: "inserted"|"updated"|"not_found" } }\`
|
|
`;
|
|
|
|
const sections: Record<Exclude<HelpTopic, "all">, string> = {
|
|
overview,
|
|
reading,
|
|
matching,
|
|
mutations,
|
|
file_ops: fileOps,
|
|
};
|
|
|
|
export function helpText(topic: HelpTopic): string {
|
|
if (topic === "all") return [overview, reading, matching, mutations, fileOps].join("\n---\n");
|
|
return sections[topic];
|
|
}
|