gigadrive 2.0.0 → 2.1.1

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,1163 +1,1552 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command } from "commander";
4
+ import { Command as Command8 } from "@effect/cli";
5
+ import { NodeContext, NodeRuntime } from "@effect/platform-node";
6
+ import { NetworkConfigLive as NetworkConfigLive2 } from "@gigadrive/network-config";
7
+ import { Effect as Effect15, Layer, LogLevel, Logger } from "effect";
5
8
 
6
- // src/util/log.ts
7
- import { createConsola } from "consola";
8
- var verbose = false;
9
- var consola = createConsola({
10
- level: verbose ? 4 : 3,
11
- // 4 includes debug messages, 3 is the default (info and above)
12
- formatOptions: {
13
- colors: true
14
- }
15
- });
16
- var updateConsola = () => {
17
- consola.level = verbose ? 4 : 3;
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";
14
+
15
+ // src/errors.ts
16
+ import { Schema } from "effect";
17
+ var OAuthDiscoveryError = class extends Schema.TaggedError()("OAuthDiscoveryError", {
18
+ message: Schema.String,
19
+ cause: Schema.optional(Schema.String)
20
+ }) {
18
21
  };
19
- var setVerbose = (value) => {
20
- verbose = value;
21
- updateConsola();
22
+ var AuthStorageReadError = class extends Schema.TaggedError()("AuthStorageReadError", {
23
+ message: Schema.String
24
+ }) {
22
25
  };
23
- var log = (message) => {
24
- consola.info(message);
26
+ var AuthStorageWriteError = class extends Schema.TaggedError()("AuthStorageWriteError", {
27
+ message: Schema.String,
28
+ cause: Schema.optional(Schema.String)
29
+ }) {
25
30
  };
26
- var error = (message) => {
27
- consola.error(message);
31
+ var LoginFlowError = class extends Schema.TaggedError()("LoginFlowError", {
32
+ message: Schema.String,
33
+ cause: Schema.optional(Schema.String)
34
+ }) {
28
35
  };
29
- var success = (message) => {
30
- consola.success(message);
36
+ var TokenExchangeError = class extends Schema.TaggedError()("TokenExchangeError", {
37
+ message: Schema.String,
38
+ statusCode: Schema.optional(Schema.Number)
39
+ }) {
31
40
  };
32
- var warn = (message) => {
33
- consola.warn(message);
41
+ var TokenRefreshError = class extends Schema.TaggedError()("TokenRefreshError", {
42
+ message: Schema.String,
43
+ statusCode: Schema.optional(Schema.Number)
44
+ }) {
34
45
  };
35
- var debug = (message, ...optionalParams) => {
36
- if (!verbose) {
37
- return;
38
- }
39
- consola.debug("[DEBUG]", message, ...optionalParams);
46
+ var NotAuthenticatedError = class extends Schema.TaggedError()("NotAuthenticatedError", {
47
+ message: Schema.String
48
+ }) {
40
49
  };
41
- var spinner = (message) => {
42
- log(message);
50
+ var UserInfoFetchError = class extends Schema.TaggedError()("UserInfoFetchError", {
51
+ message: Schema.String,
52
+ statusCode: Schema.optional(Schema.Number)
53
+ }) {
43
54
  };
44
-
45
- // src/commands/build/index.ts
46
- import { exec as exec2 } from "@gigadrive/build-utils";
47
- import fs2 from "fs";
48
- import path2 from "path";
49
-
50
- // src/util/pm.ts
51
- import fs from "fs";
52
- import path from "path";
53
-
54
- // ../build-utils/src/exec.ts
55
- import { spawn } from "child_process";
56
- async function exec({
57
- command,
58
- cwd = process.cwd(),
59
- env: env2,
60
- onOutput = (data) => console.log(data),
61
- onError = (data) => console.error(data)
55
+ var ConfigNotFoundError = class extends Schema.TaggedError()("ConfigNotFoundError", {
56
+ message: Schema.String,
57
+ directory: Schema.String
62
58
  }) {
63
- return new Promise((resolve, reject) => {
64
- let spawnCommand;
65
- let spawnArgs = [];
66
- if (typeof command === "string") {
67
- const parts = command.split(" ");
68
- spawnCommand = parts[0];
69
- spawnArgs = parts.slice(1);
70
- } else {
71
- spawnCommand = command.command;
72
- spawnArgs = command.args || [];
73
- }
74
- const childProcess = spawn(spawnCommand, spawnArgs, {
75
- cwd,
76
- shell: true,
77
- env: env2
78
- });
79
- childProcess.stdout.on("data", (data) => {
80
- onOutput(data.toString().trim());
81
- });
82
- childProcess.stderr.on("data", (data) => {
83
- onError(data.toString().trim());
84
- });
85
- childProcess.on("error", (error2) => {
86
- reject(new Error(`Failed to start process: ${error2.message}`));
87
- });
88
- childProcess.on("close", (code) => {
89
- if (code === 0) {
90
- resolve();
91
- } else {
92
- reject(new Error(`Process exited with code ${code}`));
93
- }
94
- });
95
- });
96
- }
97
-
98
- // src/util/pm.ts
99
- var getPackageManager = async ({ cwd }) => {
100
- const packageJsonPath = path.join(cwd, "package.json");
101
- if (!fs.existsSync(packageJsonPath)) {
102
- return null;
103
- }
104
- if (fs.existsSync(path.join(cwd, "yarn.lock"))) {
105
- return "yarn";
106
- }
107
- if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) {
108
- return "pnpm";
59
+ };
60
+ var ConfigParseError = class extends Schema.TaggedError()("ConfigParseError", {
61
+ message: Schema.String,
62
+ cause: Schema.optional(Schema.String)
63
+ }) {
64
+ };
65
+ var ConfigValidationError = class extends Schema.TaggedError()("ConfigValidationError", {
66
+ message: Schema.String,
67
+ errors: Schema.Array(Schema.String)
68
+ }) {
69
+ };
70
+ var PackageJsonNotFoundError = class extends Schema.TaggedError()(
71
+ "PackageJsonNotFoundError",
72
+ {
73
+ message: Schema.String,
74
+ directory: Schema.String
109
75
  }
110
- if (fs.existsSync(path.join(cwd, "bun.lockb"))) {
111
- return "bun";
76
+ ) {
77
+ };
78
+ var PackageJsonParseError = class extends Schema.TaggedError()("PackageJsonParseError", {
79
+ message: Schema.String,
80
+ directory: Schema.String,
81
+ cause: Schema.optional(Schema.String)
82
+ }) {
83
+ };
84
+ var BuildScriptNotFoundError = class extends Schema.TaggedError()(
85
+ "BuildScriptNotFoundError",
86
+ {
87
+ message: Schema.String
112
88
  }
113
- if (fs.existsSync(path.join(cwd, "package-lock.json"))) {
114
- return "npm";
89
+ ) {
90
+ };
91
+ var PackageManagerNotFoundError = class extends Schema.TaggedError()(
92
+ "PackageManagerNotFoundError",
93
+ {
94
+ message: Schema.String,
95
+ directory: Schema.String
115
96
  }
116
- try {
117
- await exec({ command: "bun --version", cwd });
118
- return "bun";
119
- } catch {
120
- return "npm";
97
+ ) {
98
+ };
99
+ var ExecError = class extends Schema.TaggedError()("ExecError", {
100
+ message: Schema.String,
101
+ command: Schema.String,
102
+ cause: Schema.optional(Schema.String)
103
+ }) {
104
+ };
105
+ var DeploymentCreateError = class extends Schema.TaggedError()("DeploymentCreateError", {
106
+ message: Schema.String,
107
+ statusCode: Schema.optional(Schema.Number)
108
+ }) {
109
+ };
110
+ var UploadStartError = class extends Schema.TaggedError()("UploadStartError", {
111
+ message: Schema.String,
112
+ statusCode: Schema.optional(Schema.Number)
113
+ }) {
114
+ };
115
+ var PresignedUrlError = class extends Schema.TaggedError()("PresignedUrlError", {
116
+ message: Schema.String,
117
+ partNumber: Schema.Number
118
+ }) {
119
+ };
120
+ var UploadPartError = class extends Schema.TaggedError()("UploadPartError", {
121
+ message: Schema.String,
122
+ partNumber: Schema.Number
123
+ }) {
124
+ };
125
+ var UploadCompleteError = class extends Schema.TaggedError()("UploadCompleteError", {
126
+ message: Schema.String
127
+ }) {
128
+ };
129
+ var DeploymentStatusError = class extends Schema.TaggedError()("DeploymentStatusError", {
130
+ message: Schema.String
131
+ }) {
132
+ };
133
+ var DeploymentLogsFetchError = class extends Schema.TaggedError()(
134
+ "DeploymentLogsFetchError",
135
+ {
136
+ message: Schema.String
121
137
  }
138
+ ) {
139
+ };
140
+ var ArchiveCreateError = class extends Schema.TaggedError()("ArchiveCreateError", {
141
+ message: Schema.String,
142
+ cause: Schema.optional(Schema.String)
143
+ }) {
144
+ };
145
+ var DeploymentAuthError = class extends Schema.TaggedError()("DeploymentAuthError", {
146
+ message: Schema.String
147
+ }) {
122
148
  };
123
- var installCommand = (pm) => {
124
- return {
125
- bun: "bun install",
126
- npm: "npm install",
127
- pnpm: "pnpm install",
128
- yarn: "yarn install"
129
- }[pm] || "npm install";
149
+ var DeploymentFailedError = class extends Schema.TaggedError()("DeploymentFailedError", {
150
+ message: Schema.String
151
+ }) {
130
152
  };
131
- var buildCommand = (pm) => {
132
- return {
133
- bun: "bun run build",
134
- npm: "npm run build",
135
- pnpm: "pnpm build",
136
- yarn: "yarn build"
137
- }[pm] || "npm run build";
153
+
154
+ // src/services/package-manager.ts
155
+ 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", {
159
+ accessors: true,
160
+ effect: Effect.gen(function* () {
161
+ const fs = yield* FileSystem.FileSystem;
162
+ 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(
167
+ Effect.mapError(
168
+ () => new PackageManagerNotFoundError({ message: "Failed to check for package.json", directory: cwd })
169
+ )
170
+ );
171
+ if (!exists) {
172
+ return yield* Effect.fail(
173
+ new PackageManagerNotFoundError({
174
+ message: "No package.json found",
175
+ directory: cwd
176
+ })
177
+ );
178
+ }
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))
198
+ );
199
+ const pm = bunAvailable ? "bun" : "npm";
200
+ yield* Effect.log("Package manager detected via fallback", { packageManager: pm });
201
+ return pm;
202
+ });
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 };
216
+ })
217
+ }) {
138
218
  };
139
219
 
140
220
  // src/commands/build/index.ts
141
- var build = (parent) => {
142
- parent.command("build").description("Build the current project").action(async () => {
143
- const env2 = {
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
+ );
236
+ if (!packageJsonExists) {
237
+ return yield* Effect2.fail(new PackageJsonNotFoundError({ message: "No package.json found", directory: cwd }));
238
+ }
239
+ const packageJsonContent = yield* fs.readFileString(packageJsonPath, "utf-8").pipe(
240
+ Effect2.mapError(() => new PackageJsonNotFoundError({ message: "Failed to read package.json", directory: cwd }))
241
+ );
242
+ const packageJson = yield* Effect2.try({
243
+ try: () => JSON.parse(packageJsonContent),
244
+ catch: (error) => new PackageJsonParseError({
245
+ message: "Failed to parse package.json",
246
+ directory: cwd,
247
+ cause: error instanceof Error ? error.message : String(error)
248
+ })
249
+ });
250
+ if (!packageJson.scripts?.build) {
251
+ return yield* Effect2.fail(new BuildScriptNotFoundError({ message: "No build script found in package.json" }));
252
+ }
253
+ const pm = yield* pmService.detect(cwd);
254
+ const buildEnv = {
144
255
  ...process.env,
145
256
  NODE_ENV: "production",
146
257
  NEBULA: "1",
147
258
  VERCEL: "1",
148
- // Vercel compatibility
149
259
  NOW_BUILDER: "1"
150
260
  };
151
- const cwd = process.cwd();
152
- const packageJsonPath = path2.join(cwd, "package.json");
153
- if (!fs2.existsSync(packageJsonPath)) {
154
- error("No package.json found");
155
- return;
156
- }
157
- const packageJson = JSON.parse(fs2.readFileSync(packageJsonPath, "utf-8"));
158
- const buildScript = packageJson.scripts?.build;
159
- if (!buildScript) {
160
- error("No build script found in package.json");
161
- return;
162
- }
163
- const packageManager = await getPackageManager({ cwd });
164
- if (!packageManager) {
165
- error("No package manager found");
166
- return;
167
- }
168
- await exec2({
169
- command: installCommand(packageManager),
170
- cwd,
171
- env: env2,
172
- onOutput: (chunk) => {
173
- log(chunk);
174
- }
261
+ const installCmd = pmService.installCommand(pm);
262
+ yield* Console.log(`Running: ${installCmd}`);
263
+ yield* Effect2.tryPromise({
264
+ try: () => exec2({
265
+ command: installCmd,
266
+ cwd,
267
+ env: buildEnv,
268
+ onOutput: (chunk) => {
269
+ Console.log(chunk).pipe(Effect2.runSync);
270
+ }
271
+ }),
272
+ catch: (error) => new ExecError({
273
+ message: "Install failed",
274
+ command: installCmd,
275
+ cause: error instanceof Error ? error.message : String(error)
276
+ })
277
+ });
278
+ const buildCmd = pmService.buildCommand(pm);
279
+ yield* Console.log(`Running: ${buildCmd}`);
280
+ yield* Effect2.tryPromise({
281
+ try: () => exec2({
282
+ command: buildCmd,
283
+ cwd,
284
+ env: buildEnv,
285
+ onOutput: (chunk) => {
286
+ Console.log(chunk).pipe(Effect2.runSync);
287
+ }
288
+ }),
289
+ catch: (error) => new ExecError({
290
+ message: "Build failed",
291
+ command: buildCmd,
292
+ cause: error instanceof Error ? error.message : String(error)
293
+ })
175
294
  });
176
- await exec2({
177
- command: buildCommand(packageManager),
178
- cwd,
179
- env: env2,
180
- onOutput: (chunk) => {
181
- log(chunk);
295
+ yield* Console.log("Build completed successfully.");
296
+ }).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)))
303
+ })
304
+ )
305
+ );
306
+
307
+ // src/commands/debug/index.ts
308
+ import { Command as Command3 } from "@effect/cli";
309
+ import { Effect as Effect5 } from "effect";
310
+
311
+ // src/commands/debug/config/index.ts
312
+ import { Command as Command2 } from "@effect/cli";
313
+ import { Console as Console2, Effect as Effect4 } from "effect";
314
+
315
+ // src/services/project-config.ts
316
+ import {
317
+ detectFramework,
318
+ mergeWithFrameworkDefaults,
319
+ NetworkConfigLive,
320
+ parseConfig,
321
+ parseConfigRaw,
322
+ postProcessConfig,
323
+ RawConfigReader
324
+ } from "@gigadrive/network-config";
325
+ import { Effect as Effect3 } from "effect";
326
+ 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 }))
334
+ })
335
+ );
336
+ var ProjectConfigService = class extends Effect3.Service()("ProjectConfigService", {
337
+ accessors: true,
338
+ dependencies: [NetworkConfigLive],
339
+ effect: Effect3.gen(function* () {
340
+ 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 });
344
+ const configPath = yield* rawReader.findConfig(cwd);
345
+ const detection = yield* detectFramework(cwd).pipe(
346
+ Effect3.catchTag("FrameworkNotDetectedError", () => Effect3.succeed(null)),
347
+ Effect3.catchTag(
348
+ "ManifestReadError",
349
+ (e) => Effect3.fail(new ConfigParseError({ message: `Framework detection failed: ${e.message}`, cause: e.filePath }))
350
+ ),
351
+ Effect3.catchTag(
352
+ "ManifestParseError",
353
+ (e) => Effect3.fail(new ConfigParseError({ message: `Framework detection failed: ${e.message}`, cause: e.filePath }))
354
+ )
355
+ );
356
+ if (detection) {
357
+ yield* Effect3.log("Framework auto-detected", {
358
+ framework: detection.framework.name,
359
+ slug: detection.framework.slug
360
+ });
361
+ }
362
+ let config;
363
+ let resolvedConfigPath = null;
364
+ let framework;
365
+ if (configPath && detection) {
366
+ yield* Effect3.log("Config file found, merging with framework defaults", { configPath });
367
+ resolvedConfigPath = configPath;
368
+ const userConfig = yield* wrapParseErrors(parseConfigRaw(configPath, cwd));
369
+ const merged = yield* mergeWithFrameworkDefaults(userConfig, detection.config);
370
+ config = yield* wrapParseErrors(postProcessConfig(merged, cwd));
371
+ framework = { name: detection.framework.name, slug: detection.framework.slug };
372
+ } else if (configPath) {
373
+ yield* Effect3.log("Config file found", { configPath });
374
+ resolvedConfigPath = configPath;
375
+ config = yield* wrapParseErrors(parseConfig(configPath, cwd));
376
+ } else if (detection) {
377
+ config = yield* wrapParseErrors(postProcessConfig(detection.config, cwd));
378
+ framework = { name: detection.framework.name, slug: detection.framework.slug };
379
+ } else {
380
+ return yield* Effect3.fail(
381
+ new ConfigNotFoundError({
382
+ message: "No config file found and no framework detected.",
383
+ directory: cwd
384
+ })
385
+ );
386
+ }
387
+ for (const warning of config.warnings) {
388
+ yield* Effect3.logWarning(warning);
182
389
  }
390
+ if (config.errors.length > 0) {
391
+ return yield* Effect3.fail(
392
+ new ConfigValidationError({
393
+ message: "Config file has validation errors",
394
+ errors: config.errors
395
+ })
396
+ );
397
+ }
398
+ return { config, configPath: resolvedConfigPath, framework };
183
399
  });
