@envpilot/cli 1.7.2 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -8
- package/dist/{app-QG4JFCWP.js → app-UT26PMDR.js} +1 -1
- package/dist/{chunk-UVAK2D4O.js → chunk-QYRSBLFW.js} +1625 -453
- package/dist/index.js +10 -5
- package/package.json +3 -1
|
@@ -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.10.0" : "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
|
|
120
|
-
}
|
|
121
|
-
function setUser(user) {
|
|
122
|
-
config.set("user", user);
|
|
412
|
+
return getActiveAccount()?.user;
|
|
123
413
|
}
|
|
124
|
-
function
|
|
125
|
-
return
|
|
414
|
+
function getUnifiedRole() {
|
|
415
|
+
return normalizeOrgRole(getActiveAccount()?.role);
|
|
126
416
|
}
|
|
127
|
-
function
|
|
128
|
-
|
|
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();
|
|
@@ -145,11 +438,11 @@ function getConfigPath() {
|
|
|
145
438
|
|
|
146
439
|
// src/ui/execute-command.ts
|
|
147
440
|
import { spawn } from "child_process";
|
|
148
|
-
import { dirname, resolve } from "path";
|
|
441
|
+
import { dirname as dirname2, resolve } from "path";
|
|
149
442
|
import { fileURLToPath } from "url";
|
|
150
443
|
async function executeCommand(argv) {
|
|
151
444
|
const scriptPath = resolve(
|
|
152
|
-
|
|
445
|
+
dirname2(fileURLToPath(import.meta.url)),
|
|
153
446
|
"index.js"
|
|
154
447
|
);
|
|
155
448
|
if (process.stdin.isTTY && process.stdin.isRaw) {
|
|
@@ -176,7 +469,7 @@ import { jsx } from "react/jsx-runtime";
|
|
|
176
469
|
async function openTUI() {
|
|
177
470
|
const [{ render }, { CLIApp }, { PressAnyKey }] = await Promise.all([
|
|
178
471
|
import("ink"),
|
|
179
|
-
import("./app-
|
|
472
|
+
import("./app-UT26PMDR.js"),
|
|
180
473
|
import("./press-any-key-64XFP4O2.js")
|
|
181
474
|
]);
|
|
182
475
|
while (true) {
|
|
@@ -314,43 +607,38 @@ function blank() {
|
|
|
314
607
|
console.log();
|
|
315
608
|
}
|
|
316
609
|
function formatRole(role) {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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);
|
|
320
616
|
case "team_lead":
|
|
321
|
-
return chalk.blue(
|
|
322
|
-
case "
|
|
323
|
-
return chalk.yellow(
|
|
324
|
-
default:
|
|
325
|
-
return chalk.dim("Unknown");
|
|
617
|
+
return chalk.blue(label);
|
|
618
|
+
case "developer":
|
|
619
|
+
return chalk.yellow(label);
|
|
326
620
|
}
|
|
327
621
|
}
|
|
328
622
|
function roleNotice(role) {
|
|
329
|
-
if (role === "
|
|
623
|
+
if (normalizeOrgRole(role) === "developer") {
|
|
330
624
|
console.log(
|
|
331
625
|
chalk.yellow(
|
|
332
|
-
" You have
|
|
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."
|
|
333
627
|
)
|
|
334
628
|
);
|
|
335
629
|
}
|
|
336
630
|
}
|
|
337
631
|
function formatProjectRole(role) {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
return chalk.green("Manager");
|
|
341
|
-
case "developer":
|
|
342
|
-
return chalk.blue("Developer");
|
|
343
|
-
case "viewer":
|
|
344
|
-
return chalk.yellow("Viewer");
|
|
345
|
-
default:
|
|
346
|
-
return chalk.dim("-");
|
|
632
|
+
if (role === void 0 || role === null || role === "") {
|
|
633
|
+
return chalk.dim("-");
|
|
347
634
|
}
|
|
635
|
+
return formatRole(role);
|
|
348
636
|
}
|
|
349
|
-
function projectRoleNotice(
|
|
350
|
-
if (
|
|
637
|
+
function projectRoleNotice(role) {
|
|
638
|
+
if (normalizeOrgRole(role) === "developer") {
|
|
351
639
|
console.log(
|
|
352
640
|
chalk.yellow(
|
|
353
|
-
" You have
|
|
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."
|
|
354
642
|
)
|
|
355
643
|
);
|
|
356
644
|
}
|
|
@@ -470,6 +758,9 @@ var APIError = class extends Error {
|
|
|
470
758
|
var APIClient = class {
|
|
471
759
|
baseUrl;
|
|
472
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;
|
|
473
764
|
constructor(options) {
|
|
474
765
|
this.baseUrl = options?.baseUrl ?? getApiUrl();
|
|
475
766
|
this.accessToken = options?.accessToken ?? getAccessToken();
|
|
@@ -548,70 +839,138 @@ var APIClient = class {
|
|
|
548
839
|
"TOO_MANY_REDIRECTS"
|
|
549
840
|
);
|
|
550
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
|
+
}
|
|
551
900
|
/**
|
|
552
901
|
* Make a GET request
|
|
553
902
|
*/
|
|
554
903
|
async get(path, params) {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
+
}
|
|
559
910
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
911
|
+
const response = await this.fetchWithSafeRedirects(url.toString(), {
|
|
912
|
+
method: "GET",
|
|
913
|
+
headers: this.getHeaders()
|
|
914
|
+
});
|
|
915
|
+
return this.handleResponse(response);
|
|
564
916
|
});
|
|
565
|
-
return this.handleResponse(response);
|
|
566
917
|
}
|
|
567
918
|
/**
|
|
568
919
|
* Make a POST request
|
|
569
920
|
*/
|
|
570
921
|
async post(path, body) {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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);
|
|
576
930
|
});
|
|
577
|
-
return this.handleResponse(response);
|
|
578
931
|
}
|
|
579
932
|
/**
|
|
580
933
|
* Make a PUT request
|
|
581
934
|
*/
|
|
582
935
|
async put(path, body) {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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);
|
|
588
944
|
});
|
|
589
|
-
return this.handleResponse(response);
|
|
590
945
|
}
|
|
591
946
|
/**
|
|
592
947
|
* Make a PATCH request
|
|
593
948
|
*/
|
|
594
949
|
async patch(path, body) {
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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);
|
|
600
958
|
});
|
|
601
|
-
return this.handleResponse(response);
|
|
602
959
|
}
|
|
603
960
|
/**
|
|
604
961
|
* Make a DELETE request
|
|
605
962
|
*/
|
|
606
963
|
async delete(path) {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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
|
+
}
|
|
611
973
|
});
|
|
612
|
-
if (!response.ok) {
|
|
613
|
-
await this.handleError(response);
|
|
614
|
-
}
|
|
615
974
|
}
|
|
616
975
|
/**
|
|
617
976
|
* Handle API response
|
|
@@ -652,18 +1011,25 @@ var APIClient = class {
|
|
|
652
1011
|
* Handle API errors
|
|
653
1012
|
*/
|
|
654
1013
|
async handleError(response) {
|
|
1014
|
+
const bodyText = await response.text();
|
|
655
1015
|
let message = `Request failed with status ${response.status}`;
|
|
656
1016
|
let code;
|
|
657
1017
|
try {
|
|
658
|
-
const data =
|
|
1018
|
+
const data = JSON.parse(bodyText);
|
|
659
1019
|
message = data.error || data.message || message;
|
|
660
1020
|
code = data.code;
|
|
661
1021
|
} catch {
|
|
662
1022
|
}
|
|
663
1023
|
if (response.status === 401) {
|
|
664
|
-
|
|
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
|
+
}
|
|
665
1031
|
throw new APIError(
|
|
666
|
-
"Authentication failed. Please run `envpilot login`.",
|
|
1032
|
+
message || "Authentication failed. Please run `envpilot login`.",
|
|
667
1033
|
401,
|
|
668
1034
|
code || "UNAUTHORIZED"
|
|
669
1035
|
);
|
|
@@ -803,6 +1169,29 @@ var APIClient = class {
|
|
|
803
1169
|
async bulkUpsertVariables(data) {
|
|
804
1170
|
return this.post("/api/cli/variables/bulk", data);
|
|
805
1171
|
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Submit a variable request (developers only — owners/PMs/team leads
|
|
1174
|
+
* create variables directly and get a 403 from the server here).
|
|
1175
|
+
*/
|
|
1176
|
+
async createVariableRequest(data) {
|
|
1177
|
+
const response = await this.post(
|
|
1178
|
+
"/api/cli/variable-requests",
|
|
1179
|
+
data
|
|
1180
|
+
);
|
|
1181
|
+
if (!response.data) {
|
|
1182
|
+
throw new APIError("No variable request returned by server", 500);
|
|
1183
|
+
}
|
|
1184
|
+
return response.data.request;
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* List variable requests for a project, optionally filtered by status.
|
|
1188
|
+
*/
|
|
1189
|
+
async listVariableRequests(projectId, status) {
|
|
1190
|
+
const params = { projectId };
|
|
1191
|
+
if (status) params.status = status;
|
|
1192
|
+
const response = await this.get("/api/cli/variable-requests", params);
|
|
1193
|
+
return response.data?.requests ?? [];
|
|
1194
|
+
}
|
|
806
1195
|
// ============================================
|
|
807
1196
|
// Authentication methods
|
|
808
1197
|
// ============================================
|
|
@@ -894,15 +1283,19 @@ async function performLogin(options) {
|
|
|
894
1283
|
"Authentication succeeded but the server did not return session tokens. This is likely a server-side issue. Please try again or contact support."
|
|
895
1284
|
);
|
|
896
1285
|
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
1286
|
+
const accountId = pollResponse.user?.id ?? `session-${pollResponse.accessToken.slice(0, 8)}`;
|
|
1287
|
+
const account = {
|
|
1288
|
+
id: accountId,
|
|
1289
|
+
user: pollResponse.user ? {
|
|
901
1290
|
id: pollResponse.user.id,
|
|
902
1291
|
email: pollResponse.user.email,
|
|
903
1292
|
name: pollResponse.user.name
|
|
904
|
-
}
|
|
905
|
-
|
|
1293
|
+
} : { id: accountId, email: `${accountId}@cli.local` },
|
|
1294
|
+
accessToken: pollResponse.accessToken,
|
|
1295
|
+
refreshToken: pollResponse.refreshToken
|
|
1296
|
+
};
|
|
1297
|
+
upsertAccount(account);
|
|
1298
|
+
setActiveAccount(account.id);
|
|
906
1299
|
console.log();
|
|
907
1300
|
success(`Logged in as ${chalk3.bold(pollResponse.user?.email)}`);
|
|
908
1301
|
return { email: pollResponse.user?.email || "" };
|
|
@@ -926,6 +1319,16 @@ var loginCommand = new Command("login").description("Authenticate with Envpilot"
|
|
|
926
1319
|
await performLogin({
|
|
927
1320
|
browser: options.browser !== false
|
|
928
1321
|
});
|
|
1322
|
+
const activeAccount = getActiveAccount();
|
|
1323
|
+
if (activeAccount) {
|
|
1324
|
+
success(`Signed in as ${activeAccount.user.email}.`);
|
|
1325
|
+
}
|
|
1326
|
+
const accounts = listAccounts();
|
|
1327
|
+
if (accounts.length > 1) {
|
|
1328
|
+
info(
|
|
1329
|
+
`You have ${accounts.length} accounts \u2014 use \`envpilot accounts\` to list/switch.`
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
929
1332
|
console.log();
|
|
930
1333
|
console.log("Next steps:");
|
|
931
1334
|
info(" envpilot init Initialize a project in the current directory");
|
|
@@ -943,9 +1346,15 @@ import chalk4 from "chalk";
|
|
|
943
1346
|
import inquirer from "inquirer";
|
|
944
1347
|
|
|
945
1348
|
// src/lib/project-config.ts
|
|
946
|
-
import {
|
|
1349
|
+
import {
|
|
1350
|
+
readFileSync as readFileSync2,
|
|
1351
|
+
writeFileSync as writeFileSync2,
|
|
1352
|
+
existsSync as existsSync2,
|
|
1353
|
+
unlinkSync as unlinkSync2,
|
|
1354
|
+
renameSync
|
|
1355
|
+
} from "fs";
|
|
947
1356
|
import { execSync } from "child_process";
|
|
948
|
-
import { join } from "path";
|
|
1357
|
+
import { join as join2 } from "path";
|
|
949
1358
|
|
|
950
1359
|
// src/types/index.ts
|
|
951
1360
|
import { z } from "zod";
|
|
@@ -954,12 +1363,25 @@ var userSchema = z.object({
|
|
|
954
1363
|
email: z.string().email(),
|
|
955
1364
|
name: z.string().optional()
|
|
956
1365
|
});
|
|
1366
|
+
var accountSchema = z.object({
|
|
1367
|
+
id: z.string(),
|
|
1368
|
+
user: userSchema,
|
|
1369
|
+
accessToken: z.string(),
|
|
1370
|
+
refreshToken: z.string().optional(),
|
|
1371
|
+
// Stored role string (legacy or unified); normalized on read.
|
|
1372
|
+
role: z.string().optional(),
|
|
1373
|
+
// Per-account active org/project selection.
|
|
1374
|
+
activeOrganizationId: z.string().optional(),
|
|
1375
|
+
activeProjectId: z.string().optional()
|
|
1376
|
+
});
|
|
957
1377
|
var organizationSchema = z.object({
|
|
958
1378
|
_id: z.string(),
|
|
959
1379
|
name: z.string(),
|
|
960
1380
|
slug: z.string(),
|
|
961
1381
|
tier: z.enum(["free", "pro"]),
|
|
962
|
-
role: z.string().optional()
|
|
1382
|
+
role: z.string().optional(),
|
|
1383
|
+
// Unified org role (additive; legacy responses omit it and fall back to role).
|
|
1384
|
+
unifiedRole: z.string().nullable().optional()
|
|
963
1385
|
});
|
|
964
1386
|
var projectSchema = z.object({
|
|
965
1387
|
_id: z.string(),
|
|
@@ -970,7 +1392,11 @@ var projectSchema = z.object({
|
|
|
970
1392
|
icon: z.string().optional(),
|
|
971
1393
|
color: z.string().optional(),
|
|
972
1394
|
userRole: z.string().nullable().optional(),
|
|
973
|
-
projectRole: z.string().nullable().optional()
|
|
1395
|
+
projectRole: z.string().nullable().optional(),
|
|
1396
|
+
// Unified-role fields (optional so legacy server responses still parse)
|
|
1397
|
+
unifiedRole: z.string().nullable().optional(),
|
|
1398
|
+
assigned: z.boolean().optional(),
|
|
1399
|
+
environmentScope: z.array(z.string()).nullable().optional()
|
|
974
1400
|
});
|
|
975
1401
|
var variableTagSchema = z.object({
|
|
976
1402
|
_id: z.string(),
|
|
@@ -988,21 +1414,49 @@ var variableSchema = z.object({
|
|
|
988
1414
|
version: z.number().optional(),
|
|
989
1415
|
updatedAt: z.number().optional(),
|
|
990
1416
|
createdAt: z.number().optional(),
|
|
991
|
-
tags: z.array(variableTagSchema).optional()
|
|
1417
|
+
tags: z.array(variableTagSchema).optional(),
|
|
1418
|
+
// Per-variable effective access under the unified model. Optional so older
|
|
1419
|
+
// server responses (which omit it) still parse.
|
|
1420
|
+
access: z.enum(["read", "write"]).optional()
|
|
992
1421
|
});
|
|
1422
|
+
var variablesMetaSchema = z.object({
|
|
1423
|
+
total: z.number().optional(),
|
|
1424
|
+
page: z.number().optional(),
|
|
1425
|
+
limit: z.number().optional(),
|
|
1426
|
+
decryptionFailures: z.array(z.string()).optional(),
|
|
1427
|
+
unifiedRole: z.string().nullable().optional(),
|
|
1428
|
+
assigned: z.boolean().optional(),
|
|
1429
|
+
grantOnly: z.boolean().optional(),
|
|
1430
|
+
environmentScope: z.array(z.string()).nullable().optional(),
|
|
1431
|
+
hasWriteAccess: z.boolean().optional(),
|
|
1432
|
+
scopeRestricted: z.boolean().optional()
|
|
1433
|
+
}).passthrough();
|
|
993
1434
|
var environmentSchema = z.enum([
|
|
994
1435
|
"development",
|
|
995
1436
|
"staging",
|
|
996
1437
|
"production"
|
|
997
1438
|
]);
|
|
998
1439
|
var cliConfigSchema = z.object({
|
|
1440
|
+
// Global CLI setting — shared across all accounts.
|
|
999
1441
|
apiUrl: z.string().url(),
|
|
1442
|
+
// Multi-account store. Keyed by account id (== user id).
|
|
1443
|
+
accounts: z.record(z.string(), accountSchema).optional(),
|
|
1444
|
+
// The currently active account id.
|
|
1445
|
+
activeAccountId: z.string().optional(),
|
|
1446
|
+
// --- Legacy single-account fields ---
|
|
1447
|
+
// Retained ONLY so pre-multi-account configs still parse and can be migrated
|
|
1448
|
+
// into `accounts` by migrateLegacyConfig(). After migration these are deleted
|
|
1449
|
+
// so `accounts` is the single source of truth. Do not read/write directly.
|
|
1000
1450
|
accessToken: z.string().optional(),
|
|
1001
1451
|
refreshToken: z.string().optional(),
|
|
1002
1452
|
activeProjectId: z.string().optional(),
|
|
1003
1453
|
activeOrganizationId: z.string().optional(),
|
|
1004
1454
|
user: userSchema.optional(),
|
|
1005
|
-
role
|
|
1455
|
+
// Stored role string. Kept as a free-form string so both legacy
|
|
1456
|
+
// ("admin"/"team_lead"/"member") and unified
|
|
1457
|
+
// ("owner"/"project_manager"/"team_lead"/"developer") values round-trip.
|
|
1458
|
+
// Normalized on read via getUnifiedRole(); no longer enum-enforced.
|
|
1459
|
+
role: z.string().optional()
|
|
1006
1460
|
});
|
|
1007
1461
|
var projectConfigSchema = z.object({
|
|
1008
1462
|
projectId: z.string(),
|
|
@@ -1014,27 +1468,71 @@ var projectEntrySchema = z.object({
|
|
|
1014
1468
|
organizationId: z.string(),
|
|
1015
1469
|
projectName: z.string().default(""),
|
|
1016
1470
|
organizationName: z.string().default(""),
|
|
1017
|
-
environment: environmentSchema.default("development")
|
|
1471
|
+
environment: environmentSchema.default("development"),
|
|
1472
|
+
// How the pulled .env file's permissions are managed for this project:
|
|
1473
|
+
// auto → derive from the caller's resolved access (default)
|
|
1474
|
+
// always → force read-only (0o400)
|
|
1475
|
+
// never → force writable (0o600)
|
|
1476
|
+
fileProtection: z.enum(["auto", "always", "never"]).optional()
|
|
1018
1477
|
});
|
|
1019
1478
|
var projectConfigV2Schema = z.object({
|
|
1020
1479
|
version: z.literal(1),
|
|
1021
1480
|
activeProjectId: z.string(),
|
|
1022
1481
|
projects: z.array(projectEntrySchema).min(1)
|
|
1023
1482
|
});
|
|
1483
|
+
var variableRequestStatusSchema = z.enum([
|
|
1484
|
+
"pending",
|
|
1485
|
+
"approved",
|
|
1486
|
+
"rejected",
|
|
1487
|
+
"canceled"
|
|
1488
|
+
]);
|
|
1489
|
+
var variableRequestUserSchema = z.object({
|
|
1490
|
+
_id: z.string(),
|
|
1491
|
+
email: z.string().optional(),
|
|
1492
|
+
name: z.string().optional()
|
|
1493
|
+
}).nullable().optional();
|
|
1494
|
+
var variableRequestSchema = z.object({
|
|
1495
|
+
_id: z.string(),
|
|
1496
|
+
key: z.string(),
|
|
1497
|
+
description: z.string().optional(),
|
|
1498
|
+
environments: z.array(z.string()),
|
|
1499
|
+
projectId: z.string(),
|
|
1500
|
+
organizationId: z.string().optional(),
|
|
1501
|
+
isSensitive: z.boolean().optional(),
|
|
1502
|
+
status: variableRequestStatusSchema,
|
|
1503
|
+
reviewReason: z.string().optional(),
|
|
1504
|
+
createdAt: z.number(),
|
|
1505
|
+
updatedAt: z.number().optional(),
|
|
1506
|
+
requester: variableRequestUserSchema,
|
|
1507
|
+
reviewer: variableRequestUserSchema
|
|
1508
|
+
});
|
|
1024
1509
|
|
|
1025
1510
|
// src/lib/project-config.ts
|
|
1026
1511
|
var CONFIG_FILE_NAME = ".envpilot";
|
|
1512
|
+
function atomicWriteFileSync(filePath, content) {
|
|
1513
|
+
const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
1514
|
+
try {
|
|
1515
|
+
writeFileSync2(tmpPath, content, "utf-8");
|
|
1516
|
+
renameSync(tmpPath, filePath);
|
|
1517
|
+
} catch (err) {
|
|
1518
|
+
try {
|
|
1519
|
+
if (existsSync2(tmpPath)) unlinkSync2(tmpPath);
|
|
1520
|
+
} catch {
|
|
1521
|
+
}
|
|
1522
|
+
throw err;
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1027
1525
|
function getProjectConfigPath(directory = process.cwd()) {
|
|
1028
|
-
return
|
|
1526
|
+
return join2(directory, CONFIG_FILE_NAME);
|
|
1029
1527
|
}
|
|
1030
1528
|
function hasProjectConfig(directory = process.cwd()) {
|
|
1031
|
-
return
|
|
1529
|
+
return existsSync2(getProjectConfigPath(directory));
|
|
1032
1530
|
}
|
|
1033
1531
|
function readRawConfig(directory = process.cwd()) {
|
|
1034
1532
|
const configPath = getProjectConfigPath(directory);
|
|
1035
|
-
if (!
|
|
1533
|
+
if (!existsSync2(configPath)) return null;
|
|
1036
1534
|
try {
|
|
1037
|
-
return JSON.parse(
|
|
1535
|
+
return JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
1038
1536
|
} catch {
|
|
1039
1537
|
return null;
|
|
1040
1538
|
}
|
|
@@ -1081,7 +1579,7 @@ function readProjectConfigV2(directory = process.cwd()) {
|
|
|
1081
1579
|
}
|
|
1082
1580
|
function writeProjectConfigV2(config2, directory = process.cwd()) {
|
|
1083
1581
|
const configPath = getProjectConfigPath(directory);
|
|
1084
|
-
|
|
1582
|
+
atomicWriteFileSync(configPath, JSON.stringify(config2, null, 2) + "\n");
|
|
1085
1583
|
}
|
|
1086
1584
|
function getActiveProject(config2) {
|
|
1087
1585
|
return config2.projects.find((p) => p.projectId === config2.activeProjectId) || config2.projects[0] || null;
|
|
@@ -1174,23 +1672,23 @@ function updateProjectConfig(updates, directory = process.cwd()) {
|
|
|
1174
1672
|
}
|
|
1175
1673
|
function deleteProjectConfig(directory = process.cwd()) {
|
|
1176
1674
|
const configPath = getProjectConfigPath(directory);
|
|
1177
|
-
if (!
|
|
1178
|
-
|
|
1675
|
+
if (!existsSync2(configPath)) return false;
|
|
1676
|
+
unlinkSync2(configPath);
|
|
1179
1677
|
return true;
|
|
1180
1678
|
}
|
|
1181
1679
|
function ensureEnvInGitignore(directory = process.cwd()) {
|
|
1182
|
-
const gitignorePath =
|
|
1183
|
-
if (!
|
|
1184
|
-
|
|
1680
|
+
const gitignorePath = join2(directory, ".gitignore");
|
|
1681
|
+
if (!existsSync2(gitignorePath)) {
|
|
1682
|
+
atomicWriteFileSync(gitignorePath, ".env\n.env.local\n");
|
|
1185
1683
|
return;
|
|
1186
1684
|
}
|
|
1187
|
-
const content =
|
|
1685
|
+
const content = readFileSync2(gitignorePath, "utf-8");
|
|
1188
1686
|
const lines = content.split("\n");
|
|
1189
1687
|
if (lines.some((line2) => line2.trim() === ".env")) {
|
|
1190
1688
|
return;
|
|
1191
1689
|
}
|
|
1192
1690
|
const newContent = content.endsWith("\n") ? content + ".env\n" : content + "\n.env\n";
|
|
1193
|
-
|
|
1691
|
+
atomicWriteFileSync(gitignorePath, newContent);
|
|
1194
1692
|
}
|
|
1195
1693
|
function getTrackedEnvFiles(directory = process.cwd()) {
|
|
1196
1694
|
try {
|
|
@@ -1206,8 +1704,14 @@ function getTrackedEnvFiles(directory = process.cwd()) {
|
|
|
1206
1704
|
}
|
|
1207
1705
|
|
|
1208
1706
|
// src/lib/env-file.ts
|
|
1209
|
-
import {
|
|
1210
|
-
|
|
1707
|
+
import {
|
|
1708
|
+
readFileSync as readFileSync3,
|
|
1709
|
+
writeFileSync as writeFileSync3,
|
|
1710
|
+
existsSync as existsSync3,
|
|
1711
|
+
chmodSync,
|
|
1712
|
+
renameSync as renameSync2
|
|
1713
|
+
} from "fs";
|
|
1714
|
+
import { join as join3 } from "path";
|
|
1211
1715
|
function parseEnvFile(content) {
|
|
1212
1716
|
const result = {};
|
|
1213
1717
|
const lines = content.split("\n");
|
|
@@ -1220,7 +1724,7 @@ function parseEnvFile(content) {
|
|
|
1220
1724
|
if (equalsIndex === -1) {
|
|
1221
1725
|
continue;
|
|
1222
1726
|
}
|
|
1223
|
-
const key = line2.substring(0, equalsIndex).trim();
|
|
1727
|
+
const key = line2.substring(0, equalsIndex).trim().replace(/^export\s+/, "");
|
|
1224
1728
|
let value = line2.substring(equalsIndex + 1);
|
|
1225
1729
|
value = parseValue(value);
|
|
1226
1730
|
if (isValidEnvKey(key)) {
|
|
@@ -1231,17 +1735,21 @@ function parseEnvFile(content) {
|
|
|
1231
1735
|
}
|
|
1232
1736
|
function parseValue(value) {
|
|
1233
1737
|
value = value.trim();
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1738
|
+
const quote = value[0];
|
|
1739
|
+
if (quote === '"' || quote === "'") {
|
|
1740
|
+
const closingIndex = value.indexOf(quote, 1);
|
|
1741
|
+
if (closingIndex !== -1) {
|
|
1742
|
+
const inner = value.slice(1, closingIndex);
|
|
1743
|
+
if (quote === '"') {
|
|
1744
|
+
return inner.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
1745
|
+
}
|
|
1746
|
+
return inner;
|
|
1243
1747
|
}
|
|
1244
1748
|
}
|
|
1749
|
+
const commentIndex = value.indexOf(" #");
|
|
1750
|
+
if (commentIndex !== -1) {
|
|
1751
|
+
value = value.substring(0, commentIndex).trim();
|
|
1752
|
+
}
|
|
1245
1753
|
return value;
|
|
1246
1754
|
}
|
|
1247
1755
|
function isValidEnvKey(key) {
|
|
@@ -1293,33 +1801,53 @@ function diffEnvVars(local, remote) {
|
|
|
1293
1801
|
return { added, removed, changed, unchanged };
|
|
1294
1802
|
}
|
|
1295
1803
|
function readEnvFile(filePath) {
|
|
1296
|
-
if (!
|
|
1804
|
+
if (!existsSync3(filePath)) {
|
|
1297
1805
|
return null;
|
|
1298
1806
|
}
|
|
1299
|
-
const content =
|
|
1807
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
1300
1808
|
return parseEnvFile(content);
|
|
1301
1809
|
}
|
|
1810
|
+
function atomicWriteFileSync2(filePath, content, mode) {
|
|
1811
|
+
const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
1812
|
+
try {
|
|
1813
|
+
writeFileSync3(tmpPath, content, { encoding: "utf-8", mode });
|
|
1814
|
+
renameSync2(tmpPath, filePath);
|
|
1815
|
+
} catch (err) {
|
|
1816
|
+
try {
|
|
1817
|
+
if (existsSync3(tmpPath)) chmodSync(tmpPath, 384);
|
|
1818
|
+
} catch {
|
|
1819
|
+
}
|
|
1820
|
+
throw err;
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1302
1823
|
function writeEnvFile(filePath, vars, options) {
|
|
1303
1824
|
const content = stringifyEnv(vars, options);
|
|
1304
|
-
|
|
1825
|
+
atomicWriteFileSync2(filePath, content, 384);
|
|
1305
1826
|
}
|
|
1306
1827
|
function getEnvPathForEnvironment(environment, directory = process.cwd()) {
|
|
1307
1828
|
if (environment === "development") {
|
|
1308
|
-
return
|
|
1829
|
+
return join3(directory, ".env.local");
|
|
1309
1830
|
}
|
|
1310
|
-
return
|
|
1831
|
+
return join3(directory, `.env.${environment}`);
|
|
1311
1832
|
}
|
|
1312
|
-
function applyFileProtection(filePath,
|
|
1313
|
-
if (!
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
} else {
|
|
1318
|
-
chmodSync(filePath, 292);
|
|
1833
|
+
function applyFileProtection(filePath, access, protection = "auto") {
|
|
1834
|
+
if (!existsSync3(filePath)) return;
|
|
1835
|
+
if (protection === "never") {
|
|
1836
|
+
chmodSync(filePath, 384);
|
|
1837
|
+
return;
|
|
1319
1838
|
}
|
|
1839
|
+
if (protection === "always") {
|
|
1840
|
+
chmodSync(filePath, 256);
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
chmodSync(filePath, isFileWritable(access) ? 384 : 256);
|
|
1320
1844
|
}
|
|
1321
1845
|
|
|
1322
1846
|
// src/commands/init.ts
|
|
1847
|
+
function orgUnifiedRole(org) {
|
|
1848
|
+
const unified = org.unifiedRole;
|
|
1849
|
+
return normalizeOrgRole(unified ?? org.role);
|
|
1850
|
+
}
|
|
1323
1851
|
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(
|
|
1324
1852
|
"-e, --environment <env>",
|
|
1325
1853
|
"Default environment (development, staging, production)"
|
|
@@ -1368,9 +1896,7 @@ var initCommand = new Command2("init").description("Initialize Envpilot in the c
|
|
|
1368
1896
|
});
|
|
1369
1897
|
setActiveOrganizationId(selectedOrg._id);
|
|
1370
1898
|
setActiveProjectId(selectedProject._id);
|
|
1371
|
-
|
|
1372
|
-
setRole(selectedOrg.role);
|
|
1373
|
-
}
|
|
1899
|
+
setUnifiedRole(orgUnifiedRole(selectedOrg));
|
|
1374
1900
|
ensureEnvInGitignore();
|
|
1375
1901
|
warnTrackedFiles();
|
|
1376
1902
|
console.log();
|
|
@@ -1382,21 +1908,23 @@ var initCommand = new Command2("init").description("Initialize Envpilot in the c
|
|
|
1382
1908
|
});
|
|
1383
1909
|
async function addProject(existingConfig, options) {
|
|
1384
1910
|
const api = createAPIClient();
|
|
1385
|
-
let role =
|
|
1386
|
-
if (role
|
|
1911
|
+
let role = getUnifiedRole();
|
|
1912
|
+
if (roleLevel(role) < ROLE_LEVEL.team_lead) {
|
|
1387
1913
|
const orgs = await withSpinner("Checking permissions...", async () => {
|
|
1388
1914
|
const response = await api.get("/api/cli/organizations");
|
|
1389
1915
|
return response.data || [];
|
|
1390
1916
|
});
|
|
1391
|
-
const
|
|
1917
|
+
const freshOrg = orgs.find(
|
|
1392
1918
|
(o) => o._id === existingConfig.projects[0]?.organizationId
|
|
1393
|
-
)
|
|
1394
|
-
if (
|
|
1395
|
-
|
|
1396
|
-
role
|
|
1919
|
+
);
|
|
1920
|
+
if (freshOrg) {
|
|
1921
|
+
role = orgUnifiedRole(freshOrg);
|
|
1922
|
+
setUnifiedRole(role);
|
|
1397
1923
|
}
|
|
1398
|
-
if (role
|
|
1399
|
-
error(
|
|
1924
|
+
if (roleLevel(role) < ROLE_LEVEL.team_lead) {
|
|
1925
|
+
error(
|
|
1926
|
+
`Only team leads and above can link multiple projects (your role: ${formatRoleLabel(role)}).`
|
|
1927
|
+
);
|
|
1400
1928
|
info("Unlink the current project first with `envpilot unlink`.");
|
|
1401
1929
|
process.exit(1);
|
|
1402
1930
|
}
|
|
@@ -1589,17 +2117,12 @@ function warnTrackedFiles() {
|
|
|
1589
2117
|
function printPostInit(selectedOrg, selectedProject) {
|
|
1590
2118
|
console.log();
|
|
1591
2119
|
console.log(chalk4.dim("Configuration saved to .envpilot"));
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
if (
|
|
1597
|
-
console.log(
|
|
1598
|
-
chalk4.dim(
|
|
1599
|
-
` Project role: ${formatProjectRole(selectedProject.projectRole)}`
|
|
1600
|
-
)
|
|
1601
|
-
);
|
|
1602
|
-
projectRoleNotice(selectedProject.projectRole);
|
|
2120
|
+
console.log(
|
|
2121
|
+
chalk4.dim(` Org role: ${formatRoleLabel(orgUnifiedRole(selectedOrg))}`)
|
|
2122
|
+
);
|
|
2123
|
+
const scope = selectedProject.environmentScope;
|
|
2124
|
+
if (scope && scope.length > 0) {
|
|
2125
|
+
console.log(chalk4.dim(` Environment scope: ${scope.join(", ")}`));
|
|
1603
2126
|
}
|
|
1604
2127
|
console.log();
|
|
1605
2128
|
console.log("Next steps:");
|
|
@@ -1932,7 +2455,7 @@ function parseNetlify(content) {
|
|
|
1932
2455
|
for (const line2 of lines) {
|
|
1933
2456
|
const trimmed = line2.trim();
|
|
1934
2457
|
if (trimmed.startsWith("[")) {
|
|
1935
|
-
inBuildEnv = trimmed === "[build.environment]" ||
|
|
2458
|
+
inBuildEnv = trimmed === "[build.environment]" || /^\[context\.[^.\]]+\.environment\]$/.test(trimmed);
|
|
1936
2459
|
continue;
|
|
1937
2460
|
}
|
|
1938
2461
|
if (!inBuildEnv) continue;
|
|
@@ -1955,6 +2478,17 @@ function parseNetlify(content) {
|
|
|
1955
2478
|
}
|
|
1956
2479
|
|
|
1957
2480
|
// src/commands/pull.ts
|
|
2481
|
+
function accessFromMeta(meta, variables) {
|
|
2482
|
+
const role = normalizeOrgRole(meta?.unifiedRole ?? meta?.role);
|
|
2483
|
+
return {
|
|
2484
|
+
role,
|
|
2485
|
+
// Owners are implicitly assigned to every project. The legacy server sends
|
|
2486
|
+
// no `assigned`/`projectRole` for owners, so fall back to true for them.
|
|
2487
|
+
assigned: role === "owner" || (meta?.assigned ?? (meta?.projectRole !== null && meta?.projectRole !== void 0)),
|
|
2488
|
+
environmentScope: meta?.environmentScope ?? null,
|
|
2489
|
+
hasWriteAccess: meta?.hasWriteAccess ?? variables.some((v) => v.access === "write")
|
|
2490
|
+
};
|
|
2491
|
+
}
|
|
1958
2492
|
var pullCommand = new Command3("pull").description("Download environment variables to local .env file").option(
|
|
1959
2493
|
"-e, --env <environment>",
|
|
1960
2494
|
"Environment (development, staging, production)"
|
|
@@ -1997,6 +2531,8 @@ var pullCommand = new Command3("pull").description("Download environment variabl
|
|
|
1997
2531
|
const projectConfig = readProjectConfig();
|
|
1998
2532
|
if (!projectConfig) throw notInitialized();
|
|
1999
2533
|
checkTrackedFiles();
|
|
2534
|
+
const configV2ForActive = readProjectConfigV2();
|
|
2535
|
+
const activeEntry = configV2ForActive ? getActiveProject(configV2ForActive) : null;
|
|
2000
2536
|
const environment = options.env || projectConfig.environment || "development";
|
|
2001
2537
|
const fmt = options.format || "env";
|
|
2002
2538
|
if (!ALL_FORMATS.includes(fmt)) {
|
|
@@ -2008,7 +2544,8 @@ var pullCommand = new Command3("pull").description("Download environment variabl
|
|
|
2008
2544
|
{
|
|
2009
2545
|
projectId: projectConfig.projectId,
|
|
2010
2546
|
organizationId: projectConfig.organizationId,
|
|
2011
|
-
environment
|
|
2547
|
+
environment,
|
|
2548
|
+
fileProtection: activeEntry?.fileProtection
|
|
2012
2549
|
},
|
|
2013
2550
|
outputPath,
|
|
2014
2551
|
options
|
|
@@ -2037,7 +2574,8 @@ async function pullAllProjects(options) {
|
|
|
2037
2574
|
{
|
|
2038
2575
|
projectId: project.projectId,
|
|
2039
2576
|
organizationId: project.organizationId,
|
|
2040
|
-
environment: project.environment
|
|
2577
|
+
environment: project.environment,
|
|
2578
|
+
fileProtection: project.fileProtection
|
|
2041
2579
|
},
|
|
2042
2580
|
outputPath,
|
|
2043
2581
|
options
|
|
@@ -2068,7 +2606,8 @@ async function pullSingleProject(project, options) {
|
|
|
2068
2606
|
{
|
|
2069
2607
|
projectId: project.projectId,
|
|
2070
2608
|
organizationId: project.organizationId,
|
|
2071
|
-
environment
|
|
2609
|
+
environment,
|
|
2610
|
+
fileProtection: project.fileProtection
|
|
2072
2611
|
},
|
|
2073
2612
|
outputPath,
|
|
2074
2613
|
options
|
|
@@ -2076,7 +2615,7 @@ async function pullSingleProject(project, options) {
|
|
|
2076
2615
|
}
|
|
2077
2616
|
async function pullProject(project, outputPath, options) {
|
|
2078
2617
|
const api = createAPIClient();
|
|
2079
|
-
let
|
|
2618
|
+
let meta;
|
|
2080
2619
|
const variables = await withSpinner(
|
|
2081
2620
|
`Fetching ${chalk5.bold(project.environment)} variables...`,
|
|
2082
2621
|
async () => {
|
|
@@ -2087,10 +2626,15 @@ async function pullProject(project, outputPath, options) {
|
|
|
2087
2626
|
organizationId: project.organizationId
|
|
2088
2627
|
}
|
|
2089
2628
|
});
|
|
2090
|
-
|
|
2629
|
+
meta = response.meta;
|
|
2091
2630
|
return response.data || [];
|
|
2092
2631
|
}
|
|
2093
2632
|
);
|
|
2633
|
+
if (meta?.scopeRestricted && meta.environmentScope?.length) {
|
|
2634
|
+
info(
|
|
2635
|
+
`Your access is scoped to ${meta.environmentScope.join(", ")}; variables in other environments are withheld.`
|
|
2636
|
+
);
|
|
2637
|
+
}
|
|
2094
2638
|
if (variables.length === 0) {
|
|
2095
2639
|
warning(`No variables found for ${project.environment} environment.`);
|
|
2096
2640
|
return;
|
|
@@ -2154,20 +2698,21 @@ async function pullProject(project, outputPath, options) {
|
|
|
2154
2698
|
});
|
|
2155
2699
|
fs.writeFileSync(outputPath, output, "utf-8");
|
|
2156
2700
|
}
|
|
2157
|
-
const
|
|
2158
|
-
|
|
2701
|
+
const access = accessFromMeta(meta, variables);
|
|
2702
|
+
const protection = project.fileProtection ?? "auto";
|
|
2703
|
+
applyFileProtection(outputPath, access, protection);
|
|
2159
2704
|
success(
|
|
2160
2705
|
`Downloaded ${variables.length} variables to ${chalk5.bold(outputPath)}`
|
|
2161
2706
|
);
|
|
2162
|
-
if (
|
|
2707
|
+
if (meta?.grantOnly) {
|
|
2163
2708
|
info(
|
|
2164
|
-
"You have
|
|
2709
|
+
"You have grant-only access to this project. You only see variables explicitly shared with you."
|
|
2165
2710
|
);
|
|
2166
2711
|
}
|
|
2167
|
-
const
|
|
2168
|
-
if (
|
|
2712
|
+
const writable = protection === "never" || protection !== "always" && isFileWritable(access);
|
|
2713
|
+
if (!writable) {
|
|
2169
2714
|
info(
|
|
2170
|
-
`File is read-only (your role: ${role
|
|
2715
|
+
`File is read-only (your role: ${formatRoleLabel(access.role)}${access.assigned ? "" : ", not assigned to this project"}).`
|
|
2171
2716
|
);
|
|
2172
2717
|
}
|
|
2173
2718
|
console.log();
|
|
@@ -2306,36 +2851,6 @@ var pushCommand = new Command4("push").description("Upload local .env file to cl
|
|
|
2306
2851
|
process.exit(1);
|
|
2307
2852
|
}
|
|
2308
2853
|
const api = createAPIClient();
|
|
2309
|
-
const projects = await api.get("/api/cli/projects", { organizationId });
|
|
2310
|
-
const currentProject = projects.data?.find((p) => p._id === projectId);
|
|
2311
|
-
const projectRole = currentProject?.projectRole;
|
|
2312
|
-
if (projectRole === "viewer") {
|
|
2313
|
-
error(
|
|
2314
|
-
"Unfortunately, as a member, you cannot push in any environment. Please access or request access to your team lead."
|
|
2315
|
-
);
|
|
2316
|
-
process.exit(1);
|
|
2317
|
-
}
|
|
2318
|
-
const role = getRole();
|
|
2319
|
-
if (role === "member") {
|
|
2320
|
-
warning(
|
|
2321
|
-
"You have Member access. Push will create pending requests that require Admin or Team Lead approval."
|
|
2322
|
-
);
|
|
2323
|
-
console.log();
|
|
2324
|
-
if (!options.force) {
|
|
2325
|
-
const { proceed } = await inquirer3.prompt([
|
|
2326
|
-
{
|
|
2327
|
-
type: "confirm",
|
|
2328
|
-
name: "proceed",
|
|
2329
|
-
message: "Continue with creating approval requests?",
|
|
2330
|
-
default: true
|
|
2331
|
-
}
|
|
2332
|
-
]);
|
|
2333
|
-
if (!proceed) {
|
|
2334
|
-
info("Push cancelled.");
|
|
2335
|
-
return;
|
|
2336
|
-
}
|
|
2337
|
-
}
|
|
2338
|
-
}
|
|
2339
2854
|
const environment = options.env || defaultEnvironment || "development";
|
|
2340
2855
|
const fmt = options.format || "env";
|
|
2341
2856
|
if (!ALL_FORMATS.includes(fmt)) {
|
|
@@ -2375,6 +2890,7 @@ var pushCommand = new Command4("push").description("Upload local .env file to cl
|
|
|
2375
2890
|
error("No valid variables to push.");
|
|
2376
2891
|
return;
|
|
2377
2892
|
}
|
|
2893
|
+
let meta;
|
|
2378
2894
|
const remoteVariables = await withSpinner(
|
|
2379
2895
|
"Fetching current variables...",
|
|
2380
2896
|
async () => {
|
|
@@ -2386,9 +2902,21 @@ var pushCommand = new Command4("push").description("Upload local .env file to cl
|
|
|
2386
2902
|
params.organizationId = organizationId;
|
|
2387
2903
|
}
|
|
2388
2904
|
const response = await api.get("/api/cli/variables", params);
|
|
2905
|
+
meta = response.meta;
|
|
2389
2906
|
return response.data || [];
|
|
2390
2907
|
}
|
|
2391
2908
|
);
|
|
2909
|
+
const scope = meta?.environmentScope;
|
|
2910
|
+
if (Array.isArray(scope) && !scope.includes(environment)) {
|
|
2911
|
+
error(
|
|
2912
|
+
`Your developer access is scoped to ${scope.join(", ")}. ${environment} variables are withheld.`
|
|
2913
|
+
);
|
|
2914
|
+
process.exit(1);
|
|
2915
|
+
}
|
|
2916
|
+
if (meta?.hasWriteAccess === false) {
|
|
2917
|
+
error(`You have read-only access to ${environment} in this project.`);
|
|
2918
|
+
process.exit(1);
|
|
2919
|
+
}
|
|
2392
2920
|
const remoteVars = {};
|
|
2393
2921
|
for (const variable of remoteVariables) {
|
|
2394
2922
|
remoteVars[variable.key] = variable.value;
|
|
@@ -2459,32 +2987,28 @@ var pushCommand = new Command4("push").description("Upload local .env file to cl
|
|
|
2459
2987
|
return response.data;
|
|
2460
2988
|
}
|
|
2461
2989
|
);
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2990
|
+
success(
|
|
2991
|
+
`Pushed ${result?.total || Object.keys(valid).length} variables to ${chalk6.bold(environment)}`
|
|
2992
|
+
);
|
|
2993
|
+
console.log();
|
|
2994
|
+
console.log(chalk6.dim(` Created: ${result?.created || 0}`));
|
|
2995
|
+
console.log(chalk6.dim(` Updated: ${result?.updated || 0}`));
|
|
2996
|
+
if (mode === "replace") {
|
|
2997
|
+
console.log(chalk6.dim(` Deleted: ${result?.deleted || 0}`));
|
|
2998
|
+
}
|
|
2999
|
+
const deniedKeys = result?.deniedKeys ?? [];
|
|
3000
|
+
if (deniedKeys.length > 0) {
|
|
2472
3001
|
console.log();
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
console.log(chalk6.dim(` Skipped: ${result.skipped}`));
|
|
2476
|
-
}
|
|
2477
|
-
} else {
|
|
2478
|
-
success(
|
|
2479
|
-
`Pushed ${result?.total || Object.keys(valid).length} variables to ${chalk6.bold(environment)}`
|
|
3002
|
+
warning(
|
|
3003
|
+
`${deniedKeys.length} variable(s) were NOT written (access denied):`
|
|
2480
3004
|
);
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
console.log(chalk6.dim(` Updated: ${result?.updated || 0}`));
|
|
2484
|
-
if (mode === "replace") {
|
|
2485
|
-
console.log(chalk6.dim(` Deleted: ${result?.deleted || 0}`));
|
|
3005
|
+
for (const key of deniedKeys) {
|
|
3006
|
+
console.log(chalk6.red(` \u2717 ${key}`));
|
|
2486
3007
|
}
|
|
2487
3008
|
}
|
|
3009
|
+
if (result?.skipped && result.skipped > 0) {
|
|
3010
|
+
console.log(chalk6.dim(` Skipped: ${result.skipped}`));
|
|
3011
|
+
}
|
|
2488
3012
|
} catch (err) {
|
|
2489
3013
|
await handleError(err);
|
|
2490
3014
|
}
|
|
@@ -2494,6 +3018,9 @@ var pushCommand = new Command4("push").description("Upload local .env file to cl
|
|
|
2494
3018
|
import { Command as Command5 } from "commander";
|
|
2495
3019
|
import chalk7 from "chalk";
|
|
2496
3020
|
import inquirer4 from "inquirer";
|
|
3021
|
+
function orgUnifiedRole2(org) {
|
|
3022
|
+
return normalizeOrgRole(org.unifiedRole ?? org.role);
|
|
3023
|
+
}
|
|
2497
3024
|
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(
|
|
2498
3025
|
"-e, --env <environment>",
|
|
2499
3026
|
"Switch environment (development, staging, production)"
|
|
@@ -2562,17 +3089,15 @@ var switchCommand = new Command5("switch").description("Switch project, environm
|
|
|
2562
3089
|
process.exit(1);
|
|
2563
3090
|
}
|
|
2564
3091
|
setActiveOrganizationId(org._id);
|
|
2565
|
-
|
|
2566
|
-
setRole(org.role);
|
|
2567
|
-
}
|
|
3092
|
+
setUnifiedRole(orgUnifiedRole2(org));
|
|
2568
3093
|
if (projectConfig) {
|
|
2569
3094
|
updateProjectConfig({ organizationId: org._id });
|
|
2570
3095
|
}
|
|
2571
3096
|
success(`Switched to organization: ${chalk7.bold(org.name)}`);
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
3097
|
+
console.log(
|
|
3098
|
+
chalk7.dim(` Role: ${formatRoleLabel(orgUnifiedRole2(org))}`)
|
|
3099
|
+
);
|
|
3100
|
+
roleNotice(orgUnifiedRole2(org));
|
|
2576
3101
|
return;
|
|
2577
3102
|
}
|
|
2578
3103
|
if (options.project || target) {
|
|
@@ -2611,9 +3136,7 @@ var switchCommand = new Command5("switch").description("Switch project, environm
|
|
|
2611
3136
|
}
|
|
2612
3137
|
if (organizations.length === 1) {
|
|
2613
3138
|
organizationId = organizations[0]._id;
|
|
2614
|
-
|
|
2615
|
-
setRole(organizations[0].role);
|
|
2616
|
-
}
|
|
3139
|
+
setUnifiedRole(orgUnifiedRole2(organizations[0]));
|
|
2617
3140
|
} else {
|
|
2618
3141
|
const { orgId } = await inquirer4.prompt([
|
|
2619
3142
|
{
|
|
@@ -2628,8 +3151,8 @@ var switchCommand = new Command5("switch").description("Switch project, environm
|
|
|
2628
3151
|
]);
|
|
2629
3152
|
organizationId = orgId;
|
|
2630
3153
|
const selectedOrg = organizations.find((o) => o._id === orgId);
|
|
2631
|
-
if (selectedOrg
|
|
2632
|
-
|
|
3154
|
+
if (selectedOrg) {
|
|
3155
|
+
setUnifiedRole(orgUnifiedRole2(selectedOrg));
|
|
2633
3156
|
}
|
|
2634
3157
|
}
|
|
2635
3158
|
}
|
|
@@ -2768,19 +3291,17 @@ var switchCommand = new Command5("switch").description("Switch project, environm
|
|
|
2768
3291
|
if (switchType === "organization") {
|
|
2769
3292
|
setActiveOrganizationId(orgId);
|
|
2770
3293
|
const org = organizations.find((o) => o._id === orgId);
|
|
2771
|
-
|
|
2772
|
-
setRole(org.role);
|
|
2773
|
-
}
|
|
3294
|
+
setUnifiedRole(orgUnifiedRole2(org));
|
|
2774
3295
|
success(`Switched to organization: ${chalk7.bold(org.name)}`);
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
3296
|
+
console.log(
|
|
3297
|
+
chalk7.dim(` Role: ${formatRoleLabel(orgUnifiedRole2(org))}`)
|
|
3298
|
+
);
|
|
3299
|
+
roleNotice(orgUnifiedRole2(org));
|
|
2779
3300
|
return;
|
|
2780
3301
|
}
|
|
2781
3302
|
const selectedOrg = organizations.find((o) => o._id === orgId);
|
|
2782
|
-
if (selectedOrg
|
|
2783
|
-
|
|
3303
|
+
if (selectedOrg) {
|
|
3304
|
+
setUnifiedRole(orgUnifiedRole2(selectedOrg));
|
|
2784
3305
|
}
|
|
2785
3306
|
const projects = await withSpinner(
|
|
2786
3307
|
"Fetching projects...",
|
|
@@ -2928,7 +3449,7 @@ async function listOrganizations(api, options) {
|
|
|
2928
3449
|
name: org.name,
|
|
2929
3450
|
slug: org.slug,
|
|
2930
3451
|
tier: org.tier === "pro" ? chalk8.green("Pro") : chalk8.dim("Free"),
|
|
2931
|
-
role: org.role
|
|
3452
|
+
role: formatRoleLabel(org.unifiedRole ?? org.role)
|
|
2932
3453
|
})),
|
|
2933
3454
|
[
|
|
2934
3455
|
{ key: "name", header: "Name" },
|
|
@@ -2986,7 +3507,9 @@ async function listProjects(api, projectConfig, options) {
|
|
|
2986
3507
|
name: project.name,
|
|
2987
3508
|
slug: project.slug,
|
|
2988
3509
|
description: project.description || chalk8.dim("-"),
|
|
2989
|
-
role:
|
|
3510
|
+
role: formatRoleLabel(
|
|
3511
|
+
project.unifiedRole ?? project.userRole ?? project.projectRole
|
|
3512
|
+
),
|
|
2990
3513
|
active: projectConfig?.projectId === project._id ? chalk8.green("\u2713") : ""
|
|
2991
3514
|
})),
|
|
2992
3515
|
[
|
|
@@ -2998,11 +3521,8 @@ async function listProjects(api, projectConfig, options) {
|
|
|
2998
3521
|
{ key: "active", header: "" }
|
|
2999
3522
|
]
|
|
3000
3523
|
);
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
console.log();
|
|
3004
|
-
console.log(chalk8.dim(`Your org role: ${formatRole(role)}`));
|
|
3005
|
-
}
|
|
3524
|
+
console.log();
|
|
3525
|
+
console.log(chalk8.dim(`Your org role: ${formatRoleLabel(getUnifiedRole())}`));
|
|
3006
3526
|
}
|
|
3007
3527
|
async function listVariables(api, projectConfig, options) {
|
|
3008
3528
|
const projectId = options.project || projectConfig?.projectId;
|
|
@@ -3011,14 +3531,14 @@ async function listVariables(api, projectConfig, options) {
|
|
|
3011
3531
|
error("No project specified. Use --project or run `envpilot init` first.");
|
|
3012
3532
|
process.exit(1);
|
|
3013
3533
|
}
|
|
3014
|
-
let
|
|
3534
|
+
let meta;
|
|
3015
3535
|
const variables = await withSpinner("Fetching variables...", async () => {
|
|
3016
3536
|
const params = { projectId };
|
|
3017
3537
|
if (environment) {
|
|
3018
3538
|
params.environment = environment;
|
|
3019
3539
|
}
|
|
3020
3540
|
const response = await api.get("/api/cli/variables", params);
|
|
3021
|
-
|
|
3541
|
+
meta = response.meta;
|
|
3022
3542
|
return response.data || [];
|
|
3023
3543
|
});
|
|
3024
3544
|
const tagFilter = options.tag?.toLowerCase();
|
|
@@ -3050,7 +3570,7 @@ async function listVariables(api, projectConfig, options) {
|
|
|
3050
3570
|
value: options.showValues ? variable.value : maskValue(variable.value),
|
|
3051
3571
|
sensitive: variable.isSensitive ? chalk8.yellow("\u25CF") : "",
|
|
3052
3572
|
tags: hasTags ? variable.tags?.map((t) => t.name).join(", ") || chalk8.dim("-") : "",
|
|
3053
|
-
version: `v${variable.version}`
|
|
3573
|
+
version: typeof variable.version === "number" ? `v${variable.version}` : ""
|
|
3054
3574
|
})),
|
|
3055
3575
|
[
|
|
3056
3576
|
{ key: "key", header: "Key" },
|
|
@@ -3062,16 +3582,14 @@ async function listVariables(api, projectConfig, options) {
|
|
|
3062
3582
|
);
|
|
3063
3583
|
console.log();
|
|
3064
3584
|
console.log(chalk8.dim(`Total: ${filtered.length} variables`));
|
|
3065
|
-
|
|
3066
|
-
if (
|
|
3067
|
-
console.log(chalk8.dim(`Your org role: ${formatRole(role)}`));
|
|
3068
|
-
}
|
|
3069
|
-
if (metaProjectRole) {
|
|
3585
|
+
console.log(chalk8.dim(`Your org role: ${formatRoleLabel(getUnifiedRole())}`));
|
|
3586
|
+
if (meta?.scopeRestricted && meta.environmentScope?.length) {
|
|
3070
3587
|
console.log(
|
|
3071
|
-
chalk8.dim(
|
|
3588
|
+
chalk8.dim(
|
|
3589
|
+
`Your access is scoped to ${meta.environmentScope.join(", ")}; variables in other environments are withheld.`
|
|
3590
|
+
)
|
|
3072
3591
|
);
|
|
3073
|
-
}
|
|
3074
|
-
if (role === "member" || metaProjectRole === "viewer") {
|
|
3592
|
+
} else if (meta?.grantOnly) {
|
|
3075
3593
|
console.log(
|
|
3076
3594
|
chalk8.dim("You may only see variables you have been granted access to.")
|
|
3077
3595
|
);
|
|
@@ -3084,6 +3602,23 @@ async function listVariables(api, projectConfig, options) {
|
|
|
3084
3602
|
// src/commands/config.ts
|
|
3085
3603
|
import { Command as Command7 } from "commander";
|
|
3086
3604
|
import chalk9 from "chalk";
|
|
3605
|
+
var FILE_PROTECTION_VALUES = ["auto", "always", "never"];
|
|
3606
|
+
var ENVIRONMENT_VALUES = ["development", "staging", "production"];
|
|
3607
|
+
function updateActiveProjectEntry(updates) {
|
|
3608
|
+
const v2 = readProjectConfigV2();
|
|
3609
|
+
if (!v2) {
|
|
3610
|
+
error(
|
|
3611
|
+
"No project initialized in this directory. Run `envpilot init` first."
|
|
3612
|
+
);
|
|
3613
|
+
process.exit(1);
|
|
3614
|
+
}
|
|
3615
|
+
const active = getActiveProject(v2);
|
|
3616
|
+
if (!active) {
|
|
3617
|
+
error("No active project found in this directory.");
|
|
3618
|
+
process.exit(1);
|
|
3619
|
+
}
|
|
3620
|
+
writeProjectConfigV2(updateProjectInConfig(v2, active.projectId, updates));
|
|
3621
|
+
}
|
|
3087
3622
|
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) => {
|
|
3088
3623
|
try {
|
|
3089
3624
|
switch (action) {
|
|
@@ -3127,6 +3662,10 @@ async function handleGet(key) {
|
|
|
3127
3662
|
console.log(" user Current authenticated user");
|
|
3128
3663
|
console.log(" activeProjectId Currently active project");
|
|
3129
3664
|
console.log(" activeOrganizationId Currently active organization");
|
|
3665
|
+
console.log(
|
|
3666
|
+
" fileProtection Active project .env protection (auto|always|never)"
|
|
3667
|
+
);
|
|
3668
|
+
console.log(" defaultEnvironment Active project's default environment");
|
|
3130
3669
|
process.exit(1);
|
|
3131
3670
|
}
|
|
3132
3671
|
const config2 = getConfig();
|
|
@@ -3147,6 +3686,18 @@ async function handleGet(key) {
|
|
|
3147
3686
|
case "activeOrganizationId":
|
|
3148
3687
|
console.log(config2.activeOrganizationId || chalk9.dim("(not set)"));
|
|
3149
3688
|
break;
|
|
3689
|
+
case "fileProtection": {
|
|
3690
|
+
const v2 = readProjectConfigV2();
|
|
3691
|
+
const active = v2 ? getActiveProject(v2) : null;
|
|
3692
|
+
console.log(active?.fileProtection ?? "auto");
|
|
3693
|
+
break;
|
|
3694
|
+
}
|
|
3695
|
+
case "defaultEnvironment": {
|
|
3696
|
+
const v2 = readProjectConfigV2();
|
|
3697
|
+
const active = v2 ? getActiveProject(v2) : null;
|
|
3698
|
+
console.log(active?.environment || chalk9.dim("(not set)"));
|
|
3699
|
+
break;
|
|
3700
|
+
}
|
|
3150
3701
|
default:
|
|
3151
3702
|
error(`Unknown key: ${key}`);
|
|
3152
3703
|
process.exit(1);
|
|
@@ -3156,8 +3707,7 @@ async function handleSet(key, value) {
|
|
|
3156
3707
|
if (!key || value === void 0) {
|
|
3157
3708
|
error("Missing key or value. Usage: envpilot config set <key> <value>");
|
|
3158
3709
|
console.log();
|
|
3159
|
-
|
|
3160
|
-
console.log(" apiUrl API endpoint URL");
|
|
3710
|
+
printSettableKeys();
|
|
3161
3711
|
process.exit(1);
|
|
3162
3712
|
}
|
|
3163
3713
|
switch (key) {
|
|
@@ -3173,14 +3723,53 @@ async function handleSet(key, value) {
|
|
|
3173
3723
|
success(`Set apiUrl to ${normalized}`);
|
|
3174
3724
|
break;
|
|
3175
3725
|
}
|
|
3726
|
+
case "fileProtection": {
|
|
3727
|
+
if (!FILE_PROTECTION_VALUES.includes(
|
|
3728
|
+
value
|
|
3729
|
+
)) {
|
|
3730
|
+
error(
|
|
3731
|
+
`Invalid fileProtection value: ${value}. Expected one of: ${FILE_PROTECTION_VALUES.join(", ")}`
|
|
3732
|
+
);
|
|
3733
|
+
process.exit(1);
|
|
3734
|
+
}
|
|
3735
|
+
updateActiveProjectEntry({
|
|
3736
|
+
fileProtection: value
|
|
3737
|
+
});
|
|
3738
|
+
success(`Set fileProtection to ${value} for the active project`);
|
|
3739
|
+
break;
|
|
3740
|
+
}
|
|
3741
|
+
case "defaultEnvironment": {
|
|
3742
|
+
if (!ENVIRONMENT_VALUES.includes(
|
|
3743
|
+
value
|
|
3744
|
+
)) {
|
|
3745
|
+
error(
|
|
3746
|
+
`Invalid defaultEnvironment value: ${value}. Expected one of: ${ENVIRONMENT_VALUES.join(", ")}`
|
|
3747
|
+
);
|
|
3748
|
+
process.exit(1);
|
|
3749
|
+
}
|
|
3750
|
+
updateActiveProjectEntry({
|
|
3751
|
+
environment: value
|
|
3752
|
+
});
|
|
3753
|
+
success(`Set defaultEnvironment to ${value} for the active project`);
|
|
3754
|
+
break;
|
|
3755
|
+
}
|
|
3176
3756
|
default:
|
|
3177
3757
|
error(`Cannot set key: ${key}`);
|
|
3178
3758
|
console.log();
|
|
3179
|
-
|
|
3180
|
-
console.log(" apiUrl API endpoint URL");
|
|
3759
|
+
printSettableKeys();
|
|
3181
3760
|
process.exit(1);
|
|
3182
3761
|
}
|
|
3183
3762
|
}
|
|
3763
|
+
function printSettableKeys() {
|
|
3764
|
+
console.log("Settable keys:");
|
|
3765
|
+
console.log(" apiUrl API endpoint URL");
|
|
3766
|
+
console.log(
|
|
3767
|
+
" fileProtection Active project .env protection (auto|always|never)"
|
|
3768
|
+
);
|
|
3769
|
+
console.log(
|
|
3770
|
+
" defaultEnvironment Active project default environment (development|staging|production)"
|
|
3771
|
+
);
|
|
3772
|
+
}
|
|
3184
3773
|
async function handleList() {
|
|
3185
3774
|
const config2 = getConfig();
|
|
3186
3775
|
const projectConfig = readProjectConfig();
|
|
@@ -3217,8 +3806,8 @@ async function handlePath() {
|
|
|
3217
3806
|
]);
|
|
3218
3807
|
}
|
|
3219
3808
|
async function handleReset() {
|
|
3220
|
-
const
|
|
3221
|
-
const { confirm } = await
|
|
3809
|
+
const inquirer7 = await import("inquirer");
|
|
3810
|
+
const { confirm } = await inquirer7.default.prompt([
|
|
3222
3811
|
{
|
|
3223
3812
|
type: "confirm",
|
|
3224
3813
|
name: "confirm",
|
|
@@ -3236,20 +3825,43 @@ async function handleReset() {
|
|
|
3236
3825
|
|
|
3237
3826
|
// src/commands/logout.ts
|
|
3238
3827
|
import { Command as Command8 } from "commander";
|
|
3239
|
-
var logoutCommand = new Command8("logout").description("Log out from Envpilot").action(async () => {
|
|
3828
|
+
var logoutCommand = new Command8("logout").description("Log out from Envpilot").option("--all", "Log out of every authenticated account").action(async (options) => {
|
|
3240
3829
|
try {
|
|
3241
3830
|
if (!isAuthenticated()) {
|
|
3242
3831
|
info("You are not logged in.");
|
|
3243
3832
|
return;
|
|
3244
3833
|
}
|
|
3245
|
-
|
|
3834
|
+
if (options.all) {
|
|
3835
|
+
const accounts = listAccounts();
|
|
3836
|
+
for (const account of accounts) {
|
|
3837
|
+
try {
|
|
3838
|
+
const api2 = new APIClient({ accessToken: account.accessToken });
|
|
3839
|
+
await api2.post("/api/cli/auth?action=revoke", {});
|
|
3840
|
+
} catch {
|
|
3841
|
+
}
|
|
3842
|
+
removeAccount(account.id);
|
|
3843
|
+
}
|
|
3844
|
+
clearAuth();
|
|
3845
|
+
success(`Logged out of all ${accounts.length} accounts.`);
|
|
3846
|
+
return;
|
|
3847
|
+
}
|
|
3848
|
+
const activeAccount = getActiveAccount();
|
|
3246
3849
|
const api = createAPIClient();
|
|
3247
3850
|
try {
|
|
3248
3851
|
await api.post("/api/cli/auth?action=revoke", {});
|
|
3249
3852
|
} catch {
|
|
3250
3853
|
}
|
|
3251
|
-
clearAuth();
|
|
3252
|
-
success(
|
|
3854
|
+
const { newActiveId } = clearAuth();
|
|
3855
|
+
success(
|
|
3856
|
+
`Logged out${activeAccount?.user.email ? ` ${activeAccount.user.email}` : ""}.`
|
|
3857
|
+
);
|
|
3858
|
+
if (newActiveId) {
|
|
3859
|
+
const remaining = listAccounts();
|
|
3860
|
+
const newActive = remaining.find((a) => a.id === newActiveId);
|
|
3861
|
+
info(`Now active: ${newActive?.user.email ?? newActiveId}`);
|
|
3862
|
+
} else {
|
|
3863
|
+
info("No accounts remaining.");
|
|
3864
|
+
}
|
|
3253
3865
|
} catch (err) {
|
|
3254
3866
|
await handleError(err);
|
|
3255
3867
|
}
|
|
@@ -3351,20 +3963,35 @@ import chalk11 from "chalk";
|
|
|
3351
3963
|
|
|
3352
3964
|
// src/lib/commit-guard.ts
|
|
3353
3965
|
import {
|
|
3354
|
-
existsSync as
|
|
3355
|
-
readFileSync as
|
|
3356
|
-
writeFileSync as
|
|
3357
|
-
mkdirSync,
|
|
3966
|
+
existsSync as existsSync4,
|
|
3967
|
+
readFileSync as readFileSync4,
|
|
3968
|
+
writeFileSync as writeFileSync4,
|
|
3969
|
+
mkdirSync as mkdirSync2,
|
|
3358
3970
|
chmodSync as chmodSync2,
|
|
3359
|
-
unlinkSync as
|
|
3360
|
-
|
|
3971
|
+
unlinkSync as unlinkSync3,
|
|
3972
|
+
renameSync as renameSync3,
|
|
3973
|
+
statSync as statSync2
|
|
3361
3974
|
} from "fs";
|
|
3362
|
-
import { join as
|
|
3975
|
+
import { join as join4, resolve as resolve2 } from "path";
|
|
3363
3976
|
import { execSync as execSync2 } from "child_process";
|
|
3977
|
+
function atomicWriteFileSync3(filePath, content, mode) {
|
|
3978
|
+
const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
3979
|
+
try {
|
|
3980
|
+
writeFileSync4(tmpPath, content, "utf-8");
|
|
3981
|
+
chmodSync2(tmpPath, mode);
|
|
3982
|
+
renameSync3(tmpPath, filePath);
|
|
3983
|
+
} catch (err) {
|
|
3984
|
+
try {
|
|
3985
|
+
if (existsSync4(tmpPath)) unlinkSync3(tmpPath);
|
|
3986
|
+
} catch {
|
|
3987
|
+
}
|
|
3988
|
+
throw err;
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3364
3991
|
var HOOK_START_MARKER = "# ENVPILOT_GUARD_START";
|
|
3365
3992
|
var HOOK_END_MARKER = "# ENVPILOT_GUARD_END";
|
|
3366
3993
|
var HOOK_BLOCK = `${HOOK_START_MARKER} - Do not remove. Installed by Envpilot CLI.
|
|
3367
|
-
ENV_FILES=$(git diff --cached --name-only | grep -E '(^|/)\\.env($|\\.)' || true)
|
|
3994
|
+
ENV_FILES=$(git diff --cached --name-only | grep -E '(^|/)\\.env($|\\.)' | grep -vE '\\.(example|sample|template|dist)$' || true)
|
|
3368
3995
|
if [ -n "$ENV_FILES" ]; then
|
|
3369
3996
|
echo ""
|
|
3370
3997
|
echo "\\033[1;31mERROR:\\033[0m Envpilot commit guard blocked this commit."
|
|
@@ -3380,7 +4007,7 @@ ${HOOK_END_MARKER}`;
|
|
|
3380
4007
|
function findGitRoot(startDir) {
|
|
3381
4008
|
let dir = startDir || process.cwd();
|
|
3382
4009
|
while (true) {
|
|
3383
|
-
if (
|
|
4010
|
+
if (existsSync4(join4(dir, ".git"))) {
|
|
3384
4011
|
return dir;
|
|
3385
4012
|
}
|
|
3386
4013
|
const parent = resolve2(dir, "..");
|
|
@@ -3391,9 +4018,9 @@ function findGitRoot(startDir) {
|
|
|
3391
4018
|
}
|
|
3392
4019
|
}
|
|
3393
4020
|
function resolveGitDir(repoRoot) {
|
|
3394
|
-
const gitPath =
|
|
4021
|
+
const gitPath = join4(repoRoot, ".git");
|
|
3395
4022
|
try {
|
|
3396
|
-
const stat =
|
|
4023
|
+
const stat = statSync2(gitPath);
|
|
3397
4024
|
if (stat.isDirectory()) {
|
|
3398
4025
|
return gitPath;
|
|
3399
4026
|
}
|
|
@@ -3401,12 +4028,12 @@ function resolveGitDir(repoRoot) {
|
|
|
3401
4028
|
return gitPath;
|
|
3402
4029
|
}
|
|
3403
4030
|
try {
|
|
3404
|
-
const content =
|
|
4031
|
+
const content = readFileSync4(gitPath, "utf-8").trim();
|
|
3405
4032
|
const match = content.match(/^gitdir:\s*(.+)$/);
|
|
3406
4033
|
if (match) {
|
|
3407
4034
|
const gitdir = resolve2(repoRoot, match[1]);
|
|
3408
4035
|
const commonDir = resolve2(gitdir, "..", "..");
|
|
3409
|
-
if (
|
|
4036
|
+
if (existsSync4(join4(commonDir, "hooks")) || existsSync4(commonDir)) {
|
|
3410
4037
|
return commonDir;
|
|
3411
4038
|
}
|
|
3412
4039
|
return gitdir;
|
|
@@ -3428,7 +4055,7 @@ function getHooksDir(repoRoot) {
|
|
|
3428
4055
|
} catch {
|
|
3429
4056
|
}
|
|
3430
4057
|
const gitDir = resolveGitDir(repoRoot);
|
|
3431
|
-
return
|
|
4058
|
+
return join4(gitDir, "hooks");
|
|
3432
4059
|
}
|
|
3433
4060
|
function installCommitGuard(repoRoot) {
|
|
3434
4061
|
const root = repoRoot || findGitRoot();
|
|
@@ -3441,19 +4068,18 @@ function installCommitGuard(repoRoot) {
|
|
|
3441
4068
|
}
|
|
3442
4069
|
try {
|
|
3443
4070
|
const hooksDir = getHooksDir(root);
|
|
3444
|
-
const hookPath =
|
|
3445
|
-
|
|
4071
|
+
const hookPath = join4(hooksDir, "pre-commit");
|
|
4072
|
+
mkdirSync2(hooksDir, { recursive: true });
|
|
3446
4073
|
let existingContent = "";
|
|
3447
4074
|
try {
|
|
3448
|
-
existingContent =
|
|
4075
|
+
existingContent = readFileSync4(hookPath, "utf-8");
|
|
3449
4076
|
} catch {
|
|
3450
4077
|
}
|
|
3451
4078
|
if (existingContent.includes(HOOK_START_MARKER)) {
|
|
3452
4079
|
const startIdx = existingContent.indexOf(HOOK_START_MARKER);
|
|
3453
4080
|
const endIdx = existingContent.indexOf(HOOK_END_MARKER) + HOOK_END_MARKER.length;
|
|
3454
4081
|
const updated = existingContent.substring(0, startIdx) + HOOK_BLOCK + existingContent.substring(endIdx);
|
|
3455
|
-
|
|
3456
|
-
chmodSync2(hookPath, 493);
|
|
4082
|
+
atomicWriteFileSync3(hookPath, updated, 493);
|
|
3457
4083
|
return {
|
|
3458
4084
|
installed: true,
|
|
3459
4085
|
hookPath,
|
|
@@ -3466,8 +4092,7 @@ function installCommitGuard(repoRoot) {
|
|
|
3466
4092
|
} else {
|
|
3467
4093
|
newContent = "#!/bin/sh\n\n" + HOOK_BLOCK + "\n";
|
|
3468
4094
|
}
|
|
3469
|
-
|
|
3470
|
-
chmodSync2(hookPath, 493);
|
|
4095
|
+
atomicWriteFileSync3(hookPath, newContent, 493);
|
|
3471
4096
|
return {
|
|
3472
4097
|
installed: true,
|
|
3473
4098
|
hookPath,
|
|
@@ -3483,6 +4108,21 @@ function installCommitGuard(repoRoot) {
|
|
|
3483
4108
|
}
|
|
3484
4109
|
|
|
3485
4110
|
// src/commands/sync.ts
|
|
4111
|
+
function accessFromMeta2(meta, variables) {
|
|
4112
|
+
const role = normalizeOrgRole(meta?.unifiedRole ?? meta?.role);
|
|
4113
|
+
return {
|
|
4114
|
+
role,
|
|
4115
|
+
// Owners are implicitly assigned to every project. The legacy server sends
|
|
4116
|
+
// no `assigned`/`projectRole` for owners, so fall back to true for them.
|
|
4117
|
+
assigned: role === "owner" || (meta?.assigned ?? (meta?.projectRole !== null && meta?.projectRole !== void 0)),
|
|
4118
|
+
environmentScope: meta?.environmentScope ?? null,
|
|
4119
|
+
hasWriteAccess: meta?.hasWriteAccess ?? variables.some((v) => v.access === "write")
|
|
4120
|
+
};
|
|
4121
|
+
}
|
|
4122
|
+
function orgUnifiedRole3(org) {
|
|
4123
|
+
const unified = org.unifiedRole;
|
|
4124
|
+
return normalizeOrgRole(unified ?? org.role);
|
|
4125
|
+
}
|
|
3486
4126
|
var syncCommand = new Command10("sync").description(
|
|
3487
4127
|
"Login, select project, pull variables, and protect files \u2014 all in one command"
|
|
3488
4128
|
).option("-o, --organization <id>", "Organization ID").option("-p, --project <id>", "Project ID or name").option(
|
|
@@ -3546,9 +4186,7 @@ var syncCommand = new Command10("sync").description(
|
|
|
3546
4186
|
environment = selection.selectedEnvironment;
|
|
3547
4187
|
projectName = selection.selectedProject.name;
|
|
3548
4188
|
organizationName = selection.selectedOrg.name;
|
|
3549
|
-
|
|
3550
|
-
setRole(selection.selectedOrg.role);
|
|
3551
|
-
}
|
|
4189
|
+
setUnifiedRole(orgUnifiedRole3(selection.selectedOrg));
|
|
3552
4190
|
writeProjectConfigV2({
|
|
3553
4191
|
version: 1,
|
|
3554
4192
|
activeProjectId: projectId,
|
|
@@ -3577,7 +4215,7 @@ var syncCommand = new Command10("sync").description(
|
|
|
3577
4215
|
setActiveProjectId(projectId);
|
|
3578
4216
|
}
|
|
3579
4217
|
console.log();
|
|
3580
|
-
let
|
|
4218
|
+
let meta;
|
|
3581
4219
|
const variables = await withSpinner(
|
|
3582
4220
|
`Fetching ${chalk11.bold(environment)} variables...`,
|
|
3583
4221
|
async () => {
|
|
@@ -3587,11 +4225,16 @@ var syncCommand = new Command10("sync").description(
|
|
|
3587
4225
|
environment,
|
|
3588
4226
|
...organizationId && { organizationId }
|
|
3589
4227
|
});
|
|
3590
|
-
|
|
4228
|
+
meta = response.meta;
|
|
3591
4229
|
return response.data || [];
|
|
3592
4230
|
}
|
|
3593
4231
|
);
|
|
3594
4232
|
const outputPath = getEnvPathForEnvironment(environment);
|
|
4233
|
+
if (meta?.scopeRestricted && meta.environmentScope?.length) {
|
|
4234
|
+
info(
|
|
4235
|
+
`Your access is scoped to ${meta.environmentScope.join(", ")}; variables in other environments are withheld.`
|
|
4236
|
+
);
|
|
4237
|
+
}
|
|
3595
4238
|
if (variables.length === 0) {
|
|
3596
4239
|
warning(`No variables found for ${environment} environment.`);
|
|
3597
4240
|
console.log();
|
|
@@ -3628,15 +4271,21 @@ var syncCommand = new Command10("sync").description(
|
|
|
3628
4271
|
}
|
|
3629
4272
|
}
|
|
3630
4273
|
writeEnvFile(outputPath, remoteVars, { sort: true, comments });
|
|
3631
|
-
const
|
|
3632
|
-
|
|
4274
|
+
const access = accessFromMeta2(meta, variables);
|
|
4275
|
+
const protection = existingConfig?.projects.find((p) => p.projectId === projectId)?.fileProtection ?? "auto";
|
|
4276
|
+
applyFileProtection(outputPath, access, protection);
|
|
3633
4277
|
success(
|
|
3634
4278
|
`Synced ${variables.length} variables to ${chalk11.bold(outputPath)}`
|
|
3635
4279
|
);
|
|
3636
|
-
|
|
3637
|
-
if (isProtected) {
|
|
4280
|
+
if (meta?.grantOnly) {
|
|
3638
4281
|
info(
|
|
3639
|
-
|
|
4282
|
+
"You have grant-only access to this project. You only see variables explicitly shared with you."
|
|
4283
|
+
);
|
|
4284
|
+
}
|
|
4285
|
+
const writable = protection === "never" || protection !== "always" && isFileWritable(access);
|
|
4286
|
+
if (!writable) {
|
|
4287
|
+
info(
|
|
4288
|
+
`File is read-only (your role: ${formatRoleLabel(access.role)}${access.assigned ? "" : ", not assigned to this project"}).`
|
|
3640
4289
|
);
|
|
3641
4290
|
}
|
|
3642
4291
|
console.log();
|
|
@@ -3784,6 +4433,7 @@ var whoamiCommand = new Command12("whoami").description("Show the current authen
|
|
|
3784
4433
|
["Name", remoteUser.name || localUser?.name],
|
|
3785
4434
|
["API URL", getApiUrl()],
|
|
3786
4435
|
["Active Organization", getActiveOrganizationId()],
|
|
4436
|
+
["Org Role", formatRoleLabel(getUnifiedRole())],
|
|
3787
4437
|
["Active Project", getActiveProjectId()],
|
|
3788
4438
|
[
|
|
3789
4439
|
"Linked Project",
|
|
@@ -3791,6 +4441,15 @@ var whoamiCommand = new Command12("whoami").description("Show the current authen
|
|
|
3791
4441
|
],
|
|
3792
4442
|
["Environment", activeProject?.environment]
|
|
3793
4443
|
]);
|
|
4444
|
+
const accounts = listAccounts();
|
|
4445
|
+
if (accounts.length > 1) {
|
|
4446
|
+
blank();
|
|
4447
|
+
console.log(
|
|
4448
|
+
chalk13.dim(
|
|
4449
|
+
`Accounts: ${accounts.length} (use \`envpilot accounts\` to switch)`
|
|
4450
|
+
)
|
|
4451
|
+
);
|
|
4452
|
+
}
|
|
3794
4453
|
blank();
|
|
3795
4454
|
console.log(chalk13.dim("Token verified against the CLI auth endpoint."));
|
|
3796
4455
|
} catch (err) {
|
|
@@ -3798,110 +4457,120 @@ var whoamiCommand = new Command12("whoami").description("Show the current authen
|
|
|
3798
4457
|
}
|
|
3799
4458
|
});
|
|
3800
4459
|
|
|
3801
|
-
// src/commands/
|
|
4460
|
+
// src/commands/accounts.ts
|
|
3802
4461
|
import { Command as Command13 } from "commander";
|
|
3803
|
-
import { spawn as spawn2 } from "child_process";
|
|
3804
4462
|
import chalk14 from "chalk";
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
writeFileSync as writeFileSync4,
|
|
3811
|
-
mkdirSync as mkdirSync2,
|
|
3812
|
-
existsSync as existsSync4,
|
|
3813
|
-
readdirSync,
|
|
3814
|
-
unlinkSync as unlinkSync3,
|
|
3815
|
-
statSync as statSync2
|
|
3816
|
-
} from "fs";
|
|
3817
|
-
import { join as join4, dirname as dirname2 } from "path";
|
|
3818
|
-
function getCacheDir() {
|
|
3819
|
-
return join4(dirname2(getConfigPath()), "run-cache");
|
|
3820
|
-
}
|
|
3821
|
-
function getCacheKey(projectId, environment, organizationId) {
|
|
3822
|
-
const tokenSlice = (getAccessToken() ?? "").slice(0, 16);
|
|
3823
|
-
return createHash("sha256").update(`${projectId}:${environment}:${organizationId}:${tokenSlice}`).digest("hex").slice(0, 20);
|
|
4463
|
+
function resolveAccount(accounts, identifier) {
|
|
4464
|
+
const normalized = identifier.trim().toLowerCase();
|
|
4465
|
+
return accounts.find(
|
|
4466
|
+
(account) => account.id === identifier || account.user.email.toLowerCase() === normalized
|
|
4467
|
+
);
|
|
3824
4468
|
}
|
|
3825
|
-
function
|
|
3826
|
-
|
|
4469
|
+
function listAvailableEmails(accounts) {
|
|
4470
|
+
console.log();
|
|
4471
|
+
console.log("Available accounts:");
|
|
4472
|
+
for (const account of accounts) {
|
|
4473
|
+
console.log(` ${account.user.email}`);
|
|
4474
|
+
}
|
|
3827
4475
|
}
|
|
3828
|
-
function
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
4476
|
+
function printAccountsList() {
|
|
4477
|
+
const accounts = listAccounts();
|
|
4478
|
+
const activeId = getActiveAccountId();
|
|
4479
|
+
if (accounts.length === 0) {
|
|
4480
|
+
console.log("No accounts. Run `envpilot login`.");
|
|
4481
|
+
return;
|
|
4482
|
+
}
|
|
4483
|
+
table(
|
|
4484
|
+
accounts.map((account) => ({
|
|
4485
|
+
active: account.id === activeId ? chalk14.green("\u25CF") : "",
|
|
4486
|
+
email: account.user.email,
|
|
4487
|
+
name: account.user.name ?? "",
|
|
4488
|
+
organization: account.activeOrganizationId ?? ""
|
|
4489
|
+
})),
|
|
4490
|
+
[
|
|
4491
|
+
{ key: "active", header: "" },
|
|
4492
|
+
{ key: "email", header: "Email" },
|
|
4493
|
+
{ key: "name", header: "Name" },
|
|
4494
|
+
{ key: "organization", header: "Active Org" }
|
|
4495
|
+
]
|
|
4496
|
+
);
|
|
4497
|
+
blank();
|
|
4498
|
+
if (accounts.length === 1) {
|
|
4499
|
+
console.log(chalk14.dim("1 account (log in again to add another)."));
|
|
4500
|
+
} else {
|
|
4501
|
+
console.log(chalk14.dim(`${accounts.length} accounts.`));
|
|
4502
|
+
}
|
|
3832
4503
|
}
|
|
3833
|
-
function
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
if (entry.apiUrl !== getApiUrl()) return null;
|
|
3841
|
-
return entry;
|
|
3842
|
-
} catch {
|
|
3843
|
-
return null;
|
|
4504
|
+
function switchAccount(identifier) {
|
|
4505
|
+
const accounts = listAccounts();
|
|
4506
|
+
const target = resolveAccount(accounts, identifier);
|
|
4507
|
+
if (!target) {
|
|
4508
|
+
error(`Account not found: ${identifier}`);
|
|
4509
|
+
listAvailableEmails(accounts);
|
|
4510
|
+
process.exit(1);
|
|
3844
4511
|
}
|
|
4512
|
+
setActiveAccount(target.id);
|
|
4513
|
+
success(`Switched to ${target.user.email}.`);
|
|
3845
4514
|
}
|
|
3846
|
-
function
|
|
3847
|
-
const
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
4515
|
+
function removeAccountByIdentifier(identifier) {
|
|
4516
|
+
const accounts = listAccounts();
|
|
4517
|
+
const target = resolveAccount(accounts, identifier);
|
|
4518
|
+
if (!target) {
|
|
4519
|
+
error(`Account not found: ${identifier}`);
|
|
4520
|
+
listAvailableEmails(accounts);
|
|
4521
|
+
process.exit(1);
|
|
4522
|
+
}
|
|
4523
|
+
const result = removeAccount(target.id);
|
|
4524
|
+
success(`Removed ${target.user.email}.`);
|
|
4525
|
+
if (result.removedActive) {
|
|
4526
|
+
if (result.newActiveId) {
|
|
4527
|
+
const remaining = listAccounts();
|
|
4528
|
+
const newActive = remaining.find((a) => a.id === result.newActiveId);
|
|
4529
|
+
console.log(
|
|
4530
|
+
chalk14.dim(`Now active: ${newActive?.user.email ?? result.newActiveId}`)
|
|
4531
|
+
);
|
|
4532
|
+
} else {
|
|
4533
|
+
console.log(chalk14.dim("No accounts remaining."));
|
|
4534
|
+
}
|
|
4535
|
+
}
|
|
3851
4536
|
}
|
|
3852
|
-
|
|
4537
|
+
var accountsCommand = new Command13("accounts").description("List and manage authenticated accounts").action(async () => {
|
|
3853
4538
|
try {
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
const path = getCachePath(key);
|
|
3858
|
-
const entry = {
|
|
3859
|
-
variables,
|
|
3860
|
-
fetchedAt: Date.now(),
|
|
3861
|
-
projectId,
|
|
3862
|
-
environment,
|
|
3863
|
-
organizationId,
|
|
3864
|
-
apiUrl: getApiUrl(),
|
|
3865
|
-
fingerprint: computeFingerprint(variables)
|
|
3866
|
-
};
|
|
3867
|
-
writeFileSync4(path, JSON.stringify(entry, null, 2), {
|
|
3868
|
-
encoding: "utf-8",
|
|
3869
|
-
mode: 384
|
|
3870
|
-
// owner read/write only — same as .env
|
|
3871
|
-
});
|
|
3872
|
-
} catch {
|
|
4539
|
+
printAccountsList();
|
|
4540
|
+
} catch (err) {
|
|
4541
|
+
await handleError(err);
|
|
3873
4542
|
}
|
|
3874
|
-
}
|
|
3875
|
-
|
|
4543
|
+
});
|
|
4544
|
+
accountsCommand.command("list").description("List all authenticated accounts").action(async () => {
|
|
3876
4545
|
try {
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
const raw = readFileSync4(path, "utf-8");
|
|
3881
|
-
const entry = JSON.parse(raw);
|
|
3882
|
-
entry.fetchedAt = Date.now();
|
|
3883
|
-
writeFileSync4(path, JSON.stringify(entry, null, 2), {
|
|
3884
|
-
encoding: "utf-8",
|
|
3885
|
-
mode: 384
|
|
3886
|
-
});
|
|
3887
|
-
} catch {
|
|
4546
|
+
printAccountsList();
|
|
4547
|
+
} catch (err) {
|
|
4548
|
+
await handleError(err);
|
|
3888
4549
|
}
|
|
3889
|
-
}
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
4550
|
+
});
|
|
4551
|
+
accountsCommand.command("switch").description("Switch the active account").argument("<identifier>", "Account id or email").action(async (identifier) => {
|
|
4552
|
+
try {
|
|
4553
|
+
switchAccount(identifier);
|
|
4554
|
+
} catch (err) {
|
|
4555
|
+
await handleError(err);
|
|
4556
|
+
}
|
|
4557
|
+
});
|
|
4558
|
+
accountsCommand.command("remove").description("Remove an account").argument("<identifier>", "Account id or email").action(async (identifier) => {
|
|
4559
|
+
try {
|
|
4560
|
+
removeAccountByIdentifier(identifier);
|
|
4561
|
+
} catch (err) {
|
|
4562
|
+
await handleError(err);
|
|
4563
|
+
}
|
|
4564
|
+
});
|
|
3900
4565
|
|
|
3901
4566
|
// src/commands/run.ts
|
|
4567
|
+
import { Command as Command14 } from "commander";
|
|
4568
|
+
import crossSpawn from "cross-spawn";
|
|
4569
|
+
import { constants as osConstants } from "os";
|
|
4570
|
+
import chalk15 from "chalk";
|
|
3902
4571
|
var DEFAULT_TTL = 3600;
|
|
3903
|
-
var runCommand = new
|
|
3904
|
-
"Run a command with project secrets injected as environment variables (no .env file written)"
|
|
4572
|
+
var runCommand = new Command14("run").description(
|
|
4573
|
+
"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."
|
|
3905
4574
|
).argument("[command...]", "Command and arguments to execute").option(
|
|
3906
4575
|
"-e, --env <environment>",
|
|
3907
4576
|
"Environment to load (development, staging, production)"
|
|
@@ -3919,10 +4588,10 @@ var runCommand = new Command13("run").description(
|
|
|
3919
4588
|
"Print the variables that would be injected and exit (no command runs)"
|
|
3920
4589
|
).option(
|
|
3921
4590
|
"--shell",
|
|
3922
|
-
|
|
4591
|
+
'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).'
|
|
3923
4592
|
).option("-q, --quiet", "Suppress informational messages").option("--no-cache", "Skip cache and always fetch fresh secrets from server").option(
|
|
3924
4593
|
"--cache-ttl <seconds>",
|
|
3925
|
-
"How long cached secrets stay fresh before a fingerprint check (default: 3600s / 1h)",
|
|
4594
|
+
"How long cached secrets stay fresh before a fingerprint check (default: 3600s / 1h; 0 = always fingerprint-check)",
|
|
3926
4595
|
String(DEFAULT_TTL)
|
|
3927
4596
|
).passThroughOptions().allowExcessArguments().action(async (commandArgs, options) => {
|
|
3928
4597
|
try {
|
|
@@ -3951,10 +4620,11 @@ var runCommand = new Command13("run").description(
|
|
|
3951
4620
|
}
|
|
3952
4621
|
const environment = options.env || project.environment;
|
|
3953
4622
|
const organizationId = options.organization || project.organizationId;
|
|
3954
|
-
const
|
|
3955
|
-
|
|
3956
|
-
|
|
4623
|
+
const parsedTtl = Number.parseInt(
|
|
4624
|
+
options.cacheTtl ?? String(DEFAULT_TTL),
|
|
4625
|
+
10
|
|
3957
4626
|
);
|
|
4627
|
+
const ttl = Number.isFinite(parsedTtl) && parsedTtl >= 0 ? parsedTtl : DEFAULT_TTL;
|
|
3958
4628
|
let variables;
|
|
3959
4629
|
let cacheHit = false;
|
|
3960
4630
|
let cacheAge = "";
|
|
@@ -4006,13 +4676,19 @@ var runCommand = new Command13("run").description(
|
|
|
4006
4676
|
project.projectId,
|
|
4007
4677
|
environment,
|
|
4008
4678
|
organizationId,
|
|
4009
|
-
variables
|
|
4679
|
+
variables,
|
|
4680
|
+
serverFingerprint
|
|
4010
4681
|
);
|
|
4011
4682
|
}
|
|
4012
4683
|
} catch {
|
|
4013
4684
|
variables = probe.entry.variables;
|
|
4014
4685
|
cacheHit = true;
|
|
4015
4686
|
cacheAge = formatAge(probe.entry.fetchedAt);
|
|
4687
|
+
if (!options.quiet) {
|
|
4688
|
+
warning(
|
|
4689
|
+
`Using offline cache (age ${cacheAge}) \u2014 could not reach the server to verify freshness.`
|
|
4690
|
+
);
|
|
4691
|
+
}
|
|
4016
4692
|
}
|
|
4017
4693
|
} else {
|
|
4018
4694
|
variables = await doFetch(
|
|
@@ -4049,9 +4725,9 @@ var runCommand = new Command13("run").description(
|
|
|
4049
4725
|
}
|
|
4050
4726
|
if (!options.quiet) {
|
|
4051
4727
|
const injectedCount = Object.keys(secrets).length;
|
|
4052
|
-
const cacheTag = cacheHit ?
|
|
4728
|
+
const cacheTag = cacheHit ? chalk15.dim(` \u26A1 cache (${cacheAge})`) : "";
|
|
4053
4729
|
info(
|
|
4054
|
-
`Injected ${
|
|
4730
|
+
`Injected ${chalk15.bold(injectedCount)} ${injectedCount === 1 ? "variable" : "variables"} from ${chalk15.bold(`${project.projectName || project.projectId}/${environment}`)}${cacheTag}`
|
|
4055
4731
|
);
|
|
4056
4732
|
if (overridden.length > 0) {
|
|
4057
4733
|
warning(
|
|
@@ -4061,13 +4737,13 @@ var runCommand = new Command13("run").description(
|
|
|
4061
4737
|
}
|
|
4062
4738
|
console.log();
|
|
4063
4739
|
}
|
|
4064
|
-
await runChild(commandArgs, finalEnv, options);
|
|
4740
|
+
process.exitCode = await runChild(commandArgs, finalEnv, options);
|
|
4065
4741
|
} catch (err) {
|
|
4066
4742
|
await handleError(err);
|
|
4067
4743
|
}
|
|
4068
4744
|
});
|
|
4069
4745
|
async function doFetch(project, environment, organizationId, quiet, labelPrefix = "Loading") {
|
|
4070
|
-
const label = `${labelPrefix} ${
|
|
4746
|
+
const label = `${labelPrefix} ${chalk15.bold(environment)} secrets for ${chalk15.bold(project.projectName || project.projectId)}...`;
|
|
4071
4747
|
const api = createAPIClient();
|
|
4072
4748
|
const { variables, decryptionFailures } = quiet ? await api.listVariables(project.projectId, environment, organizationId) : await withSpinner(
|
|
4073
4749
|
label,
|
|
@@ -4076,7 +4752,7 @@ async function doFetch(project, environment, organizationId, quiet, labelPrefix
|
|
|
4076
4752
|
if (decryptionFailures.length > 0) {
|
|
4077
4753
|
for (const key of decryptionFailures) {
|
|
4078
4754
|
warning(
|
|
4079
|
-
`Could not decrypt ${
|
|
4755
|
+
`Could not decrypt ${chalk15.bold(key)} \u2014 skipped (vault error, check server logs)`
|
|
4080
4756
|
);
|
|
4081
4757
|
}
|
|
4082
4758
|
}
|
|
@@ -4086,46 +4762,49 @@ function printInjectionPreview(secrets, project, environment) {
|
|
|
4086
4762
|
const keys = Object.keys(secrets).sort();
|
|
4087
4763
|
console.log();
|
|
4088
4764
|
console.log(
|
|
4089
|
-
|
|
4090
|
-
`Would inject ${keys.length} ${keys.length === 1 ? "variable" : "variables"} from ${
|
|
4765
|
+
chalk15.bold(
|
|
4766
|
+
`Would inject ${keys.length} ${keys.length === 1 ? "variable" : "variables"} from ${chalk15.cyan(`${project.projectName || project.projectId}/${environment}`)}:`
|
|
4091
4767
|
)
|
|
4092
4768
|
);
|
|
4093
4769
|
console.log();
|
|
4094
4770
|
if (keys.length === 0) {
|
|
4095
|
-
console.log(
|
|
4771
|
+
console.log(chalk15.dim(" (no variables)"));
|
|
4096
4772
|
} else {
|
|
4097
4773
|
for (const key of keys) {
|
|
4098
4774
|
const value = secrets[key];
|
|
4099
4775
|
const masked = maskForPreview(value);
|
|
4100
|
-
console.log(` ${
|
|
4776
|
+
console.log(` ${chalk15.cyan(key)}=${chalk15.dim(masked)}`);
|
|
4101
4777
|
}
|
|
4102
4778
|
}
|
|
4103
4779
|
console.log();
|
|
4104
4780
|
success("Dry run \u2014 no command executed.");
|
|
4105
4781
|
}
|
|
4106
4782
|
function maskForPreview(value) {
|
|
4107
|
-
|
|
4108
|
-
|
|
4783
|
+
const len = value.length;
|
|
4784
|
+
const dots = "\u2022".repeat(6);
|
|
4785
|
+
if (len < 12) return `${dots} (${len} chars)`;
|
|
4786
|
+
return `${value.slice(0, 2)}${dots} (${len} chars)`;
|
|
4109
4787
|
}
|
|
4110
4788
|
function runChild(commandArgs, env, options) {
|
|
4111
4789
|
return new Promise((resolve3) => {
|
|
4112
|
-
|
|
4790
|
+
let command;
|
|
4791
|
+
let args;
|
|
4792
|
+
if (options.shell) {
|
|
4793
|
+
command = process.env.SHELL || "/bin/sh";
|
|
4794
|
+
args = ["-c", commandArgs.join(" ")];
|
|
4795
|
+
} else {
|
|
4796
|
+
[command, ...args] = commandArgs;
|
|
4797
|
+
}
|
|
4113
4798
|
if (!command) {
|
|
4114
|
-
|
|
4799
|
+
resolve3(1);
|
|
4800
|
+
return;
|
|
4115
4801
|
}
|
|
4116
|
-
const
|
|
4117
|
-
const useShell = options.shell === true || isWindows;
|
|
4118
|
-
const child = spawn2(command, args, {
|
|
4802
|
+
const child = crossSpawn(command, args, {
|
|
4119
4803
|
stdio: "inherit",
|
|
4120
4804
|
env,
|
|
4121
|
-
shell:
|
|
4805
|
+
shell: false
|
|
4122
4806
|
});
|
|
4123
|
-
const signals = [
|
|
4124
|
-
"SIGINT",
|
|
4125
|
-
"SIGTERM",
|
|
4126
|
-
"SIGHUP",
|
|
4127
|
-
"SIGQUIT"
|
|
4128
|
-
];
|
|
4807
|
+
const signals = ["SIGTERM", "SIGHUP"];
|
|
4129
4808
|
const forward = {};
|
|
4130
4809
|
for (const sig of signals) {
|
|
4131
4810
|
const handler = () => {
|
|
@@ -4148,67 +4827,66 @@ function runChild(commandArgs, env, options) {
|
|
|
4148
4827
|
child.on("error", (err) => {
|
|
4149
4828
|
cleanup();
|
|
4150
4829
|
const code = err.code;
|
|
4830
|
+
const displayCommand = options.shell ? commandArgs.join(" ") : command;
|
|
4151
4831
|
if (code === "ENOENT") {
|
|
4152
4832
|
error(
|
|
4153
|
-
`Command not found: ${
|
|
4833
|
+
`Command not found: ${displayCommand}. Make sure it is installed and on your PATH.`
|
|
4154
4834
|
);
|
|
4835
|
+
resolve3(127);
|
|
4155
4836
|
} else if (code === "EACCES") {
|
|
4156
|
-
error(`Permission denied executing: ${
|
|
4837
|
+
error(`Permission denied executing: ${displayCommand}`);
|
|
4838
|
+
resolve3(126);
|
|
4157
4839
|
} else {
|
|
4158
4840
|
error(`Failed to spawn process: ${err.message}`);
|
|
4841
|
+
resolve3(1);
|
|
4159
4842
|
}
|
|
4160
|
-
process.exit(1);
|
|
4161
4843
|
});
|
|
4162
4844
|
child.on("exit", (code, signal) => {
|
|
4163
4845
|
cleanup();
|
|
4164
4846
|
if (signal) {
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
} catch {
|
|
4168
|
-
process.exit(1);
|
|
4169
|
-
}
|
|
4847
|
+
const signum = osConstants.signals[signal] ?? 0;
|
|
4848
|
+
resolve3(128 + signum);
|
|
4170
4849
|
} else {
|
|
4171
|
-
|
|
4850
|
+
resolve3(code ?? 0);
|
|
4172
4851
|
}
|
|
4173
|
-
resolve3();
|
|
4174
4852
|
});
|
|
4175
4853
|
});
|
|
4176
4854
|
}
|
|
4177
4855
|
|
|
4178
4856
|
// src/commands/man.ts
|
|
4179
|
-
import { Command as
|
|
4180
|
-
import
|
|
4857
|
+
import { Command as Command15 } from "commander";
|
|
4858
|
+
import chalk16 from "chalk";
|
|
4181
4859
|
function printCommandManual(commandName) {
|
|
4182
4860
|
const command = findCommandDefinition(commandName);
|
|
4183
4861
|
if (!command) {
|
|
4184
|
-
console.log(
|
|
4862
|
+
console.log(chalk16.red(`Unknown command reference: ${commandName}`));
|
|
4185
4863
|
console.log();
|
|
4186
4864
|
console.log("Run `envpilot man` to see all supported commands.");
|
|
4187
4865
|
process.exit(1);
|
|
4188
4866
|
}
|
|
4189
|
-
console.log(
|
|
4867
|
+
console.log(chalk16.bold(formatArgv(command.argv)));
|
|
4190
4868
|
if (command.args) {
|
|
4191
|
-
console.log(
|
|
4869
|
+
console.log(chalk16.dim(command.args));
|
|
4192
4870
|
}
|
|
4193
4871
|
blank();
|
|
4194
4872
|
console.log(command.description);
|
|
4195
4873
|
blank();
|
|
4196
|
-
console.log(
|
|
4874
|
+
console.log(chalk16.green("Examples"));
|
|
4197
4875
|
for (const example of command.examples) {
|
|
4198
|
-
console.log(` ${
|
|
4876
|
+
console.log(` ${chalk16.cyan(formatArgv(example))}`);
|
|
4199
4877
|
}
|
|
4200
4878
|
blank();
|
|
4201
|
-
console.log(
|
|
4879
|
+
console.log(chalk16.green("Notes"));
|
|
4202
4880
|
for (const note of command.notes) {
|
|
4203
4881
|
console.log(` - ${note}`);
|
|
4204
4882
|
}
|
|
4205
4883
|
}
|
|
4206
4884
|
function printManualIndex(commands) {
|
|
4207
|
-
console.log(
|
|
4885
|
+
console.log(chalk16.bold("ENVPILOT(1)"));
|
|
4208
4886
|
console.log("TypeScript CLI manual");
|
|
4209
4887
|
blank();
|
|
4210
4888
|
console.log(
|
|
4211
|
-
`Total supported top-level commands: ${
|
|
4889
|
+
`Total supported top-level commands: ${chalk16.green(String(CLI_COMMAND_COUNT))}`
|
|
4212
4890
|
);
|
|
4213
4891
|
blank();
|
|
4214
4892
|
console.log(
|
|
@@ -4216,15 +4894,15 @@ function printManualIndex(commands) {
|
|
|
4216
4894
|
);
|
|
4217
4895
|
blank();
|
|
4218
4896
|
line();
|
|
4219
|
-
console.log(
|
|
4897
|
+
console.log(chalk16.green("Commands"));
|
|
4220
4898
|
for (const command of commands) {
|
|
4221
4899
|
console.log(
|
|
4222
|
-
` ${
|
|
4900
|
+
` ${chalk16.cyan(formatArgv(command.argv).padEnd(24))} ${chalk16.dim(command.description)}`
|
|
4223
4901
|
);
|
|
4224
4902
|
}
|
|
4225
4903
|
blank();
|
|
4226
4904
|
line();
|
|
4227
|
-
console.log(
|
|
4905
|
+
console.log(chalk16.green("Security"));
|
|
4228
4906
|
console.log(" - `.env*` is ignored by the repository `.gitignore`.");
|
|
4229
4907
|
console.log(
|
|
4230
4908
|
" - `envpilot init` and `envpilot sync` ensure env files are added to `.gitignore` locally."
|
|
@@ -4237,7 +4915,7 @@ function printManualIndex(commands) {
|
|
|
4237
4915
|
);
|
|
4238
4916
|
blank();
|
|
4239
4917
|
line();
|
|
4240
|
-
console.log(
|
|
4918
|
+
console.log(chalk16.green("Usage"));
|
|
4241
4919
|
console.log(
|
|
4242
4920
|
" - `envpilot` or `envpilot ui` opens the interactive terminal UI."
|
|
4243
4921
|
);
|
|
@@ -4246,7 +4924,7 @@ function printManualIndex(commands) {
|
|
|
4246
4924
|
);
|
|
4247
4925
|
}
|
|
4248
4926
|
function createManCommand(commands) {
|
|
4249
|
-
return new
|
|
4927
|
+
return new Command15("man").description("Show the CLI manual page").argument("[command]", "Optional command to show detailed manual for").action((command) => {
|
|
4250
4928
|
if (command) {
|
|
4251
4929
|
printCommandManual(command);
|
|
4252
4930
|
return;
|
|
@@ -4256,9 +4934,9 @@ function createManCommand(commands) {
|
|
|
4256
4934
|
}
|
|
4257
4935
|
|
|
4258
4936
|
// src/commands/ui.ts
|
|
4259
|
-
import { Command as
|
|
4937
|
+
import { Command as Command16 } from "commander";
|
|
4260
4938
|
function createUICommand() {
|
|
4261
|
-
return new
|
|
4939
|
+
return new Command16("ui").alias("dashboard").description("Open the interactive Ink-powered terminal UI").action(async () => {
|
|
4262
4940
|
if (process.env.ENVPILOT_TUI_CHILD === "1") {
|
|
4263
4941
|
return;
|
|
4264
4942
|
}
|
|
@@ -4266,6 +4944,443 @@ function createUICommand() {
|
|
|
4266
4944
|
});
|
|
4267
4945
|
}
|
|
4268
4946
|
|
|
4947
|
+
// src/commands/request.ts
|
|
4948
|
+
import { Command as Command17 } from "commander";
|
|
4949
|
+
import chalk17 from "chalk";
|
|
4950
|
+
import inquirer6 from "inquirer";
|
|
4951
|
+
|
|
4952
|
+
// src/lib/variable-requests.ts
|
|
4953
|
+
import { z as z3 } from "zod";
|
|
4954
|
+
var ALL_REQUEST_ENVIRONMENTS = [
|
|
4955
|
+
"development",
|
|
4956
|
+
"staging",
|
|
4957
|
+
"production"
|
|
4958
|
+
];
|
|
4959
|
+
var requestKeySchema = z3.string().min(1, "Key is required").max(100, "Key must be 100 characters or less").regex(
|
|
4960
|
+
/^[A-Z][A-Z0-9_]*$/,
|
|
4961
|
+
"Key must be uppercase, start with a letter, and contain only letters, numbers, and underscores"
|
|
4962
|
+
);
|
|
4963
|
+
var requestValueSchema = z3.string().min(1, "Value is required");
|
|
4964
|
+
var requestDescriptionSchema = z3.string().max(500, "Description must be 500 characters or less");
|
|
4965
|
+
function validateRequestKey(key) {
|
|
4966
|
+
const result = requestKeySchema.safeParse(key);
|
|
4967
|
+
if (result.success) return { valid: true };
|
|
4968
|
+
return {
|
|
4969
|
+
valid: false,
|
|
4970
|
+
error: result.error.issues[0]?.message ?? "Invalid key"
|
|
4971
|
+
};
|
|
4972
|
+
}
|
|
4973
|
+
function validateRequestValue(value) {
|
|
4974
|
+
const result = requestValueSchema.safeParse(value);
|
|
4975
|
+
if (result.success) return { valid: true };
|
|
4976
|
+
return {
|
|
4977
|
+
valid: false,
|
|
4978
|
+
error: result.error.issues[0]?.message ?? "Invalid value"
|
|
4979
|
+
};
|
|
4980
|
+
}
|
|
4981
|
+
function validateRequestDescription(description) {
|
|
4982
|
+
if (description.length === 0) return { valid: true };
|
|
4983
|
+
const result = requestDescriptionSchema.safeParse(description);
|
|
4984
|
+
if (result.success) return { valid: true };
|
|
4985
|
+
return {
|
|
4986
|
+
valid: false,
|
|
4987
|
+
error: result.error.issues[0]?.message ?? "Invalid description"
|
|
4988
|
+
};
|
|
4989
|
+
}
|
|
4990
|
+
function allowedRequestEnvironments(environmentScope) {
|
|
4991
|
+
if (environmentScope == null) {
|
|
4992
|
+
return [...ALL_REQUEST_ENVIRONMENTS];
|
|
4993
|
+
}
|
|
4994
|
+
return ALL_REQUEST_ENVIRONMENTS.filter(
|
|
4995
|
+
(env) => environmentScope.includes(env)
|
|
4996
|
+
);
|
|
4997
|
+
}
|
|
4998
|
+
function buildCreateVariableRequestBody(input) {
|
|
4999
|
+
const description = input.description?.trim();
|
|
5000
|
+
return {
|
|
5001
|
+
projectId: input.projectId,
|
|
5002
|
+
key: input.key.trim(),
|
|
5003
|
+
value: input.value,
|
|
5004
|
+
...description ? { description } : {},
|
|
5005
|
+
environments: input.environments,
|
|
5006
|
+
isSensitive: input.isSensitive ?? false
|
|
5007
|
+
};
|
|
5008
|
+
}
|
|
5009
|
+
function isRequestEligibleProject(project) {
|
|
5010
|
+
const role = normalizeOrgRole(project.unifiedRole ?? project.role);
|
|
5011
|
+
return role === "developer" && project.assigned === true;
|
|
5012
|
+
}
|
|
5013
|
+
function buildEligibleRequestTargets(projects, orgNameById) {
|
|
5014
|
+
return projects.filter(isRequestEligibleProject).map((p) => ({
|
|
5015
|
+
projectId: p._id,
|
|
5016
|
+
projectName: p.name,
|
|
5017
|
+
organizationId: p.organizationId,
|
|
5018
|
+
organizationName: orgNameById[p.organizationId] ?? p.organizationId,
|
|
5019
|
+
environmentScope: p.environmentScope ?? null
|
|
5020
|
+
}));
|
|
5021
|
+
}
|
|
5022
|
+
function formatProjectChoiceLabel(target) {
|
|
5023
|
+
return `${target.projectName} \u2014 ${target.organizationName}`;
|
|
5024
|
+
}
|
|
5025
|
+
function buildProjectChoices(targets) {
|
|
5026
|
+
return targets.map((t) => ({
|
|
5027
|
+
name: formatProjectChoiceLabel(t),
|
|
5028
|
+
value: t.projectId
|
|
5029
|
+
}));
|
|
5030
|
+
}
|
|
5031
|
+
function formatRequestContextBanner(input) {
|
|
5032
|
+
return `Requesting as ${input.email}
|
|
5033
|
+
Project: ${input.projectName} \xB7 Organization: ${input.organizationName}`;
|
|
5034
|
+
}
|
|
5035
|
+
var SUMMARY_LABEL_WIDTH = 15;
|
|
5036
|
+
function formatRequestSummary(input) {
|
|
5037
|
+
const rows = [
|
|
5038
|
+
["Key:", input.key],
|
|
5039
|
+
[
|
|
5040
|
+
"Project:",
|
|
5041
|
+
formatProjectChoiceLabel({
|
|
5042
|
+
projectName: input.projectName,
|
|
5043
|
+
organizationName: input.organizationName
|
|
5044
|
+
})
|
|
5045
|
+
],
|
|
5046
|
+
["Environments:", input.environments.join(", ")],
|
|
5047
|
+
["Sensitive:", input.isSensitive ? "yes" : "no"]
|
|
5048
|
+
];
|
|
5049
|
+
return rows.map(([label, value]) => `${label.padEnd(SUMMARY_LABEL_WIDTH)}${value}`).join("\n");
|
|
5050
|
+
}
|
|
5051
|
+
function formatRequestSuccessMessage(input) {
|
|
5052
|
+
return `Request "${input.key}" submitted for ${input.projectName} (${input.organizationName}) \u2014 pending review.`;
|
|
5053
|
+
}
|
|
5054
|
+
function formatRequestsListHeader(input) {
|
|
5055
|
+
return `Requests for ${formatProjectChoiceLabel(input)}`;
|
|
5056
|
+
}
|
|
5057
|
+
function formatRequestRow(request) {
|
|
5058
|
+
return {
|
|
5059
|
+
key: request.key,
|
|
5060
|
+
environments: request.environments.join(", "),
|
|
5061
|
+
status: request.status,
|
|
5062
|
+
requested: new Date(request.createdAt).toLocaleDateString(),
|
|
5063
|
+
reason: request.reviewReason ?? ""
|
|
5064
|
+
};
|
|
5065
|
+
}
|
|
5066
|
+
function formatRequestRows(requests) {
|
|
5067
|
+
return requests.map(formatRequestRow);
|
|
5068
|
+
}
|
|
5069
|
+
|
|
5070
|
+
// src/commands/request.ts
|
|
5071
|
+
var requestCommand = new Command17("request").description(
|
|
5072
|
+
"Request creation of a new environment variable (developers only \u2014 owners, project managers, and team leads create variables directly)"
|
|
5073
|
+
).option(
|
|
5074
|
+
"--project <name-or-id>",
|
|
5075
|
+
"Submit the request for a specific linked project"
|
|
5076
|
+
).action(async (options) => {
|
|
5077
|
+
try {
|
|
5078
|
+
if (!isAuthenticated()) {
|
|
5079
|
+
throw notAuthenticated();
|
|
5080
|
+
}
|
|
5081
|
+
const email = getUser()?.email ?? "unknown account";
|
|
5082
|
+
const api = createAPIClient();
|
|
5083
|
+
const projectPassed = Boolean(options.project);
|
|
5084
|
+
const defaultEntry = resolveDefaultEntry(options.project);
|
|
5085
|
+
let target = null;
|
|
5086
|
+
let scope;
|
|
5087
|
+
if (defaultEntry) {
|
|
5088
|
+
const names = await resolveEntryNames(api, defaultEntry);
|
|
5089
|
+
const defaultTarget = {
|
|
5090
|
+
projectId: defaultEntry.projectId,
|
|
5091
|
+
projectName: names.projectName,
|
|
5092
|
+
organizationId: defaultEntry.organizationId,
|
|
5093
|
+
organizationName: names.organizationName
|
|
5094
|
+
};
|
|
5095
|
+
printBanner(email, defaultTarget);
|
|
5096
|
+
let useDefault = projectPassed;
|
|
5097
|
+
if (!projectPassed) {
|
|
5098
|
+
const { proceed } = await inquirer6.prompt([
|
|
5099
|
+
{
|
|
5100
|
+
type: "confirm",
|
|
5101
|
+
name: "proceed",
|
|
5102
|
+
message: "Request a variable for this project?",
|
|
5103
|
+
default: true
|
|
5104
|
+
}
|
|
5105
|
+
]);
|
|
5106
|
+
useDefault = proceed;
|
|
5107
|
+
}
|
|
5108
|
+
if (useDefault) {
|
|
5109
|
+
const meta = await fetchProjectMeta(api, defaultEntry);
|
|
5110
|
+
const role = normalizeOrgRole(meta?.unifiedRole ?? meta?.role);
|
|
5111
|
+
if (role !== "developer") {
|
|
5112
|
+
warning(
|
|
5113
|
+
"You have direct write access to this project. Use `envpilot push` or create the variable directly instead of submitting a request."
|
|
5114
|
+
);
|
|
5115
|
+
return;
|
|
5116
|
+
}
|
|
5117
|
+
target = defaultTarget;
|
|
5118
|
+
scope = meta?.environmentScope;
|
|
5119
|
+
}
|
|
5120
|
+
}
|
|
5121
|
+
if (!target) {
|
|
5122
|
+
const picked = await pickRequestTarget(api);
|
|
5123
|
+
if (!picked) {
|
|
5124
|
+
info(
|
|
5125
|
+
"You have direct write access in your projects \u2014 create variables from the dashboard."
|
|
5126
|
+
);
|
|
5127
|
+
process.exit(0);
|
|
5128
|
+
}
|
|
5129
|
+
target = picked;
|
|
5130
|
+
scope = picked.environmentScope;
|
|
5131
|
+
printBanner(email, target);
|
|
5132
|
+
}
|
|
5133
|
+
const envChoices = allowedRequestEnvironments(scope);
|
|
5134
|
+
if (envChoices.length === 0) {
|
|
5135
|
+
error(
|
|
5136
|
+
"Your assignment does not include any environments in this project."
|
|
5137
|
+
);
|
|
5138
|
+
process.exit(1);
|
|
5139
|
+
}
|
|
5140
|
+
const answers = await inquirer6.prompt([
|
|
5141
|
+
{
|
|
5142
|
+
type: "input",
|
|
5143
|
+
name: "key",
|
|
5144
|
+
message: "Variable key (e.g. API_SECRET):",
|
|
5145
|
+
validate: (input) => {
|
|
5146
|
+
const result = validateRequestKey(input);
|
|
5147
|
+
return result.valid || result.error;
|
|
5148
|
+
}
|
|
5149
|
+
},
|
|
5150
|
+
{
|
|
5151
|
+
type: "password",
|
|
5152
|
+
name: "value",
|
|
5153
|
+
message: "Variable value:",
|
|
5154
|
+
mask: "*",
|
|
5155
|
+
validate: (input) => {
|
|
5156
|
+
const result = validateRequestValue(input);
|
|
5157
|
+
return result.valid || result.error;
|
|
5158
|
+
}
|
|
5159
|
+
},
|
|
5160
|
+
{
|
|
5161
|
+
type: "input",
|
|
5162
|
+
name: "description",
|
|
5163
|
+
message: "Description (optional):",
|
|
5164
|
+
default: "",
|
|
5165
|
+
validate: (input) => {
|
|
5166
|
+
const result = validateRequestDescription(input);
|
|
5167
|
+
return result.valid || result.error;
|
|
5168
|
+
}
|
|
5169
|
+
},
|
|
5170
|
+
{
|
|
5171
|
+
type: "checkbox",
|
|
5172
|
+
name: "environments",
|
|
5173
|
+
message: "Environments:",
|
|
5174
|
+
choices: envChoices,
|
|
5175
|
+
validate: (input) => input.length > 0 || "Select at least one environment"
|
|
5176
|
+
},
|
|
5177
|
+
{
|
|
5178
|
+
type: "confirm",
|
|
5179
|
+
name: "isSensitive",
|
|
5180
|
+
message: "Is this value sensitive?",
|
|
5181
|
+
default: true
|
|
5182
|
+
}
|
|
5183
|
+
]);
|
|
5184
|
+
console.log();
|
|
5185
|
+
console.log(
|
|
5186
|
+
formatRequestSummary({
|
|
5187
|
+
key: answers.key.trim(),
|
|
5188
|
+
projectName: target.projectName,
|
|
5189
|
+
organizationName: target.organizationName,
|
|
5190
|
+
environments: answers.environments,
|
|
5191
|
+
isSensitive: answers.isSensitive
|
|
5192
|
+
})
|
|
5193
|
+
);
|
|
5194
|
+
console.log();
|
|
5195
|
+
const { submit } = await inquirer6.prompt([
|
|
5196
|
+
{
|
|
5197
|
+
type: "confirm",
|
|
5198
|
+
name: "submit",
|
|
5199
|
+
message: "Submit this request?",
|
|
5200
|
+
default: true
|
|
5201
|
+
}
|
|
5202
|
+
]);
|
|
5203
|
+
if (!submit) {
|
|
5204
|
+
info("Request canceled.");
|
|
5205
|
+
return;
|
|
5206
|
+
}
|
|
5207
|
+
const body = buildCreateVariableRequestBody({
|
|
5208
|
+
projectId: target.projectId,
|
|
5209
|
+
key: answers.key,
|
|
5210
|
+
value: answers.value,
|
|
5211
|
+
description: answers.description,
|
|
5212
|
+
environments: answers.environments,
|
|
5213
|
+
isSensitive: answers.isSensitive
|
|
5214
|
+
});
|
|
5215
|
+
const created = await api.createVariableRequest(body);
|
|
5216
|
+
success(
|
|
5217
|
+
formatRequestSuccessMessage({
|
|
5218
|
+
key: created.key,
|
|
5219
|
+
projectName: target.projectName,
|
|
5220
|
+
organizationName: target.organizationName
|
|
5221
|
+
})
|
|
5222
|
+
);
|
|
5223
|
+
info(`Status: ${created.status} \xB7 Id: ${created._id}`);
|
|
5224
|
+
} catch (err) {
|
|
5225
|
+
if (err instanceof APIError && err.statusCode === 403) {
|
|
5226
|
+
error(err.message);
|
|
5227
|
+
return;
|
|
5228
|
+
}
|
|
5229
|
+
await handleError(err);
|
|
5230
|
+
}
|
|
5231
|
+
});
|
|
5232
|
+
function resolveDefaultEntry(projectOption) {
|
|
5233
|
+
const configV2 = readProjectConfigV2();
|
|
5234
|
+
if (!configV2) {
|
|
5235
|
+
if (projectOption) throw notInitialized();
|
|
5236
|
+
return null;
|
|
5237
|
+
}
|
|
5238
|
+
const project = resolveProject(configV2, projectOption);
|
|
5239
|
+
if (!project) {
|
|
5240
|
+
if (projectOption) {
|
|
5241
|
+
error(`Project not found: ${projectOption}`);
|
|
5242
|
+
console.log();
|
|
5243
|
+
console.log("Linked projects:");
|
|
5244
|
+
for (const p of configV2.projects) {
|
|
5245
|
+
console.log(` ${p.projectName || p.projectId} (${p.environment})`);
|
|
5246
|
+
}
|
|
5247
|
+
process.exit(1);
|
|
5248
|
+
}
|
|
5249
|
+
return null;
|
|
5250
|
+
}
|
|
5251
|
+
return project;
|
|
5252
|
+
}
|
|
5253
|
+
function printBanner(email, target) {
|
|
5254
|
+
console.log();
|
|
5255
|
+
console.log(
|
|
5256
|
+
chalk17.dim(
|
|
5257
|
+
formatRequestContextBanner({
|
|
5258
|
+
email,
|
|
5259
|
+
projectName: target.projectName,
|
|
5260
|
+
organizationName: target.organizationName
|
|
5261
|
+
})
|
|
5262
|
+
)
|
|
5263
|
+
);
|
|
5264
|
+
console.log();
|
|
5265
|
+
}
|
|
5266
|
+
async function resolveEntryNames(api, entry) {
|
|
5267
|
+
let projectName = entry.projectName;
|
|
5268
|
+
let organizationName = entry.organizationName;
|
|
5269
|
+
if (!projectName) {
|
|
5270
|
+
try {
|
|
5271
|
+
const project = await api.getProject(entry.projectId);
|
|
5272
|
+
projectName = project.name;
|
|
5273
|
+
} catch {
|
|
5274
|
+
}
|
|
5275
|
+
}
|
|
5276
|
+
if (!organizationName) {
|
|
5277
|
+
try {
|
|
5278
|
+
const orgs = await api.listOrganizations();
|
|
5279
|
+
organizationName = orgs.find((o) => o._id === entry.organizationId)?.name ?? "";
|
|
5280
|
+
} catch {
|
|
5281
|
+
}
|
|
5282
|
+
}
|
|
5283
|
+
return {
|
|
5284
|
+
projectName: projectName || entry.projectId,
|
|
5285
|
+
organizationName: organizationName || entry.organizationId
|
|
5286
|
+
};
|
|
5287
|
+
}
|
|
5288
|
+
async function fetchProjectMeta(api, entry) {
|
|
5289
|
+
const response = await api.get("/api/cli/variables", {
|
|
5290
|
+
projectId: entry.projectId,
|
|
5291
|
+
...entry.organizationId && { organizationId: entry.organizationId }
|
|
5292
|
+
});
|
|
5293
|
+
return response.meta;
|
|
5294
|
+
}
|
|
5295
|
+
async function pickRequestTarget(api) {
|
|
5296
|
+
const targets = await withSpinner(
|
|
5297
|
+
"Finding projects you can request for...",
|
|
5298
|
+
async () => {
|
|
5299
|
+
const orgs = await api.listOrganizations();
|
|
5300
|
+
const orgNameById = Object.fromEntries(orgs.map((o) => [o._id, o.name]));
|
|
5301
|
+
const perOrg = await Promise.all(
|
|
5302
|
+
orgs.map((org) => api.listProjects(org._id))
|
|
5303
|
+
);
|
|
5304
|
+
return perOrg.flatMap(
|
|
5305
|
+
(projects) => buildEligibleRequestTargets(projects, orgNameById)
|
|
5306
|
+
);
|
|
5307
|
+
}
|
|
5308
|
+
);
|
|
5309
|
+
if (targets.length === 0) return null;
|
|
5310
|
+
const { projectId } = await inquirer6.prompt([
|
|
5311
|
+
{
|
|
5312
|
+
type: "list",
|
|
5313
|
+
name: "projectId",
|
|
5314
|
+
message: "Select a project to request for:",
|
|
5315
|
+
choices: buildProjectChoices(targets)
|
|
5316
|
+
}
|
|
5317
|
+
]);
|
|
5318
|
+
return targets.find((t) => t.projectId === projectId) ?? null;
|
|
5319
|
+
}
|
|
5320
|
+
|
|
5321
|
+
// src/commands/requests.ts
|
|
5322
|
+
import { Command as Command18 } from "commander";
|
|
5323
|
+
var requestsCommand = new Command18("requests").description("List variable requests for a project").option(
|
|
5324
|
+
"--project <name-or-id>",
|
|
5325
|
+
"List requests for a specific linked project"
|
|
5326
|
+
).option(
|
|
5327
|
+
"--status <status>",
|
|
5328
|
+
"Filter by status: pending, approved, rejected, canceled"
|
|
5329
|
+
).action(async (options) => {
|
|
5330
|
+
try {
|
|
5331
|
+
if (!isAuthenticated()) {
|
|
5332
|
+
throw notAuthenticated();
|
|
5333
|
+
}
|
|
5334
|
+
const configV2 = readProjectConfigV2();
|
|
5335
|
+
if (!configV2) throw notInitialized();
|
|
5336
|
+
const project = resolveProject(configV2, options.project);
|
|
5337
|
+
if (!project) {
|
|
5338
|
+
error(`Project not found: ${options.project}`);
|
|
5339
|
+
console.log();
|
|
5340
|
+
console.log("Linked projects:");
|
|
5341
|
+
for (const p of configV2.projects) {
|
|
5342
|
+
console.log(` ${p.projectName || p.projectId} (${p.environment})`);
|
|
5343
|
+
}
|
|
5344
|
+
process.exit(1);
|
|
5345
|
+
}
|
|
5346
|
+
let status;
|
|
5347
|
+
if (options.status) {
|
|
5348
|
+
const parsed = variableRequestStatusSchema.safeParse(options.status);
|
|
5349
|
+
if (!parsed.success) {
|
|
5350
|
+
error(
|
|
5351
|
+
`Invalid status: ${options.status}. Must be one of pending, approved, rejected, canceled.`
|
|
5352
|
+
);
|
|
5353
|
+
process.exit(1);
|
|
5354
|
+
}
|
|
5355
|
+
status = parsed.data;
|
|
5356
|
+
}
|
|
5357
|
+
const api = createAPIClient();
|
|
5358
|
+
const requests = await withSpinner(
|
|
5359
|
+
"Fetching variable requests...",
|
|
5360
|
+
() => api.listVariableRequests(project.projectId, status)
|
|
5361
|
+
);
|
|
5362
|
+
header(
|
|
5363
|
+
formatRequestsListHeader({
|
|
5364
|
+
projectName: project.projectName || project.projectId,
|
|
5365
|
+
organizationName: project.organizationName || project.organizationId
|
|
5366
|
+
})
|
|
5367
|
+
);
|
|
5368
|
+
if (requests.length === 0) {
|
|
5369
|
+
info("No variable requests found.");
|
|
5370
|
+
return;
|
|
5371
|
+
}
|
|
5372
|
+
table(formatRequestRows(requests), [
|
|
5373
|
+
{ key: "key", header: "KEY" },
|
|
5374
|
+
{ key: "environments", header: "ENVIRONMENTS" },
|
|
5375
|
+
{ key: "status", header: "STATUS" },
|
|
5376
|
+
{ key: "requested", header: "REQUESTED" },
|
|
5377
|
+
{ key: "reason", header: "REASON" }
|
|
5378
|
+
]);
|
|
5379
|
+
} catch (err) {
|
|
5380
|
+
await handleError(err);
|
|
5381
|
+
}
|
|
5382
|
+
});
|
|
5383
|
+
|
|
4269
5384
|
// src/lib/command-catalog.ts
|
|
4270
5385
|
function formatArgv(argv) {
|
|
4271
5386
|
return ["envpilot", ...argv].join(" ").trim();
|
|
@@ -4395,19 +5510,55 @@ var COMMAND_CATALOG = [
|
|
|
4395
5510
|
id: "push",
|
|
4396
5511
|
title: "Push variables",
|
|
4397
5512
|
category: "Sync",
|
|
4398
|
-
description: "Upload local variables back to Envpilot
|
|
5513
|
+
description: "Upload local variables back to Envpilot, writing only the keys you have access to.",
|
|
4399
5514
|
argv: ["push"],
|
|
4400
5515
|
args: "[--env <environment>] [--file <path>] [--merge|--replace]",
|
|
4401
5516
|
examples: [["push"], ["push", "--replace"]],
|
|
4402
5517
|
websiteSurface: "Maps to `/api/cli/variables` and `/api/cli/variables/bulk` for writes.",
|
|
4403
5518
|
notes: [
|
|
4404
|
-
"
|
|
5519
|
+
"Owners, project managers, and team leads write across the project; developers write only the variables they hold a write grant for.",
|
|
5520
|
+
"Keys you cannot write are skipped \u2014 push does not create approval requests.",
|
|
4405
5521
|
"Compares local and remote variables before applying changes."
|
|
4406
5522
|
],
|
|
4407
5523
|
keywords: ["upload", "bulk", "merge", "replace"],
|
|
4408
5524
|
topLevel: true,
|
|
4409
5525
|
createCommand: () => pushCommand
|
|
4410
5526
|
},
|
|
5527
|
+
{
|
|
5528
|
+
id: "request",
|
|
5529
|
+
title: "Request a new variable",
|
|
5530
|
+
category: "Sync",
|
|
5531
|
+
description: "Submit a request to create a new environment variable for review (developers only).",
|
|
5532
|
+
argv: ["request"],
|
|
5533
|
+
args: "[--project <name-or-id>]",
|
|
5534
|
+
examples: [["request"], ["request", "--project", "api"]],
|
|
5535
|
+
websiteSurface: "Maps to `/api/cli/variable-requests` (POST).",
|
|
5536
|
+
notes: [
|
|
5537
|
+
"Only assigned developers can submit requests \u2014 owners, project managers, and team leads create variables directly.",
|
|
5538
|
+
"Environment choices are limited to the developer's assigned environment scope.",
|
|
5539
|
+
"An owner, project manager, or team lead must approve the request before the variable is created."
|
|
5540
|
+
],
|
|
5541
|
+
keywords: ["request", "approval", "developer", "create"],
|
|
5542
|
+
topLevel: true,
|
|
5543
|
+
createCommand: () => requestCommand
|
|
5544
|
+
},
|
|
5545
|
+
{
|
|
5546
|
+
id: "requests",
|
|
5547
|
+
title: "List variable requests",
|
|
5548
|
+
category: "Browse",
|
|
5549
|
+
description: "List pending and past variable requests for a project.",
|
|
5550
|
+
argv: ["requests"],
|
|
5551
|
+
args: "[--project <name-or-id>] [--status <status>]",
|
|
5552
|
+
examples: [["requests"], ["requests", "--status", "pending"]],
|
|
5553
|
+
websiteSurface: "Maps to `/api/cli/variable-requests` (GET).",
|
|
5554
|
+
notes: [
|
|
5555
|
+
"Reviewers (owner, assigned project manager/team lead) see every request for the project.",
|
|
5556
|
+
"Developers see only their own requests."
|
|
5557
|
+
],
|
|
5558
|
+
keywords: ["requests", "approval", "review", "pending"],
|
|
5559
|
+
topLevel: true,
|
|
5560
|
+
createCommand: () => requestsCommand
|
|
5561
|
+
},
|
|
4411
5562
|
{
|
|
4412
5563
|
id: "run",
|
|
4413
5564
|
title: "Run command with secrets",
|
|
@@ -4577,6 +5728,27 @@ var COMMAND_CATALOG = [
|
|
|
4577
5728
|
topLevel: true,
|
|
4578
5729
|
createCommand: () => whoamiCommand
|
|
4579
5730
|
},
|
|
5731
|
+
{
|
|
5732
|
+
id: "accounts",
|
|
5733
|
+
title: "Manage accounts",
|
|
5734
|
+
category: "Account",
|
|
5735
|
+
description: "List authenticated accounts and switch or remove them without logging out.",
|
|
5736
|
+
argv: ["accounts"],
|
|
5737
|
+
args: "[list|switch <identifier>|remove <identifier>]",
|
|
5738
|
+
examples: [
|
|
5739
|
+
["accounts"],
|
|
5740
|
+
["accounts", "switch", "you@example.com"],
|
|
5741
|
+
["accounts", "remove", "you@example.com"]
|
|
5742
|
+
],
|
|
5743
|
+
websiteSurface: "Local multi-account state \u2014 each `envpilot login` adds an account instead of replacing the current session.",
|
|
5744
|
+
notes: [
|
|
5745
|
+
"Identifiers can be an account id or the account's email (case-insensitive).",
|
|
5746
|
+
"Switching accounts does not log anyone out; use `envpilot logout` to remove the active session."
|
|
5747
|
+
],
|
|
5748
|
+
keywords: ["accounts", "multi-account", "switch", "remove", "identity"],
|
|
5749
|
+
topLevel: true,
|
|
5750
|
+
createCommand: () => accountsCommand
|
|
5751
|
+
},
|
|
4580
5752
|
{
|
|
4581
5753
|
id: "config",
|
|
4582
5754
|
title: "Config",
|