@myclaw163/clawclaw-cli 0.6.67 → 0.6.69

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 (54) hide show
  1. package/bin/clawclaw-cli.mjs +3 -3
  2. package/package.json +1 -1
  3. package/scripts/sync-bundled-skill.mjs +1 -1
  4. package/skills/clawclaw/references/COMMANDS.md +4 -4
  5. package/skills/clawclaw/references/KNOWLEDGE.md +14 -12
  6. package/src/commands/config.ts +30 -30
  7. package/src/commands/game.ts +15 -0
  8. package/src/commands/knowledge.test.ts +4 -10
  9. package/src/commands/knowledge.ts +10 -39
  10. package/src/commands/setup/hermes.test.ts +96 -96
  11. package/src/commands/setup/hermes.ts +76 -76
  12. package/src/commands/setup/index.ts +13 -13
  13. package/src/commands/setup/openclaw.test.ts +114 -114
  14. package/src/commands/setup/openclaw.ts +147 -147
  15. package/src/commands/watch.test.ts +11 -0
  16. package/src/commands/watch.ts +2 -3
  17. package/src/lib/auth.test.ts +15 -0
  18. package/src/lib/host-config-patcher.test.ts +130 -130
  19. package/src/lib/host-config-patcher.ts +151 -151
  20. package/src/lib/hub-reminder.ts +19 -19
  21. package/src/lib/knowledge-store.test.ts +28 -38
  22. package/src/lib/knowledge-store.ts +52 -57
  23. package/src/pipeline/event-format.test.ts +82 -2
  24. package/src/pipeline/event-format.ts +114 -5
  25. package/src/pipeline/event-hints.ts +20 -3
  26. package/src/runtime/event-daemon.test.ts +34 -0
  27. package/src/runtime/event-daemon.ts +51 -3
  28. package/src/sdk/index.ts +2 -3
  29. package/src/sdk/types.ts +2 -0
  30. package/src/strategies/avoid-players.knowledge.md +7 -8
  31. package/src/strategies/avoid-players.ts +1 -1
  32. package/src/strategies/corpse-patrol.ts +1 -1
  33. package/src/strategies/game-utils.test.ts +53 -1
  34. package/src/strategies/game-utils.ts +92 -28
  35. package/src/strategies/goals/avoid-players-top.ts +3 -3
  36. package/src/strategies/goals/corpse-patrol-top.ts +23 -1
  37. package/src/strategies/goals/crab-octopus-reflexes.ts +11 -3
  38. package/src/strategies/goals/keep-away-goal.ts +9 -5
  39. package/src/strategies/goals/leaf-goal.ts +2 -0
  40. package/src/strategies/goals/lone-kill-task-top.ts +58 -11
  41. package/src/strategies/goals/normal-shrimp-top.ts +11 -11
  42. package/src/strategies/goals/paradise-fish-top.ts +32 -15
  43. package/src/strategies/goals/warrior-shrimp-top.test.ts +4 -3
  44. package/src/strategies/goals/warrior-shrimp-top.ts +62 -295
  45. package/src/strategies/hide-spots.ts +11 -75
  46. package/src/strategies/kill-lone.knowledge.md +6 -9
  47. package/src/strategies/lone-kill-task.ts +1 -1
  48. package/src/strategies/off-route-points.ts +105 -0
  49. package/src/strategies/paradise-fish.knowledge.md +7 -8
  50. package/src/strategies/paradise-fish.ts +1 -1
  51. package/src/strategies/shrimp-memory.knowledge.md +7 -8
  52. package/src/strategies/shrimp-memory.ts +1 -1
  53. package/src/strategies/warrior-memory.knowledge.md +9 -10
  54. package/src/strategies/warrior-memory.ts +1 -1
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { register } from 'tsx/esm/api';
3
- register();
4
- await import('../src/cli.ts');
2
+ import { register } from 'tsx/esm/api';
3
+ register();
4
+ await import('../src/cli.ts');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myclaw163/clawclaw-cli",
3
- "version": "0.6.67",
3
+ "version": "0.6.69",
4
4
  "type": "module",
