@glasshome/widget-cli 0.4.2 → 0.4.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,6 +1,6 @@
1
1
  {
2
2
  "name": "@glasshome/widget-cli",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "CLI for creating and managing GlassHome widgets",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,6 +15,11 @@
15
15
  "src",
16
16
  "template"
17
17
  ],
18
+ "devDependencies": {
19
+ "@types/bun": "latest",
20
+ "@types/semver": "^7.0.0",
21
+ "vite-plugin-solid": "^2.11.0"
22
+ },
18
23
  "dependencies": {
19
24
  "@clack/prompts": "^0.10.0",
20
25
  "@glasshome/widget-sdk": "0.2.3",
@@ -6,7 +6,6 @@ import { trpcMutate, trpcQuery } from "../utils/api";
6
6
  import { extractHost, getHostToken, storeHostToken } from "../utils/auth";
7
7
 
8
8
  interface RegistryWidget {
9
- tag: string;
10
9
  name: string;
11
10
  version: string;
12
11
  bundleUrl: string;
@@ -29,20 +28,18 @@ async function uploadAndRegister(
29
28
  token: string,
30
29
  ): Promise<void> {
31
30
  const api = apiUrl.replace(/\/$/, "");
31
+ // Derive slug from bundleUrl (e.g. "./area.js" → "area")
32
+ const slug = widget.bundleUrl.replace(/^\.\//, "").replace(/\.js$/, "");
32
33
 
33
- // Read the bundle file from dist/ (bundleUrl is relative like "./area.js")
34
- const bundleFilename = widget.bundleUrl.replace(/^\.\//, "");
35
- const bundlePath = resolve(distDir, bundleFilename);
36
-
34
+ const bundlePath = resolve(distDir, `${slug}.js`);
37
35
  if (!existsSync(bundlePath)) {
38
- log.warn(`Bundle not found: ${bundlePath} — skipping ${widget.tag}`);
36
+ log.warn(`Bundle not found: ${bundlePath} — skipping ${slug}`);
39
37
  return;
40
38
  }
41
39
 
42
40
  const bundleContent = readFileSync(bundlePath, "utf-8");
43
41
 
44
- // Upload bundle to API (local registry namespace)
45
- const uploadRes = await fetch(`${api}/bundles/local/${widget.tag}`, {
42
+ const uploadRes = await fetch(`${api}/bundles/local/local/${slug}`, {
46
43
  method: "POST",
47
44
  headers: {
48
45
  "Content-Type": "application/javascript",
@@ -52,10 +49,9 @@ async function uploadAndRegister(
52
49
  });
53
50
 
54
51
  if (!uploadRes.ok) {
55
- throw new Error(`Failed to upload bundle for ${widget.tag}: HTTP ${uploadRes.status}`);
52
+ throw new Error(`Failed to upload bundle for ${slug}: HTTP ${uploadRes.status}`);
56
53
  }
57
54
 
58
- // Register the widget with a local bundle path (skips redundant download)
59
55
  const manifest = { ...widget };
60
56
  delete (manifest as Record<string, unknown>).bundleUrl;
61
57
 
@@ -64,10 +60,10 @@ async function uploadAndRegister(
64
60
  path: "widget.register",
65
61
  token,
66
62
  input: {
67
- tag: widget.tag,
68
- name: widget.name,
63
+ scope: "local",
64
+ name: slug,
69
65
  version: widget.version,
70
- bundleUrl: `/bundles/local/${widget.tag}/bundle.js`,
66
+ bundleUrl: `/bundles/local/local/${slug}/bundle.js`,
71
67
  manifestJson: JSON.stringify(manifest),
72
68
  },
73
69
  });
@@ -75,19 +71,20 @@ async function uploadAndRegister(
75
71
 
76
72
  /**
77
73
  * Read registry.json and upload all widget bundles.
78
- * @returns Array of widget tags that were registered
74
+ * @returns Array of widget slugs that were registered
79
75
  */
80
76
  async function uploadAllWidgets(apiUrl: string, distDir: string, token: string): Promise<string[]> {
81
77
  const registryPath = resolve(distDir, "registry.json");
82
78
  const registry: RegistryJson = JSON.parse(readFileSync(registryPath, "utf-8"));
83
- const tags: string[] = [];
79
+ const slugs: string[] = [];
84
80
 
85
81
  for (const widget of registry.widgets) {
82
+ const slug = widget.bundleUrl.replace(/^\.\//, "").replace(/\.js$/, "");
86
83
  await uploadAndRegister(apiUrl, distDir, widget, token);
87
- tags.push(widget.tag);
84
+ slugs.push(slug);
88
85
  }
89
86
 
90
- return tags;
87
+ return slugs;
91
88
  }
92
89
 
93
90
  export async function runConnect(apiUrl: string, cwd: string): Promise<void> {
@@ -122,11 +119,25 @@ export async function runConnect(apiUrl: string, cwd: string): Promise<void> {
122
119
  const host = extractHost(api);
123
120
  let token = "";
124
121
 
122
+ // Validate stored token before using it
125
123
  const existingToken = getHostToken(host);
126
124
  if (existingToken) {
127
- token = existingToken;
128
- log.info("Using stored credentials for dashboard.");
129
- } else {
125
+ try {
126
+ const check = await fetch(`${api}/api/auth/get-session`, {
127
+ headers: { Authorization: `Bearer ${existingToken}` },
128
+ });
129
+ if (check.ok) {
130
+ token = existingToken;
131
+ log.info("Using stored credentials for dashboard.");
132
+ } else {
133
+ log.warn("Stored credentials expired — re-authenticating.");
134
+ }
135
+ } catch {
136
+ log.warn("Could not validate stored credentials — re-authenticating.");
137
+ }
138
+ }
139
+
140
+ if (!token) {
130
141
  // Request a device code
131
142
  s.start("Requesting authorization code...");
132
143
  let deviceCode: string;
@@ -160,88 +171,89 @@ export async function runConnect(apiUrl: string, cwd: string): Promise<void> {
160
171
  interval = data.interval ?? 5;
161
172
  } catch (err) {
162
173
  s.stop("Failed to request device code");
163
- log.error(
174
+ log.warn(
164
175
  `Could not reach dashboard at ${api}: ${err instanceof Error ? err.message : String(err)}`,
165
176
  );
166
- process.exit(1);
177
+ // token stays empty — caught by guard below
167
178
  }
168
- s.stop("Authorization code ready");
169
-
170
- // Prompt user to visit the verification URL
171
- log.info(`Open in browser: ${verificationUriComplete}`);
172
- log.info(`Device code: ${userCode}`);
173
-
174
- // Try to open browser automatically
175
- await import("open")
176
- .then((m) => m.default(verificationUriComplete))
177
- .catch(() => {});
178
-
179
- // Poll for approval
180
- s.start("Waiting for authorization (approve in your browser)...");
181
- const deadline = Date.now() + expiresIn * 1000;
182
- let approved = false;
183
179
 
184
- while (Date.now() < deadline) {
185
- await new Promise((r) => setTimeout(r, interval * 1000));
186
-
187
- try {
188
- const res = await fetch(`${api}/api/auth/device/token`, {
189
- method: "POST",
190
- headers: { "Content-Type": "application/json" },
191
- body: JSON.stringify({
192
- grant_type: "urn:ietf:params:oauth:grant-type:device_code",
193
- device_code: deviceCode,
194
- client_id: "glasshome-widget-cli",
195
- }),
196
- });
180
+ if (deviceCode!) {
181
+ s.stop("Authorization code ready");
182
+
183
+ // Prompt user to visit the verification URL
184
+ log.info(`Open in browser: ${verificationUriComplete!}`);
185
+ log.info(`Device code: ${userCode!}`);
186
+
187
+ // Try to open browser automatically
188
+ await import("open")
189
+ .then((m) => m.default(verificationUriComplete!))
190
+ .catch(() => {});
191
+
192
+ // Poll for approval
193
+ s.start("Waiting for authorization (approve in your browser)...");
194
+ const deadline = Date.now() + expiresIn! * 1000;
195
+
196
+ while (Date.now() < deadline) {
197
+ await new Promise((r) => setTimeout(r, interval! * 1000));
198
+
199
+ try {
200
+ const res = await fetch(`${api}/api/auth/device/token`, {
201
+ method: "POST",
202
+ headers: { "Content-Type": "application/json" },
203
+ body: JSON.stringify({
204
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
205
+ device_code: deviceCode,
206
+ client_id: "glasshome-widget-cli",
207
+ }),
208
+ });
209
+
210
+ if (res.ok) {
211
+ const data = (await res.json()) as {
212
+ access_token: string;
213
+ expires_in: number;
214
+ };
215
+ token = data.access_token;
216
+ const tokenExpiresAt = Date.now() + data.expires_in * 1000;
217
+ storeHostToken(host, token, tokenExpiresAt);
218
+ break;
219
+ }
197
220
 
198
- if (res.ok) {
199
- const data = (await res.json()) as {
200
- access_token: string;
201
- expires_in: number;
202
- };
203
- token = data.access_token;
204
- const tokenExpiresAt = Date.now() + data.expires_in * 1000;
205
- storeHostToken(host, token, tokenExpiresAt);
206
- approved = true;
221
+ const errData = (await res.json()) as { error?: string };
222
+ if (errData.error === "authorization_pending" || errData.error === "slow_down") {
223
+ if (errData.error === "slow_down") {
224
+ interval = Math.min(interval! + 5, 30);
225
+ }
226
+ continue;
227
+ }
228
+ if (errData.error === "access_denied") {
229
+ s.stop("Authorization denied");
230
+ break;
231
+ }
232
+ if (errData.error === "expired_token") {
233
+ s.stop("Device code expired");
234
+ break;
235
+ }
236
+ // Unexpected error — stop polling
237
+ s.stop(`Auth error: ${errData.error ?? res.status}`);
238
+ break;
239
+ } catch (err) {
240
+ s.stop("Authorization failed");
241
+ log.warn(`Error polling for token: ${err instanceof Error ? err.message : String(err)}`);
207
242
  break;
208
243
  }
244
+ }
209
245
 
210
- const errData = (await res.json()) as { error?: string };
211
- if (errData.error === "authorization_pending" || errData.error === "slow_down") {
212
- // Continue polling
213
- if (errData.error === "slow_down") {
214
- interval = Math.min(interval + 5, 30);
215
- }
216
- continue;
217
- }
218
- if (errData.error === "access_denied") {
219
- s.stop("Authorization denied");
220
- log.error("You denied access. Run `bun widget connect` again to retry.");
221
- process.exit(1);
222
- }
223
- if (errData.error === "expired_token") {
224
- s.stop("Device code expired");
225
- log.error("The device code expired before you approved. Run `bun widget connect` again.");
226
- process.exit(1);
227
- }
228
- // Unexpected error
229
- throw new Error(errData.error ?? `HTTP ${res.status}`);
230
- } catch (err) {
231
- s.stop("Authorization failed");
232
- log.error(
233
- `Error polling for token: ${err instanceof Error ? err.message : String(err)}`,
234
- );
235
- process.exit(1);
246
+ if (token) {
247
+ s.stop("Authorized");
248
+ } else {
249
+ s.stop("Not authorized");
236
250
  }
237
251
  }
252
+ }
238
253
 
239
- if (!approved) {
240
- s.stop("Timed out");
241
- log.error("Authorization timed out. Run `bun widget connect` again.");
242
- process.exit(1);
243
- }
244
- s.stop("Authorized");
254
+ if (!token) {
255
+ log.warn("Authentication failed — widgets won't be connected. Log in at the dashboard first, then restart.");
256
+ return;
245
257
  }
246
258
 
247
259
  // Step 4: Enable dev mode if needed
@@ -288,7 +300,7 @@ export async function runConnect(apiUrl: string, cwd: string): Promise<void> {
288
300
  const widget = registry.widgets.find((w) => w.bundleUrl === `./${widgetName}.js`);
289
301
  if (widget) {
290
302
  await uploadAndRegister(apiUrl, distDir, widget, token);
291
- log.info(`Rebuilt & uploaded ${widget.tag}`);
303
+ log.info(`Rebuilt & uploaded ${widgetName}`);
292
304
  } else {
293
305
  log.warn(`No registry entry for ${widgetName} — skipping upload`);
294
306
  }
@@ -318,13 +330,13 @@ export async function runConnect(apiUrl: string, cwd: string): Promise<void> {
318
330
  log.info("\nDisconnecting...");
319
331
  watcher.close();
320
332
 
321
- for (const tag of registeredTags) {
333
+ for (const slug of registeredTags) {
322
334
  try {
323
335
  await trpcMutate({
324
336
  apiUrl: api,
325
337
  path: "widget.unregister",
326
338
  token,
327
- input: { tag },
339
+ input: { scope: "local", name: slug },
328
340
  });
329
341
  } catch {
330
342
  // Non-fatal — API may be down
@@ -107,11 +107,11 @@ export async function confirmPublish(
107
107
  return res.json() as Promise<PublishConfirmResponse>;
108
108
  }
109
109
 
110
- export async function uploadToR2(uploadUrl: string, bundleBuffer: Buffer): Promise<void> {
110
+ export async function uploadToR2(uploadUrl: string, bundleBuffer: Uint8Array): Promise<void> {
111
111
  const res = await fetch(uploadUrl, {
112
112
  method: "PUT",
113
113
  headers: { "Content-Type": "application/javascript" },
114
- body: bundleBuffer,
114
+ body: bundleBuffer as unknown as BodyInit,
115
115
  });
116
116
 
117
117
  if (!res.ok) {