Restore missing dsh-netxops host bridge after dirty reinstalls (0.1.53).

Preset-only self-heal left tools waiting forever when the bundle host row was dropped; insert the full host block into cordis.patch.yml and require one restart.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-10-10 17:00:38 +08:00
parent e2e65b0324
commit 777589b702
12 changed files with 1623 additions and 73 deletions

View file

@ -26,6 +26,7 @@ import { homedir } from "node:os";
import { join } from "node:path";
var DESKTOP_PATCH_BEGIN = "# BEGIN dsh-netxops-preset (managed)";
var DESKTOP_PATCH_END = "# END dsh-netxops-preset (managed)";
var HOST_HEAL_ONCE = Symbol.for("dsh-netxops.host-bridge-heal-attempted");
var NETXOPS_HOST_CONFIG_DEFAULTS = Object.freeze({
apiUrl: "http://127.0.0.1:8890",
lang: "zh",
@ -147,6 +148,15 @@ function findHostEntry(lines, skip) {
}
return null;
}
function hostBridgeNamedElsewhere(lines, skip) {
for (let i = 0;i < lines.length; i += 1) {
if (skip[i])
continue;
if (/^\s+name:\s*['"]?dsh-netxops['"]?\s*$/.test(lines[i]))
return true;
}
return false;
}
function needsHeal(entry) {
if (entry.name !== "dsh-netxops")
return true;
@ -164,7 +174,7 @@ function renderHostEntry(entry) {
];
if (entry.disabled === true)
out.push(" disabled: true");
else if (entry.disabled === false)
else
out.push(" disabled: false");
out.push(" config:");
for (const [key, value] of Object.entries(config)) {
@ -172,40 +182,66 @@ function renderHostEntry(entry) {
}
return out;
}
function healNetxopsProfileEntries(logger) {
function ensureNetxopsHostInProfilePatches(logger) {
let rewritten = 0;
let inserted = 0;
for (const profileName of ["desktop", "web"]) {
const patchPath = join(resolveDshHome(), "profiles", profileName, "cordis.patch.yml");
if (!existsSync(patchPath))
const profileDir = join(resolveDshHome(), "profiles", profileName);
const patchPath = join(profileDir, "cordis.patch.yml");
if (!existsSync(profileDir))
continue;
const raw = readFileSync(patchPath, "utf8");
const raw = existsSync(patchPath) ? readFileSync(patchPath, "utf8") : "";
const nl = raw.includes(`\r
`) ? `\r
` : `
`;
const lines = raw.replace(/\r\n/g, `
const lines = raw.length > 0 ? raw.replace(/\r\n/g, `
`).split(`
`);
`) : [];
const skip = managedLineMask(lines);
const entry = findHostEntry(lines, skip);
if (entry === null)
let nextLines = null;
let kind = null;
if (entry === null) {
if (hostBridgeNamedElsewhere(lines, skip))
continue;
const rendered = renderHostEntry({ disabled: false, config: {} });
const beginIdx = lines.findIndex((line) => line.includes(DESKTOP_PATCH_BEGIN));
if (beginIdx >= 0) {
nextLines = [...lines.slice(0, beginIdx), ...rendered, "", ...lines.slice(beginIdx)];
} else if (lines.length === 0 || lines.length === 1 && lines[0].trim() === "") {
nextLines = [...rendered, ""];
} else {
const body = [...lines];
while (body.length > 0 && body[body.length - 1].trim() === "")
body.pop();
nextLines = [...body, "", ...rendered, ""];
}
kind = "insert";
} else if (needsHeal(entry)) {
nextLines = [
...lines.slice(0, entry.start),
...renderHostEntry(entry),
...lines.slice(entry.end)
];
kind = "rewrite";
}
if (nextLines === null || kind === null)
continue;
if (!needsHeal(entry))
const normalized = nextLines.join(nl);
const withNl = normalized.endsWith(nl) ? normalized : `${normalized}${nl}`;
if (withNl === raw)
continue;
const rendered = renderHostEntry(entry);
const next = [
...lines.slice(0, entry.start),
...rendered,
...lines.slice(entry.end)
].join(nl);
const normalized = next.endsWith(nl) ? next : `${next}${nl}`;
if (normalized === raw)
continue;
writeFileSync(patchPath, normalized, "utf8");
rewritten += 1;
logger.info("netxops: healed sparse host entry in profiles/%s/cordis.patch.yml " + "(public toggles hot-reload via settings; restart %s only if tools still missing after heal)", profileName, profileName === "desktop" ? "Desktop" : "dsh web");
writeFileSync(patchPath, withNl, "utf8");
if (kind === "insert") {
inserted += 1;
logger.error("netxops: host bridge dsh-netxops was MISSING from profiles/%s/cordis.patch.yml — " + "wrote it back. Fully quit and restart %s once so the host publishes the connection " + "(preset-only installs leave netxops-tools waiting forever).", profileName, profileName === "desktop" ? "Desktop" : "dsh web");
} else {
rewritten += 1;
logger.info("netxops: healed sparse host entry in profiles/%s/cordis.patch.yml " + "(public toggles hot-reload via settings; restart %s only if tools still missing)", profileName, profileName === "desktop" ? "Desktop" : "dsh web");
}
}
return rewritten;
return { rewritten, inserted };
}
// src/agent-preset-install.ts
@ -695,7 +731,7 @@ function ensureAgentPresetInstalled(logger) {
logger.error("netxops: failed to sync declarative preset into profile patch: %s", error);
}
try {
healNetxopsProfileEntries(logger);
ensureNetxopsHostInProfilePatches(logger);
} catch (error) {
logger.error("netxops: failed to heal host profile entry: %s", error);
}