soulhubcli 1.0.20 → 1.0.22

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.
Files changed (3) hide show
  1. package/README.md +133 -21
  2. package/dist/index.cjs +627 -119
  3. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -966,7 +966,7 @@ var require_command = __commonJS({
966
966
  var EventEmitter = require("events").EventEmitter;
967
967
  var childProcess = require("child_process");
968
968
  var path5 = require("path");
969
- var fs7 = require("fs");
969
+ var fs8 = require("fs");
970
970
  var process3 = require("process");
971
971
  var { Argument: Argument2, humanReadableArgName } = require_argument();
972
972
  var { CommanderError: CommanderError2 } = require_error();
@@ -1899,10 +1899,10 @@ Expecting one of '${allowedValues.join("', '")}'`);
1899
1899
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
1900
1900
  function findFile(baseDir, baseName) {
1901
1901
  const localBin = path5.resolve(baseDir, baseName);
1902
- if (fs7.existsSync(localBin)) return localBin;
1902
+ if (fs8.existsSync(localBin)) return localBin;
1903
1903
  if (sourceExt.includes(path5.extname(baseName))) return void 0;
1904
1904
  const foundExt = sourceExt.find(
1905
- (ext) => fs7.existsSync(`${localBin}${ext}`)
1905
+ (ext) => fs8.existsSync(`${localBin}${ext}`)
1906
1906
  );
1907
1907
  if (foundExt) return `${localBin}${foundExt}`;
1908
1908
  return void 0;
@@ -1914,7 +1914,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1914
1914
  if (this._scriptPath) {
1915
1915
  let resolvedScriptPath;
1916
1916
  try {
1917
- resolvedScriptPath = fs7.realpathSync(this._scriptPath);
1917
+ resolvedScriptPath = fs8.realpathSync(this._scriptPath);
1918
1918
  } catch (err) {
1919
1919
  resolvedScriptPath = this._scriptPath;
1920
1920
  }
@@ -19165,6 +19165,11 @@ function recordInstall(name, version, workspace) {
19165
19165
  saveConfig(config);
19166
19166
  logger.debug(`Recorded install`, { name, version, workspace });
19167
19167
  }
19168
+ function removeInstallRecord(name) {
19169
+ const config = loadConfig();
19170
+ config.installed = config.installed.filter((a) => a.name !== name);
19171
+ saveConfig(config);
19172
+ }
19168
19173
  function getWorkspaceDir(clawDir, agentName) {
19169
19174
  return import_node_path11.default.join(clawDir, `workspace-${agentName}`);
19170
19175
  }
@@ -19301,6 +19306,11 @@ function detectPackageKind(dir) {
19301
19306
  }
19302
19307
  return "unknown";
19303
19308
  }
19309
+ function getBackupBaseDir(clawDir) {
19310
+ const home = process.env.HOME || "~";
19311
+ const brand = detectClawBrand(clawDir).toLowerCase();
19312
+ return import_node_path11.default.join(home, ".soulhub", "backups", brand);
19313
+ }
19304
19314
  function backupAgentWorkspace(workspaceDir) {
19305
19315
  if (!import_node_fs8.default.existsSync(workspaceDir)) {
19306
19316
  return null;
@@ -19310,7 +19320,7 @@ function backupAgentWorkspace(workspaceDir) {
19310
19320
  return null;
19311
19321
  }
19312
19322
  const clawDir = import_node_path11.default.dirname(workspaceDir);
19313
- const backupBaseDir = import_node_path11.default.join(clawDir, "agentbackup");
19323
+ const backupBaseDir = getBackupBaseDir(clawDir);
19314
19324
  if (!import_node_fs8.default.existsSync(backupBaseDir)) {
19315
19325
  import_node_fs8.default.mkdirSync(backupBaseDir, { recursive: true });
19316
19326
  }
@@ -19333,7 +19343,7 @@ function moveBackupAgentWorkspace(workspaceDir) {
19333
19343
  return null;
19334
19344
  }
19335
19345
  const clawDir = import_node_path11.default.dirname(workspaceDir);
19336
- const backupBaseDir = import_node_path11.default.join(clawDir, "agentbackup");
19346
+ const backupBaseDir = getBackupBaseDir(clawDir);
19337
19347
  if (!import_node_fs8.default.existsSync(backupBaseDir)) {
19338
19348
  import_node_fs8.default.mkdirSync(backupBaseDir, { recursive: true });
19339
19349
  }
@@ -19343,7 +19353,12 @@ function moveBackupAgentWorkspace(workspaceDir) {
19343
19353
  const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
19344
19354
  backupDir = import_node_path11.default.join(backupBaseDir, `${dirName}-${timestamp2}`);
19345
19355
  }
19346
- import_node_fs8.default.renameSync(workspaceDir, backupDir);
19356
+ try {
19357
+ import_node_fs8.default.renameSync(workspaceDir, backupDir);
19358
+ } catch {
19359
+ import_node_fs8.default.cpSync(workspaceDir, backupDir, { recursive: true });
19360
+ import_node_fs8.default.rmSync(workspaceDir, { recursive: true, force: true });
19361
+ }
19347
19362
  logger.info(`Workspace moved to backup`, { from: workspaceDir, to: backupDir });
19348
19363
  return backupDir;
19349
19364
  }
@@ -19432,6 +19447,106 @@ function commitBackupRecord(record) {
19432
19447
  saveBackupManifest(manifest);
19433
19448
  logger.info(`Backup record saved`, { id: record.id, items: record.items.length, workers: record.installedWorkerIds.length });
19434
19449
  }
19450
+ async function promptSelectRole() {
19451
+ console.log();
19452
+ console.log(" ? Install as:");
19453
+ console.log();
19454
+ console.log(" 1) \u{1F477} Worker agent (\u5B50Agent\uFF0C\u5B89\u88C5\u5230 workspace-<name>/ \u76EE\u5F55)");
19455
+ console.log(" 2) \u{1F451} Main agent (\u4E3BAgent\uFF0C\u5B89\u88C5\u5230 workspace/ \u76EE\u5F55)");
19456
+ console.log();
19457
+ const rl = import_node_readline.default.createInterface({
19458
+ input: process.stdin,
19459
+ output: process.stdout
19460
+ });
19461
+ return new Promise((resolve) => {
19462
+ rl.question(" Please select (1-2) [1]: ", (answer) => {
19463
+ rl.close();
19464
+ const trimmed = answer.trim();
19465
+ if (trimmed === "" || trimmed === "1") {
19466
+ resolve("worker");
19467
+ } else if (trimmed === "2") {
19468
+ resolve("main");
19469
+ } else {
19470
+ console.log(" Invalid selection, operation cancelled.");
19471
+ resolve(null);
19472
+ }
19473
+ });
19474
+ });
19475
+ }
19476
+ async function promptMultiSelectClawDirs() {
19477
+ const dirs = findAllClawDirs();
19478
+ if (dirs.length === 0) {
19479
+ return [];
19480
+ }
19481
+ if (dirs.length === 1) {
19482
+ const brand = detectClawBrand(dirs[0]);
19483
+ console.log();
19484
+ console.log(` \u2714 Detected ${brand}: ${dirs[0]}`);
19485
+ return dirs;
19486
+ }
19487
+ console.log();
19488
+ console.log(" ? Select target Claw installations (multiple allowed):");
19489
+ console.log();
19490
+ dirs.forEach((dir, index) => {
19491
+ const brand = detectClawBrand(dir);
19492
+ console.log(` ${index + 1}) ${brand} ${dir}`);
19493
+ });
19494
+ console.log();
19495
+ const rl = import_node_readline.default.createInterface({
19496
+ input: process.stdin,
19497
+ output: process.stdout
19498
+ });
19499
+ return new Promise((resolve) => {
19500
+ const allNums = dirs.map((_2, i) => String(i + 1)).join(",");
19501
+ rl.question(` Enter numbers separated by comma (e.g. 1,2) [${allNums}]: `, (answer) => {
19502
+ rl.close();
19503
+ const trimmed = answer.trim();
19504
+ if (trimmed === "") {
19505
+ resolve(dirs);
19506
+ return;
19507
+ }
19508
+ const parts = trimmed.split(",").map((s3) => s3.trim());
19509
+ const selected = [];
19510
+ for (const part of parts) {
19511
+ const idx = parseInt(part, 10);
19512
+ if (idx >= 1 && idx <= dirs.length) {
19513
+ const dir = dirs[idx - 1];
19514
+ if (!selected.includes(dir)) {
19515
+ selected.push(dir);
19516
+ }
19517
+ } else {
19518
+ console.log(` Invalid selection: ${part}, operation cancelled.`);
19519
+ resolve([]);
19520
+ return;
19521
+ }
19522
+ }
19523
+ if (selected.length === 0) {
19524
+ console.log(" No claw selected, operation cancelled.");
19525
+ }
19526
+ resolve(selected);
19527
+ });
19528
+ });
19529
+ }
19530
+ async function promptConfirm(message, defaultYes = true) {
19531
+ const rl = import_node_readline.default.createInterface({
19532
+ input: process.stdin,
19533
+ output: process.stdout
19534
+ });
19535
+ const hint = defaultYes ? "Y/n" : "y/N";
19536
+ return new Promise((resolve) => {
19537
+ rl.question(` ${message} (${hint}) `, (answer) => {
19538
+ rl.close();
19539
+ const trimmed = answer.trim().toLowerCase();
19540
+ if (trimmed === "") {
19541
+ resolve(defaultYes);
19542
+ } else if (trimmed === "y" || trimmed === "yes") {
19543
+ resolve(true);
19544
+ } else {
19545
+ resolve(false);
19546
+ }
19547
+ });
19548
+ });
19549
+ }
19435
19550
  var CATEGORY_LABELS = {
19436
19551
  "self-media": "Self Media",
19437
19552
  development: "Development",
@@ -19440,11 +19555,12 @@ var CATEGORY_LABELS = {
19440
19555
  education: "Education",
19441
19556
  dispatcher: "Dispatcher"
19442
19557
  };
19443
- function registerAgentToOpenClaw(agentName, workspaceDir, _clawDir) {
19558
+ function registerAgentToOpenClaw(agentName, workspaceDir, clawDir) {
19444
19559
  const agentId = agentName.toLowerCase().replace(/[\s_]+/g, "-");
19445
- logger.debug(`Registering agent to OpenClaw/LightClaw`, { agentId, workspaceDir });
19560
+ const brandName = clawDir ? detectClawBrand(clawDir) : "OpenClaw/LightClaw";
19561
+ logger.debug(`Registering agent to ${brandName}`, { agentId, workspaceDir });
19446
19562
  try {
19447
- const clawCmd = detectClawCommand();
19563
+ const clawCmd = detectClawCommand(clawDir);
19448
19564
  (0, import_node_child_process.execSync)(
19449
19565
  `${clawCmd} agents add "${agentId}" --workspace "${workspaceDir}" --non-interactive --json`,
19450
19566
  { stdio: "pipe", timeout: 15e3 }
@@ -19458,8 +19574,8 @@ function registerAgentToOpenClaw(agentName, workspaceDir, _clawDir) {
19458
19574
  if (stderr.includes("already exists")) {
19459
19575
  logger.info(`Agent "${agentId}" already exists in CLI, updating config...`);
19460
19576
  try {
19461
- const clawDir = _clawDir || import_node_path11.default.dirname(workspaceDir);
19462
- addAgentToOpenClawConfig(clawDir, agentId, agentName, false);
19577
+ const resolvedClawDir = clawDir || import_node_path11.default.dirname(workspaceDir);
19578
+ addAgentToOpenClawConfig(resolvedClawDir, agentId, agentName, false);
19463
19579
  } catch {
19464
19580
  logger.warn(`Failed to update config for existing agent "${agentId}", skipping.`);
19465
19581
  }
@@ -19471,8 +19587,8 @@ function registerAgentToOpenClaw(agentName, workspaceDir, _clawDir) {
19471
19587
  const errMsg = cliError instanceof Error ? cliError.message : String(cliError);
19472
19588
  logger.warn(`CLI agents add failed, falling back to config file modification`, { agentId, stderr, error: errMsg });
19473
19589
  try {
19474
- const clawDir = _clawDir || import_node_path11.default.dirname(workspaceDir);
19475
- const configUpdated = addAgentToOpenClawConfig(clawDir, agentId, agentName, false);
19590
+ const resolvedClawDir = clawDir || import_node_path11.default.dirname(workspaceDir);
19591
+ const configUpdated = addAgentToOpenClawConfig(resolvedClawDir, agentId, agentName, false);
19476
19592
  if (configUpdated) {
19477
19593
  logger.info(`Agent "${agentId}" registered via config file fallback.`);
19478
19594
  return {
@@ -19496,7 +19612,11 @@ function registerAgentToOpenClaw(agentName, workspaceDir, _clawDir) {
19496
19612
  }
19497
19613
  }
19498
19614
  }
19499
- function detectClawCommand() {
19615
+ function detectClawCommand(clawDir) {
19616
+ if (clawDir) {
19617
+ const brand = detectClawBrand(clawDir);
19618
+ return brand === "LightClaw" ? "lightclaw" : "openclaw";
19619
+ }
19500
19620
  try {
19501
19621
  (0, import_node_child_process.execSync)("which lightclaw 2>/dev/null || where lightclaw 2>nul", { stdio: "pipe" });
19502
19622
  return "lightclaw";
@@ -19509,8 +19629,8 @@ function detectClawCommand() {
19509
19629
  }
19510
19630
  return "openclaw";
19511
19631
  }
19512
- function restartOpenClawGateway() {
19513
- const clawCmd = detectClawCommand();
19632
+ function restartOpenClawGateway(clawDir) {
19633
+ const clawCmd = detectClawCommand(clawDir);
19514
19634
  logger.debug(`Restarting ${clawCmd} Gateway`);
19515
19635
  try {
19516
19636
  (0, import_node_child_process.execSync)(`${clawCmd} gateway restart`, {
@@ -19533,7 +19653,7 @@ function restartOpenClawGateway() {
19533
19653
  }
19534
19654
 
19535
19655
  // src/commands/search.ts
19536
- var searchCommand = new Command("search").description("Search for agents in the SoulHub registry").argument("[query]", "Search query (matches name, description, tags)").option("-c, --category <category>", "Filter by category").option("-l, --limit <number>", "Max results to show", "20").action(async (query, options) => {
19656
+ var searchCommand = new Command("search").description("Search for agents in the SoulHub registry").argument("[query]", "Search query (matches name, description, tags)").option("-c, --category <category>", "Filter by category").option("-n, --limit <number>", "Max results to show", "20").option("--json", "Output results in JSON format").action(async (query, options) => {
19537
19657
  try {
19538
19658
  const index = await fetchIndex();
19539
19659
  let agents = index.agents;
@@ -19551,14 +19671,30 @@ var searchCommand = new Command("search").description("Search for agents in the
19551
19671
  const limit = parseInt(options.limit, 10);
19552
19672
  const shown = agents.slice(0, limit);
19553
19673
  if (shown.length === 0) {
19554
- console.log(source_default.yellow("No agents found matching your query."));
19555
- if (query) {
19556
- console.log(
19557
- source_default.dim(` Try: soulhub search (without query to list all)`)
19558
- );
19674
+ if (options.json) {
19675
+ console.log(JSON.stringify([], null, 2));
19676
+ } else {
19677
+ console.log(source_default.yellow("No agents found matching your query."));
19678
+ if (query) {
19679
+ console.log(
19680
+ source_default.dim(` Try: soulhub search (without query to list all)`)
19681
+ );
19682
+ }
19559
19683
  }
19560
19684
  return;
19561
19685
  }
19686
+ if (options.json) {
19687
+ const jsonOutput = shown.map((a) => ({
19688
+ name: a.name,
19689
+ displayName: a.displayName,
19690
+ version: a.version,
19691
+ description: a.description,
19692
+ category: a.category,
19693
+ tags: a.tags
19694
+ }));
19695
+ console.log(JSON.stringify(jsonOutput, null, 2));
19696
+ return;
19697
+ }
19562
19698
  console.log(
19563
19699
  source_default.bold(`
19564
19700
  Found ${agents.length} agent(s):
@@ -19599,7 +19735,7 @@ var searchCommand = new Command("search").description("Search for agents in the
19599
19735
  });
