@dotenc/cli 0.1.3 → 0.2.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/dist/commands/debug.js +3 -3
- package/dist/commands/edit.js +9 -7
- package/dist/commands/init.js +11 -11
- package/dist/commands/key/export.js +12 -0
- package/dist/commands/key/import.js +12 -0
- package/dist/commands/run.js +21 -9
- package/dist/helpers/createEnvironment.js +2 -2
- package/dist/helpers/crypto.js +13 -13
- package/dist/helpers/key.js +39 -0
- package/dist/helpers/parseEnv.js +76 -10
- package/dist/helpers/parseEnv.test.js +28 -0
- package/dist/program.js +12 -11
- package/package.json +5 -3
- package/dist/commands/token/export.js +0 -11
- package/dist/commands/token/import.js +0 -11
- package/dist/helpers/token.js +0 -39
- /package/dist/{commands/prompts → prompts}/chooseEnvironment.js +0 -0
- /package/dist/{commands/prompts → prompts}/createEnvironment.js +0 -0
package/dist/commands/debug.js
CHANGED
|
@@ -4,10 +4,10 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { decrypt, encrypt } from "../helpers/crypto.js";
|
|
6
6
|
export const debugCommand = async () => {
|
|
7
|
-
const
|
|
7
|
+
const key = crypto.randomBytes(32).toString("base64");
|
|
8
8
|
const encryptedFilePath = path.join(os.tmpdir(), "dotenc.enc");
|
|
9
|
-
await encrypt(
|
|
10
|
-
const content = await decrypt(
|
|
9
|
+
await encrypt(key, "Test", encryptedFilePath);
|
|
10
|
+
const content = await decrypt(key, encryptedFilePath);
|
|
11
11
|
await fs.unlink(encryptedFilePath);
|
|
12
12
|
if (content !== "Test") {
|
|
13
13
|
throw new Error("Decrypted content is not equal to the original content");
|
package/dist/commands/edit.js
CHANGED
|
@@ -6,8 +6,8 @@ import path from "node:path";
|
|
|
6
6
|
import { createHash } from "../helpers/createHash.js";
|
|
7
7
|
import { decrypt, encrypt } from "../helpers/crypto.js";
|
|
8
8
|
import { getDefaultEditor } from "../helpers/getDefaultEditor.js";
|
|
9
|
-
import {
|
|
10
|
-
import { chooseEnvironmentPrompt } from "
|
|
9
|
+
import { getKey } from "../helpers/key.js";
|
|
10
|
+
import { chooseEnvironmentPrompt } from "../prompts/chooseEnvironment.js";
|
|
11
11
|
export const editCommand = async (environmentArg) => {
|
|
12
12
|
let environment = environmentArg;
|
|
13
13
|
if (!environment) {
|
|
@@ -15,11 +15,12 @@ export const editCommand = async (environmentArg) => {
|
|
|
15
15
|
}
|
|
16
16
|
const environmentFilePath = path.join(process.cwd(), `.env.${environment}.enc`);
|
|
17
17
|
if (!existsSync(environmentFilePath)) {
|
|
18
|
-
|
|
18
|
+
console.error(`Environment file not found: ${environmentFilePath}`);
|
|
19
|
+
return;
|
|
19
20
|
}
|
|
20
|
-
const
|
|
21
|
+
const key = await getKey(environment);
|
|
21
22
|
const tempFilePath = path.join(os.tmpdir(), `.env.${environment}`);
|
|
22
|
-
const content = await decrypt(
|
|
23
|
+
const content = await decrypt(key, environmentFilePath);
|
|
23
24
|
await fs.writeFile(tempFilePath, content);
|
|
24
25
|
const initialHash = createHash(content);
|
|
25
26
|
const editor = await getDefaultEditor();
|
|
@@ -28,7 +29,8 @@ export const editCommand = async (environmentArg) => {
|
|
|
28
29
|
execSync(`${editor} ${tempFilePath}`, { stdio: "inherit" });
|
|
29
30
|
}
|
|
30
31
|
catch (error) {
|
|
31
|
-
|
|
32
|
+
console.error(`Failed to open editor: ${editor}`);
|
|
33
|
+
return;
|
|
32
34
|
}
|
|
33
35
|
const newContent = await fs.readFile(tempFilePath, "utf-8");
|
|
34
36
|
const finalHash = createHash(newContent);
|
|
@@ -36,7 +38,7 @@ export const editCommand = async (environmentArg) => {
|
|
|
36
38
|
console.log(`No changes were made to the environment file for "${environment}".`);
|
|
37
39
|
}
|
|
38
40
|
else {
|
|
39
|
-
await encrypt(
|
|
41
|
+
await encrypt(key, newContent, environmentFilePath);
|
|
40
42
|
console.log(`Encrypted environment file for "${environment}" and saved it to ${environmentFilePath}.`);
|
|
41
43
|
}
|
|
42
44
|
await fs.unlink(tempFilePath);
|
package/dist/commands/init.js
CHANGED
|
@@ -2,28 +2,28 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import { createEnvironment } from "../helpers/createEnvironment.js";
|
|
3
3
|
import { createLocalEnvironment } from "../helpers/createLocalEnvironment.js";
|
|
4
4
|
import { createProject } from "../helpers/createProject.js";
|
|
5
|
-
import {
|
|
6
|
-
import { createEnvironmentPrompt } from "
|
|
5
|
+
import { addKey } from "../helpers/key.js";
|
|
6
|
+
import { createEnvironmentPrompt } from "../prompts/createEnvironment.js";
|
|
7
7
|
export const initCommand = async (environmentArg) => {
|
|
8
8
|
// Generate a unique project ID
|
|
9
9
|
const { projectId } = await createProject();
|
|
10
10
|
// Setup local environment
|
|
11
11
|
await createLocalEnvironment();
|
|
12
|
-
// Generate a random
|
|
13
|
-
const
|
|
12
|
+
// Generate a random key
|
|
13
|
+
const key = crypto.randomBytes(32).toString("base64");
|
|
14
14
|
// Prompt for the environment name
|
|
15
15
|
let environment = environmentArg;
|
|
16
16
|
if (!environment) {
|
|
17
17
|
environment = await createEnvironmentPrompt("What should the environment be named?", "development");
|
|
18
18
|
}
|
|
19
|
-
await createEnvironment(environment,
|
|
20
|
-
// Store the
|
|
21
|
-
await
|
|
19
|
+
await createEnvironment(environment, key);
|
|
20
|
+
// Store the key
|
|
21
|
+
await addKey(projectId, environment, key);
|
|
22
22
|
// Output success message
|
|
23
23
|
console.log("Initialization complete!");
|
|
24
24
|
console.log("Next steps:");
|
|
25
|
-
console.log(`1. Use "dotenc edit
|
|
26
|
-
console.log(`2. Use "dotenc run -e ${environment} <command> [args...]" to run your application.`);
|
|
27
|
-
console.log('3. Use "dotenc init
|
|
28
|
-
console.log("4. Use the git-ignored .env file
|
|
25
|
+
console.log(`1. Use "dotenc edit ${environment}" to securely edit your environment variables.`);
|
|
26
|
+
console.log(`2. Use "dotenc run -e ${environment} <command> [args...]" or "DOTENC_ENV=${environment} dotenc run <command> [args...]" to run your application.`);
|
|
27
|
+
console.log('3. Use "dotenc init [environment]" to initialize a new environment.');
|
|
28
|
+
console.log("4. Use the git-ignored .env file for local development. It will have priority over any encrypted environment variables.");
|
|
29
29
|
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { getKey } from "../../helpers/key.js";
|
|
2
|
+
import { getProjectConfig } from "../../helpers/projectConfig.js";
|
|
3
|
+
export const keyExportCommand = async (environmentArg) => {
|
|
4
|
+
const environment = environmentArg;
|
|
5
|
+
const { projectId } = await getProjectConfig();
|
|
6
|
+
if (!projectId) {
|
|
7
|
+
console.error('No project found. Run "dotenc init" to create one.');
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const key = await getKey(environment);
|
|
11
|
+
console.log(`Key for the ${environment} environment: ${key}`);
|
|
12
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { addKey } from "../../helpers/key.js";
|
|
2
|
+
import { getProjectConfig } from "../../helpers/projectConfig.js";
|
|
3
|
+
export const keyImportCommand = async (key, environmentArg) => {
|
|
4
|
+
const environment = environmentArg;
|
|
5
|
+
const { projectId } = await getProjectConfig();
|
|
6
|
+
if (!projectId) {
|
|
7
|
+
console.error('No project found. Run "dotenc init" to create one.');
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
await addKey(projectId, environment, key);
|
|
11
|
+
console.log(`Key imported to the ${environment} environment.`);
|
|
12
|
+
};
|
package/dist/commands/run.js
CHANGED
|
@@ -3,18 +3,30 @@ import { existsSync } from "node:fs";
|
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { decrypt } from "../helpers/crypto.js";
|
|
6
|
+
import { getKey } from "../helpers/key.js";
|
|
6
7
|
import { parseEnv } from "../helpers/parseEnv.js";
|
|
7
|
-
|
|
8
|
-
export const runCommand = async (environmentArg, command, args) => {
|
|
8
|
+
export const runCommand = async (command, args, options) => {
|
|
9
9
|
// Get the environment
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
14
|
}
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
15
|
+
const environments = environmentArg.split(",");
|
|
16
|
+
const decryptedEnvs = await Promise.all(environments.map(async (environment) => {
|
|
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);
|
|
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
|
+
}, {});
|
|
18
30
|
// Get the local environment
|
|
19
31
|
let localEnv = {};
|
|
20
32
|
const localEnvironmentFilePath = path.join(process.cwd(), ".env");
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { encrypt } from "./crypto.js";
|
|
4
|
-
export const createEnvironment = async (name,
|
|
4
|
+
export const createEnvironment = async (name, key) => {
|
|
5
5
|
const filePath = path.join(process.cwd(), `.env.${name}.enc`);
|
|
6
6
|
if (existsSync(filePath)) {
|
|
7
7
|
throw new Error(`Environment "${name}" already exists.`);
|
|
8
8
|
}
|
|
9
|
-
await encrypt(
|
|
9
|
+
await encrypt(key, `# ${name} environment\n`, filePath);
|
|
10
10
|
};
|
package/dist/helpers/crypto.js
CHANGED
|
@@ -6,19 +6,19 @@ const IV_LENGTH = 12; // 96 bits, recommended for GCM
|
|
|
6
6
|
const AUTH_TAG_LENGTH = 16; // 128 bits, standard for GCM
|
|
7
7
|
/**
|
|
8
8
|
* Encrypts a file using AES-256-GCM.
|
|
9
|
-
* @param {string}
|
|
9
|
+
* @param {string} key - The encryption key (must be 32 bytes for AES-256).
|
|
10
10
|
* @param {string} input - The input string to encrypt.
|
|
11
11
|
* @param {string} outputFile - Path to the output encrypted file.
|
|
12
12
|
*/
|
|
13
|
-
export async function encrypt(
|
|
14
|
-
const
|
|
15
|
-
if (
|
|
16
|
-
throw new Error("
|
|
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
17
|
}
|
|
18
18
|
// Generate a random IV
|
|
19
19
|
const iv = crypto.randomBytes(IV_LENGTH);
|
|
20
20
|
// Create the cipher
|
|
21
|
-
const cipher = crypto.createCipheriv(ALGORITHM,
|
|
21
|
+
const cipher = crypto.createCipheriv(ALGORITHM, keyBuffer, iv);
|
|
22
22
|
// Encrypt the data
|
|
23
23
|
const encrypted = Buffer.concat([cipher.update(input), cipher.final()]);
|
|
24
24
|
// Get the auth tag
|
|
@@ -30,13 +30,13 @@ export async function encrypt(token, input, outputFile) {
|
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
32
|
* Decrypts a file using AES-256-GCM.
|
|
33
|
-
* @param {string}
|
|
33
|
+
* @param {string} key - The decryption key (must be 32 bytes for AES-256).
|
|
34
34
|
* @param {string} inputFile - The input file to decrypt.
|
|
35
35
|
*/
|
|
36
|
-
export async function decrypt(
|
|
37
|
-
const
|
|
38
|
-
if (
|
|
39
|
-
throw new Error("
|
|
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
40
|
}
|
|
41
41
|
// Read the encrypted file
|
|
42
42
|
const encryptedData = await fs.readFile(inputFile);
|
|
@@ -48,7 +48,7 @@ export async function decrypt(token, inputFile) {
|
|
|
48
48
|
const ciphertext = encryptedData.subarray(IV_LENGTH, encryptedData.length - AUTH_TAG_LENGTH);
|
|
49
49
|
try {
|
|
50
50
|
// Create the decipher
|
|
51
|
-
const decipher = crypto.createDecipheriv(ALGORITHM,
|
|
51
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, keyBuffer, iv);
|
|
52
52
|
decipher.setAuthTag(authTag);
|
|
53
53
|
// Decrypt the ciphertext
|
|
54
54
|
const decrypted = Buffer.concat([
|
|
@@ -61,7 +61,7 @@ export async function decrypt(token, inputFile) {
|
|
|
61
61
|
if (error instanceof Error &&
|
|
62
62
|
error.message.includes("unable to authenticate")) {
|
|
63
63
|
throw new Error("Failed to decrypt file. This could be because:\n" +
|
|
64
|
-
"1. The encryption
|
|
64
|
+
"1. The encryption key may be incorrect\n" +
|
|
65
65
|
"2. The encrypted file may be corrupted\n" +
|
|
66
66
|
"3. The encrypted file may have been tampered with");
|
|
67
67
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
export const getKey = async (environment) => {
|
|
7
|
+
if (process.env.DOTENC_KEY) {
|
|
8
|
+
return process.env.DOTENC_KEY;
|
|
9
|
+
}
|
|
10
|
+
const { projectId } = await getProjectConfig();
|
|
11
|
+
const keysFile = path.join(os.homedir(), ".dotenc", "keys.json");
|
|
12
|
+
if (existsSync(keysFile)) {
|
|
13
|
+
const keys = JSON.parse(await fs.readFile(keysFile, "utf-8"));
|
|
14
|
+
return keys[projectId][environment];
|
|
15
|
+
}
|
|
16
|
+
throw new Error("No key found. Please set the DOTENC_KEY environment variable or import the key using `dotenc import-key -e <environment> <key>`.");
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Adds or updates a key for a specific project and environment.
|
|
20
|
+
*/
|
|
21
|
+
export const addKey = async (projectId, environment, key) => {
|
|
22
|
+
const keysFile = path.join(os.homedir(), ".dotenc", "keys.json");
|
|
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
|
+
};
|
package/dist/helpers/parseEnv.js
CHANGED
|
@@ -1,21 +1,87 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Parses a .env file content and returns an object of key-value pairs.
|
|
3
|
+
* Supports multiline values when they are properly quoted.
|
|
4
|
+
* Supports nested quotes (e.g., "value with 'quotes' inside" or 'value with "quotes" inside')
|
|
3
5
|
*/
|
|
4
6
|
export const parseEnv = (content) => {
|
|
5
7
|
const env = {};
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
let currentKey = "";
|
|
9
|
+
let currentValue = "";
|
|
10
|
+
let isInSingleQuotes = false;
|
|
11
|
+
let isInDoubleQuotes = false;
|
|
12
|
+
let isInComment = false;
|
|
13
|
+
let isInKey = true;
|
|
14
|
+
const commit = (trim) => {
|
|
15
|
+
if (currentKey.trim()) {
|
|
16
|
+
env[currentKey.trim()] = trim ? currentValue.trim() : currentValue;
|
|
17
|
+
}
|
|
18
|
+
currentKey = "";
|
|
19
|
+
currentValue = "";
|
|
20
|
+
isInSingleQuotes = false;
|
|
21
|
+
isInDoubleQuotes = false;
|
|
22
|
+
isInComment = false;
|
|
23
|
+
isInKey = true;
|
|
24
|
+
};
|
|
25
|
+
for (const char of content) {
|
|
26
|
+
// Handle single quoted content
|
|
27
|
+
if (isInSingleQuotes) {
|
|
28
|
+
if (char === "'") {
|
|
29
|
+
commit(false);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
currentValue += char;
|
|
33
|
+
}
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
// Handle double quoted content
|
|
37
|
+
if (isInDoubleQuotes) {
|
|
38
|
+
if (char === '"') {
|
|
39
|
+
commit(false);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
currentValue += char;
|
|
43
|
+
}
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
// Handle comments
|
|
47
|
+
if (isInComment) {
|
|
48
|
+
if (char === "\n") {
|
|
49
|
+
isInComment = false;
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (char === "#") {
|
|
54
|
+
isInComment = true;
|
|
11
55
|
continue;
|
|
12
56
|
}
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
57
|
+
// Handle keys
|
|
58
|
+
if (isInKey) {
|
|
59
|
+
if (char === "=") {
|
|
60
|
+
isInKey = false;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
currentKey += char;
|
|
64
|
+
}
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
// Handle newlines
|
|
68
|
+
if (char === "\n") {
|
|
69
|
+
commit(true);
|
|
70
|
+
}
|
|
71
|
+
// Handle single quote opening
|
|
72
|
+
if (char === "'") {
|
|
73
|
+
isInSingleQuotes = true;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
// Handle double quote opening
|
|
77
|
+
if (char === '"') {
|
|
78
|
+
isInDoubleQuotes = true;
|
|
79
|
+
continue;
|
|
18
80
|
}
|
|
81
|
+
// Handle value
|
|
82
|
+
currentValue += char;
|
|
19
83
|
}
|
|
84
|
+
// Handle EOF
|
|
85
|
+
commit(true);
|
|
20
86
|
return env;
|
|
21
87
|
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { parseEnv } from "./parseEnv.js";
|
|
3
|
+
describe("parseEnv", () => {
|
|
4
|
+
test("should parse a simple key-value pair", () => {
|
|
5
|
+
const env = parseEnv("FOO=bar");
|
|
6
|
+
expect(env.FOO).toBe("bar");
|
|
7
|
+
});
|
|
8
|
+
test("should parse a key-value pair with a space", () => {
|
|
9
|
+
const env = parseEnv("FOO = bar");
|
|
10
|
+
expect(env.FOO).toBe("bar");
|
|
11
|
+
});
|
|
12
|
+
test("should parse a complex env file", () => {
|
|
13
|
+
const env = parseEnv(`FOO="
|
|
14
|
+
bar"
|
|
15
|
+
BAR='baz
|
|
16
|
+
foo
|
|
17
|
+
'BAZ=123
|
|
18
|
+
|
|
19
|
+
# Comment here "!#' foo
|
|
20
|
+
|
|
21
|
+
HELLO = WORLD
|
|
22
|
+
`);
|
|
23
|
+
expect(env.FOO).toBe("\nbar");
|
|
24
|
+
expect(env.BAR).toBe("baz\nfoo\n");
|
|
25
|
+
expect(env.BAZ).toBe("123");
|
|
26
|
+
expect(env.HELLO).toBe("WORLD");
|
|
27
|
+
});
|
|
28
|
+
});
|
package/dist/program.js
CHANGED
|
@@ -6,9 +6,9 @@ import { configCommand } from "./commands/config.js";
|
|
|
6
6
|
import { debugCommand } from "./commands/debug.js";
|
|
7
7
|
import { editCommand } from "./commands/edit.js";
|
|
8
8
|
import { initCommand } from "./commands/init.js";
|
|
9
|
+
import { keyExportCommand } from "./commands/key/export.js";
|
|
10
|
+
import { keyImportCommand } from "./commands/key/import.js";
|
|
9
11
|
import { runCommand } from "./commands/run.js";
|
|
10
|
-
import { tokenExportCommand } from "./commands/token/export.js";
|
|
11
|
-
import { tokenImportCommand } from "./commands/token/import.js";
|
|
12
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
13
|
const __dirname = path.dirname(__filename);
|
|
14
14
|
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "../package.json"), "utf8"));
|
|
@@ -26,18 +26,19 @@ program
|
|
|
26
26
|
.description("edit an environment")
|
|
27
27
|
.action(editCommand);
|
|
28
28
|
program
|
|
29
|
-
.command("run <
|
|
29
|
+
.command("run <command> [args...]")
|
|
30
|
+
.addOption(new Option("-e, --environment <environment>", "the environment to run the command in"))
|
|
30
31
|
.description("run a command in an environment")
|
|
31
32
|
.action(runCommand);
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
.command("import <environment> <
|
|
35
|
-
.description("import a
|
|
36
|
-
.action(
|
|
37
|
-
|
|
33
|
+
const key = program.command("key").description("Manage stored keys");
|
|
34
|
+
key
|
|
35
|
+
.command("import <environment> <key>")
|
|
36
|
+
.description("import a key for an environment")
|
|
37
|
+
.action(keyImportCommand);
|
|
38
|
+
key
|
|
38
39
|
.command("export <environment>")
|
|
39
|
-
.description("export a
|
|
40
|
-
.action(
|
|
40
|
+
.description("export a key from an environment")
|
|
41
|
+
.action(keyExportCommand);
|
|
41
42
|
program
|
|
42
43
|
.command("config <key> [value]")
|
|
43
44
|
.addOption(new Option("-r, --remove", "remove a configuration key"))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotenc/cli",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "🔐 Secure, encrypted environment variables that live in your codebase",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
"@types/node": "^22.13.7",
|
|
34
34
|
"tsc-alias": "^1.8.11",
|
|
35
35
|
"tsx": "^4.19.3",
|
|
36
|
-
"typescript": "^5.8.2"
|
|
36
|
+
"typescript": "^5.8.2",
|
|
37
|
+
"vitest": "^3.0.9"
|
|
37
38
|
},
|
|
38
39
|
"dependencies": {
|
|
39
40
|
"@paralleldrive/cuid2": "^2.2.2",
|
|
@@ -44,6 +45,7 @@
|
|
|
44
45
|
"scripts": {
|
|
45
46
|
"dev": "tsx src/cli.ts",
|
|
46
47
|
"start": "node dist/cli.js",
|
|
47
|
-
"build": "tsc && tsc-alias"
|
|
48
|
+
"build": "tsc && tsc-alias",
|
|
49
|
+
"test": "vitest"
|
|
48
50
|
}
|
|
49
51
|
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { getProjectConfig } from "../../helpers/projectConfig.js";
|
|
2
|
-
import { getToken } from "../../helpers/token.js";
|
|
3
|
-
export const tokenExportCommand = async (environmentArg) => {
|
|
4
|
-
const environment = environmentArg;
|
|
5
|
-
const { projectId } = await getProjectConfig();
|
|
6
|
-
if (!projectId) {
|
|
7
|
-
throw new Error('No project found. Run "dotenc init" to create one.');
|
|
8
|
-
}
|
|
9
|
-
const token = await getToken(environment);
|
|
10
|
-
console.log(`Token for the ${environment} environment: ${token}`);
|
|
11
|
-
};
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { getProjectConfig } from "../../helpers/projectConfig.js";
|
|
2
|
-
import { addToken } from "../../helpers/token.js";
|
|
3
|
-
export const tokenImportCommand = async (token, environmentArg) => {
|
|
4
|
-
const environment = environmentArg;
|
|
5
|
-
const { projectId } = await getProjectConfig();
|
|
6
|
-
if (!projectId) {
|
|
7
|
-
throw new Error('No project found. Run "dotenc init" to create one.');
|
|
8
|
-
}
|
|
9
|
-
await addToken(projectId, environment, token);
|
|
10
|
-
console.log(`Token imported to the ${environment} environment.`);
|
|
11
|
-
};
|
package/dist/helpers/token.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
|
-
export const getToken = async (environment) => {
|
|
7
|
-
if (process.env.DOTENC_TOKEN) {
|
|
8
|
-
return process.env.DOTENC_TOKEN;
|
|
9
|
-
}
|
|
10
|
-
const { projectId } = await getProjectConfig();
|
|
11
|
-
const tokensFile = path.join(os.homedir(), ".dotenc", "tokens.json");
|
|
12
|
-
if (existsSync(tokensFile)) {
|
|
13
|
-
const tokens = JSON.parse(await fs.readFile(tokensFile, "utf-8"));
|
|
14
|
-
return tokens[projectId][environment];
|
|
15
|
-
}
|
|
16
|
-
throw new Error("No token found. Please set the TOKEN environment variable or import the token using `dotenc import-token -e <environment> <token>`.");
|
|
17
|
-
};
|
|
18
|
-
/**
|
|
19
|
-
* Adds or updates a token for a specific project and environment.
|
|
20
|
-
*/
|
|
21
|
-
export const addToken = async (projectId, environment, token) => {
|
|
22
|
-
const tokensFile = path.join(os.homedir(), ".dotenc", "tokens.json");
|
|
23
|
-
// Ensure the tokens file exists
|
|
24
|
-
if (!existsSync(tokensFile)) {
|
|
25
|
-
await fs.mkdir(path.dirname(tokensFile), { recursive: true });
|
|
26
|
-
await fs.writeFile(tokensFile, "{}", { mode: 0o600 }); // Create an empty JSON file with secure permissions
|
|
27
|
-
}
|
|
28
|
-
// Read the existing tokens
|
|
29
|
-
const tokens = JSON.parse(await fs.readFile(tokensFile, "utf8"));
|
|
30
|
-
// Add or update the token
|
|
31
|
-
if (!tokens[projectId]) {
|
|
32
|
-
tokens[projectId] = {};
|
|
33
|
-
}
|
|
34
|
-
tokens[projectId][environment] = token;
|
|
35
|
-
// Write the updated tokens back to the file
|
|
36
|
-
await fs.writeFile(tokensFile, JSON.stringify(tokens, null, 2), {
|
|
37
|
-
mode: 0o600,
|
|
38
|
-
});
|
|
39
|
-
};
|
|
File without changes
|
|
File without changes
|