@glasshome/widget-cli 0.14.3 → 0.14.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/package.json CHANGED
@@ -1,29 +1,44 @@
1
1
  {
2
2
  "name": "@glasshome/widget-cli",
3
- "version": "0.14.3",
3
+ "version": "0.14.4",
4
4
  "description": "CLI for creating and managing GlassHome widgets",
5
- "type": "module",
5
+ "keywords": [
6
+ "cli",
7
+ "dashboard",
8
+ "glasshome",
9
+ "home-assistant",
10
+ "scaffold",
11
+ "smart-home",
12
+ "widget"
13
+ ],
14
+ "homepage": "https://github.com/glasshome/widget-cli#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/glasshome/widget-cli/issues"
17
+ },
18
+ "license": "MIT",
19
+ "author": "GlassHome Labs",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/glasshome/widget-cli.git"
23
+ },
6
24
  "bin": {
7
25
  "glasshome-widget": "bin/cli.ts"
8
26
  },
9
- "scripts": {
10
- "cli": "bun bin/cli.ts",
11
- "test": "bun test",
12
- "typecheck": "tsc --noEmit",
13
- "check:dead": "fallow dead-code --fail-on-issues"
14
- },
15
27
  "files": [
16
28
  "bin",
17
29
  "src",
18
30
  "template"
19
31
  ],
20
- "devDependencies": {
21
- "@iconify/types": "^2.0.0",
22
- "@types/bun": "^1.4.0",
23
- "@types/semver": "^7.0.0",
24
- "fallow": "3.17.0",
25
- "playwright": "^1.40.0",
26
- "typescript": "^7.0.2"
32
+ "type": "module",
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "scripts": {
37
+ "cli": "bun bin/cli.ts",
38
+ "test": "bun test",
39
+ "typecheck": "tsc --noEmit",
40
+ "check:dead": "fallow dead-code --fail-on-issues",
41
+ "lint": "oxlint --type-aware . && oxfmt --check ."
27
42
  },
28
43
  "dependencies": {
29
44
  "@clack/prompts": "^1.6.0",
@@ -43,6 +58,17 @@
43
58
  "ts-morph": "^28.0.0",
44
59
  "vite-plugin-solid": "^2.11.0"
45
60
  },
61
+ "devDependencies": {
62
+ "@iconify/types": "^2.0.0",
63
+ "@types/bun": "^1.4.0",
64
+ "@types/semver": "^7.0.0",
65
+ "fallow": "3.17.0",
66
+ "oxfmt": "0.65.0",
67
+ "oxlint": "1.81.0",
68
+ "oxlint-tsgolint": "7.0.2001",
69
+ "playwright": "^1.40.0",
70
+ "typescript": "^7.0.2"
71
+ },
46
72
  "peerDependencies": {
47
73
  "playwright": "^1.40.0",
48
74
  "vite": "^7.3.1"
@@ -52,29 +78,7 @@
52
78
  "optional": true
53
79
  }
54
80
  },
55
- "repository": {
56
- "type": "git",
57
- "url": "https://github.com/glasshome/widget-cli.git"
58
- },
59
- "keywords": [
60
- "glasshome",
61
- "widget",
62
- "cli",
63
- "scaffold",
64
- "home-assistant",
65
- "dashboard",
66
- "smart-home"
67
- ],
68
- "homepage": "https://github.com/glasshome/widget-cli#readme",
69
- "bugs": {
70
- "url": "https://github.com/glasshome/widget-cli/issues"
71
- },
72
- "publishConfig": {
73
- "access": "public"
74
- },
75
81
  "engines": {
76
82
  "bun": ">=1.0.0"
77
- },
78
- "author": "GlassHome Labs",
79
- "license": "MIT"
83
+ }
80
84
  }
@@ -201,9 +201,7 @@ async function authorizeThisDevice(api: string, host: string, s: Spinner): Promi
201
201
  s.stop("Authorization code ready");
202
202
  log.info(`Open in browser: ${grant.verificationUriComplete}`);
203
203
  log.info(`Device code: ${grant.userCode}`);
204
- await import("open")
205
- .then((m) => m.default(grant.verificationUriComplete))
206
- .catch(() => {});
204
+ await import("open").then((m) => m.default(grant.verificationUriComplete)).catch(() => {});
207
205
 
