@envsave/cli 1.0.45 → 1.0.47
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/README.md +7 -0
- package/dist/SKILL.md +174 -0
- package/dist/cli.js +73 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -185,6 +185,13 @@ envsave passwd
|
|
|
185
185
|
envsave config # show current
|
|
186
186
|
envsave config --max-gets 120 # set max gets/minute
|
|
187
187
|
|
|
188
|
+
# Install the EnvSave skill into an agent platform
|
|
189
|
+
# (--agent defaults to "claude" — codex and others are planned)
|
|
190
|
+
envsave install-skill # interactive prompt for scope
|
|
191
|
+
envsave install-skill --local # → ./.claude/skills/envsave/SKILL.md
|
|
192
|
+
envsave install-skill --global # → ~/.claude/skills/envsave/SKILL.md
|
|
193
|
+
envsave install-skill --agent claude --global # explicit agent (claude only today)
|
|
194
|
+
|
|
188
195
|
# Show version
|
|
189
196
|
envsave --version
|
|
190
197
|
```
|
package/dist/SKILL.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: envsave
|
|
3
|
+
description: Use EnvSave to securely retrieve secrets via opaque keys instead of hardcoding them. Trigger whenever code needs API keys, tokens, passwords, database URLs, connection strings, webhook secrets, credentials, environment variables, or any sensitive value. Also trigger when you see hardcoded secrets in existing code that should be migrated to EnvSave, when .env files are being read directly, or when the user mentions secrets, credentials, or vault.
|
|
4
|
+
user_invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# EnvSave Migration & Usage Skill
|
|
8
|
+
|
|
9
|
+
When this skill is triggered, follow the instructions below. Everything you need to know is in this file. Do not research, explore, or investigate envsave — just execute the steps.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
EnvSave is installed from **npm** (not from a local path on the user's machine). The npm package is scoped: `@envsave/cli`.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Global CLI (the `envsave` binary)
|
|
17
|
+
npm install -g @envsave/cli
|
|
18
|
+
|
|
19
|
+
# As a library in a project
|
|
20
|
+
npm install @envsave/cli
|
|
21
|
+
# or: bun add @envsave/cli
|
|
22
|
+
|
|
23
|
+
# Python
|
|
24
|
+
pip install envsave
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
If the user's project doesn't yet depend on envsave, add `@envsave/cli` to their dependencies before emitting `import` statements. Never tell the user to link from a local checkout — always use the npm package.
|
|
28
|
+
|
|
29
|
+
**Installing this skill in your agent:**
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
envsave install-skill # interactive — asks local or global
|
|
33
|
+
envsave install-skill --local # → ./.claude/skills/envsave/SKILL.md (this project only)
|
|
34
|
+
envsave install-skill --global # → ~/.claude/skills/envsave/SKILL.md (all your projects)
|
|
35
|
+
envsave install-skill --agent claude --global # explicit agent
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`--agent` defaults to `claude`. Codex and other agentic platforms are planned but not yet supported.
|
|
39
|
+
|
|
40
|
+
**Upgrading from the old unscoped package:** if `npm install -g @envsave/cli` fails with `EEXIST` on `bin/envsave`, the user has the old unscoped `envsave` package installed globally (pre-rename). Fix it with:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm uninstall -g envsave
|
|
44
|
+
npm install -g @envsave/cli
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## If user asks to migrate / invokes `/envsave`:
|
|
48
|
+
|
|
49
|
+
Execute these steps in order. Start with Step 1 immediately.
|
|
50
|
+
|
|
51
|
+
### Step 1 — Grep the project NOW
|
|
52
|
+
|
|
53
|
+
Use the Grep tool (not Bash, not Agent, not Explore) to search the current project directory. Run ALL of these searches in parallel in a single response:
|
|
54
|
+
|
|
55
|
+
- `process\.env` in `*.{ts,js,tsx,jsx,mjs,cjs}`
|
|
56
|
+
- `os\.environ|os\.getenv|environ\.get` in `*.py`
|
|
57
|
+
- `dotenv|load_dotenv|from dotenv|python-dotenv` in all source files
|
|
58
|
+
- `sk-[a-zA-Z0-9]{20,}|pk_[a-zA-Z0-9]{20,}|ghp_|gho_|xoxb-|xoxp-|postgres://|mongodb://|redis://|mysql://|amqp://` in all files
|
|
59
|
+
- `readFileSync.*\.env|Bun\.file.*\.env|open.*\.env` in all files
|
|
60
|
+
- Glob for `.env*` files in project root
|
|
61
|
+
|
|
62
|
+
Skip `node_modules/`, `.git/`, `dist/`, `build/`, `__pycache__/`, `.venv/`, `bun.lockb`.
|
|
63
|
+
|
|
64
|
+
### Step 2 — Show findings table
|
|
65
|
+
|
|
66
|
+
| File:Line | Env Var | Current Code | Replacement |
|
|
67
|
+
|-----------|---------|-------------|-------------|
|
|
68
|
+
| src/api.ts:12 | OPENAI_API_KEY | `process.env.OPENAI_API_KEY` | `envsave.get("es_???")` |
|
|
69
|
+
| lib/db.py:5 | DATABASE_URL | `os.getenv("DATABASE_URL")` | `envsave.get("es_???")` |
|
|
70
|
+
|
|
71
|
+
Also list `dotenv`/`load_dotenv` imports to remove and `.env` files found.
|
|
72
|
+
|
|
73
|
+
### Step 3 — Summary + copy-paste command
|
|
74
|
+
|
|
75
|
+
List unique env var names, then output ONE single `envsave reveal` command with all keys (supports multiple args, one password prompt).
|
|
76
|
+
|
|
77
|
+
**IMPORTANT: The command MUST be on a single line with no line breaks.** If the list of keys is long, still keep it on one line. Never wrap or split the command across multiple lines — line breaks will break copy-paste in the terminal.
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
## Env Vars to Migrate
|
|
81
|
+
|
|
82
|
+
| # | Env Var Name | Files |
|
|
83
|
+
|---|-------------|-------|
|
|
84
|
+
| 1 | OPENAI_API_KEY | 3 |
|
|
85
|
+
| 2 | DATABASE_URL | 1 |
|
|
86
|
+
|
|
87
|
+
## Run this to get all opaque keys (single password prompt):
|
|
88
|
+
|
|
89
|
+
envsave reveal OPENAI_API_KEY DATABASE_URL
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Tell user: run the command, paste back the output, and I'll do all code replacements.
|
|
93
|
+
|
|
94
|
+
### Step 4 — Replace code (only after user provides keys)
|
|
95
|
+
|
|
96
|
+
Wait for opaque keys. Then:
|
|
97
|
+
1. Replace `process.env.XXX` / `os.getenv("XXX")` with `envsave.get("es_actual_key")`
|
|
98
|
+
2. Add `import envsave from "@envsave/cli"` (TS/JS) or `import envsave` (Python) if missing
|
|
99
|
+
3. Remove `dotenv`/`load_dotenv` imports and `.config()` calls
|
|
100
|
+
4. List all `.env*` files found (`.env`, `.env.local`, `.env.production`, etc.) and **tell the user they must delete them** — these files contain real secrets in plain text and are no longer needed after migration to envsave. Add them to `.gitignore` if not already there.
|
|
101
|
+
|
|
102
|
+
**IMPORTANT: Always use `envsave.get("es_...")` directly — the full module call. Never create wrapper functions like `envsave_get()`, `get_secret()`, `_get_env()`, or similar. The call should always be `envsave.get("es_...")` exactly.**
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## If user needs a secret in new code:
|
|
107
|
+
|
|
108
|
+
Use `envsave.get("es_???")` as placeholder and ask user to provide the opaque key.
|
|
109
|
+
|
|
110
|
+
**TypeScript/JavaScript:**
|
|
111
|
+
```typescript
|
|
112
|
+
import envsave from "@envsave/cli";
|
|
113
|
+
const apiKey = envsave.get("es_7a3f2b1e9c8d4f6b2a1c3d5f");
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Python:**
|
|
117
|
+
```python
|
|
118
|
+
import envsave
|
|
119
|
+
api_key = envsave.get("es_7a3f2b1e9c8d4f6b2a1c3d5f")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Shell:**
|
|
123
|
+
```bash
|
|
124
|
+
envsave get es_7a3f2b1e9c8d4f6b2a1c3d5f
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Vercel / Cloud Deployment
|
|
128
|
+
|
|
129
|
+
On Vercel there is no envsave vault. When `envsave.get("es_xxx")` can't find the vault, it falls back to `process.env["es_xxx"]`. So you need to set Vercel env vars where:
|
|
130
|
+
|
|
131
|
+
- **Name** = the opaque key (e.g. `es_7a3f2b1e9c8d4f6b`)
|
|
132
|
+
- **Value** = the real secret (e.g. `sk-proj-abc123...`)
|
|
133
|
+
|
|
134
|
+
This way the code `envsave.get("es_7a3f2b1e9c8d4f6b")` resolves via `process.env.es_7a3f2b1e9c8d4f6b` on Vercel, returning the real API key — without any vault needed.
|
|
135
|
+
|
|
136
|
+
**To push secrets to Vercel**, use the built-in `envsave vercel` command. The project must be linked to Vercel first (`vercel link`).
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
# Push specific secrets
|
|
140
|
+
envsave vercel OPENAI_API_KEY STRIPE_KEY
|
|
141
|
+
|
|
142
|
+
# Target specific environments (default: production,preview,development)
|
|
143
|
+
envsave vercel OPENAI_API_KEY --env production,preview
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The command automatically:
|
|
147
|
+
- Checks the project is linked to Vercel (`.vercel/project.json`)
|
|
148
|
+
- Resolves each clean key name to its opaque key
|
|
149
|
+
- Sets the opaque key as the Vercel env var name, real secret as the value
|
|
150
|
+
- Removes existing vars before adding (handles updates)
|
|
151
|
+
- Pipes values via stdin — no shell escaping issues, no trailing newlines
|
|
152
|
+
|
|
153
|
+
**CRITICAL: Never write real secret values to any local file.** No `.env.local`, no `.env.production`, no temp files.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Rules
|
|
158
|
+
|
|
159
|
+
1. Never hardcode raw secrets. Use `envsave.get()` with opaque key.
|
|
160
|
+
2. Never read .env files directly. Use EnvSave instead.
|
|
161
|
+
3. You cannot list or discover secrets. Ask the user for opaque keys.
|
|
162
|
+
4. Don't guess opaque keys. Ask user to run `envsave reveal <NAME>`.
|
|
163
|
+
5. Handle undefined returns — `envsave.get()` returns `undefined`/`None` if key doesn't exist.
|
|
164
|
+
6. **Never write real secret values to any local file** (no `.env.local`, `.env.production`, etc.). For cloud deployments (Vercel, etc.), pipe from `envsave get` directly to the platform CLI.
|
|
165
|
+
|
|
166
|
+
## Quick Reference
|
|
167
|
+
|
|
168
|
+
| Task | User Command |
|
|
169
|
+
|------|-------------|
|
|
170
|
+
| Add a secret | `envsave set <NAME> <VALUE>` |
|
|
171
|
+
| Get opaque key | `envsave reveal <NAME>` |
|
|
172
|
+
| Rename a secret | `envsave rename <OLD_NAME> <NEW_NAME>` (creates new opaque key, deletes old — output prints OLD → NEW for code search-and-replace) |
|
|
173
|
+
| Unlock vault | `envsave list` |
|
|
174
|
+
| Push to Vercel | `envsave vercel KEY1 KEY2` |
|
package/dist/cli.js
CHANGED
|
@@ -711,7 +711,8 @@ import { createInterface } from "node:readline";
|
|
|
711
711
|
import { existsSync as existsSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
|
|
712
712
|
import { spawnSync } from "node:child_process";
|
|
713
713
|
import { createHash } from "node:crypto";
|
|
714
|
-
import { join as join4 } from "node:path";
|
|
714
|
+
import { dirname, join as join4 } from "node:path";
|
|
715
|
+
import { fileURLToPath } from "node:url";
|
|
715
716
|
import { homedir as homedir3 } from "node:os";
|
|
716
717
|
|
|
717
718
|
// src/license.ts
|
|
@@ -859,7 +860,7 @@ async function checkLicense() {
|
|
|
859
860
|
// package.json
|
|
860
861
|
var package_default = {
|
|
861
862
|
name: "@envsave/cli",
|
|
862
|
-
version: "1.0.
|
|
863
|
+
version: "1.0.47",
|
|
863
864
|
description: "Local secret vault for LLM-safe environments — encrypted secrets that agents can't bulk-discover",
|
|
864
865
|
type: "module",
|
|
865
866
|
main: "dist/index.js",
|
|
@@ -873,7 +874,7 @@ var package_default = {
|
|
|
873
874
|
],
|
|
874
875
|
scripts: {
|
|
875
876
|
prebuild: `bun -e "const p=require('./package.json');const v=p.version.split('.');v[2]=+v[2]+1;p.version=v.join('.');require('fs').writeFileSync('package.json',JSON.stringify(p,null,2)+'\\n')"`,
|
|
876
|
-
build: "bun build src/cli.ts --outdir dist --target node --format esm && bun build src/index.ts --outdir dist --target node --format esm --outfile dist/index.js",
|
|
877
|
+
build: "bun build src/cli.ts --outdir dist --target node --format esm && bun build src/index.ts --outdir dist --target node --format esm --outfile dist/index.js && cp .claude/skills/envsave/SKILL.md dist/SKILL.md",
|
|
877
878
|
dev: "bun run src/cli.ts",
|
|
878
879
|
webapp: "bun run webapp/server.ts",
|
|
879
880
|
"lint:claude": "claude -p 'you are a linter. please look at the changes vs. main and report any issues related to typos. report the filename and line number on one line, and a description of the issue on the second line. do not return any other text.'"
|
|
@@ -958,6 +959,9 @@ Usage:
|
|
|
958
959
|
envsave restore Download vault backup from server
|
|
959
960
|
envsave sync Sync vault with server (pull if cloud newer)
|
|
960
961
|
envsave vercel <KEY|es_key>... Push secrets to Vercel under both clean name + opaque id
|
|
962
|
+
envsave install-skill [--agent <name>] [--local|--global]
|
|
963
|
+
Install the EnvSave skill for an agent platform.
|
|
964
|
+
Supported: claude (default). Coming soon: codex, others.
|
|
961
965
|
envsave help Show this help
|
|
962
966
|
|
|
963
967
|
Security model:
|
|
@@ -1647,6 +1651,72 @@ Done: ${success} pushed, ${failed} failed.`);
|
|
|
1647
1651
|
process.exit(1);
|
|
1648
1652
|
break;
|
|
1649
1653
|
}
|
|
1654
|
+
case "install-skill": {
|
|
1655
|
+
const SUPPORTED_AGENTS = ["claude"];
|
|
1656
|
+
const agent = (getFlag("--agent") ?? "claude").toLowerCase();
|
|
1657
|
+
if (!SUPPORTED_AGENTS.includes(agent)) {
|
|
1658
|
+
console.error(`Agent '${agent}' is not yet supported.`);
|
|
1659
|
+
console.error(`Supported today: ${SUPPORTED_AGENTS.join(", ")}. Coming soon: codex, others.`);
|
|
1660
|
+
process.exit(1);
|
|
1661
|
+
}
|
|
1662
|
+
const cliDir = dirname(fileURLToPath(import.meta.url));
|
|
1663
|
+
const candidates = [
|
|
1664
|
+
join4(cliDir, "SKILL.md"),
|
|
1665
|
+
join4(cliDir, "..", ".claude", "skills", "envsave", "SKILL.md")
|
|
1666
|
+
];
|
|
1667
|
+
const skillSource = candidates.find((p) => existsSync3(p));
|
|
1668
|
+
if (!skillSource) {
|
|
1669
|
+
console.error("Skill source SKILL.md not found in the package.");
|
|
1670
|
+
process.exit(1);
|
|
1671
|
+
}
|
|
1672
|
+
let scope;
|
|
1673
|
+
if (args.includes("--local"))
|
|
1674
|
+
scope = "local";
|
|
1675
|
+
else if (args.includes("--global"))
|
|
1676
|
+
scope = "global";
|
|
1677
|
+
if (!scope) {
|
|
1678
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
1679
|
+
const answer = await new Promise((resolve) => {
|
|
1680
|
+
rl.question(`Install EnvSave skill for ${agent} — (l)ocally in this project or (g)lobally for your user? [l/g] `, resolve);
|
|
1681
|
+
});
|
|
1682
|
+
rl.close();
|
|
1683
|
+
const a = answer.trim().toLowerCase();
|
|
1684
|
+
if (a === "l" || a === "local")
|
|
1685
|
+
scope = "local";
|
|
1686
|
+
else if (a === "g" || a === "global")
|
|
1687
|
+
scope = "global";
|
|
1688
|
+
else {
|
|
1689
|
+
console.error("Cancelled — answer must be 'l' or 'g'.");
|
|
1690
|
+
process.exit(1);
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
const agentDir = {
|
|
1694
|
+
claude: ".claude"
|
|
1695
|
+
};
|
|
1696
|
+
const subdir = agentDir[agent];
|
|
1697
|
+
const targetDir = scope === "local" ? join4(process.cwd(), subdir, "skills", "envsave") : join4(homedir3(), subdir, "skills", "envsave");
|
|
1698
|
+
const targetFile = join4(targetDir, "SKILL.md");
|
|
1699
|
+
if (existsSync3(targetFile)) {
|
|
1700
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
1701
|
+
const answer = await new Promise((resolve) => {
|
|
1702
|
+
rl.question(`Skill already exists at ${targetFile}. Overwrite? (y/N) `, resolve);
|
|
1703
|
+
});
|
|
1704
|
+
rl.close();
|
|
1705
|
+
if (answer.trim().toLowerCase() !== "y") {
|
|
1706
|
+
console.log("Install cancelled.");
|
|
1707
|
+
break;
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
mkdirSync3(targetDir, { recursive: true });
|
|
1711
|
+
writeFileSync3(targetFile, readFileSync5(skillSource));
|
|
1712
|
+
console.log(`
|
|
1713
|
+
✓ Installed EnvSave skill for ${agent} (${scope})`);
|
|
1714
|
+
console.log(` ${targetFile}
|
|
1715
|
+
`);
|
|
1716
|
+
console.log(` Restart your agent (or open the project) and invoke /envsave to use it.
|
|
1717
|
+
`);
|
|
1718
|
+
break;
|
|
1719
|
+
}
|
|
1650
1720
|
default:
|
|
1651
1721
|
console.error(`Unknown command: ${command}`);
|
|
1652
1722
|
usage();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@envsave/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.47",
|
|
4
4
|
"description": "Local secret vault for LLM-safe environments — encrypted secrets that agents can't bulk-discover",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
16
|
"prebuild": "bun -e \"const p=require('./package.json');const v=p.version.split('.');v[2]=+v[2]+1;p.version=v.join('.');require('fs').writeFileSync('package.json',JSON.stringify(p,null,2)+'\\n')\"",
|
|
17
|
-
"build": "bun build src/cli.ts --outdir dist --target node --format esm && bun build src/index.ts --outdir dist --target node --format esm --outfile dist/index.js",
|
|
17
|
+
"build": "bun build src/cli.ts --outdir dist --target node --format esm && bun build src/index.ts --outdir dist --target node --format esm --outfile dist/index.js && cp .claude/skills/envsave/SKILL.md dist/SKILL.md",
|
|
18
18
|
"dev": "bun run src/cli.ts",
|
|
19
19
|
"webapp": "bun run webapp/server.ts",
|
|
20
20
|
"lint:claude": "claude -p 'you are a linter. please look at the changes vs. main and report any issues related to typos. report the filename and line number on one line, and a description of the issue on the second line. do not return any other text.'"
|