@teamclaws/teamclaw 2026.3.25 → 2026.3.26-2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/cli.mjs +519 -28
- package/index.ts +31 -16
- package/openclaw.plugin.json +3 -3
- package/package.json +22 -4
- package/src/config.ts +2 -2
- package/src/controller/controller-capacity.ts +23 -0
- package/src/controller/controller-service.ts +27 -1
- package/src/controller/controller-tools.ts +251 -7
- package/src/controller/http-server.ts +976 -38
- package/src/controller/orchestration-manifest.ts +105 -0
- package/src/controller/prompt-injector.ts +42 -7
- package/src/controller/worker-provisioning.ts +171 -13
- package/src/git-collaboration.ts +2 -0
- package/src/interaction-contracts.ts +459 -0
- package/src/task-executor.ts +482 -33
- package/src/types.ts +96 -0
- package/src/ui/app.js +313 -8
- package/src/ui/style.css +152 -0
- package/src/worker/http-handler.ts +15 -7
- package/src/worker/prompt-injector.ts +2 -0
- package/src/worker/skill-installer.ts +13 -0
- package/src/worker/tools.ts +172 -3
- package/src/worker/worker-service.ts +9 -6
package/cli.mjs
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
|
+
import fsSync from "node:fs";
|
|
4
5
|
import fs from "node:fs/promises";
|
|
6
|
+
import http from "node:http";
|
|
7
|
+
import { createRequire } from "node:module";
|
|
5
8
|
import os from "node:os";
|
|
6
9
|
import path from "node:path";
|
|
7
10
|
import process from "node:process";
|
|
8
11
|
import { createInterface } from "node:readline/promises";
|
|
9
12
|
import JSON5 from "json5";
|
|
10
13
|
|
|
11
|
-
const
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
const packageMetadata = require("./package.json");
|
|
16
|
+
const PACKAGE_ROOT = path.dirname(require.resolve("./package.json"));
|
|
17
|
+
const PACKAGE_NAME = packageMetadata.name;
|
|
18
|
+
const PACKAGE_VERSION = packageMetadata.version;
|
|
19
|
+
const PACKAGE_INSTALL_SPEC = `${PACKAGE_NAME}@${PACKAGE_VERSION}`;
|
|
12
20
|
const PLUGIN_ID = "teamclaw";
|
|
13
21
|
const DEFAULT_TEAMCLAW_IMAGE = "ghcr.io/topcheer/teamclaw-openclaw:latest";
|
|
14
22
|
const DEFAULT_CONTROLLER_PORT = 9527;
|
|
@@ -18,7 +26,7 @@ const DEFAULT_TEAM_NAME = "default";
|
|
|
18
26
|
const DEFAULT_TASK_TIMEOUT_MS = 1_800_000;
|
|
19
27
|
const DEFAULT_AGENT_TIMEOUT_SECONDS = 2_400;
|
|
20
28
|
const DEFAULT_LOCAL_ROLES = ["architect", "developer", "qa"];
|
|
21
|
-
const
|
|
29
|
+
const LEGACY_DEFAULT_PROVISIONING_ROLES = ["architect", "developer", "qa"];
|
|
22
30
|
|
|
23
31
|
const ROLE_OPTIONS = [
|
|
24
32
|
{ value: "pm", label: "Product Manager" },
|
|
@@ -182,6 +190,30 @@ function resolveDefaultOpenClawWorkspaceDir(env = process.env) {
|
|
|
182
190
|
return path.join(resolveDefaultOpenClawStateDir(env), "workspace");
|
|
183
191
|
}
|
|
184
192
|
|
|
193
|
+
function resolveOpenClawStateDirForConfigPath(configPath) {
|
|
194
|
+
return path.dirname(path.resolve(configPath));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function resolveOpenClawWorkspaceDirForConfigPath(configPath) {
|
|
198
|
+
return path.join(resolveOpenClawStateDirForConfigPath(configPath), "workspace");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function sanitizeInstallerPathSegment(value) {
|
|
202
|
+
const normalized = String(value || "")
|
|
203
|
+
.toLowerCase()
|
|
204
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
205
|
+
.replace(/^-+|-+$/g, "");
|
|
206
|
+
return normalized || "default";
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function resolveDefaultTeamClawWorkspaceDir(configPath, teamName) {
|
|
210
|
+
return path.join(
|
|
211
|
+
resolveOpenClawStateDirForConfigPath(configPath),
|
|
212
|
+
"teamclaw-workspaces",
|
|
213
|
+
sanitizeInstallerPathSegment(teamName),
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
185
217
|
async function pathExists(targetPath) {
|
|
186
218
|
try {
|
|
187
219
|
await fs.access(targetPath);
|
|
@@ -302,10 +334,40 @@ function getCurrentWorkspacePath(config) {
|
|
|
302
334
|
return typeof defaults.workspace === "string" ? expandUserPath(defaults.workspace) : "";
|
|
303
335
|
}
|
|
304
336
|
|
|
337
|
+
function resolveInstallerWorkspaceDefault(configPath, config, teamName) {
|
|
338
|
+
const currentWorkspacePath = getCurrentWorkspacePath(config);
|
|
339
|
+
const sharedWorkspacePath = resolveOpenClawWorkspaceDirForConfigPath(configPath);
|
|
340
|
+
if (currentWorkspacePath && path.resolve(currentWorkspacePath) !== path.resolve(sharedWorkspacePath)) {
|
|
341
|
+
return currentWorkspacePath;
|
|
342
|
+
}
|
|
343
|
+
return resolveDefaultTeamClawWorkspaceDir(configPath, teamName);
|
|
344
|
+
}
|
|
345
|
+
|
|
305
346
|
function dedupeStrings(values) {
|
|
306
347
|
return Array.from(new Set(values.filter((value) => typeof value === "string" && value.trim()).map((value) => value.trim())));
|
|
307
348
|
}
|
|
308
349
|
|
|
350
|
+
function hasSameStringSet(left, right) {
|
|
351
|
+
const normalizedLeft = dedupeStrings(left).slice().sort();
|
|
352
|
+
const normalizedRight = dedupeStrings(right).slice().sort();
|
|
353
|
+
if (normalizedLeft.length !== normalizedRight.length) {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
return normalizedLeft.every((value, index) => value === normalizedRight[index]);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function normalizeConfiguredRoleList(raw) {
|
|
360
|
+
return Array.isArray(raw) ? dedupeStrings(raw) : [];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function resolveDefaultProvisioningRoles(existingTeamClaw) {
|
|
364
|
+
const existingRoles = normalizeConfiguredRoleList(existingTeamClaw.workerProvisioningRoles);
|
|
365
|
+
if (existingRoles.length === 0) {
|
|
366
|
+
return [];
|
|
367
|
+
}
|
|
368
|
+
return hasSameStringSet(existingRoles, LEGACY_DEFAULT_PROVISIONING_ROLES) ? [] : existingRoles;
|
|
369
|
+
}
|
|
370
|
+
|
|
309
371
|
function extractModelOptions(config) {
|
|
310
372
|
const currentModel = getCurrentModel(config);
|
|
311
373
|
const models = [];
|
|
@@ -512,6 +574,28 @@ async function promptRoleList(prompter, message, defaultRoles) {
|
|
|
512
574
|
return parseRoleList(raw).values;
|
|
513
575
|
}
|
|
514
576
|
|
|
577
|
+
async function promptOptionalRoleList(prompter, message, defaultRoles) {
|
|
578
|
+
const defaultValue = defaultRoles.join(",");
|
|
579
|
+
if (!prompter.yes) {
|
|
580
|
+
console.log(
|
|
581
|
+
`Available roles: ${ROLE_OPTIONS.map((option) => `${option.value} (${option.label})`).join(", ")}. These are preferred defaults only; task-required roles can still launch automatically.`,
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
const raw = await prompter.text({
|
|
585
|
+
message,
|
|
586
|
+
defaultValue,
|
|
587
|
+
allowEmpty: true,
|
|
588
|
+
validate: (value) => {
|
|
589
|
+
const parsed = parseRoleList(value);
|
|
590
|
+
if (parsed.invalid.length > 0) {
|
|
591
|
+
return `Unknown role ids: ${parsed.invalid.join(", ")}`;
|
|
592
|
+
}
|
|
593
|
+
return "";
|
|
594
|
+
},
|
|
595
|
+
});
|
|
596
|
+
return parseRoleList(raw).values;
|
|
597
|
+
}
|
|
598
|
+
|
|
515
599
|
function buildStartCommand(configPath) {
|
|
516
600
|
const defaultPath = resolveDefaultOpenClawConfigPath();
|
|
517
601
|
if (path.resolve(configPath) === path.resolve(defaultPath)) {
|
|
@@ -527,6 +611,57 @@ function shellEscape(value) {
|
|
|
527
611
|
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
528
612
|
}
|
|
529
613
|
|
|
614
|
+
function isControllerInstallMode(installMode) {
|
|
615
|
+
return installMode !== "worker";
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function isOnDemandControllerInstallMode(installMode) {
|
|
619
|
+
return installMode === "controller-process" || installMode === "controller-docker" || installMode === "controller-kubernetes";
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function describeProvisioningRoles(roles) {
|
|
623
|
+
return Array.isArray(roles) && roles.length > 0
|
|
624
|
+
? `${roles.join(", ")} (plus any task-required roles)`
|
|
625
|
+
: "all TeamClaw roles (controller decides at runtime)";
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function getLocalUiUrl(port) {
|
|
629
|
+
return `http://127.0.0.1:${port}/ui`;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function rankLanAddress(address) {
|
|
633
|
+
if (address.startsWith("192.168.")) {
|
|
634
|
+
return 0;
|
|
635
|
+
}
|
|
636
|
+
if (address.startsWith("10.")) {
|
|
637
|
+
return 1;
|
|
638
|
+
}
|
|
639
|
+
const parts = address.split(".").map((value) => Number.parseInt(value, 10));
|
|
640
|
+
if (parts.length === 4 && parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) {
|
|
641
|
+
return 2;
|
|
642
|
+
}
|
|
643
|
+
return 3;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function listLanUiUrls(port) {
|
|
647
|
+
const urls = [];
|
|
648
|
+
const interfaces = os.networkInterfaces();
|
|
649
|
+
for (const records of Object.values(interfaces)) {
|
|
650
|
+
for (const record of records ?? []) {
|
|
651
|
+
if (!record || record.internal || record.family !== "IPv4") {
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
urls.push({
|
|
655
|
+
address: record.address,
|
|
656
|
+
url: `http://${record.address}:${port}/ui`,
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return urls
|
|
661
|
+
.sort((left, right) => rankLanAddress(left.address) - rankLanAddress(right.address) || left.address.localeCompare(right.address))
|
|
662
|
+
.map((entry) => entry.url);
|
|
663
|
+
}
|
|
664
|
+
|
|
530
665
|
function installPluginWithCommand(command, args, env) {
|
|
531
666
|
const result = spawnSync(command, args, {
|
|
532
667
|
stdio: "inherit",
|
|
@@ -539,7 +674,96 @@ function installPluginWithCommand(command, args, env) {
|
|
|
539
674
|
};
|
|
540
675
|
}
|
|
541
676
|
|
|
542
|
-
function
|
|
677
|
+
function runGatewayCommand(command, args, env) {
|
|
678
|
+
const result = spawnSync(command, args, {
|
|
679
|
+
env,
|
|
680
|
+
encoding: "utf8",
|
|
681
|
+
});
|
|
682
|
+
return {
|
|
683
|
+
status: result.status ?? 1,
|
|
684
|
+
signal: result.signal ?? null,
|
|
685
|
+
error: result.error ?? null,
|
|
686
|
+
stdout: result.stdout ?? "",
|
|
687
|
+
stderr: result.stderr ?? "",
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function readJsonIfExists(filePath) {
|
|
692
|
+
try {
|
|
693
|
+
if (!fsSync.existsSync(filePath)) {
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
return JSON.parse(fsSync.readFileSync(filePath, "utf8"));
|
|
697
|
+
} catch {
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function inspectInstalledPlugin(configPath) {
|
|
703
|
+
const stateDir = path.dirname(path.resolve(configPath));
|
|
704
|
+
const pluginDir = path.join(stateDir, "extensions", PLUGIN_ID);
|
|
705
|
+
if (!fsSync.existsSync(pluginDir)) {
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
const manifest = readJsonIfExists(path.join(pluginDir, "openclaw.plugin.json"));
|
|
709
|
+
const packageJson = readJsonIfExists(path.join(pluginDir, "package.json"));
|
|
710
|
+
const version = typeof manifest?.version === "string" && manifest.version.trim()
|
|
711
|
+
? manifest.version.trim()
|
|
712
|
+
: typeof packageJson?.version === "string" && packageJson.version.trim()
|
|
713
|
+
? packageJson.version.trim()
|
|
714
|
+
: "";
|
|
715
|
+
return {
|
|
716
|
+
pluginDir,
|
|
717
|
+
version: version || null,
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function createPackageTarball(env) {
|
|
722
|
+
let tempDir = "";
|
|
723
|
+
try {
|
|
724
|
+
tempDir = fsSync.mkdtempSync(path.join(os.tmpdir(), "teamclaw-installer-pack-"));
|
|
725
|
+
const result = spawnSync(
|
|
726
|
+
"npm",
|
|
727
|
+
["pack", PACKAGE_ROOT, "--pack-destination", tempDir, "--json", "--ignore-scripts"],
|
|
728
|
+
{
|
|
729
|
+
env,
|
|
730
|
+
encoding: "utf8",
|
|
731
|
+
},
|
|
732
|
+
);
|
|
733
|
+
if (result.status !== 0 || result.error) {
|
|
734
|
+
const detail = result.error
|
|
735
|
+
? result.error.message
|
|
736
|
+
: (result.stderr || result.stdout || `exited with code ${result.status}`).trim();
|
|
737
|
+
throw new Error(detail || "npm pack failed");
|
|
738
|
+
}
|
|
739
|
+
const payload = JSON.parse(result.stdout);
|
|
740
|
+
const filename = Array.isArray(payload) && payload[0] && typeof payload[0].filename === "string"
|
|
741
|
+
? payload[0].filename.trim()
|
|
742
|
+
: "";
|
|
743
|
+
if (!filename) {
|
|
744
|
+
throw new Error("npm pack did not report a tarball filename");
|
|
745
|
+
}
|
|
746
|
+
const tarballPath = path.join(tempDir, filename);
|
|
747
|
+
if (!fsSync.existsSync(tarballPath)) {
|
|
748
|
+
throw new Error(`tarball was not created at ${tarballPath}`);
|
|
749
|
+
}
|
|
750
|
+
return {
|
|
751
|
+
ok: true,
|
|
752
|
+
tempDir,
|
|
753
|
+
tarballPath,
|
|
754
|
+
};
|
|
755
|
+
} catch (error) {
|
|
756
|
+
if (tempDir) {
|
|
757
|
+
fsSync.rmSync(tempDir, { recursive: true, force: true });
|
|
758
|
+
}
|
|
759
|
+
return {
|
|
760
|
+
ok: false,
|
|
761
|
+
error: error instanceof Error ? error.message : String(error),
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function attemptPluginUninstall({ configPath }) {
|
|
543
767
|
const env = {
|
|
544
768
|
...process.env,
|
|
545
769
|
OPENCLAW_CONFIG_PATH: configPath,
|
|
@@ -548,18 +772,18 @@ function attemptPluginInstall({ configPath }) {
|
|
|
548
772
|
{
|
|
549
773
|
label: "openclaw",
|
|
550
774
|
command: "openclaw",
|
|
551
|
-
args: ["plugins", "
|
|
775
|
+
args: ["plugins", "uninstall", PLUGIN_ID, "--force"],
|
|
552
776
|
},
|
|
553
777
|
{
|
|
554
778
|
label: "npm exec fallback",
|
|
555
779
|
command: "npm",
|
|
556
|
-
args: ["exec", "-y", "openclaw@latest", "--", "plugins", "
|
|
780
|
+
args: ["exec", "-y", "openclaw@latest", "--", "plugins", "uninstall", PLUGIN_ID, "--force"],
|
|
557
781
|
},
|
|
558
782
|
];
|
|
559
|
-
|
|
783
|
+
const failures = [];
|
|
560
784
|
for (let index = 0; index < candidates.length; index += 1) {
|
|
561
785
|
const candidate = candidates[index];
|
|
562
|
-
console.log(`\
|
|
786
|
+
console.log(`\nRemoving existing ${PLUGIN_ID} plugin with ${candidate.label}...`);
|
|
563
787
|
const result = installPluginWithCommand(candidate.command, candidate.args, env);
|
|
564
788
|
if (result.status === 0 && !result.error) {
|
|
565
789
|
return {
|
|
@@ -568,31 +792,252 @@ function attemptPluginInstall({ configPath }) {
|
|
|
568
792
|
};
|
|
569
793
|
}
|
|
570
794
|
const errorCode = result.error && typeof result.error === "object" ? result.error.code : "";
|
|
571
|
-
if (errorCode === "ENOENT" && index < candidates.length - 1) {
|
|
572
|
-
console.log(`${candidate.command} was not found. Trying the npm exec fallback...`);
|
|
573
|
-
continue;
|
|
574
|
-
}
|
|
575
795
|
const detail = result.error
|
|
576
796
|
? result.error.message
|
|
577
797
|
: result.signal
|
|
578
798
|
? `terminated by signal ${result.signal}`
|
|
579
799
|
: `exited with code ${result.status}`;
|
|
800
|
+
failures.push(`${candidate.label} failed: ${detail}`);
|
|
801
|
+
if (errorCode === "ENOENT" && index < candidates.length - 1) {
|
|
802
|
+
console.log(`${candidate.command} was not found. Trying the next uninstall fallback...`);
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
break;
|
|
806
|
+
}
|
|
807
|
+
return {
|
|
808
|
+
ok: false,
|
|
809
|
+
error: failures.join("; "),
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function attemptPluginInstall({ configPath }) {
|
|
814
|
+
const env = {
|
|
815
|
+
...process.env,
|
|
816
|
+
OPENCLAW_CONFIG_PATH: configPath,
|
|
817
|
+
};
|
|
818
|
+
const installedPlugin = inspectInstalledPlugin(configPath);
|
|
819
|
+
if (installedPlugin?.version === PACKAGE_VERSION) {
|
|
820
|
+
console.log(
|
|
821
|
+
`\nFound existing TeamClaw plugin at ${installedPlugin.pluginDir} (version ${installedPlugin.version}). Skipping plugin reinstall.`,
|
|
822
|
+
);
|
|
823
|
+
return {
|
|
824
|
+
ok: true,
|
|
825
|
+
method: `already installed (${installedPlugin.version})`,
|
|
826
|
+
skipped: true,
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
if (installedPlugin) {
|
|
830
|
+
const installedVersion = installedPlugin.version ? `version ${installedPlugin.version}` : "an unknown version";
|
|
831
|
+
console.log(
|
|
832
|
+
`\nFound existing TeamClaw plugin at ${installedPlugin.pluginDir} (${installedVersion}). Removing it before install...`,
|
|
833
|
+
);
|
|
834
|
+
const uninstallResult = attemptPluginUninstall({ configPath });
|
|
835
|
+
if (!uninstallResult.ok) {
|
|
836
|
+
return {
|
|
837
|
+
ok: false,
|
|
838
|
+
error: `Could not remove existing TeamClaw plugin at ${installedPlugin.pluginDir}: ${uninstallResult.error}`,
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
const candidates = [];
|
|
843
|
+
const tarballResult = createPackageTarball(env);
|
|
844
|
+
if (tarballResult.ok) {
|
|
845
|
+
console.log(
|
|
846
|
+
`\nPacked ${PACKAGE_INSTALL_SPEC} into ${path.basename(tarballResult.tarballPath)} for local plugin install.`,
|
|
847
|
+
);
|
|
848
|
+
candidates.push(
|
|
849
|
+
{
|
|
850
|
+
label: "openclaw (local tarball)",
|
|
851
|
+
command: "openclaw",
|
|
852
|
+
args: ["plugins", "install", tarballResult.tarballPath],
|
|
853
|
+
targetDescription: tarballResult.tarballPath,
|
|
854
|
+
},
|
|
855
|
+
{
|
|
856
|
+
label: "npm exec fallback (local tarball)",
|
|
857
|
+
command: "npm",
|
|
858
|
+
args: ["exec", "-y", "openclaw@latest", "--", "plugins", "install", tarballResult.tarballPath],
|
|
859
|
+
targetDescription: tarballResult.tarballPath,
|
|
860
|
+
},
|
|
861
|
+
);
|
|
862
|
+
} else {
|
|
863
|
+
console.log(
|
|
864
|
+
`\nCould not pack ${PACKAGE_INSTALL_SPEC} into a local tarball (${tarballResult.error}). Falling back to registry install...`,
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
candidates.push(
|
|
868
|
+
{
|
|
869
|
+
label: "openclaw (exact version fallback)",
|
|
870
|
+
command: "openclaw",
|
|
871
|
+
args: ["plugins", "install", PACKAGE_INSTALL_SPEC],
|
|
872
|
+
targetDescription: PACKAGE_INSTALL_SPEC,
|
|
873
|
+
},
|
|
874
|
+
{
|
|
875
|
+
label: "npm exec fallback (exact version fallback)",
|
|
876
|
+
command: "npm",
|
|
877
|
+
args: ["exec", "-y", "openclaw@latest", "--", "plugins", "install", PACKAGE_INSTALL_SPEC],
|
|
878
|
+
targetDescription: PACKAGE_INSTALL_SPEC,
|
|
879
|
+
},
|
|
880
|
+
);
|
|
881
|
+
|
|
882
|
+
try {
|
|
883
|
+
const failures = [];
|
|
884
|
+
for (let index = 0; index < candidates.length; index += 1) {
|
|
885
|
+
const candidate = candidates[index];
|
|
886
|
+
console.log(`\nInstalling ${candidate.targetDescription} with ${candidate.label}...`);
|
|
887
|
+
const result = installPluginWithCommand(candidate.command, candidate.args, env);
|
|
888
|
+
if (result.status === 0 && !result.error) {
|
|
889
|
+
return {
|
|
890
|
+
ok: true,
|
|
891
|
+
method: candidate.label,
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
const errorCode = result.error && typeof result.error === "object" ? result.error.code : "";
|
|
895
|
+
const detail = result.error
|
|
896
|
+
? result.error.message
|
|
897
|
+
: result.signal
|
|
898
|
+
? `terminated by signal ${result.signal}`
|
|
899
|
+
: `exited with code ${result.status}`;
|
|
900
|
+
failures.push(`${candidate.label} failed: ${detail}`);
|
|
901
|
+
if (errorCode === "ENOENT" && index < candidates.length - 1) {
|
|
902
|
+
console.log(`${candidate.command} was not found. Trying the next install fallback...`);
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
if (index < candidates.length - 1) {
|
|
906
|
+
console.log(`${candidate.label} failed (${detail}). Trying the next install fallback...`);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
580
909
|
return {
|
|
581
910
|
ok: false,
|
|
582
|
-
error:
|
|
911
|
+
error: failures.length > 0 ? failures.join("; ") : "No install command was available.",
|
|
583
912
|
};
|
|
913
|
+
} finally {
|
|
914
|
+
if (tarballResult.ok) {
|
|
915
|
+
fsSync.rmSync(tarballResult.tempDir, { recursive: true, force: true });
|
|
916
|
+
}
|
|
584
917
|
}
|
|
918
|
+
}
|
|
585
919
|
|
|
920
|
+
function attemptGatewayRestart({ configPath }) {
|
|
921
|
+
const env = {
|
|
922
|
+
...process.env,
|
|
923
|
+
OPENCLAW_CONFIG_PATH: configPath,
|
|
924
|
+
};
|
|
925
|
+
const candidates = [
|
|
926
|
+
{
|
|
927
|
+
label: "openclaw",
|
|
928
|
+
command: "openclaw",
|
|
929
|
+
args: ["gateway", "restart"],
|
|
930
|
+
},
|
|
931
|
+
{
|
|
932
|
+
label: "npm exec fallback",
|
|
933
|
+
command: "npm",
|
|
934
|
+
args: ["exec", "-y", "openclaw@latest", "--", "gateway", "restart"],
|
|
935
|
+
},
|
|
936
|
+
];
|
|
937
|
+
const failures = [];
|
|
938
|
+
for (let index = 0; index < candidates.length; index += 1) {
|
|
939
|
+
const candidate = candidates[index];
|
|
940
|
+
const result = runGatewayCommand(candidate.command, candidate.args, env);
|
|
941
|
+
if (result.status === 0 && !result.error) {
|
|
942
|
+
return {
|
|
943
|
+
ok: true,
|
|
944
|
+
method: candidate.label,
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
const detail = result.error
|
|
948
|
+
? result.error.message
|
|
949
|
+
: (result.stderr || result.stdout || (result.signal
|
|
950
|
+
? `terminated by signal ${result.signal}`
|
|
951
|
+
: `exited with code ${result.status}`)).trim();
|
|
952
|
+
failures.push(`${candidate.label} failed: ${detail}`);
|
|
953
|
+
const errorCode = result.error && typeof result.error === "object" ? result.error.code : "";
|
|
954
|
+
if (!(errorCode === "ENOENT" && index < candidates.length - 1)) {
|
|
955
|
+
break;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
586
958
|
return {
|
|
587
959
|
ok: false,
|
|
588
|
-
error: "
|
|
960
|
+
error: failures.join("; "),
|
|
589
961
|
};
|
|
590
962
|
}
|
|
591
963
|
|
|
592
|
-
async function
|
|
964
|
+
async function waitForControllerHealth(port) {
|
|
965
|
+
const url = `http://127.0.0.1:${port}/api/v1/health`;
|
|
966
|
+
const deadline = Date.now() + 30_000;
|
|
967
|
+
let lastError = "";
|
|
968
|
+
while (Date.now() < deadline) {
|
|
969
|
+
try {
|
|
970
|
+
const response = await new Promise((resolve, reject) => {
|
|
971
|
+
const request = http.get(
|
|
972
|
+
url,
|
|
973
|
+
{
|
|
974
|
+
agent: false,
|
|
975
|
+
headers: {
|
|
976
|
+
Connection: "close",
|
|
977
|
+
},
|
|
978
|
+
},
|
|
979
|
+
(incoming) => {
|
|
980
|
+
let body = "";
|
|
981
|
+
incoming.setEncoding("utf8");
|
|
982
|
+
incoming.on("data", (chunk) => {
|
|
983
|
+
body += chunk;
|
|
984
|
+
});
|
|
985
|
+
incoming.on("end", () => {
|
|
986
|
+
resolve({
|
|
987
|
+
statusCode: incoming.statusCode ?? 0,
|
|
988
|
+
body,
|
|
989
|
+
});
|
|
990
|
+
});
|
|
991
|
+
},
|
|
992
|
+
);
|
|
993
|
+
request.setTimeout(5_000, () => {
|
|
994
|
+
request.destroy(new Error("request timed out"));
|
|
995
|
+
});
|
|
996
|
+
request.on("error", reject);
|
|
997
|
+
});
|
|
998
|
+
if (response.statusCode >= 200 && response.statusCode < 300) {
|
|
999
|
+
const payload = JSON.parse(response.body);
|
|
1000
|
+
if (payload && payload.status === "ok" && payload.mode === "controller") {
|
|
1001
|
+
return {
|
|
1002
|
+
ok: true,
|
|
1003
|
+
url,
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
lastError = "unexpected health payload";
|
|
1007
|
+
} else {
|
|
1008
|
+
lastError = `HTTP ${response.statusCode}`;
|
|
1009
|
+
}
|
|
1010
|
+
} catch (error) {
|
|
1011
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
1012
|
+
}
|
|
1013
|
+
await new Promise((resolve) => setTimeout(resolve, 1_000));
|
|
1014
|
+
}
|
|
1015
|
+
return {
|
|
1016
|
+
ok: false,
|
|
1017
|
+
url,
|
|
1018
|
+
error: lastError || "timed out after 30s",
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
async function collectInstallChoices(configPath, config, prompter) {
|
|
593
1023
|
const existingTeamClaw = getExistingTeamClawConfig(config);
|
|
594
1024
|
const existingMode = typeof existingTeamClaw.mode === "string" ? existingTeamClaw.mode.trim() : "";
|
|
595
|
-
const
|
|
1025
|
+
const existingProvisioningType =
|
|
1026
|
+
typeof existingTeamClaw.workerProvisioningType === "string" ? existingTeamClaw.workerProvisioningType.trim() : "";
|
|
1027
|
+
let modeDefault = "single-local";
|
|
1028
|
+
if (existingMode === "worker") {
|
|
1029
|
+
modeDefault = "worker";
|
|
1030
|
+
} else if (existingMode === "controller") {
|
|
1031
|
+
if (existingProvisioningType === "docker") {
|
|
1032
|
+
modeDefault = "controller-docker";
|
|
1033
|
+
} else if (existingProvisioningType === "kubernetes") {
|
|
1034
|
+
modeDefault = "controller-kubernetes";
|
|
1035
|
+
} else if (existingProvisioningType === "process") {
|
|
1036
|
+
modeDefault = "controller-process";
|
|
1037
|
+
} else if (!Array.isArray(existingTeamClaw.localRoles) || existingTeamClaw.localRoles.length === 0) {
|
|
1038
|
+
modeDefault = "controller-manual";
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
596
1041
|
|
|
597
1042
|
const installMode = await prompter.select({
|
|
598
1043
|
message: "Choose an installation mode",
|
|
@@ -625,7 +1070,7 @@ async function collectInstallChoices(config, prompter) {
|
|
|
625
1070
|
});
|
|
626
1071
|
const workspacePath = expandUserPath(await prompter.text({
|
|
627
1072
|
message: "OpenClaw workspace directory",
|
|
628
|
-
defaultValue:
|
|
1073
|
+
defaultValue: resolveInstallerWorkspaceDefault(configPath, config, teamName),
|
|
629
1074
|
}));
|
|
630
1075
|
|
|
631
1076
|
if (installMode === "worker") {
|
|
@@ -705,12 +1150,10 @@ async function collectInstallChoices(config, prompter) {
|
|
|
705
1150
|
};
|
|
706
1151
|
}
|
|
707
1152
|
|
|
708
|
-
const provisioningRoles = await
|
|
1153
|
+
const provisioningRoles = await promptOptionalRoleList(
|
|
709
1154
|
prompter,
|
|
710
|
-
"
|
|
711
|
-
|
|
712
|
-
? existingTeamClaw.workerProvisioningRoles
|
|
713
|
-
: DEFAULT_PROVISIONING_ROLES,
|
|
1155
|
+
"Preferred on-demand roles (comma-separated, leave empty for controller-decided defaults)",
|
|
1156
|
+
resolveDefaultProvisioningRoles(existingTeamClaw),
|
|
714
1157
|
);
|
|
715
1158
|
const maxPerRole = await prompter.number({
|
|
716
1159
|
message: "Maximum on-demand workers per role",
|
|
@@ -753,11 +1196,11 @@ async function collectInstallChoices(config, prompter) {
|
|
|
753
1196
|
: DEFAULT_TEAMCLAW_IMAGE,
|
|
754
1197
|
});
|
|
755
1198
|
const dockerWorkspaceVolume = await prompter.text({
|
|
756
|
-
message: "Docker workspace volume or host path (leave empty for ephemeral workspaces)",
|
|
1199
|
+
message: "Docker workspace volume or host path (leave empty for isolated ephemeral workspaces)",
|
|
757
1200
|
defaultValue:
|
|
758
1201
|
typeof existingTeamClaw.workerProvisioningDockerWorkspaceVolume === "string"
|
|
759
1202
|
? existingTeamClaw.workerProvisioningDockerWorkspaceVolume.trim()
|
|
760
|
-
: "
|
|
1203
|
+
: "",
|
|
761
1204
|
allowEmpty: true,
|
|
762
1205
|
});
|
|
763
1206
|
return {
|
|
@@ -808,7 +1251,7 @@ async function collectInstallChoices(config, prompter) {
|
|
|
808
1251
|
: "teamclaw-worker",
|
|
809
1252
|
});
|
|
810
1253
|
const kubernetesWorkspacePersistentVolumeClaim = await prompter.text({
|
|
811
|
-
message: "Kubernetes workspace PVC (leave empty for ephemeral workspaces)",
|
|
1254
|
+
message: "Kubernetes workspace PVC (leave empty for isolated ephemeral workspaces)",
|
|
812
1255
|
defaultValue:
|
|
813
1256
|
typeof existingTeamClaw.workerProvisioningKubernetesWorkspacePersistentVolumeClaim === "string"
|
|
814
1257
|
? existingTeamClaw.workerProvisioningKubernetesWorkspacePersistentVolumeClaim.trim()
|
|
@@ -1048,19 +1491,38 @@ function buildSummaryLines(params) {
|
|
|
1048
1491
|
}
|
|
1049
1492
|
if (params.pluginInstallStatus === "installed") {
|
|
1050
1493
|
lines.push(`Plugin install: completed via ${params.pluginInstallMethod}`);
|
|
1494
|
+
} else if (params.pluginInstallStatus === "already-installed") {
|
|
1495
|
+
lines.push(`Plugin install: ${params.pluginInstallMethod}`);
|
|
1051
1496
|
} else if (params.pluginInstallStatus === "skipped") {
|
|
1052
1497
|
lines.push("Plugin install: skipped");
|
|
1053
1498
|
} else if (params.pluginInstallError) {
|
|
1054
1499
|
lines.push(`Plugin install: ${params.pluginInstallError}`);
|
|
1055
1500
|
}
|
|
1501
|
+
if (params.gatewayRestartStatus === "restarted") {
|
|
1502
|
+
lines.push(`Gateway restart: completed via ${params.gatewayRestartMethod}`);
|
|
1503
|
+
} else if (params.gatewayRestartStatus === "failed") {
|
|
1504
|
+
lines.push(`Gateway restart: ${params.gatewayRestartError}`);
|
|
1505
|
+
}
|
|
1506
|
+
if (params.controllerHealthStatus === "ok") {
|
|
1507
|
+
lines.push(`Controller health: ok (${params.controllerHealthUrl})`);
|
|
1508
|
+
} else if (params.controllerHealthStatus === "failed") {
|
|
1509
|
+
lines.push(`Controller health: ${params.controllerHealthError} (${params.controllerHealthUrl})`);
|
|
1510
|
+
}
|
|
1056
1511
|
lines.push(`Start command: ${buildStartCommand(params.configPath)}`);
|
|
1057
1512
|
|
|
1058
|
-
if (params.choices.installMode
|
|
1059
|
-
|
|
1513
|
+
if (isControllerInstallMode(params.choices.installMode)) {
|
|
1514
|
+
const lanUiUrls = listLanUiUrls(params.choices.controllerPort);
|
|
1515
|
+
if (lanUiUrls.length > 0) {
|
|
1516
|
+
lines.push(`Open UI (LAN): ${lanUiUrls[0]}`);
|
|
1517
|
+
}
|
|
1518
|
+
lines.push(`Open UI (local): ${getLocalUiUrl(params.choices.controllerPort)}`);
|
|
1060
1519
|
}
|
|
1061
1520
|
if (params.choices.installMode === "controller-docker" || params.choices.installMode === "controller-kubernetes") {
|
|
1062
1521
|
lines.push(`Provisioning image: ${params.choices.workerImage}`);
|
|
1063
1522
|
}
|
|
1523
|
+
if (isOnDemandControllerInstallMode(params.choices.installMode)) {
|
|
1524
|
+
lines.push(`On-demand roles: ${describeProvisioningRoles(params.choices.provisioningRoles)}`);
|
|
1525
|
+
}
|
|
1064
1526
|
if (params.choices.installMode === "controller-docker" && params.choices.dockerWorkspaceVolume) {
|
|
1065
1527
|
lines.push(`Docker workspace volume: ${params.choices.dockerWorkspaceVolume}`);
|
|
1066
1528
|
}
|
|
@@ -1112,7 +1574,7 @@ async function runInstall(options) {
|
|
|
1112
1574
|
if (!options.skipPluginInstall && !options.dryRun) {
|
|
1113
1575
|
const installResult = attemptPluginInstall({ configPath });
|
|
1114
1576
|
if (installResult.ok) {
|
|
1115
|
-
pluginInstallStatus = "installed";
|
|
1577
|
+
pluginInstallStatus = installResult.skipped ? "already-installed" : "installed";
|
|
1116
1578
|
pluginInstallMethod = installResult.method;
|
|
1117
1579
|
} else {
|
|
1118
1580
|
pluginInstallStatus = "failed";
|
|
@@ -1129,7 +1591,7 @@ async function runInstall(options) {
|
|
|
1129
1591
|
}
|
|
1130
1592
|
|
|
1131
1593
|
const config = await readOpenClawConfig(configPath);
|
|
1132
|
-
const choices = await collectInstallChoices(config, prompter);
|
|
1594
|
+
const choices = await collectInstallChoices(configPath, config, prompter);
|
|
1133
1595
|
const nextConfig = applyInstallerChoices(config, choices);
|
|
1134
1596
|
|
|
1135
1597
|
if (options.dryRun) {
|
|
@@ -1138,6 +1600,29 @@ async function runInstall(options) {
|
|
|
1138
1600
|
await writeConfig(configPath, nextConfig);
|
|
1139
1601
|
}
|
|
1140
1602
|
|
|
1603
|
+
let gatewayRestartStatus = "skipped";
|
|
1604
|
+
let gatewayRestartMethod = "";
|
|
1605
|
+
let gatewayRestartError = "";
|
|
1606
|
+
let controllerHealthStatus = "skipped";
|
|
1607
|
+
let controllerHealthUrl = "";
|
|
1608
|
+
let controllerHealthError = "";
|
|
1609
|
+
if (!options.dryRun) {
|
|
1610
|
+
const restartResult = attemptGatewayRestart({ configPath });
|
|
1611
|
+
if (restartResult.ok) {
|
|
1612
|
+
gatewayRestartStatus = "restarted";
|
|
1613
|
+
gatewayRestartMethod = restartResult.method;
|
|
1614
|
+
if (isControllerInstallMode(choices.installMode)) {
|
|
1615
|
+
const healthResult = await waitForControllerHealth(choices.controllerPort);
|
|
1616
|
+
controllerHealthStatus = healthResult.ok ? "ok" : "failed";
|
|
1617
|
+
controllerHealthUrl = healthResult.url;
|
|
1618
|
+
controllerHealthError = healthResult.error ?? "";
|
|
1619
|
+
}
|
|
1620
|
+
} else {
|
|
1621
|
+
gatewayRestartStatus = "failed";
|
|
1622
|
+
gatewayRestartError = restartResult.error;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1141
1626
|
const summaryLines = buildSummaryLines({
|
|
1142
1627
|
configPath,
|
|
1143
1628
|
choices,
|
|
@@ -1145,6 +1630,12 @@ async function runInstall(options) {
|
|
|
1145
1630
|
pluginInstallStatus,
|
|
1146
1631
|
pluginInstallMethod,
|
|
1147
1632
|
pluginInstallError,
|
|
1633
|
+
gatewayRestartStatus,
|
|
1634
|
+
gatewayRestartMethod,
|
|
1635
|
+
gatewayRestartError,
|
|
1636
|
+
controllerHealthStatus,
|
|
1637
|
+
controllerHealthUrl,
|
|
1638
|
+
controllerHealthError,
|
|
1148
1639
|
});
|
|
1149
1640
|
|
|
1150
1641
|
prompter.note("\nTeamClaw installer summary");
|