@glasshome/widget-cli 0.2.1 → 0.3.1

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 [hub-url] 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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glasshome/widget-cli",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "CLI for creating and managing GlassHome widgets",
5
5
  "type": "module",
6
6
  "bin": {
@@ -44,8 +44,8 @@ export async function runInfo(cwd: string, widgetName?: string): Promise<void> {
44
44
  const manifest = readManifest(cwd, name);
45
45
  log.info(` Tag: ${manifest.tag}`);
46
46
  log.info(` Name: ${manifest.name}`);
47
- log.info(` Type: ${manifest.type}`);
48
- log.info(` Size: ${manifest.size}`);
47
+ log.info(` Min Size: ${manifest.minSize.w}×${manifest.minSize.h}`);
48
+ log.info(` Max Size: ${manifest.maxSize.w}×${manifest.maxSize.h}`);
49
49
  log.info(` SDK Version: ${manifest.sdkVersion}`);
50
50
  if (manifest.version) {
51
51
  log.info(` Version: ${manifest.version}`);
@@ -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";
@@ -15,97 +15,23 @@ import {
15
15
  import { runLogin } from "./login";
16
16
 
17
17
  export async function runPublish(cwd: string, hubUrlOverride?: string): Promise<void> {
18
- // Step 1: Validate all widgets
19
- log.info("Running validation...");
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}`, hint: "fails if already published" },
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
28
+ // Step 2: Authenticate
103
29
  const hubUrl = hubUrlOverride ?? getHubUrl();
104
- log.info(`Hub: ${hubUrl}`);
30
+ if (hubUrlOverride) log.info(`Hub: ${hubUrl}`);
105
31
  let token = await getToken(hubUrl);
106
32
 
107
33
  if (!token) {
108
- log.warn("Not authenticated with Hub. Starting login...");
34
+ log.warn("Not authenticated. Starting login...");
109
35
  try {
110
36
  await runLogin(hubUrl);
111
37
  } catch (err) {
@@ -114,12 +40,12 @@ export async function runPublish(cwd: string, hubUrlOverride?: string): Promise<
114
40
  }
115
41
  token = await getToken(hubUrl);
116
42
  if (!token) {
117
- log.error("Authentication failed. Run `bun widget login` manually.");
43
+ log.error("Authentication failed. Run `glasshome-widget login` manually.");
118
44
  process.exit(1);
119
45
  }
120
46
  }
121
47
 
122
- // Fetch available scopes from hub
48
+ // Step 3: Select scope
123
49
  const scopes = await fetchScopes(hubUrl, token!);
124
50
  if (scopes.length === 0) {
125
51
  log.error("No publishing scopes available. Ensure your account is properly set up.");
@@ -148,19 +74,24 @@ export async function runPublish(cwd: string, hubUrlOverride?: string): Promise<
148
74
  scope = choice as string;
149
75
  }
150
76
 
151
- // Step 7: Select widgets to publish
77
+ // Step 4: Select widget
78
+ const widgets = discoverWidgets(cwd);
152
79
  const widgetOptions = widgets.map((name) => {
153
80
  const manifest = readManifest(cwd, name);
81
+ let sizeInfo = "";
82
+ try {
83
+ const info = getWidgetBundleInfo(cwd, name);
84
+ sizeInfo = ` — ${formatBytes(info.raw)}`;
85
+ } catch {}
154
86
  return {
155
87
  value: name,
156
- label: `${manifest.tag} (${manifest.minSize.w}x${manifest.minSize.h} - ${manifest.maxSize.w}x${manifest.maxSize.h})`,
88
+ label: `${manifest.tag}${sizeInfo}`,
157
89
  };
158
90
  });
159
91
 
160
- const selected = await multiselect({
161
- message: "Select widgets to publish:",
92
+ const selected = await select({
93
+ message: "Select widget to publish:",
162
94
  options: widgetOptions,
163
- initialValues: widgets,
164
95
  });
165
96
 
166
97
  if (isCancel(selected)) {
@@ -168,100 +99,101 @@ export async function runPublish(cwd: string, hubUrlOverride?: string): Promise<
168
99
  process.exit(0);
169
100
  }
170
101
 
171
- const selectedWidgets = selected as string[];
172
- if (selectedWidgets.length === 0) {
173
- log.warn("No widgets selected.");
174
- return;
175
- }
176
-
177
- // Step 8: Publish each selected widget
178
- const published: Array<{ name: string; cdnUrl: string }> = [];
179
- const skipped: Array<{ name: string; reason: string }> = [];
102
+ const widgetName = selected as string;
103
+ const manifest = readManifest(cwd, widgetName);
180
104
 
181
- for (const widgetName of selectedWidgets) {
182
- const manifest = readManifest(cwd, widgetName);
183
- const distPath = resolve(cwd, "dist", `${widgetName}.js`);
105
+ // Step 5: Version bump (per-widget, from manifest)
106
+ const currentVersion = manifest.version ?? "0.0.0";
184
107
 
185
- let bundleBuffer: Buffer;
186
- try {
187
- bundleBuffer = Buffer.from(readFileSync(distPath));
188
- } catch {
189
- log.error(`Bundle not found: dist/${widgetName}.js`);
190
- continue;
191
- }
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
+ });
192
117
 
193
- // Compute SHA-256 hash
194
- const sha256Hash = createHash("sha256").update(bundleBuffer).digest("hex");
118
+ if (isCancel(bump)) {
119
+ cancel("Publish cancelled.");
120
+ process.exit(0);
121
+ }
195
122
 
196
- 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
+ }
197
129
 
198
- // Request publish (get presigned URL)
199
- let publishData: Awaited<ReturnType<typeof requestPublish>>;
200
- try {
201
- publishData = await requestPublish(hubUrl, token!, {
202
- scope,
203
- name: widgetName,
204
- displayName: manifest.name,
205
- description: manifest.description,
206
- minSize: manifest.minSize,
207
- maxSize: manifest.maxSize,
208
- sdkVersion: manifest.sdkVersion,
209
- version,
210
- bundleSize: bundleBuffer.byteLength,
211
- sha256Hash,
212
- manifestJson: JSON.stringify(manifest),
213
- });
214
- } catch (err: any) {
215
- if (err.status === 409) {
216
- s.stop(`Skipped ${manifest.tag}@${version}`);
217
- log.warn(`${manifest.tag}@${version}: already published (immutable)`);
218
- skipped.push({ name: manifest.tag, reason: "version already exists" });
219
- continue;
220
- }
221
- s.stop(`Failed ${manifest.tag}`);
222
- log.error(`${manifest.tag}: ${err.message}`);
223
- continue;
224
- }
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");
225
141
 
226
- // Upload to R2
227
- s.message(`Uploading ${manifest.tag} to CDN...`);
228
- try {
229
- await uploadToR2(publishData.uploadUrl, bundleBuffer);
230
- } catch (err: any) {
231
- s.stop(`Upload failed for ${manifest.tag}`);
232
- log.error(`${manifest.tag}: ${err.message}`);
233
- continue;
234
- }
142
+ // Step 7: Publish
143
+ const distPath = resolve(cwd, "dist", `${widgetName}.js`);
235
144
 
236
- // Confirm publish
237
- s.message(`Confirming ${manifest.tag}...`);
238
- try {
239
- const result = await confirmPublish(hubUrl, token!, publishData.versionId);
240
- s.stop(`Published ${manifest.tag}@${version}`);
241
- published.push({ name: manifest.tag, cdnUrl: result.bundleUrl });
242
- } catch (err: any) {
243
- s.stop(`Confirm failed for ${manifest.tag}`);
244
- log.error(`${manifest.tag}: ${err.message}`);
245
- }
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);
246
151
  }
247
152
 
248
- // Step 9: Summary
249
- log.info("");
250
- if (published.length > 0) {
251
- log.success(`Published ${published.length} widget(s):`);
252
- for (const w of published) {
253
- 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
+ icon: manifest.icon,
165
+ minSize: manifest.minSize,
166
+ maxSize: manifest.maxSize,
167
+ sdkVersion: manifest.sdkVersion,
168
+ version,
169
+ bundleSize: bundleBuffer.byteLength,
170
+ sha256Hash,
171
+ manifestJson: JSON.stringify(manifest),
172
+ });
173
+ } catch (err: any) {
174
+ if (err.status === 409) {
175
+ s.stop(`${manifest.tag}@${version} already published — bump version to republish`);
176
+ process.exit(0);
254
177
  }
178
+ s.stop(`Publish failed: ${err.message}`);
179
+ process.exit(1);
255
180
  }
256
181
 
257
- if (skipped.length > 0) {
258
- log.warn(`Skipped ${skipped.length} widget(s):`);
259
- for (const w of skipped) {
260
- log.info(` ${w.name}: ${w.reason}`);
261
- }
182
+ s.message(`Uploading ${manifest.tag} to CDN...`);
183
+ try {
184
+ await uploadToR2(publishData.uploadUrl, bundleBuffer);
185
+ } catch (err: any) {
186
+ s.stop(`Upload failed: ${err.message}`);
187
+ process.exit(1);
262
188
  }
263
189
 
264
- if (published.length === 0 && skipped.length === 0) {
265
- log.error("No widgets were published.");
190
+ s.message(`Confirming ${manifest.tag}...`);
191
+ try {
192
+ const result = await confirmPublish(hubUrl, token!, publishData.versionId);
193
+ s.stop(`Published ${manifest.tag}@${version}`);
194
+ log.info(`CDN: ${result.bundleUrl}`);
195
+ } catch (err: any) {
196
+ s.stop(`Confirmation failed: ${err.message}`);
197
+ process.exit(1);
266
198
  }
267
199
  }
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  import { log, spinner } from "@clack/prompts";
4
+ import { WidgetManifestSchema } from "@glasshome/widget-sdk/schemas";
4
5
  import {
5
6
  discoverWidgets,
6
7
  formatBytes,
@@ -22,54 +23,43 @@ function validateManifest(manifest: WidgetManifest): ValidationResult {
22
23
  const errors: string[] = [];
23
24
  const warnings: string[] = [];
24
25
 
25
- // Required fields
26
- if (!manifest.tag) errors.push("Missing required field: tag");
27
- if (!manifest.name) errors.push("Missing required field: name");
28
- if (!manifest.sdkVersion) errors.push("Missing required field: sdkVersion");
26
+ // Validate core manifest shape using the shared schema
27
+ const result = WidgetManifestSchema.safeParse(manifest);
28
+ if (!result.success) {
29
+ for (const issue of result.error.issues) {
30
+ const path = issue.path.length > 0 ? issue.path.join(".") : "manifest";
31
+ errors.push(`${path}: ${issue.message}`);
32
+ }
33
+ }
29
34
 
30
- // Tag format
35
+ // Tag format (stricter than schema — enforces kebab-case with at least one hyphen)
31
36
  if (manifest.tag && !TAG_REGEX.test(manifest.tag)) {
32
37
  errors.push(
33
38
  `Invalid tag format "${manifest.tag}" — must match ${TAG_REGEX} (e.g. glasshome-my-widget)`,
34
39
  );
35
40
  }
36
41
 
37
- // minSize validation
38
- if (
39
- !manifest.minSize ||
40
- typeof manifest.minSize !== "object" ||
41
- typeof manifest.minSize.w !== "number" ||
42
- typeof manifest.minSize.h !== "number"
43
- ) {
44
- errors.push("minSize must be an object with numeric w and h properties");
45
- }
46
-
47
- // maxSize validation
48
- if (
49
- !manifest.maxSize ||
50
- typeof manifest.maxSize !== "object" ||
51
- typeof manifest.maxSize.w !== "number" ||
52
- typeof manifest.maxSize.h !== "number"
53
- ) {
54
- errors.push("maxSize must be an object with numeric w and h properties");
55
- }
56
-
57
42
  // Schema validation
58
43
  if (manifest.schema) {
59
44
  if (typeof manifest.schema !== "object") {
60
45
  errors.push("Schema must be an object");
61
46
  } else {
62
- if (manifest.schema.type !== "object") {
47
+ if ((manifest.schema as Record<string, unknown>).type !== "object") {
63
48
  warnings.push('Schema type should be "object"');
64
49
  }
65
- if (!manifest.schema.properties || typeof manifest.schema.properties !== "object") {
50
+ if (
51
+ !(manifest.schema as Record<string, unknown>).properties ||
52
+ typeof (manifest.schema as Record<string, unknown>).properties !== "object"
53
+ ) {
66
54
  warnings.push("Schema should have a properties object");
67
55
  }
68
56
  }
69
57
 
70
58
  // Check defaultConfig matches schema properties
71
- if (manifest.defaultConfig && manifest.schema.properties) {
72
- const schemaKeys = Object.keys(manifest.schema.properties as Record<string, unknown>);
59
+ if (manifest.defaultConfig && (manifest.schema as Record<string, unknown>).properties) {
60
+ const schemaKeys = Object.keys(
61
+ (manifest.schema as Record<string, unknown>).properties as Record<string, unknown>,
62
+ );
73
63
  const configKeys = Object.keys(manifest.defaultConfig);
74
64
  for (const key of configKeys) {
75
65
  if (!schemaKeys.includes(key)) {
@@ -86,7 +76,12 @@ function validateManifest(manifest: WidgetManifest): ValidationResult {
86
76
  };
87
77
  }
88
78
 
89
- export async function runValidate(cwd: string, widgetName?: string): Promise<boolean> {
79
+ export async function runValidate(
80
+ cwd: string,
81
+ widgetName?: string,
82
+ options?: { quiet?: boolean },
83
+ ): Promise<boolean> {
84
+ const quiet = options?.quiet ?? false;
90
85
  const widgets = discoverWidgets(cwd);
91
86
 
92
87
  if (widgets.length === 0) {
@@ -106,10 +101,12 @@ export async function runValidate(cwd: string, widgetName?: string): Promise<boo
106
101
  if (!existsSync(distDir)) {
107
102
  const s = spinner();
108
103
  s.start("Building widgets (dist/ not found)...");
109
- const proc = Bun.spawnSync(["bun", "run", "build"], { cwd });
110
- if (proc.exitCode !== 0) {
104
+ const proc = Bun.spawn(["bun", "run", "build"], { cwd, stdout: "pipe", stderr: "pipe" });
105
+ const exitCode = await proc.exited;
106
+ if (exitCode !== 0) {
107
+ const stderr = await new Response(proc.stderr).text();
111
108
  s.stop("Build failed");
112
- log.error(proc.stderr.toString());
109
+ log.error(stderr);
113
110
  return false;
114
111
  }
115
112
  s.stop("Build complete");
@@ -117,15 +114,19 @@ export async function runValidate(cwd: string, widgetName?: string): Promise<boo
117
114
 
118
115
  let allPassed = true;
119
116
 
117
+ const failed: string[] = [];
118
+
120
119
  for (const name of toValidate) {
121
- log.info(`\nValidating widget: ${name}`);
120
+ if (!quiet) log.info(`\nValidating widget: ${name}`);
122
121
 
123
122
  // Read and validate manifest.json
124
123
  let manifest: WidgetManifest;
125
124
  try {
126
125
  manifest = readManifest(cwd, name);
127
126
  } catch (err) {
128
- log.error(` Failed to read manifest: ${err instanceof Error ? err.message : String(err)}`);
127
+ if (!quiet)
128
+ log.error(` Failed to read manifest: ${err instanceof Error ? err.message : String(err)}`);
129
+ failed.push(name);
129
130
  allPassed = false;
130
131
  continue;
131
132
  }
@@ -134,11 +135,11 @@ export async function runValidate(cwd: string, widgetName?: string): Promise<boo
134
135
 
135
136
  if (result.errors.length > 0) {
136
137
  for (const err of result.errors) {
137
- log.error(` - ${err}`);
138
+ if (!quiet) log.error(` - ${err}`);
138
139
  }
139
140
  }
140
141
 
141
- if (result.warnings.length > 0) {
142
+ if (!quiet && result.warnings.length > 0) {
142
143
  for (const warn of result.warnings) {
143
144
  log.warn(` - ${warn}`);
144
145
  }
@@ -147,9 +148,10 @@ export async function runValidate(cwd: string, widgetName?: string): Promise<boo
147
148
  // Check bundle exists
148
149
  const bundlePath = resolve(cwd, "dist", `${name}.js`);
149
150
  if (!existsSync(bundlePath)) {
150
- log.error(` - dist/${name}.js not found`);
151
+ if (!quiet) log.error(` - dist/${name}.js not found`);
151
152
  allPassed = false;
152
- } else {
153
+ failed.push(name);
154
+ } else if (!quiet) {
153
155
  try {
154
156
  const info = getWidgetBundleInfo(cwd, name);
155
157
  log.info(` Bundle: ${formatBytes(info.raw)} (${formatBytes(info.gzip)} gzipped)`);
@@ -159,9 +161,10 @@ export async function runValidate(cwd: string, widgetName?: string): Promise<boo
159
161
  }
160
162
 
161
163
  if (result.passed) {
162
- log.success(` ${name}: passed`);
164
+ if (!quiet) log.success(` ${name}: passed`);
163
165
  } else {
164
- log.error(` ${name}: failed`);
166
+ if (!quiet) log.error(` ${name}: failed`);
167
+ failed.push(name);
165
168
  allPassed = false;
166
169
  }
167
170
  }
@@ -169,23 +172,31 @@ export async function runValidate(cwd: string, widgetName?: string): Promise<boo
169
172
  // Validate registry.json
170
173
  const registry = readRegistry(cwd);
171
174
  if (!registry) {
172
- log.error("\ndist/registry.json not found");
175
+ if (!quiet) log.error("\ndist/registry.json not found");
173
176
  allPassed = false;
174
177
  } else {
175
178
  const registryTags = new Set(registry.widgets.map((w) => w.bundleUrl));
176
179
  for (const name of toValidate) {
177
180
  if (!registryTags.has(`./${name}.js`)) {
178
- log.error(` Widget "${name}" not listed in registry.json`);
181
+ if (!quiet) log.error(` Widget "${name}" not listed in registry.json`);
179
182
  allPassed = false;
180
183
  }
181
184
  }
182
- log.info(`\nRegistry: ${registry.widgets.length} widget(s)`);
185
+ if (!quiet) log.info(`\nRegistry: ${registry.widgets.length} widget(s)`);
183
186
  }
184
187
 
185
- if (allPassed) {
186
- log.success("\nAll checks passed");
188
+ if (quiet) {
189
+ if (allPassed) {
190
+ log.success(`Validated ${toValidate.length} widget(s)`);
191
+ } else {
192
+ log.error(`Validation failed for: ${failed.join(", ")}`);
193
+ }
187
194
  } else {
188
- log.error("\nValidation failed");
195
+ if (allPassed) {
196
+ log.success("\nAll checks passed");
197
+ } else {
198
+ log.error("\nValidation failed");
199
+ }
189
200
  }
190
201
 
191
202
  return allPassed;
package/src/utils/auth.ts CHANGED
@@ -88,9 +88,12 @@ export function clearToken(): void {
88
88
 
89
89
  export function getHubUrl(): string {
90
90
  const stored = getStoredAuth()?.hubUrl;
91
- // Ignore localhost URLs from local dev sessions
92
- if (stored && !stored.includes("localhost") && !stored.includes("127.0.0.1")) {
93
- return stored;
91
+ if (stored) {
92
+ if (stored.includes("localhost") || stored.includes("127.0.0.1")) {
93
+ clearToken();
94
+ } else {
95
+ return stored;
96
+ }
94
97
  }
95
98
  return "https://glasshome.app";
96
99
  }
@@ -15,14 +15,16 @@ export async function fetchScopes(hubUrl: string, token: string): Promise<Publis
15
15
  return data.scopes;
16
16
  }
17
17
 
18
+ import type { GridSize } from "@glasshome/widget-sdk";
19
+
18
20
  interface PublishRequestParams {
19
21
  scope: string;
20
22
  name: string;
21
23
  displayName: string;
22
24
  description?: string;
23
25
  icon?: string;
24
- minSize: { w: number; h: number };
25
- maxSize: { w: number; h: number };
26
+ minSize: GridSize;
27
+ maxSize: GridSize;
26
28
  sdkVersion: string;
27
29
  version: string;
28
30
  bundleSize: number;
@@ -62,8 +64,15 @@ export async function requestPublish(
62
64
  }
63
65
 
64
66
  if (!res.ok) {
65
- const text = await res.text();
66
- throw new Error(`Publish request failed (${res.status}): ${text}`);
67
+ let detail: string;
68
+ try {
69
+ const body = await res.text();
70
+ const json = JSON.parse(body);
71
+ detail = json.error || json.message || body;
72
+ } catch {
73
+ detail = `HTTP ${res.status} ${res.statusText}`;
74
+ }
75
+ throw Object.assign(new Error(detail), { status: res.status });
67
76
  }
68
77
 
69
78
  return res.json() as Promise<PublishRequestResponse>;
@@ -84,8 +93,15 @@ export async function confirmPublish(
84
93
  });
85
94
 
86
95
  if (!res.ok) {
87
- const text = await res.text();
88
- throw new Error(`Publish confirm failed (${res.status}): ${text}`);
96
+ let detail: string;
97
+ try {
98
+ const body = await res.text();
99
+ const json = JSON.parse(body);
100
+ detail = json.error || json.message || body;
101
+ } catch {
102
+ detail = `HTTP ${res.status} ${res.statusText}`;
103
+ }
104
+ throw Object.assign(new Error(detail), { status: res.status });
89
105
  }
90
106
 
91
107
  return res.json() as Promise<PublishConfirmResponse>;
@@ -1,18 +1,9 @@
1
1
  import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
3
  import { gzipSync } from "node:zlib";
4
+ import type { WidgetManifest as SdkWidgetManifest } from "@glasshome/widget-sdk";
4
5
 
5
- export interface WidgetManifest {
6
- tag: string;
7
- name: string;
8
- minSize: { w: number; h: number };
9
- maxSize: { w: number; h: number };
10
- sdkVersion: string;
11
- version?: string;
12
- description?: string;
13
- schema?: Record<string, unknown>;
14
- defaultConfig?: Record<string, unknown>;
15
- }
6
+ export type WidgetManifest = SdkWidgetManifest & { version?: string };
16
7
 
17
8
  export interface Registry {
18
9
  version: number;
@@ -27,49 +18,6 @@ export interface BundleInfo {
27
18
  text: string;
28
19
  }
29
20
 
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
21
  export function formatBytes(bytes: number): string {
74
22
  if (bytes < 1024) return `${bytes} B`;
75
23
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;