@ganglion/xacpx 0.10.0 → 0.10.1

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.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/dist/cli.js +822 -201
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -258,6 +258,7 @@ xacpx doctor
258
258
  xacpx doctor --verbose
259
259
  xacpx doctor --smoke
260
260
  xacpx doctor --smoke --agent codex --workspace backend
261
+ xacpx doctor --fix
261
262
  ```
262
263
 
263
264
  Notes:
@@ -266,6 +267,7 @@ Notes:
266
267
  - `--smoke` additionally runs a minimal real transport-level prompt check
267
268
  - `--agent` / `--workspace` only affect `--smoke`
268
269
  - Without `--smoke`, the related checks show as `SKIP`
270
+ - `--fix` applies safe local repairs (runtime dir permissions, stale locks, invalid state records) and re-checks; state-mutating repairs are withheld while the daemon runs — see [docs/doctor-command.md](docs/doctor-command.md)
269
271
 
270
272
  ### How to use `update`
271
273
 
package/dist/cli.js CHANGED
@@ -5301,7 +5301,7 @@ class DaemonController {
5301
5301
  this.deps = deps;
5302
5302
  this.statusStore = new DaemonStatusStore(paths.statusFile);
5303
5303
  this.startupPollIntervalMs = deps.startupPollIntervalMs ?? 50;
5304
- this.startupTimeoutMs = deps.startupTimeoutMs ?? 5000;
5304
+ this.startupTimeoutMs = deps.startupTimeoutMs ?? 1e4;
5305
5305
  this.onboardingStartupTimeoutMs = deps.onboardingStartupTimeoutMs ?? 300000;
5306
5306
  this.shutdownPollIntervalMs = deps.shutdownPollIntervalMs ?? 50;
5307
5307
  this.shutdownTimeoutMs = deps.shutdownTimeoutMs ?? 5000;
@@ -5696,6 +5696,14 @@ function resolveRuntimeDirFromConfigPath(configPath) {
5696
5696
  function resolveDaemonOrchestrationSocketPath(runtimeDir, platform = process.platform) {
5697
5697
  return resolveOrchestrationEndpoint(runtimeDir, platform).path;
5698
5698
  }
5699
+ function isProcessAlive(pid) {
5700
+ try {
5701
+ process.kill(pid, 0);
5702
+ return true;
5703
+ } catch (error) {
5704
+ return error.code === "EPERM";
5705
+ }
5706
+ }
5699
5707
  var init_daemon_files = __esm(() => {
5700
5708
  init_core_home();
5701
5709
  init_orchestration_ipc();
@@ -19815,6 +19823,122 @@ var init_plugin_loader = __esm(() => {
19815
19823
  init_plugin_home();
19816
19824
  });
19817
19825
 
19826
+ // src/plugins/plugin-doctor.ts
19827
+ import { readFile as readFile10 } from "node:fs/promises";
19828
+ import { join as join12 } from "node:path";
19829
+ function suggestedPluginPackageForChannel(type) {
19830
+ return findKnownPluginByChannel(type)?.packageName ?? `<npm-package-that-provides-${type}>`;
19831
+ }
19832
+ async function readDependencyEntries(pluginHome) {
19833
+ try {
19834
+ const raw = await readFile10(join12(pluginHome, "package.json"), "utf8");
19835
+ const parsed = JSON.parse(raw);
19836
+ const out = {};
19837
+ for (const [name, value] of Object.entries(parsed.dependencies ?? {})) {
19838
+ if (typeof value === "string")
19839
+ out[name] = value;
19840
+ }
19841
+ return out;
19842
+ } catch (error2) {
19843
+ const message = error2 instanceof Error ? error2.message : String(error2);
19844
+ throw new Error(`failed to read plugin home package.json: ${message}`);
19845
+ }
19846
+ }
19847
+ async function inspectPlugins(input) {
19848
+ const issues = [];
19849
+ let dependencies;
19850
+ try {
19851
+ dependencies = await readDependencyEntries(input.pluginHome);
19852
+ } catch (error2) {
19853
+ const message = error2 instanceof Error ? error2.message : String(error2);
19854
+ return [{ level: "error", message }];
19855
+ }
19856
+ const importPlugin = input.importPlugin ?? importPluginFromHome;
19857
+ const allConfigured = input.config.plugins;
19858
+ const filterByName = input.pluginName ? normalizePluginPackageName(input.pluginName) : null;
19859
+ if (filterByName && !allConfigured.some((plugin) => normalizePluginPackageName(plugin.name) === filterByName)) {
19860
+ return [{ level: "error", plugin: filterByName, message: `plugin is not configured; run xacpx plugin add ${filterByName}` }];
19861
+ }
19862
+ const pushIfRelevant = (issue2) => {
19863
+ if (!filterByName || issue2.plugin === filterByName)
19864
+ issues.push(issue2);
19865
+ };
19866
+ const channelProviders = new Map;
19867
+ for (const configPlugin of allConfigured) {
19868
+ if (!(configPlugin.name in dependencies)) {
19869
+ pushIfRelevant({ level: "error", plugin: configPlugin.name, message: `package not installed in plugin home; run xacpx plugin add ${configPlugin.name}`, suggestion: `xacpx plugin add ${configPlugin.name} && xacpx restart` });
19870
+ continue;
19871
+ }
19872
+ let moduleValue;
19873
+ try {
19874
+ moduleValue = await importPlugin(configPlugin.name, input.pluginHome);
19875
+ } catch (error2) {
19876
+ const message = error2 instanceof Error ? error2.message : String(error2);
19877
+ pushIfRelevant({ level: "error", plugin: configPlugin.name, message: `failed to import plugin: ${message}`, suggestion: `xacpx plugin add ${configPlugin.name} && xacpx restart` });
19878
+ continue;
19879
+ }
19880
+ try {
19881
+ const plugin = validateWeacpxPlugin(moduleValue, configPlugin.name, {
19882
+ ...input.currentXacpxVersion !== undefined ? { currentXacpxVersion: input.currentXacpxVersion } : {}
19883
+ });
19884
+ const channels = plugin.channels ?? [];
19885
+ const channelTypes = channels.map((channel) => channel.type);
19886
+ for (const type of channelTypes) {
19887
+ const existing = channelProviders.get(type);
19888
+ if (existing) {
19889
+ pushIfRelevant({ level: "error", plugin: configPlugin.name, message: `channel type ${type} is already provided by ${existing.plugin}` });
19890
+ } else {
19891
+ channelProviders.set(type, { plugin: configPlugin.name, enabled: configPlugin.enabled });
19892
+ }
19893
+ }
19894
+ pushIfRelevant({
19895
+ level: configPlugin.enabled ? "ok" : "warn",
19896
+ plugin: configPlugin.name,
19897
+ message: configPlugin.enabled ? `plugin is installed and valid; channels: ${channelTypes.length > 0 ? channelTypes.join(", ") : "none"}` : `plugin is installed and valid but disabled; run xacpx plugin enable ${configPlugin.name}`,
19898
+ ...configPlugin.enabled ? {} : { suggestion: `xacpx plugin enable ${configPlugin.name}` }
19899
+ });
19900
+ } catch (error2) {
19901
+ const message = error2 instanceof Error ? error2.message : String(error2);
19902
+ pushIfRelevant({ level: "error", plugin: configPlugin.name, message });
19903
+ }
19904
+ }
19905
+ const builtInChannelTypes = new Set(listKnownChannelIds());
19906
+ for (const channel of input.config.channels) {
19907
+ if (channel.enabled === false)
19908
+ continue;
19909
+ if (builtInChannelTypes.has(channel.type))
19910
+ continue;
19911
+ const provider = channelProviders.get(channel.type);
19912
+ if (!provider) {
19913
+ if (!filterByName) {
19914
+ const suggestedPackage = suggestedPluginPackageForChannel(channel.type);
19915
+ issues.push({
19916
+ level: "error",
19917
+ message: `channel ${channel.type} is configured but no enabled plugin provides it; run xacpx plugin add ${suggestedPackage} or another plugin that provides type "${channel.type}"`,
19918
+ suggestion: `xacpx plugin add ${suggestedPackage}`
19919
+ });
19920
+ }
19921
+ continue;
19922
+ }
19923
+ if (!provider.enabled) {
19924
+ pushIfRelevant({
19925
+ level: "error",
19926
+ plugin: provider.plugin,
19927
+ message: `channel ${channel.type} is configured but provider plugin is disabled; run xacpx plugin enable ${provider.plugin}`,
19928
+ suggestion: `xacpx plugin enable ${provider.plugin}`
19929
+ });
19930
+ }
19931
+ }
19932
+ return issues;
19933
+ }
19934
+ var init_plugin_doctor = __esm(() => {
19935
+ init_channel_scope();
19936
+ init_plugin_loader();
19937
+ init_validate_plugin();
19938
+ init_known_plugins();
19939
+ init_plugin_renames();
19940
+ });
19941
+
19818
19942
  // src/channels/bootstrap.ts
19819
19943
  function bootstrapBuiltinChannels() {
19820
19944
  bootstrapBuiltinChannelFactories();
@@ -29815,8 +29939,9 @@ async function runConsole(paths, deps) {
29815
29939
  throw error2;
29816
29940
  }
29817
29941
  }
29818
- await runtime.reapStaleQueueOwners();
29942
+ const reapPromise = Promise.resolve(runtime.reapStaleQueueOwners()).catch(() => {});
29819
29943
  if (deps.beforeReady) {
29944
+ await reapPromise;
29820
29945
  await deps.beforeReady(runtime);
29821
29946
  }
29822
29947
  if (deps.daemonRuntime) {
@@ -29830,6 +29955,7 @@ async function runConsole(paths, deps) {
29830
29955
  deps.daemonRuntime?.heartbeat().catch(() => {});
29831
29956
  }, deps.heartbeatIntervalMs ?? 30000);
29832
29957
  }
29958
+ await reapPromise;
29833
29959
  const channelStartPromise = deps.channels.startAll({
29834
29960
  agent: runtime.agent,
29835
29961
  abortSignal: shutdownController.signal,
@@ -33161,8 +33287,10 @@ var init_config_check = __esm(async () => {
33161
33287
  });
33162
33288
 
33163
33289
  // src/doctor/checks/daemon-check.ts
33290
+ import { readdir as readdir3, readFile as readFile14, rm as rm10 } from "node:fs/promises";
33164
33291
  import { fileURLToPath as fileURLToPath6 } from "node:url";
33165
33292
  import { homedir as homedir10 } from "node:os";
33293
+ import { join as join19 } from "node:path";
33166
33294
  async function checkDaemon(options = {}) {
33167
33295
  const home = options.home ?? process.env.HOME ?? homedir10();
33168
33296
  const runtimeDir = options.configPath ? resolveRuntimeDirFromConfigPath(options.configPath) : undefined;
@@ -33170,15 +33298,25 @@ async function checkDaemon(options = {}) {
33170
33298
  home,
33171
33299
  ...runtimeDir ? { runtimeDir } : {}
33172
33300
  });
33301
+ const isProcessRunning = options.isProcessRunning ?? isProcessAlive;
33302
+ const listConsumerLocks = options.listConsumerLocks ?? defaultListConsumerLocks;
33303
+ const readConsumerLock = options.readConsumerLock ?? defaultReadConsumerLock;
33304
+ const removeConsumerLock = options.removeConsumerLock ?? defaultRemoveConsumerLock;
33173
33305
  const controller = createDaemonController(paths, {
33174
33306
  processExecPath: options.processExecPath ?? process.execPath,
33175
33307
  cliEntryPath: options.cliEntryPath ?? resolveCliEntryPath(),
33176
33308
  cwd: options.cwd ?? process.cwd(),
33177
33309
  env: options.env ?? process.env,
33178
- isProcessRunning: options.isProcessRunning ?? defaultIsProcessRunning5
33310
+ isProcessRunning
33179
33311
  });
33180
33312
  try {
33181
33313
  const status = await controller.getStatus();
33314
+ const staleLockFix = status.state === "stopped" ? await detectStaleConsumerLockFix(paths.runtimeDir, {
33315
+ isProcessRunning,
33316
+ listConsumerLocks,
33317
+ readConsumerLock,
33318
+ removeConsumerLock
33319
+ }) : undefined;
33182
33320
  switch (status.state) {
33183
33321
  case "running":
33184
33322
  return {
@@ -33200,6 +33338,7 @@ async function checkDaemon(options = {}) {
33200
33338
  summary: status.stale ? "daemon was stopped and stale runtime files were cleared" : "daemon is not running",
33201
33339
  details: status.stale ? ["stale runtime files were cleared"] : undefined,
33202
33340
  suggestions: ["run: xacpx start"],
33341
+ ...staleLockFix ? { fixes: [staleLockFix] } : {},
33203
33342
  metadata: {
33204
33343
  paths,
33205
33344
  status
@@ -33246,25 +33385,210 @@ async function checkDaemon(options = {}) {
33246
33385
  };
33247
33386
  }
33248
33387
  }
33249
- function defaultIsProcessRunning5(pid) {
33388
+ async function detectStaleConsumerLockFix(runtimeDir, deps) {
33389
+ const lockFiles = await deps.listConsumerLocks(runtimeDir);
33390
+ const stalePaths = [];
33391
+ for (const fileName of lockFiles) {
33392
+ if (!fileName.endsWith(CONSUMER_LOCK_SUFFIX)) {
33393
+ continue;
33394
+ }
33395
+ const lockPath = join19(runtimeDir, fileName);
33396
+ const lock2 = await deps.readConsumerLock(lockPath);
33397
+ if (lock2 && !deps.isProcessRunning(lock2.pid)) {
33398
+ stalePaths.push(lockPath);
33399
+ }
33400
+ }
33401
+ if (stalePaths.length === 0) {
33402
+ return;
33403
+ }
33404
+ return {
33405
+ id: "daemon.clear-stale-lock",
33406
+ title: "remove stale consumer lock(s)",
33407
+ run: async () => {
33408
+ const removed = [];
33409
+ let skipped = 0;
33410
+ for (const lockPath of stalePaths) {
33411
+ const lock2 = await deps.readConsumerLock(lockPath);
33412
+ if (!lock2 || deps.isProcessRunning(lock2.pid)) {
33413
+ skipped += 1;
33414
+ continue;
33415
+ }
33416
+ await deps.removeConsumerLock(lockPath);
33417
+ removed.push(lockPath);
33418
+ }
33419
+ const skippedNote = skipped > 0 ? `; left ${skipped} no-longer-stale lock(s) alone` : "";
33420
+ return {
33421
+ ok: true,
33422
+ message: removed.length > 0 ? `removed ${removed.length} stale consumer lock(s): ${removed.join(", ")}${skippedNote}` : `no locks removed${skippedNote}`
33423
+ };
33424
+ }
33425
+ };
33426
+ }
33427
+ async function defaultListConsumerLocks(runtimeDir) {
33428
+ try {
33429
+ return await readdir3(runtimeDir);
33430
+ } catch {
33431
+ return [];
33432
+ }
33433
+ }
33434
+ async function defaultReadConsumerLock(path15) {
33250
33435
  try {
33251
- process.kill(pid, 0);
33252
- return true;
33436
+ const raw = await readFile14(path15, "utf8");
33437
+ const parsed = JSON.parse(raw);
33438
+ return typeof parsed.pid === "number" ? { pid: parsed.pid } : null;
33253
33439
  } catch {
33254
- return false;
33440
+ return null;
33255
33441
  }
33256
33442
  }
33443
+ async function defaultRemoveConsumerLock(path15) {
33444
+ await rm10(path15, { force: true });
33445
+ }
33257
33446
  function resolveCliEntryPath() {
33258
33447
  return process.argv[1] ?? fileURLToPath6(import.meta.url);
33259
33448
  }
33260
33449
  function formatError5(error2) {
33261
33450
  return error2 instanceof Error ? error2.message : String(error2);
33262
33451
  }
33452
+ var CONSUMER_LOCK_SUFFIX = "-consumer.lock.json";
33263
33453
  var init_daemon_check = __esm(() => {
33264
33454
  init_create_daemon_controller();
33265
33455
  init_daemon_files();
33266
33456
  });
33267
33457
 
33458
+ // src/doctor/checks/logs-check.ts
33459
+ import { stat as stat3, readdir as readdir4 } from "node:fs/promises";
33460
+ import { basename as basename3, join as join20 } from "node:path";
33461
+ import { homedir as homedir11 } from "node:os";
33462
+ async function checkLogs(options = {}) {
33463
+ const home = options.home ?? process.env.HOME ?? homedir11();
33464
+ const runtimeDir = options.configPath ? resolveRuntimeDirFromConfigPath(options.configPath) : undefined;
33465
+ const paths = (options.resolveDaemonPaths ?? resolveDaemonPaths)({
33466
+ home,
33467
+ ...runtimeDir ? { runtimeDir } : {}
33468
+ });
33469
+ const probe = options.probe ?? createLogsFsProbe();
33470
+ const singleFileWarnBytes = options.singleFileWarnBytes ?? DEFAULT_SINGLE_FILE_WARN_BYTES;
33471
+ const totalWarnBytes = options.totalWarnBytes ?? DEFAULT_TOTAL_WARN_BYTES;
33472
+ let entries;
33473
+ try {
33474
+ const dirStat = await probe.stat(paths.runtimeDir);
33475
+ if (!dirStat.isDirectory()) {
33476
+ return skip("runtime log directory could not be read", [
33477
+ `runtimeDir: ${paths.runtimeDir} (exists but is not a directory)`
33478
+ ]);
33479
+ }
33480
+ entries = await probe.readdir(paths.runtimeDir);
33481
+ } catch (error2) {
33482
+ if (isMissingPathError(error2)) {
33483
+ return skip("no runtime logs yet", [`runtimeDir: ${paths.runtimeDir} (missing)`]);
33484
+ }
33485
+ return skip("runtime log directory could not be read", [
33486
+ `runtimeDir: ${paths.runtimeDir}`,
33487
+ `error: ${formatError6(error2)}`
33488
+ ]);
33489
+ }
33490
+ const baseNames = [basename3(paths.appLog), basename3(paths.stdoutLog), basename3(paths.stderrLog)];
33491
+ const tracked = new Set(baseNames);
33492
+ const matched = entries.filter((entry) => isTrackedLogName(entry, tracked));
33493
+ const files = [];
33494
+ for (const name of matched) {
33495
+ const path15 = join20(paths.runtimeDir, name);
33496
+ try {
33497
+ const fileStat = await probe.stat(path15);
33498
+ if (fileStat.isDirectory()) {
33499
+ continue;
33500
+ }
33501
+ files.push({ name, path: path15, size: fileStat.size });
33502
+ } catch {
33503
+ continue;
33504
+ }
33505
+ }
33506
+ const total = files.reduce((sum, file) => sum + file.size, 0);
33507
+ const largestSingle = files.reduce((max, file) => Math.max(max, file.size), 0);
33508
+ const overSingle = files.some((file) => file.size > singleFileWarnBytes);
33509
+ const overTotal = total > totalWarnBytes;
33510
+ const sorted = [...files].sort((a, b) => b.size - a.size);
33511
+ const details = [
33512
+ ...sorted.map((file) => `${file.name}: ${formatBytes(file.size)}`),
33513
+ `total: ${formatBytes(total)}`
33514
+ ];
33515
+ if (overSingle || overTotal) {
33516
+ const reason = overSingle ? `largest single log is ${formatBytes(largestSingle)}` : `total is ${formatBytes(total)}`;
33517
+ return {
33518
+ id: "logs",
33519
+ label: "Logs",
33520
+ severity: "warn",
33521
+ summary: `log growth high: ${reason} (total ${formatBytes(total)})`,
33522
+ details,
33523
+ suggestions: [
33524
+ "logs are large; check disk space and that log rotation is configured (logging.maxSizeBytes / maxFiles)"
33525
+ ]
33526
+ };
33527
+ }
33528
+ return {
33529
+ id: "logs",
33530
+ label: "Logs",
33531
+ severity: "pass",
33532
+ summary: `logs total ${formatBytes(total)}`,
33533
+ details
33534
+ };
33535
+ }
33536
+ function skip(summary, details) {
33537
+ return {
33538
+ id: "logs",
33539
+ label: "Logs",
33540
+ severity: "skip",
33541
+ summary,
33542
+ details
33543
+ };
33544
+ }
33545
+ function isTrackedLogName(name, baseNames) {
33546
+ if (baseNames.has(name)) {
33547
+ return true;
33548
+ }
33549
+ for (const base of baseNames) {
33550
+ const prefix = `${base}.`;
33551
+ if (name.startsWith(prefix)) {
33552
+ const suffix = name.slice(prefix.length);
33553
+ if (/^\d+$/.test(suffix) && Number(suffix) > 0) {
33554
+ return true;
33555
+ }
33556
+ }
33557
+ }
33558
+ return false;
33559
+ }
33560
+ function formatBytes(bytes) {
33561
+ if (bytes < 1024) {
33562
+ return `${bytes} B`;
33563
+ }
33564
+ const units = ["KB", "MB", "GB", "TB"];
33565
+ let value = bytes / 1024;
33566
+ let unitIndex = 0;
33567
+ while (value >= 1024 && unitIndex < units.length - 1) {
33568
+ value /= 1024;
33569
+ unitIndex += 1;
33570
+ }
33571
+ return `${value.toFixed(1)} ${units[unitIndex]}`;
33572
+ }
33573
+ function createLogsFsProbe() {
33574
+ return {
33575
+ stat: async (path15) => await stat3(path15),
33576
+ readdir: async (path15) => await readdir4(path15)
33577
+ };
33578
+ }
33579
+ function formatError6(error2) {
33580
+ return error2 instanceof Error ? error2.message : String(error2);
33581
+ }
33582
+ function isMissingPathError(error2) {
33583
+ return typeof error2 === "object" && error2 !== null && "code" in error2 && (error2.code === "ENOENT" || error2.code === "ENOTDIR");
33584
+ }
33585
+ var DEFAULT_SINGLE_FILE_WARN_BYTES, DEFAULT_TOTAL_WARN_BYTES;
33586
+ var init_logs_check = __esm(() => {
33587
+ init_daemon_files();
33588
+ DEFAULT_SINGLE_FILE_WARN_BYTES = 50 * 1024 * 1024;
33589
+ DEFAULT_TOTAL_WARN_BYTES = 200 * 1024 * 1024;
33590
+ });
33591
+
33268
33592
  // src/doctor/checks/orchestration-health.ts
33269
33593
  async function checkOrchestrationHealth(options) {
33270
33594
  const state = await options.loadState();
@@ -33317,13 +33641,187 @@ var init_orchestration_health = __esm(() => {
33317
33641
  init_i18n();
33318
33642
  });
33319
33643
 
33644
+ // src/doctor/checks/orchestration-socket-check.ts
33645
+ import { homedir as homedir12 } from "node:os";
33646
+ async function checkOrchestrationSocket(options = {}) {
33647
+ const home = options.home ?? process.env.HOME ?? homedir12();
33648
+ const runtimeDir = options.configPath ? resolveRuntimeDirFromConfigPath(options.configPath) : undefined;
33649
+ const paths = (options.resolveDaemonPaths ?? resolveDaemonPaths)({
33650
+ home,
33651
+ ...runtimeDir ? { runtimeDir } : {}
33652
+ });
33653
+ const getDaemonStatus = options.getDaemonStatus ?? ((p) => defaultGetDaemonStatus(p, options));
33654
+ const probe = options.canConnectToEndpoint ?? canConnectToEndpoint;
33655
+ const resolveEndpoint = options.resolveOrchestrationEndpoint ?? ((dir) => resolveOrchestrationEndpoint(dir));
33656
+ let status;
33657
+ try {
33658
+ status = await getDaemonStatus(paths);
33659
+ } catch (error2) {
33660
+ return {
33661
+ id: "orchestration-socket",
33662
+ label: "Orchestration IPC",
33663
+ severity: "skip",
33664
+ summary: "daemon status could not be read",
33665
+ details: [`runtime dir: ${paths.runtimeDir}`, `error: ${formatError7(error2)}`]
33666
+ };
33667
+ }
33668
+ if (status.state === "stopped") {
33669
+ return {
33670
+ id: "orchestration-socket",
33671
+ label: "Orchestration IPC",
33672
+ severity: "skip",
33673
+ summary: "daemon stopped"
33674
+ };
33675
+ }
33676
+ let endpoint;
33677
+ let reachable;
33678
+ try {
33679
+ endpoint = resolveEndpoint(paths.runtimeDir);
33680
+ reachable = await probe(endpoint.path);
33681
+ } catch (error2) {
33682
+ return {
33683
+ id: "orchestration-socket",
33684
+ label: "Orchestration IPC",
33685
+ severity: "skip",
33686
+ summary: "orchestration IPC liveness could not be probed",
33687
+ details: [`runtime dir: ${paths.runtimeDir}`, `error: ${formatError7(error2)}`]
33688
+ };
33689
+ }
33690
+ if (reachable) {
33691
+ return {
33692
+ id: "orchestration-socket",
33693
+ label: "Orchestration IPC",
33694
+ severity: "pass",
33695
+ summary: "orchestration IPC is accepting connections",
33696
+ details: [`endpoint: ${endpoint.path}`]
33697
+ };
33698
+ }
33699
+ return {
33700
+ id: "orchestration-socket",
33701
+ label: "Orchestration IPC",
33702
+ severity: "fail",
33703
+ summary: "daemon is running but orchestration IPC is not accepting connections",
33704
+ details: [`endpoint: ${endpoint.path}`],
33705
+ suggestions: ["run: xacpx restart"]
33706
+ };
33707
+ }
33708
+ async function defaultGetDaemonStatus(paths, options) {
33709
+ const controller = createDaemonController(paths, {
33710
+ processExecPath: options.processExecPath ?? process.execPath,
33711
+ cliEntryPath: options.cliEntryPath ?? process.argv[1] ?? "",
33712
+ cwd: options.cwd ?? process.cwd(),
33713
+ env: options.env ?? process.env,
33714
+ isProcessRunning: options.isProcessRunning ?? isProcessAlive
33715
+ });
33716
+ return await controller.getStatus();
33717
+ }
33718
+ function formatError7(error2) {
33719
+ return error2 instanceof Error ? error2.message : String(error2);
33720
+ }
33721
+ var init_orchestration_socket_check = __esm(() => {
33722
+ init_create_daemon_controller();
33723
+ init_daemon_files();
33724
+ init_endpoint_probe();
33725
+ init_orchestration_ipc();
33726
+ });
33727
+
33728
+ // src/doctor/checks/plugin-check.ts
33729
+ async function checkPlugins(options = {}) {
33730
+ const runtimePaths = (options.resolveRuntimePaths ?? resolveRuntimePaths)();
33731
+ let config4;
33732
+ try {
33733
+ config4 = await (options.loadConfig ?? loadConfig)(runtimePaths.configPath);
33734
+ } catch (error2) {
33735
+ return {
33736
+ id: "plugins",
33737
+ label: "Plugins",
33738
+ severity: "skip",
33739
+ summary: "plugin check skipped because configuration could not be loaded",
33740
+ details: [`config path: ${runtimePaths.configPath}`, `error: ${formatError8(error2)}`],
33741
+ suggestions: ["fix the Config check first, then run: xacpx doctor"]
33742
+ };
33743
+ }
33744
+ if (!hasPluginSurface(config4)) {
33745
+ return {
33746
+ id: "plugins",
33747
+ label: "Plugins",
33748
+ severity: "skip",
33749
+ summary: "no plugins configured"
33750
+ };
33751
+ }
33752
+ const pluginHome = (options.resolvePluginHome ?? resolvePluginHome)({ home: options.home });
33753
+ const inspect = options.inspectPlugins ?? inspectPlugins;
33754
+ let issues;
33755
+ try {
33756
+ issues = await inspect({
33757
+ config: config4,
33758
+ pluginHome,
33759
+ currentXacpxVersion: options.currentXacpxVersion ?? XACPX_CORE_VERSION
33760
+ });
33761
+ } catch (error2) {
33762
+ return {
33763
+ id: "plugins",
33764
+ label: "Plugins",
33765
+ severity: "fail",
33766
+ summary: "plugin health check failed",
33767
+ details: [`plugin home: ${pluginHome}`, `error: ${formatError8(error2)}`]
33768
+ };
33769
+ }
33770
+ const errorCount = issues.filter((issue2) => issue2.level === "error").length;
33771
+ const warnCount = issues.filter((issue2) => issue2.level === "warn").length;
33772
+ const severity = errorCount > 0 ? "fail" : warnCount > 0 ? "warn" : "pass";
33773
+ const problemCount = errorCount + warnCount;
33774
+ return {
33775
+ id: "plugins",
33776
+ label: "Plugins",
33777
+ severity,
33778
+ summary: problemCount > 0 ? `${problemCount} plugin issue(s)` : "all plugins healthy",
33779
+ details: issues.filter((issue2) => issue2.level !== "ok").map(formatIssueDetail),
33780
+ suggestions: collectSuggestions(issues),
33781
+ metadata: { pluginHome, errorCount, warnCount }
33782
+ };
33783
+ }
33784
+ function hasPluginSurface(config4) {
33785
+ if ((config4.plugins ?? []).length > 0) {
33786
+ return true;
33787
+ }
33788
+ const builtInChannelTypes = new Set(listKnownChannelIds());
33789
+ return (config4.channels ?? []).some((channel) => channel.enabled !== false && !builtInChannelTypes.has(channel.type));
33790
+ }
33791
+ function formatIssueDetail(issue2) {
33792
+ return issue2.plugin ? `${issue2.plugin}: ${issue2.message}` : issue2.message;
33793
+ }
33794
+ function collectSuggestions(issues) {
33795
+ const suggestions = [];
33796
+ const seen = new Set;
33797
+ for (const issue2 of issues) {
33798
+ const suggestion = issue2.suggestion;
33799
+ if (suggestion && !seen.has(suggestion)) {
33800
+ seen.add(suggestion);
33801
+ suggestions.push(`run: ${suggestion}`);
33802
+ }
33803
+ }
33804
+ return suggestions;
33805
+ }
33806
+ function formatError8(error2) {
33807
+ return error2 instanceof Error ? error2.message : String(error2);
33808
+ }
33809
+ var init_plugin_check = __esm(async () => {
33810
+ init_load_config();
33811
+ init_channel_scope();
33812
+ init_plugin_doctor();
33813
+ init_plugin_home();
33814
+ init_version();
33815
+ await init_main();
33816
+ });
33817
+
33320
33818
  // src/doctor/checks/runtime-check.ts
33321
33819
  import { constants } from "node:fs";
33322
- import { access as access4, stat as stat3 } from "node:fs/promises";
33820
+ import { access as access4, stat as stat4 } from "node:fs/promises";
33323
33821
  import { dirname as dirname13 } from "node:path";
33324
- import { homedir as homedir11 } from "node:os";
33822
+ import { homedir as homedir13 } from "node:os";
33325
33823
  async function checkRuntime(options = {}) {
33326
- const home = options.home ?? process.env.HOME ?? homedir11();
33824
+ const home = options.home ?? process.env.HOME ?? homedir13();
33327
33825
  const runtimeDir = options.configPath ? resolveRuntimeDirFromConfigPath(options.configPath) : undefined;
33328
33826
  const paths = (options.resolveDaemonPaths ?? resolveDaemonPaths)({
33329
33827
  home,
@@ -33349,6 +33847,20 @@ async function checkRuntime(options = {}) {
33349
33847
  details: checks3.map((check) => check.detail)
33350
33848
  };
33351
33849
  }
33850
+ const privacy = await inspectRuntimeDirPrivacy(paths.runtimeDir, probe, platform);
33851
+ if (privacy.needsRepair) {
33852
+ return {
33853
+ id: "runtime",
33854
+ label: "Runtime",
33855
+ severity: "warn",
33856
+ summary: "daemon runtime dir should be private (mode 0700)",
33857
+ details: [...checks3.map((check) => check.detail), privacy.detail],
33858
+ fixes: [createEnsurePrivateDirFix(paths.runtimeDir, options.ensurePrivateRuntimeDir)],
33859
+ metadata: {
33860
+ paths
33861
+ }
33862
+ };
33863
+ }
33352
33864
  return {
33353
33865
  id: "runtime",
33354
33866
  label: "Runtime",
@@ -33360,9 +33872,50 @@ async function checkRuntime(options = {}) {
33360
33872
  }
33361
33873
  };
33362
33874
  }
33875
+ async function inspectRuntimeDirPrivacy(runtimeDir, probe, platform) {
33876
+ if (platform === "win32") {
33877
+ return { needsRepair: false, detail: "" };
33878
+ }
33879
+ try {
33880
+ const stats = await probe.stat(runtimeDir);
33881
+ if (typeof stats.mode !== "number") {
33882
+ return { needsRepair: false, detail: "" };
33883
+ }
33884
+ const mode = stats.mode & 511;
33885
+ if (mode === PRIVATE_DIR_MODE) {
33886
+ return { needsRepair: false, detail: "" };
33887
+ }
33888
+ return {
33889
+ needsRepair: true,
33890
+ detail: `runtimeDir: ${runtimeDir} (mode ${formatMode(mode)} is not 0700; group/other access should be removed)`
33891
+ };
33892
+ } catch (error2) {
33893
+ if (isMissingPathError2(error2)) {
33894
+ return {
33895
+ needsRepair: true,
33896
+ detail: `runtimeDir: ${runtimeDir} (missing; will be created with mode 0700)`
33897
+ };
33898
+ }
33899
+ return { needsRepair: false, detail: "" };
33900
+ }
33901
+ }
33902
+ function createEnsurePrivateDirFix(runtimeDir, ensureImpl) {
33903
+ const ensure = ensureImpl ?? ((dir) => ensurePrivateRuntimeDir(dir));
33904
+ return {
33905
+ id: "runtime.ensure-private-dir",
33906
+ title: "create/repair runtime dir with mode 0700",
33907
+ run: async () => {
33908
+ await ensure(runtimeDir);
33909
+ return { ok: true, message: `runtime dir ${runtimeDir} created/repaired with mode 0700` };
33910
+ }
33911
+ };
33912
+ }
33913
+ function formatMode(mode) {
33914
+ return `0${(mode & 511).toString(8)}`;
33915
+ }
33363
33916
  function createRuntimeFsProbe() {
33364
33917
  return {
33365
- stat: async (path15) => await stat3(path15),
33918
+ stat: async (path15) => await stat4(path15),
33366
33919
  access: async (path15, mode) => await access4(path15, mode)
33367
33920
  };
33368
33921
  }
@@ -33381,10 +33934,10 @@ async function checkDirectoryCreatable(label, path15, probe, platform) {
33381
33934
  detail: `${label}: ${path15} (writable)`
33382
33935
  };
33383
33936
  } catch (error2) {
33384
- if (!isMissingPathError(error2)) {
33937
+ if (!isMissingPathError2(error2)) {
33385
33938
  return {
33386
33939
  ok: false,
33387
- detail: `${label}: ${path15} (unusable: ${formatError6(error2)})`
33940
+ detail: `${label}: ${path15} (unusable: ${formatError9(error2)})`
33388
33941
  };
33389
33942
  }
33390
33943
  const parentCheck = await checkCreatableAncestorDirectory(path15, probe, platform);
@@ -33415,10 +33968,10 @@ async function checkFileCreatable(label, path15, probe, platform) {
33415
33968
  detail: `${label}: ${path15} (writable)`
33416
33969
  };
33417
33970
  } catch (error2) {
33418
- if (!isMissingPathError(error2)) {
33971
+ if (!isMissingPathError2(error2)) {
33419
33972
  return {
33420
33973
  ok: false,
33421
- detail: `${label}: ${path15} (unusable: ${formatError6(error2)})`
33974
+ detail: `${label}: ${path15} (unusable: ${formatError9(error2)})`
33422
33975
  };
33423
33976
  }
33424
33977
  const parentCheck = await checkCreatableAncestorDirectory(dirname13(path15), probe, platform);
@@ -33450,7 +34003,7 @@ async function checkCreatableAncestorDirectory(path15, probe, platform) {
33450
34003
  creatableFrom: path15
33451
34004
  };
33452
34005
  } catch (error2) {
33453
- if (!isMissingPathError(error2)) {
34006
+ if (!isMissingPathError2(error2)) {
33454
34007
  return {
33455
34008
  ok: false,
33456
34009
  creatableFrom: path15,
@@ -33478,21 +34031,22 @@ async function checkCreatableAncestorDirectory(path15, probe, platform) {
33478
34031
  function directoryAccessMode(platform) {
33479
34032
  return platform === "win32" ? constants.W_OK : DIRECTORY_USABLE;
33480
34033
  }
33481
- function isMissingPathError(error2) {
34034
+ function isMissingPathError2(error2) {
33482
34035
  return isErrnoError(error2) && (error2.code === "ENOENT" || error2.code === "ENOTDIR");
33483
34036
  }
33484
34037
  function isErrnoError(error2) {
33485
34038
  return typeof error2 === "object" && error2 !== null && "code" in error2;
33486
34039
  }
33487
- function formatError6(error2) {
34040
+ function formatError9(error2) {
33488
34041
  if (error2 instanceof Error) {
33489
34042
  return error2.message;
33490
34043
  }
33491
34044
  return String(error2);
33492
34045
  }
33493
- var DIRECTORY_USABLE;
34046
+ var DIRECTORY_USABLE, PRIVATE_DIR_MODE = 448;
33494
34047
  var init_runtime_check = __esm(() => {
33495
34048
  init_daemon_files();
34049
+ init_private_runtime_dir();
33496
34050
  DIRECTORY_USABLE = constants.W_OK | constants.X_OK;
33497
34051
  });
33498
34052
 
@@ -33604,7 +34158,7 @@ async function checkSmoke(options = {}, deps = {}) {
33604
34158
  label: "Smoke",
33605
34159
  severity: "fail",
33606
34160
  summary: "smoke transport probe failed",
33607
- details: [`config path: ${runtimePaths.configPath}`, `error: ${formatError7(error2)}`]
34161
+ details: [`config path: ${runtimePaths.configPath}`, `error: ${formatError10(error2)}`]
33608
34162
  };
33609
34163
  }
33610
34164
  }
@@ -33731,7 +34285,7 @@ function buildDetails3(options) {
33731
34285
  }
33732
34286
  return details;
33733
34287
  }
33734
- function formatError7(error2) {
34288
+ function formatError10(error2) {
33735
34289
  return error2 instanceof Error ? error2.message : String(error2);
33736
34290
  }
33737
34291
  var SMOKE_PROMPT = "Reply with exactly: ok";
@@ -33756,7 +34310,7 @@ async function checkWechat(options = {}) {
33756
34310
  } catch (error2) {
33757
34311
  return {
33758
34312
  accountId,
33759
- error: formatError8(error2)
34313
+ error: formatError11(error2)
33760
34314
  };
33761
34315
  }
33762
34316
  });
@@ -33797,7 +34351,7 @@ function buildVerboseDetails(loggedIn, verbose, accounts) {
33797
34351
  }
33798
34352
  return details;
33799
34353
  }
33800
- function formatError8(error2) {
34354
+ function formatError11(error2) {
33801
34355
  return error2 instanceof Error ? error2.message : String(error2);
33802
34356
  }
33803
34357
  var init_wechat_check = __esm(() => {
@@ -33806,43 +34360,60 @@ var init_wechat_check = __esm(() => {
33806
34360
 
33807
34361
  // src/doctor/render-doctor.ts
33808
34362
  function renderDoctor(report, options = {}) {
33809
- return options.verbose ? renderVerboseDoctor(report) : renderDefaultDoctor(report);
34363
+ const fixMode = options.fix === true;
34364
+ return options.verbose ? renderVerboseDoctor(report, fixMode) : renderDefaultDoctor(report, fixMode);
33810
34365
  }
33811
- function renderDefaultDoctor(report) {
34366
+ function renderDefaultDoctor(report, fixMode) {
33812
34367
  const lines = [];
33813
34368
  for (const check of report.checks) {
33814
- lines.push(renderCheckLine(check));
34369
+ lines.push(renderCheckLine(check, fixMode));
33815
34370
  }
34371
+ appendRepairs(lines, report, fixMode);
33816
34372
  lines.push(renderSummaryLine(report.checks));
33817
- const suggestions = collectSuggestions(report.checks);
33818
- if (suggestions.length > 0) {
33819
- lines.push("Next steps:");
33820
- for (const suggestion of suggestions) {
33821
- lines.push(`- ${suggestion}`);
33822
- }
33823
- }
34373
+ appendNextSteps(lines, report.checks);
33824
34374
  return lines;
33825
34375
  }
33826
- function renderVerboseDoctor(report) {
34376
+ function renderVerboseDoctor(report, fixMode) {
33827
34377
  const lines = [];
33828
34378
  for (const check of report.checks) {
33829
- lines.push(renderCheckLine(check));
34379
+ lines.push(renderCheckLine(check, fixMode));
33830
34380
  for (const detail of check.details ?? []) {
33831
34381
  lines.push(` detail: ${detail}`);
33832
34382
  }
33833
34383
  }
34384
+ appendRepairs(lines, report, fixMode);
33834
34385
  lines.push(renderSummaryLine(report.checks));
33835
- const suggestions = collectSuggestions(report.checks);
34386
+ appendNextSteps(lines, report.checks);
34387
+ return lines;
34388
+ }
34389
+ function appendNextSteps(lines, checks3) {
34390
+ const suggestions = collectSuggestions2(checks3);
33836
34391
  if (suggestions.length > 0) {
33837
34392
  lines.push("Next steps:");
33838
34393
  for (const suggestion of suggestions) {
33839
34394
  lines.push(`- ${suggestion}`);
33840
34395
  }
33841
34396
  }
33842
- return lines;
33843
34397
  }
33844
- function renderCheckLine(check) {
33845
- return `${SEVERITY_LABELS[check.severity]} ${check.label}: ${check.summary}`;
34398
+ function appendRepairs(lines, report, fixMode) {
34399
+ if (!fixMode) {
34400
+ return;
34401
+ }
34402
+ const repairs = report.repairs ?? [];
34403
+ if (repairs.length === 0) {
34404
+ return;
34405
+ }
34406
+ lines.push("Repairs:");
34407
+ for (const repair of repairs) {
34408
+ lines.push(`- ${repair.title}: ${repair.status} (${repair.message})`);
34409
+ }
34410
+ }
34411
+ function renderCheckLine(check, fixMode) {
34412
+ const base = `${SEVERITY_LABELS[check.severity]} ${check.label}: ${check.summary}`;
34413
+ if (!fixMode && (check.fixes?.length ?? 0) > 0) {
34414
+ return `${base} (fixable — run: xacpx doctor --fix)`;
34415
+ }
34416
+ return base;
33846
34417
  }
33847
34418
  function renderSummaryLine(checks3) {
33848
34419
  const counts = summarizeChecks(checks3);
@@ -33854,7 +34425,7 @@ function summarizeChecks(checks3) {
33854
34425
  return counts;
33855
34426
  }, { pass: 0, warn: 0, fail: 0, skip: 0 });
33856
34427
  }
33857
- function collectSuggestions(checks3) {
34428
+ function collectSuggestions2(checks3) {
33858
34429
  const seen = new Set;
33859
34430
  const suggestions = [];
33860
34431
  for (const check of checks3) {
@@ -33879,47 +34450,112 @@ var init_render_doctor = __esm(() => {
33879
34450
  });
33880
34451
 
33881
34452
  // src/doctor/doctor.ts
33882
- import { homedir as homedir12 } from "node:os";
33883
- import { join as join19 } from "node:path";
34453
+ import { homedir as homedir14 } from "node:os";
34454
+ import { join as join21 } from "node:path";
33884
34455
  async function runDoctor(options = {}, deps = {}) {
33885
- const home = deps.home ?? process.env.HOME ?? homedir12();
34456
+ const home = deps.home ?? process.env.HOME ?? homedir14();
33886
34457
  const runtimePaths = resolveDoctorRuntimePaths(home, deps.resolveRuntimePaths);
33887
34458
  const sharedLoadConfig = createSharedLoadConfig(runtimePaths, deps.loadConfig ?? loadConfig);
34459
+ const runners = [
34460
+ {
34461
+ id: "config",
34462
+ run: () => (deps.checkConfig ?? checkConfig)({
34463
+ loadConfig: sharedLoadConfig,
34464
+ resolveRuntimePaths: () => runtimePaths
34465
+ })
34466
+ },
34467
+ {
34468
+ id: "runtime",
34469
+ run: () => (deps.checkRuntime ?? checkRuntime)({
34470
+ home,
34471
+ configPath: runtimePaths.configPath
34472
+ })
34473
+ },
34474
+ {
34475
+ id: "logs",
34476
+ run: () => (deps.checkLogs ?? checkLogs)({
34477
+ home,
34478
+ configPath: runtimePaths.configPath
34479
+ })
34480
+ },
34481
+ {
34482
+ id: "daemon",
34483
+ run: () => (deps.checkDaemon ?? checkDaemon)({
34484
+ home,
34485
+ configPath: runtimePaths.configPath
34486
+ })
34487
+ },
34488
+ {
34489
+ id: "wechat",
34490
+ run: () => (deps.checkWechat ?? checkWechat)({
34491
+ verbose: options.verbose
34492
+ })
34493
+ },
34494
+ {
34495
+ id: "acpx",
34496
+ run: () => (deps.checkAcpx ?? checkAcpx)({
34497
+ verbose: options.verbose,
34498
+ loadConfig: sharedLoadConfig,
34499
+ resolveRuntimePaths: () => runtimePaths
34500
+ })
34501
+ },
34502
+ {
34503
+ id: "bridge",
34504
+ run: () => (deps.checkBridge ?? checkBridge)({
34505
+ verbose: options.verbose,
34506
+ loadConfig: sharedLoadConfig,
34507
+ resolveRuntimePaths: () => runtimePaths
34508
+ })
34509
+ },
34510
+ {
34511
+ id: "plugins",
34512
+ run: () => (deps.checkPlugins ?? checkPlugins)({
34513
+ home,
34514
+ loadConfig: sharedLoadConfig,
34515
+ resolveRuntimePaths: () => runtimePaths
34516
+ })
34517
+ },
34518
+ {
34519
+ id: "orchestration",
34520
+ run: () => (deps.checkOrchestrationHealth ?? (() => defaultCheckOrchestrationHealth({
34521
+ runtimePaths,
34522
+ loadConfig: sharedLoadConfig,
34523
+ isDaemonRunning: deps.isDaemonRunning ?? (() => defaultIsDaemonRunning(home, runtimePaths, deps.getDaemonStatus))
34524
+ })))()
34525
+ },
34526
+ {
34527
+ id: "orchestration-socket",
34528
+ run: () => (deps.checkOrchestrationSocket ?? checkOrchestrationSocket)({
34529
+ home,
34530
+ configPath: runtimePaths.configPath
34531
+ })
34532
+ },
34533
+ {
34534
+ id: "smoke",
34535
+ run: () => options.smoke === true ? (deps.checkSmoke ?? ((runOptions) => defaultCheckSmoke(runOptions, {
34536
+ resolveRuntimePaths: () => runtimePaths,
34537
+ loadConfig: sharedLoadConfig
34538
+ })))(options) : Promise.resolve(createSmokeSkipResult("smoke probe not requested"))
34539
+ }
34540
+ ];
34541
+ const runnersById = new Map(runners.map((runner) => [runner.id, runner.run]));
33888
34542
  const checks3 = [];
33889
- checks3.push(await (deps.checkConfig ?? checkConfig)({
33890
- loadConfig: sharedLoadConfig,
33891
- resolveRuntimePaths: () => runtimePaths
33892
- }));
33893
- checks3.push(await (deps.checkRuntime ?? checkRuntime)({
33894
- home,
33895
- configPath: runtimePaths.configPath
33896
- }));
33897
- checks3.push(await (deps.checkDaemon ?? checkDaemon)({
33898
- home,
33899
- configPath: runtimePaths.configPath
33900
- }));
33901
- checks3.push(await (deps.checkWechat ?? checkWechat)({
33902
- verbose: options.verbose
33903
- }));
33904
- checks3.push(await (deps.checkAcpx ?? checkAcpx)({
33905
- verbose: options.verbose,
33906
- loadConfig: sharedLoadConfig,
33907
- resolveRuntimePaths: () => runtimePaths
33908
- }));
33909
- checks3.push(await (deps.checkBridge ?? checkBridge)({
33910
- verbose: options.verbose,
33911
- loadConfig: sharedLoadConfig,
33912
- resolveRuntimePaths: () => runtimePaths
33913
- }));
33914
- checks3.push(await (deps.checkOrchestrationHealth ?? (() => defaultCheckOrchestrationHealth({
33915
- runtimePaths,
33916
- loadConfig: sharedLoadConfig
33917
- })))());
33918
- checks3.push(options.smoke === true ? await (deps.checkSmoke ?? ((runOptions) => defaultCheckSmoke(runOptions, {
33919
- resolveRuntimePaths: () => runtimePaths,
33920
- loadConfig: sharedLoadConfig
33921
- })))(options) : createSmokeSkipResult("smoke probe not requested"));
34543
+ for (const runner of runners) {
34544
+ checks3.push(await runner.run());
34545
+ }
33922
34546
  const report = { checks: checks3 };
34547
+ if (options.fix === true) {
34548
+ const { repairs, repairedCheckIds } = await applyRepairs(checks3);
34549
+ report.repairs = repairs;
34550
+ for (const checkId of repairedCheckIds) {
34551
+ const index = checks3.findIndex((check) => check.id === checkId);
34552
+ const rerun = runnersById.get(checkId);
34553
+ if (index === -1 || !rerun) {
34554
+ continue;
34555
+ }
34556
+ checks3[index] = await rerun();
34557
+ }
34558
+ }
33923
34559
  const output = (deps.renderDoctor ?? renderDoctor)(report, options);
33924
34560
  return {
33925
34561
  report,
@@ -33927,6 +34563,41 @@ async function runDoctor(options = {}, deps = {}) {
33927
34563
  exitCode: checks3.some((check) => check.severity === "fail") ? 1 : 0
33928
34564
  };
33929
34565
  }
34566
+ async function applyRepairs(checks3) {
34567
+ const repairs = [];
34568
+ const repairedCheckIds = [];
34569
+ for (const check of checks3) {
34570
+ for (const fix of check.fixes ?? []) {
34571
+ if (fix.withheld !== undefined) {
34572
+ repairs.push({
34573
+ checkId: check.id,
34574
+ fixId: fix.id,
34575
+ title: fix.title,
34576
+ status: "skipped",
34577
+ message: fix.withheld
34578
+ });
34579
+ continue;
34580
+ }
34581
+ let outcome;
34582
+ try {
34583
+ outcome = await fix.run();
34584
+ } catch (error2) {
34585
+ outcome = { ok: false, message: formatError12(error2) };
34586
+ }
34587
+ repairs.push({
34588
+ checkId: check.id,
34589
+ fixId: fix.id,
34590
+ title: fix.title,
34591
+ status: outcome.ok ? "applied" : "failed",
34592
+ message: outcome.message
34593
+ });
34594
+ if (outcome.ok && !repairedCheckIds.includes(check.id)) {
34595
+ repairedCheckIds.push(check.id);
34596
+ }
34597
+ }
34598
+ }
34599
+ return { repairs, repairedCheckIds };
34600
+ }
33930
34601
  function resolveDoctorRuntimePaths(home, resolver) {
33931
34602
  if (resolver) {
33932
34603
  return resolver();
@@ -33935,8 +34606,8 @@ function resolveDoctorRuntimePaths(home, resolver) {
33935
34606
  return resolveRuntimePaths();
33936
34607
  }
33937
34608
  return {
33938
- configPath: join19(coreHomeDir(home), "config.json"),
33939
- statePath: join19(coreHomeDir(home), "state.json")
34609
+ configPath: join21(coreHomeDir(home), "config.json"),
34610
+ statePath: join21(coreHomeDir(home), "state.json")
33940
34611
  };
33941
34612
  }
33942
34613
  function depsUseExplicitRuntimeOverrides() {
@@ -33976,7 +34647,7 @@ async function defaultCheckOrchestrationHealth(deps) {
33976
34647
  label: "Orchestration",
33977
34648
  severity: "skip",
33978
34649
  summary: "orchestration check skipped because configuration could not be loaded",
33979
- details: [`config path: ${deps.runtimePaths.configPath}`, `error: ${formatError9(error2)}`],
34650
+ details: [`config path: ${deps.runtimePaths.configPath}`, `error: ${formatError12(error2)}`],
33980
34651
  suggestions: ["fix the Config check first, then run: xacpx doctor"]
33981
34652
  };
33982
34653
  }
@@ -33988,18 +34659,19 @@ async function defaultCheckOrchestrationHealth(deps) {
33988
34659
  now: () => new Date,
33989
34660
  heartbeatThresholdSeconds: config4.orchestration.progressHeartbeatSeconds
33990
34661
  });
33991
- return applyStateInspectionReport(result, inspection.report, deps.runtimePaths.statePath);
34662
+ const daemonRunning = inspection.report ? await deps.isDaemonRunning() : false;
34663
+ return applyStateInspectionReport(result, inspection.report, deps.runtimePaths.statePath, daemonRunning, deps.isDaemonRunning);
33992
34664
  } catch (error2) {
33993
34665
  return {
33994
34666
  id: "orchestration",
33995
34667
  label: "Orchestration",
33996
34668
  severity: "fail",
33997
34669
  summary: "orchestration health check failed",
33998
- details: [`state path: ${deps.runtimePaths.statePath}`, `error: ${formatError9(error2)}`]
34670
+ details: [`state path: ${deps.runtimePaths.statePath}`, `error: ${formatError12(error2)}`]
33999
34671
  };
34000
34672
  }
34001
34673
  }
34002
- function applyStateInspectionReport(result, report, statePath) {
34674
+ function applyStateInspectionReport(result, report, statePath, daemonRunning, isDaemonRunning) {
34003
34675
  if (!report) {
34004
34676
  return result;
34005
34677
  }
@@ -34017,18 +34689,74 @@ function applyStateInspectionReport(result, report, statePath) {
34017
34689
  suggestions: [
34018
34690
  ...result.suggestions ?? [],
34019
34691
  fileCorrupt ? "back up the state file before the next daemon start if you want to attempt manual recovery" : "the daemon backs the original file up as state.json.quarantine-* before dropping these records"
34020
- ]
34692
+ ],
34693
+ fixes: [createStateQuarantineFix(statePath, daemonRunning, isDaemonRunning)]
34021
34694
  };
34022
34695
  }
34023
- function formatError9(error2) {
34696
+ function createStateQuarantineFix(statePath, daemonRunning, isDaemonRunning) {
34697
+ return {
34698
+ id: "state.quarantine",
34699
+ title: "quarantine invalid state.json records",
34700
+ ...daemonRunning ? { withheld: "stop the daemon first: xacpx stop" } : {},
34701
+ run: async () => {
34702
+ if (await isDaemonRunning()) {
34703
+ return {
34704
+ ok: false,
34705
+ message: "a daemon started since detection; stop it first: xacpx stop"
34706
+ };
34707
+ }
34708
+ const store = new StateStore(statePath);
34709
+ await store.load();
34710
+ const report = store.lastLoadReport;
34711
+ if (!report) {
34712
+ return { ok: true, message: "state.json was already valid; nothing to quarantine" };
34713
+ }
34714
+ if (report.corruptPath) {
34715
+ return { ok: true, message: `state.json was unreadable; renamed to ${report.corruptPath} and reset` };
34716
+ }
34717
+ const backup = report.quarantinePath ? ` (original backed up to ${report.quarantinePath})` : "";
34718
+ return {
34719
+ ok: true,
34720
+ message: `quarantined ${report.dropped.length} invalid state.json record(s)${backup}`
34721
+ };
34722
+ }
34723
+ };
34724
+ }
34725
+ async function defaultIsDaemonRunning(home, runtimePaths, getDaemonStatus = () => defaultGetDaemonStatus2(home, runtimePaths)) {
34726
+ try {
34727
+ const status = await getDaemonStatus();
34728
+ return status.state === "running" || status.state === "indeterminate";
34729
+ } catch {
34730
+ return true;
34731
+ }
34732
+ }
34733
+ async function defaultGetDaemonStatus2(home, runtimePaths) {
34734
+ const paths = resolveDaemonPaths({
34735
+ home,
34736
+ runtimeDir: resolveRuntimeDirFromConfigPath(runtimePaths.configPath)
34737
+ });
34738
+ const controller = createDaemonController(paths, {
34739
+ processExecPath: process.execPath,
34740
+ cliEntryPath: process.argv[1] ?? "",
34741
+ cwd: process.cwd(),
34742
+ env: process.env,
34743
+ isProcessRunning: isProcessAlive
34744
+ });
34745
+ return await controller.getStatus();
34746
+ }
34747
+ function formatError12(error2) {
34024
34748
  return error2 instanceof Error ? error2.message : String(error2);
34025
34749
  }
34026
34750
  var init_doctor = __esm(async () => {
34027
34751
  init_core_home();
34028
34752
  init_load_config();
34753
+ init_create_daemon_controller();
34754
+ init_daemon_files();
34029
34755
  init_state_store();
34030
34756
  init_daemon_check();
34757
+ init_logs_check();
34031
34758
  init_orchestration_health();
34759
+ init_orchestration_socket_check();
34032
34760
  init_runtime_check();
34033
34761
  init_wechat_check();
34034
34762
  init_render_doctor();
@@ -34037,6 +34765,7 @@ var init_doctor = __esm(async () => {
34037
34765
  init_acpx_check(),
34038
34766
  init_bridge_check(),
34039
34767
  init_config_check(),
34768
+ init_plugin_check(),
34040
34769
  init_smoke_check()
34041
34770
  ]);
34042
34771
  });
@@ -34061,8 +34790,8 @@ var init_doctor2 = __esm(async () => {
34061
34790
  // src/cli.ts
34062
34791
  init_core_home();
34063
34792
  import { randomUUID as randomUUID4 } from "node:crypto";
34064
- import { homedir as homedir13 } from "node:os";
34065
- import { dirname as dirname14, join as join20, sep } from "node:path";
34793
+ import { homedir as homedir15 } from "node:os";
34794
+ import { dirname as dirname14, join as join22, sep } from "node:path";
34066
34795
  import { fileURLToPath as fileURLToPath7 } from "node:url";
34067
34796
 
34068
34797
  // src/runtime/migrate-core-home.ts
@@ -49109,118 +49838,7 @@ import { readFile as readFile11 } from "node:fs/promises";
49109
49838
  import { isAbsolute, join as join13, resolve } from "node:path";
49110
49839
  init_plugin_loader();
49111
49840
  init_validate_plugin();
49112
-
49113
- // src/plugins/plugin-doctor.ts
49114
- init_channel_scope();
49115
- init_plugin_loader();
49116
- init_validate_plugin();
49117
- init_known_plugins();
49118
- init_plugin_renames();
49119
- import { readFile as readFile10 } from "node:fs/promises";
49120
- import { join as join12 } from "node:path";
49121
- function suggestedPluginPackageForChannel(type) {
49122
- return findKnownPluginByChannel(type)?.packageName ?? `<npm-package-that-provides-${type}>`;
49123
- }
49124
- async function readDependencyEntries(pluginHome) {
49125
- try {
49126
- const raw = await readFile10(join12(pluginHome, "package.json"), "utf8");
49127
- const parsed = JSON.parse(raw);
49128
- const out = {};
49129
- for (const [name, value] of Object.entries(parsed.dependencies ?? {})) {
49130
- if (typeof value === "string")
49131
- out[name] = value;
49132
- }
49133
- return out;
49134
- } catch (error2) {
49135
- const message = error2 instanceof Error ? error2.message : String(error2);
49136
- throw new Error(`failed to read plugin home package.json: ${message}`);
49137
- }
49138
- }
49139
- async function inspectPlugins(input) {
49140
- const issues = [];
49141
- let dependencies;
49142
- try {
49143
- dependencies = await readDependencyEntries(input.pluginHome);
49144
- } catch (error2) {
49145
- const message = error2 instanceof Error ? error2.message : String(error2);
49146
- return [{ level: "error", message }];
49147
- }
49148
- const importPlugin = input.importPlugin ?? importPluginFromHome;
49149
- const allConfigured = input.config.plugins;
49150
- const filterByName = input.pluginName ? normalizePluginPackageName(input.pluginName) : null;
49151
- if (filterByName && !allConfigured.some((plugin) => normalizePluginPackageName(plugin.name) === filterByName)) {
49152
- return [{ level: "error", plugin: filterByName, message: `plugin is not configured; run xacpx plugin add ${filterByName}` }];
49153
- }
49154
- const pushIfRelevant = (issue2) => {
49155
- if (!filterByName || issue2.plugin === filterByName)
49156
- issues.push(issue2);
49157
- };
49158
- const channelProviders = new Map;
49159
- for (const configPlugin of allConfigured) {
49160
- if (!(configPlugin.name in dependencies)) {
49161
- pushIfRelevant({ level: "error", plugin: configPlugin.name, message: `package not installed in plugin home; run xacpx plugin add ${configPlugin.name}` });
49162
- continue;
49163
- }
49164
- let moduleValue;
49165
- try {
49166
- moduleValue = await importPlugin(configPlugin.name, input.pluginHome);
49167
- } catch (error2) {
49168
- const message = error2 instanceof Error ? error2.message : String(error2);
49169
- pushIfRelevant({ level: "error", plugin: configPlugin.name, message: `failed to import plugin: ${message}` });
49170
- continue;
49171
- }
49172
- try {
49173
- const plugin = validateWeacpxPlugin(moduleValue, configPlugin.name, {
49174
- ...input.currentXacpxVersion !== undefined ? { currentXacpxVersion: input.currentXacpxVersion } : {}
49175
- });
49176
- const channels = plugin.channels ?? [];
49177
- const channelTypes = channels.map((channel) => channel.type);
49178
- for (const type of channelTypes) {
49179
- const existing = channelProviders.get(type);
49180
- if (existing) {
49181
- pushIfRelevant({ level: "error", plugin: configPlugin.name, message: `channel type ${type} is already provided by ${existing.plugin}` });
49182
- } else {
49183
- channelProviders.set(type, { plugin: configPlugin.name, enabled: configPlugin.enabled });
49184
- }
49185
- }
49186
- pushIfRelevant({
49187
- level: configPlugin.enabled ? "ok" : "warn",
49188
- plugin: configPlugin.name,
49189
- message: configPlugin.enabled ? `plugin is installed and valid; channels: ${channelTypes.length > 0 ? channelTypes.join(", ") : "none"}` : `plugin is installed and valid but disabled; run xacpx plugin enable ${configPlugin.name}`
49190
- });
49191
- } catch (error2) {
49192
- const message = error2 instanceof Error ? error2.message : String(error2);
49193
- pushIfRelevant({ level: "error", plugin: configPlugin.name, message });
49194
- }
49195
- }
49196
- const builtInChannelTypes = new Set(listKnownChannelIds());
49197
- for (const channel of input.config.channels) {
49198
- if (channel.enabled === false)
49199
- continue;
49200
- if (builtInChannelTypes.has(channel.type))
49201
- continue;
49202
- const provider = channelProviders.get(channel.type);
49203
- if (!provider) {
49204
- if (!filterByName) {
49205
- issues.push({
49206
- level: "error",
49207
- message: `channel ${channel.type} is configured but no enabled plugin provides it; run xacpx plugin add ${suggestedPluginPackageForChannel(channel.type)} or another plugin that provides type "${channel.type}"`
49208
- });
49209
- }
49210
- continue;
49211
- }
49212
- if (!provider.enabled) {
49213
- pushIfRelevant({
49214
- level: "error",
49215
- plugin: provider.plugin,
49216
- message: `channel ${channel.type} is configured but provider plugin is disabled; run xacpx plugin enable ${provider.plugin}`
49217
- });
49218
- }
49219
- }
49220
- return issues;
49221
- }
49222
-
49223
- // src/plugins/plugin-cli.ts
49841
+ init_plugin_doctor();
49224
49842
  init_known_plugins();
49225
49843
  init_plugin_renames();
49226
49844
  init_i18n();
@@ -50416,7 +51034,7 @@ async function createCliScheduledTaskService() {
50416
51034
  return new ScheduledTaskService(state, stateStore);
50417
51035
  }
50418
51036
  function resolveConfigPathForCurrentEnv() {
50419
- return coreEnv("CONFIG") ?? join20(coreHomeDir(requireHome2()), "config.json");
51037
+ return coreEnv("CONFIG") ?? join22(coreHomeDir(requireHome2()), "config.json");
50420
51038
  }
50421
51039
  function resolveDaemonPathsForCurrentConfig() {
50422
51040
  const configPath = resolveConfigPathForCurrentEnv();
@@ -50763,7 +51381,7 @@ function decodeFirstRunOnboarding(raw) {
50763
51381
  return null;
50764
51382
  }
50765
51383
  function requireHome2() {
50766
- const home = process.env.HOME ?? homedir13();
51384
+ const home = process.env.HOME ?? homedir15();
50767
51385
  if (!home) {
50768
51386
  throw new Error("Unable to resolve the current user home directory");
50769
51387
  }
@@ -50787,7 +51405,7 @@ function safeDaemonLogPaths() {
50787
51405
  const configPath = resolveConfigPathForCurrentEnv();
50788
51406
  const paths = resolveDaemonPathsForCurrentConfig();
50789
51407
  return {
50790
- appLog: join20(dirname14(configPath), "runtime", "app.log"),
51408
+ appLog: join22(dirname14(configPath), "runtime", "app.log"),
50791
51409
  stderrLog: paths.stderrLog
50792
51410
  };
50793
51411
  } catch {
@@ -50811,6 +51429,9 @@ function parseDoctorArgs(args) {
50811
51429
  case "--smoke":
50812
51430
  options.smoke = true;
50813
51431
  break;
51432
+ case "--fix":
51433
+ options.fix = true;
51434
+ break;
50814
51435
  case "--agent": {
50815
51436
  const value = args[index + 1];
50816
51437
  if (!value || value.startsWith("--")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ganglion/xacpx",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "随时随地通过聊天频道(微信 / 飞书 / 元宝等)远程控制 `acpx` 上的 Claude Code、Codex 等 Agents。",
5
5
  "keywords": [
6
6
  "acpx",