@glasshome/widget-cli 0.2.0 → 0.3.0

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 CHANGED
@@ -27,13 +27,25 @@ bunx @glasshome/widget-cli create
27
27
  | `build` | Build all widgets |
28
28
  | `connect <url>` | Connect to a running dashboard for live testing |
29
29
  | `validate [name]` | Validate all widgets or a specific one |
30
- | `publish` | Build and publish to GlassHome Hub |
31
- | `login [hub-url]` | Authenticate with GlassHome Hub |
30
+ | `publish [hub-url]` | Select and publish a widget to GlassHome Hub |
31
+ | `login [hub-url]` | Authenticate with GlassHome Hub (required before publish) |
32
32
  | `info [name]` | Show widget metadata and bundle info |
33
33
  | `upgrade` | Upgrade @glasshome/widget-sdk to latest version |
34
34
 
35
35
  Running `glasshome-widget` with no command inside a widget project shows help. Outside a widget project, it starts the create wizard.
36
36
 
37
+ ## Publishing
38
+
39
+ ```bash
40
+ # Login first (one-time)
41
+ glasshome-widget login
42
+
43
+ # Then publish
44
+ glasshome-widget publish
45
+ ```
46
+
47
+ The CLI prompts you to select a widget, choose a version bump, then builds and publishes. Each widget tracks its own version in its `manifest.json`.
48
+
37
49
  ## Documentation
38
50
 
