@etree/cli 2.0.1 → 2.0.3

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.
@@ -1,25 +0,0 @@
1
- import { Command } from "commander";
2
- import { pushSecret, pullSecrets } from "./secret";
3
-
4
- /**
5
- * Register top-level shortcut commands:
6
- * et push <wallet> <key> <value> → secret set
7
- * et pull <wallet> → secret pull --sync
8
- */
9
- export function registerShortcuts(program: Command): void {
10
- // ── et push ──
11
- program
12
- .command("push <wallet> <key> <value>")
13
- .description("Shortcut: Encrypt & store a secret (same as: secret set)")
14
- .action(pushSecret);
15
-
16
- // ── et pull ──
17
- program
18
- .command("pull <wallet>")
19
- .description("Shortcut: Sync all secrets to local .env (same as: secret pull --sync)")
20
- .option("-o, --output <file>", "Write to a file instead of syncing to .env")
21
- .action(async (walletName: string, opts: { output?: string }) => {
22
- // Default behavior: --sync (write to .env)
23
- await pullSecrets(walletName, { sync: !opts.output, output: opts.output });
24
- });
25
- }
@@ -1,97 +0,0 @@
1
- import { Command } from "commander";
2
- import chalk from "chalk";
3
- import ora from "ora";
4
- import { api } from "../lib/api";
5
-
6
- export function registerWalletCommands(program: Command): void {
7
- const wallet = program.command("wallet").description("Manage key wallets");
8
-
9
- // ── wallet create ──
10
- wallet
11
- .command("create <name>")
12
- .description("Create a new key wallet")
13
- .option("-d, --description <desc>", "Wallet description")
14
- .action(async (name: string, opts: { description?: string }) => {
15
- const spinner = ora("Creating wallet...").start();
16
- try {
17
- const result = await api.post("/wallets", {
18
- name,
19
- description: opts.description,
20
- });
21
- spinner.succeed(chalk.green(`Wallet "${name}" created`));
22
- console.log(chalk.dim(` ID: ${result.wallet.id}`));
23
- } catch (err: any) {
24
- spinner.fail(chalk.red(`Failed: ${err.message}`));
25
- }
26
- });
27
-
28
- // ── wallet list ──
29
- wallet
30
- .command("list")
31
- .alias("ls")
32
- .description("List all your wallets")
33
- .action(async () => {
34
- const spinner = ora("Fetching wallets...").start();
35
- try {
36
- const result = await api.get("/wallets");
37
- spinner.stop();
38
-
39
- if (result.owned.length === 0 && result.shared.length === 0) {
40
- console.log(
41
- chalk.yellow(
42
- "No wallets found. Create one with: envtree wallet create <name>",
43
- ),
44
- );
45
- return;
46
- }
47
-
48
- if (result.owned.length > 0) {
49
- console.log(chalk.bold("\nOwned Wallets"));
50
- for (const w of result.owned) {
51
- console.log(
52
- ` ${chalk.cyan(w.name)} ${chalk.dim(`(${w.id.substring(0, 8)}...)`)}`,
53
- );
54
- if (w.description) console.log(` ${chalk.dim(w.description)}`);
55
- }
56
- }
57
-
58
- if (result.shared.length > 0) {
59
- console.log(chalk.bold("\nShared Wallets"));
60
- for (const w of result.shared) {
61
- console.log(
62
- ` ${chalk.cyan(w.name)} ${chalk.dim(`[${w.role}]`)} ${chalk.dim(`(${w.id?.substring(0, 8)}...)`)}`,
63
- );
64
- }
65
- }
66
- console.log("");
67
- } catch (err: any) {
68
- spinner.fail(chalk.red(`Failed: ${err.message}`));
69
- }
70
- });
71
-
72
- // ── wallet delete ──
73
- wallet
74
- .command("delete <name>")
75
- .description("Delete a wallet (owner only)")
76
- .action(async (name: string) => {
77
- const spinner = ora("Finding wallet...").start();
78
- try {
79
- // Find wallet by name
80
- const listResult = await api.get("/wallets");
81
- const target = listResult.owned.find((w: any) => w.name === name);
82
-
83
- if (!target) {
84
- spinner.fail(
85
- chalk.red(`Wallet "${name}" not found or you are not the owner`),
86
- );
87
- return;
88
- }
89
-
90
- spinner.text = "Deleting wallet...";
91
- await api.delete(`/wallets/${target.id}`);
92
- spinner.succeed(chalk.green(`Wallet "${name}" deleted`));
93
- } catch (err: any) {
94
- spinner.fail(chalk.red(`Failed: ${err.message}`));
95
- }
96
- });
97
- }
package/src/index.ts DELETED
@@ -1,50 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { Command } from "commander";
4
- import { registerAuthCommands } from "./commands/auth";
5
- import { registerWalletCommands } from "./commands/wallet";
6
- import { registerMemberCommands } from "./commands/member";
7
- import { registerSecretCommands } from "./commands/secret";
8
- import { registerInitCommand } from "./commands/init";
9
- import { registerConfigCommands } from "./commands/config";
10
- import { registerShortcuts } from "./commands/shortcuts";
11
-
12
- const program = new Command();
13
-
14
- program
15
- .name("et")
16
- .description("EnvTree — Securely manage and share environment variables")
17
- .version("1.0.0")
18
- .addHelpText(
19
- "after",
20
- `
21
- Quick Start:
22
- $ et init First-time setup (signup/login)
23
- $ et push <wallet> <key> <value> Encrypt & store a secret
24
- $ et pull <wallet> Sync all secrets to local .env
25
- $ et whoami Check current user
26
-
27
- Examples:
28
- $ et init
29
- $ et push production DB_URL "postgres://..."
30
- $ et pull production
31
- $ et pull production -o .env.production
32
- $ et secret list production
33
- $ et member add production alice --role write
34
- `,
35
- );
36
-
37
- // Core commands
38
- registerAuthCommands(program);
39
- registerInitCommand(program);
40
- registerConfigCommands(program);
41
-
42
- // Resource commands
43
- registerWalletCommands(program);
44
- registerMemberCommands(program);
45
- registerSecretCommands(program);
46
-
47
- // Top-level shortcuts
48
- registerShortcuts(program);
49
-
50
- program.parse(process.argv);
package/src/lib/api.ts DELETED
@@ -1,57 +0,0 @@
1
- import { getConfig, requireSession } from "./config";
2
-
3
- interface ApiResponse {
4
- [key: string]: any;
5
- }
6
-
7
- /**
8
- * Make an authenticated API request to the EnvTree server.
9
- */
10
- export async function apiRequest(
11
- method: string,
12
- path: string,
13
- body?: Record<string, any>,
14
- requireAuth = true,
15
- ): Promise<ApiResponse> {
16
- const config = getConfig();
17
- const url = `${config.server_url}${path}`;
18
-
19
- const headers: Record<string, string> = {
20
- "Content-Type": "application/json",
21
- };
22
-
23
- if (requireAuth) {
24
- const session = requireSession();
25
- headers["Authorization"] = `Bearer ${session.access_token}`;
26
- }
27
-
28
- const options: RequestInit = {
29
- method,
30
- headers,
31
- };
32
-
33
- if (body && method !== "GET") {
34
- options.body = JSON.stringify(body);
35
- }
36
-
37
- const response = await fetch(url, options);
38
- const data = (await response.json()) as ApiResponse;
39
-
40
- if (!response.ok) {
41
- throw new Error(
42
- data.error || `Request failed with status ${response.status}`,
43
- );
44
- }
45
-
46
- return data;
47
- }
48
-
49
- /** Shorthand methods */
50
- export const api = {
51
- get: (path: string) => apiRequest("GET", path),
52
- post: (path: string, body: Record<string, any>, auth = true) =>
53
- apiRequest("POST", path, body, auth),
54
- put: (path: string, body: Record<string, any>) =>
55
- apiRequest("PUT", path, body),
56
- delete: (path: string) => apiRequest("DELETE", path),
57
- };
package/src/lib/config.ts DELETED
@@ -1,70 +0,0 @@
1
- import * as fs from "fs";
2
- import * as path from "path";
3
- import * as os from "os";
4
-
5
- const CONFIG_DIR = path.join(os.homedir(), ".envtree");
6
- const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
7
-
8
- export interface Session {
9
- access_token: string;
10
- refresh_token: string;
11
- user_id: string;
12
- email: string;
13
- username: string;
14
- }
15
-
16
- export interface Config {
17
- server_url: string;
18
- session?: Session;
19
- }
20
-
21
- function ensureConfigDir(): void {
22
- if (!fs.existsSync(CONFIG_DIR)) {
23
- fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
24
- }
25
- }
26
-
27
- export function getConfig(): Config {
28
- ensureConfigDir();
29
- if (!fs.existsSync(CONFIG_FILE)) {
30
- const defaults: Config = { server_url: "http://localhost:3001" };
31
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(defaults, null, 2), {
32
- mode: 0o600,
33
- });
34
- return defaults;
35
- }
36
- return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
37
- }
38
-
39
- export function saveConfig(config: Config): void {
40
- ensureConfigDir();
41
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), {
42
- mode: 0o600,
43
- });
44
- }
45
-
46
- export function getSession(): Session | null {
47
- const config = getConfig();
48
- return config.session || null;
49
- }
50
-
51
- export function saveSession(session: Session): void {
52
- const config = getConfig();
53
- config.session = session;
54
- saveConfig(config);
55
- }
56
-
57
- export function clearSession(): void {
58
- const config = getConfig();
59
- delete config.session;
60
- saveConfig(config);
61
- }
62
-
63
- export function requireSession(): Session {
64
- const session = getSession();
65
- if (!session) {
66
- console.error("Not logged in. Run: envtree login");
67
- process.exit(1);
68
- }
69
- return session;
70
- }
package/src/lib/crypto.ts DELETED
@@ -1,173 +0,0 @@
1
- import { spawn, ChildProcessWithoutNullStreams } from "child_process";
2
- import * as path from "path";
3
-
4
- const CRYPTO_ENGINE_PATH = path.resolve(
5
- __dirname,
6
- "../../../../packages/crypto/engine.py",
7
- );
8
-
9
- let engineProcess: ChildProcessWithoutNullStreams | null = null;
10
-
11
- interface CryptoRequest {
12
- action: string;
13
- data?: Record<string, any>;
14
- }
15
-
16
- interface CryptoResponse {
17
- status: string;
18
- [key: string]: any;
19
- }
20
-
21
- /**
22
- * Get or spawn the crypto engine Python process.
23
- */
24
- function getEngine(): ChildProcessWithoutNullStreams {
25
- if (engineProcess && !engineProcess.killed) {
26
- return engineProcess;
27
- }
28
-
29
- // Try venv python first, fall back to system python
30
- const venvDir = path.resolve(__dirname, "../../../../packages/crypto/.venv");
31
- const isWindows = process.platform === "win32";
32
-
33
- // Potential Venv Paths
34
- const potentialVenvPaths = isWindows
35
- ? [
36
- path.join(venvDir, "Scripts", "python.exe"),
37
- path.join(venvDir, "bin", "python.exe"),
38
- ]
39
- : [path.join(venvDir, "bin", "python")];
40
-
41
- let pythonCmd = isWindows ? "python" : "python3";
42
-
43
- for (const venvPath of potentialVenvPaths) {
44
- if (require("fs").existsSync(venvPath)) {
45
- pythonCmd = venvPath;
46
- break;
47
- }
48
- }
49
-
50
- // If we haven't found a venv python, verify the system command exists
51
- if (pythonCmd === "python" || pythonCmd === "python3") {
52
- try {
53
- // Use system python if it exists and works
54
- require("child_process").execSync(`${pythonCmd} --version`, {
55
- stdio: "ignore",
56
- });
57
- } catch {
58
- // If the preferred one fails, try the fallback
59
- pythonCmd = pythonCmd === "python3" ? "python" : "python3";
60
- }
61
- }
62
-
63
- engineProcess = spawn(pythonCmd, [CRYPTO_ENGINE_PATH], {
64
- stdio: ["pipe", "pipe", "pipe"],
65
- cwd: path.resolve(__dirname, "../../../../packages/crypto"),
66
- });
67
-
68
- engineProcess.on("exit", () => {
69
- engineProcess = null;
70
- });
71
-
72
- return engineProcess;
73
- }
74
-
75
- /**
76
- * Send a command to the crypto engine and get the response.
77
- */
78
- function sendCommand(request: CryptoRequest): Promise<CryptoResponse> {
79
- return new Promise((resolve, reject) => {
80
- const engine = getEngine();
81
-
82
- const onData = (data: Buffer) => {
83
- try {
84
- const response = JSON.parse(data.toString().trim());
85
- engine.stdout.removeListener("data", onData);
86
- engine.stderr.removeListener("data", onError);
87
-
88
- if (response.status === "error") {
89
- reject(new Error(response.message));
90
- } else {
91
- resolve(response);
92
- }
93
- } catch (e) {
94
- // partial data, wait for more
95
- }
96
- };
97
-
98
- const onError = (data: Buffer) => {
99
- engine.stdout.removeListener("data", onData);
100
- engine.stderr.removeListener("data", onError);
101
- reject(new Error(`Crypto engine error: ${data.toString()}`));
102
- };
103
-
104
- engine.stdout.on("data", onData);
105
- engine.stderr.on("data", onError);
106
-
107
- engine.stdin.write(JSON.stringify(request) + "\n");
108
- });
109
- }
110
-
111
- /**
112
- * Generate a new X25519 key pair.
113
- */
114
- export async function generateKeys(): Promise<{
115
- private_key: string;
116
- public_key: string;
117
- }> {
118
- const result = await sendCommand({ action: "generate_keys" });
119
- return {
120
- private_key: result.private_key,
121
- public_key: result.public_key,
122
- };
123
- }
124
-
125
- /**
126
- * Derive the public key from an existing private key.
127
- */
128
- export async function derivePublicKey(privateKey: string): Promise<string> {
129
- const result = await sendCommand({
130
- action: "derive_public_key",
131
- data: { private_key: privateKey },
132
- });
133
- return result.public_key;
134
- }
135
-
136
- /**
137
- * Encrypt a plaintext value using the recipient's public key (SealedBox).
138
- */
139
- export async function encrypt(
140
- plaintext: string,
141
- publicKey: string,
142
- ): Promise<string> {
143
- const result = await sendCommand({
144
- action: "encrypt",
145
- data: { plaintext, public_key: publicKey },
146
- });
147
- return result.ciphertext;
148
- }
149
-
150
- /**
151
- * Decrypt a ciphertext using the recipient's private key (SealedBox).
152
- */
153
- export async function decrypt(
154
- ciphertext: string,
155
- privateKey: string,
156
- ): Promise<string> {
157
- const result = await sendCommand({
158
- action: "decrypt",
159
- data: { ciphertext, private_key: privateKey },
160
- });
161
- return result.plaintext;
162
- }
163
-
164
- /**
165
- * Shut down the crypto engine process.
166
- */
167
- export function shutdown(): void {
168
- if (engineProcess && !engineProcess.killed) {
169
- engineProcess.stdin.end();
170
- engineProcess.kill();
171
- engineProcess = null;
172
- }
173
- }
@@ -1,60 +0,0 @@
1
- import * as fs from "fs";
2
- import * as path from "path";
3
-
4
- /**
5
- * Updates a key=value pair in a .env file.
6
- * Creates the file if it doesn't exist.
7
- * If the key exists, it updates its value.
8
- * If the key doesn't exist, it appends to the end.
9
- */
10
- export function updateEnvFile(key: string, value: string): void {
11
- const envPath = path.resolve(process.cwd(), ".env");
12
- let content = "";
13
-
14
- if (fs.existsSync(envPath)) {
15
- content = fs.readFileSync(envPath, "utf-8");
16
- }
17
-
18
- const lines = content.split("\n");
19
- const keyPattern = new RegExp(`^${key}=.*`);
20
- let found = false;
21
-
22
- const newLines = lines.map((line) => {
23
- if (keyPattern.test(line.trim())) {
24
- found = true;
25
- return `${key}=${value}`;
26
- }
27
- return line;
28
- });
29
-
30
- if (!found) {
31
- if (newLines.length > 0 && newLines[newLines.length - 1].trim() !== "") {
32
- newLines.push("");
33
- }
34
- newLines.push(`${key}=${value}`);
35
- }
36
-
37
- fs.writeFileSync(envPath, newLines.join("\n"), "utf-8");
38
- }
39
-
40
- /**
41
- * Ensures that a .gitignore file exists and contains the specified entry.
42
- */
43
- export function ensureGitIgnore(entry: string = ".env"): void {
44
- const gitIgnorePath = path.resolve(process.cwd(), ".gitignore");
45
- let content = "";
46
-
47
- if (fs.existsSync(gitIgnorePath)) {
48
- content = fs.readFileSync(gitIgnorePath, "utf-8");
49
- }
50
-
51
- const lines = content.split("\n").map((l) => l.trim());
52
-
53
- if (!lines.includes(entry)) {
54
- if (content.length > 0 && !content.endsWith("\n")) {
55
- content += "\n";
56
- }
57
- content += `${entry}\n`;
58
- fs.writeFileSync(gitIgnorePath, content, "utf-8");
59
- }
60
- }
@@ -1,51 +0,0 @@
1
- import * as fs from "fs";
2
- import * as path from "path";
3
- import * as os from "os";
4
-
5
- const KEY_DIR = path.join(os.homedir(), ".envtree");
6
- const KEY_FILE = path.join(KEY_DIR, "private_key");
7
-
8
- function ensureKeyDir(): void {
9
- if (!fs.existsSync(KEY_DIR)) {
10
- fs.mkdirSync(KEY_DIR, { recursive: true, mode: 0o700 });
11
- }
12
- }
13
-
14
- /**
15
- * Save the private key to ~/.envtree/private_key
16
- */
17
- export function savePrivateKey(privateKey: string): void {
18
- ensureKeyDir();
19
- fs.writeFileSync(KEY_FILE, privateKey, { mode: 0o600 });
20
- }
21
-
22
- /**
23
- * Read the private key from ~/.envtree/private_key
24
- */
25
- export function getPrivateKey(): string | null {
26
- if (!fs.existsSync(KEY_FILE)) {
27
- return null;
28
- }
29
- return fs.readFileSync(KEY_FILE, "utf-8").trim();
30
- }
31
-
32
- /**
33
- * Get private key or exit with error.
34
- */
35
- export function requirePrivateKey(): string {
36
- const key = getPrivateKey();
37
- if (!key) {
38
- console.error(
39
- "Private key not found. Run: envtree signup (or envtree login)",
40
- );
41
- process.exit(1);
42
- }
43
- return key;
44
- }
45
-
46
- /**
47
- * Check if a private key exists locally.
48
- */
49
- export function hasPrivateKey(): boolean {
50
- return fs.existsSync(KEY_FILE);
51
- }
package/src/test-e2e.ts DELETED
@@ -1,106 +0,0 @@
1
- import { apiRequest } from "./lib/api";
2
- import { saveSession } from "./lib/config";
3
- import { generateKeys, encrypt, decrypt, shutdown } from "./lib/crypto";
4
- import { savePrivateKey } from "./lib/key-store";
5
- import * as fs from "fs";
6
- import * as path from "os";
7
-
8
- async function testE2E() {
9
- const email = `test-${Date.now()}@example.com`;
10
- const username = `user-${Date.now()}`;
11
- const password = "password123";
12
-
13
- console.log("--- Phase 1: Signup ---");
14
- try {
15
- const signupResult = await apiRequest(
16
- "POST",
17
- "/auth/signup",
18
- {
19
- email,
20
- password,
21
- username,
22
- },
23
- false,
24
- );
25
-
26
- console.log("Signup response:", JSON.stringify(signupResult, null, 2));
27
-
28
- if (signupResult.session) {
29
- saveSession({
30
- access_token: signupResult.session.access_token,
31
- refresh_token: signupResult.session.refresh_token,
32
- user_id: signupResult.user.id,
33
- email,
34
- username,
35
- });
36
- console.log(" Signup successful");
37
- } else {
38
- console.log(
39
- " User created but NO session returned (possibly email verification required)",
40
- );
41
- }
42
-
43
- if (!signupResult.session) {
44
- console.warn(
45
- "⚠️ Ending test early: No session available to continue authenticated requests.",
46
- );
47
- return;
48
- }
49
-
50
- console.log("--- Phase 2: Key Generation ---");
51
- const { private_key, public_key } = await generateKeys();
52
- savePrivateKey(private_key);
53
- await apiRequest("PUT", "/profile/public-key", { public_key });
54
- console.log(" Keypair generated and public key uploaded");
55
-
56
- console.log("--- Phase 3: Wallet Creation ---");
57
- const walletName = `test-wallet-${Date.now()}`;
58
- const walletResult = await apiRequest("POST", "/wallets", {
59
- name: walletName,
60
- });
61
- const walletId = walletResult.wallet.id;
62
- console.log(` Wallet "${walletName}" created with ID: ${walletId}`);
63
-
64
- console.log("--- Phase 4: Secret Encryption & Storage ---");
65
- const secretKey = "MY_API_KEY";
66
- const secretValue = "super-secret-value-123";
67
- const encryptedValue = await encrypt(secretValue, public_key);
68
-
69
- await apiRequest("POST", `/wallets/${walletId}/secrets`, {
70
- secrets: [
71
- {
72
- key_name: secretKey,
73
- encrypted_value: encryptedValue,
74
- encrypted_for: public_key,
75
- },
76
- ],
77
- });
78
- console.log(" Secret encrypted and uploaded");
79
-
80
- console.log("--- Phase 5: Secret Retrieval & Decryption ---");
81
- const getResult = await apiRequest("GET", `/wallets/${walletId}/secrets`);
82
- const foundSecret = getResult.secrets.find(
83
- (s: any) => s.key_name === secretKey,
84
- );
85
-
86
- if (!foundSecret) throw new Error("Secret not found in response");
87
-
88
- const decryptedValue = await decrypt(
89
- foundSecret.encrypted_value,
90
- private_key,
91
- );
92
- console.log(` Secret retrieved and decrypted: ${decryptedValue}`);
93
-
94
- if (decryptedValue === secretValue) {
95
- console.log(" E2E TRANSACTION SUCCESSFUL");
96
- } else {
97
- throw new Error("Decrypted value mismatch");
98
- }
99
- } catch (error) {
100
- console.error(" E2E TEST FAILED:", error);
101
- } finally {
102
- shutdown();
103
- }
104
- }
105
-
106
- testE2E();