@envpilot/cli 1.18.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-JYN7XJXA.js → app-VAJPLVBA.js} +1 -1
- package/dist/{chunk-DYZH5VNC.js → chunk-WLHMHDPX.js} +894 -89
- package/dist/index.js +2 -2
- package/package.json +2 -2
package/LICENSE
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
MIT License
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
reproduced, distributed, or transmitted in any form or by any means without the
|
|
5
|
-
prior written permission of Envpilot.
|
|
3
|
+
Copyright (c) 2026 Syntax Lab Technology and Rafay
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -59,26 +59,49 @@ The TUI returns after each command finishes, so you can run multiple commands in
|
|
|
59
59
|
|
|
60
60
|
### Syncing Variables
|
|
61
61
|
|
|
62
|
-
| Command
|
|
63
|
-
|
|
|
64
|
-
| `envpilot pull [options]`
|
|
65
|
-
| `envpilot push [options]`
|
|
62
|
+
| Command | Description |
|
|
63
|
+
| ----------------------------- | ---------------------------------------------------------------- |
|
|
64
|
+
| `envpilot pull [options]` | Pull variables into a local `.env` file |
|
|
65
|
+
| `envpilot push [options]` | Push local `.env` changes to Envpilot |
|
|
66
|
+
| `envpilot secrets set [key]` | Set ONE secret — key first, value prompted masked (alias: `var`) |
|
|
67
|
+
| `envpilot secrets rm <key>` | Delete one secret (env-scoped for shared variables) |
|
|
68
|
+
| `envpilot diff <envA> <envB>` | Compare variable keys between two environments |
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
envpilot secrets set # guided: key → masked value → sensitive?
|
|
72
|
+
envpilot secrets set STRIPE_KEY -e production
|
|
73
|
+
envpilot secrets set API_URL=https://api.example.com # CI only — lands in shell history
|
|
74
|
+
envpilot secrets rm OLD_FLAG --yes
|
|
75
|
+
envpilot diff staging production # keys only (no decryption)
|
|
76
|
+
envpilot diff staging production --values # also compare values
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`secrets set` is role-aware: if your role can't write directly, the same
|
|
80
|
+
flow files a variable request for review instead of rejecting you. Updating
|
|
81
|
+
or deleting a value shared across environments asks for explicit
|
|
82
|
+
confirmation and only detaches the selected environment on `rm`.
|
|
66
83
|
|
|
67
84
|
### Variable Requests
|
|
68
85
|
|
|
69
86
|
Developers don't have direct write access — instead they submit a request,
|
|
70
|
-
and an owner, project manager, or team lead reviews it
|
|
71
|
-
|
|
87
|
+
and an owner, project manager, or team lead reviews it from the terminal or
|
|
88
|
+
the dashboard.
|
|
72
89
|
|
|
73
|
-
| Command
|
|
74
|
-
|
|
|
75
|
-
| `envpilot request [options]`
|
|
76
|
-
| `envpilot requests [options]`
|
|
90
|
+
| Command | Description |
|
|
91
|
+
| -------------------------------- | ---------------------------------------------------------------------- |
|
|
92
|
+
| `envpilot request [options]` | Request a new variable (interactive: key, value, environments) |
|
|
93
|
+
| `envpilot requests [options]` | List variable requests for the linked project with their review status |
|
|
94
|
+
| `envpilot requests approve <id>` | Approve a pending request (machine requests prompt masked for a value) |
|
|
95
|
+
| `envpilot requests reject <id>` | Reject a pending request (`--reason` shown to the requester) |
|
|
96
|
+
| `envpilot requests cancel <id>` | Cancel a pending request (your own, or as a reviewer) |
|
|
77
97
|
|
|
78
98
|
```bash
|
|
79
99
|
envpilot request # guided prompt: key → value → description → environments
|
|
80
|
-
envpilot requests # all requests for the linked project
|
|
81
|
-
envpilot requests --status pending
|
|
100
|
+
envpilot requests # all requests for the linked project (ID column for review)
|
|
101
|
+
envpilot requests --status pending --json
|
|
102
|
+
envpilot requests approve k5738… # valueless machine request → masked value prompt
|
|
103
|
+
printf %s "$SECRET" | envpilot requests approve k5738… --value-stdin # CI-safe
|
|
104
|
+
envpilot requests reject k5738… --reason "use the shared key"
|
|
82
105
|
```
|
|
83
106
|
|
|
84
107
|
Environment choices are limited to the environments you have access to —
|
|
@@ -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" } },
|
|
@@ -149,7 +149,7 @@ function probeCache(projectId, environment, organizationId, ttlSeconds) {
|
|
|
149
149
|
const fresh = isFresh(entry, ttlSeconds);
|
|
150
150
|
return { hit: true, fresh, entry };
|
|
151
151
|
}
|
|
152
|
-
function writeCache(projectId, environment, organizationId, variables, serverFingerprint) {
|
|
152
|
+
function writeCache(projectId, environment, organizationId, variables, serverFingerprint, meta) {
|
|
153
153
|
try {
|
|
154
154
|
const cacheDir = getCacheDir();
|
|
155
155
|
mkdirSync(cacheDir, { recursive: true, mode: 448 });
|
|
@@ -164,7 +164,9 @@ function writeCache(projectId, environment, organizationId, variables, serverFin
|
|
|
164
164
|
environment,
|
|
165
165
|
organizationId,
|
|
166
166
|
apiUrl: getApiUrl(),
|
|
167
|
-
fingerprint: serverFingerprint ?? computeFingerprint(variables)
|
|
167
|
+
fingerprint: serverFingerprint ?? computeFingerprint(variables),
|
|
168
|
+
...meta?.decryptionFailures?.length ? { decryptionFailures: meta.decryptionFailures } : {},
|
|
169
|
+
...meta?.scopeRestricted ? { scopeRestricted: true } : {}
|
|
168
170
|
};
|
|
169
171
|
writeFileSync(path, JSON.stringify(entry, null, 2), {
|
|
170
172
|
encoding: "utf-8",
|
|
@@ -470,7 +472,7 @@ function isInteractiveTerminal() {
|
|
|
470
472
|
async function openTUI() {
|
|
471
473
|
const [{ render }, { CLIApp }, { PressAnyKey }] = await Promise.all([
|
|
472
474
|
import("ink"),
|
|
473
|
-
import("./app-
|
|
475
|
+
import("./app-VAJPLVBA.js"),
|
|
474
476
|
import("./press-any-key-64XFP4O2.js")
|
|
475
477
|
]);
|
|
476
478
|
while (true) {
|
|
@@ -1082,7 +1084,18 @@ var refs = {
|
|
|
1082
1084
|
),
|
|
1083
1085
|
pullValues: fnRef("features/variables/values:pullValues"),
|
|
1084
1086
|
pushBulk: fnRef("features/variables/values:pushBulk"),
|
|
1085
|
-
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")
|
|
1086
1099
|
};
|
|
1087
1100
|
async function convexQuery(ref, ...args) {
|
|
1088
1101
|
const client = await getConvexClient();
|
|
@@ -1225,20 +1238,31 @@ var APIClient = class {
|
|
|
1225
1238
|
* Byte-compatible with the fingerprint writeCache stores from a full fetch:
|
|
1226
1239
|
* both hash `${_id}:${version}:${updatedAt}` over the accessible variables in
|
|
1227
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.
|
|
1228
1245
|
*/
|
|
1229
1246
|
async checkFingerprint(projectId, environment, _organizationId) {
|
|
1230
1247
|
const rows = await convexQuery(refs.listVariablesWithAccess, { projectId });
|
|
1231
|
-
const
|
|
1232
|
-
|
|
1248
|
+
const accessible = rows.filter((row) => row.hasAccess);
|
|
1249
|
+
const matching = accessible.filter(
|
|
1250
|
+
(row) => !environment || row.environments.includes(environment)
|
|
1233
1251
|
);
|
|
1234
|
-
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(
|
|
1235
1254
|
(row) => ({
|
|
1236
1255
|
_id: row._id,
|
|
1237
1256
|
version: row.version,
|
|
1238
1257
|
updatedAt: row.updatedAt
|
|
1239
1258
|
})
|
|
1240
1259
|
);
|
|
1241
|
-
return
|
|
1260
|
+
return {
|
|
1261
|
+
fingerprint: computeFingerprint(asVariables),
|
|
1262
|
+
totalAccessible: accessible.length,
|
|
1263
|
+
matchingCount: matching.length,
|
|
1264
|
+
otherEnvKeys
|
|
1265
|
+
};
|
|
1242
1266
|
}
|
|
1243
1267
|
/** List variable requests for a project (Convex). */
|
|
1244
1268
|
async listVariableRequests(projectId, status) {
|
|
@@ -1365,6 +1389,125 @@ var APIClient = class {
|
|
|
1365
1389
|
}
|
|
1366
1390
|
return created;
|
|
1367
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
|
+
}
|
|
1368
1511
|
};
|
|
1369
1512
|
function createAPIClient() {
|
|
1370
1513
|
return new APIClient();
|
|
@@ -2953,6 +3096,9 @@ function validateEnvVars(vars) {
|
|
|
2953
3096
|
}
|
|
2954
3097
|
return { valid, invalid };
|
|
2955
3098
|
}
|
|
3099
|
+
function validateEnvironment(env) {
|
|
3100
|
+
return environmentSchema2.safeParse(env).success;
|
|
3101
|
+
}
|
|
2956
3102
|
|
|
2957
3103
|
// src/commands/push.ts
|
|
2958
3104
|
var pushCommand = new Command4("push").description("Upload local .env file to cloud").option(
|
|
@@ -3952,8 +4098,8 @@ async function handlePath() {
|
|
|
3952
4098
|
]);
|
|
3953
4099
|
}
|
|
3954
4100
|
async function handleReset() {
|
|
3955
|
-
const
|
|
3956
|
-
const { confirm } = await
|
|
4101
|
+
const inquirer9 = await import("inquirer");
|
|
4102
|
+
const { confirm } = await inquirer9.default.prompt([
|
|
3957
4103
|
{
|
|
3958
4104
|
type: "confirm",
|
|
3959
4105
|
name: "confirm",
|
|
@@ -4710,7 +4856,7 @@ import { Command as Command14 } from "commander";
|
|
|
4710
4856
|
import crossSpawn from "cross-spawn";
|
|
4711
4857
|
import { constants as osConstants } from "os";
|
|
4712
4858
|
import chalk15 from "chalk";
|
|
4713
|
-
var DEFAULT_TTL =
|
|
4859
|
+
var DEFAULT_TTL = 0;
|
|
4714
4860
|
var runCommand = new Command14("run").description(
|
|
4715
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."
|
|
4716
4862
|
).argument("[command...]", "Command and arguments to execute").option(
|
|
@@ -4733,7 +4879,7 @@ var runCommand = new Command14("run").description(
|
|
|
4733
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).'
|
|
4734
4880
|
).option("-q, --quiet", "Suppress informational messages").option("--no-cache", "Skip cache and always fetch fresh secrets from server").option(
|
|
4735
4881
|
"--cache-ttl <seconds>",
|
|
4736
|
-
"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)",
|
|
4737
4883
|
String(DEFAULT_TTL)
|
|
4738
4884
|
).passThroughOptions().allowExcessArguments().action(async (commandArgs, options) => {
|
|
4739
4885
|
try {
|
|
@@ -4760,7 +4906,12 @@ var runCommand = new Command14("run").description(
|
|
|
4760
4906
|
}
|
|
4761
4907
|
throw notInitialized();
|
|
4762
4908
|
}
|
|
4763
|
-
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
|
+
}
|
|
4764
4915
|
const organizationId = options.organization || project.organizationId;
|
|
4765
4916
|
const parsedTtl = Number.parseInt(
|
|
4766
4917
|
options.cacheTtl ?? String(DEFAULT_TTL),
|
|
@@ -4770,14 +4921,35 @@ var runCommand = new Command14("run").description(
|
|
|
4770
4921
|
let variables;
|
|
4771
4922
|
let cacheHit = false;
|
|
4772
4923
|
let cacheAge = "";
|
|
4924
|
+
let otherEnvKeys = [];
|
|
4925
|
+
let decryptionFailures = [];
|
|
4926
|
+
let scopeRestricted = false;
|
|
4773
4927
|
if (options.cache === false) {
|
|
4774
|
-
|
|
4928
|
+
const fetched = await doFetch(
|
|
4775
4929
|
project,
|
|
4776
4930
|
environment,
|
|
4777
4931
|
organizationId,
|
|
4778
4932
|
options.quiet
|
|
4779
4933
|
);
|
|
4780
|
-
|
|
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
|
+
);
|
|
4781
4953
|
} else {
|
|
4782
4954
|
const probe = probeCache(
|
|
4783
4955
|
project.projectId,
|
|
@@ -4789,15 +4961,18 @@ var runCommand = new Command14("run").description(
|
|
|
4789
4961
|
variables = probe.entry.variables;
|
|
4790
4962
|
cacheHit = true;
|
|
4791
4963
|
cacheAge = formatAge(probe.entry.fetchedAt);
|
|
4792
|
-
|
|
4964
|
+
decryptionFailures = probe.entry.decryptionFailures ?? [];
|
|
4965
|
+
scopeRestricted = probe.entry.scopeRestricted ?? false;
|
|
4966
|
+
} else {
|
|
4793
4967
|
try {
|
|
4794
4968
|
const api = createAPIClient();
|
|
4795
|
-
const
|
|
4969
|
+
const check = await api.checkFingerprint(
|
|
4796
4970
|
project.projectId,
|
|
4797
4971
|
environment,
|
|
4798
4972
|
organizationId
|
|
4799
4973
|
);
|
|
4800
|
-
|
|
4974
|
+
otherEnvKeys = check.otherEnvKeys;
|
|
4975
|
+
if (probe.hit && check.fingerprint === probe.entry.fingerprint) {
|
|
4801
4976
|
extendCacheFreshness(
|
|
4802
4977
|
project.projectId,
|
|
4803
4978
|
environment,
|
|
@@ -4806,27 +4981,35 @@ var runCommand = new Command14("run").description(
|
|
|
4806
4981
|
variables = probe.entry.variables;
|
|
4807
4982
|
cacheHit = true;
|
|
4808
4983
|
cacheAge = formatAge(probe.entry.fetchedAt);
|
|
4984
|
+
decryptionFailures = probe.entry.decryptionFailures ?? [];
|
|
4985
|
+
scopeRestricted = probe.entry.scopeRestricted ?? false;
|
|
4809
4986
|
} else {
|
|
4810
|
-
|
|
4987
|
+
const fetched = await doFetch(
|
|
4811
4988
|
project,
|
|
4812
4989
|
environment,
|
|
4813
4990
|
organizationId,
|
|
4814
4991
|
options.quiet,
|
|
4815
|
-
"Secrets updated, refreshing"
|
|
4992
|
+
probe.hit ? "Secrets updated, refreshing" : "Loading"
|
|
4816
4993
|
);
|
|
4994
|
+
variables = fetched.variables;
|
|
4995
|
+
decryptionFailures = fetched.decryptionFailures;
|
|
4996
|
+
scopeRestricted = fetched.scopeRestricted;
|
|
4817
4997
|
writeCache(
|
|
4818
4998
|
project.projectId,
|
|
4819
4999
|
environment,
|
|
4820
5000
|
organizationId,
|
|
4821
5001
|
variables,
|
|
4822
|
-
|
|
5002
|
+
decryptionFailures.length > 0 ? void 0 : check.fingerprint,
|
|
5003
|
+
{ decryptionFailures, scopeRestricted }
|
|
4823
5004
|
);
|
|
4824
5005
|
}
|
|
4825
5006
|
} catch (err) {
|
|
4826
|
-
if (isConnectivityError(err)) {
|
|
5007
|
+
if (probe.hit && isConnectivityError(err)) {
|
|
4827
5008
|
variables = probe.entry.variables;
|
|
4828
5009
|
cacheHit = true;
|
|
4829
5010
|
cacheAge = formatAge(probe.entry.fetchedAt);
|
|
5011
|
+
decryptionFailures = probe.entry.decryptionFailures ?? [];
|
|
5012
|
+
scopeRestricted = probe.entry.scopeRestricted ?? false;
|
|
4830
5013
|
if (!options.quiet) {
|
|
4831
5014
|
warning(
|
|
4832
5015
|
`Using offline cache (age ${cacheAge}) \u2014 could not reach the server to verify freshness.`
|
|
@@ -4837,14 +5020,6 @@ var runCommand = new Command14("run").description(
|
|
|
4837
5020
|
throw err;
|
|
4838
5021
|
}
|
|
4839
5022
|
}
|
|
4840
|
-
} else {
|
|
4841
|
-
variables = await doFetch(
|
|
4842
|
-
project,
|
|
4843
|
-
environment,
|
|
4844
|
-
organizationId,
|
|
4845
|
-
options.quiet
|
|
4846
|
-
);
|
|
4847
|
-
writeCache(project.projectId, environment, organizationId, variables);
|
|
4848
5023
|
}
|
|
4849
5024
|
}
|
|
4850
5025
|
if (variables.length === 0) {
|
|
@@ -4852,6 +5027,13 @@ var runCommand = new Command14("run").description(
|
|
|
4852
5027
|
`No variables found for ${environment}. Running command without injected secrets.`
|
|
4853
5028
|
);
|
|
4854
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
|
+
}
|
|
4855
5037
|
const secrets = {};
|
|
4856
5038
|
for (const v of variables) {
|
|
4857
5039
|
secrets[v.key] = v.value;
|
|
@@ -4873,9 +5055,24 @@ var runCommand = new Command14("run").description(
|
|
|
4873
5055
|
if (!options.quiet) {
|
|
4874
5056
|
const injectedCount = Object.keys(secrets).length;
|
|
4875
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}`) : "";
|
|
4876
5060
|
info(
|
|
4877
|
-
`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}`
|
|
4878
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
|
+
}
|
|
4879
5076
|
if (overridden.length > 0) {
|
|
4880
5077
|
warning(
|
|
4881
5078
|
`Overriding ${overridden.length} shell var${overridden.length === 1 ? "" : "s"}: ${overridden.slice(0, 3).join(", ")}${overridden.length > 3 ? `, +${overridden.length - 3} more` : ""}`
|
|
@@ -4892,18 +5089,28 @@ var runCommand = new Command14("run").description(
|
|
|
4892
5089
|
async function doFetch(project, environment, organizationId, quiet, labelPrefix = "Loading") {
|
|
4893
5090
|
const label = `${labelPrefix} ${chalk15.bold(environment)} secrets for ${chalk15.bold(project.projectName || project.projectId)}...`;
|
|
4894
5091
|
const api = createAPIClient();
|
|
4895
|
-
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(
|
|
4896
5093
|
label,
|
|
4897
5094
|
() => api.listVariables(project.projectId, environment, organizationId)
|
|
4898
5095
|
);
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
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 [];
|
|
4905
5113
|
}
|
|
4906
|
-
return variables;
|
|
4907
5114
|
}
|
|
4908
5115
|
function printInjectionPreview(secrets, project, environment) {
|
|
4909
5116
|
const keys = Object.keys(secrets).sort();
|
|
@@ -5217,6 +5424,7 @@ function formatRequestsListHeader(input) {
|
|
|
5217
5424
|
}
|
|
5218
5425
|
function formatRequestRow(request) {
|
|
5219
5426
|
return {
|
|
5427
|
+
id: request._id,
|
|
5220
5428
|
key: request.key,
|
|
5221
5429
|
environments: request.environments.join(", "),
|
|
5222
5430
|
status: request.status,
|
|
@@ -5481,66 +5689,608 @@ async function pickRequestTarget(api) {
|
|
|
5481
5689
|
|
|
5482
5690
|
// src/commands/requests.ts
|
|
5483
5691
|
import { Command as Command18 } from "commander";
|
|
5484
|
-
|
|
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(
|
|
5485
5754
|
"--project <name-or-id>",
|
|
5486
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"
|
|
5487
5766
|
).option(
|
|
5488
|
-
"--
|
|
5489
|
-
|
|
5490
|
-
).
|
|
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) => {
|
|
5491
5834
|
try {
|
|
5492
|
-
if (!isAuthenticated())
|
|
5493
|
-
|
|
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})`);
|
|
5494
5901
|
}
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
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);
|
|
5504
5948
|
}
|
|
5505
|
-
|
|
5506
|
-
}
|
|
5507
|
-
let status;
|
|
5508
|
-
if (options.status) {
|
|
5509
|
-
const parsed = variableRequestStatusSchema.safeParse(options.status);
|
|
5510
|
-
if (!parsed.success) {
|
|
5949
|
+
if (route.environmentScope && !route.environmentScope.includes(environment)) {
|
|
5511
5950
|
error(
|
|
5512
|
-
`
|
|
5951
|
+
`Your assignment is scoped to [${route.environmentScope.join(", ")}] \u2014 ${environment} is outside it.`
|
|
5513
5952
|
);
|
|
5514
|
-
process.exit(
|
|
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;
|
|
5515
5987
|
}
|
|
5516
|
-
|
|
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."
|
|
6085
|
+
);
|
|
6086
|
+
}
|
|
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);
|
|
5517
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);
|
|
5518
6110
|
const api = createAPIClient();
|
|
5519
|
-
const
|
|
5520
|
-
"
|
|
5521
|
-
() => api.
|
|
6111
|
+
const roles = await withSpinner(
|
|
6112
|
+
"Checking role...",
|
|
6113
|
+
() => api.resolveProjectRoles(projectId)
|
|
5522
6114
|
);
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
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)
|
|
6124
|
+
);
|
|
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
|
+
])
|
|
5528
6205
|
);
|
|
5529
|
-
|
|
5530
|
-
|
|
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
|
+
);
|
|
5531
6260
|
return;
|
|
5532
6261
|
}
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
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
|
+
}
|
|
5540
6284
|
} catch (err) {
|
|
5541
6285
|
await handleError(err);
|
|
5542
6286
|
}
|
|
5543
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
|
+
}
|
|
5544
6294
|
|
|
5545
6295
|
// src/lib/command-catalog.ts
|
|
5546
6296
|
function formatArgv(argv) {
|
|
@@ -5705,21 +6455,76 @@ var COMMAND_CATALOG = [
|
|
|
5705
6455
|
},
|
|
5706
6456
|
{
|
|
5707
6457
|
id: "requests",
|
|
5708
|
-
title: "List variable requests",
|
|
6458
|
+
title: "List and review variable requests",
|
|
5709
6459
|
category: "Browse",
|
|
5710
|
-
description: "List
|
|
6460
|
+
description: "List variable requests, or approve/reject/cancel them without leaving the terminal.",
|
|
5711
6461
|
argv: ["requests"],
|
|
5712
|
-
args: "[--project <
|
|
5713
|
-
examples: [
|
|
5714
|
-
|
|
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).",
|
|
5715
6472
|
notes: [
|
|
5716
|
-
"Reviewers (owner, assigned project manager/team lead) see every request
|
|
5717
|
-
"
|
|
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`."
|
|
5718
6477
|
],
|
|
5719
|
-
keywords: ["requests", "approval", "review", "
|
|
6478
|
+
keywords: ["requests", "approval", "review", "approve", "reject", "cancel"],
|
|
5720
6479
|
topLevel: true,
|
|
5721
6480
|
createCommand: () => requestsCommand
|
|
5722
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
|
+
},
|
|
5723
6528
|
{
|
|
5724
6529
|
id: "run",
|
|
5725
6530
|
title: "Run command with secrets",
|
package/dist/index.js
CHANGED
|
@@ -7,13 +7,13 @@ import {
|
|
|
7
7
|
initSentry,
|
|
8
8
|
isInteractiveTerminal,
|
|
9
9
|
openTUI
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-WLHMHDPX.js";
|
|
11
11
|
|
|
12
12
|
// src/lib/program.ts
|
|
13
13
|
import { Command } from "commander";
|
|
14
14
|
|
|
15
15
|
// src/lib/cli-version.ts
|
|
16
|
-
var CLI_VERSION = true ? "1.
|
|
16
|
+
var CLI_VERSION = true ? "1.19.0" : "0.0.0";
|
|
17
17
|
|
|
18
18
|
// src/lib/version-check.ts
|
|
19
19
|
import chalk from "chalk";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@envpilot/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.0",
|
|
4
4
|
"description": "Envpilot CLI — sync and manage environment variables from the terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"secrets"
|
|
61
61
|
],
|
|
62
62
|
"author": "Envpilot <hello@envpilot.dev> (https://www.envpilot.dev)",
|
|
63
|
-
"license": "
|
|
63
|
+
"license": "MIT",
|
|
64
64
|
"repository": {
|
|
65
65
|
"type": "git",
|
|
66
66
|
"url": "https://github.com/rafay99-epic/envpilot.dev"
|