@glasshome/widget-cli 0.4.9 → 0.4.11

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@glasshome/widget-cli",
3
- "version": "0.4.9",
3
+ "version": "0.4.11",
4
4
  "description": "CLI for creating and managing GlassHome widgets",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,7 +21,7 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@clack/prompts": "^0.10.0",
24
- "@glasshome/widget-sdk": "^0.4.0",
24
+ "@glasshome/widget-sdk": "^0.5.0",
25
25
  "open": "^11.0.0",
26
26
  "semver": "^7.6.0",
27
27
  "vite-plugin-solid": "^2.11.0"
@@ -165,15 +165,16 @@ export async function runConnect(apiUrl: string, cwd: string): Promise<void> {
165
165
  const data = (await res.json()) as {
166
166
  device_code: string;
167
167
  user_code: string;
168
- verification_uri_complete: string;
168
+ verification_uri: string;
169
+ verification_uri_complete?: string;
169
170
  expires_in: number;
170
171
  interval: number;
171
172
  };
172
173
  deviceCode = data.device_code;
173
174
  userCode = data.user_code;
174
- // Build verification URL from the dashboard URL the user provided,
175
- // not the API response (which uses the API server's origin)
176
- verificationUriComplete = `${api}/device?user_code=${data.user_code}`;
175
+ verificationUriComplete =
176
+ data.verification_uri_complete ??
177
+ `${data.verification_uri}?user_code=${encodeURIComponent(data.user_code)}`;
177
178
  expiresIn = data.expires_in;
178
179
  interval = data.interval ?? 5;
179
180
  } catch (err) {
@@ -215,12 +216,19 @@ export async function runConnect(apiUrl: string, cwd: string): Promise<void> {
215
216
  });
216
217
 
217
218
  if (res.ok) {
219
+ // better-auth's device-authorization plugin returns a
220
+ // first-party session, not an OIDC token bundle. The bearer is
221
+ // session.token; expiry is session.expiresAt (ISO/epoch/Date).
218
222
  const data = (await res.json()) as {
219
- access_token: string;
220
- expires_in: number;
223
+ session?: { token: string; expiresAt: string | number | Date };
224
+ user?: unknown;
221
225
  };
222
- token = data.access_token;
223
- const tokenExpiresAt = Date.now() + data.expires_in * 1000;
226
+ if (!data.session?.token) {
227
+ s.stop("Auth response missing session token");
228
+ break;
229
+ }
230
+ token = data.session.token;
231
+ const tokenExpiresAt = new Date(data.session.expiresAt).getTime();
224
232
  storeHostToken(host, token, tokenExpiresAt);
225
233
  break;
226
234
  }
