@glasshome/widget-cli 0.2.1 → 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 +14 -2
- package/bin/cli.ts +1 -1
- package/package.json +1 -1
- package/src/commands/publish.ts +102 -171
- package/src/commands/validate.ts +38 -18
- package/src/utils/auth.ts +6 -3
- package/src/utils/hub-api.ts +18 -4
- package/src/utils/manifest.ts +0 -43
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` |
|
|
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
|
|
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
package/src/commands/publish.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { readFileSync
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
|
-
import { cancel, isCancel, log,
|
|
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
|
-
|
|
19
|
-
|
|
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:
|
|
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
|
|
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 `
|
|
43
|
+
log.error("Authentication failed. Run `glasshome-widget login` manually.");
|
|
118
44
|
process.exit(1);
|
|
119
45
|
}
|
|
120
46
|
}
|
|
121
47
|
|
|
122
|
-
//
|
|
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
|
|
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}
|
|
88
|
+
label: `${manifest.tag}${sizeInfo}`,
|
|
157
89
|
};
|
|
158
90
|
});
|
|
159
91
|
|
|
160
|
-
const selected = await
|
|
161
|
-
message: "Select
|
|
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,100 @@ export async function runPublish(cwd: string, hubUrlOverride?: string): Promise<
|
|
|
168
99
|
process.exit(0);
|
|
169
100
|
}
|
|
170
101
|
|
|
171
|
-
const
|
|
172
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
194
|
-
|
|
118
|
+
if (isCancel(bump)) {
|
|
119
|
+
cancel("Publish cancelled.");
|
|
120
|
+
process.exit(0);
|
|
121
|
+
}
|
|
195
122
|
|
|
196
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
227
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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);
|
|
254
176
|
}
|
|
177
|
+
s.stop(`Publish failed: ${err.message}`);
|
|
178
|
+
process.exit(1);
|
|
255
179
|
}
|
|
256
180
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
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);
|
|
262
187
|
}
|
|
263
188
|
|
|
264
|
-
|
|
265
|
-
|
|
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);
|
|
266
197
|
}
|
|
267
198
|
}
|
package/src/commands/validate.ts
CHANGED
|
@@ -86,7 +86,12 @@ function validateManifest(manifest: WidgetManifest): ValidationResult {
|
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
export async function runValidate(
|
|
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.
|
|
110
|
-
|
|
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(
|
|
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
|
-
|
|
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 (
|
|
186
|
-
|
|
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
|
-
|
|
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
|
@@ -88,9 +88,12 @@ export function clearToken(): void {
|
|
|
88
88
|
|
|
89
89
|
export function getHubUrl(): string {
|
|
90
90
|
const stored = getStoredAuth()?.hubUrl;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
}
|
package/src/utils/hub-api.ts
CHANGED
|
@@ -62,8 +62,15 @@ export async function requestPublish(
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
if (!res.ok) {
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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>;
|
package/src/utils/manifest.ts
CHANGED
|
@@ -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`;
|