@glasshome/widget-cli 0.7.1 → 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 +10 -2
- package/package.json +1 -1
- package/src/commands/login.ts +12 -4
- package/src/commands/upgrade.ts +1 -2
- package/src/commands/validate.ts +18 -0
- package/src/utils/version.ts +123 -14
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(
|
|
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(
|
|
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
package/src/commands/login.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
155
|
-
|
|
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({
|
package/src/commands/upgrade.ts
CHANGED
|
@@ -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
|
-
|
|
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",
|
package/src/commands/validate.ts
CHANGED
|
@@ -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)) {
|
package/src/utils/version.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
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.
|
|
49
|
-
*
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
}
|