184
- });
400
+ return { resolve };
401
+ })
402
+ }) {
185
403
  };
186
404
 
187
405
  // src/commands/debug/config/index.ts
188
- import { findConfig, parseConfig } from "@gigadrive/network-config";
189
- import { log as log2 } from "console";
190
- var config = (parent) => {
191
- parent.command("config").description("Provide debug info about the project configuration in the current directory").action(async () => {
192
- const configPath = findConfig(process.cwd());
193
- if (!configPath) {
194
- debug("No config file found");
195
- throw new Error("The current project folder does not have a valid config file.");
196
- }
197
- const config2 = await parseConfig(configPath, process.cwd());
198
- log2(`Config file found at: ${configPath}`);
199
- log2(JSON.stringify(config2, null, 2));
200
- });
201
- };
406
+ var debugConfigCommand = Command2.make(
407
+ "config",
408
+ {},
409
+ () => Effect4.gen(function* () {
410
+ const projectConfig = yield* ProjectConfigService;
411
+ const cwd = process.cwd();
412
+ 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));
415
+ }).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* () {
420
+ for (const e of err.errors) {
421
+ yield* Console2.error(e);
422
+ }
423
+ yield* Console2.error(err.message);
424
+ })
425
+ })
426
+ )
427
+ );
202
428
 
203
429
  // src/commands/debug/index.ts
204
- var debug2 = (program) => {
205
- const debug3 = program.command("debug").description("Provides various debugging utilities");
206
- config(debug3);
207
- };
430
+ var debugBase = Command3.make("debug", {}, () => Effect5.void);
431
+ var debugCommand = debugBase.pipe(Command3.withSubcommands([debugConfigCommand]));
208
432
 
