dsh-enhancement-suite 0.1.1 → 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.
Files changed (2) hide show
  1. package/bin/dsh-enhance.mjs +169 -14
  2. package/package.json +1 -1
@@ -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);
@@ -313,10 +407,21 @@ async function cmdInstall(profile, only, dryRun) {
313
407
  const failures = [];
314
408
  for (const p of plugins) {
315
409
  info(`${p.emoji} ${p.name}`);
410
+ const latest = await latestVersion(p.name);
411
+ const installed = installedVersion(p.name, profile);
316
412
  const addArgs = ["plugin", "--profile", profile, "add", p.name];
317
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
+ }
318
419
  ok(`would run: dsh ${addArgs.join(" ")}`);
319
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
+ }
320
425
  const result = await run(dsh, addArgs);
321
426
  if (result.code !== 0) {
322
427
  const tail = (result.stderr || result.stdout || "").trim().split("\n").slice(-3).join(" ");
@@ -325,6 +430,25 @@ async function cmdInstall(profile, only, dryRun) {
325
430
  continue;
326
431
  }
327
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
+ }
328
452
  }
329
453
  const mount = ensureMounted(p.name, profile);
330
454
  if (dryRun) {
@@ -365,21 +489,52 @@ async function cmdUpdate(profile, only, dryRun) {
365
489
  info(`${p.emoji} ${p.name}: not installed — run "dsh-enhance install" first`);
366
490
  continue;
367
491
  }
492
+ const installed = installedVersion(p.name, profile);
493
+ info(`${p.emoji} ${p.name} (installed ${installed})`);
494
+ const latest = await latestVersion(p.name);
368
495
  const args = ["plugin", "--profile", profile, "update", p.name];
369
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
+ }
370
502
  ok(`would run: dsh ${args.join(" ")}`);
371
- } else {
372
- const result = await run(dsh, args);
373
- if (result.code !== 0) {
374
- const tail = (result.stderr || result.stdout || "").trim().split("\n").slice(-3).join(" ");
375
- bad(`${p.name} update failed (${result.code}): ${tail || result.error?.message || "unknown error"}`);
376
- 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})`);
523
+ continue;
524
+ }
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})`);
377
532
  continue;
378
533
  }
379
- ok(`updated: ${p.name} → ${installedVersion(p.name, profile) ?? "latest"}`);
534
+ ok(`pnpm install fallback: ${after}`);
380
535
  }
381
536
  const mount = ensureMounted(p.name, profile);
382
- if (!dryRun && mount.changed) ok(mount.note);
537
+ if (mount.changed) ok(mount.note);
383
538
  }
384
539
  head("");
385
540
  if (failures.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-enhancement-suite",
3
- "version": "0.1.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",