@@ -287,35 +295,66 @@ export async function runConnect(apiUrl: string, cwd: string): Promise<void> {
287
295
  const registeredTags = await uploadAllWidgets(apiUrl, distDir, token);
288
296
  s.stop("Widgets registered");
289
297
 
290
- // Step 6: Watch src/ for changes and rebuild + re-upload only the changed widget
298
+ // Step 6: Watch src/ for changes and rebuild + re-upload only the changed widget.
299
+ //
300
+ // Debounce strategy: collect changed widget names into `pending`, fire after
301
+ // DEBOUNCE_MS of quiet. If new changes arrive while a build is in flight,
302
+ // they accumulate and trigger one more pass after the current build finishes.
303
+ // Replaces a previous `rebuilding` boolean guard that silently dropped events
304
+ // arriving during a build (lost edits) and was vulnerable to Linux fs.watch
305
+ // coalescing two rapid writes into one event with stale content.
291
306
  const srcDir = resolve(cwd, "src");
292
- let rebuilding = false;
293
- const watcher = watch(srcDir, { recursive: true }, async (_event, filename) => {
294
- if (rebuilding || !filename) return;
295
- rebuilding = true;
307
+ const DEBOUNCE_MS = 150;
308
+ const pending = new Set<string>();
309
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null;
310
+ let buildInFlight = false;
311
+
312
+ async function flush(): Promise<void> {
313
+ debounceTimer = null;
314
+ if (buildInFlight) return;
315
+ if (pending.size === 0) return;
316
+
317
+ const widgets = [...pending];
318
+ pending.clear();
319
+ buildInFlight = true;
320
+
296
321
  try {
297
- // Determine which widget changed from the file path (first path segment under src/)
298
- const widgetName = filename.split(/[\\/]/)[0] ?? filename;
299
322
  const origCwd = process.cwd();
300
323
  process.chdir(cwd);
301
- await buildWidgets({ ...buildOpts, only: [widgetName] });
302
- process.chdir(origCwd);
324
+ try {
325
+ await buildWidgets({ ...buildOpts, only: widgets });
326
+ } finally {
327
+ process.chdir(origCwd);
328
+ }
303
329
 
304
- // Upload only the changed widget
305
330
  const registryPath = resolve(distDir, "registry.json");
306
331
  const registry: RegistryJson = JSON.parse(readFileSync(registryPath, "utf-8"));
307
- const widget = registry.widgets.find((w) => w.bundleUrl === `./${widgetName}.js`);
308
- if (widget) {
309
- await uploadAndRegister(apiUrl, distDir, widget, token);
310
- log.info(`Rebuilt & uploaded ${widgetName}`);
311
- } else {
312
- log.warn(`No registry entry for ${widgetName} — skipping upload`);
332
+ for (const widgetName of widgets) {
333
+ const widget = registry.widgets.find((w) => w.bundleUrl === `./${widgetName}.js`);
334
+ if (widget) {
335
+ await uploadAndRegister(apiUrl, distDir, widget, token);
336
+ log.info(`Rebuilt & uploaded ${widgetName}`);
337
+ } else {
338
+ log.warn(`No registry entry for ${widgetName} — skipping upload`);
339
+ }
313
340
  }
314
341
  } catch (err) {
315
342
  log.warn(`Rebuild failed: ${err instanceof Error ? err.message : String(err)}`);
316
343
  } finally {
317
- rebuilding = false;
344
+ buildInFlight = false;
345
+ // Re-flush if events accumulated during this build.
346
+ if (pending.size > 0) {
347
+ flush();
348
+ }
318
349
  }
350
+ }
351
+
352
+ const watcher = watch(srcDir, { recursive: true }, (_event, filename) => {
353
+ if (!filename) return;
354
+ const widgetName = filename.split(/[\\/]/)[0] ?? filename;
355
+ pending.add(widgetName);
356
+ if (debounceTimer) clearTimeout(debounceTimer);
357
+ debounceTimer = setTimeout(flush, DEBOUNCE_MS);
319
358
  });
320
359
 
321
360
  // Count widgets
@@ -335,6 +374,7 @@ export async function runConnect(apiUrl: string, cwd: string): Promise<void> {
335
374
  // Step 7: Handle SIGINT — unregister and clean up
336
375
  const cleanup = async () => {
337
376
  log.info("\nDisconnecting...");
377
+ if (debounceTimer) clearTimeout(debounceTimer);
338
378
  watcher.close();
339
379
 
340
380
  for (const slug of registeredTags) {
@@ -4,10 +4,12 @@ import { log, spinner } from "@clack/prompts";
4
4
  import open from "open";
5
5
  import { getHubUrl, storeToken } from "../utils/auth";
6
6
 
7
+ // Public client: PKCE authenticates it, no client secret. The redirect is an
8
+ // IP-loopback URI (RFC 8252) registered on the hub.
7
9
  const CLIENT_ID = "glasshome-widget-cli";
8
- const CLIENT_SECRET = "glasshome-cli-public";
10
+ const REDIRECT_HOST = "127.0.0.1";
9
11
  const REDIRECT_PORT = 9274;
10
- const REDIRECT_URI = `http://localhost:${REDIRECT_PORT}/callback`;
12
+ const REDIRECT_URI = `http://${REDIRECT_HOST}:${REDIRECT_PORT}/callback`;
11
13
  const LOGIN_TIMEOUT_MS = 120_000;
12
14
 
13
15
  function base64url(buf: Buffer): string {
@@ -35,7 +37,7 @@ export async function runLogin(hubUrl?: string): Promise<void> {
35
37
  }, LOGIN_TIMEOUT_MS);
36
38
 
37
39
  const server = createServer((req: IncomingMessage, res: ServerResponse) => {
38
- const url = new URL(req.url!, `http://localhost:${REDIRECT_PORT}`);
40
+ const url = new URL(req.url!, `http://${REDIRECT_HOST}:${REDIRECT_PORT}`);
39
41
 
40
42
  if (url.pathname !== "/callback") {
41
43
  res.writeHead(404);
@@ -70,7 +72,7 @@ export async function runLogin(hubUrl?: string): Promise<void> {
70
72
  resolve({ code, receivedState });
71
73
  });
72
74
 
73
- server.listen(REDIRECT_PORT, () => {
75
+ server.listen(REDIRECT_PORT, REDIRECT_HOST, () => {
74
76
  const authUrl = new URL(`${hub}/api/auth/oauth2/authorize`);
75
77
  authUrl.searchParams.set("client_id", CLIENT_ID);
76
78
  authUrl.searchParams.set("redirect_uri", REDIRECT_URI);
@@ -109,7 +111,6 @@ export async function runLogin(hubUrl?: string): Promise<void> {
109
111
  redirect_uri: REDIRECT_URI,
110
112
  code_verifier: codeVerifier,
111
113
  client_id: CLIENT_ID,
112
- client_secret: CLIENT_SECRET,
113
114
  }),
114
115
  });
115
116
 
package/src/utils/auth.ts CHANGED
@@ -114,7 +114,6 @@ export async function getToken(hubUrl: string): Promise<string | null> {
114
114
  grant_type: "refresh_token",
115
115
  refresh_token: stored.refreshToken,
116
116
  client_id: "glasshome-widget-cli",
117
- client_secret: "glasshome-cli-public",
118
117
  }),
119
118
  });
120
119