209
- // src/env.ts
210
- import { createEnv } from "@t3-oss/env-core";
211
- import { config as loadEnv } from "dotenv";
212
- import fs3 from "fs";
213
- import path3 from "path";
214
- import { z } from "zod";
215
- (() => {
216
- const cwd = process.cwd();
217
- const nodeEnv = process.env.NODE_ENV;
218
- const candidates = [
219
- nodeEnv ? `.env.${nodeEnv}.local` : null,
220
- `.env.local`,
221
- nodeEnv ? `.env.${nodeEnv}` : null,
222
- `.env`
223
- ].filter(Boolean);
224
- for (const filename of candidates) {
225
- const fullPath = path3.join(cwd, filename);
226
- if (fs3.existsSync(fullPath)) {
227
- console.debug(`Loading environment variables from ${fullPath}`);
228
- loadEnv({ path: fullPath, override: false });
229
- }
230
- }
231
- })();
232
- var env = createEnv({
233
- server: {
234
- GIGADRIVE_NETWORK_OAUTH_ISSUER_URL: z.url().min(1).default("https://idp.gigadrive.de"),
235
- GIGADRIVE_NETWORK_OAUTH_CLIENT_ID: z.string().min(1).default("todo_add_client_id")
236
- },
237
- runtimeEnv: process.env,
238
- emptyStringAsUndefined: true
239
- });
240
-
241
- // src/config/oauth.ts
242
- var cachedConfig = null;
243
- async function getOAuthConfig() {
244
- if (cachedConfig) return cachedConfig;
245
- const issuerBase = env.GIGADRIVE_NETWORK_OAUTH_ISSUER_URL.replace(/\/$/, "");
246
- const controller = new AbortController();
247
- const timeout = setTimeout(() => controller.abort(), 8e3);
248
- let discovery = null;
249
- try {
250
- const discoveryUrl = `${issuerBase}/.well-known/openid-configuration`;
251
- const res = await fetch(discoveryUrl, { signal: controller.signal });
252
- if (res.ok) {
253
- discovery = await res.json();
254
- } else {
255
- discovery = null;
256
- }
257
- } catch {
258
- discovery = null;
259
- } finally {
260
- clearTimeout(timeout);
261
- }
262
- if (!discovery?.authorization_endpoint || !discovery?.token_endpoint || !discovery?.userinfo_endpoint) {
263
- throw new Error("OIDC discovery failed or returned incomplete endpoints");
264
- }
265
- const DEFAULT_SCOPE = "offline_access openid profile email";
266
- cachedConfig = {
267
- clientId: env.GIGADRIVE_NETWORK_OAUTH_CLIENT_ID,
268
- issuer: discovery.issuer ?? issuerBase,
269
- authorizeUrl: discovery.authorization_endpoint,
270
- tokenUrl: discovery.token_endpoint,
271
- scope: DEFAULT_SCOPE,
272
- userinfoUrl: discovery.userinfo_endpoint
273
- };
274
- return cachedConfig;
275
- }
433
+ // src/commands/login/index.ts
434
+ import { Command as Command4 } from "@effect/cli";
435
+ import { Console as Console4, Effect as Effect9 } from "effect";
276
436
 
277
- // src/auth/manager.ts
437
+ // src/services/auth.ts
438
+ import { Console as Console3, Effect as Effect8, Option as Option2 } from "effect";
278
439
  import * as crypto from "crypto";
279
- import * as fs4 from "fs";
280
440
  import * as http from "http";
281
- import * as os from "os";
282
- import * as path4 from "path";
283
- import { URL, URLSearchParams } from "url";
284
441
  import open from "open";
