@okclaw-build/cli 1.0.0-beta.50 → 1.0.0-beta.52

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.
@@ -1,5 +1,5 @@
1
1
  import { existsSync, cpSync, mkdirSync, rmSync } from 'node:fs';
2
- import { shell, shellRetry, shellCapture } from '../utils/shell.js';
2
+ import { runCommand, runCommandRetry, shell, shellCapture } from '../utils/shell.js';
3
3
  import { ensureNodeVersion, checkDiskSpace } from '../utils/deps.js';
4
4
  import { info, warn } from '../utils/logger.js';
5
5
  import { stopDaemonForExclusiveAccess, stopGateway, cleanupGatewayServiceFiles, } from '../utils/openclaw-daemon.js';
@@ -24,7 +24,16 @@ export class OpenclawInstaller {
24
24
  info(`Installing openclaw@${version}...`);
25
25
  // --ignore-scripts: skip postinstall to avoid @matrix-org/matrix-sdk-crypto-nodejs
26
26
  // which downloads ~40MB binary from GitHub (blocked/slow in China)
27
- await shellRetry(`npm install -g openclaw@${version} --registry ${NPM_REGISTRY} --no-audit --no-fund --ignore-scripts`);
27
+ await runCommandRetry('npm', [
28
+ 'install',
29
+ '-g',
30
+ `openclaw@${version}`,
31
+ '--registry',
32
+ NPM_REGISTRY,
33
+ '--no-audit',
34
+ '--no-fund',
35
+ '--ignore-scripts',
36
+ ]);
28
37
  // Remove the problematic postinstall script, then rebuild everything else
29
38
  info('Running native module builds...');
30
39
  await shell(`rm -f $(npm root -g)/openclaw/node_modules/@matrix-org/matrix-sdk-crypto-nodejs/download-lib.js 2>/dev/null || true`);
@@ -53,7 +62,7 @@ export class OpenclawInstaller {
53
62
  // after the full install pipeline completes.
54
63
  await stopDaemonForExclusiveAccess('openclaw deployment defaults');
55
64
  info('Applying openclaw deployment defaults...');
56
- await shell('openclaw config unset plugins.slots.contextEngine').catch(() => {
65
+ await runCommand('openclaw', ['config', 'unset', 'plugins.slots.contextEngine']).catch(() => {
57
66
  warn('Failed to unset legacy contextEngine slot, continuing...');
58
67
  });
59
68
  // openclaw 2026.4.26 起,bundled plugin 注册路径变了:dist/extensions/memory-core
@@ -61,15 +70,15 @@ export class OpenclawInstaller {
61
70
  // "plugin not found: memory-core" 失败。当前没找到 v2026.4.26 下注册它的官方手段
62
71
  // (doctor/onboard/postinstall 都不补),先 catch 住不让整个安装挂掉,让 onboard
63
72
  // 留下的默认 memory slot 跑起来;后续找到正解再去掉这层兜底。
64
- await shell('openclaw config set plugins.slots.memory memory-core').catch(() => {
73
+ await runCommand('openclaw', ['config', 'set', 'plugins.slots.memory', '--', 'memory-core']).catch(() => {
65
74
  warn('Failed to set memory slot to memory-core (plugin not registered), continuing...');
66
75
  });
67
76
  // OKClaw 部署的 OpenClaw 实例不需要主动 agent heartbeat;设为 0m
68
77
  // 是持久关闭,必须在 gateway 启动前写入配置。
69
- await shell('openclaw config set agents.defaults.heartbeat.every "0m"').catch(() => {
78
+ await runCommand('openclaw', ['config', 'set', 'agents.defaults.heartbeat.every', '--', '0m']).catch(() => {
70
79
  warn('Failed to disable OpenClaw heartbeat, continuing...');
71
80
  });
72
- await shell('openclaw config set tools.profile full');
81
+ await runCommand('openclaw', ['config', 'set', 'tools.profile', '--', 'full']);
73
82
  info(`openclaw@${version} installed.`);
74
83
  }
75
84
  /**
@@ -113,14 +122,18 @@ export class OpenclawInstaller {
113
122
  }
114
123
  }
115
124
  // Full replace via `openclaw config set ... --replace`. --strict-json
116
- // forces JSON parsing (no raw-string fallback). Single quotes are
117
- // escaped using the standard '\'' pattern so an apiKey containing a
118
- // single quote doesn't break out of the shell argument.
119
- const providersJson = JSON.stringify(config).replace(/'/g, `'\\''`);
120
- await shell(`openclaw config set models.providers '${providersJson}' --strict-json --replace`);
125
+ // forces JSON parsing (no raw-string fallback).
126
+ await runCommand('openclaw', [
127
+ 'config',
128
+ 'set',
129
+ 'models.providers',
130
+ JSON.stringify(config),
131
+ '--strict-json',
132
+ '--replace',
133
+ ]);
121
134
  info(`Replaced models.providers with: ${Object.keys(config).join(', ')}`);
122
135
  if (defaultModel) {
123
- await shell(`openclaw config set agents.defaults.model.primary '${defaultModel}'`);
136
+ await runCommand('openclaw', ['config', 'set', 'agents.defaults.model.primary', '--', defaultModel]);
124
137
  info(`Default model: ${defaultModel}`);
125
138
  }
126
139
  info('openclaw configured.');
@@ -1 +1 @@
1
- {"version":3,"file":"openclaw.js","sourceRoot":"","sources":["../../src/installers/openclaw.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"openclaw.js","sourceRoot":"","sources":["../../src/installers/openclaw.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAEhE,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACrF,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACrE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EACL,4BAA4B,EAC5B,WAAW,EACX,0BAA0B,GAC3B,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACvF,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAElE,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAElE,MAAM,eAAe,GAAG,GAAG,aAAa,gBAAgB,CAAC;AAEzD,MAAM,OAAO,iBAAiB;IAC5B,KAAK,CAAC,SAAS;QACb,MAAM,cAAc,CAAC,GAAG,CAAC,CAAC;QAC1B,MAAM,iBAAiB,EAAE,CAAC;IAC5B,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,OAAe;QAC3B,IAAI,UAAU,CAAC,eAAe,CAAC,EAAE,CAAC;YAChC,MAAM,EAAE,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACtE,MAAM,SAAS,GAAG,GAAG,iBAAiB,oBAAoB,EAAE,EAAE,CAAC;YAC/D,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAC1C,MAAM,CAAC,eAAe,EAAE,GAAG,SAAS,gBAAgB,CAAC,CAAC;YACtD,IAAI,CAAC,gCAAgC,SAAS,EAAE,CAAC,CAAC;QACpD,CAAC;QAED,IAAI,CAAC,uBAAuB,OAAO,KAAK,CAAC,CAAC;QAC1C,mFAAmF;QACnF,mEAAmE;QACnE,MAAM,eAAe,CAAC,KAAK,EAAE;YAC3B,SAAS;YACT,IAAI;YACJ,YAAY,OAAO,EAAE;YACrB,YAAY;YACZ,YAAY;YACZ,YAAY;YACZ,WAAW;YACX,kBAAkB;SACnB,CAAC,CAAC;QACH,0EAA0E;QAC1E,IAAI,CAAC,iCAAiC,CAAC,CAAC;QACxC,MAAM,KAAK,CACT,qHAAqH,CACtH,CAAC;QACF,MAAM,KAAK,CACT,6EAA6E,CAC9E,CAAC;QAEF,yEAAyE;QACzE,6EAA6E;QAC7E,IAAI,CAAC,mEAAmE,CAAC,CAAC;QAC1E,MAAM,WAAW,EAAE,CAAC;QAEpB,2DAA2D;QAC3D,2EAA2E;QAC3E,4EAA4E;QAC5E,kFAAkF;QAClF,IAAI,CAAC,4CAA4C,CAAC,CAAC;QACnD,wDAAwD;QACxD,sCAAsC;QACtC,iDAAiD;QACjD,iCAAiC;QACjC,MAAM,KAAK,CAAC,gEAAgE,CAAC,CAAC;QAE9E,mEAAmE;QACnE,wEAAwE;QACxE,uEAAuE;QACvE,0EAA0E;QAC1E,sEAAsE;QACtE,yEAAyE;QACzE,wEAAwE;QACxE,6CAA6C;QAC7C,MAAM,4BAA4B,CAAC,8BAA8B,CAAC,CAAC;QAEnE,IAAI,CAAC,0CAA0C,CAAC,CAAC;QACjD,MAAM,UAAU,CAAC,UAAU,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,6BAA6B,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE;YAC1F,IAAI,CAAC,0DAA0D,CAAC,CAAC;QACnE,CAAC,CAAC,CAAC;QACH,yEAAyE;QACzE,0CAA0C;QAC1C,gEAAgE;QAChE,+DAA+D;QAC/D,uCAAuC;QACvC,MAAM,UAAU,CAAC,UAAU,EAAE,CAAC,QAAQ,EAAE,KAAK,EAAE,sBAAsB,EAAE,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE;YACtG,IAAI,CAAC,iFAAiF,CAAC,CAAC;QAC1F,CAAC,CAAC,CAAC;QACH,oDAAoD;QACpD,6BAA6B;QAC7B,MAAM,UAAU,CAAC,UAAU,EAAE,CAAC,QAAQ,EAAE,KAAK,EAAE,iCAAiC,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE;YACxG,IAAI,CAAC,qDAAqD,CAAC,CAAC;QAC9D,CAAC,CAAC,CAAC;QACH,MAAM,UAAU,CAAC,UAAU,EAAE,CAAC,QAAQ,EAAE,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;QAE/E,IAAI,CAAC,YAAY,OAAO,aAAa,CAAC,CAAC;IACzC,CAAC;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,KAAK,CAAC,SAAS,CAAC,MAA+B;QAC7C,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAE7C,8DAA8D;QAC9D,sEAAsE;QACtE,gDAAgD;QAChD,iEAAiE;QACjE,kEAAkE;QAClE,qEAAqE;QACrE,sBAAsB;QACtB,MAAM,4BAA4B,CAAC,0BAA0B,CAAC,CAAC;QAE/D,IAAI,CAAC,4CAA4C,CAAC,CAAC;QAEnD,kDAAkD;QAClD,IAAI,YAAY,GAAG,EAAE,CAAC;QACtB,KAAK,MAAM,CAAC,YAAY,EAAE,cAAc,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YACpE,MAAM,EAAE,GAAG,cAAyC,CAAC;YACrD,MAAM,SAAS,GAAG,EAAE,CAAC,MAAoD,CAAC;YAC1E,IAAI,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC;gBACvB,YAAY,GAAG,GAAG,YAAY,IAAI,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;gBACpD,MAAM;YACR,CAAC;QACH,CAAC;QAED,sEAAsE;QACtE,gDAAgD;QAChD,MAAM,UAAU,CAAC,UAAU,EAAE;YAC3B,QAAQ;YACR,KAAK;YACL,kBAAkB;YAClB,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC;YACtB,eAAe;YACf,WAAW;SACZ,CAAC,CAAC;QACH,IAAI,CAAC,mCAAmC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAE1E,IAAI,YAAY,EAAE,CAAC;YACjB,MAAM,UAAU,CAAC,UAAU,EAAE,CAAC,QAAQ,EAAE,KAAK,EAAE,+BAA+B,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC;YACrG,IAAI,CAAC,kBAAkB,YAAY,EAAE,CAAC,CAAC;QACzC,CAAC;QAED,IAAI,CAAC,sBAAsB,CAAC,CAAC;IAC/B,CAAC;IAED,KAAK,CAAC,SAAS;QACb,IAAI,CAAC,sBAAsB,CAAC,CAAC;QAC7B,2DAA2D;QAC3D,MAAM,KAAK,CAAC,iCAAiC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAE/D,MAAM,iBAAiB,GAAG,sBAAsB,EAAE,CAAC;QACnD,IAAI,iBAAiB,EAAE,CAAC;YACtB,IAAI,CAAC,mCAAmC,iBAAiB,EAAE,CAAC,CAAC;QAC/D,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,0DAA0D,CAAC,CAAC;QACnE,CAAC;QAED,sEAAsE;QACtE,yDAAyD;QACzD,oDAAoD;QACpD,MAAM,KAAK,CAAC,gDAAgD,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAC9E,mEAAmE;QACnE,8BAA8B;QAC9B,MAAM,0BAA0B,EAAE,CAAC;QACnC,8CAA8C;QAC9C,oDAAoD;QACpD,wDAAwD;QACxD,MAAM,WAAW,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAEpC,8DAA8D;QAC9D,mCAAmC;QACnC,qEAAqE;QACrE,qEAAqE;QACrE,uBAAuB;QACvB,IAAI,CAAC,mEAAmE,CAAC,CAAC;QAC1E,MAAM,KAAK,CAAC,oEAAoE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAClG,MAAM,KAAK,CAAC,yCAAyC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QACvE,MAAM,KAAK,CAAC,iDAAiD,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAE/E,sDAAsD;QACtD,mCAAmC;QACnC,MAAM,KAAK,CAAC,mDAAmD,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAEjF,4DAA4D;QAC5D,oCAAoC;QACpC,+DAA+D;QAC/D,0CAA0C;QAC1C,4CAA4C;QAC5C,MAAM,QAAQ,GAAG,uFAAuF,CAAC;QACzG,IAAI,CAAC,uDAAuD,CAAC,CAAC;QAC9D,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAC3B,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC,SAAS,QAAQ,qCAAqC,CAAC,CAAC;YACxF,MAAM,QAAQ,GAAG,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;YAC5C,IAAI,CAAC,QAAQ;gBAAE,MAAM;YACrB,IAAI,CAAC,qBAAqB,QAAQ,YAAY,CAAC,CAAC;YAChD,uCAAuC;YACvC,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;YAC7C,MAAM,KAAK,CAAC,UAAU,IAAI,uBAAuB,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QACrE,CAAC;QAED,IAAI,CAAC,qCAAqC,CAAC,CAAC;QAC5C,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,SAAS,QAAQ,qCAAqC,CAAC,CAAC;QACzF,MAAM,YAAY,GAAG,CAAC,KAAK,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACjD,IAAI,YAAY,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CAAC,yCAAyC,YAAY,EAAE,CAAC,CAAC;QAC3E,CAAC;QAED,IAAI,CAAC,YAAY,aAAa,KAAK,CAAC,CAAC;QACrC,MAAM,CAAC,aAAa,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACxD,IAAI,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CAAC,GAAG,aAAa,6BAA6B,CAAC,CAAC;QACjE,CAAC;QAED,IAAI,CAAC,uBAAuB,CAAC,CAAC;IAChC,CAAC;CACF"}
@@ -1,10 +1,11 @@
1
1
  import type { Installer } from './base.js';
2
+ import type { PackageSource } from '../utils/install-safety.js';
2
3
  export declare function findEffectiveSkillRoot(extractDir: string): string;
3
4
  export declare class SkillInstaller implements Installer {
4
- private skillName;
5
+ private readonly skillName;
5
6
  constructor(skillName: string);
6
7
  checkDeps(): Promise<void>;
7
- install(_version: string, packageUrl?: string): Promise<void>;
8
+ install(_version: string, packageSource?: PackageSource): Promise<void>;
8
9
  configure(_config: Record<string, unknown>): Promise<void>;
9
10
  uninstall(): Promise<void>;
10
11
  }
@@ -1,8 +1,10 @@
1
- import { existsSync, mkdirSync, readdirSync, rmSync, statSync } from 'node:fs';
2
- import { shell, shellCapture } from '../utils/shell.js';
1
+ import { copyFileSync, cpSync, existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { runCommand, runCommandCapture } from '../utils/shell.js';
3
4
  import { checkDiskSpace, ensureCommand } from '../utils/deps.js';
4
5
  import { info, warn } from '../utils/logger.js';
5
6
  import { OKCLAW_TMP_DIR, OPENCLAW_WORKSPACE } from '../utils/constants.js';
7
+ import { assertSafeInstallName, skillInstallDir, tmpInstallPath } from '../utils/install-safety.js';
6
8
  const SKILLS_DIR = `${OPENCLAW_WORKSPACE}/skills`;
7
9
  // 与 okclaw-skill-api SkillUploadServiceImpl.hasSkillMdAtAcceptedRoot 对齐:上传端接受
8
10
  // (a) SKILL.md 在 zip 根;(b) SKILL.md 在唯一顶层目录下。CLI 解压必须同样支持,
@@ -10,8 +12,8 @@ const SKILLS_DIR = `${OPENCLAW_WORKSPACE}/skills`;
10
12
  export function findEffectiveSkillRoot(extractDir) {
11
13
  const entries = readdirSync(extractDir);
12
14
  if (entries.length === 1) {
13
- const candidate = `${extractDir}/${entries[0]}`;
14
- if (statSync(candidate).isDirectory() && existsSync(`${candidate}/SKILL.md`)) {
15
+ const candidate = join(extractDir, entries[0]);
16
+ if (statSync(candidate).isDirectory() && existsSync(join(candidate, 'SKILL.md'))) {
15
17
  return candidate;
16
18
  }
17
19
  }
@@ -20,59 +22,59 @@ export function findEffectiveSkillRoot(extractDir) {
20
22
  export class SkillInstaller {
21
23
  skillName;
22
24
  constructor(skillName) {
23
- this.skillName = skillName;
25
+ this.skillName = assertSafeInstallName('skill', skillName);
24
26
  }
25
27
  async checkDeps() {
26
28
  await checkDiskSpace(100);
27
29
  await ensureCommand('unzip');
28
30
  }
29
- async install(_version, packageUrl) {
30
- if (!packageUrl) {
31
+ async install(_version, packageSource) {
32
+ if (!packageSource) {
31
33
  throw new Error('--package-url is required for skill installation');
32
34
  }
33
35
  mkdirSync(OKCLAW_TMP_DIR, { recursive: true });
34
36
  mkdirSync(SKILLS_DIR, { recursive: true });
35
- const archive = `${OKCLAW_TMP_DIR}/${this.skillName}-skill.zip`;
37
+ const archive = tmpInstallPath(`${this.skillName}-skill.zip`);
36
38
  // Download
37
- if (packageUrl.startsWith('http')) {
38
- info(`Downloading skill ${this.skillName} from ${packageUrl}...`);
39
- await shell(`curl -fsSL --connect-timeout 30 --retry 3 -o "${archive}" "${packageUrl}"`);
39
+ if (packageSource.kind === 'remote') {
40
+ info(`Downloading skill ${this.skillName} from ${packageSource.url}...`);
41
+ await runCommand('curl', ['-fsSL', '--connect-timeout', '30', '--retry', '3', '-o', archive, packageSource.url]);
40
42
  }
41
- else if (existsSync(packageUrl)) {
42
- await shell(`cp "${packageUrl}" "${archive}"`);
43
+ else if (existsSync(packageSource.path)) {
44
+ copyFileSync(packageSource.path, archive);
43
45
  }
44
46
  else {
45
- throw new Error(`Package not found: ${packageUrl}`);
47
+ throw new Error(`Package not found: ${packageSource.path}`);
46
48
  }
47
49
  // Remove existing skill directory if present (overwrite)
48
- const skillDir = `${SKILLS_DIR}/${this.skillName}`;
50
+ const skillDir = skillInstallDir(this.skillName);
49
51
  if (existsSync(skillDir)) {
50
52
  info(`Overwriting existing skill at ${skillDir}...`);
51
53
  rmSync(skillDir, { recursive: true, force: true });
52
54
  }
53
55
  // Extract to staging dir, then move effective root into skillDir
54
56
  // (handles ClaudeCode-style 和 ClawHub-style 两种布局,与后端上传接受范围一致)
55
- const stageDir = `${OKCLAW_TMP_DIR}/${this.skillName}-extract`;
57
+ const stageDir = tmpInstallPath(`${this.skillName}-extract`);
56
58
  rmSync(stageDir, { recursive: true, force: true });
57
59
  mkdirSync(stageDir, { recursive: true });
58
60
  info(`Extracting skill to ${skillDir}...`);
59
- const unzipResult = await shellCapture(`unzip -o "${archive}" -d "${stageDir}"`);
61
+ const unzipResult = await runCommandCapture('unzip', ['-o', archive, '-d', stageDir]);
60
62
  if (unzipResult.code > 1) {
61
63
  throw new Error(`unzip failed (exit ${unzipResult.code}): ${unzipResult.stderr}`);
62
64
  }
63
65
  const sourceDir = findEffectiveSkillRoot(stageDir);
64
66
  mkdirSync(skillDir, { recursive: true });
65
- await shell(`find "${sourceDir}" -mindepth 1 -maxdepth 1 -exec mv {} "${skillDir}/" \\;`);
67
+ moveDirectoryContents(sourceDir, skillDir);
66
68
  rmSync(stageDir, { recursive: true, force: true });
67
69
  // Cleanup
68
- await shell(`rm -f "${archive}"`).catch(() => { });
70
+ rmSync(archive, { force: true });
69
71
  info(`Skill ${this.skillName} installed at ${skillDir}`);
70
72
  }
71
73
  async configure(_config) {
72
74
  // Skills don't need extra configuration
73
75
  }
74
76
  async uninstall() {
75
- const skillDir = `${SKILLS_DIR}/${this.skillName}`;
77
+ const skillDir = skillInstallDir(this.skillName);
76
78
  if (!existsSync(skillDir)) {
77
79
  warn(`Skill ${this.skillName} not found at ${skillDir}, skipping`);
78
80
  return;
@@ -82,4 +84,21 @@ export class SkillInstaller {
82
84
  info(`Skill ${this.skillName} uninstalled`);
83
85
  }
84
86
  }
87
+ function moveDirectoryContents(sourceDir, targetDir) {
88
+ for (const entry of readdirSync(sourceDir)) {
89
+ const src = join(sourceDir, entry);
90
+ const dest = join(targetDir, entry);
91
+ rmSync(dest, { recursive: true, force: true });
92
+ try {
93
+ renameSync(src, dest);
94
+ }
95
+ catch (e) {
96
+ if (e.code !== 'EXDEV') {
97
+ throw e;
98
+ }
99
+ cpSync(src, dest, { recursive: true, force: true });
100
+ rmSync(src, { recursive: true, force: true });
101
+ }
102
+ }
103
+ }
85
104
  //# sourceMappingURL=skill.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"skill.js","sourceRoot":"","sources":["../../src/installers/skill.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"skill.js","sourceRoot":"","sources":["../../src/installers/skill.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACjH,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3E,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAGpG,MAAM,UAAU,GAAG,GAAG,kBAAkB,SAAS,CAAC;AAElD,8EAA8E;AAC9E,2DAA2D;AAC3D,gFAAgF;AAChF,MAAM,UAAU,sBAAsB,CAAC,UAAkB;IACvD,MAAM,OAAO,GAAG,WAAW,CAAC,UAAU,CAAC,CAAC;IACxC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;QAC/C,IAAI,QAAQ,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE,IAAI,UAAU,CAAC,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC,EAAE,CAAC;YACjF,OAAO,SAAS,CAAC;QACnB,CAAC;IACH,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,MAAM,OAAO,cAAc;IACR,SAAS,CAAS;IAEnC,YAAY,SAAiB;QAC3B,IAAI,CAAC,SAAS,GAAG,qBAAqB,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IAC7D,CAAC;IAED,KAAK,CAAC,SAAS;QACb,MAAM,cAAc,CAAC,GAAG,CAAC,CAAC;QAC1B,MAAM,aAAa,CAAC,OAAO,CAAC,CAAC;IAC/B,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,QAAgB,EAAE,aAA6B;QAC3D,IAAI,CAAC,aAAa,EAAE,CAAC;YACnB,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;QACtE,CAAC;QACD,SAAS,CAAC,cAAc,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC/C,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAE3C,MAAM,OAAO,GAAG,cAAc,CAAC,GAAG,IAAI,CAAC,SAAS,YAAY,CAAC,CAAC;QAE9D,WAAW;QACX,IAAI,aAAa,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACpC,IAAI,CAAC,qBAAqB,IAAI,CAAC,SAAS,SAAS,aAAa,CAAC,GAAG,KAAK,CAAC,CAAC;YACzE,MAAM,UAAU,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,mBAAmB,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC;QACnH,CAAC;aAAM,IAAI,UAAU,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC;YAC1C,YAAY,CAAC,aAAa,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC5C,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,KAAK,CAAC,sBAAsB,aAAa,CAAC,IAAI,EAAE,CAAC,CAAC;QAC9D,CAAC;QAED,yDAAyD;QACzD,MAAM,QAAQ,GAAG,eAAe,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACjD,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YACzB,IAAI,CAAC,iCAAiC,QAAQ,KAAK,CAAC,CAAC;YACrD,MAAM,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACrD,CAAC;QAED,iEAAiE;QACjE,8DAA8D;QAC9D,MAAM,QAAQ,GAAG,cAAc,CAAC,GAAG,IAAI,CAAC,SAAS,UAAU,CAAC,CAAC;QAC7D,MAAM,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACnD,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAEzC,IAAI,CAAC,uBAAuB,QAAQ,KAAK,CAAC,CAAC;QAC3C,MAAM,WAAW,GAAG,MAAM,iBAAiB,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC;QACtF,IAAI,WAAW,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CAAC,sBAAsB,WAAW,CAAC,IAAI,MAAM,WAAW,CAAC,MAAM,EAAE,CAAC,CAAC;QACpF,CAAC;QAED,MAAM,SAAS,GAAG,sBAAsB,CAAC,QAAQ,CAAC,CAAC;QACnD,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACzC,qBAAqB,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;QAC3C,MAAM,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAEnD,UAAU;QACV,MAAM,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAEjC,IAAI,CAAC,SAAS,IAAI,CAAC,SAAS,iBAAiB,QAAQ,EAAE,CAAC,CAAC;IAC3D,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,OAAgC;QAC9C,wCAAwC;IAC1C,CAAC;IAED,KAAK,CAAC,SAAS;QACb,MAAM,QAAQ,GAAG,eAAe,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACjD,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC1B,IAAI,CAAC,SAAS,IAAI,CAAC,SAAS,iBAAiB,QAAQ,YAAY,CAAC,CAAC;YACnE,OAAO;QACT,CAAC;QACD,IAAI,CAAC,kBAAkB,IAAI,CAAC,SAAS,SAAS,QAAQ,KAAK,CAAC,CAAC;QAC7D,MAAM,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACnD,IAAI,CAAC,SAAS,IAAI,CAAC,SAAS,cAAc,CAAC,CAAC;IAC9C,CAAC;CACF;AAED,SAAS,qBAAqB,CAAC,SAAiB,EAAE,SAAiB;IACjE,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC,SAAS,CAAC,EAAE,CAAC;QAC3C,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;QACnC,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;QACpC,MAAM,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAC/C,IAAI,CAAC;YACH,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACxB,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,IAAK,CAA2B,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;gBAClD,MAAM,CAAC,CAAC;YACV,CAAC;YACD,MAAM,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YACpD,MAAM,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAChD,CAAC;IACH,CAAC;AACH,CAAC"}
@@ -1,5 +1,13 @@
1
1
  export declare const OPENCLAW_USER_DATA_FILES: readonly ["MEMORY.md", "USER.md", "IDENTITY.md", "SOUL.md", "TOOLS.md", "AGENTS.md", "HEARTBEAT.md"];
2
2
  export declare const LEGACY_OPENCLAW_WORKSPACE = "/root/openclaw/workspace";
3
+ type SkipReporter = (relativePath: string, reason: string) => void;
4
+ type BackupOptions = {
5
+ includeWorkspaceExtra?: boolean;
6
+ onSkip?: SkipReporter;
7
+ };
8
+ type RestoreOptions = {
9
+ includeWorkspaceExtra?: boolean;
10
+ };
3
11
  export type BackupFs = {
4
12
  existsSync(path: string): boolean;
5
13
  mkdirSync(path: string, options?: {
@@ -11,6 +19,10 @@ export type BackupFs = {
11
19
  statSync(path: string): {
12
20
  isDirectory(): boolean;
13
21
  };
22
+ readdirSync?: (path: string, options?: {
23
+ withFileTypes?: boolean;
24
+ }) => Array<DirentLike>;
25
+ readlinkSync?: (path: string) => string;
14
26
  };
15
27
  export type ArchiveFs = BackupFs & {
16
28
  readdirSync(path: string, options?: {
@@ -34,6 +46,7 @@ type DirentLike = {
34
46
  name: string;
35
47
  isDirectory(): boolean;
36
48
  isFile(): boolean;
49
+ isSymbolicLink?(): boolean;
37
50
  };
38
51
  export type ArchiveResult = {
39
52
  backupDir: string | null;
@@ -50,17 +63,19 @@ type ShellOps = {
50
63
  capture?(command: string): Promise<string>;
51
64
  };
52
65
  export declare function formatBackupTimestamp(now: Date): string;
53
- export declare function backupOpenclawUserData(now?: Date, fsOps?: BackupFs): string | null;
66
+ export declare function backupOpenclawUserData(now?: Date, fsOps?: BackupFs, options?: BackupOptions): string | null;
54
67
  export declare function createOpenclawUserDataArchive(options?: {
55
68
  archivePath?: string;
56
69
  now?: Date;
57
70
  fsOps?: ArchiveFs;
58
71
  shellOps?: ShellOps;
72
+ includeWorkspaceExtra?: boolean;
73
+ onSkip?: SkipReporter;
59
74
  }): Promise<ArchiveResult>;
60
75
  export declare function restoreOpenclawUserDataArchive(options: {
61
76
  archivePath: string;
62
77
  now?: Date;
63
78
  fsOps?: RestoreFs;
64
79
  shellOps?: ShellOps;
65
- }): Promise<RestoreResult>;
80
+ } & RestoreOptions): Promise<RestoreResult>;
66
81
  export {};
@@ -1,6 +1,7 @@
1
- import { existsSync, cpSync, mkdirSync, rmSync, statSync, lstatSync, chmodSync, readdirSync, } from 'node:fs';
1
+ import { existsSync, cpSync, mkdirSync, rmSync, statSync, lstatSync, chmodSync, readdirSync, readlinkSync, } from 'node:fs';
2
2
  import { posix as pathPosix } from 'node:path';
3
3
  import { shell, shellCapture } from './utils/shell.js';
4
+ import { warn } from './utils/logger.js';
4
5
  import { OKCLAW_BACKUP_DIR, OPENCLAW_WORKSPACE } from './utils/constants.js';
5
6
  export const OPENCLAW_USER_DATA_FILES = [
6
7
  'MEMORY.md',
@@ -15,8 +16,14 @@ export const LEGACY_OPENCLAW_WORKSPACE = '/root/openclaw/workspace';
15
16
  const WORKSPACE_BACKUP_DIR = 'workspace';
16
17
  const LEGACY_WORKSPACE_BACKUP_DIR = 'workspace-root-openclaw';
17
18
  const SKILLS_DIR = 'skills';
18
- const OPENCLAW_CONFIG_FILE = 'openclaw.json';
19
- const realBackupFs = { existsSync, mkdirSync, cpSync, statSync };
19
+ const realBackupFs = {
20
+ existsSync,
21
+ mkdirSync,
22
+ cpSync,
23
+ statSync,
24
+ readdirSync: readdirSync,
25
+ readlinkSync,
26
+ };
20
27
  const realArchiveFs = { ...realBackupFs, readdirSync: readdirSync };
21
28
  const realRestoreFs = {
22
29
  ...realBackupFs,
@@ -34,7 +41,7 @@ function copyIfExists(fsOps, src, dest, recursive = false) {
34
41
  fsOps.cpSync(src, dest, recursive ? { recursive: true } : undefined);
35
42
  return true;
36
43
  }
37
- export function backupOpenclawUserData(now = new Date(), fsOps = realBackupFs) {
44
+ export function backupOpenclawUserData(now = new Date(), fsOps = realBackupFs, options = {}) {
38
45
  const backupDir = `${OKCLAW_BACKUP_DIR}/openclaw-user-data-${formatBackupTimestamp(now)}`;
39
46
  const workspaces = Array.from(new Set([OPENCLAW_WORKSPACE, LEGACY_OPENCLAW_WORKSPACE]));
40
47
  let copiedItems = 0;
@@ -42,9 +49,12 @@ export function backupOpenclawUserData(now = new Date(), fsOps = realBackupFs) {
42
49
  for (const workspace of workspaces) {
43
50
  if (!fsOps.existsSync(workspace))
44
51
  continue;
45
- const workspaceBackupName = copiedWorkspaces === 0 ? WORKSPACE_BACKUP_DIR : LEGACY_WORKSPACE_BACKUP_DIR;
52
+ const workspaceBackupName = options.includeWorkspaceExtra
53
+ ? (workspace === OPENCLAW_WORKSPACE ? WORKSPACE_BACKUP_DIR : LEGACY_WORKSPACE_BACKUP_DIR)
54
+ : (copiedWorkspaces === 0 ? WORKSPACE_BACKUP_DIR : LEGACY_WORKSPACE_BACKUP_DIR);
46
55
  const workspaceBackupDir = `${backupDir}/${workspaceBackupName}`;
47
56
  let workspaceDirCreated = false;
57
+ const copiedRootEntries = new Set();
48
58
  const ensureWorkspaceBackupDir = () => {
49
59
  if (workspaceDirCreated)
50
60
  return;
@@ -58,6 +68,7 @@ export function backupOpenclawUserData(now = new Date(), fsOps = realBackupFs) {
58
68
  ensureWorkspaceBackupDir();
59
69
  if (copyIfExists(fsOps, src, `${workspaceBackupDir}/${file}`)) {
60
70
  copiedItems += 1;
71
+ copiedRootEntries.add(file);
61
72
  }
62
73
  }
63
74
  const skillsDir = `${workspace}/${SKILLS_DIR}`;
@@ -65,19 +76,107 @@ export function backupOpenclawUserData(now = new Date(), fsOps = realBackupFs) {
65
76
  ensureWorkspaceBackupDir();
66
77
  if (copyIfExists(fsOps, skillsDir, `${workspaceBackupDir}/${SKILLS_DIR}`, true)) {
67
78
  copiedItems += 1;
79
+ copiedRootEntries.add(SKILLS_DIR);
68
80
  }
69
81
  }
82
+ if (options.includeWorkspaceExtra) {
83
+ copiedItems += copyWorkspaceExtraEntries(fsOps, workspace, workspaceBackupDir, copiedRootEntries, ensureWorkspaceBackupDir, options.onSkip ?? defaultBackupSkip);
84
+ }
70
85
  if (workspaceDirCreated) {
71
86
  copiedWorkspaces += 1;
72
87
  }
73
88
  }
74
89
  return copiedItems > 0 ? backupDir : null;
75
90
  }
91
+ function defaultBackupSkip(relativePath, reason) {
92
+ warn(`全量迁移跳过不可移植条目 ${relativePath}: ${reason}`);
93
+ }
94
+ // 全量模式只把"可移植"条目纳入归档,让归档天生可在目标主机还原:
95
+ // 普通文件、目录、以及目标仍落在 workspace 内的软链都保留;指向 workspace 之外的软链
96
+ // 和 socket/fifo 等特殊文件在 backup 阶段就跳过并告警,而不是搬进去等 restore 整包失败。
97
+ function copyWorkspaceExtraEntries(fsOps, workspace, workspaceBackupDir, copiedRootEntries, ensureWorkspaceBackupDir, onSkip) {
98
+ if (!fsOps.readdirSync) {
99
+ throw new Error('backup fs does not support readdirSync for full workspace backup');
100
+ }
101
+ let copied = 0;
102
+ for (const entry of fsOps.readdirSync(workspace, { withFileTypes: true })) {
103
+ if (copiedRootEntries.has(entry.name))
104
+ continue;
105
+ const src = `${workspace}/${entry.name}`;
106
+ const dest = `${workspaceBackupDir}/${entry.name}`;
107
+ const rel = entry.name;
108
+ if (entry.isSymbolicLink?.()) {
109
+ if (!isPortableSymlink(fsOps, src, rel, onSkip))
110
+ continue;
111
+ ensureWorkspaceBackupDir();
112
+ fsOps.cpSync(src, dest);
113
+ copied += 1;
114
+ copiedRootEntries.add(entry.name);
115
+ continue;
116
+ }
117
+ if (entry.isDirectory()) {
118
+ ensureWorkspaceBackupDir();
119
+ copyPortableTree(fsOps, src, dest, rel, onSkip);
120
+ copied += 1;
121
+ copiedRootEntries.add(entry.name);
122
+ continue;
123
+ }
124
+ if (entry.isFile()) {
125
+ ensureWorkspaceBackupDir();
126
+ fsOps.cpSync(src, dest);
127
+ copied += 1;
128
+ copiedRootEntries.add(entry.name);
129
+ continue;
130
+ }
131
+ onSkip(rel, '不支持的文件类型');
132
+ }
133
+ return copied;
134
+ }
135
+ // 递归拷贝目录,逐项套用与 restore 校验一致的"可移植"判定,使嵌套软链/特殊文件也被过滤。
136
+ function copyPortableTree(fsOps, srcDir, destDir, relDir, onSkip) {
137
+ if (!fsOps.readdirSync) {
138
+ throw new Error('backup fs does not support readdirSync for full workspace backup');
139
+ }
140
+ fsOps.mkdirSync(destDir, { recursive: true });
141
+ for (const entry of fsOps.readdirSync(srcDir, { withFileTypes: true })) {
142
+ const src = `${srcDir}/${entry.name}`;
143
+ const dest = `${destDir}/${entry.name}`;
144
+ const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
145
+ if (entry.isSymbolicLink?.()) {
146
+ if (isPortableSymlink(fsOps, src, rel, onSkip)) {
147
+ fsOps.cpSync(src, dest);
148
+ }
149
+ continue;
150
+ }
151
+ if (entry.isDirectory()) {
152
+ copyPortableTree(fsOps, src, dest, rel, onSkip);
153
+ continue;
154
+ }
155
+ if (entry.isFile()) {
156
+ fsOps.cpSync(src, dest);
157
+ continue;
158
+ }
159
+ onSkip(rel, '不支持的文件类型');
160
+ }
161
+ }
162
+ function isPortableSymlink(fsOps, src, rel, onSkip) {
163
+ if (!fsOps.readlinkSync) {
164
+ throw new Error('backup fs does not support readlinkSync for full workspace backup');
165
+ }
166
+ const target = fsOps.readlinkSync(src);
167
+ if (symlinkTargetWithinWorkspace(rel, target))
168
+ return true;
169
+ onSkip(rel, `软链目标逃逸 workspace: ${target}`);
170
+ return false;
171
+ }
76
172
  export async function createOpenclawUserDataArchive(options = {}) {
77
173
  const now = options.now ?? new Date();
78
174
  const fsOps = options.fsOps ?? realArchiveFs;
79
175
  const shellOps = options.shellOps ?? { shell };
80
- const backupDir = backupOpenclawUserData(now, fsOps);
176
+ const backupDir = backupOpenclawUserData(now, fsOps, {
177
+ includeWorkspaceExtra: options.includeWorkspaceExtra,
178
+ onSkip: options.onSkip,
179
+ });
81
180
  if (!backupDir) {
82
181
  return { backupDir: null, archivePath: null, includedFiles: [] };
83
182
  }
@@ -109,16 +208,21 @@ export async function restoreOpenclawUserDataArchive(options) {
109
208
  validateArchiveEntries(entries);
110
209
  fsOps.mkdirSync(OPENCLAW_WORKSPACE, { recursive: true });
111
210
  fsOps.chmodSync(OPENCLAW_WORKSPACE, 0o755);
112
- const restored = new Set();
211
+ const restored = new Map();
113
212
  restoreWorkspaceBackup(fsOps, `${stagingDir}/${WORKSPACE_BACKUP_DIR}`, restored);
114
213
  restoreWorkspaceBackup(fsOps, `${stagingDir}/${LEGACY_WORKSPACE_BACKUP_DIR}`, restored);
115
- const restoredFiles = Array.from(restored).sort();
214
+ if (options.includeWorkspaceExtra) {
215
+ restoreWorkspaceExtraEntries(fsOps, `${stagingDir}/${WORKSPACE_BACKUP_DIR}`, restored);
216
+ restoreWorkspaceExtraEntries(fsOps, `${stagingDir}/${LEGACY_WORKSPACE_BACKUP_DIR}`, restored);
217
+ }
218
+ const restoredFiles = Array.from(restored.keys()).sort();
116
219
  for (const file of restoredFiles) {
117
220
  const path = `${OPENCLAW_WORKSPACE}/${file}`;
118
- if (file === SKILLS_DIR) {
221
+ const type = restored.get(file);
222
+ if (type === 'directory') {
119
223
  applyRestoredPermissions(fsOps, path);
120
224
  }
121
- else {
225
+ else if (type === 'file') {
122
226
  fsOps.chmodSync(path, 0o644);
123
227
  }
124
228
  }
@@ -149,13 +253,13 @@ function parseTarVerboseEntries(stdout) {
149
253
  const modeType = line[0];
150
254
  const tokens = line.split(/\s+/);
151
255
  const rawPath = tokens.slice(5).join(' ');
152
- const linkIndex = rawPath.indexOf(' -> ');
153
- const path = linkIndex >= 0 ? rawPath.slice(0, linkIndex) : rawPath;
154
- const linkTarget = linkIndex >= 0 ? rawPath.slice(linkIndex + ' -> '.length) : undefined;
155
256
  const type = modeType === '-' ? 'file'
156
257
  : modeType === 'd' ? 'directory'
157
258
  : modeType === 'l' ? 'symlink'
158
259
  : 'unsupported';
260
+ const linkIndex = type === 'symlink' ? rawPath.indexOf(' -> ') : -1;
261
+ const path = linkIndex >= 0 ? rawPath.slice(0, linkIndex) : rawPath;
262
+ const linkTarget = linkIndex >= 0 ? rawPath.slice(linkIndex + ' -> '.length) : undefined;
159
263
  return { type, path, linkTarget, raw: line };
160
264
  });
161
265
  }
@@ -177,13 +281,20 @@ function assertSymlinkWithinWorkspace(entry) {
177
281
  if (relative === null || !target) {
178
282
  throw new Error(`unsafe archive symlink: ${entry.raw}`);
179
283
  }
180
- const linkDir = pathPosix.dirname(`${OPENCLAW_WORKSPACE}/${relative}`);
284
+ if (!symlinkTargetWithinWorkspace(relative, target)) {
285
+ throw new Error(`unsafe archive symlink target: ${entry.raw}`);
286
+ }
287
+ }
288
+ // backup 过滤与 restore 校验共用的判定:软链在还原到 OPENCLAW_WORKSPACE 后,
289
+ // 其目标是否仍落在 workspace 内。relativePath 是软链相对 workspace 根的路径。
290
+ function symlinkTargetWithinWorkspace(relativePath, target) {
291
+ if (!target)
292
+ return false;
293
+ const linkDir = pathPosix.dirname(`${OPENCLAW_WORKSPACE}/${relativePath}`);
181
294
  const resolvedTarget = target.startsWith('/')
182
295
  ? pathPosix.normalize(target)
183
296
  : pathPosix.normalize(pathPosix.join(linkDir, target));
184
- if (resolvedTarget !== OPENCLAW_WORKSPACE && !resolvedTarget.startsWith(`${OPENCLAW_WORKSPACE}/`)) {
185
- throw new Error(`unsafe archive symlink target: ${entry.raw}`);
186
- }
297
+ return resolvedTarget === OPENCLAW_WORKSPACE || resolvedTarget.startsWith(`${OPENCLAW_WORKSPACE}/`);
187
298
  }
188
299
  function workspaceRelativePath(entryPath) {
189
300
  const normalized = normalizeArchiveEntry(entryPath);
@@ -205,13 +316,31 @@ function restoreWorkspaceBackup(fsOps, backupWorkspaceDir, restored) {
205
316
  if (!fsOps.existsSync(src))
206
317
  continue;
207
318
  fsOps.cpSync(src, `${OPENCLAW_WORKSPACE}/${file}`);
208
- restored.add(file);
319
+ restored.set(file, 'file');
209
320
  }
210
321
  const skillsSrc = `${backupWorkspaceDir}/${SKILLS_DIR}`;
211
322
  if (!restored.has(SKILLS_DIR) && fsOps.existsSync(skillsSrc) && fsOps.statSync(skillsSrc).isDirectory()) {
212
323
  fsOps.rmSync(`${OPENCLAW_WORKSPACE}/${SKILLS_DIR}`, { recursive: true, force: true });
213
324
  fsOps.cpSync(skillsSrc, `${OPENCLAW_WORKSPACE}/${SKILLS_DIR}`, { recursive: true });
214
- restored.add(SKILLS_DIR);
325
+ restored.set(SKILLS_DIR, 'directory');
326
+ }
327
+ }
328
+ function restoreWorkspaceExtraEntries(fsOps, backupWorkspaceDir, restored) {
329
+ if (!fsOps.existsSync(backupWorkspaceDir))
330
+ return;
331
+ for (const entry of fsOps.readdirSync(backupWorkspaceDir, { withFileTypes: true })) {
332
+ if (restored.has(entry.name))
333
+ continue;
334
+ const src = `${backupWorkspaceDir}/${entry.name}`;
335
+ const dest = `${OPENCLAW_WORKSPACE}/${entry.name}`;
336
+ fsOps.rmSync(dest, { recursive: true, force: true });
337
+ if (entry.isDirectory()) {
338
+ fsOps.cpSync(src, dest, { recursive: true });
339
+ restored.set(entry.name, 'directory');
340
+ continue;
341
+ }
342
+ fsOps.cpSync(src, dest);
343
+ restored.set(entry.name, entry.isSymbolicLink?.() ? 'symlink' : 'file');
215
344
  }
216
345
  }
217
346
  function applyRestoredPermissions(fsOps, path) {
@@ -256,8 +385,7 @@ function validateArchiveEntries(entries) {
256
385
  continue;
257
386
  const parts = normalized.split('/');
258
387
  if (entry.startsWith('/') ||
259
- parts.includes('..') ||
260
- parts.includes(OPENCLAW_CONFIG_FILE)) {
388
+ parts.includes('..')) {
261
389
  throw new Error(`unsafe archive entry: ${entry}`);
262
390
  }
263
391
  }