@envpilot/cli 1.17.0 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +19 -6
- package/README.md +35 -12
- package/dist/{app-7IL7GWIN.js → app-VAJPLVBA.js} +1 -1
- package/dist/{chunk-VEZRLJGM.js → chunk-WLHMHDPX.js} +935 -120
- package/dist/index.js +2 -2
- package/package.json +2 -2
|
@@ -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.19.0" : "0.0.0",
|
|
11
11
|
// All EnvPilot surfaces report to one Sentry project; the surface tag
|
|
12
12
|
// is how dashboards tell web / cli / extension events apart.
|
|
13
13
|
initialScope: { tags: { surface: "cli" } },
|
|
@@ -54,16 +54,12 @@ import Conf from "conf";
|
|
|
54
54
|
|
|
55
55
|
// src/lib/roles.ts
|
|
56
56
|
var ROLE_LEVEL = {
|
|
57
|
-
owner:
|
|
58
|
-
project_manager:
|
|
59
|
-
team_lead:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
owner: "Owner",
|
|
64
|
-
project_manager: "Project Manager",
|
|
65
|
-
team_lead: "Team Lead",
|
|
66
|
-
developer: "Developer"
|
|
57
|
+
owner: 100,
|
|
58
|
+
project_manager: 80,
|
|
59
|
+
team_lead: 60,
|
|
60
|
+
editor: 50,
|
|
61
|
+
developer: 40,
|
|
62
|
+
viewer: 20
|
|
67
63
|
};
|
|
68
64
|
function normalizeOrgRole(role) {
|
|
69
65
|
switch (role) {
|
|
@@ -71,20 +67,20 @@ function normalizeOrgRole(role) {
|
|
|
71
67
|
return "owner";
|
|
72
68
|
case "member":
|
|
73
69
|
return "developer";
|
|
74
|
-
case
|
|
75
|
-
case
|
|
76
|
-
case "
|
|
77
|
-
case "developer":
|
|
78
|
-
return role;
|
|
79
|
-
default:
|
|
70
|
+
case void 0:
|
|
71
|
+
case null:
|
|
72
|
+
case "":
|
|
80
73
|
return "developer";
|
|
74
|
+
default:
|
|
75
|
+
return role;
|
|
81
76
|
}
|
|
82
77
|
}
|
|
83
78
|
function roleLevel(role) {
|
|
84
|
-
return ROLE_LEVEL[normalizeOrgRole(role)];
|
|
79
|
+
return ROLE_LEVEL[normalizeOrgRole(role)] ?? 0;
|
|
85
80
|
}
|
|
86
|
-
function formatRoleLabel(role) {
|
|
87
|
-
|
|
81
|
+
function formatRoleLabel(role, meta) {
|
|
82
|
+
if (meta?.displayName) return meta.displayName;
|
|
83
|
+
return normalizeOrgRole(role).split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
88
84
|
}
|
|
89
85
|
function isFileWritable(access) {
|
|
90
86
|
if (access.role === "owner") return true;
|
|
@@ -93,7 +89,7 @@ function isFileWritable(access) {
|
|
|
93
89
|
case "project_manager":
|
|
94
90
|
case "team_lead":
|
|
95
91
|
return true;
|
|
96
|
-
|
|
92
|
+
default:
|
|
97
93
|
return access.hasWriteAccess;
|
|
98
94
|
}
|
|
99
95
|
}
|
|
@@ -153,7 +149,7 @@ function probeCache(projectId, environment, organizationId, ttlSeconds) {
|
|
|
153
149
|
const fresh = isFresh(entry, ttlSeconds);
|
|
154
150
|
return { hit: true, fresh, entry };
|
|
155
151
|
}
|
|
156
|
-
function writeCache(projectId, environment, organizationId, variables, serverFingerprint) {
|
|
152
|
+
function writeCache(projectId, environment, organizationId, variables, serverFingerprint, meta) {
|
|
157
153
|
try {
|
|
158
154
|
const cacheDir = getCacheDir();
|
|
159
155
|
mkdirSync(cacheDir, { recursive: true, mode: 448 });
|
|
@@ -168,7 +164,9 @@ function writeCache(projectId, environment, organizationId, variables, serverFin
|
|
|
168
164
|
environment,
|
|
169
165
|
organizationId,
|
|
170
166
|
apiUrl: getApiUrl(),
|
|
171
|
-
fingerprint: serverFingerprint ?? computeFingerprint(variables)
|
|
167
|
+
fingerprint: serverFingerprint ?? computeFingerprint(variables),
|
|
168
|
+
...meta?.decryptionFailures?.length ? { decryptionFailures: meta.decryptionFailures } : {},
|
|
169
|
+
...meta?.scopeRestricted ? { scopeRestricted: true } : {}
|
|
172
170
|
};
|
|
173
171
|
writeFileSync(path, JSON.stringify(entry, null, 2), {
|
|
174
172
|
encoding: "utf-8",
|
|
@@ -474,7 +472,7 @@ function isInteractiveTerminal() {
|
|
|
474
472
|
async function openTUI() {
|
|
475
473
|
const [{ render }, { CLIApp }, { PressAnyKey }] = await Promise.all([
|
|
476
474
|
import("ink"),
|
|
477
|
-
import("./app-
|
|
475
|
+
import("./app-VAJPLVBA.js"),
|
|
478
476
|
import("./press-any-key-64XFP4O2.js")
|
|
479
477
|
]);
|
|
480
478
|
while (true) {
|
|
@@ -622,6 +620,8 @@ function formatRole(role) {
|
|
|
622
620
|
return chalk.blue(label);
|
|
623
621
|
case "developer":
|
|
624
622
|
return chalk.yellow(label);
|
|
623
|
+
default:
|
|
624
|
+
return label;
|
|
625
625
|
}
|
|
626
626
|
}
|
|
627
627
|
function roleNotice(role) {
|
|
@@ -1084,7 +1084,18 @@ var refs = {
|
|
|
1084
1084
|
),
|
|
1085
1085
|
pullValues: fnRef("features/variables/values:pullValues"),
|
|
1086
1086
|
pushBulk: fnRef("features/variables/values:pushBulk"),
|
|
1087
|
-
createVariableRequest: fnRef("features/variables/requests/actions:createWithValue")
|
|
1087
|
+
createVariableRequest: fnRef("features/variables/requests/actions:createWithValue"),
|
|
1088
|
+
reviewRequest: fnRef("features/variables/requests/mutations:review"),
|
|
1089
|
+
approveRequestWithValue: fnRef("features/variables/requests/actions:approveWithValue"),
|
|
1090
|
+
cancelRequest: fnRef(
|
|
1091
|
+
"features/variables/requests/mutations:cancel"
|
|
1092
|
+
),
|
|
1093
|
+
removeVariable: fnRef(
|
|
1094
|
+
"features/variables/mutations:remove"
|
|
1095
|
+
),
|
|
1096
|
+
removeVariableFromEnvironment: fnRef("features/variables/mutations:removeFromEnvironment"),
|
|
1097
|
+
resolveProjectRoles: fnRef("features/auth/queries:resolveLegacyRoles"),
|
|
1098
|
+
getVariableRequest: fnRef("features/variables/requests/queries:getById")
|
|
1088
1099
|
};
|
|
1089
1100
|
async function convexQuery(ref, ...args) {
|
|
1090
1101
|
const client = await getConvexClient();
|
|
@@ -1227,20 +1238,31 @@ var APIClient = class {
|
|
|
1227
1238
|
* Byte-compatible with the fingerprint writeCache stores from a full fetch:
|
|
1228
1239
|
* both hash `${_id}:${version}:${updatedAt}` over the accessible variables in
|
|
1229
1240
|
* the requested environment.
|
|
1241
|
+
*
|
|
1242
|
+
* Also reports how the environment filter carved up the accessible set, so
|
|
1243
|
+
* `run` can say "injected 8 of 12 — 4 exist only in staging" instead of
|
|
1244
|
+
* silently dropping variables that live in other environments.
|
|
1230
1245
|
*/
|
|
1231
1246
|
async checkFingerprint(projectId, environment, _organizationId) {
|
|
1232
1247
|
const rows = await convexQuery(refs.listVariablesWithAccess, { projectId });
|
|
1233
|
-
const
|
|
1234
|
-
|
|
1248
|
+
const accessible = rows.filter((row) => row.hasAccess);
|
|
1249
|
+
const matching = accessible.filter(
|
|
1250
|
+
(row) => !environment || row.environments.includes(environment)
|
|
1235
1251
|
);
|
|
1236
|
-
const
|
|
1252
|
+
const otherEnvKeys = accessible.filter((row) => environment && !row.environments.includes(environment)).map((row) => ({ key: row.key, environments: row.environments }));
|
|
1253
|
+
const asVariables = matching.map(
|
|
1237
1254
|
(row) => ({
|
|
1238
1255
|
_id: row._id,
|
|
1239
1256
|
version: row.version,
|
|
1240
1257
|
updatedAt: row.updatedAt
|
|
1241
1258
|
})
|
|
1242
1259
|
);
|
|
1243
|
-
return
|
|
1260
|
+
return {
|
|
1261
|
+
fingerprint: computeFingerprint(asVariables),
|
|
1262
|
+
totalAccessible: accessible.length,
|
|
1263
|
+
matchingCount: matching.length,
|
|
1264
|
+
otherEnvKeys
|
|
1265
|
+
};
|
|
1244
1266
|
}
|
|
1245
1267
|
/** List variable requests for a project (Convex). */
|
|
1246
1268
|
async listVariableRequests(projectId, status) {
|
|
@@ -1330,7 +1352,8 @@ var APIClient = class {
|
|
|
1330
1352
|
environmentScope: result.meta.environmentScope,
|
|
1331
1353
|
hasWriteAccess: result.meta.hasWriteAccess,
|
|
1332
1354
|
scopeRestricted: result.meta.scopeRestricted,
|
|
1333
|
-
decryptionFailures: result.meta.decryptionFailures
|
|
1355
|
+
decryptionFailures: result.meta.decryptionFailures,
|
|
1356
|
+
capabilities: result.meta.capabilities
|
|
1334
1357
|
};
|
|
1335
1358
|
return {
|
|
1336
1359
|
variables,
|
|
@@ -1366,6 +1389,125 @@ var APIClient = class {
|
|
|
1366
1389
|
}
|
|
1367
1390
|
return created;
|
|
1368
1391
|
}
|
|
1392
|
+
/** Approve or reject a request that already carries a value (developer flow). */
|
|
1393
|
+
async reviewRequest(requestId, action, reviewReason) {
|
|
1394
|
+
await convexMutation(refs.reviewRequest, {
|
|
1395
|
+
requestId,
|
|
1396
|
+
action,
|
|
1397
|
+
reviewReason
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
/**
|
|
1401
|
+
* Approve a VALUELESS (machine) request by supplying the secret value —
|
|
1402
|
+
* the server encrypts it into the vault at approval time.
|
|
1403
|
+
*/
|
|
1404
|
+
async approveRequestWithValue(requestId, value, reviewReason) {
|
|
1405
|
+
await convexAction(refs.approveRequestWithValue, {
|
|
1406
|
+
requestId,
|
|
1407
|
+
value,
|
|
1408
|
+
reviewReason
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
/** Cancel a pending request (requester or a reviewer). */
|
|
1412
|
+
async cancelRequest(requestId) {
|
|
1413
|
+
await convexMutation(refs.cancelRequest, { requestId });
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Role + capability snapshot for the caller in one project. Used by the
|
|
1417
|
+
* CLI ONLY to route flows (direct write vs request) and phrase denials —
|
|
1418
|
+
* the Convex mutations remain the enforcement boundary.
|
|
1419
|
+
*/
|
|
1420
|
+
async resolveProjectRoles(projectId) {
|
|
1421
|
+
return convexQuery(refs.resolveProjectRoles, { projectId });
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* One request by id — enough to know whether it is pending and whether it
|
|
1425
|
+
* carries a value (machine requests don't; approval must supply one).
|
|
1426
|
+
*/
|
|
1427
|
+
async getVariableRequest(requestId) {
|
|
1428
|
+
return convexQuery(refs.getVariableRequest, { requestId });
|
|
1429
|
+
}
|
|
1430
|
+
/** Soft-delete a single variable by id (moves it to trash). */
|
|
1431
|
+
async removeVariable(variableId) {
|
|
1432
|
+
await convexMutation(refs.removeVariable, { variableId });
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* Set a single variable's value in one environment — upsert via the bulk
|
|
1436
|
+
* vault path with merge semantics (creates if absent, updates if present;
|
|
1437
|
+
* leaves every other variable untouched).
|
|
1438
|
+
*
|
|
1439
|
+
* Throws when the server accepted the call but wrote nothing (the single
|
|
1440
|
+
* entry was skipped/denied by variable-level authorization) — a silent
|
|
1441
|
+
* "success" here would lie to the user.
|
|
1442
|
+
*/
|
|
1443
|
+
async setVariable(projectId, environment, key, value, opts) {
|
|
1444
|
+
const result = await convexAction(refs.pushBulk, {
|
|
1445
|
+
projectId,
|
|
1446
|
+
environment,
|
|
1447
|
+
variables: [
|
|
1448
|
+
{
|
|
1449
|
+
key,
|
|
1450
|
+
value,
|
|
1451
|
+
description: opts?.description,
|
|
1452
|
+
isSensitive: opts?.isSensitive
|
|
1453
|
+
}
|
|
1454
|
+
],
|
|
1455
|
+
mode: "merge"
|
|
1456
|
+
});
|
|
1457
|
+
if (result.deniedKeys?.includes(key)) {
|
|
1458
|
+
throw new APIError(
|
|
1459
|
+
`You do not have write access to ${key}.`,
|
|
1460
|
+
403,
|
|
1461
|
+
"PERMISSION_DENIED"
|
|
1462
|
+
);
|
|
1463
|
+
}
|
|
1464
|
+
return {
|
|
1465
|
+
created: result.created,
|
|
1466
|
+
updated: result.updated,
|
|
1467
|
+
unchanged: result.created === 0 && result.updated === 0
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* Metadata for the active variable holding `key` in `environment` (id +
|
|
1472
|
+
* the FULL environments list, so single-env commands can detect that the
|
|
1473
|
+
* variable is shared across environments). Returns null if absent.
|
|
1474
|
+
*/
|
|
1475
|
+
async findVariable(projectId, environment, key) {
|
|
1476
|
+
const rows = await convexQuery(refs.listVariablesWithAccess, {
|
|
1477
|
+
projectId,
|
|
1478
|
+
limit: 5e3
|
|
1479
|
+
});
|
|
1480
|
+
const match = rows.find(
|
|
1481
|
+
(row) => row.key === key && row.hasAccess && row.environments.includes(environment)
|
|
1482
|
+
);
|
|
1483
|
+
return match ? { _id: match._id, environments: match.environments } : null;
|
|
1484
|
+
}
|
|
1485
|
+
/**
|
|
1486
|
+
* Metadata-only variable listing for an environment (no vault reads, no
|
|
1487
|
+
* value-access audit entries). Used by `diff` without --values.
|
|
1488
|
+
*/
|
|
1489
|
+
async listVariableKeys(projectId, environment) {
|
|
1490
|
+
const limit = 5e3;
|
|
1491
|
+
const rows = await convexQuery(refs.listVariablesWithAccess, {
|
|
1492
|
+
projectId,
|
|
1493
|
+
limit
|
|
1494
|
+
});
|
|
1495
|
+
return {
|
|
1496
|
+
keys: rows.filter((row) => row.environments.includes(environment)).map((row) => row.key),
|
|
1497
|
+
truncated: rows.length >= limit
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1501
|
+
* Re-scope a shared variable: atomically detach `environment` from it
|
|
1502
|
+
* server-side (the mutation re-reads the row, so a concurrent edit can't
|
|
1503
|
+
* be clobbered by a stale client-side array).
|
|
1504
|
+
*/
|
|
1505
|
+
async removeVariableFromEnvironment(variableId, environment) {
|
|
1506
|
+
await convexMutation(refs.removeVariableFromEnvironment, {
|
|
1507
|
+
variableId,
|
|
1508
|
+
environment
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1369
1511
|
};
|
|
1370
1512
|
function createAPIClient() {
|
|
1371
1513
|
return new APIClient();
|
|
@@ -1566,7 +1708,9 @@ var projectSchema = z2.object({
|
|
|
1566
1708
|
// Unified-role fields (optional so legacy server responses still parse)
|
|
1567
1709
|
unifiedRole: z2.string().nullable().optional(),
|
|
1568
1710
|
assigned: z2.boolean().optional(),
|
|
1569
|
-
environmentScope: z2.array(z2.string()).nullable().optional()
|
|
1711
|
+
environmentScope: z2.array(z2.string()).nullable().optional(),
|
|
1712
|
+
// Resolved capability map (additive; absent on older deployments).
|
|
1713
|
+
capabilities: z2.record(z2.string(), z2.boolean()).nullable().optional()
|
|
1570
1714
|
});
|
|
1571
1715
|
var variableTagSchema = z2.object({
|
|
1572
1716
|
_id: z2.string(),
|
|
@@ -1599,7 +1743,9 @@ var variablesMetaSchema = z2.object({
|
|
|
1599
1743
|
grantOnly: z2.boolean().optional(),
|
|
1600
1744
|
environmentScope: z2.array(z2.string()).nullable().optional(),
|
|
1601
1745
|
hasWriteAccess: z2.boolean().optional(),
|
|
1602
|
-
scopeRestricted: z2.boolean().optional()
|
|
1746
|
+
scopeRestricted: z2.boolean().optional(),
|
|
1747
|
+
// Resolved capability map (additive; absent on older deployments).
|
|
1748
|
+
capabilities: z2.record(z2.string(), z2.boolean()).optional()
|
|
1603
1749
|
}).passthrough();
|
|
1604
1750
|
var environmentSchema = z2.enum([
|
|
1605
1751
|
"development",
|
|
@@ -2950,6 +3096,9 @@ function validateEnvVars(vars) {
|
|
|
2950
3096
|
}
|
|
2951
3097
|
return { valid, invalid };
|
|
2952
3098
|
}
|
|
3099
|
+
function validateEnvironment(env) {
|
|
3100
|
+
return environmentSchema2.safeParse(env).success;
|
|
3101
|
+
}
|
|
2953
3102
|
|
|
2954
3103
|
// src/commands/push.ts
|
|
2955
3104
|
var pushCommand = new Command4("push").description("Upload local .env file to cloud").option(
|
|
@@ -3949,8 +4098,8 @@ async function handlePath() {
|
|
|
3949
4098
|
]);
|
|
3950
4099
|
}
|
|
3951
4100
|
async function handleReset() {
|
|
3952
|
-
const
|
|
3953
|
-
const { confirm } = await
|
|
4101
|
+
const inquirer9 = await import("inquirer");
|
|
4102
|
+
const { confirm } = await inquirer9.default.prompt([
|
|
3954
4103
|
{
|
|
3955
4104
|
type: "confirm",
|
|
3956
4105
|
name: "confirm",
|
|
@@ -4707,7 +4856,7 @@ import { Command as Command14 } from "commander";
|
|
|
4707
4856
|
import crossSpawn from "cross-spawn";
|
|
4708
4857
|
import { constants as osConstants } from "os";
|
|
4709
4858
|
import chalk15 from "chalk";
|
|
4710
|
-
var DEFAULT_TTL =
|
|
4859
|
+
var DEFAULT_TTL = 0;
|
|
4711
4860
|
var runCommand = new Command14("run").description(
|
|
4712
4861
|
"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."
|
|
4713
4862
|
).argument("[command...]", "Command and arguments to execute").option(
|
|
@@ -4730,7 +4879,7 @@ var runCommand = new Command14("run").description(
|
|
|
4730
4879
|
'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).'
|
|
4731
4880
|
).option("-q, --quiet", "Suppress informational messages").option("--no-cache", "Skip cache and always fetch fresh secrets from server").option(
|
|
4732
4881
|
"--cache-ttl <seconds>",
|
|
4733
|
-
"How long cached secrets
|
|
4882
|
+
"How long cached secrets are served without even a fingerprint check (default: 0 = verify freshness on every run; the check is one cheap metadata query, secrets are only re-decrypted when they changed)",
|
|
4734
4883
|
String(DEFAULT_TTL)
|
|
4735
4884
|
).passThroughOptions().allowExcessArguments().action(async (commandArgs, options) => {
|
|
4736
4885
|
try {
|
|
@@ -4757,7 +4906,12 @@ var runCommand = new Command14("run").description(
|
|
|
4757
4906
|
}
|
|
4758
4907
|
throw notInitialized();
|
|
4759
4908
|
}
|
|
4760
|
-
const environment = options.env
|
|
4909
|
+
const environment = options.env !== void 0 ? options.env : project.environment;
|
|
4910
|
+
if (options.env !== void 0 && !validateEnvironment(options.env)) {
|
|
4911
|
+
throw invalidInput(
|
|
4912
|
+
`Unknown environment "${options.env}". Valid environments: development, staging, production.`
|
|
4913
|
+
);
|
|
4914
|
+
}
|
|
4761
4915
|
const organizationId = options.organization || project.organizationId;
|
|
4762
4916
|
const parsedTtl = Number.parseInt(
|
|
4763
4917
|
options.cacheTtl ?? String(DEFAULT_TTL),
|
|
@@ -4767,14 +4921,35 @@ var runCommand = new Command14("run").description(
|
|
|
4767
4921
|
let variables;
|
|
4768
4922
|
let cacheHit = false;
|
|
4769
4923
|
let cacheAge = "";
|
|
4924
|
+
let otherEnvKeys = [];
|
|
4925
|
+
let decryptionFailures = [];
|
|
4926
|
+
let scopeRestricted = false;
|
|
4770
4927
|
if (options.cache === false) {
|
|
4771
|
-
|
|
4928
|
+
const fetched = await doFetch(
|
|
4772
4929
|
project,
|
|
4773
4930
|
environment,
|
|
4774
4931
|
organizationId,
|
|
4775
4932
|
options.quiet
|
|
4776
4933
|
);
|
|
4777
|
-
|
|
4934
|
+
variables = fetched.variables;
|
|
4935
|
+
decryptionFailures = fetched.decryptionFailures;
|
|
4936
|
+
scopeRestricted = fetched.scopeRestricted;
|
|
4937
|
+
otherEnvKeys = await fetchOtherEnvKeys(
|
|
4938
|
+
project.projectId,
|
|
4939
|
+
environment,
|
|
4940
|
+
organizationId
|
|
4941
|
+
);
|
|
4942
|
+
writeCache(
|
|
4943
|
+
project.projectId,
|
|
4944
|
+
environment,
|
|
4945
|
+
organizationId,
|
|
4946
|
+
variables,
|
|
4947
|
+
void 0,
|
|
4948
|
+
{
|
|
4949
|
+
decryptionFailures,
|
|
4950
|
+
scopeRestricted
|
|
4951
|
+
}
|
|
4952
|
+
);
|
|
4778
4953
|
} else {
|
|
4779
4954
|
const probe = probeCache(
|
|
4780
4955
|
project.projectId,
|
|
@@ -4786,15 +4961,18 @@ var runCommand = new Command14("run").description(
|
|
|
4786
4961
|
variables = probe.entry.variables;
|
|
4787
4962
|
cacheHit = true;
|
|
4788
4963
|
cacheAge = formatAge(probe.entry.fetchedAt);
|
|
4789
|
-
|
|
4964
|
+
decryptionFailures = probe.entry.decryptionFailures ?? [];
|
|
4965
|
+
scopeRestricted = probe.entry.scopeRestricted ?? false;
|
|
4966
|
+
} else {
|
|
4790
4967
|
try {
|
|
4791
4968
|
const api = createAPIClient();
|
|
4792
|
-
const
|
|
4969
|
+
const check = await api.checkFingerprint(
|
|
4793
4970
|
project.projectId,
|
|
4794
4971
|
environment,
|
|
4795
4972
|
organizationId
|
|
4796
4973
|
);
|
|
4797
|
-
|
|
4974
|
+
otherEnvKeys = check.otherEnvKeys;
|
|
4975
|
+
if (probe.hit && check.fingerprint === probe.entry.fingerprint) {
|
|
4798
4976
|
extendCacheFreshness(
|
|
4799
4977
|
project.projectId,
|
|
4800
4978
|
environment,
|
|
@@ -4803,27 +4981,35 @@ var runCommand = new Command14("run").description(
|
|
|
4803
4981
|
variables = probe.entry.variables;
|
|
4804
4982
|
cacheHit = true;
|
|
4805
4983
|
cacheAge = formatAge(probe.entry.fetchedAt);
|
|
4984
|
+
decryptionFailures = probe.entry.decryptionFailures ?? [];
|
|
4985
|
+
scopeRestricted = probe.entry.scopeRestricted ?? false;
|
|
4806
4986
|
} else {
|
|
4807
|
-
|
|
4987
|
+
const fetched = await doFetch(
|
|
4808
4988
|
project,
|
|
4809
4989
|
environment,
|
|
4810
4990
|
organizationId,
|
|
4811
4991
|
options.quiet,
|
|
4812
|
-
"Secrets updated, refreshing"
|
|
4992
|
+
probe.hit ? "Secrets updated, refreshing" : "Loading"
|
|
4813
4993
|
);
|
|
4994
|
+
variables = fetched.variables;
|
|
4995
|
+
decryptionFailures = fetched.decryptionFailures;
|
|
4996
|
+
scopeRestricted = fetched.scopeRestricted;
|
|
4814
4997
|
writeCache(
|
|
4815
4998
|
project.projectId,
|
|
4816
4999
|
environment,
|
|
4817
5000
|
organizationId,
|
|
4818
5001
|
variables,
|
|
4819
|
-
|
|
5002
|
+
decryptionFailures.length > 0 ? void 0 : check.fingerprint,
|
|
5003
|
+
{ decryptionFailures, scopeRestricted }
|
|
4820
5004
|
);
|
|
4821
5005
|
}
|
|
4822
5006
|
} catch (err) {
|
|
4823
|
-
if (isConnectivityError(err)) {
|
|
5007
|
+
if (probe.hit && isConnectivityError(err)) {
|
|
4824
5008
|
variables = probe.entry.variables;
|
|
4825
5009
|
cacheHit = true;
|
|
4826
5010
|
cacheAge = formatAge(probe.entry.fetchedAt);
|
|
5011
|
+
decryptionFailures = probe.entry.decryptionFailures ?? [];
|
|
5012
|
+
scopeRestricted = probe.entry.scopeRestricted ?? false;
|
|
4827
5013
|
if (!options.quiet) {
|
|
4828
5014
|
warning(
|
|
4829
5015
|
`Using offline cache (age ${cacheAge}) \u2014 could not reach the server to verify freshness.`
|
|
@@ -4834,14 +5020,6 @@ var runCommand = new Command14("run").description(
|
|
|
4834
5020
|
throw err;
|
|
4835
5021
|
}
|
|
4836
5022
|
}
|
|
4837
|
-
} else {
|
|
4838
|
-
variables = await doFetch(
|
|
4839
|
-
project,
|
|
4840
|
-
environment,
|
|
4841
|
-
organizationId,
|
|
4842
|
-
options.quiet
|
|
4843
|
-
);
|
|
4844
|
-
writeCache(project.projectId, environment, organizationId, variables);
|
|
4845
5023
|
}
|
|
4846
5024
|
}
|
|
4847
5025
|
if (variables.length === 0) {
|
|
@@ -4849,6 +5027,13 @@ var runCommand = new Command14("run").description(
|
|
|
4849
5027
|
`No variables found for ${environment}. Running command without injected secrets.`
|
|
4850
5028
|
);
|
|
4851
5029
|
}
|
|
5030
|
+
if (!options.quiet && decryptionFailures.length > 0) {
|
|
5031
|
+
for (const key of decryptionFailures) {
|
|
5032
|
+
warning(
|
|
5033
|
+
`Could not decrypt ${chalk15.bold(key)} \u2014 skipped (vault error, check server logs)`
|
|
5034
|
+
);
|
|
5035
|
+
}
|
|
5036
|
+
}
|
|
4852
5037
|
const secrets = {};
|
|
4853
5038
|
for (const v of variables) {
|
|
4854
5039
|
secrets[v.key] = v.value;
|
|
@@ -4870,9 +5055,24 @@ var runCommand = new Command14("run").description(
|
|
|
4870
5055
|
if (!options.quiet) {
|
|
4871
5056
|
const injectedCount = Object.keys(secrets).length;
|
|
4872
5057
|
const cacheTag = cacheHit ? chalk15.dim(` \u26A1 cache (${cacheAge})`) : "";
|
|
5058
|
+
const total = injectedCount + decryptionFailures.length + otherEnvKeys.length;
|
|
5059
|
+
const ofTotal = otherEnvKeys.length > 0 ? chalk15.dim(` of ${total}`) : "";
|
|
4873
5060
|
info(
|
|
4874
|
-
`Injected ${chalk15.bold(injectedCount)} ${injectedCount === 1 ? "variable" : "variables"} from ${chalk15.bold(`${project.projectName || project.projectId}/${environment}`)}${cacheTag}`
|
|
5061
|
+
`Injected ${chalk15.bold(injectedCount)}${ofTotal} ${injectedCount === 1 ? "variable" : "variables"} from ${chalk15.bold(`${project.projectName || project.projectId}/${environment}`)}${cacheTag}`
|
|
4875
5062
|
);
|
|
5063
|
+
if (otherEnvKeys.length > 0) {
|
|
5064
|
+
const names = otherEnvKeys.slice(0, 5).map((v) => v.key);
|
|
5065
|
+
const more = otherEnvKeys.length > 5 ? `, +${otherEnvKeys.length - 5} more` : "";
|
|
5066
|
+
warning(
|
|
5067
|
+
`${otherEnvKeys.length} variable${otherEnvKeys.length === 1 ? "" : "s"} not in ${chalk15.bold(environment)}, not injected: ${names.join(", ")}${more}`
|
|
5068
|
+
);
|
|
5069
|
+
info(`Use -e <environment> to load a different environment.`);
|
|
5070
|
+
}
|
|
5071
|
+
if (scopeRestricted) {
|
|
5072
|
+
warning(
|
|
5073
|
+
`Your role restricts which variables you can see \u2014 some may be withheld for ${chalk15.bold(environment)}.`
|
|
5074
|
+
);
|
|
5075
|
+
}
|
|
4876
5076
|
if (overridden.length > 0) {
|
|
4877
5077
|
warning(
|
|
4878
5078
|
`Overriding ${overridden.length} shell var${overridden.length === 1 ? "" : "s"}: ${overridden.slice(0, 3).join(", ")}${overridden.length > 3 ? `, +${overridden.length - 3} more` : ""}`
|
|
@@ -4889,18 +5089,28 @@ var runCommand = new Command14("run").description(
|
|
|
4889
5089
|
async function doFetch(project, environment, organizationId, quiet, labelPrefix = "Loading") {
|
|
4890
5090
|
const label = `${labelPrefix} ${chalk15.bold(environment)} secrets for ${chalk15.bold(project.projectName || project.projectId)}...`;
|
|
4891
5091
|
const api = createAPIClient();
|
|
4892
|
-
const { variables, decryptionFailures } = quiet ? await api.listVariables(project.projectId, environment, organizationId) : await withSpinner(
|
|
5092
|
+
const { variables, meta, decryptionFailures } = quiet ? await api.listVariables(project.projectId, environment, organizationId) : await withSpinner(
|
|
4893
5093
|
label,
|
|
4894
5094
|
() => api.listVariables(project.projectId, environment, organizationId)
|
|
4895
5095
|
);
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
5096
|
+
return {
|
|
5097
|
+
variables,
|
|
5098
|
+
decryptionFailures,
|
|
5099
|
+
scopeRestricted: meta?.scopeRestricted ?? false
|
|
5100
|
+
};
|
|
5101
|
+
}
|
|
5102
|
+
async function fetchOtherEnvKeys(projectId, environment, organizationId) {
|
|
5103
|
+
try {
|
|
5104
|
+
const api = createAPIClient();
|
|
5105
|
+
const check = await api.checkFingerprint(
|
|
5106
|
+
projectId,
|
|
5107
|
+
environment,
|
|
5108
|
+
organizationId
|
|
5109
|
+
);
|
|
5110
|
+
return check.otherEnvKeys;
|
|
5111
|
+
} catch {
|
|
5112
|
+
return [];
|
|
4902
5113
|
}
|
|
4903
|
-
return variables;
|
|
4904
5114
|
}
|
|
4905
5115
|
function printInjectionPreview(secrets, project, environment) {
|
|
4906
5116
|
const keys = Object.keys(secrets).sort();
|
|
@@ -5156,9 +5366,17 @@ function buildCreateVariableRequestBody(input) {
|
|
|
5156
5366
|
isSensitive: input.isSensitive ?? false
|
|
5157
5367
|
};
|
|
5158
5368
|
}
|
|
5369
|
+
function canSubmitRequests(input) {
|
|
5370
|
+
if (input.capabilities) {
|
|
5371
|
+
return input.capabilities["project.requests.submit"] === true;
|
|
5372
|
+
}
|
|
5373
|
+
return normalizeOrgRole(input.role) === "developer";
|
|
5374
|
+
}
|
|
5159
5375
|
function isRequestEligibleProject(project) {
|
|
5160
|
-
|
|
5161
|
-
|
|
5376
|
+
return canSubmitRequests({
|
|
5377
|
+
capabilities: project.capabilities,
|
|
5378
|
+
role: project.unifiedRole ?? project.role
|
|
5379
|
+
}) && project.assigned === true;
|
|
5162
5380
|
}
|
|
5163
5381
|
function buildEligibleRequestTargets(projects, orgNameById) {
|
|
5164
5382
|
return projects.filter(isRequestEligibleProject).map((p) => ({
|
|
@@ -5206,6 +5424,7 @@ function formatRequestsListHeader(input) {
|
|
|
5206
5424
|
}
|
|
5207
5425
|
function formatRequestRow(request) {
|
|
5208
5426
|
return {
|
|
5427
|
+
id: request._id,
|
|
5209
5428
|
key: request.key,
|
|
5210
5429
|
environments: request.environments.join(", "),
|
|
5211
5430
|
status: request.status,
|
|
@@ -5257,8 +5476,11 @@ var requestCommand = new Command17("request").description(
|
|
|
5257
5476
|
}
|
|
5258
5477
|
if (useDefault) {
|
|
5259
5478
|
const meta = await fetchProjectMeta(api, defaultEntry);
|
|
5260
|
-
const
|
|
5261
|
-
|
|
5479
|
+
const eligible = canSubmitRequests({
|
|
5480
|
+
capabilities: meta?.capabilities,
|
|
5481
|
+
role: meta?.unifiedRole ?? meta?.role
|
|
5482
|
+
});
|
|
5483
|
+
if (!eligible) {
|
|
5262
5484
|
warning(
|
|
5263
5485
|
"You have direct write access to this project. Use `envpilot push` or create the variable directly instead of submitting a request."
|
|
5264
5486
|
);
|
|
@@ -5372,10 +5594,6 @@ var requestCommand = new Command17("request").description(
|
|
|
5372
5594
|
);
|
|
5373
5595
|
info(`Status: ${created.status} \xB7 Id: ${created._id}`);
|
|
5374
5596
|
} catch (err) {
|
|
5375
|
-
if (err instanceof APIError && err.statusCode === 403) {
|
|
5376
|
-
error(err.message);
|
|
5377
|
-
return;
|
|
5378
|
-
}
|
|
5379
5597
|
await handleError(err);
|
|
5380
5598
|
}
|
|
5381
5599
|
});
|
|
@@ -5471,66 +5689,608 @@ async function pickRequestTarget(api) {
|
|
|
5471
5689
|
|
|
5472
5690
|
// src/commands/requests.ts
|
|
5473
5691
|
import { Command as Command18 } from "commander";
|
|
5474
|
-
|
|
5692
|
+
import inquirer7 from "inquirer";
|
|
5693
|
+
async function readStdin() {
|
|
5694
|
+
const chunks = [];
|
|
5695
|
+
for await (const chunk of process.stdin) {
|
|
5696
|
+
chunks.push(Buffer.from(chunk));
|
|
5697
|
+
}
|
|
5698
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
5699
|
+
}
|
|
5700
|
+
async function runList(options) {
|
|
5701
|
+
if (!isAuthenticated()) throw notAuthenticated();
|
|
5702
|
+
const configV2 = readProjectConfigV2();
|
|
5703
|
+
if (!configV2) throw notInitialized();
|
|
5704
|
+
const project = resolveProject(configV2, options.project);
|
|
5705
|
+
if (!project) {
|
|
5706
|
+
error(`Project not found: ${options.project}`);
|
|
5707
|
+
console.log();
|
|
5708
|
+
console.log("Linked projects:");
|
|
5709
|
+
for (const p of configV2.projects) {
|
|
5710
|
+
console.log(` ${p.projectName || p.projectId} (${p.environment})`);
|
|
5711
|
+
}
|
|
5712
|
+
process.exit(1);
|
|
5713
|
+
}
|
|
5714
|
+
let status;
|
|
5715
|
+
if (options.status) {
|
|
5716
|
+
const parsed = variableRequestStatusSchema.safeParse(options.status);
|
|
5717
|
+
if (!parsed.success) {
|
|
5718
|
+
error(
|
|
5719
|
+
`Invalid status: ${options.status}. Must be one of pending, approved, rejected, canceled.`
|
|
5720
|
+
);
|
|
5721
|
+
process.exit(1);
|
|
5722
|
+
}
|
|
5723
|
+
status = parsed.data;
|
|
5724
|
+
}
|
|
5725
|
+
const api = createAPIClient();
|
|
5726
|
+
const requests = await withSpinner(
|
|
5727
|
+
"Fetching variable requests...",
|
|
5728
|
+
() => api.listVariableRequests(project.projectId, status)
|
|
5729
|
+
);
|
|
5730
|
+
if (options.json) {
|
|
5731
|
+
console.log(JSON.stringify(requests, null, 2));
|
|
5732
|
+
return;
|
|
5733
|
+
}
|
|
5734
|
+
header(
|
|
5735
|
+
formatRequestsListHeader({
|
|
5736
|
+
projectName: project.projectName || project.projectId,
|
|
5737
|
+
organizationName: project.organizationName || project.organizationId
|
|
5738
|
+
})
|
|
5739
|
+
);
|
|
5740
|
+
if (requests.length === 0) {
|
|
5741
|
+
info("No variable requests found.");
|
|
5742
|
+
return;
|
|
5743
|
+
}
|
|
5744
|
+
table(formatRequestRows(requests), [
|
|
5745
|
+
{ key: "id", header: "ID" },
|
|
5746
|
+
{ key: "key", header: "KEY" },
|
|
5747
|
+
{ key: "environments", header: "ENVIRONMENTS" },
|
|
5748
|
+
{ key: "status", header: "STATUS" },
|
|
5749
|
+
{ key: "requested", header: "REQUESTED" },
|
|
5750
|
+
{ key: "reason", header: "REASON" }
|
|
5751
|
+
]);
|
|
5752
|
+
}
|
|
5753
|
+
var listSub = new Command18("list").description("List variable requests for a project").option(
|
|
5475
5754
|
"--project <name-or-id>",
|
|
5476
5755
|
"List requests for a specific linked project"
|
|
5756
|
+
).option("--status <status>", "Filter: pending, approved, rejected, canceled").option("--json", "Output as JSON").action(async (options) => {
|
|
5757
|
+
try {
|
|
5758
|
+
await runList(options);
|
|
5759
|
+
} catch (err) {
|
|
5760
|
+
await handleError(err);
|
|
5761
|
+
}
|
|
5762
|
+
});
|
|
5763
|
+
var approveSub = new Command18("approve").description("Approve a pending request").argument("<id>", "Request id (from `envpilot requests`)").option(
|
|
5764
|
+
"--value <value>",
|
|
5765
|
+
"Secret value for a machine (valueless) request \u2014 CI only; interactively you are prompted MASKED instead so the value stays out of shell history"
|
|
5477
5766
|
).option(
|
|
5478
|
-
"--
|
|
5479
|
-
|
|
5480
|
-
).
|
|
5767
|
+
"--value-stdin",
|
|
5768
|
+
'Read the secret value from stdin (CI-safe: keeps it out of argv), e.g. printf %s "$SECRET" | envpilot requests approve <id> --value-stdin'
|
|
5769
|
+
).option("--reason <text>", "Optional review note").action(
|
|
5770
|
+
async (id, options) => {
|
|
5771
|
+
try {
|
|
5772
|
+
if (!isAuthenticated()) throw notAuthenticated();
|
|
5773
|
+
const api = createAPIClient();
|
|
5774
|
+
const request = await withSpinner(
|
|
5775
|
+
"Loading request...",
|
|
5776
|
+
() => api.getVariableRequest(id)
|
|
5777
|
+
);
|
|
5778
|
+
if (!request) {
|
|
5779
|
+
error("Request not found (or you lack access to it).");
|
|
5780
|
+
process.exit(1);
|
|
5781
|
+
}
|
|
5782
|
+
if (request.status !== "pending") {
|
|
5783
|
+
error(
|
|
5784
|
+
`Only pending requests can be approved (current: ${request.status}).`
|
|
5785
|
+
);
|
|
5786
|
+
process.exit(1);
|
|
5787
|
+
}
|
|
5788
|
+
let value = options.value;
|
|
5789
|
+
if (value !== void 0 && process.stdin.isTTY) {
|
|
5790
|
+
error(
|
|
5791
|
+
"--value is for CI only \u2014 interactively you are prompted masked so the secret stays out of shell history. Re-run without --value."
|
|
5792
|
+
);
|
|
5793
|
+
process.exit(1);
|
|
5794
|
+
}
|
|
5795
|
+
if (options.valueStdin) {
|
|
5796
|
+
value = (await readStdin()).replace(/\r?\n$/, "");
|
|
5797
|
+
if (!value) {
|
|
5798
|
+
error("--value-stdin was passed but stdin was empty.");
|
|
5799
|
+
process.exit(1);
|
|
5800
|
+
}
|
|
5801
|
+
}
|
|
5802
|
+
if (request.vaultRef === void 0 && value === void 0) {
|
|
5803
|
+
if (!process.stdin.isTTY) {
|
|
5804
|
+
error(
|
|
5805
|
+
`Request ${request.key} carries no value \u2014 non-interactive approval needs --value.`
|
|
5806
|
+
);
|
|
5807
|
+
process.exit(1);
|
|
5808
|
+
}
|
|
5809
|
+
warning(
|
|
5810
|
+
`${request.key} is a machine request with no value \u2014 you supply it now (encrypted server-side).`
|
|
5811
|
+
);
|
|
5812
|
+
const answer = await inquirer7.prompt([
|
|
5813
|
+
{
|
|
5814
|
+
type: "password",
|
|
5815
|
+
name: "value",
|
|
5816
|
+
message: `Value for ${request.key}:`,
|
|
5817
|
+
mask: "*",
|
|
5818
|
+
validate: (input) => input.length > 0 || "Value cannot be empty."
|
|
5819
|
+
}
|
|
5820
|
+
]);
|
|
5821
|
+
value = answer.value;
|
|
5822
|
+
}
|
|
5823
|
+
await withSpinner(
|
|
5824
|
+
"Approving request...",
|
|
5825
|
+
() => request.vaultRef === void 0 && value !== void 0 ? api.approveRequestWithValue(id, value, options.reason) : api.reviewRequest(id, "approve", options.reason)
|
|
5826
|
+
);
|
|
5827
|
+
success(`Request approved: ${request.key}.`);
|
|
5828
|
+
} catch (err) {
|
|
5829
|
+
await handleError(err);
|
|
5830
|
+
}
|
|
5831
|
+
}
|
|
5832
|
+
);
|
|
5833
|
+
var rejectSub = new Command18("reject").description("Reject a pending request").argument("<id>", "Request id (from `envpilot requests`)").option("--reason <text>", "Optional review note shown to the requester").action(async (id, options) => {
|
|
5481
5834
|
try {
|
|
5482
|
-
if (!isAuthenticated())
|
|
5483
|
-
|
|
5835
|
+
if (!isAuthenticated()) throw notAuthenticated();
|
|
5836
|
+
const api = createAPIClient();
|
|
5837
|
+
await withSpinner(
|
|
5838
|
+
"Rejecting request...",
|
|
5839
|
+
() => api.reviewRequest(id, "reject", options.reason)
|
|
5840
|
+
);
|
|
5841
|
+
success("Request rejected.");
|
|
5842
|
+
} catch (err) {
|
|
5843
|
+
await handleError(err);
|
|
5844
|
+
}
|
|
5845
|
+
});
|
|
5846
|
+
var cancelSub = new Command18("cancel").description("Cancel a pending request (your own, or as a reviewer)").argument("<id>", "Request id (from `envpilot requests`)").action(async (id) => {
|
|
5847
|
+
try {
|
|
5848
|
+
if (!isAuthenticated()) throw notAuthenticated();
|
|
5849
|
+
const api = createAPIClient();
|
|
5850
|
+
await withSpinner("Canceling request...", () => api.cancelRequest(id));
|
|
5851
|
+
success("Request canceled.");
|
|
5852
|
+
} catch (err) {
|
|
5853
|
+
await handleError(err);
|
|
5854
|
+
}
|
|
5855
|
+
});
|
|
5856
|
+
var requestsCommand = new Command18("requests").description("List and review variable requests").option(
|
|
5857
|
+
"--project <name-or-id>",
|
|
5858
|
+
"List requests for a specific linked project"
|
|
5859
|
+
).option("--status <status>", "Filter: pending, approved, rejected, canceled").option("--json", "Output as JSON").action(async (options) => {
|
|
5860
|
+
try {
|
|
5861
|
+
await runList(options);
|
|
5862
|
+
} catch (err) {
|
|
5863
|
+
await handleError(err);
|
|
5864
|
+
}
|
|
5865
|
+
}).addCommand(listSub).addCommand(approveSub).addCommand(rejectSub).addCommand(cancelSub);
|
|
5866
|
+
|
|
5867
|
+
// src/commands/secrets.ts
|
|
5868
|
+
import { Command as Command19 } from "commander";
|
|
5869
|
+
import inquirer8 from "inquirer";
|
|
5870
|
+
import chalk18 from "chalk";
|
|
5871
|
+
var KEY_PATTERN = /^[A-Z][A-Z0-9_]*$/;
|
|
5872
|
+
function parseAssignment(assignment) {
|
|
5873
|
+
const eq = assignment.indexOf("=");
|
|
5874
|
+
if (eq < 1) {
|
|
5875
|
+
return {
|
|
5876
|
+
ok: false,
|
|
5877
|
+
error: "Expected KEY=VALUE (e.g. envpilot secrets set API_URL=https://api.example.com)"
|
|
5878
|
+
};
|
|
5879
|
+
}
|
|
5880
|
+
const key = assignment.slice(0, eq);
|
|
5881
|
+
const value = assignment.slice(eq + 1);
|
|
5882
|
+
if (!KEY_PATTERN.test(key) || key.length > 100) {
|
|
5883
|
+
return {
|
|
5884
|
+
ok: false,
|
|
5885
|
+
error: "Variable key must be UPPER_SNAKE_CASE (A-Z, 0-9, _), start with a letter, max 100 chars."
|
|
5886
|
+
};
|
|
5887
|
+
}
|
|
5888
|
+
return { ok: true, key, value };
|
|
5889
|
+
}
|
|
5890
|
+
function resolveTarget(options) {
|
|
5891
|
+
if (!isAuthenticated()) throw notAuthenticated();
|
|
5892
|
+
const config2 = readProjectConfigV2();
|
|
5893
|
+
if (!config2) throw notInitialized();
|
|
5894
|
+
const project = resolveProject(config2, options.project);
|
|
5895
|
+
if (!project) {
|
|
5896
|
+
error(`Project not found: ${options.project ?? "(active)"}`);
|
|
5897
|
+
console.log();
|
|
5898
|
+
console.log("Linked projects:");
|
|
5899
|
+
for (const p of config2.projects) {
|
|
5900
|
+
console.log(` ${p.projectName || p.projectId} (${p.environment})`);
|
|
5484
5901
|
}
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5902
|
+
process.exit(1);
|
|
5903
|
+
}
|
|
5904
|
+
const environment = options.env !== void 0 ? options.env : project.environment;
|
|
5905
|
+
if (!validateEnvironment(environment)) {
|
|
5906
|
+
throw invalidInput(
|
|
5907
|
+
`Unknown environment "${environment}". Valid environments: development, staging, production.`
|
|
5908
|
+
);
|
|
5909
|
+
}
|
|
5910
|
+
return {
|
|
5911
|
+
projectId: project.projectId,
|
|
5912
|
+
projectName: project.projectName || project.projectId,
|
|
5913
|
+
environment
|
|
5914
|
+
};
|
|
5915
|
+
}
|
|
5916
|
+
async function resolveWriteRoute(projectId) {
|
|
5917
|
+
const api = createAPIClient();
|
|
5918
|
+
const roles = await api.resolveProjectRoles(projectId);
|
|
5919
|
+
const assigned = roles.assigned;
|
|
5920
|
+
return {
|
|
5921
|
+
role: roles.role,
|
|
5922
|
+
canWrite: assigned && (roles.capabilities["project.variables.create"] === true || roles.capabilities["project.variables.update"] === true),
|
|
5923
|
+
canRequest: assigned && roles.capabilities["project.requests.submit"] === true,
|
|
5924
|
+
environmentScope: roles.environmentScope
|
|
5925
|
+
};
|
|
5926
|
+
}
|
|
5927
|
+
var setCommand = new Command19("set").description(
|
|
5928
|
+
"Set (create or update) a single secret. Two-step by default: the key is validated first, then the value is prompted MASKED so it never lands in shell history. KEY=VALUE inline works for CI."
|
|
5929
|
+
).argument(
|
|
5930
|
+
"[key-or-assignment]",
|
|
5931
|
+
"KEY (value prompted masked) or KEY=VALUE (CI; lands in shell history)"
|
|
5932
|
+
).option("-e, --env <environment>", "Environment (defaults to active)").option("-p, --project <name-or-id>", "Linked project (defaults to active)").option("-d, --description <text>", "Optional description").option("--sensitive", "Mark the secret as sensitive").option(
|
|
5933
|
+
"--all-envs",
|
|
5934
|
+
"Confirm updating a variable whose value is shared across multiple environments"
|
|
5935
|
+
).action(
|
|
5936
|
+
async (keyOrAssignment, options) => {
|
|
5937
|
+
try {
|
|
5938
|
+
const { projectId, projectName, environment } = resolveTarget(options);
|
|
5939
|
+
const route = await withSpinner(
|
|
5940
|
+
"Checking role and plan...",
|
|
5941
|
+
() => resolveWriteRoute(projectId)
|
|
5942
|
+
);
|
|
5943
|
+
if (!route.canWrite && !route.canRequest) {
|
|
5944
|
+
error(
|
|
5945
|
+
`Your role (${route.role}) cannot write variables or file requests in this project.`
|
|
5946
|
+
);
|
|
5947
|
+
process.exit(3);
|
|
5494
5948
|
}
|
|
5495
|
-
|
|
5496
|
-
}
|
|
5497
|
-
let status;
|
|
5498
|
-
if (options.status) {
|
|
5499
|
-
const parsed = variableRequestStatusSchema.safeParse(options.status);
|
|
5500
|
-
if (!parsed.success) {
|
|
5949
|
+
if (route.environmentScope && !route.environmentScope.includes(environment)) {
|
|
5501
5950
|
error(
|
|
5502
|
-
`
|
|
5951
|
+
`Your assignment is scoped to [${route.environmentScope.join(", ")}] \u2014 ${environment} is outside it.`
|
|
5952
|
+
);
|
|
5953
|
+
process.exit(3);
|
|
5954
|
+
}
|
|
5955
|
+
let key;
|
|
5956
|
+
let inlineValue;
|
|
5957
|
+
if (keyOrAssignment && keyOrAssignment.includes("=")) {
|
|
5958
|
+
const parsed = parseAssignment(keyOrAssignment);
|
|
5959
|
+
if (!parsed.ok) throw invalidInput(parsed.error);
|
|
5960
|
+
key = parsed.key;
|
|
5961
|
+
inlineValue = parsed.value;
|
|
5962
|
+
warning(
|
|
5963
|
+
"Value passed on the command line \u2014 it is now in your shell history. Prefer `envpilot secrets set KEY` to be prompted masked."
|
|
5964
|
+
);
|
|
5965
|
+
} else if (keyOrAssignment) {
|
|
5966
|
+
if (!KEY_PATTERN.test(keyOrAssignment) || keyOrAssignment.length > 100) {
|
|
5967
|
+
throw invalidInput(
|
|
5968
|
+
"Variable key must be UPPER_SNAKE_CASE (A-Z, 0-9, _), start with a letter, max 100 chars."
|
|
5969
|
+
);
|
|
5970
|
+
}
|
|
5971
|
+
key = keyOrAssignment;
|
|
5972
|
+
} else {
|
|
5973
|
+
if (!process.stdin.isTTY) {
|
|
5974
|
+
throw invalidInput(
|
|
5975
|
+
"No key provided. Non-interactive usage: envpilot secrets set KEY=VALUE"
|
|
5976
|
+
);
|
|
5977
|
+
}
|
|
5978
|
+
const answer = await inquirer8.prompt([
|
|
5979
|
+
{
|
|
5980
|
+
type: "input",
|
|
5981
|
+
name: "key",
|
|
5982
|
+
message: "Key:",
|
|
5983
|
+
validate: (input) => KEY_PATTERN.test(input) && input.length <= 100 || "Must be UPPER_SNAKE_CASE (A-Z, 0-9, _), start with a letter, max 100 chars."
|
|
5984
|
+
}
|
|
5985
|
+
]);
|
|
5986
|
+
key = answer.key;
|
|
5987
|
+
}
|
|
5988
|
+
const existing = await withSpinner(
|
|
5989
|
+
`Checking ${key}...`,
|
|
5990
|
+
() => createAPIClient().findVariable(projectId, environment, key)
|
|
5991
|
+
);
|
|
5992
|
+
if (existing && existing.environments.length > 1 && !options.allEnvs) {
|
|
5993
|
+
const envList = existing.environments.join(", ");
|
|
5994
|
+
if (process.stdin.isTTY) {
|
|
5995
|
+
const { proceed } = await inquirer8.prompt([
|
|
5996
|
+
{
|
|
5997
|
+
type: "confirm",
|
|
5998
|
+
name: "proceed",
|
|
5999
|
+
message: `${key}'s value is shared across [${envList}] \u2014 updating it changes ALL of them. Continue?`,
|
|
6000
|
+
default: false
|
|
6001
|
+
}
|
|
6002
|
+
]);
|
|
6003
|
+
if (!proceed) {
|
|
6004
|
+
info(
|
|
6005
|
+
"Nothing changed. To give this environment its own value, edit the variable's environments in the dashboard first."
|
|
6006
|
+
);
|
|
6007
|
+
return;
|
|
6008
|
+
}
|
|
6009
|
+
} else {
|
|
6010
|
+
error(
|
|
6011
|
+
`${key} is shared across [${envList}] \u2014 updating it changes all of them. Re-run with --all-envs to confirm.`
|
|
6012
|
+
);
|
|
6013
|
+
process.exit(1);
|
|
6014
|
+
}
|
|
6015
|
+
}
|
|
6016
|
+
let value;
|
|
6017
|
+
let isSensitive = options.sensitive ?? false;
|
|
6018
|
+
if (inlineValue !== void 0) {
|
|
6019
|
+
value = inlineValue;
|
|
6020
|
+
} else {
|
|
6021
|
+
if (!process.stdin.isTTY) {
|
|
6022
|
+
throw invalidInput(
|
|
6023
|
+
"No value provided. Non-interactive usage: envpilot secrets set KEY=VALUE"
|
|
6024
|
+
);
|
|
6025
|
+
}
|
|
6026
|
+
const answers = await inquirer8.prompt([
|
|
6027
|
+
{
|
|
6028
|
+
type: "password",
|
|
6029
|
+
name: "value",
|
|
6030
|
+
message: "Value:",
|
|
6031
|
+
mask: "*",
|
|
6032
|
+
validate: (input) => input.length > 0 || "Value cannot be empty."
|
|
6033
|
+
},
|
|
6034
|
+
...options.sensitive === void 0 ? [
|
|
6035
|
+
{
|
|
6036
|
+
type: "confirm",
|
|
6037
|
+
name: "isSensitive",
|
|
6038
|
+
message: "Mark as sensitive?",
|
|
6039
|
+
default: false
|
|
6040
|
+
}
|
|
6041
|
+
] : []
|
|
6042
|
+
]);
|
|
6043
|
+
value = answers.value;
|
|
6044
|
+
if (options.sensitive === void 0) {
|
|
6045
|
+
isSensitive = answers.isSensitive ?? false;
|
|
6046
|
+
}
|
|
6047
|
+
}
|
|
6048
|
+
const api = createAPIClient();
|
|
6049
|
+
if (route.canWrite) {
|
|
6050
|
+
const result = await withSpinner(
|
|
6051
|
+
`Setting ${key} in ${environment}...`,
|
|
6052
|
+
() => api.setVariable(projectId, environment, key, value, {
|
|
6053
|
+
description: options.description,
|
|
6054
|
+
isSensitive
|
|
6055
|
+
})
|
|
6056
|
+
);
|
|
6057
|
+
if (result.unchanged) {
|
|
6058
|
+
success(
|
|
6059
|
+
`${chalk18.bold(key)} already has this value in ${projectName}/${environment} \u2014 nothing to change.`
|
|
6060
|
+
);
|
|
6061
|
+
} else {
|
|
6062
|
+
const verb = result.created > 0 ? "Created" : "Updated";
|
|
6063
|
+
success(
|
|
6064
|
+
`${verb} ${chalk18.bold(key)} in ${projectName}/${environment}.`
|
|
6065
|
+
);
|
|
6066
|
+
}
|
|
6067
|
+
return;
|
|
6068
|
+
}
|
|
6069
|
+
if (process.stdin.isTTY && inlineValue === void 0) {
|
|
6070
|
+
const { proceed } = await inquirer8.prompt([
|
|
6071
|
+
{
|
|
6072
|
+
type: "confirm",
|
|
6073
|
+
name: "proceed",
|
|
6074
|
+
message: "Your role can't write variables directly \u2014 file this as a request for review?",
|
|
6075
|
+
default: true
|
|
6076
|
+
}
|
|
6077
|
+
]);
|
|
6078
|
+
if (!proceed) {
|
|
6079
|
+
info("Nothing submitted.");
|
|
6080
|
+
return;
|
|
6081
|
+
}
|
|
6082
|
+
} else {
|
|
6083
|
+
warning(
|
|
6084
|
+
"Your role can't write variables directly \u2014 filing this as a request for review."
|
|
5503
6085
|
);
|
|
5504
|
-
process.exit(1);
|
|
5505
6086
|
}
|
|
5506
|
-
|
|
6087
|
+
const request = await withSpinner(
|
|
6088
|
+
"Submitting request...",
|
|
6089
|
+
() => api.createVariableRequest({
|
|
6090
|
+
projectId,
|
|
6091
|
+
key,
|
|
6092
|
+
value,
|
|
6093
|
+
environments: [environment],
|
|
6094
|
+
isSensitive,
|
|
6095
|
+
description: options.description
|
|
6096
|
+
})
|
|
6097
|
+
);
|
|
6098
|
+
success(`Request submitted for ${chalk18.bold(key)} (${environment}).`);
|
|
6099
|
+
info(
|
|
6100
|
+
`A reviewer can approve it with: envpilot requests approve ${request._id}`
|
|
6101
|
+
);
|
|
6102
|
+
} catch (err) {
|
|
6103
|
+
await handleError(err);
|
|
5507
6104
|
}
|
|
6105
|
+
}
|
|
6106
|
+
);
|
|
6107
|
+
var rmCommand = new Command19("rm").alias("delete").description("Delete a single secret from one environment (moves to trash)").argument("<key>", "Variable key to delete").option("-e, --env <environment>", "Environment (defaults to active)").option("-p, --project <name-or-id>", "Linked project (defaults to active)").option("-y, --yes", "Skip the confirmation prompt").action(async (key, options) => {
|
|
6108
|
+
try {
|
|
6109
|
+
const { projectId, projectName, environment } = resolveTarget(options);
|
|
5508
6110
|
const api = createAPIClient();
|
|
5509
|
-
const
|
|
5510
|
-
"
|
|
5511
|
-
() => api.
|
|
6111
|
+
const roles = await withSpinner(
|
|
6112
|
+
"Checking role...",
|
|
6113
|
+
() => api.resolveProjectRoles(projectId)
|
|
5512
6114
|
);
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
6115
|
+
if (!roles.assigned || roles.capabilities["project.variables.delete"] !== true) {
|
|
6116
|
+
error(
|
|
6117
|
+
`Your role (${roles.role}) cannot delete variables in this project.`
|
|
6118
|
+
);
|
|
6119
|
+
process.exit(3);
|
|
6120
|
+
}
|
|
6121
|
+
const found = await withSpinner(
|
|
6122
|
+
`Finding ${key}...`,
|
|
6123
|
+
() => api.findVariable(projectId, environment, key)
|
|
5518
6124
|
);
|
|
5519
|
-
if (
|
|
5520
|
-
|
|
6125
|
+
if (!found) {
|
|
6126
|
+
error(
|
|
6127
|
+
`No variable ${key} found in ${projectName}/${environment} (or you lack access).`
|
|
6128
|
+
);
|
|
6129
|
+
process.exit(1);
|
|
6130
|
+
}
|
|
6131
|
+
const shared = found.environments.length > 1;
|
|
6132
|
+
const consequence = shared ? `remove ${key} from ${environment} (still live in: ${found.environments.filter((e) => e !== environment).join(", ")})` : `move ${key} (${environment}) to trash`;
|
|
6133
|
+
if (!options.yes) {
|
|
6134
|
+
if (!process.stdin.isTTY) {
|
|
6135
|
+
warning(`This will ${consequence}. Re-run with --yes to confirm.`);
|
|
6136
|
+
return;
|
|
6137
|
+
}
|
|
6138
|
+
const { proceed } = await inquirer8.prompt([
|
|
6139
|
+
{
|
|
6140
|
+
type: "confirm",
|
|
6141
|
+
name: "proceed",
|
|
6142
|
+
message: `${consequence[0].toUpperCase()}${consequence.slice(1)} \u2014 continue?`,
|
|
6143
|
+
default: false
|
|
6144
|
+
}
|
|
6145
|
+
]);
|
|
6146
|
+
if (!proceed) {
|
|
6147
|
+
info("Nothing deleted.");
|
|
6148
|
+
return;
|
|
6149
|
+
}
|
|
6150
|
+
}
|
|
6151
|
+
if (shared) {
|
|
6152
|
+
await withSpinner(
|
|
6153
|
+
`Removing ${key} from ${environment}...`,
|
|
6154
|
+
() => api.removeVariableFromEnvironment(found._id, environment)
|
|
6155
|
+
);
|
|
6156
|
+
success(
|
|
6157
|
+
`Removed ${chalk18.bold(key)} from ${environment} \u2014 still live in: ${found.environments.filter((e) => e !== environment).join(", ")}.`
|
|
6158
|
+
);
|
|
6159
|
+
} else {
|
|
6160
|
+
await withSpinner(
|
|
6161
|
+
`Deleting ${key}...`,
|
|
6162
|
+
() => api.removeVariable(found._id)
|
|
6163
|
+
);
|
|
6164
|
+
success(
|
|
6165
|
+
`Deleted ${chalk18.bold(key)} from ${projectName}/${environment}.`
|
|
6166
|
+
);
|
|
6167
|
+
info("Recover it from the dashboard trash if this was a mistake.");
|
|
6168
|
+
}
|
|
6169
|
+
} catch (err) {
|
|
6170
|
+
await handleError(err);
|
|
6171
|
+
}
|
|
6172
|
+
});
|
|
6173
|
+
var secretsCommand = new Command19("secrets").alias("var").description("Manage a single secret (set, delete)").addCommand(setCommand).addCommand(rmCommand);
|
|
6174
|
+
|
|
6175
|
+
// src/commands/diff.ts
|
|
6176
|
+
import { Command as Command20 } from "commander";
|
|
6177
|
+
import chalk19 from "chalk";
|
|
6178
|
+
var diffCommand = new Command20("diff").description("Compare variables between two environments").argument("<envA>", "First environment").argument("<envB>", "Second environment").option("-p, --project <name-or-id>", "Linked project (defaults to active)").option("--values", "Also compare values (decrypts both environments)").option("--json", "Output as JSON").action(async (envA, envB, options) => {
|
|
6179
|
+
try {
|
|
6180
|
+
if (!isAuthenticated()) throw notAuthenticated();
|
|
6181
|
+
for (const env of [envA, envB]) {
|
|
6182
|
+
if (!validateEnvironment(env)) {
|
|
6183
|
+
throw invalidInput(
|
|
6184
|
+
`Unknown environment "${env}". Valid: development, staging, production.`
|
|
6185
|
+
);
|
|
6186
|
+
}
|
|
6187
|
+
}
|
|
6188
|
+
if (envA === envB) {
|
|
6189
|
+
throw invalidInput("Pick two different environments to compare.");
|
|
6190
|
+
}
|
|
6191
|
+
const config2 = readProjectConfigV2();
|
|
6192
|
+
if (!config2) throw notInitialized();
|
|
6193
|
+
const project = resolveProject(config2, options.project);
|
|
6194
|
+
if (!project) {
|
|
6195
|
+
error(`Project not found: ${options.project ?? "(active)"}`);
|
|
6196
|
+
process.exit(1);
|
|
6197
|
+
}
|
|
6198
|
+
const api = createAPIClient();
|
|
6199
|
+
const [metaA, metaB] = await withSpinner(
|
|
6200
|
+
`Comparing ${envA} vs ${envB}...`,
|
|
6201
|
+
() => Promise.all([
|
|
6202
|
+
api.listVariableKeys(project.projectId, envA),
|
|
6203
|
+
api.listVariableKeys(project.projectId, envB)
|
|
6204
|
+
])
|
|
6205
|
+
);
|
|
6206
|
+
const keysA = new Set(metaA.keys);
|
|
6207
|
+
const keysB = new Set(metaB.keys);
|
|
6208
|
+
const truncated = metaA.truncated || metaB.truncated;
|
|
6209
|
+
let valsA = /* @__PURE__ */ new Map();
|
|
6210
|
+
let valsB = /* @__PURE__ */ new Map();
|
|
6211
|
+
let undecryptable = [];
|
|
6212
|
+
if (options.values) {
|
|
6213
|
+
const [a, b] = await withSpinner(
|
|
6214
|
+
`Decrypting values...`,
|
|
6215
|
+
() => Promise.all([
|
|
6216
|
+
api.listVariables(project.projectId, envA, project.organizationId),
|
|
6217
|
+
api.listVariables(project.projectId, envB, project.organizationId)
|
|
6218
|
+
])
|
|
6219
|
+
);
|
|
6220
|
+
valsA = new Map(a.variables.map((v) => [v.key, v.value]));
|
|
6221
|
+
valsB = new Map(b.variables.map((v) => [v.key, v.value]));
|
|
6222
|
+
undecryptable = [
|
|
6223
|
+
.../* @__PURE__ */ new Set([...a.decryptionFailures, ...b.decryptionFailures])
|
|
6224
|
+
].sort();
|
|
6225
|
+
}
|
|
6226
|
+
const allKeys = [.../* @__PURE__ */ new Set([...keysA, ...keysB])].sort();
|
|
6227
|
+
const unknown = new Set(undecryptable);
|
|
6228
|
+
const onlyA = [];
|
|
6229
|
+
const onlyB = [];
|
|
6230
|
+
const differing = [];
|
|
6231
|
+
const same = [];
|
|
6232
|
+
for (const key of allKeys) {
|
|
6233
|
+
const inA = keysA.has(key);
|
|
6234
|
+
const inB = keysB.has(key);
|
|
6235
|
+
if (inA && !inB) onlyA.push(key);
|
|
6236
|
+
else if (!inA && inB) onlyB.push(key);
|
|
6237
|
+
else if (unknown.has(key))
|
|
6238
|
+
continue;
|
|
6239
|
+
else if (options.values && valsA.has(key) && valsB.has(key) && valsA.get(key) !== valsB.get(key))
|
|
6240
|
+
differing.push(key);
|
|
6241
|
+
else same.push(key);
|
|
6242
|
+
}
|
|
6243
|
+
if (options.json) {
|
|
6244
|
+
console.log(
|
|
6245
|
+
JSON.stringify(
|
|
6246
|
+
{
|
|
6247
|
+
envA,
|
|
6248
|
+
envB,
|
|
6249
|
+
onlyA,
|
|
6250
|
+
onlyB,
|
|
6251
|
+
differing,
|
|
6252
|
+
same,
|
|
6253
|
+
undecryptable,
|
|
6254
|
+
truncated
|
|
6255
|
+
},
|
|
6256
|
+
null,
|
|
6257
|
+
2
|
|
6258
|
+
)
|
|
6259
|
+
);
|
|
5521
6260
|
return;
|
|
5522
6261
|
}
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
6262
|
+
if (truncated) {
|
|
6263
|
+
warning(
|
|
6264
|
+
"Project exceeds the 5000-variable read window \u2014 this diff may be incomplete."
|
|
6265
|
+
);
|
|
6266
|
+
}
|
|
6267
|
+
header(`Diff: ${chalk19.bold(envA)} vs ${chalk19.bold(envB)}`);
|
|
6268
|
+
console.log();
|
|
6269
|
+
printGroup(`Only in ${envA}`, onlyA, chalk19.yellow);
|
|
6270
|
+
printGroup(`Only in ${envB}`, onlyB, chalk19.cyan);
|
|
6271
|
+
if (options.values) {
|
|
6272
|
+
printGroup("Different values", differing, chalk19.red);
|
|
6273
|
+
printGroup(
|
|
6274
|
+
"Could not decrypt (comparison unknown)",
|
|
6275
|
+
undecryptable,
|
|
6276
|
+
chalk19.magenta
|
|
6277
|
+
);
|
|
6278
|
+
}
|
|
6279
|
+
if (onlyA.length === 0 && onlyB.length === 0 && differing.length === 0 && undecryptable.length === 0) {
|
|
6280
|
+
info(
|
|
6281
|
+
options.values ? "Environments are identical (keys and values)." : "Same keys in both environments (run with --values to compare values)."
|
|
6282
|
+
);
|
|
6283
|
+
}
|
|
5530
6284
|
} catch (err) {
|
|
5531
6285
|
await handleError(err);
|
|
5532
6286
|
}
|
|
5533
6287
|
});
|
|
6288
|
+
function printGroup(title, keys, color) {
|
|
6289
|
+
if (keys.length === 0) return;
|
|
6290
|
+
console.log(color(`${title} (${keys.length}):`));
|
|
6291
|
+
for (const key of keys) console.log(` ${key}`);
|
|
6292
|
+
console.log();
|
|
6293
|
+
}
|
|
5534
6294
|
|
|
5535
6295
|
// src/lib/command-catalog.ts
|
|
5536
6296
|
function formatArgv(argv) {
|
|
@@ -5695,21 +6455,76 @@ var COMMAND_CATALOG = [
|
|
|
5695
6455
|
},
|
|
5696
6456
|
{
|
|
5697
6457
|
id: "requests",
|
|
5698
|
-
title: "List variable requests",
|
|
6458
|
+
title: "List and review variable requests",
|
|
5699
6459
|
category: "Browse",
|
|
5700
|
-
description: "List
|
|
6460
|
+
description: "List variable requests, or approve/reject/cancel them without leaving the terminal.",
|
|
5701
6461
|
argv: ["requests"],
|
|
5702
|
-
args: "[--project <
|
|
5703
|
-
examples: [
|
|
5704
|
-
|
|
6462
|
+
args: "[list] [--project <p>] [--status <s>] [--json] | approve <id> [--value <v>|--value-stdin] [--reason <t>] | reject <id> [--reason <t>] | cancel <id>",
|
|
6463
|
+
examples: [
|
|
6464
|
+
["requests"],
|
|
6465
|
+
["requests", "--status", "pending"],
|
|
6466
|
+
["requests", "approve", "<id>"],
|
|
6467
|
+
["requests", "approve", "<id>", "--value", "sk_live_\u2026"],
|
|
6468
|
+
["requests", "reject", "<id>", "--reason", "use the shared key"],
|
|
6469
|
+
["requests", "cancel", "<id>"]
|
|
6470
|
+
],
|
|
6471
|
+
websiteSurface: "Convex features/variables/requests (review/cancel).",
|
|
5705
6472
|
notes: [
|
|
5706
|
-
"Reviewers (owner, assigned project manager/team lead) see every request
|
|
5707
|
-
"
|
|
6473
|
+
"Reviewers (owner, assigned project manager/team lead) see every request; developers see only their own.",
|
|
6474
|
+
"--status/--json apply to listing only; --value/--value-stdin/--reason apply to review subcommands only.",
|
|
6475
|
+
"Approving a machine (valueless) request prompts MASKED for the value; --value-stdin reads it from stdin for CI (keeps it out of argv), --value is a last resort that lands in shell history.",
|
|
6476
|
+
"Get the <id> from the ID column of `envpilot requests`."
|
|
5708
6477
|
],
|
|
5709
|
-
keywords: ["requests", "approval", "review", "
|
|
6478
|
+
keywords: ["requests", "approval", "review", "approve", "reject", "cancel"],
|
|
5710
6479
|
topLevel: true,
|
|
5711
6480
|
createCommand: () => requestsCommand
|
|
5712
6481
|
},
|
|
6482
|
+
{
|
|
6483
|
+
id: "secrets",
|
|
6484
|
+
title: "Set or delete a single secret",
|
|
6485
|
+
category: "Sync",
|
|
6486
|
+
description: "Change one secret without pull/edit/push. Two-step by default: key first, value prompted MASKED (never in shell history).",
|
|
6487
|
+
argv: ["secrets"],
|
|
6488
|
+
aliases: [["var"]],
|
|
6489
|
+
args: "set [<key>|<key=value>] [-e <env>] [-p <project>] [-d <text>] [--sensitive] [--all-envs] | rm <key> [-e <env>] [-p <project>] [--yes]",
|
|
6490
|
+
examples: [
|
|
6491
|
+
["secrets", "set"],
|
|
6492
|
+
["secrets", "set", "STRIPE_SECRET_KEY", "--env", "production"],
|
|
6493
|
+
["secrets", "set", "API_URL=https://api.example.com"],
|
|
6494
|
+
["secrets", "rm", "OLD_FLAG", "--env", "staging", "--yes"]
|
|
6495
|
+
],
|
|
6496
|
+
websiteSurface: "Convex features/variables (bulk upsert / remove).",
|
|
6497
|
+
notes: [
|
|
6498
|
+
"Interactive by default: the key is validated first, then the value is prompted masked so it never lands in shell history \u2014 KEY=VALUE inline is for CI and prints a history warning.",
|
|
6499
|
+
"Role-aware: direct-write roles set immediately; request-only roles are offered the request workflow instead (a reviewer approves with `requests approve`).",
|
|
6500
|
+
"Plan limits are enforced server-side and reported readably; check `envpilot usage` for your tier.",
|
|
6501
|
+
"set upserts one key in one environment (merge); updating a value shared across environments requires confirmation (--all-envs non-interactively).",
|
|
6502
|
+
"rm on a single-environment secret moves it to trash (recoverable from the dashboard); on a shared secret it only removes THIS environment \u2014 the value stays live in the others.",
|
|
6503
|
+
"`envpilot var \u2026` still works as an alias."
|
|
6504
|
+
],
|
|
6505
|
+
keywords: ["secrets", "var", "set", "delete", "remove", "variable", "edit"],
|
|
6506
|
+
topLevel: true,
|
|
6507
|
+
createCommand: () => secretsCommand
|
|
6508
|
+
},
|
|
6509
|
+
{
|
|
6510
|
+
id: "diff",
|
|
6511
|
+
title: "Compare two environments",
|
|
6512
|
+
category: "Browse",
|
|
6513
|
+
description: "Show which variable keys differ between two environments (add --values to compare values).",
|
|
6514
|
+
argv: ["diff"],
|
|
6515
|
+
args: "<envA> <envB> [--values] [--project <name-or-id>] [--json]",
|
|
6516
|
+
examples: [
|
|
6517
|
+
["diff", "staging", "production"],
|
|
6518
|
+
["diff", "development", "staging", "--values"]
|
|
6519
|
+
],
|
|
6520
|
+
websiteSurface: "Convex features/variables (client-side compare).",
|
|
6521
|
+
notes: [
|
|
6522
|
+
"Default is a metadata-only key comparison; --values decrypts both environments."
|
|
6523
|
+
],
|
|
6524
|
+
keywords: ["diff", "compare", "environments", "drift"],
|
|
6525
|
+
topLevel: true,
|
|
6526
|
+
createCommand: () => diffCommand
|
|
6527
|
+
},
|
|
5713
6528
|
{
|
|
5714
6529
|
id: "run",
|
|
5715
6530
|
title: "Run command with secrets",
|