285
- function getAuthStoragePaths() {
286
- const homeDir = os.homedir();
287
- const authDir = path4.join(homeDir, ".gigadrive");
288
- const authFile = path4.join(authDir, "auth.json");
289
- return { directory: authDir, file: authFile };
290
- }
291
- async function ensureAuthDirectory() {
292
- const { directory } = getAuthStoragePaths();
293
- try {
294
- await fs4.promises.mkdir(directory, { recursive: true, mode: 448 });
295
- await fs4.promises.chmod(directory, 448);
296
- } catch (error2) {
297
- const errorMessage = error2 instanceof Error ? error2.message : "Unknown error";
298
- throw new Error(`Failed to create auth directory: ${errorMessage}`);
299
- }
300
- }
301
- function generatePKCE() {
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
+ });
487
+
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 };
559
+ })
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
+ });
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
+ })
605
+ });
606
+ const decoded = yield* Schema4.decodeUnknown(OpenIdDiscoveryDocument)(json).pipe(
607
+ Effect7.mapError(
608
+ () => new OAuthDiscoveryError({
609
+ message: "Invalid OIDC discovery document schema"
610
+ })
611
+ )
612
+ );
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
+ );
619
+ }
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;
631
+ });
632
+ return { getConfig };
633
+ })
634
+ }) {
635
+ };
636
+
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 = () => {
302
641
  const codeVerifier = base64URLEncode(crypto.randomBytes(64));
303
642
  const challengeBytes = crypto.createHash("sha256").update(codeVerifier, "utf8").digest();
304
643
  const codeChallenge = base64URLEncode(challengeBytes);
305
644
  return { codeVerifier, codeChallenge };
306
- }
307
- function base64URLEncode(buffer) {
308
- return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
309
- }
310
- var AuthManager = class {
311
- accessToken = null;
312
- tokenExpirationTime = 0;
313
- // Unix timestamp in milliseconds
314
- codeVerifier = null;
315
- state = null;
316
- constructor() {
317
- }
318
- /**
319
- * Retrieves the stored auth data from the local file.
320
- * @returns {Promise<StoredAuthData | null>} The stored auth data if found, otherwise null.
321
- * @private
322
- */
323
- async _loadStoredAuthData() {
324
- try {
325
- const { file } = getAuthStoragePaths();
326
- const data = await fs4.promises.readFile(file, "utf8");
327
- const authData = JSON.parse(data);
328
- if (authData.accessToken && authData.tokenExpirationTime && Date.now() < authData.tokenExpirationTime) {
329
- this.accessToken = authData.accessToken;
330
- this.tokenExpirationTime = authData.tokenExpirationTime;
645
+ };
646
+ var AuthService = class extends Effect8.Service()("AuthService", {
647
+ 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
686
+ })
687
+ );
331
688
  }
332
- return authData;
333
- } catch {
334
- return null;
335
- }
336
- }
337
- /**
338
- * Stores the auth data in the local file.
339
- * @param {StoredAuthData} authData The auth data to store.
340
- * @private
341
- */
342
- async _saveStoredAuthData(authData) {
343
- try {
344
- await ensureAuthDirectory();
345
- const { file } = getAuthStoragePaths();
346
- await fs4.promises.writeFile(file, JSON.stringify(authData, null, 2), { encoding: "utf8", mode: 384 });
347
- await fs4.promises.chmod(file, 384);
348
- console.debug("Auth data saved to local storage.");
349
- } catch (error2) {
350
- const errorMessage = error2 instanceof Error ? error2.message : "Unknown error";
351
- console.error("Error saving auth data to local storage:", errorMessage);
352
- }
353
- }
354
- /**
355
- * Deletes the stored auth data from the local file.
356
- */
357
- async logout() {
358
- this.accessToken = null;
359
- this.tokenExpirationTime = 0;
360
- try {
361
- const { file } = getAuthStoragePaths();
362
- await fs4.promises.unlink(file);
363
- log("Logged out. Auth data removed from local storage.");
364
- } catch {
365
- log("Logged out.");
366
- }
367
- }
368
- /**
369
- * Initiates the OAuth login flow with the IDP via a web portal.
370
- * Opens a browser for user authentication and listens for the redirect.
371
- * @returns {Promise<boolean>} True if login was successful, false otherwise.
372
- */
373
- async login() {
374
- await this.logout();
375
- const { codeVerifier, codeChallenge } = generatePKCE();
376
- this.codeVerifier = codeVerifier;
377
- this.state = crypto.randomBytes(16).toString("hex");
378
- const OAUTH_CONFIG = await getOAuthConfig();
379
- const loginState = this.state;
380
- if (!loginState) {
381
- throw new Error("Internal error: login state not initialized");
382
- }
383
- const server = http.createServer();
384
- let usedRedirectUri = "";
385
- const serverPromise = new Promise((resolve, reject) => {
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;
694
+ });
695
+ const startCallbackServer = (config, codeChallenge, state) => Effect8.async((resume) => {
696
+ const server = http.createServer();
386
697
  const redirectPath = "/callback";
387
698
  server.listen(0, "127.0.0.1", () => {
388
699
  const address = server.address();
389
700
  if (!address || typeof address !== "object") {
390
- reject(new Error("Failed to determine callback server address"));
701
+ server.close(() => {
702
+ resume(Effect8.fail(new LoginFlowError({ message: "Failed to determine callback server address" })));
703
+ });
391
704
  return;
392
705
  }
393
706
  const port = address.port;
394
- usedRedirectUri = `http://127.0.0.1:${port}${redirectPath}`;
395
- const authUrl = new URL(OAUTH_CONFIG.authorizeUrl);
396
- authUrl.searchParams.append("client_id", OAUTH_CONFIG.clientId);
397
- authUrl.searchParams.append("redirect_uri", usedRedirectUri);
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);
398
711
  authUrl.searchParams.append("response_type", "code");
399
- authUrl.searchParams.append("scope", OAUTH_CONFIG.scope);
712
+ authUrl.searchParams.append("scope", config.scope);
400
713
  authUrl.searchParams.append("code_challenge", codeChallenge);
401
714
  authUrl.searchParams.append("code_challenge_method", "S256");
402
- authUrl.searchParams.append("state", loginState);
403
- log(`Please open this URL in your browser to log in:
715
+ authUrl.searchParams.append("state", state);
716
+ Console3.log(`Please open this URL in your browser to log in:
404
717
  ${authUrl.toString()}
405
- `);
406
- log("Waiting for login callback...");
718
+ `).pipe(Effect8.runSync);
719
+ Console3.log("Waiting for login callback...").pipe(Effect8.runSync);
407
720
  void open(authUrl.toString());
408
- });
409
- server.on("request", (req, res) => {
410
- const base = usedRedirectUri || "http://127.0.0.1/callback";
411
- const reqUrl = new URL(req.url || "", base);
412
- if (reqUrl.pathname === "/callback") {
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
+ }
413
727
  const authCode = reqUrl.searchParams.get("code");
414
728
  const receivedState = reqUrl.searchParams.get("state");
415
- const error2 = reqUrl.searchParams.get("error");
729
+ const oauthError = reqUrl.searchParams.get("error");
416
730
  res.writeHead(200, { "Content-Type": "text/html", Connection: "close" });
417
- if (error2) {
418
- res.end("<h1>Login Failed!</h1><p>Error: " + error2 + "</p><p>You can close this tab.</p>");
419
- reject(new Error(`OAuth error: ${error2}`));
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>`);
420
734
  server.close();
735
+ resume(Effect8.fail(new LoginFlowError({ message: `OAuth error: ${oauthError}` })));
421
736
  return;
422
737
  }
423
- if (receivedState !== this.state) {
424
- res.end("<h1>Login Failed!</h1><p>State mismatch. Possible CSRF attack.</p><p>You can close this tab.</p>");
425
- reject(new Error("State mismatch"));
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
+ );
426
742
  server.close();
743
+ resume(Effect8.fail(new LoginFlowError({ message: "State mismatch" })));
427
744
  return;
428
745
  }
429
746
  if (authCode) {
430
747
  res.end("<h1>Login Successful!</h1><p>You can close this tab.</p>");
431
- resolve(authCode);
432
748
  server.close();
749
+ resume(Effect8.succeed({ authCode, redirectUri }));
433
750
  } else {
434
751
  res.end("<h1>Login Failed!</h1><p>No authorization code received.</p><p>You can close this tab.</p>");
435
- reject(new Error("No authorization code received"));
436
752
  server.close();
753
+ resume(Effect8.fail(new LoginFlowError({ message: "No authorization code received" })));
437
754
  }
438
- } else {
439
- res.writeHead(404).end("Not Found");
440
- }
755
+ });
756
+ server.on("error", (err) => {
757
+ server.close();
758
+ resume(Effect8.fail(new LoginFlowError({ message: `Local server error: ${err.message}` })));
759
+ });
441
760
  });
442
- server.on("error", (err) => {
443
- console.error("Local server error:", err);
444
- reject(err);
761
+ });
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
445
780
  });
781
+ yield* Effect8.log("Login successful");
782
+ return true;
446
783
  });
447
- try {
448
- const authCode = await serverPromise;
449
- const tokenResponse = await fetch(OAUTH_CONFIG.tokenUrl, {
450
- method: "POST",
451
- headers: {
452
- "Content-Type": "application/x-www-form-urlencoded"
453
- },
454
- body: new URLSearchParams({
455
- grant_type: "authorization_code",
456
- client_id: OAUTH_CONFIG.clientId,
457
- code: authCode,
458
- ...usedRedirectUri ? { redirect_uri: usedRedirectUri } : {},
459
- code_verifier: this.codeVerifier || ""
460
- // Use the stored codeVerifier
461
- }).toString()
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 })))
790
+ );
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
+ })
462
808
  });
463
- if (!tokenResponse.ok) {
464
- const errorData = await tokenResponse.json().catch(() => null);
465
- throw new Error(
466
- `Token exchange failed: ${tokenResponse.status} ${tokenResponse.statusText} - ${errorData ? JSON.stringify(errorData) : "Unknown error"}`
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
+ })
467
828
  );
468
829
  }
469
- const tokens = await tokenResponse.json();
470
- this.accessToken = tokens.access_token;
471
- this.tokenExpirationTime = Date.now() + (tokens.expires_in || 3600) * 1e3;
472
- if (tokens.refresh_token) {
473
- await this._saveStoredAuthData({
474
- refreshToken: tokens.refresh_token,
475
- accessToken: this.accessToken,
476
- tokenExpirationTime: this.tokenExpirationTime
477
- });
478
- success("Login successful!");
479
- return true;
480
- } else {
481
- console.error("Error: No refresh token received from Gigadrive IDP.");
482
- return false;
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
+ });
845
+ const getAccessToken = Effect8.gen(function* () {
846
+ if (accessToken && Date.now() < tokenExpirationTime) {
847
+ return accessToken;
483
848
  }
484
- } catch (error2) {
485
- const errorMessage = error2 instanceof Error ? error2.message : "Unknown error";
486
- console.error("Login process failed:", errorMessage);
487
- this.accessToken = null;
488
- this.tokenExpirationTime = 0;
489
- return false;
490
- } finally {
491
- this.codeVerifier = null;
492
- this.state = null;
493
- }
494
- }
495
- /**
496
- * Refreshes the access token using the stored refresh token.
497
- * This method implicitly loads the refresh token from local storage.
498
- * @returns {Promise<boolean>} True if refresh was successful, false otherwise.
499
- */
500
- async refreshAccessToken() {
501
- const storedData = await this._loadStoredAuthData();
502
- const OAUTH_CONFIG = await getOAuthConfig();
503
- if (!storedData?.refreshToken) {
504
- error("No refresh token available for refresh. Please log in.");
505
- return false;
506
- }
507
- try {
508
- const tokenResponse = await fetch(OAUTH_CONFIG.tokenUrl, {
509
- method: "POST",
510
- headers: {
511
- "Content-Type": "application/x-www-form-urlencoded"
849
+ const stored = yield* storage.load.pipe(
850
+ Effect8.catchAll(() => Effect8.succeed(Option2.none()))
851
+ );
852
+ Option2.match(stored, {
853
+ onNone: () => {
512
854
  },
513
- body: new URLSearchParams({
514
- grant_type: "refresh_token",
515
- client_id: OAUTH_CONFIG.clientId,
516
- refresh_token: storedData.refreshToken
517
- }).toString()
518
- });
519
- if (!tokenResponse.ok) {
520
- const errorData = await tokenResponse.json().catch(() => null);
521
- if (tokenResponse.status === 400 && errorData && (errorData.error === "oauth_token_not_found" || errorData.errors?.[0]?.code === "token_invalid" || errorData.errors?.[0]?.code === "token_expired" || errorData.errors?.[0]?.code === "token_revoked")) {
522
- console.error("Refresh token is invalid or expired. Please log in again.");
523
- await this.logout();
524
- return false;
855
+ onSome: (data) => {
856
+ if (data.accessToken && data.tokenExpirationTime && Date.now() < data.tokenExpirationTime) {
857
+ accessToken = data.accessToken;
858
+ tokenExpirationTime = data.tokenExpirationTime;
859
+ }
525
860
  }
526
- throw new Error(
527
- `Token refresh failed: ${tokenResponse.status} ${tokenResponse.statusText} - ${errorData ? JSON.stringify(errorData) : "Unknown error"}`
528
- );
861
+ });
862
+ if (accessToken && Date.now() < tokenExpirationTime) {
863
+ return accessToken;
529
864
  }
530
- const tokens = await tokenResponse.json();
531
- this.accessToken = tokens.access_token;
532
- this.tokenExpirationTime = Date.now() + (tokens.expires_in || 3600) * 1e3;
533
- const newRefreshToken = tokens.refresh_token || storedData.refreshToken;
534
- if (tokens.refresh_token && tokens.refresh_token !== storedData.refreshToken) {
535
- console.debug("New refresh token received and updated.");
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
+ );
536
873
  }
537
- await this._saveStoredAuthData({
538
- refreshToken: newRefreshToken,
539
- accessToken: this.accessToken,
540
- tokenExpirationTime: this.tokenExpirationTime
874
+ return accessToken;
875
+ });
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)}`
888
+ })
541
889
  });
542
- console.debug("Access token refreshed successfully!");
543
- return true;
544
- } catch (error2) {
545
- const errorMessage = error2 instanceof Error ? error2.message : "Unknown error";
546
- console.error("Error refreshing access token:", errorMessage);
547
- this.accessToken = null;
548
- this.tokenExpirationTime = 0;
549
- return false;
550
- }
551
- }
552
- /**
553
- * Provides the current valid access token. If the in-memory token is expired,
554
- * it attempts to refresh it using the stored refresh token.
555
- * @returns {Promise<string | null>} The valid access token or null if no session.
556
- */
557
- async getAccessToken() {
558
- if (this.accessToken && Date.now() < this.tokenExpirationTime) {
559
- return this.accessToken;
560
- }
561
- console.debug("Access token expired or not available. Attempting to refresh...");
562
- if (await this.refreshAccessToken()) {
563
- return this.accessToken;
564
- }
565
- error("No valid access token available. Please log in first.");
566
- return null;
567
- }
568
- /**
569
- * Fetches the current user's information from the configured userinfo endpoint.
570
- * Attempts a single token refresh and retry on 401/403 responses.
571
- */
572
- async getUserInfo() {
573
- const OAUTH_CONFIG = await getOAuthConfig();
574
- const currentToken = await this.getAccessToken();
575
- if (!currentToken) {
576
- error("Not authenticated. Please log in.");
577
- return null;
578
- }
579
- const fetchUserInfo = async (token) => fetch(OAUTH_CONFIG.userinfoUrl, {
580
- headers: {
581
- Authorization: `Bearer ${token}`,
582
- Accept: "application/json"
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);
583
899
  }
584
- });
585
- let response = await fetchUserInfo(currentToken);
586
- if (response.status === 401 || response.status === 403) {
587
- const refreshed = await this.refreshAccessToken();
588
- if (!refreshed || !this.accessToken) {
589
- return null;
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
909
+ })
910
+ );
590
911
  }
591
- response = await fetchUserInfo(this.accessToken);
592
- }
593
- if (!response.ok) {
594
- const body = await response.text().catch(() => "");
595
- throw new Error(
596
- `Failed to fetch user info: ${response.status} ${response.statusText}${body ? ` - ${body}` : ""}`
597
- );
598
- }
599
- const data = await response.json();
600
- return data;
601
- }
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
+ }) {
602
930
  };
603
- var authManager = new AuthManager();
604
931
 
605
932
  // src/commands/login/index.ts
