@rogatio/cli 6.15.0 → 6.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,6 +45,8 @@ rogatio <command> [options]
45
45
  | `rogatio runtime <install\|uninstall\|verify>` | Register the native-messaging host with `install` and trust the device-local CA in one **transactional** call; it needs elevated privileges (Linux `sudo`, macOS keychain authorization, Windows Administrator) and rolls back with exit `1` and `trust unsupported: <reasons>` when a capability or elevation is missing. Release users never pass `--extension-id`. That flag is only for development (a local unpacked build without the release key) and for forks. `uninstall` removes the host manifest, the device-local CA files, and the CA trust installation (idempotent). `verify` reports whether manifest, `runtime-host` wrapper, allowed origins, and CA trust are all valid, and fails when `allowed_origins` omits the release ID. Release users never pass `--extension-id` on verify either. |
46
46
  | `rogatio runtime host [path]` | Run the consolidated native-messaging host for the project (pair/authorize/body transforms and mock responses over stdio). Normally launched by the browser; run manually only for debugging. `--root <dir>` overrides the confined mock file root. When omitted, a saved device-local root for that project file is used, otherwise the project directory. A stdin project that contains a file mock requires `--root`. |
47
47
 
48
+ `rogatio edit` includes **Run checks**. It calls `POST /api/doctor` (CSRF-protected) and shows editor checks plus the same report as `rogatio doctor` for the open file. A provider configured after the page opened is picked up on the next `rogatio edit`.
49
+
48
50
  Global options: `--help, -h` and `--version, -v`. Run `rogatio <command> --help`
49
51
  for command-specific usage.
50
52
 
@@ -23225,9 +23225,9 @@ function generatePacScript(routes, endpoint, options) {
23225
23225
  }
23226
23226
  const checksByKey = /* @__PURE__ */ new Map();
23227
23227
  for (const route of routes) {
23228
- const check2 = pacCheck(route);
23229
- if (check2 === null) continue;
23230
- checksByKey.set(check2.key, check2.line);
23228
+ const check22 = pacCheck(route);
23229
+ if (check22 === null) continue;
23230
+ checksByKey.set(check22.key, check22.line);
23231
23231
  }
