openclaw-channel-dmwork 0.5.21 → 0.6.0-dev.cea9ee46

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.
@@ -4,7 +4,7 @@
4
4
  * argument arrays to avoid shell-quoting issues.
5
5
  */
6
6
  import { execFileSync, execSync } from "node:child_process";
7
- import { readFileSync, writeFileSync, copyFileSync, existsSync } from "node:fs";
7
+ import { readFileSync, writeFileSync, copyFileSync, existsSync, rmSync, readdirSync, statSync, renameSync } from "node:fs";
8
8
  import { homedir } from "node:os";
9
9
  import { resolve } from "node:path";
10
10
  /**
@@ -52,6 +52,10 @@ function findGlobalOpenclaw() {
52
52
  return "openclaw";
53
53
  }
54
54
  const OPENCLAW = findGlobalOpenclaw();
55
+ /** Get the resolved openclaw binary path */
56
+ export function getOpenClawBin() {
57
+ return OPENCLAW;
58
+ }
55
59
  /** Expand ~ to home directory */
56
60
  function expandHome(p) {
57
61
  if (p.startsWith("~/"))
@@ -62,14 +66,45 @@ function expandHome(p) {
62
66
  // Config helpers
63
67
  // ---------------------------------------------------------------------------
64
68
  export function getConfigFilePath() {
65
- return execFileSync(OPENCLAW, ["config", "file"], { encoding: "utf-8" }).trim();
69
+ const out = execFileSync(OPENCLAW, ["config", "file"], { encoding: "utf-8" });
70
+ // openclaw may prepend warnings/box-drawing to stdout; extract the actual path
71
+ // The path is typically the last non-empty line containing openclaw.json
72
+ const lines = out.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
73
+ const pathLine = lines.find((l) => l.endsWith("openclaw.json")) ?? lines[lines.length - 1];
74
+ return pathLine ?? out.trim();
75
+ }
76
+ /**
77
+ * Strip OpenClaw stdout noise (banner, plugin log lines, timestamps).
78
+ * Old OpenClaw versions mix these into stdout alongside the actual value.
79
+ */
80
+ function stripStdoutNoise(raw) {
81
+ return raw
82
+ .split("\n")
83
+ .filter((line) => {
84
+ const t = line.trim();
85
+ if (!t)
86
+ return false;
87
+ // Banner: 🦞 OpenClaw ...
88
+ if (/^[\u{1F980}\u{1F600}-\u{1FAFF}]/u.test(t))
89
+ return false;
90
+ // Plugin log: [plugins] ..., [dmwork] ...
91
+ if (/^\[[\w-]+\]/.test(t))
92
+ return false;
93
+ // Timestamped log: 17:37:26 [plugins] ...
94
+ if (/^\d{1,2}:\d{2}(:\d{2})?\s*\[/.test(t))
95
+ return false;
96
+ return true;
97
+ })
98
+ .join("\n")
99
+ .trim();
66
100
  }
67
101
  export function configGet(path) {
68
102
  try {
69
- const val = execFileSync(OPENCLAW, ["config", "get", path], {
103
+ const raw = execFileSync(OPENCLAW, ["config", "get", path], {
70
104
  encoding: "utf-8",
71
105
  stdio: ["pipe", "pipe", "pipe"],
72
- }).trim();
106
+ });
107
+ const val = stripStdoutNoise(raw);
73
108
  return val === "" ? null : val;
74
109
  }
75
110
  catch {
@@ -89,7 +124,25 @@ export function configGetJson(path) {
89
124
  : Math.max(jsonStart, arrStart);
90
125
  if (start < 0)
91
126
  return null;
92
- return JSON.parse(out.slice(start));
127
+ // Find matching end bracket to avoid trailing log noise breaking JSON.parse
128
+ const openChar = out[start];
129
+ const closeChar = openChar === "{" ? "}" : "]";
130
+ let depth = 0;
131
+ let end = -1;
132
+ for (let i = start; i < out.length; i++) {
133
+ if (out[i] === openChar)
134
+ depth++;
135
+ else if (out[i] === closeChar) {
136
+ depth--;
137
+ if (depth === 0) {
138
+ end = i;
139
+ break;
140
+ }
141
+ }
142
+ }
143
+ if (end < 0)
144
+ return null;
145
+ return JSON.parse(out.slice(start, end + 1));
93
146
  }
94
147
  catch {
95
148
  return null;
@@ -119,18 +172,81 @@ export function configUnset(path) {
119
172
  // ---------------------------------------------------------------------------
120
173
  // Plugin helpers
121
174
  // ---------------------------------------------------------------------------
175
+ /**
176
+ * Check if an error indicates an unsupported CLI option.
177
+ * Checks stderr/stdout/message across different Node versions and shells.
178
+ */
179
+ function isUnsupportedOptionError(err) {
180
+ const sources = [
181
+ err?.stderr?.toString?.(),
182
+ err?.stdout?.toString?.(),
183
+ err?.message,
184
+ String(err),
185
+ ];
186
+ return sources.some((s) => s && (/unknown option|unrecognized option/i.test(s)));
187
+ }
188
+ function isPluginNotInstalledError(err) {
189
+ const sources = [
190
+ err?.stderr?.toString?.(),
191
+ err?.stdout?.toString?.(),
192
+ err?.message,
193
+ String(err),
194
+ ];
195
+ return sources.some((s) => s && (/not installed|no such plugin|plugin not found/i.test(s)));
196
+ }
122
197
  export function pluginsInstall(spec, quiet, force) {
123
- const args = ["plugins", "install", spec, "--dangerously-force-unsafe-install"];
124
- if (force)
125
- args.push("--force");
126
- execFileSync(OPENCLAW, args, {
127
- stdio: quiet ? ["pipe", "pipe", "pipe"] : "inherit",
128
- });
198
+ const baseArgs = ["plugins", "install", spec];
199
+ // 3-layer degradation for old openclaw versions:
200
+ // 1. --force --dangerously-force-unsafe-install (newest openclaw)
201
+ // 2. --force (mid-age openclaw)
202
+ // 3. bare install (oldest openclaw)
203
+ const attempts = force
204
+ ? [
205
+ [...baseArgs, "--force", "--dangerously-force-unsafe-install"],
206
+ [...baseArgs, "--force"],
207
+ baseArgs,
208
+ ]
209
+ : [
210
+ [...baseArgs, "--dangerously-force-unsafe-install"],
211
+ baseArgs,
212
+ ];
213
+ // Always pipe to capture stderr for degradation detection.
214
+ // stdio: "inherit" causes Node to omit stderr from the error object,
215
+ // making isUnsupportedOptionError() unable to detect "unknown option".
216
+ for (let i = 0; i < attempts.length; i++) {
217
+ try {
218
+ const result = execFileSync(OPENCLAW, attempts[i], {
219
+ stdio: ["pipe", "pipe", "pipe"],
220
+ encoding: "utf-8",
221
+ });
222
+ if (!quiet && result)
223
+ process.stdout.write(result);
224
+ return;
225
+ }
226
+ catch (err) {
227
+ if (isUnsupportedOptionError(err) && i < attempts.length - 1) {
228
+ continue; // try next degradation level
229
+ }
230
+ // Final attempt failed: replay captured output, then throw
231
+ if (!quiet) {
232
+ const stdout = err?.stdout?.toString?.();
233
+ const stderr = err?.stderr?.toString?.();
234
+ if (stdout)
235
+ process.stdout.write(stdout);
236
+ if (stderr)
237
+ process.stderr.write(stderr);
238
+ }
239
+ throw err;
240
+ }
241
+ }
129
242
  }
130
243
  export function pluginsUpdate(id, quiet) {
131
- execFileSync(OPENCLAW, ["plugins", "update", id], {
132
- stdio: quiet ? ["pipe", "pipe", "pipe"] : "inherit",
244
+ const result = execFileSync(OPENCLAW, ["plugins", "update", id], {
245
+ stdio: ["pipe", "pipe", "pipe"],
246
+ encoding: "utf-8",
133
247
  });
248
+ if (!quiet && result)
249
+ process.stdout.write(result);
134
250
  }
135
251
  export function pluginsUninstall(id, yes) {
136
252
  const args = ["plugins", "uninstall", id];
@@ -138,21 +254,99 @@ export function pluginsUninstall(id, yes) {
138
254
  args.push("--force");
139
255
  execFileSync(OPENCLAW, args, { stdio: "inherit" });
140
256
  }
141
- export function pluginsInspect(id) {
257
+ /**
258
+ * Inspect a plugin. Returns structured outcome distinguishing:
259
+ * - ok + data: inspect succeeded
260
+ * - unsupported: old OpenClaw without `plugins inspect`
261
+ * - not_found: plugin genuinely not found
262
+ * - error: other failure (config corruption, plugin load crash, etc.)
263
+ */
264
+ export function pluginsInspectDetailed(id) {
142
265
  try {
143
266
  const out = execFileSync(OPENCLAW, ["plugins", "inspect", id, "--json"], {
144
267
  encoding: "utf-8",
145
268
  stdio: ["pipe", "pipe", "pipe"],
146
269
  });
147
- // stdout may contain plugin log noise before JSON — find the JSON object
148
270
  const jsonStart = out.indexOf("{");
149
271
  if (jsonStart < 0)
150
- return null;
151
- return JSON.parse(out.slice(jsonStart));
272
+ return { ok: false, data: null, failReason: "error" };
273
+ const data = JSON.parse(out.slice(jsonStart));
274
+ return { ok: true, data, failReason: null };
152
275
  }
153
- catch {
154
- return null;
276
+ catch (err) {
277
+ const sources = [
278
+ err?.stderr?.toString?.(),
279
+ err?.stdout?.toString?.(),
280
+ err?.message,
281
+ String(err),
282
+ ];
283
+ const text = sources.filter(Boolean).join(" ");
284
+ if (/unknown command|unrecognized command/i.test(text)) {
285
+ return { ok: false, data: null, failReason: "unsupported" };
286
+ }
287
+ if (/not found|not installed|no such plugin/i.test(text)) {
288
+ return { ok: false, data: null, failReason: "not_found" };
289
+ }
290
+ return { ok: false, data: null, failReason: "error" };
291
+ }
292
+ }
293
+ /** Backward-compatible wrapper: returns data or null. */
294
+ export function pluginsInspect(id) {
295
+ const outcome = pluginsInspectDetailed(id);
296
+ return outcome.ok ? outcome.data : null;
297
+ }
298
+ /**
299
+ * Resolve plugin install state. Uses `plugins inspect` when available,
300
+ * falls back to config entries + directory + package.json for old OpenClaw
301
+ * versions that don't support `plugins inspect`.
302
+ *
303
+ * Fallback installed = all 3 artifacts present (entries + installs + dir),
304
+ * matching detectScenario()'s healthy definition. Partial presence is NOT
305
+ * considered installed — that's a broken state for doctor --fix to handle.
306
+ */
307
+ export function resolvePluginState(id) {
308
+ // Try inspect first
309
+ const outcome = pluginsInspectDetailed(id);
310
+ if (outcome.ok && outcome.data?.plugin) {
311
+ return {
312
+ installed: true,
313
+ enabled: outcome.data.plugin.enabled,
314
+ version: outcome.data.plugin.version,
315
+ installPath: outcome.data.install?.installPath ?? null,
316
+ source: "inspect",
317
+ inspectFailReason: null,
318
+ };
319
+ }
320
+ // Fallback: check config + filesystem
321
+ const cfg = readConfigFromFile();
322
+ const extDir = getConfigFilePathSafe().replace(/openclaw\.json$/, "extensions");
323
+ const pluginDir = resolve(extDir, id);
324
+ const hasDir = existsSync(pluginDir);
325
+ const entries = cfg?.plugins?.entries?.[id];
326
+ const installs = cfg?.plugins?.installs?.[id];
327
+ const hasEntry = Boolean(entries);
328
+ const hasInstall = Boolean(installs);
329
+ // Healthy install requires all 3 artifacts, same as detectScenario().
330
+ // Partial presence (e.g. dir exists but no entries/installs) is broken, not installed.
331
+ const installed = hasDir && hasEntry && hasInstall;
332
+ if (!installed) {
333
+ return {
334
+ installed: false, enabled: null, version: null, installPath: null,
335
+ source: "fallback", inspectFailReason: outcome.failReason,
336
+ };
337
+ }
338
+ // Resolve version: installs record > package.json on disk
339
+ let version = installs?.version ?? null;
340
+ if (!version && hasDir) {
341
+ try {
342
+ const pkg = JSON.parse(readFileSync(resolve(pluginDir, "package.json"), "utf-8"));
343
+ version = pkg.version ?? null;
344
+ }
345
+ catch { /* no package.json */ }
155
346
  }
347
+ const enabled = entries?.enabled ?? null;
348
+ const installPath = installs?.installPath ?? (hasDir ? `~/.openclaw/extensions/${id}` : null);
349
+ return { installed, enabled, version, installPath, source: "fallback", inspectFailReason: outcome.failReason };
156
350
  }
157
351
  // ---------------------------------------------------------------------------
158
352
  // Gateway helpers
@@ -166,11 +360,28 @@ export function gatewayStatus() {
166
360
  const jsonStart = out.indexOf("{");
167
361
  if (jsonStart < 0)
168
362
  return { running: false };
169
- const data = JSON.parse(out.slice(jsonStart));
170
- // Real structure: { service.runtime.status: "running", health.healthy: true }
363
+ // Find matching } to avoid trailing log noise
364
+ let depth = 0;
365
+ let end = -1;
366
+ for (let i = jsonStart; i < out.length; i++) {
367
+ if (out[i] === "{")
368
+ depth++;
369
+ else if (out[i] === "}") {
370
+ depth--;
371
+ if (depth === 0) {
372
+ end = i;
373
+ break;
374
+ }
375
+ }
376
+ }
377
+ if (end < 0)
378
+ return { running: false };
379
+ const data = JSON.parse(out.slice(jsonStart, end + 1));
171
380
  const runtimeRunning = data.service?.runtime?.status === "running";
172
381
  const healthy = data.health?.healthy === true;
173
- return { running: runtimeRunning || healthy };
382
+ // Fallback: port is busy with an openclaw-gateway process = gateway is running
383
+ const portBusy = data.port?.status === "busy";
384
+ return { running: runtimeRunning || healthy || portBusy };
174
385
  }
175
386
  catch {
176
387
  return { running: false };
@@ -218,7 +429,7 @@ export function getOpenClawVersion() {
218
429
  */
219
430
  export function saveChannelConfigFromFile() {
220
431
  try {
221
- const configPath = expandHome(getConfigFilePath());
432
+ const configPath = getConfigFilePathSafe();
222
433
  const raw = readFileSync(configPath, "utf-8");
223
434
  const cfg = JSON.parse(raw);
224
435
  return cfg?.channels?.dmwork ?? null;
@@ -233,7 +444,7 @@ export function saveChannelConfigFromFile() {
233
444
  * Creates a .bak backup before writing.
234
445
  */
235
446
  export function restoreChannelConfigToFile(dmworkConfig) {
236
- const configPath = expandHome(getConfigFilePath());
447
+ const configPath = getConfigFilePathSafe();
237
448
  // Backup
238
449
  copyFileSync(configPath, configPath + ".bak");
239
450
  // Read, merge, write
@@ -242,7 +453,7 @@ export function restoreChannelConfigToFile(dmworkConfig) {
242
453
  if (!cfg.channels)
243
454
  cfg.channels = {};
244
455
  cfg.channels.dmwork = dmworkConfig;
245
- writeFileSync(configPath, JSON.stringify(cfg, null, 2), "utf-8");
456
+ writeConfigAtomic(cfg);
246
457
  }
247
458
  /**
248
459
  * Remove channels.dmwork directly from the JSON file.
@@ -254,7 +465,7 @@ export function restoreChannelConfigToFile(dmworkConfig) {
254
465
  * Falls back to the standard default when CLI is unavailable
255
466
  * (e.g. during uninstall when config validation fails).
256
467
  */
257
- function getConfigFilePathSafe() {
468
+ export function getConfigFilePathSafe() {
258
469
  try {
259
470
  return expandHome(getConfigFilePath());
260
471
  }
@@ -266,11 +477,10 @@ export function removeChannelConfigFromFile() {
266
477
  try {
267
478
  const configPath = getConfigFilePathSafe();
268
479
  copyFileSync(configPath, configPath + ".bak");
269
- const raw = readFileSync(configPath, "utf-8");
270
- const cfg = JSON.parse(raw);
271
- if (cfg.channels?.dmwork) {
480
+ const cfg = readConfigFromFile();
481
+ if (cfg?.channels?.dmwork) {
272
482
  delete cfg.channels.dmwork;
273
- writeFileSync(configPath, JSON.stringify(cfg, null, 2), "utf-8");
483
+ writeConfigAtomic(cfg);
274
484
  }
275
485
  }
276
486
  catch {
@@ -311,7 +521,7 @@ export function removeOrphanedBindingsFromFile(channel, validAccountIds) {
311
521
  // Keep only if accountId is in valid list (or no accountId specified)
312
522
  return !b.match.accountId || validAccountIds.includes(b.match.accountId);
313
523
  });
314
- writeFileSync(configPath, JSON.stringify(cfg, null, 2), "utf-8");
524
+ writeConfigAtomic(cfg);
315
525
  }
316
526
  catch {
317
527
  // best effort
@@ -348,7 +558,6 @@ export function cleanupLegacyPlugin() {
348
558
  }
349
559
  // Remove legacy directory
350
560
  try {
351
- const { rmSync } = require("node:fs");
352
561
  rmSync(legacyDir, { recursive: true, force: true });
353
562
  actions.push(`Removed legacy directory: ${legacyDir}`);
354
563
  }
@@ -370,7 +579,7 @@ export function cleanupLegacyPlugin() {
370
579
  if (Array.isArray(cfg.plugins?.allow)) {
371
580
  cfg.plugins.allow = cfg.plugins.allow.filter((id) => id !== LEGACY_PLUGIN_ID);
372
581
  }
373
- writeFileSync(configPath, JSON.stringify(cfg, null, 2), "utf-8");
582
+ writeConfigAtomic(cfg);
374
583
  actions.push(`Cleaned legacy entries from openclaw.json`);
375
584
  }
376
585
  }
@@ -379,4 +588,318 @@ export function cleanupLegacyPlugin() {
379
588
  }
380
589
  return actions;
381
590
  }
591
+ /**
592
+ * Clean up stale openclaw-channel-dmwork directory that is not registered
593
+ * in plugins.installs (orphaned from a failed previous install).
594
+ *
595
+ * Only removes the directory if ALL of these are true:
596
+ * 1. The directory exists
597
+ * 2. pluginsInspect returns null (openclaw doesn't recognize it)
598
+ * 3. plugins.installs has no record for openclaw-channel-dmwork
599
+ */
600
+ export function cleanupStalePluginDir() {
601
+ const actions = [];
602
+ const extensionsDir = getConfigFilePathSafe().replace(/openclaw\.json$/, "extensions");
603
+ const pluginDir = resolve(extensionsDir, "openclaw-channel-dmwork");
604
+ if (!existsSync(pluginDir))
605
+ return actions;
606
+ // Check if openclaw recognizes it
607
+ const inspect = pluginsInspect("openclaw-channel-dmwork");
608
+ if (inspect?.plugin)
609
+ return actions; // recognized, don't touch
610
+ // Check if it's in installs registry
611
+ try {
612
+ const cfg = readConfigFromFile();
613
+ if (cfg?.plugins?.installs?.["openclaw-channel-dmwork"]) {
614
+ return actions; // has install record, might just be inspect anomaly
615
+ }
616
+ }
617
+ catch { /* proceed with cleanup */ }
618
+ // All three conditions met: exists + not recognized + not in registry → stale
619
+ try {
620
+ rmSync(pluginDir, { recursive: true, force: true });
621
+ actions.push(`Removed stale plugin directory: ${pluginDir}`);
622
+ }
623
+ catch {
624
+ actions.push(`Warning: could not remove stale directory: ${pluginDir}`);
625
+ }
626
+ return actions;
627
+ }
628
+ /**
629
+ * Clean up stale openclaw-install-stage directories that belong to DMWork.
630
+ * Only removes directories that:
631
+ * 1. Match .openclaw-install-stage-* pattern
632
+ * 2. Are older than 10 minutes (not a current installation)
633
+ * 3. Contain a package.json with name "openclaw-channel-dmwork"
634
+ */
635
+ export function cleanupStaleStageDirectories() {
636
+ const actions = [];
637
+ const extensionsDir = getConfigFilePathSafe().replace(/openclaw\.json$/, "extensions");
638
+ try {
639
+ const entries = readdirSync(extensionsDir);
640
+ const now = Date.now();
641
+ const TEN_MINUTES = 10 * 60 * 1000;
642
+ for (const entry of entries) {
643
+ if (!entry.startsWith(".openclaw-install-stage-"))
644
+ continue;
645
+ const stagePath = resolve(extensionsDir, entry);
646
+ try {
647
+ const stat = statSync(stagePath);
648
+ if (!stat.isDirectory())
649
+ continue;
650
+ if (now - stat.mtimeMs < TEN_MINUTES)
651
+ continue; // too recent, skip
652
+ // Check if it's DMWork's stage directory
653
+ const pkgPath = resolve(stagePath, "package", "package.json");
654
+ const altPkgPath = resolve(stagePath, "package.json");
655
+ let isDmwork = false;
656
+ for (const p of [pkgPath, altPkgPath]) {
657
+ try {
658
+ const pkg = JSON.parse(readFileSync(p, "utf-8"));
659
+ if (pkg.name === "openclaw-channel-dmwork") {
660
+ isDmwork = true;
661
+ break;
662
+ }
663
+ }
664
+ catch { /* try next */ }
665
+ }
666
+ if (!isDmwork)
667
+ continue; // not ours, don't touch
668
+ rmSync(stagePath, { recursive: true, force: true });
669
+ actions.push(`Removed stale stage directory: ${entry}`);
670
+ }
671
+ catch { /* skip this entry */ }
672
+ }
673
+ }
674
+ catch { /* best effort */ }
675
+ return actions;
676
+ }
677
+ // ---------------------------------------------------------------------------
678
+ // Atomic config write
679
+ // ---------------------------------------------------------------------------
680
+ /**
681
+ * Write openclaw.json atomically: write to .tmp then rename.
682
+ * Prevents gateway watcher from reading half-written/truncated JSON.
683
+ */
684
+ export function writeConfigAtomic(cfg) {
685
+ const configPath = getConfigFilePathSafe();
686
+ const tmpPath = configPath + ".tmp";
687
+ writeFileSync(tmpPath, JSON.stringify(cfg, null, 2), "utf-8");
688
+ renameSync(tmpPath, configPath);
689
+ }
690
+ export function detectScenario() {
691
+ const cfg = readConfigFromFile();
692
+ const extDir = getConfigFilePathSafe().replace(/openclaw\.json$/, "extensions");
693
+ const hasLegacyDir = existsSync(resolve(extDir, "dmwork"));
694
+ const hasLegacyEntries = Boolean(cfg?.plugins?.entries?.["dmwork"]);
695
+ const hasLegacyInstalls = Boolean(cfg?.plugins?.installs?.["dmwork"]);
696
+ const hasLegacy = hasLegacyDir || hasLegacyEntries || hasLegacyInstalls;
697
+ const hasNewDir = existsSync(resolve(extDir, "openclaw-channel-dmwork"));
698
+ const hasNewEntries = Boolean(cfg?.plugins?.entries?.["openclaw-channel-dmwork"]);
699
+ const hasNewInstalls = Boolean(cfg?.plugins?.installs?.["openclaw-channel-dmwork"]);
700
+ const inspectOk = Boolean(pluginsInspect("openclaw-channel-dmwork")?.plugin);
701
+ const isHealthy = inspectOk || (hasNewDir && hasNewEntries && hasNewInstalls);
702
+ const hasNewPartial = (hasNewDir || hasNewEntries || hasNewInstalls) && !isHealthy;
703
+ const hasDmworkChannel = Boolean(cfg?.channels?.dmwork);
704
+ if (hasLegacy)
705
+ return "legacy";
706
+ if (isHealthy)
707
+ return "update";
708
+ if (hasNewPartial)
709
+ return "broken";
710
+ if (hasDmworkChannel)
711
+ return "deadlock";
712
+ return "fresh";
713
+ }
714
+ export function isHealthyInstall() {
715
+ const cfg = readConfigFromFile();
716
+ const extDir = getConfigFilePathSafe().replace(/openclaw\.json$/, "extensions");
717
+ const hasNewDir = existsSync(resolve(extDir, "openclaw-channel-dmwork"));
718
+ const hasNewEntries = Boolean(cfg?.plugins?.entries?.["openclaw-channel-dmwork"]);
719
+ const hasNewInstalls = Boolean(cfg?.plugins?.installs?.["openclaw-channel-dmwork"]);
720
+ const inspectOk = Boolean(pluginsInspect("openclaw-channel-dmwork")?.plugin);
721
+ return inspectOk || (hasNewDir && hasNewEntries && hasNewInstalls);
722
+ }
723
+ export function ensurePluginsAllow() {
724
+ try {
725
+ const cfg = readConfigFromFile();
726
+ if (!cfg?.plugins?.allow || !Array.isArray(cfg.plugins.allow))
727
+ return;
728
+ if (cfg.plugins.allow.includes("openclaw-channel-dmwork"))
729
+ return;
730
+ cfg.plugins.allow.push("openclaw-channel-dmwork");
731
+ writeConfigAtomic(cfg);
732
+ }
733
+ catch { /* best effort */ }
734
+ }
735
+ // ---------------------------------------------------------------------------
736
+ // pluginsUpdateCompat
737
+ // ---------------------------------------------------------------------------
738
+ export function pluginsUpdateCompat(id, tag, quiet) {
739
+ try {
740
+ const result = execFileSync(OPENCLAW, ["plugins", "update", id], {
741
+ stdio: ["pipe", "pipe", "pipe"],
742
+ encoding: "utf-8",
743
+ });
744
+ if (!quiet && result)
745
+ process.stdout.write(result);
746
+ }
747
+ catch (err) {
748
+ // Only fallback to install when update is unsupported or plugin not installed.
749
+ // Other errors (network, permissions, etc.) should propagate.
750
+ if (isUnsupportedOptionError(err) || isPluginNotInstalledError(err)) {
751
+ pluginsInstall(`${id}@${tag}`, quiet, true);
752
+ return;
753
+ }
754
+ if (!quiet) {
755
+ const stdout = err?.stdout?.toString?.();
756
+ const stderr = err?.stderr?.toString?.();
757
+ if (stdout)
758
+ process.stdout.write(stdout);
759
+ if (stderr)
760
+ process.stderr.write(stderr);
761
+ }
762
+ throw err;
763
+ }
764
+ }
765
+ // ---------------------------------------------------------------------------
766
+ // Legacy migration helpers
767
+ // ---------------------------------------------------------------------------
768
+ export function renameLegacyDir() {
769
+ const extDir = getConfigFilePathSafe().replace(/openclaw\.json$/, "extensions");
770
+ const legacyDir = resolve(extDir, "dmwork");
771
+ const backupDir = resolve(extDir, ".dmwork-backup");
772
+ if (!existsSync(legacyDir))
773
+ return false;
774
+ try {
775
+ if (existsSync(backupDir))
776
+ rmSync(backupDir, { recursive: true, force: true });
777
+ renameSync(legacyDir, backupDir);
778
+ return true;
779
+ }
780
+ catch {
781
+ return false;
782
+ }
783
+ }
784
+ export function restoreLegacyDir() {
785
+ const extDir = getConfigFilePathSafe().replace(/openclaw\.json$/, "extensions");
786
+ const legacyDir = resolve(extDir, "dmwork");
787
+ const backupDir = resolve(extDir, ".dmwork-backup");
788
+ if (!existsSync(backupDir))
789
+ return;
790
+ try {
791
+ if (existsSync(legacyDir))
792
+ rmSync(legacyDir, { recursive: true, force: true });
793
+ renameSync(backupDir, legacyDir);
794
+ }
795
+ catch { /* best effort */ }
796
+ }
797
+ export function deleteLegacyBackup() {
798
+ const extDir = getConfigFilePathSafe().replace(/openclaw\.json$/, "extensions");
799
+ const backupDir = resolve(extDir, ".dmwork-backup");
800
+ if (existsSync(backupDir)) {
801
+ try {
802
+ rmSync(backupDir, { recursive: true, force: true });
803
+ }
804
+ catch { /* best effort */ }
805
+ }
806
+ }
807
+ export function removeLegacyFromConfig() {
808
+ try {
809
+ const cfg = readConfigFromFile();
810
+ if (!cfg)
811
+ return;
812
+ if (cfg.plugins?.entries?.["dmwork"])
813
+ delete cfg.plugins.entries["dmwork"];
814
+ if (cfg.plugins?.installs?.["dmwork"])
815
+ delete cfg.plugins.installs["dmwork"];
816
+ if (Array.isArray(cfg.plugins?.allow)) {
817
+ cfg.plugins.allow = cfg.plugins.allow.filter((id) => id !== "dmwork");
818
+ }
819
+ if (cfg.channels?.dmwork)
820
+ delete cfg.channels.dmwork;
821
+ writeConfigAtomic(cfg);
822
+ }
823
+ catch { /* best effort */ }
824
+ }
825
+ export function saveChannelConfigToDisk() {
826
+ try {
827
+ const backupPath = getConfigFilePathSafe().replace(/openclaw\.json$/, "channels-dmwork-backup.json");
828
+ const cfg = readConfigFromFile();
829
+ const dmwork = cfg?.channels?.dmwork;
830
+ if (dmwork) {
831
+ writeFileSync(backupPath, JSON.stringify(dmwork, null, 2), "utf-8");
832
+ }
833
+ else {
834
+ // No channels.dmwork — remove stale backup to prevent wrong restore
835
+ if (existsSync(backupPath))
836
+ rmSync(backupPath, { force: true });
837
+ }
838
+ }
839
+ catch { /* best effort */ }
840
+ }
841
+ export function restoreChannelConfigFromDisk() {
842
+ try {
843
+ const backupPath = getConfigFilePathSafe().replace(/openclaw\.json$/, "channels-dmwork-backup.json");
844
+ if (!existsSync(backupPath))
845
+ return;
846
+ let dmwork = JSON.parse(readFileSync(backupPath, "utf-8"));
847
+ // Migrate flat config → accounts.default
848
+ if (dmwork.botToken && !dmwork.accounts) {
849
+ dmwork = {
850
+ ...dmwork,
851
+ accounts: { default: { botToken: dmwork.botToken, apiUrl: dmwork.apiUrl } },
852
+ };
853
+ delete dmwork.botToken;
854
+ }
855
+ const cfg = readConfigFromFile();
856
+ if (!cfg)
857
+ return;
858
+ if (!cfg.channels)
859
+ cfg.channels = {};
860
+ cfg.channels.dmwork = dmwork;
861
+ writeConfigAtomic(cfg);
862
+ rmSync(backupPath, { force: true });
863
+ }
864
+ catch { /* best effort */ }
865
+ }
866
+ export function cleanupBrokenInstall() {
867
+ const actions = [];
868
+ const cfg = readConfigFromFile();
869
+ const extDir = getConfigFilePathSafe().replace(/openclaw\.json$/, "extensions");
870
+ const pluginDir = resolve(extDir, "openclaw-channel-dmwork");
871
+ const hasDir = existsSync(pluginDir);
872
+ const hasEntries = Boolean(cfg?.plugins?.entries?.["openclaw-channel-dmwork"]);
873
+ const hasInstalls = Boolean(cfg?.plugins?.installs?.["openclaw-channel-dmwork"]);
874
+ // Use same healthy definition as detectScenario(): inspect OK OR all 3 artifacts present
875
+ const inspectOk = Boolean(pluginsInspect("openclaw-channel-dmwork")?.plugin);
876
+ const isHealthy = inspectOk || (hasDir && hasEntries && hasInstalls);
877
+ if (isHealthy)
878
+ return actions; // Actually healthy, nothing to clean
879
+ // Remove directory if it exists (orphan or partial)
880
+ if (hasDir) {
881
+ try {
882
+ rmSync(pluginDir, { recursive: true, force: true });
883
+ actions.push("Removed broken/orphan plugin directory");
884
+ }
885
+ catch { /* best effort */ }
886
+ }
887
+ // Remove stale config entries
888
+ if (cfg && (hasEntries || hasInstalls)) {
889
+ let changed = false;
890
+ if (hasEntries) {
891
+ delete cfg.plugins.entries["openclaw-channel-dmwork"];
892
+ changed = true;
893
+ }
894
+ if (hasInstalls) {
895
+ delete cfg.plugins.installs["openclaw-channel-dmwork"];
896
+ changed = true;
897
+ }
898
+ if (changed) {
899
+ writeConfigAtomic(cfg);
900
+ actions.push("Cleaned stale config entries");
901
+ }
902
+ }
903
+ return actions;
904
+ }
382
905
  //# sourceMappingURL=openclaw-cli.js.map