dsh-auto-compaction-engine 0.0.0-stage → 0.2.0
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.md +8 -2
- package/lib/index.js +650 -0
- package/lib/plan.js +79 -0
- package/lib/status.js +52 -0
- package/lib/summary.js +147 -0
- package/package.json +45 -4
- package/scripts/preset-scope.mjs +409 -0
- package/scripts/row-native.yml +6 -0
- package/scripts/row-plugin.yml +10 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* preset-scope.mjs — switch an editable DSH preset between the handoff plugin and
|
|
3
|
+
* DSH's native compaction, reversibly.
|
|
4
|
+
*
|
|
5
|
+
* Two layers must agree for "native off" to be real:
|
|
6
|
+
* 1. the preset's marker-delimited compaction slot (the row that supplies
|
|
7
|
+
* `ctx.compaction` inside the preset group), and
|
|
8
|
+
* 2. the profile-level root `compaction-basic` node (DSH's default engine,
|
|
9
|
+
* which also listens on `agent/pre-step` and can compact before the
|
|
10
|
+
* preset's isolated plugin is consulted).
|
|
11
|
+
*
|
|
12
|
+
* Layer 1 is replaced wholesale between row-plugin.yml and row-native.yml.
|
|
13
|
+
* Layer 2 is guarded by a marker comment that sets `auto: false` while the
|
|
14
|
+
* plugin is enabled and is removed on disable, restoring the stock default.
|
|
15
|
+
*
|
|
16
|
+
* Without --preset/--all-local, `enable` reads the plugin settings sidecar:
|
|
17
|
+
* `scope: "preset"` switches exactly the listed preset ids to the plugin and
|
|
18
|
+
* resets every other editable preset to native; `scope: "global"` switches all.
|
|
19
|
+
* `disable` restores native everywhere. The root guard is on only while at
|
|
20
|
+
* least one preset uses the plugin.
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
* node scripts/preset-scope.mjs status [--preset <cordis.patch.yml>] [--profile-patch <cordis.patch.yml>] [--all-local] [--skip-profile]
|
|
24
|
+
* node scripts/preset-scope.mjs enable [--preset <cordis.patch.yml>] [--profile-patch <cordis.patch.yml>] [--all-local] [--skip-profile]
|
|
25
|
+
* node scripts/preset-scope.mjs disable [--preset <cordis.patch.yml>] [--profile-patch <cordis.patch.yml>] [--all-local] [--skip-profile]
|
|
26
|
+
* Restart DSH afterwards.
|
|
27
|
+
*/
|
|
28
|
+
import fs from "node:fs";
|
|
29
|
+
import { homedir } from "node:os";
|
|
30
|
+
import path from "node:path";
|
|
31
|
+
import { fileURLToPath } from "node:url";
|
|
32
|
+
import { createRequire } from "node:module";
|
|
33
|
+
|
|
34
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
35
|
+
const APP_DATA = process.env.APPDATA ?? path.join(homedir(), "AppData", "Roaming");
|
|
36
|
+
const DEFAULT_PROFILE_PATCH = process.env.DSH_PROFILE_PATCH
|
|
37
|
+
?? path.join(APP_DATA, "dsh-desktop", "harness", "profiles", "web", "cordis.patch.yml");
|
|
38
|
+
const DEFAULT_SETTINGS = path.join(homedir(), ".dsh-handoff-compaction", "settings.json");
|
|
39
|
+
const LOCAL_PKGS = process.env.DSH_LOCAL_PKGS ?? path.join(homedir(), ".dsh-local-pkgs");
|
|
40
|
+
const SETTINGS_FILE = process.env.DSH_HANDOFF_SETTINGS_FILE ?? DEFAULT_SETTINGS;
|
|
41
|
+
const SLOT_BEGIN = "# >>> handoff:slot:begin";
|
|
42
|
+
const SLOT_END = "# <<< handoff:slot:end";
|
|
43
|
+
const GUARD_BEGIN = "# >>> handoff:native-guard:begin";
|
|
44
|
+
const GUARD_END = "# <<< handoff:native-guard:end";
|
|
45
|
+
const ROW_INDENT = 14;
|
|
46
|
+
|
|
47
|
+
const argv = process.argv.slice(2);
|
|
48
|
+
const mode = (argv.find((a) => !a.startsWith("--")) ?? "status").toLowerCase();
|
|
49
|
+
const presetArg = argv.indexOf("--preset");
|
|
50
|
+
const profileArg = argv.indexOf("--profile-patch");
|
|
51
|
+
const allLocal = argv.includes("--all-local");
|
|
52
|
+
const skipProfile = argv.includes("--skip-profile");
|
|
53
|
+
const profilePatch = profileArg >= 0
|
|
54
|
+
? argv[profileArg + 1]
|
|
55
|
+
: (process.env.DSH_PROFILE_PATCH ?? DEFAULT_PROFILE_PATCH);
|
|
56
|
+
|
|
57
|
+
function readSettings() {
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(fs.readFileSync(SETTINGS_FILE, "utf8"));
|
|
60
|
+
return parsed && typeof parsed === "object" ? parsed : undefined;
|
|
61
|
+
} catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function presetIdOf(text) {
|
|
67
|
+
const yaml = loadYaml();
|
|
68
|
+
if (yaml === undefined) return undefined;
|
|
69
|
+
try {
|
|
70
|
+
const doc = yaml.load(text, { schema: yamlSchema(yaml) });
|
|
71
|
+
return doc?.[0]?.insert?.[0]?.config?.id;
|
|
72
|
+
} catch {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Every editable local preset that carries the handoff slot markers. */
|
|
78
|
+
function discoverPresetFiles() {
|
|
79
|
+
if (!fs.existsSync(LOCAL_PKGS)) return [];
|
|
80
|
+
return fs.readdirSync(LOCAL_PKGS)
|
|
81
|
+
.map((name) => path.join(LOCAL_PKGS, name, "cordis.patch.yml"))
|
|
82
|
+
.filter((file) => fs.existsSync(file) && fs.readFileSync(file, "utf8").includes(SLOT_BEGIN));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function defaultPresetFile() {
|
|
86
|
+
return discoverPresetFiles()[0]
|
|
87
|
+
?? path.join(LOCAL_PKGS, "dsh-preset-lightweight-local", "cordis.patch.yml");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Rows to apply. `enable: true` means the plugin takes this preset; `false`
|
|
92
|
+
* restores its own native row. Non-selected presets are reset to native so a
|
|
93
|
+
* previous switch cannot leak into a preset the user did not select.
|
|
94
|
+
*/
|
|
95
|
+
function selection(action) {
|
|
96
|
+
if (presetArg >= 0) {
|
|
97
|
+
return [{ file: argv[presetArg + 1], enable: action === "enable" }];
|
|
98
|
+
}
|
|
99
|
+
const files = discoverPresetFiles();
|
|
100
|
+
if (action === "disable") return files.map((file) => ({ file, enable: false }));
|
|
101
|
+
if (allLocal) return files.map((file) => ({ file, enable: true }));
|
|
102
|
+
const settings = readSettings();
|
|
103
|
+
if (settings?.scope === "global") return files.map((file) => ({ file, enable: true }));
|
|
104
|
+
if (settings?.scope === "preset") {
|
|
105
|
+
const selected = new Set(Array.isArray(settings.presets) ? settings.presets : []);
|
|
106
|
+
const planned = files.map((file) => {
|
|
107
|
+
const id = presetIdOf(fs.readFileSync(file, "utf8"));
|
|
108
|
+
return { file, id, enable: id !== undefined && selected.has(id) };
|
|
109
|
+
});
|
|
110
|
+
for (const id of selected) {
|
|
111
|
+
if (!planned.some((entry) => entry.id === id)) {
|
|
112
|
+
console.error(`warn: selected preset "${id}" has no editable handoff slot; left native`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return planned;
|
|
116
|
+
}
|
|
117
|
+
return [{ file: defaultPresetFile(), enable: action === "enable" }];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function statusPlan() {
|
|
121
|
+
if (presetArg >= 0) return [{ file: argv[presetArg + 1] }];
|
|
122
|
+
const settings = readSettings();
|
|
123
|
+
const files = discoverPresetFiles();
|
|
124
|
+
if (settings?.scope === "global") return files.map((file) => ({ file, selected: true }));
|
|
125
|
+
if (settings?.scope === "preset") {
|
|
126
|
+
const selected = new Set(Array.isArray(settings.presets) ? settings.presets : []);
|
|
127
|
+
return files.map((file) => ({ file, selected: selected.has(presetIdOf(fs.readFileSync(file, "utf8"))) }));
|
|
128
|
+
}
|
|
129
|
+
return [{ file: defaultPresetFile() }];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function pluginRow(presetId) {
|
|
133
|
+
const text = fs.readFileSync(path.join(HERE, "row-plugin.yml"), "utf8");
|
|
134
|
+
const trace = path.join(homedir(), ".dsh-handoff-compaction", "trace.log").replace(/\\/g, "/");
|
|
135
|
+
const withId = (presetId === undefined
|
|
136
|
+
? text
|
|
137
|
+
: text.replace(/^(\s*presetId:\s*).*$/m, `$1${presetId}`))
|
|
138
|
+
.replace(/^(\s*traceFile:\s*).*$/m, `$1"${trace}"`);
|
|
139
|
+
return indentRow(withId);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function indentRow(text) {
|
|
143
|
+
return text.replace(/\r\n/g, "\n").replace(/\n+$/, "").split("\n")
|
|
144
|
+
.map((line) => (line.trim() === "" ? "" : " ".repeat(ROW_INDENT) + line));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function loadYaml() {
|
|
148
|
+
try {
|
|
149
|
+
return createRequire(import.meta.url)("js-yaml");
|
|
150
|
+
} catch {
|
|
151
|
+
// The engine's own dependency is preferred; a profile-installed copy is the fallback.
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const profileRoot = path.join(APP_DATA, "dsh-desktop", "harness", "profiles", "web");
|
|
155
|
+
return createRequire(path.join(profileRoot, "package.json"))("js-yaml");
|
|
156
|
+
} catch {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function yamlSchema(yaml) {
|
|
162
|
+
const jsTag = new yaml.Type("tag:yaml.org,2002:js", { kind: "scalar", construct: (d) => d });
|
|
163
|
+
return yaml.DEFAULT_SCHEMA.extend([jsTag]);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function validatePreset(text) {
|
|
167
|
+
const yaml = loadYaml();
|
|
168
|
+
if (yaml === undefined) return "skipped (js-yaml unavailable)";
|
|
169
|
+
const doc = yaml.load(text, { schema: yamlSchema(yaml) });
|
|
170
|
+
const group = doc[0].insert[0].config.plugins.find((entry) => entry.id === "compaction");
|
|
171
|
+
const rows = group.config.filter((entry) => entry.id === "compaction-basic");
|
|
172
|
+
if (rows.length !== 1) throw new Error(`expected exactly one compaction-basic row, found ${rows.length}`);
|
|
173
|
+
return `ok -> ${rows[0].name}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function validateProfile(text) {
|
|
177
|
+
const yaml = loadYaml();
|
|
178
|
+
if (yaml === undefined) return "skipped (js-yaml unavailable)";
|
|
179
|
+
const doc = yaml.load(text, { schema: yamlSchema(yaml) });
|
|
180
|
+
if (!Array.isArray(doc)) throw new Error("profile patch: expected a top-level array");
|
|
181
|
+
const row = doc.find((entry) => entry?.id === "compaction-basic" && entry?.name === "@deepseek-ai/dsh-compaction-basic");
|
|
182
|
+
if (row === undefined) throw new Error("profile patch: native compaction-basic entry not found");
|
|
183
|
+
return `ok -> ${row.name}, auto=${row.config?.auto === undefined ? "default" : String(row.config.auto)}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function slotBounds(lines) {
|
|
187
|
+
const begin = lines.findIndex((line) => line.includes(SLOT_BEGIN));
|
|
188
|
+
const end = lines.findIndex((line) => line.includes(SLOT_END));
|
|
189
|
+
if (begin < 0 || end < 0 || end <= begin) return undefined;
|
|
190
|
+
return { begin, end };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function activeName(lines) {
|
|
194
|
+
const bounds = slotBounds(lines);
|
|
195
|
+
if (bounds === undefined) return "no slot";
|
|
196
|
+
for (let i = bounds.begin + 1; i < bounds.end; i += 1) {
|
|
197
|
+
const match = lines[i].match(/^\s*name:\s*"([^"]+)"/);
|
|
198
|
+
if (match) return match[1];
|
|
199
|
+
}
|
|
200
|
+
return "unknown";
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Locate the top-level native compaction-basic entry that carries a name + config. */
|
|
204
|
+
function nativeBounds(lines) {
|
|
205
|
+
const starts = [];
|
|
206
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
207
|
+
if (/^-\s+id:\s*compaction-basic\s*$/.test(lines[i])) starts.push(i);
|
|
208
|
+
}
|
|
209
|
+
for (let index = 0; index < starts.length; index += 1) {
|
|
210
|
+
const start = starts[index];
|
|
211
|
+
const end = index + 1 < starts.length ? starts[index + 1] : lines.length;
|
|
212
|
+
const block = lines.slice(start, end).join("\n");
|
|
213
|
+
if (/name:\s*["']?@deepseek-ai\/dsh-compaction-basic["']?/.test(block) && /^\s{2}config:\s*$/m.test(block)) {
|
|
214
|
+
return { start, end };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function configLine(lines, bounds) {
|
|
221
|
+
for (let i = bounds.start + 1; i < bounds.end; i += 1) {
|
|
222
|
+
if (/^\s{2}config:\s*$/.test(lines[i])) return i;
|
|
223
|
+
}
|
|
224
|
+
return -1;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function guardBounds(lines, bounds) {
|
|
228
|
+
let begin = -1;
|
|
229
|
+
let end = -1;
|
|
230
|
+
for (let i = bounds.start; i < bounds.end; i += 1) {
|
|
231
|
+
if (lines[i].includes(GUARD_BEGIN)) begin = i;
|
|
232
|
+
if (lines[i].includes(GUARD_END)) end = i;
|
|
233
|
+
}
|
|
234
|
+
return begin >= 0 && end > begin ? { begin, end } : undefined;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function autoLine(lines, configIdx, bounds) {
|
|
238
|
+
for (let i = configIdx + 1; i < bounds.end; i += 1) {
|
|
239
|
+
if (/^- /.test(lines[i])) break;
|
|
240
|
+
if (/^\s{2}[A-Za-z]/.test(lines[i])) break;
|
|
241
|
+
if (/^\s{4}auto:\s*/.test(lines[i])) return i;
|
|
242
|
+
}
|
|
243
|
+
return -1;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function guardState(lines, bounds) {
|
|
247
|
+
const cfg = configLine(lines, bounds);
|
|
248
|
+
if (cfg < 0) return "no config";
|
|
249
|
+
const guard = guardBounds(lines, bounds);
|
|
250
|
+
if (guard !== undefined) {
|
|
251
|
+
const hasFalse = lines.slice(guard.begin + 1, guard.end).some((line) => /^\s*auto:\s*false\s*$/.test(line));
|
|
252
|
+
return hasFalse ? "auto:false (plugin guard)" : "guarded (auto missing!)";
|
|
253
|
+
}
|
|
254
|
+
const auto = autoLine(lines, cfg, bounds);
|
|
255
|
+
return auto < 0 ? "absent (default true)" : `auto:${lines[auto].split(":")[1].trim()}`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function editProfilePatch(file, action) {
|
|
259
|
+
if (!fs.existsSync(file)) {
|
|
260
|
+
console.error(`skip (missing profile patch): ${file}`);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const original = fs.readFileSync(file, "utf8");
|
|
264
|
+
const lines = original.split(/\r?\n/);
|
|
265
|
+
const bounds = nativeBounds(lines);
|
|
266
|
+
if (bounds === undefined) {
|
|
267
|
+
console.error(`skip (no native compaction-basic entry): ${file}`);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const cfg = configLine(lines, bounds);
|
|
271
|
+
if (cfg < 0) {
|
|
272
|
+
console.error(`skip (native entry has no config block): ${file}`);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const guard = guardBounds(lines, bounds);
|
|
276
|
+
let nextLines = [...lines];
|
|
277
|
+
|
|
278
|
+
if (action === "enable") {
|
|
279
|
+
if (guard !== undefined) {
|
|
280
|
+
const hasAuto = nextLines.slice(guard.begin + 1, guard.end).some((line) => /^\s*auto:\s*/.test(line));
|
|
281
|
+
nextLines = nextLines.map((line, index) => (index > guard.begin && index < guard.end && /^\s*auto:\s*/.test(line)
|
|
282
|
+
? line.replace(/^(\s*)auto:\s*.*$/, "$1auto: false")
|
|
283
|
+
: line));
|
|
284
|
+
if (!hasAuto) nextLines.splice(guard.begin + 1, 0, " auto: false");
|
|
285
|
+
} else {
|
|
286
|
+
const auto = autoLine(nextLines, cfg, bounds);
|
|
287
|
+
if (auto >= 0) {
|
|
288
|
+
nextLines[auto] = nextLines[auto].replace(/^(\s*)auto:\s*.*$/, "$1auto: false");
|
|
289
|
+
} else {
|
|
290
|
+
nextLines.splice(cfg + 1, 0,
|
|
291
|
+
" " + GUARD_BEGIN,
|
|
292
|
+
" auto: false",
|
|
293
|
+
" " + GUARD_END);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
} else {
|
|
297
|
+
if (guard !== undefined) {
|
|
298
|
+
nextLines.splice(guard.begin, guard.end - guard.begin + 1);
|
|
299
|
+
} else {
|
|
300
|
+
const auto = autoLine(nextLines, cfg, bounds);
|
|
301
|
+
if (auto >= 0) nextLines[auto] = nextLines[auto].replace(/^(\s*)auto:\s*.*$/, "$1auto: true");
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const next = nextLines.join("\n");
|
|
306
|
+
if (next === original) {
|
|
307
|
+
console.log(`${file}\n -> native ${action === "enable" ? "auto:false" : "auto:true"} [no change]`);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const verdict = validateProfile(next);
|
|
311
|
+
if (!verdict.startsWith("ok")) {
|
|
312
|
+
console.error(`aborted (validation): ${verdict}`);
|
|
313
|
+
process.exitCode = 1;
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
fs.copyFileSync(file, `${file}.bak-scope`);
|
|
317
|
+
fs.writeFileSync(file, next);
|
|
318
|
+
console.log(`${file}\n -> native ${action === "enable" ? "auto:false" : "auto:true"} [${verdict}]`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function profileStatus() {
|
|
322
|
+
if (skipProfile) return;
|
|
323
|
+
if (!fs.existsSync(profilePatch)) {
|
|
324
|
+
console.log(`${profilePatch}\n missing`);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const original = fs.readFileSync(profilePatch, "utf8");
|
|
328
|
+
const lines = original.split(/\r?\n/);
|
|
329
|
+
const bounds = nativeBounds(lines);
|
|
330
|
+
if (bounds === undefined) {
|
|
331
|
+
console.log(`${profilePatch}\n native compaction-basic entry not found`);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
console.log(`${profilePatch}\n 原生守卫: ${guardState(lines, bounds)}\n YAML: ${validateProfile(original)}`);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (mode !== "status" && mode !== "enable" && mode !== "disable") {
|
|
338
|
+
console.error("usage: preset-scope.mjs <status|enable|disable> [--preset <file>] [--profile-patch <file>] [--all-local] [--skip-profile]");
|
|
339
|
+
process.exit(2);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const plan = mode === "status" ? statusPlan() : selection(mode);
|
|
343
|
+
for (const entry of plan) {
|
|
344
|
+
const file = entry.file;
|
|
345
|
+
if (!fs.existsSync(file)) {
|
|
346
|
+
console.error(`skip (missing): ${file}`);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const original = fs.readFileSync(file, "utf8");
|
|
350
|
+
const lines = original.split(/\r?\n/);
|
|
351
|
+
const bounds = slotBounds(lines);
|
|
352
|
+
if (bounds === undefined) {
|
|
353
|
+
console.error(`skip (no slot markers): ${file}`);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (mode === "status") {
|
|
358
|
+
const current = activeName(lines);
|
|
359
|
+
const kind = current === "dsh-handoff-compaction" || current === "dsh-auto-compaction" || current === "dsh-auto-compaction-engine" ? "插件接管(原生关闭)"
|
|
360
|
+
: current === "@deepseek-ai/dsh-compaction-basic" ? "DSH 原生压缩"
|
|
361
|
+
: "未知";
|
|
362
|
+
const selected = entry.selected === undefined ? "" : `\n 勾选: ${entry.selected ? "是(插件)" : "否(原生)"}`;
|
|
363
|
+
const switchLine = readSettings()?.enabled === false
|
|
364
|
+
? "\n 开关: 已停用(设置页关闭,运行时压缩交回原生行为)"
|
|
365
|
+
: "\n 开关: 启用";
|
|
366
|
+
console.log(`${file}\n 当前: ${current}\n 含义: ${kind}${selected}${switchLine}\n YAML: ${validatePreset(original)}`);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const id = presetIdOf(original);
|
|
371
|
+
if (entry.enable && id === undefined) {
|
|
372
|
+
console.error(`skip (cannot read preset id): ${file}`);
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
const row = entry.enable
|
|
376
|
+
? pluginRow(id)
|
|
377
|
+
: indentRow(fs.readFileSync(path.join(HERE, "row-native.yml"), "utf8"));
|
|
378
|
+
const next = [
|
|
379
|
+
...lines.slice(0, bounds.begin + 1),
|
|
380
|
+
...row,
|
|
381
|
+
...lines.slice(bounds.end),
|
|
382
|
+
].join("\n");
|
|
383
|
+
|
|
384
|
+
if (next === original) {
|
|
385
|
+
console.log(`${file}\n -> ${activeName(lines)} [no change]`);
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const verdict = validatePreset(next);
|
|
390
|
+
if (!verdict.startsWith("ok")) {
|
|
391
|
+
console.error(`aborted (validation): ${verdict}`);
|
|
392
|
+
process.exit(1);
|
|
393
|
+
}
|
|
394
|
+
fs.copyFileSync(file, `${file}.bak-scope`);
|
|
395
|
+
fs.writeFileSync(file, next);
|
|
396
|
+
console.log(`${file}\n -> ${activeName(next.split(/\r?\n/))} [${verdict}]`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (mode === "status") {
|
|
400
|
+
profileStatus();
|
|
401
|
+
} else {
|
|
402
|
+
// The root guard only disables DSH's profile-level duplicate engine. Each
|
|
403
|
+
// preset's own compaction row stays native unless the plan selected it, so
|
|
404
|
+
// this is not "native off for every preset".
|
|
405
|
+
const anyPlugin = plan.some((entry) => entry.enable);
|
|
406
|
+
if (!skipProfile) editProfilePatch(profilePatch, mode === "enable" && anyPlugin ? "enable" : "disable");
|
|
407
|
+
if (mode === "enable" && !anyPlugin) console.error("warn: no selected preset uses the plugin; root native fallback left on");
|
|
408
|
+
console.log("重启 DSH 后生效。");
|
|
409
|
+
}
|