mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-15 13:23:22 +08:00
Fix Desktop Netx Ops preset using host standard and workflow-ptc.
Prefer Desktop's standard.patch.yml / align CLI worker-thread rows to workflow-ptc, and refuse compositions the host cannot resolve. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
7a289cf409
commit
6bf5268dfb
8 changed files with 688 additions and 60 deletions
|
|
@ -9,7 +9,7 @@ One install wires **all of**:
|
||||||
| Host tools | Capability groups **ops / topology** (one group ↔ one skill). Default ops in Ops preset; topology and all public off. Other agents may mount `dsh-netxops/tools-ops|topology` |
|
| Host tools | Capability groups **ops / topology** (one group ↔ one skill). Default ops in Ops preset; topology and all public off. Other agents may mount `dsh-netxops/tools-ops|topology` |
|
||||||
| Companion plugins | **dsh-im-ops** + **dsh-ops-cron** — add as **direct** profile deps in the same command (DSH/pnpm blocks `github:` as transitive deps) |
|
| Companion plugins | **dsh-im-ops** + **dsh-ops-cron** — add as **direct** profile deps in the same command (DSH/pnpm blocks `github:` as transitive deps) |
|
||||||
| Plugins card | Settings → Plugins → **Netx Ops** (URL / API lang / thinking+reply language / token / capability groups) |
|
| Plugins card | Settings → Plugins → **Netx Ops** (URL / API lang / thinking+reply language / token / capability groups) |
|
||||||
| Agent preset + skills | Settings → Agent presets → **Netx Ops**. Host composes from shipped **standard** + Netx Ops overlays (no preset inheritance): web uses `~/.dsh/.agent-presets/netxops`; Desktop 0.2 also syncs a declarative `@deepseek-ai/dsh-agent-preset` row into the profile patch. Restart Desktop once after upgrade. Playbooks follow the same group toggles |
|
| Agent preset + skills | Settings → Agent presets → **Netx Ops**. Host composes from **this host's** shipped **standard** + Netx Ops overlays (no preset inheritance): Desktop 0.2 prefers `@deepseek-ai/dsh-web-app/presets/standard.patch.yml` (`workflow-ptc`); web/CLI use `dsh-agent-presets` directory standard. Web installs land in `~/.dsh/.agent-presets/netxops`; Desktop also syncs a declarative `@deepseek-ai/dsh-agent-preset` row into the profile patch. Restart Desktop once after upgrade. Playbooks follow the same group toggles |
|
||||||
|
|
||||||
You do **not** run `link-preset.ps1` for normal use. That script is only a manual fallback.
|
You do **not** run `link-preset.ps1` for normal use. That script is only a manual fallback.
|
||||||
|
|
||||||
|
|
|
||||||
266
lib/index.js
266
lib/index.js
|
|
@ -22,6 +22,7 @@ import { fileURLToPath } from "node:url";
|
||||||
var NETXOPS_PRESET_ID = "netxops";
|
var NETXOPS_PRESET_ID = "netxops";
|
||||||
var DESKTOP_PATCH_BEGIN = "# BEGIN dsh-netxops-preset (managed)";
|
var DESKTOP_PATCH_BEGIN = "# BEGIN dsh-netxops-preset (managed)";
|
||||||
var DESKTOP_PATCH_END = "# END dsh-netxops-preset (managed)";
|
var DESKTOP_PATCH_END = "# END dsh-netxops-preset (managed)";
|
||||||
|
var PATCH_PLUGIN_INDENT = " ";
|
||||||
function packageRoot() {
|
function packageRoot() {
|
||||||
return join(dirname(fileURLToPath(import.meta.url)), "..");
|
return join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
}
|
}
|
||||||
|
|
@ -31,22 +32,72 @@ function resolveDshHome() {
|
||||||
return fromEnv;
|
return fromEnv;
|
||||||
return join(homedir(), ".dsh");
|
return join(homedir(), ".dsh");
|
||||||
}
|
}
|
||||||
|
function electronResourcesRoots() {
|
||||||
|
const roots = [];
|
||||||
|
const rp = process.resourcesPath;
|
||||||
|
if (typeof rp === "string" && rp.length > 0)
|
||||||
|
roots.push(rp);
|
||||||
|
roots.push(join(dirname(process.execPath), "resources"));
|
||||||
|
return roots;
|
||||||
|
}
|
||||||
|
function readableFile(path) {
|
||||||
|
try {
|
||||||
|
readFileSync(path, { encoding: "utf8", flag: "r" });
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function resolveDesktopStandardPatchPath() {
|
||||||
|
const dshHome = resolveDshHome();
|
||||||
|
const envOverride = process.env.NETXOPS_STANDARD_PATCH?.trim();
|
||||||
|
const candidates = [];
|
||||||
|
if (envOverride)
|
||||||
|
candidates.push(envOverride);
|
||||||
|
for (const resources of electronResourcesRoots()) {
|
||||||
|
candidates.push(join(resources, "app.asar", "dsh", "node_modules", "@deepseek-ai", "dsh-web-app", "presets", "standard.patch.yml"), join(resources, "app.asar.unpacked", "dsh", "node_modules", "@deepseek-ai", "dsh-web-app", "presets", "standard.patch.yml"));
|
||||||
|
}
|
||||||
|
const requireAnchors = [
|
||||||
|
join(dshHome, "profiles", "desktop", "package.json"),
|
||||||
|
join(dshHome, "profiles", "desktop", "node_modules", "dsh-netxops", "package.json"),
|
||||||
|
join(packageRoot(), "package.json"),
|
||||||
|
join(dshHome, "profiles", "node_modules", "@deepseek-ai", "dsh-web-app", "package.json"),
|
||||||
|
join(dshHome, "profiles", "web", "package.json")
|
||||||
|
];
|
||||||
|
for (const anchor of requireAnchors) {
|
||||||
|
if (!existsSync(anchor))
|
||||||
|
continue;
|
||||||
|
try {
|
||||||
|
const req = createRequire2(anchor);
|
||||||
|
try {
|
||||||
|
candidates.push(req.resolve("@deepseek-ai/dsh-web-app/presets/standard.patch.yml"));
|
||||||
|
} catch {}
|
||||||
|
const pkg = dirname(req.resolve("@deepseek-ai/dsh-web-app/package.json"));
|
||||||
|
candidates.push(join(pkg, "presets", "standard.patch.yml"));
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
for (const path of candidates) {
|
||||||
|
if (readableFile(path))
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
function resolveStandardPresetDir() {
|
function resolveStandardPresetDir() {
|
||||||
const dshHome = resolveDshHome();
|
const dshHome = resolveDshHome();
|
||||||
const candidates = [
|
const candidates = [
|
||||||
join(dshHome, "profiles", "node_modules", "@deepseek-ai", "dsh-agent-presets", "presets", "standard"),
|
|
||||||
join(dshHome, "profiles", "web", "node_modules", "@deepseek-ai", "dsh-agent-presets", "presets", "standard"),
|
|
||||||
join(dshHome, "profiles", "desktop", "node_modules", "@deepseek-ai", "dsh-agent-presets", "presets", "standard"),
|
join(dshHome, "profiles", "desktop", "node_modules", "@deepseek-ai", "dsh-agent-presets", "presets", "standard"),
|
||||||
join(dirname(fileURLToPath(import.meta.url)), "..", "..", "DeepSeekHarness", "packages", "preset", "agent-presets", "presets", "standard")
|
join(dshHome, "profiles", "web", "node_modules", "@deepseek-ai", "dsh-agent-presets", "presets", "standard"),
|
||||||
|
join(dirname(fileURLToPath(import.meta.url)), "..", "..", "DeepSeekHarness", "packages", "preset", "agent-presets", "presets", "standard"),
|
||||||
|
join(dshHome, "profiles", "node_modules", "@deepseek-ai", "dsh-agent-presets", "presets", "standard")
|
||||||
];
|
];
|
||||||
for (const dir of candidates) {
|
for (const dir of candidates) {
|
||||||
if (existsSync(join(dir, "agent.cordis.yml")))
|
if (existsSync(join(dir, "agent.cordis.yml")))
|
||||||
return dir;
|
return dir;
|
||||||
}
|
}
|
||||||
for (const nm of [
|
for (const nm of [
|
||||||
join(dshHome, "profiles", "node_modules"),
|
join(dshHome, "profiles", "desktop", "node_modules"),
|
||||||
join(dshHome, "profiles", "web", "node_modules"),
|
join(dshHome, "profiles", "web", "node_modules"),
|
||||||
join(dshHome, "profiles", "desktop", "node_modules")
|
join(dshHome, "profiles", "node_modules")
|
||||||
]) {
|
]) {
|
||||||
const pkgJson = join(nm, "@deepseek-ai", "dsh-agent-presets", "package.json");
|
const pkgJson = join(nm, "@deepseek-ai", "dsh-agent-presets", "package.json");
|
||||||
if (!existsSync(pkgJson))
|
if (!existsSync(pkgJson))
|
||||||
|
|
@ -68,6 +119,155 @@ function resolveStandardPresetDir() {
|
||||||
} catch {}
|
} catch {}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
function desktopRuntimeLikelyPresent() {
|
||||||
|
if (typeof process.versions.electron === "string")
|
||||||
|
return true;
|
||||||
|
for (const resources of electronResourcesRoots()) {
|
||||||
|
if (existsSync(join(resources, "app.asar")))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const localAppData = process.env.LOCALAPPDATA?.trim();
|
||||||
|
if (localAppData) {
|
||||||
|
if (existsSync(join(localAppData, "Programs", "DeepSeek Harness", "resources", "app.asar"))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function hostResolveBases() {
|
||||||
|
const dshHome = resolveDshHome();
|
||||||
|
const bases = [];
|
||||||
|
for (const resources of electronResourcesRoots()) {
|
||||||
|
bases.push(join(resources, "app.asar", "dsh", "package.json"));
|
||||||
|
bases.push(join(resources, "app.asar", "dsh", "node_modules"));
|
||||||
|
}
|
||||||
|
const localAppData = process.env.LOCALAPPDATA?.trim();
|
||||||
|
if (localAppData) {
|
||||||
|
const asarDsh = join(localAppData, "Programs", "DeepSeek Harness", "resources", "app.asar", "dsh");
|
||||||
|
bases.push(join(asarDsh, "package.json"), join(asarDsh, "node_modules"));
|
||||||
|
}
|
||||||
|
bases.push(join(dshHome, "profiles", "desktop", "package.json"), join(dshHome, "profiles", "desktop", "node_modules"), join(dshHome, "profiles", "web", "package.json"), join(dshHome, "profiles", "web", "node_modules"), join(dshHome, "profiles", "node_modules"), join(packageRoot(), "package.json"));
|
||||||
|
return [...new Set(bases.filter((path) => path.includes("app.asar") || existsSync(path)))];
|
||||||
|
}
|
||||||
|
function packageInstalledAbove(name, bases) {
|
||||||
|
if (name.startsWith("cordis:") || name.startsWith("node:"))
|
||||||
|
return true;
|
||||||
|
const pkg = name.split("/").slice(0, name.startsWith("@") ? 2 : 1).join("/");
|
||||||
|
for (const base of bases) {
|
||||||
|
let dir = base.endsWith("package.json") ? dirname(base) : base;
|
||||||
|
for (;; ) {
|
||||||
|
if (readableFile(join(dir, "node_modules", pkg, "package.json")))
|
||||||
|
return true;
|
||||||
|
if (readableFile(join(dir, pkg, "package.json")))
|
||||||
|
return true;
|
||||||
|
const parent = dirname(dir);
|
||||||
|
if (parent === dir)
|
||||||
|
break;
|
||||||
|
dir = parent;
|
||||||
|
}
|
||||||
|
const requireAnchor = base.endsWith("package.json") ? base : existsSync(join(base, "package.json")) ? join(base, "package.json") : null;
|
||||||
|
if (requireAnchor !== null) {
|
||||||
|
try {
|
||||||
|
createRequire2(requireAnchor).resolve(`${pkg}/package.json`);
|
||||||
|
return true;
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function collectCompositionPackageNames(yaml) {
|
||||||
|
const names = [];
|
||||||
|
for (const match of yaml.matchAll(/^\s*name:\s*(.+)$/gm)) {
|
||||||
|
let raw = match[1].trim();
|
||||||
|
if (raw.startsWith("'") && raw.endsWith("'") || raw.startsWith('"') && raw.endsWith('"')) {
|
||||||
|
raw = raw.slice(1, -1);
|
||||||
|
}
|
||||||
|
if (raw.length === 0)
|
||||||
|
continue;
|
||||||
|
if (raw.startsWith("./") || raw.startsWith("../") || raw.startsWith("file:"))
|
||||||
|
continue;
|
||||||
|
names.push(raw);
|
||||||
|
}
|
||||||
|
return [...new Set(names)];
|
||||||
|
}
|
||||||
|
function unresolvableCompositionPackages(yaml, bases) {
|
||||||
|
return collectCompositionPackageNames(yaml).filter((name) => !packageInstalledAbove(name, bases));
|
||||||
|
}
|
||||||
|
function remapWorkflowToPtc(cordis) {
|
||||||
|
return cordis.replaceAll("- id: workflow-worker-thread", "- id: workflow-ptc").replaceAll("@deepseek-ai/dsh-workflow-worker-thread", "@deepseek-ai/dsh-workflow-ptc");
|
||||||
|
}
|
||||||
|
function remapWorkflowToWorkerThread(cordis) {
|
||||||
|
return cordis.replaceAll("- id: workflow-ptc", "- id: workflow-worker-thread").replaceAll("@deepseek-ai/dsh-workflow-ptc", "@deepseek-ai/dsh-workflow-worker-thread");
|
||||||
|
}
|
||||||
|
function alignWorkflowEngineForHost(cordis, bases, host = "auto") {
|
||||||
|
const hasPtc = packageInstalledAbove("@deepseek-ai/dsh-workflow-ptc", bases);
|
||||||
|
const hasWorker = packageInstalledAbove("@deepseek-ai/dsh-workflow-worker-thread", bases);
|
||||||
|
const desktop = host === "desktop" || host === "auto" && desktopRuntimeLikelyPresent();
|
||||||
|
if (hasPtc && !hasWorker)
|
||||||
|
return remapWorkflowToPtc(cordis);
|
||||||
|
if (hasPtc && hasWorker && desktop)
|
||||||
|
return remapWorkflowToPtc(cordis);
|
||||||
|
if (!hasPtc && hasWorker && desktop) {
|
||||||
|
return remapWorkflowToPtc(cordis);
|
||||||
|
}
|
||||||
|
if (!hasPtc && !hasWorker && desktop && cordis.includes("workflow-worker-thread")) {
|
||||||
|
return remapWorkflowToPtc(cordis);
|
||||||
|
}
|
||||||
|
if (hasWorker && !hasPtc && !desktop)
|
||||||
|
return remapWorkflowToWorkerThread(cordis);
|
||||||
|
if (hasWorker && hasPtc && host === "web")
|
||||||
|
return remapWorkflowToWorkerThread(cordis);
|
||||||
|
return cordis;
|
||||||
|
}
|
||||||
|
function extractPluginsBodyFromStandardPatch(patchYaml) {
|
||||||
|
const text = patchYaml.replace(/\r\n/g, `
|
||||||
|
`);
|
||||||
|
const marker = /^([ \t]*)plugins:\s*$/m;
|
||||||
|
const match = marker.exec(text);
|
||||||
|
if (match === null) {
|
||||||
|
throw new Error("standard.patch.yml has no plugins: block");
|
||||||
|
}
|
||||||
|
const pluginsKeyIndent = match[1].length;
|
||||||
|
const start = match.index + match[0].length;
|
||||||
|
const rest = text.slice(start).replace(/^\n/, "");
|
||||||
|
const lines = rest.split(`
|
||||||
|
`);
|
||||||
|
const body = [];
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.trim() === "") {
|
||||||
|
body.push(line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const indent = line.match(/^[ \t]*/)?.[0].length ?? 0;
|
||||||
|
if (indent <= pluginsKeyIndent)
|
||||||
|
break;
|
||||||
|
body.push(line);
|
||||||
|
}
|
||||||
|
while (body.length > 0 && body[body.length - 1].trim() === "")
|
||||||
|
body.pop();
|
||||||
|
return body.join(`
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
function pluginsBodyToAgentCordis(pluginsBody) {
|
||||||
|
const prefix = PATCH_PLUGIN_INDENT;
|
||||||
|
return `${pluginsBody.replace(/\r\n/g, `
|
||||||
|
`).split(`
|
||||||
|
`).map((line) => {
|
||||||
|
if (line.length === 0)
|
||||||
|
return "";
|
||||||
|
if (line.startsWith(prefix))
|
||||||
|
return line.slice(prefix.length);
|
||||||
|
return line.trimStart();
|
||||||
|
}).join(`
|
||||||
|
`).trimEnd()}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
function agentCordisToPluginsBody(agentCordis) {
|
||||||
|
return agentCordis.replace(/\r\n/g, `
|
||||||
|
`).split(`
|
||||||
|
`).map((line) => line.length === 0 ? "" : `${PATCH_PLUGIN_INDENT}${line}`).join(`
|
||||||
|
`);
|
||||||
|
}
|
||||||
function replaceTopLevelEntry(cordis, id, replacement) {
|
function replaceTopLevelEntry(cordis, id, replacement) {
|
||||||
const lines = cordis.replace(/\r\n/g, `
|
const lines = cordis.replace(/\r\n/g, `
|
||||||
`).split(`
|
`).split(`
|
||||||
|
|
@ -146,6 +346,9 @@ function readPresetMeta(metaPath) {
|
||||||
order: (metaText.match(/^order:\s*(\d+)/m) || [])[1] || "50"
|
order: (metaText.match(/^order:\s*(\d+)/m) || [])[1] || "50"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
function desktopProfilePresent() {
|
||||||
|
return existsSync(join(resolveDshHome(), "profiles", "desktop"));
|
||||||
|
}
|
||||||
function composeNetxopsPresetArtifacts() {
|
function composeNetxopsPresetArtifacts() {
|
||||||
const overlayRoot = join(packageRoot(), "presets", NETXOPS_PRESET_ID);
|
const overlayRoot = join(packageRoot(), "presets", NETXOPS_PRESET_ID);
|
||||||
const personaPath = join(overlayRoot, "PERSONA.md");
|
const personaPath = join(overlayRoot, "PERSONA.md");
|
||||||
|
|
@ -153,21 +356,43 @@ function composeNetxopsPresetArtifacts() {
|
||||||
if (!existsSync(personaPath) || !existsSync(metaPath)) {
|
if (!existsSync(personaPath) || !existsSync(metaPath)) {
|
||||||
throw new Error(`bundled preset overlay missing under ${overlayRoot}`);
|
throw new Error(`bundled preset overlay missing under ${overlayRoot}`);
|
||||||
}
|
}
|
||||||
const standardDir = resolveStandardPresetDir();
|
|
||||||
if (standardDir === null) {
|
|
||||||
throw new Error("cannot find shipped standard preset (@deepseek-ai/dsh-agent-presets)");
|
|
||||||
}
|
|
||||||
const meta = readPresetMeta(metaPath);
|
const meta = readPresetMeta(metaPath);
|
||||||
let agentCordis = replaceTopLevelEntry(readFileSync(join(standardDir, "agent.cordis.yml"), "utf8"), "persona", personaEntryFromMarkdown(readFileSync(personaPath, "utf8")));
|
const bases = hostResolveBases();
|
||||||
|
const patchPath = resolveDesktopStandardPatchPath();
|
||||||
|
const usePatch = patchPath !== null && process.env.NETXOPS_PREFER_DESKTOP_PATCH !== "0" && (typeof process.versions.electron === "string" || process.env.NETXOPS_PREFER_DESKTOP_PATCH === "1" || desktopProfilePresent() || desktopRuntimeLikelyPresent());
|
||||||
|
let standardSource;
|
||||||
|
let standardKind;
|
||||||
|
let standardDir;
|
||||||
|
let agentCordis;
|
||||||
|
if (usePatch && patchPath !== null) {
|
||||||
|
standardSource = patchPath;
|
||||||
|
standardKind = "desktop-patch";
|
||||||
|
standardDir = null;
|
||||||
|
agentCordis = pluginsBodyToAgentCordis(extractPluginsBodyFromStandardPatch(readFileSync(patchPath, "utf8")));
|
||||||
|
} else {
|
||||||
|
const dir = resolveStandardPresetDir();
|
||||||
|
if (dir === null) {
|
||||||
|
throw new Error("cannot find host standard preset (Desktop standard.patch.yml or @deepseek-ai/dsh-agent-presets)");
|
||||||
|
}
|
||||||
|
standardSource = dir;
|
||||||
|
standardKind = "directory";
|
||||||
|
standardDir = dir;
|
||||||
|
agentCordis = readFileSync(join(dir, "agent.cordis.yml"), "utf8");
|
||||||
|
}
|
||||||
|
agentCordis = replaceTopLevelEntry(agentCordis, "persona", personaEntryFromMarkdown(readFileSync(personaPath, "utf8")));
|
||||||
agentCordis = ensureNetxopsToolsRow(agentCordis);
|
agentCordis = ensureNetxopsToolsRow(agentCordis);
|
||||||
|
agentCordis = alignWorkflowEngineForHost(agentCordis, bases);
|
||||||
if (!agentCordis.endsWith(`
|
if (!agentCordis.endsWith(`
|
||||||
`))
|
`))
|
||||||
agentCordis += `
|
agentCordis += `
|
||||||
`;
|
`;
|
||||||
const pluginsBody = agentCordis.replace(/\r\n/g, `
|
const missing = unresolvableCompositionPackages(agentCordis, bases).filter((name) => !name.startsWith("dsh-netxops"));
|
||||||
`).split(`
|
const desktopAsar = desktopRuntimeLikelyPresent();
|
||||||
`).map((line) => line.length === 0 ? "" : ` ${line}`).join(`
|
const blocking = missing.filter((name) => !(desktopAsar && name.startsWith("@deepseek-ai/")));
|
||||||
`);
|
if (blocking.length > 0) {
|
||||||
|
throw new Error(`composed Netx Ops preset names packages the host cannot resolve: ${blocking.join(", ")} ` + `(standard-source: ${standardSource})`);
|
||||||
|
}
|
||||||
|
const pluginsBody = agentCordisToPluginsBody(agentCordis);
|
||||||
const declarativePatch = `# Declarative Netx Ops preset for Desktop 0.2 (@deepseek-ai/dsh-agent-preset).
|
const declarativePatch = `# Declarative Netx Ops preset for Desktop 0.2 (@deepseek-ai/dsh-agent-preset).
|
||||||
# Generated from host \`standard\` + presets/netxops overlays — do not hand-edit.
|
# Generated from host \`standard\` + presets/netxops overlays — do not hand-edit.
|
||||||
# Regenerate: bun run scripts/gen-preset-patch.mjs
|
# Regenerate: bun run scripts/gen-preset-patch.mjs
|
||||||
|
|
@ -183,7 +408,7 @@ function composeNetxopsPresetArtifacts() {
|
||||||
plugins:
|
plugins:
|
||||||
${pluginsBody}
|
${pluginsBody}
|
||||||
`;
|
`;
|
||||||
return { standardDir, ...meta, agentCordis, declarativePatch };
|
return { standardSource, standardKind, standardDir, ...meta, agentCordis, declarativePatch };
|
||||||
}
|
}
|
||||||
function installDirectoryPreset(artifacts, logger) {
|
function installDirectoryPreset(artifacts, logger) {
|
||||||
const overlayRoot = join(packageRoot(), "presets", NETXOPS_PRESET_ID);
|
const overlayRoot = join(packageRoot(), "presets", NETXOPS_PRESET_ID);
|
||||||
|
|
@ -192,7 +417,10 @@ function installDirectoryPreset(artifacts, logger) {
|
||||||
mkdirSync(destParent, { recursive: true });
|
mkdirSync(destParent, { recursive: true });
|
||||||
if (existsSync(dest))
|
if (existsSync(dest))
|
||||||
rmSync(dest, { recursive: true, force: true });
|
rmSync(dest, { recursive: true, force: true });
|
||||||
cpSync(artifacts.standardDir, dest, { recursive: true });
|
mkdirSync(dest, { recursive: true });
|
||||||
|
if (artifacts.standardDir !== null && existsSync(artifacts.standardDir)) {
|
||||||
|
cpSync(artifacts.standardDir, dest, { recursive: true });
|
||||||
|
}
|
||||||
writeFileSync(join(dest, "preset.yml"), readFileSync(join(overlayRoot, "preset.yml"), "utf8"));
|
writeFileSync(join(dest, "preset.yml"), readFileSync(join(overlayRoot, "preset.yml"), "utf8"));
|
||||||
writeFileSync(join(dest, "PERSONA.md"), readFileSync(join(overlayRoot, "PERSONA.md"), "utf8"));
|
writeFileSync(join(dest, "PERSONA.md"), readFileSync(join(overlayRoot, "PERSONA.md"), "utf8"));
|
||||||
writeFileSync(join(dest, "agent.cordis.yml"), artifacts.agentCordis);
|
writeFileSync(join(dest, "agent.cordis.yml"), artifacts.agentCordis);
|
||||||
|
|
@ -200,13 +428,13 @@ function installDirectoryPreset(artifacts, logger) {
|
||||||
if (existsSync(skillsSrc))
|
if (existsSync(skillsSrc))
|
||||||
cpSync(skillsSrc, join(dest, "skills"), { recursive: true });
|
cpSync(skillsSrc, join(dest, "skills"), { recursive: true });
|
||||||
writeFileSync(join(dest, ".dsh-netxops-managed"), [
|
writeFileSync(join(dest, ".dsh-netxops-managed"), [
|
||||||
"composed-from: standard",
|
`composed-from: ${artifacts.standardKind}`,
|
||||||
`standard-source: ${artifacts.standardDir}`,
|
`standard-source: ${artifacts.standardSource}`,
|
||||||
`at: ${new Date().toISOString()}`,
|
`at: ${new Date().toISOString()}`,
|
||||||
""
|
""
|
||||||
].join(`
|
].join(`
|
||||||
`), "utf8");
|
`), "utf8");
|
||||||
logger.info("netxops: directory preset composed from standard → %s", dest);
|
logger.info("netxops: directory preset composed from %s → %s", artifacts.standardSource, dest);
|
||||||
}
|
}
|
||||||
function syncDeclarativeIntoProfilePatch(profileName, declarativePatch, logger) {
|
function syncDeclarativeIntoProfilePatch(profileName, declarativePatch, logger) {
|
||||||
if (profileName !== "desktop")
|
if (profileName !== "desktop")
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "dsh-netxops",
|
"name": "dsh-netxops",
|
||||||
"version": "0.1.48",
|
"version": "0.1.49",
|
||||||
"description": "DeepSeek Harness Netx Ops: ops/topology + IM delivery + session export + knowledge-base MANIFEST/_skills + localSkills",
|
"description": "DeepSeek Harness Netx Ops: ops/topology + IM delivery + session export + knowledge-base MANIFEST/_skills + localSkills",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
|
||||||
|
|
@ -314,8 +314,8 @@
|
||||||
backgroundMode: one-shot
|
backgroundMode: one-shot
|
||||||
maxDepth: provider-managed
|
maxDepth: provider-managed
|
||||||
|
|
||||||
- id: workflow-worker-thread
|
- id: workflow-ptc
|
||||||
name: '@deepseek-ai/dsh-workflow-worker-thread'
|
name: '@deepseek-ai/dsh-workflow-ptc'
|
||||||
config:
|
config:
|
||||||
provider: spawn
|
provider: spawn
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
# Netx Ops delta only — NOT a full agent composition.
|
# Netx Ops delta only — NOT a full agent composition.
|
||||||
#
|
#
|
||||||
# At Host apply, `ensureAgentPresetInstalled` copies the Harness shipped
|
# At Host apply, `ensureAgentPresetInstalled` composes the host's shipped
|
||||||
# `standard` preset into `$DSH_HOME/.agent-presets/netxops`, then:
|
# `standard` (Desktop 0.2: dsh-web-app `standard.patch.yml`; web/CLI:
|
||||||
|
# dsh-agent-presets directory) into `$DSH_HOME/.agent-presets/netxops`, then:
|
||||||
# 1. replaces `persona` with presets/netxops/PERSONA.md
|
# 1. replaces `persona` with presets/netxops/PERSONA.md
|
||||||
# 2. ensures the `netxops-tools` row below is present
|
# 2. ensures the `netxops-tools` row below is present
|
||||||
# 3. copies presets/netxops/skills/
|
# 3. aligns workflow engine package to what the host can resolve
|
||||||
#
|
# 4. copies presets/netxops/skills/
|
||||||
# DSH has no preset inheritance; composing from `standard` at install time is
|
# Desktop also syncs a declarative `@deepseek-ai/dsh-agent-preset` row into
|
||||||
# how Netx Ops stays aligned with the running Harness without embedding (or
|
# the profile patch. DSH has no preset inheritance.
|
||||||
# declaring via `@deepseek-ai/dsh-agent-preset`) a full standard tree.
|
|
||||||
|
|
||||||
- id: netxops-tools
|
- id: netxops-tools
|
||||||
name: dsh-netxops/tools
|
name: dsh-netxops/tools
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,10 @@ const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
||||||
const artifacts = composeNetxopsPresetArtifacts()
|
const artifacts = composeNetxopsPresetArtifacts()
|
||||||
const declarativePath = join(root, 'presets', 'netxops.preset.patch.yml')
|
const declarativePath = join(root, 'presets', 'netxops.preset.patch.yml')
|
||||||
writeFileSync(declarativePath, artifacts.declarativePatch)
|
writeFileSync(declarativePath, artifacts.declarativePatch)
|
||||||
console.log(`wrote ${declarativePath} (${artifacts.declarativePatch.length} bytes) from ${artifacts.standardDir}`)
|
console.log(
|
||||||
|
`wrote ${declarativePath} (${artifacts.declarativePatch.length} bytes) `
|
||||||
|
+ `from ${artifacts.standardKind}: ${artifacts.standardSource}`,
|
||||||
|
)
|
||||||
|
|
||||||
const host = readFileSync(join(root, 'cordis.patch.yml'), 'utf8')
|
const host = readFileSync(join(root, 'cordis.patch.yml'), 'utf8')
|
||||||
const bundle = `# Host-only bundle patch (web-safe).
|
const bundle = `# Host-only bundle patch (web-safe).
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,15 @@
|
||||||
* - Web / source CLI (`@deepseek-ai/dsh-agent-presets`): directory under
|
* - Web / source CLI (`@deepseek-ai/dsh-agent-presets`): directory under
|
||||||
* `$DSH_HOME/.agent-presets/netxops`.
|
* `$DSH_HOME/.agent-presets/netxops`.
|
||||||
* - Desktop 0.2 (`@deepseek-ai/dsh-agent-preset` + registry): declarative
|
* - Desktop 0.2 (`@deepseek-ai/dsh-agent-preset` + registry): declarative
|
||||||
* insert row in `presets/netxops.preset.patch.yml`, synced into the desktop
|
* insert row synced into the desktop profile `cordis.patch.yml` on apply.
|
||||||
* profile `cordis.patch.yml` on apply (not the package bundle — keeps web safe).
|
|
||||||
*
|
*
|
||||||
* Both are composed from the host shipped `standard` + Netx Ops overlays
|
* Both are composed from the **host's** shipped `standard` + Netx Ops overlays
|
||||||
* (DSH has no preset `extends`).
|
* (DSH has no preset `extends`). Desktop 0.2 ships standard as
|
||||||
|
* `@deepseek-ai/dsh-web-app/presets/standard.patch.yml` (`workflow-ptc`); the
|
||||||
|
* CLI heal tree under `$DSH_HOME/profiles/node_modules` may still carry an
|
||||||
|
* older `dsh-agent-presets` standard (`workflow-worker-thread`). Prefer the
|
||||||
|
* Desktop patch when present, and refuse to write a composition whose package
|
||||||
|
* names cannot resolve from the host anchors.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
@ -30,6 +34,9 @@ export const NETXOPS_PRESET_ID = 'netxops'
|
||||||
const DESKTOP_PATCH_BEGIN = '# BEGIN dsh-netxops-preset (managed)'
|
const DESKTOP_PATCH_BEGIN = '# BEGIN dsh-netxops-preset (managed)'
|
||||||
const DESKTOP_PATCH_END = '# END dsh-netxops-preset (managed)'
|
const DESKTOP_PATCH_END = '# END dsh-netxops-preset (managed)'
|
||||||
|
|
||||||
|
/** Indent of plugin rows inside `dsh-web-app/presets/*.patch.yml`. */
|
||||||
|
const PATCH_PLUGIN_INDENT = ' '
|
||||||
|
|
||||||
function packageRoot(): string {
|
function packageRoot(): string {
|
||||||
return join(dirname(fileURLToPath(import.meta.url)), '..')
|
return join(dirname(fileURLToPath(import.meta.url)), '..')
|
||||||
}
|
}
|
||||||
|
|
@ -40,23 +47,93 @@ function resolveDshHome(): string {
|
||||||
return join(homedir(), '.dsh')
|
return join(homedir(), '.dsh')
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Locate the shipped `standard` preset directory on this machine. */
|
function electronResourcesRoots(): string[] {
|
||||||
|
const roots: string[] = []
|
||||||
|
const rp = (process as NodeJS.Process & { resourcesPath?: string }).resourcesPath
|
||||||
|
if (typeof rp === 'string' && rp.length > 0) roots.push(rp)
|
||||||
|
// Packaged Desktop: `<install>/DeepSeek Harness.exe` beside `resources/`.
|
||||||
|
roots.push(join(dirname(process.execPath), 'resources'))
|
||||||
|
return roots
|
||||||
|
}
|
||||||
|
|
||||||
|
function readableFile(path: string): boolean {
|
||||||
|
try {
|
||||||
|
readFileSync(path, { encoding: 'utf8', flag: 'r' })
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Locate Desktop 0.2's declarative `standard` patch (`workflow-ptc`).
|
||||||
|
* Prefer Electron `app.asar` (what the running Desktop actually loads) over
|
||||||
|
* `$DSH_HOME/profiles/node_modules` (often the older CLI heal tree).
|
||||||
|
*/
|
||||||
|
export function resolveDesktopStandardPatchPath(): string | null {
|
||||||
|
const dshHome = resolveDshHome()
|
||||||
|
const envOverride = process.env.NETXOPS_STANDARD_PATCH?.trim()
|
||||||
|
const candidates: string[] = []
|
||||||
|
if (envOverride) candidates.push(envOverride)
|
||||||
|
|
||||||
|
for (const resources of electronResourcesRoots()) {
|
||||||
|
candidates.push(
|
||||||
|
join(resources, 'app.asar', 'dsh', 'node_modules', '@deepseek-ai', 'dsh-web-app', 'presets', 'standard.patch.yml'),
|
||||||
|
join(resources, 'app.asar.unpacked', 'dsh', 'node_modules', '@deepseek-ai', 'dsh-web-app', 'presets', 'standard.patch.yml'),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const requireAnchors = [
|
||||||
|
join(dshHome, 'profiles', 'desktop', 'package.json'),
|
||||||
|
join(dshHome, 'profiles', 'desktop', 'node_modules', 'dsh-netxops', 'package.json'),
|
||||||
|
join(packageRoot(), 'package.json'),
|
||||||
|
join(dshHome, 'profiles', 'node_modules', '@deepseek-ai', 'dsh-web-app', 'package.json'),
|
||||||
|
join(dshHome, 'profiles', 'web', 'package.json'),
|
||||||
|
]
|
||||||
|
for (const anchor of requireAnchors) {
|
||||||
|
if (!existsSync(anchor)) continue
|
||||||
|
try {
|
||||||
|
const req = createRequire(anchor)
|
||||||
|
try {
|
||||||
|
candidates.push(req.resolve('@deepseek-ai/dsh-web-app/presets/standard.patch.yml'))
|
||||||
|
} catch {
|
||||||
|
// older web-app builds do not export the subpath
|
||||||
|
}
|
||||||
|
const pkg = dirname(req.resolve('@deepseek-ai/dsh-web-app/package.json'))
|
||||||
|
candidates.push(join(pkg, 'presets', 'standard.patch.yml'))
|
||||||
|
} catch {
|
||||||
|
// try next anchor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const path of candidates) {
|
||||||
|
if (readableFile(path)) return path
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Locate the shipped directory `standard` (`agent.cordis.yml`).
|
||||||
|
* Profile-local copies win over the shared `$DSH_HOME/profiles/node_modules`
|
||||||
|
* heal tree, which is frequently an older CLI generation than Desktop.
|
||||||
|
*/
|
||||||
export function resolveStandardPresetDir(): string | null {
|
export function resolveStandardPresetDir(): string | null {
|
||||||
const dshHome = resolveDshHome()
|
const dshHome = resolveDshHome()
|
||||||
const candidates = [
|
const candidates = [
|
||||||
join(dshHome, 'profiles', 'node_modules', '@deepseek-ai', 'dsh-agent-presets', 'presets', 'standard'),
|
|
||||||
join(dshHome, 'profiles', 'web', 'node_modules', '@deepseek-ai', 'dsh-agent-presets', 'presets', 'standard'),
|
|
||||||
join(dshHome, 'profiles', 'desktop', 'node_modules', '@deepseek-ai', 'dsh-agent-presets', 'presets', 'standard'),
|
join(dshHome, 'profiles', 'desktop', 'node_modules', '@deepseek-ai', 'dsh-agent-presets', 'presets', 'standard'),
|
||||||
|
join(dshHome, 'profiles', 'web', 'node_modules', '@deepseek-ai', 'dsh-agent-presets', 'presets', 'standard'),
|
||||||
// Source checkout used by `dsh` CLI / local Harness.
|
// Source checkout used by `dsh` CLI / local Harness.
|
||||||
join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'DeepSeekHarness', 'packages', 'preset', 'agent-presets', 'presets', 'standard'),
|
join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'DeepSeekHarness', 'packages', 'preset', 'agent-presets', 'presets', 'standard'),
|
||||||
|
// Shared heal tree last — often lags Desktop 0.2.
|
||||||
|
join(dshHome, 'profiles', 'node_modules', '@deepseek-ai', 'dsh-agent-presets', 'presets', 'standard'),
|
||||||
]
|
]
|
||||||
for (const dir of candidates) {
|
for (const dir of candidates) {
|
||||||
if (existsSync(join(dir, 'agent.cordis.yml'))) return dir
|
if (existsSync(join(dir, 'agent.cordis.yml'))) return dir
|
||||||
}
|
}
|
||||||
for (const nm of [
|
for (const nm of [
|
||||||
join(dshHome, 'profiles', 'node_modules'),
|
|
||||||
join(dshHome, 'profiles', 'web', 'node_modules'),
|
|
||||||
join(dshHome, 'profiles', 'desktop', 'node_modules'),
|
join(dshHome, 'profiles', 'desktop', 'node_modules'),
|
||||||
|
join(dshHome, 'profiles', 'web', 'node_modules'),
|
||||||
|
join(dshHome, 'profiles', 'node_modules'),
|
||||||
]) {
|
]) {
|
||||||
const pkgJson = join(nm, '@deepseek-ai', 'dsh-agent-presets', 'package.json')
|
const pkgJson = join(nm, '@deepseek-ai', 'dsh-agent-presets', 'package.json')
|
||||||
if (!existsSync(pkgJson)) continue
|
if (!existsSync(pkgJson)) continue
|
||||||
|
|
@ -80,6 +157,193 @@ export function resolveStandardPresetDir(): string | null {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** True when a packaged Desktop install is on disk (asar may be opaque to plain Node). */
|
||||||
|
export function desktopRuntimeLikelyPresent(): boolean {
|
||||||
|
if (typeof process.versions.electron === 'string') return true
|
||||||
|
for (const resources of electronResourcesRoots()) {
|
||||||
|
if (existsSync(join(resources, 'app.asar'))) return true
|
||||||
|
}
|
||||||
|
const localAppData = process.env.LOCALAPPDATA?.trim()
|
||||||
|
if (localAppData) {
|
||||||
|
if (existsSync(join(localAppData, 'Programs', 'DeepSeek Harness', 'resources', 'app.asar'))) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Host roots used for bare package health checks (same walk shape as DSH discovery). */
|
||||||
|
export function hostResolveBases(): string[] {
|
||||||
|
const dshHome = resolveDshHome()
|
||||||
|
const bases: string[] = []
|
||||||
|
for (const resources of electronResourcesRoots()) {
|
||||||
|
bases.push(join(resources, 'app.asar', 'dsh', 'package.json'))
|
||||||
|
bases.push(join(resources, 'app.asar', 'dsh', 'node_modules'))
|
||||||
|
}
|
||||||
|
const localAppData = process.env.LOCALAPPDATA?.trim()
|
||||||
|
if (localAppData) {
|
||||||
|
const asarDsh = join(localAppData, 'Programs', 'DeepSeek Harness', 'resources', 'app.asar', 'dsh')
|
||||||
|
bases.push(join(asarDsh, 'package.json'), join(asarDsh, 'node_modules'))
|
||||||
|
}
|
||||||
|
bases.push(
|
||||||
|
join(dshHome, 'profiles', 'desktop', 'package.json'),
|
||||||
|
join(dshHome, 'profiles', 'desktop', 'node_modules'),
|
||||||
|
join(dshHome, 'profiles', 'web', 'package.json'),
|
||||||
|
join(dshHome, 'profiles', 'web', 'node_modules'),
|
||||||
|
join(dshHome, 'profiles', 'node_modules'),
|
||||||
|
join(packageRoot(), 'package.json'),
|
||||||
|
)
|
||||||
|
// Keep asar paths even when plain Node cannot stat them — Electron can.
|
||||||
|
return [...new Set(bases.filter((path) => path.includes('app.asar') || existsSync(path)))]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a package (optionally with subpath) is installed above any base.
|
||||||
|
* Mirrors `@deepseek-ai/dsh-agent-presets` discovery `packageInstalled`.
|
||||||
|
*/
|
||||||
|
export function packageInstalledAbove(name: string, bases: readonly string[]): boolean {
|
||||||
|
if (name.startsWith('cordis:') || name.startsWith('node:')) return true
|
||||||
|
const pkg = name.split('/').slice(0, name.startsWith('@') ? 2 : 1).join('/')
|
||||||
|
for (const base of bases) {
|
||||||
|
let dir = base.endsWith('package.json') ? dirname(base) : base
|
||||||
|
for (;;) {
|
||||||
|
if (readableFile(join(dir, 'node_modules', pkg, 'package.json'))) return true
|
||||||
|
// Electron asar: packages live under `…/app.asar/dsh/node_modules`.
|
||||||
|
if (readableFile(join(dir, pkg, 'package.json'))) return true
|
||||||
|
const parent = dirname(dir)
|
||||||
|
if (parent === dir) break
|
||||||
|
dir = parent
|
||||||
|
}
|
||||||
|
const requireAnchor = base.endsWith('package.json')
|
||||||
|
? base
|
||||||
|
: existsSync(join(base, 'package.json'))
|
||||||
|
? join(base, 'package.json')
|
||||||
|
: null
|
||||||
|
if (requireAnchor !== null) {
|
||||||
|
try {
|
||||||
|
createRequire(requireAnchor).resolve(`${pkg}/package.json`)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
// try next base
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Package names referenced by top-level / nested `name:` rows in a composition. */
|
||||||
|
export function collectCompositionPackageNames(yaml: string): string[] {
|
||||||
|
const names: string[] = []
|
||||||
|
for (const match of yaml.matchAll(/^\s*name:\s*(.+)$/gm)) {
|
||||||
|
let raw = match[1]!.trim()
|
||||||
|
if ((raw.startsWith("'") && raw.endsWith("'")) || (raw.startsWith('"') && raw.endsWith('"'))) {
|
||||||
|
raw = raw.slice(1, -1)
|
||||||
|
}
|
||||||
|
if (raw.length === 0) continue
|
||||||
|
if (raw.startsWith('./') || raw.startsWith('../') || raw.startsWith('file:')) continue
|
||||||
|
names.push(raw)
|
||||||
|
}
|
||||||
|
return [...new Set(names)]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unresolvableCompositionPackages(
|
||||||
|
yaml: string,
|
||||||
|
bases: readonly string[],
|
||||||
|
): string[] {
|
||||||
|
return collectCompositionPackageNames(yaml).filter((name) => !packageInstalledAbove(name, bases))
|
||||||
|
}
|
||||||
|
|
||||||
|
function remapWorkflowToPtc(cordis: string): string {
|
||||||
|
return cordis
|
||||||
|
.replaceAll('- id: workflow-worker-thread', '- id: workflow-ptc')
|
||||||
|
.replaceAll('@deepseek-ai/dsh-workflow-worker-thread', '@deepseek-ai/dsh-workflow-ptc')
|
||||||
|
}
|
||||||
|
|
||||||
|
function remapWorkflowToWorkerThread(cordis: string): string {
|
||||||
|
return cordis
|
||||||
|
.replaceAll('- id: workflow-ptc', '- id: workflow-worker-thread')
|
||||||
|
.replaceAll('@deepseek-ai/dsh-workflow-ptc', '@deepseek-ai/dsh-workflow-worker-thread')
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WorkflowHostKind = 'desktop' | 'web' | 'auto'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Align workflow engine id/name with what the host can resolve.
|
||||||
|
* Desktop 0.2 ships `dsh-workflow-ptc`; older CLI standards name
|
||||||
|
* `dsh-workflow-worker-thread`. Pass `host: 'desktop' | 'web'` when composing
|
||||||
|
* for one surface so a dual-install machine does not poison the other.
|
||||||
|
*/
|
||||||
|
export function alignWorkflowEngineForHost(
|
||||||
|
cordis: string,
|
||||||
|
bases: readonly string[],
|
||||||
|
host: WorkflowHostKind = 'auto',
|
||||||
|
): string {
|
||||||
|
const hasPtc = packageInstalledAbove('@deepseek-ai/dsh-workflow-ptc', bases)
|
||||||
|
const hasWorker = packageInstalledAbove('@deepseek-ai/dsh-workflow-worker-thread', bases)
|
||||||
|
const desktop = host === 'desktop' || (host === 'auto' && desktopRuntimeLikelyPresent())
|
||||||
|
if (hasPtc && !hasWorker) return remapWorkflowToPtc(cordis)
|
||||||
|
if (hasPtc && hasWorker && desktop) return remapWorkflowToPtc(cordis)
|
||||||
|
if (!hasPtc && hasWorker && desktop) {
|
||||||
|
// Plain Node cannot see into app.asar; Desktop 0.2 still only has ptc there.
|
||||||
|
return remapWorkflowToPtc(cordis)
|
||||||
|
}
|
||||||
|
if (!hasPtc && !hasWorker && desktop && cordis.includes('workflow-worker-thread')) {
|
||||||
|
return remapWorkflowToPtc(cordis)
|
||||||
|
}
|
||||||
|
if (hasWorker && !hasPtc && !desktop) return remapWorkflowToWorkerThread(cordis)
|
||||||
|
if (hasWorker && hasPtc && host === 'web') return remapWorkflowToWorkerThread(cordis)
|
||||||
|
return cordis
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pull the `plugins:` list body (already indented) out of a standard.patch.yml. */
|
||||||
|
export function extractPluginsBodyFromStandardPatch(patchYaml: string): string {
|
||||||
|
const text = patchYaml.replace(/\r\n/g, '\n')
|
||||||
|
const marker = /^([ \t]*)plugins:\s*$/m
|
||||||
|
const match = marker.exec(text)
|
||||||
|
if (match === null) {
|
||||||
|
throw new Error('standard.patch.yml has no plugins: block')
|
||||||
|
}
|
||||||
|
const pluginsKeyIndent = match[1]!.length
|
||||||
|
const start = match.index + match[0].length
|
||||||
|
const rest = text.slice(start).replace(/^\n/, '')
|
||||||
|
const lines = rest.split('\n')
|
||||||
|
const body: string[] = []
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.trim() === '') {
|
||||||
|
body.push(line)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const indent = line.match(/^[ \t]*/)?.[0].length ?? 0
|
||||||
|
if (indent <= pluginsKeyIndent) break
|
||||||
|
body.push(line)
|
||||||
|
}
|
||||||
|
while (body.length > 0 && body[body.length - 1]!.trim() === '') body.pop()
|
||||||
|
return body.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convert patch-indented plugins body into a top-level `agent.cordis.yml` list. */
|
||||||
|
export function pluginsBodyToAgentCordis(pluginsBody: string): string {
|
||||||
|
const prefix = PATCH_PLUGIN_INDENT
|
||||||
|
return `${pluginsBody
|
||||||
|
.replace(/\r\n/g, '\n')
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => {
|
||||||
|
if (line.length === 0) return ''
|
||||||
|
if (line.startsWith(prefix)) return line.slice(prefix.length)
|
||||||
|
return line.trimStart()
|
||||||
|
})
|
||||||
|
.join('\n')
|
||||||
|
.trimEnd()}\n`
|
||||||
|
}
|
||||||
|
|
||||||
|
function agentCordisToPluginsBody(agentCordis: string): string {
|
||||||
|
return agentCordis
|
||||||
|
.replace(/\r\n/g, '\n')
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => (line.length === 0 ? '' : `${PATCH_PLUGIN_INDENT}${line}`))
|
||||||
|
.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
function replaceTopLevelEntry(cordis: string, id: string, replacement: string): string {
|
function replaceTopLevelEntry(cordis: string, id: string, replacement: string): string {
|
||||||
const lines = cordis.replace(/\r\n/g, '\n').split('\n')
|
const lines = cordis.replace(/\r\n/g, '\n').split('\n')
|
||||||
const start = lines.findIndex((line) => line === `- id: ${id}`)
|
const start = lines.findIndex((line) => line === `- id: ${id}`)
|
||||||
|
|
@ -122,7 +386,12 @@ function personaEntryFromMarkdown(md: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NetxopsPresetArtifacts {
|
export interface NetxopsPresetArtifacts {
|
||||||
standardDir: string
|
/** Human-readable source path (directory or patch file). */
|
||||||
|
standardSource: string
|
||||||
|
/** Kind of host standard used for composition. */
|
||||||
|
standardKind: 'desktop-patch' | 'directory'
|
||||||
|
/** Directory to seed `.agent-presets/netxops` from; null when patch-only. */
|
||||||
|
standardDir: string | null
|
||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
order: string
|
order: string
|
||||||
|
|
@ -141,6 +410,10 @@ function readPresetMeta(metaPath: string): { name: string; description: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function desktopProfilePresent(): boolean {
|
||||||
|
return existsSync(join(resolveDshHome(), 'profiles', 'desktop'))
|
||||||
|
}
|
||||||
|
|
||||||
/** Compose standard + Netx Ops overlays into directory + declarative artifacts. */
|
/** Compose standard + Netx Ops overlays into directory + declarative artifacts. */
|
||||||
export function composeNetxopsPresetArtifacts(): NetxopsPresetArtifacts {
|
export function composeNetxopsPresetArtifacts(): NetxopsPresetArtifacts {
|
||||||
const overlayRoot = join(packageRoot(), 'presets', NETXOPS_PRESET_ID)
|
const overlayRoot = join(packageRoot(), 'presets', NETXOPS_PRESET_ID)
|
||||||
|
|
@ -149,28 +422,64 @@ export function composeNetxopsPresetArtifacts(): NetxopsPresetArtifacts {
|
||||||
if (!existsSync(personaPath) || !existsSync(metaPath)) {
|
if (!existsSync(personaPath) || !existsSync(metaPath)) {
|
||||||
throw new Error(`bundled preset overlay missing under ${overlayRoot}`)
|
throw new Error(`bundled preset overlay missing under ${overlayRoot}`)
|
||||||
}
|
}
|
||||||
const standardDir = resolveStandardPresetDir()
|
|
||||||
if (standardDir === null) {
|
|
||||||
throw new Error('cannot find shipped standard preset (@deepseek-ai/dsh-agent-presets)')
|
|
||||||
}
|
|
||||||
const meta = readPresetMeta(metaPath)
|
const meta = readPresetMeta(metaPath)
|
||||||
let agentCordis = replaceTopLevelEntry(
|
const bases = hostResolveBases()
|
||||||
readFileSync(join(standardDir, 'agent.cordis.yml'), 'utf8'),
|
const patchPath = resolveDesktopStandardPatchPath()
|
||||||
|
const usePatch = patchPath !== null && (
|
||||||
|
process.env.NETXOPS_PREFER_DESKTOP_PATCH !== '0'
|
||||||
|
) && (
|
||||||
|
typeof process.versions.electron === 'string'
|
||||||
|
|| process.env.NETXOPS_PREFER_DESKTOP_PATCH === '1'
|
||||||
|
|| desktopProfilePresent()
|
||||||
|
|| desktopRuntimeLikelyPresent()
|
||||||
|
)
|
||||||
|
|
||||||
|
let standardSource: string
|
||||||
|
let standardKind: 'desktop-patch' | 'directory'
|
||||||
|
let standardDir: string | null
|
||||||
|
let agentCordis: string
|
||||||
|
|
||||||
|
if (usePatch && patchPath !== null) {
|
||||||
|
standardSource = patchPath
|
||||||
|
standardKind = 'desktop-patch'
|
||||||
|
standardDir = null
|
||||||
|
agentCordis = pluginsBodyToAgentCordis(extractPluginsBodyFromStandardPatch(readFileSync(patchPath, 'utf8')))
|
||||||
|
} else {
|
||||||
|
const dir = resolveStandardPresetDir()
|
||||||
|
if (dir === null) {
|
||||||
|
throw new Error(
|
||||||
|
'cannot find host standard preset (Desktop standard.patch.yml or @deepseek-ai/dsh-agent-presets)',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
standardSource = dir
|
||||||
|
standardKind = 'directory'
|
||||||
|
standardDir = dir
|
||||||
|
agentCordis = readFileSync(join(dir, 'agent.cordis.yml'), 'utf8')
|
||||||
|
}
|
||||||
|
|
||||||
|
agentCordis = replaceTopLevelEntry(
|
||||||
|
agentCordis,
|
||||||
'persona',
|
'persona',
|
||||||
personaEntryFromMarkdown(readFileSync(personaPath, 'utf8')),
|
personaEntryFromMarkdown(readFileSync(personaPath, 'utf8')),
|
||||||
)
|
)
|
||||||
agentCordis = ensureNetxopsToolsRow(agentCordis)
|
agentCordis = ensureNetxopsToolsRow(agentCordis)
|
||||||
|
agentCordis = alignWorkflowEngineForHost(agentCordis, bases)
|
||||||
if (!agentCordis.endsWith('\n')) agentCordis += '\n'
|
if (!agentCordis.endsWith('\n')) agentCordis += '\n'
|
||||||
|
|
||||||
// Match dsh-web-app/presets/*.patch.yml: plugins items are indented 10 spaces
|
const missing = unresolvableCompositionPackages(agentCordis, bases)
|
||||||
// under ` plugins:` (8). Same-level `- id:` makes YAML treat them as
|
.filter((name) => !name.startsWith('dsh-netxops'))
|
||||||
// siblings of `plugins` and the declaration fails to mount.
|
// Packaged Desktop keeps most `@deepseek-ai/*` only inside app.asar; plain
|
||||||
const pluginsBody = agentCordis
|
// Node cannot probe them, but the Host can. Do not block the write for those.
|
||||||
.replace(/\r\n/g, '\n')
|
const desktopAsar = desktopRuntimeLikelyPresent()
|
||||||
.split('\n')
|
const blocking = missing.filter((name) => !(desktopAsar && name.startsWith('@deepseek-ai/')))
|
||||||
.map((line) => (line.length === 0 ? '' : ` ${line}`))
|
if (blocking.length > 0) {
|
||||||
.join('\n')
|
throw new Error(
|
||||||
|
`composed Netx Ops preset names packages the host cannot resolve: ${blocking.join(', ')} `
|
||||||
|
+ `(standard-source: ${standardSource})`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginsBody = agentCordisToPluginsBody(agentCordis)
|
||||||
const declarativePatch = `# Declarative Netx Ops preset for Desktop 0.2 (@deepseek-ai/dsh-agent-preset).
|
const declarativePatch = `# Declarative Netx Ops preset for Desktop 0.2 (@deepseek-ai/dsh-agent-preset).
|
||||||
# Generated from host \`standard\` + presets/netxops overlays — do not hand-edit.
|
# Generated from host \`standard\` + presets/netxops overlays — do not hand-edit.
|
||||||
# Regenerate: bun run scripts/gen-preset-patch.mjs
|
# Regenerate: bun run scripts/gen-preset-patch.mjs
|
||||||
|
|
@ -186,7 +495,7 @@ export function composeNetxopsPresetArtifacts(): NetxopsPresetArtifacts {
|
||||||
plugins:
|
plugins:
|
||||||
${pluginsBody}
|
${pluginsBody}
|
||||||
`
|
`
|
||||||
return { standardDir, ...meta, agentCordis, declarativePatch }
|
return { standardSource, standardKind, standardDir, ...meta, agentCordis, declarativePatch }
|
||||||
}
|
}
|
||||||
|
|
||||||
function installDirectoryPreset(artifacts: NetxopsPresetArtifacts, logger: Context['logger']): void {
|
function installDirectoryPreset(artifacts: NetxopsPresetArtifacts, logger: Context['logger']): void {
|
||||||
|
|
@ -195,7 +504,10 @@ function installDirectoryPreset(artifacts: NetxopsPresetArtifacts, logger: Conte
|
||||||
const dest = join(destParent, NETXOPS_PRESET_ID)
|
const dest = join(destParent, NETXOPS_PRESET_ID)
|
||||||
mkdirSync(destParent, { recursive: true })
|
mkdirSync(destParent, { recursive: true })
|
||||||
if (existsSync(dest)) rmSync(dest, { recursive: true, force: true })
|
if (existsSync(dest)) rmSync(dest, { recursive: true, force: true })
|
||||||
cpSync(artifacts.standardDir, dest, { recursive: true })
|
mkdirSync(dest, { recursive: true })
|
||||||
|
if (artifacts.standardDir !== null && existsSync(artifacts.standardDir)) {
|
||||||
|
cpSync(artifacts.standardDir, dest, { recursive: true })
|
||||||
|
}
|
||||||
writeFileSync(join(dest, 'preset.yml'), readFileSync(join(overlayRoot, 'preset.yml'), 'utf8'))
|
writeFileSync(join(dest, 'preset.yml'), readFileSync(join(overlayRoot, 'preset.yml'), 'utf8'))
|
||||||
writeFileSync(join(dest, 'PERSONA.md'), readFileSync(join(overlayRoot, 'PERSONA.md'), 'utf8'))
|
writeFileSync(join(dest, 'PERSONA.md'), readFileSync(join(overlayRoot, 'PERSONA.md'), 'utf8'))
|
||||||
writeFileSync(join(dest, 'agent.cordis.yml'), artifacts.agentCordis)
|
writeFileSync(join(dest, 'agent.cordis.yml'), artifacts.agentCordis)
|
||||||
|
|
@ -204,14 +516,18 @@ function installDirectoryPreset(artifacts: NetxopsPresetArtifacts, logger: Conte
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
join(dest, '.dsh-netxops-managed'),
|
join(dest, '.dsh-netxops-managed'),
|
||||||
[
|
[
|
||||||
'composed-from: standard',
|
`composed-from: ${artifacts.standardKind}`,
|
||||||
`standard-source: ${artifacts.standardDir}`,
|
`standard-source: ${artifacts.standardSource}`,
|
||||||
`at: ${new Date().toISOString()}`,
|
`at: ${new Date().toISOString()}`,
|
||||||
'',
|
'',
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
'utf8',
|
'utf8',
|
||||||
)
|
)
|
||||||
logger.info('netxops: directory preset composed from standard → %s', dest)
|
logger.info(
|
||||||
|
'netxops: directory preset composed from %s → %s',
|
||||||
|
artifacts.standardSource,
|
||||||
|
dest,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
81
test/agent-preset-install.test.mjs
Normal file
81
test/agent-preset-install.test.mjs
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
|
||||||
|
import { tmpdir } from 'node:os'
|
||||||
|
import { join } from 'node:path'
|
||||||
|
import { describe, it } from 'node:test'
|
||||||
|
import {
|
||||||
|
alignWorkflowEngineForHost,
|
||||||
|
collectCompositionPackageNames,
|
||||||
|
extractPluginsBodyFromStandardPatch,
|
||||||
|
packageInstalledAbove,
|
||||||
|
pluginsBodyToAgentCordis,
|
||||||
|
} from '../src/agent-preset-install.ts'
|
||||||
|
|
||||||
|
const SAMPLE_PATCH = `# comment
|
||||||
|
- insert:
|
||||||
|
- id: preset-standard
|
||||||
|
name: '@deepseek-ai/dsh-agent-preset'
|
||||||
|
config:
|
||||||
|
id: standard
|
||||||
|
order: 1
|
||||||
|
plugins:
|
||||||
|
- id: persona
|
||||||
|
name: '@deepseek-ai/dsh-persona'
|
||||||
|
config:
|
||||||
|
prefix: hello
|
||||||
|
- id: tool-skill
|
||||||
|
name: '@deepseek-ai/dsh-tool-skill'
|
||||||
|
- id: workflow-worker-thread
|
||||||
|
name: '@deepseek-ai/dsh-workflow-worker-thread'
|
||||||
|
`
|
||||||
|
|
||||||
|
function seedPackage(root, name) {
|
||||||
|
const dir = join(root, 'node_modules', ...name.split('/'))
|
||||||
|
mkdirSync(dir, { recursive: true })
|
||||||
|
writeFileSync(join(dir, 'package.json'), JSON.stringify({ name, version: '0.0.0' }))
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('extractPluginsBodyFromStandardPatch', () => {
|
||||||
|
it('keeps indented plugin rows and drops the insert wrapper', () => {
|
||||||
|
const body = extractPluginsBodyFromStandardPatch(SAMPLE_PATCH)
|
||||||
|
assert.match(body, /^ - id: persona$/m)
|
||||||
|
assert.match(body, /workflow-worker-thread/)
|
||||||
|
assert.doesNotMatch(body, /^- insert:/m)
|
||||||
|
assert.doesNotMatch(body, /preset-standard/)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('pluginsBodyToAgentCordis', () => {
|
||||||
|
it('strips the declarative plugin indent', () => {
|
||||||
|
const body = extractPluginsBodyFromStandardPatch(SAMPLE_PATCH)
|
||||||
|
const cordis = pluginsBodyToAgentCordis(body)
|
||||||
|
assert.match(cordis, /^- id: persona$/m)
|
||||||
|
assert.match(cordis, /^ name: '@deepseek-ai\/dsh-persona'$/m)
|
||||||
|
assert.doesNotMatch(cordis, /^ - id:/m)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('alignWorkflowEngineForHost', () => {
|
||||||
|
const worker = '- id: workflow-worker-thread\n name: \'@deepseek-ai/dsh-workflow-worker-thread\'\n'
|
||||||
|
|
||||||
|
it('rewrites worker-thread → ptc when only ptc is installed above bases', () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), 'netxops-preset-'))
|
||||||
|
seedPackage(root, '@deepseek-ai/dsh-workflow-ptc')
|
||||||
|
assert.equal(packageInstalledAbove('@deepseek-ai/dsh-workflow-ptc', [root]), true)
|
||||||
|
assert.equal(packageInstalledAbove('@deepseek-ai/dsh-workflow-worker-thread', [root]), false)
|
||||||
|
const out = alignWorkflowEngineForHost(worker, [root])
|
||||||
|
assert.match(out, /- id: workflow-ptc/)
|
||||||
|
assert.match(out, /dsh-workflow-ptc/)
|
||||||
|
assert.doesNotMatch(out, /workflow-worker-thread/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('collectCompositionPackageNames reads quoted names', () => {
|
||||||
|
const names = collectCompositionPackageNames(
|
||||||
|
'- id: a\n name: \'@deepseek-ai/dsh-workflow-ptc\'\n- id: b\n name: "@deepseek-ai/dsh-tool-workflow"\n',
|
||||||
|
)
|
||||||
|
assert.deepEqual(
|
||||||
|
names.sort(),
|
||||||
|
['@deepseek-ai/dsh-tool-workflow', '@deepseek-ai/dsh-workflow-ptc'].sort(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Add table
Add a link
Reference in a new issue