@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 +14 -2
- package/bin/cli.ts +3 -2
- package/package.json +1 -1
- package/src/commands/publish.ts +104 -172
- package/src/commands/validate.ts +38 -18
- package/src/utils/auth.ts +9 -1
- 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
|
|
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
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";
|
|
@@ -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
|
-
|
|
19
|
-
|
|
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:
|
|
28
|
-
const
|
|
29
|
-
|
|
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
|
|
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 `
|
|
43
|
+
log.error("Authentication failed. Run `glasshome-widget login` manually.");
|
|
117
44
|
process.exit(1);
|
|
118
45
|
}
|
|
119
46
|
}
|
|
120
47
|
|
|
121
|
-
//
|
|
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
|
|
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}
|
|
88
|
+
label: `${manifest.tag}${sizeInfo}`,
|
|
156
89
|
};
|
|
157
90
|
});
|
|
158
91
|
|
|
159
|
-
const selected = await
|
|
160
|
-
message: "Select
|
|
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
|
|
171
|
-
|
|
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
|
-
|
|
181
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
193
|
-
|
|
118
|
+
if (isCancel(bump)) {
|
|
119
|
+
cancel("Publish cancelled.");
|
|
120
|
+
process.exit(0);
|
|
121
|
+
}
|
|
194
122
|
|
|
195
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
226
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
264
|
-
|
|
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
|
}
|
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
|
@@ -87,7 +87,15 @@ export function clearToken(): void {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
export function getHubUrl(): string {
|
|
90
|
-
|
|
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 {
|
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`;
|