@envpilot/cli 1.9.1 → 1.11.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,10 @@ function initSentry() {
|
|
|
7
7
|
Sentry.init({
|
|
8
8
|
dsn,
|
|
9
9
|
environment: "cli",
|
|
10
|
-
release: true ? "1.
|
|
10
|
+
release: true ? "1.11.0" : "0.0.0",
|
|
11
|
+
// All EnvPilot surfaces report to one Sentry project; the surface tag
|
|
12
|
+
// is how dashboards tell web / cli / extension events apart.
|
|
13
|
+
initialScope: { tags: { surface: "cli" } },
|
|
11
14
|
// Free tier: disable performance monitoring
|
|
12
15
|
tracesSampleRate: 0,
|
|
13
16
|
beforeSend(event) {
|
|
@@ -469,7 +472,7 @@ import { jsx } from "react/jsx-runtime";
|
|
|
469
472
|
async function openTUI() {
|
|
470
473
|
const [{ render }, { CLIApp }, { PressAnyKey }] = await Promise.all([
|
|
471
474
|
import("ink"),
|
|
472
|
-
import("./app-
|
|
475
|
+
import("./app-SRLCVHPE.js"),
|
|
473
476
|
import("./press-any-key-64XFP4O2.js")
|
|
474
477
|
]);
|
|
475
478
|
while (true) {
|
|
@@ -1169,6 +1172,29 @@ var APIClient = class {
|
|
|
1169
1172
|
async bulkUpsertVariables(data) {
|
|
1170
1173
|
return this.post("/api/cli/variables/bulk", data);
|
|
1171
1174
|
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Submit a variable request (developers only — owners/PMs/team leads
|
|
1177
|
+
* create variables directly and get a 403 from the server here).
|
|
1178
|
+
*/
|
|
1179
|
+
async createVariableRequest(data) {
|
|
1180
|
+
const response = await this.post(
|
|
1181
|
+
"/api/cli/variable-requests",
|
|
1182
|
+
data
|
|
1183
|
+
);
|
|
1184
|
+
if (!response.data) {
|
|
1185
|
+
throw new APIError("No variable request returned by server", 500);
|
|
1186
|
+
}
|
|
1187
|
+
return response.data.request;
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* List variable requests for a project, optionally filtered by status.
|
|
1191
|
+
*/
|
|
1192
|
+
async listVariableRequests(projectId, status) {
|
|
1193
|
+
const params = { projectId };
|
|
1194
|
+
if (status) params.status = status;
|
|
1195
|
+
const response = await this.get("/api/cli/variable-requests", params);
|
|
1196
|
+
return response.data?.requests ?? [];
|
|
1197
|
+
}
|
|
1172
1198
|
// ============================================
|
|
1173
1199
|
// Authentication methods
|
|
1174
1200
|
// ============================================
|
|
@@ -1457,6 +1483,32 @@ var projectConfigV2Schema = z.object({
|
|
|
1457
1483
|
activeProjectId: z.string(),
|
|
1458
1484
|
projects: z.array(projectEntrySchema).min(1)
|
|
1459
1485
|
});
|
|
1486
|
+
var variableRequestStatusSchema = z.enum([
|
|
1487
|
+
"pending",
|
|
1488
|
+
"approved",
|
|
1489
|
+
"rejected",
|
|
1490
|
+
"canceled"
|
|
1491
|
+
]);
|
|
1492
|
+
var variableRequestUserSchema = z.object({
|
|
1493
|
+
_id: z.string(),
|
|
1494
|
+
email: z.string().optional(),
|
|
1495
|
+
name: z.string().optional()
|
|
1496
|
+
}).nullable().optional();
|
|
1497
|
+
var variableRequestSchema = z.object({
|
|
1498
|
+
_id: z.string(),
|
|
1499
|
+
key: z.string(),
|
|
1500
|
+
description: z.string().optional(),
|
|
1501
|
+
environments: z.array(z.string()),
|
|
1502
|
+
projectId: z.string(),
|
|
1503
|
+
organizationId: z.string().optional(),
|
|
1504
|
+
isSensitive: z.boolean().optional(),
|
|
1505
|
+
status: variableRequestStatusSchema,
|
|
1506
|
+
reviewReason: z.string().optional(),
|
|
1507
|
+
createdAt: z.number(),
|
|
1508
|
+
updatedAt: z.number().optional(),
|
|
1509
|
+
requester: variableRequestUserSchema,
|
|
1510
|
+
reviewer: variableRequestUserSchema
|
|
1511
|
+
});
|
|
1460
1512
|
|
|
1461
1513
|
// src/lib/project-config.ts
|
|
1462
1514
|
var CONFIG_FILE_NAME = ".envpilot";
|
|
@@ -3757,8 +3809,8 @@ async function handlePath() {
|
|
|
3757
3809
|
]);
|
|
3758
3810
|
}
|
|
3759
3811
|
async function handleReset() {
|
|
3760
|
-
const
|
|
3761
|
-
const { confirm } = await
|
|
3812
|
+
const inquirer7 = await import("inquirer");
|
|
3813
|
+
const { confirm } = await inquirer7.default.prompt([
|
|
3762
3814
|
{
|
|
3763
3815
|
type: "confirm",
|
|
3764
3816
|
name: "confirm",
|
|
@@ -4895,6 +4947,443 @@ function createUICommand() {
|
|
|
4895
4947
|
});
|
|
4896
4948
|
}
|
|
4897
4949
|
|
|
4950
|
+
// src/commands/request.ts
|
|
4951
|
+
import { Command as Command17 } from "commander";
|
|
4952
|
+
import chalk17 from "chalk";
|
|
4953
|
+
import inquirer6 from "inquirer";
|
|
4954
|
+
|
|
4955
|
+
// src/lib/variable-requests.ts
|
|
4956
|
+
import { z as z3 } from "zod";
|
|
4957
|
+
var ALL_REQUEST_ENVIRONMENTS = [
|
|
4958
|
+
"development",
|
|
4959
|
+
"staging",
|
|
4960
|
+
"production"
|
|
4961
|
+
];
|
|
4962
|
+
var requestKeySchema = z3.string().min(1, "Key is required").max(100, "Key must be 100 characters or less").regex(
|
|
4963
|
+
/^[A-Z][A-Z0-9_]*$/,
|
|
4964
|
+
"Key must be uppercase, start with a letter, and contain only letters, numbers, and underscores"
|
|
4965
|
+
);
|
|
4966
|
+
var requestValueSchema = z3.string().min(1, "Value is required");
|
|
4967
|
+
var requestDescriptionSchema = z3.string().max(500, "Description must be 500 characters or less");
|
|
4968
|
+
function validateRequestKey(key) {
|
|
4969
|
+
const result = requestKeySchema.safeParse(key);
|
|
4970
|
+
if (result.success) return { valid: true };
|
|
4971
|
+
return {
|
|
4972
|
+
valid: false,
|
|
4973
|
+
error: result.error.issues[0]?.message ?? "Invalid key"
|
|
4974
|
+
};
|
|
4975
|
+
}
|
|
4976
|
+
function validateRequestValue(value) {
|
|
4977
|
+
const result = requestValueSchema.safeParse(value);
|
|
4978
|
+
if (result.success) return { valid: true };
|
|
4979
|
+
return {
|
|
4980
|
+
valid: false,
|
|
4981
|
+
error: result.error.issues[0]?.message ?? "Invalid value"
|
|
4982
|
+
};
|
|
4983
|
+
}
|
|
4984
|
+
function validateRequestDescription(description) {
|
|
4985
|
+
if (description.length === 0) return { valid: true };
|
|
4986
|
+
const result = requestDescriptionSchema.safeParse(description);
|
|
4987
|
+
if (result.success) return { valid: true };
|
|
4988
|
+
return {
|
|
4989
|
+
valid: false,
|
|
4990
|
+
error: result.error.issues[0]?.message ?? "Invalid description"
|
|
4991
|
+
};
|
|
4992
|
+
}
|
|
4993
|
+
function allowedRequestEnvironments(environmentScope) {
|
|
4994
|
+
if (environmentScope == null) {
|
|
4995
|
+
return [...ALL_REQUEST_ENVIRONMENTS];
|
|
4996
|
+
}
|
|
4997
|
+
return ALL_REQUEST_ENVIRONMENTS.filter(
|
|
4998
|
+
(env) => environmentScope.includes(env)
|
|
4999
|
+
);
|
|
5000
|
+
}
|
|
5001
|
+
function buildCreateVariableRequestBody(input) {
|
|
5002
|
+
const description = input.description?.trim();
|
|
5003
|
+
return {
|
|
5004
|
+
projectId: input.projectId,
|
|
5005
|
+
key: input.key.trim(),
|
|
5006
|
+
value: input.value,
|
|
5007
|
+
...description ? { description } : {},
|
|
5008
|
+
environments: input.environments,
|
|
5009
|
+
isSensitive: input.isSensitive ?? false
|
|
5010
|
+
};
|
|
5011
|
+
}
|
|
5012
|
+
function isRequestEligibleProject(project) {
|
|
5013
|
+
const role = normalizeOrgRole(project.unifiedRole ?? project.role);
|
|
5014
|
+
return role === "developer" && project.assigned === true;
|
|
5015
|
+
}
|
|
5016
|
+
function buildEligibleRequestTargets(projects, orgNameById) {
|
|
5017
|
+
return projects.filter(isRequestEligibleProject).map((p) => ({
|
|
5018
|
+
projectId: p._id,
|
|
5019
|
+
projectName: p.name,
|
|
5020
|
+
organizationId: p.organizationId,
|
|
5021
|
+
organizationName: orgNameById[p.organizationId] ?? p.organizationId,
|
|
5022
|
+
environmentScope: p.environmentScope ?? null
|
|
5023
|
+
}));
|
|
5024
|
+
}
|
|
5025
|
+
function formatProjectChoiceLabel(target) {
|
|
5026
|
+
return `${target.projectName} \u2014 ${target.organizationName}`;
|
|
5027
|
+
}
|
|
5028
|
+
function buildProjectChoices(targets) {
|
|
5029
|
+
return targets.map((t) => ({
|
|
5030
|
+
name: formatProjectChoiceLabel(t),
|
|
5031
|
+
value: t.projectId
|
|
5032
|
+
}));
|
|
5033
|
+
}
|
|
5034
|
+
function formatRequestContextBanner(input) {
|
|
5035
|
+
return `Requesting as ${input.email}
|
|
5036
|
+
Project: ${input.projectName} \xB7 Organization: ${input.organizationName}`;
|
|
5037
|
+
}
|
|
5038
|
+
var SUMMARY_LABEL_WIDTH = 15;
|
|
5039
|
+
function formatRequestSummary(input) {
|
|
5040
|
+
const rows = [
|
|
5041
|
+
["Key:", input.key],
|
|
5042
|
+
[
|
|
5043
|
+
"Project:",
|
|
5044
|
+
formatProjectChoiceLabel({
|
|
5045
|
+
projectName: input.projectName,
|
|
5046
|
+
organizationName: input.organizationName
|
|
5047
|
+
})
|
|
5048
|
+
],
|
|
5049
|
+
["Environments:", input.environments.join(", ")],
|
|
5050
|
+
["Sensitive:", input.isSensitive ? "yes" : "no"]
|
|
5051
|
+
];
|
|
5052
|
+
return rows.map(([label, value]) => `${label.padEnd(SUMMARY_LABEL_WIDTH)}${value}`).join("\n");
|
|
5053
|
+
}
|
|
5054
|
+
function formatRequestSuccessMessage(input) {
|
|
5055
|
+
return `Request "${input.key}" submitted for ${input.projectName} (${input.organizationName}) \u2014 pending review.`;
|
|
5056
|
+
}
|
|
5057
|
+
function formatRequestsListHeader(input) {
|
|
5058
|
+
return `Requests for ${formatProjectChoiceLabel(input)}`;
|
|
5059
|
+
}
|
|
5060
|
+
function formatRequestRow(request) {
|
|
5061
|
+
return {
|
|
5062
|
+
key: request.key,
|
|
5063
|
+
environments: request.environments.join(", "),
|
|
5064
|
+
status: request.status,
|
|
5065
|
+
requested: new Date(request.createdAt).toLocaleDateString(),
|
|
5066
|
+
reason: request.reviewReason ?? ""
|
|
5067
|
+
};
|
|
5068
|
+
}
|
|
5069
|
+
function formatRequestRows(requests) {
|
|
5070
|
+
return requests.map(formatRequestRow);
|
|
5071
|
+
}
|
|
5072
|
+
|
|
5073
|
+
// src/commands/request.ts
|
|
5074
|
+
var requestCommand = new Command17("request").description(
|
|
5075
|
+
"Request creation of a new environment variable (developers only \u2014 owners, project managers, and team leads create variables directly)"
|
|
5076
|
+
).option(
|
|
5077
|
+
"--project <name-or-id>",
|
|
5078
|
+
"Submit the request for a specific linked project"
|
|
5079
|
+
).action(async (options) => {
|
|
5080
|
+
try {
|
|
5081
|
+
if (!isAuthenticated()) {
|
|
5082
|
+
throw notAuthenticated();
|
|
5083
|
+
}
|
|
5084
|
+
const email = getUser()?.email ?? "unknown account";
|
|
5085
|
+
const api = createAPIClient();
|
|
5086
|
+
const projectPassed = Boolean(options.project);
|
|
5087
|
+
const defaultEntry = resolveDefaultEntry(options.project);
|
|
5088
|
+
let target = null;
|
|
5089
|
+
let scope;
|
|
5090
|
+
if (defaultEntry) {
|
|
5091
|
+
const names = await resolveEntryNames(api, defaultEntry);
|
|
5092
|
+
const defaultTarget = {
|
|
5093
|
+
projectId: defaultEntry.projectId,
|
|
5094
|
+
projectName: names.projectName,
|
|
5095
|
+
organizationId: defaultEntry.organizationId,
|
|
5096
|
+
organizationName: names.organizationName
|
|
5097
|
+
};
|
|
5098
|
+
printBanner(email, defaultTarget);
|
|
5099
|
+
let useDefault = projectPassed;
|
|
5100
|
+
if (!projectPassed) {
|
|
5101
|
+
const { proceed } = await inquirer6.prompt([
|
|
5102
|
+
{
|
|
5103
|
+
type: "confirm",
|
|
5104
|
+
name: "proceed",
|
|
5105
|
+
message: "Request a variable for this project?",
|
|
5106
|
+
default: true
|
|
5107
|
+
}
|
|
5108
|
+
]);
|
|
5109
|
+
useDefault = proceed;
|
|
5110
|
+
}
|
|
5111
|
+
if (useDefault) {
|
|
5112
|
+
const meta = await fetchProjectMeta(api, defaultEntry);
|
|
5113
|
+
const role = normalizeOrgRole(meta?.unifiedRole ?? meta?.role);
|
|
5114
|
+
if (role !== "developer") {
|
|
5115
|
+
warning(
|
|
5116
|
+
"You have direct write access to this project. Use `envpilot push` or create the variable directly instead of submitting a request."
|
|
5117
|
+
);
|
|
5118
|
+
return;
|
|
5119
|
+
}
|
|
5120
|
+
target = defaultTarget;
|
|
5121
|
+
scope = meta?.environmentScope;
|
|
5122
|
+
}
|
|
5123
|
+
}
|
|
5124
|
+
if (!target) {
|
|
5125
|
+
const picked = await pickRequestTarget(api);
|
|
5126
|
+
if (!picked) {
|
|
5127
|
+
info(
|
|
5128
|
+
"You have direct write access in your projects \u2014 create variables from the dashboard."
|
|
5129
|
+
);
|
|
5130
|
+
process.exit(0);
|
|
5131
|
+
}
|
|
5132
|
+
target = picked;
|
|
5133
|
+
scope = picked.environmentScope;
|
|
5134
|
+
printBanner(email, target);
|
|
5135
|
+
}
|
|
5136
|
+
const envChoices = allowedRequestEnvironments(scope);
|
|
5137
|
+
if (envChoices.length === 0) {
|
|
5138
|
+
error(
|
|
5139
|
+
"Your assignment does not include any environments in this project."
|
|
5140
|
+
);
|
|
5141
|
+
process.exit(1);
|
|
5142
|
+
}
|
|
5143
|
+
const answers = await inquirer6.prompt([
|
|
5144
|
+
{
|
|
5145
|
+
type: "input",
|
|
5146
|
+
name: "key",
|
|
5147
|
+
message: "Variable key (e.g. API_SECRET):",
|
|
5148
|
+
validate: (input) => {
|
|
5149
|
+
const result = validateRequestKey(input);
|
|
5150
|
+
return result.valid || result.error;
|
|
5151
|
+
}
|
|
5152
|
+
},
|
|
5153
|
+
{
|
|
5154
|
+
type: "password",
|
|
5155
|
+
name: "value",
|
|
5156
|
+
message: "Variable value:",
|
|
5157
|
+
mask: "*",
|
|
5158
|
+
validate: (input) => {
|
|
5159
|
+
const result = validateRequestValue(input);
|
|
5160
|
+
return result.valid || result.error;
|
|
5161
|
+
}
|
|
5162
|
+
},
|
|
5163
|
+
{
|
|
5164
|
+
type: "input",
|
|
5165
|
+
name: "description",
|
|
5166
|
+
message: "Description (optional):",
|
|
5167
|
+
default: "",
|
|
5168
|
+
validate: (input) => {
|
|
5169
|
+
const result = validateRequestDescription(input);
|
|
5170
|
+
return result.valid || result.error;
|
|
5171
|
+
}
|
|
5172
|
+
},
|
|
5173
|
+
{
|
|
5174
|
+
type: "checkbox",
|
|
5175
|
+
name: "environments",
|
|
5176
|
+
message: "Environments:",
|
|
5177
|
+
choices: envChoices,
|
|
5178
|
+
validate: (input) => input.length > 0 || "Select at least one environment"
|
|
5179
|
+
},
|
|
5180
|
+
{
|
|
5181
|
+
type: "confirm",
|
|
5182
|
+
name: "isSensitive",
|
|
5183
|
+
message: "Is this value sensitive?",
|
|
5184
|
+
default: true
|
|
5185
|
+
}
|
|
5186
|
+
]);
|
|
5187
|
+
console.log();
|
|
5188
|
+
console.log(
|
|
5189
|
+
formatRequestSummary({
|
|
5190
|
+
key: answers.key.trim(),
|
|
5191
|
+
projectName: target.projectName,
|
|
5192
|
+
organizationName: target.organizationName,
|
|
5193
|
+
environments: answers.environments,
|
|
5194
|
+
isSensitive: answers.isSensitive
|
|
5195
|
+
})
|
|
5196
|
+
);
|
|
5197
|
+
console.log();
|
|
5198
|
+
const { submit } = await inquirer6.prompt([
|
|
5199
|
+
{
|
|
5200
|
+
type: "confirm",
|
|
5201
|
+
name: "submit",
|
|
5202
|
+
message: "Submit this request?",
|
|
5203
|
+
default: true
|
|
5204
|
+
}
|
|
5205
|
+
]);
|
|
5206
|
+
if (!submit) {
|
|
5207
|
+
info("Request canceled.");
|
|
5208
|
+
return;
|
|
5209
|
+
}
|
|
5210
|
+
const body = buildCreateVariableRequestBody({
|
|
5211
|
+
projectId: target.projectId,
|
|
5212
|
+
key: answers.key,
|
|
5213
|
+
value: answers.value,
|
|
5214
|
+
description: answers.description,
|
|
5215
|
+
environments: answers.environments,
|
|
5216
|
+
isSensitive: answers.isSensitive
|
|
5217
|
+
});
|
|
5218
|
+
const created = await api.createVariableRequest(body);
|
|
5219
|
+
success(
|
|
5220
|
+
formatRequestSuccessMessage({
|
|
5221
|
+
key: created.key,
|
|
5222
|
+
projectName: target.projectName,
|
|
5223
|
+
organizationName: target.organizationName
|
|
5224
|
+
})
|
|
5225
|
+
);
|
|
5226
|
+
info(`Status: ${created.status} \xB7 Id: ${created._id}`);
|
|
5227
|
+
} catch (err) {
|
|
5228
|
+
if (err instanceof APIError && err.statusCode === 403) {
|
|
5229
|
+
error(err.message);
|
|
5230
|
+
return;
|
|
5231
|
+
}
|
|
5232
|
+
await handleError(err);
|
|
5233
|
+
}
|
|
5234
|
+
});
|
|
5235
|
+
function resolveDefaultEntry(projectOption) {
|
|
5236
|
+
const configV2 = readProjectConfigV2();
|
|
5237
|
+
if (!configV2) {
|
|
5238
|
+
if (projectOption) throw notInitialized();
|
|
5239
|
+
return null;
|
|
5240
|
+
}
|
|
5241
|
+
const project = resolveProject(configV2, projectOption);
|
|
5242
|
+
if (!project) {
|
|
5243
|
+
if (projectOption) {
|
|
5244
|
+
error(`Project not found: ${projectOption}`);
|
|
5245
|
+
console.log();
|
|
5246
|
+
console.log("Linked projects:");
|
|
5247
|
+
for (const p of configV2.projects) {
|
|
5248
|
+
console.log(` ${p.projectName || p.projectId} (${p.environment})`);
|
|
5249
|
+
}
|
|
5250
|
+
process.exit(1);
|
|
5251
|
+
}
|
|
5252
|
+
return null;
|
|
5253
|
+
}
|
|
5254
|
+
return project;
|
|
5255
|
+
}
|
|
5256
|
+
function printBanner(email, target) {
|
|
5257
|
+
console.log();
|
|
5258
|
+
console.log(
|
|
5259
|
+
chalk17.dim(
|
|
5260
|
+
formatRequestContextBanner({
|
|
5261
|
+
email,
|
|
5262
|
+
projectName: target.projectName,
|
|
5263
|
+
organizationName: target.organizationName
|
|
5264
|
+
})
|
|
5265
|
+
)
|
|
5266
|
+
);
|
|
5267
|
+
console.log();
|
|
5268
|
+
}
|
|
5269
|
+
async function resolveEntryNames(api, entry) {
|
|
5270
|
+
let projectName = entry.projectName;
|
|
5271
|
+
let organizationName = entry.organizationName;
|
|
5272
|
+
if (!projectName) {
|
|
5273
|
+
try {
|
|
5274
|
+
const project = await api.getProject(entry.projectId);
|
|
5275
|
+
projectName = project.name;
|
|
5276
|
+
} catch {
|
|
5277
|
+
}
|
|
5278
|
+
}
|
|
5279
|
+
if (!organizationName) {
|
|
5280
|
+
try {
|
|
5281
|
+
const orgs = await api.listOrganizations();
|
|
5282
|
+
organizationName = orgs.find((o) => o._id === entry.organizationId)?.name ?? "";
|
|
5283
|
+
} catch {
|
|
5284
|
+
}
|
|
5285
|
+
}
|
|
5286
|
+
return {
|
|
5287
|
+
projectName: projectName || entry.projectId,
|
|
5288
|
+
organizationName: organizationName || entry.organizationId
|
|
5289
|
+
};
|
|
5290
|
+
}
|
|
5291
|
+
async function fetchProjectMeta(api, entry) {
|
|
5292
|
+
const response = await api.get("/api/cli/variables", {
|
|
5293
|
+
projectId: entry.projectId,
|
|
5294
|
+
...entry.organizationId && { organizationId: entry.organizationId }
|
|
5295
|
+
});
|
|
5296
|
+
return response.meta;
|
|
5297
|
+
}
|
|
5298
|
+
async function pickRequestTarget(api) {
|
|
5299
|
+
const targets = await withSpinner(
|
|
5300
|
+
"Finding projects you can request for...",
|
|
5301
|
+
async () => {
|
|
5302
|
+
const orgs = await api.listOrganizations();
|
|
5303
|
+
const orgNameById = Object.fromEntries(orgs.map((o) => [o._id, o.name]));
|
|
5304
|
+
const perOrg = await Promise.all(
|
|
5305
|
+
orgs.map((org) => api.listProjects(org._id))
|
|
5306
|
+
);
|
|
5307
|
+
return perOrg.flatMap(
|
|
5308
|
+
(projects) => buildEligibleRequestTargets(projects, orgNameById)
|
|
5309
|
+
);
|
|
5310
|
+
}
|
|
5311
|
+
);
|
|
5312
|
+
if (targets.length === 0) return null;
|
|
5313
|
+
const { projectId } = await inquirer6.prompt([
|
|
5314
|
+
{
|
|
5315
|
+
type: "list",
|
|
5316
|
+
name: "projectId",
|
|
5317
|
+
message: "Select a project to request for:",
|
|
5318
|
+
choices: buildProjectChoices(targets)
|
|
5319
|
+
}
|
|
5320
|
+
]);
|
|
5321
|
+
return targets.find((t) => t.projectId === projectId) ?? null;
|
|
5322
|
+
}
|
|
5323
|
+
|
|
5324
|
+
// src/commands/requests.ts
|
|
5325
|
+
import { Command as Command18 } from "commander";
|
|
5326
|
+
var requestsCommand = new Command18("requests").description("List variable requests for a project").option(
|
|
5327
|
+
"--project <name-or-id>",
|
|
5328
|
+
"List requests for a specific linked project"
|
|
5329
|
+
).option(
|
|
5330
|
+
"--status <status>",
|
|
5331
|
+
"Filter by status: pending, approved, rejected, canceled"
|
|
5332
|
+
).action(async (options) => {
|
|
5333
|
+
try {
|
|
5334
|
+
if (!isAuthenticated()) {
|
|
5335
|
+
throw notAuthenticated();
|
|
5336
|
+
}
|
|
5337
|
+
const configV2 = readProjectConfigV2();
|
|
5338
|
+
if (!configV2) throw notInitialized();
|
|
5339
|
+
const project = resolveProject(configV2, options.project);
|
|
5340
|
+
if (!project) {
|
|
5341
|
+
error(`Project not found: ${options.project}`);
|
|
5342
|
+
console.log();
|
|
5343
|
+
console.log("Linked projects:");
|
|
5344
|
+
for (const p of configV2.projects) {
|
|
5345
|
+
console.log(` ${p.projectName || p.projectId} (${p.environment})`);
|
|
5346
|
+
}
|
|
5347
|
+
process.exit(1);
|
|
5348
|
+
}
|
|
5349
|
+
let status;
|
|
5350
|
+
if (options.status) {
|
|
5351
|
+
const parsed = variableRequestStatusSchema.safeParse(options.status);
|
|
5352
|
+
if (!parsed.success) {
|
|
5353
|
+
error(
|
|
5354
|
+
`Invalid status: ${options.status}. Must be one of pending, approved, rejected, canceled.`
|
|
5355
|
+
);
|
|
5356
|
+
process.exit(1);
|
|
5357
|
+
}
|
|
5358
|
+
status = parsed.data;
|
|
5359
|
+
}
|
|
5360
|
+
const api = createAPIClient();
|
|
5361
|
+
const requests = await withSpinner(
|
|
5362
|
+
"Fetching variable requests...",
|
|
5363
|
+
() => api.listVariableRequests(project.projectId, status)
|
|
5364
|
+
);
|
|
5365
|
+
header(
|
|
5366
|
+
formatRequestsListHeader({
|
|
5367
|
+
projectName: project.projectName || project.projectId,
|
|
5368
|
+
organizationName: project.organizationName || project.organizationId
|
|
5369
|
+
})
|
|
5370
|
+
);
|
|
5371
|
+
if (requests.length === 0) {
|
|
5372
|
+
info("No variable requests found.");
|
|
5373
|
+
return;
|
|
5374
|
+
}
|
|
5375
|
+
table(formatRequestRows(requests), [
|
|
5376
|
+
{ key: "key", header: "KEY" },
|
|
5377
|
+
{ key: "environments", header: "ENVIRONMENTS" },
|
|
5378
|
+
{ key: "status", header: "STATUS" },
|
|
5379
|
+
{ key: "requested", header: "REQUESTED" },
|
|
5380
|
+
{ key: "reason", header: "REASON" }
|
|
5381
|
+
]);
|
|
5382
|
+
} catch (err) {
|
|
5383
|
+
await handleError(err);
|
|
5384
|
+
}
|
|
5385
|
+
});
|
|
5386
|
+
|
|
4898
5387
|
// src/lib/command-catalog.ts
|
|
4899
5388
|
function formatArgv(argv) {
|
|
4900
5389
|
return ["envpilot", ...argv].join(" ").trim();
|
|
@@ -5038,6 +5527,41 @@ var COMMAND_CATALOG = [
|
|
|
5038
5527
|
topLevel: true,
|
|
5039
5528
|
createCommand: () => pushCommand
|
|
5040
5529
|
},
|
|
5530
|
+
{
|
|
5531
|
+
id: "request",
|
|
5532
|
+
title: "Request a new variable",
|
|
5533
|
+
category: "Sync",
|
|
5534
|
+
description: "Submit a request to create a new environment variable for review (developers only).",
|
|
5535
|
+
argv: ["request"],
|
|
5536
|
+
args: "[--project <name-or-id>]",
|
|
5537
|
+
examples: [["request"], ["request", "--project", "api"]],
|
|
5538
|
+
websiteSurface: "Maps to `/api/cli/variable-requests` (POST).",
|
|
5539
|
+
notes: [
|
|
5540
|
+
"Only assigned developers can submit requests \u2014 owners, project managers, and team leads create variables directly.",
|
|
5541
|
+
"Environment choices are limited to the developer's assigned environment scope.",
|
|
5542
|
+
"An owner, project manager, or team lead must approve the request before the variable is created."
|
|
5543
|
+
],
|
|
5544
|
+
keywords: ["request", "approval", "developer", "create"],
|
|
5545
|
+
topLevel: true,
|
|
5546
|
+
createCommand: () => requestCommand
|
|
5547
|
+
},
|
|
5548
|
+
{
|
|
5549
|
+
id: "requests",
|
|
5550
|
+
title: "List variable requests",
|
|
5551
|
+
category: "Browse",
|
|
5552
|
+
description: "List pending and past variable requests for a project.",
|
|
5553
|
+
argv: ["requests"],
|
|
5554
|
+
args: "[--project <name-or-id>] [--status <status>]",
|
|
5555
|
+
examples: [["requests"], ["requests", "--status", "pending"]],
|
|
5556
|
+
websiteSurface: "Maps to `/api/cli/variable-requests` (GET).",
|
|
5557
|
+
notes: [
|
|
5558
|
+
"Reviewers (owner, assigned project manager/team lead) see every request for the project.",
|
|
5559
|
+
"Developers see only their own requests."
|
|
5560
|
+
],
|
|
5561
|
+
keywords: ["requests", "approval", "review", "pending"],
|
|
5562
|
+
topLevel: true,
|
|
5563
|
+
createCommand: () => requestsCommand
|
|
5564
|
+
},
|
|
5041
5565
|
{
|
|
5042
5566
|
id: "run",
|
|
5043
5567
|
title: "Run command with secrets",
|
|
@@ -5305,6 +5829,8 @@ function findCommandDefinition(commandIdOrName) {
|
|
|
5305
5829
|
|
|
5306
5830
|
export {
|
|
5307
5831
|
initSentry,
|
|
5832
|
+
captureError,
|
|
5833
|
+
flushSentry,
|
|
5308
5834
|
getApiUrl,
|
|
5309
5835
|
getUser,
|
|
5310
5836
|
isAuthenticated,
|
package/dist/index.js
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
captureError,
|
|
4
|
+
flushSentry,
|
|
3
5
|
getApiUrl,
|
|
4
6
|
getTopLevelCommandCatalog,
|
|
5
7
|
initSentry,
|
|
6
8
|
openTUI
|
|
7
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-AS4NIUYS.js";
|
|
8
10
|
|
|
9
11
|
// src/lib/program.ts
|
|
10
12
|
import { Command } from "commander";
|
|
11
13
|
|
|
12
14
|
// src/lib/cli-version.ts
|
|
13
|
-
var CLI_VERSION = true ? "1.
|
|
15
|
+
var CLI_VERSION = true ? "1.11.0" : "0.0.0";
|
|
14
16
|
|
|
15
17
|
// src/lib/version-check.ts
|
|
16
18
|
import chalk from "chalk";
|
|
17
19
|
import Conf from "conf";
|
|
18
|
-
var CLI_VERSION2 = true ? "1.
|
|
20
|
+
var CLI_VERSION2 = true ? "1.11.0" : "0.0.0";
|
|
19
21
|
var CHECK_INTERVAL = 60 * 60 * 1e3;
|
|
20
22
|
var _cache = null;
|
|
21
23
|
function getCache() {
|
|
@@ -89,7 +91,9 @@ async function main() {
|
|
|
89
91
|
await program.parseAsync();
|
|
90
92
|
}
|
|
91
93
|
}
|
|
92
|
-
main().catch((err) => {
|
|
94
|
+
main().catch(async (err) => {
|
|
93
95
|
console.error(err);
|
|
96
|
+
captureError(err, { phase: "top-level" });
|
|
97
|
+
await flushSentry();
|
|
94
98
|
process.exit(1);
|
|
95
99
|
});
|