208
206
  s.start("Waiting for authorization (approve in your browser)...");
209
207
  const outcome = await pollDeviceToken(api, grant);
@@ -217,11 +215,7 @@ async function authorizeThisDevice(api: string, host: string, s: Spinner): Promi
217
215
  return outcome.token;
218
216
  }
219
217
 
220
- async function obtainToken(
221
- api: string,
222
- opts: { reAuth?: boolean },
223
- s: Spinner,
224
- ): Promise<string> {
218
+ async function obtainToken(api: string, opts: { reAuth?: boolean }, s: Spinner): Promise<string> {
225
219
  const host = extractHost(api);
226
220
  if (opts.reAuth) {
227
221
  clearHostToken(host);
@@ -410,7 +404,9 @@ export async function runConnect(
410
404
  const api = apiUrl.replace(/\/$/, "");
411
405
  const token = await obtainToken(api, opts, s);
412
406
  if (!token) {
413
- log.warn("Authentication failed, widgets won't be connected. Log in at the dashboard first, then restart.");
407
+ log.warn(
408
+ "Authentication failed, widgets won't be connected. Log in at the dashboard first, then restart.",
409
+ );
414
410
  return;
415
411
  }
416
412
 
@@ -115,8 +115,7 @@ export async function runCreate() {
115
115
  rmSync(oldSrcIndex);
116
116
  }
117
117
 
118
- const projectDesc =
119
- projectDescription || `A GlassHome widget project: ${projectName}`;
118
+ const projectDesc = projectDescription || `A GlassHome widget project: ${projectName}`;
120
119
 
121
120
  // Replace project-level placeholders in package.json. Pin the CLI to the
122
121
  // version that scaffolded the project (caret) so `bun widget` resolves the
@@ -62,7 +62,9 @@ export async function runInfo(cwd: string, widgetName?: string): Promise<void> {
62
62
 
63
63
  note(lines.join("\n"), `${manifest.name} ${color.dim(`(${name})`)}`);
64
64
  } catch (err) {
65
- log.warn(`${name}: could not read manifest: ${err instanceof Error ? err.message : String(err)}`);
65
+ log.warn(
66
+ `${name}: could not read manifest: ${err instanceof Error ? err.message : String(err)}`,
67
+ );
66
68
  }
67
69
  }
68
70
 
@@ -18,7 +18,9 @@ async function requirePlaywright(): Promise<void> {
18
18
  await import("playwright");
19
19
  } catch {
20
20
  log.error("Preview needs Playwright + a Chromium build, which are not installed.");
21
- log.info(`Install them with:\n ${color.bold("bun add -d playwright && bunx playwright install chromium")}`);
21
+ log.info(
22
+ `Install them with:\n ${color.bold("bun add -d playwright && bunx playwright install chromium")}`,
23
+ );
22
24
  process.exit(1);
23
25
  }
24
26
  }
@@ -125,7 +127,10 @@ function parseSizes(value: string | boolean | undefined): Box[] {
125
127
  .split(",")
126
128
  .map((s) => {
127
129
  const m = s.trim().match(/^(\d+)x(\d+)$/);
128
- if (!m) fail(`--sizes takes grid, or WIDTHxHEIGHT pairs in pixels like 150x156,340x242 (got "${s}")`);
130
+ if (!m)
131
+ fail(
132
+ `--sizes takes grid, or WIDTHxHEIGHT pairs in pixels like 150x156,340x242 (got "${s}")`,
133
+ );
129
134
  return { width: Number(m[1]), height: Number(m[2]) };
130
135
  });
131
136
  }
@@ -133,7 +138,8 @@ function parseSizes(value: string | boolean | undefined): Box[] {
133
138
  function parseThemes(value: string | undefined): Theme[] {
134
139
  if (!value) return ["light", "dark"];
135
140
  const themes = value.split(",");
136
- for (const t of themes) if (t !== "light" && t !== "dark") fail(`--theme is light, dark or both (got "${t}")`);
141
+ for (const t of themes)
142
+ if (t !== "light" && t !== "dark") fail(`--theme is light, dark or both (got "${t}")`);
137
143
  return themes as Theme[];
138
144
  }
139
145
 
@@ -151,12 +157,18 @@ function parseConfig(value: string | undefined): Record<string, unknown> | undef
151
157
  * overridden, into `<project>/preview/sweep/`, plus a contact sheet per example
152
158
  * and theme when several sizes are rendered.
153
159
  */
154
- export async function runSweepPreview(cwd: string, names: string[], flags: SweepFlags): Promise<void> {
160
+ export async function runSweepPreview(
161
+ cwd: string,
162
+ names: string[],
163
+ flags: SweepFlags,
164
+ ): Promise<void> {
155
165
  await requirePlaywright();
156
166
  const at = flags.at ? new Date(flags.at) : undefined;
157
- if (at && Number.isNaN(at.getTime())) fail(`--at takes an ISO time like 2026-06-15T21:00:00 (got "${flags.at}")`);
167
+ if (at && Number.isNaN(at.getTime()))
168
+ fail(`--at takes an ISO time like 2026-06-15T21:00:00 (got "${flags.at}")`);
158
169
  const examples = flags.example ? flags.example.split(",").map(Number) : [];
159
- if (examples.some((n) => !Number.isInteger(n) || n < 0)) fail("--example takes example numbers, starting at 0");
170
+ if (examples.some((n) => !Number.isInteger(n) || n < 0))
171
+ fail("--example takes example numbers, starting at 0");
160
172
 
161
173
  const summary = await withQuietStdout(() =>
162
174
  runSweep({
@@ -75,7 +75,9 @@ export async function runPublish(
75
75
  if (options?.scope) {
76
76
  const match = scopes.find((s) => s.name === options.scope);
77
77
  if (!match) {
78
- log.error(`Scope @${options.scope} not found. Available: ${scopes.map((s) => s.name).join(", ")}`);
78
+ log.error(
79
+ `Scope @${options.scope} not found. Available: ${scopes.map((s) => s.name).join(", ")}`,
80
+ );
79
81
  process.exit(1);
80
82
  }
81
83
  scope = match.name;
@@ -145,7 +147,9 @@ export async function runPublish(
145
147
  let bump: string;
146
148
  if (options?.bump) {
147
149
  bump = options.bump;
148
- log.info(`Version: ${currentVersion} → ${bump === "keep" ? currentVersion : semver.inc(currentVersion, bump as semver.ReleaseType) ?? currentVersion}`);
150
+ log.info(
151
+ `Version: ${currentVersion} → ${bump === "keep" ? currentVersion : (semver.inc(currentVersion, bump as semver.ReleaseType) ?? currentVersion)}`,
152
+ );
149
153
  } else {
150
154
  const choice = await select({
151
155
  message: `${manifest.name} version: ${currentVersion}. Bump?`,
@@ -246,9 +250,7 @@ export async function runPublish(
246
250
  version,
247
251
  bundleSize: bundleBuffer.byteLength,
248
252
  sha256Hash,
249
- ...(cssBuffer && cssSha256Hash
250
- ? { cssSize: cssBuffer.byteLength, cssSha256Hash }
251
- : {}),
253
+ ...(cssBuffer && cssSha256Hash ? { cssSize: cssBuffer.byteLength, cssSha256Hash } : {}),
252
254
  manifestJson: JSON.stringify(builtManifest),
253
255
  });
254
256
  } catch (err) {
@@ -41,7 +41,11 @@ function validateManifest(manifest: WidgetManifest, installedSdk: string | null)
41
41
  if (!schemaResult.success) {
42
42
  errors.push(`Manifest schema: ${formatSchemaError(schemaResult.error)}`);
43
43
  const capabilities = (manifest as { capabilities?: unknown }).capabilities;
44
- if (manifest.sdkVersion && requiresCapabilities(manifest.sdkVersion) && capabilities === undefined) {
44
+ if (
45
+ manifest.sdkVersion &&
46
+ requiresCapabilities(manifest.sdkVersion) &&
47
+ capabilities === undefined
48
+ ) {
45
49
  errors.push(
46
50
  `sdkVersion "${manifest.sdkVersion}" only admits SDK >= 1.0.0, so manifest.json must declare "capabilities", use [] if the widget never reads or controls Home Assistant`,
47
51
  );
@@ -48,7 +48,9 @@ describe("migrateConfigSource, field kinds", () => {
48
48
  expect(r.warnings).toEqual([]);
49
49
  expect(r.code).toContain("analogOptions: field.group({");
50
50
  expect(r.code).toContain('border: field.toggle({ title: "Show Border", default: false }),');
51
- expect(r.code).toContain('ticks: field.choice(["none", "hour"], { title: "Tick Marks", default: "hour" }),');
51
+ expect(r.code).toContain(
52
+ 'ticks: field.choice(["none", "hour"], { title: "Tick Marks", default: "hour" }),',
53
+ );
52
54
  expect(r.code).toContain('}, { title: "Analog Options" }),');
53
55
  });
54
56
 
@@ -40,9 +40,7 @@ export interface EgressLock {
40
40
  * closer analogue of the container egress allowlist the worker will actually
41
41
  * rely on.
42
42
  */
43
- const NO_EGRESS_ARGS = [
44
- "--host-resolver-rules=MAP * ~NOTFOUND, EXCLUDE localhost",
45
- ];
43
+ const NO_EGRESS_ARGS = ["--host-resolver-rules=MAP * ~NOTFOUND, EXCLUDE localhost"];
46
44
 
47
45
  /** `CHROMIUM_PATH` points at a system Chromium where Playwright's own build is absent or cannot run. */
48
46
  const launchLocked = () =>
@@ -163,12 +161,10 @@ const CONTEXT_OPTIONS = { deviceScaleFactor: 2, reducedMotion: "reduce" } as con
163
161
  async function hardClose(browser: Browser): Promise<void> {
164
162
  // `process()` exists at runtime on a launched Chromium but is absent from the
165
163
  // base Browser type; narrow it here rather than at the call site.
166
- const proc = (browser as Browser & { process?: () => { kill: (s: string) => void } | null })
167
- .process?.();
168
- await Promise.race([
169
- browser.close().catch(() => {}),
170
- new Promise((r) => setTimeout(r, 3_000)),
171
- ]);
164
+ const proc = (
165
+ browser as Browser & { process?: () => { kill: (s: string) => void } | null }
166
+ ).process?.();
167
+ await Promise.race([browser.close().catch(() => {}), new Promise((r) => setTimeout(r, 3_000))]);
172
168
  proc?.kill("SIGKILL");
173
169
  }
174
170
 
@@ -230,7 +226,9 @@ export async function withSharedBrowser<T>(fn: (browser: SharedBrowser) => Promi
230
226
  * The worker must not reuse a process across widgets — one widget's residue
231
227
  * (globals, caches, service workers) must never reach another's shot.
232
228
  */
233
- export async function withFreshBrowser<T>(fn: (page: Page, browser: Browser) => Promise<T>): Promise<T> {
229
+ export async function withFreshBrowser<T>(
230
+ fn: (page: Page, browser: Browser) => Promise<T>,
231
+ ): Promise<T> {
234
232
  // On NixOS the Playwright-downloaded Chromium can't find its system libs;
235
233
  // point nix-ld at them so the bundled browser launches unmodified.
236
234
  applyNixLdOnce();
@@ -135,7 +135,9 @@ export async function runSweep(opts: SweepOptions): Promise<SweepSummary> {
135
135
  state: "attached",
136
136
  timeout: 30_000,
137
137
  });
138
- examples = JSON.parse((await probe.getAttribute("html", "data-harness-examples")) ?? "[]");
138
+ examples = JSON.parse(
139
+ (await probe.getAttribute("html", "data-harness-examples")) ?? "[]",
140
+ );
139
141
  bounds = JSON.parse((await probe.getAttribute("html", "data-harness-bounds")) ?? "null");
140
142
  } finally {
141
143
  await probe.context().close();
@@ -187,7 +189,12 @@ export async function runSweep(opts: SweepOptions): Promise<SweepSummary> {
187
189
  if (shot.length > 1) {
188
190
  const page = await browser.newPage();
189
191
  try {
190
- await writeSheet(page, theme, shot, resolve(outDir, `${widget}-${label}-${theme}.png`));
192
+ await writeSheet(
193
+ page,
194
+ theme,
195
+ shot,
196
+ resolve(outDir, `${widget}-${label}-${theme}.png`),
197
+ );
191
198
  sheets++;
192
199
  } finally {
193
200
  await page
@@ -122,7 +122,9 @@ describe("deviceAuthorize", () => {
122
122
 
123
123
  test("throws when the server answers without a device code", async () => {
124
124
  const outcome = deviceAuthorize(API, () =>
125
- Promise.resolve(json(200, { user_code: "A", verification_uri: `${API}/device`, expires_in: 900 })),
125
+ Promise.resolve(
126
+ json(200, { user_code: "A", verification_uri: `${API}/device`, expires_in: 900 }),
127
+ ),
126
128
  );
127
129
  await expect(outcome).rejects.toThrow("no device code");
128
130
  });
package/src/utils/auth.ts CHANGED
@@ -35,9 +35,7 @@ function ensureDir(): void {
35
35
  function readAuthFile(): StoredAuthFile {
36
36
  if (!existsSync(AUTH_FILE)) return { hosts: {} };
37
37
  try {
38
- const raw = JSON.parse(readFileSync(AUTH_FILE, "utf-8")) as
39
- | StoredAuthFile
40
- | LegacyStoredAuth;
38
+ const raw = JSON.parse(readFileSync(AUTH_FILE, "utf-8")) as StoredAuthFile | LegacyStoredAuth;
41
39
  // Migrate legacy format (hub-only, no hosts key)
42
40
  if ("accessToken" in raw) {
43
41
  return { hub: raw as HubAuth, hosts: {} };
@@ -115,7 +115,7 @@ export async function confirmPublish(
115
115
  export async function uploadToR2(
116
116
  uploadUrl: string,
117
117
  bundleBuffer: Uint8Array,
118
- contentType: string = "application/javascript",
118
+ contentType = "application/javascript",
119
119
  ): Promise<void> {
120
120
  const res = await fetch(uploadUrl, {
121
121
  method: "PUT",
@@ -114,15 +114,18 @@ describe("runTypecheck", () => {
114
114
  expect(result.reason).toContain("not installed");
115
115
  });
116
116
 
117
- test.skipIf(noTypescript)("uses an ancestor's typescript, as hoisted and workspace installs give", () => {
118
- // Requiring a copy inside the project itself would skip every monorepo.
119
- const parent = project({ typescript: true });
120
- const child = join(parent, "nested");
121
- mkdirSync(join(child, "src"), { recursive: true });
122
- cpSync(join(parent, "package.json"), join(child, "package.json"));
123
- cpSync(join(parent, "tsconfig.json"), join(child, "tsconfig.json"));
124
- writeFileSync(join(child, "src", "index.ts"), `export const n: number = "nope";\n`);
117
+ test.skipIf(noTypescript)(
118
+ "uses an ancestor's typescript, as hoisted and workspace installs give",
119
+ () => {
120
+ // Requiring a copy inside the project itself would skip every monorepo.
121
+ const parent = project({ typescript: true });
122
+ const child = join(parent, "nested");
123
+ mkdirSync(join(child, "src"), { recursive: true });
124
+ cpSync(join(parent, "package.json"), join(child, "package.json"));
125
+ cpSync(join(parent, "tsconfig.json"), join(child, "tsconfig.json"));
126
+ writeFileSync(join(child, "src", "index.ts"), `export const n: number = "nope";\n`);
125
127
 
126
- expect(runTypecheck(child).status).toBe("failed");
127
- });
128
+ expect(runTypecheck(child).status).toBe("failed");
129
+ },
130
+ );
128
131
  });
@@ -107,10 +107,7 @@ async function getLatestVersions(): Promise<{ cli: string | null; sdk: string |
107
107
  if (cache && now - cache.lastCheck < UPDATE_CHECK_INTERVAL_MS) {
108
108
  return { cli: cache.cli, sdk: cache.sdk };
109
109
  }
110
- const [cli, sdk] = await Promise.all([
111
- fetchLatestVersion(CLI_PKG),
112
- fetchLatestVersion(SDK_PKG),
113
- ]);
110
+ const [cli, sdk] = await Promise.all([fetchLatestVersion(CLI_PKG), fetchLatestVersion(SDK_PKG)]);
114
111
  const merged = { cli: cli ?? cache?.cli ?? null, sdk: sdk ?? cache?.sdk ?? null };
115
112
  writeUpdateCache({ lastCheck: now, ...merged });
116
113
  return merged;