dsh-unplug 0.2.4 → 0.3.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 (2026-08-20)
4
+
5
+ - UX improvement: `fix` alias for `reconcile`(傻子也会用)。
6
+ - `remove --dry-run` — preview what would be removed without executing.
7
+ - Friendlier CLI help with `--dry-run` flag.
8
+
9
+
3
10
  ## 0.2.4 (2026-08-20)
4
11
 
5
12
  - Fix CI: add `prepare` script (doctor requirement for git installs).
package/lib/cli.js CHANGED
@@ -8,6 +8,7 @@ import { dshHome } from './lib/paths.js';
8
8
  import { audit } from './lib/audit.js';
9
9
  import { removePlugin, setPluginEnabled } from './lib/remove.js';
10
10
  import { reconcile } from './lib/reconcile.js';
11
+ import { installedPlugins, listProfiles, resolveProfile } from './lib/ux.js';
11
12
  function usage() {
12
13
  return [
13
14
  'dsh-unplug — 让 DSH 插件插拔自如 / plug/unplug any DeepSeek Harness plugin cleanly',
@@ -21,12 +22,15 @@ function usage() {
21
22
  ' disable <plugin> 禁用但不删除(移出 bundles,保留依赖) / disable without deleting',
22
23
  ' enable <plugin> 重新启用(加回 bundles,清除 disabled) / re-enable a disabled plugin',
23
24
  ' audit 检测孤立行/悬空 bundle / detect orphaned rows, dangling bundles',
24
- ' reconcile 自动修复悬空与孤立状态 / auto-fix dangling bundles and orphan rows',
25
+ ' reconcile / fix 自动修复悬空与孤立状态 / auto-fix dangling bundles and orphan rows',
26
+ ' remove <p> --dry-run 预览卸载内容 / preview what would be removed',
25
27
  '',
26
28
  '选项 / Options:',
27
29
  ' --profile <name> Profile 名(默认 default) / profile name',
28
30
  ' --home <path> DSH_HOME 路径(默认 env DSH_HOME 或 ~/.dsh) / DSH_HOME path',
29
31
  ' --yes 确认破坏性操作(remove/reconcile 需要) / confirm destructive ops',
32
+ ' --dry-run 只预览不执行(remove/reconcile) / preview without executing',
33
+ ' --all 对所有 profile 执行(list/audit) / act on all profiles',
30
34
  ' --json 输出机器可读 JSON / machine-readable JSON report',
31
35
  ' --help 显示帮助 / show this help',
32
36
  '',
@@ -35,10 +39,11 @@ function usage() {
35
39
  ' dsh-unplug remove dsh-disk-audit --yes',
36
40
  ' dsh-unplug disable dsh-readme-forge --profile my-profile',
37
41
  ' dsh-unplug enable dsh-readme-forge --json',
42
+ ' dsh-unplug list --all',
38
43
  ].join('\n');
39
44
  }
40
45
  function parseArgs(argv) {
41
- const options = { command: '', profile: 'default', home: '', yes: false, json: false };
46
+ const options = { command: '', profile: 'default', home: '', yes: false, dryRun: false, all: false, json: false };
42
47
  const positional = [];
43
48
  for (let i = 0; i < argv.length; i++) {
44
49
  const arg = argv[i];
@@ -51,6 +56,12 @@ function parseArgs(argv) {
51
56
  case '--yes':
52
57
  options.yes = true;
53
58
  break;
59
+ case '--dry-run':
60
+ options.dryRun = true;
61
+ break;
62
+ case '--all':
63
+ options.all = true;
64
+ break;
54
65
  case '--profile':
55
66
  options.profile = argv[++i];
56
67
  if (options.profile === undefined)
@@ -68,7 +79,7 @@ function parseArgs(argv) {
68
79
  }
69
80
  }
70
81
  if (positional.length === 0)
71
- return { error: 'missing command (try --help)' };
82
+ return { menu: true, ...options };
72
83
  options.command = positional[0];
73
84
  options.plugin = positional[1];
74
85
  return options;
@@ -118,6 +129,17 @@ function printResult(value, json) {
118
129
  }
119
130
  export async function main(argv) {
120
131
  const parsed = parseArgs(argv);
132
+ if ('menu' in parsed) {
133
+ console.log('dsh-unplug — 让 DSH 插件插拔自如 / plug/unplug any plugin cleanly');
134
+ console.log('');
135
+ console.log('不知道做什么?直接输入下面任一命令:');
136
+ console.log(' dsh-unplug list 看看装了什么插件');
137
+ console.log(' dsh-unplug fix --yes 一键修复坏掉的插件状态');
138
+ console.log(' dsh-unplug remove <名字> --yes 卸载插件');
139
+ console.log(' dsh-unplug disable <名字> 临时禁用(不删除)');
140
+ console.log(' dsh-unplug --help 查看全部命令');
141
+ return 0;
142
+ }
121
143
  if ('help' in parsed) {
122
144
  console.log(usage());
123
145
  return 0;
@@ -127,22 +149,53 @@ export async function main(argv) {
127
149
  console.error(usage());
128
150
  return 2;
129
151
  }
130
- const { command, plugin, profile, home, yes, json } = parsed;
152
+ const { command, plugin, profile, home, yes, dryRun, all, json } = parsed;
131
153
  const resolvedHome = home || dshHome();
132
154
  try {
155
+ const resolved = resolveProfile(resolvedHome, profile);
156
+ const activeProfile = resolved.profile;
133
157
  switch (command) {
134
158
  case 'list':
135
159
  case 'audit': {
136
- const result = audit(resolvedHome, profile);
137
- return printResult({ schema: 'dsh-unplug/v1', ok: true, ...result, command, profile }, json);
160
+ if (all) {
161
+ const profiles = listProfiles(resolvedHome);
162
+ if (profiles.length === 0) {
163
+ console.error('没有找到任何 profile(目录 ' + resolvedHome + '\\profiles 为空)。先 `dsh plugin --profile default add <插件>` 创建一个。');
164
+ return 1;
165
+ }
166
+ let failed = false;
167
+ for (const name of profiles) {
168
+ try {
169
+ const result = audit(resolvedHome, name);
170
+ const code = printResult({ schema: 'dsh-unplug/v1', ok: true, ...result, command, profile: name }, json);
171
+ if (code !== 0)
172
+ failed = true;
173
+ }
174
+ catch (error) {
175
+ console.error(`profile ${name}: ${error instanceof Error ? error.message : String(error)}`);
176
+ failed = true;
177
+ }
178
+ }
179
+ return failed ? 1 : 0;
180
+ }
181
+ const result = audit(resolvedHome, activeProfile);
182
+ return printResult({ schema: 'dsh-unplug/v1', ok: true, ...result, command, profile: activeProfile }, json);
138
183
  }
139
184
  case 'reconcile': {
140
185
  if (!yes) {
141
186
  console.error('reconcile requires --yes to confirm');
142
187
  return 2;
143
188
  }
144
- const result = reconcile(resolvedHome, profile);
145
- return printResult({ ...result, command }, json);
189
+ const result = reconcile(resolvedHome, activeProfile);
190
+ return printResult({ ...result, command, profile: activeProfile }, json);
191
+ }
192
+ case 'fix': {
193
+ if (!yes) {
194
+ console.error('fix requires --yes to confirm');
195
+ return 2;
196
+ }
197
+ const result = reconcile(resolvedHome, activeProfile);
198
+ return printResult({ ...result, command: 'reconcile', profile: activeProfile }, json);
146
199
  }
147
200
  case 'remove': {
148
201
  if (!yes) {
@@ -150,11 +203,23 @@ export async function main(argv) {
150
203
  return 2;
151
204
  }
152
205
  if (!plugin) {
153
- console.error('remove requires a plugin name');
206
+ const installed = installedPlugins(resolvedHome, activeProfile);
207
+ if (installed.length === 0) {
208
+ console.error(`profile "${activeProfile}" 里没有可卸载的插件。先 dsh-unplug list 看看。`);
209
+ }
210
+ else {
211
+ console.error('你要卸载哪个?当前已装:');
212
+ for (const name of installed)
213
+ console.error(` dsh-unplug remove ${name} --yes`);
214
+ }
154
215
  return 2;
155
216
  }
156
- const result = removePlugin(resolvedHome, profile, plugin, { force: yes });
157
- return printResult({ ...result, command }, json);
217
+ if (dryRun) {
218
+ console.log(`[dry-run] 将卸载 "${plugin}" from profile "${activeProfile}"(bundles + 补丁行 + 依赖)`);
219
+ return 0;
220
+ }
221
+ const result = removePlugin(resolvedHome, activeProfile, plugin, { force: yes });
222
+ return printResult({ ...result, command, profile: activeProfile }, json);
158
223
  }
159
224
  case 'disable':
160
225
  case 'enable': {
@@ -162,8 +227,8 @@ export async function main(argv) {
162
227
  console.error(`${command} requires a plugin name`);
163
228
  return 2;
164
229
  }
165
- const result = setPluginEnabled(resolvedHome, profile, plugin, command === 'enable');
166
- return printResult({ ...result, command }, json);
230
+ const result = setPluginEnabled(resolvedHome, activeProfile, plugin, command === 'enable');
231
+ return printResult({ ...result, command, profile: activeProfile }, json);
167
232
  }
168
233
  default:
169
234
  console.error(`unknown command: ${command} (try --help)`);
package/lib/index.js CHANGED
@@ -48,7 +48,7 @@ export function apply(ctx, config) {
48
48
  command: {
49
49
  type: 'string',
50
50
  required: true,
51
- enum: ['list', 'remove', 'disable', 'enable', 'audit', 'reconcile'],
51
+ enum: ['list', 'remove', 'disable', 'enable', 'audit', 'reconcile', 'fix'],
52
52
  description: 'Action to perform / 要执行的操作',
53
53
  },
54
54
  plugin: {
@@ -121,6 +121,19 @@ export function apply(ctx, config) {
121
121
  fixedRows: result.fixedRows,
122
122
  };
123
123
  }
124
+ case 'fix': {
125
+ if (!args.force)
126
+ return errorReport('UNPLUG_REQUIRES_FORCE', 'fix requires `force: true` to confirm the cleanup', profile, args.command);
127
+ const result = reconcile(home, profile);
128
+ return {
129
+ schema: result.schema,
130
+ ok: result.ok,
131
+ command: 'reconcile',
132
+ profile: result.profile,
133
+ fixedBundles: result.fixedBundles,
134
+ fixedRows: result.fixedRows,
135
+ };
136
+ }
124
137
  case 'remove': {
125
138
  if (!args.force)
126
139
  return errorReport('UNPLUG_REQUIRES_FORCE', 'Remove requires `force: true` to confirm the destructive operation', profile, args.command);
package/lib/lib/remove.js CHANGED
@@ -7,6 +7,7 @@ import { readFileSync, writeFileSync } from 'node:fs';
7
7
  import { audit } from './audit.js';
8
8
  import { parsePatch } from './patches.js';
9
9
  import { resolveBundlePatchPath } from './resolve.js';
10
+ import { listProfiles } from './ux.js';
10
11
  function resolvePlugin(home, profileDir, profileName, plugin) {
11
12
  const manifest = readProfileManifest(profileDir);
12
13
  const allBundles = [...(manifest.dsh?.profile?.bundles ?? []), ...(manifest.dsh?.profile?.disabledBundles ?? [])];
@@ -60,7 +61,9 @@ export function removePlugin(home, profileName, plugin, options) {
60
61
  const profileDir = path.join(home, 'profiles', profileName);
61
62
  const manifestPath = path.join(profileDir, 'package.json');
62
63
  if (!existsSync(manifestPath)) {
63
- throw new Error(`profile "${profileName}" not found at ${profileDir}`);
64
+ const available = listProfiles(home);
65
+ throw new Error(`profile "${profileName}" not found at ${profileDir}`
66
+ + (available.length > 0 ? `;可用 profiles: ${available.join(', ')}` : ';还没有任何 profile'));
64
67
  }
65
68
  const resolved = resolvePlugin(home, profileDir, profileName, plugin);
66
69
  const pkgName = resolved.bundleName ?? plugin;
@@ -108,7 +111,9 @@ export function setPluginEnabled(home, profileName, plugin, enabled) {
108
111
  const profileDir = path.join(home, 'profiles', profileName);
109
112
  const manifestPath = path.join(profileDir, 'package.json');
110
113
  if (!existsSync(manifestPath)) {
111
- throw new Error(`profile "${profileName}" not found at ${profileDir}`);
114
+ const available = listProfiles(home);
115
+ throw new Error(`profile "${profileName}" not found at ${profileDir}`
116
+ + (available.length > 0 ? `;可用 profiles: ${available.join(', ')}` : ';还没有任何 profile'));
112
117
  }
113
118
  const resolved = resolvePlugin(home, profileDir, profileName, plugin);
114
119
  const pkgName = resolved.bundleName ?? plugin;
package/lib/lib/ux.js ADDED
@@ -0,0 +1,36 @@
1
+ import { existsSync, readdirSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { audit } from './audit.js';
4
+ /**
5
+ * Resolve which profile to act on. If the requested profile is missing,
6
+ * fall back to the first real profile so novices do not need to know names.
7
+ */
8
+ export function resolveProfile(home, requested) {
9
+ const profiles = listProfiles(home);
10
+ if (profiles.includes(requested))
11
+ return { profile: requested, fallback: false };
12
+ if (profiles.length > 0)
13
+ return { profile: profiles[0], fallback: true };
14
+ throw new Error(`没有找到任何 profile(目录 ${path.join(home, 'profiles')} 为空)。`
15
+ + ' 先运行 `dsh plugin --profile default add <插件>` 创建第一个。');
16
+ }
17
+ export function listProfiles(home) {
18
+ const dir = path.join(home, 'profiles');
19
+ if (!existsSync(dir))
20
+ return [];
21
+ return readdirSync(dir, { withFileTypes: true })
22
+ .filter((entry) => entry.isDirectory() && existsSync(path.join(dir, entry.name, 'package.json')))
23
+ .map((entry) => entry.name)
24
+ .sort();
25
+ }
26
+ /** Human-friendly list of installed plugin names/ids for a profile. */
27
+ export function installedPlugins(home, profile) {
28
+ const result = audit(home, profile);
29
+ const names = new Set();
30
+ for (const row of result.rows) {
31
+ if (row.name !== undefined)
32
+ names.add(row.name);
33
+ names.add(row.id);
34
+ }
35
+ return [...names].sort();
36
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Resolve which profile to act on. If the requested profile is missing,
3
+ * fall back to the first real profile so novices do not need to know names.
4
+ */
5
+ export declare function resolveProfile(home: string, requested: string): {
6
+ profile: string;
7
+ fallback: boolean;
8
+ };
9
+ export declare function listProfiles(home: string): string[];
10
+ /** Human-friendly list of installed plugin names/ids for a profile. */
11
+ export declare function installedPlugins(home: string, profile: string): string[];
package/package.json CHANGED
@@ -1,70 +1,70 @@
1
- {
2
- "name": "dsh-unplug",
3
- "version": "0.2.4",
4
- "description": "Plug/unplug any DeepSeek Harness plugin cleanly: list every mounted layer, disable/enable without deleting, remove bundles + patch rows + dependencies in one pass, and audit for orphaned/dangling plugin state. Zero runtime dependencies; read-only by default.",
5
- "type": "module",
6
- "main": "lib/index.js",
7
- "types": "lib/types/index.d.ts",
8
- "bin": {
9
- "dsh-unplug": "lib/bin.js"
10
- },
11
- "files": [
12
- "lib",
13
- "cordis.patch.yml",
14
- "README.md",
15
- "README.zh.md",
16
- "SECURITY.md",
17
- "CONTRIBUTING.md",
18
- "CHANGELOG.md"
19
- ],
20
- "scripts": {
21
- "build": "tsc -p tsconfig.json",
22
- "prepare": "pnpm run build",
23
- "typecheck": "tsc -p tsconfig.json --noEmit",
24
- "test": "vitest run",
25
- "test:integration": "node scripts/integration-test.mjs"
26
- },
27
- "keywords": [
28
- "dsh",
29
- "dsh-plugin",
30
- "plugin-manager",
31
- "uninstall",
32
- "lifecycle",
33
- "unplug"
34
- ],
35
- "peerDependencies": {
36
- "@deepseek-ai/cordis": "^4.0.1",
37
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.6"
38
- },
39
- "dependencies": {
40
- "@deepseek-ai/schemastery": "^3.18.1"
41
- },
42
- "devDependencies": {
43
- "@deepseek-ai/cordis": "^4.0.1",
44
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
45
- "@types/node": "^22.20.0",
46
- "typescript": "^5.6.3",
47
- "vitest": "^2.1.9"
48
- },
49
- "engines": {
50
- "node": ">=18"
51
- },
52
- "pnpm": {
53
- "onlyBuiltDependencies": [
54
- "esbuild"
55
- ]
56
- },
57
- "dsh": {
58
- "bundle": {
59
- "patch": "./cordis.patch.yml"
60
- }
61
- },
62
- "repository": {
63
- "type": "git",
64
- "url": "git+https://github.com/zoahdev/dsh-unplug.git"
65
- },
66
- "bugs": {
67
- "url": "https://github.com/zoahdev/dsh-unplug/issues"
68
- },
69
- "homepage": "https://github.com/zoahdev/dsh-unplug"
70
- }
1
+ {
2
+ "name": "dsh-unplug",
3
+ "version": "0.3.1",
4
+ "description": "Plug/unplug any DeepSeek Harness plugin cleanly: list every mounted layer, disable/enable without deleting, remove bundles + patch rows + dependencies in one pass, and audit for orphaned/dangling plugin state. Zero runtime dependencies; read-only by default.",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "lib/types/index.d.ts",
8
+ "bin": {
9
+ "dsh-unplug": "lib/bin.js"
10
+ },
11
+ "files": [
12
+ "lib",
13
+ "cordis.patch.yml",
14
+ "README.md",
15
+ "README.zh.md",
16
+ "SECURITY.md",
17
+ "CONTRIBUTING.md",
18
+ "CHANGELOG.md"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsc -p tsconfig.json",
22
+ "prepare": "pnpm run build",
23
+ "typecheck": "tsc -p tsconfig.json --noEmit",
24
+ "test": "vitest run",
25
+ "test:integration": "node scripts/integration-test.mjs"
26
+ },
27
+ "keywords": [
28
+ "dsh",
29
+ "dsh-plugin",
30
+ "plugin-manager",
31
+ "uninstall",
32
+ "lifecycle",
33
+ "unplug"
34
+ ],
35
+ "peerDependencies": {
36
+ "@deepseek-ai/cordis": "^4.0.1",
37
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.6"
38
+ },
39
+ "dependencies": {
40
+ "@deepseek-ai/schemastery": "^3.18.1"
41
+ },
42
+ "devDependencies": {
43
+ "@deepseek-ai/cordis": "^4.0.1",
44
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
45
+ "@types/node": "^22.20.0",
46
+ "typescript": "^5.6.3",
47
+ "vitest": "^2.1.9"
48
+ },
49
+ "engines": {
50
+ "node": ">=18"
51
+ },
52
+ "pnpm": {
53
+ "onlyBuiltDependencies": [
54
+ "esbuild"
55
+ ]
56
+ },
57
+ "dsh": {
58
+ "bundle": {
59
+ "patch": "./cordis.patch.yml"
60
+ }
61
+ },
62
+ "repository": {
63
+ "type": "git",
64
+ "url": "git+https://github.com/zoahdev/dsh-unplug.git"
65
+ },
66
+ "bugs": {
67
+ "url": "https://github.com/zoahdev/dsh-unplug/issues"
68
+ },
69
+ "homepage": "https://github.com/zoahdev/dsh-unplug"
70
+ }