gigadrive 2.2.0 → 2.4.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/dist/index.mjs CHANGED
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command14 } from "@effect/cli";
4
+ import { Command as Command15 } from "@effect/cli";
5
5
  import { NodeContext, NodeRuntime } from "@effect/platform-node";
6
6
  import { NetworkConfigLive as NetworkConfigLive2 } from "@gigadrive/network-config";
7
- import { Effect as Effect23, Layer, LogLevel, Logger } from "effect";
7
+ import { Effect as Effect27, Layer, LogLevel, Logger } from "effect";
8
8
 
9
9
  // package.json
10
10
  var package_default = {
11
11
  name: "gigadrive",
12
- version: "2.2.0",
12
+ version: "2.4.0",
13
13
  repository: {
14
14
  type: "git",
15
15
  url: "https://github.com/Gigadrive/sdk.git",
@@ -35,7 +35,7 @@ var package_default = {
35
35
  dependencies: {
36
36
  "@effect/cli": "^0.75.0",
37
37
  "@effect/platform": "^0.96.0",
38
- "@effect/platform-node": "^0.106.0",
38
+ "@effect/platform-node": "^0.107.0",
39
39
  "@gigadrive/build-utils": "workspace:*",
40
40
  "@gigadrive/commons": "workspace:*",
41
41
  "@gigadrive/network-config": "workspace:*",
@@ -46,8 +46,8 @@ var package_default = {
46
46
  open: "^10.2.0"
47
47
  },
48
48
  devDependencies: {
49
- "@effect/language-service": "^0.84.0",
50
- "@types/archiver": "^6.0.3",
49
+ "@effect/language-service": "^0.86.0",
50
+ "@types/archiver": "^6.0.4",
51
51
  esbuild: "^0.28.0",
52
52
  typescript: "catalog:"
53
53
  },
@@ -90,6 +90,25 @@ var TokenExchangeError = class extends Schema.TaggedError()("TokenExchangeError"
90
90
  statusCode: Schema.optional(Schema.Number)
91
91
  }) {
92
92
  };
93
+ var DeviceAuthorizationError = class extends Schema.TaggedError()(
94
+ "DeviceAuthorizationError",
95
+ {
96
+ message: Schema.String,
97
+ cause: Schema.optional(Schema.String)
98
+ }
99
+ ) {
100
+ };
101
+ var AuthorizationDeniedError = class extends Schema.TaggedError()(
102
+ "AuthorizationDeniedError",
103
+ {
104
+ message: Schema.String
105
+ }
106
+ ) {
107
+ };
108
+ var DeviceCodeExpiredError = class extends Schema.TaggedError()("DeviceCodeExpiredError", {
109
+ message: Schema.String
110
+ }) {
111
+ };
93
112
  var TokenRefreshError = class extends Schema.TaggedError()("TokenRefreshError", {
94
113
  message: Schema.String,
95
114
  statusCode: Schema.optional(Schema.Number)
@@ -135,6 +154,31 @@ var ApplicationNotFoundError = class extends Schema.TaggedError()(
135
154
  }
136
155
  ) {
137
156
  };
157
+ var EnvFileReadError = class extends Schema.TaggedError()("EnvFileReadError", {
158
+ message: Schema.String,
159
+ cause: Schema.optional(Schema.String)
160
+ }) {
161
+ };
162
+ var EnvFileWriteError = class extends Schema.TaggedError()("EnvFileWriteError", {
163
+ message: Schema.String,
164
+ cause: Schema.optional(Schema.String)
165
+ }) {
166
+ };
167
+ var DevCredentialsStoreReadError = class extends Schema.TaggedError()(
168
+ "DevCredentialsStoreReadError",
169
+ {
170
+ message: Schema.String
171
+ }
172
+ ) {
173
+ };
174
+ var DevCredentialsStoreWriteError = class extends Schema.TaggedError()(
175
+ "DevCredentialsStoreWriteError",
176
+ {
177
+ message: Schema.String,
178
+ cause: Schema.optional(Schema.String)
179
+ }
180
+ ) {
181
+ };
138
182
  var ConfigNotFoundError = class extends Schema.TaggedError()("ConfigNotFoundError", {
139
183
  message: Schema.String,
140
184
  directory: Schema.String
@@ -261,16 +305,10 @@ import { ApiError, GigadriveClient } from "@gigadrive/sdk";
261
305
  import { Config as Config2, Effect as Effect4 } from "effect";
262
306
 
263
307
  // src/services/auth.ts
264
- import { Console, Effect as Effect3, Option as Option2 } from "effect";
265
- import * as crypto from "crypto";
266
- import * as http from "http";
308
+ import { Console, Duration, Effect as Effect3, Option as Option2, Schema as Schema5 } from "effect";
309
+ import process3 from "process";
267
310
  import open from "open";
268
311
 
269
- // src/services/auth-storage.ts
270
- import { FileSystem, Path } from "@effect/platform";
271
- import { Effect, Option, Schema as Schema3 } from "effect";
272
- import * as os from "os";
273
-
274
312
  // src/domain.ts
275
313
  import { Schema as Schema2 } from "effect";
276
314
  var DeploymentId = Schema2.String.pipe(Schema2.brand("@Gigadrive/DeploymentId"));
@@ -288,9 +326,18 @@ var OAuthClientConfig = Schema2.Struct({
288
326
  issuer: Schema2.String,
289
327
  authorizeUrl: Schema2.String,
290
328
  tokenUrl: Schema2.String,
329
+ deviceAuthorizeUrl: Schema2.String,
291
330
  scope: Schema2.String,
292
331
  userinfoUrl: Schema2.String
293
332
  });
333
+ var DeviceAuthorizationResponse = Schema2.Struct({
334
+ device_code: Schema2.String,
335
+ user_code: Schema2.String,
336
+ verification_uri: Schema2.String,
337
+ verification_uri_complete: Schema2.optional(Schema2.String),
338
+ expires_in: Schema2.Number,
339
+ interval: Schema2.optional(Schema2.Number)
340
+ });
294
341
  var StoredAuthData = Schema2.Struct({
295
342
  refreshToken: Schema2.String,
296
343
  accessToken: Schema2.optional(Schema2.String),
@@ -300,8 +347,52 @@ var ProjectLink = Schema2.Struct({
300
347
  applicationId: Schema2.String,
301
348
  organizationId: Schema2.optional(Schema2.String)
302
349
  });
350
+ var DevKeyEntry = Schema2.Struct({
351
+ apiKeyId: Schema2.String
352
+ });
353
+ var DevKeyStore = Schema2.Record({ key: Schema2.String, value: DevKeyEntry });
354
+
355
+ // src/lib/clipboard.ts
356
+ import { spawn } from "child_process";
357
+ import process2 from "process";
358
+ var clipboardCandidates = () => {
359
+ switch (process2.platform) {
360
+ case "darwin":
361
+ return [{ cmd: "pbcopy", args: [] }];
362
+ case "win32":
363
+ return [{ cmd: "clip", args: [] }];
364
+ default:
365
+ return [
366
+ { cmd: "wl-copy", args: [] },
367
+ { cmd: "xclip", args: ["-selection", "clipboard"] },
368
+ { cmd: "xsel", args: ["--clipboard", "--input"] }
369
+ ];
370
+ }
371
+ };
372
+ function writeClipboard(text) {
373
+ const candidates = clipboardCandidates();
374
+ const tryNext = (index) => {
375
+ if (index >= candidates.length) return;
376
+ const { cmd, args } = candidates[index];
377
+ let proc;
378
+ try {
379
+ proc = spawn(cmd, args, { stdio: ["pipe", "ignore", "ignore"] });
380
+ } catch {
381
+ tryNext(index + 1);
382
+ return;
383
+ }
384
+ proc.on("error", () => tryNext(index + 1));
385
+ proc.stdin?.on("error", () => {
386
+ });
387
+ proc.stdin?.end(text);
388
+ };
389
+ tryNext(0);
390
+ }
303
391
 
304
392
  // src/services/auth-storage.ts
393
+ import { FileSystem, Path } from "@effect/platform";
394
+ import { Effect, Option, Schema as Schema3 } from "effect";
395
+ import * as os from "os";
305
396
  var AuthStorageService = class extends Effect.Service()("AuthStorageService", {
306
397
  accessors: true,
307
398
  effect: Effect.gen(function* () {
@@ -380,15 +471,46 @@ var AuthStorageService = class extends Effect.Service()("AuthStorageService", {
380
471
  import { Config, Effect as Effect2, Schema as Schema4 } from "effect";
381
472
  var OAuthEnvConfig = Config.all({
382
473
  issuerUrl: Config.string("GIGADRIVE_NETWORK_OAUTH_ISSUER_URL").pipe(Config.withDefault("https://idp.gigadrive.de")),
383
- clientId: Config.string("GIGADRIVE_NETWORK_OAUTH_CLIENT_ID").pipe(Config.withDefault("todo_add_client_id"))
474
+ // Default is the first-party CLI's public OAuth client (seeded in the Network
475
+ // repo). Override with GIGADRIVE_NETWORK_OAUTH_CLIENT_ID per environment.
476
+ clientId: Config.string("GIGADRIVE_NETWORK_OAUTH_CLIENT_ID").pipe(
477
+ Config.withDefault("0195c11c-0000-7000-8000-000000000001")
478
+ )
384
479
  });
385
480
  var OpenIdDiscoveryDocument = Schema4.Struct({
386
481
  issuer: Schema4.optional(Schema4.String),
387
482
  authorization_endpoint: Schema4.optional(Schema4.String),
388
483
  token_endpoint: Schema4.optional(Schema4.String),
389
- userinfo_endpoint: Schema4.optional(Schema4.String)
484
+ userinfo_endpoint: Schema4.optional(Schema4.String),
485
+ device_authorization_endpoint: Schema4.optional(Schema4.String)
390
486
  });
391
- var DEFAULT_SCOPE = "offline_access openid profile email";
487
+ var CLI_SCOPES = [
488
+ // Identity / OIDC
489
+ "offline_access",
490
+ "openid",
491
+ "profile",
492
+ "email",
493
+ // Network API capabilities
494
+ "network:applications:read",
495
+ "network:env_vars:read",
496
+ "network:env_vars:write",
497
+ "network:env_vars:delete",
498
+ "network:deployments:read",
499
+ "network:deployments:write",
500
+ "network:deployments:trigger",
501
+ "network:ai_gateway:chat",
502
+ "network:ai_gateway:models",
503
+ "network:ai_gateway:usage:read",
504
+ "network:ai_gateway:budgets:read",
505
+ "network:ai_gateway:budgets:write",
506
+ "network:ai_gateway:policies:read",
507
+ "network:ai_gateway:policies:write",
508
+ // Provisioning local-dev API credentials
509
+ "platform:api_keys:read",
510
+ "platform:api_keys:write",
511
+ "platform:api_keys:delete"
512
+ ];
513
+ var DEFAULT_SCOPE = CLI_SCOPES.join(" ");
392
514
  var OAuthConfigService = class extends Effect2.Service()("OAuthConfigService", {
393
515
  accessors: true,
394
516
  effect: Effect2.gen(function* () {
@@ -426,7 +548,7 @@ var OAuthConfigService = class extends Effect2.Service()("OAuthConfigService", {
426
548
  })
427
549
  )
428
550
  );
429
- if (!decoded.authorization_endpoint || !decoded.token_endpoint || !decoded.userinfo_endpoint) {
551
+ if (!decoded.authorization_endpoint || !decoded.token_endpoint || !decoded.userinfo_endpoint || !decoded.device_authorization_endpoint) {
430
552
  return yield* Effect2.fail(
431
553
  new OAuthDiscoveryError({
432
554
  message: "OIDC discovery returned incomplete endpoints"
@@ -438,6 +560,7 @@ var OAuthConfigService = class extends Effect2.Service()("OAuthConfigService", {
438
560
  issuer: decoded.issuer ?? issuerBase,
439
561
  authorizeUrl: decoded.authorization_endpoint,
440
562
  tokenUrl: decoded.token_endpoint,
563
+ deviceAuthorizeUrl: decoded.device_authorization_endpoint,
441
564
  scope: DEFAULT_SCOPE,
442
565
  userinfoUrl: decoded.userinfo_endpoint
443
566
  };
@@ -451,14 +574,9 @@ var OAuthConfigService = class extends Effect2.Service()("OAuthConfigService", {
451
574
  };
452
575
 
453
576
  // src/services/auth.ts
454
- var escapeHtml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/\//g, "&#x2F;");
455
- var base64URLEncode = (buffer) => buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
456
- var generatePKCE = () => {
457
- const codeVerifier = base64URLEncode(crypto.randomBytes(64));
458
- const challengeBytes = crypto.createHash("sha256").update(codeVerifier, "utf8").digest();
459
- const codeChallenge = base64URLEncode(challengeBytes);
460
- return { codeVerifier, codeChallenge };
461
- };
577
+ var DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
578
+ var DEFAULT_POLL_INTERVAL_SECONDS = 5;
579
+ var REQUEST_TIMEOUT_SECONDS = 30;
462
580
  var AuthService = class extends Effect3.Service()("AuthService", {
463
581
  accessors: true,
464
582
  dependencies: [OAuthConfigService.Default, AuthStorageService.Default],
@@ -473,115 +591,178 @@ var AuthService = class extends Effect3.Service()("AuthService", {
473
591
  yield* storage.remove;
474
592
  yield* Effect3.log("Logged out");
475
593
  });
476
- const exchangeCodeForTokens = (config, authCode, redirectUri, codeVerifier) => Effect3.gen(function* () {
594
+ const requestDeviceAuthorization = (config) => Effect3.gen(function* () {
477
595
  const response = yield* Effect3.tryPromise({
478
- try: () => fetch(config.tokenUrl, {
596
+ try: (signal) => fetch(config.deviceAuthorizeUrl, {
479
597
  method: "POST",
480
598
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
481
- body: new URLSearchParams({
482
- grant_type: "authorization_code",
483
- client_id: config.clientId,
484
- code: authCode,
485
- redirect_uri: redirectUri,
486
- code_verifier: codeVerifier
487
- }).toString()
599
+ body: new URLSearchParams({ client_id: config.clientId, scope: config.scope }).toString(),
600
+ signal
488
601
  }),
489
- catch: (error) => new TokenExchangeError({
490
- message: `Token exchange request failed: ${error instanceof Error ? error.message : String(error)}`
602
+ catch: (error) => new DeviceAuthorizationError({
603
+ message: `Device authorization request failed: ${error instanceof Error ? error.message : String(error)}`
491
604
  })
492
605
  });
493
606
  if (!response.ok) {
494
607
  const body = yield* Effect3.tryPromise({
495
608
  try: () => response.text(),
496
- catch: () => new TokenExchangeError({ message: "Failed to read token exchange error response" })
609
+ catch: () => new DeviceAuthorizationError({ message: "Failed to read device authorization error response" })
497
610
  });
498
611
  return yield* Effect3.fail(
499
- new TokenExchangeError({
500
- message: `Token exchange failed: ${response.status} ${response.statusText} - ${body}`,
501
- statusCode: response.status
612
+ new DeviceAuthorizationError({
613
+ message: `Device authorization failed: ${response.status} ${response.statusText} - ${body}`
502
614
  })
503
615
  );
504
616
  }
505
- const tokens = yield* Effect3.tryPromise({
617
+ const json = yield* Effect3.tryPromise({
506
618
  try: () => response.json(),
507
- catch: () => new TokenExchangeError({ message: "Failed to parse token exchange response" })
619
+ catch: () => new DeviceAuthorizationError({ message: "Failed to parse device authorization response" })
508
620
  });
509
- return tokens;
621
+ return yield* Schema5.decodeUnknown(DeviceAuthorizationResponse)(json).pipe(
622
+ Effect3.mapError(
623
+ () => new DeviceAuthorizationError({ message: "Invalid device authorization response schema" })
624
+ )
625
+ );
626
+ }).pipe(
627
+ Effect3.timeoutFail({
628
+ duration: Duration.seconds(REQUEST_TIMEOUT_SECONDS),
629
+ onTimeout: () => new DeviceAuthorizationError({ message: "Device authorization request timed out" })
630
+ })
631
+ );
632
+ const pollTokenOnce = (config, deviceCode) => Effect3.gen(function* () {
633
+ const response = yield* Effect3.tryPromise({
634
+ try: (signal) => fetch(config.tokenUrl, {
635
+ method: "POST",
636
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
637
+ body: new URLSearchParams({
638
+ grant_type: DEVICE_CODE_GRANT_TYPE,
639
+ client_id: config.clientId,
640
+ device_code: deviceCode
641
+ }).toString(),
642
+ signal
643
+ }),
644
+ catch: (error) => new TokenExchangeError({
645
+ message: `Token poll request failed: ${error instanceof Error ? error.message : String(error)}`
646
+ })
647
+ });
648
+ if (response.ok) {
649
+ const tokens = yield* Effect3.tryPromise({
650
+ try: () => response.json(),
651
+ catch: () => new TokenExchangeError({ message: "Failed to parse token response" })
652
+ });
653
+ return { kind: "tokens", tokens };
654
+ }
655
+ const errorBody = yield* Effect3.tryPromise({
656
+ try: () => response.json(),
657
+ catch: () => new TokenExchangeError({ message: "Failed to parse token poll error response" })
658
+ });
659
+ switch (errorBody.error) {
660
+ case "authorization_pending":
661
+ return { kind: "pending" };
662
+ case "slow_down":
663
+ return { kind: "slow_down" };
664
+ case "access_denied":
665
+ return { kind: "denied" };
666
+ case "expired_token":
667
+ return { kind: "expired" };
668
+ default:
669
+ return yield* Effect3.fail(
670
+ new TokenExchangeError({
671
+ message: `Token poll failed: ${response.status} ${response.statusText} - ${errorBody.error ?? "unknown_error"}`,
672
+ statusCode: response.status
673
+ })
674
+ );
675
+ }
510
676
  });
511
- const startCallbackServer = (config, codeChallenge, state) => Effect3.async((resume) => {
512
- const server = http.createServer();
513
- const redirectPath = "/callback";
514
- server.listen(0, "127.0.0.1", () => {
515
- const address = server.address();
516
- if (!address || typeof address !== "object") {
517
- server.close(() => {
518
- resume(Effect3.fail(new LoginFlowError({ message: "Failed to determine callback server address" })));
519
- });
520
- return;
677
+ const pollForToken = (config, device) => Effect3.gen(function* () {
678
+ const deadline = Date.now() + device.expires_in * 1e3;
679
+ let interval = device.interval ?? DEFAULT_POLL_INTERVAL_SECONDS;
680
+ while (true) {
681
+ yield* Effect3.sleep(Duration.seconds(interval));
682
+ if (Date.now() >= deadline) {
683
+ return yield* Effect3.fail(
684
+ new DeviceCodeExpiredError({
685
+ message: "The login code expired before it was approved. Please run `gigadrive login` again."
686
+ })
687
+ );
521
688
  }
522
- const port = address.port;
523
- const redirectUri = `http://127.0.0.1:${port}${redirectPath}`;
524
- const authUrl = new URL(config.authorizeUrl);
525
- authUrl.searchParams.append("client_id", config.clientId);
526
- authUrl.searchParams.append("redirect_uri", redirectUri);
527
- authUrl.searchParams.append("response_type", "code");
528
- authUrl.searchParams.append("scope", config.scope);
529
- authUrl.searchParams.append("code_challenge", codeChallenge);
530
- authUrl.searchParams.append("code_challenge_method", "S256");
531
- authUrl.searchParams.append("state", state);
532
- Console.log(`Please open this URL in your browser to log in:
533
- ${authUrl.toString()}
534
- `).pipe(Effect3.runSync);
535
- Console.log("Waiting for login callback...").pipe(Effect3.runSync);
536
- void open(authUrl.toString());
537
- server.on("request", (req, res) => {
538
- const reqUrl = new URL(req.url || "", redirectUri);
539
- if (reqUrl.pathname !== "/callback") {
540
- res.writeHead(404).end("Not Found");
541
- return;
542
- }
543
- const authCode = reqUrl.searchParams.get("code");
544
- const receivedState = reqUrl.searchParams.get("state");
545
- const oauthError = reqUrl.searchParams.get("error");
546
- res.writeHead(200, { "Content-Type": "text/html", Connection: "close" });
547
- if (oauthError) {
548
- const safeOauthError = escapeHtml(oauthError);
549
- res.end(`<h1>Login Failed!</h1><p>Error: ${safeOauthError}</p><p>You can close this tab.</p>`);
550
- server.close();
551
- resume(Effect3.fail(new LoginFlowError({ message: `OAuth error: ${oauthError}` })));
552
- return;
553
- }
554
- if (receivedState !== state) {
555
- res.end(
556
- "<h1>Login Failed!</h1><p>State mismatch. Possible CSRF attack.</p><p>You can close this tab.</p>"
689
+ const result = yield* pollTokenOnce(config, device.device_code).pipe(
690
+ // A hung poll must not stall the loop: on timeout treat it as pending so
691
+ // the next iteration re-checks the expiry deadline.
692
+ Effect3.timeoutTo({
693
+ duration: Duration.seconds(REQUEST_TIMEOUT_SECONDS),
694
+ onSuccess: (value) => value,
695
+ onTimeout: () => ({ kind: "pending" })
696
+ })
697
+ );
698
+ switch (result.kind) {
699
+ case "tokens":
700
+ return result.tokens;
701
+ case "pending":
702
+ continue;
703
+ case "slow_down":
704
+ interval += 5;
705
+ continue;
706
+ case "denied":
707
+ return yield* Effect3.fail(new AuthorizationDeniedError({ message: "The login request was denied." }));
708
+ case "expired":
709
+ return yield* Effect3.fail(
710
+ new DeviceCodeExpiredError({
711
+ message: "The login code expired before it was approved. Please run `gigadrive login` again."
712
+ })
557
713
  );
558
- server.close();
559
- resume(Effect3.fail(new LoginFlowError({ message: "State mismatch" })));
560
- return;
561
- }
562
- if (authCode) {
563
- res.end("<h1>Login Successful!</h1><p>You can close this tab.</p>");
564
- server.close();
565
- resume(Effect3.succeed({ authCode, redirectUri }));
566
- } else {
567
- res.end("<h1>Login Failed!</h1><p>No authorization code received.</p><p>You can close this tab.</p>");
568
- server.close();
569
- resume(Effect3.fail(new LoginFlowError({ message: "No authorization code received" })));
570
- }
571
- });
572
- server.on("error", (err) => {
573
- server.close();
574
- resume(Effect3.fail(new LoginFlowError({ message: `Local server error: ${err.message}` })));
575
- });
714
+ }
715
+ }
716
+ });
717
+ const watchCopyKey = (url) => Effect3.async((resume) => {
718
+ const stdin = process3.stdin;
719
+ if (!stdin.isTTY) {
720
+ return;
721
+ }
722
+ stdin.setRawMode(true);
723
+ stdin.resume();
724
+ stdin.setEncoding("utf8");
725
+ const onData = (key) => {
726
+ if (key === "c" || key === "C") {
727
+ writeClipboard(url);
728
+ Console.log("Copied the login URL to your clipboard.").pipe(Effect3.runSync);
729
+ } else if (key === "") {
730
+ resume(Effect3.fail(new LoginFlowError({ message: "Login canceled." })));
731
+ }
732
+ };
733
+ stdin.on("data", onData);
734
+ return Effect3.sync(() => {
735
+ stdin.off("data", onData);
736
+ if (stdin.isTTY) stdin.setRawMode(false);
737
+ stdin.pause();
576
738
  });
577
739
  });
578
740
  const login = Effect3.gen(function* () {
579
741
  yield* logout;
580
- const { codeVerifier, codeChallenge } = generatePKCE();
581
- const state = crypto.randomBytes(16).toString("hex");
582
742
  const config = yield* oauthConfig.getConfig;
583
- const { authCode, redirectUri } = yield* startCallbackServer(config, codeChallenge, state);
584
- const tokens = yield* exchangeCodeForTokens(config, authCode, redirectUri, codeVerifier);
743
+ const device = yield* requestDeviceAuthorization(config);
744
+ const verificationUri = device.verification_uri;
745
+ const verificationUriComplete = device.verification_uri_complete ?? verificationUri;
746
+ const interactive = process3.stdin.isTTY === true;
747
+ yield* Console.log(
748
+ `
749
+ To sign in, visit:
750
+
751
+ ${verificationUri}
752
+
753
+ and enter the code:
754
+
755
+ ${device.user_code}
756
+ `
757
+ );
758
+ if (interactive) {
759
+ yield* Effect3.tryPromise(() => open(verificationUriComplete)).pipe(Effect3.catchAll(() => Effect3.void));
760
+ yield* Console.log("We tried to open your browser automatically. If it did not open, use the URL above.");
761
+ yield* Console.log('Press "c" to copy the URL to your clipboard, or Ctrl+C to cancel.');
762
+ }
763
+ yield* Console.log("\nWaiting for you to approve the login\u2026");
764
+ const poll = pollForToken(config, device);
765
+ const tokens = yield* interactive ? poll.pipe(Effect3.raceFirst(watchCopyKey(verificationUriComplete))) : poll;
585
766
  if (!tokens.refresh_token) {
586
767
  return yield* Effect3.fail(
587
768
  new TokenExchangeError({ message: "No refresh token received from identity provider" })
@@ -776,7 +957,7 @@ var ApiClientService = class extends Effect4.Service()("ApiClientService", {
776
957
 
777
958
  // src/services/project-link.ts
778
959
  import { FileSystem as FileSystem2, Path as Path2 } from "@effect/platform";
779
- import { Effect as Effect5, Option as Option3, Schema as Schema5 } from "effect";
960
+ import { Effect as Effect5, Option as Option3, Schema as Schema6 } from "effect";
780
961
  var LINK_DIR = ".gigadrive";
781
962
  var LINK_FILE = "project.json";
782
963
  var ProjectLinkService = class extends Effect5.Service()("ProjectLinkService", {
@@ -795,7 +976,7 @@ var ProjectLinkService = class extends Effect5.Service()("ProjectLinkService", {
795
976
  try: () => JSON.parse(data),
796
977
  catch: () => new ProjectLinkReadError({ message: `${LINK_DIR}/${LINK_FILE} contains invalid JSON` })
797
978
  });
798
- const link = yield* Schema5.decodeUnknown(ProjectLink)(json).pipe(
979
+ const link = yield* Schema6.decodeUnknown(ProjectLink)(json).pipe(
799
980
  Effect5.mapError(() => new ProjectLinkReadError({ message: `${LINK_DIR}/${LINK_FILE} has an invalid schema` }))
800
981
  );
801
982
  return Option3.some(link);
@@ -1539,8 +1720,231 @@ var deploymentsCommand = deploymentsBase.pipe(
1539
1720
  );
1540
1721
 
1541
1722
  // src/commands/env/index.ts
1542
- import { Args as Args3, Command as Command7, Options as Options4 } from "@effect/cli";
1543
- import { Console as Console7, Effect as Effect14, Option as Option7 } from "effect";
1723
+ import { Args as Args3, Command as Command7, Options as Options4, Prompt } from "@effect/cli";
1724
+ import { Console as Console7, Effect as Effect17, Option as Option10 } from "effect";
1725
+
1726
+ // src/services/env-file.ts
1727
+ import { FileSystem as FileSystem6, Path as Path5 } from "@effect/platform";
1728
+ import { Effect as Effect14, Option as Option7 } from "effect";
1729
+
1730
+ // src/lib/dotenv.ts
1731
+ var DEFAULT_ENV_HEADER = "# Generated by `gigadrive env pull` \u2014 do not edit by hand.";
1732
+ var UNQUOTED_SAFE = /^[A-Za-z0-9_.\-/@:+]*$/;
1733
+ var formatEnvValue = (value) => {
1734
+ if (UNQUOTED_SAFE.test(value)) {
1735
+ return value;
1736
+ }
1737
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
1738
+ return `"${escaped}"`;
1739
+ };
1740
+ var serializeEnv = (entries, opts) => {
1741
+ const header = opts?.header ?? DEFAULT_ENV_HEADER;
1742
+ if (entries.length === 0) {
1743
+ return `${header}
1744
+ `;
1745
+ }
1746
+ const body = entries.map((e) => `${e.key}=${formatEnvValue(e.value)}`).join("\n");
1747
+ return `${header}
1748
+ ${body}
1749
+ `;
1750
+ };
1751
+ var parseEnv = (content) => {
1752
+ const result = {};
1753
+ for (const rawLine of content.split("\n")) {
1754
+ const line = rawLine.trim();
1755
+ if (line.length === 0 || line.startsWith("#")) {
1756
+ continue;
1757
+ }
1758
+ const eq = line.indexOf("=");
1759
+ if (eq <= 0) {
1760
+ continue;
1761
+ }
1762
+ const key = line.slice(0, eq).trim();
1763
+ let value = line.slice(eq + 1).trim();
1764
+ if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
1765
+ value = value.slice(1, -1).replace(/\\(\\|"|n|r|t)/g, (_, ch) => {
1766
+ switch (ch) {
1767
+ case "n":
1768
+ return "\n";
1769
+ case "r":
1770
+ return "\r";
1771
+ case "t":
1772
+ return " ";
1773
+ case '"':
1774
+ return '"';
1775
+ default:
1776
+ return "\\";
1777
+ }
1778
+ });
1779
+ }
1780
+ result[key] = value;
1781
+ }
1782
+ return result;
1783
+ };
1784
+
1785
+ // src/services/env-file.ts
1786
+ var EnvFileService = class extends Effect14.Service()("EnvFileService", {
1787
+ accessors: true,
1788
+ effect: Effect14.gen(function* () {
1789
+ const fs = yield* FileSystem6.FileSystem;
1790
+ const pathService = yield* Path5.Path;
1791
+ const exists = Effect14.fn("EnvFileService.exists")(function* (filePath) {
1792
+ return yield* fs.exists(filePath).pipe(Effect14.mapError(() => new EnvFileReadError({ message: `Failed to check whether ${filePath} exists` })));
1793
+ });
1794
+ const read = Effect14.fn("EnvFileService.read")(function* (filePath) {
1795
+ const present = yield* exists(filePath);
1796
+ if (!present) return Option7.none();
1797
+ const contents = yield* fs.readFileString(filePath, "utf8").pipe(Effect14.mapError(() => new EnvFileReadError({ message: `Failed to read ${filePath}` })));
1798
+ return Option7.some(contents);
1799
+ });
1800
+ const write = Effect14.fn("EnvFileService.write")(function* (filePath, entries, opts) {
1801
+ const dir = pathService.dirname(filePath);
1802
+ yield* fs.makeDirectory(dir, { recursive: true }).pipe(
1803
+ Effect14.mapError(
1804
+ (error) => new EnvFileWriteError({ message: `Failed to create directory for ${filePath}`, cause: String(error) })
1805
+ )
1806
+ );
1807
+ yield* fs.writeFileString(filePath, serializeEnv(entries, opts), { mode: 384 }).pipe(
1808
+ Effect14.mapError(
1809
+ (error) => new EnvFileWriteError({ message: `Failed to write ${filePath}`, cause: String(error) })
1810
+ )
1811
+ );
1812
+ yield* fs.chmod(filePath, 384).pipe(
1813
+ Effect14.mapError(
1814
+ (error) => new EnvFileWriteError({ message: `Failed to secure ${filePath}`, cause: String(error) })
1815
+ )
1816
+ );
1817
+ });
1818
+ const ensureGitignored = Effect14.fn("EnvFileService.ensureGitignored")(function* (dir, entry) {
1819
+ const gitignorePath = pathService.join(dir, ".gitignore");
1820
+ const existing = yield* read(gitignorePath);
1821
+ const current = Option7.getOrElse(existing, () => "");
1822
+ const alreadyIgnored = current.split("\n").some((line) => line.trim() === entry);
1823
+ if (alreadyIgnored) {
1824
+ return false;
1825
+ }
1826
+ const separator = current.length > 0 && !current.endsWith("\n") ? "\n" : "";
1827
+ yield* fs.writeFileString(gitignorePath, `${current}${separator}${entry}
1828
+ `).pipe(
1829
+ Effect14.mapError(
1830
+ (error) => new EnvFileWriteError({ message: "Failed to update .gitignore", cause: String(error) })
1831
+ )
1832
+ );
1833
+ return true;
1834
+ });
1835
+ return { exists, read, write, ensureGitignored };
1836
+ })
1837
+ }) {
1838
+ };
1839
+
1840
+ // src/services/local-credentials.ts
1841
+ import { Config as Config3, Effect as Effect16, Option as Option9 } from "effect";
1842
+ import * as os3 from "os";
1843
+
1844
+ // src/services/dev-credentials-store.ts
1845
+ import { FileSystem as FileSystem7, Path as Path6 } from "@effect/platform";
1846
+ import { Effect as Effect15, Option as Option8, Schema as Schema7 } from "effect";
1847
+ import * as os2 from "os";
1848
+ var DevCredentialsStore = class extends Effect15.Service()("DevCredentialsStore", {
1849
+ accessors: true,
1850
+ effect: Effect15.gen(function* () {
1851
+ const fs = yield* FileSystem7.FileSystem;
1852
+ const pathService = yield* Path6.Path;
1853
+ const homeDir = os2.homedir();
1854
+ const storeDir = pathService.join(homeDir, ".gigadrive");
1855
+ const storeFile = pathService.join(storeDir, "dev-keys.json");
1856
+ const ensureDirectory = Effect15.gen(function* () {
1857
+ yield* fs.makeDirectory(storeDir, { recursive: true }).pipe(
1858
+ Effect15.mapError(
1859
+ (error) => new DevCredentialsStoreWriteError({ message: "Failed to create ~/.gigadrive", cause: String(error) })
1860
+ )
1861
+ );
1862
+ yield* fs.chmod(storeDir, 448).pipe(
1863
+ Effect15.mapError(
1864
+ (error) => new DevCredentialsStoreWriteError({
1865
+ message: "Failed to set ~/.gigadrive permissions",
1866
+ cause: String(error)
1867
+ })
1868
+ )
1869
+ );
1870
+ });
1871
+ const load = Effect15.gen(function* () {
1872
+ const present = yield* fs.exists(storeFile).pipe(Effect15.mapError(() => new DevCredentialsStoreReadError({ message: "Failed to check dev-keys.json" })));
1873
+ if (!present) return {};
1874
+ const data = yield* fs.readFileString(storeFile, "utf8").pipe(Effect15.mapError(() => new DevCredentialsStoreReadError({ message: "Failed to read dev-keys.json" })));
1875
+ const json = yield* Effect15.try({
1876
+ try: () => JSON.parse(data),
1877
+ catch: () => new DevCredentialsStoreReadError({ message: "dev-keys.json contains invalid JSON" })
1878
+ });
1879
+ return yield* Schema7.decodeUnknown(DevKeyStore)(json).pipe(
1880
+ Effect15.mapError(() => new DevCredentialsStoreReadError({ message: "dev-keys.json has an invalid schema" }))
1881
+ );
1882
+ });
1883
+ const get = Effect15.fn("DevCredentialsStore.get")(function* (applicationId) {
1884
+ const store = yield* load;
1885
+ const entry = store[applicationId];
1886
+ return entry ? Option8.some(entry) : Option8.none();
1887
+ });
1888
+ const set = Effect15.fn("DevCredentialsStore.set")(function* (applicationId, entry) {
1889
+ const store = yield* load;
1890
+ const next = { ...store, [applicationId]: entry };
1891
+ yield* ensureDirectory;
1892
+ yield* fs.writeFileString(storeFile, `${JSON.stringify(next, null, 2)}
1893
+ `, { mode: 384 }).pipe(
1894
+ Effect15.mapError(
1895
+ (error) => new DevCredentialsStoreWriteError({ message: "Failed to write dev-keys.json", cause: String(error) })
1896
+ )
1897
+ );
1898
+ });
1899
+ return { get, set };
1900
+ })
1901
+ }) {
1902
+ };
1903
+
1904
+ // src/services/local-credentials.ts
1905
+ var DEV_KEY_SCOPES = ["network:env_vars:read"];
1906
+ var ApiBaseUrl2 = Config3.string("GIGADRIVE_API_BASE_URL").pipe(Config3.withDefault("https://api.gigadrive.network"));
1907
+ var credentialEntries = (clientId, clientSecret, baseUrl) => [
1908
+ { key: "GIGADRIVE_CLIENT_ID", value: clientId },
1909
+ { key: "GIGADRIVE_CLIENT_SECRET", value: clientSecret },
1910
+ { key: "GIGADRIVE_API_BASE_URL", value: baseUrl }
1911
+ ];
1912
+ var LocalCredentialsService = class extends Effect16.Service()("LocalCredentialsService", {
1913
+ accessors: true,
1914
+ dependencies: [ApiClientService.Default, DevCredentialsStore.Default],
1915
+ effect: Effect16.gen(function* () {
1916
+ const apiClient = yield* ApiClientService;
1917
+ const store = yield* DevCredentialsStore;
1918
+ const baseUrl = yield* ApiBaseUrl2;
1919
+ const keyName = (applicationId) => `cli-dev:${os3.hostname()}:${applicationId}`;
1920
+ const provision = Effect16.fn("LocalCredentialsService.provision")(function* (input) {
1921
+ const stored = yield* store.get(input.applicationId);
1922
+ const existing = input.existingContent ? parseEnv(input.existingContent) : {};
1923
+ const existingClientId = existing["GIGADRIVE_CLIENT_ID"];
1924
+ const existingClientSecret = existing["GIGADRIVE_CLIENT_SECRET"];
1925
+ const canReuse = !input.rotate && existingClientId !== void 0 && existingClientSecret !== void 0 && Option9.isSome(stored) && stored.value.apiKeyId === existingClientId;
1926
+ if (canReuse) {
1927
+ return credentialEntries(existingClientId, existingClientSecret, baseUrl);
1928
+ }
1929
+ if (Option9.isSome(stored)) {
1930
+ yield* apiClient.request((client) => client.apiKeys.delete(stored.value.apiKeyId)).pipe(Effect16.catchTag("ApiRequestError", () => Effect16.void));
1931
+ }
1932
+ const created = yield* apiClient.request(
1933
+ (client) => client.apiKeys.create({
1934
+ name: keyName(input.applicationId),
1935
+ applicationId: input.applicationId,
1936
+ scopes: DEV_KEY_SCOPES
1937
+ })
1938
+ );
1939
+ yield* store.set(input.applicationId, { apiKeyId: created.id });
1940
+ return credentialEntries(created.id, created.secret, baseUrl);
1941
+ });
1942
+ return { provision };
1943
+ })
1944
+ }) {
1945
+ };
1946
+
1947
+ // src/commands/env/index.ts
1544
1948
  var appOption3 = Options4.text("app").pipe(
1545
1949
  Options4.withAlias("a"),
1546
1950
  Options4.withDescription("Operate on this application (overrides the linked application)"),
@@ -1563,21 +1967,40 @@ var keyValueArg = Args3.text({ name: "key=value" }).pipe(
1563
1967
  Args3.withDescription("Variable assignment, e.g. DATABASE_URL=postgres://user:pass@host/db")
1564
1968
  );
1565
1969
  var keyOrIdArg = Args3.text({ name: "key-or-id" }).pipe(Args3.withDescription("Variable key or ID to remove"));
1566
- var resolveScope = (app, org) => Effect14.gen(function* () {
1567
- if (Option7.isSome(org)) {
1970
+ var fileArg = Args3.text({ name: "file" }).pipe(
1971
+ Args3.withDescription("Output file to write (default: .env.local)"),
1972
+ Args3.optional
1973
+ );
1974
+ var environmentOption = Options4.text("environment").pipe(
1975
+ Options4.withAlias("e"),
1976
+ Options4.withDescription("Environment slug to resolve overrides for (e.g. preview). Omit for the local baseline"),
1977
+ Options4.optional
1978
+ );
1979
+ var yesOption = Options4.boolean("yes").pipe(
1980
+ Options4.withAlias("y"),
1981
+ Options4.withDescription("Overwrite an existing file without confirmation")
1982
+ );
1983
+ var withCredentialsOption = Options4.boolean("with-credentials").pipe(
1984
+ Options4.withDescription("Also provision API-key credentials so the local app can call the API as this application")
1985
+ );
1986
+ var rotateOption = Options4.boolean("rotate").pipe(
1987
+ Options4.withDescription("Force a fresh API key when provisioning credentials, revoking the previous one")
1988
+ );
1989
+ var resolveScope = (app, org) => Effect17.gen(function* () {
1990
+ if (Option10.isSome(org)) {
1568
1991
  return { kind: "organization", id: org.value };
1569
1992
  }
1570
1993
  const projectLink = yield* ProjectLinkService;
1571
- const id = yield* Option7.match(app, {
1572
- onSome: (value) => Effect14.succeed(value),
1573
- onNone: () => projectLink.resolve(process.cwd()).pipe(Effect14.map((link) => link.applicationId))
1994
+ const id = yield* Option10.match(app, {
1995
+ onSome: (value) => Effect17.succeed(value),
1996
+ onNone: () => projectLink.resolve(process.cwd()).pipe(Effect17.map((link) => link.applicationId))
1574
1997
  });
1575
1998
  return { kind: "application", id };
1576
1999
  });
1577
2000
  var envListCommand = Command7.make(
1578
2001
  "list",
1579
2002
  { app: appOption3, org: orgOption4 },
1580
- ({ app, org }) => Effect14.gen(function* () {
2003
+ ({ app, org }) => Effect17.gen(function* () {
1581
2004
  const apiClient = yield* ApiClientService;
1582
2005
  const scope = yield* resolveScope(app, org);
1583
2006
  const { items } = yield* apiClient.request(
@@ -1591,23 +2014,23 @@ var envListCommand = Command7.make(
1591
2014
  yield* Console7.log(`${v.key}=${v.sensitive ? "***" : v.value ?? ""}`);
1592
2015
  }
1593
2016
  }).pipe(
1594
- Effect14.catchTags({
2017
+ Effect17.catchTags({
1595
2018
  NotAuthenticatedError: (err) => Console7.error('You are not logged in. Run "gigadrive login" to authenticate.').pipe(
1596
- Effect14.andThen(Effect14.fail(err))
2019
+ Effect17.andThen(Effect17.fail(err))
1597
2020
  ),
1598
- ApiRequestError: (err) => Console7.error(`Failed to list environment variables: ${err.message}`).pipe(Effect14.andThen(Effect14.fail(err))),
1599
- ProjectNotLinkedError: (err) => Console7.error(err.message).pipe(Effect14.andThen(Effect14.fail(err))),
1600
- ProjectLinkReadError: (err) => Console7.error(err.message).pipe(Effect14.andThen(Effect14.fail(err)))
2021
+ ApiRequestError: (err) => Console7.error(`Failed to list environment variables: ${err.message}`).pipe(Effect17.andThen(Effect17.fail(err))),
2022
+ ProjectNotLinkedError: (err) => Console7.error(err.message).pipe(Effect17.andThen(Effect17.fail(err))),
2023
+ ProjectLinkReadError: (err) => Console7.error(err.message).pipe(Effect17.andThen(Effect17.fail(err)))
1601
2024
  })
1602
2025
  )
1603
2026
  );
1604
2027
  var envSetCommand = Command7.make(
1605
2028
  "set",
1606
2029
  { keyValue: keyValueArg, app: appOption3, org: orgOption4, sensitive: sensitiveOption, env: envIdOption },
1607
- ({ keyValue, app, org, sensitive, env }) => Effect14.gen(function* () {
2030
+ ({ keyValue, app, org, sensitive, env }) => Effect17.gen(function* () {
1608
2031
  const separator = keyValue.indexOf("=");
1609
2032
  if (separator <= 0) {
1610
- return yield* Effect14.fail(
2033
+ return yield* Effect17.fail(
1611
2034
  new InvalidEnvVarFormatError({
1612
2035
  message: 'Invalid format. Use KEY=VALUE, e.g. "gigadrive env set DATABASE_URL=postgres://...".'
1613
2036
  })
@@ -1617,27 +2040,27 @@ var envSetCommand = Command7.make(
1617
2040
  const value = keyValue.slice(separator + 1);
1618
2041
  const apiClient = yield* ApiClientService;
1619
2042
  const scope = yield* resolveScope(app, org);
1620
- const environmentId = Option7.getOrUndefined(env);
2043
+ const environmentId = Option10.getOrUndefined(env);
1621
2044
  const created = yield* apiClient.request(
1622
2045
  (client) => scope.kind === "organization" ? client.organizations.envVars.create(scope.id, { key, value, sensitive }) : client.applications.envVars.create(scope.id, { key, value, sensitive, environmentId })
1623
2046
  );
1624
2047
  yield* Console7.log(`Set ${created.key}.`);
1625
2048
  }).pipe(
1626
- Effect14.catchTags({
1627
- InvalidEnvVarFormatError: (err) => Console7.error(err.message).pipe(Effect14.andThen(Effect14.fail(err))),
2049
+ Effect17.catchTags({
2050
+ InvalidEnvVarFormatError: (err) => Console7.error(err.message).pipe(Effect17.andThen(Effect17.fail(err))),
1628
2051
  NotAuthenticatedError: (err) => Console7.error('You are not logged in. Run "gigadrive login" to authenticate.').pipe(
1629
- Effect14.andThen(Effect14.fail(err))
2052
+ Effect17.andThen(Effect17.fail(err))
1630
2053
  ),
1631
- ApiRequestError: (err) => Console7.error(`Failed to set environment variable: ${err.message}`).pipe(Effect14.andThen(Effect14.fail(err))),
1632
- ProjectNotLinkedError: (err) => Console7.error(err.message).pipe(Effect14.andThen(Effect14.fail(err))),
1633
- ProjectLinkReadError: (err) => Console7.error(err.message).pipe(Effect14.andThen(Effect14.fail(err)))
2054
+ ApiRequestError: (err) => Console7.error(`Failed to set environment variable: ${err.message}`).pipe(Effect17.andThen(Effect17.fail(err))),
2055
+ ProjectNotLinkedError: (err) => Console7.error(err.message).pipe(Effect17.andThen(Effect17.fail(err))),
2056
+ ProjectLinkReadError: (err) => Console7.error(err.message).pipe(Effect17.andThen(Effect17.fail(err)))
1634
2057
  })
1635
2058
  )
1636
2059
  );
1637
2060
  var envRmCommand = Command7.make(
1638
2061
  "rm",
1639
2062
  { keyOrId: keyOrIdArg, app: appOption3, org: orgOption4 },
1640
- ({ keyOrId, app, org }) => Effect14.gen(function* () {
2063
+ ({ keyOrId, app, org }) => Effect17.gen(function* () {
1641
2064
  const apiClient = yield* ApiClientService;
1642
2065
  const scope = yield* resolveScope(app, org);
1643
2066
  const { items } = yield* apiClient.request(
@@ -1645,7 +2068,7 @@ var envRmCommand = Command7.make(
1645
2068
  );
1646
2069
  const target = items.find((v) => v.id === keyOrId || v.key === keyOrId);
1647
2070
  if (target === void 0) {
1648
- return yield* Effect14.fail(
2071
+ return yield* Effect17.fail(
1649
2072
  new EnvVarNotFoundError({ message: `No environment variable matching "${keyOrId}".` })
1650
2073
  );
1651
2074
  }
@@ -1654,23 +2077,97 @@ var envRmCommand = Command7.make(
1654
2077
  );
1655
2078
  yield* Console7.log(`Removed ${target.key}.`);
1656
2079
  }).pipe(
1657
- Effect14.catchTags({
1658
- EnvVarNotFoundError: (err) => Console7.error(err.message).pipe(Effect14.andThen(Effect14.fail(err))),
2080
+ Effect17.catchTags({
2081
+ EnvVarNotFoundError: (err) => Console7.error(err.message).pipe(Effect17.andThen(Effect17.fail(err))),
2082
+ NotAuthenticatedError: (err) => Console7.error('You are not logged in. Run "gigadrive login" to authenticate.').pipe(
2083
+ Effect17.andThen(Effect17.fail(err))
2084
+ ),
2085
+ ApiRequestError: (err) => Console7.error(`Failed to remove environment variable: ${err.message}`).pipe(Effect17.andThen(Effect17.fail(err))),
2086
+ ProjectNotLinkedError: (err) => Console7.error(err.message).pipe(Effect17.andThen(Effect17.fail(err))),
2087
+ ProjectLinkReadError: (err) => Console7.error(err.message).pipe(Effect17.andThen(Effect17.fail(err)))
2088
+ })
2089
+ )
2090
+ );
2091
+ var envPullCommand = Command7.make(
2092
+ "pull",
2093
+ {
2094
+ file: fileArg,
2095
+ app: appOption3,
2096
+ environment: environmentOption,
2097
+ yes: yesOption,
2098
+ withCredentials: withCredentialsOption,
2099
+ rotate: rotateOption
2100
+ },
2101
+ ({ file, app, environment, yes, withCredentials, rotate }) => Effect17.gen(function* () {
2102
+ const apiClient = yield* ApiClientService;
2103
+ const projectLink = yield* ProjectLinkService;
2104
+ const envFile = yield* EnvFileService;
2105
+ const applicationId = yield* Option10.match(app, {
2106
+ onSome: (value) => Effect17.succeed(value),
2107
+ onNone: () => projectLink.resolve(process.cwd()).pipe(Effect17.map((link) => link.applicationId))
2108
+ });
2109
+ const target = Option10.getOrElse(file, () => ".env.local");
2110
+ const environmentSlug = Option10.getOrUndefined(environment);
2111
+ const { items, omittedSensitive } = yield* apiClient.request(
2112
+ (client) => client.applications.envVars.pull(applicationId, environmentSlug ? { environment: environmentSlug } : void 0)
2113
+ );
2114
+ const existing = yield* envFile.read(target);
2115
+ if (Option10.isSome(existing) && !yes) {
2116
+ const overwrite = yield* Prompt.run(
2117
+ Prompt.confirm({ message: `${target} already exists. Overwrite?`, initial: false })
2118
+ );
2119
+ if (!overwrite) {
2120
+ return yield* Console7.log("Cancelled.");
2121
+ }
2122
+ }
2123
+ let credentialEntries2 = [];
2124
+ if (withCredentials) {
2125
+ const localCredentials = yield* LocalCredentialsService;
2126
+ credentialEntries2 = yield* localCredentials.provision({
2127
+ applicationId,
2128
+ rotate,
2129
+ existingContent: Option10.getOrUndefined(existing)
2130
+ });
2131
+ }
2132
+ const entries = [...items.map((v) => ({ key: v.key, value: v.value })), ...credentialEntries2];
2133
+ yield* envFile.write(target, entries);
2134
+ const gitignored = yield* envFile.ensureGitignored(process.cwd(), target);
2135
+ yield* Console7.log(
2136
+ `Pulled ${items.length} environment variable(s)${withCredentials ? " and API credentials" : ""} to ${target}.`
2137
+ );
2138
+ if (omittedSensitive > 0) {
2139
+ yield* Console7.log(
2140
+ `Skipped ${omittedSensitive} sensitive variable(s); secrets live only in production/preview environments.`
2141
+ );
2142
+ }
2143
+ if (gitignored) {
2144
+ yield* Console7.log(`Added ${target} to .gitignore.`);
2145
+ }
2146
+ }).pipe(
2147
+ Effect17.catchTags({
2148
+ // Ctrl+C during the overwrite prompt is a deliberate cancel — exit 0.
2149
+ QuitException: () => Console7.log("Cancelled."),
1659
2150
  NotAuthenticatedError: (err) => Console7.error('You are not logged in. Run "gigadrive login" to authenticate.').pipe(
1660
- Effect14.andThen(Effect14.fail(err))
2151
+ Effect17.andThen(Effect17.fail(err))
1661
2152
  ),
1662
- ApiRequestError: (err) => Console7.error(`Failed to remove environment variable: ${err.message}`).pipe(Effect14.andThen(Effect14.fail(err))),
1663
- ProjectNotLinkedError: (err) => Console7.error(err.message).pipe(Effect14.andThen(Effect14.fail(err))),
1664
- ProjectLinkReadError: (err) => Console7.error(err.message).pipe(Effect14.andThen(Effect14.fail(err)))
2153
+ ApiRequestError: (err) => Console7.error(`Failed to pull environment variables: ${err.message}`).pipe(Effect17.andThen(Effect17.fail(err))),
2154
+ ProjectNotLinkedError: (err) => Console7.error(err.message).pipe(Effect17.andThen(Effect17.fail(err))),
2155
+ ProjectLinkReadError: (err) => Console7.error(err.message).pipe(Effect17.andThen(Effect17.fail(err))),
2156
+ EnvFileReadError: (err) => Console7.error(err.message).pipe(Effect17.andThen(Effect17.fail(err))),
2157
+ EnvFileWriteError: (err) => Console7.error(err.message).pipe(Effect17.andThen(Effect17.fail(err))),
2158
+ DevCredentialsStoreReadError: (err) => Console7.error(err.message).pipe(Effect17.andThen(Effect17.fail(err))),
2159
+ DevCredentialsStoreWriteError: (err) => Console7.error(err.message).pipe(Effect17.andThen(Effect17.fail(err)))
1665
2160
  })
1666
2161
  )
1667
2162
  );
1668
- var envBase = Command7.make("env", {}, () => Effect14.void);
1669
- var envCommand = envBase.pipe(Command7.withSubcommands([envListCommand, envSetCommand, envRmCommand]));
2163
+ var envBase = Command7.make("env", {}, () => Effect17.void);
2164
+ var envCommand = envBase.pipe(
2165
+ Command7.withSubcommands([envListCommand, envSetCommand, envRmCommand, envPullCommand])
2166
+ );
1670
2167
 
1671
2168
  // src/commands/link/index.ts
1672
- import { Command as Command8, Options as Options5, Prompt } from "@effect/cli";
1673
- import { Console as Console8, Effect as Effect15, Option as Option8 } from "effect";
2169
+ import { Command as Command8, Options as Options5, Prompt as Prompt2 } from "@effect/cli";
2170
+ import { Console as Console8, Effect as Effect18, Option as Option11 } from "effect";
1674
2171
  var appOption4 = Options5.text("app").pipe(
1675
2172
  Options5.withAlias("a"),
1676
2173
  Options5.withDescription("Application ID to link (skips the interactive picker)"),
@@ -1684,26 +2181,26 @@ var orgOption5 = Options5.text("org").pipe(
1684
2181
  var linkCommand = Command8.make(
1685
2182
  "link",
1686
2183
  { app: appOption4, org: orgOption5 },
1687
- ({ app, org }) => Effect15.gen(function* () {
2184
+ ({ app, org }) => Effect18.gen(function* () {
1688
2185
  const apiClient = yield* ApiClientService;
1689
2186
  const projectLink = yield* ProjectLinkService;
1690
2187
  const cwd = process.cwd();
1691
- const organizationId = Option8.getOrUndefined(org);
2188
+ const organizationId = Option11.getOrUndefined(org);
1692
2189
  const { items: apps } = yield* apiClient.request((client) => client.applications.list({ organizationId }));
1693
2190
  if (apps.length === 0) {
1694
- return yield* Effect15.fail(
2191
+ return yield* Effect18.fail(
1695
2192
  new NoApplicationsFoundError({
1696
2193
  message: 'No applications found. Create one in the Gigadrive console, then run "gigadrive link" again.'
1697
2194
  })
1698
2195
  );
1699
2196
  }
1700
- const explicitAppId = Option8.getOrUndefined(app);
2197
+ const explicitAppId = Option11.getOrUndefined(app);
1701
2198
  let applicationId;
1702
2199
  let linkedOrganizationId;
1703
2200
  if (explicitAppId !== void 0) {
1704
2201
  const match = apps.find((a) => a.id === explicitAppId);
1705
2202
  if (match === void 0) {
1706
- return yield* Effect15.fail(
2203
+ return yield* Effect18.fail(
1707
2204
  new ApplicationNotFoundError({
1708
2205
  message: `Application "${explicitAppId}" was not found among your applications.`
1709
2206
  })
@@ -1715,8 +2212,8 @@ var linkCommand = Command8.make(
1715
2212
  applicationId = apps[0].id;
1716
2213
  linkedOrganizationId = apps[0].organization.id;
1717
2214
  } else {
1718
- const chosen = yield* Prompt.run(
1719
- Prompt.select({
2215
+ const chosen = yield* Prompt2.run(
2216
+ Prompt2.select({
1720
2217
  message: "Select an application to link",
1721
2218
  choices: apps.map((a) => ({ title: `${a.name} (${a.organization.name})`, value: a }))
1722
2219
  })
@@ -1728,49 +2225,52 @@ var linkCommand = Command8.make(
1728
2225
  yield* Console8.log(`Linked this directory to application ${applicationId}.`);
1729
2226
  yield* Console8.log('Tip: add ".gigadrive/" to your .gitignore.');
1730
2227
  }).pipe(
1731
- Effect15.catchTags({
2228
+ Effect18.catchTags({
1732
2229
  // Ctrl+C during the picker is a deliberate cancel — exit 0.
1733
2230
  QuitException: () => Console8.log("Cancelled."),
1734
2231
  NotAuthenticatedError: (err) => Console8.error('You are not logged in. Run "gigadrive login" to authenticate.').pipe(
1735
- Effect15.andThen(Effect15.fail(err))
2232
+ Effect18.andThen(Effect18.fail(err))
1736
2233
  ),
1737
- ApiRequestError: (err) => Console8.error(`Failed to list applications: ${err.message}`).pipe(Effect15.andThen(Effect15.fail(err))),
1738
- NoApplicationsFoundError: (err) => Console8.error(err.message).pipe(Effect15.andThen(Effect15.fail(err))),
1739
- ApplicationNotFoundError: (err) => Console8.error(err.message).pipe(Effect15.andThen(Effect15.fail(err))),
1740
- ProjectLinkWriteError: (err) => Console8.error(err.message).pipe(Effect15.andThen(Effect15.fail(err)))
2234
+ ApiRequestError: (err) => Console8.error(`Failed to list applications: ${err.message}`).pipe(Effect18.andThen(Effect18.fail(err))),
2235
+ NoApplicationsFoundError: (err) => Console8.error(err.message).pipe(Effect18.andThen(Effect18.fail(err))),
2236
+ ApplicationNotFoundError: (err) => Console8.error(err.message).pipe(Effect18.andThen(Effect18.fail(err))),
2237
+ ProjectLinkWriteError: (err) => Console8.error(err.message).pipe(Effect18.andThen(Effect18.fail(err)))
1741
2238
  })
1742
2239
  )
1743
2240
  );
1744
2241
  var unlinkCommand = Command8.make(
1745
2242
  "unlink",
1746
2243
  {},
1747
- () => Effect15.gen(function* () {
2244
+ () => Effect18.gen(function* () {
1748
2245
  const projectLink = yield* ProjectLinkService;
1749
2246
  yield* projectLink.remove(process.cwd());
1750
2247
  yield* Console8.log("Unlinked this directory.");
1751
2248
  }).pipe(
1752
- Effect15.catchTags({
1753
- ProjectLinkWriteError: (err) => Console8.error(err.message).pipe(Effect15.andThen(Effect15.fail(err)))
2249
+ Effect18.catchTags({
2250
+ ProjectLinkWriteError: (err) => Console8.error(err.message).pipe(Effect18.andThen(Effect18.fail(err)))
1754
2251
  })
1755
2252
  )
1756
2253
  );
1757
2254
 
1758
2255
  // src/commands/login/index.ts
1759
2256
  import { Command as Command9 } from "@effect/cli";
1760
- import { Console as Console9, Effect as Effect16 } from "effect";
2257
+ import { Console as Console9, Effect as Effect19 } from "effect";
1761
2258
  var loginCommand = Command9.make(
1762
2259
  "login",
1763
2260
  {},
1764
- () => Effect16.gen(function* () {
2261
+ () => Effect19.gen(function* () {
1765
2262
  yield* Console9.log("Initiating login...");
1766
2263
  const auth = yield* AuthService;
1767
2264
  yield* auth.login;
1768
- const info = yield* auth.getUserInfo.pipe(Effect16.catchAll(() => Effect16.succeed({})));
2265
+ const info = yield* auth.getUserInfo.pipe(Effect19.catchAll(() => Effect19.succeed({})));
1769
2266
  const name = auth.inferUserName(info);
1770
2267
  yield* Console9.log(`You are now logged in as ${name}.`);
1771
2268
  }).pipe(
1772
- Effect16.catchTags({
1773
- LoginFlowError: (err) => Console9.error(`Login failed: ${err.message}`),
2269
+ Effect19.catchTags({
2270
+ LoginFlowError: (err) => Console9.error(err.message),
2271
+ DeviceAuthorizationError: (err) => Console9.error(`Login failed: ${err.message}`),
2272
+ AuthorizationDeniedError: (err) => Console9.error(err.message),
2273
+ DeviceCodeExpiredError: (err) => Console9.error(err.message),
1774
2274
  TokenExchangeError: (err) => Console9.error(`Login failed: ${err.message}`),
1775
2275
  OAuthDiscoveryError: (err) => Console9.error(`Login failed: ${err.message}`),
1776
2276
  AuthStorageWriteError: (err) => Console9.error(`Failed to save credentials: ${err.message}`)
@@ -1780,51 +2280,51 @@ var loginCommand = Command9.make(
1780
2280
 
1781
2281
  // src/commands/logout/index.ts
1782
2282
  import { Command as Command10 } from "@effect/cli";
1783
- import { Console as Console10, Effect as Effect17 } from "effect";
2283
+ import { Console as Console10, Effect as Effect20 } from "effect";
1784
2284
  var logoutCommand = Command10.make(
1785
2285
  "logout",
1786
2286
  {},
1787
- () => Effect17.gen(function* () {
2287
+ () => Effect20.gen(function* () {
1788
2288
  const auth = yield* AuthService;
1789
2289
  yield* auth.logout;
1790
2290
  yield* Console10.log("Logged out.");
1791
2291
  }).pipe(
1792
- Effect17.catchTags({
1793
- AuthStorageWriteError: (err) => Console10.error(`Failed to log out: ${err.message}`).pipe(Effect17.andThen(Effect17.fail(err)))
2292
+ Effect20.catchTags({
2293
+ AuthStorageWriteError: (err) => Console10.error(`Failed to log out: ${err.message}`).pipe(Effect20.andThen(Effect20.fail(err)))
1794
2294
  })
1795
2295
  )
1796
2296
  );
1797
2297
 
1798
2298
  // src/commands/platform/index.ts
1799
2299
  import { Command as Command12 } from "@effect/cli";
1800
- import { Effect as Effect21 } from "effect";
2300
+ import { Effect as Effect24 } from "effect";
1801
2301
 
1802
2302
  // src/commands/platform/deploy/index.ts
1803
2303
  import { Command as Command11, Options as Options6 } from "@effect/cli";
1804
- import { FileSystem as FileSystem7, Path as Path6 } from "@effect/platform";
2304
+ import { FileSystem as FileSystem9, Path as Path8 } from "@effect/platform";
1805
2305
  import { formatFileSize } from "@gigadrive/commons";
1806
- import { Console as Console11, Duration, Effect as Effect20, Option as Option9, Schedule, Stream } from "effect";
1807
- import * as os2 from "os";
2306
+ import { Console as Console11, Duration as Duration2, Effect as Effect23, Option as Option12, Schedule, Stream } from "effect";
2307
+ import * as os4 from "os";
1808
2308
 
1809
2309
  // src/services/archive.ts
1810
- import { FileSystem as FileSystem6, Path as Path5 } from "@effect/platform";
2310
+ import { FileSystem as FileSystem8, Path as Path7 } from "@effect/platform";
1811
2311
  import { getFilesForPattern } from "@gigadrive/build-utils";
1812
2312
  import archiver from "archiver";
1813
- import { Effect as Effect18 } from "effect";
2313
+ import { Effect as Effect21 } from "effect";
1814
2314
  import ignore from "ignore";
1815
2315
  import * as nodeFs from "fs";
1816
2316
  import * as nodeFsPromises from "fs/promises";
1817
2317
  var IGNORE_FILE_NAMES = [".gigadriveignore", ".vercelignore", ".dockerignore", ".nowignore"];
1818
2318
  var readIgnoreFile = (fs, ignorePath) => fs.readFileString(ignorePath, "utf8").pipe(
1819
- Effect18.map((content) => content.split("\n").filter((line) => line.trim() !== "")),
1820
- Effect18.catchAll(() => Effect18.succeed([]))
2319
+ Effect21.map((content) => content.split("\n").filter((line) => line.trim() !== "")),
2320
+ Effect21.catchAll(() => Effect21.succeed([]))
1821
2321
  );
1822
- var collectIgnorePatterns = (fs, pathService, dir, baseDir, ignoreRules) => Effect18.gen(function* () {
1823
- const items = yield* fs.readDirectory(dir).pipe(Effect18.mapError(() => new ArchiveCreateError({ message: `Failed to read directory: ${dir}` })));
2322
+ var collectIgnorePatterns = (fs, pathService, dir, baseDir, ignoreRules) => Effect21.gen(function* () {
2323
+ const items = yield* fs.readDirectory(dir).pipe(Effect21.mapError(() => new ArchiveCreateError({ message: `Failed to read directory: ${dir}` })));
1824
2324
  for (const item of items) {
1825
2325
  const fullPath = pathService.join(dir, item);
1826
2326
  const relativePath = pathService.relative(baseDir, fullPath);
1827
- const stat = yield* Effect18.tryPromise({
2327
+ const stat = yield* Effect21.tryPromise({
1828
2328
  try: () => nodeFsPromises.lstat(fullPath),
1829
2329
  catch: () => new ArchiveCreateError({ message: `Failed to stat: ${fullPath}` })
1830
2330
  });
@@ -1844,8 +2344,8 @@ var collectIgnorePatterns = (fs, pathService, dir, baseDir, ignoreRules) => Effe
1844
2344
  yield* collectIgnorePatterns(fs, pathService, fullPath, baseDir, ignoreRules);
1845
2345
  }
1846
2346
  }
1847
- }).pipe(Effect18.catchAll(() => Effect18.void));
1848
- var initializeIgnoreRules = (fs, pathService, baseDir) => Effect18.gen(function* () {
2347
+ }).pipe(Effect21.catchAll(() => Effect21.void));
2348
+ var initializeIgnoreRules = (fs, pathService, baseDir) => Effect21.gen(function* () {
1849
2349
  const ignoreRules = ignore();
1850
2350
  ignoreRules.add([".git/", "**/.DS_Store", "**/Thumbs.db"]);
1851
2351
  for (const ignoreFileName of IGNORE_FILE_NAMES) {
@@ -1860,14 +2360,14 @@ var initializeIgnoreRules = (fs, pathService, baseDir) => Effect18.gen(function*
1860
2360
  yield* collectIgnorePatterns(fs, pathService, baseDir, baseDir, ignoreRules);
1861
2361
  return ignoreRules;
1862
2362
  });
1863
- var getFilesToInclude = (fs, pathService, dir, ignoreRules) => Effect18.gen(function* () {
2363
+ var getFilesToInclude = (fs, pathService, dir, ignoreRules) => Effect21.gen(function* () {
1864
2364
  const filesToInclude = [];
1865
- const walkDir = (currentDir) => Effect18.gen(function* () {
1866
- const items = yield* fs.readDirectory(currentDir).pipe(Effect18.mapError(() => new ArchiveCreateError({ message: `Failed to read directory: ${currentDir}` })));
2365
+ const walkDir = (currentDir) => Effect21.gen(function* () {
2366
+ const items = yield* fs.readDirectory(currentDir).pipe(Effect21.mapError(() => new ArchiveCreateError({ message: `Failed to read directory: ${currentDir}` })));
1867
2367
  for (const item of items) {
1868
2368
  const fullPath = pathService.join(currentDir, item);
1869
2369
  const relativePath = pathService.relative(dir, fullPath);
1870
- const stat = yield* Effect18.tryPromise({
2370
+ const stat = yield* Effect21.tryPromise({
1871
2371
  try: () => nodeFsPromises.lstat(fullPath),
1872
2372
  catch: () => new ArchiveCreateError({ message: `Failed to stat: ${fullPath}` })
1873
2373
  });
@@ -1881,21 +2381,21 @@ var getFilesToInclude = (fs, pathService, dir, ignoreRules) => Effect18.gen(func
1881
2381
  }
1882
2382
  }
1883
2383
  }
1884
- }).pipe(Effect18.catchAll(() => Effect18.void));
2384
+ }).pipe(Effect21.catchAll(() => Effect21.void));
1885
2385
  yield* walkDir(dir);
1886
2386
  return filesToInclude;
1887
2387
  });
1888
- var ArchiveService = class extends Effect18.Service()("ArchiveService", {
2388
+ var ArchiveService = class extends Effect21.Service()("ArchiveService", {
1889
2389
  accessors: true,
1890
- effect: Effect18.gen(function* () {
1891
- const fs = yield* FileSystem6.FileSystem;
1892
- const pathService = yield* Path5.Path;
1893
- const createZipArchive = Effect18.fn("ArchiveService.createZipArchive")(function* (inputDir, outputFile, options = {}) {
1894
- yield* Effect18.annotateCurrentSpan("inputDir", inputDir);
1895
- yield* Effect18.annotateCurrentSpan("outputFile", outputFile);
1896
- yield* Effect18.log("Creating zip archive", { inputDir, outputFile });
2390
+ effect: Effect21.gen(function* () {
2391
+ const fs = yield* FileSystem8.FileSystem;
2392
+ const pathService = yield* Path7.Path;
2393
+ const createZipArchive = Effect21.fn("ArchiveService.createZipArchive")(function* (inputDir, outputFile, options = {}) {
2394
+ yield* Effect21.annotateCurrentSpan("inputDir", inputDir);
2395
+ yield* Effect21.annotateCurrentSpan("outputFile", outputFile);
2396
+ yield* Effect21.log("Creating zip archive", { inputDir, outputFile });
1897
2397
  const ignoreRules = options.useIgnoreFiles !== false ? yield* initializeIgnoreRules(fs, pathService, inputDir) : ignore();
1898
- const filesToInclude = options.whitelist ? yield* Effect18.tryPromise({
2398
+ const filesToInclude = options.whitelist ? yield* Effect21.tryPromise({
1899
2399
  try: () => Promise.all(
1900
2400
  options.whitelist.map((file) => getFilesForPattern(file, inputDir, options.excludeFiles ?? []))
1901
2401
  ),
@@ -1903,25 +2403,25 @@ var ArchiveService = class extends Effect18.Service()("ArchiveService", {
1903
2403
  message: "Failed to resolve whitelist patterns",
1904
2404
  cause: error instanceof Error ? error.message : String(error)
1905
2405
  })
1906
- }) : yield* Effect18.gen(function* () {
2406
+ }) : yield* Effect21.gen(function* () {
1907
2407
  if (options.excludeFiles && options.excludeFiles.length > 0) {
1908
2408
  ignoreRules.add(options.excludeFiles);
1909
2409
  }
1910
2410
  return yield* getFilesToInclude(fs, pathService, inputDir, ignoreRules).pipe(
1911
- Effect18.map((files) => [files])
2411
+ Effect21.map((files) => [files])
1912
2412
  );
1913
2413
  });
1914
- const archiveExists = yield* fs.exists(outputFile).pipe(Effect18.catchAll(() => Effect18.succeed(false)));
2414
+ const archiveExists = yield* fs.exists(outputFile).pipe(Effect21.catchAll(() => Effect21.succeed(false)));
1915
2415
  if (archiveExists) {
1916
2416
  yield* fs.remove(outputFile).pipe(
1917
- Effect18.mapError(
2417
+ Effect21.mapError(
1918
2418
  (error) => new ArchiveCreateError({
1919
2419
  message: `Failed to remove existing archive: ${String(error)}`
1920
2420
  })
1921
2421
  )
1922
2422
  );
1923
2423
  }
1924
- yield* Effect18.tryPromise({
2424
+ yield* Effect21.tryPromise({
1925
2425
  try: () => new Promise((resolve, reject) => {
1926
2426
  const output = nodeFs.createWriteStream(outputFile);
1927
2427
  const archive = archiver("zip", { zlib: { level: 9 } });
@@ -1940,14 +2440,14 @@ var ArchiveService = class extends Effect18.Service()("ArchiveService", {
1940
2440
  })
1941
2441
  });
1942
2442
  const fileInfo = yield* fs.stat(outputFile).pipe(
1943
- Effect18.mapError(
2443
+ Effect21.mapError(
1944
2444
  (error) => new ArchiveCreateError({
1945
2445
  message: `Failed to stat archive: ${String(error)}`
1946
2446
  })
1947
2447
  )
1948
2448
  );
1949
2449
  const size = Number(fileInfo.size);
1950
- yield* Effect18.log("Archive created", { size });
2450
+ yield* Effect21.log("Archive created", { size });
1951
2451
  return { path: outputFile, size };
1952
2452
  });
1953
2453
  return { createZipArchive };
@@ -1956,32 +2456,32 @@ var ArchiveService = class extends Effect18.Service()("ArchiveService", {
1956
2456
  };
1957
2457
 
1958
2458
  // src/services/deployment-api.ts
1959
- import { Effect as Effect19 } from "effect";
1960
- var DeploymentApiService = class extends Effect19.Service()("DeploymentApiService", {
2459
+ import { Effect as Effect22 } from "effect";
2460
+ var DeploymentApiService = class extends Effect22.Service()("DeploymentApiService", {
1961
2461
  accessors: true,
1962
2462
  dependencies: [ApiClientService.Default],
1963
- effect: Effect19.gen(function* () {
2463
+ effect: Effect22.gen(function* () {
1964
2464
  const apiClient = yield* ApiClientService;
1965
2465
  const getClient = apiClient.getClient.pipe(
1966
- Effect19.mapError((err) => new DeploymentAuthError({ message: `Authentication failed: ${err.message}` }))
2466
+ Effect22.mapError((err) => new DeploymentAuthError({ message: `Authentication failed: ${err.message}` }))
1967
2467
  );
1968
- const createDeployment = Effect19.fn("DeploymentApiService.createDeployment")(function* (applicationId) {
1969
- yield* Effect19.annotateCurrentSpan("applicationId", applicationId);
1970
- yield* Effect19.log("Creating deployment", { applicationId });
2468
+ const createDeployment = Effect22.fn("DeploymentApiService.createDeployment")(function* (applicationId) {
2469
+ yield* Effect22.annotateCurrentSpan("applicationId", applicationId);
2470
+ yield* Effect22.log("Creating deployment", { applicationId });
1971
2471
  const client = yield* getClient;
1972
- const deployment = yield* Effect19.tryPromise({
2472
+ const deployment = yield* Effect22.tryPromise({
1973
2473
  try: () => client.deployments.create({ applicationId }),
1974
2474
  catch: (error) => new DeploymentCreateError({
1975
2475
  message: `Failed to create deployment: ${error instanceof Error ? error.message : String(error)}`
1976
2476
  })
1977
2477
  });
1978
- yield* Effect19.log("Deployment created", { deploymentId: deployment.id });
2478
+ yield* Effect22.log("Deployment created", { deploymentId: deployment.id });
1979
2479
  return deployment.id;
1980
2480
  });
1981
- const startMultipartUpload = Effect19.fn("DeploymentApiService.startMultipartUpload")(function* (deploymentId) {
1982
- yield* Effect19.annotateCurrentSpan("deploymentId", deploymentId);
2481
+ const startMultipartUpload = Effect22.fn("DeploymentApiService.startMultipartUpload")(function* (deploymentId) {
2482
+ yield* Effect22.annotateCurrentSpan("deploymentId", deploymentId);
1983
2483
  const client = yield* getClient;
1984
- const result = yield* Effect19.tryPromise({
2484
+ const result = yield* Effect22.tryPromise({
1985
2485
  try: () => client.deployments.startUpload(deploymentId),
1986
2486
  catch: (error) => new UploadStartError({
1987
2487
  message: `Failed to start multipart upload: ${error instanceof Error ? error.message : String(error)}`
@@ -1989,9 +2489,9 @@ var DeploymentApiService = class extends Effect19.Service()("DeploymentApiServic
1989
2489
  });
1990
2490
  return result.uploadId;
1991
2491
  });
1992
- const getPresignedUrl = Effect19.fn("DeploymentApiService.getPresignedUrl")(function* (deploymentId, uploadId, partNumber) {
2492
+ const getPresignedUrl = Effect22.fn("DeploymentApiService.getPresignedUrl")(function* (deploymentId, uploadId, partNumber) {
1993
2493
  const client = yield* getClient;
1994
- const result = yield* Effect19.tryPromise({
2494
+ const result = yield* Effect22.tryPromise({
1995
2495
  try: () => client.deployments.getPresignedUrl(deploymentId, uploadId, partNumber),
1996
2496
  catch: (error) => new PresignedUrlError({
1997
2497
  message: `Failed to get presigned URL: ${error instanceof Error ? error.message : String(error)}`,
@@ -2000,11 +2500,11 @@ var DeploymentApiService = class extends Effect19.Service()("DeploymentApiServic
2000
2500
  });
2001
2501
  return result.url;
2002
2502
  });
2003
- const uploadPart = Effect19.fn("DeploymentApiService.uploadPart")(function* (presignedUrl, data, partNumber) {
2004
- yield* Effect19.annotateCurrentSpan("partNumber", partNumber);
2005
- yield* Effect19.annotateCurrentSpan("size", data.length);
2503
+ const uploadPart = Effect22.fn("DeploymentApiService.uploadPart")(function* (presignedUrl, data, partNumber) {
2504
+ yield* Effect22.annotateCurrentSpan("partNumber", partNumber);
2505
+ yield* Effect22.annotateCurrentSpan("size", data.length);
2006
2506
  const client = yield* getClient;
2007
- const result = yield* Effect19.tryPromise({
2507
+ const result = yield* Effect22.tryPromise({
2008
2508
  try: () => client.deployments.uploadPart(presignedUrl, data, partNumber),
2009
2509
  catch: (error) => new UploadPartError({
2010
2510
  message: `Failed to upload part ${partNumber}: ${error instanceof Error ? error.message : String(error)}`,
@@ -2013,21 +2513,21 @@ var DeploymentApiService = class extends Effect19.Service()("DeploymentApiServic
2013
2513
  });
2014
2514
  return result;
2015
2515
  });
2016
- const completeUpload = Effect19.fn("DeploymentApiService.completeUpload")(function* (deploymentId, uploadId, parts) {
2017
- yield* Effect19.annotateCurrentSpan("deploymentId", deploymentId);
2018
- yield* Effect19.annotateCurrentSpan("partsCount", parts.length);
2516
+ const completeUpload = Effect22.fn("DeploymentApiService.completeUpload")(function* (deploymentId, uploadId, parts) {
2517
+ yield* Effect22.annotateCurrentSpan("deploymentId", deploymentId);
2518
+ yield* Effect22.annotateCurrentSpan("partsCount", parts.length);
2019
2519
  const client = yield* getClient;
2020
- yield* Effect19.tryPromise({
2520
+ yield* Effect22.tryPromise({
2021
2521
  try: () => client.deployments.completeUpload(deploymentId, uploadId, parts),
2022
2522
  catch: (error) => new UploadCompleteError({
2023
2523
  message: `Failed to complete upload: ${error instanceof Error ? error.message : String(error)}`
2024
2524
  })
2025
2525
  });
2026
- yield* Effect19.log("Upload completed successfully");
2526
+ yield* Effect22.log("Upload completed successfully");
2027
2527
  });
2028
- const getDeploymentStatus = Effect19.fn("DeploymentApiService.getDeploymentStatus")(function* (deploymentId) {
2528
+ const getDeploymentStatus = Effect22.fn("DeploymentApiService.getDeploymentStatus")(function* (deploymentId) {
2029
2529
  const client = yield* getClient;
2030
- const deployment = yield* Effect19.tryPromise({
2530
+ const deployment = yield* Effect22.tryPromise({
2031
2531
  try: () => client.deployments.get(deploymentId),
2032
2532
  catch: (error) => new DeploymentStatusError({
2033
2533
  message: `Failed to get deployment status: ${error instanceof Error ? error.message : String(error)}`
@@ -2035,9 +2535,9 @@ var DeploymentApiService = class extends Effect19.Service()("DeploymentApiServic
2035
2535
  });
2036
2536
  return deployment.status;
2037
2537
  });
2038
- const getLogs = Effect19.fn("DeploymentApiService.getLogs")(function* (deploymentId, options) {
2538
+ const getLogs = Effect22.fn("DeploymentApiService.getLogs")(function* (deploymentId, options) {
2039
2539
  const client = yield* getClient;
2040
- const result = yield* Effect19.tryPromise({
2540
+ const result = yield* Effect22.tryPromise({
2041
2541
  try: () => client.deployments.getLogs(deploymentId, options),
2042
2542
  catch: (error) => new DeploymentLogsFetchError({
2043
2543
  message: `Failed to get logs: ${error instanceof Error ? error.message : String(error)}`
@@ -2045,9 +2545,9 @@ var DeploymentApiService = class extends Effect19.Service()("DeploymentApiServic
2045
2545
  });
2046
2546
  return result;
2047
2547
  });
2048
- const getHostnames = Effect19.fn("DeploymentApiService.getHostnames")(function* (deploymentId) {
2548
+ const getHostnames = Effect22.fn("DeploymentApiService.getHostnames")(function* (deploymentId) {
2049
2549
  const client = yield* getClient;
2050
- const result = yield* Effect19.tryPromise({
2550
+ const result = yield* Effect22.tryPromise({
2051
2551
  try: () => client.deployments.getHostnames(deploymentId),
2052
2552
  catch: (error) => new DeploymentHostnamesFetchError({
2053
2553
  message: `Failed to get hostnames: ${error instanceof Error ? error.message : String(error)}`
@@ -2078,24 +2578,24 @@ var appOption5 = Options6.text("app").pipe(
2078
2578
  var deployCommand = Command11.make(
2079
2579
  "deploy",
2080
2580
  { app: appOption5 },
2081
- ({ app }) => Effect20.gen(function* () {
2581
+ ({ app }) => Effect23.gen(function* () {
2082
2582
  const projectConfig = yield* ProjectConfigService;
2083
2583
  const deploymentApi = yield* DeploymentApiService;
2084
2584
  const archiveService = yield* ArchiveService;
2085
2585
  const projectLink = yield* ProjectLinkService;
2086
- const pathService = yield* Path6.Path;
2586
+ const pathService = yield* Path8.Path;
2087
2587
  const cwd = process.cwd();
2088
- const applicationId = yield* Option9.match(app, {
2089
- onSome: (id) => Effect20.succeed(id),
2090
- onNone: () => projectLink.resolve(cwd).pipe(Effect20.map((link) => link.applicationId))
2588
+ const applicationId = yield* Option12.match(app, {
2589
+ onSome: (id) => Effect23.succeed(id),
2590
+ onNone: () => projectLink.resolve(cwd).pipe(Effect23.map((link) => link.applicationId))
2091
2591
  });
2092
- yield* Effect20.log("Starting deploy command");
2592
+ yield* Effect23.log("Starting deploy command");
2093
2593
  const { config } = yield* projectConfig.resolve(cwd);
2094
2594
  yield* Console11.log("Creating deployment...");
2095
2595
  const deploymentId = yield* deploymentApi.createDeployment(applicationId);
2096
2596
  yield* Console11.log(`Deployment ID: ${deploymentId}`);
2097
2597
  yield* Console11.log("Creating archive...");
2098
- const archivePath = pathService.join(os2.tmpdir(), `project-${Date.now()}.zip`);
2598
+ const archivePath = pathService.join(os4.tmpdir(), `project-${Date.now()}.zip`);
2099
2599
  const archive = yield* archiveService.createZipArchive(config.userArchive?.rootOverwrite || cwd, archivePath, {
2100
2600
  whitelist: config.userArchive?.fileWhitelist,
2101
2601
  excludeFiles: config.excludeFiles,
@@ -2109,55 +2609,55 @@ var deployCommand = Command11.make(
2109
2609
  yield* Console11.log("The deployment pipeline is now being provisioned. This may take a few seconds.");
2110
2610
  yield* pollDeployment(deploymentId);
2111
2611
  }).pipe(
2112
- Effect20.catchTags({
2113
- ProjectNotLinkedError: (err) => Console11.error(err.message).pipe(Effect20.andThen(Effect20.fail(err))),
2114
- ProjectLinkReadError: (err) => Console11.error(err.message).pipe(Effect20.andThen(Effect20.fail(err))),
2115
- ConfigNotFoundError: (err) => Console11.error(err.message).pipe(Effect20.andThen(Effect20.fail(err))),
2116
- ConfigParseError: (err) => Console11.error(`Config parse error: ${err.message}`).pipe(Effect20.andThen(Effect20.fail(err))),
2117
- ConfigValidationError: (err) => Effect20.gen(function* () {
2612
+ Effect23.catchTags({
2613
+ ProjectNotLinkedError: (err) => Console11.error(err.message).pipe(Effect23.andThen(Effect23.fail(err))),
2614
+ ProjectLinkReadError: (err) => Console11.error(err.message).pipe(Effect23.andThen(Effect23.fail(err))),
2615
+ ConfigNotFoundError: (err) => Console11.error(err.message).pipe(Effect23.andThen(Effect23.fail(err))),
2616
+ ConfigParseError: (err) => Console11.error(`Config parse error: ${err.message}`).pipe(Effect23.andThen(Effect23.fail(err))),
2617
+ ConfigValidationError: (err) => Effect23.gen(function* () {
2118
2618
  for (const e of err.errors) {
2119
2619
  yield* Console11.error(e);
2120
2620
  }
2121
2621
  yield* Console11.error(err.message);
2122
- }).pipe(Effect20.andThen(Effect20.fail(err))),
2123
- DeploymentCreateError: (err) => Console11.error(`Deployment creation failed: ${err.message}`).pipe(Effect20.andThen(Effect20.fail(err))),
2124
- ArchiveCreateError: (err) => Console11.error(`Archive creation failed: ${err.message}`).pipe(Effect20.andThen(Effect20.fail(err))),
2125
- UploadStartError: (err) => Console11.error(`Upload failed: ${err.message}`).pipe(Effect20.andThen(Effect20.fail(err))),
2126
- UploadPartError: (err) => Console11.error(`Upload failed: ${err.message}`).pipe(Effect20.andThen(Effect20.fail(err))),
2127
- UploadCompleteError: (err) => Console11.error(`Upload failed: ${err.message}`).pipe(Effect20.andThen(Effect20.fail(err))),
2128
- PresignedUrlError: (err) => Console11.error(`Upload failed: ${err.message}`).pipe(Effect20.andThen(Effect20.fail(err))),
2129
- DeploymentAuthError: (err) => Console11.error(`Authentication failed: ${err.message}`).pipe(Effect20.andThen(Effect20.fail(err))),
2130
- DeploymentFailedError: (err) => Console11.error(err.message).pipe(Effect20.andThen(Effect20.fail(err)))
2622
+ }).pipe(Effect23.andThen(Effect23.fail(err))),
2623
+ DeploymentCreateError: (err) => Console11.error(`Deployment creation failed: ${err.message}`).pipe(Effect23.andThen(Effect23.fail(err))),
2624
+ ArchiveCreateError: (err) => Console11.error(`Archive creation failed: ${err.message}`).pipe(Effect23.andThen(Effect23.fail(err))),
2625
+ UploadStartError: (err) => Console11.error(`Upload failed: ${err.message}`).pipe(Effect23.andThen(Effect23.fail(err))),
2626
+ UploadPartError: (err) => Console11.error(`Upload failed: ${err.message}`).pipe(Effect23.andThen(Effect23.fail(err))),
2627
+ UploadCompleteError: (err) => Console11.error(`Upload failed: ${err.message}`).pipe(Effect23.andThen(Effect23.fail(err))),
2628
+ PresignedUrlError: (err) => Console11.error(`Upload failed: ${err.message}`).pipe(Effect23.andThen(Effect23.fail(err))),
2629
+ DeploymentAuthError: (err) => Console11.error(`Authentication failed: ${err.message}`).pipe(Effect23.andThen(Effect23.fail(err))),
2630
+ DeploymentFailedError: (err) => Console11.error(err.message).pipe(Effect23.andThen(Effect23.fail(err)))
2131
2631
  })
2132
2632
  )
2133
2633
  );
2134
- var uploadArchive = (deploymentId, archivePath, fileSize) => Effect20.gen(function* () {
2634
+ var uploadArchive = (deploymentId, archivePath, fileSize) => Effect23.gen(function* () {
2135
2635
  const deploymentApi = yield* DeploymentApiService;
2136
- const fs = yield* FileSystem7.FileSystem;
2636
+ const fs = yield* FileSystem9.FileSystem;
2137
2637
  const uploadId = yield* deploymentApi.startMultipartUpload(deploymentId);
2138
2638
  const FILE_CHUNK_SIZE = 10 * 1024 * 1024;
2139
2639
  const numChunks = Math.ceil(fileSize / FILE_CHUNK_SIZE);
2140
- yield* Effect20.log("Starting multipart upload", { uploadId, numChunks, fileSize });
2141
- const parts = yield* Effect20.all(
2640
+ yield* Effect23.log("Starting multipart upload", { uploadId, numChunks, fileSize });
2641
+ const parts = yield* Effect23.all(
2142
2642
  Array.from({ length: numChunks }, (_, i) => {
2143
2643
  const partNumber = i + 1;
2144
2644
  const start = i * FILE_CHUNK_SIZE;
2145
2645
  const bytesToRead = partNumber < numChunks ? FILE_CHUNK_SIZE : fileSize - start;
2146
- return Effect20.gen(function* () {
2646
+ return Effect23.gen(function* () {
2147
2647
  const presignedUrl = yield* deploymentApi.getPresignedUrl(deploymentId, uploadId, partNumber);
2148
2648
  const chunkData = yield* Stream.runCollect(fs.stream(archivePath, { offset: start, bytesToRead })).pipe(
2149
- Effect20.map((chunks) => {
2649
+ Effect23.map((chunks) => {
2150
2650
  const arrays = Array.from(chunks);
2151
2651
  return Buffer.concat(arrays);
2152
2652
  }),
2153
- Effect20.mapError(
2653
+ Effect23.mapError(
2154
2654
  (error) => new UploadPartError({
2155
2655
  message: `Failed to read chunk ${partNumber}: ${String(error)}`,
2156
2656
  partNumber
2157
2657
  })
2158
2658
  )
2159
2659
  );
2160
- yield* Effect20.log(`Uploading part ${partNumber}/${numChunks}`, { size: chunkData.length });
2660
+ yield* Effect23.log(`Uploading part ${partNumber}/${numChunks}`, { size: chunkData.length });
2161
2661
  return yield* deploymentApi.uploadPart(presignedUrl, chunkData, partNumber);
2162
2662
  });
2163
2663
  }),
@@ -2165,20 +2665,20 @@ var uploadArchive = (deploymentId, archivePath, fileSize) => Effect20.gen(functi
2165
2665
  );
2166
2666
  yield* deploymentApi.completeUpload(deploymentId, uploadId, parts);
2167
2667
  });
2168
- var pollDeployment = (deploymentId) => Effect20.gen(function* () {
2668
+ var pollDeployment = (deploymentId) => Effect23.gen(function* () {
2169
2669
  const deploymentApi = yield* DeploymentApiService;
2170
2670
  let lastStatus = "PENDING";
2171
2671
  let logOffset = 0;
2172
2672
  yield* Console11.log("Status: Pending");
2173
- yield* Effect20.repeat(
2174
- Effect20.gen(function* () {
2175
- const newStatus = yield* deploymentApi.getDeploymentStatus(deploymentId).pipe(Effect20.catchTag("DeploymentStatusError", () => Effect20.succeed(lastStatus)));
2673
+ yield* Effect23.repeat(
2674
+ Effect23.gen(function* () {
2675
+ const newStatus = yield* deploymentApi.getDeploymentStatus(deploymentId).pipe(Effect23.catchTag("DeploymentStatusError", () => Effect23.succeed(lastStatus)));
2176
2676
  if (newStatus !== lastStatus) {
2177
2677
  lastStatus = newStatus;
2178
2678
  const label = newStatus.charAt(0) + newStatus.slice(1).toLowerCase();
2179
2679
  yield* Console11.log(`Status: ${label}`);
2180
2680
  }
2181
- const logsPage = yield* deploymentApi.getLogs(deploymentId, { offset: logOffset, limit: 100 }).pipe(Effect20.catchTag("DeploymentLogsFetchError", () => Effect20.succeed(null)));
2681
+ const logsPage = yield* deploymentApi.getLogs(deploymentId, { offset: logOffset, limit: 100 }).pipe(Effect23.catchTag("DeploymentLogsFetchError", () => Effect23.succeed(null)));
2182
2682
  if (logsPage) {
2183
2683
  for (const logEntry of logsPage.items) {
2184
2684
  logOffset++;
@@ -2186,28 +2686,28 @@ var pollDeployment = (deploymentId) => Effect20.gen(function* () {
2186
2686
  }
2187
2687
  }
2188
2688
  if (newStatus === "ACTIVE") {
2189
- const hostnames = yield* deploymentApi.getHostnames(deploymentId).pipe(Effect20.catchTag("DeploymentHostnamesFetchError", () => Effect20.succeed([])));
2689
+ const hostnames = yield* deploymentApi.getHostnames(deploymentId).pipe(Effect23.catchTag("DeploymentHostnamesFetchError", () => Effect23.succeed([])));
2190
2690
  const active = hostnames.find((h) => h.active) ?? hostnames.at(0);
2191
2691
  if (active) {
2192
2692
  yield* Console11.log(`Deployed to https://${active.hostname}`);
2193
2693
  } else {
2194
2694
  yield* Console11.log("Deployment is live.");
2195
2695
  }
2196
- return yield* Effect20.fail("done");
2696
+ return yield* Effect23.fail("done");
2197
2697
  }
2198
2698
  if (newStatus === "FAILED") {
2199
- return yield* Effect20.fail(
2699
+ return yield* Effect23.fail(
2200
2700
  new DeploymentFailedError({
2201
2701
  message: "The deployment failed. Please check the logs for more information."
2202
2702
  })
2203
2703
  );
2204
2704
  }
2205
2705
  }),
2206
- Schedule.addDelay(Schedule.forever, () => Duration.seconds(1))
2706
+ Schedule.addDelay(Schedule.forever, () => Duration2.seconds(1))
2207
2707
  ).pipe(
2208
- Effect20.catchAll((err) => {
2209
- if (err === "done") return Effect20.void;
2210
- return Effect20.fail(err);
2708
+ Effect23.catchAll((err) => {
2709
+ if (err === "done") return Effect23.void;
2710
+ return Effect23.fail(err);
2211
2711
  })
2212
2712
  );
2213
2713
  });
@@ -2223,31 +2723,174 @@ var printLog = (logEntry) => {
2223
2723
  };
2224
2724
 
2225
2725
  // src/commands/platform/index.ts
2226
- var platformBase = Command12.make("platform", {}, () => Effect21.void);
2726
+ var platformBase = Command12.make("platform", {}, () => Effect24.void);
2227
2727
  var platformCommand = platformBase.pipe(Command12.withSubcommands([deployCommand]));
2228
2728
 
2729
+ // src/commands/setup/index.ts
2730
+ import { Args as Args4, Command as Command13, Options as Options7, Prompt as Prompt3 } from "@effect/cli";
2731
+ import { Console as Console12, Effect as Effect25, Option as Option13 } from "effect";
2732
+ var fileArg2 = Args4.text({ name: "file" }).pipe(
2733
+ Args4.withDescription("Output file to write (default: .env.local)"),
2734
+ Args4.optional
2735
+ );
2736
+ var appOption6 = Options7.text("app").pipe(
2737
+ Options7.withAlias("a"),
2738
+ Options7.withDescription("Application ID to set up (skips the interactive picker)"),
2739
+ Options7.optional
2740
+ );
2741
+ var orgOption6 = Options7.text("org").pipe(
2742
+ Options7.withAlias("o"),
2743
+ Options7.withDescription("Only list applications belonging to this organization ID when picking"),
2744
+ Options7.optional
2745
+ );
2746
+ var environmentOption2 = Options7.text("environment").pipe(
2747
+ Options7.withAlias("e"),
2748
+ Options7.withDescription("Environment slug to resolve overrides for (e.g. preview)"),
2749
+ Options7.optional
2750
+ );
2751
+ var yesOption2 = Options7.boolean("yes").pipe(
2752
+ Options7.withAlias("y"),
2753
+ Options7.withDescription("Overwrite an existing file without confirmation")
2754
+ );
2755
+ var rotateOption2 = Options7.boolean("rotate").pipe(
2756
+ Options7.withDescription("Force a fresh API key, revoking the previous one")
2757
+ );
2758
+ var setupCommand = Command13.make(
2759
+ "setup",
2760
+ {
2761
+ file: fileArg2,
2762
+ app: appOption6,
2763
+ org: orgOption6,
2764
+ environment: environmentOption2,
2765
+ yes: yesOption2,
2766
+ rotate: rotateOption2
2767
+ },
2768
+ ({ file, app, org, environment, yes, rotate }) => Effect25.gen(function* () {
2769
+ const apiClient = yield* ApiClientService;
2770
+ const projectLink = yield* ProjectLinkService;
2771
+ const envFile = yield* EnvFileService;
2772
+ const localCredentials = yield* LocalCredentialsService;
2773
+ const cwd = process.cwd();
2774
+ const existingLink = yield* projectLink.load(cwd);
2775
+ let applicationId;
2776
+ if (Option13.isSome(existingLink)) {
2777
+ applicationId = existingLink.value.applicationId;
2778
+ if (Option13.isSome(app) || Option13.isSome(org)) {
2779
+ yield* Console12.log(
2780
+ `Note: this directory is already linked to ${applicationId}; ignoring --app/--org. Run "gigadrive unlink" first to target a different application.`
2781
+ );
2782
+ }
2783
+ } else {
2784
+ const organizationId = Option13.getOrUndefined(org);
2785
+ const { items: apps } = yield* apiClient.request((client) => client.applications.list({ organizationId }));
2786
+ if (apps.length === 0) {
2787
+ return yield* Effect25.fail(
2788
+ new NoApplicationsFoundError({
2789
+ message: 'No applications found. Create one in the Gigadrive console, then run "gigadrive setup" again.'
2790
+ })
2791
+ );
2792
+ }
2793
+ const explicitAppId = Option13.getOrUndefined(app);
2794
+ let chosen;
2795
+ if (explicitAppId !== void 0) {
2796
+ const match = apps.find((a) => a.id === explicitAppId);
2797
+ if (match === void 0) {
2798
+ return yield* Effect25.fail(
2799
+ new ApplicationNotFoundError({
2800
+ message: `Application "${explicitAppId}" was not found among your applications.`
2801
+ })
2802
+ );
2803
+ }
2804
+ chosen = match;
2805
+ } else if (apps.length === 1) {
2806
+ chosen = apps[0];
2807
+ } else {
2808
+ chosen = yield* Prompt3.run(
2809
+ Prompt3.select({
2810
+ message: "Select an application to set up",
2811
+ choices: apps.map((a) => ({ title: `${a.name} (${a.organization.name})`, value: a }))
2812
+ })
2813
+ );
2814
+ }
2815
+ applicationId = chosen.id;
2816
+ yield* projectLink.save(cwd, { applicationId, organizationId: chosen.organization.id });
2817
+ yield* Console12.log(`Linked this directory to application ${applicationId}.`);
2818
+ }
2819
+ const target = Option13.getOrElse(file, () => ".env.local");
2820
+ const environmentSlug = Option13.getOrUndefined(environment);
2821
+ const { items, omittedSensitive } = yield* apiClient.request(
2822
+ (client) => client.applications.envVars.pull(applicationId, environmentSlug ? { environment: environmentSlug } : void 0)
2823
+ );
2824
+ const existing = yield* envFile.read(target);
2825
+ if (Option13.isSome(existing) && !yes) {
2826
+ const overwrite = yield* Prompt3.run(
2827
+ Prompt3.confirm({ message: `${target} already exists. Overwrite?`, initial: false })
2828
+ );
2829
+ if (!overwrite) {
2830
+ return yield* Console12.log("Cancelled.");
2831
+ }
2832
+ }
2833
+ const credentialEntries2 = yield* localCredentials.provision({
2834
+ applicationId,
2835
+ rotate,
2836
+ existingContent: Option13.getOrUndefined(existing)
2837
+ });
2838
+ const entries = [...items.map((v) => ({ key: v.key, value: v.value })), ...credentialEntries2];
2839
+ yield* envFile.write(target, entries);
2840
+ const gitignored = yield* envFile.ensureGitignored(cwd, target);
2841
+ yield* Console12.log("\nLocal dev environment ready.");
2842
+ yield* Console12.log(` \u2022 Wrote ${items.length} variable(s) and API credentials to ${target}.`);
2843
+ if (omittedSensitive > 0) {
2844
+ yield* Console12.log(
2845
+ ` \u2022 Skipped ${omittedSensitive} sensitive variable(s) (only production/preview hold secrets).`
2846
+ );
2847
+ }
2848
+ if (gitignored) {
2849
+ yield* Console12.log(` \u2022 Added ${target} to .gitignore.`);
2850
+ }
2851
+ yield* Console12.log(" \u2022 Your app can now call the Gigadrive API via the SDK (GIGADRIVE_CLIENT_ID/SECRET).");
2852
+ }).pipe(
2853
+ Effect25.catchTags({
2854
+ // Ctrl+C during a prompt is a deliberate cancel — exit 0.
2855
+ QuitException: () => Console12.log("Cancelled."),
2856
+ NotAuthenticatedError: (err) => Console12.error('You are not logged in. Run "gigadrive login" to authenticate.').pipe(
2857
+ Effect25.andThen(Effect25.fail(err))
2858
+ ),
2859
+ ApiRequestError: (err) => Console12.error(`Setup failed: ${err.message}`).pipe(Effect25.andThen(Effect25.fail(err))),
2860
+ NoApplicationsFoundError: (err) => Console12.error(err.message).pipe(Effect25.andThen(Effect25.fail(err))),
2861
+ ApplicationNotFoundError: (err) => Console12.error(err.message).pipe(Effect25.andThen(Effect25.fail(err))),
2862
+ ProjectLinkReadError: (err) => Console12.error(err.message).pipe(Effect25.andThen(Effect25.fail(err))),
2863
+ ProjectLinkWriteError: (err) => Console12.error(err.message).pipe(Effect25.andThen(Effect25.fail(err))),
2864
+ EnvFileReadError: (err) => Console12.error(err.message).pipe(Effect25.andThen(Effect25.fail(err))),
2865
+ EnvFileWriteError: (err) => Console12.error(err.message).pipe(Effect25.andThen(Effect25.fail(err))),
2866
+ DevCredentialsStoreReadError: (err) => Console12.error(err.message).pipe(Effect25.andThen(Effect25.fail(err))),
2867
+ DevCredentialsStoreWriteError: (err) => Console12.error(err.message).pipe(Effect25.andThen(Effect25.fail(err)))
2868
+ })
2869
+ )
2870
+ );
2871
+
2229
2872
  // src/commands/whoami/index.ts
2230
- import { Command as Command13 } from "@effect/cli";
2231
- import { Console as Console12, Effect as Effect22 } from "effect";
2232
- var whoamiCommand = Command13.make(
2873
+ import { Command as Command14 } from "@effect/cli";
2874
+ import { Console as Console13, Effect as Effect26 } from "effect";
2875
+ var whoamiCommand = Command14.make(
2233
2876
  "whoami",
2234
2877
  {},
2235
- () => Effect22.gen(function* () {
2878
+ () => Effect26.gen(function* () {
2236
2879
  const auth = yield* AuthService;
2237
2880
  const info = yield* auth.getUserInfo;
2238
- yield* Console12.log(JSON.stringify(info, null, 2));
2881
+ yield* Console13.log(JSON.stringify(info, null, 2));
2239
2882
  }).pipe(
2240
- Effect22.catchTags({
2241
- NotAuthenticatedError: () => Console12.log('You are not logged in. Run "gigadrive login" to authenticate.'),
2242
- UserInfoFetchError: (err) => Console12.error(`Failed to retrieve user info: ${err.message}`),
2243
- OAuthDiscoveryError: (err) => Console12.error(`Failed to retrieve user info: ${err.message}`)
2883
+ Effect26.catchTags({
2884
+ NotAuthenticatedError: () => Console13.log('You are not logged in. Run "gigadrive login" to authenticate.'),
2885
+ UserInfoFetchError: (err) => Console13.error(`Failed to retrieve user info: ${err.message}`),
2886
+ OAuthDiscoveryError: (err) => Console13.error(`Failed to retrieve user info: ${err.message}`)
2244
2887
  })
2245
2888
  )
2246
2889
  );
2247
2890
 
2248
2891
  // src/index.ts
2249
- var gigadrive = Command14.make("gigadrive", {}, () => Effect23.void).pipe(
2250
- Command14.withSubcommands([
2892
+ var gigadrive = Command15.make("gigadrive", {}, () => Effect27.void).pipe(
2893
+ Command15.withSubcommands([
2251
2894
  loginCommand,
2252
2895
  logoutCommand,
2253
2896
  whoamiCommand,
@@ -2259,10 +2902,11 @@ var gigadrive = Command14.make("gigadrive", {}, () => Effect23.void).pipe(
2259
2902
  aiCommand,
2260
2903
  buildCommand,
2261
2904
  debugCommand,
2262
- platformCommand
2905
+ platformCommand,
2906
+ setupCommand
2263
2907
  ])
2264
2908
  );
2265
- var cli = Command14.run(gigadrive, {
2909
+ var cli = Command15.run(gigadrive, {
2266
2910
  name: "Gigadrive CLI",
2267
2911
  version: package_default.version
2268
2912
  });
@@ -2273,13 +2917,16 @@ var BaseServices = Layer.mergeAll(
2273
2917
  ArchiveService.Default,
2274
2918
  NetworkConfigLive2,
2275
2919
  ProjectConfigService.Default,
2276
- ProjectLinkService.Default
2920
+ ProjectLinkService.Default,
2921
+ EnvFileService.Default,
2922
+ DevCredentialsStore.Default
2277
2923
  ).pipe(Layer.provideMerge(AuthService.Default));
2278
2924
  var ApiClientLive = Layer.provide(ApiClientService.Default, BaseServices);
2279
2925
  var DeploymentApiLive = Layer.provide(DeploymentApiService.Default, BaseServices);
2280
- var ServicesLive = Layer.mergeAll(BaseServices, ApiClientLive, DeploymentApiLive);
2926
+ var LocalCredentialsLive = Layer.provide(LocalCredentialsService.Default, BaseServices);
2927
+ var ServicesLive = Layer.mergeAll(BaseServices, ApiClientLive, DeploymentApiLive, LocalCredentialsLive);
2281
2928
  var AppLive = Layer.mergeAll(ServicesLive, Logger.minimumLogLevel(LogLevel.Info)).pipe(
2282
2929
  Layer.provideMerge(NodeContext.layer)
2283
2930
  );
2284
- NodeRuntime.runMain(Effect23.provide(cli(process.argv), AppLive));
2931
+ NodeRuntime.runMain(Effect27.provide(cli(process.argv), AppLive));
2285
2932
  //# sourceMappingURL=index.mjs.map