@envpilot/cli 1.9.1 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -64,6 +64,26 @@ The TUI returns after each command finishes, so you can run multiple commands in
|
|
|
64
64
|
| `envpilot pull [options]` | Pull variables into a local `.env` file |
|
|
65
65
|
| `envpilot push [options]` | Push local `.env` changes to Envpilot |
|
|
66
66
|
|
|
67
|
+
### Variable Requests
|
|
68
|
+
|
|
69
|
+
Developers don't have direct write access — instead they submit a request,
|
|
70
|
+
and an owner, project manager, or team lead reviews it on the dashboard
|
|
71
|
+
(choosing the final environments on approval).
|
|
72
|
+
|
|
73
|
+
| Command | Description |
|
|
74
|
+
| ----------------------------- | ---------------------------------------------------------------------- |
|
|
75
|
+
| `envpilot request [options]` | Request a new variable (interactive: key, value, environments) |
|
|
76
|
+
| `envpilot requests [options]` | List variable requests for the linked project with their review status |
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
envpilot request # guided prompt: key → value → description → environments
|
|
80
|
+
envpilot requests # all requests for the linked project
|
|
81
|
+
envpilot requests --status pending # filter: pending | approved | rejected | canceled
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Environment choices are limited to the environments you have access to —
|
|
85
|
+
a developer scoped to `development` cannot request a production variable.
|
|
86
|
+
|
|
67
87
|
### Browsing Resources
|
|
68
88
|
|
|
69
89
|
| Command | Description |
|
|
@@ -107,6 +127,8 @@ Run `envpilot man` for the full command reference, or `envpilot man <command>` f
|
|
|
107
127
|
| `--dry-run` | `pull` | Preview changes without writing to disk |
|
|
108
128
|
| `--json` | `list projects`, `list organizations`, `usage` | Output as JSON |
|
|
109
129
|
| `--force` | `unlink` | Skip confirmation prompt |
|
|
130
|
+
| `--project <id>` | `request`, `requests` | Target a specific linked project |
|
|
131
|
+
| `--status <status>` | `requests` | Filter requests: pending, approved, rejected, canceled |
|
|
110
132
|
|
|
111
133
|
## Pull Formats
|
|
112
134
|
|
|
@@ -124,17 +146,20 @@ envpilot pull --format docker-compose # Docker Compose
|
|
|
124
146
|
|
|
125
147
|
## Role-Based Access
|
|
126
148
|
|
|
127
|
-
Envpilot
|
|
149
|
+
Envpilot uses one unified organization role per user:
|
|
128
150
|
|
|
129
|
-
**
|
|
151
|
+
**Owner > Project Manager > Team Lead > Developer**
|
|
130
152
|
|
|
131
|
-
|
|
153
|
+
What you can do in a project follows from this role plus whether you are
|
|
154
|
+
assigned to the project. Developers can additionally be scoped to specific
|
|
155
|
+
environments, and per-variable read/write grants control their access.
|
|
132
156
|
|
|
133
|
-
| Role
|
|
134
|
-
|
|
|
135
|
-
|
|
|
136
|
-
|
|
|
137
|
-
|
|
|
157
|
+
| Role | Pull | Push (direct) | Request new variable | Review requests | Manage permissions |
|
|
158
|
+
| --------------- | -------------------------------------- | ------------- | -------------------- | --------------- | ------------------ |
|
|
159
|
+
| Owner | Yes | Yes | -- (creates direct) | Yes | Yes |
|
|
160
|
+
| Project Manager | Yes (assigned projects) | Yes | -- (creates direct) | Yes | Yes |
|
|
161
|
+
| Team Lead | Yes (assigned projects) | Yes | -- (creates direct) | Yes | Yes |
|
|
162
|
+
| Developer | Granted variables, scoped environments | No | Yes | No | No |
|
|
138
163
|
|
|
139
164
|
## Security
|
|
140
165
|
|
|
@@ -7,7 +7,7 @@ function initSentry() {
|
|
|
7
7
|
Sentry.init({
|
|
8
8
|
dsn,
|
|
9
9
|
environment: "cli",
|
|
10
|
-
release: true ? "1.
|
|
10
|
+
release: true ? "1.10.0" : "0.0.0",
|
|
11
11
|
// Free tier: disable performance monitoring
|
|
12
12
|
tracesSampleRate: 0,
|
|
13
13
|
beforeSend(event) {
|
|
@@ -469,7 +469,7 @@ import { jsx } from "react/jsx-runtime";
|
|
|
469
469
|
async function openTUI() {
|
|
470
470
|
const [{ render }, { CLIApp }, { PressAnyKey }] = await Promise.all([
|
|
471
471
|
import("ink"),
|
|
472
|
-
import("./app-
|
|
472
|
+
import("./app-UT26PMDR.js"),
|
|
473
473
|
import("./press-any-key-64XFP4O2.js")
|
|
474
474
|
]);
|
|
475
475
|
while (true) {
|
|
@@ -1169,6 +1169,29 @@ var APIClient = class {
|
|
|
1169
1169
|
async bulkUpsertVariables(data) {
|
|
1170
1170
|
return this.post("/api/cli/variables/bulk", data);
|
|
1171
1171
|
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Submit a variable request (developers only — owners/PMs/team leads
|
|
1174
|
+
* create variables directly and get a 403 from the server here).
|
|
1175
|
+
*/
|
|
1176
|
+
async createVariableRequest(data) {
|
|
1177
|
+
const response = await this.post(
|
|
1178
|
+
"/api/cli/variable-requests",
|
|
1179
|
+
data
|
|
1180
|
+
);
|
|
1181
|
+
if (!response.data) {
|
|
1182
|
+
throw new APIError("No variable request returned by server", 500);
|
|
1183
|
+
}
|
|
1184
|
+
return response.data.request;
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* List variable requests for a project, optionally filtered by status.
|
|
1188
|
+
*/
|
|
1189
|
+
async listVariableRequests(projectId, status) {
|
|
1190
|
+
const params = { projectId };
|
|
1191
|
+
if (status) params.status = status;
|
|
1192
|
+
const response = await this.get("/api/cli/variable-requests", params);
|
|
1193
|
+
return response.data?.requests ?? [];
|
|
1194
|
+
}
|
|
1172
1195
|
// ============================================
|
|
1173
1196
|
// Authentication methods
|
|
1174
1197
|
// ============================================
|
|
@@ -1457,6 +1480,32 @@ var projectConfigV2Schema = z.object({
|
|
|
1457
1480
|
activeProjectId: z.string(),
|
|
1458
1481
|
projects: z.array(projectEntrySchema).min(1)
|
|
1459
1482
|
});
|
|
1483
|
+
var variableRequestStatusSchema = z.enum([
|
|
1484
|
+
"pending",
|
|
1485
|
+
"approved",
|
|
1486
|
+
"rejected",
|
|
1487
|
+
"canceled"
|
|
1488
|
+
]);
|
|
1489
|
+
var variableRequestUserSchema = z.object({
|
|
1490
|
+
_id: z.string(),
|
|
1491
|
+
email: z.string().optional(),
|
|
1492
|
+
name: z.string().optional()
|
|
1493
|
+
}).nullable().optional();
|
|
1494
|
+
var variableRequestSchema = z.object({
|
|
1495
|
+
_id: z.string(),
|
|
1496
|
+
key: z.string(),
|
|
1497
|
+
description: z.string().optional(),
|
|
1498
|
+
environments: z.array(z.string()),
|
|
1499
|
+
projectId: z.string(),
|
|
1500
|
+
organizationId: z.string().optional(),
|
|
1501
|
+
isSensitive: z.boolean().optional(),
|
|
1502
|
+
status: variableRequestStatusSchema,
|
|
1503
|
+
reviewReason: z.string().optional(),
|
|
1504
|
+
createdAt: z.number(),
|
|
1505
|
+
updatedAt: z.number().optional(),
|
|
1506
|
+
requester: variableRequestUserSchema,
|
|
1507
|
+
reviewer: variableRequestUserSchema
|
|
1508
|
+
});
|
|
1460
1509
|
|
|
1461
1510
|
// src/lib/project-config.ts
|
|
1462
1511
|
var CONFIG_FILE_NAME = ".envpilot";
|
|
@@ -3757,8 +3806,8 @@ async function handlePath() {
|
|
|
3757
3806
|
]);
|
|
3758
3807
|
}
|
|
3759
3808
|
async function handleReset() {
|
|
3760
|
-
const
|
|
3761
|
-
const { confirm } = await
|
|
3809
|
+
const inquirer7 = await import("inquirer");
|
|
3810
|
+
const { confirm } = await inquirer7.default.prompt([
|
|
3762
3811
|
{
|
|
3763
3812
|
type: "confirm",
|
|
3764
3813
|
name: "confirm",
|
|
@@ -4895,6 +4944,443 @@ function createUICommand() {
|
|
|
4895
4944
|
});
|
|
4896
4945
|
}
|
|
4897
4946
|
|
|
4947
|
+
// src/commands/request.ts
|
|
4948
|
+
import { Command as Command17 } from "commander";
|
|
4949
|
+
import chalk17 from "chalk";
|
|
4950
|
+
import inquirer6 from "inquirer";
|
|
4951
|
+
|
|
4952
|
+
// src/lib/variable-requests.ts
|
|
4953
|
+
import { z as z3 } from "zod";
|
|
4954
|
+
var ALL_REQUEST_ENVIRONMENTS = [
|
|
4955
|
+
"development",
|
|
4956
|
+
"staging",
|
|
4957
|
+
"production"
|
|
4958
|
+
];
|
|
4959
|
+
var requestKeySchema = z3.string().min(1, "Key is required").max(100, "Key must be 100 characters or less").regex(
|
|
4960
|
+
/^[A-Z][A-Z0-9_]*$/,
|
|
4961
|
+
"Key must be uppercase, start with a letter, and contain only letters, numbers, and underscores"
|
|
4962
|
+
);
|
|
4963
|
+
var requestValueSchema = z3.string().min(1, "Value is required");
|
|
4964
|
+
var requestDescriptionSchema = z3.string().max(500, "Description must be 500 characters or less");
|
|
4965
|
+
function validateRequestKey(key) {
|
|
4966
|
+
const result = requestKeySchema.safeParse(key);
|
|
4967
|
+
if (result.success) return { valid: true };
|
|
4968
|
+
return {
|
|
4969
|
+
valid: false,
|
|
4970
|
+
error: result.error.issues[0]?.message ?? "Invalid key"
|
|
4971
|
+
};
|
|
4972
|
+
}
|
|
4973
|
+
function validateRequestValue(value) {
|
|
4974
|
+
const result = requestValueSchema.safeParse(value);
|
|
4975
|
+
if (result.success) return { valid: true };
|
|
4976
|
+
return {
|
|
4977
|
+
valid: false,
|
|
4978
|
+
error: result.error.issues[0]?.message ?? "Invalid value"
|
|
4979
|
+
};
|
|
4980
|
+
}
|
|
4981
|
+
function validateRequestDescription(description) {
|
|
4982
|
+
if (description.length === 0) return { valid: true };
|
|
4983
|
+
const result = requestDescriptionSchema.safeParse(description);
|
|
4984
|
+
if (result.success) return { valid: true };
|
|
4985
|
+
return {
|
|
4986
|
+
valid: false,
|
|
4987
|
+
error: result.error.issues[0]?.message ?? "Invalid description"
|
|
4988
|
+
};
|
|
4989
|
+
}
|
|
4990
|
+
function allowedRequestEnvironments(environmentScope) {
|
|
4991
|
+
if (environmentScope == null) {
|
|
4992
|
+
return [...ALL_REQUEST_ENVIRONMENTS];
|
|
4993
|
+
}
|
|
4994
|
+
return ALL_REQUEST_ENVIRONMENTS.filter(
|
|
4995
|
+
(env) => environmentScope.includes(env)
|
|
4996
|
+
);
|
|
4997
|
+
}
|
|
4998
|
+
function buildCreateVariableRequestBody(input) {
|
|
4999
|
+
const description = input.description?.trim();
|
|
5000
|
+
return {
|
|
5001
|
+
projectId: input.projectId,
|
|
5002
|
+
key: input.key.trim(),
|
|
5003
|
+
value: input.value,
|
|
5004
|
+
...description ? { description } : {},
|
|
5005
|
+
environments: input.environments,
|
|
5006
|
+
isSensitive: input.isSensitive ?? false
|
|
5007
|
+
};
|
|
5008
|
+
}
|
|
5009
|
+
function isRequestEligibleProject(project) {
|
|
5010
|
+
const role = normalizeOrgRole(project.unifiedRole ?? project.role);
|
|
5011
|
+
return role === "developer" && project.assigned === true;
|
|
5012
|
+
}
|
|
5013
|
+
function buildEligibleRequestTargets(projects, orgNameById) {
|
|
5014
|
+
return projects.filter(isRequestEligibleProject).map((p) => ({
|
|
5015
|
+
projectId: p._id,
|
|
5016
|
+
projectName: p.name,
|
|
5017
|
+
organizationId: p.organizationId,
|
|
5018
|
+
organizationName: orgNameById[p.organizationId] ?? p.organizationId,
|
|
5019
|
+
environmentScope: p.environmentScope ?? null
|
|
5020
|
+
}));
|
|
5021
|
+
}
|
|
5022
|
+
function formatProjectChoiceLabel(target) {
|
|
5023
|
+
return `${target.projectName} \u2014 ${target.organizationName}`;
|
|
5024
|
+
}
|
|
5025
|
+
function buildProjectChoices(targets) {
|
|
5026
|
+
return targets.map((t) => ({
|
|
5027
|
+
name: formatProjectChoiceLabel(t),
|
|
5028
|
+
value: t.projectId
|
|
5029
|
+
}));
|
|
5030
|
+
}
|
|
5031
|
+
function formatRequestContextBanner(input) {
|
|
5032
|
+
return `Requesting as ${input.email}
|
|
5033
|
+
Project: ${input.projectName} \xB7 Organization: ${input.organizationName}`;
|
|
5034
|
+
}
|
|
5035
|
+
var SUMMARY_LABEL_WIDTH = 15;
|
|
5036
|
+
function formatRequestSummary(input) {
|
|
5037
|
+
const rows = [
|
|
5038
|
+
["Key:", input.key],
|
|
5039
|
+
[
|
|
5040
|
+
"Project:",
|
|
5041
|
+
formatProjectChoiceLabel({
|
|
5042
|
+
projectName: input.projectName,
|
|
5043
|
+
organizationName: input.organizationName
|
|
5044
|
+
})
|
|
5045
|
+
],
|
|
5046
|
+
["Environments:", input.environments.join(", ")],
|
|
5047
|
+
["Sensitive:", input.isSensitive ? "yes" : "no"]
|
|
5048
|
+
];
|
|
5049
|
+
return rows.map(([label, value]) => `${label.padEnd(SUMMARY_LABEL_WIDTH)}${value}`).join("\n");
|
|
5050
|
+
}
|
|
5051
|
+
function formatRequestSuccessMessage(input) {
|
|
5052
|
+
return `Request "${input.key}" submitted for ${input.projectName} (${input.organizationName}) \u2014 pending review.`;
|
|
5053
|
+
}
|
|
5054
|
+
function formatRequestsListHeader(input) {
|
|
5055
|
+
return `Requests for ${formatProjectChoiceLabel(input)}`;
|
|
5056
|
+
}
|
|
5057
|
+
function formatRequestRow(request) {
|
|
5058
|
+
return {
|
|
5059
|
+
key: request.key,
|
|
5060
|
+
environments: request.environments.join(", "),
|
|
5061
|
+
status: request.status,
|
|
5062
|
+
requested: new Date(request.createdAt).toLocaleDateString(),
|
|
5063
|
+
reason: request.reviewReason ?? ""
|
|
5064
|
+
};
|
|
5065
|
+
}
|
|
5066
|
+
function formatRequestRows(requests) {
|
|
5067
|
+
return requests.map(formatRequestRow);
|
|
5068
|
+
}
|
|
5069
|
+
|
|
5070
|
+
// src/commands/request.ts
|
|
5071
|
+
var requestCommand = new Command17("request").description(
|
|
5072
|
+
"Request creation of a new environment variable (developers only \u2014 owners, project managers, and team leads create variables directly)"
|
|
5073
|
+
).option(
|
|
5074
|
+
"--project <name-or-id>",
|
|
5075
|
+
"Submit the request for a specific linked project"
|
|
5076
|
+
).action(async (options) => {
|
|
5077
|
+
try {
|
|
5078
|
+
if (!isAuthenticated()) {
|
|
5079
|
+
throw notAuthenticated();
|
|
5080
|
+
}
|
|
5081
|
+
const email = getUser()?.email ?? "unknown account";
|
|
5082
|
+
const api = createAPIClient();
|
|
5083
|
+
const projectPassed = Boolean(options.project);
|
|
5084
|
+
const defaultEntry = resolveDefaultEntry(options.project);
|
|
5085
|
+
let target = null;
|
|
5086
|
+
let scope;
|
|
5087
|
+
if (defaultEntry) {
|
|
5088
|
+
const names = await resolveEntryNames(api, defaultEntry);
|
|
5089
|
+
const defaultTarget = {
|
|
5090
|
+
projectId: defaultEntry.projectId,
|
|
5091
|
+
projectName: names.projectName,
|
|
5092
|
+
organizationId: defaultEntry.organizationId,
|
|
5093
|
+
organizationName: names.organizationName
|
|
5094
|
+
};
|
|
5095
|
+
printBanner(email, defaultTarget);
|
|
5096
|
+
let useDefault = projectPassed;
|
|
5097
|
+
if (!projectPassed) {
|
|
5098
|
+
const { proceed } = await inquirer6.prompt([
|
|
5099
|
+
{
|
|
5100
|
+
type: "confirm",
|
|
5101
|
+
name: "proceed",
|
|
5102
|
+
message: "Request a variable for this project?",
|
|
5103
|
+
default: true
|
|
5104
|
+
}
|
|
5105
|
+
]);
|
|
5106
|
+
useDefault = proceed;
|
|
5107
|
+
}
|
|
5108
|
+
if (useDefault) {
|
|
5109
|
+
const meta = await fetchProjectMeta(api, defaultEntry);
|
|
5110
|
+
const role = normalizeOrgRole(meta?.unifiedRole ?? meta?.role);
|
|
5111
|
+
if (role !== "developer") {
|
|
5112
|
+
warning(
|
|
5113
|
+
"You have direct write access to this project. Use `envpilot push` or create the variable directly instead of submitting a request."
|
|
5114
|
+
);
|
|
5115
|
+
return;
|
|
5116
|
+
}
|
|
5117
|
+
target = defaultTarget;
|
|
5118
|
+
scope = meta?.environmentScope;
|
|
5119
|
+
}
|
|
5120
|
+
}
|
|
5121
|
+
if (!target) {
|
|
5122
|
+
const picked = await pickRequestTarget(api);
|
|
5123
|
+
if (!picked) {
|
|
5124
|
+
info(
|
|
5125
|
+
"You have direct write access in your projects \u2014 create variables from the dashboard."
|
|
5126
|
+
);
|
|
5127
|
+
process.exit(0);
|
|
5128
|
+
}
|
|
5129
|
+
target = picked;
|
|
5130
|
+
scope = picked.environmentScope;
|
|
5131
|
+
printBanner(email, target);
|
|
5132
|
+
}
|
|
5133
|
+
const envChoices = allowedRequestEnvironments(scope);
|
|
5134
|
+
if (envChoices.length === 0) {
|
|
5135
|
+
error(
|
|
5136
|
+
"Your assignment does not include any environments in this project."
|
|
5137
|
+
);
|
|
5138
|
+
process.exit(1);
|
|
5139
|
+
}
|
|
5140
|
+
const answers = await inquirer6.prompt([
|
|
5141
|
+
{
|
|
5142
|
+
type: "input",
|
|
5143
|
+
name: "key",
|
|
5144
|
+
message: "Variable key (e.g. API_SECRET):",
|
|
5145
|
+
validate: (input) => {
|
|
5146
|
+
const result = validateRequestKey(input);
|
|
5147
|
+
return result.valid || result.error;
|
|
5148
|
+
}
|
|
5149
|
+
},
|
|
5150
|
+
{
|
|
5151
|
+
type: "password",
|
|
5152
|
+
name: "value",
|
|
5153
|
+
message: "Variable value:",
|
|
5154
|
+
mask: "*",
|
|
5155
|
+
validate: (input) => {
|
|
5156
|
+
const result = validateRequestValue(input);
|
|
5157
|
+
return result.valid || result.error;
|
|
5158
|
+
}
|
|
5159
|
+
},
|
|
5160
|
+
{
|
|
5161
|
+
type: "input",
|
|
5162
|
+
name: "description",
|
|
5163
|
+
message: "Description (optional):",
|
|
5164
|
+
default: "",
|
|
5165
|
+
validate: (input) => {
|
|
5166
|
+
const result = validateRequestDescription(input);
|
|
5167
|
+
return result.valid || result.error;
|
|
5168
|
+
}
|
|
5169
|
+
},
|
|
5170
|
+
{
|
|
5171
|
+
type: "checkbox",
|
|
5172
|
+
name: "environments",
|
|
5173
|
+
message: "Environments:",
|
|
5174
|
+
choices: envChoices,
|
|
5175
|
+
validate: (input) => input.length > 0 || "Select at least one environment"
|
|
5176
|
+
},
|
|
5177
|
+
{
|
|
5178
|
+
type: "confirm",
|
|
5179
|
+
name: "isSensitive",
|
|
5180
|
+
message: "Is this value sensitive?",
|
|
5181
|
+
default: true
|
|
5182
|
+
}
|
|
5183
|
+
]);
|
|
5184
|
+
console.log();
|
|
5185
|
+
console.log(
|
|
5186
|
+
formatRequestSummary({
|
|
5187
|
+
key: answers.key.trim(),
|
|
5188
|
+
projectName: target.projectName,
|
|
5189
|
+
organizationName: target.organizationName,
|
|
5190
|
+
environments: answers.environments,
|
|
5191
|
+
isSensitive: answers.isSensitive
|
|
5192
|
+
})
|
|
5193
|
+
);
|
|
5194
|
+
console.log();
|
|
5195
|
+
const { submit } = await inquirer6.prompt([
|
|
5196
|
+
{
|
|
5197
|
+
type: "confirm",
|
|
5198
|
+
name: "submit",
|
|
5199
|
+
message: "Submit this request?",
|
|
5200
|
+
default: true
|
|
5201
|
+
}
|
|
5202
|
+
]);
|
|
5203
|
+
if (!submit) {
|
|
5204
|
+
info("Request canceled.");
|
|
5205
|
+
return;
|
|
5206
|
+
}
|
|
5207
|
+
const body = buildCreateVariableRequestBody({
|
|
5208
|
+
projectId: target.projectId,
|
|
5209
|
+
key: answers.key,
|
|
5210
|
+
value: answers.value,
|
|
5211
|
+
description: answers.description,
|
|
5212
|
+
environments: answers.environments,
|
|
5213
|
+
isSensitive: answers.isSensitive
|
|
5214
|
+
});
|
|
5215
|
+
const created = await api.createVariableRequest(body);
|
|
5216
|
+
success(
|
|
5217
|
+
formatRequestSuccessMessage({
|
|
5218
|
+
key: created.key,
|
|
5219
|
+
projectName: target.projectName,
|
|
5220
|
+
organizationName: target.organizationName
|
|
5221
|
+
})
|
|
5222
|
+
);
|
|
5223
|
+
info(`Status: ${created.status} \xB7 Id: ${created._id}`);
|
|
5224
|
+
} catch (err) {
|
|
5225
|
+
if (err instanceof APIError && err.statusCode === 403) {
|
|
5226
|
+
error(err.message);
|
|
5227
|
+
return;
|
|
5228
|
+
}
|
|
5229
|
+
await handleError(err);
|
|
5230
|
+
}
|
|
5231
|
+
});
|
|
5232
|
+
function resolveDefaultEntry(projectOption) {
|
|
5233
|
+
const configV2 = readProjectConfigV2();
|
|
5234
|
+
if (!configV2) {
|
|
5235
|
+
if (projectOption) throw notInitialized();
|
|
5236
|
+
return null;
|
|
5237
|
+
}
|
|
5238
|
+
const project = resolveProject(configV2, projectOption);
|
|
5239
|
+
if (!project) {
|
|
5240
|
+
if (projectOption) {
|
|
5241
|
+
error(`Project not found: ${projectOption}`);
|
|
5242
|
+
console.log();
|
|
5243
|
+
console.log("Linked projects:");
|
|
5244
|
+
for (const p of configV2.projects) {
|
|
5245
|
+
console.log(` ${p.projectName || p.projectId} (${p.environment})`);
|
|
5246
|
+
}
|
|
5247
|
+
process.exit(1);
|
|
5248
|
+
}
|
|
5249
|
+
return null;
|
|
5250
|
+
}
|
|
5251
|
+
return project;
|
|
5252
|
+
}
|
|
5253
|
+
function printBanner(email, target) {
|
|
5254
|
+
console.log();
|
|
5255
|
+
console.log(
|
|
5256
|
+
chalk17.dim(
|
|
5257
|
+
formatRequestContextBanner({
|
|
5258
|
+
email,
|
|
5259
|
+
projectName: target.projectName,
|
|
5260
|
+
organizationName: target.organizationName
|
|
5261
|
+
})
|
|
5262
|
+
)
|
|
5263
|
+
);
|
|
5264
|
+
console.log();
|
|
5265
|
+
}
|
|
5266
|
+
async function resolveEntryNames(api, entry) {
|
|
5267
|
+
let projectName = entry.projectName;
|
|
5268
|
+
let organizationName = entry.organizationName;
|
|
5269
|
+
if (!projectName) {
|
|
5270
|
+
try {
|
|
5271
|
+
const project = await api.getProject(entry.projectId);
|
|
5272
|
+
projectName = project.name;
|
|
5273
|
+
} catch {
|
|
5274
|
+
}
|
|
5275
|
+
}
|
|
5276
|
+
if (!organizationName) {
|
|
5277
|
+
try {
|
|
5278
|
+
const orgs = await api.listOrganizations();
|
|
5279
|
+
organizationName = orgs.find((o) => o._id === entry.organizationId)?.name ?? "";
|
|
5280
|
+
} catch {
|
|
5281
|
+
}
|
|
5282
|
+
}
|
|
5283
|
+
return {
|
|
5284
|
+
projectName: projectName || entry.projectId,
|
|
5285
|
+
organizationName: organizationName || entry.organizationId
|
|
5286
|
+
};
|
|
5287
|
+
}
|
|
5288
|
+
async function fetchProjectMeta(api, entry) {
|
|
5289
|
+
const response = await api.get("/api/cli/variables", {
|
|
5290
|
+
projectId: entry.projectId,
|
|
5291
|
+
...entry.organizationId && { organizationId: entry.organizationId }
|
|
5292
|
+
});
|
|
5293
|
+
return response.meta;
|
|
5294
|
+
}
|
|
5295
|
+
async function pickRequestTarget(api) {
|
|
5296
|
+
const targets = await withSpinner(
|
|
5297
|
+
"Finding projects you can request for...",
|
|
5298
|
+
async () => {
|
|
5299
|
+
const orgs = await api.listOrganizations();
|
|
5300
|
+
const orgNameById = Object.fromEntries(orgs.map((o) => [o._id, o.name]));
|
|
5301
|
+
const perOrg = await Promise.all(
|
|
5302
|
+
orgs.map((org) => api.listProjects(org._id))
|
|
5303
|
+
);
|
|
5304
|
+
return perOrg.flatMap(
|
|
5305
|
+
(projects) => buildEligibleRequestTargets(projects, orgNameById)
|
|
5306
|
+
);
|
|
5307
|
+
}
|
|
5308
|
+
);
|
|
5309
|
+
if (targets.length === 0) return null;
|
|
5310
|
+
const { projectId } = await inquirer6.prompt([
|
|
5311
|
+
{
|
|
5312
|
+
type: "list",
|
|
5313
|
+
name: "projectId",
|
|
5314
|
+
message: "Select a project to request for:",
|
|
5315
|
+
choices: buildProjectChoices(targets)
|
|
5316
|
+
}
|
|
5317
|
+
]);
|
|
5318
|
+
return targets.find((t) => t.projectId === projectId) ?? null;
|
|
5319
|
+
}
|
|
5320
|
+
|
|
5321
|
+
// src/commands/requests.ts
|
|
5322
|
+
import { Command as Command18 } from "commander";
|
|
5323
|
+
var requestsCommand = new Command18("requests").description("List variable requests for a project").option(
|
|
5324
|
+
"--project <name-or-id>",
|
|
5325
|
+
"List requests for a specific linked project"
|
|
5326
|
+
).option(
|
|
5327
|
+
"--status <status>",
|
|
5328
|
+
"Filter by status: pending, approved, rejected, canceled"
|
|
5329
|
+
).action(async (options) => {
|
|
5330
|
+
try {
|
|
5331
|
+
if (!isAuthenticated()) {
|
|
5332
|
+
throw notAuthenticated();
|
|
5333
|
+
}
|
|
5334
|
+
const configV2 = readProjectConfigV2();
|
|
5335
|
+
if (!configV2) throw notInitialized();
|
|
5336
|
+
const project = resolveProject(configV2, options.project);
|
|
5337
|
+
if (!project) {
|
|
5338
|
+
error(`Project not found: ${options.project}`);
|
|
5339
|
+
console.log();
|
|
5340
|
+
console.log("Linked projects:");
|
|
5341
|
+
for (const p of configV2.projects) {
|
|
5342
|
+
console.log(` ${p.projectName || p.projectId} (${p.environment})`);
|
|
5343
|
+
}
|
|
5344
|
+
process.exit(1);
|
|
5345
|
+
}
|
|
5346
|
+
let status;
|
|
5347
|
+
if (options.status) {
|
|
5348
|
+
const parsed = variableRequestStatusSchema.safeParse(options.status);
|
|
5349
|
+
if (!parsed.success) {
|
|
5350
|
+
error(
|
|
5351
|
+
`Invalid status: ${options.status}. Must be one of pending, approved, rejected, canceled.`
|
|
5352
|
+
);
|
|
5353
|
+
process.exit(1);
|
|
5354
|
+
}
|
|
5355
|
+
status = parsed.data;
|
|
5356
|
+
}
|
|
5357
|
+
const api = createAPIClient();
|
|
5358
|
+
const requests = await withSpinner(
|
|
5359
|
+
"Fetching variable requests...",
|
|
5360
|
+
() => api.listVariableRequests(project.projectId, status)
|
|
5361
|
+
);
|
|
5362
|
+
header(
|
|
5363
|
+
formatRequestsListHeader({
|
|
5364
|
+
projectName: project.projectName || project.projectId,
|
|
5365
|
+
organizationName: project.organizationName || project.organizationId
|
|
5366
|
+
})
|
|
5367
|
+
);
|
|
5368
|
+
if (requests.length === 0) {
|
|
5369
|
+
info("No variable requests found.");
|
|
5370
|
+
return;
|
|
5371
|
+
}
|
|
5372
|
+
table(formatRequestRows(requests), [
|
|
5373
|
+
{ key: "key", header: "KEY" },
|
|
5374
|
+
{ key: "environments", header: "ENVIRONMENTS" },
|
|
5375
|
+
{ key: "status", header: "STATUS" },
|
|
5376
|
+
{ key: "requested", header: "REQUESTED" },
|
|
5377
|
+
{ key: "reason", header: "REASON" }
|
|
5378
|
+
]);
|
|
5379
|
+
} catch (err) {
|
|
5380
|
+
await handleError(err);
|
|
5381
|
+
}
|
|
5382
|
+
});
|
|
5383
|
+
|
|
4898
5384
|
// src/lib/command-catalog.ts
|
|
4899
5385
|
function formatArgv(argv) {
|
|
4900
5386
|
return ["envpilot", ...argv].join(" ").trim();
|
|
@@ -5038,6 +5524,41 @@ var COMMAND_CATALOG = [
|
|
|
5038
5524
|
topLevel: true,
|
|
5039
5525
|
createCommand: () => pushCommand
|
|
5040
5526
|
},
|
|
5527
|
+
{
|
|
5528
|
+
id: "request",
|
|
5529
|
+
title: "Request a new variable",
|
|
5530
|
+
category: "Sync",
|
|
5531
|
+
description: "Submit a request to create a new environment variable for review (developers only).",
|
|
5532
|
+
argv: ["request"],
|
|
5533
|
+
args: "[--project <name-or-id>]",
|
|
5534
|
+
examples: [["request"], ["request", "--project", "api"]],
|
|
5535
|
+
websiteSurface: "Maps to `/api/cli/variable-requests` (POST).",
|
|
5536
|
+
notes: [
|
|
5537
|
+
"Only assigned developers can submit requests \u2014 owners, project managers, and team leads create variables directly.",
|
|
5538
|
+
"Environment choices are limited to the developer's assigned environment scope.",
|
|
5539
|
+
"An owner, project manager, or team lead must approve the request before the variable is created."
|
|
5540
|
+
],
|
|
5541
|
+
keywords: ["request", "approval", "developer", "create"],
|
|
5542
|
+
topLevel: true,
|
|
5543
|
+
createCommand: () => requestCommand
|
|
5544
|
+
},
|
|
5545
|
+
{
|
|
5546
|
+
id: "requests",
|
|
5547
|
+
title: "List variable requests",
|
|
5548
|
+
category: "Browse",
|
|
5549
|
+
description: "List pending and past variable requests for a project.",
|
|
5550
|
+
argv: ["requests"],
|
|
5551
|
+
args: "[--project <name-or-id>] [--status <status>]",
|
|
5552
|
+
examples: [["requests"], ["requests", "--status", "pending"]],
|
|
5553
|
+
websiteSurface: "Maps to `/api/cli/variable-requests` (GET).",
|
|
5554
|
+
notes: [
|
|
5555
|
+
"Reviewers (owner, assigned project manager/team lead) see every request for the project.",
|
|
5556
|
+
"Developers see only their own requests."
|
|
5557
|
+
],
|
|
5558
|
+
keywords: ["requests", "approval", "review", "pending"],
|
|
5559
|
+
topLevel: true,
|
|
5560
|
+
createCommand: () => requestsCommand
|
|
5561
|
+
},
|
|
5041
5562
|
{
|
|
5042
5563
|
id: "run",
|
|
5043
5564
|
title: "Run command with secrets",
|
package/dist/index.js
CHANGED
|
@@ -4,18 +4,18 @@ import {
|
|
|
4
4
|
getTopLevelCommandCatalog,
|
|
5
5
|
initSentry,
|
|
6
6
|
openTUI
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-QYRSBLFW.js";
|
|
8
8
|
|
|
9
9
|
// src/lib/program.ts
|
|
10
10
|
import { Command } from "commander";
|
|
11
11
|
|
|
12
12
|
// src/lib/cli-version.ts
|
|
13
|
-
var CLI_VERSION = true ? "1.
|
|
13
|
+
var CLI_VERSION = true ? "1.10.0" : "0.0.0";
|
|
14
14
|
|
|
15
15
|
// src/lib/version-check.ts
|
|
16
16
|
import chalk from "chalk";
|
|
17
17
|
import Conf from "conf";
|
|
18
|
-
var CLI_VERSION2 = true ? "1.
|
|
18
|
+
var CLI_VERSION2 = true ? "1.10.0" : "0.0.0";
|
|
19
19
|
var CHECK_INTERVAL = 60 * 60 * 1e3;
|
|
20
20
|
var _cache = null;
|
|
21
21
|
function getCache() {
|