@glassly/cli 2.0.0-dev.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/src/index.ts ADDED
@@ -0,0 +1,871 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { Command } from "commander";
4
+ import { buildProduction as buildMiniappProduction, dev as devMiniapp, pack as packMiniapp } from "@glassly/miniapp-cli";
5
+ import { existsSync, readFileSync, statSync } from "node:fs";
6
+ import { basename, join, resolve } from "node:path";
7
+ import {
8
+ createAdminRegistryRevision,
9
+ createApp,
10
+ createRelease,
11
+ deleteApp,
12
+ ensureAdminRegistry,
13
+ getAdminMe,
14
+ getOrg,
15
+ listAdminPreinstallReleases,
16
+ listAdminRegistries,
17
+ listAdminRegistryRevisions,
18
+ listApps,
19
+ listReleases,
20
+ promoteAdminRegistryRevision,
21
+ pollLoginToken,
22
+ refreshLoginToken,
23
+ startLogin,
24
+ submitRelease,
25
+ upsertOrg,
26
+ type AdminReleaseSummary,
27
+ type AdminRegistry,
28
+ type PreinstallEnvironment,
29
+ type PreinstallPolicy,
30
+ } from "./api";
31
+ import { getConfig } from "./config";
32
+ import { clearCredentials, loadCredentials, saveCredentials, type CliCredentials } from "./credentials";
33
+ import { openBrowser } from "./open-browser";
34
+ import { encodeDevAttestation, ensureSigningKey, signBundleMetadata, signDevAttestation } from "./signing";
35
+
36
+ const program = new Command();
37
+
38
+ program
39
+ .name("glassly")
40
+ .description("Glassly developer CLI")
41
+ .version("2.0.0-alpha.0");
42
+
43
+ program
44
+ .command("login")
45
+ .description("Sign in to Glassly Developer Console")
46
+ .option("--no-open", "print the login URL without opening a browser")
47
+ .action(async (options: { open: boolean }) => {
48
+ const config = getConfig();
49
+ let challenge;
50
+ try {
51
+ challenge = await startLogin(config);
52
+ } catch (error) {
53
+ console.error(error instanceof Error ? error.message : String(error));
54
+ process.exitCode = 1;
55
+ return;
56
+ }
57
+
58
+ console.log("Sign in to Glassly Developer Console");
59
+ console.log("");
60
+ console.log(`Open: ${challenge.verification_uri_complete}`);
61
+ console.log(`Code: ${challenge.user_code}`);
62
+ console.log("");
63
+
64
+ if (options.open) {
65
+ const opened = await openBrowser(challenge.verification_uri_complete);
66
+ if (!opened) console.log("Could not open a browser automatically.");
67
+ }
68
+
69
+ const deadline = Date.now() + challenge.expires_in * 1000;
70
+ process.stdout.write("Waiting for browser approval");
71
+
72
+ while (Date.now() < deadline) {
73
+ const token = await pollLoginToken(config, challenge.device_code);
74
+ if ("status" in token) {
75
+ if (token.status === "slow_down") {
76
+ await sleep(challenge.interval * 1000);
77
+ }
78
+ } else {
79
+ process.stdout.write("\n");
80
+ const storedAt = new Date();
81
+ const expiresAt =
82
+ typeof token.expires_in === "number"
83
+ ? new Date(storedAt.getTime() + token.expires_in * 1000)
84
+ : expiresAtFromToken(token.access_token)
85
+ ? new Date(expiresAtFromToken(token.access_token)! * 1000)
86
+ : undefined;
87
+ const storage = await saveCredentials({
88
+ token: token.access_token,
89
+ refreshToken: token.refresh_token,
90
+ workosUserId: token.user.id,
91
+ email: token.user.email,
92
+ organizationId: token.organization_id,
93
+ authenticationMethod: token.authentication_method,
94
+ coreUrl: config.coreUrl,
95
+ storedAt: storedAt.toISOString(),
96
+ expiresAt: expiresAt?.toISOString(),
97
+ });
98
+ console.log(`Signed in as ${token.user.email}`);
99
+ if (token.organization_id) console.log(`Organization: ${token.organization_id}`);
100
+ console.log(`Credentials stored in ${storage === "keychain" ? "OS keychain" : "~/.glassly/cli-v2"}`);
101
+ return;
102
+ }
103
+
104
+ process.stdout.write(".");
105
+ await sleep(challenge.interval * 1000);
106
+ }
107
+
108
+ process.stdout.write("\n");
109
+ console.error("Login timed out. Run `glassly login` to try again.");
110
+ process.exitCode = 1;
111
+ });
112
+
113
+ program
114
+ .command("whoami")
115
+ .description("Show the current CLI login")
116
+ .action(async () => {
117
+ const config = getConfig();
118
+ const creds = await loadFreshCredentials(config);
119
+ if (!creds) {
120
+ console.error("Not signed in. Run `glassly login`.");
121
+ process.exitCode = 1;
122
+ return;
123
+ }
124
+
125
+ console.log(`Email: ${creds.email}`);
126
+ console.log(`WorkOS user: ${creds.workosUserId}`);
127
+ if (creds.organizationId) console.log(`Organization: ${creds.organizationId}`);
128
+ console.log(`Core: ${config.coreUrl}`);
129
+ if (creds.expiresAt) console.log(`Expires: ${new Date(creds.expiresAt).toLocaleString()}`);
130
+ });
131
+
132
+ const org = program.command("org").description("Manage the current developer organization");
133
+
134
+ org
135
+ .command("show")
136
+ .description("Show the current developer organization")
137
+ .action(async () => {
138
+ const creds = await requireCredentials();
139
+ if (!creds) return;
140
+
141
+ try {
142
+ const { org: developerOrg } = await getOrg(creds);
143
+ if (!developerOrg) {
144
+ console.log("No developer org yet. Run `glassly org init --name \"Your Org\" --prefix com.example`.");
145
+ return;
146
+ }
147
+
148
+ console.log(`Name: ${developerOrg.name}`);
149
+ console.log(`Package prefix: ${developerOrg.packagePrefix}`);
150
+ console.log(`Prefix status: ${developerOrg.packagePrefixStatus}`);
151
+ if (developerOrg.workosOrgId) console.log(`WorkOS org: ${developerOrg.workosOrgId}`);
152
+ } catch (error) {
153
+ fail(error);
154
+ }
155
+ });
156
+
157
+ org
158
+ .command("init")
159
+ .description("Create or update the current developer organization")
160
+ .requiredOption("--name <name>", "organization display name")
161
+ .requiredOption("--prefix <prefix>", "package prefix, e.g. com.example")
162
+ .action(async (options: { name: string; prefix: string }) => {
163
+ const creds = await requireCredentials();
164
+ if (!creds) return;
165
+
166
+ try {
167
+ const { org: developerOrg } = await upsertOrg(creds, {
168
+ displayName: options.name,
169
+ packagePrefix: options.prefix,
170
+ });
171
+ console.log(`Developer org ready: ${developerOrg.name}`);
172
+ console.log(`Package prefix: ${developerOrg.packagePrefix} (${developerOrg.packagePrefixStatus})`);
173
+ if (developerOrg.workosOrgId) console.log(`WorkOS org: ${developerOrg.workosOrgId}`);
174
+ } catch (error) {
175
+ fail(error);
176
+ }
177
+ });
178
+
179
+ const miniapps = program.command("miniapps").description("Manage miniapp package records");
180
+
181
+ miniapps
182
+ .command("list")
183
+ .description("List miniapps owned by the current developer org")
184
+ .action(async () => {
185
+ const creds = await requireCredentials();
186
+ if (!creds) return;
187
+
188
+ try {
189
+ const { apps: appList } = await listApps(creds);
190
+ if (appList.length === 0) {
191
+ console.log("No miniapps yet.");
192
+ return;
193
+ }
194
+
195
+ for (const app of appList) {
196
+ const release = app.activeRelease ?? app.latestRelease;
197
+ const releaseLabel = release ? `${release.version} (${release.status})` : "no releases";
198
+ console.log(`${app.packageName}\t${app.name}\t${app.status}\t${releaseLabel}`);
199
+ }
200
+ } catch (error) {
201
+ fail(error);
202
+ }
203
+ });
204
+
205
+ miniapps
206
+ .command("create")
207
+ .argument("<packageName>", "stable package name, e.g. com.glassly.myminiapp")
208
+ .requiredOption("--name <name>", "display name")
209
+ .option("--description <description>", "short miniapp description")
210
+ .description("Reserve a miniapp package name")
211
+ .action(async (packageName: string, options: { name: string; description?: string }) => {
212
+ const creds = await requireCredentials();
213
+ if (!creds) return;
214
+
215
+ try {
216
+ const { app } = await createApp(creds, {
217
+ packageName,
218
+ displayName: options.name,
219
+ description: options.description ?? null,
220
+ });
221
+ console.log(`Miniapp ready: ${app.packageName} (${app.name})`);
222
+ } catch (error) {
223
+ fail(error);
224
+ }
225
+ });
226
+
227
+ miniapps
228
+ .command("delete")
229
+ .argument("<packageName>", "package name to archive")
230
+ .description("Archive a miniapp package record")
231
+ .action(async (packageName: string) => {
232
+ const creds = await requireCredentials();
233
+ if (!creds) return;
234
+
235
+ try {
236
+ await deleteApp(creds, packageName);
237
+ console.log(`Archived ${packageName}`);
238
+ } catch (error) {
239
+ fail(error);
240
+ }
241
+ });
242
+
243
+ const releases = program.command("releases").description("Inspect miniapp releases");
244
+
245
+ releases
246
+ .command("list")
247
+ .argument("<packageName>", "package name")
248
+ .description("List releases for a miniapp")
249
+ .action(async (packageName: string) => {
250
+ const creds = await requireCredentials();
251
+ if (!creds) return;
252
+
253
+ try {
254
+ const { releases: releaseList } = await listReleases(creds, packageName);
255
+ if (releaseList.length === 0) {
256
+ console.log("No releases yet.");
257
+ return;
258
+ }
259
+
260
+ for (const release of releaseList) {
261
+ const size = release.bundleSizeBytes ? `${Math.round(release.bundleSizeBytes / 1024)} KB` : "no bundle";
262
+ console.log(`${release.version}\t${release.status}\t${size}\t${release.bundleSha256 ?? "no hash"}`);
263
+ }
264
+ } catch (error) {
265
+ fail(error);
266
+ }
267
+ });
268
+
269
+ releases
270
+ .command("submit")
271
+ .argument("<packageName>", "package name")
272
+ .argument("<releaseId>", "release id")
273
+ .description("Submit an uploaded release for admin review")
274
+ .action(async (packageName: string, releaseId: string) => {
275
+ const creds = await requireCredentials();
276
+ if (!creds) return;
277
+
278
+ try {
279
+ const { release } = await submitRelease(creds, { packageName, releaseId });
280
+ console.log(`Submitted ${packageName}@${release.version} for review`);
281
+ } catch (error) {
282
+ fail(error);
283
+ }
284
+ });
285
+
286
+ const admin = program.command("admin").description("Internal Glassly admin operations");
287
+
288
+ admin
289
+ .command("me")
290
+ .description("Show the current admin identity")
291
+ .action(async () => {
292
+ const creds = await requireCredentials();
293
+ if (!creds) return;
294
+
295
+ try {
296
+ const me = await getAdminMe(creds);
297
+ console.log(`Admin: ${me.user?.email ?? "unknown"}`);
298
+ console.log(`Core: ${creds.coreUrl}`);
299
+ } catch (error) {
300
+ fail(error);
301
+ }
302
+ });
303
+
304
+ const preinstall = admin
305
+ .command("preinstall")
306
+ .description("Manage the internal preinstalled miniapp registry");
307
+
308
+ preinstall
309
+ .command("releases")
310
+ .description("List releases eligible for preinstall")
311
+ .option("-e, --environment <environment>", "target registry environment")
312
+ .option("--verbose", "include release ids")
313
+ .action(async (options: { environment?: string; verbose?: boolean }) => {
314
+ const creds = await requireCredentials();
315
+ if (!creds) return;
316
+
317
+ try {
318
+ const environment = options.environment
319
+ ? parseEnvironment(options.environment)
320
+ : inferEnvironment(creds.coreUrl);
321
+ const { releases: releaseList } = await listAdminPreinstallReleases(creds);
322
+ if (releaseList.length === 0) {
323
+ console.log("No publishable releases.");
324
+ return;
325
+ }
326
+ const activeReleaseIds = await activePreinstallReleaseIds(creds, environment);
327
+ const groups = groupAdminReleases(releaseList, activeReleaseIds);
328
+ console.log(`Publishable miniapps for ${environment} preinstall:`);
329
+ for (const group of groups) {
330
+ console.log(formatAdminReleaseGroup(group));
331
+ if (options.verbose) {
332
+ for (const release of group.releases) {
333
+ const marker = release.id === group.current?.id ? "*" : " ";
334
+ console.log(` ${marker} ${release.version}\t${release.status}\t${release.id}`);
335
+ }
336
+ }
337
+ }
338
+ console.log("");
339
+ console.log("Publish with:");
340
+ console.log(` ${commandNameForCoreUrl(creds.coreUrl)} admin preinstall publish --release ${groups[0]?.packageName}@${groups[0]?.latest.version} --environment ${environment}`);
341
+ } catch (error) {
342
+ fail(error);
343
+ }
344
+ });
345
+
346
+ preinstall
347
+ .command("status")
348
+ .description("Show the active preinstall registry for an environment")
349
+ .option("-e, --environment <environment>", "target registry environment")
350
+ .action(async (options: { environment?: string }) => {
351
+ const creds = await requireCredentials();
352
+ if (!creds) return;
353
+
354
+ try {
355
+ const environment = options.environment
356
+ ? parseEnvironment(options.environment)
357
+ : inferEnvironment(creds.coreUrl);
358
+ const { registries } = await listAdminRegistries(creds);
359
+ const registry = registries.find(item => item.environment === environment && item.name === "default");
360
+ if (!registry) {
361
+ console.log(`No active ${environment} preinstall registry.`);
362
+ return;
363
+ }
364
+
365
+ console.log(`${environment} preinstall registry`);
366
+ console.log(`Registry: ${registry.id}`);
367
+ console.log(`Status: ${registry.status}`);
368
+ if (!registry.activeRevisionId) {
369
+ console.log("Active revision: none");
370
+ return;
371
+ }
372
+
373
+ const { revisions } = await listAdminRegistryRevisions(creds, registry.id);
374
+ const active = revisions.find(revision => revision.id === registry.activeRevisionId);
375
+ console.log(`Active revision: ${registry.activeRevisionId}`);
376
+ if (!active || active.entries.length === 0) {
377
+ console.log("No preinstalled releases.");
378
+ return;
379
+ }
380
+
381
+ const { releases: releaseList } = await listAdminPreinstallReleases(creds);
382
+ const byId = new Map(releaseList.map(release => [release.id, release]));
383
+ for (const entry of active.entries) {
384
+ const release = byId.get(entry.releaseId);
385
+ const label = release ? `${release.packageName}@${release.version}` : entry.releaseId;
386
+ console.log(`- ${label}\t${entry.installPolicy}\trequired=${entry.required}`);
387
+ }
388
+ } catch (error) {
389
+ fail(error);
390
+ }
391
+ });
392
+
393
+ preinstall
394
+ .command("registries")
395
+ .description("List preinstall registries and active revisions")
396
+ .option("-e, --environment <environment>", "filter by environment")
397
+ .action(async (options: { environment?: string }) => {
398
+ const creds = await requireCredentials();
399
+ if (!creds) return;
400
+
401
+ try {
402
+ const environment = options.environment ? parseEnvironment(options.environment) : null;
403
+ const { registries } = await listAdminRegistries(creds);
404
+ const filtered = environment
405
+ ? registries.filter(registry => registry.environment === environment)
406
+ : registries;
407
+ if (filtered.length === 0) {
408
+ console.log("No preinstall registries.");
409
+ return;
410
+ }
411
+
412
+ for (const registry of filtered) {
413
+ console.log(formatRegistry(registry));
414
+ if (registry.activeRevisionId) {
415
+ const { revisions } = await listAdminRegistryRevisions(creds, registry.id);
416
+ const active = revisions.find(revision => revision.id === registry.activeRevisionId);
417
+ if (active) {
418
+ console.log(` active revision: ${active.id}`);
419
+ for (const entry of active.entries) {
420
+ console.log(` - ${entry.releaseId}\t${entry.installPolicy}\trequired=${entry.required}`);
421
+ }
422
+ }
423
+ }
424
+ }
425
+ } catch (error) {
426
+ fail(error);
427
+ }
428
+ });
429
+
430
+ preinstall
431
+ .command("publish")
432
+ .description("Replace the active preinstalled miniapp list with selected releases")
433
+ .requiredOption("-r, --release <release>", "release id or package@version; repeat for multiple releases", collect, [])
434
+ .option("-e, --environment <environment>", "target registry environment")
435
+ .option("--policy <policy>", "install policy for all entries", "keep_updated")
436
+ .option("--required", "mark entries required")
437
+ .option("--reason <reason>", "audit reason")
438
+ .option("--yes", "confirm prod registry replacement")
439
+ .action(async (options: {
440
+ release: string[];
441
+ environment?: string;
442
+ policy: string;
443
+ required?: boolean;
444
+ reason?: string;
445
+ yes?: boolean;
446
+ }) => {
447
+ const creds = await requireCredentials();
448
+ if (!creds) return;
449
+
450
+ try {
451
+ const environment = options.environment
452
+ ? parseEnvironment(options.environment)
453
+ : inferEnvironment(creds.coreUrl);
454
+ const installPolicy = parsePolicy(options.policy);
455
+ if (environment === "prod" && !options.yes) {
456
+ throw new Error("prod preinstall publish requires --yes");
457
+ }
458
+
459
+ const { releases: releaseList } = await listAdminPreinstallReleases(creds);
460
+ const selected = resolveReleaseRefs(options.release, releaseList);
461
+ const { registry } = await ensureAdminRegistry(creds, { environment });
462
+ const { revision } = await createAdminRegistryRevision(creds, registry.id, {
463
+ reason: options.reason ?? `Admin preinstall registry publish for ${environment}`,
464
+ entries: selected.map((release, index) => ({
465
+ releaseId: release.id,
466
+ required: Boolean(options.required),
467
+ installPolicy,
468
+ priority: index,
469
+ })),
470
+ });
471
+ const promoted = await promoteAdminRegistryRevision(creds, registry.id, revision.id);
472
+
473
+ console.log(`Published ${selected.length} release(s) to ${environment} preinstall registry.`);
474
+ console.log(`Registry: ${promoted.registry.id}`);
475
+ console.log(`Revision: ${promoted.revision.id}`);
476
+ for (const release of selected) {
477
+ console.log(`- ${release.packageName}@${release.version}\t${release.id}`);
478
+ }
479
+ } catch (error) {
480
+ fail(error);
481
+ }
482
+ });
483
+
484
+ program
485
+ .command("dev")
486
+ .description("Start the local miniapp dev server with signed Cloud V2 identity when logged in")
487
+ .option("--cwd <path>", "miniapp project directory", process.cwd())
488
+ .option("--auth", "require signed dev auto-auth setup before starting")
489
+ .action(async (options: { cwd: string; auth?: boolean }) => {
490
+ const cwd = resolve(options.cwd);
491
+ try {
492
+ const manifest = readManifest(cwd);
493
+ const packageName = stringField(manifest, "packageName");
494
+ const name = stringField(manifest, "name") || packageName;
495
+ const description = typeof manifest.description === "string" ? manifest.description : null;
496
+ const creds = await loadFreshCredentials(getConfig());
497
+ let signer:
498
+ | ((input: { packageName: string; devServerUrl: string }) => string)
499
+ | undefined;
500
+
501
+ if (creds) {
502
+ await ensureMiniappRecord(creds, { packageName, displayName: name, description });
503
+ const signingKey = await ensureSigningKey(creds);
504
+ signer = ({ packageName: signedPackageName, devServerUrl }) =>
505
+ encodeDevAttestation(signDevAttestation({
506
+ signingKey,
507
+ packageName: signedPackageName,
508
+ devServerUrl,
509
+ }));
510
+ console.log(`Dev auto-auth enabled for ${packageName}`);
511
+ } else if (options.auth) {
512
+ throw new Error("Not signed in. Run `glassly login` before `glassly dev --auth`.");
513
+ } else {
514
+ console.log("Dev auto-auth disabled. Run `glassly login` if this miniapp uses session.auth.");
515
+ }
516
+
517
+ await devMiniapp({ cwd, signDevAttestation: signer });
518
+ } catch (error) {
519
+ fail(error);
520
+ }
521
+ });
522
+
523
+ program
524
+ .command("build")
525
+ .description("Build the current miniapp")
526
+ .option("--cwd <path>", "miniapp project directory", process.cwd())
527
+ .action(async (options: { cwd: string }) => {
528
+ try {
529
+ await buildMiniappProduction(resolve(options.cwd));
530
+ } catch (error) {
531
+ fail(error);
532
+ }
533
+ });
534
+
535
+ program
536
+ .command("pack")
537
+ .description("Pack the current miniapp into build/<packageName>-<version>.zip")
538
+ .option("--cwd <path>", "miniapp project directory", process.cwd())
539
+ .option("--no-build", "skip production build before packing")
540
+ .action(async (options: { cwd: string; build: boolean }) => {
541
+ try {
542
+ await packMiniapp({ cwd: resolve(options.cwd), build: options.build });
543
+ } catch (error) {
544
+ fail(error);
545
+ }
546
+ });
547
+
548
+ program
549
+ .command("publish")
550
+ .description("Build, pack, and upload the current miniapp release bundle")
551
+ .option("--cwd <path>", "miniapp project directory", process.cwd())
552
+ .option("--no-build", "skip running bun run build before packing")
553
+ .option("--no-pack", "skip running bun run pack and upload the existing build zip")
554
+ .action(async (options: { cwd: string; build: boolean; pack: boolean }) => {
555
+ const creds = await requireCredentials();
556
+ if (!creds) return;
557
+
558
+ const cwd = resolve(options.cwd);
559
+ try {
560
+ const manifest = readManifest(cwd);
561
+ const packageName = stringField(manifest, "packageName");
562
+ const version = stringField(manifest, "version");
563
+ const name = stringField(manifest, "name") || packageName;
564
+ const description = typeof manifest.description === "string" ? manifest.description : null;
565
+
566
+ await ensureMiniappRecord(creds, { packageName, displayName: name, description });
567
+
568
+ if (options.pack) {
569
+ await packMiniapp({ cwd, build: options.build });
570
+ } else if (options.build) {
571
+ await buildMiniappProduction(cwd);
572
+ }
573
+
574
+ const zipPath = join(cwd, "build", `${packageName}-${version}.zip`);
575
+ if (!existsSync(zipPath)) {
576
+ throw new Error(`Release bundle not found: ${zipPath}`);
577
+ }
578
+ const bundle = readFileSync(zipPath);
579
+ const signingKey = await ensureSigningKey(creds);
580
+ const signedBundle = signBundleMetadata({
581
+ signingKey,
582
+ packageName,
583
+ version,
584
+ manifest,
585
+ bundle,
586
+ });
587
+ const { release } = await createRelease(creds, {
588
+ packageName,
589
+ version,
590
+ manifest,
591
+ bundleBase64: bundle.toString("base64"),
592
+ fileName: basename(zipPath),
593
+ signedBundle,
594
+ });
595
+ const submitted = await submitRelease(creds, {
596
+ packageName,
597
+ releaseId: release.id,
598
+ });
599
+ const sizeKb = Math.round(statSync(zipPath).size / 1024);
600
+ console.log(`Published ${packageName}@${release.version}`);
601
+ console.log(`Release: ${submitted.release.status}`);
602
+ console.log(`Bundle: ${basename(zipPath)} (${sizeKb} KB)`);
603
+ console.log(`Signing key: ${signedBundle.signingKeyId}`);
604
+ if (release.bundleSha256) console.log(`SHA-256: ${release.bundleSha256}`);
605
+ } catch (error) {
606
+ fail(error);
607
+ }
608
+ });
609
+
610
+ program
611
+ .command("logout")
612
+ .description("Clear the current CLI login")
613
+ .action(async () => {
614
+ await clearCredentials(getConfig().coreUrl);
615
+ console.log("Logged out");
616
+ });
617
+
618
+ program.parse();
619
+
620
+ async function requireCredentials(): Promise<CliCredentials | null> {
621
+ const config = getConfig();
622
+ const creds = await loadFreshCredentials(config);
623
+ if (!creds) {
624
+ console.error("Not signed in. Run `glassly login`.");
625
+ process.exitCode = 1;
626
+ return null;
627
+ }
628
+ return creds;
629
+ }
630
+
631
+ async function loadFreshCredentials(config = getConfig()): Promise<CliCredentials | null> {
632
+ const creds = await loadCredentials(config.coreUrl);
633
+ if (!creds) return null;
634
+ if (!shouldRefresh(creds)) return creds;
635
+
636
+ if (!creds.refreshToken) {
637
+ console.error("Session expired. Run `glassly login`.");
638
+ process.exitCode = 1;
639
+ return null;
640
+ }
641
+
642
+ try {
643
+ const refreshed = await refreshLoginToken(config, creds.refreshToken, creds.organizationId);
644
+ const storedAt = new Date();
645
+ const expiresAt =
646
+ typeof refreshed.expires_in === "number"
647
+ ? new Date(storedAt.getTime() + refreshed.expires_in * 1000)
648
+ : expiresAtFromToken(refreshed.access_token)
649
+ ? new Date(expiresAtFromToken(refreshed.access_token)! * 1000)
650
+ : undefined;
651
+ const nextCredentials: CliCredentials = {
652
+ token: refreshed.access_token,
653
+ refreshToken: refreshed.refresh_token ?? creds.refreshToken,
654
+ workosUserId: refreshed.user.id,
655
+ email: refreshed.user.email,
656
+ organizationId: refreshed.organization_id,
657
+ authenticationMethod: refreshed.authentication_method ?? creds.authenticationMethod,
658
+ coreUrl: config.coreUrl,
659
+ storedAt: storedAt.toISOString(),
660
+ expiresAt: expiresAt?.toISOString(),
661
+ };
662
+ await saveCredentials(nextCredentials);
663
+ return nextCredentials;
664
+ } catch (error) {
665
+ console.error(error instanceof Error ? error.message : String(error));
666
+ process.exitCode = 1;
667
+ return null;
668
+ }
669
+ }
670
+
671
+ function shouldRefresh(creds: CliCredentials): boolean {
672
+ const expiresAtMs = creds.expiresAt ? Date.parse(creds.expiresAt) : expiresAtFromToken(creds.token) * 1000;
673
+ if (!Number.isFinite(expiresAtMs) || expiresAtMs <= 0) return false;
674
+ return expiresAtMs - Date.now() < 60_000;
675
+ }
676
+
677
+ function expiresAtFromToken(token: string): number {
678
+ try {
679
+ const payload = JSON.parse(Buffer.from(token.split(".")[1] ?? "", "base64url").toString("utf8")) as { exp?: unknown };
680
+ return typeof payload.exp === "number" ? payload.exp : 0;
681
+ } catch {
682
+ return 0;
683
+ }
684
+ }
685
+
686
+ function readManifest(cwd: string): Record<string, unknown> {
687
+ const path = join(cwd, "miniapp.json");
688
+ if (!existsSync(path)) throw new Error(`miniapp.json not found in ${cwd}`);
689
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown;
690
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
691
+ throw new Error("miniapp.json must be a JSON object");
692
+ }
693
+ return parsed as Record<string, unknown>;
694
+ }
695
+
696
+ function stringField(manifest: Record<string, unknown>, field: string): string {
697
+ const value = manifest[field];
698
+ if (typeof value !== "string" || !value.trim()) {
699
+ throw new Error(`miniapp.json is missing string field "${field}"`);
700
+ }
701
+ return value.trim();
702
+ }
703
+
704
+ async function ensureMiniappRecord(
705
+ creds: CliCredentials,
706
+ input: { packageName: string; displayName: string; description?: string | null },
707
+ ): Promise<void> {
708
+ try {
709
+ await createApp(creds, input);
710
+ return;
711
+ } catch (error) {
712
+ const message = error instanceof Error ? error.message : String(error);
713
+ if (!message.toLowerCase().includes("package name is already claimed")) {
714
+ throw error;
715
+ }
716
+ }
717
+
718
+ const { apps } = await listApps(creds);
719
+ const existing = apps.find(app => app.packageName === input.packageName && app.status !== "archived");
720
+ if (!existing) {
721
+ throw new Error(`Package ${input.packageName} is already claimed by another developer org.`);
722
+ }
723
+ }
724
+
725
+ function fail(error: unknown): void {
726
+ console.error(error instanceof Error ? error.message : String(error));
727
+ process.exitCode = 1;
728
+ }
729
+
730
+ function collect(value: string, previous: string[]): string[] {
731
+ return [...previous, value];
732
+ }
733
+
734
+ function parseEnvironment(value: string): PreinstallEnvironment {
735
+ const normalized = value.trim().toLowerCase();
736
+ if (["debug", "dev", "staging", "prod"].includes(normalized)) {
737
+ return normalized as PreinstallEnvironment;
738
+ }
739
+ throw new Error("environment must be debug, dev, staging, or prod");
740
+ }
741
+
742
+ function inferEnvironment(coreUrl: string): PreinstallEnvironment {
743
+ const host = new URL(coreUrl).hostname;
744
+ if (host.includes(".debug.")) return "debug";
745
+ if (host.includes(".dev.")) return "dev";
746
+ if (host.includes(".staging.")) return "staging";
747
+ if (host === "localhost" || host === "127.0.0.1") return "dev";
748
+ return "prod";
749
+ }
750
+
751
+ function parsePolicy(value: string): PreinstallPolicy {
752
+ const normalized = value.trim().toLowerCase();
753
+ if (["install_once", "keep_updated", "mandatory"].includes(normalized)) {
754
+ return normalized as PreinstallPolicy;
755
+ }
756
+ throw new Error("policy must be install_once, keep_updated, or mandatory");
757
+ }
758
+
759
+ function resolveReleaseRefs(refs: string[], releases: AdminReleaseSummary[]): AdminReleaseSummary[] {
760
+ const selected = refs.map(ref => {
761
+ const trimmed = ref.trim();
762
+ const byId = releases.find(release => release.id === trimmed);
763
+ if (byId) return byId;
764
+
765
+ const atIndex = trimmed.lastIndexOf("@");
766
+ if (atIndex > 0) {
767
+ const packageName = trimmed.slice(0, atIndex);
768
+ const version = trimmed.slice(atIndex + 1);
769
+ const matches = releases.filter(release => release.packageName === packageName && release.version === version);
770
+ if (matches.length === 1) return matches[0];
771
+ if (matches.length > 1) throw new Error(`release ref ${trimmed} matched more than one release`);
772
+ }
773
+
774
+ throw new Error(`release not found or not publishable: ${trimmed}`);
775
+ });
776
+
777
+ const seen = new Set<string>();
778
+ const seenPackages = new Map<string, string>();
779
+ for (const release of selected) {
780
+ if (seen.has(release.id)) throw new Error(`duplicate release: ${release.id}`);
781
+ seen.add(release.id);
782
+
783
+ const previous = seenPackages.get(release.packageName);
784
+ if (previous) {
785
+ throw new Error(
786
+ `preinstall registry can only include one release per miniapp; ${release.packageName} selected as ${previous} and ${release.version}`,
787
+ );
788
+ }
789
+ seenPackages.set(release.packageName, release.version);
790
+ }
791
+ return selected;
792
+ }
793
+
794
+ async function activePreinstallReleaseIds(
795
+ credentials: CliCredentials,
796
+ environment: PreinstallEnvironment,
797
+ ): Promise<Set<string>> {
798
+ const { registries } = await listAdminRegistries(credentials);
799
+ const registry = registries.find(item => item.environment === environment && item.name === "default");
800
+ if (!registry?.activeRevisionId) return new Set();
801
+ const { revisions } = await listAdminRegistryRevisions(credentials, registry.id);
802
+ const active = revisions.find(revision => revision.id === registry.activeRevisionId);
803
+ return new Set(active?.entries.map(entry => entry.releaseId) ?? []);
804
+ }
805
+
806
+ interface AdminReleaseGroup {
807
+ packageName: string;
808
+ displayName: string;
809
+ latest: AdminReleaseSummary;
810
+ current: AdminReleaseSummary | null;
811
+ releases: AdminReleaseSummary[];
812
+ }
813
+
814
+ function groupAdminReleases(
815
+ releases: AdminReleaseSummary[],
816
+ activeReleaseIds: Set<string>,
817
+ ): AdminReleaseGroup[] {
818
+ const groups = new Map<string, AdminReleaseSummary[]>();
819
+ for (const release of releases) {
820
+ groups.set(release.packageName, [...(groups.get(release.packageName) ?? []), release]);
821
+ }
822
+
823
+ return [...groups.values()].map(groupReleases => {
824
+ const sorted = [...groupReleases].sort(compareReleaseRecency);
825
+ return {
826
+ packageName: sorted[0]!.packageName,
827
+ displayName: sorted[0]!.displayName,
828
+ latest: sorted[0]!,
829
+ current: sorted.find(release => activeReleaseIds.has(release.id)) ?? null,
830
+ releases: sorted,
831
+ };
832
+ }).sort((a, b) => {
833
+ if (a.current && !b.current) return -1;
834
+ if (!a.current && b.current) return 1;
835
+ return a.packageName.localeCompare(b.packageName);
836
+ });
837
+ }
838
+
839
+ function compareReleaseRecency(a: AdminReleaseSummary, b: AdminReleaseSummary): number {
840
+ const aTime = a.createdAt ? Date.parse(a.createdAt) : 0;
841
+ const bTime = b.createdAt ? Date.parse(b.createdAt) : 0;
842
+ if (aTime !== bTime) return bTime - aTime;
843
+ return b.version.localeCompare(a.version, undefined, { numeric: true, sensitivity: "base" });
844
+ }
845
+
846
+ function formatAdminReleaseGroup(group: AdminReleaseGroup): string {
847
+ const marker = group.current ? "ACTIVE" : " ";
848
+ const current = group.current?.version ?? "none";
849
+ const latest = group.latest.version;
850
+ const label = group.displayName && group.displayName !== group.packageName
851
+ ? `${group.displayName} (${group.packageName})`
852
+ : group.packageName;
853
+ return `${marker} ${label}\tcurrent=${current}\tlatest=${latest}\tversions=${group.releases.length}`;
854
+ }
855
+
856
+ function formatRegistry(registry: AdminRegistry): string {
857
+ const active = registry.activeRevisionId ? `active=${registry.activeRevisionId}` : "no active revision";
858
+ return `${registry.id}\t${registry.environment}\t${registry.name}\t${registry.status}\t${active}`;
859
+ }
860
+
861
+ function commandNameForCoreUrl(coreUrl: string): string {
862
+ const environment = inferEnvironment(coreUrl);
863
+ const host = new URL(coreUrl).hostname;
864
+ if (host === "localhost" || host === "127.0.0.1") return "glassly:local";
865
+ if (environment === "prod") return "glassly:prod";
866
+ return `glassly:${environment}`;
867
+ }
868
+
869
+ function sleep(ms: number): Promise<void> {
870
+ return new Promise((resolve) => setTimeout(resolve, ms));
871
+ }