@cavuno/board 1.36.0 → 1.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,641 @@
1
+ // src/constants.ts
2
+ var DEFAULT_CAVUNO_API_URL = "https://api.cavuno.com";
3
+
4
+ // src/skills.ts
5
+ import { readFileSync } from "fs";
6
+ import { dirname, resolve } from "path";
7
+ import { fileURLToPath } from "url";
8
+ function packageRoot() {
9
+ return resolve(dirname(fileURLToPath(import.meta.url)), "..");
10
+ }
11
+ function resolveFromPackageRoot(relativePath, baseDir) {
12
+ return resolve(baseDir ?? packageRoot(), relativePath);
13
+ }
14
+ function loadSkillManifest(baseDir) {
15
+ const manifestPath = resolveFromPackageRoot("skills/manifest.json", baseDir);
16
+ return JSON.parse(readFileSync(manifestPath, "utf8"));
17
+ }
18
+ function loadSkillCorpus(baseDir) {
19
+ const manifest = loadSkillManifest(baseDir);
20
+ return {
21
+ version: manifest.version,
22
+ skills: manifest.skills.map((skill) => ({
23
+ ...skill,
24
+ content: readFileSync(
25
+ resolveFromPackageRoot(skill.path, baseDir),
26
+ "utf8"
27
+ )
28
+ }))
29
+ };
30
+ }
31
+
32
+ // src/doctor/checks.ts
33
+ function record(id, tier) {
34
+ return (status, detail) => ({
35
+ id,
36
+ tier,
37
+ status,
38
+ detail
39
+ });
40
+ }
41
+ var ENV_API_URL = record("env.api-url", 1);
42
+ var ENV_BOARD_KEY = record("env.board-key", 1);
43
+ var PK_RE = /^pk_[0-9a-f]{32}$/;
44
+ function apiBase(apiUrl) {
45
+ return apiUrl.replace(/\/+$/, "");
46
+ }
47
+ function checkEnv(env) {
48
+ const results = [];
49
+ if (!env.apiUrl) {
50
+ results.push(ENV_API_URL("fail", "PUBLIC_CAVUNO_API_URL is not set"));
51
+ } else {
52
+ let origin = null;
53
+ try {
54
+ origin = new URL(env.apiUrl).origin;
55
+ } catch {
56
+ origin = null;
57
+ }
58
+ results.push(
59
+ origin ? ENV_API_URL("pass", origin) : ENV_API_URL(
60
+ "fail",
61
+ `PUBLIC_CAVUNO_API_URL is not a valid URL: ${env.apiUrl}`
62
+ )
63
+ );
64
+ }
65
+ if (!env.boardKey) {
66
+ results.push(ENV_BOARD_KEY("fail", "PUBLIC_CAVUNO_BOARD is not set"));
67
+ } else {
68
+ results.push(
69
+ PK_RE.test(env.boardKey) ? ENV_BOARD_KEY("pass", "pk_ key") : ENV_BOARD_KEY(
70
+ "fail",
71
+ "PUBLIC_CAVUNO_BOARD must be a publishable key (pk_ + 32 hex chars)"
72
+ )
73
+ );
74
+ }
75
+ return results;
76
+ }
77
+ var JOB_DETAIL_LINK_RE = /href="(\/companies\/[a-z0-9-]+\/jobs\/[a-z0-9-]+)"/i;
78
+ function extractJobDetailLink(html) {
79
+ return html.match(JOB_DETAIL_LINK_RE)?.[1] ?? null;
80
+ }
81
+ var JSON_LD_RE = /<script[^>]*type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/gi;
82
+ function extractJobPostingJsonLd(html) {
83
+ for (const match of html.matchAll(JSON_LD_RE)) {
84
+ try {
85
+ const parsed = JSON.parse(match[1].trim());
86
+ if (parsed["@type"] === "JobPosting") return parsed;
87
+ } catch {
88
+ }
89
+ }
90
+ return null;
91
+ }
92
+ var LOC_RE = /<loc>([^<]+)<\/loc>/gi;
93
+ function parseSitemap(xml) {
94
+ const kind = /<urlset[\s>]/i.test(xml) ? "urlset" : /<sitemapindex[\s>]/i.test(xml) ? "index" : null;
95
+ if (!kind) return null;
96
+ return { kind, urls: [...xml.matchAll(LOC_RE)].map((m) => m[1].trim()) };
97
+ }
98
+ function summarize(results) {
99
+ const passed = results.filter((r) => r.status === "pass").map((r) => r.id);
100
+ const failed = results.filter((r) => r.status === "fail").map((r) => r.id);
101
+ const skipped = results.filter((r) => r.status === "skip").map((r) => r.id);
102
+ return { exitCode: failed.length > 0 ? 1 : 0, passed, failed, skipped };
103
+ }
104
+
105
+ // src/doctor/probe.ts
106
+ var FETCH_TIMEOUT_MS = 1e4;
107
+ async function probe(fetchImpl, url) {
108
+ try {
109
+ const response = await fetchImpl(url, {
110
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
111
+ });
112
+ return {
113
+ ok: response.ok,
114
+ status: response.status,
115
+ body: await response.text()
116
+ };
117
+ } catch (error) {
118
+ return {
119
+ ok: false,
120
+ status: 0,
121
+ body: error instanceof Error ? error.message : String(error)
122
+ };
123
+ }
124
+ }
125
+
126
+ // src/doctor/read.ts
127
+ var READ = {
128
+ home: record("read.home", 2),
129
+ jobs: record("read.jobs", 2),
130
+ jsonld: record("read.jsonld", 2),
131
+ sitemap: record("read.sitemap", 2),
132
+ robots: record("read.robots", 2)
133
+ };
134
+ function skipReadProbes(reason) {
135
+ return Object.values(READ).map((make) => make("skip", reason));
136
+ }
137
+ async function probeJobsAndJsonLd(fetchImpl, base) {
138
+ const jobs = await probe(fetchImpl, `${base}/jobs`);
139
+ const jobLink = jobs.ok ? extractJobDetailLink(jobs.body) : null;
140
+ const jobsResult = jobs.ok && jobLink ? READ.jobs("pass", `listing renders with job detail links (${jobLink})`) : READ.jobs(
141
+ "fail",
142
+ jobs.ok ? "listing renders but contains no /companies/{c}/jobs/{s} detail links" : `listing HTTP ${jobs.status}`
143
+ );
144
+ if (!jobLink) {
145
+ return [
146
+ jobsResult,
147
+ READ.jsonld(
148
+ "skip",
149
+ jobs.ok ? "not probed \u2014 the listing rendered no job detail link (see read.jobs failure)" : "not probed \u2014 the /jobs listing itself failed (see read.jobs failure)"
150
+ )
151
+ ];
152
+ }
153
+ const detail = await probe(fetchImpl, `${base}${jobLink}`);
154
+ const posting = detail.ok ? extractJobPostingJsonLd(detail.body) : null;
155
+ return [
156
+ jobsResult,
157
+ posting ? READ.jsonld(
158
+ "pass",
159
+ `JobPosting JSON-LD present (${String(posting.title ?? "")})`
160
+ ) : READ.jsonld(
161
+ "fail",
162
+ detail.ok ? "job detail page has no parseable JobPosting JSON-LD" : `job detail HTTP ${detail.status}`
163
+ )
164
+ ];
165
+ }
166
+ function onFrontend(base, locUrl) {
167
+ try {
168
+ const parsed = new URL(locUrl);
169
+ return `${base}${parsed.pathname}${parsed.search}`;
170
+ } catch {
171
+ return locUrl;
172
+ }
173
+ }
174
+ async function probeSitemap(fetchImpl, base) {
175
+ const sitemap = await probe(fetchImpl, `${base}/sitemap.xml`);
176
+ if (!sitemap.ok) {
177
+ return READ.sitemap("fail", `sitemap.xml HTTP ${sitemap.status}`);
178
+ }
179
+ const doc = parseSitemap(sitemap.body);
180
+ if (!doc || doc.urls.length === 0) {
181
+ return READ.sitemap(
182
+ "fail",
183
+ doc ? "sitemap.xml has no <loc> entries" : "sitemap.xml is not a sitemap"
184
+ );
185
+ }
186
+ let urls = doc.urls;
187
+ if (doc.kind === "index") {
188
+ const child = await probe(fetchImpl, onFrontend(base, urls[0]));
189
+ const childDoc = child.ok ? parseSitemap(child.body) : null;
190
+ if (!childDoc || childDoc.urls.length === 0) {
191
+ return READ.sitemap(
192
+ "fail",
193
+ `child sitemap ${urls[0]} ${child.ok ? "has no <loc> entries" : `HTTP ${child.status}`}`
194
+ );
195
+ }
196
+ urls = childDoc.urls;
197
+ }
198
+ const sampleUrl = onFrontend(base, urls[0]);
199
+ const sample = await probe(fetchImpl, sampleUrl);
200
+ return sample.ok ? READ.sitemap("pass", `${urls.length} entries; sample page resolves`) : READ.sitemap("fail", `sitemap page ${sampleUrl} \u2192 HTTP ${sample.status}`);
201
+ }
202
+ async function runReadProbes(fetchImpl, frontendUrl) {
203
+ const base = frontendUrl.replace(/\/$/, "");
204
+ const [home, jobsAndJsonLd, sitemap, robots] = await Promise.all([
205
+ probe(fetchImpl, base),
206
+ probeJobsAndJsonLd(fetchImpl, base),
207
+ probeSitemap(fetchImpl, base),
208
+ probe(fetchImpl, `${base}/robots.txt`)
209
+ ]);
210
+ return [
211
+ home.ok && /<(html|body|div|main)[\s>]/i.test(home.body) ? READ.home("pass", "home renders") : READ.home(
212
+ "fail",
213
+ home.ok ? "home returned 200 but no HTML document \u2014 captive portal or empty shell?" : `home HTTP ${home.status}`
214
+ ),
215
+ ...jobsAndJsonLd,
216
+ sitemap,
217
+ robots.ok && /user-agent\s*:/i.test(robots.body) ? READ.robots("pass", "present") : READ.robots(
218
+ "fail",
219
+ robots.ok ? "robots.txt returned 200 but has no User-agent directive" : `robots.txt HTTP ${robots.status}`
220
+ )
221
+ ];
222
+ }
223
+
224
+ // src/doctor/theme.ts
225
+ import { createHash } from "crypto";
226
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
227
+ import { join } from "path";
228
+ var THEME = record("static.theme", 1);
229
+ function checkThemeFreshness(projectRoot, context) {
230
+ const tokensPath = join(projectRoot, "src/tokens.css");
231
+ if (!existsSync(tokensPath)) {
232
+ return [
233
+ THEME(
234
+ "skip",
235
+ "board is not tokens-migrated (ADR-0065) \u2014 no src/tokens.css"
236
+ )
237
+ ];
238
+ }
239
+ const tokensHash = createHash("sha256").update(readFileSync2(tokensPath, "utf8"), "utf8").digest("hex");
240
+ const resolvedPath = join(projectRoot, "src/theme/resolved.ts");
241
+ const resolvedHash = existsSync(resolvedPath) ? readFileSync2(resolvedPath, "utf8").match(
242
+ /tokensHash = '([0-9a-f]{64})'/
243
+ )?.[1] ?? null : null;
244
+ if (resolvedHash !== tokensHash) {
245
+ return [
246
+ THEME(
247
+ "fail",
248
+ `src/theme/resolved.ts is ${resolvedHash ? "stale" : "missing"} \u2014 run \`npm run gen:theme\` (OG images render from it)`
249
+ )
250
+ ];
251
+ }
252
+ if (!context) {
253
+ return [
254
+ THEME(
255
+ "skip",
256
+ "local derivations fresh; platform snapshot unverified \u2014 board context unavailable"
257
+ )
258
+ ];
259
+ }
260
+ if (context.themeSnapshotHash !== tokensHash) {
261
+ return [
262
+ THEME(
263
+ "fail",
264
+ `platform theme snapshot is ${context.themeSnapshotHash ? "stale" : "missing"} \u2014 emails render ${context.themeSnapshotHash ? "an old" : "the legacy"} theme; sync it: \`npm run gen:theme -- --payload | npx convex run boards/themeSnapshot:sync\``
265
+ )
266
+ ];
267
+ }
268
+ return [
269
+ THEME(
270
+ "pass",
271
+ `tokens.css \u21C4 resolved module \u21C4 platform snapshot (${tokensHash.slice(0, 12)}\u2026)`
272
+ )
273
+ ];
274
+ }
275
+
276
+ // src/doctor/writes.ts
277
+ var WRITE = {
278
+ board: record("write.board", 3),
279
+ register: record("write.register", 3),
280
+ login: record("write.login", 3),
281
+ email: record("write.email", 3),
282
+ apply: record("write.apply", 3),
283
+ alert: record("write.alert", 3)
284
+ };
285
+ var DEPENDENT_CHECKS = [
286
+ WRITE.register,
287
+ WRITE.login,
288
+ WRITE.email,
289
+ WRITE.apply,
290
+ WRITE.alert
291
+ ];
292
+ function skipWriteProbes(reason) {
293
+ return Object.values(WRITE).map((make) => make("skip", reason));
294
+ }
295
+ var FETCH_TIMEOUT_MS2 = 1e4;
296
+ var RESEND_API_BASE = "https://api.resend.com";
297
+ async function request(fetchImpl, url, init) {
298
+ try {
299
+ const response = await fetchImpl(url, {
300
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS2),
301
+ ...init
302
+ });
303
+ let json = null;
304
+ try {
305
+ json = await response.json();
306
+ } catch {
307
+ json = null;
308
+ }
309
+ return { ok: response.ok, status: response.status, json };
310
+ } catch (error) {
311
+ return {
312
+ ok: false,
313
+ status: 0,
314
+ json: { error: error instanceof Error ? error.message : String(error) }
315
+ };
316
+ }
317
+ }
318
+ function post(fetchImpl, url, body, bearer) {
319
+ return request(fetchImpl, url, {
320
+ method: "POST",
321
+ headers: {
322
+ "content-type": "application/json",
323
+ accept: "application/json",
324
+ ...bearer ? { authorization: `Bearer ${bearer}` } : {}
325
+ },
326
+ body: JSON.stringify(body)
327
+ });
328
+ }
329
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
330
+ async function observeVerificationEmail(fetchImpl, apiKey, recipient, pollMs, timeoutMs) {
331
+ const deadline = Date.now() + timeoutMs;
332
+ const subjectMarker = `[To: ${recipient}]`;
333
+ do {
334
+ const list = await request(
335
+ fetchImpl,
336
+ `${RESEND_API_BASE}/emails?limit=20`,
337
+ {
338
+ headers: { authorization: `Bearer ${apiKey}` }
339
+ }
340
+ );
341
+ if (!list.ok) {
342
+ return WRITE.email(
343
+ "fail",
344
+ `Resend list returned HTTP ${list.status} \u2014 wrong RESEND_API_KEY?`
345
+ );
346
+ }
347
+ const items = list.json?.data ?? [];
348
+ const match = items.find(
349
+ (item) => item.subject?.includes(subjectMarker) || item.to?.includes(recipient)
350
+ );
351
+ if (match) {
352
+ return WRITE.email(
353
+ "pass",
354
+ `verification send observed in Resend for ${recipient}`
355
+ );
356
+ }
357
+ await sleep(pollMs);
358
+ } while (Date.now() < deadline);
359
+ return WRITE.email(
360
+ "fail",
361
+ `no verification send for ${recipient} appeared in Resend within ${timeoutMs}ms`
362
+ );
363
+ }
364
+ async function runWriteProbes(options) {
365
+ const { fetchImpl } = options;
366
+ const base = `${apiBase(options.env.apiUrl)}/v1/boards/${encodeURIComponent(options.env.boardKey)}`;
367
+ const nonce = options.nonce ?? `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
368
+ const email = `doctor+${nonce}@cavuno.com`;
369
+ const password = `doctor-probe-${nonce}-A1!`;
370
+ let context = options.boardContext ?? null;
371
+ if (context === null) {
372
+ const r = await request(fetchImpl, base);
373
+ if (!r.ok) {
374
+ return [
375
+ WRITE.board("fail", `board context did not resolve (HTTP ${r.status})`),
376
+ ...DEPENDENT_CHECKS.map(
377
+ (make) => make("skip", "not run \u2014 the write gate failed (see write.board)")
378
+ )
379
+ ];
380
+ }
381
+ context = r.json ?? {};
382
+ }
383
+ if (context.sandbox !== true) {
384
+ return [
385
+ WRITE.board(
386
+ "fail",
387
+ "refusing write probes: this board is not the platform sandbox (`sandbox` is not true) \u2014 point PUBLIC_CAVUNO_BOARD at the sandbox pk_"
388
+ ),
389
+ ...DEPENDENT_CHECKS.map(
390
+ (make) => make("skip", "not run \u2014 the write gate failed (see write.board)")
391
+ )
392
+ ];
393
+ }
394
+ const results = [
395
+ WRITE.board("pass", "board is the platform sandbox")
396
+ ];
397
+ const registered = await post(fetchImpl, `${base}/auth/register`, {
398
+ role: "candidate",
399
+ method: "emailpass",
400
+ email,
401
+ password,
402
+ displayName: "Doctor Probe"
403
+ });
404
+ const sessionOk = (r) => r.ok && typeof r.json?.accessToken === "string";
405
+ results.push(
406
+ sessionOk(registered) ? WRITE.register("pass", `candidate registered (${email})`) : WRITE.register(
407
+ "fail",
408
+ `register returned HTTP ${registered.status}` + (registered.status === 429 ? " \u2014 rate-limited: register allows 5 signups / 15 min per IP; wait before rerunning" : "")
409
+ )
410
+ );
411
+ let accessToken = null;
412
+ if (sessionOk(registered)) {
413
+ const login = await post(fetchImpl, `${base}/auth/login`, {
414
+ email,
415
+ password
416
+ });
417
+ if (sessionOk(login)) {
418
+ accessToken = login.json.accessToken;
419
+ results.push(WRITE.login("pass", "emailpass login returns a session"));
420
+ } else {
421
+ results.push(WRITE.login("fail", `login returned HTTP ${login.status}`));
422
+ }
423
+ } else {
424
+ results.push(WRITE.login("skip", "not run \u2014 register failed"));
425
+ }
426
+ if (!sessionOk(registered)) {
427
+ results.push(WRITE.email("skip", "not run \u2014 register failed"));
428
+ } else if (!options.resendApiKey) {
429
+ results.push(
430
+ WRITE.email(
431
+ "skip",
432
+ "RESEND_API_KEY not set \u2014 the verification send was NOT verified (platform-operator check)"
433
+ )
434
+ );
435
+ } else {
436
+ results.push(
437
+ await observeVerificationEmail(
438
+ fetchImpl,
439
+ options.resendApiKey,
440
+ email,
441
+ options.emailPollMs ?? 2e3,
442
+ options.emailTimeoutMs ?? 3e4
443
+ )
444
+ );
445
+ }
446
+ if (!accessToken) {
447
+ results.push(WRITE.apply("skip", "not run \u2014 no session (see above)"));
448
+ } else {
449
+ const listing = await request(fetchImpl, `${base}/jobs?limit=1`);
450
+ const card = listing.json?.data?.[0];
451
+ if (!listing.ok || !card?.slug) {
452
+ results.push(
453
+ WRITE.apply(
454
+ "fail",
455
+ listing.ok ? "the board has no jobs to apply to \u2014 is the sandbox seeded?" : `jobs listing returned HTTP ${listing.status}`
456
+ )
457
+ );
458
+ } else {
459
+ const applied = await post(
460
+ fetchImpl,
461
+ `${base}/jobs/${encodeURIComponent(card.slug)}/apply`,
462
+ {},
463
+ accessToken
464
+ );
465
+ results.push(
466
+ applied.ok && typeof applied.json?.id === "string" ? WRITE.apply("pass", `applied to ${card.slug}`) : WRITE.apply("fail", `apply returned HTTP ${applied.status}`)
467
+ );
468
+ }
469
+ }
470
+ const alert = await post(fetchImpl, `${base}/job-alerts`, {
471
+ email,
472
+ consent: true
473
+ });
474
+ results.push(
475
+ alert.ok ? WRITE.alert("pass", "job-alert subscription accepted") : WRITE.alert("fail", `job-alerts returned HTTP ${alert.status}`)
476
+ );
477
+ return results;
478
+ }
479
+
480
+ // src/doctor/run.ts
481
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
482
+ import { join as join2 } from "path";
483
+ var STATIC_API = record("static.api", 1);
484
+ var STATIC_BOARD = record("static.board", 1);
485
+ var STATIC_SKILLS = record("static.skills", 1);
486
+ async function checkApiReachable(fetchImpl, apiUrl) {
487
+ try {
488
+ new URL(apiUrl);
489
+ } catch {
490
+ return null;
491
+ }
492
+ const result = await probe(fetchImpl, `${apiBase(apiUrl)}/v1/openapi.json`);
493
+ if (!result.ok) {
494
+ return STATIC_API(
495
+ "fail",
496
+ `OpenAPI spec unreachable (HTTP ${result.status})`
497
+ );
498
+ }
499
+ try {
500
+ const parsed = JSON.parse(result.body);
501
+ if (typeof parsed.openapi !== "string") throw new Error("not a spec");
502
+ } catch {
503
+ return STATIC_API(
504
+ "fail",
505
+ "endpoint returned 200 but not an OpenAPI document \u2014 proxy or captive portal in the way?"
506
+ );
507
+ }
508
+ return STATIC_API("pass", "OpenAPI spec reachable");
509
+ }
510
+ async function checkBoardResolves(fetchImpl, env) {
511
+ const result = await probe(
512
+ fetchImpl,
513
+ `${apiBase(env.apiUrl)}/v1/boards/${encodeURIComponent(env.boardKey)}`
514
+ );
515
+ if (!result.ok) {
516
+ return {
517
+ result: STATIC_BOARD(
518
+ "fail",
519
+ `board did not resolve for this key (HTTP ${result.status}) \u2014 revoked or wrong key?`
520
+ ),
521
+ context: null
522
+ };
523
+ }
524
+ let parsed = null;
525
+ try {
526
+ parsed = JSON.parse(result.body);
527
+ if (typeof parsed !== "object" || parsed === null) throw new Error("shape");
528
+ } catch {
529
+ return {
530
+ result: STATIC_BOARD(
531
+ "fail",
532
+ "board endpoint returned 200 but not JSON \u2014 proxy or captive portal in the way?"
533
+ ),
534
+ context: null
535
+ };
536
+ }
537
+ return {
538
+ result: STATIC_BOARD("pass", "publishable key resolves the board"),
539
+ context: parsed
540
+ };
541
+ }
542
+ var SKILL_ROOTS = [
543
+ ".claude/skills",
544
+ ".agents/skills",
545
+ ".cursor/skills"
546
+ ];
547
+ function checkSkillsFreshness(projectRoot) {
548
+ const roots = SKILL_ROOTS.map((root) => join2(projectRoot, root)).filter(
549
+ (root) => existsSync2(root)
550
+ );
551
+ if (roots.length === 0) {
552
+ return STATIC_SKILLS(
553
+ "skip",
554
+ "no .claude/skills, .agents/skills, or .cursor/skills directory \u2014 run `npx @cavuno/board setup` to install agent skills"
555
+ );
556
+ }
557
+ const corpus = loadSkillCorpus();
558
+ const stale = /* @__PURE__ */ new Set();
559
+ const seen = /* @__PURE__ */ new Set();
560
+ for (const root of roots) {
561
+ for (const skill of corpus.skills) {
562
+ const copied = join2(root, skill.name, "SKILL.md");
563
+ if (!existsSync2(copied)) continue;
564
+ seen.add(skill.name);
565
+ if (readFileSync3(copied, "utf8") !== skill.content) {
566
+ stale.add(skill.name);
567
+ }
568
+ }
569
+ }
570
+ const found = seen.size;
571
+ if (found === 0) {
572
+ return STATIC_SKILLS(
573
+ "skip",
574
+ "no cavuno-board-* skills installed \u2014 run `npx @cavuno/board setup`"
575
+ );
576
+ }
577
+ return stale.size === 0 ? STATIC_SKILLS(
578
+ "pass",
579
+ `${found} installed skills match v${corpus.version}`
580
+ ) : STATIC_SKILLS(
581
+ "fail",
582
+ `stale skills (re-run \`npx @cavuno/board setup\`): ${[...stale].join(", ")}`
583
+ );
584
+ }
585
+ async function runDoctor(options) {
586
+ const fetchImpl = options.fetchImpl ?? fetch;
587
+ const env = {
588
+ ...options.env,
589
+ apiUrl: options.env.apiUrl ?? DEFAULT_CAVUNO_API_URL
590
+ };
591
+ const results = [];
592
+ const envResults = checkEnv(env);
593
+ results.push(...envResults);
594
+ const envOk = envResults.every((r) => r.status === "pass");
595
+ if (env.apiUrl) {
596
+ const api = await checkApiReachable(fetchImpl, env.apiUrl);
597
+ results.push(
598
+ api ?? STATIC_API("skip", "API URL malformed \u2014 fix env.api-url first")
599
+ );
600
+ } else {
601
+ results.push(STATIC_API("skip", "PUBLIC_CAVUNO_API_URL not set"));
602
+ }
603
+ let boardContext = null;
604
+ if (envOk) {
605
+ const resolved = await checkBoardResolves(fetchImpl, env);
606
+ boardContext = resolved.context;
607
+ results.push(resolved.result);
608
+ } else {
609
+ results.push(STATIC_BOARD("skip", "env checks failed \u2014 fix them first"));
610
+ }
611
+ results.push(checkSkillsFreshness(options.projectRoot ?? process.cwd()));
612
+ results.push(
613
+ ...checkThemeFreshness(options.projectRoot ?? process.cwd(), boardContext)
614
+ );
615
+ results.push(
616
+ ...options.frontendUrl ? await runReadProbes(fetchImpl, options.frontendUrl) : skipReadProbes("no --frontend <url> given")
617
+ );
618
+ if (!options.sandbox) {
619
+ results.push(
620
+ ...skipWriteProbes(
621
+ "write probes not enabled \u2014 run with --sandbox against the platform sandbox"
622
+ )
623
+ );
624
+ } else if (!envOk) {
625
+ results.push(...skipWriteProbes("env checks failed \u2014 fix them first"));
626
+ } else {
627
+ results.push(
628
+ ...await runWriteProbes({
629
+ env,
630
+ fetchImpl,
631
+ resendApiKey: options.resendApiKey,
632
+ nonce: options.writeProbeNonce,
633
+ boardContext
634
+ })
635
+ );
636
+ }
637
+ return { results, summary: summarize(results) };
638
+ }
639
+ export {
640
+ runDoctor
641
+ };
@@ -1,5 +1,5 @@
1
1
  import { B as BoardLabelOverrides } from './ui-copy-CKfFTtLk.mjs';
2
- import { J as JobSort, E as EmploymentType, R as RemoteOption, S as Seniority } from './jobs-5qXIfBcR.mjs';
2
+ import { J as JobSort, E as EmploymentType, R as RemoteOption, S as Seniority } from './jobs-CMCADU_-.mjs';
3
3
 
4
4
  declare const REMOTE_OPTIONS: readonly RemoteOption[];
5
5
  /**
package/dist/filters.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { B as BoardLabelOverrides } from './ui-copy-CKfFTtLk.js';
2
- import { J as JobSort, E as EmploymentType, R as RemoteOption, S as Seniority } from './jobs-5qXIfBcR.js';
2
+ import { J as JobSort, E as EmploymentType, R as RemoteOption, S as Seniority } from './jobs-CMCADU_-.js';
3
3
 
4
4
  declare const REMOTE_OPTIONS: readonly RemoteOption[];
5
5
  /**