@moonquake2004/dsh-doctor 0.4.20 → 0.4.22

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/dsh-doctor.mjs +112 -1
  2. package/package.json +1 -1
package/dsh-doctor.mjs CHANGED
@@ -2106,6 +2106,49 @@ function runBootCheckSync(profileDir) {
2106
2106
  return out;
2107
2107
  }
2108
2108
 
2109
+
2110
+ /* ---- 升级前后基线(2026-09):升级 dsh 前记一次,升级后自动复检 ---- */
2111
+
2112
+ const PRE_UPGRADE_FILE = '.dsh-doctor-pre-upgrade.json';
2113
+
2114
+ /** 取当前 dsh 核心版本:先问 CLI,再从 PATH 里 dsh 的安装树读 package.json。 */
2115
+ function coreVersion() {
2116
+ try {
2117
+ const bin = process.platform === 'win32' ? 'dsh.cmd' : 'dsh';
2118
+ const r = spawnSync(bin, ['--version'], { encoding: 'utf8', timeout: 10000, shell: process.platform === 'win32' });
2119
+ const m = String(r.stdout || '').trim().match(/\d+\.\d+\.\d+[-\w.]*/);
2120
+ if (m) return m[0];
2121
+ } catch { /* 落到安装树 */ }
2122
+ for (const p of (process.env.PATH || '').split(PATH_DELIM)) {
2123
+ if (p.endsWith('node_modules/.bin') && existsSync(join(p, 'dsh'))) {
2124
+ try {
2125
+ const mf = join(dirname(p), '@deepseek-ai', 'dsh', 'package.json');
2126
+ return JSON.parse(readFileSync(mf, 'utf8')).version ?? null;
2127
+ } catch { /* 继续找 */ }
2128
+ }
2129
+ }
2130
+ return null;
2131
+ }
2132
+
2133
+ /** 升级前基线:核心版本 + bundle 版本 + entry 列表 + manifest 原文(用于必要时人工比对)。 */
2134
+ function writePreUpgrade(profileDir) {
2135
+ const snap = bootSnapshot(profileDir);
2136
+ const payload = {
2137
+ kind: 'pre-upgrade',
2138
+ at: snap.at,
2139
+ coreVersion: coreVersion(),
2140
+ bundles: snap.bundles,
2141
+ entries: snap.entries,
2142
+ bundleCount: Object.keys(snap.bundles).length,
2143
+ };
2144
+ writeFileSync(join(profileDir, PRE_UPGRADE_FILE), JSON.stringify(payload, null, 2) + '\n');
2145
+ return payload;
2146
+ }
2147
+
2148
+ function readPreUpgrade(profileDir) {
2149
+ try { return JSON.parse(readFileSync(join(profileDir, PRE_UPGRADE_FILE), 'utf8')); } catch { return null; }
2150
+ }
2151
+
2109
2152
  async function runBootCheck(profileDir) {
2110
2153
  const entries = collectBootEntries(profileDir);
2111
2154
  const targets = entries.filter((e) => e.name);
@@ -2158,6 +2201,67 @@ async function run() {
2158
2201
  const quarantineArg = flagValue('--quarantine');
2159
2202
  const unquarantineArg = flagValue('--unquarantine');
2160
2203
  const safeAddArg = flagValue('--safe-add');
2204
+ const preUpgradeArg = process.argv.includes('--pre-upgrade');
2205
+ const postUpgradeArg = process.argv.includes('--post-upgrade');
2206
+ if (preUpgradeArg || postUpgradeArg) {
2207
+ const profDir = resolveProfile(profileArg || 'web');
2208
+ if (preUpgradeArg) {
2209
+ const base = writePreUpgrade(profDir);
2210
+ if (jsonOut) console.log(JSON.stringify({ ok: true, ...base }, null, 2));
2211
+ else {
2212
+ console.log(`✓ 已记录升级前基线(${profDir})`);
2213
+ console.log(` 核心版本: ${base.coreVersion ?? '(未能确定)'} | bundle ${base.bundleCount} 个 | entry ${Object.keys(base.entries).length} 条`);
2214
+ console.log(' 下一步:升级 dsh,然后运行');
2215
+ console.log(` npx @moonquake2004/dsh-doctor --post-upgrade --profile ${profileArg || 'web'}`);
2216
+ console.log(' (--post-upgrade 会自动对比核心与插件变化、跑装载模拟,并给出修复/隔离命令)');
2217
+ }
2218
+ process.exit(0);
2219
+ }
2220
+ const base = readPreUpgrade(profDir);
2221
+ if (!base) {
2222
+ console.error(`✗ 没找到升级前基线(${join(profDir, PRE_UPGRADE_FILE)})——请先在升级前运行 --pre-upgrade`);
2223
+ process.exit(1);
2224
+ }
2225
+ const nowCore = coreVersion();
2226
+ const nowSnap = bootSnapshot(profDir);
2227
+ const drift = diffSnapshot({ bundles: base.bundles, entries: base.entries }, nowSnap) ?? {};
2228
+ const results = runBootCheckSync(profDir);
2229
+ const failed = results.filter((r) => r.status === 'failed');
2230
+ const coreChanged = base.coreVersion !== nowCore;
2231
+ const lines = [];
2232
+ lines.push(`核心版本: ${base.coreVersion ?? '?'} → ${nowCore ?? '?'}${coreChanged ? '(已变化)' : '(未变化)'}`);
2233
+ if ((drift.addedBundles ?? []).length) lines.push(`新增 bundle: ${drift.addedBundles.join(', ')}`);
2234
+ if ((drift.removedBundles ?? []).length) lines.push(`移除 bundle: ${drift.removedBundles.join(', ')}`);
2235
+ if ((drift.versionChanged ?? []).length) lines.push(`插件版本变化: ${drift.versionChanged.join('; ')}`);
2236
+ if ((drift.addedEntries ?? []).length) lines.push(`新增/变更 entry: ${drift.addedEntries.join('; ')}`);
2237
+ if (jsonOut) {
2238
+ console.log(JSON.stringify({ ok: failed.length === 0, profile: profDir, baselineAt: base.at, coreChanged, from: base.coreVersion, to: nowCore, drift, checked: results.length, failed: failed.length, results }, null, 2));
2239
+ process.exit(failed.length ? 2 : 0);
2240
+ }
2241
+ console.log(`升级后复检(基线取自 ${String(base.at).slice(0, 16)}):`);
2242
+ for (const l of lines) console.log(` · ${l}`);
2243
+ if (!failed.length) {
2244
+ console.log(' ✓ 装载模拟通过——升级未破坏任何可探测 entry');
2245
+ writeSnapshot(profDir, nowSnap);
2246
+ } else {
2247
+ console.log(` ✗ 装载模拟失败 ${failed.length} 条(这就是"升级后起不来"的直接原因):`);
2248
+ for (const f of failed) {
2249
+ console.log(` [${f.bundle}] ${f.id} → ${f.spec}`);
2250
+ console.log(` ${f.kind}: ${f.error}`);
2251
+ console.log(` 修复方向:${f.hint}`);
2252
+ console.log(` 先起来:npx @moonquake2004/dsh-doctor --quarantine ${f.bundle} --profile ${profileArg || 'web'}`);
2253
+ }
2254
+ if (process.argv.includes('--auto-quarantine')) {
2255
+ const q = [];
2256
+ for (const name of new Set(failed.map((f) => f.bundle))) {
2257
+ if (name === '(user patch)') continue;
2258
+ try { const r = quarantineBundle(profDir, name, false); if (r.ok) q.push(name); } catch { /* 忽略 */ }
2259
+ }
2260
+ if (q.length) console.log(` → 已自动隔离: ${q.join(', ')}(重启 dsh 即可;用 --unquarantine 放回)`);
2261
+ }
2262
+ }
2263
+ process.exit(failed.length ? 2 : 0);
2264
+ }
2161
2265
  if (safeAddArg) {
2162
2266
  // 装完立即预检、坏了自动回滚——"装了个不兼容插件导致 dsh 起不来"的预防手段
2163
2267
  const profDir = resolveProfile(profileArg || 'web');
@@ -2182,7 +2286,14 @@ async function run() {
2182
2286
  if (quarantineArg || unquarantineArg) {
2183
2287
  const r = quarantineBundle(profDir, quarantineArg || unquarantineArg, !!unquarantineArg);
2184
2288
  if (!r.ok) { console.error(`✗ ${r.error}`); process.exit(1); }
2185
- console.log(JSON.stringify({ ok: true, action: unquarantineArg ? 'unquarantine' : 'quarantine', ...r, next: '重启 dsh;随后用 --boot-check 复查,或用 --unquarantine 撤销' }, null, 2));
2289
+ const payload = { ok: true, action: unquarantineArg ? 'unquarantine' : 'quarantine', ...r, next: '重启 dsh;随后用 --boot-check 复查,或用 --unquarantine 撤销' };
2290
+ if (jsonOut) console.log(JSON.stringify(payload, null, 2));
2291
+ else if (unquarantineArg) {
2292
+ console.log(`✓ 已放回 ${unquarantineArg}(当前启动列表 ${r.bundles} 项)——重启 dsh 生效`);
2293
+ } else {
2294
+ console.log(`✓ 已隔离 ${quarantineArg}(启动列表现为 ${r.bundles} 项;原始 package.json 已备份)`);
2295
+ console.log(` 重启 dsh,然后用 --boot-check 复查;要放回:--unquarantine ${quarantineArg}`);
2296
+ }
2186
2297
  process.exit(0);
2187
2298
  }
2188
2299
  const results = await runBootCheck(profDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moonquake2004/dsh-doctor",
3
- "version": "0.4.20",
3
+ "version": "0.4.22",
4
4
  "description": "Offline diagnostic for DeepSeek Harness — 28 built-in + 5 catalog checks across env/profile/session (Layer A checks-as-data), self-update (Layer B), and a semi-automatic LLM observer (Layer C, --observe); Doctor panel in web UI settings.",
5
5
  "main": "lib/index.js",
6
6
  "files": [