@xiaoso/dsh-tool-plus 0.1.9 → 0.1.10-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +12 -5
- package/README.md +11 -4
- package/THIRD_PARTY_NOTICES.md +36 -0
- package/lib/client.js +272 -181
- package/lib/index.mjs +875 -1081
- package/lib/{scrapers-mhvFrvjH.mjs → scrapers-DODCPcZW.mjs} +1 -1
- package/lib/types/client/BashPlusCard.d.ts +6 -6
- 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 +25 -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-D80w8vry.mjs} +1 -1
- package/package.json +61 -46
- package/presets/tool-plus-ptc.patch.yml +164 -0
- package/presets/tool-plus-standard.patch.yml +158 -0
- 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-D80w8vry.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";
|
|
@@ -1966,8 +1966,10 @@ var OutputSink = class extends OutputSink$1 {
|
|
|
1966
1966
|
};
|
|
1967
1967
|
/**
|
|
1968
1968
|
* Composition wrapper over the verbatim {@link TailBuffer}, adding the DSH
|
|
1969
|
-
* `reset()` increment
|
|
1970
|
-
*
|
|
1969
|
+
* `reset()` increment: the consuming-read cursor reset the bash background
|
|
1970
|
+
* producer used while `JobHooks.readOutput` existed. 0.1.7 moves the model's
|
|
1971
|
+
* cursor onto the job registry's output ring, so the increment is no longer
|
|
1972
|
+
* part of that path; the preview buffer still offers it.
|
|
1971
1973
|
*/
|
|
1972
1974
|
var TailBuffer = class {
|
|
1973
1975
|
#inner;
|
|
@@ -2651,25 +2653,25 @@ function sweepStaleSpillFiles(now = Date.now()) {
|
|
|
2651
2653
|
* Background-job adaptation for the `ctx.jobs` seam: one managed job runs its
|
|
2652
2654
|
* command on its OWN `:async:` Shell (never the session's persistent shell),
|
|
2653
2655
|
* 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
|
|
2656
|
+
* identity, lifecycle state, the output ring, and completion notices
|
|
2657
|
+
* (`dsh-tool-jobs` injects them); this module owns the execution resources,
|
|
2658
|
+
* streams the run into that ring, and appends the plugin's completion message
|
|
2659
|
+
* once the run ends.
|
|
2656
2660
|
* @module @xiaoso/dsh-tool-plus/background
|
|
2657
2661
|
*/
|
|
2658
2662
|
/**
|
|
2659
2663
|
* Start one managed background bash job. The producer owns a per-job
|
|
2660
|
-
* `AbortController` (cancellation kills only this job's shell)
|
|
2661
|
-
* preview tail
|
|
2664
|
+
* `AbortController` (cancellation kills only this job's shell) and a bounded
|
|
2665
|
+
* preview tail; the registry owns the model's consuming cursor over the ring
|
|
2666
|
+
* this job writes to.
|
|
2662
2667
|
* @param options - job identity and execution parameters.
|
|
2663
2668
|
* @returns the registry hooks plus the tool-result completion promise.
|
|
2664
2669
|
*/
|
|
2665
2670
|
function startBashJob(options) {
|
|
2666
|
-
const { sessionId, command, cwd, timeoutMs, env, config } = options;
|
|
2671
|
+
const { sessionId, command, cwd, timeoutMs, env, config, job } = options;
|
|
2667
2672
|
const shellKey = `${sessionId}:async:${randomBytes(4).toString("hex")}`;
|
|
2668
2673
|
const abortController = new AbortController();
|
|
2669
2674
|
const preview = new TailBuffer(config.outputMaxBytes);
|
|
2670
|
-
const delta = new TailBuffer(config.outputMaxBytes);
|
|
2671
|
-
let settled = false;
|
|
2672
|
-
let settledText = "";
|
|
2673
2675
|
const completion = (async () => {
|
|
2674
2676
|
const startedAt = performance.now();
|
|
2675
2677
|
const spillPath = allocateSpillFile();
|
|
@@ -2699,11 +2701,9 @@ function startBashJob(options) {
|
|
|
2699
2701
|
onMinimizedSave: (originalText) => saveOriginalText(originalText),
|
|
2700
2702
|
onChunk: (chunk) => {
|
|
2701
2703
|
preview.append(chunk);
|
|
2702
|
-
|
|
2704
|
+
job?.append(chunk);
|
|
2703
2705
|
}
|
|
2704
2706
|
});
|
|
2705
|
-
settled = true;
|
|
2706
|
-
settledText = preview.text();
|
|
2707
2707
|
const wallTimeMs = performance.now() - startedAt;
|
|
2708
2708
|
const aborted = result.cancelled && abortController.signal.aborted;
|
|
2709
2709
|
const timedOut = result.timedOut ?? (result.cancelled && !aborted);
|
|
@@ -2712,6 +2712,9 @@ function startBashJob(options) {
|
|
|
2712
2712
|
const seconds = Math.max(1, Math.round((timeoutMs ?? 0) / 1e3));
|
|
2713
2713
|
if (!text.includes("[Command timed out after")) text = `[Command timed out after ${seconds} seconds]\n\n${text}`;
|
|
2714
2714
|
}
|
|
2715
|
+
const previewText = preview.text();
|
|
2716
|
+
const message = options.formatCompletion?.(previewText, result.spillPath) ?? previewText;
|
|
2717
|
+
if (job !== void 0 && message !== previewText) job.append(message);
|
|
2715
2718
|
return {
|
|
2716
2719
|
kind: "foreground",
|
|
2717
2720
|
exitCode: result.exitCode ?? null,
|
|
@@ -2756,13 +2759,7 @@ function startBashJob(options) {
|
|
|
2756
2759
|
}, (error) => ({
|
|
2757
2760
|
status: "failed",
|
|
2758
2761
|
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
|
-
}
|
|
2762
|
+
}))
|
|
2766
2763
|
},
|
|
2767
2764
|
completion
|
|
2768
2765
|
};
|
|
@@ -4887,8 +4884,24 @@ function toolPlusField(name) {
|
|
|
4887
4884
|
TOOL_PLUS_FIELDS.map((field) => field.name);
|
|
4888
4885
|
//#endregion
|
|
4889
4886
|
//#region src/config/settings.ts
|
|
4890
|
-
/**
|
|
4891
|
-
|
|
4887
|
+
/**
|
|
4888
|
+
* Protocol key `@deepseek-ai/cosmokit` stamps on a live config reference.
|
|
4889
|
+
* `Symbol.for` keeps the check valid across copies of the shared library — the
|
|
4890
|
+
* reason cosmokit's own `isVolatile` resolves the same key.
|
|
4891
|
+
*/
|
|
4892
|
+
const VOLATILE_WRITE = Symbol.for("cosmokit.volatile.write");
|
|
4893
|
+
/**
|
|
4894
|
+
* Whether a parsed value is one of the Loader's live config references.
|
|
4895
|
+
*
|
|
4896
|
+
* `@deepseek-ai/cordis` re-exports the `Volatile` TYPE only, and cosmokit —
|
|
4897
|
+
* which owns the runtime check — is not a dependency of this package, so the
|
|
4898
|
+
* protocol is read here instead of imported.
|
|
4899
|
+
* @param value - a parsed config value.
|
|
4900
|
+
* @returns whether the value is a live reference.
|
|
4901
|
+
*/
|
|
4902
|
+
function isLiveField(value) {
|
|
4903
|
+
return typeof value === "object" && value !== null && VOLATILE_WRITE in value;
|
|
4904
|
+
}
|
|
4892
4905
|
/** OMP-parity defaults for the completion-message truncation policy. */
|
|
4893
4906
|
const DEFAULT_OUTPUT_TRUNCATE = {
|
|
4894
4907
|
strategy: "bytes",
|
|
@@ -4923,73 +4936,81 @@ function fieldDefault(name, fallback) {
|
|
|
4923
4936
|
return value === void 0 ? fallback : value;
|
|
4924
4937
|
}
|
|
4925
4938
|
/**
|
|
4926
|
-
* Runtime configuration schema for the plugin
|
|
4927
|
-
*
|
|
4939
|
+
* Runtime configuration schema for the plugin, in the dsh 0.1.7 settings shape:
|
|
4940
|
+
* EVERY field is declared `.volatile()`, because a settings namespace is the
|
|
4941
|
+
* plugin's own entry and `SettingsForms.describe()` serves exactly the fields
|
|
4942
|
+
* whose schema declares them live. {@link resolveConfig} maps the flat surface
|
|
4943
|
+
* onto {@link RuntimeConfig}.
|
|
4944
|
+
*
|
|
4945
|
+
* The annotation is deliberately inferred rather than written as `z<Config>`:
|
|
4946
|
+
* the schema's parsed output is {@link LiveConfig} (one stable reference per
|
|
4947
|
+
* field), while {@link Config} stays the plain settings DOCUMENT the browser
|
|
4948
|
+
* edits and the specs pass in.
|
|
4928
4949
|
*/
|
|
4929
4950
|
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)),
|
|
4951
|
+
enableRunInBackground: z.boolean().default(fieldDefault("enableRunInBackground", true)).volatile(),
|
|
4952
|
+
autoBackgroundMs: z.number().default(fieldDefault("autoBackgroundMs", DEFAULT_AUTO_BACKGROUND_MS)).volatile(),
|
|
4953
|
+
defaultTimeoutMs: z.number().default(fieldDefault("defaultTimeoutMs", DEFAULT_TIMEOUT_MS)).volatile(),
|
|
4954
|
+
maxTimeoutMs: z.number().default(fieldDefault("maxTimeoutMs", DEFAULT_MAX_TIMEOUT_MS)).volatile(),
|
|
4955
|
+
outputMaxBytes: z.number().default(fieldDefault("outputMaxBytes", 51200)).volatile(),
|
|
4956
|
+
outputSinkTailBytes: z.number().default(fieldDefault("outputSinkTailBytes", 51200)).volatile(),
|
|
4957
|
+
outputSinkHeadBytes: z.number().default(fieldDefault("outputSinkHeadBytes", 20480)).volatile(),
|
|
4958
|
+
minimizerEnabled: z.boolean().default(fieldDefault("minimizerEnabled", true)).volatile(),
|
|
4959
|
+
interceptorEnabled: z.boolean().default(fieldDefault("interceptorEnabled", true)).volatile(),
|
|
4960
|
+
nonInteractiveEnv: z.boolean().default(fieldDefault("nonInteractiveEnv", true)).volatile(),
|
|
4961
|
+
snapshotEnabled: z.boolean().default(fieldDefault("snapshotEnabled", true)).volatile(),
|
|
4962
|
+
rmSafe: z.boolean().default(fieldDefault("rmSafe", true)).volatile(),
|
|
4963
|
+
useShellCommandWrapper: z.boolean().default(fieldDefault("useShellCommandWrapper", false)).volatile(),
|
|
4964
|
+
webCards: z.boolean().default(fieldDefault("webCards", true)).volatile(),
|
|
4965
|
+
maxBackgroundJobs: z.number().default(fieldDefault("maxBackgroundJobs", 15)).volatile(),
|
|
4966
|
+
outputTruncateStrategy: z.union(["bytes", "lines"]).default(fieldDefault("outputTruncateStrategy", "bytes")).volatile(),
|
|
4967
|
+
outputTruncateTriggerBytes: z.number().default(fieldDefault("outputTruncateTriggerBytes", 10240)).volatile(),
|
|
4968
|
+
outputTruncateTriggerLines: z.number().default(fieldDefault("outputTruncateTriggerLines", 100)).volatile(),
|
|
4948
4969
|
outputTruncateByteMode: z.union([
|
|
4949
4970
|
"head",
|
|
4950
4971
|
"tail",
|
|
4951
4972
|
"middle"
|
|
4952
|
-
]).default(fieldDefault("outputTruncateByteMode", "middle")),
|
|
4953
|
-
outputTruncateByteHeadBytes: z.number().default(fieldDefault("outputTruncateByteHeadBytes", 4096)),
|
|
4954
|
-
outputTruncateByteTailBytes: z.number().default(fieldDefault("outputTruncateByteTailBytes", 4096)),
|
|
4973
|
+
]).default(fieldDefault("outputTruncateByteMode", "middle")).volatile(),
|
|
4974
|
+
outputTruncateByteHeadBytes: z.number().default(fieldDefault("outputTruncateByteHeadBytes", 4096)).volatile(),
|
|
4975
|
+
outputTruncateByteTailBytes: z.number().default(fieldDefault("outputTruncateByteTailBytes", 4096)).volatile(),
|
|
4955
4976
|
outputTruncateLineMode: z.union([
|
|
4956
4977
|
"head",
|
|
4957
4978
|
"tail",
|
|
4958
4979
|
"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)),
|
|
4980
|
+
]).default(fieldDefault("outputTruncateLineMode", "middle")).volatile(),
|
|
4981
|
+
outputTruncateLineHeadLines: z.number().default(fieldDefault("outputTruncateLineHeadLines", 50)).volatile(),
|
|
4982
|
+
outputTruncateLineTailLines: z.number().default(fieldDefault("outputTruncateLineTailLines", 100)).volatile(),
|
|
4983
|
+
editBlockAutoGenerated: z.boolean().default(fieldDefault("editBlockAutoGenerated", true)).volatile(),
|
|
4984
|
+
readSummarizeEnabled: z.boolean().default(fieldDefault("readSummarizeEnabled", true)).volatile(),
|
|
4985
|
+
readSummarizeProse: z.boolean().default(fieldDefault("readSummarizeProse", false)).volatile(),
|
|
4986
|
+
readSummarizeMinBodyLines: z.number().default(fieldDefault("readSummarizeMinBodyLines", 4)).volatile(),
|
|
4987
|
+
readSummarizeMinCommentLines: z.number().default(fieldDefault("readSummarizeMinCommentLines", 6)).volatile(),
|
|
4988
|
+
readSummarizeMinTotalLines: z.number().default(fieldDefault("readSummarizeMinTotalLines", 100)).volatile(),
|
|
4989
|
+
readSummarizeUnfoldUntil: z.number().default(fieldDefault("readSummarizeUnfoldUntil", 50)).volatile(),
|
|
4990
|
+
readSummarizeUnfoldLimit: z.number().default(fieldDefault("readSummarizeUnfoldLimit", 100)).volatile(),
|
|
4970
4991
|
editMode: z.union([
|
|
4971
4992
|
"replace",
|
|
4972
4993
|
"patch",
|
|
4973
4994
|
"hashline",
|
|
4974
4995
|
"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)),
|
|
4996
|
+
]).default(fieldDefault("editMode", "replace")).volatile(),
|
|
4997
|
+
editFuzzyMatch: z.boolean().default(fieldDefault("editFuzzyMatch", true)).volatile(),
|
|
4998
|
+
editFuzzyThreshold: z.number().default(fieldDefault("editFuzzyThreshold", .95)).volatile(),
|
|
4999
|
+
editEnforceSeenLines: z.boolean().default(fieldDefault("editEnforceSeenLines", false)).volatile(),
|
|
5000
|
+
readDefaultLimit: z.number().default(fieldDefault("readDefaultLimit", 300)).volatile(),
|
|
5001
|
+
readLineNumbers: z.boolean().default(fieldDefault("readLineNumbers", false)).volatile(),
|
|
5002
|
+
readRenderMarkdown: z.boolean().default(fieldDefault("readRenderMarkdown", false)).volatile(),
|
|
5003
|
+
readConcurrentSafe: z.boolean().default(fieldDefault("readConcurrentSafe", true)).volatile(),
|
|
5004
|
+
grepContextBefore: z.number().default(fieldDefault("grepContextBefore", 1)).volatile(),
|
|
5005
|
+
grepContextAfter: z.number().default(fieldDefault("grepContextAfter", 3)).volatile(),
|
|
5006
|
+
grepCaseDefault: z.boolean().default(fieldDefault("grepCaseDefault", true)).volatile(),
|
|
5007
|
+
grepGitignoreDefault: z.boolean().default(fieldDefault("grepGitignoreDefault", true)).volatile(),
|
|
5008
|
+
globGitignoreDefault: z.boolean().default(fieldDefault("globGitignoreDefault", true)).volatile(),
|
|
5009
|
+
globHiddenDefault: z.boolean().default(fieldDefault("globHiddenDefault", true)).volatile(),
|
|
5010
|
+
astGrepEnabled: z.boolean().default(fieldDefault("astGrepEnabled", false)).volatile(),
|
|
5011
|
+
astEditEnabled: z.boolean().default(fieldDefault("astEditEnabled", true)).volatile(),
|
|
5012
|
+
fetchEnabled: z.boolean().default(fieldDefault("fetchEnabled", true)).volatile(),
|
|
5013
|
+
fetchMaxTimeoutSeconds: z.number().default(fieldDefault("fetchMaxTimeoutSeconds", 0)).volatile(),
|
|
4993
5014
|
fetchReader: z.union([
|
|
4994
5015
|
"auto",
|
|
4995
5016
|
"native",
|
|
@@ -4998,26 +5019,39 @@ const Config = z.object({
|
|
|
4998
5019
|
"parallel",
|
|
4999
5020
|
"jina",
|
|
5000
5021
|
"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))
|
|
5022
|
+
]).default(fieldDefault("fetchReader", "auto")).volatile(),
|
|
5023
|
+
browserReaderEnabled: z.boolean().default(fieldDefault("browserReaderEnabled", true)).volatile(),
|
|
5024
|
+
imagesAutoResize: z.boolean().default(fieldDefault("imagesAutoResize", true)).volatile(),
|
|
5025
|
+
imagesBlockImages: z.boolean().default(fieldDefault("imagesBlockImages", false)).volatile(),
|
|
5026
|
+
imagesExcludeWebp: z.boolean().default(fieldDefault("imagesExcludeWebp", false)).volatile(),
|
|
5027
|
+
imagesInputMaxBytes: z.number().default(fieldDefault("imagesInputMaxBytes", 20971520)).volatile(),
|
|
5028
|
+
imagesResizeMaxSide: z.number().default(fieldDefault("imagesResizeMaxSide", 1568)).volatile(),
|
|
5029
|
+
imagesResizeMaxBytes: z.number().default(fieldDefault("imagesResizeMaxBytes", 512e3)).volatile(),
|
|
5030
|
+
imagesResizeMinSide: z.number().default(fieldDefault("imagesResizeMinSide", 200)).volatile(),
|
|
5031
|
+
imagesResizeJpegQuality: z.number().default(fieldDefault("imagesResizeJpegQuality", 80)).volatile()
|
|
5011
5032
|
});
|
|
5012
5033
|
/**
|
|
5034
|
+
* Unwrap the Loader's live field references into a plain settings document.
|
|
5035
|
+
* @param input - the entry's live config, or an already-plain document.
|
|
5036
|
+
* @returns a plain copy carrying the fields the input has.
|
|
5037
|
+
*/
|
|
5038
|
+
function plainFields(input) {
|
|
5039
|
+
const out = {};
|
|
5040
|
+
for (const [field, value] of Object.entries(input)) Object.assign(out, { [field]: isLiveField(value) ? value.get() : value });
|
|
5041
|
+
return out;
|
|
5042
|
+
}
|
|
5043
|
+
/**
|
|
5013
5044
|
* Resolve the flat settings (composition entry or the settings document) to
|
|
5014
5045
|
* the nested runtime config the ported OMP runtime consumes. A direct mount
|
|
5015
5046
|
* (`ctx.plugin`) skips the Loader's schema parse, so the `?? default` pass
|
|
5016
5047
|
* mirrors the schema; the settings namespace resolution rides the same path.
|
|
5017
|
-
*
|
|
5048
|
+
* Accepts both shapes a caller can hold: the Loader's live entry config (one
|
|
5049
|
+
* reference per field) and a plain settings document.
|
|
5050
|
+
* @param input - the entry's live config, or a plain (possibly partial) document.
|
|
5018
5051
|
* @returns the fully-defaulted nested runtime config.
|
|
5019
5052
|
*/
|
|
5020
|
-
function resolveConfig(
|
|
5053
|
+
function resolveConfig(input) {
|
|
5054
|
+
const config = plainFields(input);
|
|
5021
5055
|
const resolveMinimizer = () => ({
|
|
5022
5056
|
enabled: config.minimizerEnabled ?? fieldDefault("minimizerEnabled", true),
|
|
5023
5057
|
only: [],
|
|
@@ -5094,29 +5128,46 @@ function resolveConfig(config) {
|
|
|
5094
5128
|
};
|
|
5095
5129
|
}
|
|
5096
5130
|
/**
|
|
5097
|
-
* Install the optional-settings consumer wiring for this plugin
|
|
5098
|
-
*
|
|
5099
|
-
*
|
|
5100
|
-
*
|
|
5101
|
-
*
|
|
5102
|
-
*
|
|
5131
|
+
* Install the optional-settings consumer wiring for this plugin, in the dsh
|
|
5132
|
+
* 0.1.7 shape.
|
|
5133
|
+
*
|
|
5134
|
+
* 0.1.7 deleted the namespace-registration API (`SettingsForms` no longer has
|
|
5135
|
+
* `installSection`): a settings namespace is not something a plugin installs
|
|
5136
|
+
* any more — it IS the plugin's own profile entry, and the service serves
|
|
5137
|
+
* exactly the Config fields whose schema declares them live
|
|
5138
|
+
* ({@link Config} declares every field `.volatile()`). The plugin therefore
|
|
5139
|
+
* keeps one authoritative source, its own entry config:
|
|
5140
|
+
*
|
|
5141
|
+
* - the Loader merges the composition layers and the profile override before
|
|
5142
|
+
* `apply` runs, so the entry already carries the effective settings;
|
|
5143
|
+
* - a settings write is committed INTO that entry's live references without a
|
|
5144
|
+
* remount, and the Loader then announces the changed paths on this fiber
|
|
5145
|
+
* (`loader/volatile-update`) — which is the signal for refreshing
|
|
5146
|
+
* registration facts (the `editMode`-sensitive descriptions and the AST
|
|
5147
|
+
* switches), exactly as the settings docs prescribe.
|
|
5148
|
+
*
|
|
5149
|
+
* The plugin ships its own settings pages in the browser half, so it owns its
|
|
5150
|
+
* page policy: `configure({ auto: false })` tells the settings service not to
|
|
5151
|
+
* auto-generate a schema page for this entry. That call rides an optional
|
|
5152
|
+
* `ctx.inject(['settings'], ...)` child, so the plugin runs unchanged in a
|
|
5153
|
+
* deployment that composes no settings service at all.
|
|
5154
|
+
*
|
|
5155
|
+
* Mirrors the official bash-local pattern (`packages/shell/bash-local`): the
|
|
5156
|
+
* business plugin reads its own Config references and registers only its page
|
|
5157
|
+
* policy; nothing re-registers the namespace.
|
|
5103
5158
|
* @param ctx - plugin context owning the wiring.
|
|
5104
|
-
* @param entry -
|
|
5159
|
+
* @param entry - the entry's config as the Loader resolved it (live references
|
|
5160
|
+
* over the composition layers and the profile override), or a plain document.
|
|
5105
5161
|
* @param onSource - receives a thunk of the currently authoritative runtime config.
|
|
5106
5162
|
*/
|
|
5107
5163
|
function installBashPlusSettings(ctx, entry, onSource) {
|
|
5108
|
-
|
|
5164
|
+
const current = () => resolveConfig(entry);
|
|
5165
|
+
onSource(current);
|
|
5166
|
+
ctx.on("loader/volatile-update", () => {
|
|
5167
|
+
onSource(current);
|
|
5168
|
+
});
|
|
5109
5169
|
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
|
-
});
|
|
5170
|
+
settingsCtx.effect(() => settingsCtx.settings.configure({ auto: false }, ctx.fiber), "tool-plus: own settings pages");
|
|
5120
5171
|
});
|
|
5121
5172
|
}
|
|
5122
5173
|
//#endregion
|
|
@@ -5135,13 +5186,27 @@ function installBashPlusSettings(ctx, entry, onSource) {
|
|
|
5135
5186
|
/** Logical RPC channel registered by the host half of this plugin. */
|
|
5136
5187
|
const TOOL_PLUS_RPC_CHANNEL = "/tool-plus";
|
|
5137
5188
|
/**
|
|
5138
|
-
* The preset ids this plugin
|
|
5139
|
-
*
|
|
5140
|
-
*
|
|
5141
|
-
*
|
|
5189
|
+
* The preset ids this plugin **declares**. Single source of truth for both
|
|
5190
|
+
* halves: the host picks its own declarations out of the profile's composed
|
|
5191
|
+
* configuration, and the panel uses the list to tell "ours" from everything else.
|
|
5192
|
+
*
|
|
5193
|
+
* They ship as `@deepseek-ai/dsh-agent-preset` declaration rows in this
|
|
5194
|
+
* package's own bundle patches (`presets/<id>.patch.yml`), so installing the
|
|
5195
|
+
* plugin as a profile bundle declares them: no install step, no file copy, and
|
|
5196
|
+
* nothing written at startup.
|
|
5142
5197
|
*/
|
|
5143
5198
|
const BUNDLED_PRESET_IDS = ["tool-plus-standard", "tool-plus-ptc"];
|
|
5144
5199
|
/**
|
|
5200
|
+
* Loader row id a preset declaration is addressed by. A profile patch overrides
|
|
5201
|
+
* a declaration **by this id** — the harness's config editor locates the row by
|
|
5202
|
+
* id, and the override replaces the row's whole `config`.
|
|
5203
|
+
* @param presetId - The preset identity (the declaration's `config.id`).
|
|
5204
|
+
* @returns The row id as it appears in profile patches.
|
|
5205
|
+
*/
|
|
5206
|
+
function profileRowId(presetId) {
|
|
5207
|
+
return `preset-${presetId}`;
|
|
5208
|
+
}
|
|
5209
|
+
/**
|
|
5145
5210
|
* Whether one difference belongs to the "yours" group (you added or changed it)
|
|
5146
5211
|
* rather than the "behind" group (the template has it, you do not). Lives in the
|
|
5147
5212
|
* shared contract so host and client group the exact same way.
|
|
@@ -5288,8 +5353,8 @@ function parsePresetComparePayload(payload) {
|
|
|
5288
5353
|
}
|
|
5289
5354
|
/**
|
|
5290
5355
|
* 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.
|
|
5356
|
+
* `{ id: non-empty string, action: 'upgrade' | 'align' | 'revert', templateId?: non-empty string }`
|
|
5357
|
+
* is rejected, so the write endpoint never receives a half-formed request.
|
|
5293
5358
|
* @param payload - the raw RPC payload.
|
|
5294
5359
|
* @returns the narrowed payload, or undefined when invalid.
|
|
5295
5360
|
*/
|
|
@@ -5297,7 +5362,7 @@ function parsePresetActionPayload(payload) {
|
|
|
5297
5362
|
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) return void 0;
|
|
5298
5363
|
const { id, action, templateId } = payload;
|
|
5299
5364
|
if (typeof id !== "string" || id.length === 0) return void 0;
|
|
5300
|
-
if (action !== "upgrade" && action !== "
|
|
5365
|
+
if (action !== "upgrade" && action !== "align" && action !== "revert") return void 0;
|
|
5301
5366
|
if (templateId !== void 0 && (typeof templateId !== "string" || templateId.length === 0)) return void 0;
|
|
5302
5367
|
return templateId === void 0 ? {
|
|
5303
5368
|
id,
|
|
@@ -5308,26 +5373,8 @@ function parsePresetActionPayload(payload) {
|
|
|
5308
5373
|
templateId
|
|
5309
5374
|
};
|
|
5310
5375
|
}
|
|
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
5376
|
/** One RPC error result in the gateway's shape. */
|
|
5330
|
-
function failure$
|
|
5377
|
+
function failure$2(error, code = "internal") {
|
|
5331
5378
|
return {
|
|
5332
5379
|
ok: false,
|
|
5333
5380
|
error: {
|
|
@@ -5338,62 +5385,47 @@ function failure$1(error, code = "internal") {
|
|
|
5338
5385
|
};
|
|
5339
5386
|
}
|
|
5340
5387
|
/**
|
|
5341
|
-
* Answer one preset endpoint on the shared `/tool-plus`
|
|
5388
|
+
* Answer one preset endpoint on the shared `/tool-plus` route.
|
|
5342
5389
|
*
|
|
5343
5390
|
* 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.
|
|
5391
|
+
* (the single route handler) can keep serving its own families.
|
|
5392
|
+
* @param ctx - context forwarded to the injected host implementation.
|
|
5393
|
+
* @param deps - injected host implementation.
|
|
5349
5394
|
* @param endpoint - the requested endpoint name.
|
|
5350
5395
|
* @param payload - the raw RPC payload.
|
|
5351
5396
|
* @returns the RPC result, or undefined when the endpoint is not ours.
|
|
5352
5397
|
*/
|
|
5353
5398
|
async function handlePresetEndpoint(ctx, deps, endpoint, payload) {
|
|
5354
5399
|
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
5400
|
if (endpoint === "presets/status") try {
|
|
5358
|
-
const roster = await service.list();
|
|
5359
5401
|
return {
|
|
5360
5402
|
ok: true,
|
|
5361
|
-
value:
|
|
5362
|
-
presets: deps.listStatuses(roster.map(toRosterEntry)),
|
|
5363
|
-
templates: deps.listTemplates()
|
|
5364
|
-
}
|
|
5403
|
+
value: await deps.listPresets(ctx)
|
|
5365
5404
|
};
|
|
5366
5405
|
} catch (error) {
|
|
5367
|
-
return failure$
|
|
5406
|
+
return failure$2(error);
|
|
5368
5407
|
}
|
|
5369
5408
|
if (endpoint === "presets/compare") {
|
|
5370
5409
|
const parsed = parsePresetComparePayload(payload);
|
|
5371
|
-
if (!parsed) return failure$
|
|
5410
|
+
if (!parsed) return failure$2("presets/compare expects { presetId: string, templateId: string }", "bad-request");
|
|
5372
5411
|
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
5412
|
return {
|
|
5377
5413
|
ok: true,
|
|
5378
|
-
value:
|
|
5379
|
-
presetId: parsed.presetId,
|
|
5380
|
-
templateId: parsed.templateId,
|
|
5381
|
-
...deps.comparePreset(preset, parsed.templateId)
|
|
5382
|
-
}
|
|
5414
|
+
value: await deps.comparePreset(ctx, parsed.presetId, parsed.templateId)
|
|
5383
5415
|
};
|
|
5384
5416
|
} catch (error) {
|
|
5385
|
-
return failure$
|
|
5417
|
+
return failure$2(error);
|
|
5386
5418
|
}
|
|
5387
5419
|
}
|
|
5388
5420
|
const parsed = parsePresetActionPayload(payload);
|
|
5389
|
-
if (!parsed) return failure$
|
|
5421
|
+
if (!parsed) return failure$2("presets/apply expects { id: string, action: \"upgrade\" | \"align\" | \"revert\", templateId?: string }", "bad-request");
|
|
5390
5422
|
try {
|
|
5391
5423
|
return {
|
|
5392
5424
|
ok: true,
|
|
5393
|
-
value: deps.applyAction(parsed.id, parsed.action, parsed.templateId)
|
|
5425
|
+
value: await deps.applyAction(ctx, parsed.id, parsed.action, parsed.templateId)
|
|
5394
5426
|
};
|
|
5395
5427
|
} catch (error) {
|
|
5396
|
-
return failure$
|
|
5428
|
+
return failure$2(error);
|
|
5397
5429
|
}
|
|
5398
5430
|
}
|
|
5399
5431
|
//#endregion
|
|
@@ -5511,7 +5543,8 @@ async function queryRmSafeStatus(deps) {
|
|
|
5511
5543
|
*
|
|
5512
5544
|
* **Why this registers its own route instead of calling `connection.rpc.handle`.**
|
|
5513
5545
|
* The documented helper is unusable from a plugin in a real web deployment on
|
|
5514
|
-
* dsh 0.1.5-rc.1
|
|
5546
|
+
* dsh 0.1.5-rc.1 — and still on 0.1.7-rc.1, re-checked for this migration:
|
|
5547
|
+
* `HostConnectionService.rpc` builds its handle against
|
|
5515
5548
|
* `owner = this.ctx` (`rpc-host.ts: "Generic channel registry scoped to the
|
|
5516
5549
|
* Context reading this service"`), and `register()` then evaluates
|
|
5517
5550
|
* `owner.webServer.register(route)`. The client-connection row declares
|
|
@@ -5542,7 +5575,7 @@ async function queryRmSafeStatus(deps) {
|
|
|
5542
5575
|
/** Cap on one request body; every legitimate payload here is a few hundred bytes. */
|
|
5543
5576
|
const MAX_BODY_BYTES = 1048576;
|
|
5544
5577
|
/** One internal error result in the gateway's shape. */
|
|
5545
|
-
function failure(error) {
|
|
5578
|
+
function failure$1(error) {
|
|
5546
5579
|
return {
|
|
5547
5580
|
ok: false,
|
|
5548
5581
|
error: {
|
|
@@ -5626,7 +5659,7 @@ function installBrowserProbeRpc(ctx, deps) {
|
|
|
5626
5659
|
value: { found: probeBrowsers() }
|
|
5627
5660
|
};
|
|
5628
5661
|
} catch (error) {
|
|
5629
|
-
return failure(error);
|
|
5662
|
+
return failure$1(error);
|
|
5630
5663
|
}
|
|
5631
5664
|
if (endpoint === "rmSafe/status") {
|
|
5632
5665
|
if (!deps.getRmSafe()) return {
|
|
@@ -5647,7 +5680,7 @@ function installBrowserProbeRpc(ctx, deps) {
|
|
|
5647
5680
|
})
|
|
5648
5681
|
};
|
|
5649
5682
|
} catch (error) {
|
|
5650
|
-
return failure(error);
|
|
5683
|
+
return failure$1(error);
|
|
5651
5684
|
}
|
|
5652
5685
|
}
|
|
5653
5686
|
if (deps.presets !== void 0) {
|
|
@@ -5733,772 +5766,295 @@ function installBrowserProbeRpc(ctx, deps) {
|
|
|
5733
5766
|
};
|
|
5734
5767
|
}
|
|
5735
5768
|
//#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);
|
|
5769
|
+
//#region src/presets/rows.ts
|
|
5770
|
+
/** 行 id 的取值(非空字符串才认,与官方对 `name` 的要求同款)。 */
|
|
5771
|
+
function rowId(row) {
|
|
5772
|
+
return typeof row.id === "string" && row.id.length > 0 ? row.id : void 0;
|
|
5805
5773
|
}
|
|
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;
|
|
5866
|
-
}
|
|
5867
|
-
/** 逐字节重建(`splitLines` 的逆运算)。 */
|
|
5868
|
-
function joinLines(lines) {
|
|
5869
|
-
return lines.map((line) => line.text + line.eol).join("");
|
|
5774
|
+
/** 行模块名(官方 entry-list 要求每行都有非空 `name`)。 */
|
|
5775
|
+
function rowName(row) {
|
|
5776
|
+
return typeof row.name === "string" && row.name.length > 0 ? row.name : void 0;
|
|
5870
5777
|
}
|
|
5871
|
-
/**
|
|
5872
|
-
function
|
|
5873
|
-
|
|
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;
|
|
5778
|
+
/** 这一行是不是 group 载体(group 的子行在 `config` 里)。 */
|
|
5779
|
+
function isGroupRow(row) {
|
|
5780
|
+
return row.group === true && Array.isArray(row.config);
|
|
5888
5781
|
}
|
|
5889
5782
|
/**
|
|
5890
|
-
*
|
|
5891
|
-
*
|
|
5892
|
-
*
|
|
5893
|
-
*
|
|
5783
|
+
* 判定一份 `plugins` 值是不是合法的顶层行列表。规则与官方
|
|
5784
|
+
* `entryListProblem` 一致:必须是数组,每一项必须是带非空 `name` 的映射。
|
|
5785
|
+
* @param value - 待判定的值(来自宿主配置或测试注入)。
|
|
5786
|
+
* @returns 合法时为 true。
|
|
5894
5787
|
*/
|
|
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";
|
|
5788
|
+
function isRowList(value) {
|
|
5789
|
+
if (!Array.isArray(value)) return false;
|
|
5790
|
+
return value.every((row) => typeof row === "object" && row !== null && !Array.isArray(row) && rowName(row) !== void 0);
|
|
5901
5791
|
}
|
|
5902
|
-
/**
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5792
|
+
/**
|
|
5793
|
+
* 展平一棵行树(含 group 嵌套),键为 `父/子` 形式的路径。
|
|
5794
|
+
*
|
|
5795
|
+
* 路径是**给面板看的标识**,也用于我们自己的差异比较;官方
|
|
5796
|
+
* `compositionInventory()` 回的行没有路径(它压平后只剩 entryId/moduleName),
|
|
5797
|
+
* 所以需要分组结构时只能从声明本身取。
|
|
5798
|
+
* @param rows - 顶层行列表。
|
|
5799
|
+
* @param prefix - 递归时的父路径。
|
|
5800
|
+
* @returns 路径 → 行。
|
|
5801
|
+
*/
|
|
5802
|
+
function flattenRows(rows, prefix = "") {
|
|
5803
|
+
const out = /* @__PURE__ */ new Map();
|
|
5804
|
+
for (const row of rows) {
|
|
5805
|
+
const id = rowId(row);
|
|
5806
|
+
if (id === void 0) continue;
|
|
5807
|
+
const path = prefix.length > 0 ? `${prefix}/${id}` : id;
|
|
5808
|
+
out.set(path, row);
|
|
5809
|
+
if (isGroupRow(row)) for (const [nested, nestedRow] of flattenRows(row.config, path)) out.set(nested, nestedRow);
|
|
5914
5810
|
}
|
|
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
|
-
});
|
|
5811
|
+
return out;
|
|
5938
5812
|
}
|
|
5939
5813
|
/**
|
|
5940
|
-
*
|
|
5941
|
-
*
|
|
5814
|
+
* `!!js` 表达式标记:entry-list 方言把它解析成 `{ __jsExpr: <源码> }`(见
|
|
5815
|
+
* `vendor/include/src/index.ts` 的 `JsExpr`)。它对我们是**标量**——一整段待求值的
|
|
5816
|
+
* 源码,不是可以逐键比较的结构。漏了这一步会造出 `disabled.__jsExpr` 这种假叶子,
|
|
5817
|
+
* 顺带让"冲突行的 disabled 归 conflicts 管"这条跳过规则失效。
|
|
5818
|
+
* @param value - 待判定的值。
|
|
5819
|
+
* @returns 是表达式标记时为 true。
|
|
5942
5820
|
*/
|
|
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
|
-
}
|
|
5821
|
+
function isJsExpr(value) {
|
|
5822
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 1 && typeof value.__jsExpr === "string";
|
|
5950
5823
|
}
|
|
5951
|
-
/**
|
|
5952
|
-
function
|
|
5953
|
-
|
|
5954
|
-
if (
|
|
5955
|
-
return
|
|
5824
|
+
/** 规范化一个标量用于比较与展示。 */
|
|
5825
|
+
function canonical(value) {
|
|
5826
|
+
if (value === void 0) return "";
|
|
5827
|
+
if (isJsExpr(value)) return `!!js ${value.__jsExpr}`;
|
|
5828
|
+
return JSON.stringify(value) ?? String(value);
|
|
5956
5829
|
}
|
|
5957
5830
|
/**
|
|
5958
|
-
*
|
|
5831
|
+
* 收集一行自己的叶子路径(不含 `id`,不含 group 的子行)。
|
|
5959
5832
|
*
|
|
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
|
|
5833
|
+
* 用规范化 JSON 而不是 `String()`:Loader 对 `disabled` 的语义是
|
|
5834
|
+
* "布尔取 Boolean(value)",所以字符串 `"false"` 与布尔 `false` **语义相反**;
|
|
5835
|
+
* 两者若都显示成 `false`,面板就会把一处真实差异藏起来。
|
|
5836
|
+
* @param row - 目标行。
|
|
5837
|
+
* @param base - 递归时的前缀。
|
|
5838
|
+
* @returns 该行的叶子清单(顺序为键的声明顺序)。
|
|
5839
|
+
*/
|
|
5840
|
+
function rowLeaves(row, base = "") {
|
|
5841
|
+
const out = [];
|
|
5842
|
+
const walk = (value, path) => {
|
|
5843
|
+
if (isJsExpr(value)) {
|
|
5844
|
+
out.push({
|
|
5845
|
+
path,
|
|
5846
|
+
value: canonical(value)
|
|
6010
5847
|
});
|
|
6011
|
-
|
|
5848
|
+
return;
|
|
6012
5849
|
}
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
5850
|
+
if (Array.isArray(value)) {
|
|
5851
|
+
value.forEach((item, index) => walk(item, `${path}[${index}]`));
|
|
5852
|
+
return;
|
|
5853
|
+
}
|
|
5854
|
+
if (value !== null && typeof value === "object") {
|
|
5855
|
+
for (const [key, nested] of Object.entries(value)) walk(nested, path.length > 0 ? `${path}.${key}` : key);
|
|
5856
|
+
return;
|
|
5857
|
+
}
|
|
5858
|
+
out.push({
|
|
5859
|
+
path,
|
|
5860
|
+
value: canonical(value)
|
|
6019
5861
|
});
|
|
5862
|
+
};
|
|
5863
|
+
for (const [key, value] of Object.entries(row)) {
|
|
5864
|
+
if (key === "id") continue;
|
|
5865
|
+
if (key === "config" && isGroupRow(row)) continue;
|
|
5866
|
+
walk(value, base.length > 0 ? `${base}.${key}` : key);
|
|
6020
5867
|
}
|
|
6021
|
-
return
|
|
5868
|
+
return out;
|
|
5869
|
+
}
|
|
5870
|
+
/** 规范化的深比较(两侧都是 YAML/JSON 值,无函数与原型差异)。 */
|
|
5871
|
+
function sameValue(a, b) {
|
|
5872
|
+
return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
6022
5873
|
}
|
|
5874
|
+
//#endregion
|
|
5875
|
+
//#region src/presets/conflicts.ts
|
|
6023
5876
|
/**
|
|
6024
|
-
*
|
|
5877
|
+
* 冲突不变式:一个 preset「接上了本插件的工具」当且仅当官方那 4 个工具行
|
|
5878
|
+
* 处于**不存在**或 **`disabled: true`** 状态。
|
|
6025
5879
|
*
|
|
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
|
-
};
|
|
5880
|
+
* 原因(这套设计的地基,别丢):`@xiaoso/dsh-tool-plus` 作为包在**宿主面**
|
|
5881
|
+
* 加载时,它自己的 profile 补丁已经禁用了这 4 行并插入**一行全局** `tool-plus`;
|
|
5882
|
+
* 注册表按 scope 分层、就近优先,所以宿主面那一份已经服务所有会话。若某个
|
|
5883
|
+
* preset 又在 **agent 面**挂一份同名工具行,就会用**第二个 `apply()` 影子实例**
|
|
5884
|
+
* 盖掉健康的宿主实例,而那个实例的 settings 注入永不触发(cfg 退化成
|
|
5885
|
+
* replace-mode 读取 —— 2026-08-25 hashline 事故的根因)。
|
|
5886
|
+
*
|
|
5887
|
+
* 与旧文本实现的唯一区别是**载体**:以前逐行读 `agent.cordis.yml` 的文本,
|
|
5888
|
+
* 现在直接看宿主交给我们的行对象。判定规则一字不改:
|
|
5889
|
+
* - 只认**顶层**行;group 里嵌套的同名行不碰;
|
|
5890
|
+
* - `disabled` 严格等于布尔 `true` 才算已处理 —— 缺席、`false`、字符串、
|
|
5891
|
+
* `!!js` 表达式统统算冲突(`!!js` 在挂载时才求值,我们要的是无条件关闭,
|
|
5892
|
+
* 所以和旧实现一样整值翻成 `true`);
|
|
5893
|
+
* - 输出恒为新数组,输入不变;结果与输入逐值相同时 `changes` 为空(调用方以
|
|
5894
|
+
* 此判定"不写盘")。
|
|
5895
|
+
* @module @xiaoso/dsh-tool-plus/presets/conflicts
|
|
5896
|
+
*/
|
|
5897
|
+
/** 官方 standard / ptc / cordis 会挂的 4 个工具行 id。 */
|
|
5898
|
+
const PRESET_CONFLICT_IDS = [
|
|
5899
|
+
"tool-bash",
|
|
5900
|
+
"tool-pwsh",
|
|
5901
|
+
"tool-fs",
|
|
5902
|
+
"tool-fs-search"
|
|
5903
|
+
];
|
|
5904
|
+
/** 该行是否已被无条件关闭(只有布尔 `true` 算)。 */
|
|
5905
|
+
function disabled(row) {
|
|
5906
|
+
return row.disabled === true;
|
|
5907
|
+
}
|
|
5908
|
+
/** 顶层行里按 id 找第一行(只看顶层,与不变式一致)。 */
|
|
5909
|
+
function topRow(rows, id) {
|
|
5910
|
+
return rows.find((row) => rowId(row) === id);
|
|
6081
5911
|
}
|
|
6082
5912
|
/**
|
|
6083
|
-
*
|
|
6084
|
-
* @param
|
|
6085
|
-
* @param conflictIds -
|
|
6086
|
-
* @returns
|
|
5913
|
+
* 只读分析一份 `plugins` 值。
|
|
5914
|
+
* @param plugins - 声明的子插件列表(来自宿主配置,形状未知)。
|
|
5915
|
+
* @param conflictIds - 冲突行清单,默认 {@link PRESET_CONFLICT_IDS}。
|
|
5916
|
+
* @returns 冲突清单、是否干净、形状是否可识别。
|
|
6087
5917
|
*/
|
|
6088
|
-
function
|
|
6089
|
-
|
|
6090
|
-
const shape = detectShape(lines);
|
|
6091
|
-
if (shape === "unrecognized") return {
|
|
5918
|
+
function analyzePlugins(plugins, conflictIds = PRESET_CONFLICT_IDS) {
|
|
5919
|
+
if (!isRowList(plugins)) return {
|
|
6092
5920
|
conflicts: [],
|
|
6093
5921
|
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";
|
|
5922
|
+
shape: "unrecognized"
|
|
5923
|
+
};
|
|
5924
|
+
const conflicts = conflictIds.filter((id) => {
|
|
5925
|
+
const row = topRow(plugins, id);
|
|
5926
|
+
return row !== void 0 && !disabled(row);
|
|
6102
5927
|
});
|
|
6103
5928
|
return {
|
|
6104
5929
|
conflicts,
|
|
6105
5930
|
clean: conflicts.length === 0,
|
|
6106
|
-
shape
|
|
5931
|
+
shape: "ok"
|
|
6107
5932
|
};
|
|
6108
5933
|
}
|
|
6109
|
-
/** 行内 `key: value`(顶层键;嵌套子键缩进更大,不在这里取)。 */
|
|
6110
|
-
const ROW_KEY = /^[ \t]*([^#\s:][^:]*):[ \t]*(.*)$/;
|
|
6111
|
-
/** 去掉行尾注释与引号,得到可比的值。 */
|
|
6112
|
-
function leafValue(text) {
|
|
6113
|
-
return stripQuotes(text.replace(/[ \t]+#.*$/, "").trim());
|
|
6114
|
-
}
|
|
6115
5934
|
/**
|
|
6116
|
-
*
|
|
6117
|
-
*
|
|
5935
|
+
* 把那 4 个仍挂着的冲突行翻成 `disabled: true`(不改别的行、不删行)。
|
|
5936
|
+
* @param plugins - 合法顶层行列表。
|
|
5937
|
+
* @param conflictIds - 冲突行清单,默认 {@link PRESET_CONFLICT_IDS}。
|
|
5938
|
+
* @returns 新列表与实际改动清单(无改动时 `changes` 为空且列表为原引用)。
|
|
6118
5939
|
*/
|
|
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
|
|
5940
|
+
function disableConflicts(plugins, conflictIds = PRESET_CONFLICT_IDS) {
|
|
5941
|
+
const changes = [];
|
|
5942
|
+
let touched = false;
|
|
5943
|
+
const next = plugins.map((row) => {
|
|
5944
|
+
const id = rowId(row);
|
|
5945
|
+
if (id === void 0 || !conflictIds.includes(id) || disabled(row)) return row;
|
|
5946
|
+
changes.push({
|
|
5947
|
+
id,
|
|
5948
|
+
action: Object.hasOwn(row, "disabled") ? "flipped" : "disabled"
|
|
6166
5949
|
});
|
|
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
|
-
}
|
|
5950
|
+
touched = true;
|
|
6194
5951
|
return {
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
leaves: walkRowLeaves(lines, row)
|
|
5952
|
+
...row,
|
|
5953
|
+
disabled: true
|
|
6198
5954
|
};
|
|
6199
5955
|
});
|
|
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}`,
|
|
5956
|
+
if (!touched) return {
|
|
5957
|
+
plugins,
|
|
6329
5958
|
changes: []
|
|
6330
5959
|
};
|
|
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);
|
|
5960
|
+
changes.sort((a, b) => conflictIds.indexOf(a.id) - conflictIds.indexOf(b.id));
|
|
6339
5961
|
return {
|
|
6340
|
-
|
|
6341
|
-
changed: true,
|
|
6342
|
-
backupPath,
|
|
5962
|
+
plugins: next,
|
|
6343
5963
|
changes
|
|
6344
5964
|
};
|
|
6345
5965
|
}
|
|
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
5966
|
//#endregion
|
|
6410
5967
|
//#region src/presets/delta.ts
|
|
6411
5968
|
/**
|
|
6412
|
-
*
|
|
5969
|
+
* 差异清单:只读地说清"这份 preset 与另一份(通常是随包声明)差在哪"。
|
|
6413
5970
|
*
|
|
6414
5971
|
* 为什么需要它:`templateDiffers: boolean` 只能说"不一样",而面板上紧挨着的
|
|
6415
|
-
* 是"
|
|
6416
|
-
*
|
|
6417
|
-
*
|
|
5972
|
+
* 是"冲突行都已禁用、无需调整"。两句都对,读起来却打架 —— 前者说的是
|
|
5973
|
+
* **内容被改过**,后者说的是**我们自己那 4 个冲突行**。把差异逐条列出来,
|
|
5974
|
+
* 用户才不用猜"到底哪里不同、对齐会不会动它"。
|
|
6418
5975
|
*
|
|
6419
|
-
*
|
|
6420
|
-
*
|
|
6421
|
-
*
|
|
6422
|
-
*
|
|
6423
|
-
*
|
|
5976
|
+
* 契约(自旧文本实现原样继承,语义未变):
|
|
5977
|
+
* - 比**展开到叶子的点号路径**(`config.thresholdRatio`、`config.models[0].id`)——
|
|
5978
|
+
* 真实漂移常藏在嵌套键里,只比顶层键会得出"没有差异"的错误结论;
|
|
5979
|
+
* - 冲突行的 `disabled` 由 `conflicts` 负责,不在差异里重复报;
|
|
5980
|
+
* - 形状不可识别 → 空清单,绝不猜;
|
|
5981
|
+
* - 顺序 = 你这份的行序在前(含 group 内子行),对方独有的行补在后;
|
|
5982
|
+
* - `total` 是全量,`items` 可被 `limit` 截断(面板据此说"等 N 项")。
|
|
6424
5983
|
* @module @xiaoso/dsh-tool-plus/presets/delta
|
|
6425
5984
|
*/
|
|
6426
|
-
/**
|
|
5985
|
+
/** 默认最多列几条(再多就该去看配置本身了)。 */
|
|
6427
5986
|
const DEFAULT_LIMIT$1 = 6;
|
|
6428
|
-
/**
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
5987
|
+
/** 空结果(任一侧形状不可识别时)。 */
|
|
5988
|
+
const EMPTY = {
|
|
5989
|
+
items: [],
|
|
5990
|
+
total: 0,
|
|
5991
|
+
yoursCount: 0,
|
|
5992
|
+
behindCount: 0
|
|
5993
|
+
};
|
|
5994
|
+
/** 某个叶子的 `disabled` 是不是"我们自己的冲突行开关"——那属于 conflicts 的话题。 */
|
|
5995
|
+
function isConflictSwitch(rowPath, leafPath, conflictIds) {
|
|
5996
|
+
return leafPath === "disabled" && !rowPath.includes("/") && conflictIds.includes(rowPath);
|
|
6435
5997
|
}
|
|
6436
5998
|
/**
|
|
6437
|
-
*
|
|
6438
|
-
* @param
|
|
6439
|
-
* @param
|
|
5999
|
+
* 比较两份 `config.plugins`,列出差异(只读,不改任何值)。
|
|
6000
|
+
* @param yours - 你这一侧的 plugins(形状未知)。
|
|
6001
|
+
* @param theirs - 对方的 plugins(形状未知)。
|
|
6440
6002
|
* @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
|
-
};
|
|
6003
|
+
* @returns 差异清单;两侧任一形状不可识别时为空清单。
|
|
6004
|
+
*/
|
|
6005
|
+
function presetDelta(yours, theirs, options = {}) {
|
|
6006
|
+
if (!isRowList(yours) || !isRowList(theirs)) return EMPTY;
|
|
6452
6007
|
const conflictIds = options.conflictIds ?? PRESET_CONFLICT_IDS;
|
|
6453
6008
|
const limit = options.limit ?? DEFAULT_LIMIT$1;
|
|
6454
|
-
const
|
|
6455
|
-
const
|
|
6009
|
+
const mine = flattenRows(yours);
|
|
6010
|
+
const other = flattenRows(theirs);
|
|
6456
6011
|
const items = [];
|
|
6457
|
-
|
|
6458
|
-
|
|
6012
|
+
const leavesOf = (row) => new Map(rowLeaves(row).map((leaf) => [leaf.path, leaf.value]));
|
|
6013
|
+
for (const [path, row] of mine) {
|
|
6014
|
+
const counterpart = other.get(path);
|
|
6459
6015
|
if (counterpart === void 0) {
|
|
6460
6016
|
items.push({
|
|
6461
6017
|
kind: "row-only-yours",
|
|
6462
|
-
row:
|
|
6018
|
+
row: path,
|
|
6463
6019
|
path: ""
|
|
6464
6020
|
});
|
|
6465
6021
|
continue;
|
|
6466
6022
|
}
|
|
6467
|
-
const
|
|
6468
|
-
const
|
|
6469
|
-
for (const [
|
|
6470
|
-
if (isConflictSwitch(
|
|
6471
|
-
if (!
|
|
6023
|
+
const a = leavesOf(row);
|
|
6024
|
+
const b = leavesOf(counterpart);
|
|
6025
|
+
for (const [leaf, value] of a) {
|
|
6026
|
+
if (isConflictSwitch(path, leaf, conflictIds)) continue;
|
|
6027
|
+
if (!b.has(leaf)) {
|
|
6472
6028
|
items.push({
|
|
6473
6029
|
kind: "only-yours",
|
|
6474
|
-
row:
|
|
6475
|
-
path,
|
|
6030
|
+
row: path,
|
|
6031
|
+
path: leaf,
|
|
6476
6032
|
yours: value
|
|
6477
6033
|
});
|
|
6478
6034
|
continue;
|
|
6479
6035
|
}
|
|
6480
|
-
const
|
|
6481
|
-
if (
|
|
6036
|
+
const theirsValue = b.get(leaf) ?? "";
|
|
6037
|
+
if (theirsValue !== value) items.push({
|
|
6482
6038
|
kind: "changed",
|
|
6483
|
-
row:
|
|
6484
|
-
path,
|
|
6039
|
+
row: path,
|
|
6040
|
+
path: leaf,
|
|
6485
6041
|
yours: value,
|
|
6486
|
-
template:
|
|
6042
|
+
template: theirsValue
|
|
6487
6043
|
});
|
|
6488
6044
|
}
|
|
6489
|
-
for (const [
|
|
6490
|
-
if (isConflictSwitch(
|
|
6491
|
-
if (!
|
|
6045
|
+
for (const [leaf, value] of b) {
|
|
6046
|
+
if (isConflictSwitch(path, leaf, conflictIds)) continue;
|
|
6047
|
+
if (!a.has(leaf)) items.push({
|
|
6492
6048
|
kind: "only-template",
|
|
6493
|
-
row:
|
|
6494
|
-
path,
|
|
6049
|
+
row: path,
|
|
6050
|
+
path: leaf,
|
|
6495
6051
|
template: value
|
|
6496
6052
|
});
|
|
6497
6053
|
}
|
|
6498
6054
|
}
|
|
6499
|
-
for (const
|
|
6055
|
+
for (const path of other.keys()) if (!mine.has(path)) items.push({
|
|
6500
6056
|
kind: "row-only-template",
|
|
6501
|
-
row:
|
|
6057
|
+
row: path,
|
|
6502
6058
|
path: ""
|
|
6503
6059
|
});
|
|
6504
6060
|
const yoursCount = items.filter((item) => isYoursDelta(item.kind)).length;
|
|
@@ -6510,99 +6066,19 @@ function presetDelta(userText, templateText, options = {}) {
|
|
|
6510
6066
|
};
|
|
6511
6067
|
}
|
|
6512
6068
|
//#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
6069
|
//#region src/presets/compare.ts
|
|
6590
6070
|
/**
|
|
6591
|
-
* 预设比较:把"
|
|
6071
|
+
* 预设比较:把"某份 preset 生效的 plugins"和"某个随包声明的 plugins"放在一起看。
|
|
6592
6072
|
*
|
|
6593
|
-
*
|
|
6594
|
-
*
|
|
6595
|
-
*
|
|
6596
|
-
*
|
|
6597
|
-
*
|
|
6598
|
-
* "
|
|
6073
|
+
* 面板是两块信息、两个状态点:
|
|
6074
|
+
* ①**工具行**:这份 preset 里那 4 个冲突行都禁用了吗(=`conflicts`);
|
|
6075
|
+
* ②**完整比较**:内容上和选中的模板差多少(=`items`,弹窗里逐条展示)。
|
|
6076
|
+
* 只读,绝不写盘。比较一律**不跳过**冲突行的 `disabled` —— 第二个点要的是
|
|
6077
|
+
* "完整的比较",工具开关的不同也必须出现在差异清单里,否则会出现"说内容一致、
|
|
6078
|
+
* 却说工具行没配好"这种自相矛盾的读数。
|
|
6599
6079
|
* @module @xiaoso/dsh-tool-plus/presets/compare
|
|
6600
6080
|
*/
|
|
6601
|
-
/**
|
|
6602
|
-
function templatesRoot(deps) {
|
|
6603
|
-
return path$1.join(packageRootDir(deps), "presets");
|
|
6604
|
-
}
|
|
6605
|
-
/** 读不到/不可识别时的统一结果(面板渲染红点,不抛)。 */
|
|
6081
|
+
/** 任一侧不可比较时的统一结果(面板渲染红点,不抛)。 */
|
|
6606
6082
|
const UNREADABLE_COMPARISON = {
|
|
6607
6083
|
status: "unreadable",
|
|
6608
6084
|
conflicts: [],
|
|
@@ -6611,56 +6087,17 @@ const UNREADABLE_COMPARISON = {
|
|
|
6611
6087
|
behindCount: 0,
|
|
6612
6088
|
items: []
|
|
6613
6089
|
};
|
|
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
6090
|
/**
|
|
6655
|
-
*
|
|
6656
|
-
* @param
|
|
6657
|
-
* @param
|
|
6091
|
+
* 比较两份 `config.plugins`(纯函数,只读)。
|
|
6092
|
+
* @param presetPlugins - 所选 preset 生效的 plugins。
|
|
6093
|
+
* @param templatePlugins - 所选模板的 plugins(随包声明)。
|
|
6658
6094
|
* @returns 两个状态点与弹窗需要的全部数据。
|
|
6659
6095
|
*/
|
|
6660
|
-
function
|
|
6661
|
-
|
|
6096
|
+
function comparePlugins(presetPlugins, templatePlugins) {
|
|
6097
|
+
if (!isRowList(presetPlugins) || !isRowList(templatePlugins)) return UNREADABLE_COMPARISON;
|
|
6098
|
+
const analysis = analyzePlugins(presetPlugins);
|
|
6662
6099
|
if (analysis.shape === "unrecognized") return UNREADABLE_COMPARISON;
|
|
6663
|
-
const delta = presetDelta(
|
|
6100
|
+
const delta = presetDelta(presetPlugins, templatePlugins, {
|
|
6664
6101
|
conflictIds: [],
|
|
6665
6102
|
limit: Number.MAX_SAFE_INTEGER
|
|
6666
6103
|
});
|
|
@@ -6673,19 +6110,420 @@ function compareCompositions(presetText, templateText) {
|
|
|
6673
6110
|
items: delta.items
|
|
6674
6111
|
};
|
|
6675
6112
|
}
|
|
6113
|
+
//#endregion
|
|
6114
|
+
//#region src/presets/plan.ts
|
|
6115
|
+
/** 生效的 plugins(覆盖优先)。 */
|
|
6116
|
+
function effectivePlugins$1(input) {
|
|
6117
|
+
const plugins = input.effective?.plugins;
|
|
6118
|
+
return isRowList(plugins) ? plugins : void 0;
|
|
6119
|
+
}
|
|
6120
|
+
/**
|
|
6121
|
+
* 判定一个动作该不该写、会改什么。
|
|
6122
|
+
* @param input - 判定所需的事实。
|
|
6123
|
+
* @returns 计划;`noop` 时调用方**不要**调用 `configEditor.edit`。
|
|
6124
|
+
*/
|
|
6125
|
+
function planPresetAction(input) {
|
|
6126
|
+
if (input.action === "revert") {
|
|
6127
|
+
if (!input.customized) return {
|
|
6128
|
+
kind: "noop",
|
|
6129
|
+
reason: "this preset carries no profile override to remove"
|
|
6130
|
+
};
|
|
6131
|
+
return { kind: "revert" };
|
|
6132
|
+
}
|
|
6133
|
+
if (input.action === "align") {
|
|
6134
|
+
if (!input.ours) return {
|
|
6135
|
+
kind: "noop",
|
|
6136
|
+
reason: "only presets this plugin declares can be aligned"
|
|
6137
|
+
};
|
|
6138
|
+
if (input.templateId === void 0) return {
|
|
6139
|
+
kind: "noop",
|
|
6140
|
+
reason: "align needs a template id"
|
|
6141
|
+
};
|
|
6142
|
+
if (!isRowList(input.templatePlugins)) return {
|
|
6143
|
+
kind: "noop",
|
|
6144
|
+
reason: `template ${input.templateId} has no readable plugin list`
|
|
6145
|
+
};
|
|
6146
|
+
if (sameValue(input.templatePlugins, input.inherited?.plugins)) {
|
|
6147
|
+
if (!input.customized) return {
|
|
6148
|
+
kind: "noop",
|
|
6149
|
+
reason: "this preset already matches the bundled declaration"
|
|
6150
|
+
};
|
|
6151
|
+
return { kind: "revert" };
|
|
6152
|
+
}
|
|
6153
|
+
if (sameValue(input.templatePlugins, input.effective?.plugins)) return {
|
|
6154
|
+
kind: "noop",
|
|
6155
|
+
reason: `this preset already matches template ${input.templateId}`
|
|
6156
|
+
};
|
|
6157
|
+
return { kind: "align" };
|
|
6158
|
+
}
|
|
6159
|
+
const plugins = effectivePlugins$1(input);
|
|
6160
|
+
if (plugins === void 0) return {
|
|
6161
|
+
kind: "noop",
|
|
6162
|
+
reason: "the plugin list is not a readable row list, nothing was written"
|
|
6163
|
+
};
|
|
6164
|
+
if (analyzePlugins(plugins).conflicts.length === 0) return {
|
|
6165
|
+
kind: "noop",
|
|
6166
|
+
reason: "no official tool row needs disabling"
|
|
6167
|
+
};
|
|
6168
|
+
const { changes } = disableConflicts(plugins);
|
|
6169
|
+
if (changes.length === 0) return {
|
|
6170
|
+
kind: "noop",
|
|
6171
|
+
reason: "no official tool row needs disabling"
|
|
6172
|
+
};
|
|
6173
|
+
return {
|
|
6174
|
+
kind: "upgrade",
|
|
6175
|
+
changes
|
|
6176
|
+
};
|
|
6177
|
+
}
|
|
6178
|
+
//#endregion
|
|
6179
|
+
//#region src/presets/status.ts
|
|
6180
|
+
/** 生效的 plugins:宿主的生效 config 优先,回落到覆盖层、再回落到随包声明。 */
|
|
6181
|
+
function effectivePlugins(facts) {
|
|
6182
|
+
if (facts.effective?.plugins !== void 0) return facts.effective.plugins;
|
|
6183
|
+
const overridePlugins = facts.override?.plugins;
|
|
6184
|
+
if (overridePlugins !== void 0) return overridePlugins;
|
|
6185
|
+
return facts.inherited?.plugins;
|
|
6186
|
+
}
|
|
6187
|
+
/** 覆盖里是不是真带了内容(`{}` 表示没有覆盖行)。 */
|
|
6188
|
+
function isCustomized(facts) {
|
|
6189
|
+
return facts.override !== void 0 && Object.keys(facts.override).length > 0;
|
|
6190
|
+
}
|
|
6191
|
+
/**
|
|
6192
|
+
* 把一个 preset 折成面板状态。
|
|
6193
|
+
* @param facts - 宿主收集的原始事实。
|
|
6194
|
+
* @param templateIds - 随包声明的模板清单(用于"与随包声明是否一致")。
|
|
6195
|
+
* @returns 面板状态行。
|
|
6196
|
+
*/
|
|
6197
|
+
function toPresetStatus(facts, templateIds) {
|
|
6198
|
+
const plugins = effectivePlugins(facts);
|
|
6199
|
+
const analysis = analyzePlugins(plugins);
|
|
6200
|
+
const customized = isCustomized(facts);
|
|
6201
|
+
const rows = isRowList(plugins) ? flattenRows(plugins).size : 0;
|
|
6202
|
+
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;
|
|
6203
|
+
const templateDelta = templateDiffers ? presetDelta(plugins, facts.inherited?.plugins) : void 0;
|
|
6204
|
+
const delta = templateDelta !== void 0 && templateDelta.total > 0 ? templateDelta : void 0;
|
|
6205
|
+
return {
|
|
6206
|
+
id: facts.id,
|
|
6207
|
+
...facts.entryId === void 0 ? {} : { entryId: facts.entryId },
|
|
6208
|
+
...facts.name === void 0 ? {} : { name: facts.name },
|
|
6209
|
+
...facts.description === void 0 ? {} : { description: facts.description },
|
|
6210
|
+
source: facts.ours ? "ours" : "other",
|
|
6211
|
+
isDefault: facts.isDefault,
|
|
6212
|
+
conflicts: analysis.conflicts,
|
|
6213
|
+
clean: analysis.clean,
|
|
6214
|
+
unrecognized: analysis.shape === "unrecognized",
|
|
6215
|
+
...facts.broken === void 0 ? {} : { broken: facts.broken },
|
|
6216
|
+
customized,
|
|
6217
|
+
templateDiffers,
|
|
6218
|
+
...delta === void 0 ? {} : { templateDelta: delta },
|
|
6219
|
+
rowCount: rows
|
|
6220
|
+
};
|
|
6221
|
+
}
|
|
6222
|
+
/**
|
|
6223
|
+
* 列出每个事实的状态,并给出模板清单。
|
|
6224
|
+
* @param facts - 宿主收集的原始事实(顺序即面板顺序)。
|
|
6225
|
+
* @param writable - 本部署有没有可编辑的 profile(`configEditor` 是否在场)。
|
|
6226
|
+
* @returns `presets/status` 的完整返回值。
|
|
6227
|
+
*/
|
|
6228
|
+
function listPresetStatuses$1(facts, writable) {
|
|
6229
|
+
const templateIds = facts.filter((item) => item.ours).map((item) => item.id);
|
|
6230
|
+
const templates = facts.filter((item) => item.ours).map((item) => item.name === void 0 ? { id: item.id } : {
|
|
6231
|
+
id: item.id,
|
|
6232
|
+
name: item.name
|
|
6233
|
+
});
|
|
6234
|
+
return {
|
|
6235
|
+
presets: facts.map((item) => toPresetStatus(item, templateIds)),
|
|
6236
|
+
templates,
|
|
6237
|
+
writable
|
|
6238
|
+
};
|
|
6239
|
+
}
|
|
6676
6240
|
/**
|
|
6677
|
-
*
|
|
6678
|
-
*
|
|
6679
|
-
* @param
|
|
6680
|
-
* @
|
|
6681
|
-
* @param deps - 路径注入(测试用)。
|
|
6682
|
-
* @returns 比较结果。
|
|
6241
|
+
* 取某个模板声明的 plugins(面板"对比 / 对齐"的右侧)。
|
|
6242
|
+
* @param facts - 同一个事实集合。
|
|
6243
|
+
* @param templateId - 模板 id(本插件声明的某个 preset)。
|
|
6244
|
+
* @returns 该模板的 plugins;不是我们的模板时 undefined。
|
|
6683
6245
|
*/
|
|
6684
|
-
function
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6246
|
+
function templatePlugins(facts, templateId) {
|
|
6247
|
+
return facts.find((item) => item.ours && item.id === templateId)?.inherited?.plugins;
|
|
6248
|
+
}
|
|
6249
|
+
//#endregion
|
|
6250
|
+
//#region src/host/preset-host.ts
|
|
6251
|
+
/**
|
|
6252
|
+
* Host half of the preset surface: read the deployment's declared presets and
|
|
6253
|
+
* apply the panel's three actions through the harness's own config editor.
|
|
6254
|
+
*
|
|
6255
|
+
* WHY WE DO NOT WRITE FILES OURSELVES: dsh 0.1.7 removed the directory preset
|
|
6256
|
+
* mechanism and the `agent_preset` tool with it. The only supported programmatic
|
|
6257
|
+
* write path is `ctx.configEditor.edit()`, and it is a better one than we had —
|
|
6258
|
+
* it takes the profile lock, serializes with HMR, validates the next config
|
|
6259
|
+
* through the Loader's own waterfall, writes atomically, rolls back on a failed
|
|
6260
|
+
* reconcile, and refuses when a home patch or `--patch` overlay would defeat the
|
|
6261
|
+
* edit. It also gives us "revert to the bundled declaration" for free: handing
|
|
6262
|
+
* back the inherited config makes it delete the override's `config` key.
|
|
6263
|
+
*
|
|
6264
|
+
* The service is resolved per call, so a deployment that mounts the editor
|
|
6265
|
+
* after this plugin still works instead of caching an early `undefined`.
|
|
6266
|
+
*
|
|
6267
|
+
* Everything here is a thin adapter; the decisions live in `src/presets/**`
|
|
6268
|
+
* (pure, fs-free, unit-testable) and this module only resolves services and
|
|
6269
|
+
* shapes results.
|
|
6270
|
+
* @module @xiaoso/dsh-tool-plus/host/preset-host
|
|
6271
|
+
*/
|
|
6272
|
+
/** Narrow a non-empty string, else undefined. */
|
|
6273
|
+
function text(value) {
|
|
6274
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
6275
|
+
}
|
|
6276
|
+
/** `{ key: value }` only when the value is present, so optional fields stay absent. */
|
|
6277
|
+
function optional(key, value) {
|
|
6278
|
+
return value === void 0 ? {} : { [key]: value };
|
|
6279
|
+
}
|
|
6280
|
+
/** Read a service only when it actually looks like the one we need. */
|
|
6281
|
+
function configEditorOf(ctx) {
|
|
6282
|
+
const service = ctx.get("configEditor");
|
|
6283
|
+
return service !== void 0 && typeof service.edit === "function" && typeof service.configuration === "function" ? service : void 0;
|
|
6284
|
+
}
|
|
6285
|
+
/** Read the preset registry roster only when it exposes one of its two readers. */
|
|
6286
|
+
async function rosterOf(ctx) {
|
|
6287
|
+
const service = ctx.get("agentPresets");
|
|
6288
|
+
if (service === void 0) return void 0;
|
|
6289
|
+
try {
|
|
6290
|
+
if (typeof service.remoteExportList === "function") {
|
|
6291
|
+
const exported = await service.remoteExportList();
|
|
6292
|
+
if (Array.isArray(exported?.presets)) return exported.presets;
|
|
6293
|
+
}
|
|
6294
|
+
if (typeof service.list === "function") return await service.list();
|
|
6295
|
+
return;
|
|
6296
|
+
} catch {
|
|
6297
|
+
return;
|
|
6298
|
+
}
|
|
6299
|
+
}
|
|
6300
|
+
/**
|
|
6301
|
+
* Collect every declared preset this deployment can see.
|
|
6302
|
+
*
|
|
6303
|
+
* Ordering is OURS FIRST (the panel's primary subject), then the rest in the
|
|
6304
|
+
* registry's own order — which the registry sorts by each declaration's `order`.
|
|
6305
|
+
* @param ctx - context used to resolve the services.
|
|
6306
|
+
* @returns the facts, the editor (when present) and the row→entry map.
|
|
6307
|
+
*/
|
|
6308
|
+
async function collect(ctx) {
|
|
6309
|
+
const editor = configEditorOf(ctx);
|
|
6310
|
+
const roster = await rosterOf(ctx);
|
|
6311
|
+
const byId = /* @__PURE__ */ new Map();
|
|
6312
|
+
const entries = /* @__PURE__ */ new Map();
|
|
6313
|
+
if (editor !== void 0) for (const row of editor.configuration()) {
|
|
6314
|
+
const entryId = text(row.entry.options.id);
|
|
6315
|
+
if (entryId === void 0 || !entryId.startsWith("preset-")) continue;
|
|
6316
|
+
const effective = row.entry.options.config ?? {};
|
|
6317
|
+
const id = text(row.inherited.id) ?? text(row.override.id) ?? text(effective.id) ?? entryId.slice(7);
|
|
6318
|
+
entries.set(entryId, row.entry);
|
|
6319
|
+
byId.set(id, {
|
|
6320
|
+
id,
|
|
6321
|
+
entryId,
|
|
6322
|
+
...optional("name", text(row.inherited.name) ?? text(effective.name)),
|
|
6323
|
+
...optional("description", text(row.inherited.description) ?? text(effective.description)),
|
|
6324
|
+
isDefault: false,
|
|
6325
|
+
effective,
|
|
6326
|
+
inherited: row.inherited,
|
|
6327
|
+
override: row.override,
|
|
6328
|
+
ours: BUNDLED_PRESET_IDS.includes(id)
|
|
6329
|
+
});
|
|
6330
|
+
}
|
|
6331
|
+
const rosterOrder = [];
|
|
6332
|
+
for (const row of roster ?? []) {
|
|
6333
|
+
const id = text(row.id);
|
|
6334
|
+
if (id === void 0) continue;
|
|
6335
|
+
rosterOrder.push(id);
|
|
6336
|
+
const existing = byId.get(id);
|
|
6337
|
+
byId.set(id, {
|
|
6338
|
+
id,
|
|
6339
|
+
...existing?.entryId === void 0 ? {} : { entryId: existing.entryId },
|
|
6340
|
+
...optional("name", text(row.name) ?? existing?.name),
|
|
6341
|
+
...optional("description", text(row.description) ?? existing?.description),
|
|
6342
|
+
isDefault: row.isDefault === true,
|
|
6343
|
+
...optional("broken", text(row.broken)),
|
|
6344
|
+
...existing?.effective === void 0 ? {} : { effective: existing.effective },
|
|
6345
|
+
...existing?.inherited === void 0 ? {} : { inherited: existing.inherited },
|
|
6346
|
+
...existing?.override === void 0 ? {} : { override: existing.override },
|
|
6347
|
+
ours: BUNDLED_PRESET_IDS.includes(id)
|
|
6348
|
+
});
|
|
6349
|
+
}
|
|
6350
|
+
const ours = BUNDLED_PRESET_IDS.map((id) => byId.get(id) ?? {
|
|
6351
|
+
id,
|
|
6352
|
+
isDefault: false,
|
|
6353
|
+
ours: true
|
|
6354
|
+
});
|
|
6355
|
+
const others = rosterOrder.filter((id) => !BUNDLED_PRESET_IDS.includes(id)).map((id) => byId.get(id)).filter((fact) => fact !== void 0);
|
|
6356
|
+
return {
|
|
6357
|
+
facts: [...ours, ...others],
|
|
6358
|
+
editor,
|
|
6359
|
+
entries
|
|
6360
|
+
};
|
|
6361
|
+
}
|
|
6362
|
+
/**
|
|
6363
|
+
* The pre-0.1.7 user preset directory, reported ONLY as a cleanup hint.
|
|
6364
|
+
*
|
|
6365
|
+
* dsh reads no directory any more ("Definitions are ordinary plugin rows; the
|
|
6366
|
+
* registry neither scans directories nor accepts preset paths"), so a leftover
|
|
6367
|
+
* `$DSH_HOME/.agent-presets` is dead weight. We surface its path so the panel can
|
|
6368
|
+
* tell the user it is safe to delete; we never read or write presets through it.
|
|
6369
|
+
* @returns the absolute path when the directory exists and holds entries.
|
|
6370
|
+
*/
|
|
6371
|
+
function legacyPresetRoot() {
|
|
6372
|
+
const home = process.env.DSH_HOME?.trim() || join(homedir(), ".dsh");
|
|
6373
|
+
const dir = join(home, ".agent-presets");
|
|
6374
|
+
try {
|
|
6375
|
+
return readdirSync(dir).length > 0 ? dir : void 0;
|
|
6376
|
+
} catch {
|
|
6377
|
+
return;
|
|
6378
|
+
}
|
|
6379
|
+
}
|
|
6380
|
+
/**
|
|
6381
|
+
* This plugin's own version, for the backup file name.
|
|
6382
|
+
*
|
|
6383
|
+
* Probes two candidates because the module URL differs between the built single
|
|
6384
|
+
* file (`lib/index.mjs` → `../package.json` is the package root) and the source
|
|
6385
|
+
* tree under test (`src/host/…` → `../../package.json`). Falls back to
|
|
6386
|
+
* `'unknown'` rather than failing an action over a cosmetic label.
|
|
6387
|
+
* @returns the version string, or `'unknown'`.
|
|
6388
|
+
*/
|
|
6389
|
+
function pluginVersion() {
|
|
6390
|
+
for (const candidate of ["../package.json", "../../package.json"]) try {
|
|
6391
|
+
const raw = JSON.parse(readFileSync(fileURLToPath(new URL(candidate, import.meta.url)), "utf8"));
|
|
6392
|
+
if (typeof raw.version === "string" && raw.version.length > 0) return raw.version;
|
|
6393
|
+
} catch {}
|
|
6394
|
+
return "unknown";
|
|
6395
|
+
}
|
|
6396
|
+
/**
|
|
6397
|
+
* Copy the profile patch aside before we change it.
|
|
6398
|
+
*
|
|
6399
|
+
* The config editor already gives us atomicity and rollback, but NOT a durable
|
|
6400
|
+
* "what it looked like before" artifact — and `align` overwrites content the
|
|
6401
|
+
* user may have written there. This restores the pre-0.1.10 affordance exactly:
|
|
6402
|
+
* one `<patch>.bak-<plugin version>` beside the patch, written once and never
|
|
6403
|
+
* overwritten, so the earliest pre-change state survives repeated actions.
|
|
6404
|
+
* @param documentPath - the profile patch the editor is about to rewrite.
|
|
6405
|
+
* @returns the backup path, or undefined when there was nothing to copy.
|
|
6406
|
+
*/
|
|
6407
|
+
function backupProfilePatch(documentPath) {
|
|
6408
|
+
if (!existsSync(documentPath)) return void 0;
|
|
6409
|
+
const target = `${documentPath}.bak-${pluginVersion()}`;
|
|
6410
|
+
if (existsSync(target)) return target;
|
|
6411
|
+
copyFileSync(documentPath, target);
|
|
6412
|
+
return target;
|
|
6413
|
+
}
|
|
6414
|
+
/** One failure in the gateway's shape. */
|
|
6415
|
+
function failure(reason) {
|
|
6416
|
+
return {
|
|
6417
|
+
ok: false,
|
|
6418
|
+
changed: false,
|
|
6419
|
+
reason,
|
|
6420
|
+
changes: []
|
|
6421
|
+
};
|
|
6422
|
+
}
|
|
6423
|
+
/**
|
|
6424
|
+
* Answer `presets/status`.
|
|
6425
|
+
* @param ctx - context used to resolve the config editor and the registry.
|
|
6426
|
+
* @returns the panel's full status payload.
|
|
6427
|
+
*/
|
|
6428
|
+
async function listPresetStatuses(ctx) {
|
|
6429
|
+
const { facts, editor } = await collect(ctx);
|
|
6430
|
+
const list = listPresetStatuses$1(facts, editor !== void 0);
|
|
6431
|
+
const legacy = legacyPresetRoot();
|
|
6432
|
+
return legacy === void 0 ? list : {
|
|
6433
|
+
...list,
|
|
6434
|
+
legacyPresetRoot: legacy
|
|
6435
|
+
};
|
|
6436
|
+
}
|
|
6437
|
+
/**
|
|
6438
|
+
* Answer `presets/apply` by planning the action and, when it is worth writing,
|
|
6439
|
+
* delegating the write to the harness's own config editor.
|
|
6440
|
+
* @param ctx - context used to resolve the services.
|
|
6441
|
+
* @param presetId - the preset to act on.
|
|
6442
|
+
* @param action - `upgrade`, `align` or `revert`.
|
|
6443
|
+
* @param templateId - the align target (required for `align`).
|
|
6444
|
+
* @returns the action result, never throwing.
|
|
6445
|
+
*/
|
|
6446
|
+
async function applyPresetAction(ctx, presetId, action, templateId) {
|
|
6447
|
+
try {
|
|
6448
|
+
const { facts, editor, entries } = await collect(ctx);
|
|
6449
|
+
const fact = facts.find((item) => item.id === presetId);
|
|
6450
|
+
if (fact === void 0) return failure(`unknown preset: ${presetId}`);
|
|
6451
|
+
const wanted = templateId === void 0 ? void 0 : templatePlugins(facts, templateId);
|
|
6452
|
+
const plan = planPresetAction({
|
|
6453
|
+
action,
|
|
6454
|
+
ours: fact.ours,
|
|
6455
|
+
customized: fact.override !== void 0 && Object.keys(fact.override).length > 0,
|
|
6456
|
+
effective: fact.effective,
|
|
6457
|
+
inherited: fact.inherited,
|
|
6458
|
+
...wanted === void 0 ? {} : { templatePlugins: wanted },
|
|
6459
|
+
...templateId === void 0 ? {} : { templateId }
|
|
6460
|
+
});
|
|
6461
|
+
if (plan.kind === "noop") return {
|
|
6462
|
+
ok: true,
|
|
6463
|
+
changed: false,
|
|
6464
|
+
reason: plan.reason,
|
|
6465
|
+
changes: []
|
|
6466
|
+
};
|
|
6467
|
+
if (editor === void 0) return failure("this deployment has no editable profile: ctx.configEditor is unavailable");
|
|
6468
|
+
const entry = fact.entryId === void 0 ? void 0 : entries.get(fact.entryId);
|
|
6469
|
+
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`);
|
|
6470
|
+
const backupPath = backupProfilePatch(editor.documentPath);
|
|
6471
|
+
if (plan.kind === "revert") await editor.edit(entry, (_current, inherited) => inherited);
|
|
6472
|
+
else if (plan.kind === "align") await editor.edit(entry, (current) => ({
|
|
6473
|
+
...current,
|
|
6474
|
+
plugins: wanted
|
|
6475
|
+
}));
|
|
6476
|
+
else await editor.edit(entry, (current) => {
|
|
6477
|
+
if (!isRowList(current.plugins)) return current;
|
|
6478
|
+
return {
|
|
6479
|
+
...current,
|
|
6480
|
+
plugins: disableConflicts(current.plugins).plugins
|
|
6481
|
+
};
|
|
6482
|
+
});
|
|
6483
|
+
return {
|
|
6484
|
+
ok: true,
|
|
6485
|
+
changed: true,
|
|
6486
|
+
...backupPath === void 0 ? {} : { backupPath },
|
|
6487
|
+
changes: plan.kind === "upgrade" ? plan.changes : []
|
|
6488
|
+
};
|
|
6489
|
+
} catch (error) {
|
|
6490
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
6491
|
+
}
|
|
6492
|
+
}
|
|
6493
|
+
/**
|
|
6494
|
+
* Answer `presets/compare`: one preset's effective plugins against one of our
|
|
6495
|
+
* declared templates. Never throws; an unresolvable side reports `unreadable`
|
|
6496
|
+
* (that is a state the panel renders as a red dot, not a failed call).
|
|
6497
|
+
* @param ctx - context used to resolve the services.
|
|
6498
|
+
* @param presetId - the preset being inspected.
|
|
6499
|
+
* @param templateId - the template to compare it against.
|
|
6500
|
+
* @returns the comparison.
|
|
6501
|
+
*/
|
|
6502
|
+
async function comparePresetAgainstTemplate(ctx, presetId, templateId) {
|
|
6503
|
+
const empty = {
|
|
6504
|
+
presetId,
|
|
6505
|
+
templateId,
|
|
6506
|
+
status: "unreadable",
|
|
6507
|
+
conflicts: [],
|
|
6508
|
+
identical: false,
|
|
6509
|
+
yoursCount: 0,
|
|
6510
|
+
behindCount: 0,
|
|
6511
|
+
items: []
|
|
6512
|
+
};
|
|
6513
|
+
try {
|
|
6514
|
+
const { facts } = await collect(ctx);
|
|
6515
|
+
const fact = facts.find((item) => item.id === presetId);
|
|
6516
|
+
if (fact === void 0) return empty;
|
|
6517
|
+
const template = templatePlugins(facts, templateId);
|
|
6518
|
+
if (template === void 0) return empty;
|
|
6519
|
+
return {
|
|
6520
|
+
presetId,
|
|
6521
|
+
templateId,
|
|
6522
|
+
...comparePlugins(fact.effective?.plugins, template)
|
|
6523
|
+
};
|
|
6524
|
+
} catch {
|
|
6525
|
+
return empty;
|
|
6526
|
+
}
|
|
6689
6527
|
}
|
|
6690
6528
|
//#endregion
|
|
6691
6529
|
//#region src/web/host/bash.ts
|
|
@@ -11558,7 +11396,7 @@ let specialHandlersPromise;
|
|
|
11558
11396
|
* requires a special handler, so we keep them out of the cold-startup graph.
|
|
11559
11397
|
*/
|
|
11560
11398
|
function loadSpecialHandlers() {
|
|
11561
|
-
specialHandlersPromise ??= import("./scrapers-
|
|
11399
|
+
specialHandlersPromise ??= import("./scrapers-DODCPcZW.mjs").then((m) => m.specialHandlers);
|
|
11562
11400
|
return specialHandlersPromise;
|
|
11563
11401
|
}
|
|
11564
11402
|
/**
|
|
@@ -23738,19 +23576,11 @@ function apply(ctx, config = {}) {
|
|
|
23738
23576
|
const disposeBrowserProbe = installBrowserProbeRpc(ctx, {
|
|
23739
23577
|
getRmSafe: () => cfg.rmSafe,
|
|
23740
23578
|
presets: {
|
|
23741
|
-
|
|
23742
|
-
applyAction: (id, action, templateId) => applyPresetAction(id, action,
|
|
23743
|
-
|
|
23744
|
-
comparePreset: (preset, templateId) => comparePresetFile(preset.path, templateId)
|
|
23579
|
+
listPresets: (presetCtx) => listPresetStatuses(presetCtx),
|
|
23580
|
+
applyAction: (presetCtx, id, action, templateId) => applyPresetAction(presetCtx, id, action, templateId),
|
|
23581
|
+
comparePreset: (presetCtx, presetId, templateId) => comparePresetAgainstTemplate(presetCtx, presetId, templateId)
|
|
23745
23582
|
}
|
|
23746
23583
|
});
|
|
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
23584
|
ctx.systemPrompt.section({
|
|
23755
23585
|
name: "tool:bash",
|
|
23756
23586
|
order: 105,
|
|
@@ -23928,72 +23758,47 @@ function apply(ctx, config = {}) {
|
|
|
23928
23758
|
...commandEnv
|
|
23929
23759
|
};
|
|
23930
23760
|
const jobs = ctx.get("jobs");
|
|
23761
|
+
const owner = exec.agent?.id;
|
|
23931
23762
|
const backgroundSlotsAvailable = () => {
|
|
23932
23763
|
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
|
-
};
|
|
23764
|
+
return jobs.list(owner).filter((j) => j.status === "running" || j.status === "stopping").length < cfg.maxBackgroundJobs;
|
|
23963
23765
|
};
|
|
23766
|
+
const startManagedJob = (job) => startBashJob({
|
|
23767
|
+
sessionId,
|
|
23768
|
+
command,
|
|
23769
|
+
cwd: commandCwd,
|
|
23770
|
+
timeoutMs: args.timeoutMs ?? 0,
|
|
23771
|
+
env,
|
|
23772
|
+
config: cfg,
|
|
23773
|
+
...job !== void 0 ? { job } : {},
|
|
23774
|
+
formatCompletion: (text, spillPath) => applyConfiguredTruncation(text, spillPath, cfg.outputTruncate)
|
|
23775
|
+
});
|
|
23776
|
+
const jobSpec = (run) => ({
|
|
23777
|
+
kind: "bash",
|
|
23778
|
+
label: command,
|
|
23779
|
+
...owner !== void 0 ? { owner } : {},
|
|
23780
|
+
...Number.isSafeInteger(cfg.outputMaxBytes) && cfg.outputMaxBytes > 0 ? { outputLimitBytes: cfg.outputMaxBytes } : {},
|
|
23781
|
+
run
|
|
23782
|
+
});
|
|
23964
23783
|
if (args.run_in_background === true) {
|
|
23965
23784
|
if (!cfg.enableRunInBackground) throw new Error("run_in_background is disabled for this deployment (enableRunInBackground: false)");
|
|
23966
23785
|
if (jobs === void 0) throw new Error("background jobs unavailable: load @deepseek-ai/dsh-jobs and @deepseek-ai/dsh-tool-jobs");
|
|
23967
23786
|
if (!backgroundSlotsAvailable()) throw new Error(`background jobs at capacity (maxBackgroundJobs: ${cfg.maxBackgroundJobs})`);
|
|
23968
23787
|
if (exec.signal.aborted) throw abortError();
|
|
23969
|
-
let managed;
|
|
23970
23788
|
return {
|
|
23971
23789
|
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
|
-
})
|
|
23790
|
+
jobId: jobs.start(jobSpec((job) => startManagedJob(job).hooks))
|
|
23981
23791
|
};
|
|
23982
23792
|
}
|
|
23983
23793
|
if (cfg.autoBackgroundMs > 0 && jobs !== void 0 && backgroundSlotsAvailable() && !exec.signal.aborted) {
|
|
23984
|
-
const autoBgWaitMs = timeoutMs === 0 ? cfg.autoBackgroundMs : Math.
|
|
23794
|
+
const autoBgWaitMs = Math.max(1, timeoutMs === 0 ? cfg.autoBackgroundMs : Math.min(cfg.autoBackgroundMs, timeoutMs - 1e3));
|
|
23985
23795
|
let managed;
|
|
23986
23796
|
let id;
|
|
23987
23797
|
try {
|
|
23988
|
-
id = jobs.start({
|
|
23989
|
-
|
|
23990
|
-
|
|
23991
|
-
|
|
23992
|
-
run: () => {
|
|
23993
|
-
managed = startManagedJob();
|
|
23994
|
-
return managed.hooks;
|
|
23995
|
-
}
|
|
23996
|
-
});
|
|
23798
|
+
id = jobs.start(jobSpec((job) => {
|
|
23799
|
+
managed = startManagedJob(job);
|
|
23800
|
+
return managed.hooks;
|
|
23801
|
+
}));
|
|
23997
23802
|
} catch {
|
|
23998
23803
|
const startedAt = performance.now();
|
|
23999
23804
|
const result = await executeBash(command, {
|
|
@@ -24025,34 +23830,23 @@ function apply(ctx, config = {}) {
|
|
|
24025
23830
|
if (result.workingDir !== void 0) state.cwd = result.workingDir;
|
|
24026
23831
|
return buildForeground(result, performance.now() - startedAt, timeoutMs);
|
|
24027
23832
|
}
|
|
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 {
|
|
23833
|
+
let settled = false;
|
|
23834
|
+
try {
|
|
23835
|
+
const view = await jobs.wait(id, autoBgWaitMs, owner, exec.signal);
|
|
23836
|
+
settled = view.status !== "running" && view.status !== "stopping";
|
|
23837
|
+
} catch {
|
|
23838
|
+
jobs.kill(id, owner, "tool call aborted");
|
|
23839
|
+
await jobs.wait(id, Math.max(1e3, timeoutMs), owner);
|
|
23840
|
+
throw abortError();
|
|
23841
|
+
}
|
|
23842
|
+
if (!settled) return {
|
|
24046
23843
|
kind: "background",
|
|
24047
23844
|
jobId: id
|
|
24048
23845
|
};
|
|
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;
|
|
23846
|
+
const value = await managed.completion;
|
|
23847
|
+
jobs.remove(id, owner);
|
|
23848
|
+
if (value.workingDir !== void 0) state.cwd = value.workingDir;
|
|
23849
|
+
return value;
|
|
24056
23850
|
}
|
|
24057
23851
|
const startedAt = performance.now();
|
|
24058
23852
|
const result = await executeBash(command, {
|