@glassly/create-miniapp 0.1.0-beta.7

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,66 @@
1
+ # @glassly/create-miniapp
2
+
3
+ Scaffold a new [Glassly](https://github.com/tetramo-labs/glassly)
4
+ miniapp project. This is the `bun create` starter for apps that run on Glassly
5
+ smart glasses.
6
+
7
+ ## Usage
8
+
9
+ ```sh
10
+ bunx @glassly/create-miniapp my-app
11
+ cd my-app
12
+ bun install
13
+ bun run dev
14
+ ```
15
+
16
+ > The scaffolder and the generated project's tooling run under
17
+ > [Bun](https://bun.sh) only. Use `bun` and `bunx`, not `npx`.
18
+
19
+ ### `--prefix`
20
+
21
+ Every miniapp lives under its developer org's reverse-DNS package prefix (set
22
+ when you create the org in the Developer Console, for example `com.example`).
23
+ The scaffolder asks for it and writes `<prefix>.<project_name>` into
24
+ `miniapp.json` as the `packageName`, with dashes in the project name turned into
25
+ underscores so the name is valid. The Store rejects releases outside your org's
26
+ prefix, so use the real one. Pass `--prefix` to skip the prompt:
27
+
28
+ ```sh
29
+ bunx @glassly/create-miniapp my-app --prefix=com.example
30
+ ```
31
+
32
+ ### `--type`
33
+
34
+ The scaffolder asks what kind of glasses the miniapp targets, which decides the
35
+ `hardwareRequirements` it writes into `miniapp.json`. Pass `--type` to skip the
36
+ prompt:
37
+
38
+ | Value | Glasses | `hardwareRequirements` |
39
+ | --- | --- | --- |
40
+ | `display` | Even Realities G1/G2, Vuzix Z100 | `DISPLAY` + `MICROPHONE`, both `REQUIRED` |
41
+ | `camera` | Mentra Live | `CAMERA` + `MICROPHONE`, both `REQUIRED` |
42
+
43
+ ```sh
44
+ bunx @glassly/create-miniapp my-app --type=display
45
+ ```
46
+
47
+ Use the `--flag=value` form for both flags; a space-separated `--type value` is
48
+ not parsed.
49
+ Requirements are only a starting point — edit `miniapp.json` directly, or with
50
+ `glassly hardware add`, as the miniapp grows.
51
+
52
+ The generated project comes wired with
53
+ [`@glassly/miniapp`](https://www.npmjs.com/package/@glassly/miniapp) (the SDK
54
+ runtime) and [`@glassly/cli`](https://www.npmjs.com/package/@glassly/cli) (the
55
+ `glassly` command: `dev`, `release`, `pack`, `publish`), a `miniapp.json`
56
+ manifest, and a TypeScript setup. Dependency pins are stamped at publish time to the exact
57
+ SDK versions this scaffolder shipped with, so a generated project installs
58
+ on any release channel.
59
+
60
+ To publish a finished miniapp to the Glassly Miniapp Store, run `glassly login`
61
+ once and then `glassly publish` from the project folder.
62
+
63
+ ## Part of Glassly
64
+
65
+ Source lives in the [Glassly monorepo](https://github.com/tetramo-labs/glassly)
66
+ under `sdk/create-miniapp`. Issues and contributions welcome there.
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ // Extensionless launcher: npm strips bin entries that point at .ts files.
3
+ // The scaffolder runs under Bun; when invoked from Node (npx, npm create)
4
+ // delegate to Bun if it is installed, otherwise say how to get it.
5
+ import {spawnSync} from "node:child_process"
6
+ import {fileURLToPath} from "node:url"
7
+
8
+ const entry = fileURLToPath(new URL("./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("create-miniapp runs on Bun, which is not installed or not on PATH.")
16
+ console.error("Install it from https://bun.sh, then run: bunx @glassly/create-miniapp my-miniapp")
17
+ process.exit(1)
18
+ }
19
+ process.exit(result.status ?? 1)
20
+ }
package/bin/index.ts ADDED
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import * as fs from "fs"
4
+ import * as path from "path"
5
+ import {intro, outro, text, select, isCancel, cancel, note} from "@clack/prompts"
6
+
7
+ type GlassesType = "display" | "camera"
8
+
9
+ interface CliFlags {
10
+ projectName?: string
11
+ type?: GlassesType
12
+ prefix?: string
13
+ }
14
+
15
+ // Package names are reverse-DNS: letters, digits, underscores, each segment
16
+ // starting with a letter. The Store rejects anything else, and every miniapp
17
+ // must sit under the developer org's package prefix.
18
+ const PACKAGE_SEGMENT = /^[a-z][a-z0-9_]*$/
19
+ const PACKAGE_PREFIX = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/
20
+
21
+ function parseArgs(argv: string[]): CliFlags {
22
+ const flags: CliFlags = {}
23
+ for (const arg of argv) {
24
+ if (arg.startsWith("--type=")) {
25
+ const value = arg.slice("--type=".length)
26
+ if (value === "display" || value === "camera") {
27
+ flags.type = value
28
+ } else {
29
+ console.error(`Invalid --type value: "${value}". Expected "display" or "camera".`)
30
+ process.exit(1)
31
+ }
32
+ } else if (arg.startsWith("--prefix=")) {
33
+ flags.prefix = arg.slice("--prefix=".length)
34
+ } else if (!arg.startsWith("-") && !flags.projectName) {
35
+ flags.projectName = arg
36
+ }
37
+ }
38
+ return flags
39
+ }
40
+
41
+ function toKebabCase(str: string): string {
42
+ return str
43
+ .replace(/([a-z])([A-Z])/g, "$1-$2")
44
+ .replace(/[\s_]+/g, "-")
45
+ .toLowerCase()
46
+ }
47
+
48
+ function toPackageSegment(str: string): string {
49
+ return toKebabCase(str).replace(/-+/g, "_")
50
+ }
51
+
52
+ function validateProjectName(name: string): string | undefined {
53
+ if (!name.trim()) return "Project name is required"
54
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9-_ ]*$/.test(name)) {
55
+ return "Use letters, numbers, dashes, underscores, or spaces (cannot start with a symbol)"
56
+ }
57
+ if (!PACKAGE_SEGMENT.test(toPackageSegment(name))) {
58
+ return "Project name must start with a letter"
59
+ }
60
+ if (fs.existsSync(path.resolve(process.cwd(), name))) {
61
+ return `Directory "${name}" already exists`
62
+ }
63
+ return undefined
64
+ }
65
+
66
+ function validatePrefix(prefix: string): string | undefined {
67
+ if (!prefix.trim()) return "Package prefix is required"
68
+ if (!PACKAGE_PREFIX.test(prefix)) {
69
+ return "Use reverse-DNS form like com.example: lowercase letters, digits, underscores, at least two segments"
70
+ }
71
+ return undefined
72
+ }
73
+
74
+ function bailIfCancelled<T>(value: T | symbol): T {
75
+ if (isCancel(value)) {
76
+ cancel("Cancelled.")
77
+ process.exit(0)
78
+ }
79
+ return value as T
80
+ }
81
+
82
+ const flags = parseArgs(process.argv.slice(2))
83
+
84
+ intro("create-miniapp")
85
+
86
+ let projectName = flags.projectName
87
+ if (projectName) {
88
+ const err = validateProjectName(projectName)
89
+ if (err) {
90
+ cancel(err)
91
+ process.exit(1)
92
+ }
93
+ } else {
94
+ projectName = bailIfCancelled(
95
+ await text({
96
+ message: "What's your project name?",
97
+ placeholder: "my-miniapp",
98
+ validate: validateProjectName,
99
+ }),
100
+ )
101
+ }
102
+
103
+ let prefix = flags.prefix
104
+ if (prefix) {
105
+ const err = validatePrefix(prefix)
106
+ if (err) {
107
+ cancel(err)
108
+ process.exit(1)
109
+ }
110
+ } else {
111
+ prefix = bailIfCancelled(
112
+ await text({
113
+ message: "Package prefix? (your developer org's prefix from the Developer Console)",
114
+ placeholder: "com.example",
115
+ validate: validatePrefix,
116
+ }),
117
+ )
118
+ }
119
+
120
+ let glassesType = flags.type
121
+ if (!glassesType) {
122
+ glassesType = bailIfCancelled(
123
+ await select<GlassesType>({
124
+ message: "What kind of glasses is this miniapp for?",
125
+ options: [
126
+ {
127
+ value: "display",
128
+ label: "Display glasses",
129
+ hint: "Even Realities G1/G2, Vuzix Z100",
130
+ },
131
+ {
132
+ value: "camera",
133
+ label: "Camera glasses",
134
+ hint: "Mentra Live",
135
+ },
136
+ ],
137
+ }),
138
+ )
139
+ }
140
+
141
+ const targetDir = path.resolve(process.cwd(), projectName)
142
+ const templateDir = path.resolve(import.meta.dirname, "..", "template")
143
+
144
+ const packageName = `${prefix}.${toPackageSegment(projectName)}`
145
+ const hardwareRequirements =
146
+ glassesType === "camera"
147
+ ? [
148
+ {type: "CAMERA", level: "REQUIRED"},
149
+ {type: "MICROPHONE", level: "REQUIRED"},
150
+ ]
151
+ : [
152
+ {type: "DISPLAY", level: "REQUIRED"},
153
+ {type: "MICROPHONE", level: "REQUIRED"},
154
+ ]
155
+
156
+ function renderFile(srcPath: string, destPath: string): void {
157
+ if (path.basename(srcPath) === "miniapp.json") {
158
+ const raw = fs.readFileSync(srcPath, "utf-8")
159
+ const manifest = JSON.parse(raw.replace(/\{\{packageName\}\}/g, packageName))
160
+ manifest.hardwareRequirements = hardwareRequirements
161
+ fs.writeFileSync(destPath, JSON.stringify(manifest, null, 2) + "\n")
162
+ return
163
+ }
164
+
165
+ const content = fs.readFileSync(srcPath, "utf-8").replace(/\{\{packageName\}\}/g, packageName)
166
+ fs.writeFileSync(destPath, content)
167
+ }
168
+
169
+ function copyDir(src: string, dest: string): void {
170
+ fs.mkdirSync(dest, {recursive: true})
171
+ for (const entry of fs.readdirSync(src, {withFileTypes: true})) {
172
+ const srcPath = path.join(src, entry.name)
173
+ // npm strips literal `.gitignore` files on publish; the template
174
+ // ships it as `_gitignore` instead and we rename on copy so the
175
+ // scaffolded project starts with a proper `.gitignore`.
176
+ const renamed = entry.name === "_gitignore" ? ".gitignore" : entry.name
177
+ const destPath = path.join(dest, renamed)
178
+ if (entry.isDirectory()) {
179
+ copyDir(srcPath, destPath)
180
+ } else {
181
+ renderFile(srcPath, destPath)
182
+ }
183
+ }
184
+ }
185
+
186
+ copyDir(templateDir, targetDir)
187
+
188
+ note(`cd ${projectName}\nbun install\nbun dev`, "Next steps")
189
+
190
+ outro(`Created ${packageName} in ./${projectName}`)
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@glassly/create-miniapp",
3
+ "version": "0.1.0-beta.7",
4
+ "description": "Scaffold a new Glassly miniapp project",
5
+ "type": "module",
6
+ "publishConfig": {
7
+ "access": "public",
8
+ "tag": "dev"
9
+ },
10
+ "bin": {
11
+ "create-miniapp": "bin/create-miniapp"
12
+ },
13
+ "scripts": {
14
+ "build": "echo 'No build needed — runs via bun'"
15
+ },
16
+ "dependencies": {
17
+ "@clack/prompts": "^1.2.0"
18
+ },
19
+ "devDependencies": {
20
+ "typescript": "^5.0.0"
21
+ },
22
+ "files": [
23
+ "bin",
24
+ "template"
25
+ ],
26
+ "keywords": [
27
+ "glassly",
28
+ "glassly",
29
+ "miniapp",
30
+ "create",
31
+ "scaffold"
32
+ ],
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/tetramo-labs/glassly.git",
36
+ "directory": "sdk/create-miniapp"
37
+ },
38
+ "homepage": "https://github.com/tetramo-labs/glassly/tree/dev/sdk/create-miniapp#readme",
39
+ "bugs": {
40
+ "url": "https://github.com/tetramo-labs/glassly/issues"
41
+ }
42
+ }
@@ -0,0 +1,18 @@
1
+ # Miniapp implementation rules
2
+
3
+ - Put glasses subscriptions, `session.*` calls, durable state, and logic that
4
+ must survive UI closure in `src/background/`.
5
+ - Put React, rendering, DOM access, and browser UI libraries in `src/ui/`.
6
+ - The layers do not share memory. Exchange serializable data through the typed
7
+ `glassly.*` / `session.ui.*` channels declared in `src/shared/channels.ts`.
8
+ - The background is a bare JavaScript engine, not a browser or Node. Supported
9
+ globals are `console`, timers, `queueMicrotask`, `fetch`, `WebSocket`,
10
+ `localStorage`, `crypto.getRandomValues`, `crypto.randomUUID`, `TextEncoder`,
11
+ `TextDecoder`, `atob`, `btoa`, and the SDK-supported `AbortController` subset.
12
+ - Do not use `window`, `document`, DOM elements, `performance`,
13
+ `XMLHttpRequest`, `crypto.subtle`, Node built-ins, `process`, or runtime module
14
+ resolution in `src/background/`. Use `Date.now()` instead of
15
+ `performance.now()`.
16
+ - `bun run dev` is live and temporary. Multiple dev miniapps can coexist when
17
+ they use distinct manifest package names. Use `bun run release` for a
18
+ persistent local install that runs without the development computer.
@@ -0,0 +1,43 @@
1
+ # Glassly miniapp
2
+
3
+ This project has two runtimes with different jobs:
4
+
5
+ - `src/background/` is always on while the miniapp is enabled. Put glasses
6
+ subscriptions, `session.*` calls, durable state, and logic that must keep
7
+ running after the UI closes here.
8
+ - `src/ui/` is a normal WebView that exists only while the user has the miniapp
9
+ open. Put React, rendering, DOM access, and browser UI libraries here.
10
+
11
+ The background is a bare JavaScript engine, not a browser or Node. It supports
12
+ `console`, timers, `fetch`, `WebSocket`, `localStorage`,
13
+ `crypto.getRandomValues`, `crypto.randomUUID`, `TextEncoder`, `TextDecoder`,
14
+ `atob`, `btoa`, and the SDK-supported `AbortController` subset. It does not
15
+ support `window`, `document`, DOM elements, `performance`, `XMLHttpRequest`,
16
+ `crypto.subtle`, Node built-ins, `process`, or runtime module resolution. Use
17
+ `Date.now()` instead of `performance.now()`.
18
+
19
+ `bun run build` checks background source (including imported shared files) and
20
+ reports common unsupported browser or Node APIs with a file, line, and supported
21
+ alternative before it emits a bundle.
22
+
23
+ The layers do not share memory. Exchange serializable data through the typed
24
+ `glassly.*` / `session.ui.*` message bus declared in `src/shared/channels.ts`.
25
+
26
+ ## Commands
27
+
28
+ ```bash
29
+ bun run dev # live, temporary; keep the computer and CLI running
30
+ bun run release # persistent local install that runs without the computer
31
+ bun run build # build both layers into dist/
32
+ bun run pack # production-build, validate the manifest, zip to build/<pkg>-<version>.zip
33
+ bun run typecheck # tsc --noEmit, no bundle emitted
34
+ ```
35
+
36
+ `pack` is the distributable artifact on its own: `release` builds and packs for
37
+ you, so reach for `pack` when you want the zip without serving it. `typecheck`
38
+ is types only — the unsupported-API checks live in `build`.
39
+
40
+ The Glassly App keeps one dev entry per manifest package name, so you can scan and
41
+ test multiple dev miniapps side by side. It also caches each entry's name and
42
+ icon. Rescanning the same package updates that entry. Use `bun run release` when
43
+ you want the miniapp to run without the computer.
@@ -0,0 +1,7 @@
1
+ node_modules/
2
+ dist/
3
+ build/
4
+ *.zip
5
+ .env
6
+ .env.local
7
+ .DS_Store
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Production build script — two-output bundle.
3
+ *
4
+ * Two-layer miniapps ship TWO bundles in one zip:
5
+ * - dist/background/index.js — JSContext entry, no DOM
6
+ * - dist/ui/index.html + assets — WebView entry, full DOM
7
+ *
8
+ * Bun.build() handles both. The background bundle uses `format: "iife"`
9
+ * because JSC + Zipline/QuickJS evaluate it as a classic script — any
10
+ * top-level ESM `export` is a SyntaxError there. `@glassly/miniapp/background`
11
+ * is bundled in (not external) because the JSContext has no module
12
+ * resolver; the host's polyfill installs `__dispatch`/`__deliver` but
13
+ * does NOT provide the SDK surface. The UI bundle uses target: "browser"
14
+ * with no externals.
15
+ *
16
+ * Build-time env vars: any `PUBLIC_*` key in your shell or
17
+ * miniapp-local `.env` gets inlined into both bundles via define.
18
+ * Anything inlined into the UI bundle is visible in WebView network
19
+ * requests and source maps — secrets MUST live behind your own backend
20
+ * rather than in PUBLIC_*.
21
+ *
22
+ * Add bundler plugins (Tailwind, Vue, etc.) by pushing them into the
23
+ * matching `plugins` array. For example, with Tailwind v4 (UI side only):
24
+ *
25
+ * const tailwind = (await import("bun-plugin-tailwind")).default
26
+ * uiBuild.plugins.push(tailwind)
27
+ *
28
+ * Output goes to ./dist with the shape `glassly release` expects.
29
+ */
30
+
31
+ import {rm} from "fs/promises"
32
+ import {backgroundRuntimeGuardPlugin, reactSingletonPlugin} from "@glassly/miniapp-cli/build-helpers"
33
+
34
+ const distDir = "./dist"
35
+
36
+ // Wipe dist/ so old chunks don't accumulate across builds.
37
+ await rm(distDir, {recursive: true, force: true})
38
+
39
+ // Pick up PUBLIC_* env vars from the shell / .env. Bun loads .env
40
+ // from cwd automatically; we just need to forward the keys to define.
41
+ const define: Record<string, string> = {}
42
+ for (const [k, v] of Object.entries(process.env)) {
43
+ if (k.startsWith("PUBLIC_") && typeof v === "string") {
44
+ define[`process.env.${k}`] = JSON.stringify(v)
45
+ }
46
+ }
47
+
48
+ const backgroundResult = await Bun.build({
49
+ entrypoints: ["./src/background/index.ts"],
50
+ outdir: `${distDir}/background`,
51
+ target: "browser",
52
+ format: "iife",
53
+ plugins: [backgroundRuntimeGuardPlugin(import.meta.url)],
54
+ minify: false,
55
+ define,
56
+ })
57
+
58
+ if (!backgroundResult.success) {
59
+ console.error("Background build failed:")
60
+ for (const log of backgroundResult.logs) console.error(log)
61
+ process.exit(1)
62
+ }
63
+
64
+ const uiResult = await Bun.build({
65
+ entrypoints: ["./src/ui/index.html"],
66
+ outdir: `${distDir}/ui`,
67
+ target: "browser",
68
+ plugins: [reactSingletonPlugin(import.meta.url)],
69
+ minify: true,
70
+ define,
71
+ })
72
+
73
+ if (!uiResult.success) {
74
+ console.error("UI build failed:")
75
+ for (const log of uiResult.logs) console.error(log)
76
+ process.exit(1)
77
+ }
78
+
79
+ // Silence on success — the dev-server's `reload →` line is the
80
+ // developer-facing confirmation. Failures already print via the
81
+ // .success branches above.
Binary file
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "./node_modules/@glassly/miniapp-cli/schema/miniapp.schema.json",
3
+ "packageName": "{{packageName}}",
4
+ "version": "1.0.0",
5
+ "name": "My Miniapp",
6
+ "description": "A Glassly miniapp",
7
+ "icon": "icon.png",
8
+ "type": "standard",
9
+ "sdkVersion": "0.3.1",
10
+ "minHostVersion": "0.3.1",
11
+ "entry": {
12
+ "background": "background/index.js",
13
+ "ui": "ui/index.html"
14
+ },
15
+ "permissions": [],
16
+ "hardwareRequirements": [
17
+ {"type": "DISPLAY", "level": "REQUIRED", "description": "Renders text + UI on glasses"}
18
+ ]
19
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "{{packageName}}",
3
+ "private": true,
4
+ "scripts": {
5
+ "dev": "glassly dev",
6
+ "build": "bun run build.ts",
7
+ "typecheck": "tsc --noEmit",
8
+ "release": "glassly release",
9
+ "pack": "glassly pack"
10
+ },
11
+ "dependencies": {
12
+ "@glassly/miniapp": "0.3.1-dev.528",
13
+ "react": "^19.0.0",
14
+ "react-dom": "^19.0.0"
15
+ },
16
+ "devDependencies": {
17
+ "@glassly/cli": "^2.0.0-beta.4",
18
+ "@glassly/miniapp-cli": "^0.1.0-beta.4",
19
+ "@types/react": "^19.0.0",
20
+ "@types/react-dom": "^19.0.0",
21
+ "typescript": "^5.6.0"
22
+ }
23
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Background JSContext entry point. The Glassly host loads this file
3
+ * inside a per-miniapp JSContext (iOS-JSC / Android-QuickJS) and calls
4
+ * the `registerMiniapp` handler once CONNECT completes. All glasses-side
5
+ * logic lives in here.
6
+ *
7
+ * Background runs **always** while the host app process is alive — even
8
+ * when the user hasn't opened the miniapp's UI. Register your event
9
+ * subscriptions inside the handler; the runtime tears them down when the
10
+ * miniapp is disabled or uninstalled.
11
+ */
12
+
13
+ import {registerMiniapp} from "@glassly/miniapp/background"
14
+ import type {Channels} from "../shared/channels"
15
+
16
+ registerMiniapp<Channels>(async (session) => {
17
+ // Hydrate any persisted state from session.storage (string KV — JSON-encode
18
+ // structured data yourself).
19
+ // const stored = await session.storage.get("notes")
20
+ // const notes: Note[] = stored ? JSON.parse(stored) : []
21
+
22
+ // Glasses button → demo display. display.render() replaces the whole frame:
23
+ // describe everything that should be on screen and the host diffs it against
24
+ // the previous frame (stable ids update in place; render([]) clears). Box
25
+ // coordinates are raw device px — read `session.capabilities.display`
26
+ // (populated on the "ready" event) for the real canvas size.
27
+ session.input.onButtonPress(() => {
28
+ const d = session.capabilities?.display
29
+ void session.display.render([
30
+ {
31
+ type: "text",
32
+ id: "hello",
33
+ box: {x: 0, y: 0, w: d?.width ?? 576, h: d?.height ?? 288},
34
+ text: "Hello from JSRuntime",
35
+ },
36
+ ])
37
+ })
38
+
39
+ // UI bus: forward "ping" → "pong" with a roundtrip timestamp.
40
+ session.ui.onOpen(() => {
41
+ // Push initial state to the WebView whenever it opens.
42
+ session.ui.send("pong", {at: Date.now(), roundtripMs: 0})
43
+ })
44
+ session.ui.on("ping", ({at}) => {
45
+ const roundtripMs = Date.now() - at
46
+ session.ui.send("pong", {at: Date.now(), roundtripMs})
47
+ })
48
+ })
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Typed channel registry — single source of truth for the names + payload
3
+ * shapes that flow between this miniapp's background JSContext and its UI
4
+ * WebView. Both halves import this file at build time; the bundler inlines
5
+ * the declarations so there's no runtime resolution.
6
+ *
7
+ * Add a key per channel. The TypeScript generic on `glassly.send` /
8
+ * `glassly.on` / `session.ui.send` / `session.ui.on` enforces names + payload
9
+ * shapes at compile time so the two halves can't drift.
10
+ */
11
+
12
+ export interface Channels {
13
+ // WebView → background
14
+ "ping": {at: number}
15
+
16
+ // background → WebView
17
+ "pong": {at: number; roundtripMs: number}
18
+ }
19
+
20
+ declare global {
21
+ // Augment the `glassly` global from @glassly/miniapp/ui with this miniapp's
22
+ // typed channel registry so authors get compile-time enforcement on every
23
+ // glassly.send / glassly.on call without re-declaring it.
24
+ // eslint-disable-next-line no-var
25
+ var glassly: import("@glassly/miniapp/ui").TypedUiGlobal<Channels>
26
+ }
@@ -0,0 +1,26 @@
1
+ import {useEffect, useState} from "react"
2
+
3
+ export function App() {
4
+ const [roundtripMs, setRoundtripMs] = useState<number | null>(null)
5
+
6
+ useEffect(() => {
7
+ const unsub = glassly.on("pong", ({roundtripMs}) => {
8
+ setRoundtripMs(roundtripMs)
9
+ })
10
+ return unsub
11
+ }, [])
12
+
13
+ const ping = () => {
14
+ glassly.send("ping", {at: Date.now()})
15
+ }
16
+
17
+ return (
18
+ <div className="app">
19
+ <h1>{`{{packageName}}`}</h1>
20
+ <p>Round-trip to background: {roundtripMs == null ? "—" : `${roundtripMs} ms`}</p>
21
+ <button type="button" onClick={ping}>
22
+ Ping background
23
+ </button>
24
+ </div>
25
+ )
26
+ }
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
6
+ <title>{{packageName}}</title>
7
+ <link rel="stylesheet" href="./styles.css" />
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="./main.tsx"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,30 @@
1
+ /**
2
+ * WebView entry point. Imports the `glassly` global from the auto-injected
3
+ * host shim and the typed `Channels` registry from src/shared/. The
4
+ * WebView has zero direct native access — all hardware calls (glasses
5
+ * display, sensors, BLE, etc.) go through `glassly.send(channel, payload)`
6
+ * to background, which translates them into `session.*` SDK calls.
7
+ */
8
+
9
+ import {createRoot} from "react-dom/client"
10
+ import {MiniappProvider} from "@glassly/miniapp/ui"
11
+ import "../shared/channels"
12
+ import {App} from "./App"
13
+ import "./styles.css"
14
+
15
+ const root = document.getElementById("root")
16
+ if (!root) {
17
+ throw new Error("Missing #root element in index.html")
18
+ }
19
+
20
+ // MiniappProvider syncs <html class="dark"> with the host color scheme.
21
+ // Purely a CSS theme bridge — does NOT construct a MiniappSession.
22
+ createRoot(root).render(
23
+ <MiniappProvider>
24
+ <App />
25
+ </MiniappProvider>,
26
+ )
27
+
28
+ // MUST call glassly.ready() on bootstrap so the host knows the WebView is
29
+ // mounted and can flush any buffered `session.ui.send` calls.
30
+ glassly.ready()
@@ -0,0 +1,43 @@
1
+ :root {
2
+ color-scheme: light dark;
3
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
4
+ }
5
+
6
+ body {
7
+ margin: 0;
8
+ background: #0b0b0b;
9
+ color: #f5f5f5;
10
+ min-height: 100vh;
11
+ display: flex;
12
+ align-items: center;
13
+ justify-content: center;
14
+ }
15
+
16
+ .app {
17
+ text-align: center;
18
+ padding: 24px;
19
+ }
20
+
21
+ h1 {
22
+ font-size: 1.5rem;
23
+ margin: 0 0 12px;
24
+ }
25
+
26
+ p {
27
+ margin: 8px 0 24px;
28
+ opacity: 0.7;
29
+ }
30
+
31
+ button {
32
+ font-size: 1rem;
33
+ padding: 12px 24px;
34
+ border-radius: 12px;
35
+ border: none;
36
+ background: #4a9eff;
37
+ color: white;
38
+ cursor: pointer;
39
+ }
40
+
41
+ button:active {
42
+ transform: scale(0.97);
43
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "jsx": "react-jsx",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "outDir": "./dist",
11
+ "rootDir": ".",
12
+ "lib": ["ES2022", "DOM", "DOM.Iterable"]
13
+ },
14
+ "include": ["src/**/*.ts", "src/**/*.tsx"]
15
+ }