@xfxstudio/claworld 0.2.16 → 0.2.18

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,2115 +0,0 @@
1
- import { accessSync, constants as FS_CONSTANTS } from 'fs';
2
- import fs from 'fs/promises';
3
- import os from 'os';
4
- import path from 'path';
5
- import { spawnSync } from 'child_process';
6
- import vm from 'vm';
7
- import {
8
- applyClaworldBootstrapConfig,
9
- DEFAULT_CLAWORLD_ACCOUNT_ID,
10
- DEFAULT_CLAWORLD_AGENT_ID,
11
- DEFAULT_CLAWORLD_SERVER_URL,
12
- ensureObject,
13
- expandUserPath,
14
- findClaworldManagedRuntimeBackup,
15
- normalizeText,
16
- resolveClaworldManagedRuntimeOptions,
17
- setClaworldManagedRuntimeBackupState,
18
- stripClaworldManagedRuntimeConfig,
19
- } from '../plugin/managed-config.js';
20
- import {
21
- defaultClaworldAccountId,
22
- inspectClaworldChannelAccount,
23
- listClaworldAccountIds,
24
- } from '../plugin/config-schema.js';
25
- import {
26
- CLAWORLD_INSTALLER_COMMAND,
27
- CLAWORLD_INSTALLER_DEFAULT_PLUGIN_SOURCE,
28
- CLAWORLD_INSTALLER_PACKAGE_NAME,
29
- CLAWORLD_OPENCLAW_MIN_HOST_VERSION,
30
- CLAWORLD_UNINSTALL_COMMAND,
31
- CLAWORLD_UPDATE_COMMAND,
32
- } from './constants.js';
33
- import { seedManagedWorkspaceContract } from './workspace-contract.js';
34
-
35
- export const DEFAULT_OPENCLAW_BIN = 'openclaw';
36
- const DEFAULT_NPM_BIN = process.platform === 'win32' ? 'npm.cmd' : 'npm';
37
- export const DEFAULT_OPENCLAW_CONFIG_PATH = '~/.openclaw/openclaw.json';
38
- export const DEFAULT_OPENCLAW_STATE_DIR = null;
39
- export const DEFAULT_INSTALL_TIMEOUT_MS = 15_000;
40
- export const DEFAULT_VERIFICATION_ATTEMPTS = 6;
41
- export const DEFAULT_VERIFICATION_DELAY_MS = 1_000;
42
- export const seedManagedWorkspace = seedManagedWorkspaceContract;
43
- const TRACKED_PLUGIN_UPDATEABLE_SOURCES = new Set(['npm', 'marketplace']);
44
-
45
- function resolveRequireGatewayRunning(env = process.env) {
46
- const normalized = normalizeText(env?.CLAWORLD_INSTALLER_REQUIRE_GATEWAY_RUNNING, null);
47
- if (!normalized) return true;
48
- const lowered = normalized.toLowerCase();
49
- if (['0', 'false', 'no', 'off'].includes(lowered)) return false;
50
- if (['1', 'true', 'yes', 'on'].includes(lowered)) return true;
51
- return true;
52
- }
53
-
54
- function normalizeComparablePath(value) {
55
- if (!value) return null;
56
- return path.resolve(String(value));
57
- }
58
-
59
- function splitPackageNameSegments(packageName = CLAWORLD_INSTALLER_PACKAGE_NAME) {
60
- const normalized = String(packageName || '').trim();
61
- if (!normalized) return ['claworld'];
62
- return normalized.split('/').filter(Boolean);
63
- }
64
-
65
- function resolveInstallerManagedPluginInstallRoot(configPath = DEFAULT_OPENCLAW_CONFIG_PATH) {
66
- const resolvedConfigPath = path.resolve(expandUserPath(configPath, os.homedir()));
67
- return path.join(path.dirname(resolvedConfigPath), 'extensions', 'claworld');
68
- }
69
-
70
- function resolveInstallerManagedPluginSourcePath(installRoot, packageName = CLAWORLD_INSTALLER_PACKAGE_NAME) {
71
- return path.join(installRoot, 'node_modules', ...splitPackageNameSegments(packageName));
72
- }
73
-
74
- async function readJsonFile(filePath) {
75
- return JSON.parse(await fs.readFile(filePath, 'utf8'));
76
- }
77
-
78
- async function resolveLocalPluginInstallTarget({
79
- installMode = 'npm',
80
- installSource = CLAWORLD_INSTALLER_DEFAULT_PLUGIN_SOURCE,
81
- repoRoot = null,
82
- commandRunner = defaultCommandRunner,
83
- cwd = process.cwd(),
84
- env = process.env,
85
- dryRun = false,
86
- } = {}) {
87
- if (installMode === 'npm') {
88
- return {
89
- installSource,
90
- managedRepoRoot: repoRoot,
91
- stagedPackageRoot: null,
92
- };
93
- }
94
-
95
- const resolvedRepoRoot = normalizeComparablePath(repoRoot);
96
- const resolvedInstallSource = normalizeComparablePath(installSource);
97
- const derivedRepoRoot = resolvedRepoRoot
98
- || (
99
- resolvedInstallSource?.endsWith(`${path.sep}packages${path.sep}openclaw-plugin`)
100
- ? path.resolve(resolvedInstallSource, '..', '..')
101
- : null
102
- );
103
- const shouldStageRepo = Boolean(
104
- derivedRepoRoot
105
- && (
106
- resolvedInstallSource === derivedRepoRoot
107
- || resolvedInstallSource === path.join(derivedRepoRoot, 'packages', 'openclaw-plugin')
108
- || resolvedInstallSource === CLAWORLD_INSTALLER_PACKAGE_NAME
109
- || resolvedInstallSource === CLAWORLD_INSTALLER_DEFAULT_PLUGIN_SOURCE
110
- ),
111
- );
112
-
113
- if (!shouldStageRepo) {
114
- return {
115
- installSource,
116
- managedRepoRoot: installMode === 'link' ? installSource : repoRoot,
117
- stagedPackageRoot: null,
118
- };
119
- }
120
-
121
- const stagedPackageRoot = path.join(derivedRepoRoot, '.tmp', 'openclaw-plugin-package');
122
- const buildScriptPath = path.join(derivedRepoRoot, 'scripts', 'build-openclaw-plugin-package.mjs');
123
- if (!dryRun) {
124
- await executeCommand({
125
- commandRunner,
126
- bin: process.execPath,
127
- args: [buildScriptPath, '--output-dir', stagedPackageRoot],
128
- cwd,
129
- env,
130
- dryRun,
131
- });
132
- }
133
- return {
134
- installSource: stagedPackageRoot,
135
- managedRepoRoot: stagedPackageRoot,
136
- stagedPackageRoot,
137
- };
138
- }
139
-
140
- function listBindings(config = {}) {
141
- return Array.isArray(config.bindings) ? config.bindings : [];
142
- }
143
-
144
- function findAgentEntry(config = {}, agentId) {
145
- const list = Array.isArray(config?.agents?.list) ? config.agents.list : [];
146
- return list
147
- .map((item) => ensureObject(item))
148
- .find((item) => item.id === agentId) || null;
149
- }
150
-
151
- export function hasManagedBinding(config = {}, { agentId, accountId } = {}) {
152
- return listBindings(config).some((binding) => {
153
- const candidate = ensureObject(binding);
154
- const match = ensureObject(candidate.match);
155
- return candidate.agentId === agentId
156
- && match.channel === 'claworld'
157
- && match.accountId === accountId;
158
- });
159
- }
160
-
161
- export function isManagedToolAllowlistReady(config = {}, options = {}) {
162
- return true;
163
- }
164
-
165
- export function isRelayBootstrapReady(account = {}) {
166
- return Boolean(
167
- account?.configured
168
- && normalizeText(account?.appToken, null),
169
- );
170
- }
171
-
172
- function parseCommandVersion(text) {
173
- const match = String(text || '').match(/OpenClaw\s+([0-9]+(?:\.[0-9]+)+(?:-[0-9A-Za-z.-]+)?)/i)
174
- || String(text || '').match(/([0-9]+(?:\.[0-9]+)+(?:-[0-9A-Za-z.-]+)?)/);
175
- return match ? match[1] : null;
176
- }
177
-
178
- function parseVersionParts(version) {
179
- const normalized = String(version || '')
180
- .trim()
181
- .replace(/^[^\d]+/, '');
182
- return normalized
183
- .split('.')
184
- .map((value) => Number.parseInt(value, 10))
185
- .filter((value) => Number.isFinite(value));
186
- }
187
-
188
- function compareVersionParts(leftVersion, rightVersion) {
189
- const leftParts = parseVersionParts(leftVersion);
190
- const rightParts = parseVersionParts(rightVersion);
191
- const length = Math.max(leftParts.length, rightParts.length);
192
- for (let index = 0; index < length; index += 1) {
193
- const left = leftParts[index] ?? 0;
194
- const right = rightParts[index] ?? 0;
195
- if (left > right) return 1;
196
- if (left < right) return -1;
197
- }
198
- return 0;
199
- }
200
-
201
- function normalizeRelayHttpBaseUrl(serverUrl) {
202
- const parsed = new URL(serverUrl);
203
- if (parsed.protocol === 'ws:') parsed.protocol = 'http:';
204
- if (parsed.protocol === 'wss:') parsed.protocol = 'https:';
205
- parsed.pathname = '';
206
- parsed.search = '';
207
- parsed.hash = '';
208
- return parsed.toString().replace(/\/$/, '');
209
- }
210
-
211
- function buildFetchHeaders({ apiKey = null, appToken = null, body = false } = {}) {
212
- return {
213
- accept: 'application/json',
214
- ...(body ? { 'content-type': 'application/json' } : {}),
215
- ...(apiKey ? { 'x-api-key': apiKey } : {}),
216
- ...(appToken ? { authorization: `Bearer ${appToken}` } : {}),
217
- };
218
- }
219
-
220
- function createInstallerError(code, message, context = {}) {
221
- const error = new Error(message);
222
- error.code = code;
223
- error.context = context;
224
- return error;
225
- }
226
-
227
- function cloneObject(value = {}) {
228
- return JSON.parse(JSON.stringify(ensureObject(value)));
229
- }
230
-
231
- function isExplicitCommandPath(command = '') {
232
- const normalized = String(command || '').trim();
233
- if (!normalized) return false;
234
- return normalized.includes('/') || normalized.includes('\\') || path.isAbsolute(normalized);
235
- }
236
-
237
- function splitPathEnvEntries(pathValue = '') {
238
- return String(pathValue || '')
239
- .split(path.delimiter)
240
- .map((entry) => entry.trim())
241
- .filter(Boolean);
242
- }
243
-
244
- function isNodeModulesBinEntry(entry = '') {
245
- const normalized = path.resolve(String(entry || ''));
246
- return path.basename(normalized) === '.bin'
247
- && path.basename(path.dirname(normalized)) === 'node_modules';
248
- }
249
-
250
- function resolveCommandNameCandidates(command = '', env = process.env) {
251
- const normalized = String(command || '').trim();
252
- if (!normalized) return [];
253
- if (process.platform !== 'win32' || path.extname(normalized)) {
254
- return [normalized];
255
- }
256
-
257
- const pathExt = splitPathEnvEntries(env?.PATHEXT || process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD')
258
- .map((ext) => ext.toLowerCase());
259
- return [...new Set([normalized, ...pathExt.map((ext) => `${normalized}${ext}`)])];
260
- }
261
-
262
- function hasExecutableAccess(filePath = '') {
263
- try {
264
- accessSync(filePath, FS_CONSTANTS.X_OK);
265
- return true;
266
- } catch {
267
- return false;
268
- }
269
- }
270
-
271
- export function resolveOpenclawCliBinary({
272
- openclawBin = DEFAULT_OPENCLAW_BIN,
273
- env = process.env,
274
- } = {}) {
275
- const requestedBin = normalizeText(openclawBin, DEFAULT_OPENCLAW_BIN);
276
- if (
277
- requestedBin !== DEFAULT_OPENCLAW_BIN
278
- || isExplicitCommandPath(requestedBin)
279
- ) {
280
- return {
281
- requestedBin,
282
- binaryPath: requestedBin,
283
- binarySource: isExplicitCommandPath(requestedBin) ? 'explicit_path' : 'explicit_command',
284
- skippedLocalBinaryPaths: [],
285
- };
286
- }
287
-
288
- const candidates = [];
289
- for (const entry of splitPathEnvEntries(env?.PATH || process.env.PATH || '')) {
290
- for (const name of resolveCommandNameCandidates(requestedBin, env)) {
291
- const candidatePath = path.join(entry, name);
292
- if (hasExecutableAccess(candidatePath)) {
293
- candidates.push(candidatePath);
294
- }
295
- }
296
- }
297
-
298
- const uniqueCandidates = [...new Set(candidates)];
299
- const hostCandidates = uniqueCandidates.filter((candidate) => !isNodeModulesBinEntry(path.dirname(candidate)));
300
- const localCandidates = uniqueCandidates.filter((candidate) => isNodeModulesBinEntry(path.dirname(candidate)));
301
- const binaryPath = hostCandidates[0] || uniqueCandidates[0] || requestedBin;
302
-
303
- return {
304
- requestedBin,
305
- binaryPath,
306
- binarySource: hostCandidates[0]
307
- ? 'host_path'
308
- : uniqueCandidates[0]
309
- ? 'package_local_path'
310
- : 'default_command',
311
- skippedLocalBinaryPaths: localCandidates.filter((candidate) => candidate !== binaryPath),
312
- };
313
- }
314
-
315
- function parseJson(text, fallback = null) {
316
- try {
317
- return JSON.parse(String(text || '').trim());
318
- } catch {
319
- return fallback;
320
- }
321
- }
322
-
323
- export function parseJsonDocument(text, fallback = null) {
324
- const source = String(text || '').trim();
325
- const direct = parseJson(source, null);
326
- if (direct) return direct;
327
-
328
- const lines = source.split('\n');
329
- for (let index = 0; index < lines.length; index += 1) {
330
- const candidate = lines.slice(index).join('\n').trim();
331
- if (!candidate.startsWith('{') && !candidate.startsWith('[')) continue;
332
- const parsed = parseJson(candidate, null);
333
- if (parsed) return parsed;
334
- }
335
-
336
- return fallback;
337
- }
338
-
339
- function parseCommandJsonOutput(result = {}, fallback = null) {
340
- const stdout = String(result?.stdout || '').trim();
341
- const stderr = String(result?.stderr || '').trim();
342
- const combined = [stdout, stderr].filter(Boolean).join('\n');
343
- const reversed = [stderr, stdout].filter(Boolean).join('\n');
344
- return (
345
- parseJsonDocument(stdout, null)
346
- || parseJsonDocument(stderr, null)
347
- || parseJsonDocument(combined, null)
348
- || parseJsonDocument(reversed, null)
349
- || fallback
350
- );
351
- }
352
-
353
- function readClaworldAccountStatus(channelsStatus, accountId) {
354
- const items = Array.isArray(channelsStatus?.channelAccounts?.claworld)
355
- ? channelsStatus.channelAccounts.claworld
356
- : [];
357
- return items.find((item) => item?.accountId === accountId) || null;
358
- }
359
-
360
- function synthesizeChannelStatusFromConfig(config = {}) {
361
- const claworldChannel = config?.channels?.claworld;
362
- const configuredAccounts = claworldChannel?.accounts && typeof claworldChannel.accounts === 'object'
363
- ? Object.entries(claworldChannel.accounts)
364
- : [];
365
- if (configuredAccounts.length === 0) {
366
- return null;
367
- }
368
-
369
- const accounts = configuredAccounts.map(([entryKey, entryValue]) => {
370
- const entry = entryValue && typeof entryValue === 'object' ? entryValue : {};
371
- const accountId = String(entry.accountId || entryKey || '').trim();
372
- const hasToken = Boolean(
373
- entry.appToken
374
- || entry.apiKey
375
- || entry?.relay?.appToken
376
- || entry?.relay?.credentialToken,
377
- );
378
- return {
379
- accountId,
380
- enabled: entry.enabled !== false,
381
- configured: true,
382
- tokenSource: hasToken ? 'config' : 'missing',
383
- tokenStatus: hasToken ? 'available' : 'missing',
384
- lastInboundAt: null,
385
- lastOutboundAt: null,
386
- };
387
- }).filter((entry) => entry.accountId);
388
-
389
- if (accounts.length === 0) {
390
- return null;
391
- }
392
-
393
- const defaultAccountId = String(claworldChannel?.defaultAccount || accounts[0]?.accountId || '').trim() || null;
394
-
395
- return {
396
- ts: Date.now(),
397
- channelOrder: ['claworld'],
398
- channelLabels: {
399
- claworld: 'Claworld',
400
- },
401
- channelDetailLabels: {
402
- claworld: 'Claworld A2A Relay Channel',
403
- },
404
- channelSystemImages: {},
405
- channelMeta: [
406
- {
407
- id: 'claworld',
408
- label: 'Claworld',
409
- detailLabel: 'Claworld A2A Relay Channel',
410
- },
411
- ],
412
- channels: {
413
- claworld: {
414
- configured: true,
415
- },
416
- },
417
- channelAccounts: {
418
- claworld: accounts,
419
- },
420
- channelDefaultAccountId: defaultAccountId
421
- ? { claworld: defaultAccountId }
422
- : {},
423
- };
424
- }
425
-
426
- function parsePluginInfo(text = '') {
427
- const source = String(text || '');
428
- const status = source.match(/^\s*Status:\s+(.+)$/m)?.[1]?.trim() || null;
429
- const install = source.match(/^\s*Install:\s+(.+)$/m)?.[1]?.trim() || null;
430
- const sourcePath = source.match(/^\s*Source path:\s+(.+)$/m)?.[1]?.trim() || null;
431
- const version = source.match(/^\s*Version:\s+(.+)$/m)?.[1]?.trim()
432
- || source.match(/^\s*Recorded version:\s+(.+)$/m)?.[1]?.trim()
433
- || null;
434
- return {
435
- installed: Boolean(source.trim()),
436
- loaded: status === 'loaded',
437
- status,
438
- install,
439
- sourcePath,
440
- version,
441
- raw: source,
442
- };
443
- }
444
-
445
- function parseBindingLine(text = '', { agentId, accountId } = {}) {
446
- return String(text || '')
447
- .split('\n')
448
- .map((line) => line.trim())
449
- .find((line) => line === `- ${agentId} <- claworld accountId=${accountId}`) || null;
450
- }
451
-
452
- function normalizePathSuffix(value = '') {
453
- return String(value || '').replace(/\\/g, '/').replace(/\/+$/, '');
454
- }
455
-
456
- function isInstallerManagedLocalSourceRecord(installRecord = {}) {
457
- const source = normalizeText(installRecord.source, null);
458
- if (source !== 'path') {
459
- return false;
460
- }
461
- const spec = normalizeText(installRecord.spec, null) || normalizeText(installRecord.resolvedSpec, null);
462
- if (!spec || !spec.startsWith('@xfxstudio/claworld@')) {
463
- return false;
464
- }
465
- const installPath = normalizePathSuffix(installRecord.installPath);
466
- const sourcePath = normalizePathSuffix(installRecord.sourcePath);
467
- return (
468
- installPath.endsWith('/extensions/claworld')
469
- && sourcePath.endsWith('/extensions/claworld/node_modules/@xfxstudio/claworld')
470
- );
471
- }
472
-
473
- function inspectTrackedClaworldPluginInstall(config = {}) {
474
- const installRecord = ensureObject(config?.plugins?.installs?.claworld);
475
- const tracked = Object.keys(installRecord).length > 0;
476
- const recordedSource = normalizeText(installRecord.source, null);
477
- const source = isInstallerManagedLocalSourceRecord(installRecord)
478
- ? 'installer_npm'
479
- : recordedSource;
480
- return {
481
- tracked,
482
- updateable: tracked && TRACKED_PLUGIN_UPDATEABLE_SOURCES.has(source),
483
- source,
484
- recordedSource,
485
- spec: normalizeText(installRecord.spec, null),
486
- resolvedSpec: normalizeText(installRecord.resolvedSpec, null),
487
- resolvedVersion: normalizeText(installRecord.resolvedVersion, null),
488
- installPath: normalizeText(installRecord.installPath, null),
489
- sourcePath: normalizeText(installRecord.sourcePath, null),
490
- record: tracked ? installRecord : null,
491
- };
492
- }
493
-
494
- function needsPluginBootstrapConfig(config = {}) {
495
- if (config?.channels?.claworld) {
496
- return true;
497
- }
498
- return listBindings(config).some((binding) => ensureObject(binding).match?.channel === 'claworld');
499
- }
500
-
501
- function buildPluginBootstrapConfig(config = {}) {
502
- const next = cloneObject(config);
503
-
504
- const channels = ensureObject(next.channels);
505
- if (Object.prototype.hasOwnProperty.call(channels, 'claworld')) {
506
- delete channels.claworld;
507
- }
508
- if (Object.keys(channels).length > 0) {
509
- next.channels = channels;
510
- } else {
511
- delete next.channels;
512
- }
513
-
514
- if (Array.isArray(next.bindings)) {
515
- next.bindings = next.bindings.filter((binding) => ensureObject(binding).match?.channel !== 'claworld');
516
- }
517
-
518
- return next;
519
- }
520
-
521
- async function preparePluginBootstrapConfig({
522
- configPath = null,
523
- config = {},
524
- dryRun = false,
525
- } = {}) {
526
- if (!configPath || !needsPluginBootstrapConfig(config)) {
527
- return null;
528
- }
529
-
530
- if (dryRun) {
531
- return {
532
- configPath,
533
- pluginConfig: ensureObject(config.plugins),
534
- cleanup: async () => {},
535
- };
536
- }
537
-
538
- const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'claworld-plugin-bootstrap-'));
539
- const tempConfigPath = path.join(tempRoot, path.basename(configPath || 'openclaw.json'));
540
- await writeConfig(tempConfigPath, buildPluginBootstrapConfig(config));
541
- return {
542
- configPath: tempConfigPath,
543
- pluginConfig: null,
544
- cleanup: async () => {
545
- await fs.rm(tempRoot, { recursive: true, force: true });
546
- },
547
- };
548
- }
549
-
550
- function mergePluginMetadata(config = {}, pluginConfig = {}) {
551
- const extraPlugins = ensureObject(pluginConfig);
552
- if (Object.keys(extraPlugins).length === 0) {
553
- return cloneObject(config);
554
- }
555
-
556
- const next = cloneObject(config);
557
- const existingPlugins = ensureObject(next.plugins);
558
- next.plugins = {
559
- ...existingPlugins,
560
- ...extraPlugins,
561
- entries: {
562
- ...ensureObject(existingPlugins.entries),
563
- ...ensureObject(extraPlugins.entries),
564
- },
565
- installs: {
566
- ...ensureObject(existingPlugins.installs),
567
- ...ensureObject(extraPlugins.installs),
568
- },
569
- };
570
- return next;
571
- }
572
-
573
- async function installPublishedClaworldPackageToLocalSource({
574
- installSource = CLAWORLD_INSTALLER_DEFAULT_PLUGIN_SOURCE,
575
- configPath = DEFAULT_OPENCLAW_CONFIG_PATH,
576
- commandRunner = defaultCommandRunner,
577
- cwd = process.cwd(),
578
- env = process.env,
579
- dryRun = false,
580
- refresh = false,
581
- } = {}) {
582
- const installRoot = resolveInstallerManagedPluginInstallRoot(configPath);
583
- const sourcePath = resolveInstallerManagedPluginSourcePath(installRoot);
584
- if (!dryRun && refresh) {
585
- await fs.rm(installRoot, { recursive: true, force: true });
586
- }
587
- if (!dryRun) {
588
- await fs.mkdir(installRoot, { recursive: true });
589
- }
590
-
591
- await executeCommand({
592
- commandRunner,
593
- bin: DEFAULT_NPM_BIN,
594
- args: [
595
- 'install',
596
- '--ignore-scripts',
597
- '--no-package-lock',
598
- '--omit=dev',
599
- '--prefix',
600
- installRoot,
601
- installSource,
602
- ],
603
- cwd,
604
- env,
605
- dryRun,
606
- capture: false,
607
- });
608
-
609
- let resolvedVersion = null;
610
- if (!dryRun) {
611
- const sourcePackageJson = await readJsonFile(path.join(sourcePath, 'package.json'));
612
- resolvedVersion = normalizeText(sourcePackageJson?.version, null);
613
- }
614
-
615
- return {
616
- installRoot,
617
- sourcePath,
618
- resolvedVersion,
619
- pluginConfig: {
620
- installs: {
621
- claworld: {
622
- source: 'path',
623
- spec: installSource,
624
- resolvedSpec: installSource,
625
- ...(resolvedVersion ? { resolvedVersion } : {}),
626
- installPath: installRoot,
627
- sourcePath,
628
- },
629
- },
630
- },
631
- };
632
- }
633
-
634
- function defaultCommandRunner({
635
- bin,
636
- args,
637
- cwd = process.cwd(),
638
- env = process.env,
639
- dryRun = false,
640
- capture = true,
641
- } = {}) {
642
- const resolvedBin = resolveOpenclawCliBinary({
643
- openclawBin: bin,
644
- env,
645
- });
646
- const effectiveBin = resolvedBin.binaryPath;
647
- const rendered = [effectiveBin, ...args].join(' ');
648
- if (dryRun) {
649
- return {
650
- status: 0,
651
- stdout: '',
652
- stderr: '',
653
- rendered,
654
- dryRun: true,
655
- requestedBin: bin,
656
- resolvedBin: effectiveBin,
657
- binSource: resolvedBin.binarySource,
658
- skippedBinCandidates: resolvedBin.skippedLocalBinaryPaths,
659
- };
660
- }
661
-
662
- const useShell = process.platform === 'win32' && /\.(cmd|bat)$/i.test(effectiveBin);
663
- const result = spawnSync(effectiveBin, args, {
664
- cwd,
665
- env,
666
- shell: useShell,
667
- windowsHide: true,
668
- stdio: capture ? 'pipe' : 'inherit',
669
- encoding: 'utf8',
670
- });
671
-
672
- if (result.error) {
673
- const error = createInstallerError(
674
- 'openclaw_command_failed',
675
- `Failed to run "${rendered}": ${result.error.message}`,
676
- { rendered },
677
- );
678
- error.cause = result.error;
679
- throw error;
680
- }
681
-
682
- return {
683
- status: result.status ?? 0,
684
- stdout: result.stdout || '',
685
- stderr: result.stderr || '',
686
- rendered,
687
- requestedBin: bin,
688
- resolvedBin: effectiveBin,
689
- binSource: resolvedBin.binarySource,
690
- skippedBinCandidates: resolvedBin.skippedLocalBinaryPaths,
691
- };
692
- }
693
-
694
- async function executeCommand({
695
- commandRunner = defaultCommandRunner,
696
- bin,
697
- args,
698
- cwd = process.cwd(),
699
- env = process.env,
700
- dryRun = false,
701
- capture = true,
702
- allowFailure = false,
703
- } = {}) {
704
- const result = await Promise.resolve(commandRunner({
705
- bin,
706
- args,
707
- cwd,
708
- env,
709
- dryRun,
710
- capture,
711
- }));
712
- if (!allowFailure && result.status !== 0) {
713
- throw createInstallerError(
714
- 'openclaw_command_failed',
715
- `Command failed (${result.status}): ${result.rendered || [bin, ...args].join(' ')}`,
716
- { result },
717
- );
718
- }
719
- return result;
720
- }
721
-
722
- export function parseConfigObject(sourceText, sourceLabel = 'openclaw config') {
723
- const text = String(sourceText || '').replace(/^\uFEFF/, '').trim();
724
- if (!text) return {};
725
-
726
- try {
727
- const parsed = JSON.parse(text);
728
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
729
- throw new Error('config root must be an object');
730
- }
731
- return parsed;
732
- } catch {}
733
-
734
- try {
735
- const expression = text.replace(/;\s*$/, '');
736
- const script = new vm.Script(`(${expression}\n)`, { filename: sourceLabel });
737
- const result = script.runInNewContext(Object.create(null), { timeout: 1000 });
738
- if (!result || typeof result !== 'object' || Array.isArray(result)) {
739
- throw new Error('config root must evaluate to an object');
740
- }
741
- return JSON.parse(JSON.stringify(result));
742
- } catch (error) {
743
- throw createInstallerError(
744
- 'invalid_openclaw_config',
745
- `Failed to parse ${sourceLabel}: ${error.message}`,
746
- { sourceLabel },
747
- );
748
- }
749
- }
750
-
751
- export async function loadConfigFromDisk(configPath) {
752
- try {
753
- const raw = await fs.readFile(configPath, 'utf8');
754
- return { existed: true, config: parseConfigObject(raw, configPath) };
755
- } catch (error) {
756
- if (error && error.code === 'ENOENT') {
757
- return { existed: false, config: {} };
758
- }
759
- throw error;
760
- }
761
- }
762
-
763
- export function resolveClaworldInstallerStatePath(configPath) {
764
- const resolvedConfigPath = path.resolve(String(configPath || DEFAULT_OPENCLAW_CONFIG_PATH));
765
- return path.join(path.dirname(resolvedConfigPath), '.claworld-installer-state.json');
766
- }
767
-
768
- export async function loadInstallerStateFromDisk(installerStatePath) {
769
- try {
770
- const raw = await fs.readFile(installerStatePath, 'utf8');
771
- return { existed: true, state: parseConfigObject(raw, installerStatePath) };
772
- } catch (error) {
773
- if (error && error.code === 'ENOENT') {
774
- return { existed: false, state: {} };
775
- }
776
- throw error;
777
- }
778
- }
779
-
780
- export async function backupConfigIfPresent(configPath, existed, dryRun = false) {
781
- if (!existed) return null;
782
- const stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+$/, '').replace('T', '-');
783
- const backupPath = `${configPath}.bak.${stamp}`;
784
- if (dryRun) return backupPath;
785
- await fs.copyFile(configPath, backupPath);
786
- return backupPath;
787
- }
788
-
789
- export async function writeConfig(configPath, config, dryRun = false) {
790
- const rendered = `${JSON.stringify(config, null, 2)}\n`;
791
- if (dryRun) return rendered;
792
- await fs.mkdir(path.dirname(configPath), { recursive: true });
793
- await fs.writeFile(configPath, rendered, 'utf8');
794
- return rendered;
795
- }
796
-
797
- export async function writeInstallerState(installerStatePath, installerState, dryRun = false) {
798
- const renderedState = ensureObject(installerState);
799
- if (Object.keys(renderedState).length === 0) {
800
- if (dryRun) return '';
801
- await fs.rm(installerStatePath, { force: true });
802
- return '';
803
- }
804
- const rendered = `${JSON.stringify(renderedState, null, 2)}\n`;
805
- if (dryRun) return rendered;
806
- await fs.mkdir(path.dirname(installerStatePath), { recursive: true });
807
- await fs.writeFile(installerStatePath, rendered, 'utf8');
808
- return rendered;
809
- }
810
-
811
- export function buildOpenclawCommandEnv({ configPath, stateDir = null, env = process.env } = {}) {
812
- return {
813
- ...env,
814
- ...(configPath ? { OPENCLAW_CONFIG_PATH: configPath } : {}),
815
- ...(stateDir ? { OPENCLAW_STATE_DIR: stateDir } : {}),
816
- };
817
- }
818
-
819
- export function inspectManagedClaworldInstall({
820
- cfg = {},
821
- accountId = DEFAULT_CLAWORLD_ACCOUNT_ID,
822
- input = {},
823
- overrides = {},
824
- installerState = {},
825
- } = {}) {
826
- const configuredAccountIds = listClaworldAccountIds(cfg);
827
- const hasAnyConfig = configuredAccountIds.length > 0 || cfg?.channels?.claworld != null;
828
- const managedOptions = resolveClaworldManagedRuntimeOptions({
829
- cfg,
830
- accountId,
831
- input,
832
- overrides,
833
- installerState,
834
- });
835
- const managedAgentPresent = Boolean(findAgentEntry(cfg, managedOptions.agentId));
836
- const managedBindingPresent = hasManagedBinding(cfg, managedOptions);
837
- const toolsReady = isManagedToolAllowlistReady(cfg, managedOptions);
838
- const managedAccountPresent = configuredAccountIds.includes(managedOptions.accountId);
839
- const accountStatus = managedAccountPresent
840
- ? inspectClaworldChannelAccount(cfg, managedOptions.accountId)
841
- : inspectClaworldChannelAccount({}, managedOptions.accountId);
842
- const managedRuntimeReady = isRelayBootstrapReady(accountStatus);
843
- const managedReady = Boolean(
844
- managedAccountPresent
845
- && managedRuntimeReady
846
- && managedBindingPresent
847
- && toolsReady
848
- && (managedOptions.manageAgentEntry !== true || managedAgentPresent)
849
- );
850
-
851
- let statusLabel = 'needs setup';
852
- let selectionHint = 'remote relay world channel';
853
- if (managedRuntimeReady) {
854
- statusLabel = managedReady ? 'configured' : 'configured (managed refresh recommended)';
855
- selectionHint = managedReady ? 'configured · managed runtime' : 'configured · managed refresh';
856
- } else if (hasAnyConfig) {
857
- statusLabel = 'configured (activation pending)';
858
- selectionHint = 'configured · activation pending';
859
- }
860
-
861
- return {
862
- hasAnyConfig,
863
- configuredAccountIds,
864
- defaultAccountId: defaultClaworldAccountId(cfg) || null,
865
- managedOptions,
866
- managedAccountPresent,
867
- managedAgentPresent,
868
- managedBindingPresent,
869
- toolsReady,
870
- accountStatus,
871
- managedRuntimeReady,
872
- managedReady,
873
- reusableAppToken: normalizeText(
874
- managedOptions.appToken,
875
- normalizeText(accountStatus?.appToken, null),
876
- ),
877
- statusLabel,
878
- selectionHint,
879
- quickstartScore: managedRuntimeReady ? 2 : 5,
880
- };
881
- }
882
-
883
- export function buildManagedOnboardingStatus({ cfg = {}, accountId = DEFAULT_CLAWORLD_ACCOUNT_ID } = {}) {
884
- const inspection = inspectManagedClaworldInstall({ cfg, accountId });
885
- const configured = inspection.configuredAccountIds.some((configuredAccountId) =>
886
- isRelayBootstrapReady(inspectClaworldChannelAccount(cfg, configuredAccountId)),
887
- );
888
- return {
889
- configured,
890
- statusLines: [`Claworld: ${inspection.statusLabel}`],
891
- selectionHint: inspection.selectionHint,
892
- quickstartScore: configured ? 2 : inspection.quickstartScore,
893
- };
894
- }
895
-
896
- export async function detectOpenclawHost({
897
- openclawBin = DEFAULT_OPENCLAW_BIN,
898
- commandRunner = defaultCommandRunner,
899
- cwd = process.cwd(),
900
- env = process.env,
901
- dryRun = false,
902
- } = {}) {
903
- const result = await executeCommand({
904
- commandRunner,
905
- bin: openclawBin,
906
- args: ['--version'],
907
- cwd,
908
- env,
909
- dryRun,
910
- });
911
- const version = parseCommandVersion(result.stdout || result.stderr);
912
- if (!version) {
913
- throw createInstallerError(
914
- 'openclaw_version_unreadable',
915
- 'Unable to determine the installed OpenClaw version.',
916
- { result },
917
- );
918
- }
919
- return {
920
- version,
921
- raw: (result.stdout || result.stderr || '').trim(),
922
- requestedBin: result.requestedBin || openclawBin,
923
- binaryPath: result.resolvedBin || openclawBin,
924
- binarySource: result.binSource || 'default_command',
925
- skippedLocalBinaryPaths: Array.isArray(result.skippedBinCandidates)
926
- ? result.skippedBinCandidates
927
- : [],
928
- };
929
- }
930
-
931
- export async function inspectClaworldPluginInstall({
932
- openclawBin = DEFAULT_OPENCLAW_BIN,
933
- configPath = null,
934
- stateDir = DEFAULT_OPENCLAW_STATE_DIR,
935
- commandRunner = defaultCommandRunner,
936
- cwd = process.cwd(),
937
- env = process.env,
938
- dryRun = false,
939
- } = {}) {
940
- const result = await executeCommand({
941
- commandRunner,
942
- bin: openclawBin,
943
- args: ['plugins', 'info', 'claworld'],
944
- cwd,
945
- env: buildOpenclawCommandEnv({ configPath, stateDir, env }),
946
- dryRun,
947
- allowFailure: true,
948
- });
949
- if (result.status !== 0) {
950
- return {
951
- installed: false,
952
- loaded: false,
953
- status: null,
954
- raw: `${result.stdout || ''}${result.stderr || ''}`.trim(),
955
- };
956
- }
957
- return parsePluginInfo(`${result.stdout || ''}${result.stderr || ''}`);
958
- }
959
-
960
- export async function ensureClaworldPluginInstalled({
961
- openclawBin = DEFAULT_OPENCLAW_BIN,
962
- configPath = null,
963
- stateDir = DEFAULT_OPENCLAW_STATE_DIR,
964
- config = {},
965
- commandRunner = defaultCommandRunner,
966
- cwd = process.cwd(),
967
- env = process.env,
968
- dryRun = false,
969
- installMode = 'npm',
970
- installSource = CLAWORLD_INSTALLER_DEFAULT_PLUGIN_SOURCE,
971
- refresh = false,
972
- } = {}) {
973
- const trackedInstall = inspectTrackedClaworldPluginInstall(config);
974
- const bootstrap = await preparePluginBootstrapConfig({
975
- configPath,
976
- config,
977
- dryRun,
978
- });
979
- const pluginConfigPath = bootstrap?.configPath || configPath;
980
-
981
- try {
982
- const before = await inspectClaworldPluginInstall({
983
- openclawBin,
984
- configPath: pluginConfigPath,
985
- stateDir,
986
- commandRunner,
987
- cwd,
988
- env,
989
- dryRun,
990
- });
991
- const reusableInstallerManagedNpmInstall = (
992
- installMode === 'npm'
993
- && trackedInstall.tracked
994
- && trackedInstall.source === 'installer_npm'
995
- && trackedInstall.recordedSource === 'path'
996
- && normalizeText(trackedInstall.sourcePath, null)
997
- );
998
- if (
999
- before.installed
1000
- && !refresh
1001
- && (installMode !== 'npm' || reusableInstallerManagedNpmInstall)
1002
- ) {
1003
- return {
1004
- changed: false,
1005
- action: 'reused_existing_plugin_install',
1006
- plugin: before,
1007
- pluginConfig: bootstrap?.pluginConfig || null,
1008
- };
1009
- }
1010
-
1011
- if (installMode === 'skip') {
1012
- return {
1013
- changed: false,
1014
- action: 'skipped_plugin_install',
1015
- plugin: before,
1016
- pluginConfig: bootstrap?.pluginConfig || null,
1017
- };
1018
- }
1019
-
1020
- if (installMode === 'npm') {
1021
- const localInstall = await installPublishedClaworldPackageToLocalSource({
1022
- installSource,
1023
- configPath,
1024
- commandRunner,
1025
- cwd,
1026
- env,
1027
- dryRun,
1028
- refresh,
1029
- });
1030
- return {
1031
- changed: true,
1032
- action: reusableInstallerManagedNpmInstall ? 'refreshed_local_plugin_source' : 'staged_local_plugin_source',
1033
- plugin: before,
1034
- pluginConfig: {
1035
- ...ensureObject(bootstrap?.pluginConfig),
1036
- ...ensureObject(localInstall.pluginConfig),
1037
- entries: {
1038
- ...ensureObject(bootstrap?.pluginConfig?.entries),
1039
- ...ensureObject(localInstall.pluginConfig?.entries),
1040
- },
1041
- installs: {
1042
- ...ensureObject(bootstrap?.pluginConfig?.installs),
1043
- ...ensureObject(localInstall.pluginConfig?.installs),
1044
- },
1045
- },
1046
- installSource,
1047
- managedRepoRoot: localInstall.sourcePath,
1048
- };
1049
- }
1050
-
1051
- if (before.installed && refresh && installMode === 'copy') {
1052
- await executeCommand({
1053
- commandRunner,
1054
- bin: openclawBin,
1055
- args: ['plugins', 'uninstall', 'claworld', '--force'],
1056
- cwd,
1057
- env: buildOpenclawCommandEnv({ configPath: pluginConfigPath, stateDir, env }),
1058
- dryRun,
1059
- capture: false,
1060
- });
1061
- }
1062
-
1063
- const args = ['plugins', 'install', '--dangerously-force-unsafe-install'];
1064
- if (installMode === 'link') args.push('--link');
1065
- args.push(installSource);
1066
- await executeCommand({
1067
- commandRunner,
1068
- bin: openclawBin,
1069
- args,
1070
- cwd,
1071
- env: buildOpenclawCommandEnv({ configPath: pluginConfigPath, stateDir, env }),
1072
- dryRun,
1073
- capture: false,
1074
- });
1075
-
1076
- const after = await inspectClaworldPluginInstall({
1077
- openclawBin,
1078
- configPath: pluginConfigPath,
1079
- stateDir,
1080
- commandRunner,
1081
- cwd,
1082
- env,
1083
- dryRun,
1084
- });
1085
- if (!after.installed) {
1086
- throw createInstallerError(
1087
- 'claworld_plugin_install_failed',
1088
- 'OpenClaw did not report the claworld plugin as installed after plugin install.',
1089
- { installMode, installSource, before, after },
1090
- );
1091
- }
1092
-
1093
- const pluginConfig = pluginConfigPath
1094
- ? (await loadConfigFromDisk(pluginConfigPath)).config?.plugins || null
1095
- : null;
1096
-
1097
- return {
1098
- changed: true,
1099
- action: before.installed ? 'refreshed_plugin_install' : 'installed_plugin',
1100
- plugin: after,
1101
- pluginConfig,
1102
- installSource,
1103
- };
1104
- } finally {
1105
- if (bootstrap) {
1106
- await bootstrap.cleanup();
1107
- }
1108
- }
1109
- }
1110
-
1111
- export async function updateTrackedClaworldPluginInstall({
1112
- openclawBin = DEFAULT_OPENCLAW_BIN,
1113
- configPath = null,
1114
- stateDir = DEFAULT_OPENCLAW_STATE_DIR,
1115
- config = {},
1116
- installSource = null,
1117
- commandRunner = defaultCommandRunner,
1118
- cwd = process.cwd(),
1119
- env = process.env,
1120
- dryRun = false,
1121
- } = {}) {
1122
- const trackedInstall = inspectTrackedClaworldPluginInstall(config);
1123
- const before = await inspectClaworldPluginInstall({
1124
- openclawBin,
1125
- configPath,
1126
- stateDir,
1127
- commandRunner,
1128
- cwd,
1129
- env,
1130
- dryRun,
1131
- });
1132
-
1133
- if (!before.installed) {
1134
- throw createInstallerError(
1135
- 'claworld_plugin_not_installed',
1136
- 'The Claworld plugin is not installed or not discoverable. Run the install command before update.',
1137
- { plugin: before, trackedInstall },
1138
- );
1139
- }
1140
-
1141
- if (trackedInstall.source === 'installer_npm') {
1142
- const localInstall = await installPublishedClaworldPackageToLocalSource({
1143
- installSource: normalizeText(
1144
- installSource,
1145
- trackedInstall.spec || CLAWORLD_INSTALLER_DEFAULT_PLUGIN_SOURCE,
1146
- ),
1147
- configPath,
1148
- commandRunner,
1149
- cwd,
1150
- env,
1151
- dryRun,
1152
- refresh: true,
1153
- });
1154
- return {
1155
- changed: true,
1156
- action: dryRun ? 'dry_run_updated_local_plugin_source' : 'updated_local_plugin_source',
1157
- trackedInstall,
1158
- plugin: before,
1159
- pluginConfig: localInstall.pluginConfig,
1160
- managedRepoRoot: localInstall.sourcePath,
1161
- };
1162
- }
1163
-
1164
- if (!trackedInstall.tracked) {
1165
- return {
1166
- changed: false,
1167
- action: 'skipped_untracked_plugin_update',
1168
- reason: 'plugins.installs.claworld_missing',
1169
- trackedInstall,
1170
- plugin: before,
1171
- };
1172
- }
1173
-
1174
- if (!trackedInstall.updateable) {
1175
- return {
1176
- changed: false,
1177
- action: 'skipped_untracked_plugin_update',
1178
- reason: `plugins.installs.claworld_source_${trackedInstall.source || 'unknown'}_is_not_host_updateable`,
1179
- trackedInstall,
1180
- plugin: before,
1181
- };
1182
- }
1183
-
1184
- await executeCommand({
1185
- commandRunner,
1186
- bin: openclawBin,
1187
- args: ['plugins', 'update', 'claworld'],
1188
- cwd,
1189
- env: buildOpenclawCommandEnv({ configPath, stateDir, env }),
1190
- dryRun,
1191
- capture: false,
1192
- });
1193
-
1194
- const after = await inspectClaworldPluginInstall({
1195
- openclawBin,
1196
- configPath,
1197
- stateDir,
1198
- commandRunner,
1199
- cwd,
1200
- env,
1201
- dryRun,
1202
- });
1203
-
1204
- if (!after.installed) {
1205
- throw createInstallerError(
1206
- 'claworld_plugin_update_failed',
1207
- 'OpenClaw no longer reported the claworld plugin as installed after plugin update.',
1208
- { before, after, trackedInstall },
1209
- );
1210
- }
1211
-
1212
- return {
1213
- changed: true,
1214
- action: dryRun ? 'dry_run_tracked_plugin_update' : 'updated_tracked_plugin',
1215
- trackedInstall,
1216
- plugin: after,
1217
- };
1218
- }
1219
-
1220
- async function fetchJson(fetchImpl, url, init = {}) {
1221
- let response;
1222
- try {
1223
- response = await fetchImpl(url, init);
1224
- } catch (error) {
1225
- throw createInstallerError(
1226
- 'installer_fetch_failed',
1227
- `Failed to reach ${url}: ${error?.message || String(error)}`,
1228
- { url, method: init?.method || 'GET' },
1229
- );
1230
- }
1231
-
1232
- const text = await response.text();
1233
- return {
1234
- ok: response.ok,
1235
- status: response.status,
1236
- body: parseJsonDocument(text, null),
1237
- text,
1238
- };
1239
- }
1240
-
1241
- export async function fetchInstallManifest({
1242
- serverUrl = DEFAULT_CLAWORLD_SERVER_URL,
1243
- apiKey = null,
1244
- fetchImpl = globalThis.fetch?.bind(globalThis),
1245
- } = {}) {
1246
- if (typeof fetchImpl !== 'function') {
1247
- throw createInstallerError('missing_fetch', 'Install manifest fetch requires a fetch implementation.');
1248
- }
1249
- const url = `${normalizeRelayHttpBaseUrl(serverUrl)}/v1/meta/install`;
1250
- const response = await fetchJson(fetchImpl, url, {
1251
- method: 'GET',
1252
- headers: buildFetchHeaders({ apiKey }),
1253
- });
1254
- if (!response.ok || !response.body) {
1255
- throw createInstallerError(
1256
- 'install_manifest_unavailable',
1257
- `Failed to read Claworld install contract from ${url}.`,
1258
- { url, response },
1259
- );
1260
- }
1261
- return response.body;
1262
- }
1263
-
1264
- export async function activateInstall({
1265
- serverUrl = DEFAULT_CLAWORLD_SERVER_URL,
1266
- apiKey = null,
1267
- appToken = null,
1268
- displayName = null,
1269
- fetchImpl = globalThis.fetch?.bind(globalThis),
1270
- } = {}) {
1271
- if (typeof fetchImpl !== 'function') {
1272
- throw createInstallerError('missing_fetch', 'Install activation requires a fetch implementation.');
1273
- }
1274
- const url = `${normalizeRelayHttpBaseUrl(serverUrl)}/v1/onboarding/activate`;
1275
- const body = {};
1276
- if (displayName) {
1277
- body.displayName = displayName;
1278
- }
1279
- const response = await fetchJson(fetchImpl, url, {
1280
- method: 'POST',
1281
- headers: buildFetchHeaders({ apiKey, appToken, body: true }),
1282
- body: JSON.stringify(body),
1283
- });
1284
- if (!response.ok || !response.body) {
1285
- throw createInstallerError(
1286
- 'install_activation_failed',
1287
- `Failed to activate Claworld install via ${url}.`,
1288
- { url, response },
1289
- );
1290
- }
1291
- return response.body;
1292
- }
1293
-
1294
- export async function readGatewayStatus({
1295
- openclawBin = DEFAULT_OPENCLAW_BIN,
1296
- configPath = null,
1297
- stateDir = DEFAULT_OPENCLAW_STATE_DIR,
1298
- commandRunner = defaultCommandRunner,
1299
- cwd = process.cwd(),
1300
- env = process.env,
1301
- dryRun = false,
1302
- } = {}) {
1303
- const result = await executeCommand({
1304
- commandRunner,
1305
- bin: openclawBin,
1306
- args: ['gateway', 'status', '--json', '--no-probe'],
1307
- cwd,
1308
- env: buildOpenclawCommandEnv({ configPath, stateDir, env }),
1309
- dryRun,
1310
- });
1311
- const payload = parseCommandJsonOutput(result, null);
1312
- if (!payload) {
1313
- throw createInstallerError(
1314
- 'invalid_gateway_status',
1315
- 'OpenClaw gateway status did not produce JSON output.',
1316
- { result },
1317
- );
1318
- }
1319
- return payload;
1320
- }
1321
-
1322
- export async function readChannelStatus({
1323
- openclawBin = DEFAULT_OPENCLAW_BIN,
1324
- configPath = null,
1325
- stateDir = DEFAULT_OPENCLAW_STATE_DIR,
1326
- commandRunner = defaultCommandRunner,
1327
- cwd = process.cwd(),
1328
- env = process.env,
1329
- dryRun = false,
1330
- } = {}) {
1331
- const result = await executeCommand({
1332
- commandRunner,
1333
- bin: openclawBin,
1334
- args: ['channels', 'status', '--json'],
1335
- cwd,
1336
- env: buildOpenclawCommandEnv({ configPath, stateDir, env }),
1337
- dryRun,
1338
- });
1339
- const payload = parseCommandJsonOutput(result, null);
1340
- if (!payload) {
1341
- const configFromDisk = configPath
1342
- ? await loadConfigFromDisk(configPath).catch(() => ({ existed: false, config: null }))
1343
- : { existed: false, config: null };
1344
- const synthesizedPayload = synthesizeChannelStatusFromConfig(configFromDisk?.config || null);
1345
- if (synthesizedPayload) {
1346
- return synthesizedPayload;
1347
- }
1348
- throw createInstallerError(
1349
- 'invalid_channel_status',
1350
- 'OpenClaw channel status did not produce JSON output.',
1351
- { result },
1352
- );
1353
- }
1354
- return payload;
1355
- }
1356
-
1357
- export async function readAgentBindings({
1358
- openclawBin = DEFAULT_OPENCLAW_BIN,
1359
- configPath = null,
1360
- stateDir = DEFAULT_OPENCLAW_STATE_DIR,
1361
- commandRunner = defaultCommandRunner,
1362
- cwd = process.cwd(),
1363
- env = process.env,
1364
- dryRun = false,
1365
- } = {}) {
1366
- return await executeCommand({
1367
- commandRunner,
1368
- bin: openclawBin,
1369
- args: ['agents', 'bindings'],
1370
- cwd,
1371
- env: buildOpenclawCommandEnv({ configPath, stateDir, env }),
1372
- dryRun,
1373
- allowFailure: true,
1374
- });
1375
- }
1376
-
1377
- export async function validateOpenclawConfig({
1378
- openclawBin = DEFAULT_OPENCLAW_BIN,
1379
- configPath = null,
1380
- stateDir = DEFAULT_OPENCLAW_STATE_DIR,
1381
- commandRunner = defaultCommandRunner,
1382
- cwd = process.cwd(),
1383
- env = process.env,
1384
- dryRun = false,
1385
- } = {}) {
1386
- await executeCommand({
1387
- commandRunner,
1388
- bin: openclawBin,
1389
- args: ['config', 'validate'],
1390
- cwd,
1391
- env: buildOpenclawCommandEnv({ configPath, stateDir, env }),
1392
- dryRun,
1393
- capture: false,
1394
- });
1395
- return { ok: true };
1396
- }
1397
-
1398
- export async function refreshOpenclawRuntime({
1399
- openclawBin = DEFAULT_OPENCLAW_BIN,
1400
- configPath = null,
1401
- stateDir = DEFAULT_OPENCLAW_STATE_DIR,
1402
- commandRunner = defaultCommandRunner,
1403
- cwd = process.cwd(),
1404
- env = process.env,
1405
- dryRun = false,
1406
- } = {}) {
1407
- const commandEnv = buildOpenclawCommandEnv({ configPath, stateDir, env });
1408
- const restart = await executeCommand({
1409
- commandRunner,
1410
- bin: openclawBin,
1411
- args: ['gateway', 'restart'],
1412
- cwd,
1413
- env: commandEnv,
1414
- dryRun,
1415
- capture: false,
1416
- allowFailure: true,
1417
- });
1418
- if (restart.status === 0) {
1419
- return { ok: true, action: 'restart', result: restart };
1420
- }
1421
-
1422
- const start = await executeCommand({
1423
- commandRunner,
1424
- bin: openclawBin,
1425
- args: ['gateway', 'start'],
1426
- cwd,
1427
- env: commandEnv,
1428
- dryRun,
1429
- capture: false,
1430
- allowFailure: true,
1431
- });
1432
- if (start.status === 0) {
1433
- return { ok: true, action: 'start', result: start };
1434
- }
1435
-
1436
- throw createInstallerError(
1437
- 'openclaw_gateway_refresh_failed',
1438
- 'Failed to restart or start the OpenClaw gateway service.',
1439
- { restart, start },
1440
- );
1441
- }
1442
-
1443
- function wait(delayMs) {
1444
- return new Promise((resolve) => {
1445
- setTimeout(resolve, delayMs);
1446
- });
1447
- }
1448
-
1449
- export async function verifyClaworldInstall({
1450
- openclawBin = DEFAULT_OPENCLAW_BIN,
1451
- configPath = null,
1452
- stateDir = DEFAULT_OPENCLAW_STATE_DIR,
1453
- accountId = DEFAULT_CLAWORLD_ACCOUNT_ID,
1454
- agentId = DEFAULT_CLAWORLD_AGENT_ID,
1455
- commandRunner = defaultCommandRunner,
1456
- cwd = process.cwd(),
1457
- env = process.env,
1458
- dryRun = false,
1459
- attempts = DEFAULT_VERIFICATION_ATTEMPTS,
1460
- delayMs = DEFAULT_VERIFICATION_DELAY_MS,
1461
- requireGatewayRunning = resolveRequireGatewayRunning(env),
1462
- requireChannelToken = true,
1463
- } = {}) {
1464
- let lastResult = null;
1465
-
1466
- for (let attempt = 1; attempt <= attempts; attempt += 1) {
1467
- const [plugin, gatewayStatus, channelStatus, bindings] = await Promise.all([
1468
- inspectClaworldPluginInstall({
1469
- openclawBin,
1470
- configPath,
1471
- stateDir,
1472
- commandRunner,
1473
- cwd,
1474
- env,
1475
- dryRun,
1476
- }),
1477
- readGatewayStatus({
1478
- openclawBin,
1479
- configPath,
1480
- stateDir,
1481
- commandRunner,
1482
- cwd,
1483
- env,
1484
- dryRun,
1485
- }),
1486
- readChannelStatus({
1487
- openclawBin,
1488
- configPath,
1489
- stateDir,
1490
- commandRunner,
1491
- cwd,
1492
- env,
1493
- dryRun,
1494
- }),
1495
- readAgentBindings({
1496
- openclawBin,
1497
- configPath,
1498
- stateDir,
1499
- commandRunner,
1500
- cwd,
1501
- env,
1502
- dryRun,
1503
- }),
1504
- ]);
1505
-
1506
- const channelAccount = readClaworldAccountStatus(channelStatus, accountId);
1507
- const gatewayRunning = gatewayStatus?.service?.runtime?.status === 'running';
1508
- const bindingLine = parseBindingLine(`${bindings.stdout || ''}${bindings.stderr || ''}`, {
1509
- agentId,
1510
- accountId,
1511
- });
1512
- const channelReady = Boolean(
1513
- channelAccount
1514
- && channelAccount.configured === true
1515
- && channelAccount.enabled !== false
1516
- && (
1517
- requireChannelToken !== true
1518
- || channelAccount.tokenStatus === 'available'
1519
- )
1520
- );
1521
- const pluginReady = Boolean(plugin.installed);
1522
- const bindingReady = Boolean(bindingLine);
1523
-
1524
- lastResult = {
1525
- ok: pluginReady && channelReady && bindingReady && (!requireGatewayRunning || gatewayRunning),
1526
- attempt,
1527
- plugin,
1528
- gatewayStatus,
1529
- channelStatus,
1530
- channelAccount,
1531
- bindingLine,
1532
- gatewayRunning,
1533
- channelReady,
1534
- bindingReady,
1535
- requireGatewayRunning,
1536
- requireChannelToken,
1537
- };
1538
-
1539
- if (lastResult.ok) {
1540
- return lastResult;
1541
- }
1542
- if (attempt < attempts) {
1543
- await wait(delayMs);
1544
- }
1545
- }
1546
-
1547
- return lastResult || {
1548
- ok: false,
1549
- attempt: 0,
1550
- };
1551
- }
1552
-
1553
- async function reconcileManagedClaworldRuntime({
1554
- openclawBin = DEFAULT_OPENCLAW_BIN,
1555
- configPath = DEFAULT_OPENCLAW_CONFIG_PATH,
1556
- installerStatePath = null,
1557
- stateDir = DEFAULT_OPENCLAW_STATE_DIR,
1558
- currentConfigState = { existed: false, config: {} },
1559
- currentInstallerState = {},
1560
- currentConfig = {},
1561
- host = null,
1562
- serverUrl = null,
1563
- apiKey = null,
1564
- accountId = DEFAULT_CLAWORLD_ACCOUNT_ID,
1565
- agentId = DEFAULT_CLAWORLD_AGENT_ID,
1566
- workspace = null,
1567
- displayName = null,
1568
- toolProfile = null,
1569
- sessionDmScope = null,
1570
- repoRoot = null,
1571
- fetchImpl = globalThis.fetch?.bind(globalThis),
1572
- commandRunner = defaultCommandRunner,
1573
- cwd = process.cwd(),
1574
- env = process.env,
1575
- dryRun = false,
1576
- timeoutMs = DEFAULT_INSTALL_TIMEOUT_MS,
1577
- } = {}) {
1578
- const existingInstall = inspectManagedClaworldInstall({
1579
- cfg: currentConfig,
1580
- accountId,
1581
- installerState: currentInstallerState,
1582
- overrides: {
1583
- agentId,
1584
- workspace,
1585
- displayName,
1586
- toolProfile,
1587
- sessionDmScope,
1588
- ...(repoRoot ? { repoRoot } : {}),
1589
- },
1590
- });
1591
- const effectiveServerUrl = normalizeText(
1592
- serverUrl,
1593
- normalizeText(existingInstall.accountStatus?.serverUrl, DEFAULT_CLAWORLD_SERVER_URL),
1594
- );
1595
-
1596
- const installAccountId = normalizeText(
1597
- accountId,
1598
- DEFAULT_CLAWORLD_ACCOUNT_ID,
1599
- );
1600
- const installAgentId = normalizeText(
1601
- agentId,
1602
- installAccountId,
1603
- );
1604
-
1605
- const preflight = inspectManagedClaworldInstall({
1606
- cfg: currentConfig,
1607
- accountId: installAccountId,
1608
- installerState: currentInstallerState,
1609
- overrides: {
1610
- agentId: installAgentId,
1611
- workspace,
1612
- serverUrl: effectiveServerUrl,
1613
- displayName,
1614
- toolProfile,
1615
- sessionDmScope,
1616
- ...(repoRoot ? { repoRoot } : {}),
1617
- },
1618
- });
1619
- const desiredDisplayName = normalizeText(
1620
- displayName,
1621
- normalizeText(preflight.managedOptions.displayName, null),
1622
- );
1623
-
1624
- const managedOptions = resolveClaworldManagedRuntimeOptions({
1625
- cfg: currentConfig,
1626
- installerState: currentInstallerState,
1627
- accountId: installAccountId,
1628
- overrides: {
1629
- agentId: installAgentId,
1630
- workspace,
1631
- serverUrl: effectiveServerUrl,
1632
- apiKey,
1633
- displayName: desiredDisplayName,
1634
- toolProfile,
1635
- sessionDmScope,
1636
- replaceManagedRuntime: true,
1637
- installPlugin: false,
1638
- pluginInstallMode: 'skip',
1639
- ...(repoRoot ? { repoRoot } : {}),
1640
- },
1641
- });
1642
-
1643
- const transformed = applyClaworldBootstrapConfig(currentConfig, managedOptions);
1644
- const configChanged = JSON.stringify(currentConfigState.config) !== JSON.stringify(transformed.config);
1645
- const backupPath = configChanged
1646
- ? await backupConfigIfPresent(configPath, currentConfigState.existed, dryRun)
1647
- : null;
1648
- if (configChanged) {
1649
- await writeConfig(configPath, transformed.config, dryRun);
1650
- }
1651
-
1652
- const workspaceActions = managedOptions.manageWorkspace
1653
- ? await seedManagedWorkspace(managedOptions, dryRun)
1654
- : [];
1655
- await validateOpenclawConfig({
1656
- openclawBin,
1657
- configPath,
1658
- stateDir,
1659
- commandRunner,
1660
- cwd,
1661
- env,
1662
- dryRun,
1663
- });
1664
- const runtimeRefresh = await refreshOpenclawRuntime({
1665
- openclawBin,
1666
- configPath,
1667
- stateDir,
1668
- commandRunner,
1669
- cwd,
1670
- env,
1671
- dryRun,
1672
- });
1673
- const verification = await verifyClaworldInstall({
1674
- openclawBin,
1675
- configPath,
1676
- stateDir,
1677
- accountId: installAccountId,
1678
- agentId: installAgentId,
1679
- commandRunner,
1680
- cwd,
1681
- env,
1682
- dryRun,
1683
- delayMs: Math.min(timeoutMs, DEFAULT_VERIFICATION_DELAY_MS),
1684
- requireChannelToken: Boolean(managedOptions.appToken),
1685
- });
1686
- if (!verification.ok) {
1687
- throw createInstallerError(
1688
- 'claworld_install_verification_failed',
1689
- 'Claworld install verification did not confirm the managed channel runtime shape.',
1690
- { verification },
1691
- );
1692
- }
1693
-
1694
- let installerStateChanged = false;
1695
- if (installerStatePath && installAccountId) {
1696
- const nextInstallerState = JSON.parse(JSON.stringify(ensureObject(currentInstallerState)));
1697
- setClaworldManagedRuntimeBackupState(nextInstallerState, installAccountId, null);
1698
- installerStateChanged = JSON.stringify(currentInstallerState) !== JSON.stringify(nextInstallerState);
1699
- if (installerStateChanged) {
1700
- await writeInstallerState(installerStatePath, nextInstallerState, dryRun);
1701
- }
1702
- }
1703
-
1704
- return {
1705
- backupPath,
1706
- existingInstall,
1707
- effectiveServerUrl,
1708
- preflight,
1709
- activationStatus: managedOptions.appToken ? 'ready' : 'pending',
1710
- managedOptions,
1711
- transformed,
1712
- configChanged,
1713
- workspaceActions,
1714
- runtimeRefresh,
1715
- verification,
1716
- installerStatePath,
1717
- installerStateChanged,
1718
- };
1719
- }
1720
-
1721
- export async function runClaworldInstallerInstall({
1722
- openclawBin = DEFAULT_OPENCLAW_BIN,
1723
- configPath = DEFAULT_OPENCLAW_CONFIG_PATH,
1724
- stateDir = DEFAULT_OPENCLAW_STATE_DIR,
1725
- serverUrl = null,
1726
- apiKey = null,
1727
- accountId = DEFAULT_CLAWORLD_ACCOUNT_ID,
1728
- agentId = DEFAULT_CLAWORLD_AGENT_ID,
1729
- workspace = null,
1730
- displayName = null,
1731
- toolProfile = null,
1732
- sessionDmScope = null,
1733
- repoRoot = null,
1734
- pluginInstallMode = 'npm',
1735
- pluginInstallSource = CLAWORLD_INSTALLER_DEFAULT_PLUGIN_SOURCE,
1736
- fetchImpl = globalThis.fetch?.bind(globalThis),
1737
- commandRunner = defaultCommandRunner,
1738
- cwd = process.cwd(),
1739
- env = process.env,
1740
- dryRun = false,
1741
- timeoutMs = DEFAULT_INSTALL_TIMEOUT_MS,
1742
- } = {}) {
1743
- const resolvedConfigPath = path.resolve(expandUserPath(configPath, os.homedir()));
1744
- const installerStatePath = resolveClaworldInstallerStatePath(resolvedConfigPath);
1745
- const resolvedStateDir = stateDir ? path.resolve(expandUserPath(stateDir, os.homedir())) : null;
1746
- const commandEnv = buildOpenclawCommandEnv({
1747
- configPath: resolvedConfigPath,
1748
- stateDir: resolvedStateDir,
1749
- env,
1750
- });
1751
- const currentConfigState = await loadConfigFromDisk(resolvedConfigPath);
1752
- const currentInstallerState = (await loadInstallerStateFromDisk(installerStatePath)).state;
1753
- const currentConfigBeforePluginInstall = currentConfigState.config;
1754
- const host = await detectOpenclawHost({
1755
- openclawBin,
1756
- commandRunner,
1757
- cwd,
1758
- env: commandEnv,
1759
- dryRun,
1760
- });
1761
- if (compareVersionParts(host.version, CLAWORLD_OPENCLAW_MIN_HOST_VERSION) < 0) {
1762
- throw createInstallerError(
1763
- 'openclaw_version_too_old',
1764
- `OpenClaw ${host.version} is below the required minimum ${CLAWORLD_OPENCLAW_MIN_HOST_VERSION}.`,
1765
- { hostVersion: host.version, minHostVersion: CLAWORLD_OPENCLAW_MIN_HOST_VERSION },
1766
- );
1767
- }
1768
-
1769
- const localPluginInstall = await resolveLocalPluginInstallTarget({
1770
- installMode: pluginInstallMode,
1771
- installSource: pluginInstallSource,
1772
- repoRoot,
1773
- commandRunner,
1774
- cwd,
1775
- env,
1776
- dryRun,
1777
- });
1778
-
1779
- const plugin = await ensureClaworldPluginInstalled({
1780
- openclawBin,
1781
- configPath: resolvedConfigPath,
1782
- stateDir: resolvedStateDir,
1783
- config: currentConfigBeforePluginInstall,
1784
- commandRunner,
1785
- cwd,
1786
- env,
1787
- dryRun,
1788
- installMode: pluginInstallMode,
1789
- installSource: localPluginInstall.installSource,
1790
- refresh: false,
1791
- });
1792
- const currentConfig = mergePluginMetadata(
1793
- currentConfigBeforePluginInstall,
1794
- plugin.pluginConfig,
1795
- );
1796
- const lifecycle = await reconcileManagedClaworldRuntime({
1797
- openclawBin,
1798
- configPath: resolvedConfigPath,
1799
- installerStatePath,
1800
- stateDir: resolvedStateDir,
1801
- currentConfigState,
1802
- currentInstallerState,
1803
- currentConfig,
1804
- host,
1805
- serverUrl,
1806
- apiKey,
1807
- accountId,
1808
- agentId,
1809
- workspace,
1810
- displayName,
1811
- toolProfile,
1812
- sessionDmScope,
1813
- repoRoot: plugin.managedRepoRoot || localPluginInstall.managedRepoRoot,
1814
- fetchImpl,
1815
- commandRunner,
1816
- cwd,
1817
- env,
1818
- dryRun,
1819
- timeoutMs,
1820
- });
1821
-
1822
- return {
1823
- ok: true,
1824
- command: CLAWORLD_INSTALLER_COMMAND,
1825
- configPath: resolvedConfigPath,
1826
- installerStatePath,
1827
- stateDir: resolvedStateDir,
1828
- host,
1829
- plugin,
1830
- ...lifecycle,
1831
- };
1832
- }
1833
-
1834
- export async function runClaworldInstallerUpdate({
1835
- openclawBin = DEFAULT_OPENCLAW_BIN,
1836
- configPath = DEFAULT_OPENCLAW_CONFIG_PATH,
1837
- stateDir = DEFAULT_OPENCLAW_STATE_DIR,
1838
- serverUrl = null,
1839
- apiKey = null,
1840
- accountId = DEFAULT_CLAWORLD_ACCOUNT_ID,
1841
- agentId = DEFAULT_CLAWORLD_AGENT_ID,
1842
- workspace = null,
1843
- displayName = null,
1844
- pluginInstallSource = null,
1845
- toolProfile = null,
1846
- sessionDmScope = null,
1847
- fetchImpl = globalThis.fetch?.bind(globalThis),
1848
- commandRunner = defaultCommandRunner,
1849
- cwd = process.cwd(),
1850
- env = process.env,
1851
- dryRun = false,
1852
- timeoutMs = DEFAULT_INSTALL_TIMEOUT_MS,
1853
- } = {}) {
1854
- const resolvedConfigPath = path.resolve(expandUserPath(configPath, os.homedir()));
1855
- const installerStatePath = resolveClaworldInstallerStatePath(resolvedConfigPath);
1856
- const resolvedStateDir = stateDir ? path.resolve(expandUserPath(stateDir, os.homedir())) : null;
1857
- const commandEnv = buildOpenclawCommandEnv({
1858
- configPath: resolvedConfigPath,
1859
- stateDir: resolvedStateDir,
1860
- env,
1861
- });
1862
- const currentConfigState = await loadConfigFromDisk(resolvedConfigPath);
1863
- const currentInstallerState = (await loadInstallerStateFromDisk(installerStatePath)).state;
1864
- const host = await detectOpenclawHost({
1865
- openclawBin,
1866
- commandRunner,
1867
- cwd,
1868
- env: commandEnv,
1869
- dryRun,
1870
- });
1871
- if (compareVersionParts(host.version, CLAWORLD_OPENCLAW_MIN_HOST_VERSION) < 0) {
1872
- throw createInstallerError(
1873
- 'openclaw_version_too_old',
1874
- `OpenClaw ${host.version} is below the required minimum ${CLAWORLD_OPENCLAW_MIN_HOST_VERSION}.`,
1875
- { hostVersion: host.version, minHostVersion: CLAWORLD_OPENCLAW_MIN_HOST_VERSION },
1876
- );
1877
- }
1878
-
1879
- const plugin = await updateTrackedClaworldPluginInstall({
1880
- openclawBin,
1881
- configPath: resolvedConfigPath,
1882
- stateDir: resolvedStateDir,
1883
- config: currentConfigState.config,
1884
- installSource: pluginInstallSource,
1885
- commandRunner,
1886
- cwd,
1887
- env,
1888
- dryRun,
1889
- });
1890
-
1891
- const refreshedConfigState = await loadConfigFromDisk(resolvedConfigPath);
1892
- const currentConfig = mergePluginMetadata(refreshedConfigState.config, plugin.pluginConfig || null);
1893
- const lifecycle = await reconcileManagedClaworldRuntime({
1894
- openclawBin,
1895
- configPath: resolvedConfigPath,
1896
- installerStatePath,
1897
- stateDir: resolvedStateDir,
1898
- currentConfigState: refreshedConfigState,
1899
- currentInstallerState,
1900
- currentConfig,
1901
- host,
1902
- serverUrl,
1903
- apiKey,
1904
- accountId,
1905
- agentId,
1906
- workspace,
1907
- displayName,
1908
- toolProfile,
1909
- sessionDmScope,
1910
- repoRoot: plugin.managedRepoRoot || null,
1911
- fetchImpl,
1912
- commandRunner,
1913
- cwd,
1914
- env,
1915
- dryRun,
1916
- timeoutMs,
1917
- });
1918
-
1919
- return {
1920
- ok: true,
1921
- command: CLAWORLD_UPDATE_COMMAND,
1922
- configPath: resolvedConfigPath,
1923
- installerStatePath,
1924
- stateDir: resolvedStateDir,
1925
- host,
1926
- plugin,
1927
- ...lifecycle,
1928
- };
1929
- }
1930
-
1931
- export async function runClaworldInstallerUninstall({
1932
- openclawBin = DEFAULT_OPENCLAW_BIN,
1933
- configPath = DEFAULT_OPENCLAW_CONFIG_PATH,
1934
- stateDir = DEFAULT_OPENCLAW_STATE_DIR,
1935
- accountId = DEFAULT_CLAWORLD_ACCOUNT_ID,
1936
- agentId = DEFAULT_CLAWORLD_AGENT_ID,
1937
- commandRunner = defaultCommandRunner,
1938
- cwd = process.cwd(),
1939
- env = process.env,
1940
- dryRun = false,
1941
- } = {}) {
1942
- const resolvedConfigPath = path.resolve(expandUserPath(configPath, os.homedir()));
1943
- const installerStatePath = resolveClaworldInstallerStatePath(resolvedConfigPath);
1944
- const resolvedStateDir = stateDir ? path.resolve(expandUserPath(stateDir, os.homedir())) : null;
1945
- const commandEnv = buildOpenclawCommandEnv({
1946
- configPath: resolvedConfigPath,
1947
- stateDir: resolvedStateDir,
1948
- env,
1949
- });
1950
- const currentConfigState = await loadConfigFromDisk(resolvedConfigPath);
1951
- const currentInstallerState = (await loadInstallerStateFromDisk(installerStatePath)).state;
1952
- const currentConfig = currentConfigState.config;
1953
- const trackedInstall = inspectTrackedClaworldPluginInstall(currentConfig);
1954
- const host = await detectOpenclawHost({
1955
- openclawBin,
1956
- commandRunner,
1957
- cwd,
1958
- env: commandEnv,
1959
- dryRun,
1960
- });
1961
- if (compareVersionParts(host.version, CLAWORLD_OPENCLAW_MIN_HOST_VERSION) < 0) {
1962
- throw createInstallerError(
1963
- 'openclaw_version_too_old',
1964
- `OpenClaw ${host.version} is below the required minimum ${CLAWORLD_OPENCLAW_MIN_HOST_VERSION}.`,
1965
- { hostVersion: host.version, minHostVersion: CLAWORLD_OPENCLAW_MIN_HOST_VERSION },
1966
- );
1967
- }
1968
-
1969
- const pluginBefore = await inspectClaworldPluginInstall({
1970
- openclawBin,
1971
- configPath: resolvedConfigPath,
1972
- stateDir: resolvedStateDir,
1973
- commandRunner,
1974
- cwd,
1975
- env,
1976
- dryRun,
1977
- });
1978
- const transformed = stripClaworldManagedRuntimeConfig(currentConfig, {
1979
- accountId,
1980
- agentId,
1981
- preserveBackup: true,
1982
- });
1983
- const configChanged = JSON.stringify(currentConfig) !== JSON.stringify(transformed.config);
1984
- const backupPath = configChanged
1985
- ? await backupConfigIfPresent(resolvedConfigPath, currentConfigState.existed, dryRun)
1986
- : null;
1987
- if (configChanged) {
1988
- await writeConfig(resolvedConfigPath, transformed.config, dryRun);
1989
- }
1990
- const nextInstallerState = JSON.parse(JSON.stringify(ensureObject(currentInstallerState)));
1991
- setClaworldManagedRuntimeBackupState(nextInstallerState, accountId, transformed.backup);
1992
- const installerStateChanged = JSON.stringify(currentInstallerState) !== JSON.stringify(nextInstallerState);
1993
- if (installerStateChanged) {
1994
- await writeInstallerState(installerStatePath, nextInstallerState, dryRun);
1995
- }
1996
-
1997
- await validateOpenclawConfig({
1998
- openclawBin,
1999
- configPath: resolvedConfigPath,
2000
- stateDir: resolvedStateDir,
2001
- commandRunner,
2002
- cwd,
2003
- env,
2004
- dryRun,
2005
- });
2006
-
2007
- const uninstallBootstrap = pluginBefore.installed
2008
- ? await preparePluginBootstrapConfig({
2009
- configPath: resolvedConfigPath,
2010
- config: currentConfig,
2011
- dryRun,
2012
- })
2013
- : null;
2014
-
2015
- let pluginAction = 'plugin_already_absent';
2016
- try {
2017
- if (trackedInstall.source === 'installer_npm') {
2018
- const localInstallPath = normalizeText(trackedInstall.installPath, null) || normalizeText(trackedInstall.sourcePath, null);
2019
- if (!localInstallPath) {
2020
- throw createInstallerError(
2021
- 'claworld_installer_managed_source_missing',
2022
- 'Installer-managed Claworld source is missing its local install path.',
2023
- { trackedInstall },
2024
- );
2025
- }
2026
- if (!dryRun) {
2027
- await fs.rm(localInstallPath, { recursive: true, force: true });
2028
- }
2029
- pluginAction = 'removed_local_plugin_source';
2030
- } else if (pluginBefore.installed) {
2031
- await executeCommand({
2032
- commandRunner,
2033
- bin: openclawBin,
2034
- args: ['plugins', 'uninstall', 'claworld', '--force'],
2035
- cwd,
2036
- env: buildOpenclawCommandEnv({
2037
- configPath: uninstallBootstrap?.configPath || resolvedConfigPath,
2038
- stateDir: resolvedStateDir,
2039
- env,
2040
- }),
2041
- dryRun,
2042
- capture: false,
2043
- });
2044
- pluginAction = 'uninstalled_plugin';
2045
- }
2046
- } finally {
2047
- if (uninstallBootstrap) {
2048
- await uninstallBootstrap.cleanup();
2049
- }
2050
- }
2051
-
2052
- const runtimeRefresh = await refreshOpenclawRuntime({
2053
- openclawBin,
2054
- configPath: resolvedConfigPath,
2055
- stateDir: resolvedStateDir,
2056
- commandRunner,
2057
- cwd,
2058
- env,
2059
- dryRun,
2060
- });
2061
- const pluginAfter = await inspectClaworldPluginInstall({
2062
- openclawBin,
2063
- configPath: resolvedConfigPath,
2064
- stateDir: resolvedStateDir,
2065
- commandRunner,
2066
- cwd,
2067
- env,
2068
- dryRun,
2069
- });
2070
- if ((pluginBefore.installed || trackedInstall.source === 'installer_npm') && pluginAfter.installed) {
2071
- throw createInstallerError(
2072
- 'claworld_plugin_uninstall_failed',
2073
- 'OpenClaw still reports the claworld plugin as installed after uninstall.',
2074
- { before: pluginBefore, after: pluginAfter },
2075
- );
2076
- }
2077
-
2078
- const gatewayStatus = await readGatewayStatus({
2079
- openclawBin,
2080
- configPath: resolvedConfigPath,
2081
- stateDir: resolvedStateDir,
2082
- commandRunner,
2083
- cwd,
2084
- env,
2085
- dryRun,
2086
- });
2087
-
2088
- return {
2089
- ok: true,
2090
- command: CLAWORLD_UNINSTALL_COMMAND,
2091
- configPath: resolvedConfigPath,
2092
- installerStatePath,
2093
- stateDir: resolvedStateDir,
2094
- host,
2095
- backupPath,
2096
- transformed,
2097
- configChanged,
2098
- installerStateChanged,
2099
- plugin: {
2100
- action: pluginAction,
2101
- before: pluginBefore,
2102
- after: pluginAfter,
2103
- },
2104
- runtimeRefresh,
2105
- gatewayStatus,
2106
- };
2107
- }
2108
-
2109
- export {
2110
- compareVersionParts,
2111
- defaultCommandRunner,
2112
- findAgentEntry,
2113
- parseCommandVersion,
2114
- readClaworldAccountStatus,
2115
- };