@etree/cli 2.0.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/README.md +199 -0
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/auth.js +220 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +42 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +159 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/member.d.ts +2 -0
- package/dist/commands/member.js +119 -0
- package/dist/commands/member.js.map +1 -0
- package/dist/commands/secret.d.ts +13 -0
- package/dist/commands/secret.js +307 -0
- package/dist/commands/secret.js.map +1 -0
- package/dist/commands/shortcuts.d.ts +7 -0
- package/dist/commands/shortcuts.js +26 -0
- package/dist/commands/shortcuts.js.map +1 -0
- package/dist/commands/wallet.d.ts +2 -0
- package/dist/commands/wallet.js +88 -0
- package/dist/commands/wallet.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/api.d.ts +15 -0
- package/dist/lib/api.js +40 -0
- package/dist/lib/api.js.map +1 -0
- package/dist/lib/config.d.ts +17 -0
- package/dist/lib/config.js +91 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/crypto.d.ts +23 -0
- package/dist/lib/crypto.js +173 -0
- package/dist/lib/crypto.js.map +1 -0
- package/dist/lib/env-manager.d.ts +11 -0
- package/dist/lib/env-manager.js +88 -0
- package/dist/lib/env-manager.js.map +1 -0
- package/dist/lib/key-store.d.ts +16 -0
- package/dist/lib/key-store.js +83 -0
- package/dist/lib/key-store.js.map +1 -0
- package/dist/test-e2e.d.ts +1 -0
- package/dist/test-e2e.js +84 -0
- package/dist/test-e2e.js.map +1 -0
- package/eslint.config.mjs +4 -0
- package/package.json +30 -0
- package/src/commands/auth.ts +258 -0
- package/src/commands/config.ts +47 -0
- package/src/commands/init.ts +187 -0
- package/src/commands/member.ts +146 -0
- package/src/commands/secret.ts +381 -0
- package/src/commands/shortcuts.ts +25 -0
- package/src/commands/wallet.ts +97 -0
- package/src/index.ts +50 -0
- package/src/lib/api.ts +57 -0
- package/src/lib/config.ts +70 -0
- package/src/lib/crypto.ts +173 -0
- package/src/lib/env-manager.ts +60 -0
- package/src/lib/key-store.ts +51 -0
- package/src/test-e2e.ts +106 -0
- package/tsconfig.json +18 -0
package/dist/test-e2e.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const api_1 = require("./lib/api");
|
|
4
|
+
const config_1 = require("./lib/config");
|
|
5
|
+
const crypto_1 = require("./lib/crypto");
|
|
6
|
+
const key_store_1 = require("./lib/key-store");
|
|
7
|
+
async function testE2E() {
|
|
8
|
+
const email = `test-${Date.now()}@example.com`;
|
|
9
|
+
const username = `user-${Date.now()}`;
|
|
10
|
+
const password = "password123";
|
|
11
|
+
console.log("--- Phase 1: Signup ---");
|
|
12
|
+
try {
|
|
13
|
+
const signupResult = await (0, api_1.apiRequest)("POST", "/auth/signup", {
|
|
14
|
+
email,
|
|
15
|
+
password,
|
|
16
|
+
username,
|
|
17
|
+
}, false);
|
|
18
|
+
console.log("Signup response:", JSON.stringify(signupResult, null, 2));
|
|
19
|
+
if (signupResult.session) {
|
|
20
|
+
(0, config_1.saveSession)({
|
|
21
|
+
access_token: signupResult.session.access_token,
|
|
22
|
+
refresh_token: signupResult.session.refresh_token,
|
|
23
|
+
user_id: signupResult.user.id,
|
|
24
|
+
email,
|
|
25
|
+
username,
|
|
26
|
+
});
|
|
27
|
+
console.log(" Signup successful");
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
console.log(" User created but NO session returned (possibly email verification required)");
|
|
31
|
+
}
|
|
32
|
+
if (!signupResult.session) {
|
|
33
|
+
console.warn("⚠️ Ending test early: No session available to continue authenticated requests.");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
console.log("--- Phase 2: Key Generation ---");
|
|
37
|
+
const { private_key, public_key } = await (0, crypto_1.generateKeys)();
|
|
38
|
+
(0, key_store_1.savePrivateKey)(private_key);
|
|
39
|
+
await (0, api_1.apiRequest)("PUT", "/profile/public-key", { public_key });
|
|
40
|
+
console.log(" Keypair generated and public key uploaded");
|
|
41
|
+
console.log("--- Phase 3: Wallet Creation ---");
|
|
42
|
+
const walletName = `test-wallet-${Date.now()}`;
|
|
43
|
+
const walletResult = await (0, api_1.apiRequest)("POST", "/wallets", {
|
|
44
|
+
name: walletName,
|
|
45
|
+
});
|
|
46
|
+
const walletId = walletResult.wallet.id;
|
|
47
|
+
console.log(` Wallet "${walletName}" created with ID: ${walletId}`);
|
|
48
|
+
console.log("--- Phase 4: Secret Encryption & Storage ---");
|
|
49
|
+
const secretKey = "MY_API_KEY";
|
|
50
|
+
const secretValue = "super-secret-value-123";
|
|
51
|
+
const encryptedValue = await (0, crypto_1.encrypt)(secretValue, public_key);
|
|
52
|
+
await (0, api_1.apiRequest)("POST", `/wallets/${walletId}/secrets`, {
|
|
53
|
+
secrets: [
|
|
54
|
+
{
|
|
55
|
+
key_name: secretKey,
|
|
56
|
+
encrypted_value: encryptedValue,
|
|
57
|
+
encrypted_for: public_key,
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
});
|
|
61
|
+
console.log(" Secret encrypted and uploaded");
|
|
62
|
+
console.log("--- Phase 5: Secret Retrieval & Decryption ---");
|
|
63
|
+
const getResult = await (0, api_1.apiRequest)("GET", `/wallets/${walletId}/secrets`);
|
|
64
|
+
const foundSecret = getResult.secrets.find((s) => s.key_name === secretKey);
|
|
65
|
+
if (!foundSecret)
|
|
66
|
+
throw new Error("Secret not found in response");
|
|
67
|
+
const decryptedValue = await (0, crypto_1.decrypt)(foundSecret.encrypted_value, private_key);
|
|
68
|
+
console.log(` Secret retrieved and decrypted: ${decryptedValue}`);
|
|
69
|
+
if (decryptedValue === secretValue) {
|
|
70
|
+
console.log(" E2E TRANSACTION SUCCESSFUL");
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
throw new Error("Decrypted value mismatch");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
console.error(" E2E TEST FAILED:", error);
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
(0, crypto_1.shutdown)();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
testE2E();
|
|
84
|
+
//# sourceMappingURL=test-e2e.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-e2e.js","sourceRoot":"","sources":["../src/test-e2e.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@etree/cli",
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"bin": {
|
|
6
|
+
"et": "./dist/index.js",
|
|
7
|
+
"envtree": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "tsc --watch",
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"lint": "eslint ."
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"chalk": "^4.1.2",
|
|
17
|
+
"commander": "^13.1.0",
|
|
18
|
+
"conf": "^10.2.0",
|
|
19
|
+
"inquirer": "^8.2.6",
|
|
20
|
+
"ora": "^5.4.1"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@envtree/eslint-config": "workspace:*",
|
|
24
|
+
"@envtree/typescript-config": "workspace:*",
|
|
25
|
+
"@types/inquirer": "^8.2.10",
|
|
26
|
+
"@types/node": "^22.15.2",
|
|
27
|
+
"tsx": "^4.19.4",
|
|
28
|
+
"typescript": "5.9.2"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import inquirer from "inquirer";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import ora from "ora";
|
|
5
|
+
import { apiRequest } from "../lib/api";
|
|
6
|
+
import { saveSession, clearSession, getSession } from "../lib/config";
|
|
7
|
+
import { generateKeys, derivePublicKey, shutdown } from "../lib/crypto";
|
|
8
|
+
import { savePrivateKey, hasPrivateKey } from "../lib/key-store";
|
|
9
|
+
|
|
10
|
+
export function registerAuthCommands(program: Command): void {
|
|
11
|
+
// ── signup ──
|
|
12
|
+
program
|
|
13
|
+
.command("signup")
|
|
14
|
+
.description("Create a new EnvTree account")
|
|
15
|
+
.action(async () => {
|
|
16
|
+
const answers = await inquirer.prompt([
|
|
17
|
+
{ type: "input", name: "email", message: "Email:" },
|
|
18
|
+
{ type: "input", name: "username", message: "Username:" },
|
|
19
|
+
{
|
|
20
|
+
type: "password",
|
|
21
|
+
name: "password",
|
|
22
|
+
message: "Password:",
|
|
23
|
+
mask: "*",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
type: "password",
|
|
27
|
+
name: "confirmPassword",
|
|
28
|
+
message: "Confirm Password:",
|
|
29
|
+
mask: "*",
|
|
30
|
+
},
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
if (answers.password !== answers.confirmPassword) {
|
|
34
|
+
console.log(chalk.red("Passwords do not match"));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const spinner = ora("Creating account...").start();
|
|
39
|
+
try {
|
|
40
|
+
const result = await apiRequest(
|
|
41
|
+
"POST",
|
|
42
|
+
"/auth/signup",
|
|
43
|
+
{
|
|
44
|
+
email: answers.email,
|
|
45
|
+
password: answers.password,
|
|
46
|
+
username: answers.username,
|
|
47
|
+
},
|
|
48
|
+
false,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
if (!result.session) {
|
|
52
|
+
spinner.succeed(
|
|
53
|
+
"Account created! Please verify your email, then run: et login",
|
|
54
|
+
);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
saveSession({
|
|
59
|
+
access_token: result.session.access_token,
|
|
60
|
+
refresh_token: result.session.refresh_token,
|
|
61
|
+
user_id: result.user.id,
|
|
62
|
+
email: answers.email,
|
|
63
|
+
username: answers.username,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
spinner.text = "Generating encryption keys...";
|
|
67
|
+
const keys = await generateKeys();
|
|
68
|
+
savePrivateKey(keys.private_key);
|
|
69
|
+
|
|
70
|
+
await apiRequest("PUT", "/profile/public-key", {
|
|
71
|
+
public_key: keys.public_key,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
spinner.succeed(chalk.green("Account created & keys generated!"));
|
|
75
|
+
console.log(
|
|
76
|
+
chalk.dim(` User: ${answers.username} (${answers.email})`),
|
|
77
|
+
);
|
|
78
|
+
console.log(
|
|
79
|
+
chalk.dim(" Private key saved to ~/.envtree/private_key"),
|
|
80
|
+
);
|
|
81
|
+
} catch (err: any) {
|
|
82
|
+
spinner.fail(chalk.red(`Signup failed: ${err.message}`));
|
|
83
|
+
} finally {
|
|
84
|
+
shutdown();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ── login ──
|
|
89
|
+
program
|
|
90
|
+
.command("login")
|
|
91
|
+
.description("Log in to your EnvTree account")
|
|
92
|
+
.option("-e, --email <email>", "Email (non-interactive)")
|
|
93
|
+
.option("-p, --password <password>", "Password (non-interactive)")
|
|
94
|
+
.action(async (opts: { email?: string; password?: string }) => {
|
|
95
|
+
let email = opts.email;
|
|
96
|
+
let password = opts.password;
|
|
97
|
+
|
|
98
|
+
// Interactive mode if flags not provided
|
|
99
|
+
if (!email || !password) {
|
|
100
|
+
const answers = await inquirer.prompt([
|
|
101
|
+
...(!email
|
|
102
|
+
? [{ type: "input", name: "email", message: "Email:" }]
|
|
103
|
+
: []),
|
|
104
|
+
...(!password
|
|
105
|
+
? [
|
|
106
|
+
{
|
|
107
|
+
type: "password",
|
|
108
|
+
name: "password",
|
|
109
|
+
message: "Password:",
|
|
110
|
+
mask: "*",
|
|
111
|
+
},
|
|
112
|
+
]
|
|
113
|
+
: []),
|
|
114
|
+
]);
|
|
115
|
+
email = email || answers.email;
|
|
116
|
+
password = password || answers.password;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const spinner = ora("Logging in...").start();
|
|
120
|
+
try {
|
|
121
|
+
const result = await apiRequest(
|
|
122
|
+
"POST",
|
|
123
|
+
"/auth/login",
|
|
124
|
+
{ email, password },
|
|
125
|
+
false,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
saveSession({
|
|
129
|
+
access_token: result.session.access_token,
|
|
130
|
+
refresh_token: result.session.refresh_token,
|
|
131
|
+
user_id: result.user.id,
|
|
132
|
+
email: result.user.email,
|
|
133
|
+
username: email!,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Fetch profile to get real username
|
|
137
|
+
const profileResult = await apiRequest(
|
|
138
|
+
"GET",
|
|
139
|
+
"/profile/me",
|
|
140
|
+
undefined,
|
|
141
|
+
true,
|
|
142
|
+
);
|
|
143
|
+
if (profileResult.profile?.username) {
|
|
144
|
+
saveSession({
|
|
145
|
+
access_token: result.session.access_token,
|
|
146
|
+
refresh_token: result.session.refresh_token,
|
|
147
|
+
user_id: result.user.id,
|
|
148
|
+
email: result.user.email,
|
|
149
|
+
username: profileResult.profile.username,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Always sync public key with server
|
|
154
|
+
if (hasPrivateKey()) {
|
|
155
|
+
// Derive public key from existing private key and upload it
|
|
156
|
+
spinner.text = "Syncing encryption keys...";
|
|
157
|
+
const privateKey = require("../lib/key-store").getPrivateKey();
|
|
158
|
+
const publicKey = await derivePublicKey(privateKey);
|
|
159
|
+
await apiRequest("PUT", "/profile/public-key", {
|
|
160
|
+
public_key: publicKey,
|
|
161
|
+
});
|
|
162
|
+
spinner.succeed(chalk.green("Logged in!"));
|
|
163
|
+
} else {
|
|
164
|
+
// Generate a new keypair
|
|
165
|
+
spinner.text = "Generating encryption keys...";
|
|
166
|
+
const keys = await generateKeys();
|
|
167
|
+
savePrivateKey(keys.private_key);
|
|
168
|
+
|
|
169
|
+
await apiRequest("PUT", "/profile/public-key", {
|
|
170
|
+
public_key: keys.public_key,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
spinner.succeed(chalk.green("Logged in & keys generated!"));
|
|
174
|
+
console.log(
|
|
175
|
+
chalk.dim(" Private key saved to ~/.envtree/private_key"),
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
console.log(chalk.dim(` User: ${result.user.email}`));
|
|
180
|
+
} catch (err: any) {
|
|
181
|
+
spinner.fail(chalk.red(`Login failed: ${err.message}`));
|
|
182
|
+
} finally {
|
|
183
|
+
shutdown();
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// ── logout ──
|
|
188
|
+
program
|
|
189
|
+
.command("logout")
|
|
190
|
+
.description("Log out of your EnvTree account")
|
|
191
|
+
.action(() => {
|
|
192
|
+
clearSession();
|
|
193
|
+
console.log(chalk.green("Logged out"));
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// ── whoami ──
|
|
197
|
+
program
|
|
198
|
+
.command("whoami")
|
|
199
|
+
.description("Show the current logged-in user")
|
|
200
|
+
.action(async () => {
|
|
201
|
+
const session = getSession();
|
|
202
|
+
if (!session) {
|
|
203
|
+
console.log(chalk.yellow("Not logged in. Run: et login"));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const result = await apiRequest("GET", "/profile/me");
|
|
209
|
+
console.log(chalk.bold("EnvTree User"));
|
|
210
|
+
console.log(` Username: ${result.profile.username}`);
|
|
211
|
+
console.log(` Email: ${result.profile.email}`);
|
|
212
|
+
console.log(
|
|
213
|
+
` Public Key: ${result.profile.public_key ? result.profile.public_key.substring(0, 20) + "..." : chalk.yellow("not set")}`,
|
|
214
|
+
);
|
|
215
|
+
} catch {
|
|
216
|
+
console.log(chalk.dim(` Username: ${session.username}`));
|
|
217
|
+
console.log(chalk.dim(` Email: ${session.email}`));
|
|
218
|
+
console.log(chalk.yellow(" (offline — showing cached info)"));
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// ── delete-account ──
|
|
223
|
+
program
|
|
224
|
+
.command("delete-account")
|
|
225
|
+
.description("Permanently delete your EnvTree account")
|
|
226
|
+
.action(async () => {
|
|
227
|
+
const session = getSession();
|
|
228
|
+
if (!session) {
|
|
229
|
+
console.log(chalk.yellow("Not logged in. Run: et login"));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const { confirm } = await inquirer.prompt([
|
|
234
|
+
{
|
|
235
|
+
type: "confirm",
|
|
236
|
+
name: "confirm",
|
|
237
|
+
message: chalk.red(
|
|
238
|
+
"⚠ This will permanently delete your account and all your wallets. Are you sure?",
|
|
239
|
+
),
|
|
240
|
+
default: false,
|
|
241
|
+
},
|
|
242
|
+
]);
|
|
243
|
+
|
|
244
|
+
if (!confirm) {
|
|
245
|
+
console.log(chalk.dim("Cancelled."));
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const spinner = ora("Deleting account...").start();
|
|
250
|
+
try {
|
|
251
|
+
await apiRequest("DELETE", "/auth/account");
|
|
252
|
+
clearSession();
|
|
253
|
+
spinner.succeed(chalk.green("Account deleted. Goodbye!"));
|
|
254
|
+
} catch (err: any) {
|
|
255
|
+
spinner.fail(chalk.red(`Failed: ${err.message}`));
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { getConfig, saveConfig } from "../lib/config";
|
|
4
|
+
|
|
5
|
+
export function registerConfigCommands(program: Command): void {
|
|
6
|
+
const config = program
|
|
7
|
+
.command("config")
|
|
8
|
+
.description("Manage CLI configuration");
|
|
9
|
+
|
|
10
|
+
// ── config show ──
|
|
11
|
+
config
|
|
12
|
+
.command("show")
|
|
13
|
+
.description("Display current configuration")
|
|
14
|
+
.action(() => {
|
|
15
|
+
const cfg = getConfig();
|
|
16
|
+
console.log(chalk.bold("\nEnvTree Config"));
|
|
17
|
+
console.log(` Server: ${chalk.cyan(cfg.server_url)}`);
|
|
18
|
+
console.log(
|
|
19
|
+
` Session: ${cfg.session ? chalk.green("active") + chalk.dim(` (${cfg.session.email})`) : chalk.yellow("none")}`,
|
|
20
|
+
);
|
|
21
|
+
console.log(
|
|
22
|
+
chalk.dim(` Config: ~/.envtree/config.json`),
|
|
23
|
+
);
|
|
24
|
+
console.log(
|
|
25
|
+
chalk.dim(` Keys: ~/.envtree/private_key`),
|
|
26
|
+
);
|
|
27
|
+
console.log("");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// ── config set ──
|
|
31
|
+
config
|
|
32
|
+
.command("set <key> <value>")
|
|
33
|
+
.description("Set a configuration value (e.g. server URL)")
|
|
34
|
+
.action((key: string, value: string) => {
|
|
35
|
+
const cfg = getConfig();
|
|
36
|
+
|
|
37
|
+
if (key === "server" || key === "server_url") {
|
|
38
|
+
cfg.server_url = value;
|
|
39
|
+
saveConfig(cfg);
|
|
40
|
+
console.log(chalk.green(`Server URL set to: ${value}`));
|
|
41
|
+
} else {
|
|
42
|
+
console.log(
|
|
43
|
+
chalk.red(`Unknown config key: "${key}". Available: server`),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import inquirer from "inquirer";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import ora from "ora";
|
|
5
|
+
import { apiRequest } from "../lib/api";
|
|
6
|
+
import { saveSession } from "../lib/config";
|
|
7
|
+
import { generateKeys, derivePublicKey, shutdown } from "../lib/crypto";
|
|
8
|
+
import { savePrivateKey, hasPrivateKey, getPrivateKey } from "../lib/key-store";
|
|
9
|
+
|
|
10
|
+
export function registerInitCommand(program: Command): void {
|
|
11
|
+
program
|
|
12
|
+
.command("init")
|
|
13
|
+
.description("First-time setup — create an account or log in")
|
|
14
|
+
.action(async () => {
|
|
15
|
+
console.log(chalk.bold("\n🌲 Welcome to EnvTree!\n"));
|
|
16
|
+
|
|
17
|
+
const { action } = await inquirer.prompt([
|
|
18
|
+
{
|
|
19
|
+
type: "list",
|
|
20
|
+
name: "action",
|
|
21
|
+
message: "What would you like to do?",
|
|
22
|
+
choices: [
|
|
23
|
+
{ name: "Create a new account", value: "signup" },
|
|
24
|
+
{ name: "Log in to existing account", value: "login" },
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
if (action === "signup") {
|
|
30
|
+
const answers = await inquirer.prompt([
|
|
31
|
+
{ type: "input", name: "email", message: "Email:" },
|
|
32
|
+
{ type: "input", name: "username", message: "Username:" },
|
|
33
|
+
{
|
|
34
|
+
type: "password",
|
|
35
|
+
name: "password",
|
|
36
|
+
message: "Password:",
|
|
37
|
+
mask: "*",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
type: "password",
|
|
41
|
+
name: "confirmPassword",
|
|
42
|
+
message: "Confirm Password:",
|
|
43
|
+
mask: "*",
|
|
44
|
+
},
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
if (answers.password !== answers.confirmPassword) {
|
|
48
|
+
console.log(chalk.red("\n Passwords do not match"));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const spinner = ora("Creating account...").start();
|
|
53
|
+
try {
|
|
54
|
+
const result = await apiRequest(
|
|
55
|
+
"POST",
|
|
56
|
+
"/auth/signup",
|
|
57
|
+
{
|
|
58
|
+
email: answers.email,
|
|
59
|
+
password: answers.password,
|
|
60
|
+
username: answers.username,
|
|
61
|
+
},
|
|
62
|
+
false,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (!result.session) {
|
|
66
|
+
spinner.succeed(
|
|
67
|
+
"Account created! Check your email, then run: et login",
|
|
68
|
+
);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
saveSession({
|
|
73
|
+
access_token: result.session.access_token,
|
|
74
|
+
refresh_token: result.session.refresh_token,
|
|
75
|
+
user_id: result.user.id,
|
|
76
|
+
email: answers.email,
|
|
77
|
+
username: answers.username,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
spinner.text = "Generating encryption keys...";
|
|
81
|
+
const keys = await generateKeys();
|
|
82
|
+
savePrivateKey(keys.private_key);
|
|
83
|
+
|
|
84
|
+
await apiRequest("PUT", "/profile/public-key", {
|
|
85
|
+
public_key: keys.public_key,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
spinner.succeed(chalk.green("All set! You're ready to go."));
|
|
89
|
+
console.log(
|
|
90
|
+
chalk.dim(` User: ${answers.username} (${answers.email})`),
|
|
91
|
+
);
|
|
92
|
+
console.log("");
|
|
93
|
+
console.log(chalk.bold(" Next steps:"));
|
|
94
|
+
console.log(
|
|
95
|
+
chalk.cyan(" et wallet create my-project ") +
|
|
96
|
+
chalk.dim("Create a wallet"),
|
|
97
|
+
);
|
|
98
|
+
console.log(
|
|
99
|
+
chalk.cyan(' et push my-project API_KEY "sk-..." ') +
|
|
100
|
+
chalk.dim("Store a secret"),
|
|
101
|
+
);
|
|
102
|
+
console.log(
|
|
103
|
+
chalk.cyan(" et pull my-project ") +
|
|
104
|
+
chalk.dim("Sync to .env"),
|
|
105
|
+
);
|
|
106
|
+
console.log("");
|
|
107
|
+
} catch (err: any) {
|
|
108
|
+
spinner.fail(chalk.red(`Signup failed: ${err.message}`));
|
|
109
|
+
} finally {
|
|
110
|
+
shutdown();
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
// Login flow
|
|
114
|
+
const answers = await inquirer.prompt([
|
|
115
|
+
{ type: "input", name: "email", message: "Email:" },
|
|
116
|
+
{
|
|
117
|
+
type: "password",
|
|
118
|
+
name: "password",
|
|
119
|
+
message: "Password:",
|
|
120
|
+
mask: "*",
|
|
121
|
+
},
|
|
122
|
+
]);
|
|
123
|
+
|
|
124
|
+
const spinner = ora("Logging in...").start();
|
|
125
|
+
try {
|
|
126
|
+
const result = await apiRequest(
|
|
127
|
+
"POST",
|
|
128
|
+
"/auth/login",
|
|
129
|
+
{
|
|
130
|
+
email: answers.email,
|
|
131
|
+
password: answers.password,
|
|
132
|
+
},
|
|
133
|
+
false,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
saveSession({
|
|
137
|
+
access_token: result.session.access_token,
|
|
138
|
+
refresh_token: result.session.refresh_token,
|
|
139
|
+
user_id: result.user.id,
|
|
140
|
+
email: result.user.email,
|
|
141
|
+
username: answers.email,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const profileResult = await apiRequest(
|
|
145
|
+
"GET",
|
|
146
|
+
"/profile/me",
|
|
147
|
+
undefined,
|
|
148
|
+
true,
|
|
149
|
+
);
|
|
150
|
+
if (profileResult.profile?.username) {
|
|
151
|
+
saveSession({
|
|
152
|
+
access_token: result.session.access_token,
|
|
153
|
+
refresh_token: result.session.refresh_token,
|
|
154
|
+
user_id: result.user.id,
|
|
155
|
+
email: result.user.email,
|
|
156
|
+
username: profileResult.profile.username,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Always sync public key with server
|
|
161
|
+
if (hasPrivateKey()) {
|
|
162
|
+
spinner.text = "Syncing encryption keys...";
|
|
163
|
+
const pk = getPrivateKey()!;
|
|
164
|
+
const publicKey = await derivePublicKey(pk);
|
|
165
|
+
await apiRequest("PUT", "/profile/public-key", {
|
|
166
|
+
public_key: publicKey,
|
|
167
|
+
});
|
|
168
|
+
} else {
|
|
169
|
+
spinner.text = "Generating encryption keys...";
|
|
170
|
+
const keys = await generateKeys();
|
|
171
|
+
savePrivateKey(keys.private_key);
|
|
172
|
+
await apiRequest("PUT", "/profile/public-key", {
|
|
173
|
+
public_key: keys.public_key,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
spinner.succeed(chalk.green("Logged in! You're ready to go."));
|
|
178
|
+
console.log(chalk.dim(` User: ${result.user.email}`));
|
|
179
|
+
console.log("");
|
|
180
|
+
} catch (err: any) {
|
|
181
|
+
spinner.fail(chalk.red(`Login failed: ${err.message}`));
|
|
182
|
+
} finally {
|
|
183
|
+
shutdown();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|