crabenv 0.0.1
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 +112 -0
- package/bin.js +45 -0
- package/install.js +158 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# crabenv
|
|
2
|
+
|
|
3
|
+
The simplest, opinionated way to keep .env files, schemas, and examples aligned.
|
|
4
|
+
|
|
5
|
+
crabenv is an **opinionated**, language-agnostic CLI that keeps your environment variables aligned across schema, template, and local files. No new config file required. It validates, copies, and checks for drift so your team doesn't have to.
|
|
6
|
+
|
|
7
|
+
This project will once and for all solve environment variables typesafe schema definition and documentation so it will never drift. This CLI only does what you can already do manually. It doesn't introduce any new config files so if your team doesn't want to use crabenv, it'll be completely fine!
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
brew install blankeos/tap/crabenv # Homebrew (macOS/Linux)
|
|
13
|
+
npm install -g crabenv # or npm
|
|
14
|
+
bun install -g crabenv # or bun
|
|
15
|
+
cargo binstall crabenv # or cargo-binstall (prebuilt binary, faster)
|
|
16
|
+
cargo install crabenv # or cargo (build from source)
|
|
17
|
+
curl -sSL https://raw.githubusercontent.com/Blankeos/crabenv/main/install.sh | sh # or linux/macos (via curl)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### 📁 Languages supported:
|
|
21
|
+
|
|
22
|
+
- [x] TypeScript/Javascript and Monorepos (includes React, Solid, Vue, Svelte, Vite, NextJS, ReactNative, Backends, and Cloudflare apps)
|
|
23
|
+
- [x] Python
|
|
24
|
+
- [x] Rust
|
|
25
|
+
- [x] Flutter
|
|
26
|
+
- [x] More? Request an adapter.
|
|
27
|
+
|
|
28
|
+
### 🤒 Pains solved:
|
|
29
|
+
|
|
30
|
+
- [x] CRUD and Documentation drift
|
|
31
|
+
- [ ] Deployment/sink drift via explicit managed blocks, not arbitrary file inference
|
|
32
|
+
- [x] `.env.example` and `env.ts` drift
|
|
33
|
+
- [x] Validation
|
|
34
|
+
- [x] Client and Server Boundaries
|
|
35
|
+
- [x] Local Development (creating the first .env) to Actual Deployment (translating that into the env on deployment) stories
|
|
36
|
+
- [x] A better `cp .env.example .env` command (This is not enough!) Creating envs (local env or for new dev,staging,prod envs).
|
|
37
|
+
- [x] Use templating patterns like `"RSA_KEY=$(openssl rand -base64 32)` - the crabenv copy
|
|
38
|
+
- [x] ~Rotating??~ Kinda impossible actually.
|
|
39
|
+
- [ ] Cloudflare `.dev.vars` guidance/docs
|
|
40
|
+
|
|
41
|
+
## Philosophy
|
|
42
|
+
|
|
43
|
+
- No new config file. Your team doesn't have to install crabenv, but it helps a lot! The CLI just abstracts the manual maintenance.
|
|
44
|
+
- Env config is synced across **surfaces**:
|
|
45
|
+
- 1. **Schema** - The validator language-specific schema. Multiple based on apps. i.e. `env.ts`, `env.private.ts`, `env.public.ts`, `config.rs`, `config.dart`.
|
|
46
|
+
- 2. **Template (`.env.example`)** - Safe example/template values for env values. Multiple based on apps.
|
|
47
|
+
- 3. **Local (`.env`)** - The actual local runtime values/secrets. 1 only.
|
|
48
|
+
- 4. **Sinks** - Reserved for future explicit integrations. crabenv does not currently infer or rewrite arbitrary deployment files.
|
|
49
|
+
- More? Make an adapter
|
|
50
|
+
- Packages in monorepos don't have .env.
|
|
51
|
+
|
|
52
|
+
## Agent skill
|
|
53
|
+
|
|
54
|
+
Install the crabenv skill for coding agents with:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
npx skills add blankeos/crabenv
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
This installs the `crabenv` agent skill from `skills/crabenv`, including language-specific references for TypeScript/JavaScript, Python, Rust, and Flutter/Dart.
|
|
61
|
+
|
|
62
|
+
## Usage
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
crabenv init
|
|
66
|
+
crabenv copy # or crabenv cp.
|
|
67
|
+
crabenv doctor # It's a checklist of common mistakes
|
|
68
|
+
crabenv doctor --fix
|
|
69
|
+
|
|
70
|
+
# CRUD
|
|
71
|
+
crabenv list # Lists variables
|
|
72
|
+
crabenv add # Wizard-like experience
|
|
73
|
+
crabenv update # Wizard-like experience
|
|
74
|
+
crabenv remove # Wizard-like experience
|
|
75
|
+
|
|
76
|
+
crabenv add {VARIABLE_NAME}
|
|
77
|
+
--shared # if monorepo, adds it to all apps
|
|
78
|
+
--example # optional, for .env.example
|
|
79
|
+
--optional # optional, required by default
|
|
80
|
+
--default # optional
|
|
81
|
+
# Type flags
|
|
82
|
+
--string # optional, passed by default
|
|
83
|
+
--numeric # optional, conflicts w/ string. It's a string, but validates as a number (a numeric string)
|
|
84
|
+
--number # optional, conflicts w/ string (implicitly z.preprocess(Number))
|
|
85
|
+
--boolean # optional, conflicts w/ string (implicitly z.preprocess(Boolean))
|
|
86
|
+
--enum # optional, i.e. --enum=development,production
|
|
87
|
+
# Custom regex
|
|
88
|
+
--testRegex # optional
|
|
89
|
+
--testRegexMessage # optional, required when used with `testRegex`
|
|
90
|
+
# ... essentially a cli version of zod
|
|
91
|
+
# Special environment overrides
|
|
92
|
+
--{}_development # Checks env=development, before applying the type
|
|
93
|
+
--{}_staging # Checks env=staging, before applying the type
|
|
94
|
+
--{}_production # Checks env=production, before applying the type
|
|
95
|
+
--{}_ci # Checks CI=true, before applying the type
|
|
96
|
+
crabenv remove {VARIABLE_NAME}
|
|
97
|
+
crabenv update {VARIABLE_NAME} # Same flags as add
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Current CLI
|
|
101
|
+
|
|
102
|
+
This repo currently has a first Rust CLI implementation that works against the sample projects in `_checks/sample-projects`.
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
crabenv --root _checks/sample-projects/basic-npm list
|
|
106
|
+
crabenv --root _checks/sample-projects/basic-npm doctor
|
|
107
|
+
crabenv --root _checks/sample-projects/basic-npm copy
|
|
108
|
+
|
|
109
|
+
crabenv --root _checks/sample-projects/monorepo doctor --fix --yes
|
|
110
|
+
crabenv --root _checks/sample-projects/monorepo add NEXT_PUBLIC_ANALYTICS_ID --owner apps/next-web --public --example dev --optional
|
|
111
|
+
crabenv --root _checks/sample-projects/monorepo remove NEXT_PUBLIC_ANALYTICS_ID --owner apps/next-web --public
|
|
112
|
+
```
|
package/bin.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawn } = require("child_process");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const { install } = require("./install");
|
|
7
|
+
|
|
8
|
+
const binaryName = process.platform === "win32" ? "crabenv.exe" : "crabenv";
|
|
9
|
+
const binaryPath = path.join(__dirname, "bin", binaryName);
|
|
10
|
+
|
|
11
|
+
async function ensureBinary() {
|
|
12
|
+
if (fs.existsSync(binaryPath)) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
console.error("crabenv binary not found. Attempting download...");
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
await install();
|
|
20
|
+
} catch (error) {
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!fs.existsSync(binaryPath)) {
|
|
25
|
+
console.error("❌ crabenv binary still missing after download.");
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function run() {
|
|
31
|
+
await ensureBinary();
|
|
32
|
+
|
|
33
|
+
const child = spawn(binaryPath, process.argv.slice(2), { stdio: "inherit" });
|
|
34
|
+
|
|
35
|
+
child.on("error", (err) => {
|
|
36
|
+
console.error("❌ Failed to start crabenv:", err.message);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
child.on("exit", (code, signal) => {
|
|
41
|
+
process.exit(signal ? 1 : code || 0);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
run();
|
package/install.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { execSync } = require("child_process");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const https = require("https");
|
|
7
|
+
|
|
8
|
+
// Version should match your Rust crate version.
|
|
9
|
+
const VERSION = require("./package.json").version;
|
|
10
|
+
const BINARY_NAME = "crabenv";
|
|
11
|
+
|
|
12
|
+
function getPlatformInfo() {
|
|
13
|
+
const platform = process.platform;
|
|
14
|
+
const arch = process.arch;
|
|
15
|
+
|
|
16
|
+
// Map Node.js platform/arch to Rust target triples.
|
|
17
|
+
const platformMap = {
|
|
18
|
+
darwin: {
|
|
19
|
+
x64: "x86_64-apple-darwin",
|
|
20
|
+
arm64: "aarch64-apple-darwin",
|
|
21
|
+
},
|
|
22
|
+
linux: {
|
|
23
|
+
x64: "x86_64-unknown-linux-gnu",
|
|
24
|
+
arm64: "aarch64-unknown-linux-gnu",
|
|
25
|
+
},
|
|
26
|
+
win32: {
|
|
27
|
+
x64: "x86_64-pc-windows-msvc",
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
if (!platformMap[platform]) {
|
|
32
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!platformMap[platform][arch]) {
|
|
36
|
+
throw new Error(`Unsupported architecture: ${arch} on ${platform}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const target = platformMap[platform][arch];
|
|
40
|
+
const extension = platform === "win32" ? ".zip" : ".tar.xz";
|
|
41
|
+
const binaryName = platform === "win32" ? `${BINARY_NAME}.exe` : BINARY_NAME;
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
target,
|
|
45
|
+
extension,
|
|
46
|
+
binaryName,
|
|
47
|
+
filename: `${BINARY_NAME}-${target}${extension}`,
|
|
48
|
+
url: `https://github.com/Blankeos/crabenv/releases/download/v${VERSION}/${BINARY_NAME}-${target}${extension}`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function downloadFile(url, dest) {
|
|
53
|
+
console.log(`Downloading ${url}...`);
|
|
54
|
+
|
|
55
|
+
const file = fs.createWriteStream(dest);
|
|
56
|
+
const response = await new Promise((resolve, reject) => {
|
|
57
|
+
https
|
|
58
|
+
.get(url, (res) => {
|
|
59
|
+
if (res.statusCode === 302 || res.statusCode === 301) {
|
|
60
|
+
https.get(res.headers.location, resolve).on("error", reject);
|
|
61
|
+
} else if (res.statusCode === 200) {
|
|
62
|
+
resolve(res);
|
|
63
|
+
} else {
|
|
64
|
+
reject(new Error(`Failed to download: ${res.statusCode} ${res.statusMessage}`));
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
.on("error", reject);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
response.pipe(file);
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
file.on("finish", () => {
|
|
73
|
+
file.close();
|
|
74
|
+
resolve();
|
|
75
|
+
});
|
|
76
|
+
file.on("error", (err) => {
|
|
77
|
+
fs.unlink(dest, () => {});
|
|
78
|
+
reject(err);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function extractArchive(archivePath, extractDir, platformInfo) {
|
|
84
|
+
console.log("Extracting binary...");
|
|
85
|
+
|
|
86
|
+
const cmd =
|
|
87
|
+
platformInfo.extension === ".zip"
|
|
88
|
+
? `unzip -o "${archivePath}" -d "${extractDir}" 2>/dev/null || powershell -command "Expand-Archive -Path '${archivePath}' -DestinationPath '${extractDir}' -Force"`
|
|
89
|
+
: `tar -xf "${archivePath}" -C "${extractDir}"`;
|
|
90
|
+
|
|
91
|
+
execSync(cmd, { stdio: "inherit" });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function logInstallFailure(error) {
|
|
95
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
96
|
+
console.error("Installation failed:", message);
|
|
97
|
+
console.error("\nYou can install crabenv directly using:");
|
|
98
|
+
console.error(
|
|
99
|
+
'curl --proto "=https" --tlsv1.2 -LsSf https://github.com/Blankeos/crabenv/releases/latest/download/crabenv-installer.sh | sh',
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function install({ exitOnComplete = false } = {}) {
|
|
104
|
+
try {
|
|
105
|
+
const platformInfo = getPlatformInfo();
|
|
106
|
+
const binDir = path.join(__dirname, "bin");
|
|
107
|
+
const archivePath = path.join(__dirname, platformInfo.filename);
|
|
108
|
+
const binaryPath = path.join(binDir, platformInfo.binaryName);
|
|
109
|
+
|
|
110
|
+
if (!fs.existsSync(binDir)) fs.mkdirSync(binDir, { recursive: true });
|
|
111
|
+
|
|
112
|
+
await downloadFile(platformInfo.url, archivePath);
|
|
113
|
+
extractArchive(archivePath, __dirname, platformInfo);
|
|
114
|
+
|
|
115
|
+
const extractedBinaryPath = path.join(__dirname, platformInfo.binaryName);
|
|
116
|
+
if (fs.existsSync(extractedBinaryPath)) {
|
|
117
|
+
fs.renameSync(extractedBinaryPath, binaryPath);
|
|
118
|
+
} else {
|
|
119
|
+
const subdirPath = path.join(__dirname, `${BINARY_NAME}-${platformInfo.target}`, platformInfo.binaryName);
|
|
120
|
+
if (fs.existsSync(subdirPath)) {
|
|
121
|
+
fs.renameSync(subdirPath, binaryPath);
|
|
122
|
+
fs.rmSync(path.dirname(subdirPath), { recursive: true, force: true });
|
|
123
|
+
} else {
|
|
124
|
+
throw new Error("Binary not found after extraction");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (process.platform !== "win32") {
|
|
129
|
+
fs.chmodSync(binaryPath, 0o755);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
fs.unlinkSync(archivePath);
|
|
133
|
+
console.log(`crabenv v${VERSION} installed successfully!`);
|
|
134
|
+
|
|
135
|
+
if (exitOnComplete) {
|
|
136
|
+
process.exit(0);
|
|
137
|
+
return binaryPath;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return binaryPath;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
logInstallFailure(error);
|
|
143
|
+
|
|
144
|
+
if (exitOnComplete) {
|
|
145
|
+
process.exit(1);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Only run install if this script is executed directly.
|
|
154
|
+
if (require.main === module) {
|
|
155
|
+
install({ exitOnComplete: true });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = { getPlatformInfo, install };
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "crabenv",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "The simplest, opinionated way to keep .env files, schemas, and examples aligned.",
|
|
5
|
+
"main": "bin.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"crabenv": "./bin.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"postinstall": "node install.js"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/blankeos/crabenv.git"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"env",
|
|
18
|
+
"cli",
|
|
19
|
+
"config",
|
|
20
|
+
"dotenv",
|
|
21
|
+
"schema"
|
|
22
|
+
],
|
|
23
|
+
"author": "Blankeos",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"files": [
|
|
26
|
+
"install.js",
|
|
27
|
+
"bin.js",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=12"
|
|
32
|
+
},
|
|
33
|
+
"os": [
|
|
34
|
+
"darwin",
|
|
35
|
+
"linux",
|
|
36
|
+
"win32"
|
|
37
|
+
]
|
|
38
|
+
}
|