rshow 0.0.1 → 0.0.3

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/dist/main.js CHANGED
@@ -1,8 +1,115 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/main.ts
4
- import path21 from "node:path";
5
- import process15 from "node:process";
4
+ import path25 from "node:path";
5
+ import process23 from "node:process";
6
+
7
+ // src/binary.ts
8
+ import { mkdir, mkdtemp, rename, rm, stat, writeFile } from "node:fs/promises";
9
+ import { homedir } from "node:os";
10
+ import path from "node:path";
11
+ import process from "node:process";
12
+
13
+ // src/version.ts
14
+ import { readFileSync } from "node:fs";
15
+ import { fileURLToPath } from "node:url";
16
+ var VERSION = true ? "0.0.3" : versionFromPackage();
17
+
18
+ // src/binary.ts
19
+ var ASSET_MANIFEST = "assets.json";
20
+ var COMPLETE_MARKER = ".complete";
21
+ var sea;
22
+ function seaModule() {
23
+ if (sea === void 0) {
24
+ sea = import("node:sea").then(
25
+ (mod) => mod,
26
+ () => void 0
27
+ );
28
+ }
29
+ return sea;
30
+ }
31
+ async function isBinary() {
32
+ const mod = await seaModule();
33
+ if (mod === void 0 || typeof mod.isSea !== "function") return false;
34
+ try {
35
+ return mod.isSea();
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+ function cacheRoot(env = process.env) {
41
+ const xdg = (env.XDG_CACHE_HOME ?? "").trim();
42
+ if (xdg !== "") return path.resolve(xdg);
43
+ const home = (env.HOME ?? "").trim();
44
+ return path.join(home !== "" ? home : homedir(), ".cache");
45
+ }
46
+ function binaryCacheDir(env = process.env, version = VERSION) {
47
+ return path.join(cacheRoot(env), "rshow", version);
48
+ }
49
+ async function exists(file) {
50
+ return await stat(file).catch(() => void 0) !== void 0;
51
+ }
52
+ async function extractAssets(bundle, dir) {
53
+ if (await exists(path.join(dir, COMPLETE_MARKER))) return;
54
+ const parent = path.dirname(dir);
55
+ await mkdir(parent, { recursive: true });
56
+ const tmp = await mkdtemp(path.join(parent, `${path.basename(dir)}.tmp-`));
57
+ try {
58
+ for (const key2 of bundle.keys) {
59
+ const file = path.join(tmp, ...key2.split("/"));
60
+ await mkdir(path.dirname(file), { recursive: true });
61
+ await writeFile(file, bundle.read(key2));
62
+ }
63
+ await writeFile(path.join(tmp, COMPLETE_MARKER), `${VERSION}
64
+ `);
65
+ await rm(dir, { recursive: true, force: true });
66
+ await rename(tmp, dir);
67
+ } catch (err) {
68
+ await rm(tmp, { recursive: true, force: true });
69
+ throw err;
70
+ }
71
+ }
72
+ function safeKey(key2) {
73
+ return key2 !== "" && !path.isAbsolute(key2) && !key2.split("/").includes("..");
74
+ }
75
+ function manifestKeys(read4) {
76
+ try {
77
+ const doc = JSON.parse(new TextDecoder().decode(read4(ASSET_MANIFEST)));
78
+ if (doc !== null && typeof doc === "object" && Array.isArray(doc.keys)) {
79
+ const keys = doc.keys.filter((key2) => typeof key2 === "string");
80
+ if (keys.length > 0) return keys.filter(safeKey);
81
+ }
82
+ } catch {
83
+ }
84
+ return [];
85
+ }
86
+ function bundleOf(mod) {
87
+ const raw = mod.getRawAsset;
88
+ const read4 = (key2) => new Uint8Array(raw(key2));
89
+ const withManifest = manifestKeys(read4);
90
+ return { keys: withManifest.length > 0 ? withManifest : (mod.getAssetKeys?.() ?? []).filter(safeKey), read: read4 };
91
+ }
92
+ async function unpack(env) {
93
+ if (!await isBinary()) return void 0;
94
+ const mod = await seaModule();
95
+ if (mod?.getRawAsset === void 0) throw new Error("this rshow binary carries no assets");
96
+ const dir = binaryCacheDir(env);
97
+ await extractAssets(bundleOf(mod), dir);
98
+ return dir;
99
+ }
100
+ var root;
101
+ function binaryRoot(env = process.env) {
102
+ if (root === void 0) root = unpack(env);
103
+ return root;
104
+ }
105
+ async function binaryThemeBases(env = process.env) {
106
+ const dir = await binaryRoot(env);
107
+ return dir === void 0 ? [] : [path.join(dir, "themes")];
108
+ }
109
+ async function binarySkillBases(env = process.env) {
110
+ const dir = await binaryRoot(env);
111
+ return dir === void 0 ? [] : [path.join(dir, "skills")];
112
+ }
6
113
 
7
114
  // src/exit.ts
8
115
  var EXIT = { ok: 0, usage: 2, auth: 4, api: 5, build: 5, check: 6, compare: 7 };
@@ -14,18 +121,18 @@ var UsageError = class extends Error {
14
121
  };
15
122
  var AuthError = class extends Error {
16
123
  status;
17
- constructor(message, status) {
124
+ constructor(message, status2) {
18
125
  super(message);
19
126
  this.name = "AuthError";
20
- this.status = status;
127
+ this.status = status2;
21
128
  }
22
129
  };
23
130
  var ApiError = class extends Error {
24
131
  status;
25
- constructor(message, status = 0) {
132
+ constructor(message, status2 = 0) {
26
133
  super(message);
27
134
  this.name = "ApiError";
28
- this.status = status;
135
+ this.status = status2;
29
136
  }
30
137
  };
31
138
  var BuildError = class extends Error {
@@ -41,20 +148,150 @@ var CheckError = class extends Error {
41
148
  }
42
149
  };
43
150
 
44
- // src/version.ts
45
- import { readFileSync } from "node:fs";
46
- import { fileURLToPath } from "node:url";
47
- var VERSION = true ? "0.0.1" : versionFromPackage();
151
+ // src/network.ts
152
+ import { readFile } from "node:fs/promises";
153
+ import process3 from "node:process";
154
+ import tls from "node:tls";
48
155
 
49
- // src/client.ts
156
+ // src/warn.ts
157
+ import process2 from "node:process";
158
+ function warn(message) {
159
+ process2.stderr.write(`rshow: warning: ${message}
160
+ `);
161
+ }
162
+
163
+ // src/network.ts
164
+ var PROXY_VARIABLES = ["https_proxy", "HTTPS_PROXY", "http_proxy", "HTTP_PROXY"];
165
+ var TRUST_HINT = "your network may inspect HTTPS: set RSHOW_EXTRA_CA_FILE to its root certificate, or check that the system store has it";
166
+ var PROXY_HINT = "if a proxy stands between you and that host, set https_proxy and run with NODE_USE_ENV_PROXY=1";
167
+ var DNS_HINT = "check the url in rshow config and your DNS";
168
+ var TRUSTED = { reason: "certificate not trusted", hint: TRUST_HINT };
169
+ var REASONS = {
170
+ UNABLE_TO_GET_ISSUER_CERT_LOCALLY: TRUSTED,
171
+ UNABLE_TO_VERIFY_LEAF_SIGNATURE: TRUSTED,
172
+ SELF_SIGNED_CERT_IN_CHAIN: TRUSTED,
173
+ DEPTH_ZERO_SELF_SIGNED_CERT: TRUSTED,
174
+ CERT_HAS_EXPIRED: TRUSTED,
175
+ ERR_TLS_CERT_ALTNAME_INVALID: TRUSTED,
176
+ ENOTFOUND: { reason: "host not found", hint: DNS_HINT },
177
+ EAI_AGAIN: { reason: "host not found", hint: DNS_HINT },
178
+ ECONNREFUSED: { reason: "connection refused" },
179
+ ETIMEDOUT: { reason: "connection timed out", proxyHint: true },
180
+ UND_ERR_CONNECT_TIMEOUT: { reason: "connection timed out", proxyHint: true },
181
+ ECONNRESET: { reason: "connection reset", proxyHint: true }
182
+ };
50
183
  function messageOf(err) {
51
184
  return err instanceof Error ? err.message : String(err);
52
185
  }
53
- function errorText(body, status) {
186
+ function pemCertificatesOf(text) {
187
+ return text.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g) ?? [];
188
+ }
189
+ async function extraCertificates(env) {
190
+ const file = (env.RSHOW_EXTRA_CA_FILE ?? "").trim();
191
+ if (file === "") return [];
192
+ let text;
193
+ try {
194
+ text = await readFile(file, "utf8");
195
+ } catch (err) {
196
+ warn(`cannot read RSHOW_EXTRA_CA_FILE ${file}: ${messageOf(err)}`);
197
+ return [];
198
+ }
199
+ const certs = pemCertificatesOf(text);
200
+ if (certs.length === 0) warn(`RSHOW_EXTRA_CA_FILE ${file} holds no PEM certificate`);
201
+ return certs;
202
+ }
203
+ async function installCaCertificates(env = process3.env) {
204
+ const extra = await extraCertificates(env);
205
+ const api = tls;
206
+ if (typeof api.getCACertificates !== "function" || typeof api.setDefaultCACertificates !== "function") return;
207
+ try {
208
+ const certs = [...api.getCACertificates("default")];
209
+ if (env.RSHOW_SYSTEM_CA !== "0") certs.push(...api.getCACertificates("system"));
210
+ certs.push(...extra);
211
+ api.setDefaultCACertificates([...new Set(certs)]);
212
+ } catch {
213
+ }
214
+ }
215
+ function proxyVariableOf(env) {
216
+ return PROXY_VARIABLES.find((name) => (env[name] ?? "").trim() !== "");
217
+ }
218
+ function envProxyEnabled(env) {
219
+ const flag = (env.NODE_USE_ENV_PROXY ?? "").trim();
220
+ if (flag !== "" && flag !== "0") return true;
221
+ return process3.execArgv.includes("--use-env-proxy");
222
+ }
223
+ var proxyNoticed = false;
224
+ function noticeProxy(env) {
225
+ if (proxyNoticed) return;
226
+ proxyNoticed = true;
227
+ const name = proxyVariableOf(env);
228
+ if (name === void 0 || envProxyEnabled(env)) return;
229
+ warn(`${name} is set; run with NODE_USE_ENV_PROXY=1 (Node 24+) so rshow uses it`);
230
+ }
231
+ var installed;
232
+ function readyNetwork(env = process3.env) {
233
+ installed ??= installCaCertificates(env);
234
+ noticeProxy(env);
235
+ return installed;
236
+ }
237
+ function codesOf(err, depth = 0) {
238
+ if (depth > 5 || err === null || typeof err !== "object") return [];
239
+ const out = [];
240
+ const code = err.code;
241
+ if (typeof code === "string" && code !== "") out.push(code);
242
+ const inner = err.errors;
243
+ if (Array.isArray(inner)) for (const one of inner) out.push(...codesOf(one, depth + 1));
244
+ out.push(...codesOf(err.cause, depth + 1));
245
+ return out;
246
+ }
247
+ function deepestMessageOf(err, depth = 0) {
248
+ if (depth > 5 || err === null || typeof err !== "object") return "";
249
+ const cause = deepestMessageOf(err.cause, depth + 1);
250
+ if (cause !== "") return cause;
251
+ const message = messageOf(err);
252
+ return message === "" ? "" : message;
253
+ }
254
+ function hostOf(url) {
255
+ try {
256
+ return new URL(url).host;
257
+ } catch {
258
+ return url;
259
+ }
260
+ }
261
+ function faultOf(err, env = process3.env) {
262
+ const codes = codesOf(err).reverse();
263
+ for (const code of codes) {
264
+ const known = REASONS[code];
265
+ if (known === void 0) continue;
266
+ const hint = known.proxyHint === true ? proxyVariableOf(env) === void 0 ? PROXY_HINT : void 0 : known.hint;
267
+ return hint === void 0 ? { reason: known.reason } : { reason: known.reason, hint };
268
+ }
269
+ return { reason: codes[0] ?? deepestMessageOf(err) };
270
+ }
271
+ function networkDetail(err, url, env = process3.env) {
272
+ return `network error: ${hostOf(url)}: ${faultOf(err, env).reason}`;
273
+ }
274
+ function networkText(err, url, env = process3.env) {
275
+ const fault = faultOf(err, env);
276
+ const line = `network error: ${hostOf(url)}: ${fault.reason}`;
277
+ return fault.hint === void 0 ? line : `${line}
278
+ hint: ${fault.hint}`;
279
+ }
280
+ async function sendRequest(fetchImpl, url, init, env = process3.env) {
281
+ await readyNetwork(env);
282
+ try {
283
+ return await fetchImpl(url, init);
284
+ } catch (err) {
285
+ throw new ApiError(networkText(err, url, env));
286
+ }
287
+ }
288
+
289
+ // src/client.ts
290
+ function errorText(body, status2) {
54
291
  if (body !== null && typeof body === "object" && typeof body.error === "string") {
55
292
  return body.error;
56
293
  }
57
- return `HTTP ${status}`;
294
+ return `HTTP ${status2}`;
58
295
  }
59
296
  function parseBody(text) {
60
297
  if (text.trim() === "") return void 0;
@@ -87,12 +324,7 @@ var Client = class {
87
324
  headers["content-type"] = opts.contentType ?? "application/octet-stream";
88
325
  body = opts.bytes;
89
326
  }
90
- let res;
91
- try {
92
- res = await this.fetchImpl(this.baseUrl + pathname, { method, headers, body });
93
- } catch (err) {
94
- throw new ApiError(`network error: ${messageOf(err)}`);
95
- }
327
+ const res = await sendRequest(this.fetchImpl, this.baseUrl + pathname, { method, headers, body });
96
328
  const text = await res.text();
97
329
  const parsed = parseBody(text);
98
330
  if (opts.allow404 === true && res.status === 404) return { status: 404, body: void 0 };
@@ -178,73 +410,9 @@ var Client = class {
178
410
  };
179
411
 
180
412
  // src/config.ts
181
- import { readFile } from "node:fs/promises";
182
- import { homedir } from "node:os";
183
- import path from "node:path";
184
-
185
- // src/keychain.ts
186
- import { spawn } from "node:child_process";
187
- import process from "node:process";
188
- var SERVICE = "rshow";
189
- var MISSING = 44;
190
- var NO_MACOS = "the keychain needs macOS; set RSHOW_TOKEN instead";
191
- function bin() {
192
- return process.env.RSHOW_SECURITY_BIN ?? "security";
193
- }
194
- function usable() {
195
- return process.platform === "darwin" || process.env.RSHOW_SECURITY_BIN !== void 0;
196
- }
197
- function run(args, input) {
198
- return new Promise((resolve2, reject) => {
199
- const child = spawn(bin(), args, { stdio: ["pipe", "pipe", "pipe"] });
200
- let stdout = "";
201
- let stderr = "";
202
- child.stdout.setEncoding("utf8");
203
- child.stdout.on("data", (chunk2) => {
204
- stdout += chunk2;
205
- });
206
- child.stderr.setEncoding("utf8");
207
- child.stderr.on("data", (chunk2) => {
208
- stderr += chunk2;
209
- });
210
- child.stdin.on("error", () => {
211
- });
212
- child.on("error", (err) => {
213
- if (err.code === "ENOENT") resolve2({ code: -1, stdout, stderr, absent: true });
214
- else reject(new ApiError(`keychain: ${err.message}`));
215
- });
216
- child.on("close", (code) => resolve2({ code: code ?? -1, stdout, stderr, absent: false }));
217
- child.stdin.end(input ?? "");
218
- });
219
- }
220
- function failure(doing, result, secret) {
221
- const text = secret === void 0 ? result.stderr : result.stderr.split(secret).join("[redacted]");
222
- const detail = text.trim();
223
- return new ApiError(`${doing}: ${detail === "" ? `exit ${result.code}` : detail}`);
224
- }
225
- async function save(url, token) {
226
- if (!usable()) throw new UsageError(NO_MACOS);
227
- const line = `add-generic-password -U -s ${SERVICE} -a ${url} -w ${token}
228
- `;
229
- const result = await run(["-i"], line);
230
- if (result.code !== 0) throw failure("cannot save the token in the keychain", result, token);
231
- if (await read(url) !== token) throw failure("cannot save the token in the keychain", result, token);
232
- }
233
- async function read(url) {
234
- if (!usable()) return void 0;
235
- const result = await run(["find-generic-password", "-s", SERVICE, "-a", url, "-w"]);
236
- if (result.absent || result.code === MISSING) return void 0;
237
- if (result.code !== 0) throw failure("cannot read the token from the keychain", result);
238
- const token = result.stdout.trim();
239
- return token === "" ? void 0 : token;
240
- }
241
- async function remove(url) {
242
- if (!usable()) throw new UsageError(NO_MACOS);
243
- const result = await run(["delete-generic-password", "-s", SERVICE, "-a", url]);
244
- if (result.absent || result.code === MISSING) return false;
245
- if (result.code !== 0) throw failure("cannot remove the token from the keychain", result);
246
- return true;
247
- }
413
+ import { readFile as readFile3 } from "node:fs/promises";
414
+ import { homedir as homedir3 } from "node:os";
415
+ import path3 from "node:path";
248
416
 
249
417
  // ../shared/src/index.ts
250
418
  var SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;
@@ -291,8 +459,8 @@ function tagsOf(value) {
291
459
  if (!Array.isArray(value)) return [];
292
460
  return value.filter((tag) => typeof tag === "string").map((tag) => tag.toLowerCase());
293
461
  }
294
- function withTags(current, add2, remove3) {
295
- const next = unique(tagsOf(current)).filter((tag) => !remove3.includes(tag));
462
+ function withTags(current, add2, remove5) {
463
+ const next = unique(tagsOf(current)).filter((tag) => !remove5.includes(tag));
296
464
  for (const tag of add2) {
297
465
  if (!next.includes(tag)) next.push(tag);
298
466
  }
@@ -304,16 +472,306 @@ function sameTags(left, right) {
304
472
  return a.length === b.length && a.every((tag, index) => tag === b[index]);
305
473
  }
306
474
 
475
+ // src/tokenstore.ts
476
+ import process8 from "node:process";
477
+
478
+ // src/credfile.ts
479
+ import { chmod, mkdir as mkdir2, readFile as readFile2, rename as rename2, stat as stat2, unlink, writeFile as writeFile2 } from "node:fs/promises";
480
+ import { homedir as homedir2 } from "node:os";
481
+ import path2 from "node:path";
482
+ import process4 from "node:process";
483
+ var DIR = "rshow";
484
+ var FILE = "credentials";
485
+ var VERSION2 = 1;
486
+ var FILE_MODE = 384;
487
+ var DIR_MODE = 448;
488
+ var SHARED = 63;
489
+ var fixed = /* @__PURE__ */ new Set();
490
+ function messageOf2(err) {
491
+ return err instanceof Error ? err.message : String(err);
492
+ }
493
+ function isRecord(value) {
494
+ return value !== null && typeof value === "object" && !Array.isArray(value);
495
+ }
496
+ function credentialsPath(env) {
497
+ const xdg = (env.XDG_CONFIG_HOME ?? "").trim();
498
+ const home = (env.HOME ?? "").trim();
499
+ const base = xdg !== "" ? xdg : path2.join(home !== "" ? home : homedir2(), ".config");
500
+ return path2.join(base, DIR, FILE);
501
+ }
502
+ async function tighten(file) {
503
+ await chmod(file, FILE_MODE).catch(() => {
504
+ });
505
+ if (fixed.has(file)) return;
506
+ fixed.add(file);
507
+ warn(`${file} was readable by group or other; its mode is now 600`);
508
+ }
509
+ async function load(file) {
510
+ const info = await stat2(file).catch(() => void 0);
511
+ if (info === void 0) return {};
512
+ if ((info.mode & SHARED) !== 0) await tighten(file);
513
+ let text;
514
+ try {
515
+ text = await readFile2(file, "utf8");
516
+ } catch (err) {
517
+ if (err.code === "ENOENT") return {};
518
+ throw new UsageError(`cannot read ${file}: ${messageOf2(err)}`);
519
+ }
520
+ let parsed;
521
+ try {
522
+ parsed = JSON.parse(text);
523
+ } catch {
524
+ throw new UsageError(`invalid JSON in ${file}`);
525
+ }
526
+ if (!isRecord(parsed)) throw new UsageError(`${file} must hold a JSON object`);
527
+ if (parsed.version !== void 0 && parsed.version !== VERSION2) {
528
+ throw new UsageError(`${file}: version must be ${VERSION2}`);
529
+ }
530
+ const raw = parsed.tokens ?? {};
531
+ if (!isRecord(raw)) throw new UsageError(`${file}: tokens must be an object`);
532
+ const tokens = {};
533
+ for (const [url, token] of Object.entries(raw)) {
534
+ if (typeof token !== "string") throw new UsageError(`${file}: tokens.${url} must be a string`);
535
+ tokens[url] = token;
536
+ }
537
+ return tokens;
538
+ }
539
+ async function store(file, tokens) {
540
+ const dir = path2.dirname(file);
541
+ let created;
542
+ try {
543
+ created = await mkdir2(dir, { recursive: true, mode: DIR_MODE });
544
+ } catch (err) {
545
+ throw new UsageError(`cannot make ${dir}: ${messageOf2(err)}`);
546
+ }
547
+ if (created !== void 0) await chmod(dir, DIR_MODE).catch(() => {
548
+ });
549
+ const temp = path2.join(dir, `.${path2.basename(file)}.${process4.pid}.${Date.now()}.tmp`);
550
+ const body = `${JSON.stringify({ version: VERSION2, tokens }, null, 2)}
551
+ `;
552
+ try {
553
+ await writeFile2(temp, body, { mode: FILE_MODE, flag: "wx" });
554
+ await chmod(temp, FILE_MODE);
555
+ await rename2(temp, file);
556
+ } catch (err) {
557
+ await unlink(temp).catch(() => {
558
+ });
559
+ throw new UsageError(`cannot write ${file}: ${messageOf2(err)}`);
560
+ }
561
+ }
562
+ async function read(file, url) {
563
+ return (await load(file))[url];
564
+ }
565
+ async function save(file, url, token) {
566
+ const tokens = await load(file);
567
+ tokens[url] = token;
568
+ await store(file, tokens);
569
+ }
570
+ async function remove(file, url) {
571
+ const tokens = await load(file);
572
+ if (tokens[url] === void 0) return false;
573
+ delete tokens[url];
574
+ if (Object.keys(tokens).length === 0) {
575
+ await unlink(file).catch((err) => {
576
+ if (err.code !== "ENOENT") throw new UsageError(`cannot remove ${file}: ${messageOf2(err)}`);
577
+ });
578
+ return true;
579
+ }
580
+ await store(file, tokens);
581
+ return true;
582
+ }
583
+
584
+ // src/keychain.ts
585
+ import process6 from "node:process";
586
+
587
+ // src/tool.ts
588
+ import { spawn } from "node:child_process";
589
+ import process5 from "node:process";
590
+ function childEnv(env) {
591
+ const out = { ...process5.env };
592
+ for (const [key2, value] of Object.entries(env ?? {})) {
593
+ if (value === void 0) delete out[key2];
594
+ else out[key2] = value;
595
+ }
596
+ return out;
597
+ }
598
+ function runTool(bin3, args, input, env) {
599
+ return new Promise((resolve2, reject) => {
600
+ const child = spawn(bin3, args, { stdio: ["pipe", "pipe", "pipe"], env: childEnv(env) });
601
+ let stdout = "";
602
+ let stderr = "";
603
+ child.stdout.setEncoding("utf8");
604
+ child.stdout.on("data", (chunk2) => {
605
+ stdout += chunk2;
606
+ });
607
+ child.stderr.setEncoding("utf8");
608
+ child.stderr.on("data", (chunk2) => {
609
+ stderr += chunk2;
610
+ });
611
+ child.stdin.on("error", () => {
612
+ });
613
+ child.on("error", (err) => {
614
+ if (err.code === "ENOENT") resolve2({ code: -1, stdout, stderr, absent: true });
615
+ else reject(new ApiError(`${bin3}: ${err.message}`));
616
+ });
617
+ child.on("close", (code) => resolve2({ code: code ?? -1, stdout, stderr, absent: false }));
618
+ child.stdin.end(input ?? "");
619
+ });
620
+ }
621
+ function toolFailure(doing, result, secret) {
622
+ const text = secret === void 0 ? result.stderr : result.stderr.split(secret).join("[redacted]");
623
+ const detail = text.trim();
624
+ return new ApiError(`${doing}: ${detail === "" ? `exit ${result.code}` : detail}`);
625
+ }
626
+
627
+ // src/keychain.ts
628
+ var SERVICE = "rshow";
629
+ var MISSING = 44;
630
+ function bin(env) {
631
+ const set = (env.RSHOW_SECURITY_BIN ?? "").trim();
632
+ return set === "" ? "security" : set;
633
+ }
634
+ function canSave(platform = process6.platform, securityBin = process6.env.RSHOW_SECURITY_BIN) {
635
+ return platform === "darwin" || (securityBin ?? "").trim() !== "";
636
+ }
637
+ async function save2(url, token, env = process6.env) {
638
+ if (!canSave(process6.platform, env.RSHOW_SECURITY_BIN)) {
639
+ throw new UsageError("the keychain needs macOS; set RSHOW_TOKEN instead");
640
+ }
641
+ const line = `add-generic-password -U -s ${SERVICE} -a ${url} -w ${token}
642
+ `;
643
+ const result = await runTool(bin(env), ["-i"], line, env);
644
+ if (result.code !== 0) throw toolFailure("cannot save the token in the keychain", result, token);
645
+ if (await read2(url, env) !== token) {
646
+ throw toolFailure("cannot save the token in the keychain", result, token);
647
+ }
648
+ }
649
+ async function read2(url, env = process6.env) {
650
+ if (!canSave(process6.platform, env.RSHOW_SECURITY_BIN)) return void 0;
651
+ const args = ["find-generic-password", "-s", SERVICE, "-a", url, "-w"];
652
+ const result = await runTool(bin(env), args, void 0, env);
653
+ if (result.absent || result.code === MISSING) return void 0;
654
+ if (result.code !== 0) throw toolFailure("cannot read the token from the keychain", result);
655
+ const token = result.stdout.trim();
656
+ return token === "" ? void 0 : token;
657
+ }
658
+ async function remove2(url, env = process6.env) {
659
+ if (!canSave(process6.platform, env.RSHOW_SECURITY_BIN)) return false;
660
+ const args = ["delete-generic-password", "-s", SERVICE, "-a", url];
661
+ const result = await runTool(bin(env), args, void 0, env);
662
+ if (result.absent || result.code === MISSING) return false;
663
+ if (result.code !== 0) throw toolFailure("cannot remove the token from the keychain", result);
664
+ return true;
665
+ }
666
+
667
+ // src/keyring.ts
668
+ import process7 from "node:process";
669
+ var SERVICE2 = "rshow";
670
+ var PROBE_SERVICE = "rshow-probe";
671
+ var PROBE_ACCOUNT = "probe";
672
+ var NOT_FOUND = 1;
673
+ function bin2(env) {
674
+ const set = (env.RSHOW_SECRET_TOOL_BIN ?? "").trim();
675
+ return set === "" ? "secret-tool" : set;
676
+ }
677
+ function canUse(env, platform = process7.platform) {
678
+ return platform === "linux" || (env.RSHOW_SECRET_TOOL_BIN ?? "").trim() !== "";
679
+ }
680
+ async function usable(env = process7.env) {
681
+ const args = ["lookup", "service", PROBE_SERVICE, "account", PROBE_ACCOUNT];
682
+ const result = await runTool(bin2(env), args, void 0, env);
683
+ return !result.absent && (result.code === 0 || result.code === NOT_FOUND);
684
+ }
685
+ async function save3(url, token, env = process7.env) {
686
+ const args = ["store", "--label", `rshow ${url}`, "service", SERVICE2, "account", url];
687
+ const result = await runTool(bin2(env), args, token, env);
688
+ if (result.code !== 0) throw toolFailure("cannot save the token in the keyring", result, token);
689
+ if (await read3(url, env) !== token) {
690
+ throw toolFailure("cannot save the token in the keyring", result, token);
691
+ }
692
+ }
693
+ async function read3(url, env = process7.env) {
694
+ const args = ["lookup", "service", SERVICE2, "account", url];
695
+ const result = await runTool(bin2(env), args, void 0, env);
696
+ if (result.absent || result.code === NOT_FOUND) return void 0;
697
+ if (result.code !== 0) throw toolFailure("cannot read the token from the keyring", result);
698
+ const token = result.stdout.trim();
699
+ return token === "" ? void 0 : token;
700
+ }
701
+ async function remove3(url, env = process7.env) {
702
+ const args = ["clear", "service", SERVICE2, "account", url];
703
+ const result = await runTool(bin2(env), args, void 0, env);
704
+ if (result.absent || result.code === NOT_FOUND) return false;
705
+ if (result.code !== 0) throw toolFailure("cannot remove the token from the keyring", result);
706
+ return true;
707
+ }
708
+
709
+ // src/tokenstore.ts
710
+ var BACKENDS = ["keychain", "keyring", "file"];
711
+ function storeOf(backend, env) {
712
+ if (backend === "keychain") {
713
+ return {
714
+ backend,
715
+ detail: "keychain",
716
+ description: "the macOS Keychain",
717
+ read: (url) => read2(url, env),
718
+ save: (url, token) => save2(url, token, env),
719
+ remove: (url) => remove2(url, env)
720
+ };
721
+ }
722
+ if (backend === "keyring") {
723
+ return {
724
+ backend,
725
+ detail: "keyring",
726
+ description: "the Linux keyring",
727
+ read: (url) => read3(url, env),
728
+ save: (url, token) => save3(url, token, env),
729
+ remove: (url) => remove3(url, env)
730
+ };
731
+ }
732
+ const file = credentialsPath(env);
733
+ return {
734
+ backend,
735
+ detail: `file ${file}`,
736
+ description: `${file} (mode 600)`,
737
+ read: (url) => read(file, url),
738
+ save: (url, token) => save(file, url, token),
739
+ remove: (url) => remove(file, url)
740
+ };
741
+ }
742
+ function forced(env) {
743
+ const text = (env.RSHOW_TOKEN_STORE ?? "").trim();
744
+ if (text === "") return void 0;
745
+ for (const backend of BACKENDS) {
746
+ if (backend === text) return backend;
747
+ }
748
+ throw new UsageError(`RSHOW_TOKEN_STORE must be keychain, keyring or file, not ${text}`);
749
+ }
750
+ async function choose(env = process8.env, platform = process8.platform) {
751
+ const forcedBackend = forced(env);
752
+ if (forcedBackend !== void 0) return storeOf(forcedBackend, env);
753
+ if (canSave(platform, env.RSHOW_SECURITY_BIN)) return storeOf("keychain", env);
754
+ if (canUse(env, platform) && await usable(env)) return storeOf("keyring", env);
755
+ return storeOf("file", env);
756
+ }
757
+ async function every(env = process8.env, platform = process8.platform) {
758
+ const stores = [];
759
+ if (canSave(platform, env.RSHOW_SECURITY_BIN)) stores.push(storeOf("keychain", env));
760
+ if (canUse(env, platform) && await usable(env)) stores.push(storeOf("keyring", env));
761
+ stores.push(storeOf("file", env));
762
+ return stores;
763
+ }
764
+
307
765
  // src/config.ts
308
766
  var DEFAULT_OUT = "dist";
309
767
  var GLOBAL_DIR = "rshow";
310
768
  var GLOBAL_FILE = "config.json";
311
769
  var LOCAL_FILE = "rshow.json";
312
770
  var NO_SERVER = "no server configured: run rshow init --url <admin-url> --art <viewer-url>";
313
- function messageOf2(err) {
771
+ function messageOf3(err) {
314
772
  return err instanceof Error ? err.message : String(err);
315
773
  }
316
- function isRecord(value) {
774
+ function isRecord2(value) {
317
775
  return value !== null && typeof value === "object" && !Array.isArray(value);
318
776
  }
319
777
  function withoutSlash(value) {
@@ -322,14 +780,14 @@ function withoutSlash(value) {
322
780
  function globalConfigPath(env) {
323
781
  const xdg = (env.XDG_CONFIG_HOME ?? "").trim();
324
782
  const home = (env.HOME ?? "").trim();
325
- const base = xdg !== "" ? xdg : path.join(home !== "" ? home : homedir(), ".config");
326
- return path.join(base, GLOBAL_DIR, GLOBAL_FILE);
783
+ const base = xdg !== "" ? xdg : path3.join(home !== "" ? home : homedir3(), ".config");
784
+ return path3.join(base, GLOBAL_DIR, GLOBAL_FILE);
327
785
  }
328
786
  function localConfigPath(dir) {
329
- return path.join(path.resolve(dir), LOCAL_FILE);
787
+ return path3.join(path3.resolve(dir), LOCAL_FILE);
330
788
  }
331
789
  function layerOf(file, value) {
332
- if (!isRecord(value)) throw new UsageError(`${file} must hold a JSON object`);
790
+ if (!isRecord2(value)) throw new UsageError(`${file} must hold a JSON object`);
333
791
  const fieldOf = (key2) => {
334
792
  const raw = value[key2];
335
793
  if (raw === void 0 || raw === null) return void 0;
@@ -349,7 +807,7 @@ function layerOf(file, value) {
349
807
  };
350
808
  const rawTokens = value.tokens;
351
809
  if (rawTokens !== void 0 && rawTokens !== null) {
352
- if (!isRecord(rawTokens)) throw new UsageError(`${file}: tokens must be an object`);
810
+ if (!isRecord2(rawTokens)) throw new UsageError(`${file}: tokens must be an object`);
353
811
  const tokens = {};
354
812
  for (const [key2, raw] of Object.entries(rawTokens)) {
355
813
  if (typeof raw !== "string") throw new UsageError(`${file}: tokens.${key2} must be a string`);
@@ -371,10 +829,10 @@ function layerOf(file, value) {
371
829
  async function readConfigFile(file) {
372
830
  let text;
373
831
  try {
374
- text = await readFile(file, "utf8");
832
+ text = await readFile3(file, "utf8");
375
833
  } catch (err) {
376
834
  if (err.code === "ENOENT") return {};
377
- throw new UsageError(`cannot read ${file}: ${messageOf2(err)}`);
835
+ throw new UsageError(`cannot read ${file}: ${messageOf3(err)}`);
378
836
  }
379
837
  let parsed;
380
838
  try {
@@ -436,13 +894,14 @@ function requireArt(config) {
436
894
  async function readToken(env, url) {
437
895
  const token = (env.RSHOW_TOKEN ?? "").trim();
438
896
  if (token !== "") return token;
439
- const saved = await read(url);
897
+ const store2 = await choose(env);
898
+ const saved = await store2.read(url);
440
899
  if (saved !== void 0) return saved;
441
900
  throw new UsageError("no token: run rshow login, or set RSHOW_TOKEN");
442
901
  }
443
902
 
444
903
  // src/output.ts
445
- import process2 from "node:process";
904
+ import process9 from "node:process";
446
905
  var Output = class {
447
906
  json;
448
907
  constructor(json) {
@@ -450,16 +909,16 @@ var Output = class {
450
909
  }
451
910
  data(value) {
452
911
  if (!this.json) return;
453
- process2.stdout.write(`${JSON.stringify(value ?? null, null, 2)}
912
+ process9.stdout.write(`${JSON.stringify(value ?? null, null, 2)}
454
913
  `);
455
914
  }
456
915
  line(text) {
457
916
  if (this.json) return;
458
- process2.stdout.write(`${text}
917
+ process9.stdout.write(`${text}
459
918
  `);
460
919
  }
461
920
  raw(text) {
462
- process2.stdout.write(`${text}
921
+ process9.stdout.write(`${text}
463
922
  `);
464
923
  }
465
924
  };
@@ -475,12 +934,13 @@ commands:
475
934
  preview <dir> serve a folder on 127.0.0.1 for a look, offline
476
935
  config [dir] print the merged config as JSON
477
936
  init --url u --art a write the global config file
478
- doctor [dir] check node, config, token, api, art and theme
937
+ doctor [dir] check node, config, token, api, art, theme, skills
479
938
  theme new <name> write a preset folder from a built-in theme
480
939
  theme check <dir> check a theme folder, preset or full
481
940
  theme preview <dir> build a theme, serve it on 127.0.0.1 for a look
482
941
  theme publish <dir> publish a theme folder as theme-<name>
483
942
  theme list list theme artifacts
943
+ skills <cmd> install, remove or show the agent skills, per target
484
944
  project <cmd> create, list, show, share or delete a project
485
945
  tag <slug> add or remove tags on an artifact
486
946
  list [filters] list artifacts you own or share
@@ -488,7 +948,7 @@ commands:
488
948
  share <slug> set mode or change shares
489
949
  pin <slug> <n|live> serve version n, or live for the newest
490
950
  delete <slug> --yes delete an artifact
491
- login [--url u] save the API token in the macOS Keychain
951
+ login [--url u] save the API token for this account
492
952
  logout [--url u] remove the saved token for a server
493
953
 
494
954
  global:
@@ -499,9 +959,13 @@ global:
499
959
  environment:
500
960
  RSHOW_URL API base URL, overrides the config file
501
961
  RSHOW_ART art base URL, overrides the config file
502
- RSHOW_TOKEN the API token, else the token saved by rshow login
503
- in the macOS Keychain; never read from a file
504
- XDG_CONFIG_HOME where to find rshow/config.json (default ~/.config)
962
+ RSHOW_TOKEN the API token, else the token rshow login saved in
963
+ the macOS Keychain, the Linux keyring or a 0600 file
964
+ RSHOW_TOKEN_STORE force the keychain, keyring or file token store
965
+ XDG_CONFIG_HOME where rshow keeps config.json and credentials
966
+ RSHOW_SYSTEM_CA set to 0 to trust only the roots node bundles
967
+ RSHOW_EXTRA_CA_FILE a PEM file of extra roots, such as a cluster's CA
968
+ NODE_USE_ENV_PROXY set to 1 so fetch uses https_proxy (Node 24+)
505
969
 
506
970
  Start here: rshow init --url <admin-url> --art <viewer-url>, then rshow login.
507
971
 
@@ -599,8 +1063,8 @@ files win. Run rshow init to write url and art.
599
1063
  A theme with no version, or @latest, is looked up on the art host and printed
600
1064
  as <name>@<n>; when that fails a warning goes to stderr. --offline skips it, and
601
1065
  so does a config with no art host.
602
- The token is RSHOW_TOKEN, else the token saved by rshow login in the macOS
603
- Keychain; never read from a file.`;
1066
+ The token is RSHOW_TOKEN, else the token rshow login saved in the macOS
1067
+ Keychain, the Linux keyring or a file with mode 600.`;
604
1068
  var INIT_USAGE = `usage: rshow init --url <admin-url> --art <viewer-url> [--theme <name>] [--force]
605
1069
 
606
1070
  Writes the global config file that rshow reads first:
@@ -616,9 +1080,11 @@ Prints the path it wrote and the next step: rshow login. --json prints
616
1080
  var DOCTOR_USAGE = `usage: rshow doctor [dir] [--json]
617
1081
 
618
1082
  Checks the rshow version, the node version against the engines.node of the
619
- package, the config files, the token, the api, the art host and the theme, one
620
- line each: ok|warn|fail <check> <detail>. With [dir] it reads <dir>/rshow.json too.
1083
+ package, the config files, the token, the api, the art host, the theme and the
1084
+ skills, one line each: ok|warn|fail <check> <detail>. With [dir] it reads
1085
+ <dir>/rshow.json too. A skill line warns and never fails.
621
1086
  Network calls time out after 5 s. Exit 0 when no check failed, 6 when one did.
1087
+ The token line names its source: env, keychain, keyring, or file and its path.
622
1088
  The token is never printed.`;
623
1089
  var THEME_USAGE = `usage: rshow theme <new|check|preview|publish|list> [options]
624
1090
 
@@ -676,6 +1142,54 @@ Prints /_themes/<name>@<n>/.`;
676
1142
  var THEME_LIST_USAGE = `usage: rshow theme list
677
1143
 
678
1144
  Lists artifacts whose slug starts with theme- and their served version.`;
1145
+ var SKILLS_USAGE = `usage: rshow skills <install|uninstall|status> [target ...] [options]
1146
+
1147
+ skills install [target ...] link or copy the three skills into an agent
1148
+ skills uninstall [target ...] remove the links and copies rshow made
1149
+ skills status [target ...] say what each target holds, per skill
1150
+
1151
+ A target is where an agent reads its skills from:
1152
+ claude $HOME/.claude/skills
1153
+ pi $HOME/.pi/agent/skills
1154
+ codex $HOME/.codex/skills
1155
+ With no target, every target whose parent folder exists is used.`;
1156
+ var SKILLS_INSTALL_USAGE = `usage: rshow skills install [target ...] [--link|--copy] [--dry-run]
1157
+
1158
+ Puts the three skills rshow ships, rshow, rshow-slides and rshow-implement, in
1159
+ the skills folder of every target, so that claude, pi and codex read them:
1160
+ $HOME/.claude/skills, $HOME/.pi/agent/skills and $HOME/.codex/skills. With no
1161
+ target, every target whose parent folder exists is used; naming one of claude,
1162
+ pi and codex picks it, and a usage error names them when none is detected.
1163
+ Each skill is a symlink to the folder rshow reads it from, so an upgrade of
1164
+ rshow is nothing to do. --copy copies the folder and writes a .rshow-skill
1165
+ marker in it, {"version": <rshow version>, "source": "rshow"}, which is how a
1166
+ newer rshow knows the copy is its own. The default is a link, and a copy when
1167
+ that folder sits in an npx cache, which npm may delete under a link, or when
1168
+ rshow runs as a single executable, whose files live in a per-version cache.
1169
+ A skill that is ours and current prints ok, and one of ours from another path or
1170
+ another version is replaced and prints updated. Anything else, a folder, a file
1171
+ or a link that rshow did not make, is left alone: skip goes to stderr, the rest
1172
+ is still installed, and the run exits 6.
1173
+ --dry-run prints what would happen, with would, and changes nothing.
1174
+ Prints one line per skill, then installed <n> skills for <targets>; --json
1175
+ prints {targets, actions: [{target, skill, action, path}]}.
1176
+ RSHOW_SKILLS_DIR reads the skills from another folder.`;
1177
+ var SKILLS_UNINSTALL_USAGE = `usage: rshow skills uninstall [target ...] [--dry-run]
1178
+
1179
+ Removes the skills rshow put in every target: a symlink into a skills folder
1180
+ that holds rshow skills, and a copy that carries the .rshow-skill marker.
1181
+ It removes nothing else, with no line and no error. With no target, every
1182
+ target whose parent folder exists is used.
1183
+ Prints removed <path> per skill, then removed <n> skills for <targets>, and
1184
+ --dry-run prints what would happen, with would, and changes nothing.`;
1185
+ var SKILLS_STATUS_USAGE = `usage: rshow skills status [target ...] [--json]
1186
+
1187
+ Prints one line per target and skill, <target> <skill> <state>, where the
1188
+ state is link -> <path>, copy v<version>, missing or foreign. It prints nothing
1189
+ for a target whose parent folder does not exist, and --json prints {targets,
1190
+ actions: [{target, skill, action, path}]}.
1191
+ A copy of another version also warns on stderr, because rshow skills install
1192
+ replaces it.`;
679
1193
  var LIST_USAGE = `usage: rshow list [--project p] [--tag t] [--q text]
680
1194
 
681
1195
  Lists artifacts you own or that are shared with you.
@@ -697,14 +1211,19 @@ var DELETE_USAGE = `usage: rshow delete <slug> --yes
697
1211
  Deletes the artifact and needs --yes.`;
698
1212
  var LOGIN_USAGE = `usage: rshow login [--url u]
699
1213
 
700
- Reads the token from stdin, checks it against the API, then saves it in the
701
- macOS Keychain. On a terminal it asks for the token with echo off, so it is
702
- never typed on a command line or pasted anywhere. The API base URL is the
703
- account, so each server keeps its own token; --url logs in to another one.
704
- RSHOW_TOKEN still wins over the saved token.`;
1214
+ Reads the token from stdin, checks it with the API, then saves it in the macOS
1215
+ Keychain, in the Linux keyring, or in $XDG_CONFIG_HOME/rshow/credentials with
1216
+ mode 600: the first store this machine has, and it reads it back to confirm.
1217
+ On a terminal it asks for the token with echo off, so it is never typed on a
1218
+ command line or pasted anywhere. The API base URL is the account, so each
1219
+ server keeps its own token; --url logs in to another one.
1220
+ Prints the email, the scope and where the token went.
1221
+ RSHOW_TOKEN still wins over the saved token, and RSHOW_TOKEN_STORE forces one
1222
+ of keychain, keyring or file.`;
705
1223
  var LOGOUT_USAGE = `usage: rshow logout [--url u]
706
1224
 
707
- Removes the token this server has in the macOS Keychain.`;
1225
+ Removes the token this server has from every store that holds it: the macOS
1226
+ Keychain, the Linux keyring and the 0600 credentials file.`;
708
1227
  var PROJECT_USAGE = `usage: rshow project <create|list|show|share|delete> [options]
709
1228
 
710
1229
  project create <slug> [--title t] [--description d]
@@ -830,14 +1349,14 @@ function usageLine(usage) {
830
1349
  }
831
1350
 
832
1351
  // src/commands/publish.ts
833
- import { stat as stat3 } from "node:fs/promises";
834
- import path6 from "node:path";
1352
+ import { stat as stat5 } from "node:fs/promises";
1353
+ import path8 from "node:path";
835
1354
 
836
1355
  // src/args.ts
837
1356
  import { parseArgs } from "node:util";
838
1357
  var LIST_FLAGS = /* @__PURE__ */ new Set(["--add", "--editor", "--remove", "--tag"]);
839
1358
  var LOWERCASE_TAG = new RegExp(TAG_PATTERN.source, "i");
840
- function messageOf3(err) {
1359
+ function messageOf4(err) {
841
1360
  return err instanceof Error ? err.message : String(err);
842
1361
  }
843
1362
  function isListValue(token) {
@@ -873,7 +1392,7 @@ function parseCommandArgs(argv, options, minPositionals, maxPositionals, hint) {
873
1392
  try {
874
1393
  parsed = parseArgs({ args: expanded, options, allowPositionals: true, strict: true });
875
1394
  } catch (err) {
876
- throw new UsageError(`${messageOf3(err)} (try: ${usageLine(hint)})`);
1395
+ throw new UsageError(`${messageOf4(err)} (try: ${usageLine(hint)})`);
877
1396
  }
878
1397
  const count = parsed.positionals.length;
879
1398
  if (count < minPositionals || count > maxPositionals) {
@@ -905,21 +1424,21 @@ function emailList(args, name) {
905
1424
 
906
1425
  // src/build.ts
907
1426
  import { spawn as spawn2 } from "node:child_process";
908
- import process3 from "node:process";
909
- function messageOf4(err) {
1427
+ import process10 from "node:process";
1428
+ function messageOf5(err) {
910
1429
  return err instanceof Error ? err.message : String(err);
911
1430
  }
912
1431
  function shellPath(env) {
913
1432
  const shell = (env.SHELL ?? "").trim();
914
1433
  return shell === "" ? "/bin/sh" : shell;
915
1434
  }
916
- async function runBuild(dir, command, env = process3.env) {
1435
+ async function runBuild(dir, command, env = process10.env) {
917
1436
  const code = await new Promise((resolve2, reject) => {
918
1437
  const child = spawn2(shellPath(env), ["-c", command], { cwd: dir, stdio: "inherit" });
919
1438
  child.once("error", reject);
920
1439
  child.once("close", (value) => resolve2(value));
921
1440
  }).catch((err) => {
922
- throw new BuildError(`build failed: ${messageOf4(err)}`);
1441
+ throw new BuildError(`build failed: ${messageOf5(err)}`);
923
1442
  });
924
1443
  if (code !== 0) {
925
1444
  throw new BuildError(`build failed: ${code === null ? "killed by a signal" : `exit ${code}`}`);
@@ -927,12 +1446,12 @@ async function runBuild(dir, command, env = process3.env) {
927
1446
  }
928
1447
 
929
1448
  // src/check.ts
930
- import { readFile as readFile4, readdir, stat } from "node:fs/promises";
931
- import path4 from "node:path";
1449
+ import { readFile as readFile6, readdir, stat as stat3 } from "node:fs/promises";
1450
+ import path6 from "node:path";
932
1451
 
933
1452
  // src/mockup.ts
934
- import { readFile as readFile2 } from "node:fs/promises";
935
- import path2 from "node:path";
1453
+ import { readFile as readFile4 } from "node:fs/promises";
1454
+ import path4 from "node:path";
936
1455
  var MAX_SCAN_BYTES = 4 * 1024 * 1024;
937
1456
  var HTML_EXTENSIONS = /* @__PURE__ */ new Set([".html", ".htm"]);
938
1457
  var MIN_COLS = 80;
@@ -1002,20 +1521,20 @@ function idOf(value) {
1002
1521
  const id = String(value ?? "").trim().replace(/^#/, "");
1003
1522
  return id === "" ? void 0 : id;
1004
1523
  }
1005
- function warn(scan, line, message) {
1524
+ function warn2(scan, line, message) {
1006
1525
  scan.out.push({ path: scan.path, line, message });
1007
1526
  }
1008
1527
  function keyHint(scan, masked, from, to, line) {
1009
1528
  const hit = TERMINAL_KEYS.exec(masked.slice(from, to).replace(ANY_TAG, ""));
1010
1529
  if (hit === null) return;
1011
- warn(scan, line, `key hint binds the terminal's key: ${hit[0].trim()}`);
1530
+ warn2(scan, line, `key hint binds the terminal's key: ${hit[0].trim()}`);
1012
1531
  }
1013
1532
  function sizeGate(scan, attrs, line) {
1014
1533
  if (attrs.has("data-allow-small")) return;
1015
1534
  const cols = sizeOf(attrs, "data-cols", MIN_COLS);
1016
1535
  const rows = sizeOf(attrs, "data-rows", MIN_ROWS);
1017
1536
  if (cols >= MIN_COLS && rows >= MIN_ROWS) return;
1018
- warn(scan, line, `tui below the 80x24 minimum (${cols}x${rows}), or mark it with data-allow-small`);
1537
+ warn2(scan, line, `tui below the 80x24 minimum (${cols}x${rows}), or mark it with data-allow-small`);
1019
1538
  }
1020
1539
  function walk(scan, masked) {
1021
1540
  const stack = [];
@@ -1095,25 +1614,25 @@ function resolve(scan) {
1095
1614
  ids.add(item.id);
1096
1615
  const count = (seen.get(item.id) ?? 0) + 1;
1097
1616
  seen.set(item.id, count);
1098
- if (count === 2) warn(scan, item.line, `duplicate artboard id: ${item.id}`);
1617
+ if (count === 2) warn2(scan, item.line, `duplicate artboard id: ${item.id}`);
1099
1618
  }
1100
1619
  const unknown = /* @__PURE__ */ new Set();
1101
1620
  for (const flow of scan.flows) {
1102
1621
  if (ids.has(flow.id) || unknown.has(flow.id)) continue;
1103
1622
  unknown.add(flow.id);
1104
- warn(scan, flow.line, `flow to #${flow.id} is not an artboard id in this file`);
1623
+ warn2(scan, flow.line, `flow to #${flow.id} is not an artboard id in this file`);
1105
1624
  }
1106
1625
  for (const item of scan.boards) {
1107
- if (!item.title) warn(scan, item.line, `artboard ${item.label} has no data-title`);
1626
+ if (!item.title) warn2(scan, item.line, `artboard ${item.label} has no data-title`);
1108
1627
  for (const block of item.blocks) {
1109
1628
  const missing = block.states.filter((state) => state !== "default" && !item.states.includes(state));
1110
1629
  if (missing.length === 0) continue;
1111
- warn(scan, block.line, `block data-state "${missing.join(",")}" is not in the artboard data-states`);
1630
+ warn2(scan, block.line, `block data-state "${missing.join(",")}" is not in the artboard data-states`);
1112
1631
  }
1113
1632
  for (const state of item.states) {
1114
1633
  if (state === "default") continue;
1115
1634
  if (item.blocks.some((block) => block.states.includes(state))) continue;
1116
- warn(scan, item.line, `artboard ${item.label} lists state "${state}" that no block uses`);
1635
+ warn2(scan, item.line, `artboard ${item.label} lists state "${state}" that no block uses`);
1117
1636
  }
1118
1637
  }
1119
1638
  }
@@ -1127,9 +1646,9 @@ function htmlWarnings(rel, text) {
1127
1646
  async function mockupWarnings(entries) {
1128
1647
  const out = [];
1129
1648
  for (const entry of entries) {
1130
- const ext = path2.extname(entry.rel).toLowerCase();
1649
+ const ext = path4.extname(entry.rel).toLowerCase();
1131
1650
  if (!HTML_EXTENSIONS.has(ext) || entry.size > MAX_SCAN_BYTES) continue;
1132
- const text = await readFile2(entry.abs, "utf8").catch(() => void 0);
1651
+ const text = await readFile4(entry.abs, "utf8").catch(() => void 0);
1133
1652
  if (text === void 0) continue;
1134
1653
  out.push(...htmlWarnings(entry.rel, text));
1135
1654
  }
@@ -1137,8 +1656,8 @@ async function mockupWarnings(entries) {
1137
1656
  }
1138
1657
 
1139
1658
  // src/style.ts
1140
- import { readFile as readFile3 } from "node:fs/promises";
1141
- import path3 from "node:path";
1659
+ import { readFile as readFile5 } from "node:fs/promises";
1660
+ import path5 from "node:path";
1142
1661
  var MAX_SCAN_BYTES2 = 4 * 1024 * 1024;
1143
1662
  var STYLE_EXTENSIONS = /* @__PURE__ */ new Set([".html", ".htm", ".css"]);
1144
1663
  var GENERIC_FAMILIES = /* @__PURE__ */ new Set([
@@ -1535,9 +2054,9 @@ async function styleWarnings(entries) {
1535
2054
  const loaded = /* @__PURE__ */ new Set();
1536
2055
  const used = [];
1537
2056
  for (const entry of entries) {
1538
- const ext = path3.extname(entry.rel).toLowerCase();
2057
+ const ext = path5.extname(entry.rel).toLowerCase();
1539
2058
  if (!STYLE_EXTENSIONS.has(ext) || entry.size > MAX_SCAN_BYTES2) continue;
1540
- const text = await readFile3(entry.abs, "utf8").catch(() => void 0);
2059
+ const text = await readFile5(entry.abs, "utf8").catch(() => void 0);
1541
2060
  if (text === void 0) continue;
1542
2061
  collectLoaded(text, loaded);
1543
2062
  if (stylePathSkipped(entry.rel)) continue;
@@ -1588,9 +2107,6 @@ var CONNECT_PATTERNS = [
1588
2107
  pattern: /\.open\s*\(\s*["'`][A-Za-z]+["'`]\s*,\s*["'`](https?:\/\/[^"'`\s)]+)/g
1589
2108
  }
1590
2109
  ];
1591
- function messageOf5(err) {
1592
- return err instanceof Error ? err.message : String(err);
1593
- }
1594
2110
  function add(scan, file, line, severity, message) {
1595
2111
  scan.findings.push({ path: file, line, severity, message });
1596
2112
  }
@@ -1641,7 +2157,7 @@ function attributes2(raw) {
1641
2157
  }
1642
2158
  return found;
1643
2159
  }
1644
- function hostOf(value) {
2160
+ function hostOf2(value) {
1645
2161
  if (!EXTERNAL.test(value) || IGNORED.test(value)) return void 0;
1646
2162
  try {
1647
2163
  const host = new URL(value, BASE_URL).hostname;
@@ -1665,7 +2181,7 @@ function resolvePath(file, ref) {
1665
2181
  decoded = ref;
1666
2182
  }
1667
2183
  const wantsIndex = decoded.endsWith("/");
1668
- const base = path4.posix.dirname(file);
2184
+ const base = path6.posix.dirname(file);
1669
2185
  const parts2 = base === "." ? [] : base.split("/");
1670
2186
  for (const segment of decoded.split("/")) {
1671
2187
  if (segment === "" || segment === ".") continue;
@@ -1716,7 +2232,7 @@ function resolveRef(scan, file, raw, line) {
1716
2232
  }
1717
2233
  function checkScript(scan, file, line, attrs) {
1718
2234
  const value = (attrs.get("src") ?? "").trim();
1719
- const host = hostOf(value);
2235
+ const host = hostOf2(value);
1720
2236
  if (host === void 0) return;
1721
2237
  if (!SCRIPT_HOSTS.includes(host)) {
1722
2238
  add(scan, file, line, "error", `script host not allowed: ${value} (allowed: ${SCRIPT_HOSTS.join(", ")})`);
@@ -1732,7 +2248,7 @@ function checkLink(scan, file, line, attrs) {
1732
2248
  const rel = (attrs.get("rel") ?? "").toLowerCase().split(/\s+/);
1733
2249
  if (!rel.includes("stylesheet")) return;
1734
2250
  const value = (attrs.get("href") ?? "").trim();
1735
- const host = hostOf(value);
2251
+ const host = hostOf2(value);
1736
2252
  if (host === void 0 || STYLE_HOSTS.includes(host)) return;
1737
2253
  add(scan, file, line, "error", `stylesheet host not allowed: ${value} (allowed: ${STYLE_HOSTS.join(", ")})`);
1738
2254
  }
@@ -1788,9 +2304,9 @@ function scanHtml(scan, file, text, facts) {
1788
2304
  scanConnect(scan, file, masked, at);
1789
2305
  }
1790
2306
  async function scanFile(scan, entry, facts) {
1791
- const ext = path4.extname(entry.rel).toLowerCase();
2307
+ const ext = path6.extname(entry.rel).toLowerCase();
1792
2308
  if (!SCAN_EXTENSIONS.has(ext) || entry.size > MAX_SCAN_BYTES3) return;
1793
- const text = await readFile4(entry.abs, "utf8");
2309
+ const text = await readFile6(entry.abs, "utf8");
1794
2310
  if (ext === ".html" || ext === ".htm") {
1795
2311
  scanHtml(scan, entry.rel, text, facts);
1796
2312
  return;
@@ -1805,7 +2321,7 @@ async function collect(scan, dir, prefix, entries) {
1805
2321
  for (const item of items) {
1806
2322
  if (item.name.startsWith(".") || SKIPPED_NAMES.has(item.name)) continue;
1807
2323
  const rel = prefix === "" ? item.name : `${prefix}/${item.name}`;
1808
- const abs = path4.join(dir, item.name);
2324
+ const abs = path6.join(dir, item.name);
1809
2325
  const problem = pathProblem(rel);
1810
2326
  if (problem !== void 0) add(scan, rel, 1, "error", problem);
1811
2327
  if (item.isDirectory()) {
@@ -1813,54 +2329,56 @@ async function collect(scan, dir, prefix, entries) {
1813
2329
  continue;
1814
2330
  }
1815
2331
  if (!item.isFile()) continue;
1816
- const info = await stat(abs).catch(() => void 0);
2332
+ const info = await stat3(abs).catch(() => void 0);
1817
2333
  if (info === void 0) continue;
1818
2334
  entries.push({ rel, abs, size: info.size });
1819
2335
  }
1820
2336
  }
1821
2337
  async function checkThemeFile(scan, key2, rel, where) {
1822
2338
  const url = `${scan.art}/_themes/${key2}/${rel}`;
1823
- let status;
2339
+ let status2;
2340
+ await readyNetwork();
1824
2341
  try {
1825
2342
  const reply = await scan.fetchImpl(url, { method: "HEAD" });
1826
- status = reply.status;
2343
+ status2 = reply.status;
1827
2344
  } catch (err) {
1828
- add(scan, where.file, where.line, "warning", `cannot check theme ${key2}/${rel}: ${messageOf5(err)}`);
2345
+ add(scan, where.file, where.line, "warning", `cannot check theme ${key2}/${rel}: ${networkDetail(err, url)}`);
1829
2346
  return;
1830
2347
  }
1831
- if (status === 404) add(scan, where.file, where.line, "error", `theme ${key2} has no ${rel} (${url})`);
1832
- else if (status < 200 || status > 399) {
1833
- add(scan, where.file, where.line, "warning", `cannot check theme ${key2}/${rel}: HTTP ${status}`);
2348
+ if (status2 === 404) add(scan, where.file, where.line, "error", `theme ${key2} has no ${rel} (${url})`);
2349
+ else if (status2 < 200 || status2 > 399) {
2350
+ add(scan, where.file, where.line, "warning", `cannot check theme ${key2}/${rel}: HTTP ${status2}`);
1834
2351
  }
1835
2352
  }
1836
2353
  async function checkTheme(scan, key2, where) {
1837
2354
  if (scan.offline || scan.art === "") return;
1838
2355
  const url = `${scan.art}/_themes/${key2}/tokens.css`;
1839
- let status;
2356
+ let status2;
2357
+ await readyNetwork();
1840
2358
  try {
1841
2359
  const reply = await scan.fetchImpl(url, { method: "HEAD" });
1842
- status = reply.status;
2360
+ status2 = reply.status;
1843
2361
  } catch (err) {
1844
- add(scan, where.file, where.line, "warning", `cannot check theme ${key2}: ${messageOf5(err)}`);
2362
+ add(scan, where.file, where.line, "warning", `cannot check theme ${key2}: ${networkDetail(err, url)}`);
1845
2363
  return;
1846
2364
  }
1847
- if (status === 404) {
2365
+ if (status2 === 404) {
1848
2366
  add(scan, where.file, where.line, "error", `theme ${key2} is not published (${url})`);
1849
2367
  return;
1850
2368
  }
1851
- if (status < 200 || status > 399) {
1852
- add(scan, where.file, where.line, "warning", `cannot check theme ${key2}: HTTP ${status}`);
2369
+ if (status2 < 200 || status2 > 399) {
2370
+ add(scan, where.file, where.line, "warning", `cannot check theme ${key2}: HTTP ${status2}`);
1853
2371
  return;
1854
2372
  }
1855
- if (status > 299) return;
2373
+ if (status2 > 299) return;
1856
2374
  for (const [rel, ref] of where.files) {
1857
2375
  if (rel === "tokens.css") continue;
1858
2376
  await checkThemeFile(scan, key2, rel, ref);
1859
2377
  }
1860
2378
  }
1861
2379
  async function checkFolder(input, opts = {}) {
1862
- const root = path4.resolve(input);
1863
- const info = await stat(root).catch(() => void 0);
2380
+ const root2 = path6.resolve(input);
2381
+ const info = await stat3(root2).catch(() => void 0);
1864
2382
  if (info === void 0 || !info.isDirectory()) throw new UsageError(`no such folder: ${input}`);
1865
2383
  const scan = {
1866
2384
  files: /* @__PURE__ */ new Set(),
@@ -1871,7 +2389,7 @@ async function checkFolder(input, opts = {}) {
1871
2389
  themes: /* @__PURE__ */ new Map()
1872
2390
  };
1873
2391
  const entries = [];
1874
- await collect(scan, root, "", entries);
2392
+ await collect(scan, root2, "", entries);
1875
2393
  let bytes = 0;
1876
2394
  for (const entry of entries) {
1877
2395
  scan.files.add(entry.rel);
@@ -1952,7 +2470,7 @@ function reportCheck(out, result) {
1952
2470
  out.line(summaryLine(result));
1953
2471
  }
1954
2472
  async function enforceCheck(out, target, opts = {}) {
1955
- const info = await stat(target).catch(() => void 0);
2473
+ const info = await stat3(target).catch(() => void 0);
1956
2474
  if (info === void 0 || !info.isDirectory()) return;
1957
2475
  const result = await checkFolder(target, opts);
1958
2476
  if (result.findings.length > 0) reportCheck(out, result);
@@ -1960,12 +2478,12 @@ async function enforceCheck(out, target, opts = {}) {
1960
2478
  }
1961
2479
 
1962
2480
  // src/publish.ts
1963
- import { readFile as readFile6 } from "node:fs/promises";
2481
+ import { readFile as readFile8 } from "node:fs/promises";
1964
2482
 
1965
2483
  // src/files.ts
1966
2484
  import { createHash } from "node:crypto";
1967
- import { readdir as readdir2, readFile as readFile5, stat as stat2 } from "node:fs/promises";
1968
- import path5 from "node:path";
2485
+ import { readdir as readdir2, readFile as readFile7, stat as stat4 } from "node:fs/promises";
2486
+ import path7 from "node:path";
1969
2487
  var CONTENT_TYPES = {
1970
2488
  html: "text/html; charset=utf-8",
1971
2489
  css: "text/css; charset=utf-8",
@@ -1997,14 +2515,14 @@ var CONTENT_TYPES = {
1997
2515
  var DEFAULT_CONTENT_TYPE = "application/octet-stream";
1998
2516
  var SKIPPED_NAMES2 = /* @__PURE__ */ new Set(["node_modules"]);
1999
2517
  function contentTypeFor(filePath) {
2000
- const ext = path5.extname(filePath).replace(/^\./, "").toLowerCase();
2518
+ const ext = path7.extname(filePath).replace(/^\./, "").toLowerCase();
2001
2519
  return CONTENT_TYPES[ext] ?? DEFAULT_CONTENT_TYPE;
2002
2520
  }
2003
2521
  function sha256Hex(bytes) {
2004
2522
  return createHash("sha256").update(bytes).digest("hex");
2005
2523
  }
2006
2524
  async function readEntry(abs, rel) {
2007
- const bytes = await readFile5(abs);
2525
+ const bytes = await readFile7(abs);
2008
2526
  return {
2009
2527
  path: rel,
2010
2528
  file: abs,
@@ -2013,7 +2531,7 @@ async function readEntry(abs, rel) {
2013
2531
  type: contentTypeFor(rel)
2014
2532
  };
2015
2533
  }
2016
- async function collectPaths(root) {
2534
+ async function collectPaths(root2) {
2017
2535
  const found = [];
2018
2536
  async function walk2(dir, prefix) {
2019
2537
  const entries = await readdir2(dir, { withFileTypes: true });
@@ -2022,7 +2540,7 @@ async function collectPaths(root) {
2022
2540
  if (entry.name.startsWith(".")) continue;
2023
2541
  if (SKIPPED_NAMES2.has(entry.name)) continue;
2024
2542
  const rel = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
2025
- const abs = path5.join(dir, entry.name);
2543
+ const abs = path7.join(dir, entry.name);
2026
2544
  if (entry.isDirectory()) {
2027
2545
  await walk2(abs, rel);
2028
2546
  continue;
@@ -2031,22 +2549,22 @@ async function collectPaths(root) {
2031
2549
  found.push(rel);
2032
2550
  }
2033
2551
  }
2034
- await walk2(root, "");
2552
+ await walk2(root2, "");
2035
2553
  return found;
2036
2554
  }
2037
2555
  async function buildManifest(input, opts = {}) {
2038
- const root = path5.resolve(input);
2039
- const info = await stat2(root).catch(() => void 0);
2556
+ const root2 = path7.resolve(input);
2557
+ const info = await stat4(root2).catch(() => void 0);
2040
2558
  if (info === void 0) throw new UsageError(`no such file or folder: ${input}`);
2041
2559
  const entries = [];
2042
2560
  if (info.isFile()) {
2043
- entries.push(await readEntry(root, "index.html"));
2561
+ entries.push(await readEntry(root2, "index.html"));
2044
2562
  } else if (info.isDirectory()) {
2045
- const paths = await collectPaths(root);
2563
+ const paths = await collectPaths(root2);
2046
2564
  if (opts.requireIndex !== false && !paths.includes("index.html")) {
2047
2565
  throw new UsageError(`${input} has no index.html`);
2048
2566
  }
2049
- for (const rel of paths) entries.push(await readEntry(path5.join(root, rel), rel));
2567
+ for (const rel of paths) entries.push(await readEntry(path7.join(root2, rel), rel));
2050
2568
  } else {
2051
2569
  throw new UsageError(`not a file or folder: ${input}`);
2052
2570
  }
@@ -2164,7 +2682,7 @@ async function publishTarget(api, target, opts) {
2164
2682
  let uploaded = 0;
2165
2683
  await runPool(missing, UPLOAD_CONCURRENCY, async (hash) => {
2166
2684
  const entry = byHash.get(hash);
2167
- await api.putBlob(hash, await readFile6(entry.file));
2685
+ await api.putBlob(hash, await readFile8(entry.file));
2168
2686
  uploaded += 1;
2169
2687
  });
2170
2688
  const version = await api.createVersion(opts.slug, { label: opts.label, files: manifest.files });
@@ -2192,7 +2710,7 @@ function sameFiles(local, remote) {
2192
2710
  const paths = Object.keys(local);
2193
2711
  const other = remote;
2194
2712
  if (paths.length !== Object.keys(other).length) return false;
2195
- return paths.every((path22) => other[path22]?.hash === local[path22]?.hash);
2713
+ return paths.every((path26) => other[path26]?.hash === local[path26]?.hash);
2196
2714
  }
2197
2715
  function reportUnchanged(out, result) {
2198
2716
  out.data({ unchanged: true, slug: result.slug, version: result.version.n });
@@ -2220,26 +2738,19 @@ function validateSlug(slug) {
2220
2738
  return slug;
2221
2739
  }
2222
2740
 
2223
- // src/warn.ts
2224
- import process4 from "node:process";
2225
- function warn2(message) {
2226
- process4.stderr.write(`rshow: warning: ${message}
2227
- `);
2228
- }
2229
-
2230
2741
  // src/commands/publish.ts
2231
2742
  async function directoryOf(input) {
2232
- const dir = path6.resolve(input);
2233
- const info = await stat3(dir).catch(() => void 0);
2743
+ const dir = path8.resolve(input);
2744
+ const info = await stat5(dir).catch(() => void 0);
2234
2745
  return info !== void 0 && info.isDirectory() ? dir : void 0;
2235
2746
  }
2236
2747
  async function targetOf(input, dir, built, out) {
2237
- if (dir === void 0) return path6.resolve(input);
2238
- if (built) return path6.resolve(dir, out);
2748
+ if (dir === void 0) return path8.resolve(input);
2749
+ if (built) return path8.resolve(dir, out);
2239
2750
  const local = await readConfigFile(localConfigPath(dir));
2240
- return local.out === void 0 ? dir : path6.resolve(dir, out);
2751
+ return local.out === void 0 ? dir : path8.resolve(dir, out);
2241
2752
  }
2242
- async function run2(argv, ctx) {
2753
+ async function run(argv, ctx) {
2243
2754
  const args = parseCommandArgs(
2244
2755
  argv,
2245
2756
  {
@@ -2266,7 +2777,7 @@ async function run2(argv, ctx) {
2266
2777
  }
2267
2778
  const target = await targetOf(input, dir, build !== void 0, config.out);
2268
2779
  const slug = validateSlug(
2269
- stringValue(args, "slug") ?? config.slug ?? deriveSlug(path6.basename(path6.resolve(input)))
2780
+ stringValue(args, "slug") ?? config.slug ?? deriveSlug(path8.basename(path8.resolve(input)))
2270
2781
  );
2271
2782
  const title = stringValue(args, "title") ?? config.title;
2272
2783
  const label = stringValue(args, "label");
@@ -2277,7 +2788,7 @@ async function run2(argv, ctx) {
2277
2788
  const opts = { slug, title, label, project, tags, force: boolValue(args, "force") };
2278
2789
  if (!boolValue(args, "no-check")) {
2279
2790
  opts.beforeUpload = () => {
2280
- if (config.art === void 0) warn2("no art host configured; skipping theme checks");
2791
+ if (config.art === void 0) warn("no art host configured; skipping theme checks");
2281
2792
  return enforceCheck(ctx.out, target, { art: config.art, offline: config.art === void 0 });
2282
2793
  };
2283
2794
  }
@@ -2301,9 +2812,9 @@ async function run2(argv, ctx) {
2301
2812
  }
2302
2813
 
2303
2814
  // src/commands/check.ts
2304
- import { stat as stat4 } from "node:fs/promises";
2305
- import path7 from "node:path";
2306
- async function run3(argv, ctx) {
2815
+ import { stat as stat6 } from "node:fs/promises";
2816
+ import path9 from "node:path";
2817
+ async function run2(argv, ctx) {
2307
2818
  const args = parseCommandArgs(
2308
2819
  argv,
2309
2820
  { offline: { type: "boolean" }, "no-style": { type: "boolean" } },
@@ -2312,13 +2823,13 @@ async function run3(argv, ctx) {
2312
2823
  CHECK_USAGE
2313
2824
  );
2314
2825
  const input = args.positionals[0];
2315
- const dir = path7.resolve(input);
2316
- const info = await stat4(dir).catch(() => void 0);
2826
+ const dir = path9.resolve(input);
2827
+ const info = await stat6(dir).catch(() => void 0);
2317
2828
  if (info === void 0 || !info.isDirectory()) throw new UsageError(`no such folder: ${input}`);
2318
2829
  const config = await ctx.config(dir);
2319
2830
  const offline = boolValue(args, "offline");
2320
2831
  const art = config.art;
2321
- if (art === void 0 && !offline) warn2("no art host configured; skipping theme checks");
2832
+ if (art === void 0 && !offline) warn("no art host configured; skipping theme checks");
2322
2833
  const result = await checkFolder(dir, {
2323
2834
  art,
2324
2835
  offline: offline || art === void 0,
@@ -2336,21 +2847,21 @@ async function run3(argv, ctx) {
2336
2847
  }
2337
2848
 
2338
2849
  // src/commands/preview.ts
2339
- import { stat as stat7 } from "node:fs/promises";
2340
- import path10 from "node:path";
2850
+ import { stat as stat9 } from "node:fs/promises";
2851
+ import path12 from "node:path";
2341
2852
 
2342
2853
  // src/serve.ts
2343
2854
  import { watch } from "node:fs";
2344
- import { mkdtemp, readFile as readFile8, rm as rm2, stat as stat6, writeFile as writeFile2 } from "node:fs/promises";
2855
+ import { mkdtemp as mkdtemp2, readFile as readFile10, rm as rm3, stat as stat8, writeFile as writeFile4 } from "node:fs/promises";
2345
2856
  import { createHash as createHash2 } from "node:crypto";
2346
2857
  import { createServer } from "node:http";
2347
2858
  import { tmpdir } from "node:os";
2348
- import path9 from "node:path";
2349
- import process5 from "node:process";
2859
+ import path11 from "node:path";
2860
+ import process11 from "node:process";
2350
2861
 
2351
2862
  // src/themes.ts
2352
- import { cp, mkdir, readFile as readFile7, rm, stat as stat5, writeFile } from "node:fs/promises";
2353
- import path8 from "node:path";
2863
+ import { cp, mkdir as mkdir3, readFile as readFile9, rm as rm2, stat as stat7, writeFile as writeFile3 } from "node:fs/promises";
2864
+ import path10 from "node:path";
2354
2865
  import { fileURLToPath as fileURLToPath2 } from "node:url";
2355
2866
 
2356
2867
  // src/color.ts
@@ -2682,11 +3193,21 @@ function flattenDtcg(tree) {
2682
3193
  }
2683
3194
 
2684
3195
  // src/themes.ts
2685
- var HERE = path8.dirname(fileURLToPath2(import.meta.url));
2686
- var BUILTIN_BASES = [
2687
- path8.resolve(HERE, "../../../themes"),
2688
- path8.resolve(HERE, "../themes")
2689
- ];
3196
+ function sourceDir() {
3197
+ try {
3198
+ const url = import.meta.url;
3199
+ return typeof url === "string" && url !== "" ? path10.dirname(fileURLToPath2(url)) : void 0;
3200
+ } catch {
3201
+ return void 0;
3202
+ }
3203
+ }
3204
+ var HERE = sourceDir();
3205
+ function nearSource(...segments) {
3206
+ if (HERE === void 0) return [];
3207
+ return [path10.resolve(HERE, "..", "..", "..", ...segments), path10.resolve(HERE, "..", ...segments)];
3208
+ }
3209
+ var BUILTIN_BASES = nearSource("themes");
3210
+ var BUILTIN_SKILL_BASES = nearSource("skills");
2690
3211
  var ROLE_TOKENS = ["--bg", "--fg", "--muted", "--accent", "--layer-01"];
2691
3212
  var TEXT_ROLES = ["--fg", "--muted", "--accent"];
2692
3213
  var GROUND_TOKENS = ["--bg", "--layer-01"];
@@ -2701,7 +3222,7 @@ var FONT_FORMATS = {
2701
3222
  ".ttf": "truetype",
2702
3223
  ".otf": "opentype"
2703
3224
  };
2704
- function isRecord2(value) {
3225
+ function isRecord3(value) {
2705
3226
  return value !== null && typeof value === "object" && !Array.isArray(value);
2706
3227
  }
2707
3228
  function stringField(value) {
@@ -2711,16 +3232,16 @@ function messageOf6(err) {
2711
3232
  return err instanceof Error ? err.message : String(err);
2712
3233
  }
2713
3234
  async function isFile(file) {
2714
- const info = await stat5(file).catch(() => void 0);
3235
+ const info = await stat7(file).catch(() => void 0);
2715
3236
  return info !== void 0 && info.isFile();
2716
3237
  }
2717
3238
  async function isDir(dir) {
2718
- const info = await stat5(dir).catch(() => void 0);
3239
+ const info = await stat7(dir).catch(() => void 0);
2719
3240
  return info !== void 0 && info.isDirectory();
2720
3241
  }
2721
3242
  function tokenMap(value) {
2722
3243
  const out = {};
2723
- if (!isRecord2(value)) return out;
3244
+ if (!isRecord3(value)) return out;
2724
3245
  for (const [name, raw] of Object.entries(value)) {
2725
3246
  if (typeof raw === "string" && raw.trim() !== "") out[name] = raw.trim();
2726
3247
  }
@@ -2730,7 +3251,7 @@ function fontList(value) {
2730
3251
  if (!Array.isArray(value)) return [];
2731
3252
  const out = [];
2732
3253
  for (const item of value) {
2733
- if (!isRecord2(item)) continue;
3254
+ if (!isRecord3(item)) continue;
2734
3255
  const family = stringField(item.family);
2735
3256
  const file = stringField(item.file);
2736
3257
  if (family === void 0 || file === void 0) continue;
@@ -2741,8 +3262,8 @@ function fontList(value) {
2741
3262
  return out;
2742
3263
  }
2743
3264
  function themeValues(json) {
2744
- const doc = isRecord2(json) ? json : {};
2745
- const modes = isRecord2(doc.modeTokens) ? doc.modeTokens : {};
3265
+ const doc = isRecord3(json) ? json : {};
3266
+ const modes = isRecord3(doc.modeTokens) ? doc.modeTokens : {};
2746
3267
  return {
2747
3268
  tokens: tokenMap(doc.tokens),
2748
3269
  light: tokenMap(modes.light),
@@ -2750,10 +3271,10 @@ function themeValues(json) {
2750
3271
  };
2751
3272
  }
2752
3273
  async function readThemeJson(dir) {
2753
- const file = path8.join(dir, "theme.json");
3274
+ const file = path10.join(dir, "theme.json");
2754
3275
  let text;
2755
3276
  try {
2756
- text = await readFile7(file, "utf8");
3277
+ text = await readFile9(file, "utf8");
2757
3278
  } catch (err) {
2758
3279
  if (err.code === "ENOENT") throw new UsageError(`${dir} has no theme.json`);
2759
3280
  throw new UsageError(`cannot read ${file}: ${messageOf6(err)}`);
@@ -2764,17 +3285,23 @@ async function readThemeJson(dir) {
2764
3285
  } catch {
2765
3286
  throw new UsageError(`invalid JSON in ${file}`);
2766
3287
  }
2767
- if (!isRecord2(parsed)) throw new UsageError(`${file} must hold a JSON object`);
3288
+ if (!isRecord3(parsed)) throw new UsageError(`${file} must hold a JSON object`);
2768
3289
  return { dir, text, json: parsed };
2769
3290
  }
2770
3291
  async function builtinThemeDir(name, bases = BUILTIN_BASES) {
2771
3292
  if (!THEME_NAME_PATTERN.test(name)) return void 0;
2772
- for (const base of bases) {
2773
- const dir = path8.join(base, name);
3293
+ for (const base of [...await binaryThemeBases(), ...bases]) {
3294
+ const dir = path10.join(base, name);
2774
3295
  if (await isDir(dir)) return dir;
2775
3296
  }
2776
3297
  return void 0;
2777
3298
  }
3299
+ async function builtinSkillsDir(bases = BUILTIN_SKILL_BASES) {
3300
+ for (const base of [...await binarySkillBases(), ...bases]) {
3301
+ if (await isDir(base)) return base;
3302
+ }
3303
+ return void 0;
3304
+ }
2778
3305
  function stripPrint(css) {
2779
3306
  let out = "";
2780
3307
  let index = 0;
@@ -2800,8 +3327,8 @@ function stripPrint(css) {
2800
3327
  return out;
2801
3328
  }
2802
3329
  async function themeTokenSets(dir) {
2803
- const file = path8.join(dir, "tokens.css");
2804
- const text = await readFile7(file, "utf8").catch(() => void 0);
3330
+ const file = path10.join(dir, "tokens.css");
3331
+ const text = await readFile9(file, "utf8").catch(() => void 0);
2805
3332
  return extractTokens(text === void 0 ? [] : [stripPrint(text)]);
2806
3333
  }
2807
3334
  function declMap(list3) {
@@ -2823,9 +3350,9 @@ function declarations(values, indent) {
2823
3350
  function presetTokenCss(name, values) {
2824
3351
  const parts2 = [];
2825
3352
  const light = { ...values.tokens, ...values.light };
2826
- const root = declarations(light, " ");
2827
- if (root !== "") parts2.push(`:root {
2828
- ${root}
3353
+ const root2 = declarations(light, " ");
3354
+ if (root2 !== "") parts2.push(`:root {
3355
+ ${root2}
2829
3356
  }`);
2830
3357
  const dark = declarations(values.dark, " ");
2831
3358
  if (dark !== "") {
@@ -2854,7 +3381,7 @@ function presetFontCss(fonts) {
2854
3381
  lines.push(` font-weight: ${font.weight ?? "400"};`);
2855
3382
  lines.push(" font-display: swap;");
2856
3383
  const url = fontUrl(font.file);
2857
- const format = FONT_FORMATS[path8.extname(url).toLowerCase()];
3384
+ const format = FONT_FORMATS[path10.extname(url).toLowerCase()];
2858
3385
  const source = format === void 0 ? `url("${url}")` : `url("${url}") format("${format}")`;
2859
3386
  lines.push(` src: ${source};`);
2860
3387
  return `@font-face {
@@ -2879,15 +3406,15 @@ function mergeTheme(base, preset, baseName) {
2879
3406
  }
2880
3407
  async function appendFile(file, addition) {
2881
3408
  if (addition === "") return;
2882
- const before = await readFile7(file, "utf8").catch(() => "");
3409
+ const before = await readFile9(file, "utf8").catch(() => "");
2883
3410
  const glue = before === "" || before.endsWith("\n") ? "" : "\n";
2884
- await writeFile(file, `${before}${glue}${addition}`);
3411
+ await writeFile3(file, `${before}${glue}${addition}`);
2885
3412
  }
2886
3413
  async function buildPreset(presetDir, outDir) {
2887
3414
  const preset = await readThemeJson(presetDir);
2888
3415
  const baseName = stringField(preset.json.extends);
2889
- await rm(outDir, { recursive: true, force: true });
2890
- await mkdir(outDir, { recursive: true });
3416
+ await rm2(outDir, { recursive: true, force: true });
3417
+ await mkdir3(outDir, { recursive: true });
2891
3418
  if (baseName === void 0) {
2892
3419
  await cp(presetDir, outDir, { recursive: true });
2893
3420
  return;
@@ -2898,15 +3425,15 @@ async function buildPreset(presetDir, outDir) {
2898
3425
  await cp(baseDir, outDir, { recursive: true });
2899
3426
  const values = themeValues(preset.json);
2900
3427
  const name = stringField(preset.json.name) ?? baseName;
2901
- await appendFile(path8.join(outDir, "tokens.css"), presetTokenCss(name, values));
3428
+ await appendFile(path10.join(outDir, "tokens.css"), presetTokenCss(name, values));
2902
3429
  const fonts = fontList(preset.json.fonts);
2903
3430
  if (fonts.length > 0) {
2904
- const from = path8.join(presetDir, "fonts");
2905
- if (await isDir(from)) await cp(from, path8.join(outDir, "fonts"), { recursive: true });
2906
- await appendFile(path8.join(outDir, "fonts.css"), presetFontCss(fonts));
3431
+ const from = path10.join(presetDir, "fonts");
3432
+ if (await isDir(from)) await cp(from, path10.join(outDir, "fonts"), { recursive: true });
3433
+ await appendFile(path10.join(outDir, "fonts.css"), presetFontCss(fonts));
2907
3434
  }
2908
3435
  const merged = mergeTheme(base.json, preset.json, baseName);
2909
- await writeFile(path8.join(outDir, "theme.json"), `${JSON.stringify(merged, null, 2)}
3436
+ await writeFile3(path10.join(outDir, "theme.json"), `${JSON.stringify(merged, null, 2)}
2910
3437
  `);
2911
3438
  }
2912
3439
  function keyLine(text, key2, mode) {
@@ -2921,8 +3448,8 @@ function keyLine(text, key2, mode) {
2921
3448
  }
2922
3449
  return 0;
2923
3450
  }
2924
- function finding(path22, line, severity, message) {
2925
- return { path: path22, line: Math.max(1, line), severity, message };
3451
+ function finding(path26, line, severity, message) {
3452
+ return { path: path26, line: Math.max(1, line), severity, message };
2926
3453
  }
2927
3454
  function resolveValue(name, map, fallback) {
2928
3455
  let current = map[name] ?? fallback[name];
@@ -3020,13 +3547,13 @@ async function checkPreset(presetDir) {
3020
3547
  }
3021
3548
  const fonts = fontList(preset.json.fonts);
3022
3549
  for (const font of fonts) {
3023
- const file = path8.join(presetDir, font.file);
3550
+ const file = path10.join(presetDir, font.file);
3024
3551
  if (!await isFile(file)) {
3025
3552
  findings.push(finding("theme.json", keyLine(text, "file"), "error", `missing font file: ${font.file}`));
3026
3553
  }
3027
3554
  }
3028
3555
  if (fonts.length > 0) {
3029
- const found = await Promise.all(LICENCE_FILES.map((rel) => isFile(path8.join(presetDir, "fonts", rel))));
3556
+ const found = await Promise.all(LICENCE_FILES.map((rel) => isFile(path10.join(presetDir, "fonts", rel))));
3030
3557
  if (!found.some(Boolean)) {
3031
3558
  findings.push(
3032
3559
  finding(
@@ -3124,7 +3651,7 @@ var SCRIPT_TYPE = "text/javascript; charset=utf-8";
3124
3651
  function messageOf7(err) {
3125
3652
  return err instanceof Error ? err.message : String(err);
3126
3653
  }
3127
- function isRecord3(value) {
3654
+ function isRecord4(value) {
3128
3655
  return value !== null && typeof value === "object" && !Array.isArray(value);
3129
3656
  }
3130
3657
  function etagOf(text) {
@@ -3144,14 +3671,14 @@ function parseValue(text) {
3144
3671
  function dataLimit(key2) {
3145
3672
  return key2.startsWith(SKETCH_DATA_KEY_PREFIX) ? MAX_SKETCH_DATA_BYTES : MAX_DATA_BYTES;
3146
3673
  }
3147
- function jsonReply(res, status, body, headers = {}) {
3148
- res.writeHead(status, { "content-type": JSON_TYPE, "cache-control": "no-store", ...headers });
3674
+ function jsonReply(res, status2, body, headers = {}) {
3675
+ res.writeHead(status2, { "content-type": JSON_TYPE, "cache-control": "no-store", ...headers });
3149
3676
  res.end(JSON.stringify(body));
3150
3677
  }
3151
- function jsonError(res, message, status) {
3152
- jsonReply(res, status, { error: message });
3678
+ function jsonError(res, message, status2) {
3679
+ jsonReply(res, status2, { error: message });
3153
3680
  }
3154
- async function resolveFile(root, pathname) {
3681
+ async function resolveFile(root2, pathname) {
3155
3682
  let decoded;
3156
3683
  try {
3157
3684
  decoded = decodeURIComponent(pathname);
@@ -3159,25 +3686,26 @@ async function resolveFile(root, pathname) {
3159
3686
  return void 0;
3160
3687
  }
3161
3688
  if (decoded.includes("\0")) return void 0;
3162
- const target = path9.resolve(root, decoded.replace(/^\/+/, ""));
3163
- const rel = path9.relative(root, target);
3164
- if (rel.startsWith("..") || path9.isAbsolute(rel)) return void 0;
3165
- const info = await stat6(target).catch(() => void 0);
3689
+ const target = path11.resolve(root2, decoded.replace(/^\/+/, ""));
3690
+ const rel = path11.relative(root2, target);
3691
+ if (rel.startsWith("..") || path11.isAbsolute(rel)) return void 0;
3692
+ const info = await stat8(target).catch(() => void 0);
3166
3693
  if (info === void 0) return void 0;
3167
3694
  if (info.isFile()) return target;
3168
3695
  if (!info.isDirectory()) return void 0;
3169
- const index = path9.join(target, "index.html");
3170
- const indexInfo = await stat6(index).catch(() => void 0);
3696
+ const index = path11.join(target, "index.html");
3697
+ const indexInfo = await stat8(index).catch(() => void 0);
3171
3698
  if (indexInfo === void 0 || !indexInfo.isFile()) return void 0;
3172
3699
  return index;
3173
3700
  }
3174
3701
  async function proxy(art, pathname, method, res) {
3175
3702
  let reply;
3703
+ await readyNetwork();
3176
3704
  try {
3177
3705
  reply = await fetch(art + pathname, { method: method === "HEAD" ? "HEAD" : "GET" });
3178
3706
  } catch (err) {
3179
3707
  res.writeHead(502, { "content-type": TEXT_TYPE, "x-rshow-source": "proxy" });
3180
- res.end(`proxy error: ${messageOf7(err)}
3708
+ res.end(`proxy error: ${networkText(err, art + pathname)}
3181
3709
  `);
3182
3710
  return;
3183
3711
  }
@@ -3193,10 +3721,10 @@ async function proxy(art, pathname, method, res) {
3193
3721
  async function prepareTheme(dir) {
3194
3722
  const doc = await readThemeJson(dir);
3195
3723
  const declared = typeof doc.json.name === "string" ? doc.json.name.trim() : "";
3196
- const name = declared === "" ? path9.basename(dir) : declared;
3724
+ const name = declared === "" ? path11.basename(dir) : declared;
3197
3725
  const base = typeof doc.json.extends === "string" ? doc.json.extends.trim() : "";
3198
3726
  if (base === "") return { name, source: dir, built: dir, label: "local" };
3199
- const built = await mkdtemp(path9.join(tmpdir(), "rshow-theme-"));
3727
+ const built = await mkdtemp2(path11.join(tmpdir(), "rshow-theme-"));
3200
3728
  await buildPreset(dir, built);
3201
3729
  return { name, source: dir, built, label: "preset" };
3202
3730
  }
@@ -3267,9 +3795,9 @@ function createDataStore() {
3267
3795
  const values = /* @__PURE__ */ new Map();
3268
3796
  let dir;
3269
3797
  const persist = async () => {
3270
- if (dir === void 0) dir = await mkdtemp(path9.join(tmpdir(), "rshow-data-"));
3798
+ if (dir === void 0) dir = await mkdtemp2(path11.join(tmpdir(), "rshow-data-"));
3271
3799
  const doc = Object.fromEntries(values);
3272
- await writeFile2(path9.join(dir, "data.json"), `${JSON.stringify(doc, null, 2)}
3800
+ await writeFile4(path11.join(dir, "data.json"), `${JSON.stringify(doc, null, 2)}
3273
3801
  `);
3274
3802
  };
3275
3803
  return {
@@ -3283,7 +3811,7 @@ function createDataStore() {
3283
3811
  },
3284
3812
  close: async () => {
3285
3813
  if (dir === void 0) return;
3286
- await rm2(dir, { recursive: true, force: true });
3814
+ await rm3(dir, { recursive: true, force: true });
3287
3815
  dir = void 0;
3288
3816
  }
3289
3817
  };
@@ -3315,9 +3843,9 @@ async function readBody(req, limit) {
3315
3843
  function valueText(value) {
3316
3844
  return JSON.stringify(value === void 0 ? null : value);
3317
3845
  }
3318
- async function handleData(req, res, store, key2, method) {
3846
+ async function handleData(req, res, store2, key2, method) {
3319
3847
  if (method === "GET" || method === "HEAD") {
3320
- const entry = store.get(key2);
3848
+ const entry = store2.get(key2);
3321
3849
  if (entry === void 0) {
3322
3850
  jsonError(res, "not found", 404);
3323
3851
  return;
@@ -3358,12 +3886,12 @@ async function handleData(req, res, store, key2, method) {
3358
3886
  jsonError(res, "expected a JSON body", 400);
3359
3887
  return;
3360
3888
  }
3361
- if (!isRecord3(parsed) || !("value" in parsed)) {
3889
+ if (!isRecord4(parsed) || !("value" in parsed)) {
3362
3890
  jsonError(res, "expected {value}", 400);
3363
3891
  return;
3364
3892
  }
3365
3893
  const text = valueText(parsed.value);
3366
- const existing = store.get(key2);
3894
+ const existing = store2.get(key2);
3367
3895
  const ifMatch = req.headers["if-match"] ?? null;
3368
3896
  if (ifMatch !== null && ifMatch.trim() !== "") {
3369
3897
  if (existing === void 0 || !etagMatches(ifMatch, etagOf(existing.text))) {
@@ -3371,7 +3899,7 @@ async function handleData(req, res, store, key2, method) {
3371
3899
  return;
3372
3900
  }
3373
3901
  }
3374
- if (existing === void 0 && store.count() >= MAX_DATA_KEYS) {
3902
+ if (existing === void 0 && store2.count() >= MAX_DATA_KEYS) {
3375
3903
  jsonError(res, "too many keys for this artifact", 409);
3376
3904
  return;
3377
3905
  }
@@ -3379,7 +3907,7 @@ async function handleData(req, res, store, key2, method) {
3379
3907
  jsonError(res, "value too large", 413);
3380
3908
  return;
3381
3909
  }
3382
- const written = await store.write(key2, text);
3910
+ const written = await store2.write(key2, text);
3383
3911
  jsonReply(res, 200, { ok: true, key: key2, updated_at: written.updated_at }, { etag: written.etag });
3384
3912
  }
3385
3913
  function subscribe(res, session) {
@@ -3410,7 +3938,7 @@ function injectReload(html) {
3410
3938
  ${html.slice(at)}`;
3411
3939
  }
3412
3940
  async function sendFile(res, method, session, file, headers = {}) {
3413
- const bytes = await readFile8(file);
3941
+ const bytes = await readFile10(file);
3414
3942
  const type = contentTypeFor(file);
3415
3943
  const all = { "content-type": type, "cache-control": "no-store", ...headers };
3416
3944
  if (method === "HEAD") {
@@ -3435,7 +3963,7 @@ function startWatchers(session, targets) {
3435
3963
  const done = rebuild ? buildPreset(theme.source, theme.built) : Promise.resolve();
3436
3964
  done.then(
3437
3965
  () => broadcast(session),
3438
- (err) => warn2(`cannot rebuild ${target.dir}: ${messageOf7(err)}`)
3966
+ (err) => warn(`cannot rebuild ${target.dir}: ${messageOf7(err)}`)
3439
3967
  );
3440
3968
  }, DEBOUNCE_MS);
3441
3969
  timer.unref();
@@ -3445,7 +3973,7 @@ function startWatchers(session, targets) {
3445
3973
  watcher.on("error", () => void 0);
3446
3974
  watchers.push(watcher);
3447
3975
  } catch (err) {
3448
- warn2(`cannot watch ${target.dir}: ${messageOf7(err)}`);
3976
+ warn(`cannot watch ${target.dir}: ${messageOf7(err)}`);
3449
3977
  }
3450
3978
  }
3451
3979
  return watchers;
@@ -3488,19 +4016,19 @@ async function handle(req, res, session) {
3488
4016
  }
3489
4017
  function closeOnSignals(preview2) {
3490
4018
  const stop = () => {
3491
- preview2.close().catch(() => void 0).finally(() => process5.exit(0));
4019
+ preview2.close().catch(() => void 0).finally(() => process11.exit(0));
3492
4020
  };
3493
- process5.once("SIGINT", stop);
3494
- process5.once("SIGTERM", stop);
4021
+ process11.once("SIGINT", stop);
4022
+ process11.once("SIGTERM", stop);
3495
4023
  }
3496
4024
  async function startPreview(opts) {
3497
- const dir = path9.resolve(opts.dir);
4025
+ const dir = path11.resolve(opts.dir);
3498
4026
  const art = (opts.art ?? "").replace(/\/+$/, "");
3499
4027
  const host = opts.host ?? "127.0.0.1";
3500
- const theme = opts.theme === void 0 ? void 0 : await prepareTheme(path9.resolve(opts.theme));
3501
- const root = opts.root === "theme" && theme !== void 0 ? theme.built : dir;
4028
+ const theme = opts.theme === void 0 ? void 0 : await prepareTheme(path11.resolve(opts.theme));
4029
+ const root2 = opts.root === "theme" && theme !== void 0 ? theme.built : dir;
3502
4030
  const session = {
3503
- root,
4031
+ root: root2,
3504
4032
  art,
3505
4033
  offline: opts.offline === true,
3506
4034
  reload: opts.reload !== false,
@@ -3509,8 +4037,8 @@ async function startPreview(opts) {
3509
4037
  clients: /* @__PURE__ */ new Set(),
3510
4038
  builtin: /* @__PURE__ */ new Map()
3511
4039
  };
3512
- const targets = [{ dir: root, rebuild: false }];
3513
- if (theme !== void 0 && theme.source !== root) targets.push({ dir: theme.source, rebuild: true });
4040
+ const targets = [{ dir: root2, rebuild: false }];
4041
+ if (theme !== void 0 && theme.source !== root2) targets.push({ dir: theme.source, rebuild: true });
3514
4042
  const watchers = startWatchers(session, targets);
3515
4043
  const server = createServer((req, res) => {
3516
4044
  handle(req, res, session).catch((err) => {
@@ -3525,7 +4053,7 @@ async function startPreview(opts) {
3525
4053
  session.clients.clear();
3526
4054
  await session.store.close();
3527
4055
  if (theme !== void 0 && theme.built !== theme.source) {
3528
- await rm2(theme.built, { recursive: true, force: true });
4056
+ await rm3(theme.built, { recursive: true, force: true });
3529
4057
  }
3530
4058
  };
3531
4059
  try {
@@ -3565,12 +4093,12 @@ function portOf(value) {
3565
4093
  return port;
3566
4094
  }
3567
4095
  async function requireFolder(input, what) {
3568
- const dir = path10.resolve(input);
3569
- const info = await stat7(dir).catch(() => void 0);
4096
+ const dir = path12.resolve(input);
4097
+ const info = await stat9(dir).catch(() => void 0);
3570
4098
  if (info === void 0 || !info.isDirectory()) throw new UsageError(`no such ${what}: ${input}`);
3571
4099
  return dir;
3572
4100
  }
3573
- async function run4(argv, ctx) {
4101
+ async function run3(argv, ctx) {
3574
4102
  const args = parseCommandArgs(
3575
4103
  argv,
3576
4104
  {
@@ -3589,7 +4117,7 @@ async function run4(argv, ctx) {
3589
4117
  const offline = boolValue(args, "offline");
3590
4118
  const port = portOf(stringValue(args, "port"));
3591
4119
  const config = await ctx.config(dir);
3592
- if (config.art === void 0 && !offline) warn2("no art host configured; skipping the /_themes proxy");
4120
+ if (config.art === void 0 && !offline) warn("no art host configured; skipping the /_themes proxy");
3593
4121
  const preview2 = await startPreview({
3594
4122
  dir,
3595
4123
  art: offline ? "" : config.art ?? "",
@@ -3613,7 +4141,14 @@ function themeRefOf(theme) {
3613
4141
  return { name: theme.slice(0, at), version: theme.slice(at + 1) };
3614
4142
  }
3615
4143
  async function fetchVersion(art, name) {
3616
- const reply = await fetch(`${art}/_themes/${name}.json`, { signal: AbortSignal.timeout(THEME_TIMEOUT_MS) });
4144
+ const url = `${art}/_themes/${name}.json`;
4145
+ await readyNetwork();
4146
+ let reply;
4147
+ try {
4148
+ reply = await fetch(url, { signal: AbortSignal.timeout(THEME_TIMEOUT_MS) });
4149
+ } catch (err) {
4150
+ throw new Error(networkDetail(err, url));
4151
+ }
3617
4152
  if (reply.status !== 200) throw new Error(`HTTP ${reply.status}`);
3618
4153
  const body = await reply.json();
3619
4154
  const version = body?.version;
@@ -3632,17 +4167,17 @@ async function resolveTheme2(config) {
3632
4167
  const ref = themeRefOf(theme);
3633
4168
  if (ref.version !== void 0 && ref.version !== "latest") return config;
3634
4169
  if (config.art === void 0) {
3635
- warn2("no art host configured; skipping theme version lookup");
4170
+ warn("no art host configured; skipping theme version lookup");
3636
4171
  return config;
3637
4172
  }
3638
4173
  try {
3639
4174
  return { ...config, theme: await resolvedTheme(config.art, theme) };
3640
4175
  } catch (err) {
3641
- warn2(`cannot resolve theme ${ref.name}: ${messageOf8(err)}`);
4176
+ warn(`cannot resolve theme ${ref.name}: ${messageOf8(err)}`);
3642
4177
  return config.theme === void 0 ? config : { ...config, theme: ref.name };
3643
4178
  }
3644
4179
  }
3645
- async function run5(argv, ctx) {
4180
+ async function run4(argv, ctx) {
3646
4181
  const args = parseCommandArgs(argv, { offline: { type: "boolean" } }, 0, 1, CONFIG_USAGE);
3647
4182
  const dir = args.positionals[0];
3648
4183
  const loaded = await ctx.config(dir);
@@ -3652,14 +4187,14 @@ async function run5(argv, ctx) {
3652
4187
  }
3653
4188
 
3654
4189
  // src/commands/init.ts
3655
- import { mkdir as mkdir2, readFile as readFile9, writeFile as writeFile3 } from "node:fs/promises";
3656
- import path11 from "node:path";
3657
- import process6 from "node:process";
4190
+ import { mkdir as mkdir4, readFile as readFile11, writeFile as writeFile5 } from "node:fs/promises";
4191
+ import path13 from "node:path";
4192
+ import process12 from "node:process";
3658
4193
  var LOCAL_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost"]);
3659
4194
  function messageOf9(err) {
3660
4195
  return err instanceof Error ? err.message : String(err);
3661
4196
  }
3662
- function isRecord4(value) {
4197
+ function isRecord5(value) {
3663
4198
  return value !== null && typeof value === "object" && !Array.isArray(value);
3664
4199
  }
3665
4200
  function serverUrl(value, flag) {
@@ -3681,16 +4216,16 @@ function serverUrl(value, flag) {
3681
4216
  async function rawConfig(file) {
3682
4217
  let text;
3683
4218
  try {
3684
- text = await readFile9(file, "utf8");
4219
+ text = await readFile11(file, "utf8");
3685
4220
  } catch (err) {
3686
4221
  if (err.code === "ENOENT") return {};
3687
4222
  throw new UsageError(`cannot read ${file}: ${messageOf9(err)}`);
3688
4223
  }
3689
4224
  const parsed = JSON.parse(text);
3690
- if (!isRecord4(parsed)) throw new UsageError(`${file} must hold a JSON object`);
4225
+ if (!isRecord5(parsed)) throw new UsageError(`${file} must hold a JSON object`);
3691
4226
  return parsed;
3692
4227
  }
3693
- async function run6(argv, ctx) {
4228
+ async function run5(argv, ctx) {
3694
4229
  const args = parseCommandArgs(
3695
4230
  argv,
3696
4231
  {
@@ -3706,7 +4241,7 @@ async function run6(argv, ctx) {
3706
4241
  const url = serverUrl(stringValue(args, "url"), "url");
3707
4242
  const art = serverUrl(stringValue(args, "art"), "art");
3708
4243
  const theme = (stringValue(args, "theme") ?? "").trim();
3709
- const file = globalConfigPath(process6.env);
4244
+ const file = globalConfigPath(process12.env);
3710
4245
  const current = await readConfigFile(file);
3711
4246
  const same = (have, want) => have === void 0 || have.replace(/\/+$/, "") === want;
3712
4247
  if (!boolValue(args, "force")) {
@@ -3721,8 +4256,8 @@ async function run6(argv, ctx) {
3721
4256
  raw.url = url;
3722
4257
  raw.art = art;
3723
4258
  if (theme !== "") raw.theme = theme;
3724
- await mkdir2(path11.dirname(file), { recursive: true });
3725
- await writeFile3(file, `${JSON.stringify(raw, null, 2)}
4259
+ await mkdir4(path13.dirname(file), { recursive: true });
4260
+ await writeFile5(file, `${JSON.stringify(raw, null, 2)}
3726
4261
  `, "utf8");
3727
4262
  ctx.out.data({ path: file, url, art });
3728
4263
  ctx.out.line(`wrote ${file}`);
@@ -3730,24 +4265,226 @@ async function run6(argv, ctx) {
3730
4265
  }
3731
4266
 
3732
4267
  // src/commands/doctor.ts
3733
- import { readFile as readFile10, stat as stat8 } from "node:fs/promises";
3734
- import path12 from "node:path";
3735
- import process7 from "node:process";
4268
+ import { readFile as readFile13, stat as stat11 } from "node:fs/promises";
4269
+ import path15 from "node:path";
4270
+ import process13 from "node:process";
3736
4271
  import { fileURLToPath as fileURLToPath3 } from "node:url";
4272
+
4273
+ // src/skills.ts
4274
+ import { cp as cp2, lstat, mkdir as mkdir5, readFile as readFile12, readdir as readdir3, readlink, rm as rm4, stat as stat10, symlink, writeFile as writeFile6 } from "node:fs/promises";
4275
+ import { homedir as homedir4 } from "node:os";
4276
+ import path14 from "node:path";
4277
+ var SKILL_TARGETS = ["claude", "pi", "codex"];
4278
+ var SKILL_MARKER = ".rshow-skill";
4279
+ var SKILL_SOURCE = "rshow";
4280
+ var TARGET_SEGMENTS = {
4281
+ claude: [".claude"],
4282
+ pi: [".pi", "agent"],
4283
+ codex: [".codex"]
4284
+ };
4285
+ async function isDir2(dir) {
4286
+ const info = await stat10(dir).catch(() => void 0);
4287
+ return info !== void 0 && info.isDirectory();
4288
+ }
4289
+ async function isFile2(file) {
4290
+ const info = await stat10(file).catch(() => void 0);
4291
+ return info !== void 0 && info.isFile();
4292
+ }
4293
+ function isUnder(file, dir) {
4294
+ const rel = path14.relative(dir, file);
4295
+ return rel !== "" && rel !== ".." && !rel.startsWith(`..${path14.sep}`) && !path14.isAbsolute(rel);
4296
+ }
4297
+ function homeDir(env) {
4298
+ const home = (env.HOME ?? "").trim();
4299
+ return home !== "" ? home : homedir4();
4300
+ }
4301
+ function targetParent(home, target) {
4302
+ return path14.join(home, ...TARGET_SEGMENTS[target]);
4303
+ }
4304
+ function targetSkillsDir(home, target) {
4305
+ return path14.join(targetParent(home, target), "skills");
4306
+ }
4307
+ async function detectedTargets(home) {
4308
+ const out = [];
4309
+ for (const target of SKILL_TARGETS) {
4310
+ if (await isDir2(targetParent(home, target))) out.push(target);
4311
+ }
4312
+ return out;
4313
+ }
4314
+ function parseTargets(names) {
4315
+ const out = [];
4316
+ for (const name of names) {
4317
+ const target = SKILL_TARGETS.find((item) => item === name);
4318
+ if (target === void 0) {
4319
+ throw new UsageError(`unknown target: ${name} (use ${SKILL_TARGETS.join(", ")})`);
4320
+ }
4321
+ if (!out.includes(target)) out.push(target);
4322
+ }
4323
+ return out;
4324
+ }
4325
+ function targetHint() {
4326
+ return SKILL_TARGETS.join(", ");
4327
+ }
4328
+ function defaultModeOf(dir) {
4329
+ return dir.split(path14.sep).join("/").includes("/_npx/") ? "copy" : "link";
4330
+ }
4331
+ async function skillsSource(env) {
4332
+ const override = (env.RSHOW_SKILLS_DIR ?? "").trim();
4333
+ let dir;
4334
+ if (override !== "") {
4335
+ dir = path14.resolve(override);
4336
+ if (!await isDir2(dir)) throw new UsageError(`RSHOW_SKILLS_DIR is not a folder: ${dir}`);
4337
+ } else {
4338
+ dir = await builtinSkillsDir();
4339
+ }
4340
+ if (dir === void 0) return void 0;
4341
+ const skills = [];
4342
+ for (const entry of await readdir3(dir, { withFileTypes: true }).catch(() => [])) {
4343
+ if (entry.isDirectory() && await isFile2(path14.join(dir, entry.name, "SKILL.md"))) skills.push(entry.name);
4344
+ }
4345
+ skills.sort();
4346
+ return { dir, mode: await isBinary() ? "copy" : defaultModeOf(dir), skills };
4347
+ }
4348
+ function isOurLink(target, source) {
4349
+ const resolved = path14.resolve(target);
4350
+ if (isUnder(resolved, path14.resolve(source))) return true;
4351
+ return /(^|\/)rshow\/skills\//.test(resolved.split(path14.sep).join("/"));
4352
+ }
4353
+ async function linkTargetOf(dst) {
4354
+ const value = await readlink(dst);
4355
+ return path14.isAbsolute(value) ? value : path14.resolve(path14.dirname(dst), value);
4356
+ }
4357
+ async function readMarker(dir) {
4358
+ const text = await readFile12(path14.join(dir, SKILL_MARKER), "utf8").catch(() => void 0);
4359
+ if (text === void 0) return void 0;
4360
+ let parsed;
4361
+ try {
4362
+ parsed = JSON.parse(text);
4363
+ } catch {
4364
+ return void 0;
4365
+ }
4366
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
4367
+ const doc = parsed;
4368
+ if (doc.source !== SKILL_SOURCE || typeof doc.version !== "string" || doc.version === "") return void 0;
4369
+ return { version: doc.version, source: SKILL_SOURCE };
4370
+ }
4371
+ async function inspectSkill(dst, source, skill) {
4372
+ const info = await lstat(dst).catch(() => void 0);
4373
+ if (info === void 0) return { kind: "missing" };
4374
+ if (info.isSymbolicLink()) {
4375
+ const link = await linkTargetOf(dst);
4376
+ if (!isOurLink(link, source)) return { kind: "foreign" };
4377
+ return { kind: "link", path: link, current: path14.resolve(link) === path14.join(path14.resolve(source), skill) };
4378
+ }
4379
+ if (info.isDirectory()) {
4380
+ const marker = await readMarker(dst);
4381
+ return marker === void 0 ? { kind: "foreign" } : { kind: "copy", version: marker.version };
4382
+ }
4383
+ return { kind: "foreign" };
4384
+ }
4385
+ function skillIsCurrent(state, version) {
4386
+ if (state.kind === "link") return true;
4387
+ if (state.kind === "copy") return state.version === version;
4388
+ return false;
4389
+ }
4390
+ function skillStateLabel(state) {
4391
+ switch (state.kind) {
4392
+ case "missing":
4393
+ return "missing";
4394
+ case "foreign":
4395
+ return "foreign";
4396
+ case "link":
4397
+ return `link -> ${state.path}`;
4398
+ case "copy":
4399
+ return `copy v${state.version}`;
4400
+ }
4401
+ }
4402
+ function actionFor(state, version) {
4403
+ if (state.kind === "missing") return "installed";
4404
+ if (state.kind === "foreign") return "skip";
4405
+ if (state.kind === "link") return state.current ? "ok" : "updated";
4406
+ return state.version === version ? "ok" : "updated";
4407
+ }
4408
+ async function writeSkill(dst, src, mode, version) {
4409
+ if (mode === "link") {
4410
+ await symlink(src, dst);
4411
+ return;
4412
+ }
4413
+ await cp2(src, dst, { recursive: true });
4414
+ const marker = { version, source: SKILL_SOURCE };
4415
+ await writeFile6(path14.join(dst, SKILL_MARKER), `${JSON.stringify(marker, null, 2)}
4416
+ `, "utf8");
4417
+ }
4418
+ async function installSkills(options) {
4419
+ const entries = [];
4420
+ for (const target of options.targets) {
4421
+ const dir = targetSkillsDir(options.home, target);
4422
+ if (!options.dryRun) await mkdir5(dir, { recursive: true });
4423
+ for (const skill of options.source.skills) {
4424
+ const dst = path14.join(dir, skill);
4425
+ const src = path14.join(options.source.dir, skill);
4426
+ const state = await inspectSkill(dst, options.source.dir, skill);
4427
+ const action = actionFor(state, options.version);
4428
+ if (!options.dryRun && (action === "installed" || action === "updated")) {
4429
+ if (action === "updated") await rm4(dst, { recursive: true, force: true });
4430
+ await writeSkill(dst, src, options.mode, options.version);
4431
+ }
4432
+ entries.push({ target, skill, action, path: dst });
4433
+ }
4434
+ }
4435
+ return entries;
4436
+ }
4437
+ async function uninstallSkills(options) {
4438
+ const entries = [];
4439
+ for (const target of options.targets) {
4440
+ const dir = targetSkillsDir(options.home, target);
4441
+ for (const skill of (await readdir3(dir).catch(() => [])).sort()) {
4442
+ const dst = path14.join(dir, skill);
4443
+ const state = await inspectSkill(dst, options.source.dir, skill);
4444
+ if (state.kind !== "link" && state.kind !== "copy") continue;
4445
+ if (!options.dryRun) await rm4(dst, { recursive: true, force: true });
4446
+ entries.push({ target, skill, action: "removed", path: dst });
4447
+ }
4448
+ }
4449
+ return entries;
4450
+ }
4451
+ async function skillsStatus(options) {
4452
+ const out = [];
4453
+ for (const target of options.targets) {
4454
+ const dir = targetSkillsDir(options.home, target);
4455
+ for (const skill of options.source.skills) {
4456
+ const dst = path14.join(dir, skill);
4457
+ out.push({ target, skill, state: await inspectSkill(dst, options.source.dir, skill), path: dst });
4458
+ }
4459
+ }
4460
+ return out;
4461
+ }
4462
+
4463
+ // src/commands/doctor.ts
3737
4464
  var TIMEOUT_MS = 5e3;
3738
4465
  function messageOf10(err) {
3739
4466
  return err instanceof Error ? err.message : String(err);
3740
4467
  }
3741
- async function isFile2(file) {
3742
- const info = await stat8(file).catch(() => void 0);
4468
+ async function isFile3(file) {
4469
+ const info = await stat11(file).catch(() => void 0);
3743
4470
  return info !== void 0 && info.isFile();
3744
4471
  }
4472
+ function sourceDir2() {
4473
+ try {
4474
+ const url = import.meta.url;
4475
+ return typeof url === "string" && url !== "" ? path15.dirname(fileURLToPath3(url)) : void 0;
4476
+ } catch {
4477
+ return void 0;
4478
+ }
4479
+ }
3745
4480
  async function packageJsonPath() {
3746
- let dir = path12.dirname(fileURLToPath3(import.meta.url));
4481
+ const start = sourceDir2();
4482
+ if (start === void 0) return void 0;
4483
+ let dir = start;
3747
4484
  for (let i = 0; i < 6; i += 1) {
3748
- const file = path12.join(dir, "package.json");
3749
- if (await isFile2(file)) return file;
3750
- const up = path12.dirname(dir);
4485
+ const file = path15.join(dir, "package.json");
4486
+ if (await isFile3(file)) return file;
4487
+ const up = path15.dirname(dir);
3751
4488
  if (up === dir) break;
3752
4489
  dir = up;
3753
4490
  }
@@ -3771,36 +4508,41 @@ function minimumOf(range) {
3771
4508
  }
3772
4509
  async function nodeCheck() {
3773
4510
  const name = "node";
3774
- const current = process7.versions.node;
4511
+ const current = process13.versions.node;
3775
4512
  const file = await packageJsonPath();
3776
- if (file === void 0) return { name, status: "warn", detail: `v${current} (package.json not found)` };
4513
+ if (file === void 0) {
4514
+ if (await isBinary()) return { name, status: "ok", detail: `v${current} (inside the binary)` };
4515
+ return { name, status: "warn", detail: `v${current} (package.json not found)` };
4516
+ }
3777
4517
  let range;
3778
4518
  try {
3779
- const body = JSON.parse(await readFile10(file, "utf8"));
4519
+ const body = JSON.parse(await readFile13(file, "utf8"));
3780
4520
  range = typeof body.engines?.node === "string" ? body.engines.node : "";
3781
4521
  } catch (err) {
3782
4522
  return { name, status: "warn", detail: `v${current} (${messageOf10(err)})` };
3783
4523
  }
3784
4524
  const needed = minimumOf(range);
3785
4525
  if (needed === void 0) return { name, status: "warn", detail: `v${current} (no engines.node)` };
3786
- const status = atLeast(versionParts(current), needed) ? "ok" : "warn";
3787
- return { name, status, detail: `v${current} (needs ${range})` };
4526
+ const status2 = atLeast(versionParts(current), needed) ? "ok" : "warn";
4527
+ return { name, status: status2, detail: `v${current} (needs ${range})` };
3788
4528
  }
3789
4529
  async function tokenOf(url) {
3790
- const env = (process7.env.RSHOW_TOKEN ?? "").trim();
4530
+ const env = (process13.env.RSHOW_TOKEN ?? "").trim();
3791
4531
  if (env !== "") return { token: env, source: "env" };
3792
- if (url === void 0) return { source: "none" };
4532
+ if (url === void 0) return {};
3793
4533
  try {
3794
- const saved = await read(url);
3795
- if (saved !== void 0) return { token: saved, source: "keychain" };
3796
- return { source: "none" };
4534
+ const store2 = await choose(process13.env);
4535
+ const saved = await store2.read(url);
4536
+ if (saved !== void 0) return { token: saved, source: store2.detail };
4537
+ return {};
3797
4538
  } catch (err) {
3798
- return { source: "none", error: messageOf10(err) };
4539
+ return { error: messageOf10(err) };
3799
4540
  }
3800
4541
  }
3801
4542
  async function getJson(url, token) {
3802
4543
  const headers = {};
3803
4544
  if (token !== void 0) headers.authorization = `Bearer ${token}`;
4545
+ await readyNetwork();
3804
4546
  const reply = await fetch(url, { headers, signal: AbortSignal.timeout(TIMEOUT_MS) });
3805
4547
  if (reply.status !== 200) return { status: reply.status };
3806
4548
  const body = await reply.json().catch(() => void 0);
@@ -3826,7 +4568,7 @@ function tokenCheck(token) {
3826
4568
  const reason = token.error === void 0 ? "" : ` (${token.error})`;
3827
4569
  return { name, status: "fail", detail: `none${reason}: run rshow login, or set RSHOW_TOKEN` };
3828
4570
  }
3829
- return { name, status: "ok", detail: `source: ${token.source}` };
4571
+ return { name, status: "ok", detail: `source: ${token.source ?? "none"}` };
3830
4572
  }
3831
4573
  async function apiCheck(url, token) {
3832
4574
  const name = "api";
@@ -3840,7 +4582,7 @@ async function apiCheck(url, token) {
3840
4582
  const scope = typeof body.scope === "string" ? body.scope : "";
3841
4583
  return { name, status: "ok", detail: `${email} (scope ${scope})` };
3842
4584
  } catch (err) {
3843
- return { name, status: "fail", detail: messageOf10(err) };
4585
+ return { name, status: "fail", detail: networkDetail(err, `${url}/api/me`) };
3844
4586
  }
3845
4587
  }
3846
4588
  async function artCheck(art) {
@@ -3855,7 +4597,7 @@ async function artCheck(art) {
3855
4597
  }
3856
4598
  return { name, status: "ok", detail: `version ${version}` };
3857
4599
  } catch (err) {
3858
- return { name, status: "warn", detail: messageOf10(err) };
4600
+ return { name, status: "warn", detail: networkDetail(err, `${art}/_themes/default.json`) };
3859
4601
  }
3860
4602
  }
3861
4603
  async function themeCheck(config) {
@@ -3872,12 +4614,28 @@ async function themeCheck(config) {
3872
4614
  return { name, status: "warn", detail: messageOf10(err) };
3873
4615
  }
3874
4616
  }
3875
- async function run7(argv, ctx) {
4617
+ async function skillsCheck() {
4618
+ const name = "skills";
4619
+ const home = homeDir(process13.env);
4620
+ const targets = await detectedTargets(home);
4621
+ if (targets.length === 0) return { name, status: "ok", detail: "no target detected" };
4622
+ const source = await skillsSource(process13.env).catch(() => void 0);
4623
+ if (source === void 0) return { name, status: "warn", detail: "no skills folder next to rshow" };
4624
+ const list3 = await skillsStatus({ home, source, targets });
4625
+ const problems = [];
4626
+ for (const target of targets) {
4627
+ const bad = list3.filter((item) => item.target === target && !skillIsCurrent(item.state, VERSION)).map((item) => `${item.skill} ${skillStateLabel(item.state)}`);
4628
+ if (bad.length > 0) problems.push(`${target}: ${bad.join(", ")}`);
4629
+ }
4630
+ if (problems.length === 0) return { name, status: "ok", detail: targets.join(", ") };
4631
+ return { name, status: "warn", detail: `${problems.join("; ")} (run rshow skills install)` };
4632
+ }
4633
+ async function run6(argv, ctx) {
3876
4634
  const args = parseCommandArgs(argv, {}, 0, 1, DOCTOR_USAGE);
3877
4635
  const dir = args.positionals[0];
3878
4636
  const found = [];
3879
- for (const file of [globalConfigPath(process7.env), dir === void 0 ? void 0 : localConfigPath(dir)]) {
3880
- if (file !== void 0 && await isFile2(file)) found.push(file);
4637
+ for (const file of [globalConfigPath(process13.env), dir === void 0 ? void 0 : localConfigPath(dir)]) {
4638
+ if (file !== void 0 && await isFile3(file)) found.push(file);
3881
4639
  }
3882
4640
  let config;
3883
4641
  let configError;
@@ -3888,7 +4646,7 @@ async function run7(argv, ctx) {
3888
4646
  }
3889
4647
  const token = await tokenOf(config?.url);
3890
4648
  const checks = [
3891
- { name: "rshow", status: "ok", detail: VERSION },
4649
+ { name: "rshow", status: "ok", detail: await isBinary() ? `${VERSION} (binary)` : VERSION },
3892
4650
  await nodeCheck(),
3893
4651
  configCheck(found, config, configError),
3894
4652
  tokenCheck(token),
@@ -3897,19 +4655,20 @@ async function run7(argv, ctx) {
3897
4655
  ];
3898
4656
  const theme = await themeCheck(config);
3899
4657
  if (theme !== void 0) checks.push(theme);
4658
+ checks.push(await skillsCheck());
3900
4659
  ctx.out.data({ checks });
3901
4660
  for (const check2 of checks) ctx.out.line(`${check2.status} ${check2.name} ${check2.detail}`);
3902
4661
  return checks.some((check2) => check2.status === "fail") ? EXIT.check : EXIT.ok;
3903
4662
  }
3904
4663
 
3905
4664
  // src/commands/theme.ts
3906
- import { mkdir as mkdir3, mkdtemp as mkdtemp2, rm as rm3, stat as stat10, writeFile as writeFile4 } from "node:fs/promises";
4665
+ import { mkdir as mkdir6, mkdtemp as mkdtemp3, rm as rm5, stat as stat13, writeFile as writeFile7 } from "node:fs/promises";
3907
4666
  import { tmpdir as tmpdir2 } from "node:os";
3908
- import path14 from "node:path";
4667
+ import path17 from "node:path";
3909
4668
 
3910
4669
  // src/theme.ts
3911
- import { readFile as readFile11, stat as stat9 } from "node:fs/promises";
3912
- import path13 from "node:path";
4670
+ import { readFile as readFile14, stat as stat12 } from "node:fs/promises";
4671
+ import path16 from "node:path";
3913
4672
  function themeSlug(name) {
3914
4673
  return `theme-${name}`;
3915
4674
  }
@@ -3917,10 +4676,10 @@ function themeUrl(name, n) {
3917
4676
  return `/_themes/${name}@${n}/`;
3918
4677
  }
3919
4678
  async function readThemeMeta(dir) {
3920
- const file = path13.join(dir, "theme.json");
4679
+ const file = path16.join(dir, "theme.json");
3921
4680
  let text;
3922
4681
  try {
3923
- text = await readFile11(file, "utf8");
4682
+ text = await readFile14(file, "utf8");
3924
4683
  } catch (err) {
3925
4684
  if (err.code === "ENOENT") throw new UsageError(`${dir} has no theme.json`);
3926
4685
  throw new UsageError(`cannot read ${file}: ${err instanceof Error ? err.message : String(err)}`);
@@ -3941,8 +4700,8 @@ async function readThemeMeta(dir) {
3941
4700
  return { name };
3942
4701
  }
3943
4702
  async function requireThemeDir(input) {
3944
- const dir = path13.resolve(input);
3945
- const info = await stat9(dir).catch(() => void 0);
4703
+ const dir = path16.resolve(input);
4704
+ const info = await stat12(dir).catch(() => void 0);
3946
4705
  if (info === void 0 || !info.isDirectory()) throw new UsageError(`no such folder: ${input}`);
3947
4706
  return dir;
3948
4707
  }
@@ -3950,7 +4709,7 @@ async function requireThemeDir(input) {
3950
4709
  // src/commands/theme.ts
3951
4710
  async function presetCheck(dir) {
3952
4711
  const findings = await checkPreset(dir);
3953
- const info = await stat10(path14.join(dir, "theme.json")).catch(() => void 0);
4712
+ const info = await stat13(path17.join(dir, "theme.json")).catch(() => void 0);
3954
4713
  const errors = findings.filter((finding2) => finding2.severity === "error").length;
3955
4714
  return { findings, errors, warnings: findings.length - errors, files: 1, bytes: info?.size ?? 0 };
3956
4715
  }
@@ -3979,14 +4738,14 @@ async function publish(argv, ctx) {
3979
4738
  if (!boolValue(args, "no-check")) {
3980
4739
  if (preset) await enforcePresetCheck(ctx.out, dir);
3981
4740
  else {
3982
- if (config.art === void 0) warn2("no art host configured; skipping theme checks");
4741
+ if (config.art === void 0) warn("no art host configured; skipping theme checks");
3983
4742
  await enforceCheck(ctx.out, dir, { art: config.art, offline: config.art === void 0 });
3984
4743
  }
3985
4744
  }
3986
4745
  let target = dir;
3987
4746
  let temp;
3988
4747
  if (preset) {
3989
- temp = await mkdtemp2(path14.join(tmpdir2(), "rshow-theme-"));
4748
+ temp = await mkdtemp3(path17.join(tmpdir2(), "rshow-theme-"));
3990
4749
  await buildPreset(dir, temp);
3991
4750
  target = temp;
3992
4751
  }
@@ -4032,7 +4791,7 @@ async function publish(argv, ctx) {
4032
4791
  reportPublish(ctx.out, { ...result, artifact });
4033
4792
  ctx.out.line(url);
4034
4793
  } finally {
4035
- if (temp !== void 0) await rm3(temp, { recursive: true, force: true });
4794
+ if (temp !== void 0) await rm5(temp, { recursive: true, force: true });
4036
4795
  }
4037
4796
  }
4038
4797
  async function list(argv, ctx) {
@@ -4074,14 +4833,14 @@ async function create(argv, ctx) {
4074
4833
  const from = stringValue(args, "from") ?? "default";
4075
4834
  const baseDir = await builtinThemeDir(from);
4076
4835
  if (baseDir === void 0) throw new UsageError(`no built-in theme: ${from}`);
4077
- const parent = path14.resolve(stringValue(args, "dir") ?? ".");
4078
- const target = path14.join(parent, name);
4079
- const exists = await stat10(target).catch(() => void 0);
4080
- if (exists !== void 0) throw new UsageError(`${target} already exists`);
4836
+ const parent = path17.resolve(stringValue(args, "dir") ?? ".");
4837
+ const target = path17.join(parent, name);
4838
+ const exists2 = await stat13(target).catch(() => void 0);
4839
+ if (exists2 !== void 0) throw new UsageError(`${target} already exists`);
4081
4840
  const values = await baseRoleValues(baseDir, ROLE_TOKENS);
4082
4841
  const doc = { name, extends: from, tokens: {}, modeTokens: values };
4083
- await mkdir3(target, { recursive: true });
4084
- await writeFile4(path14.join(target, "theme.json"), `${JSON.stringify(doc, null, 2)}
4842
+ await mkdir6(target, { recursive: true });
4843
+ await writeFile7(path17.join(target, "theme.json"), `${JSON.stringify(doc, null, 2)}
4085
4844
  `);
4086
4845
  ctx.out.data({ path: target, name, extends: from });
4087
4846
  ctx.out.line(target);
@@ -4116,7 +4875,7 @@ async function preview(argv, ctx) {
4116
4875
  const port = portOf(stringValue(args, "port"));
4117
4876
  const offline = boolValue(args, "offline");
4118
4877
  const config = await ctx.config(dir);
4119
- if (config.art === void 0 && !offline) warn2("no art host configured; skipping the /_themes proxy");
4878
+ if (config.art === void 0 && !offline) warn("no art host configured; skipping the /_themes proxy");
4120
4879
  const server = await startPreview({
4121
4880
  dir,
4122
4881
  theme: dir,
@@ -4130,7 +4889,7 @@ async function preview(argv, ctx) {
4130
4889
  ctx.out.raw(`${server.url}/index.html`);
4131
4890
  return EXIT.ok;
4132
4891
  }
4133
- async function run8(argv, ctx) {
4892
+ async function run7(argv, ctx) {
4134
4893
  const [sub, ...rest] = argv;
4135
4894
  if (sub === void 0) throw new UsageError(`expected a theme command (try: ${usageLine(THEME_USAGE)})`);
4136
4895
  if (sub === "new") return create(rest, ctx);
@@ -4147,6 +4906,122 @@ async function run8(argv, ctx) {
4147
4906
  throw new UsageError(`unknown theme command: ${sub} (try: ${usageLine(THEME_USAGE)})`);
4148
4907
  }
4149
4908
 
4909
+ // src/commands/skills.ts
4910
+ import path18 from "node:path";
4911
+ import process14 from "node:process";
4912
+ var CHANGE_ACTIONS = /* @__PURE__ */ new Set(["installed", "updated", "removed"]);
4913
+ async function sourceOf() {
4914
+ const source = await skillsSource(process14.env);
4915
+ if (source === void 0) {
4916
+ throw new UsageError(`no skills folder next to rshow (set RSHOW_SKILLS_DIR to one)`);
4917
+ }
4918
+ return source;
4919
+ }
4920
+ async function targetsWithDetection(names, home) {
4921
+ const chosen = parseTargets(names);
4922
+ if (chosen.length > 0) return chosen;
4923
+ return detectedTargets(home);
4924
+ }
4925
+ function noTargetsError(home) {
4926
+ return new UsageError(
4927
+ `no target detected (looked for ${path18.join(home, ".claude")}, ${path18.join(home, ".pi/agent")} and ${path18.join(home, ".codex")}); name one of ${targetHint()}`
4928
+ );
4929
+ }
4930
+ function actionLine(entry, dryRun) {
4931
+ const would = dryRun && CHANGE_ACTIONS.has(entry.action) ? "would " : "";
4932
+ return `${would}${entry.action} ${entry.path}`;
4933
+ }
4934
+ function countOf(entries) {
4935
+ return entries.filter((entry) => entry.action !== "skip").length;
4936
+ }
4937
+ var DRY_RUN_VERB = { installed: "install", removed: "remove" };
4938
+ function summaryLine2(verb, entries, targets, dryRun) {
4939
+ const phrase = dryRun ? `would ${DRY_RUN_VERB[verb] ?? verb}` : verb;
4940
+ return `${phrase} ${countOf(entries)} skills for ${targets.join(", ")}`;
4941
+ }
4942
+ async function install(argv, ctx) {
4943
+ const args = parseCommandArgs(
4944
+ argv,
4945
+ { link: { type: "boolean" }, copy: { type: "boolean" }, "dry-run": { type: "boolean" } },
4946
+ 0,
4947
+ 3,
4948
+ SKILLS_INSTALL_USAGE
4949
+ );
4950
+ const link = boolValue(args, "link");
4951
+ const copy = boolValue(args, "copy");
4952
+ if (link && copy) throw new UsageError(`--link and --copy cannot be used together (try: ${usageLine(SKILLS_INSTALL_USAGE)})`);
4953
+ const home = homeDir(process14.env);
4954
+ const targets = await targetsWithDetection(args.positionals, home);
4955
+ if (targets.length === 0) throw noTargetsError(home);
4956
+ const source = await sourceOf();
4957
+ const mode = link ? "link" : copy ? "copy" : source.mode;
4958
+ const dryRun = boolValue(args, "dry-run");
4959
+ const entries = await installSkills({ home, source, targets, mode, dryRun, version: VERSION });
4960
+ ctx.out.data({ targets, actions: entries });
4961
+ const skipped = [];
4962
+ for (const entry of entries) {
4963
+ if (entry.action === "skip") skipped.push(entry.path);
4964
+ else ctx.out.line(actionLine(entry, dryRun));
4965
+ }
4966
+ ctx.out.line(summaryLine2("installed", entries, targets, dryRun));
4967
+ for (const dst of skipped) process14.stderr.write(`skip ${dst}: exists and is not from rshow
4968
+ `);
4969
+ return skipped.length > 0 ? EXIT.check : EXIT.ok;
4970
+ }
4971
+ async function uninstall(argv, ctx) {
4972
+ const args = parseCommandArgs(argv, { "dry-run": { type: "boolean" } }, 0, 3, SKILLS_UNINSTALL_USAGE);
4973
+ const home = homeDir(process14.env);
4974
+ const targets = await targetsWithDetection(args.positionals, home);
4975
+ if (targets.length === 0) return EXIT.ok;
4976
+ const source = await sourceOf();
4977
+ const dryRun = boolValue(args, "dry-run");
4978
+ const entries = await uninstallSkills({ home, source, targets, dryRun });
4979
+ ctx.out.data({ targets, actions: entries });
4980
+ for (const entry of entries) ctx.out.line(actionLine(entry, dryRun));
4981
+ ctx.out.line(summaryLine2("removed", entries, targets, dryRun));
4982
+ return EXIT.ok;
4983
+ }
4984
+ function actionOf(item) {
4985
+ if (item.state.kind === "link") return { action: "link", link: item.state.path };
4986
+ if (item.state.kind === "copy") return { action: "copy", version: item.state.version };
4987
+ return { action: item.state.kind };
4988
+ }
4989
+ async function status(argv, ctx) {
4990
+ const args = parseCommandArgs(argv, {}, 0, 3, SKILLS_STATUS_USAGE);
4991
+ const home = homeDir(process14.env);
4992
+ const targets = await targetsWithDetection(args.positionals, home);
4993
+ if (targets.length === 0) return EXIT.ok;
4994
+ const source = await sourceOf();
4995
+ const list3 = await skillsStatus({ home, source, targets });
4996
+ const targetWidth = Math.max(...targets.map((target) => target.length));
4997
+ const skillWidth = Math.max(...source.skills.map((skill) => skill.length));
4998
+ ctx.out.data({
4999
+ targets,
5000
+ actions: list3.map((item) => ({ target: item.target, skill: item.skill, path: item.path, ...actionOf(item) }))
5001
+ });
5002
+ for (const item of list3) {
5003
+ const name = `${item.target.padEnd(targetWidth)} ${item.skill.padEnd(skillWidth)}`;
5004
+ ctx.out.line(`${name} ${skillStateLabel(item.state)}`);
5005
+ if (item.state.kind === "copy" && !skillIsCurrent(item.state, VERSION)) {
5006
+ warn(`${item.path} is a copy of v${item.state.version}, this rshow is ${VERSION}: run rshow skills install`);
5007
+ }
5008
+ }
5009
+ return EXIT.ok;
5010
+ }
5011
+ async function run8(argv, ctx) {
5012
+ const [sub, ...rest] = argv;
5013
+ switch (sub) {
5014
+ case "install":
5015
+ return install(rest, ctx);
5016
+ case "uninstall":
5017
+ return uninstall(rest, ctx);
5018
+ case "status":
5019
+ return status(rest, ctx);
5020
+ default:
5021
+ throw new UsageError(`skills needs a subcommand (try: ${usageLine(SKILLS_USAGE)})`);
5022
+ }
5023
+ }
5024
+
4150
5025
  // src/commands/list.ts
4151
5026
  async function run9(argv, ctx) {
4152
5027
  const args = parseCommandArgs(
@@ -4390,7 +5265,7 @@ async function share(argv, ctx) {
4390
5265
  for (const member of members) ctx.out.line(`member ${member.email} ${member.role}`);
4391
5266
  for (const email of removals) ctx.out.line(`removed ${email}`);
4392
5267
  }
4393
- async function remove2(argv, ctx) {
5268
+ async function remove4(argv, ctx) {
4394
5269
  const args = parseCommandArgs(argv, { yes: { type: "boolean" } }, 1, 1, PROJECT_DELETE_USAGE);
4395
5270
  const slug = validateSlug(args.positionals[0]);
4396
5271
  if (!boolValue(args, "yes")) {
@@ -4410,7 +5285,7 @@ async function run14(argv, ctx) {
4410
5285
  if (sub === "list") return list2(rest, ctx);
4411
5286
  if (sub === "show") return show(rest, ctx);
4412
5287
  if (sub === "share") return share(rest, ctx);
4413
- if (sub === "delete") return remove2(rest, ctx);
5288
+ if (sub === "delete") return remove4(rest, ctx);
4414
5289
  throw new UsageError(`unknown project command: ${sub} (try: ${usageLine(PROJECT_USAGE)})`);
4415
5290
  }
4416
5291
 
@@ -4425,13 +5300,13 @@ async function run15(argv, ctx) {
4425
5300
  );
4426
5301
  const slug = validateSlug(args.positionals[0]);
4427
5302
  const add2 = normalizeTags(stringList(args, "add"));
4428
- const remove3 = normalizeTags(stringList(args, "remove"));
4429
- if (add2.length === 0 && remove3.length === 0) {
5303
+ const remove5 = normalizeTags(stringList(args, "remove"));
5304
+ if (add2.length === 0 && remove5.length === 0) {
4430
5305
  throw new UsageError(`nothing to change (try: ${usageLine(TAG_USAGE)})`);
4431
5306
  }
4432
5307
  const api = await ctx.api();
4433
5308
  const current = await api.artifact(slug);
4434
- const tags = withTags(tagsOf(current?.tags), add2, remove3);
5309
+ const tags = withTags(tagsOf(current?.tags), add2, remove5);
4435
5310
  const artifact = await api.patchArtifact(slug, { tags });
4436
5311
  ctx.out.data({ artifact, tags });
4437
5312
  ctx.out.line(`slug: ${slug}`);
@@ -4439,20 +5314,20 @@ async function run15(argv, ctx) {
4439
5314
  }
4440
5315
 
4441
5316
  // src/commands/handoff.ts
4442
- import process11 from "node:process";
5317
+ import process18 from "node:process";
4443
5318
 
4444
5319
  // src/handoff.ts
4445
- import { mkdir as mkdir4, stat as stat11, writeFile as writeFile6 } from "node:fs/promises";
4446
- import path17 from "node:path";
4447
- import process10 from "node:process";
5320
+ import { mkdir as mkdir7, stat as stat14, writeFile as writeFile9 } from "node:fs/promises";
5321
+ import path21 from "node:path";
5322
+ import process17 from "node:process";
4448
5323
 
4449
5324
  // src/cdp.ts
4450
5325
  import { spawn as spawn3 } from "node:child_process";
4451
5326
  import { existsSync, rmSync } from "node:fs";
4452
- import { mkdtemp as mkdtemp3, rm as rm4 } from "node:fs/promises";
5327
+ import { mkdtemp as mkdtemp4, rm as rm6 } from "node:fs/promises";
4453
5328
  import { tmpdir as tmpdir3 } from "node:os";
4454
- import path15 from "node:path";
4455
- import process8 from "node:process";
5329
+ import path19 from "node:path";
5330
+ import process15 from "node:process";
4456
5331
  var CHROME_CANDIDATES = [
4457
5332
  "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
4458
5333
  "/Applications/Chromium.app/Contents/MacOS/Chromium",
@@ -4472,9 +5347,9 @@ var LOAD_TIMEOUT_MS = 2e4;
4472
5347
  var liveBrowsers = /* @__PURE__ */ new Map();
4473
5348
  var signalHandlersInstalled = false;
4474
5349
  function killBrowser(child, signal) {
4475
- if (process8.platform !== "win32" && child.pid !== void 0) {
5350
+ if (process15.platform !== "win32" && child.pid !== void 0) {
4476
5351
  try {
4477
- process8.kill(-child.pid, signal);
5352
+ process15.kill(-child.pid, signal);
4478
5353
  return;
4479
5354
  } catch {
4480
5355
  }
@@ -4499,24 +5374,24 @@ function reapBrowsers() {
4499
5374
  }
4500
5375
  function onInterrupt() {
4501
5376
  reapBrowsers();
4502
- process8.exit(130);
5377
+ process15.exit(130);
4503
5378
  }
4504
5379
  function onTerminate() {
4505
5380
  reapBrowsers();
4506
- process8.exit(143);
5381
+ process15.exit(143);
4507
5382
  }
4508
5383
  function dropSignalHandlers() {
4509
5384
  if (!signalHandlersInstalled) return;
4510
5385
  signalHandlersInstalled = false;
4511
- process8.off("SIGINT", onInterrupt);
4512
- process8.off("SIGTERM", onTerminate);
5386
+ process15.off("SIGINT", onInterrupt);
5387
+ process15.off("SIGTERM", onTerminate);
4513
5388
  }
4514
5389
  function trackBrowser(child, profile) {
4515
5390
  liveBrowsers.set(child, profile);
4516
5391
  if (!signalHandlersInstalled) {
4517
5392
  signalHandlersInstalled = true;
4518
- process8.on("SIGINT", onInterrupt);
4519
- process8.on("SIGTERM", onTerminate);
5393
+ process15.on("SIGINT", onInterrupt);
5394
+ process15.on("SIGTERM", onTerminate);
4520
5395
  }
4521
5396
  }
4522
5397
  function forgetBrowser(child) {
@@ -4527,20 +5402,20 @@ var exitHookInstalled = false;
4527
5402
  function installExitHook() {
4528
5403
  if (exitHookInstalled) return;
4529
5404
  exitHookInstalled = true;
4530
- process8.once("exit", reapBrowsers);
5405
+ process15.once("exit", reapBrowsers);
4531
5406
  }
4532
5407
  function messageOf11(err) {
4533
5408
  return err instanceof Error ? err.message : String(err);
4534
5409
  }
4535
5410
  function whichInPath(name, pathValue) {
4536
- for (const dir of String(pathValue ?? "").split(path15.delimiter)) {
5411
+ for (const dir of String(pathValue ?? "").split(path19.delimiter)) {
4537
5412
  if (dir === "") continue;
4538
- const file = path15.join(dir, name);
5413
+ const file = path19.join(dir, name);
4539
5414
  if (existsSync(file)) return file;
4540
5415
  }
4541
5416
  return void 0;
4542
5417
  }
4543
- function findChrome(env = process8.env) {
5418
+ function findChrome(env = process15.env) {
4544
5419
  const explicit = (env.CHROME ?? "").trim();
4545
5420
  if (explicit !== "") {
4546
5421
  if (existsSync(explicit)) return explicit;
@@ -4711,7 +5586,7 @@ var Chrome = class _Chrome {
4711
5586
  static async launch(opts = {}) {
4712
5587
  const binary = opts.binary ?? findChrome();
4713
5588
  if (binary === void 0) throw new UsageError("no Chrome or Chromium found, set CHROME");
4714
- const profile = await mkdtemp3(path15.join(tmpdir3(), "rshow-chrome-"));
5589
+ const profile = await mkdtemp4(path19.join(tmpdir3(), "rshow-chrome-"));
4715
5590
  const child = spawn3(
4716
5591
  binary,
4717
5592
  [
@@ -4731,7 +5606,7 @@ var Chrome = class _Chrome {
4731
5606
  "--window-size=1440,900",
4732
5607
  "about:blank"
4733
5608
  ],
4734
- { stdio: ["ignore", "pipe", "pipe"], detached: process8.platform !== "win32" }
5609
+ { stdio: ["ignore", "pipe", "pipe"], detached: process15.platform !== "win32" }
4735
5610
  );
4736
5611
  installExitHook();
4737
5612
  trackBrowser(child, profile);
@@ -4742,7 +5617,7 @@ var Chrome = class _Chrome {
4742
5617
  } catch (err) {
4743
5618
  forgetBrowser(child);
4744
5619
  killBrowser(child, "SIGKILL");
4745
- await rm4(profile, { recursive: true, force: true }).catch(() => {
5620
+ await rm6(profile, { recursive: true, force: true }).catch(() => {
4746
5621
  });
4747
5622
  if (err instanceof ApiError || err instanceof UsageError) throw err;
4748
5623
  throw new ApiError(`cannot start chrome: ${messageOf11(err)}`);
@@ -4757,7 +5632,7 @@ var Chrome = class _Chrome {
4757
5632
  killBrowser(this.child, "SIGTERM");
4758
5633
  await delay(50);
4759
5634
  if (this.child.exitCode === null) killBrowser(this.child, "SIGKILL");
4760
- await rm4(this.profile, PROFILE_REMOVE).catch(() => {
5635
+ await rm6(this.profile, PROFILE_REMOVE).catch(() => {
4761
5636
  });
4762
5637
  forgetBrowser(this.child);
4763
5638
  }
@@ -4863,16 +5738,16 @@ var Page = class _Page {
4863
5738
  };
4864
5739
 
4865
5740
  // src/map.ts
4866
- import { readFile as readFile12, writeFile as writeFile5 } from "node:fs/promises";
4867
- import path16 from "node:path";
4868
- import process9 from "node:process";
5741
+ import { readFile as readFile15, writeFile as writeFile8 } from "node:fs/promises";
5742
+ import path20 from "node:path";
5743
+ import process16 from "node:process";
4869
5744
  var MAP_FILE = "rshow.map.json";
4870
- function isRecord5(value) {
5745
+ function isRecord6(value) {
4871
5746
  return value !== null && typeof value === "object" && !Array.isArray(value);
4872
5747
  }
4873
5748
  function textMap(file, key2, value) {
4874
5749
  if (value === void 0 || value === null) return {};
4875
- if (!isRecord5(value)) throw new UsageError(`${file}: ${key2} must be an object`);
5750
+ if (!isRecord6(value)) throw new UsageError(`${file}: ${key2} must be an object`);
4876
5751
  const out = {};
4877
5752
  for (const [name, target] of Object.entries(value)) {
4878
5753
  if (typeof target !== "string") throw new UsageError(`${file}: ${key2}.${name} must be a string`);
@@ -4881,12 +5756,12 @@ function textMap(file, key2, value) {
4881
5756
  return out;
4882
5757
  }
4883
5758
  function mapPath(dir) {
4884
- return path16.resolve(dir ?? process9.cwd(), MAP_FILE);
5759
+ return path20.resolve(dir ?? process16.cwd(), MAP_FILE);
4885
5760
  }
4886
5761
  async function readMapFile(file) {
4887
5762
  let text;
4888
5763
  try {
4889
- text = await readFile12(file, "utf8");
5764
+ text = await readFile15(file, "utf8");
4890
5765
  } catch (err) {
4891
5766
  if (err.code === "ENOENT") return void 0;
4892
5767
  throw new UsageError(`cannot read ${file}: ${err instanceof Error ? err.message : String(err)}`);
@@ -4897,7 +5772,7 @@ async function readMapFile(file) {
4897
5772
  } catch {
4898
5773
  throw new UsageError(`invalid JSON in ${file}`);
4899
5774
  }
4900
- if (!isRecord5(parsed)) throw new UsageError(`${file} must hold a JSON object`);
5775
+ if (!isRecord6(parsed)) throw new UsageError(`${file} must hold a JSON object`);
4901
5776
  return {
4902
5777
  components: textMap(file, "components", parsed.components),
4903
5778
  tokens: textMap(file, "tokens", parsed.tokens),
@@ -4905,7 +5780,7 @@ async function readMapFile(file) {
4905
5780
  };
4906
5781
  }
4907
5782
  async function writeMapFile(file, map) {
4908
- await writeFile5(file, `${JSON.stringify(sortMap(map), null, 2)}
5783
+ await writeFile8(file, `${JSON.stringify(sortMap(map), null, 2)}
4909
5784
  `);
4910
5785
  }
4911
5786
  function sortRecord(value) {
@@ -5349,14 +6224,14 @@ function parseTarget(input) {
5349
6224
  if (remote !== null) {
5350
6225
  return { kind: "remote", slug: remote[1], version: Number(remote[2]) };
5351
6226
  }
5352
- const dir = path17.resolve(input);
5353
- return { kind: "local", dir, slug: deriveSlug(path17.basename(dir)) || "handoff" };
6227
+ const dir = path21.resolve(input);
6228
+ return { kind: "local", dir, slug: deriveSlug(path21.basename(dir)) || "handoff" };
5354
6229
  }
5355
6230
  async function requireLocalDir(target) {
5356
6231
  if (target.kind !== "local") throw new UsageError("expected a local folder");
5357
- const info = await stat11(target.dir).catch(() => void 0);
6232
+ const info = await stat14(target.dir).catch(() => void 0);
5358
6233
  if (info === void 0 || !info.isDirectory()) throw new UsageError(`no such folder: ${target.dir}`);
5359
- const index = await stat11(path17.join(target.dir, "index.html")).catch(() => void 0);
6234
+ const index = await stat14(path21.join(target.dir, "index.html")).catch(() => void 0);
5360
6235
  if (index === void 0 || !index.isFile()) throw new UsageError(`${target.dir} has no index.html`);
5361
6236
  return target.dir;
5362
6237
  }
@@ -5371,10 +6246,10 @@ function safeName(text) {
5371
6246
  return cleaned === "" ? "screen" : cleaned;
5372
6247
  }
5373
6248
  function displayPath(target) {
5374
- const resolved = path17.resolve(target);
5375
- const relative = path17.relative(process10.cwd(), resolved);
6249
+ const resolved = path21.resolve(target);
6250
+ const relative = path21.relative(process17.cwd(), resolved);
5376
6251
  if (relative === "") return ".";
5377
- if (relative.startsWith("..") || path17.isAbsolute(relative)) return resolved;
6252
+ if (relative.startsWith("..") || path21.isAbsolute(relative)) return resolved;
5378
6253
  return relative;
5379
6254
  }
5380
6255
  function baselineName(screen, state, width, mode) {
@@ -5409,10 +6284,11 @@ async function collectStyleTexts(url, fetchImpl = fetch) {
5409
6284
  }
5410
6285
  async function assertPublic(url, art, fetchImpl = fetch) {
5411
6286
  let reply;
6287
+ await readyNetwork();
5412
6288
  try {
5413
6289
  reply = await fetchImpl(url, { headers: { accept: "text/html" }, redirect: "follow" });
5414
6290
  } catch (err) {
5415
- throw new UsageError(`cannot reach ${url}: ${err instanceof Error ? err.message : String(err)}`);
6291
+ throw new UsageError(`cannot reach ${url}: ${networkDetail(err, url)}`);
5416
6292
  }
5417
6293
  const host = new URL(art).host;
5418
6294
  const wandered = new URL(reply.url === "" ? url : reply.url).host !== host;
@@ -5535,7 +6411,7 @@ async function runHandoff(target, opts) {
5535
6411
  const version = target.kind === "remote" ? target.version : null;
5536
6412
  const slug = target.slug;
5537
6413
  const url = target.kind === "remote" ? `${art}/${slug}/v${version}/` : "";
5538
- const out = path17.resolve(opts.out);
6414
+ const out = path21.resolve(opts.out);
5539
6415
  let closePreview;
5540
6416
  let pageUrl = url;
5541
6417
  if (target.kind === "remote") {
@@ -5550,8 +6426,8 @@ async function runHandoff(target, opts) {
5550
6426
  const styleTexts = await collectStyleTexts(pageUrl, fetchImpl);
5551
6427
  const tokens = extractTokens(styleTexts);
5552
6428
  say(`tokens: ${tokens.light.length} light, ${tokens.dark.length} dark`);
5553
- await mkdir4(path17.join(out, "baseline"), { recursive: true });
5554
- await writeFile6(path17.join(out, "tokens.json"), `${JSON.stringify(dtcgTokens(tokens), null, 2)}
6429
+ await mkdir7(path21.join(out, "baseline"), { recursive: true });
6430
+ await writeFile9(path21.join(out, "tokens.json"), `${JSON.stringify(dtcgTokens(tokens), null, 2)}
5555
6431
  `);
5556
6432
  const screens = [];
5557
6433
  const baselines = [];
@@ -5602,7 +6478,7 @@ async function runHandoff(target, opts) {
5602
6478
  }
5603
6479
  const png = await capture(page, screen);
5604
6480
  const name = baselineName(screen.id, state, width, mode);
5605
- await writeFile6(path17.join(out, "baseline", name), png);
6481
+ await writeFile9(path21.join(out, "baseline", name), png);
5606
6482
  baselines.push({ screen: screen.id, state, width, mode, file: `baseline/${name}` });
5607
6483
  }
5608
6484
  }
@@ -5631,7 +6507,7 @@ async function runHandoff(target, opts) {
5631
6507
  screens,
5632
6508
  baselines
5633
6509
  };
5634
- await writeFile6(path17.join(out, "spec.json"), `${JSON.stringify(spec, null, 2)}
6510
+ await writeFile9(path21.join(out, "spec.json"), `${JSON.stringify(spec, null, 2)}
5635
6511
  `);
5636
6512
  const mapFile = MAP_FILE;
5637
6513
  let mapWritten = false;
@@ -5650,7 +6526,7 @@ async function runHandoff(target, opts) {
5650
6526
  createdAt,
5651
6527
  mapFile
5652
6528
  });
5653
- await writeFile6(path17.join(out, "HANDOFF.md"), markdown);
6529
+ await writeFile9(path21.join(out, "HANDOFF.md"), markdown);
5654
6530
  return {
5655
6531
  out,
5656
6532
  url: pageUrl,
@@ -5707,14 +6583,14 @@ async function run16(argv, ctx) {
5707
6583
  requireUrl(config);
5708
6584
  requireArt(config);
5709
6585
  } else if (config.art === void 0) {
5710
- warn2("no art host configured; skipping the /_themes proxy");
6586
+ warn("no art host configured; skipping the /_themes proxy");
5711
6587
  }
5712
6588
  const result = await runHandoff(target, {
5713
6589
  out,
5714
6590
  widths: widthList(stringValue(args, "widths")),
5715
6591
  modes: modeList(stringValue(args, "modes")),
5716
6592
  art: config.art ?? "",
5717
- chrome: process11.env.CHROME,
6593
+ chrome: process18.env.CHROME,
5718
6594
  onProgress: (line) => ctx.out.line(line)
5719
6595
  });
5720
6596
  const display = displayPath(result.out);
@@ -5740,11 +6616,11 @@ async function run16(argv, ctx) {
5740
6616
  }
5741
6617
 
5742
6618
  // src/commands/compare.ts
5743
- import process13 from "node:process";
6619
+ import process20 from "node:process";
5744
6620
 
5745
6621
  // src/compare.ts
5746
- import { copyFile, mkdir as mkdir5, readFile as readFile14, writeFile as writeFile7 } from "node:fs/promises";
5747
- import path19 from "node:path";
6622
+ import { copyFile, mkdir as mkdir8, readFile as readFile17, writeFile as writeFile10 } from "node:fs/promises";
6623
+ import path23 from "node:path";
5748
6624
 
5749
6625
  // src/diff.ts
5750
6626
  var MAX_Y = 35215;
@@ -5906,10 +6782,10 @@ function diffImages(before, after, opts = {}) {
5906
6782
 
5907
6783
  // src/native.ts
5908
6784
  import { spawn as spawn4 } from "node:child_process";
5909
- import { readFile as readFile13, unlink } from "node:fs/promises";
6785
+ import { readFile as readFile16, unlink as unlink2 } from "node:fs/promises";
5910
6786
  import { tmpdir as tmpdir4 } from "node:os";
5911
- import path18 from "node:path";
5912
- import process12 from "node:process";
6787
+ import path22 from "node:path";
6788
+ import process19 from "node:process";
5913
6789
  var FILE_PREFIX = "file:";
5914
6790
  var WINDOW_PREFIX = "window:";
5915
6791
  var TMUX_PREFIX = "tmux:";
@@ -5938,9 +6814,9 @@ function isNative(source) {
5938
6814
  }
5939
6815
  async function captureNative(source, mapDir, deps = {}) {
5940
6816
  if (source.kind === "file") {
5941
- const file = path18.resolve(mapDir, source.file);
6817
+ const file = path22.resolve(mapDir, source.file);
5942
6818
  try {
5943
- return new Uint8Array(await readFile13(file));
6819
+ return new Uint8Array(await readFile16(file));
5944
6820
  } catch {
5945
6821
  throw new UsageError(`no screenshot at ${source.file}, which is relative to rshow.map.json`);
5946
6822
  }
@@ -5988,8 +6864,8 @@ function noWindow(title, seen) {
5988
6864
  return new UsageError(`no window matches ${JSON.stringify(title)} (${list3}); use ${WINDOW_PREFIX} with an owner name or a title, or ${NATIVE_HINT}`);
5989
6865
  }
5990
6866
  var systemRunner = {
5991
- run: (bin2, args) => new Promise((resolve2, reject) => {
5992
- const child = spawn4(bin2, [...args], { stdio: ["ignore", "pipe", "pipe"] });
6867
+ run: (bin3, args) => new Promise((resolve2, reject) => {
6868
+ const child = spawn4(bin3, [...args], { stdio: ["ignore", "pipe", "pipe"] });
5993
6869
  let stdout = "";
5994
6870
  let stderr = "";
5995
6871
  child.stdout?.setEncoding("utf8");
@@ -6000,10 +6876,10 @@ var systemRunner = {
6000
6876
  child.stderr?.on("data", (chunk2) => {
6001
6877
  stderr += chunk2;
6002
6878
  });
6003
- child.on("error", (err) => reject(new UsageError(`cannot run ${bin2}: ${err.message}`)));
6879
+ child.on("error", (err) => reject(new UsageError(`cannot run ${bin3}: ${err.message}`)));
6004
6880
  child.on("close", (code) => {
6005
6881
  if (code !== 0) {
6006
- reject(new UsageError(stderr.trim() === "" ? `${bin2} exited ${code}` : `${bin2}: ${stderr.trim()}`));
6882
+ reject(new UsageError(stderr.trim() === "" ? `${bin3} exited ${code}` : `${bin3}: ${stderr.trim()}`));
6007
6883
  return;
6008
6884
  }
6009
6885
  resolve2(stdout);
@@ -6011,9 +6887,9 @@ var systemRunner = {
6011
6887
  })
6012
6888
  };
6013
6889
  async function captureWindow(title, deps = {}) {
6014
- const platform = deps.platform ?? process12.platform;
6890
+ const platform = deps.platform ?? process19.platform;
6015
6891
  const runner = deps.runner ?? systemRunner;
6016
- const scratch = path18.join(tmpdir4(), `rshow-window-${process12.pid}-${Date.now()}.png`);
6892
+ const scratch = path22.join(tmpdir4(), `rshow-window-${process19.pid}-${Date.now()}.png`);
6017
6893
  const wrap = (err) => err instanceof UsageError ? err : new UsageError(`window: ${title}: ${err instanceof Error ? err.message : String(err)}; ${NATIVE_HINT}`);
6018
6894
  try {
6019
6895
  if (platform === "darwin") {
@@ -6029,11 +6905,11 @@ async function captureWindow(title, deps = {}) {
6029
6905
  } else {
6030
6906
  throw new UsageError(`window: capture is not supported on ${platform}; ${NATIVE_HINT}`);
6031
6907
  }
6032
- return new Uint8Array(await readFile13(scratch));
6908
+ return new Uint8Array(await readFile16(scratch));
6033
6909
  } catch (err) {
6034
6910
  throw wrap(err);
6035
6911
  } finally {
6036
- await unlink(scratch).catch(() => {
6912
+ await unlink2(scratch).catch(() => {
6037
6913
  });
6038
6914
  }
6039
6915
  }
@@ -6070,23 +6946,23 @@ function extractPalette(image, k = 8) {
6070
6946
  const g = image.data[pos + 1];
6071
6947
  const b = image.data[pos + 2];
6072
6948
  const key2 = r >> BIN_SHIFT << BITS * 2 | g >> BIN_SHIFT << BITS | b >> BIN_SHIFT;
6073
- const bin2 = bins.get(key2);
6074
- if (bin2 === void 0) bins.set(key2, { count: 1, r, g, b });
6949
+ const bin3 = bins.get(key2);
6950
+ if (bin3 === void 0) bins.set(key2, { count: 1, r, g, b });
6075
6951
  else {
6076
- bin2.count += 1;
6077
- bin2.r += r;
6078
- bin2.g += g;
6079
- bin2.b += b;
6952
+ bin3.count += 1;
6953
+ bin3.r += r;
6954
+ bin3.g += g;
6955
+ bin3.b += b;
6080
6956
  }
6081
6957
  used += 1;
6082
6958
  }
6083
6959
  }
6084
6960
  if (used === 0) return [];
6085
6961
  const seeds = [...bins.values()].sort((left, right) => right.count - left.count).slice(0, k);
6086
- let centers = seeds.map((bin2) => ({ r: bin2.r / bin2.count, g: bin2.g / bin2.count, b: bin2.b / bin2.count }));
6087
- const points = [...bins.values()].map((bin2) => ({
6088
- color: { r: bin2.r / bin2.count, g: bin2.g / bin2.count, b: bin2.b / bin2.count },
6089
- weight: bin2.count
6962
+ let centers = seeds.map((bin3) => ({ r: bin3.r / bin3.count, g: bin3.g / bin3.count, b: bin3.b / bin3.count }));
6963
+ const points = [...bins.values()].map((bin3) => ({
6964
+ color: { r: bin3.r / bin3.count, g: bin3.g / bin3.count, b: bin3.b / bin3.count },
6965
+ weight: bin3.count
6090
6966
  }));
6091
6967
  for (let round2 = 0; round2 < ROUNDS && centers.length > 1; round2 += 1) {
6092
6968
  const sums = centers.map(() => ({ r: 0, g: 0, b: 0, weight: 0 }));
@@ -6801,9 +7677,9 @@ function compareGrid(expected, actual) {
6801
7677
  }
6802
7678
  return { rows, differing, pass: differing.length === 0 };
6803
7679
  }
6804
- function captureTmux(target, bin2 = "tmux") {
7680
+ function captureTmux(target, bin3 = "tmux") {
6805
7681
  return new Promise((resolve2, reject) => {
6806
- const child = spawn5(bin2, ["capture-pane", "-p", "-e", "-t", target], { stdio: ["ignore", "pipe", "pipe"] });
7682
+ const child = spawn5(bin3, ["capture-pane", "-p", "-e", "-t", target], { stdio: ["ignore", "pipe", "pipe"] });
6807
7683
  let stdout = "";
6808
7684
  let stderr = "";
6809
7685
  child.stdout?.setEncoding("utf8");
@@ -6815,11 +7691,11 @@ function captureTmux(target, bin2 = "tmux") {
6815
7691
  stderr += chunk2;
6816
7692
  });
6817
7693
  child.on("error", (err) => {
6818
- reject(new ApiError(`cannot run ${bin2}: ${err.message}`));
7694
+ reject(new ApiError(`cannot run ${bin3}: ${err.message}`));
6819
7695
  });
6820
7696
  child.on("close", (code) => {
6821
7697
  if (code !== 0) {
6822
- reject(new ApiError(stderr.trim() === "" ? `${bin2} exited ${code}` : stderr.trim()));
7698
+ reject(new ApiError(stderr.trim() === "" ? `${bin3} exited ${code}` : stderr.trim()));
6823
7699
  return;
6824
7700
  }
6825
7701
  resolve2(stdout);
@@ -6843,9 +7719,9 @@ var EMPTY_MAP = { components: {}, tokens: {}, screens: {} };
6843
7719
  async function readJson(file, what) {
6844
7720
  let text;
6845
7721
  try {
6846
- text = await readFile14(file, "utf8");
7722
+ text = await readFile17(file, "utf8");
6847
7723
  } catch {
6848
- throw new UsageError(`${what} is missing ${path19.basename(file)} (run rshow handoff first)`);
7724
+ throw new UsageError(`${what} is missing ${path23.basename(file)} (run rshow handoff first)`);
6849
7725
  }
6850
7726
  try {
6851
7727
  return JSON.parse(text);
@@ -6854,13 +7730,13 @@ async function readJson(file, what) {
6854
7730
  }
6855
7731
  }
6856
7732
  async function readHandoff(dir) {
6857
- const spec = await readJson(path19.join(dir, "spec.json"), dir);
7733
+ const spec = await readJson(path23.join(dir, "spec.json"), dir);
6858
7734
  if (spec === null || typeof spec !== "object" || !Array.isArray(spec.screens)) {
6859
7735
  throw new UsageError(`${dir}/spec.json does not hold a handoff spec`);
6860
7736
  }
6861
7737
  let tokens = { light: [], dark: [] };
6862
7738
  try {
6863
- tokens = flattenDtcg(JSON.parse(await readFile14(path19.join(dir, "tokens.json"), "utf8")));
7739
+ tokens = flattenDtcg(JSON.parse(await readFile17(path23.join(dir, "tokens.json"), "utf8")));
6864
7740
  } catch {
6865
7741
  tokens = { light: [], dark: [] };
6866
7742
  }
@@ -6883,15 +7759,15 @@ function clipOf(info, screenId) {
6883
7759
  async function runCompare(opts) {
6884
7760
  const say = opts.onProgress ?? (() => {
6885
7761
  });
6886
- const dir = path19.resolve(opts.against);
7762
+ const dir = path23.resolve(opts.against);
6887
7763
  const { spec, tokens } = await readHandoff(dir);
6888
- const mapFile = opts.map === void 0 ? mapPath() : path19.resolve(opts.map);
7764
+ const mapFile = opts.map === void 0 ? mapPath() : path23.resolve(opts.map);
6889
7765
  const map = await readMapFile(mapFile) ?? EMPTY_MAP;
6890
- const mapDir = path19.dirname(mapFile);
6891
- const reportDir = path19.join(dir, "compare-report");
6892
- const imagesDir = path19.join(reportDir, "images");
6893
- await mkdir5(imagesDir, { recursive: true });
6894
- await mkdir5(path19.join(dir, "diff"), { recursive: true });
7766
+ const mapDir = path23.dirname(mapFile);
7767
+ const reportDir = path23.join(dir, "compare-report");
7768
+ const imagesDir = path23.join(reportDir, "images");
7769
+ await mkdir8(imagesDir, { recursive: true });
7770
+ await mkdir8(path23.join(dir, "diff"), { recursive: true });
6895
7771
  const results = [];
6896
7772
  const collected = [];
6897
7773
  let skipped = 0;
@@ -6926,16 +7802,16 @@ async function runCompare(opts) {
6926
7802
  throw new UsageError(`tmux capture failed for ${target}: ${err instanceof Error ? err.message : String(err)}`);
6927
7803
  });
6928
7804
  const text = opts.tuiColors ? captured : stripAnsi(captured);
6929
- const baselineFile = path19.join(dir, "baseline", `${safeName(screen)}.txt`);
7805
+ const baselineFile = path23.join(dir, "baseline", `${safeName(screen)}.txt`);
6930
7806
  let expected;
6931
7807
  try {
6932
- expected = await readFile14(baselineFile, "utf8");
7808
+ expected = await readFile17(baselineFile, "utf8");
6933
7809
  } catch {
6934
7810
  expected = void 0;
6935
7811
  }
6936
7812
  if (expected === void 0) {
6937
- await mkdir5(path19.dirname(baselineFile), { recursive: true });
6938
- await writeFile7(baselineFile, text);
7813
+ await mkdir8(path23.dirname(baselineFile), { recursive: true });
7814
+ await writeFile10(baselineFile, text);
6939
7815
  results.push({
6940
7816
  screen,
6941
7817
  state: "default",
@@ -6980,12 +7856,12 @@ async function runCompare(opts) {
6980
7856
  const raw = (map.screens[screen] ?? "").trim();
6981
7857
  say(`capture: ${raw}`);
6982
7858
  let capture2;
6983
- let failure2;
7859
+ let failure;
6984
7860
  try {
6985
7861
  capture2 = decodePng(await captureNative(group.source, mapDir));
6986
7862
  } catch (err) {
6987
7863
  if (err instanceof UsageError) throw err;
6988
- failure2 = err instanceof Error ? err.message : String(err);
7864
+ failure = err instanceof Error ? err.message : String(err);
6989
7865
  }
6990
7866
  const screenSpec = spec.screens.find((item) => item.id === screen);
6991
7867
  const rubricFile = nativeName(screen);
@@ -7004,13 +7880,13 @@ async function runCompare(opts) {
7004
7880
  rubric: rubricFile,
7005
7881
  mismatches: []
7006
7882
  };
7007
- if (failure2 !== void 0 || capture2 === void 0) {
7008
- results.push({ ...base, note: `capture failed: ${failure2 ?? "no pixels"}` });
7883
+ if (failure !== void 0 || capture2 === void 0) {
7884
+ results.push({ ...base, note: `capture failed: ${failure ?? "no pixels"}` });
7009
7885
  continue;
7010
7886
  }
7011
7887
  let expectedImage;
7012
7888
  try {
7013
- expectedImage = decodePng(new Uint8Array(await readFile14(path19.join(dir, baseline.file))));
7889
+ expectedImage = decodePng(new Uint8Array(await readFile17(path23.join(dir, baseline.file))));
7014
7890
  } catch {
7015
7891
  results.push({ ...base, note: `missing baseline ${baseline.file}` });
7016
7892
  continue;
@@ -7022,10 +7898,10 @@ async function runCompare(opts) {
7022
7898
  threshold: opts.threshold
7023
7899
  });
7024
7900
  const diffPng = encodePng(diff.image);
7025
- await writeFile7(path19.join(imagesDir, `${stem}-diff.png`), diffPng);
7026
- await writeFile7(path19.join(dir, "diff", `${stem}.png`), diffPng);
7027
- await writeFile7(path19.join(imagesDir, `${stem}-capture.png`), encodePng(scaled));
7028
- await copyFile(path19.join(dir, baseline.file), path19.join(imagesDir, `${stem}-baseline.png`));
7901
+ await writeFile10(path23.join(imagesDir, `${stem}-diff.png`), diffPng);
7902
+ await writeFile10(path23.join(dir, "diff", `${stem}.png`), diffPng);
7903
+ await writeFile10(path23.join(imagesDir, `${stem}-capture.png`), encodePng(scaled));
7904
+ await copyFile(path23.join(dir, baseline.file), path23.join(imagesDir, `${stem}-baseline.png`));
7029
7905
  const expected = screenSpec === void 0 ? [] : normativeColors(screenSpec, tokens, baseline.state, baseline.mode);
7030
7906
  const palette = checkPalette(extractPalette(scaled), expected);
7031
7907
  const pixelOk = diff.ratio <= opts.nativeMaxDiffRatio;
@@ -7070,7 +7946,7 @@ async function runCompare(opts) {
7070
7946
  });
7071
7947
  }
7072
7948
  if (screenSpec !== void 0) {
7073
- await writeFile7(path19.join(reportDir, rubricFile), `${rubricMarkdown({ screen: screenSpec, shots })}
7949
+ await writeFile10(path23.join(reportDir, rubricFile), `${rubricMarkdown({ screen: screenSpec, shots })}
7074
7950
  `);
7075
7951
  }
7076
7952
  }
@@ -7127,12 +8003,12 @@ async function runCompare(opts) {
7127
8003
  }
7128
8004
  const clip = clipOf(info, screenId) ? await screenClip(page, screenId) : null;
7129
8005
  const actual = await page.screenshot(clip ?? void 0);
7130
- const actualFile = path19.join(imagesDir, `${stem}-actual.png`);
7131
- await writeFile7(actualFile, actual);
7132
- const baselineFile = path19.join(dir, baseline.file);
8006
+ const actualFile = path23.join(imagesDir, `${stem}-actual.png`);
8007
+ await writeFile10(actualFile, actual);
8008
+ const baselineFile = path23.join(dir, baseline.file);
7133
8009
  let expected;
7134
8010
  try {
7135
- expected = new Uint8Array(await readFile14(baselineFile));
8011
+ expected = new Uint8Array(await readFile17(baselineFile));
7136
8012
  } catch {
7137
8013
  results.push({
7138
8014
  screen: baseline.screen,
@@ -7147,14 +8023,14 @@ async function runCompare(opts) {
7147
8023
  });
7148
8024
  continue;
7149
8025
  }
7150
- const baselineCopy = path19.join(imagesDir, `${stem}-baseline.png`);
8026
+ const baselineCopy = path23.join(imagesDir, `${stem}-baseline.png`);
7151
8027
  await copyFile(baselineFile, baselineCopy);
7152
8028
  const left = decodePng(expected);
7153
8029
  const right = decodePng(actual);
7154
8030
  const diff = diffImages(left, right, { threshold: opts.threshold });
7155
8031
  const diffPng = encodePng(diff.image);
7156
- await writeFile7(path19.join(imagesDir, `${stem}-diff.png`), diffPng);
7157
- await writeFile7(path19.join(dir, "diff", `${stem}.png`), diffPng);
8032
+ await writeFile10(path23.join(imagesDir, `${stem}-diff.png`), diffPng);
8033
+ await writeFile10(path23.join(dir, "diff", `${stem}.png`), diffPng);
7158
8034
  const note = left.width === right.width && left.height === right.height ? void 0 : `size ${left.width}x${left.height} against ${right.width}x${right.height}`;
7159
8035
  results.push({
7160
8036
  screen: baseline.screen,
@@ -7223,8 +8099,8 @@ async function runCompare(opts) {
7223
8099
  const checked = results.filter((item) => item.source !== "").length;
7224
8100
  const review = results.filter((item) => item.status === "review").length;
7225
8101
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
7226
- await writeFile7(
7227
- path19.join(reportDir, "report.json"),
8102
+ await writeFile10(
8103
+ path23.join(reportDir, "report.json"),
7228
8104
  `${JSON.stringify(
7229
8105
  {
7230
8106
  schema: 1,
@@ -7260,8 +8136,8 @@ async function runCompare(opts) {
7260
8136
  )}
7261
8137
  `
7262
8138
  );
7263
- await writeFile7(
7264
- path19.join(reportDir, "index.html"),
8139
+ await writeFile10(
8140
+ path23.join(reportDir, "index.html"),
7265
8141
  reportHtml({
7266
8142
  spec,
7267
8143
  results,
@@ -7314,15 +8190,15 @@ function reportHtml(input) {
7314
8190
  const nativeLimit = input.nativeMaxDiffRatio ?? 0.15;
7315
8191
  const statusOf = (item) => item.status ?? (item.pass ? "pass" : "fail");
7316
8192
  const rows = results.map((item) => {
7317
- const status = statusOf(item);
7318
- const mark = status === "pass" ? '<span class="pass">pass</span>' : status === "review" ? '<span class="review">review</span>' : '<span class="fail">fail</span>';
8193
+ const status2 = statusOf(item);
8194
+ const mark = status2 === "pass" ? '<span class="pass">pass</span>' : status2 === "review" ? '<span class="review">review</span>' : '<span class="fail">fail</span>';
7319
8195
  const ratio2 = item.diffRatio === void 0 ? "-" : item.diffRatio.toFixed(4);
7320
8196
  const rubric = item.rubric === void 0 ? "" : ` <a href="${escapeHtml(item.rubric)}">rubric</a>`;
7321
8197
  return `<tr><td>${escapeHtml(item.screen)}</td><td>${escapeHtml(item.state)}</td><td>${escapeHtml(item.mode)}</td><td>${item.width === 0 ? "-" : item.width}</td><td>${mark}</td><td>${ratio2}</td><td>${item.mismatches.length}</td><td>${escapeHtml(item.note ?? "")}${rubric}</td></tr>`;
7322
8198
  }).join("\n");
7323
8199
  const sections = results.filter((item) => item.images !== void 0 || item.mismatches.length > 0).map((item) => {
7324
- const status = statusOf(item);
7325
- const badge = `<span class="badge ${status}">${status}</span>`;
8200
+ const status2 = statusOf(item);
8201
+ const badge = `<span class="badge ${status2}">${status2}</span>`;
7326
8202
  const captureCaption = item.kind === "native" ? "capture" : "actual";
7327
8203
  const shots = item.images === void 0 ? "" : `<div class="shots">
7328
8204
  <figure><img src="${escapeHtml(item.images.baseline)}" alt="baseline"><figcaption>baseline</figcaption></figure>
@@ -7412,7 +8288,7 @@ async function run17(argv, ctx) {
7412
8288
  DEFAULT_NATIVE_MAX_DIFF_RATIO
7413
8289
  ),
7414
8290
  tuiColors: boolValue(args, "tui-colors"),
7415
- chrome: process13.env.CHROME,
8291
+ chrome: process20.env.CHROME,
7416
8292
  onProgress: (line) => ctx.out.line(line)
7417
8293
  });
7418
8294
  const failing = result.results.filter((item) => !item.pass);
@@ -7427,8 +8303,8 @@ async function run17(argv, ctx) {
7427
8303
  const where = item.width === 0 ? item.mode : `${item.mode} ${item.width}`;
7428
8304
  const ratio2 = item.diffRatio === void 0 ? "" : ` diff ${item.diffRatio.toFixed(4)}`;
7429
8305
  const note = item.note === void 0 ? "" : ` (${item.note})`;
7430
- const status = item.status ?? (item.pass ? "pass" : "fail");
7431
- ctx.out.line(`${status} ${item.screen} ${item.state} ${where}${ratio2}${note}`);
8306
+ const status2 = item.status ?? (item.pass ? "pass" : "fail");
8307
+ ctx.out.line(`${status2} ${item.screen} ${item.state} ${where}${ratio2}${note}`);
7432
8308
  for (const mismatch of item.mismatches.slice(0, 20)) {
7433
8309
  ctx.out.line(
7434
8310
  ` mismatch ${item.screen} ${item.state} ${mismatch.component}: ${mismatch.property} expected ${mismatch.expected} actual ${mismatch.actual}`
@@ -7460,8 +8336,8 @@ async function run17(argv, ctx) {
7460
8336
  }
7461
8337
 
7462
8338
  // src/commands/docs.ts
7463
- import { mkdir as mkdir6, readFile as readFile15, rm as rm5, writeFile as writeFile8 } from "node:fs/promises";
7464
- import path20 from "node:path";
8339
+ import { mkdir as mkdir9, readFile as readFile18, rm as rm7, writeFile as writeFile11 } from "node:fs/promises";
8340
+ import path24 from "node:path";
7465
8341
  var USAGE = {
7466
8342
  publish: PUBLISH_USAGE,
7467
8343
  check: CHECK_USAGE,
@@ -7477,6 +8353,10 @@ var USAGE = {
7477
8353
  "theme preview": THEME_PREVIEW_USAGE,
7478
8354
  "theme publish": THEME_PUBLISH_USAGE,
7479
8355
  "theme list": THEME_LIST_USAGE,
8356
+ skills: SKILLS_USAGE,
8357
+ "skills install": SKILLS_INSTALL_USAGE,
8358
+ "skills uninstall": SKILLS_UNINSTALL_USAGE,
8359
+ "skills status": SKILLS_STATUS_USAGE,
7480
8360
  project: PROJECT_USAGE,
7481
8361
  "project create": PROJECT_CREATE_USAGE,
7482
8362
  "project list": PROJECT_LIST_USAGE,
@@ -7498,7 +8378,7 @@ var WHEN = {
7498
8378
  auth: "the API refused the token with 401 or 403.",
7499
8379
  api: "the API returned another error.",
7500
8380
  build: "the `--build` command failed.",
7501
- check: "`rshow check`, `rshow publish` or `rshow theme publish` found an error.",
8381
+ check: "`rshow check`, `rshow publish` or `rshow theme publish` found an error, or `rshow skills install` found a skill folder that is not from rshow.",
7502
8382
  compare: "`rshow compare` found a difference."
7503
8383
  };
7504
8384
  var ARG_WORD = /^(?:<[^>]+>|\[[^\]]*\]|[a-z0-9-]+@v<n>|--?\S+)$/;
@@ -7736,7 +8616,7 @@ function heading(label) {
7736
8616
  async function themeTokensPage() {
7737
8617
  const dir = await builtinThemeDir("default");
7738
8618
  if (dir === void 0) throw new Error("no built-in theme: default");
7739
- const css = await readFile15(path20.join(dir, "tokens.css"), "utf8");
8619
+ const css = await readFile18(path24.join(dir, "tokens.css"), "utf8");
7740
8620
  const end = css.indexOf("*/");
7741
8621
  const roles = parseRoles(end < 0 ? css : css.slice(0, end));
7742
8622
  const sections = parseSections(rootBlock(css));
@@ -7784,28 +8664,28 @@ function exitCodesPage() {
7784
8664
  `;
7785
8665
  }
7786
8666
  async function generate(out) {
7787
- const dir = path20.join(out, "commands");
7788
- await rm5(dir, { recursive: true, force: true });
7789
- await mkdir6(dir, { recursive: true });
8667
+ const dir = path24.join(out, "commands");
8668
+ await rm7(dir, { recursive: true, force: true });
8669
+ await mkdir9(dir, { recursive: true });
7790
8670
  const rows = commandRows();
7791
8671
  const files = [];
7792
8672
  for (const row of rows) {
7793
- const file = path20.join(dir, row.file);
7794
- await writeFile8(file, commandPage(row, rows));
8673
+ const file = path24.join(dir, row.file);
8674
+ await writeFile11(file, commandPage(row, rows));
7795
8675
  files.push(file);
7796
8676
  }
7797
- const index = path20.join(dir, "index.md");
7798
- await writeFile8(index, indexPage(rows));
8677
+ const index = path24.join(dir, "index.md");
8678
+ await writeFile11(index, indexPage(rows));
7799
8679
  files.push(index);
7800
8680
  for (const [name, text] of [
7801
8681
  ["theme-tokens.md", await themeTokensPage()],
7802
8682
  ["exit-codes.md", exitCodesPage()]
7803
8683
  ]) {
7804
- const file = path20.join(out, name);
7805
- await writeFile8(file, text);
8684
+ const file = path24.join(out, name);
8685
+ await writeFile11(file, text);
7806
8686
  files.push(file);
7807
8687
  }
7808
- return { commands: rows.length, files: files.map((file) => path20.relative(out, file)).sort() };
8688
+ return { commands: rows.length, files: files.map((file) => path24.relative(out, file)).sort() };
7809
8689
  }
7810
8690
  async function run18(argv, ctx) {
7811
8691
  const args = parseCommandArgs(argv, { out: { type: "string" } }, 0, 0, DOCS_USAGE);
@@ -7813,16 +8693,16 @@ async function run18(argv, ctx) {
7813
8693
  if (out === void 0) throw new UsageError(`docs needs --out <dir> (try: ${usageLine(DOCS_USAGE)})`);
7814
8694
  const result = await generate(out);
7815
8695
  ctx.out.data(result);
7816
- ctx.out.line(`wrote ${result.commands} command pages and the index under ${path20.join(out, "commands")}`);
7817
- ctx.out.line(`wrote ${path20.join(out, "theme-tokens.md")}`);
7818
- ctx.out.line(`wrote ${path20.join(out, "exit-codes.md")}`);
8696
+ ctx.out.line(`wrote ${result.commands} command pages and the index under ${path24.join(out, "commands")}`);
8697
+ ctx.out.line(`wrote ${path24.join(out, "theme-tokens.md")}`);
8698
+ ctx.out.line(`wrote ${path24.join(out, "exit-codes.md")}`);
7819
8699
  }
7820
8700
 
7821
8701
  // src/commands/login.ts
7822
- import process14 from "node:process";
8702
+ import process21 from "node:process";
7823
8703
  async function hiddenLine() {
7824
- process14.stderr.write("token: ");
7825
- const stdin = process14.stdin;
8704
+ process21.stderr.write("token: ");
8705
+ const stdin = process21.stdin;
7826
8706
  stdin.setRawMode(true);
7827
8707
  stdin.resume();
7828
8708
  stdin.setEncoding("utf8");
@@ -7832,7 +8712,7 @@ async function hiddenLine() {
7832
8712
  stdin.off("data", onData);
7833
8713
  stdin.setRawMode(false);
7834
8714
  stdin.pause();
7835
- process14.stderr.write("\n");
8715
+ process21.stderr.write("\n");
7836
8716
  if (err === void 0) resolve2(text);
7837
8717
  else reject(err);
7838
8718
  };
@@ -7855,16 +8735,16 @@ async function hiddenLine() {
7855
8735
  }
7856
8736
  async function pipedText() {
7857
8737
  const chunks = [];
7858
- for await (const chunk2 of process14.stdin) chunks.push(chunk2);
8738
+ for await (const chunk2 of process21.stdin) chunks.push(chunk2);
7859
8739
  return Buffer.concat(chunks).toString("utf8").trim();
7860
8740
  }
7861
8741
  async function tokenFromStdin() {
7862
- const token = process14.stdin.isTTY === true ? await hiddenLine() : await pipedText();
8742
+ const token = process21.stdin.isTTY === true ? await hiddenLine() : await pipedText();
7863
8743
  if (token === "") throw new UsageError("expected a token on stdin");
7864
8744
  if (!token.startsWith(TOKEN_PREFIX)) throw new UsageError(`that is not an rshow token: it must start with ${TOKEN_PREFIX}`);
7865
8745
  return token;
7866
8746
  }
7867
- async function run19(argv, ctx) {
8747
+ async function run19(argv, ctx, platform = process21.platform) {
7868
8748
  const args = parseCommandArgs(argv, { url: { type: "string" } }, 0, 0, LOGIN_USAGE);
7869
8749
  const settings = await ctx.config();
7870
8750
  const given = (stringValue(args, "url") ?? "").trim();
@@ -7873,32 +8753,45 @@ async function run19(argv, ctx) {
7873
8753
  const me = (await new Client(url, token).send("GET", "/api/me")).body;
7874
8754
  const email = typeof me?.email === "string" ? me.email : "";
7875
8755
  const scope = typeof me?.scope === "string" ? me.scope : "";
7876
- await save(url, token);
8756
+ const store2 = await choose(process21.env, platform);
8757
+ await store2.save(url, token);
8758
+ if (await store2.read(url) !== token) {
8759
+ throw new ApiError(`cannot save the token in ${store2.description}`);
8760
+ }
7877
8761
  ctx.out.data({ email, scope, url });
7878
- ctx.out.line(`logged in as ${email} (scope ${scope}) for ${url}`);
8762
+ ctx.out.line(`logged in as ${email} (scope ${scope}) for ${url}, saved in ${store2.description}`);
7879
8763
  }
7880
8764
 
7881
8765
  // src/commands/logout.ts
7882
- async function run20(argv, ctx) {
8766
+ import process22 from "node:process";
8767
+ async function run20(argv, ctx, platform = process22.platform) {
7883
8768
  const args = parseCommandArgs(argv, { url: { type: "string" } }, 0, 0, LOGOUT_USAGE);
7884
8769
  const settings = await ctx.config();
7885
8770
  const given = (stringValue(args, "url") ?? "").trim();
7886
8771
  const url = (given === "" ? requireUrl(settings) : given).replace(/\/+$/, "");
7887
- const removed = await remove(url);
7888
- ctx.out.line(removed ? `logged out of ${url}` : `no saved token for ${url}`);
8772
+ const removed = [];
8773
+ for (const store2 of await every(process22.env, platform)) {
8774
+ if (await store2.remove(url)) removed.push(store2.description);
8775
+ }
8776
+ if (removed.length === 0) {
8777
+ ctx.out.line(`no saved token for ${url}`);
8778
+ return;
8779
+ }
8780
+ ctx.out.line(`logged out of ${url}, removed from ${removed.join(" and ")}`);
7889
8781
  }
7890
8782
 
7891
8783
  // src/main.ts
7892
8784
  var COMMANDS = {
7893
- publish: { usage: PUBLISH_USAGE, run: run2 },
7894
- check: { usage: CHECK_USAGE, run: run3 },
8785
+ publish: { usage: PUBLISH_USAGE, run },
8786
+ check: { usage: CHECK_USAGE, run: run2 },
7895
8787
  handoff: { usage: HANDOFF_USAGE, run: run16 },
7896
8788
  compare: { usage: COMPARE_USAGE, run: run17 },
7897
- preview: { usage: PREVIEW_USAGE, run: run4 },
7898
- config: { usage: CONFIG_USAGE, run: run5 },
7899
- init: { usage: INIT_USAGE, run: run6 },
7900
- doctor: { usage: DOCTOR_USAGE, run: run7 },
7901
- theme: { usage: THEME_USAGE, run: run8 },
8789
+ preview: { usage: PREVIEW_USAGE, run: run3 },
8790
+ config: { usage: CONFIG_USAGE, run: run4 },
8791
+ init: { usage: INIT_USAGE, run: run5 },
8792
+ doctor: { usage: DOCTOR_USAGE, run: run6 },
8793
+ theme: { usage: THEME_USAGE, run: run7 },
8794
+ skills: { usage: SKILLS_USAGE, run: run8 },
7902
8795
  project: { usage: PROJECT_USAGE, run: run14 },
7903
8796
  tag: { usage: TAG_USAGE, run: run15 },
7904
8797
  list: { usage: LIST_USAGE, run: run9 },
@@ -7913,7 +8806,7 @@ var COMMANDS = {
7913
8806
  };
7914
8807
  function fail(err) {
7915
8808
  const message = err instanceof Error ? err.message : String(err);
7916
- process15.stderr.write(`rshow: ${message}
8809
+ process23.stderr.write(`rshow: ${message}
7917
8810
  `);
7918
8811
  if (err instanceof UsageError) return EXIT.usage;
7919
8812
  if (err instanceof CheckError) return EXIT.check;
@@ -7930,17 +8823,18 @@ async function main(argv) {
7930
8823
  const name = args.find((arg) => !arg.startsWith("-"));
7931
8824
  const spec = name === void 0 ? void 0 : COMMANDS[name];
7932
8825
  if (wantsVersion) {
7933
- process15.stdout.write(`rshow ${VERSION}
8826
+ const suffix = await isBinary() ? " (binary)" : "";
8827
+ process23.stdout.write(`rshow ${VERSION}${suffix}
7934
8828
  `);
7935
8829
  return EXIT.ok;
7936
8830
  }
7937
8831
  if (wantsHelp) {
7938
- process15.stdout.write(`${spec === void 0 ? GENERAL_USAGE : spec.usage}
8832
+ process23.stdout.write(`${spec === void 0 ? GENERAL_USAGE : spec.usage}
7939
8833
  `);
7940
8834
  return EXIT.ok;
7941
8835
  }
7942
8836
  if (name === void 0) {
7943
- process15.stderr.write(`${GENERAL_USAGE}
8837
+ process23.stderr.write(`${GENERAL_USAGE}
7944
8838
  `);
7945
8839
  return EXIT.usage;
7946
8840
  }
@@ -7951,12 +8845,12 @@ async function main(argv) {
7951
8845
  const rest = [...args.slice(0, index), ...args.slice(index + 1)];
7952
8846
  const configs = /* @__PURE__ */ new Map();
7953
8847
  const clients = /* @__PURE__ */ new Map();
7954
- const keyOf = (dir) => dir === void 0 ? "" : path21.resolve(dir);
8848
+ const keyOf = (dir) => dir === void 0 ? "" : path25.resolve(dir);
7955
8849
  const configFor = (dir) => {
7956
8850
  const key2 = keyOf(dir);
7957
8851
  let pending = configs.get(key2);
7958
8852
  if (pending === void 0) {
7959
- pending = loadConfig(process15.env, dir);
8853
+ pending = loadConfig(process23.env, dir);
7960
8854
  configs.set(key2, pending);
7961
8855
  }
7962
8856
  return pending;
@@ -7969,7 +8863,7 @@ async function main(argv) {
7969
8863
  if (cached !== void 0) return cached;
7970
8864
  const settings = await configFor(dir);
7971
8865
  const url = requireUrl(settings);
7972
- const client = new Client(url, await readToken(process15.env, url));
8866
+ const client = new Client(url, await readToken(process23.env, url));
7973
8867
  clients.set(key2, client);
7974
8868
  return client;
7975
8869
  },
@@ -7978,4 +8872,11 @@ async function main(argv) {
7978
8872
  const code = await spec.run(rest, ctx);
7979
8873
  return code ?? EXIT.ok;
7980
8874
  }
7981
- process15.exitCode = await main(process15.argv.slice(2)).catch(fail);
8875
+ main(process23.argv.slice(2)).then(
8876
+ (code) => {
8877
+ process23.exitCode = code;
8878
+ },
8879
+ (err) => {
8880
+ process23.exitCode = fail(err);
8881
+ }
8882
+ );