@etree/cli 2.0.1 → 2.0.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/dist/index.js +0 -0
- package/package.json +6 -3
- package/eslint.config.mjs +0 -4
- package/src/commands/auth.ts +0 -258
- package/src/commands/config.ts +0 -47
- package/src/commands/init.ts +0 -187
- package/src/commands/member.ts +0 -146
- package/src/commands/secret.ts +0 -381
- package/src/commands/shortcuts.ts +0 -25
- package/src/commands/wallet.ts +0 -97
- package/src/index.ts +0 -50
- package/src/lib/api.ts +0 -57
- package/src/lib/config.ts +0 -70
- package/src/lib/crypto.ts +0 -173
- package/src/lib/env-manager.ts +0 -60
- package/src/lib/key-store.ts +0 -51
- package/src/test-e2e.ts +0 -106
- package/tsconfig.json +0 -18
package/src/commands/secret.ts
DELETED
|
@@ -1,381 +0,0 @@
|
|
|
1
|
-
import { Command } from "commander";
|
|
2
|
-
import chalk from "chalk";
|
|
3
|
-
import ora from "ora";
|
|
4
|
-
import { api } from "../lib/api";
|
|
5
|
-
import { encrypt, decrypt, shutdown } from "../lib/crypto";
|
|
6
|
-
import { requirePrivateKey } from "../lib/key-store";
|
|
7
|
-
import { updateEnvFile, ensureGitIgnore } from "../lib/env-manager";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Helper: find a wallet ID by name.
|
|
11
|
-
*/
|
|
12
|
-
async function findWalletId(name: string): Promise<string | null> {
|
|
13
|
-
const result = await api.get("/wallets");
|
|
14
|
-
const owned = result.owned.find((w: any) => w.name === name);
|
|
15
|
-
if (owned) return owned.id;
|
|
16
|
-
const shared = result.shared.find((w: any) => w.name === name);
|
|
17
|
-
if (shared) return shared.id;
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Get all public keys of wallet members (owner + members).
|
|
23
|
-
*/
|
|
24
|
-
async function getWalletPublicKeys(walletId: string): Promise<string[]> {
|
|
25
|
-
const result = await api.get(`/wallets/${walletId}/members`);
|
|
26
|
-
const keys: string[] = [];
|
|
27
|
-
|
|
28
|
-
if (result.owner?.public_key) {
|
|
29
|
-
keys.push(result.owner.public_key);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (result.members) {
|
|
33
|
-
for (const m of result.members) {
|
|
34
|
-
if (m.user?.public_key) {
|
|
35
|
-
keys.push(m.user.public_key);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return keys;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Shared logic for pushing a secret (used by `secret set` and top-level `push`).
|
|
45
|
-
*/
|
|
46
|
-
export async function pushSecret(
|
|
47
|
-
walletName: string,
|
|
48
|
-
keyName: string,
|
|
49
|
-
value: string,
|
|
50
|
-
): Promise<void> {
|
|
51
|
-
const spinner = ora("Encrypting and saving secret...").start();
|
|
52
|
-
try {
|
|
53
|
-
const walletId = await findWalletId(walletName);
|
|
54
|
-
if (!walletId) {
|
|
55
|
-
spinner.fail(chalk.red(`Wallet "${walletName}" not found`));
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
spinner.text = "Fetching member public keys...";
|
|
60
|
-
const publicKeys = await getWalletPublicKeys(walletId);
|
|
61
|
-
|
|
62
|
-
if (publicKeys.length === 0) {
|
|
63
|
-
spinner.fail(chalk.red("No members with public keys found"));
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
spinner.text = `Encrypting for ${publicKeys.length} member(s)...`;
|
|
68
|
-
const secrets = [];
|
|
69
|
-
for (const pk of publicKeys) {
|
|
70
|
-
const ciphertext = await encrypt(value, pk);
|
|
71
|
-
secrets.push({
|
|
72
|
-
key_name: keyName,
|
|
73
|
-
encrypted_value: ciphertext,
|
|
74
|
-
encrypted_for: pk,
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
spinner.text = "Uploading encrypted secrets...";
|
|
79
|
-
await api.post(`/wallets/${walletId}/secrets`, { secrets });
|
|
80
|
-
|
|
81
|
-
spinner.succeed(
|
|
82
|
-
chalk.green(
|
|
83
|
-
`Secret "${keyName}" saved (encrypted for ${publicKeys.length} member(s))`,
|
|
84
|
-
),
|
|
85
|
-
);
|
|
86
|
-
} catch (err: any) {
|
|
87
|
-
spinner.fail(chalk.red(`Failed: ${err.message}`));
|
|
88
|
-
} finally {
|
|
89
|
-
shutdown();
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Shared logic for pulling secrets (used by `secret pull` and top-level `pull`).
|
|
95
|
-
*/
|
|
96
|
-
export async function pullSecrets(
|
|
97
|
-
walletName: string,
|
|
98
|
-
opts: { output?: string; sync?: boolean },
|
|
99
|
-
): Promise<void> {
|
|
100
|
-
const privateKey = requirePrivateKey();
|
|
101
|
-
const spinner = ora("Pulling secrets...").start();
|
|
102
|
-
try {
|
|
103
|
-
const walletId = await findWalletId(walletName);
|
|
104
|
-
if (!walletId) {
|
|
105
|
-
spinner.fail(chalk.red(`Wallet "${walletName}" not found`));
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const result = await api.get(`/wallets/${walletId}/secrets`);
|
|
110
|
-
|
|
111
|
-
if (result.secrets.length === 0) {
|
|
112
|
-
spinner.warn(chalk.yellow(`No secrets found in wallet "${walletName}"`));
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
spinner.text = `Decrypting ${result.secrets.length} secret(s)...`;
|
|
117
|
-
|
|
118
|
-
const lines: string[] = [];
|
|
119
|
-
lines.push(`# EnvTree — ${walletName}`);
|
|
120
|
-
lines.push(`# Generated at ${new Date().toISOString()}`);
|
|
121
|
-
lines.push("");
|
|
122
|
-
|
|
123
|
-
const decrypted: { key: string; value: string }[] = [];
|
|
124
|
-
for (const secret of result.secrets) {
|
|
125
|
-
try {
|
|
126
|
-
const plaintext = await decrypt(secret.encrypted_value, privateKey);
|
|
127
|
-
lines.push(`${secret.key_name}=${plaintext}`);
|
|
128
|
-
decrypted.push({ key: secret.key_name, value: plaintext });
|
|
129
|
-
} catch {
|
|
130
|
-
lines.push(`# FAILED TO DECRYPT: ${secret.key_name}`);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const outputStr = lines.join("\n") + "\n";
|
|
135
|
-
|
|
136
|
-
if (opts.sync) {
|
|
137
|
-
// Sync directly into the local .env
|
|
138
|
-
spinner.text = "Syncing secrets to .env...";
|
|
139
|
-
for (const { key, value } of decrypted) {
|
|
140
|
-
updateEnvFile(key, value);
|
|
141
|
-
}
|
|
142
|
-
ensureGitIgnore(".env");
|
|
143
|
-
spinner.succeed(
|
|
144
|
-
chalk.green(
|
|
145
|
-
`${decrypted.length} secret(s) synced to .env`,
|
|
146
|
-
),
|
|
147
|
-
);
|
|
148
|
-
} else if (opts.output) {
|
|
149
|
-
const fs = require("fs");
|
|
150
|
-
fs.writeFileSync(opts.output, outputStr);
|
|
151
|
-
spinner.succeed(
|
|
152
|
-
chalk.green(
|
|
153
|
-
`${result.secrets.length} secret(s) written to ${opts.output}`,
|
|
154
|
-
),
|
|
155
|
-
);
|
|
156
|
-
} else {
|
|
157
|
-
spinner.stop();
|
|
158
|
-
console.log(outputStr);
|
|
159
|
-
}
|
|
160
|
-
} catch (err: any) {
|
|
161
|
-
spinner.fail(chalk.red(`Failed: ${err.message}`));
|
|
162
|
-
} finally {
|
|
163
|
-
shutdown();
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export function registerSecretCommands(program: Command): void {
|
|
168
|
-
const secret = program
|
|
169
|
-
.command("secret")
|
|
170
|
-
.description("Manage encrypted secrets");
|
|
171
|
-
|
|
172
|
-
// ── secret set ──
|
|
173
|
-
secret
|
|
174
|
-
.command("set <wallet> <key> <value>")
|
|
175
|
-
.description("Encrypt & store a secret for all wallet members")
|
|
176
|
-
.action(pushSecret);
|
|
177
|
-
|
|
178
|
-
// ── secret update ──
|
|
179
|
-
secret
|
|
180
|
-
.command("update <wallet> <key> <value>")
|
|
181
|
-
.description("Update an existing secret's value (re-encrypts for all members)")
|
|
182
|
-
.action(pushSecret);
|
|
183
|
-
|
|
184
|
-
// ── secret get ──
|
|
185
|
-
secret
|
|
186
|
-
.command("get <wallet> <key>")
|
|
187
|
-
.description("Decrypt and display a specific secret")
|
|
188
|
-
.option("-s, --save", "Save the value to your local .env file")
|
|
189
|
-
.action(
|
|
190
|
-
async (
|
|
191
|
-
walletName: string,
|
|
192
|
-
keyName: string,
|
|
193
|
-
opts: { save?: boolean },
|
|
194
|
-
) => {
|
|
195
|
-
const privateKey = requirePrivateKey();
|
|
196
|
-
const spinner = ora("Fetching and decrypting...").start();
|
|
197
|
-
try {
|
|
198
|
-
const walletId = await findWalletId(walletName);
|
|
199
|
-
if (!walletId) {
|
|
200
|
-
spinner.fail(chalk.red(`Wallet "${walletName}" not found`));
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const result = await api.get(`/wallets/${walletId}/secrets`);
|
|
205
|
-
const secret = result.secrets.find(
|
|
206
|
-
(s: any) => s.key_name === keyName,
|
|
207
|
-
);
|
|
208
|
-
|
|
209
|
-
if (!secret) {
|
|
210
|
-
spinner.fail(
|
|
211
|
-
chalk.red(
|
|
212
|
-
`Secret "${keyName}" not found in wallet "${walletName}"`,
|
|
213
|
-
),
|
|
214
|
-
);
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
spinner.text = "Decrypting...";
|
|
219
|
-
const plaintext = await decrypt(
|
|
220
|
-
secret.encrypted_value,
|
|
221
|
-
privateKey,
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
if (opts.save) {
|
|
225
|
-
spinner.text = "Saving to .env...";
|
|
226
|
-
updateEnvFile(keyName, plaintext);
|
|
227
|
-
ensureGitIgnore(".env");
|
|
228
|
-
spinner.succeed(
|
|
229
|
-
chalk.green(`Secret "${keyName}" saved to .env`),
|
|
230
|
-
);
|
|
231
|
-
} else {
|
|
232
|
-
spinner.stop();
|
|
233
|
-
console.log(`${chalk.cyan(keyName)}=${plaintext}`);
|
|
234
|
-
}
|
|
235
|
-
} catch (err: any) {
|
|
236
|
-
spinner.fail(chalk.red(`Failed: ${err.message}`));
|
|
237
|
-
} finally {
|
|
238
|
-
shutdown();
|
|
239
|
-
}
|
|
240
|
-
},
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
// ── secret list ──
|
|
244
|
-
secret
|
|
245
|
-
.command("list <wallet>")
|
|
246
|
-
.alias("ls")
|
|
247
|
-
.description("List all secret keys in a wallet")
|
|
248
|
-
.action(async (walletName: string) => {
|
|
249
|
-
const spinner = ora("Fetching secrets...").start();
|
|
250
|
-
try {
|
|
251
|
-
const walletId = await findWalletId(walletName);
|
|
252
|
-
if (!walletId) {
|
|
253
|
-
spinner.fail(chalk.red(`Wallet "${walletName}" not found`));
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const result = await api.get(`/wallets/${walletId}/secrets/all`);
|
|
258
|
-
spinner.stop();
|
|
259
|
-
|
|
260
|
-
if (result.keys.length === 0) {
|
|
261
|
-
console.log(chalk.yellow(`No secrets in wallet "${walletName}"`));
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
console.log(chalk.bold(`\nSecrets in "${walletName}"`));
|
|
266
|
-
for (const key of result.keys) {
|
|
267
|
-
console.log(` ${chalk.cyan(key)}`);
|
|
268
|
-
}
|
|
269
|
-
console.log(chalk.dim(`\n ${result.keys.length} secret(s) total\n`));
|
|
270
|
-
} catch (err: any) {
|
|
271
|
-
spinner.fail(chalk.red(`Failed: ${err.message}`));
|
|
272
|
-
}
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
// ── secret pull ──
|
|
276
|
-
secret
|
|
277
|
-
.command("pull <wallet>")
|
|
278
|
-
.description("Pull and decrypt all secrets")
|
|
279
|
-
.option("-o, --output <file>", "Write to a file instead of stdout")
|
|
280
|
-
.option("--sync", "Sync all secrets directly to your local .env")
|
|
281
|
-
.action(
|
|
282
|
-
async (
|
|
283
|
-
walletName: string,
|
|
284
|
-
opts: { output?: string; sync?: boolean },
|
|
285
|
-
) => {
|
|
286
|
-
await pullSecrets(walletName, opts);
|
|
287
|
-
},
|
|
288
|
-
);
|
|
289
|
-
|
|
290
|
-
// ── secret delete ──
|
|
291
|
-
secret
|
|
292
|
-
.command("delete <wallet> <key>")
|
|
293
|
-
.alias("rm")
|
|
294
|
-
.description("Delete a secret from a wallet")
|
|
295
|
-
.action(async (walletName: string, keyName: string) => {
|
|
296
|
-
const spinner = ora("Deleting secret...").start();
|
|
297
|
-
try {
|
|
298
|
-
const walletId = await findWalletId(walletName);
|
|
299
|
-
if (!walletId) {
|
|
300
|
-
spinner.fail(chalk.red(`Wallet "${walletName}" not found`));
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const result = await api.get(`/wallets/${walletId}/secrets`);
|
|
305
|
-
const secret = result.secrets.find(
|
|
306
|
-
(s: any) => s.key_name === keyName,
|
|
307
|
-
);
|
|
308
|
-
|
|
309
|
-
if (!secret) {
|
|
310
|
-
spinner.fail(chalk.red(`Secret "${keyName}" not found`));
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
await api.delete(`/wallets/${walletId}/secrets/${secret.id}`);
|
|
315
|
-
spinner.succeed(
|
|
316
|
-
chalk.green(`Secret "${keyName}" deleted from "${walletName}"`),
|
|
317
|
-
);
|
|
318
|
-
} catch (err: any) {
|
|
319
|
-
spinner.fail(chalk.red(`Failed: ${err.message}`));
|
|
320
|
-
}
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
// ── secret share ──
|
|
324
|
-
secret
|
|
325
|
-
.command("share <wallet>")
|
|
326
|
-
.description("Re-encrypt all secrets for all current wallet members")
|
|
327
|
-
.action(async (walletName: string) => {
|
|
328
|
-
const privateKey = requirePrivateKey();
|
|
329
|
-
const spinner = ora(
|
|
330
|
-
"Re-encrypting secrets for all members...",
|
|
331
|
-
).start();
|
|
332
|
-
try {
|
|
333
|
-
const walletId = await findWalletId(walletName);
|
|
334
|
-
if (!walletId) {
|
|
335
|
-
spinner.fail(chalk.red(`Wallet "${walletName}" not found`));
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const mySecrets = await api.get(`/wallets/${walletId}/secrets`);
|
|
340
|
-
if (mySecrets.secrets.length === 0) {
|
|
341
|
-
spinner.warn(chalk.yellow("No secrets to share"));
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const publicKeys = await getWalletPublicKeys(walletId);
|
|
346
|
-
|
|
347
|
-
spinner.text = `Re-encrypting ${mySecrets.secrets.length} secret(s) for ${publicKeys.length} member(s)...`;
|
|
348
|
-
|
|
349
|
-
const allSecrets = [];
|
|
350
|
-
for (const secret of mySecrets.secrets) {
|
|
351
|
-
const plaintext = await decrypt(
|
|
352
|
-
secret.encrypted_value,
|
|
353
|
-
privateKey,
|
|
354
|
-
);
|
|
355
|
-
|
|
356
|
-
for (const pk of publicKeys) {
|
|
357
|
-
const ciphertext = await encrypt(plaintext, pk);
|
|
358
|
-
allSecrets.push({
|
|
359
|
-
key_name: secret.key_name,
|
|
360
|
-
encrypted_value: ciphertext,
|
|
361
|
-
encrypted_for: pk,
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
await api.post(`/wallets/${walletId}/secrets`, {
|
|
367
|
-
secrets: allSecrets,
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
spinner.succeed(
|
|
371
|
-
chalk.green(
|
|
372
|
-
`${mySecrets.secrets.length} secret(s) re-encrypted for ${publicKeys.length} member(s)`,
|
|
373
|
-
),
|
|
374
|
-
);
|
|
375
|
-
} catch (err: any) {
|
|
376
|
-
spinner.fail(chalk.red(`Failed: ${err.message}`));
|
|
377
|
-
} finally {
|
|
378
|
-
shutdown();
|
|
379
|
-
}
|
|
380
|
-
});
|
|
381
|
-
}
|
|
@@ -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
|
-
}
|
package/src/commands/wallet.ts
DELETED
|
@@ -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
|
-
}
|