@glassly/miniapp-cli
v0.1.0-dev.1
Published
Dev tools for Glassly miniapps
Readme
@glassly/miniapp-cli (glassly-miniapp)
Author-facing CLI for Glassly miniapps. Pairs with @glassly/miniapp.
glassly-miniapp <command>Commands at a glance
| Command | What it does |
| ------------------------------------------------- | ------------------------------------------------------------------------- |
| dev | Starts the dev server with hot reload, prints a QR to load it on a phone |
| release | Builds, packs, and serves a QR to install the release on a phone over LAN |
| pack | Validates the manifest and zips dist/ into <pkg>-<version>.zip |
| manifest | Interactive top-level wizard for editing miniapp.json |
| permission list \| add \| remove | Object-verb manifest edits for permissions |
| hardware list \| add \| remove | Object-verb manifest edits for hardware requirements |
| schema print | Prints the canonical miniapp.json JSON Schema to stdout |
Run with no args to print the same usage table.
dev
glassly-miniapp devWhat it does:
- Reads + validates
miniapp.json(hard-fails on bad permissions / hardware types so you don't have to debug it on the phone). - Runs the project's
build.tssodist/background/index.jsanddist/ui/*are current. - Picks the first free adjacent port pair starting at
port: one for static files and the next for the dev sidecar. - Starts a static server that serves
miniapp.json,icon.png, and project files. - Starts a dev sidecar on
port + 1— a WebSocket the phone connects to for live reload + console-log forwarding back to your terminal. Failure here is non-fatal; the miniapp still runs without live reload. - Detects the LAN IP, builds a
miniapp://dev?url=…&name=…&package=…&dev=<sidecarPort>URL, and prints a terminal QR + the raw URL. - Watches for LAN-IP changes (Wi-Fi switch) every 10s and reprints the QR.
Default port is 3000; override the starting point with a "port": <n> field in miniapp.json. If that port or its sidecar neighbor is busy, dev scans upward until it finds a free adjacent pair.
On the phone: open the Glassly App → Settings → Developer settings → Mini App Development → Scan Mini App QR Code. Phone and laptop must be on the same Wi-Fi.
dev is live and temporary. Keep the CLI and computer running because the
Glassly App loads the runtime bundle from that LAN server. Dev miniapps are keyed
by their manifest package name, so you can scan and test several side by side;
rescanning the same package updates only that entry. The Glassly App caches each
entry's name and icon. Use bun run release when you need an installed miniapp
that works without the computer.
Ctrl+C stops the server, the sidecar, and the IP watcher.
release
glassly-miniapp release
glassly-miniapp release --no-cache # force rebuild even if cache is freshThe all-in-one verb: build a release, pack it, and serve it behind a QR so you can install on as many phones as you like.
Flow:
- Validates
miniapp.json. - Build cache. Looks for
build/<packageName>-<version>.zip. If it exists and every project source file (excludingnode_modules,dist,build,.git) is older than the zip, reuses it. Otherwise rebuilds. - Build. Detects your package manager (
bun.lock→bun,pnpm-lock.yaml→pnpm,yarn.lock→yarn, elsenpm) and runs<pm> run build. Yourpackage.jsonmust define abuildscript that producesdist/. - Pack. Calls the same logic as
glassly-miniapp pack— validates the manifest, copiesminiapp.json+icon.pngintodist/, zips tobuild/<packageName>-<version>.zip. Prints size + duration. - Serve. Picks a free port between 6789 and 6798. Hosts the bundle, manifest, and icon over HTTP on
0.0.0.0:GET /miniapp.jsonGET /icon.pngGET /bundle.zipGET /__glassly_release/health
- Prints a
miniapp://release?url=<lan-base>&package=…&version=…&name=…URL + QR. - Stays up so multiple devices can install. Each
/bundle.zipfetch logs✓ Install #N — <name>@<version> → <remote>.
Ctrl+C to stop the server.
On the phone: the Glassly App's QR scanner branches on miniapp://release and uses the dev composer to download + install the bundle. The miniapp lands in lmas/<package>/<version>/ and behaves like any installed local miniapp — runs offline, persists across restarts, no laptop required after install.
Why "release" and not "install":
installcollides with package managers (bun run installis reserved). Naming the action after the artifact you're producing avoids that collision and matches Android'sinstallReleasemental model.
pack
glassly-miniapp pack
glassly-miniapp pack --no-build # zip dist/ as-is, skip the buildProduces a distributable ZIP. Use this when you want the artifact only — release calls pack internally.
Steps:
- Runs
<pm> run buildwithNODE_ENV=production(same asrelease), so the zip always contains a production bundle — never a stale dev build left behind bydev. Pass--no-buildto skip this and zip whatever is already indist/. - Verifies
dist/exists. - Validates
miniapp.json. - Copies
miniapp.jsonandicon.pngintodist/. - Runs the system
zip -rcommand to producebuild/<packageName>-<version>.zipand prints the absolute path.
build/ is self-ignoring — the CLI writes a .gitignore containing * into it on creation, so packed zips stay out of version control in any repo without touching the project's own .gitignore.
The resulting ZIP is the artifact you'd upload to the miniapp store.
Requires the
zipbinary onPATH(preinstalled on macOS and most Linux distros). On Windows, installzipvia WSL or use a Unix-like shell.
manifest
glassly-miniapp manifestInteractive top-level wizard for miniapp.json (Clack-based). Loop:
- Edit permissions — add, remove
- Edit hardware requirements — add, remove
- Show current manifest — pretty-prints the JSON
- Done — exits
Persists after every confirmed change, so Ctrl+C never loses a saved edit.
The wizard shares its mutation backend (manifest-mutate.ts) with the object-verb commands below — behavior and validation are identical.
permission
glassly-miniapp permission list
glassly-miniapp permission add [TYPE]
glassly-miniapp permission remove [TYPE]add / remove are interactive when called without TYPE (Clack select prompts) and non-interactive when TYPE is provided.
Allowed TYPE values: MICROPHONE, CAMERA, CALENDAR, LOCATION, BACKGROUND_LOCATION, READ_NOTIFICATIONS, POST_NOTIFICATIONS.
Adding a permission interactively prompts for an optional human-readable description (shown in the OS prompt when the user is asked to grant the permission).
hardware
glassly-miniapp hardware list
glassly-miniapp hardware add [TYPE] [LEVEL]
glassly-miniapp hardware remove [TYPE]Allowed TYPE values: CAMERA, DISPLAY, MICROPHONE, SPEAKER, IMU, BUTTON, LIGHT, WIFI.
Allowed LEVEL values: REQUIRED, OPTIONAL.
REQUIRED— glasses without this hardware can't run the app (hidden in the store / launcher on incompatible devices).OPTIONAL— glasses without this hardware still run the app, in a degraded state.
Add is interactive when called without TYPE / LEVEL. Non-interactive form requires both.
The
EXISThardware type is injected by the phone at runtime (every miniapp implicitly requires that glasses are present). It's intentionally not in the allowed-types list — don't declare it.
schema
glassly-miniapp schema printPrints the canonical miniapp.json JSON Schema to stdout. Useful for piping into IDE config or for validation in CI.
The schema is generated from the same constants the validator uses (ALLOWED_PERMISSIONS, ALLOWED_HARDWARE_TYPES, ALLOWED_HARDWARE_LEVELS), so it can never drift from validation behavior.
The published schema file ships at node_modules/@glassly/miniapp-cli/schema/miniapp.schema.json for editors that read $schema from miniapp.json. The scaffolder (create-miniapp) injects this $schema line into new projects automatically.
glassly-miniapp schema regenerateexists too but is a CLI-internal command — it rewrites the published schema file from the in-source allowed-values lists. Authors don't need it.
miniapp.json shape
{
"$schema": "./node_modules/@glassly/miniapp-cli/schema/miniapp.schema.json",
"packageName": "com.glassly.example",
"version": "1.0.0",
"name": "Glassly Example",
"description": "…",
"icon": "icon.png",
"port": 3000,
"permissions": [{"type": "MICROPHONE", "description": "Listen for what to caption."}],
"hardwareRequirements": [
{"type": "DISPLAY", "level": "REQUIRED"},
{"type": "MICROPHONE", "level": "REQUIRED"}
]
}Required: packageName, version, name, hardwareRequirements. Everything else is optional.
packageName must be reverse-DNS (^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$).
port defaults to 3000 for dev and is ignored by release (which picks its own free port). For dev, this is the starting port; if the port or its sidecar neighbor is busy, the CLI scans upward until it finds a free adjacent pair.
The CLI's allowed-value lists are mirrored by hand from @glassly/types to keep the CLI dependency-light so bunx glassly-miniapp stays fast. Drift between the two is caught at validation time, not import time.
File map
- Subcommand handlers:
src/{dev,release,pack,permission,hardware,schema,manifest-wizard}.ts - Manifest validation + allowed-value lists:
src/manifest.ts - Manifest mutation backend (shared by wizard + object-verb commands):
src/manifest-mutate.ts - Manifest read/write helpers:
src/manifest-format.ts - Permission/hardware human-readable hints:
src/permission-hints.ts - Dev sidecar WebSocket server:
src/dev-server.ts - QR rendering:
src/qr.ts - Generated JSON Schema:
schema/miniapp.schema.json(regenerated viaschema regenerate)
