@glassly/cli 2.0.0-dev.0 → 2.0.0-dev.2
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 +41 -15
- package/package.json +3 -3
- package/src/api.ts +67 -1
- package/src/config.ts +14 -4
- package/src/credentials.ts +8 -8
- package/src/index.ts +353 -24
package/README.md
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
# @glassly/cli
|
|
2
2
|
|
|
3
|
-
The
|
|
4
|
-
Glassly miniapps
|
|
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.
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
`
|
|
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.
|
|
9
13
|
|
|
10
14
|
> **Bun-only.** This CLI ships as TypeScript and runs under [Bun](https://bun.sh)
|
|
11
15
|
> (`#!/usr/bin/env bun`). Use `bun` / `bunx`, not `npx`/Node.
|
|
@@ -13,26 +17,48 @@ It wraps [`@glassly/miniapp-cli`](https://www.npmjs.com/package/@glassly/miniapp
|
|
|
13
17
|
## Install
|
|
14
18
|
|
|
15
19
|
```bash
|
|
16
|
-
bun add -g @glassly/cli
|
|
20
|
+
bun add -g @glassly/cli
|
|
17
21
|
glassly --help
|
|
18
22
|
```
|
|
19
23
|
|
|
20
24
|
Or run without installing:
|
|
21
25
|
|
|
22
26
|
```bash
|
|
23
|
-
bunx @glassly/cli
|
|
27
|
+
bunx @glassly/cli --help
|
|
24
28
|
```
|
|
25
29
|
|
|
26
|
-
##
|
|
30
|
+
## Develop and test on a phone
|
|
27
31
|
|
|
28
32
|
```bash
|
|
29
|
-
glassly
|
|
30
|
-
glassly
|
|
31
|
-
glassly build
|
|
32
|
-
glassly pack
|
|
33
|
-
glassly
|
|
34
|
-
glassly
|
|
35
|
-
glassly
|
|
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
|
|
36
55
|
```
|
|
37
56
|
|
|
38
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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glassly/cli",
|
|
3
|
-
"version": "2.0.0-dev.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.0.0-dev.2",
|
|
4
|
+
"description": "The glassly command: develop, pack, release, and publish Glassly miniapps. Bun-only.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"glassly": "./src/index.ts"
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"typecheck": "tsc --noEmit"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@glassly/miniapp-cli": "0.1.0-dev.
|
|
24
|
+
"@glassly/miniapp-cli": "0.1.0-dev.2",
|
|
25
25
|
"commander": "^14.0.2",
|
|
26
26
|
"qrcode": "^1.5.4"
|
|
27
27
|
},
|
package/src/api.ts
CHANGED
|
@@ -73,6 +73,37 @@ export interface DeveloperRelease {
|
|
|
73
73
|
updatedAt: string | null;
|
|
74
74
|
}
|
|
75
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
|
+
|
|
76
107
|
export interface DeveloperOrg {
|
|
77
108
|
id: string;
|
|
78
109
|
ownerUserId: string;
|
|
@@ -322,6 +353,40 @@ export async function listReleases(
|
|
|
322
353
|
return coreRequest(credentials, `/api/console/apps/${encodeURIComponent(packageName)}/releases`);
|
|
323
354
|
}
|
|
324
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
|
+
|
|
325
390
|
export async function createRelease(
|
|
326
391
|
credentials: CliCredentials,
|
|
327
392
|
input: {
|
|
@@ -331,6 +396,7 @@ export async function createRelease(
|
|
|
331
396
|
bundleBase64: string;
|
|
332
397
|
fileName?: string;
|
|
333
398
|
signedBundle: SignedBundleMetadata;
|
|
399
|
+
media?: MediaUpload[];
|
|
334
400
|
},
|
|
335
401
|
): Promise<{ release: DeveloperRelease }> {
|
|
336
402
|
return coreRequest(credentials, `/api/console/apps/${encodeURIComponent(input.packageName)}/releases`, {
|
|
@@ -388,7 +454,7 @@ async function coreRequest<T>(
|
|
|
388
454
|
function assertWorkosClientId(config: CliConfig): void {
|
|
389
455
|
if (!config.workosClientId) {
|
|
390
456
|
throw new Error(
|
|
391
|
-
"WORKOS_CLIENT_ID is
|
|
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.",
|
|
392
458
|
);
|
|
393
459
|
}
|
|
394
460
|
}
|
package/src/config.ts
CHANGED
|
@@ -5,12 +5,22 @@ export interface CliConfig {
|
|
|
5
5
|
workosApiBaseUrl: string;
|
|
6
6
|
}
|
|
7
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
|
+
|
|
8
19
|
export function getConfig(): CliConfig {
|
|
9
|
-
const workosClientId = process.env.GLASSLY_WORKOS_CLIENT_ID || process.env.WORKOS_CLIENT_ID || "";
|
|
10
20
|
return {
|
|
11
|
-
coreUrl: normalizeUrl(process.env.
|
|
12
|
-
consoleUrl: normalizeUrl(process.env.
|
|
13
|
-
workosClientId,
|
|
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,
|
|
14
24
|
workosApiBaseUrl: normalizeUrl(process.env.WORKOS_API_BASE_URL || "https://api.workos.com"),
|
|
15
25
|
};
|
|
16
26
|
}
|
package/src/credentials.ts
CHANGED
|
@@ -4,8 +4,8 @@ import { join } from "node:path";
|
|
|
4
4
|
|
|
5
5
|
const SERVICE = "glassly-cli-v2";
|
|
6
6
|
const LEGACY_NAME = "credentials";
|
|
7
|
-
const
|
|
8
|
-
const CREDS_DIR = join(
|
|
7
|
+
const CONFIG_DIR = join(homedir(), ".glassly");
|
|
8
|
+
const CREDS_DIR = join(CONFIG_DIR, "cli-v2");
|
|
9
9
|
const LEGACY_CREDS_FILE = join(CREDS_DIR, "credentials.json");
|
|
10
10
|
const SIGNING_KEY_SERVICE = "glassly-cli-v2-signing";
|
|
11
11
|
|
|
@@ -60,13 +60,13 @@ export async function saveCredentials(credentials: CliCredentials): Promise<"key
|
|
|
60
60
|
|
|
61
61
|
export async function loadCredentials(coreUrl?: string): Promise<CliCredentials | null> {
|
|
62
62
|
const targetCoreUrl = coreUrl ? normalizeCoreUrl(coreUrl) : undefined;
|
|
63
|
-
if (process.env.
|
|
63
|
+
if (process.env.CLI_TOKEN) {
|
|
64
64
|
return {
|
|
65
|
-
token: process.env.
|
|
66
|
-
workosUserId: process.env.
|
|
67
|
-
email: process.env.
|
|
68
|
-
organizationId: process.env.
|
|
69
|
-
coreUrl: targetCoreUrl || process.env.
|
|
65
|
+
token: process.env.CLI_TOKEN,
|
|
66
|
+
workosUserId: process.env.CLI_WORKOS_USER_ID || "unknown",
|
|
67
|
+
email: process.env.CLI_EMAIL || "unknown",
|
|
68
|
+
organizationId: process.env.CLI_ORGANIZATION_ID,
|
|
69
|
+
coreUrl: targetCoreUrl || process.env.CLOUD_CORE_URL || "http://localhost:3000",
|
|
70
70
|
storedAt: new Date().toISOString(),
|
|
71
71
|
};
|
|
72
72
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,14 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
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
4
|
import {
|
|
5
|
+
addHardwareCmd,
|
|
6
|
+
addPermissionCmd,
|
|
7
|
+
buildProduction as buildMiniappProduction,
|
|
8
|
+
dev as devMiniapp,
|
|
9
|
+
listHardwareCmd,
|
|
10
|
+
listPermissionsCmd,
|
|
11
|
+
pack as packMiniapp,
|
|
12
|
+
regenerateSchemaFile,
|
|
13
|
+
release as releaseMiniapp,
|
|
14
|
+
removeHardwareCmd,
|
|
15
|
+
removePermissionCmd,
|
|
16
|
+
runManifestWizard,
|
|
17
|
+
schemaPrint,
|
|
18
|
+
type QRSize,
|
|
19
|
+
} from "@glassly/miniapp-cli";
|
|
20
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
21
|
+
import { basename, extname, join, resolve } from "node:path";
|
|
22
|
+
import {
|
|
23
|
+
addMedia,
|
|
8
24
|
createAdminRegistryRevision,
|
|
9
25
|
createApp,
|
|
10
26
|
createRelease,
|
|
11
27
|
deleteApp,
|
|
28
|
+
deleteMedia,
|
|
12
29
|
ensureAdminRegistry,
|
|
13
30
|
getAdminMe,
|
|
14
31
|
getOrg,
|
|
@@ -16,15 +33,19 @@ import {
|
|
|
16
33
|
listAdminRegistries,
|
|
17
34
|
listAdminRegistryRevisions,
|
|
18
35
|
listApps,
|
|
36
|
+
listMedia,
|
|
19
37
|
listReleases,
|
|
20
38
|
promoteAdminRegistryRevision,
|
|
21
39
|
pollLoginToken,
|
|
22
40
|
refreshLoginToken,
|
|
23
41
|
startLogin,
|
|
24
42
|
submitRelease,
|
|
43
|
+
updateListing,
|
|
25
44
|
upsertOrg,
|
|
26
45
|
type AdminReleaseSummary,
|
|
27
46
|
type AdminRegistry,
|
|
47
|
+
type MediaRole,
|
|
48
|
+
type MediaUpload,
|
|
28
49
|
type PreinstallEnvironment,
|
|
29
50
|
type PreinstallPolicy,
|
|
30
51
|
} from "./api";
|
|
@@ -33,12 +54,29 @@ import { clearCredentials, loadCredentials, saveCredentials, type CliCredentials
|
|
|
33
54
|
import { openBrowser } from "./open-browser";
|
|
34
55
|
import { encodeDevAttestation, ensureSigningKey, signBundleMetadata, signDevAttestation } from "./signing";
|
|
35
56
|
|
|
57
|
+
const cliVersion = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version as string;
|
|
58
|
+
|
|
59
|
+
const QR_SIZES: Record<string, QRSize> = {
|
|
60
|
+
sm: "small",
|
|
61
|
+
small: "small",
|
|
62
|
+
md: "medium",
|
|
63
|
+
medium: "medium",
|
|
64
|
+
lg: "large",
|
|
65
|
+
large: "large",
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
function parseQrSize(raw: string): QRSize {
|
|
69
|
+
const size = QR_SIZES[raw.toLowerCase()];
|
|
70
|
+
if (!size) throw new Error(`Invalid --qr size "${raw}" (expected small|medium|large or sm|md|lg)`);
|
|
71
|
+
return size;
|
|
72
|
+
}
|
|
73
|
+
|
|
36
74
|
const program = new Command();
|
|
37
75
|
|
|
38
76
|
program
|
|
39
77
|
.name("glassly")
|
|
40
78
|
.description("Glassly developer CLI")
|
|
41
|
-
.version(
|
|
79
|
+
.version(cliVersion);
|
|
42
80
|
|
|
43
81
|
program
|
|
44
82
|
.command("login")
|
|
@@ -283,6 +321,103 @@ releases
|
|
|
283
321
|
}
|
|
284
322
|
});
|
|
285
323
|
|
|
324
|
+
const listing = program.command("listing").description("Store listing copy (overrides the manifest)");
|
|
325
|
+
|
|
326
|
+
listing
|
|
327
|
+
.command("set")
|
|
328
|
+
.argument("<packageName>", "package name")
|
|
329
|
+
.option("--tagline <text>", "one line under the name (blank clears the override)")
|
|
330
|
+
.option("--description <text>", "store description (blank clears the override)")
|
|
331
|
+
.option("--tags <a,b,c>", "comma-separated lowercase tags (blank clears the override)")
|
|
332
|
+
.description("Set store listing overrides for a miniapp")
|
|
333
|
+
.action(async (packageName: string, options: { tagline?: string; description?: string; tags?: string }) => {
|
|
334
|
+
const creds = await requireCredentials();
|
|
335
|
+
if (!creds) return;
|
|
336
|
+
if (options.tagline === undefined && options.description === undefined && options.tags === undefined) {
|
|
337
|
+
fail(new Error("pass at least one of --tagline, --description, --tags"));
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
const input: { tagline?: string | null; description?: string | null; tags?: string[] | null } = {};
|
|
343
|
+
if (options.tagline !== undefined) input.tagline = options.tagline.trim() || null;
|
|
344
|
+
if (options.description !== undefined) input.description = options.description.trim() || null;
|
|
345
|
+
if (options.tags !== undefined) {
|
|
346
|
+
const tags = options.tags.split(",").map(tag => tag.trim()).filter(Boolean);
|
|
347
|
+
input.tags = tags.length > 0 ? tags : null;
|
|
348
|
+
}
|
|
349
|
+
const { listing: saved } = await updateListing(creds, packageName, input);
|
|
350
|
+
console.log(`Listing for ${packageName}`);
|
|
351
|
+
console.log(` tagline: ${saved.tagline ?? "(manifest)"}`);
|
|
352
|
+
console.log(` description: ${saved.description ?? "(manifest)"}`);
|
|
353
|
+
console.log(` tags: ${saved.tags ? saved.tags.join(", ") : "(manifest)"}`);
|
|
354
|
+
} catch (error) {
|
|
355
|
+
fail(error);
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const media = program.command("media").description("Store icon and screenshots");
|
|
360
|
+
|
|
361
|
+
media
|
|
362
|
+
.command("list")
|
|
363
|
+
.argument("<packageName>", "package name")
|
|
364
|
+
.description("List store media for a miniapp")
|
|
365
|
+
.action(async (packageName: string) => {
|
|
366
|
+
const creds = await requireCredentials();
|
|
367
|
+
if (!creds) return;
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
const { media: assets } = await listMedia(creds, packageName);
|
|
371
|
+
if (assets.length === 0) {
|
|
372
|
+
console.log("No store media yet.");
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
for (const asset of assets) {
|
|
376
|
+
const dims = asset.width && asset.height ? `${asset.width}x${asset.height}` : "?";
|
|
377
|
+
const origin = asset.role === "store_icon" ? (asset.releaseId ? "bundle" : "upload") : `#${asset.sortOrder ?? 0}`;
|
|
378
|
+
console.log(`${asset.id}\t${asset.role}\t${origin}\t${dims}\t${Math.round(asset.sizeBytes / 1024)} KB\t${asset.fileName}`);
|
|
379
|
+
}
|
|
380
|
+
} catch (error) {
|
|
381
|
+
fail(error);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
media
|
|
386
|
+
.command("add")
|
|
387
|
+
.argument("<packageName>", "package name")
|
|
388
|
+
.argument("<file>", "PNG or JPEG file")
|
|
389
|
+
.option("--role <role>", "store_icon or gallery_screenshot", "gallery_screenshot")
|
|
390
|
+
.description("Upload a store icon or screenshot")
|
|
391
|
+
.action(async (packageName: string, file: string, options: { role: string }) => {
|
|
392
|
+
const creds = await requireCredentials();
|
|
393
|
+
if (!creds) return;
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
const role = parseMediaRole(options.role);
|
|
397
|
+
const { asset } = await addMedia(creds, packageName, readMediaFile(resolve(file), role));
|
|
398
|
+
console.log(`Uploaded ${asset.role} ${asset.id} (${asset.width}x${asset.height}, ${Math.round(asset.sizeBytes / 1024)} KB)`);
|
|
399
|
+
} catch (error) {
|
|
400
|
+
fail(error);
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
media
|
|
405
|
+
.command("rm")
|
|
406
|
+
.argument("<packageName>", "package name")
|
|
407
|
+
.argument("<assetId>", "asset id from `media list`")
|
|
408
|
+
.description("Remove an uploaded icon or screenshot")
|
|
409
|
+
.action(async (packageName: string, assetId: string) => {
|
|
410
|
+
const creds = await requireCredentials();
|
|
411
|
+
if (!creds) return;
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
await deleteMedia(creds, packageName, assetId);
|
|
415
|
+
console.log(`Removed ${assetId}`);
|
|
416
|
+
} catch (error) {
|
|
417
|
+
fail(error);
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
|
|
286
421
|
const admin = program.command("admin").description("Internal Glassly admin operations");
|
|
287
422
|
|
|
288
423
|
admin
|
|
@@ -383,7 +518,7 @@ preinstall
|
|
|
383
518
|
for (const entry of active.entries) {
|
|
384
519
|
const release = byId.get(entry.releaseId);
|
|
385
520
|
const label = release ? `${release.packageName}@${release.version}` : entry.releaseId;
|
|
386
|
-
console.log(`- ${label}\t${entry.installPolicy}
|
|
521
|
+
console.log(`- ${label}\t${installModeLabel(entry.installPolicy)}`);
|
|
387
522
|
}
|
|
388
523
|
} catch (error) {
|
|
389
524
|
fail(error);
|
|
@@ -417,7 +552,7 @@ preinstall
|
|
|
417
552
|
if (active) {
|
|
418
553
|
console.log(` active revision: ${active.id}`);
|
|
419
554
|
for (const entry of active.entries) {
|
|
420
|
-
console.log(` - ${entry.releaseId}\t${entry.installPolicy}
|
|
555
|
+
console.log(` - ${entry.releaseId}\t${installModeLabel(entry.installPolicy)}`);
|
|
421
556
|
}
|
|
422
557
|
}
|
|
423
558
|
}
|
|
@@ -430,17 +565,15 @@ preinstall
|
|
|
430
565
|
preinstall
|
|
431
566
|
.command("publish")
|
|
432
567
|
.description("Replace the active preinstalled miniapp list with selected releases")
|
|
433
|
-
.
|
|
568
|
+
.option("-r, --release <release>", "release id or package@version installed by default: once, on phones that do not have it; repeat for multiple releases", collect, [])
|
|
569
|
+
.option("-m, --mandatory <release>", "release reinstalled whenever it is missing; repeat for multiple releases", collect, [])
|
|
434
570
|
.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
571
|
.option("--reason <reason>", "audit reason")
|
|
438
572
|
.option("--yes", "confirm prod registry replacement")
|
|
439
573
|
.action(async (options: {
|
|
440
574
|
release: string[];
|
|
575
|
+
mandatory: string[];
|
|
441
576
|
environment?: string;
|
|
442
|
-
policy: string;
|
|
443
|
-
required?: boolean;
|
|
444
577
|
reason?: string;
|
|
445
578
|
yes?: boolean;
|
|
446
579
|
}) => {
|
|
@@ -451,20 +584,27 @@ preinstall
|
|
|
451
584
|
const environment = options.environment
|
|
452
585
|
? parseEnvironment(options.environment)
|
|
453
586
|
: inferEnvironment(creds.coreUrl);
|
|
454
|
-
const installPolicy = parsePolicy(options.policy);
|
|
455
587
|
if (environment === "prod" && !options.yes) {
|
|
456
588
|
throw new Error("prod preinstall publish requires --yes");
|
|
457
589
|
}
|
|
458
590
|
|
|
591
|
+
if (options.release.length + options.mandatory.length === 0) {
|
|
592
|
+
throw new Error("give at least one --release or --mandatory");
|
|
593
|
+
}
|
|
594
|
+
|
|
459
595
|
const { releases: releaseList } = await listAdminPreinstallReleases(creds);
|
|
460
|
-
const selected = resolveReleaseRefs(options.release, releaseList);
|
|
596
|
+
const selected = resolveReleaseRefs([...options.release, ...options.mandatory], releaseList);
|
|
597
|
+
// The mandatory releases come last in `selected`.
|
|
598
|
+
const mandatoryIds = new Set(selected.slice(options.release.length).map(release => release.id));
|
|
599
|
+
const policyOf = (releaseId: string): PreinstallPolicy =>
|
|
600
|
+
mandatoryIds.has(releaseId) ? "mandatory" : "install_once";
|
|
461
601
|
const { registry } = await ensureAdminRegistry(creds, { environment });
|
|
462
602
|
const { revision } = await createAdminRegistryRevision(creds, registry.id, {
|
|
463
603
|
reason: options.reason ?? `Admin preinstall registry publish for ${environment}`,
|
|
464
604
|
entries: selected.map((release, index) => ({
|
|
465
605
|
releaseId: release.id,
|
|
466
|
-
required:
|
|
467
|
-
installPolicy,
|
|
606
|
+
required: true,
|
|
607
|
+
installPolicy: policyOf(release.id),
|
|
468
608
|
priority: index,
|
|
469
609
|
})),
|
|
470
610
|
});
|
|
@@ -474,7 +614,7 @@ preinstall
|
|
|
474
614
|
console.log(`Registry: ${promoted.registry.id}`);
|
|
475
615
|
console.log(`Revision: ${promoted.revision.id}`);
|
|
476
616
|
for (const release of selected) {
|
|
477
|
-
console.log(`- ${release.packageName}@${release.version}\t${release.id}`);
|
|
617
|
+
console.log(`- ${release.packageName}@${release.version}\t${release.id}\t${installModeLabel(policyOf(release.id))}`);
|
|
478
618
|
}
|
|
479
619
|
} catch (error) {
|
|
480
620
|
fail(error);
|
|
@@ -486,9 +626,12 @@ program
|
|
|
486
626
|
.description("Start the local miniapp dev server with signed Cloud V2 identity when logged in")
|
|
487
627
|
.option("--cwd <path>", "miniapp project directory", process.cwd())
|
|
488
628
|
.option("--auth", "require signed dev auto-auth setup before starting")
|
|
489
|
-
.
|
|
629
|
+
.option("-q, --qr <size>", "terminal QR size: small|medium|large (sm|md|lg)", "medium")
|
|
630
|
+
.option("--qr-output <path>", "also write the QR as a PNG at this path")
|
|
631
|
+
.action(async (options: { cwd: string; auth?: boolean; qr: string; qrOutput?: string }) => {
|
|
490
632
|
const cwd = resolve(options.cwd);
|
|
491
633
|
try {
|
|
634
|
+
const qrSize = parseQrSize(options.qr);
|
|
492
635
|
const manifest = readManifest(cwd);
|
|
493
636
|
const packageName = stringField(manifest, "packageName");
|
|
494
637
|
const name = stringField(manifest, "name") || packageName;
|
|
@@ -514,7 +657,7 @@ program
|
|
|
514
657
|
console.log("Dev auto-auth disabled. Run `glassly login` if this miniapp uses session.auth.");
|
|
515
658
|
}
|
|
516
659
|
|
|
517
|
-
await devMiniapp({ cwd, signDevAttestation: signer });
|
|
660
|
+
await devMiniapp({ cwd, qrSize, qrOutput: options.qrOutput, signDevAttestation: signer });
|
|
518
661
|
} catch (error) {
|
|
519
662
|
fail(error);
|
|
520
663
|
}
|
|
@@ -545,6 +688,142 @@ program
|
|
|
545
688
|
}
|
|
546
689
|
});
|
|
547
690
|
|
|
691
|
+
program
|
|
692
|
+
.command("release")
|
|
693
|
+
.description("Build, pack, and serve a QR that installs the miniapp on phones over LAN")
|
|
694
|
+
.option("--cwd <path>", "miniapp project directory", process.cwd())
|
|
695
|
+
.option("--no-cache", "rebuild even if the cached zip is newer than every source file")
|
|
696
|
+
.option("-q, --qr <size>", "terminal QR size: small|medium|large (sm|md|lg)", "medium")
|
|
697
|
+
.option("--qr-output <path>", "also write the QR as a PNG at this path")
|
|
698
|
+
.action(async (options: { cwd: string; cache: boolean; qr: string; qrOutput?: string }) => {
|
|
699
|
+
try {
|
|
700
|
+
await releaseMiniapp({
|
|
701
|
+
cwd: resolve(options.cwd),
|
|
702
|
+
noCache: !options.cache,
|
|
703
|
+
qrSize: parseQrSize(options.qr),
|
|
704
|
+
qrOutput: options.qrOutput,
|
|
705
|
+
});
|
|
706
|
+
} catch (error) {
|
|
707
|
+
fail(error);
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
program
|
|
712
|
+
.command("manifest")
|
|
713
|
+
.description("Edit miniapp.json interactively")
|
|
714
|
+
.option("--cwd <path>", "miniapp project directory", process.cwd())
|
|
715
|
+
.action(async (options: { cwd: string }) => {
|
|
716
|
+
try {
|
|
717
|
+
process.chdir(resolve(options.cwd));
|
|
718
|
+
await runManifestWizard();
|
|
719
|
+
} catch (error) {
|
|
720
|
+
fail(error);
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
const permission = program.command("permission").description("Declared permissions in miniapp.json");
|
|
725
|
+
|
|
726
|
+
permission
|
|
727
|
+
.command("list")
|
|
728
|
+
.description("List declared permissions")
|
|
729
|
+
.option("--cwd <path>", "miniapp project directory", process.cwd())
|
|
730
|
+
.action(async (options: { cwd: string }) => {
|
|
731
|
+
try {
|
|
732
|
+
process.chdir(resolve(options.cwd));
|
|
733
|
+
await listPermissionsCmd();
|
|
734
|
+
} catch (error) {
|
|
735
|
+
fail(error);
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
permission
|
|
740
|
+
.command("add")
|
|
741
|
+
.argument("[type]", "permission type, e.g. MICROPHONE (interactive when omitted)")
|
|
742
|
+
.description("Add a permission")
|
|
743
|
+
.option("--cwd <path>", "miniapp project directory", process.cwd())
|
|
744
|
+
.action(async (type: string | undefined, options: { cwd: string }) => {
|
|
745
|
+
try {
|
|
746
|
+
process.chdir(resolve(options.cwd));
|
|
747
|
+
await addPermissionCmd(type);
|
|
748
|
+
} catch (error) {
|
|
749
|
+
fail(error);
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
permission
|
|
754
|
+
.command("remove")
|
|
755
|
+
.argument("[type]", "permission type (interactive when omitted)")
|
|
756
|
+
.description("Remove a declared permission")
|
|
757
|
+
.option("--cwd <path>", "miniapp project directory", process.cwd())
|
|
758
|
+
.action(async (type: string | undefined, options: { cwd: string }) => {
|
|
759
|
+
try {
|
|
760
|
+
process.chdir(resolve(options.cwd));
|
|
761
|
+
await removePermissionCmd(type);
|
|
762
|
+
} catch (error) {
|
|
763
|
+
fail(error);
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
const hardware = program.command("hardware").description("Hardware requirements in miniapp.json");
|
|
768
|
+
|
|
769
|
+
hardware
|
|
770
|
+
.command("list")
|
|
771
|
+
.description("List declared hardware requirements")
|
|
772
|
+
.option("--cwd <path>", "miniapp project directory", process.cwd())
|
|
773
|
+
.action(async (options: { cwd: string }) => {
|
|
774
|
+
try {
|
|
775
|
+
process.chdir(resolve(options.cwd));
|
|
776
|
+
await listHardwareCmd();
|
|
777
|
+
} catch (error) {
|
|
778
|
+
fail(error);
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
hardware
|
|
783
|
+
.command("add")
|
|
784
|
+
.argument("[type]", "hardware type, e.g. DISPLAY (interactive when omitted)")
|
|
785
|
+
.argument("[level]", "REQUIRED or OPTIONAL")
|
|
786
|
+
.description("Add a hardware requirement")
|
|
787
|
+
.option("--cwd <path>", "miniapp project directory", process.cwd())
|
|
788
|
+
.action(async (type: string | undefined, level: string | undefined, options: { cwd: string }) => {
|
|
789
|
+
try {
|
|
790
|
+
process.chdir(resolve(options.cwd));
|
|
791
|
+
await addHardwareCmd(type, level);
|
|
792
|
+
} catch (error) {
|
|
793
|
+
fail(error);
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
hardware
|
|
798
|
+
.command("remove")
|
|
799
|
+
.argument("[type]", "hardware type (interactive when omitted)")
|
|
800
|
+
.description("Remove a declared hardware requirement")
|
|
801
|
+
.option("--cwd <path>", "miniapp project directory", process.cwd())
|
|
802
|
+
.action(async (type: string | undefined, options: { cwd: string }) => {
|
|
803
|
+
try {
|
|
804
|
+
process.chdir(resolve(options.cwd));
|
|
805
|
+
await removeHardwareCmd(type);
|
|
806
|
+
} catch (error) {
|
|
807
|
+
fail(error);
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
const schema = program.command("schema").description("The miniapp.json JSON Schema");
|
|
812
|
+
|
|
813
|
+
schema
|
|
814
|
+
.command("print")
|
|
815
|
+
.description("Print the miniapp.json JSON Schema to stdout")
|
|
816
|
+
.action(() => {
|
|
817
|
+
schemaPrint();
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
schema
|
|
821
|
+
.command("regenerate")
|
|
822
|
+
.description("Rewrite @glassly/miniapp-cli's published schema file (package-internal)")
|
|
823
|
+
.action(() => {
|
|
824
|
+
regenerateSchemaFile();
|
|
825
|
+
});
|
|
826
|
+
|
|
548
827
|
program
|
|
549
828
|
.command("publish")
|
|
550
829
|
.description("Build, pack, and upload the current miniapp release bundle")
|
|
@@ -584,6 +863,7 @@ program
|
|
|
584
863
|
manifest,
|
|
585
864
|
bundle,
|
|
586
865
|
});
|
|
866
|
+
const storeMedia = collectStoreMedia(cwd);
|
|
587
867
|
const { release } = await createRelease(creds, {
|
|
588
868
|
packageName,
|
|
589
869
|
version,
|
|
@@ -591,6 +871,7 @@ program
|
|
|
591
871
|
bundleBase64: bundle.toString("base64"),
|
|
592
872
|
fileName: basename(zipPath),
|
|
593
873
|
signedBundle,
|
|
874
|
+
media: storeMedia.length > 0 ? storeMedia : undefined,
|
|
594
875
|
});
|
|
595
876
|
const submitted = await submitRelease(creds, {
|
|
596
877
|
packageName,
|
|
@@ -602,6 +883,11 @@ program
|
|
|
602
883
|
console.log(`Bundle: ${basename(zipPath)} (${sizeKb} KB)`);
|
|
603
884
|
console.log(`Signing key: ${signedBundle.signingKeyId}`);
|
|
604
885
|
if (release.bundleSha256) console.log(`SHA-256: ${release.bundleSha256}`);
|
|
886
|
+
if (storeMedia.length > 0) {
|
|
887
|
+
const icons = storeMedia.filter(item => item.role === "store_icon").length;
|
|
888
|
+
const shots = storeMedia.length - icons;
|
|
889
|
+
console.log(`Store media: ${icons ? "icon override, " : ""}${shots} screenshot${shots === 1 ? "" : "s"} from store/`);
|
|
890
|
+
}
|
|
605
891
|
} catch (error) {
|
|
606
892
|
fail(error);
|
|
607
893
|
}
|
|
@@ -693,6 +979,52 @@ function readManifest(cwd: string): Record<string, unknown> {
|
|
|
693
979
|
return parsed as Record<string, unknown>;
|
|
694
980
|
}
|
|
695
981
|
|
|
982
|
+
const MEDIA_CONTENT_TYPES: Record<string, string> = {
|
|
983
|
+
".png": "image/png",
|
|
984
|
+
".jpg": "image/jpeg",
|
|
985
|
+
".jpeg": "image/jpeg",
|
|
986
|
+
};
|
|
987
|
+
|
|
988
|
+
function parseMediaRole(value: string): MediaRole {
|
|
989
|
+
const normalized = value.trim().toLowerCase();
|
|
990
|
+
if (normalized === "store_icon" || normalized === "gallery_screenshot") return normalized;
|
|
991
|
+
throw new Error("role must be store_icon or gallery_screenshot");
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function readMediaFile(path: string, role: MediaRole): MediaUpload {
|
|
995
|
+
if (!existsSync(path)) throw new Error(`file not found: ${path}`);
|
|
996
|
+
const contentType = MEDIA_CONTENT_TYPES[extname(path).toLowerCase()];
|
|
997
|
+
if (!contentType) throw new Error(`${basename(path)}: only .png, .jpg and .jpeg files can be uploaded`);
|
|
998
|
+
return {
|
|
999
|
+
role,
|
|
1000
|
+
fileName: basename(path),
|
|
1001
|
+
contentType,
|
|
1002
|
+
dataBase64: readFileSync(path).toString("base64"),
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* Store media next to miniapp.json, uploaded with the release:
|
|
1008
|
+
* store/icon.png optional override of the bundle icon
|
|
1009
|
+
* store/screenshots/*.png gallery, in file-name order
|
|
1010
|
+
*/
|
|
1011
|
+
function collectStoreMedia(cwd: string): MediaUpload[] {
|
|
1012
|
+
const storeDir = join(cwd, "store");
|
|
1013
|
+
if (!existsSync(storeDir) || !statSync(storeDir).isDirectory()) return [];
|
|
1014
|
+
const out: MediaUpload[] = [];
|
|
1015
|
+
const icon = join(storeDir, "icon.png");
|
|
1016
|
+
if (existsSync(icon)) out.push(readMediaFile(icon, "store_icon"));
|
|
1017
|
+
const shotsDir = join(storeDir, "screenshots");
|
|
1018
|
+
if (existsSync(shotsDir) && statSync(shotsDir).isDirectory()) {
|
|
1019
|
+
const files = readdirSync(shotsDir)
|
|
1020
|
+
.filter(name => MEDIA_CONTENT_TYPES[extname(name).toLowerCase()] !== undefined)
|
|
1021
|
+
.sort((a, b) => a.localeCompare(b, "en", { numeric: true }));
|
|
1022
|
+
if (files.length > 8) throw new Error(`store/screenshots has ${files.length} images; the store allows at most 8`);
|
|
1023
|
+
for (const name of files) out.push(readMediaFile(join(shotsDir, name), "gallery_screenshot"));
|
|
1024
|
+
}
|
|
1025
|
+
return out;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
696
1028
|
function stringField(manifest: Record<string, unknown>, field: string): string {
|
|
697
1029
|
const value = manifest[field];
|
|
698
1030
|
if (typeof value !== "string" || !value.trim()) {
|
|
@@ -748,12 +1080,9 @@ function inferEnvironment(coreUrl: string): PreinstallEnvironment {
|
|
|
748
1080
|
return "prod";
|
|
749
1081
|
}
|
|
750
1082
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
return normalized as PreinstallPolicy;
|
|
755
|
-
}
|
|
756
|
-
throw new Error("policy must be install_once, keep_updated, or mandatory");
|
|
1083
|
+
/** The two ways a phone treats a preinstalled release. */
|
|
1084
|
+
function installModeLabel(policy: PreinstallPolicy): string {
|
|
1085
|
+
return policy === "mandatory" ? "mandatory" : "installed by default";
|
|
757
1086
|
}
|
|
758
1087
|
|
|
759
1088
|
function resolveReleaseRefs(refs: string[], releases: AdminReleaseSummary[]): AdminReleaseSummary[] {
|