39
51
  Full docs at [glasshome.app/docs](https://glasshome.app/docs)
package/bin/cli.ts CHANGED
@@ -26,7 +26,7 @@ Commands:
26
26
  build Build all widgets (per-widget, self-contained bundles)
27
27
  connect <url> Connect to a running dashboard for live testing
28
28
  validate [name] Validate all widgets or a specific one
29
- publish Build all widgets and publish to Hub
29
+ publish [hub-url] Build and publish a widget to Hub
30
30
  login [hub-url] Authenticate with GlassHome Hub
31
31
  info [name] Show widget metadata and bundle info
32
32
  upgrade Upgrade @glasshome/widget-sdk to latest version
@@ -130,8 +130,9 @@ switch (effectiveCommand) {
130
130
  }
131
131
 
132
132
  case "publish": {
133
+ const hubUrlArg = args[0];
133
134
  const { runPublish } = await import("../src/commands/publish");
134
- await runPublish(resolveWidgetDir());
135
+ await runPublish(resolveWidgetDir(), hubUrlArg);
135
136
  outro("Done");
136
137
  break;
137
138
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glasshome/widget-cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "CLI for creating and managing GlassHome widgets",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
- import { readFileSync, writeFileSync } from "node:fs";
2
+ import { readFileSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
- import { cancel, isCancel, log, multiselect, select, spinner } from "@clack/prompts";
4
+ import { cancel, isCancel, log, select, spinner } from "@clack/prompts";
5
5
  import semver from "semver";
6
6
  import { getHubUrl, getToken } from "../utils/auth";
7
7
  import { confirmPublish, fetchScopes, requestPublish, uploadToR2 } from "../utils/hub-api";
@@ -14,97 +14,24 @@ import {
14
14
  } from "../utils/manifest";
15
15
  import { runLogin } from "./login";
16
16
 
17
- export async function runPublish(cwd: string): Promise<void> {
18
- // Step 1: Validate all widgets
19
- log.info("Running validation...");
17
+ export async function runPublish(cwd: string, hubUrlOverride?: string): Promise<void> {
18
+ const s = spinner();
19
+
20
+ // Step 1: Validate (quiet)
20
21
  const { runValidate } = await import("./validate");
21
- const valid = await runValidate(cwd);
22
+ const valid = await runValidate(cwd, undefined, { quiet: true });
22
23
  if (!valid) {
23
24
  log.error("Fix validation errors before publishing.");
24
25
  process.exit(1);
25
26
  }
26
27
 
27
- // Step 2: Read package.json
28
- const pkgPath = resolve(cwd, "package.json");
29
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
30
- const currentVersion = pkg.version ?? "0.0.0";
31
-
32
- // Step 3: Version bump prompt
33
- const bump = await select({
34
- message: `Current version: ${currentVersion}. Bump version?`,
35
- options: [
36
- { value: "keep", label: `Keep ${currentVersion}` },
37
- {
38
- value: "patch",
39
- label: `Patch (${semver.inc(currentVersion, "patch")})`,
40
- },
41
- {
42
- value: "minor",
43
- label: `Minor (${semver.inc(currentVersion, "minor")})`,
44
- },
45
- {
46
- value: "major",
47
- label: `Major (${semver.inc(currentVersion, "major")})`,
48
- },
49
- ],
50
- });
51
-
52
- if (isCancel(bump)) {
53
- cancel("Publish cancelled.");
54
- process.exit(0);
55
- }
56
-
57
- let version = currentVersion;
58
- if (bump !== "keep") {
59
- version = semver.inc(currentVersion, bump as "patch" | "minor" | "major") ?? currentVersion;
60
-
61
- pkg.version = version;
62
- writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
63
-
64
- const allWidgets = discoverWidgets(cwd);
65
- for (const name of allWidgets) {
66
- const manifest = readManifest(cwd, name);
67
- manifest.version = version;
68
- writeManifest(cwd, name, manifest);
69
- }
70
-
71
- log.info(`Version bumped to ${version}`);
72
- }
73
-
74
- // Step 4: Rebuild
75
- const s = spinner();
76
- s.start("Building widgets...");
77
- const buildProc = Bun.spawnSync(["bun", "run", "build"], { cwd });
78
- if (buildProc.exitCode !== 0) {
79
- s.stop("Build failed");
80
- log.error(buildProc.stderr.toString());
81
- process.exit(1);
82
- }
83
- s.stop("Build complete");
84
-
85
- // Step 5: Show summary
86
- const widgets = discoverWidgets(cwd);
87
- log.success(`Ready for publishing: ${pkg.name}@${version}`);
88
- log.info("");
89
-
90
- for (const name of widgets) {
91
- const manifest = readManifest(cwd, name);
92
- let sizeInfo = "";
93
- try {
94
- const info = getWidgetBundleInfo(cwd, name);
95
- sizeInfo = ` — ${formatBytes(info.raw)} (${formatBytes(info.gzip)} gzipped)`;
96
- } catch {}
97
- log.info(
98
- ` ${manifest.tag} [${manifest.minSize.w}x${manifest.minSize.h} - ${manifest.maxSize.w}x${manifest.maxSize.h}]${sizeInfo}`,
99
- );
100
- }
101
-
102
- // Step 6: Authenticate with Hub
103
- const hubUrl = getHubUrl();
28
+ // Step 2: Authenticate
29
+ const hubUrl = hubUrlOverride ?? getHubUrl();
30
+ if (hubUrlOverride) log.info(`Hub: ${hubUrl}`);
104
31
  let token = await getToken(hubUrl);
105
32
 
106
33
  if (!token) {
107
- log.warn("Not authenticated with Hub. Starting login...");
34
+ log.warn("Not authenticated. Starting login...");
108
35
  try {
109
36
  await runLogin(hubUrl);
110
37
  } catch (err) {
@@ -113,12 +40,12 @@ export async function runPublish(cwd: string): Promise<void> {
113
40
  }
114
41
  token = await getToken(hubUrl);
115
42
  if (!token) {
116
- log.error("Authentication failed. Run `bun widget login` manually.");
43
+ log.error("Authentication failed. Run `glasshome-widget login` manually.");
117
44
  process.exit(1);
118
45
  }
119
46
  }
120
47
 
121
- // Fetch available scopes from hub
48
+ // Step 3: Select scope
122
49
  const scopes = await fetchScopes(hubUrl, token!);
123
50
  if (scopes.length === 0) {
124
51
  log.error("No publishing scopes available. Ensure your account is properly set up.");
@@ -147,19 +74,24 @@ export async function runPublish(cwd: string): Promise<void> {
147
74
  scope = choice as string;
148
75
  }
149
76
 
150
- // Step 7: Select widgets to publish
77
+ // Step 4: Select widget
78
+ const widgets = discoverWidgets(cwd);
151
79
  const widgetOptions = widgets.map((name) => {
152
80
  const manifest = readManifest(cwd, name);
81
+ let sizeInfo = "";
82
+ try {
83
+ const info = getWidgetBundleInfo(cwd, name);
84
+ sizeInfo = ` — ${formatBytes(info.raw)}`;
85
+ } catch {}
153
86
  return {
154
87
  value: name,
155
- label: `${manifest.tag} (${manifest.minSize.w}x${manifest.minSize.h} - ${manifest.maxSize.w}x${manifest.maxSize.h})`,
88
+ label: `${manifest.tag}${sizeInfo}`,
156
89
  };
157
90
  });
158
91
 
159
- const selected = await multiselect({
160
- message: "Select widgets to publish:",
92
+ const selected = await select({
93
+ message: "Select widget to publish:",
161
94
  options: widgetOptions,
162
- initialValues: widgets,
163
95
  });
164
96
 
165
97
  if (isCancel(selected)) {
@@ -167,100 +99,100 @@ export async function runPublish(cwd: string): Promise<void> {
167
99
  process.exit(0);
168
100
  }
169
101
 
170
- const selectedWidgets = selected as string[];
171
- if (selectedWidgets.length === 0) {
172
- log.warn("No widgets selected.");
173
- return;
174
- }
175
-
176
- // Step 8: Publish each selected widget
177
- const published: Array<{ name: string; cdnUrl: string }> = [];
178
- const skipped: Array<{ name: string; reason: string }> = [];
102
+ const widgetName = selected as string;
103
+ const manifest = readManifest(cwd, widgetName);
179
104
 
180
- for (const widgetName of selectedWidgets) {
181
- const manifest = readManifest(cwd, widgetName);
182
- const distPath = resolve(cwd, "dist", `${widgetName}.js`);
105
+ // Step 5: Version bump (per-widget, from manifest)
106
+ const currentVersion = manifest.version ?? "0.0.0";
183
107
 
184
- let bundleBuffer: Buffer;
185
- try {
186
- bundleBuffer = Buffer.from(readFileSync(distPath));
187
- } catch {
188
- log.error(`Bundle not found: dist/${widgetName}.js`);
189
- continue;
190
- }
108
+ const bump = await select({
109
+ message: `${manifest.tag} version: ${currentVersion}. Bump?`,
110
+ options: [
111
+ { value: "keep", label: `Keep ${currentVersion}`, hint: "fails if already published" },
112
+ { value: "patch", label: `Patch (${semver.inc(currentVersion, "patch")})` },
113
+ { value: "minor", label: `Minor (${semver.inc(currentVersion, "minor")})` },
114
+ { value: "major", label: `Major (${semver.inc(currentVersion, "major")})` },
115
+ ],
116
+ });
191
117
 
192
- // Compute SHA-256 hash
193
- const sha256Hash = createHash("sha256").update(bundleBuffer).digest("hex");
118
+ if (isCancel(bump)) {
119
+ cancel("Publish cancelled.");
120
+ process.exit(0);
121
+ }
194
122
 
195
- s.start(`Publishing ${manifest.tag}@${version}...`);
123
+ let version = currentVersion;
124
+ if (bump !== "keep") {
125
+ version = semver.inc(currentVersion, bump as "patch" | "minor" | "major") ?? currentVersion;
126
+ manifest.version = version;
127
+ writeManifest(cwd, widgetName, manifest);
128
+ }
196
129
 
197
- // Request publish (get presigned URL)
198
- let publishData: Awaited<ReturnType<typeof requestPublish>>;
199
- try {
200
- publishData = await requestPublish(hubUrl, token!, {
201
- scope,
202
- name: widgetName,
203
- displayName: manifest.name,
204
- description: manifest.description,
205
- minSize: manifest.minSize,
206
- maxSize: manifest.maxSize,
207
- sdkVersion: manifest.sdkVersion,
208
- version,
209
- bundleSize: bundleBuffer.byteLength,
210
- sha256Hash,
211
- manifestJson: JSON.stringify(manifest),
212
- });
213
- } catch (err: any) {
214
- if (err.status === 409) {
215
- s.stop(`Skipped ${manifest.tag}@${version}`);
216
- log.warn(`${manifest.tag}@${version}: already published (immutable)`);
217
- skipped.push({ name: manifest.tag, reason: "version already exists" });
218
- continue;
219
- }
220
- s.stop(`Failed ${manifest.tag}`);
221
- log.error(`${manifest.tag}: ${err.message}`);
222
- continue;
223
- }
130
+ // Step 6: Build (async so spinner animates)
131
+ s.start("Building widget...");
132
+ const buildProc = Bun.spawn(["bun", "run", "build"], { cwd, stdout: "pipe", stderr: "pipe" });
133
+ const buildExit = await buildProc.exited;
134
+ if (buildExit !== 0) {
135
+ const stderr = await new Response(buildProc.stderr).text();
136
+ s.stop("Build failed");
137
+ log.error(stderr);
138
+ process.exit(1);
139
+ }
140
+ s.stop("Build complete");
224
141
 
225
- // Upload to R2
226
- s.message(`Uploading ${manifest.tag} to CDN...`);
227
- try {
228
- await uploadToR2(publishData.uploadUrl, bundleBuffer);
229
- } catch (err: any) {
230
- s.stop(`Upload failed for ${manifest.tag}`);
231
- log.error(`${manifest.tag}: ${err.message}`);
232
- continue;
233
- }
142
+ // Step 7: Publish
143
+ const distPath = resolve(cwd, "dist", `${widgetName}.js`);
234
144
 
235
- // Confirm publish
236
- s.message(`Confirming ${manifest.tag}...`);
237
- try {
238
- const result = await confirmPublish(hubUrl, token!, publishData.versionId);
239
- s.stop(`Published ${manifest.tag}@${version}`);
240
- published.push({ name: manifest.tag, cdnUrl: result.bundleUrl });
241
- } catch (err: any) {
242
- s.stop(`Confirm failed for ${manifest.tag}`);
243
- log.error(`${manifest.tag}: ${err.message}`);
244
- }
145
+ let bundleBuffer: Buffer;
146
+ try {
147
+ bundleBuffer = Buffer.from(readFileSync(distPath));
148
+ } catch {
149
+ log.error(`Bundle not found: dist/${widgetName}.js`);
150
+ process.exit(1);
245
151
  }
246
152
 
247
- // Step 9: Summary
248
- log.info("");
249
- if (published.length > 0) {
250
- log.success(`Published ${published.length} widget(s):`);
251
- for (const w of published) {
252
- log.info(` ${w.name} -> ${w.cdnUrl}`);
153
+ const sha256Hash = createHash("sha256").update(bundleBuffer).digest("hex");
154
+
155
+ s.start(`Publishing ${manifest.tag}@${version}...`);
156
+
157
+ let publishData: Awaited<ReturnType<typeof requestPublish>>;
158
+ try {
159
+ publishData = await requestPublish(hubUrl, token!, {
160
+ scope,
161
+ name: widgetName,
162
+ displayName: manifest.name,
163
+ description: manifest.description,
164
+ minSize: manifest.minSize,
165
+ maxSize: manifest.maxSize,
166
+ sdkVersion: manifest.sdkVersion,
167
+ version,
168
+ bundleSize: bundleBuffer.byteLength,
169
+ sha256Hash,
170
+ manifestJson: JSON.stringify(manifest),
171
+ });
172
+ } catch (err: any) {
173
+ if (err.status === 409) {
174
+ s.stop(`${manifest.tag}@${version} already published — bump version to republish`);
175
+ process.exit(0);
253
176
  }
177
+ s.stop(`Publish failed: ${err.message}`);
178
+ process.exit(1);
254
179
  }
255
180
 
256
- if (skipped.length > 0) {
257
- log.warn(`Skipped ${skipped.length} widget(s):`);
258
- for (const w of skipped) {
259
- log.info(` ${w.name}: ${w.reason}`);
260
- }
181
+ s.message(`Uploading ${manifest.tag} to CDN...`);
182
+ try {
183
+ await uploadToR2(publishData.uploadUrl, bundleBuffer);
184
+ } catch (err: any) {
185
+ s.stop(`Upload failed: ${err.message}`);
186
+ process.exit(1);
261
187
  }
262
188
 
263
- if (published.length === 0 && skipped.length === 0) {
264
- log.error("No widgets were published.");
189
+ s.message(`Confirming ${manifest.tag}...`);
190
+ try {
191
+ const result = await confirmPublish(hubUrl, token!, publishData.versionId);
192
+ s.stop(`Published ${manifest.tag}@${version}`);
193
+ log.info(`CDN: ${result.bundleUrl}`);
194
+ } catch (err: any) {
195
+ s.stop(`Confirmation failed: ${err.message}`);
196
+ process.exit(1);
265
197
  }
266
198
  }
@@ -86,7 +86,12 @@ function validateManifest(manifest: WidgetManifest): ValidationResult {
86
86
  };
87
87
  }
88
88
 
89
- export async function runValidate(cwd: string, widgetName?: string): Promise<boolean> {
89
+ export async function runValidate(
90
+ cwd: string,
91
+ widgetName?: string,
92
+ options?: { quiet?: boolean },
93
+ ): Promise<boolean> {
94
+ const quiet = options?.quiet ?? false;
90
95
  const widgets = discoverWidgets(cwd);
91
96
 
92
97
  if (widgets.length === 0) {
@@ -106,10 +111,12 @@ export async function runValidate(cwd: string, widgetName?: string): Promise<boo
106
111
  if (!existsSync(distDir)) {
107
112
  const s = spinner();
108
113
  s.start("Building widgets (dist/ not found)...");
109
- const proc = Bun.spawnSync(["bun", "run", "build"], { cwd });
110
- if (proc.exitCode !== 0) {
114
+ const proc = Bun.spawn(["bun", "run", "build"], { cwd, stdout: "pipe", stderr: "pipe" });
115
+ const exitCode = await proc.exited;
116
+ if (exitCode !== 0) {
117
+ const stderr = await new Response(proc.stderr).text();
111
118
  s.stop("Build failed");
112
- log.error(proc.stderr.toString());
119
+ log.error(stderr);
113
120
  return false;
114
121
  }
115
122
  s.stop("Build complete");
@@ -117,15 +124,18 @@ export async function runValidate(cwd: string, widgetName?: string): Promise<boo
117
124
 
118
125
  let allPassed = true;
119
126
 
127
+ const failed: string[] = [];
128
+
120
129
  for (const name of toValidate) {
121
- log.info(`\nValidating widget: ${name}`);
130
+ if (!quiet) log.info(`\nValidating widget: ${name}`);
122
131
 
123
132
  // Read and validate manifest.json
124
133
  let manifest: WidgetManifest;
125
134
  try {
126
135
  manifest = readManifest(cwd, name);
127
136
  } catch (err) {
128
- log.error(` Failed to read manifest: ${err instanceof Error ? err.message : String(err)}`);
137
+ if (!quiet) log.error(` Failed to read manifest: ${err instanceof Error ? err.message : String(err)}`);
138
+ failed.push(name);
129
139
  allPassed = false;
130
140
  continue;
131
141
  }
@@ -134,11 +144,11 @@ export async function runValidate(cwd: string, widgetName?: string): Promise<boo
134
144
 
135
145
  if (result.errors.length > 0) {
136
146
  for (const err of result.errors) {
137
- log.error(` - ${err}`);
147
+ if (!quiet) log.error(` - ${err}`);
138
148
  }
139
149
  }
140
150
 
141
- if (result.warnings.length > 0) {
151
+ if (!quiet && result.warnings.length > 0) {
142
152
  for (const warn of result.warnings) {
143
153
  log.warn(` - ${warn}`);
144
154
  }
@@ -147,9 +157,10 @@ export async function runValidate(cwd: string, widgetName?: string): Promise<boo
147
157
  // Check bundle exists
148
158
  const bundlePath = resolve(cwd, "dist", `${name}.js`);
149
159
  if (!existsSync(bundlePath)) {
150
- log.error(` - dist/${name}.js not found`);
160
+ if (!quiet) log.error(` - dist/${name}.js not found`);
151
161
  allPassed = false;
152
- } else {
162
+ failed.push(name);
163
+ } else if (!quiet) {
153
164
  try {
154
165
  const info = getWidgetBundleInfo(cwd, name);
155
166
  log.info(` Bundle: ${formatBytes(info.raw)} (${formatBytes(info.gzip)} gzipped)`);
@@ -159,9 +170,10 @@ export async function runValidate(cwd: string, widgetName?: string): Promise<boo
159
170
  }
160
171
 
161
172
  if (result.passed) {
162
- log.success(` ${name}: passed`);
173
+ if (!quiet) log.success(` ${name}: passed`);
163
174
  } else {
164
- log.error(` ${name}: failed`);
175
+ if (!quiet) log.error(` ${name}: failed`);
176
+ failed.push(name);
165
177
  allPassed = false;
166
178
  }
167
179
  }
@@ -169,23 +181,31 @@ export async function runValidate(cwd: string, widgetName?: string): Promise<boo
169
181
  // Validate registry.json
170
182
  const registry = readRegistry(cwd);
171
183
  if (!registry) {
172
- log.error("\ndist/registry.json not found");
184
+ if (!quiet) log.error("\ndist/registry.json not found");
173
185
  allPassed = false;
174
186
  } else {
175
187
  const registryTags = new Set(registry.widgets.map((w) => w.bundleUrl));
176
188
  for (const name of toValidate) {
177
189
  if (!registryTags.has(`./${name}.js`)) {
178
- log.error(` Widget "${name}" not listed in registry.json`);
190
+ if (!quiet) log.error(` Widget "${name}" not listed in registry.json`);
179
191
  allPassed = false;
180
192
  }
181
193
  }
182
- log.info(`\nRegistry: ${registry.widgets.length} widget(s)`);
194
+ if (!quiet) log.info(`\nRegistry: ${registry.widgets.length} widget(s)`);
183
195
  }
184
196
 
185
- if (allPassed) {
186
- log.success("\nAll checks passed");
197
+ if (quiet) {
198
+ if (allPassed) {
199
+ log.success(`Validated ${toValidate.length} widget(s)`);
200
+ } else {
201
+ log.error(`Validation failed for: ${failed.join(", ")}`);
202
+ }
187
203
  } else {
188
- log.error("\nValidation failed");
204
+ if (allPassed) {
205
+ log.success("\nAll checks passed");
206
+ } else {
207
+ log.error("\nValidation failed");
208
+ }
189
209
  }
190
210
 
191
211
  return allPassed;
package/src/utils/auth.ts CHANGED
@@ -87,7 +87,15 @@ export function clearToken(): void {
87
87
  }
88
88
 
89
89
  export function getHubUrl(): string {
90
- return getStoredAuth()?.hubUrl ?? "https://glasshome.app";
90
+ const stored = getStoredAuth()?.hubUrl;
91
+ if (stored) {
92
+ if (stored.includes("localhost") || stored.includes("127.0.0.1")) {
93
+ clearToken();
94
+ } else {
95
+ return stored;
96
+ }
97
+ }
98
+ return "https://glasshome.app";
91
99
  }
92
100
 
93
101
  export function getScope(): string | null {
@@ -62,8 +62,15 @@ export async function requestPublish(
62
62
  }
63
63
 
64
64
  if (!res.ok) {
65
- const text = await res.text();
66
- throw new Error(`Publish request failed (${res.status}): ${text}`);
65
+ let detail: string;
66
+ try {
67
+ const body = await res.text();
68
+ const json = JSON.parse(body);
69
+ detail = json.error || json.message || body;
70
+ } catch {
71
+ detail = `HTTP ${res.status} ${res.statusText}`;
72
+ }
73
+ throw Object.assign(new Error(detail), { status: res.status });
67
74
  }
68
75
 
69
76
  return res.json() as Promise<PublishRequestResponse>;
@@ -84,8 +91,15 @@ export async function confirmPublish(
84
91
  });
85
92
 
86
93
  if (!res.ok) {
87
- const text = await res.text();
88
- throw new Error(`Publish confirm failed (${res.status}): ${text}`);
94
+ let detail: string;
95
+ try {
96
+ const body = await res.text();
97
+ const json = JSON.parse(body);
98
+ detail = json.error || json.message || body;
99
+ } catch {
100
+ detail = `HTTP ${res.status} ${res.statusText}`;
101
+ }
102
+ throw Object.assign(new Error(detail), { status: res.status });
89
103
  }
90
104
 
91
105
  return res.json() as Promise<PublishConfirmResponse>;
@@ -27,49 +27,6 @@ export interface BundleInfo {
27
27
  text: string;
28
28
  }
29
29
 
30
- // ---------------------------------------------------------------------------
31
- // Legacy single-widget helpers (kept for backward compat)
32
- // ---------------------------------------------------------------------------
33
-
34
- /**
35
- * Extract the widget manifest from the built dist/index.js bundle
36
- * by dynamic-importing it and reading the registered custom element's metadata.
37
- */
38
- export async function extractManifest(cwd: string): Promise<WidgetManifest> {
39
- const distPath = resolve(cwd, "dist/index.js");
40
- if (!existsSync(distPath)) {
41
- throw new Error(
42
- "dist/index.js not found. Run `bun run build` first or let the command build for you.",
43
- );
44
- }
45
-
46
- // Dynamic import the bundle — it self-registers a custom element via @glasshome/widget-sdk's defineWidget.
47
- // The SDK's defineWidget stores manifest on the custom element class.
48
- const mod = await import(distPath);
49
-
50
- // The module default export or the manifest property contains widget metadata
51
- const manifest: WidgetManifest | undefined = mod.manifest ?? mod.default?.manifest;
52
-
53
- if (!manifest) {
54
- throw new Error(
55
- "Could not extract manifest from dist/index.js. Ensure your widget uses defineWidget() from @glasshome/widget-sdk.",
56
- );
57
- }
58
-
59
- return manifest;
60
- }
61
-
62
- /**
63
- * Read the raw bundle text and compute sizes.
64
- */
65
- export function getBundleInfo(cwd: string): BundleInfo {
66
- const distPath = resolve(cwd, "dist/index.js");
67
- const text = readFileSync(distPath, "utf-8");
68
- const raw = Buffer.byteLength(text, "utf-8");
69
- const gzip = gzipSync(text).byteLength;
70
- return { raw, gzip, text };
71
- }
72
-
73
30
  export function formatBytes(bytes: number): string {
74
31
  if (bytes < 1024) return `${bytes} B`;
75
32
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;