@glasshome/widget-cli 0.7.0 → 0.8.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/bin/cli.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  import { existsSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
4
  import { intro, log, outro } from "@clack/prompts";
5
+ import { notifyCliUpdate, notifySdkUpdate } from "../src/utils/version";
5
6
 
6
7
  function resolveWidgetDir(): string {
7
8
  if (explicitDir) {
@@ -94,6 +95,9 @@ if (effectiveCommand === "help") {
94
95
 
95
96
  intro(`glasshome-widget ${effectiveCommand}`);
96
97
 
98
+ // Best-effort, throttled nudge when a newer CLI is on npm. Never blocks.
99
+ await notifyCliUpdate();
100
+
97
101
  switch (effectiveCommand) {
98
102
  case "create": {
99
103
  const { runCreate } = await import("../src/commands/create");
@@ -110,8 +114,10 @@ switch (effectiveCommand) {
110
114
  }
111
115
 
112
116
  case "build": {
117
+ const widgetDir = resolveWidgetDir();
118
+ await notifySdkUpdate(widgetDir);
113
119
  const { runBuild } = await import("../src/commands/build");
114
- await runBuild(resolveWidgetDir());
120
+ await runBuild(widgetDir);
115
121
  outro("Done");
116
122
  break;
117
123
  }
@@ -130,8 +136,10 @@ switch (effectiveCommand) {
130
136
 
131
137
  case "validate": {
132
138
  const widgetName = args[0];
139
+ const widgetDir = resolveWidgetDir();
140
+ await notifySdkUpdate(widgetDir);
133
141
  const { runValidate } = await import("../src/commands/validate");
134
- const passed = await runValidate(resolveWidgetDir(), widgetName);
142
+ const passed = await runValidate(widgetDir, widgetName);
135
143
  outro(passed ? "All checks passed" : "Validation failed");
136
144
  process.exit(passed ? 0 : 1);
137
145
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glasshome/widget-cli",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "CLI for creating and managing GlassHome widgets",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,7 +22,7 @@
22
22
  "dependencies": {
23
23
  "@clack/prompts": "^1.6.0",
24
24
  "@glasshome/widget-contract": "0.1.1",
25
- "@glasshome/widget-sdk": "1.4.0",
25
+ "@glasshome/widget-sdk": "1.4.1",
26
26
  "open": "^11.0.0",
27
27
  "picocolors": "^1.1.1",
28
28
  "semver": "^7.6.0",
@@ -3,7 +3,7 @@ import { createServer, type IncomingMessage, type ServerResponse } from "node:ht
3
3
  import { log, spinner } from "@clack/prompts";
4
4
  import open from "open";
5
5
  import { getHubUrl, storeToken } from "../utils/auth";
6
- import { enforceCliVersion, notifyCliUpdate } from "../utils/version";
6
+ import { enforceCliVersion } from "../utils/version";
7
7
 
8
8
  // Public client: PKCE authenticates it, no client secret. The redirect is an
9
9
  // IP-loopback URI (RFC 8252) registered on the hub.
@@ -23,7 +23,6 @@ export async function runLogin(hubUrl?: string): Promise<void> {
23
23
  // Refuse to start the OAuth dance if the hub no longer supports this CLI;
24
24
  // otherwise a stale client fails mid-flow with an opaque redirect error.
25
25
  await enforceCliVersion(hub);
26
- await notifyCliUpdate();
27
26
 
28
27
  // Generate PKCE parameters
29
28
  const codeVerifier = base64url(randomBytes(32));
@@ -149,10 +148,19 @@ export async function runLogin(hubUrl?: string): Promise<void> {
149
148
  name?: string;
150
149
  email?: string;
151
150
  sub?: string;
151
+ username?: string;
152
152
  };
153
153
  const displayName = userInfo.name || userInfo.email || userInfo.sub || "unknown";
154
- const scopeBase = userInfo.name || userInfo.email?.split("@")[0] || userInfo.sub || "unknown";
155
- const scope = scopeBase.toLowerCase().replace(/[^a-z0-9-]/g, "-");
154
+ // The hub assigns each account a stable, unique personal scope (its username)
155
+ // and is the source of truth; publish always re-fetches the live scope list
156
+ // from /api/widgets/scopes. Prefer that username here so the stored value and
157
+ // the login message match what the user actually publishes under. Fall back to
158
+ // local derivation only for older hubs that don't return a username yet.
159
+ const scope =
160
+ userInfo.username ||
161
+ (userInfo.name || userInfo.email?.split("@")[0] || userInfo.sub || "unknown")
162
+ .toLowerCase()
163
+ .replace(/[^a-z0-9-]/g, "-");
156
164
 
157
165
  // Store tokens
158
166
  storeToken({
@@ -122,8 +122,7 @@ export async function runUpgrade(cwd: string): Promise<void> {
122
122
  log.warn(`Synced to ${sdkVersion} but validation has warnings/errors. Check above.`);
123
123
  }
124
124
  } else {
125
- // Standalone mode: can't fetch from npm (private package), guide user
126
- log.info("@glasshome/widget-sdk is a workspace package and is not published to npm.");
125
+ log.info("Not inside the widget workspace. Upgrade @glasshome/widget-sdk manually:");
127
126
  note(
128
127
  [
129
128
  "1. Bump @glasshome/widget-sdk in package.json peerDependencies",
@@ -6,6 +6,7 @@ import {
6
6
  publishManifestSchema,
7
7
  requiresCapabilities,
8
8
  } from "@glasshome/widget-contract";
9
+ import semver from "semver";
9
10
  import {
10
11
  discoverWidgets,
11
12
  formatBytes,
@@ -14,6 +15,7 @@ import {
14
15
  readRegistry,
15
16
  type WidgetManifest,
16
17
  } from "../utils/manifest";
18
+ import { getProjectSdkVersion } from "../utils/version";
17
19
 
18
20
  interface ValidationResult {
19
21
  passed: boolean;
@@ -155,6 +157,9 @@ export async function runValidate(
155
157
 
156
158
  const failed: string[] = [];
157
159
 
160
+ // Manifest sdkVersion range must admit the pinned SDK, else it lies about compatibility.
161
+ const projectSdk = getProjectSdkVersion(cwd);
162
+
158
163
  for (const name of toValidate) {
159
164
  if (!quiet) log.step(name);
160
165
 
@@ -184,6 +189,19 @@ export async function runValidate(
184
189
  }
185
190
  }
186
191
 
192
+ if (
193
+ !quiet &&
194
+ projectSdk &&
195
+ manifest.sdkVersion &&
196
+ manifest.sdkVersion !== "*" &&
197
+ semver.validRange(manifest.sdkVersion) &&
198
+ !semver.satisfies(projectSdk, manifest.sdkVersion)
199
+ ) {
200
+ log.warn(
201
+ ` - manifest sdkVersion "${manifest.sdkVersion}" excludes the installed SDK ${projectSdk}; run \`bun widget upgrade\``,
202
+ );
203
+ }
204
+
187
205
  // Check bundle exists
188
206
  const bundlePath = resolve(cwd, "dist", `${name}.js`);
189
207
  if (!existsSync(bundlePath)) {
@@ -1,5 +1,6 @@
1
- import { readFileSync } from "node:fs";
2
- import { resolve } from "node:path";
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join, resolve } from "node:path";
3
4
  import { log } from "@clack/prompts";
4
5
  import semver from "semver";
5
6
 
@@ -44,23 +45,131 @@ export async function enforceCliVersion(hubUrl: string): Promise<void> {
44
45
  }
45
46
  }
46
47
 
48
+ const UPDATE_CACHE_FILE = join(homedir(), ".glasshome", "update-check.json");
49
+ // Hit npm at most once per day; every other run reads the cached result.
50
+ const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
51
+ // Cap the daily refresh so a slow registry never stalls a command.
52
+ const UPDATE_FETCH_TIMEOUT_MS = 2000;
53
+ const CLI_PKG = "@glasshome/widget-cli";
54
+ const SDK_PKG = "@glasshome/widget-sdk";
55
+
56
+ interface UpdateCache {
57
+ lastCheck: number;
58
+ cli: string | null;
59
+ sdk: string | null;
60
+ }
61
+
62
+ function notifierDisabled(): boolean {
63
+ return Boolean(process.env.GLASSHOME_NO_UPDATE_NOTIFIER || process.env.CI);
64
+ }
65
+
66
+ function readUpdateCache(): UpdateCache | null {
67
+ try {
68
+ const raw = JSON.parse(readFileSync(UPDATE_CACHE_FILE, "utf-8")) as Partial<UpdateCache>;
69
+ if (typeof raw.lastCheck === "number") {
70
+ return {
71
+ lastCheck: raw.lastCheck,
72
+ cli: typeof raw.cli === "string" ? raw.cli : null,
73
+ sdk: typeof raw.sdk === "string" ? raw.sdk : null,
74
+ };
75
+ }
76
+ } catch {}
77
+ return null;
78
+ }
79
+
80
+ function writeUpdateCache(cache: UpdateCache): void {
81
+ try {
82
+ mkdirSync(join(homedir(), ".glasshome"), { recursive: true });
83
+ writeFileSync(UPDATE_CACHE_FILE, JSON.stringify(cache), { mode: 0o600 });
84
+ } catch {}
85
+ }
86
+
87
+ async function fetchLatestVersion(pkg: string): Promise<string | null> {
88
+ try {
89
+ const res = await fetch(`https://registry.npmjs.org/${pkg}/latest`, {
90
+ signal: AbortSignal.timeout(UPDATE_FETCH_TIMEOUT_MS),
91
+ });
92
+ if (!res.ok) return null;
93
+ const data = (await res.json()) as { version?: string };
94
+ return data.version ?? null;
95
+ } catch {
96
+ return null;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Latest published cli + sdk versions, refreshing the shared daily cache when
102
+ * stale. Warm runs stay offline; a failed refresh keeps the last known value.
103
+ */
104
+ async function getLatestVersions(): Promise<{ cli: string | null; sdk: string | null }> {
105
+ const cache = readUpdateCache();
106
+ const now = Date.now();
107
+ if (cache && now - cache.lastCheck < UPDATE_CHECK_INTERVAL_MS) {
108
+ return { cli: cache.cli, sdk: cache.sdk };
109
+ }
110
+ const [cli, sdk] = await Promise.all([
111
+ fetchLatestVersion(CLI_PKG),
112
+ fetchLatestVersion(SDK_PKG),
113
+ ]);
114
+ const merged = { cli: cli ?? cache?.cli ?? null, sdk: sdk ?? cache?.sdk ?? null };
115
+ writeUpdateCache({ lastCheck: now, ...merged });
116
+ return merged;
117
+ }
118
+
47
119
  /**
48
- * Best-effort nudge: warn when a newer CLI is published on npm. Never blocks;
49
- * any failure (offline, registry hiccup) is swallowed.
120
+ * Best-effort nudge: warn when a newer CLI is published on npm. Runs on every
121
+ * command but only queries the registry once per day (shared cache in
122
+ * ~/.glasshome/update-check.json) so warm runs stay offline and instant. Never
123
+ * blocks; any failure (offline, registry hiccup) is swallowed. Opt out with
124
+ * GLASSHOME_NO_UPDATE_NOTIFIER or CI.
50
125
  */
51
126
  export async function notifyCliUpdate(): Promise<void> {
127
+ if (notifierDisabled()) return;
128
+
52
129
  const current = getCliVersion();
130
+ const { cli: latest } = await getLatestVersions();
131
+ if (latest && semver.valid(latest) && semver.gt(latest, current)) {
132
+ log.warn(
133
+ `A newer widget CLI is available (${current} → ${latest}). Update: bun add -g ${CLI_PKG}@latest`,
134
+ );
135
+ }
136
+ }
137
+
138
+ /** The @glasshome/widget-sdk version this project declares in its package.json. */
139
+ export function getProjectSdkVersion(cwd: string): string | null {
53
140
  try {
54
- const res = await fetch("https://registry.npmjs.org/@glasshome/widget-cli/latest");
55
- if (!res.ok) return;
56
- const data = (await res.json()) as { version?: string };
57
- const latest = data.version;
58
- if (latest && semver.valid(latest) && semver.gt(latest, current)) {
59
- log.warn(
60
- `A newer widget CLI is available (${current} → ${latest}). Update: bun add -g @glasshome/widget-cli@latest`,
61
- );
62
- }
141
+ const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf-8")) as {
142
+ dependencies?: Record<string, string>;
143
+ devDependencies?: Record<string, string>;
144
+ peerDependencies?: Record<string, string>;
145
+ };
146
+ const range =
147
+ pkg.peerDependencies?.[SDK_PKG] ??
148
+ pkg.devDependencies?.[SDK_PKG] ??
149
+ pkg.dependencies?.[SDK_PKG];
150
+ if (!range) return null;
151
+ const clean = range.replace(/^[\^~]/, "");
152
+ return semver.valid(clean) ? clean : null;
63
153
  } catch {
64
- // ignore
154
+ return null;
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Best-effort nudge: warn when the project's @glasshome/widget-sdk is behind the
160
+ * latest published SDK. Same throttled cache as the CLI check; opt out the same
161
+ * way. No-ops outside a widget project or when the pin isn't a plain version.
162
+ */
163
+ export async function notifySdkUpdate(cwd: string): Promise<void> {
164
+ if (notifierDisabled()) return;
165
+
166
+ const declared = getProjectSdkVersion(cwd);
167
+ if (!declared) return;
168
+
169
+ const { sdk: latest } = await getLatestVersions();
170
+ if (latest && semver.valid(latest) && semver.gt(latest, declared)) {
171
+ log.warn(
172
+ `A newer widget SDK is available (${declared} → ${latest}). Update: bun widget upgrade`,
173
+ );
65
174
  }
66
175
  }