@envpilot/cli 1.3.0 → 1.3.2
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/dist/index.js +1450 -514
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ function initSentry() {
|
|
|
9
9
|
Sentry.init({
|
|
10
10
|
dsn,
|
|
11
11
|
environment: "cli",
|
|
12
|
-
release: true ? "1.3.
|
|
12
|
+
release: true ? "1.3.2" : "0.0.0",
|
|
13
13
|
// Free tier: disable performance monitoring
|
|
14
14
|
tracesSampleRate: 0,
|
|
15
15
|
beforeSend(event) {
|
|
@@ -49,12 +49,10 @@ async function flushSentry() {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
// src/index.ts
|
|
52
|
-
import { Command as
|
|
52
|
+
import { Command as Command12 } from "commander";
|
|
53
53
|
|
|
54
54
|
// src/commands/login.ts
|
|
55
55
|
import { Command } from "commander";
|
|
56
|
-
import chalk3 from "chalk";
|
|
57
|
-
import open from "open";
|
|
58
56
|
|
|
59
57
|
// src/lib/ui.ts
|
|
60
58
|
import chalk from "chalk";
|
|
@@ -189,6 +187,91 @@ function projectRoleNotice(projectRole) {
|
|
|
189
187
|
}
|
|
190
188
|
}
|
|
191
189
|
|
|
190
|
+
// src/lib/errors.ts
|
|
191
|
+
import chalk2 from "chalk";
|
|
192
|
+
var CLIError = class extends Error {
|
|
193
|
+
constructor(message, code, suggestion) {
|
|
194
|
+
super(message);
|
|
195
|
+
this.code = code;
|
|
196
|
+
this.suggestion = suggestion;
|
|
197
|
+
this.name = "CLIError";
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
var ErrorCodes = {
|
|
201
|
+
NOT_AUTHENTICATED: "NOT_AUTHENTICATED",
|
|
202
|
+
NOT_INITIALIZED: "NOT_INITIALIZED",
|
|
203
|
+
PROJECT_NOT_FOUND: "PROJECT_NOT_FOUND",
|
|
204
|
+
ORGANIZATION_NOT_FOUND: "ORGANIZATION_NOT_FOUND",
|
|
205
|
+
VARIABLE_NOT_FOUND: "VARIABLE_NOT_FOUND",
|
|
206
|
+
INVALID_CONFIG: "INVALID_CONFIG",
|
|
207
|
+
NETWORK_ERROR: "NETWORK_ERROR",
|
|
208
|
+
PERMISSION_DENIED: "PERMISSION_DENIED",
|
|
209
|
+
TIER_LIMIT_EXCEEDED: "TIER_LIMIT_EXCEEDED",
|
|
210
|
+
FILE_NOT_FOUND: "FILE_NOT_FOUND",
|
|
211
|
+
INVALID_INPUT: "INVALID_INPUT",
|
|
212
|
+
UNKNOWN_ERROR: "UNKNOWN_ERROR"
|
|
213
|
+
};
|
|
214
|
+
function formatError(error2) {
|
|
215
|
+
if (error2 instanceof CLIError) {
|
|
216
|
+
let message = chalk2.red(`Error: ${error2.message}`);
|
|
217
|
+
if (error2.suggestion) {
|
|
218
|
+
message += `
|
|
219
|
+
${chalk2.yellow("Suggestion:")} ${error2.suggestion}`;
|
|
220
|
+
}
|
|
221
|
+
return message;
|
|
222
|
+
}
|
|
223
|
+
if (error2 instanceof Error) {
|
|
224
|
+
return chalk2.red(`Error: ${error2.message}`);
|
|
225
|
+
}
|
|
226
|
+
return chalk2.red(`Error: ${String(error2)}`);
|
|
227
|
+
}
|
|
228
|
+
async function handleError(error2) {
|
|
229
|
+
console.error(formatError(error2));
|
|
230
|
+
const skipCodes = /* @__PURE__ */ new Set([
|
|
231
|
+
ErrorCodes.NOT_AUTHENTICATED,
|
|
232
|
+
ErrorCodes.INVALID_INPUT,
|
|
233
|
+
ErrorCodes.NOT_INITIALIZED
|
|
234
|
+
]);
|
|
235
|
+
if (error2 instanceof CLIError) {
|
|
236
|
+
if (!skipCodes.has(error2.code)) {
|
|
237
|
+
captureError(error2, { errorCode: error2.code });
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
captureError(error2);
|
|
241
|
+
}
|
|
242
|
+
await flushSentry();
|
|
243
|
+
if (error2 instanceof CLIError) {
|
|
244
|
+
switch (error2.code) {
|
|
245
|
+
case ErrorCodes.NOT_AUTHENTICATED:
|
|
246
|
+
process.exit(2);
|
|
247
|
+
case ErrorCodes.PERMISSION_DENIED:
|
|
248
|
+
process.exit(3);
|
|
249
|
+
case ErrorCodes.TIER_LIMIT_EXCEEDED:
|
|
250
|
+
process.exit(4);
|
|
251
|
+
default:
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
257
|
+
function notAuthenticated() {
|
|
258
|
+
return new CLIError(
|
|
259
|
+
"You are not authenticated.",
|
|
260
|
+
ErrorCodes.NOT_AUTHENTICATED,
|
|
261
|
+
"Run `envpilot login` to authenticate."
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
function notInitialized() {
|
|
265
|
+
return new CLIError(
|
|
266
|
+
"This directory is not initialized with Envpilot.",
|
|
267
|
+
ErrorCodes.NOT_INITIALIZED,
|
|
268
|
+
"Run `envpilot init` to initialize."
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
function fileNotFound(path) {
|
|
272
|
+
return new CLIError(`File not found: ${path}`, ErrorCodes.FILE_NOT_FOUND);
|
|
273
|
+
}
|
|
274
|
+
|
|
192
275
|
// src/lib/config.ts
|
|
193
276
|
import Conf from "conf";
|
|
194
277
|
var DEFAULT_API_URL = "https://www.envpilot.dev";
|
|
@@ -261,6 +344,11 @@ function getConfigPath() {
|
|
|
261
344
|
return config.path;
|
|
262
345
|
}
|
|
263
346
|
|
|
347
|
+
// src/lib/auth-flow.ts
|
|
348
|
+
import open from "open";
|
|
349
|
+
import chalk3 from "chalk";
|
|
350
|
+
import { hostname } from "os";
|
|
351
|
+
|
|
264
352
|
// src/lib/api.ts
|
|
265
353
|
var APIError = class extends Error {
|
|
266
354
|
constructor(message, statusCode, code) {
|
|
@@ -546,174 +634,84 @@ function createAPIClient() {
|
|
|
546
634
|
return new APIClient();
|
|
547
635
|
}
|
|
548
636
|
|
|
549
|
-
// src/lib/
|
|
550
|
-
|
|
551
|
-
var
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
this.code = code;
|
|
555
|
-
this.suggestion = suggestion;
|
|
556
|
-
this.name = "CLIError";
|
|
557
|
-
}
|
|
558
|
-
};
|
|
559
|
-
var ErrorCodes = {
|
|
560
|
-
NOT_AUTHENTICATED: "NOT_AUTHENTICATED",
|
|
561
|
-
NOT_INITIALIZED: "NOT_INITIALIZED",
|
|
562
|
-
PROJECT_NOT_FOUND: "PROJECT_NOT_FOUND",
|
|
563
|
-
ORGANIZATION_NOT_FOUND: "ORGANIZATION_NOT_FOUND",
|
|
564
|
-
VARIABLE_NOT_FOUND: "VARIABLE_NOT_FOUND",
|
|
565
|
-
INVALID_CONFIG: "INVALID_CONFIG",
|
|
566
|
-
NETWORK_ERROR: "NETWORK_ERROR",
|
|
567
|
-
PERMISSION_DENIED: "PERMISSION_DENIED",
|
|
568
|
-
TIER_LIMIT_EXCEEDED: "TIER_LIMIT_EXCEEDED",
|
|
569
|
-
FILE_NOT_FOUND: "FILE_NOT_FOUND",
|
|
570
|
-
INVALID_INPUT: "INVALID_INPUT",
|
|
571
|
-
UNKNOWN_ERROR: "UNKNOWN_ERROR"
|
|
572
|
-
};
|
|
573
|
-
function formatError(error2) {
|
|
574
|
-
if (error2 instanceof CLIError) {
|
|
575
|
-
let message = chalk2.red(`Error: ${error2.message}`);
|
|
576
|
-
if (error2.suggestion) {
|
|
577
|
-
message += `
|
|
578
|
-
${chalk2.yellow("Suggestion:")} ${error2.suggestion}`;
|
|
579
|
-
}
|
|
580
|
-
return message;
|
|
581
|
-
}
|
|
582
|
-
if (error2 instanceof Error) {
|
|
583
|
-
return chalk2.red(`Error: ${error2.message}`);
|
|
584
|
-
}
|
|
585
|
-
return chalk2.red(`Error: ${String(error2)}`);
|
|
637
|
+
// src/lib/auth-flow.ts
|
|
638
|
+
var POLL_INTERVAL_MS = 2e3;
|
|
639
|
+
var MAX_POLL_ATTEMPTS = 150;
|
|
640
|
+
function sleep(ms) {
|
|
641
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
586
642
|
}
|
|
587
|
-
async function
|
|
588
|
-
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
643
|
+
async function performLogin(options) {
|
|
644
|
+
const api = createAPIClient();
|
|
645
|
+
const deviceName = `CLI - ${hostname()}`;
|
|
646
|
+
info("Starting authentication flow...");
|
|
647
|
+
const spinner = createSpinner("Generating authentication code...");
|
|
648
|
+
spinner.start();
|
|
649
|
+
const initResponse = await api.post("/api/cli/auth?action=initiate", { deviceName });
|
|
650
|
+
spinner.stop();
|
|
651
|
+
console.log();
|
|
652
|
+
console.log(chalk3.bold("Your authentication code:"));
|
|
653
|
+
console.log();
|
|
654
|
+
console.log(chalk3.cyan.bold(` ${initResponse.code}`));
|
|
655
|
+
console.log();
|
|
656
|
+
console.log(`Open this URL to authenticate:`);
|
|
657
|
+
console.log(chalk3.dim(initResponse.url));
|
|
658
|
+
console.log();
|
|
659
|
+
if (options?.browser !== false) {
|
|
660
|
+
info("Opening browser...");
|
|
661
|
+
await open(initResponse.url);
|
|
662
|
+
}
|
|
663
|
+
const pollSpinner = createSpinner("Waiting for authentication...");
|
|
664
|
+
pollSpinner.start();
|
|
665
|
+
for (let attempts = 0; attempts < MAX_POLL_ATTEMPTS; attempts++) {
|
|
666
|
+
await sleep(POLL_INTERVAL_MS);
|
|
667
|
+
const pollResponse = await api.get("/api/cli/auth", { action: "poll", code: initResponse.code });
|
|
668
|
+
if (pollResponse.status === "authenticated") {
|
|
669
|
+
pollSpinner.stop();
|
|
670
|
+
if (pollResponse.accessToken) {
|
|
671
|
+
setAccessToken(pollResponse.accessToken);
|
|
672
|
+
}
|
|
673
|
+
if (pollResponse.refreshToken) {
|
|
674
|
+
setRefreshToken(pollResponse.refreshToken);
|
|
675
|
+
}
|
|
676
|
+
if (pollResponse.user) {
|
|
677
|
+
setUser({
|
|
678
|
+
id: pollResponse.user.id,
|
|
679
|
+
email: pollResponse.user.email,
|
|
680
|
+
name: pollResponse.user.name
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
console.log();
|
|
684
|
+
success(`Logged in as ${chalk3.bold(pollResponse.user?.email)}`);
|
|
685
|
+
return { email: pollResponse.user?.email || "" };
|
|
597
686
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
await flushSentry();
|
|
602
|
-
if (error2 instanceof CLIError) {
|
|
603
|
-
switch (error2.code) {
|
|
604
|
-
case ErrorCodes.NOT_AUTHENTICATED:
|
|
605
|
-
process.exit(2);
|
|
606
|
-
case ErrorCodes.PERMISSION_DENIED:
|
|
607
|
-
process.exit(3);
|
|
608
|
-
case ErrorCodes.TIER_LIMIT_EXCEEDED:
|
|
609
|
-
process.exit(4);
|
|
610
|
-
default:
|
|
611
|
-
process.exit(1);
|
|
687
|
+
if (pollResponse.status === "expired" || pollResponse.status === "not_found") {
|
|
688
|
+
pollSpinner.stop();
|
|
689
|
+
throw new Error("Authentication code expired. Please try again.");
|
|
612
690
|
}
|
|
613
691
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
function notAuthenticated() {
|
|
617
|
-
return new CLIError(
|
|
618
|
-
"You are not authenticated.",
|
|
619
|
-
ErrorCodes.NOT_AUTHENTICATED,
|
|
620
|
-
"Run `envpilot login` to authenticate."
|
|
621
|
-
);
|
|
622
|
-
}
|
|
623
|
-
function notInitialized() {
|
|
624
|
-
return new CLIError(
|
|
625
|
-
"This directory is not initialized with Envpilot.",
|
|
626
|
-
ErrorCodes.NOT_INITIALIZED,
|
|
627
|
-
"Run `envpilot init` to initialize."
|
|
628
|
-
);
|
|
629
|
-
}
|
|
630
|
-
function fileNotFound(path) {
|
|
631
|
-
return new CLIError(`File not found: ${path}`, ErrorCodes.FILE_NOT_FOUND);
|
|
692
|
+
pollSpinner.stop();
|
|
693
|
+
throw new Error("Authentication timed out. Please try again.");
|
|
632
694
|
}
|
|
633
695
|
|
|
634
696
|
// src/commands/login.ts
|
|
635
|
-
import { hostname } from "os";
|
|
636
|
-
var POLL_INTERVAL_MS = 2e3;
|
|
637
|
-
var MAX_POLL_ATTEMPTS = 150;
|
|
638
697
|
var loginCommand = new Command("login").description("Authenticate with Envpilot").option("--api-url <url>", "API URL (default: http://localhost:3000)").option("--no-browser", "Do not automatically open the browser").action(async (options) => {
|
|
639
698
|
try {
|
|
640
699
|
if (options.apiUrl) {
|
|
641
700
|
setApiUrl(options.apiUrl);
|
|
642
701
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
const spinner = createSpinner("Generating authentication code...");
|
|
647
|
-
spinner.start();
|
|
648
|
-
const initResponse = await api.post("/api/cli/auth?action=initiate", { deviceName });
|
|
649
|
-
spinner.stop();
|
|
650
|
-
console.log();
|
|
651
|
-
console.log(chalk3.bold("Your authentication code:"));
|
|
652
|
-
console.log();
|
|
653
|
-
console.log(chalk3.cyan.bold(` ${initResponse.code}`));
|
|
702
|
+
await performLogin({
|
|
703
|
+
browser: options.browser !== false
|
|
704
|
+
});
|
|
654
705
|
console.log();
|
|
655
|
-
console.log(
|
|
656
|
-
|
|
706
|
+
console.log("Next steps:");
|
|
707
|
+
info(" envpilot init Initialize a project in the current directory");
|
|
708
|
+
info(" envpilot list List your projects and organizations");
|
|
709
|
+
info(" envpilot sync Login, select project, and pull \u2014 all at once");
|
|
657
710
|
console.log();
|
|
658
|
-
if (options.browser !== false) {
|
|
659
|
-
info("Opening browser...");
|
|
660
|
-
await open(initResponse.url);
|
|
661
|
-
}
|
|
662
|
-
const pollSpinner = createSpinner("Waiting for authentication...");
|
|
663
|
-
pollSpinner.start();
|
|
664
|
-
let authenticated = false;
|
|
665
|
-
let attempts = 0;
|
|
666
|
-
while (!authenticated && attempts < MAX_POLL_ATTEMPTS) {
|
|
667
|
-
await sleep(POLL_INTERVAL_MS);
|
|
668
|
-
const pollResponse = await api.get("/api/cli/auth", { action: "poll", code: initResponse.code });
|
|
669
|
-
if (pollResponse.status === "authenticated") {
|
|
670
|
-
pollSpinner.stop();
|
|
671
|
-
if (pollResponse.accessToken) {
|
|
672
|
-
setAccessToken(pollResponse.accessToken);
|
|
673
|
-
}
|
|
674
|
-
if (pollResponse.refreshToken) {
|
|
675
|
-
setRefreshToken(pollResponse.refreshToken);
|
|
676
|
-
}
|
|
677
|
-
if (pollResponse.user) {
|
|
678
|
-
setUser({
|
|
679
|
-
id: pollResponse.user.id,
|
|
680
|
-
email: pollResponse.user.email,
|
|
681
|
-
name: pollResponse.user.name
|
|
682
|
-
});
|
|
683
|
-
}
|
|
684
|
-
authenticated = true;
|
|
685
|
-
console.log();
|
|
686
|
-
success(`Logged in as ${chalk3.bold(pollResponse.user?.email)}`);
|
|
687
|
-
console.log();
|
|
688
|
-
console.log("Next steps:");
|
|
689
|
-
console.log(
|
|
690
|
-
` ${chalk3.cyan("envpilot init")} Initialize a project in the current directory`
|
|
691
|
-
);
|
|
692
|
-
console.log(
|
|
693
|
-
` ${chalk3.cyan("envpilot list")} List your projects and organizations`
|
|
694
|
-
);
|
|
695
|
-
console.log();
|
|
696
|
-
break;
|
|
697
|
-
}
|
|
698
|
-
if (pollResponse.status === "expired" || pollResponse.status === "not_found") {
|
|
699
|
-
pollSpinner.stop();
|
|
700
|
-
error("Authentication code expired. Please try again.");
|
|
701
|
-
process.exit(1);
|
|
702
|
-
}
|
|
703
|
-
attempts++;
|
|
704
|
-
}
|
|
705
|
-
if (!authenticated) {
|
|
706
|
-
pollSpinner.stop();
|
|
707
|
-
error("Authentication timed out. Please try again.");
|
|
708
|
-
process.exit(1);
|
|
709
|
-
}
|
|
710
711
|
} catch (err) {
|
|
711
712
|
await handleError(err);
|
|
712
713
|
}
|
|
713
714
|
});
|
|
714
|
-
function sleep(ms) {
|
|
715
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
716
|
-
}
|
|
717
715
|
|
|
718
716
|
// src/commands/init.ts
|
|
719
717
|
import { Command as Command2 } from "commander";
|
|
@@ -779,6 +777,18 @@ var projectConfigSchema = z.object({
|
|
|
779
777
|
organizationId: z.string(),
|
|
780
778
|
environment: environmentSchema.default("development")
|
|
781
779
|
});
|
|
780
|
+
var projectEntrySchema = z.object({
|
|
781
|
+
projectId: z.string(),
|
|
782
|
+
organizationId: z.string(),
|
|
783
|
+
projectName: z.string().default(""),
|
|
784
|
+
organizationName: z.string().default(""),
|
|
785
|
+
environment: environmentSchema.default("development")
|
|
786
|
+
});
|
|
787
|
+
var projectConfigV2Schema = z.object({
|
|
788
|
+
version: z.literal(1),
|
|
789
|
+
activeProjectId: z.string(),
|
|
790
|
+
projects: z.array(projectEntrySchema).min(1)
|
|
791
|
+
});
|
|
782
792
|
|
|
783
793
|
// src/lib/project-config.ts
|
|
784
794
|
var CONFIG_FILE_NAME = ".envpilot";
|
|
@@ -788,51 +798,173 @@ function getProjectConfigPath(directory = process.cwd()) {
|
|
|
788
798
|
function hasProjectConfig(directory = process.cwd()) {
|
|
789
799
|
return existsSync(getProjectConfigPath(directory));
|
|
790
800
|
}
|
|
791
|
-
function
|
|
801
|
+
function readRawConfig(directory = process.cwd()) {
|
|
792
802
|
const configPath = getProjectConfigPath(directory);
|
|
793
|
-
if (!existsSync(configPath))
|
|
803
|
+
if (!existsSync(configPath)) return null;
|
|
804
|
+
try {
|
|
805
|
+
return JSON.parse(readFileSync(configPath, "utf-8"));
|
|
806
|
+
} catch {
|
|
794
807
|
return null;
|
|
795
808
|
}
|
|
809
|
+
}
|
|
810
|
+
function migrateV1toV2(v1) {
|
|
811
|
+
return {
|
|
812
|
+
version: 1,
|
|
813
|
+
activeProjectId: v1.projectId,
|
|
814
|
+
projects: [
|
|
815
|
+
{
|
|
816
|
+
projectId: v1.projectId,
|
|
817
|
+
organizationId: v1.organizationId,
|
|
818
|
+
projectName: "",
|
|
819
|
+
organizationName: "",
|
|
820
|
+
environment: v1.environment
|
|
821
|
+
}
|
|
822
|
+
]
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
function readProjectConfigV2(directory = process.cwd()) {
|
|
826
|
+
const raw = readRawConfig(directory);
|
|
827
|
+
if (!raw || typeof raw !== "object") return null;
|
|
828
|
+
const rawVersion = raw.version;
|
|
829
|
+
if (rawVersion === 1 || rawVersion === 2) {
|
|
830
|
+
try {
|
|
831
|
+
const normalized = { ...raw, version: 1 };
|
|
832
|
+
const parsed = projectConfigV2Schema.parse(normalized);
|
|
833
|
+
if (rawVersion === 2) {
|
|
834
|
+
writeProjectConfigV2(parsed, directory);
|
|
835
|
+
}
|
|
836
|
+
return parsed;
|
|
837
|
+
} catch {
|
|
838
|
+
return null;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
796
841
|
try {
|
|
797
|
-
const
|
|
798
|
-
const
|
|
799
|
-
|
|
842
|
+
const v1 = projectConfigSchema.parse(raw);
|
|
843
|
+
const v2 = migrateV1toV2(v1);
|
|
844
|
+
writeProjectConfigV2(v2, directory);
|
|
845
|
+
return v2;
|
|
800
846
|
} catch {
|
|
801
847
|
return null;
|
|
802
848
|
}
|
|
803
849
|
}
|
|
804
|
-
function
|
|
850
|
+
function writeProjectConfigV2(config2, directory = process.cwd()) {
|
|
805
851
|
const configPath = getProjectConfigPath(directory);
|
|
806
|
-
|
|
807
|
-
writeFileSync(configPath, content, "utf-8");
|
|
852
|
+
writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
|
|
808
853
|
}
|
|
809
|
-
function
|
|
810
|
-
|
|
811
|
-
if (!existing) {
|
|
812
|
-
throw new Error("No project config found. Run `envpilot init` first.");
|
|
813
|
-
}
|
|
814
|
-
const updated = { ...existing, ...updates };
|
|
815
|
-
writeProjectConfig(updated, directory);
|
|
854
|
+
function getActiveProject(config2) {
|
|
855
|
+
return config2.projects.find((p) => p.projectId === config2.activeProjectId) || config2.projects[0] || null;
|
|
816
856
|
}
|
|
817
|
-
function
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
857
|
+
function resolveProject(config2, identifier) {
|
|
858
|
+
if (!identifier) return getActiveProject(config2);
|
|
859
|
+
return config2.projects.find(
|
|
860
|
+
(p) => p.projectId === identifier || p.projectName.toLowerCase() === identifier.toLowerCase()
|
|
861
|
+
) || null;
|
|
862
|
+
}
|
|
863
|
+
function addProjectToConfig(config2, entry) {
|
|
864
|
+
if (config2.projects.some((p) => p.projectId === entry.projectId)) {
|
|
865
|
+
throw new Error("Project already linked");
|
|
822
866
|
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
867
|
+
return { ...config2, projects: [...config2.projects, entry] };
|
|
868
|
+
}
|
|
869
|
+
function removeProjectFromConfig(config2, projectId) {
|
|
870
|
+
const filtered = config2.projects.filter((p) => p.projectId !== projectId);
|
|
871
|
+
if (filtered.length === 0) return null;
|
|
872
|
+
const activeId = config2.activeProjectId === projectId ? filtered[0].projectId : config2.activeProjectId;
|
|
873
|
+
return { ...config2, activeProjectId: activeId, projects: filtered };
|
|
874
|
+
}
|
|
875
|
+
function setActiveProjectInConfig(config2, projectId) {
|
|
876
|
+
if (!config2.projects.some((p) => p.projectId === projectId)) {
|
|
877
|
+
throw new Error("Project not found in config");
|
|
827
878
|
}
|
|
828
|
-
|
|
829
|
-
writeFileSync(gitignorePath, newContent, "utf-8");
|
|
879
|
+
return { ...config2, activeProjectId: projectId };
|
|
830
880
|
}
|
|
831
|
-
function
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
881
|
+
function updateProjectInConfig(config2, projectId, updates) {
|
|
882
|
+
return {
|
|
883
|
+
...config2,
|
|
884
|
+
projects: config2.projects.map(
|
|
885
|
+
(p) => p.projectId === projectId ? { ...p, ...updates } : p
|
|
886
|
+
)
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
function readProjectConfig(directory = process.cwd()) {
|
|
890
|
+
const v2 = readProjectConfigV2(directory);
|
|
891
|
+
if (!v2) return null;
|
|
892
|
+
const active = getActiveProject(v2);
|
|
893
|
+
if (!active) return null;
|
|
894
|
+
return {
|
|
895
|
+
projectId: active.projectId,
|
|
896
|
+
organizationId: active.organizationId,
|
|
897
|
+
environment: active.environment
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
function writeProjectConfig(config2, directory = process.cwd()) {
|
|
901
|
+
const existing = readProjectConfigV2(directory);
|
|
902
|
+
if (existing) {
|
|
903
|
+
const updated = updateProjectInConfig(existing, existing.activeProjectId, {
|
|
904
|
+
projectId: config2.projectId,
|
|
905
|
+
organizationId: config2.organizationId,
|
|
906
|
+
environment: config2.environment
|
|
907
|
+
});
|
|
908
|
+
writeProjectConfigV2(
|
|
909
|
+
{ ...updated, activeProjectId: config2.projectId },
|
|
910
|
+
directory
|
|
911
|
+
);
|
|
912
|
+
} else {
|
|
913
|
+
writeProjectConfigV2(
|
|
914
|
+
{
|
|
915
|
+
version: 1,
|
|
916
|
+
activeProjectId: config2.projectId,
|
|
917
|
+
projects: [
|
|
918
|
+
{
|
|
919
|
+
projectId: config2.projectId,
|
|
920
|
+
organizationId: config2.organizationId,
|
|
921
|
+
projectName: "",
|
|
922
|
+
organizationName: "",
|
|
923
|
+
environment: config2.environment
|
|
924
|
+
}
|
|
925
|
+
]
|
|
926
|
+
},
|
|
927
|
+
directory
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
function updateProjectConfig(updates, directory = process.cwd()) {
|
|
932
|
+
const v2 = readProjectConfigV2(directory);
|
|
933
|
+
if (!v2) {
|
|
934
|
+
throw new Error("No project config found. Run `envpilot init` first.");
|
|
935
|
+
}
|
|
936
|
+
const active = getActiveProject(v2);
|
|
937
|
+
if (!active) {
|
|
938
|
+
throw new Error("No active project found.");
|
|
939
|
+
}
|
|
940
|
+
const updated = updateProjectInConfig(v2, active.projectId, updates);
|
|
941
|
+
writeProjectConfigV2(updated, directory);
|
|
942
|
+
}
|
|
943
|
+
function deleteProjectConfig(directory = process.cwd()) {
|
|
944
|
+
const configPath = getProjectConfigPath(directory);
|
|
945
|
+
if (!existsSync(configPath)) return false;
|
|
946
|
+
unlinkSync(configPath);
|
|
947
|
+
return true;
|
|
948
|
+
}
|
|
949
|
+
function ensureEnvInGitignore(directory = process.cwd()) {
|
|
950
|
+
const gitignorePath = join(directory, ".gitignore");
|
|
951
|
+
if (!existsSync(gitignorePath)) {
|
|
952
|
+
writeFileSync(gitignorePath, ".env\n.env.local\n", "utf-8");
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
const content = readFileSync(gitignorePath, "utf-8");
|
|
956
|
+
const lines = content.split("\n");
|
|
957
|
+
if (lines.some((line) => line.trim() === ".env")) {
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
const newContent = content.endsWith("\n") ? content + ".env\n" : content + "\n.env\n";
|
|
961
|
+
writeFileSync(gitignorePath, newContent, "utf-8");
|
|
962
|
+
}
|
|
963
|
+
function getTrackedEnvFiles(directory = process.cwd()) {
|
|
964
|
+
try {
|
|
965
|
+
const result = execSync("git ls-files --cached .env .env.* .env.local", {
|
|
966
|
+
cwd: directory,
|
|
967
|
+
encoding: "utf-8",
|
|
836
968
|
stdio: ["pipe", "pipe", "pipe"]
|
|
837
969
|
});
|
|
838
970
|
return result.trim().split("\n").filter((f) => f.length > 0);
|
|
@@ -841,199 +973,8 @@ function getTrackedEnvFiles(directory = process.cwd()) {
|
|
|
841
973
|
}
|
|
842
974
|
}
|
|
843
975
|
|
|
844
|
-
// src/commands/init.ts
|
|
845
|
-
var initCommand = new Command2("init").description("Initialize Envpilot in the current directory").option("-o, --organization <id>", "Organization ID").option("-p, --project <id>", "Project ID").option(
|
|
846
|
-
"-e, --environment <env>",
|
|
847
|
-
"Default environment (development, staging, production)"
|
|
848
|
-
).option("-f, --force", "Overwrite existing configuration").action(async (options) => {
|
|
849
|
-
try {
|
|
850
|
-
if (!isAuthenticated()) {
|
|
851
|
-
throw notAuthenticated();
|
|
852
|
-
}
|
|
853
|
-
if (hasProjectConfig() && !options.force) {
|
|
854
|
-
warning("This directory is already initialized with Envpilot.");
|
|
855
|
-
const { proceed } = await inquirer.prompt([
|
|
856
|
-
{
|
|
857
|
-
type: "confirm",
|
|
858
|
-
name: "proceed",
|
|
859
|
-
message: "Do you want to reinitialize?",
|
|
860
|
-
default: false
|
|
861
|
-
}
|
|
862
|
-
]);
|
|
863
|
-
if (!proceed) {
|
|
864
|
-
info("Initialization cancelled.");
|
|
865
|
-
return;
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
const api = createAPIClient();
|
|
869
|
-
const organizations = await withSpinner(
|
|
870
|
-
"Fetching organizations...",
|
|
871
|
-
async () => {
|
|
872
|
-
const response = await api.get("/api/cli/organizations");
|
|
873
|
-
return response.data || [];
|
|
874
|
-
}
|
|
875
|
-
);
|
|
876
|
-
if (organizations.length === 0) {
|
|
877
|
-
error("No organizations found. Please create an organization first.");
|
|
878
|
-
process.exit(1);
|
|
879
|
-
}
|
|
880
|
-
let selectedOrg;
|
|
881
|
-
if (options.organization) {
|
|
882
|
-
const org = organizations.find(
|
|
883
|
-
(o) => o._id === options.organization || o.slug === options.organization
|
|
884
|
-
);
|
|
885
|
-
if (!org) {
|
|
886
|
-
error(`Organization not found: ${options.organization}`);
|
|
887
|
-
process.exit(1);
|
|
888
|
-
}
|
|
889
|
-
selectedOrg = org;
|
|
890
|
-
} else if (organizations.length === 1) {
|
|
891
|
-
selectedOrg = organizations[0];
|
|
892
|
-
info(`Using organization: ${chalk4.bold(selectedOrg.name)}`);
|
|
893
|
-
} else {
|
|
894
|
-
const { orgId } = await inquirer.prompt([
|
|
895
|
-
{
|
|
896
|
-
type: "list",
|
|
897
|
-
name: "orgId",
|
|
898
|
-
message: "Select an organization:",
|
|
899
|
-
choices: organizations.map((org) => ({
|
|
900
|
-
name: `${org.name} ${org.tier === "pro" ? chalk4.green("(Pro)") : chalk4.dim("(Free)")}`,
|
|
901
|
-
value: org._id
|
|
902
|
-
}))
|
|
903
|
-
}
|
|
904
|
-
]);
|
|
905
|
-
selectedOrg = organizations.find((o) => o._id === orgId);
|
|
906
|
-
}
|
|
907
|
-
const projects = await withSpinner("Fetching projects...", async () => {
|
|
908
|
-
const response = await api.get(
|
|
909
|
-
"/api/cli/projects",
|
|
910
|
-
{ organizationId: selectedOrg._id }
|
|
911
|
-
);
|
|
912
|
-
return response.data || [];
|
|
913
|
-
});
|
|
914
|
-
if (projects.length === 0) {
|
|
915
|
-
error("No projects found. Please create a project first.");
|
|
916
|
-
process.exit(1);
|
|
917
|
-
}
|
|
918
|
-
let selectedProject;
|
|
919
|
-
if (options.project) {
|
|
920
|
-
const project = projects.find(
|
|
921
|
-
(p) => p._id === options.project || p.slug === options.project
|
|
922
|
-
);
|
|
923
|
-
if (!project) {
|
|
924
|
-
error(`Project not found: ${options.project}`);
|
|
925
|
-
process.exit(1);
|
|
926
|
-
}
|
|
927
|
-
selectedProject = project;
|
|
928
|
-
} else if (projects.length === 1) {
|
|
929
|
-
selectedProject = projects[0];
|
|
930
|
-
info(`Using project: ${chalk4.bold(selectedProject.name)}`);
|
|
931
|
-
} else {
|
|
932
|
-
const { projectId } = await inquirer.prompt([
|
|
933
|
-
{
|
|
934
|
-
type: "list",
|
|
935
|
-
name: "projectId",
|
|
936
|
-
message: "Select a project:",
|
|
937
|
-
choices: projects.map((project) => ({
|
|
938
|
-
name: `${project.icon || "\u{1F4E6}"} ${project.name}`,
|
|
939
|
-
value: project._id
|
|
940
|
-
}))
|
|
941
|
-
}
|
|
942
|
-
]);
|
|
943
|
-
selectedProject = projects.find((p) => p._id === projectId);
|
|
944
|
-
}
|
|
945
|
-
let selectedEnvironment = "development";
|
|
946
|
-
if (options.environment) {
|
|
947
|
-
if (!["development", "staging", "production"].includes(
|
|
948
|
-
options.environment
|
|
949
|
-
)) {
|
|
950
|
-
error(
|
|
951
|
-
"Invalid environment. Must be: development, staging, or production"
|
|
952
|
-
);
|
|
953
|
-
process.exit(1);
|
|
954
|
-
}
|
|
955
|
-
selectedEnvironment = options.environment;
|
|
956
|
-
} else {
|
|
957
|
-
const { environment } = await inquirer.prompt([
|
|
958
|
-
{
|
|
959
|
-
type: "list",
|
|
960
|
-
name: "environment",
|
|
961
|
-
message: "Select default environment:",
|
|
962
|
-
choices: [
|
|
963
|
-
{ name: "Development", value: "development" },
|
|
964
|
-
{ name: "Staging", value: "staging" },
|
|
965
|
-
{ name: "Production", value: "production" }
|
|
966
|
-
],
|
|
967
|
-
default: "development"
|
|
968
|
-
}
|
|
969
|
-
]);
|
|
970
|
-
selectedEnvironment = environment;
|
|
971
|
-
}
|
|
972
|
-
writeProjectConfig({
|
|
973
|
-
projectId: selectedProject._id,
|
|
974
|
-
organizationId: selectedOrg._id,
|
|
975
|
-
environment: selectedEnvironment
|
|
976
|
-
});
|
|
977
|
-
setActiveOrganizationId(selectedOrg._id);
|
|
978
|
-
setActiveProjectId(selectedProject._id);
|
|
979
|
-
if (selectedOrg.role) {
|
|
980
|
-
setRole(selectedOrg.role);
|
|
981
|
-
}
|
|
982
|
-
ensureEnvInGitignore();
|
|
983
|
-
const trackedFiles = getTrackedEnvFiles();
|
|
984
|
-
if (trackedFiles.length > 0) {
|
|
985
|
-
console.log();
|
|
986
|
-
warning("Security risk: .env files are tracked by git!");
|
|
987
|
-
for (const file of trackedFiles) {
|
|
988
|
-
console.log(chalk4.red(` tracked: ${file}`));
|
|
989
|
-
}
|
|
990
|
-
console.log();
|
|
991
|
-
console.log(
|
|
992
|
-
chalk4.yellow(
|
|
993
|
-
" Run the following to untrack them (without deleting the files):"
|
|
994
|
-
)
|
|
995
|
-
);
|
|
996
|
-
for (const file of trackedFiles) {
|
|
997
|
-
console.log(chalk4.cyan(` git rm --cached ${file}`));
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
console.log();
|
|
1001
|
-
success("Project initialized!");
|
|
1002
|
-
console.log();
|
|
1003
|
-
console.log(chalk4.dim("Configuration saved to .envpilot"));
|
|
1004
|
-
if (selectedOrg.role) {
|
|
1005
|
-
console.log(chalk4.dim(` Org role: ${formatRole(selectedOrg.role)}`));
|
|
1006
|
-
roleNotice(selectedOrg.role);
|
|
1007
|
-
}
|
|
1008
|
-
if (selectedProject.projectRole) {
|
|
1009
|
-
console.log(
|
|
1010
|
-
chalk4.dim(
|
|
1011
|
-
` Project role: ${formatProjectRole(selectedProject.projectRole)}`
|
|
1012
|
-
)
|
|
1013
|
-
);
|
|
1014
|
-
projectRoleNotice(selectedProject.projectRole);
|
|
1015
|
-
}
|
|
1016
|
-
console.log();
|
|
1017
|
-
console.log("Next steps:");
|
|
1018
|
-
console.log(
|
|
1019
|
-
` ${chalk4.cyan("envpilot pull")} Download environment variables`
|
|
1020
|
-
);
|
|
1021
|
-
console.log(
|
|
1022
|
-
` ${chalk4.cyan("envpilot push")} Upload local .env to cloud`
|
|
1023
|
-
);
|
|
1024
|
-
console.log();
|
|
1025
|
-
} catch (err) {
|
|
1026
|
-
await handleError(err);
|
|
1027
|
-
}
|
|
1028
|
-
});
|
|
1029
|
-
|
|
1030
|
-
// src/commands/pull.ts
|
|
1031
|
-
import { Command as Command3 } from "commander";
|
|
1032
|
-
import chalk5 from "chalk";
|
|
1033
|
-
import inquirer2 from "inquirer";
|
|
1034
|
-
|
|
1035
976
|
// src/lib/env-file.ts
|
|
1036
|
-
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
|
|
977
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, chmodSync } from "fs";
|
|
1037
978
|
import { join as join2 } from "path";
|
|
1038
979
|
function parseEnvFile(content) {
|
|
1039
980
|
const result = {};
|
|
@@ -1132,136 +1073,543 @@ function writeEnvFile(filePath, vars, options) {
|
|
|
1132
1073
|
}
|
|
1133
1074
|
function getEnvPathForEnvironment(environment, directory = process.cwd()) {
|
|
1134
1075
|
if (environment === "development") {
|
|
1135
|
-
return join2(directory, ".env");
|
|
1076
|
+
return join2(directory, ".env.local");
|
|
1136
1077
|
}
|
|
1137
1078
|
return join2(directory, `.env.${environment}`);
|
|
1138
1079
|
}
|
|
1080
|
+
function applyFileProtection(filePath, role, projectRole) {
|
|
1081
|
+
if (!existsSync2(filePath)) return;
|
|
1082
|
+
const isWritable = role === "admin" || role === "team_lead" || projectRole === "manager";
|
|
1083
|
+
if (isWritable) {
|
|
1084
|
+
chmodSync(filePath, 420);
|
|
1085
|
+
} else {
|
|
1086
|
+
chmodSync(filePath, 292);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1139
1089
|
|
|
1140
|
-
// src/commands/
|
|
1141
|
-
var
|
|
1142
|
-
"-e, --
|
|
1143
|
-
"
|
|
1144
|
-
).option("-f, --
|
|
1090
|
+
// src/commands/init.ts
|
|
1091
|
+
var initCommand = new Command2("init").description("Initialize Envpilot in the current directory").option("-o, --organization <id>", "Organization ID").option("-p, --project <id>", "Project ID").option(
|
|
1092
|
+
"-e, --environment <env>",
|
|
1093
|
+
"Default environment (development, staging, production)"
|
|
1094
|
+
).option("-f, --force", "Overwrite existing configuration").option("--add", "Add another project to existing config").action(async (options) => {
|
|
1145
1095
|
try {
|
|
1146
1096
|
if (!isAuthenticated()) {
|
|
1147
1097
|
throw notAuthenticated();
|
|
1148
1098
|
}
|
|
1149
|
-
const
|
|
1150
|
-
if (
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
if (trackedFiles.length > 0) {
|
|
1155
|
-
error("Security risk: .env files are tracked by git!");
|
|
1156
|
-
console.log();
|
|
1157
|
-
for (const file of trackedFiles) {
|
|
1158
|
-
console.log(chalk5.red(` tracked: ${file}`));
|
|
1099
|
+
const existingConfig = readProjectConfigV2();
|
|
1100
|
+
if (options.add) {
|
|
1101
|
+
if (!existingConfig) {
|
|
1102
|
+
error("No existing config. Run `envpilot init` first.");
|
|
1103
|
+
process.exit(1);
|
|
1159
1104
|
}
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1105
|
+
await addProject(existingConfig, options);
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
if (hasProjectConfig() && !options.force) {
|
|
1109
|
+
warning("This directory is already initialized with Envpilot.");
|
|
1110
|
+
const { proceed } = await inquirer.prompt([
|
|
1111
|
+
{
|
|
1112
|
+
type: "confirm",
|
|
1113
|
+
name: "proceed",
|
|
1114
|
+
message: "Do you want to reinitialize?",
|
|
1115
|
+
default: false
|
|
1116
|
+
}
|
|
1117
|
+
]);
|
|
1118
|
+
if (!proceed) {
|
|
1119
|
+
info("Initialization cancelled.");
|
|
1120
|
+
return;
|
|
1168
1121
|
}
|
|
1169
|
-
|
|
1122
|
+
}
|
|
1123
|
+
const { selectedOrg, selectedProject, selectedEnvironment } = await selectOrgProjectEnv(options);
|
|
1124
|
+
writeProjectConfigV2({
|
|
1125
|
+
version: 1,
|
|
1126
|
+
activeProjectId: selectedProject._id,
|
|
1127
|
+
projects: [
|
|
1128
|
+
{
|
|
1129
|
+
projectId: selectedProject._id,
|
|
1130
|
+
organizationId: selectedOrg._id,
|
|
1131
|
+
projectName: selectedProject.name,
|
|
1132
|
+
organizationName: selectedOrg.name,
|
|
1133
|
+
environment: selectedEnvironment
|
|
1134
|
+
}
|
|
1135
|
+
]
|
|
1136
|
+
});
|
|
1137
|
+
setActiveOrganizationId(selectedOrg._id);
|
|
1138
|
+
setActiveProjectId(selectedProject._id);
|
|
1139
|
+
if (selectedOrg.role) {
|
|
1140
|
+
setRole(selectedOrg.role);
|
|
1141
|
+
}
|
|
1142
|
+
ensureEnvInGitignore();
|
|
1143
|
+
warnTrackedFiles();
|
|
1144
|
+
console.log();
|
|
1145
|
+
success("Project initialized!");
|
|
1146
|
+
printPostInit(selectedOrg, selectedProject);
|
|
1147
|
+
} catch (err) {
|
|
1148
|
+
await handleError(err);
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
async function addProject(existingConfig, options) {
|
|
1152
|
+
const api = createAPIClient();
|
|
1153
|
+
let role = getRole();
|
|
1154
|
+
if (role !== "admin" && role !== "team_lead") {
|
|
1155
|
+
const orgs = await withSpinner("Checking permissions...", async () => {
|
|
1156
|
+
const response = await api.get("/api/cli/organizations");
|
|
1157
|
+
return response.data || [];
|
|
1158
|
+
});
|
|
1159
|
+
const freshRole = orgs.find(
|
|
1160
|
+
(o) => o._id === existingConfig.projects[0]?.organizationId
|
|
1161
|
+
)?.role;
|
|
1162
|
+
if (freshRole) {
|
|
1163
|
+
setRole(freshRole);
|
|
1164
|
+
role = freshRole;
|
|
1165
|
+
}
|
|
1166
|
+
if (role !== "admin" && role !== "team_lead") {
|
|
1167
|
+
error("Only admins and team leads can link multiple projects.");
|
|
1168
|
+
info("Unlink the current project first with `envpilot unlink`.");
|
|
1170
1169
|
process.exit(1);
|
|
1171
1170
|
}
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
});
|
|
1186
|
-
metaProjectRole = response.meta?.projectRole;
|
|
1187
|
-
return response.data || [];
|
|
1188
|
-
}
|
|
1171
|
+
}
|
|
1172
|
+
const { selectedOrg, selectedProject, selectedEnvironment } = await selectOrgProjectEnv(options);
|
|
1173
|
+
if (existingConfig.projects.some((p) => p.projectId === selectedProject._id)) {
|
|
1174
|
+
error(`"${selectedProject.name}" is already linked.`);
|
|
1175
|
+
process.exit(1);
|
|
1176
|
+
}
|
|
1177
|
+
const envFile = getEnvPathForEnvironment(selectedEnvironment);
|
|
1178
|
+
const conflicting = existingConfig.projects.find(
|
|
1179
|
+
(p) => p.environment === selectedEnvironment
|
|
1180
|
+
);
|
|
1181
|
+
if (conflicting) {
|
|
1182
|
+
warning(
|
|
1183
|
+
`"${conflicting.projectName || conflicting.projectId}" already syncs to ${envFile} (${selectedEnvironment}). Both projects will write to the same file \u2014 consider using a different environment.`
|
|
1189
1184
|
);
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1185
|
+
}
|
|
1186
|
+
const newEntry = {
|
|
1187
|
+
projectId: selectedProject._id,
|
|
1188
|
+
organizationId: selectedOrg._id,
|
|
1189
|
+
projectName: selectedProject.name,
|
|
1190
|
+
organizationName: selectedOrg.name,
|
|
1191
|
+
environment: selectedEnvironment
|
|
1192
|
+
};
|
|
1193
|
+
let updatedConfig = addProjectToConfig(existingConfig, newEntry);
|
|
1194
|
+
const { setActive } = await inquirer.prompt([
|
|
1195
|
+
{
|
|
1196
|
+
type: "confirm",
|
|
1197
|
+
name: "setActive",
|
|
1198
|
+
message: `Set "${selectedProject.name}" as the active project?`,
|
|
1199
|
+
default: false
|
|
1193
1200
|
}
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1201
|
+
]);
|
|
1202
|
+
if (setActive) {
|
|
1203
|
+
updatedConfig = {
|
|
1204
|
+
...updatedConfig,
|
|
1205
|
+
activeProjectId: selectedProject._id
|
|
1206
|
+
};
|
|
1207
|
+
setActiveProjectId(selectedProject._id);
|
|
1208
|
+
setActiveOrganizationId(selectedOrg._id);
|
|
1209
|
+
}
|
|
1210
|
+
updatedConfig = backfillNames(updatedConfig);
|
|
1211
|
+
writeProjectConfigV2(updatedConfig);
|
|
1212
|
+
console.log();
|
|
1213
|
+
success(`Added "${selectedProject.name}" to linked projects!`);
|
|
1214
|
+
console.log(
|
|
1215
|
+
chalk4.dim(` ${existingConfig.projects.length + 1} projects now linked`)
|
|
1216
|
+
);
|
|
1217
|
+
console.log();
|
|
1218
|
+
console.log("Next steps:");
|
|
1219
|
+
console.log(
|
|
1220
|
+
` ${chalk4.cyan("envpilot pull --all")} Pull all projects`
|
|
1221
|
+
);
|
|
1222
|
+
console.log(
|
|
1223
|
+
` ${chalk4.cyan(`envpilot pull --project "${selectedProject.name}"`)} Pull this project`
|
|
1224
|
+
);
|
|
1225
|
+
console.log(
|
|
1226
|
+
` ${chalk4.cyan("envpilot list linked")} See all linked projects`
|
|
1227
|
+
);
|
|
1228
|
+
console.log();
|
|
1229
|
+
}
|
|
1230
|
+
async function selectOrgProjectEnv(options) {
|
|
1231
|
+
const api = createAPIClient();
|
|
1232
|
+
const organizations = await withSpinner(
|
|
1233
|
+
"Fetching organizations...",
|
|
1234
|
+
async () => {
|
|
1235
|
+
const response = await api.get("/api/cli/organizations");
|
|
1236
|
+
return response.data || [];
|
|
1197
1237
|
}
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1238
|
+
);
|
|
1239
|
+
if (organizations.length === 0) {
|
|
1240
|
+
error("No organizations found. Please create an organization first.");
|
|
1241
|
+
process.exit(1);
|
|
1242
|
+
}
|
|
1243
|
+
let selectedOrg;
|
|
1244
|
+
if (options.organization) {
|
|
1245
|
+
const org = organizations.find(
|
|
1246
|
+
(o) => o._id === options.organization || o.slug === options.organization
|
|
1247
|
+
);
|
|
1248
|
+
if (!org) {
|
|
1249
|
+
error(`Organization not found: ${options.organization}`);
|
|
1250
|
+
process.exit(1);
|
|
1204
1251
|
}
|
|
1252
|
+
selectedOrg = org;
|
|
1253
|
+
} else if (organizations.length === 1) {
|
|
1254
|
+
selectedOrg = organizations[0];
|
|
1255
|
+
info(`Using organization: ${chalk4.bold(selectedOrg.name)}`);
|
|
1256
|
+
} else {
|
|
1257
|
+
const { orgId } = await inquirer.prompt([
|
|
1258
|
+
{
|
|
1259
|
+
type: "list",
|
|
1260
|
+
name: "orgId",
|
|
1261
|
+
message: "Select an organization:",
|
|
1262
|
+
choices: organizations.map((org) => ({
|
|
1263
|
+
name: `${org.name} ${org.tier === "pro" ? chalk4.green("(Pro)") : chalk4.dim("(Free)")}`,
|
|
1264
|
+
value: org._id
|
|
1265
|
+
}))
|
|
1266
|
+
}
|
|
1267
|
+
]);
|
|
1268
|
+
selectedOrg = organizations.find((o) => o._id === orgId);
|
|
1269
|
+
}
|
|
1270
|
+
const projects = await withSpinner("Fetching projects...", async () => {
|
|
1271
|
+
const response = await api.get(
|
|
1272
|
+
"/api/cli/projects",
|
|
1273
|
+
{ organizationId: selectedOrg._id }
|
|
1274
|
+
);
|
|
1275
|
+
return response.data || [];
|
|
1276
|
+
});
|
|
1277
|
+
if (projects.length === 0) {
|
|
1278
|
+
error("No projects found. Please create a project first.");
|
|
1279
|
+
process.exit(1);
|
|
1280
|
+
}
|
|
1281
|
+
let selectedProject;
|
|
1282
|
+
if (options.project) {
|
|
1283
|
+
const project = projects.find(
|
|
1284
|
+
(p) => p._id === options.project || p.slug === options.project
|
|
1285
|
+
);
|
|
1286
|
+
if (!project) {
|
|
1287
|
+
error(`Project not found: ${options.project}`);
|
|
1288
|
+
process.exit(1);
|
|
1289
|
+
}
|
|
1290
|
+
selectedProject = project;
|
|
1291
|
+
} else if (projects.length === 1) {
|
|
1292
|
+
selectedProject = projects[0];
|
|
1293
|
+
info(`Using project: ${chalk4.bold(selectedProject.name)}`);
|
|
1294
|
+
} else {
|
|
1295
|
+
const { projectId } = await inquirer.prompt([
|
|
1296
|
+
{
|
|
1297
|
+
type: "list",
|
|
1298
|
+
name: "projectId",
|
|
1299
|
+
message: "Select a project:",
|
|
1300
|
+
choices: projects.map((project) => ({
|
|
1301
|
+
name: `${project.icon || "\u{1F4E6}"} ${project.name}`,
|
|
1302
|
+
value: project._id
|
|
1303
|
+
}))
|
|
1304
|
+
}
|
|
1305
|
+
]);
|
|
1306
|
+
selectedProject = projects.find((p) => p._id === projectId);
|
|
1307
|
+
}
|
|
1308
|
+
let selectedEnvironment = "development";
|
|
1309
|
+
if (options.environment) {
|
|
1310
|
+
if (!["development", "staging", "production"].includes(options.environment)) {
|
|
1311
|
+
error(
|
|
1312
|
+
"Invalid environment. Must be: development, staging, or production"
|
|
1313
|
+
);
|
|
1314
|
+
process.exit(1);
|
|
1315
|
+
}
|
|
1316
|
+
selectedEnvironment = options.environment;
|
|
1317
|
+
} else {
|
|
1318
|
+
const { environment } = await inquirer.prompt([
|
|
1319
|
+
{
|
|
1320
|
+
type: "list",
|
|
1321
|
+
name: "environment",
|
|
1322
|
+
message: "Select default environment:",
|
|
1323
|
+
choices: [
|
|
1324
|
+
{ name: "Development", value: "development" },
|
|
1325
|
+
{ name: "Staging", value: "staging" },
|
|
1326
|
+
{ name: "Production", value: "production" }
|
|
1327
|
+
],
|
|
1328
|
+
default: "development"
|
|
1329
|
+
}
|
|
1330
|
+
]);
|
|
1331
|
+
selectedEnvironment = environment;
|
|
1332
|
+
}
|
|
1333
|
+
return { selectedOrg, selectedProject, selectedEnvironment };
|
|
1334
|
+
}
|
|
1335
|
+
function backfillNames(config2) {
|
|
1336
|
+
return config2;
|
|
1337
|
+
}
|
|
1338
|
+
function warnTrackedFiles() {
|
|
1339
|
+
const trackedFiles = getTrackedEnvFiles();
|
|
1340
|
+
if (trackedFiles.length > 0) {
|
|
1205
1341
|
console.log();
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1342
|
+
warning("Security risk: .env files are tracked by git!");
|
|
1343
|
+
for (const file of trackedFiles) {
|
|
1344
|
+
console.log(chalk4.red(` tracked: ${file}`));
|
|
1345
|
+
}
|
|
1209
1346
|
console.log();
|
|
1210
|
-
|
|
1211
|
-
|
|
1347
|
+
console.log(
|
|
1348
|
+
chalk4.yellow(
|
|
1349
|
+
" Run the following to untrack them (without deleting the files):"
|
|
1350
|
+
)
|
|
1351
|
+
);
|
|
1352
|
+
for (const file of trackedFiles) {
|
|
1353
|
+
console.log(chalk4.cyan(` git rm --cached ${file}`));
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
function printPostInit(selectedOrg, selectedProject) {
|
|
1358
|
+
console.log();
|
|
1359
|
+
console.log(chalk4.dim("Configuration saved to .envpilot"));
|
|
1360
|
+
if (selectedOrg.role) {
|
|
1361
|
+
console.log(chalk4.dim(` Org role: ${formatRole(selectedOrg.role)}`));
|
|
1362
|
+
roleNotice(selectedOrg.role);
|
|
1363
|
+
}
|
|
1364
|
+
if (selectedProject.projectRole) {
|
|
1365
|
+
console.log(
|
|
1366
|
+
chalk4.dim(
|
|
1367
|
+
` Project role: ${formatProjectRole(selectedProject.projectRole)}`
|
|
1368
|
+
)
|
|
1369
|
+
);
|
|
1370
|
+
projectRoleNotice(selectedProject.projectRole);
|
|
1371
|
+
}
|
|
1372
|
+
console.log();
|
|
1373
|
+
console.log("Next steps:");
|
|
1374
|
+
console.log(
|
|
1375
|
+
` ${chalk4.cyan("envpilot pull")} Download environment variables`
|
|
1376
|
+
);
|
|
1377
|
+
console.log(
|
|
1378
|
+
` ${chalk4.cyan("envpilot push")} Upload local .env to cloud`
|
|
1379
|
+
);
|
|
1380
|
+
console.log();
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// src/commands/pull.ts
|
|
1384
|
+
import { Command as Command3 } from "commander";
|
|
1385
|
+
import chalk5 from "chalk";
|
|
1386
|
+
import inquirer2 from "inquirer";
|
|
1387
|
+
var pullCommand = new Command3("pull").description("Download environment variables to local .env file").option(
|
|
1388
|
+
"-e, --env <environment>",
|
|
1389
|
+
"Environment (development, staging, production)"
|
|
1390
|
+
).option("-f, --file <path>", "Output file path (default: .env)").option("--force", "Overwrite without confirmation").option("--format <format>", "Output format: env, json", "env").option("--dry-run", "Show what would be downloaded without writing").option("--project <name-or-id>", "Pull a specific linked project").option("--all", "Pull all linked projects").action(async (options) => {
|
|
1391
|
+
try {
|
|
1392
|
+
if (!isAuthenticated()) {
|
|
1393
|
+
throw notAuthenticated();
|
|
1394
|
+
}
|
|
1395
|
+
if (options.all) {
|
|
1396
|
+
if (options.env || options.file) {
|
|
1397
|
+
error("Cannot use --env or --file with --all.");
|
|
1398
|
+
process.exit(1);
|
|
1399
|
+
}
|
|
1400
|
+
await pullAllProjects(options);
|
|
1212
1401
|
return;
|
|
1213
1402
|
}
|
|
1214
|
-
if (
|
|
1215
|
-
const
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1403
|
+
if (options.project) {
|
|
1404
|
+
const configV2 = readProjectConfigV2();
|
|
1405
|
+
if (!configV2) throw notInitialized();
|
|
1406
|
+
const project = resolveProject(configV2, options.project);
|
|
1407
|
+
if (!project) {
|
|
1408
|
+
error(`Project not found: ${options.project}`);
|
|
1409
|
+
console.log();
|
|
1410
|
+
console.log("Linked projects:");
|
|
1411
|
+
for (const p of configV2.projects) {
|
|
1412
|
+
console.log(` ${p.projectName || p.projectId} (${p.environment})`);
|
|
1221
1413
|
}
|
|
1222
|
-
|
|
1223
|
-
if (!proceed) {
|
|
1224
|
-
info("Pull cancelled.");
|
|
1225
|
-
return;
|
|
1414
|
+
process.exit(1);
|
|
1226
1415
|
}
|
|
1416
|
+
await pullSingleProject(project, options);
|
|
1417
|
+
return;
|
|
1227
1418
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1419
|
+
const projectConfig = readProjectConfig();
|
|
1420
|
+
if (!projectConfig) throw notInitialized();
|
|
1421
|
+
checkTrackedFiles();
|
|
1422
|
+
const environment = options.env || projectConfig.environment || "development";
|
|
1423
|
+
const outputPath = options.file || getEnvPathForEnvironment(environment);
|
|
1424
|
+
await pullProject(
|
|
1425
|
+
{
|
|
1426
|
+
projectId: projectConfig.projectId,
|
|
1427
|
+
organizationId: projectConfig.organizationId,
|
|
1428
|
+
environment
|
|
1429
|
+
},
|
|
1430
|
+
outputPath,
|
|
1431
|
+
options
|
|
1432
|
+
);
|
|
1433
|
+
} catch (err) {
|
|
1434
|
+
await handleError(err);
|
|
1435
|
+
}
|
|
1436
|
+
});
|
|
1437
|
+
async function pullAllProjects(options) {
|
|
1438
|
+
const configV2 = readProjectConfigV2();
|
|
1439
|
+
if (!configV2) throw notInitialized();
|
|
1440
|
+
checkTrackedFiles();
|
|
1441
|
+
let totalPulled = 0;
|
|
1442
|
+
let totalFailed = 0;
|
|
1443
|
+
for (const project of configV2.projects) {
|
|
1444
|
+
const outputPath = getEnvPathForEnvironment(project.environment);
|
|
1445
|
+
const displayName = project.projectName || project.projectId;
|
|
1446
|
+
console.log();
|
|
1447
|
+
console.log(
|
|
1448
|
+
chalk5.bold(
|
|
1449
|
+
`Pulling "${displayName}" (${project.environment}) \u2192 ${outputPath}`
|
|
1450
|
+
)
|
|
1451
|
+
);
|
|
1452
|
+
try {
|
|
1453
|
+
await pullProject(
|
|
1454
|
+
{
|
|
1455
|
+
projectId: project.projectId,
|
|
1456
|
+
organizationId: project.organizationId,
|
|
1457
|
+
environment: project.environment
|
|
1458
|
+
},
|
|
1231
1459
|
outputPath,
|
|
1232
|
-
|
|
1460
|
+
options
|
|
1233
1461
|
);
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1462
|
+
totalPulled++;
|
|
1463
|
+
} catch (err) {
|
|
1464
|
+
totalFailed++;
|
|
1465
|
+
if (err instanceof Error) {
|
|
1466
|
+
error(` Failed: ${err.message}`);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
console.log();
|
|
1471
|
+
if (totalFailed === 0) {
|
|
1472
|
+
success(`Pulled ${totalPulled} project${totalPulled !== 1 ? "s" : ""}`);
|
|
1473
|
+
} else {
|
|
1474
|
+
warning(
|
|
1475
|
+
`Pulled ${totalPulled}/${totalPulled + totalFailed} projects. ${totalFailed} failed.`
|
|
1476
|
+
);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
async function pullSingleProject(project, options) {
|
|
1480
|
+
checkTrackedFiles();
|
|
1481
|
+
const environment = options.env || project.environment;
|
|
1482
|
+
const outputPath = options.file || getEnvPathForEnvironment(environment);
|
|
1483
|
+
await pullProject(
|
|
1484
|
+
{
|
|
1485
|
+
projectId: project.projectId,
|
|
1486
|
+
organizationId: project.organizationId,
|
|
1487
|
+
environment
|
|
1488
|
+
},
|
|
1489
|
+
outputPath,
|
|
1490
|
+
options
|
|
1491
|
+
);
|
|
1492
|
+
}
|
|
1493
|
+
async function pullProject(project, outputPath, options) {
|
|
1494
|
+
const api = createAPIClient();
|
|
1495
|
+
let metaProjectRole;
|
|
1496
|
+
const variables = await withSpinner(
|
|
1497
|
+
`Fetching ${chalk5.bold(project.environment)} variables...`,
|
|
1498
|
+
async () => {
|
|
1499
|
+
const response = await api.get("/api/cli/variables", {
|
|
1500
|
+
projectId: project.projectId,
|
|
1501
|
+
environment: project.environment,
|
|
1502
|
+
...project.organizationId && {
|
|
1503
|
+
organizationId: project.organizationId
|
|
1239
1504
|
}
|
|
1505
|
+
});
|
|
1506
|
+
metaProjectRole = response.meta?.projectRole;
|
|
1507
|
+
return response.data || [];
|
|
1508
|
+
}
|
|
1509
|
+
);
|
|
1510
|
+
if (variables.length === 0) {
|
|
1511
|
+
warning(`No variables found for ${project.environment} environment.`);
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
const remoteVars = {};
|
|
1515
|
+
for (const variable of variables) {
|
|
1516
|
+
remoteVars[variable.key] = variable.value;
|
|
1517
|
+
}
|
|
1518
|
+
const localVars = readEnvFile(outputPath) || {};
|
|
1519
|
+
const diffResult = diffEnvVars(remoteVars, localVars);
|
|
1520
|
+
const hasChanges = Object.keys(diffResult.added).length > 0 || Object.keys(diffResult.removed).length > 0 || Object.keys(diffResult.changed).length > 0;
|
|
1521
|
+
if (!hasChanges) {
|
|
1522
|
+
success("Local file is up to date.");
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
console.log();
|
|
1526
|
+
console.log(chalk5.bold("Changes:"));
|
|
1527
|
+
console.log();
|
|
1528
|
+
diff(diffResult.added, diffResult.removed, diffResult.changed);
|
|
1529
|
+
console.log();
|
|
1530
|
+
if (options.dryRun) {
|
|
1531
|
+
info("Dry run - no changes written.");
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
if (!options.force && Object.keys(localVars).length > 0) {
|
|
1535
|
+
const { proceed } = await inquirer2.prompt([
|
|
1536
|
+
{
|
|
1537
|
+
type: "confirm",
|
|
1538
|
+
name: "proceed",
|
|
1539
|
+
message: `Overwrite ${outputPath}?`,
|
|
1540
|
+
default: true
|
|
1240
1541
|
}
|
|
1241
|
-
|
|
1542
|
+
]);
|
|
1543
|
+
if (!proceed) {
|
|
1544
|
+
info("Pull cancelled.");
|
|
1545
|
+
return;
|
|
1242
1546
|
}
|
|
1243
|
-
|
|
1244
|
-
|
|
1547
|
+
}
|
|
1548
|
+
try {
|
|
1549
|
+
const fs = await import("fs");
|
|
1550
|
+
if (fs.existsSync(outputPath)) {
|
|
1551
|
+
fs.chmodSync(outputPath, 420);
|
|
1552
|
+
}
|
|
1553
|
+
} catch {
|
|
1554
|
+
}
|
|
1555
|
+
if (options.format === "json") {
|
|
1556
|
+
const fs = await import("fs");
|
|
1557
|
+
fs.writeFileSync(outputPath, JSON.stringify(remoteVars, null, 2) + "\n");
|
|
1558
|
+
} else {
|
|
1559
|
+
const comments = {};
|
|
1560
|
+
for (const variable of variables) {
|
|
1561
|
+
if (variable.description) {
|
|
1562
|
+
comments[variable.key] = variable.description;
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
writeEnvFile(outputPath, remoteVars, { sort: true, comments });
|
|
1566
|
+
}
|
|
1567
|
+
const role = getRole();
|
|
1568
|
+
applyFileProtection(outputPath, role, metaProjectRole);
|
|
1569
|
+
success(
|
|
1570
|
+
`Downloaded ${variables.length} variables to ${chalk5.bold(outputPath)}`
|
|
1571
|
+
);
|
|
1572
|
+
if (metaProjectRole === "viewer") {
|
|
1573
|
+
info(
|
|
1574
|
+
"You have Viewer access to this project. You may only see variables you have been explicitly granted access to."
|
|
1245
1575
|
);
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1576
|
+
}
|
|
1577
|
+
const isProtected = role !== "admin" && role !== "team_lead" && metaProjectRole !== "manager";
|
|
1578
|
+
if (isProtected) {
|
|
1579
|
+
info(
|
|
1580
|
+
`File is read-only (your role: ${role || metaProjectRole || "member"}).`
|
|
1581
|
+
);
|
|
1582
|
+
}
|
|
1583
|
+
console.log();
|
|
1584
|
+
console.log(chalk5.dim(` Added: ${Object.keys(diffResult.added).length}`));
|
|
1585
|
+
console.log(
|
|
1586
|
+
chalk5.dim(` Changed: ${Object.keys(diffResult.changed).length}`)
|
|
1587
|
+
);
|
|
1588
|
+
console.log(
|
|
1589
|
+
chalk5.dim(` Removed: ${Object.keys(diffResult.removed).length}`)
|
|
1590
|
+
);
|
|
1591
|
+
}
|
|
1592
|
+
function checkTrackedFiles() {
|
|
1593
|
+
const trackedFiles = getTrackedEnvFiles();
|
|
1594
|
+
if (trackedFiles.length > 0) {
|
|
1595
|
+
error("Security risk: .env files are tracked by git!");
|
|
1596
|
+
console.log();
|
|
1597
|
+
for (const file of trackedFiles) {
|
|
1598
|
+
console.log(chalk5.red(` tracked: ${file}`));
|
|
1250
1599
|
}
|
|
1251
1600
|
console.log();
|
|
1252
1601
|
console.log(
|
|
1253
|
-
chalk5.
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
chalk5.dim(` Changed: ${Object.keys(diffResult.changed).length}`)
|
|
1257
|
-
);
|
|
1258
|
-
console.log(
|
|
1259
|
-
chalk5.dim(` Removed: ${Object.keys(diffResult.removed).length}`)
|
|
1602
|
+
chalk5.yellow(
|
|
1603
|
+
" Run the following to untrack them (without deleting the files):"
|
|
1604
|
+
)
|
|
1260
1605
|
);
|
|
1261
|
-
|
|
1262
|
-
|
|
1606
|
+
for (const file of trackedFiles) {
|
|
1607
|
+
console.log(chalk5.cyan(` git rm --cached ${file}`));
|
|
1608
|
+
}
|
|
1609
|
+
console.log();
|
|
1610
|
+
process.exit(1);
|
|
1263
1611
|
}
|
|
1264
|
-
}
|
|
1612
|
+
}
|
|
1265
1613
|
|
|
1266
1614
|
// src/commands/push.ts
|
|
1267
1615
|
import { Command as Command4 } from "commander";
|
|
@@ -1310,14 +1658,36 @@ function validateEnvVars(vars) {
|
|
|
1310
1658
|
var pushCommand = new Command4("push").description("Upload local .env file to cloud").option(
|
|
1311
1659
|
"-e, --env <environment>",
|
|
1312
1660
|
"Target environment (development, staging, production)"
|
|
1313
|
-
).option("-f, --file <path>", "Input file path (default: .env)").option("--merge", "Merge with existing variables (default)").option("--replace", "Replace all existing variables").option("--dry-run", "Show what would be uploaded without making changes").option("--force", "Skip confirmation").action(async (options) => {
|
|
1661
|
+
).option("-f, --file <path>", "Input file path (default: .env)").option("--merge", "Merge with existing variables (default)").option("--replace", "Replace all existing variables").option("--dry-run", "Show what would be uploaded without making changes").option("--force", "Skip confirmation").option("--project <name-or-id>", "Push to a specific linked project").action(async (options) => {
|
|
1314
1662
|
try {
|
|
1315
1663
|
if (!isAuthenticated()) {
|
|
1316
1664
|
throw notAuthenticated();
|
|
1317
1665
|
}
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1666
|
+
let projectId;
|
|
1667
|
+
let organizationId;
|
|
1668
|
+
let defaultEnvironment;
|
|
1669
|
+
if (options.project) {
|
|
1670
|
+
const configV2 = readProjectConfigV2();
|
|
1671
|
+
if (!configV2) throw notInitialized();
|
|
1672
|
+
const resolved = resolveProject(configV2, options.project);
|
|
1673
|
+
if (!resolved) {
|
|
1674
|
+
error(`Project not found: ${options.project}`);
|
|
1675
|
+
console.log();
|
|
1676
|
+
console.log("Linked projects:");
|
|
1677
|
+
for (const p of configV2.projects) {
|
|
1678
|
+
console.log(` ${p.projectName || p.projectId} (${p.environment})`);
|
|
1679
|
+
}
|
|
1680
|
+
process.exit(1);
|
|
1681
|
+
}
|
|
1682
|
+
projectId = resolved.projectId;
|
|
1683
|
+
organizationId = resolved.organizationId;
|
|
1684
|
+
defaultEnvironment = resolved.environment;
|
|
1685
|
+
} else {
|
|
1686
|
+
const projectConfig = readProjectConfig();
|
|
1687
|
+
if (!projectConfig) throw notInitialized();
|
|
1688
|
+
projectId = projectConfig.projectId;
|
|
1689
|
+
organizationId = projectConfig.organizationId;
|
|
1690
|
+
defaultEnvironment = projectConfig.environment;
|
|
1321
1691
|
}
|
|
1322
1692
|
const trackedFiles = getTrackedEnvFiles();
|
|
1323
1693
|
if (trackedFiles.length > 0) {
|
|
@@ -1339,10 +1709,8 @@ var pushCommand = new Command4("push").description("Upload local .env file to cl
|
|
|
1339
1709
|
process.exit(1);
|
|
1340
1710
|
}
|
|
1341
1711
|
const api = createAPIClient();
|
|
1342
|
-
const projects = await api.get("/api/cli/projects", { organizationId
|
|
1343
|
-
const currentProject = projects.data?.find(
|
|
1344
|
-
(p) => p._id === projectConfig.projectId
|
|
1345
|
-
);
|
|
1712
|
+
const projects = await api.get("/api/cli/projects", { organizationId });
|
|
1713
|
+
const currentProject = projects.data?.find((p) => p._id === projectId);
|
|
1346
1714
|
const projectRole = currentProject?.projectRole;
|
|
1347
1715
|
if (projectRole === "viewer") {
|
|
1348
1716
|
error(
|
|
@@ -1371,7 +1739,7 @@ var pushCommand = new Command4("push").description("Upload local .env file to cl
|
|
|
1371
1739
|
}
|
|
1372
1740
|
}
|
|
1373
1741
|
}
|
|
1374
|
-
const environment = options.env ||
|
|
1742
|
+
const environment = options.env || defaultEnvironment || "development";
|
|
1375
1743
|
const inputPath = options.file || getEnvPathForEnvironment(environment);
|
|
1376
1744
|
const mode = options.replace ? "replace" : "merge";
|
|
1377
1745
|
const localVars = readEnvFile(inputPath);
|
|
@@ -1398,11 +1766,11 @@ var pushCommand = new Command4("push").description("Upload local .env file to cl
|
|
|
1398
1766
|
"Fetching current variables...",
|
|
1399
1767
|
async () => {
|
|
1400
1768
|
const params = {
|
|
1401
|
-
projectId
|
|
1769
|
+
projectId,
|
|
1402
1770
|
environment
|
|
1403
1771
|
};
|
|
1404
|
-
if (
|
|
1405
|
-
params.organizationId =
|
|
1772
|
+
if (organizationId) {
|
|
1773
|
+
params.organizationId = organizationId;
|
|
1406
1774
|
}
|
|
1407
1775
|
const response = await api.get("/api/cli/variables", params);
|
|
1408
1776
|
return response.data || [];
|
|
@@ -1466,16 +1834,14 @@ var pushCommand = new Command4("push").description("Upload local .env file to cl
|
|
|
1466
1834
|
`Pushing variables to ${chalk6.bold(environment)}...`,
|
|
1467
1835
|
async () => {
|
|
1468
1836
|
const response = await api.post("/api/cli/variables/bulk", {
|
|
1469
|
-
projectId
|
|
1837
|
+
projectId,
|
|
1470
1838
|
environment,
|
|
1471
1839
|
variables: Object.entries(valid).map(([key, value]) => ({
|
|
1472
1840
|
key,
|
|
1473
1841
|
value
|
|
1474
1842
|
})),
|
|
1475
1843
|
mode,
|
|
1476
|
-
...
|
|
1477
|
-
organizationId: projectConfig.organizationId
|
|
1478
|
-
}
|
|
1844
|
+
...organizationId && { organizationId }
|
|
1479
1845
|
});
|
|
1480
1846
|
return response.data;
|
|
1481
1847
|
}
|
|
@@ -1515,16 +1881,48 @@ var pushCommand = new Command4("push").description("Upload local .env file to cl
|
|
|
1515
1881
|
import { Command as Command5 } from "commander";
|
|
1516
1882
|
import chalk7 from "chalk";
|
|
1517
1883
|
import inquirer4 from "inquirer";
|
|
1518
|
-
var switchCommand = new Command5("switch").description("Switch project or
|
|
1884
|
+
var switchCommand = new Command5("switch").description("Switch project, environment, or active linked project").argument("[target]", "project slug or environment name").option("-o, --organization <id>", "Switch organization").option("-p, --project <id>", "Switch project").option(
|
|
1519
1885
|
"-e, --env <environment>",
|
|
1520
1886
|
"Switch environment (development, staging, production)"
|
|
1521
|
-
).action(async (target, options) => {
|
|
1887
|
+
).option("--active <name-or-id>", "Set a linked project as active").action(async (target, options) => {
|
|
1522
1888
|
try {
|
|
1523
1889
|
if (!isAuthenticated()) {
|
|
1524
1890
|
throw notAuthenticated();
|
|
1525
1891
|
}
|
|
1526
1892
|
const api = createAPIClient();
|
|
1527
1893
|
const projectConfig = readProjectConfig();
|
|
1894
|
+
if (options.active) {
|
|
1895
|
+
const configV2 = readProjectConfigV2();
|
|
1896
|
+
if (!configV2 || configV2.projects.length < 2) {
|
|
1897
|
+
error(
|
|
1898
|
+
"No multiple projects linked. Use `envpilot init --add` to link another project."
|
|
1899
|
+
);
|
|
1900
|
+
process.exit(1);
|
|
1901
|
+
}
|
|
1902
|
+
const target2 = configV2.projects.find(
|
|
1903
|
+
(p) => p.projectId === options.active || p.projectName.toLowerCase() === options.active.toLowerCase()
|
|
1904
|
+
);
|
|
1905
|
+
if (!target2) {
|
|
1906
|
+
error(`Project not found: ${options.active}`);
|
|
1907
|
+
console.log();
|
|
1908
|
+
console.log("Linked projects:");
|
|
1909
|
+
for (const p of configV2.projects) {
|
|
1910
|
+
const mark = p.projectId === configV2.activeProjectId ? chalk7.green(" *") : "";
|
|
1911
|
+
console.log(
|
|
1912
|
+
` ${p.projectName || p.projectId} (${p.environment})${mark}`
|
|
1913
|
+
);
|
|
1914
|
+
}
|
|
1915
|
+
process.exit(1);
|
|
1916
|
+
}
|
|
1917
|
+
const updated = setActiveProjectInConfig(configV2, target2.projectId);
|
|
1918
|
+
writeProjectConfigV2(updated);
|
|
1919
|
+
setActiveProjectId(target2.projectId);
|
|
1920
|
+
setActiveOrganizationId(target2.organizationId);
|
|
1921
|
+
success(
|
|
1922
|
+
`Active project: ${chalk7.bold(target2.projectName || target2.projectId)}`
|
|
1923
|
+
);
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1528
1926
|
if (options.env || target && ["development", "staging", "production"].includes(target)) {
|
|
1529
1927
|
const environment = options.env || target;
|
|
1530
1928
|
if (!projectConfig) {
|
|
@@ -1566,6 +1964,25 @@ var switchCommand = new Command5("switch").description("Switch project or enviro
|
|
|
1566
1964
|
}
|
|
1567
1965
|
if (options.project || target) {
|
|
1568
1966
|
const projectIdentifier = options.project || target;
|
|
1967
|
+
const configV2 = readProjectConfigV2();
|
|
1968
|
+
if (configV2) {
|
|
1969
|
+
const linked = configV2.projects.find(
|
|
1970
|
+
(p) => p.projectId === projectIdentifier || p.projectName.toLowerCase() === projectIdentifier.toLowerCase()
|
|
1971
|
+
);
|
|
1972
|
+
if (linked) {
|
|
1973
|
+
const updated = setActiveProjectInConfig(
|
|
1974
|
+
configV2,
|
|
1975
|
+
linked.projectId
|
|
1976
|
+
);
|
|
1977
|
+
writeProjectConfigV2(updated);
|
|
1978
|
+
setActiveProjectId(linked.projectId);
|
|
1979
|
+
setActiveOrganizationId(linked.organizationId);
|
|
1980
|
+
success(
|
|
1981
|
+
`Switched to project: ${chalk7.bold(linked.projectName || linked.projectId)}`
|
|
1982
|
+
);
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1569
1986
|
let organizationId = projectConfig?.organizationId;
|
|
1570
1987
|
if (!organizationId) {
|
|
1571
1988
|
const organizations = await withSpinner(
|
|
@@ -1604,10 +2021,7 @@ var switchCommand = new Command5("switch").description("Switch project or enviro
|
|
|
1604
2021
|
}
|
|
1605
2022
|
}
|
|
1606
2023
|
const projects = await withSpinner("Fetching projects...", async () => {
|
|
1607
|
-
const response = await api.get(
|
|
1608
|
-
"/api/cli/projects",
|
|
1609
|
-
{ organizationId }
|
|
1610
|
-
);
|
|
2024
|
+
const response = await api.get("/api/cli/projects", { organizationId });
|
|
1611
2025
|
return response.data || [];
|
|
1612
2026
|
});
|
|
1613
2027
|
const project = projects.find(
|
|
@@ -1641,19 +2055,57 @@ var switchCommand = new Command5("switch").description("Switch project or enviro
|
|
|
1641
2055
|
}
|
|
1642
2056
|
return;
|
|
1643
2057
|
}
|
|
1644
|
-
if (!target && !options.project && !options.organization && !options.env) {
|
|
2058
|
+
if (!target && !options.project && !options.organization && !options.env && !options.active) {
|
|
2059
|
+
const configV2 = readProjectConfigV2();
|
|
2060
|
+
const hasMultipleProjects = configV2 && configV2.projects.length > 1;
|
|
2061
|
+
const choices = [];
|
|
2062
|
+
if (hasMultipleProjects) {
|
|
2063
|
+
choices.push({
|
|
2064
|
+
name: "Active project",
|
|
2065
|
+
value: "active"
|
|
2066
|
+
});
|
|
2067
|
+
}
|
|
2068
|
+
choices.push(
|
|
2069
|
+
{ name: "Environment", value: "environment" },
|
|
2070
|
+
{ name: "Project", value: "project" },
|
|
2071
|
+
{ name: "Organization", value: "organization" }
|
|
2072
|
+
);
|
|
1645
2073
|
const { switchType } = await inquirer4.prompt([
|
|
1646
2074
|
{
|
|
1647
2075
|
type: "list",
|
|
1648
2076
|
name: "switchType",
|
|
1649
2077
|
message: "What would you like to switch?",
|
|
1650
|
-
choices
|
|
1651
|
-
{ name: "Environment", value: "environment" },
|
|
1652
|
-
{ name: "Project", value: "project" },
|
|
1653
|
-
{ name: "Organization", value: "organization" }
|
|
1654
|
-
]
|
|
2078
|
+
choices
|
|
1655
2079
|
}
|
|
1656
2080
|
]);
|
|
2081
|
+
if (switchType === "active" && configV2) {
|
|
2082
|
+
const { projectId } = await inquirer4.prompt([
|
|
2083
|
+
{
|
|
2084
|
+
type: "list",
|
|
2085
|
+
name: "projectId",
|
|
2086
|
+
message: "Select active project:",
|
|
2087
|
+
choices: configV2.projects.map((p) => {
|
|
2088
|
+
const isActive = p.projectId === configV2.activeProjectId;
|
|
2089
|
+
return {
|
|
2090
|
+
name: `${p.projectName || p.projectId} (${p.environment})${isActive ? chalk7.green(" *current") : ""}`,
|
|
2091
|
+
value: p.projectId
|
|
2092
|
+
};
|
|
2093
|
+
}),
|
|
2094
|
+
default: configV2.activeProjectId
|
|
2095
|
+
}
|
|
2096
|
+
]);
|
|
2097
|
+
const selected = configV2.projects.find(
|
|
2098
|
+
(p) => p.projectId === projectId
|
|
2099
|
+
);
|
|
2100
|
+
const updated = setActiveProjectInConfig(configV2, projectId);
|
|
2101
|
+
writeProjectConfigV2(updated);
|
|
2102
|
+
setActiveProjectId(projectId);
|
|
2103
|
+
setActiveOrganizationId(selected.organizationId);
|
|
2104
|
+
success(
|
|
2105
|
+
`Active project: ${chalk7.bold(selected.projectName || selected.projectId)}`
|
|
2106
|
+
);
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
1657
2109
|
if (switchType === "environment") {
|
|
1658
2110
|
if (!projectConfig) {
|
|
1659
2111
|
error("No project initialized. Run `envpilot init` first.");
|
|
@@ -1770,7 +2222,7 @@ import { Command as Command6 } from "commander";
|
|
|
1770
2222
|
import chalk8 from "chalk";
|
|
1771
2223
|
var listCommand = new Command6("list").description("List resources").argument(
|
|
1772
2224
|
"[resource]",
|
|
1773
|
-
"Resource type: projects, organizations, variables",
|
|
2225
|
+
"Resource type: projects, organizations, variables, linked",
|
|
1774
2226
|
"projects"
|
|
1775
2227
|
).option("-o, --organization <id>", "Organization ID (for projects/variables)").option("-p, --project <id>", "Project ID (for variables)").option("-e, --env <environment>", "Environment filter (for variables)").option("--show-values", "Show actual variable values (masked by default)").option("--json", "Output as JSON").action(async (resource, options) => {
|
|
1776
2228
|
try {
|
|
@@ -1791,6 +2243,9 @@ var listCommand = new Command6("list").description("List resources").argument(
|
|
|
1791
2243
|
case "variables":
|
|
1792
2244
|
await listVariables(api, projectConfig, options);
|
|
1793
2245
|
break;
|
|
2246
|
+
case "linked":
|
|
2247
|
+
listLinked();
|
|
2248
|
+
break;
|
|
1794
2249
|
default:
|
|
1795
2250
|
error(`Unknown resource: ${resource}`);
|
|
1796
2251
|
console.log();
|
|
@@ -1800,12 +2255,43 @@ var listCommand = new Command6("list").description("List resources").argument(
|
|
|
1800
2255
|
" projects List projects in an organization"
|
|
1801
2256
|
);
|
|
1802
2257
|
console.log(" variables (vars) List variables in a project");
|
|
2258
|
+
console.log(
|
|
2259
|
+
" linked List projects linked in this directory"
|
|
2260
|
+
);
|
|
1803
2261
|
process.exit(1);
|
|
1804
2262
|
}
|
|
1805
2263
|
} catch (err) {
|
|
1806
2264
|
await handleError(err);
|
|
1807
2265
|
}
|
|
1808
2266
|
});
|
|
2267
|
+
function listLinked() {
|
|
2268
|
+
const configV2 = readProjectConfigV2();
|
|
2269
|
+
if (!configV2) {
|
|
2270
|
+
info("No projects linked. Run `envpilot init` to get started.");
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2273
|
+
header(`Linked Projects (${configV2.projects.length})`);
|
|
2274
|
+
console.log();
|
|
2275
|
+
for (const project of configV2.projects) {
|
|
2276
|
+
const isActive = project.projectId === configV2.activeProjectId;
|
|
2277
|
+
const marker = isActive ? chalk8.green("*") : " ";
|
|
2278
|
+
const envFile = getEnvPathForEnvironment(project.environment);
|
|
2279
|
+
console.log(
|
|
2280
|
+
` ${marker} ${chalk8.bold(project.projectName || project.projectId)} ${chalk8.dim(`(${project.organizationName || project.organizationId})`)}`
|
|
2281
|
+
);
|
|
2282
|
+
console.log(` ${project.environment} ${chalk8.dim("\u2192")} ${envFile}`);
|
|
2283
|
+
console.log();
|
|
2284
|
+
}
|
|
2285
|
+
if (configV2.projects.length > 1) {
|
|
2286
|
+
console.log(chalk8.dim(" (* = active project)"));
|
|
2287
|
+
console.log();
|
|
2288
|
+
console.log(
|
|
2289
|
+
chalk8.dim(
|
|
2290
|
+
' Use `envpilot switch --active "<name>"` to change the active project'
|
|
2291
|
+
)
|
|
2292
|
+
);
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
1809
2295
|
async function listOrganizations(api, options) {
|
|
1810
2296
|
const organizations = await withSpinner(
|
|
1811
2297
|
"Fetching organizations...",
|
|
@@ -2105,8 +2591,8 @@ async function handlePath() {
|
|
|
2105
2591
|
]);
|
|
2106
2592
|
}
|
|
2107
2593
|
async function handleReset() {
|
|
2108
|
-
const
|
|
2109
|
-
const { confirm } = await
|
|
2594
|
+
const inquirer6 = await import("inquirer");
|
|
2595
|
+
const { confirm } = await inquirer6.default.prompt([
|
|
2110
2596
|
{
|
|
2111
2597
|
type: "confirm",
|
|
2112
2598
|
name: "confirm",
|
|
@@ -2143,21 +2629,421 @@ var logoutCommand = new Command8("logout").description("Log out from Envpilot").
|
|
|
2143
2629
|
}
|
|
2144
2630
|
});
|
|
2145
2631
|
|
|
2146
|
-
// src/commands/
|
|
2632
|
+
// src/commands/unlink.ts
|
|
2147
2633
|
import { Command as Command9 } from "commander";
|
|
2148
2634
|
import chalk10 from "chalk";
|
|
2635
|
+
import inquirer5 from "inquirer";
|
|
2636
|
+
var unlinkCommand = new Command9("unlink").description("Remove a linked project from this directory").argument("[project]", "Project name or ID to unlink").option("--force", "Skip confirmation").action(async (projectArg, options) => {
|
|
2637
|
+
try {
|
|
2638
|
+
if (!isAuthenticated()) {
|
|
2639
|
+
throw notAuthenticated();
|
|
2640
|
+
}
|
|
2641
|
+
const config2 = readProjectConfigV2();
|
|
2642
|
+
if (!config2 || config2.projects.length === 0) {
|
|
2643
|
+
error("No projects linked. Run `envpilot init` first.");
|
|
2644
|
+
process.exit(1);
|
|
2645
|
+
}
|
|
2646
|
+
let targetProject;
|
|
2647
|
+
if (projectArg) {
|
|
2648
|
+
targetProject = resolveProject(config2, projectArg);
|
|
2649
|
+
if (!targetProject) {
|
|
2650
|
+
error(`Project not found: ${projectArg}`);
|
|
2651
|
+
console.log();
|
|
2652
|
+
console.log("Linked projects:");
|
|
2653
|
+
for (const p of config2.projects) {
|
|
2654
|
+
console.log(
|
|
2655
|
+
` ${p.projectName || p.projectId} (${p.organizationName || p.organizationId})`
|
|
2656
|
+
);
|
|
2657
|
+
}
|
|
2658
|
+
process.exit(1);
|
|
2659
|
+
}
|
|
2660
|
+
} else if (config2.projects.length > 1) {
|
|
2661
|
+
const { projectId } = await inquirer5.prompt([
|
|
2662
|
+
{
|
|
2663
|
+
type: "list",
|
|
2664
|
+
name: "projectId",
|
|
2665
|
+
message: "Select a project to unlink:",
|
|
2666
|
+
choices: config2.projects.map((p) => {
|
|
2667
|
+
const isActive = p.projectId === config2.activeProjectId;
|
|
2668
|
+
return {
|
|
2669
|
+
name: `${p.projectName || p.projectId} (${p.organizationName || p.organizationId})${isActive ? chalk10.green(" *active") : ""}`,
|
|
2670
|
+
value: p.projectId
|
|
2671
|
+
};
|
|
2672
|
+
})
|
|
2673
|
+
}
|
|
2674
|
+
]);
|
|
2675
|
+
targetProject = config2.projects.find((p) => p.projectId === projectId);
|
|
2676
|
+
} else {
|
|
2677
|
+
targetProject = config2.projects[0];
|
|
2678
|
+
}
|
|
2679
|
+
const displayName = targetProject.projectName || targetProject.projectId;
|
|
2680
|
+
if (!options.force) {
|
|
2681
|
+
const { proceed } = await inquirer5.prompt([
|
|
2682
|
+
{
|
|
2683
|
+
type: "confirm",
|
|
2684
|
+
name: "proceed",
|
|
2685
|
+
message: `Unlink "${displayName}"? Your .env files won't be deleted.`,
|
|
2686
|
+
default: false
|
|
2687
|
+
}
|
|
2688
|
+
]);
|
|
2689
|
+
if (!proceed) {
|
|
2690
|
+
info("Unlink cancelled.");
|
|
2691
|
+
return;
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
const updated = removeProjectFromConfig(config2, targetProject.projectId);
|
|
2695
|
+
if (!updated) {
|
|
2696
|
+
deleteProjectConfig();
|
|
2697
|
+
success(`Unlinked "${displayName}". No projects remaining.`);
|
|
2698
|
+
info("Run `envpilot init` to link a new project.");
|
|
2699
|
+
} else {
|
|
2700
|
+
writeProjectConfigV2(updated);
|
|
2701
|
+
const newActive = getActiveProject(updated);
|
|
2702
|
+
if (newActive) {
|
|
2703
|
+
setActiveProjectId(newActive.projectId);
|
|
2704
|
+
}
|
|
2705
|
+
success(`Unlinked "${displayName}".`);
|
|
2706
|
+
if (config2.activeProjectId === targetProject.projectId && newActive) {
|
|
2707
|
+
info(
|
|
2708
|
+
`Active project switched to "${newActive.projectName || newActive.projectId}".`
|
|
2709
|
+
);
|
|
2710
|
+
}
|
|
2711
|
+
console.log(
|
|
2712
|
+
chalk10.dim(
|
|
2713
|
+
` ${updated.projects.length} project${updated.projects.length !== 1 ? "s" : ""} remaining`
|
|
2714
|
+
)
|
|
2715
|
+
);
|
|
2716
|
+
}
|
|
2717
|
+
} catch (err) {
|
|
2718
|
+
await handleError(err);
|
|
2719
|
+
}
|
|
2720
|
+
});
|
|
2721
|
+
|
|
2722
|
+
// src/commands/sync.ts
|
|
2723
|
+
import { Command as Command10 } from "commander";
|
|
2724
|
+
import chalk11 from "chalk";
|
|
2725
|
+
|
|
2726
|
+
// src/lib/commit-guard.ts
|
|
2727
|
+
import {
|
|
2728
|
+
existsSync as existsSync3,
|
|
2729
|
+
readFileSync as readFileSync3,
|
|
2730
|
+
writeFileSync as writeFileSync3,
|
|
2731
|
+
mkdirSync,
|
|
2732
|
+
chmodSync as chmodSync2,
|
|
2733
|
+
unlinkSync as unlinkSync2,
|
|
2734
|
+
statSync
|
|
2735
|
+
} from "fs";
|
|
2736
|
+
import { join as join3, resolve } from "path";
|
|
2737
|
+
import { execSync as execSync2 } from "child_process";
|
|
2738
|
+
var HOOK_START_MARKER = "# ENVPILOT_GUARD_START";
|
|
2739
|
+
var HOOK_END_MARKER = "# ENVPILOT_GUARD_END";
|
|
2740
|
+
var HOOK_BLOCK = `${HOOK_START_MARKER} - Do not remove. Installed by Envpilot CLI.
|
|
2741
|
+
ENV_FILES=$(git diff --cached --name-only | grep -E '(^|/)\\.env($|\\.)' || true)
|
|
2742
|
+
if [ -n "$ENV_FILES" ]; then
|
|
2743
|
+
echo ""
|
|
2744
|
+
echo "\\033[1;31mERROR:\\033[0m Envpilot commit guard blocked this commit."
|
|
2745
|
+
echo ""
|
|
2746
|
+
echo "The following .env files were staged:"
|
|
2747
|
+
echo "$ENV_FILES" | while IFS= read -r f; do echo " - $f"; done
|
|
2748
|
+
echo ""
|
|
2749
|
+
echo "Remove them with: git reset HEAD <file>"
|
|
2750
|
+
echo "To bypass (not recommended): git commit --no-verify"
|
|
2751
|
+
exit 1
|
|
2752
|
+
fi
|
|
2753
|
+
${HOOK_END_MARKER}`;
|
|
2754
|
+
function findGitRoot(startDir) {
|
|
2755
|
+
let dir = startDir || process.cwd();
|
|
2756
|
+
while (true) {
|
|
2757
|
+
if (existsSync3(join3(dir, ".git"))) {
|
|
2758
|
+
return dir;
|
|
2759
|
+
}
|
|
2760
|
+
const parent = resolve(dir, "..");
|
|
2761
|
+
if (parent === dir) {
|
|
2762
|
+
return null;
|
|
2763
|
+
}
|
|
2764
|
+
dir = parent;
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
function resolveGitDir(repoRoot) {
|
|
2768
|
+
const gitPath = join3(repoRoot, ".git");
|
|
2769
|
+
try {
|
|
2770
|
+
const stat = statSync(gitPath);
|
|
2771
|
+
if (stat.isDirectory()) {
|
|
2772
|
+
return gitPath;
|
|
2773
|
+
}
|
|
2774
|
+
} catch {
|
|
2775
|
+
return gitPath;
|
|
2776
|
+
}
|
|
2777
|
+
try {
|
|
2778
|
+
const content = readFileSync3(gitPath, "utf-8").trim();
|
|
2779
|
+
const match = content.match(/^gitdir:\s*(.+)$/);
|
|
2780
|
+
if (match) {
|
|
2781
|
+
const gitdir = resolve(repoRoot, match[1]);
|
|
2782
|
+
const commonDir = resolve(gitdir, "..", "..");
|
|
2783
|
+
if (existsSync3(join3(commonDir, "hooks")) || existsSync3(commonDir)) {
|
|
2784
|
+
return commonDir;
|
|
2785
|
+
}
|
|
2786
|
+
return gitdir;
|
|
2787
|
+
}
|
|
2788
|
+
} catch {
|
|
2789
|
+
}
|
|
2790
|
+
return gitPath;
|
|
2791
|
+
}
|
|
2792
|
+
function getHooksDir(repoRoot) {
|
|
2793
|
+
try {
|
|
2794
|
+
const customPath = execSync2("git config core.hooksPath", {
|
|
2795
|
+
cwd: repoRoot,
|
|
2796
|
+
encoding: "utf-8",
|
|
2797
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2798
|
+
}).trim();
|
|
2799
|
+
if (customPath) {
|
|
2800
|
+
return resolve(repoRoot, customPath);
|
|
2801
|
+
}
|
|
2802
|
+
} catch {
|
|
2803
|
+
}
|
|
2804
|
+
const gitDir = resolveGitDir(repoRoot);
|
|
2805
|
+
return join3(gitDir, "hooks");
|
|
2806
|
+
}
|
|
2807
|
+
function installCommitGuard(repoRoot) {
|
|
2808
|
+
const root = repoRoot || findGitRoot();
|
|
2809
|
+
if (!root) {
|
|
2810
|
+
return {
|
|
2811
|
+
installed: false,
|
|
2812
|
+
hookPath: null,
|
|
2813
|
+
message: "Not a git repository"
|
|
2814
|
+
};
|
|
2815
|
+
}
|
|
2816
|
+
try {
|
|
2817
|
+
const hooksDir = getHooksDir(root);
|
|
2818
|
+
const hookPath = join3(hooksDir, "pre-commit");
|
|
2819
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
2820
|
+
let existingContent = "";
|
|
2821
|
+
try {
|
|
2822
|
+
existingContent = readFileSync3(hookPath, "utf-8");
|
|
2823
|
+
} catch {
|
|
2824
|
+
}
|
|
2825
|
+
if (existingContent.includes(HOOK_START_MARKER)) {
|
|
2826
|
+
const startIdx = existingContent.indexOf(HOOK_START_MARKER);
|
|
2827
|
+
const endIdx = existingContent.indexOf(HOOK_END_MARKER) + HOOK_END_MARKER.length;
|
|
2828
|
+
const updated = existingContent.substring(0, startIdx) + HOOK_BLOCK + existingContent.substring(endIdx);
|
|
2829
|
+
writeFileSync3(hookPath, updated, "utf-8");
|
|
2830
|
+
chmodSync2(hookPath, 493);
|
|
2831
|
+
return {
|
|
2832
|
+
installed: true,
|
|
2833
|
+
hookPath,
|
|
2834
|
+
message: "Pre-commit hook updated"
|
|
2835
|
+
};
|
|
2836
|
+
}
|
|
2837
|
+
let newContent;
|
|
2838
|
+
if (existingContent.trim()) {
|
|
2839
|
+
newContent = existingContent.trimEnd() + "\n\n" + HOOK_BLOCK + "\n";
|
|
2840
|
+
} else {
|
|
2841
|
+
newContent = "#!/bin/sh\n\n" + HOOK_BLOCK + "\n";
|
|
2842
|
+
}
|
|
2843
|
+
writeFileSync3(hookPath, newContent, "utf-8");
|
|
2844
|
+
chmodSync2(hookPath, 493);
|
|
2845
|
+
return {
|
|
2846
|
+
installed: true,
|
|
2847
|
+
hookPath,
|
|
2848
|
+
message: "Pre-commit hook installed"
|
|
2849
|
+
};
|
|
2850
|
+
} catch (err) {
|
|
2851
|
+
return {
|
|
2852
|
+
installed: false,
|
|
2853
|
+
hookPath: null,
|
|
2854
|
+
message: `Failed to install hook: ${err instanceof Error ? err.message : String(err)}`
|
|
2855
|
+
};
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
// src/commands/sync.ts
|
|
2860
|
+
var syncCommand = new Command10("sync").description(
|
|
2861
|
+
"Login, select project, pull variables, and protect files \u2014 all in one command"
|
|
2862
|
+
).option("-o, --organization <id>", "Organization ID").option("-p, --project <id>", "Project ID or name").option(
|
|
2863
|
+
"-e, --env <environment>",
|
|
2864
|
+
"Environment (development, staging, production)"
|
|
2865
|
+
).option("--force", "Overwrite without confirmation").option("--no-guard", "Skip pre-commit hook installation").action(async (options) => {
|
|
2866
|
+
try {
|
|
2867
|
+
if (!isAuthenticated()) {
|
|
2868
|
+
console.log();
|
|
2869
|
+
info("Not logged in. Starting authentication...");
|
|
2870
|
+
console.log();
|
|
2871
|
+
await performLogin();
|
|
2872
|
+
console.log();
|
|
2873
|
+
} else {
|
|
2874
|
+
info("Already authenticated.");
|
|
2875
|
+
}
|
|
2876
|
+
ensureEnvInGitignore();
|
|
2877
|
+
if (options.guard !== false) {
|
|
2878
|
+
const guardResult = installCommitGuard();
|
|
2879
|
+
if (guardResult.installed) {
|
|
2880
|
+
success(guardResult.message);
|
|
2881
|
+
info("Staged .env files will be blocked from commits.");
|
|
2882
|
+
} else if (guardResult.message !== "Not a git repository") {
|
|
2883
|
+
warning(guardResult.message);
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
console.log();
|
|
2887
|
+
let projectId;
|
|
2888
|
+
let organizationId;
|
|
2889
|
+
let environment;
|
|
2890
|
+
let projectName;
|
|
2891
|
+
let organizationName;
|
|
2892
|
+
const existingConfig = readProjectConfigV2();
|
|
2893
|
+
if (existingConfig && !options.organization && !options.project && !options.env) {
|
|
2894
|
+
const active = getActiveProject(existingConfig);
|
|
2895
|
+
if (active) {
|
|
2896
|
+
projectId = active.projectId;
|
|
2897
|
+
organizationId = active.organizationId;
|
|
2898
|
+
environment = active.environment;
|
|
2899
|
+
projectName = active.projectName;
|
|
2900
|
+
organizationName = active.organizationName;
|
|
2901
|
+
info(
|
|
2902
|
+
`Using project ${chalk11.bold(projectName || projectId)} (${environment})`
|
|
2903
|
+
);
|
|
2904
|
+
} else {
|
|
2905
|
+
const selection = await selectOrgProjectEnv(options);
|
|
2906
|
+
projectId = selection.selectedProject._id;
|
|
2907
|
+
organizationId = selection.selectedOrg._id;
|
|
2908
|
+
environment = selection.selectedEnvironment;
|
|
2909
|
+
projectName = selection.selectedProject.name;
|
|
2910
|
+
organizationName = selection.selectedOrg.name;
|
|
2911
|
+
}
|
|
2912
|
+
} else {
|
|
2913
|
+
const selection = await selectOrgProjectEnv({
|
|
2914
|
+
organization: options.organization,
|
|
2915
|
+
project: options.project,
|
|
2916
|
+
environment: options.env
|
|
2917
|
+
});
|
|
2918
|
+
projectId = selection.selectedProject._id;
|
|
2919
|
+
organizationId = selection.selectedOrg._id;
|
|
2920
|
+
environment = selection.selectedEnvironment;
|
|
2921
|
+
projectName = selection.selectedProject.name;
|
|
2922
|
+
organizationName = selection.selectedOrg.name;
|
|
2923
|
+
if (selection.selectedOrg.role) {
|
|
2924
|
+
setRole(selection.selectedOrg.role);
|
|
2925
|
+
}
|
|
2926
|
+
writeProjectConfigV2({
|
|
2927
|
+
version: 1,
|
|
2928
|
+
activeProjectId: projectId,
|
|
2929
|
+
projects: existingConfig ? [
|
|
2930
|
+
...existingConfig.projects.filter(
|
|
2931
|
+
(p) => p.projectId !== projectId
|
|
2932
|
+
),
|
|
2933
|
+
{
|
|
2934
|
+
projectId,
|
|
2935
|
+
organizationId,
|
|
2936
|
+
projectName,
|
|
2937
|
+
organizationName,
|
|
2938
|
+
environment
|
|
2939
|
+
}
|
|
2940
|
+
] : [
|
|
2941
|
+
{
|
|
2942
|
+
projectId,
|
|
2943
|
+
organizationId,
|
|
2944
|
+
projectName,
|
|
2945
|
+
organizationName,
|
|
2946
|
+
environment
|
|
2947
|
+
}
|
|
2948
|
+
]
|
|
2949
|
+
});
|
|
2950
|
+
setActiveOrganizationId(organizationId);
|
|
2951
|
+
setActiveProjectId(projectId);
|
|
2952
|
+
}
|
|
2953
|
+
console.log();
|
|
2954
|
+
let metaProjectRole;
|
|
2955
|
+
const variables = await withSpinner(
|
|
2956
|
+
`Fetching ${chalk11.bold(environment)} variables...`,
|
|
2957
|
+
async () => {
|
|
2958
|
+
const api = createAPIClient();
|
|
2959
|
+
const response = await api.get("/api/cli/variables", {
|
|
2960
|
+
projectId,
|
|
2961
|
+
environment,
|
|
2962
|
+
...organizationId && { organizationId }
|
|
2963
|
+
});
|
|
2964
|
+
metaProjectRole = response.meta?.projectRole;
|
|
2965
|
+
return response.data || [];
|
|
2966
|
+
}
|
|
2967
|
+
);
|
|
2968
|
+
const outputPath = getEnvPathForEnvironment(environment);
|
|
2969
|
+
if (variables.length === 0) {
|
|
2970
|
+
warning(`No variables found for ${environment} environment.`);
|
|
2971
|
+
console.log();
|
|
2972
|
+
return;
|
|
2973
|
+
}
|
|
2974
|
+
const remoteVars = {};
|
|
2975
|
+
for (const variable of variables) {
|
|
2976
|
+
remoteVars[variable.key] = variable.value;
|
|
2977
|
+
}
|
|
2978
|
+
const localVars = readEnvFile(outputPath) || {};
|
|
2979
|
+
const diffResult = diffEnvVars(remoteVars, localVars);
|
|
2980
|
+
const hasChanges = Object.keys(diffResult.added).length > 0 || Object.keys(diffResult.removed).length > 0 || Object.keys(diffResult.changed).length > 0;
|
|
2981
|
+
if (!hasChanges) {
|
|
2982
|
+
success(`${chalk11.bold(outputPath)} is up to date.`);
|
|
2983
|
+
console.log();
|
|
2984
|
+
return;
|
|
2985
|
+
}
|
|
2986
|
+
console.log();
|
|
2987
|
+
console.log(chalk11.bold("Changes:"));
|
|
2988
|
+
console.log();
|
|
2989
|
+
diff(diffResult.added, diffResult.removed, diffResult.changed);
|
|
2990
|
+
console.log();
|
|
2991
|
+
try {
|
|
2992
|
+
const fs = await import("fs");
|
|
2993
|
+
if (fs.existsSync(outputPath)) {
|
|
2994
|
+
fs.chmodSync(outputPath, 420);
|
|
2995
|
+
}
|
|
2996
|
+
} catch {
|
|
2997
|
+
}
|
|
2998
|
+
const comments = {};
|
|
2999
|
+
for (const variable of variables) {
|
|
3000
|
+
if (variable.description) {
|
|
3001
|
+
comments[variable.key] = variable.description;
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
writeEnvFile(outputPath, remoteVars, { sort: true, comments });
|
|
3005
|
+
const role = getRole();
|
|
3006
|
+
applyFileProtection(outputPath, role, metaProjectRole);
|
|
3007
|
+
success(
|
|
3008
|
+
`Synced ${variables.length} variables to ${chalk11.bold(outputPath)}`
|
|
3009
|
+
);
|
|
3010
|
+
const isProtected = role !== "admin" && role !== "team_lead" && metaProjectRole !== "manager";
|
|
3011
|
+
if (isProtected) {
|
|
3012
|
+
info(
|
|
3013
|
+
`File is read-only (your role: ${role || metaProjectRole || "member"}).`
|
|
3014
|
+
);
|
|
3015
|
+
}
|
|
3016
|
+
console.log();
|
|
3017
|
+
console.log(
|
|
3018
|
+
chalk11.dim(` Added: ${Object.keys(diffResult.added).length}`)
|
|
3019
|
+
);
|
|
3020
|
+
console.log(
|
|
3021
|
+
chalk11.dim(` Changed: ${Object.keys(diffResult.changed).length}`)
|
|
3022
|
+
);
|
|
3023
|
+
console.log(
|
|
3024
|
+
chalk11.dim(` Removed: ${Object.keys(diffResult.removed).length}`)
|
|
3025
|
+
);
|
|
3026
|
+
console.log();
|
|
3027
|
+
} catch (err) {
|
|
3028
|
+
await handleError(err);
|
|
3029
|
+
}
|
|
3030
|
+
});
|
|
3031
|
+
|
|
3032
|
+
// src/commands/usage.ts
|
|
3033
|
+
import { Command as Command11 } from "commander";
|
|
3034
|
+
import chalk12 from "chalk";
|
|
2149
3035
|
function formatUsage(current, limit) {
|
|
2150
3036
|
const limitStr = limit === null ? "unlimited" : String(limit);
|
|
2151
3037
|
const ratio = `${current}/${limitStr}`;
|
|
2152
|
-
if (limit === null) return
|
|
2153
|
-
if (current >= limit) return
|
|
2154
|
-
if (current >= limit * 0.8) return
|
|
2155
|
-
return
|
|
3038
|
+
if (limit === null) return chalk12.green(ratio);
|
|
3039
|
+
if (current >= limit) return chalk12.red(ratio);
|
|
3040
|
+
if (current >= limit * 0.8) return chalk12.yellow(ratio);
|
|
3041
|
+
return chalk12.green(ratio);
|
|
2156
3042
|
}
|
|
2157
3043
|
function featureStatus(enabled) {
|
|
2158
|
-
return enabled ?
|
|
3044
|
+
return enabled ? chalk12.green("Enabled") : chalk12.dim("Disabled (Pro)");
|
|
2159
3045
|
}
|
|
2160
|
-
var usageCommand = new
|
|
3046
|
+
var usageCommand = new Command11("usage").description("Show plan usage and limits for the active organization").option("-o, --organization <id>", "Organization ID").option("--json", "Output as JSON").action(async (options) => {
|
|
2161
3047
|
try {
|
|
2162
3048
|
if (!isAuthenticated()) {
|
|
2163
3049
|
throw notAuthenticated();
|
|
@@ -2200,7 +3086,7 @@ var usageCommand = new Command9("usage").description("Show plan usage and limits
|
|
|
2200
3086
|
console.log(JSON.stringify(usage, null, 2));
|
|
2201
3087
|
return;
|
|
2202
3088
|
}
|
|
2203
|
-
const tierLabel = usage.tier === "pro" ?
|
|
3089
|
+
const tierLabel = usage.tier === "pro" ? chalk12.green("Pro") : chalk12.white("Free");
|
|
2204
3090
|
header(`Plan: ${tierLabel}`);
|
|
2205
3091
|
blank();
|
|
2206
3092
|
if (!usage.enforcementEnabled) {
|
|
@@ -2252,10 +3138,55 @@ var usageCommand = new Command9("usage").description("Show plan usage and limits
|
|
|
2252
3138
|
}
|
|
2253
3139
|
});
|
|
2254
3140
|
|
|
3141
|
+
// src/lib/version-check.ts
|
|
3142
|
+
import chalk13 from "chalk";
|
|
3143
|
+
import Conf2 from "conf";
|
|
3144
|
+
var CLI_VERSION = "1.3.1";
|
|
3145
|
+
var CHECK_INTERVAL = 60 * 60 * 1e3;
|
|
3146
|
+
var _cache = null;
|
|
3147
|
+
function getCache() {
|
|
3148
|
+
if (!_cache) {
|
|
3149
|
+
_cache = new Conf2({
|
|
3150
|
+
projectName: "envpilot",
|
|
3151
|
+
configName: "version-cache"
|
|
3152
|
+
});
|
|
3153
|
+
}
|
|
3154
|
+
return _cache;
|
|
3155
|
+
}
|
|
3156
|
+
function checkForUpdate() {
|
|
3157
|
+
const cache = getCache();
|
|
3158
|
+
const lastCheck = cache.get("lastVersionCheck");
|
|
3159
|
+
if (lastCheck && Date.now() - lastCheck < CHECK_INTERVAL) return;
|
|
3160
|
+
const apiUrl = getApiUrl();
|
|
3161
|
+
fetch(`${apiUrl}/api/version`, { signal: AbortSignal.timeout(5e3) }).then((res) => {
|
|
3162
|
+
if (!res.ok) return;
|
|
3163
|
+
return res.json();
|
|
3164
|
+
}).then((data) => {
|
|
3165
|
+
if (!data?.cli) return;
|
|
3166
|
+
cache.set("lastVersionCheck", Date.now());
|
|
3167
|
+
if (data.cli !== CLI_VERSION) {
|
|
3168
|
+
console.log();
|
|
3169
|
+
console.log(
|
|
3170
|
+
chalk13.yellow(" Update available:"),
|
|
3171
|
+
chalk13.dim(CLI_VERSION),
|
|
3172
|
+
chalk13.yellow("\u2192"),
|
|
3173
|
+
chalk13.green(data.cli)
|
|
3174
|
+
);
|
|
3175
|
+
console.log(
|
|
3176
|
+
chalk13.dim(" Run"),
|
|
3177
|
+
chalk13.cyan("npm update -g @envpilot/cli"),
|
|
3178
|
+
chalk13.dim("to update")
|
|
3179
|
+
);
|
|
3180
|
+
console.log();
|
|
3181
|
+
}
|
|
3182
|
+
}).catch(() => {
|
|
3183
|
+
});
|
|
3184
|
+
}
|
|
3185
|
+
|
|
2255
3186
|
// src/index.ts
|
|
2256
3187
|
initSentry();
|
|
2257
|
-
var program = new
|
|
2258
|
-
program.name("envpilot").description("Envpilot CLI - Sync, secure, and share environment variables").version("1.3.
|
|
3188
|
+
var program = new Command12();
|
|
3189
|
+
program.name("envpilot").description("Envpilot CLI - Sync, secure, and share environment variables").version("1.3.2");
|
|
2259
3190
|
program.addCommand(loginCommand);
|
|
2260
3191
|
program.addCommand(logoutCommand);
|
|
2261
3192
|
program.addCommand(initCommand);
|
|
@@ -2264,5 +3195,10 @@ program.addCommand(pushCommand);
|
|
|
2264
3195
|
program.addCommand(switchCommand);
|
|
2265
3196
|
program.addCommand(listCommand);
|
|
2266
3197
|
program.addCommand(configCommand);
|
|
3198
|
+
program.addCommand(unlinkCommand);
|
|
3199
|
+
program.addCommand(syncCommand);
|
|
2267
3200
|
program.addCommand(usageCommand);
|
|
3201
|
+
program.hook("postAction", () => {
|
|
3202
|
+
checkForUpdate();
|
|
3203
|
+
});
|
|
2268
3204
|
program.parse();
|