@xiaoso/dsh-tool-plus 0.1.9 → 0.1.10-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +12 -5
- package/README.md +11 -4
- package/THIRD_PARTY_NOTICES.md +36 -0
- package/lib/client.js +377 -906
- package/lib/index.mjs +885 -1087
- package/lib/{scrapers-mhvFrvjH.mjs → scrapers-DSArdJ6N.mjs} +1 -1
- package/lib/types/client/ToolPlusSection.d.ts +3 -3
- package/lib/types/client/forms.d.ts +6 -6
- package/lib/types/client/index.d.ts +29 -12
- package/lib/types/client/locales.d.ts +1 -1
- package/lib/types/client/preset-panel.d.ts +54 -42
- package/lib/types/config/settings.d.ts +202 -21
- package/lib/types/host/browser-probe-rpc.d.ts +2 -1
- package/lib/types/host/preset-host.d.ts +103 -0
- package/lib/types/host/preset-rpc.d.ts +37 -72
- package/lib/types/presets/compare.d.ts +14 -37
- package/lib/types/presets/conflicts.d.ts +59 -0
- package/lib/types/presets/delta.d.ts +23 -18
- package/lib/types/presets/plan.d.ts +55 -0
- package/lib/types/presets/rows.d.ts +84 -0
- package/lib/types/presets/status.d.ts +56 -47
- package/lib/types/tools/bash/background.d.ts +20 -5
- package/lib/types/tools/bash/streaming-output.d.ts +4 -2
- package/lib/types/tools/shared/browser-rpc-channel.d.ts +84 -42
- package/lib/types/web/client/labels.d.ts +1 -1
- package/lib/types/web/client/row-utils.d.ts +26 -1
- package/lib/{utils-eiUz9n8I.mjs → utils-DA8R1_Q8.mjs} +1 -1
- package/package.json +166 -151
- package/presets/tool-plus-ptc.patch.yml +164 -0
- package/presets/tool-plus-standard.patch.yml +158 -0
- package/lib/types/client/BashPlusCard.d.ts +0 -53
- package/lib/types/presets/install.d.ts +0 -44
- package/lib/types/presets/paths.d.ts +0 -41
- package/lib/types/presets/rewrite.d.ts +0 -86
- package/presets/install-presets.mjs +0 -143
- package/presets/package.json +0 -42
- package/presets/tool-plus-ptc/agent.cordis.yml +0 -284
- package/presets/tool-plus-ptc/preset.yml +0 -3
- package/presets/tool-plus-standard/agent.cordis.yml +0 -269
- package/presets/tool-plus-standard/preset.yml +0 -3
package/lib/index.mjs
CHANGED
|
@@ -3,24 +3,24 @@ __installBunShim();
|
|
|
3
3
|
import { n as fs_promises_shim_exports, t as exists } from "./fs-promises-shim-Bx0fpxuk.mjs";
|
|
4
4
|
import { i as setRuntimeLogger, n as systemChromiumCandidatesForTest, r as runtimeLogger } from "./launch-Uq7920ng.mjs";
|
|
5
5
|
import { $ as isEnoent, A as formatCount, C as warn, D as installBunShim, F as $flag, N as $env, O as formatAge, P as $envpos, Q as hasFsCode, U as getAutoQaDbPath, V as VERSION, X as getRemoteDir, Y as getPuppeteerDir, at as findOnPath, b as debug, c as combineSignals, d as $which, et as isEnotdir, it as untilAborted, k as formatBytes$1, l as exec, m as readImageMetadata, nt as peekFile, q as getInstallId, tt as isProbablyBinary, u as render, x as error } from "./src-DUUqyo9y.mjs";
|
|
6
|
-
import { C as truncateToWidth$1, S as replaceTabs, _ as formatCodeFrameLine, a as fetchBinary, b as LRUCache, c as getParallelExtractContent, d as convertFileWithMarkit, f as Settings, g as capParseErrors, h as OMP_DEFAULTS, i as convertWithMarkit, l as ensureTool, m as settings, o as extractWithParallel, p as isSettingsInitialized, s as findParallelApiKey, u as CONVERTIBLE_EXTENSIONS, v as formatParseErrors, x as padding, y as shortenPath } from "./utils-
|
|
6
|
+
import { C as truncateToWidth$1, S as replaceTabs, _ as formatCodeFrameLine, a as fetchBinary, b as LRUCache, c as getParallelExtractContent, d as convertFileWithMarkit, f as Settings, g as capParseErrors, h as OMP_DEFAULTS, i as convertWithMarkit, l as ensureTool, m as settings, o as extractWithParallel, p as isSettingsInitialized, s as findParallelApiKey, u as CONVERTIBLE_EXTENSIONS, v as formatParseErrors, x as padding, y as shortenPath } from "./utils-DA8R1_Q8.mjs";
|
|
7
7
|
import { t as sanitizeText } from "./sanitize-text-BCf5KmCf.mjs";
|
|
8
8
|
import { n as isRecord$5 } from "./type-guards-CttAANEx.mjs";
|
|
9
9
|
import { n as ToolError, r as throwIfAborted, t as ToolAbortError } from "./tool-errors-DoyxivAR.mjs";
|
|
10
10
|
import { a as openArchive, c as sniffArchiveFormat, d as writeArchive, i as listArchiveRoot, o as parseArchivePathCandidates, r as formatArchiveEntryLines, s as readArchiveEntries, t as archiveFormatFromPath } from "./zip-CNtIFfyV.mjs";
|
|
11
11
|
import { a as finalizeOutput, d as looksLikeHtml, n as MAX_OUTPUT_CHARS, t as MAX_BYTES, u as loadPage } from "./types-DT9vgZ3S.mjs";
|
|
12
12
|
import * as fs from "node:fs";
|
|
13
|
-
import { existsSync } from "node:fs";
|
|
13
|
+
import { copyFileSync, existsSync, readFileSync, readdirSync } from "node:fs";
|
|
14
14
|
import { TOOL_ABORTED, defineTool } from "@deepseek-ai/dsh-tools";
|
|
15
15
|
import { HarnessError } from "@deepseek-ai/dsh-llm";
|
|
16
16
|
import { DSH_ENV_PREFIX, parseExitStatus } from "@deepseek-ai/dsh-shell";
|
|
17
17
|
import * as natives from "@oh-my-pi/pi-natives";
|
|
18
18
|
import { EditSession, EditStore, FileType, GrepOutputMode, Shell, astEdit, astGrep, diffLines, editDescription, enclosingBlockBoundaries, glob, grep, hashlineFormatHeader, hashlineFormatNumberedLines, hashlineStripPrefixes, htmlToMarkdown, invalidateFsScanCache, listWorkspace, notebookToEditableText, structuredPatchHunks, summarizeCode } from "@oh-my-pi/pi-natives";
|
|
19
19
|
import * as path$1 from "node:path";
|
|
20
|
-
import path from "node:path";
|
|
20
|
+
import path, { join } from "node:path";
|
|
21
21
|
import { spawn } from "node:child_process";
|
|
22
22
|
import * as os from "node:os";
|
|
23
|
-
import { tmpdir } from "node:os";
|
|
23
|
+
import { homedir, tmpdir } from "node:os";
|
|
24
24
|
import * as url from "node:url";
|
|
25
25
|
import { fileURLToPath } from "node:url";
|
|
26
26
|
import { scheduler } from "node:timers/promises";
|
|
@@ -726,7 +726,8 @@ async function getOrCreateSnapshot(shell, env, timeoutMs = SNAPSHOT_TIMEOUT_MS)
|
|
|
726
726
|
env: spawnEnv,
|
|
727
727
|
timeout: timeoutMs,
|
|
728
728
|
stdio: "ignore",
|
|
729
|
-
killSignal: "SIGKILL"
|
|
729
|
+
killSignal: "SIGKILL",
|
|
730
|
+
windowsHide: true
|
|
730
731
|
});
|
|
731
732
|
await new Promise((resolve, reject) => {
|
|
732
733
|
child.on("close", (code) => {
|
|
@@ -1966,8 +1967,10 @@ var OutputSink = class extends OutputSink$1 {
|
|
|
1966
1967
|
};
|
|
1967
1968
|
/**
|
|
1968
1969
|
* Composition wrapper over the verbatim {@link TailBuffer}, adding the DSH
|
|
1969
|
-
* `reset()` increment
|
|
1970
|
-
*
|
|
1970
|
+
* `reset()` increment: the consuming-read cursor reset the bash background
|
|
1971
|
+
* producer used while `JobHooks.readOutput` existed. 0.1.7 moves the model's
|
|
1972
|
+
* cursor onto the job registry's output ring, so the increment is no longer
|
|
1973
|
+
* part of that path; the preview buffer still offers it.
|
|
1971
1974
|
*/
|
|
1972
1975
|
var TailBuffer = class {
|
|
1973
1976
|
#inner;
|
|
@@ -2651,25 +2654,25 @@ function sweepStaleSpillFiles(now = Date.now()) {
|
|
|
2651
2654
|
* Background-job adaptation for the `ctx.jobs` seam: one managed job runs its
|
|
2652
2655
|
* command on its OWN `:async:` Shell (never the session's persistent shell),
|
|
2653
2656
|
* so background work never blocks the foreground session. The registry owns
|
|
2654
|
-
* identity, lifecycle state, and completion notices
|
|
2655
|
-
* them); this module owns the execution resources
|
|
2657
|
+
* identity, lifecycle state, the output ring, and completion notices
|
|
2658
|
+
* (`dsh-tool-jobs` injects them); this module owns the execution resources,
|
|
2659
|
+
* streams the run into that ring, and appends the plugin's completion message
|
|
2660
|
+
* once the run ends.
|
|
2656
2661
|
* @module @xiaoso/dsh-tool-plus/background
|
|
2657
2662
|
*/
|
|
2658
2663
|
/**
|
|
2659
2664
|
* Start one managed background bash job. The producer owns a per-job
|
|
2660
|
-
* `AbortController` (cancellation kills only this job's shell)
|
|
2661
|
-
* preview tail
|
|
2665
|
+
* `AbortController` (cancellation kills only this job's shell) and a bounded
|
|
2666
|
+
* preview tail; the registry owns the model's consuming cursor over the ring
|
|
2667
|
+
* this job writes to.
|
|
2662
2668
|
* @param options - job identity and execution parameters.
|
|
2663
2669
|
* @returns the registry hooks plus the tool-result completion promise.
|
|
2664
2670
|
*/
|
|
2665
2671
|
function startBashJob(options) {
|
|
2666
|
-
const { sessionId, command, cwd, timeoutMs, env, config } = options;
|
|
2672
|
+
const { sessionId, command, cwd, timeoutMs, env, config, job } = options;
|
|
2667
2673
|
const shellKey = `${sessionId}:async:${randomBytes(4).toString("hex")}`;
|
|
2668
2674
|
const abortController = new AbortController();
|
|
2669
2675
|
const preview = new TailBuffer(config.outputMaxBytes);
|
|
2670
|
-
const delta = new TailBuffer(config.outputMaxBytes);
|
|
2671
|
-
let settled = false;
|
|
2672
|
-
let settledText = "";
|
|
2673
2676
|
const completion = (async () => {
|
|
2674
2677
|
const startedAt = performance.now();
|
|
2675
2678
|
const spillPath = allocateSpillFile();
|
|
@@ -2699,11 +2702,9 @@ function startBashJob(options) {
|
|
|
2699
2702
|
onMinimizedSave: (originalText) => saveOriginalText(originalText),
|
|
2700
2703
|
onChunk: (chunk) => {
|
|
2701
2704
|
preview.append(chunk);
|
|
2702
|
-
|
|
2705
|
+
job?.append(chunk);
|
|
2703
2706
|
}
|
|
2704
2707
|
});
|
|
2705
|
-
settled = true;
|
|
2706
|
-
settledText = preview.text();
|
|
2707
2708
|
const wallTimeMs = performance.now() - startedAt;
|
|
2708
2709
|
const aborted = result.cancelled && abortController.signal.aborted;
|
|
2709
2710
|
const timedOut = result.timedOut ?? (result.cancelled && !aborted);
|
|
@@ -2712,6 +2713,9 @@ function startBashJob(options) {
|
|
|
2712
2713
|
const seconds = Math.max(1, Math.round((timeoutMs ?? 0) / 1e3));
|
|
2713
2714
|
if (!text.includes("[Command timed out after")) text = `[Command timed out after ${seconds} seconds]\n\n${text}`;
|
|
2714
2715
|
}
|
|
2716
|
+
const previewText = preview.text();
|
|
2717
|
+
const message = options.formatCompletion?.(previewText, result.spillPath) ?? previewText;
|
|
2718
|
+
if (job !== void 0 && message !== previewText) job.append(message);
|
|
2715
2719
|
return {
|
|
2716
2720
|
kind: "foreground",
|
|
2717
2721
|
exitCode: result.exitCode ?? null,
|
|
@@ -2756,13 +2760,7 @@ function startBashJob(options) {
|
|
|
2756
2760
|
}, (error) => ({
|
|
2757
2761
|
status: "failed",
|
|
2758
2762
|
detail: error instanceof Error ? error.message : String(error)
|
|
2759
|
-
}))
|
|
2760
|
-
readOutput: () => {
|
|
2761
|
-
if (settled) return settledText;
|
|
2762
|
-
const text = delta.text();
|
|
2763
|
-
delta.reset();
|
|
2764
|
-
return text;
|
|
2765
|
-
}
|
|
2763
|
+
}))
|
|
2766
2764
|
},
|
|
2767
2765
|
completion
|
|
2768
2766
|
};
|
|
@@ -4887,8 +4885,24 @@ function toolPlusField(name) {
|
|
|
4887
4885
|
TOOL_PLUS_FIELDS.map((field) => field.name);
|
|
4888
4886
|
//#endregion
|
|
4889
4887
|
//#region src/config/settings.ts
|
|
4890
|
-
/**
|
|
4891
|
-
|
|
4888
|
+
/**
|
|
4889
|
+
* Protocol key `@deepseek-ai/cosmokit` stamps on a live config reference.
|
|
4890
|
+
* `Symbol.for` keeps the check valid across copies of the shared library — the
|
|
4891
|
+
* reason cosmokit's own `isVolatile` resolves the same key.
|
|
4892
|
+
*/
|
|
4893
|
+
const VOLATILE_WRITE = Symbol.for("cosmokit.volatile.write");
|
|
4894
|
+
/**
|
|
4895
|
+
* Whether a parsed value is one of the Loader's live config references.
|
|
4896
|
+
*
|
|
4897
|
+
* `@deepseek-ai/cordis` re-exports the `Volatile` TYPE only, and cosmokit —
|
|
4898
|
+
* which owns the runtime check — is not a dependency of this package, so the
|
|
4899
|
+
* protocol is read here instead of imported.
|
|
4900
|
+
* @param value - a parsed config value.
|
|
4901
|
+
* @returns whether the value is a live reference.
|
|
4902
|
+
*/
|
|
4903
|
+
function isLiveField(value) {
|
|
4904
|
+
return typeof value === "object" && value !== null && VOLATILE_WRITE in value;
|
|
4905
|
+
}
|
|
4892
4906
|
/** OMP-parity defaults for the completion-message truncation policy. */
|
|
4893
4907
|
const DEFAULT_OUTPUT_TRUNCATE = {
|
|
4894
4908
|
strategy: "bytes",
|
|
@@ -4923,73 +4937,81 @@ function fieldDefault(name, fallback) {
|
|
|
4923
4937
|
return value === void 0 ? fallback : value;
|
|
4924
4938
|
}
|
|
4925
4939
|
/**
|
|
4926
|
-
* Runtime configuration schema for the plugin
|
|
4927
|
-
*
|
|
4940
|
+
* Runtime configuration schema for the plugin, in the dsh 0.1.7 settings shape:
|
|
4941
|
+
* EVERY field is declared `.volatile()`, because a settings namespace is the
|
|
4942
|
+
* plugin's own entry and `SettingsForms.describe()` serves exactly the fields
|
|
4943
|
+
* whose schema declares them live. {@link resolveConfig} maps the flat surface
|
|
4944
|
+
* onto {@link RuntimeConfig}.
|
|
4945
|
+
*
|
|
4946
|
+
* The annotation is deliberately inferred rather than written as `z<Config>`:
|
|
4947
|
+
* the schema's parsed output is {@link LiveConfig} (one stable reference per
|
|
4948
|
+
* field), while {@link Config} stays the plain settings DOCUMENT the browser
|
|
4949
|
+
* edits and the specs pass in.
|
|
4928
4950
|
*/
|
|
4929
4951
|
const Config = z.object({
|
|
4930
|
-
enableRunInBackground: z.boolean().default(fieldDefault("enableRunInBackground", true)),
|
|
4931
|
-
autoBackgroundMs: z.number().default(fieldDefault("autoBackgroundMs", DEFAULT_AUTO_BACKGROUND_MS)),
|
|
4932
|
-
defaultTimeoutMs: z.number().default(fieldDefault("defaultTimeoutMs", DEFAULT_TIMEOUT_MS)),
|
|
4933
|
-
maxTimeoutMs: z.number().default(fieldDefault("maxTimeoutMs", DEFAULT_MAX_TIMEOUT_MS)),
|
|
4934
|
-
outputMaxBytes: z.number().default(fieldDefault("outputMaxBytes", 51200)),
|
|
4935
|
-
outputSinkTailBytes: z.number().default(fieldDefault("outputSinkTailBytes", 51200)),
|
|
4936
|
-
outputSinkHeadBytes: z.number().default(fieldDefault("outputSinkHeadBytes", 20480)),
|
|
4937
|
-
minimizerEnabled: z.boolean().default(fieldDefault("minimizerEnabled", true)),
|
|
4938
|
-
interceptorEnabled: z.boolean().default(fieldDefault("interceptorEnabled", true)),
|
|
4939
|
-
nonInteractiveEnv: z.boolean().default(fieldDefault("nonInteractiveEnv", true)),
|
|
4940
|
-
snapshotEnabled: z.boolean().default(fieldDefault("snapshotEnabled", true)),
|
|
4941
|
-
rmSafe: z.boolean().default(fieldDefault("rmSafe", true)),
|
|
4942
|
-
useShellCommandWrapper: z.boolean().default(fieldDefault("useShellCommandWrapper", false)),
|
|
4943
|
-
webCards: z.boolean().default(fieldDefault("webCards", true)),
|
|
4944
|
-
maxBackgroundJobs: z.number().default(fieldDefault("maxBackgroundJobs", 15)),
|
|
4945
|
-
outputTruncateStrategy: z.union(["bytes", "lines"]).default(fieldDefault("outputTruncateStrategy", "bytes")),
|
|
4946
|
-
outputTruncateTriggerBytes: z.number().default(fieldDefault("outputTruncateTriggerBytes", 10240)),
|
|
4947
|
-
outputTruncateTriggerLines: z.number().default(fieldDefault("outputTruncateTriggerLines", 100)),
|
|
4952
|
+
enableRunInBackground: z.boolean().default(fieldDefault("enableRunInBackground", true)).volatile(),
|
|
4953
|
+
autoBackgroundMs: z.number().default(fieldDefault("autoBackgroundMs", DEFAULT_AUTO_BACKGROUND_MS)).volatile(),
|
|
4954
|
+
defaultTimeoutMs: z.number().default(fieldDefault("defaultTimeoutMs", DEFAULT_TIMEOUT_MS)).volatile(),
|
|
4955
|
+
maxTimeoutMs: z.number().default(fieldDefault("maxTimeoutMs", DEFAULT_MAX_TIMEOUT_MS)).volatile(),
|
|
4956
|
+
outputMaxBytes: z.number().default(fieldDefault("outputMaxBytes", 51200)).volatile(),
|
|
4957
|
+
outputSinkTailBytes: z.number().default(fieldDefault("outputSinkTailBytes", 51200)).volatile(),
|
|
4958
|
+
outputSinkHeadBytes: z.number().default(fieldDefault("outputSinkHeadBytes", 20480)).volatile(),
|
|
4959
|
+
minimizerEnabled: z.boolean().default(fieldDefault("minimizerEnabled", true)).volatile(),
|
|
4960
|
+
interceptorEnabled: z.boolean().default(fieldDefault("interceptorEnabled", true)).volatile(),
|
|
4961
|
+
nonInteractiveEnv: z.boolean().default(fieldDefault("nonInteractiveEnv", true)).volatile(),
|
|
4962
|
+
snapshotEnabled: z.boolean().default(fieldDefault("snapshotEnabled", true)).volatile(),
|
|
4963
|
+
rmSafe: z.boolean().default(fieldDefault("rmSafe", true)).volatile(),
|
|
4964
|
+
useShellCommandWrapper: z.boolean().default(fieldDefault("useShellCommandWrapper", false)).volatile(),
|
|
4965
|
+
webCards: z.boolean().default(fieldDefault("webCards", true)).volatile(),
|
|
4966
|
+
maxBackgroundJobs: z.number().default(fieldDefault("maxBackgroundJobs", 15)).volatile(),
|
|
4967
|
+
outputTruncateStrategy: z.union(["bytes", "lines"]).default(fieldDefault("outputTruncateStrategy", "bytes")).volatile(),
|
|
4968
|
+
outputTruncateTriggerBytes: z.number().default(fieldDefault("outputTruncateTriggerBytes", 10240)).volatile(),
|
|
4969
|
+
outputTruncateTriggerLines: z.number().default(fieldDefault("outputTruncateTriggerLines", 100)).volatile(),
|
|
4948
4970
|
outputTruncateByteMode: z.union([
|
|
4949
4971
|
"head",
|
|
4950
4972
|
"tail",
|
|
4951
4973
|
"middle"
|
|
4952
|
-
]).default(fieldDefault("outputTruncateByteMode", "middle")),
|
|
4953
|
-
outputTruncateByteHeadBytes: z.number().default(fieldDefault("outputTruncateByteHeadBytes", 4096)),
|
|
4954
|
-
outputTruncateByteTailBytes: z.number().default(fieldDefault("outputTruncateByteTailBytes", 4096)),
|
|
4974
|
+
]).default(fieldDefault("outputTruncateByteMode", "middle")).volatile(),
|
|
4975
|
+
outputTruncateByteHeadBytes: z.number().default(fieldDefault("outputTruncateByteHeadBytes", 4096)).volatile(),
|
|
4976
|
+
outputTruncateByteTailBytes: z.number().default(fieldDefault("outputTruncateByteTailBytes", 4096)).volatile(),
|
|
4955
4977
|
outputTruncateLineMode: z.union([
|
|
4956
4978
|
"head",
|
|
4957
4979
|
"tail",
|
|
4958
4980
|
"middle"
|
|
4959
|
-
]).default(fieldDefault("outputTruncateLineMode", "middle")),
|
|
4960
|
-
outputTruncateLineHeadLines: z.number().default(fieldDefault("outputTruncateLineHeadLines", 50)),
|
|
4961
|
-
outputTruncateLineTailLines: z.number().default(fieldDefault("outputTruncateLineTailLines", 100)),
|
|
4962
|
-
editBlockAutoGenerated: z.boolean().default(fieldDefault("editBlockAutoGenerated", true)),
|
|
4963
|
-
readSummarizeEnabled: z.boolean().default(fieldDefault("readSummarizeEnabled", true)),
|
|
4964
|
-
readSummarizeProse: z.boolean().default(fieldDefault("readSummarizeProse", false)),
|
|
4965
|
-
readSummarizeMinBodyLines: z.number().default(fieldDefault("readSummarizeMinBodyLines", 4)),
|
|
4966
|
-
readSummarizeMinCommentLines: z.number().default(fieldDefault("readSummarizeMinCommentLines", 6)),
|
|
4967
|
-
readSummarizeMinTotalLines: z.number().default(fieldDefault("readSummarizeMinTotalLines", 100)),
|
|
4968
|
-
readSummarizeUnfoldUntil: z.number().default(fieldDefault("readSummarizeUnfoldUntil", 50)),
|
|
4969
|
-
readSummarizeUnfoldLimit: z.number().default(fieldDefault("readSummarizeUnfoldLimit", 100)),
|
|
4981
|
+
]).default(fieldDefault("outputTruncateLineMode", "middle")).volatile(),
|
|
4982
|
+
outputTruncateLineHeadLines: z.number().default(fieldDefault("outputTruncateLineHeadLines", 50)).volatile(),
|
|
4983
|
+
outputTruncateLineTailLines: z.number().default(fieldDefault("outputTruncateLineTailLines", 100)).volatile(),
|
|
4984
|
+
editBlockAutoGenerated: z.boolean().default(fieldDefault("editBlockAutoGenerated", true)).volatile(),
|
|
4985
|
+
readSummarizeEnabled: z.boolean().default(fieldDefault("readSummarizeEnabled", true)).volatile(),
|
|
4986
|
+
readSummarizeProse: z.boolean().default(fieldDefault("readSummarizeProse", false)).volatile(),
|
|
4987
|
+
readSummarizeMinBodyLines: z.number().default(fieldDefault("readSummarizeMinBodyLines", 4)).volatile(),
|
|
4988
|
+
readSummarizeMinCommentLines: z.number().default(fieldDefault("readSummarizeMinCommentLines", 6)).volatile(),
|
|
4989
|
+
readSummarizeMinTotalLines: z.number().default(fieldDefault("readSummarizeMinTotalLines", 100)).volatile(),
|
|
4990
|
+
readSummarizeUnfoldUntil: z.number().default(fieldDefault("readSummarizeUnfoldUntil", 50)).volatile(),
|
|
4991
|
+
readSummarizeUnfoldLimit: z.number().default(fieldDefault("readSummarizeUnfoldLimit", 100)).volatile(),
|
|
4970
4992
|
editMode: z.union([
|
|
4971
4993
|
"replace",
|
|
4972
4994
|
"patch",
|
|
4973
4995
|
"hashline",
|
|
4974
4996
|
"apply_patch"
|
|
4975
|
-
]).default(fieldDefault("editMode", "replace")),
|
|
4976
|
-
editFuzzyMatch: z.boolean().default(fieldDefault("editFuzzyMatch", true)),
|
|
4977
|
-
editFuzzyThreshold: z.number().default(fieldDefault("editFuzzyThreshold", .95)),
|
|
4978
|
-
editEnforceSeenLines: z.boolean().default(fieldDefault("editEnforceSeenLines", false)),
|
|
4979
|
-
readDefaultLimit: z.number().default(fieldDefault("readDefaultLimit", 300)),
|
|
4980
|
-
readLineNumbers: z.boolean().default(fieldDefault("readLineNumbers", false)),
|
|
4981
|
-
readRenderMarkdown: z.boolean().default(fieldDefault("readRenderMarkdown", false)),
|
|
4982
|
-
readConcurrentSafe: z.boolean().default(fieldDefault("readConcurrentSafe", true)),
|
|
4983
|
-
grepContextBefore: z.number().default(fieldDefault("grepContextBefore", 1)),
|
|
4984
|
-
grepContextAfter: z.number().default(fieldDefault("grepContextAfter", 3)),
|
|
4985
|
-
grepCaseDefault: z.boolean().default(fieldDefault("grepCaseDefault", true)),
|
|
4986
|
-
grepGitignoreDefault: z.boolean().default(fieldDefault("grepGitignoreDefault", true)),
|
|
4987
|
-
globGitignoreDefault: z.boolean().default(fieldDefault("globGitignoreDefault", true)),
|
|
4988
|
-
globHiddenDefault: z.boolean().default(fieldDefault("globHiddenDefault", true)),
|
|
4989
|
-
astGrepEnabled: z.boolean().default(fieldDefault("astGrepEnabled", false)),
|
|
4990
|
-
astEditEnabled: z.boolean().default(fieldDefault("astEditEnabled", true)),
|
|
4991
|
-
fetchEnabled: z.boolean().default(fieldDefault("fetchEnabled", true)),
|
|
4992
|
-
fetchMaxTimeoutSeconds: z.number().default(fieldDefault("fetchMaxTimeoutSeconds", 0)),
|
|
4997
|
+
]).default(fieldDefault("editMode", "replace")).volatile(),
|
|
4998
|
+
editFuzzyMatch: z.boolean().default(fieldDefault("editFuzzyMatch", true)).volatile(),
|
|
4999
|
+
editFuzzyThreshold: z.number().default(fieldDefault("editFuzzyThreshold", .95)).volatile(),
|
|
5000
|
+
editEnforceSeenLines: z.boolean().default(fieldDefault("editEnforceSeenLines", false)).volatile(),
|
|
5001
|
+
readDefaultLimit: z.number().default(fieldDefault("readDefaultLimit", 300)).volatile(),
|
|
5002
|
+
readLineNumbers: z.boolean().default(fieldDefault("readLineNumbers", false)).volatile(),
|
|
5003
|
+
readRenderMarkdown: z.boolean().default(fieldDefault("readRenderMarkdown", false)).volatile(),
|
|
5004
|
+
readConcurrentSafe: z.boolean().default(fieldDefault("readConcurrentSafe", true)).volatile(),
|
|
5005
|
+
grepContextBefore: z.number().default(fieldDefault("grepContextBefore", 1)).volatile(),
|
|
5006
|
+
grepContextAfter: z.number().default(fieldDefault("grepContextAfter", 3)).volatile(),
|
|
5007
|
+
grepCaseDefault: z.boolean().default(fieldDefault("grepCaseDefault", true)).volatile(),
|
|
5008
|
+
grepGitignoreDefault: z.boolean().default(fieldDefault("grepGitignoreDefault", true)).volatile(),
|
|
5009
|
+
globGitignoreDefault: z.boolean().default(fieldDefault("globGitignoreDefault", true)).volatile(),
|
|
5010
|
+
globHiddenDefault: z.boolean().default(fieldDefault("globHiddenDefault", true)).volatile(),
|
|
5011
|
+
astGrepEnabled: z.boolean().default(fieldDefault("astGrepEnabled", false)).volatile(),
|
|
5012
|
+
astEditEnabled: z.boolean().default(fieldDefault("astEditEnabled", true)).volatile(),
|
|
5013
|
+
fetchEnabled: z.boolean().default(fieldDefault("fetchEnabled", true)).volatile(),
|
|
5014
|
+
fetchMaxTimeoutSeconds: z.number().default(fieldDefault("fetchMaxTimeoutSeconds", 0)).volatile(),
|
|
4993
5015
|
fetchReader: z.union([
|
|
4994
5016
|
"auto",
|
|
4995
5017
|
"native",
|
|
@@ -4998,26 +5020,39 @@ const Config = z.object({
|
|
|
4998
5020
|
"parallel",
|
|
4999
5021
|
"jina",
|
|
5000
5022
|
"browser"
|
|
5001
|
-
]).default(fieldDefault("fetchReader", "auto")),
|
|
5002
|
-
browserReaderEnabled: z.boolean().default(fieldDefault("browserReaderEnabled", true)),
|
|
5003
|
-
imagesAutoResize: z.boolean().default(fieldDefault("imagesAutoResize", true)),
|
|
5004
|
-
imagesBlockImages: z.boolean().default(fieldDefault("imagesBlockImages", false)),
|
|
5005
|
-
imagesExcludeWebp: z.boolean().default(fieldDefault("imagesExcludeWebp", false)),
|
|
5006
|
-
imagesInputMaxBytes: z.number().default(fieldDefault("imagesInputMaxBytes", 20971520)),
|
|
5007
|
-
imagesResizeMaxSide: z.number().default(fieldDefault("imagesResizeMaxSide", 1568)),
|
|
5008
|
-
imagesResizeMaxBytes: z.number().default(fieldDefault("imagesResizeMaxBytes", 512e3)),
|
|
5009
|
-
imagesResizeMinSide: z.number().default(fieldDefault("imagesResizeMinSide", 200)),
|
|
5010
|
-
imagesResizeJpegQuality: z.number().default(fieldDefault("imagesResizeJpegQuality", 80))
|
|
5023
|
+
]).default(fieldDefault("fetchReader", "auto")).volatile(),
|
|
5024
|
+
browserReaderEnabled: z.boolean().default(fieldDefault("browserReaderEnabled", true)).volatile(),
|
|
5025
|
+
imagesAutoResize: z.boolean().default(fieldDefault("imagesAutoResize", true)).volatile(),
|
|
5026
|
+
imagesBlockImages: z.boolean().default(fieldDefault("imagesBlockImages", false)).volatile(),
|
|
5027
|
+
imagesExcludeWebp: z.boolean().default(fieldDefault("imagesExcludeWebp", false)).volatile(),
|
|
5028
|
+
imagesInputMaxBytes: z.number().default(fieldDefault("imagesInputMaxBytes", 20971520)).volatile(),
|
|
5029
|
+
imagesResizeMaxSide: z.number().default(fieldDefault("imagesResizeMaxSide", 1568)).volatile(),
|
|
5030
|
+
imagesResizeMaxBytes: z.number().default(fieldDefault("imagesResizeMaxBytes", 512e3)).volatile(),
|
|
5031
|
+
imagesResizeMinSide: z.number().default(fieldDefault("imagesResizeMinSide", 200)).volatile(),
|
|
5032
|
+
imagesResizeJpegQuality: z.number().default(fieldDefault("imagesResizeJpegQuality", 80)).volatile()
|
|
5011
5033
|
});
|
|
5012
5034
|
/**
|
|
5035
|
+
* Unwrap the Loader's live field references into a plain settings document.
|
|
5036
|
+
* @param input - the entry's live config, or an already-plain document.
|
|
5037
|
+
* @returns a plain copy carrying the fields the input has.
|
|
5038
|
+
*/
|
|
5039
|
+
function plainFields(input) {
|
|
5040
|
+
const out = {};
|
|
5041
|
+
for (const [field, value] of Object.entries(input)) Object.assign(out, { [field]: isLiveField(value) ? value.get() : value });
|
|
5042
|
+
return out;
|
|
5043
|
+
}
|
|
5044
|
+
/**
|
|
5013
5045
|
* Resolve the flat settings (composition entry or the settings document) to
|
|
5014
5046
|
* the nested runtime config the ported OMP runtime consumes. A direct mount
|
|
5015
5047
|
* (`ctx.plugin`) skips the Loader's schema parse, so the `?? default` pass
|
|
5016
5048
|
* mirrors the schema; the settings namespace resolution rides the same path.
|
|
5017
|
-
*
|
|
5049
|
+
* Accepts both shapes a caller can hold: the Loader's live entry config (one
|
|
5050
|
+
* reference per field) and a plain settings document.
|
|
5051
|
+
* @param input - the entry's live config, or a plain (possibly partial) document.
|
|
5018
5052
|
* @returns the fully-defaulted nested runtime config.
|
|
5019
5053
|
*/
|
|
5020
|
-
function resolveConfig(
|
|
5054
|
+
function resolveConfig(input) {
|
|
5055
|
+
const config = plainFields(input);
|
|
5021
5056
|
const resolveMinimizer = () => ({
|
|
5022
5057
|
enabled: config.minimizerEnabled ?? fieldDefault("minimizerEnabled", true),
|
|
5023
5058
|
only: [],
|
|
@@ -5094,29 +5129,46 @@ function resolveConfig(config) {
|
|
|
5094
5129
|
};
|
|
5095
5130
|
}
|
|
5096
5131
|
/**
|
|
5097
|
-
* Install the optional-settings consumer wiring for this plugin
|
|
5098
|
-
*
|
|
5099
|
-
*
|
|
5100
|
-
*
|
|
5101
|
-
*
|
|
5102
|
-
*
|
|
5132
|
+
* Install the optional-settings consumer wiring for this plugin, in the dsh
|
|
5133
|
+
* 0.1.7 shape.
|
|
5134
|
+
*
|
|
5135
|
+
* 0.1.7 deleted the namespace-registration API (`SettingsForms` no longer has
|
|
5136
|
+
* `installSection`): a settings namespace is not something a plugin installs
|
|
5137
|
+
* any more — it IS the plugin's own profile entry, and the service serves
|
|
5138
|
+
* exactly the Config fields whose schema declares them live
|
|
5139
|
+
* ({@link Config} declares every field `.volatile()`). The plugin therefore
|
|
5140
|
+
* keeps one authoritative source, its own entry config:
|
|
5141
|
+
*
|
|
5142
|
+
* - the Loader merges the composition layers and the profile override before
|
|
5143
|
+
* `apply` runs, so the entry already carries the effective settings;
|
|
5144
|
+
* - a settings write is committed INTO that entry's live references without a
|
|
5145
|
+
* remount, and the Loader then announces the changed paths on this fiber
|
|
5146
|
+
* (`loader/volatile-update`) — which is the signal for refreshing
|
|
5147
|
+
* registration facts (the `editMode`-sensitive descriptions and the AST
|
|
5148
|
+
* switches), exactly as the settings docs prescribe.
|
|
5149
|
+
*
|
|
5150
|
+
* The plugin ships its own settings pages in the browser half, so it owns its
|
|
5151
|
+
* page policy: `configure({ auto: false })` tells the settings service not to
|
|
5152
|
+
* auto-generate a schema page for this entry. That call rides an optional
|
|
5153
|
+
* `ctx.inject(['settings'], ...)` child, so the plugin runs unchanged in a
|
|
5154
|
+
* deployment that composes no settings service at all.
|
|
5155
|
+
*
|
|
5156
|
+
* Mirrors the official bash-local pattern (`packages/shell/bash-local`): the
|
|
5157
|
+
* business plugin reads its own Config references and registers only its page
|
|
5158
|
+
* policy; nothing re-registers the namespace.
|
|
5103
5159
|
* @param ctx - plugin context owning the wiring.
|
|
5104
|
-
* @param entry -
|
|
5160
|
+
* @param entry - the entry's config as the Loader resolved it (live references
|
|
5161
|
+
* over the composition layers and the profile override), or a plain document.
|
|
5105
5162
|
* @param onSource - receives a thunk of the currently authoritative runtime config.
|
|
5106
5163
|
*/
|
|
5107
5164
|
function installBashPlusSettings(ctx, entry, onSource) {
|
|
5108
|
-
|
|
5165
|
+
const current = () => resolveConfig(entry);
|
|
5166
|
+
onSource(current);
|
|
5167
|
+
ctx.on("loader/volatile-update", () => {
|
|
5168
|
+
onSource(current);
|
|
5169
|
+
});
|
|
5109
5170
|
ctx.inject(["settings"], (settingsCtx) => {
|
|
5110
|
-
settingsCtx.settings.
|
|
5111
|
-
setSource: (current) => {
|
|
5112
|
-
currentSource = current;
|
|
5113
|
-
onSource(() => resolveConfig(current()));
|
|
5114
|
-
},
|
|
5115
|
-
onChange: () => {
|
|
5116
|
-
const source = currentSource;
|
|
5117
|
-
if (source) onSource(() => resolveConfig(source()));
|
|
5118
|
-
}
|
|
5119
|
-
});
|
|
5171
|
+
settingsCtx.effect(() => settingsCtx.settings.configure({ auto: false }, ctx.fiber), "tool-plus: own settings pages");
|
|
5120
5172
|
});
|
|
5121
5173
|
}
|
|
5122
5174
|
//#endregion
|
|
@@ -5135,13 +5187,27 @@ function installBashPlusSettings(ctx, entry, onSource) {
|
|
|
5135
5187
|
/** Logical RPC channel registered by the host half of this plugin. */
|
|
5136
5188
|
const TOOL_PLUS_RPC_CHANNEL = "/tool-plus";
|
|
5137
5189
|
/**
|
|
5138
|
-
* The preset ids this plugin
|
|
5139
|
-
*
|
|
5140
|
-
*
|
|
5141
|
-
*
|
|
5190
|
+
* The preset ids this plugin **declares**. Single source of truth for both
|
|
5191
|
+
* halves: the host picks its own declarations out of the profile's composed
|
|
5192
|
+
* configuration, and the panel uses the list to tell "ours" from everything else.
|
|
5193
|
+
*
|
|
5194
|
+
* They ship as `@deepseek-ai/dsh-agent-preset` declaration rows in this
|
|
5195
|
+
* package's own bundle patches (`presets/<id>.patch.yml`), so installing the
|
|
5196
|
+
* plugin as a profile bundle declares them: no install step, no file copy, and
|
|
5197
|
+
* nothing written at startup.
|
|
5142
5198
|
*/
|
|
5143
5199
|
const BUNDLED_PRESET_IDS = ["tool-plus-standard", "tool-plus-ptc"];
|
|
5144
5200
|
/**
|
|
5201
|
+
* Loader row id a preset declaration is addressed by. A profile patch overrides
|
|
5202
|
+
* a declaration **by this id** — the harness's config editor locates the row by
|
|
5203
|
+
* id, and the override replaces the row's whole `config`.
|
|
5204
|
+
* @param presetId - The preset identity (the declaration's `config.id`).
|
|
5205
|
+
* @returns The row id as it appears in profile patches.
|
|
5206
|
+
*/
|
|
5207
|
+
function profileRowId(presetId) {
|
|
5208
|
+
return `preset-${presetId}`;
|
|
5209
|
+
}
|
|
5210
|
+
/**
|
|
5145
5211
|
* Whether one difference belongs to the "yours" group (you added or changed it)
|
|
5146
5212
|
* rather than the "behind" group (the template has it, you do not). Lives in the
|
|
5147
5213
|
* shared contract so host and client group the exact same way.
|
|
@@ -5288,8 +5354,8 @@ function parsePresetComparePayload(payload) {
|
|
|
5288
5354
|
}
|
|
5289
5355
|
/**
|
|
5290
5356
|
* Validate a `presets/apply` payload. Anything that is not exactly
|
|
5291
|
-
* `{ id: non-empty string, action: 'upgrade' | '
|
|
5292
|
-
* write endpoint never receives a half-formed request.
|
|
5357
|
+
* `{ id: non-empty string, action: 'upgrade' | 'align' | 'revert', templateId?: non-empty string }`
|
|
5358
|
+
* is rejected, so the write endpoint never receives a half-formed request.
|
|
5293
5359
|
* @param payload - the raw RPC payload.
|
|
5294
5360
|
* @returns the narrowed payload, or undefined when invalid.
|
|
5295
5361
|
*/
|
|
@@ -5297,7 +5363,7 @@ function parsePresetActionPayload(payload) {
|
|
|
5297
5363
|
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) return void 0;
|
|
5298
5364
|
const { id, action, templateId } = payload;
|
|
5299
5365
|
if (typeof id !== "string" || id.length === 0) return void 0;
|
|
5300
|
-
if (action !== "upgrade" && action !== "
|
|
5366
|
+
if (action !== "upgrade" && action !== "align" && action !== "revert") return void 0;
|
|
5301
5367
|
if (templateId !== void 0 && (typeof templateId !== "string" || templateId.length === 0)) return void 0;
|
|
5302
5368
|
return templateId === void 0 ? {
|
|
5303
5369
|
id,
|
|
@@ -5308,26 +5374,8 @@ function parsePresetActionPayload(payload) {
|
|
|
5308
5374
|
templateId
|
|
5309
5375
|
};
|
|
5310
5376
|
}
|
|
5311
|
-
/**
|
|
5312
|
-
* Narrow one roster entry coming from the official service. Unknown trust is
|
|
5313
|
-
* treated as `user` (the conservative side: it is a writable preset only when
|
|
5314
|
-
* the caller proves otherwise — the caller matches ids against our templates).
|
|
5315
|
-
* @param preset - raw entry from `ctx.agentPresets.list()`.
|
|
5316
|
-
* @returns the entry in our own shape.
|
|
5317
|
-
*/
|
|
5318
|
-
function toRosterEntry(preset) {
|
|
5319
|
-
const text = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
|
|
5320
|
-
return {
|
|
5321
|
-
id: text(preset.id) ?? "",
|
|
5322
|
-
trust: preset.trust === "system" ? "system" : "user",
|
|
5323
|
-
path: text(preset.path) ?? "",
|
|
5324
|
-
name: text(preset.name),
|
|
5325
|
-
description: text(preset.description),
|
|
5326
|
-
broken: text(preset.broken)
|
|
5327
|
-
};
|
|
5328
|
-
}
|
|
5329
5377
|
/** One RPC error result in the gateway's shape. */
|
|
5330
|
-
function failure$
|
|
5378
|
+
function failure$2(error, code = "internal") {
|
|
5331
5379
|
return {
|
|
5332
5380
|
ok: false,
|
|
5333
5381
|
error: {
|
|
@@ -5338,62 +5386,47 @@ function failure$1(error, code = "internal") {
|
|
|
5338
5386
|
};
|
|
5339
5387
|
}
|
|
5340
5388
|
/**
|
|
5341
|
-
* Answer one preset endpoint on the shared `/tool-plus`
|
|
5389
|
+
* Answer one preset endpoint on the shared `/tool-plus` route.
|
|
5342
5390
|
*
|
|
5343
5391
|
* Returns `undefined` for an endpoint this module does not own, so the caller
|
|
5344
|
-
* (the single
|
|
5345
|
-
*
|
|
5346
|
-
*
|
|
5347
|
-
* @param ctx - context used to resolve the `agentPresets` service.
|
|
5348
|
-
* @param deps - injected preset analysis.
|
|
5392
|
+
* (the single route handler) can keep serving its own families.
|
|
5393
|
+
* @param ctx - context forwarded to the injected host implementation.
|
|
5394
|
+
* @param deps - injected host implementation.
|
|
5349
5395
|
* @param endpoint - the requested endpoint name.
|
|
5350
5396
|
* @param payload - the raw RPC payload.
|
|
5351
5397
|
* @returns the RPC result, or undefined when the endpoint is not ours.
|
|
5352
5398
|
*/
|
|
5353
5399
|
async function handlePresetEndpoint(ctx, deps, endpoint, payload) {
|
|
5354
5400
|
if (endpoint !== "presets/status" && endpoint !== "presets/apply" && endpoint !== "presets/compare") return;
|
|
5355
|
-
const service = ctx.get("agentPresets");
|
|
5356
|
-
if (!service || typeof service.list !== "function") return failure$1("agent presets are not available in this deployment");
|
|
5357
5401
|
if (endpoint === "presets/status") try {
|
|
5358
|
-
const roster = await service.list();
|
|
5359
5402
|
return {
|
|
5360
5403
|
ok: true,
|
|
5361
|
-
value:
|
|
5362
|
-
presets: deps.listStatuses(roster.map(toRosterEntry)),
|
|
5363
|
-
templates: deps.listTemplates()
|
|
5364
|
-
}
|
|
5404
|
+
value: await deps.listPresets(ctx)
|
|
5365
5405
|
};
|
|
5366
5406
|
} catch (error) {
|
|
5367
|
-
return failure$
|
|
5407
|
+
return failure$2(error);
|
|
5368
5408
|
}
|
|
5369
5409
|
if (endpoint === "presets/compare") {
|
|
5370
5410
|
const parsed = parsePresetComparePayload(payload);
|
|
5371
|
-
if (!parsed) return failure$
|
|
5411
|
+
if (!parsed) return failure$2("presets/compare expects { presetId: string, templateId: string }", "bad-request");
|
|
5372
5412
|
try {
|
|
5373
|
-
const preset = (await service.list()).map(toRosterEntry).find((entry) => entry.id === parsed.presetId);
|
|
5374
|
-
if (preset === void 0) return failure$1(`unknown preset: ${parsed.presetId}`, "bad-request");
|
|
5375
|
-
if (!deps.listTemplates().some((template) => template.id === parsed.templateId)) return failure$1(`unknown template: ${parsed.templateId}`, "bad-request");
|
|
5376
5413
|
return {
|
|
5377
5414
|
ok: true,
|
|
5378
|
-
value:
|
|
5379
|
-
presetId: parsed.presetId,
|
|
5380
|
-
templateId: parsed.templateId,
|
|
5381
|
-
...deps.comparePreset(preset, parsed.templateId)
|
|
5382
|
-
}
|
|
5415
|
+
value: await deps.comparePreset(ctx, parsed.presetId, parsed.templateId)
|
|
5383
5416
|
};
|
|
5384
5417
|
} catch (error) {
|
|
5385
|
-
return failure$
|
|
5418
|
+
return failure$2(error);
|
|
5386
5419
|
}
|
|
5387
5420
|
}
|
|
5388
5421
|
const parsed = parsePresetActionPayload(payload);
|
|
5389
|
-
if (!parsed) return failure$
|
|
5422
|
+
if (!parsed) return failure$2("presets/apply expects { id: string, action: \"upgrade\" | \"align\" | \"revert\", templateId?: string }", "bad-request");
|
|
5390
5423
|
try {
|
|
5391
5424
|
return {
|
|
5392
5425
|
ok: true,
|
|
5393
|
-
value: deps.applyAction(parsed.id, parsed.action, parsed.templateId)
|
|
5426
|
+
value: await deps.applyAction(ctx, parsed.id, parsed.action, parsed.templateId)
|
|
5394
5427
|
};
|
|
5395
5428
|
} catch (error) {
|
|
5396
|
-
return failure$
|
|
5429
|
+
return failure$2(error);
|
|
5397
5430
|
}
|
|
5398
5431
|
}
|
|
5399
5432
|
//#endregion
|
|
@@ -5431,11 +5464,14 @@ function toGitBashPath(p) {
|
|
|
5431
5464
|
}
|
|
5432
5465
|
/** 默认 spawn 实现:收集 stdout,超时 kill 并标记 timedOut。 */
|
|
5433
5466
|
const defaultSpawnDeps = { spawnShell: (shell, args, timeoutMs) => new Promise((resolve, reject) => {
|
|
5434
|
-
const child = spawn(shell, args, {
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5467
|
+
const child = spawn(shell, args, {
|
|
5468
|
+
stdio: [
|
|
5469
|
+
"ignore",
|
|
5470
|
+
"pipe",
|
|
5471
|
+
"pipe"
|
|
5472
|
+
],
|
|
5473
|
+
windowsHide: true
|
|
5474
|
+
});
|
|
5439
5475
|
let stdout = "";
|
|
5440
5476
|
let timedOut = false;
|
|
5441
5477
|
const timer = setTimeout(() => {
|
|
@@ -5511,7 +5547,8 @@ async function queryRmSafeStatus(deps) {
|
|
|
5511
5547
|
*
|
|
5512
5548
|
* **Why this registers its own route instead of calling `connection.rpc.handle`.**
|
|
5513
5549
|
* The documented helper is unusable from a plugin in a real web deployment on
|
|
5514
|
-
* dsh 0.1.5-rc.1
|
|
5550
|
+
* dsh 0.1.5-rc.1 — and still on 0.1.7-rc.1, re-checked for this migration:
|
|
5551
|
+
* `HostConnectionService.rpc` builds its handle against
|
|
5515
5552
|
* `owner = this.ctx` (`rpc-host.ts: "Generic channel registry scoped to the
|
|
5516
5553
|
* Context reading this service"`), and `register()` then evaluates
|
|
5517
5554
|
* `owner.webServer.register(route)`. The client-connection row declares
|
|
@@ -5542,7 +5579,7 @@ async function queryRmSafeStatus(deps) {
|
|
|
5542
5579
|
/** Cap on one request body; every legitimate payload here is a few hundred bytes. */
|
|
5543
5580
|
const MAX_BODY_BYTES = 1048576;
|
|
5544
5581
|
/** One internal error result in the gateway's shape. */
|
|
5545
|
-
function failure(error) {
|
|
5582
|
+
function failure$1(error) {
|
|
5546
5583
|
return {
|
|
5547
5584
|
ok: false,
|
|
5548
5585
|
error: {
|
|
@@ -5626,7 +5663,7 @@ function installBrowserProbeRpc(ctx, deps) {
|
|
|
5626
5663
|
value: { found: probeBrowsers() }
|
|
5627
5664
|
};
|
|
5628
5665
|
} catch (error) {
|
|
5629
|
-
return failure(error);
|
|
5666
|
+
return failure$1(error);
|
|
5630
5667
|
}
|
|
5631
5668
|
if (endpoint === "rmSafe/status") {
|
|
5632
5669
|
if (!deps.getRmSafe()) return {
|
|
@@ -5647,7 +5684,7 @@ function installBrowserProbeRpc(ctx, deps) {
|
|
|
5647
5684
|
})
|
|
5648
5685
|
};
|
|
5649
5686
|
} catch (error) {
|
|
5650
|
-
return failure(error);
|
|
5687
|
+
return failure$1(error);
|
|
5651
5688
|
}
|
|
5652
5689
|
}
|
|
5653
5690
|
if (deps.presets !== void 0) {
|
|
@@ -5733,772 +5770,295 @@ function installBrowserProbeRpc(ctx, deps) {
|
|
|
5733
5770
|
};
|
|
5734
5771
|
}
|
|
5735
5772
|
//#endregion
|
|
5736
|
-
//#region src/presets/
|
|
5737
|
-
/**
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
* 三处路径各有唯一来源:
|
|
5741
|
-
* - `<dshHome>`:`PresetDeps.homeDir`(测试注入)→ `$DSH_HOME`(trim 后非空)
|
|
5742
|
-
* → `~/.dsh`。规则与官方 `@deepseek-ai/dsh-home-paths` 的
|
|
5743
|
-
* `defaultDshHome()` / `DSH_HOME_ENV`(`'DSH_HOME'`)/ 目录名 `.dsh`
|
|
5744
|
-
* 完全一致;这里就地实现而不 import 该包——它在插件安装树里解析不到,
|
|
5745
|
-
* 静态 import 一个解析不到的模块会让整个插件加载失败。
|
|
5746
|
-
* - 用户根:`<dshHome>/.agent-presets`(与 `dsh-agent-presets` 的
|
|
5747
|
-
* `USER_PRESET_DIR` 同值,所有 profile 共享)。
|
|
5748
|
-
* - 模板:插件包内 `presets/<id>/`,用 `import.meta.url` 相对解析(与
|
|
5749
|
-
* `rmSafeCliPath()` 同一手法,见 src/tools/bash/rm-safe.ts)。构建后入口
|
|
5750
|
-
* 是单文件 `lib/index.mjs`,源码期是 `src/presets/paths.ts`,两者到包根的
|
|
5751
|
-
* 相对深度不同,所以向上探两个候选、取同时含 `package.json` 与 `presets/`
|
|
5752
|
-
* 的那个。
|
|
5753
|
-
* @module @xiaoso/dsh-tool-plus/presets/paths
|
|
5754
|
-
*/
|
|
5755
|
-
/** 用户根预设目录名(`dsh-agent-presets` 的 `USER_PRESET_DIR` 同值)。 */
|
|
5756
|
-
const PRESET_DIR_NAME = ".agent-presets";
|
|
5757
|
-
/** 让一个目录成为 preset 的组合文件名。 */
|
|
5758
|
-
const COMPOSITION_FILE_NAME = "agent.cordis.yml";
|
|
5759
|
-
/** 组合文件旁的可选展示元数据文件名。 */
|
|
5760
|
-
const METADATA_FILE_NAME = "preset.yml";
|
|
5761
|
-
/**
|
|
5762
|
-
* 本插件随包的两个预设 id(启动补缺只认它们)。名单的单一来源是共享契约
|
|
5763
|
-
* 模块 {@link BUNDLED_PRESET_IDS},这里只做本模块语义下的别名。
|
|
5764
|
-
*/
|
|
5765
|
-
const DEFAULT_PRESET_IDS = BUNDLED_PRESET_IDS;
|
|
5766
|
-
/** 覆盖 `DSH_HOME` 的环境变量名(与官方 `DSH_HOME_ENV` 同值)。 */
|
|
5767
|
-
const DSH_HOME_ENV = "DSH_HOME";
|
|
5768
|
-
/** 默认 harness home 的目录名(与官方 `DSH_HOME_DIR_NAME` 同值)。 */
|
|
5769
|
-
const DSH_HOME_DIR_NAME = ".dsh";
|
|
5770
|
-
/**
|
|
5771
|
-
* 解析 `<dshHome>`。优先级:注入的 `homeDir` → `$DSH_HOME`(trim 后非空)
|
|
5772
|
-
* → `~/.dsh`,与官方 `@deepseek-ai/dsh-home-paths` 的 `defaultDshHome()` 同规则。
|
|
5773
|
-
* @param deps - 可选的注入覆盖。
|
|
5774
|
-
* @returns 绝对路径的 harness home。
|
|
5775
|
-
*/
|
|
5776
|
-
function dshHomeDir(deps = {}) {
|
|
5777
|
-
if (deps.homeDir !== void 0 && deps.homeDir.trim().length > 0) return path$1.resolve(deps.homeDir);
|
|
5778
|
-
const env = process.env[DSH_HOME_ENV];
|
|
5779
|
-
return env !== void 0 && env.trim().length > 0 ? path$1.resolve(env) : path$1.join(os.homedir(), DSH_HOME_DIR_NAME);
|
|
5780
|
-
}
|
|
5781
|
-
/** 用户根预设目录:`<dshHome>/.agent-presets`(所有 profile 共享的写入根)。 */
|
|
5782
|
-
function userPresetRoot(deps = {}) {
|
|
5783
|
-
return path$1.join(dshHomeDir(deps), PRESET_DIR_NAME);
|
|
5784
|
-
}
|
|
5785
|
-
/** 某个预设的用户根目录:`<dshHome>/.agent-presets/<id>`。 */
|
|
5786
|
-
function userPresetDir(id, deps = {}) {
|
|
5787
|
-
return path$1.join(userPresetRoot(deps), id);
|
|
5788
|
-
}
|
|
5789
|
-
/**
|
|
5790
|
-
* 插件包根:注入的 `packageRoot` 优先,否则按 `import.meta.url` 向上探。
|
|
5791
|
-
* 构建后入口是 `lib/index.mjs`(`..` 即包根),源码期是
|
|
5792
|
-
* `src/presets/paths.ts`(`../..` 即包根),两个候选都试、取同时含
|
|
5793
|
-
* `package.json` 与 `presets/` 的那个;都不命中时返回 `../..` 候选。
|
|
5794
|
-
*/
|
|
5795
|
-
function packageRootDir(deps = {}) {
|
|
5796
|
-
if (deps.packageRoot !== void 0 && deps.packageRoot.trim().length > 0) return path$1.resolve(deps.packageRoot);
|
|
5797
|
-
const here = path$1.dirname(fileURLToPath(import.meta.url));
|
|
5798
|
-
const candidates = [path$1.resolve(here, "..", ".."), path$1.resolve(here, "..")];
|
|
5799
|
-
for (const candidate of candidates) if (fs.existsSync(path$1.join(candidate, "package.json")) && fs.existsSync(path$1.join(candidate, "presets"))) return candidate;
|
|
5800
|
-
return candidates[0];
|
|
5801
|
-
}
|
|
5802
|
-
/** 包内模板目录:`<包根>/presets/<id>`。 */
|
|
5803
|
-
function templateDir(id, deps = {}) {
|
|
5804
|
-
return path$1.join(packageRootDir(deps), "presets", id);
|
|
5773
|
+
//#region src/presets/rows.ts
|
|
5774
|
+
/** 行 id 的取值(非空字符串才认,与官方对 `name` 的要求同款)。 */
|
|
5775
|
+
function rowId(row) {
|
|
5776
|
+
return typeof row.id === "string" && row.id.length > 0 ? row.id : void 0;
|
|
5805
5777
|
}
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
* preset 组合改写:纯函数,文本进文本出,零 I/O。
|
|
5810
|
-
*
|
|
5811
|
-
* 一个预设要"支持本插件的工具",只需让官方 4 个冲突行 id 处于「不存在」或
|
|
5812
|
-
* `disabled: true` 状态(插件已在宿主面全局接管这 4 个工具;预设里再挂一份
|
|
5813
|
-
* 会造成 per-session 影子实例)。本模块只做这一件事:
|
|
5814
|
-
*
|
|
5815
|
-
* - 只认**顶层**行 `- id: <target>`(`-` 在第 0 列),嵌套子行一律不碰;
|
|
5816
|
-
* - 行块已有 `disabled: true` → no-op;`disabled: false`(或任何非 `true`
|
|
5817
|
-
* 标量)→ 就地改成 `true`;没有 `disabled` → 在行块末尾追加一行
|
|
5818
|
-
* ` disabled: true`(缩进与行内其它键对齐);行块不存在 → 不动;
|
|
5819
|
-
* - 注释、空行、缩进、CRLF/LF、YAML 顺序、用户的本地调参全部逐字节保留;
|
|
5820
|
-
* - 输出与输入逐字节相同时 `changes` 为空——调用方以字节相等判定是否写盘
|
|
5821
|
-
* (preset 代际以 `agent.cordis.yml` 的 mtime 为键,白写一次就白造一代);
|
|
5822
|
-
* - 文本不构成顶层行列表 → `shape: 'unrecognized'`,一律不改写。
|
|
5823
|
-
*
|
|
5824
|
-
* `PresetChange.action` 的 `'absent'` 供 RPC 摘要层描述"这行本来就不挂",
|
|
5825
|
-
* 改写本身只报告真实发生的改动(否则"幂等时 changes 为空"不成立)。
|
|
5826
|
-
* @module @xiaoso/dsh-tool-plus/presets/rewrite
|
|
5827
|
-
*/
|
|
5828
|
-
/** 默认冲突行 id:官方 standard / ptc / cordis 挂的 4 个官方工具行。 */
|
|
5829
|
-
const PRESET_CONFLICT_IDS = [
|
|
5830
|
-
"tool-bash",
|
|
5831
|
-
"tool-pwsh",
|
|
5832
|
-
"tool-fs",
|
|
5833
|
-
"tool-fs-search"
|
|
5834
|
-
];
|
|
5835
|
-
/** 顶层 `- id: <value>` 行(允许尾随注释;忽略缩进的同名行)。 */
|
|
5836
|
-
const TOP_ID_ROW = /^-[ \t]+id:[ \t]*([^#\s]+)[ \t]*(?:#.*)?$/;
|
|
5837
|
-
/** 任意顶层行(块边界判定)。 */
|
|
5838
|
-
const TOP_ROW = /^-(?:[ \t]|$)/;
|
|
5839
|
-
/** 行内键:`disabled: <value>`(任意缩进,缩进另判)。 */
|
|
5840
|
-
const DISABLED_KEY = /^([ \t]*)disabled[ \t]*:[ \t]*(.*)$/;
|
|
5841
|
-
/** 注释行(含缩进的纯注释行)。 */
|
|
5842
|
-
const COMMENT_LINE = /^[ \t]*#/;
|
|
5843
|
-
/** 空行(含仅空白行)。 */
|
|
5844
|
-
const BLANK_LINE = /^[ \t]*$/;
|
|
5845
|
-
/**
|
|
5846
|
-
* 无损拆行:每行保留自己的行尾符,`joinLines` 后与原文逐字节相同。
|
|
5847
|
-
* 只区分 LF 与 CRLF(YAML 组合文件的两种现实写法);孤立 `\r` 当内容。
|
|
5848
|
-
*/
|
|
5849
|
-
function splitLines(text) {
|
|
5850
|
-
const lines = [];
|
|
5851
|
-
let start = 0;
|
|
5852
|
-
for (let i = 0; i < text.length; i++) {
|
|
5853
|
-
if (text[i] !== "\n") continue;
|
|
5854
|
-
const crlf = i > start && text[i - 1] === "\r";
|
|
5855
|
-
lines.push({
|
|
5856
|
-
text: text.slice(start, crlf ? i - 1 : i),
|
|
5857
|
-
eol: crlf ? "\r\n" : "\n"
|
|
5858
|
-
});
|
|
5859
|
-
start = i + 1;
|
|
5860
|
-
}
|
|
5861
|
-
if (start < text.length) lines.push({
|
|
5862
|
-
text: text.slice(start),
|
|
5863
|
-
eol: ""
|
|
5864
|
-
});
|
|
5865
|
-
return lines;
|
|
5778
|
+
/** 行模块名(官方 entry-list 要求每行都有非空 `name`)。 */
|
|
5779
|
+
function rowName(row) {
|
|
5780
|
+
return typeof row.name === "string" && row.name.length > 0 ? row.name : void 0;
|
|
5866
5781
|
}
|
|
5867
|
-
/**
|
|
5868
|
-
function
|
|
5869
|
-
return
|
|
5870
|
-
}
|
|
5871
|
-
/** 文件的行尾风格:取第一个带行尾符的行,缺省 LF。 */
|
|
5872
|
-
function fileEol(lines) {
|
|
5873
|
-
for (const line of lines) if (line.eol !== "") return line.eol;
|
|
5874
|
-
return "\n";
|
|
5875
|
-
}
|
|
5876
|
-
/** 去掉 YAML 单双引号(`- id: 'tool-bash'`)。 */
|
|
5877
|
-
function stripQuotes(value) {
|
|
5878
|
-
if (value.length >= 2) {
|
|
5879
|
-
const first = value[0];
|
|
5880
|
-
const last = value[value.length - 1];
|
|
5881
|
-
if (first === "'" && last === "'" || first === "\"" && last === "\"") return value.slice(1, -1);
|
|
5882
|
-
}
|
|
5883
|
-
return value;
|
|
5884
|
-
}
|
|
5885
|
-
/** 缩进宽度(空格与制表符都按 1 计)。 */
|
|
5886
|
-
function indentWidth(line) {
|
|
5887
|
-
return line.length - line.trimStart().length;
|
|
5782
|
+
/** 这一行是不是 group 载体(group 的子行在 `config` 里)。 */
|
|
5783
|
+
function isGroupRow(row) {
|
|
5784
|
+
return row.group === true && Array.isArray(row.config);
|
|
5888
5785
|
}
|
|
5889
5786
|
/**
|
|
5890
|
-
*
|
|
5891
|
-
*
|
|
5892
|
-
*
|
|
5893
|
-
*
|
|
5787
|
+
* 判定一份 `plugins` 值是不是合法的顶层行列表。规则与官方
|
|
5788
|
+
* `entryListProblem` 一致:必须是数组,每一项必须是带非空 `name` 的映射。
|
|
5789
|
+
* @param value - 待判定的值(来自宿主配置或测试注入)。
|
|
5790
|
+
* @returns 合法时为 true。
|
|
5894
5791
|
*/
|
|
5895
|
-
function
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
if (!TOP_ROW.test(significant[0].text)) return "unrecognized";
|
|
5899
|
-
for (const line of significant) if (line.text.trimStart() === line.text && !TOP_ROW.test(line.text)) return "unrecognized";
|
|
5900
|
-
return "ok";
|
|
5792
|
+
function isRowList(value) {
|
|
5793
|
+
if (!Array.isArray(value)) return false;
|
|
5794
|
+
return value.every((row) => typeof row === "object" && row !== null && !Array.isArray(row) && rowName(row) !== void 0);
|
|
5901
5795
|
}
|
|
5902
|
-
/**
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5796
|
+
/**
|
|
5797
|
+
* 展平一棵行树(含 group 嵌套),键为 `父/子` 形式的路径。
|
|
5798
|
+
*
|
|
5799
|
+
* 路径是**给面板看的标识**,也用于我们自己的差异比较;官方
|
|
5800
|
+
* `compositionInventory()` 回的行没有路径(它压平后只剩 entryId/moduleName),
|
|
5801
|
+
* 所以需要分组结构时只能从声明本身取。
|
|
5802
|
+
* @param rows - 顶层行列表。
|
|
5803
|
+
* @param prefix - 递归时的父路径。
|
|
5804
|
+
* @returns 路径 → 行。
|
|
5805
|
+
*/
|
|
5806
|
+
function flattenRows(rows, prefix = "") {
|
|
5807
|
+
const out = /* @__PURE__ */ new Map();
|
|
5808
|
+
for (const row of rows) {
|
|
5809
|
+
const id = rowId(row);
|
|
5810
|
+
if (id === void 0) continue;
|
|
5811
|
+
const path = prefix.length > 0 ? `${prefix}/${id}` : id;
|
|
5812
|
+
out.set(path, row);
|
|
5813
|
+
if (isGroupRow(row)) for (const [nested, nestedRow] of flattenRows(row.config, path)) out.set(nested, nestedRow);
|
|
5914
5814
|
}
|
|
5915
|
-
return
|
|
5916
|
-
let end = lines.length;
|
|
5917
|
-
for (let i = row.index + 1; i < lines.length; i++) if (TOP_ROW.test(lines[i].text)) {
|
|
5918
|
-
end = i;
|
|
5919
|
-
break;
|
|
5920
|
-
}
|
|
5921
|
-
let keyIndent;
|
|
5922
|
-
let keyIndentText;
|
|
5923
|
-
for (let i = row.index + 1; i < end; i++) {
|
|
5924
|
-
const text = lines[i].text;
|
|
5925
|
-
if (BLANK_LINE.test(text) || COMMENT_LINE.test(text)) continue;
|
|
5926
|
-
keyIndent = indentWidth(text);
|
|
5927
|
-
keyIndentText = text.slice(0, keyIndent);
|
|
5928
|
-
break;
|
|
5929
|
-
}
|
|
5930
|
-
return {
|
|
5931
|
-
id: row.id,
|
|
5932
|
-
start: row.index,
|
|
5933
|
-
end,
|
|
5934
|
-
keyIndent,
|
|
5935
|
-
keyIndentText
|
|
5936
|
-
};
|
|
5937
|
-
});
|
|
5815
|
+
return out;
|
|
5938
5816
|
}
|
|
5939
5817
|
/**
|
|
5940
|
-
*
|
|
5941
|
-
*
|
|
5818
|
+
* `!!js` 表达式标记:entry-list 方言把它解析成 `{ __jsExpr: <源码> }`(见
|
|
5819
|
+
* `vendor/include/src/index.ts` 的 `JsExpr`)。它对我们是**标量**——一整段待求值的
|
|
5820
|
+
* 源码,不是可以逐键比较的结构。漏了这一步会造出 `disabled.__jsExpr` 这种假叶子,
|
|
5821
|
+
* 顺带让"冲突行的 disabled 归 conflicts 管"这条跳过规则失效。
|
|
5822
|
+
* @param value - 待判定的值。
|
|
5823
|
+
* @returns 是表达式标记时为 true。
|
|
5942
5824
|
*/
|
|
5943
|
-
function
|
|
5944
|
-
|
|
5945
|
-
const text = lines[i].text;
|
|
5946
|
-
if (BLANK_LINE.test(text) || COMMENT_LINE.test(text)) continue;
|
|
5947
|
-
if (row.keyIndent === void 0 || indentWidth(text) !== row.keyIndent) continue;
|
|
5948
|
-
if (DISABLED_KEY.test(text)) return i;
|
|
5949
|
-
}
|
|
5825
|
+
function isJsExpr(value) {
|
|
5826
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 1 && typeof value.__jsExpr === "string";
|
|
5950
5827
|
}
|
|
5951
|
-
/**
|
|
5952
|
-
function
|
|
5953
|
-
|
|
5954
|
-
if (
|
|
5955
|
-
return
|
|
5828
|
+
/** 规范化一个标量用于比较与展示。 */
|
|
5829
|
+
function canonical(value) {
|
|
5830
|
+
if (value === void 0) return "";
|
|
5831
|
+
if (isJsExpr(value)) return `!!js ${value.__jsExpr}`;
|
|
5832
|
+
return JSON.stringify(value) ?? String(value);
|
|
5956
5833
|
}
|
|
5957
5834
|
/**
|
|
5958
|
-
*
|
|
5835
|
+
* 收集一行自己的叶子路径(不含 `id`,不含 group 的子行)。
|
|
5959
5836
|
*
|
|
5960
|
-
*
|
|
5961
|
-
*
|
|
5962
|
-
* `false
|
|
5963
|
-
*
|
|
5964
|
-
*
|
|
5965
|
-
*
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
const commentPart = /([ \t]+#.*)$/.exec(rest)?.[1] ?? "";
|
|
5975
|
-
const value = (commentPart === "" ? rest : rest.slice(0, rest.length - commentPart.length)).trim();
|
|
5976
|
-
const note = value === "" || value === "false" ? "" : `; was: ${value}`;
|
|
5977
|
-
if (commentPart !== "") return `${indent}disabled: true${commentPart}${note}`;
|
|
5978
|
-
return note === "" ? `${indent}disabled: true` : `${indent}disabled: true # was: ${value}`;
|
|
5979
|
-
}
|
|
5980
|
-
/**
|
|
5981
|
-
* 追加位置:行块末尾最后一处"内容行"之后(跳过尾随空行与注释行,
|
|
5982
|
-
* 免得把 `disabled` 插到下一行块的说明注释之后)。
|
|
5983
|
-
*/
|
|
5984
|
-
function appendIndex(lines, row) {
|
|
5985
|
-
let last = row.start;
|
|
5986
|
-
for (let i = row.start + 1; i < row.end; i++) {
|
|
5987
|
-
const text = lines[i].text;
|
|
5988
|
-
if (BLANK_LINE.test(text) || COMMENT_LINE.test(text)) continue;
|
|
5989
|
-
last = i;
|
|
5990
|
-
}
|
|
5991
|
-
return last + 1;
|
|
5992
|
-
}
|
|
5993
|
-
/** 去重后按出现顺序的冲突 id。 */
|
|
5994
|
-
function normalizeIds(conflictIds) {
|
|
5995
|
-
return [...new Set((conflictIds ?? PRESET_CONFLICT_IDS).filter((id) => id.length > 0))];
|
|
5996
|
-
}
|
|
5997
|
-
/** 逐个目标 id 定处置(不改动文本)。 */
|
|
5998
|
-
function plan(lines, rows, ids) {
|
|
5999
|
-
const plans = [];
|
|
6000
|
-
for (const id of ids) {
|
|
6001
|
-
const row = rows.find((candidate) => candidate.id === id);
|
|
6002
|
-
if (row === void 0) continue;
|
|
6003
|
-
const disabledAt = findDisabledLine(lines, row);
|
|
6004
|
-
if (disabledAt !== void 0) {
|
|
6005
|
-
if (disabledValue(lines[disabledAt].text) === "true") continue;
|
|
6006
|
-
plans.push({
|
|
6007
|
-
kind: "flip",
|
|
6008
|
-
id,
|
|
6009
|
-
line: disabledAt
|
|
5837
|
+
* 用规范化 JSON 而不是 `String()`:Loader 对 `disabled` 的语义是
|
|
5838
|
+
* "布尔取 Boolean(value)",所以字符串 `"false"` 与布尔 `false` **语义相反**;
|
|
5839
|
+
* 两者若都显示成 `false`,面板就会把一处真实差异藏起来。
|
|
5840
|
+
* @param row - 目标行。
|
|
5841
|
+
* @param base - 递归时的前缀。
|
|
5842
|
+
* @returns 该行的叶子清单(顺序为键的声明顺序)。
|
|
5843
|
+
*/
|
|
5844
|
+
function rowLeaves(row, base = "") {
|
|
5845
|
+
const out = [];
|
|
5846
|
+
const walk = (value, path) => {
|
|
5847
|
+
if (isJsExpr(value)) {
|
|
5848
|
+
out.push({
|
|
5849
|
+
path,
|
|
5850
|
+
value: canonical(value)
|
|
6010
5851
|
});
|
|
6011
|
-
|
|
5852
|
+
return;
|
|
6012
5853
|
}
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
5854
|
+
if (Array.isArray(value)) {
|
|
5855
|
+
value.forEach((item, index) => walk(item, `${path}[${index}]`));
|
|
5856
|
+
return;
|
|
5857
|
+
}
|
|
5858
|
+
if (value !== null && typeof value === "object") {
|
|
5859
|
+
for (const [key, nested] of Object.entries(value)) walk(nested, path.length > 0 ? `${path}.${key}` : key);
|
|
5860
|
+
return;
|
|
5861
|
+
}
|
|
5862
|
+
out.push({
|
|
5863
|
+
path,
|
|
5864
|
+
value: canonical(value)
|
|
6019
5865
|
});
|
|
5866
|
+
};
|
|
5867
|
+
for (const [key, value] of Object.entries(row)) {
|
|
5868
|
+
if (key === "id") continue;
|
|
5869
|
+
if (key === "config" && isGroupRow(row)) continue;
|
|
5870
|
+
walk(value, base.length > 0 ? `${base}.${key}` : key);
|
|
6020
5871
|
}
|
|
6021
|
-
return
|
|
5872
|
+
return out;
|
|
6022
5873
|
}
|
|
5874
|
+
/** 规范化的深比较(两侧都是 YAML/JSON 值,无函数与原型差异)。 */
|
|
5875
|
+
function sameValue(a, b) {
|
|
5876
|
+
return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
5877
|
+
}
|
|
5878
|
+
//#endregion
|
|
5879
|
+
//#region src/presets/conflicts.ts
|
|
6023
5880
|
/**
|
|
6024
|
-
*
|
|
5881
|
+
* 冲突不变式:一个 preset「接上了本插件的工具」当且仅当官方那 4 个工具行
|
|
5882
|
+
* 处于**不存在**或 **`disabled: true`** 状态。
|
|
6025
5883
|
*
|
|
6026
|
-
* `
|
|
6027
|
-
*
|
|
6028
|
-
*
|
|
6029
|
-
*
|
|
6030
|
-
*
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
for (const insert of inserts.sort((a, b) => b.at - a.at)) {
|
|
6058
|
-
if (insert.at === lines.length && lines.length > 0 && lines[lines.length - 1].eol === "") {
|
|
6059
|
-
lines[lines.length - 1] = {
|
|
6060
|
-
text: lines[lines.length - 1].text,
|
|
6061
|
-
eol
|
|
6062
|
-
};
|
|
6063
|
-
lines.push({
|
|
6064
|
-
text: `${insert.indent}disabled: true`,
|
|
6065
|
-
eol: ""
|
|
6066
|
-
});
|
|
6067
|
-
} else lines.splice(insert.at, 0, {
|
|
6068
|
-
text: `${insert.indent}disabled: true`,
|
|
6069
|
-
eol
|
|
6070
|
-
});
|
|
6071
|
-
changes.push({
|
|
6072
|
-
id: insert.id,
|
|
6073
|
-
action: "disabled"
|
|
6074
|
-
});
|
|
6075
|
-
}
|
|
6076
|
-
const ordered = ids.map((id) => changes.find((change) => change.id === id)).filter((change) => change !== void 0);
|
|
6077
|
-
return {
|
|
6078
|
-
text: joinLines(lines),
|
|
6079
|
-
changes: ordered
|
|
6080
|
-
};
|
|
5884
|
+
* 原因(这套设计的地基,别丢):`@xiaoso/dsh-tool-plus` 作为包在**宿主面**
|
|
5885
|
+
* 加载时,它自己的 profile 补丁已经禁用了这 4 行并插入**一行全局** `tool-plus`;
|
|
5886
|
+
* 注册表按 scope 分层、就近优先,所以宿主面那一份已经服务所有会话。若某个
|
|
5887
|
+
* preset 又在 **agent 面**挂一份同名工具行,就会用**第二个 `apply()` 影子实例**
|
|
5888
|
+
* 盖掉健康的宿主实例,而那个实例的 settings 注入永不触发(cfg 退化成
|
|
5889
|
+
* replace-mode 读取 —— 2026-08-25 hashline 事故的根因)。
|
|
5890
|
+
*
|
|
5891
|
+
* 与旧文本实现的唯一区别是**载体**:以前逐行读 `agent.cordis.yml` 的文本,
|
|
5892
|
+
* 现在直接看宿主交给我们的行对象。判定规则一字不改:
|
|
5893
|
+
* - 只认**顶层**行;group 里嵌套的同名行不碰;
|
|
5894
|
+
* - `disabled` 严格等于布尔 `true` 才算已处理 —— 缺席、`false`、字符串、
|
|
5895
|
+
* `!!js` 表达式统统算冲突(`!!js` 在挂载时才求值,我们要的是无条件关闭,
|
|
5896
|
+
* 所以和旧实现一样整值翻成 `true`);
|
|
5897
|
+
* - 输出恒为新数组,输入不变;结果与输入逐值相同时 `changes` 为空(调用方以
|
|
5898
|
+
* 此判定"不写盘")。
|
|
5899
|
+
* @module @xiaoso/dsh-tool-plus/presets/conflicts
|
|
5900
|
+
*/
|
|
5901
|
+
/** 官方 standard / ptc / cordis 会挂的 4 个工具行 id。 */
|
|
5902
|
+
const PRESET_CONFLICT_IDS = [
|
|
5903
|
+
"tool-bash",
|
|
5904
|
+
"tool-pwsh",
|
|
5905
|
+
"tool-fs",
|
|
5906
|
+
"tool-fs-search"
|
|
5907
|
+
];
|
|
5908
|
+
/** 该行是否已被无条件关闭(只有布尔 `true` 算)。 */
|
|
5909
|
+
function disabled(row) {
|
|
5910
|
+
return row.disabled === true;
|
|
5911
|
+
}
|
|
5912
|
+
/** 顶层行里按 id 找第一行(只看顶层,与不变式一致)。 */
|
|
5913
|
+
function topRow(rows, id) {
|
|
5914
|
+
return rows.find((row) => rowId(row) === id);
|
|
6081
5915
|
}
|
|
6082
5916
|
/**
|
|
6083
|
-
*
|
|
6084
|
-
* @param
|
|
6085
|
-
* @param conflictIds -
|
|
6086
|
-
* @returns
|
|
5917
|
+
* 只读分析一份 `plugins` 值。
|
|
5918
|
+
* @param plugins - 声明的子插件列表(来自宿主配置,形状未知)。
|
|
5919
|
+
* @param conflictIds - 冲突行清单,默认 {@link PRESET_CONFLICT_IDS}。
|
|
5920
|
+
* @returns 冲突清单、是否干净、形状是否可识别。
|
|
6087
5921
|
*/
|
|
6088
|
-
function
|
|
6089
|
-
|
|
6090
|
-
const shape = detectShape(lines);
|
|
6091
|
-
if (shape === "unrecognized") return {
|
|
5922
|
+
function analyzePlugins(plugins, conflictIds = PRESET_CONFLICT_IDS) {
|
|
5923
|
+
if (!isRowList(plugins)) return {
|
|
6092
5924
|
conflicts: [],
|
|
6093
5925
|
clean: false,
|
|
6094
|
-
shape
|
|
6095
|
-
};
|
|
6096
|
-
const
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
if (row === void 0) return false;
|
|
6100
|
-
const disabledAt = findDisabledLine(lines, row);
|
|
6101
|
-
return disabledAt === void 0 || disabledValue(lines[disabledAt].text) !== "true";
|
|
5926
|
+
shape: "unrecognized"
|
|
5927
|
+
};
|
|
5928
|
+
const conflicts = conflictIds.filter((id) => {
|
|
5929
|
+
const row = topRow(plugins, id);
|
|
5930
|
+
return row !== void 0 && !disabled(row);
|
|
6102
5931
|
});
|
|
6103
5932
|
return {
|
|
6104
5933
|
conflicts,
|
|
6105
5934
|
clean: conflicts.length === 0,
|
|
6106
|
-
shape
|
|
5935
|
+
shape: "ok"
|
|
6107
5936
|
};
|
|
6108
5937
|
}
|
|
6109
|
-
/** 行内 `key: value`(顶层键;嵌套子键缩进更大,不在这里取)。 */
|
|
6110
|
-
const ROW_KEY = /^[ \t]*([^#\s:][^:]*):[ \t]*(.*)$/;
|
|
6111
|
-
/** 去掉行尾注释与引号,得到可比的值。 */
|
|
6112
|
-
function leafValue(text) {
|
|
6113
|
-
return stripQuotes(text.replace(/[ \t]+#.*$/, "").trim());
|
|
6114
|
-
}
|
|
6115
5938
|
/**
|
|
6116
|
-
*
|
|
6117
|
-
*
|
|
5939
|
+
* 把那 4 个仍挂着的冲突行翻成 `disabled: true`(不改别的行、不删行)。
|
|
5940
|
+
* @param plugins - 合法顶层行列表。
|
|
5941
|
+
* @param conflictIds - 冲突行清单,默认 {@link PRESET_CONFLICT_IDS}。
|
|
5942
|
+
* @returns 新列表与实际改动清单(无改动时 `changes` 为空且列表为原引用)。
|
|
6118
5943
|
*/
|
|
6119
|
-
function
|
|
6120
|
-
const
|
|
6121
|
-
|
|
6122
|
-
const
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
while (stack.length > 0 && stack[stack.length - 1].indent >= indent) stack.pop();
|
|
6129
|
-
const prefix = stack.length === 0 ? "" : stack[stack.length - 1].path;
|
|
6130
|
-
if (trimmed === "-" || trimmed.startsWith("- ")) {
|
|
6131
|
-
const index = counters.get(prefix) ?? 0;
|
|
6132
|
-
counters.set(prefix, index + 1);
|
|
6133
|
-
const itemPath = prefix === "" ? `[${index}]` : `${prefix}[${index}]`;
|
|
6134
|
-
stack.push({
|
|
6135
|
-
indent,
|
|
6136
|
-
path: itemPath
|
|
6137
|
-
});
|
|
6138
|
-
const body = trimmed.replace(/^-[ \t]*/, "");
|
|
6139
|
-
const match = ROW_KEY.exec(` ${body}`);
|
|
6140
|
-
if (match === null) {
|
|
6141
|
-
out.push({
|
|
6142
|
-
path: itemPath,
|
|
6143
|
-
value: leafValue(body)
|
|
6144
|
-
});
|
|
6145
|
-
continue;
|
|
6146
|
-
}
|
|
6147
|
-
const value = leafValue(match[2]);
|
|
6148
|
-
if (value !== "") out.push({
|
|
6149
|
-
path: `${itemPath}.${match[1].trim()}`,
|
|
6150
|
-
value
|
|
6151
|
-
});
|
|
6152
|
-
continue;
|
|
6153
|
-
}
|
|
6154
|
-
const match = ROW_KEY.exec(trimmed);
|
|
6155
|
-
if (match === null) continue;
|
|
6156
|
-
const key = match[1].trim();
|
|
6157
|
-
const path = prefix === "" ? key : `${prefix}.${key}`;
|
|
6158
|
-
const value = leafValue(match[2]);
|
|
6159
|
-
if (value === "") stack.push({
|
|
6160
|
-
indent,
|
|
6161
|
-
path
|
|
6162
|
-
});
|
|
6163
|
-
else out.push({
|
|
6164
|
-
path,
|
|
6165
|
-
value
|
|
5944
|
+
function disableConflicts(plugins, conflictIds = PRESET_CONFLICT_IDS) {
|
|
5945
|
+
const changes = [];
|
|
5946
|
+
let touched = false;
|
|
5947
|
+
const next = plugins.map((row) => {
|
|
5948
|
+
const id = rowId(row);
|
|
5949
|
+
if (id === void 0 || !conflictIds.includes(id) || disabled(row)) return row;
|
|
5950
|
+
changes.push({
|
|
5951
|
+
id,
|
|
5952
|
+
action: Object.hasOwn(row, "disabled") ? "flipped" : "disabled"
|
|
6166
5953
|
});
|
|
6167
|
-
|
|
6168
|
-
return out;
|
|
6169
|
-
}
|
|
6170
|
-
/**
|
|
6171
|
-
* 只读读出顶层行的键值对与展开叶子。形状不可识别时返回 undefined(不猜、不修)。
|
|
6172
|
-
*
|
|
6173
|
-
* 与 {@link analyzePresetComposition} 共用同一套扫描器(`splitLines` /
|
|
6174
|
-
* `detectShape` / `scanRows`):解析只有一份,差异视图与改写视图永远同源。
|
|
6175
|
-
* @param text - `agent.cordis.yml` 原文。
|
|
6176
|
-
* @returns 逐行视图,或形状不可识别时的 undefined。
|
|
6177
|
-
*/
|
|
6178
|
-
function readPresetRows(text) {
|
|
6179
|
-
const lines = splitLines(text);
|
|
6180
|
-
if (detectShape(lines) !== "ok") return void 0;
|
|
6181
|
-
return scanRows(lines).map((row) => {
|
|
6182
|
-
const keys = [];
|
|
6183
|
-
if (row.keyIndent !== void 0) for (let i = row.start; i < row.end; i += 1) {
|
|
6184
|
-
const line = lines[i].text;
|
|
6185
|
-
if (COMMENT_LINE.test(line) || BLANK_LINE.test(line)) continue;
|
|
6186
|
-
if (indentWidth(line) !== row.keyIndent) continue;
|
|
6187
|
-
const match = ROW_KEY.exec(line);
|
|
6188
|
-
if (match === null) continue;
|
|
6189
|
-
keys.push({
|
|
6190
|
-
key: match[1].trim(),
|
|
6191
|
-
value: leafValue(match[2])
|
|
6192
|
-
});
|
|
6193
|
-
}
|
|
5954
|
+
touched = true;
|
|
6194
5955
|
return {
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
leaves: walkRowLeaves(lines, row)
|
|
5956
|
+
...row,
|
|
5957
|
+
disabled: true
|
|
6198
5958
|
};
|
|
6199
5959
|
});
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
//#region src/presets/install.ts
|
|
6203
|
-
/**
|
|
6204
|
-
* preset 安装 fs 层:启动补缺、升级(只改冲突行)、重置(整份覆盖)。
|
|
6205
|
-
*
|
|
6206
|
-
* 三条纪律:
|
|
6207
|
-
* - **启动唯一自动动作**是"目录不存在才整份写入";目录已存在则一个字节都不碰
|
|
6208
|
-
* (不检查、不补文件、不更新),失败绝不抛出、绝不阻塞启动。
|
|
6209
|
-
* - **内容相同就不写盘**:preset 代际以 `agent.cordis.yml` 的 mtime 为键,
|
|
6210
|
-
* 白写一次就白造一代 + 白起 watcher。判定一律用"写前先读回比较字节"。
|
|
6211
|
-
* - **写盘一律原子**:同目录临时文件 + `rename`;替换前若同名备份不存在,
|
|
6212
|
-
* 先落一份 `agent.cordis.yml.bak-<插件版本>`(改写前的内容,同名不覆盖)。
|
|
6213
|
-
* @module @xiaoso/dsh-tool-plus/presets/install
|
|
6214
|
-
*/
|
|
6215
|
-
/**
|
|
6216
|
-
* 读取包内模板目录的全部常规文件("整份写入"的来源)。
|
|
6217
|
-
* 模板目录不存在、或里面没有 `agent.cordis.yml` → 抛错,由调用方收进 reason。
|
|
6218
|
-
*/
|
|
6219
|
-
function readTemplateFiles(id, deps) {
|
|
6220
|
-
const dir = templateDir(id, deps);
|
|
6221
|
-
const names = fs.readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isFile()).map((entry) => entry.name);
|
|
6222
|
-
if (!names.includes("agent.cordis.yml")) throw new Error(`preset template is incomplete: ${path$1.join(dir, COMPOSITION_FILE_NAME)} not found`);
|
|
6223
|
-
return names.map((name) => ({
|
|
6224
|
-
name,
|
|
6225
|
-
content: fs.readFileSync(path$1.join(dir, name))
|
|
6226
|
-
}));
|
|
6227
|
-
}
|
|
6228
|
-
/** 把异常收成一句人话(启动路径永不抛)。 */
|
|
6229
|
-
function reasonOf(error) {
|
|
6230
|
-
return error instanceof Error ? error.message : String(error);
|
|
6231
|
-
}
|
|
6232
|
-
/** 只读文本文件;读不到返回 undefined(不抛)。 */
|
|
6233
|
-
function readTextFile$1(file) {
|
|
6234
|
-
try {
|
|
6235
|
-
return fs.readFileSync(file, "utf8");
|
|
6236
|
-
} catch {
|
|
6237
|
-
return;
|
|
6238
|
-
}
|
|
6239
|
-
}
|
|
6240
|
-
/** 只读原始字节;读不到返回 undefined(不抛)。 */
|
|
6241
|
-
function readFile(file) {
|
|
6242
|
-
try {
|
|
6243
|
-
return fs.readFileSync(file);
|
|
6244
|
-
} catch {
|
|
6245
|
-
return;
|
|
6246
|
-
}
|
|
6247
|
-
}
|
|
6248
|
-
/**
|
|
6249
|
-
* 启动补缺:`<dshHome>/.agent-presets/<id>` 目录不存在时,从包内模板整份写入
|
|
6250
|
-
* (`mkdir -p` + 模板目录里的每个文件)。
|
|
6251
|
-
*
|
|
6252
|
-
* 目录已存在 → 一律不碰(含"目录存在但缺文件"的情形,那种只在设置页状态里
|
|
6253
|
-
* 显示)。整个流程每个 id 单独 try/catch,**永不抛**。
|
|
6254
|
-
* @param deps - 路径注入(fs 层测试用)。
|
|
6255
|
-
* @returns 新建成功的 id 列表与失败原因列表(顺序均为 {@link DEFAULT_PRESET_IDS})。
|
|
6256
|
-
*/
|
|
6257
|
-
function ensureDefaultPresets(deps = {}) {
|
|
6258
|
-
const created = [];
|
|
6259
|
-
const failed = [];
|
|
6260
|
-
for (const id of DEFAULT_PRESET_IDS) try {
|
|
6261
|
-
const dir = userPresetDir(id, deps);
|
|
6262
|
-
if (fs.existsSync(dir)) continue;
|
|
6263
|
-
const files = readTemplateFiles(id, deps);
|
|
6264
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
6265
|
-
for (const file of files) fs.writeFileSync(path$1.join(dir, file.name), file.content);
|
|
6266
|
-
created.push(id);
|
|
6267
|
-
} catch (error) {
|
|
6268
|
-
failed.push({
|
|
6269
|
-
id,
|
|
6270
|
-
reason: reasonOf(error)
|
|
6271
|
-
});
|
|
6272
|
-
}
|
|
6273
|
-
return {
|
|
6274
|
-
created,
|
|
6275
|
-
failed
|
|
6276
|
-
};
|
|
6277
|
-
}
|
|
6278
|
-
/** 本插件的版本号(备份名后缀):读包根 package.json,读不到就退化成 `unknown`。 */
|
|
6279
|
-
function pluginVersion(deps) {
|
|
6280
|
-
try {
|
|
6281
|
-
const raw = JSON.parse(fs.readFileSync(path$1.join(packageRootDir(deps), "package.json"), "utf8"));
|
|
6282
|
-
if (typeof raw.version === "string" && raw.version.length > 0) return raw.version;
|
|
6283
|
-
} catch {}
|
|
6284
|
-
return "unknown";
|
|
6285
|
-
}
|
|
6286
|
-
/** 备份路径:与被替换的文件同目录,`<文件>.bak-<插件版本>`。 */
|
|
6287
|
-
function backupFileFor(file, deps) {
|
|
6288
|
-
return `${file}.bak-${pluginVersion(deps)}`;
|
|
6289
|
-
}
|
|
6290
|
-
/**
|
|
6291
|
-
* 替换前落一份"改写前内容"的备份;同名备份已存在则不覆盖(返回既有路径)。
|
|
6292
|
-
* 备份是纯新增文件,不参与写入链的原子性。
|
|
6293
|
-
*/
|
|
6294
|
-
function writeBackup(file, content, deps) {
|
|
6295
|
-
const backup = backupFileFor(file, deps);
|
|
6296
|
-
if (!fs.existsSync(backup)) fs.writeFileSync(backup, content);
|
|
6297
|
-
return backup;
|
|
6298
|
-
}
|
|
6299
|
-
/** 同目录临时文件 + `rename` 的原子替换;失败时清掉临时文件再把错抛出去。 */
|
|
6300
|
-
function writeFileAtomic(file, content) {
|
|
6301
|
-
const temp = path$1.join(path$1.dirname(file), `.${path$1.basename(file)}.${process.pid}.${Date.now().toString(36)}.tmp`);
|
|
6302
|
-
fs.writeFileSync(temp, content);
|
|
6303
|
-
try {
|
|
6304
|
-
fs.renameSync(temp, file);
|
|
6305
|
-
} catch (error) {
|
|
6306
|
-
try {
|
|
6307
|
-
fs.unlinkSync(temp);
|
|
6308
|
-
} catch {}
|
|
6309
|
-
throw error;
|
|
6310
|
-
}
|
|
6311
|
-
}
|
|
6312
|
-
/**
|
|
6313
|
-
* 升级:只改冲突行(追加 `disabled: true` 或把既有值翻成 `true`)。
|
|
6314
|
-
* 形状无法识别 → 不写;内容逐字节相同 → 不写盘、不备份。
|
|
6315
|
-
*/
|
|
6316
|
-
function upgrade(id, deps) {
|
|
6317
|
-
const file = path$1.join(userPresetDir(id, deps), COMPOSITION_FILE_NAME);
|
|
6318
|
-
const original = readTextFile$1(file);
|
|
6319
|
-
if (original === void 0) return {
|
|
6320
|
-
ok: false,
|
|
6321
|
-
changed: false,
|
|
6322
|
-
reason: `preset is not installed: ${file} cannot be read`,
|
|
6323
|
-
changes: []
|
|
6324
|
-
};
|
|
6325
|
-
if (analyzePresetComposition(original).shape === "unrecognized") return {
|
|
6326
|
-
ok: false,
|
|
6327
|
-
changed: false,
|
|
6328
|
-
reason: `composition shape is unrecognized, nothing was written: ${file}`,
|
|
5960
|
+
if (!touched) return {
|
|
5961
|
+
plugins,
|
|
6329
5962
|
changes: []
|
|
6330
5963
|
};
|
|
6331
|
-
|
|
6332
|
-
if (text === original) return {
|
|
6333
|
-
ok: true,
|
|
6334
|
-
changed: false,
|
|
6335
|
-
changes: []
|
|
6336
|
-
};
|
|
6337
|
-
const backupPath = writeBackup(file, original, deps);
|
|
6338
|
-
writeFileAtomic(file, text);
|
|
5964
|
+
changes.sort((a, b) => conflictIds.indexOf(a.id) - conflictIds.indexOf(b.id));
|
|
6339
5965
|
return {
|
|
6340
|
-
|
|
6341
|
-
changed: true,
|
|
6342
|
-
backupPath,
|
|
5966
|
+
plugins: next,
|
|
6343
5967
|
changes
|
|
6344
5968
|
};
|
|
6345
5969
|
}
|
|
6346
|
-
/**
|
|
6347
|
-
* 重置:用**指定的**模板整份覆盖该预设目录(组合文件 + 元数据等每个模板文件)。
|
|
6348
|
-
*
|
|
6349
|
-
* 模板来源按用户所选:`templateId` 给了就用它(可以把 A 预设对齐到 B 模板),
|
|
6350
|
-
* 没给才退回"同名模板"(历史行为)。内容相同则一个字节都不写;组合文件被改写
|
|
6351
|
-
* 前先落备份。`changes` 恒为空:重置不是"逐行改写",行级摘要不在它的契约里。
|
|
6352
|
-
*/
|
|
6353
|
-
function reset(id, deps, templateId) {
|
|
6354
|
-
const source = templateId ?? id;
|
|
6355
|
-
const sourceComposition = path$1.join(templateDir(source, deps), COMPOSITION_FILE_NAME);
|
|
6356
|
-
if (!fs.existsSync(sourceComposition)) return {
|
|
6357
|
-
ok: false,
|
|
6358
|
-
changed: false,
|
|
6359
|
-
reason: `template is missing: ${sourceComposition}`,
|
|
6360
|
-
changes: []
|
|
6361
|
-
};
|
|
6362
|
-
const dir = userPresetDir(id, deps);
|
|
6363
|
-
const files = readTemplateFiles(source, deps);
|
|
6364
|
-
path$1.join(dir, COMPOSITION_FILE_NAME);
|
|
6365
|
-
let changed = false;
|
|
6366
|
-
let backupPath;
|
|
6367
|
-
for (const file of files) {
|
|
6368
|
-
const target = path$1.join(dir, file.name);
|
|
6369
|
-
const current = readFile(target);
|
|
6370
|
-
if (current !== void 0 && current.equals(file.content)) continue;
|
|
6371
|
-
if (current !== void 0 && file.name === "agent.cordis.yml") backupPath = writeBackup(target, current, deps);
|
|
6372
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
6373
|
-
writeFileAtomic(target, file.content);
|
|
6374
|
-
changed = true;
|
|
6375
|
-
}
|
|
6376
|
-
return {
|
|
6377
|
-
ok: true,
|
|
6378
|
-
changed,
|
|
6379
|
-
backupPath,
|
|
6380
|
-
changes: []
|
|
6381
|
-
};
|
|
6382
|
-
}
|
|
6383
|
-
/**
|
|
6384
|
-
* 用户在设置页对某个预设执行的动作。
|
|
6385
|
-
*
|
|
6386
|
-
* - `upgrade`:只改官方冲突行(面板上叫「最小更新」);
|
|
6387
|
-
* - `reset`:用**所选模板**整份覆盖该预设(先备份)。模板由 `templateId` 指定,
|
|
6388
|
-
* 缺省才是同名模板 —— 面板总是把用户挑的那份模板传进来。
|
|
6389
|
-
*
|
|
6390
|
-
* 永不抛:任何异常收进 `ok: false` + `reason`。
|
|
6391
|
-
* @param id - 预设 id(目录名)。
|
|
6392
|
-
* @param action - `upgrade` 或 `reset`。
|
|
6393
|
-
* @param deps - 路径注入(fs 层测试用)。
|
|
6394
|
-
* @param templateId - `reset` 的模板来源(默认同名模板)。
|
|
6395
|
-
* @returns 结果(`changed: false` = 一个字节都没写)。
|
|
6396
|
-
*/
|
|
6397
|
-
function applyPresetAction(id, action, deps = {}, templateId) {
|
|
6398
|
-
try {
|
|
6399
|
-
return action === "reset" ? reset(id, deps, templateId) : upgrade(id, deps);
|
|
6400
|
-
} catch (error) {
|
|
6401
|
-
return {
|
|
6402
|
-
ok: false,
|
|
6403
|
-
changed: false,
|
|
6404
|
-
reason: reasonOf(error),
|
|
6405
|
-
changes: []
|
|
6406
|
-
};
|
|
6407
|
-
}
|
|
6408
|
-
}
|
|
6409
5970
|
//#endregion
|
|
6410
5971
|
//#region src/presets/delta.ts
|
|
6411
5972
|
/**
|
|
6412
|
-
*
|
|
5973
|
+
* 差异清单:只读地说清"这份 preset 与另一份(通常是随包声明)差在哪"。
|
|
6413
5974
|
*
|
|
6414
5975
|
* 为什么需要它:`templateDiffers: boolean` 只能说"不一样",而面板上紧挨着的
|
|
6415
|
-
* 是"
|
|
6416
|
-
*
|
|
6417
|
-
*
|
|
5976
|
+
* 是"冲突行都已禁用、无需调整"。两句都对,读起来却打架 —— 前者说的是
|
|
5977
|
+
* **内容被改过**,后者说的是**我们自己那 4 个冲突行**。把差异逐条列出来,
|
|
5978
|
+
* 用户才不用猜"到底哪里不同、对齐会不会动它"。
|
|
6418
5979
|
*
|
|
6419
|
-
*
|
|
6420
|
-
*
|
|
6421
|
-
*
|
|
6422
|
-
*
|
|
6423
|
-
*
|
|
5980
|
+
* 契约(自旧文本实现原样继承,语义未变):
|
|
5981
|
+
* - 比**展开到叶子的点号路径**(`config.thresholdRatio`、`config.models[0].id`)——
|
|
5982
|
+
* 真实漂移常藏在嵌套键里,只比顶层键会得出"没有差异"的错误结论;
|
|
5983
|
+
* - 冲突行的 `disabled` 由 `conflicts` 负责,不在差异里重复报;
|
|
5984
|
+
* - 形状不可识别 → 空清单,绝不猜;
|
|
5985
|
+
* - 顺序 = 你这份的行序在前(含 group 内子行),对方独有的行补在后;
|
|
5986
|
+
* - `total` 是全量,`items` 可被 `limit` 截断(面板据此说"等 N 项")。
|
|
6424
5987
|
* @module @xiaoso/dsh-tool-plus/presets/delta
|
|
6425
5988
|
*/
|
|
6426
|
-
/**
|
|
5989
|
+
/** 默认最多列几条(再多就该去看配置本身了)。 */
|
|
6427
5990
|
const DEFAULT_LIMIT$1 = 6;
|
|
6428
|
-
/**
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
5991
|
+
/** 空结果(任一侧形状不可识别时)。 */
|
|
5992
|
+
const EMPTY = {
|
|
5993
|
+
items: [],
|
|
5994
|
+
total: 0,
|
|
5995
|
+
yoursCount: 0,
|
|
5996
|
+
behindCount: 0
|
|
5997
|
+
};
|
|
5998
|
+
/** 某个叶子的 `disabled` 是不是"我们自己的冲突行开关"——那属于 conflicts 的话题。 */
|
|
5999
|
+
function isConflictSwitch(rowPath, leafPath, conflictIds) {
|
|
6000
|
+
return leafPath === "disabled" && !rowPath.includes("/") && conflictIds.includes(rowPath);
|
|
6435
6001
|
}
|
|
6436
6002
|
/**
|
|
6437
|
-
*
|
|
6438
|
-
* @param
|
|
6439
|
-
* @param
|
|
6003
|
+
* 比较两份 `config.plugins`,列出差异(只读,不改任何值)。
|
|
6004
|
+
* @param yours - 你这一侧的 plugins(形状未知)。
|
|
6005
|
+
* @param theirs - 对方的 plugins(形状未知)。
|
|
6440
6006
|
* @param options - `conflictIds`(默认 {@link PRESET_CONFLICT_IDS})与 `limit`(默认 6)。
|
|
6441
|
-
* @returns
|
|
6442
|
-
*/
|
|
6443
|
-
function presetDelta(
|
|
6444
|
-
|
|
6445
|
-
const theirs = readPresetRows(templateText);
|
|
6446
|
-
if (yours === void 0 || theirs === void 0) return {
|
|
6447
|
-
items: [],
|
|
6448
|
-
total: 0,
|
|
6449
|
-
yoursCount: 0,
|
|
6450
|
-
behindCount: 0
|
|
6451
|
-
};
|
|
6007
|
+
* @returns 差异清单;两侧任一形状不可识别时为空清单。
|
|
6008
|
+
*/
|
|
6009
|
+
function presetDelta(yours, theirs, options = {}) {
|
|
6010
|
+
if (!isRowList(yours) || !isRowList(theirs)) return EMPTY;
|
|
6452
6011
|
const conflictIds = options.conflictIds ?? PRESET_CONFLICT_IDS;
|
|
6453
6012
|
const limit = options.limit ?? DEFAULT_LIMIT$1;
|
|
6454
|
-
const
|
|
6455
|
-
const
|
|
6013
|
+
const mine = flattenRows(yours);
|
|
6014
|
+
const other = flattenRows(theirs);
|
|
6456
6015
|
const items = [];
|
|
6457
|
-
|
|
6458
|
-
|
|
6016
|
+
const leavesOf = (row) => new Map(rowLeaves(row).map((leaf) => [leaf.path, leaf.value]));
|
|
6017
|
+
for (const [path, row] of mine) {
|
|
6018
|
+
const counterpart = other.get(path);
|
|
6459
6019
|
if (counterpart === void 0) {
|
|
6460
6020
|
items.push({
|
|
6461
6021
|
kind: "row-only-yours",
|
|
6462
|
-
row:
|
|
6022
|
+
row: path,
|
|
6463
6023
|
path: ""
|
|
6464
6024
|
});
|
|
6465
6025
|
continue;
|
|
6466
6026
|
}
|
|
6467
|
-
const
|
|
6468
|
-
const
|
|
6469
|
-
for (const [
|
|
6470
|
-
if (isConflictSwitch(
|
|
6471
|
-
if (!
|
|
6027
|
+
const a = leavesOf(row);
|
|
6028
|
+
const b = leavesOf(counterpart);
|
|
6029
|
+
for (const [leaf, value] of a) {
|
|
6030
|
+
if (isConflictSwitch(path, leaf, conflictIds)) continue;
|
|
6031
|
+
if (!b.has(leaf)) {
|
|
6472
6032
|
items.push({
|
|
6473
6033
|
kind: "only-yours",
|
|
6474
|
-
row:
|
|
6475
|
-
path,
|
|
6034
|
+
row: path,
|
|
6035
|
+
path: leaf,
|
|
6476
6036
|
yours: value
|
|
6477
6037
|
});
|
|
6478
6038
|
continue;
|
|
6479
6039
|
}
|
|
6480
|
-
const
|
|
6481
|
-
if (
|
|
6040
|
+
const theirsValue = b.get(leaf) ?? "";
|
|
6041
|
+
if (theirsValue !== value) items.push({
|
|
6482
6042
|
kind: "changed",
|
|
6483
|
-
row:
|
|
6484
|
-
path,
|
|
6043
|
+
row: path,
|
|
6044
|
+
path: leaf,
|
|
6485
6045
|
yours: value,
|
|
6486
|
-
template:
|
|
6046
|
+
template: theirsValue
|
|
6487
6047
|
});
|
|
6488
6048
|
}
|
|
6489
|
-
for (const [
|
|
6490
|
-
if (isConflictSwitch(
|
|
6491
|
-
if (!
|
|
6049
|
+
for (const [leaf, value] of b) {
|
|
6050
|
+
if (isConflictSwitch(path, leaf, conflictIds)) continue;
|
|
6051
|
+
if (!a.has(leaf)) items.push({
|
|
6492
6052
|
kind: "only-template",
|
|
6493
|
-
row:
|
|
6494
|
-
path,
|
|
6053
|
+
row: path,
|
|
6054
|
+
path: leaf,
|
|
6495
6055
|
template: value
|
|
6496
6056
|
});
|
|
6497
6057
|
}
|
|
6498
6058
|
}
|
|
6499
|
-
for (const
|
|
6059
|
+
for (const path of other.keys()) if (!mine.has(path)) items.push({
|
|
6500
6060
|
kind: "row-only-template",
|
|
6501
|
-
row:
|
|
6061
|
+
row: path,
|
|
6502
6062
|
path: ""
|
|
6503
6063
|
});
|
|
6504
6064
|
const yoursCount = items.filter((item) => isYoursDelta(item.kind)).length;
|
|
@@ -6510,99 +6070,19 @@ function presetDelta(userText, templateText, options = {}) {
|
|
|
6510
6070
|
};
|
|
6511
6071
|
}
|
|
6512
6072
|
//#endregion
|
|
6513
|
-
//#region src/presets/status.ts
|
|
6514
|
-
/**
|
|
6515
|
-
* preset 状态探测:把 roster 变成设置页要的清单。同步、只读。
|
|
6516
|
-
*
|
|
6517
|
-
* roster 由调用方(RPC 层)从 `ctx.agentPresets.list()` 映射后传入——本模块
|
|
6518
|
-
* 不依赖 cordis 类型,只吃一个最小结构,因此可以纯 fs 单测。三种来源按设计
|
|
6519
|
-
* 文档 §4.2 区分:
|
|
6520
|
-
* - `ours`:本插件随包的两个预设(id 在 {@link DEFAULT_PRESET_IDS} 里),
|
|
6521
|
-
* 可"更新"(只改冲突行)与"重置"(整份对齐模板);
|
|
6522
|
-
* - `user`:用户根里的其它预设,只能"升级"(只改冲突行);
|
|
6523
|
-
* - `shipped`:随部署提供的官方预设(`trust: 'system'`),只读展示。
|
|
6524
|
-
*
|
|
6525
|
-
* 判定只读文件、绝不写入:官方随附根与用户根在这里都只是被读。
|
|
6526
|
-
* @module @xiaoso/dsh-tool-plus/presets/status
|
|
6527
|
-
*/
|
|
6528
|
-
/** 读组合文件;绝不抛。ENOENT/ENOTDIR 归为"不存在",其余归为"读不了"。 */
|
|
6529
|
-
function readComposition(file) {
|
|
6530
|
-
try {
|
|
6531
|
-
return {
|
|
6532
|
-
kind: "ok",
|
|
6533
|
-
text: fs.readFileSync(file, "utf8")
|
|
6534
|
-
};
|
|
6535
|
-
} catch (error) {
|
|
6536
|
-
const code = error.code;
|
|
6537
|
-
return code === "ENOENT" || code === "ENOTDIR" ? { kind: "missing" } : { kind: "unreadable" };
|
|
6538
|
-
}
|
|
6539
|
-
}
|
|
6540
|
-
/** 只读文本文件;读不到返回 undefined(不抛)。 */
|
|
6541
|
-
function readTextFile(file) {
|
|
6542
|
-
try {
|
|
6543
|
-
return fs.readFileSync(file, "utf8");
|
|
6544
|
-
} catch {
|
|
6545
|
-
return;
|
|
6546
|
-
}
|
|
6547
|
-
}
|
|
6548
|
-
/**
|
|
6549
|
-
* 列出每个 roster 行的状态(顺序与入参一致)。
|
|
6550
|
-
* @param roster - 调用方映射好的预设清单(`path` = 组合文件路径)。
|
|
6551
|
-
* @param deps - 路径注入(fs 层测试用)。
|
|
6552
|
-
* @returns 逐行的设置页状态。
|
|
6553
|
-
*/
|
|
6554
|
-
function listPresetStatuses(roster, deps = {}) {
|
|
6555
|
-
return roster.map((entry) => {
|
|
6556
|
-
const source = DEFAULT_PRESET_IDS.includes(entry.id) ? "ours" : entry.trust === "system" ? "shipped" : "user";
|
|
6557
|
-
const installed = fs.existsSync(userPresetDir(entry.id, deps));
|
|
6558
|
-
const templateFile = path$1.join(templateDir(entry.id, deps), COMPOSITION_FILE_NAME);
|
|
6559
|
-
const templatePresent = fs.existsSync(templateFile);
|
|
6560
|
-
const read = readComposition(entry.path);
|
|
6561
|
-
const analysis = read.kind === "ok" ? analyzePresetComposition(read.text) : {
|
|
6562
|
-
conflicts: [],
|
|
6563
|
-
clean: false,
|
|
6564
|
-
shape: read.kind === "unreadable" ? "unrecognized" : "ok"
|
|
6565
|
-
};
|
|
6566
|
-
const unrecognized = read.kind === "missing" ? false : analysis.shape === "unrecognized";
|
|
6567
|
-
const templateText = installed && templatePresent ? readTextFile(templateFile) : void 0;
|
|
6568
|
-
const templateDiffers = templateText !== void 0 && read.kind === "ok" && templateText !== read.text;
|
|
6569
|
-
const templateDelta = templateDiffers && templateText !== void 0 && read.kind === "ok" ? presetDelta(read.text, templateText) : void 0;
|
|
6570
|
-
const delta = templateDelta !== void 0 && templateDelta.total > 0 ? templateDelta : void 0;
|
|
6571
|
-
return {
|
|
6572
|
-
id: entry.id,
|
|
6573
|
-
name: entry.name,
|
|
6574
|
-
description: entry.description,
|
|
6575
|
-
source,
|
|
6576
|
-
path: entry.path,
|
|
6577
|
-
conflicts: analysis.conflicts,
|
|
6578
|
-
clean: analysis.clean,
|
|
6579
|
-
unrecognized,
|
|
6580
|
-
broken: entry.broken,
|
|
6581
|
-
installed,
|
|
6582
|
-
templatePresent,
|
|
6583
|
-
templateDiffers,
|
|
6584
|
-
templateDelta: delta
|
|
6585
|
-
};
|
|
6586
|
-
});
|
|
6587
|
-
}
|
|
6588
|
-
//#endregion
|
|
6589
6073
|
//#region src/presets/compare.ts
|
|
6590
6074
|
/**
|
|
6591
|
-
* 预设比较:把"
|
|
6075
|
+
* 预设比较:把"某份 preset 生效的 plugins"和"某个随包声明的 plugins"放在一起看。
|
|
6592
6076
|
*
|
|
6593
|
-
*
|
|
6594
|
-
*
|
|
6595
|
-
*
|
|
6596
|
-
*
|
|
6597
|
-
*
|
|
6598
|
-
* "
|
|
6077
|
+
* 面板是两块信息、两个状态点:
|
|
6078
|
+
* ①**工具行**:这份 preset 里那 4 个冲突行都禁用了吗(=`conflicts`);
|
|
6079
|
+
* ②**完整比较**:内容上和选中的模板差多少(=`items`,弹窗里逐条展示)。
|
|
6080
|
+
* 只读,绝不写盘。比较一律**不跳过**冲突行的 `disabled` —— 第二个点要的是
|
|
6081
|
+
* "完整的比较",工具开关的不同也必须出现在差异清单里,否则会出现"说内容一致、
|
|
6082
|
+
* 却说工具行没配好"这种自相矛盾的读数。
|
|
6599
6083
|
* @module @xiaoso/dsh-tool-plus/presets/compare
|
|
6600
6084
|
*/
|
|
6601
|
-
/**
|
|
6602
|
-
function templatesRoot(deps) {
|
|
6603
|
-
return path$1.join(packageRootDir(deps), "presets");
|
|
6604
|
-
}
|
|
6605
|
-
/** 读不到/不可识别时的统一结果(面板渲染红点,不抛)。 */
|
|
6085
|
+
/** 任一侧不可比较时的统一结果(面板渲染红点,不抛)。 */
|
|
6606
6086
|
const UNREADABLE_COMPARISON = {
|
|
6607
6087
|
status: "unreadable",
|
|
6608
6088
|
conflicts: [],
|
|
@@ -6611,56 +6091,17 @@ const UNREADABLE_COMPARISON = {
|
|
|
6611
6091
|
behindCount: 0,
|
|
6612
6092
|
items: []
|
|
6613
6093
|
};
|
|
6614
|
-
/** 只读文本文件;读不到返回 undefined(不抛)。 */
|
|
6615
|
-
function readText(file) {
|
|
6616
|
-
try {
|
|
6617
|
-
return fs.readFileSync(file, "utf8");
|
|
6618
|
-
} catch {
|
|
6619
|
-
return;
|
|
6620
|
-
}
|
|
6621
|
-
}
|
|
6622
|
-
/** 模板元数据里的 `name:`(一行,去掉引号);读不到就退回 id。 */
|
|
6623
|
-
function templateName(id, deps) {
|
|
6624
|
-
const text = readText(path$1.join(templatesRoot(deps), id, METADATA_FILE_NAME));
|
|
6625
|
-
if (text === void 0) return void 0;
|
|
6626
|
-
const match = /^name[ \t]*:[ \t]*(.+)$/m.exec(text);
|
|
6627
|
-
if (match === null) return void 0;
|
|
6628
|
-
return match[1].trim().replace(/^['"]|['"]$/g, "");
|
|
6629
|
-
}
|
|
6630
|
-
/**
|
|
6631
|
-
* 列出包里带模板的预设 id(面板"对比模板"下拉的选项)。只读目录,不抛。
|
|
6632
|
-
* @param deps - 路径注入(测试用)。
|
|
6633
|
-
* @returns 按 id 排序的模板清单(读不到目录时为空数组)。
|
|
6634
|
-
*/
|
|
6635
|
-
function listTemplates(deps = {}) {
|
|
6636
|
-
let entries;
|
|
6637
|
-
try {
|
|
6638
|
-
entries = fs.readdirSync(templatesRoot(deps), { withFileTypes: true });
|
|
6639
|
-
} catch {
|
|
6640
|
-
return [];
|
|
6641
|
-
}
|
|
6642
|
-
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((id) => fs.existsSync(path$1.join(templatesRoot(deps), id, COMPOSITION_FILE_NAME))).sort().map((id) => {
|
|
6643
|
-
const name = templateName(id, deps);
|
|
6644
|
-
return name === void 0 ? { id } : {
|
|
6645
|
-
id,
|
|
6646
|
-
name
|
|
6647
|
-
};
|
|
6648
|
-
});
|
|
6649
|
-
}
|
|
6650
|
-
/** 读一份模板的组合文件;不存在返回 undefined。 */
|
|
6651
|
-
function readTemplateComposition(templateId, deps = {}) {
|
|
6652
|
-
return readText(path$1.join(templatesRoot(deps), templateId, COMPOSITION_FILE_NAME));
|
|
6653
|
-
}
|
|
6654
6094
|
/**
|
|
6655
|
-
*
|
|
6656
|
-
* @param
|
|
6657
|
-
* @param
|
|
6095
|
+
* 比较两份 `config.plugins`(纯函数,只读)。
|
|
6096
|
+
* @param presetPlugins - 所选 preset 生效的 plugins。
|
|
6097
|
+
* @param templatePlugins - 所选模板的 plugins(随包声明)。
|
|
6658
6098
|
* @returns 两个状态点与弹窗需要的全部数据。
|
|
6659
6099
|
*/
|
|
6660
|
-
function
|
|
6661
|
-
|
|
6100
|
+
function comparePlugins(presetPlugins, templatePlugins) {
|
|
6101
|
+
if (!isRowList(presetPlugins) || !isRowList(templatePlugins)) return UNREADABLE_COMPARISON;
|
|
6102
|
+
const analysis = analyzePlugins(presetPlugins);
|
|
6662
6103
|
if (analysis.shape === "unrecognized") return UNREADABLE_COMPARISON;
|
|
6663
|
-
const delta = presetDelta(
|
|
6104
|
+
const delta = presetDelta(presetPlugins, templatePlugins, {
|
|
6664
6105
|
conflictIds: [],
|
|
6665
6106
|
limit: Number.MAX_SAFE_INTEGER
|
|
6666
6107
|
});
|
|
@@ -6673,19 +6114,420 @@ function compareCompositions(presetText, templateText) {
|
|
|
6673
6114
|
items: delta.items
|
|
6674
6115
|
};
|
|
6675
6116
|
}
|
|
6117
|
+
//#endregion
|
|
6118
|
+
//#region src/presets/plan.ts
|
|
6119
|
+
/** 生效的 plugins(覆盖优先)。 */
|
|
6120
|
+
function effectivePlugins$1(input) {
|
|
6121
|
+
const plugins = input.effective?.plugins;
|
|
6122
|
+
return isRowList(plugins) ? plugins : void 0;
|
|
6123
|
+
}
|
|
6124
|
+
/**
|
|
6125
|
+
* 判定一个动作该不该写、会改什么。
|
|
6126
|
+
* @param input - 判定所需的事实。
|
|
6127
|
+
* @returns 计划;`noop` 时调用方**不要**调用 `configEditor.edit`。
|
|
6128
|
+
*/
|
|
6129
|
+
function planPresetAction(input) {
|
|
6130
|
+
if (input.action === "revert") {
|
|
6131
|
+
if (!input.customized) return {
|
|
6132
|
+
kind: "noop",
|
|
6133
|
+
reason: "this preset carries no profile override to remove"
|
|
6134
|
+
};
|
|
6135
|
+
return { kind: "revert" };
|
|
6136
|
+
}
|
|
6137
|
+
if (input.action === "align") {
|
|
6138
|
+
if (!input.ours) return {
|
|
6139
|
+
kind: "noop",
|
|
6140
|
+
reason: "only presets this plugin declares can be aligned"
|
|
6141
|
+
};
|
|
6142
|
+
if (input.templateId === void 0) return {
|
|
6143
|
+
kind: "noop",
|
|
6144
|
+
reason: "align needs a template id"
|
|
6145
|
+
};
|
|
6146
|
+
if (!isRowList(input.templatePlugins)) return {
|
|
6147
|
+
kind: "noop",
|
|
6148
|
+
reason: `template ${input.templateId} has no readable plugin list`
|
|
6149
|
+
};
|
|
6150
|
+
if (sameValue(input.templatePlugins, input.inherited?.plugins)) {
|
|
6151
|
+
if (!input.customized) return {
|
|
6152
|
+
kind: "noop",
|
|
6153
|
+
reason: "this preset already matches the bundled declaration"
|
|
6154
|
+
};
|
|
6155
|
+
return { kind: "revert" };
|
|
6156
|
+
}
|
|
6157
|
+
if (sameValue(input.templatePlugins, input.effective?.plugins)) return {
|
|
6158
|
+
kind: "noop",
|
|
6159
|
+
reason: `this preset already matches template ${input.templateId}`
|
|
6160
|
+
};
|
|
6161
|
+
return { kind: "align" };
|
|
6162
|
+
}
|
|
6163
|
+
const plugins = effectivePlugins$1(input);
|
|
6164
|
+
if (plugins === void 0) return {
|
|
6165
|
+
kind: "noop",
|
|
6166
|
+
reason: "the plugin list is not a readable row list, nothing was written"
|
|
6167
|
+
};
|
|
6168
|
+
if (analyzePlugins(plugins).conflicts.length === 0) return {
|
|
6169
|
+
kind: "noop",
|
|
6170
|
+
reason: "no official tool row needs disabling"
|
|
6171
|
+
};
|
|
6172
|
+
const { changes } = disableConflicts(plugins);
|
|
6173
|
+
if (changes.length === 0) return {
|
|
6174
|
+
kind: "noop",
|
|
6175
|
+
reason: "no official tool row needs disabling"
|
|
6176
|
+
};
|
|
6177
|
+
return {
|
|
6178
|
+
kind: "upgrade",
|
|
6179
|
+
changes
|
|
6180
|
+
};
|
|
6181
|
+
}
|
|
6182
|
+
//#endregion
|
|
6183
|
+
//#region src/presets/status.ts
|
|
6184
|
+
/** 生效的 plugins:宿主的生效 config 优先,回落到覆盖层、再回落到随包声明。 */
|
|
6185
|
+
function effectivePlugins(facts) {
|
|
6186
|
+
if (facts.effective?.plugins !== void 0) return facts.effective.plugins;
|
|
6187
|
+
const overridePlugins = facts.override?.plugins;
|
|
6188
|
+
if (overridePlugins !== void 0) return overridePlugins;
|
|
6189
|
+
return facts.inherited?.plugins;
|
|
6190
|
+
}
|
|
6191
|
+
/** 覆盖里是不是真带了内容(`{}` 表示没有覆盖行)。 */
|
|
6192
|
+
function isCustomized(facts) {
|
|
6193
|
+
return facts.override !== void 0 && Object.keys(facts.override).length > 0;
|
|
6194
|
+
}
|
|
6195
|
+
/**
|
|
6196
|
+
* 把一个 preset 折成面板状态。
|
|
6197
|
+
* @param facts - 宿主收集的原始事实。
|
|
6198
|
+
* @param templateIds - 随包声明的模板清单(用于"与随包声明是否一致")。
|
|
6199
|
+
* @returns 面板状态行。
|
|
6200
|
+
*/
|
|
6201
|
+
function toPresetStatus(facts, templateIds) {
|
|
6202
|
+
const plugins = effectivePlugins(facts);
|
|
6203
|
+
const analysis = analyzePlugins(plugins);
|
|
6204
|
+
const customized = isCustomized(facts);
|
|
6205
|
+
const rows = isRowList(plugins) ? flattenRows(plugins).size : 0;
|
|
6206
|
+
const templateDiffers = facts.ours && templateIds.includes(facts.id) && isRowList(plugins) && isRowList(facts.inherited?.plugins) && presetDelta(plugins, facts.inherited?.plugins, { limit: Number.MAX_SAFE_INTEGER }).total > 0;
|
|
6207
|
+
const templateDelta = templateDiffers ? presetDelta(plugins, facts.inherited?.plugins) : void 0;
|
|
6208
|
+
const delta = templateDelta !== void 0 && templateDelta.total > 0 ? templateDelta : void 0;
|
|
6209
|
+
return {
|
|
6210
|
+
id: facts.id,
|
|
6211
|
+
...facts.entryId === void 0 ? {} : { entryId: facts.entryId },
|
|
6212
|
+
...facts.name === void 0 ? {} : { name: facts.name },
|
|
6213
|
+
...facts.description === void 0 ? {} : { description: facts.description },
|
|
6214
|
+
source: facts.ours ? "ours" : "other",
|
|
6215
|
+
isDefault: facts.isDefault,
|
|
6216
|
+
conflicts: analysis.conflicts,
|
|
6217
|
+
clean: analysis.clean,
|
|
6218
|
+
unrecognized: analysis.shape === "unrecognized",
|
|
6219
|
+
...facts.broken === void 0 ? {} : { broken: facts.broken },
|
|
6220
|
+
customized,
|
|
6221
|
+
templateDiffers,
|
|
6222
|
+
...delta === void 0 ? {} : { templateDelta: delta },
|
|
6223
|
+
rowCount: rows
|
|
6224
|
+
};
|
|
6225
|
+
}
|
|
6226
|
+
/**
|
|
6227
|
+
* 列出每个事实的状态,并给出模板清单。
|
|
6228
|
+
* @param facts - 宿主收集的原始事实(顺序即面板顺序)。
|
|
6229
|
+
* @param writable - 本部署有没有可编辑的 profile(`configEditor` 是否在场)。
|
|
6230
|
+
* @returns `presets/status` 的完整返回值。
|
|
6231
|
+
*/
|
|
6232
|
+
function listPresetStatuses$1(facts, writable) {
|
|
6233
|
+
const templateIds = facts.filter((item) => item.ours).map((item) => item.id);
|
|
6234
|
+
const templates = facts.filter((item) => item.ours).map((item) => item.name === void 0 ? { id: item.id } : {
|
|
6235
|
+
id: item.id,
|
|
6236
|
+
name: item.name
|
|
6237
|
+
});
|
|
6238
|
+
return {
|
|
6239
|
+
presets: facts.map((item) => toPresetStatus(item, templateIds)),
|
|
6240
|
+
templates,
|
|
6241
|
+
writable
|
|
6242
|
+
};
|
|
6243
|
+
}
|
|
6676
6244
|
/**
|
|
6677
|
-
*
|
|
6678
|
-
*
|
|
6679
|
-
* @param
|
|
6680
|
-
* @
|
|
6681
|
-
* @param deps - 路径注入(测试用)。
|
|
6682
|
-
* @returns 比较结果。
|
|
6245
|
+
* 取某个模板声明的 plugins(面板"对比 / 对齐"的右侧)。
|
|
6246
|
+
* @param facts - 同一个事实集合。
|
|
6247
|
+
* @param templateId - 模板 id(本插件声明的某个 preset)。
|
|
6248
|
+
* @returns 该模板的 plugins;不是我们的模板时 undefined。
|
|
6683
6249
|
*/
|
|
6684
|
-
function
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6250
|
+
function templatePlugins(facts, templateId) {
|
|
6251
|
+
return facts.find((item) => item.ours && item.id === templateId)?.inherited?.plugins;
|
|
6252
|
+
}
|
|
6253
|
+
//#endregion
|
|
6254
|
+
//#region src/host/preset-host.ts
|
|
6255
|
+
/**
|
|
6256
|
+
* Host half of the preset surface: read the deployment's declared presets and
|
|
6257
|
+
* apply the panel's three actions through the harness's own config editor.
|
|
6258
|
+
*
|
|
6259
|
+
* WHY WE DO NOT WRITE FILES OURSELVES: dsh 0.1.7 removed the directory preset
|
|
6260
|
+
* mechanism and the `agent_preset` tool with it. The only supported programmatic
|
|
6261
|
+
* write path is `ctx.configEditor.edit()`, and it is a better one than we had —
|
|
6262
|
+
* it takes the profile lock, serializes with HMR, validates the next config
|
|
6263
|
+
* through the Loader's own waterfall, writes atomically, rolls back on a failed
|
|
6264
|
+
* reconcile, and refuses when a home patch or `--patch` overlay would defeat the
|
|
6265
|
+
* edit. It also gives us "revert to the bundled declaration" for free: handing
|
|
6266
|
+
* back the inherited config makes it delete the override's `config` key.
|
|
6267
|
+
*
|
|
6268
|
+
* The service is resolved per call, so a deployment that mounts the editor
|
|
6269
|
+
* after this plugin still works instead of caching an early `undefined`.
|
|
6270
|
+
*
|
|
6271
|
+
* Everything here is a thin adapter; the decisions live in `src/presets/**`
|
|
6272
|
+
* (pure, fs-free, unit-testable) and this module only resolves services and
|
|
6273
|
+
* shapes results.
|
|
6274
|
+
* @module @xiaoso/dsh-tool-plus/host/preset-host
|
|
6275
|
+
*/
|
|
6276
|
+
/** Narrow a non-empty string, else undefined. */
|
|
6277
|
+
function text(value) {
|
|
6278
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
6279
|
+
}
|
|
6280
|
+
/** `{ key: value }` only when the value is present, so optional fields stay absent. */
|
|
6281
|
+
function optional(key, value) {
|
|
6282
|
+
return value === void 0 ? {} : { [key]: value };
|
|
6283
|
+
}
|
|
6284
|
+
/** Read a service only when it actually looks like the one we need. */
|
|
6285
|
+
function configEditorOf(ctx) {
|
|
6286
|
+
const service = ctx.get("configEditor");
|
|
6287
|
+
return service !== void 0 && typeof service.edit === "function" && typeof service.configuration === "function" ? service : void 0;
|
|
6288
|
+
}
|
|
6289
|
+
/** Read the preset registry roster only when it exposes one of its two readers. */
|
|
6290
|
+
async function rosterOf(ctx) {
|
|
6291
|
+
const service = ctx.get("agentPresets");
|
|
6292
|
+
if (service === void 0) return void 0;
|
|
6293
|
+
try {
|
|
6294
|
+
if (typeof service.remoteExportList === "function") {
|
|
6295
|
+
const exported = await service.remoteExportList();
|
|
6296
|
+
if (Array.isArray(exported?.presets)) return exported.presets;
|
|
6297
|
+
}
|
|
6298
|
+
if (typeof service.list === "function") return await service.list();
|
|
6299
|
+
return;
|
|
6300
|
+
} catch {
|
|
6301
|
+
return;
|
|
6302
|
+
}
|
|
6303
|
+
}
|
|
6304
|
+
/**
|
|
6305
|
+
* Collect every declared preset this deployment can see.
|
|
6306
|
+
*
|
|
6307
|
+
* Ordering is OURS FIRST (the panel's primary subject), then the rest in the
|
|
6308
|
+
* registry's own order — which the registry sorts by each declaration's `order`.
|
|
6309
|
+
* @param ctx - context used to resolve the services.
|
|
6310
|
+
* @returns the facts, the editor (when present) and the row→entry map.
|
|
6311
|
+
*/
|
|
6312
|
+
async function collect(ctx) {
|
|
6313
|
+
const editor = configEditorOf(ctx);
|
|
6314
|
+
const roster = await rosterOf(ctx);
|
|
6315
|
+
const byId = /* @__PURE__ */ new Map();
|
|
6316
|
+
const entries = /* @__PURE__ */ new Map();
|
|
6317
|
+
if (editor !== void 0) for (const row of editor.configuration()) {
|
|
6318
|
+
const entryId = text(row.entry.options.id);
|
|
6319
|
+
if (entryId === void 0 || !entryId.startsWith("preset-")) continue;
|
|
6320
|
+
const effective = row.entry.options.config ?? {};
|
|
6321
|
+
const id = text(row.inherited.id) ?? text(row.override.id) ?? text(effective.id) ?? entryId.slice(7);
|
|
6322
|
+
entries.set(entryId, row.entry);
|
|
6323
|
+
byId.set(id, {
|
|
6324
|
+
id,
|
|
6325
|
+
entryId,
|
|
6326
|
+
...optional("name", text(row.inherited.name) ?? text(effective.name)),
|
|
6327
|
+
...optional("description", text(row.inherited.description) ?? text(effective.description)),
|
|
6328
|
+
isDefault: false,
|
|
6329
|
+
effective,
|
|
6330
|
+
inherited: row.inherited,
|
|
6331
|
+
override: row.override,
|
|
6332
|
+
ours: BUNDLED_PRESET_IDS.includes(id)
|
|
6333
|
+
});
|
|
6334
|
+
}
|
|
6335
|
+
const rosterOrder = [];
|
|
6336
|
+
for (const row of roster ?? []) {
|
|
6337
|
+
const id = text(row.id);
|
|
6338
|
+
if (id === void 0) continue;
|
|
6339
|
+
rosterOrder.push(id);
|
|
6340
|
+
const existing = byId.get(id);
|
|
6341
|
+
byId.set(id, {
|
|
6342
|
+
id,
|
|
6343
|
+
...existing?.entryId === void 0 ? {} : { entryId: existing.entryId },
|
|
6344
|
+
...optional("name", text(row.name) ?? existing?.name),
|
|
6345
|
+
...optional("description", text(row.description) ?? existing?.description),
|
|
6346
|
+
isDefault: row.isDefault === true,
|
|
6347
|
+
...optional("broken", text(row.broken)),
|
|
6348
|
+
...existing?.effective === void 0 ? {} : { effective: existing.effective },
|
|
6349
|
+
...existing?.inherited === void 0 ? {} : { inherited: existing.inherited },
|
|
6350
|
+
...existing?.override === void 0 ? {} : { override: existing.override },
|
|
6351
|
+
ours: BUNDLED_PRESET_IDS.includes(id)
|
|
6352
|
+
});
|
|
6353
|
+
}
|
|
6354
|
+
const ours = BUNDLED_PRESET_IDS.map((id) => byId.get(id) ?? {
|
|
6355
|
+
id,
|
|
6356
|
+
isDefault: false,
|
|
6357
|
+
ours: true
|
|
6358
|
+
});
|
|
6359
|
+
const others = rosterOrder.filter((id) => !BUNDLED_PRESET_IDS.includes(id)).map((id) => byId.get(id)).filter((fact) => fact !== void 0);
|
|
6360
|
+
return {
|
|
6361
|
+
facts: [...ours, ...others],
|
|
6362
|
+
editor,
|
|
6363
|
+
entries
|
|
6364
|
+
};
|
|
6365
|
+
}
|
|
6366
|
+
/**
|
|
6367
|
+
* The pre-0.1.7 user preset directory, reported ONLY as a cleanup hint.
|
|
6368
|
+
*
|
|
6369
|
+
* dsh reads no directory any more ("Definitions are ordinary plugin rows; the
|
|
6370
|
+
* registry neither scans directories nor accepts preset paths"), so a leftover
|
|
6371
|
+
* `$DSH_HOME/.agent-presets` is dead weight. We surface its path so the panel can
|
|
6372
|
+
* tell the user it is safe to delete; we never read or write presets through it.
|
|
6373
|
+
* @returns the absolute path when the directory exists and holds entries.
|
|
6374
|
+
*/
|
|
6375
|
+
function legacyPresetRoot() {
|
|
6376
|
+
const home = process.env.DSH_HOME?.trim() || join(homedir(), ".dsh");
|
|
6377
|
+
const dir = join(home, ".agent-presets");
|
|
6378
|
+
try {
|
|
6379
|
+
return readdirSync(dir).length > 0 ? dir : void 0;
|
|
6380
|
+
} catch {
|
|
6381
|
+
return;
|
|
6382
|
+
}
|
|
6383
|
+
}
|
|
6384
|
+
/**
|
|
6385
|
+
* This plugin's own version, for the backup file name.
|
|
6386
|
+
*
|
|
6387
|
+
* Probes two candidates because the module URL differs between the built single
|
|
6388
|
+
* file (`lib/index.mjs` → `../package.json` is the package root) and the source
|
|
6389
|
+
* tree under test (`src/host/…` → `../../package.json`). Falls back to
|
|
6390
|
+
* `'unknown'` rather than failing an action over a cosmetic label.
|
|
6391
|
+
* @returns the version string, or `'unknown'`.
|
|
6392
|
+
*/
|
|
6393
|
+
function pluginVersion() {
|
|
6394
|
+
for (const candidate of ["../package.json", "../../package.json"]) try {
|
|
6395
|
+
const raw = JSON.parse(readFileSync(fileURLToPath(new URL(candidate, import.meta.url)), "utf8"));
|
|
6396
|
+
if (typeof raw.version === "string" && raw.version.length > 0) return raw.version;
|
|
6397
|
+
} catch {}
|
|
6398
|
+
return "unknown";
|
|
6399
|
+
}
|
|
6400
|
+
/**
|
|
6401
|
+
* Copy the profile patch aside before we change it.
|
|
6402
|
+
*
|
|
6403
|
+
* The config editor already gives us atomicity and rollback, but NOT a durable
|
|
6404
|
+
* "what it looked like before" artifact — and `align` overwrites content the
|
|
6405
|
+
* user may have written there. This restores the pre-0.1.10 affordance exactly:
|
|
6406
|
+
* one `<patch>.bak-<plugin version>` beside the patch, written once and never
|
|
6407
|
+
* overwritten, so the earliest pre-change state survives repeated actions.
|
|
6408
|
+
* @param documentPath - the profile patch the editor is about to rewrite.
|
|
6409
|
+
* @returns the backup path, or undefined when there was nothing to copy.
|
|
6410
|
+
*/
|
|
6411
|
+
function backupProfilePatch(documentPath) {
|
|
6412
|
+
if (!existsSync(documentPath)) return void 0;
|
|
6413
|
+
const target = `${documentPath}.bak-${pluginVersion()}`;
|
|
6414
|
+
if (existsSync(target)) return target;
|
|
6415
|
+
copyFileSync(documentPath, target);
|
|
6416
|
+
return target;
|
|
6417
|
+
}
|
|
6418
|
+
/** One failure in the gateway's shape. */
|
|
6419
|
+
function failure(reason) {
|
|
6420
|
+
return {
|
|
6421
|
+
ok: false,
|
|
6422
|
+
changed: false,
|
|
6423
|
+
reason,
|
|
6424
|
+
changes: []
|
|
6425
|
+
};
|
|
6426
|
+
}
|
|
6427
|
+
/**
|
|
6428
|
+
* Answer `presets/status`.
|
|
6429
|
+
* @param ctx - context used to resolve the config editor and the registry.
|
|
6430
|
+
* @returns the panel's full status payload.
|
|
6431
|
+
*/
|
|
6432
|
+
async function listPresetStatuses(ctx) {
|
|
6433
|
+
const { facts, editor } = await collect(ctx);
|
|
6434
|
+
const list = listPresetStatuses$1(facts, editor !== void 0);
|
|
6435
|
+
const legacy = legacyPresetRoot();
|
|
6436
|
+
return legacy === void 0 ? list : {
|
|
6437
|
+
...list,
|
|
6438
|
+
legacyPresetRoot: legacy
|
|
6439
|
+
};
|
|
6440
|
+
}
|
|
6441
|
+
/**
|
|
6442
|
+
* Answer `presets/apply` by planning the action and, when it is worth writing,
|
|
6443
|
+
* delegating the write to the harness's own config editor.
|
|
6444
|
+
* @param ctx - context used to resolve the services.
|
|
6445
|
+
* @param presetId - the preset to act on.
|
|
6446
|
+
* @param action - `upgrade`, `align` or `revert`.
|
|
6447
|
+
* @param templateId - the align target (required for `align`).
|
|
6448
|
+
* @returns the action result, never throwing.
|
|
6449
|
+
*/
|
|
6450
|
+
async function applyPresetAction(ctx, presetId, action, templateId) {
|
|
6451
|
+
try {
|
|
6452
|
+
const { facts, editor, entries } = await collect(ctx);
|
|
6453
|
+
const fact = facts.find((item) => item.id === presetId);
|
|
6454
|
+
if (fact === void 0) return failure(`unknown preset: ${presetId}`);
|
|
6455
|
+
const wanted = templateId === void 0 ? void 0 : templatePlugins(facts, templateId);
|
|
6456
|
+
const plan = planPresetAction({
|
|
6457
|
+
action,
|
|
6458
|
+
ours: fact.ours,
|
|
6459
|
+
customized: fact.override !== void 0 && Object.keys(fact.override).length > 0,
|
|
6460
|
+
effective: fact.effective,
|
|
6461
|
+
inherited: fact.inherited,
|
|
6462
|
+
...wanted === void 0 ? {} : { templatePlugins: wanted },
|
|
6463
|
+
...templateId === void 0 ? {} : { templateId }
|
|
6464
|
+
});
|
|
6465
|
+
if (plan.kind === "noop") return {
|
|
6466
|
+
ok: true,
|
|
6467
|
+
changed: false,
|
|
6468
|
+
reason: plan.reason,
|
|
6469
|
+
changes: []
|
|
6470
|
+
};
|
|
6471
|
+
if (editor === void 0) return failure("this deployment has no editable profile: ctx.configEditor is unavailable");
|
|
6472
|
+
const entry = fact.entryId === void 0 ? void 0 : entries.get(fact.entryId);
|
|
6473
|
+
if (entry === void 0) return failure(`this profile has no addressable row for ${presetId} (expected id ${profileRowId(presetId)}); install the plugin as a profile bundle so its patch declares the preset`);
|
|
6474
|
+
const backupPath = backupProfilePatch(editor.documentPath);
|
|
6475
|
+
if (plan.kind === "revert") await editor.edit(entry, (_current, inherited) => inherited);
|
|
6476
|
+
else if (plan.kind === "align") await editor.edit(entry, (current) => ({
|
|
6477
|
+
...current,
|
|
6478
|
+
plugins: wanted
|
|
6479
|
+
}));
|
|
6480
|
+
else await editor.edit(entry, (current) => {
|
|
6481
|
+
if (!isRowList(current.plugins)) return current;
|
|
6482
|
+
return {
|
|
6483
|
+
...current,
|
|
6484
|
+
plugins: disableConflicts(current.plugins).plugins
|
|
6485
|
+
};
|
|
6486
|
+
});
|
|
6487
|
+
return {
|
|
6488
|
+
ok: true,
|
|
6489
|
+
changed: true,
|
|
6490
|
+
...backupPath === void 0 ? {} : { backupPath },
|
|
6491
|
+
changes: plan.kind === "upgrade" ? plan.changes : []
|
|
6492
|
+
};
|
|
6493
|
+
} catch (error) {
|
|
6494
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
6495
|
+
}
|
|
6496
|
+
}
|
|
6497
|
+
/**
|
|
6498
|
+
* Answer `presets/compare`: one preset's effective plugins against one of our
|
|
6499
|
+
* declared templates. Never throws; an unresolvable side reports `unreadable`
|
|
6500
|
+
* (that is a state the panel renders as a red dot, not a failed call).
|
|
6501
|
+
* @param ctx - context used to resolve the services.
|
|
6502
|
+
* @param presetId - the preset being inspected.
|
|
6503
|
+
* @param templateId - the template to compare it against.
|
|
6504
|
+
* @returns the comparison.
|
|
6505
|
+
*/
|
|
6506
|
+
async function comparePresetAgainstTemplate(ctx, presetId, templateId) {
|
|
6507
|
+
const empty = {
|
|
6508
|
+
presetId,
|
|
6509
|
+
templateId,
|
|
6510
|
+
status: "unreadable",
|
|
6511
|
+
conflicts: [],
|
|
6512
|
+
identical: false,
|
|
6513
|
+
yoursCount: 0,
|
|
6514
|
+
behindCount: 0,
|
|
6515
|
+
items: []
|
|
6516
|
+
};
|
|
6517
|
+
try {
|
|
6518
|
+
const { facts } = await collect(ctx);
|
|
6519
|
+
const fact = facts.find((item) => item.id === presetId);
|
|
6520
|
+
if (fact === void 0) return empty;
|
|
6521
|
+
const template = templatePlugins(facts, templateId);
|
|
6522
|
+
if (template === void 0) return empty;
|
|
6523
|
+
return {
|
|
6524
|
+
presetId,
|
|
6525
|
+
templateId,
|
|
6526
|
+
...comparePlugins(fact.effective?.plugins, template)
|
|
6527
|
+
};
|
|
6528
|
+
} catch {
|
|
6529
|
+
return empty;
|
|
6530
|
+
}
|
|
6689
6531
|
}
|
|
6690
6532
|
//#endregion
|
|
6691
6533
|
//#region src/web/host/bash.ts
|
|
@@ -11558,7 +11400,7 @@ let specialHandlersPromise;
|
|
|
11558
11400
|
* requires a special handler, so we keep them out of the cold-startup graph.
|
|
11559
11401
|
*/
|
|
11560
11402
|
function loadSpecialHandlers() {
|
|
11561
|
-
specialHandlersPromise ??= import("./scrapers-
|
|
11403
|
+
specialHandlersPromise ??= import("./scrapers-DSArdJ6N.mjs").then((m) => m.specialHandlers);
|
|
11562
11404
|
return specialHandlersPromise;
|
|
11563
11405
|
}
|
|
11564
11406
|
/**
|
|
@@ -23738,19 +23580,11 @@ function apply(ctx, config = {}) {
|
|
|
23738
23580
|
const disposeBrowserProbe = installBrowserProbeRpc(ctx, {
|
|
23739
23581
|
getRmSafe: () => cfg.rmSafe,
|
|
23740
23582
|
presets: {
|
|
23741
|
-
|
|
23742
|
-
applyAction: (id, action, templateId) => applyPresetAction(id, action,
|
|
23743
|
-
|
|
23744
|
-
comparePreset: (preset, templateId) => comparePresetFile(preset.path, templateId)
|
|
23583
|
+
listPresets: (presetCtx) => listPresetStatuses(presetCtx),
|
|
23584
|
+
applyAction: (presetCtx, id, action, templateId) => applyPresetAction(presetCtx, id, action, templateId),
|
|
23585
|
+
comparePreset: (presetCtx, presetId, templateId) => comparePresetAgainstTemplate(presetCtx, presetId, templateId)
|
|
23745
23586
|
}
|
|
23746
23587
|
});
|
|
23747
|
-
try {
|
|
23748
|
-
const { created, failed } = ensureDefaultPresets();
|
|
23749
|
-
if (created.length > 0) runtimeLogger().info(`preset templates installed: ${created.join(", ")}`);
|
|
23750
|
-
for (const item of failed) runtimeLogger().warn(`preset template ${item.id} not installed: ${item.reason}`);
|
|
23751
|
-
} catch (error) {
|
|
23752
|
-
runtimeLogger().warn(`preset bootstrap skipped: ${error instanceof Error ? error.message : String(error)}`);
|
|
23753
|
-
}
|
|
23754
23588
|
ctx.systemPrompt.section({
|
|
23755
23589
|
name: "tool:bash",
|
|
23756
23590
|
order: 105,
|
|
@@ -23928,72 +23762,47 @@ function apply(ctx, config = {}) {
|
|
|
23928
23762
|
...commandEnv
|
|
23929
23763
|
};
|
|
23930
23764
|
const jobs = ctx.get("jobs");
|
|
23765
|
+
const owner = exec.agent?.id;
|
|
23931
23766
|
const backgroundSlotsAvailable = () => {
|
|
23932
23767
|
if (jobs === void 0 || cfg.maxBackgroundJobs <= 0) return true;
|
|
23933
|
-
return jobs.list(
|
|
23934
|
-
};
|
|
23935
|
-
const startManagedJob = () => {
|
|
23936
|
-
const managed = startBashJob({
|
|
23937
|
-
sessionId,
|
|
23938
|
-
command,
|
|
23939
|
-
cwd: commandCwd,
|
|
23940
|
-
timeoutMs: args.timeoutMs ?? 0,
|
|
23941
|
-
env,
|
|
23942
|
-
config: cfg
|
|
23943
|
-
});
|
|
23944
|
-
let settled = false;
|
|
23945
|
-
let settledSpillPath;
|
|
23946
|
-
managed.completion.then((value) => {
|
|
23947
|
-
settled = true;
|
|
23948
|
-
settledSpillPath = value.output.spillPath;
|
|
23949
|
-
}, () => {
|
|
23950
|
-
settled = true;
|
|
23951
|
-
settledSpillPath = void 0;
|
|
23952
|
-
});
|
|
23953
|
-
return {
|
|
23954
|
-
hooks: {
|
|
23955
|
-
...managed.hooks,
|
|
23956
|
-
readOutput: () => {
|
|
23957
|
-
const text = managed.hooks.readOutput?.() ?? "";
|
|
23958
|
-
return settled ? applyConfiguredTruncation(text, settledSpillPath, cfg.outputTruncate) : text;
|
|
23959
|
-
}
|
|
23960
|
-
},
|
|
23961
|
-
completion: managed.completion
|
|
23962
|
-
};
|
|
23768
|
+
return jobs.list(owner).filter((j) => j.status === "running" || j.status === "stopping").length < cfg.maxBackgroundJobs;
|
|
23963
23769
|
};
|
|
23770
|
+
const startManagedJob = (job) => startBashJob({
|
|
23771
|
+
sessionId,
|
|
23772
|
+
command,
|
|
23773
|
+
cwd: commandCwd,
|
|
23774
|
+
timeoutMs: args.timeoutMs ?? 0,
|
|
23775
|
+
env,
|
|
23776
|
+
config: cfg,
|
|
23777
|
+
...job !== void 0 ? { job } : {},
|
|
23778
|
+
formatCompletion: (text, spillPath) => applyConfiguredTruncation(text, spillPath, cfg.outputTruncate)
|
|
23779
|
+
});
|
|
23780
|
+
const jobSpec = (run) => ({
|
|
23781
|
+
kind: "bash",
|
|
23782
|
+
label: command,
|
|
23783
|
+
...owner !== void 0 ? { owner } : {},
|
|
23784
|
+
...Number.isSafeInteger(cfg.outputMaxBytes) && cfg.outputMaxBytes > 0 ? { outputLimitBytes: cfg.outputMaxBytes } : {},
|
|
23785
|
+
run
|
|
23786
|
+
});
|
|
23964
23787
|
if (args.run_in_background === true) {
|
|
23965
23788
|
if (!cfg.enableRunInBackground) throw new Error("run_in_background is disabled for this deployment (enableRunInBackground: false)");
|
|
23966
23789
|
if (jobs === void 0) throw new Error("background jobs unavailable: load @deepseek-ai/dsh-jobs and @deepseek-ai/dsh-tool-jobs");
|
|
23967
23790
|
if (!backgroundSlotsAvailable()) throw new Error(`background jobs at capacity (maxBackgroundJobs: ${cfg.maxBackgroundJobs})`);
|
|
23968
23791
|
if (exec.signal.aborted) throw abortError();
|
|
23969
|
-
let managed;
|
|
23970
23792
|
return {
|
|
23971
23793
|
kind: "background",
|
|
23972
|
-
jobId: jobs.start(
|
|
23973
|
-
kind: "bash",
|
|
23974
|
-
label: command,
|
|
23975
|
-
...exec.agent !== void 0 ? { owner: exec.agent } : {},
|
|
23976
|
-
run: () => {
|
|
23977
|
-
managed = startManagedJob();
|
|
23978
|
-
return managed.hooks;
|
|
23979
|
-
}
|
|
23980
|
-
})
|
|
23794
|
+
jobId: jobs.start(jobSpec((job) => startManagedJob(job).hooks))
|
|
23981
23795
|
};
|
|
23982
23796
|
}
|
|
23983
23797
|
if (cfg.autoBackgroundMs > 0 && jobs !== void 0 && backgroundSlotsAvailable() && !exec.signal.aborted) {
|
|
23984
|
-
const autoBgWaitMs = timeoutMs === 0 ? cfg.autoBackgroundMs : Math.
|
|
23798
|
+
const autoBgWaitMs = Math.max(1, timeoutMs === 0 ? cfg.autoBackgroundMs : Math.min(cfg.autoBackgroundMs, timeoutMs - 1e3));
|
|
23985
23799
|
let managed;
|
|
23986
23800
|
let id;
|
|
23987
23801
|
try {
|
|
23988
|
-
id = jobs.start({
|
|
23989
|
-
|
|
23990
|
-
|
|
23991
|
-
|
|
23992
|
-
run: () => {
|
|
23993
|
-
managed = startManagedJob();
|
|
23994
|
-
return managed.hooks;
|
|
23995
|
-
}
|
|
23996
|
-
});
|
|
23802
|
+
id = jobs.start(jobSpec((job) => {
|
|
23803
|
+
managed = startManagedJob(job);
|
|
23804
|
+
return managed.hooks;
|
|
23805
|
+
}));
|
|
23997
23806
|
} catch {
|
|
23998
23807
|
const startedAt = performance.now();
|
|
23999
23808
|
const result = await executeBash(command, {
|
|
@@ -24025,34 +23834,23 @@ function apply(ctx, config = {}) {
|
|
|
24025
23834
|
if (result.workingDir !== void 0) state.cwd = result.workingDir;
|
|
24026
23835
|
return buildForeground(result, performance.now() - startedAt, timeoutMs);
|
|
24027
23836
|
}
|
|
24028
|
-
|
|
24029
|
-
|
|
24030
|
-
|
|
24031
|
-
|
|
24032
|
-
|
|
24033
|
-
|
|
24034
|
-
|
|
24035
|
-
|
|
24036
|
-
|
|
24037
|
-
|
|
24038
|
-
if (exec.signal.aborted) {
|
|
24039
|
-
reject(abortError());
|
|
24040
|
-
return;
|
|
24041
|
-
}
|
|
24042
|
-
exec.signal.addEventListener("abort", () => reject(abortError()), { once: true });
|
|
24043
|
-
})
|
|
24044
|
-
]);
|
|
24045
|
-
if (window.kind === "window") return {
|
|
23837
|
+
let settled = false;
|
|
23838
|
+
try {
|
|
23839
|
+
const view = await jobs.wait(id, autoBgWaitMs, owner, exec.signal);
|
|
23840
|
+
settled = view.status !== "running" && view.status !== "stopping";
|
|
23841
|
+
} catch {
|
|
23842
|
+
jobs.kill(id, owner, "tool call aborted");
|
|
23843
|
+
await jobs.wait(id, Math.max(1e3, timeoutMs), owner);
|
|
23844
|
+
throw abortError();
|
|
23845
|
+
}
|
|
23846
|
+
if (!settled) return {
|
|
24046
23847
|
kind: "background",
|
|
24047
23848
|
jobId: id
|
|
24048
23849
|
};
|
|
24049
|
-
|
|
24050
|
-
|
|
24051
|
-
|
|
24052
|
-
|
|
24053
|
-
jobs.read(id, exec.agent);
|
|
24054
|
-
if (window.value.workingDir !== void 0) state.cwd = window.value.workingDir;
|
|
24055
|
-
return window.value;
|
|
23850
|
+
const value = await managed.completion;
|
|
23851
|
+
jobs.remove(id, owner);
|
|
23852
|
+
if (value.workingDir !== void 0) state.cwd = value.workingDir;
|
|
23853
|
+
return value;
|
|
24056
23854
|
}
|
|
24057
23855
|
const startedAt = performance.now();
|
|
24058
23856
|
const result = await executeBash(command, {
|