@dotenc/cli 0.1.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.
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@dotenc/cli",
3
+ "version": "0.1.0",
4
+ "description": "🔐 Secure, encrypted environment variables that live in your codebase",
5
+ "type": "module",
6
+ "bin": {
7
+ "dotenc": "./dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "dev": "tsx src/cli.ts",
14
+ "build": "tsc"
15
+ },
16
+ "keywords": [
17
+ "environment",
18
+ "variables",
19
+ "safe",
20
+ "secure",
21
+ "codebase",
22
+ "cli",
23
+ "command",
24
+ "line",
25
+ "tool",
26
+ "utility",
27
+ "env",
28
+ "box",
29
+ "dotenv",
30
+ "encrypted",
31
+ "codebase"
32
+ ],
33
+ "author": "Ivan Filho <i@ivanfilho.com>",
34
+ "license": "MIT",
35
+ "devDependencies": {
36
+ "@biomejs/biome": "^1.9.4",
37
+ "@types/node": "^22.13.7",
38
+ "tsx": "^4.19.3",
39
+ "typescript": "^5.8.2"
40
+ },
41
+ "dependencies": {
42
+ "@paralleldrive/cuid2": "^2.2.2",
43
+ "commander": "^13.1.0",
44
+ "inquirer": "^12.4.2",
45
+ "zod": "^3.24.2"
46
+ }
47
+ }
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+ import { Command, Option } from "commander";
3
+ import pkg from "../package.json" assert { type: "json" };
4
+ import { configCommand } from "./commands/config";
5
+ import { debugCommand } from "./commands/debug";
6
+ import { editCommand } from "./commands/edit";
7
+ import { initCommand } from "./commands/init";
8
+ import { runCommand } from "./commands/run";
9
+ import { tokenExportCommand } from "./commands/token/export";
10
+ import { tokenImportCommand } from "./commands/token/import";
11
+ const program = new Command();
12
+ program.name("dotenc").description(pkg.description).version(pkg.version);
13
+ if (process.env.NODE_ENV !== "production") {
14
+ program.command("debug").description("Debug the CLI").action(debugCommand);
15
+ }
16
+ program
17
+ .command("init [environment]")
18
+ .description("Initialize a new safe environment")
19
+ .action(initCommand);
20
+ program
21
+ .command("edit [environment]")
22
+ .description("Edit an environment")
23
+ .action(editCommand);
24
+ program
25
+ .command("run <environment> <command> [args...]")
26
+ .description("Run a command in an environment")
27
+ .action(runCommand);
28
+ const token = program.command("token").description("Manage stored tokens");
29
+ token
30
+ .command("import <environment> <token>")
31
+ .description("Import a token for an environment")
32
+ .action(tokenImportCommand);
33
+ token
34
+ .command("export <environment>")
35
+ .description("Export a token from an environment")
36
+ .action(tokenExportCommand);
37
+ program
38
+ .command("config <key> [value]")
39
+ .addOption(new Option("-r, --remove", "Remove a configuration key"))
40
+ .description("Manage global configuration")
41
+ .action(configCommand);
42
+ program.parse();
@@ -0,0 +1,15 @@
1
+ import { getHomeConfig, setHomeConfig } from "../helpers/homeConfig";
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
+ };
@@ -0,0 +1,16 @@
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";
6
+ export const debugCommand = async () => {
7
+ const token = crypto.randomBytes(32).toString("base64");
8
+ const encryptedFilePath = path.join(os.tmpdir(), "dotenc.enc");
9
+ await encrypt(token, "Test", encryptedFilePath);
10
+ const content = await decrypt(token, 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
+ };
@@ -0,0 +1,44 @@
1
+ import { execSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import fs from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { createHash } from "../helpers/createHash";
7
+ import { decrypt, encrypt } from "../helpers/crypto";
8
+ import { getDefaultEditor } from "../helpers/getDefaultEditor";
9
+ import { getToken } from "../helpers/token";
10
+ import { chooseEnvironmentPrompt } from "./prompts/chooseEnvironment";
11
+ export const editCommand = async (environmentArg) => {
12
+ let environment = environmentArg;
13
+ if (!environment) {
14
+ environment = await chooseEnvironmentPrompt("What environment do you want to edit?");
15
+ }
16
+ const environmentFilePath = path.join(process.cwd(), `.env.${environment}.enc`);
17
+ if (!existsSync(environmentFilePath)) {
18
+ throw new Error(`Environment file not found: ${environmentFilePath}`);
19
+ }
20
+ const token = await getToken(environment);
21
+ const tempFilePath = path.join(os.tmpdir(), `.env.${environment}`);
22
+ const content = await decrypt(token, environmentFilePath);
23
+ await fs.writeFile(tempFilePath, content);
24
+ const initialHash = createHash(content);
25
+ const editor = await getDefaultEditor();
26
+ try {
27
+ // This will block until the editor process is closed
28
+ execSync(`${editor} ${tempFilePath}`, { stdio: "inherit" });
29
+ }
30
+ catch (error) {
31
+ throw new Error(`Failed to open editor: ${editor}`);
32
+ }
33
+ const newContent = await fs.readFile(tempFilePath, "utf-8");
34
+ const finalHash = createHash(newContent);
35
+ if (initialHash === finalHash) {
36
+ console.log(`No changes were made to the environment file for "${environment}".`);
37
+ }
38
+ else {
39
+ await encrypt(token, newContent, environmentFilePath);
40
+ console.log(`Encrypted environment file for "${environment}" and saved it to ${environmentFilePath}.`);
41
+ }
42
+ await fs.unlink(tempFilePath);
43
+ console.debug(`Temporary file deleted: ${tempFilePath}`);
44
+ };
@@ -0,0 +1,29 @@
1
+ import crypto from "node:crypto";
2
+ import { createEnvironment } from "../helpers/createEnvironment";
3
+ import { createLocalEnvironment } from "../helpers/createLocalEnvironment";
4
+ import { createProject } from "../helpers/createProject";
5
+ import { addToken } from "../helpers/token";
6
+ import { createEnvironmentPrompt } from "./prompts/createEnvironment";
7
+ export const initCommand = async (environmentArg) => {
8
+ // Generate a unique project ID
9
+ const { projectId } = await createProject();
10
+ // Setup local environment
11
+ await createLocalEnvironment();
12
+ // Generate a random token
13
+ const token = crypto.randomBytes(32).toString("base64");
14
+ // Prompt for the environment name
15
+ let environment = environmentArg;
16
+ if (!environment) {
17
+ environment = await createEnvironmentPrompt("What should the environment be named?", "development");
18
+ }
19
+ await createEnvironment(environment, token);
20
+ // Store the token
21
+ await addToken(projectId, environment, token);
22
+ // Output success message
23
+ console.log("Initialization complete!");
24
+ console.log("Next steps:");
25
+ console.log(`1. Use "npx dotenc edit -e ${environment}" to securely edit your safe environment variables.`);
26
+ console.log(`2. Use "npx dotenc run -e ${environment} <command> [args...]" to run your application.`);
27
+ console.log('3. Use "npx dotenc init -e [environment]" to initialize a new environment.');
28
+ console.log("4. Use the git-ignored .env file to edit your local environment variables. They will have priority over any safe environment.");
29
+ };
@@ -0,0 +1,18 @@
1
+ import inquirer from "inquirer";
2
+ import fs from "node:fs/promises";
3
+ export const chooseEnvironmentPrompt = async (message) => {
4
+ const files = await fs.readdir(process.cwd());
5
+ const envFiles = files.filter((file) => file.startsWith(".env.") && file.endsWith(".enc"));
6
+ if (!envFiles.length) {
7
+ console.log('No environment files found. To create a new environment, run "npx dotenc init"');
8
+ }
9
+ const result = await inquirer.prompt([
10
+ {
11
+ type: "list",
12
+ name: "environment",
13
+ message,
14
+ choices: envFiles.map((file) => file.replace(".env.", "").replace(".enc", "")),
15
+ },
16
+ ]);
17
+ return result.environment;
18
+ };
@@ -0,0 +1,12 @@
1
+ import inquirer from "inquirer";
2
+ export const createEnvironmentPrompt = async (message, defaultValue) => {
3
+ const result = await inquirer.prompt([
4
+ {
5
+ type: "input",
6
+ name: "environment",
7
+ message,
8
+ default: defaultValue,
9
+ },
10
+ ]);
11
+ return result.environment;
12
+ };
@@ -0,0 +1,34 @@
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";
6
+ import { parseEnv } from "../helpers/parseEnv";
7
+ import { getToken } from "../helpers/token";
8
+ export const runCommand = async (environmentArg, command, args) => {
9
+ // Get the environment
10
+ const environment = environmentArg;
11
+ const environmentFilePath = path.join(process.cwd(), `.env.${environment}.enc`);
12
+ if (!existsSync(environmentFilePath)) {
13
+ throw new Error(`Environment file not found: ${environmentFilePath}`);
14
+ }
15
+ const token = await getToken(environment);
16
+ const content = await decrypt(token, environmentFilePath);
17
+ const decryptedEnv = parseEnv(content);
18
+ // Get the local environment
19
+ let localEnv = {};
20
+ const localEnvironmentFilePath = path.join(process.cwd(), ".env");
21
+ if (existsSync(localEnvironmentFilePath)) {
22
+ const localEnvContent = await fs.readFile(localEnvironmentFilePath, "utf-8");
23
+ localEnv = parseEnv(localEnvContent);
24
+ }
25
+ // Merge the environment variables and run the command
26
+ const mergedEnv = { ...process.env, ...decryptedEnv, ...localEnv };
27
+ const child = spawn(command, args, {
28
+ env: mergedEnv,
29
+ stdio: "inherit",
30
+ });
31
+ child.on("exit", (code) => {
32
+ process.exit(code);
33
+ });
34
+ };
@@ -0,0 +1,11 @@
1
+ import { getProjectConfig } from "../../helpers/projectConfig";
2
+ import { getToken } from "../../helpers/token";
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 "npx dotenc init" to create one.');
8
+ }
9
+ const token = await getToken(environment);
10
+ console.log(`Token for the ${environment} environment: ${token}`);
11
+ };
@@ -0,0 +1,11 @@
1
+ import { getProjectConfig } from "../../helpers/projectConfig";
2
+ import { addToken } from "../../helpers/token";
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 "npx dotenc init" to create one.');
8
+ }
9
+ await addToken(projectId, environment, token);
10
+ console.log(`Token imported to the ${environment} environment.`);
11
+ };
@@ -0,0 +1,11 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { encrypt } from "./crypto";
4
+ export const createEnvironment = async (name, token) => {
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(token, `# ${name} environment\n`, filePath);
10
+ console.debug(`Environment "${name}" created.`);
11
+ };
@@ -0,0 +1,7 @@
1
+ import crypto from "node:crypto";
2
+ /**
3
+ * Computes a hash of the input string.
4
+ */
5
+ export const createHash = (input) => {
6
+ return crypto.createHash("sha256").update(input).digest("hex");
7
+ };
@@ -0,0 +1,29 @@
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
+ console.debug("Updated .gitignore to ignore .env file.");
17
+ }
18
+ else {
19
+ console.debug(".env file is already ignored in .gitignore.");
20
+ }
21
+ const envPath = path.join(process.cwd(), ".env");
22
+ if (existsSync(envPath)) {
23
+ console.debug(".env file already exists.");
24
+ }
25
+ else {
26
+ await fs.writeFile(envPath, "");
27
+ console.debug("Created .env file.");
28
+ }
29
+ };
@@ -0,0 +1,13 @@
1
+ import { createId } from "@paralleldrive/cuid2";
2
+ import { getProjectConfig, setProjectConfig } from "./projectConfig";
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
+ };
@@ -0,0 +1,72 @@
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} token - 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(token, input, outputFile) {
14
+ const tokenBuffer = Buffer.from(token, "base64");
15
+ if (tokenBuffer.length !== 32) {
16
+ throw new Error("Token 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, tokenBuffer, 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
+ console.debug(`File encrypted successfully: ${outputFile}`);
31
+ }
32
+ /**
33
+ * Decrypts a file using AES-256-GCM.
34
+ * @param {string} token - The decryption key (must be 32 bytes for AES-256).
35
+ * @param {string} inputFile - The input file to decrypt.
36
+ */
37
+ export async function decrypt(token, inputFile) {
38
+ const tokenBuffer = Buffer.from(token, "base64");
39
+ if (tokenBuffer.length !== 32) {
40
+ throw new Error("Token must be 32 bytes (256 bits) for AES-256-GCM.");
41
+ }
42
+ // Read the encrypted file
43
+ const encryptedData = await fs.readFile(inputFile);
44
+ // Extract the IV from the start of the file
45
+ const iv = encryptedData.subarray(0, IV_LENGTH);
46
+ // Extract the auth tag from the end of the file
47
+ const authTag = encryptedData.subarray(encryptedData.length - AUTH_TAG_LENGTH);
48
+ // Extract the ciphertext (everything between IV and auth tag)
49
+ const ciphertext = encryptedData.subarray(IV_LENGTH, encryptedData.length - AUTH_TAG_LENGTH);
50
+ try {
51
+ // Create the decipher
52
+ const decipher = crypto.createDecipheriv(ALGORITHM, tokenBuffer, iv);
53
+ decipher.setAuthTag(authTag);
54
+ // Decrypt the ciphertext
55
+ const decrypted = Buffer.concat([
56
+ decipher.update(ciphertext),
57
+ decipher.final(),
58
+ ]);
59
+ console.debug(`File decrypted successfully: ${inputFile}`);
60
+ return decrypted.toString();
61
+ }
62
+ catch (error) {
63
+ if (error instanceof Error &&
64
+ error.message.includes("unable to authenticate")) {
65
+ throw new Error("Failed to decrypt file. This could be because:\n" +
66
+ "1. The encryption token may be incorrect\n" +
67
+ "2. The encrypted file may be corrupted\n" +
68
+ "3. The encrypted file may have been tampered with");
69
+ }
70
+ throw error;
71
+ }
72
+ }
@@ -0,0 +1,41 @@
1
+ import { execSync } from "node:child_process";
2
+ import { getHomeConfig } from "./homeConfig";
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 "npx dotenc config editor <command>", or install a text editor (e.g., nano, vim, or notepad).');
41
+ };
@@ -0,0 +1,23 @@
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
+ console.debug("config.json saved");
16
+ };
17
+ export const getHomeConfig = async () => {
18
+ if (existsSync(configPath)) {
19
+ const config = JSON.parse(await fs.readFile(configPath, "utf-8"));
20
+ return homeConfigSchema.parse(config);
21
+ }
22
+ return {};
23
+ };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Parses a .env file content and returns an object of key-value pairs.
3
+ */
4
+ export const parseEnv = (content) => {
5
+ const env = {};
6
+ const lines = content.split("\n");
7
+ for (const line of lines) {
8
+ const trimmedLine = line.trim();
9
+ // Skip empty lines and comments
10
+ if (!trimmedLine || trimmedLine.startsWith("#")) {
11
+ continue;
12
+ }
13
+ // Parse key-value pairs
14
+ const [key, ...valueParts] = trimmedLine.split("=");
15
+ const value = valueParts.join("=").trim();
16
+ if (key) {
17
+ env[key.trim()] = value.replace(/(^['"])|(['"]$)/g, ""); // Remove surrounding quotes
18
+ }
19
+ }
20
+ return env;
21
+ };
@@ -0,0 +1,22 @@
1
+ import { existsSync } from "node:fs";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { z } from "zod";
5
+ const projectConfigSchema = z.object({
6
+ projectId: z.string(),
7
+ });
8
+ const configPath = path.join(process.cwd(), "dotenc.json");
9
+ export const setProjectConfig = async (config) => {
10
+ const parsedConfig = projectConfigSchema.parse(config);
11
+ await fs.writeFile(configPath, JSON.stringify(parsedConfig, null, 2), {
12
+ mode: 0o600,
13
+ });
14
+ console.debug(`dotenc.json saved for projectId "${parsedConfig.projectId}".`);
15
+ };
16
+ export const getProjectConfig = async () => {
17
+ if (existsSync(configPath)) {
18
+ const config = JSON.parse(await fs.readFile(configPath, "utf-8"));
19
+ return projectConfigSchema.parse(config);
20
+ }
21
+ return {};
22
+ };
@@ -0,0 +1,40 @@
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";
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 `npx 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
+ console.debug(`Token for project "${projectId}" and environment "${environment}" added successfully.`);
40
+ };
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@dotenc/cli",
3
+ "version": "0.1.0",
4
+ "description": "🔐 Secure, encrypted environment variables that live in your codebase",
5
+ "type": "module",
6
+ "bin": {
7
+ "dotenc": "./dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "keywords": [
13
+ "environment",
14
+ "variables",
15
+ "safe",
16
+ "secure",
17
+ "codebase",
18
+ "cli",
19
+ "command",
20
+ "line",
21
+ "tool",
22
+ "utility",
23
+ "env",
24
+ "box",
25
+ "dotenv",
26
+ "encrypted",
27
+ "codebase"
28
+ ],
29
+ "author": "Ivan Filho <i@ivanfilho.com>",
30
+ "license": "MIT",
31
+ "devDependencies": {
32
+ "@biomejs/biome": "^1.9.4",
33
+ "@types/node": "^22.13.7",
34
+ "tsx": "^4.19.3",
35
+ "typescript": "^5.8.2"
36
+ },
37
+ "dependencies": {
38
+ "@paralleldrive/cuid2": "^2.2.2",
39
+ "commander": "^13.1.0",
40
+ "inquirer": "^12.4.2",
41
+ "zod": "^3.24.2"
42
+ },
43
+ "scripts": {
44
+ "dev": "tsx src/cli.ts",
45
+ "build": "tsc"
46
+ }
47
+ }