606
- var login = (program) => {
607
- program.command("login").description("Login to the platform").action(async () => {
608
- log("Initiating login...");
609
- const result = await authManager.login();
610
- if (result) {
611
- try {
612
- const info = await authManager.getUserInfo() ?? {};
613
- const isNonEmptyString = (value) => typeof value === "string" && value.trim() !== "";
614
- const nameField = info.name;
615
- const givenNameField = info.given_name;
616
- const familyNameField = info.family_name;
617
- const preferredUsernameField = info.preferred_username;
618
- const emailField = info.email;
619
- const inferredName = isNonEmptyString(nameField) && nameField || (isNonEmptyString(givenNameField) && isNonEmptyString(familyNameField) ? `${givenNameField} ${familyNameField}` : void 0) || isNonEmptyString(preferredUsernameField) && preferredUsernameField || isNonEmptyString(emailField) && emailField || "your account";
620
- success(`You are now logged in as ${inferredName}.`);
621
- } catch {
622
- success("You are now logged in.");
623
- }
624
- } else {
625
- error("Login failed. Please try again.");
626
- }
627
- });
628
- };
933
+ var loginCommand = Command4.make(
934
+ "login",
935
+ {},
936
+ () => Effect9.gen(function* () {
937
+ yield* Console4.log("Initiating login...");
938
+ const auth = yield* AuthService;
939
+ yield* auth.login;
940
+ const info = yield* auth.getUserInfo.pipe(Effect9.catchAll(() => Effect9.succeed({})));
941
+ const name = auth.inferUserName(info);
942
+ yield* Console4.log(`You are now logged in as ${name}.`);
943
+ }).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}`)
949
+ })
950
+ )
951
+ );
952
+
953
+ // src/commands/platform/index.ts
954
+ import { Command as Command6 } from "@effect/cli";
955
+ import { Effect as Effect13 } from "effect";
629
956
 
630
957
  // src/commands/platform/deploy/index.ts
631
- import { formatFileSize, objectToQueryString } from "@gigadrive/commons";
632
- import { findConfig as findConfig2, parseConfig as parseConfig2 } from "@gigadrive/network-config";
633
- import fs6 from "fs";
634
- import os2 from "os";
635
- import path6 from "path";
958
+ import { Command as Command5 } from "@effect/cli";
959
+ import { FileSystem as FileSystem5, Path as Path5 } from "@effect/platform";
960
+ import { formatFileSize } from "@gigadrive/commons";
961
+ import { Console as Console5, Duration, Effect as Effect12, Schedule, Stream } from "effect";
962
+ import * as os2 from "os";
636
963
 
637
- // src/util/zip.ts
964
+ // src/services/archive.ts
965
+ import { FileSystem as FileSystem4, Path as Path4 } from "@effect/platform";
638
966
  import { getFilesForPattern } from "@gigadrive/build-utils";
639
967
  import archiver from "archiver";
640
- import fs5 from "fs";
641
- import fsPromises from "fs/promises";
968
+ import { Effect as Effect10 } from "effect";
642
969
  import ignore from "ignore";
643
- import path5 from "path";
644
- var ignoreFileNames = [
645
- /*'.gitignore', */
646
- ".gigadriveignore",
647
- ".vercelignore",
648
- ".dockerignore",
649
- ".nowignore"
650
- ];
651
- async function readIgnoreFile(ignorePath) {
652
- try {
653
- await fsPromises.access(ignorePath);
654
- const content = await fsPromises.readFile(ignorePath, "utf8");
655
- return content.split("\n").filter((line) => line.trim() !== "");
656
- } catch {
657
- return [];
658
- }
659
- }
660
- async function collectIgnorePatterns(dir, baseDir, ignoreRules) {
661
- const items = await fsPromises.readdir(dir);
970
+ import * as nodeFs from "fs";
971
+ import * as nodeFsPromises from "fs/promises";
972
+ var IGNORE_FILE_NAMES = [".gigadriveignore", ".vercelignore", ".dockerignore", ".nowignore"];
973
+ 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([]))
976
+ );
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}` })));
662
979
  for (const item of items) {
663
- const fullPath = path5.join(dir, item);
664
- const relativePath = path5.relative(baseDir, fullPath);
665
- const stat = await fsPromises.lstat(fullPath);
980
+ const fullPath = pathService.join(dir, item);
981
+ const relativePath = pathService.relative(baseDir, fullPath);
982
+ const stat = yield* Effect10.tryPromise({
983
+ try: () => nodeFsPromises.lstat(fullPath),
984
+ catch: () => new ArchiveCreateError({ message: `Failed to stat: ${fullPath}` })
985
+ });
666
986
  if (stat.isDirectory()) {
667
- if (ignoreRules.ignores(`${relativePath}/`)) {
668
- continue;
669
- }
670
- for (const ignoreFileName of ignoreFileNames) {
671
- const ignoreFilePath = path5.join(fullPath, ignoreFileName);
672
- try {
673
- await fsPromises.access(ignoreFilePath);
674
- const patterns = (await readIgnoreFile(ignoreFilePath)).map((pattern) => {
675
- return pattern.startsWith("!") ? `!${path5.join(relativePath, pattern.slice(1))}` : path5.join(relativePath, pattern);
676
- });
677
- ignoreRules.add(patterns);
678
- } catch {
987
+ if (ignoreRules.ignores(`${relativePath}/`)) continue;
988
+ for (const ignoreFileName of IGNORE_FILE_NAMES) {
989
+ const ignoreFilePath = pathService.join(fullPath, ignoreFileName);
990
+ const patterns = yield* readIgnoreFile(fs, ignoreFilePath);
991
+ if (patterns.length > 0) {
992
+ ignoreRules.add(
993
+ patterns.map(
994
+ (pattern) => pattern.startsWith("!") ? `!${pathService.join(relativePath, pattern.slice(1))}` : pathService.join(relativePath, pattern)
995
+ )
996
+ );
679
997
  }
680
998
  }
681
- await collectIgnorePatterns(fullPath, baseDir, ignoreRules);
999
+ yield* collectIgnorePatterns(fs, pathService, fullPath, baseDir, ignoreRules);
682
1000
  }
683
1001
  }
684
- }
685
- async function initializeIgnoreRules(baseDir) {
1002
+ }).pipe(Effect10.catchAll(() => Effect10.void));
1003
+ var initializeIgnoreRules = (fs, pathService, baseDir) => Effect10.gen(function* () {
686
1004
  const ignoreRules = ignore();
687
1005
  ignoreRules.add([".git/", "**/.DS_Store", "**/Thumbs.db"]);
688
- for (const ignoreFileName of ignoreFileNames) {
689
- const ignoreFilePath = path5.join(baseDir, ignoreFileName);
690
- try {
691
- await fsPromises.access(ignoreFilePath);
692
- const patterns = (await readIgnoreFile(ignoreFilePath)).map((pattern) => {
693
- return pattern.startsWith("!") ? `!${pattern.slice(1)}` : pattern;
694
- });
695
- ignoreRules.add(patterns);
696
- } catch {
1006
+ for (const ignoreFileName of IGNORE_FILE_NAMES) {
1007
+ const ignoreFilePath = pathService.join(baseDir, ignoreFileName);
1008
+ const patterns = yield* readIgnoreFile(fs, ignoreFilePath);
1009
+ if (patterns.length > 0) {
1010
+ ignoreRules.add(
1011
+ patterns.map((pattern) => pattern.startsWith("!") ? `!${pattern.slice(1)}` : pattern)
1012
+ );
697
1013
  }
698
1014
  }
699
- await collectIgnorePatterns(baseDir, baseDir, ignoreRules);
1015
+ yield* collectIgnorePatterns(fs, pathService, baseDir, baseDir, ignoreRules);
700
1016
  return ignoreRules;
701
- }
702
- async function getFilesToInclude(dir, ignoreRules, whitelist) {
1017
+ });
1018
+ var getFilesToInclude = (fs, pathService, dir, ignoreRules) => Effect10.gen(function* () {
703
1019
  const filesToInclude = [];
704
- async function walkDir(currentDir) {
705
- const items = await fsPromises.readdir(currentDir);
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}` })));
706
1022
  for (const item of items) {
707
- const fullPath = path5.join(currentDir, item);
708
- const relativePath = path5.relative(dir, fullPath);
709
- const stat = await fsPromises.lstat(fullPath);
1023
+ const fullPath = pathService.join(currentDir, item);
1024
+ const relativePath = pathService.relative(dir, fullPath);
1025
+ const stat = yield* Effect10.tryPromise({
1026
+ try: () => nodeFsPromises.lstat(fullPath),
1027
+ catch: () => new ArchiveCreateError({ message: `Failed to stat: ${fullPath}` })
1028
+ });
710
1029
  if (stat.isDirectory()) {
711
- if (ignoreRules.ignores(`${relativePath}/`)) {
712
- continue;
1030
+ if (!ignoreRules.ignores(`${relativePath}/`)) {
1031
+ yield* walkDir(fullPath);
713
1032
  }
714
- await walkDir(fullPath);
715
1033
  } else {
716
1034
  if (!ignoreRules.ignores(relativePath)) {
717
1035
  filesToInclude.push(fullPath);
718
- } else if (ignoreRules.ignores(relativePath) && ignoreRules.test(relativePath).unignored) {
719
- filesToInclude.push(fullPath);
720
1036
  }
721
1037
  }
722
1038
  }
723
- }
724
- await walkDir(dir);
1039
+ }).pipe(Effect10.catchAll(() => Effect10.void));
1040
+ yield* walkDir(dir);
725
1041
  return filesToInclude;
726
- }
727
- async function createZipArchive(inputDir, outputFile, options = {}) {
728
- const ignoreRules = options.useIgnoreFiles !== false ? await initializeIgnoreRules(inputDir) : ignore();
729
- const filesToInclude = options.whitelist ? await Promise.all(options.whitelist.map((file) => getFilesForPattern(inputDir, file))) : await getFilesToInclude(inputDir, ignoreRules);
730
- const output = fs5.createWriteStream(outputFile);
731
- const archive = archiver("zip", { zlib: { level: 9 } });
732
- return new Promise((resolve, reject) => {
733
- output.on("close", () => resolve());
734
- archive.on("error", reject);
735
- archive.pipe(output);
736
- for (const file of filesToInclude.flat()) {
737
- archive.file(file, { name: path5.relative(inputDir, file) });
738
- }
739
- void archive.finalize();
740
- });
741
- }
742
-
743
- // src/commands/platform/deploy/index.ts
744
- var deploy = (parent) => {
745
- parent.command("deploy").description("Deploy the current project").action(async () => {
746
- debug("Starting deploy command");
747
- const configPath = findConfig2(process.cwd());
748
- if (!configPath) {
749
- debug("No config file found");
750
- throw new Error("The current project folder does not have a valid config file.");
751
- }
752
- debug(`Found config at: ${configPath}`);
753
- const config2 = await parseConfig2(configPath, process.cwd());
754
- debug("Parsed config", config2);
755
- if (config2.warnings.length > 0) {
756
- debug(`Found ${config2.warnings.length} warnings in config`);
757
- config2.warnings.forEach((message) => {
758
- warn(message);
1042
+ });
1043
+ var ArchiveService = class extends Effect10.Service()("ArchiveService", {
1044
+ 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 });
1052
+ const ignoreRules = options.useIgnoreFiles !== false ? yield* initializeIgnoreRules(fs, pathService, inputDir) : ignore();
1053
+ const filesToInclude = options.whitelist ? yield* Effect10.tryPromise({
1054
+ try: () => Promise.all(
1055
+ options.whitelist.map((file) => getFilesForPattern(file, inputDir, options.excludeFiles ?? []))
1056
+ ),
1057
+ catch: (error) => new ArchiveCreateError({
1058
+ message: "Failed to resolve whitelist patterns",
1059
+ cause: error instanceof Error ? error.message : String(error)
1060
+ })
1061
+ }) : yield* Effect10.gen(function* () {
1062
+ if (options.excludeFiles && options.excludeFiles.length > 0) {
1063
+ ignoreRules.add(options.excludeFiles);
1064
+ }
1065
+ return yield* getFilesToInclude(fs, pathService, inputDir, ignoreRules).pipe(
1066
+ Effect10.map((files) => [files])
1067
+ );
759
1068
  });
760
- }
761
- if (config2.errors.length > 0) {
762
- debug(`Found ${config2.errors.length} errors in config`);
763
- config2.errors.forEach((message) => {
764
- error(message);
1069
+ const archiveExists = yield* fs.exists(outputFile).pipe(Effect10.catchAll(() => Effect10.succeed(false)));
1070
+ if (archiveExists) {
1071
+ yield* fs.remove(outputFile).pipe(
1072
+ Effect10.mapError(
1073
+ (error) => new ArchiveCreateError({
1074
+ message: `Failed to remove existing archive: ${String(error)}`
1075
+ })
1076
+ )
1077
+ );
1078
+ }
1079
+ yield* Effect10.tryPromise({
1080
+ try: () => new Promise((resolve, reject) => {
1081
+ const output = nodeFs.createWriteStream(outputFile);
1082
+ const archive = archiver("zip", { zlib: { level: 9 } });
1083
+ output.on("close", () => resolve());
1084
+ output.on("error", reject);
1085
+ archive.on("error", reject);
1086
+ archive.pipe(output);
1087
+ for (const file of filesToInclude.flat()) {
1088
+ archive.file(file, { name: pathService.relative(inputDir, file) });
1089
+ }
1090
+ void archive.finalize();
1091
+ }),
1092
+ catch: (error) => new ArchiveCreateError({
1093
+ message: "Failed to create zip archive",
1094
+ cause: error instanceof Error ? error.message : String(error)
1095
+ })
765
1096
  });
766
- throw new Error("The current project folder does not have a valid config file.");
767
- }
768
- debug("Creating deployment");
769
- const deploymentId = await createDeployment({
770
- applicationId: "test"
771
- // TODO
1097
+ const fileInfo = yield* fs.stat(outputFile).pipe(
1098
+ Effect10.mapError(
1099
+ (error) => new ArchiveCreateError({
1100
+ message: `Failed to stat archive: ${String(error)}`
1101
+ })
1102
+ )
1103
+ );
1104
+ const size = Number(fileInfo.size);
1105
+ yield* Effect10.log("Archive created", { size });
1106
+ return { path: outputFile, size };
772
1107
  });
773
- debug(`Deployment ID: ${deploymentId}`);
774
- debug("Starting deployment process");
775
- await handleDeployment(deploymentId, config2);
776
- });
1108
+ return { createZipArchive };
1109
+ })
1110
+ }) {
777
1111
  };
778
- var handleDeployment = async (deploymentId, config2) => {
779
- debug("Deployment process initialized");
780
- let status = "PENDING";
781
- const logs = [];
782
- addLog("Deployment ID: " + deploymentId, "INFO", logs);
783
- try {
784
- debug("Starting upload process");
785
- await uploadArchive(deploymentId, logs, config2);
786
- debug("Upload completed successfully");
787
- addLog("Upload complete.", "INFO", logs);
788
- addLog("The deployment pipeline is now being provisioned. This may take a few seconds.", "INFO", logs);
789
- } catch (err) {
790
- debug(`Upload failed with error: ${err.message}`);
791
- addLog(`Upload failed: ${err.message}`, "ERROR", logs);
792
- status = "FAILED";
793
- error("The deployment failed. Please check the logs for more information.");
794
- process.exit(1);
795
- }
796
- const statusInterval = setInterval(() => {
797
- void (async () => {
798
- try {
799
- debug(`Fetching deployment status for ${deploymentId}`);
800
- const newStatus = await getDeploymentStatus(deploymentId);
801
- debug(`Current status: ${status}, New status: ${newStatus}`);
802
- if (newStatus === "ACTIVE" || newStatus === "FAILED") {
803
- debug(`Final status reached: ${newStatus}, clearing interval`);
804
- clearInterval(statusInterval);
805
- clearInterval(logsInterval);
806
- if (newStatus === "ACTIVE") {
807
- success(`Deployed to https://${deploymentId}.gigadrivedev.com`);
808
- } else {
809
- error("The deployment failed. Please check the logs for more information.");
810
- }
811
- return;
812
- }
813
- if (newStatus !== status) {
814
- debug(`Updating status from ${status} to ${newStatus}`);
815
- status = newStatus;
816
- spinner(`Status: ${status.toLowerCase().replace(/^\w/, (c) => c.toUpperCase())}`);
817
- }
818
- } catch (err) {
819
- debug(`Error fetching status: ${err.message}`);
1112
+
1113
+ // 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", {
1137
+ 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
+ }),
1157
+ catch: (error) => new DeploymentCreateError({
1158
+ message: `Failed to create deployment: ${error instanceof Error ? error.message : String(error)}`
1159
+ })
1160
+ });
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
+ );
820
1175
  }
