@glasshome/widget-cli 0.8.4 → 0.10.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.
@@ -0,0 +1,49 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+
4
+ /**
5
+ * The Playwright-downloaded Chromium ships a glibc dynamic linker
6
+ * (`/lib64/ld-linux-x86-64.so.2`) and expects system libs (glib, nss, X11, …)
7
+ * that do not exist on NixOS, so it dies with `libglib-2.0.so.0: cannot open
8
+ * shared object file`. NixOS's nix-ld provides the interpreter shim via
9
+ * `NIX_LD`; it just needs a library search path. This harvests that path from
10
+ * the installed nix Chromium's runtime closure — version-independent and
11
+ * offline — so the downloaded browser resolves every lib.
12
+ *
13
+ * Returns the `:`-joined path (existing `NIX_LD_LIBRARY_PATH` prepended), or
14
+ * null when not on NixOS or no nix Chromium is installed (caller should then
15
+ * launch normally).
16
+ */
17
+ export function nixLdLibraryPath(): string | null {
18
+ if (!existsSync("/etc/NIXOS")) return null;
19
+
20
+ let chromePath: string;
21
+ try {
22
+ chromePath = execFileSync("bash", ["-lc", 'readlink -f "$(command -v chromium)"'], {
23
+ encoding: "utf-8",
24
+ }).trim();
25
+ } catch {
26
+ return null;
27
+ }
28
+ if (!chromePath || !existsSync(chromePath)) return null;
29
+
30
+ let closure: string[];
31
+ try {
32
+ closure = execFileSync("nix-store", ["-qR", chromePath], { encoding: "utf-8" })
33
+ .split("\n")
34
+ .filter(Boolean);
35
+ } catch {
36
+ return null;
37
+ }
38
+
39
+ const dirs: string[] = [];
40
+ for (const p of closure) {
41
+ for (const sub of [`${p}/lib`, `${p}/lib64`]) {
42
+ if (existsSync(sub)) dirs.push(sub);
43
+ }
44
+ }
45
+ if (dirs.length === 0) return null;
46
+
47
+ const existing = process.env.NIX_LD_LIBRARY_PATH;
48
+ return existing ? `${existing}:${dirs.join(":")}` : dirs.join(":");
49
+ }
@@ -0,0 +1,22 @@
1
+ import { homedir } from "node:os";
2
+ import { join, resolve, sep } from "node:path";
3
+
4
+ /**
5
+ * Whether a resolved module path is bun's global install cache rather than a
6
+ * real install.
7
+ *
8
+ * With a package absent, bun still resolves its bare name, from
9
+ * `~/.bun/install/cache/typescript@7.0.2@@@1/`. Tools that shell out to what
10
+ * they resolved would then run a version the project never installed.
11
+ *
12
+ * Testing for a `node_modules` segment instead looks equivalent and is not: in
13
+ * a workspace, `@glasshome/widget-sdk` resolves through a symlink to its real
14
+ * source directory, which has no such segment, so that test rejects the very
15
+ * install it should accept and the guard silently does nothing.
16
+ */
17
+ export function isBunGlobalCache(modulePath: string): boolean {
18
+ const cacheDir =
19
+ process.env.BUN_INSTALL_CACHE_DIR ??
20
+ join(process.env.BUN_INSTALL ?? join(homedir(), ".bun"), "install", "cache");
21
+ return resolve(modulePath).startsWith(resolve(cacheDir) + sep);
22
+ }
@@ -0,0 +1,85 @@
1
+ import { afterAll, describe, expect, test } from "bun:test";
2
+ import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { isBunGlobalCache } from "./resolution";
6
+ import { bareVersion, defaultSdkRange, getInstalledSdkVersion } from "./sdk-version";
7
+
8
+ const tmpDir = mkdtempSync(join(tmpdir(), "glasshome-sdk-version-"));
9
+
10
+ afterAll(() => {
11
+ rmSync(tmpDir, { recursive: true, force: true });
12
+ });
13
+
14
+ let fixtureCount = 0;
15
+
16
+ /** A fake installed SDK, either under node_modules or as a workspace symlink
17
+ to a source tree, which is how a monorepo actually resolves it. */
18
+ function project(opts: { version?: string; asWorkspaceLink?: boolean }): string {
19
+ const dir = join(tmpDir, `project-${fixtureCount++}`);
20
+ mkdirSync(dir, { recursive: true });
21
+ writeFileSync(join(dir, "package.json"), JSON.stringify({ name: "fixture" }));
22
+
23
+ if (opts.version) {
24
+ const target = opts.asWorkspaceLink
25
+ ? join(dir, "packages", "widget-sdk")
26
+ : join(dir, "node_modules", "@glasshome", "widget-sdk");
27
+ mkdirSync(target, { recursive: true });
28
+ writeFileSync(
29
+ join(target, "package.json"),
30
+ JSON.stringify({ name: "@glasshome/widget-sdk", version: opts.version }),
31
+ );
32
+ if (opts.asWorkspaceLink) {
33
+ mkdirSync(join(dir, "node_modules", "@glasshome"), { recursive: true });
34
+ symlinkSync(target, join(dir, "node_modules", "@glasshome", "widget-sdk"), "dir");
35
+ }
36
+ }
37
+ return dir;
38
+ }
39
+
40
+ describe("getInstalledSdkVersion", () => {
41
+ test("reads the version from a normal install", () => {
42
+ expect(getInstalledSdkVersion(project({ version: "1.8.1" }))).toBe("1.8.1");
43
+ });
44
+
45
+ test("reads it through a workspace symlink to a source tree", () => {
46
+ // Regression: an earlier version required a `node_modules` segment in the
47
+ // resolved path. A workspace symlink resolves to the SDK's real source
48
+ // directory, which has none, so the check returned null and every guard
49
+ // built on it silently did nothing in the monorepo.
50
+ expect(getInstalledSdkVersion(project({ version: "1.7.0", asWorkspaceLink: true }))).toBe(
51
+ "1.7.0",
52
+ );
53
+ });
54
+
55
+ test("returns null when no SDK is installed", () => {
56
+ // Must not fall back to bun's global cache copy.
57
+ expect(getInstalledSdkVersion(project({}))).toBeNull();
58
+ });
59
+ });
60
+
61
+ describe("defaultSdkRange", () => {
62
+ test("carets the installed version", () => {
63
+ expect(defaultSdkRange(project({ version: "1.8.1" }))).toBe("^1.8.1");
64
+ });
65
+
66
+ test("falls back to the SDK this CLI ships against", () => {
67
+ // No project SDK, so it must still produce a 1.x range rather than nothing:
68
+ // a pre-1.0 range would excuse the widget from declaring capabilities.
69
+ const range = defaultSdkRange(project({}));
70
+ expect(range).toMatch(/^\^\d+\.\d+\.\d+/);
71
+ expect(bareVersion(range as string).startsWith("0.")).toBe(false);
72
+ });
73
+ });
74
+
75
+ describe("isBunGlobalCache", () => {
76
+ test("recognises the cache directory", () => {
77
+ const cache = join(tmpDir, "cache");
78
+ process.env.BUN_INSTALL_CACHE_DIR = cache;
79
+ expect(isBunGlobalCache(join(cache, "typescript@7.0.2@@@1", "package.json"))).toBe(true);
80
+ expect(isBunGlobalCache(join(tmpDir, "node_modules", "typescript", "package.json"))).toBe(
81
+ false,
82
+ );
83
+ delete process.env.BUN_INSTALL_CACHE_DIR;
84
+ });
85
+ });
@@ -0,0 +1,76 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { join, resolve } from "node:path";
4
+ import { isBunGlobalCache } from "./resolution";
5
+
6
+ /**
7
+ * The SDK a widget actually builds against.
8
+ *
9
+ * `@glasshome/widget-sdk` is host-provided (`HOST_PROVIDED_MODULES`), so the
10
+ * build externalizes it and the published bundle runs against whatever SDK the
11
+ * host serves. That is why `manifest.sdkVersion` exists: it is the range the
12
+ * host checks before mounting. It must be derived from, and checked against,
13
+ * the SDK really resolved here — comparing it to the range declared in
14
+ * package.json only ever compares a claim with itself.
15
+ */
16
+
17
+ const SDK_PKG = "@glasshome/widget-sdk";
18
+
19
+ /**
20
+ * Last resort when neither the project nor this CLI can be read. Deliberately a
21
+ * 1.x range: anything below 1.0.0 excuses a widget from declaring capabilities,
22
+ * so a wrong-but-modern default fails loudly instead of silently opting out of
23
+ * the security contract.
24
+ */
25
+ export const FALLBACK_SDK_RANGE = "^1.0.0";
26
+
27
+ /**
28
+ * Version of the installed SDK, or null when none is installed.
29
+ *
30
+ * An ancestor's `node_modules` is fine and expected (hoisted installs put it
31
+ * there), as is a workspace symlink resolving to the SDK's own source tree.
32
+ * Bun's global install cache is not: with nothing installed it still resolves
33
+ * the bare name, and that is not what the build would use.
34
+ */
35
+ export function getInstalledSdkVersion(cwd: string): string | null {
36
+ try {
37
+ const require = createRequire(join(cwd, "package.json"));
38
+ const pkgPath = resolve(require.resolve(`${SDK_PKG}/package.json`));
39
+ if (isBunGlobalCache(pkgPath)) return null;
40
+ const version = JSON.parse(readFileSync(pkgPath, "utf-8")).version;
41
+ return typeof version === "string" ? version : null;
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * The SDK this CLI itself was built against, used when scaffolding a project
49
+ * that has nothing installed yet.
50
+ */
51
+ export function getCliSdkVersion(): string | null {
52
+ try {
53
+ const pkgPath = resolve(import.meta.dir, "../../package.json");
54
+ if (!existsSync(pkgPath)) return null;
55
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
56
+ const range = pkg.dependencies?.[SDK_PKG] ?? pkg.peerDependencies?.[SDK_PKG];
57
+ return typeof range === "string" ? range.replace(/^[\^~]/, "") : null;
58
+ } catch {
59
+ return null;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * The `^x.y.z` range to write when scaffolding: what the project has installed,
65
+ * else what this CLI ships against. A caret, so patch and minor SDK releases do
66
+ * not invalidate every published widget.
67
+ */
68
+ export function defaultSdkRange(cwd?: string): string | null {
69
+ const version = (cwd ? getInstalledSdkVersion(cwd) : null) ?? getCliSdkVersion();
70
+ return version ? `^${version}` : null;
71
+ }
72
+
73
+ /** Strip a leading caret/tilde so a range can be compared as a version. */
74
+ export function bareVersion(range: string): string {
75
+ return range.replace(/^[\^~]/, "");
76
+ }
@@ -0,0 +1,128 @@
1
+ import { afterAll, describe, expect, test } from "bun:test";
2
+ import { cpSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import { tmpdir } from "node:os";
5
+ import { dirname, join, sep } from "node:path";
6
+ import { runTypecheck } from "./typecheck";
7
+
8
+ // Inside the package so fixtures can resolve typescript from its node_modules,
9
+ // which is what a hoisted or workspace install looks like.
10
+ const tmpDir = mkdtempSync(join(import.meta.dir, ".typecheck-test-"));
11
+ // Outside it, for the one case that must find no typescript anywhere above it.
12
+ const isolatedDir = mkdtempSync(join(tmpdir(), "glasshome-typecheck-"));
13
+
14
+ afterAll(() => {
15
+ rmSync(tmpDir, { recursive: true, force: true });
16
+ rmSync(isolatedDir, { recursive: true, force: true });
17
+ });
18
+
19
+ /**
20
+ * A real installed typescript to link into fixtures, or null.
21
+ *
22
+ * Resolved lazily and allowed to be absent: this package does not declare
23
+ * typescript, so in the monorepo it comes from the root's node_modules and in a
24
+ * standalone checkout there may be none at all. A hard resolve at module load
25
+ * would take the whole file down there. The `node_modules` check keeps bun's
26
+ * global-cache fallback out, which would not be a real install.
27
+ */
28
+ function findTypescript(): string | null {
29
+ try {
30
+ const pkg = createRequire(import.meta.url).resolve("typescript/package.json");
31
+ return pkg.split(sep).includes("node_modules") ? dirname(pkg) : null;
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ const typescriptDir = findTypescript();
38
+ const noTypescript = typescriptDir === null;
39
+
40
+ let fixtureCount = 0;
41
+
42
+ interface ProjectOptions {
43
+ source?: string;
44
+ tsconfig?: boolean;
45
+ typescript?: boolean;
46
+ /** Somewhere with no node_modules above it, so nothing can be inherited. */
47
+ isolated?: boolean;
48
+ }
49
+
50
+ function project({
51
+ source,
52
+ tsconfig = true,
53
+ typescript = true,
54
+ isolated = false,
55
+ }: ProjectOptions): string {
56
+ const dir = join(isolated ? isolatedDir : tmpDir, `project-${fixtureCount++}`);
57
+ mkdirSync(join(dir, "src"), { recursive: true });
58
+ writeFileSync(join(dir, "package.json"), JSON.stringify({ name: "fixture", type: "module" }));
59
+ if (tsconfig) {
60
+ writeFileSync(
61
+ join(dir, "tsconfig.json"),
62
+ JSON.stringify({
63
+ compilerOptions: {
64
+ strict: true,
65
+ noEmit: true,
66
+ module: "ESNext",
67
+ target: "ESNext",
68
+ moduleResolution: "bundler",
69
+ // Hermetic: without this tsc auto-loads every @types package it finds
70
+ // walking up to the monorepo root, which is both irrelevant to what
71
+ // is under test and slow enough to blow the test timeout.
72
+ types: [],
73
+ },
74
+ include: ["src"],
75
+ }),
76
+ );
77
+ }
78
+ if (typescript && typescriptDir) {
79
+ mkdirSync(join(dir, "node_modules"), { recursive: true });
80
+ symlinkSync(typescriptDir, join(dir, "node_modules", "typescript"), "dir");
81
+ }
82
+ writeFileSync(join(dir, "src", "index.ts"), source ?? "export const n: number = 1;\n");
83
+ return dir;
84
+ }
85
+
86
+ describe("runTypecheck", () => {
87
+ test.skipIf(noTypescript)("passes a project whose types are sound", () => {
88
+ expect(runTypecheck(project({})).status).toBe("ok");
89
+ });
90
+
91
+ test.skipIf(noTypescript)("fails on a type error and reports the file and message", () => {
92
+ const result = runTypecheck(project({ source: `export const n: number = "nope";\n` }));
93
+ expect(result.status).toBe("failed");
94
+ if (result.status !== "failed") return;
95
+ expect(result.output).toContain("src/index.ts");
96
+ expect(result.output).toContain("TS2322");
97
+ });
98
+
99
+ test("skips a project with no tsconfig rather than failing it", () => {
100
+ // A JS-only or custom-setup project is not broken, it is just not ours to check.
101
+ const result = runTypecheck(project({ tsconfig: false }));
102
+ expect(result.status).toBe("skipped");
103
+ if (result.status !== "skipped") return;
104
+ expect(result.reason).toContain("tsconfig");
105
+ });
106
+
107
+ test("skips when typescript is not installed in the project", () => {
108
+ // Load-bearing: with nothing installed, bun still resolves a bare
109
+ // "typescript" from its global install cache, so without the node_modules
110
+ // check this would silently check against whatever major is cached there.
111
+ const result = runTypecheck(project({ typescript: false, isolated: true }));
112
+ expect(result.status).toBe("skipped");
113
+ if (result.status !== "skipped") return;
114
+ expect(result.reason).toContain("not installed");
115
+ });
116
+
117
+ test.skipIf(noTypescript)("uses an ancestor's typescript, as hoisted and workspace installs give", () => {
118
+ // Requiring a copy inside the project itself would skip every monorepo.
119
+ const parent = project({ typescript: true });
120
+ const child = join(parent, "nested");
121
+ mkdirSync(join(child, "src"), { recursive: true });
122
+ cpSync(join(parent, "package.json"), join(child, "package.json"));
123
+ cpSync(join(parent, "tsconfig.json"), join(child, "tsconfig.json"));
124
+ writeFileSync(join(child, "src", "index.ts"), `export const n: number = "nope";\n`);
125
+
126
+ expect(runTypecheck(child).status).toBe("failed");
127
+ });
128
+ });
@@ -0,0 +1,84 @@
1
+ import { existsSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { log } from "@clack/prompts";
5
+ import { isBunGlobalCache } from "./resolution";
6
+
7
+ /**
8
+ * Typecheck a widget project before building it.
9
+ *
10
+ * Scaffolded projects build with a bare `vite build`, which strips types without
11
+ * checking them, so a widget could bundle and publish with its config type and
12
+ * its `configSchema` disagreeing — including `examples` whose config the widget
13
+ * would never accept. tsc is the only thing that catches that at authoring time.
14
+ *
15
+ * Skips (loudly) rather than failing when the project has no tsconfig or no
16
+ * local typescript: a guard that cannot run has to say so.
17
+ */
18
+
19
+ export type TypecheckResult =
20
+ | { status: "ok" }
21
+ | { status: "failed"; output: string }
22
+ | { status: "skipped"; reason: string };
23
+
24
+ /**
25
+ * An installed typescript, never bun's global cache.
26
+ *
27
+ * Load-bearing: when a project has no typescript installed, bun still resolves
28
+ * the bare name, from `~/.bun/install/cache/typescript@7.0.2@@@1/`, so the CLI
29
+ * would silently check against whatever major happens to be cached there.
30
+ * Resolving to an *ancestor's* `node_modules` is fine and deliberate: hoisted
31
+ * and workspace installs put it there, so requiring a copy inside the project
32
+ * would skip every monorepo.
33
+ */
34
+ function resolveTsc(cwd: string): string | null {
35
+ try {
36
+ const require = createRequire(join(cwd, "package.json"));
37
+ const pkg = resolve(require.resolve("typescript/package.json"));
38
+ if (isBunGlobalCache(pkg)) return null;
39
+ const bin = join(dirname(pkg), "bin", "tsc");
40
+ return existsSync(bin) ? bin : null;
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ export function runTypecheck(cwd: string): TypecheckResult {
47
+ const tsconfig = join(cwd, "tsconfig.json");
48
+ if (!existsSync(tsconfig)) return { status: "skipped", reason: "no tsconfig.json" };
49
+
50
+ const tsc = resolveTsc(cwd);
51
+ if (!tsc) {
52
+ return { status: "skipped", reason: "typescript is not installed in this project" };
53
+ }
54
+
55
+ const proc = Bun.spawnSync([process.execPath, tsc, "--noEmit", "-p", tsconfig], {
56
+ cwd,
57
+ stdout: "pipe",
58
+ stderr: "pipe",
59
+ });
60
+ if (proc.exitCode === 0) return { status: "ok" };
61
+
62
+ const output = [proc.stdout.toString(), proc.stderr.toString()].join("").trim();
63
+ return { status: "failed", output: output || `tsc exited ${proc.exitCode}` };
64
+ }
65
+
66
+ /**
67
+ * Typecheck and report. Returns false when the build should stop.
68
+ *
69
+ * Blocking on purpose: a type error here means the bundle that reaches the Hub
70
+ * is built from code the author never checked.
71
+ */
72
+ export function typecheckAndReport(cwd: string): boolean {
73
+ const result = runTypecheck(cwd);
74
+ if (result.status === "ok") return true;
75
+
76
+ if (result.status === "skipped") {
77
+ log.warn(`Types were NOT checked: ${result.reason}.`);
78
+ return true;
79
+ }
80
+
81
+ log.error("Typecheck failed:");
82
+ log.message(result.output);
83
+ return false;
84
+ }
@@ -1,4 +1,6 @@
1
1
  node_modules/
2
2
  dist/
3
+ preview/
4
+ .glasshome-preview/
3
5
  .env
4
6
  *.local
@@ -18,7 +18,12 @@ bun install
18
18
  bun run build
19
19
  ```
20
20
 
21
- This produces `dist/{widget}.js` for each widget and generates `dist/registry.json`.
21
+ This typechecks the project, produces `dist/{widget}.js` for each widget and
22
+ generates `dist/registry.json`. To check types on their own:
23
+
24
+ ```bash
25
+ bun run typecheck
26
+ ```
22
27
 
23
28
  ## Adding widgets
24
29
 
@@ -7,13 +7,14 @@
7
7
  "dist"
8
8
  ],
9
9
  "scripts": {
10
- "build": "vite build",
10
+ "build": "tsc --noEmit && vite build",
11
+ "typecheck": "tsc --noEmit",
11
12
  "dev": "vite build --watch",
12
13
  "widget": "glasshome-widget"
13
14
  },
14
15
  "peerDependencies": {
15
16
  "solid-js": "^1.9.11",
16
- "@glasshome/widget-sdk": "^0.2.0"
17
+ "@glasshome/widget-sdk": "SDK_RANGE"
17
18
  },
18
19
  "devDependencies": {
19
20
  "@glasshome/widget-cli": "CLI_VERSION",
@@ -31,7 +31,7 @@ export default defineWidget<WidgetConfig>({
31
31
  minSize: { w: 1, h: 1 },
32
32
  maxSize: { w: 4, h: 4 },
33
33
  defaultSize: { w: 2, h: 2 },
34
- sdkVersion: "^0.2.0",
34
+ sdkVersion: "SDK_RANGE",
35
35
  schema: {
36
36
  type: "object",
37
37
  properties: {
@@ -1,86 +0,0 @@
1
- import { log, outro, spinner } from "@clack/prompts";
2
- import { trpcMutate, trpcQuery } from "../utils/api";
3
-
4
- interface DevStatusResponse {
5
- devMode: boolean;
6
- registries: Array<{ url: string; name: string; isDefault: boolean }>;
7
- }
8
-
9
- export async function runAddDevRegistry(options: {
10
- registryUrl?: string;
11
- apiUrl?: string;
12
- }): Promise<void> {
13
- const registryUrl = options.registryUrl || "http://localhost:5173/__widgets/registry.json";
14
- const apiUrl = options.apiUrl || "http://localhost:3333";
15
-
16
- const s = spinner();
17
-
18
- // Step 1: Check if dev mode is enabled
19
- s.start("Checking dev mode status...");
20
- let devEnabled = false;
21
- try {
22
- const status = await trpcQuery<DevStatusResponse>({
23
- apiUrl,
24
- path: "appConfig.get",
25
- });
26
- devEnabled = status.devMode;
27
- } catch (err) {
28
- s.stop("Failed to connect");
29
- log.error(
30
- `Could not reach dashboard API at ${apiUrl}: ${err instanceof Error ? err.message : String(err)}`,
31
- );
32
- log.info("Make sure the dashboard API server is running and the URL is correct.");
33
- process.exit(1);
34
- }
35
-
36
- // Step 2: Enable dev mode if needed
37
- if (!devEnabled) {
38
- s.message("Enabling dev mode...");
39
- try {
40
- const result = await trpcMutate<{ devMode: boolean }>({
41
- apiUrl,
42
- path: "appConfig.toggleDevMode",
43
- input: {},
44
- });
45
- if (!result.devMode) {
46
- s.stop("Failed");
47
- log.error("Could not enable dev mode");
48
- process.exit(1);
49
- }
50
- } catch (err) {
51
- s.stop("Failed");
52
- log.error(`Failed to enable dev mode: ${err instanceof Error ? err.message : String(err)}`);
53
- process.exit(1);
54
- }
55
- }
56
- s.stop("Dev mode enabled");
57
-
58
- // Step 3: Add the registry via /dev/registries endpoint
59
- s.start("Adding dev registry...");
60
- try {
61
- const res = await fetch(`${apiUrl.replace(/\/$/, "")}/dev/registries`, {
62
- method: "POST",
63
- headers: { "Content-Type": "application/json" },
64
- body: JSON.stringify({ url: registryUrl, name: "Local Dev Server" }),
65
- });
66
-
67
- if (res.status === 409) {
68
- s.stop("Registry already registered");
69
- log.info(`Registry ${registryUrl} is already configured`);
70
- } else if (res.ok) {
71
- s.stop("Registry added");
72
- log.success(`Added registry: ${registryUrl}`);
73
- } else {
74
- const err = (await res.json()) as { error: string };
75
- s.stop("Failed");
76
- log.error(`Failed to add registry: ${err.error}`);
77
- process.exit(1);
78
- }
79
- } catch (err) {
80
- s.stop("Failed");
81
- log.error(`Failed to add registry: ${err instanceof Error ? err.message : String(err)}`);
82
- process.exit(1);
83
- }
84
-
85
- outro("Done! Refresh your dashboard to see dev widgets.");
86
- }