@glasshome/widget-cli 0.6.0 → 0.7.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/bin/cli.ts +1 -1
- package/package.json +3 -2
- package/src/commands/build.ts +8 -5
- package/src/commands/connect.ts +30 -23
- package/src/commands/create.ts +20 -20
- package/src/commands/info.ts +22 -27
- package/src/commands/login.ts +4 -5
- package/src/commands/migrate.ts +2 -2
- package/src/commands/publish.ts +3 -3
- package/src/commands/upgrade.ts +10 -7
- package/src/commands/validate.ts +8 -12
- package/src/migrate/config-codemod.test.ts +4 -4
- package/src/migrate/config-codemod.ts +1 -1
- package/src/utils/quiet.ts +20 -0
package/bin/cli.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glasshome/widget-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "CLI for creating and managing GlassHome widgets",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -22,8 +22,9 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@clack/prompts": "^1.6.0",
|
|
24
24
|
"@glasshome/widget-contract": "0.1.1",
|
|
25
|
-
"@glasshome/widget-sdk": "1.4.
|
|
25
|
+
"@glasshome/widget-sdk": "1.4.1",
|
|
26
26
|
"open": "^11.0.0",
|
|
27
|
+
"picocolors": "^1.1.1",
|
|
27
28
|
"semver": "^7.6.0",
|
|
28
29
|
"ts-morph": "^28.0.0",
|
|
29
30
|
"vite-plugin-solid": "^2.11.0"
|
package/src/commands/build.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { log, spinner } from "@clack/prompts";
|
|
2
2
|
import { buildWidgets } from "@glasshome/widget-sdk/vite";
|
|
3
3
|
import { lintAndReport } from "../utils/lint-source";
|
|
4
|
+
import { withQuietStdout } from "../utils/quiet";
|
|
4
5
|
|
|
5
6
|
export async function runBuild(cwd: string): Promise<void> {
|
|
6
7
|
const s = spinner();
|
|
@@ -9,11 +10,13 @@ export async function runBuild(cwd: string): Promise<void> {
|
|
|
9
10
|
try {
|
|
10
11
|
const solid = (await import("vite-plugin-solid")).default;
|
|
11
12
|
process.chdir(cwd);
|
|
12
|
-
await
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
await withQuietStdout(() =>
|
|
14
|
+
buildWidgets({
|
|
15
|
+
srcDir: "src",
|
|
16
|
+
outDir: "dist",
|
|
17
|
+
plugins: [solid()],
|
|
18
|
+
}),
|
|
19
|
+
);
|
|
17
20
|
s.stop("Build complete");
|
|
18
21
|
lintAndReport(cwd);
|
|
19
22
|
} catch (err) {
|
package/src/commands/connect.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, statSync, watch } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
-
import { log, spinner } from "@clack/prompts";
|
|
3
|
+
import { log, note, spinner } from "@clack/prompts";
|
|
4
|
+
import color from "picocolors";
|
|
4
5
|
import { buildWidgets } from "@glasshome/widget-sdk/vite";
|
|
5
6
|
import { trpcMutate, trpcQuery } from "../utils/api";
|
|
6
7
|
import { clearHostToken, extractHost, getHostToken, storeHostToken } from "../utils/auth";
|
|
7
8
|
import { lintAndReport } from "../utils/lint-source";
|
|
9
|
+
import { withQuietStdout } from "../utils/quiet";
|
|
8
10
|
|
|
9
11
|
interface RegistryWidget {
|
|
10
12
|
name: string;
|
|
@@ -34,7 +36,7 @@ async function uploadAndRegister(
|
|
|
34
36
|
|
|
35
37
|
const bundlePath = resolve(distDir, `${slug}.js`);
|
|
36
38
|
if (!existsSync(bundlePath)) {
|
|
37
|
-
log.warn(`Bundle not found: ${bundlePath}
|
|
39
|
+
log.warn(`Bundle not found: ${bundlePath}, skipping ${slug}`);
|
|
38
40
|
return;
|
|
39
41
|
}
|
|
40
42
|
|
|
@@ -116,7 +118,7 @@ export async function runConnect(
|
|
|
116
118
|
): Promise<void> {
|
|
117
119
|
const distDir = resolve(cwd, "dist");
|
|
118
120
|
const solid = (await import("vite-plugin-solid")).default;
|
|
119
|
-
// delegateEvents: false
|
|
121
|
+
// delegateEvents: false, widgets run in closed shadow roots where Solid's
|
|
120
122
|
// document-level event delegation cannot see the target.
|
|
121
123
|
const buildOpts = {
|
|
122
124
|
srcDir: "src",
|
|
@@ -130,7 +132,7 @@ export async function runConnect(
|
|
|
130
132
|
try {
|
|
131
133
|
const origCwd = process.cwd();
|
|
132
134
|
process.chdir(cwd);
|
|
133
|
-
await buildWidgets(buildOpts);
|
|
135
|
+
await withQuietStdout(() => buildWidgets(buildOpts));
|
|
134
136
|
process.chdir(origCwd);
|
|
135
137
|
} catch (err) {
|
|
136
138
|
s.stop("Build failed");
|
|
@@ -154,7 +156,7 @@ export async function runConnect(
|
|
|
154
156
|
|
|
155
157
|
if (opts.reAuth) {
|
|
156
158
|
clearHostToken(host);
|
|
157
|
-
log.info("Discarded stored credentials (--re-auth)
|
|
159
|
+
log.info("Discarded stored credentials (--re-auth)");
|
|
158
160
|
}
|
|
159
161
|
|
|
160
162
|
// Validate stored token before using it
|
|
@@ -168,17 +170,17 @@ export async function runConnect(
|
|
|
168
170
|
const body = (await check.json()) as { session?: unknown } | null;
|
|
169
171
|
if (body?.session) {
|
|
170
172
|
token = existingToken;
|
|
171
|
-
log.info("Using stored credentials
|
|
173
|
+
log.info("Using stored credentials");
|
|
172
174
|
} else {
|
|
173
175
|
clearHostToken(host);
|
|
174
|
-
log.warn("Stored credentials expired
|
|
176
|
+
log.warn("Stored credentials expired, re-authenticating");
|
|
175
177
|
}
|
|
176
178
|
} else {
|
|
177
179
|
clearHostToken(host);
|
|
178
|
-
log.warn("Stored credentials expired
|
|
180
|
+
log.warn("Stored credentials expired, re-authenticating");
|
|
179
181
|
}
|
|
180
182
|
} catch {
|
|
181
|
-
log.warn("Could not validate stored credentials
|
|
183
|
+
log.warn("Could not validate stored credentials, re-authenticating");
|
|
182
184
|
}
|
|
183
185
|
}
|
|
184
186
|
|
|
@@ -220,7 +222,7 @@ export async function runConnect(
|
|
|
220
222
|
log.warn(
|
|
221
223
|
`Could not reach dashboard at ${api}: ${err instanceof Error ? err.message : String(err)}`,
|
|
222
224
|
);
|
|
223
|
-
// token stays empty
|
|
225
|
+
// token stays empty, caught by guard below
|
|
224
226
|
}
|
|
225
227
|
|
|
226
228
|
if (deviceCode!) {
|
|
@@ -286,7 +288,7 @@ export async function runConnect(
|
|
|
286
288
|
s.stop("Device code expired");
|
|
287
289
|
break;
|
|
288
290
|
}
|
|
289
|
-
// Unexpected error
|
|
291
|
+
// Unexpected error, stop polling
|
|
290
292
|
s.stop(`Auth error: ${errData.error ?? res.status}`);
|
|
291
293
|
break;
|
|
292
294
|
} catch (err) {
|
|
@@ -305,7 +307,7 @@ export async function runConnect(
|
|
|
305
307
|
}
|
|
306
308
|
|
|
307
309
|
if (!token) {
|
|
308
|
-
log.warn("Authentication failed
|
|
310
|
+
log.warn("Authentication failed, widgets won't be connected. Log in at the dashboard first, then restart.");
|
|
309
311
|
return;
|
|
310
312
|
}
|
|
311
313
|
|
|
@@ -326,7 +328,7 @@ export async function runConnect(
|
|
|
326
328
|
});
|
|
327
329
|
}
|
|
328
330
|
} catch {
|
|
329
|
-
// Non-fatal
|
|
331
|
+
// Non-fatal, dev mode may already be enabled
|
|
330
332
|
}
|
|
331
333
|
|
|
332
334
|
// Step 5: Upload bundles and register widgets
|
|
@@ -360,7 +362,7 @@ export async function runConnect(
|
|
|
360
362
|
const origCwd = process.cwd();
|
|
361
363
|
process.chdir(cwd);
|
|
362
364
|
try {
|
|
363
|
-
await buildWidgets({ ...buildOpts, only: widgets });
|
|
365
|
+
await withQuietStdout(() => buildWidgets({ ...buildOpts, only: widgets }));
|
|
364
366
|
} finally {
|
|
365
367
|
process.chdir(origCwd);
|
|
366
368
|
}
|
|
@@ -373,7 +375,7 @@ export async function runConnect(
|
|
|
373
375
|
await uploadAndRegister(apiUrl, distDir, widget, token);
|
|
374
376
|
log.info(`Rebuilt & uploaded ${widgetName}`);
|
|
375
377
|
} else {
|
|
376
|
-
log.warn(`No registry entry for ${widgetName}
|
|
378
|
+
log.warn(`No registry entry for ${widgetName}, skipping upload`);
|
|
377
379
|
}
|
|
378
380
|
}
|
|
379
381
|
} catch (err) {
|
|
@@ -404,14 +406,19 @@ export async function runConnect(
|
|
|
404
406
|
).length
|
|
405
407
|
: 0;
|
|
406
408
|
|
|
407
|
-
log.success(`Connected
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
409
|
+
log.success(color.green(`Connected: ${widgetCount} widget(s) live`));
|
|
410
|
+
note(
|
|
411
|
+
[
|
|
412
|
+
`${color.cyan("watching")} src/ (edits auto-rebuild & re-upload)`,
|
|
413
|
+
`${color.cyan("add")} new widgets with ${color.bold("bun widget add")}`,
|
|
414
|
+
`${color.cyan("stop")} press ${color.bold("Ctrl+C")} to disconnect`,
|
|
415
|
+
].join("\n"),
|
|
416
|
+
"Live testing",
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
// Step 7: Handle SIGINT, unregister and clean up
|
|
413
420
|
const cleanup = async () => {
|
|
414
|
-
log.info("
|
|
421
|
+
log.info("Disconnecting...");
|
|
415
422
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
416
423
|
watcher.close();
|
|
417
424
|
|
|
@@ -424,7 +431,7 @@ export async function runConnect(
|
|
|
424
431
|
input: { scope: "local", name: slug },
|
|
425
432
|
});
|
|
426
433
|
} catch {
|
|
427
|
-
// Non-fatal
|
|
434
|
+
// Non-fatal, API may be down
|
|
428
435
|
}
|
|
429
436
|
}
|
|
430
437
|
log.info("Widgets unregistered");
|
package/src/commands/create.ts
CHANGED
|
@@ -9,7 +9,8 @@ import {
|
|
|
9
9
|
writeFileSync,
|
|
10
10
|
} from "node:fs";
|
|
11
11
|
import { dirname, join, resolve } from "node:path";
|
|
12
|
-
import { cancel, confirm, isCancel, log, spinner, text } from "@clack/prompts";
|
|
12
|
+
import { cancel, confirm, isCancel, log, note, spinner, text } from "@clack/prompts";
|
|
13
|
+
import color from "picocolors";
|
|
13
14
|
import { getCliVersion } from "../utils/version";
|
|
14
15
|
import { promptWidgetDetails, scaffoldWidget } from "./add";
|
|
15
16
|
|
|
@@ -157,8 +158,8 @@ export async function runCreate() {
|
|
|
157
158
|
} else {
|
|
158
159
|
installSpinner.stop(
|
|
159
160
|
monorepoRoot
|
|
160
|
-
? "Install failed
|
|
161
|
-
: "Install failed
|
|
161
|
+
? "Install failed, run `bun install` from the monorepo root"
|
|
162
|
+
: "Install failed, you can run `bun install` manually",
|
|
162
163
|
);
|
|
163
164
|
}
|
|
164
165
|
}
|
|
@@ -167,7 +168,7 @@ export async function runCreate() {
|
|
|
167
168
|
const inGitRepo = isInsideGitRepo(process.cwd());
|
|
168
169
|
|
|
169
170
|
if (inGitRepo) {
|
|
170
|
-
log.info("Skipping git init
|
|
171
|
+
log.info("Skipping git init, already in a git repository");
|
|
171
172
|
} else {
|
|
172
173
|
const shouldGit = await confirm({
|
|
173
174
|
message: "Initialize git repository?",
|
|
@@ -183,30 +184,29 @@ export async function runCreate() {
|
|
|
183
184
|
Bun.spawnSync(["git", "commit", "-m", "Initial widget scaffold"], { cwd: targetDir });
|
|
184
185
|
gitSpinner.stop("Git repository initialized");
|
|
185
186
|
} else {
|
|
186
|
-
gitSpinner.stop("Git init failed
|
|
187
|
+
gitSpinner.stop("Git init failed, you can run `git init` manually");
|
|
187
188
|
}
|
|
188
189
|
}
|
|
189
190
|
}
|
|
190
191
|
|
|
191
192
|
// Next-steps guide
|
|
192
|
-
|
|
193
|
-
|
|
193
|
+
const cmd = (c: string, desc: string) => `${color.cyan(c.padEnd(30))} ${color.dim(desc)}`;
|
|
194
|
+
const steps: string[] = [];
|
|
194
195
|
if (monorepoRoot) {
|
|
195
|
-
|
|
196
|
-
|
|
196
|
+
steps.push(cmd("bun install", "from monorepo root, links workspace packages"));
|
|
197
|
+
steps.push(color.cyan(`cd ${projectName}`));
|
|
197
198
|
} else {
|
|
198
|
-
|
|
199
|
-
if (isCancel(shouldInstall) || !shouldInstall)
|
|
200
|
-
log.info(" bun install");
|
|
201
|
-
}
|
|
199
|
+
steps.push(color.cyan(`cd ${projectName}`));
|
|
200
|
+
if (isCancel(shouldInstall) || !shouldInstall) steps.push(color.cyan("bun install"));
|
|
202
201
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
202
|
+
steps.push(cmd("bun widget connect <url>", "connect to a dashboard for live testing"));
|
|
203
|
+
steps.push(cmd("bun widget add", "add another widget"));
|
|
204
|
+
steps.push(cmd("bun widget build", "build all widgets"));
|
|
205
|
+
steps.push(cmd("bun widget validate", "validate all widgets"));
|
|
206
|
+
steps.push(cmd("bun widget publish", "build & publish to GlassHome Hub"));
|
|
207
|
+
steps.push(cmd("bun widget info", "show widget metadata"));
|
|
208
|
+
steps.push(cmd("bun widget upgrade", "upgrade @glasshome/widget-sdk"));
|
|
209
|
+
note(steps.join("\n"), "Next steps");
|
|
210
210
|
} catch (err) {
|
|
211
211
|
s.stop("Failed to scaffold widget project.");
|
|
212
212
|
rmSync(targetDir, { recursive: true, force: true });
|
package/src/commands/info.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
-
import { log } from "@clack/prompts";
|
|
3
|
+
import { log, note } from "@clack/prompts";
|
|
4
|
+
import color from "picocolors";
|
|
4
5
|
import {
|
|
5
6
|
discoverWidgets,
|
|
6
7
|
formatBytes,
|
|
@@ -18,8 +19,6 @@ export async function runInfo(cwd: string, widgetName?: string): Promise<void> {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
21
|
-
log.info(`Project: ${pkg.name ?? "unknown"}`);
|
|
22
|
-
log.info(`Version: ${pkg.version ?? "0.0.0"}`);
|
|
23
22
|
|
|
24
23
|
const widgets = discoverWidgets(cwd);
|
|
25
24
|
|
|
@@ -35,44 +34,40 @@ export async function runInfo(cwd: string, widgetName?: string): Promise<void> {
|
|
|
35
34
|
process.exit(1);
|
|
36
35
|
}
|
|
37
36
|
|
|
38
|
-
log.
|
|
37
|
+
log.step(
|
|
38
|
+
`${color.bold(pkg.name ?? "unknown")} ${color.dim(`v${pkg.version ?? "0.0.0"}`)} ${color.dim("·")} ${widgets.length} widget(s)`,
|
|
39
|
+
);
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
// Right-pad labels so values line up inside the note box.
|
|
42
|
+
const field = (label: string, value: string) => `${color.dim(label.padEnd(9))} ${value}`;
|
|
42
43
|
|
|
44
|
+
for (const name of toShow) {
|
|
43
45
|
try {
|
|
44
46
|
const manifest = readManifest(cwd, name);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (manifest.
|
|
53
|
-
log.info(` Version: ${manifest.version}`);
|
|
54
|
-
}
|
|
55
|
-
if (manifest.description) {
|
|
56
|
-
log.info(` Description: ${manifest.description}`);
|
|
57
|
-
}
|
|
47
|
+
const lines: string[] = [];
|
|
48
|
+
|
|
49
|
+
const size = (s: { w: number; h: number }) => `${s.w}×${s.h}`;
|
|
50
|
+
lines.push(field("Size", `${size(manifest.minSize)} → ${size(manifest.maxSize)}`));
|
|
51
|
+
if (manifest.defaultSize) lines.push(field("Default", size(manifest.defaultSize)));
|
|
52
|
+
lines.push(field("SDK", manifest.sdkVersion));
|
|
53
|
+
if (manifest.version) lines.push(field("Version", manifest.version));
|
|
54
|
+
if (manifest.description) lines.push(field("About", color.dim(manifest.description)));
|
|
58
55
|
|
|
59
|
-
// Bundle size (if built)
|
|
60
56
|
try {
|
|
61
57
|
const info = getWidgetBundleInfo(cwd, name);
|
|
62
|
-
|
|
58
|
+
lines.push(field("Bundle", `${formatBytes(info.raw)} (${formatBytes(info.gzip)} gzipped)`));
|
|
63
59
|
} catch {
|
|
64
|
-
|
|
60
|
+
lines.push(field("Bundle", color.dim("not built yet")));
|
|
65
61
|
}
|
|
62
|
+
|
|
63
|
+
note(lines.join("\n"), `${manifest.name} ${color.dim(`(${name})`)}`);
|
|
66
64
|
} catch (err) {
|
|
67
|
-
log.warn(
|
|
65
|
+
log.warn(`${name}: could not read manifest: ${err instanceof Error ? err.message : String(err)}`);
|
|
68
66
|
}
|
|
69
67
|
}
|
|
70
68
|
|
|
71
|
-
// Registry summary
|
|
72
69
|
const registry = readRegistry(cwd);
|
|
73
70
|
if (registry) {
|
|
74
|
-
log.info(
|
|
75
|
-
`\nRegistry: ${registry.widgets.length} widget(s) (generated ${registry.generatedAt})`,
|
|
76
|
-
);
|
|
71
|
+
log.info(`Registry: ${registry.widgets.length} widget(s) (generated ${registry.generatedAt})`);
|
|
77
72
|
}
|
|
78
73
|
}
|
package/src/commands/login.ts
CHANGED
|
@@ -39,7 +39,7 @@ export async function runLogin(hubUrl?: string): Promise<void> {
|
|
|
39
39
|
}>((resolve, reject) => {
|
|
40
40
|
const timeout = setTimeout(() => {
|
|
41
41
|
server.close();
|
|
42
|
-
reject(new Error("Login timed out
|
|
42
|
+
reject(new Error("Login timed out, no response from browser within 120 seconds."));
|
|
43
43
|
}, LOGIN_TIMEOUT_MS);
|
|
44
44
|
|
|
45
45
|
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
|
|
@@ -96,17 +96,16 @@ export async function runLogin(hubUrl?: string): Promise<void> {
|
|
|
96
96
|
});
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
-
s.stop("
|
|
99
|
+
s.stop("Browser login complete");
|
|
100
100
|
|
|
101
|
-
// Verify state
|
|
101
|
+
// Verify state (spinner already stopped, use log, not s.stop)
|
|
102
102
|
if (receivedState !== state) {
|
|
103
|
-
s.stop("Login failed");
|
|
104
103
|
log.error("State mismatch. Possible CSRF attack.");
|
|
105
104
|
process.exit(1);
|
|
106
105
|
}
|
|
107
106
|
|
|
108
107
|
// Exchange code for tokens
|
|
109
|
-
s.
|
|
108
|
+
s.start("Exchanging code for tokens...");
|
|
110
109
|
|
|
111
110
|
const tokenRes = await fetch(`${hub}/api/auth/oauth2/token`, {
|
|
112
111
|
method: "POST",
|
package/src/commands/migrate.ts
CHANGED
|
@@ -26,7 +26,7 @@ export interface MigrateOptions {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
* `bun widget migrate config
|
|
29
|
+
* `bun widget migrate config`, rewrite widgets' raw-zod config to `defineConfig` +
|
|
30
30
|
* `field.*`. Assistive: unmigratable fields stay raw zod and are reported as manual
|
|
31
31
|
* TODOs; validation is never dropped. Use `--dry` to preview without writing.
|
|
32
32
|
*/
|
|
@@ -73,7 +73,7 @@ export async function runMigrate(
|
|
|
73
73
|
} else {
|
|
74
74
|
totalTodos += warnings.length;
|
|
75
75
|
log.warn(
|
|
76
|
-
`${widget}: ${verb} ${rel}
|
|
76
|
+
`${widget}: ${verb} ${rel}, ${warnings.length} field(s) left as raw zod (manual TODO):`,
|
|
77
77
|
);
|
|
78
78
|
for (const w of warnings) {
|
|
79
79
|
log.message(` • ${w.field}: ${w.reason}`);
|
package/src/commands/publish.ts
CHANGED
|
@@ -113,7 +113,7 @@ export async function runPublish(
|
|
|
113
113
|
let sizeInfo = "";
|
|
114
114
|
try {
|
|
115
115
|
const info = getWidgetBundleInfo(cwd, name);
|
|
116
|
-
sizeInfo =
|
|
116
|
+
sizeInfo = `, ${formatBytes(info.raw)}`;
|
|
117
117
|
} catch {}
|
|
118
118
|
return {
|
|
119
119
|
value: name,
|
|
@@ -226,7 +226,7 @@ export async function runPublish(
|
|
|
226
226
|
});
|
|
227
227
|
} catch (err: any) {
|
|
228
228
|
if (err.status === 409) {
|
|
229
|
-
s.stop(`${manifest.name}@${version} already published
|
|
229
|
+
s.stop(`${manifest.name}@${version} already published, bump version to republish`);
|
|
230
230
|
process.exit(0);
|
|
231
231
|
}
|
|
232
232
|
s.stop(`Publish failed: ${err.message}`);
|
|
@@ -244,7 +244,7 @@ export async function runPublish(
|
|
|
244
244
|
if (cssBuffer) {
|
|
245
245
|
if (!publishData.cssUploadUrl) {
|
|
246
246
|
s.stop(
|
|
247
|
-
`dist/${widgetName}.css exists but the Hub did not return a css upload URL
|
|
247
|
+
`dist/${widgetName}.css exists but the Hub did not return a css upload URL, update the Hub or remove the stylesheet`,
|
|
248
248
|
);
|
|
249
249
|
process.exit(1);
|
|
250
250
|
}
|
package/src/commands/upgrade.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
|
-
import { log, spinner } from "@clack/prompts";
|
|
3
|
+
import { log, note, spinner } from "@clack/prompts";
|
|
4
4
|
import { discoverWidgets, readManifest, writeManifest } from "../utils/manifest";
|
|
5
5
|
import { runValidate } from "./validate";
|
|
6
6
|
|
|
@@ -124,11 +124,14 @@ export async function runUpgrade(cwd: string): Promise<void> {
|
|
|
124
124
|
} else {
|
|
125
125
|
// Standalone mode: can't fetch from npm (private package), guide user
|
|
126
126
|
log.info("@glasshome/widget-sdk is a workspace package and is not published to npm.");
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
127
|
+
note(
|
|
128
|
+
[
|
|
129
|
+
"1. Bump @glasshome/widget-sdk in package.json peerDependencies",
|
|
130
|
+
"2. bun install",
|
|
131
|
+
"3. bun widget upgrade (sync manifest files)",
|
|
132
|
+
"4. bun widget validate (check compatibility)",
|
|
133
|
+
].join("\n"),
|
|
134
|
+
"To upgrade",
|
|
135
|
+
);
|
|
133
136
|
}
|
|
134
137
|
}
|
package/src/commands/validate.ts
CHANGED
|
@@ -40,18 +40,18 @@ function validateManifest(manifest: WidgetManifest): ValidationResult {
|
|
|
40
40
|
const capabilities = (manifest as { capabilities?: unknown }).capabilities;
|
|
41
41
|
if (manifest.sdkVersion && requiresCapabilities(manifest.sdkVersion) && capabilities === undefined) {
|
|
42
42
|
errors.push(
|
|
43
|
-
`sdkVersion "${manifest.sdkVersion}" only admits SDK >= 1.0.0, so manifest.json must declare "capabilities"
|
|
43
|
+
`sdkVersion "${manifest.sdkVersion}" only admits SDK >= 1.0.0, so manifest.json must declare "capabilities", use [] if the widget never reads or controls Home Assistant`,
|
|
44
44
|
);
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
// INS-05: "*" sdkVersion is rejected at publish time
|
|
48
|
+
// INS-05: "*" sdkVersion is rejected at publish time, authors must declare
|
|
49
49
|
// a real range (e.g. "^0.3.0"). Installed widgets that already ship "*" are
|
|
50
50
|
// still tolerated by the loader with a warn-and-mount, but new publishes
|
|
51
51
|
// are blocked here.
|
|
52
52
|
if (manifest.sdkVersion === "*") {
|
|
53
53
|
errors.push(
|
|
54
|
-
`sdkVersion "*" is not allowed
|
|
54
|
+
`sdkVersion "*" is not allowed, declare a semver range (e.g. "^0.3.0") to opt into host compatibility checks`,
|
|
55
55
|
);
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -156,7 +156,7 @@ export async function runValidate(
|
|
|
156
156
|
const failed: string[] = [];
|
|
157
157
|
|
|
158
158
|
for (const name of toValidate) {
|
|
159
|
-
if (!quiet) log.
|
|
159
|
+
if (!quiet) log.step(name);
|
|
160
160
|
|
|
161
161
|
// Read and validate manifest.json
|
|
162
162
|
let manifest: WidgetManifest;
|
|
@@ -211,7 +211,7 @@ export async function runValidate(
|
|
|
211
211
|
// Validate registry.json
|
|
212
212
|
const registry = readRegistry(cwd);
|
|
213
213
|
if (!registry) {
|
|
214
|
-
if (!quiet) log.error("
|
|
214
|
+
if (!quiet) log.error("dist/registry.json not found");
|
|
215
215
|
allPassed = false;
|
|
216
216
|
} else {
|
|
217
217
|
const registryTags = new Set(registry.widgets.map((w) => w.bundleUrl));
|
|
@@ -221,21 +221,17 @@ export async function runValidate(
|
|
|
221
221
|
allPassed = false;
|
|
222
222
|
}
|
|
223
223
|
}
|
|
224
|
-
if (!quiet) log.info(
|
|
224
|
+
if (!quiet) log.info(`Registry: ${registry.widgets.length} widget(s)`);
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
// Quiet callers (publish) get a one-line summary; interactive runs let the
|
|
228
|
+
// caller's outro own the final banner so it isn't printed twice.
|
|
227
229
|
if (quiet) {
|
|
228
230
|
if (allPassed) {
|
|
229
231
|
log.success(`Validated ${toValidate.length} widget(s)`);
|
|
230
232
|
} else {
|
|
231
233
|
log.error(`Validation failed for: ${failed.join(", ")}`);
|
|
232
234
|
}
|
|
233
|
-
} else {
|
|
234
|
-
if (allPassed) {
|
|
235
|
-
log.success("\nAll checks passed");
|
|
236
|
-
} else {
|
|
237
|
-
log.error("\nValidation failed");
|
|
238
|
-
}
|
|
239
235
|
}
|
|
240
236
|
|
|
241
237
|
return allPassed;
|
|
@@ -6,7 +6,7 @@ function migrate(body: string) {
|
|
|
6
6
|
return migrateConfigSource(code, "widget.tsx");
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
describe("migrateConfigSource
|
|
9
|
+
describe("migrateConfigSource, field kinds", () => {
|
|
10
10
|
test("widgetFields.* map to field.*", () => {
|
|
11
11
|
const r = migrate(
|
|
12
12
|
`export const configSchema = z.object({\n title: widgetFields.title(),\n area: widgetFields.areaId(),\n lights: widgetFields.entityIds("light"),\n th: widgetFields.singleEntity("sensor", { deviceClass: "temperature" }),\n});`,
|
|
@@ -60,7 +60,7 @@ describe("migrateConfigSource — field kinds", () => {
|
|
|
60
60
|
});
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
-
describe("migrateConfigSource
|
|
63
|
+
describe("migrateConfigSource, imports", () => {
|
|
64
64
|
test("adds defineConfig/field/Infer, drops widgetFields/z when unused", () => {
|
|
65
65
|
const r = migrate(
|
|
66
66
|
`export const configSchema = z.object({ title: widgetFields.title() });\nexport type Config = z.infer<typeof configSchema>;`,
|
|
@@ -69,7 +69,7 @@ describe("migrateConfigSource — imports", () => {
|
|
|
69
69
|
expect(importLine).toContain("defineConfig");
|
|
70
70
|
expect(importLine).toContain("field");
|
|
71
71
|
expect(importLine).toContain("Infer");
|
|
72
|
-
// `Infer` is a type
|
|
72
|
+
// `Infer` is a type, must be type-only for verbatimModuleSyntax widgets.
|
|
73
73
|
expect(importLine).toContain("type Infer");
|
|
74
74
|
expect(importLine).not.toContain("widgetFields");
|
|
75
75
|
expect(importLine).not.toContain(" z,");
|
|
@@ -105,7 +105,7 @@ describe("migrateConfigSource — imports", () => {
|
|
|
105
105
|
});
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
-
describe("migrateConfigSource
|
|
108
|
+
describe("migrateConfigSource, report and skip (never drop validation)", () => {
|
|
109
109
|
test("factory-built + identifier fields are left raw and reported", () => {
|
|
110
110
|
const r = migrate(
|
|
111
111
|
`const powerEntity = (l: string) => z.array(z.string()).default([]).meta({ domain: "sensor", title: l });\nexport const configSchema = z.object({\n title: widgetFields.title(),\n solar: powerEntity("Solar"),\n strategy: consumptionStrategy,\n});`,
|
|
@@ -216,7 +216,7 @@ function buildShape(
|
|
|
216
216
|
const lines: string[] = [];
|
|
217
217
|
for (const prop of obj.getProperties()) {
|
|
218
218
|
if (!Node.isPropertyAssignment(prop)) {
|
|
219
|
-
// Spread, shorthand, method
|
|
219
|
+
// Spread, shorthand, method, leave the whole thing untouched conservatively.
|
|
220
220
|
lines.push(`${indent} ${prop.getText()}`);
|
|
221
221
|
warnings.push({
|
|
222
222
|
field: prop.getText().slice(0, 40),
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run `fn` with stdout writes swallowed. `buildWidgets()` (SDK) console.logs a
|
|
3
|
+
* "[registry] Generated..." line mid-build; when the caller drives a clack
|
|
4
|
+
* spinner that raw write lands on the spinner's line and garbles it. Stderr is
|
|
5
|
+
* left alone so real warnings/errors still surface.
|
|
6
|
+
*/
|
|
7
|
+
export async function withQuietStdout<T>(fn: () => Promise<T>): Promise<T> {
|
|
8
|
+
const originalWrite = process.stdout.write.bind(process.stdout);
|
|
9
|
+
const originalLog = console.log;
|
|
10
|
+
// Bun's console.log writes to fd 1 natively and bypasses a process.stdout.write
|
|
11
|
+
// override, so both paths are silenced.
|
|
12
|
+
process.stdout.write = (() => true) as typeof process.stdout.write;
|
|
13
|
+
console.log = () => {};
|
|
14
|
+
try {
|
|
15
|
+
return await fn();
|
|
16
|
+
} finally {
|
|
17
|
+
process.stdout.write = originalWrite;
|
|
18
|
+
console.log = originalLog;
|
|
19
|
+
}
|
|
20
|
+
}
|