@envpilot/cli 1.6.1 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -7,7 +7,7 @@ function initSentry() {
|
|
|
7
7
|
Sentry.init({
|
|
8
8
|
dsn,
|
|
9
9
|
environment: "cli",
|
|
10
|
-
release: true ? "1.
|
|
10
|
+
release: true ? "1.9.1" : "0.0.0",
|
|
11
11
|
// Free tier: disable performance monitoring
|
|
12
12
|
tracesSampleRate: 0,
|
|
13
13
|
beforeSend(event) {
|
|
@@ -48,6 +48,195 @@ async function flushSentry() {
|
|
|
48
48
|
|
|
49
49
|
// src/lib/config.ts
|
|
50
50
|
import Conf from "conf";
|
|
51
|
+
|
|
52
|
+
// src/lib/roles.ts
|
|
53
|
+
var ROLE_LEVEL = {
|
|
54
|
+
owner: 4,
|
|
55
|
+
project_manager: 3,
|
|
56
|
+
team_lead: 2,
|
|
57
|
+
developer: 1
|
|
58
|
+
};
|
|
59
|
+
var ORG_ROLE_LABELS = {
|
|
60
|
+
owner: "Owner",
|
|
61
|
+
project_manager: "Project Manager",
|
|
62
|
+
team_lead: "Team Lead",
|
|
63
|
+
developer: "Developer"
|
|
64
|
+
};
|
|
65
|
+
function normalizeOrgRole(role) {
|
|
66
|
+
switch (role) {
|
|
67
|
+
case "admin":
|
|
68
|
+
return "owner";
|
|
69
|
+
case "member":
|
|
70
|
+
return "developer";
|
|
71
|
+
case "owner":
|
|
72
|
+
case "project_manager":
|
|
73
|
+
case "team_lead":
|
|
74
|
+
case "developer":
|
|
75
|
+
return role;
|
|
76
|
+
default:
|
|
77
|
+
return "developer";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function roleLevel(role) {
|
|
81
|
+
return ROLE_LEVEL[normalizeOrgRole(role)];
|
|
82
|
+
}
|
|
83
|
+
function formatRoleLabel(role) {
|
|
84
|
+
return ORG_ROLE_LABELS[normalizeOrgRole(role)];
|
|
85
|
+
}
|
|
86
|
+
function isFileWritable(access) {
|
|
87
|
+
if (access.role === "owner") return true;
|
|
88
|
+
if (!access.assigned) return false;
|
|
89
|
+
switch (access.role) {
|
|
90
|
+
case "project_manager":
|
|
91
|
+
case "team_lead":
|
|
92
|
+
return true;
|
|
93
|
+
case "developer":
|
|
94
|
+
return access.hasWriteAccess;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/lib/variables-cache.ts
|
|
99
|
+
import { createHash } from "crypto";
|
|
100
|
+
import {
|
|
101
|
+
readFileSync,
|
|
102
|
+
writeFileSync,
|
|
103
|
+
mkdirSync,
|
|
104
|
+
existsSync,
|
|
105
|
+
readdirSync,
|
|
106
|
+
unlinkSync,
|
|
107
|
+
rmdirSync,
|
|
108
|
+
statSync
|
|
109
|
+
} from "fs";
|
|
110
|
+
import { join, dirname } from "path";
|
|
111
|
+
var HARD_MAX_AGE_MS = 12 * 60 * 60 * 1e3;
|
|
112
|
+
function getCacheDir() {
|
|
113
|
+
return join(dirname(getConfigPath()), "run-cache");
|
|
114
|
+
}
|
|
115
|
+
function getCacheKey(projectId, environment, organizationId) {
|
|
116
|
+
const tokenSlice = (getAccessToken() ?? "").slice(0, 16);
|
|
117
|
+
return createHash("sha256").update(`${projectId}:${environment}:${organizationId}:${tokenSlice}`).digest("hex").slice(0, 20);
|
|
118
|
+
}
|
|
119
|
+
function getCachePath(key) {
|
|
120
|
+
return join(getCacheDir(), `${key}.json`);
|
|
121
|
+
}
|
|
122
|
+
function computeFingerprint(variables) {
|
|
123
|
+
return createHash("sha256").update(
|
|
124
|
+
variables.map((v) => `${v._id}:${v.version}:${v.updatedAt}`).sort().join("|")
|
|
125
|
+
).digest("hex").slice(0, 16);
|
|
126
|
+
}
|
|
127
|
+
function readCache(projectId, environment, organizationId) {
|
|
128
|
+
try {
|
|
129
|
+
const key = getCacheKey(projectId, environment, organizationId);
|
|
130
|
+
const path = getCachePath(key);
|
|
131
|
+
if (!existsSync(path)) return null;
|
|
132
|
+
const raw = readFileSync(path, "utf-8");
|
|
133
|
+
const entry = JSON.parse(raw);
|
|
134
|
+
if (entry.apiUrl !== getApiUrl()) return null;
|
|
135
|
+
if (!isWithinHardMaxAge(entry)) {
|
|
136
|
+
try {
|
|
137
|
+
unlinkSync(path);
|
|
138
|
+
} catch {
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
return entry;
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function probeCache(projectId, environment, organizationId, ttlSeconds) {
|
|
148
|
+
const entry = readCache(projectId, environment, organizationId);
|
|
149
|
+
if (!entry) return { hit: false };
|
|
150
|
+
const fresh = isFresh(entry, ttlSeconds);
|
|
151
|
+
return { hit: true, fresh, entry };
|
|
152
|
+
}
|
|
153
|
+
function writeCache(projectId, environment, organizationId, variables, serverFingerprint) {
|
|
154
|
+
try {
|
|
155
|
+
const cacheDir = getCacheDir();
|
|
156
|
+
mkdirSync(cacheDir, { recursive: true, mode: 448 });
|
|
157
|
+
const key = getCacheKey(projectId, environment, organizationId);
|
|
158
|
+
const path = getCachePath(key);
|
|
159
|
+
const now = Date.now();
|
|
160
|
+
const entry = {
|
|
161
|
+
variables,
|
|
162
|
+
fetchedAt: now,
|
|
163
|
+
firstFetchedAt: now,
|
|
164
|
+
projectId,
|
|
165
|
+
environment,
|
|
166
|
+
organizationId,
|
|
167
|
+
apiUrl: getApiUrl(),
|
|
168
|
+
fingerprint: serverFingerprint ?? computeFingerprint(variables)
|
|
169
|
+
};
|
|
170
|
+
writeFileSync(path, JSON.stringify(entry, null, 2), {
|
|
171
|
+
encoding: "utf-8",
|
|
172
|
+
mode: 384
|
|
173
|
+
// owner read/write only — same as .env
|
|
174
|
+
});
|
|
175
|
+
} catch {
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function extendCacheFreshness(projectId, environment, organizationId) {
|
|
179
|
+
try {
|
|
180
|
+
const key = getCacheKey(projectId, environment, organizationId);
|
|
181
|
+
const path = getCachePath(key);
|
|
182
|
+
if (!existsSync(path)) return;
|
|
183
|
+
const raw = readFileSync(path, "utf-8");
|
|
184
|
+
const entry = JSON.parse(raw);
|
|
185
|
+
if (!isWithinHardMaxAge(entry)) return;
|
|
186
|
+
if (entry.firstFetchedAt === void 0)
|
|
187
|
+
entry.firstFetchedAt = entry.fetchedAt;
|
|
188
|
+
entry.fetchedAt = Date.now();
|
|
189
|
+
writeFileSync(path, JSON.stringify(entry, null, 2), {
|
|
190
|
+
encoding: "utf-8",
|
|
191
|
+
mode: 384
|
|
192
|
+
});
|
|
193
|
+
} catch {
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function clearAllCache() {
|
|
197
|
+
let count = 0;
|
|
198
|
+
try {
|
|
199
|
+
const dir = getCacheDir();
|
|
200
|
+
if (!existsSync(dir)) return 0;
|
|
201
|
+
for (const file of readdirSync(dir)) {
|
|
202
|
+
if (!file.endsWith(".json")) continue;
|
|
203
|
+
try {
|
|
204
|
+
unlinkSync(join(dir, file));
|
|
205
|
+
count++;
|
|
206
|
+
} catch {
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
} catch {
|
|
210
|
+
}
|
|
211
|
+
return count;
|
|
212
|
+
}
|
|
213
|
+
function clearRunCache() {
|
|
214
|
+
const removed = clearAllCache();
|
|
215
|
+
try {
|
|
216
|
+
const dir = getCacheDir();
|
|
217
|
+
if (existsSync(dir) && readdirSync(dir).length === 0) {
|
|
218
|
+
rmdirSync(dir);
|
|
219
|
+
}
|
|
220
|
+
} catch {
|
|
221
|
+
}
|
|
222
|
+
return removed;
|
|
223
|
+
}
|
|
224
|
+
function isFresh(entry, ttlSeconds) {
|
|
225
|
+
return Date.now() - entry.fetchedAt < ttlSeconds * 1e3;
|
|
226
|
+
}
|
|
227
|
+
function isWithinHardMaxAge(entry) {
|
|
228
|
+
const anchor = entry.firstFetchedAt ?? entry.fetchedAt;
|
|
229
|
+
return Date.now() - anchor < HARD_MAX_AGE_MS;
|
|
230
|
+
}
|
|
231
|
+
function formatAge(fetchedAt) {
|
|
232
|
+
const ageSec = Math.floor((Date.now() - fetchedAt) / 1e3);
|
|
233
|
+
if (ageSec < 60) return `${ageSec}s ago`;
|
|
234
|
+
const ageMin = Math.floor(ageSec / 60);
|
|
235
|
+
if (ageMin < 60) return `${ageMin}m ago`;
|
|
236
|
+
return `${Math.floor(ageMin / 60)}h ago`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/lib/config.ts
|
|
51
240
|
var DEFAULT_API_URL = "https://www.envpilot.dev";
|
|
52
241
|
var HOST_CANONICALIZATION = {
|
|
53
242
|
"envpilot.dev": "www.envpilot.dev"
|
|
@@ -75,15 +264,116 @@ var config = new Conf({
|
|
|
75
264
|
apiUrl: DEFAULT_API_URL
|
|
76
265
|
}
|
|
77
266
|
});
|
|
267
|
+
function migrateLegacyConfigData(data) {
|
|
268
|
+
const accounts = { ...data.accounts ?? {} };
|
|
269
|
+
const hasAccounts = Object.keys(accounts).length > 0;
|
|
270
|
+
let activeAccountId = data.activeAccountId;
|
|
271
|
+
if (!hasAccounts && data.accessToken) {
|
|
272
|
+
const legacyUser = data.user;
|
|
273
|
+
const id = legacyUser?.id ?? (data.accessToken.length >= 8 ? `legacy-${data.accessToken.slice(0, 8)}` : "default");
|
|
274
|
+
const user = legacyUser ?? { id, email: `${id}@cli.local` };
|
|
275
|
+
accounts[id] = {
|
|
276
|
+
id,
|
|
277
|
+
user,
|
|
278
|
+
accessToken: data.accessToken,
|
|
279
|
+
refreshToken: data.refreshToken,
|
|
280
|
+
role: data.role,
|
|
281
|
+
activeOrganizationId: data.activeOrganizationId,
|
|
282
|
+
activeProjectId: data.activeProjectId
|
|
283
|
+
};
|
|
284
|
+
activeAccountId = id;
|
|
285
|
+
}
|
|
286
|
+
const result = { ...data, accounts, activeAccountId };
|
|
287
|
+
delete result.accessToken;
|
|
288
|
+
delete result.refreshToken;
|
|
289
|
+
delete result.user;
|
|
290
|
+
delete result.role;
|
|
291
|
+
delete result.activeOrganizationId;
|
|
292
|
+
delete result.activeProjectId;
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
function migrateLegacyConfig() {
|
|
296
|
+
const data = config.store;
|
|
297
|
+
const hadLegacy = data.accessToken !== void 0 || data.refreshToken !== void 0 || data.user !== void 0 || data.role !== void 0 || data.activeOrganizationId !== void 0 || data.activeProjectId !== void 0;
|
|
298
|
+
if (!hadLegacy) return;
|
|
299
|
+
config.store = migrateLegacyConfigData(data);
|
|
300
|
+
}
|
|
301
|
+
function readAccounts() {
|
|
302
|
+
return config.get("accounts") ?? {};
|
|
303
|
+
}
|
|
304
|
+
function writeAccounts(accounts) {
|
|
305
|
+
config.set("accounts", accounts);
|
|
306
|
+
}
|
|
307
|
+
function listAccounts() {
|
|
308
|
+
migrateLegacyConfig();
|
|
309
|
+
return Object.values(readAccounts());
|
|
310
|
+
}
|
|
311
|
+
function getActiveAccount() {
|
|
312
|
+
migrateLegacyConfig();
|
|
313
|
+
const id = config.get("activeAccountId");
|
|
314
|
+
if (!id) return void 0;
|
|
315
|
+
return readAccounts()[id];
|
|
316
|
+
}
|
|
317
|
+
function getActiveAccountId() {
|
|
318
|
+
migrateLegacyConfig();
|
|
319
|
+
return config.get("activeAccountId");
|
|
320
|
+
}
|
|
321
|
+
function setActiveAccount(id) {
|
|
322
|
+
const accounts = readAccounts();
|
|
323
|
+
if (!accounts[id]) return false;
|
|
324
|
+
config.set("activeAccountId", id);
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
function upsertAccount(account) {
|
|
328
|
+
const accounts = readAccounts();
|
|
329
|
+
const isFirst = Object.keys(accounts).length === 0;
|
|
330
|
+
accounts[account.id] = account;
|
|
331
|
+
writeAccounts(accounts);
|
|
332
|
+
if (isFirst) {
|
|
333
|
+
config.set("activeAccountId", account.id);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function removeAccount(id) {
|
|
337
|
+
const accounts = readAccounts();
|
|
338
|
+
if (!accounts[id]) {
|
|
339
|
+
return { removedActive: false };
|
|
340
|
+
}
|
|
341
|
+
const wasActive = config.get("activeAccountId") === id;
|
|
342
|
+
delete accounts[id];
|
|
343
|
+
writeAccounts(accounts);
|
|
344
|
+
let newActiveId;
|
|
345
|
+
if (wasActive) {
|
|
346
|
+
newActiveId = Object.keys(accounts)[0];
|
|
347
|
+
if (newActiveId) {
|
|
348
|
+
config.set("activeAccountId", newActiveId);
|
|
349
|
+
} else {
|
|
350
|
+
config.delete("activeAccountId");
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return { removedActive: wasActive, newActiveId };
|
|
354
|
+
}
|
|
355
|
+
function updateActiveAccount(patch) {
|
|
356
|
+
migrateLegacyConfig();
|
|
357
|
+
const id = config.get("activeAccountId");
|
|
358
|
+
if (!id) return;
|
|
359
|
+
const accounts = readAccounts();
|
|
360
|
+
const existing = accounts[id];
|
|
361
|
+
if (!existing) return;
|
|
362
|
+
accounts[id] = { ...existing, ...patch, id: existing.id };
|
|
363
|
+
writeAccounts(accounts);
|
|
364
|
+
}
|
|
78
365
|
function getConfig() {
|
|
366
|
+
const active = getActiveAccount();
|
|
79
367
|
return {
|
|
80
368
|
apiUrl: normalizeApiUrl(config.get("apiUrl") ?? DEFAULT_API_URL),
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
369
|
+
accounts: readAccounts(),
|
|
370
|
+
activeAccountId: config.get("activeAccountId"),
|
|
371
|
+
accessToken: active?.accessToken,
|
|
372
|
+
refreshToken: active?.refreshToken,
|
|
373
|
+
activeProjectId: active?.activeProjectId,
|
|
374
|
+
activeOrganizationId: active?.activeOrganizationId,
|
|
375
|
+
user: active?.user,
|
|
376
|
+
role: active?.role
|
|
87
377
|
};
|
|
88
378
|
}
|
|
89
379
|
function getApiUrl() {
|
|
@@ -95,46 +385,49 @@ function setApiUrl(url) {
|
|
|
95
385
|
config.set("apiUrl", normalizeApiUrl(url));
|
|
96
386
|
}
|
|
97
387
|
function getAccessToken() {
|
|
98
|
-
return
|
|
388
|
+
return getActiveAccount()?.accessToken;
|
|
99
389
|
}
|
|
100
390
|
function setAccessToken(token) {
|
|
101
|
-
|
|
391
|
+
updateActiveAccount({ accessToken: token });
|
|
392
|
+
}
|
|
393
|
+
function getRefreshToken() {
|
|
394
|
+
return getActiveAccount()?.refreshToken;
|
|
102
395
|
}
|
|
103
396
|
function setRefreshToken(token) {
|
|
104
|
-
|
|
397
|
+
updateActiveAccount({ refreshToken: token });
|
|
105
398
|
}
|
|
106
399
|
function getActiveProjectId() {
|
|
107
|
-
return
|
|
400
|
+
return getActiveAccount()?.activeProjectId;
|
|
108
401
|
}
|
|
109
402
|
function setActiveProjectId(projectId) {
|
|
110
|
-
|
|
403
|
+
updateActiveAccount({ activeProjectId: projectId });
|
|
111
404
|
}
|
|
112
405
|
function getActiveOrganizationId() {
|
|
113
|
-
return
|
|
406
|
+
return getActiveAccount()?.activeOrganizationId;
|
|
114
407
|
}
|
|
115
408
|
function setActiveOrganizationId(organizationId) {
|
|
116
|
-
|
|
409
|
+
updateActiveAccount({ activeOrganizationId: organizationId });
|
|
117
410
|
}
|
|
118
411
|
function getUser() {
|
|
119
|
-
return
|
|
412
|
+
return getActiveAccount()?.user;
|
|
120
413
|
}
|
|
121
|
-
function
|
|
122
|
-
|
|
414
|
+
function getUnifiedRole() {
|
|
415
|
+
return normalizeOrgRole(getActiveAccount()?.role);
|
|
123
416
|
}
|
|
124
|
-
function
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
function setRole(role) {
|
|
128
|
-
config.set("role", role);
|
|
417
|
+
function setUnifiedRole(role) {
|
|
418
|
+
updateActiveAccount({ role });
|
|
129
419
|
}
|
|
130
420
|
function isAuthenticated() {
|
|
131
|
-
return
|
|
421
|
+
return getActiveAccount() !== void 0;
|
|
132
422
|
}
|
|
133
423
|
function clearAuth() {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
424
|
+
const id = getActiveAccountId();
|
|
425
|
+
const result = id ? removeAccount(id) : { newActiveId: void 0 };
|
|
426
|
+
try {
|
|
427
|
+
clearRunCache();
|
|
428
|
+
} catch {
|
|
429
|
+
}
|
|
430
|
+
return { newActiveId: result.newActiveId };
|
|
138
431
|
}
|
|
139
432
|
function clearConfig() {
|
|
140
433
|
config.clear();
|
|
@@ -143,16 +436,13 @@ function getConfigPath() {
|
|
|
143
436
|
return config.path;
|
|
144
437
|
}
|
|
145
438
|
|
|
146
|
-
// src/ui/render-tui.tsx
|
|
147
|
-
import chalk from "chalk";
|
|
148
|
-
|
|
149
439
|
// src/ui/execute-command.ts
|
|
150
440
|
import { spawn } from "child_process";
|
|
151
|
-
import { dirname, resolve } from "path";
|
|
441
|
+
import { dirname as dirname2, resolve } from "path";
|
|
152
442
|
import { fileURLToPath } from "url";
|
|
153
443
|
async function executeCommand(argv) {
|
|
154
444
|
const scriptPath = resolve(
|
|
155
|
-
|
|
445
|
+
dirname2(fileURLToPath(import.meta.url)),
|
|
156
446
|
"index.js"
|
|
157
447
|
);
|
|
158
448
|
if (process.stdin.isTTY && process.stdin.isRaw) {
|
|
@@ -177,9 +467,10 @@ async function executeCommand(argv) {
|
|
|
177
467
|
// src/ui/render-tui.tsx
|
|
178
468
|
import { jsx } from "react/jsx-runtime";
|
|
179
469
|
async function openTUI() {
|
|
180
|
-
const [{ render }, { CLIApp }] = await Promise.all([
|
|
470
|
+
const [{ render }, { CLIApp }, { PressAnyKey }] = await Promise.all([
|
|
181
471
|
import("ink"),
|
|
182
|
-
import("./app-
|
|
472
|
+
import("./app-3LJYJ4RK.js"),
|
|
473
|
+
import("./press-any-key-64XFP4O2.js")
|
|
183
474
|
]);
|
|
184
475
|
while (true) {
|
|
185
476
|
let selectedArgv = null;
|
|
@@ -201,24 +492,18 @@ async function openTUI() {
|
|
|
201
492
|
if (code !== 0) {
|
|
202
493
|
process.exitCode = code;
|
|
203
494
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
process.stdin.resume();
|
|
213
|
-
process.stdin.once("data", (data) => {
|
|
214
|
-
const ch = data.toString();
|
|
215
|
-
if (process.stdin.isTTY) {
|
|
216
|
-
process.stdin.setRawMode(false);
|
|
495
|
+
let quit = false;
|
|
496
|
+
const prompt = render(
|
|
497
|
+
/* @__PURE__ */ jsx(
|
|
498
|
+
PressAnyKey,
|
|
499
|
+
{
|
|
500
|
+
onResolve: (q) => {
|
|
501
|
+
quit = q;
|
|
502
|
+
}
|
|
217
503
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
});
|
|
504
|
+
)
|
|
505
|
+
);
|
|
506
|
+
await prompt.waitUntilExit();
|
|
222
507
|
if (quit) {
|
|
223
508
|
break;
|
|
224
509
|
}
|
|
@@ -229,7 +514,7 @@ async function openTUI() {
|
|
|
229
514
|
import { Command } from "commander";
|
|
230
515
|
|
|
231
516
|
// src/lib/ui.ts
|
|
232
|
-
import
|
|
517
|
+
import chalk from "chalk";
|
|
233
518
|
import ora from "ora";
|
|
234
519
|
function createSpinner(text) {
|
|
235
520
|
return ora({
|
|
@@ -250,25 +535,25 @@ async function withSpinner(text, operation, options) {
|
|
|
250
535
|
}
|
|
251
536
|
}
|
|
252
537
|
function success(message) {
|
|
253
|
-
console.log(
|
|
538
|
+
console.log(chalk.green("\u2713"), message);
|
|
254
539
|
}
|
|
255
540
|
function info(message) {
|
|
256
|
-
console.log(
|
|
541
|
+
console.log(chalk.blue("\u2139"), message);
|
|
257
542
|
}
|
|
258
543
|
function warning(message) {
|
|
259
|
-
console.log(
|
|
544
|
+
console.log(chalk.yellow("\u26A0"), message);
|
|
260
545
|
}
|
|
261
546
|
function error(message) {
|
|
262
|
-
console.log(
|
|
547
|
+
console.log(chalk.red("\u2717"), message);
|
|
263
548
|
}
|
|
264
549
|
function header(text) {
|
|
265
550
|
console.log();
|
|
266
|
-
console.log(
|
|
267
|
-
console.log(
|
|
551
|
+
console.log(chalk.bold(text));
|
|
552
|
+
console.log(chalk.dim("\u2500".repeat(text.length)));
|
|
268
553
|
}
|
|
269
554
|
function table(data, columns) {
|
|
270
555
|
if (data.length === 0) {
|
|
271
|
-
console.log(
|
|
556
|
+
console.log(chalk.dim("No data to display"));
|
|
272
557
|
return;
|
|
273
558
|
}
|
|
274
559
|
const widths = columns.map((col) => {
|
|
@@ -279,8 +564,8 @@ function table(data, columns) {
|
|
|
279
564
|
return col.width ?? Math.max(headerWidth, maxDataWidth);
|
|
280
565
|
});
|
|
281
566
|
const headerLine = columns.map((col, i) => col.header.padEnd(widths[i])).join(" ");
|
|
282
|
-
console.log(
|
|
283
|
-
console.log(
|
|
567
|
+
console.log(chalk.bold(headerLine));
|
|
568
|
+
console.log(chalk.dim("\u2500".repeat(headerLine.length)));
|
|
284
569
|
for (const row of data) {
|
|
285
570
|
const line2 = columns.map((col, i) => String(row[col.key] ?? "").padEnd(widths[i])).join(" ");
|
|
286
571
|
console.log(line2);
|
|
@@ -290,23 +575,23 @@ function keyValue(pairs) {
|
|
|
290
575
|
const maxKeyLength = Math.max(...pairs.map(([key]) => key.length));
|
|
291
576
|
for (const [key, value] of pairs) {
|
|
292
577
|
const paddedKey = key.padEnd(maxKeyLength);
|
|
293
|
-
console.log(`${
|
|
578
|
+
console.log(`${chalk.dim(paddedKey)} ${value ?? chalk.dim("(not set)")}`);
|
|
294
579
|
}
|
|
295
580
|
}
|
|
296
581
|
function diff(added, removed, changed) {
|
|
297
582
|
if (Object.keys(added).length === 0 && Object.keys(removed).length === 0 && Object.keys(changed).length === 0) {
|
|
298
|
-
console.log(
|
|
583
|
+
console.log(chalk.dim("No changes"));
|
|
299
584
|
return;
|
|
300
585
|
}
|
|
301
586
|
for (const [key, value] of Object.entries(added)) {
|
|
302
|
-
console.log(
|
|
587
|
+
console.log(chalk.green(`+ ${key}=${maskValue(value)}`));
|
|
303
588
|
}
|
|
304
589
|
for (const [key, value] of Object.entries(removed)) {
|
|
305
|
-
console.log(
|
|
590
|
+
console.log(chalk.red(`- ${key}=${maskValue(value)}`));
|
|
306
591
|
}
|
|
307
592
|
for (const [key, { local, remote }] of Object.entries(changed)) {
|
|
308
|
-
console.log(
|
|
309
|
-
console.log(
|
|
593
|
+
console.log(chalk.red(`- ${key}=${maskValue(remote)}`));
|
|
594
|
+
console.log(chalk.green(`+ ${key}=${maskValue(local)}`));
|
|
310
595
|
}
|
|
311
596
|
}
|
|
312
597
|
function maskValue(value, showChars = 4) {
|
|
@@ -316,56 +601,51 @@ function maskValue(value, showChars = 4) {
|
|
|
316
601
|
return value.slice(0, showChars) + "****" + value.slice(-showChars);
|
|
317
602
|
}
|
|
318
603
|
function line() {
|
|
319
|
-
console.log(
|
|
604
|
+
console.log(chalk.dim("\u2500".repeat(50)));
|
|
320
605
|
}
|
|
321
606
|
function blank() {
|
|
322
607
|
console.log();
|
|
323
608
|
}
|
|
324
609
|
function formatRole(role) {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
610
|
+
const label = formatRoleLabel(role);
|
|
611
|
+
switch (normalizeOrgRole(role)) {
|
|
612
|
+
case "owner":
|
|
613
|
+
return chalk.green(label);
|
|
614
|
+
case "project_manager":
|
|
615
|
+
return chalk.magenta(label);
|
|
328
616
|
case "team_lead":
|
|
329
|
-
return
|
|
330
|
-
case "
|
|
331
|
-
return
|
|
332
|
-
default:
|
|
333
|
-
return chalk2.dim("Unknown");
|
|
617
|
+
return chalk.blue(label);
|
|
618
|
+
case "developer":
|
|
619
|
+
return chalk.yellow(label);
|
|
334
620
|
}
|
|
335
621
|
}
|
|
336
622
|
function roleNotice(role) {
|
|
337
|
-
if (role === "
|
|
623
|
+
if (normalizeOrgRole(role) === "developer") {
|
|
338
624
|
console.log(
|
|
339
|
-
|
|
340
|
-
" You have
|
|
625
|
+
chalk.yellow(
|
|
626
|
+
" You have Developer access. You can read the variables assigned to you and write only those you hold a write grant for; keys without a grant are skipped, not queued for approval."
|
|
341
627
|
)
|
|
342
628
|
);
|
|
343
629
|
}
|
|
344
630
|
}
|
|
345
631
|
function formatProjectRole(role) {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
return chalk2.green("Manager");
|
|
349
|
-
case "developer":
|
|
350
|
-
return chalk2.blue("Developer");
|
|
351
|
-
case "viewer":
|
|
352
|
-
return chalk2.yellow("Viewer");
|
|
353
|
-
default:
|
|
354
|
-
return chalk2.dim("-");
|
|
632
|
+
if (role === void 0 || role === null || role === "") {
|
|
633
|
+
return chalk.dim("-");
|
|
355
634
|
}
|
|
635
|
+
return formatRole(role);
|
|
356
636
|
}
|
|
357
|
-
function projectRoleNotice(
|
|
358
|
-
if (
|
|
637
|
+
function projectRoleNotice(role) {
|
|
638
|
+
if (normalizeOrgRole(role) === "developer") {
|
|
359
639
|
console.log(
|
|
360
|
-
|
|
361
|
-
" You have
|
|
640
|
+
chalk.yellow(
|
|
641
|
+
" You have Developer access to this project. You can only see variables you have been granted, and your assignment may exclude some environments such as production."
|
|
362
642
|
)
|
|
363
643
|
);
|
|
364
644
|
}
|
|
365
645
|
}
|
|
366
646
|
|
|
367
647
|
// src/lib/errors.ts
|
|
368
|
-
import
|
|
648
|
+
import chalk2 from "chalk";
|
|
369
649
|
var CLIError = class extends Error {
|
|
370
650
|
constructor(message, code, suggestion) {
|
|
371
651
|
super(message);
|
|
@@ -390,17 +670,17 @@ var ErrorCodes = {
|
|
|
390
670
|
};
|
|
391
671
|
function formatError(error2) {
|
|
392
672
|
if (error2 instanceof CLIError) {
|
|
393
|
-
let message =
|
|
673
|
+
let message = chalk2.red(`Error: ${error2.message}`);
|
|
394
674
|
if (error2.suggestion) {
|
|
395
675
|
message += `
|
|
396
|
-
${
|
|
676
|
+
${chalk2.yellow("Suggestion:")} ${error2.suggestion}`;
|
|
397
677
|
}
|
|
398
678
|
return message;
|
|
399
679
|
}
|
|
400
680
|
if (error2 instanceof Error) {
|
|
401
|
-
return
|
|
681
|
+
return chalk2.red(`Error: ${error2.message}`);
|
|
402
682
|
}
|
|
403
|
-
return
|
|
683
|
+
return chalk2.red(`Error: ${String(error2)}`);
|
|
404
684
|
}
|
|
405
685
|
async function handleError(error2) {
|
|
406
686
|
console.error(formatError(error2));
|
|
@@ -457,7 +737,7 @@ function invalidInput(message) {
|
|
|
457
737
|
|
|
458
738
|
// src/lib/auth-flow.ts
|
|
459
739
|
import open from "open";
|
|
460
|
-
import
|
|
740
|
+
import chalk3 from "chalk";
|
|
461
741
|
import { hostname } from "os";
|
|
462
742
|
|
|
463
743
|
// src/lib/api.ts
|
|
@@ -478,6 +758,9 @@ var APIError = class extends Error {
|
|
|
478
758
|
var APIClient = class {
|
|
479
759
|
baseUrl;
|
|
480
760
|
accessToken;
|
|
761
|
+
// Re-entrancy guard: true while a token refresh is in flight so a 401 from
|
|
762
|
+
// the refresh endpoint itself can never trigger another refresh (no loops).
|
|
763
|
+
refreshing = false;
|
|
481
764
|
constructor(options) {
|
|
482
765
|
this.baseUrl = options?.baseUrl ?? getApiUrl();
|
|
483
766
|
this.accessToken = options?.accessToken ?? getAccessToken();
|
|
@@ -556,70 +839,138 @@ var APIClient = class {
|
|
|
556
839
|
"TOO_MANY_REDIRECTS"
|
|
557
840
|
);
|
|
558
841
|
}
|
|
842
|
+
/**
|
|
843
|
+
* Attempt a one-shot access-token refresh using the stored refresh token.
|
|
844
|
+
* Persists the rotated tokens and updates this client's in-memory token on
|
|
845
|
+
* success. Returns false (without throwing) when there is no refresh token,
|
|
846
|
+
* a refresh is already in flight, or the refresh call fails — the caller
|
|
847
|
+
* then treats the session as dead.
|
|
848
|
+
*/
|
|
849
|
+
async tryRefreshToken() {
|
|
850
|
+
if (this.refreshing) return false;
|
|
851
|
+
const refreshToken = getRefreshToken();
|
|
852
|
+
if (!refreshToken) return false;
|
|
853
|
+
this.refreshing = true;
|
|
854
|
+
try {
|
|
855
|
+
const result = await this.refreshToken(refreshToken);
|
|
856
|
+
setAccessToken(result.accessToken);
|
|
857
|
+
setRefreshToken(result.refreshToken);
|
|
858
|
+
this.accessToken = result.accessToken;
|
|
859
|
+
return true;
|
|
860
|
+
} catch {
|
|
861
|
+
return false;
|
|
862
|
+
} finally {
|
|
863
|
+
this.refreshing = false;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Run a request thunk with automatic one-shot token refresh on a genuine 401.
|
|
868
|
+
*
|
|
869
|
+
* - An AUTH_REDIRECT (HTML sign-in page) is NOT a rejected token — rethrow
|
|
870
|
+
* without clearing creds or refreshing.
|
|
871
|
+
* - A genuine 401 triggers a single refresh attempt. On success the request
|
|
872
|
+
* is retried once with the rotated token; on failure (or a second 401) the
|
|
873
|
+
* local credentials are cleared so the user is prompted to log in again.
|
|
874
|
+
*
|
|
875
|
+
* The thunk rebuilds its headers on each call, so the retry automatically
|
|
876
|
+
* picks up the refreshed access token.
|
|
877
|
+
*/
|
|
878
|
+
async withAuthRetry(exec) {
|
|
879
|
+
try {
|
|
880
|
+
return await exec();
|
|
881
|
+
} catch (err) {
|
|
882
|
+
if (!(err instanceof APIError) || err.statusCode !== 401 || err.code === "AUTH_REDIRECT" || this.refreshing) {
|
|
883
|
+
throw err;
|
|
884
|
+
}
|
|
885
|
+
const refreshed = await this.tryRefreshToken();
|
|
886
|
+
if (!refreshed) {
|
|
887
|
+
clearAuth();
|
|
888
|
+
throw err;
|
|
889
|
+
}
|
|
890
|
+
try {
|
|
891
|
+
return await exec();
|
|
892
|
+
} catch (retryErr) {
|
|
893
|
+
if (retryErr instanceof APIError && retryErr.statusCode === 401 && retryErr.code !== "AUTH_REDIRECT") {
|
|
894
|
+
clearAuth();
|
|
895
|
+
}
|
|
896
|
+
throw retryErr;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
559
900
|
/**
|
|
560
901
|
* Make a GET request
|
|
561
902
|
*/
|
|
562
903
|
async get(path, params) {
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
904
|
+
return this.withAuthRetry(async () => {
|
|
905
|
+
const url = new URL(path, this.baseUrl);
|
|
906
|
+
if (params) {
|
|
907
|
+
for (const [key, value] of Object.entries(params)) {
|
|
908
|
+
url.searchParams.set(key, value);
|
|
909
|
+
}
|
|
567
910
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
911
|
+
const response = await this.fetchWithSafeRedirects(url.toString(), {
|
|
912
|
+
method: "GET",
|
|
913
|
+
headers: this.getHeaders()
|
|
914
|
+
});
|
|
915
|
+
return this.handleResponse(response);
|
|
572
916
|
});
|
|
573
|
-
return this.handleResponse(response);
|
|
574
917
|
}
|
|
575
918
|
/**
|
|
576
919
|
* Make a POST request
|
|
577
920
|
*/
|
|
578
921
|
async post(path, body) {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
922
|
+
return this.withAuthRetry(async () => {
|
|
923
|
+
const url = new URL(path, this.baseUrl);
|
|
924
|
+
const response = await this.fetchWithSafeRedirects(url.toString(), {
|
|
925
|
+
method: "POST",
|
|
926
|
+
headers: this.getHeaders(),
|
|
927
|
+
body: body ? JSON.stringify(body) : void 0
|
|
928
|
+
});
|
|
929
|
+
return this.handleResponse(response);
|
|
584
930
|
});
|
|
585
|
-
return this.handleResponse(response);
|
|
586
931
|
}
|
|
587
932
|
/**
|
|
588
933
|
* Make a PUT request
|
|
589
934
|
*/
|
|
590
935
|
async put(path, body) {
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
936
|
+
return this.withAuthRetry(async () => {
|
|
937
|
+
const url = new URL(path, this.baseUrl);
|
|
938
|
+
const response = await this.fetchWithSafeRedirects(url.toString(), {
|
|
939
|
+
method: "PUT",
|
|
940
|
+
headers: this.getHeaders(),
|
|
941
|
+
body: body ? JSON.stringify(body) : void 0
|
|
942
|
+
});
|
|
943
|
+
return this.handleResponse(response);
|
|
596
944
|
});
|
|
597
|
-
return this.handleResponse(response);
|
|
598
945
|
}
|
|
599
946
|
/**
|
|
600
947
|
* Make a PATCH request
|
|
601
948
|
*/
|
|
602
949
|
async patch(path, body) {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
950
|
+
return this.withAuthRetry(async () => {
|
|
951
|
+
const url = new URL(path, this.baseUrl);
|
|
952
|
+
const response = await this.fetchWithSafeRedirects(url.toString(), {
|
|
953
|
+
method: "PATCH",
|
|
954
|
+
headers: this.getHeaders(),
|
|
955
|
+
body: body ? JSON.stringify(body) : void 0
|
|
956
|
+
});
|
|
957
|
+
return this.handleResponse(response);
|
|
608
958
|
});
|
|
609
|
-
return this.handleResponse(response);
|
|
610
959
|
}
|
|
611
960
|
/**
|
|
612
961
|
* Make a DELETE request
|
|
613
962
|
*/
|
|
614
963
|
async delete(path) {
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
964
|
+
return this.withAuthRetry(async () => {
|
|
965
|
+
const url = new URL(path, this.baseUrl);
|
|
966
|
+
const response = await this.fetchWithSafeRedirects(url.toString(), {
|
|
967
|
+
method: "DELETE",
|
|
968
|
+
headers: this.getHeaders()
|
|
969
|
+
});
|
|
970
|
+
if (!response.ok) {
|
|
971
|
+
await this.handleError(response);
|
|
972
|
+
}
|
|
619
973
|
});
|
|
620
|
-
if (!response.ok) {
|
|
621
|
-
await this.handleError(response);
|
|
622
|
-
}
|
|
623
974
|
}
|
|
624
975
|
/**
|
|
625
976
|
* Handle API response
|
|
@@ -660,18 +1011,25 @@ var APIClient = class {
|
|
|
660
1011
|
* Handle API errors
|
|
661
1012
|
*/
|
|
662
1013
|
async handleError(response) {
|
|
1014
|
+
const bodyText = await response.text();
|
|
663
1015
|
let message = `Request failed with status ${response.status}`;
|
|
664
1016
|
let code;
|
|
665
1017
|
try {
|
|
666
|
-
const data =
|
|
1018
|
+
const data = JSON.parse(bodyText);
|
|
667
1019
|
message = data.error || data.message || message;
|
|
668
1020
|
code = data.code;
|
|
669
1021
|
} catch {
|
|
670
1022
|
}
|
|
671
1023
|
if (response.status === 401) {
|
|
672
|
-
|
|
1024
|
+
if (this.isAuthRedirect(response, bodyText)) {
|
|
1025
|
+
throw new APIError(
|
|
1026
|
+
"Your CLI session is not authorized for this endpoint. Please run `envpilot login` and try again.",
|
|
1027
|
+
401,
|
|
1028
|
+
"AUTH_REDIRECT"
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
673
1031
|
throw new APIError(
|
|
674
|
-
"Authentication failed. Please run `envpilot login`.",
|
|
1032
|
+
message || "Authentication failed. Please run `envpilot login`.",
|
|
675
1033
|
401,
|
|
676
1034
|
code || "UNAUTHORIZED"
|
|
677
1035
|
);
|
|
@@ -865,12 +1223,12 @@ async function performLogin(options) {
|
|
|
865
1223
|
}
|
|
866
1224
|
spinner.stop();
|
|
867
1225
|
console.log();
|
|
868
|
-
console.log(
|
|
1226
|
+
console.log(chalk3.bold("Your authentication code:"));
|
|
869
1227
|
console.log();
|
|
870
|
-
console.log(
|
|
1228
|
+
console.log(chalk3.cyan.bold(` ${initResponse.code}`));
|
|
871
1229
|
console.log();
|
|
872
1230
|
console.log(`Open this URL to authenticate:`);
|
|
873
|
-
console.log(
|
|
1231
|
+
console.log(chalk3.dim(initResponse.url));
|
|
874
1232
|
console.log();
|
|
875
1233
|
if (options?.browser !== false) {
|
|
876
1234
|
info("Opening browser...");
|
|
@@ -902,17 +1260,21 @@ async function performLogin(options) {
|
|
|
902
1260
|
"Authentication succeeded but the server did not return session tokens. This is likely a server-side issue. Please try again or contact support."
|
|
903
1261
|
);
|
|
904
1262
|
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1263
|
+
const accountId = pollResponse.user?.id ?? `session-${pollResponse.accessToken.slice(0, 8)}`;
|
|
1264
|
+
const account = {
|
|
1265
|
+
id: accountId,
|
|
1266
|
+
user: pollResponse.user ? {
|
|
909
1267
|
id: pollResponse.user.id,
|
|
910
1268
|
email: pollResponse.user.email,
|
|
911
1269
|
name: pollResponse.user.name
|
|
912
|
-
}
|
|
913
|
-
|
|
1270
|
+
} : { id: accountId, email: `${accountId}@cli.local` },
|
|
1271
|
+
accessToken: pollResponse.accessToken,
|
|
1272
|
+
refreshToken: pollResponse.refreshToken
|
|
1273
|
+
};
|
|
1274
|
+
upsertAccount(account);
|
|
1275
|
+
setActiveAccount(account.id);
|
|
914
1276
|
console.log();
|
|
915
|
-
success(`Logged in as ${
|
|
1277
|
+
success(`Logged in as ${chalk3.bold(pollResponse.user?.email)}`);
|
|
916
1278
|
return { email: pollResponse.user?.email || "" };
|
|
917
1279
|
}
|
|
918
1280
|
if (pollResponse.status === "expired" || pollResponse.status === "not_found") {
|
|
@@ -934,6 +1296,16 @@ var loginCommand = new Command("login").description("Authenticate with Envpilot"
|
|
|
934
1296
|
await performLogin({
|
|
935
1297
|
browser: options.browser !== false
|
|
936
1298
|
});
|
|
1299
|
+
const activeAccount = getActiveAccount();
|
|
1300
|
+
if (activeAccount) {
|
|
1301
|
+
success(`Signed in as ${activeAccount.user.email}.`);
|
|
1302
|
+
}
|
|
1303
|
+
const accounts = listAccounts();
|
|
1304
|
+
if (accounts.length > 1) {
|
|
1305
|
+
info(
|
|
1306
|
+
`You have ${accounts.length} accounts \u2014 use \`envpilot accounts\` to list/switch.`
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
937
1309
|
console.log();
|
|
938
1310
|
console.log("Next steps:");
|
|
939
1311
|
info(" envpilot init Initialize a project in the current directory");
|
|
@@ -947,13 +1319,19 @@ var loginCommand = new Command("login").description("Authenticate with Envpilot"
|
|
|
947
1319
|
|
|
948
1320
|
// src/commands/init.ts
|
|
949
1321
|
import { Command as Command2 } from "commander";
|
|
950
|
-
import
|
|
1322
|
+
import chalk4 from "chalk";
|
|
951
1323
|
import inquirer from "inquirer";
|
|
952
1324
|
|
|
953
1325
|
// src/lib/project-config.ts
|
|
954
|
-
import {
|
|
1326
|
+
import {
|
|
1327
|
+
readFileSync as readFileSync2,
|
|
1328
|
+
writeFileSync as writeFileSync2,
|
|
1329
|
+
existsSync as existsSync2,
|
|
1330
|
+
unlinkSync as unlinkSync2,
|
|
1331
|
+
renameSync
|
|
1332
|
+
} from "fs";
|
|
955
1333
|
import { execSync } from "child_process";
|
|
956
|
-
import { join } from "path";
|
|
1334
|
+
import { join as join2 } from "path";
|
|
957
1335
|
|
|
958
1336
|
// src/types/index.ts
|
|
959
1337
|
import { z } from "zod";
|
|
@@ -962,12 +1340,25 @@ var userSchema = z.object({
|
|
|
962
1340
|
email: z.string().email(),
|
|
963
1341
|
name: z.string().optional()
|
|
964
1342
|
});
|
|
1343
|
+
var accountSchema = z.object({
|
|
1344
|
+
id: z.string(),
|
|
1345
|
+
user: userSchema,
|
|
1346
|
+
accessToken: z.string(),
|
|
1347
|
+
refreshToken: z.string().optional(),
|
|
1348
|
+
// Stored role string (legacy or unified); normalized on read.
|
|
1349
|
+
role: z.string().optional(),
|
|
1350
|
+
// Per-account active org/project selection.
|
|
1351
|
+
activeOrganizationId: z.string().optional(),
|
|
1352
|
+
activeProjectId: z.string().optional()
|
|
1353
|
+
});
|
|
965
1354
|
var organizationSchema = z.object({
|
|
966
1355
|
_id: z.string(),
|
|
967
1356
|
name: z.string(),
|
|
968
1357
|
slug: z.string(),
|
|
969
1358
|
tier: z.enum(["free", "pro"]),
|
|
970
|
-
role: z.string().optional()
|
|
1359
|
+
role: z.string().optional(),
|
|
1360
|
+
// Unified org role (additive; legacy responses omit it and fall back to role).
|
|
1361
|
+
unifiedRole: z.string().nullable().optional()
|
|
971
1362
|
});
|
|
972
1363
|
var projectSchema = z.object({
|
|
973
1364
|
_id: z.string(),
|
|
@@ -978,7 +1369,11 @@ var projectSchema = z.object({
|
|
|
978
1369
|
icon: z.string().optional(),
|
|
979
1370
|
color: z.string().optional(),
|
|
980
1371
|
userRole: z.string().nullable().optional(),
|
|
981
|
-
projectRole: z.string().nullable().optional()
|
|
1372
|
+
projectRole: z.string().nullable().optional(),
|
|
1373
|
+
// Unified-role fields (optional so legacy server responses still parse)
|
|
1374
|
+
unifiedRole: z.string().nullable().optional(),
|
|
1375
|
+
assigned: z.boolean().optional(),
|
|
1376
|
+
environmentScope: z.array(z.string()).nullable().optional()
|
|
982
1377
|
});
|
|
983
1378
|
var variableTagSchema = z.object({
|
|
984
1379
|
_id: z.string(),
|
|
@@ -996,21 +1391,49 @@ var variableSchema = z.object({
|
|
|
996
1391
|
version: z.number().optional(),
|
|
997
1392
|
updatedAt: z.number().optional(),
|
|
998
1393
|
createdAt: z.number().optional(),
|
|
999
|
-
tags: z.array(variableTagSchema).optional()
|
|
1394
|
+
tags: z.array(variableTagSchema).optional(),
|
|
1395
|
+
// Per-variable effective access under the unified model. Optional so older
|
|
1396
|
+
// server responses (which omit it) still parse.
|
|
1397
|
+
access: z.enum(["read", "write"]).optional()
|
|
1000
1398
|
});
|
|
1399
|
+
var variablesMetaSchema = z.object({
|
|
1400
|
+
total: z.number().optional(),
|
|
1401
|
+
page: z.number().optional(),
|
|
1402
|
+
limit: z.number().optional(),
|
|
1403
|
+
decryptionFailures: z.array(z.string()).optional(),
|
|
1404
|
+
unifiedRole: z.string().nullable().optional(),
|
|
1405
|
+
assigned: z.boolean().optional(),
|
|
1406
|
+
grantOnly: z.boolean().optional(),
|
|
1407
|
+
environmentScope: z.array(z.string()).nullable().optional(),
|
|
1408
|
+
hasWriteAccess: z.boolean().optional(),
|
|
1409
|
+
scopeRestricted: z.boolean().optional()
|
|
1410
|
+
}).passthrough();
|
|
1001
1411
|
var environmentSchema = z.enum([
|
|
1002
1412
|
"development",
|
|
1003
1413
|
"staging",
|
|
1004
1414
|
"production"
|
|
1005
1415
|
]);
|
|
1006
1416
|
var cliConfigSchema = z.object({
|
|
1417
|
+
// Global CLI setting — shared across all accounts.
|
|
1007
1418
|
apiUrl: z.string().url(),
|
|
1419
|
+
// Multi-account store. Keyed by account id (== user id).
|
|
1420
|
+
accounts: z.record(z.string(), accountSchema).optional(),
|
|
1421
|
+
// The currently active account id.
|
|
1422
|
+
activeAccountId: z.string().optional(),
|
|
1423
|
+
// --- Legacy single-account fields ---
|
|
1424
|
+
// Retained ONLY so pre-multi-account configs still parse and can be migrated
|
|
1425
|
+
// into `accounts` by migrateLegacyConfig(). After migration these are deleted
|
|
1426
|
+
// so `accounts` is the single source of truth. Do not read/write directly.
|
|
1008
1427
|
accessToken: z.string().optional(),
|
|
1009
1428
|
refreshToken: z.string().optional(),
|
|
1010
1429
|
activeProjectId: z.string().optional(),
|
|
1011
1430
|
activeOrganizationId: z.string().optional(),
|
|
1012
1431
|
user: userSchema.optional(),
|
|
1013
|
-
role
|
|
1432
|
+
// Stored role string. Kept as a free-form string so both legacy
|
|
1433
|
+
// ("admin"/"team_lead"/"member") and unified
|
|
1434
|
+
// ("owner"/"project_manager"/"team_lead"/"developer") values round-trip.
|
|
1435
|
+
// Normalized on read via getUnifiedRole(); no longer enum-enforced.
|
|
1436
|
+
role: z.string().optional()
|
|
1014
1437
|
});
|
|
1015
1438
|
var projectConfigSchema = z.object({
|
|
1016
1439
|
projectId: z.string(),
|
|
@@ -1022,7 +1445,12 @@ var projectEntrySchema = z.object({
|
|
|
1022
1445
|
organizationId: z.string(),
|
|
1023
1446
|
projectName: z.string().default(""),
|
|
1024
1447
|
organizationName: z.string().default(""),
|
|
1025
|
-
environment: environmentSchema.default("development")
|
|
1448
|
+
environment: environmentSchema.default("development"),
|
|
1449
|
+
// How the pulled .env file's permissions are managed for this project:
|
|
1450
|
+
// auto → derive from the caller's resolved access (default)
|
|
1451
|
+
// always → force read-only (0o400)
|
|
1452
|
+
// never → force writable (0o600)
|
|
1453
|
+
fileProtection: z.enum(["auto", "always", "never"]).optional()
|
|
1026
1454
|
});
|
|
1027
1455
|
var projectConfigV2Schema = z.object({
|
|
1028
1456
|
version: z.literal(1),
|
|
@@ -1032,17 +1460,30 @@ var projectConfigV2Schema = z.object({
|
|
|
1032
1460
|
|
|
1033
1461
|
// src/lib/project-config.ts
|
|
1034
1462
|
var CONFIG_FILE_NAME = ".envpilot";
|
|
1463
|
+
function atomicWriteFileSync(filePath, content) {
|
|
1464
|
+
const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
1465
|
+
try {
|
|
1466
|
+
writeFileSync2(tmpPath, content, "utf-8");
|
|
1467
|
+
renameSync(tmpPath, filePath);
|
|
1468
|
+
} catch (err) {
|
|
1469
|
+
try {
|
|
1470
|
+
if (existsSync2(tmpPath)) unlinkSync2(tmpPath);
|
|
1471
|
+
} catch {
|
|
1472
|
+
}
|
|
1473
|
+
throw err;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1035
1476
|
function getProjectConfigPath(directory = process.cwd()) {
|
|
1036
|
-
return
|
|
1477
|
+
return join2(directory, CONFIG_FILE_NAME);
|
|
1037
1478
|
}
|
|
1038
1479
|
function hasProjectConfig(directory = process.cwd()) {
|
|
1039
|
-
return
|
|
1480
|
+
return existsSync2(getProjectConfigPath(directory));
|
|
1040
1481
|
}
|
|
1041
1482
|
function readRawConfig(directory = process.cwd()) {
|
|
1042
1483
|
const configPath = getProjectConfigPath(directory);
|
|
1043
|
-
if (!
|
|
1484
|
+
if (!existsSync2(configPath)) return null;
|
|
1044
1485
|
try {
|
|
1045
|
-
return JSON.parse(
|
|
1486
|
+
return JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
1046
1487
|
} catch {
|
|
1047
1488
|
return null;
|
|
1048
1489
|
}
|
|
@@ -1089,7 +1530,7 @@ function readProjectConfigV2(directory = process.cwd()) {
|
|
|
1089
1530
|
}
|
|
1090
1531
|
function writeProjectConfigV2(config2, directory = process.cwd()) {
|
|
1091
1532
|
const configPath = getProjectConfigPath(directory);
|
|
1092
|
-
|
|
1533
|
+
atomicWriteFileSync(configPath, JSON.stringify(config2, null, 2) + "\n");
|
|
1093
1534
|
}
|
|
1094
1535
|
function getActiveProject(config2) {
|
|
1095
1536
|
return config2.projects.find((p) => p.projectId === config2.activeProjectId) || config2.projects[0] || null;
|
|
@@ -1182,23 +1623,23 @@ function updateProjectConfig(updates, directory = process.cwd()) {
|
|
|
1182
1623
|
}
|
|
1183
1624
|
function deleteProjectConfig(directory = process.cwd()) {
|
|
1184
1625
|
const configPath = getProjectConfigPath(directory);
|
|
1185
|
-
if (!
|
|
1186
|
-
|
|
1626
|
+
if (!existsSync2(configPath)) return false;
|
|
1627
|
+
unlinkSync2(configPath);
|
|
1187
1628
|
return true;
|
|
1188
1629
|
}
|
|
1189
1630
|
function ensureEnvInGitignore(directory = process.cwd()) {
|
|
1190
|
-
const gitignorePath =
|
|
1191
|
-
if (!
|
|
1192
|
-
|
|
1631
|
+
const gitignorePath = join2(directory, ".gitignore");
|
|
1632
|
+
if (!existsSync2(gitignorePath)) {
|
|
1633
|
+
atomicWriteFileSync(gitignorePath, ".env\n.env.local\n");
|
|
1193
1634
|
return;
|
|
1194
1635
|
}
|
|
1195
|
-
const content =
|
|
1636
|
+
const content = readFileSync2(gitignorePath, "utf-8");
|
|
1196
1637
|
const lines = content.split("\n");
|
|
1197
1638
|
if (lines.some((line2) => line2.trim() === ".env")) {
|
|
1198
1639
|
return;
|
|
1199
1640
|
}
|
|
1200
1641
|
const newContent = content.endsWith("\n") ? content + ".env\n" : content + "\n.env\n";
|
|
1201
|
-
|
|
1642
|
+
atomicWriteFileSync(gitignorePath, newContent);
|
|
1202
1643
|
}
|
|
1203
1644
|
function getTrackedEnvFiles(directory = process.cwd()) {
|
|
1204
1645
|
try {
|
|
@@ -1214,8 +1655,14 @@ function getTrackedEnvFiles(directory = process.cwd()) {
|
|
|
1214
1655
|
}
|
|
1215
1656
|
|
|
1216
1657
|
// src/lib/env-file.ts
|
|
1217
|
-
import {
|
|
1218
|
-
|
|
1658
|
+
import {
|
|
1659
|
+
readFileSync as readFileSync3,
|
|
1660
|
+
writeFileSync as writeFileSync3,
|
|
1661
|
+
existsSync as existsSync3,
|
|
1662
|
+
chmodSync,
|
|
1663
|
+
renameSync as renameSync2
|
|
1664
|
+
} from "fs";
|
|
1665
|
+
import { join as join3 } from "path";
|
|
1219
1666
|
function parseEnvFile(content) {
|
|
1220
1667
|
const result = {};
|
|
1221
1668
|
const lines = content.split("\n");
|
|
@@ -1228,7 +1675,7 @@ function parseEnvFile(content) {
|
|
|
1228
1675
|
if (equalsIndex === -1) {
|
|
1229
1676
|
continue;
|
|
1230
1677
|
}
|
|
1231
|
-
const key = line2.substring(0, equalsIndex).trim();
|
|
1678
|
+
const key = line2.substring(0, equalsIndex).trim().replace(/^export\s+/, "");
|
|
1232
1679
|
let value = line2.substring(equalsIndex + 1);
|
|
1233
1680
|
value = parseValue(value);
|
|
1234
1681
|
if (isValidEnvKey(key)) {
|
|
@@ -1239,17 +1686,21 @@ function parseEnvFile(content) {
|
|
|
1239
1686
|
}
|
|
1240
1687
|
function parseValue(value) {
|
|
1241
1688
|
value = value.trim();
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1689
|
+
const quote = value[0];
|
|
1690
|
+
if (quote === '"' || quote === "'") {
|
|
1691
|
+
const closingIndex = value.indexOf(quote, 1);
|
|
1692
|
+
if (closingIndex !== -1) {
|
|
1693
|
+
const inner = value.slice(1, closingIndex);
|
|
1694
|
+
if (quote === '"') {
|
|
1695
|
+
return inner.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
1696
|
+
}
|
|
1697
|
+
return inner;
|
|
1251
1698
|
}
|
|
1252
1699
|
}
|
|
1700
|
+
const commentIndex = value.indexOf(" #");
|
|
1701
|
+
if (commentIndex !== -1) {
|
|
1702
|
+
value = value.substring(0, commentIndex).trim();
|
|
1703
|
+
}
|
|
1253
1704
|
return value;
|
|
1254
1705
|
}
|
|
1255
1706
|
function isValidEnvKey(key) {
|
|
@@ -1301,33 +1752,53 @@ function diffEnvVars(local, remote) {
|
|
|
1301
1752
|
return { added, removed, changed, unchanged };
|
|
1302
1753
|
}
|
|
1303
1754
|
function readEnvFile(filePath) {
|
|
1304
|
-
if (!
|
|
1755
|
+
if (!existsSync3(filePath)) {
|
|
1305
1756
|
return null;
|
|
1306
1757
|
}
|
|
1307
|
-
const content =
|
|
1758
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
1308
1759
|
return parseEnvFile(content);
|
|
1309
1760
|
}
|
|
1761
|
+
function atomicWriteFileSync2(filePath, content, mode) {
|
|
1762
|
+
const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
1763
|
+
try {
|
|
1764
|
+
writeFileSync3(tmpPath, content, { encoding: "utf-8", mode });
|
|
1765
|
+
renameSync2(tmpPath, filePath);
|
|
1766
|
+
} catch (err) {
|
|
1767
|
+
try {
|
|
1768
|
+
if (existsSync3(tmpPath)) chmodSync(tmpPath, 384);
|
|
1769
|
+
} catch {
|
|
1770
|
+
}
|
|
1771
|
+
throw err;
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1310
1774
|
function writeEnvFile(filePath, vars, options) {
|
|
1311
1775
|
const content = stringifyEnv(vars, options);
|
|
1312
|
-
|
|
1776
|
+
atomicWriteFileSync2(filePath, content, 384);
|
|
1313
1777
|
}
|
|
1314
1778
|
function getEnvPathForEnvironment(environment, directory = process.cwd()) {
|
|
1315
1779
|
if (environment === "development") {
|
|
1316
|
-
return
|
|
1780
|
+
return join3(directory, ".env.local");
|
|
1317
1781
|
}
|
|
1318
|
-
return
|
|
1782
|
+
return join3(directory, `.env.${environment}`);
|
|
1319
1783
|
}
|
|
1320
|
-
function applyFileProtection(filePath,
|
|
1321
|
-
if (!
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1784
|
+
function applyFileProtection(filePath, access, protection = "auto") {
|
|
1785
|
+
if (!existsSync3(filePath)) return;
|
|
1786
|
+
if (protection === "never") {
|
|
1787
|
+
chmodSync(filePath, 384);
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
if (protection === "always") {
|
|
1791
|
+
chmodSync(filePath, 256);
|
|
1792
|
+
return;
|
|
1327
1793
|
}
|
|
1794
|
+
chmodSync(filePath, isFileWritable(access) ? 384 : 256);
|
|
1328
1795
|
}
|
|
1329
1796
|
|
|
1330
1797
|
// src/commands/init.ts
|
|
1798
|
+
function orgUnifiedRole(org) {
|
|
1799
|
+
const unified = org.unifiedRole;
|
|
1800
|
+
return normalizeOrgRole(unified ?? org.role);
|
|
1801
|
+
}
|
|
1331
1802
|
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(
|
|
1332
1803
|
"-e, --environment <env>",
|
|
1333
1804
|
"Default environment (development, staging, production)"
|
|
@@ -1376,9 +1847,7 @@ var initCommand = new Command2("init").description("Initialize Envpilot in the c
|
|
|
1376
1847
|
});
|
|
1377
1848
|
setActiveOrganizationId(selectedOrg._id);
|
|
1378
1849
|
setActiveProjectId(selectedProject._id);
|
|
1379
|
-
|
|
1380
|
-
setRole(selectedOrg.role);
|
|
1381
|
-
}
|
|
1850
|
+
setUnifiedRole(orgUnifiedRole(selectedOrg));
|
|
1382
1851
|
ensureEnvInGitignore();
|
|
1383
1852
|
warnTrackedFiles();
|
|
1384
1853
|
console.log();
|
|
@@ -1390,21 +1859,23 @@ var initCommand = new Command2("init").description("Initialize Envpilot in the c
|
|
|
1390
1859
|
});
|
|
1391
1860
|
async function addProject(existingConfig, options) {
|
|
1392
1861
|
const api = createAPIClient();
|
|
1393
|
-
let role =
|
|
1394
|
-
if (role
|
|
1862
|
+
let role = getUnifiedRole();
|
|
1863
|
+
if (roleLevel(role) < ROLE_LEVEL.team_lead) {
|
|
1395
1864
|
const orgs = await withSpinner("Checking permissions...", async () => {
|
|
1396
1865
|
const response = await api.get("/api/cli/organizations");
|
|
1397
1866
|
return response.data || [];
|
|
1398
1867
|
});
|
|
1399
|
-
const
|
|
1868
|
+
const freshOrg = orgs.find(
|
|
1400
1869
|
(o) => o._id === existingConfig.projects[0]?.organizationId
|
|
1401
|
-
)
|
|
1402
|
-
if (
|
|
1403
|
-
|
|
1404
|
-
role
|
|
1870
|
+
);
|
|
1871
|
+
if (freshOrg) {
|
|
1872
|
+
role = orgUnifiedRole(freshOrg);
|
|
1873
|
+
setUnifiedRole(role);
|
|
1405
1874
|
}
|
|
1406
|
-
if (role
|
|
1407
|
-
error(
|
|
1875
|
+
if (roleLevel(role) < ROLE_LEVEL.team_lead) {
|
|
1876
|
+
error(
|
|
1877
|
+
`Only team leads and above can link multiple projects (your role: ${formatRoleLabel(role)}).`
|
|
1878
|
+
);
|
|
1408
1879
|
info("Unlink the current project first with `envpilot unlink`.");
|
|
1409
1880
|
process.exit(1);
|
|
1410
1881
|
}
|
|
@@ -1452,18 +1923,18 @@ async function addProject(existingConfig, options) {
|
|
|
1452
1923
|
console.log();
|
|
1453
1924
|
success(`Added "${selectedProject.name}" to linked projects!`);
|
|
1454
1925
|
console.log(
|
|
1455
|
-
|
|
1926
|
+
chalk4.dim(` ${existingConfig.projects.length + 1} projects now linked`)
|
|
1456
1927
|
);
|
|
1457
1928
|
console.log();
|
|
1458
1929
|
console.log("Next steps:");
|
|
1459
1930
|
console.log(
|
|
1460
|
-
` ${
|
|
1931
|
+
` ${chalk4.cyan("envpilot pull --all")} Pull all projects`
|
|
1461
1932
|
);
|
|
1462
1933
|
console.log(
|
|
1463
|
-
` ${
|
|
1934
|
+
` ${chalk4.cyan(`envpilot pull --project "${selectedProject.name}"`)} Pull this project`
|
|
1464
1935
|
);
|
|
1465
1936
|
console.log(
|
|
1466
|
-
` ${
|
|
1937
|
+
` ${chalk4.cyan("envpilot list linked")} See all linked projects`
|
|
1467
1938
|
);
|
|
1468
1939
|
console.log();
|
|
1469
1940
|
}
|
|
@@ -1492,7 +1963,7 @@ async function selectOrgProjectEnv(options) {
|
|
|
1492
1963
|
selectedOrg = org;
|
|
1493
1964
|
} else if (organizations.length === 1) {
|
|
1494
1965
|
selectedOrg = organizations[0];
|
|
1495
|
-
info(`Using organization: ${
|
|
1966
|
+
info(`Using organization: ${chalk4.bold(selectedOrg.name)}`);
|
|
1496
1967
|
} else {
|
|
1497
1968
|
const { orgId } = await inquirer.prompt([
|
|
1498
1969
|
{
|
|
@@ -1500,7 +1971,7 @@ async function selectOrgProjectEnv(options) {
|
|
|
1500
1971
|
name: "orgId",
|
|
1501
1972
|
message: "Select an organization:",
|
|
1502
1973
|
choices: organizations.map((org) => ({
|
|
1503
|
-
name: `${org.name} ${org.tier === "pro" ?
|
|
1974
|
+
name: `${org.name} ${org.tier === "pro" ? chalk4.green("(Pro)") : chalk4.dim("(Free)")}`,
|
|
1504
1975
|
value: org._id
|
|
1505
1976
|
}))
|
|
1506
1977
|
}
|
|
@@ -1530,7 +2001,7 @@ async function selectOrgProjectEnv(options) {
|
|
|
1530
2001
|
selectedProject = project;
|
|
1531
2002
|
} else if (projects.length === 1) {
|
|
1532
2003
|
selectedProject = projects[0];
|
|
1533
|
-
info(`Using project: ${
|
|
2004
|
+
info(`Using project: ${chalk4.bold(selectedProject.name)}`);
|
|
1534
2005
|
} else {
|
|
1535
2006
|
const { projectId } = await inquirer.prompt([
|
|
1536
2007
|
{
|
|
@@ -1581,48 +2052,43 @@ function warnTrackedFiles() {
|
|
|
1581
2052
|
console.log();
|
|
1582
2053
|
warning("Security risk: .env files are tracked by git!");
|
|
1583
2054
|
for (const file of trackedFiles) {
|
|
1584
|
-
console.log(
|
|
2055
|
+
console.log(chalk4.red(` tracked: ${file}`));
|
|
1585
2056
|
}
|
|
1586
2057
|
console.log();
|
|
1587
2058
|
console.log(
|
|
1588
|
-
|
|
2059
|
+
chalk4.yellow(
|
|
1589
2060
|
" Run the following to untrack them (without deleting the files):"
|
|
1590
2061
|
)
|
|
1591
2062
|
);
|
|
1592
2063
|
for (const file of trackedFiles) {
|
|
1593
|
-
console.log(
|
|
2064
|
+
console.log(chalk4.cyan(` git rm --cached ${file}`));
|
|
1594
2065
|
}
|
|
1595
2066
|
}
|
|
1596
2067
|
}
|
|
1597
2068
|
function printPostInit(selectedOrg, selectedProject) {
|
|
1598
2069
|
console.log();
|
|
1599
|
-
console.log(
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
if (
|
|
1605
|
-
console.log(
|
|
1606
|
-
chalk5.dim(
|
|
1607
|
-
` Project role: ${formatProjectRole(selectedProject.projectRole)}`
|
|
1608
|
-
)
|
|
1609
|
-
);
|
|
1610
|
-
projectRoleNotice(selectedProject.projectRole);
|
|
2070
|
+
console.log(chalk4.dim("Configuration saved to .envpilot"));
|
|
2071
|
+
console.log(
|
|
2072
|
+
chalk4.dim(` Org role: ${formatRoleLabel(orgUnifiedRole(selectedOrg))}`)
|
|
2073
|
+
);
|
|
2074
|
+
const scope = selectedProject.environmentScope;
|
|
2075
|
+
if (scope && scope.length > 0) {
|
|
2076
|
+
console.log(chalk4.dim(` Environment scope: ${scope.join(", ")}`));
|
|
1611
2077
|
}
|
|
1612
2078
|
console.log();
|
|
1613
2079
|
console.log("Next steps:");
|
|
1614
2080
|
console.log(
|
|
1615
|
-
` ${
|
|
2081
|
+
` ${chalk4.cyan("envpilot pull")} Download environment variables`
|
|
1616
2082
|
);
|
|
1617
2083
|
console.log(
|
|
1618
|
-
` ${
|
|
2084
|
+
` ${chalk4.cyan("envpilot push")} Upload local .env to cloud`
|
|
1619
2085
|
);
|
|
1620
2086
|
console.log();
|
|
1621
2087
|
}
|
|
1622
2088
|
|
|
1623
2089
|
// src/commands/pull.ts
|
|
1624
2090
|
import { Command as Command3 } from "commander";
|
|
1625
|
-
import
|
|
2091
|
+
import chalk5 from "chalk";
|
|
1626
2092
|
import inquirer2 from "inquirer";
|
|
1627
2093
|
|
|
1628
2094
|
// src/lib/format-converter.ts
|
|
@@ -1940,7 +2406,7 @@ function parseNetlify(content) {
|
|
|
1940
2406
|
for (const line2 of lines) {
|
|
1941
2407
|
const trimmed = line2.trim();
|
|
1942
2408
|
if (trimmed.startsWith("[")) {
|
|
1943
|
-
inBuildEnv = trimmed === "[build.environment]" ||
|
|
2409
|
+
inBuildEnv = trimmed === "[build.environment]" || /^\[context\.[^.\]]+\.environment\]$/.test(trimmed);
|
|
1944
2410
|
continue;
|
|
1945
2411
|
}
|
|
1946
2412
|
if (!inBuildEnv) continue;
|
|
@@ -1963,6 +2429,17 @@ function parseNetlify(content) {
|
|
|
1963
2429
|
}
|
|
1964
2430
|
|
|
1965
2431
|
// src/commands/pull.ts
|
|
2432
|
+
function accessFromMeta(meta, variables) {
|
|
2433
|
+
const role = normalizeOrgRole(meta?.unifiedRole ?? meta?.role);
|
|
2434
|
+
return {
|
|
2435
|
+
role,
|
|
2436
|
+
// Owners are implicitly assigned to every project. The legacy server sends
|
|
2437
|
+
// no `assigned`/`projectRole` for owners, so fall back to true for them.
|
|
2438
|
+
assigned: role === "owner" || (meta?.assigned ?? (meta?.projectRole !== null && meta?.projectRole !== void 0)),
|
|
2439
|
+
environmentScope: meta?.environmentScope ?? null,
|
|
2440
|
+
hasWriteAccess: meta?.hasWriteAccess ?? variables.some((v) => v.access === "write")
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
1966
2443
|
var pullCommand = new Command3("pull").description("Download environment variables to local .env file").option(
|
|
1967
2444
|
"-e, --env <environment>",
|
|
1968
2445
|
"Environment (development, staging, production)"
|
|
@@ -2005,6 +2482,8 @@ var pullCommand = new Command3("pull").description("Download environment variabl
|
|
|
2005
2482
|
const projectConfig = readProjectConfig();
|
|
2006
2483
|
if (!projectConfig) throw notInitialized();
|
|
2007
2484
|
checkTrackedFiles();
|
|
2485
|
+
const configV2ForActive = readProjectConfigV2();
|
|
2486
|
+
const activeEntry = configV2ForActive ? getActiveProject(configV2ForActive) : null;
|
|
2008
2487
|
const environment = options.env || projectConfig.environment || "development";
|
|
2009
2488
|
const fmt = options.format || "env";
|
|
2010
2489
|
if (!ALL_FORMATS.includes(fmt)) {
|
|
@@ -2016,7 +2495,8 @@ var pullCommand = new Command3("pull").description("Download environment variabl
|
|
|
2016
2495
|
{
|
|
2017
2496
|
projectId: projectConfig.projectId,
|
|
2018
2497
|
organizationId: projectConfig.organizationId,
|
|
2019
|
-
environment
|
|
2498
|
+
environment,
|
|
2499
|
+
fileProtection: activeEntry?.fileProtection
|
|
2020
2500
|
},
|
|
2021
2501
|
outputPath,
|
|
2022
2502
|
options
|
|
@@ -2036,7 +2516,7 @@ async function pullAllProjects(options) {
|
|
|
2036
2516
|
const displayName = project.projectName || project.projectId;
|
|
2037
2517
|
console.log();
|
|
2038
2518
|
console.log(
|
|
2039
|
-
|
|
2519
|
+
chalk5.bold(
|
|
2040
2520
|
`Pulling "${displayName}" (${project.environment}) \u2192 ${outputPath}`
|
|
2041
2521
|
)
|
|
2042
2522
|
);
|
|
@@ -2045,7 +2525,8 @@ async function pullAllProjects(options) {
|
|
|
2045
2525
|
{
|
|
2046
2526
|
projectId: project.projectId,
|
|
2047
2527
|
organizationId: project.organizationId,
|
|
2048
|
-
environment: project.environment
|
|
2528
|
+
environment: project.environment,
|
|
2529
|
+
fileProtection: project.fileProtection
|
|
2049
2530
|
},
|
|
2050
2531
|
outputPath,
|
|
2051
2532
|
options
|
|
@@ -2076,7 +2557,8 @@ async function pullSingleProject(project, options) {
|
|
|
2076
2557
|
{
|
|
2077
2558
|
projectId: project.projectId,
|
|
2078
2559
|
organizationId: project.organizationId,
|
|
2079
|
-
environment
|
|
2560
|
+
environment,
|
|
2561
|
+
fileProtection: project.fileProtection
|
|
2080
2562
|
},
|
|
2081
2563
|
outputPath,
|
|
2082
2564
|
options
|
|
@@ -2084,9 +2566,9 @@ async function pullSingleProject(project, options) {
|
|
|
2084
2566
|
}
|
|
2085
2567
|
async function pullProject(project, outputPath, options) {
|
|
2086
2568
|
const api = createAPIClient();
|
|
2087
|
-
let
|
|
2569
|
+
let meta;
|
|
2088
2570
|
const variables = await withSpinner(
|
|
2089
|
-
`Fetching ${
|
|
2571
|
+
`Fetching ${chalk5.bold(project.environment)} variables...`,
|
|
2090
2572
|
async () => {
|
|
2091
2573
|
const response = await api.get("/api/cli/variables", {
|
|
2092
2574
|
projectId: project.projectId,
|
|
@@ -2095,10 +2577,15 @@ async function pullProject(project, outputPath, options) {
|
|
|
2095
2577
|
organizationId: project.organizationId
|
|
2096
2578
|
}
|
|
2097
2579
|
});
|
|
2098
|
-
|
|
2580
|
+
meta = response.meta;
|
|
2099
2581
|
return response.data || [];
|
|
2100
2582
|
}
|
|
2101
2583
|
);
|
|
2584
|
+
if (meta?.scopeRestricted && meta.environmentScope?.length) {
|
|
2585
|
+
info(
|
|
2586
|
+
`Your access is scoped to ${meta.environmentScope.join(", ")}; variables in other environments are withheld.`
|
|
2587
|
+
);
|
|
2588
|
+
}
|
|
2102
2589
|
if (variables.length === 0) {
|
|
2103
2590
|
warning(`No variables found for ${project.environment} environment.`);
|
|
2104
2591
|
return;
|
|
@@ -2115,7 +2602,7 @@ async function pullProject(project, outputPath, options) {
|
|
|
2115
2602
|
return;
|
|
2116
2603
|
}
|
|
2117
2604
|
console.log();
|
|
2118
|
-
console.log(
|
|
2605
|
+
console.log(chalk5.bold("Changes:"));
|
|
2119
2606
|
console.log();
|
|
2120
2607
|
diff(diffResult.added, diffResult.removed, diffResult.changed);
|
|
2121
2608
|
console.log();
|
|
@@ -2162,29 +2649,30 @@ async function pullProject(project, outputPath, options) {
|
|
|
2162
2649
|
});
|
|
2163
2650
|
fs.writeFileSync(outputPath, output, "utf-8");
|
|
2164
2651
|
}
|
|
2165
|
-
const
|
|
2166
|
-
|
|
2652
|
+
const access = accessFromMeta(meta, variables);
|
|
2653
|
+
const protection = project.fileProtection ?? "auto";
|
|
2654
|
+
applyFileProtection(outputPath, access, protection);
|
|
2167
2655
|
success(
|
|
2168
|
-
`Downloaded ${variables.length} variables to ${
|
|
2656
|
+
`Downloaded ${variables.length} variables to ${chalk5.bold(outputPath)}`
|
|
2169
2657
|
);
|
|
2170
|
-
if (
|
|
2658
|
+
if (meta?.grantOnly) {
|
|
2171
2659
|
info(
|
|
2172
|
-
"You have
|
|
2660
|
+
"You have grant-only access to this project. You only see variables explicitly shared with you."
|
|
2173
2661
|
);
|
|
2174
2662
|
}
|
|
2175
|
-
const
|
|
2176
|
-
if (
|
|
2663
|
+
const writable = protection === "never" || protection !== "always" && isFileWritable(access);
|
|
2664
|
+
if (!writable) {
|
|
2177
2665
|
info(
|
|
2178
|
-
`File is read-only (your role: ${role
|
|
2666
|
+
`File is read-only (your role: ${formatRoleLabel(access.role)}${access.assigned ? "" : ", not assigned to this project"}).`
|
|
2179
2667
|
);
|
|
2180
2668
|
}
|
|
2181
2669
|
console.log();
|
|
2182
|
-
console.log(
|
|
2670
|
+
console.log(chalk5.dim(` Added: ${Object.keys(diffResult.added).length}`));
|
|
2183
2671
|
console.log(
|
|
2184
|
-
|
|
2672
|
+
chalk5.dim(` Changed: ${Object.keys(diffResult.changed).length}`)
|
|
2185
2673
|
);
|
|
2186
2674
|
console.log(
|
|
2187
|
-
|
|
2675
|
+
chalk5.dim(` Removed: ${Object.keys(diffResult.removed).length}`)
|
|
2188
2676
|
);
|
|
2189
2677
|
}
|
|
2190
2678
|
function checkTrackedFiles() {
|
|
@@ -2193,16 +2681,16 @@ function checkTrackedFiles() {
|
|
|
2193
2681
|
error("Security risk: .env files are tracked by git!");
|
|
2194
2682
|
console.log();
|
|
2195
2683
|
for (const file of trackedFiles) {
|
|
2196
|
-
console.log(
|
|
2684
|
+
console.log(chalk5.red(` tracked: ${file}`));
|
|
2197
2685
|
}
|
|
2198
2686
|
console.log();
|
|
2199
2687
|
console.log(
|
|
2200
|
-
|
|
2688
|
+
chalk5.yellow(
|
|
2201
2689
|
" Run the following to untrack them (without deleting the files):"
|
|
2202
2690
|
)
|
|
2203
2691
|
);
|
|
2204
2692
|
for (const file of trackedFiles) {
|
|
2205
|
-
console.log(
|
|
2693
|
+
console.log(chalk5.cyan(` git rm --cached ${file}`));
|
|
2206
2694
|
}
|
|
2207
2695
|
console.log();
|
|
2208
2696
|
process.exit(1);
|
|
@@ -2211,7 +2699,7 @@ function checkTrackedFiles() {
|
|
|
2211
2699
|
|
|
2212
2700
|
// src/commands/push.ts
|
|
2213
2701
|
import { Command as Command4 } from "commander";
|
|
2214
|
-
import
|
|
2702
|
+
import chalk6 from "chalk";
|
|
2215
2703
|
import inquirer3 from "inquirer";
|
|
2216
2704
|
|
|
2217
2705
|
// src/lib/validators.ts
|
|
@@ -2299,51 +2787,21 @@ var pushCommand = new Command4("push").description("Upload local .env file to cl
|
|
|
2299
2787
|
error("Security risk: .env files are tracked by git!");
|
|
2300
2788
|
console.log();
|
|
2301
2789
|
for (const file of trackedFiles) {
|
|
2302
|
-
console.log(
|
|
2790
|
+
console.log(chalk6.red(` tracked: ${file}`));
|
|
2303
2791
|
}
|
|
2304
2792
|
console.log();
|
|
2305
2793
|
console.log(
|
|
2306
|
-
|
|
2794
|
+
chalk6.yellow(
|
|
2307
2795
|
" Run the following to untrack them (without deleting the files):"
|
|
2308
2796
|
)
|
|
2309
2797
|
);
|
|
2310
2798
|
for (const file of trackedFiles) {
|
|
2311
|
-
console.log(
|
|
2799
|
+
console.log(chalk6.cyan(` git rm --cached ${file}`));
|
|
2312
2800
|
}
|
|
2313
2801
|
console.log();
|
|
2314
2802
|
process.exit(1);
|
|
2315
2803
|
}
|
|
2316
2804
|
const api = createAPIClient();
|
|
2317
|
-
const projects = await api.get("/api/cli/projects", { organizationId });
|
|
2318
|
-
const currentProject = projects.data?.find((p) => p._id === projectId);
|
|
2319
|
-
const projectRole = currentProject?.projectRole;
|
|
2320
|
-
if (projectRole === "viewer") {
|
|
2321
|
-
error(
|
|
2322
|
-
"Unfortunately, as a member, you cannot push in any environment. Please access or request access to your team lead."
|
|
2323
|
-
);
|
|
2324
|
-
process.exit(1);
|
|
2325
|
-
}
|
|
2326
|
-
const role = getRole();
|
|
2327
|
-
if (role === "member") {
|
|
2328
|
-
warning(
|
|
2329
|
-
"You have Member access. Push will create pending requests that require Admin or Team Lead approval."
|
|
2330
|
-
);
|
|
2331
|
-
console.log();
|
|
2332
|
-
if (!options.force) {
|
|
2333
|
-
const { proceed } = await inquirer3.prompt([
|
|
2334
|
-
{
|
|
2335
|
-
type: "confirm",
|
|
2336
|
-
name: "proceed",
|
|
2337
|
-
message: "Continue with creating approval requests?",
|
|
2338
|
-
default: true
|
|
2339
|
-
}
|
|
2340
|
-
]);
|
|
2341
|
-
if (!proceed) {
|
|
2342
|
-
info("Push cancelled.");
|
|
2343
|
-
return;
|
|
2344
|
-
}
|
|
2345
|
-
}
|
|
2346
|
-
}
|
|
2347
2805
|
const environment = options.env || defaultEnvironment || "development";
|
|
2348
2806
|
const fmt = options.format || "env";
|
|
2349
2807
|
if (!ALL_FORMATS.includes(fmt)) {
|
|
@@ -2375,7 +2833,7 @@ var pushCommand = new Command4("push").description("Upload local .env file to cl
|
|
|
2375
2833
|
if (invalid.length > 0) {
|
|
2376
2834
|
warning("Some variables have invalid keys and will be skipped:");
|
|
2377
2835
|
for (const { key, error: err } of invalid) {
|
|
2378
|
-
console.log(
|
|
2836
|
+
console.log(chalk6.red(` ${key}: ${err}`));
|
|
2379
2837
|
}
|
|
2380
2838
|
console.log();
|
|
2381
2839
|
}
|
|
@@ -2383,6 +2841,7 @@ var pushCommand = new Command4("push").description("Upload local .env file to cl
|
|
|
2383
2841
|
error("No valid variables to push.");
|
|
2384
2842
|
return;
|
|
2385
2843
|
}
|
|
2844
|
+
let meta;
|
|
2386
2845
|
const remoteVariables = await withSpinner(
|
|
2387
2846
|
"Fetching current variables...",
|
|
2388
2847
|
async () => {
|
|
@@ -2394,9 +2853,21 @@ var pushCommand = new Command4("push").description("Upload local .env file to cl
|
|
|
2394
2853
|
params.organizationId = organizationId;
|
|
2395
2854
|
}
|
|
2396
2855
|
const response = await api.get("/api/cli/variables", params);
|
|
2856
|
+
meta = response.meta;
|
|
2397
2857
|
return response.data || [];
|
|
2398
2858
|
}
|
|
2399
2859
|
);
|
|
2860
|
+
const scope = meta?.environmentScope;
|
|
2861
|
+
if (Array.isArray(scope) && !scope.includes(environment)) {
|
|
2862
|
+
error(
|
|
2863
|
+
`Your developer access is scoped to ${scope.join(", ")}. ${environment} variables are withheld.`
|
|
2864
|
+
);
|
|
2865
|
+
process.exit(1);
|
|
2866
|
+
}
|
|
2867
|
+
if (meta?.hasWriteAccess === false) {
|
|
2868
|
+
error(`You have read-only access to ${environment} in this project.`);
|
|
2869
|
+
process.exit(1);
|
|
2870
|
+
}
|
|
2400
2871
|
const remoteVars = {};
|
|
2401
2872
|
for (const variable of remoteVariables) {
|
|
2402
2873
|
remoteVars[variable.key] = variable.value;
|
|
@@ -2408,14 +2879,14 @@ var pushCommand = new Command4("push").description("Upload local .env file to cl
|
|
|
2408
2879
|
return;
|
|
2409
2880
|
}
|
|
2410
2881
|
console.log();
|
|
2411
|
-
console.log(
|
|
2882
|
+
console.log(chalk6.bold("Changes to push:"));
|
|
2412
2883
|
console.log();
|
|
2413
2884
|
const removedToShow = mode === "replace" ? diffResult.removed : {};
|
|
2414
2885
|
diff(diffResult.added, removedToShow, diffResult.changed);
|
|
2415
2886
|
if (mode === "merge" && Object.keys(diffResult.removed).length > 0) {
|
|
2416
2887
|
console.log();
|
|
2417
2888
|
console.log(
|
|
2418
|
-
|
|
2889
|
+
chalk6.dim(
|
|
2419
2890
|
`Note: ${Object.keys(diffResult.removed).length} remote variables not in local file will be preserved (use --replace to remove them)`
|
|
2420
2891
|
)
|
|
2421
2892
|
);
|
|
@@ -2452,7 +2923,7 @@ var pushCommand = new Command4("push").description("Upload local .env file to cl
|
|
|
2452
2923
|
}
|
|
2453
2924
|
}
|
|
2454
2925
|
const result = await withSpinner(
|
|
2455
|
-
`Pushing variables to ${
|
|
2926
|
+
`Pushing variables to ${chalk6.bold(environment)}...`,
|
|
2456
2927
|
async () => {
|
|
2457
2928
|
const response = await api.post("/api/cli/variables/bulk", {
|
|
2458
2929
|
projectId,
|
|
@@ -2467,32 +2938,28 @@ var pushCommand = new Command4("push").description("Upload local .env file to cl
|
|
|
2467
2938
|
return response.data;
|
|
2468
2939
|
}
|
|
2469
2940
|
);
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2941
|
+
success(
|
|
2942
|
+
`Pushed ${result?.total || Object.keys(valid).length} variables to ${chalk6.bold(environment)}`
|
|
2943
|
+
);
|
|
2944
|
+
console.log();
|
|
2945
|
+
console.log(chalk6.dim(` Created: ${result?.created || 0}`));
|
|
2946
|
+
console.log(chalk6.dim(` Updated: ${result?.updated || 0}`));
|
|
2947
|
+
if (mode === "replace") {
|
|
2948
|
+
console.log(chalk6.dim(` Deleted: ${result?.deleted || 0}`));
|
|
2949
|
+
}
|
|
2950
|
+
const deniedKeys = result?.deniedKeys ?? [];
|
|
2951
|
+
if (deniedKeys.length > 0) {
|
|
2480
2952
|
console.log();
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
console.log(chalk7.dim(` Skipped: ${result.skipped}`));
|
|
2484
|
-
}
|
|
2485
|
-
} else {
|
|
2486
|
-
success(
|
|
2487
|
-
`Pushed ${result?.total || Object.keys(valid).length} variables to ${chalk7.bold(environment)}`
|
|
2953
|
+
warning(
|
|
2954
|
+
`${deniedKeys.length} variable(s) were NOT written (access denied):`
|
|
2488
2955
|
);
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
console.log(chalk7.dim(` Updated: ${result?.updated || 0}`));
|
|
2492
|
-
if (mode === "replace") {
|
|
2493
|
-
console.log(chalk7.dim(` Deleted: ${result?.deleted || 0}`));
|
|
2956
|
+
for (const key of deniedKeys) {
|
|
2957
|
+
console.log(chalk6.red(` \u2717 ${key}`));
|
|
2494
2958
|
}
|
|
2495
2959
|
}
|
|
2960
|
+
if (result?.skipped && result.skipped > 0) {
|
|
2961
|
+
console.log(chalk6.dim(` Skipped: ${result.skipped}`));
|
|
2962
|
+
}
|
|
2496
2963
|
} catch (err) {
|
|
2497
2964
|
await handleError(err);
|
|
2498
2965
|
}
|
|
@@ -2500,8 +2967,11 @@ var pushCommand = new Command4("push").description("Upload local .env file to cl
|
|
|
2500
2967
|
|
|
2501
2968
|
// src/commands/switch.ts
|
|
2502
2969
|
import { Command as Command5 } from "commander";
|
|
2503
|
-
import
|
|
2970
|
+
import chalk7 from "chalk";
|
|
2504
2971
|
import inquirer4 from "inquirer";
|
|
2972
|
+
function orgUnifiedRole2(org) {
|
|
2973
|
+
return normalizeOrgRole(org.unifiedRole ?? org.role);
|
|
2974
|
+
}
|
|
2505
2975
|
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(
|
|
2506
2976
|
"-e, --env <environment>",
|
|
2507
2977
|
"Switch environment (development, staging, production)"
|
|
@@ -2528,7 +2998,7 @@ var switchCommand = new Command5("switch").description("Switch project, environm
|
|
|
2528
2998
|
console.log();
|
|
2529
2999
|
console.log("Linked projects:");
|
|
2530
3000
|
for (const p of configV2.projects) {
|
|
2531
|
-
const mark = p.projectId === configV2.activeProjectId ?
|
|
3001
|
+
const mark = p.projectId === configV2.activeProjectId ? chalk7.green(" *") : "";
|
|
2532
3002
|
console.log(
|
|
2533
3003
|
` ${p.projectName || p.projectId} (${p.environment})${mark}`
|
|
2534
3004
|
);
|
|
@@ -2540,7 +3010,7 @@ var switchCommand = new Command5("switch").description("Switch project, environm
|
|
|
2540
3010
|
setActiveProjectId(target2.projectId);
|
|
2541
3011
|
setActiveOrganizationId(target2.organizationId);
|
|
2542
3012
|
success(
|
|
2543
|
-
`Active project: ${
|
|
3013
|
+
`Active project: ${chalk7.bold(target2.projectName || target2.projectId)}`
|
|
2544
3014
|
);
|
|
2545
3015
|
return;
|
|
2546
3016
|
}
|
|
@@ -2551,7 +3021,7 @@ var switchCommand = new Command5("switch").description("Switch project, environm
|
|
|
2551
3021
|
process.exit(1);
|
|
2552
3022
|
}
|
|
2553
3023
|
updateProjectConfig({ environment });
|
|
2554
|
-
success(`Switched to ${
|
|
3024
|
+
success(`Switched to ${chalk7.bold(environment)} environment`);
|
|
2555
3025
|
return;
|
|
2556
3026
|
}
|
|
2557
3027
|
if (options.organization) {
|
|
@@ -2570,17 +3040,15 @@ var switchCommand = new Command5("switch").description("Switch project, environm
|
|
|
2570
3040
|
process.exit(1);
|
|
2571
3041
|
}
|
|
2572
3042
|
setActiveOrganizationId(org._id);
|
|
2573
|
-
|
|
2574
|
-
setRole(org.role);
|
|
2575
|
-
}
|
|
3043
|
+
setUnifiedRole(orgUnifiedRole2(org));
|
|
2576
3044
|
if (projectConfig) {
|
|
2577
3045
|
updateProjectConfig({ organizationId: org._id });
|
|
2578
3046
|
}
|
|
2579
|
-
success(`Switched to organization: ${
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
3047
|
+
success(`Switched to organization: ${chalk7.bold(org.name)}`);
|
|
3048
|
+
console.log(
|
|
3049
|
+
chalk7.dim(` Role: ${formatRoleLabel(orgUnifiedRole2(org))}`)
|
|
3050
|
+
);
|
|
3051
|
+
roleNotice(orgUnifiedRole2(org));
|
|
2584
3052
|
return;
|
|
2585
3053
|
}
|
|
2586
3054
|
if (options.project || target) {
|
|
@@ -2599,7 +3067,7 @@ var switchCommand = new Command5("switch").description("Switch project, environm
|
|
|
2599
3067
|
setActiveProjectId(linked.projectId);
|
|
2600
3068
|
setActiveOrganizationId(linked.organizationId);
|
|
2601
3069
|
success(
|
|
2602
|
-
`Switched to project: ${
|
|
3070
|
+
`Switched to project: ${chalk7.bold(linked.projectName || linked.projectId)}`
|
|
2603
3071
|
);
|
|
2604
3072
|
return;
|
|
2605
3073
|
}
|
|
@@ -2619,9 +3087,7 @@ var switchCommand = new Command5("switch").description("Switch project, environm
|
|
|
2619
3087
|
}
|
|
2620
3088
|
if (organizations.length === 1) {
|
|
2621
3089
|
organizationId = organizations[0]._id;
|
|
2622
|
-
|
|
2623
|
-
setRole(organizations[0].role);
|
|
2624
|
-
}
|
|
3090
|
+
setUnifiedRole(orgUnifiedRole2(organizations[0]));
|
|
2625
3091
|
} else {
|
|
2626
3092
|
const { orgId } = await inquirer4.prompt([
|
|
2627
3093
|
{
|
|
@@ -2629,15 +3095,15 @@ var switchCommand = new Command5("switch").description("Switch project, environm
|
|
|
2629
3095
|
name: "orgId",
|
|
2630
3096
|
message: "Select an organization:",
|
|
2631
3097
|
choices: organizations.map((org) => ({
|
|
2632
|
-
name: `${org.name} ${org.tier === "pro" ?
|
|
3098
|
+
name: `${org.name} ${org.tier === "pro" ? chalk7.green("(Pro)") : chalk7.dim("(Free)")}`,
|
|
2633
3099
|
value: org._id
|
|
2634
3100
|
}))
|
|
2635
3101
|
}
|
|
2636
3102
|
]);
|
|
2637
3103
|
organizationId = orgId;
|
|
2638
3104
|
const selectedOrg = organizations.find((o) => o._id === orgId);
|
|
2639
|
-
if (selectedOrg
|
|
2640
|
-
|
|
3105
|
+
if (selectedOrg) {
|
|
3106
|
+
setUnifiedRole(orgUnifiedRole2(selectedOrg));
|
|
2641
3107
|
}
|
|
2642
3108
|
}
|
|
2643
3109
|
}
|
|
@@ -2665,10 +3131,10 @@ var switchCommand = new Command5("switch").description("Switch project, environm
|
|
|
2665
3131
|
organizationId,
|
|
2666
3132
|
environment
|
|
2667
3133
|
});
|
|
2668
|
-
success(`Switched to project: ${
|
|
3134
|
+
success(`Switched to project: ${chalk7.bold(project.name)}`);
|
|
2669
3135
|
if (project.projectRole) {
|
|
2670
3136
|
console.log(
|
|
2671
|
-
|
|
3137
|
+
chalk7.dim(
|
|
2672
3138
|
` Project role: ${formatProjectRole(project.projectRole)}`
|
|
2673
3139
|
)
|
|
2674
3140
|
);
|
|
@@ -2708,7 +3174,7 @@ var switchCommand = new Command5("switch").description("Switch project, environm
|
|
|
2708
3174
|
choices: configV2.projects.map((p) => {
|
|
2709
3175
|
const isActive = p.projectId === configV2.activeProjectId;
|
|
2710
3176
|
return {
|
|
2711
|
-
name: `${p.projectName || p.projectId} (${p.environment})${isActive ?
|
|
3177
|
+
name: `${p.projectName || p.projectId} (${p.environment})${isActive ? chalk7.green(" *current") : ""}`,
|
|
2712
3178
|
value: p.projectId
|
|
2713
3179
|
};
|
|
2714
3180
|
}),
|
|
@@ -2723,7 +3189,7 @@ var switchCommand = new Command5("switch").description("Switch project, environm
|
|
|
2723
3189
|
setActiveProjectId(projectId);
|
|
2724
3190
|
setActiveOrganizationId(selected.organizationId);
|
|
2725
3191
|
success(
|
|
2726
|
-
`Active project: ${
|
|
3192
|
+
`Active project: ${chalk7.bold(selected.projectName || selected.projectId)}`
|
|
2727
3193
|
);
|
|
2728
3194
|
return;
|
|
2729
3195
|
}
|
|
@@ -2746,7 +3212,7 @@ var switchCommand = new Command5("switch").description("Switch project, environm
|
|
|
2746
3212
|
}
|
|
2747
3213
|
]);
|
|
2748
3214
|
updateProjectConfig({ environment });
|
|
2749
|
-
success(`Switched to ${
|
|
3215
|
+
success(`Switched to ${chalk7.bold(environment)} environment`);
|
|
2750
3216
|
return;
|
|
2751
3217
|
}
|
|
2752
3218
|
if (switchType === "organization" || switchType === "project") {
|
|
@@ -2767,7 +3233,7 @@ var switchCommand = new Command5("switch").description("Switch project, environm
|
|
|
2767
3233
|
name: "orgId",
|
|
2768
3234
|
message: "Select an organization:",
|
|
2769
3235
|
choices: organizations.map((org) => ({
|
|
2770
|
-
name: `${org.name} ${org.tier === "pro" ?
|
|
3236
|
+
name: `${org.name} ${org.tier === "pro" ? chalk7.green("(Pro)") : chalk7.dim("(Free)")}`,
|
|
2771
3237
|
value: org._id
|
|
2772
3238
|
})),
|
|
2773
3239
|
default: projectConfig?.organizationId
|
|
@@ -2776,19 +3242,17 @@ var switchCommand = new Command5("switch").description("Switch project, environm
|
|
|
2776
3242
|
if (switchType === "organization") {
|
|
2777
3243
|
setActiveOrganizationId(orgId);
|
|
2778
3244
|
const org = organizations.find((o) => o._id === orgId);
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
roleNotice(org.role);
|
|
2786
|
-
}
|
|
3245
|
+
setUnifiedRole(orgUnifiedRole2(org));
|
|
3246
|
+
success(`Switched to organization: ${chalk7.bold(org.name)}`);
|
|
3247
|
+
console.log(
|
|
3248
|
+
chalk7.dim(` Role: ${formatRoleLabel(orgUnifiedRole2(org))}`)
|
|
3249
|
+
);
|
|
3250
|
+
roleNotice(orgUnifiedRole2(org));
|
|
2787
3251
|
return;
|
|
2788
3252
|
}
|
|
2789
3253
|
const selectedOrg = organizations.find((o) => o._id === orgId);
|
|
2790
|
-
if (selectedOrg
|
|
2791
|
-
|
|
3254
|
+
if (selectedOrg) {
|
|
3255
|
+
setUnifiedRole(orgUnifiedRole2(selectedOrg));
|
|
2792
3256
|
}
|
|
2793
3257
|
const projects = await withSpinner(
|
|
2794
3258
|
"Fetching projects...",
|
|
@@ -2822,10 +3286,10 @@ var switchCommand = new Command5("switch").description("Switch project, environm
|
|
|
2822
3286
|
organizationId: orgId,
|
|
2823
3287
|
environment
|
|
2824
3288
|
});
|
|
2825
|
-
success(`Switched to project: ${
|
|
3289
|
+
success(`Switched to project: ${chalk7.bold(project.name)}`);
|
|
2826
3290
|
if (project.projectRole) {
|
|
2827
3291
|
console.log(
|
|
2828
|
-
|
|
3292
|
+
chalk7.dim(
|
|
2829
3293
|
` Project role: ${formatProjectRole(project.projectRole)}`
|
|
2830
3294
|
)
|
|
2831
3295
|
);
|
|
@@ -2840,7 +3304,7 @@ var switchCommand = new Command5("switch").description("Switch project, environm
|
|
|
2840
3304
|
|
|
2841
3305
|
// src/commands/list.ts
|
|
2842
3306
|
import { Command as Command6 } from "commander";
|
|
2843
|
-
import
|
|
3307
|
+
import chalk8 from "chalk";
|
|
2844
3308
|
var listCommand = new Command6("list").description("List resources").argument(
|
|
2845
3309
|
"[resource]",
|
|
2846
3310
|
"Resource type: projects, organizations, variables, linked",
|
|
@@ -2895,19 +3359,19 @@ function listLinked() {
|
|
|
2895
3359
|
console.log();
|
|
2896
3360
|
for (const project of configV2.projects) {
|
|
2897
3361
|
const isActive = project.projectId === configV2.activeProjectId;
|
|
2898
|
-
const marker = isActive ?
|
|
3362
|
+
const marker = isActive ? chalk8.green("*") : " ";
|
|
2899
3363
|
const envFile = getEnvPathForEnvironment(project.environment);
|
|
2900
3364
|
console.log(
|
|
2901
|
-
` ${marker} ${
|
|
3365
|
+
` ${marker} ${chalk8.bold(project.projectName || project.projectId)} ${chalk8.dim(`(${project.organizationName || project.organizationId})`)}`
|
|
2902
3366
|
);
|
|
2903
|
-
console.log(` ${project.environment} ${
|
|
3367
|
+
console.log(` ${project.environment} ${chalk8.dim("\u2192")} ${envFile}`);
|
|
2904
3368
|
console.log();
|
|
2905
3369
|
}
|
|
2906
3370
|
if (configV2.projects.length > 1) {
|
|
2907
|
-
console.log(
|
|
3371
|
+
console.log(chalk8.dim(" (* = active project)"));
|
|
2908
3372
|
console.log();
|
|
2909
3373
|
console.log(
|
|
2910
|
-
|
|
3374
|
+
chalk8.dim(
|
|
2911
3375
|
' Use `envpilot switch --active "<name>"` to change the active project'
|
|
2912
3376
|
)
|
|
2913
3377
|
);
|
|
@@ -2935,8 +3399,8 @@ async function listOrganizations(api, options) {
|
|
|
2935
3399
|
organizations.map((org) => ({
|
|
2936
3400
|
name: org.name,
|
|
2937
3401
|
slug: org.slug,
|
|
2938
|
-
tier: org.tier === "pro" ?
|
|
2939
|
-
role: org.role
|
|
3402
|
+
tier: org.tier === "pro" ? chalk8.green("Pro") : chalk8.dim("Free"),
|
|
3403
|
+
role: formatRoleLabel(org.unifiedRole ?? org.role)
|
|
2940
3404
|
})),
|
|
2941
3405
|
[
|
|
2942
3406
|
{ key: "name", header: "Name" },
|
|
@@ -2993,9 +3457,11 @@ async function listProjects(api, projectConfig, options) {
|
|
|
2993
3457
|
icon: project.icon || "\u{1F4E6}",
|
|
2994
3458
|
name: project.name,
|
|
2995
3459
|
slug: project.slug,
|
|
2996
|
-
description: project.description ||
|
|
2997
|
-
role:
|
|
2998
|
-
|
|
3460
|
+
description: project.description || chalk8.dim("-"),
|
|
3461
|
+
role: formatRoleLabel(
|
|
3462
|
+
project.unifiedRole ?? project.userRole ?? project.projectRole
|
|
3463
|
+
),
|
|
3464
|
+
active: projectConfig?.projectId === project._id ? chalk8.green("\u2713") : ""
|
|
2999
3465
|
})),
|
|
3000
3466
|
[
|
|
3001
3467
|
{ key: "icon", header: "" },
|
|
@@ -3006,11 +3472,8 @@ async function listProjects(api, projectConfig, options) {
|
|
|
3006
3472
|
{ key: "active", header: "" }
|
|
3007
3473
|
]
|
|
3008
3474
|
);
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
console.log();
|
|
3012
|
-
console.log(chalk9.dim(`Your org role: ${formatRole(role)}`));
|
|
3013
|
-
}
|
|
3475
|
+
console.log();
|
|
3476
|
+
console.log(chalk8.dim(`Your org role: ${formatRoleLabel(getUnifiedRole())}`));
|
|
3014
3477
|
}
|
|
3015
3478
|
async function listVariables(api, projectConfig, options) {
|
|
3016
3479
|
const projectId = options.project || projectConfig?.projectId;
|
|
@@ -3019,14 +3482,14 @@ async function listVariables(api, projectConfig, options) {
|
|
|
3019
3482
|
error("No project specified. Use --project or run `envpilot init` first.");
|
|
3020
3483
|
process.exit(1);
|
|
3021
3484
|
}
|
|
3022
|
-
let
|
|
3485
|
+
let meta;
|
|
3023
3486
|
const variables = await withSpinner("Fetching variables...", async () => {
|
|
3024
3487
|
const params = { projectId };
|
|
3025
3488
|
if (environment) {
|
|
3026
3489
|
params.environment = environment;
|
|
3027
3490
|
}
|
|
3028
3491
|
const response = await api.get("/api/cli/variables", params);
|
|
3029
|
-
|
|
3492
|
+
meta = response.meta;
|
|
3030
3493
|
return response.data || [];
|
|
3031
3494
|
});
|
|
3032
3495
|
const tagFilter = options.tag?.toLowerCase();
|
|
@@ -3056,9 +3519,9 @@ async function listVariables(api, projectConfig, options) {
|
|
|
3056
3519
|
filtered.map((variable) => ({
|
|
3057
3520
|
key: variable.key,
|
|
3058
3521
|
value: options.showValues ? variable.value : maskValue(variable.value),
|
|
3059
|
-
sensitive: variable.isSensitive ?
|
|
3060
|
-
tags: hasTags ? variable.tags?.map((t) => t.name).join(", ") ||
|
|
3061
|
-
version: `v${variable.version}`
|
|
3522
|
+
sensitive: variable.isSensitive ? chalk8.yellow("\u25CF") : "",
|
|
3523
|
+
tags: hasTags ? variable.tags?.map((t) => t.name).join(", ") || chalk8.dim("-") : "",
|
|
3524
|
+
version: typeof variable.version === "number" ? `v${variable.version}` : ""
|
|
3062
3525
|
})),
|
|
3063
3526
|
[
|
|
3064
3527
|
{ key: "key", header: "Key" },
|
|
@@ -3069,29 +3532,44 @@ async function listVariables(api, projectConfig, options) {
|
|
|
3069
3532
|
]
|
|
3070
3533
|
);
|
|
3071
3534
|
console.log();
|
|
3072
|
-
console.log(
|
|
3073
|
-
|
|
3074
|
-
if (
|
|
3075
|
-
console.log(chalk9.dim(`Your org role: ${formatRole(role)}`));
|
|
3076
|
-
}
|
|
3077
|
-
if (metaProjectRole) {
|
|
3535
|
+
console.log(chalk8.dim(`Total: ${filtered.length} variables`));
|
|
3536
|
+
console.log(chalk8.dim(`Your org role: ${formatRoleLabel(getUnifiedRole())}`));
|
|
3537
|
+
if (meta?.scopeRestricted && meta.environmentScope?.length) {
|
|
3078
3538
|
console.log(
|
|
3079
|
-
|
|
3539
|
+
chalk8.dim(
|
|
3540
|
+
`Your access is scoped to ${meta.environmentScope.join(", ")}; variables in other environments are withheld.`
|
|
3541
|
+
)
|
|
3080
3542
|
);
|
|
3081
|
-
}
|
|
3082
|
-
if (role === "member" || metaProjectRole === "viewer") {
|
|
3543
|
+
} else if (meta?.grantOnly) {
|
|
3083
3544
|
console.log(
|
|
3084
|
-
|
|
3545
|
+
chalk8.dim("You may only see variables you have been granted access to.")
|
|
3085
3546
|
);
|
|
3086
3547
|
}
|
|
3087
3548
|
if (!options.showValues) {
|
|
3088
|
-
console.log(
|
|
3549
|
+
console.log(chalk8.dim("Use --show-values to see actual values"));
|
|
3089
3550
|
}
|
|
3090
3551
|
}
|
|
3091
3552
|
|
|
3092
3553
|
// src/commands/config.ts
|
|
3093
3554
|
import { Command as Command7 } from "commander";
|
|
3094
|
-
import
|
|
3555
|
+
import chalk9 from "chalk";
|
|
3556
|
+
var FILE_PROTECTION_VALUES = ["auto", "always", "never"];
|
|
3557
|
+
var ENVIRONMENT_VALUES = ["development", "staging", "production"];
|
|
3558
|
+
function updateActiveProjectEntry(updates) {
|
|
3559
|
+
const v2 = readProjectConfigV2();
|
|
3560
|
+
if (!v2) {
|
|
3561
|
+
error(
|
|
3562
|
+
"No project initialized in this directory. Run `envpilot init` first."
|
|
3563
|
+
);
|
|
3564
|
+
process.exit(1);
|
|
3565
|
+
}
|
|
3566
|
+
const active = getActiveProject(v2);
|
|
3567
|
+
if (!active) {
|
|
3568
|
+
error("No active project found in this directory.");
|
|
3569
|
+
process.exit(1);
|
|
3570
|
+
}
|
|
3571
|
+
writeProjectConfigV2(updateProjectInConfig(v2, active.projectId, updates));
|
|
3572
|
+
}
|
|
3095
3573
|
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) => {
|
|
3096
3574
|
try {
|
|
3097
3575
|
switch (action) {
|
|
@@ -3135,6 +3613,10 @@ async function handleGet(key) {
|
|
|
3135
3613
|
console.log(" user Current authenticated user");
|
|
3136
3614
|
console.log(" activeProjectId Currently active project");
|
|
3137
3615
|
console.log(" activeOrganizationId Currently active organization");
|
|
3616
|
+
console.log(
|
|
3617
|
+
" fileProtection Active project .env protection (auto|always|never)"
|
|
3618
|
+
);
|
|
3619
|
+
console.log(" defaultEnvironment Active project's default environment");
|
|
3138
3620
|
process.exit(1);
|
|
3139
3621
|
}
|
|
3140
3622
|
const config2 = getConfig();
|
|
@@ -3146,15 +3628,27 @@ async function handleGet(key) {
|
|
|
3146
3628
|
if (config2.user) {
|
|
3147
3629
|
console.log(JSON.stringify(config2.user, null, 2));
|
|
3148
3630
|
} else {
|
|
3149
|
-
console.log(
|
|
3631
|
+
console.log(chalk9.dim("(not set)"));
|
|
3150
3632
|
}
|
|
3151
3633
|
break;
|
|
3152
3634
|
case "activeProjectId":
|
|
3153
|
-
console.log(config2.activeProjectId ||
|
|
3635
|
+
console.log(config2.activeProjectId || chalk9.dim("(not set)"));
|
|
3154
3636
|
break;
|
|
3155
3637
|
case "activeOrganizationId":
|
|
3156
|
-
console.log(config2.activeOrganizationId ||
|
|
3638
|
+
console.log(config2.activeOrganizationId || chalk9.dim("(not set)"));
|
|
3639
|
+
break;
|
|
3640
|
+
case "fileProtection": {
|
|
3641
|
+
const v2 = readProjectConfigV2();
|
|
3642
|
+
const active = v2 ? getActiveProject(v2) : null;
|
|
3643
|
+
console.log(active?.fileProtection ?? "auto");
|
|
3644
|
+
break;
|
|
3645
|
+
}
|
|
3646
|
+
case "defaultEnvironment": {
|
|
3647
|
+
const v2 = readProjectConfigV2();
|
|
3648
|
+
const active = v2 ? getActiveProject(v2) : null;
|
|
3649
|
+
console.log(active?.environment || chalk9.dim("(not set)"));
|
|
3157
3650
|
break;
|
|
3651
|
+
}
|
|
3158
3652
|
default:
|
|
3159
3653
|
error(`Unknown key: ${key}`);
|
|
3160
3654
|
process.exit(1);
|
|
@@ -3164,8 +3658,7 @@ async function handleSet(key, value) {
|
|
|
3164
3658
|
if (!key || value === void 0) {
|
|
3165
3659
|
error("Missing key or value. Usage: envpilot config set <key> <value>");
|
|
3166
3660
|
console.log();
|
|
3167
|
-
|
|
3168
|
-
console.log(" apiUrl API endpoint URL");
|
|
3661
|
+
printSettableKeys();
|
|
3169
3662
|
process.exit(1);
|
|
3170
3663
|
}
|
|
3171
3664
|
switch (key) {
|
|
@@ -3181,14 +3674,53 @@ async function handleSet(key, value) {
|
|
|
3181
3674
|
success(`Set apiUrl to ${normalized}`);
|
|
3182
3675
|
break;
|
|
3183
3676
|
}
|
|
3677
|
+
case "fileProtection": {
|
|
3678
|
+
if (!FILE_PROTECTION_VALUES.includes(
|
|
3679
|
+
value
|
|
3680
|
+
)) {
|
|
3681
|
+
error(
|
|
3682
|
+
`Invalid fileProtection value: ${value}. Expected one of: ${FILE_PROTECTION_VALUES.join(", ")}`
|
|
3683
|
+
);
|
|
3684
|
+
process.exit(1);
|
|
3685
|
+
}
|
|
3686
|
+
updateActiveProjectEntry({
|
|
3687
|
+
fileProtection: value
|
|
3688
|
+
});
|
|
3689
|
+
success(`Set fileProtection to ${value} for the active project`);
|
|
3690
|
+
break;
|
|
3691
|
+
}
|
|
3692
|
+
case "defaultEnvironment": {
|
|
3693
|
+
if (!ENVIRONMENT_VALUES.includes(
|
|
3694
|
+
value
|
|
3695
|
+
)) {
|
|
3696
|
+
error(
|
|
3697
|
+
`Invalid defaultEnvironment value: ${value}. Expected one of: ${ENVIRONMENT_VALUES.join(", ")}`
|
|
3698
|
+
);
|
|
3699
|
+
process.exit(1);
|
|
3700
|
+
}
|
|
3701
|
+
updateActiveProjectEntry({
|
|
3702
|
+
environment: value
|
|
3703
|
+
});
|
|
3704
|
+
success(`Set defaultEnvironment to ${value} for the active project`);
|
|
3705
|
+
break;
|
|
3706
|
+
}
|
|
3184
3707
|
default:
|
|
3185
3708
|
error(`Cannot set key: ${key}`);
|
|
3186
3709
|
console.log();
|
|
3187
|
-
|
|
3188
|
-
console.log(" apiUrl API endpoint URL");
|
|
3710
|
+
printSettableKeys();
|
|
3189
3711
|
process.exit(1);
|
|
3190
3712
|
}
|
|
3191
3713
|
}
|
|
3714
|
+
function printSettableKeys() {
|
|
3715
|
+
console.log("Settable keys:");
|
|
3716
|
+
console.log(" apiUrl API endpoint URL");
|
|
3717
|
+
console.log(
|
|
3718
|
+
" fileProtection Active project .env protection (auto|always|never)"
|
|
3719
|
+
);
|
|
3720
|
+
console.log(
|
|
3721
|
+
" defaultEnvironment Active project default environment (development|staging|production)"
|
|
3722
|
+
);
|
|
3723
|
+
}
|
|
3192
3724
|
async function handleList() {
|
|
3193
3725
|
const config2 = getConfig();
|
|
3194
3726
|
const projectConfig = readProjectConfig();
|
|
@@ -3196,7 +3728,7 @@ async function handleList() {
|
|
|
3196
3728
|
console.log();
|
|
3197
3729
|
keyValue([
|
|
3198
3730
|
["API URL", config2.apiUrl],
|
|
3199
|
-
["Authenticated", isAuthenticated() ?
|
|
3731
|
+
["Authenticated", isAuthenticated() ? chalk9.green("Yes") : chalk9.red("No")],
|
|
3200
3732
|
["User", config2.user?.email],
|
|
3201
3733
|
["Active Organization", config2.activeOrganizationId],
|
|
3202
3734
|
["Active Project", config2.activeProjectId]
|
|
@@ -3244,20 +3776,43 @@ async function handleReset() {
|
|
|
3244
3776
|
|
|
3245
3777
|
// src/commands/logout.ts
|
|
3246
3778
|
import { Command as Command8 } from "commander";
|
|
3247
|
-
var logoutCommand = new Command8("logout").description("Log out from Envpilot").action(async () => {
|
|
3779
|
+
var logoutCommand = new Command8("logout").description("Log out from Envpilot").option("--all", "Log out of every authenticated account").action(async (options) => {
|
|
3248
3780
|
try {
|
|
3249
3781
|
if (!isAuthenticated()) {
|
|
3250
3782
|
info("You are not logged in.");
|
|
3251
3783
|
return;
|
|
3252
3784
|
}
|
|
3253
|
-
|
|
3785
|
+
if (options.all) {
|
|
3786
|
+
const accounts = listAccounts();
|
|
3787
|
+
for (const account of accounts) {
|
|
3788
|
+
try {
|
|
3789
|
+
const api2 = new APIClient({ accessToken: account.accessToken });
|
|
3790
|
+
await api2.post("/api/cli/auth?action=revoke", {});
|
|
3791
|
+
} catch {
|
|
3792
|
+
}
|
|
3793
|
+
removeAccount(account.id);
|
|
3794
|
+
}
|
|
3795
|
+
clearAuth();
|
|
3796
|
+
success(`Logged out of all ${accounts.length} accounts.`);
|
|
3797
|
+
return;
|
|
3798
|
+
}
|
|
3799
|
+
const activeAccount = getActiveAccount();
|
|
3254
3800
|
const api = createAPIClient();
|
|
3255
3801
|
try {
|
|
3256
3802
|
await api.post("/api/cli/auth?action=revoke", {});
|
|
3257
3803
|
} catch {
|
|
3258
3804
|
}
|
|
3259
|
-
clearAuth();
|
|
3260
|
-
success(
|
|
3805
|
+
const { newActiveId } = clearAuth();
|
|
3806
|
+
success(
|
|
3807
|
+
`Logged out${activeAccount?.user.email ? ` ${activeAccount.user.email}` : ""}.`
|
|
3808
|
+
);
|
|
3809
|
+
if (newActiveId) {
|
|
3810
|
+
const remaining = listAccounts();
|
|
3811
|
+
const newActive = remaining.find((a) => a.id === newActiveId);
|
|
3812
|
+
info(`Now active: ${newActive?.user.email ?? newActiveId}`);
|
|
3813
|
+
} else {
|
|
3814
|
+
info("No accounts remaining.");
|
|
3815
|
+
}
|
|
3261
3816
|
} catch (err) {
|
|
3262
3817
|
await handleError(err);
|
|
3263
3818
|
}
|
|
@@ -3265,7 +3820,7 @@ var logoutCommand = new Command8("logout").description("Log out from Envpilot").
|
|
|
3265
3820
|
|
|
3266
3821
|
// src/commands/unlink.ts
|
|
3267
3822
|
import { Command as Command9 } from "commander";
|
|
3268
|
-
import
|
|
3823
|
+
import chalk10 from "chalk";
|
|
3269
3824
|
import inquirer5 from "inquirer";
|
|
3270
3825
|
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) => {
|
|
3271
3826
|
try {
|
|
@@ -3300,7 +3855,7 @@ var unlinkCommand = new Command9("unlink").description("Remove a linked project
|
|
|
3300
3855
|
choices: config2.projects.map((p) => {
|
|
3301
3856
|
const isActive = p.projectId === config2.activeProjectId;
|
|
3302
3857
|
return {
|
|
3303
|
-
name: `${p.projectName || p.projectId} (${p.organizationName || p.organizationId})${isActive ?
|
|
3858
|
+
name: `${p.projectName || p.projectId} (${p.organizationName || p.organizationId})${isActive ? chalk10.green(" *active") : ""}`,
|
|
3304
3859
|
value: p.projectId
|
|
3305
3860
|
};
|
|
3306
3861
|
})
|
|
@@ -3343,7 +3898,7 @@ var unlinkCommand = new Command9("unlink").description("Remove a linked project
|
|
|
3343
3898
|
);
|
|
3344
3899
|
}
|
|
3345
3900
|
console.log(
|
|
3346
|
-
|
|
3901
|
+
chalk10.dim(
|
|
3347
3902
|
` ${updated.projects.length} project${updated.projects.length !== 1 ? "s" : ""} remaining`
|
|
3348
3903
|
)
|
|
3349
3904
|
);
|
|
@@ -3355,24 +3910,39 @@ var unlinkCommand = new Command9("unlink").description("Remove a linked project
|
|
|
3355
3910
|
|
|
3356
3911
|
// src/commands/sync.ts
|
|
3357
3912
|
import { Command as Command10 } from "commander";
|
|
3358
|
-
import
|
|
3913
|
+
import chalk11 from "chalk";
|
|
3359
3914
|
|
|
3360
3915
|
// src/lib/commit-guard.ts
|
|
3361
3916
|
import {
|
|
3362
|
-
existsSync as
|
|
3363
|
-
readFileSync as
|
|
3364
|
-
writeFileSync as
|
|
3365
|
-
mkdirSync,
|
|
3917
|
+
existsSync as existsSync4,
|
|
3918
|
+
readFileSync as readFileSync4,
|
|
3919
|
+
writeFileSync as writeFileSync4,
|
|
3920
|
+
mkdirSync as mkdirSync2,
|
|
3366
3921
|
chmodSync as chmodSync2,
|
|
3367
|
-
unlinkSync as
|
|
3368
|
-
|
|
3922
|
+
unlinkSync as unlinkSync3,
|
|
3923
|
+
renameSync as renameSync3,
|
|
3924
|
+
statSync as statSync2
|
|
3369
3925
|
} from "fs";
|
|
3370
|
-
import { join as
|
|
3926
|
+
import { join as join4, resolve as resolve2 } from "path";
|
|
3371
3927
|
import { execSync as execSync2 } from "child_process";
|
|
3928
|
+
function atomicWriteFileSync3(filePath, content, mode) {
|
|
3929
|
+
const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
3930
|
+
try {
|
|
3931
|
+
writeFileSync4(tmpPath, content, "utf-8");
|
|
3932
|
+
chmodSync2(tmpPath, mode);
|
|
3933
|
+
renameSync3(tmpPath, filePath);
|
|
3934
|
+
} catch (err) {
|
|
3935
|
+
try {
|
|
3936
|
+
if (existsSync4(tmpPath)) unlinkSync3(tmpPath);
|
|
3937
|
+
} catch {
|
|
3938
|
+
}
|
|
3939
|
+
throw err;
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3372
3942
|
var HOOK_START_MARKER = "# ENVPILOT_GUARD_START";
|
|
3373
3943
|
var HOOK_END_MARKER = "# ENVPILOT_GUARD_END";
|
|
3374
3944
|
var HOOK_BLOCK = `${HOOK_START_MARKER} - Do not remove. Installed by Envpilot CLI.
|
|
3375
|
-
ENV_FILES=$(git diff --cached --name-only | grep -E '(^|/)\\.env($|\\.)' || true)
|
|
3945
|
+
ENV_FILES=$(git diff --cached --name-only | grep -E '(^|/)\\.env($|\\.)' | grep -vE '\\.(example|sample|template|dist)$' || true)
|
|
3376
3946
|
if [ -n "$ENV_FILES" ]; then
|
|
3377
3947
|
echo ""
|
|
3378
3948
|
echo "\\033[1;31mERROR:\\033[0m Envpilot commit guard blocked this commit."
|
|
@@ -3388,7 +3958,7 @@ ${HOOK_END_MARKER}`;
|
|
|
3388
3958
|
function findGitRoot(startDir) {
|
|
3389
3959
|
let dir = startDir || process.cwd();
|
|
3390
3960
|
while (true) {
|
|
3391
|
-
if (
|
|
3961
|
+
if (existsSync4(join4(dir, ".git"))) {
|
|
3392
3962
|
return dir;
|
|
3393
3963
|
}
|
|
3394
3964
|
const parent = resolve2(dir, "..");
|
|
@@ -3399,9 +3969,9 @@ function findGitRoot(startDir) {
|
|
|
3399
3969
|
}
|
|
3400
3970
|
}
|
|
3401
3971
|
function resolveGitDir(repoRoot) {
|
|
3402
|
-
const gitPath =
|
|
3972
|
+
const gitPath = join4(repoRoot, ".git");
|
|
3403
3973
|
try {
|
|
3404
|
-
const stat =
|
|
3974
|
+
const stat = statSync2(gitPath);
|
|
3405
3975
|
if (stat.isDirectory()) {
|
|
3406
3976
|
return gitPath;
|
|
3407
3977
|
}
|
|
@@ -3409,12 +3979,12 @@ function resolveGitDir(repoRoot) {
|
|
|
3409
3979
|
return gitPath;
|
|
3410
3980
|
}
|
|
3411
3981
|
try {
|
|
3412
|
-
const content =
|
|
3982
|
+
const content = readFileSync4(gitPath, "utf-8").trim();
|
|
3413
3983
|
const match = content.match(/^gitdir:\s*(.+)$/);
|
|
3414
3984
|
if (match) {
|
|
3415
3985
|
const gitdir = resolve2(repoRoot, match[1]);
|
|
3416
3986
|
const commonDir = resolve2(gitdir, "..", "..");
|
|
3417
|
-
if (
|
|
3987
|
+
if (existsSync4(join4(commonDir, "hooks")) || existsSync4(commonDir)) {
|
|
3418
3988
|
return commonDir;
|
|
3419
3989
|
}
|
|
3420
3990
|
return gitdir;
|
|
@@ -3436,7 +4006,7 @@ function getHooksDir(repoRoot) {
|
|
|
3436
4006
|
} catch {
|
|
3437
4007
|
}
|
|
3438
4008
|
const gitDir = resolveGitDir(repoRoot);
|
|
3439
|
-
return
|
|
4009
|
+
return join4(gitDir, "hooks");
|
|
3440
4010
|
}
|
|
3441
4011
|
function installCommitGuard(repoRoot) {
|
|
3442
4012
|
const root = repoRoot || findGitRoot();
|
|
@@ -3449,19 +4019,18 @@ function installCommitGuard(repoRoot) {
|
|
|
3449
4019
|
}
|
|
3450
4020
|
try {
|
|
3451
4021
|
const hooksDir = getHooksDir(root);
|
|
3452
|
-
const hookPath =
|
|
3453
|
-
|
|
4022
|
+
const hookPath = join4(hooksDir, "pre-commit");
|
|
4023
|
+
mkdirSync2(hooksDir, { recursive: true });
|
|
3454
4024
|
let existingContent = "";
|
|
3455
4025
|
try {
|
|
3456
|
-
existingContent =
|
|
4026
|
+
existingContent = readFileSync4(hookPath, "utf-8");
|
|
3457
4027
|
} catch {
|
|
3458
4028
|
}
|
|
3459
4029
|
if (existingContent.includes(HOOK_START_MARKER)) {
|
|
3460
4030
|
const startIdx = existingContent.indexOf(HOOK_START_MARKER);
|
|
3461
4031
|
const endIdx = existingContent.indexOf(HOOK_END_MARKER) + HOOK_END_MARKER.length;
|
|
3462
4032
|
const updated = existingContent.substring(0, startIdx) + HOOK_BLOCK + existingContent.substring(endIdx);
|
|
3463
|
-
|
|
3464
|
-
chmodSync2(hookPath, 493);
|
|
4033
|
+
atomicWriteFileSync3(hookPath, updated, 493);
|
|
3465
4034
|
return {
|
|
3466
4035
|
installed: true,
|
|
3467
4036
|
hookPath,
|
|
@@ -3474,8 +4043,7 @@ function installCommitGuard(repoRoot) {
|
|
|
3474
4043
|
} else {
|
|
3475
4044
|
newContent = "#!/bin/sh\n\n" + HOOK_BLOCK + "\n";
|
|
3476
4045
|
}
|
|
3477
|
-
|
|
3478
|
-
chmodSync2(hookPath, 493);
|
|
4046
|
+
atomicWriteFileSync3(hookPath, newContent, 493);
|
|
3479
4047
|
return {
|
|
3480
4048
|
installed: true,
|
|
3481
4049
|
hookPath,
|
|
@@ -3491,6 +4059,21 @@ function installCommitGuard(repoRoot) {
|
|
|
3491
4059
|
}
|
|
3492
4060
|
|
|
3493
4061
|
// src/commands/sync.ts
|
|
4062
|
+
function accessFromMeta2(meta, variables) {
|
|
4063
|
+
const role = normalizeOrgRole(meta?.unifiedRole ?? meta?.role);
|
|
4064
|
+
return {
|
|
4065
|
+
role,
|
|
4066
|
+
// Owners are implicitly assigned to every project. The legacy server sends
|
|
4067
|
+
// no `assigned`/`projectRole` for owners, so fall back to true for them.
|
|
4068
|
+
assigned: role === "owner" || (meta?.assigned ?? (meta?.projectRole !== null && meta?.projectRole !== void 0)),
|
|
4069
|
+
environmentScope: meta?.environmentScope ?? null,
|
|
4070
|
+
hasWriteAccess: meta?.hasWriteAccess ?? variables.some((v) => v.access === "write")
|
|
4071
|
+
};
|
|
4072
|
+
}
|
|
4073
|
+
function orgUnifiedRole3(org) {
|
|
4074
|
+
const unified = org.unifiedRole;
|
|
4075
|
+
return normalizeOrgRole(unified ?? org.role);
|
|
4076
|
+
}
|
|
3494
4077
|
var syncCommand = new Command10("sync").description(
|
|
3495
4078
|
"Login, select project, pull variables, and protect files \u2014 all in one command"
|
|
3496
4079
|
).option("-o, --organization <id>", "Organization ID").option("-p, --project <id>", "Project ID or name").option(
|
|
@@ -3533,7 +4116,7 @@ var syncCommand = new Command10("sync").description(
|
|
|
3533
4116
|
projectName = active.projectName;
|
|
3534
4117
|
organizationName = active.organizationName;
|
|
3535
4118
|
info(
|
|
3536
|
-
`Using project ${
|
|
4119
|
+
`Using project ${chalk11.bold(projectName || projectId)} (${environment})`
|
|
3537
4120
|
);
|
|
3538
4121
|
} else {
|
|
3539
4122
|
const selection = await selectOrgProjectEnv(options);
|
|
@@ -3554,9 +4137,7 @@ var syncCommand = new Command10("sync").description(
|
|
|
3554
4137
|
environment = selection.selectedEnvironment;
|
|
3555
4138
|
projectName = selection.selectedProject.name;
|
|
3556
4139
|
organizationName = selection.selectedOrg.name;
|
|
3557
|
-
|
|
3558
|
-
setRole(selection.selectedOrg.role);
|
|
3559
|
-
}
|
|
4140
|
+
setUnifiedRole(orgUnifiedRole3(selection.selectedOrg));
|
|
3560
4141
|
writeProjectConfigV2({
|
|
3561
4142
|
version: 1,
|
|
3562
4143
|
activeProjectId: projectId,
|
|
@@ -3585,9 +4166,9 @@ var syncCommand = new Command10("sync").description(
|
|
|
3585
4166
|
setActiveProjectId(projectId);
|
|
3586
4167
|
}
|
|
3587
4168
|
console.log();
|
|
3588
|
-
let
|
|
4169
|
+
let meta;
|
|
3589
4170
|
const variables = await withSpinner(
|
|
3590
|
-
`Fetching ${
|
|
4171
|
+
`Fetching ${chalk11.bold(environment)} variables...`,
|
|
3591
4172
|
async () => {
|
|
3592
4173
|
const api = createAPIClient();
|
|
3593
4174
|
const response = await api.get("/api/cli/variables", {
|
|
@@ -3595,11 +4176,16 @@ var syncCommand = new Command10("sync").description(
|
|
|
3595
4176
|
environment,
|
|
3596
4177
|
...organizationId && { organizationId }
|
|
3597
4178
|
});
|
|
3598
|
-
|
|
4179
|
+
meta = response.meta;
|
|
3599
4180
|
return response.data || [];
|
|
3600
4181
|
}
|
|
3601
4182
|
);
|
|
3602
4183
|
const outputPath = getEnvPathForEnvironment(environment);
|
|
4184
|
+
if (meta?.scopeRestricted && meta.environmentScope?.length) {
|
|
4185
|
+
info(
|
|
4186
|
+
`Your access is scoped to ${meta.environmentScope.join(", ")}; variables in other environments are withheld.`
|
|
4187
|
+
);
|
|
4188
|
+
}
|
|
3603
4189
|
if (variables.length === 0) {
|
|
3604
4190
|
warning(`No variables found for ${environment} environment.`);
|
|
3605
4191
|
console.log();
|
|
@@ -3613,12 +4199,12 @@ var syncCommand = new Command10("sync").description(
|
|
|
3613
4199
|
const diffResult = diffEnvVars(remoteVars, localVars);
|
|
3614
4200
|
const hasChanges = Object.keys(diffResult.added).length > 0 || Object.keys(diffResult.removed).length > 0 || Object.keys(diffResult.changed).length > 0;
|
|
3615
4201
|
if (!hasChanges) {
|
|
3616
|
-
success(`${
|
|
4202
|
+
success(`${chalk11.bold(outputPath)} is up to date.`);
|
|
3617
4203
|
console.log();
|
|
3618
4204
|
return;
|
|
3619
4205
|
}
|
|
3620
4206
|
console.log();
|
|
3621
|
-
console.log(
|
|
4207
|
+
console.log(chalk11.bold("Changes:"));
|
|
3622
4208
|
console.log();
|
|
3623
4209
|
diff(diffResult.added, diffResult.removed, diffResult.changed);
|
|
3624
4210
|
console.log();
|
|
@@ -3636,26 +4222,32 @@ var syncCommand = new Command10("sync").description(
|
|
|
3636
4222
|
}
|
|
3637
4223
|
}
|
|
3638
4224
|
writeEnvFile(outputPath, remoteVars, { sort: true, comments });
|
|
3639
|
-
const
|
|
3640
|
-
|
|
4225
|
+
const access = accessFromMeta2(meta, variables);
|
|
4226
|
+
const protection = existingConfig?.projects.find((p) => p.projectId === projectId)?.fileProtection ?? "auto";
|
|
4227
|
+
applyFileProtection(outputPath, access, protection);
|
|
3641
4228
|
success(
|
|
3642
|
-
`Synced ${variables.length} variables to ${
|
|
4229
|
+
`Synced ${variables.length} variables to ${chalk11.bold(outputPath)}`
|
|
3643
4230
|
);
|
|
3644
|
-
|
|
3645
|
-
if (isProtected) {
|
|
4231
|
+
if (meta?.grantOnly) {
|
|
3646
4232
|
info(
|
|
3647
|
-
|
|
4233
|
+
"You have grant-only access to this project. You only see variables explicitly shared with you."
|
|
4234
|
+
);
|
|
4235
|
+
}
|
|
4236
|
+
const writable = protection === "never" || protection !== "always" && isFileWritable(access);
|
|
4237
|
+
if (!writable) {
|
|
4238
|
+
info(
|
|
4239
|
+
`File is read-only (your role: ${formatRoleLabel(access.role)}${access.assigned ? "" : ", not assigned to this project"}).`
|
|
3648
4240
|
);
|
|
3649
4241
|
}
|
|
3650
4242
|
console.log();
|
|
3651
4243
|
console.log(
|
|
3652
|
-
|
|
4244
|
+
chalk11.dim(` Added: ${Object.keys(diffResult.added).length}`)
|
|
3653
4245
|
);
|
|
3654
4246
|
console.log(
|
|
3655
|
-
|
|
4247
|
+
chalk11.dim(` Changed: ${Object.keys(diffResult.changed).length}`)
|
|
3656
4248
|
);
|
|
3657
4249
|
console.log(
|
|
3658
|
-
|
|
4250
|
+
chalk11.dim(` Removed: ${Object.keys(diffResult.removed).length}`)
|
|
3659
4251
|
);
|
|
3660
4252
|
console.log();
|
|
3661
4253
|
} catch (err) {
|
|
@@ -3665,17 +4257,17 @@ var syncCommand = new Command10("sync").description(
|
|
|
3665
4257
|
|
|
3666
4258
|
// src/commands/usage.ts
|
|
3667
4259
|
import { Command as Command11 } from "commander";
|
|
3668
|
-
import
|
|
4260
|
+
import chalk12 from "chalk";
|
|
3669
4261
|
function formatUsage(current, limit) {
|
|
3670
4262
|
const limitStr = limit === null ? "unlimited" : String(limit);
|
|
3671
4263
|
const ratio = `${current}/${limitStr}`;
|
|
3672
|
-
if (limit === null) return
|
|
3673
|
-
if (current >= limit) return
|
|
3674
|
-
if (current >= limit * 0.8) return
|
|
3675
|
-
return
|
|
4264
|
+
if (limit === null) return chalk12.green(ratio);
|
|
4265
|
+
if (current >= limit) return chalk12.red(ratio);
|
|
4266
|
+
if (current >= limit * 0.8) return chalk12.yellow(ratio);
|
|
4267
|
+
return chalk12.green(ratio);
|
|
3676
4268
|
}
|
|
3677
4269
|
function featureStatus(enabled) {
|
|
3678
|
-
return enabled ?
|
|
4270
|
+
return enabled ? chalk12.green("Enabled") : chalk12.dim("Disabled (Pro)");
|
|
3679
4271
|
}
|
|
3680
4272
|
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) => {
|
|
3681
4273
|
try {
|
|
@@ -3720,7 +4312,7 @@ var usageCommand = new Command11("usage").description("Show plan usage and limit
|
|
|
3720
4312
|
console.log(JSON.stringify(usage, null, 2));
|
|
3721
4313
|
return;
|
|
3722
4314
|
}
|
|
3723
|
-
const tierLabel = usage.tier === "pro" ?
|
|
4315
|
+
const tierLabel = usage.tier === "pro" ? chalk12.green("Pro") : chalk12.white("Free");
|
|
3724
4316
|
header(`Plan: ${tierLabel}`);
|
|
3725
4317
|
blank();
|
|
3726
4318
|
if (!usage.enforcementEnabled) {
|
|
@@ -3774,7 +4366,7 @@ var usageCommand = new Command11("usage").description("Show plan usage and limit
|
|
|
3774
4366
|
|
|
3775
4367
|
// src/commands/whoami.ts
|
|
3776
4368
|
import { Command as Command12 } from "commander";
|
|
3777
|
-
import
|
|
4369
|
+
import chalk13 from "chalk";
|
|
3778
4370
|
var whoamiCommand = new Command12("whoami").description("Show the current authenticated user and active CLI context").action(async () => {
|
|
3779
4371
|
try {
|
|
3780
4372
|
if (!isAuthenticated()) {
|
|
@@ -3792,6 +4384,7 @@ var whoamiCommand = new Command12("whoami").description("Show the current authen
|
|
|
3792
4384
|
["Name", remoteUser.name || localUser?.name],
|
|
3793
4385
|
["API URL", getApiUrl()],
|
|
3794
4386
|
["Active Organization", getActiveOrganizationId()],
|
|
4387
|
+
["Org Role", formatRoleLabel(getUnifiedRole())],
|
|
3795
4388
|
["Active Project", getActiveProjectId()],
|
|
3796
4389
|
[
|
|
3797
4390
|
"Linked Project",
|
|
@@ -3799,117 +4392,136 @@ var whoamiCommand = new Command12("whoami").description("Show the current authen
|
|
|
3799
4392
|
],
|
|
3800
4393
|
["Environment", activeProject?.environment]
|
|
3801
4394
|
]);
|
|
4395
|
+
const accounts = listAccounts();
|
|
4396
|
+
if (accounts.length > 1) {
|
|
4397
|
+
blank();
|
|
4398
|
+
console.log(
|
|
4399
|
+
chalk13.dim(
|
|
4400
|
+
`Accounts: ${accounts.length} (use \`envpilot accounts\` to switch)`
|
|
4401
|
+
)
|
|
4402
|
+
);
|
|
4403
|
+
}
|
|
3802
4404
|
blank();
|
|
3803
|
-
console.log(
|
|
4405
|
+
console.log(chalk13.dim("Token verified against the CLI auth endpoint."));
|
|
3804
4406
|
} catch (err) {
|
|
3805
4407
|
await handleError(err);
|
|
3806
4408
|
}
|
|
3807
4409
|
});
|
|
3808
4410
|
|
|
3809
|
-
// src/commands/
|
|
4411
|
+
// src/commands/accounts.ts
|
|
3810
4412
|
import { Command as Command13 } from "commander";
|
|
3811
|
-
import
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
readFileSync as readFileSync4,
|
|
3818
|
-
writeFileSync as writeFileSync4,
|
|
3819
|
-
mkdirSync as mkdirSync2,
|
|
3820
|
-
existsSync as existsSync4,
|
|
3821
|
-
readdirSync,
|
|
3822
|
-
unlinkSync as unlinkSync3,
|
|
3823
|
-
statSync as statSync2
|
|
3824
|
-
} from "fs";
|
|
3825
|
-
import { join as join4, dirname as dirname2 } from "path";
|
|
3826
|
-
function getCacheDir() {
|
|
3827
|
-
return join4(dirname2(getConfigPath()), "run-cache");
|
|
3828
|
-
}
|
|
3829
|
-
function getCacheKey(projectId, environment, organizationId) {
|
|
3830
|
-
const tokenSlice = (getAccessToken() ?? "").slice(0, 16);
|
|
3831
|
-
return createHash("sha256").update(`${projectId}:${environment}:${organizationId}:${tokenSlice}`).digest("hex").slice(0, 20);
|
|
4413
|
+
import chalk14 from "chalk";
|
|
4414
|
+
function resolveAccount(accounts, identifier) {
|
|
4415
|
+
const normalized = identifier.trim().toLowerCase();
|
|
4416
|
+
return accounts.find(
|
|
4417
|
+
(account) => account.id === identifier || account.user.email.toLowerCase() === normalized
|
|
4418
|
+
);
|
|
3832
4419
|
}
|
|
3833
|
-
function
|
|
3834
|
-
|
|
4420
|
+
function listAvailableEmails(accounts) {
|
|
4421
|
+
console.log();
|
|
4422
|
+
console.log("Available accounts:");
|
|
4423
|
+
for (const account of accounts) {
|
|
4424
|
+
console.log(` ${account.user.email}`);
|
|
4425
|
+
}
|
|
3835
4426
|
}
|
|
3836
|
-
function
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
4427
|
+
function printAccountsList() {
|
|
4428
|
+
const accounts = listAccounts();
|
|
4429
|
+
const activeId = getActiveAccountId();
|
|
4430
|
+
if (accounts.length === 0) {
|
|
4431
|
+
console.log("No accounts. Run `envpilot login`.");
|
|
4432
|
+
return;
|
|
4433
|
+
}
|
|
4434
|
+
table(
|
|
4435
|
+
accounts.map((account) => ({
|
|
4436
|
+
active: account.id === activeId ? chalk14.green("\u25CF") : "",
|
|
4437
|
+
email: account.user.email,
|
|
4438
|
+
name: account.user.name ?? "",
|
|
4439
|
+
organization: account.activeOrganizationId ?? ""
|
|
4440
|
+
})),
|
|
4441
|
+
[
|
|
4442
|
+
{ key: "active", header: "" },
|
|
4443
|
+
{ key: "email", header: "Email" },
|
|
4444
|
+
{ key: "name", header: "Name" },
|
|
4445
|
+
{ key: "organization", header: "Active Org" }
|
|
4446
|
+
]
|
|
4447
|
+
);
|
|
4448
|
+
blank();
|
|
4449
|
+
if (accounts.length === 1) {
|
|
4450
|
+
console.log(chalk14.dim("1 account (log in again to add another)."));
|
|
4451
|
+
} else {
|
|
4452
|
+
console.log(chalk14.dim(`${accounts.length} accounts.`));
|
|
4453
|
+
}
|
|
3840
4454
|
}
|
|
3841
|
-
function
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
if (entry.apiUrl !== getApiUrl()) return null;
|
|
3849
|
-
return entry;
|
|
3850
|
-
} catch {
|
|
3851
|
-
return null;
|
|
4455
|
+
function switchAccount(identifier) {
|
|
4456
|
+
const accounts = listAccounts();
|
|
4457
|
+
const target = resolveAccount(accounts, identifier);
|
|
4458
|
+
if (!target) {
|
|
4459
|
+
error(`Account not found: ${identifier}`);
|
|
4460
|
+
listAvailableEmails(accounts);
|
|
4461
|
+
process.exit(1);
|
|
3852
4462
|
}
|
|
4463
|
+
setActiveAccount(target.id);
|
|
4464
|
+
success(`Switched to ${target.user.email}.`);
|
|
3853
4465
|
}
|
|
3854
|
-
function
|
|
3855
|
-
const
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
4466
|
+
function removeAccountByIdentifier(identifier) {
|
|
4467
|
+
const accounts = listAccounts();
|
|
4468
|
+
const target = resolveAccount(accounts, identifier);
|
|
4469
|
+
if (!target) {
|
|
4470
|
+
error(`Account not found: ${identifier}`);
|
|
4471
|
+
listAvailableEmails(accounts);
|
|
4472
|
+
process.exit(1);
|
|
4473
|
+
}
|
|
4474
|
+
const result = removeAccount(target.id);
|
|
4475
|
+
success(`Removed ${target.user.email}.`);
|
|
4476
|
+
if (result.removedActive) {
|
|
4477
|
+
if (result.newActiveId) {
|
|
4478
|
+
const remaining = listAccounts();
|
|
4479
|
+
const newActive = remaining.find((a) => a.id === result.newActiveId);
|
|
4480
|
+
console.log(
|
|
4481
|
+
chalk14.dim(`Now active: ${newActive?.user.email ?? result.newActiveId}`)
|
|
4482
|
+
);
|
|
4483
|
+
} else {
|
|
4484
|
+
console.log(chalk14.dim("No accounts remaining."));
|
|
4485
|
+
}
|
|
4486
|
+
}
|
|
3859
4487
|
}
|
|
3860
|
-
|
|
4488
|
+
var accountsCommand = new Command13("accounts").description("List and manage authenticated accounts").action(async () => {
|
|
3861
4489
|
try {
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
const path = getCachePath(key);
|
|
3866
|
-
const entry = {
|
|
3867
|
-
variables,
|
|
3868
|
-
fetchedAt: Date.now(),
|
|
3869
|
-
projectId,
|
|
3870
|
-
environment,
|
|
3871
|
-
organizationId,
|
|
3872
|
-
apiUrl: getApiUrl(),
|
|
3873
|
-
fingerprint: computeFingerprint(variables)
|
|
3874
|
-
};
|
|
3875
|
-
writeFileSync4(path, JSON.stringify(entry, null, 2), {
|
|
3876
|
-
encoding: "utf-8",
|
|
3877
|
-
mode: 384
|
|
3878
|
-
// owner read/write only — same as .env
|
|
3879
|
-
});
|
|
3880
|
-
} catch {
|
|
4490
|
+
printAccountsList();
|
|
4491
|
+
} catch (err) {
|
|
4492
|
+
await handleError(err);
|
|
3881
4493
|
}
|
|
3882
|
-
}
|
|
3883
|
-
|
|
4494
|
+
});
|
|
4495
|
+
accountsCommand.command("list").description("List all authenticated accounts").action(async () => {
|
|
3884
4496
|
try {
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
const raw = readFileSync4(path, "utf-8");
|
|
3889
|
-
const entry = JSON.parse(raw);
|
|
3890
|
-
entry.fetchedAt = Date.now();
|
|
3891
|
-
writeFileSync4(path, JSON.stringify(entry, null, 2), {
|
|
3892
|
-
encoding: "utf-8",
|
|
3893
|
-
mode: 384
|
|
3894
|
-
});
|
|
3895
|
-
} catch {
|
|
4497
|
+
printAccountsList();
|
|
4498
|
+
} catch (err) {
|
|
4499
|
+
await handleError(err);
|
|
3896
4500
|
}
|
|
3897
|
-
}
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
4501
|
+
});
|
|
4502
|
+
accountsCommand.command("switch").description("Switch the active account").argument("<identifier>", "Account id or email").action(async (identifier) => {
|
|
4503
|
+
try {
|
|
4504
|
+
switchAccount(identifier);
|
|
4505
|
+
} catch (err) {
|
|
4506
|
+
await handleError(err);
|
|
4507
|
+
}
|
|
4508
|
+
});
|
|
4509
|
+
accountsCommand.command("remove").description("Remove an account").argument("<identifier>", "Account id or email").action(async (identifier) => {
|
|
4510
|
+
try {
|
|
4511
|
+
removeAccountByIdentifier(identifier);
|
|
4512
|
+
} catch (err) {
|
|
4513
|
+
await handleError(err);
|
|
4514
|
+
}
|
|
4515
|
+
});
|
|
3908
4516
|
|
|
3909
4517
|
// src/commands/run.ts
|
|
4518
|
+
import { Command as Command14 } from "commander";
|
|
4519
|
+
import crossSpawn from "cross-spawn";
|
|
4520
|
+
import { constants as osConstants } from "os";
|
|
4521
|
+
import chalk15 from "chalk";
|
|
3910
4522
|
var DEFAULT_TTL = 3600;
|
|
3911
|
-
var runCommand = new
|
|
3912
|
-
"Run a command with project secrets injected as environment variables (no .env file written)"
|
|
4523
|
+
var runCommand = new Command14("run").description(
|
|
4524
|
+
"Run a command with project secrets injected as environment variables. Secrets are injected into the child process in-memory (no .env file is written), but a decrypted copy is cached locally at ~/.config/envpilot/run-cache (mode 0600, same protection as a .env) to avoid re-fetching on every run. Use --no-cache to skip the cache, or `envpilot logout` to purge it."
|
|
3913
4525
|
).argument("[command...]", "Command and arguments to execute").option(
|
|
3914
4526
|
"-e, --env <environment>",
|
|
3915
4527
|
"Environment to load (development, staging, production)"
|
|
@@ -3927,10 +4539,10 @@ var runCommand = new Command13("run").description(
|
|
|
3927
4539
|
"Print the variables that would be injected and exit (no command runs)"
|
|
3928
4540
|
).option(
|
|
3929
4541
|
"--shell",
|
|
3930
|
-
|
|
4542
|
+
'Run the command string through your shell ($SHELL or /bin/sh -c "..."), enabling pipes, &&, and $VAR expansion. Without this flag the command is executed directly with no shell, so arguments are passed through verbatim (safe from shell injection).'
|
|
3931
4543
|
).option("-q, --quiet", "Suppress informational messages").option("--no-cache", "Skip cache and always fetch fresh secrets from server").option(
|
|
3932
4544
|
"--cache-ttl <seconds>",
|
|
3933
|
-
"How long cached secrets stay fresh before a fingerprint check (default: 3600s / 1h)",
|
|
4545
|
+
"How long cached secrets stay fresh before a fingerprint check (default: 3600s / 1h; 0 = always fingerprint-check)",
|
|
3934
4546
|
String(DEFAULT_TTL)
|
|
3935
4547
|
).passThroughOptions().allowExcessArguments().action(async (commandArgs, options) => {
|
|
3936
4548
|
try {
|
|
@@ -3959,10 +4571,11 @@ var runCommand = new Command13("run").description(
|
|
|
3959
4571
|
}
|
|
3960
4572
|
const environment = options.env || project.environment;
|
|
3961
4573
|
const organizationId = options.organization || project.organizationId;
|
|
3962
|
-
const
|
|
3963
|
-
|
|
3964
|
-
|
|
4574
|
+
const parsedTtl = Number.parseInt(
|
|
4575
|
+
options.cacheTtl ?? String(DEFAULT_TTL),
|
|
4576
|
+
10
|
|
3965
4577
|
);
|
|
4578
|
+
const ttl = Number.isFinite(parsedTtl) && parsedTtl >= 0 ? parsedTtl : DEFAULT_TTL;
|
|
3966
4579
|
let variables;
|
|
3967
4580
|
let cacheHit = false;
|
|
3968
4581
|
let cacheAge = "";
|
|
@@ -4014,13 +4627,19 @@ var runCommand = new Command13("run").description(
|
|
|
4014
4627
|
project.projectId,
|
|
4015
4628
|
environment,
|
|
4016
4629
|
organizationId,
|
|
4017
|
-
variables
|
|
4630
|
+
variables,
|
|
4631
|
+
serverFingerprint
|
|
4018
4632
|
);
|
|
4019
4633
|
}
|
|
4020
4634
|
} catch {
|
|
4021
4635
|
variables = probe.entry.variables;
|
|
4022
4636
|
cacheHit = true;
|
|
4023
4637
|
cacheAge = formatAge(probe.entry.fetchedAt);
|
|
4638
|
+
if (!options.quiet) {
|
|
4639
|
+
warning(
|
|
4640
|
+
`Using offline cache (age ${cacheAge}) \u2014 could not reach the server to verify freshness.`
|
|
4641
|
+
);
|
|
4642
|
+
}
|
|
4024
4643
|
}
|
|
4025
4644
|
} else {
|
|
4026
4645
|
variables = await doFetch(
|
|
@@ -4069,7 +4688,7 @@ var runCommand = new Command13("run").description(
|
|
|
4069
4688
|
}
|
|
4070
4689
|
console.log();
|
|
4071
4690
|
}
|
|
4072
|
-
await runChild(commandArgs, finalEnv, options);
|
|
4691
|
+
process.exitCode = await runChild(commandArgs, finalEnv, options);
|
|
4073
4692
|
} catch (err) {
|
|
4074
4693
|
await handleError(err);
|
|
4075
4694
|
}
|
|
@@ -4112,28 +4731,31 @@ function printInjectionPreview(secrets, project, environment) {
|
|
|
4112
4731
|
success("Dry run \u2014 no command executed.");
|
|
4113
4732
|
}
|
|
4114
4733
|
function maskForPreview(value) {
|
|
4115
|
-
|
|
4116
|
-
|
|
4734
|
+
const len = value.length;
|
|
4735
|
+
const dots = "\u2022".repeat(6);
|
|
4736
|
+
if (len < 12) return `${dots} (${len} chars)`;
|
|
4737
|
+
return `${value.slice(0, 2)}${dots} (${len} chars)`;
|
|
4117
4738
|
}
|
|
4118
4739
|
function runChild(commandArgs, env, options) {
|
|
4119
4740
|
return new Promise((resolve3) => {
|
|
4120
|
-
|
|
4741
|
+
let command;
|
|
4742
|
+
let args;
|
|
4743
|
+
if (options.shell) {
|
|
4744
|
+
command = process.env.SHELL || "/bin/sh";
|
|
4745
|
+
args = ["-c", commandArgs.join(" ")];
|
|
4746
|
+
} else {
|
|
4747
|
+
[command, ...args] = commandArgs;
|
|
4748
|
+
}
|
|
4121
4749
|
if (!command) {
|
|
4122
|
-
|
|
4750
|
+
resolve3(1);
|
|
4751
|
+
return;
|
|
4123
4752
|
}
|
|
4124
|
-
const
|
|
4125
|
-
const useShell = options.shell === true || isWindows;
|
|
4126
|
-
const child = spawn2(command, args, {
|
|
4753
|
+
const child = crossSpawn(command, args, {
|
|
4127
4754
|
stdio: "inherit",
|
|
4128
4755
|
env,
|
|
4129
|
-
shell:
|
|
4756
|
+
shell: false
|
|
4130
4757
|
});
|
|
4131
|
-
const signals = [
|
|
4132
|
-
"SIGINT",
|
|
4133
|
-
"SIGTERM",
|
|
4134
|
-
"SIGHUP",
|
|
4135
|
-
"SIGQUIT"
|
|
4136
|
-
];
|
|
4758
|
+
const signals = ["SIGTERM", "SIGHUP"];
|
|
4137
4759
|
const forward = {};
|
|
4138
4760
|
for (const sig of signals) {
|
|
4139
4761
|
const handler = () => {
|
|
@@ -4156,35 +4778,34 @@ function runChild(commandArgs, env, options) {
|
|
|
4156
4778
|
child.on("error", (err) => {
|
|
4157
4779
|
cleanup();
|
|
4158
4780
|
const code = err.code;
|
|
4781
|
+
const displayCommand = options.shell ? commandArgs.join(" ") : command;
|
|
4159
4782
|
if (code === "ENOENT") {
|
|
4160
4783
|
error(
|
|
4161
|
-
`Command not found: ${
|
|
4784
|
+
`Command not found: ${displayCommand}. Make sure it is installed and on your PATH.`
|
|
4162
4785
|
);
|
|
4786
|
+
resolve3(127);
|
|
4163
4787
|
} else if (code === "EACCES") {
|
|
4164
|
-
error(`Permission denied executing: ${
|
|
4788
|
+
error(`Permission denied executing: ${displayCommand}`);
|
|
4789
|
+
resolve3(126);
|
|
4165
4790
|
} else {
|
|
4166
4791
|
error(`Failed to spawn process: ${err.message}`);
|
|
4792
|
+
resolve3(1);
|
|
4167
4793
|
}
|
|
4168
|
-
process.exit(1);
|
|
4169
4794
|
});
|
|
4170
4795
|
child.on("exit", (code, signal) => {
|
|
4171
4796
|
cleanup();
|
|
4172
4797
|
if (signal) {
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
} catch {
|
|
4176
|
-
process.exit(1);
|
|
4177
|
-
}
|
|
4798
|
+
const signum = osConstants.signals[signal] ?? 0;
|
|
4799
|
+
resolve3(128 + signum);
|
|
4178
4800
|
} else {
|
|
4179
|
-
|
|
4801
|
+
resolve3(code ?? 0);
|
|
4180
4802
|
}
|
|
4181
|
-
resolve3();
|
|
4182
4803
|
});
|
|
4183
4804
|
});
|
|
4184
4805
|
}
|
|
4185
4806
|
|
|
4186
4807
|
// src/commands/man.ts
|
|
4187
|
-
import { Command as
|
|
4808
|
+
import { Command as Command15 } from "commander";
|
|
4188
4809
|
import chalk16 from "chalk";
|
|
4189
4810
|
function printCommandManual(commandName) {
|
|
4190
4811
|
const command = findCommandDefinition(commandName);
|
|
@@ -4254,7 +4875,7 @@ function printManualIndex(commands) {
|
|
|
4254
4875
|
);
|
|
4255
4876
|
}
|
|
4256
4877
|
function createManCommand(commands) {
|
|
4257
|
-
return new
|
|
4878
|
+
return new Command15("man").description("Show the CLI manual page").argument("[command]", "Optional command to show detailed manual for").action((command) => {
|
|
4258
4879
|
if (command) {
|
|
4259
4880
|
printCommandManual(command);
|
|
4260
4881
|
return;
|
|
@@ -4264,9 +4885,9 @@ function createManCommand(commands) {
|
|
|
4264
4885
|
}
|
|
4265
4886
|
|
|
4266
4887
|
// src/commands/ui.ts
|
|
4267
|
-
import { Command as
|
|
4888
|
+
import { Command as Command16 } from "commander";
|
|
4268
4889
|
function createUICommand() {
|
|
4269
|
-
return new
|
|
4890
|
+
return new Command16("ui").alias("dashboard").description("Open the interactive Ink-powered terminal UI").action(async () => {
|
|
4270
4891
|
if (process.env.ENVPILOT_TUI_CHILD === "1") {
|
|
4271
4892
|
return;
|
|
4272
4893
|
}
|
|
@@ -4403,13 +5024,14 @@ var COMMAND_CATALOG = [
|
|
|
4403
5024
|
id: "push",
|
|
4404
5025
|
title: "Push variables",
|
|
4405
5026
|
category: "Sync",
|
|
4406
|
-
description: "Upload local variables back to Envpilot
|
|
5027
|
+
description: "Upload local variables back to Envpilot, writing only the keys you have access to.",
|
|
4407
5028
|
argv: ["push"],
|
|
4408
5029
|
args: "[--env <environment>] [--file <path>] [--merge|--replace]",
|
|
4409
5030
|
examples: [["push"], ["push", "--replace"]],
|
|
4410
5031
|
websiteSurface: "Maps to `/api/cli/variables` and `/api/cli/variables/bulk` for writes.",
|
|
4411
5032
|
notes: [
|
|
4412
|
-
"
|
|
5033
|
+
"Owners, project managers, and team leads write across the project; developers write only the variables they hold a write grant for.",
|
|
5034
|
+
"Keys you cannot write are skipped \u2014 push does not create approval requests.",
|
|
4413
5035
|
"Compares local and remote variables before applying changes."
|
|
4414
5036
|
],
|
|
4415
5037
|
keywords: ["upload", "bulk", "merge", "replace"],
|
|
@@ -4585,6 +5207,27 @@ var COMMAND_CATALOG = [
|
|
|
4585
5207
|
topLevel: true,
|
|
4586
5208
|
createCommand: () => whoamiCommand
|
|
4587
5209
|
},
|
|
5210
|
+
{
|
|
5211
|
+
id: "accounts",
|
|
5212
|
+
title: "Manage accounts",
|
|
5213
|
+
category: "Account",
|
|
5214
|
+
description: "List authenticated accounts and switch or remove them without logging out.",
|
|
5215
|
+
argv: ["accounts"],
|
|
5216
|
+
args: "[list|switch <identifier>|remove <identifier>]",
|
|
5217
|
+
examples: [
|
|
5218
|
+
["accounts"],
|
|
5219
|
+
["accounts", "switch", "you@example.com"],
|
|
5220
|
+
["accounts", "remove", "you@example.com"]
|
|
5221
|
+
],
|
|
5222
|
+
websiteSurface: "Local multi-account state \u2014 each `envpilot login` adds an account instead of replacing the current session.",
|
|
5223
|
+
notes: [
|
|
5224
|
+
"Identifiers can be an account id or the account's email (case-insensitive).",
|
|
5225
|
+
"Switching accounts does not log anyone out; use `envpilot logout` to remove the active session."
|
|
5226
|
+
],
|
|
5227
|
+
keywords: ["accounts", "multi-account", "switch", "remove", "identity"],
|
|
5228
|
+
topLevel: true,
|
|
5229
|
+
createCommand: () => accountsCommand
|
|
5230
|
+
},
|
|
4588
5231
|
{
|
|
4589
5232
|
id: "config",
|
|
4590
5233
|
title: "Config",
|