gigadrive 2.1.3 → 2.3.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,16 +1,68 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command8 } 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 Effect15, Layer, LogLevel, Logger } from "effect";
7
+ import { Effect as Effect27, Layer, LogLevel, Logger } from "effect";
8
8
 
9
- // src/commands/build/index.ts
10
- import { Command } from "@effect/cli";
11
- import { FileSystem as FileSystem2, Path as Path2 } from "@effect/platform";
12
- import { exec as exec2 } from "@gigadrive/build-utils";
13
- import { Console, Effect as Effect2 } from "effect";
9
+ // package.json
10
+ var package_default = {
11
+ name: "gigadrive",
12
+ version: "2.3.0",
13
+ repository: {
14
+ type: "git",
15
+ url: "https://github.com/Gigadrive/sdk.git",
16
+ directory: "packages/cli"
17
+ },
18
+ license: "Apache-2.0",
19
+ author: "Gigadrive <business@gigadrivegroup.com> (https://gigadrive.de)",
20
+ type: "module",
21
+ module: "src/index.ts",
22
+ bin: {
23
+ gigadrive: "dist/index.mjs",
24
+ gn: "dist/index.mjs"
25
+ },
26
+ files: [
27
+ "dist"
28
+ ],
29
+ scripts: {
30
+ build: "tsup",
31
+ clean: "rm -rf dist",
32
+ dev: "pnpm build && node dist/index.mjs",
33
+ typecheck: "tsc --noEmit"
34
+ },
35
+ dependencies: {
36
+ "@effect/cli": "^0.75.0",
37
+ "@effect/platform": "^0.96.0",
38
+ "@effect/platform-node": "^0.106.0",
39
+ "@gigadrive/build-utils": "workspace:*",
40
+ "@gigadrive/commons": "workspace:*",
41
+ "@gigadrive/network-config": "workspace:*",
42
+ "@gigadrive/sdk": "workspace:*",
43
+ archiver: "^7.0.1",
44
+ effect: "^3.19.17",
45
+ ignore: "^7.0.5",
46
+ open: "^10.2.0"
47
+ },
48
+ devDependencies: {
49
+ "@effect/language-service": "^0.84.0",
50
+ "@types/archiver": "^6.0.3",
51
+ esbuild: "^0.28.0",
52
+ typescript: "catalog:"
53
+ },
54
+ engines: {
55
+ node: ">=20.0.0"
56
+ },
57
+ publishConfig: {
58
+ access: "public"
59
+ }
60
+ };
61
+
62
+ // src/commands/ai/index.ts
63
+ import { Args, Command, Options } from "@effect/cli";
64
+ import { FileSystem as FileSystem3 } from "@effect/platform";
65
+ import { Console as Console2, Effect as Effect6, Option as Option4 } from "effect";
14
66
 
15
67
  // src/errors.ts
16
68
  import { Schema } from "effect";
@@ -52,11 +104,82 @@ var UserInfoFetchError = class extends Schema.TaggedError()("UserInfoFetchError"
52
104
  statusCode: Schema.optional(Schema.Number)
53
105
  }) {
54
106
  };
107
+ var ApiRequestError = class extends Schema.TaggedError()("ApiRequestError", {
108
+ message: Schema.String,
109
+ statusCode: Schema.optional(Schema.Number),
110
+ code: Schema.optional(Schema.String)
111
+ }) {
112
+ };
113
+ var InvalidEnvVarFormatError = class extends Schema.TaggedError()(
114
+ "InvalidEnvVarFormatError",
115
+ {
116
+ message: Schema.String
117
+ }
118
+ ) {
119
+ };
120
+ var EnvVarNotFoundError = class extends Schema.TaggedError()("EnvVarNotFoundError", {
121
+ message: Schema.String
122
+ }) {
123
+ };
124
+ var NoApplicationsFoundError = class extends Schema.TaggedError()(
125
+ "NoApplicationsFoundError",
126
+ {
127
+ message: Schema.String
128
+ }
129
+ ) {
130
+ };
131
+ var ApplicationNotFoundError = class extends Schema.TaggedError()(
132
+ "ApplicationNotFoundError",
133
+ {
134
+ message: Schema.String
135
+ }
136
+ ) {
137
+ };
138
+ var EnvFileReadError = class extends Schema.TaggedError()("EnvFileReadError", {
139
+ message: Schema.String,
140
+ cause: Schema.optional(Schema.String)
141
+ }) {
142
+ };
143
+ var EnvFileWriteError = class extends Schema.TaggedError()("EnvFileWriteError", {
144
+ message: Schema.String,
145
+ cause: Schema.optional(Schema.String)
146
+ }) {
147
+ };
148
+ var DevCredentialsStoreReadError = class extends Schema.TaggedError()(
149
+ "DevCredentialsStoreReadError",
150
+ {
151
+ message: Schema.String
152
+ }
153
+ ) {
154
+ };
155
+ var DevCredentialsStoreWriteError = class extends Schema.TaggedError()(
156
+ "DevCredentialsStoreWriteError",
157
+ {
158
+ message: Schema.String,
159
+ cause: Schema.optional(Schema.String)
160
+ }
161
+ ) {
162
+ };
55
163
  var ConfigNotFoundError = class extends Schema.TaggedError()("ConfigNotFoundError", {
56
164
  message: Schema.String,
57
165
  directory: Schema.String
58
166
  }) {
59
167
  };