821
- })();
822
- }, 1e3);
823
- const logsInterval = setInterval(() => {
824
- void (async () => {
825
- try {
826
- debug(`Fetching logs for ${deploymentId}, current log count: ${logs.length}`);
827
- const newLogs = await getLogs(deploymentId, {
828
- offset: logs.length,
829
- limit: 100,
830
- "createdAt[gt]": logs[logs.length - 1]?.createdAt
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;
1185
+ });
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
+ }),
1194
+ catch: (error) => new UploadStartError({
1195
+ message: `Failed to start multipart upload: ${error instanceof Error ? error.message : String(error)}`
1196
+ })
1197
+ });
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" })
831
1202
  });
832
- debug(`Fetched ${newLogs.items.length} new logs`);
833
- for (const logEntry of newLogs.items) {
834
- logs.push(logEntry);
835
- if (logEntry.type === "ERROR") {
836
- error(logEntry.message);
837
- } else if (logEntry.type === "WARN") {
838
- warn(logEntry.message);
839
- } else {
840
- log(logEntry.message);
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;
1215
+ });
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 }
841
1224
  }
842
- }
843
- } catch (err) {
844
- debug(`Error fetching logs: ${err.message}`);
1225
+ ),
1226
+ catch: (error) => new PresignedUrlError({
1227
+ message: `Failed to get presigned URL: ${error instanceof Error ? error.message : String(error)}`,
1228
+ partNumber
1229
+ })
1230
+ });
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
+ );
845
1239
  }
