@glasshome/widget-cli 0.14.3 → 0.14.4
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/package.json +42 -38
- package/src/commands/connect.ts +5 -9
- package/src/commands/create.ts +1 -2
- package/src/commands/info.ts +3 -1
- package/src/commands/preview.ts +18 -6
- package/src/commands/publish.ts +7 -5
- package/src/commands/validate.ts +5 -1
- package/src/migrate/config-codemod.test.ts +3 -1
- package/src/preview/constraints.ts +8 -10
- package/src/preview/sweep.ts +9 -2
- package/src/utils/auth.test.ts +3 -1
- package/src/utils/auth.ts +1 -3
- package/src/utils/hub-api.ts +1 -1
- package/src/utils/typecheck.test.ts +13 -10
- package/src/utils/version.ts +1 -4
package/package.json
CHANGED
|
@@ -1,29 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glasshome/widget-cli",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.4",
|
|
4
4
|
"description": "CLI for creating and managing GlassHome widgets",
|
|
5
|
-
"
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cli",
|
|
7
|
+
"dashboard",
|
|
8
|
+
"glasshome",
|
|
9
|
+
"home-assistant",
|
|
10
|
+
"scaffold",
|
|
11
|
+
"smart-home",
|
|
12
|
+
"widget"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/glasshome/widget-cli#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/glasshome/widget-cli/issues"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": "GlassHome Labs",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/glasshome/widget-cli.git"
|
|
23
|
+
},
|
|
6
24
|
"bin": {
|
|
7
25
|
"glasshome-widget": "bin/cli.ts"
|
|
8
26
|
},
|
|
9
|
-
"scripts": {
|
|
10
|
-
"cli": "bun bin/cli.ts",
|
|
11
|
-
"test": "bun test",
|
|
12
|
-
"typecheck": "tsc --noEmit",
|
|
13
|
-
"check:dead": "fallow dead-code --fail-on-issues"
|
|
14
|
-
},
|
|
15
27
|
"files": [
|
|
16
28
|
"bin",
|
|
17
29
|
"src",
|
|
18
30
|
"template"
|
|
19
31
|
],
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
"
|
|
32
|
+
"type": "module",
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"cli": "bun bin/cli.ts",
|
|
38
|
+
"test": "bun test",
|
|
39
|
+
"typecheck": "tsc --noEmit",
|
|
40
|
+
"check:dead": "fallow dead-code --fail-on-issues",
|
|
41
|
+
"lint": "oxlint --type-aware . && oxfmt --check ."
|
|
27
42
|
},
|
|
28
43
|
"dependencies": {
|
|
29
44
|
"@clack/prompts": "^1.6.0",
|
|
@@ -43,6 +58,17 @@
|
|
|
43
58
|
"ts-morph": "^28.0.0",
|
|
44
59
|
"vite-plugin-solid": "^2.11.0"
|
|
45
60
|
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@iconify/types": "^2.0.0",
|
|
63
|
+
"@types/bun": "^1.4.0",
|
|
64
|
+
"@types/semver": "^7.0.0",
|
|
65
|
+
"fallow": "3.17.0",
|
|
66
|
+
"oxfmt": "0.65.0",
|
|
67
|
+
"oxlint": "1.81.0",
|
|
68
|
+
"oxlint-tsgolint": "7.0.2001",
|
|
69
|
+
"playwright": "^1.40.0",
|
|
70
|
+
"typescript": "^7.0.2"
|
|
71
|
+
},
|
|
46
72
|
"peerDependencies": {
|
|
47
73
|
"playwright": "^1.40.0",
|
|
48
74
|
"vite": "^7.3.1"
|
|
@@ -52,29 +78,7 @@
|
|
|
52
78
|
"optional": true
|
|
53
79
|
}
|
|
54
80
|
},
|
|
55
|
-
"repository": {
|
|
56
|
-
"type": "git",
|
|
57
|
-
"url": "https://github.com/glasshome/widget-cli.git"
|
|
58
|
-
},
|
|
59
|
-
"keywords": [
|
|
60
|
-
"glasshome",
|
|
61
|
-
"widget",
|
|
62
|
-
"cli",
|
|
63
|
-
"scaffold",
|
|
64
|
-
"home-assistant",
|
|
65
|
-
"dashboard",
|
|
66
|
-
"smart-home"
|
|
67
|
-
],
|
|
68
|
-
"homepage": "https://github.com/glasshome/widget-cli#readme",
|
|
69
|
-
"bugs": {
|
|
70
|
-
"url": "https://github.com/glasshome/widget-cli/issues"
|
|
71
|
-
},
|
|
72
|
-
"publishConfig": {
|
|
73
|
-
"access": "public"
|
|
74
|
-
},
|
|
75
81
|
"engines": {
|
|
76
82
|
"bun": ">=1.0.0"
|
|
77
|
-
}
|
|
78
|
-
"author": "GlassHome Labs",
|
|
79
|
-
"license": "MIT"
|
|
83
|
+
}
|
|
80
84
|
}
|
package/src/commands/connect.ts
CHANGED
|
@@ -201,9 +201,7 @@ async function authorizeThisDevice(api: string, host: string, s: Spinner): Promi
|
|
|
201
201
|
s.stop("Authorization code ready");
|
|
202
202
|
log.info(`Open in browser: ${grant.verificationUriComplete}`);
|
|
203
203
|
log.info(`Device code: ${grant.userCode}`);
|
|
204
|
-
await import("open")
|
|
205
|
-
.then((m) => m.default(grant.verificationUriComplete))
|
|
206
|
-
.catch(() => {});
|
|
204
|
+
await import("open").then((m) => m.default(grant.verificationUriComplete)).catch(() => {});
|
|
207
205
|
|
|
208
206
|
s.start("Waiting for authorization (approve in your browser)...");
|
|
209
207
|
const outcome = await pollDeviceToken(api, grant);
|
|
@@ -217,11 +215,7 @@ async function authorizeThisDevice(api: string, host: string, s: Spinner): Promi
|
|
|
217
215
|
return outcome.token;
|
|
218
216
|
}
|
|
219
217
|
|
|
220
|
-
async function obtainToken(
|
|
221
|
-
api: string,
|
|
222
|
-
opts: { reAuth?: boolean },
|
|
223
|
-
s: Spinner,
|
|
224
|
-
): Promise<string> {
|
|
218
|
+
async function obtainToken(api: string, opts: { reAuth?: boolean }, s: Spinner): Promise<string> {
|
|
225
219
|
const host = extractHost(api);
|
|
226
220
|
if (opts.reAuth) {
|
|
227
221
|
clearHostToken(host);
|
|
@@ -410,7 +404,9 @@ export async function runConnect(
|
|
|
410
404
|
const api = apiUrl.replace(/\/$/, "");
|
|
411
405
|
const token = await obtainToken(api, opts, s);
|
|
412
406
|
if (!token) {
|
|
413
|
-
log.warn(
|
|
407
|
+
log.warn(
|
|
408
|
+
"Authentication failed, widgets won't be connected. Log in at the dashboard first, then restart.",
|
|
409
|
+
);
|
|
414
410
|
return;
|
|
415
411
|
}
|
|
416
412
|
|
package/src/commands/create.ts
CHANGED
|
@@ -115,8 +115,7 @@ export async function runCreate() {
|
|
|
115
115
|
rmSync(oldSrcIndex);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
const projectDesc =
|
|
119
|
-
projectDescription || `A GlassHome widget project: ${projectName}`;
|
|
118
|
+
const projectDesc = projectDescription || `A GlassHome widget project: ${projectName}`;
|
|
120
119
|
|
|
121
120
|
// Replace project-level placeholders in package.json. Pin the CLI to the
|
|
122
121
|
// version that scaffolded the project (caret) so `bun widget` resolves the
|
package/src/commands/info.ts
CHANGED
|
@@ -62,7 +62,9 @@ export async function runInfo(cwd: string, widgetName?: string): Promise<void> {
|
|
|
62
62
|
|
|
63
63
|
note(lines.join("\n"), `${manifest.name} ${color.dim(`(${name})`)}`);
|
|
64
64
|
} catch (err) {
|
|
65
|
-
log.warn(
|
|
65
|
+
log.warn(
|
|
66
|
+
`${name}: could not read manifest: ${err instanceof Error ? err.message : String(err)}`,
|
|
67
|
+
);
|
|
66
68
|
}
|
|
67
69
|
}
|
|
68
70
|
|
package/src/commands/preview.ts
CHANGED
|
@@ -18,7 +18,9 @@ async function requirePlaywright(): Promise<void> {
|
|
|
18
18
|
await import("playwright");
|
|
19
19
|
} catch {
|
|
20
20
|
log.error("Preview needs Playwright + a Chromium build, which are not installed.");
|
|
21
|
-
log.info(
|
|
21
|
+
log.info(
|
|
22
|
+
`Install them with:\n ${color.bold("bun add -d playwright && bunx playwright install chromium")}`,
|
|
23
|
+
);
|
|
22
24
|
process.exit(1);
|
|
23
25
|
}
|
|
24
26
|
}
|
|
@@ -125,7 +127,10 @@ function parseSizes(value: string | boolean | undefined): Box[] {
|
|
|
125
127
|
.split(",")
|
|
126
128
|
.map((s) => {
|
|
127
129
|
const m = s.trim().match(/^(\d+)x(\d+)$/);
|
|
128
|
-
if (!m)
|
|
130
|
+
if (!m)
|
|
131
|
+
fail(
|
|
132
|
+
`--sizes takes grid, or WIDTHxHEIGHT pairs in pixels like 150x156,340x242 (got "${s}")`,
|
|
133
|
+
);
|
|
129
134
|
return { width: Number(m[1]), height: Number(m[2]) };
|
|
130
135
|
});
|
|
131
136
|
}
|
|
@@ -133,7 +138,8 @@ function parseSizes(value: string | boolean | undefined): Box[] {
|
|
|
133
138
|
function parseThemes(value: string | undefined): Theme[] {
|
|
134
139
|
if (!value) return ["light", "dark"];
|
|
135
140
|
const themes = value.split(",");
|
|
136
|
-
for (const t of themes)
|
|
141
|
+
for (const t of themes)
|
|
142
|
+
if (t !== "light" && t !== "dark") fail(`--theme is light, dark or both (got "${t}")`);
|
|
137
143
|
return themes as Theme[];
|
|
138
144
|
}
|
|
139
145
|
|
|
@@ -151,12 +157,18 @@ function parseConfig(value: string | undefined): Record<string, unknown> | undef
|
|
|
151
157
|
* overridden, into `<project>/preview/sweep/`, plus a contact sheet per example
|
|
152
158
|
* and theme when several sizes are rendered.
|
|
153
159
|
*/
|
|
154
|
-
export async function runSweepPreview(
|
|
160
|
+
export async function runSweepPreview(
|
|
161
|
+
cwd: string,
|
|
162
|
+
names: string[],
|
|
163
|
+
flags: SweepFlags,
|
|
164
|
+
): Promise<void> {
|
|
155
165
|
await requirePlaywright();
|
|
156
166
|
const at = flags.at ? new Date(flags.at) : undefined;
|
|
157
|
-
if (at && Number.isNaN(at.getTime()))
|
|
167
|
+
if (at && Number.isNaN(at.getTime()))
|
|
168
|
+
fail(`--at takes an ISO time like 2026-06-15T21:00:00 (got "${flags.at}")`);
|
|
158
169
|
const examples = flags.example ? flags.example.split(",").map(Number) : [];
|
|
159
|
-
if (examples.some((n) => !Number.isInteger(n) || n < 0))
|
|
170
|
+
if (examples.some((n) => !Number.isInteger(n) || n < 0))
|
|
171
|
+
fail("--example takes example numbers, starting at 0");
|
|
160
172
|
|
|
161
173
|
const summary = await withQuietStdout(() =>
|
|
162
174
|
runSweep({
|
package/src/commands/publish.ts
CHANGED
|
@@ -75,7 +75,9 @@ export async function runPublish(
|
|
|
75
75
|
if (options?.scope) {
|
|
76
76
|
const match = scopes.find((s) => s.name === options.scope);
|
|
77
77
|
if (!match) {
|
|
78
|
-
log.error(
|
|
78
|
+
log.error(
|
|
79
|
+
`Scope @${options.scope} not found. Available: ${scopes.map((s) => s.name).join(", ")}`,
|
|
80
|
+
);
|
|
79
81
|
process.exit(1);
|
|
80
82
|
}
|
|
81
83
|
scope = match.name;
|
|
@@ -145,7 +147,9 @@ export async function runPublish(
|
|
|
145
147
|
let bump: string;
|
|
146
148
|
if (options?.bump) {
|
|
147
149
|
bump = options.bump;
|
|
148
|
-
log.info(
|
|
150
|
+
log.info(
|
|
151
|
+
`Version: ${currentVersion} → ${bump === "keep" ? currentVersion : (semver.inc(currentVersion, bump as semver.ReleaseType) ?? currentVersion)}`,
|
|
152
|
+
);
|
|
149
153
|
} else {
|
|
150
154
|
const choice = await select({
|
|
151
155
|
message: `${manifest.name} version: ${currentVersion}. Bump?`,
|
|
@@ -246,9 +250,7 @@ export async function runPublish(
|
|
|
246
250
|
version,
|
|
247
251
|
bundleSize: bundleBuffer.byteLength,
|
|
248
252
|
sha256Hash,
|
|
249
|
-
...(cssBuffer && cssSha256Hash
|
|
250
|
-
? { cssSize: cssBuffer.byteLength, cssSha256Hash }
|
|
251
|
-
: {}),
|
|
253
|
+
...(cssBuffer && cssSha256Hash ? { cssSize: cssBuffer.byteLength, cssSha256Hash } : {}),
|
|
252
254
|
manifestJson: JSON.stringify(builtManifest),
|
|
253
255
|
});
|
|
254
256
|
} catch (err) {
|
package/src/commands/validate.ts
CHANGED
|
@@ -41,7 +41,11 @@ function validateManifest(manifest: WidgetManifest, installedSdk: string | null)
|
|
|
41
41
|
if (!schemaResult.success) {
|
|
42
42
|
errors.push(`Manifest schema: ${formatSchemaError(schemaResult.error)}`);
|
|
43
43
|
const capabilities = (manifest as { capabilities?: unknown }).capabilities;
|
|
44
|
-
if (
|
|
44
|
+
if (
|
|
45
|
+
manifest.sdkVersion &&
|
|
46
|
+
requiresCapabilities(manifest.sdkVersion) &&
|
|
47
|
+
capabilities === undefined
|
|
48
|
+
) {
|
|
45
49
|
errors.push(
|
|
46
50
|
`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`,
|
|
47
51
|
);
|
|
@@ -48,7 +48,9 @@ describe("migrateConfigSource, field kinds", () => {
|
|
|
48
48
|
expect(r.warnings).toEqual([]);
|
|
49
49
|
expect(r.code).toContain("analogOptions: field.group({");
|
|
50
50
|
expect(r.code).toContain('border: field.toggle({ title: "Show Border", default: false }),');
|
|
51
|
-
expect(r.code).toContain(
|
|
51
|
+
expect(r.code).toContain(
|
|
52
|
+
'ticks: field.choice(["none", "hour"], { title: "Tick Marks", default: "hour" }),',
|
|
53
|
+
);
|
|
52
54
|
expect(r.code).toContain('}, { title: "Analog Options" }),');
|
|
53
55
|
});
|
|
54
56
|
|
|
@@ -40,9 +40,7 @@ export interface EgressLock {
|
|
|
40
40
|
* closer analogue of the container egress allowlist the worker will actually
|
|
41
41
|
* rely on.
|
|
42
42
|
*/
|
|
43
|
-
const NO_EGRESS_ARGS = [
|
|
44
|
-
"--host-resolver-rules=MAP * ~NOTFOUND, EXCLUDE localhost",
|
|
45
|
-
];
|
|
43
|
+
const NO_EGRESS_ARGS = ["--host-resolver-rules=MAP * ~NOTFOUND, EXCLUDE localhost"];
|
|
46
44
|
|
|
47
45
|
/** `CHROMIUM_PATH` points at a system Chromium where Playwright's own build is absent or cannot run. */
|
|
48
46
|
const launchLocked = () =>
|
|
@@ -163,12 +161,10 @@ const CONTEXT_OPTIONS = { deviceScaleFactor: 2, reducedMotion: "reduce" } as con
|
|
|
163
161
|
async function hardClose(browser: Browser): Promise<void> {
|
|
164
162
|
// `process()` exists at runtime on a launched Chromium but is absent from the
|
|
165
163
|
// base Browser type; narrow it here rather than at the call site.
|
|
166
|
-
const proc = (
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
new Promise((r) => setTimeout(r, 3_000)),
|
|
171
|
-
]);
|
|
164
|
+
const proc = (
|
|
165
|
+
browser as Browser & { process?: () => { kill: (s: string) => void } | null }
|
|
166
|
+
).process?.();
|
|
167
|
+
await Promise.race([browser.close().catch(() => {}), new Promise((r) => setTimeout(r, 3_000))]);
|
|
172
168
|
proc?.kill("SIGKILL");
|
|
173
169
|
}
|
|
174
170
|
|
|
@@ -230,7 +226,9 @@ export async function withSharedBrowser<T>(fn: (browser: SharedBrowser) => Promi
|
|
|
230
226
|
* The worker must not reuse a process across widgets — one widget's residue
|
|
231
227
|
* (globals, caches, service workers) must never reach another's shot.
|
|
232
228
|
*/
|
|
233
|
-
export async function withFreshBrowser<T>(
|
|
229
|
+
export async function withFreshBrowser<T>(
|
|
230
|
+
fn: (page: Page, browser: Browser) => Promise<T>,
|
|
231
|
+
): Promise<T> {
|
|
234
232
|
// On NixOS the Playwright-downloaded Chromium can't find its system libs;
|
|
235
233
|
// point nix-ld at them so the bundled browser launches unmodified.
|
|
236
234
|
applyNixLdOnce();
|
package/src/preview/sweep.ts
CHANGED
|
@@ -135,7 +135,9 @@ export async function runSweep(opts: SweepOptions): Promise<SweepSummary> {
|
|
|
135
135
|
state: "attached",
|
|
136
136
|
timeout: 30_000,
|
|
137
137
|
});
|
|
138
|
-
examples = JSON.parse(
|
|
138
|
+
examples = JSON.parse(
|
|
139
|
+
(await probe.getAttribute("html", "data-harness-examples")) ?? "[]",
|
|
140
|
+
);
|
|
139
141
|
bounds = JSON.parse((await probe.getAttribute("html", "data-harness-bounds")) ?? "null");
|
|
140
142
|
} finally {
|
|
141
143
|
await probe.context().close();
|
|
@@ -187,7 +189,12 @@ export async function runSweep(opts: SweepOptions): Promise<SweepSummary> {
|
|
|
187
189
|
if (shot.length > 1) {
|
|
188
190
|
const page = await browser.newPage();
|
|
189
191
|
try {
|
|
190
|
-
await writeSheet(
|
|
192
|
+
await writeSheet(
|
|
193
|
+
page,
|
|
194
|
+
theme,
|
|
195
|
+
shot,
|
|
196
|
+
resolve(outDir, `${widget}-${label}-${theme}.png`),
|
|
197
|
+
);
|
|
191
198
|
sheets++;
|
|
192
199
|
} finally {
|
|
193
200
|
await page
|
package/src/utils/auth.test.ts
CHANGED
|
@@ -122,7 +122,9 @@ describe("deviceAuthorize", () => {
|
|
|
122
122
|
|
|
123
123
|
test("throws when the server answers without a device code", async () => {
|
|
124
124
|
const outcome = deviceAuthorize(API, () =>
|
|
125
|
-
Promise.resolve(
|
|
125
|
+
Promise.resolve(
|
|
126
|
+
json(200, { user_code: "A", verification_uri: `${API}/device`, expires_in: 900 }),
|
|
127
|
+
),
|
|
126
128
|
);
|
|
127
129
|
await expect(outcome).rejects.toThrow("no device code");
|
|
128
130
|
});
|
package/src/utils/auth.ts
CHANGED
|
@@ -35,9 +35,7 @@ function ensureDir(): void {
|
|
|
35
35
|
function readAuthFile(): StoredAuthFile {
|
|
36
36
|
if (!existsSync(AUTH_FILE)) return { hosts: {} };
|
|
37
37
|
try {
|
|
38
|
-
const raw = JSON.parse(readFileSync(AUTH_FILE, "utf-8")) as
|
|
39
|
-
| StoredAuthFile
|
|
40
|
-
| LegacyStoredAuth;
|
|
38
|
+
const raw = JSON.parse(readFileSync(AUTH_FILE, "utf-8")) as StoredAuthFile | LegacyStoredAuth;
|
|
41
39
|
// Migrate legacy format (hub-only, no hosts key)
|
|
42
40
|
if ("accessToken" in raw) {
|
|
43
41
|
return { hub: raw as HubAuth, hosts: {} };
|
package/src/utils/hub-api.ts
CHANGED
|
@@ -115,7 +115,7 @@ export async function confirmPublish(
|
|
|
115
115
|
export async function uploadToR2(
|
|
116
116
|
uploadUrl: string,
|
|
117
117
|
bundleBuffer: Uint8Array,
|
|
118
|
-
contentType
|
|
118
|
+
contentType = "application/javascript",
|
|
119
119
|
): Promise<void> {
|
|
120
120
|
const res = await fetch(uploadUrl, {
|
|
121
121
|
method: "PUT",
|
|
@@ -114,15 +114,18 @@ describe("runTypecheck", () => {
|
|
|
114
114
|
expect(result.reason).toContain("not installed");
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
-
test.skipIf(noTypescript)(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
117
|
+
test.skipIf(noTypescript)(
|
|
118
|
+
"uses an ancestor's typescript, as hoisted and workspace installs give",
|
|
119
|
+
() => {
|
|
120
|
+
// Requiring a copy inside the project itself would skip every monorepo.
|
|
121
|
+
const parent = project({ typescript: true });
|
|
122
|
+
const child = join(parent, "nested");
|
|
123
|
+
mkdirSync(join(child, "src"), { recursive: true });
|
|
124
|
+
cpSync(join(parent, "package.json"), join(child, "package.json"));
|
|
125
|
+
cpSync(join(parent, "tsconfig.json"), join(child, "tsconfig.json"));
|
|
126
|
+
writeFileSync(join(child, "src", "index.ts"), `export const n: number = "nope";\n`);
|
|
125
127
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
+
expect(runTypecheck(child).status).toBe("failed");
|
|
129
|
+
},
|
|
130
|
+
);
|
|
128
131
|
});
|
package/src/utils/version.ts
CHANGED
|
@@ -107,10 +107,7 @@ async function getLatestVersions(): Promise<{ cli: string | null; sdk: string |
|
|
|
107
107
|
if (cache && now - cache.lastCheck < UPDATE_CHECK_INTERVAL_MS) {
|
|
108
108
|
return { cli: cache.cli, sdk: cache.sdk };
|
|
109
109
|
}
|
|
110
|
-
const [cli, sdk] = await Promise.all([
|
|
111
|
-
fetchLatestVersion(CLI_PKG),
|
|
112
|
-
fetchLatestVersion(SDK_PKG),
|
|
113
|
-
]);
|
|
110
|
+
const [cli, sdk] = await Promise.all([fetchLatestVersion(CLI_PKG), fetchLatestVersion(SDK_PKG)]);
|
|
114
111
|
const merged = { cli: cli ?? cache?.cli ?? null, sdk: sdk ?? cache?.sdk ?? null };
|
|
115
112
|
writeUpdateCache({ lastCheck: now, ...merged });
|
|
116
113
|
return merged;
|