23232
23232
  const sorted = [...checksByKey.entries()].sort(
23233
23233
  ([left], [right]) => left.localeCompare(right)
@@ -23325,6 +23325,9 @@ function exactRegistryVersion(value) {
23325
23325
  const match = REGISTRY_VERSION_RE.exec(value);
23326
23326
  return match === null ? null : match[0];
23327
23327
  }
23328
+ function exactDoctorVersion(value) {
23329
+ return exactRegistryVersion(value);
23330
+ }
23328
23331
  function doctorFixCliUpdate(version) {
23329
23332
  return `npm install -g @rogatio/cli@${version}`;
23330
23333
  }
@@ -24371,13 +24374,13 @@ async function fetchAuthorized(operation, options = {}) {
24371
24374
  const protocol = url.protocol === "https:" ? "https:" : "http:";
24372
24375
  const port = protocol === "https:" ? 443 : 80;
24373
24376
  const timeoutMs = options.operationTimeoutMs ?? RUNTIME_LIMITS.operationTimeoutMs;
24374
- const combined = combineSignals(
24377
+ const combined2 = combineSignals(
24375
24378
  options.signal,
24376
24379
  Math.max(1, Math.min(timeoutMs, RUNTIME_LIMITS.operationTimeoutMs))
24377
24380
  );
24378
24381
  try {
24379
24382
  const resolver = options.resolver ?? defaultResolver;
24380
- const resolved = await resolver.lookup(url.hostname, combined.signal);
24383
+ const resolved = await resolver.lookup(url.hostname, combined2.signal);
24381
24384
  const addresses = validateResolvedAddresses(
24382
24385
  resolved,
24383
24386
  RUNTIME_LIMITS.maxDnsAddresses
@@ -24397,12 +24400,12 @@ async function fetchAuthorized(operation, options = {}) {
24397
24400
  method: operation.method,
24398
24401
  headers: { host: url.host, "accept-encoding": "identity" },
24399
24402
  ...protocol === "https:" ? { servername: url.hostname } : {},
24400
- signal: combined.signal
24403
+ signal: combined2.signal
24401
24404
  });
24402
24405
  } catch {
24403
- return combined.signal.aborted ? failure("runtime.timeout") : failure("runtime.internal");
24406
+ return combined2.signal.aborted ? failure("runtime.timeout") : failure("runtime.internal");
24404
24407
  }
24405
- if (combined.signal.aborted) return failure("runtime.timeout");
24408
+ if (combined2.signal.aborted) return failure("runtime.timeout");
24406
24409
  if (response.status >= 300 && response.status <= 399)
24407
24410
  return failure("runtime.redirect-rejected");
24408
24411
  if (responseHeaderBytes(response.headers) > RUNTIME_LIMITS.maxResponseHeaderBytes)
@@ -24414,7 +24417,7 @@ async function fetchAuthorized(operation, options = {}) {
24414
24417
  let total = 0;
24415
24418
  try {
24416
24419
  for await (const chunk of response.body) {
24417
- if (combined.signal.aborted) return failure("runtime.timeout");
24420
+ if (combined2.signal.aborted) return failure("runtime.timeout");
24418
24421
  const bytes = chunk instanceof Uint8Array ? Uint8Array.from(chunk) : Uint8Array.from(chunk);
24419
24422
  total += bytes.byteLength;
24420
24423
  if (total > RUNTIME_LIMITS.maxResponseBodyBytes)
@@ -24422,7 +24425,7 @@ async function fetchAuthorized(operation, options = {}) {
24422
24425
  chunks.push(bytes);
24423
24426
  }
24424
24427
  } catch {
24425
- return combined.signal.aborted ? failure("runtime.timeout") : failure("runtime.internal");
24428
+ return combined2.signal.aborted ? failure("runtime.timeout") : failure("runtime.internal");
24426
24429
  }
24427
24430
  const body = new Uint8Array(total);
24428
24431
  let offset = 0;
@@ -24435,9 +24438,9 @@ async function fetchAuthorized(operation, options = {}) {
24435
24438
  value: { status: response.status, headers: response.headers, body }
24436
24439
  };
24437
24440
  } catch {
24438
- return combined.signal.aborted ? failure("runtime.timeout") : failure("runtime.dns-failed");
24441
+ return combined2.signal.aborted ? failure("runtime.timeout") : failure("runtime.dns-failed");
24439
24442
  } finally {
24440
- combined.dispose();
24443
+ combined2.dispose();
24441
24444
  }
24442
24445
  }
24443
24446
  function supportedContentType2(value) {
@@ -27003,6 +27006,13 @@ function decodeEnvelopeFrame(buffer) {
27003
27006
  function defaultTrustRoot() {
27004
27007
  return defaultTrustInstallRoot(process.platform);
27005
27008
  }
27009
+ function doctorReplyMetadata(report2, cliVersion) {
27010
+ const version = typeof cliVersion === "string" ? exactDoctorVersion(cliVersion) : null;
27011
+ return {
27012
+ report: doctorReportValue(report2),
27013
+ ...version !== null ? { cliVersion: version } : {}
27014
+ };
27015
+ }
27006
27016
  function createNativeHost(options) {
27007
27017
  const controller = createNativeRuntimeController({
27008
27018
  preset: options.preset,
@@ -27248,7 +27258,7 @@ function createNativeHost(options) {
27248
27258
  type: "runtime.doctor",
27249
27259
  ...envelope.requestId !== void 0 ? { requestId: envelope.requestId } : {},
27250
27260
  timestamp: Date.now(),
27251
- metadata: { report: doctorReportValue(doctorReport) }
27261
+ metadata: doctorReplyMetadata(doctorReport, options.cliVersion)
27252
27262
  });
27253
27263
  } catch {
27254
27264
  return encodeEnvelopeFrame({
@@ -27256,7 +27266,10 @@ function createNativeHost(options) {
27256
27266
  type: "runtime.doctor",
27257
27267
  ...envelope.requestId !== void 0 ? { requestId: envelope.requestId } : {},
27258
27268
  timestamp: Date.now(),
27259
- metadata: { report: doctorReportValue(doctorInterruptedReport()) }
27269
+ metadata: doctorReplyMetadata(
27270
+ doctorInterruptedReport(),
27271
+ options.cliVersion
27272
+ )
27260
27273
  });
27261
27274
  }
27262
27275
  }
@@ -28327,6 +28340,319 @@ async function doctorCommand(args, options = {}) {
28327
28340
  // packages/cli/src/commands/edit.ts
28328
28341
  import { resolve as resolve6 } from "node:path";
28329
28342
 
28343
+ // packages/cli/src/editor-doctor.ts
28344
+ var EDITOR_CHECK_TITLES = {
28345
+ "editor.server": "Editor server",
28346
+ "editor.session": "Editor session",
28347
+ "editor.project": "Draft",
28348
+ "editor.ai": "AI Assist",
28349
+ "editor.mockRoot": "Mock file root"
28350
+ };
28351
+ var EDITOR_SERVER_STOPPED_SUMMARY = "The editor server stopped.";
28352
+ var EDITOR_SERVER_PASS_SUMMARY = "The editor server answered.";
28353
+ var EDITOR_SERVER_REJECTED_SUMMARY = "The editor server could not finish doctor.";
28354
+ var EDITOR_SERVER_REJECTED_FIX = "rogatio doctor";
28355
+ var EDITOR_SESSION_STALE_SUMMARY = "This tab is stale. Its editor token was rejected.";
28356
+ var EDITOR_SESSION_STALE_FIX = "Reload this tab";
28357
+ var EDITOR_SESSION_PASS_SUMMARY = "The editor token was accepted.";
28358
+ var EDITOR_PROJECT_INVALID_SUMMARY = "The draft is invalid.";
28359
+ var EDITOR_PROJECT_NOT_JSON_SUMMARY = "The draft is not JSON.";
28360
+ var EDITOR_PROJECT_VALID_SUMMARY = "The draft is valid.";
28361
+ var EDITOR_AI_NONE_SUMMARY = "Optional. No AI provider is configured.";
28362
+ var EDITOR_AI_NONE_FIX = "rogatio ai setup";
28363
+ var EDITOR_AI_AFTER_START_SUMMARY = "AI Assist was configured after this editor started.";
28364
+ var EDITOR_AI_AFTER_START_FIX = "Restart rogatio edit";
28365
+ var EDITOR_AI_PASS_SUMMARY = "AI Assist is available in this page.";
28366
+ var EDITOR_AI_REMOVED_SUMMARY = "AI Assist is no longer configured.";
28367
+ var EDITOR_MOCK_SAVED_SUMMARY = "A mock file root is saved.";
28368
+ var EDITOR_MOCK_MISSING_SUMMARY = "Mock rules have no saved mock file root.";
28369
+ var EDITOR_MOCK_MISSING_FIX = "Set mock file root";
28370
+ var MAX_DIAGNOSTICS2 = 20;
28371
+ var MAX_DIAGNOSTIC_FIELD = 200;
28372
+ function check2(id2, status, optional, summary, fix) {
28373
+ return { id: id2, status, optional, summary, fix };
28374
+ }
28375
+ function combined(checks, host) {
28376
+ const uiFailed = checks.some(
28377
+ (item) => item.status === "fail" && item.optional === false
28378
+ );
28379
+ return {
28380
+ version: 1,
28381
+ surface: "editor",
28382
+ ok: !uiFailed && (host === null || host.ok),
28383
+ ui: { checks },
28384
+ host
28385
+ };
28386
+ }
28387
+ function quoteEditorArg(value, platform = process.platform) {
28388
+ if (platform !== "win32") return quoteDoctorArg(value);
28389
+ return `"${value.replaceAll("%", "%%").replaceAll('"', '""')}"`;
28390
+ }
28391
+ function editorEditFix(filePath, platform = process.platform) {
28392
+ return `rogatio edit ${quoteEditorArg(filePath, platform)}`;
28393
+ }
28394
+ function editorVerifyFix(filePath, platform = process.platform) {
28395
+ return `rogatio verify ${quoteEditorArg(filePath, platform)}`;
28396
+ }
28397
+ function editorServerStoppedReport(filePath) {
28398
+ return combined(
28399
+ [
28400
+ check2(
28401
+ "editor.server",
28402
+ "fail",
28403
+ false,
28404
+ EDITOR_SERVER_STOPPED_SUMMARY,
28405
+ editorEditFix(filePath)
28406
+ )
28407
+ ],
28408
+ null
28409
+ );
28410
+ }
28411
+ function editorSessionStaleReport() {
28412
+ return combined(
28413
+ [
28414
+ check2("editor.server", "pass", false, EDITOR_SERVER_PASS_SUMMARY, null),
28415
+ check2(
28416
+ "editor.session",
28417
+ "fail",
28418
+ false,
28419
+ EDITOR_SESSION_STALE_SUMMARY,
28420
+ EDITOR_SESSION_STALE_FIX
28421
+ )
28422
+ ],
28423
+ null
28424
+ );
28425
+ }
28426
+ function editorServerRejectedReport() {
28427
+ return combined(
28428
+ [
28429
+ check2(
28430
+ "editor.server",
28431
+ "fail",
28432
+ false,
28433
+ EDITOR_SERVER_REJECTED_SUMMARY,
28434
+ EDITOR_SERVER_REJECTED_FIX
28435
+ )
28436
+ ],
28437
+ null
28438
+ );
28439
+ }
28440
+ function scrubField(value) {
28441
+ let scrubbed = "";
28442
+ for (const char of value) {
28443
+ const code = char.codePointAt(0) ?? 0;
28444
+ scrubbed += code <= 31 || code === 127 ? " " : char;
28445
+ if (scrubbed.length >= MAX_DIAGNOSTIC_FIELD) break;
28446
+ }
28447
+ return scrubbed.slice(0, MAX_DIAGNOSTIC_FIELD);
28448
+ }
28449
+ function diagnosticLines(diagnostics) {
28450
+ const shown = diagnostics.slice(0, MAX_DIAGNOSTICS2).map((item) => {
28451
+ return `${scrubField(item.path)}: ${scrubField(item.message)} (${scrubField(item.code)})`;
28452
+ });
28453
+ const text = shown.join("\n");
28454
+ if (diagnostics.length <= MAX_DIAGNOSTICS2) return text;
28455
+ return `${text}
28456
+ ${diagnostics.length - MAX_DIAGNOSTICS2} more`;
28457
+ }
28458
+ function projectCheck2(draft, parsed, filePath) {
28459
+ const fix = editorVerifyFix(filePath);
28460
+ if (!parsed) {
28461
+ return check2(
28462
+ "editor.project",
28463
+ "fail",
28464
+ false,
28465
+ EDITOR_PROJECT_NOT_JSON_SUMMARY,
28466
+ fix
28467
+ );
28468
+ }
28469
+ try {
28470
+ const diagnosis = diagnoseProjectData(draft);
28471
+ if (diagnosis.diagnostics.length === 0) {
28472
+ return check2(
28473
+ "editor.project",
28474
+ "pass",
28475
+ false,
28476
+ EDITOR_PROJECT_VALID_SUMMARY,
28477
+ null
28478
+ );
28479
+ }
28480
+ const lines = diagnosticLines(diagnosis.diagnostics);
28481
+ return check2(
28482
+ "editor.project",
28483
+ "fail",
28484
+ false,
28485
+ lines.length > 0 ? `${EDITOR_PROJECT_INVALID_SUMMARY}
28486
+ ${lines}` : EDITOR_PROJECT_INVALID_SUMMARY,
28487
+ fix
28488
+ );
28489
+ } catch {
28490
+ return check2(
28491
+ "editor.project",
28492
+ "fail",
28493
+ false,
28494
+ EDITOR_PROJECT_INVALID_SUMMARY,
28495
+ fix
28496
+ );
28497
+ }
28498
+ }
28499
+ function aiCheck2(assistInPage, configuredNow) {
28500
+ if (!assistInPage && !configuredNow) {
28501
+ return check2(
28502
+ "editor.ai",
28503
+ "warn",
28504
+ true,
28505
+ EDITOR_AI_NONE_SUMMARY,
28506
+ EDITOR_AI_NONE_FIX
28507
+ );
28508
+ }
28509
+ if (!assistInPage && configuredNow) {
28510
+ return check2(
28511
+ "editor.ai",
28512
+ "warn",
28513
+ true,
28514
+ EDITOR_AI_AFTER_START_SUMMARY,
28515
+ EDITOR_AI_AFTER_START_FIX
28516
+ );
28517
+ }
28518
+ if (assistInPage && configuredNow) {
28519
+ return check2("editor.ai", "pass", true, EDITOR_AI_PASS_SUMMARY, null);
28520
+ }
28521
+ return check2(
28522
+ "editor.ai",
28523
+ "warn",
28524
+ true,
28525
+ EDITOR_AI_REMOVED_SUMMARY,
28526
+ EDITOR_AI_NONE_FIX
28527
+ );
28528
+ }
28529
+ function draftHasMockRule(draft) {
28530
+ if (draft === null || typeof draft !== "object" || Array.isArray(draft)) {
28531
+ return false;
28532
+ }
28533
+ const groups = draft.groups;
28534
+ if (!Array.isArray(groups)) return false;
28535
+ for (const group of groups) {
28536
+ if (group === null || typeof group !== "object" || Array.isArray(group)) {
28537
+ continue;
28538
+ }
28539
+ const rules = group.rules;
28540
+ if (!Array.isArray(rules)) continue;
28541
+ for (const rule of rules) {
28542
+ if (rule === null || typeof rule !== "object" || Array.isArray(rule)) {
28543
+ continue;
28544
+ }
28545
+ if (rule.type === "mock") return true;
28546
+ }
28547
+ }
28548
+ return false;
28549
+ }
28550
+ function mockRootCheck(saved) {
28551
+ if (saved) {
28552
+ return check2(
28553
+ "editor.mockRoot",
28554
+ "pass",
28555
+ true,
28556
+ EDITOR_MOCK_SAVED_SUMMARY,
28557
+ null
28558
+ );
28559
+ }
28560
+ return check2(
28561
+ "editor.mockRoot",
28562
+ "warn",
28563
+ true,
28564
+ EDITOR_MOCK_MISSING_SUMMARY,
28565
+ EDITOR_MOCK_MISSING_FIX
28566
+ );
28567
+ }
28568
+ function buildEditorDoctor(input) {
28569
+ const checks = [
28570
+ check2("editor.server", "pass", false, EDITOR_SERVER_PASS_SUMMARY, null),
28571
+ check2("editor.session", "pass", false, EDITOR_SESSION_PASS_SUMMARY, null),
28572
+ projectCheck2(input.draft, input.draftParsed, input.filePath),
28573
+ aiCheck2(input.assistInPage, input.configuredNow)
28574
+ ];
28575
+ if (input.draftParsed && draftHasMockRule(input.draft)) {
28576
+ checks.push(mockRootCheck(input.mockRootSaved));
28577
+ }
28578
+ return combined(checks, input.host);
28579
+ }
28580
+ function mapReadError2(error) {
28581
+ if (error instanceof ProjectStorageError) {
28582
+ if (error.code === "not-found") return { ok: false, reason: "missing" };
28583
+ if (error.code === "invalid-json" || error.code === "invalid-format") {
28584
+ return { ok: false, reason: "parse" };
28585
+ }
28586
+ }
28587
+ return { ok: false, reason: "unreadable" };
28588
+ }
28589
+ function editorFileDoctorOptions(input) {
28590
+ return {
28591
+ cliVersion: input.cliVersion,
28592
+ checkUpdates: false,
28593
+ project: {
28594
+ source: "file",
28595
+ path: input.filePath,
28596
+ read: input.read
28597
+ }
28598
+ };
28599
+ }
28600
+ async function runEditorFileDoctor(input) {
28601
+ try {
28602
+ return await runInstalledDoctor(
28603
+ editorFileDoctorOptions({
28604
+ filePath: input.filePath,
28605
+ cliVersion: input.cliVersion,
28606
+ read: async () => {
28607
+ try {
28608
+ return { ok: true, data: await input.storageGet(input.filePath) };
28609
+ } catch (error) {
28610
+ return mapReadError2(error);
28611
+ }
28612
+ }
28613
+ })
28614
+ );
28615
+ } catch {
28616
+ return doctorInterruptedReport();
28617
+ }
28618
+ }
28619
+ function serializeEditorDiagnostics(report2) {
28620
+ return `${JSON.stringify(
28621
+ {
28622
+ version: 1,
28623
+ surface: "editor",
28624
+ ok: report2.ok,
28625
+ ui: {
28626
+ checks: report2.ui.checks.map((item) => ({
28627
+ id: item.id,
28628
+ status: item.status,
28629
+ optional: item.optional,
28630
+ summary: item.summary,
28631
+ fix: item.fix
28632
+ }))
28633
+ },
28634
+ host: report2.host === null ? null : doctorReportValue(report2.host)
28635
+ },
28636
+ null,
28637
+ 2
28638
+ )}
28639
+ `;
28640
+ }
28641
+ function embedJson(value) {
28642
+ return JSON.stringify(value).replace(/</g, "\\u003c");
28643
+ }
28644
+ function editorPageTitles() {
28645
+ return {
28646
+ ...EDITOR_CHECK_TITLES,
28647
+ node: "Node and CLI",
28648
+ project: "Project file",
28649
+ host: "Native host",
28650
+ ca: "Device CA",
28651
+ pac: "Runtime PAC",
28652
+ ai: "AI Assist"
28653
+ };
28654
+ }
28655
+
28330
28656
  // packages/cli/src/server/http.ts
28331
28657
  import {
28332
28658
  createServer as createHttpServer
@@ -29093,6 +29419,67 @@ function createRoutes(context) {
29093
29419
  res.end(JSON.stringify({ diagnostics: diagnosis.diagnostics }));
29094
29420
  return;
29095
29421
  }
29422
+ if (pathname === "/api/doctor" && method === "POST") {
29423
+ if (!validateCsrf(req, context.csrfToken)) {
29424
+ res.writeHead(403, { "Content-Type": "application/json" });
29425
+ res.end(
29426
+ JSON.stringify({
29427
+ code: "csrf-invalid",
29428
+ message: "Invalid CSRF token"
29429
+ })
29430
+ );
29431
+ return;
29432
+ }
29433
+ let draft = context.project;
29434
+ let draftParsed = true;
29435
+ try {
29436
+ const bodyText = await getRequestBody(req);
29437
+ if (bodyText.length > 0) draft = JSON.parse(bodyText);
29438
+ } catch (error) {
29439
+ if (error instanceof RequestBodyError) {
29440
+ const failure2 = bodyErrorResponse(error);
29441
+ res.writeHead(failure2.status, { "Content-Type": "application/json" });
29442
+ res.end(JSON.stringify(failure2.body));
29443
+ return;
29444
+ }
29445
+ draftParsed = false;
29446
+ }
29447
+ let host2;
29448
+ try {
29449
+ host2 = context.runFileDoctor ? await context.runFileDoctor() : await runEditorFileDoctor({
29450
+ filePath: context.filePath,
29451
+ cliVersion: readCliVersion(),
29452
+ storageGet: (path2) => context.storage.get(path2)
29453
+ });
29454
+ } catch {
29455
+ host2 = doctorInterruptedReport();
29456
+ }
29457
+ let configuredNow = false;
29458
+ try {
29459
+ const config = await (context.readProviderConfig ?? (async () => null))();
29460
+ configuredNow = config !== null && config !== void 0;
29461
+ } catch {
29462
+ configuredNow = false;
29463
+ }
29464
+ let mockRootSaved = false;
29465
+ try {
29466
+ mockRootSaved = await readSavedMockRoot(context.filePath) !== void 0;
29467
+ } catch {
29468
+ mockRootSaved = false;
29469
+ }
29470
+ const report2 = buildEditorDoctor({
29471
+ filePath: context.filePath,
29472
+ draft,
29473
+ draftParsed,
29474
+ assistInPage: context.aiClient !== void 0,
29475
+ configuredNow,
29476
+ mockRootSaved,
29477
+ host: host2
29478
+ });
29479
+ res.writeHead(200, { "Content-Type": "application/json" });
29480
+ res.end(serializeEditorDiagnostics(report2));
29481
+ return;
29482
+ }
29096
29483
  if (pathname === "/api/mock-root" && method === "GET") {
29097
29484
  const saved = await readSavedMockRoot(context.filePath);
29098
29485
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -29669,7 +30056,8 @@ async function editCommand(args, options = {}) {
29669
30056
  editorCssPath: "",
29670
30057
  editorFontsPath: "",
29671
30058
  aiClient,
29672
- aiProviderConfig: providerConfig ?? void 0
30059
+ aiProviderConfig: providerConfig ?? void 0,
30060
+ readProviderConfig
29673
30061
  };
29674
30062
  let server;
29675
30063
  try {
@@ -29799,9 +30187,23 @@ function generateEditorHtml(apiBase, csrfToken, filePath, aiConfigured) {
29799
30187
  background-size: 24px 24px;
29800
30188
  }
29801
30189
  #editor-root { min-height: 100vh; }
30190
+ #editor-doctor {
30191
+ position: sticky;
30192
+ top: 0;
30193
+ z-index: 2;
30194
+ background: #1c1f24;
30195
+ color: #e7eaf0;
30196
+ padding: 12px 16px;
30197
+ font: 13px/1.45 ui-monospace, monospace;
30198
+ white-space: pre-wrap;
30199
+ }
30200
+ #editor-doctor[hidden] { display: none; }
30201
+ #editor-doctor p { margin: 0 0 0.35rem; }
30202
+ #editor-doctor button { margin-top: 0.5rem; }
29802
30203
  </style>
29803
30204
  </head>
29804
30205
  <body>
30206
+ <div id="editor-doctor" hidden></div>
29805
30207
  <div id="editor-root"></div>
29806
30208
  <script type="importmap">
29807
30209
  { "imports": { "@rogatio/editor": "/vendor/editor.js" } }
@@ -29812,7 +30214,11 @@ function generateEditorHtml(apiBase, csrfToken, filePath, aiConfigured) {
29812
30214
  const root = document.getElementById('editor-root');
29813
30215
  const apiBase = ${JSON.stringify(apiBase)};
29814
30216
  const csrfToken = ${JSON.stringify(csrfToken)};
29815
- const filePath = ${JSON.stringify(filePath)};
30217
+ const filePath = ${embedJson(filePath)};
30218
+ const doctorTitles = ${embedJson(editorPageTitles())};
30219
+ const serverStoppedJson = ${embedJson(serializeEditorDiagnostics(editorServerStoppedReport(filePath)))};
30220
+ const sessionStaleJson = ${embedJson(serializeEditorDiagnostics(editorSessionStaleReport()))};
30221
+ const serverRejectedJson = ${embedJson(serializeEditorDiagnostics(editorServerRejectedReport()))};
29816
30222
 
29817
30223
  async function fetchProject() {
29818
30224
  const res = await fetch(apiBase + '/api/project');
@@ -29961,15 +30367,110 @@ function generateEditorHtml(apiBase, csrfToken, filePath, aiConfigured) {
29961
30367
  cancel();
29962
30368
  },
29963
30369
  projectActions: [
30370
+ { command: 'run-doctor', label: 'Run checks' },
29964
30371
  { command: 'set-mock-root', label: 'Set mock file root' },
29965
30372
  { command: 'clear-mock-root', label: 'Clear mock file root' },
29966
30373
  ],
29967
30374
  ${aiAssistHandler}
29968
30375
  });
30376
+ let lastDoctorJson = '';
30377
+ let doctorRunning = false;
30378
+ function renderDoctor(report, rawJson) {
30379
+ const panel = document.getElementById('editor-doctor');
30380
+ if (!panel) return;
30381
+ panel.replaceChildren();
30382
+ panel.hidden = false;
30383
+ lastDoctorJson = rawJson;
30384
+ const ui = report && report.ui && Array.isArray(report.ui.checks)
30385
+ ? report.ui.checks
30386
+ : [];
30387
+ const host = report && report.host && Array.isArray(report.host.checks)
30388
+ ? report.host.checks
30389
+ : [];
30390
+ for (const item of ui.concat(host)) {
30391
+ if (!item || typeof item.summary !== 'string' || typeof item.status !== 'string') {
30392
+ continue;
30393
+ }
30394
+ const id = typeof item.id === 'string' ? item.id : '';
30395
+ const title = Object.prototype.hasOwnProperty.call(doctorTitles, id)
30396
+ ? doctorTitles[id]
30397
+ : id;
30398
+ const line = document.createElement('p');
30399
+ line.dataset.check = id;
30400
+ const fix = typeof item.fix === 'string' ? item.fix : null;
30401
+ line.textContent = fix === null
30402
+ ? item.status + ' ' + title + ' ' + item.summary
30403
+ : item.status + ' ' + title + ' ' + item.summary + '\\nFix: ' + fix;
30404
+ panel.append(line);
30405
+ }
30406
+ const copy = document.createElement('button');
30407
+ copy.type = 'button';
30408
+ copy.textContent = 'Copy diagnostics';
30409
+ copy.addEventListener('click', () => {
30410
+ if (navigator.clipboard && navigator.clipboard.writeText) {
30411
+ void navigator.clipboard.writeText(lastDoctorJson);
30412
+ }
30413
+ });
30414
+ panel.append(copy);
30415
+ }
30416
+ async function runEditorDoctor() {
30417
+ if (doctorRunning) return;
30418
+ doctorRunning = true;
30419
+ try {
30420
+ let body;
30421
+ try {
30422
+ body = JSON.stringify(editor.getDraft());
30423
+ } catch (error) {
30424
+ renderDoctor(JSON.parse(serverRejectedJson), serverRejectedJson);
30425
+ return;
30426
+ }
30427
+ let res;
30428
+ try {
30429
+ res = await fetch(apiBase + '/api/doctor', {
30430
+ method: 'POST',
30431
+ headers: {
30432
+ 'Content-Type': 'application/json',
30433
+ 'X-CSRF-Token': csrfToken,
30434
+ },
30435
+ body,
30436
+ });
30437
+ } catch (error) {
30438
+ renderDoctor(JSON.parse(serverStoppedJson), serverStoppedJson);
30439
+ return;
30440
+ }
30441
+ const text = await res.text();
30442
+ if (res.status === 403) {
30443
+ let payload = null;
30444
+ try { payload = JSON.parse(text); } catch (error) { payload = null; }
30445
+ if (payload && payload.code === 'csrf-invalid') {
30446
+ renderDoctor(JSON.parse(sessionStaleJson), sessionStaleJson);
30447
+ return;
30448
+ }
30449
+ }
30450
+ if (!res.ok) {
30451
+ renderDoctor(JSON.parse(serverRejectedJson), serverRejectedJson);
30452
+ return;
30453
+ }
30454
+ let report;
30455
+ try {
30456
+ report = JSON.parse(text);
30457
+ } catch (error) {
30458
+ renderDoctor(JSON.parse(serverRejectedJson), serverRejectedJson);
30459
+ return;
30460
+ }
30461
+ renderDoctor(report, text);
30462
+ } finally {
30463
+ doctorRunning = false;
30464
+ }
30465
+ }
29969
30466
  document.addEventListener('click', (event) => {
29970
30467
  const target = event.target;
29971
30468
  if (!(target instanceof Element)) return;
29972
30469
  const command = target.closest('[data-command]')?.getAttribute('data-command');
30470
+ if (command === 'run-doctor') {
30471
+ void runEditorDoctor();
30472
+ return;
30473
+ }
29973
30474
  if (command !== 'set-mock-root' && command !== 'clear-mock-root') return;
29974
30475
  const root = command === 'clear-mock-root'
29975
30476
  ? null
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rogatio/cli",
3
- "version": "6.15.0",
3
+ "version": "6.16.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Local-first browser request and response rules — editor host, file verification, test runner, and runtime dispatch.",