@glassly/cli 2.0.0-beta.4
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 +64 -0
- package/bin/glassly.js +20 -0
- package/package.json +48 -0
- package/src/api.ts +469 -0
- package/src/config.ts +34 -0
- package/src/credentials.ts +216 -0
- package/src/index.ts +1200 -0
- package/src/open-browser.ts +41 -0
- package/src/signing.ts +109 -0
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# @glassly/cli
|
|
2
|
+
|
|
3
|
+
The `glassly` command: the one CLI for developing, releasing, and publishing
|
|
4
|
+
Glassly miniapps. Scaffolded projects install it as a dev dependency, so
|
|
5
|
+
`bun run dev`, `bun run release`, and `bun run pack` call it directly.
|
|
6
|
+
|
|
7
|
+
The project-side commands (`dev`, `build`, `pack`, `release`, `manifest`,
|
|
8
|
+
`permission`, `hardware`, `schema`) are implemented in
|
|
9
|
+
[`@glassly/miniapp-cli`](https://www.npmjs.com/package/@glassly/miniapp-cli),
|
|
10
|
+
which this package wraps. The account and Store commands (`login`, `org`,
|
|
11
|
+
`miniapps`, `releases`, `listing`, `media`, `publish`) talk to the Glassly
|
|
12
|
+
Developer Console.
|
|
13
|
+
|
|
14
|
+
> **Bun-only.** This CLI ships as TypeScript and runs under [Bun](https://bun.sh)
|
|
15
|
+
> (`#!/usr/bin/env bun`). Use `bun` / `bunx`, not `npx`/Node.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bun add -g @glassly/cli
|
|
21
|
+
glassly --help
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or run without installing:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
bunx @glassly/cli --help
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Develop and test on a phone
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
glassly dev # hot-reload dev server + QR; signed dev identity when logged in
|
|
34
|
+
glassly release # build, pack, and serve a QR that installs on phones over LAN
|
|
35
|
+
glassly build # build both layers into dist/
|
|
36
|
+
glassly pack # production-build and zip into build/<pkg>-<version>.zip
|
|
37
|
+
glassly manifest # interactive miniapp.json wizard
|
|
38
|
+
glassly permission add MICROPHONE
|
|
39
|
+
glassly hardware add DISPLAY REQUIRED
|
|
40
|
+
glassly schema print # the miniapp.json JSON Schema
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`dev` and `release` take `--qr sm|md|lg` and `--qr-output <path>`. Every
|
|
44
|
+
project command takes `--cwd <path>` to run against another directory.
|
|
45
|
+
|
|
46
|
+
## Publish to the Glassly Miniapp Store
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
glassly login # sign in to the Developer Console (opens a browser)
|
|
50
|
+
glassly publish # build, pack, sign, upload, and submit for review
|
|
51
|
+
glassly miniapps list # miniapps owned by your org
|
|
52
|
+
glassly releases list <packageName>
|
|
53
|
+
glassly listing set <packageName> --tagline "..."
|
|
54
|
+
glassly media add <packageName> screenshot ./store/screenshots/01.png
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Run `glassly <command> --help` for the full option set.
|
|
58
|
+
|
|
59
|
+
## Backend selection
|
|
60
|
+
|
|
61
|
+
The CLI talks to the production Glassly backend and Developer Console by
|
|
62
|
+
default; nothing needs configuring. To point it at another deployment, set
|
|
63
|
+
`CLOUD_CORE_URL`, `CLOUD_CONSOLE_URL`, and that backend's public WorkOS
|
|
64
|
+
AuthKit client id in `WORKOS_CLIENT_ID`. Logins are stored per backend URL.
|
package/bin/glassly.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The CLI runs under Bun: the dev server, bundler plugins, and credential
|
|
3
|
+
// store use Bun APIs. This launcher lets `npx @glassly/cli` and plain `node`
|
|
4
|
+
// still work when Bun is installed, and explains what to do when it is not.
|
|
5
|
+
import {spawnSync} from "node:child_process";
|
|
6
|
+
import {fileURLToPath} from "node:url";
|
|
7
|
+
|
|
8
|
+
const entry = fileURLToPath(new URL("../src/index.ts", import.meta.url));
|
|
9
|
+
|
|
10
|
+
if (process.versions.bun) {
|
|
11
|
+
await import(entry);
|
|
12
|
+
} else {
|
|
13
|
+
const result = spawnSync("bun", [entry, ...process.argv.slice(2)], {stdio: "inherit"});
|
|
14
|
+
if (result.error && result.error.code === "ENOENT") {
|
|
15
|
+
console.error("glassly runs on Bun, which is not installed or not on PATH.");
|
|
16
|
+
console.error("Install it from https://bun.sh, then run: bunx @glassly/cli --help");
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
process.exit(result.status ?? 1);
|
|
20
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@glassly/cli",
|
|
3
|
+
"version": "2.0.0-beta.4",
|
|
4
|
+
"description": "The glassly command: develop, pack, release, and publish Glassly miniapps. Bun-only.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"glassly": "./bin/glassly.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"bun": ">=1.0.0"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public",
|
|
18
|
+
"tag": "dev"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"dev": "bun src/index.ts",
|
|
22
|
+
"typecheck": "tsc --noEmit"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@glassly/miniapp-cli": "0.1.0-beta.4",
|
|
26
|
+
"commander": "^14.0.2",
|
|
27
|
+
"qrcode": "^1.5.4"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/bun": "1.3.14",
|
|
31
|
+
"@types/qrcode": "^1.5.5"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"glassly",
|
|
35
|
+
"glassly",
|
|
36
|
+
"cli",
|
|
37
|
+
"miniapp"
|
|
38
|
+
],
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/tetramo-labs/glassly.git",
|
|
42
|
+
"directory": "sdk/cli"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/tetramo-labs/glassly/tree/dev/sdk/cli#readme",
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/tetramo-labs/glassly/issues"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
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 type MediaRole = "store_icon" | "gallery_screenshot";
|
|
77
|
+
|
|
78
|
+
export interface MediaUpload {
|
|
79
|
+
role: MediaRole;
|
|
80
|
+
fileName: string;
|
|
81
|
+
contentType: string;
|
|
82
|
+
dataBase64: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface MediaAsset {
|
|
86
|
+
id: string;
|
|
87
|
+
role: MediaRole;
|
|
88
|
+
fileName: string;
|
|
89
|
+
contentType: string;
|
|
90
|
+
sizeBytes: number;
|
|
91
|
+
sha256: string;
|
|
92
|
+
width: number | null;
|
|
93
|
+
height: number | null;
|
|
94
|
+
sortOrder: number | null;
|
|
95
|
+
releaseId: string | null;
|
|
96
|
+
createdAt: string | null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ListingOverride {
|
|
100
|
+
tagline: string | null;
|
|
101
|
+
description: string | null;
|
|
102
|
+
tags: string[] | null;
|
|
103
|
+
featured: boolean;
|
|
104
|
+
featuredAt: string | null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface DeveloperOrg {
|
|
108
|
+
id: string;
|
|
109
|
+
ownerUserId: string;
|
|
110
|
+
workosOrgId: string | null;
|
|
111
|
+
name: string;
|
|
112
|
+
packagePrefix: string;
|
|
113
|
+
packagePrefixStatus: "unverified" | "verified" | "rejected";
|
|
114
|
+
createdAt: string | null;
|
|
115
|
+
updatedAt: string | null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface DeveloperSigningKey {
|
|
119
|
+
id: string;
|
|
120
|
+
orgId: string;
|
|
121
|
+
workosUserId: string;
|
|
122
|
+
publicKeyJwk: CliJwk;
|
|
123
|
+
status: "active" | "revoked";
|
|
124
|
+
lastUsedAt: string | null;
|
|
125
|
+
createdAt: string | null;
|
|
126
|
+
updatedAt: string | null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface SignedBundleMetadata {
|
|
130
|
+
signingKeyId: string;
|
|
131
|
+
signature: string;
|
|
132
|
+
payload: {
|
|
133
|
+
packageName: string;
|
|
134
|
+
version: string;
|
|
135
|
+
bundleSha256: string;
|
|
136
|
+
manifestSha256: string;
|
|
137
|
+
createdAt: string;
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export type PreinstallEnvironment = "debug" | "dev" | "staging" | "prod";
|
|
142
|
+
export type PreinstallPolicy = "install_once" | "keep_updated" | "mandatory";
|
|
143
|
+
|
|
144
|
+
export interface AdminUser {
|
|
145
|
+
developerId: string;
|
|
146
|
+
email: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface AdminRegistry {
|
|
150
|
+
id: string;
|
|
151
|
+
name: string;
|
|
152
|
+
environment: PreinstallEnvironment;
|
|
153
|
+
tenantId: string | null;
|
|
154
|
+
status: "draft" | "active" | "archived";
|
|
155
|
+
activeRevisionId: string | null;
|
|
156
|
+
createdAt: string | null;
|
|
157
|
+
updatedAt: string | null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface AdminReleaseSummary {
|
|
161
|
+
id: string;
|
|
162
|
+
miniAppId?: string;
|
|
163
|
+
packageName: string;
|
|
164
|
+
displayName: string;
|
|
165
|
+
version: string;
|
|
166
|
+
status: DeveloperRelease["status"];
|
|
167
|
+
bundleSha256: string | null;
|
|
168
|
+
bundleSizeBytes: number | null;
|
|
169
|
+
createdAt: string | null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface AdminRegistryRevision {
|
|
173
|
+
id: string;
|
|
174
|
+
registryId: string;
|
|
175
|
+
status: "draft" | "active" | "archived";
|
|
176
|
+
reason: string | null;
|
|
177
|
+
entries: Array<{
|
|
178
|
+
id: string;
|
|
179
|
+
miniAppId: string;
|
|
180
|
+
releaseId: string;
|
|
181
|
+
required: boolean;
|
|
182
|
+
installPolicy: PreinstallPolicy;
|
|
183
|
+
minMobileVersion: string | null;
|
|
184
|
+
maxMobileVersion: string | null;
|
|
185
|
+
priority: number;
|
|
186
|
+
}>;
|
|
187
|
+
promotedAt: string | null;
|
|
188
|
+
createdAt: string | null;
|
|
189
|
+
updatedAt: string | null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function startLogin(config: CliConfig): Promise<DeviceAuthorizationResponse> {
|
|
193
|
+
assertWorkosClientId(config);
|
|
194
|
+
const body = new URLSearchParams({ client_id: config.workosClientId });
|
|
195
|
+
const response = await fetch(`${config.workosApiBaseUrl}/user_management/authorize/device`, {
|
|
196
|
+
method: "POST",
|
|
197
|
+
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
|
|
198
|
+
body,
|
|
199
|
+
});
|
|
200
|
+
if (!response.ok) throw new Error(await errorMessage(response));
|
|
201
|
+
return (await response.json()) as DeviceAuthorizationResponse;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function pollLoginToken(
|
|
205
|
+
config: CliConfig,
|
|
206
|
+
deviceCode: string,
|
|
207
|
+
): Promise<LoginTokenResponse | PendingDeviceAuthorization | SlowDownDeviceAuthorization> {
|
|
208
|
+
assertWorkosClientId(config);
|
|
209
|
+
const body = new URLSearchParams({
|
|
210
|
+
grant_type: DEVICE_AUTH_GRANT_TYPE,
|
|
211
|
+
device_code: deviceCode,
|
|
212
|
+
client_id: config.workosClientId,
|
|
213
|
+
});
|
|
214
|
+
const response = await fetch(`${config.workosApiBaseUrl}/user_management/authenticate`, {
|
|
215
|
+
method: "POST",
|
|
216
|
+
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
|
|
217
|
+
body,
|
|
218
|
+
});
|
|
219
|
+
if (response.status === 400 || response.status === 403) {
|
|
220
|
+
const result = await response.json().catch(() => ({})) as { error?: string; error_description?: string };
|
|
221
|
+
if (result.error === "authorization_pending") return { status: "pending" };
|
|
222
|
+
if (result.error === "slow_down") return { status: "slow_down" };
|
|
223
|
+
throw new Error(result.error_description || result.error || `HTTP ${response.status}`);
|
|
224
|
+
}
|
|
225
|
+
if (!response.ok) throw new Error(await errorMessage(response));
|
|
226
|
+
return (await response.json()) as LoginTokenResponse;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export async function refreshLoginToken(
|
|
230
|
+
config: CliConfig,
|
|
231
|
+
refreshToken: string,
|
|
232
|
+
organizationId?: string | null,
|
|
233
|
+
): Promise<LoginTokenResponse> {
|
|
234
|
+
assertWorkosClientId(config);
|
|
235
|
+
const body: Record<string, string> = {
|
|
236
|
+
client_id: config.workosClientId,
|
|
237
|
+
grant_type: "refresh_token",
|
|
238
|
+
refresh_token: refreshToken,
|
|
239
|
+
};
|
|
240
|
+
if (organizationId) body.organization_id = organizationId;
|
|
241
|
+
|
|
242
|
+
const response = await fetch(`${config.workosApiBaseUrl}/user_management/authenticate`, {
|
|
243
|
+
method: "POST",
|
|
244
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
245
|
+
body: JSON.stringify(body),
|
|
246
|
+
});
|
|
247
|
+
if (!response.ok) throw new Error(await errorMessage(response));
|
|
248
|
+
return (await response.json()) as LoginTokenResponse;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function listApps(credentials: CliCredentials): Promise<{ apps: DeveloperApp[] }> {
|
|
252
|
+
return coreRequest(credentials, "/api/console/apps");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export async function getAdminMe(credentials: CliCredentials): Promise<{ authenticated: true; admin: true; user: AdminUser | null }> {
|
|
256
|
+
return coreRequest(credentials, "/api/admin/me");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export async function listAdminRegistries(credentials: CliCredentials): Promise<{ registries: AdminRegistry[] }> {
|
|
260
|
+
return coreRequest(credentials, "/api/admin/preinstalled/registries");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export async function ensureAdminRegistry(
|
|
264
|
+
credentials: CliCredentials,
|
|
265
|
+
input: { environment: PreinstallEnvironment; name?: string; tenantId?: string | null },
|
|
266
|
+
): Promise<{ registry: AdminRegistry }> {
|
|
267
|
+
return coreRequest(credentials, "/api/admin/preinstalled/registries", {
|
|
268
|
+
method: "POST",
|
|
269
|
+
body: JSON.stringify(input),
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export async function listAdminPreinstallReleases(
|
|
274
|
+
credentials: CliCredentials,
|
|
275
|
+
): Promise<{ releases: AdminReleaseSummary[] }> {
|
|
276
|
+
return coreRequest(credentials, "/api/admin/preinstalled/releases");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export async function listAdminRegistryRevisions(
|
|
280
|
+
credentials: CliCredentials,
|
|
281
|
+
registryId: string,
|
|
282
|
+
): Promise<{ revisions: AdminRegistryRevision[] }> {
|
|
283
|
+
return coreRequest(credentials, `/api/admin/preinstalled/registries/${encodeURIComponent(registryId)}/revisions`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export async function createAdminRegistryRevision(
|
|
287
|
+
credentials: CliCredentials,
|
|
288
|
+
registryId: string,
|
|
289
|
+
input: {
|
|
290
|
+
reason?: string | null;
|
|
291
|
+
entries: Array<{
|
|
292
|
+
releaseId: string;
|
|
293
|
+
required?: boolean;
|
|
294
|
+
installPolicy?: PreinstallPolicy;
|
|
295
|
+
minMobileVersion?: string | null;
|
|
296
|
+
maxMobileVersion?: string | null;
|
|
297
|
+
priority?: number;
|
|
298
|
+
}>;
|
|
299
|
+
},
|
|
300
|
+
): Promise<{ revision: AdminRegistryRevision }> {
|
|
301
|
+
return coreRequest(credentials, `/api/admin/preinstalled/registries/${encodeURIComponent(registryId)}/revisions`, {
|
|
302
|
+
method: "POST",
|
|
303
|
+
body: JSON.stringify(input),
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export async function promoteAdminRegistryRevision(
|
|
308
|
+
credentials: CliCredentials,
|
|
309
|
+
registryId: string,
|
|
310
|
+
revisionId: string,
|
|
311
|
+
): Promise<{ registry: AdminRegistry; revision: AdminRegistryRevision }> {
|
|
312
|
+
return coreRequest(
|
|
313
|
+
credentials,
|
|
314
|
+
`/api/admin/preinstalled/registries/${encodeURIComponent(registryId)}/revisions/${encodeURIComponent(revisionId)}/promote`,
|
|
315
|
+
{ method: "POST" },
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export async function getOrg(credentials: CliCredentials): Promise<{ org: DeveloperOrg | null }> {
|
|
320
|
+
return coreRequest(credentials, "/api/console/org");
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export async function upsertOrg(
|
|
324
|
+
credentials: CliCredentials,
|
|
325
|
+
input: { displayName: string; packagePrefix: string },
|
|
326
|
+
): Promise<{ org: DeveloperOrg }> {
|
|
327
|
+
return coreRequest(credentials, "/api/console/org", {
|
|
328
|
+
method: "PUT",
|
|
329
|
+
body: JSON.stringify(input),
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export async function createApp(
|
|
334
|
+
credentials: CliCredentials,
|
|
335
|
+
input: { packageName: string; displayName: string; description?: string | null },
|
|
336
|
+
): Promise<{ app: DeveloperApp }> {
|
|
337
|
+
return coreRequest(credentials, "/api/console/apps", {
|
|
338
|
+
method: "POST",
|
|
339
|
+
body: JSON.stringify(input),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export async function deleteApp(credentials: CliCredentials, packageName: string): Promise<{ ok: true }> {
|
|
344
|
+
return coreRequest(credentials, `/api/console/apps/${encodeURIComponent(packageName)}`, {
|
|
345
|
+
method: "DELETE",
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export async function listReleases(
|
|
350
|
+
credentials: CliCredentials,
|
|
351
|
+
packageName: string,
|
|
352
|
+
): Promise<{ releases: DeveloperRelease[] }> {
|
|
353
|
+
return coreRequest(credentials, `/api/console/apps/${encodeURIComponent(packageName)}/releases`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export async function updateListing(
|
|
357
|
+
credentials: CliCredentials,
|
|
358
|
+
packageName: string,
|
|
359
|
+
input: { tagline?: string | null; description?: string | null; tags?: string[] | null },
|
|
360
|
+
): Promise<{ listing: ListingOverride }> {
|
|
361
|
+
return coreRequest(credentials, `/api/console/apps/${encodeURIComponent(packageName)}/listing`, {
|
|
362
|
+
method: "PUT",
|
|
363
|
+
body: JSON.stringify(input),
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export async function listMedia(credentials: CliCredentials, packageName: string): Promise<{ media: MediaAsset[] }> {
|
|
368
|
+
return coreRequest(credentials, `/api/console/apps/${encodeURIComponent(packageName)}/media`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export async function addMedia(
|
|
372
|
+
credentials: CliCredentials,
|
|
373
|
+
packageName: string,
|
|
374
|
+
input: MediaUpload,
|
|
375
|
+
): Promise<{ asset: MediaAsset }> {
|
|
376
|
+
return coreRequest(credentials, `/api/console/apps/${encodeURIComponent(packageName)}/media`, {
|
|
377
|
+
method: "POST",
|
|
378
|
+
body: JSON.stringify(input),
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export async function deleteMedia(credentials: CliCredentials, packageName: string, assetId: string): Promise<{ ok: true }> {
|
|
383
|
+
return coreRequest(
|
|
384
|
+
credentials,
|
|
385
|
+
`/api/console/apps/${encodeURIComponent(packageName)}/media/${encodeURIComponent(assetId)}`,
|
|
386
|
+
{ method: "DELETE" },
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export async function createRelease(
|
|
391
|
+
credentials: CliCredentials,
|
|
392
|
+
input: {
|
|
393
|
+
packageName: string;
|
|
394
|
+
version: string;
|
|
395
|
+
manifest: Record<string, unknown>;
|
|
396
|
+
bundleBase64: string;
|
|
397
|
+
fileName?: string;
|
|
398
|
+
signedBundle: SignedBundleMetadata;
|
|
399
|
+
media?: MediaUpload[];
|
|
400
|
+
},
|
|
401
|
+
): Promise<{ release: DeveloperRelease }> {
|
|
402
|
+
return coreRequest(credentials, `/api/console/apps/${encodeURIComponent(input.packageName)}/releases`, {
|
|
403
|
+
method: "POST",
|
|
404
|
+
body: JSON.stringify(input),
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export async function listSigningKeys(credentials: CliCredentials): Promise<{ keys: DeveloperSigningKey[] }> {
|
|
409
|
+
return coreRequest(credentials, "/api/console/signing-keys");
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export async function registerSigningKey(
|
|
413
|
+
credentials: CliCredentials,
|
|
414
|
+
input: { publicKeyJwk: CliJwk },
|
|
415
|
+
): Promise<{ key: DeveloperSigningKey }> {
|
|
416
|
+
return coreRequest(credentials, "/api/console/signing-keys", {
|
|
417
|
+
method: "POST",
|
|
418
|
+
body: JSON.stringify(input),
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export async function submitRelease(
|
|
423
|
+
credentials: CliCredentials,
|
|
424
|
+
input: {
|
|
425
|
+
packageName: string;
|
|
426
|
+
releaseId: string;
|
|
427
|
+
},
|
|
428
|
+
): Promise<{ release: DeveloperRelease }> {
|
|
429
|
+
return coreRequest(
|
|
430
|
+
credentials,
|
|
431
|
+
`/api/console/apps/${encodeURIComponent(input.packageName)}/releases/${encodeURIComponent(input.releaseId)}/submit`,
|
|
432
|
+
{ method: "POST" },
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function coreRequest<T>(
|
|
437
|
+
credentials: CliCredentials,
|
|
438
|
+
path: string,
|
|
439
|
+
init?: RequestInit,
|
|
440
|
+
): Promise<T> {
|
|
441
|
+
const response = await fetch(`${credentials.coreUrl}${path}`, {
|
|
442
|
+
...init,
|
|
443
|
+
headers: {
|
|
444
|
+
accept: "application/json",
|
|
445
|
+
authorization: `Bearer ${credentials.token}`,
|
|
446
|
+
...(init?.body ? { "content-type": "application/json" } : {}),
|
|
447
|
+
...init?.headers,
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
if (!response.ok) throw new Error(await errorMessage(response));
|
|
451
|
+
return await response.json() as T;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function assertWorkosClientId(config: CliConfig): void {
|
|
455
|
+
if (!config.workosClientId) {
|
|
456
|
+
throw new Error(
|
|
457
|
+
"WORKOS_CLIENT_ID is set but empty. Unset it to use the production login, or set it to the AuthKit client id of the backend in CLOUD_CORE_URL.",
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async function errorMessage(response: Response): Promise<string> {
|
|
463
|
+
try {
|
|
464
|
+
const body = (await response.json()) as { error?: string; error_description?: string };
|
|
465
|
+
return body.error_description || body.error || `HTTP ${response.status}`;
|
|
466
|
+
} catch {
|
|
467
|
+
return `HTTP ${response.status}`;
|
|
468
|
+
}
|
|
469
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface CliConfig {
|
|
2
|
+
coreUrl: string;
|
|
3
|
+
consoleUrl: string;
|
|
4
|
+
workosClientId: string;
|
|
5
|
+
workosApiBaseUrl: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Production is the default so a developer can run `glassly login` with no
|
|
9
|
+
// setup. The WorkOS client id is the public AuthKit identifier of the
|
|
10
|
+
// production environment, not a secret. Internal use against dev or staging
|
|
11
|
+
// overrides all three through the environment (see the glassly:* scripts in
|
|
12
|
+
// cloud-v2/package.json, which take them from Doppler).
|
|
13
|
+
const PRODUCTION = {
|
|
14
|
+
coreUrl: "https://core.glassly.ai",
|
|
15
|
+
consoleUrl: "https://console.glassly.ai",
|
|
16
|
+
workosClientId: "client_01KY5EVQT1PE8V11ZKZ1Q56XTK",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function getConfig(): CliConfig {
|
|
20
|
+
return {
|
|
21
|
+
coreUrl: normalizeUrl(process.env.CLOUD_CORE_URL || PRODUCTION.coreUrl),
|
|
22
|
+
consoleUrl: normalizeUrl(process.env.CLOUD_CONSOLE_URL || PRODUCTION.consoleUrl),
|
|
23
|
+
workosClientId: process.env.WORKOS_CLIENT_ID || PRODUCTION.workosClientId,
|
|
24
|
+
workosApiBaseUrl: normalizeUrl(process.env.WORKOS_API_BASE_URL || "https://api.workos.com"),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeUrl(value: string): string {
|
|
29
|
+
const url = new URL(value);
|
|
30
|
+
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
31
|
+
url.search = "";
|
|
32
|
+
url.hash = "";
|
|
33
|
+
return url.toString().replace(/\/$/, "");
|
|
34
|
+
}
|