846
- })();
847
- }, 1e3);
848
- spinner(`Status: ${status.toLowerCase().replace(/^\w/, (c) => c.toUpperCase())}`);
849
- };
850
- var addLog = (message, type, logs) => {
851
- debug(`Adding log: [${type}] ${message}`);
852
- logs.push({
853
- id: String(logs.length),
854
- message,
855
- type,
856
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
857
- });
858
- if (type === "ERROR") {
859
- error(message);
860
- } else if (type === "WARN") {
861
- warn(message);
862
- } else {
863
- log(message);
864
- }
865
- };
866
- var uploadArchive = async (deploymentId, logs, config2) => {
867
- debug("Starting uploadArchive");
868
- addLog("Creating archive...", "INFO", logs);
869
- const archivePath = path6.join(process.env.TEMP || os2.tmpdir(), `project-${Date.now()}.zip`);
870
- debug(`Archive path: ${archivePath}`);
871
- if (fs6.existsSync(archivePath)) {
872
- debug(`Archive already exists, removing: ${archivePath}`);
873
- fs6.unlinkSync(archivePath);
874
- }
875
- debug("Creating zip archive");
876
- await createZipArchive(config2.userArchive?.rootOverwrite || process.cwd(), archivePath, {
877
- whitelist: config2.userArchive?.fileWhitelist,
878
- useIgnoreFiles: false,
879
- useManagedIgnore: false
880
- });
881
- const fileStats = fs6.statSync(archivePath);
882
- const fileSize = fileStats.size;
883
- debug(`Archive created, size: ${fileSize} bytes (${formatFileSize(fileSize)})`);
884
- addLog(`Archive created (${formatFileSize(fileSize)})`, "INFO", logs);
885
- addLog("Uploading archive...", "INFO", logs);
886
- debug(`Starting multipart upload for deployment: ${deploymentId}`);
887
- const uploadId = await startMultipartUpload(deploymentId);
888
- debug(`Upload ID: ${uploadId}`);
889
- debug("Uploading archive...");
890
- const fileChunkSize = 10 * 1024 * 1024;
891
- const numChunks = Math.ceil(fileSize / fileChunkSize);
892
- debug(`File size: ${fileSize}, chunk size: ${fileChunkSize}, number of chunks: ${numChunks}`);
893
- const promises2 = [];
894
- let start, end;
895
- for (let index = 1; index < numChunks + 1; index++) {
896
- start = (index - 1) * fileChunkSize;
897
- end = index * fileChunkSize;
898
- debug(`Processing chunk ${index}/${numChunks}, start: ${start}, end: ${end}`);
899
- const presignedUrl = await getPresignedUrl(deploymentId, uploadId, index);
900
- debug("Presigned URL for part", index, presignedUrl);
901
- const promise = new Promise((resolve, reject) => {
902
- void (async () => {
903
- try {
904
- debug(`Reading chunk ${index} data`);
905
- const readStream = fs6.createReadStream(archivePath, {
906
- start,
907
- end: index < numChunks ? end - 1 : void 0
908
- });
909
- const chunks = [];
910
- readStream.on("data", (chunk) => {
911
- chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
912
- });
913
- await new Promise((resolveRead) => {
914
- readStream.on("end", () => resolveRead());
915
- readStream.on("error", reject);
916
- });
917
- const buffer = Buffer.concat(chunks);
918
- debug(`Uploading chunk ${index} to presigned URL (${buffer.length} bytes)`);
919
- const response = await fetch(presignedUrl, {
920
- method: "PUT",
921
- headers: {
922
- "Content-Type": "application/zip",
923
- "Content-Length": String(buffer.length)
924
- },
925
- body: buffer
926
- });
927
- resolve(response);
928
- } catch (err) {
929
- reject(err instanceof Error ? err : new Error(String(err)));
930
- }
931
- })();
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;
932
1248
  });
933
- promises2.push(promise);
934
- }
935
- const finishedParts = [];
936
- try {
937
- debug(`Waiting for all ${promises2.length} upload promises to complete`);
938
- const responses = await Promise.all(promises2);
939
- debug("All upload promises completed");
940
- for (let index = 0; index < responses.length; index++) {
941
- const res = responses[index];
942
- debug(`Checking response for part ${index + 1}, status: ${res.status}`);
943
- if (!res.ok) {
944
- debug(`Failed to upload part ${index + 1}: ${res.statusText} ${await res.text()}`);
945
- throw new Error(`Failed to upload part ${index + 1}: ${res.statusText}`);
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
+ }),
1261
+ catch: (error) => new UploadPartError({
1262
+ message: `Failed to upload part ${partNumber}: ${error instanceof Error ? error.message : String(error)}`,
1263
+ partNumber
1264
+ })
1265
+ });
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
+ );
946
1273
  }
947
- const etag = res.headers.get("ETag");
1274
+ const etag = response.headers.get("ETag");
948
1275
  if (!etag) {
949
- debug(`Failed to get ETag for part ${index + 1}`);
950
- throw new Error(`Failed to get ETag for part ${index + 1}`);
1276
+ return yield* Effect11.fail(
1277
+ new UploadPartError({ message: `No ETag received for part ${partNumber}`, partNumber })
1278
+ );
951
1279
  }
952
- debug(`Part ${index + 1} uploaded successfully, ETag: ${etag}`);
953
- finishedParts.push({
954
- partNumber: index + 1,
955
- etag
1280
+ return { partNumber, etag };
1281
+ });
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
+ }),
1292
+ catch: (error) => new UploadCompleteError({
1293
+ message: `Failed to complete upload: ${error instanceof Error ? error.message : String(error)}`
1294
+ })
956
1295
  });
957
- }
958
- debug(`Completing upload with ${finishedParts.length} parts`);
959
- await completeUpload(deploymentId, uploadId, finishedParts);
960
- debug("Upload completed successfully");
961
- } catch (err) {
962
- const error2 = err;
963
- debug(`Upload error: ${error2.message}`);
964
- addLog(`Upload error: ${error2.message}`, "ERROR", logs);
965
- throw error2;
966
- }
967
- };
968
- var createDeployment = async ({ applicationId }) => {
969
- debug(`Creating deployment for application: ${applicationId}`);
970
- const res = await fetch(`http://localhost:3000/${applicationId}/deployments`, {
971
- method: "POST",
972
- headers: {
973
- "Content-Type": "application/json"
974
- },
975
- body: JSON.stringify({}),
976
- duplex: "half"
977
- });
978
- if (!res.ok) {
979
- const errorText = await res.text();
980
- debug(`Failed to create deployment: ${errorText}`);
981
- throw new Error("Failed to create deployment: " + errorText);
982
- }
983
- const deployment = await res.json();
984
- debug(`Deployment created with ID: ${deployment.id}`);
985
- return deployment.id;
986
- };
987
- var startMultipartUpload = async (deploymentId) => {
988
- debug(`Starting multipart upload for deployment: ${deploymentId}`);
989
- const res = await fetch(`http://localhost:3000/deployments/${deploymentId}/pre-signed-url/start`, {
990
- method: "GET",
991
- headers: {
992
- "Content-Type": "application/json"
993
- }
994
- });
995
- if (!res.ok) {
996
- const errorText = await res.text();
997
- debug(`Failed to start multipart upload: ${errorText}`);
998
- throw new Error("Failed to start multipart upload: " + errorText);
999
- }
1000
- const upload = await res.json();
1001
- debug(`Multipart upload started with ID: ${upload.uploadId}`);
1002
- return upload.uploadId;
1003
- };
1004
- var getPresignedUrl = async (deploymentId, uploadId, partNumber) => {
1005
- debug(`Getting presigned URL for deployment: ${deploymentId}, upload: ${uploadId}, part: ${partNumber}`);
1006
- const res = await fetch(
1007
- `http://localhost:3000/deployments/${deploymentId}/pre-signed-url/part?uploadId=${uploadId}&partNumber=${partNumber}`,
1008
- {
1009
- method: "GET",
1010
- headers: {
1011
- "Content-Type": "application/json"
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}` }));
1012
1302
  }
1013
- }
1014
- );
1015
- if (!res.ok) {
1016
- const errorText = await res.text();
1017
- debug(`Failed to get pre-signed URL: ${errorText}`);
1018
- throw new Error("Failed to get pre-signed URL: " + errorText);
1019
- }
1020
- const upload = await res.json();
1021
- debug(`Got presigned URL for part ${partNumber}`);
1022
- return upload.url;
1303
+ yield* Effect11.log("Upload completed successfully");
1304
+ });
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
+ }),
1312
+ catch: (error) => new DeploymentStatusError({
1313
+ message: `Failed to get deployment status: ${error instanceof Error ? error.message : String(error)}`
1314
+ })
1315
+ });
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;
1331
+ });
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
+ }),
1339
+ catch: (error) => new DeploymentLogsFetchError({
1340
+ message: `Failed to get logs: ${error instanceof Error ? error.message : String(error)}`
1341
+ })
1342
+ });
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" })
1353
+ });
1354
+ const decoded = yield* Schema5.decodeUnknown(DeploymentLogPageResponse)(json).pipe(
1355
+ Effect11.mapError(() => new DeploymentLogsFetchError({ message: "Invalid logs response schema" }))
1356
+ );
1357
+ return decoded;
1358
+ });
1359
+ return {
1360
+ createDeployment,
1361
+ startMultipartUpload,
1362
+ getPresignedUrl,
1363
+ uploadPart,
1364
+ completeUpload,
1365
+ getDeploymentStatus,
1366
+ getLogs
1367
+ };
1368
+ })
1369
+ }) {
1023
1370
  };
1024
- var completeUpload = async (deploymentId, uploadId, parts) => {
1025
- debug(`Completing upload for deployment: ${deploymentId}, upload: ${uploadId}, parts: ${parts.length}`);
1026
- const res = await fetch(`http://localhost:3000/deployments/${deploymentId}/pre-signed-url/complete`, {
1027
- method: "POST",
1028
- headers: {
1029
- "Content-Type": "application/json"
1030
- },
1031
- body: JSON.stringify({
1032
- uploadId,
1033
- parts
1371
+
1372
+ // src/commands/platform/deploy/index.ts
1373
+ var deployCommand = Command5.make(
1374
+ "deploy",
1375
+ {},
1376
+ () => Effect12.gen(function* () {
1377
+ const projectConfig = yield* ProjectConfigService;
1378
+ const deploymentApi = yield* DeploymentApiService;
1379
+ const archiveService = yield* ArchiveService;
1380
+ const pathService = yield* Path5.Path;
1381
+ const cwd = process.cwd();
1382
+ yield* Effect12.log("Starting deploy command");
1383
+ 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`);
1389
+ const archive = yield* archiveService.createZipArchive(config.userArchive?.rootOverwrite || cwd, archivePath, {
1390
+ whitelist: config.userArchive?.fileWhitelist,
1391
+ excludeFiles: config.excludeFiles,
1392
+ useIgnoreFiles: false,
1393
+ useManagedIgnore: false
1394
+ });
1395
+ yield* Console5.log(`Archive created (${formatFileSize(archive.size)})`);
1396
+ yield* Console5.log("Uploading archive...");
1397
+ 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.");
1400
+ yield* pollDeployment(deploymentId);
1401
+ }).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* () {
1406
+ for (const e of err.errors) {
1407
+ yield* Console5.error(e);
1408
+ }
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)))
1419
+ })
1420
+ )
1421
+ );
1422
+ var uploadArchive = (deploymentId, archivePath, fileSize) => Effect12.gen(function* () {
1423
+ const deploymentApi = yield* DeploymentApiService;
1424
+ const fs = yield* FileSystem5.FileSystem;
1425
+ const uploadId = yield* deploymentApi.startMultipartUpload(deploymentId);
1426
+ const FILE_CHUNK_SIZE = 10 * 1024 * 1024;
1427
+ const numChunks = Math.ceil(fileSize / FILE_CHUNK_SIZE);
1428
+ yield* Effect12.log("Starting multipart upload", { uploadId, numChunks, fileSize });
1429
+ const parts = yield* Effect12.all(
1430
+ Array.from({ length: numChunks }, (_, i) => {
1431
+ const partNumber = i + 1;
1432
+ const start = i * FILE_CHUNK_SIZE;
1433
+ const bytesToRead = partNumber < numChunks ? FILE_CHUNK_SIZE : fileSize - start;
1434
+ return Effect12.gen(function* () {
1435
+ const presignedUrl = yield* deploymentApi.getPresignedUrl(deploymentId, uploadId, partNumber);
1436
+ const chunkData = yield* Stream.runCollect(fs.stream(archivePath, { offset: start, bytesToRead })).pipe(
1437
+ Effect12.map((chunks) => {
1438
+ const arrays = Array.from(chunks);
1439
+ return Buffer.concat(arrays);
1440
+ }),
1441
+ Effect12.mapError(
1442
+ (error) => new UploadPartError({
1443
+ message: `Failed to read chunk ${partNumber}: ${String(error)}`,
1444
+ partNumber
1445
+ })
1446
+ )
1447
+ );
1448
+ yield* Effect12.log(`Uploading part ${partNumber}/${numChunks}`, { size: chunkData.length });
1449
+ return yield* deploymentApi.uploadPart(presignedUrl, chunkData, partNumber);
1450
+ });
1034
1451
  }),
1035
- duplex: "half"
1036
- });
1037
- if (!res.ok) {
1038
- const errorText = await res.text();
1039
- debug(`Failed to complete upload: ${errorText}`);
1040
- throw new Error("Failed to complete upload: " + errorText);
1041
- }
1042
- debug("Upload completed successfully");
1043
- };
1044
- var getDeploymentStatus = async (deploymentId) => {
1045
- debug(`Getting deployment status for: ${deploymentId}`);
1046
- const res = await fetch(`http://localhost:3000/deployments/${deploymentId}`, {
1047
- method: "GET",
1048
- headers: {
1049
- "Content-Type": "application/json"
1050
- }
1051
- });
1052
- if (!res.ok) {
1053
- const errorText = await res.text();
1054
- debug(`Failed to get deployment status: ${errorText}`);
1055
- throw new Error("Failed to get deployment status: " + errorText);
1056
- }
1057
- const deployment = await res.json();
1058
- debug(`Deployment status: ${deployment.status}`);
1059
- return deployment.status;
1060
- };
1061
- var getLogs = async (deploymentId, options) => {
1062
- debug(`Getting logs for deployment: ${deploymentId}, options:`, options ?? "No options");
1063
- const res = await fetch(`http://localhost:3000/deployments/${deploymentId}/logs${objectToQueryString(options)}`, {
1064
- method: "GET",
1065
- headers: {
1066
- "Content-Type": "application/json"
1067
- }
1068
- });
1069
- if (!res.ok) {
1070
- const errorText = await res.text();
1071
- debug(`Failed to get logs: ${errorText}`);
1072
- throw new Error("Failed to get logs: " + errorText);
1452
+ { concurrency: 3 }
1453
+ );
1454
+ yield* deploymentApi.completeUpload(deploymentId, uploadId, parts);
1455
+ });
1456
+ var pollDeployment = (deploymentId) => Effect12.gen(function* () {
1457
+ const deploymentApi = yield* DeploymentApiService;
1458
+ let lastStatus = "PENDING";
1459
+ 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)));
1464
+ if (newStatus !== lastStatus) {
1465
+ lastStatus = newStatus;
1466
+ const label = newStatus.charAt(0) + newStatus.slice(1).toLowerCase();
1467
+ yield* Console5.log(`Status: ${label}`);
1468
+ }
1469
+ const logsPage = yield* deploymentApi.getLogs(deploymentId, { offset: logOffset, limit: 100 }).pipe(Effect12.catchTag("DeploymentLogsFetchError", () => Effect12.succeed(null)));
1470
+ if (logsPage) {
1471
+ for (const logEntry of logsPage.items) {
1472
+ logOffset++;
1473
+ yield* printLog(logEntry);
1474
+ }
1475
+ }
1476
+ if (newStatus === "ACTIVE") {
1477
+ yield* Console5.log(`Deployed to https://${deploymentId}.gigadrivedev.com`);
1478
+ return yield* Effect12.fail("done");
1479
+ }
1480
+ if (newStatus === "FAILED") {
1481
+ return yield* Effect12.fail(
1482
+ new DeploymentFailedError({
1483
+ message: "The deployment failed. Please check the logs for more information."
1484
+ })
1485
+ );
1486
+ }
1487
+ }),
1488
+ Schedule.addDelay(Schedule.forever, () => Duration.seconds(1))
1489
+ ).pipe(
1490
+ Effect12.catchAll((err) => {
1491
+ if (err === "done") return Effect12.void;
1492
+ return Effect12.fail(err);
1493
+ })
1494
+ );
1495
+ });
1496
+ var printLog = (logEntry) => {
1497
+ switch (logEntry.type) {
1498
+ case "ERROR":
1499
+ return Console5.error(logEntry.message);
1500
+ case "WARN":
1501
+ return Console5.warn(logEntry.message);
1502
+ default:
1503
+ return Console5.log(logEntry.message);
1073
1504
  }
1074
- const logs = await res.json();
1075
- debug(`Got ${logs.items.length} logs, total: ${logs.totalItems}`);
1076
- return logs;
1077
1505
  };
