@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 +14 -2
- package/bin/cli.ts +1 -1
- package/package.json +1 -1
- package/src/commands/info.ts +2 -2
- package/src/commands/publish.ts +103 -171
- package/src/commands/validate.ts +58 -47
- package/src/utils/auth.ts +6 -3
- package/src/utils/hub-api.ts +22 -6
- package/src/utils/manifest.ts +2 -54
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/info.ts
CHANGED
|
@@ -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(`
|
|
48
|
-
log.info(` 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}`);
|
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,101 @@ 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
|
+
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
265
|
-
|
|
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
|
}
|
package/src/commands/validate.ts
CHANGED
|
@@ -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
|
-
//
|
|
26
|
-
|
|
27
|
-
if (!
|
|
28
|
-
|
|
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 (
|
|
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(
|
|
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(
|
|
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.
|
|
110
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
186
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
@@ -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:
|
|
25
|
-
maxSize:
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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>;
|
package/src/utils/manifest.ts
CHANGED
|
@@ -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
|
|
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`;
|