@glasshome/widget-cli 0.10.6 → 0.11.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 CHANGED
@@ -33,7 +33,7 @@ Commands:
33
33
  publish [hub-url] Build and publish a widget to Hub
34
34
  login [hub-url] Authenticate with GlassHome Hub
35
35
  info [name] Show widget metadata and bundle info
36
- upgrade Upgrade @glasshome/widget-sdk to latest version
36
+ upgrade Upgrade @glasshome/widget-sdk (latest, or --to <version>) and sync manifests
37
37
  migrate config Rewrite widgets' raw-zod config to defineConfig + field.*
38
38
  --name <widget> targets one; --dry previews without writing
39
39
  help Show this help message
@@ -56,6 +56,7 @@ Examples:
56
56
  glasshome-widget login
57
57
  glasshome-widget login https://my-hub.example.com
58
58
  glasshome-widget upgrade
59
+ glasshome-widget upgrade --to 1.10.3
59
60
  glasshome-widget publish --dir packages/widgets
60
61
  `.trim();
61
62
 
@@ -68,6 +69,7 @@ const { values: flags, positionals } = parseArgs({
68
69
  name: { type: "string" },
69
70
  bump: { type: "string" },
70
71
  scope: { type: "string" },
72
+ to: { type: "string" },
71
73
  "re-auth": { type: "boolean" },
72
74
  isolate: { type: "boolean" },
73
75
  dry: { type: "boolean" },
@@ -193,7 +195,7 @@ switch (effectiveCommand) {
193
195
 
194
196
  case "upgrade": {
195
197
  const { runUpgrade } = await import("../src/commands/upgrade");
196
- await runUpgrade(resolveWidgetDir());
198
+ await runUpgrade(resolveWidgetDir(), { to: str(flags.to) });
197
199
  outro("Done");
198
200
  break;
199
201
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glasshome/widget-cli",
3
- "version": "0.10.6",
3
+ "version": "0.11.1",
4
4
  "description": "CLI for creating and managing GlassHome widgets",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,7 +30,7 @@
30
30
  "@fontsource-variable/geist-mono": "^5.2.8",
31
31
  "@glasshome/sync-layer": "0.7.2",
32
32
  "@glasshome/widget-contract": "0.2.0",
33
- "@glasshome/widget-sdk": "1.10.3",
33
+ "@glasshome/widget-sdk": "1.11.0",
34
34
  "@iconify-json/lucide": "^1.2.0",
35
35
  "@iconify-json/mdi": "^1.2.3",
36
36
  "@tailwindcss/vite": "^4.1.13",
@@ -2,7 +2,7 @@ import { existsSync, readdirSync, readFileSync, statSync, watch } from "node:fs"
2
2
  import { resolve } from "node:path";
3
3
  import { log, note, spinner } from "@clack/prompts";
4
4
  import color from "picocolors";
5
- import { buildWidgets } from "@glasshome/widget-sdk/vite";
5
+ import { buildWidgets, createIntrospectSession } from "@glasshome/widget-sdk/vite";
6
6
  import { trpcMutate, trpcQuery } from "../utils/api";
7
7
  import { clearHostToken, extractHost, getHostToken, storeHostToken } from "../utils/auth";
8
8
  import { lintAndReport } from "../utils/lint-source";
@@ -348,6 +348,9 @@ export async function runConnect(
348
348
  const pending = new Set<string>();
349
349
  let debounceTimer: ReturnType<typeof setTimeout> | null = null;
350
350
  let buildInFlight = false;
351
+ // One worker for the whole session: a fresh introspection process per save is
352
+ // the boot tax this avoids (docs/superpowers/specs/2026-08-18-widget-sdk-persistent-introspect-worker-design.md).
353
+ const introspectSession = createIntrospectSession();
351
354
 
352
355
  async function flush(): Promise<void> {
353
356
  debounceTimer = null;
@@ -362,7 +365,9 @@ export async function runConnect(
362
365
  const origCwd = process.cwd();
363
366
  process.chdir(cwd);
364
367
  try {
365
- await withQuietStdout(() => buildWidgets({ ...buildOpts, only: widgets }));
368
+ await withQuietStdout(() =>
369
+ buildWidgets({ ...buildOpts, only: widgets, session: introspectSession }),
370
+ );
366
371
  } finally {
367
372
  process.chdir(origCwd);
368
373
  }
@@ -421,6 +426,9 @@ export async function runConnect(
421
426
  log.info("Disconnecting...");
422
427
  if (debounceTimer) clearTimeout(debounceTimer);
423
428
  watcher.close();
429
+ // connect never exits normally, so without this the worker only dies via
430
+ // its stdin-EOF watchdog.
431
+ await introspectSession.dispose();
424
432
 
425
433
  for (const slug of registeredTags) {
426
434
  try {
@@ -21,14 +21,8 @@ export async function runPreview(cwd: string, names: string[], isolate: boolean)
21
21
  process.exit(1);
22
22
  }
23
23
 
24
- // Capture the REAL stdout writer before withQuietStdout no-ops
25
- // process.stdout.write. A clack spinner writes through that same override and
26
- // would freeze inside the quiet block, so progress goes out this bound
27
- // reference (which the override cannot intercept) as plain lines. Build/vite
28
- // noise stays silenced; only these phase lines get through.
29
- const realWrite = process.stdout.write.bind(process.stdout);
30
24
  const progress = (m: string) => {
31
- realWrite(`${color.gray("│")} ${color.dim(m)}\n`);
25
+ process.stdout.write(`${color.gray("│")} ${color.dim(m)}\n`);
32
26
  };
33
27
 
34
28
  log.info(names.length ? `Previewing ${names.join(", ")}` : "Previewing all widgets");
@@ -0,0 +1,20 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { sdkDependencySection } from "./upgrade";
3
+
4
+ describe("sdkDependencySection", () => {
5
+ test("finds the SDK in whichever section declares it", () => {
6
+ expect(sdkDependencySection({ peerDependencies: { "@glasshome/widget-sdk": "^1.10.2" } })).toBe(
7
+ "peerDependencies",
8
+ );
9
+ expect(sdkDependencySection({ devDependencies: { "@glasshome/widget-sdk": "^1.10.2" } })).toBe(
10
+ "devDependencies",
11
+ );
12
+ expect(sdkDependencySection({ dependencies: { "@glasshome/widget-sdk": "^1.10.2" } })).toBe(
13
+ "dependencies",
14
+ );
15
+ });
16
+
17
+ test("returns null when the SDK is not declared", () => {
18
+ expect(sdkDependencySection({ dependencies: { zod: "^4" } })).toBeNull();
19
+ });
20
+ });
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
- import { log, note, spinner } from "@clack/prompts";
3
+ import { log, spinner } from "@clack/prompts";
4
4
  import { discoverWidgets, readManifest, writeManifest } from "../utils/manifest";
5
5
  import { getInstalledSdkVersion } from "../utils/sdk-version";
6
6
  import { runValidate } from "./validate";
@@ -58,20 +58,60 @@ function syncManifestSdkVersions(cwd: string, newVersion: string): void {
58
58
  log.info(`Updated sdkVersion to ${sdkRange} in ${widgets.length} manifest(s)`);
59
59
  }
60
60
 
61
- export async function runUpgrade(cwd: string): Promise<void> {
61
+ const SDK_PKG = "@glasshome/widget-sdk";
62
+
63
+ type DependencySection = "peerDependencies" | "devDependencies" | "dependencies";
64
+ type PackageManifest = Partial<Record<DependencySection, Record<string, string>>>;
65
+
66
+ const SECTION_FLAG: Record<DependencySection, string[]> = {
67
+ peerDependencies: ["--peer"],
68
+ devDependencies: ["--dev"],
69
+ dependencies: [],
70
+ };
71
+
72
+ /** Which section declares the SDK, so the bump lands where the author put it. */
73
+ export function sdkDependencySection(pkg: PackageManifest): DependencySection | null {
74
+ const sections: DependencySection[] = ["peerDependencies", "devDependencies", "dependencies"];
75
+ return sections.find((section) => pkg[section]?.[SDK_PKG] !== undefined) ?? null;
76
+ }
77
+
78
+ /**
79
+ * Standalone project: `bun add` the SDK at the requested version (default
80
+ * latest) into the section that already declares it. Bun installs the SDK's
81
+ * required peers alongside, so @glasshome/ui follows without a second step.
82
+ */
83
+ function bumpSdkDependency(cwd: string, section: DependencySection, target: string): boolean {
84
+ const s = spinner();
85
+ s.start(`Installing ${SDK_PKG}@${target}...`);
86
+ const proc = Bun.spawnSync(["bun", "add", ...SECTION_FLAG[section], `${SDK_PKG}@${target}`], {
87
+ cwd,
88
+ });
89
+ if (proc.exitCode !== 0) {
90
+ s.stop("Install failed");
91
+ log.error(proc.stderr.toString());
92
+ return false;
93
+ }
94
+ s.stop(`Installed ${SDK_PKG}@${target}`);
95
+ return true;
96
+ }
97
+
98
+ export interface UpgradeOptions {
99
+ /** Version or dist-tag to move to in a standalone project. Default "latest". */
100
+ to?: string;
101
+ }
102
+
103
+ export async function runUpgrade(cwd: string, options: UpgradeOptions = {}): Promise<void> {
62
104
  const pkgPath = resolve(cwd, "package.json");
63
105
  if (!existsSync(pkgPath)) {
64
106
  log.error("No package.json found in current directory.");
65
107
  process.exit(1);
66
108
  }
67
109
 
68
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
69
- const currentVersion =
70
- pkg.peerDependencies?.["@glasshome/widget-sdk"] ??
71
- pkg.devDependencies?.["@glasshome/widget-sdk"] ??
72
- pkg.dependencies?.["@glasshome/widget-sdk"];
110
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as PackageManifest;
111
+ const section = sdkDependencySection(pkg);
112
+ const currentVersion = section ? pkg[section]?.[SDK_PKG] : undefined;
73
113
 
74
- if (!currentVersion) {
114
+ if (!section || !currentVersion) {
75
115
  log.error("@glasshome/widget-sdk is not listed in dependencies or peerDependencies.");
76
116
  process.exit(1);
77
117
  }
@@ -125,27 +165,21 @@ export async function runUpgrade(cwd: string): Promise<void> {
125
165
  return;
126
166
  }
127
167
 
128
- // Standalone project. Bumping the dependency is still the author's call, but
129
- // syncing the manifests to whatever is installed is not: a manifest range
130
- // that excludes the installed SDK fails validation, and this is the command
131
- // that error tells people to run.
168
+ // Standalone project: bump the dependency, then sync every manifest to what
169
+ // is now installed. A manifest range that excludes the installed SDK fails
170
+ // validation, and this is the command that error tells people to run.
171
+ if (!bumpSdkDependency(cwd, section, options.to ?? "latest")) process.exit(1);
172
+
132
173
  const installed = getInstalledSdkVersion(cwd);
133
- if (installed) {
134
- syncManifestSdkVersions(cwd, installed);
135
- const valid = await runValidate(cwd);
136
- if (!valid) log.warn(`Synced manifests to ${installed}, but validation found problems above.`);
137
- } else {
174
+ if (!installed) {
138
175
  log.warn("No @glasshome/widget-sdk installed here, so manifests were left alone.");
176
+ return;
177
+ }
178
+ syncManifestSdkVersions(cwd, installed);
179
+ const valid = await runValidate(cwd);
180
+ if (valid) {
181
+ log.success(`Upgraded to @glasshome/widget-sdk@${installed}`);
182
+ } else {
183
+ log.warn(`Upgraded to ${installed}, but validation found problems above.`);
139
184
  }
140
-
141
- log.info("To move to a different SDK version:");
142
- note(
143
- [
144
- "1. Bump @glasshome/widget-sdk in package.json peerDependencies",
145
- "2. bun install",
146
- "3. bun widget upgrade (sync manifest files)",
147
- "4. bun widget validate (check compatibility)",
148
- ].join("\n"),
149
- "To upgrade",
150
- );
151
185
  }
@@ -1,20 +1,24 @@
1
1
  /**
2
- * Run `fn` with stdout writes swallowed. `buildWidgets()` (SDK) console.logs a
2
+ * Run `fn` with console logging swallowed. `buildWidgets()` (SDK) logs a
3
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.
4
+ * spinner that line lands on the spinner's line and garbles it.
5
+ *
6
+ * Only the console methods are silenced, never `process.stdout.write`: a clack
7
+ * spinner renders its frames through that write, so overriding it left every
8
+ * caller's spinner frozen for the whole build. Bun's console writes to fd 1
9
+ * natively and bypasses a `process.stdout.write` override anyway, so the
10
+ * console patch is the one that ever did the work. Stderr is left alone so real
11
+ * warnings and errors still surface.
6
12
  */
7
13
  export async function withQuietStdout<T>(fn: () => Promise<T>): Promise<T> {
8
- const originalWrite = process.stdout.write.bind(process.stdout);
9
14
  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;
15
+ const originalInfo = console.info;
13
16
  console.log = () => {};
17
+ console.info = () => {};
14
18
  try {
15
19
  return await fn();
16
20
  } finally {
17
- process.stdout.write = originalWrite;
18
21
  console.log = originalLog;
22
+ console.info = originalInfo;
19
23
  }
20
24
  }