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