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 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. `dsh-plugin-marketplace` declares `dsh.bundle.patch`, so `dsh plugin add` reconciles it into `dsh.profile.bundles` and its bundle layer auto-mounts it.
98
- 3. The other three are plain dependencies; the suite appends their mount rows to the profile's `cordis.patch.yml` with the same safe, idempotent logic the marketplace's own one-click install uses (an empty `[]` patch is replaced correctly, duplicates are skipped, and a flow-style array is refused instead of corrupted).
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.
@@ -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 the resolved .cmd shim is executed through the shell with
90
- * per-argument quoting applied by Node, which keeps every argument opaque.
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 child = spawn(bin, args, {
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
- if (isInProfileBundles(pkg, profile)) return { changed: false, note: "already mounted via dsh.profile.bundles" };
160
- if (isBundlePackage(pkg, profile)) return { changed: false, note: "bundle plugin: auto-mounted via its own dsh.bundle layer" };
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
- } else {
349
- const result = await run(dsh, args);
350
- if (result.code !== 0) {
351
- const tail = (result.stderr || result.stdout || "").trim().split("\n").slice(-3).join(" ");
352
- bad(`${p.name} update failed (${result.code}): ${tail || result.error?.message || "unknown error"}`);
353
- failures.push(`${p.name}: dsh plugin update exited ${result.code}`);
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
- ok(`updated: ${p.name} → ${installedVersion(p.name, profile) ?? "latest"}`);
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 (!dryRun && mount.changed) ok(mount.note);
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 mounted = isInProfileBundles(p.name, profile) || isMountedInPatch(p.name, profile) || isBundlePackage(p.name, profile);
402
- if (mounted) info(` mounted: ${isInProfileBundles(p.name, profile) ? "dsh.profile.bundles" : isMountedInPatch(p.name, profile) ? "cordis.patch.yml" : "own dsh.bundle layer"}`);
403
- else {
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.0",
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",