19600
19736
 
19601
19737
  // src/commands/info.ts
19602
- var infoCommand = new Command("info").description("Show details of an agent (identity, soul, skills, etc.)").argument("<name>", "Agent name").option("--identity", "Show IDENTITY.md content").option("--soul", "Show SOUL.md content").action(async (name, options) => {
19738
+ var infoCommand = new Command("info").description("Show details of an agent (identity, soul, skills, etc.)").argument("<name>", "Agent name").option("--identity", "Show IDENTITY.md content").option("--soul", "Show SOUL.md content").option("--json", "Output results in JSON format").action(async (name, options) => {
19603
19739
  try {
19604
19740
  const index = await fetchIndex();
19605
19741
  const agent = index.agents.find((a) => a.name === name);
@@ -19611,6 +19747,22 @@ var infoCommand = new Command("info").description("Show details of an agent (ide
19611
19747
  process.exit(1);
19612
19748
  }
19613
19749
  const category = CATEGORY_LABELS[agent.category] || agent.category;
19750
+ if (options.json) {
19751
+ const jsonOutput = {
19752
+ name: agent.name,
19753
+ displayName: agent.displayName,
19754
+ version: agent.version,
19755
+ description: agent.description,
19756
+ category: agent.category,
19757
+ author: agent.author,
19758
+ tags: agent.tags,
19759
+ minClawVersion: agent.minClawVersion,
19760
+ downloads: agent.downloads,
19761
+ files: agent.files
19762
+ };
19763
+ console.log(JSON.stringify(jsonOutput, null, 2));
19764
+ return;
19765
+ }
19614
19766
  console.log();
19615
19767
  console.log(source_default.bold.cyan(` ${agent.displayName}`));
19616
19768
  console.log(source_default.dim(` ${agent.name} v${agent.version}`));
@@ -19639,7 +19791,7 @@ var infoCommand = new Command("info").description("Show details of an agent (ide
19639
19791
  console.log(` ${fileName} ${source_default.dim(`(${sizeStr})`)}`);
19640
19792
  }
19641
19793
  if (options.identity || options.soul) {
19642
- const fs7 = await import("fs");
19794
+ const fs8 = await import("fs");
19643
19795
  const pkgDir = await downloadAgentPackage(name, agent.version);
19644
19796
  try {
19645
19797
  if (options.identity) {
@@ -19647,8 +19799,8 @@ var infoCommand = new Command("info").description("Show details of an agent (ide
19647
19799
  console.log(source_default.bold(" \u2500\u2500 IDENTITY.md \u2500\u2500"));
19648
19800
  console.log();
19649
19801
  const identityPath = (await import("path")).default.join(pkgDir, "IDENTITY.md");
19650
- if (fs7.default.existsSync(identityPath)) {
19651
- const content = fs7.default.readFileSync(identityPath, "utf-8");
19802
+ if (fs8.default.existsSync(identityPath)) {
19803
+ const content = fs8.default.readFileSync(identityPath, "utf-8");
19652
19804
  console.log(
19653
19805
  content.split("\n").map((l) => ` ${l}`).join("\n")
19654
19806
  );
@@ -19661,8 +19813,8 @@ var infoCommand = new Command("info").description("Show details of an agent (ide
19661
19813
  console.log(source_default.bold(" \u2500\u2500 SOUL.md \u2500\u2500"));
19662
19814
  console.log();
19663
19815
  const soulPath = (await import("path")).default.join(pkgDir, "SOUL.md");
19664
- if (fs7.default.existsSync(soulPath)) {
19665
- const content = fs7.default.readFileSync(soulPath, "utf-8");
19816
+ if (fs8.default.existsSync(soulPath)) {
19817
+ const content = fs8.default.readFileSync(soulPath, "utf-8");
19666
19818
  console.log(
19667
19819
  content.split("\n").map((l) => ` ${l}`).join("\n")
19668
19820
  );
@@ -19671,7 +19823,7 @@ var infoCommand = new Command("info").description("Show details of an agent (ide
19671
19823
  }
19672
19824
  }
19673
19825
  } finally {
19674
- fs7.default.rmSync(pkgDir, { recursive: true, force: true });
19826
+ fs8.default.rmSync(pkgDir, { recursive: true, force: true });
19675
19827
  }
19676
19828
  }
19677
19829
  console.log();
@@ -19773,27 +19925,39 @@ function resolveAllClawDirs(clawDir) {
19773
19925
  }
19774
19926
  return findAllClawDirs();
19775
19927
  }
19776
- var installCommand = new Command("install").description("Install an agent or team from the SoulHub registry (default: as worker, installs to all detected claws)").argument("[name]", "Agent or team name to install").option("--from <source>", "Install from a local directory, ZIP file, or URL").option("--main", "Install as main agent").option(
19928
+ var installCommand = new Command("install").description("Install an agent or team from the SoulHub registry").argument("[name]", "Agent or team name to install").option("--from <source>", "Install from a local directory, ZIP file, or URL").option("-r, --role <role>", "Install role: main or worker (skip role selection prompt)").option(
19777
19929
  "--dir <path>",
19778
19930
  "Target directory (defaults to OpenClaw/LightClaw workspace)"
19779
19931
  ).option(
19780
- "--clawtype <type>",
19932
+ "--claw-type <type>",
19781
19933
  "Specify claw type: OpenClaw or LightClaw (case-insensitive)"
19782
- ).action(async (name, options) => {
19934
+ ).option("-y, --yes", "Skip all confirmation prompts (auto-confirm)").action(async (name, options) => {
19783
19935
  try {
19784
- const asMain = !!options.main;
19936
+ const roleExplicit = !!options.role;
19937
+ const clawExplicit = !!options.clawType || !!options.dir;
19938
+ const skipConfirm = !!options.yes;
19939
+ if (options.role && !["main", "worker"].includes(options.role.toLowerCase())) {
19940
+ console.error(source_default.red(`Invalid role: "${options.role}". Must be "main" or "worker".`));
19941
+ process.exit(1);
19942
+ }
19785
19943
  if (options.from) {
19786
- await installFromSource(options.from, options.dir, options.clawtype, asMain);
19944
+ const asMain = await resolveRole(roleExplicit ? options.role.toLowerCase() === "main" : void 0);
19945
+ if (asMain === null) return;
19946
+ await installFromSource(options.from, options.dir, options.clawType, asMain, clawExplicit, skipConfirm);
19787
19947
  } else if (name) {
19788
- await installFromRegistry(name, options.dir, options.clawtype, asMain);
19948
+ const resolvedRole = roleExplicit ? options.role.toLowerCase() === "main" : void 0;
19949
+ await installFromRegistry(name, options.dir, options.clawType, resolvedRole, clawExplicit, skipConfirm);
19789
19950
  } else {
19790
19951
  console.error(source_default.red("Please specify an agent or team name, or use --from to install from a local source."));
19791
- console.log(source_default.dim(" Examples:"));
19792
- console.log(source_default.dim(" soulhub install writer-wechat # Install as worker to all detected claws"));
19793
- console.log(source_default.dim(" soulhub install writer-wechat --main # Install as main agent"));
19794
- console.log(source_default.dim(" soulhub install dev-squad # Install a team from registry"));
19795
- console.log(source_default.dim(" soulhub install --from ./agent-team/ # Install from local directory"));
19796
- console.log(source_default.dim(" soulhub install writer-wechat --clawtype LightClaw # Install to specific claw"));
19952
+ console.log();
19953
+ console.log(source_default.dim(" Usage:"));
19954
+ console.log(source_default.dim(" soulhub install <name> # Interactive: select role & claw"));
19955
+ console.log(source_default.dim(" soulhub install <name> --role main # As main agent, interactive claw selection"));
19956
+ console.log(source_default.dim(" soulhub install <name> --role worker # As worker agent, interactive claw selection"));
19957
+ console.log(source_default.dim(" soulhub install <name> --claw-type LightClaw # Interactive role, install to specific claw"));
19958
+ console.log(source_default.dim(" soulhub install <name> --role worker --claw-type OpenClaw # Fully non-interactive"));
19959
+ console.log(source_default.dim(" soulhub install <name> --role main --claw-type OpenClaw -y # Non-interactive, skip confirmation"));
19960
+ console.log(source_default.dim(" soulhub install --from ./agent-dir/ # Install from local directory"));
19797
19961
  process.exit(1);
19798
19962
  }
19799
19963
  } catch (error) {
@@ -19805,15 +19969,60 @@ var installCommand = new Command("install").description("Install an agent or tea
19805
19969
  process.exit(1);
19806
19970
  }
19807
19971
  });
19808
- async function installFromRegistry(name, targetDir, clawDir, asMain) {
19972
+ async function resolveRole(explicitMain) {
19973
+ if (explicitMain !== void 0) {
19974
+ return explicitMain;
19975
+ }
19976
+ const role = await promptSelectRole();
19977
+ if (role === null) return null;
19978
+ return role === "main";
19979
+ }
19980
+ async function resolveClawDirsInteractive(clawDir, clawExplicit) {
19981
+ if (clawDir) {
19982
+ return resolveAllClawDirs(clawDir);
19983
+ }
19984
+ if (clawExplicit) {
19985
+ return [];
19986
+ }
19987
+ return promptMultiSelectClawDirs();
19988
+ }
19989
+ async function installFromRegistry(name, targetDir, clawDir, asMain, clawExplicit, skipConfirm = false) {
19809
19990
  const spinner = createSpinner(`Checking registry for ${source_default.cyan(name)}...`).start();
19810
19991
  const index = await fetchIndex();
19811
19992
  const agent = index.agents.find((a) => a.name === name);
19812
19993
  const recipe = index.recipes.find((r) => r.name === name);
19813
19994
  if (agent && !recipe) {
19814
19995
  spinner.stop();
19815
- logger.info(`Installing single agent from registry: ${name}, asMain=${asMain}`);
19816
- await installSingleAgent(name, targetDir, clawDir, asMain);
19996
+ printAgentInfo(agent);
19997
+ const resolvedMain = await resolveRole(asMain);
19998
+ if (resolvedMain === null) return;
19999
+ if (resolvedMain) {
20000
+ console.log();
20001
+ console.log(source_default.yellow(" \u26A0 Installing as main agent will overwrite the current workspace/ content."));
20002
+ console.log(source_default.yellow(" The existing persona (IDENTITY.md, SOUL.md, etc.) will be replaced."));
20003
+ console.log(source_default.yellow(" Memory and conversation history will NOT be affected."));
20004
+ console.log();
20005
+ if (!skipConfirm) {
20006
+ const confirmed = await promptConfirm("Continue?", true);
20007
+ if (!confirmed) {
20008
+ console.log(source_default.dim(" Installation cancelled."));
20009
+ return;
20010
+ }
20011
+ } else {
20012
+ console.log(source_default.dim(" Auto-confirmed with --yes flag."));
20013
+ }
20014
+ }
20015
+ let resolvedClawDirs;
20016
+ if (!targetDir) {
20017
+ resolvedClawDirs = await resolveClawDirsInteractive(clawDir, clawExplicit);
20018
+ if (resolvedClawDirs.length === 0) {
20019
+ console.log(source_default.red("\n OpenClaw/LightClaw workspace directory not found."));
20020
+ printOpenClawInstallHelp();
20021
+ return;
20022
+ }
20023
+ }
20024
+ logger.info(`Installing single agent from registry: ${name}, asMain=${resolvedMain}`);
20025
+ await installSingleAgent(name, targetDir, clawDir, resolvedMain, resolvedClawDirs);
19817
20026
  } else if (recipe) {
19818
20027
  spinner.stop();
19819
20028
  logger.info(`Installing team recipe from registry: ${name}`);
@@ -19824,12 +20033,25 @@ async function installFromRegistry(name, targetDir, clawDir, asMain) {
19824
20033
  console.log(source_default.dim(" Use 'soulhub search' to find available agents and teams."));
19825
20034
  }
19826
20035
  }
19827
- async function installSingleAgent(name, targetDir, clawDir, asMain = false) {
20036
+ function printAgentInfo(agent) {
20037
+ console.log();
20038
+ console.log(source_default.bold(` \u{1F4E6} ${agent.displayName}`), source_default.dim(`v${agent.version}`));
20039
+ if (agent.description) {
20040
+ console.log(source_default.dim(` ${agent.description}`));
20041
+ }
20042
+ if (agent.category) {
20043
+ console.log(source_default.dim(` Category: ${agent.category}`));
20044
+ }
20045
+ if (agent.tags && agent.tags.length > 0) {
20046
+ console.log(source_default.dim(` Tags: ${agent.tags.join(", ")}`));
20047
+ }
20048
+ }
20049
+ async function installSingleAgent(name, targetDir, clawDir, asMain = false, preResolvedClawDirs) {
19828
20050
  if (targetDir) {
19829
20051
  await installSingleAgentToClaw(name, null, targetDir, asMain);
19830
20052
  return;
19831
20053
  }
19832
- const allClawDirs = resolveAllClawDirs(clawDir);
20054
+ const allClawDirs = preResolvedClawDirs || resolveAllClawDirs(clawDir);
19833
20055
  if (allClawDirs.length === 0) {
19834
20056
  console.log(source_default.red("OpenClaw/LightClaw workspace directory not found."));
19835
20057
  printOpenClawInstallHelp();
@@ -19963,7 +20185,7 @@ async function installSingleAgentToClaw(name, selectedClawDir, targetDir, asMain
19963
20185
  console.log(` ${source_default.dim("Version:")} ${agent.version}`);
19964
20186
  console.log(` ${source_default.dim("Type:")} ${typeLabel}`);
19965
20187
  if (!targetDir) {
19966
- await tryRestartGateway();
20188
+ await tryRestartGateway(selectedClawDir || void 0);
19967
20189
  }
19968
20190
  console.log();
19969
20191
  }
@@ -20024,7 +20246,7 @@ async function installRecipeFromRegistry(name, recipe, targetDir, clawDir) {
20024
20246
  const agentId = worker.name;
20025
20247
  const workerDir = targetDir ? import_node_path12.default.join(resolvedClawDir, `workspace-${agentId}`) : getWorkspaceDir(resolvedClawDir, agentId);
20026
20248
  if (!targetDir) {
20027
- const regResult = registerAgentToOpenClaw(agentId, workerDir, clawDir);
20249
+ const regResult = registerAgentToOpenClaw(agentId, workerDir, resolvedClawDir);
20028
20250
  if (!regResult.success) {
20029
20251
  console.log(source_default.yellow(` \u26A0 Failed to register ${agentId}: ${regResult.message}`));
20030
20252
  continue;
@@ -20056,10 +20278,11 @@ async function installRecipeFromRegistry(name, recipe, targetDir, clawDir) {
20056
20278
  );
20057
20279
  printTeamSummary(pkg, workerIds);
20058
20280
  if (!targetDir) {
20059
- await tryRestartGateway();
20281
+ await tryRestartGateway(resolvedClawDir);
20060
20282
  }
20061
20283
  }
20062
- async function installFromSource(source, targetDir, clawDir, asMain) {
20284
+ async function installFromSource(source, targetDir, clawDir, asMain, clawExplicit, skipConfirm = false) {
20285
+ if (asMain === null) return;
20063
20286
  const spinner = createSpinner("Analyzing package...").start();
20064
20287
  let packageDir;
20065
20288
  let tempDir = null;
@@ -20113,7 +20336,7 @@ async function installFromSource(source, targetDir, clawDir, asMain) {
20113
20336
  spinner.text = `Detected package type: ${source_default.blue(kind)}`;
20114
20337
  if (kind === "agent") {
20115
20338
  spinner.stop();
20116
- await installSingleAgentFromDir(packageDir, targetDir, clawDir, asMain);
20339
+ await installSingleAgentFromDir(packageDir, targetDir, clawDir, asMain, clawExplicit, skipConfirm);
20117
20340
  } else if (kind === "team") {
20118
20341
  spinner.stop();
20119
20342
  await installTeamFromDir(packageDir, targetDir, clawDir);
@@ -20125,14 +20348,30 @@ async function installFromSource(source, targetDir, clawDir, asMain) {
20125
20348
  import_node_fs9.default.rmSync(tempDir, { recursive: true, force: true });
20126
20349
  }
20127
20350
  }
20128
- async function installSingleAgentFromDir(packageDir, targetDir, clawDir, asMain = false) {
20351
+ async function installSingleAgentFromDir(packageDir, targetDir, clawDir, asMain = false, clawExplicit, skipConfirm = false) {
20129
20352
  const pkg = readSoulHubPackage(packageDir);
20130
20353
  const agentName = pkg?.name || import_node_path12.default.basename(packageDir);
20354
+ if (asMain) {
20355
+ console.log();
20356
+ console.log(source_default.yellow(" \u26A0 Installing as main agent will overwrite the current workspace/ content."));
20357
+ console.log(source_default.yellow(" The existing persona (IDENTITY.md, SOUL.md, etc.) will be replaced."));
20358
+ console.log(source_default.yellow(" Memory and conversation history will NOT be affected."));
20359
+ console.log();
20360
+ if (!skipConfirm) {
20361
+ const confirmed = await promptConfirm("Continue?", true);
20362
+ if (!confirmed) {
20363
+ console.log(source_default.dim(" Installation cancelled."));
20364
+ return;
20365
+ }
20366
+ } else {
20367
+ console.log(source_default.dim(" Auto-confirmed with --yes flag."));
20368
+ }
20369
+ }
20131
20370
  if (targetDir) {
20132
20371
  await installSingleAgentFromDirToClaw(packageDir, agentName, pkg, null, targetDir, asMain);
20133
20372
  return;
20134
20373
  }
20135
- const allClawDirs = resolveAllClawDirs(clawDir);
20374
+ const allClawDirs = await resolveClawDirsInteractive(clawDir, clawExplicit);
20136
20375
  if (allClawDirs.length === 0) {
20137
20376
  console.log(source_default.red("OpenClaw/LightClaw workspace directory not found."));
20138
20377
  printOpenClawInstallHelp();
@@ -20229,7 +20468,7 @@ async function installSingleAgentFromDirToClaw(packageDir, agentName, pkg, selec
20229
20468
  console.log(` ${source_default.dim("Source:")} ${packageDir}`);
20230
20469
  console.log(` ${source_default.dim("Type:")} ${typeLabel}`);
20231
20470
  if (!targetDir) {
20232
- await tryRestartGateway();
20471
+ await tryRestartGateway(selectedClawDir || void 0);
20233
20472
  }
20234
20473
  console.log();
20235
20474
  }
@@ -20315,7 +20554,7 @@ async function installTeamFromDir(packageDir, targetDir, clawDir) {
20315
20554
  spinner.text = `Installing worker ${source_default.cyan(agentId)}...`;
20316
20555
  const workerWorkspace = targetDir ? import_node_path12.default.join(resolvedClawDir, `workspace-${agentId}`) : getWorkspaceDir(resolvedClawDir, agentId);
20317
20556
  if (!targetDir) {
20318
- const regResult = registerAgentToOpenClaw(agentId, workerWorkspace, clawDir);
20557
+ const regResult = registerAgentToOpenClaw(agentId, workerWorkspace, resolvedClawDir);
20319
20558
  if (!regResult.success) {
20320
20559
  console.log(source_default.yellow(` \u26A0 Failed to register ${agentId}: ${regResult.message}`));
20321
20560
  continue;
@@ -20345,7 +20584,7 @@ async function installTeamFromDir(packageDir, targetDir, clawDir) {
20345
20584
  );
20346
20585
  printTeamSummary(pkg, workerIds);
20347
20586
  if (!targetDir) {
20348
- await tryRestartGateway();
20587
+ await tryRestartGateway(resolvedClawDir);
20349
20588
  }
20350
20589
  }
20351
20590
  function copyAgentFilesFromDir(sourceDir, targetDir) {
@@ -20413,7 +20652,7 @@ async function extractZipToDir(zip, targetDir) {
20413
20652
  }
20414
20653
  function printOpenClawInstallHelp() {
20415
20654
  console.log(source_default.dim(" Please install OpenClaw or LightClaw first, or use one of the following options:"));
20416
- console.log(source_default.dim(" --clawtype <type> Specify claw type: OpenClaw or LightClaw"));
20655
+ console.log(source_default.dim(" --claw-type <type> Specify claw type: OpenClaw or LightClaw"));
20417
20656
  console.log(source_default.dim(" --dir <path> Specify agent target directory directly"));
20418
20657
  console.log(source_default.dim(" OPENCLAW_HOME=<path> Set environment variable (for OpenClaw)"));
20419
20658
  console.log(source_default.dim(" LIGHTCLAW_HOME=<path> Set environment variable (for LightClaw)"));
@@ -20433,11 +20672,11 @@ function printTeamSummary(pkg, workerIds) {
20433
20672
  }
20434
20673
  console.log();
20435
20674
  }
20436
- async function tryRestartGateway() {
20437
- const clawCmd = detectClawCommand();
20675
+ async function tryRestartGateway(clawDir) {
20676
+ const clawCmd = detectClawCommand(clawDir);
20438
20677
  const brandName = clawCmd === "lightclaw" ? "LightClaw" : "OpenClaw";
20439
20678
  const restartSpinner = createSpinner(`Restarting ${brandName} Gateway...`).start();
20440
- const result = restartOpenClawGateway();
20679
+ const result = restartOpenClawGateway(clawDir);
20441
20680
  if (result.success) {
20442
20681
  restartSpinner.succeed(`${brandName} Gateway restarted successfully.`);
20443
20682
  } else {
@@ -20446,17 +20685,31 @@ async function tryRestartGateway() {
20446
20685
  }
20447
20686
 
20448
20687
  // src/commands/list.ts
20449
- var listCommand = new Command("list").description("List installed agents").alias("ls").action(async () => {
20688
+ var listCommand = new Command("list").description("List installed agents").alias("ls").option("--json", "Output results in JSON format").action(async (options) => {
20450
20689
  try {
20451
20690
  const config = loadConfig();
20452
20691
  if (config.installed.length === 0) {
20453
- console.log(source_default.yellow("\n No agents installed yet.\n"));
20454
- console.log(
20455
- source_default.dim(" Install one: soulhub install <name>")
20456
- );
20457
- console.log(
20458
- source_default.dim(" Browse all: soulhub search\n")
20459
- );
20692
+ if (options.json) {
20693
+ console.log(JSON.stringify([], null, 2));
20694
+ } else {
20695
+ console.log(source_default.yellow("\n No agents installed yet.\n"));
20696
+ console.log(
20697
+ source_default.dim(" Install one: soulhub install <name>")
20698
+ );
20699
+ console.log(
20700
+ source_default.dim(" Browse all: soulhub search\n")
20701
+ );
20702
+ }
20703
+ return;
20704
+ }
20705
+ if (options.json) {
20706
+ const jsonOutput = config.installed.map((a) => ({
20707
+ name: a.name,
20708
+ version: a.version,
20709
+ installedAt: a.installedAt,
20710
+ workspace: a.workspace
20711
+ }));
20712
+ console.log(JSON.stringify(jsonOutput, null, 2));
20460
20713
  return;
20461
20714
  }
20462
20715
  console.log(
@@ -20514,6 +20767,19 @@ var updateCommand = new Command("update").description("Update installed agents t
20514
20767
  }
20515
20768
  spinner.text = `Updating ${source_default.cyan(installed.name)} (${installed.version} \u2192 ${remote.version})...`;
20516
20769
  const workspaceDir = installed.workspace;
20770
+ const backupDir = backupAgentWorkspace(workspaceDir);
20771
+ if (backupDir) {
20772
+ const backupRecord = createBackupRecord("single-agent", installed.name, workspaceDir);
20773
+ addBackupItem(backupRecord, {
20774
+ originalPath: workspaceDir,
20775
+ backupPath: backupDir,
20776
+ method: "cp",
20777
+ role: "worker",
20778
+ agentId: installed.name
20779
+ });
20780
+ commitBackupRecord(backupRecord);
20781
+ logger.info(`Update backup created for ${installed.name}`, { backupDir });
20782
+ }
20517
20783
  if (!import_node_fs10.default.existsSync(workspaceDir)) {
20518
20784
  import_node_fs10.default.mkdirSync(workspaceDir, { recursive: true });
20519
20785
  }
@@ -20542,20 +20808,109 @@ var updateCommand = new Command("update").description("Update installed agents t
20542
20808
  }
20543
20809
  });
20544
20810
 
20545
- // src/commands/rollback.ts
20811
+ // src/commands/uninstall.ts
20546
20812
  var import_node_fs11 = __toESM(require("fs"), 1);
20813
+ var uninstallCommand = new Command("uninstall").description("Uninstall an agent template").alias("rm").argument("<name>", "Agent name to uninstall").option("--keep-files", "Remove from registry but keep workspace files").option("-y, --yes", "Skip all confirmation prompts (auto-confirm)").action(async (name, options) => {
20814
+ try {
20815
+ const config = loadConfig();
20816
+ const installed = config.installed.find((a) => a.name === name);
20817
+ if (!installed) {
20818
+ console.error(
20819
+ source_default.red(`
20820
+ Agent "${name}" is not installed.
20821
+ `)
20822
+ );
20823
+ console.log(
20824
+ source_default.dim(" Use 'soulhub list' to see installed agents.")
20825
+ );
20826
+ process.exit(1);
20827
+ }
20828
+ const spinner = createSpinner(
20829
+ `Uninstalling ${source_default.cyan(name)}...`
20830
+ ).start();
20831
+ const manifest = loadBackupManifest();
20832
+ const relatedRecords = manifest.records.filter((r) => r.packageName === name);
20833
+ if (relatedRecords.length > 0) {
20834
+ spinner.stop();
20835
+ console.log(
20836
+ source_default.yellow(`
20837
+ \u26A0 Found ${relatedRecords.length} backup record(s) for "${name}".`)
20838
+ );
20839
+ console.log(
20840
+ source_default.yellow(` Uninstalling will also delete all related backup files.`)
20841
+ );
20842
+ console.log(
20843
+ source_default.yellow(` After deletion, you will NOT be able to rollback this agent.
20844
+ `)
20845
+ );
20846
+ if (!options.yes) {
20847
+ const confirmed = await promptConfirm("Proceed with uninstall?");
20848
+ if (!confirmed) {
20849
+ console.log(source_default.dim("\n Uninstall cancelled.\n"));
20850
+ return;
20851
+ }
20852
+ } else {
20853
+ console.log(source_default.dim(" Auto-confirmed with --yes flag."));
20854
+ }
20855
+ spinner.start(`Uninstalling ${source_default.cyan(name)}...`);
20856
+ }
20857
+ if (!options.keepFiles && import_node_fs11.default.existsSync(installed.workspace)) {
20858
+ import_node_fs11.default.rmSync(installed.workspace, { recursive: true, force: true });
20859
+ spinner.text = `Removed workspace: ${installed.workspace}`;
20860
+ }
20861
+ removeInstallRecord(name);
20862
+ if (relatedRecords.length > 0) {
20863
+ for (const record of relatedRecords) {
20864
+ for (const item of record.items) {
20865
+ if (import_node_fs11.default.existsSync(item.backupPath)) {
20866
+ import_node_fs11.default.rmSync(item.backupPath, { recursive: true, force: true });
20867
+ logger.info(`Cleaned backup file for uninstalled agent`, { backupPath: item.backupPath });
20868
+ }
20869
+ }
20870
+ }
20871
+ manifest.records = manifest.records.filter((r) => r.packageName !== name);
20872
+ saveBackupManifest(manifest);
20873
+ spinner.text = `Cleaned ${relatedRecords.length} backup record(s)`;
20874
+ logger.info(`Cleaned ${relatedRecords.length} backup record(s) for ${name}`);
20875
+ }
20876
+ logger.info(`Agent uninstalled: ${name}`, { workspace: installed.workspace, keepFiles: !!options.keepFiles });
20877
+ spinner.succeed(
20878
+ `${source_default.cyan.bold(name)} uninstalled.`
20879
+ );
20880
+ if (options.keepFiles) {
20881
+ console.log(
20882
+ source_default.dim(` Files kept at: ${installed.workspace}`)
20883
+ );
20884
+ }
20885
+ console.log();
20886
+ } catch (error) {
20887
+ logger.errorObj("Uninstall command failed", error);
20888
+ console.error(
20889
+ source_default.red(`Error: ${error instanceof Error ? error.message : error}`)
20890
+ );
20891
+ console.error(source_default.dim(` See logs: ${logger.getTodayLogFile()}`));
20892
+ process.exit(1);
20893
+ }
20894
+ });
20895
+
20896
+ // src/commands/rollback.ts
20897
+ var import_node_fs12 = __toESM(require("fs"), 1);
20547
20898
  var import_node_path13 = __toESM(require("path"), 1);
20548
- var rollbackCommand = new Command("rollback").description("Rollback to a previous agent installation state").option("--list", "List available rollback records").option("--id <id>", "Rollback to a specific backup record by ID").option(
20549
- "--clawtype <type>",
20899
+ var import_node_readline2 = __toESM(require("readline"), 1);
20900
+ var rollbackCommand = new Command("rollback").description("Rollback to a previous agent installation state").option("--list", "List available rollback records").option("--id <id>", "Rollback to a specific backup record by ID").option("--last <n>", "Rollback the Nth most recent installation (1 = latest, 2 = second latest, etc.)", parseInt).option(
20901
+ "--claw-type <type>",
20550
20902
  "Specify claw type: OpenClaw or LightClaw (case-insensitive)"
20551
- ).action(async (options) => {
20903
+ ).option("-y, --yes", "Skip all confirmation prompts (auto-confirm)").action(async (options) => {
20552
20904
  try {
20905
+ const skipConfirm = !!options.yes;
20553
20906
  if (options.list) {
20554
20907
  listBackupRecords();
20555
20908
  } else if (options.id) {
20556
- await performRollback(options.id, options.clawtype);
20909
+ await performRollback(options.id, options.clawType, skipConfirm);
20910
+ } else if (options.last) {
20911
+ await performRollbackByIndex(options.last, options.clawType, skipConfirm);
20557
20912
  } else {
20558
- await performRollback(void 0, options.clawtype);
20913
+ await interactiveRollback(options.clawType);
20559
20914
  }
20560
20915
  } catch (error) {
20561
20916
  logger.errorObj("Rollback command failed", error);
@@ -20574,53 +20929,192 @@ function listBackupRecords() {
20574
20929
  return;
20575
20930
  }
20576
20931
  console.log(source_default.bold("\nAvailable rollback records:\n"));
20932
+ printRecordTable(manifest.records);
20933
+ console.log();
20934
+ console.log(source_default.dim(" Usage:"));
20935
+ console.log(source_default.dim(" soulhub rollback # Interactive: select a record to rollback"));
20936
+ console.log(source_default.dim(" soulhub rollback --last 1 # Rollback the latest installation"));
20937
+ console.log(source_default.dim(" soulhub rollback --last 2 # Rollback the 2nd latest installation"));
20938
+ console.log(source_default.dim(" soulhub rollback --id <id> # Rollback to a specific record by ID"));
20939
+ console.log();
20940
+ }
20941
+ function printRecordTable(records) {
20577
20942
  console.log(
20578
20943
  source_default.dim(
20579
- ` ${"ID".padEnd(20)} ${"Type".padEnd(20)} ${"Package".padEnd(20)} ${"Date".padEnd(22)} Items`
20944
+ ` ${"#".padEnd(4)} ${"ID".padEnd(20)} ${"Type".padEnd(20)} ${"Package".padEnd(20)} ${"Claw".padEnd(14)} ${"Date".padEnd(22)} Items`
20580
20945
  )
20581
20946
  );
20582
- console.log(source_default.dim(" " + "\u2500".repeat(90)));
20583
- for (const record of manifest.records) {
20947
+ console.log(source_default.dim(" " + "\u2500".repeat(108)));
20948
+ records.forEach((record, index) => {
20584
20949
  const date = new Date(record.createdAt).toLocaleString();
20585
20950
  const typeLabel = formatInstallType(record.installType);
20586
20951
  const itemCount = record.items.length;
20952
+ const clawBrand = detectClawBrandFromDir(record.clawDir);
20587
20953
  console.log(
20588
- ` ${source_default.cyan(record.id.padEnd(20))} ${typeLabel.padEnd(20)} ${source_default.white(record.packageName.padEnd(20))} ${source_default.dim(date.padEnd(22))} ${itemCount} backup(s)`
20954
+ ` ${source_default.yellow(String(index + 1).padEnd(4))} ${source_default.cyan(record.id.padEnd(20))} ${typeLabel.padEnd(20)} ${source_default.white(record.packageName.padEnd(20))} ${source_default.dim(clawBrand.padEnd(14))} ${source_default.dim(date.padEnd(22))} ${itemCount} backup(s)`
20589
20955
  );
20956
+ });
20957
+ }
20958
+ async function interactiveRollback(clawDir) {
20959
+ const manifest = loadBackupManifest();
20960
+ if (manifest.records.length === 0) {
20961
+ console.log(source_default.yellow("No backup records found. Nothing to rollback."));
20962
+ console.log(source_default.dim(" Backup records are created automatically when you install agents."));
20963
+ return;
20590
20964
  }
20965
+ console.log(source_default.bold("\n Select a record to rollback:\n"));
20966
+ printRecordTable(manifest.records);
20591
20967
  console.log();
20592
- console.log(source_default.dim(" Usage:"));
20593
- console.log(source_default.dim(" soulhub rollback # Rollback last installation"));
20594
- console.log(source_default.dim(" soulhub rollback --id <id> # Rollback to a specific record"));
20968
+ const rl = import_node_readline2.default.createInterface({
20969
+ input: process.stdin,
20970
+ output: process.stdout
20971
+ });
20972
+ const selected = await new Promise((resolve) => {
20973
+ rl.question(` Enter number to rollback (1-${manifest.records.length}), or 'q' to cancel: `, (answer) => {
20974
+ rl.close();
20975
+ const trimmed = answer.trim().toLowerCase();
20976
+ if (trimmed === "q" || trimmed === "quit" || trimmed === "") {
20977
+ resolve(null);
20978
+ return;
20979
+ }
20980
+ const idx = parseInt(trimmed, 10);
20981
+ if (idx >= 1 && idx <= manifest.records.length) {
20982
+ resolve(idx);
20983
+ } else {
20984
+ resolve(null);
20985
+ }
20986
+ });
20987
+ });
20988
+ if (selected === null) {
20989
+ console.log(source_default.dim(" Rollback cancelled."));
20990
+ return;
20991
+ }
20992
+ const record = manifest.records[selected - 1];
20595
20993
  console.log();
20994
+ console.log(source_default.dim(` Selected: ${source_default.cyan(record.id)} (${record.packageName})`));
20995
+ printRollbackDetails(record);
20996
+ const confirmed = await promptConfirmRollback();
20997
+ if (!confirmed) {
20998
+ console.log(source_default.dim(" Rollback cancelled."));
20999
+ return;
21000
+ }
21001
+ await executeRollback(record, clawDir);
20596
21002
  }
20597
- async function performRollback(recordId, clawDir) {
21003
+ async function performRollbackByIndex(n, clawDir, skipConfirm = false) {
20598
21004
  const manifest = loadBackupManifest();
20599
21005
  if (manifest.records.length === 0) {
20600
21006
  console.log(source_default.yellow("No backup records found. Nothing to rollback."));
20601
21007
  return;
20602
21008
  }
20603
- let record;
20604
- if (recordId) {
20605
- record = manifest.records.find((r) => r.id === recordId);
20606
- if (!record) {
20607
- console.error(source_default.red(`Backup record "${recordId}" not found.`));
20608
- console.log(source_default.dim(" Use 'soulhub rollback --list' to see available records."));
21009
+ if (n < 1 || n > manifest.records.length) {
21010
+ console.error(source_default.red(`Invalid index: ${n}. Available range: 1-${manifest.records.length}`));
21011
+ console.log(source_default.dim(" Use 'soulhub rollback --list' to see all available records."));
21012
+ return;
21013
+ }
21014
+ const record = manifest.records[n - 1];
21015
+ console.log(
21016
+ source_default.dim(
21017
+ `
21018
+ Rolling back #${n}: ${source_default.cyan(record.id)} (${record.packageName})`
21019
+ )
21020
+ );
21021
+ printRollbackDetails(record);
21022
+ if (!skipConfirm) {
21023
+ const rl2 = import_node_readline2.default.createInterface({
21024
+ input: process.stdin,
21025
+ output: process.stdout
21026
+ });
21027
+ const confirmed = await new Promise((resolve) => {
21028
+ rl2.question(` ${source_default.yellow("\u26A0")} Proceed with rollback? (Y/n) `, (answer) => {
21029
+ rl2.close();
21030
+ const trimmed = answer.trim().toLowerCase();
21031
+ resolve(trimmed === "" || trimmed === "y" || trimmed === "yes");
21032
+ });
21033
+ });
21034
+ if (!confirmed) {
21035
+ console.log(source_default.dim(" Rollback cancelled."));
20609
21036
  return;
20610
21037
  }
20611
21038
  } else {
20612
- record = manifest.records[0];
20613
- console.log(
20614
- source_default.dim(
20615
- `Rolling back last installation: ${source_default.cyan(record.id)} (${record.packageName})`
20616
- )
20617
- );
21039
+ console.log(source_default.dim(" Auto-confirmed with --yes flag."));
21040
+ }
21041
+ await executeRollback(record, clawDir);
21042
+ }
21043
+ async function performRollback(recordId, clawDir, skipConfirm = false) {
21044
+ const manifest = loadBackupManifest();
21045
+ if (manifest.records.length === 0) {
21046
+ console.log(source_default.yellow("No backup records found. Nothing to rollback."));
21047
+ return;
21048
+ }
21049
+ const record = manifest.records.find((r) => r.id === recordId);
21050
+ if (!record) {
21051
+ console.error(source_default.red(`Backup record "${recordId}" not found.`));
21052
+ console.log(source_default.dim(" Use 'soulhub rollback --list' to see available records."));
21053
+ return;
21054
+ }
21055
+ console.log(
21056
+ source_default.dim(
21057
+ `
21058
+ Rolling back: ${source_default.cyan(record.id)} (${record.packageName})`
21059
+ )
21060
+ );
21061
+ printRollbackDetails(record);
21062
+ if (!skipConfirm) {
21063
+ const confirmed = await promptConfirmRollback();
21064
+ if (!confirmed) {
21065
+ console.log(source_default.dim(" Rollback cancelled."));
21066
+ return;
21067
+ }
21068
+ } else {
21069
+ console.log(source_default.dim(" Auto-confirmed with --yes flag."));
21070
+ }
21071
+ await executeRollback(record, clawDir);
21072
+ }
21073
+ function printRollbackDetails(record) {
21074
+ console.log();
21075
+ console.log(source_default.dim(" Rollback details:"));
21076
+ console.log(source_default.dim(` Package: ${record.packageName}`));
21077
+ console.log(source_default.dim(` Type: ${formatInstallType(record.installType)}`));
21078
+ console.log(source_default.dim(` Claw dir: ${record.clawDir}`));
21079
+ console.log(source_default.dim(` Date: ${new Date(record.createdAt).toLocaleString()}`));
21080
+ if (record.items.length > 0) {
21081
+ console.log(source_default.dim(` Backups to restore:`));
21082
+ for (const item of record.items) {
21083
+ const methodLabel = item.method === "mv" ? "move back" : "copy back";
21084
+ console.log(source_default.dim(` - ${item.agentId} (${item.role}, ${methodLabel})`));
21085
+ }
21086
+ }
21087
+ if (record.installedWorkerIds.length > 0) {
21088
+ console.log(source_default.dim(` Workers to remove: ${record.installedWorkerIds.join(", ")}`));
21089
+ }
21090
+ if (record.installedMainAgent) {
21091
+ console.log(source_default.dim(` Main agent to revert: ${record.installedMainAgent}`));
20618
21092
  }
21093
+ console.log();
21094
+ }
21095
+ async function promptConfirmRollback() {
21096
+ const rl = import_node_readline2.default.createInterface({
21097
+ input: process.stdin,
21098
+ output: process.stdout
21099
+ });
21100
+ return new Promise((resolve) => {
21101
+ rl.question(` ${source_default.yellow("\u26A0")} Proceed with rollback? (Y/n) `, (answer) => {
21102
+ rl.close();
21103
+ const trimmed = answer.trim().toLowerCase();
21104
+ if (trimmed === "" || trimmed === "y" || trimmed === "yes") {
21105
+ resolve(true);
21106
+ } else {
21107
+ resolve(false);
21108
+ }
21109
+ });
21110
+ });
21111
+ }
21112
+ async function executeRollback(record, clawDir) {
20619
21113
  const spinner = createSpinner(
20620
21114
  `Rolling back ${source_default.cyan(record.packageName)}...`
20621
21115
  ).start();
20622
- const resolvedClawDir = clawDir ? findOpenClawDir(clawDir) || record.clawDir : await promptSelectClawDir() || record.clawDir;
20623
- if (!resolvedClawDir || !import_node_fs11.default.existsSync(resolvedClawDir)) {
21116
+ const resolvedClawDir = clawDir ? findOpenClawDir(clawDir) || record.clawDir : record.clawDir;
21117
+ if (!resolvedClawDir || !import_node_fs12.default.existsSync(resolvedClawDir)) {
20624
21118
  spinner.fail(`OpenClaw/LightClaw directory not found: ${record.clawDir}`);
20625
21119
  return;
20626
21120
  }
@@ -20640,66 +21134,72 @@ async function performRollback(recordId, clawDir) {
20640
21134
  spinner.text = "Removing installed workers...";
20641
21135
  for (const workerId of record.installedWorkerIds) {
20642
21136
  const workerDir = getWorkspaceDir(resolvedClawDir, workerId);
20643
- if (import_node_fs11.default.existsSync(workerDir)) {
20644
- import_node_fs11.default.rmSync(workerDir, { recursive: true, force: true });
21137
+ if (import_node_fs12.default.existsSync(workerDir)) {
21138
+ import_node_fs12.default.rmSync(workerDir, { recursive: true, force: true });
20645
21139
  logger.info(`Removed installed worker directory`, { workerId, dir: workerDir });
20646
21140
  }
20647
21141
  const agentConfigDir = import_node_path13.default.join(resolvedClawDir, "agents", workerId);
20648
- if (import_node_fs11.default.existsSync(agentConfigDir)) {
20649
- import_node_fs11.default.rmSync(agentConfigDir, { recursive: true, force: true });
21142
+ if (import_node_fs12.default.existsSync(agentConfigDir)) {
21143
+ import_node_fs12.default.rmSync(agentConfigDir, { recursive: true, force: true });
20650
21144
  }
20651
21145
  }
20652
21146
  }
20653
21147
  if (record.installedMainAgent) {
20654
21148
  const mainWorkspace = getMainWorkspaceDir(resolvedClawDir);
20655
21149
  const hasMainBackup = record.items.some((item) => item.role === "main");
20656
- if (hasMainBackup && import_node_fs11.default.existsSync(mainWorkspace)) {
21150
+ if (hasMainBackup && import_node_fs12.default.existsSync(mainWorkspace)) {
20657
21151
  spinner.text = "Cleaning current main workspace...";
20658
- const entries = import_node_fs11.default.readdirSync(mainWorkspace);
21152
+ const entries = import_node_fs12.default.readdirSync(mainWorkspace);
20659
21153
  for (const entry of entries) {
20660
- import_node_fs11.default.rmSync(import_node_path13.default.join(mainWorkspace, entry), { recursive: true, force: true });
21154
+ import_node_fs12.default.rmSync(import_node_path13.default.join(mainWorkspace, entry), { recursive: true, force: true });
20661
21155
  }
20662
21156
  }
20663
21157
  }
20664
21158
  let restoredCount = 0;
20665
21159
  for (const item of record.items) {
20666
- if (!import_node_fs11.default.existsSync(item.backupPath)) {
21160
+ if (!import_node_fs12.default.existsSync(item.backupPath)) {
20667
21161
  logger.warn(`Backup path not found, skipping`, { backupPath: item.backupPath });
20668
21162
  console.log(source_default.yellow(` \u26A0 Backup not found: ${item.backupPath}, skipping...`));
20669
21163
  continue;
20670
21164
  }
20671
21165
  spinner.text = `Restoring ${source_default.cyan(item.agentId)} (${item.role})...`;
20672
21166
  if (item.method === "mv") {
20673
- if (import_node_fs11.default.existsSync(item.originalPath)) {
20674
- import_node_fs11.default.rmSync(item.originalPath, { recursive: true, force: true });
21167
+ if (import_node_fs12.default.existsSync(item.originalPath)) {
21168
+ import_node_fs12.default.rmSync(item.originalPath, { recursive: true, force: true });
21169
+ }
21170
+ import_node_fs12.default.mkdirSync(import_node_path13.default.dirname(item.originalPath), { recursive: true });
21171
+ try {
21172
+ import_node_fs12.default.renameSync(item.backupPath, item.originalPath);
21173
+ } catch {
21174
+ import_node_fs12.default.cpSync(item.backupPath, item.originalPath, { recursive: true });
21175
+ import_node_fs12.default.rmSync(item.backupPath, { recursive: true, force: true });
20675
21176
  }
20676
- import_node_fs11.default.mkdirSync(import_node_path13.default.dirname(item.originalPath), { recursive: true });
20677
- import_node_fs11.default.renameSync(item.backupPath, item.originalPath);
20678
21177
  logger.info(`Restored (mv back)`, { from: item.backupPath, to: item.originalPath });
20679
21178
  } else {
20680
- if (import_node_fs11.default.existsSync(item.originalPath)) {
20681
- const entries = import_node_fs11.default.readdirSync(item.originalPath);
21179
+ if (import_node_fs12.default.existsSync(item.originalPath)) {
21180
+ const entries = import_node_fs12.default.readdirSync(item.originalPath);
20682
21181
  for (const entry of entries) {
20683
- import_node_fs11.default.rmSync(import_node_path13.default.join(item.originalPath, entry), { recursive: true, force: true });
21182
+ import_node_fs12.default.rmSync(import_node_path13.default.join(item.originalPath, entry), { recursive: true, force: true });
20684
21183
  }
20685
21184
  } else {
20686
- import_node_fs11.default.mkdirSync(item.originalPath, { recursive: true });
21185
+ import_node_fs12.default.mkdirSync(item.originalPath, { recursive: true });
20687
21186
  }
20688
- import_node_fs11.default.cpSync(item.backupPath, item.originalPath, { recursive: true });
20689
- import_node_fs11.default.rmSync(item.backupPath, { recursive: true, force: true });
21187
+ import_node_fs12.default.cpSync(item.backupPath, item.originalPath, { recursive: true });
21188
+ import_node_fs12.default.rmSync(item.backupPath, { recursive: true, force: true });
20690
21189
  logger.info(`Restored (cp back)`, { from: item.backupPath, to: item.originalPath });
20691
21190
  }
20692
21191
  restoredCount++;
20693
21192
  }
21193
+ const manifest = loadBackupManifest();
20694
21194
  manifest.records = manifest.records.filter((r) => r.id !== record.id);
20695
21195
  saveBackupManifest(manifest);
20696
21196
  spinner.succeed(
20697
21197
  `Rolled back ${source_default.cyan.bold(record.packageName)} successfully! (${restoredCount} item(s) restored)`
20698
21198
  );
20699
- const clawCmd = detectClawCommand();
21199
+ const clawCmd = detectClawCommand(resolvedClawDir);
20700
21200
  const brandName = clawCmd === "lightclaw" ? "LightClaw" : "OpenClaw";
20701
21201
  const restartSpinner = createSpinner(`Restarting ${brandName} Gateway...`).start();
20702
- const result = restartOpenClawGateway();
21202
+ const result = restartOpenClawGateway(resolvedClawDir);
20703
21203
  if (result.success) {
20704
21204
  restartSpinner.succeed(`${brandName} Gateway restarted successfully.`);
20705
21205
  } else {
@@ -20724,16 +21224,23 @@ function formatInstallType(type2) {
20724
21224
  return type2;
20725
21225
  }
20726
21226
  }
21227
+ function detectClawBrandFromDir(clawDir) {
21228
+ const dirName = import_node_path13.default.basename(clawDir).toLowerCase();
21229
+ if (dirName.includes("lightclaw")) {
21230
+ return "LightClaw";
21231
+ }
21232
+ return "OpenClaw";
21233
+ }
20727
21234
 
20728
21235
  // src/index.ts
20729
21236
  var program2 = new Command();
20730
- program2.name("soulhub").description("SoulHub CLI - Discover, install and manage AI agent souls").version("1.0.20").option("--verbose", "Enable verbose debug logging").hook("preAction", () => {
21237
+ program2.name("soulhub").description("SoulHub CLI - Discover, install and manage AI agent souls").version("1.0.22").option("--verbose", "Enable verbose debug logging").hook("preAction", () => {
20731
21238
  const opts = program2.opts();
20732
21239
  const verbose = opts.verbose || process.env.SOULHUB_DEBUG === "1";
20733
21240
  logger.init(verbose);
20734
21241
  logger.info("CLI started", {
20735
21242
  args: process.argv.slice(2),
20736
- version: "1.0.20",
21243
+ version: "1.0.22",
20737
21244
  node: process.version
20738
21245
  });
20739
21246
  });
@@ -20742,6 +21249,7 @@ program2.addCommand(infoCommand);
20742
21249
  program2.addCommand(installCommand);
20743
21250
  program2.addCommand(listCommand);
20744
21251
  program2.addCommand(updateCommand);
21252
+ program2.addCommand(uninstallCommand);
20745
21253
  program2.addCommand(rollbackCommand);
20746
21254
  program2.parse();
20747
21255
  /*! Bundled license information: