@envpilot/cli 1.4.0 → 1.5.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.
- package/LICENSE +8 -0
- package/README.md +109 -32
- package/dist/app-KHDRTCOB.js +216 -0
- package/dist/chunk-CIIQSYAS.js +4164 -0
- package/dist/index.js +45 -3546
- package/package.json +7 -3
|
@@ -0,0 +1,4164 @@
|
|
|
1
|
+
// src/lib/sentry.ts
|
|
2
|
+
import * as Sentry from "@sentry/node";
|
|
3
|
+
var initialized = false;
|
|
4
|
+
function initSentry() {
|
|
5
|
+
const dsn = true ? "" : "";
|
|
6
|
+
if (initialized || !dsn) return;
|
|
7
|
+
Sentry.init({
|
|
8
|
+
dsn,
|
|
9
|
+
environment: "cli",
|
|
10
|
+
release: true ? "1.5.0" : "0.0.0",
|
|
11
|
+
// Free tier: disable performance monitoring
|
|
12
|
+
tracesSampleRate: 0,
|
|
13
|
+
beforeSend(event) {
|
|
14
|
+
if (event.exception?.values) {
|
|
15
|
+
for (const exc of event.exception.values) {
|
|
16
|
+
if (exc.stacktrace?.frames) {
|
|
17
|
+
for (const frame of exc.stacktrace.frames) {
|
|
18
|
+
if (frame.filename) {
|
|
19
|
+
frame.filename = frame.filename.replace(
|
|
20
|
+
/\/Users\/[^/]+/g,
|
|
21
|
+
"/~"
|
|
22
|
+
);
|
|
23
|
+
frame.filename = frame.filename.replace(
|
|
24
|
+
/C:\\Users\\[^\\]+/g,
|
|
25
|
+
"C:\\~"
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (event.request?.data) {
|
|
33
|
+
event.request.data = "[REDACTED]";
|
|
34
|
+
}
|
|
35
|
+
return event;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
initialized = true;
|
|
39
|
+
}
|
|
40
|
+
function captureError(error2, context) {
|
|
41
|
+
if (!initialized) return;
|
|
42
|
+
Sentry.captureException(error2, { tags: context });
|
|
43
|
+
}
|
|
44
|
+
async function flushSentry() {
|
|
45
|
+
if (!initialized) return;
|
|
46
|
+
await Sentry.flush(2e3);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/lib/config.ts
|
|
50
|
+
import Conf from "conf";
|
|
51
|
+
var DEFAULT_API_URL = "https://www.envpilot.dev";
|
|
52
|
+
var config = new Conf({
|
|
53
|
+
projectName: "envpilot",
|
|
54
|
+
defaults: {
|
|
55
|
+
apiUrl: DEFAULT_API_URL
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
function getConfig() {
|
|
59
|
+
return {
|
|
60
|
+
apiUrl: config.get("apiUrl") ?? DEFAULT_API_URL,
|
|
61
|
+
accessToken: config.get("accessToken"),
|
|
62
|
+
refreshToken: config.get("refreshToken"),
|
|
63
|
+
activeProjectId: config.get("activeProjectId"),
|
|
64
|
+
activeOrganizationId: config.get("activeOrganizationId"),
|
|
65
|
+
user: config.get("user"),
|
|
66
|
+
role: config.get("role")
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function getApiUrl() {
|
|
70
|
+
return config.get("apiUrl") ?? DEFAULT_API_URL;
|
|
71
|
+
}
|
|
72
|
+
function setApiUrl(url) {
|
|
73
|
+
config.set("apiUrl", url);
|
|
74
|
+
}
|
|
75
|
+
function getAccessToken() {
|
|
76
|
+
return config.get("accessToken");
|
|
77
|
+
}
|
|
78
|
+
function setAccessToken(token) {
|
|
79
|
+
config.set("accessToken", token);
|
|
80
|
+
}
|
|
81
|
+
function setRefreshToken(token) {
|
|
82
|
+
config.set("refreshToken", token);
|
|
83
|
+
}
|
|
84
|
+
function getActiveProjectId() {
|
|
85
|
+
return config.get("activeProjectId");
|
|
86
|
+
}
|
|
87
|
+
function setActiveProjectId(projectId) {
|
|
88
|
+
config.set("activeProjectId", projectId);
|
|
89
|
+
}
|
|
90
|
+
function getActiveOrganizationId() {
|
|
91
|
+
return config.get("activeOrganizationId");
|
|
92
|
+
}
|
|
93
|
+
function setActiveOrganizationId(organizationId) {
|
|
94
|
+
config.set("activeOrganizationId", organizationId);
|
|
95
|
+
}
|
|
96
|
+
function getUser() {
|
|
97
|
+
return config.get("user");
|
|
98
|
+
}
|
|
99
|
+
function setUser(user) {
|
|
100
|
+
config.set("user", user);
|
|
101
|
+
}
|
|
102
|
+
function getRole() {
|
|
103
|
+
return config.get("role");
|
|
104
|
+
}
|
|
105
|
+
function setRole(role) {
|
|
106
|
+
config.set("role", role);
|
|
107
|
+
}
|
|
108
|
+
function isAuthenticated() {
|
|
109
|
+
return !!config.get("accessToken");
|
|
110
|
+
}
|
|
111
|
+
function clearAuth() {
|
|
112
|
+
config.delete("accessToken");
|
|
113
|
+
config.delete("refreshToken");
|
|
114
|
+
config.delete("user");
|
|
115
|
+
config.delete("role");
|
|
116
|
+
}
|
|
117
|
+
function clearConfig() {
|
|
118
|
+
config.clear();
|
|
119
|
+
}
|
|
120
|
+
function getConfigPath() {
|
|
121
|
+
return config.path;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/ui/render-tui.tsx
|
|
125
|
+
import chalk from "chalk";
|
|
126
|
+
|
|
127
|
+
// src/ui/execute-command.ts
|
|
128
|
+
import { spawn } from "child_process";
|
|
129
|
+
import { dirname, resolve } from "path";
|
|
130
|
+
import { fileURLToPath } from "url";
|
|
131
|
+
async function executeCommand(argv) {
|
|
132
|
+
const scriptPath = resolve(
|
|
133
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
134
|
+
"index.js"
|
|
135
|
+
);
|
|
136
|
+
if (process.stdin.isTTY && process.stdin.isRaw) {
|
|
137
|
+
process.stdin.setRawMode(false);
|
|
138
|
+
}
|
|
139
|
+
process.stdin.pause();
|
|
140
|
+
return new Promise((resolve3, reject) => {
|
|
141
|
+
const child = spawn(process.execPath, [scriptPath, ...argv], {
|
|
142
|
+
stdio: "inherit",
|
|
143
|
+
env: {
|
|
144
|
+
...process.env,
|
|
145
|
+
ENVPILOT_TUI_CHILD: "1"
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
child.on("error", reject);
|
|
149
|
+
child.on("exit", (code) => {
|
|
150
|
+
resolve3(code ?? 0);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/ui/render-tui.tsx
|
|
156
|
+
import { jsx } from "react/jsx-runtime";
|
|
157
|
+
async function openTUI() {
|
|
158
|
+
const [{ render }, { CLIApp }] = await Promise.all([
|
|
159
|
+
import("ink"),
|
|
160
|
+
import("./app-KHDRTCOB.js")
|
|
161
|
+
]);
|
|
162
|
+
while (true) {
|
|
163
|
+
let selectedArgv = null;
|
|
164
|
+
const app = render(
|
|
165
|
+
/* @__PURE__ */ jsx(
|
|
166
|
+
CLIApp,
|
|
167
|
+
{
|
|
168
|
+
onSelectCommand: (argv) => {
|
|
169
|
+
selectedArgv = argv;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
)
|
|
173
|
+
);
|
|
174
|
+
await app.waitUntilExit();
|
|
175
|
+
if (!selectedArgv) {
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
const code = await executeCommand(selectedArgv);
|
|
179
|
+
if (code !== 0) {
|
|
180
|
+
process.exitCode = code;
|
|
181
|
+
}
|
|
182
|
+
console.log();
|
|
183
|
+
console.log(
|
|
184
|
+
chalk.dim(" Press any key to return to the TUI\u2026 (q to quit)")
|
|
185
|
+
);
|
|
186
|
+
const quit = await new Promise((resolve3) => {
|
|
187
|
+
if (process.stdin.isTTY) {
|
|
188
|
+
process.stdin.setRawMode(true);
|
|
189
|
+
}
|
|
190
|
+
process.stdin.resume();
|
|
191
|
+
process.stdin.once("data", (data) => {
|
|
192
|
+
const ch = data.toString();
|
|
193
|
+
if (process.stdin.isTTY) {
|
|
194
|
+
process.stdin.setRawMode(false);
|
|
195
|
+
}
|
|
196
|
+
process.stdin.pause();
|
|
197
|
+
resolve3(ch === "q" || ch === "Q");
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
if (quit) {
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/commands/login.ts
|
|
207
|
+
import { Command } from "commander";
|
|
208
|
+
|
|
209
|
+
// src/lib/ui.ts
|
|
210
|
+
import chalk2 from "chalk";
|
|
211
|
+
import ora from "ora";
|
|
212
|
+
function createSpinner(text) {
|
|
213
|
+
return ora({
|
|
214
|
+
text,
|
|
215
|
+
color: "cyan"
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
async function withSpinner(text, operation, options) {
|
|
219
|
+
const spinner = createSpinner(text);
|
|
220
|
+
spinner.start();
|
|
221
|
+
try {
|
|
222
|
+
const result = await operation();
|
|
223
|
+
spinner.succeed(options?.successText ?? text);
|
|
224
|
+
return result;
|
|
225
|
+
} catch (error2) {
|
|
226
|
+
spinner.fail(options?.failText ?? text);
|
|
227
|
+
throw error2;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function success(message) {
|
|
231
|
+
console.log(chalk2.green("\u2713"), message);
|
|
232
|
+
}
|
|
233
|
+
function info(message) {
|
|
234
|
+
console.log(chalk2.blue("\u2139"), message);
|
|
235
|
+
}
|
|
236
|
+
function warning(message) {
|
|
237
|
+
console.log(chalk2.yellow("\u26A0"), message);
|
|
238
|
+
}
|
|
239
|
+
function error(message) {
|
|
240
|
+
console.log(chalk2.red("\u2717"), message);
|
|
241
|
+
}
|
|
242
|
+
function header(text) {
|
|
243
|
+
console.log();
|
|
244
|
+
console.log(chalk2.bold(text));
|
|
245
|
+
console.log(chalk2.dim("\u2500".repeat(text.length)));
|
|
246
|
+
}
|
|
247
|
+
function table(data, columns) {
|
|
248
|
+
if (data.length === 0) {
|
|
249
|
+
console.log(chalk2.dim("No data to display"));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const widths = columns.map((col) => {
|
|
253
|
+
const headerWidth = col.header.length;
|
|
254
|
+
const maxDataWidth = Math.max(
|
|
255
|
+
...data.map((row) => String(row[col.key] ?? "").length)
|
|
256
|
+
);
|
|
257
|
+
return col.width ?? Math.max(headerWidth, maxDataWidth);
|
|
258
|
+
});
|
|
259
|
+
const headerLine = columns.map((col, i) => col.header.padEnd(widths[i])).join(" ");
|
|
260
|
+
console.log(chalk2.bold(headerLine));
|
|
261
|
+
console.log(chalk2.dim("\u2500".repeat(headerLine.length)));
|
|
262
|
+
for (const row of data) {
|
|
263
|
+
const line2 = columns.map((col, i) => String(row[col.key] ?? "").padEnd(widths[i])).join(" ");
|
|
264
|
+
console.log(line2);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function keyValue(pairs) {
|
|
268
|
+
const maxKeyLength = Math.max(...pairs.map(([key]) => key.length));
|
|
269
|
+
for (const [key, value] of pairs) {
|
|
270
|
+
const paddedKey = key.padEnd(maxKeyLength);
|
|
271
|
+
console.log(`${chalk2.dim(paddedKey)} ${value ?? chalk2.dim("(not set)")}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function diff(added, removed, changed) {
|
|
275
|
+
if (Object.keys(added).length === 0 && Object.keys(removed).length === 0 && Object.keys(changed).length === 0) {
|
|
276
|
+
console.log(chalk2.dim("No changes"));
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
for (const [key, value] of Object.entries(added)) {
|
|
280
|
+
console.log(chalk2.green(`+ ${key}=${maskValue(value)}`));
|
|
281
|
+
}
|
|
282
|
+
for (const [key, value] of Object.entries(removed)) {
|
|
283
|
+
console.log(chalk2.red(`- ${key}=${maskValue(value)}`));
|
|
284
|
+
}
|
|
285
|
+
for (const [key, { local, remote }] of Object.entries(changed)) {
|
|
286
|
+
console.log(chalk2.red(`- ${key}=${maskValue(remote)}`));
|
|
287
|
+
console.log(chalk2.green(`+ ${key}=${maskValue(local)}`));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function maskValue(value, showChars = 4) {
|
|
291
|
+
if (value.length <= showChars * 2) {
|
|
292
|
+
return "*".repeat(value.length);
|
|
293
|
+
}
|
|
294
|
+
return value.slice(0, showChars) + "****" + value.slice(-showChars);
|
|
295
|
+
}
|
|
296
|
+
function line() {
|
|
297
|
+
console.log(chalk2.dim("\u2500".repeat(50)));
|
|
298
|
+
}
|
|
299
|
+
function blank() {
|
|
300
|
+
console.log();
|
|
301
|
+
}
|
|
302
|
+
function formatRole(role) {
|
|
303
|
+
switch (role) {
|
|
304
|
+
case "admin":
|
|
305
|
+
return chalk2.green("Admin");
|
|
306
|
+
case "team_lead":
|
|
307
|
+
return chalk2.blue("Team Lead");
|
|
308
|
+
case "member":
|
|
309
|
+
return chalk2.yellow("Member");
|
|
310
|
+
default:
|
|
311
|
+
return chalk2.dim("Unknown");
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function roleNotice(role) {
|
|
315
|
+
if (role === "member") {
|
|
316
|
+
console.log(
|
|
317
|
+
chalk2.yellow(
|
|
318
|
+
" You have Member access. Write operations will create pending requests for approval."
|
|
319
|
+
)
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function formatProjectRole(role) {
|
|
324
|
+
switch (role) {
|
|
325
|
+
case "manager":
|
|
326
|
+
return chalk2.green("Manager");
|
|
327
|
+
case "developer":
|
|
328
|
+
return chalk2.blue("Developer");
|
|
329
|
+
case "viewer":
|
|
330
|
+
return chalk2.yellow("Viewer");
|
|
331
|
+
default:
|
|
332
|
+
return chalk2.dim("-");
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function projectRoleNotice(projectRole) {
|
|
336
|
+
if (projectRole === "viewer") {
|
|
337
|
+
console.log(
|
|
338
|
+
chalk2.yellow(
|
|
339
|
+
" You have Viewer access to this project. You can only view variables you have been explicitly granted access to."
|
|
340
|
+
)
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// src/lib/errors.ts
|
|
346
|
+
import chalk3 from "chalk";
|
|
347
|
+
var CLIError = class extends Error {
|
|
348
|
+
constructor(message, code, suggestion) {
|
|
349
|
+
super(message);
|
|
350
|
+
this.code = code;
|
|
351
|
+
this.suggestion = suggestion;
|
|
352
|
+
this.name = "CLIError";
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
var ErrorCodes = {
|
|
356
|
+
NOT_AUTHENTICATED: "NOT_AUTHENTICATED",
|
|
357
|
+
NOT_INITIALIZED: "NOT_INITIALIZED",
|
|
358
|
+
PROJECT_NOT_FOUND: "PROJECT_NOT_FOUND",
|
|
359
|
+
ORGANIZATION_NOT_FOUND: "ORGANIZATION_NOT_FOUND",
|
|
360
|
+
VARIABLE_NOT_FOUND: "VARIABLE_NOT_FOUND",
|
|
361
|
+
INVALID_CONFIG: "INVALID_CONFIG",
|
|
362
|
+
NETWORK_ERROR: "NETWORK_ERROR",
|
|
363
|
+
PERMISSION_DENIED: "PERMISSION_DENIED",
|
|
364
|
+
TIER_LIMIT_EXCEEDED: "TIER_LIMIT_EXCEEDED",
|
|
365
|
+
FILE_NOT_FOUND: "FILE_NOT_FOUND",
|
|
366
|
+
INVALID_INPUT: "INVALID_INPUT",
|
|
367
|
+
UNKNOWN_ERROR: "UNKNOWN_ERROR"
|
|
368
|
+
};
|
|
369
|
+
function formatError(error2) {
|
|
370
|
+
if (error2 instanceof CLIError) {
|
|
371
|
+
let message = chalk3.red(`Error: ${error2.message}`);
|
|
372
|
+
if (error2.suggestion) {
|
|
373
|
+
message += `
|
|
374
|
+
${chalk3.yellow("Suggestion:")} ${error2.suggestion}`;
|
|
375
|
+
}
|
|
376
|
+
return message;
|
|
377
|
+
}
|
|
378
|
+
if (error2 instanceof Error) {
|
|
379
|
+
return chalk3.red(`Error: ${error2.message}`);
|
|
380
|
+
}
|
|
381
|
+
return chalk3.red(`Error: ${String(error2)}`);
|
|
382
|
+
}
|
|
383
|
+
async function handleError(error2) {
|
|
384
|
+
console.error(formatError(error2));
|
|
385
|
+
const skipCodes = /* @__PURE__ */ new Set([
|
|
386
|
+
ErrorCodes.NOT_AUTHENTICATED,
|
|
387
|
+
ErrorCodes.INVALID_INPUT,
|
|
388
|
+
ErrorCodes.NOT_INITIALIZED
|
|
389
|
+
]);
|
|
390
|
+
if (error2 instanceof CLIError) {
|
|
391
|
+
if (!skipCodes.has(error2.code)) {
|
|
392
|
+
captureError(error2, { errorCode: error2.code });
|
|
393
|
+
}
|
|
394
|
+
} else {
|
|
395
|
+
captureError(error2);
|
|
396
|
+
}
|
|
397
|
+
await flushSentry();
|
|
398
|
+
if (error2 instanceof CLIError) {
|
|
399
|
+
switch (error2.code) {
|
|
400
|
+
case ErrorCodes.NOT_AUTHENTICATED:
|
|
401
|
+
process.exit(2);
|
|
402
|
+
break;
|
|
403
|
+
case ErrorCodes.PERMISSION_DENIED:
|
|
404
|
+
process.exit(3);
|
|
405
|
+
break;
|
|
406
|
+
case ErrorCodes.TIER_LIMIT_EXCEEDED:
|
|
407
|
+
process.exit(4);
|
|
408
|
+
break;
|
|
409
|
+
default:
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
process.exit(1);
|
|
414
|
+
}
|
|
415
|
+
function notAuthenticated() {
|
|
416
|
+
return new CLIError(
|
|
417
|
+
"You are not authenticated.",
|
|
418
|
+
ErrorCodes.NOT_AUTHENTICATED,
|
|
419
|
+
"Run `envpilot login` to authenticate."
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
function notInitialized() {
|
|
423
|
+
return new CLIError(
|
|
424
|
+
"This directory is not initialized with Envpilot.",
|
|
425
|
+
ErrorCodes.NOT_INITIALIZED,
|
|
426
|
+
"Run `envpilot init` to initialize."
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
function fileNotFound(path) {
|
|
430
|
+
return new CLIError(`File not found: ${path}`, ErrorCodes.FILE_NOT_FOUND);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// src/lib/auth-flow.ts
|
|
434
|
+
import open from "open";
|
|
435
|
+
import chalk4 from "chalk";
|
|
436
|
+
import { hostname } from "os";
|
|
437
|
+
|
|
438
|
+
// src/lib/api.ts
|
|
439
|
+
var APIError = class extends Error {
|
|
440
|
+
constructor(message, statusCode, code) {
|
|
441
|
+
super(message);
|
|
442
|
+
this.statusCode = statusCode;
|
|
443
|
+
this.code = code;
|
|
444
|
+
this.name = "APIError";
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
var APIClient = class {
|
|
448
|
+
baseUrl;
|
|
449
|
+
accessToken;
|
|
450
|
+
constructor(options) {
|
|
451
|
+
this.baseUrl = options?.baseUrl ?? getApiUrl();
|
|
452
|
+
this.accessToken = options?.accessToken ?? getAccessToken();
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Get headers for API requests
|
|
456
|
+
*/
|
|
457
|
+
getHeaders() {
|
|
458
|
+
const headers = {
|
|
459
|
+
"Content-Type": "application/json"
|
|
460
|
+
};
|
|
461
|
+
if (this.accessToken) {
|
|
462
|
+
headers["Authorization"] = `Bearer ${this.accessToken}`;
|
|
463
|
+
}
|
|
464
|
+
return headers;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Detect auth middleware redirects that returned HTML instead of CLI JSON.
|
|
468
|
+
*/
|
|
469
|
+
isAuthRedirect(response, bodyText) {
|
|
470
|
+
const location = response.headers.get("location") || "";
|
|
471
|
+
const finalUrl = response.url || "";
|
|
472
|
+
const contentType = response.headers.get("content-type") || "";
|
|
473
|
+
const preview = (bodyText || "").slice(0, 512).toLowerCase();
|
|
474
|
+
return response.redirected || location.includes("authkit") || finalUrl.includes("authkit") || contentType.includes("text/html") && (preview.includes("authorization_session_id") || preview.includes("client_id=") || preview.includes("<!doctype html"));
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Make a GET request
|
|
478
|
+
*/
|
|
479
|
+
async get(path, params) {
|
|
480
|
+
const url = new URL(path, this.baseUrl);
|
|
481
|
+
if (params) {
|
|
482
|
+
for (const [key, value] of Object.entries(params)) {
|
|
483
|
+
url.searchParams.set(key, value);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
const response = await fetch(url.toString(), {
|
|
487
|
+
method: "GET",
|
|
488
|
+
headers: this.getHeaders(),
|
|
489
|
+
redirect: "follow"
|
|
490
|
+
});
|
|
491
|
+
return this.handleResponse(response);
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Make a POST request
|
|
495
|
+
*/
|
|
496
|
+
async post(path, body) {
|
|
497
|
+
const url = new URL(path, this.baseUrl);
|
|
498
|
+
const response = await fetch(url.toString(), {
|
|
499
|
+
method: "POST",
|
|
500
|
+
headers: this.getHeaders(),
|
|
501
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
502
|
+
redirect: "follow"
|
|
503
|
+
});
|
|
504
|
+
return this.handleResponse(response);
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Make a PUT request
|
|
508
|
+
*/
|
|
509
|
+
async put(path, body) {
|
|
510
|
+
const url = new URL(path, this.baseUrl);
|
|
511
|
+
const response = await fetch(url.toString(), {
|
|
512
|
+
method: "PUT",
|
|
513
|
+
headers: this.getHeaders(),
|
|
514
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
515
|
+
redirect: "follow"
|
|
516
|
+
});
|
|
517
|
+
return this.handleResponse(response);
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Make a PATCH request
|
|
521
|
+
*/
|
|
522
|
+
async patch(path, body) {
|
|
523
|
+
const url = new URL(path, this.baseUrl);
|
|
524
|
+
const response = await fetch(url.toString(), {
|
|
525
|
+
method: "PATCH",
|
|
526
|
+
headers: this.getHeaders(),
|
|
527
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
528
|
+
redirect: "follow"
|
|
529
|
+
});
|
|
530
|
+
return this.handleResponse(response);
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Make a DELETE request
|
|
534
|
+
*/
|
|
535
|
+
async delete(path) {
|
|
536
|
+
const url = new URL(path, this.baseUrl);
|
|
537
|
+
const response = await fetch(url.toString(), {
|
|
538
|
+
method: "DELETE",
|
|
539
|
+
headers: this.getHeaders(),
|
|
540
|
+
redirect: "follow"
|
|
541
|
+
});
|
|
542
|
+
if (!response.ok) {
|
|
543
|
+
await this.handleError(response);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Handle API response
|
|
548
|
+
*/
|
|
549
|
+
async handleResponse(response) {
|
|
550
|
+
if (!response.ok) {
|
|
551
|
+
await this.handleError(response);
|
|
552
|
+
}
|
|
553
|
+
const contentType = response.headers.get("content-type") || "";
|
|
554
|
+
if (!contentType.includes("application/json")) {
|
|
555
|
+
const body2 = await response.text();
|
|
556
|
+
if (this.isAuthRedirect(response, body2)) {
|
|
557
|
+
clearAuth();
|
|
558
|
+
throw new APIError(
|
|
559
|
+
"Your CLI session is not authorized for this endpoint. Please run `envpilot login` and try again.",
|
|
560
|
+
401,
|
|
561
|
+
"AUTH_REDIRECT"
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
const preview = body2.replace(/\s+/g, " ").slice(0, 160);
|
|
565
|
+
throw new APIError(
|
|
566
|
+
`Expected JSON but got ${contentType || "unknown content type"} from ${response.url}. Response starts with: ${preview}`,
|
|
567
|
+
response.status || 500
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
const body = await response.text();
|
|
571
|
+
try {
|
|
572
|
+
const data = JSON.parse(body);
|
|
573
|
+
return data;
|
|
574
|
+
} catch {
|
|
575
|
+
const preview = body.replace(/\s+/g, " ").slice(0, 160);
|
|
576
|
+
throw new APIError(
|
|
577
|
+
`Failed to parse JSON response from ${response.url}. Response starts with: ${preview}`,
|
|
578
|
+
response.status || 500
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Handle API errors
|
|
584
|
+
*/
|
|
585
|
+
async handleError(response) {
|
|
586
|
+
let message = `Request failed with status ${response.status}`;
|
|
587
|
+
let code;
|
|
588
|
+
try {
|
|
589
|
+
const data = await response.json();
|
|
590
|
+
message = data.error || data.message || message;
|
|
591
|
+
code = data.code;
|
|
592
|
+
} catch {
|
|
593
|
+
}
|
|
594
|
+
if (response.status >= 300 && response.status < 400 && this.isAuthRedirect(response)) {
|
|
595
|
+
clearAuth();
|
|
596
|
+
throw new APIError(
|
|
597
|
+
"Your CLI session expired or the server redirected this request to browser sign-in. Please run `envpilot login`.",
|
|
598
|
+
401,
|
|
599
|
+
"AUTH_REDIRECT"
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
if (response.status === 401) {
|
|
603
|
+
clearAuth();
|
|
604
|
+
throw new APIError(
|
|
605
|
+
"Authentication required. Please run `envpilot login`.",
|
|
606
|
+
401,
|
|
607
|
+
"UNAUTHORIZED"
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
if (response.status === 403 && code === "TIER_LIMIT_REACHED") {
|
|
611
|
+
throw new APIError(
|
|
612
|
+
message || "Tier limit reached. Run `envpilot usage` to see your plan limits.",
|
|
613
|
+
403,
|
|
614
|
+
"TIER_LIMIT_REACHED"
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
if (response.status === 403) {
|
|
618
|
+
throw new APIError(message || "Access denied.", 403, code || "FORBIDDEN");
|
|
619
|
+
}
|
|
620
|
+
if (response.status === 402) {
|
|
621
|
+
throw new APIError(
|
|
622
|
+
message || "Tier limit reached. Run `envpilot usage` to see your plan limits.",
|
|
623
|
+
402,
|
|
624
|
+
"PAYMENT_REQUIRED"
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
throw new APIError(message, response.status, code);
|
|
628
|
+
}
|
|
629
|
+
// ============================================
|
|
630
|
+
// High-level API methods
|
|
631
|
+
// ============================================
|
|
632
|
+
/**
|
|
633
|
+
* Get current user info
|
|
634
|
+
*/
|
|
635
|
+
async getCurrentUser() {
|
|
636
|
+
return this.get("/api/cli/auth/me");
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Get tier info for the active organization
|
|
640
|
+
*/
|
|
641
|
+
async getTierInfo(organizationId) {
|
|
642
|
+
return this.get("/api/cli/tier", { organizationId });
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Get usage info for the active organization
|
|
646
|
+
*/
|
|
647
|
+
async getUsage(organizationId) {
|
|
648
|
+
return this.get("/api/cli/usage", { organizationId });
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* List organizations the user has access to
|
|
652
|
+
*/
|
|
653
|
+
async listOrganizations() {
|
|
654
|
+
const response = await this.get(
|
|
655
|
+
"/api/cli/organizations"
|
|
656
|
+
);
|
|
657
|
+
return response.data || [];
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* List projects in an organization
|
|
661
|
+
*/
|
|
662
|
+
async listProjects(organizationId) {
|
|
663
|
+
const response = await this.get(
|
|
664
|
+
"/api/cli/projects",
|
|
665
|
+
{ organizationId }
|
|
666
|
+
);
|
|
667
|
+
return response.data || [];
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Get a project by ID
|
|
671
|
+
*/
|
|
672
|
+
async getProject(projectId) {
|
|
673
|
+
return this.get(`/api/cli/projects/${projectId}`);
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* List variables in a project
|
|
677
|
+
*/
|
|
678
|
+
async listVariables(projectId, environment, organizationId) {
|
|
679
|
+
const params = { projectId };
|
|
680
|
+
if (environment) {
|
|
681
|
+
params.environment = environment;
|
|
682
|
+
}
|
|
683
|
+
if (organizationId) {
|
|
684
|
+
params.organizationId = organizationId;
|
|
685
|
+
}
|
|
686
|
+
const response = await this.get(
|
|
687
|
+
"/api/cli/variables",
|
|
688
|
+
params
|
|
689
|
+
);
|
|
690
|
+
return response.data || [];
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Get a variable by ID (with decrypted value)
|
|
694
|
+
*/
|
|
695
|
+
async getVariable(variableId) {
|
|
696
|
+
return this.get(`/api/cli/variables/${variableId}`);
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Create a new variable
|
|
700
|
+
*/
|
|
701
|
+
async createVariable(data) {
|
|
702
|
+
return this.post("/api/cli/variables", data);
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Update a variable
|
|
706
|
+
*/
|
|
707
|
+
async updateVariable(variableId, data) {
|
|
708
|
+
return this.patch(`/api/cli/variables/${variableId}`, data);
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Delete a variable
|
|
712
|
+
*/
|
|
713
|
+
async deleteVariable(variableId) {
|
|
714
|
+
return this.delete(`/api/cli/variables/${variableId}`);
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Bulk create/update variables
|
|
718
|
+
*/
|
|
719
|
+
async bulkUpsertVariables(data) {
|
|
720
|
+
return this.post("/api/cli/variables/bulk", data);
|
|
721
|
+
}
|
|
722
|
+
// ============================================
|
|
723
|
+
// Authentication methods
|
|
724
|
+
// ============================================
|
|
725
|
+
/**
|
|
726
|
+
* Initiate CLI authentication flow
|
|
727
|
+
*/
|
|
728
|
+
async initiateAuth(deviceName) {
|
|
729
|
+
return this.post("/api/cli/auth/initiate", { deviceName });
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Poll for authentication status
|
|
733
|
+
*/
|
|
734
|
+
async pollAuth(code) {
|
|
735
|
+
return this.get("/api/cli/auth/poll", { code });
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Refresh access token
|
|
739
|
+
*/
|
|
740
|
+
async refreshToken(refreshToken) {
|
|
741
|
+
return this.post("/api/cli/auth/refresh", { refreshToken });
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Revoke access token (logout)
|
|
745
|
+
*/
|
|
746
|
+
async revokeToken() {
|
|
747
|
+
return this.post("/api/cli/auth/revoke", {});
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
function createAPIClient() {
|
|
751
|
+
return new APIClient();
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// src/lib/auth-flow.ts
|
|
755
|
+
var POLL_INTERVAL_MS = 2e3;
|
|
756
|
+
var MAX_POLL_ATTEMPTS = 150;
|
|
757
|
+
var MAX_CONSECUTIVE_ERRORS = 5;
|
|
758
|
+
function sleep(ms) {
|
|
759
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
760
|
+
}
|
|
761
|
+
async function performLogin(options) {
|
|
762
|
+
const api = createAPIClient();
|
|
763
|
+
const deviceName = `CLI - ${hostname()}`;
|
|
764
|
+
info("Starting authentication flow...");
|
|
765
|
+
const spinner = createSpinner("Generating authentication code...");
|
|
766
|
+
spinner.start();
|
|
767
|
+
let initResponse;
|
|
768
|
+
try {
|
|
769
|
+
initResponse = await api.post("/api/cli/auth?action=initiate", { deviceName });
|
|
770
|
+
} catch (error2) {
|
|
771
|
+
spinner.stop();
|
|
772
|
+
throw error2;
|
|
773
|
+
}
|
|
774
|
+
spinner.stop();
|
|
775
|
+
console.log();
|
|
776
|
+
console.log(chalk4.bold("Your authentication code:"));
|
|
777
|
+
console.log();
|
|
778
|
+
console.log(chalk4.cyan.bold(` ${initResponse.code}`));
|
|
779
|
+
console.log();
|
|
780
|
+
console.log(`Open this URL to authenticate:`);
|
|
781
|
+
console.log(chalk4.dim(initResponse.url));
|
|
782
|
+
console.log();
|
|
783
|
+
if (options?.browser !== false) {
|
|
784
|
+
info("Opening browser...");
|
|
785
|
+
await open(initResponse.url);
|
|
786
|
+
}
|
|
787
|
+
const pollSpinner = createSpinner("Waiting for authentication...");
|
|
788
|
+
pollSpinner.start();
|
|
789
|
+
let consecutiveErrors = 0;
|
|
790
|
+
try {
|
|
791
|
+
for (let attempts = 0; attempts < MAX_POLL_ATTEMPTS; attempts++) {
|
|
792
|
+
await sleep(POLL_INTERVAL_MS);
|
|
793
|
+
let pollResponse;
|
|
794
|
+
try {
|
|
795
|
+
pollResponse = await api.get("/api/cli/auth", {
|
|
796
|
+
action: "poll",
|
|
797
|
+
code: initResponse.code
|
|
798
|
+
});
|
|
799
|
+
consecutiveErrors = 0;
|
|
800
|
+
} catch {
|
|
801
|
+
consecutiveErrors++;
|
|
802
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
803
|
+
throw new Error(
|
|
804
|
+
"Too many consecutive network errors while polling. Please check your connection and try again."
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
if (pollResponse.status === "authenticated") {
|
|
810
|
+
pollSpinner.stop();
|
|
811
|
+
if (pollResponse.accessToken) {
|
|
812
|
+
setAccessToken(pollResponse.accessToken);
|
|
813
|
+
}
|
|
814
|
+
if (pollResponse.refreshToken) {
|
|
815
|
+
setRefreshToken(pollResponse.refreshToken);
|
|
816
|
+
}
|
|
817
|
+
if (pollResponse.user) {
|
|
818
|
+
setUser({
|
|
819
|
+
id: pollResponse.user.id,
|
|
820
|
+
email: pollResponse.user.email,
|
|
821
|
+
name: pollResponse.user.name
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
console.log();
|
|
825
|
+
success(`Logged in as ${chalk4.bold(pollResponse.user?.email)}`);
|
|
826
|
+
return { email: pollResponse.user?.email || "" };
|
|
827
|
+
}
|
|
828
|
+
if (pollResponse.status === "expired" || pollResponse.status === "not_found") {
|
|
829
|
+
throw new Error("Authentication code expired. Please try again.");
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
throw new Error("Authentication timed out. Please try again.");
|
|
833
|
+
} finally {
|
|
834
|
+
pollSpinner.stop();
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// src/commands/login.ts
|
|
839
|
+
var loginCommand = new Command("login").description("Authenticate with Envpilot").option("--api-url <url>", "API URL (default: http://localhost:3000)").option("--no-browser", "Do not automatically open the browser").action(async (options) => {
|
|
840
|
+
try {
|
|
841
|
+
if (options.apiUrl) {
|
|
842
|
+
setApiUrl(options.apiUrl);
|
|
843
|
+
}
|
|
844
|
+
await performLogin({
|
|
845
|
+
browser: options.browser !== false
|
|
846
|
+
});
|
|
847
|
+
console.log();
|
|
848
|
+
console.log("Next steps:");
|
|
849
|
+
info(" envpilot init Initialize a project in the current directory");
|
|
850
|
+
info(" envpilot list List your projects and organizations");
|
|
851
|
+
info(" envpilot sync Login, select project, and pull \u2014 all at once");
|
|
852
|
+
console.log();
|
|
853
|
+
} catch (err) {
|
|
854
|
+
await handleError(err);
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
// src/commands/init.ts
|
|
859
|
+
import { Command as Command2 } from "commander";
|
|
860
|
+
import chalk5 from "chalk";
|
|
861
|
+
import inquirer from "inquirer";
|
|
862
|
+
|
|
863
|
+
// src/lib/project-config.ts
|
|
864
|
+
import { readFileSync, writeFileSync, existsSync, unlinkSync } from "fs";
|
|
865
|
+
import { execSync } from "child_process";
|
|
866
|
+
import { join } from "path";
|
|
867
|
+
|
|
868
|
+
// src/types/index.ts
|
|
869
|
+
import { z } from "zod";
|
|
870
|
+
var userSchema = z.object({
|
|
871
|
+
id: z.string(),
|
|
872
|
+
email: z.string().email(),
|
|
873
|
+
name: z.string().optional()
|
|
874
|
+
});
|
|
875
|
+
var organizationSchema = z.object({
|
|
876
|
+
_id: z.string(),
|
|
877
|
+
name: z.string(),
|
|
878
|
+
slug: z.string(),
|
|
879
|
+
tier: z.enum(["free", "pro"]),
|
|
880
|
+
role: z.string().optional()
|
|
881
|
+
});
|
|
882
|
+
var projectSchema = z.object({
|
|
883
|
+
_id: z.string(),
|
|
884
|
+
name: z.string(),
|
|
885
|
+
slug: z.string(),
|
|
886
|
+
organizationId: z.string(),
|
|
887
|
+
description: z.string().optional(),
|
|
888
|
+
icon: z.string().optional(),
|
|
889
|
+
color: z.string().optional(),
|
|
890
|
+
userRole: z.string().nullable().optional(),
|
|
891
|
+
projectRole: z.string().nullable().optional()
|
|
892
|
+
});
|
|
893
|
+
var variableTagSchema = z.object({
|
|
894
|
+
_id: z.string(),
|
|
895
|
+
name: z.string(),
|
|
896
|
+
color: z.string()
|
|
897
|
+
});
|
|
898
|
+
var variableSchema = z.object({
|
|
899
|
+
_id: z.string(),
|
|
900
|
+
key: z.string(),
|
|
901
|
+
value: z.string(),
|
|
902
|
+
environment: z.enum(["development", "staging", "production"]),
|
|
903
|
+
projectId: z.string(),
|
|
904
|
+
description: z.string().optional(),
|
|
905
|
+
isSensitive: z.boolean().optional(),
|
|
906
|
+
version: z.number().optional(),
|
|
907
|
+
tags: z.array(variableTagSchema).optional()
|
|
908
|
+
});
|
|
909
|
+
var environmentSchema = z.enum([
|
|
910
|
+
"development",
|
|
911
|
+
"staging",
|
|
912
|
+
"production"
|
|
913
|
+
]);
|
|
914
|
+
var cliConfigSchema = z.object({
|
|
915
|
+
apiUrl: z.string().url(),
|
|
916
|
+
accessToken: z.string().optional(),
|
|
917
|
+
refreshToken: z.string().optional(),
|
|
918
|
+
activeProjectId: z.string().optional(),
|
|
919
|
+
activeOrganizationId: z.string().optional(),
|
|
920
|
+
user: userSchema.optional(),
|
|
921
|
+
role: z.enum(["admin", "team_lead", "member"]).optional()
|
|
922
|
+
});
|
|
923
|
+
var projectConfigSchema = z.object({
|
|
924
|
+
projectId: z.string(),
|
|
925
|
+
organizationId: z.string(),
|
|
926
|
+
environment: environmentSchema.default("development")
|
|
927
|
+
});
|
|
928
|
+
var projectEntrySchema = z.object({
|
|
929
|
+
projectId: z.string(),
|
|
930
|
+
organizationId: z.string(),
|
|
931
|
+
projectName: z.string().default(""),
|
|
932
|
+
organizationName: z.string().default(""),
|
|
933
|
+
environment: environmentSchema.default("development")
|
|
934
|
+
});
|
|
935
|
+
var projectConfigV2Schema = z.object({
|
|
936
|
+
version: z.literal(1),
|
|
937
|
+
activeProjectId: z.string(),
|
|
938
|
+
projects: z.array(projectEntrySchema).min(1)
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
// src/lib/project-config.ts
|
|
942
|
+
var CONFIG_FILE_NAME = ".envpilot";
|
|
943
|
+
function getProjectConfigPath(directory = process.cwd()) {
|
|
944
|
+
return join(directory, CONFIG_FILE_NAME);
|
|
945
|
+
}
|
|
946
|
+
function hasProjectConfig(directory = process.cwd()) {
|
|
947
|
+
return existsSync(getProjectConfigPath(directory));
|
|
948
|
+
}
|
|
949
|
+
function readRawConfig(directory = process.cwd()) {
|
|
950
|
+
const configPath = getProjectConfigPath(directory);
|
|
951
|
+
if (!existsSync(configPath)) return null;
|
|
952
|
+
try {
|
|
953
|
+
return JSON.parse(readFileSync(configPath, "utf-8"));
|
|
954
|
+
} catch {
|
|
955
|
+
return null;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
function migrateV1toV2(v1) {
|
|
959
|
+
return {
|
|
960
|
+
version: 1,
|
|
961
|
+
activeProjectId: v1.projectId,
|
|
962
|
+
projects: [
|
|
963
|
+
{
|
|
964
|
+
projectId: v1.projectId,
|
|
965
|
+
organizationId: v1.organizationId,
|
|
966
|
+
projectName: "",
|
|
967
|
+
organizationName: "",
|
|
968
|
+
environment: v1.environment
|
|
969
|
+
}
|
|
970
|
+
]
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
function readProjectConfigV2(directory = process.cwd()) {
|
|
974
|
+
const raw = readRawConfig(directory);
|
|
975
|
+
if (!raw || typeof raw !== "object") return null;
|
|
976
|
+
const rawVersion = raw.version;
|
|
977
|
+
if (rawVersion === 1 || rawVersion === 2) {
|
|
978
|
+
try {
|
|
979
|
+
const normalized = { ...raw, version: 1 };
|
|
980
|
+
const parsed = projectConfigV2Schema.parse(normalized);
|
|
981
|
+
if (rawVersion === 2) {
|
|
982
|
+
writeProjectConfigV2(parsed, directory);
|
|
983
|
+
}
|
|
984
|
+
return parsed;
|
|
985
|
+
} catch {
|
|
986
|
+
return null;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
try {
|
|
990
|
+
const v1 = projectConfigSchema.parse(raw);
|
|
991
|
+
const v2 = migrateV1toV2(v1);
|
|
992
|
+
writeProjectConfigV2(v2, directory);
|
|
993
|
+
return v2;
|
|
994
|
+
} catch {
|
|
995
|
+
return null;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
function writeProjectConfigV2(config2, directory = process.cwd()) {
|
|
999
|
+
const configPath = getProjectConfigPath(directory);
|
|
1000
|
+
writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
|
|
1001
|
+
}
|
|
1002
|
+
function getActiveProject(config2) {
|
|
1003
|
+
return config2.projects.find((p) => p.projectId === config2.activeProjectId) || config2.projects[0] || null;
|
|
1004
|
+
}
|
|
1005
|
+
function resolveProject(config2, identifier) {
|
|
1006
|
+
if (!identifier) return getActiveProject(config2);
|
|
1007
|
+
return config2.projects.find(
|
|
1008
|
+
(p) => p.projectId === identifier || p.projectName.toLowerCase() === identifier.toLowerCase()
|
|
1009
|
+
) || null;
|
|
1010
|
+
}
|
|
1011
|
+
function addProjectToConfig(config2, entry) {
|
|
1012
|
+
if (config2.projects.some((p) => p.projectId === entry.projectId)) {
|
|
1013
|
+
throw new Error("Project already linked");
|
|
1014
|
+
}
|
|
1015
|
+
return { ...config2, projects: [...config2.projects, entry] };
|
|
1016
|
+
}
|
|
1017
|
+
function removeProjectFromConfig(config2, projectId) {
|
|
1018
|
+
const filtered = config2.projects.filter((p) => p.projectId !== projectId);
|
|
1019
|
+
if (filtered.length === 0) return null;
|
|
1020
|
+
const activeId = config2.activeProjectId === projectId ? filtered[0].projectId : config2.activeProjectId;
|
|
1021
|
+
return { ...config2, activeProjectId: activeId, projects: filtered };
|
|
1022
|
+
}
|
|
1023
|
+
function setActiveProjectInConfig(config2, projectId) {
|
|
1024
|
+
if (!config2.projects.some((p) => p.projectId === projectId)) {
|
|
1025
|
+
throw new Error("Project not found in config");
|
|
1026
|
+
}
|
|
1027
|
+
return { ...config2, activeProjectId: projectId };
|
|
1028
|
+
}
|
|
1029
|
+
function updateProjectInConfig(config2, projectId, updates) {
|
|
1030
|
+
return {
|
|
1031
|
+
...config2,
|
|
1032
|
+
projects: config2.projects.map(
|
|
1033
|
+
(p) => p.projectId === projectId ? { ...p, ...updates } : p
|
|
1034
|
+
)
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
function readProjectConfig(directory = process.cwd()) {
|
|
1038
|
+
const v2 = readProjectConfigV2(directory);
|
|
1039
|
+
if (!v2) return null;
|
|
1040
|
+
const active = getActiveProject(v2);
|
|
1041
|
+
if (!active) return null;
|
|
1042
|
+
return {
|
|
1043
|
+
projectId: active.projectId,
|
|
1044
|
+
organizationId: active.organizationId,
|
|
1045
|
+
environment: active.environment
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
function writeProjectConfig(config2, directory = process.cwd()) {
|
|
1049
|
+
const existing = readProjectConfigV2(directory);
|
|
1050
|
+
if (existing) {
|
|
1051
|
+
const updated = updateProjectInConfig(existing, existing.activeProjectId, {
|
|
1052
|
+
projectId: config2.projectId,
|
|
1053
|
+
organizationId: config2.organizationId,
|
|
1054
|
+
environment: config2.environment
|
|
1055
|
+
});
|
|
1056
|
+
writeProjectConfigV2(
|
|
1057
|
+
{ ...updated, activeProjectId: config2.projectId },
|
|
1058
|
+
directory
|
|
1059
|
+
);
|
|
1060
|
+
} else {
|
|
1061
|
+
writeProjectConfigV2(
|
|
1062
|
+
{
|
|
1063
|
+
version: 1,
|
|
1064
|
+
activeProjectId: config2.projectId,
|
|
1065
|
+
projects: [
|
|
1066
|
+
{
|
|
1067
|
+
projectId: config2.projectId,
|
|
1068
|
+
organizationId: config2.organizationId,
|
|
1069
|
+
projectName: "",
|
|
1070
|
+
organizationName: "",
|
|
1071
|
+
environment: config2.environment
|
|
1072
|
+
}
|
|
1073
|
+
]
|
|
1074
|
+
},
|
|
1075
|
+
directory
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
function updateProjectConfig(updates, directory = process.cwd()) {
|
|
1080
|
+
const v2 = readProjectConfigV2(directory);
|
|
1081
|
+
if (!v2) {
|
|
1082
|
+
throw new Error("No project config found. Run `envpilot init` first.");
|
|
1083
|
+
}
|
|
1084
|
+
const active = getActiveProject(v2);
|
|
1085
|
+
if (!active) {
|
|
1086
|
+
throw new Error("No active project found.");
|
|
1087
|
+
}
|
|
1088
|
+
const updated = updateProjectInConfig(v2, active.projectId, updates);
|
|
1089
|
+
writeProjectConfigV2(updated, directory);
|
|
1090
|
+
}
|
|
1091
|
+
function deleteProjectConfig(directory = process.cwd()) {
|
|
1092
|
+
const configPath = getProjectConfigPath(directory);
|
|
1093
|
+
if (!existsSync(configPath)) return false;
|
|
1094
|
+
unlinkSync(configPath);
|
|
1095
|
+
return true;
|
|
1096
|
+
}
|
|
1097
|
+
function ensureEnvInGitignore(directory = process.cwd()) {
|
|
1098
|
+
const gitignorePath = join(directory, ".gitignore");
|
|
1099
|
+
if (!existsSync(gitignorePath)) {
|
|
1100
|
+
writeFileSync(gitignorePath, ".env\n.env.local\n", "utf-8");
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
const content = readFileSync(gitignorePath, "utf-8");
|
|
1104
|
+
const lines = content.split("\n");
|
|
1105
|
+
if (lines.some((line2) => line2.trim() === ".env")) {
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
const newContent = content.endsWith("\n") ? content + ".env\n" : content + "\n.env\n";
|
|
1109
|
+
writeFileSync(gitignorePath, newContent, "utf-8");
|
|
1110
|
+
}
|
|
1111
|
+
function getTrackedEnvFiles(directory = process.cwd()) {
|
|
1112
|
+
try {
|
|
1113
|
+
const result = execSync("git ls-files --cached .env .env.* .env.local", {
|
|
1114
|
+
cwd: directory,
|
|
1115
|
+
encoding: "utf-8",
|
|
1116
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1117
|
+
});
|
|
1118
|
+
return result.trim().split("\n").filter((f) => f.length > 0);
|
|
1119
|
+
} catch {
|
|
1120
|
+
return [];
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// src/lib/env-file.ts
|
|
1125
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, chmodSync } from "fs";
|
|
1126
|
+
import { join as join2 } from "path";
|
|
1127
|
+
function parseEnvFile(content) {
|
|
1128
|
+
const result = {};
|
|
1129
|
+
const lines = content.split("\n");
|
|
1130
|
+
for (const line2 of lines) {
|
|
1131
|
+
const trimmed = line2.trim();
|
|
1132
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
1133
|
+
continue;
|
|
1134
|
+
}
|
|
1135
|
+
const equalsIndex = line2.indexOf("=");
|
|
1136
|
+
if (equalsIndex === -1) {
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1139
|
+
const key = line2.substring(0, equalsIndex).trim();
|
|
1140
|
+
let value = line2.substring(equalsIndex + 1);
|
|
1141
|
+
value = parseValue(value);
|
|
1142
|
+
if (isValidEnvKey(key)) {
|
|
1143
|
+
result[key] = value;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
return result;
|
|
1147
|
+
}
|
|
1148
|
+
function parseValue(value) {
|
|
1149
|
+
value = value.trim();
|
|
1150
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
1151
|
+
value = value.slice(1, -1);
|
|
1152
|
+
value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
1153
|
+
} else if (value.startsWith("'") && value.endsWith("'")) {
|
|
1154
|
+
value = value.slice(1, -1);
|
|
1155
|
+
} else {
|
|
1156
|
+
const commentIndex = value.indexOf(" #");
|
|
1157
|
+
if (commentIndex !== -1) {
|
|
1158
|
+
value = value.substring(0, commentIndex).trim();
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
return value;
|
|
1162
|
+
}
|
|
1163
|
+
function isValidEnvKey(key) {
|
|
1164
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(key);
|
|
1165
|
+
}
|
|
1166
|
+
function stringifyEnv(vars, options) {
|
|
1167
|
+
let keys = Object.keys(vars);
|
|
1168
|
+
if (options?.sort) {
|
|
1169
|
+
keys = keys.sort();
|
|
1170
|
+
}
|
|
1171
|
+
const lines = [];
|
|
1172
|
+
for (const key of keys) {
|
|
1173
|
+
const value = vars[key];
|
|
1174
|
+
if (options?.comments?.[key]) {
|
|
1175
|
+
lines.push(`# ${options.comments[key]}`);
|
|
1176
|
+
}
|
|
1177
|
+
const formattedValue = formatValue(value);
|
|
1178
|
+
lines.push(`${key}=${formattedValue}`);
|
|
1179
|
+
}
|
|
1180
|
+
return lines.join("\n") + "\n";
|
|
1181
|
+
}
|
|
1182
|
+
function formatValue(value) {
|
|
1183
|
+
const needsQuotes = value.includes("\n") || value.includes("\r") || value.includes('"') || value.includes("'") || value.includes(" ") || value.includes("#") || value.startsWith(" ") || value.endsWith(" ");
|
|
1184
|
+
if (!needsQuotes) {
|
|
1185
|
+
return value;
|
|
1186
|
+
}
|
|
1187
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
|
1188
|
+
return `"${escaped}"`;
|
|
1189
|
+
}
|
|
1190
|
+
function diffEnvVars(local, remote) {
|
|
1191
|
+
const added = {};
|
|
1192
|
+
const removed = {};
|
|
1193
|
+
const changed = {};
|
|
1194
|
+
const unchanged = [];
|
|
1195
|
+
for (const [key, value] of Object.entries(local)) {
|
|
1196
|
+
if (!(key in remote)) {
|
|
1197
|
+
added[key] = value;
|
|
1198
|
+
} else if (remote[key] !== value) {
|
|
1199
|
+
changed[key] = { local: value, remote: remote[key] };
|
|
1200
|
+
} else {
|
|
1201
|
+
unchanged.push(key);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
for (const [key, value] of Object.entries(remote)) {
|
|
1205
|
+
if (!(key in local)) {
|
|
1206
|
+
removed[key] = value;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
return { added, removed, changed, unchanged };
|
|
1210
|
+
}
|
|
1211
|
+
function readEnvFile(filePath) {
|
|
1212
|
+
if (!existsSync2(filePath)) {
|
|
1213
|
+
return null;
|
|
1214
|
+
}
|
|
1215
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
1216
|
+
return parseEnvFile(content);
|
|
1217
|
+
}
|
|
1218
|
+
function writeEnvFile(filePath, vars, options) {
|
|
1219
|
+
const content = stringifyEnv(vars, options);
|
|
1220
|
+
writeFileSync2(filePath, content, "utf-8");
|
|
1221
|
+
}
|
|
1222
|
+
function getEnvPathForEnvironment(environment, directory = process.cwd()) {
|
|
1223
|
+
if (environment === "development") {
|
|
1224
|
+
return join2(directory, ".env.local");
|
|
1225
|
+
}
|
|
1226
|
+
return join2(directory, `.env.${environment}`);
|
|
1227
|
+
}
|
|
1228
|
+
function applyFileProtection(filePath, role, projectRole) {
|
|
1229
|
+
if (!existsSync2(filePath)) return;
|
|
1230
|
+
const isWritable = role === "admin" || role === "team_lead" || projectRole === "manager";
|
|
1231
|
+
if (isWritable) {
|
|
1232
|
+
chmodSync(filePath, 420);
|
|
1233
|
+
} else {
|
|
1234
|
+
chmodSync(filePath, 292);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// src/commands/init.ts
|
|
1239
|
+
var initCommand = new Command2("init").description("Initialize Envpilot in the current directory").option("-o, --organization <id>", "Organization ID").option("-p, --project <id>", "Project ID").option(
|
|
1240
|
+
"-e, --environment <env>",
|
|
1241
|
+
"Default environment (development, staging, production)"
|
|
1242
|
+
).option("-f, --force", "Overwrite existing configuration").option("--add", "Add another project to existing config").action(async (options) => {
|
|
1243
|
+
try {
|
|
1244
|
+
if (!isAuthenticated()) {
|
|
1245
|
+
throw notAuthenticated();
|
|
1246
|
+
}
|
|
1247
|
+
const existingConfig = readProjectConfigV2();
|
|
1248
|
+
if (options.add) {
|
|
1249
|
+
if (!existingConfig) {
|
|
1250
|
+
error("No existing config. Run `envpilot init` first.");
|
|
1251
|
+
process.exit(1);
|
|
1252
|
+
}
|
|
1253
|
+
await addProject(existingConfig, options);
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
if (hasProjectConfig() && !options.force) {
|
|
1257
|
+
warning("This directory is already initialized with Envpilot.");
|
|
1258
|
+
const { proceed } = await inquirer.prompt([
|
|
1259
|
+
{
|
|
1260
|
+
type: "confirm",
|
|
1261
|
+
name: "proceed",
|
|
1262
|
+
message: "Do you want to reinitialize?",
|
|
1263
|
+
default: false
|
|
1264
|
+
}
|
|
1265
|
+
]);
|
|
1266
|
+
if (!proceed) {
|
|
1267
|
+
info("Initialization cancelled.");
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
const { selectedOrg, selectedProject, selectedEnvironment } = await selectOrgProjectEnv(options);
|
|
1272
|
+
writeProjectConfigV2({
|
|
1273
|
+
version: 1,
|
|
1274
|
+
activeProjectId: selectedProject._id,
|
|
1275
|
+
projects: [
|
|
1276
|
+
{
|
|
1277
|
+
projectId: selectedProject._id,
|
|
1278
|
+
organizationId: selectedOrg._id,
|
|
1279
|
+
projectName: selectedProject.name,
|
|
1280
|
+
organizationName: selectedOrg.name,
|
|
1281
|
+
environment: selectedEnvironment
|
|
1282
|
+
}
|
|
1283
|
+
]
|
|
1284
|
+
});
|
|
1285
|
+
setActiveOrganizationId(selectedOrg._id);
|
|
1286
|
+
setActiveProjectId(selectedProject._id);
|
|
1287
|
+
if (selectedOrg.role) {
|
|
1288
|
+
setRole(selectedOrg.role);
|
|
1289
|
+
}
|
|
1290
|
+
ensureEnvInGitignore();
|
|
1291
|
+
warnTrackedFiles();
|
|
1292
|
+
console.log();
|
|
1293
|
+
success("Project initialized!");
|
|
1294
|
+
printPostInit(selectedOrg, selectedProject);
|
|
1295
|
+
} catch (err) {
|
|
1296
|
+
await handleError(err);
|
|
1297
|
+
}
|
|
1298
|
+
});
|
|
1299
|
+
async function addProject(existingConfig, options) {
|
|
1300
|
+
const api = createAPIClient();
|
|
1301
|
+
let role = getRole();
|
|
1302
|
+
if (role !== "admin" && role !== "team_lead") {
|
|
1303
|
+
const orgs = await withSpinner("Checking permissions...", async () => {
|
|
1304
|
+
const response = await api.get("/api/cli/organizations");
|
|
1305
|
+
return response.data || [];
|
|
1306
|
+
});
|
|
1307
|
+
const freshRole = orgs.find(
|
|
1308
|
+
(o) => o._id === existingConfig.projects[0]?.organizationId
|
|
1309
|
+
)?.role;
|
|
1310
|
+
if (freshRole) {
|
|
1311
|
+
setRole(freshRole);
|
|
1312
|
+
role = freshRole;
|
|
1313
|
+
}
|
|
1314
|
+
if (role !== "admin" && role !== "team_lead") {
|
|
1315
|
+
error("Only admins and team leads can link multiple projects.");
|
|
1316
|
+
info("Unlink the current project first with `envpilot unlink`.");
|
|
1317
|
+
process.exit(1);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
const { selectedOrg, selectedProject, selectedEnvironment } = await selectOrgProjectEnv(options);
|
|
1321
|
+
if (existingConfig.projects.some((p) => p.projectId === selectedProject._id)) {
|
|
1322
|
+
error(`"${selectedProject.name}" is already linked.`);
|
|
1323
|
+
process.exit(1);
|
|
1324
|
+
}
|
|
1325
|
+
const envFile = getEnvPathForEnvironment(selectedEnvironment);
|
|
1326
|
+
const conflicting = existingConfig.projects.find(
|
|
1327
|
+
(p) => p.environment === selectedEnvironment
|
|
1328
|
+
);
|
|
1329
|
+
if (conflicting) {
|
|
1330
|
+
warning(
|
|
1331
|
+
`"${conflicting.projectName || conflicting.projectId}" already syncs to ${envFile} (${selectedEnvironment}). Both projects will write to the same file \u2014 consider using a different environment.`
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
const newEntry = {
|
|
1335
|
+
projectId: selectedProject._id,
|
|
1336
|
+
organizationId: selectedOrg._id,
|
|
1337
|
+
projectName: selectedProject.name,
|
|
1338
|
+
organizationName: selectedOrg.name,
|
|
1339
|
+
environment: selectedEnvironment
|
|
1340
|
+
};
|
|
1341
|
+
let updatedConfig = addProjectToConfig(existingConfig, newEntry);
|
|
1342
|
+
const { setActive } = await inquirer.prompt([
|
|
1343
|
+
{
|
|
1344
|
+
type: "confirm",
|
|
1345
|
+
name: "setActive",
|
|
1346
|
+
message: `Set "${selectedProject.name}" as the active project?`,
|
|
1347
|
+
default: false
|
|
1348
|
+
}
|
|
1349
|
+
]);
|
|
1350
|
+
if (setActive) {
|
|
1351
|
+
updatedConfig = {
|
|
1352
|
+
...updatedConfig,
|
|
1353
|
+
activeProjectId: selectedProject._id
|
|
1354
|
+
};
|
|
1355
|
+
setActiveProjectId(selectedProject._id);
|
|
1356
|
+
setActiveOrganizationId(selectedOrg._id);
|
|
1357
|
+
}
|
|
1358
|
+
updatedConfig = backfillNames(updatedConfig);
|
|
1359
|
+
writeProjectConfigV2(updatedConfig);
|
|
1360
|
+
console.log();
|
|
1361
|
+
success(`Added "${selectedProject.name}" to linked projects!`);
|
|
1362
|
+
console.log(
|
|
1363
|
+
chalk5.dim(` ${existingConfig.projects.length + 1} projects now linked`)
|
|
1364
|
+
);
|
|
1365
|
+
console.log();
|
|
1366
|
+
console.log("Next steps:");
|
|
1367
|
+
console.log(
|
|
1368
|
+
` ${chalk5.cyan("envpilot pull --all")} Pull all projects`
|
|
1369
|
+
);
|
|
1370
|
+
console.log(
|
|
1371
|
+
` ${chalk5.cyan(`envpilot pull --project "${selectedProject.name}"`)} Pull this project`
|
|
1372
|
+
);
|
|
1373
|
+
console.log(
|
|
1374
|
+
` ${chalk5.cyan("envpilot list linked")} See all linked projects`
|
|
1375
|
+
);
|
|
1376
|
+
console.log();
|
|
1377
|
+
}
|
|
1378
|
+
async function selectOrgProjectEnv(options) {
|
|
1379
|
+
const api = createAPIClient();
|
|
1380
|
+
const organizations = await withSpinner(
|
|
1381
|
+
"Fetching organizations...",
|
|
1382
|
+
async () => {
|
|
1383
|
+
const response = await api.get("/api/cli/organizations");
|
|
1384
|
+
return response.data || [];
|
|
1385
|
+
}
|
|
1386
|
+
);
|
|
1387
|
+
if (organizations.length === 0) {
|
|
1388
|
+
error("No organizations found. Please create an organization first.");
|
|
1389
|
+
process.exit(1);
|
|
1390
|
+
}
|
|
1391
|
+
let selectedOrg;
|
|
1392
|
+
if (options.organization) {
|
|
1393
|
+
const org = organizations.find(
|
|
1394
|
+
(o) => o._id === options.organization || o.slug === options.organization
|
|
1395
|
+
);
|
|
1396
|
+
if (!org) {
|
|
1397
|
+
error(`Organization not found: ${options.organization}`);
|
|
1398
|
+
process.exit(1);
|
|
1399
|
+
}
|
|
1400
|
+
selectedOrg = org;
|
|
1401
|
+
} else if (organizations.length === 1) {
|
|
1402
|
+
selectedOrg = organizations[0];
|
|
1403
|
+
info(`Using organization: ${chalk5.bold(selectedOrg.name)}`);
|
|
1404
|
+
} else {
|
|
1405
|
+
const { orgId } = await inquirer.prompt([
|
|
1406
|
+
{
|
|
1407
|
+
type: "list",
|
|
1408
|
+
name: "orgId",
|
|
1409
|
+
message: "Select an organization:",
|
|
1410
|
+
choices: organizations.map((org) => ({
|
|
1411
|
+
name: `${org.name} ${org.tier === "pro" ? chalk5.green("(Pro)") : chalk5.dim("(Free)")}`,
|
|
1412
|
+
value: org._id
|
|
1413
|
+
}))
|
|
1414
|
+
}
|
|
1415
|
+
]);
|
|
1416
|
+
selectedOrg = organizations.find((o) => o._id === orgId);
|
|
1417
|
+
}
|
|
1418
|
+
const projects = await withSpinner("Fetching projects...", async () => {
|
|
1419
|
+
const response = await api.get(
|
|
1420
|
+
"/api/cli/projects",
|
|
1421
|
+
{ organizationId: selectedOrg._id }
|
|
1422
|
+
);
|
|
1423
|
+
return response.data || [];
|
|
1424
|
+
});
|
|
1425
|
+
if (projects.length === 0) {
|
|
1426
|
+
error("No projects found. Please create a project first.");
|
|
1427
|
+
process.exit(1);
|
|
1428
|
+
}
|
|
1429
|
+
let selectedProject;
|
|
1430
|
+
if (options.project) {
|
|
1431
|
+
const project = projects.find(
|
|
1432
|
+
(p) => p._id === options.project || p.slug === options.project
|
|
1433
|
+
);
|
|
1434
|
+
if (!project) {
|
|
1435
|
+
error(`Project not found: ${options.project}`);
|
|
1436
|
+
process.exit(1);
|
|
1437
|
+
}
|
|
1438
|
+
selectedProject = project;
|
|
1439
|
+
} else if (projects.length === 1) {
|
|
1440
|
+
selectedProject = projects[0];
|
|
1441
|
+
info(`Using project: ${chalk5.bold(selectedProject.name)}`);
|
|
1442
|
+
} else {
|
|
1443
|
+
const { projectId } = await inquirer.prompt([
|
|
1444
|
+
{
|
|
1445
|
+
type: "list",
|
|
1446
|
+
name: "projectId",
|
|
1447
|
+
message: "Select a project:",
|
|
1448
|
+
choices: projects.map((project) => ({
|
|
1449
|
+
name: `${project.icon || "\u{1F4E6}"} ${project.name}`,
|
|
1450
|
+
value: project._id
|
|
1451
|
+
}))
|
|
1452
|
+
}
|
|
1453
|
+
]);
|
|
1454
|
+
selectedProject = projects.find((p) => p._id === projectId);
|
|
1455
|
+
}
|
|
1456
|
+
let selectedEnvironment = "development";
|
|
1457
|
+
if (options.environment) {
|
|
1458
|
+
if (!["development", "staging", "production"].includes(options.environment)) {
|
|
1459
|
+
error(
|
|
1460
|
+
"Invalid environment. Must be: development, staging, or production"
|
|
1461
|
+
);
|
|
1462
|
+
process.exit(1);
|
|
1463
|
+
}
|
|
1464
|
+
selectedEnvironment = options.environment;
|
|
1465
|
+
} else {
|
|
1466
|
+
const { environment } = await inquirer.prompt([
|
|
1467
|
+
{
|
|
1468
|
+
type: "list",
|
|
1469
|
+
name: "environment",
|
|
1470
|
+
message: "Select default environment:",
|
|
1471
|
+
choices: [
|
|
1472
|
+
{ name: "Development", value: "development" },
|
|
1473
|
+
{ name: "Staging", value: "staging" },
|
|
1474
|
+
{ name: "Production", value: "production" }
|
|
1475
|
+
],
|
|
1476
|
+
default: "development"
|
|
1477
|
+
}
|
|
1478
|
+
]);
|
|
1479
|
+
selectedEnvironment = environment;
|
|
1480
|
+
}
|
|
1481
|
+
return { selectedOrg, selectedProject, selectedEnvironment };
|
|
1482
|
+
}
|
|
1483
|
+
function backfillNames(config2) {
|
|
1484
|
+
return config2;
|
|
1485
|
+
}
|
|
1486
|
+
function warnTrackedFiles() {
|
|
1487
|
+
const trackedFiles = getTrackedEnvFiles();
|
|
1488
|
+
if (trackedFiles.length > 0) {
|
|
1489
|
+
console.log();
|
|
1490
|
+
warning("Security risk: .env files are tracked by git!");
|
|
1491
|
+
for (const file of trackedFiles) {
|
|
1492
|
+
console.log(chalk5.red(` tracked: ${file}`));
|
|
1493
|
+
}
|
|
1494
|
+
console.log();
|
|
1495
|
+
console.log(
|
|
1496
|
+
chalk5.yellow(
|
|
1497
|
+
" Run the following to untrack them (without deleting the files):"
|
|
1498
|
+
)
|
|
1499
|
+
);
|
|
1500
|
+
for (const file of trackedFiles) {
|
|
1501
|
+
console.log(chalk5.cyan(` git rm --cached ${file}`));
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
function printPostInit(selectedOrg, selectedProject) {
|
|
1506
|
+
console.log();
|
|
1507
|
+
console.log(chalk5.dim("Configuration saved to .envpilot"));
|
|
1508
|
+
if (selectedOrg.role) {
|
|
1509
|
+
console.log(chalk5.dim(` Org role: ${formatRole(selectedOrg.role)}`));
|
|
1510
|
+
roleNotice(selectedOrg.role);
|
|
1511
|
+
}
|
|
1512
|
+
if (selectedProject.projectRole) {
|
|
1513
|
+
console.log(
|
|
1514
|
+
chalk5.dim(
|
|
1515
|
+
` Project role: ${formatProjectRole(selectedProject.projectRole)}`
|
|
1516
|
+
)
|
|
1517
|
+
);
|
|
1518
|
+
projectRoleNotice(selectedProject.projectRole);
|
|
1519
|
+
}
|
|
1520
|
+
console.log();
|
|
1521
|
+
console.log("Next steps:");
|
|
1522
|
+
console.log(
|
|
1523
|
+
` ${chalk5.cyan("envpilot pull")} Download environment variables`
|
|
1524
|
+
);
|
|
1525
|
+
console.log(
|
|
1526
|
+
` ${chalk5.cyan("envpilot push")} Upload local .env to cloud`
|
|
1527
|
+
);
|
|
1528
|
+
console.log();
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
// src/commands/pull.ts
|
|
1532
|
+
import { Command as Command3 } from "commander";
|
|
1533
|
+
import chalk6 from "chalk";
|
|
1534
|
+
import inquirer2 from "inquirer";
|
|
1535
|
+
|
|
1536
|
+
// src/lib/format-converter.ts
|
|
1537
|
+
var ALL_FORMATS = [
|
|
1538
|
+
"env",
|
|
1539
|
+
"json",
|
|
1540
|
+
"yaml",
|
|
1541
|
+
"docker-compose",
|
|
1542
|
+
"aws",
|
|
1543
|
+
"vercel",
|
|
1544
|
+
"netlify"
|
|
1545
|
+
];
|
|
1546
|
+
function serialize(vars, format, meta) {
|
|
1547
|
+
switch (format) {
|
|
1548
|
+
case "env":
|
|
1549
|
+
return serializeEnv(vars, meta);
|
|
1550
|
+
case "json":
|
|
1551
|
+
return serializeJson(vars);
|
|
1552
|
+
case "yaml":
|
|
1553
|
+
return serializeYaml(vars, meta);
|
|
1554
|
+
case "docker-compose":
|
|
1555
|
+
return serializeDockerCompose(vars, meta);
|
|
1556
|
+
case "aws":
|
|
1557
|
+
return serializeAws(vars, meta);
|
|
1558
|
+
case "vercel":
|
|
1559
|
+
return serializeVercel(vars);
|
|
1560
|
+
case "netlify":
|
|
1561
|
+
return serializeNetlify(vars, meta);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
function parse(content, format, options) {
|
|
1565
|
+
switch (format) {
|
|
1566
|
+
case "env":
|
|
1567
|
+
return parseEnvFile(content);
|
|
1568
|
+
case "json":
|
|
1569
|
+
return parseJson(content);
|
|
1570
|
+
case "yaml":
|
|
1571
|
+
return parseYaml(content);
|
|
1572
|
+
case "docker-compose":
|
|
1573
|
+
return parseDockerCompose(content);
|
|
1574
|
+
case "aws":
|
|
1575
|
+
return parseAws(content, options?.prefix);
|
|
1576
|
+
case "vercel":
|
|
1577
|
+
return parseVercel(content);
|
|
1578
|
+
case "netlify":
|
|
1579
|
+
return parseNetlify(content);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
function getDefaultFilename(format, environment) {
|
|
1583
|
+
const env = environment || "development";
|
|
1584
|
+
switch (format) {
|
|
1585
|
+
case "env":
|
|
1586
|
+
return env === "development" ? ".env.local" : `.env.${env}`;
|
|
1587
|
+
case "json":
|
|
1588
|
+
return `${env}.json`;
|
|
1589
|
+
case "yaml":
|
|
1590
|
+
return `${env}.yaml`;
|
|
1591
|
+
case "docker-compose":
|
|
1592
|
+
return "docker-compose.yml";
|
|
1593
|
+
case "aws":
|
|
1594
|
+
return `${env}.aws.json`;
|
|
1595
|
+
case "vercel":
|
|
1596
|
+
return `${env}.vercel.json`;
|
|
1597
|
+
case "netlify":
|
|
1598
|
+
return "netlify.toml";
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
function serializeEnv(vars, meta) {
|
|
1602
|
+
const lines = [];
|
|
1603
|
+
if (meta?.environment) {
|
|
1604
|
+
lines.push(`# Environment: ${meta.environment}`);
|
|
1605
|
+
}
|
|
1606
|
+
if (meta?.projectName) {
|
|
1607
|
+
lines.push(`# Project: ${meta.projectName}`);
|
|
1608
|
+
}
|
|
1609
|
+
if (lines.length > 0) {
|
|
1610
|
+
lines.push(`# Exported: ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
1611
|
+
lines.push("");
|
|
1612
|
+
}
|
|
1613
|
+
return lines.join("\n") + stringifyEnv(vars, { sort: true });
|
|
1614
|
+
}
|
|
1615
|
+
function serializeJson(vars) {
|
|
1616
|
+
const sorted = Object.keys(vars).sort().reduce(
|
|
1617
|
+
(obj, key) => {
|
|
1618
|
+
obj[key] = vars[key];
|
|
1619
|
+
return obj;
|
|
1620
|
+
},
|
|
1621
|
+
{}
|
|
1622
|
+
);
|
|
1623
|
+
return JSON.stringify(sorted, null, 2) + "\n";
|
|
1624
|
+
}
|
|
1625
|
+
function parseJson(content) {
|
|
1626
|
+
const data = JSON.parse(content);
|
|
1627
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
1628
|
+
throw new Error("Expected a JSON object with string key-value pairs");
|
|
1629
|
+
}
|
|
1630
|
+
const result = {};
|
|
1631
|
+
for (const [key, value] of Object.entries(data)) {
|
|
1632
|
+
if (typeof value === "string") {
|
|
1633
|
+
result[key] = value;
|
|
1634
|
+
} else if (value !== null && value !== void 0) {
|
|
1635
|
+
result[key] = String(value);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
return result;
|
|
1639
|
+
}
|
|
1640
|
+
function yamlQuote(value) {
|
|
1641
|
+
if (value === "" || value === "true" || value === "false" || value === "null" || value === "~" || /^[0-9]/.test(value) || /[:#\[\]{}&*!|>'"%@`]/.test(value) || value.includes("\n") || value.startsWith(" ") || value.endsWith(" ")) {
|
|
1642
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
|
1643
|
+
return `"${escaped}"`;
|
|
1644
|
+
}
|
|
1645
|
+
return value;
|
|
1646
|
+
}
|
|
1647
|
+
function serializeYaml(vars, meta) {
|
|
1648
|
+
const lines = [];
|
|
1649
|
+
if (meta?.environment) {
|
|
1650
|
+
lines.push(`# Environment: ${meta.environment}`);
|
|
1651
|
+
}
|
|
1652
|
+
if (meta?.projectName) {
|
|
1653
|
+
lines.push(`# Project: ${meta.projectName}`);
|
|
1654
|
+
}
|
|
1655
|
+
if (lines.length > 0) {
|
|
1656
|
+
lines.push("");
|
|
1657
|
+
}
|
|
1658
|
+
for (const key of Object.keys(vars).sort()) {
|
|
1659
|
+
lines.push(`${key}: ${yamlQuote(vars[key])}`);
|
|
1660
|
+
}
|
|
1661
|
+
return lines.join("\n") + "\n";
|
|
1662
|
+
}
|
|
1663
|
+
function parseYaml(content) {
|
|
1664
|
+
const result = {};
|
|
1665
|
+
for (const line2 of content.split("\n")) {
|
|
1666
|
+
const trimmed = line2.trim();
|
|
1667
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1668
|
+
const colonIndex = trimmed.indexOf(": ");
|
|
1669
|
+
if (colonIndex === -1) {
|
|
1670
|
+
if (trimmed.endsWith(":")) {
|
|
1671
|
+
const key2 = trimmed.slice(0, -1).trim();
|
|
1672
|
+
if (key2 && /^[A-Za-z_][A-Za-z0-9_]*$/.test(key2)) {
|
|
1673
|
+
result[key2] = "";
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
continue;
|
|
1677
|
+
}
|
|
1678
|
+
const key = trimmed.substring(0, colonIndex).trim();
|
|
1679
|
+
let value = trimmed.substring(colonIndex + 2).trim();
|
|
1680
|
+
if (line2.startsWith(" ") || line2.startsWith(" ")) continue;
|
|
1681
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
1682
|
+
value = value.slice(1, -1);
|
|
1683
|
+
if (value.includes("\\")) {
|
|
1684
|
+
value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
1688
|
+
result[key] = value;
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
return result;
|
|
1692
|
+
}
|
|
1693
|
+
function serializeDockerCompose(vars, meta) {
|
|
1694
|
+
const lines = [];
|
|
1695
|
+
if (meta?.environment || meta?.projectName) {
|
|
1696
|
+
lines.push("# Docker Compose environment variables");
|
|
1697
|
+
if (meta.projectName) lines.push(`# Project: ${meta.projectName}`);
|
|
1698
|
+
if (meta.environment) lines.push(`# Environment: ${meta.environment}`);
|
|
1699
|
+
lines.push("");
|
|
1700
|
+
}
|
|
1701
|
+
lines.push("services:");
|
|
1702
|
+
lines.push(" app:");
|
|
1703
|
+
lines.push(" environment:");
|
|
1704
|
+
for (const key of Object.keys(vars).sort()) {
|
|
1705
|
+
lines.push(` ${key}: ${yamlQuote(vars[key])}`);
|
|
1706
|
+
}
|
|
1707
|
+
return lines.join("\n") + "\n";
|
|
1708
|
+
}
|
|
1709
|
+
function parseDockerCompose(content) {
|
|
1710
|
+
const result = {};
|
|
1711
|
+
const lines = content.split("\n");
|
|
1712
|
+
let inEnvironment = false;
|
|
1713
|
+
let environmentIndent = -1;
|
|
1714
|
+
for (const line2 of lines) {
|
|
1715
|
+
const trimmed = line2.trim();
|
|
1716
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1717
|
+
const indent = line2.length - line2.trimStart().length;
|
|
1718
|
+
if (trimmed === "environment:") {
|
|
1719
|
+
inEnvironment = true;
|
|
1720
|
+
environmentIndent = indent;
|
|
1721
|
+
continue;
|
|
1722
|
+
}
|
|
1723
|
+
if (inEnvironment) {
|
|
1724
|
+
if (indent <= environmentIndent && trimmed !== "") {
|
|
1725
|
+
inEnvironment = false;
|
|
1726
|
+
environmentIndent = -1;
|
|
1727
|
+
continue;
|
|
1728
|
+
}
|
|
1729
|
+
if (trimmed.startsWith("- ")) {
|
|
1730
|
+
const entry = trimmed.slice(2);
|
|
1731
|
+
const eqIdx = entry.indexOf("=");
|
|
1732
|
+
if (eqIdx !== -1) {
|
|
1733
|
+
const key = entry.substring(0, eqIdx).trim();
|
|
1734
|
+
let value = entry.substring(eqIdx + 1).trim();
|
|
1735
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
1736
|
+
value = value.slice(1, -1);
|
|
1737
|
+
}
|
|
1738
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
1739
|
+
result[key] = value;
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
continue;
|
|
1743
|
+
}
|
|
1744
|
+
const colonIndex = trimmed.indexOf(": ");
|
|
1745
|
+
if (colonIndex !== -1) {
|
|
1746
|
+
const key = trimmed.substring(0, colonIndex).trim();
|
|
1747
|
+
let value = trimmed.substring(colonIndex + 2).trim();
|
|
1748
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
1749
|
+
value = value.slice(1, -1);
|
|
1750
|
+
if (value.includes("\\")) {
|
|
1751
|
+
value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
1755
|
+
result[key] = value;
|
|
1756
|
+
}
|
|
1757
|
+
} else if (trimmed.endsWith(":")) {
|
|
1758
|
+
const key = trimmed.slice(0, -1).trim();
|
|
1759
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
1760
|
+
result[key] = "";
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
return result;
|
|
1766
|
+
}
|
|
1767
|
+
function serializeAws(vars, meta) {
|
|
1768
|
+
const prefix = meta?.prefix || `/${meta?.projectName || "app"}`;
|
|
1769
|
+
const parameters = Object.keys(vars).sort().map((key) => ({
|
|
1770
|
+
name: `${prefix}/${key}`,
|
|
1771
|
+
value: vars[key],
|
|
1772
|
+
type: "SecureString"
|
|
1773
|
+
}));
|
|
1774
|
+
return JSON.stringify({ parameters }, null, 2) + "\n";
|
|
1775
|
+
}
|
|
1776
|
+
function parseAws(content, prefix) {
|
|
1777
|
+
const data = JSON.parse(content);
|
|
1778
|
+
const result = {};
|
|
1779
|
+
const params = data.parameters || data.Parameters || [];
|
|
1780
|
+
if (!Array.isArray(params)) {
|
|
1781
|
+
throw new Error(
|
|
1782
|
+
"Expected AWS Parameter Store format with 'parameters' array"
|
|
1783
|
+
);
|
|
1784
|
+
}
|
|
1785
|
+
for (const param of params) {
|
|
1786
|
+
const name = param.name || param.Name || "";
|
|
1787
|
+
const value = param.value || param.Value || "";
|
|
1788
|
+
let key;
|
|
1789
|
+
if (prefix && name.startsWith(prefix + "/")) {
|
|
1790
|
+
key = name.substring(prefix.length + 1);
|
|
1791
|
+
} else {
|
|
1792
|
+
const lastSlash = name.lastIndexOf("/");
|
|
1793
|
+
key = lastSlash !== -1 ? name.substring(lastSlash + 1) : name;
|
|
1794
|
+
}
|
|
1795
|
+
if (key && /^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
1796
|
+
result[key] = value;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
return result;
|
|
1800
|
+
}
|
|
1801
|
+
function serializeVercel(vars) {
|
|
1802
|
+
const sorted = Object.keys(vars).sort().reduce(
|
|
1803
|
+
(obj, key) => {
|
|
1804
|
+
obj[key] = vars[key];
|
|
1805
|
+
return obj;
|
|
1806
|
+
},
|
|
1807
|
+
{}
|
|
1808
|
+
);
|
|
1809
|
+
return JSON.stringify({ env: sorted }, null, 2) + "\n";
|
|
1810
|
+
}
|
|
1811
|
+
function parseVercel(content) {
|
|
1812
|
+
const data = JSON.parse(content);
|
|
1813
|
+
const result = {};
|
|
1814
|
+
const env = data.env;
|
|
1815
|
+
if (!env || typeof env !== "object" || Array.isArray(env)) {
|
|
1816
|
+
throw new Error("Expected Vercel format with 'env' object");
|
|
1817
|
+
}
|
|
1818
|
+
for (const [key, value] of Object.entries(env)) {
|
|
1819
|
+
if (typeof value === "string") {
|
|
1820
|
+
result[key] = value;
|
|
1821
|
+
} else if (value !== null && value !== void 0) {
|
|
1822
|
+
result[key] = String(value);
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
return result;
|
|
1826
|
+
}
|
|
1827
|
+
function tomlQuote(value) {
|
|
1828
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
|
1829
|
+
return `"${escaped}"`;
|
|
1830
|
+
}
|
|
1831
|
+
function serializeNetlify(vars, meta) {
|
|
1832
|
+
const lines = [];
|
|
1833
|
+
if (meta?.environment || meta?.projectName) {
|
|
1834
|
+
if (meta.projectName) lines.push(`# Project: ${meta.projectName}`);
|
|
1835
|
+
if (meta.environment) lines.push(`# Environment: ${meta.environment}`);
|
|
1836
|
+
lines.push("");
|
|
1837
|
+
}
|
|
1838
|
+
lines.push("[build.environment]");
|
|
1839
|
+
for (const key of Object.keys(vars).sort()) {
|
|
1840
|
+
lines.push(` ${key} = ${tomlQuote(vars[key])}`);
|
|
1841
|
+
}
|
|
1842
|
+
return lines.join("\n") + "\n";
|
|
1843
|
+
}
|
|
1844
|
+
function parseNetlify(content) {
|
|
1845
|
+
const result = {};
|
|
1846
|
+
const lines = content.split("\n");
|
|
1847
|
+
let inBuildEnv = false;
|
|
1848
|
+
for (const line2 of lines) {
|
|
1849
|
+
const trimmed = line2.trim();
|
|
1850
|
+
if (trimmed.startsWith("[")) {
|
|
1851
|
+
inBuildEnv = trimmed === "[build.environment]" || trimmed === "[build.environment]";
|
|
1852
|
+
continue;
|
|
1853
|
+
}
|
|
1854
|
+
if (!inBuildEnv) continue;
|
|
1855
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1856
|
+
const eqIndex = trimmed.indexOf("=");
|
|
1857
|
+
if (eqIndex === -1) continue;
|
|
1858
|
+
const key = trimmed.substring(0, eqIndex).trim();
|
|
1859
|
+
let value = trimmed.substring(eqIndex + 1).trim();
|
|
1860
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
1861
|
+
value = value.slice(1, -1);
|
|
1862
|
+
value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
1863
|
+
} else if (value.startsWith("'") && value.endsWith("'")) {
|
|
1864
|
+
value = value.slice(1, -1);
|
|
1865
|
+
}
|
|
1866
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
1867
|
+
result[key] = value;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
return result;
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
// src/commands/pull.ts
|
|
1874
|
+
var pullCommand = new Command3("pull").description("Download environment variables to local .env file").option(
|
|
1875
|
+
"-e, --env <environment>",
|
|
1876
|
+
"Environment (development, staging, production)"
|
|
1877
|
+
).option("-f, --file <path>", "Output file path (default: .env)").option("--force", "Overwrite without confirmation").option(
|
|
1878
|
+
"--format <format>",
|
|
1879
|
+
"Output format: env, json, yaml, docker-compose, aws, vercel, netlify",
|
|
1880
|
+
"env"
|
|
1881
|
+
).option(
|
|
1882
|
+
"--prefix <prefix>",
|
|
1883
|
+
"AWS Parameter Store path prefix (default: /project-name)"
|
|
1884
|
+
).option("--dry-run", "Show what would be downloaded without writing").option("--project <name-or-id>", "Pull a specific linked project").option("--all", "Pull all linked projects").action(async (options) => {
|
|
1885
|
+
try {
|
|
1886
|
+
if (!isAuthenticated()) {
|
|
1887
|
+
throw notAuthenticated();
|
|
1888
|
+
}
|
|
1889
|
+
if (options.all) {
|
|
1890
|
+
if (options.env || options.file) {
|
|
1891
|
+
error("Cannot use --env or --file with --all.");
|
|
1892
|
+
process.exit(1);
|
|
1893
|
+
}
|
|
1894
|
+
await pullAllProjects(options);
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1897
|
+
if (options.project) {
|
|
1898
|
+
const configV2 = readProjectConfigV2();
|
|
1899
|
+
if (!configV2) throw notInitialized();
|
|
1900
|
+
const project = resolveProject(configV2, options.project);
|
|
1901
|
+
if (!project) {
|
|
1902
|
+
error(`Project not found: ${options.project}`);
|
|
1903
|
+
console.log();
|
|
1904
|
+
console.log("Linked projects:");
|
|
1905
|
+
for (const p of configV2.projects) {
|
|
1906
|
+
console.log(` ${p.projectName || p.projectId} (${p.environment})`);
|
|
1907
|
+
}
|
|
1908
|
+
process.exit(1);
|
|
1909
|
+
}
|
|
1910
|
+
await pullSingleProject(project, options);
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
const projectConfig = readProjectConfig();
|
|
1914
|
+
if (!projectConfig) throw notInitialized();
|
|
1915
|
+
checkTrackedFiles();
|
|
1916
|
+
const environment = options.env || projectConfig.environment || "development";
|
|
1917
|
+
const fmt = options.format || "env";
|
|
1918
|
+
if (!ALL_FORMATS.includes(fmt)) {
|
|
1919
|
+
error(`Unknown format: ${fmt}. Supported: ${ALL_FORMATS.join(", ")}`);
|
|
1920
|
+
process.exit(1);
|
|
1921
|
+
}
|
|
1922
|
+
const outputPath = options.file || (fmt === "env" ? getEnvPathForEnvironment(environment) : getDefaultFilename(fmt, environment));
|
|
1923
|
+
await pullProject(
|
|
1924
|
+
{
|
|
1925
|
+
projectId: projectConfig.projectId,
|
|
1926
|
+
organizationId: projectConfig.organizationId,
|
|
1927
|
+
environment
|
|
1928
|
+
},
|
|
1929
|
+
outputPath,
|
|
1930
|
+
options
|
|
1931
|
+
);
|
|
1932
|
+
} catch (err) {
|
|
1933
|
+
await handleError(err);
|
|
1934
|
+
}
|
|
1935
|
+
});
|
|
1936
|
+
async function pullAllProjects(options) {
|
|
1937
|
+
const configV2 = readProjectConfigV2();
|
|
1938
|
+
if (!configV2) throw notInitialized();
|
|
1939
|
+
checkTrackedFiles();
|
|
1940
|
+
let totalPulled = 0;
|
|
1941
|
+
let totalFailed = 0;
|
|
1942
|
+
for (const project of configV2.projects) {
|
|
1943
|
+
const outputPath = getEnvPathForEnvironment(project.environment);
|
|
1944
|
+
const displayName = project.projectName || project.projectId;
|
|
1945
|
+
console.log();
|
|
1946
|
+
console.log(
|
|
1947
|
+
chalk6.bold(
|
|
1948
|
+
`Pulling "${displayName}" (${project.environment}) \u2192 ${outputPath}`
|
|
1949
|
+
)
|
|
1950
|
+
);
|
|
1951
|
+
try {
|
|
1952
|
+
await pullProject(
|
|
1953
|
+
{
|
|
1954
|
+
projectId: project.projectId,
|
|
1955
|
+
organizationId: project.organizationId,
|
|
1956
|
+
environment: project.environment
|
|
1957
|
+
},
|
|
1958
|
+
outputPath,
|
|
1959
|
+
options
|
|
1960
|
+
);
|
|
1961
|
+
totalPulled++;
|
|
1962
|
+
} catch (err) {
|
|
1963
|
+
totalFailed++;
|
|
1964
|
+
if (err instanceof Error) {
|
|
1965
|
+
error(` Failed: ${err.message}`);
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
console.log();
|
|
1970
|
+
if (totalFailed === 0) {
|
|
1971
|
+
success(`Pulled ${totalPulled} project${totalPulled !== 1 ? "s" : ""}`);
|
|
1972
|
+
} else {
|
|
1973
|
+
warning(
|
|
1974
|
+
`Pulled ${totalPulled}/${totalPulled + totalFailed} projects. ${totalFailed} failed.`
|
|
1975
|
+
);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
async function pullSingleProject(project, options) {
|
|
1979
|
+
checkTrackedFiles();
|
|
1980
|
+
const environment = options.env || project.environment;
|
|
1981
|
+
const fmt = options.format || "env";
|
|
1982
|
+
const outputPath = options.file || (fmt === "env" ? getEnvPathForEnvironment(environment) : getDefaultFilename(fmt, environment));
|
|
1983
|
+
await pullProject(
|
|
1984
|
+
{
|
|
1985
|
+
projectId: project.projectId,
|
|
1986
|
+
organizationId: project.organizationId,
|
|
1987
|
+
environment
|
|
1988
|
+
},
|
|
1989
|
+
outputPath,
|
|
1990
|
+
options
|
|
1991
|
+
);
|
|
1992
|
+
}
|
|
1993
|
+
async function pullProject(project, outputPath, options) {
|
|
1994
|
+
const api = createAPIClient();
|
|
1995
|
+
let metaProjectRole;
|
|
1996
|
+
const variables = await withSpinner(
|
|
1997
|
+
`Fetching ${chalk6.bold(project.environment)} variables...`,
|
|
1998
|
+
async () => {
|
|
1999
|
+
const response = await api.get("/api/cli/variables", {
|
|
2000
|
+
projectId: project.projectId,
|
|
2001
|
+
environment: project.environment,
|
|
2002
|
+
...project.organizationId && {
|
|
2003
|
+
organizationId: project.organizationId
|
|
2004
|
+
}
|
|
2005
|
+
});
|
|
2006
|
+
metaProjectRole = response.meta?.projectRole;
|
|
2007
|
+
return response.data || [];
|
|
2008
|
+
}
|
|
2009
|
+
);
|
|
2010
|
+
if (variables.length === 0) {
|
|
2011
|
+
warning(`No variables found for ${project.environment} environment.`);
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
2014
|
+
const remoteVars = {};
|
|
2015
|
+
for (const variable of variables) {
|
|
2016
|
+
remoteVars[variable.key] = variable.value;
|
|
2017
|
+
}
|
|
2018
|
+
const localVars = readEnvFile(outputPath) || {};
|
|
2019
|
+
const diffResult = diffEnvVars(remoteVars, localVars);
|
|
2020
|
+
const hasChanges = Object.keys(diffResult.added).length > 0 || Object.keys(diffResult.removed).length > 0 || Object.keys(diffResult.changed).length > 0;
|
|
2021
|
+
if (!hasChanges) {
|
|
2022
|
+
success("Local file is up to date.");
|
|
2023
|
+
return;
|
|
2024
|
+
}
|
|
2025
|
+
console.log();
|
|
2026
|
+
console.log(chalk6.bold("Changes:"));
|
|
2027
|
+
console.log();
|
|
2028
|
+
diff(diffResult.added, diffResult.removed, diffResult.changed);
|
|
2029
|
+
console.log();
|
|
2030
|
+
if (options.dryRun) {
|
|
2031
|
+
info("Dry run - no changes written.");
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
2034
|
+
if (!options.force && Object.keys(localVars).length > 0) {
|
|
2035
|
+
const { proceed } = await inquirer2.prompt([
|
|
2036
|
+
{
|
|
2037
|
+
type: "confirm",
|
|
2038
|
+
name: "proceed",
|
|
2039
|
+
message: `Overwrite ${outputPath}?`,
|
|
2040
|
+
default: true
|
|
2041
|
+
}
|
|
2042
|
+
]);
|
|
2043
|
+
if (!proceed) {
|
|
2044
|
+
info("Pull cancelled.");
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
try {
|
|
2049
|
+
const fs = await import("fs");
|
|
2050
|
+
if (fs.existsSync(outputPath)) {
|
|
2051
|
+
fs.chmodSync(outputPath, 420);
|
|
2052
|
+
}
|
|
2053
|
+
} catch {
|
|
2054
|
+
}
|
|
2055
|
+
const fmt = options.format || "env";
|
|
2056
|
+
if (fmt === "env") {
|
|
2057
|
+
const comments = {};
|
|
2058
|
+
for (const variable of variables) {
|
|
2059
|
+
if (variable.description) {
|
|
2060
|
+
comments[variable.key] = variable.description;
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
writeEnvFile(outputPath, remoteVars, { sort: true, comments });
|
|
2064
|
+
} else {
|
|
2065
|
+
const fs = await import("fs");
|
|
2066
|
+
const output = serialize(remoteVars, fmt, {
|
|
2067
|
+
projectName: project.projectId,
|
|
2068
|
+
environment: project.environment,
|
|
2069
|
+
prefix: options.prefix
|
|
2070
|
+
});
|
|
2071
|
+
fs.writeFileSync(outputPath, output, "utf-8");
|
|
2072
|
+
}
|
|
2073
|
+
const role = getRole();
|
|
2074
|
+
applyFileProtection(outputPath, role, metaProjectRole);
|
|
2075
|
+
success(
|
|
2076
|
+
`Downloaded ${variables.length} variables to ${chalk6.bold(outputPath)}`
|
|
2077
|
+
);
|
|
2078
|
+
if (metaProjectRole === "viewer") {
|
|
2079
|
+
info(
|
|
2080
|
+
"You have Viewer access to this project. You may only see variables you have been explicitly granted access to."
|
|
2081
|
+
);
|
|
2082
|
+
}
|
|
2083
|
+
const isProtected = role !== "admin" && role !== "team_lead" && metaProjectRole !== "manager";
|
|
2084
|
+
if (isProtected) {
|
|
2085
|
+
info(
|
|
2086
|
+
`File is read-only (your role: ${role || metaProjectRole || "member"}).`
|
|
2087
|
+
);
|
|
2088
|
+
}
|
|
2089
|
+
console.log();
|
|
2090
|
+
console.log(chalk6.dim(` Added: ${Object.keys(diffResult.added).length}`));
|
|
2091
|
+
console.log(
|
|
2092
|
+
chalk6.dim(` Changed: ${Object.keys(diffResult.changed).length}`)
|
|
2093
|
+
);
|
|
2094
|
+
console.log(
|
|
2095
|
+
chalk6.dim(` Removed: ${Object.keys(diffResult.removed).length}`)
|
|
2096
|
+
);
|
|
2097
|
+
}
|
|
2098
|
+
function checkTrackedFiles() {
|
|
2099
|
+
const trackedFiles = getTrackedEnvFiles();
|
|
2100
|
+
if (trackedFiles.length > 0) {
|
|
2101
|
+
error("Security risk: .env files are tracked by git!");
|
|
2102
|
+
console.log();
|
|
2103
|
+
for (const file of trackedFiles) {
|
|
2104
|
+
console.log(chalk6.red(` tracked: ${file}`));
|
|
2105
|
+
}
|
|
2106
|
+
console.log();
|
|
2107
|
+
console.log(
|
|
2108
|
+
chalk6.yellow(
|
|
2109
|
+
" Run the following to untrack them (without deleting the files):"
|
|
2110
|
+
)
|
|
2111
|
+
);
|
|
2112
|
+
for (const file of trackedFiles) {
|
|
2113
|
+
console.log(chalk6.cyan(` git rm --cached ${file}`));
|
|
2114
|
+
}
|
|
2115
|
+
console.log();
|
|
2116
|
+
process.exit(1);
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
// src/commands/push.ts
|
|
2121
|
+
import { Command as Command4 } from "commander";
|
|
2122
|
+
import chalk7 from "chalk";
|
|
2123
|
+
import inquirer3 from "inquirer";
|
|
2124
|
+
|
|
2125
|
+
// src/lib/validators.ts
|
|
2126
|
+
import { z as z2 } from "zod";
|
|
2127
|
+
var envKeySchema = z2.string().min(1, "Key cannot be empty").max(256, "Key cannot exceed 256 characters").regex(
|
|
2128
|
+
/^[A-Za-z_][A-Za-z0-9_]*$/,
|
|
2129
|
+
"Key must start with a letter or underscore, followed by letters, numbers, or underscores"
|
|
2130
|
+
);
|
|
2131
|
+
var envValueSchema = z2.string().max(65536, "Value cannot exceed 64KB");
|
|
2132
|
+
var environmentSchema2 = z2.enum([
|
|
2133
|
+
"development",
|
|
2134
|
+
"staging",
|
|
2135
|
+
"production"
|
|
2136
|
+
]);
|
|
2137
|
+
var projectSlugSchema = z2.string().min(1, "Slug cannot be empty").max(128, "Slug cannot exceed 128 characters").regex(
|
|
2138
|
+
/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/,
|
|
2139
|
+
"Slug must be lowercase alphanumeric with hyphens, cannot start or end with hyphen"
|
|
2140
|
+
);
|
|
2141
|
+
var urlSchema = z2.string().url("Must be a valid URL");
|
|
2142
|
+
var tokenSchema = z2.string().min(1, "Token cannot be empty").regex(/^env_[A-Za-z0-9]{48}$/, "Invalid token format");
|
|
2143
|
+
var filePathSchema = z2.string().min(1, "File path cannot be empty");
|
|
2144
|
+
function validateEnvVars(vars) {
|
|
2145
|
+
const valid = {};
|
|
2146
|
+
const invalid = [];
|
|
2147
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
2148
|
+
const keyResult = envKeySchema.safeParse(key);
|
|
2149
|
+
const valueResult = envValueSchema.safeParse(value);
|
|
2150
|
+
if (!keyResult.success) {
|
|
2151
|
+
invalid.push({ key, error: keyResult.error.issues[0].message });
|
|
2152
|
+
continue;
|
|
2153
|
+
}
|
|
2154
|
+
if (!valueResult.success) {
|
|
2155
|
+
invalid.push({ key, error: valueResult.error.issues[0].message });
|
|
2156
|
+
continue;
|
|
2157
|
+
}
|
|
2158
|
+
valid[key] = value;
|
|
2159
|
+
}
|
|
2160
|
+
return { valid, invalid };
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
// src/commands/push.ts
|
|
2164
|
+
var pushCommand = new Command4("push").description("Upload local .env file to cloud").option(
|
|
2165
|
+
"-e, --env <environment>",
|
|
2166
|
+
"Target environment (development, staging, production)"
|
|
2167
|
+
).option("-f, --file <path>", "Input file path (default: .env)").option("--merge", "Merge with existing variables (default)").option("--replace", "Replace all existing variables").option(
|
|
2168
|
+
"--format <format>",
|
|
2169
|
+
"Input format: env, json, yaml, docker-compose, aws, vercel, netlify",
|
|
2170
|
+
"env"
|
|
2171
|
+
).option(
|
|
2172
|
+
"--prefix <prefix>",
|
|
2173
|
+
"AWS Parameter Store path prefix (default: /project-name)"
|
|
2174
|
+
).option("--dry-run", "Show what would be uploaded without making changes").option("--force", "Skip confirmation").option("--project <name-or-id>", "Push to a specific linked project").action(async (options) => {
|
|
2175
|
+
try {
|
|
2176
|
+
if (!isAuthenticated()) {
|
|
2177
|
+
throw notAuthenticated();
|
|
2178
|
+
}
|
|
2179
|
+
let projectId;
|
|
2180
|
+
let organizationId;
|
|
2181
|
+
let defaultEnvironment;
|
|
2182
|
+
if (options.project) {
|
|
2183
|
+
const configV2 = readProjectConfigV2();
|
|
2184
|
+
if (!configV2) throw notInitialized();
|
|
2185
|
+
const resolved = resolveProject(configV2, options.project);
|
|
2186
|
+
if (!resolved) {
|
|
2187
|
+
error(`Project not found: ${options.project}`);
|
|
2188
|
+
console.log();
|
|
2189
|
+
console.log("Linked projects:");
|
|
2190
|
+
for (const p of configV2.projects) {
|
|
2191
|
+
console.log(` ${p.projectName || p.projectId} (${p.environment})`);
|
|
2192
|
+
}
|
|
2193
|
+
process.exit(1);
|
|
2194
|
+
}
|
|
2195
|
+
projectId = resolved.projectId;
|
|
2196
|
+
organizationId = resolved.organizationId;
|
|
2197
|
+
defaultEnvironment = resolved.environment;
|
|
2198
|
+
} else {
|
|
2199
|
+
const projectConfig = readProjectConfig();
|
|
2200
|
+
if (!projectConfig) throw notInitialized();
|
|
2201
|
+
projectId = projectConfig.projectId;
|
|
2202
|
+
organizationId = projectConfig.organizationId;
|
|
2203
|
+
defaultEnvironment = projectConfig.environment;
|
|
2204
|
+
}
|
|
2205
|
+
const trackedFiles = getTrackedEnvFiles();
|
|
2206
|
+
if (trackedFiles.length > 0) {
|
|
2207
|
+
error("Security risk: .env files are tracked by git!");
|
|
2208
|
+
console.log();
|
|
2209
|
+
for (const file of trackedFiles) {
|
|
2210
|
+
console.log(chalk7.red(` tracked: ${file}`));
|
|
2211
|
+
}
|
|
2212
|
+
console.log();
|
|
2213
|
+
console.log(
|
|
2214
|
+
chalk7.yellow(
|
|
2215
|
+
" Run the following to untrack them (without deleting the files):"
|
|
2216
|
+
)
|
|
2217
|
+
);
|
|
2218
|
+
for (const file of trackedFiles) {
|
|
2219
|
+
console.log(chalk7.cyan(` git rm --cached ${file}`));
|
|
2220
|
+
}
|
|
2221
|
+
console.log();
|
|
2222
|
+
process.exit(1);
|
|
2223
|
+
}
|
|
2224
|
+
const api = createAPIClient();
|
|
2225
|
+
const projects = await api.get("/api/cli/projects", { organizationId });
|
|
2226
|
+
const currentProject = projects.data?.find((p) => p._id === projectId);
|
|
2227
|
+
const projectRole = currentProject?.projectRole;
|
|
2228
|
+
if (projectRole === "viewer") {
|
|
2229
|
+
error(
|
|
2230
|
+
"Unfortunately, as a member, you cannot push in any environment. Please access or request access to your team lead."
|
|
2231
|
+
);
|
|
2232
|
+
process.exit(1);
|
|
2233
|
+
}
|
|
2234
|
+
const role = getRole();
|
|
2235
|
+
if (role === "member") {
|
|
2236
|
+
warning(
|
|
2237
|
+
"You have Member access. Push will create pending requests that require Admin or Team Lead approval."
|
|
2238
|
+
);
|
|
2239
|
+
console.log();
|
|
2240
|
+
if (!options.force) {
|
|
2241
|
+
const { proceed } = await inquirer3.prompt([
|
|
2242
|
+
{
|
|
2243
|
+
type: "confirm",
|
|
2244
|
+
name: "proceed",
|
|
2245
|
+
message: "Continue with creating approval requests?",
|
|
2246
|
+
default: true
|
|
2247
|
+
}
|
|
2248
|
+
]);
|
|
2249
|
+
if (!proceed) {
|
|
2250
|
+
info("Push cancelled.");
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
const environment = options.env || defaultEnvironment || "development";
|
|
2256
|
+
const fmt = options.format || "env";
|
|
2257
|
+
if (!ALL_FORMATS.includes(fmt)) {
|
|
2258
|
+
error(`Unknown format: ${fmt}. Supported: ${ALL_FORMATS.join(", ")}`);
|
|
2259
|
+
process.exit(1);
|
|
2260
|
+
}
|
|
2261
|
+
const inputPath = options.file || (fmt === "env" ? getEnvPathForEnvironment(environment) : getDefaultFilename(fmt, environment));
|
|
2262
|
+
const mode = options.replace ? "replace" : "merge";
|
|
2263
|
+
let localVars;
|
|
2264
|
+
if (fmt === "env") {
|
|
2265
|
+
localVars = readEnvFile(inputPath);
|
|
2266
|
+
} else {
|
|
2267
|
+
const { readFileSync: readFileSync4, existsSync: existsSync4 } = await import("fs");
|
|
2268
|
+
if (!existsSync4(inputPath)) {
|
|
2269
|
+
localVars = null;
|
|
2270
|
+
} else {
|
|
2271
|
+
const content = readFileSync4(inputPath, "utf-8");
|
|
2272
|
+
localVars = parse(content, fmt, { prefix: options.prefix });
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
if (!localVars) {
|
|
2276
|
+
throw fileNotFound(inputPath);
|
|
2277
|
+
}
|
|
2278
|
+
if (Object.keys(localVars).length === 0) {
|
|
2279
|
+
warning(`No variables found in ${inputPath}`);
|
|
2280
|
+
return;
|
|
2281
|
+
}
|
|
2282
|
+
const { valid, invalid } = validateEnvVars(localVars);
|
|
2283
|
+
if (invalid.length > 0) {
|
|
2284
|
+
warning("Some variables have invalid keys and will be skipped:");
|
|
2285
|
+
for (const { key, error: err } of invalid) {
|
|
2286
|
+
console.log(chalk7.red(` ${key}: ${err}`));
|
|
2287
|
+
}
|
|
2288
|
+
console.log();
|
|
2289
|
+
}
|
|
2290
|
+
if (Object.keys(valid).length === 0) {
|
|
2291
|
+
error("No valid variables to push.");
|
|
2292
|
+
return;
|
|
2293
|
+
}
|
|
2294
|
+
const remoteVariables = await withSpinner(
|
|
2295
|
+
"Fetching current variables...",
|
|
2296
|
+
async () => {
|
|
2297
|
+
const params = {
|
|
2298
|
+
projectId,
|
|
2299
|
+
environment
|
|
2300
|
+
};
|
|
2301
|
+
if (organizationId) {
|
|
2302
|
+
params.organizationId = organizationId;
|
|
2303
|
+
}
|
|
2304
|
+
const response = await api.get("/api/cli/variables", params);
|
|
2305
|
+
return response.data || [];
|
|
2306
|
+
}
|
|
2307
|
+
);
|
|
2308
|
+
const remoteVars = {};
|
|
2309
|
+
for (const variable of remoteVariables) {
|
|
2310
|
+
remoteVars[variable.key] = variable.value;
|
|
2311
|
+
}
|
|
2312
|
+
const diffResult = diffEnvVars(valid, remoteVars);
|
|
2313
|
+
const hasChanges = Object.keys(diffResult.added).length > 0 || Object.keys(diffResult.changed).length > 0 || mode === "replace" && Object.keys(diffResult.removed).length > 0;
|
|
2314
|
+
if (!hasChanges) {
|
|
2315
|
+
success("Remote is up to date.");
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
console.log();
|
|
2319
|
+
console.log(chalk7.bold("Changes to push:"));
|
|
2320
|
+
console.log();
|
|
2321
|
+
const removedToShow = mode === "replace" ? diffResult.removed : {};
|
|
2322
|
+
diff(diffResult.added, removedToShow, diffResult.changed);
|
|
2323
|
+
if (mode === "merge" && Object.keys(diffResult.removed).length > 0) {
|
|
2324
|
+
console.log();
|
|
2325
|
+
console.log(
|
|
2326
|
+
chalk7.dim(
|
|
2327
|
+
`Note: ${Object.keys(diffResult.removed).length} remote variables not in local file will be preserved (use --replace to remove them)`
|
|
2328
|
+
)
|
|
2329
|
+
);
|
|
2330
|
+
}
|
|
2331
|
+
console.log();
|
|
2332
|
+
if (options.dryRun) {
|
|
2333
|
+
info("Dry run - no changes made.");
|
|
2334
|
+
console.log();
|
|
2335
|
+
console.log("Summary:");
|
|
2336
|
+
console.log(` Would add: ${Object.keys(diffResult.added).length}`);
|
|
2337
|
+
console.log(
|
|
2338
|
+
` Would update: ${Object.keys(diffResult.changed).length}`
|
|
2339
|
+
);
|
|
2340
|
+
if (mode === "replace") {
|
|
2341
|
+
console.log(
|
|
2342
|
+
` Would delete: ${Object.keys(diffResult.removed).length}`
|
|
2343
|
+
);
|
|
2344
|
+
}
|
|
2345
|
+
return;
|
|
2346
|
+
}
|
|
2347
|
+
if (!options.force) {
|
|
2348
|
+
const confirmMessage = mode === "replace" ? `Push ${Object.keys(valid).length} variables and delete ${Object.keys(diffResult.removed).length} remote-only variables?` : `Push ${Object.keys(valid).length} variables to ${environment}?`;
|
|
2349
|
+
const { proceed } = await inquirer3.prompt([
|
|
2350
|
+
{
|
|
2351
|
+
type: "confirm",
|
|
2352
|
+
name: "proceed",
|
|
2353
|
+
message: confirmMessage,
|
|
2354
|
+
default: true
|
|
2355
|
+
}
|
|
2356
|
+
]);
|
|
2357
|
+
if (!proceed) {
|
|
2358
|
+
info("Push cancelled.");
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
const result = await withSpinner(
|
|
2363
|
+
`Pushing variables to ${chalk7.bold(environment)}...`,
|
|
2364
|
+
async () => {
|
|
2365
|
+
const response = await api.post("/api/cli/variables/bulk", {
|
|
2366
|
+
projectId,
|
|
2367
|
+
environment,
|
|
2368
|
+
variables: Object.entries(valid).map(([key, value]) => ({
|
|
2369
|
+
key,
|
|
2370
|
+
value
|
|
2371
|
+
})),
|
|
2372
|
+
mode,
|
|
2373
|
+
...organizationId && { organizationId }
|
|
2374
|
+
});
|
|
2375
|
+
return response.data;
|
|
2376
|
+
}
|
|
2377
|
+
);
|
|
2378
|
+
if (result?.requested && result.requested > 0) {
|
|
2379
|
+
success(
|
|
2380
|
+
`Created ${result.requested} pending request(s) for ${chalk7.bold(environment)}`
|
|
2381
|
+
);
|
|
2382
|
+
console.log();
|
|
2383
|
+
console.log(
|
|
2384
|
+
chalk7.yellow(
|
|
2385
|
+
" These changes require approval from an Admin or Team Lead."
|
|
2386
|
+
)
|
|
2387
|
+
);
|
|
2388
|
+
console.log();
|
|
2389
|
+
console.log(chalk7.dim(` Requested: ${result.requested}`));
|
|
2390
|
+
if (result?.skipped && result.skipped > 0) {
|
|
2391
|
+
console.log(chalk7.dim(` Skipped: ${result.skipped}`));
|
|
2392
|
+
}
|
|
2393
|
+
} else {
|
|
2394
|
+
success(
|
|
2395
|
+
`Pushed ${result?.total || Object.keys(valid).length} variables to ${chalk7.bold(environment)}`
|
|
2396
|
+
);
|
|
2397
|
+
console.log();
|
|
2398
|
+
console.log(chalk7.dim(` Created: ${result?.created || 0}`));
|
|
2399
|
+
console.log(chalk7.dim(` Updated: ${result?.updated || 0}`));
|
|
2400
|
+
if (mode === "replace") {
|
|
2401
|
+
console.log(chalk7.dim(` Deleted: ${result?.deleted || 0}`));
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
} catch (err) {
|
|
2405
|
+
await handleError(err);
|
|
2406
|
+
}
|
|
2407
|
+
});
|
|
2408
|
+
|
|
2409
|
+
// src/commands/switch.ts
|
|
2410
|
+
import { Command as Command5 } from "commander";
|
|
2411
|
+
import chalk8 from "chalk";
|
|
2412
|
+
import inquirer4 from "inquirer";
|
|
2413
|
+
var switchCommand = new Command5("switch").description("Switch project, environment, or active linked project").argument("[target]", "project slug or environment name").option("-o, --organization <id>", "Switch organization").option("-p, --project <id>", "Switch project").option(
|
|
2414
|
+
"-e, --env <environment>",
|
|
2415
|
+
"Switch environment (development, staging, production)"
|
|
2416
|
+
).option("--active <name-or-id>", "Set a linked project as active").action(async (target, options) => {
|
|
2417
|
+
try {
|
|
2418
|
+
if (!isAuthenticated()) {
|
|
2419
|
+
throw notAuthenticated();
|
|
2420
|
+
}
|
|
2421
|
+
const api = createAPIClient();
|
|
2422
|
+
const projectConfig = readProjectConfig();
|
|
2423
|
+
if (options.active) {
|
|
2424
|
+
const configV2 = readProjectConfigV2();
|
|
2425
|
+
if (!configV2 || configV2.projects.length < 2) {
|
|
2426
|
+
error(
|
|
2427
|
+
"No multiple projects linked. Use `envpilot init --add` to link another project."
|
|
2428
|
+
);
|
|
2429
|
+
process.exit(1);
|
|
2430
|
+
}
|
|
2431
|
+
const target2 = configV2.projects.find(
|
|
2432
|
+
(p) => p.projectId === options.active || p.projectName.toLowerCase() === options.active.toLowerCase()
|
|
2433
|
+
);
|
|
2434
|
+
if (!target2) {
|
|
2435
|
+
error(`Project not found: ${options.active}`);
|
|
2436
|
+
console.log();
|
|
2437
|
+
console.log("Linked projects:");
|
|
2438
|
+
for (const p of configV2.projects) {
|
|
2439
|
+
const mark = p.projectId === configV2.activeProjectId ? chalk8.green(" *") : "";
|
|
2440
|
+
console.log(
|
|
2441
|
+
` ${p.projectName || p.projectId} (${p.environment})${mark}`
|
|
2442
|
+
);
|
|
2443
|
+
}
|
|
2444
|
+
process.exit(1);
|
|
2445
|
+
}
|
|
2446
|
+
const updated = setActiveProjectInConfig(configV2, target2.projectId);
|
|
2447
|
+
writeProjectConfigV2(updated);
|
|
2448
|
+
setActiveProjectId(target2.projectId);
|
|
2449
|
+
setActiveOrganizationId(target2.organizationId);
|
|
2450
|
+
success(
|
|
2451
|
+
`Active project: ${chalk8.bold(target2.projectName || target2.projectId)}`
|
|
2452
|
+
);
|
|
2453
|
+
return;
|
|
2454
|
+
}
|
|
2455
|
+
if (options.env || target && ["development", "staging", "production"].includes(target)) {
|
|
2456
|
+
const environment = options.env || target;
|
|
2457
|
+
if (!projectConfig) {
|
|
2458
|
+
error("No project initialized. Run `envpilot init` first.");
|
|
2459
|
+
process.exit(1);
|
|
2460
|
+
}
|
|
2461
|
+
updateProjectConfig({ environment });
|
|
2462
|
+
success(`Switched to ${chalk8.bold(environment)} environment`);
|
|
2463
|
+
return;
|
|
2464
|
+
}
|
|
2465
|
+
if (options.organization) {
|
|
2466
|
+
const organizations = await withSpinner(
|
|
2467
|
+
"Fetching organizations...",
|
|
2468
|
+
async () => {
|
|
2469
|
+
const response = await api.get("/api/cli/organizations");
|
|
2470
|
+
return response.data || [];
|
|
2471
|
+
}
|
|
2472
|
+
);
|
|
2473
|
+
const org = organizations.find(
|
|
2474
|
+
(o) => o._id === options.organization || o.slug === options.organization
|
|
2475
|
+
);
|
|
2476
|
+
if (!org) {
|
|
2477
|
+
error(`Organization not found: ${options.organization}`);
|
|
2478
|
+
process.exit(1);
|
|
2479
|
+
}
|
|
2480
|
+
setActiveOrganizationId(org._id);
|
|
2481
|
+
if (org.role) {
|
|
2482
|
+
setRole(org.role);
|
|
2483
|
+
}
|
|
2484
|
+
if (projectConfig) {
|
|
2485
|
+
updateProjectConfig({ organizationId: org._id });
|
|
2486
|
+
}
|
|
2487
|
+
success(`Switched to organization: ${chalk8.bold(org.name)}`);
|
|
2488
|
+
if (org.role) {
|
|
2489
|
+
console.log(chalk8.dim(` Role: ${formatRole(org.role)}`));
|
|
2490
|
+
roleNotice(org.role);
|
|
2491
|
+
}
|
|
2492
|
+
return;
|
|
2493
|
+
}
|
|
2494
|
+
if (options.project || target) {
|
|
2495
|
+
const projectIdentifier = options.project || target;
|
|
2496
|
+
const configV2 = readProjectConfigV2();
|
|
2497
|
+
if (configV2) {
|
|
2498
|
+
const linked = configV2.projects.find(
|
|
2499
|
+
(p) => p.projectId === projectIdentifier || p.projectName.toLowerCase() === projectIdentifier.toLowerCase()
|
|
2500
|
+
);
|
|
2501
|
+
if (linked) {
|
|
2502
|
+
const updated = setActiveProjectInConfig(
|
|
2503
|
+
configV2,
|
|
2504
|
+
linked.projectId
|
|
2505
|
+
);
|
|
2506
|
+
writeProjectConfigV2(updated);
|
|
2507
|
+
setActiveProjectId(linked.projectId);
|
|
2508
|
+
setActiveOrganizationId(linked.organizationId);
|
|
2509
|
+
success(
|
|
2510
|
+
`Switched to project: ${chalk8.bold(linked.projectName || linked.projectId)}`
|
|
2511
|
+
);
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
let organizationId = projectConfig?.organizationId;
|
|
2516
|
+
if (!organizationId) {
|
|
2517
|
+
const organizations = await withSpinner(
|
|
2518
|
+
"Fetching organizations...",
|
|
2519
|
+
async () => {
|
|
2520
|
+
const response = await api.get("/api/cli/organizations");
|
|
2521
|
+
return response.data || [];
|
|
2522
|
+
}
|
|
2523
|
+
);
|
|
2524
|
+
if (organizations.length === 0) {
|
|
2525
|
+
error("No organizations found.");
|
|
2526
|
+
process.exit(1);
|
|
2527
|
+
}
|
|
2528
|
+
if (organizations.length === 1) {
|
|
2529
|
+
organizationId = organizations[0]._id;
|
|
2530
|
+
if (organizations[0].role) {
|
|
2531
|
+
setRole(organizations[0].role);
|
|
2532
|
+
}
|
|
2533
|
+
} else {
|
|
2534
|
+
const { orgId } = await inquirer4.prompt([
|
|
2535
|
+
{
|
|
2536
|
+
type: "list",
|
|
2537
|
+
name: "orgId",
|
|
2538
|
+
message: "Select an organization:",
|
|
2539
|
+
choices: organizations.map((org) => ({
|
|
2540
|
+
name: `${org.name} ${org.tier === "pro" ? chalk8.green("(Pro)") : chalk8.dim("(Free)")}`,
|
|
2541
|
+
value: org._id
|
|
2542
|
+
}))
|
|
2543
|
+
}
|
|
2544
|
+
]);
|
|
2545
|
+
organizationId = orgId;
|
|
2546
|
+
const selectedOrg = organizations.find((o) => o._id === orgId);
|
|
2547
|
+
if (selectedOrg?.role) {
|
|
2548
|
+
setRole(selectedOrg.role);
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
const projects = await withSpinner("Fetching projects...", async () => {
|
|
2553
|
+
const response = await api.get("/api/cli/projects", { organizationId });
|
|
2554
|
+
return response.data || [];
|
|
2555
|
+
});
|
|
2556
|
+
const project = projects.find(
|
|
2557
|
+
(p) => p._id === projectIdentifier || p.slug === projectIdentifier
|
|
2558
|
+
);
|
|
2559
|
+
if (!project) {
|
|
2560
|
+
error(`Project not found: ${projectIdentifier}`);
|
|
2561
|
+
console.log();
|
|
2562
|
+
console.log("Available projects:");
|
|
2563
|
+
for (const p of projects) {
|
|
2564
|
+
console.log(` ${p.icon || "\u{1F4E6}"} ${p.name} (${p.slug})`);
|
|
2565
|
+
}
|
|
2566
|
+
process.exit(1);
|
|
2567
|
+
}
|
|
2568
|
+
setActiveProjectId(project._id);
|
|
2569
|
+
setActiveOrganizationId(organizationId);
|
|
2570
|
+
const environment = projectConfig?.environment || "development";
|
|
2571
|
+
writeProjectConfig({
|
|
2572
|
+
projectId: project._id,
|
|
2573
|
+
organizationId,
|
|
2574
|
+
environment
|
|
2575
|
+
});
|
|
2576
|
+
success(`Switched to project: ${chalk8.bold(project.name)}`);
|
|
2577
|
+
if (project.projectRole) {
|
|
2578
|
+
console.log(
|
|
2579
|
+
chalk8.dim(
|
|
2580
|
+
` Project role: ${formatProjectRole(project.projectRole)}`
|
|
2581
|
+
)
|
|
2582
|
+
);
|
|
2583
|
+
projectRoleNotice(project.projectRole);
|
|
2584
|
+
}
|
|
2585
|
+
return;
|
|
2586
|
+
}
|
|
2587
|
+
if (!target && !options.project && !options.organization && !options.env && !options.active) {
|
|
2588
|
+
const configV2 = readProjectConfigV2();
|
|
2589
|
+
const hasMultipleProjects = configV2 && configV2.projects.length > 1;
|
|
2590
|
+
const choices = [];
|
|
2591
|
+
if (hasMultipleProjects) {
|
|
2592
|
+
choices.push({
|
|
2593
|
+
name: "Active project",
|
|
2594
|
+
value: "active"
|
|
2595
|
+
});
|
|
2596
|
+
}
|
|
2597
|
+
choices.push(
|
|
2598
|
+
{ name: "Environment", value: "environment" },
|
|
2599
|
+
{ name: "Project", value: "project" },
|
|
2600
|
+
{ name: "Organization", value: "organization" }
|
|
2601
|
+
);
|
|
2602
|
+
const { switchType } = await inquirer4.prompt([
|
|
2603
|
+
{
|
|
2604
|
+
type: "list",
|
|
2605
|
+
name: "switchType",
|
|
2606
|
+
message: "What would you like to switch?",
|
|
2607
|
+
choices
|
|
2608
|
+
}
|
|
2609
|
+
]);
|
|
2610
|
+
if (switchType === "active" && configV2) {
|
|
2611
|
+
const { projectId } = await inquirer4.prompt([
|
|
2612
|
+
{
|
|
2613
|
+
type: "list",
|
|
2614
|
+
name: "projectId",
|
|
2615
|
+
message: "Select active project:",
|
|
2616
|
+
choices: configV2.projects.map((p) => {
|
|
2617
|
+
const isActive = p.projectId === configV2.activeProjectId;
|
|
2618
|
+
return {
|
|
2619
|
+
name: `${p.projectName || p.projectId} (${p.environment})${isActive ? chalk8.green(" *current") : ""}`,
|
|
2620
|
+
value: p.projectId
|
|
2621
|
+
};
|
|
2622
|
+
}),
|
|
2623
|
+
default: configV2.activeProjectId
|
|
2624
|
+
}
|
|
2625
|
+
]);
|
|
2626
|
+
const selected = configV2.projects.find(
|
|
2627
|
+
(p) => p.projectId === projectId
|
|
2628
|
+
);
|
|
2629
|
+
const updated = setActiveProjectInConfig(configV2, projectId);
|
|
2630
|
+
writeProjectConfigV2(updated);
|
|
2631
|
+
setActiveProjectId(projectId);
|
|
2632
|
+
setActiveOrganizationId(selected.organizationId);
|
|
2633
|
+
success(
|
|
2634
|
+
`Active project: ${chalk8.bold(selected.projectName || selected.projectId)}`
|
|
2635
|
+
);
|
|
2636
|
+
return;
|
|
2637
|
+
}
|
|
2638
|
+
if (switchType === "environment") {
|
|
2639
|
+
if (!projectConfig) {
|
|
2640
|
+
error("No project initialized. Run `envpilot init` first.");
|
|
2641
|
+
process.exit(1);
|
|
2642
|
+
}
|
|
2643
|
+
const { environment } = await inquirer4.prompt([
|
|
2644
|
+
{
|
|
2645
|
+
type: "list",
|
|
2646
|
+
name: "environment",
|
|
2647
|
+
message: "Select environment:",
|
|
2648
|
+
choices: [
|
|
2649
|
+
{ name: "Development", value: "development" },
|
|
2650
|
+
{ name: "Staging", value: "staging" },
|
|
2651
|
+
{ name: "Production", value: "production" }
|
|
2652
|
+
],
|
|
2653
|
+
default: projectConfig.environment
|
|
2654
|
+
}
|
|
2655
|
+
]);
|
|
2656
|
+
updateProjectConfig({ environment });
|
|
2657
|
+
success(`Switched to ${chalk8.bold(environment)} environment`);
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
if (switchType === "organization" || switchType === "project") {
|
|
2661
|
+
const organizations = await withSpinner(
|
|
2662
|
+
"Fetching organizations...",
|
|
2663
|
+
async () => {
|
|
2664
|
+
const response = await api.get("/api/cli/organizations");
|
|
2665
|
+
return response.data || [];
|
|
2666
|
+
}
|
|
2667
|
+
);
|
|
2668
|
+
if (organizations.length === 0) {
|
|
2669
|
+
error("No organizations found.");
|
|
2670
|
+
process.exit(1);
|
|
2671
|
+
}
|
|
2672
|
+
const { orgId } = await inquirer4.prompt([
|
|
2673
|
+
{
|
|
2674
|
+
type: "list",
|
|
2675
|
+
name: "orgId",
|
|
2676
|
+
message: "Select an organization:",
|
|
2677
|
+
choices: organizations.map((org) => ({
|
|
2678
|
+
name: `${org.name} ${org.tier === "pro" ? chalk8.green("(Pro)") : chalk8.dim("(Free)")}`,
|
|
2679
|
+
value: org._id
|
|
2680
|
+
})),
|
|
2681
|
+
default: projectConfig?.organizationId
|
|
2682
|
+
}
|
|
2683
|
+
]);
|
|
2684
|
+
if (switchType === "organization") {
|
|
2685
|
+
setActiveOrganizationId(orgId);
|
|
2686
|
+
const org = organizations.find((o) => o._id === orgId);
|
|
2687
|
+
if (org.role) {
|
|
2688
|
+
setRole(org.role);
|
|
2689
|
+
}
|
|
2690
|
+
success(`Switched to organization: ${chalk8.bold(org.name)}`);
|
|
2691
|
+
if (org.role) {
|
|
2692
|
+
console.log(chalk8.dim(` Role: ${formatRole(org.role)}`));
|
|
2693
|
+
roleNotice(org.role);
|
|
2694
|
+
}
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2697
|
+
const selectedOrg = organizations.find((o) => o._id === orgId);
|
|
2698
|
+
if (selectedOrg?.role) {
|
|
2699
|
+
setRole(selectedOrg.role);
|
|
2700
|
+
}
|
|
2701
|
+
const projects = await withSpinner(
|
|
2702
|
+
"Fetching projects...",
|
|
2703
|
+
async () => {
|
|
2704
|
+
const response = await api.get("/api/cli/projects", { organizationId: orgId });
|
|
2705
|
+
return response.data || [];
|
|
2706
|
+
}
|
|
2707
|
+
);
|
|
2708
|
+
if (projects.length === 0) {
|
|
2709
|
+
error("No projects found in this organization.");
|
|
2710
|
+
process.exit(1);
|
|
2711
|
+
}
|
|
2712
|
+
const { projectId } = await inquirer4.prompt([
|
|
2713
|
+
{
|
|
2714
|
+
type: "list",
|
|
2715
|
+
name: "projectId",
|
|
2716
|
+
message: "Select a project:",
|
|
2717
|
+
choices: projects.map((project2) => ({
|
|
2718
|
+
name: `${project2.icon || "\u{1F4E6}"} ${project2.name}`,
|
|
2719
|
+
value: project2._id
|
|
2720
|
+
})),
|
|
2721
|
+
default: projectConfig?.projectId
|
|
2722
|
+
}
|
|
2723
|
+
]);
|
|
2724
|
+
const project = projects.find((p) => p._id === projectId);
|
|
2725
|
+
const environment = projectConfig?.environment || "development";
|
|
2726
|
+
setActiveProjectId(projectId);
|
|
2727
|
+
setActiveOrganizationId(orgId);
|
|
2728
|
+
writeProjectConfig({
|
|
2729
|
+
projectId,
|
|
2730
|
+
organizationId: orgId,
|
|
2731
|
+
environment
|
|
2732
|
+
});
|
|
2733
|
+
success(`Switched to project: ${chalk8.bold(project.name)}`);
|
|
2734
|
+
if (project.projectRole) {
|
|
2735
|
+
console.log(
|
|
2736
|
+
chalk8.dim(
|
|
2737
|
+
` Project role: ${formatProjectRole(project.projectRole)}`
|
|
2738
|
+
)
|
|
2739
|
+
);
|
|
2740
|
+
projectRoleNotice(project.projectRole);
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
} catch (err) {
|
|
2745
|
+
await handleError(err);
|
|
2746
|
+
}
|
|
2747
|
+
});
|
|
2748
|
+
|
|
2749
|
+
// src/commands/list.ts
|
|
2750
|
+
import { Command as Command6 } from "commander";
|
|
2751
|
+
import chalk9 from "chalk";
|
|
2752
|
+
var listCommand = new Command6("list").description("List resources").argument(
|
|
2753
|
+
"[resource]",
|
|
2754
|
+
"Resource type: projects, organizations, variables, linked",
|
|
2755
|
+
"projects"
|
|
2756
|
+
).option("-o, --organization <id>", "Organization ID (for projects/variables)").option("-p, --project <id>", "Project ID (for variables)").option("-e, --env <environment>", "Environment filter (for variables)").option("-t, --tag <name>", "Filter by tag name (for variables)").option("--show-values", "Show actual variable values (masked by default)").option("--json", "Output as JSON").action(async (resource, options) => {
|
|
2757
|
+
try {
|
|
2758
|
+
if (!isAuthenticated()) {
|
|
2759
|
+
throw notAuthenticated();
|
|
2760
|
+
}
|
|
2761
|
+
const api = createAPIClient();
|
|
2762
|
+
const projectConfig = readProjectConfig();
|
|
2763
|
+
switch (resource) {
|
|
2764
|
+
case "orgs":
|
|
2765
|
+
case "organizations":
|
|
2766
|
+
await listOrganizations(api, options);
|
|
2767
|
+
break;
|
|
2768
|
+
case "projects":
|
|
2769
|
+
await listProjects(api, projectConfig, options);
|
|
2770
|
+
break;
|
|
2771
|
+
case "vars":
|
|
2772
|
+
case "variables":
|
|
2773
|
+
await listVariables(api, projectConfig, options);
|
|
2774
|
+
break;
|
|
2775
|
+
case "linked":
|
|
2776
|
+
listLinked();
|
|
2777
|
+
break;
|
|
2778
|
+
default:
|
|
2779
|
+
error(`Unknown resource: ${resource}`);
|
|
2780
|
+
console.log();
|
|
2781
|
+
console.log("Available resources:");
|
|
2782
|
+
console.log(" organizations (orgs) List your organizations");
|
|
2783
|
+
console.log(
|
|
2784
|
+
" projects List projects in an organization"
|
|
2785
|
+
);
|
|
2786
|
+
console.log(" variables (vars) List variables in a project");
|
|
2787
|
+
console.log(
|
|
2788
|
+
" linked List projects linked in this directory"
|
|
2789
|
+
);
|
|
2790
|
+
process.exit(1);
|
|
2791
|
+
}
|
|
2792
|
+
} catch (err) {
|
|
2793
|
+
await handleError(err);
|
|
2794
|
+
}
|
|
2795
|
+
});
|
|
2796
|
+
function listLinked() {
|
|
2797
|
+
const configV2 = readProjectConfigV2();
|
|
2798
|
+
if (!configV2) {
|
|
2799
|
+
info("No projects linked. Run `envpilot init` to get started.");
|
|
2800
|
+
return;
|
|
2801
|
+
}
|
|
2802
|
+
header(`Linked Projects (${configV2.projects.length})`);
|
|
2803
|
+
console.log();
|
|
2804
|
+
for (const project of configV2.projects) {
|
|
2805
|
+
const isActive = project.projectId === configV2.activeProjectId;
|
|
2806
|
+
const marker = isActive ? chalk9.green("*") : " ";
|
|
2807
|
+
const envFile = getEnvPathForEnvironment(project.environment);
|
|
2808
|
+
console.log(
|
|
2809
|
+
` ${marker} ${chalk9.bold(project.projectName || project.projectId)} ${chalk9.dim(`(${project.organizationName || project.organizationId})`)}`
|
|
2810
|
+
);
|
|
2811
|
+
console.log(` ${project.environment} ${chalk9.dim("\u2192")} ${envFile}`);
|
|
2812
|
+
console.log();
|
|
2813
|
+
}
|
|
2814
|
+
if (configV2.projects.length > 1) {
|
|
2815
|
+
console.log(chalk9.dim(" (* = active project)"));
|
|
2816
|
+
console.log();
|
|
2817
|
+
console.log(
|
|
2818
|
+
chalk9.dim(
|
|
2819
|
+
' Use `envpilot switch --active "<name>"` to change the active project'
|
|
2820
|
+
)
|
|
2821
|
+
);
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
async function listOrganizations(api, options) {
|
|
2825
|
+
const organizations = await withSpinner(
|
|
2826
|
+
"Fetching organizations...",
|
|
2827
|
+
async () => {
|
|
2828
|
+
const response = await api.get("/api/cli/organizations");
|
|
2829
|
+
return response.data || [];
|
|
2830
|
+
}
|
|
2831
|
+
);
|
|
2832
|
+
if (organizations.length === 0) {
|
|
2833
|
+
info("No organizations found.");
|
|
2834
|
+
return;
|
|
2835
|
+
}
|
|
2836
|
+
if (options.json) {
|
|
2837
|
+
console.log(JSON.stringify(organizations, null, 2));
|
|
2838
|
+
return;
|
|
2839
|
+
}
|
|
2840
|
+
header("Organizations");
|
|
2841
|
+
console.log();
|
|
2842
|
+
table(
|
|
2843
|
+
organizations.map((org) => ({
|
|
2844
|
+
name: org.name,
|
|
2845
|
+
slug: org.slug,
|
|
2846
|
+
tier: org.tier === "pro" ? chalk9.green("Pro") : chalk9.dim("Free"),
|
|
2847
|
+
role: org.role
|
|
2848
|
+
})),
|
|
2849
|
+
[
|
|
2850
|
+
{ key: "name", header: "Name" },
|
|
2851
|
+
{ key: "slug", header: "Slug" },
|
|
2852
|
+
{ key: "tier", header: "Tier" },
|
|
2853
|
+
{ key: "role", header: "Role" }
|
|
2854
|
+
]
|
|
2855
|
+
);
|
|
2856
|
+
}
|
|
2857
|
+
async function listProjects(api, projectConfig, options) {
|
|
2858
|
+
let organizationId = options.organization || projectConfig?.organizationId;
|
|
2859
|
+
if (!organizationId) {
|
|
2860
|
+
const organizations = await withSpinner(
|
|
2861
|
+
"Fetching organizations...",
|
|
2862
|
+
async () => {
|
|
2863
|
+
const response = await api.get("/api/cli/organizations");
|
|
2864
|
+
return response.data || [];
|
|
2865
|
+
}
|
|
2866
|
+
);
|
|
2867
|
+
if (organizations.length === 0) {
|
|
2868
|
+
info("No organizations found.");
|
|
2869
|
+
return;
|
|
2870
|
+
}
|
|
2871
|
+
if (organizations.length === 1) {
|
|
2872
|
+
organizationId = organizations[0]._id;
|
|
2873
|
+
} else {
|
|
2874
|
+
info("Multiple organizations found. Use --organization to specify one.");
|
|
2875
|
+
console.log();
|
|
2876
|
+
for (const org of organizations) {
|
|
2877
|
+
console.log(` ${org.name} (${org.slug}): --organization ${org._id}`);
|
|
2878
|
+
}
|
|
2879
|
+
return;
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
const projects = await withSpinner("Fetching projects...", async () => {
|
|
2883
|
+
const response = await api.get(
|
|
2884
|
+
"/api/cli/projects",
|
|
2885
|
+
{ organizationId }
|
|
2886
|
+
);
|
|
2887
|
+
return response.data || [];
|
|
2888
|
+
});
|
|
2889
|
+
if (projects.length === 0) {
|
|
2890
|
+
info("No projects found.");
|
|
2891
|
+
return;
|
|
2892
|
+
}
|
|
2893
|
+
if (options.json) {
|
|
2894
|
+
console.log(JSON.stringify(projects, null, 2));
|
|
2895
|
+
return;
|
|
2896
|
+
}
|
|
2897
|
+
header("Projects");
|
|
2898
|
+
console.log();
|
|
2899
|
+
table(
|
|
2900
|
+
projects.map((project) => ({
|
|
2901
|
+
icon: project.icon || "\u{1F4E6}",
|
|
2902
|
+
name: project.name,
|
|
2903
|
+
slug: project.slug,
|
|
2904
|
+
description: project.description || chalk9.dim("-"),
|
|
2905
|
+
role: project.userRole === "admin" ? formatRole("admin") : formatProjectRole(project.projectRole),
|
|
2906
|
+
active: projectConfig?.projectId === project._id ? chalk9.green("\u2713") : ""
|
|
2907
|
+
})),
|
|
2908
|
+
[
|
|
2909
|
+
{ key: "icon", header: "" },
|
|
2910
|
+
{ key: "name", header: "Name" },
|
|
2911
|
+
{ key: "slug", header: "Slug" },
|
|
2912
|
+
{ key: "description", header: "Description", width: 30 },
|
|
2913
|
+
{ key: "role", header: "Role" },
|
|
2914
|
+
{ key: "active", header: "" }
|
|
2915
|
+
]
|
|
2916
|
+
);
|
|
2917
|
+
const role = getRole();
|
|
2918
|
+
if (role) {
|
|
2919
|
+
console.log();
|
|
2920
|
+
console.log(chalk9.dim(`Your org role: ${formatRole(role)}`));
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
async function listVariables(api, projectConfig, options) {
|
|
2924
|
+
const projectId = options.project || projectConfig?.projectId;
|
|
2925
|
+
const environment = options.env || projectConfig?.environment;
|
|
2926
|
+
if (!projectId) {
|
|
2927
|
+
error("No project specified. Use --project or run `envpilot init` first.");
|
|
2928
|
+
process.exit(1);
|
|
2929
|
+
}
|
|
2930
|
+
let metaProjectRole;
|
|
2931
|
+
const variables = await withSpinner("Fetching variables...", async () => {
|
|
2932
|
+
const params = { projectId };
|
|
2933
|
+
if (environment) {
|
|
2934
|
+
params.environment = environment;
|
|
2935
|
+
}
|
|
2936
|
+
const response = await api.get("/api/cli/variables", params);
|
|
2937
|
+
metaProjectRole = response.meta?.projectRole;
|
|
2938
|
+
return response.data || [];
|
|
2939
|
+
});
|
|
2940
|
+
const tagFilter = options.tag?.toLowerCase();
|
|
2941
|
+
const filtered = tagFilter ? variables.filter(
|
|
2942
|
+
(v) => v.tags?.some((t) => t.name.toLowerCase() === tagFilter)
|
|
2943
|
+
) : variables;
|
|
2944
|
+
if (filtered.length === 0) {
|
|
2945
|
+
info(
|
|
2946
|
+
`No variables found${environment ? ` for ${environment}` : ""}${tagFilter ? ` with tag "${options.tag}"` : ""}.`
|
|
2947
|
+
);
|
|
2948
|
+
return;
|
|
2949
|
+
}
|
|
2950
|
+
if (options.json) {
|
|
2951
|
+
const output = filtered.map((v) => ({
|
|
2952
|
+
...v,
|
|
2953
|
+
value: options.showValues ? v.value : maskValue(v.value)
|
|
2954
|
+
}));
|
|
2955
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2956
|
+
return;
|
|
2957
|
+
}
|
|
2958
|
+
header(
|
|
2959
|
+
`Variables${environment ? ` (${environment})` : ""}${tagFilter ? ` [tag: ${options.tag}]` : ""}`
|
|
2960
|
+
);
|
|
2961
|
+
console.log();
|
|
2962
|
+
const hasTags = filtered.some((v) => v.tags && v.tags.length > 0);
|
|
2963
|
+
table(
|
|
2964
|
+
filtered.map((variable) => ({
|
|
2965
|
+
key: variable.key,
|
|
2966
|
+
value: options.showValues ? variable.value : maskValue(variable.value),
|
|
2967
|
+
sensitive: variable.isSensitive ? chalk9.yellow("\u25CF") : "",
|
|
2968
|
+
tags: hasTags ? variable.tags?.map((t) => t.name).join(", ") || chalk9.dim("-") : "",
|
|
2969
|
+
version: `v${variable.version}`
|
|
2970
|
+
})),
|
|
2971
|
+
[
|
|
2972
|
+
{ key: "key", header: "Key" },
|
|
2973
|
+
{ key: "value", header: "Value", width: 40 },
|
|
2974
|
+
{ key: "sensitive", header: "" },
|
|
2975
|
+
...hasTags ? [{ key: "tags", header: "Tags", width: 25 }] : [],
|
|
2976
|
+
{ key: "version", header: "Ver" }
|
|
2977
|
+
]
|
|
2978
|
+
);
|
|
2979
|
+
console.log();
|
|
2980
|
+
console.log(chalk9.dim(`Total: ${filtered.length} variables`));
|
|
2981
|
+
const role = getRole();
|
|
2982
|
+
if (role) {
|
|
2983
|
+
console.log(chalk9.dim(`Your org role: ${formatRole(role)}`));
|
|
2984
|
+
}
|
|
2985
|
+
if (metaProjectRole) {
|
|
2986
|
+
console.log(
|
|
2987
|
+
chalk9.dim(`Your project role: ${formatProjectRole(metaProjectRole)}`)
|
|
2988
|
+
);
|
|
2989
|
+
}
|
|
2990
|
+
if (role === "member" || metaProjectRole === "viewer") {
|
|
2991
|
+
console.log(
|
|
2992
|
+
chalk9.dim("You may only see variables you have been granted access to.")
|
|
2993
|
+
);
|
|
2994
|
+
}
|
|
2995
|
+
if (!options.showValues) {
|
|
2996
|
+
console.log(chalk9.dim("Use --show-values to see actual values"));
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
// src/commands/config.ts
|
|
3001
|
+
import { Command as Command7 } from "commander";
|
|
3002
|
+
import chalk10 from "chalk";
|
|
3003
|
+
var configCommand = new Command7("config").description("Manage CLI configuration").argument("[action]", "Action: get, set, list, path, reset").argument("[key]", "Config key (for get/set)").argument("[value]", "Config value (for set)").action(async (action, key, value) => {
|
|
3004
|
+
try {
|
|
3005
|
+
switch (action) {
|
|
3006
|
+
case "get":
|
|
3007
|
+
await handleGet(key);
|
|
3008
|
+
break;
|
|
3009
|
+
case "set":
|
|
3010
|
+
await handleSet(key, value);
|
|
3011
|
+
break;
|
|
3012
|
+
case "list":
|
|
3013
|
+
case void 0:
|
|
3014
|
+
await handleList();
|
|
3015
|
+
break;
|
|
3016
|
+
case "path":
|
|
3017
|
+
await handlePath();
|
|
3018
|
+
break;
|
|
3019
|
+
case "reset":
|
|
3020
|
+
await handleReset();
|
|
3021
|
+
break;
|
|
3022
|
+
default:
|
|
3023
|
+
error(`Unknown action: ${action}`);
|
|
3024
|
+
console.log();
|
|
3025
|
+
console.log("Available actions:");
|
|
3026
|
+
console.log(" list Show all configuration");
|
|
3027
|
+
console.log(" get <key> Get a specific config value");
|
|
3028
|
+
console.log(" set <key> <value> Set a config value");
|
|
3029
|
+
console.log(" path Show config file locations");
|
|
3030
|
+
console.log(" reset Reset all configuration");
|
|
3031
|
+
process.exit(1);
|
|
3032
|
+
}
|
|
3033
|
+
} catch (err) {
|
|
3034
|
+
await handleError(err);
|
|
3035
|
+
}
|
|
3036
|
+
});
|
|
3037
|
+
async function handleGet(key) {
|
|
3038
|
+
if (!key) {
|
|
3039
|
+
error("Missing key. Usage: envpilot config get <key>");
|
|
3040
|
+
console.log();
|
|
3041
|
+
console.log("Available keys:");
|
|
3042
|
+
console.log(" apiUrl API endpoint URL");
|
|
3043
|
+
console.log(" user Current authenticated user");
|
|
3044
|
+
console.log(" activeProjectId Currently active project");
|
|
3045
|
+
console.log(" activeOrganizationId Currently active organization");
|
|
3046
|
+
process.exit(1);
|
|
3047
|
+
}
|
|
3048
|
+
const config2 = getConfig();
|
|
3049
|
+
switch (key) {
|
|
3050
|
+
case "apiUrl":
|
|
3051
|
+
console.log(config2.apiUrl);
|
|
3052
|
+
break;
|
|
3053
|
+
case "user":
|
|
3054
|
+
if (config2.user) {
|
|
3055
|
+
console.log(JSON.stringify(config2.user, null, 2));
|
|
3056
|
+
} else {
|
|
3057
|
+
console.log(chalk10.dim("(not set)"));
|
|
3058
|
+
}
|
|
3059
|
+
break;
|
|
3060
|
+
case "activeProjectId":
|
|
3061
|
+
console.log(config2.activeProjectId || chalk10.dim("(not set)"));
|
|
3062
|
+
break;
|
|
3063
|
+
case "activeOrganizationId":
|
|
3064
|
+
console.log(config2.activeOrganizationId || chalk10.dim("(not set)"));
|
|
3065
|
+
break;
|
|
3066
|
+
default:
|
|
3067
|
+
error(`Unknown key: ${key}`);
|
|
3068
|
+
process.exit(1);
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
async function handleSet(key, value) {
|
|
3072
|
+
if (!key || value === void 0) {
|
|
3073
|
+
error("Missing key or value. Usage: envpilot config set <key> <value>");
|
|
3074
|
+
console.log();
|
|
3075
|
+
console.log("Settable keys:");
|
|
3076
|
+
console.log(" apiUrl API endpoint URL");
|
|
3077
|
+
process.exit(1);
|
|
3078
|
+
}
|
|
3079
|
+
switch (key) {
|
|
3080
|
+
case "apiUrl":
|
|
3081
|
+
try {
|
|
3082
|
+
new URL(value);
|
|
3083
|
+
} catch {
|
|
3084
|
+
error("Invalid URL format");
|
|
3085
|
+
process.exit(1);
|
|
3086
|
+
}
|
|
3087
|
+
setApiUrl(value);
|
|
3088
|
+
success(`Set apiUrl to ${value}`);
|
|
3089
|
+
break;
|
|
3090
|
+
default:
|
|
3091
|
+
error(`Cannot set key: ${key}`);
|
|
3092
|
+
console.log();
|
|
3093
|
+
console.log("Settable keys:");
|
|
3094
|
+
console.log(" apiUrl API endpoint URL");
|
|
3095
|
+
process.exit(1);
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
async function handleList() {
|
|
3099
|
+
const config2 = getConfig();
|
|
3100
|
+
const projectConfig = readProjectConfig();
|
|
3101
|
+
header("Global Configuration");
|
|
3102
|
+
console.log();
|
|
3103
|
+
keyValue([
|
|
3104
|
+
["API URL", config2.apiUrl],
|
|
3105
|
+
["Authenticated", isAuthenticated() ? chalk10.green("Yes") : chalk10.red("No")],
|
|
3106
|
+
["User", config2.user?.email],
|
|
3107
|
+
["Active Organization", config2.activeOrganizationId],
|
|
3108
|
+
["Active Project", config2.activeProjectId]
|
|
3109
|
+
]);
|
|
3110
|
+
console.log();
|
|
3111
|
+
if (projectConfig) {
|
|
3112
|
+
header("Project Configuration (.envpilot)");
|
|
3113
|
+
console.log();
|
|
3114
|
+
keyValue([
|
|
3115
|
+
["Project ID", projectConfig.projectId],
|
|
3116
|
+
["Organization ID", projectConfig.organizationId],
|
|
3117
|
+
["Environment", projectConfig.environment]
|
|
3118
|
+
]);
|
|
3119
|
+
console.log();
|
|
3120
|
+
} else {
|
|
3121
|
+
info("No project configuration found in current directory.");
|
|
3122
|
+
console.log();
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
async function handlePath() {
|
|
3126
|
+
header("Configuration Paths");
|
|
3127
|
+
console.log();
|
|
3128
|
+
keyValue([
|
|
3129
|
+
["Global config", getConfigPath()],
|
|
3130
|
+
["Project config", getProjectConfigPath()]
|
|
3131
|
+
]);
|
|
3132
|
+
}
|
|
3133
|
+
async function handleReset() {
|
|
3134
|
+
const inquirer6 = await import("inquirer");
|
|
3135
|
+
const { confirm } = await inquirer6.default.prompt([
|
|
3136
|
+
{
|
|
3137
|
+
type: "confirm",
|
|
3138
|
+
name: "confirm",
|
|
3139
|
+
message: "Are you sure you want to reset all configuration? This will log you out.",
|
|
3140
|
+
default: false
|
|
3141
|
+
}
|
|
3142
|
+
]);
|
|
3143
|
+
if (!confirm) {
|
|
3144
|
+
info("Reset cancelled.");
|
|
3145
|
+
return;
|
|
3146
|
+
}
|
|
3147
|
+
clearConfig();
|
|
3148
|
+
success("Configuration reset.");
|
|
3149
|
+
}
|
|
3150
|
+
|
|
3151
|
+
// src/commands/logout.ts
|
|
3152
|
+
import { Command as Command8 } from "commander";
|
|
3153
|
+
var logoutCommand = new Command8("logout").description("Log out from Envpilot").action(async () => {
|
|
3154
|
+
try {
|
|
3155
|
+
if (!isAuthenticated()) {
|
|
3156
|
+
info("You are not logged in.");
|
|
3157
|
+
return;
|
|
3158
|
+
}
|
|
3159
|
+
const user = getUser();
|
|
3160
|
+
const api = createAPIClient();
|
|
3161
|
+
try {
|
|
3162
|
+
await api.post("/api/cli/auth?action=revoke", {});
|
|
3163
|
+
} catch {
|
|
3164
|
+
}
|
|
3165
|
+
clearAuth();
|
|
3166
|
+
success(`Logged out${user?.email ? ` from ${user.email}` : ""}`);
|
|
3167
|
+
} catch (err) {
|
|
3168
|
+
await handleError(err);
|
|
3169
|
+
}
|
|
3170
|
+
});
|
|
3171
|
+
|
|
3172
|
+
// src/commands/unlink.ts
|
|
3173
|
+
import { Command as Command9 } from "commander";
|
|
3174
|
+
import chalk11 from "chalk";
|
|
3175
|
+
import inquirer5 from "inquirer";
|
|
3176
|
+
var unlinkCommand = new Command9("unlink").description("Remove a linked project from this directory").argument("[project]", "Project name or ID to unlink").option("--force", "Skip confirmation").action(async (projectArg, options) => {
|
|
3177
|
+
try {
|
|
3178
|
+
if (!isAuthenticated()) {
|
|
3179
|
+
throw notAuthenticated();
|
|
3180
|
+
}
|
|
3181
|
+
const config2 = readProjectConfigV2();
|
|
3182
|
+
if (!config2 || config2.projects.length === 0) {
|
|
3183
|
+
error("No projects linked. Run `envpilot init` first.");
|
|
3184
|
+
process.exit(1);
|
|
3185
|
+
}
|
|
3186
|
+
let targetProject;
|
|
3187
|
+
if (projectArg) {
|
|
3188
|
+
targetProject = resolveProject(config2, projectArg);
|
|
3189
|
+
if (!targetProject) {
|
|
3190
|
+
error(`Project not found: ${projectArg}`);
|
|
3191
|
+
console.log();
|
|
3192
|
+
console.log("Linked projects:");
|
|
3193
|
+
for (const p of config2.projects) {
|
|
3194
|
+
console.log(
|
|
3195
|
+
` ${p.projectName || p.projectId} (${p.organizationName || p.organizationId})`
|
|
3196
|
+
);
|
|
3197
|
+
}
|
|
3198
|
+
process.exit(1);
|
|
3199
|
+
}
|
|
3200
|
+
} else if (config2.projects.length > 1) {
|
|
3201
|
+
const { projectId } = await inquirer5.prompt([
|
|
3202
|
+
{
|
|
3203
|
+
type: "list",
|
|
3204
|
+
name: "projectId",
|
|
3205
|
+
message: "Select a project to unlink:",
|
|
3206
|
+
choices: config2.projects.map((p) => {
|
|
3207
|
+
const isActive = p.projectId === config2.activeProjectId;
|
|
3208
|
+
return {
|
|
3209
|
+
name: `${p.projectName || p.projectId} (${p.organizationName || p.organizationId})${isActive ? chalk11.green(" *active") : ""}`,
|
|
3210
|
+
value: p.projectId
|
|
3211
|
+
};
|
|
3212
|
+
})
|
|
3213
|
+
}
|
|
3214
|
+
]);
|
|
3215
|
+
targetProject = config2.projects.find((p) => p.projectId === projectId);
|
|
3216
|
+
} else {
|
|
3217
|
+
targetProject = config2.projects[0];
|
|
3218
|
+
}
|
|
3219
|
+
const displayName = targetProject.projectName || targetProject.projectId;
|
|
3220
|
+
if (!options.force) {
|
|
3221
|
+
const { proceed } = await inquirer5.prompt([
|
|
3222
|
+
{
|
|
3223
|
+
type: "confirm",
|
|
3224
|
+
name: "proceed",
|
|
3225
|
+
message: `Unlink "${displayName}"? Your .env files won't be deleted.`,
|
|
3226
|
+
default: false
|
|
3227
|
+
}
|
|
3228
|
+
]);
|
|
3229
|
+
if (!proceed) {
|
|
3230
|
+
info("Unlink cancelled.");
|
|
3231
|
+
return;
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
const updated = removeProjectFromConfig(config2, targetProject.projectId);
|
|
3235
|
+
if (!updated) {
|
|
3236
|
+
deleteProjectConfig();
|
|
3237
|
+
success(`Unlinked "${displayName}". No projects remaining.`);
|
|
3238
|
+
info("Run `envpilot init` to link a new project.");
|
|
3239
|
+
} else {
|
|
3240
|
+
writeProjectConfigV2(updated);
|
|
3241
|
+
const newActive = getActiveProject(updated);
|
|
3242
|
+
if (newActive) {
|
|
3243
|
+
setActiveProjectId(newActive.projectId);
|
|
3244
|
+
}
|
|
3245
|
+
success(`Unlinked "${displayName}".`);
|
|
3246
|
+
if (config2.activeProjectId === targetProject.projectId && newActive) {
|
|
3247
|
+
info(
|
|
3248
|
+
`Active project switched to "${newActive.projectName || newActive.projectId}".`
|
|
3249
|
+
);
|
|
3250
|
+
}
|
|
3251
|
+
console.log(
|
|
3252
|
+
chalk11.dim(
|
|
3253
|
+
` ${updated.projects.length} project${updated.projects.length !== 1 ? "s" : ""} remaining`
|
|
3254
|
+
)
|
|
3255
|
+
);
|
|
3256
|
+
}
|
|
3257
|
+
} catch (err) {
|
|
3258
|
+
await handleError(err);
|
|
3259
|
+
}
|
|
3260
|
+
});
|
|
3261
|
+
|
|
3262
|
+
// src/commands/sync.ts
|
|
3263
|
+
import { Command as Command10 } from "commander";
|
|
3264
|
+
import chalk12 from "chalk";
|
|
3265
|
+
|
|
3266
|
+
// src/lib/commit-guard.ts
|
|
3267
|
+
import {
|
|
3268
|
+
existsSync as existsSync3,
|
|
3269
|
+
readFileSync as readFileSync3,
|
|
3270
|
+
writeFileSync as writeFileSync3,
|
|
3271
|
+
mkdirSync,
|
|
3272
|
+
chmodSync as chmodSync2,
|
|
3273
|
+
unlinkSync as unlinkSync2,
|
|
3274
|
+
statSync
|
|
3275
|
+
} from "fs";
|
|
3276
|
+
import { join as join3, resolve as resolve2 } from "path";
|
|
3277
|
+
import { execSync as execSync2 } from "child_process";
|
|
3278
|
+
var HOOK_START_MARKER = "# ENVPILOT_GUARD_START";
|
|
3279
|
+
var HOOK_END_MARKER = "# ENVPILOT_GUARD_END";
|
|
3280
|
+
var HOOK_BLOCK = `${HOOK_START_MARKER} - Do not remove. Installed by Envpilot CLI.
|
|
3281
|
+
ENV_FILES=$(git diff --cached --name-only | grep -E '(^|/)\\.env($|\\.)' || true)
|
|
3282
|
+
if [ -n "$ENV_FILES" ]; then
|
|
3283
|
+
echo ""
|
|
3284
|
+
echo "\\033[1;31mERROR:\\033[0m Envpilot commit guard blocked this commit."
|
|
3285
|
+
echo ""
|
|
3286
|
+
echo "The following .env files were staged:"
|
|
3287
|
+
echo "$ENV_FILES" | while IFS= read -r f; do echo " - $f"; done
|
|
3288
|
+
echo ""
|
|
3289
|
+
echo "Remove them with: git reset HEAD <file>"
|
|
3290
|
+
echo "To bypass (not recommended): git commit --no-verify"
|
|
3291
|
+
exit 1
|
|
3292
|
+
fi
|
|
3293
|
+
${HOOK_END_MARKER}`;
|
|
3294
|
+
function findGitRoot(startDir) {
|
|
3295
|
+
let dir = startDir || process.cwd();
|
|
3296
|
+
while (true) {
|
|
3297
|
+
if (existsSync3(join3(dir, ".git"))) {
|
|
3298
|
+
return dir;
|
|
3299
|
+
}
|
|
3300
|
+
const parent = resolve2(dir, "..");
|
|
3301
|
+
if (parent === dir) {
|
|
3302
|
+
return null;
|
|
3303
|
+
}
|
|
3304
|
+
dir = parent;
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
function resolveGitDir(repoRoot) {
|
|
3308
|
+
const gitPath = join3(repoRoot, ".git");
|
|
3309
|
+
try {
|
|
3310
|
+
const stat = statSync(gitPath);
|
|
3311
|
+
if (stat.isDirectory()) {
|
|
3312
|
+
return gitPath;
|
|
3313
|
+
}
|
|
3314
|
+
} catch {
|
|
3315
|
+
return gitPath;
|
|
3316
|
+
}
|
|
3317
|
+
try {
|
|
3318
|
+
const content = readFileSync3(gitPath, "utf-8").trim();
|
|
3319
|
+
const match = content.match(/^gitdir:\s*(.+)$/);
|
|
3320
|
+
if (match) {
|
|
3321
|
+
const gitdir = resolve2(repoRoot, match[1]);
|
|
3322
|
+
const commonDir = resolve2(gitdir, "..", "..");
|
|
3323
|
+
if (existsSync3(join3(commonDir, "hooks")) || existsSync3(commonDir)) {
|
|
3324
|
+
return commonDir;
|
|
3325
|
+
}
|
|
3326
|
+
return gitdir;
|
|
3327
|
+
}
|
|
3328
|
+
} catch {
|
|
3329
|
+
}
|
|
3330
|
+
return gitPath;
|
|
3331
|
+
}
|
|
3332
|
+
function getHooksDir(repoRoot) {
|
|
3333
|
+
try {
|
|
3334
|
+
const customPath = execSync2("git config core.hooksPath", {
|
|
3335
|
+
cwd: repoRoot,
|
|
3336
|
+
encoding: "utf-8",
|
|
3337
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3338
|
+
}).trim();
|
|
3339
|
+
if (customPath) {
|
|
3340
|
+
return resolve2(repoRoot, customPath);
|
|
3341
|
+
}
|
|
3342
|
+
} catch {
|
|
3343
|
+
}
|
|
3344
|
+
const gitDir = resolveGitDir(repoRoot);
|
|
3345
|
+
return join3(gitDir, "hooks");
|
|
3346
|
+
}
|
|
3347
|
+
function installCommitGuard(repoRoot) {
|
|
3348
|
+
const root = repoRoot || findGitRoot();
|
|
3349
|
+
if (!root) {
|
|
3350
|
+
return {
|
|
3351
|
+
installed: false,
|
|
3352
|
+
hookPath: null,
|
|
3353
|
+
message: "Not a git repository"
|
|
3354
|
+
};
|
|
3355
|
+
}
|
|
3356
|
+
try {
|
|
3357
|
+
const hooksDir = getHooksDir(root);
|
|
3358
|
+
const hookPath = join3(hooksDir, "pre-commit");
|
|
3359
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
3360
|
+
let existingContent = "";
|
|
3361
|
+
try {
|
|
3362
|
+
existingContent = readFileSync3(hookPath, "utf-8");
|
|
3363
|
+
} catch {
|
|
3364
|
+
}
|
|
3365
|
+
if (existingContent.includes(HOOK_START_MARKER)) {
|
|
3366
|
+
const startIdx = existingContent.indexOf(HOOK_START_MARKER);
|
|
3367
|
+
const endIdx = existingContent.indexOf(HOOK_END_MARKER) + HOOK_END_MARKER.length;
|
|
3368
|
+
const updated = existingContent.substring(0, startIdx) + HOOK_BLOCK + existingContent.substring(endIdx);
|
|
3369
|
+
writeFileSync3(hookPath, updated, "utf-8");
|
|
3370
|
+
chmodSync2(hookPath, 493);
|
|
3371
|
+
return {
|
|
3372
|
+
installed: true,
|
|
3373
|
+
hookPath,
|
|
3374
|
+
message: "Pre-commit hook updated"
|
|
3375
|
+
};
|
|
3376
|
+
}
|
|
3377
|
+
let newContent;
|
|
3378
|
+
if (existingContent.trim()) {
|
|
3379
|
+
newContent = existingContent.trimEnd() + "\n\n" + HOOK_BLOCK + "\n";
|
|
3380
|
+
} else {
|
|
3381
|
+
newContent = "#!/bin/sh\n\n" + HOOK_BLOCK + "\n";
|
|
3382
|
+
}
|
|
3383
|
+
writeFileSync3(hookPath, newContent, "utf-8");
|
|
3384
|
+
chmodSync2(hookPath, 493);
|
|
3385
|
+
return {
|
|
3386
|
+
installed: true,
|
|
3387
|
+
hookPath,
|
|
3388
|
+
message: "Pre-commit hook installed"
|
|
3389
|
+
};
|
|
3390
|
+
} catch (err) {
|
|
3391
|
+
return {
|
|
3392
|
+
installed: false,
|
|
3393
|
+
hookPath: null,
|
|
3394
|
+
message: `Failed to install hook: ${err instanceof Error ? err.message : String(err)}`
|
|
3395
|
+
};
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3399
|
+
// src/commands/sync.ts
|
|
3400
|
+
var syncCommand = new Command10("sync").description(
|
|
3401
|
+
"Login, select project, pull variables, and protect files \u2014 all in one command"
|
|
3402
|
+
).option("-o, --organization <id>", "Organization ID").option("-p, --project <id>", "Project ID or name").option(
|
|
3403
|
+
"-e, --env <environment>",
|
|
3404
|
+
"Environment (development, staging, production)"
|
|
3405
|
+
).option("--force", "Overwrite without confirmation").option("--no-guard", "Skip pre-commit hook installation").action(async (options) => {
|
|
3406
|
+
try {
|
|
3407
|
+
if (!isAuthenticated()) {
|
|
3408
|
+
console.log();
|
|
3409
|
+
info("Not logged in. Starting authentication...");
|
|
3410
|
+
console.log();
|
|
3411
|
+
await performLogin();
|
|
3412
|
+
console.log();
|
|
3413
|
+
} else {
|
|
3414
|
+
info("Already authenticated.");
|
|
3415
|
+
}
|
|
3416
|
+
ensureEnvInGitignore();
|
|
3417
|
+
if (options.guard !== false) {
|
|
3418
|
+
const guardResult = installCommitGuard();
|
|
3419
|
+
if (guardResult.installed) {
|
|
3420
|
+
success(guardResult.message);
|
|
3421
|
+
info("Staged .env files will be blocked from commits.");
|
|
3422
|
+
} else if (guardResult.message !== "Not a git repository") {
|
|
3423
|
+
warning(guardResult.message);
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
console.log();
|
|
3427
|
+
let projectId;
|
|
3428
|
+
let organizationId;
|
|
3429
|
+
let environment;
|
|
3430
|
+
let projectName;
|
|
3431
|
+
let organizationName;
|
|
3432
|
+
const existingConfig = readProjectConfigV2();
|
|
3433
|
+
if (existingConfig && !options.organization && !options.project && !options.env) {
|
|
3434
|
+
const active = getActiveProject(existingConfig);
|
|
3435
|
+
if (active) {
|
|
3436
|
+
projectId = active.projectId;
|
|
3437
|
+
organizationId = active.organizationId;
|
|
3438
|
+
environment = active.environment;
|
|
3439
|
+
projectName = active.projectName;
|
|
3440
|
+
organizationName = active.organizationName;
|
|
3441
|
+
info(
|
|
3442
|
+
`Using project ${chalk12.bold(projectName || projectId)} (${environment})`
|
|
3443
|
+
);
|
|
3444
|
+
} else {
|
|
3445
|
+
const selection = await selectOrgProjectEnv(options);
|
|
3446
|
+
projectId = selection.selectedProject._id;
|
|
3447
|
+
organizationId = selection.selectedOrg._id;
|
|
3448
|
+
environment = selection.selectedEnvironment;
|
|
3449
|
+
projectName = selection.selectedProject.name;
|
|
3450
|
+
organizationName = selection.selectedOrg.name;
|
|
3451
|
+
}
|
|
3452
|
+
} else {
|
|
3453
|
+
const selection = await selectOrgProjectEnv({
|
|
3454
|
+
organization: options.organization,
|
|
3455
|
+
project: options.project,
|
|
3456
|
+
environment: options.env
|
|
3457
|
+
});
|
|
3458
|
+
projectId = selection.selectedProject._id;
|
|
3459
|
+
organizationId = selection.selectedOrg._id;
|
|
3460
|
+
environment = selection.selectedEnvironment;
|
|
3461
|
+
projectName = selection.selectedProject.name;
|
|
3462
|
+
organizationName = selection.selectedOrg.name;
|
|
3463
|
+
if (selection.selectedOrg.role) {
|
|
3464
|
+
setRole(selection.selectedOrg.role);
|
|
3465
|
+
}
|
|
3466
|
+
writeProjectConfigV2({
|
|
3467
|
+
version: 1,
|
|
3468
|
+
activeProjectId: projectId,
|
|
3469
|
+
projects: existingConfig ? [
|
|
3470
|
+
...existingConfig.projects.filter(
|
|
3471
|
+
(p) => p.projectId !== projectId
|
|
3472
|
+
),
|
|
3473
|
+
{
|
|
3474
|
+
projectId,
|
|
3475
|
+
organizationId,
|
|
3476
|
+
projectName,
|
|
3477
|
+
organizationName,
|
|
3478
|
+
environment
|
|
3479
|
+
}
|
|
3480
|
+
] : [
|
|
3481
|
+
{
|
|
3482
|
+
projectId,
|
|
3483
|
+
organizationId,
|
|
3484
|
+
projectName,
|
|
3485
|
+
organizationName,
|
|
3486
|
+
environment
|
|
3487
|
+
}
|
|
3488
|
+
]
|
|
3489
|
+
});
|
|
3490
|
+
setActiveOrganizationId(organizationId);
|
|
3491
|
+
setActiveProjectId(projectId);
|
|
3492
|
+
}
|
|
3493
|
+
console.log();
|
|
3494
|
+
let metaProjectRole;
|
|
3495
|
+
const variables = await withSpinner(
|
|
3496
|
+
`Fetching ${chalk12.bold(environment)} variables...`,
|
|
3497
|
+
async () => {
|
|
3498
|
+
const api = createAPIClient();
|
|
3499
|
+
const response = await api.get("/api/cli/variables", {
|
|
3500
|
+
projectId,
|
|
3501
|
+
environment,
|
|
3502
|
+
...organizationId && { organizationId }
|
|
3503
|
+
});
|
|
3504
|
+
metaProjectRole = response.meta?.projectRole;
|
|
3505
|
+
return response.data || [];
|
|
3506
|
+
}
|
|
3507
|
+
);
|
|
3508
|
+
const outputPath = getEnvPathForEnvironment(environment);
|
|
3509
|
+
if (variables.length === 0) {
|
|
3510
|
+
warning(`No variables found for ${environment} environment.`);
|
|
3511
|
+
console.log();
|
|
3512
|
+
return;
|
|
3513
|
+
}
|
|
3514
|
+
const remoteVars = {};
|
|
3515
|
+
for (const variable of variables) {
|
|
3516
|
+
remoteVars[variable.key] = variable.value;
|
|
3517
|
+
}
|
|
3518
|
+
const localVars = readEnvFile(outputPath) || {};
|
|
3519
|
+
const diffResult = diffEnvVars(remoteVars, localVars);
|
|
3520
|
+
const hasChanges = Object.keys(diffResult.added).length > 0 || Object.keys(diffResult.removed).length > 0 || Object.keys(diffResult.changed).length > 0;
|
|
3521
|
+
if (!hasChanges) {
|
|
3522
|
+
success(`${chalk12.bold(outputPath)} is up to date.`);
|
|
3523
|
+
console.log();
|
|
3524
|
+
return;
|
|
3525
|
+
}
|
|
3526
|
+
console.log();
|
|
3527
|
+
console.log(chalk12.bold("Changes:"));
|
|
3528
|
+
console.log();
|
|
3529
|
+
diff(diffResult.added, diffResult.removed, diffResult.changed);
|
|
3530
|
+
console.log();
|
|
3531
|
+
try {
|
|
3532
|
+
const fs = await import("fs");
|
|
3533
|
+
if (fs.existsSync(outputPath)) {
|
|
3534
|
+
fs.chmodSync(outputPath, 420);
|
|
3535
|
+
}
|
|
3536
|
+
} catch {
|
|
3537
|
+
}
|
|
3538
|
+
const comments = {};
|
|
3539
|
+
for (const variable of variables) {
|
|
3540
|
+
if (variable.description) {
|
|
3541
|
+
comments[variable.key] = variable.description;
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
writeEnvFile(outputPath, remoteVars, { sort: true, comments });
|
|
3545
|
+
const role = getRole();
|
|
3546
|
+
applyFileProtection(outputPath, role, metaProjectRole);
|
|
3547
|
+
success(
|
|
3548
|
+
`Synced ${variables.length} variables to ${chalk12.bold(outputPath)}`
|
|
3549
|
+
);
|
|
3550
|
+
const isProtected = role !== "admin" && role !== "team_lead" && metaProjectRole !== "manager";
|
|
3551
|
+
if (isProtected) {
|
|
3552
|
+
info(
|
|
3553
|
+
`File is read-only (your role: ${role || metaProjectRole || "member"}).`
|
|
3554
|
+
);
|
|
3555
|
+
}
|
|
3556
|
+
console.log();
|
|
3557
|
+
console.log(
|
|
3558
|
+
chalk12.dim(` Added: ${Object.keys(diffResult.added).length}`)
|
|
3559
|
+
);
|
|
3560
|
+
console.log(
|
|
3561
|
+
chalk12.dim(` Changed: ${Object.keys(diffResult.changed).length}`)
|
|
3562
|
+
);
|
|
3563
|
+
console.log(
|
|
3564
|
+
chalk12.dim(` Removed: ${Object.keys(diffResult.removed).length}`)
|
|
3565
|
+
);
|
|
3566
|
+
console.log();
|
|
3567
|
+
} catch (err) {
|
|
3568
|
+
await handleError(err);
|
|
3569
|
+
}
|
|
3570
|
+
});
|
|
3571
|
+
|
|
3572
|
+
// src/commands/usage.ts
|
|
3573
|
+
import { Command as Command11 } from "commander";
|
|
3574
|
+
import chalk13 from "chalk";
|
|
3575
|
+
function formatUsage(current, limit) {
|
|
3576
|
+
const limitStr = limit === null ? "unlimited" : String(limit);
|
|
3577
|
+
const ratio = `${current}/${limitStr}`;
|
|
3578
|
+
if (limit === null) return chalk13.green(ratio);
|
|
3579
|
+
if (current >= limit) return chalk13.red(ratio);
|
|
3580
|
+
if (current >= limit * 0.8) return chalk13.yellow(ratio);
|
|
3581
|
+
return chalk13.green(ratio);
|
|
3582
|
+
}
|
|
3583
|
+
function featureStatus(enabled) {
|
|
3584
|
+
return enabled ? chalk13.green("Enabled") : chalk13.dim("Disabled (Pro)");
|
|
3585
|
+
}
|
|
3586
|
+
var usageCommand = new Command11("usage").description("Show plan usage and limits for the active organization").option("-o, --organization <id>", "Organization ID").option("--json", "Output as JSON").action(async (options) => {
|
|
3587
|
+
try {
|
|
3588
|
+
if (!isAuthenticated()) {
|
|
3589
|
+
throw notAuthenticated();
|
|
3590
|
+
}
|
|
3591
|
+
const api = createAPIClient();
|
|
3592
|
+
const projectConfig = readProjectConfig();
|
|
3593
|
+
let organizationId = options.organization || projectConfig?.organizationId || getActiveOrganizationId();
|
|
3594
|
+
if (!organizationId) {
|
|
3595
|
+
const orgs = await withSpinner(
|
|
3596
|
+
"Fetching organizations...",
|
|
3597
|
+
async () => {
|
|
3598
|
+
const response = await api.get("/api/cli/organizations");
|
|
3599
|
+
return response.data || [];
|
|
3600
|
+
}
|
|
3601
|
+
);
|
|
3602
|
+
if (orgs.length === 0) {
|
|
3603
|
+
error("No organizations found.");
|
|
3604
|
+
process.exit(1);
|
|
3605
|
+
}
|
|
3606
|
+
if (orgs.length === 1) {
|
|
3607
|
+
organizationId = orgs[0]._id;
|
|
3608
|
+
} else {
|
|
3609
|
+
error(
|
|
3610
|
+
"Multiple organizations found. Use --organization to specify one."
|
|
3611
|
+
);
|
|
3612
|
+
console.log();
|
|
3613
|
+
for (const org of orgs) {
|
|
3614
|
+
console.log(
|
|
3615
|
+
` ${org.name} (${org.slug}): --organization ${org._id}`
|
|
3616
|
+
);
|
|
3617
|
+
}
|
|
3618
|
+
process.exit(1);
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
const usage = await withSpinner(
|
|
3622
|
+
"Fetching usage...",
|
|
3623
|
+
() => api.getUsage(organizationId)
|
|
3624
|
+
);
|
|
3625
|
+
if (options.json) {
|
|
3626
|
+
console.log(JSON.stringify(usage, null, 2));
|
|
3627
|
+
return;
|
|
3628
|
+
}
|
|
3629
|
+
const tierLabel = usage.tier === "pro" ? chalk13.green("Pro") : chalk13.white("Free");
|
|
3630
|
+
header(`Plan: ${tierLabel}`);
|
|
3631
|
+
blank();
|
|
3632
|
+
if (!usage.enforcementEnabled) {
|
|
3633
|
+
info("Pre-alpha mode \u2014 all limits are bypassed. Billing coming soon.");
|
|
3634
|
+
blank();
|
|
3635
|
+
}
|
|
3636
|
+
header("Resource Usage");
|
|
3637
|
+
blank();
|
|
3638
|
+
keyValue([
|
|
3639
|
+
["Projects", formatUsage(usage.usage.projects, usage.limits.projects)],
|
|
3640
|
+
[
|
|
3641
|
+
"Team Members",
|
|
3642
|
+
formatUsage(usage.usage.teamMembers, usage.limits.teamMembers)
|
|
3643
|
+
],
|
|
3644
|
+
["Pending Invitations", String(usage.usage.pendingInvitations)],
|
|
3645
|
+
["Total Variables", String(usage.usage.totalVariables)]
|
|
3646
|
+
]);
|
|
3647
|
+
blank();
|
|
3648
|
+
if (usage.usage.variablesPerProject.length > 0) {
|
|
3649
|
+
header("Variables per Project");
|
|
3650
|
+
blank();
|
|
3651
|
+
table(
|
|
3652
|
+
usage.usage.variablesPerProject.map((p) => ({
|
|
3653
|
+
project: p.projectName,
|
|
3654
|
+
variables: formatUsage(p.count, usage.limits.variablesPerProject)
|
|
3655
|
+
})),
|
|
3656
|
+
[
|
|
3657
|
+
{ key: "project", header: "Project" },
|
|
3658
|
+
{ key: "variables", header: "Variables" }
|
|
3659
|
+
]
|
|
3660
|
+
);
|
|
3661
|
+
blank();
|
|
3662
|
+
}
|
|
3663
|
+
header("Features");
|
|
3664
|
+
blank();
|
|
3665
|
+
keyValue([
|
|
3666
|
+
["Version History", featureStatus(usage.features.versionHistory)],
|
|
3667
|
+
["Bulk Import", featureStatus(usage.features.bulkImport)],
|
|
3668
|
+
[
|
|
3669
|
+
"Granular Permissions",
|
|
3670
|
+
featureStatus(usage.features.granularPermissions)
|
|
3671
|
+
],
|
|
3672
|
+
["Extension Access", featureStatus(usage.features.extensionAccess)],
|
|
3673
|
+
["Audit Log Retention", `${usage.features.auditLogRetentionDays} days`]
|
|
3674
|
+
]);
|
|
3675
|
+
blank();
|
|
3676
|
+
} catch (err) {
|
|
3677
|
+
await handleError(err);
|
|
3678
|
+
}
|
|
3679
|
+
});
|
|
3680
|
+
|
|
3681
|
+
// src/commands/whoami.ts
|
|
3682
|
+
import { Command as Command12 } from "commander";
|
|
3683
|
+
import chalk14 from "chalk";
|
|
3684
|
+
var whoamiCommand = new Command12("whoami").description("Show the current authenticated user and active CLI context").action(async () => {
|
|
3685
|
+
try {
|
|
3686
|
+
if (!isAuthenticated()) {
|
|
3687
|
+
throw notAuthenticated();
|
|
3688
|
+
}
|
|
3689
|
+
const api = createAPIClient();
|
|
3690
|
+
const remoteUser = await api.getCurrentUser();
|
|
3691
|
+
const localUser = getUser();
|
|
3692
|
+
const linkedConfig = readProjectConfigV2();
|
|
3693
|
+
const activeProject = linkedConfig ? getActiveProject(linkedConfig) : null;
|
|
3694
|
+
header("Current Session");
|
|
3695
|
+
blank();
|
|
3696
|
+
keyValue([
|
|
3697
|
+
["Email", remoteUser.email],
|
|
3698
|
+
["Name", remoteUser.name || localUser?.name],
|
|
3699
|
+
["API URL", getApiUrl()],
|
|
3700
|
+
["Active Organization", getActiveOrganizationId()],
|
|
3701
|
+
["Active Project", getActiveProjectId()],
|
|
3702
|
+
[
|
|
3703
|
+
"Linked Project",
|
|
3704
|
+
activeProject?.projectName || activeProject?.projectId
|
|
3705
|
+
],
|
|
3706
|
+
["Environment", activeProject?.environment]
|
|
3707
|
+
]);
|
|
3708
|
+
blank();
|
|
3709
|
+
console.log(chalk14.dim("Token verified against the CLI auth endpoint."));
|
|
3710
|
+
} catch (err) {
|
|
3711
|
+
await handleError(err);
|
|
3712
|
+
}
|
|
3713
|
+
});
|
|
3714
|
+
|
|
3715
|
+
// src/commands/man.ts
|
|
3716
|
+
import { Command as Command13 } from "commander";
|
|
3717
|
+
import chalk15 from "chalk";
|
|
3718
|
+
function printCommandManual(commandName) {
|
|
3719
|
+
const command = findCommandDefinition(commandName);
|
|
3720
|
+
if (!command) {
|
|
3721
|
+
console.log(chalk15.red(`Unknown command reference: ${commandName}`));
|
|
3722
|
+
console.log();
|
|
3723
|
+
console.log("Run `envpilot man` to see all supported commands.");
|
|
3724
|
+
process.exit(1);
|
|
3725
|
+
}
|
|
3726
|
+
console.log(chalk15.bold(formatArgv(command.argv)));
|
|
3727
|
+
if (command.args) {
|
|
3728
|
+
console.log(chalk15.dim(command.args));
|
|
3729
|
+
}
|
|
3730
|
+
blank();
|
|
3731
|
+
console.log(command.description);
|
|
3732
|
+
blank();
|
|
3733
|
+
console.log(chalk15.green("Examples"));
|
|
3734
|
+
for (const example of command.examples) {
|
|
3735
|
+
console.log(` ${chalk15.cyan(formatArgv(example))}`);
|
|
3736
|
+
}
|
|
3737
|
+
blank();
|
|
3738
|
+
console.log(chalk15.green("Notes"));
|
|
3739
|
+
for (const note of command.notes) {
|
|
3740
|
+
console.log(` - ${note}`);
|
|
3741
|
+
}
|
|
3742
|
+
}
|
|
3743
|
+
function printManualIndex(commands) {
|
|
3744
|
+
console.log(chalk15.bold("ENVPILOT(1)"));
|
|
3745
|
+
console.log("TypeScript CLI manual");
|
|
3746
|
+
blank();
|
|
3747
|
+
console.log(
|
|
3748
|
+
`Total supported top-level commands: ${chalk15.green(String(CLI_COMMAND_COUNT))}`
|
|
3749
|
+
);
|
|
3750
|
+
blank();
|
|
3751
|
+
console.log(
|
|
3752
|
+
"The CLI is implemented in TypeScript, uses an Ink terminal UI, and keeps env files out of git with both `.gitignore` and an optional pre-commit guard."
|
|
3753
|
+
);
|
|
3754
|
+
blank();
|
|
3755
|
+
line();
|
|
3756
|
+
console.log(chalk15.green("Commands"));
|
|
3757
|
+
for (const command of commands) {
|
|
3758
|
+
console.log(
|
|
3759
|
+
` ${chalk15.cyan(formatArgv(command.argv).padEnd(24))} ${chalk15.dim(command.description)}`
|
|
3760
|
+
);
|
|
3761
|
+
}
|
|
3762
|
+
blank();
|
|
3763
|
+
line();
|
|
3764
|
+
console.log(chalk15.green("Security"));
|
|
3765
|
+
console.log(" - `.env*` is ignored by the repository `.gitignore`.");
|
|
3766
|
+
console.log(
|
|
3767
|
+
" - `envpilot init` and `envpilot sync` ensure env files are added to `.gitignore` locally."
|
|
3768
|
+
);
|
|
3769
|
+
console.log(
|
|
3770
|
+
" - `envpilot push` hard-blocks if tracked `.env` files are detected in git."
|
|
3771
|
+
);
|
|
3772
|
+
console.log(
|
|
3773
|
+
" - The CLI can install a pre-commit hook to stop staged env files from being committed."
|
|
3774
|
+
);
|
|
3775
|
+
blank();
|
|
3776
|
+
line();
|
|
3777
|
+
console.log(chalk15.green("Usage"));
|
|
3778
|
+
console.log(
|
|
3779
|
+
" - `envpilot` or `envpilot ui` opens the interactive terminal UI."
|
|
3780
|
+
);
|
|
3781
|
+
console.log(
|
|
3782
|
+
" - `envpilot man <command>` shows details for a single command."
|
|
3783
|
+
);
|
|
3784
|
+
}
|
|
3785
|
+
function createManCommand(commands) {
|
|
3786
|
+
return new Command13("man").description("Show the CLI manual page").argument("[command]", "Optional command to show detailed manual for").action((command) => {
|
|
3787
|
+
if (command) {
|
|
3788
|
+
printCommandManual(command);
|
|
3789
|
+
return;
|
|
3790
|
+
}
|
|
3791
|
+
printManualIndex(commands);
|
|
3792
|
+
});
|
|
3793
|
+
}
|
|
3794
|
+
|
|
3795
|
+
// src/commands/ui.ts
|
|
3796
|
+
import { Command as Command14 } from "commander";
|
|
3797
|
+
function createUICommand() {
|
|
3798
|
+
return new Command14("ui").alias("dashboard").description("Open the interactive Ink-powered terminal UI").action(async () => {
|
|
3799
|
+
if (process.env.ENVPILOT_TUI_CHILD === "1") {
|
|
3800
|
+
return;
|
|
3801
|
+
}
|
|
3802
|
+
await openTUI();
|
|
3803
|
+
});
|
|
3804
|
+
}
|
|
3805
|
+
|
|
3806
|
+
// src/lib/command-catalog.ts
|
|
3807
|
+
function formatArgv(argv) {
|
|
3808
|
+
return ["envpilot", ...argv].join(" ").trim();
|
|
3809
|
+
}
|
|
3810
|
+
function matchesArgv(argv, normalized) {
|
|
3811
|
+
return formatArgv(argv).toLowerCase() === normalized || argv.join(" ").toLowerCase() === normalized;
|
|
3812
|
+
}
|
|
3813
|
+
var COMMAND_CATALOG = [
|
|
3814
|
+
{
|
|
3815
|
+
id: "open",
|
|
3816
|
+
title: "Open interactive UI",
|
|
3817
|
+
category: "Get Started",
|
|
3818
|
+
description: "Open the Ink-powered terminal dashboard for discovering and running commands.",
|
|
3819
|
+
argv: [],
|
|
3820
|
+
aliases: [["ui"], ["dashboard"]],
|
|
3821
|
+
examples: [[], ["ui"], ["dashboard"]],
|
|
3822
|
+
websiteSurface: "Terminal-first dashboard aligned with the same auth, project, and variable surfaces as the website.",
|
|
3823
|
+
notes: [
|
|
3824
|
+
"This is the default when you run `envpilot` with no subcommand.",
|
|
3825
|
+
"Supports search, keyboard navigation, and command launch."
|
|
3826
|
+
],
|
|
3827
|
+
keywords: ["dashboard", "tui", "ink", "interactive"],
|
|
3828
|
+
topLevel: true
|
|
3829
|
+
},
|
|
3830
|
+
{
|
|
3831
|
+
id: "ui",
|
|
3832
|
+
title: "Interactive UI command",
|
|
3833
|
+
category: "Get Started",
|
|
3834
|
+
description: "Open the interactive Ink-powered terminal UI.",
|
|
3835
|
+
argv: ["ui"],
|
|
3836
|
+
aliases: [["dashboard"]],
|
|
3837
|
+
examples: [["ui"], ["dashboard"]],
|
|
3838
|
+
websiteSurface: "First-class parsed command that launches the same Ink dashboard as bare `envpilot`.",
|
|
3839
|
+
notes: [
|
|
3840
|
+
"Use this when you want the UI explicitly instead of relying on the default no-arg launcher."
|
|
3841
|
+
],
|
|
3842
|
+
keywords: ["dashboard", "tui", "ink"],
|
|
3843
|
+
topLevel: true,
|
|
3844
|
+
createCommand: createUICommand
|
|
3845
|
+
},
|
|
3846
|
+
{
|
|
3847
|
+
id: "sync",
|
|
3848
|
+
title: "Sync project",
|
|
3849
|
+
category: "Sync",
|
|
3850
|
+
description: "Authenticate, select a project, pull variables, and set up local protection in one flow.",
|
|
3851
|
+
argv: ["sync"],
|
|
3852
|
+
args: "[--organization <id>] [--project <id>] [--env <environment>]",
|
|
3853
|
+
examples: [["sync"], ["sync", "--env", "production"]],
|
|
3854
|
+
websiteSurface: "Uses the website CLI auth, organizations, projects, and variables endpoints.",
|
|
3855
|
+
notes: [
|
|
3856
|
+
"Best first-run workflow for local setup.",
|
|
3857
|
+
"Reuses the existing login, init, and pull logic under the hood."
|
|
3858
|
+
],
|
|
3859
|
+
keywords: ["login", "auth", "pull", "setup", "bootstrap"],
|
|
3860
|
+
topLevel: true,
|
|
3861
|
+
createCommand: () => syncCommand
|
|
3862
|
+
},
|
|
3863
|
+
{
|
|
3864
|
+
id: "man",
|
|
3865
|
+
title: "Manual page",
|
|
3866
|
+
category: "Get Started",
|
|
3867
|
+
description: "Show the CLI manual page with commands, workflows, and security guidance.",
|
|
3868
|
+
argv: ["man"],
|
|
3869
|
+
args: "[command]",
|
|
3870
|
+
examples: [["man"], ["man", "pull"]],
|
|
3871
|
+
websiteSurface: "Documents the implemented CLI surface from the same catalog used by the TUI and command registration.",
|
|
3872
|
+
notes: [
|
|
3873
|
+
"Use this to see the supported command set.",
|
|
3874
|
+
"Supports per-command manual sections."
|
|
3875
|
+
],
|
|
3876
|
+
keywords: ["manual", "docs", "help", "reference"],
|
|
3877
|
+
topLevel: true,
|
|
3878
|
+
createCommand: () => createManCommand(getTopLevelCommandCatalog())
|
|
3879
|
+
},
|
|
3880
|
+
{
|
|
3881
|
+
id: "login",
|
|
3882
|
+
title: "Login",
|
|
3883
|
+
category: "Get Started",
|
|
3884
|
+
description: "Authenticate the CLI against the Envpilot web app.",
|
|
3885
|
+
argv: ["login"],
|
|
3886
|
+
args: "[--api-url <url>] [--no-browser]",
|
|
3887
|
+
examples: [["login"], ["login", "--no-browser"]],
|
|
3888
|
+
websiteSurface: "Maps to `/api/cli/auth` and the `/cli/auth` browser flow.",
|
|
3889
|
+
notes: [
|
|
3890
|
+
"Opens the web authentication page by default.",
|
|
3891
|
+
"Required before organization or project commands will work."
|
|
3892
|
+
],
|
|
3893
|
+
keywords: ["signin", "authenticate", "browser"],
|
|
3894
|
+
topLevel: true,
|
|
3895
|
+
createCommand: () => loginCommand
|
|
3896
|
+
},
|
|
3897
|
+
{
|
|
3898
|
+
id: "init",
|
|
3899
|
+
title: "Initialize directory",
|
|
3900
|
+
category: "Get Started",
|
|
3901
|
+
description: "Link the current directory to a project and choose a default environment.",
|
|
3902
|
+
argv: ["init"],
|
|
3903
|
+
args: "[--organization <id>] [--project <id>] [--env <environment>]",
|
|
3904
|
+
examples: [["init"], ["init", "--add"]],
|
|
3905
|
+
websiteSurface: "Uses website-backed organization and project selection before writing local config.",
|
|
3906
|
+
notes: [
|
|
3907
|
+
"Creates or updates the local `.envpilot` file.",
|
|
3908
|
+
"Supports linking multiple projects with `--add`."
|
|
3909
|
+
],
|
|
3910
|
+
keywords: ["link", "directory", "project", "environment"],
|
|
3911
|
+
topLevel: true,
|
|
3912
|
+
createCommand: () => initCommand
|
|
3913
|
+
},
|
|
3914
|
+
{
|
|
3915
|
+
id: "pull",
|
|
3916
|
+
title: "Pull variables",
|
|
3917
|
+
category: "Sync",
|
|
3918
|
+
description: "Download project variables into a local file or export format.",
|
|
3919
|
+
argv: ["pull"],
|
|
3920
|
+
args: "[--env <environment>] [--file <path>] [--format <format>]",
|
|
3921
|
+
examples: [["pull"], ["pull", "--env", "staging", "--dry-run"]],
|
|
3922
|
+
websiteSurface: "Maps to `/api/cli/variables` for read access.",
|
|
3923
|
+
notes: [
|
|
3924
|
+
"Supports `.env`, JSON, YAML, Vercel, Netlify, AWS, and Docker Compose formats.",
|
|
3925
|
+
"Can pull the active linked project or all linked projects."
|
|
3926
|
+
],
|
|
3927
|
+
keywords: ["download", "export", "variables", "env"],
|
|
3928
|
+
topLevel: true,
|
|
3929
|
+
createCommand: () => pullCommand
|
|
3930
|
+
},
|
|
3931
|
+
{
|
|
3932
|
+
id: "push",
|
|
3933
|
+
title: "Push variables",
|
|
3934
|
+
category: "Sync",
|
|
3935
|
+
description: "Upload local variables back to Envpilot with approval-aware write behavior.",
|
|
3936
|
+
argv: ["push"],
|
|
3937
|
+
args: "[--env <environment>] [--file <path>] [--merge|--replace]",
|
|
3938
|
+
examples: [["push"], ["push", "--replace"]],
|
|
3939
|
+
websiteSurface: "Maps to `/api/cli/variables` and `/api/cli/variables/bulk` for writes.",
|
|
3940
|
+
notes: [
|
|
3941
|
+
"Managers/admins write directly; lower roles create requests for approval.",
|
|
3942
|
+
"Compares local and remote variables before applying changes."
|
|
3943
|
+
],
|
|
3944
|
+
keywords: ["upload", "bulk", "merge", "replace"],
|
|
3945
|
+
topLevel: true,
|
|
3946
|
+
createCommand: () => pushCommand
|
|
3947
|
+
},
|
|
3948
|
+
{
|
|
3949
|
+
id: "list",
|
|
3950
|
+
title: "List resources",
|
|
3951
|
+
category: "Browse",
|
|
3952
|
+
description: "List organizations, projects, variables, or linked projects from the terminal.",
|
|
3953
|
+
argv: ["list"],
|
|
3954
|
+
args: "[resource]",
|
|
3955
|
+
examples: [["list"], ["list", "projects"], ["list", "variables"]],
|
|
3956
|
+
websiteSurface: "Uses the CLI organizations, projects, and variables endpoints depending on the selected resource.",
|
|
3957
|
+
notes: [
|
|
3958
|
+
"Default resource is `projects`.",
|
|
3959
|
+
"Use `linked` to inspect local `.envpilot` project links."
|
|
3960
|
+
],
|
|
3961
|
+
keywords: ["browse", "organizations", "projects", "variables", "linked"],
|
|
3962
|
+
topLevel: true,
|
|
3963
|
+
createCommand: () => listCommand
|
|
3964
|
+
},
|
|
3965
|
+
{
|
|
3966
|
+
id: "list-organizations",
|
|
3967
|
+
title: "List organizations",
|
|
3968
|
+
category: "Browse",
|
|
3969
|
+
description: "List organizations available to the current user.",
|
|
3970
|
+
argv: ["list", "organizations"],
|
|
3971
|
+
aliases: [["list", "orgs"]],
|
|
3972
|
+
examples: [
|
|
3973
|
+
["list", "organizations"],
|
|
3974
|
+
["list", "orgs", "--json"]
|
|
3975
|
+
],
|
|
3976
|
+
websiteSurface: "Maps to `/api/cli/organizations`.",
|
|
3977
|
+
notes: ["Useful for discovering organization IDs and roles."],
|
|
3978
|
+
keywords: ["orgs", "organizations", "roles"]
|
|
3979
|
+
},
|
|
3980
|
+
{
|
|
3981
|
+
id: "list-projects",
|
|
3982
|
+
title: "List projects",
|
|
3983
|
+
category: "Browse",
|
|
3984
|
+
description: "Browse projects in the active organization.",
|
|
3985
|
+
argv: ["list", "projects"],
|
|
3986
|
+
args: "[--organization <id>] [--json]",
|
|
3987
|
+
examples: [
|
|
3988
|
+
["list", "projects"],
|
|
3989
|
+
["list", "projects", "--json"]
|
|
3990
|
+
],
|
|
3991
|
+
websiteSurface: "Maps to `/api/cli/projects`.",
|
|
3992
|
+
notes: [
|
|
3993
|
+
"Shows project roles when available.",
|
|
3994
|
+
"Useful for selecting project IDs for automation and scripts."
|
|
3995
|
+
],
|
|
3996
|
+
keywords: ["browse", "projects", "organization"]
|
|
3997
|
+
},
|
|
3998
|
+
{
|
|
3999
|
+
id: "list-variables",
|
|
4000
|
+
title: "List variables",
|
|
4001
|
+
category: "Browse",
|
|
4002
|
+
description: "Inspect variables for a project with environment and tag filtering.",
|
|
4003
|
+
argv: ["list", "variables"],
|
|
4004
|
+
aliases: [["list", "vars"]],
|
|
4005
|
+
args: "[--project <id>] [--env <environment>] [--tag <name>]",
|
|
4006
|
+
examples: [
|
|
4007
|
+
["list", "variables"],
|
|
4008
|
+
["list", "variables", "--env", "production", "--show-values"]
|
|
4009
|
+
],
|
|
4010
|
+
websiteSurface: "Maps to `/api/cli/variables`.",
|
|
4011
|
+
notes: [
|
|
4012
|
+
"Values are masked by default.",
|
|
4013
|
+
"Designed to mirror the web app\u2019s searchable variable surface."
|
|
4014
|
+
],
|
|
4015
|
+
keywords: ["vars", "keys", "filter", "tags"]
|
|
4016
|
+
},
|
|
4017
|
+
{
|
|
4018
|
+
id: "list-linked",
|
|
4019
|
+
title: "List linked projects",
|
|
4020
|
+
category: "Browse",
|
|
4021
|
+
description: "Show projects linked in the current directory.",
|
|
4022
|
+
argv: ["list", "linked"],
|
|
4023
|
+
examples: [["list", "linked"]],
|
|
4024
|
+
websiteSurface: "Local `.envpilot` configuration inspection.",
|
|
4025
|
+
notes: ["Shows the active linked project and environment mapping."],
|
|
4026
|
+
keywords: ["linked", "local", "project-config"]
|
|
4027
|
+
},
|
|
4028
|
+
{
|
|
4029
|
+
id: "switch",
|
|
4030
|
+
title: "Switch active target",
|
|
4031
|
+
category: "Project",
|
|
4032
|
+
description: "Switch the active organization, project, environment, or linked project.",
|
|
4033
|
+
argv: ["switch"],
|
|
4034
|
+
args: "[--organization <id>] [--project <id>] [--env <environment>] [--active <name-or-id>]",
|
|
4035
|
+
examples: [
|
|
4036
|
+
["switch", "--env", "production"],
|
|
4037
|
+
["switch", "--active", "api"]
|
|
4038
|
+
],
|
|
4039
|
+
websiteSurface: "Works against the same organization/project inventory returned by website APIs.",
|
|
4040
|
+
notes: [
|
|
4041
|
+
"Updates local CLI state without rewriting the whole project config.",
|
|
4042
|
+
"Supports both linked projects and remote project lookup."
|
|
4043
|
+
],
|
|
4044
|
+
keywords: ["active", "target", "environment", "organization"],
|
|
4045
|
+
topLevel: true,
|
|
4046
|
+
createCommand: () => switchCommand
|
|
4047
|
+
},
|
|
4048
|
+
{
|
|
4049
|
+
id: "usage",
|
|
4050
|
+
title: "Usage and limits",
|
|
4051
|
+
category: "Browse",
|
|
4052
|
+
description: "Inspect current plan usage and feature availability for the active organization.",
|
|
4053
|
+
argv: ["usage"],
|
|
4054
|
+
args: "[--organization <id>] [--json]",
|
|
4055
|
+
examples: [["usage"], ["usage", "--json"]],
|
|
4056
|
+
websiteSurface: "Maps to `/api/cli/usage` and `/api/cli/tier`.",
|
|
4057
|
+
notes: [
|
|
4058
|
+
"Shows project, member, and variable limits.",
|
|
4059
|
+
"Useful for CLI feature-gate troubleshooting."
|
|
4060
|
+
],
|
|
4061
|
+
keywords: ["plan", "limits", "billing", "tier"],
|
|
4062
|
+
topLevel: true,
|
|
4063
|
+
createCommand: () => usageCommand
|
|
4064
|
+
},
|
|
4065
|
+
{
|
|
4066
|
+
id: "whoami",
|
|
4067
|
+
title: "Current identity",
|
|
4068
|
+
category: "Account",
|
|
4069
|
+
description: "Show the authenticated user, API target, and current active CLI context.",
|
|
4070
|
+
argv: ["whoami"],
|
|
4071
|
+
examples: [["whoami"]],
|
|
4072
|
+
websiteSurface: "Uses the same website CLI auth identity endpoint exposed to the terminal client.",
|
|
4073
|
+
notes: [
|
|
4074
|
+
"Validates the current access token against the website.",
|
|
4075
|
+
"Useful for debugging stale auth or wrong API URL targets."
|
|
4076
|
+
],
|
|
4077
|
+
keywords: ["user", "identity", "session", "auth"],
|
|
4078
|
+
topLevel: true,
|
|
4079
|
+
createCommand: () => whoamiCommand
|
|
4080
|
+
},
|
|
4081
|
+
{
|
|
4082
|
+
id: "config",
|
|
4083
|
+
title: "Config",
|
|
4084
|
+
category: "Account",
|
|
4085
|
+
description: "Inspect or update local CLI configuration such as the active API URL.",
|
|
4086
|
+
argv: ["config"],
|
|
4087
|
+
args: "[list|get|set|path|reset]",
|
|
4088
|
+
examples: [["config"], ["config", "path"]],
|
|
4089
|
+
websiteSurface: "Local-only state for the CLI shell, pointing at the same website backend.",
|
|
4090
|
+
notes: [
|
|
4091
|
+
"Useful for local development against non-production API URLs.",
|
|
4092
|
+
"Shows both global and project-level config paths."
|
|
4093
|
+
],
|
|
4094
|
+
keywords: ["settings", "path", "api", "state"],
|
|
4095
|
+
topLevel: true,
|
|
4096
|
+
createCommand: () => configCommand
|
|
4097
|
+
},
|
|
4098
|
+
{
|
|
4099
|
+
id: "logout",
|
|
4100
|
+
title: "Logout",
|
|
4101
|
+
category: "Account",
|
|
4102
|
+
description: "Revoke the current CLI session and clear local auth state.",
|
|
4103
|
+
argv: ["logout"],
|
|
4104
|
+
examples: [["logout"]],
|
|
4105
|
+
websiteSurface: "Maps to `/api/cli/auth?action=revoke`.",
|
|
4106
|
+
notes: [
|
|
4107
|
+
"Best way to reset a stale CLI session cleanly.",
|
|
4108
|
+
"Clears local tokens even if the revoke call fails."
|
|
4109
|
+
],
|
|
4110
|
+
keywords: ["signout", "revoke", "session"],
|
|
4111
|
+
topLevel: true,
|
|
4112
|
+
createCommand: () => logoutCommand
|
|
4113
|
+
},
|
|
4114
|
+
{
|
|
4115
|
+
id: "unlink",
|
|
4116
|
+
title: "Unlink project",
|
|
4117
|
+
category: "Project",
|
|
4118
|
+
description: "Remove a linked project from the current directory without deleting local env files.",
|
|
4119
|
+
argv: ["unlink"],
|
|
4120
|
+
args: "[project] [--force]",
|
|
4121
|
+
examples: [["unlink"], ["unlink", "api", "--force"]],
|
|
4122
|
+
websiteSurface: "Local project-link management for website-backed projects.",
|
|
4123
|
+
notes: [
|
|
4124
|
+
"Updates `.envpilot` and active-project state.",
|
|
4125
|
+
"Leaves existing `.env` files on disk."
|
|
4126
|
+
],
|
|
4127
|
+
keywords: ["remove", "disconnect", "local"],
|
|
4128
|
+
topLevel: true,
|
|
4129
|
+
createCommand: () => unlinkCommand
|
|
4130
|
+
}
|
|
4131
|
+
];
|
|
4132
|
+
var _topLevelCache = null;
|
|
4133
|
+
function getTopLevelCommandCatalog() {
|
|
4134
|
+
if (!_topLevelCache) {
|
|
4135
|
+
_topLevelCache = COMMAND_CATALOG.filter((command) => command.topLevel);
|
|
4136
|
+
}
|
|
4137
|
+
return _topLevelCache;
|
|
4138
|
+
}
|
|
4139
|
+
var CLI_COMMAND_COUNT = getTopLevelCommandCatalog().length;
|
|
4140
|
+
function findCommandDefinition(commandIdOrName) {
|
|
4141
|
+
if (!commandIdOrName) {
|
|
4142
|
+
return void 0;
|
|
4143
|
+
}
|
|
4144
|
+
const normalized = commandIdOrName.trim().toLowerCase();
|
|
4145
|
+
return COMMAND_CATALOG.find((command) => {
|
|
4146
|
+
if (command.id.toLowerCase() === normalized) {
|
|
4147
|
+
return true;
|
|
4148
|
+
}
|
|
4149
|
+
if (matchesArgv(command.argv, normalized)) {
|
|
4150
|
+
return true;
|
|
4151
|
+
}
|
|
4152
|
+
return command.aliases?.some((alias) => matchesArgv(alias, normalized));
|
|
4153
|
+
});
|
|
4154
|
+
}
|
|
4155
|
+
|
|
4156
|
+
export {
|
|
4157
|
+
initSentry,
|
|
4158
|
+
getApiUrl,
|
|
4159
|
+
getUser,
|
|
4160
|
+
isAuthenticated,
|
|
4161
|
+
openTUI,
|
|
4162
|
+
formatArgv,
|
|
4163
|
+
getTopLevelCommandCatalog
|
|
4164
|
+
};
|