dsh-enhancement-suite 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/dsh-enhance.mjs +203 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -94,8 +94,8 @@ dsh-enhance update --profile headless
|
|
|
94
94
|
## How it installs
|
|
95
95
|
|
|
96
96
|
1. Every plugin is installed through the **official CLI**: `dsh plugin --profile <profile> add <package>`. This suite never loads the plugins through its own dependency graph or its own cordis patch — the four upstream packages stay independent in the profile.
|
|
97
|
-
2.
|
|
98
|
-
3.
|
|
97
|
+
2. All four plugins now declare `dsh.bundle.patch` (as of vision 0.3.9 / soul-md 0.5.4 / tdai-memory 0.2.9 / marketplace 0.2.4), so `dsh plugin add` reconciles each into `dsh.profile.bundles` and its own bundle layer auto-mounts it.
|
|
98
|
+
3. For profiles that mounted a plugin manually before it gained its bundle manifest, the suite **removes the stale manual row** from `cordis.patch.yml` — two mounts of the same entry id would abort boot. (Non-bundle third-party plugins still get the safe idempotent mount logic: empty `[]` patches are replaced correctly, duplicates are skipped, flow-style arrays are refused instead of corrupted.)
|
|
99
99
|
4. Restart `dsh web` and the plugins load.
|
|
100
100
|
|
|
101
101
|
Every step reports a clear `✓` / `✗`, failures are never swallowed, and no API key is read, printed, or forwarded.
|
package/bin/dsh-enhance.mjs
CHANGED
|
@@ -84,18 +84,30 @@ function findDsh() {
|
|
|
84
84
|
return null;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/** Quote one argument for a Windows cmd line (no shell interpolation). */
|
|
88
|
+
function quoteArg(arg) {
|
|
89
|
+
return '"' + String(arg).replace(/"/g, '\\"') + '"';
|
|
90
|
+
}
|
|
91
|
+
|
|
87
92
|
/**
|
|
88
93
|
* Spawn one subprocess with an argument array (never a shell string).
|
|
89
|
-
* On Windows
|
|
90
|
-
*
|
|
94
|
+
* On Windows a .cmd shim (dsh.cmd / pnpm.cmd) is executed through the
|
|
95
|
+
* shell; the command line is built here with per-argument quoting, so
|
|
96
|
+
* Node's `args + shell:true` deprecation (DEP0190) is not triggered.
|
|
91
97
|
*/
|
|
92
|
-
function run(bin, args, { capture = false } = {}) {
|
|
98
|
+
function run(bin, args, { capture = false, cwd } = {}) {
|
|
93
99
|
return new Promise((resolve) => {
|
|
94
|
-
const
|
|
95
|
-
shell: process.platform === "win32",
|
|
100
|
+
const opts = {
|
|
96
101
|
stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit",
|
|
97
102
|
windowsHide: true,
|
|
98
|
-
|
|
103
|
+
...(cwd ? { cwd } : {}),
|
|
104
|
+
};
|
|
105
|
+
let child;
|
|
106
|
+
if (process.platform === "win32") {
|
|
107
|
+
child = spawn([bin, ...args].map(quoteArg).join(" "), { ...opts, shell: true });
|
|
108
|
+
} else {
|
|
109
|
+
child = spawn(bin, args, opts);
|
|
110
|
+
}
|
|
99
111
|
let stdout = "";
|
|
100
112
|
let stderr = "";
|
|
101
113
|
if (capture) {
|
|
@@ -127,6 +139,88 @@ function installedVersion(pkg, profile) {
|
|
|
127
139
|
return installedManifest(pkg, profile)?.version ?? null;
|
|
128
140
|
}
|
|
129
141
|
|
|
142
|
+
const dryRunMode = () => process.env.DSH_ENHANCE_DRY_RUN === "1";
|
|
143
|
+
|
|
144
|
+
/** Locate pnpm on PATH (pnpm.cmd / pnpm.exe / pnpm). */
|
|
145
|
+
function findPnpm() {
|
|
146
|
+
const names = process.platform === "win32" ? ["pnpm.cmd", "pnpm.exe", "pnpm"] : ["pnpm"];
|
|
147
|
+
const entries = (process.env.PATH ?? "").split(process.platform === "win32" ? ";" : ":").filter(Boolean);
|
|
148
|
+
for (const dir of entries) {
|
|
149
|
+
for (const name of names) {
|
|
150
|
+
const candidate = join(dir, name);
|
|
151
|
+
if (existsSync(candidate)) return candidate;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Latest published version of a package from the npm registry (no auth). */
|
|
158
|
+
async function latestVersion(pkg) {
|
|
159
|
+
try {
|
|
160
|
+
const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(pkg)}`, {
|
|
161
|
+
headers: { Accept: "application/vnd.npm.install-v1+json" },
|
|
162
|
+
signal: AbortSignal.timeout(15000),
|
|
163
|
+
});
|
|
164
|
+
if (!res.ok) return null;
|
|
165
|
+
const data = await res.json();
|
|
166
|
+
return (data && data["dist-tags"] && data["dist-tags"].latest) || null;
|
|
167
|
+
} catch {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function profileJson(profile) {
|
|
173
|
+
try {
|
|
174
|
+
return JSON.parse(readFileSync(join(profileDir(profile), "package.json"), "utf8"));
|
|
175
|
+
} catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Set dependencies[pkg] to `^version` in the profile manifest so pnpm
|
|
182
|
+
* resolves the freshly published version (plain `pnpm update` keeps the
|
|
183
|
+
* lockfile resolution when the old version still satisfies the range).
|
|
184
|
+
* Returns true when the manifest would be / was changed.
|
|
185
|
+
*/
|
|
186
|
+
function bumpProfileRange(profile, pkg, version) {
|
|
187
|
+
const json = profileJson(profile);
|
|
188
|
+
if (!json || !json.dependencies || typeof json.dependencies[pkg] !== "string") return false;
|
|
189
|
+
const want = `^${version}`;
|
|
190
|
+
if (json.dependencies[pkg] === want) return false;
|
|
191
|
+
if (dryRunMode()) return true;
|
|
192
|
+
json.dependencies[pkg] = want;
|
|
193
|
+
writeFileSync(join(profileDir(profile), "package.json"), JSON.stringify(json, null, 2) + "\n", "utf8");
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Make sure `${pkg}@${version}` appears in pnpm-workspace.yaml's
|
|
199
|
+
* minimumReleaseAgeExclude (pnpm 11 blocks freshly published packages until
|
|
200
|
+
* they reach a minimum release age). Returns true when the file changed.
|
|
201
|
+
*/
|
|
202
|
+
function ensureReleaseAgeExclude(profile, pkg, version) {
|
|
203
|
+
const patch = join(profileDir(profile), "pnpm-workspace.yaml");
|
|
204
|
+
if (!existsSync(patch)) return false;
|
|
205
|
+
const entry = `${pkg}@${version}`;
|
|
206
|
+
const src = readFileSync(patch, "utf8");
|
|
207
|
+
if (src.includes(entry)) return false;
|
|
208
|
+
if (dryRunMode()) return true;
|
|
209
|
+
const lines = src.split("\n");
|
|
210
|
+
const idx = lines.findIndex((l) => l.trim() === "minimumReleaseAgeExclude:");
|
|
211
|
+
let next;
|
|
212
|
+
if (idx >= 0) {
|
|
213
|
+
let insertAt = idx + 1;
|
|
214
|
+
while (insertAt < lines.length && /^[ \t]+- /.test(lines[insertAt])) insertAt += 1;
|
|
215
|
+
lines.splice(insertAt, 0, ` - ${entry}`);
|
|
216
|
+
next = lines.join("\n");
|
|
217
|
+
} else {
|
|
218
|
+
next = src.replace(/\s*$/, "") + `\nminimumReleaseAgeExclude:\n - ${entry}\n`;
|
|
219
|
+
}
|
|
220
|
+
writeFileSync(patch, next, "utf8");
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
|
|
130
224
|
/** Bundle plugins are auto-mounted by their own dsh.bundle layer. */
|
|
131
225
|
function isBundlePackage(pkg, profile) {
|
|
132
226
|
const manifest = installedManifest(pkg, profile);
|
|
@@ -151,13 +245,36 @@ function isMountedInPatch(pkg, profile) {
|
|
|
151
245
|
* Mirrors the marketplace's own logic: bundle plugins are skipped (their
|
|
152
246
|
* bundle layer auto-mounts them), an empty `[]` patch is replaced with a
|
|
153
247
|
* block-style insert list, and a non-empty flow-style array is refused
|
|
154
|
-
* rather than corrupted.
|
|
248
|
+
* rather than corrupted. For bundle plugins, any STALE manual row left by an
|
|
249
|
+
* earlier non-bundle version is removed — two mounts of the same entry id
|
|
250
|
+
* abort boot.
|
|
155
251
|
*/
|
|
252
|
+
function escapeRegExp(text) {
|
|
253
|
+
return String(text).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Remove a `- id: ...` + `name: '<pkg>'` manual mount block, if present. */
|
|
257
|
+
function stripManualRow(src, pkg) {
|
|
258
|
+
const pattern = new RegExp(`[ \\t]*- id: [^\\n]*\\n[ \\t]*name: ['"]${escapeRegExp(pkg)}['"]\\n?`, "g");
|
|
259
|
+
return src.replace(pattern, "");
|
|
260
|
+
}
|
|
261
|
+
|
|
156
262
|
function ensureMounted(pkg, profile) {
|
|
157
263
|
const patch = join(profileDir(profile), "cordis.patch.yml");
|
|
158
264
|
if (!existsSync(patch)) return { changed: false, note: "cordis.patch.yml not found; mount manually" };
|
|
159
|
-
|
|
160
|
-
if (isBundlePackage(pkg, profile))
|
|
265
|
+
const inBundles = isInProfileBundles(pkg, profile);
|
|
266
|
+
if (inBundles || isBundlePackage(pkg, profile)) {
|
|
267
|
+
// The bundle layer auto-mounts this plugin. Remove a stale manual row
|
|
268
|
+
// (written before the package gained its dsh.bundle manifest) so boot
|
|
269
|
+
// does not fail with a duplicate loader entry.
|
|
270
|
+
let src = readFileSync(patch, "utf8");
|
|
271
|
+
const cleaned = stripManualRow(src, pkg);
|
|
272
|
+
if (cleaned !== src) {
|
|
273
|
+
if (process.env.DSH_ENHANCE_DRY_RUN !== "1") writeFileSync(patch, cleaned, "utf8");
|
|
274
|
+
return { changed: true, note: "removed stale manual mount row (bundle layer auto-mounts)" };
|
|
275
|
+
}
|
|
276
|
+
return { changed: false, note: inBundles ? "already mounted via dsh.profile.bundles" : "bundle plugin: auto-mounted via its own dsh.bundle layer" };
|
|
277
|
+
}
|
|
161
278
|
let src = readFileSync(patch, "utf8");
|
|
162
279
|
if (src.includes(`name: '${pkg}'`) || src.includes(`name: "${pkg}"`)) return { changed: false, note: "already mounted in cordis.patch.yml" };
|
|
163
280
|
const id = pkg.replace(/^@[^/]+\//, "").replace(/[^a-z0-9-]/g, "-") || pkg;
|
|
@@ -290,10 +407,21 @@ async function cmdInstall(profile, only, dryRun) {
|
|
|
290
407
|
const failures = [];
|
|
291
408
|
for (const p of plugins) {
|
|
292
409
|
info(`${p.emoji} ${p.name}`);
|
|
410
|
+
const latest = await latestVersion(p.name);
|
|
411
|
+
const installed = installedVersion(p.name, profile);
|
|
293
412
|
const addArgs = ["plugin", "--profile", profile, "add", p.name];
|
|
294
413
|
if (dryRun) {
|
|
414
|
+
ok(`npm latest: ${latest ?? "unknown (network?)"}; installed: ${installed ?? "not installed"}`);
|
|
415
|
+
if (latest && installed && installed !== latest) {
|
|
416
|
+
ok(`would bump range to ^${latest} (pnpm keeps stale lockfile versions otherwise)`);
|
|
417
|
+
ok(`would ensure minimumReleaseAgeExclude entry for ${p.name}@${latest}`);
|
|
418
|
+
}
|
|
295
419
|
ok(`would run: dsh ${addArgs.join(" ")}`);
|
|
296
420
|
} else {
|
|
421
|
+
if (latest && installed && installed !== latest) {
|
|
422
|
+
if (bumpProfileRange(profile, p.name, latest)) info(`range align: ^${installed} → ^${latest}`);
|
|
423
|
+
ensureReleaseAgeExclude(profile, p.name, latest);
|
|
424
|
+
}
|
|
297
425
|
const result = await run(dsh, addArgs);
|
|
298
426
|
if (result.code !== 0) {
|
|
299
427
|
const tail = (result.stderr || result.stdout || "").trim().split("\n").slice(-3).join(" ");
|
|
@@ -302,6 +430,25 @@ async function cmdInstall(profile, only, dryRun) {
|
|
|
302
430
|
continue;
|
|
303
431
|
}
|
|
304
432
|
ok(`installed via dsh plugin add`);
|
|
433
|
+
const after = installedVersion(p.name, profile);
|
|
434
|
+
if (latest && after !== latest) {
|
|
435
|
+
const pnpm = findPnpm();
|
|
436
|
+
if (!pnpm) {
|
|
437
|
+
bad(`${p.name}: still at ${after ?? "?"}, npm latest ${latest} — install pnpm and re-run`);
|
|
438
|
+
failures.push(`${p.name}: version mismatch (${after ?? "?"} ≠ ${latest})`);
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
info(`installed ${after ?? "?"} ≠ latest ${latest} — running pnpm install as fallback`);
|
|
442
|
+
const pr = await run(pnpm, ["install"], { capture: true, cwd: profileDir(profile) });
|
|
443
|
+
const final = installedVersion(p.name, profile);
|
|
444
|
+
if (pr.code !== 0 || final !== latest) {
|
|
445
|
+
const tail = (pr.stderr || pr.stdout || "").trim().split("\n").slice(-3).join(" ");
|
|
446
|
+
bad(`${p.name}: still at ${final ?? "?"}, npm latest ${latest} (pnpm exit ${pr.code}): ${tail || "no output"}`);
|
|
447
|
+
failures.push(`${p.name}: version mismatch (${final ?? "?"} ≠ ${latest})`);
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
ok(`pnpm install fallback: ${final}`);
|
|
451
|
+
}
|
|
305
452
|
}
|
|
306
453
|
const mount = ensureMounted(p.name, profile);
|
|
307
454
|
if (dryRun) {
|
|
@@ -342,21 +489,52 @@ async function cmdUpdate(profile, only, dryRun) {
|
|
|
342
489
|
info(`${p.emoji} ${p.name}: not installed — run "dsh-enhance install" first`);
|
|
343
490
|
continue;
|
|
344
491
|
}
|
|
492
|
+
const installed = installedVersion(p.name, profile);
|
|
493
|
+
info(`${p.emoji} ${p.name} (installed ${installed})`);
|
|
494
|
+
const latest = await latestVersion(p.name);
|
|
345
495
|
const args = ["plugin", "--profile", profile, "update", p.name];
|
|
346
496
|
if (dryRun) {
|
|
497
|
+
ok(`npm latest: ${latest ?? "unknown (network?)"}`);
|
|
498
|
+
if (latest && installed && installed !== latest) {
|
|
499
|
+
ok(`would bump range to ^${latest} (pnpm keeps stale lockfile versions otherwise)`);
|
|
500
|
+
ok(`would ensure minimumReleaseAgeExclude entry for ${p.name}@${latest}`);
|
|
501
|
+
}
|
|
347
502
|
ok(`would run: dsh ${args.join(" ")}`);
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
if (latest && installed && installed !== latest) {
|
|
506
|
+
if (bumpProfileRange(profile, p.name, latest)) info(`range align: ^${installed} → ^${latest}`);
|
|
507
|
+
ensureReleaseAgeExclude(profile, p.name, latest);
|
|
508
|
+
}
|
|
509
|
+
const result = await run(dsh, args);
|
|
510
|
+
if (result.code !== 0) {
|
|
511
|
+
const tail = (result.stderr || result.stdout || "").trim().split("\n").slice(-3).join(" ");
|
|
512
|
+
bad(`${p.name} update failed (${result.code}): ${tail || result.error?.message || "unknown error"}`);
|
|
513
|
+
failures.push(`${p.name}: dsh plugin update exited ${result.code}`);
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
let after = installedVersion(p.name, profile);
|
|
517
|
+
ok(`updated: ${after ?? "latest"}`);
|
|
518
|
+
if (latest && after !== latest) {
|
|
519
|
+
const pnpm = findPnpm();
|
|
520
|
+
if (!pnpm) {
|
|
521
|
+
bad(`${p.name}: still at ${after ?? "?"}, npm latest ${latest} — install pnpm and re-run`);
|
|
522
|
+
failures.push(`${p.name}: version mismatch (${after ?? "?"} ≠ ${latest})`);
|
|
354
523
|
continue;
|
|
355
524
|
}
|
|
356
|
-
|
|
525
|
+
info(`installed ${after ?? "?"} ≠ latest ${latest} — running pnpm install as fallback`);
|
|
526
|
+
const pr = await run(pnpm, ["install"], { capture: true, cwd: profileDir(profile) });
|
|
527
|
+
after = installedVersion(p.name, profile);
|
|
528
|
+
if (pr.code !== 0 || after !== latest) {
|
|
529
|
+
const tail = (pr.stderr || pr.stdout || "").trim().split("\n").slice(-3).join(" ");
|
|
530
|
+
bad(`${p.name}: still at ${after ?? "?"}, npm latest ${latest} (pnpm exit ${pr.code}): ${tail || "no output"}`);
|
|
531
|
+
failures.push(`${p.name}: version mismatch (${after ?? "?"} ≠ ${latest})`);
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
ok(`pnpm install fallback: ${after}`);
|
|
357
535
|
}
|
|
358
536
|
const mount = ensureMounted(p.name, profile);
|
|
359
|
-
if (
|
|
537
|
+
if (mount.changed) ok(mount.note);
|
|
360
538
|
}
|
|
361
539
|
head("");
|
|
362
540
|
if (failures.length > 0) {
|
|
@@ -398,9 +576,14 @@ function cmdDoctor(profile) {
|
|
|
398
576
|
continue;
|
|
399
577
|
}
|
|
400
578
|
ok(`${p.emoji} ${p.name}: installed (${version})`);
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
579
|
+
const inBundles = isInProfileBundles(p.name, profile);
|
|
580
|
+
const inPatch = isMountedInPatch(p.name, profile);
|
|
581
|
+
if (inBundles && inPatch) {
|
|
582
|
+
bad(` duplicate mount: manual row in cordis.patch.yml AND dsh.profile.bundles — run "dsh-enhance install" to clean up`);
|
|
583
|
+
healthy = false;
|
|
584
|
+
} else if (inBundles || inPatch || isBundlePackage(p.name, profile)) {
|
|
585
|
+
info(` mounted: ${inBundles ? "dsh.profile.bundles" : inPatch ? "cordis.patch.yml" : "own dsh.bundle layer"}`);
|
|
586
|
+
} else {
|
|
404
587
|
bad(` not mounted — run "dsh-enhance install"`);
|
|
405
588
|
healthy = false;
|
|
406
589
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-enhancement-suite",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Enhancement suite for DeepSeek Harness — the official installer and registry for Scorp1o117's Vision, Soul/Persona, Memory & Marketplace plugins.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|