@envpilot/cli 1.7.2 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -7,7 +7,7 @@ function initSentry() {
|
|
|
7
7
|
Sentry.init({
|
|
8
8
|
dsn,
|
|
9
9
|
environment: "cli",
|
|
10
|
-
release: true ? "1.
|
|
10
|
+
release: true ? "1.9.1" : "0.0.0",
|
|
11
11
|
// Free tier: disable performance monitoring
|
|
12
12
|
tracesSampleRate: 0,
|
|
13
13
|
beforeSend(event) {
|
|
@@ -48,6 +48,195 @@ async function flushSentry() {
|
|
|
48
48
|
|
|
49
49
|
// src/lib/config.ts
|
|
50
50
|
import Conf from "conf";
|
|
51
|
+
|
|
52
|
+
// src/lib/roles.ts
|
|
53
|
+
var ROLE_LEVEL = {
|
|
54
|
+
owner: 4,
|
|
55
|
+
project_manager: 3,
|
|
56
|
+
team_lead: 2,
|
|
57
|
+
developer: 1
|
|
58
|
+
};
|
|
59
|
+
var ORG_ROLE_LABELS = {
|
|
60
|
+
owner: "Owner",
|
|
61
|
+
project_manager: "Project Manager",
|
|
62
|
+
team_lead: "Team Lead",
|
|
63
|
+
developer: "Developer"
|
|
64
|
+
};
|
|
65
|
+
function normalizeOrgRole(role) {
|
|
66
|
+
switch (role) {
|
|
67
|
+
case "admin":
|
|
68
|
+
return "owner";
|
|
69
|
+
case "member":
|
|
70
|
+
return "developer";
|
|
71
|
+
case "owner":
|
|
72
|
+
case "project_manager":
|
|
73
|
+
case "team_lead":
|
|
74
|
+
case "developer":
|
|
75
|
+
return role;
|
|
76
|
+
default:
|
|
77
|
+
return "developer";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function roleLevel(role) {
|
|
81
|
+
return ROLE_LEVEL[normalizeOrgRole(role)];
|
|
82
|
+
}
|
|
83
|
+
function formatRoleLabel(role) {
|
|
84
|
+
return ORG_ROLE_LABELS[normalizeOrgRole(role)];
|
|
85
|
+
}
|
|
86
|
+
function isFileWritable(access) {
|
|
87
|
+
if (access.role === "owner") return true;
|
|
88
|
+
if (!access.assigned) return false;
|
|
89
|
+
switch (access.role) {
|
|
90
|
+
case "project_manager":
|
|
91
|
+
case "team_lead":
|
|
92
|
+
return true;
|
|
93
|
+
case "developer":
|
|
94
|
+
return access.hasWriteAccess;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/lib/variables-cache.ts
|
|
99
|
+
import { createHash } from "crypto";
|
|
100
|
+
import {
|
|
101
|
+
readFileSync,
|
|
102
|
+
writeFileSync,
|
|
103
|
+
mkdirSync,
|
|
104
|
+
existsSync,
|
|
105
|
+
readdirSync,
|
|
106
|
+
unlinkSync,
|
|
107
|
+
rmdirSync,
|
|
108
|
+
statSync
|
|
109
|
+
} from "fs";
|
|
110
|
+
import { join, dirname } from "path";
|
|
111
|
+
var HARD_MAX_AGE_MS = 12 * 60 * 60 * 1e3;
|
|
112
|
+
function getCacheDir() {
|
|
113
|
+
return join(dirname(getConfigPath()), "run-cache");
|
|
114
|
+
}
|
|
115
|
+
function getCacheKey(projectId, environment, organizationId) {
|
|
116
|
+
const tokenSlice = (getAccessToken() ?? "").slice(0, 16);
|
|
117
|
+
return createHash("sha256").update(`${projectId}:${environment}:${organizationId}:${tokenSlice}`).digest("hex").slice(0, 20);
|
|
118
|
+
}
|
|
119
|
+
function getCachePath(key) {
|
|
120
|
+
return join(getCacheDir(), `${key}.json`);
|
|
121
|
+
}
|
|
122
|
+
function computeFingerprint(variables) {
|
|
123
|
+
return createHash("sha256").update(
|
|
124
|
+
variables.map((v) => `${v._id}:${v.version}:${v.updatedAt}`).sort().join("|")
|
|
125
|
+
).digest("hex").slice(0, 16);
|
|
126
|
+
}
|
|
127
|
+
function readCache(projectId, environment, organizationId) {
|
|
128
|
+
try {
|
|
129
|
+
const key = getCacheKey(projectId, environment, organizationId);
|
|
130
|
+
const path = getCachePath(key);
|
|
131
|
+
if (!existsSync(path)) return null;
|
|
132
|
+
const raw = readFileSync(path, "utf-8");
|
|
133
|
+
const entry = JSON.parse(raw);
|
|
134
|
+
if (entry.apiUrl !== getApiUrl()) return null;
|
|
135
|
+
if (!isWithinHardMaxAge(entry)) {
|
|
136
|
+
try {
|
|
137
|
+
unlinkSync(path);
|
|
138
|
+
} catch {
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
return entry;
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function probeCache(projectId, environment, organizationId, ttlSeconds) {
|
|
148
|
+
const entry = readCache(projectId, environment, organizationId);
|
|
149
|
+
if (!entry) return { hit: false };
|
|
150
|
+
const fresh = isFresh(entry, ttlSeconds);
|
|
151
|
+
return { hit: true, fresh, entry };
|
|
152
|
+
}
|
|
153
|
+
function writeCache(projectId, environment, organizationId, variables, serverFingerprint) {
|
|
154
|
+
try {
|
|
155
|
+
const cacheDir = getCacheDir();
|
|
156
|
+
mkdirSync(cacheDir, { recursive: true, mode: 448 });
|
|
157
|
+
const key = getCacheKey(projectId, environment, organizationId);
|
|
158
|
+
const path = getCachePath(key);
|
|
159
|
+
const now = Date.now();
|
|
160
|
+
const entry = {
|
|
161
|
+
variables,
|
|
162
|
+
fetchedAt: now,
|
|
163
|
+
firstFetchedAt: now,
|
|
164
|
+
projectId,
|
|
165
|
+
environment,
|
|
166
|
+
organizationId,
|
|
167
|
+
apiUrl: getApiUrl(),
|
|
168
|
+
fingerprint: serverFingerprint ?? computeFingerprint(variables)
|
|
169
|
+
};
|
|
170
|
+
writeFileSync(path, JSON.stringify(entry, null, 2), {
|
|
171
|
+
encoding: "utf-8",
|
|
172
|
+
mode: 384
|
|
173
|
+
// owner read/write only — same as .env
|
|
174
|
+
});
|
|
175
|
+
} catch {
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function extendCacheFreshness(projectId, environment, organizationId) {
|
|
179
|
+
try {
|
|
180
|
+
const key = getCacheKey(projectId, environment, organizationId);
|
|
181
|
+
const path = getCachePath(key);
|
|
182
|
+
if (!existsSync(path)) return;
|
|
183
|
+
const raw = readFileSync(path, "utf-8");
|
|
184
|
+
const entry = JSON.parse(raw);
|
|
185
|
+
if (!isWithinHardMaxAge(entry)) return;
|
|
186
|
+
if (entry.firstFetchedAt === void 0)
|
|
187
|
+
entry.firstFetchedAt = entry.fetchedAt;
|
|
188
|
+
entry.fetchedAt = Date.now();
|
|
189
|
+
writeFileSync(path, JSON.stringify(entry, null, 2), {
|
|
190
|
+
encoding: "utf-8",
|
|
191
|
+
mode: 384
|
|
192
|
+
});
|
|
193
|
+
} catch {
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function clearAllCache() {
|
|
197
|
+
let count = 0;
|
|
198
|
+
try {
|
|
199
|
+
const dir = getCacheDir();
|
|
200
|
+
if (!existsSync(dir)) return 0;
|
|
201
|
+
for (const file of readdirSync(dir)) {
|
|
202
|
+
if (!file.endsWith(".json")) continue;
|
|
203
|
+
try {
|
|
204
|
+
unlinkSync(join(dir, file));
|
|
205
|
+
count++;
|
|
206
|
+
} catch {
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
} catch {
|
|
210
|
+
}
|
|
211
|
+
return count;
|
|
212
|
+
}
|
|
213
|
+
function clearRunCache() {
|
|
214
|
+
const removed = clearAllCache();
|
|
215
|
+
try {
|
|
216
|
+
const dir = getCacheDir();
|
|
217
|
+
if (existsSync(dir) && readdirSync(dir).length === 0) {
|
|
218
|
+
rmdirSync(dir);
|
|
219
|
+
}
|
|
220
|
+
} catch {
|
|
221
|
+
}
|
|
222
|
+
return removed;
|
|
223
|
+
}
|
|
224
|
+
function isFresh(entry, ttlSeconds) {
|
|
225
|
+
return Date.now() - entry.fetchedAt < ttlSeconds * 1e3;
|
|
226
|
+
}
|
|
227
|
+
function isWithinHardMaxAge(entry) {
|
|
228
|
+
const anchor = entry.firstFetchedAt ?? entry.fetchedAt;
|
|
229
|
+
return Date.now() - anchor < HARD_MAX_AGE_MS;
|
|
230
|
+
}
|
|
231
|
+
function formatAge(fetchedAt) {
|
|
232
|
+
const ageSec = Math.floor((Date.now() - fetchedAt) / 1e3);
|
|
233
|
+
if (ageSec < 60) return `${ageSec}s ago`;
|
|
234
|
+
const ageMin = Math.floor(ageSec / 60);
|
|
235
|
+
if (ageMin < 60) return `${ageMin}m ago`;
|
|
236
|
+
return `${Math.floor(ageMin / 60)}h ago`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/lib/config.ts
|
|
51
240
|
var DEFAULT_API_URL = "https://www.envpilot.dev";
|
|
52
241
|
var HOST_CANONICALIZATION = {
|
|
53
242
|
"envpilot.dev": "www.envpilot.dev"
|
|
@@ -75,15 +264,116 @@ var config = new Conf({
|
|
|
75
264
|
apiUrl: DEFAULT_API_URL
|
|
76
265
|
}
|
|
77
266
|
});
|
|
267
|
+
function migrateLegacyConfigData(data) {
|
|
268
|
+
const accounts = { ...data.accounts ?? {} };
|
|
269
|
+
const hasAccounts = Object.keys(accounts).length > 0;
|
|
270
|
+
let activeAccountId = data.activeAccountId;
|
|
271
|
+
if (!hasAccounts && data.accessToken) {
|
|
272
|
+
const legacyUser = data.user;
|
|
273
|
+
const id = legacyUser?.id ?? (data.accessToken.length >= 8 ? `legacy-${data.accessToken.slice(0, 8)}` : "default");
|
|
274
|
+
const user = legacyUser ?? { id, email: `${id}@cli.local` };
|
|
275
|
+
accounts[id] = {
|
|
276
|
+
id,
|
|
277
|
+
user,
|
|
278
|
+
accessToken: data.accessToken,
|
|
279
|
+
refreshToken: data.refreshToken,
|
|
280
|
+
role: data.role,
|
|
281
|
+
activeOrganizationId: data.activeOrganizationId,
|
|
282
|
+
activeProjectId: data.activeProjectId
|
|
283
|
+
};
|
|
284
|
+
activeAccountId = id;
|
|
285
|
+
}
|
|
286
|
+
const result = { ...data, accounts, activeAccountId };
|
|
287
|
+
delete result.accessToken;
|
|
288
|
+
delete result.refreshToken;
|
|
289
|
+
delete result.user;
|
|
290
|
+
delete result.role;
|
|
291
|
+
delete result.activeOrganizationId;
|
|
292
|
+
delete result.activeProjectId;
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
function migrateLegacyConfig() {
|
|
296
|
+
const data = config.store;
|
|
297
|
+
const hadLegacy = data.accessToken !== void 0 || data.refreshToken !== void 0 || data.user !== void 0 || data.role !== void 0 || data.activeOrganizationId !== void 0 || data.activeProjectId !== void 0;
|
|
298
|
+
if (!hadLegacy) return;
|
|
299
|
+
config.store = migrateLegacyConfigData(data);
|
|
300
|
+
}
|
|
301
|
+
function readAccounts() {
|
|
302
|
+
return config.get("accounts") ?? {};
|
|
303
|
+
}
|
|
304
|
+
function writeAccounts(accounts) {
|
|
305
|
+
config.set("accounts", accounts);
|
|
306
|
+
}
|
|
307
|
+
function listAccounts() {
|
|
308
|
+
migrateLegacyConfig();
|
|
309
|
+
return Object.values(readAccounts());
|
|
310
|
+
}
|
|
311
|
+
function getActiveAccount() {
|
|
312
|
+
migrateLegacyConfig();
|
|
313
|
+
const id = config.get("activeAccountId");
|
|
314
|
+
if (!id) return void 0;
|
|
315
|
+
return readAccounts()[id];
|
|
316
|
+
}
|
|
317
|
+
function getActiveAccountId() {
|
|
318
|
+
migrateLegacyConfig();
|
|
319
|
+
return config.get("activeAccountId");
|
|
320
|
+
}
|
|
321
|
+
function setActiveAccount(id) {
|
|
322
|
+
const accounts = readAccounts();
|
|
323
|
+
if (!accounts[id]) return false;
|
|
324
|
+
config.set("activeAccountId", id);
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
function upsertAccount(account) {
|
|
328
|
+
const accounts = readAccounts();
|
|
329
|
+
const isFirst = Object.keys(accounts).length === 0;
|
|
330
|
+
accounts[account.id] = account;
|
|
331
|
+
writeAccounts(accounts);
|
|
332
|
+
if (isFirst) {
|
|
333
|
+
config.set("activeAccountId", account.id);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function removeAccount(id) {
|
|
337
|
+
const accounts = readAccounts();
|
|
338
|
+
if (!accounts[id]) {
|
|
339
|
+
return { removedActive: false };
|
|
340
|
+
}
|
|
341
|
+
const wasActive = config.get("activeAccountId") === id;
|
|
342
|
+
delete accounts[id];
|
|
343
|
+
writeAccounts(accounts);
|
|
344
|
+
let newActiveId;
|
|
345
|
+
if (wasActive) {
|
|
346
|
+
newActiveId = Object.keys(accounts)[0];
|
|
347
|
+
if (newActiveId) {
|
|
348
|
+
config.set("activeAccountId", newActiveId);
|
|
349
|
+
} else {
|
|
350
|
+
config.delete("activeAccountId");
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return { removedActive: wasActive, newActiveId };
|
|
354
|
+
}
|
|
355
|
+
function updateActiveAccount(patch) {
|
|
356
|
+
migrateLegacyConfig();
|
|
357
|
+
const id = config.get("activeAccountId");
|
|
358
|
+
if (!id) return;
|
|
359
|
+
const accounts = readAccounts();
|
|
360
|
+
const existing = accounts[id];
|
|
361
|
+
if (!existing) return;
|
|
362
|
+
accounts[id] = { ...existing, ...patch, id: existing.id };
|
|
363
|
+
writeAccounts(accounts);
|
|
364
|
+
}
|
|
78
365
|
function getConfig() {
|
|
366
|
+
const active = getActiveAccount();
|
|
79
367
|
return {
|
|
80
368
|
apiUrl: normalizeApiUrl(config.get("apiUrl") ?? DEFAULT_API_URL),
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
369
|
+
accounts: readAccounts(),
|
|
370
|
+
activeAccountId: config.get("activeAccountId"),
|
|
371
|
+
accessToken: active?.accessToken,
|
|
372
|
+
refreshToken: active?.refreshToken,
|
|
373
|
+
activeProjectId: active?.activeProjectId,
|
|
374
|
+
activeOrganizationId: active?.activeOrganizationId,
|
|
375
|
+
user: active?.user,
|
|
376
|
+
role: active?.role
|
|
87
377
|
};
|
|
88
378
|
}
|
|
89
379
|
function getApiUrl() {
|
|
@@ -95,46 +385,49 @@ function setApiUrl(url) {
|
|
|
95
385
|
config.set("apiUrl", normalizeApiUrl(url));
|
|
96
386
|
}
|
|
97
387
|
function getAccessToken() {
|
|
98
|
-
return
|
|
388
|
+
return getActiveAccount()?.accessToken;
|
|
99
389
|
}
|
|
100
390
|
function setAccessToken(token) {
|
|
101
|
-
|
|
391
|
+
updateActiveAccount({ accessToken: token });
|
|
392
|
+
}
|
|
393
|
+
function getRefreshToken() {
|
|
394
|
+
return getActiveAccount()?.refreshToken;
|
|
102
395
|
}
|
|
103
396
|
function setRefreshToken(token) {
|
|
104
|
-
|
|
397
|
+
updateActiveAccount({ refreshToken: token });
|
|
105
398
|
}
|
|
106
399
|
function getActiveProjectId() {
|
|
107
|
-
return
|
|
400
|
+
return getActiveAccount()?.activeProjectId;
|
|
108
401
|
}
|
|
109
402
|
function setActiveProjectId(projectId) {
|
|
110
|
-
|
|
403
|
+
updateActiveAccount({ activeProjectId: projectId });
|
|
111
404
|
}
|
|
112
405
|
function getActiveOrganizationId() {
|
|
113
|
-
return
|
|
406
|
+
return getActiveAccount()?.activeOrganizationId;
|
|
114
407
|
}
|
|
115
408
|
function setActiveOrganizationId(organizationId) {
|
|
116
|
-
|
|
409
|
+
updateActiveAccount({ activeOrganizationId: organizationId });
|
|
117
410
|
}
|
|
118
411
|
function getUser() {
|
|
119
|
-
return
|
|
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-3LJYJ4RK.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
|
);
|
|
@@ -894,15 +1260,19 @@ async function performLogin(options) {
|
|
|
894
1260
|
"Authentication succeeded but the server did not return session tokens. This is likely a server-side issue. Please try again or contact support."
|
|
895
1261
|
);
|
|
896
1262
|
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
1263
|
+
const accountId = pollResponse.user?.id ?? `session-${pollResponse.accessToken.slice(0, 8)}`;
|
|
1264
|
+
const account = {
|
|
1265
|
+
id: accountId,
|
|
1266
|
+
user: pollResponse.user ? {
|
|
901
1267
|
id: pollResponse.user.id,
|
|
902
1268
|
email: pollResponse.user.email,
|
|
903
1269
|
name: pollResponse.user.name
|
|
904
|
-
}
|
|
905
|
-
|
|
1270
|
+
} : { id: accountId, email: `${accountId}@cli.local` },
|
|
1271
|
+
accessToken: pollResponse.accessToken,
|
|
1272
|
+
refreshToken: pollResponse.refreshToken
|
|
1273
|
+
};
|
|
1274
|
+
upsertAccount(account);
|
|
1275
|
+
setActiveAccount(account.id);
|
|
906
1276
|
console.log();
|
|
907
1277
|
success(`Logged in as ${chalk3.bold(pollResponse.user?.email)}`);
|
|
908
1278
|
return { email: pollResponse.user?.email || "" };
|
|
@@ -926,6 +1296,16 @@ var loginCommand = new Command("login").description("Authenticate with Envpilot"
|
|
|
926
1296
|
await performLogin({
|
|
927
1297
|
browser: options.browser !== false
|
|
928
1298
|
});
|
|
1299
|
+
const activeAccount = getActiveAccount();
|
|
1300
|
+
if (activeAccount) {
|
|
1301
|
+
success(`Signed in as ${activeAccount.user.email}.`);
|
|
1302
|
+
}
|
|
1303
|
+
const accounts = listAccounts();
|
|
1304
|
+
if (accounts.length > 1) {
|
|
1305
|
+
info(
|
|
1306
|
+
`You have ${accounts.length} accounts \u2014 use \`envpilot accounts\` to list/switch.`
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
929
1309
|
console.log();
|
|
930
1310
|
console.log("Next steps:");
|
|
931
1311
|
info(" envpilot init Initialize a project in the current directory");
|
|
@@ -943,9 +1323,15 @@ import chalk4 from "chalk";
|
|
|
943
1323
|
import inquirer from "inquirer";
|
|
944
1324
|
|
|
945
1325
|
// src/lib/project-config.ts
|
|
946
|
-
import {
|
|
1326
|
+
import {
|
|
1327
|
+
readFileSync as readFileSync2,
|
|
1328
|
+
writeFileSync as writeFileSync2,
|
|
1329
|
+
existsSync as existsSync2,
|
|
1330
|
+
unlinkSync as unlinkSync2,
|
|
1331
|
+
renameSync
|
|
1332
|
+
} from "fs";
|
|
947
1333
|
import { execSync } from "child_process";
|
|
948
|
-
import { join } from "path";
|
|
1334
|
+
import { join as join2 } from "path";
|
|
949
1335
|
|
|
950
1336
|
// src/types/index.ts
|
|
951
1337
|
import { z } from "zod";
|
|
@@ -954,12 +1340,25 @@ var userSchema = z.object({
|
|
|
954
1340
|
email: z.string().email(),
|
|
955
1341
|
name: z.string().optional()
|
|
956
1342
|
});
|
|
1343
|
+
var accountSchema = z.object({
|
|
1344
|
+
id: z.string(),
|
|
1345
|
+
user: userSchema,
|
|
1346
|
+
accessToken: z.string(),
|
|
1347
|
+
refreshToken: z.string().optional(),
|
|
1348
|
+
// Stored role string (legacy or unified); normalized on read.
|
|
1349
|
+
role: z.string().optional(),
|
|
1350
|
+
// Per-account active org/project selection.
|
|
1351
|
+
activeOrganizationId: z.string().optional(),
|
|
1352
|
+
activeProjectId: z.string().optional()
|
|
1353
|
+
});
|
|
957
1354
|
var organizationSchema = z.object({
|
|
958
1355
|
_id: z.string(),
|
|
959
1356
|
name: z.string(),
|
|
960
1357
|
slug: z.string(),
|
|
961
1358
|
tier: z.enum(["free", "pro"]),
|
|
962
|
-
role: z.string().optional()
|
|
1359
|
+
role: z.string().optional(),
|
|
1360
|
+
// Unified org role (additive; legacy responses omit it and fall back to role).
|
|
1361
|
+
unifiedRole: z.string().nullable().optional()
|
|
963
1362
|
});
|
|
964
1363
|
var projectSchema = z.object({
|
|
965
1364
|
_id: z.string(),
|
|
@@ -970,7 +1369,11 @@ var projectSchema = z.object({
|
|
|
970
1369
|
icon: z.string().optional(),
|
|
971
1370
|
color: z.string().optional(),
|
|
972
1371
|
userRole: z.string().nullable().optional(),
|
|
973
|
-
projectRole: z.string().nullable().optional()
|
|
1372
|
+
projectRole: z.string().nullable().optional(),
|
|
1373
|
+
// Unified-role fields (optional so legacy server responses still parse)
|
|
1374
|
+
unifiedRole: z.string().nullable().optional(),
|
|
1375
|
+
assigned: z.boolean().optional(),
|
|
1376
|
+
environmentScope: z.array(z.string()).nullable().optional()
|
|
974
1377
|
});
|
|
975
1378
|
var variableTagSchema = z.object({
|
|
976
1379
|
_id: z.string(),
|
|
@@ -988,21 +1391,49 @@ var variableSchema = z.object({
|
|
|
988
1391
|
version: z.number().optional(),
|
|
989
1392
|
updatedAt: z.number().optional(),
|
|
990
1393
|
createdAt: z.number().optional(),
|
|
991
|
-
tags: z.array(variableTagSchema).optional()
|
|
1394
|
+
tags: z.array(variableTagSchema).optional(),
|
|
1395
|
+
// Per-variable effective access under the unified model. Optional so older
|
|
1396
|
+
// server responses (which omit it) still parse.
|
|
1397
|
+
access: z.enum(["read", "write"]).optional()
|
|
992
1398
|
});
|
|
1399
|
+
var variablesMetaSchema = z.object({
|
|
1400
|
+
total: z.number().optional(),
|
|
1401
|
+
page: z.number().optional(),
|
|
1402
|
+
limit: z.number().optional(),
|
|
1403
|
+
decryptionFailures: z.array(z.string()).optional(),
|
|
1404
|
+
unifiedRole: z.string().nullable().optional(),
|
|
1405
|
+
assigned: z.boolean().optional(),
|
|
1406
|
+
grantOnly: z.boolean().optional(),
|
|
1407
|
+
environmentScope: z.array(z.string()).nullable().optional(),
|
|
1408
|
+
hasWriteAccess: z.boolean().optional(),
|
|
1409
|
+
scopeRestricted: z.boolean().optional()
|
|
1410
|
+
}).passthrough();
|
|
993
1411
|
var environmentSchema = z.enum([
|
|
994
1412
|
"development",
|
|
995
1413
|
"staging",
|
|
996
1414
|
"production"
|
|
997
1415
|
]);
|
|
998
1416
|
var cliConfigSchema = z.object({
|
|
1417
|
+
// Global CLI setting — shared across all accounts.
|
|
999
1418
|
apiUrl: z.string().url(),
|
|
1419
|
+
// Multi-account store. Keyed by account id (== user id).
|
|
1420
|
+
accounts: z.record(z.string(), accountSchema).optional(),
|
|
1421
|
+
// The currently active account id.
|
|
1422
|
+
activeAccountId: z.string().optional(),
|
|
1423
|
+
// --- Legacy single-account fields ---
|
|
1424
|
+
// Retained ONLY so pre-multi-account configs still parse and can be migrated
|
|
1425
|
+
// into `accounts` by migrateLegacyConfig(). After migration these are deleted
|
|
1426
|
+
// so `accounts` is the single source of truth. Do not read/write directly.
|
|
1000
1427
|
accessToken: z.string().optional(),
|
|
1001
1428
|
refreshToken: z.string().optional(),
|
|
1002
1429
|
activeProjectId: z.string().optional(),
|
|
1003
1430
|
activeOrganizationId: z.string().optional(),
|
|
1004
1431
|
user: userSchema.optional(),
|
|
1005
|
-
role
|
|
1432
|
+
// Stored role string. Kept as a free-form string so both legacy
|
|
1433
|
+
// ("admin"/"team_lead"/"member") and unified
|
|
1434
|
+
// ("owner"/"project_manager"/"team_lead"/"developer") values round-trip.
|
|
1435
|
+
// Normalized on read via getUnifiedRole(); no longer enum-enforced.
|
|
1436
|
+
role: z.string().optional()
|
|
1006
1437
|
});
|
|
1007
1438
|
var projectConfigSchema = z.object({
|
|
1008
1439
|
projectId: z.string(),
|
|
@@ -1014,7 +1445,12 @@ var projectEntrySchema = z.object({
|
|
|
1014
1445
|
organizationId: z.string(),
|
|
1015
1446
|
projectName: z.string().default(""),
|
|
1016
1447
|
organizationName: z.string().default(""),
|
|
1017
|
-
environment: environmentSchema.default("development")
|
|
1448
|
+
environment: environmentSchema.default("development"),
|
|
1449
|
+
// How the pulled .env file's permissions are managed for this project:
|
|
1450
|
+
// auto → derive from the caller's resolved access (default)
|
|
1451
|
+
// always → force read-only (0o400)
|
|
1452
|
+
// never → force writable (0o600)
|
|
1453
|
+
fileProtection: z.enum(["auto", "always", "never"]).optional()
|
|
1018
1454
|
});
|
|
1019
1455
|
var projectConfigV2Schema = z.object({
|
|
1020
1456
|
version: z.literal(1),
|
|
@@ -1024,17 +1460,30 @@ var projectConfigV2Schema = z.object({
|
|
|
1024
1460
|
|
|
1025
1461
|
// src/lib/project-config.ts
|
|
1026
1462
|
var CONFIG_FILE_NAME = ".envpilot";
|
|
1463
|
+
function atomicWriteFileSync(filePath, content) {
|
|
1464
|
+
const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
1465
|
+
try {
|
|
1466
|
+
writeFileSync2(tmpPath, content, "utf-8");
|
|
1467
|
+
renameSync(tmpPath, filePath);
|
|
1468
|
+
} catch (err) {
|
|
1469
|
+
try {
|
|
1470
|
+
if (existsSync2(tmpPath)) unlinkSync2(tmpPath);
|
|
1471
|
+
} catch {
|
|
1472
|
+
}
|
|
1473
|
+
throw err;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1027
1476
|
function getProjectConfigPath(directory = process.cwd()) {
|
|
1028
|
-
return
|
|
1477
|
+
return join2(directory, CONFIG_FILE_NAME);
|
|
1029
1478
|
}
|
|
1030
1479
|
function hasProjectConfig(directory = process.cwd()) {
|
|
1031
|
-
return
|
|
1480
|
+
return existsSync2(getProjectConfigPath(directory));
|
|
1032
1481
|
}
|
|
1033
1482
|
function readRawConfig(directory = process.cwd()) {
|
|
1034
1483
|
const configPath = getProjectConfigPath(directory);
|
|
1035
|
-
if (!
|
|
1484
|
+
if (!existsSync2(configPath)) return null;
|
|
1036
1485
|
try {
|
|
1037
|
-
return JSON.parse(
|
|
1486
|
+
return JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
1038
1487
|
} catch {
|
|
1039
1488
|
return null;
|
|
1040
1489
|
}
|
|
@@ -1081,7 +1530,7 @@ function readProjectConfigV2(directory = process.cwd()) {
|
|
|
1081
1530
|
}
|
|
1082
1531
|
function writeProjectConfigV2(config2, directory = process.cwd()) {
|
|
1083
1532
|
const configPath = getProjectConfigPath(directory);
|
|
1084
|
-
|
|
1533
|
+
atomicWriteFileSync(configPath, JSON.stringify(config2, null, 2) + "\n");
|
|
1085
1534
|
}
|
|
1086
1535
|
function getActiveProject(config2) {
|
|
1087
1536
|
return config2.projects.find((p) => p.projectId === config2.activeProjectId) || config2.projects[0] || null;
|
|
@@ -1174,23 +1623,23 @@ function updateProjectConfig(updates, directory = process.cwd()) {
|
|
|
1174
1623
|
}
|
|
1175
1624
|
function deleteProjectConfig(directory = process.cwd()) {
|
|
1176
1625
|
const configPath = getProjectConfigPath(directory);
|
|
1177
|
-
if (!
|
|
1178
|
-
|
|
1626
|
+
if (!existsSync2(configPath)) return false;
|
|
1627
|
+
unlinkSync2(configPath);
|
|
1179
1628
|
return true;
|
|
1180
1629
|
}
|
|
1181
1630
|
function ensureEnvInGitignore(directory = process.cwd()) {
|
|
1182
|
-
const gitignorePath =
|
|
1183
|
-
if (!
|
|
1184
|
-
|
|
1631
|
+
const gitignorePath = join2(directory, ".gitignore");
|
|
1632
|
+
if (!existsSync2(gitignorePath)) {
|
|
1633
|
+
atomicWriteFileSync(gitignorePath, ".env\n.env.local\n");
|
|
1185
1634
|
return;
|
|
1186
1635
|
}
|
|
1187
|
-
const content =
|
|
1636
|
+
const content = readFileSync2(gitignorePath, "utf-8");
|
|
1188
1637
|
const lines = content.split("\n");
|
|
1189
1638
|
if (lines.some((line2) => line2.trim() === ".env")) {
|
|
1190
1639
|
return;
|
|
1191
1640
|
}
|
|
1192
1641
|
const newContent = content.endsWith("\n") ? content + ".env\n" : content + "\n.env\n";
|
|
1193
|
-
|
|
1642
|
+
atomicWriteFileSync(gitignorePath, newContent);
|
|
1194
1643
|
}
|
|
1195
1644
|
function getTrackedEnvFiles(directory = process.cwd()) {
|
|
1196
1645
|
try {
|
|
@@ -1206,8 +1655,14 @@ function getTrackedEnvFiles(directory = process.cwd()) {
|
|
|
1206
1655
|
}
|
|
1207
1656
|
|
|
1208
1657
|
// src/lib/env-file.ts
|
|
1209
|
-
import {
|
|
1210
|
-
|
|
1658
|
+
import {
|
|
1659
|
+
readFileSync as readFileSync3,
|
|
1660
|
+
writeFileSync as writeFileSync3,
|
|
1661
|
+
existsSync as existsSync3,
|
|
1662
|
+
chmodSync,
|
|
1663
|
+
renameSync as renameSync2
|
|
1664
|
+
} from "fs";
|
|
1665
|
+
import { join as join3 } from "path";
|
|
1211
1666
|
function parseEnvFile(content) {
|
|
1212
1667
|
const result = {};
|
|
1213
1668
|
const lines = content.split("\n");
|
|
@@ -1220,7 +1675,7 @@ function parseEnvFile(content) {
|
|
|
1220
1675
|
if (equalsIndex === -1) {
|
|
1221
1676
|
continue;
|
|
1222
1677
|
}
|
|
1223
|
-
const key = line2.substring(0, equalsIndex).trim();
|
|
1678
|
+
const key = line2.substring(0, equalsIndex).trim().replace(/^export\s+/, "");
|
|
1224
1679
|
let value = line2.substring(equalsIndex + 1);
|
|
1225
1680
|
value = parseValue(value);
|
|
1226
1681
|
if (isValidEnvKey(key)) {
|
|
@@ -1231,17 +1686,21 @@ function parseEnvFile(content) {
|
|
|
1231
1686
|
}
|
|
1232
1687
|
function parseValue(value) {
|
|
1233
1688
|
value = value.trim();
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1689
|
+
const quote = value[0];
|
|
1690
|
+
if (quote === '"' || quote === "'") {
|
|
1691
|
+
const closingIndex = value.indexOf(quote, 1);
|
|
1692
|
+
if (closingIndex !== -1) {
|
|
1693
|
+
const inner = value.slice(1, closingIndex);
|
|
1694
|
+
if (quote === '"') {
|
|
1695
|
+
return inner.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
1696
|
+
}
|
|
1697
|
+
return inner;
|
|
1243
1698
|
}
|
|
1244
1699
|
}
|
|
1700
|
+
const commentIndex = value.indexOf(" #");
|
|
1701
|
+
if (commentIndex !== -1) {
|
|
1702
|
+
value = value.substring(0, commentIndex).trim();
|
|
1703
|
+
}
|
|
1245
1704
|
return value;
|
|
1246
1705
|
}
|
|
1247
1706
|
function isValidEnvKey(key) {
|
|
@@ -1293,33 +1752,53 @@ function diffEnvVars(local, remote) {
|
|
|
1293
1752
|
return { added, removed, changed, unchanged };
|
|
1294
1753
|
}
|
|
1295
1754
|
function readEnvFile(filePath) {
|
|
1296
|
-
if (!
|
|
1755
|
+
if (!existsSync3(filePath)) {
|
|
1297
1756
|
return null;
|
|
1298
1757
|
}
|
|
1299
|
-
const content =
|
|
1758
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
1300
1759
|
return parseEnvFile(content);
|
|
1301
1760
|
}
|
|
1761
|
+
function atomicWriteFileSync2(filePath, content, mode) {
|
|
1762
|
+
const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
1763
|
+
try {
|
|
1764
|
+
writeFileSync3(tmpPath, content, { encoding: "utf-8", mode });
|
|
1765
|
+
renameSync2(tmpPath, filePath);
|
|
1766
|
+
} catch (err) {
|
|
1767
|
+
try {
|
|
1768
|
+
if (existsSync3(tmpPath)) chmodSync(tmpPath, 384);
|
|
1769
|
+
} catch {
|
|
1770
|
+
}
|
|
1771
|
+
throw err;
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1302
1774
|
function writeEnvFile(filePath, vars, options) {
|
|
1303
1775
|
const content = stringifyEnv(vars, options);
|
|
1304
|
-
|
|
1776
|
+
atomicWriteFileSync2(filePath, content, 384);
|
|
1305
1777
|
}
|
|
1306
1778
|
function getEnvPathForEnvironment(environment, directory = process.cwd()) {
|
|
1307
1779
|
if (environment === "development") {
|
|
1308
|
-
return
|
|
1780
|
+
return join3(directory, ".env.local");
|
|
1309
1781
|
}
|
|
1310
|
-
return
|
|
1782
|
+
return join3(directory, `.env.${environment}`);
|
|
1311
1783
|
}
|
|
1312
|
-
function applyFileProtection(filePath,
|
|
1313
|
-
if (!
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1784
|
+
function applyFileProtection(filePath, access, protection = "auto") {
|
|
1785
|
+
if (!existsSync3(filePath)) return;
|
|
1786
|
+
if (protection === "never") {
|
|
1787
|
+
chmodSync(filePath, 384);
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
if (protection === "always") {
|
|
1791
|
+
chmodSync(filePath, 256);
|
|
1792
|
+
return;
|
|
1319
1793
|
}
|
|
1794
|
+
chmodSync(filePath, isFileWritable(access) ? 384 : 256);
|
|
1320
1795
|
}
|
|
1321
1796
|
|
|
1322
1797
|
// src/commands/init.ts
|
|
1798
|
+
function orgUnifiedRole(org) {
|
|
1799
|
+
const unified = org.unifiedRole;
|
|
1800
|
+
return normalizeOrgRole(unified ?? org.role);
|
|
1801
|
+
}
|
|
1323
1802
|
var initCommand = new Command2("init").description("Initialize Envpilot in the current directory").option("-o, --organization <id>", "Organization ID").option("-p, --project <id>", "Project ID").option(
|
|
1324
1803
|
"-e, --environment <env>",
|
|
1325
1804
|
"Default environment (development, staging, production)"
|
|
@@ -1368,9 +1847,7 @@ var initCommand = new Command2("init").description("Initialize Envpilot in the c
|
|
|
1368
1847
|
});
|
|
1369
1848
|
setActiveOrganizationId(selectedOrg._id);
|
|
1370
1849
|
setActiveProjectId(selectedProject._id);
|
|
1371
|
-
|
|
1372
|
-
setRole(selectedOrg.role);
|
|
1373
|
-
}
|
|
1850
|
+
setUnifiedRole(orgUnifiedRole(selectedOrg));
|
|
1374
1851
|
ensureEnvInGitignore();
|
|
1375
1852
|
warnTrackedFiles();
|
|
1376
1853
|
console.log();
|
|
@@ -1382,21 +1859,23 @@ var initCommand = new Command2("init").description("Initialize Envpilot in the c
|
|
|
1382
1859
|
});
|
|
1383
1860
|
async function addProject(existingConfig, options) {
|
|
1384
1861
|
const api = createAPIClient();
|
|
1385
|
-
let role =
|
|
1386
|
-
if (role
|
|
1862
|
+
let role = getUnifiedRole();
|
|
1863
|
+
if (roleLevel(role) < ROLE_LEVEL.team_lead) {
|
|
1387
1864
|
const orgs = await withSpinner("Checking permissions...", async () => {
|
|
1388
1865
|
const response = await api.get("/api/cli/organizations");
|
|
1389
1866
|
return response.data || [];
|
|
1390
1867
|
});
|
|
1391
|
-
const
|
|
1868
|
+
const freshOrg = orgs.find(
|
|
1392
1869
|
(o) => o._id === existingConfig.projects[0]?.organizationId
|
|
1393
|
-
)
|
|
1394
|
-
if (
|
|
1395
|
-
|
|
1396
|
-
role
|
|
1870
|
+
);
|
|
1871
|
+
if (freshOrg) {
|
|
1872
|
+
role = orgUnifiedRole(freshOrg);
|
|
1873
|
+
setUnifiedRole(role);
|
|
1397
1874
|
}
|
|
1398
|
-
if (role
|
|
1399
|
-
error(
|
|
1875
|
+
if (roleLevel(role) < ROLE_LEVEL.team_lead) {
|
|
1876
|
+
error(
|
|
1877
|
+
`Only team leads and above can link multiple projects (your role: ${formatRoleLabel(role)}).`
|
|
1878
|
+
);
|
|
1400
1879
|
info("Unlink the current project first with `envpilot unlink`.");
|
|
1401
1880
|
process.exit(1);
|
|
1402
1881
|
}
|
|
@@ -1589,17 +2068,12 @@ function warnTrackedFiles() {
|
|
|
1589
2068
|
function printPostInit(selectedOrg, selectedProject) {
|
|
1590
2069
|
console.log();
|
|
1591
2070
|
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);
|
|
2071
|
+
console.log(
|
|
2072
|
+
chalk4.dim(` Org role: ${formatRoleLabel(orgUnifiedRole(selectedOrg))}`)
|
|
2073
|
+
);
|
|
2074
|
+
const scope = selectedProject.environmentScope;
|
|
2075
|
+
if (scope && scope.length > 0) {
|
|
2076
|
+
console.log(chalk4.dim(` Environment scope: ${scope.join(", ")}`));
|
|
1603
2077
|
}
|
|
1604
2078
|
console.log();
|
|
1605
2079
|
console.log("Next steps:");
|
|
@@ -1932,7 +2406,7 @@ function parseNetlify(content) {
|
|
|
1932
2406
|
for (const line2 of lines) {
|
|
1933
2407
|
const trimmed = line2.trim();
|
|
1934
2408
|
if (trimmed.startsWith("[")) {
|
|
1935
|
-
inBuildEnv = trimmed === "[build.environment]" ||
|
|
2409
|
+
inBuildEnv = trimmed === "[build.environment]" || /^\[context\.[^.\]]+\.environment\]$/.test(trimmed);
|
|
1936
2410
|
continue;
|
|
1937
2411
|
}
|
|
1938
2412
|
if (!inBuildEnv) continue;
|
|
@@ -1955,6 +2429,17 @@ function parseNetlify(content) {
|
|
|
1955
2429
|
}
|
|
1956
2430
|
|
|
1957
2431
|
// src/commands/pull.ts
|
|
2432
|
+
function accessFromMeta(meta, variables) {
|
|
2433
|
+
const role = normalizeOrgRole(meta?.unifiedRole ?? meta?.role);
|
|
2434
|
+
return {
|
|
2435
|
+
role,
|
|
2436
|
+
// Owners are implicitly assigned to every project. The legacy server sends
|
|
2437
|
+
// no `assigned`/`projectRole` for owners, so fall back to true for them.
|
|
2438
|
+
assigned: role === "owner" || (meta?.assigned ?? (meta?.projectRole !== null && meta?.projectRole !== void 0)),
|
|
2439
|
+
environmentScope: meta?.environmentScope ?? null,
|
|
2440
|
+
hasWriteAccess: meta?.hasWriteAccess ?? variables.some((v) => v.access === "write")
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
1958
2443
|
var pullCommand = new Command3("pull").description("Download environment variables to local .env file").option(
|
|
1959
2444
|
"-e, --env <environment>",
|
|
1960
2445
|
"Environment (development, staging, production)"
|
|
@@ -1997,6 +2482,8 @@ var pullCommand = new Command3("pull").description("Download environment variabl
|
|
|
1997
2482
|
const projectConfig = readProjectConfig();
|
|
1998
2483
|
if (!projectConfig) throw notInitialized();
|
|
1999
2484
|
checkTrackedFiles();
|
|
2485
|
+
const configV2ForActive = readProjectConfigV2();
|
|
2486
|
+
const activeEntry = configV2ForActive ? getActiveProject(configV2ForActive) : null;
|
|
2000
2487
|
const environment = options.env || projectConfig.environment || "development";
|
|
2001
2488
|
const fmt = options.format || "env";
|
|
2002
2489
|
if (!ALL_FORMATS.includes(fmt)) {
|
|
@@ -2008,7 +2495,8 @@ var pullCommand = new Command3("pull").description("Download environment variabl
|
|
|
2008
2495
|
{
|
|
2009
2496
|
projectId: projectConfig.projectId,
|
|
2010
2497
|
organizationId: projectConfig.organizationId,
|
|
2011
|
-
environment
|
|
2498
|
+
environment,
|
|
2499
|
+
fileProtection: activeEntry?.fileProtection
|
|
2012
2500
|
},
|
|
2013
2501
|
outputPath,
|
|
2014
2502
|
options
|
|
@@ -2037,7 +2525,8 @@ async function pullAllProjects(options) {
|
|
|
2037
2525
|
{
|
|
2038
2526
|
projectId: project.projectId,
|
|
2039
2527
|
organizationId: project.organizationId,
|
|
2040
|
-
environment: project.environment
|
|
2528
|
+
environment: project.environment,
|
|
2529
|
+
fileProtection: project.fileProtection
|
|
2041
2530
|
},
|
|
2042
2531
|
outputPath,
|
|
2043
2532
|
options
|
|
@@ -2068,7 +2557,8 @@ async function pullSingleProject(project, options) {
|
|
|
2068
2557
|
{
|
|
2069
2558
|
projectId: project.projectId,
|
|
2070
2559
|
organizationId: project.organizationId,
|
|
2071
|
-
environment
|
|
2560
|
+
environment,
|
|
2561
|
+
fileProtection: project.fileProtection
|
|
2072
2562
|
},
|
|
2073
2563
|
outputPath,
|
|
2074
2564
|
options
|
|
@@ -2076,7 +2566,7 @@ async function pullSingleProject(project, options) {
|
|
|
2076
2566
|
}
|
|
2077
2567
|
async function pullProject(project, outputPath, options) {
|
|
2078
2568
|
const api = createAPIClient();
|
|
2079
|
-
let
|
|
2569
|
+
let meta;
|
|
2080
2570
|
const variables = await withSpinner(
|
|
2081
2571
|
`Fetching ${chalk5.bold(project.environment)} variables...`,
|
|
2082
2572
|
async () => {
|
|
@@ -2087,10 +2577,15 @@ async function pullProject(project, outputPath, options) {
|
|
|
2087
2577
|
organizationId: project.organizationId
|
|
2088
2578
|
}
|
|
2089
2579
|
});
|
|
2090
|
-
|
|
2580
|
+
meta = response.meta;
|
|
2091
2581
|
return response.data || [];
|
|
2092
2582
|
}
|
|
2093
2583
|
);
|
|
2584
|
+
if (meta?.scopeRestricted && meta.environmentScope?.length) {
|
|
2585
|
+
info(
|
|
2586
|
+
`Your access is scoped to ${meta.environmentScope.join(", ")}; variables in other environments are withheld.`
|
|
2587
|
+
);
|
|
2588
|
+
}
|
|
2094
2589
|
if (variables.length === 0) {
|
|
2095
2590
|
warning(`No variables found for ${project.environment} environment.`);
|
|
2096
2591
|
return;
|
|
@@ -2154,20 +2649,21 @@ async function pullProject(project, outputPath, options) {
|
|
|
2154
2649
|
});
|
|
2155
2650
|
fs.writeFileSync(outputPath, output, "utf-8");
|
|
2156
2651
|
}
|
|
2157
|
-
const
|
|
2158
|
-
|
|
2652
|
+
const access = accessFromMeta(meta, variables);
|
|
2653
|
+
const protection = project.fileProtection ?? "auto";
|
|
2654
|
+
applyFileProtection(outputPath, access, protection);
|
|
2159
2655
|
success(
|
|
2160
2656
|
`Downloaded ${variables.length} variables to ${chalk5.bold(outputPath)}`
|
|
2161
2657
|
);
|
|
2162
|
-
if (
|
|
2658
|
+
if (meta?.grantOnly) {
|
|
2163
2659
|
info(
|
|
2164
|
-
"You have
|
|
2660
|
+
"You have grant-only access to this project. You only see variables explicitly shared with you."
|
|
2165
2661
|
);
|
|
2166
2662
|
}
|
|
2167
|
-
const
|
|
2168
|
-
if (
|
|
2663
|
+
const writable = protection === "never" || protection !== "always" && isFileWritable(access);
|
|
2664
|
+
if (!writable) {
|
|
2169
2665
|
info(
|
|
2170
|
-
`File is read-only (your role: ${role
|
|
2666
|
+
`File is read-only (your role: ${formatRoleLabel(access.role)}${access.assigned ? "" : ", not assigned to this project"}).`
|
|
2171
2667
|
);
|
|
2172
2668
|
}
|
|
2173
2669
|
console.log();
|
|
@@ -2306,40 +2802,10 @@ var pushCommand = new Command4("push").description("Upload local .env file to cl
|
|
|
2306
2802
|
process.exit(1);
|
|
2307
2803
|
}
|
|
2308
2804
|
const api = createAPIClient();
|
|
2309
|
-
const
|
|
2310
|
-
const
|
|
2311
|
-
|
|
2312
|
-
|
|
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
|
-
const environment = options.env || defaultEnvironment || "development";
|
|
2340
|
-
const fmt = options.format || "env";
|
|
2341
|
-
if (!ALL_FORMATS.includes(fmt)) {
|
|
2342
|
-
error(`Unknown format: ${fmt}. Supported: ${ALL_FORMATS.join(", ")}`);
|
|
2805
|
+
const environment = options.env || defaultEnvironment || "development";
|
|
2806
|
+
const fmt = options.format || "env";
|
|
2807
|
+
if (!ALL_FORMATS.includes(fmt)) {
|
|
2808
|
+
error(`Unknown format: ${fmt}. Supported: ${ALL_FORMATS.join(", ")}`);
|
|
2343
2809
|
process.exit(1);
|
|
2344
2810
|
}
|
|
2345
2811
|
const inputPath = options.file || (fmt === "env" ? getEnvPathForEnvironment(environment) : getDefaultFilename(fmt, environment));
|
|
@@ -2375,6 +2841,7 @@ var pushCommand = new Command4("push").description("Upload local .env file to cl
|
|
|
2375
2841
|
error("No valid variables to push.");
|
|
2376
2842
|
return;
|
|
2377
2843
|
}
|
|
2844
|
+
let meta;
|
|
2378
2845
|
const remoteVariables = await withSpinner(
|
|
2379
2846
|
"Fetching current variables...",
|
|
2380
2847
|
async () => {
|
|
@@ -2386,9 +2853,21 @@ var pushCommand = new Command4("push").description("Upload local .env file to cl
|
|
|
2386
2853
|
params.organizationId = organizationId;
|
|
2387
2854
|
}
|
|
2388
2855
|
const response = await api.get("/api/cli/variables", params);
|
|
2856
|
+
meta = response.meta;
|
|
2389
2857
|
return response.data || [];
|
|
2390
2858
|
}
|
|
2391
2859
|
);
|
|
2860
|
+
const scope = meta?.environmentScope;
|
|
2861
|
+
if (Array.isArray(scope) && !scope.includes(environment)) {
|
|
2862
|
+
error(
|
|
2863
|
+
`Your developer access is scoped to ${scope.join(", ")}. ${environment} variables are withheld.`
|
|
2864
|
+
);
|
|
2865
|
+
process.exit(1);
|
|
2866
|
+
}
|
|
2867
|
+
if (meta?.hasWriteAccess === false) {
|
|
2868
|
+
error(`You have read-only access to ${environment} in this project.`);
|
|
2869
|
+
process.exit(1);
|
|
2870
|
+
}
|
|
2392
2871
|
const remoteVars = {};
|
|
2393
2872
|
for (const variable of remoteVariables) {
|
|
2394
2873
|
remoteVars[variable.key] = variable.value;
|
|
@@ -2459,32 +2938,28 @@ var pushCommand = new Command4("push").description("Upload local .env file to cl
|
|
|
2459
2938
|
return response.data;
|
|
2460
2939
|
}
|
|
2461
2940
|
);
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2941
|
+
success(
|
|
2942
|
+
`Pushed ${result?.total || Object.keys(valid).length} variables to ${chalk6.bold(environment)}`
|
|
2943
|
+
);
|
|
2944
|
+
console.log();
|
|
2945
|
+
console.log(chalk6.dim(` Created: ${result?.created || 0}`));
|
|
2946
|
+
console.log(chalk6.dim(` Updated: ${result?.updated || 0}`));
|
|
2947
|
+
if (mode === "replace") {
|
|
2948
|
+
console.log(chalk6.dim(` Deleted: ${result?.deleted || 0}`));
|
|
2949
|
+
}
|
|
2950
|
+
const deniedKeys = result?.deniedKeys ?? [];
|
|
2951
|
+
if (deniedKeys.length > 0) {
|
|
2472
2952
|
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)}`
|
|
2953
|
+
warning(
|
|
2954
|
+
`${deniedKeys.length} variable(s) were NOT written (access denied):`
|
|
2480
2955
|
);
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
console.log(chalk6.dim(` Updated: ${result?.updated || 0}`));
|
|
2484
|
-
if (mode === "replace") {
|
|
2485
|
-
console.log(chalk6.dim(` Deleted: ${result?.deleted || 0}`));
|
|
2956
|
+
for (const key of deniedKeys) {
|
|
2957
|
+
console.log(chalk6.red(` \u2717 ${key}`));
|
|
2486
2958
|
}
|
|
2487
2959
|
}
|
|
2960
|
+
if (result?.skipped && result.skipped > 0) {
|
|
2961
|
+
console.log(chalk6.dim(` Skipped: ${result.skipped}`));
|
|
2962
|
+
}
|
|
2488
2963
|
} catch (err) {
|
|
2489
2964
|
await handleError(err);
|
|
2490
2965
|
}
|
|
@@ -2494,6 +2969,9 @@ var pushCommand = new Command4("push").description("Upload local .env file to cl
|
|
|
2494
2969
|
import { Command as Command5 } from "commander";
|
|
2495
2970
|
import chalk7 from "chalk";
|
|
2496
2971
|
import inquirer4 from "inquirer";
|
|
2972
|
+
function orgUnifiedRole2(org) {
|
|
2973
|
+
return normalizeOrgRole(org.unifiedRole ?? org.role);
|
|
2974
|
+
}
|
|
2497
2975
|
var switchCommand = new Command5("switch").description("Switch project, environment, or active linked project").argument("[target]", "project slug or environment name").option("-o, --organization <id>", "Switch organization").option("-p, --project <id>", "Switch project").option(
|
|
2498
2976
|
"-e, --env <environment>",
|
|
2499
2977
|
"Switch environment (development, staging, production)"
|
|
@@ -2562,17 +3040,15 @@ var switchCommand = new Command5("switch").description("Switch project, environm
|
|
|
2562
3040
|
process.exit(1);
|
|
2563
3041
|
}
|
|
2564
3042
|
setActiveOrganizationId(org._id);
|
|
2565
|
-
|
|
2566
|
-
setRole(org.role);
|
|
2567
|
-
}
|
|
3043
|
+
setUnifiedRole(orgUnifiedRole2(org));
|
|
2568
3044
|
if (projectConfig) {
|
|
2569
3045
|
updateProjectConfig({ organizationId: org._id });
|
|
2570
3046
|
}
|
|
2571
3047
|
success(`Switched to organization: ${chalk7.bold(org.name)}`);
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
3048
|
+
console.log(
|
|
3049
|
+
chalk7.dim(` Role: ${formatRoleLabel(orgUnifiedRole2(org))}`)
|
|
3050
|
+
);
|
|
3051
|
+
roleNotice(orgUnifiedRole2(org));
|
|
2576
3052
|
return;
|
|
2577
3053
|
}
|
|
2578
3054
|
if (options.project || target) {
|
|
@@ -2611,9 +3087,7 @@ var switchCommand = new Command5("switch").description("Switch project, environm
|
|
|
2611
3087
|
}
|
|
2612
3088
|
if (organizations.length === 1) {
|
|
2613
3089
|
organizationId = organizations[0]._id;
|
|
2614
|
-
|
|
2615
|
-
setRole(organizations[0].role);
|
|
2616
|
-
}
|
|
3090
|
+
setUnifiedRole(orgUnifiedRole2(organizations[0]));
|
|
2617
3091
|
} else {
|
|
2618
3092
|
const { orgId } = await inquirer4.prompt([
|
|
2619
3093
|
{
|
|
@@ -2628,8 +3102,8 @@ var switchCommand = new Command5("switch").description("Switch project, environm
|
|
|
2628
3102
|
]);
|
|
2629
3103
|
organizationId = orgId;
|
|
2630
3104
|
const selectedOrg = organizations.find((o) => o._id === orgId);
|
|
2631
|
-
if (selectedOrg
|
|
2632
|
-
|
|
3105
|
+
if (selectedOrg) {
|
|
3106
|
+
setUnifiedRole(orgUnifiedRole2(selectedOrg));
|
|
2633
3107
|
}
|
|
2634
3108
|
}
|
|
2635
3109
|
}
|
|
@@ -2768,19 +3242,17 @@ var switchCommand = new Command5("switch").description("Switch project, environm
|
|
|
2768
3242
|
if (switchType === "organization") {
|
|
2769
3243
|
setActiveOrganizationId(orgId);
|
|
2770
3244
|
const org = organizations.find((o) => o._id === orgId);
|
|
2771
|
-
|
|
2772
|
-
setRole(org.role);
|
|
2773
|
-
}
|
|
3245
|
+
setUnifiedRole(orgUnifiedRole2(org));
|
|
2774
3246
|
success(`Switched to organization: ${chalk7.bold(org.name)}`);
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
3247
|
+
console.log(
|
|
3248
|
+
chalk7.dim(` Role: ${formatRoleLabel(orgUnifiedRole2(org))}`)
|
|
3249
|
+
);
|
|
3250
|
+
roleNotice(orgUnifiedRole2(org));
|
|
2779
3251
|
return;
|
|
2780
3252
|
}
|
|
2781
3253
|
const selectedOrg = organizations.find((o) => o._id === orgId);
|
|
2782
|
-
if (selectedOrg
|
|
2783
|
-
|
|
3254
|
+
if (selectedOrg) {
|
|
3255
|
+
setUnifiedRole(orgUnifiedRole2(selectedOrg));
|
|
2784
3256
|
}
|
|
2785
3257
|
const projects = await withSpinner(
|
|
2786
3258
|
"Fetching projects...",
|
|
@@ -2928,7 +3400,7 @@ async function listOrganizations(api, options) {
|
|
|
2928
3400
|
name: org.name,
|
|
2929
3401
|
slug: org.slug,
|
|
2930
3402
|
tier: org.tier === "pro" ? chalk8.green("Pro") : chalk8.dim("Free"),
|
|
2931
|
-
role: org.role
|
|
3403
|
+
role: formatRoleLabel(org.unifiedRole ?? org.role)
|
|
2932
3404
|
})),
|
|
2933
3405
|
[
|
|
2934
3406
|
{ key: "name", header: "Name" },
|
|
@@ -2986,7 +3458,9 @@ async function listProjects(api, projectConfig, options) {
|
|
|
2986
3458
|
name: project.name,
|
|
2987
3459
|
slug: project.slug,
|
|
2988
3460
|
description: project.description || chalk8.dim("-"),
|
|
2989
|
-
role:
|
|
3461
|
+
role: formatRoleLabel(
|
|
3462
|
+
project.unifiedRole ?? project.userRole ?? project.projectRole
|
|
3463
|
+
),
|
|
2990
3464
|
active: projectConfig?.projectId === project._id ? chalk8.green("\u2713") : ""
|
|
2991
3465
|
})),
|
|
2992
3466
|
[
|
|
@@ -2998,11 +3472,8 @@ async function listProjects(api, projectConfig, options) {
|
|
|
2998
3472
|
{ key: "active", header: "" }
|
|
2999
3473
|
]
|
|
3000
3474
|
);
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
console.log();
|
|
3004
|
-
console.log(chalk8.dim(`Your org role: ${formatRole(role)}`));
|
|
3005
|
-
}
|
|
3475
|
+
console.log();
|
|
3476
|
+
console.log(chalk8.dim(`Your org role: ${formatRoleLabel(getUnifiedRole())}`));
|
|
3006
3477
|
}
|
|
3007
3478
|
async function listVariables(api, projectConfig, options) {
|
|
3008
3479
|
const projectId = options.project || projectConfig?.projectId;
|
|
@@ -3011,14 +3482,14 @@ async function listVariables(api, projectConfig, options) {
|
|
|
3011
3482
|
error("No project specified. Use --project or run `envpilot init` first.");
|
|
3012
3483
|
process.exit(1);
|
|
3013
3484
|
}
|
|
3014
|
-
let
|
|
3485
|
+
let meta;
|
|
3015
3486
|
const variables = await withSpinner("Fetching variables...", async () => {
|
|
3016
3487
|
const params = { projectId };
|
|
3017
3488
|
if (environment) {
|
|
3018
3489
|
params.environment = environment;
|
|
3019
3490
|
}
|
|
3020
3491
|
const response = await api.get("/api/cli/variables", params);
|
|
3021
|
-
|
|
3492
|
+
meta = response.meta;
|
|
3022
3493
|
return response.data || [];
|
|
3023
3494
|
});
|
|
3024
3495
|
const tagFilter = options.tag?.toLowerCase();
|
|
@@ -3050,7 +3521,7 @@ async function listVariables(api, projectConfig, options) {
|
|
|
3050
3521
|
value: options.showValues ? variable.value : maskValue(variable.value),
|
|
3051
3522
|
sensitive: variable.isSensitive ? chalk8.yellow("\u25CF") : "",
|
|
3052
3523
|
tags: hasTags ? variable.tags?.map((t) => t.name).join(", ") || chalk8.dim("-") : "",
|
|
3053
|
-
version: `v${variable.version}`
|
|
3524
|
+
version: typeof variable.version === "number" ? `v${variable.version}` : ""
|
|
3054
3525
|
})),
|
|
3055
3526
|
[
|
|
3056
3527
|
{ key: "key", header: "Key" },
|
|
@@ -3062,16 +3533,14 @@ async function listVariables(api, projectConfig, options) {
|
|
|
3062
3533
|
);
|
|
3063
3534
|
console.log();
|
|
3064
3535
|
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) {
|
|
3536
|
+
console.log(chalk8.dim(`Your org role: ${formatRoleLabel(getUnifiedRole())}`));
|
|
3537
|
+
if (meta?.scopeRestricted && meta.environmentScope?.length) {
|
|
3070
3538
|
console.log(
|
|
3071
|
-
chalk8.dim(
|
|
3539
|
+
chalk8.dim(
|
|
3540
|
+
`Your access is scoped to ${meta.environmentScope.join(", ")}; variables in other environments are withheld.`
|
|
3541
|
+
)
|
|
3072
3542
|
);
|
|
3073
|
-
}
|
|
3074
|
-
if (role === "member" || metaProjectRole === "viewer") {
|
|
3543
|
+
} else if (meta?.grantOnly) {
|
|
3075
3544
|
console.log(
|
|
3076
3545
|
chalk8.dim("You may only see variables you have been granted access to.")
|
|
3077
3546
|
);
|
|
@@ -3084,6 +3553,23 @@ async function listVariables(api, projectConfig, options) {
|
|
|
3084
3553
|
// src/commands/config.ts
|
|
3085
3554
|
import { Command as Command7 } from "commander";
|
|
3086
3555
|
import chalk9 from "chalk";
|
|
3556
|
+
var FILE_PROTECTION_VALUES = ["auto", "always", "never"];
|
|
3557
|
+
var ENVIRONMENT_VALUES = ["development", "staging", "production"];
|
|
3558
|
+
function updateActiveProjectEntry(updates) {
|
|
3559
|
+
const v2 = readProjectConfigV2();
|
|
3560
|
+
if (!v2) {
|
|
3561
|
+
error(
|
|
3562
|
+
"No project initialized in this directory. Run `envpilot init` first."
|
|
3563
|
+
);
|
|
3564
|
+
process.exit(1);
|
|
3565
|
+
}
|
|
3566
|
+
const active = getActiveProject(v2);
|
|
3567
|
+
if (!active) {
|
|
3568
|
+
error("No active project found in this directory.");
|
|
3569
|
+
process.exit(1);
|
|
3570
|
+
}
|
|
3571
|
+
writeProjectConfigV2(updateProjectInConfig(v2, active.projectId, updates));
|
|
3572
|
+
}
|
|
3087
3573
|
var configCommand = new Command7("config").description("Manage CLI configuration").argument("[action]", "Action: get, set, list, path, reset").argument("[key]", "Config key (for get/set)").argument("[value]", "Config value (for set)").action(async (action, key, value) => {
|
|
3088
3574
|
try {
|
|
3089
3575
|
switch (action) {
|
|
@@ -3127,6 +3613,10 @@ async function handleGet(key) {
|
|
|
3127
3613
|
console.log(" user Current authenticated user");
|
|
3128
3614
|
console.log(" activeProjectId Currently active project");
|
|
3129
3615
|
console.log(" activeOrganizationId Currently active organization");
|
|
3616
|
+
console.log(
|
|
3617
|
+
" fileProtection Active project .env protection (auto|always|never)"
|
|
3618
|
+
);
|
|
3619
|
+
console.log(" defaultEnvironment Active project's default environment");
|
|
3130
3620
|
process.exit(1);
|
|
3131
3621
|
}
|
|
3132
3622
|
const config2 = getConfig();
|
|
@@ -3147,6 +3637,18 @@ async function handleGet(key) {
|
|
|
3147
3637
|
case "activeOrganizationId":
|
|
3148
3638
|
console.log(config2.activeOrganizationId || chalk9.dim("(not set)"));
|
|
3149
3639
|
break;
|
|
3640
|
+
case "fileProtection": {
|
|
3641
|
+
const v2 = readProjectConfigV2();
|
|
3642
|
+
const active = v2 ? getActiveProject(v2) : null;
|
|
3643
|
+
console.log(active?.fileProtection ?? "auto");
|
|
3644
|
+
break;
|
|
3645
|
+
}
|
|
3646
|
+
case "defaultEnvironment": {
|
|
3647
|
+
const v2 = readProjectConfigV2();
|
|
3648
|
+
const active = v2 ? getActiveProject(v2) : null;
|
|
3649
|
+
console.log(active?.environment || chalk9.dim("(not set)"));
|
|
3650
|
+
break;
|
|
3651
|
+
}
|
|
3150
3652
|
default:
|
|
3151
3653
|
error(`Unknown key: ${key}`);
|
|
3152
3654
|
process.exit(1);
|
|
@@ -3156,8 +3658,7 @@ async function handleSet(key, value) {
|
|
|
3156
3658
|
if (!key || value === void 0) {
|
|
3157
3659
|
error("Missing key or value. Usage: envpilot config set <key> <value>");
|
|
3158
3660
|
console.log();
|
|
3159
|
-
|
|
3160
|
-
console.log(" apiUrl API endpoint URL");
|
|
3661
|
+
printSettableKeys();
|
|
3161
3662
|
process.exit(1);
|
|
3162
3663
|
}
|
|
3163
3664
|
switch (key) {
|
|
@@ -3173,14 +3674,53 @@ async function handleSet(key, value) {
|
|
|
3173
3674
|
success(`Set apiUrl to ${normalized}`);
|
|
3174
3675
|
break;
|
|
3175
3676
|
}
|
|
3677
|
+
case "fileProtection": {
|
|
3678
|
+
if (!FILE_PROTECTION_VALUES.includes(
|
|
3679
|
+
value
|
|
3680
|
+
)) {
|
|
3681
|
+
error(
|
|
3682
|
+
`Invalid fileProtection value: ${value}. Expected one of: ${FILE_PROTECTION_VALUES.join(", ")}`
|
|
3683
|
+
);
|
|
3684
|
+
process.exit(1);
|
|
3685
|
+
}
|
|
3686
|
+
updateActiveProjectEntry({
|
|
3687
|
+
fileProtection: value
|
|
3688
|
+
});
|
|
3689
|
+
success(`Set fileProtection to ${value} for the active project`);
|
|
3690
|
+
break;
|
|
3691
|
+
}
|
|
3692
|
+
case "defaultEnvironment": {
|
|
3693
|
+
if (!ENVIRONMENT_VALUES.includes(
|
|
3694
|
+
value
|
|
3695
|
+
)) {
|
|
3696
|
+
error(
|
|
3697
|
+
`Invalid defaultEnvironment value: ${value}. Expected one of: ${ENVIRONMENT_VALUES.join(", ")}`
|
|
3698
|
+
);
|
|
3699
|
+
process.exit(1);
|
|
3700
|
+
}
|
|
3701
|
+
updateActiveProjectEntry({
|
|
3702
|
+
environment: value
|
|
3703
|
+
});
|
|
3704
|
+
success(`Set defaultEnvironment to ${value} for the active project`);
|
|
3705
|
+
break;
|
|
3706
|
+
}
|
|
3176
3707
|
default:
|
|
3177
3708
|
error(`Cannot set key: ${key}`);
|
|
3178
3709
|
console.log();
|
|
3179
|
-
|
|
3180
|
-
console.log(" apiUrl API endpoint URL");
|
|
3710
|
+
printSettableKeys();
|
|
3181
3711
|
process.exit(1);
|
|
3182
3712
|
}
|
|
3183
3713
|
}
|
|
3714
|
+
function printSettableKeys() {
|
|
3715
|
+
console.log("Settable keys:");
|
|
3716
|
+
console.log(" apiUrl API endpoint URL");
|
|
3717
|
+
console.log(
|
|
3718
|
+
" fileProtection Active project .env protection (auto|always|never)"
|
|
3719
|
+
);
|
|
3720
|
+
console.log(
|
|
3721
|
+
" defaultEnvironment Active project default environment (development|staging|production)"
|
|
3722
|
+
);
|
|
3723
|
+
}
|
|
3184
3724
|
async function handleList() {
|
|
3185
3725
|
const config2 = getConfig();
|
|
3186
3726
|
const projectConfig = readProjectConfig();
|
|
@@ -3236,20 +3776,43 @@ async function handleReset() {
|
|
|
3236
3776
|
|
|
3237
3777
|
// src/commands/logout.ts
|
|
3238
3778
|
import { Command as Command8 } from "commander";
|
|
3239
|
-
var logoutCommand = new Command8("logout").description("Log out from Envpilot").action(async () => {
|
|
3779
|
+
var logoutCommand = new Command8("logout").description("Log out from Envpilot").option("--all", "Log out of every authenticated account").action(async (options) => {
|
|
3240
3780
|
try {
|
|
3241
3781
|
if (!isAuthenticated()) {
|
|
3242
3782
|
info("You are not logged in.");
|
|
3243
3783
|
return;
|
|
3244
3784
|
}
|
|
3245
|
-
|
|
3785
|
+
if (options.all) {
|
|
3786
|
+
const accounts = listAccounts();
|
|
3787
|
+
for (const account of accounts) {
|
|
3788
|
+
try {
|
|
3789
|
+
const api2 = new APIClient({ accessToken: account.accessToken });
|
|
3790
|
+
await api2.post("/api/cli/auth?action=revoke", {});
|
|
3791
|
+
} catch {
|
|
3792
|
+
}
|
|
3793
|
+
removeAccount(account.id);
|
|
3794
|
+
}
|
|
3795
|
+
clearAuth();
|
|
3796
|
+
success(`Logged out of all ${accounts.length} accounts.`);
|
|
3797
|
+
return;
|
|
3798
|
+
}
|
|
3799
|
+
const activeAccount = getActiveAccount();
|
|
3246
3800
|
const api = createAPIClient();
|
|
3247
3801
|
try {
|
|
3248
3802
|
await api.post("/api/cli/auth?action=revoke", {});
|
|
3249
3803
|
} catch {
|
|
3250
3804
|
}
|
|
3251
|
-
clearAuth();
|
|
3252
|
-
success(
|
|
3805
|
+
const { newActiveId } = clearAuth();
|
|
3806
|
+
success(
|
|
3807
|
+
`Logged out${activeAccount?.user.email ? ` ${activeAccount.user.email}` : ""}.`
|
|
3808
|
+
);
|
|
3809
|
+
if (newActiveId) {
|
|
3810
|
+
const remaining = listAccounts();
|
|
3811
|
+
const newActive = remaining.find((a) => a.id === newActiveId);
|
|
3812
|
+
info(`Now active: ${newActive?.user.email ?? newActiveId}`);
|
|
3813
|
+
} else {
|
|
3814
|
+
info("No accounts remaining.");
|
|
3815
|
+
}
|
|
3253
3816
|
} catch (err) {
|
|
3254
3817
|
await handleError(err);
|
|
3255
3818
|
}
|
|
@@ -3351,20 +3914,35 @@ import chalk11 from "chalk";
|
|
|
3351
3914
|
|
|
3352
3915
|
// src/lib/commit-guard.ts
|
|
3353
3916
|
import {
|
|
3354
|
-
existsSync as
|
|
3355
|
-
readFileSync as
|
|
3356
|
-
writeFileSync as
|
|
3357
|
-
mkdirSync,
|
|
3917
|
+
existsSync as existsSync4,
|
|
3918
|
+
readFileSync as readFileSync4,
|
|
3919
|
+
writeFileSync as writeFileSync4,
|
|
3920
|
+
mkdirSync as mkdirSync2,
|
|
3358
3921
|
chmodSync as chmodSync2,
|
|
3359
|
-
unlinkSync as
|
|
3360
|
-
|
|
3922
|
+
unlinkSync as unlinkSync3,
|
|
3923
|
+
renameSync as renameSync3,
|
|
3924
|
+
statSync as statSync2
|
|
3361
3925
|
} from "fs";
|
|
3362
|
-
import { join as
|
|
3926
|
+
import { join as join4, resolve as resolve2 } from "path";
|
|
3363
3927
|
import { execSync as execSync2 } from "child_process";
|
|
3928
|
+
function atomicWriteFileSync3(filePath, content, mode) {
|
|
3929
|
+
const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
3930
|
+
try {
|
|
3931
|
+
writeFileSync4(tmpPath, content, "utf-8");
|
|
3932
|
+
chmodSync2(tmpPath, mode);
|
|
3933
|
+
renameSync3(tmpPath, filePath);
|
|
3934
|
+
} catch (err) {
|
|
3935
|
+
try {
|
|
3936
|
+
if (existsSync4(tmpPath)) unlinkSync3(tmpPath);
|
|
3937
|
+
} catch {
|
|
3938
|
+
}
|
|
3939
|
+
throw err;
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3364
3942
|
var HOOK_START_MARKER = "# ENVPILOT_GUARD_START";
|
|
3365
3943
|
var HOOK_END_MARKER = "# ENVPILOT_GUARD_END";
|
|
3366
3944
|
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)
|
|
3945
|
+
ENV_FILES=$(git diff --cached --name-only | grep -E '(^|/)\\.env($|\\.)' | grep -vE '\\.(example|sample|template|dist)$' || true)
|
|
3368
3946
|
if [ -n "$ENV_FILES" ]; then
|
|
3369
3947
|
echo ""
|
|
3370
3948
|
echo "\\033[1;31mERROR:\\033[0m Envpilot commit guard blocked this commit."
|
|
@@ -3380,7 +3958,7 @@ ${HOOK_END_MARKER}`;
|
|
|
3380
3958
|
function findGitRoot(startDir) {
|
|
3381
3959
|
let dir = startDir || process.cwd();
|
|
3382
3960
|
while (true) {
|
|
3383
|
-
if (
|
|
3961
|
+
if (existsSync4(join4(dir, ".git"))) {
|
|
3384
3962
|
return dir;
|
|
3385
3963
|
}
|
|
3386
3964
|
const parent = resolve2(dir, "..");
|
|
@@ -3391,9 +3969,9 @@ function findGitRoot(startDir) {
|
|
|
3391
3969
|
}
|
|
3392
3970
|
}
|
|
3393
3971
|
function resolveGitDir(repoRoot) {
|
|
3394
|
-
const gitPath =
|
|
3972
|
+
const gitPath = join4(repoRoot, ".git");
|
|
3395
3973
|
try {
|
|
3396
|
-
const stat =
|
|
3974
|
+
const stat = statSync2(gitPath);
|
|
3397
3975
|
if (stat.isDirectory()) {
|
|
3398
3976
|
return gitPath;
|
|
3399
3977
|
}
|
|
@@ -3401,12 +3979,12 @@ function resolveGitDir(repoRoot) {
|
|
|
3401
3979
|
return gitPath;
|
|
3402
3980
|
}
|
|
3403
3981
|
try {
|
|
3404
|
-
const content =
|
|
3982
|
+
const content = readFileSync4(gitPath, "utf-8").trim();
|
|
3405
3983
|
const match = content.match(/^gitdir:\s*(.+)$/);
|
|
3406
3984
|
if (match) {
|
|
3407
3985
|
const gitdir = resolve2(repoRoot, match[1]);
|
|
3408
3986
|
const commonDir = resolve2(gitdir, "..", "..");
|
|
3409
|
-
if (
|
|
3987
|
+
if (existsSync4(join4(commonDir, "hooks")) || existsSync4(commonDir)) {
|
|
3410
3988
|
return commonDir;
|
|
3411
3989
|
}
|
|
3412
3990
|
return gitdir;
|
|
@@ -3428,7 +4006,7 @@ function getHooksDir(repoRoot) {
|
|
|
3428
4006
|
} catch {
|
|
3429
4007
|
}
|
|
3430
4008
|
const gitDir = resolveGitDir(repoRoot);
|
|
3431
|
-
return
|
|
4009
|
+
return join4(gitDir, "hooks");
|
|
3432
4010
|
}
|
|
3433
4011
|
function installCommitGuard(repoRoot) {
|
|
3434
4012
|
const root = repoRoot || findGitRoot();
|
|
@@ -3441,19 +4019,18 @@ function installCommitGuard(repoRoot) {
|
|
|
3441
4019
|
}
|
|
3442
4020
|
try {
|
|
3443
4021
|
const hooksDir = getHooksDir(root);
|
|
3444
|
-
const hookPath =
|
|
3445
|
-
|
|
4022
|
+
const hookPath = join4(hooksDir, "pre-commit");
|
|
4023
|
+
mkdirSync2(hooksDir, { recursive: true });
|
|
3446
4024
|
let existingContent = "";
|
|
3447
4025
|
try {
|
|
3448
|
-
existingContent =
|
|
4026
|
+
existingContent = readFileSync4(hookPath, "utf-8");
|
|
3449
4027
|
} catch {
|
|
3450
4028
|
}
|
|
3451
4029
|
if (existingContent.includes(HOOK_START_MARKER)) {
|
|
3452
4030
|
const startIdx = existingContent.indexOf(HOOK_START_MARKER);
|
|
3453
4031
|
const endIdx = existingContent.indexOf(HOOK_END_MARKER) + HOOK_END_MARKER.length;
|
|
3454
4032
|
const updated = existingContent.substring(0, startIdx) + HOOK_BLOCK + existingContent.substring(endIdx);
|
|
3455
|
-
|
|
3456
|
-
chmodSync2(hookPath, 493);
|
|
4033
|
+
atomicWriteFileSync3(hookPath, updated, 493);
|
|
3457
4034
|
return {
|
|
3458
4035
|
installed: true,
|
|
3459
4036
|
hookPath,
|
|
@@ -3466,8 +4043,7 @@ function installCommitGuard(repoRoot) {
|
|
|
3466
4043
|
} else {
|
|
3467
4044
|
newContent = "#!/bin/sh\n\n" + HOOK_BLOCK + "\n";
|
|
3468
4045
|
}
|
|
3469
|
-
|
|
3470
|
-
chmodSync2(hookPath, 493);
|
|
4046
|
+
atomicWriteFileSync3(hookPath, newContent, 493);
|
|
3471
4047
|
return {
|
|
3472
4048
|
installed: true,
|
|
3473
4049
|
hookPath,
|
|
@@ -3483,6 +4059,21 @@ function installCommitGuard(repoRoot) {
|
|
|
3483
4059
|
}
|
|
3484
4060
|
|
|
3485
4061
|
// src/commands/sync.ts
|
|
4062
|
+
function accessFromMeta2(meta, variables) {
|
|
4063
|
+
const role = normalizeOrgRole(meta?.unifiedRole ?? meta?.role);
|
|
4064
|
+
return {
|
|
4065
|
+
role,
|
|
4066
|
+
// Owners are implicitly assigned to every project. The legacy server sends
|
|
4067
|
+
// no `assigned`/`projectRole` for owners, so fall back to true for them.
|
|
4068
|
+
assigned: role === "owner" || (meta?.assigned ?? (meta?.projectRole !== null && meta?.projectRole !== void 0)),
|
|
4069
|
+
environmentScope: meta?.environmentScope ?? null,
|
|
4070
|
+
hasWriteAccess: meta?.hasWriteAccess ?? variables.some((v) => v.access === "write")
|
|
4071
|
+
};
|
|
4072
|
+
}
|
|
4073
|
+
function orgUnifiedRole3(org) {
|
|
4074
|
+
const unified = org.unifiedRole;
|
|
4075
|
+
return normalizeOrgRole(unified ?? org.role);
|
|
4076
|
+
}
|
|
3486
4077
|
var syncCommand = new Command10("sync").description(
|
|
3487
4078
|
"Login, select project, pull variables, and protect files \u2014 all in one command"
|
|
3488
4079
|
).option("-o, --organization <id>", "Organization ID").option("-p, --project <id>", "Project ID or name").option(
|
|
@@ -3546,9 +4137,7 @@ var syncCommand = new Command10("sync").description(
|
|
|
3546
4137
|
environment = selection.selectedEnvironment;
|
|
3547
4138
|
projectName = selection.selectedProject.name;
|
|
3548
4139
|
organizationName = selection.selectedOrg.name;
|
|
3549
|
-
|
|
3550
|
-
setRole(selection.selectedOrg.role);
|
|
3551
|
-
}
|
|
4140
|
+
setUnifiedRole(orgUnifiedRole3(selection.selectedOrg));
|
|
3552
4141
|
writeProjectConfigV2({
|
|
3553
4142
|
version: 1,
|
|
3554
4143
|
activeProjectId: projectId,
|
|
@@ -3577,7 +4166,7 @@ var syncCommand = new Command10("sync").description(
|
|
|
3577
4166
|
setActiveProjectId(projectId);
|
|
3578
4167
|
}
|
|
3579
4168
|
console.log();
|
|
3580
|
-
let
|
|
4169
|
+
let meta;
|
|
3581
4170
|
const variables = await withSpinner(
|
|
3582
4171
|
`Fetching ${chalk11.bold(environment)} variables...`,
|
|
3583
4172
|
async () => {
|
|
@@ -3587,11 +4176,16 @@ var syncCommand = new Command10("sync").description(
|
|
|
3587
4176
|
environment,
|
|
3588
4177
|
...organizationId && { organizationId }
|
|
3589
4178
|
});
|
|
3590
|
-
|
|
4179
|
+
meta = response.meta;
|
|
3591
4180
|
return response.data || [];
|
|
3592
4181
|
}
|
|
3593
4182
|
);
|
|
3594
4183
|
const outputPath = getEnvPathForEnvironment(environment);
|
|
4184
|
+
if (meta?.scopeRestricted && meta.environmentScope?.length) {
|
|
4185
|
+
info(
|
|
4186
|
+
`Your access is scoped to ${meta.environmentScope.join(", ")}; variables in other environments are withheld.`
|
|
4187
|
+
);
|
|
4188
|
+
}
|
|
3595
4189
|
if (variables.length === 0) {
|
|
3596
4190
|
warning(`No variables found for ${environment} environment.`);
|
|
3597
4191
|
console.log();
|
|
@@ -3628,15 +4222,21 @@ var syncCommand = new Command10("sync").description(
|
|
|
3628
4222
|
}
|
|
3629
4223
|
}
|
|
3630
4224
|
writeEnvFile(outputPath, remoteVars, { sort: true, comments });
|
|
3631
|
-
const
|
|
3632
|
-
|
|
4225
|
+
const access = accessFromMeta2(meta, variables);
|
|
4226
|
+
const protection = existingConfig?.projects.find((p) => p.projectId === projectId)?.fileProtection ?? "auto";
|
|
4227
|
+
applyFileProtection(outputPath, access, protection);
|
|
3633
4228
|
success(
|
|
3634
4229
|
`Synced ${variables.length} variables to ${chalk11.bold(outputPath)}`
|
|
3635
4230
|
);
|
|
3636
|
-
|
|
3637
|
-
if (isProtected) {
|
|
4231
|
+
if (meta?.grantOnly) {
|
|
3638
4232
|
info(
|
|
3639
|
-
|
|
4233
|
+
"You have grant-only access to this project. You only see variables explicitly shared with you."
|
|
4234
|
+
);
|
|
4235
|
+
}
|
|
4236
|
+
const writable = protection === "never" || protection !== "always" && isFileWritable(access);
|
|
4237
|
+
if (!writable) {
|
|
4238
|
+
info(
|
|
4239
|
+
`File is read-only (your role: ${formatRoleLabel(access.role)}${access.assigned ? "" : ", not assigned to this project"}).`
|
|
3640
4240
|
);
|
|
3641
4241
|
}
|
|
3642
4242
|
console.log();
|
|
@@ -3784,6 +4384,7 @@ var whoamiCommand = new Command12("whoami").description("Show the current authen
|
|
|
3784
4384
|
["Name", remoteUser.name || localUser?.name],
|
|
3785
4385
|
["API URL", getApiUrl()],
|
|
3786
4386
|
["Active Organization", getActiveOrganizationId()],
|
|
4387
|
+
["Org Role", formatRoleLabel(getUnifiedRole())],
|
|
3787
4388
|
["Active Project", getActiveProjectId()],
|
|
3788
4389
|
[
|
|
3789
4390
|
"Linked Project",
|
|
@@ -3791,6 +4392,15 @@ var whoamiCommand = new Command12("whoami").description("Show the current authen
|
|
|
3791
4392
|
],
|
|
3792
4393
|
["Environment", activeProject?.environment]
|
|
3793
4394
|
]);
|
|
4395
|
+
const accounts = listAccounts();
|
|
4396
|
+
if (accounts.length > 1) {
|
|
4397
|
+
blank();
|
|
4398
|
+
console.log(
|
|
4399
|
+
chalk13.dim(
|
|
4400
|
+
`Accounts: ${accounts.length} (use \`envpilot accounts\` to switch)`
|
|
4401
|
+
)
|
|
4402
|
+
);
|
|
4403
|
+
}
|
|
3794
4404
|
blank();
|
|
3795
4405
|
console.log(chalk13.dim("Token verified against the CLI auth endpoint."));
|
|
3796
4406
|
} catch (err) {
|
|
@@ -3798,110 +4408,120 @@ var whoamiCommand = new Command12("whoami").description("Show the current authen
|
|
|
3798
4408
|
}
|
|
3799
4409
|
});
|
|
3800
4410
|
|
|
3801
|
-
// src/commands/
|
|
4411
|
+
// src/commands/accounts.ts
|
|
3802
4412
|
import { Command as Command13 } from "commander";
|
|
3803
|
-
import { spawn as spawn2 } from "child_process";
|
|
3804
4413
|
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);
|
|
4414
|
+
function resolveAccount(accounts, identifier) {
|
|
4415
|
+
const normalized = identifier.trim().toLowerCase();
|
|
4416
|
+
return accounts.find(
|
|
4417
|
+
(account) => account.id === identifier || account.user.email.toLowerCase() === normalized
|
|
4418
|
+
);
|
|
3824
4419
|
}
|
|
3825
|
-
function
|
|
3826
|
-
|
|
4420
|
+
function listAvailableEmails(accounts) {
|
|
4421
|
+
console.log();
|
|
4422
|
+
console.log("Available accounts:");
|
|
4423
|
+
for (const account of accounts) {
|
|
4424
|
+
console.log(` ${account.user.email}`);
|
|
4425
|
+
}
|
|
3827
4426
|
}
|
|
3828
|
-
function
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
4427
|
+
function printAccountsList() {
|
|
4428
|
+
const accounts = listAccounts();
|
|
4429
|
+
const activeId = getActiveAccountId();
|
|
4430
|
+
if (accounts.length === 0) {
|
|
4431
|
+
console.log("No accounts. Run `envpilot login`.");
|
|
4432
|
+
return;
|
|
4433
|
+
}
|
|
4434
|
+
table(
|
|
4435
|
+
accounts.map((account) => ({
|
|
4436
|
+
active: account.id === activeId ? chalk14.green("\u25CF") : "",
|
|
4437
|
+
email: account.user.email,
|
|
4438
|
+
name: account.user.name ?? "",
|
|
4439
|
+
organization: account.activeOrganizationId ?? ""
|
|
4440
|
+
})),
|
|
4441
|
+
[
|
|
4442
|
+
{ key: "active", header: "" },
|
|
4443
|
+
{ key: "email", header: "Email" },
|
|
4444
|
+
{ key: "name", header: "Name" },
|
|
4445
|
+
{ key: "organization", header: "Active Org" }
|
|
4446
|
+
]
|
|
4447
|
+
);
|
|
4448
|
+
blank();
|
|
4449
|
+
if (accounts.length === 1) {
|
|
4450
|
+
console.log(chalk14.dim("1 account (log in again to add another)."));
|
|
4451
|
+
} else {
|
|
4452
|
+
console.log(chalk14.dim(`${accounts.length} accounts.`));
|
|
4453
|
+
}
|
|
3832
4454
|
}
|
|
3833
|
-
function
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
if (entry.apiUrl !== getApiUrl()) return null;
|
|
3841
|
-
return entry;
|
|
3842
|
-
} catch {
|
|
3843
|
-
return null;
|
|
4455
|
+
function switchAccount(identifier) {
|
|
4456
|
+
const accounts = listAccounts();
|
|
4457
|
+
const target = resolveAccount(accounts, identifier);
|
|
4458
|
+
if (!target) {
|
|
4459
|
+
error(`Account not found: ${identifier}`);
|
|
4460
|
+
listAvailableEmails(accounts);
|
|
4461
|
+
process.exit(1);
|
|
3844
4462
|
}
|
|
4463
|
+
setActiveAccount(target.id);
|
|
4464
|
+
success(`Switched to ${target.user.email}.`);
|
|
3845
4465
|
}
|
|
3846
|
-
function
|
|
3847
|
-
const
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
4466
|
+
function removeAccountByIdentifier(identifier) {
|
|
4467
|
+
const accounts = listAccounts();
|
|
4468
|
+
const target = resolveAccount(accounts, identifier);
|
|
4469
|
+
if (!target) {
|
|
4470
|
+
error(`Account not found: ${identifier}`);
|
|
4471
|
+
listAvailableEmails(accounts);
|
|
4472
|
+
process.exit(1);
|
|
4473
|
+
}
|
|
4474
|
+
const result = removeAccount(target.id);
|
|
4475
|
+
success(`Removed ${target.user.email}.`);
|
|
4476
|
+
if (result.removedActive) {
|
|
4477
|
+
if (result.newActiveId) {
|
|
4478
|
+
const remaining = listAccounts();
|
|
4479
|
+
const newActive = remaining.find((a) => a.id === result.newActiveId);
|
|
4480
|
+
console.log(
|
|
4481
|
+
chalk14.dim(`Now active: ${newActive?.user.email ?? result.newActiveId}`)
|
|
4482
|
+
);
|
|
4483
|
+
} else {
|
|
4484
|
+
console.log(chalk14.dim("No accounts remaining."));
|
|
4485
|
+
}
|
|
4486
|
+
}
|
|
3851
4487
|
}
|
|
3852
|
-
|
|
4488
|
+
var accountsCommand = new Command13("accounts").description("List and manage authenticated accounts").action(async () => {
|
|
3853
4489
|
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 {
|
|
4490
|
+
printAccountsList();
|
|
4491
|
+
} catch (err) {
|
|
4492
|
+
await handleError(err);
|
|
3873
4493
|
}
|
|
3874
|
-
}
|
|
3875
|
-
|
|
4494
|
+
});
|
|
4495
|
+
accountsCommand.command("list").description("List all authenticated accounts").action(async () => {
|
|
3876
4496
|
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 {
|
|
4497
|
+
printAccountsList();
|
|
4498
|
+
} catch (err) {
|
|
4499
|
+
await handleError(err);
|
|
3888
4500
|
}
|
|
3889
|
-
}
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
4501
|
+
});
|
|
4502
|
+
accountsCommand.command("switch").description("Switch the active account").argument("<identifier>", "Account id or email").action(async (identifier) => {
|
|
4503
|
+
try {
|
|
4504
|
+
switchAccount(identifier);
|
|
4505
|
+
} catch (err) {
|
|
4506
|
+
await handleError(err);
|
|
4507
|
+
}
|
|
4508
|
+
});
|
|
4509
|
+
accountsCommand.command("remove").description("Remove an account").argument("<identifier>", "Account id or email").action(async (identifier) => {
|
|
4510
|
+
try {
|
|
4511
|
+
removeAccountByIdentifier(identifier);
|
|
4512
|
+
} catch (err) {
|
|
4513
|
+
await handleError(err);
|
|
4514
|
+
}
|
|
4515
|
+
});
|
|
3900
4516
|
|
|
3901
4517
|
// src/commands/run.ts
|
|
4518
|
+
import { Command as Command14 } from "commander";
|
|
4519
|
+
import crossSpawn from "cross-spawn";
|
|
4520
|
+
import { constants as osConstants } from "os";
|
|
4521
|
+
import chalk15 from "chalk";
|
|
3902
4522
|
var DEFAULT_TTL = 3600;
|
|
3903
|
-
var runCommand = new
|
|
3904
|
-
"Run a command with project secrets injected as environment variables (no .env file written)"
|
|
4523
|
+
var runCommand = new Command14("run").description(
|
|
4524
|
+
"Run a command with project secrets injected as environment variables. Secrets are injected into the child process in-memory (no .env file is written), but a decrypted copy is cached locally at ~/.config/envpilot/run-cache (mode 0600, same protection as a .env) to avoid re-fetching on every run. Use --no-cache to skip the cache, or `envpilot logout` to purge it."
|
|
3905
4525
|
).argument("[command...]", "Command and arguments to execute").option(
|
|
3906
4526
|
"-e, --env <environment>",
|
|
3907
4527
|
"Environment to load (development, staging, production)"
|
|
@@ -3919,10 +4539,10 @@ var runCommand = new Command13("run").description(
|
|
|
3919
4539
|
"Print the variables that would be injected and exit (no command runs)"
|
|
3920
4540
|
).option(
|
|
3921
4541
|
"--shell",
|
|
3922
|
-
|
|
4542
|
+
'Run the command string through your shell ($SHELL or /bin/sh -c "..."), enabling pipes, &&, and $VAR expansion. Without this flag the command is executed directly with no shell, so arguments are passed through verbatim (safe from shell injection).'
|
|
3923
4543
|
).option("-q, --quiet", "Suppress informational messages").option("--no-cache", "Skip cache and always fetch fresh secrets from server").option(
|
|
3924
4544
|
"--cache-ttl <seconds>",
|
|
3925
|
-
"How long cached secrets stay fresh before a fingerprint check (default: 3600s / 1h)",
|
|
4545
|
+
"How long cached secrets stay fresh before a fingerprint check (default: 3600s / 1h; 0 = always fingerprint-check)",
|
|
3926
4546
|
String(DEFAULT_TTL)
|
|
3927
4547
|
).passThroughOptions().allowExcessArguments().action(async (commandArgs, options) => {
|
|
3928
4548
|
try {
|
|
@@ -3951,10 +4571,11 @@ var runCommand = new Command13("run").description(
|
|
|
3951
4571
|
}
|
|
3952
4572
|
const environment = options.env || project.environment;
|
|
3953
4573
|
const organizationId = options.organization || project.organizationId;
|
|
3954
|
-
const
|
|
3955
|
-
|
|
3956
|
-
|
|
4574
|
+
const parsedTtl = Number.parseInt(
|
|
4575
|
+
options.cacheTtl ?? String(DEFAULT_TTL),
|
|
4576
|
+
10
|
|
3957
4577
|
);
|
|
4578
|
+
const ttl = Number.isFinite(parsedTtl) && parsedTtl >= 0 ? parsedTtl : DEFAULT_TTL;
|
|
3958
4579
|
let variables;
|
|
3959
4580
|
let cacheHit = false;
|
|
3960
4581
|
let cacheAge = "";
|
|
@@ -4006,13 +4627,19 @@ var runCommand = new Command13("run").description(
|
|
|
4006
4627
|
project.projectId,
|
|
4007
4628
|
environment,
|
|
4008
4629
|
organizationId,
|
|
4009
|
-
variables
|
|
4630
|
+
variables,
|
|
4631
|
+
serverFingerprint
|
|
4010
4632
|
);
|
|
4011
4633
|
}
|
|
4012
4634
|
} catch {
|
|
4013
4635
|
variables = probe.entry.variables;
|
|
4014
4636
|
cacheHit = true;
|
|
4015
4637
|
cacheAge = formatAge(probe.entry.fetchedAt);
|
|
4638
|
+
if (!options.quiet) {
|
|
4639
|
+
warning(
|
|
4640
|
+
`Using offline cache (age ${cacheAge}) \u2014 could not reach the server to verify freshness.`
|
|
4641
|
+
);
|
|
4642
|
+
}
|
|
4016
4643
|
}
|
|
4017
4644
|
} else {
|
|
4018
4645
|
variables = await doFetch(
|
|
@@ -4049,9 +4676,9 @@ var runCommand = new Command13("run").description(
|
|
|
4049
4676
|
}
|
|
4050
4677
|
if (!options.quiet) {
|
|
4051
4678
|
const injectedCount = Object.keys(secrets).length;
|
|
4052
|
-
const cacheTag = cacheHit ?
|
|
4679
|
+
const cacheTag = cacheHit ? chalk15.dim(` \u26A1 cache (${cacheAge})`) : "";
|
|
4053
4680
|
info(
|
|
4054
|
-
`Injected ${
|
|
4681
|
+
`Injected ${chalk15.bold(injectedCount)} ${injectedCount === 1 ? "variable" : "variables"} from ${chalk15.bold(`${project.projectName || project.projectId}/${environment}`)}${cacheTag}`
|
|
4055
4682
|
);
|
|
4056
4683
|
if (overridden.length > 0) {
|
|
4057
4684
|
warning(
|
|
@@ -4061,13 +4688,13 @@ var runCommand = new Command13("run").description(
|
|
|
4061
4688
|
}
|
|
4062
4689
|
console.log();
|
|
4063
4690
|
}
|
|
4064
|
-
await runChild(commandArgs, finalEnv, options);
|
|
4691
|
+
process.exitCode = await runChild(commandArgs, finalEnv, options);
|
|
4065
4692
|
} catch (err) {
|
|
4066
4693
|
await handleError(err);
|
|
4067
4694
|
}
|
|
4068
4695
|
});
|
|
4069
4696
|
async function doFetch(project, environment, organizationId, quiet, labelPrefix = "Loading") {
|
|
4070
|
-
const label = `${labelPrefix} ${
|
|
4697
|
+
const label = `${labelPrefix} ${chalk15.bold(environment)} secrets for ${chalk15.bold(project.projectName || project.projectId)}...`;
|
|
4071
4698
|
const api = createAPIClient();
|
|
4072
4699
|
const { variables, decryptionFailures } = quiet ? await api.listVariables(project.projectId, environment, organizationId) : await withSpinner(
|
|
4073
4700
|
label,
|
|
@@ -4076,7 +4703,7 @@ async function doFetch(project, environment, organizationId, quiet, labelPrefix
|
|
|
4076
4703
|
if (decryptionFailures.length > 0) {
|
|
4077
4704
|
for (const key of decryptionFailures) {
|
|
4078
4705
|
warning(
|
|
4079
|
-
`Could not decrypt ${
|
|
4706
|
+
`Could not decrypt ${chalk15.bold(key)} \u2014 skipped (vault error, check server logs)`
|
|
4080
4707
|
);
|
|
4081
4708
|
}
|
|
4082
4709
|
}
|
|
@@ -4086,46 +4713,49 @@ function printInjectionPreview(secrets, project, environment) {
|
|
|
4086
4713
|
const keys = Object.keys(secrets).sort();
|
|
4087
4714
|
console.log();
|
|
4088
4715
|
console.log(
|
|
4089
|
-
|
|
4090
|
-
`Would inject ${keys.length} ${keys.length === 1 ? "variable" : "variables"} from ${
|
|
4716
|
+
chalk15.bold(
|
|
4717
|
+
`Would inject ${keys.length} ${keys.length === 1 ? "variable" : "variables"} from ${chalk15.cyan(`${project.projectName || project.projectId}/${environment}`)}:`
|
|
4091
4718
|
)
|
|
4092
4719
|
);
|
|
4093
4720
|
console.log();
|
|
4094
4721
|
if (keys.length === 0) {
|
|
4095
|
-
console.log(
|
|
4722
|
+
console.log(chalk15.dim(" (no variables)"));
|
|
4096
4723
|
} else {
|
|
4097
4724
|
for (const key of keys) {
|
|
4098
4725
|
const value = secrets[key];
|
|
4099
4726
|
const masked = maskForPreview(value);
|
|
4100
|
-
console.log(` ${
|
|
4727
|
+
console.log(` ${chalk15.cyan(key)}=${chalk15.dim(masked)}`);
|
|
4101
4728
|
}
|
|
4102
4729
|
}
|
|
4103
4730
|
console.log();
|
|
4104
4731
|
success("Dry run \u2014 no command executed.");
|
|
4105
4732
|
}
|
|
4106
4733
|
function maskForPreview(value) {
|
|
4107
|
-
|
|
4108
|
-
|
|
4734
|
+
const len = value.length;
|
|
4735
|
+
const dots = "\u2022".repeat(6);
|
|
4736
|
+
if (len < 12) return `${dots} (${len} chars)`;
|
|
4737
|
+
return `${value.slice(0, 2)}${dots} (${len} chars)`;
|
|
4109
4738
|
}
|
|
4110
4739
|
function runChild(commandArgs, env, options) {
|
|
4111
4740
|
return new Promise((resolve3) => {
|
|
4112
|
-
|
|
4741
|
+
let command;
|
|
4742
|
+
let args;
|
|
4743
|
+
if (options.shell) {
|
|
4744
|
+
command = process.env.SHELL || "/bin/sh";
|
|
4745
|
+
args = ["-c", commandArgs.join(" ")];
|
|
4746
|
+
} else {
|
|
4747
|
+
[command, ...args] = commandArgs;
|
|
4748
|
+
}
|
|
4113
4749
|
if (!command) {
|
|
4114
|
-
|
|
4750
|
+
resolve3(1);
|
|
4751
|
+
return;
|
|
4115
4752
|
}
|
|
4116
|
-
const
|
|
4117
|
-
const useShell = options.shell === true || isWindows;
|
|
4118
|
-
const child = spawn2(command, args, {
|
|
4753
|
+
const child = crossSpawn(command, args, {
|
|
4119
4754
|
stdio: "inherit",
|
|
4120
4755
|
env,
|
|
4121
|
-
shell:
|
|
4756
|
+
shell: false
|
|
4122
4757
|
});
|
|
4123
|
-
const signals = [
|
|
4124
|
-
"SIGINT",
|
|
4125
|
-
"SIGTERM",
|
|
4126
|
-
"SIGHUP",
|
|
4127
|
-
"SIGQUIT"
|
|
4128
|
-
];
|
|
4758
|
+
const signals = ["SIGTERM", "SIGHUP"];
|
|
4129
4759
|
const forward = {};
|
|
4130
4760
|
for (const sig of signals) {
|
|
4131
4761
|
const handler = () => {
|
|
@@ -4148,67 +4778,66 @@ function runChild(commandArgs, env, options) {
|
|
|
4148
4778
|
child.on("error", (err) => {
|
|
4149
4779
|
cleanup();
|
|
4150
4780
|
const code = err.code;
|
|
4781
|
+
const displayCommand = options.shell ? commandArgs.join(" ") : command;
|
|
4151
4782
|
if (code === "ENOENT") {
|
|
4152
4783
|
error(
|
|
4153
|
-
`Command not found: ${
|
|
4784
|
+
`Command not found: ${displayCommand}. Make sure it is installed and on your PATH.`
|
|
4154
4785
|
);
|
|
4786
|
+
resolve3(127);
|
|
4155
4787
|
} else if (code === "EACCES") {
|
|
4156
|
-
error(`Permission denied executing: ${
|
|
4788
|
+
error(`Permission denied executing: ${displayCommand}`);
|
|
4789
|
+
resolve3(126);
|
|
4157
4790
|
} else {
|
|
4158
4791
|
error(`Failed to spawn process: ${err.message}`);
|
|
4792
|
+
resolve3(1);
|
|
4159
4793
|
}
|
|
4160
|
-
process.exit(1);
|
|
4161
4794
|
});
|
|
4162
4795
|
child.on("exit", (code, signal) => {
|
|
4163
4796
|
cleanup();
|
|
4164
4797
|
if (signal) {
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
} catch {
|
|
4168
|
-
process.exit(1);
|
|
4169
|
-
}
|
|
4798
|
+
const signum = osConstants.signals[signal] ?? 0;
|
|
4799
|
+
resolve3(128 + signum);
|
|
4170
4800
|
} else {
|
|
4171
|
-
|
|
4801
|
+
resolve3(code ?? 0);
|
|
4172
4802
|
}
|
|
4173
|
-
resolve3();
|
|
4174
4803
|
});
|
|
4175
4804
|
});
|
|
4176
4805
|
}
|
|
4177
4806
|
|
|
4178
4807
|
// src/commands/man.ts
|
|
4179
|
-
import { Command as
|
|
4180
|
-
import
|
|
4808
|
+
import { Command as Command15 } from "commander";
|
|
4809
|
+
import chalk16 from "chalk";
|
|
4181
4810
|
function printCommandManual(commandName) {
|
|
4182
4811
|
const command = findCommandDefinition(commandName);
|
|
4183
4812
|
if (!command) {
|
|
4184
|
-
console.log(
|
|
4813
|
+
console.log(chalk16.red(`Unknown command reference: ${commandName}`));
|
|
4185
4814
|
console.log();
|
|
4186
4815
|
console.log("Run `envpilot man` to see all supported commands.");
|
|
4187
4816
|
process.exit(1);
|
|
4188
4817
|
}
|
|
4189
|
-
console.log(
|
|
4818
|
+
console.log(chalk16.bold(formatArgv(command.argv)));
|
|
4190
4819
|
if (command.args) {
|
|
4191
|
-
console.log(
|
|
4820
|
+
console.log(chalk16.dim(command.args));
|
|
4192
4821
|
}
|
|
4193
4822
|
blank();
|
|
4194
4823
|
console.log(command.description);
|
|
4195
4824
|
blank();
|
|
4196
|
-
console.log(
|
|
4825
|
+
console.log(chalk16.green("Examples"));
|
|
4197
4826
|
for (const example of command.examples) {
|
|
4198
|
-
console.log(` ${
|
|
4827
|
+
console.log(` ${chalk16.cyan(formatArgv(example))}`);
|
|
4199
4828
|
}
|
|
4200
4829
|
blank();
|
|
4201
|
-
console.log(
|
|
4830
|
+
console.log(chalk16.green("Notes"));
|
|
4202
4831
|
for (const note of command.notes) {
|
|
4203
4832
|
console.log(` - ${note}`);
|
|
4204
4833
|
}
|
|
4205
4834
|
}
|
|
4206
4835
|
function printManualIndex(commands) {
|
|
4207
|
-
console.log(
|
|
4836
|
+
console.log(chalk16.bold("ENVPILOT(1)"));
|
|
4208
4837
|
console.log("TypeScript CLI manual");
|
|
4209
4838
|
blank();
|
|
4210
4839
|
console.log(
|
|
4211
|
-
`Total supported top-level commands: ${
|
|
4840
|
+
`Total supported top-level commands: ${chalk16.green(String(CLI_COMMAND_COUNT))}`
|
|
4212
4841
|
);
|
|
4213
4842
|
blank();
|
|
4214
4843
|
console.log(
|
|
@@ -4216,15 +4845,15 @@ function printManualIndex(commands) {
|
|
|
4216
4845
|
);
|
|
4217
4846
|
blank();
|
|
4218
4847
|
line();
|
|
4219
|
-
console.log(
|
|
4848
|
+
console.log(chalk16.green("Commands"));
|
|
4220
4849
|
for (const command of commands) {
|
|
4221
4850
|
console.log(
|
|
4222
|
-
` ${
|
|
4851
|
+
` ${chalk16.cyan(formatArgv(command.argv).padEnd(24))} ${chalk16.dim(command.description)}`
|
|
4223
4852
|
);
|
|
4224
4853
|
}
|
|
4225
4854
|
blank();
|
|
4226
4855
|
line();
|
|
4227
|
-
console.log(
|
|
4856
|
+
console.log(chalk16.green("Security"));
|
|
4228
4857
|
console.log(" - `.env*` is ignored by the repository `.gitignore`.");
|
|
4229
4858
|
console.log(
|
|
4230
4859
|
" - `envpilot init` and `envpilot sync` ensure env files are added to `.gitignore` locally."
|
|
@@ -4237,7 +4866,7 @@ function printManualIndex(commands) {
|
|
|
4237
4866
|
);
|
|
4238
4867
|
blank();
|
|
4239
4868
|
line();
|
|
4240
|
-
console.log(
|
|
4869
|
+
console.log(chalk16.green("Usage"));
|
|
4241
4870
|
console.log(
|
|
4242
4871
|
" - `envpilot` or `envpilot ui` opens the interactive terminal UI."
|
|
4243
4872
|
);
|
|
@@ -4246,7 +4875,7 @@ function printManualIndex(commands) {
|
|
|
4246
4875
|
);
|
|
4247
4876
|
}
|
|
4248
4877
|
function createManCommand(commands) {
|
|
4249
|
-
return new
|
|
4878
|
+
return new Command15("man").description("Show the CLI manual page").argument("[command]", "Optional command to show detailed manual for").action((command) => {
|
|
4250
4879
|
if (command) {
|
|
4251
4880
|
printCommandManual(command);
|
|
4252
4881
|
return;
|
|
@@ -4256,9 +4885,9 @@ function createManCommand(commands) {
|
|
|
4256
4885
|
}
|
|
4257
4886
|
|
|
4258
4887
|
// src/commands/ui.ts
|
|
4259
|
-
import { Command as
|
|
4888
|
+
import { Command as Command16 } from "commander";
|
|
4260
4889
|
function createUICommand() {
|
|
4261
|
-
return new
|
|
4890
|
+
return new Command16("ui").alias("dashboard").description("Open the interactive Ink-powered terminal UI").action(async () => {
|
|
4262
4891
|
if (process.env.ENVPILOT_TUI_CHILD === "1") {
|
|
4263
4892
|
return;
|
|
4264
4893
|
}
|
|
@@ -4395,13 +5024,14 @@ var COMMAND_CATALOG = [
|
|
|
4395
5024
|
id: "push",
|
|
4396
5025
|
title: "Push variables",
|
|
4397
5026
|
category: "Sync",
|
|
4398
|
-
description: "Upload local variables back to Envpilot
|
|
5027
|
+
description: "Upload local variables back to Envpilot, writing only the keys you have access to.",
|
|
4399
5028
|
argv: ["push"],
|
|
4400
5029
|
args: "[--env <environment>] [--file <path>] [--merge|--replace]",
|
|
4401
5030
|
examples: [["push"], ["push", "--replace"]],
|
|
4402
5031
|
websiteSurface: "Maps to `/api/cli/variables` and `/api/cli/variables/bulk` for writes.",
|
|
4403
5032
|
notes: [
|
|
4404
|
-
"
|
|
5033
|
+
"Owners, project managers, and team leads write across the project; developers write only the variables they hold a write grant for.",
|
|
5034
|
+
"Keys you cannot write are skipped \u2014 push does not create approval requests.",
|
|
4405
5035
|
"Compares local and remote variables before applying changes."
|
|
4406
5036
|
],
|
|
4407
5037
|
keywords: ["upload", "bulk", "merge", "replace"],
|
|
@@ -4577,6 +5207,27 @@ var COMMAND_CATALOG = [
|
|
|
4577
5207
|
topLevel: true,
|
|
4578
5208
|
createCommand: () => whoamiCommand
|
|
4579
5209
|
},
|
|
5210
|
+
{
|
|
5211
|
+
id: "accounts",
|
|
5212
|
+
title: "Manage accounts",
|
|
5213
|
+
category: "Account",
|
|
5214
|
+
description: "List authenticated accounts and switch or remove them without logging out.",
|
|
5215
|
+
argv: ["accounts"],
|
|
5216
|
+
args: "[list|switch <identifier>|remove <identifier>]",
|
|
5217
|
+
examples: [
|
|
5218
|
+
["accounts"],
|
|
5219
|
+
["accounts", "switch", "you@example.com"],
|
|
5220
|
+
["accounts", "remove", "you@example.com"]
|
|
5221
|
+
],
|
|
5222
|
+
websiteSurface: "Local multi-account state \u2014 each `envpilot login` adds an account instead of replacing the current session.",
|
|
5223
|
+
notes: [
|
|
5224
|
+
"Identifiers can be an account id or the account's email (case-insensitive).",
|
|
5225
|
+
"Switching accounts does not log anyone out; use `envpilot logout` to remove the active session."
|
|
5226
|
+
],
|
|
5227
|
+
keywords: ["accounts", "multi-account", "switch", "remove", "identity"],
|
|
5228
|
+
topLevel: true,
|
|
5229
|
+
createCommand: () => accountsCommand
|
|
5230
|
+
},
|
|
4580
5231
|
{
|
|
4581
5232
|
id: "config",
|
|
4582
5233
|
title: "Config",
|