dsh-unplug 0.1.0 → 0.2.0

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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0 (2026-08-20)
4
+
5
+ - New `reconcile` command: auto-fix dangling bundles and orphaned patch rows
6
+ (profile manifest + profile/home cordis.patch.yml), with `--yes`/`force: true`.
7
+ - CLI and agent tool both expose `reconcile`.
8
+
9
+ ## 0.1.1 (2026-08-20)
10
+
11
+ - resolvePlugin now scans installed bundle patches, so `remove`/`disable`/`enable`
12
+ work when a plugin row lives only inside the bundle package (not the profile patch).
13
+ - Tests for bundle-only row resolution.
14
+
3
15
  ## 0.1.0 (2026-08-20)
4
16
 
5
17
  - Initial release.
package/lib/cli.js CHANGED
@@ -7,6 +7,7 @@
7
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
+ import { reconcile } from './lib/reconcile.js';
10
11
  function usage() {
11
12
  return [
12
13
  'dsh-unplug — plug/unplug any DeepSeek Harness plugin cleanly',
@@ -20,6 +21,7 @@ function usage() {
20
21
  ' disable <plugin> Disable without deleting (removes from bundles, keeps deps)',
21
22
  ' enable <plugin> Re-enable a disabled plugin (re-adds to bundles, clears disabled flag)',
22
23
  ' audit Detect orphaned rows, dangling bundles, missing patch files',
24
+ ' reconcile Auto-fix dangling bundles and orphaned rows',
23
25
  '',
24
26
  'Options:',
25
27
  ' --profile <name> Profile name (default: default)',
@@ -131,6 +133,14 @@ export async function main(argv) {
131
133
  const result = audit(resolvedHome, profile);
132
134
  return printResult({ ...result, command, profile }, json);
133
135
  }
136
+ case 'reconcile': {
137
+ if (!yes) {
138
+ console.error('reconcile requires --yes to confirm');
139
+ return 2;
140
+ }
141
+ const result = reconcile(resolvedHome, profile);
142
+ return printResult({ ...result, command }, json);
143
+ }
134
144
  case 'remove': {
135
145
  if (!yes) {
136
146
  console.error('remove requires --yes to confirm');
package/lib/index.js CHANGED
@@ -9,6 +9,7 @@ import { satisfiesCaret } from './lib/version.js';
9
9
  import { dshHome } from './lib/paths.js';
10
10
  import { audit } from './lib/audit.js';
11
11
  import { removePlugin, setPluginEnabled } from './lib/remove.js';
12
+ import { reconcile } from './lib/reconcile.js';
12
13
  export const name = 'dsh-unplug';
13
14
  export const inject = ['tools'];
14
15
  export const TESTED_PEER_RANGE = '^0.1.0-rc.6';
@@ -47,7 +48,7 @@ export function apply(ctx, config) {
47
48
  command: {
48
49
  type: 'string',
49
50
  required: true,
50
- enum: ['list', 'remove', 'disable', 'enable', 'audit'],
51
+ enum: ['list', 'remove', 'disable', 'enable', 'audit', 'reconcile'],
51
52
  description: 'Action to perform',
52
53
  },
53
54
  plugin: {
@@ -105,6 +106,19 @@ export function apply(ctx, config) {
105
106
  const result = audit(home, profile);
106
107
  return { schema: 'dsh-unplug/v1', ok: true, command: args.command, profile, bundles: result.bundles, disabledBundles: result.disabledBundles };
107
108
  }
109
+ case 'reconcile': {
110
+ if (!args.force)
111
+ return errorReport('UNPLUG_REQUIRES_FORCE', 'reconcile requires `force: true` to confirm the cleanup', profile, args.command);
112
+ const result = reconcile(home, profile);
113
+ return {
114
+ schema: result.schema,
115
+ ok: result.ok,
116
+ command: args.command,
117
+ profile: result.profile,
118
+ fixedBundles: result.fixedBundles,
119
+ fixedRows: result.fixedRows,
120
+ };
121
+ }
108
122
  case 'remove': {
109
123
  if (!args.force)
110
124
  return errorReport('UNPLUG_REQUIRES_FORCE', 'Remove requires `force: true` to confirm the destructive operation', profile, args.command);
@@ -0,0 +1,54 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { audit } from './audit.js';
4
+ import { bundlesOf, disabledBundlesOf, readProfileManifest, removeBundle, writeProfileManifest } from './manifest.js';
5
+ import { parsePatch, removeEntry } from './patches.js';
6
+ import { isResolvable } from './resolve.js';
7
+ /**
8
+ * Auto-fix dangling state: remove unresolvable bundles from the profile manifest
9
+ * and strip patch rows that reference packages which are not installed.
10
+ */
11
+ export function reconcile(home, profileName) {
12
+ const profileDir = path.join(home, 'profiles', profileName);
13
+ const manifestPath = path.join(profileDir, 'package.json');
14
+ if (!existsSync(manifestPath)) {
15
+ throw new Error(`profile "${profileName}" not found at ${profileDir}`);
16
+ }
17
+ const manifest = readProfileManifest(profileDir);
18
+ const fixedBundles = [];
19
+ for (const pkg of [...new Set([...bundlesOf(manifest), ...disabledBundlesOf(manifest)])]) {
20
+ if (!isResolvable(profileDir, pkg)) {
21
+ removeBundle(manifest, pkg);
22
+ fixedBundles.push(pkg);
23
+ }
24
+ }
25
+ if (fixedBundles.length > 0)
26
+ writeProfileManifest(profileDir, manifest);
27
+ const resolvable = new Set([...bundlesOf(manifest), ...disabledBundlesOf(manifest)]);
28
+ let fixedRows = 0;
29
+ for (const file of [path.join(profileDir, 'cordis.patch.yml'), path.join(home, 'cordis.patch.yml')]) {
30
+ if (!existsSync(file))
31
+ continue;
32
+ let text = readFileSync(file, 'utf8');
33
+ let changed = false;
34
+ for (;;) {
35
+ const { entries } = parsePatch(text);
36
+ const orphan = entries.find((entry) => entry.name !== undefined && !resolvable.has(entry.name));
37
+ if (orphan === undefined)
38
+ break;
39
+ text = removeEntry(text, orphan.id).text;
40
+ fixedRows++;
41
+ changed = true;
42
+ }
43
+ if (changed)
44
+ writeFileSync(file, text, 'utf8');
45
+ }
46
+ return {
47
+ schema: 'dsh-unplug/v1',
48
+ ok: true,
49
+ profile: profileName,
50
+ fixedBundles,
51
+ fixedRows,
52
+ audit: audit(home, profileName),
53
+ };
54
+ }
package/lib/lib/remove.js CHANGED
@@ -6,12 +6,23 @@ import { removeEntry, setEntryDisabled } from './patches.js';
6
6
  import { readFileSync, writeFileSync } from 'node:fs';
7
7
  import { audit } from './audit.js';
8
8
  import { parsePatch } from './patches.js';
9
+ import { resolveBundlePatchPath } from './resolve.js';
9
10
  function resolvePlugin(home, profileDir, profileName, plugin) {
10
11
  const manifest = readProfileManifest(profileDir);
11
12
  const allBundles = [...(manifest.dsh?.profile?.bundles ?? []), ...(manifest.dsh?.profile?.disabledBundles ?? [])];
12
13
  // Direct bundle name match
13
14
  if (allBundles.includes(plugin))
14
15
  return { bundleName: plugin, rowId: plugin };
16
+ // Scan every installed bundle's patch rows too (rows live in node_modules).
17
+ for (const pkg of allBundles) {
18
+ const patchPath = resolveBundlePatchPath(profileDir, pkg);
19
+ if (patchPath === undefined || !existsSync(patchPath))
20
+ continue;
21
+ for (const entry of parsePatch(readFileSync(patchPath, 'utf8')).entries) {
22
+ if (entry.id === plugin || entry.name === plugin)
23
+ return { bundleName: pkg, rowId: entry.id };
24
+ }
25
+ }
15
26
  // Check patch rows for id/name match
16
27
  for (const file of [path.join(profileDir, 'cordis.patch.yml'), path.join(home, 'cordis.patch.yml')]) {
17
28
  if (!existsSync(file))
@@ -0,0 +1,14 @@
1
+ import { type AuditResult } from './audit.js';
2
+ export interface ReconcileResult {
3
+ schema: 'dsh-unplug/v1';
4
+ ok: boolean;
5
+ profile: string;
6
+ fixedBundles: string[];
7
+ fixedRows: number;
8
+ audit: AuditResult;
9
+ }
10
+ /**
11
+ * Auto-fix dangling state: remove unresolvable bundles from the profile manifest
12
+ * and strip patch rows that reference packages which are not installed.
13
+ */
14
+ export declare function reconcile(home: string, profileName: string): ReconcileResult;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-unplug",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
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
5
  "type": "module",
6
6
  "main": "lib/index.js",