168
+ var ProjectNotLinkedError = class extends Schema.TaggedError()("ProjectNotLinkedError", {
169
+ message: Schema.String,
170
+ directory: Schema.String
171
+ }) {
172
+ };
173
+ var ProjectLinkReadError = class extends Schema.TaggedError()("ProjectLinkReadError", {
174
+ message: Schema.String,
175
+ cause: Schema.optional(Schema.String)
176
+ }) {
177
+ };
178
+ var ProjectLinkWriteError = class extends Schema.TaggedError()("ProjectLinkWriteError", {
179
+ message: Schema.String,
180
+ cause: Schema.optional(Schema.String)
181
+ }) {
182
+ };
60
183
  var ConfigParseError = class extends Schema.TaggedError()("ConfigParseError", {
61
184
  message: Schema.String,
62
185
  cause: Schema.optional(Schema.String)
@@ -137,6 +260,13 @@ var DeploymentLogsFetchError = class extends Schema.TaggedError()(
137
260
  }
138
261
  ) {
139
262
  };
263
+ var DeploymentHostnamesFetchError = class extends Schema.TaggedError()(
264
+ "DeploymentHostnamesFetchError",
265
+ {
266
+ message: Schema.String
267
+ }
268
+ ) {
269
+ };
140
270
  var ArchiveCreateError = class extends Schema.TaggedError()("ArchiveCreateError", {
141
271
  message: Schema.String,
142
272
  cause: Schema.optional(Schema.String)
@@ -151,95 +281,1040 @@ var DeploymentFailedError = class extends Schema.TaggedError()("DeploymentFailed
151
281
  }) {
152
282
  };
153
283
 
154
- // src/services/package-manager.ts
284
+ // src/services/api-client.ts
285
+ import { ApiError, GigadriveClient } from "@gigadrive/sdk";
286
+ import { Config as Config2, Effect as Effect4 } from "effect";
287
+
288
+ // src/services/auth.ts
289
+ import { Console, Effect as Effect3, Option as Option2 } from "effect";
290
+ import * as crypto from "crypto";
291
+ import * as http from "http";
292
+ import open from "open";
293
+
294
+ // src/services/auth-storage.ts
155
295
  import { FileSystem, Path } from "@effect/platform";
156
- import { exec } from "@gigadrive/build-utils";
157
- import { Effect } from "effect";
158
- var PackageManagerService = class extends Effect.Service()("PackageManagerService", {
296
+ import { Effect, Option, Schema as Schema3 } from "effect";
297
+ import * as os from "os";
298
+
299
+ // src/domain.ts
300
+ import { Schema as Schema2 } from "effect";
301
+ var DeploymentId = Schema2.String.pipe(Schema2.brand("@Gigadrive/DeploymentId"));
302
+ var ApplicationId = Schema2.String.pipe(Schema2.brand("@Gigadrive/ApplicationId"));
303
+ var UploadId = Schema2.String.pipe(Schema2.brand("@Gigadrive/UploadId"));
304
+ var PackageManager = Schema2.Literal("npm", "yarn", "pnpm", "bun");
305
+ var OAuthTokens = Schema2.Struct({
306
+ access_token: Schema2.String,
307
+ refresh_token: Schema2.optional(Schema2.String),
308
+ expires_in: Schema2.optional(Schema2.Number),
309
+ token_type: Schema2.optional(Schema2.String)
310
+ });
311
+ var OAuthClientConfig = Schema2.Struct({
312
+ clientId: Schema2.String,
313
+ issuer: Schema2.String,
314
+ authorizeUrl: Schema2.String,
315
+ tokenUrl: Schema2.String,
316
+ scope: Schema2.String,
317
+ userinfoUrl: Schema2.String
318
+ });
319
+ var StoredAuthData = Schema2.Struct({
320
+ refreshToken: Schema2.String,
321
+ accessToken: Schema2.optional(Schema2.String),
322
+ tokenExpirationTime: Schema2.optional(Schema2.Number)
323
+ });
324
+ var ProjectLink = Schema2.Struct({
325
+ applicationId: Schema2.String,
326
+ organizationId: Schema2.optional(Schema2.String)
327
+ });
328
+ var DevKeyEntry = Schema2.Struct({
329
+ apiKeyId: Schema2.String
330
+ });
331
+ var DevKeyStore = Schema2.Record({ key: Schema2.String, value: DevKeyEntry });
332
+
333
+ // src/services/auth-storage.ts
334
+ var AuthStorageService = class extends Effect.Service()("AuthStorageService", {
159
335
  accessors: true,
160
336
  effect: Effect.gen(function* () {
161
337
  const fs = yield* FileSystem.FileSystem;
162
338
  const pathService = yield* Path.Path;
163
- const detect = Effect.fn("PackageManagerService.detect")(function* (cwd) {
164
- yield* Effect.annotateCurrentSpan("cwd", cwd);
165
- const packageJsonPath = pathService.join(cwd, "package.json");
166
- const exists = yield* fs.exists(packageJsonPath).pipe(
339
+ const homeDir = os.homedir();
340
+ const authDir = pathService.join(homeDir, ".gigadrive");
341
+ const authFile = pathService.join(authDir, "auth.json");
342
+ const ensureDirectory = Effect.gen(function* () {
343
+ yield* fs.makeDirectory(authDir, { recursive: true }).pipe(
167
344
  Effect.mapError(
168
- () => new PackageManagerNotFoundError({ message: "Failed to check for package.json", directory: cwd })
345
+ (error) => new AuthStorageWriteError({
346
+ message: "Failed to create auth directory",
347
+ cause: String(error)
348
+ })
169
349
  )
170
350
  );
171
- if (!exists) {
172
- return yield* Effect.fail(
173
- new PackageManagerNotFoundError({
174
- message: "No package.json found",
175
- directory: cwd
351
+ yield* fs.chmod(authDir, 448).pipe(
352
+ Effect.mapError(
353
+ (error) => new AuthStorageWriteError({
354
+ message: "Failed to set auth directory permissions",
355
+ cause: String(error)
356
+ })
357
+ )
358
+ );
359
+ });
360
+ const load = Effect.gen(function* () {
361
+ const exists = yield* fs.exists(authFile).pipe(Effect.mapError(() => new AuthStorageReadError({ message: "Failed to check auth file existence" })));
362
+ if (!exists) return Option.none();
363
+ const data = yield* fs.readFileString(authFile, "utf8").pipe(Effect.mapError(() => new AuthStorageReadError({ message: "Failed to read auth file" })));
364
+ const json = yield* Effect.try({
365
+ try: () => JSON.parse(data),
366
+ catch: () => new AuthStorageReadError({ message: "Auth file contains invalid JSON" })
367
+ });
368
+ const authData = yield* Schema3.decodeUnknown(StoredAuthData)(json).pipe(
369
+ Effect.mapError(() => new AuthStorageReadError({ message: "Auth file has invalid schema" }))
370
+ );
371
+ return Option.some(authData);
372
+ });
373
+ const save = Effect.fn("AuthStorageService.save")(function* (authData) {
374
+ yield* ensureDirectory;
375
+ yield* fs.writeFileString(authFile, JSON.stringify(authData, null, 2), { mode: 384 }).pipe(
376
+ Effect.mapError(
377
+ (error) => new AuthStorageWriteError({
378
+ message: "Failed to write auth file",
379
+ cause: String(error)
380
+ })
381
+ )
382
+ );
383
+ yield* Effect.log("Auth data saved to local storage");
384
+ });
385
+ const remove = Effect.gen(function* () {
386
+ yield* fs.remove(authFile).pipe(
387
+ Effect.mapError((error) => {
388
+ const cause = error instanceof Error ? error : error.error;
389
+ const code = cause && typeof cause === "object" && "code" in cause ? cause.code : "";
390
+ return { isNotFound: code === "ENOENT" || String(error).includes("ENOENT"), original: error };
391
+ }),
392
+ Effect.catchAll(
393
+ (mapped) => mapped.isNotFound ? Effect.void : Effect.fail(
394
+ new AuthStorageWriteError({
395
+ message: "Failed to remove auth file",
396
+ cause: String(mapped.original)
397
+ })
398
+ )
399
+ )
400
+ );
401
+ yield* Effect.log("Auth data removed from local storage");
402
+ });
403
+ return { load, save, remove };
404
+ })
405
+ }) {
406
+ };
407
+
408
+ // src/services/oauth-config.ts
409
+ import { Config, Effect as Effect2, Schema as Schema4 } from "effect";
410
+ var OAuthEnvConfig = Config.all({
411
+ issuerUrl: Config.string("GIGADRIVE_NETWORK_OAUTH_ISSUER_URL").pipe(Config.withDefault("https://idp.gigadrive.de")),
412
+ // Default is the first-party CLI's public OAuth client (seeded in the Network
413
+ // repo). Override with GIGADRIVE_NETWORK_OAUTH_CLIENT_ID per environment.
414
+ clientId: Config.string("GIGADRIVE_NETWORK_OAUTH_CLIENT_ID").pipe(
415
+ Config.withDefault("0195c11c-0000-7000-8000-000000000001")
416
+ )
417
+ });
418
+ var OpenIdDiscoveryDocument = Schema4.Struct({
419
+ issuer: Schema4.optional(Schema4.String),
420
+ authorization_endpoint: Schema4.optional(Schema4.String),
421
+ token_endpoint: Schema4.optional(Schema4.String),
422
+ userinfo_endpoint: Schema4.optional(Schema4.String)
423
+ });
424
+ var CLI_SCOPES = [
425
+ // Identity / OIDC
426
+ "offline_access",
427
+ "openid",
428
+ "profile",
429
+ "email",
430
+ // Network API capabilities
431
+ "network:applications:read",
432
+ "network:env_vars:read",
433
+ "network:env_vars:write",
434
+ "network:env_vars:delete",
435
+ "network:deployments:read",
436
+ "network:deployments:write",
437
+ "network:deployments:trigger",
438
+ "network:ai_gateway:chat",
439
+ "network:ai_gateway:models",
440
+ "network:ai_gateway:usage:read",
441
+ "network:ai_gateway:budgets:read",
442
+ "network:ai_gateway:budgets:write",
443
+ "network:ai_gateway:policies:read",
444
+ "network:ai_gateway:policies:write",
445
+ // Provisioning local-dev API credentials
446
+ "platform:api_keys:read",
447
+ "platform:api_keys:write",
448
+ "platform:api_keys:delete"
449
+ ];
450
+ var DEFAULT_SCOPE = CLI_SCOPES.join(" ");
451
+ var OAuthConfigService = class extends Effect2.Service()("OAuthConfigService", {
452
+ accessors: true,
453
+ effect: Effect2.gen(function* () {
454
+ const envConfig = yield* OAuthEnvConfig;
455
+ let cached = null;
456
+ const getConfig = Effect2.gen(function* () {
457
+ if (cached) return cached;
458
+ const issuerBase = envConfig.issuerUrl.replace(/\/$/, "");
459
+ const discoveryUrl = `${issuerBase}/.well-known/openid-configuration`;
460
+ yield* Effect2.log("Fetching OIDC discovery document", { discoveryUrl });
461
+ const response = yield* Effect2.tryPromise({
462
+ try: (signal) => fetch(discoveryUrl, { signal }),
463
+ catch: (error) => new OAuthDiscoveryError({
464
+ message: "Failed to fetch OIDC discovery document",
465
+ cause: error instanceof Error ? error.message : String(error)
466
+ })
467
+ });
468
+ if (!response.ok) {
469
+ return yield* Effect2.fail(
470
+ new OAuthDiscoveryError({
471
+ message: `OIDC discovery returned ${response.status} ${response.statusText}`
176
472
  })
177
473
  );
178
474
  }
179
- const lockChecks = [
180
- { file: "yarn.lock", pm: "yarn" },
181
- { file: "pnpm-lock.yaml", pm: "pnpm" },
182
- { file: "bun.lockb", pm: "bun" },
183
- { file: "package-lock.json", pm: "npm" }
184
- ];
185
- for (const { file, pm: pm2 } of lockChecks) {
186
- const lockExists = yield* fs.exists(pathService.join(cwd, file)).pipe(Effect.catchAll(() => Effect.succeed(false)));
187
- if (lockExists) {
188
- yield* Effect.log("Package manager detected from lock file", { packageManager: pm2, lockFile: file });
189
- return pm2;
190
- }
191
- }
192
- const bunAvailable = yield* Effect.tryPromise({
193
- try: () => exec({ command: "bun --version", cwd }),
194
- catch: () => new ExecError({ message: "bun not found", command: "bun --version" })
195
- }).pipe(
196
- Effect.as(true),
197
- Effect.catchTag("ExecError", () => Effect.succeed(false))
475
+ const json = yield* Effect2.tryPromise({
476
+ try: () => response.json(),
477
+ catch: () => new OAuthDiscoveryError({
478
+ message: "Failed to parse OIDC discovery JSON"
479
+ })
480
+ });
481
+ const decoded = yield* Schema4.decodeUnknown(OpenIdDiscoveryDocument)(json).pipe(
482
+ Effect2.mapError(
483
+ () => new OAuthDiscoveryError({
484
+ message: "Invalid OIDC discovery document schema"
485
+ })
486
+ )
198
487
  );
199
- const pm = bunAvailable ? "bun" : "npm";
200
- yield* Effect.log("Package manager detected via fallback", { packageManager: pm });
201
- return pm;
488
+ if (!decoded.authorization_endpoint || !decoded.token_endpoint || !decoded.userinfo_endpoint) {
489
+ return yield* Effect2.fail(
490
+ new OAuthDiscoveryError({
491
+ message: "OIDC discovery returned incomplete endpoints"
492
+ })
493
+ );
494
+ }
495
+ const config = {
496
+ clientId: envConfig.clientId,
497
+ issuer: decoded.issuer ?? issuerBase,
498
+ authorizeUrl: decoded.authorization_endpoint,
499
+ tokenUrl: decoded.token_endpoint,
500
+ scope: DEFAULT_SCOPE,
501
+ userinfoUrl: decoded.userinfo_endpoint
502
+ };
503
+ cached = config;
504
+ yield* Effect2.log("OIDC discovery completed", { issuer: config.issuer });
505
+ return config;
202
506
  });
203
- const installCommand = (pm) => ({
204
- bun: "bun install",
205
- npm: "npm install",
206
- pnpm: "pnpm install",
207
- yarn: "yarn install"
208
- })[pm];
209
- const buildCommand2 = (pm) => ({
210
- bun: "bun run build",
211
- npm: "npm run build",
212
- pnpm: "pnpm build",
213
- yarn: "yarn build"
214
- })[pm];
215
- return { detect, installCommand, buildCommand: buildCommand2 };
507
+ return { getConfig };
216
508
  })
217
509
  }) {
218
510
  };
219
511
 
220
- // src/commands/build/index.ts
221
- var buildCommand = Command.make(
222
- "build",
223
- {},
224
- () => Effect2.gen(function* () {
225
- const pmService = yield* PackageManagerService;
226
- const fs = yield* FileSystem2.FileSystem;
227
- const pathService = yield* Path2.Path;
228
- const cwd = process.cwd();
229
- yield* Effect2.log("Starting build", { cwd });
230
- const packageJsonPath = pathService.join(cwd, "package.json");
231
- const packageJsonExists = yield* fs.exists(packageJsonPath).pipe(
232
- Effect2.mapError(
233
- () => new PackageJsonNotFoundError({ message: "Failed to check for package.json", directory: cwd })
234
- )
235
- );
512
+ // src/services/auth.ts
513
+ var escapeHtml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/\//g, "&#x2F;");
514
+ var base64URLEncode = (buffer) => buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
515
+ var generatePKCE = () => {
516
+ const codeVerifier = base64URLEncode(crypto.randomBytes(64));
517
+ const challengeBytes = crypto.createHash("sha256").update(codeVerifier, "utf8").digest();
518
+ const codeChallenge = base64URLEncode(challengeBytes);
519
+ return { codeVerifier, codeChallenge };
520
+ };
521
+ var AuthService = class extends Effect3.Service()("AuthService", {
522
+ accessors: true,
523
+ dependencies: [OAuthConfigService.Default, AuthStorageService.Default],
524
+ effect: Effect3.gen(function* () {
525
+ const oauthConfig = yield* OAuthConfigService;
526
+ const storage = yield* AuthStorageService;
527
+ let accessToken = null;
528
+ let tokenExpirationTime = 0;
529
+ const logout = Effect3.gen(function* () {
530
+ accessToken = null;
531
+ tokenExpirationTime = 0;
532
+ yield* storage.remove;
533
+ yield* Effect3.log("Logged out");
534
+ });
535
+ const exchangeCodeForTokens = (config, authCode, redirectUri, codeVerifier) => Effect3.gen(function* () {
536
+ const response = yield* Effect3.tryPromise({
537
+ try: () => fetch(config.tokenUrl, {
538
+ method: "POST",
539
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
540
+ body: new URLSearchParams({
541
+ grant_type: "authorization_code",
542
+ client_id: config.clientId,
543
+ code: authCode,
544
+ redirect_uri: redirectUri,
545
+ code_verifier: codeVerifier
546
+ }).toString()
547
+ }),
548
+ catch: (error) => new TokenExchangeError({
549
+ message: `Token exchange request failed: ${error instanceof Error ? error.message : String(error)}`
550
+ })
551
+ });
552
+ if (!response.ok) {
553
+ const body = yield* Effect3.tryPromise({
554
+ try: () => response.text(),
555
+ catch: () => new TokenExchangeError({ message: "Failed to read token exchange error response" })
556
+ });
557
+ return yield* Effect3.fail(
558
+ new TokenExchangeError({
559
+ message: `Token exchange failed: ${response.status} ${response.statusText} - ${body}`,
560
+ statusCode: response.status
561
+ })
562
+ );
563
+ }
564
+ const tokens = yield* Effect3.tryPromise({
565
+ try: () => response.json(),
566
+ catch: () => new TokenExchangeError({ message: "Failed to parse token exchange response" })
567
+ });
568
+ return tokens;
569
+ });
570
+ const startCallbackServer = (config, codeChallenge, state) => Effect3.async((resume) => {
571
+ const server = http.createServer();
572
+ const redirectPath = "/callback";
573
+ server.listen(0, "127.0.0.1", () => {
574
+ const address = server.address();
575
+ if (!address || typeof address !== "object") {
576
+ server.close(() => {
577
+ resume(Effect3.fail(new LoginFlowError({ message: "Failed to determine callback server address" })));
578
+ });
579
+ return;
580
+ }
581
+ const port = address.port;
582
+ const redirectUri = `http://127.0.0.1:${port}${redirectPath}`;
583
+ const authUrl = new URL(config.authorizeUrl);
584
+ authUrl.searchParams.append("client_id", config.clientId);
585
+ authUrl.searchParams.append("redirect_uri", redirectUri);
586
+ authUrl.searchParams.append("response_type", "code");
587
+ authUrl.searchParams.append("scope", config.scope);
588
+ authUrl.searchParams.append("code_challenge", codeChallenge);
589
+ authUrl.searchParams.append("code_challenge_method", "S256");
590
+ authUrl.searchParams.append("state", state);
591
+ Console.log(`Please open this URL in your browser to log in:
592
+ ${authUrl.toString()}
593
+ `).pipe(Effect3.runSync);
594
+ Console.log("Waiting for login callback...").pipe(Effect3.runSync);
595
+ void open(authUrl.toString());
596
+ server.on("request", (req, res) => {
597
+ const reqUrl = new URL(req.url || "", redirectUri);
598
+ if (reqUrl.pathname !== "/callback") {
599
+ res.writeHead(404).end("Not Found");
600
+ return;
601
+ }
602
+ const authCode = reqUrl.searchParams.get("code");
603
+ const receivedState = reqUrl.searchParams.get("state");
604
+ const oauthError = reqUrl.searchParams.get("error");
605
+ res.writeHead(200, { "Content-Type": "text/html", Connection: "close" });
606
+ if (oauthError) {
607
+ const safeOauthError = escapeHtml(oauthError);
608
+ res.end(`<h1>Login Failed!</h1><p>Error: ${safeOauthError}</p><p>You can close this tab.</p>`);
609
+ server.close();
610
+ resume(Effect3.fail(new LoginFlowError({ message: `OAuth error: ${oauthError}` })));
611
+ return;
612
+ }
613
+ if (receivedState !== state) {
614
+ res.end(
615
+ "<h1>Login Failed!</h1><p>State mismatch. Possible CSRF attack.</p><p>You can close this tab.</p>"
616
+ );
617
+ server.close();
618
+ resume(Effect3.fail(new LoginFlowError({ message: "State mismatch" })));
619
+ return;
620
+ }
621
+ if (authCode) {
622
+ res.end("<h1>Login Successful!</h1><p>You can close this tab.</p>");
623
+ server.close();
624
+ resume(Effect3.succeed({ authCode, redirectUri }));
625
+ } else {
626
+ res.end("<h1>Login Failed!</h1><p>No authorization code received.</p><p>You can close this tab.</p>");
627
+ server.close();
628
+ resume(Effect3.fail(new LoginFlowError({ message: "No authorization code received" })));
629
+ }
630
+ });
631
+ server.on("error", (err) => {
632
+ server.close();
633
+ resume(Effect3.fail(new LoginFlowError({ message: `Local server error: ${err.message}` })));
634
+ });
635
+ });
636
+ });
637
+ const login = Effect3.gen(function* () {
638
+ yield* logout;
639
+ const { codeVerifier, codeChallenge } = generatePKCE();
640
+ const state = crypto.randomBytes(16).toString("hex");
641
+ const config = yield* oauthConfig.getConfig;
642
+ const { authCode, redirectUri } = yield* startCallbackServer(config, codeChallenge, state);
643
+ const tokens = yield* exchangeCodeForTokens(config, authCode, redirectUri, codeVerifier);
644
+ if (!tokens.refresh_token) {
645
+ return yield* Effect3.fail(
646
+ new TokenExchangeError({ message: "No refresh token received from identity provider" })
647
+ );
648
+ }
649
+ accessToken = tokens.access_token;
650
+ tokenExpirationTime = Date.now() + (tokens.expires_in || 3600) * 1e3;
651
+ yield* storage.save({
652
+ refreshToken: tokens.refresh_token,
653
+ accessToken,
654
+ tokenExpirationTime
655
+ });
656
+ yield* Effect3.log("Login successful");
657
+ return true;
658
+ });
659
+ const refreshAccessToken = Effect3.gen(function* () {
660
+ const stored = yield* storage.load.pipe(
661
+ Effect3.catchTag("AuthStorageReadError", (err) => Effect3.fail(new TokenRefreshError({ message: err.message })))
662
+ );
663
+ const config = yield* oauthConfig.getConfig.pipe(
664
+ Effect3.catchTag("OAuthDiscoveryError", (err) => Effect3.fail(new TokenRefreshError({ message: err.message })))
665
+ );
666
+ const authData = yield* Option2.match(stored, {
667
+ onNone: () => Effect3.fail(new TokenRefreshError({ message: "No refresh token available. Please log in." })),
668
+ onSome: (data) => data.refreshToken ? Effect3.succeed(data) : Effect3.fail(new TokenRefreshError({ message: "No refresh token available. Please log in." }))
669
+ });
670
+ const response = yield* Effect3.tryPromise({
671
+ try: () => fetch(config.tokenUrl, {
672
+ method: "POST",
673
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
674
+ body: new URLSearchParams({
675
+ grant_type: "refresh_token",
676
+ client_id: config.clientId,
677
+ refresh_token: authData.refreshToken
678
+ }).toString()
679
+ }),
680
+ catch: (error) => new TokenRefreshError({
681
+ message: `Token refresh request failed: ${error instanceof Error ? error.message : String(error)}`
682
+ })
683
+ });
684
+ if (!response.ok) {
685
+ if (response.status === 400) {
686
+ yield* logout.pipe(Effect3.catchAll(() => Effect3.void));
687
+ return yield* Effect3.fail(
688
+ new TokenRefreshError({
689
+ message: "Refresh token is invalid or expired. Please log in again.",
690
+ statusCode: response.status
691
+ })
692
+ );
693
+ }
694
+ const body = yield* Effect3.tryPromise({
695
+ try: () => response.text(),
696
+ catch: () => new TokenRefreshError({ message: "Failed to read refresh error response" })
697
+ });
698
+ return yield* Effect3.fail(
699
+ new TokenRefreshError({
700
+ message: `Token refresh failed: ${response.status} ${response.statusText} - ${body}`,
701
+ statusCode: response.status
702
+ })
703
+ );
704
+ }
705
+ const tokens = yield* Effect3.tryPromise({
706
+ try: () => response.json(),
707
+ catch: () => new TokenRefreshError({ message: "Failed to parse token refresh response" })
708
+ });
709
+ accessToken = tokens.access_token;
710
+ tokenExpirationTime = Date.now() + (tokens.expires_in || 3600) * 1e3;
711
+ const newRefreshToken = tokens.refresh_token || authData.refreshToken;
712
+ yield* storage.save({
713
+ refreshToken: newRefreshToken,
714
+ accessToken,
715
+ tokenExpirationTime
716
+ }).pipe(Effect3.catchAll(() => Effect3.void));
717
+ yield* Effect3.log("Access token refreshed successfully");
718
+ return true;
719
+ });
720
+ const getAccessToken = Effect3.gen(function* () {
721
+ if (accessToken && Date.now() < tokenExpirationTime) {
722
+ return accessToken;
723
+ }
724
+ const stored = yield* storage.load.pipe(
725
+ Effect3.catchAll(() => Effect3.succeed(Option2.none()))
726
+ );
727
+ Option2.match(stored, {
728
+ onNone: () => {
729
+ },
730
+ onSome: (data) => {
731
+ if (data.accessToken && data.tokenExpirationTime && Date.now() < data.tokenExpirationTime) {
732
+ accessToken = data.accessToken;
733
+ tokenExpirationTime = data.tokenExpirationTime;
734
+ }
735
+ }
736
+ });
737
+ if (accessToken && Date.now() < tokenExpirationTime) {
738
+ return accessToken;
739
+ }
740
+ yield* Effect3.log("Access token expired or not available. Attempting to refresh...");
741
+ yield* refreshAccessToken.pipe(
742
+ Effect3.catchTag("TokenRefreshError", (err) => Effect3.fail(new NotAuthenticatedError({ message: err.message })))
743
+ );
744
+ if (!accessToken) {
745
+ return yield* Effect3.fail(
746
+ new NotAuthenticatedError({ message: "No valid access token available. Please log in first." })
747
+ );
748
+ }
749
+ return accessToken;
750
+ });
751
+ const getUserInfo = Effect3.gen(function* () {
752
+ const config = yield* oauthConfig.getConfig;
753
+ const token = yield* getAccessToken;
754
+ const fetchUserInfo = (t) => Effect3.tryPromise({
755
+ try: () => fetch(config.userinfoUrl, {
756
+ headers: {
757
+ Authorization: `Bearer ${t}`,
758
+ Accept: "application/json"
759
+ }
760
+ }),
761
+ catch: (error) => new UserInfoFetchError({
762
+ message: `Failed to fetch user info: ${error instanceof Error ? error.message : String(error)}`
763
+ })
764
+ });
765
+ let response = yield* fetchUserInfo(token);
766
+ if (response.status === 401 || response.status === 403) {
767
+ yield* refreshAccessToken.pipe(
768
+ Effect3.catchTag("TokenRefreshError", (err) => Effect3.fail(new UserInfoFetchError({ message: err.message })))
769
+ );
770
+ if (!accessToken) {
771
+ return yield* Effect3.fail(new UserInfoFetchError({ message: "Token refresh failed" }));
772
+ }
773
+ response = yield* fetchUserInfo(accessToken);
774
+ }
775
+ if (!response.ok) {
776
+ const body = yield* Effect3.tryPromise({
777
+ try: () => response.text(),
778
+ catch: () => new UserInfoFetchError({ message: "Failed to read user info error response" })
779
+ });
780
+ return yield* Effect3.fail(
781
+ new UserInfoFetchError({
782
+ message: `Failed to fetch user info: ${response.status} ${response.statusText}${body ? ` - ${body}` : ""}`,
783
+ statusCode: response.status
784
+ })
785
+ );
786
+ }
787
+ const data = yield* Effect3.tryPromise({
788
+ try: () => response.json(),
789
+ catch: () => new UserInfoFetchError({ message: "Failed to parse user info response" })
790
+ });
791
+ return data;
792
+ });
793
+ const inferUserName = (info) => {
794
+ const isNonEmptyString = (value) => typeof value === "string" && value.trim() !== "";
795
+ const name = info.name;
796
+ const givenName = info.given_name;
797
+ const familyName = info.family_name;
798
+ const preferredUsername = info.preferred_username;
799
+ const email = info.email;
800
+ return isNonEmptyString(name) && name || (isNonEmptyString(givenName) && isNonEmptyString(familyName) ? `${givenName} ${familyName}` : void 0) || isNonEmptyString(preferredUsername) && preferredUsername || isNonEmptyString(email) && email || "your account";
801
+ };
802
+ return { login, logout, getAccessToken, getUserInfo, refreshAccessToken, inferUserName };
803
+ })
804
+ }) {
805
+ };
806
+
807
+ // src/services/api-client.ts
808
+ var ApiBaseUrl = Config2.string("GIGADRIVE_API_BASE_URL").pipe(Config2.withDefault("https://api.gigadrive.network"));
809
+ var toApiRequestError = (error) => {
810
+ if (error instanceof ApiError) {
811
+ return new ApiRequestError({ message: error.message, statusCode: error.status, code: error.code });
812
+ }
813
+ return new ApiRequestError({ message: error instanceof Error ? error.message : String(error) });
814
+ };
815
+ var ApiClientService = class extends Effect4.Service()("ApiClientService", {
816
+ accessors: true,
817
+ effect: Effect4.gen(function* () {
818
+ const baseUrl = yield* ApiBaseUrl;
819
+ const authService = yield* AuthService;
820
+ const getClient = Effect4.gen(function* () {
821
+ const token = yield* authService.getAccessToken;
822
+ return new GigadriveClient({ bearerToken: token, baseUrl });
823
+ });
824
+ const request = (f) => Effect4.gen(function* () {
825
+ const client = yield* getClient;
826
+ return yield* Effect4.tryPromise({
827
+ try: () => f(client),
828
+ catch: toApiRequestError
829
+ });
830
+ });
831
+ return { getClient, request };
832
+ })
833
+ }) {
834
+ };
835
+
836
+ // src/services/project-link.ts
837
+ import { FileSystem as FileSystem2, Path as Path2 } from "@effect/platform";
838
+ import { Effect as Effect5, Option as Option3, Schema as Schema5 } from "effect";
839
+ var LINK_DIR = ".gigadrive";
840
+ var LINK_FILE = "project.json";
841
+ var ProjectLinkService = class extends Effect5.Service()("ProjectLinkService", {
842
+ accessors: true,
843
+ effect: Effect5.gen(function* () {
844
+ const fs = yield* FileSystem2.FileSystem;
845
+ const pathService = yield* Path2.Path;
846
+ const linkDir = (projectFolder) => pathService.join(projectFolder, LINK_DIR);
847
+ const linkFile = (projectFolder) => pathService.join(projectFolder, LINK_DIR, LINK_FILE);
848
+ const load = Effect5.fn("ProjectLinkService.load")(function* (projectFolder) {
849
+ const file = linkFile(projectFolder);
850
+ const exists = yield* fs.exists(file).pipe(Effect5.mapError(() => new ProjectLinkReadError({ message: "Failed to check project link existence" })));
851
+ if (!exists) return Option3.none();
852
+ const data = yield* fs.readFileString(file, "utf8").pipe(Effect5.mapError(() => new ProjectLinkReadError({ message: "Failed to read project link file" })));
853
+ const json = yield* Effect5.try({
854
+ try: () => JSON.parse(data),
855
+ catch: () => new ProjectLinkReadError({ message: `${LINK_DIR}/${LINK_FILE} contains invalid JSON` })
856
+ });
857
+ const link = yield* Schema5.decodeUnknown(ProjectLink)(json).pipe(
858
+ Effect5.mapError(() => new ProjectLinkReadError({ message: `${LINK_DIR}/${LINK_FILE} has an invalid schema` }))
859
+ );
860
+ return Option3.some(link);
861
+ });
862
+ const resolve = Effect5.fn("ProjectLinkService.resolve")(function* (projectFolder) {
863
+ const link = yield* load(projectFolder);
864
+ return yield* Option3.match(link, {
865
+ onNone: () => Effect5.fail(
866
+ new ProjectNotLinkedError({
867
+ message: 'This directory is not linked to an application. Run "gigadrive link" first.',
868
+ directory: projectFolder
869
+ })
870
+ ),
871
+ onSome: (value) => Effect5.succeed(value)
872
+ });
873
+ });
874
+ const save = Effect5.fn("ProjectLinkService.save")(function* (projectFolder, link) {
875
+ yield* fs.makeDirectory(linkDir(projectFolder), { recursive: true }).pipe(
876
+ Effect5.mapError(
877
+ (error) => new ProjectLinkWriteError({ message: "Failed to create .gigadrive directory", cause: String(error) })
878
+ )
879
+ );
880
+ yield* fs.writeFileString(linkFile(projectFolder), `${JSON.stringify(link, null, 2)}
881
+ `).pipe(
882
+ Effect5.mapError(
883
+ (error) => new ProjectLinkWriteError({ message: "Failed to write project link file", cause: String(error) })
884
+ )
885
+ );
886
+ yield* Effect5.log("Project link saved", { applicationId: link.applicationId });
887
+ });
888
+ const remove = Effect5.fn("ProjectLinkService.remove")(function* (projectFolder) {
889
+ yield* fs.remove(linkFile(projectFolder), { force: true }).pipe(
890
+ Effect5.mapError(
891
+ (error) => new ProjectLinkWriteError({ message: "Failed to remove project link file", cause: String(error) })
892
+ )
893
+ );
894
+ });
895
+ return { load, resolve, save, remove };
896
+ })
897
+ }) {
898
+ };
899
+
900
+ // src/commands/ai/index.ts
901
+ var orgOption = Options.text("org").pipe(
902
+ Options.withAlias("o"),
903
+ Options.withDescription("Organization ID (defaults to the linked organization)"),
904
+ Options.optional
905
+ );
906
+ var appOption = Options.text("app").pipe(
907
+ Options.withAlias("a"),
908
+ Options.withDescription("Restrict to this application ID"),
909
+ Options.optional
910
+ );
911
+ var fromOption = Options.text("from").pipe(
912
+ Options.withDescription("Include usage at or after this ISO 8601 timestamp"),
913
+ Options.optional
914
+ );
915
+ var toOption = Options.text("to").pipe(
916
+ Options.withDescription("Include usage before or at this ISO 8601 timestamp"),
917
+ Options.optional
918
+ );
919
+ var fileOption = Options.text("file").pipe(
920
+ Options.withAlias("f"),
921
+ Options.withDescription('Path to a JSON file, or "-" for stdin (default: stdin)'),
922
+ Options.optional
923
+ );
924
+ var resolveOrgId = (org) => Effect6.gen(function* () {
925
+ if (Option4.isSome(org)) return org.value;
926
+ const projectLink = yield* ProjectLinkService;
927
+ const link = yield* projectLink.resolve(process.cwd());
928
+ if (link.organizationId === void 0) {
929
+ return yield* Effect6.fail(
930
+ new ProjectNotLinkedError({
931
+ message: 'No organization is linked. Pass --org <id> or re-run "gigadrive link".',
932
+ directory: process.cwd()
933
+ })
934
+ );
935
+ }
936
+ return link.organizationId;
937
+ });
938
+ var readStdin = Effect6.async((resume) => {
939
+ let data = "";
940
+ const stdin = process.stdin;
941
+ stdin.setEncoding("utf8");
942
+ const onData = (chunk) => {
943
+ data += chunk;
944
+ };
945
+ const cleanup = () => {
946
+ stdin.off("data", onData);
947
+ stdin.off("end", onEnd);
948
+ stdin.off("error", onError);
949
+ };
950
+ const onEnd = () => {
951
+ cleanup();
952
+ resume(Effect6.succeed(data));
953
+ };
954
+ const onError = (err) => {
955
+ cleanup();
956
+ resume(Effect6.fail(new ApiRequestError({ message: `Failed to read stdin: ${err.message}` })));
957
+ };
958
+ stdin.on("data", onData);
959
+ stdin.on("end", onEnd);
960
+ stdin.on("error", onError);
961
+ stdin.resume();
962
+ });
963
+ var readJsonInput = (file) => Effect6.gen(function* () {
964
+ const path = Option4.getOrUndefined(file);
965
+ let raw;
966
+ if (path === void 0 || path === "-") {
967
+ raw = yield* readStdin;
968
+ } else {
969
+ const fs = yield* FileSystem3.FileSystem;
970
+ raw = yield* fs.readFileString(path, "utf8").pipe(Effect6.mapError((e) => new ApiRequestError({ message: `Failed to read ${path}: ${String(e)}` })));
971
+ }
972
+ return yield* Effect6.try({
973
+ try: () => JSON.parse(raw),
974
+ catch: () => new ApiRequestError({ message: "Input is not valid JSON." })
975
+ });
976
+ });
977
+ var governanceErrorHandlers = {
978
+ NotAuthenticatedError: (err) => Console2.error('You are not logged in. Run "gigadrive login" to authenticate.').pipe(
979
+ Effect6.andThen(Effect6.fail(err))
980
+ ),
981
+ ApiRequestError: (err) => Console2.error(err.message).pipe(Effect6.andThen(Effect6.fail(err))),
982
+ ProjectNotLinkedError: (err) => Console2.error(err.message).pipe(Effect6.andThen(Effect6.fail(err))),
983
+ ProjectLinkReadError: (err) => Console2.error(err.message).pipe(Effect6.andThen(Effect6.fail(err)))
984
+ };
985
+ var usageSummaryCommand = Command.make(
986
+ "summary",
987
+ { org: orgOption, app: appOption, from: fromOption, to: toOption },
988
+ ({ org, app, from, to }) => Effect6.gen(function* () {
989
+ const apiClient = yield* ApiClientService;
990
+ const orgId = yield* resolveOrgId(org);
991
+ const summary = yield* apiClient.request(
992
+ (client) => client.organizations.aiGateway.usage.summary(orgId, {
993
+ applicationId: Option4.getOrUndefined(app),
994
+ from: Option4.getOrUndefined(from),
995
+ to: Option4.getOrUndefined(to)
996
+ })
997
+ );
998
+ yield* Console2.log(JSON.stringify(summary, null, 2));
999
+ }).pipe(Effect6.catchTags(governanceErrorHandlers))
1000
+ );
1001
+ var usageRequestsCommand = Command.make(
1002
+ "requests",
1003
+ { org: orgOption, app: appOption, from: fromOption, to: toOption },
1004
+ ({ org, app, from, to }) => Effect6.gen(function* () {
1005
+ const apiClient = yield* ApiClientService;
1006
+ const orgId = yield* resolveOrgId(org);
1007
+ const { items, total } = yield* apiClient.request(
1008
+ (client) => client.organizations.aiGateway.usage.requests(orgId, {
1009
+ applicationId: Option4.getOrUndefined(app),
1010
+ from: Option4.getOrUndefined(from),
1011
+ to: Option4.getOrUndefined(to)
1012
+ })
1013
+ );
1014
+ if (items.length === 0) {
1015
+ yield* Console2.log("No AI Gateway requests found.");
1016
+ return;
1017
+ }
1018
+ for (const e of items) {
1019
+ yield* Console2.log(
1020
+ `${e.createdAt} ${e.modelRequested} ${e.status} ${e.totalTokens} tok ${e.billableCostMicros}\xB5$`
1021
+ );
1022
+ }
1023
+ yield* Console2.log(`
1024
+ ${total} request(s).`);
1025
+ }).pipe(Effect6.catchTags(governanceErrorHandlers))
1026
+ );
1027
+ var outOption = Options.text("out").pipe(
1028
+ Options.withDescription("Write the CSV to this file instead of stdout"),
1029
+ Options.optional
1030
+ );
1031
+ var usageExportCommand = Command.make(
1032
+ "export",
1033
+ { org: orgOption, app: appOption, from: fromOption, to: toOption, out: outOption },
1034
+ ({ org, app, from, to, out }) => Effect6.gen(function* () {
1035
+ const apiClient = yield* ApiClientService;
1036
+ const orgId = yield* resolveOrgId(org);
1037
+ const csv = yield* apiClient.request(
1038
+ (client) => client.organizations.aiGateway.usage.export(orgId, {
1039
+ applicationId: Option4.getOrUndefined(app),
1040
+ from: Option4.getOrUndefined(from),
1041
+ to: Option4.getOrUndefined(to)
1042
+ })
1043
+ );
1044
+ const outPath = Option4.getOrUndefined(out);
1045
+ if (outPath === void 0) {
1046
+ yield* Console2.log(csv);
1047
+ } else {
1048
+ const fs = yield* FileSystem3.FileSystem;
1049
+ yield* fs.writeFileString(outPath, csv).pipe(Effect6.mapError((e) => new ApiRequestError({ message: `Failed to write ${outPath}: ${String(e)}` })));
1050
+ yield* Console2.log(`Wrote ${outPath}.`);
1051
+ }
1052
+ }).pipe(Effect6.catchTags(governanceErrorHandlers))
1053
+ );
1054
+ var usageCommand = Command.make("usage", {}, () => Effect6.void).pipe(
1055
+ Command.withSubcommands([usageSummaryCommand, usageRequestsCommand, usageExportCommand])
1056
+ );
1057
+ var budgetsGetCommand = Command.make(
1058
+ "get",
1059
+ { org: orgOption },
1060
+ ({ org }) => Effect6.gen(function* () {
1061
+ const apiClient = yield* ApiClientService;
1062
+ const orgId = yield* resolveOrgId(org);
1063
+ const { items } = yield* apiClient.request((client) => client.organizations.aiGateway.budgets.list(orgId));
1064
+ yield* Console2.log(JSON.stringify(items, null, 2));
1065
+ }).pipe(Effect6.catchTags(governanceErrorHandlers))
1066
+ );
1067
+ var budgetsSetCommand = Command.make(
1068
+ "set",
1069
+ { org: orgOption, file: fileOption },
1070
+ ({ org, file }) => Effect6.gen(function* () {
1071
+ const apiClient = yield* ApiClientService;
1072
+ const orgId = yield* resolveOrgId(org);
1073
+ const parsed = yield* readJsonInput(file);
1074
+ const budgets = Array.isArray(parsed) ? parsed : parsed.budgets;
1075
+ if (!Array.isArray(budgets)) {
1076
+ return yield* Effect6.fail(
1077
+ new ApiRequestError({
1078
+ message: 'Expected a JSON array of budgets, or an object of the form { "budgets": [...] }.'
1079
+ })
1080
+ );
1081
+ }
1082
+ const { items } = yield* apiClient.request(
1083
+ (client) => client.organizations.aiGateway.budgets.replace(orgId, budgets)
1084
+ );
1085
+ yield* Console2.log(`Saved ${items.length} budget(s).`);
1086
+ }).pipe(Effect6.catchTags(governanceErrorHandlers))
1087
+ );
1088
+ var budgetsCommand = Command.make("budgets", {}, () => Effect6.void).pipe(
1089
+ Command.withSubcommands([budgetsGetCommand, budgetsSetCommand])
1090
+ );
1091
+ var policiesGetCommand = Command.make(
1092
+ "get",
1093
+ { org: orgOption, app: appOption },
1094
+ ({ org, app }) => Effect6.gen(function* () {
1095
+ const apiClient = yield* ApiClientService;
1096
+ const orgId = yield* resolveOrgId(org);
1097
+ const policy = yield* apiClient.request(
1098
+ (client) => client.organizations.aiGateway.policies.get(orgId, { applicationId: Option4.getOrUndefined(app) })
1099
+ );
1100
+ yield* Console2.log(policy === null ? "No policy set." : JSON.stringify(policy, null, 2));
1101
+ }).pipe(Effect6.catchTags(governanceErrorHandlers))
1102
+ );
1103
+ var policiesSetCommand = Command.make(
1104
+ "set",
1105
+ { org: orgOption, file: fileOption },
1106
+ ({ org, file }) => Effect6.gen(function* () {
1107
+ const apiClient = yield* ApiClientService;
1108
+ const orgId = yield* resolveOrgId(org);
1109
+ const parsed = yield* readJsonInput(file);
1110
+ const policy = yield* apiClient.request(
1111
+ (client) => client.organizations.aiGateway.policies.put(orgId, parsed)
1112
+ );
1113
+ yield* Console2.log(`Policy saved${policy.applicationId ? ` for application ${policy.applicationId}` : ""}.`);
1114
+ }).pipe(Effect6.catchTags(governanceErrorHandlers))
1115
+ );
1116
+ var policiesCommand = Command.make("policies", {}, () => Effect6.void).pipe(
1117
+ Command.withSubcommands([policiesGetCommand, policiesSetCommand])
1118
+ );
1119
+ var inferenceErrorHandlers = {
1120
+ NotAuthenticatedError: (err) => Console2.error('You are not logged in. Run "gigadrive login" to authenticate.').pipe(
1121
+ Effect6.andThen(Effect6.fail(err))
1122
+ ),
1123
+ ApiRequestError: (err) => Console2.error(err.message).pipe(Effect6.andThen(Effect6.fail(err)))
1124
+ };
1125
+ var modelsListCommand = Command.make(
1126
+ "list",
1127
+ {},
1128
+ () => Effect6.gen(function* () {
1129
+ const apiClient = yield* ApiClientService;
1130
+ const { items } = yield* apiClient.request((client) => client.aiGateway.listModels());
1131
+ for (const m of items) {
1132
+ yield* Console2.log(`${m.id} ${m.name} (ctx ${m.contextWindow})`);
1133
+ }
1134
+ }).pipe(Effect6.catchTags(inferenceErrorHandlers))
1135
+ );
1136
+ var modelIdArg = Args.text({ name: "model-id" }).pipe(Args.withDescription("The model ID, e.g. openai/gpt-4o"));
1137
+ var modelsGetCommand = Command.make(
1138
+ "get",
1139
+ { modelId: modelIdArg },
1140
+ ({ modelId }) => Effect6.gen(function* () {
1141
+ const apiClient = yield* ApiClientService;
1142
+ const model = yield* apiClient.request((client) => client.aiGateway.getModel(modelId));
1143
+ yield* Console2.log(JSON.stringify(model, null, 2));
1144
+ }).pipe(Effect6.catchTags(inferenceErrorHandlers))
1145
+ );
1146
+ var modelsCommand = Command.make("models", {}, () => Effect6.void).pipe(
1147
+ Command.withSubcommands([modelsListCommand, modelsGetCommand])
1148
+ );
1149
+ var modelOption = Options.text("model").pipe(
1150
+ Options.withAlias("m"),
1151
+ Options.withDescription("The model ID to use, e.g. openai/gpt-4o")
1152
+ );
1153
+ var streamOption = Options.boolean("stream").pipe(Options.withDescription("Stream the response token by token"));
1154
+ var promptArg = Args.text({ name: "prompt" }).pipe(
1155
+ Args.withDescription('The prompt text, or "-" to read from stdin')
1156
+ );
1157
+ var chatCommand = Command.make(
1158
+ "chat",
1159
+ { prompt: promptArg, model: modelOption, stream: streamOption },
1160
+ ({ prompt, model, stream }) => Effect6.gen(function* () {
1161
+ const apiClient = yield* ApiClientService;
1162
+ const text = prompt === "-" ? yield* readStdin : prompt;
1163
+ const messages = [{ role: "user", content: text }];
1164
+ if (stream) {
1165
+ const client = yield* apiClient.getClient;
1166
+ yield* Effect6.tryPromise({
1167
+ try: async () => {
1168
+ for await (const chunk of client.aiGateway.chatCompletionsStream({ model, messages })) {
1169
+ process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
1170
+ }
1171
+ process.stdout.write("\n");
1172
+ },
1173
+ catch: (error) => new ApiRequestError({ message: error instanceof Error ? error.message : String(error) })
1174
+ });
1175
+ return;
1176
+ }
1177
+ const response = yield* apiClient.request((client) => client.aiGateway.chatCompletions({ model, messages }));
1178
+ const content = response.choices[0]?.message.content;
1179
+ yield* Console2.log(typeof content === "string" ? content : JSON.stringify(content));
1180
+ }).pipe(Effect6.catchTags(inferenceErrorHandlers))
1181
+ );
1182
+ var aiBase = Command.make("ai", {}, () => Effect6.void);
1183
+ var aiCommand = aiBase.pipe(
1184
+ Command.withSubcommands([usageCommand, budgetsCommand, policiesCommand, modelsCommand, chatCommand])
1185
+ );
1186
+
1187
+ // src/commands/apps/index.ts
1188
+ import { Command as Command2, Options as Options2 } from "@effect/cli";
1189
+ import { Console as Console3, Effect as Effect7, Option as Option5 } from "effect";
1190
+ var orgOption2 = Options2.text("org").pipe(
1191
+ Options2.withAlias("o"),
1192
+ Options2.withDescription("Only list applications belonging to this organization ID"),
1193
+ Options2.optional
1194
+ );
1195
+ var appsListCommand = Command2.make(
1196
+ "list",
1197
+ { org: orgOption2 },
1198
+ ({ org }) => Effect7.gen(function* () {
1199
+ const apiClient = yield* ApiClientService;
1200
+ const organizationId = Option5.getOrUndefined(org);
1201
+ const { items, total } = yield* apiClient.request((client) => client.applications.list({ organizationId }));
1202
+ if (items.length === 0) {
1203
+ yield* Console3.log("No applications found.");
1204
+ return;
1205
+ }
1206
+ for (const app of items) {
1207
+ yield* Console3.log(`${app.id} ${app.name} (${app.organization.name})`);
1208
+ }
1209
+ yield* Console3.log(`
1210
+ ${total} application(s).`);
1211
+ }).pipe(
1212
+ Effect7.catchTags({
1213
+ NotAuthenticatedError: (err) => Console3.error('You are not logged in. Run "gigadrive login" to authenticate.').pipe(
1214
+ Effect7.andThen(Effect7.fail(err))
1215
+ ),
1216
+ ApiRequestError: (err) => Console3.error(`Failed to list applications: ${err.message}`).pipe(Effect7.andThen(Effect7.fail(err)))
1217
+ })
1218
+ )
1219
+ );
1220
+ var appsBase = Command2.make("apps", {}, () => Effect7.void);
1221
+ var appsCommand = appsBase.pipe(Command2.withSubcommands([appsListCommand]));
1222
+
1223
+ // src/commands/build/index.ts
1224
+ import { Command as Command3 } from "@effect/cli";
1225
+ import { FileSystem as FileSystem5, Path as Path4 } from "@effect/platform";
1226
+ import { exec as exec2 } from "@gigadrive/build-utils";
1227
+ import { Console as Console4, Effect as Effect9 } from "effect";
1228
+
1229
+ // src/services/package-manager.ts
1230
+ import { FileSystem as FileSystem4, Path as Path3 } from "@effect/platform";
1231
+ import { exec } from "@gigadrive/build-utils";
1232
+ import { Effect as Effect8 } from "effect";
1233
+ var PackageManagerService = class extends Effect8.Service()("PackageManagerService", {
1234
+ accessors: true,
1235
+ effect: Effect8.gen(function* () {
1236
+ const fs = yield* FileSystem4.FileSystem;
1237
+ const pathService = yield* Path3.Path;
1238
+ const detect = Effect8.fn("PackageManagerService.detect")(function* (cwd) {
1239
+ yield* Effect8.annotateCurrentSpan("cwd", cwd);
1240
+ const packageJsonPath = pathService.join(cwd, "package.json");
1241
+ const exists = yield* fs.exists(packageJsonPath).pipe(
1242
+ Effect8.mapError(
1243
+ () => new PackageManagerNotFoundError({ message: "Failed to check for package.json", directory: cwd })
1244
+ )
1245
+ );
1246
+ if (!exists) {
1247
+ return yield* Effect8.fail(
1248
+ new PackageManagerNotFoundError({
1249
+ message: "No package.json found",
1250
+ directory: cwd
1251
+ })
1252
+ );
1253
+ }
1254
+ const lockChecks = [
1255
+ { file: "yarn.lock", pm: "yarn" },
1256
+ { file: "pnpm-lock.yaml", pm: "pnpm" },
1257
+ { file: "bun.lockb", pm: "bun" },
1258
+ { file: "package-lock.json", pm: "npm" }
1259
+ ];
1260
+ for (const { file, pm: pm2 } of lockChecks) {
1261
+ const lockExists = yield* fs.exists(pathService.join(cwd, file)).pipe(Effect8.catchAll(() => Effect8.succeed(false)));
1262
+ if (lockExists) {
1263
+ yield* Effect8.log("Package manager detected from lock file", { packageManager: pm2, lockFile: file });
1264
+ return pm2;
1265
+ }
1266
+ }
1267
+ const bunAvailable = yield* Effect8.tryPromise({
1268
+ try: () => exec({ command: "bun --version", cwd }),
1269
+ catch: () => new ExecError({ message: "bun not found", command: "bun --version" })
1270
+ }).pipe(
1271
+ Effect8.as(true),
1272
+ Effect8.catchTag("ExecError", () => Effect8.succeed(false))
1273
+ );
1274
+ const pm = bunAvailable ? "bun" : "npm";
1275
+ yield* Effect8.log("Package manager detected via fallback", { packageManager: pm });
1276
+ return pm;
1277
+ });
1278
+ const installCommand = (pm) => ({
1279
+ bun: "bun install",
1280
+ npm: "npm install",
1281
+ pnpm: "pnpm install",
1282
+ yarn: "yarn install"
1283
+ })[pm];
1284
+ const buildCommand2 = (pm) => ({
1285
+ bun: "bun run build",
1286
+ npm: "npm run build",
1287
+ pnpm: "pnpm build",
1288
+ yarn: "yarn build"
1289
+ })[pm];
1290
+ return { detect, installCommand, buildCommand: buildCommand2 };
1291
+ })
1292
+ }) {
1293
+ };
1294
+
1295
+ // src/commands/build/index.ts
1296
+ var buildCommand = Command3.make(
1297
+ "build",
1298
+ {},
1299
+ () => Effect9.gen(function* () {
1300
+ const pmService = yield* PackageManagerService;
1301
+ const fs = yield* FileSystem5.FileSystem;
1302
+ const pathService = yield* Path4.Path;
1303
+ const cwd = process.cwd();
1304
+ yield* Effect9.log("Starting build", { cwd });
1305
+ const packageJsonPath = pathService.join(cwd, "package.json");
1306
+ const packageJsonExists = yield* fs.exists(packageJsonPath).pipe(
1307
+ Effect9.mapError(
1308
+ () => new PackageJsonNotFoundError({ message: "Failed to check for package.json", directory: cwd })
1309
+ )
1310
+ );
236
1311
  if (!packageJsonExists) {
237
- return yield* Effect2.fail(new PackageJsonNotFoundError({ message: "No package.json found", directory: cwd }));
1312
+ return yield* Effect9.fail(new PackageJsonNotFoundError({ message: "No package.json found", directory: cwd }));
238
1313
  }
239
1314
  const packageJsonContent = yield* fs.readFileString(packageJsonPath, "utf-8").pipe(
240
- Effect2.mapError(() => new PackageJsonNotFoundError({ message: "Failed to read package.json", directory: cwd }))
1315
+ Effect9.mapError(() => new PackageJsonNotFoundError({ message: "Failed to read package.json", directory: cwd }))
241
1316
  );
242
- const packageJson = yield* Effect2.try({
1317
+ const packageJson = yield* Effect9.try({
243
1318
  try: () => JSON.parse(packageJsonContent),
244
1319
  catch: (error) => new PackageJsonParseError({
245
1320
  message: "Failed to parse package.json",
@@ -248,7 +1323,7 @@ var buildCommand = Command.make(
248
1323
  })
249
1324
  });
250
1325
  if (!packageJson.scripts?.build) {
251
- return yield* Effect2.fail(new BuildScriptNotFoundError({ message: "No build script found in package.json" }));
1326
+ return yield* Effect9.fail(new BuildScriptNotFoundError({ message: "No build script found in package.json" }));
252
1327
  }
253
1328
  const pm = yield* pmService.detect(cwd);
254
1329
  const buildEnv = {
@@ -259,14 +1334,14 @@ var buildCommand = Command.make(
259
1334
  NOW_BUILDER: "1"
260
1335
  };
261
1336
  const installCmd = pmService.installCommand(pm);
262
- yield* Console.log(`Running: ${installCmd}`);
263
- yield* Effect2.tryPromise({
1337
+ yield* Console4.log(`Running: ${installCmd}`);
1338
+ yield* Effect9.tryPromise({
264
1339
  try: () => exec2({
265
1340
  command: installCmd,
266
1341
  cwd,
267
1342
  env: buildEnv,
268
1343
  onOutput: (chunk) => {
269
- Console.log(chunk).pipe(Effect2.runSync);
1344
+ Console4.log(chunk).pipe(Effect9.runSync);
270
1345
  }
271
1346
  }),
272
1347
  catch: (error) => new ExecError({
@@ -276,14 +1351,14 @@ var buildCommand = Command.make(
276
1351
  })
277
1352
  });
278
1353
  const buildCmd = pmService.buildCommand(pm);
279
- yield* Console.log(`Running: ${buildCmd}`);
280
- yield* Effect2.tryPromise({
1354
+ yield* Console4.log(`Running: ${buildCmd}`);
1355
+ yield* Effect9.tryPromise({
281
1356
  try: () => exec2({
282
1357
  command: buildCmd,
283
1358
  cwd,
284
1359
  env: buildEnv,
285
1360
  onOutput: (chunk) => {
286
- Console.log(chunk).pipe(Effect2.runSync);
1361
+ Console4.log(chunk).pipe(Effect9.runSync);
287
1362
  }
288
1363
  }),
289
1364
  catch: (error) => new ExecError({
@@ -292,25 +1367,25 @@ var buildCommand = Command.make(
292
1367
  cause: error instanceof Error ? error.message : String(error)
293
1368
  })
294
1369
  });
295
- yield* Console.log("Build completed successfully.");
1370
+ yield* Console4.log("Build completed successfully.");
296
1371
  }).pipe(
297
- Effect2.catchTags({
298
- PackageJsonNotFoundError: (err) => Console.error(err.message).pipe(Effect2.andThen(Effect2.fail(err))),
299
- PackageJsonParseError: (err) => Console.error(err.message).pipe(Effect2.andThen(Effect2.fail(err))),
300
- BuildScriptNotFoundError: (err) => Console.error(err.message).pipe(Effect2.andThen(Effect2.fail(err))),
301
- PackageManagerNotFoundError: (err) => Console.error(err.message).pipe(Effect2.andThen(Effect2.fail(err))),
302
- ExecError: (err) => Console.error(`${err.message}: ${err.cause ?? err.command}`).pipe(Effect2.andThen(Effect2.fail(err)))
1372
+ Effect9.catchTags({
1373
+ PackageJsonNotFoundError: (err) => Console4.error(err.message).pipe(Effect9.andThen(Effect9.fail(err))),
1374
+ PackageJsonParseError: (err) => Console4.error(err.message).pipe(Effect9.andThen(Effect9.fail(err))),
1375
+ BuildScriptNotFoundError: (err) => Console4.error(err.message).pipe(Effect9.andThen(Effect9.fail(err))),
1376
+ PackageManagerNotFoundError: (err) => Console4.error(err.message).pipe(Effect9.andThen(Effect9.fail(err))),
1377
+ ExecError: (err) => Console4.error(`${err.message}: ${err.cause ?? err.command}`).pipe(Effect9.andThen(Effect9.fail(err)))
303
1378
  })
304
1379
  )
305
1380
  );
306
1381
 
307
1382
  // src/commands/debug/index.ts
308
- import { Command as Command3 } from "@effect/cli";
309
- import { Effect as Effect5 } from "effect";
1383
+ import { Command as Command5 } from "@effect/cli";
1384
+ import { Effect as Effect12 } from "effect";
310
1385
 
311
1386
  // src/commands/debug/config/index.ts
312
- import { Command as Command2 } from "@effect/cli";
313
- import { Console as Console2, Effect as Effect4 } from "effect";
1387
+ import { Command as Command4 } from "@effect/cli";
1388
+ import { Console as Console5, Effect as Effect11 } from "effect";
314
1389
 
315
1390
  // src/services/project-config.ts
316
1391
  import {
@@ -322,39 +1397,39 @@ import {
322
1397
  postProcessConfig,
323
1398
  RawConfigReader
324
1399
  } from "@gigadrive/network-config";
325
- import { Effect as Effect3 } from "effect";
1400
+ import { Effect as Effect10 } from "effect";
326
1401
  var wrapParseErrors = (effect) => effect.pipe(
327
- Effect3.catchTags({
328
- ConfigFileNotFoundError: (e) => Effect3.fail(new ConfigParseError({ message: e.message, cause: e.filePath })),
329
- ConfigFileEmptyError: (e) => Effect3.fail(new ConfigParseError({ message: e.message, cause: e.filePath })),
330
- ConfigFileParseError: (e) => Effect3.fail(new ConfigParseError({ message: e.message, cause: e.cause })),
331
- ConfigVersionError: (e) => Effect3.fail(new ConfigParseError({ message: e.message, cause: e.filePath })),
332
- ConfigSchemaValidationError: (e) => Effect3.fail(new ConfigParseError({ message: e.message, cause: e.validationErrors.join(", ") })),
333
- FunctionConfigError: (e) => Effect3.fail(new ConfigParseError({ message: e.message, cause: e.functionPath }))
1402
+ Effect10.catchTags({
1403
+ ConfigFileNotFoundError: (e) => Effect10.fail(new ConfigParseError({ message: e.message, cause: e.filePath })),
1404
+ ConfigFileEmptyError: (e) => Effect10.fail(new ConfigParseError({ message: e.message, cause: e.filePath })),
1405
+ ConfigFileParseError: (e) => Effect10.fail(new ConfigParseError({ message: e.message, cause: e.cause })),
1406
+ ConfigVersionError: (e) => Effect10.fail(new ConfigParseError({ message: e.message, cause: e.filePath })),
1407
+ ConfigSchemaValidationError: (e) => Effect10.fail(new ConfigParseError({ message: e.message, cause: e.validationErrors.join(", ") })),
1408
+ FunctionConfigError: (e) => Effect10.fail(new ConfigParseError({ message: e.message, cause: e.functionPath }))
334
1409
  })
335
1410
  );
336
- var ProjectConfigService = class extends Effect3.Service()("ProjectConfigService", {
1411
+ var ProjectConfigService = class extends Effect10.Service()("ProjectConfigService", {
337
1412
  accessors: true,
338
1413
  dependencies: [NetworkConfigLive],
339
- effect: Effect3.gen(function* () {
1414
+ effect: Effect10.gen(function* () {
340
1415
  const rawReader = yield* RawConfigReader;
341
- const resolve = Effect3.fn("ProjectConfigService.resolve")(function* (cwd) {
342
- yield* Effect3.annotateCurrentSpan("cwd", cwd);
343
- yield* Effect3.log("Resolving project config", { cwd });
1416
+ const resolve = Effect10.fn("ProjectConfigService.resolve")(function* (cwd) {
1417
+ yield* Effect10.annotateCurrentSpan("cwd", cwd);
1418
+ yield* Effect10.log("Resolving project config", { cwd });
344
1419
  const configPath = yield* rawReader.findConfig(cwd);
345
1420
  const detection = yield* detectFramework(cwd).pipe(
346
- Effect3.catchTag("FrameworkNotDetectedError", () => Effect3.succeed(null)),
347
- Effect3.catchTag(
1421
+ Effect10.catchTag("FrameworkNotDetectedError", () => Effect10.succeed(null)),
1422
+ Effect10.catchTag(
348
1423
  "ManifestReadError",
349
- (e) => Effect3.fail(new ConfigParseError({ message: `Framework detection failed: ${e.message}`, cause: e.filePath }))
1424
+ (e) => Effect10.fail(new ConfigParseError({ message: `Framework detection failed: ${e.message}`, cause: e.filePath }))
350
1425
  ),
351
- Effect3.catchTag(
1426
+ Effect10.catchTag(
352
1427
  "ManifestParseError",
353
- (e) => Effect3.fail(new ConfigParseError({ message: `Framework detection failed: ${e.message}`, cause: e.filePath }))
1428
+ (e) => Effect10.fail(new ConfigParseError({ message: `Framework detection failed: ${e.message}`, cause: e.filePath }))
354
1429
  )
355
1430
  );
356
1431
  if (detection) {
357
- yield* Effect3.log("Framework auto-detected", {
1432
+ yield* Effect10.log("Framework auto-detected", {
358
1433
  framework: detection.framework.name,
359
1434
  slug: detection.framework.slug
360
1435
  });
@@ -363,21 +1438,21 @@ var ProjectConfigService = class extends Effect3.Service()("ProjectConfigService
363
1438
  let resolvedConfigPath = null;
364
1439
  let framework;
365
1440
  if (configPath && detection) {
366
- yield* Effect3.log("Config file found, merging with framework defaults", { configPath });
1441
+ yield* Effect10.log("Config file found, merging with framework defaults", { configPath });
367
1442
  resolvedConfigPath = configPath;
368
1443
  const userConfig = yield* wrapParseErrors(parseConfigRaw(configPath, cwd));
369
1444
  const merged = yield* mergeWithFrameworkDefaults(userConfig, detection.config);
370
1445
  config = yield* wrapParseErrors(postProcessConfig(merged, cwd));
371
1446
  framework = { name: detection.framework.name, slug: detection.framework.slug };
372
1447
  } else if (configPath) {
373
- yield* Effect3.log("Config file found", { configPath });
1448
+ yield* Effect10.log("Config file found", { configPath });
374
1449
  resolvedConfigPath = configPath;
375
1450
  config = yield* wrapParseErrors(parseConfig(configPath, cwd));
376
1451
  } else if (detection) {
377
1452
  config = yield* wrapParseErrors(postProcessConfig(detection.config, cwd));
378
1453
  framework = { name: detection.framework.name, slug: detection.framework.slug };
379
1454
  } else {
380
- return yield* Effect3.fail(
1455
+ return yield* Effect10.fail(
381
1456
  new ConfigNotFoundError({
382
1457
  message: "No config file found and no framework detected.",
383
1458
  directory: cwd
@@ -385,10 +1460,10 @@ var ProjectConfigService = class extends Effect3.Service()("ProjectConfigService
385
1460
  );
386
1461
  }
387
1462
  for (const warning of config.warnings) {
388
- yield* Effect3.logWarning(warning);
1463
+ yield* Effect10.logWarning(warning);
389
1464
  }
390
1465
  if (config.errors.length > 0) {
391
- return yield* Effect3.fail(
1466
+ return yield* Effect10.fail(
392
1467
  new ConfigValidationError({
393
1468
  message: "Config file has validation errors",
394
1469
  errors: config.errors
@@ -403,583 +1478,728 @@ var ProjectConfigService = class extends Effect3.Service()("ProjectConfigService
403
1478
  };
404
1479
 
405
1480
  // src/commands/debug/config/index.ts
406
- var debugConfigCommand = Command2.make(
1481
+ var debugConfigCommand = Command4.make(
407
1482
  "config",
408
1483
  {},
409
- () => Effect4.gen(function* () {
1484
+ () => Effect11.gen(function* () {
410
1485
  const projectConfig = yield* ProjectConfigService;
411
1486
  const cwd = process.cwd();
412
1487
  const { config, configPath } = yield* projectConfig.resolve(cwd);
413
- yield* Console2.log(`Config file found at: ${configPath}`);
414
- yield* Console2.log(JSON.stringify(config, null, 2));
1488
+ yield* Console5.log(`Config file found at: ${configPath}`);
1489
+ yield* Console5.log(JSON.stringify(config, null, 2));
415
1490
  }).pipe(
416
- Effect4.catchTags({
417
- ConfigNotFoundError: (err) => Console2.error(err.message),
418
- ConfigParseError: (err) => Console2.error(`Config parse error: ${err.message}`),
419
- ConfigValidationError: (err) => Effect4.gen(function* () {
1491
+ Effect11.catchTags({
1492
+ ConfigNotFoundError: (err) => Console5.error(err.message),
1493
+ ConfigParseError: (err) => Console5.error(`Config parse error: ${err.message}`),
1494
+ ConfigValidationError: (err) => Effect11.gen(function* () {
420
1495
  for (const e of err.errors) {
421
- yield* Console2.error(e);
1496
+ yield* Console5.error(e);
422
1497
  }
423
- yield* Console2.error(err.message);
1498
+ yield* Console5.error(err.message);
424
1499
  })
425
1500
  })
426
1501
  )
427
1502
  );
428
1503
 
429
1504
  // src/commands/debug/index.ts
430
- var debugBase = Command3.make("debug", {}, () => Effect5.void);
431
- var debugCommand = debugBase.pipe(Command3.withSubcommands([debugConfigCommand]));
432
-
433
- // src/commands/login/index.ts
434
- import { Command as Command4 } from "@effect/cli";
435
- import { Console as Console4, Effect as Effect9 } from "effect";
436
-
437
- // src/services/auth.ts
438
- import { Console as Console3, Effect as Effect8, Option as Option2 } from "effect";
439
- import * as crypto from "crypto";
440
- import * as http from "http";
441
- import open from "open";
442
-
443
- // src/services/auth-storage.ts
444
- import { FileSystem as FileSystem3, Path as Path3 } from "@effect/platform";
445
- import { Effect as Effect6, Option, Schema as Schema3 } from "effect";
446
- import * as os from "os";
447
-
448
- // src/domain.ts
449
- import { Schema as Schema2 } from "effect";
450
- var DeploymentId = Schema2.String.pipe(Schema2.brand("@Gigadrive/DeploymentId"));
451
- var ApplicationId = Schema2.String.pipe(Schema2.brand("@Gigadrive/ApplicationId"));
452
- var UploadId = Schema2.String.pipe(Schema2.brand("@Gigadrive/UploadId"));
453
- var DeploymentStatus = Schema2.Literal("PENDING", "BUILDING", "PROVISIONING", "FAILED", "ACTIVE", "SUSPENDED");
454
- var DeploymentLogType = Schema2.Literal("INFO", "ERROR", "WARN");
455
- var DeploymentLog = Schema2.Struct({
456
- id: Schema2.String,
457
- message: Schema2.String,
458
- type: DeploymentLogType,
459
- createdAt: Schema2.String
460
- });
461
- var DeploymentLogPage = Schema2.Struct({
462
- totalItems: Schema2.Number,
463
- limit: Schema2.Number,
464
- offset: Schema2.Number,
465
- items: Schema2.Array(DeploymentLog)
466
- });
467
- var PackageManager = Schema2.Literal("npm", "yarn", "pnpm", "bun");
468
- var OAuthTokens = Schema2.Struct({
469
- access_token: Schema2.String,
470
- refresh_token: Schema2.optional(Schema2.String),
471
- expires_in: Schema2.optional(Schema2.Number),
472
- token_type: Schema2.optional(Schema2.String)
473
- });
474
- var OAuthClientConfig = Schema2.Struct({
475
- clientId: Schema2.String,
476
- issuer: Schema2.String,
477
- authorizeUrl: Schema2.String,
478
- tokenUrl: Schema2.String,
479
- scope: Schema2.String,
480
- userinfoUrl: Schema2.String
481
- });
482
- var StoredAuthData = Schema2.Struct({
483
- refreshToken: Schema2.String,
484
- accessToken: Schema2.optional(Schema2.String),
485
- tokenExpirationTime: Schema2.optional(Schema2.Number)
486
- });
1505
+ var debugBase = Command5.make("debug", {}, () => Effect12.void);
1506
+ var debugCommand = debugBase.pipe(Command5.withSubcommands([debugConfigCommand]));
487
1507
 
488
- // src/services/auth-storage.ts
489
- var AuthStorageService = class extends Effect6.Service()("AuthStorageService", {
490
- accessors: true,
491
- effect: Effect6.gen(function* () {
492
- const fs = yield* FileSystem3.FileSystem;
493
- const pathService = yield* Path3.Path;
494
- const homeDir = os.homedir();
495
- const authDir = pathService.join(homeDir, ".gigadrive");
496
- const authFile = pathService.join(authDir, "auth.json");
497
- const ensureDirectory = Effect6.gen(function* () {
498
- yield* fs.makeDirectory(authDir, { recursive: true }).pipe(
499
- Effect6.mapError(
500
- (error) => new AuthStorageWriteError({
501
- message: "Failed to create auth directory",
502
- cause: String(error)
503
- })
504
- )
505
- );
506
- yield* fs.chmod(authDir, 448).pipe(
507
- Effect6.mapError(
508
- (error) => new AuthStorageWriteError({
509
- message: "Failed to set auth directory permissions",
510
- cause: String(error)
511
- })
512
- )
513
- );
514
- });
515
- const load = Effect6.gen(function* () {
516
- const exists = yield* fs.exists(authFile).pipe(Effect6.mapError(() => new AuthStorageReadError({ message: "Failed to check auth file existence" })));
517
- if (!exists) return Option.none();
518
- const data = yield* fs.readFileString(authFile, "utf8").pipe(Effect6.mapError(() => new AuthStorageReadError({ message: "Failed to read auth file" })));
519
- const json = yield* Effect6.try({
520
- try: () => JSON.parse(data),
521
- catch: () => new AuthStorageReadError({ message: "Auth file contains invalid JSON" })
522
- });
523
- const authData = yield* Schema3.decodeUnknown(StoredAuthData)(json).pipe(
524
- Effect6.mapError(() => new AuthStorageReadError({ message: "Auth file has invalid schema" }))
525
- );
526
- return Option.some(authData);
527
- });
528
- const save = Effect6.fn("AuthStorageService.save")(function* (authData) {
529
- yield* ensureDirectory;
530
- yield* fs.writeFileString(authFile, JSON.stringify(authData, null, 2), { mode: 384 }).pipe(
531
- Effect6.mapError(
532
- (error) => new AuthStorageWriteError({
533
- message: "Failed to write auth file",
534
- cause: String(error)
535
- })
536
- )
537
- );
538
- yield* Effect6.log("Auth data saved to local storage");
539
- });
540
- const remove = Effect6.gen(function* () {
541
- yield* fs.remove(authFile).pipe(
542
- Effect6.mapError((error) => {
543
- const cause = error instanceof Error ? error : error.error;
544
- const code = cause && typeof cause === "object" && "code" in cause ? cause.code : "";
545
- return { isNotFound: code === "ENOENT" || String(error).includes("ENOENT"), original: error };
546
- }),
547
- Effect6.catchAll(
548
- (mapped) => mapped.isNotFound ? Effect6.void : Effect6.fail(
549
- new AuthStorageWriteError({
550
- message: "Failed to remove auth file",
551
- cause: String(mapped.original)
552
- })
553
- )
554
- )
555
- );
556
- yield* Effect6.log("Auth data removed from local storage");
557
- });
558
- return { load, save, remove };
1508
+ // src/commands/deployments/index.ts
1509
+ import { Args as Args2, Command as Command6, Options as Options3 } from "@effect/cli";
1510
+ import { Console as Console6, Effect as Effect13, Option as Option6 } from "effect";
1511
+ var DEPLOYMENT_STATUSES = ["PENDING", "QUEUED", "STARTING", "BUILDING", "PROVISIONING", "ACTIVE", "FAILED"];
1512
+ var appOption2 = Options3.text("app").pipe(
1513
+ Options3.withAlias("a"),
1514
+ Options3.withDescription("Filter by application ID (defaults to the linked application)"),
1515
+ Options3.optional
1516
+ );
1517
+ var orgOption3 = Options3.text("org").pipe(
1518
+ Options3.withAlias("o"),
1519
+ Options3.withDescription("Filter by organization ID"),
1520
+ Options3.optional
1521
+ );
1522
+ var statusOption = Options3.choice("status", DEPLOYMENT_STATUSES).pipe(
1523
+ Options3.withAlias("s"),
1524
+ Options3.withDescription("Filter by deployment status"),
1525
+ Options3.optional
1526
+ );
1527
+ var resolveApplicationFilter = (app) => Option6.match(app, {
1528
+ onSome: (id) => Effect13.succeed(id),
1529
+ onNone: () => Effect13.gen(function* () {
1530
+ const projectLink = yield* ProjectLinkService;
1531
+ const link = yield* projectLink.load(process.cwd());
1532
+ return Option6.match(link, { onNone: () => void 0, onSome: (l) => l.applicationId });
559
1533
  })
560
- }) {
561
- };
562
-
563
- // src/services/oauth-config.ts
564
- import { Config, Effect as Effect7, Schema as Schema4 } from "effect";
565
- var OAuthEnvConfig = Config.all({
566
- issuerUrl: Config.string("GIGADRIVE_NETWORK_OAUTH_ISSUER_URL").pipe(Config.withDefault("https://idp.gigadrive.de")),
567
- clientId: Config.string("GIGADRIVE_NETWORK_OAUTH_CLIENT_ID").pipe(Config.withDefault("todo_add_client_id"))
568
- });
569
- var OpenIdDiscoveryDocument = Schema4.Struct({
570
- issuer: Schema4.optional(Schema4.String),
571
- authorization_endpoint: Schema4.optional(Schema4.String),
572
- token_endpoint: Schema4.optional(Schema4.String),
573
- userinfo_endpoint: Schema4.optional(Schema4.String)
574
1534
  });
575
- var DEFAULT_SCOPE = "offline_access openid profile email";
576
- var OAuthConfigService = class extends Effect7.Service()("OAuthConfigService", {
577
- accessors: true,
578
- effect: Effect7.gen(function* () {
579
- const envConfig = yield* OAuthEnvConfig;
580
- let cached = null;
581
- const getConfig = Effect7.gen(function* () {
582
- if (cached) return cached;
583
- const issuerBase = envConfig.issuerUrl.replace(/\/$/, "");
584
- const discoveryUrl = `${issuerBase}/.well-known/openid-configuration`;
585
- yield* Effect7.log("Fetching OIDC discovery document", { discoveryUrl });
586
- const response = yield* Effect7.tryPromise({
587
- try: (signal) => fetch(discoveryUrl, { signal }),
588
- catch: (error) => new OAuthDiscoveryError({
589
- message: "Failed to fetch OIDC discovery document",
590
- cause: error instanceof Error ? error.message : String(error)
591
- })
592
- });
593
- if (!response.ok) {
594
- return yield* Effect7.fail(
595
- new OAuthDiscoveryError({
596
- message: `OIDC discovery returned ${response.status} ${response.statusText}`
597
- })
598
- );
599
- }
600
- const json = yield* Effect7.tryPromise({
601
- try: () => response.json(),
602
- catch: () => new OAuthDiscoveryError({
603
- message: "Failed to parse OIDC discovery JSON"
604
- })
1535
+ var deploymentsListCommand = Command6.make(
1536
+ "list",
1537
+ { app: appOption2, org: orgOption3, status: statusOption },
1538
+ ({ app, org, status }) => Effect13.gen(function* () {
1539
+ const apiClient = yield* ApiClientService;
1540
+ const applicationId = yield* resolveApplicationFilter(app);
1541
+ const { items, total } = yield* apiClient.request(
1542
+ (client) => client.deployments.list({
1543
+ applicationId,
1544
+ organizationId: Option6.getOrUndefined(org),
1545
+ status: Option6.getOrUndefined(status)
1546
+ })
1547
+ );
1548
+ if (items.length === 0) {
1549
+ yield* Console6.log("No deployments found.");
1550
+ return;
1551
+ }
1552
+ for (const d of items) {
1553
+ yield* Console6.log(`${d.id} ${d.status.padEnd(12)} ${d.createdAt}`);
1554
+ }
1555
+ yield* Console6.log(`
1556
+ ${total} deployment(s).`);
1557
+ }).pipe(
1558
+ Effect13.catchTags({
1559
+ NotAuthenticatedError: (err) => Console6.error('You are not logged in. Run "gigadrive login" to authenticate.').pipe(
1560
+ Effect13.andThen(Effect13.fail(err))
1561
+ ),
1562
+ ApiRequestError: (err) => Console6.error(`Failed to list deployments: ${err.message}`).pipe(Effect13.andThen(Effect13.fail(err))),
1563
+ ProjectLinkReadError: (err) => Console6.error(err.message).pipe(Effect13.andThen(Effect13.fail(err)))
1564
+ })
1565
+ )
1566
+ );
1567
+ var deploymentIdArg = Args2.text({ name: "deployment-id" }).pipe(Args2.withDescription("The deployment ID to inspect"));
1568
+ var deploymentsInspectCommand = Command6.make(
1569
+ "inspect",
1570
+ { deploymentId: deploymentIdArg },
1571
+ ({ deploymentId }) => Effect13.gen(function* () {
1572
+ const apiClient = yield* ApiClientService;
1573
+ const deployment = yield* apiClient.request((client) => client.deployments.get(deploymentId));
1574
+ yield* Console6.log(`ID: ${deployment.id}`);
1575
+ yield* Console6.log(`Application: ${deployment.applicationId}`);
1576
+ yield* Console6.log(`Status: ${deployment.status}`);
1577
+ yield* Console6.log(`Created: ${deployment.createdAt}`);
1578
+ yield* Console6.log(`Updated: ${deployment.updatedAt}`);
1579
+ const hostnames = yield* apiClient.request((client) => client.deployments.getHostnames(deploymentId)).pipe(Effect13.catchAll(() => Effect13.succeed({ items: [], total: 0 })));
1580
+ if (hostnames.items.length > 0) {
1581
+ yield* Console6.log("Hostnames:");
1582
+ for (const h of hostnames.items) {
1583
+ yield* Console6.log(` https://${h.hostname}${h.active ? "" : " (inactive)"}`);
1584
+ }
1585
+ }
1586
+ }).pipe(
1587
+ Effect13.catchTags({
1588
+ NotAuthenticatedError: (err) => Console6.error('You are not logged in. Run "gigadrive login" to authenticate.').pipe(
1589
+ Effect13.andThen(Effect13.fail(err))
1590
+ ),
1591
+ ApiRequestError: (err) => Console6.error(`Failed to inspect deployment: ${err.message}`).pipe(Effect13.andThen(Effect13.fail(err)))
1592
+ })
1593
+ )
1594
+ );
1595
+ var deploymentsBase = Command6.make("deployments", {}, () => Effect13.void);
1596
+ var deploymentsCommand = deploymentsBase.pipe(
1597
+ Command6.withSubcommands([deploymentsListCommand, deploymentsInspectCommand])
1598
+ );
1599
+
1600
+ // src/commands/env/index.ts
1601
+ import { Args as Args3, Command as Command7, Options as Options4, Prompt } from "@effect/cli";
1602
+ import { Console as Console7, Effect as Effect17, Option as Option10 } from "effect";
1603
+
1604
+ // src/services/env-file.ts
1605
+ import { FileSystem as FileSystem6, Path as Path5 } from "@effect/platform";
1606
+ import { Effect as Effect14, Option as Option7 } from "effect";
1607
+
1608
+ // src/lib/dotenv.ts
1609
+ var DEFAULT_ENV_HEADER = "# Generated by `gigadrive env pull` \u2014 do not edit by hand.";
1610
+ var UNQUOTED_SAFE = /^[A-Za-z0-9_.\-/@:+]*$/;
1611
+ var formatEnvValue = (value) => {
1612
+ if (UNQUOTED_SAFE.test(value)) {
1613
+ return value;
1614
+ }
1615
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
1616
+ return `"${escaped}"`;
1617
+ };
1618
+ var serializeEnv = (entries, opts) => {
1619
+ const header = opts?.header ?? DEFAULT_ENV_HEADER;
1620
+ if (entries.length === 0) {
1621
+ return `${header}
1622
+ `;
1623
+ }
1624
+ const body = entries.map((e) => `${e.key}=${formatEnvValue(e.value)}`).join("\n");
1625
+ return `${header}
1626
+ ${body}
1627
+ `;
1628
+ };
1629
+ var parseEnv = (content) => {
1630
+ const result = {};
1631
+ for (const rawLine of content.split("\n")) {
1632
+ const line = rawLine.trim();
1633
+ if (line.length === 0 || line.startsWith("#")) {
1634
+ continue;
1635
+ }
1636
+ const eq = line.indexOf("=");
1637
+ if (eq <= 0) {
1638
+ continue;
1639
+ }
1640
+ const key = line.slice(0, eq).trim();
1641
+ let value = line.slice(eq + 1).trim();
1642
+ if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
1643
+ value = value.slice(1, -1).replace(/\\(\\|"|n|r|t)/g, (_, ch) => {
1644
+ switch (ch) {
1645
+ case "n":
1646
+ return "\n";
1647
+ case "r":
1648
+ return "\r";
1649
+ case "t":
1650
+ return " ";
1651
+ case '"':
1652
+ return '"';
1653
+ default:
1654
+ return "\\";
1655
+ }
605
1656
  });
606
- const decoded = yield* Schema4.decodeUnknown(OpenIdDiscoveryDocument)(json).pipe(
607
- Effect7.mapError(
608
- () => new OAuthDiscoveryError({
609
- message: "Invalid OIDC discovery document schema"
610
- })
1657
+ }
1658
+ result[key] = value;
1659
+ }
1660
+ return result;
1661
+ };
1662
+
1663
+ // src/services/env-file.ts
1664
+ var EnvFileService = class extends Effect14.Service()("EnvFileService", {
1665
+ accessors: true,
1666
+ effect: Effect14.gen(function* () {
1667
+ const fs = yield* FileSystem6.FileSystem;
1668
+ const pathService = yield* Path5.Path;
1669
+ const exists = Effect14.fn("EnvFileService.exists")(function* (filePath) {
1670
+ return yield* fs.exists(filePath).pipe(Effect14.mapError(() => new EnvFileReadError({ message: `Failed to check whether ${filePath} exists` })));
1671
+ });
1672
+ const read = Effect14.fn("EnvFileService.read")(function* (filePath) {
1673
+ const present = yield* exists(filePath);
1674
+ if (!present) return Option7.none();
1675
+ const contents = yield* fs.readFileString(filePath, "utf8").pipe(Effect14.mapError(() => new EnvFileReadError({ message: `Failed to read ${filePath}` })));
1676
+ return Option7.some(contents);
1677
+ });
1678
+ const write = Effect14.fn("EnvFileService.write")(function* (filePath, entries, opts) {
1679
+ const dir = pathService.dirname(filePath);
1680
+ yield* fs.makeDirectory(dir, { recursive: true }).pipe(
1681
+ Effect14.mapError(
1682
+ (error) => new EnvFileWriteError({ message: `Failed to create directory for ${filePath}`, cause: String(error) })
611
1683
  )
612
1684
  );
613
- if (!decoded.authorization_endpoint || !decoded.token_endpoint || !decoded.userinfo_endpoint) {
614
- return yield* Effect7.fail(
615
- new OAuthDiscoveryError({
616
- message: "OIDC discovery returned incomplete endpoints"
617
- })
618
- );
1685
+ yield* fs.writeFileString(filePath, serializeEnv(entries, opts), { mode: 384 }).pipe(
1686
+ Effect14.mapError(
1687
+ (error) => new EnvFileWriteError({ message: `Failed to write ${filePath}`, cause: String(error) })
1688
+ )
1689
+ );
1690
+ yield* fs.chmod(filePath, 384).pipe(
1691
+ Effect14.mapError(
1692
+ (error) => new EnvFileWriteError({ message: `Failed to secure ${filePath}`, cause: String(error) })
1693
+ )
1694
+ );
1695
+ });
1696
+ const ensureGitignored = Effect14.fn("EnvFileService.ensureGitignored")(function* (dir, entry) {
1697
+ const gitignorePath = pathService.join(dir, ".gitignore");
1698
+ const existing = yield* read(gitignorePath);
1699
+ const current = Option7.getOrElse(existing, () => "");
1700
+ const alreadyIgnored = current.split("\n").some((line) => line.trim() === entry);
1701
+ if (alreadyIgnored) {
1702
+ return false;
619
1703
  }
620
- const config = {
621
- clientId: envConfig.clientId,
622
- issuer: decoded.issuer ?? issuerBase,
623
- authorizeUrl: decoded.authorization_endpoint,
624
- tokenUrl: decoded.token_endpoint,
625
- scope: DEFAULT_SCOPE,
626
- userinfoUrl: decoded.userinfo_endpoint
627
- };
628
- cached = config;
629
- yield* Effect7.log("OIDC discovery completed", { issuer: config.issuer });
630
- return config;
1704
+ const separator = current.length > 0 && !current.endsWith("\n") ? "\n" : "";
1705
+ yield* fs.writeFileString(gitignorePath, `${current}${separator}${entry}
1706
+ `).pipe(
1707
+ Effect14.mapError(
1708
+ (error) => new EnvFileWriteError({ message: "Failed to update .gitignore", cause: String(error) })
1709
+ )
1710
+ );
1711
+ return true;
631
1712
  });
632
- return { getConfig };
1713
+ return { exists, read, write, ensureGitignored };
633
1714
  })
634
1715
  }) {
635
1716
  };
636
1717
 
637
- // src/services/auth.ts
638
- var escapeHtml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/\//g, "&#x2F;");
639
- var base64URLEncode = (buffer) => buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
640
- var generatePKCE = () => {
641
- const codeVerifier = base64URLEncode(crypto.randomBytes(64));
642
- const challengeBytes = crypto.createHash("sha256").update(codeVerifier, "utf8").digest();
643
- const codeChallenge = base64URLEncode(challengeBytes);
644
- return { codeVerifier, codeChallenge };
645
- };
646
- var AuthService = class extends Effect8.Service()("AuthService", {
1718
+ // src/services/local-credentials.ts
1719
+ import { Config as Config3, Effect as Effect16, Option as Option9 } from "effect";
1720
+ import * as os3 from "os";
1721
+
1722
+ // src/services/dev-credentials-store.ts
1723
+ import { FileSystem as FileSystem7, Path as Path6 } from "@effect/platform";
1724
+ import { Effect as Effect15, Option as Option8, Schema as Schema6 } from "effect";
1725
+ import * as os2 from "os";
1726
+ var DevCredentialsStore = class extends Effect15.Service()("DevCredentialsStore", {
647
1727
  accessors: true,
648
- dependencies: [OAuthConfigService.Default, AuthStorageService.Default],
649
- effect: Effect8.gen(function* () {
650
- const oauthConfig = yield* OAuthConfigService;
651
- const storage = yield* AuthStorageService;
652
- let accessToken = null;
653
- let tokenExpirationTime = 0;
654
- const logout = Effect8.gen(function* () {
655
- accessToken = null;
656
- tokenExpirationTime = 0;
657
- yield* storage.remove;
658
- yield* Effect8.log("Logged out");
659
- });
660
- const exchangeCodeForTokens = (config, authCode, redirectUri, codeVerifier) => Effect8.gen(function* () {
661
- const response = yield* Effect8.tryPromise({
662
- try: () => fetch(config.tokenUrl, {
663
- method: "POST",
664
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
665
- body: new URLSearchParams({
666
- grant_type: "authorization_code",
667
- client_id: config.clientId,
668
- code: authCode,
669
- redirect_uri: redirectUri,
670
- code_verifier: codeVerifier
671
- }).toString()
672
- }),
673
- catch: (error) => new TokenExchangeError({
674
- message: `Token exchange request failed: ${error instanceof Error ? error.message : String(error)}`
675
- })
676
- });
677
- if (!response.ok) {
678
- const body = yield* Effect8.tryPromise({
679
- try: () => response.text(),
680
- catch: () => new TokenExchangeError({ message: "Failed to read token exchange error response" })
681
- });
682
- return yield* Effect8.fail(
683
- new TokenExchangeError({
684
- message: `Token exchange failed: ${response.status} ${response.statusText} - ${body}`,
685
- statusCode: response.status
1728
+ effect: Effect15.gen(function* () {
1729
+ const fs = yield* FileSystem7.FileSystem;
1730
+ const pathService = yield* Path6.Path;
1731
+ const homeDir = os2.homedir();
1732
+ const storeDir = pathService.join(homeDir, ".gigadrive");
1733
+ const storeFile = pathService.join(storeDir, "dev-keys.json");
1734
+ const ensureDirectory = Effect15.gen(function* () {
1735
+ yield* fs.makeDirectory(storeDir, { recursive: true }).pipe(
1736
+ Effect15.mapError(
1737
+ (error) => new DevCredentialsStoreWriteError({ message: "Failed to create ~/.gigadrive", cause: String(error) })
1738
+ )
1739
+ );
1740
+ yield* fs.chmod(storeDir, 448).pipe(
1741
+ Effect15.mapError(
1742
+ (error) => new DevCredentialsStoreWriteError({
1743
+ message: "Failed to set ~/.gigadrive permissions",
1744
+ cause: String(error)
686
1745
  })
687
- );
688
- }
689
- const tokens = yield* Effect8.tryPromise({
690
- try: () => response.json(),
691
- catch: () => new TokenExchangeError({ message: "Failed to parse token exchange response" })
692
- });
693
- return tokens;
1746
+ )
1747
+ );
694
1748
  });
695
- const startCallbackServer = (config, codeChallenge, state) => Effect8.async((resume) => {
696
- const server = http.createServer();
697
- const redirectPath = "/callback";
698
- server.listen(0, "127.0.0.1", () => {
699
- const address = server.address();
700
- if (!address || typeof address !== "object") {
701
- server.close(() => {
702
- resume(Effect8.fail(new LoginFlowError({ message: "Failed to determine callback server address" })));
703
- });
704
- return;
705
- }
706
- const port = address.port;
707
- const redirectUri = `http://127.0.0.1:${port}${redirectPath}`;
708
- const authUrl = new URL(config.authorizeUrl);
709
- authUrl.searchParams.append("client_id", config.clientId);
710
- authUrl.searchParams.append("redirect_uri", redirectUri);
711
- authUrl.searchParams.append("response_type", "code");
712
- authUrl.searchParams.append("scope", config.scope);
713
- authUrl.searchParams.append("code_challenge", codeChallenge);
714
- authUrl.searchParams.append("code_challenge_method", "S256");
715
- authUrl.searchParams.append("state", state);
716
- Console3.log(`Please open this URL in your browser to log in:
717
- ${authUrl.toString()}
718
- `).pipe(Effect8.runSync);
719
- Console3.log("Waiting for login callback...").pipe(Effect8.runSync);
720
- void open(authUrl.toString());
721
- server.on("request", (req, res) => {
722
- const reqUrl = new URL(req.url || "", redirectUri);
723
- if (reqUrl.pathname !== "/callback") {
724
- res.writeHead(404).end("Not Found");
725
- return;
726
- }
727
- const authCode = reqUrl.searchParams.get("code");
728
- const receivedState = reqUrl.searchParams.get("state");
729
- const oauthError = reqUrl.searchParams.get("error");
730
- res.writeHead(200, { "Content-Type": "text/html", Connection: "close" });
731
- if (oauthError) {
732
- const safeOauthError = escapeHtml(oauthError);
733
- res.end(`<h1>Login Failed!</h1><p>Error: ${safeOauthError}</p><p>You can close this tab.</p>`);
734
- server.close();
735
- resume(Effect8.fail(new LoginFlowError({ message: `OAuth error: ${oauthError}` })));
736
- return;
737
- }
738
- if (receivedState !== state) {
739
- res.end(
740
- "<h1>Login Failed!</h1><p>State mismatch. Possible CSRF attack.</p><p>You can close this tab.</p>"
741
- );
742
- server.close();
743
- resume(Effect8.fail(new LoginFlowError({ message: "State mismatch" })));
744
- return;
745
- }
746
- if (authCode) {
747
- res.end("<h1>Login Successful!</h1><p>You can close this tab.</p>");
748
- server.close();
749
- resume(Effect8.succeed({ authCode, redirectUri }));
750
- } else {
751
- res.end("<h1>Login Failed!</h1><p>No authorization code received.</p><p>You can close this tab.</p>");
752
- server.close();
753
- resume(Effect8.fail(new LoginFlowError({ message: "No authorization code received" })));
754
- }
755
- });
756
- server.on("error", (err) => {
757
- server.close();
758
- resume(Effect8.fail(new LoginFlowError({ message: `Local server error: ${err.message}` })));
759
- });
1749
+ const load = Effect15.gen(function* () {
1750
+ const present = yield* fs.exists(storeFile).pipe(Effect15.mapError(() => new DevCredentialsStoreReadError({ message: "Failed to check dev-keys.json" })));
1751
+ if (!present) return {};
1752
+ const data = yield* fs.readFileString(storeFile, "utf8").pipe(Effect15.mapError(() => new DevCredentialsStoreReadError({ message: "Failed to read dev-keys.json" })));
1753
+ const json = yield* Effect15.try({
1754
+ try: () => JSON.parse(data),
1755
+ catch: () => new DevCredentialsStoreReadError({ message: "dev-keys.json contains invalid JSON" })
760
1756
  });
1757
+ return yield* Schema6.decodeUnknown(DevKeyStore)(json).pipe(
1758
+ Effect15.mapError(() => new DevCredentialsStoreReadError({ message: "dev-keys.json has an invalid schema" }))
1759
+ );
761
1760
  });
762
- const login = Effect8.gen(function* () {
763
- yield* logout;
764
- const { codeVerifier, codeChallenge } = generatePKCE();
765
- const state = crypto.randomBytes(16).toString("hex");
766
- const config = yield* oauthConfig.getConfig;
767
- const { authCode, redirectUri } = yield* startCallbackServer(config, codeChallenge, state);
768
- const tokens = yield* exchangeCodeForTokens(config, authCode, redirectUri, codeVerifier);
769
- if (!tokens.refresh_token) {
770
- return yield* Effect8.fail(
771
- new TokenExchangeError({ message: "No refresh token received from identity provider" })
772
- );
773
- }
774
- accessToken = tokens.access_token;
775
- tokenExpirationTime = Date.now() + (tokens.expires_in || 3600) * 1e3;
776
- yield* storage.save({
777
- refreshToken: tokens.refresh_token,
778
- accessToken,
779
- tokenExpirationTime
780
- });
781
- yield* Effect8.log("Login successful");
782
- return true;
1761
+ const get = Effect15.fn("DevCredentialsStore.get")(function* (applicationId) {
1762
+ const store = yield* load;
1763
+ const entry = store[applicationId];
1764
+ return entry ? Option8.some(entry) : Option8.none();
783
1765
  });
784
- const refreshAccessToken = Effect8.gen(function* () {
785
- const stored = yield* storage.load.pipe(
786
- Effect8.catchTag("AuthStorageReadError", (err) => Effect8.fail(new TokenRefreshError({ message: err.message })))
787
- );
788
- const config = yield* oauthConfig.getConfig.pipe(
789
- Effect8.catchTag("OAuthDiscoveryError", (err) => Effect8.fail(new TokenRefreshError({ message: err.message })))
1766
+ const set = Effect15.fn("DevCredentialsStore.set")(function* (applicationId, entry) {
1767
+ const store = yield* load;
1768
+ const next = { ...store, [applicationId]: entry };
1769
+ yield* ensureDirectory;
1770
+ yield* fs.writeFileString(storeFile, `${JSON.stringify(next, null, 2)}
1771
+ `, { mode: 384 }).pipe(
1772
+ Effect15.mapError(
1773
+ (error) => new DevCredentialsStoreWriteError({ message: "Failed to write dev-keys.json", cause: String(error) })
1774
+ )
790
1775
  );
791
- const authData = yield* Option2.match(stored, {
792
- onNone: () => Effect8.fail(new TokenRefreshError({ message: "No refresh token available. Please log in." })),
793
- onSome: (data) => data.refreshToken ? Effect8.succeed(data) : Effect8.fail(new TokenRefreshError({ message: "No refresh token available. Please log in." }))
794
- });
795
- const response = yield* Effect8.tryPromise({
796
- try: () => fetch(config.tokenUrl, {
797
- method: "POST",
798
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
799
- body: new URLSearchParams({
800
- grant_type: "refresh_token",
801
- client_id: config.clientId,
802
- refresh_token: authData.refreshToken
803
- }).toString()
804
- }),
805
- catch: (error) => new TokenRefreshError({
806
- message: `Token refresh request failed: ${error instanceof Error ? error.message : String(error)}`
807
- })
808
- });
809
- if (!response.ok) {
810
- if (response.status === 400) {
811
- yield* logout.pipe(Effect8.catchAll(() => Effect8.void));
812
- return yield* Effect8.fail(
813
- new TokenRefreshError({
814
- message: "Refresh token is invalid or expired. Please log in again.",
815
- statusCode: response.status
816
- })
817
- );
818
- }
819
- const body = yield* Effect8.tryPromise({
820
- try: () => response.text(),
821
- catch: () => new TokenRefreshError({ message: "Failed to read refresh error response" })
822
- });
823
- return yield* Effect8.fail(
824
- new TokenRefreshError({
825
- message: `Token refresh failed: ${response.status} ${response.statusText} - ${body}`,
826
- statusCode: response.status
827
- })
828
- );
829
- }
830
- const tokens = yield* Effect8.tryPromise({
831
- try: () => response.json(),
832
- catch: () => new TokenRefreshError({ message: "Failed to parse token refresh response" })
833
- });
834
- accessToken = tokens.access_token;
835
- tokenExpirationTime = Date.now() + (tokens.expires_in || 3600) * 1e3;
836
- const newRefreshToken = tokens.refresh_token || authData.refreshToken;
837
- yield* storage.save({
838
- refreshToken: newRefreshToken,
839
- accessToken,
840
- tokenExpirationTime
841
- }).pipe(Effect8.catchAll(() => Effect8.void));
842
- yield* Effect8.log("Access token refreshed successfully");
843
- return true;
844
1776
  });
845
- const getAccessToken = Effect8.gen(function* () {
846
- if (accessToken && Date.now() < tokenExpirationTime) {
847
- return accessToken;
848
- }
849
- const stored = yield* storage.load.pipe(
850
- Effect8.catchAll(() => Effect8.succeed(Option2.none()))
851
- );
852
- Option2.match(stored, {
853
- onNone: () => {
854
- },
855
- onSome: (data) => {
856
- if (data.accessToken && data.tokenExpirationTime && Date.now() < data.tokenExpirationTime) {
857
- accessToken = data.accessToken;
858
- tokenExpirationTime = data.tokenExpirationTime;
859
- }
860
- }
861
- });
862
- if (accessToken && Date.now() < tokenExpirationTime) {
863
- return accessToken;
1777
+ return { get, set };
1778
+ })
1779
+ }) {
1780
+ };
1781
+
1782
+ // src/services/local-credentials.ts
1783
+ var DEV_KEY_SCOPES = ["network:env_vars:read"];
1784
+ var ApiBaseUrl2 = Config3.string("GIGADRIVE_API_BASE_URL").pipe(Config3.withDefault("https://api.gigadrive.network"));
1785
+ var credentialEntries = (clientId, clientSecret, baseUrl) => [
1786
+ { key: "GIGADRIVE_CLIENT_ID", value: clientId },
1787
+ { key: "GIGADRIVE_CLIENT_SECRET", value: clientSecret },
1788
+ { key: "GIGADRIVE_API_BASE_URL", value: baseUrl }
1789
+ ];
1790
+ var LocalCredentialsService = class extends Effect16.Service()("LocalCredentialsService", {
1791
+ accessors: true,
1792
+ dependencies: [ApiClientService.Default, DevCredentialsStore.Default],
1793
+ effect: Effect16.gen(function* () {
1794
+ const apiClient = yield* ApiClientService;
1795
+ const store = yield* DevCredentialsStore;
1796
+ const baseUrl = yield* ApiBaseUrl2;
1797
+ const keyName = (applicationId) => `cli-dev:${os3.hostname()}:${applicationId}`;
1798
+ const provision = Effect16.fn("LocalCredentialsService.provision")(function* (input) {
1799
+ const stored = yield* store.get(input.applicationId);
1800
+ const existing = input.existingContent ? parseEnv(input.existingContent) : {};
1801
+ const existingClientId = existing["GIGADRIVE_CLIENT_ID"];
1802
+ const existingClientSecret = existing["GIGADRIVE_CLIENT_SECRET"];
1803
+ const canReuse = !input.rotate && existingClientId !== void 0 && existingClientSecret !== void 0 && Option9.isSome(stored) && stored.value.apiKeyId === existingClientId;
1804
+ if (canReuse) {
1805
+ return credentialEntries(existingClientId, existingClientSecret, baseUrl);
864
1806
  }
865
- yield* Effect8.log("Access token expired or not available. Attempting to refresh...");
866
- yield* refreshAccessToken.pipe(
867
- Effect8.catchTag("TokenRefreshError", (err) => Effect8.fail(new NotAuthenticatedError({ message: err.message })))
868
- );
869
- if (!accessToken) {
870
- return yield* Effect8.fail(
871
- new NotAuthenticatedError({ message: "No valid access token available. Please log in first." })
872
- );
1807
+ if (Option9.isSome(stored)) {
1808
+ yield* apiClient.request((client) => client.apiKeys.delete(stored.value.apiKeyId)).pipe(Effect16.catchTag("ApiRequestError", () => Effect16.void));
873
1809
  }
874
- return accessToken;
1810
+ const created = yield* apiClient.request(
1811
+ (client) => client.apiKeys.create({
1812
+ name: keyName(input.applicationId),
1813
+ applicationId: input.applicationId,
1814
+ scopes: DEV_KEY_SCOPES
1815
+ })
1816
+ );
1817
+ yield* store.set(input.applicationId, { apiKeyId: created.id });
1818
+ return credentialEntries(created.id, created.secret, baseUrl);
875
1819
  });
876
- const getUserInfo = Effect8.gen(function* () {
877
- const config = yield* oauthConfig.getConfig;
878
- const token = yield* getAccessToken;
879
- const fetchUserInfo = (t) => Effect8.tryPromise({
880
- try: () => fetch(config.userinfoUrl, {
881
- headers: {
882
- Authorization: `Bearer ${t}`,
883
- Accept: "application/json"
884
- }
885
- }),
886
- catch: (error) => new UserInfoFetchError({
887
- message: `Failed to fetch user info: ${error instanceof Error ? error.message : String(error)}`
1820
+ return { provision };
1821
+ })
1822
+ }) {
1823
+ };
1824
+
1825
+ // src/commands/env/index.ts
1826
+ var appOption3 = Options4.text("app").pipe(
1827
+ Options4.withAlias("a"),
1828
+ Options4.withDescription("Operate on this application (overrides the linked application)"),
1829
+ Options4.optional
1830
+ );
1831
+ var orgOption4 = Options4.text("org").pipe(
1832
+ Options4.withAlias("o"),
1833
+ Options4.withDescription("Operate on this organization instead of an application"),
1834
+ Options4.optional
1835
+ );
1836
+ var sensitiveOption = Options4.boolean("sensitive").pipe(
1837
+ Options4.withDescription("Store the value as sensitive (hidden in API responses)")
1838
+ );
1839
+ var envIdOption = Options4.text("env").pipe(
1840
+ Options4.withAlias("e"),
1841
+ Options4.withDescription("Environment ID for an environment-scoped variable (application scope only)"),
1842
+ Options4.optional
1843
+ );
1844
+ var keyValueArg = Args3.text({ name: "key=value" }).pipe(
1845
+ Args3.withDescription("Variable assignment, e.g. DATABASE_URL=postgres://user:pass@host/db")
1846
+ );
1847
+ var keyOrIdArg = Args3.text({ name: "key-or-id" }).pipe(Args3.withDescription("Variable key or ID to remove"));
1848
+ var fileArg = Args3.text({ name: "file" }).pipe(
1849
+ Args3.withDescription("Output file to write (default: .env.local)"),
1850
+ Args3.optional
1851
+ );
1852
+ var environmentOption = Options4.text("environment").pipe(
1853
+ Options4.withAlias("e"),
1854
+ Options4.withDescription("Environment slug to resolve overrides for (e.g. preview). Omit for the local baseline"),
1855
+ Options4.optional
1856
+ );
1857
+ var yesOption = Options4.boolean("yes").pipe(
1858
+ Options4.withAlias("y"),
1859
+ Options4.withDescription("Overwrite an existing file without confirmation")
1860
+ );
1861
+ var withCredentialsOption = Options4.boolean("with-credentials").pipe(
1862
+ Options4.withDescription("Also provision API-key credentials so the local app can call the API as this application")
1863
+ );
1864
+ var rotateOption = Options4.boolean("rotate").pipe(
1865
+ Options4.withDescription("Force a fresh API key when provisioning credentials, revoking the previous one")
1866
+ );
1867
+ var resolveScope = (app, org) => Effect17.gen(function* () {
1868
+ if (Option10.isSome(org)) {
1869
+ return { kind: "organization", id: org.value };
1870
+ }
1871
+ const projectLink = yield* ProjectLinkService;
1872
+ const id = yield* Option10.match(app, {
1873
+ onSome: (value) => Effect17.succeed(value),
1874
+ onNone: () => projectLink.resolve(process.cwd()).pipe(Effect17.map((link) => link.applicationId))
1875
+ });
1876
+ return { kind: "application", id };
1877
+ });
1878
+ var envListCommand = Command7.make(
1879
+ "list",
1880
+ { app: appOption3, org: orgOption4 },
1881
+ ({ app, org }) => Effect17.gen(function* () {
1882
+ const apiClient = yield* ApiClientService;
1883
+ const scope = yield* resolveScope(app, org);
1884
+ const { items } = yield* apiClient.request(
1885
+ (client) => scope.kind === "organization" ? client.organizations.envVars.list(scope.id) : client.applications.envVars.list(scope.id)
1886
+ );
1887
+ if (items.length === 0) {
1888
+ yield* Console7.log("No environment variables set.");
1889
+ return;
1890
+ }
1891
+ for (const v of items) {
1892
+ yield* Console7.log(`${v.key}=${v.sensitive ? "***" : v.value ?? ""}`);
1893
+ }
1894
+ }).pipe(
1895
+ Effect17.catchTags({
1896
+ NotAuthenticatedError: (err) => Console7.error('You are not logged in. Run "gigadrive login" to authenticate.').pipe(
1897
+ Effect17.andThen(Effect17.fail(err))
1898
+ ),
1899
+ ApiRequestError: (err) => Console7.error(`Failed to list environment variables: ${err.message}`).pipe(Effect17.andThen(Effect17.fail(err))),
1900
+ ProjectNotLinkedError: (err) => Console7.error(err.message).pipe(Effect17.andThen(Effect17.fail(err))),
1901
+ ProjectLinkReadError: (err) => Console7.error(err.message).pipe(Effect17.andThen(Effect17.fail(err)))
1902
+ })
1903
+ )
1904
+ );
1905
+ var envSetCommand = Command7.make(
1906
+ "set",
1907
+ { keyValue: keyValueArg, app: appOption3, org: orgOption4, sensitive: sensitiveOption, env: envIdOption },
1908
+ ({ keyValue, app, org, sensitive, env }) => Effect17.gen(function* () {
1909
+ const separator = keyValue.indexOf("=");
1910
+ if (separator <= 0) {
1911
+ return yield* Effect17.fail(
1912
+ new InvalidEnvVarFormatError({
1913
+ message: 'Invalid format. Use KEY=VALUE, e.g. "gigadrive env set DATABASE_URL=postgres://...".'
888
1914
  })
889
- });
890
- let response = yield* fetchUserInfo(token);
891
- if (response.status === 401 || response.status === 403) {
892
- yield* refreshAccessToken.pipe(
893
- Effect8.catchTag("TokenRefreshError", (err) => Effect8.fail(new UserInfoFetchError({ message: err.message })))
894
- );
895
- if (!accessToken) {
896
- return yield* Effect8.fail(new UserInfoFetchError({ message: "Token refresh failed" }));
897
- }
898
- response = yield* fetchUserInfo(accessToken);
1915
+ );
1916
+ }
1917
+ const key = keyValue.slice(0, separator);
1918
+ const value = keyValue.slice(separator + 1);
1919
+ const apiClient = yield* ApiClientService;
1920
+ const scope = yield* resolveScope(app, org);
1921
+ const environmentId = Option10.getOrUndefined(env);
1922
+ const created = yield* apiClient.request(
1923
+ (client) => scope.kind === "organization" ? client.organizations.envVars.create(scope.id, { key, value, sensitive }) : client.applications.envVars.create(scope.id, { key, value, sensitive, environmentId })
1924
+ );
1925
+ yield* Console7.log(`Set ${created.key}.`);
1926
+ }).pipe(
1927
+ Effect17.catchTags({
1928
+ InvalidEnvVarFormatError: (err) => Console7.error(err.message).pipe(Effect17.andThen(Effect17.fail(err))),
1929
+ NotAuthenticatedError: (err) => Console7.error('You are not logged in. Run "gigadrive login" to authenticate.').pipe(
1930
+ Effect17.andThen(Effect17.fail(err))
1931
+ ),
1932
+ ApiRequestError: (err) => Console7.error(`Failed to set environment variable: ${err.message}`).pipe(Effect17.andThen(Effect17.fail(err))),
1933
+ ProjectNotLinkedError: (err) => Console7.error(err.message).pipe(Effect17.andThen(Effect17.fail(err))),
1934
+ ProjectLinkReadError: (err) => Console7.error(err.message).pipe(Effect17.andThen(Effect17.fail(err)))
1935
+ })
1936
+ )
1937
+ );
1938
+ var envRmCommand = Command7.make(
1939
+ "rm",
1940
+ { keyOrId: keyOrIdArg, app: appOption3, org: orgOption4 },
1941
+ ({ keyOrId, app, org }) => Effect17.gen(function* () {
1942
+ const apiClient = yield* ApiClientService;
1943
+ const scope = yield* resolveScope(app, org);
1944
+ const { items } = yield* apiClient.request(
1945
+ (client) => scope.kind === "organization" ? client.organizations.envVars.list(scope.id) : client.applications.envVars.list(scope.id)
1946
+ );
1947
+ const target = items.find((v) => v.id === keyOrId || v.key === keyOrId);
1948
+ if (target === void 0) {
1949
+ return yield* Effect17.fail(
1950
+ new EnvVarNotFoundError({ message: `No environment variable matching "${keyOrId}".` })
1951
+ );
1952
+ }
1953
+ yield* apiClient.request(
1954
+ (client) => scope.kind === "organization" ? client.organizations.envVars.delete(scope.id, target.id) : client.applications.envVars.delete(scope.id, target.id)
1955
+ );
1956
+ yield* Console7.log(`Removed ${target.key}.`);
1957
+ }).pipe(
1958
+ Effect17.catchTags({
1959
+ EnvVarNotFoundError: (err) => Console7.error(err.message).pipe(Effect17.andThen(Effect17.fail(err))),
1960
+ NotAuthenticatedError: (err) => Console7.error('You are not logged in. Run "gigadrive login" to authenticate.').pipe(
1961
+ Effect17.andThen(Effect17.fail(err))
1962
+ ),
1963
+ ApiRequestError: (err) => Console7.error(`Failed to remove environment variable: ${err.message}`).pipe(Effect17.andThen(Effect17.fail(err))),
1964
+ ProjectNotLinkedError: (err) => Console7.error(err.message).pipe(Effect17.andThen(Effect17.fail(err))),
1965
+ ProjectLinkReadError: (err) => Console7.error(err.message).pipe(Effect17.andThen(Effect17.fail(err)))
1966
+ })
1967
+ )
1968
+ );
1969
+ var envPullCommand = Command7.make(
1970
+ "pull",
1971
+ {
1972
+ file: fileArg,
1973
+ app: appOption3,
1974
+ environment: environmentOption,
1975
+ yes: yesOption,
1976
+ withCredentials: withCredentialsOption,
1977
+ rotate: rotateOption
1978
+ },
1979
+ ({ file, app, environment, yes, withCredentials, rotate }) => Effect17.gen(function* () {
1980
+ const apiClient = yield* ApiClientService;
1981
+ const projectLink = yield* ProjectLinkService;
1982
+ const envFile = yield* EnvFileService;
1983
+ const applicationId = yield* Option10.match(app, {
1984
+ onSome: (value) => Effect17.succeed(value),
1985
+ onNone: () => projectLink.resolve(process.cwd()).pipe(Effect17.map((link) => link.applicationId))
1986
+ });
1987
+ const target = Option10.getOrElse(file, () => ".env.local");
1988
+ const environmentSlug = Option10.getOrUndefined(environment);
1989
+ const { items, omittedSensitive } = yield* apiClient.request(
1990
+ (client) => client.applications.envVars.pull(applicationId, environmentSlug ? { environment: environmentSlug } : void 0)
1991
+ );
1992
+ const existing = yield* envFile.read(target);
1993
+ if (Option10.isSome(existing) && !yes) {
1994
+ const overwrite = yield* Prompt.run(
1995
+ Prompt.confirm({ message: `${target} already exists. Overwrite?`, initial: false })
1996
+ );
1997
+ if (!overwrite) {
1998
+ return yield* Console7.log("Cancelled.");
899
1999
  }
900
- if (!response.ok) {
901
- const body = yield* Effect8.tryPromise({
902
- try: () => response.text(),
903
- catch: () => new UserInfoFetchError({ message: "Failed to read user info error response" })
904
- });
905
- return yield* Effect8.fail(
906
- new UserInfoFetchError({
907
- message: `Failed to fetch user info: ${response.status} ${response.statusText}${body ? ` - ${body}` : ""}`,
908
- statusCode: response.status
2000
+ }
2001
+ let credentialEntries2 = [];
2002
+ if (withCredentials) {
2003
+ const localCredentials = yield* LocalCredentialsService;
2004
+ credentialEntries2 = yield* localCredentials.provision({
2005
+ applicationId,
2006
+ rotate,
2007
+ existingContent: Option10.getOrUndefined(existing)
2008
+ });
2009
+ }
2010
+ const entries = [...items.map((v) => ({ key: v.key, value: v.value })), ...credentialEntries2];
2011
+ yield* envFile.write(target, entries);
2012
+ const gitignored = yield* envFile.ensureGitignored(process.cwd(), target);
2013
+ yield* Console7.log(
2014
+ `Pulled ${items.length} environment variable(s)${withCredentials ? " and API credentials" : ""} to ${target}.`
2015
+ );
2016
+ if (omittedSensitive > 0) {
2017
+ yield* Console7.log(
2018
+ `Skipped ${omittedSensitive} sensitive variable(s); secrets live only in production/preview environments.`
2019
+ );
2020
+ }
2021
+ if (gitignored) {
2022
+ yield* Console7.log(`Added ${target} to .gitignore.`);
2023
+ }
2024
+ }).pipe(
2025
+ Effect17.catchTags({
2026
+ // Ctrl+C during the overwrite prompt is a deliberate cancel — exit 0.
2027
+ QuitException: () => Console7.log("Cancelled."),
2028
+ NotAuthenticatedError: (err) => Console7.error('You are not logged in. Run "gigadrive login" to authenticate.').pipe(
2029
+ Effect17.andThen(Effect17.fail(err))
2030
+ ),
2031
+ ApiRequestError: (err) => Console7.error(`Failed to pull environment variables: ${err.message}`).pipe(Effect17.andThen(Effect17.fail(err))),
2032
+ ProjectNotLinkedError: (err) => Console7.error(err.message).pipe(Effect17.andThen(Effect17.fail(err))),
2033
+ ProjectLinkReadError: (err) => Console7.error(err.message).pipe(Effect17.andThen(Effect17.fail(err))),
2034
+ EnvFileReadError: (err) => Console7.error(err.message).pipe(Effect17.andThen(Effect17.fail(err))),
2035
+ EnvFileWriteError: (err) => Console7.error(err.message).pipe(Effect17.andThen(Effect17.fail(err))),
2036
+ DevCredentialsStoreReadError: (err) => Console7.error(err.message).pipe(Effect17.andThen(Effect17.fail(err))),
2037
+ DevCredentialsStoreWriteError: (err) => Console7.error(err.message).pipe(Effect17.andThen(Effect17.fail(err)))
2038
+ })
2039
+ )
2040
+ );
2041
+ var envBase = Command7.make("env", {}, () => Effect17.void);
2042
+ var envCommand = envBase.pipe(
2043
+ Command7.withSubcommands([envListCommand, envSetCommand, envRmCommand, envPullCommand])
2044
+ );
2045
+
2046
+ // src/commands/link/index.ts
2047
+ import { Command as Command8, Options as Options5, Prompt as Prompt2 } from "@effect/cli";
2048
+ import { Console as Console8, Effect as Effect18, Option as Option11 } from "effect";
2049
+ var appOption4 = Options5.text("app").pipe(
2050
+ Options5.withAlias("a"),
2051
+ Options5.withDescription("Application ID to link (skips the interactive picker)"),
2052
+ Options5.optional
2053
+ );
2054
+ var orgOption5 = Options5.text("org").pipe(
2055
+ Options5.withAlias("o"),
2056
+ Options5.withDescription("Only list applications belonging to this organization ID"),
2057
+ Options5.optional
2058
+ );
2059
+ var linkCommand = Command8.make(
2060
+ "link",
2061
+ { app: appOption4, org: orgOption5 },
2062
+ ({ app, org }) => Effect18.gen(function* () {
2063
+ const apiClient = yield* ApiClientService;
2064
+ const projectLink = yield* ProjectLinkService;
2065
+ const cwd = process.cwd();
2066
+ const organizationId = Option11.getOrUndefined(org);
2067
+ const { items: apps } = yield* apiClient.request((client) => client.applications.list({ organizationId }));
2068
+ if (apps.length === 0) {
2069
+ return yield* Effect18.fail(
2070
+ new NoApplicationsFoundError({
2071
+ message: 'No applications found. Create one in the Gigadrive console, then run "gigadrive link" again.'
2072
+ })
2073
+ );
2074
+ }
2075
+ const explicitAppId = Option11.getOrUndefined(app);
2076
+ let applicationId;
2077
+ let linkedOrganizationId;
2078
+ if (explicitAppId !== void 0) {
2079
+ const match = apps.find((a) => a.id === explicitAppId);
2080
+ if (match === void 0) {
2081
+ return yield* Effect18.fail(
2082
+ new ApplicationNotFoundError({
2083
+ message: `Application "${explicitAppId}" was not found among your applications.`
909
2084
  })
910
2085
  );
911
2086
  }
912
- const data = yield* Effect8.tryPromise({
913
- try: () => response.json(),
914
- catch: () => new UserInfoFetchError({ message: "Failed to parse user info response" })
915
- });
916
- return data;
917
- });
918
- const inferUserName = (info) => {
919
- const isNonEmptyString = (value) => typeof value === "string" && value.trim() !== "";
920
- const name = info.name;
921
- const givenName = info.given_name;
922
- const familyName = info.family_name;
923
- const preferredUsername = info.preferred_username;
924
- const email = info.email;
925
- return isNonEmptyString(name) && name || (isNonEmptyString(givenName) && isNonEmptyString(familyName) ? `${givenName} ${familyName}` : void 0) || isNonEmptyString(preferredUsername) && preferredUsername || isNonEmptyString(email) && email || "your account";
926
- };
927
- return { login, logout, getAccessToken, getUserInfo, refreshAccessToken, inferUserName };
928
- })
929
- }) {
930
- };
2087
+ applicationId = match.id;
2088
+ linkedOrganizationId = match.organization.id;
2089
+ } else if (apps.length === 1) {
2090
+ applicationId = apps[0].id;
2091
+ linkedOrganizationId = apps[0].organization.id;
2092
+ } else {
2093
+ const chosen = yield* Prompt2.run(
2094
+ Prompt2.select({
2095
+ message: "Select an application to link",
2096
+ choices: apps.map((a) => ({ title: `${a.name} (${a.organization.name})`, value: a }))
2097
+ })
2098
+ );
2099
+ applicationId = chosen.id;
2100
+ linkedOrganizationId = chosen.organization.id;
2101
+ }
2102
+ yield* projectLink.save(cwd, { applicationId, organizationId: linkedOrganizationId });
2103
+ yield* Console8.log(`Linked this directory to application ${applicationId}.`);
2104
+ yield* Console8.log('Tip: add ".gigadrive/" to your .gitignore.');
2105
+ }).pipe(
2106
+ Effect18.catchTags({
2107
+ // Ctrl+C during the picker is a deliberate cancel — exit 0.
2108
+ QuitException: () => Console8.log("Cancelled."),
2109
+ NotAuthenticatedError: (err) => Console8.error('You are not logged in. Run "gigadrive login" to authenticate.').pipe(
2110
+ Effect18.andThen(Effect18.fail(err))
2111
+ ),
2112
+ ApiRequestError: (err) => Console8.error(`Failed to list applications: ${err.message}`).pipe(Effect18.andThen(Effect18.fail(err))),
2113
+ NoApplicationsFoundError: (err) => Console8.error(err.message).pipe(Effect18.andThen(Effect18.fail(err))),
2114
+ ApplicationNotFoundError: (err) => Console8.error(err.message).pipe(Effect18.andThen(Effect18.fail(err))),
2115
+ ProjectLinkWriteError: (err) => Console8.error(err.message).pipe(Effect18.andThen(Effect18.fail(err)))
2116
+ })
2117
+ )
2118
+ );
2119
+ var unlinkCommand = Command8.make(
2120
+ "unlink",
2121
+ {},
2122
+ () => Effect18.gen(function* () {
2123
+ const projectLink = yield* ProjectLinkService;
2124
+ yield* projectLink.remove(process.cwd());
2125
+ yield* Console8.log("Unlinked this directory.");
2126
+ }).pipe(
2127
+ Effect18.catchTags({
2128
+ ProjectLinkWriteError: (err) => Console8.error(err.message).pipe(Effect18.andThen(Effect18.fail(err)))
2129
+ })
2130
+ )
2131
+ );
931
2132
 
932
2133
  // src/commands/login/index.ts
933
- var loginCommand = Command4.make(
2134
+ import { Command as Command9 } from "@effect/cli";
2135
+ import { Console as Console9, Effect as Effect19 } from "effect";
2136
+ var loginCommand = Command9.make(
934
2137
  "login",
935
2138
  {},
936
- () => Effect9.gen(function* () {
937
- yield* Console4.log("Initiating login...");
2139
+ () => Effect19.gen(function* () {
2140
+ yield* Console9.log("Initiating login...");
938
2141
  const auth = yield* AuthService;
939
2142
  yield* auth.login;
940
- const info = yield* auth.getUserInfo.pipe(Effect9.catchAll(() => Effect9.succeed({})));
2143
+ const info = yield* auth.getUserInfo.pipe(Effect19.catchAll(() => Effect19.succeed({})));
941
2144
  const name = auth.inferUserName(info);
942
- yield* Console4.log(`You are now logged in as ${name}.`);
2145
+ yield* Console9.log(`You are now logged in as ${name}.`);
943
2146
  }).pipe(
944
- Effect9.catchTags({
945
- LoginFlowError: (err) => Console4.error(`Login failed: ${err.message}`),
946
- TokenExchangeError: (err) => Console4.error(`Login failed: ${err.message}`),
947
- OAuthDiscoveryError: (err) => Console4.error(`Login failed: ${err.message}`),
948
- AuthStorageWriteError: (err) => Console4.error(`Failed to save credentials: ${err.message}`)
2147
+ Effect19.catchTags({
2148
+ LoginFlowError: (err) => Console9.error(`Login failed: ${err.message}`),
2149
+ TokenExchangeError: (err) => Console9.error(`Login failed: ${err.message}`),
2150
+ OAuthDiscoveryError: (err) => Console9.error(`Login failed: ${err.message}`),
2151
+ AuthStorageWriteError: (err) => Console9.error(`Failed to save credentials: ${err.message}`)
2152
+ })
2153
+ )
2154
+ );
2155
+
2156
+ // src/commands/logout/index.ts
2157
+ import { Command as Command10 } from "@effect/cli";
2158
+ import { Console as Console10, Effect as Effect20 } from "effect";
2159
+ var logoutCommand = Command10.make(
2160
+ "logout",
2161
+ {},
2162
+ () => Effect20.gen(function* () {
2163
+ const auth = yield* AuthService;
2164
+ yield* auth.logout;
2165
+ yield* Console10.log("Logged out.");
2166
+ }).pipe(
2167
+ Effect20.catchTags({
2168
+ AuthStorageWriteError: (err) => Console10.error(`Failed to log out: ${err.message}`).pipe(Effect20.andThen(Effect20.fail(err)))
949
2169
  })
950
2170
  )
951
2171
  );
952
2172
 
953
2173
  // src/commands/platform/index.ts
954
- import { Command as Command6 } from "@effect/cli";
955
- import { Effect as Effect13 } from "effect";
2174
+ import { Command as Command12 } from "@effect/cli";
2175
+ import { Effect as Effect24 } from "effect";
956
2176
 
957
2177
  // src/commands/platform/deploy/index.ts
958
- import { Command as Command5 } from "@effect/cli";
959
- import { FileSystem as FileSystem5, Path as Path5 } from "@effect/platform";
2178
+ import { Command as Command11, Options as Options6 } from "@effect/cli";
2179
+ import { FileSystem as FileSystem9, Path as Path8 } from "@effect/platform";
960
2180
  import { formatFileSize } from "@gigadrive/commons";
961
- import { Console as Console5, Duration, Effect as Effect12, Schedule, Stream } from "effect";
962
- import * as os2 from "os";
2181
+ import { Console as Console11, Duration, Effect as Effect23, Option as Option12, Schedule, Stream } from "effect";
2182
+ import * as os4 from "os";
963
2183
 
964
2184
  // src/services/archive.ts
965
- import { FileSystem as FileSystem4, Path as Path4 } from "@effect/platform";
2185
+ import { FileSystem as FileSystem8, Path as Path7 } from "@effect/platform";
966
2186
  import { getFilesForPattern } from "@gigadrive/build-utils";
967
2187
  import archiver from "archiver";
968
- import { Effect as Effect10 } from "effect";
2188
+ import { Effect as Effect21 } from "effect";
969
2189
  import ignore from "ignore";
970
2190
  import * as nodeFs from "fs";
971
2191
  import * as nodeFsPromises from "fs/promises";
972
2192
  var IGNORE_FILE_NAMES = [".gigadriveignore", ".vercelignore", ".dockerignore", ".nowignore"];
973
2193
  var readIgnoreFile = (fs, ignorePath) => fs.readFileString(ignorePath, "utf8").pipe(
974
- Effect10.map((content) => content.split("\n").filter((line) => line.trim() !== "")),
975
- Effect10.catchAll(() => Effect10.succeed([]))
2194
+ Effect21.map((content) => content.split("\n").filter((line) => line.trim() !== "")),
2195
+ Effect21.catchAll(() => Effect21.succeed([]))
976
2196
  );
977
- var collectIgnorePatterns = (fs, pathService, dir, baseDir, ignoreRules) => Effect10.gen(function* () {
978
- const items = yield* fs.readDirectory(dir).pipe(Effect10.mapError(() => new ArchiveCreateError({ message: `Failed to read directory: ${dir}` })));
2197
+ var collectIgnorePatterns = (fs, pathService, dir, baseDir, ignoreRules) => Effect21.gen(function* () {
2198
+ const items = yield* fs.readDirectory(dir).pipe(Effect21.mapError(() => new ArchiveCreateError({ message: `Failed to read directory: ${dir}` })));
979
2199
  for (const item of items) {
980
2200
  const fullPath = pathService.join(dir, item);
981
2201
  const relativePath = pathService.relative(baseDir, fullPath);
982
- const stat = yield* Effect10.tryPromise({
2202
+ const stat = yield* Effect21.tryPromise({
983
2203
  try: () => nodeFsPromises.lstat(fullPath),
984
2204
  catch: () => new ArchiveCreateError({ message: `Failed to stat: ${fullPath}` })
985
2205
  });
@@ -999,8 +2219,8 @@ var collectIgnorePatterns = (fs, pathService, dir, baseDir, ignoreRules) => Effe
999
2219
  yield* collectIgnorePatterns(fs, pathService, fullPath, baseDir, ignoreRules);
1000
2220
  }
1001
2221
  }
1002
- }).pipe(Effect10.catchAll(() => Effect10.void));
1003
- var initializeIgnoreRules = (fs, pathService, baseDir) => Effect10.gen(function* () {
2222
+ }).pipe(Effect21.catchAll(() => Effect21.void));
2223
+ var initializeIgnoreRules = (fs, pathService, baseDir) => Effect21.gen(function* () {
1004
2224
  const ignoreRules = ignore();
1005
2225
  ignoreRules.add([".git/", "**/.DS_Store", "**/Thumbs.db"]);
1006
2226
  for (const ignoreFileName of IGNORE_FILE_NAMES) {
@@ -1015,14 +2235,14 @@ var initializeIgnoreRules = (fs, pathService, baseDir) => Effect10.gen(function*
1015
2235
  yield* collectIgnorePatterns(fs, pathService, baseDir, baseDir, ignoreRules);
1016
2236
  return ignoreRules;
1017
2237
  });
1018
- var getFilesToInclude = (fs, pathService, dir, ignoreRules) => Effect10.gen(function* () {
2238
+ var getFilesToInclude = (fs, pathService, dir, ignoreRules) => Effect21.gen(function* () {
1019
2239
  const filesToInclude = [];
1020
- const walkDir = (currentDir) => Effect10.gen(function* () {
1021
- const items = yield* fs.readDirectory(currentDir).pipe(Effect10.mapError(() => new ArchiveCreateError({ message: `Failed to read directory: ${currentDir}` })));
2240
+ const walkDir = (currentDir) => Effect21.gen(function* () {
2241
+ const items = yield* fs.readDirectory(currentDir).pipe(Effect21.mapError(() => new ArchiveCreateError({ message: `Failed to read directory: ${currentDir}` })));
1022
2242
  for (const item of items) {
1023
2243
  const fullPath = pathService.join(currentDir, item);
1024
2244
  const relativePath = pathService.relative(dir, fullPath);
1025
- const stat = yield* Effect10.tryPromise({
2245
+ const stat = yield* Effect21.tryPromise({
1026
2246
  try: () => nodeFsPromises.lstat(fullPath),
1027
2247
  catch: () => new ArchiveCreateError({ message: `Failed to stat: ${fullPath}` })
1028
2248
  });
@@ -1036,21 +2256,21 @@ var getFilesToInclude = (fs, pathService, dir, ignoreRules) => Effect10.gen(func
1036
2256
  }
1037
2257
  }
1038
2258
  }
1039
- }).pipe(Effect10.catchAll(() => Effect10.void));
2259
+ }).pipe(Effect21.catchAll(() => Effect21.void));
1040
2260
  yield* walkDir(dir);
1041
2261
  return filesToInclude;
1042
2262
  });
1043
- var ArchiveService = class extends Effect10.Service()("ArchiveService", {
2263
+ var ArchiveService = class extends Effect21.Service()("ArchiveService", {
1044
2264
  accessors: true,
1045
- effect: Effect10.gen(function* () {
1046
- const fs = yield* FileSystem4.FileSystem;
1047
- const pathService = yield* Path4.Path;
1048
- const createZipArchive = Effect10.fn("ArchiveService.createZipArchive")(function* (inputDir, outputFile, options = {}) {
1049
- yield* Effect10.annotateCurrentSpan("inputDir", inputDir);
1050
- yield* Effect10.annotateCurrentSpan("outputFile", outputFile);
1051
- yield* Effect10.log("Creating zip archive", { inputDir, outputFile });
2265
+ effect: Effect21.gen(function* () {
2266
+ const fs = yield* FileSystem8.FileSystem;
2267
+ const pathService = yield* Path7.Path;
2268
+ const createZipArchive = Effect21.fn("ArchiveService.createZipArchive")(function* (inputDir, outputFile, options = {}) {
2269
+ yield* Effect21.annotateCurrentSpan("inputDir", inputDir);
2270
+ yield* Effect21.annotateCurrentSpan("outputFile", outputFile);
2271
+ yield* Effect21.log("Creating zip archive", { inputDir, outputFile });
1052
2272
  const ignoreRules = options.useIgnoreFiles !== false ? yield* initializeIgnoreRules(fs, pathService, inputDir) : ignore();
1053
- const filesToInclude = options.whitelist ? yield* Effect10.tryPromise({
2273
+ const filesToInclude = options.whitelist ? yield* Effect21.tryPromise({
1054
2274
  try: () => Promise.all(
1055
2275
  options.whitelist.map((file) => getFilesForPattern(file, inputDir, options.excludeFiles ?? []))
1056
2276
  ),
@@ -1058,25 +2278,25 @@ var ArchiveService = class extends Effect10.Service()("ArchiveService", {
1058
2278
  message: "Failed to resolve whitelist patterns",
1059
2279
  cause: error instanceof Error ? error.message : String(error)
1060
2280
  })
1061
- }) : yield* Effect10.gen(function* () {
2281
+ }) : yield* Effect21.gen(function* () {
1062
2282
  if (options.excludeFiles && options.excludeFiles.length > 0) {
1063
2283
  ignoreRules.add(options.excludeFiles);
1064
2284
  }
1065
2285
  return yield* getFilesToInclude(fs, pathService, inputDir, ignoreRules).pipe(
1066
- Effect10.map((files) => [files])
2286
+ Effect21.map((files) => [files])
1067
2287
  );
1068
2288
  });
1069
- const archiveExists = yield* fs.exists(outputFile).pipe(Effect10.catchAll(() => Effect10.succeed(false)));
2289
+ const archiveExists = yield* fs.exists(outputFile).pipe(Effect21.catchAll(() => Effect21.succeed(false)));
1070
2290
  if (archiveExists) {
1071
2291
  yield* fs.remove(outputFile).pipe(
1072
- Effect10.mapError(
2292
+ Effect21.mapError(
1073
2293
  (error) => new ArchiveCreateError({
1074
2294
  message: `Failed to remove existing archive: ${String(error)}`
1075
2295
  })
1076
2296
  )
1077
2297
  );
1078
2298
  }
1079
- yield* Effect10.tryPromise({
2299
+ yield* Effect21.tryPromise({
1080
2300
  try: () => new Promise((resolve, reject) => {
1081
2301
  const output = nodeFs.createWriteStream(outputFile);
1082
2302
  const archive = archiver("zip", { zlib: { level: 9 } });
@@ -1095,14 +2315,14 @@ var ArchiveService = class extends Effect10.Service()("ArchiveService", {
1095
2315
  })
1096
2316
  });
1097
2317
  const fileInfo = yield* fs.stat(outputFile).pipe(
1098
- Effect10.mapError(
2318
+ Effect21.mapError(
1099
2319
  (error) => new ArchiveCreateError({
1100
2320
  message: `Failed to stat archive: ${String(error)}`
1101
2321
  })
1102
2322
  )
1103
2323
  );
1104
2324
  const size = Number(fileInfo.size);
1105
- yield* Effect10.log("Archive created", { size });
2325
+ yield* Effect21.log("Archive created", { size });
1106
2326
  return { path: outputFile, size };
1107
2327
  });
1108
2328
  return { createZipArchive };
@@ -1111,250 +2331,104 @@ var ArchiveService = class extends Effect10.Service()("ArchiveService", {
1111
2331
  };
1112
2332
 
1113
2333
  // src/services/deployment-api.ts
1114
- import { objectToQueryString } from "@gigadrive/commons";
1115
- import { Config as Config2, Effect as Effect11, Schema as Schema5 } from "effect";
1116
- var CreateDeploymentResponse = Schema5.Struct({ id: Schema5.String });
1117
- var StartUploadResponse = Schema5.Struct({ uploadId: Schema5.String });
1118
- var PresignedUrlResponse = Schema5.Struct({ url: Schema5.String });
1119
- var DeploymentStatusResponse = Schema5.Struct({
1120
- status: Schema5.Literal("PENDING", "BUILDING", "PROVISIONING", "FAILED", "ACTIVE", "SUSPENDED")
1121
- });
1122
- var DeploymentLogPageResponse = Schema5.Struct({
1123
- totalItems: Schema5.Number,
1124
- limit: Schema5.Number,
1125
- offset: Schema5.Number,
1126
- items: Schema5.Array(
1127
- Schema5.Struct({
1128
- id: Schema5.String,
1129
- message: Schema5.String,
1130
- type: Schema5.Literal("INFO", "ERROR", "WARN"),
1131
- createdAt: Schema5.String
1132
- })
1133
- )
1134
- });
1135
- var ApiBaseUrl = Config2.string("GIGADRIVE_API_BASE_URL").pipe(Config2.withDefault("http://localhost:3000"));
1136
- var DeploymentApiService = class extends Effect11.Service()("DeploymentApiService", {
2334
+ import { Effect as Effect22 } from "effect";
2335
+ var DeploymentApiService = class extends Effect22.Service()("DeploymentApiService", {
1137
2336
  accessors: true,
1138
- effect: Effect11.gen(function* () {
1139
- const baseUrl = yield* ApiBaseUrl;
1140
- const authService = yield* AuthService;
1141
- const getAuthHeaders = Effect11.gen(function* () {
1142
- const token = yield* authService.getAccessToken.pipe(
1143
- Effect11.mapError((err) => new DeploymentAuthError({ message: `Authentication failed: ${err.message}` }))
1144
- );
1145
- return { Authorization: `Bearer ${token}` };
1146
- });
1147
- const createDeployment = Effect11.fn("DeploymentApiService.createDeployment")(function* (applicationId) {
1148
- yield* Effect11.annotateCurrentSpan("applicationId", applicationId);
1149
- yield* Effect11.log("Creating deployment", { applicationId });
1150
- const authHeaders = yield* getAuthHeaders;
1151
- const response = yield* Effect11.tryPromise({
1152
- try: () => fetch(`${baseUrl}/${applicationId}/deployments`, {
1153
- method: "POST",
1154
- headers: { "Content-Type": "application/json", ...authHeaders },
1155
- body: JSON.stringify({})
1156
- }),
2337
+ dependencies: [ApiClientService.Default],
2338
+ effect: Effect22.gen(function* () {
2339
+ const apiClient = yield* ApiClientService;
2340
+ const getClient = apiClient.getClient.pipe(
2341
+ Effect22.mapError((err) => new DeploymentAuthError({ message: `Authentication failed: ${err.message}` }))
2342
+ );
2343
+ const createDeployment = Effect22.fn("DeploymentApiService.createDeployment")(function* (applicationId) {
2344
+ yield* Effect22.annotateCurrentSpan("applicationId", applicationId);
2345
+ yield* Effect22.log("Creating deployment", { applicationId });
2346
+ const client = yield* getClient;
2347
+ const deployment = yield* Effect22.tryPromise({
2348
+ try: () => client.deployments.create({ applicationId }),
1157
2349
  catch: (error) => new DeploymentCreateError({
1158
2350
  message: `Failed to create deployment: ${error instanceof Error ? error.message : String(error)}`
1159
2351
  })
1160
2352
  });
1161
- if (!response.ok) {
1162
- const body = yield* Effect11.tryPromise({
1163
- try: () => response.text(),
1164
- catch: () => new DeploymentCreateError({
1165
- message: "Failed to read deployment creation error",
1166
- statusCode: response.status
1167
- })
1168
- });
1169
- return yield* Effect11.fail(
1170
- new DeploymentCreateError({
1171
- message: `Failed to create deployment: ${body}`,
1172
- statusCode: response.status
1173
- })
1174
- );
1175
- }
1176
- const json = yield* Effect11.tryPromise({
1177
- try: () => response.json(),
1178
- catch: () => new DeploymentCreateError({ message: "Failed to parse deployment response" })
1179
- });
1180
- const decoded = yield* Schema5.decodeUnknown(CreateDeploymentResponse)(json).pipe(
1181
- Effect11.mapError(() => new DeploymentCreateError({ message: "Invalid deployment response schema" }))
1182
- );
1183
- yield* Effect11.log("Deployment created", { deploymentId: decoded.id });
1184
- return decoded.id;
2353
+ yield* Effect22.log("Deployment created", { deploymentId: deployment.id });
2354
+ return deployment.id;
1185
2355
  });
1186
- const startMultipartUpload = Effect11.fn("DeploymentApiService.startMultipartUpload")(function* (deploymentId) {
1187
- yield* Effect11.annotateCurrentSpan("deploymentId", deploymentId);
1188
- const authHeaders = yield* getAuthHeaders;
1189
- const response = yield* Effect11.tryPromise({
1190
- try: () => fetch(`${baseUrl}/deployments/${deploymentId}/pre-signed-url/start`, {
1191
- method: "GET",
1192
- headers: { "Content-Type": "application/json", ...authHeaders }
1193
- }),
2356
+ const startMultipartUpload = Effect22.fn("DeploymentApiService.startMultipartUpload")(function* (deploymentId) {
2357
+ yield* Effect22.annotateCurrentSpan("deploymentId", deploymentId);
2358
+ const client = yield* getClient;
2359
+ const result = yield* Effect22.tryPromise({
2360
+ try: () => client.deployments.startUpload(deploymentId),
1194
2361
  catch: (error) => new UploadStartError({
1195
2362
  message: `Failed to start multipart upload: ${error instanceof Error ? error.message : String(error)}`
1196
2363
  })
1197
2364
  });
1198
- if (!response.ok) {
1199
- const body = yield* Effect11.tryPromise({
1200
- try: () => response.text(),
1201
- catch: () => new UploadStartError({ message: "Failed to read upload start error" })
1202
- });
1203
- return yield* Effect11.fail(
1204
- new UploadStartError({ message: `Failed to start multipart upload: ${body}`, statusCode: response.status })
1205
- );
1206
- }
1207
- const json = yield* Effect11.tryPromise({
1208
- try: () => response.json(),
1209
- catch: () => new UploadStartError({ message: "Failed to parse upload start response" })
1210
- });
1211
- const decoded = yield* Schema5.decodeUnknown(StartUploadResponse)(json).pipe(
1212
- Effect11.mapError(() => new UploadStartError({ message: "Invalid upload start response schema" }))
1213
- );
1214
- return decoded.uploadId;
2365
+ return result.uploadId;
1215
2366
  });
1216
- const getPresignedUrl = Effect11.fn("DeploymentApiService.getPresignedUrl")(function* (deploymentId, uploadId, partNumber) {
1217
- const authHeaders = yield* getAuthHeaders;
1218
- const response = yield* Effect11.tryPromise({
1219
- try: () => fetch(
1220
- `${baseUrl}/deployments/${deploymentId}/pre-signed-url/part?uploadId=${uploadId}&partNumber=${partNumber}`,
1221
- {
1222
- method: "GET",
1223
- headers: { "Content-Type": "application/json", ...authHeaders }
1224
- }
1225
- ),
2367
+ const getPresignedUrl = Effect22.fn("DeploymentApiService.getPresignedUrl")(function* (deploymentId, uploadId, partNumber) {
2368
+ const client = yield* getClient;
2369
+ const result = yield* Effect22.tryPromise({
2370
+ try: () => client.deployments.getPresignedUrl(deploymentId, uploadId, partNumber),
1226
2371
  catch: (error) => new PresignedUrlError({
1227
2372
  message: `Failed to get presigned URL: ${error instanceof Error ? error.message : String(error)}`,
1228
2373
  partNumber
1229
2374
  })
1230
2375
  });
1231
- if (!response.ok) {
1232
- const body = yield* Effect11.tryPromise({
1233
- try: () => response.text(),
1234
- catch: () => new PresignedUrlError({ message: "Failed to read presigned URL error", partNumber })
1235
- });
1236
- return yield* Effect11.fail(
1237
- new PresignedUrlError({ message: `Failed to get presigned URL: ${body}`, partNumber })
1238
- );
1239
- }
1240
- const json = yield* Effect11.tryPromise({
1241
- try: () => response.json(),
1242
- catch: () => new PresignedUrlError({ message: "Failed to parse presigned URL response", partNumber })
1243
- });
1244
- const decoded = yield* Schema5.decodeUnknown(PresignedUrlResponse)(json).pipe(
1245
- Effect11.mapError(() => new PresignedUrlError({ message: "Invalid presigned URL response schema", partNumber }))
1246
- );
1247
- return decoded.url;
2376
+ return result.url;
1248
2377
  });
1249
- const uploadPart = Effect11.fn("DeploymentApiService.uploadPart")(function* (presignedUrl, data, partNumber) {
1250
- yield* Effect11.annotateCurrentSpan("partNumber", partNumber);
1251
- yield* Effect11.annotateCurrentSpan("size", data.length);
1252
- const response = yield* Effect11.tryPromise({
1253
- try: () => fetch(presignedUrl, {
1254
- method: "PUT",
1255
- headers: {
1256
- "Content-Type": "application/zip",
1257
- "Content-Length": String(data.length)
1258
- },
1259
- body: data
1260
- }),
2378
+ const uploadPart = Effect22.fn("DeploymentApiService.uploadPart")(function* (presignedUrl, data, partNumber) {
2379
+ yield* Effect22.annotateCurrentSpan("partNumber", partNumber);
2380
+ yield* Effect22.annotateCurrentSpan("size", data.length);
2381
+ const client = yield* getClient;
2382
+ const result = yield* Effect22.tryPromise({
2383
+ try: () => client.deployments.uploadPart(presignedUrl, data, partNumber),
1261
2384
  catch: (error) => new UploadPartError({
1262
2385
  message: `Failed to upload part ${partNumber}: ${error instanceof Error ? error.message : String(error)}`,
1263
2386
  partNumber
1264
2387
  })
1265
2388
  });
1266
- if (!response.ok) {
1267
- return yield* Effect11.fail(
1268
- new UploadPartError({
1269
- message: `Failed to upload part ${partNumber}: ${response.statusText}`,
1270
- partNumber
1271
- })
1272
- );
1273
- }
1274
- const etag = response.headers.get("ETag");
1275
- if (!etag) {
1276
- return yield* Effect11.fail(
1277
- new UploadPartError({ message: `No ETag received for part ${partNumber}`, partNumber })
1278
- );
1279
- }
1280
- return { partNumber, etag };
2389
+ return result;
1281
2390
  });
1282
- const completeUpload = Effect11.fn("DeploymentApiService.completeUpload")(function* (deploymentId, uploadId, parts) {
1283
- yield* Effect11.annotateCurrentSpan("deploymentId", deploymentId);
1284
- yield* Effect11.annotateCurrentSpan("partsCount", parts.length);
1285
- const authHeaders = yield* getAuthHeaders;
1286
- const response = yield* Effect11.tryPromise({
1287
- try: () => fetch(`${baseUrl}/deployments/${deploymentId}/pre-signed-url/complete`, {
1288
- method: "POST",
1289
- headers: { "Content-Type": "application/json", ...authHeaders },
1290
- body: JSON.stringify({ uploadId, parts })
1291
- }),
2391
+ const completeUpload = Effect22.fn("DeploymentApiService.completeUpload")(function* (deploymentId, uploadId, parts) {
2392
+ yield* Effect22.annotateCurrentSpan("deploymentId", deploymentId);
2393
+ yield* Effect22.annotateCurrentSpan("partsCount", parts.length);
2394
+ const client = yield* getClient;
2395
+ yield* Effect22.tryPromise({
2396
+ try: () => client.deployments.completeUpload(deploymentId, uploadId, parts),
1292
2397
  catch: (error) => new UploadCompleteError({
1293
2398
  message: `Failed to complete upload: ${error instanceof Error ? error.message : String(error)}`
1294
2399
  })
1295
2400
  });
1296
- if (!response.ok) {
1297
- const body = yield* Effect11.tryPromise({
1298
- try: () => response.text(),
1299
- catch: () => new UploadCompleteError({ message: "Failed to read upload complete error" })
1300
- });
1301
- return yield* Effect11.fail(new UploadCompleteError({ message: `Failed to complete upload: ${body}` }));
1302
- }
1303
- yield* Effect11.log("Upload completed successfully");
2401
+ yield* Effect22.log("Upload completed successfully");
1304
2402
  });
1305
- const getDeploymentStatus = Effect11.fn("DeploymentApiService.getDeploymentStatus")(function* (deploymentId) {
1306
- const authHeaders = yield* getAuthHeaders;
1307
- const response = yield* Effect11.tryPromise({
1308
- try: () => fetch(`${baseUrl}/deployments/${deploymentId}`, {
1309
- method: "GET",
1310
- headers: { "Content-Type": "application/json", ...authHeaders }
1311
- }),
2403
+ const getDeploymentStatus = Effect22.fn("DeploymentApiService.getDeploymentStatus")(function* (deploymentId) {
2404
+ const client = yield* getClient;
2405
+ const deployment = yield* Effect22.tryPromise({
2406
+ try: () => client.deployments.get(deploymentId),
1312
2407
  catch: (error) => new DeploymentStatusError({
1313
2408
  message: `Failed to get deployment status: ${error instanceof Error ? error.message : String(error)}`
1314
2409
  })
1315
2410
  });
1316
- if (!response.ok) {
1317
- const body = yield* Effect11.tryPromise({
1318
- try: () => response.text(),
1319
- catch: () => new DeploymentStatusError({ message: "Failed to read deployment status error" })
1320
- });
1321
- return yield* Effect11.fail(new DeploymentStatusError({ message: `Failed to get deployment status: ${body}` }));
1322
- }
1323
- const json = yield* Effect11.tryPromise({
1324
- try: () => response.json(),
1325
- catch: () => new DeploymentStatusError({ message: "Failed to parse deployment status response" })
1326
- });
1327
- const decoded = yield* Schema5.decodeUnknown(DeploymentStatusResponse)(json).pipe(
1328
- Effect11.mapError(() => new DeploymentStatusError({ message: "Invalid deployment status response schema" }))
1329
- );
1330
- return decoded.status;
2411
+ return deployment.status;
1331
2412
  });
1332
- const getLogs = Effect11.fn("DeploymentApiService.getLogs")(function* (deploymentId, options) {
1333
- const authHeaders = yield* getAuthHeaders;
1334
- const response = yield* Effect11.tryPromise({
1335
- try: () => fetch(`${baseUrl}/deployments/${deploymentId}/logs${objectToQueryString(options)}`, {
1336
- method: "GET",
1337
- headers: { "Content-Type": "application/json", ...authHeaders }
1338
- }),
2413
+ const getLogs = Effect22.fn("DeploymentApiService.getLogs")(function* (deploymentId, options) {
2414
+ const client = yield* getClient;
2415
+ const result = yield* Effect22.tryPromise({
2416
+ try: () => client.deployments.getLogs(deploymentId, options),
1339
2417
  catch: (error) => new DeploymentLogsFetchError({
1340
2418
  message: `Failed to get logs: ${error instanceof Error ? error.message : String(error)}`
1341
2419
  })
1342
2420
  });
1343
- if (!response.ok) {
1344
- const body = yield* Effect11.tryPromise({
1345
- try: () => response.text(),
1346
- catch: () => new DeploymentLogsFetchError({ message: "Failed to read logs error" })
1347
- });
1348
- return yield* Effect11.fail(new DeploymentLogsFetchError({ message: `Failed to get logs: ${body}` }));
1349
- }
1350
- const json = yield* Effect11.tryPromise({
1351
- try: () => response.json(),
1352
- catch: () => new DeploymentLogsFetchError({ message: "Failed to parse logs response" })
2421
+ return result;
2422
+ });
2423
+ const getHostnames = Effect22.fn("DeploymentApiService.getHostnames")(function* (deploymentId) {
2424
+ const client = yield* getClient;
2425
+ const result = yield* Effect22.tryPromise({
2426
+ try: () => client.deployments.getHostnames(deploymentId),
2427
+ catch: (error) => new DeploymentHostnamesFetchError({
2428
+ message: `Failed to get hostnames: ${error instanceof Error ? error.message : String(error)}`
2429
+ })
1353
2430
  });
1354
- const decoded = yield* Schema5.decodeUnknown(DeploymentLogPageResponse)(json).pipe(
1355
- Effect11.mapError(() => new DeploymentLogsFetchError({ message: "Invalid logs response schema" }))
1356
- );
1357
- return decoded;
2431
+ return result.items;
1358
2432
  });
1359
2433
  return {
1360
2434
  createDeployment,
@@ -1363,89 +2437,102 @@ var DeploymentApiService = class extends Effect11.Service()("DeploymentApiServic
1363
2437
  uploadPart,
1364
2438
  completeUpload,
1365
2439
  getDeploymentStatus,
1366
- getLogs
2440
+ getLogs,
2441
+ getHostnames
1367
2442
  };
1368
2443
  })
1369
2444
  }) {
1370
2445
  };
1371
2446
 
1372
2447
  // src/commands/platform/deploy/index.ts
1373
- var deployCommand = Command5.make(
2448
+ var appOption5 = Options6.text("app").pipe(
2449
+ Options6.withAlias("a"),
2450
+ Options6.withDescription("Application ID to deploy to (overrides the linked application)"),
2451
+ Options6.optional
2452
+ );
2453
+ var deployCommand = Command11.make(
1374
2454
  "deploy",
1375
- {},
1376
- () => Effect12.gen(function* () {
2455
+ { app: appOption5 },
2456
+ ({ app }) => Effect23.gen(function* () {
1377
2457
  const projectConfig = yield* ProjectConfigService;
1378
2458
  const deploymentApi = yield* DeploymentApiService;
1379
2459
  const archiveService = yield* ArchiveService;
1380
- const pathService = yield* Path5.Path;
2460
+ const projectLink = yield* ProjectLinkService;
2461
+ const pathService = yield* Path8.Path;
1381
2462
  const cwd = process.cwd();
1382
- yield* Effect12.log("Starting deploy command");
2463
+ const applicationId = yield* Option12.match(app, {
2464
+ onSome: (id) => Effect23.succeed(id),
2465
+ onNone: () => projectLink.resolve(cwd).pipe(Effect23.map((link) => link.applicationId))
2466
+ });
2467
+ yield* Effect23.log("Starting deploy command");
1383
2468
  const { config } = yield* projectConfig.resolve(cwd);
1384
- yield* Console5.log("Creating deployment...");
1385
- const deploymentId = yield* deploymentApi.createDeployment("test");
1386
- yield* Console5.log(`Deployment ID: ${deploymentId}`);
1387
- yield* Console5.log("Creating archive...");
1388
- const archivePath = pathService.join(os2.tmpdir(), `project-${Date.now()}.zip`);
2469
+ yield* Console11.log("Creating deployment...");
2470
+ const deploymentId = yield* deploymentApi.createDeployment(applicationId);
2471
+ yield* Console11.log(`Deployment ID: ${deploymentId}`);
2472
+ yield* Console11.log("Creating archive...");
2473
+ const archivePath = pathService.join(os4.tmpdir(), `project-${Date.now()}.zip`);
1389
2474
  const archive = yield* archiveService.createZipArchive(config.userArchive?.rootOverwrite || cwd, archivePath, {
1390
2475
  whitelist: config.userArchive?.fileWhitelist,
1391
2476
  excludeFiles: config.excludeFiles,
1392
2477
  useIgnoreFiles: false,
1393
2478
  useManagedIgnore: false
1394
2479
  });
1395
- yield* Console5.log(`Archive created (${formatFileSize(archive.size)})`);
1396
- yield* Console5.log("Uploading archive...");
2480
+ yield* Console11.log(`Archive created (${formatFileSize(archive.size)})`);
2481
+ yield* Console11.log("Uploading archive...");
1397
2482
  yield* uploadArchive(deploymentId, archivePath, archive.size);
1398
- yield* Console5.log("Upload complete.");
1399
- yield* Console5.log("The deployment pipeline is now being provisioned. This may take a few seconds.");
2483
+ yield* Console11.log("Upload complete.");
2484
+ yield* Console11.log("The deployment pipeline is now being provisioned. This may take a few seconds.");
1400
2485
  yield* pollDeployment(deploymentId);
1401
2486
  }).pipe(
1402
- Effect12.catchTags({
1403
- ConfigNotFoundError: (err) => Console5.error(err.message).pipe(Effect12.andThen(Effect12.fail(err))),
1404
- ConfigParseError: (err) => Console5.error(`Config parse error: ${err.message}`).pipe(Effect12.andThen(Effect12.fail(err))),
1405
- ConfigValidationError: (err) => Effect12.gen(function* () {
2487
+ Effect23.catchTags({
2488
+ ProjectNotLinkedError: (err) => Console11.error(err.message).pipe(Effect23.andThen(Effect23.fail(err))),
2489
+ ProjectLinkReadError: (err) => Console11.error(err.message).pipe(Effect23.andThen(Effect23.fail(err))),
2490
+ ConfigNotFoundError: (err) => Console11.error(err.message).pipe(Effect23.andThen(Effect23.fail(err))),
2491
+ ConfigParseError: (err) => Console11.error(`Config parse error: ${err.message}`).pipe(Effect23.andThen(Effect23.fail(err))),
2492
+ ConfigValidationError: (err) => Effect23.gen(function* () {
1406
2493
  for (const e of err.errors) {
1407
- yield* Console5.error(e);
2494
+ yield* Console11.error(e);
1408
2495
  }
1409
- yield* Console5.error(err.message);
1410
- }).pipe(Effect12.andThen(Effect12.fail(err))),
1411
- DeploymentCreateError: (err) => Console5.error(`Deployment creation failed: ${err.message}`).pipe(Effect12.andThen(Effect12.fail(err))),
1412
- ArchiveCreateError: (err) => Console5.error(`Archive creation failed: ${err.message}`).pipe(Effect12.andThen(Effect12.fail(err))),
1413
- UploadStartError: (err) => Console5.error(`Upload failed: ${err.message}`).pipe(Effect12.andThen(Effect12.fail(err))),
1414
- UploadPartError: (err) => Console5.error(`Upload failed: ${err.message}`).pipe(Effect12.andThen(Effect12.fail(err))),
1415
- UploadCompleteError: (err) => Console5.error(`Upload failed: ${err.message}`).pipe(Effect12.andThen(Effect12.fail(err))),
1416
- PresignedUrlError: (err) => Console5.error(`Upload failed: ${err.message}`).pipe(Effect12.andThen(Effect12.fail(err))),
1417
- DeploymentAuthError: (err) => Console5.error(`Authentication failed: ${err.message}`).pipe(Effect12.andThen(Effect12.fail(err))),
1418
- DeploymentFailedError: (err) => Console5.error(err.message).pipe(Effect12.andThen(Effect12.fail(err)))
2496
+ yield* Console11.error(err.message);
2497
+ }).pipe(Effect23.andThen(Effect23.fail(err))),
2498
+ DeploymentCreateError: (err) => Console11.error(`Deployment creation failed: ${err.message}`).pipe(Effect23.andThen(Effect23.fail(err))),
2499
+ ArchiveCreateError: (err) => Console11.error(`Archive creation failed: ${err.message}`).pipe(Effect23.andThen(Effect23.fail(err))),
2500
+ UploadStartError: (err) => Console11.error(`Upload failed: ${err.message}`).pipe(Effect23.andThen(Effect23.fail(err))),
2501
+ UploadPartError: (err) => Console11.error(`Upload failed: ${err.message}`).pipe(Effect23.andThen(Effect23.fail(err))),
2502
+ UploadCompleteError: (err) => Console11.error(`Upload failed: ${err.message}`).pipe(Effect23.andThen(Effect23.fail(err))),
2503
+ PresignedUrlError: (err) => Console11.error(`Upload failed: ${err.message}`).pipe(Effect23.andThen(Effect23.fail(err))),
2504
+ DeploymentAuthError: (err) => Console11.error(`Authentication failed: ${err.message}`).pipe(Effect23.andThen(Effect23.fail(err))),
2505
+ DeploymentFailedError: (err) => Console11.error(err.message).pipe(Effect23.andThen(Effect23.fail(err)))
1419
2506
  })
1420
2507
  )
1421
2508
  );
1422
- var uploadArchive = (deploymentId, archivePath, fileSize) => Effect12.gen(function* () {
2509
+ var uploadArchive = (deploymentId, archivePath, fileSize) => Effect23.gen(function* () {
1423
2510
  const deploymentApi = yield* DeploymentApiService;
1424
- const fs = yield* FileSystem5.FileSystem;
2511
+ const fs = yield* FileSystem9.FileSystem;
1425
2512
  const uploadId = yield* deploymentApi.startMultipartUpload(deploymentId);
1426
2513
  const FILE_CHUNK_SIZE = 10 * 1024 * 1024;
1427
2514
  const numChunks = Math.ceil(fileSize / FILE_CHUNK_SIZE);
1428
- yield* Effect12.log("Starting multipart upload", { uploadId, numChunks, fileSize });
1429
- const parts = yield* Effect12.all(
2515
+ yield* Effect23.log("Starting multipart upload", { uploadId, numChunks, fileSize });
2516
+ const parts = yield* Effect23.all(
1430
2517
  Array.from({ length: numChunks }, (_, i) => {
1431
2518
  const partNumber = i + 1;
1432
2519
  const start = i * FILE_CHUNK_SIZE;
1433
2520
  const bytesToRead = partNumber < numChunks ? FILE_CHUNK_SIZE : fileSize - start;
1434
- return Effect12.gen(function* () {
2521
+ return Effect23.gen(function* () {
1435
2522
  const presignedUrl = yield* deploymentApi.getPresignedUrl(deploymentId, uploadId, partNumber);
1436
2523
  const chunkData = yield* Stream.runCollect(fs.stream(archivePath, { offset: start, bytesToRead })).pipe(
1437
- Effect12.map((chunks) => {
2524
+ Effect23.map((chunks) => {
1438
2525
  const arrays = Array.from(chunks);
1439
2526
  return Buffer.concat(arrays);
1440
2527
  }),
1441
- Effect12.mapError(
2528
+ Effect23.mapError(
1442
2529
  (error) => new UploadPartError({
1443
2530
  message: `Failed to read chunk ${partNumber}: ${String(error)}`,
1444
2531
  partNumber
1445
2532
  })
1446
2533
  )
1447
2534
  );
1448
- yield* Effect12.log(`Uploading part ${partNumber}/${numChunks}`, { size: chunkData.length });
2535
+ yield* Effect23.log(`Uploading part ${partNumber}/${numChunks}`, { size: chunkData.length });
1449
2536
  return yield* deploymentApi.uploadPart(presignedUrl, chunkData, partNumber);
1450
2537
  });
1451
2538
  }),
@@ -1453,20 +2540,20 @@ var uploadArchive = (deploymentId, archivePath, fileSize) => Effect12.gen(functi
1453
2540
  );
1454
2541
  yield* deploymentApi.completeUpload(deploymentId, uploadId, parts);
1455
2542
  });
1456
- var pollDeployment = (deploymentId) => Effect12.gen(function* () {
2543
+ var pollDeployment = (deploymentId) => Effect23.gen(function* () {
1457
2544
  const deploymentApi = yield* DeploymentApiService;
1458
2545
  let lastStatus = "PENDING";
1459
2546
  let logOffset = 0;
1460
- yield* Console5.log("Status: Pending");
1461
- yield* Effect12.repeat(
1462
- Effect12.gen(function* () {
1463
- const newStatus = yield* deploymentApi.getDeploymentStatus(deploymentId).pipe(Effect12.catchTag("DeploymentStatusError", () => Effect12.succeed(lastStatus)));
2547
+ yield* Console11.log("Status: Pending");
2548
+ yield* Effect23.repeat(
2549
+ Effect23.gen(function* () {
2550
+ const newStatus = yield* deploymentApi.getDeploymentStatus(deploymentId).pipe(Effect23.catchTag("DeploymentStatusError", () => Effect23.succeed(lastStatus)));
1464
2551
  if (newStatus !== lastStatus) {
1465
2552
  lastStatus = newStatus;
1466
2553
  const label = newStatus.charAt(0) + newStatus.slice(1).toLowerCase();
1467
- yield* Console5.log(`Status: ${label}`);
2554
+ yield* Console11.log(`Status: ${label}`);
1468
2555
  }
1469
- const logsPage = yield* deploymentApi.getLogs(deploymentId, { offset: logOffset, limit: 100 }).pipe(Effect12.catchTag("DeploymentLogsFetchError", () => Effect12.succeed(null)));
2556
+ const logsPage = yield* deploymentApi.getLogs(deploymentId, { offset: logOffset, limit: 100 }).pipe(Effect23.catchTag("DeploymentLogsFetchError", () => Effect23.succeed(null)));
1470
2557
  if (logsPage) {
1471
2558
  for (const logEntry of logsPage.items) {
1472
2559
  logOffset++;
@@ -1474,11 +2561,17 @@ var pollDeployment = (deploymentId) => Effect12.gen(function* () {
1474
2561
  }
1475
2562
  }
1476
2563
  if (newStatus === "ACTIVE") {
1477
- yield* Console5.log(`Deployed to https://${deploymentId}.gigadrivedev.com`);
1478
- return yield* Effect12.fail("done");
2564
+ const hostnames = yield* deploymentApi.getHostnames(deploymentId).pipe(Effect23.catchTag("DeploymentHostnamesFetchError", () => Effect23.succeed([])));
2565
+ const active = hostnames.find((h) => h.active) ?? hostnames.at(0);
2566
+ if (active) {
2567
+ yield* Console11.log(`Deployed to https://${active.hostname}`);
2568
+ } else {
2569
+ yield* Console11.log("Deployment is live.");
2570
+ }
2571
+ return yield* Effect23.fail("done");
1479
2572
  }
1480
2573
  if (newStatus === "FAILED") {
1481
- return yield* Effect12.fail(
2574
+ return yield* Effect23.fail(
1482
2575
  new DeploymentFailedError({
1483
2576
  message: "The deployment failed. Please check the logs for more information."
1484
2577
  })
@@ -1487,53 +2580,210 @@ var pollDeployment = (deploymentId) => Effect12.gen(function* () {
1487
2580
  }),
1488
2581
  Schedule.addDelay(Schedule.forever, () => Duration.seconds(1))
1489
2582
  ).pipe(
1490
- Effect12.catchAll((err) => {
1491
- if (err === "done") return Effect12.void;
1492
- return Effect12.fail(err);
2583
+ Effect23.catchAll((err) => {
2584
+ if (err === "done") return Effect23.void;
2585
+ return Effect23.fail(err);
1493
2586
  })
1494
2587
  );
1495
2588
  });
1496
2589
  var printLog = (logEntry) => {
1497
2590
  switch (logEntry.type) {
1498
2591
  case "ERROR":
1499
- return Console5.error(logEntry.message);
2592
+ return Console11.error(logEntry.message);
1500
2593
  case "WARN":
1501
- return Console5.warn(logEntry.message);
2594
+ return Console11.warn(logEntry.message);
1502
2595
  default:
1503
- return Console5.log(logEntry.message);
2596
+ return Console11.log(logEntry.message);
1504
2597
  }
1505
2598
  };
1506
2599
 
1507
2600
  // src/commands/platform/index.ts
1508
- var platformBase = Command6.make("platform", {}, () => Effect13.void);
1509
- var platformCommand = platformBase.pipe(Command6.withSubcommands([deployCommand]));
2601
+ var platformBase = Command12.make("platform", {}, () => Effect24.void);
2602
+ var platformCommand = platformBase.pipe(Command12.withSubcommands([deployCommand]));
2603
+
2604
+ // src/commands/setup/index.ts
2605
+ import { Args as Args4, Command as Command13, Options as Options7, Prompt as Prompt3 } from "@effect/cli";
2606
+ import { Console as Console12, Effect as Effect25, Option as Option13 } from "effect";
2607
+ var fileArg2 = Args4.text({ name: "file" }).pipe(
2608
+ Args4.withDescription("Output file to write (default: .env.local)"),
2609
+ Args4.optional
2610
+ );
2611
+ var appOption6 = Options7.text("app").pipe(
2612
+ Options7.withAlias("a"),
2613
+ Options7.withDescription("Application ID to set up (skips the interactive picker)"),
2614
+ Options7.optional
2615
+ );
2616
+ var orgOption6 = Options7.text("org").pipe(
2617
+ Options7.withAlias("o"),
2618
+ Options7.withDescription("Only list applications belonging to this organization ID when picking"),
2619
+ Options7.optional
2620
+ );
2621
+ var environmentOption2 = Options7.text("environment").pipe(
2622
+ Options7.withAlias("e"),
2623
+ Options7.withDescription("Environment slug to resolve overrides for (e.g. preview)"),
2624
+ Options7.optional
2625
+ );
2626
+ var yesOption2 = Options7.boolean("yes").pipe(
2627
+ Options7.withAlias("y"),
2628
+ Options7.withDescription("Overwrite an existing file without confirmation")
2629
+ );
2630
+ var rotateOption2 = Options7.boolean("rotate").pipe(
2631
+ Options7.withDescription("Force a fresh API key, revoking the previous one")
2632
+ );
2633
+ var setupCommand = Command13.make(
2634
+ "setup",
2635
+ {
2636
+ file: fileArg2,
2637
+ app: appOption6,
2638
+ org: orgOption6,
2639
+ environment: environmentOption2,
2640
+ yes: yesOption2,
2641
+ rotate: rotateOption2
2642
+ },
2643
+ ({ file, app, org, environment, yes, rotate }) => Effect25.gen(function* () {
2644
+ const apiClient = yield* ApiClientService;
2645
+ const projectLink = yield* ProjectLinkService;
2646
+ const envFile = yield* EnvFileService;
2647
+ const localCredentials = yield* LocalCredentialsService;
2648
+ const cwd = process.cwd();
2649
+ const existingLink = yield* projectLink.load(cwd);
2650
+ let applicationId;
2651
+ if (Option13.isSome(existingLink)) {
2652
+ applicationId = existingLink.value.applicationId;
2653
+ if (Option13.isSome(app) || Option13.isSome(org)) {
2654
+ yield* Console12.log(
2655
+ `Note: this directory is already linked to ${applicationId}; ignoring --app/--org. Run "gigadrive unlink" first to target a different application.`
2656
+ );
2657
+ }
2658
+ } else {
2659
+ const organizationId = Option13.getOrUndefined(org);
2660
+ const { items: apps } = yield* apiClient.request((client) => client.applications.list({ organizationId }));
2661
+ if (apps.length === 0) {
2662
+ return yield* Effect25.fail(
2663
+ new NoApplicationsFoundError({
2664
+ message: 'No applications found. Create one in the Gigadrive console, then run "gigadrive setup" again.'
2665
+ })
2666
+ );
2667
+ }
2668
+ const explicitAppId = Option13.getOrUndefined(app);
2669
+ let chosen;
2670
+ if (explicitAppId !== void 0) {
2671
+ const match = apps.find((a) => a.id === explicitAppId);
2672
+ if (match === void 0) {
2673
+ return yield* Effect25.fail(
2674
+ new ApplicationNotFoundError({
2675
+ message: `Application "${explicitAppId}" was not found among your applications.`
2676
+ })
2677
+ );
2678
+ }
2679
+ chosen = match;
2680
+ } else if (apps.length === 1) {
2681
+ chosen = apps[0];
2682
+ } else {
2683
+ chosen = yield* Prompt3.run(
2684
+ Prompt3.select({
2685
+ message: "Select an application to set up",
2686
+ choices: apps.map((a) => ({ title: `${a.name} (${a.organization.name})`, value: a }))
2687
+ })
2688
+ );
2689
+ }
2690
+ applicationId = chosen.id;
2691
+ yield* projectLink.save(cwd, { applicationId, organizationId: chosen.organization.id });
2692
+ yield* Console12.log(`Linked this directory to application ${applicationId}.`);
2693
+ }
2694
+ const target = Option13.getOrElse(file, () => ".env.local");
2695
+ const environmentSlug = Option13.getOrUndefined(environment);
2696
+ const { items, omittedSensitive } = yield* apiClient.request(
2697
+ (client) => client.applications.envVars.pull(applicationId, environmentSlug ? { environment: environmentSlug } : void 0)
2698
+ );
2699
+ const existing = yield* envFile.read(target);
2700
+ if (Option13.isSome(existing) && !yes) {
2701
+ const overwrite = yield* Prompt3.run(
2702
+ Prompt3.confirm({ message: `${target} already exists. Overwrite?`, initial: false })
2703
+ );
2704
+ if (!overwrite) {
2705
+ return yield* Console12.log("Cancelled.");
2706
+ }
2707
+ }
2708
+ const credentialEntries2 = yield* localCredentials.provision({
2709
+ applicationId,
2710
+ rotate,
2711
+ existingContent: Option13.getOrUndefined(existing)
2712
+ });
2713
+ const entries = [...items.map((v) => ({ key: v.key, value: v.value })), ...credentialEntries2];
2714
+ yield* envFile.write(target, entries);
2715
+ const gitignored = yield* envFile.ensureGitignored(cwd, target);
2716
+ yield* Console12.log("\nLocal dev environment ready.");
2717
+ yield* Console12.log(` \u2022 Wrote ${items.length} variable(s) and API credentials to ${target}.`);
2718
+ if (omittedSensitive > 0) {
2719
+ yield* Console12.log(
2720
+ ` \u2022 Skipped ${omittedSensitive} sensitive variable(s) (only production/preview hold secrets).`
2721
+ );
2722
+ }
2723
+ if (gitignored) {
2724
+ yield* Console12.log(` \u2022 Added ${target} to .gitignore.`);
2725
+ }
2726
+ yield* Console12.log(" \u2022 Your app can now call the Gigadrive API via the SDK (GIGADRIVE_CLIENT_ID/SECRET).");
2727
+ }).pipe(
2728
+ Effect25.catchTags({
2729
+ // Ctrl+C during a prompt is a deliberate cancel — exit 0.
2730
+ QuitException: () => Console12.log("Cancelled."),
2731
+ NotAuthenticatedError: (err) => Console12.error('You are not logged in. Run "gigadrive login" to authenticate.').pipe(
2732
+ Effect25.andThen(Effect25.fail(err))
2733
+ ),
2734
+ ApiRequestError: (err) => Console12.error(`Setup failed: ${err.message}`).pipe(Effect25.andThen(Effect25.fail(err))),
2735
+ NoApplicationsFoundError: (err) => Console12.error(err.message).pipe(Effect25.andThen(Effect25.fail(err))),
2736
+ ApplicationNotFoundError: (err) => Console12.error(err.message).pipe(Effect25.andThen(Effect25.fail(err))),
2737
+ ProjectLinkReadError: (err) => Console12.error(err.message).pipe(Effect25.andThen(Effect25.fail(err))),
2738
+ ProjectLinkWriteError: (err) => Console12.error(err.message).pipe(Effect25.andThen(Effect25.fail(err))),
2739
+ EnvFileReadError: (err) => Console12.error(err.message).pipe(Effect25.andThen(Effect25.fail(err))),
2740
+ EnvFileWriteError: (err) => Console12.error(err.message).pipe(Effect25.andThen(Effect25.fail(err))),
2741
+ DevCredentialsStoreReadError: (err) => Console12.error(err.message).pipe(Effect25.andThen(Effect25.fail(err))),
2742
+ DevCredentialsStoreWriteError: (err) => Console12.error(err.message).pipe(Effect25.andThen(Effect25.fail(err)))
2743
+ })
2744
+ )
2745
+ );
1510
2746
 
1511
2747
  // src/commands/whoami/index.ts
1512
- import { Command as Command7 } from "@effect/cli";
1513
- import { Console as Console6, Effect as Effect14 } from "effect";
1514
- var whoamiCommand = Command7.make(
2748
+ import { Command as Command14 } from "@effect/cli";
2749
+ import { Console as Console13, Effect as Effect26 } from "effect";
2750
+ var whoamiCommand = Command14.make(
1515
2751
  "whoami",
1516
2752
  {},
1517
- () => Effect14.gen(function* () {
2753
+ () => Effect26.gen(function* () {
1518
2754
  const auth = yield* AuthService;
1519
2755
  const info = yield* auth.getUserInfo;
1520
- yield* Console6.log(JSON.stringify(info, null, 2));
2756
+ yield* Console13.log(JSON.stringify(info, null, 2));
1521
2757
  }).pipe(
1522
- Effect14.catchTags({
1523
- NotAuthenticatedError: () => Console6.log('You are not logged in. Run "gigadrive login" to authenticate.'),
1524
- UserInfoFetchError: (err) => Console6.error(`Failed to retrieve user info: ${err.message}`),
1525
- OAuthDiscoveryError: (err) => Console6.error(`Failed to retrieve user info: ${err.message}`)
2758
+ Effect26.catchTags({
2759
+ NotAuthenticatedError: () => Console13.log('You are not logged in. Run "gigadrive login" to authenticate.'),
2760
+ UserInfoFetchError: (err) => Console13.error(`Failed to retrieve user info: ${err.message}`),
2761
+ OAuthDiscoveryError: (err) => Console13.error(`Failed to retrieve user info: ${err.message}`)
1526
2762
  })
1527
2763
  )
1528
2764
  );
1529
2765
 
1530
2766
  // src/index.ts
1531
- var gigadrive = Command8.make("gigadrive", {}, () => Effect15.void).pipe(
1532
- Command8.withSubcommands([loginCommand, whoamiCommand, buildCommand, debugCommand, platformCommand])
2767
+ var gigadrive = Command15.make("gigadrive", {}, () => Effect27.void).pipe(
2768
+ Command15.withSubcommands([
2769
+ loginCommand,
2770
+ logoutCommand,
2771
+ whoamiCommand,
2772
+ linkCommand,
2773
+ unlinkCommand,
2774
+ appsCommand,
2775
+ envCommand,
2776
+ deploymentsCommand,
2777
+ aiCommand,
2778
+ buildCommand,
2779
+ debugCommand,
2780
+ platformCommand,
2781
+ setupCommand
2782
+ ])
1533
2783
  );
1534
- var cli = Command8.run(gigadrive, {
2784
+ var cli = Command15.run(gigadrive, {
1535
2785
  name: "Gigadrive CLI",
1536
- version: "2.0.0"
2786
+ version: package_default.version
1537
2787
  });
1538
2788
  var BaseServices = Layer.mergeAll(
1539
2789
  OAuthConfigService.Default,
@@ -1541,12 +2791,17 @@ var BaseServices = Layer.mergeAll(
1541
2791
  PackageManagerService.Default,
1542
2792
  ArchiveService.Default,
1543
2793
  NetworkConfigLive2,
1544
- ProjectConfigService.Default
2794
+ ProjectConfigService.Default,
2795
+ ProjectLinkService.Default,
2796
+ EnvFileService.Default,
2797
+ DevCredentialsStore.Default
1545
2798
  ).pipe(Layer.provideMerge(AuthService.Default));
2799
+ var ApiClientLive = Layer.provide(ApiClientService.Default, BaseServices);
1546
2800
  var DeploymentApiLive = Layer.provide(DeploymentApiService.Default, BaseServices);
1547
- var ServicesLive = Layer.mergeAll(BaseServices, DeploymentApiLive);
2801
+ var LocalCredentialsLive = Layer.provide(LocalCredentialsService.Default, BaseServices);
2802
+ var ServicesLive = Layer.mergeAll(BaseServices, ApiClientLive, DeploymentApiLive, LocalCredentialsLive);
1548
2803
  var AppLive = Layer.mergeAll(ServicesLive, Logger.minimumLogLevel(LogLevel.Info)).pipe(
1549
2804
  Layer.provideMerge(NodeContext.layer)
1550
2805
  );
1551
- NodeRuntime.runMain(Effect15.provide(cli(process.argv), AppLive));
2806
+ NodeRuntime.runMain(Effect27.provide(cli(process.argv), AppLive));
1552
2807
  //# sourceMappingURL=index.mjs.map