@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/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # @glassly/cli
2
+
3
+ The Glassly developer CLI — the `glassly` command. Build, publish, and manage
4
+ Glassly miniapps against the Cloud V2 developer console.
5
+
6
+ It wraps [`@glassly/miniapp-cli`](https://www.npmjs.com/package/@glassly/miniapp-cli)
7
+ (the `dev` / `build` / `pack` author flow) and adds account and store operations:
8
+ `login`, `whoami`, `org`, `miniapps`, `releases`, and `publish`.
9
+
10
+ > **Bun-only.** This CLI ships as TypeScript and runs under [Bun](https://bun.sh)
11
+ > (`#!/usr/bin/env bun`). Use `bun` / `bunx`, not `npx`/Node.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ bun add -g @glassly/cli@dev
17
+ glassly --help
18
+ ```
19
+
20
+ Or run without installing:
21
+
22
+ ```bash
23
+ bunx @glassly/cli@dev --help
24
+ ```
25
+
26
+ ## Common commands
27
+
28
+ ```bash
29
+ glassly login # sign in to the Glassly Developer Console
30
+ glassly dev # local dev server with a signed Cloud V2 identity
31
+ glassly build # build the current miniapp
32
+ glassly pack # zip dist/ into a submittable release
33
+ glassly publish # upload + publish a release
34
+ glassly miniapps list # miniapps owned by your org
35
+ glassly releases submit # submit an uploaded release for review
36
+ ```
37
+
38
+ Run `glassly <command> --help` for the full option set.
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@glassly/cli",
3
+ "version": "2.0.0-dev.0",
4
+ "description": "Glassly developer CLI — build, publish, and manage Glassly miniapps (Cloud V2). Bun-only.",
5
+ "type": "module",
6
+ "bin": {
7
+ "glassly": "./src/index.ts"
8
+ },
9
+ "files": [
10
+ "src"
11
+ ],
12
+ "engines": {
13
+ "bun": ">=1.0.0"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public",
17
+ "tag": "dev"
18
+ },
19
+ "scripts": {
20
+ "dev": "bun src/index.ts",
21
+ "typecheck": "tsc --noEmit"
22
+ },
23
+ "dependencies": {
24
+ "@glassly/miniapp-cli": "0.1.0-dev.1",
25
+ "commander": "^14.0.2",
26
+ "qrcode": "^1.5.4"
27
+ },
28
+ "devDependencies": {
29
+ "@types/bun": "1.3.14",
30
+ "@types/qrcode": "^1.5.5"
31
+ },
32
+ "keywords": [
33
+ "glassly",
34
+ "glassly",
35
+ "cli",
36
+ "miniapp"
37
+ ],
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/tetramo-labs/glassly.git",
41
+ "directory": "cloud-v2/packages/cli"
42
+ },
43
+ "homepage": "https://github.com/tetramo-labs/glassly/tree/dev/cloud-v2/packages/cli#readme",
44
+ "bugs": {
45
+ "url": "https://github.com/tetramo-labs/glassly/issues"
46
+ }
47
+ }
package/src/api.ts ADDED
@@ -0,0 +1,403 @@
1
+ import type { CliConfig } from "./config";
2
+ import type { CliCredentials } from "./credentials";
3
+
4
+ const DEVICE_AUTH_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
5
+
6
+ export type CliJwk = Record<string, unknown> & {
7
+ kty?: string;
8
+ crv?: string;
9
+ x?: string;
10
+ d?: string;
11
+ };
12
+
13
+ export interface DeviceAuthorizationResponse {
14
+ device_code: string;
15
+ user_code: string;
16
+ verification_uri: string;
17
+ verification_uri_complete: string;
18
+ expires_in: number;
19
+ interval: number;
20
+ }
21
+
22
+ export interface LoginTokenResponse {
23
+ access_token: string;
24
+ refresh_token?: string;
25
+ token_type?: "Bearer";
26
+ expires_in?: number;
27
+ authentication_method?: string;
28
+ organization_id?: string | null;
29
+ user: {
30
+ id: string;
31
+ email: string;
32
+ first_name?: string | null;
33
+ last_name?: string | null;
34
+ };
35
+ }
36
+
37
+ export interface PendingDeviceAuthorization {
38
+ status: "pending";
39
+ interval?: number;
40
+ }
41
+
42
+ export interface SlowDownDeviceAuthorization {
43
+ status: "slow_down";
44
+ interval?: number;
45
+ }
46
+
47
+ export interface DeveloperApp {
48
+ id: string;
49
+ packageName: string;
50
+ name: string;
51
+ description: string | null;
52
+ status: "active" | "archived" | "suspended";
53
+ activeRelease: DeveloperRelease | null;
54
+ latestRelease: DeveloperRelease | null;
55
+ releaseCount: number;
56
+ createdAt: string | null;
57
+ updatedAt: string | null;
58
+ }
59
+
60
+ export interface DeveloperRelease {
61
+ id: string;
62
+ version: string;
63
+ status: "draft" | "submitted" | "in_review" | "accepted" | "rejected" | "published" | "suspended";
64
+ releaseBundleAssetId: string | null;
65
+ bundleSha256: string | null;
66
+ bundleSizeBytes: number | null;
67
+ manifestSha256?: string | null;
68
+ signingKeyId?: string | null;
69
+ signedAt?: string | null;
70
+ reviewedBy?: string | null;
71
+ reviewNotes?: string | null;
72
+ createdAt: string | null;
73
+ updatedAt: string | null;
74
+ }
75
+
76
+ export interface DeveloperOrg {
77
+ id: string;
78
+ ownerUserId: string;
79
+ workosOrgId: string | null;
80
+ name: string;
81
+ packagePrefix: string;
82
+ packagePrefixStatus: "unverified" | "verified" | "rejected";
83
+ createdAt: string | null;
84
+ updatedAt: string | null;
85
+ }
86
+
87
+ export interface DeveloperSigningKey {
88
+ id: string;
89
+ orgId: string;
90
+ workosUserId: string;
91
+ publicKeyJwk: CliJwk;
92
+ status: "active" | "revoked";
93
+ lastUsedAt: string | null;
94
+ createdAt: string | null;
95
+ updatedAt: string | null;
96
+ }
97
+
98
+ export interface SignedBundleMetadata {
99
+ signingKeyId: string;
100
+ signature: string;
101
+ payload: {
102
+ packageName: string;
103
+ version: string;
104
+ bundleSha256: string;
105
+ manifestSha256: string;
106
+ createdAt: string;
107
+ };
108
+ }
109
+
110
+ export type PreinstallEnvironment = "debug" | "dev" | "staging" | "prod";
111
+ export type PreinstallPolicy = "install_once" | "keep_updated" | "mandatory";
112
+
113
+ export interface AdminUser {
114
+ developerId: string;
115
+ email: string;
116
+ }
117
+
118
+ export interface AdminRegistry {
119
+ id: string;
120
+ name: string;
121
+ environment: PreinstallEnvironment;
122
+ tenantId: string | null;
123
+ status: "draft" | "active" | "archived";
124
+ activeRevisionId: string | null;
125
+ createdAt: string | null;
126
+ updatedAt: string | null;
127
+ }
128
+
129
+ export interface AdminReleaseSummary {
130
+ id: string;
131
+ miniAppId?: string;
132
+ packageName: string;
133
+ displayName: string;
134
+ version: string;
135
+ status: DeveloperRelease["status"];
136
+ bundleSha256: string | null;
137
+ bundleSizeBytes: number | null;
138
+ createdAt: string | null;
139
+ }
140
+
141
+ export interface AdminRegistryRevision {
142
+ id: string;
143
+ registryId: string;
144
+ status: "draft" | "active" | "archived";
145
+ reason: string | null;
146
+ entries: Array<{
147
+ id: string;
148
+ miniAppId: string;
149
+ releaseId: string;
150
+ required: boolean;
151
+ installPolicy: PreinstallPolicy;
152
+ minMobileVersion: string | null;
153
+ maxMobileVersion: string | null;
154
+ priority: number;
155
+ }>;
156
+ promotedAt: string | null;
157
+ createdAt: string | null;
158
+ updatedAt: string | null;
159
+ }
160
+
161
+ export async function startLogin(config: CliConfig): Promise<DeviceAuthorizationResponse> {
162
+ assertWorkosClientId(config);
163
+ const body = new URLSearchParams({ client_id: config.workosClientId });
164
+ const response = await fetch(`${config.workosApiBaseUrl}/user_management/authorize/device`, {
165
+ method: "POST",
166
+ headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
167
+ body,
168
+ });
169
+ if (!response.ok) throw new Error(await errorMessage(response));
170
+ return (await response.json()) as DeviceAuthorizationResponse;
171
+ }
172
+
173
+ export async function pollLoginToken(
174
+ config: CliConfig,
175
+ deviceCode: string,
176
+ ): Promise<LoginTokenResponse | PendingDeviceAuthorization | SlowDownDeviceAuthorization> {
177
+ assertWorkosClientId(config);
178
+ const body = new URLSearchParams({
179
+ grant_type: DEVICE_AUTH_GRANT_TYPE,
180
+ device_code: deviceCode,
181
+ client_id: config.workosClientId,
182
+ });
183
+ const response = await fetch(`${config.workosApiBaseUrl}/user_management/authenticate`, {
184
+ method: "POST",
185
+ headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
186
+ body,
187
+ });
188
+ if (response.status === 400 || response.status === 403) {
189
+ const result = await response.json().catch(() => ({})) as { error?: string; error_description?: string };
190
+ if (result.error === "authorization_pending") return { status: "pending" };
191
+ if (result.error === "slow_down") return { status: "slow_down" };
192
+ throw new Error(result.error_description || result.error || `HTTP ${response.status}`);
193
+ }
194
+ if (!response.ok) throw new Error(await errorMessage(response));
195
+ return (await response.json()) as LoginTokenResponse;
196
+ }
197
+
198
+ export async function refreshLoginToken(
199
+ config: CliConfig,
200
+ refreshToken: string,
201
+ organizationId?: string | null,
202
+ ): Promise<LoginTokenResponse> {
203
+ assertWorkosClientId(config);
204
+ const body: Record<string, string> = {
205
+ client_id: config.workosClientId,
206
+ grant_type: "refresh_token",
207
+ refresh_token: refreshToken,
208
+ };
209
+ if (organizationId) body.organization_id = organizationId;
210
+
211
+ const response = await fetch(`${config.workosApiBaseUrl}/user_management/authenticate`, {
212
+ method: "POST",
213
+ headers: { "content-type": "application/json", accept: "application/json" },
214
+ body: JSON.stringify(body),
215
+ });
216
+ if (!response.ok) throw new Error(await errorMessage(response));
217
+ return (await response.json()) as LoginTokenResponse;
218
+ }
219
+
220
+ export async function listApps(credentials: CliCredentials): Promise<{ apps: DeveloperApp[] }> {
221
+ return coreRequest(credentials, "/api/console/apps");
222
+ }
223
+
224
+ export async function getAdminMe(credentials: CliCredentials): Promise<{ authenticated: true; admin: true; user: AdminUser | null }> {
225
+ return coreRequest(credentials, "/api/admin/me");
226
+ }
227
+
228
+ export async function listAdminRegistries(credentials: CliCredentials): Promise<{ registries: AdminRegistry[] }> {
229
+ return coreRequest(credentials, "/api/admin/preinstalled/registries");
230
+ }
231
+
232
+ export async function ensureAdminRegistry(
233
+ credentials: CliCredentials,
234
+ input: { environment: PreinstallEnvironment; name?: string; tenantId?: string | null },
235
+ ): Promise<{ registry: AdminRegistry }> {
236
+ return coreRequest(credentials, "/api/admin/preinstalled/registries", {
237
+ method: "POST",
238
+ body: JSON.stringify(input),
239
+ });
240
+ }
241
+
242
+ export async function listAdminPreinstallReleases(
243
+ credentials: CliCredentials,
244
+ ): Promise<{ releases: AdminReleaseSummary[] }> {
245
+ return coreRequest(credentials, "/api/admin/preinstalled/releases");
246
+ }
247
+
248
+ export async function listAdminRegistryRevisions(
249
+ credentials: CliCredentials,
250
+ registryId: string,
251
+ ): Promise<{ revisions: AdminRegistryRevision[] }> {
252
+ return coreRequest(credentials, `/api/admin/preinstalled/registries/${encodeURIComponent(registryId)}/revisions`);
253
+ }
254
+
255
+ export async function createAdminRegistryRevision(
256
+ credentials: CliCredentials,
257
+ registryId: string,
258
+ input: {
259
+ reason?: string | null;
260
+ entries: Array<{
261
+ releaseId: string;
262
+ required?: boolean;
263
+ installPolicy?: PreinstallPolicy;
264
+ minMobileVersion?: string | null;
265
+ maxMobileVersion?: string | null;
266
+ priority?: number;
267
+ }>;
268
+ },
269
+ ): Promise<{ revision: AdminRegistryRevision }> {
270
+ return coreRequest(credentials, `/api/admin/preinstalled/registries/${encodeURIComponent(registryId)}/revisions`, {
271
+ method: "POST",
272
+ body: JSON.stringify(input),
273
+ });
274
+ }
275
+
276
+ export async function promoteAdminRegistryRevision(
277
+ credentials: CliCredentials,
278
+ registryId: string,
279
+ revisionId: string,
280
+ ): Promise<{ registry: AdminRegistry; revision: AdminRegistryRevision }> {
281
+ return coreRequest(
282
+ credentials,
283
+ `/api/admin/preinstalled/registries/${encodeURIComponent(registryId)}/revisions/${encodeURIComponent(revisionId)}/promote`,
284
+ { method: "POST" },
285
+ );
286
+ }
287
+
288
+ export async function getOrg(credentials: CliCredentials): Promise<{ org: DeveloperOrg | null }> {
289
+ return coreRequest(credentials, "/api/console/org");
290
+ }
291
+
292
+ export async function upsertOrg(
293
+ credentials: CliCredentials,
294
+ input: { displayName: string; packagePrefix: string },
295
+ ): Promise<{ org: DeveloperOrg }> {
296
+ return coreRequest(credentials, "/api/console/org", {
297
+ method: "PUT",
298
+ body: JSON.stringify(input),
299
+ });
300
+ }
301
+
302
+ export async function createApp(
303
+ credentials: CliCredentials,
304
+ input: { packageName: string; displayName: string; description?: string | null },
305
+ ): Promise<{ app: DeveloperApp }> {
306
+ return coreRequest(credentials, "/api/console/apps", {
307
+ method: "POST",
308
+ body: JSON.stringify(input),
309
+ });
310
+ }
311
+
312
+ export async function deleteApp(credentials: CliCredentials, packageName: string): Promise<{ ok: true }> {
313
+ return coreRequest(credentials, `/api/console/apps/${encodeURIComponent(packageName)}`, {
314
+ method: "DELETE",
315
+ });
316
+ }
317
+
318
+ export async function listReleases(
319
+ credentials: CliCredentials,
320
+ packageName: string,
321
+ ): Promise<{ releases: DeveloperRelease[] }> {
322
+ return coreRequest(credentials, `/api/console/apps/${encodeURIComponent(packageName)}/releases`);
323
+ }
324
+
325
+ export async function createRelease(
326
+ credentials: CliCredentials,
327
+ input: {
328
+ packageName: string;
329
+ version: string;
330
+ manifest: Record<string, unknown>;
331
+ bundleBase64: string;
332
+ fileName?: string;
333
+ signedBundle: SignedBundleMetadata;
334
+ },
335
+ ): Promise<{ release: DeveloperRelease }> {
336
+ return coreRequest(credentials, `/api/console/apps/${encodeURIComponent(input.packageName)}/releases`, {
337
+ method: "POST",
338
+ body: JSON.stringify(input),
339
+ });
340
+ }
341
+
342
+ export async function listSigningKeys(credentials: CliCredentials): Promise<{ keys: DeveloperSigningKey[] }> {
343
+ return coreRequest(credentials, "/api/console/signing-keys");
344
+ }
345
+
346
+ export async function registerSigningKey(
347
+ credentials: CliCredentials,
348
+ input: { publicKeyJwk: CliJwk },
349
+ ): Promise<{ key: DeveloperSigningKey }> {
350
+ return coreRequest(credentials, "/api/console/signing-keys", {
351
+ method: "POST",
352
+ body: JSON.stringify(input),
353
+ });
354
+ }
355
+
356
+ export async function submitRelease(
357
+ credentials: CliCredentials,
358
+ input: {
359
+ packageName: string;
360
+ releaseId: string;
361
+ },
362
+ ): Promise<{ release: DeveloperRelease }> {
363
+ return coreRequest(
364
+ credentials,
365
+ `/api/console/apps/${encodeURIComponent(input.packageName)}/releases/${encodeURIComponent(input.releaseId)}/submit`,
366
+ { method: "POST" },
367
+ );
368
+ }
369
+
370
+ async function coreRequest<T>(
371
+ credentials: CliCredentials,
372
+ path: string,
373
+ init?: RequestInit,
374
+ ): Promise<T> {
375
+ const response = await fetch(`${credentials.coreUrl}${path}`, {
376
+ ...init,
377
+ headers: {
378
+ accept: "application/json",
379
+ authorization: `Bearer ${credentials.token}`,
380
+ ...(init?.body ? { "content-type": "application/json" } : {}),
381
+ ...init?.headers,
382
+ },
383
+ });
384
+ if (!response.ok) throw new Error(await errorMessage(response));
385
+ return await response.json() as T;
386
+ }
387
+
388
+ function assertWorkosClientId(config: CliConfig): void {
389
+ if (!config.workosClientId) {
390
+ throw new Error(
391
+ "WORKOS_CLIENT_ID is not configured. Add the public AuthKit client id to Doppler cloud-v2/dev as WORKOS_CLIENT_ID.",
392
+ );
393
+ }
394
+ }
395
+
396
+ async function errorMessage(response: Response): Promise<string> {
397
+ try {
398
+ const body = (await response.json()) as { error?: string; error_description?: string };
399
+ return body.error_description || body.error || `HTTP ${response.status}`;
400
+ } catch {
401
+ return `HTTP ${response.status}`;
402
+ }
403
+ }
package/src/config.ts ADDED
@@ -0,0 +1,24 @@
1
+ export interface CliConfig {
2
+ coreUrl: string;
3
+ consoleUrl: string;
4
+ workosClientId: string;
5
+ workosApiBaseUrl: string;
6
+ }
7
+
8
+ export function getConfig(): CliConfig {
9
+ const workosClientId = process.env.GLASSLY_WORKOS_CLIENT_ID || process.env.WORKOS_CLIENT_ID || "";
10
+ return {
11
+ coreUrl: normalizeUrl(process.env.GLASSLY_CORE_URL || "http://localhost:3000"),
12
+ consoleUrl: normalizeUrl(process.env.GLASSLY_CONSOLE_URL || "http://localhost:5173"),
13
+ workosClientId,
14
+ workosApiBaseUrl: normalizeUrl(process.env.WORKOS_API_BASE_URL || "https://api.workos.com"),
15
+ };
16
+ }
17
+
18
+ function normalizeUrl(value: string): string {
19
+ const url = new URL(value);
20
+ url.pathname = url.pathname.replace(/\/+$/, "");
21
+ url.search = "";
22
+ url.hash = "";
23
+ return url.toString().replace(/\/$/, "");
24
+ }