@dotenc/cli 0.3.4 → 0.4.2
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 +393 -38
- package/dist/cli.js +2070 -2
- package/package.json +38 -28
- package/dist/commands/config.js +0 -15
- package/dist/commands/debug.js +0 -16
- package/dist/commands/edit.js +0 -51
- package/dist/commands/init.js +0 -44
- package/dist/commands/key/export.js +0 -21
- package/dist/commands/key/import.js +0 -22
- package/dist/commands/key/rotate.js +0 -35
- package/dist/commands/run.js +0 -46
- package/dist/helpers/createEnvironment.js +0 -10
- package/dist/helpers/createHash.js +0 -7
- package/dist/helpers/createLocalEnvironment.js +0 -21
- package/dist/helpers/createProject.js +0 -13
- package/dist/helpers/crypto.js +0 -70
- package/dist/helpers/environmentExists.js +0 -6
- package/dist/helpers/getDefaultEditor.js +0 -41
- package/dist/helpers/getEnvironmentNameSuggestion.js +0 -5
- package/dist/helpers/homeConfig.js +0 -22
- package/dist/helpers/key.js +0 -39
- package/dist/helpers/parseEnv.js +0 -89
- package/dist/helpers/projectConfig.js +0 -21
- package/dist/program.js +0 -62
- package/dist/prompts/chooseEnvironment.js +0 -18
- package/dist/prompts/createEnvironment.js +0 -12
- package/dist/prompts/inputKey.js +0 -13
- package/dist/tests/e2e.test.js +0 -64
- package/dist/tests/helpers/cleanupProjectKeys.js +0 -13
- package/dist/tests/helpers/waitForFile.js +0 -15
- package/dist/tests/parseEnv.test.js +0 -30
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotenc/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "🔐
|
|
3
|
+
"version": "0.4.2",
|
|
4
|
+
"description": "🔐 Git-native encrypted environments powered by your SSH keys",
|
|
5
5
|
"author": "Ivan Filho <i@ivanfilho.com>",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
@@ -11,18 +11,29 @@
|
|
|
11
11
|
"files": [
|
|
12
12
|
"dist"
|
|
13
13
|
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"dev": "bun src/cli.ts",
|
|
16
|
+
"start": "node dist/cli.js",
|
|
17
|
+
"build": "bun build src/cli.ts --outdir dist --target node --packages external && { echo '#!/usr/bin/env node'; cat dist/cli.js; } > dist/cli.tmp && mv dist/cli.tmp dist/cli.js",
|
|
18
|
+
"build:binary": "bun run build:binary:darwin-arm64 && bun run build:binary:darwin-x64 && bun run build:binary:linux-x64 && bun run build:binary:linux-arm64 && bun run build:binary:windows-x64",
|
|
19
|
+
"build:binary:darwin-arm64": "bun build src/cli.ts --compile --target=bun-darwin-arm64 --outfile dist/dotenc-darwin-arm64",
|
|
20
|
+
"build:binary:darwin-x64": "bun build src/cli.ts --compile --target=bun-darwin-x64 --outfile dist/dotenc-darwin-x64",
|
|
21
|
+
"build:binary:linux-x64": "bun build src/cli.ts --compile --target=bun-linux-x64 --outfile dist/dotenc-linux-x64",
|
|
22
|
+
"build:binary:linux-arm64": "bun build src/cli.ts --compile --target=bun-linux-arm64 --outfile dist/dotenc-linux-arm64",
|
|
23
|
+
"build:binary:windows-x64": "bun build src/cli.ts --compile --target=bun-windows-x64 --outfile dist/dotenc-windows-x64",
|
|
24
|
+
"test": "bun test",
|
|
25
|
+
"test:e2e": "docker build -t dotenc-e2e -f e2e/Dockerfile .. && docker run --rm dotenc-e2e"
|
|
26
|
+
},
|
|
14
27
|
"devDependencies": {
|
|
15
|
-
"@biomejs/biome": "^1.
|
|
16
|
-
"@types/
|
|
17
|
-
"
|
|
18
|
-
"tsx": "^4.19.3",
|
|
19
|
-
"typescript": "^5.8.2",
|
|
20
|
-
"vitest": "^3.0.9"
|
|
28
|
+
"@biomejs/biome": "^2.1.2",
|
|
29
|
+
"@types/bun": "^1.3.9",
|
|
30
|
+
"typescript": "^5.8.2"
|
|
21
31
|
},
|
|
22
32
|
"dependencies": {
|
|
23
33
|
"@paralleldrive/cuid2": "^2.2.2",
|
|
24
34
|
"chalk": "^5.4.1",
|
|
25
35
|
"commander": "^13.1.0",
|
|
36
|
+
"eciesjs": "^0.4.15",
|
|
26
37
|
"inquirer": "^12.4.2",
|
|
27
38
|
"zod": "^3.24.2"
|
|
28
39
|
},
|
|
@@ -35,26 +46,25 @@
|
|
|
35
46
|
},
|
|
36
47
|
"homepage": "https://github.com/ivanfilhoz/dotenc#readme",
|
|
37
48
|
"keywords": [
|
|
49
|
+
"dotenv",
|
|
50
|
+
"env",
|
|
38
51
|
"environment",
|
|
39
52
|
"variables",
|
|
40
|
-
"
|
|
41
|
-
"secure",
|
|
42
|
-
"codebase",
|
|
43
|
-
"cli",
|
|
44
|
-
"command",
|
|
45
|
-
"line",
|
|
46
|
-
"tool",
|
|
47
|
-
"utility",
|
|
48
|
-
"env",
|
|
49
|
-
"box",
|
|
50
|
-
"dotenv",
|
|
53
|
+
"secrets",
|
|
51
54
|
"encrypted",
|
|
52
|
-
"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
"encryption",
|
|
56
|
+
"aes-256-gcm",
|
|
57
|
+
"ssh",
|
|
58
|
+
"ssh-keys",
|
|
59
|
+
"ed25519",
|
|
60
|
+
"rsa",
|
|
61
|
+
"git",
|
|
62
|
+
"cli",
|
|
63
|
+
"secure",
|
|
64
|
+
"security",
|
|
65
|
+
"devops",
|
|
66
|
+
"cicd",
|
|
67
|
+
"configuration",
|
|
68
|
+
"config"
|
|
69
|
+
]
|
|
70
|
+
}
|
package/dist/commands/config.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { getHomeConfig, setHomeConfig } from "../helpers/homeConfig.js";
|
|
2
|
-
export const configCommand = async (key, value, options) => {
|
|
3
|
-
const config = await getHomeConfig();
|
|
4
|
-
if (options.remove) {
|
|
5
|
-
delete config[key];
|
|
6
|
-
await setHomeConfig(config);
|
|
7
|
-
return;
|
|
8
|
-
}
|
|
9
|
-
if (value) {
|
|
10
|
-
config[key] = value;
|
|
11
|
-
await setHomeConfig(config);
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
console.log(config[key]);
|
|
15
|
-
};
|
package/dist/commands/debug.js
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
|
-
import fs from "node:fs/promises";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { decrypt, encrypt } from "../helpers/crypto.js";
|
|
6
|
-
export const debugCommand = async () => {
|
|
7
|
-
const key = crypto.randomBytes(32).toString("base64");
|
|
8
|
-
const encryptedFilePath = path.join(os.tmpdir(), "dotenc.enc");
|
|
9
|
-
await encrypt(key, "Test", encryptedFilePath);
|
|
10
|
-
const content = await decrypt(key, encryptedFilePath);
|
|
11
|
-
await fs.unlink(encryptedFilePath);
|
|
12
|
-
if (content !== "Test") {
|
|
13
|
-
throw new Error("Decrypted content is not equal to the original content");
|
|
14
|
-
}
|
|
15
|
-
console.log("Decryption successful");
|
|
16
|
-
};
|
package/dist/commands/edit.js
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
|
-
import { execSync } from "node:child_process";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
4
|
-
import fs from "node:fs/promises";
|
|
5
|
-
import os from "node:os";
|
|
6
|
-
import path from "node:path";
|
|
7
|
-
import { createHash } from "../helpers/createHash.js";
|
|
8
|
-
import { decrypt, encrypt } from "../helpers/crypto.js";
|
|
9
|
-
import { getDefaultEditor } from "../helpers/getDefaultEditor.js";
|
|
10
|
-
import { getKey } from "../helpers/key.js";
|
|
11
|
-
import { chooseEnvironmentPrompt } from "../prompts/chooseEnvironment.js";
|
|
12
|
-
export const editCommand = async (environmentArg) => {
|
|
13
|
-
let environment = environmentArg;
|
|
14
|
-
if (!environment) {
|
|
15
|
-
environment = await chooseEnvironmentPrompt("What environment do you want to edit?");
|
|
16
|
-
}
|
|
17
|
-
const environmentFile = `.env.${environment}.enc`;
|
|
18
|
-
const environmentFilePath = path.join(process.cwd(), environmentFile);
|
|
19
|
-
if (!existsSync(environmentFilePath)) {
|
|
20
|
-
console.error(`Environment file not found: ${environmentFilePath}`);
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
const key = await getKey(environment);
|
|
24
|
-
if (!key) {
|
|
25
|
-
console.error(`\n${chalk.red("Error:")} no key found for the ${chalk.cyan(environment)} environment.`);
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
const tempFilePath = path.join(os.tmpdir(), `.env.${environment}`);
|
|
29
|
-
const content = await decrypt(key, environmentFilePath);
|
|
30
|
-
await fs.writeFile(tempFilePath, content);
|
|
31
|
-
const initialHash = createHash(content);
|
|
32
|
-
const editor = await getDefaultEditor();
|
|
33
|
-
try {
|
|
34
|
-
// This will block until the editor process is closed
|
|
35
|
-
execSync(`${editor} ${tempFilePath}`, { stdio: "inherit" });
|
|
36
|
-
}
|
|
37
|
-
catch (error) {
|
|
38
|
-
console.error(`\nFailed to open editor: ${editor}`);
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
const newContent = await fs.readFile(tempFilePath, "utf-8");
|
|
42
|
-
const finalHash = createHash(newContent);
|
|
43
|
-
if (initialHash === finalHash) {
|
|
44
|
-
console.log(`\nNo changes were made to the ${chalk.cyan(environment)} environment.`);
|
|
45
|
-
}
|
|
46
|
-
else {
|
|
47
|
-
await encrypt(key, newContent, environmentFilePath);
|
|
48
|
-
console.log(`\nEncrypted ${chalk.cyan(environment)} environment and saved it to ${chalk.gray(environmentFile)}.`);
|
|
49
|
-
}
|
|
50
|
-
await fs.unlink(tempFilePath);
|
|
51
|
-
};
|
package/dist/commands/init.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
|
-
import crypto from "node:crypto";
|
|
3
|
-
import { createEnvironment } from "../helpers/createEnvironment.js";
|
|
4
|
-
import { createLocalEnvironment } from "../helpers/createLocalEnvironment.js";
|
|
5
|
-
import { createProject } from "../helpers/createProject.js";
|
|
6
|
-
import { environmentExists } from "../helpers/environmentExists.js";
|
|
7
|
-
import { getEnvironmentNameSuggestion } from "../helpers/getEnvironmentNameSuggestion.js";
|
|
8
|
-
import { addKey } from "../helpers/key.js";
|
|
9
|
-
import { createEnvironmentPrompt } from "../prompts/createEnvironment.js";
|
|
10
|
-
export const initCommand = async (environmentArg) => {
|
|
11
|
-
// Generate a unique project ID
|
|
12
|
-
const { projectId } = await createProject();
|
|
13
|
-
// Setup local environment
|
|
14
|
-
await createLocalEnvironment();
|
|
15
|
-
// Generate a random key
|
|
16
|
-
const key = crypto.randomBytes(32).toString("base64");
|
|
17
|
-
// Prompt for the environment name
|
|
18
|
-
let environment = environmentArg;
|
|
19
|
-
if (!environment) {
|
|
20
|
-
environment = await createEnvironmentPrompt("What should the environment be named?", getEnvironmentNameSuggestion());
|
|
21
|
-
}
|
|
22
|
-
if (!environment) {
|
|
23
|
-
console.log(`${chalk.red("Error:")} no environment name provided`);
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
if (environmentExists(environment)) {
|
|
27
|
-
console.log(`${chalk.red("Error:")} environment ${environment} already exists. To edit it, use ${chalk.gray(`dotenc edit ${environment}`)}`);
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
await createEnvironment(environment, key);
|
|
31
|
-
// Store the key
|
|
32
|
-
await addKey(projectId, environment, key);
|
|
33
|
-
// Output success message
|
|
34
|
-
console.log(`${chalk.green("✔")} Initialization complete!`);
|
|
35
|
-
console.log("\nSome useful tips:");
|
|
36
|
-
const editCommand = chalk.gray(`dotenc edit ${environment}`);
|
|
37
|
-
console.log(`\n- To securely edit your environment:\t${editCommand}`);
|
|
38
|
-
const runCommand = chalk.gray(`dotenc run -e ${environment} <command> [args...]`);
|
|
39
|
-
const runCommandWithEnv = chalk.gray(`DOTENC_ENV=${environment} dotenc run <command> [args...]`);
|
|
40
|
-
console.log(`- To run your application:\t\t${runCommand} or ${runCommandWithEnv}`);
|
|
41
|
-
const initCommand = chalk.gray("dotenc init [environment]");
|
|
42
|
-
console.log(`- To initialize a new environment:\t${initCommand}`);
|
|
43
|
-
console.log(`- Use the git-ignored ${chalk.gray(".env")} file for local development. It will have priority over any encrypted environments.`);
|
|
44
|
-
};
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
|
-
import { getKey } from "../../helpers/key.js";
|
|
3
|
-
import { getProjectConfig } from "../../helpers/projectConfig.js";
|
|
4
|
-
import { chooseEnvironmentPrompt } from "../../prompts/chooseEnvironment.js";
|
|
5
|
-
export const keyExportCommand = async (environmentArg) => {
|
|
6
|
-
const { projectId } = await getProjectConfig();
|
|
7
|
-
if (!projectId) {
|
|
8
|
-
console.error('No project found. Run "dotenc init" to create one.');
|
|
9
|
-
return;
|
|
10
|
-
}
|
|
11
|
-
let environment = environmentArg;
|
|
12
|
-
if (!environment) {
|
|
13
|
-
environment = await chooseEnvironmentPrompt("What environment do you want to export the key from?");
|
|
14
|
-
}
|
|
15
|
-
const key = await getKey(environment);
|
|
16
|
-
if (!key) {
|
|
17
|
-
console.error(`\nNo key found for the ${chalk.cyan(environment)} environment.`);
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
console.log(`\nKey for the ${chalk.cyan(environment)} environment: ${chalk.gray(key)}`);
|
|
21
|
-
};
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
|
-
import { addKey } from "../../helpers/key.js";
|
|
3
|
-
import { getProjectConfig } from "../../helpers/projectConfig.js";
|
|
4
|
-
import { chooseEnvironmentPrompt } from "../../prompts/chooseEnvironment.js";
|
|
5
|
-
import { inputKeyPrompt } from "../../prompts/inputKey.js";
|
|
6
|
-
export const keyImportCommand = async (environmentArg, keyArg) => {
|
|
7
|
-
const { projectId } = await getProjectConfig();
|
|
8
|
-
if (!projectId) {
|
|
9
|
-
console.error('No project found. Run "dotenc init" to create one.');
|
|
10
|
-
return;
|
|
11
|
-
}
|
|
12
|
-
let environment = environmentArg;
|
|
13
|
-
if (!environment) {
|
|
14
|
-
environment = await chooseEnvironmentPrompt("What environment do you want to import the key to?");
|
|
15
|
-
}
|
|
16
|
-
let key = keyArg;
|
|
17
|
-
if (!key) {
|
|
18
|
-
key = await inputKeyPrompt("Paste the key here:");
|
|
19
|
-
}
|
|
20
|
-
await addKey(projectId, environment, key);
|
|
21
|
-
console.log(`\nKey imported to the ${chalk.cyan(environment)} environment.`);
|
|
22
|
-
};
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
|
-
import crypto from "node:crypto";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { decrypt, encrypt } from "../../helpers/crypto.js";
|
|
6
|
-
import { addKey, getKey } from "../../helpers/key.js";
|
|
7
|
-
import { getProjectConfig } from "../../helpers/projectConfig.js";
|
|
8
|
-
import { chooseEnvironmentPrompt } from "../../prompts/chooseEnvironment.js";
|
|
9
|
-
export const keyRotateCommand = async (environmentArg) => {
|
|
10
|
-
const { projectId } = await getProjectConfig();
|
|
11
|
-
if (!projectId) {
|
|
12
|
-
console.error('No project found. Run "dotenc init" to create one.');
|
|
13
|
-
return;
|
|
14
|
-
}
|
|
15
|
-
let environment = environmentArg;
|
|
16
|
-
if (!environment) {
|
|
17
|
-
environment = await chooseEnvironmentPrompt("What environment do you want to rotate the key for?");
|
|
18
|
-
}
|
|
19
|
-
const environmentFile = `.env.${environment}.enc`;
|
|
20
|
-
const environmentFilePath = path.join(process.cwd(), environmentFile);
|
|
21
|
-
if (!existsSync(environmentFilePath)) {
|
|
22
|
-
console.error(`Environment file not found: ${environmentFilePath}`);
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
const key = await getKey(environment);
|
|
26
|
-
if (!key) {
|
|
27
|
-
console.error(`\nNo key found for the ${chalk.cyan(environment)} environment.`);
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
const content = await decrypt(key, environmentFilePath);
|
|
31
|
-
const newKey = crypto.randomBytes(32).toString("base64");
|
|
32
|
-
await encrypt(newKey, content, environmentFilePath);
|
|
33
|
-
await addKey(projectId, environment, newKey);
|
|
34
|
-
console.log(`\nKey rotated for the ${chalk.cyan(environment)} environment.`);
|
|
35
|
-
};
|
package/dist/commands/run.js
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import fs from "node:fs/promises";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { decrypt } from "../helpers/crypto.js";
|
|
6
|
-
import { getKey } from "../helpers/key.js";
|
|
7
|
-
import { parseEnv } from "../helpers/parseEnv.js";
|
|
8
|
-
export const runCommand = async (command, args, options) => {
|
|
9
|
-
// Get the environment
|
|
10
|
-
const environmentArg = options.env || process.env.DOTENC_ENV;
|
|
11
|
-
if (!environmentArg) {
|
|
12
|
-
console.error('No environment provided. Use -e or set DOTENC_ENV to the environment you want to run the command in.\nTo start a new environment, use "dotenc init [environment]".');
|
|
13
|
-
return;
|
|
14
|
-
}
|
|
15
|
-
const environments = environmentArg.split(",");
|
|
16
|
-
const decryptedEnvs = await Promise.all(environments.map(async (environment, index) => {
|
|
17
|
-
const environmentFilePath = path.join(process.cwd(), `.env.${environment}.enc`);
|
|
18
|
-
if (!existsSync(environmentFilePath)) {
|
|
19
|
-
console.error(`Environment file not found: ${environmentFilePath}`);
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
const key = await getKey(environment, index);
|
|
23
|
-
const content = await decrypt(key, environmentFilePath);
|
|
24
|
-
const decryptedEnv = parseEnv(content);
|
|
25
|
-
return decryptedEnv;
|
|
26
|
-
}));
|
|
27
|
-
const decryptedEnv = decryptedEnvs.reduce((acc, env) => {
|
|
28
|
-
return { ...acc, ...env };
|
|
29
|
-
}, {});
|
|
30
|
-
// Get the local environment
|
|
31
|
-
let localEnv = {};
|
|
32
|
-
const localEnvironmentFilePath = path.join(process.cwd(), ".env");
|
|
33
|
-
if (existsSync(localEnvironmentFilePath)) {
|
|
34
|
-
const localEnvContent = await fs.readFile(localEnvironmentFilePath, "utf-8");
|
|
35
|
-
localEnv = parseEnv(localEnvContent);
|
|
36
|
-
}
|
|
37
|
-
// Merge the environment variables and run the command
|
|
38
|
-
const mergedEnv = { ...process.env, ...decryptedEnv, ...localEnv };
|
|
39
|
-
const child = spawn(command, args, {
|
|
40
|
-
env: mergedEnv,
|
|
41
|
-
stdio: "inherit",
|
|
42
|
-
});
|
|
43
|
-
child.on("exit", (code) => {
|
|
44
|
-
process.exit(code);
|
|
45
|
-
});
|
|
46
|
-
};
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { encrypt } from "./crypto.js";
|
|
4
|
-
export const createEnvironment = async (name, key) => {
|
|
5
|
-
const filePath = path.join(process.cwd(), `.env.${name}.enc`);
|
|
6
|
-
if (existsSync(filePath)) {
|
|
7
|
-
throw new Error(`Environment "${name}" already exists.`);
|
|
8
|
-
}
|
|
9
|
-
await encrypt(key, `# ${name} environment\n`, filePath);
|
|
10
|
-
};
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import fs from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
export const createLocalEnvironment = async () => {
|
|
5
|
-
const gitignorePath = path.join(process.cwd(), ".gitignore");
|
|
6
|
-
const envEntry = ".env";
|
|
7
|
-
let gitignoreContent = [];
|
|
8
|
-
if (existsSync(gitignorePath)) {
|
|
9
|
-
gitignoreContent = (await fs.readFile(gitignorePath, "utf8")).split("\n");
|
|
10
|
-
}
|
|
11
|
-
// Check if the .env entry already exists (ignoring comments and whitespace)
|
|
12
|
-
const isEnvIgnored = gitignoreContent.some((line) => line.trim() === envEntry);
|
|
13
|
-
if (!isEnvIgnored) {
|
|
14
|
-
// Append the .env entry to the .gitignore file
|
|
15
|
-
await fs.appendFile(gitignorePath, `\n# Ignore local environment file\n${envEntry}\n`);
|
|
16
|
-
}
|
|
17
|
-
const envPath = path.join(process.cwd(), ".env");
|
|
18
|
-
if (!existsSync(envPath)) {
|
|
19
|
-
await fs.writeFile(envPath, "# Local environment variables\n");
|
|
20
|
-
}
|
|
21
|
-
};
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { createId } from "@paralleldrive/cuid2";
|
|
2
|
-
import { getProjectConfig, setProjectConfig } from "./projectConfig.js";
|
|
3
|
-
export const createProject = async () => {
|
|
4
|
-
const config = await getProjectConfig();
|
|
5
|
-
if (config.projectId) {
|
|
6
|
-
return config;
|
|
7
|
-
}
|
|
8
|
-
const newConfig = {
|
|
9
|
-
projectId: createId(),
|
|
10
|
-
};
|
|
11
|
-
await setProjectConfig(newConfig);
|
|
12
|
-
return newConfig;
|
|
13
|
-
};
|
package/dist/helpers/crypto.js
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import crypto from "node:crypto";
|
|
2
|
-
import fs from "node:fs/promises";
|
|
3
|
-
// AES-256-GCM constants
|
|
4
|
-
const ALGORITHM = "aes-256-gcm";
|
|
5
|
-
const IV_LENGTH = 12; // 96 bits, recommended for GCM
|
|
6
|
-
const AUTH_TAG_LENGTH = 16; // 128 bits, standard for GCM
|
|
7
|
-
/**
|
|
8
|
-
* Encrypts a file using AES-256-GCM.
|
|
9
|
-
* @param {string} key - The encryption key (must be 32 bytes for AES-256).
|
|
10
|
-
* @param {string} input - The input string to encrypt.
|
|
11
|
-
* @param {string} outputFile - Path to the output encrypted file.
|
|
12
|
-
*/
|
|
13
|
-
export async function encrypt(key, input, outputFile) {
|
|
14
|
-
const keyBuffer = Buffer.from(key, "base64");
|
|
15
|
-
if (keyBuffer.length !== 32) {
|
|
16
|
-
throw new Error("Key must be 32 bytes (256 bits) for AES-256-GCM.");
|
|
17
|
-
}
|
|
18
|
-
// Generate a random IV
|
|
19
|
-
const iv = crypto.randomBytes(IV_LENGTH);
|
|
20
|
-
// Create the cipher
|
|
21
|
-
const cipher = crypto.createCipheriv(ALGORITHM, keyBuffer, iv);
|
|
22
|
-
// Encrypt the data
|
|
23
|
-
const encrypted = Buffer.concat([cipher.update(input), cipher.final()]);
|
|
24
|
-
// Get the auth tag
|
|
25
|
-
const authTag = cipher.getAuthTag();
|
|
26
|
-
// Combine IV + encrypted content + auth tag
|
|
27
|
-
const result = Buffer.concat([iv, encrypted, authTag]);
|
|
28
|
-
// Write the encrypted file
|
|
29
|
-
await fs.writeFile(outputFile, result);
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Decrypts a file using AES-256-GCM.
|
|
33
|
-
* @param {string} key - The decryption key (must be 32 bytes for AES-256).
|
|
34
|
-
* @param {string} inputFile - The input file to decrypt.
|
|
35
|
-
*/
|
|
36
|
-
export async function decrypt(key, inputFile) {
|
|
37
|
-
const keyBuffer = Buffer.from(key, "base64");
|
|
38
|
-
if (keyBuffer.length !== 32) {
|
|
39
|
-
throw new Error("Key must be 32 bytes (256 bits) for AES-256-GCM.");
|
|
40
|
-
}
|
|
41
|
-
// Read the encrypted file
|
|
42
|
-
const encryptedData = await fs.readFile(inputFile);
|
|
43
|
-
// Extract the IV from the start of the file
|
|
44
|
-
const iv = encryptedData.subarray(0, IV_LENGTH);
|
|
45
|
-
// Extract the auth tag from the end of the file
|
|
46
|
-
const authTag = encryptedData.subarray(encryptedData.length - AUTH_TAG_LENGTH);
|
|
47
|
-
// Extract the ciphertext (everything between IV and auth tag)
|
|
48
|
-
const ciphertext = encryptedData.subarray(IV_LENGTH, encryptedData.length - AUTH_TAG_LENGTH);
|
|
49
|
-
try {
|
|
50
|
-
// Create the decipher
|
|
51
|
-
const decipher = crypto.createDecipheriv(ALGORITHM, keyBuffer, iv);
|
|
52
|
-
decipher.setAuthTag(authTag);
|
|
53
|
-
// Decrypt the ciphertext
|
|
54
|
-
const decrypted = Buffer.concat([
|
|
55
|
-
decipher.update(ciphertext),
|
|
56
|
-
decipher.final(),
|
|
57
|
-
]);
|
|
58
|
-
return decrypted.toString();
|
|
59
|
-
}
|
|
60
|
-
catch (error) {
|
|
61
|
-
if (error instanceof Error &&
|
|
62
|
-
error.message.includes("unable to authenticate")) {
|
|
63
|
-
throw new Error("Failed to decrypt file. This could be because:\n" +
|
|
64
|
-
"1. The encryption key may be incorrect\n" +
|
|
65
|
-
"2. The encrypted file may be corrupted\n" +
|
|
66
|
-
"3. The encrypted file may have been tampered with");
|
|
67
|
-
}
|
|
68
|
-
throw error;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
2
|
-
import { getHomeConfig } from "./homeConfig.js";
|
|
3
|
-
/**
|
|
4
|
-
* Determines the default text editor for the system.
|
|
5
|
-
* @returns {string} The command to launch the default text editor.
|
|
6
|
-
*/
|
|
7
|
-
export const getDefaultEditor = async () => {
|
|
8
|
-
const config = await getHomeConfig();
|
|
9
|
-
// Check the editor field in the config file
|
|
10
|
-
if (config.editor) {
|
|
11
|
-
return config.editor;
|
|
12
|
-
}
|
|
13
|
-
// Check the EDITOR environment variable
|
|
14
|
-
if (process.env.EDITOR) {
|
|
15
|
-
return process.env.EDITOR;
|
|
16
|
-
}
|
|
17
|
-
// Check the VISUAL environment variable
|
|
18
|
-
if (process.env.VISUAL) {
|
|
19
|
-
return process.env.VISUAL;
|
|
20
|
-
}
|
|
21
|
-
// Platform-specific defaults
|
|
22
|
-
const platform = process.platform;
|
|
23
|
-
if (platform === "win32") {
|
|
24
|
-
// Windows: Use notepad as the fallback editor
|
|
25
|
-
return "notepad";
|
|
26
|
-
}
|
|
27
|
-
// Linux/macOS: Try nano, vim, or vi
|
|
28
|
-
const editors = ["nano", "vim", "vi"];
|
|
29
|
-
for (const editor of editors) {
|
|
30
|
-
try {
|
|
31
|
-
// Check if the editor is available
|
|
32
|
-
execSync(`command -v ${editor}`, { stdio: "ignore" });
|
|
33
|
-
return editor; // Return the first available editor
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
// Ignore errors and try the next editor
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
// If no editor is found, throw an error
|
|
40
|
-
throw new Error('No text editor found. Please set the EDITOR environment variable, configure an editor using "dotenc config editor <command>", or install a text editor (e.g., nano, vim, or notepad).');
|
|
41
|
-
};
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import fs from "node:fs/promises";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { z } from "zod";
|
|
6
|
-
const homeConfigSchema = z.object({
|
|
7
|
-
editor: z.string().nullish(),
|
|
8
|
-
});
|
|
9
|
-
const configPath = path.join(os.homedir(), ".dotenc", "config.json");
|
|
10
|
-
export const setHomeConfig = async (config) => {
|
|
11
|
-
const parsedConfig = homeConfigSchema.parse(config);
|
|
12
|
-
await fs.writeFile(configPath, JSON.stringify(parsedConfig, null, 2), {
|
|
13
|
-
mode: 0o600,
|
|
14
|
-
});
|
|
15
|
-
};
|
|
16
|
-
export const getHomeConfig = async () => {
|
|
17
|
-
if (existsSync(configPath)) {
|
|
18
|
-
const config = JSON.parse(await fs.readFile(configPath, "utf-8"));
|
|
19
|
-
return homeConfigSchema.parse(config);
|
|
20
|
-
}
|
|
21
|
-
return {};
|
|
22
|
-
};
|
package/dist/helpers/key.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import fs from "node:fs/promises";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { getProjectConfig } from "./projectConfig.js";
|
|
6
|
-
const keysFile = path.join(os.homedir(), ".dotenc", "keys.json");
|
|
7
|
-
export const getKey = async (environment, index = 0) => {
|
|
8
|
-
if (process.env.DOTENC_KEY) {
|
|
9
|
-
const keys = process.env.DOTENC_KEY.split(",");
|
|
10
|
-
return keys[index];
|
|
11
|
-
}
|
|
12
|
-
const { projectId } = await getProjectConfig();
|
|
13
|
-
if (existsSync(keysFile)) {
|
|
14
|
-
const keys = JSON.parse(await fs.readFile(keysFile, "utf-8"));
|
|
15
|
-
return keys[projectId][environment];
|
|
16
|
-
}
|
|
17
|
-
throw new Error("No key found. Please set the DOTENC_KEY environment variable or import the key using `dotenc key import <environment> <key>`.");
|
|
18
|
-
};
|
|
19
|
-
/**
|
|
20
|
-
* Adds or updates a key for a specific project and environment.
|
|
21
|
-
*/
|
|
22
|
-
export const addKey = async (projectId, environment, key) => {
|
|
23
|
-
// Ensure the keys file exists
|
|
24
|
-
if (!existsSync(keysFile)) {
|
|
25
|
-
await fs.mkdir(path.dirname(keysFile), { recursive: true });
|
|
26
|
-
await fs.writeFile(keysFile, "{}", { mode: 0o600 }); // Create an empty JSON file with secure permissions
|
|
27
|
-
}
|
|
28
|
-
// Read the existing keys
|
|
29
|
-
const keys = JSON.parse(await fs.readFile(keysFile, "utf8"));
|
|
30
|
-
// Add or update the key
|
|
31
|
-
if (!keys[projectId]) {
|
|
32
|
-
keys[projectId] = {};
|
|
33
|
-
}
|
|
34
|
-
keys[projectId][environment] = key;
|
|
35
|
-
// Write the updated keys back to the file
|
|
36
|
-
await fs.writeFile(keysFile, JSON.stringify(keys, null, 2), {
|
|
37
|
-
mode: 0o600,
|
|
38
|
-
});
|
|
39
|
-
};
|