enylock_cli 1.0.0

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 ADDED
@@ -0,0 +1,238 @@
1
+ # EnvLock
2
+
3
+ [![npm version](https://img.shields.io/npm/v/envlock.svg?style=flat-rounded)](https://www.npmjs.com/package/envlock)
4
+ [![npm downloads](https://img.shields.io/npm/dm/envlock.svg?style=flat-rounded)](https://www.npmjs.com/package/envlock)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ EnvLock is a secrets management system for development teams. It consists of a web dashboard, where projects and secrets are organized and controlled, and a command-line tool, which developers use to bring those secrets onto their own machines. It replaces the common practice of sharing `.env` files over chat or email with a controlled, auditable alternative.
8
+
9
+ ---
10
+
11
+ ## Table of Contents
12
+
13
+ - [How It Works](#how-it-works)
14
+ - [Features](#features)
15
+ - [Requirements](#requirements)
16
+ - [Installation](#installation--usage)
17
+ - [Getting Started](#getting-started)
18
+ - [CLI Command Reference](#cli-command-reference)
19
+ - [Security](#security)
20
+ - [Contributing](#contributing)
21
+ - [Support & Links](#support--links)
22
+
23
+ ---
24
+
25
+ ## How It Works
26
+
27
+ EnvLock is made up of two parts that work together: a dashboard and a CLI.
28
+
29
+ ### The Dashboard
30
+
31
+ The dashboard, at envlock.app, is where a project actually lives. From there, a project owner can:
32
+
33
+ - Create a project and invite team members, assigning each person a role (owner, admin, member, or viewer).
34
+ - Define environments within that project, typically development, staging, and production, each holding its own independent set of secrets.
35
+ - Add, edit, and version secrets under any environment.
36
+ - Issue and revoke access tokens, each one scoped to a specific project and to specific environments within it.
37
+
38
+ Every change made on the dashboard is recorded in an audit log, so a team can see who added or modified a secret, and when.
39
+
40
+ ### The CLI
41
+
42
+ The CLI is what a developer uses day to day, once a project has already been set up on the dashboard. Running `envlock login` opens a browser window where the developer approves access to a specific project and, depending on the role granted, specific environments within it. That approval only needs to happen once per project folder. After that, secrets can be pulled into a local `.env` file, or injected directly into a running process, without opening the dashboard again.
43
+
44
+ In short: the dashboard is where secrets are organized and controlled, and the CLI is how they reach a developer's machine.
45
+
46
+ ---
47
+
48
+ ## Features
49
+
50
+ - Authentication through a browser-based device authorization flow, rather than pasted API keys.
51
+ - Access tokens scoped to a single project and specific environments, so a compromised token cannot expose secrets outside its intended scope.
52
+ - Session credentials stored per project folder, so working on multiple EnvLock projects on the same machine does not cause conflicts.
53
+ - An option to inject secrets directly into a running process's memory, so plaintext values never need to be written to disk.
54
+ - Automatic `.gitignore` configuration on login, to prevent secrets from being committed by mistake.
55
+ - A full audit log on the dashboard, recording who changed which secret and when.
56
+
57
+ ---
58
+
59
+ ## Requirements
60
+
61
+ - Node.js 18 or later
62
+ - An EnvLock account, created at envlock.app
63
+
64
+ ---
65
+
66
+ ## Installation & Usage
67
+
68
+ EnvLock can be used in either of two ways.
69
+
70
+ ### Option A: Zero Install
71
+
72
+ Run commands directly without a global install. This is the recommended path for CI/CD pipelines, such as GitHub Actions:
73
+
74
+ ```bash
75
+ npx envlock login
76
+ npx envlock pull
77
+ ```
78
+
79
+ ### Option B: Global Installation
80
+
81
+ Install once, then run it from anywhere:
82
+
83
+ ```bash
84
+ npm install -g envlock
85
+
86
+ envlock login
87
+ envlock pull
88
+ ```
89
+
90
+ ---
91
+
92
+ ## Getting Started
93
+
94
+ ### 1. Authenticate
95
+
96
+ Run the login command inside your project directory:
97
+
98
+ ```bash
99
+ envlock login
100
+ ```
101
+
102
+ This will print a user code in your terminal, open your browser to complete approval, and prompt you to choose which project this directory should have access to. Once approved, your local `.gitignore` is updated automatically.
103
+
104
+ ### 2. Pull Secrets
105
+
106
+ Write your project's secrets to a local `.env` file:
107
+
108
+ ```bash
109
+ envlock pull
110
+ ```
111
+
112
+ Pull from a specific environment:
113
+
114
+ ```bash
115
+ envlock pull --env production
116
+ ```
117
+
118
+ ### 3. Run Commands with Secrets Injected
119
+
120
+ Run your application with secrets passed directly into the process, without writing them to disk:
121
+
122
+ ```bash
123
+ envlock run -- node app.js
124
+ ```
125
+
126
+ Use a different environment:
127
+
128
+ ```bash
129
+ envlock run --env staging -- npm run dev
130
+ ```
131
+
132
+ ---
133
+
134
+ ## CLI Command Reference
135
+
136
+ Every command supports `--help` for details on its arguments and options.
137
+
138
+ ### Global Flags
139
+
140
+ | Flag | Description |
141
+ | --------------- | ---------------------------------------------- |
142
+ | `-h, --help` | Show help for the CLI or a specific subcommand |
143
+ | `-V, --version` | Output the current version number |
144
+
145
+ ### `envlock login`
146
+
147
+ Authenticates the current project folder through the device authorization flow: displays a user code, opens the browser, and waits for approval.
148
+
149
+ ```bash
150
+ envlock login
151
+ ```
152
+
153
+ ### `envlock pull`
154
+
155
+ Downloads decrypted secrets and writes them locally as key-value pairs.
156
+
157
+ | Option | Description | Default |
158
+ | --------------------- | --------------------------------------------------- | ------------- |
159
+ | `-e, --env <name>` | Environment to pull from | `development` |
160
+ | `-o, --output <file>` | Target filename to write | `.env` |
161
+ | `--overwrite` | Allow overwriting a file that already contains keys | `false` |
162
+
163
+ ```bash
164
+ envlock pull --output .env.local
165
+ envlock pull --env production --overwrite
166
+ ```
167
+
168
+ ### `envlock run`
169
+
170
+ Fetches decrypted secrets in memory and runs a command with them injected. Plaintext values are never written to disk.
171
+
172
+ | Option | Description | Default |
173
+ | ------------------ | -------------------------------------- | ------------- |
174
+ | `-e, --env <name>` | Environment to retrieve variables from | `development` |
175
+
176
+ Place `--` before your target command so its own arguments pass through correctly.
177
+
178
+ ```bash
179
+ envlock run -- node index.js
180
+ envlock run --env production -- npm start
181
+ ```
182
+
183
+ ### `envlock whoami`
184
+
185
+ Shows which project and environment this directory is currently authenticated to.
186
+
187
+ ```bash
188
+ envlock whoami
189
+ ```
190
+
191
+ ### `envlock logout`
192
+
193
+ Removes the local session by deleting the credentials stored in `.envlock/`.
194
+
195
+ ```bash
196
+ envlock logout
197
+ ```
198
+
199
+ ### `envlock help`
200
+
201
+ Lists all available commands with a short description of each.
202
+
203
+ ```bash
204
+ envlock help
205
+ ```
206
+
207
+ ---
208
+
209
+ ## Security
210
+
211
+ - Session tokens stored locally in `.envlock/credentials.json` are saved with owner-only read/write permissions (`0o600`).
212
+ - During login, the server holds an approved token in a pending state and deletes it the moment the CLI retrieves it, so it is never persisted longer than necessary.
213
+ - The CLI ensures `.envlock/` and `.env*` patterns exist in your local `.gitignore`, reducing the chance of secrets being committed by accident.
214
+
215
+ ---
216
+
217
+ ## Contributing
218
+
219
+ Contributions, issues, and feature requests are welcome.
220
+
221
+ ```bash
222
+ git clone https://github.com/leyroy/envlock_cli.git
223
+ cd envlock_cli
224
+ npm install
225
+ npm link
226
+ ```
227
+
228
+ `npm link` makes your local changes available as the global `envlock` command, so you can test them directly. Open a pull request once your change is ready; for larger changes, opening an issue first to discuss the approach is appreciated.
229
+
230
+ ---
231
+
232
+ ## Support & Links
233
+
234
+ - Website: envlock.app
235
+ - Repository: github.com/leyroy/envlock_cli
236
+ - Issues: github.com/leyroy/envlock_cli/issues
237
+ - Author: Ley Roy (atiwensolomon@gmail.com)
238
+ - License: MIT
package/bin/envlock.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Entry point — delegates to the Commander program in src/index.js
4
+ import "../src/index.js";
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "enylock_cli",
3
+ "version": "1.0.0",
4
+ "description": "Encrypted secrets management CLI — pull, run, and manage env vars from EnvLock",
5
+ "keywords": [
6
+ "secrets",
7
+ "env",
8
+ "dotenv",
9
+ "cli",
10
+ "envlock",
11
+ "security"
12
+ ],
13
+ "homepage": "https://envlock.app",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/enylockapp/envlock-cli"
17
+ },
18
+ "author": {
19
+ "name": "Ley Roy",
20
+ "email": "atiwensolomon@gmail.com",
21
+ "url": "https://ley-roy.vercel.app/"
22
+ },
23
+ "license": "MIT",
24
+ "type": "module",
25
+ "main": "./src/index.js",
26
+ "bin": {
27
+ "envlock": "./bin/envlock.js"
28
+ },
29
+ "files": [
30
+ "bin/",
31
+ "src/"
32
+ ],
33
+ "engines": {
34
+ "node": ">=18.0.0"
35
+ },
36
+ "dependencies": {
37
+ "chalk": "^5.3.0",
38
+ "commander": "^12.1.0",
39
+ "open": "^10.1.0",
40
+ "ora": "^8.1.1"
41
+ }
42
+ }
@@ -0,0 +1,120 @@
1
+ import chalk from "chalk";
2
+ import ora from "ora";
3
+ import open from "open";
4
+ import { initDevice, pollToken } from "../lib/api.js";
5
+ import { saveCredentials, loadCredentials } from "../lib/credentials.js";
6
+ import { ensureGitignore, detectProjectType } from "../lib/gitignore.js";
7
+
8
+ const POLL_INTERVAL_MS = 5000;
9
+
10
+ /**
11
+ * `envlock login`
12
+ *
13
+ * Runs the device authorization flow:
14
+ * 1. Request a device code from the API
15
+ * 2. Open the browser to the verification URI
16
+ * 3. Poll until the user approves or the code expires
17
+ * 4. Save the token to .envlock/credentials.json
18
+ */
19
+ export async function loginCommand() {
20
+ const cwd = process.cwd();
21
+
22
+ // Skip if already logged in
23
+ const existing = loadCredentials(cwd);
24
+ if (existing?.token) {
25
+ console.log(
26
+ chalk.yellow("⚠ Already logged in.") + " Run `envlock logout` first to switch accounts.",
27
+ );
28
+ return;
29
+ }
30
+
31
+ let deviceData;
32
+ try {
33
+ deviceData = await initDevice();
34
+ } catch (error) {
35
+ console.log(error);
36
+ console.error(chalk.red("✖ Could not reach EnvLock API:"), error.message);
37
+ process.exit(1);
38
+ }
39
+
40
+ const { device_code, user_code, verification_uri, expires_in, interval } = deviceData;
41
+ const pollIntervalMs = Math.max((interval ?? 5) * 1000, POLL_INTERVAL_MS);
42
+
43
+ console.log();
44
+ console.log(chalk.bold(" EnvLock CLI Login"));
45
+ console.log();
46
+ console.log(" 1. Your browser will open to:");
47
+ console.log(" " + chalk.cyan(verification_uri));
48
+ console.log();
49
+ console.log(" 2. Enter this code when prompted:");
50
+ console.log(" " + chalk.bold.green(user_code));
51
+ console.log();
52
+ console.log(chalk.dim(` Code expires in ${Math.round(expires_in / 60)} minutes.`));
53
+ console.log();
54
+
55
+ // Attempt to open browser; non-fatal if it fails
56
+ try {
57
+ await open(`${verification_uri}?code=${encodeURIComponent(user_code)}`);
58
+ } catch {
59
+ console.log(chalk.dim(" (Could not open browser automatically — please open the URL above)"));
60
+ }
61
+
62
+ const spinner = ora("Waiting for browser approval…").start();
63
+
64
+ const deadline = Date.now() + expires_in * 1000;
65
+
66
+ // Poll until approved, denied, or expired
67
+ while (Date.now() < deadline) {
68
+ await sleep(pollIntervalMs);
69
+
70
+ try {
71
+ const result = await pollToken(device_code);
72
+
73
+ if (result.status === "authorization_pending") {
74
+ continue; // still waiting
75
+ }
76
+
77
+ if (result.status === "approved" && result.access_token) {
78
+ spinner.succeed(chalk.green("Logged in successfully!"));
79
+
80
+ saveCredentials(cwd, {
81
+ token: result.access_token,
82
+ loggedInAt: new Date().toISOString(),
83
+ });
84
+
85
+ // Ensure .gitignore is updated
86
+ const added = ensureGitignore(cwd);
87
+ if (added.length > 0) {
88
+ console.log(chalk.dim(` Added to .gitignore: ${added.join(", ")}`));
89
+ }
90
+
91
+ const projectType = detectProjectType(cwd);
92
+ if (projectType) {
93
+ console.log(chalk.dim(` Detected ${projectType} project.`));
94
+ }
95
+
96
+ console.log();
97
+ console.log(chalk.bold(" Next steps:"));
98
+ console.log(" " + chalk.cyan("envlock pull") + " — write secrets to .env");
99
+ console.log(
100
+ " " + chalk.cyan("envlock run -- <cmd>") + " — run a command with secrets injected",
101
+ );
102
+ console.log();
103
+ return;
104
+ }
105
+ } catch (err) {
106
+ if (err.status === 400 && (err.body?.error === "denied" || err.body?.error === "expired")) {
107
+ break;
108
+ }
109
+ // Transient network error — keep polling
110
+ }
111
+ }
112
+
113
+ spinner.fail(chalk.red("Login timed out or was denied."));
114
+ console.log(" Run `envlock login` to try again.");
115
+ process.exit(1);
116
+ }
117
+
118
+ function sleep(ms) {
119
+ return new Promise((resolve) => setTimeout(resolve, ms));
120
+ }
@@ -0,0 +1,24 @@
1
+ import chalk from "chalk";
2
+ import { clearCredentials, loadCredentials } from "../lib/credentials.js";
3
+
4
+ /**
5
+ * `envlock logout`
6
+ *
7
+ * Removes the local credentials file from .envlock/credentials.json.
8
+ * The access token on the server is NOT revoked — this is intentional so
9
+ * automated pipelines that share the same token can continue running.
10
+ * To fully revoke, use the EnvLock dashboard.
11
+ */
12
+ export function logoutCommand() {
13
+ const cwd = process.cwd();
14
+ const existing = loadCredentials(cwd);
15
+
16
+ if (!existing?.token) {
17
+ console.log(chalk.yellow("Not currently logged in for this directory."));
18
+ return;
19
+ }
20
+
21
+ clearCredentials(cwd);
22
+ console.log(chalk.green("✔ Logged out.") + " Credentials removed from .envlock/credentials.json");
23
+ console.log(chalk.dim(" Note: To fully revoke the token, visit the EnvLock dashboard."));
24
+ }
@@ -0,0 +1,82 @@
1
+ import { writeFileSync, readFileSync, existsSync } from "fs";
2
+ import chalk from "chalk";
3
+ import ora from "ora";
4
+ import { fetchSecrets } from "../lib/api.js";
5
+ import { requireCredentials } from "../lib/credentials.js";
6
+
7
+ /**
8
+ * `envlock pull [--env <name>] [--output <file>] [--overwrite]`
9
+ *
10
+ * Fetches decrypted secrets from the EnvLock API and writes them to a .env file.
11
+ *
12
+ * @param {object} options
13
+ * @param {string} options.env - Environment name (default: "development")
14
+ * @param {string} options.output - Output file path (default: ".env")
15
+ * @param {boolean} options.overwrite - If false and the file exists, exit with a warning
16
+ */
17
+ export async function pullCommand(options = {}) {
18
+ const cwd = process.cwd();
19
+ const creds = requireCredentials(cwd);
20
+
21
+ const environmentName = options.env ?? "development";
22
+ const outputFile = options.output ?? ".env";
23
+ const overwrite = options.overwrite ?? false;
24
+
25
+ // Guard against accidentally overwriting an existing file
26
+ if (!overwrite && existsSync(outputFile)) {
27
+ const existing = readFileSync(outputFile, "utf8").trim();
28
+ if (existing.length > 0) {
29
+ console.log(chalk.yellow(`⚠ ${outputFile} already exists.`) + " Use --overwrite to replace it.");
30
+ process.exit(1);
31
+ }
32
+ }
33
+
34
+ const spinner = ora(`Pulling ${chalk.bold(environmentName)} secrets…`).start();
35
+
36
+ let result;
37
+ try {
38
+ result = await fetchSecrets(creds.token, environmentName);
39
+ } catch (err) {
40
+ spinner.fail(chalk.red("Failed to fetch secrets"));
41
+ if (err.status === 401) {
42
+ console.error(" Token invalid or expired. Run `envlock login` to re-authenticate.");
43
+ } else if (err.status === 403) {
44
+ console.error(` Access denied for the "${environmentName}" environment.`);
45
+ } else if (err.status === 404) {
46
+ console.error(` Environment "${environmentName}" not found. Check your project settings.`);
47
+ } else {
48
+ console.error(" ", err.message);
49
+ }
50
+ process.exit(1);
51
+ }
52
+
53
+ const { secrets } = result;
54
+ const entries = Object.entries(secrets);
55
+
56
+ if (entries.length === 0) {
57
+ spinner.warn(chalk.yellow(`No secrets found in "${environmentName}".`));
58
+ return;
59
+ }
60
+
61
+ const content =
62
+ `# Generated by EnvLock — ${new Date().toISOString()}\n` +
63
+ `# Environment: ${environmentName}\n` +
64
+ `# DO NOT COMMIT — this file is in .gitignore\n\n` +
65
+ entries.map(([key, value]) => formatEnvLine(key, value)).join("\n") +
66
+ "\n";
67
+
68
+ writeFileSync(outputFile, content, "utf8");
69
+ spinner.succeed(
70
+ chalk.green(`✔ Wrote ${entries.length} secret${entries.length !== 1 ? "s" : ""}`) +
71
+ ` to ${chalk.bold(outputFile)}`
72
+ );
73
+ }
74
+
75
+ /**
76
+ * Formats a key=value pair, quoting the value if it contains spaces or special chars.
77
+ */
78
+ function formatEnvLine(key, value) {
79
+ const needsQuotes = /[\s"'\\#$`]/.test(value) || value === "";
80
+ const escaped = needsQuotes ? `"${value.replace(/"/g, '\\"')}"` : value;
81
+ return `${key}=${escaped}`;
82
+ }
@@ -0,0 +1,65 @@
1
+ import { spawn } from "child_process";
2
+ import chalk from "chalk";
3
+ import ora from "ora";
4
+ import { fetchSecrets } from "../lib/api.js";
5
+ import { requireCredentials } from "../lib/credentials.js";
6
+
7
+ /**
8
+ * `envlock run [--env <name>] -- <command> [...args]`
9
+ *
10
+ * Injects decrypted secrets as environment variables and spawns a command.
11
+ * Secrets never touch the filesystem — they live only in the child process env.
12
+ *
13
+ * @param {string[]} args - Command and its arguments (everything after --)
14
+ * @param {object} options
15
+ * @param {string} options.env - Environment name (default: "development")
16
+ */
17
+ export async function runCommand(args, options = {}) {
18
+ if (!args || args.length === 0) {
19
+ console.error(chalk.red("✖ No command provided."));
20
+ console.error(" Usage: envlock run [--env <name>] -- <command> [args...]");
21
+ process.exit(1);
22
+ }
23
+
24
+ const cwd = process.cwd();
25
+ const creds = requireCredentials(cwd);
26
+ const environmentName = options.env ?? "development";
27
+
28
+ const spinner = ora(`Loading ${chalk.bold(environmentName)} secrets…`).start();
29
+
30
+ let secrets;
31
+ try {
32
+ const result = await fetchSecrets(creds.token, environmentName);
33
+ secrets = result.secrets;
34
+ } catch (err) {
35
+ spinner.fail(chalk.red("Failed to fetch secrets"));
36
+ if (err.status === 401) {
37
+ console.error(" Token invalid or expired. Run `envlock login` to re-authenticate.");
38
+ } else {
39
+ console.error(" ", err.message);
40
+ }
41
+ process.exit(1);
42
+ }
43
+
44
+ spinner.stop();
45
+
46
+ const [cmd, ...cmdArgs] = args;
47
+
48
+ // Merge fetched secrets into the current process environment
49
+ const childEnv = { ...process.env, ...secrets };
50
+
51
+ const child = spawn(cmd, cmdArgs, {
52
+ stdio: "inherit",
53
+ env: childEnv,
54
+ shell: process.platform === "win32", // shell required on Windows for built-ins
55
+ });
56
+
57
+ child.on("error", (err) => {
58
+ console.error(chalk.red(`✖ Failed to start command: ${err.message}`));
59
+ process.exit(1);
60
+ });
61
+
62
+ child.on("close", (code) => {
63
+ process.exit(code ?? 0);
64
+ });
65
+ }
@@ -0,0 +1,32 @@
1
+ import chalk from "chalk";
2
+ import { loadCredentials } from "../lib/credentials.js";
3
+
4
+ /**
5
+ * `envlock whoami`
6
+ *
7
+ * Displays information about the currently stored credentials.
8
+ */
9
+ export function whoamiCommand() {
10
+ const cwd = process.cwd();
11
+ const creds = loadCredentials(cwd);
12
+
13
+ if (!creds?.token) {
14
+ console.log(chalk.yellow("Not logged in.") + " Run `envlock login` to authenticate.");
15
+ return;
16
+ }
17
+
18
+ const tokenPreview =
19
+ creds.token.length > 16
20
+ ? creds.token.slice(0, 12) + "…" + creds.token.slice(-4)
21
+ : creds.token;
22
+
23
+ console.log();
24
+ console.log(chalk.bold(" Logged in"));
25
+ console.log();
26
+ console.log(" Token: " + chalk.cyan(tokenPreview));
27
+ if (creds.loggedInAt) {
28
+ console.log(" Since: " + new Date(creds.loggedInAt).toLocaleString());
29
+ }
30
+ console.log(" Scope: " + chalk.dim(process.cwd()));
31
+ console.log();
32
+ }
package/src/index.js ADDED
@@ -0,0 +1,66 @@
1
+ import { Command } from "commander";
2
+ import { loginCommand } from "./commands/login.js";
3
+ import { logoutCommand } from "./commands/logout.js";
4
+ import { pullCommand } from "./commands/pull.js";
5
+ import { runCommand } from "./commands/run.js";
6
+ import { whoamiCommand } from "./commands/whoami.js";
7
+
8
+ const program = new Command();
9
+
10
+ program
11
+ .name("envlock")
12
+ .description("Encrypted secrets management — pull, run, and manage env vars securely")
13
+ .version("1.0.0")
14
+ .enablePositionalOptions();
15
+
16
+
17
+ program
18
+ .command("login")
19
+ .description("Authenticate via browser and store credentials for this project directory")
20
+ .action(loginCommand);
21
+
22
+ program
23
+ .command("logout")
24
+ .description("Remove stored credentials from .envlock/credentials.json")
25
+ .action(logoutCommand);
26
+
27
+ program
28
+ .command("pull")
29
+ .description("Fetch secrets from EnvLock and write them to a .env file")
30
+ .option("-e, --env <name>", "environment to pull (e.g. production, staging)", "development")
31
+ .option("-o, --output <file>", "output file path", ".env")
32
+ .option("--overwrite", "overwrite the output file if it already exists", false)
33
+ .action(pullCommand);
34
+
35
+ program
36
+ .command("run")
37
+ .description("Run a command with secrets injected as environment variables (secrets never touch disk)")
38
+ .option("-e, --env <name>", "environment to load secrets from", "development")
39
+ .passThroughOptions()
40
+ .argument("[args...]", "command and arguments to run (after --)")
41
+ .addHelpText(
42
+ "after",
43
+ `
44
+ Examples:
45
+ envlock run -- node server.js
46
+ envlock run --env production -- npm start
47
+ `
48
+ )
49
+ .action((args, options) => {
50
+ // Strip leading "--" separator if Commander passes it through
51
+ const cleanArgs = args[0] === "--" ? args.slice(1) : args;
52
+ return runCommand(cleanArgs, options);
53
+ });
54
+
55
+
56
+ program
57
+ .command("whoami")
58
+ .description("Show current authentication status and token info")
59
+ .action(whoamiCommand);
60
+
61
+ program
62
+ .command("help", { isDefault: false, hidden: true })
63
+ .description("Show help information")
64
+ .action(() => program.help());
65
+
66
+ program.parse(process.argv);
package/src/lib/api.js ADDED
@@ -0,0 +1,81 @@
1
+ // Thin fetch wrapper for the EnvLock API.
2
+ // BASE_URL defaults to production; override with ENVLOCK_API_URL for local dev.
3
+
4
+ const BASE_URL = (process.env.ENVLOCK_API_URL ?? "https://envlock.app").replace(/\/$/, "");
5
+
6
+ /**
7
+ * Generic JSON fetch with error normalisation.
8
+ * Returns the parsed response body on success; throws on non-2xx.
9
+ */
10
+ async function apiFetch(path, options = {}) {
11
+ const url = `${BASE_URL}${path}`;
12
+ const res = await fetch(url, {
13
+ ...options,
14
+ headers: {
15
+ "Content-Type": "application/json",
16
+ ...(options.headers ?? {}),
17
+ },
18
+ });
19
+
20
+ const body = await res.json().catch(() => ({}));
21
+
22
+ if (!res.ok) {
23
+ const message = body.error ?? `HTTP ${res.status}`;
24
+ const err = new Error(message);
25
+ err.status = res.status;
26
+ err.body = body;
27
+ throw err;
28
+ }
29
+
30
+ return body;
31
+ }
32
+
33
+ /**
34
+ * POST /api/cli/device/init
35
+ * Returns { device_code, user_code, verification_uri, expires_in, interval }
36
+ */
37
+ export function initDevice() {
38
+ return apiFetch("/api/cli/device/init", {
39
+ method: "POST",
40
+ body: JSON.stringify({ client_id: "envlock-cli", scopes: "secrets:read" }),
41
+ });
42
+ }
43
+
44
+ /**
45
+ * POST /api/cli/device/token
46
+ * Returns { status: "approved", access_token } on success,
47
+ * { status: "authorization_pending" } (428) while waiting,
48
+ * or throws on denied / expired.
49
+ */
50
+ export async function pollToken(deviceCode) {
51
+ const res = await fetch(`${BASE_URL}/api/cli/device/token`, {
52
+ method: "POST",
53
+ headers: { "Content-Type": "application/json" },
54
+ body: JSON.stringify({ device_code: deviceCode }),
55
+ });
56
+
57
+ const body = await res.json().catch(() => ({}));
58
+
59
+ if (res.status === 428) {
60
+ // Still waiting for user to approve in browser
61
+ return { status: "authorization_pending" };
62
+ }
63
+
64
+ if (!res.ok) {
65
+ const err = new Error(body.error ?? `HTTP ${res.status}`);
66
+ err.status = res.status;
67
+ throw err;
68
+ }
69
+
70
+ return body; // { status: "approved", access_token: "..." }
71
+ }
72
+
73
+ /**
74
+ * GET /api/cli/secrets?environment=<env>
75
+ * Returns { secrets: Record<string,string>, environment: string }
76
+ */
77
+ export function fetchSecrets(token, environment = "development") {
78
+ return apiFetch(`/api/cli/secrets?environment=${encodeURIComponent(environment)}`, {
79
+ headers: { Authorization: `Bearer ${token}` },
80
+ });
81
+ }
@@ -0,0 +1,63 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync } from "fs";
2
+ import { join } from "path";
3
+
4
+ const ENVLOCK_DIR = ".envlock";
5
+ const CREDENTIALS_FILE = "credentials.json";
6
+
7
+ /**
8
+ * Returns the full path to .envlock/credentials.json relative to the given cwd.
9
+ */
10
+ function credentialsPath(cwd) {
11
+ return join(cwd, ENVLOCK_DIR, CREDENTIALS_FILE);
12
+ }
13
+
14
+ /**
15
+ * Loads stored credentials from .envlock/credentials.json.
16
+ * Returns null if the file doesn't exist or is malformed.
17
+ */
18
+ export function loadCredentials(cwd = process.cwd()) {
19
+ const filePath = credentialsPath(cwd);
20
+ if (!existsSync(filePath)) return null;
21
+ try {
22
+ return JSON.parse(readFileSync(filePath, "utf8"));
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Persists credentials to .envlock/credentials.json, creating the directory
30
+ * if needed. Writes with mode 0o600 so only the owner can read it.
31
+ */
32
+ export function saveCredentials(cwd = process.cwd(), data) {
33
+ const dir = join(cwd, ENVLOCK_DIR);
34
+ mkdirSync(dir, { recursive: true });
35
+ const filePath = join(dir, CREDENTIALS_FILE);
36
+ writeFileSync(filePath, JSON.stringify(data, null, 2), { mode: 0o600, encoding: "utf8" });
37
+ }
38
+
39
+ /**
40
+ * Removes the credentials file. Ignores errors if the file doesn't exist.
41
+ */
42
+ export function clearCredentials(cwd = process.cwd()) {
43
+ const filePath = credentialsPath(cwd);
44
+ if (existsSync(filePath)) {
45
+ try {
46
+ rmSync(filePath);
47
+ } catch {
48
+ // Non-fatal — the file may have already been removed
49
+ }
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Returns credentials or throws a user-friendly error instructing login.
55
+ */
56
+ export function requireCredentials(cwd = process.cwd()) {
57
+ const creds = loadCredentials(cwd);
58
+ if (!creds?.token) {
59
+ console.error("Not logged in. Run `envlock login` first.");
60
+ process.exit(1);
61
+ }
62
+ return creds;
63
+ }
@@ -0,0 +1,52 @@
1
+ import { readFileSync, writeFileSync, existsSync } from "fs";
2
+ import { join } from "path";
3
+
4
+ const GITIGNORE_FILE = ".gitignore";
5
+
6
+ // Entries that envlock ensures are present in .gitignore
7
+ const REQUIRED_ENTRIES = [".envlock/", ".env", ".env.*", "!.env.example"];
8
+
9
+ /**
10
+ * Detects the project language/ecosystem from common config files.
11
+ * Used only for informational messaging — gitignore logic is the same regardless.
12
+ */
13
+ function detectProjectType(cwd) {
14
+ const markers = {
15
+ "package.json": "Node.js",
16
+ "go.mod": "Go",
17
+ "Cargo.toml": "Rust",
18
+ "requirements.txt": "Python",
19
+ "pyproject.toml": "Python",
20
+ "composer.json": "PHP",
21
+ Gemfile: "Ruby",
22
+ };
23
+ for (const [file, lang] of Object.entries(markers)) {
24
+ if (existsSync(join(cwd, file))) return lang;
25
+ }
26
+ return null;
27
+ }
28
+
29
+ /**
30
+ * Ensures .envlock/ and common .env patterns are in .gitignore.
31
+ * Creates the file if it doesn't exist. Only appends missing entries.
32
+ * Returns an array of the entries that were actually added.
33
+ */
34
+ export function ensureGitignore(cwd = process.cwd()) {
35
+ const gitignorePath = join(cwd, GITIGNORE_FILE);
36
+ const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf8") : "";
37
+ const lines = existing.split("\n").map((l) => l.trim());
38
+
39
+ const missing = REQUIRED_ENTRIES.filter((entry) => !lines.includes(entry));
40
+ if (missing.length === 0) return [];
41
+
42
+ const section = [
43
+ "",
44
+ "# EnvLock — secrets management",
45
+ ...missing,
46
+ ].join("\n");
47
+
48
+ writeFileSync(gitignorePath, existing.trimEnd() + section + "\n", "utf8");
49
+ return missing;
50
+ }
51
+
52
+ export { detectProjectType };