5
5
  "description": "ClawClaw social deduction game CLI",
6
6
  "bin": {
@@ -1,4 +1,4 @@
1
- import { spawnSync } from 'child_process';
1
+ import { spawnSync } from 'child_process';
2
2
  import { createHash } from 'crypto';
3
3
  import {
4
4
  cpSync,
@@ -67,10 +67,10 @@ ccl strategy --list # 列出所有可用策略
67
67
  ccl strategy --stop # 停止当前策略
68
68
  ccl strategy --info <id> # 查看策略的知识合约(该写入什么、何时)
69
69
 
70
- # 知识:向正在运行的策略推入判断——无需重启
71
- ccl knowledge mark 5 suspect --confidence 0.8 --note "电力房尾随" # 可疑:回避,不主动杀
72
- ccl knowledge mark 5 hostile --note "确认敌对" # 敌对:带刀可追杀
73
- ccl knowledge mark 3 trusted # 可信:不回避,绝不攻击
70
+ # 知识:向正在运行的策略推入判断——无需重启(只标坏人/好人,未标记 = 默认被怀疑)
71
+ ccl knowledge mark 5 hostile --note "确认敌对" # 坏人:带刀可追杀,无刀硬躲
72
+ ccl knowledge mark 3 trusted --note "不在场证明成立" # 好人:不回避,绝不攻击
73
+ ccl knowledge del player 5 # 取消标记 → 回到默认被怀疑
74
74
  ccl knowledge get
75
75
  ccl knowledge clear
76
76
 
@@ -2,16 +2,19 @@
2
2
 
3
3
  部分策略读取每局知识库,你通过 `ccl knowledge` 写入。这向**已在运行中**的策略推送判断而**无需重启**——策略在约 1–2 秒内就会读取到。运行 `ccl strategy --info <id>` 查看策略是否消费知识以及应该写入什么。
4
4
 
5
- ## 快速操作
5
+ ## 玩家只有两档标记,其余默认「被怀疑」
6
+
7
+ 只需显式标记你确认的两类人——**坏人** `hostile` 和 **好人** `trusted`。**没被标记的所有人一律默认「被怀疑」**,不需要(也无法)手动标 suspect。也没有置信度参数。
6
8
 
7
9
  | 目标 | 命令 | 效果 |
8
10
  |------|------|------|
9
- | 标记为可疑 | `ccl knowledge mark <座位\|名字> suspect` | 所有记忆策略提高警惕并回避;不会仅凭怀疑主动出刀 |
10
- | 标记为敌对 | `ccl knowledge mark <座位\|名字> hostile` | 带刀记忆策略主动追杀;无刀策略回避 |
11
- | 标记为可信 | `ccl knowledge mark <座位\|名字> trusted` | 不回避,击杀策略绝不攻击 |
12
- | 撤回单条 | `ccl knowledge del player <x>` | 删除该条目 |
11
+ | 标记为坏人 | `ccl knowledge mark <座位\|名字> hostile` | 带刀记忆策略刀好时主动追杀;无刀策略硬回避 |
12
+ | 标记为好人 | `ccl knowledge mark <座位\|名字> trusted` | 不回避,击杀策略绝不攻击 |
13
+ | 取消标记 | `ccl knowledge del player <x>` | 删除该条目 → 回到默认「被怀疑」 |
13
14
  | 清空所有 | `ccl knowledge clear` | 重置本局所有知识 |
14
15
 
16
+ **被怀疑(默认,未标记)** = 记忆策略提高警惕:保持距离观察,不会仅凭怀疑主动出刀;带刀虾也只回避,绝不因为贴身追击或退无可退而自卫先手。要让某人升级为「见到就追杀/必躲」,把它标 `hostile`。
17
+
15
18
  ## 工作原理
16
19
 
17
20
  知识作用于当前对局,下一局自动失效(与其他比赛事实完全一致)。用它来让**特定玩家/房间判断**被**当前策略**执行;如果是整体目标变化则切换策略。扩展库和自定义策略也可以声明自己的知识合约——`ccl strategy --info <id>` 始终告诉你策略读取什么。
@@ -19,13 +22,12 @@
19
22
  ## 完整 CLI 参考
20
23
 
21
24
  ```bash
22
- # 策略三档标记
23
- ccl knowledge mark 5 suspect --confidence 0.7 --note "行为可疑"
24
- ccl knowledge mark 5 hostile --confidence 0.9 --note "确认敌对"
25
+ # 玩家两档标记(未标记 = 默认被怀疑;无置信度参数)
26
+ ccl knowledge mark 5 hostile --note "确认敌对"
25
27
  ccl knowledge mark 3 trusted --note "不在场证明成立"
26
28
 
27
29
  # 身份事实单独记录,不直接控制策略
28
- ccl knowledge set player 5 role octopus --confidence 0.9
30
+ ccl knowledge set player 5 role octopus --note "vent read"
29
31
 
30
32
  # 查询
31
33
  ccl knowledge get # 所有知识
@@ -33,11 +35,11 @@ ccl knowledge get player 5 # 单个对象
33
35
  ccl knowledge get player # 所有玩家
34
36
 
35
37
  # 删除
36
- ccl knowledge del player 5 # 移除整个玩家
37
- ccl knowledge del player 5 --tag avoid # 仅移除一个标签
38
+ ccl knowledge del player 5 # 移除整个玩家 → 回到默认被怀疑
39
+ ccl knowledge del player 5 --tag hostile # 仅移除一个标签
38
40
 
39
41
  # 清空
40
42
  ccl knowledge clear # 重置当前对局
41
43
  ```
42
44
 
43
- 旧命令 `ccl knowledge suspect ...` 与旧标签 `avoid / kill_if_armed / cleared / protected / do_not_kill` 仍可读取,便于兼容已有脚本;新代码和新提示统一使用 `mark suspect|hostile|trusted`。
45
+ 旧标签 `kill_if_armed hostile`、`cleared / protected / do_not_kill trusted` 在读取时仍兼容映射;旧的 `suspect / avoid` 标签与 `ccl knowledge suspect` 命令已移除(未标记本身就是被怀疑)。
@@ -1,30 +1,30 @@
1
- import { Command } from 'commander';
2
- import { getWorkspaceDir } from '../lib/init-command.js';
3
- import { AuthStore } from '../lib/auth.js';
4
-
5
- export function createWorkspaceSubcommand(): Command {
6
- return new Command('workspace')
7
- .description('Print the workspace directory path')
8
- .action(() => {
9
- console.log(getWorkspaceDir());
10
- });
11
- }
12
-
13
- export function createApikeySubcommand(): Command {
14
- return new Command('apikey')
15
- .description('Print the active account API key')
16
- .action(() => {
17
- const store = new AuthStore();
18
- const profile = store.getActive();
19
- if (!profile) throw new Error('Not logged in. Run: clawclaw-cli account register');
20
- console.log(profile.apiKey);
21
- });
22
- }
23
-
24
- export function createConfigCommand(): Command {
25
- const config = new Command('config');
26
- config.description('Query ClawClaw CLI configuration.');
27
- config.addCommand(createWorkspaceSubcommand());
28
- config.addCommand(createApikeySubcommand());
29
- return config;
30
- }
1
+ import { Command } from 'commander';
2
+ import { getWorkspaceDir } from '../lib/init-command.js';
3
+ import { AuthStore } from '../lib/auth.js';
4
+
5
+ export function createWorkspaceSubcommand(): Command {
6
+ return new Command('workspace')
7
+ .description('Print the workspace directory path')
8
+ .action(() => {
9
+ console.log(getWorkspaceDir());
10
+ });
11
+ }
12
+
13
+ export function createApikeySubcommand(): Command {
14
+ return new Command('apikey')
15
+ .description('Print the active account API key')
16
+ .action(() => {
17
+ const store = new AuthStore();
18
+ const profile = store.getActive();
19
+ if (!profile) throw new Error('Not logged in. Run: clawclaw-cli account register');
20
+ console.log(profile.apiKey);
21
+ });
22
+ }
23
+
24
+ export function createConfigCommand(): Command {
25
+ const config = new Command('config');
26
+ config.description('Query ClawClaw CLI configuration.');
27
+ config.addCommand(createWorkspaceSubcommand());
28
+ config.addCommand(createApikeySubcommand());
29
+ return config;
30
+ }
@@ -45,6 +45,19 @@ function queueStatus(result: any): string | undefined {
45
45
 
46
46
  const DEFAULT_QUEUE_WAIT_TIMEOUT_SECS = 30;
47
47
 
48
+ function clearCachedGameServerUrl(authStore: AuthStore, profileName?: string): void {
49
+ try {
50
+ authStore.updateGameServerUrl(undefined, profileName);
51
+ } catch {
52
+ // Cache cleanup is best-effort; queue status will discover the current server.
53
+ }
54
+ }
55
+
56
+ function isLeaveGameSuccess(result: any): boolean {
57
+ const data = result?.data ?? result;
58
+ return data?.ok === true && data?.message === 'left_game';
59
+ }
60
+
48
61
  function isPidAlive(pid: number): boolean {
49
62
  if (!Number.isFinite(pid) || pid <= 0 || pid === process.pid) return false;
50
63
  try { process.kill(pid, 0); return true; } catch { return false; }
@@ -366,6 +379,7 @@ async function runGameStart(opts: { watch: boolean; force?: boolean }): Promise<
366
379
 
367
380
  const stateDir = getProfileStateDir(profile);
368
381
  const feedPath = join(stateDir, 'feed.json');
382
+ clearCachedGameServerUrl(authStore, profile.agentName);
369
383
  const client = GameClient.fromAuth();
370
384
  let eventRuntime: EventRuntime | undefined;
371
385
  let streamAbortController: AbortController | null = null;
@@ -957,6 +971,7 @@ export function createGameCommand(): Command {
957
971
  } catch (err: any) {
958
972
  result = { error: err?.message ?? String(err) };
959
973
  }
974
+ if (isLeaveGameSuccess(result)) clearCachedGameServerUrl(authStore, profile.agentName);
960
975
  const stateDir = getProfileStateDir(profile);
961
976
  endMatch(stateDir);
962
977
  const owner = await stopOwnerWithCommand(stateDir, 'quit');
@@ -1,19 +1,13 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { normalizeLegacySuspectVerdict, normalizePlayerMark } from './knowledge.js';
2
+ import { normalizePlayerMark } from './knowledge.js';
3
3
 
4
4
  describe('knowledge player marks', () => {
5
- it('accepts only the three canonical mark names', () => {
6
- expect(normalizePlayerMark('suspect')).toBe('suspect');
5
+ it('accepts only the two canonical mark names', () => {
7
6
  expect(normalizePlayerMark('hostile')).toBe('hostile');
8
7
  expect(normalizePlayerMark('trusted')).toBe('trusted');
8
+ // suspect 不再是可设标记:未标记 = 默认被怀疑。
9
+ expect(normalizePlayerMark('suspect')).toBeNull();
9
10
  expect(normalizePlayerMark('impostor')).toBeNull();
10
11
  expect(normalizePlayerMark('avoid')).toBeNull();
11
12
  });
12
-
13
- it('keeps old suspect command verdicts as compatibility aliases', () => {
14
- expect(normalizeLegacySuspectVerdict()).toBe('suspect');
15
- expect(normalizeLegacySuspectVerdict('impostor')).toBe('hostile');
16
- expect(normalizeLegacySuspectVerdict('cleared')).toBe('trusted');
17
- expect(normalizeLegacySuspectVerdict('typo')).toBeNull();
18
- });
19
13
  });
@@ -1,24 +1,15 @@
1
1
  import { Command } from 'commander';
2
- import { KnowledgeStore, type KnowledgeSetInput, type PlayerMark } from '../lib/knowledge-store.js';
2
+ import { DROPPED_PLAYER_MARK_TAGS, KnowledgeStore, type KnowledgeSetInput, type PlayerMark } from '../lib/knowledge-store.js';
3
3
 
4
- const PLAYER_MARKS = new Set<PlayerMark>(['suspect', 'hostile', 'trusted']);
5
- const LEGACY_MARK_TAGS = ['avoid', 'kill_if_armed', 'cleared', 'protected', 'do_not_kill'];
6
-
7
- const LEGACY_SUSPECT_VERDICTS: Record<string, PlayerMark> = {
8
- suspect: 'suspect', suspicious: 'suspect', avoid: 'suspect',
9
- hostile: 'hostile', impostor: 'hostile', bad: 'hostile',
10
- trusted: 'trusted', cleared: 'trusted', clear: 'trusted', innocent: 'trusted', safe: 'trusted',
11
- };
4
+ // 玩家只标两档:hostile(坏人)/ trusted(好人)。未标记的默认「被怀疑」,无需也无法显式标。
5
+ const PLAYER_MARKS = new Set<PlayerMark>(['hostile', 'trusted']);
6
+ const LEGACY_MARK_TAGS = ['kill_if_armed', 'cleared', 'protected', 'do_not_kill'];
12
7
 
13
8
  export function normalizePlayerMark(mark?: string): PlayerMark | null {
14
9
  const normalized = mark?.trim().toLowerCase() as PlayerMark | undefined;
15
10
  return normalized && PLAYER_MARKS.has(normalized) ? normalized : null;
16
11
  }
17
12
 
18
- export function normalizeLegacySuspectVerdict(verdict?: string): PlayerMark | null {
19
- return LEGACY_SUSPECT_VERDICTS[(verdict ?? 'suspect').trim().toLowerCase()] ?? null;
20
- }
21
-
22
13
  /** "true"/"false"/数字 自动转类型,其余按字符串。 */
23
14
  function coerceValue(raw: string | undefined): unknown {
24
15
  if (raw == null) return true;
@@ -41,17 +32,17 @@ function fail(error: string, message: string): never {
41
32
  process.exit(1);
42
33
  }
43
34
 
44
- function setPlayerMark(store: KnowledgeStore, player: string, mark: PlayerMark, opts: { confidence?: number; note?: string }) {
35
+ function setPlayerMark(store: KnowledgeStore, player: string, mark: PlayerMark, opts: { note?: string }) {
45
36
  const removeTags = [
46
37
  ...[...PLAYER_MARKS].filter(candidate => candidate !== mark),
47
38
  ...LEGACY_MARK_TAGS,
39
+ ...DROPPED_PLAYER_MARK_TAGS, // 顺手清掉旧数据残留的 suspect/avoid raw tag,别再从 get/subject() 漏出去
48
40
  ];
49
41
  return store.set({
50
42
  type: 'player',
51
43
  selector: player,
52
44
  tags: [mark],
53
45
  removeTags,
54
- confidence: opts.confidence,
55
46
  note: opts.note,
56
47
  });
57
48
  }
@@ -65,11 +56,10 @@ export function createKnowledgeCommand(): Command {
65
56
  .command('set [type] [selector] [key] [value]')
66
57
  .description('Advanced: upsert arbitrary facts/tags. Use `knowledge mark` for player strategy behavior.')
67
58
  .option('--tag <tag>', 'Custom tag, repeatable', collectTag, [])
68
- .option('--confidence <n>', 'Confidence 0..1', parseFloat)
69
59
  .option('--note <text>', 'Free-text reason')
70
60
  .option('--json <json>', 'Provide the whole input object as JSON instead of positional args')
71
61
  .action((type: string | undefined, selector: string | undefined, key: string | undefined, value: string | undefined, opts: {
72
- tag: string[]; confidence?: number; note?: string; json?: string;
62
+ tag: string[]; note?: string; json?: string;
73
63
  }) => {
74
64
  try {
75
65
  const store = KnowledgeStore.forActiveAccount();
@@ -86,7 +76,6 @@ export function createKnowledgeCommand(): Command {
86
76
  key,
87
77
  value: key != null ? coerceValue(value) : undefined,
88
78
  tags: opts.tag.length > 0 ? opts.tag : undefined,
89
- confidence: opts.confidence,
90
79
  note: opts.note,
91
80
  };
92
81
  }
@@ -99,12 +88,11 @@ export function createKnowledgeCommand(): Command {
99
88
 
100
89
  cmd
101
90
  .command('mark <player> <mark>')
102
- .description('Set the strategy mark for a player: suspect | hostile | trusted')
103
- .option('--confidence <n>', 'Confidence 0..1', parseFloat)
91
+ .description('Set the strategy mark for a player: hostile | trusted (unmarked players are suspected by default)')
104
92
  .option('--note <text>', 'Free-text reason')
105
- .action((player: string, rawMark: string, opts: { confidence?: number; note?: string }) => {
93
+ .action((player: string, rawMark: string, opts: { note?: string }) => {
106
94
  const mark = normalizePlayerMark(rawMark);
107
- if (!mark) fail('invalid_mark', `Unknown mark '${rawMark}'. Use: suspect | hostile | trusted.`);
95
+ if (!mark) fail('invalid_mark', `Unknown mark '${rawMark}'. Use: hostile | trusted. (Unmarked = suspected by default.)`);
108
96
  try {
109
97
  const store = KnowledgeStore.forActiveAccount();
110
98
  const entry = setPlayerMark(store, player, mark, opts);
@@ -114,23 +102,6 @@ export function createKnowledgeCommand(): Command {
114
102
  }
115
103
  });
116
104
 
117
- cmd
118
- .command('suspect <player> [verdict]')
119
- .description('Legacy alias for `knowledge mark`: suspect(default) | impostor/hostile | cleared/trusted')
120
- .option('--confidence <n>', 'Confidence 0..1', parseFloat)
121
- .option('--note <text>', 'Free-text reason')
122
- .action((player: string, verdict: string | undefined, opts: { confidence?: number; note?: string }) => {
123
- const mark = normalizeLegacySuspectVerdict(verdict);
124
- if (!mark) fail('invalid_verdict', `Unknown verdict '${verdict}'. Prefer: ccl knowledge mark <player> suspect|hostile|trusted.`);
125
- try {
126
- const store = KnowledgeStore.forActiveAccount();
127
- const entry = setPlayerMark(store, player, mark, opts);
128
- print({ ok: true, gameId: store.gameId ?? null, mark, subject: entry, deprecated_command: 'suspect' });
129
- } catch (e) {
130
- fail('knowledge_set_failed', e instanceof Error ? e.message : String(e));
131
- }
132
- });
133
-
134
105
  cmd
135
106
  .command('get [type] [selector]')
136
107
  .description('Read knowledge: all, all of a type, or one subject')
@@ -1,96 +1,96 @@
1
- import { describe, expect, it, vi, beforeEach } from 'vitest';
2
- import { runHermesSetup } from './hermes.js';
3
- import { spawnSync } from 'child_process';
4
-
5
- vi.mock('child_process', () => ({
6
- spawnSync: vi.fn(),
7
- }));
8
-
9
- const mockedSpawnSync = vi.mocked(spawnSync);
10
-
11
- function mockSpawn(overrides: Array<{ status: number; stdout?: string; stderr?: string }>) {
12
- mockedSpawnSync.mockImplementation((_cmd: any, _args?: any, _opts?: any): any => {
13
- const next = overrides.shift();
14
- if (!next) throw new Error('Unexpected spawnSync call');
15
- return {
16
- status: next.status,
17
- stdout: next.stdout ?? '',
18
- stderr: next.stderr ?? '',
19
- };
20
- });
21
- }
22
-
23
- beforeEach(() => {
24
- vi.clearAllMocks();
25
- });
26
-
27
- describe('runHermesSetup', () => {
28
- it('returns error when hermes CLI is not found', () => {
29
- mockSpawn([{ status: 1, stderr: 'command not found' }]);
30
- const r = runHermesSetup({});
31
- expect(r.exitCode).toBe(1);
32
- expect(r.output.some((l) => l.includes('not found'))).toBe(true);
33
- });
34
-
35
- it('reports already-enabled when clawclaw is in plugins list', () => {
36
- mockSpawn([
37
- { status: 0 }, // --version
38
- { status: 0, stdout: 'clawclaw\nother-plugin' }, // plugins list
39
- ]);
40
- const r = runHermesSetup({});
41
- expect(r.exitCode).toBe(0);
42
- expect(r.output.some((l) => l.includes('already enabled'))).toBe(true);
43
- });
44
-
45
- it('dry-run shows pending message when not enabled', () => {
46
- mockSpawn([
47
- { status: 0 }, // --version
48
- { status: 0, stdout: 'other-plugin' }, // plugins list
49
- ]);
50
- const r = runHermesSetup({});
51
- expect(r.exitCode).toBe(2);
52
- expect(r.output.some((l) => l.includes('Dry-run'))).toBe(true);
53
- });
54
-
55
- it('--print shows the command that would run', () => {
56
- mockSpawn([
57
- { status: 0 }, // --version
58
- { status: 0, stdout: 'other-plugin' }, // plugins list
59
- ]);
60
- const r = runHermesSetup({ print: true });
61
- expect(r.exitCode).toBe(0);
62
- expect(r.output.some((l) => l.includes('hermes plugins enable clawclaw'))).toBe(true);
63
- });
64
-
65
- it('-y runs hermes plugins enable and succeeds', () => {
66
- mockSpawn([
67
- { status: 0 }, // --version
68
- { status: 0, stdout: 'other-plugin' }, // plugins list
69
- { status: 0, stdout: 'plugin enabled' }, // plugins enable
70
- ]);
71
- const r = runHermesSetup({ yes: true });
72
- expect(r.exitCode).toBe(0);
73
- expect(r.output.some((l) => l.includes('enabled'))).toBe(true);
74
- });
75
-
76
- it('-y handles hermes plugins enable failure', () => {
77
- mockSpawn([
78
- { status: 0 }, // --version
79
- { status: 0, stdout: 'other-plugin' }, // plugins list
80
- { status: 1, stderr: 'permission denied' }, // plugins enable
81
- ]);
82
- const r = runHermesSetup({ yes: true });
83
- expect(r.exitCode).toBe(1);
84
- expect(r.output.some((l) => l.includes('Failed'))).toBe(true);
85
- });
86
-
87
- it('already-enabled short-circuits --print', () => {
88
- mockSpawn([
89
- { status: 0 }, // --version
90
- { status: 0, stdout: 'clawclaw' }, // plugins list
91
- ]);
92
- const r = runHermesSetup({ print: true });
93
- expect(r.exitCode).toBe(0);
94
- expect(r.output.some((l) => l.includes('already enabled'))).toBe(true);
95
- });
96
- });
1
+ import { describe, expect, it, vi, beforeEach } from 'vitest';
2
+ import { runHermesSetup } from './hermes.js';
3
+ import { spawnSync } from 'child_process';
4
+
5
+ vi.mock('child_process', () => ({
6
+ spawnSync: vi.fn(),
7
+ }));
8
+
9
+ const mockedSpawnSync = vi.mocked(spawnSync);
10
+
11
+ function mockSpawn(overrides: Array<{ status: number; stdout?: string; stderr?: string }>) {
12
+ mockedSpawnSync.mockImplementation((_cmd: any, _args?: any, _opts?: any): any => {
13
+ const next = overrides.shift();
14
+ if (!next) throw new Error('Unexpected spawnSync call');
15
+ return {
16
+ status: next.status,
17
+ stdout: next.stdout ?? '',
18
+ stderr: next.stderr ?? '',
19
+ };
20
+ });
21
+ }
22
+
23
+ beforeEach(() => {
24
+ vi.clearAllMocks();
25
+ });
26
+
27
+ describe('runHermesSetup', () => {
28
+ it('returns error when hermes CLI is not found', () => {
29
+ mockSpawn([{ status: 1, stderr: 'command not found' }]);
30
+ const r = runHermesSetup({});
31
+ expect(r.exitCode).toBe(1);
32
+ expect(r.output.some((l) => l.includes('not found'))).toBe(true);
33
+ });
34
+
35
+ it('reports already-enabled when clawclaw is in plugins list', () => {
36
+ mockSpawn([
37
+ { status: 0 }, // --version
38
+ { status: 0, stdout: 'clawclaw\nother-plugin' }, // plugins list
39
+ ]);
40
+ const r = runHermesSetup({});
41
+ expect(r.exitCode).toBe(0);
42
+ expect(r.output.some((l) => l.includes('already enabled'))).toBe(true);
43
+ });
44
+
45
+ it('dry-run shows pending message when not enabled', () => {
46
+ mockSpawn([
47
+ { status: 0 }, // --version
48
+ { status: 0, stdout: 'other-plugin' }, // plugins list
49
+ ]);
50
+ const r = runHermesSetup({});
51
+ expect(r.exitCode).toBe(2);
52
+ expect(r.output.some((l) => l.includes('Dry-run'))).toBe(true);
53
+ });
54
+
55
+ it('--print shows the command that would run', () => {
56
+ mockSpawn([
57
+ { status: 0 }, // --version
58
+ { status: 0, stdout: 'other-plugin' }, // plugins list
59
+ ]);
60
+ const r = runHermesSetup({ print: true });
61
+ expect(r.exitCode).toBe(0);
62
+ expect(r.output.some((l) => l.includes('hermes plugins enable clawclaw'))).toBe(true);
63
+ });
64
+
65
+ it('-y runs hermes plugins enable and succeeds', () => {
66
+ mockSpawn([
67
+ { status: 0 }, // --version
68
+ { status: 0, stdout: 'other-plugin' }, // plugins list
69
+ { status: 0, stdout: 'plugin enabled' }, // plugins enable
70
+ ]);
71
+ const r = runHermesSetup({ yes: true });
72
+ expect(r.exitCode).toBe(0);
73
+ expect(r.output.some((l) => l.includes('enabled'))).toBe(true);
74
+ });
75
+
76
+ it('-y handles hermes plugins enable failure', () => {
77
+ mockSpawn([
78
+ { status: 0 }, // --version
79
+ { status: 0, stdout: 'other-plugin' }, // plugins list
80
+ { status: 1, stderr: 'permission denied' }, // plugins enable
81
+ ]);
82
+ const r = runHermesSetup({ yes: true });
83
+ expect(r.exitCode).toBe(1);
84
+ expect(r.output.some((l) => l.includes('Failed'))).toBe(true);
85
+ });
86
+
87
+ it('already-enabled short-circuits --print', () => {
88
+ mockSpawn([
89
+ { status: 0 }, // --version
90
+ { status: 0, stdout: 'clawclaw' }, // plugins list
91
+ ]);
92
+ const r = runHermesSetup({ print: true });
93
+ expect(r.exitCode).toBe(0);
94
+ expect(r.output.some((l) => l.includes('already enabled'))).toBe(true);
95
+ });
96
+ });