1078
1506
 
1079
1507
  // src/commands/platform/index.ts
1080
- var platform = (program) => {
1081
- const platform2 = program.command("platform").description("Manage the platform");
1082
- deploy(platform2);
1083
- };
1508
+ var platformBase = Command6.make("platform", {}, () => Effect13.void);
1509
+ var platformCommand = platformBase.pipe(Command6.withSubcommands([deployCommand]));
1084
1510
 
1085
1511
  // src/commands/whoami/index.ts
1086
- var whoami = (program) => {
1087
- program.command("whoami").description("Show the currently authenticated user information").action(async () => {
1088
- try {
1089
- const info = await authManager.getUserInfo();
1090
- if (!info) {
1091
- console.log('You are not logged in. Run "gigadrive login" to authenticate.');
1092
- return;
1093
- }
1094
- console.log(JSON.stringify(info, null, 2));
1095
- } catch (err) {
1096
- const msg = err instanceof Error ? err.message : String(err);
1097
- console.error(`Failed to retrieve user info: ${msg}`);
1098
- }
1099
- });
1100
- };
1512
+ import { Command as Command7 } from "@effect/cli";
1513
+ import { Console as Console6, Effect as Effect14 } from "effect";
1514
+ var whoamiCommand = Command7.make(
1515
+ "whoami",
1516
+ {},
1517
+ () => Effect14.gen(function* () {
1518
+ const auth = yield* AuthService;
1519
+ const info = yield* auth.getUserInfo;
1520
+ yield* Console6.log(JSON.stringify(info, null, 2));
1521
+ }).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}`)
1526
+ })
1527
+ )
1528
+ );
1101
1529
 
1102
1530
  // src/index.ts
1103
- var { isTTY } = process.stdout;
1104
- var main = async () => {
1105
- if (process.env.FORCE_TTY === "1") {
1106
- isTTY = true;
1107
- process.stdout.isTTY = true;
1108
- process.stdin.isTTY = true;
1109
- }
1110
- let exitCode = 0;
1111
- const program = new Command();
1112
- program.option("-v, --verbose", "Enable verbose logging", false);
1113
- program.hook("preSubcommand", (thisCommand) => {
1114
- setVerbose(thisCommand.opts().verbose === true);
1115
- if (thisCommand.opts().verbose) {
1116
- log("Using verbose mode");
1117
- }
1118
- });
1119
- program.hook("preAction", (thisCommand) => {
1120
- setVerbose(thisCommand.opts().verbose === true);
1121
- });
1122
- platform(program);
1123
- debug2(program);
1124
- build(program);
1125
- login(program);
1126
- whoami(program);
1127
- try {
1128
- await program.parseAsync(process.argv);
1129
- } catch (err) {
1130
- error(err);
1131
- exitCode = 1;
1132
- }
1133
- return exitCode;
1134
- };
1135
- var handleRejection = (err) => {
1136
- if (err) {
1137
- if (err instanceof Error) {
1138
- handleUnexpected(err);
1139
- } else {
1140
- console.error(`An unexpected rejection occurred
1141
- ${JSON.stringify(err)}`);
1142
- }
1143
- } else {
1144
- console.error("An unexpected empty rejection occurred");
1145
- }
1146
- process.exit(1);
1147
- };
1148
- var handleUnexpected = (error2) => {
1149
- const { message } = error2;
1150
- console.error(`An unexpected error occurred
1151
- ${message}`);
1152
- process.exit(1);
1153
- };
1154
- process.on("unhandledRejection", handleRejection);
1155
- process.on("uncaughtException", handleUnexpected);
1156
- process.on("exit", () => {
1157
- const terminal = process.stderr.isTTY ? process.stderr : process.stdout.isTTY ? process.stdout : void 0;
1158
- terminal?.write("\x1B[?25h");
1531
+ var gigadrive = Command8.make("gigadrive", {}, () => Effect15.void).pipe(
1532
+ Command8.withSubcommands([loginCommand, whoamiCommand, buildCommand, debugCommand, platformCommand])
1533
+ );
1534
+ var cli = Command8.run(gigadrive, {
1535
+ name: "Gigadrive CLI",
1536
+ version: "2.0.0"
1159
1537
  });
1160
- main().then((exitCode) => {
1161
- process.exit(exitCode);
1162
- }).catch(handleUnexpected);
1538
+ var BaseServices = Layer.mergeAll(
1539
+ OAuthConfigService.Default,
1540
+ AuthStorageService.Default,
1541
+ PackageManagerService.Default,
1542
+ ArchiveService.Default,
1543
+ NetworkConfigLive2,
1544
+ ProjectConfigService.Default
1545
+ ).pipe(Layer.provideMerge(AuthService.Default));
1546
+ var DeploymentApiLive = Layer.provide(DeploymentApiService.Default, BaseServices);
1547
+ var ServicesLive = Layer.mergeAll(BaseServices, DeploymentApiLive);
1548
+ var AppLive = Layer.mergeAll(ServicesLive, Logger.minimumLogLevel(LogLevel.Info)).pipe(
1549
+ Layer.provideMerge(NodeContext.layer)
1550
+ );
1551
+ NodeRuntime.runMain(Effect15.provide(cli(process.argv), AppLive));
1163
1552
  //# sourceMappingURL=index.mjs.map