nfo-cli 0.0.3 → 0.0.5

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 (173) hide show
  1. package/dist/claude-command.js +6 -1
  2. package/dist/claude-command.js.map +1 -1
  3. package/dist/claude-trust.js +46 -0
  4. package/dist/claude-trust.js.map +1 -0
  5. package/dist/cli.js +64 -54
  6. package/dist/cli.js.map +1 -1
  7. package/dist/commands/restore.js +0 -1
  8. package/dist/commands/restore.js.map +1 -1
  9. package/dist/commands/tui.js +6 -4
  10. package/dist/commands/tui.js.map +1 -1
  11. package/dist/mcp/handlers.js +5 -0
  12. package/dist/mcp/handlers.js.map +1 -1
  13. package/dist/mcp/tool-defs.js +10 -0
  14. package/dist/mcp/tool-defs.js.map +1 -1
  15. package/dist/musicians/dismiss.js +1 -1
  16. package/dist/musicians/dismiss.js.map +1 -1
  17. package/dist/musicians/roles.js +15 -0
  18. package/dist/musicians/roles.js.map +1 -0
  19. package/dist/musicians/spawn.js +53 -18
  20. package/dist/musicians/spawn.js.map +1 -1
  21. package/dist/permission.js +14 -8
  22. package/dist/permission.js.map +1 -1
  23. package/dist/prompts/musician-role.js +2 -1
  24. package/dist/prompts/musician-role.js.map +1 -1
  25. package/dist/prompts/orchestrator-role.js +42 -8
  26. package/dist/prompts/orchestrator-role.js.map +1 -1
  27. package/dist/prompts/tool-discipline.js +10 -0
  28. package/dist/prompts/tool-discipline.js.map +1 -1
  29. package/dist/tui/{App.js → components/App.js} +20 -20
  30. package/dist/tui/components/App.js.map +1 -0
  31. package/dist/tui/components/AppView.js +13 -0
  32. package/dist/tui/components/AppView.js.map +1 -0
  33. package/dist/tui/{Auditorium.js → components/Auditorium.js} +2 -2
  34. package/dist/tui/components/Auditorium.js.map +1 -0
  35. package/dist/tui/components/ConcertHall.js.map +1 -0
  36. package/dist/tui/{Help.js → components/Help.js} +0 -8
  37. package/dist/tui/components/Help.js.map +1 -0
  38. package/dist/tui/components/OrchestratorPane.js.map +1 -0
  39. package/dist/tui/components/SidebarHeader.js +6 -0
  40. package/dist/tui/components/SidebarHeader.js.map +1 -0
  41. package/dist/tui/{StatusBar.js → components/StatusBar.js} +1 -1
  42. package/dist/tui/components/StatusBar.js.map +1 -0
  43. package/package.json +8 -1
  44. package/assets/agent-screen.png +0 -0
  45. package/assets/main-screen.png +0 -0
  46. package/assets/orche-clawd.png +0 -0
  47. package/dist/tui/App.js.map +0 -1
  48. package/dist/tui/AppView.js +0 -13
  49. package/dist/tui/AppView.js.map +0 -1
  50. package/dist/tui/Auditorium.js.map +0 -1
  51. package/dist/tui/ConcertHall.js.map +0 -1
  52. package/dist/tui/Help.js.map +0 -1
  53. package/dist/tui/OrchestratorPane.js.map +0 -1
  54. package/dist/tui/SidebarHeader.js +0 -6
  55. package/dist/tui/SidebarHeader.js.map +0 -1
  56. package/dist/tui/StatusBar.js.map +0 -1
  57. package/docs/plans/2026-05-29-nfo-phase-1-bootstrap.md +0 -2152
  58. package/docs/plans/2026-05-29-nfo-phase-2-mcp-musicians.md +0 -2467
  59. package/docs/plans/2026-05-29-nfo-phase-3-ink-tui.md +0 -1611
  60. package/docs/plans/2026-05-29-nfo-phase-4-permission-prompts.md +0 -460
  61. package/docs/plans/2026-05-29-nfo-phase-5-help-and-notify.md +0 -933
  62. package/docs/specs/2026-05-29-nfo-design.md +0 -468
  63. package/src/claude-command.ts +0 -35
  64. package/src/claude-detect.ts +0 -42
  65. package/src/cli.ts +0 -164
  66. package/src/commands/attach.ts +0 -24
  67. package/src/commands/dashboard-window.ts +0 -33
  68. package/src/commands/kill.ts +0 -50
  69. package/src/commands/launch.ts +0 -134
  70. package/src/commands/list.ts +0 -43
  71. package/src/commands/mcp-server.ts +0 -18
  72. package/src/commands/notes.ts +0 -18
  73. package/src/commands/restore.ts +0 -153
  74. package/src/commands/tui.tsx +0 -16
  75. package/src/config.ts +0 -44
  76. package/src/dashboard.ts +0 -1
  77. package/src/mcp/config.ts +0 -39
  78. package/src/mcp/handlers.ts +0 -141
  79. package/src/mcp/server.ts +0 -50
  80. package/src/mcp/tool-defs.ts +0 -151
  81. package/src/musicians/dismiss.ts +0 -60
  82. package/src/musicians/ids.ts +0 -21
  83. package/src/musicians/lookup.ts +0 -13
  84. package/src/musicians/message-log.ts +0 -152
  85. package/src/musicians/message.ts +0 -99
  86. package/src/musicians/query.ts +0 -19
  87. package/src/musicians/spawn.ts +0 -139
  88. package/src/notes.ts +0 -39
  89. package/src/notify.ts +0 -62
  90. package/src/orchestrator/report-back.ts +0 -33
  91. package/src/permission.ts +0 -30
  92. package/src/project-key.ts +0 -12
  93. package/src/prompts/musician-role.ts +0 -22
  94. package/src/prompts/orchestrator-role.ts +0 -60
  95. package/src/prompts/tool-discipline.ts +0 -35
  96. package/src/repo.ts +0 -14
  97. package/src/shell-quote.ts +0 -7
  98. package/src/state-updaters.ts +0 -132
  99. package/src/state.ts +0 -49
  100. package/src/state.types.ts +0 -67
  101. package/src/tmux.ts +0 -226
  102. package/src/tui/App.tsx +0 -532
  103. package/src/tui/AppView.tsx +0 -96
  104. package/src/tui/Auditorium.tsx +0 -56
  105. package/src/tui/ConcertHall.tsx +0 -31
  106. package/src/tui/Help.tsx +0 -72
  107. package/src/tui/OrchestratorPane.tsx +0 -98
  108. package/src/tui/SidebarHeader.tsx +0 -32
  109. package/src/tui/StatusBar.tsx +0 -44
  110. package/src/tui/activity-line.ts +0 -16
  111. package/src/tui/detect-permission.ts +0 -93
  112. package/src/tui/embedded-session-lifecycle.ts +0 -44
  113. package/src/tui/embedded-terminal.ts +0 -325
  114. package/src/tui/format-time.ts +0 -25
  115. package/src/tui/keymap.ts +0 -104
  116. package/src/tui/poll-activity.ts +0 -25
  117. package/src/tui/poll-idle.ts +0 -149
  118. package/src/tui/poll-permission.ts +0 -50
  119. package/src/tui/status-icon.ts +0 -35
  120. package/src/tui/terminal-input.ts +0 -136
  121. package/src/tui/watch-state.ts +0 -43
  122. package/src/worktree.ts +0 -41
  123. package/tests/claude-command.test.ts +0 -30
  124. package/tests/claude-detect.test.ts +0 -14
  125. package/tests/commands/attach.test.ts +0 -60
  126. package/tests/commands/kill.test.ts +0 -66
  127. package/tests/commands/launch.test.ts +0 -75
  128. package/tests/commands/list.test.ts +0 -47
  129. package/tests/commands/notes.test.ts +0 -53
  130. package/tests/commands/restore.test.ts +0 -126
  131. package/tests/helpers/tmp-config.ts +0 -16
  132. package/tests/helpers/tmp-repo.ts +0 -29
  133. package/tests/integration/orchestrator-spawn.test.ts +0 -108
  134. package/tests/mcp/handlers.test.ts +0 -163
  135. package/tests/mcp/tool-defs.test.ts +0 -35
  136. package/tests/musicians/dismiss.test.ts +0 -102
  137. package/tests/musicians/message.test.ts +0 -159
  138. package/tests/musicians/query.test.ts +0 -65
  139. package/tests/musicians/spawn.test.ts +0 -125
  140. package/tests/notes.test.ts +0 -56
  141. package/tests/notify.test.ts +0 -80
  142. package/tests/orchestrator/report-back.test.ts +0 -18
  143. package/tests/permission.test.ts +0 -29
  144. package/tests/project-key.test.ts +0 -33
  145. package/tests/prompts/tool-discipline.test.ts +0 -25
  146. package/tests/repo.test.ts +0 -38
  147. package/tests/state-updaters.test.ts +0 -126
  148. package/tests/state.test.ts +0 -85
  149. package/tests/tmux.test.ts +0 -126
  150. package/tests/tui/AppView.test.tsx +0 -92
  151. package/tests/tui/Auditorium.test.tsx +0 -67
  152. package/tests/tui/ConcertHall.test.tsx +0 -22
  153. package/tests/tui/Help.test.tsx +0 -38
  154. package/tests/tui/OrchestratorPane.test.ts +0 -30
  155. package/tests/tui/SidebarHeader.test.tsx +0 -20
  156. package/tests/tui/StatusBar.test.tsx +0 -51
  157. package/tests/tui/activity-line.test.ts +0 -21
  158. package/tests/tui/detect-permission.test.ts +0 -92
  159. package/tests/tui/embedded-session-lifecycle.test.ts +0 -55
  160. package/tests/tui/embedded-terminal.test.ts +0 -80
  161. package/tests/tui/format-time.test.ts +0 -25
  162. package/tests/tui/keymap.test.ts +0 -93
  163. package/tests/tui/poll-activity.test.ts +0 -81
  164. package/tests/tui/poll-idle.test.ts +0 -159
  165. package/tests/tui/poll-permission.test.ts +0 -222
  166. package/tests/tui/status-icon.test.ts +0 -27
  167. package/tests/tui/terminal-input.test.ts +0 -113
  168. package/tests/tui/watch-state.test.ts +0 -54
  169. package/tests/worktree.test.ts +0 -73
  170. package/tsconfig.json +0 -19
  171. package/vitest.config.ts +0 -12
  172. /package/dist/tui/{ConcertHall.js → components/ConcertHall.js} +0 -0
  173. /package/dist/tui/{OrchestratorPane.js → components/OrchestratorPane.js} +0 -0
@@ -1,222 +0,0 @@
1
- import { describe, it, expect, afterEach, beforeEach } from 'vitest';
2
- import { pollPermissions } from '../../src/tui/poll-permission.js';
3
- import { makeTmpRepo, type TmpRepo } from '../helpers/tmp-repo.js';
4
- import { makeTmpConfig } from '../helpers/tmp-config.js';
5
- import { ensureOrchestraDir, writeState } from '../../src/state.js';
6
- import { makeInitialState } from '../../src/state.types.js';
7
- import { projectKeyFromPath } from '../../src/project-key.js';
8
- import { createDetachedSession, sessionName, killSession } from '../../src/tmux.js';
9
- import { execa } from 'execa';
10
- import type { OrchestraState } from '../../src/state.types.js';
11
-
12
- // A realistic claude permission-prompt block that satisfies all three detector signals:
13
- // 1. An intro line matching /allow\s+\S+/i
14
- // 2. A numbered choice starting with "1." (yes-line)
15
- // 3. A numbered choice starting with "3." containing "No" (no-line)
16
- const PERMISSION_PROMPT_TEXT = [
17
- 'Allow Bash to run `ls`?',
18
- '',
19
- ' 1. Yes',
20
- ' 2. Yes, and don\'t ask again for Bash commands',
21
- ' 3. No, and tell Claude what to do differently',
22
- '',
23
- '❯ 1',
24
- ].join('\n');
25
-
26
- describe('pollPermissions', () => {
27
- const cleanups: Array<() => Promise<void>> = [];
28
- const sessionsToKill: string[] = [];
29
-
30
- beforeEach(() => { process.env.NFO_HOME = ''; });
31
-
32
- afterEach(async () => {
33
- for (const s of sessionsToKill) {
34
- try { await killSession(s); } catch { /* ignore */ }
35
- }
36
- sessionsToKill.length = 0;
37
- for (const c of cleanups) { await c(); }
38
- cleanups.length = 0;
39
- delete process.env.NFO_HOME;
40
- });
41
-
42
- // Helper: build a minimal OrchestraState with a single musician fixture.
43
- function makeStateWithMusician(
44
- orchId: string,
45
- projectPath: string,
46
- musicianStatus: OrchestraState['musicians'][number]['status'],
47
- windowId: string,
48
- ): OrchestraState {
49
- const base = makeInitialState({ orchestraId: orchId, projectPath, permissionLevel: 'supervised' });
50
- base.musicians.push({
51
- id: 'mus-001',
52
- name: 'x',
53
- task_summary: 't',
54
- status: musicianStatus,
55
- pending_permission: null,
56
- tmux_window_id: windowId,
57
- claude_session_id: null,
58
- worktree_path: null,
59
- branch: null,
60
- spawned_at: '2026-05-29T10:00:00Z',
61
- last_activity: '2026-05-29T10:00:00Z',
62
- });
63
- return base;
64
- }
65
-
66
- it('transitions working → awaiting_permission when pane shows a permission prompt', async () => {
67
- const cfg = await makeTmpConfig();
68
- cleanups.push(cfg.cleanup);
69
- process.env.NFO_HOME = cfg.path;
70
-
71
- const repo: TmpRepo = await makeTmpRepo();
72
- cleanups.push(repo.cleanup);
73
-
74
- const orchId = projectKeyFromPath(repo.path);
75
- await ensureOrchestraDir(orchId);
76
-
77
- const sess = sessionName(orchId);
78
- sessionsToKill.push(sess);
79
- await createDetachedSession(sess, repo.path, 220, 50);
80
-
81
- // Create a new window and capture its id.
82
- const { stdout: winIdRaw } = await execa('tmux', [
83
- 'new-window', '-t', sess, '-n', 'mus-001-perm', '-c', repo.path, '-d',
84
- '-P', '-F', '#{window_id}',
85
- ]);
86
- const winId = winIdRaw.trim();
87
-
88
- // Write the permission-prompt text into the pane using printf (literal, no Enter needed).
89
- await execa('tmux', ['send-keys', '-l', '-t', `${sess}:${winId}`, '--', PERMISSION_PROMPT_TEXT]);
90
- // Give tmux time to render the output.
91
- await new Promise((r) => { setTimeout(r, 250); });
92
-
93
- const state = makeStateWithMusician(orchId, repo.path, 'working', winId);
94
- await writeState(orchId, state);
95
-
96
- const transitions = await pollPermissions(state);
97
-
98
- expect(transitions).toHaveLength(1);
99
- expect(transitions[0].musicianId).toBe('mus-001');
100
- expect(transitions[0].newStatus).toBe('awaiting_permission');
101
- expect(transitions[0].pendingPermission).not.toBeNull();
102
- expect(transitions[0].pendingPermission!.startsWith('Bash')).toBe(true);
103
- });
104
-
105
- it('transitions awaiting_permission → working when pane is cleared', async () => {
106
- const cfg = await makeTmpConfig();
107
- cleanups.push(cfg.cleanup);
108
- process.env.NFO_HOME = cfg.path;
109
-
110
- const repo: TmpRepo = await makeTmpRepo();
111
- cleanups.push(repo.cleanup);
112
-
113
- const orchId = projectKeyFromPath(repo.path);
114
- await ensureOrchestraDir(orchId);
115
-
116
- const sess = sessionName(orchId);
117
- sessionsToKill.push(sess);
118
- await createDetachedSession(sess, repo.path, 220, 50);
119
-
120
- const { stdout: winIdRaw } = await execa('tmux', [
121
- 'new-window', '-t', sess, '-n', 'mus-001-clear', '-c', repo.path, '-d',
122
- '-P', '-F', '#{window_id}',
123
- ]);
124
- const winId = winIdRaw.trim();
125
-
126
- // Clear the pane so no permission-prompt signals are present.
127
- await execa('tmux', ['send-keys', '-t', `${sess}:${winId}`, 'clear', 'Enter']);
128
- await new Promise((r) => { setTimeout(r, 250); });
129
-
130
- // Musician is currently marked awaiting_permission but pane is clean.
131
- const state = makeStateWithMusician(orchId, repo.path, 'awaiting_permission', winId);
132
- await writeState(orchId, state);
133
-
134
- const transitions = await pollPermissions(state);
135
-
136
- expect(transitions).toHaveLength(1);
137
- expect(transitions[0].musicianId).toBe('mus-001');
138
- expect(transitions[0].newStatus).toBe('working');
139
- expect(transitions[0].pendingPermission).toBeNull();
140
- });
141
-
142
- it('emits no transition for a stopped musician', async () => {
143
- const cfg = await makeTmpConfig();
144
- cleanups.push(cfg.cleanup);
145
- process.env.NFO_HOME = cfg.path;
146
-
147
- const repo: TmpRepo = await makeTmpRepo();
148
- cleanups.push(repo.cleanup);
149
-
150
- const orchId = projectKeyFromPath(repo.path);
151
- await ensureOrchestraDir(orchId);
152
-
153
- // No real session needed — stopped musicians are skipped before any I/O.
154
- const state = makeStateWithMusician(orchId, repo.path, 'stopped', '@0');
155
- await writeState(orchId, state);
156
-
157
- const transitions = await pollPermissions(state);
158
-
159
- expect(transitions).toHaveLength(0);
160
- });
161
-
162
- it('swallows errors for a non-existent window and emits no transition', async () => {
163
- const cfg = await makeTmpConfig();
164
- cleanups.push(cfg.cleanup);
165
- process.env.NFO_HOME = cfg.path;
166
-
167
- const repo: TmpRepo = await makeTmpRepo();
168
- cleanups.push(repo.cleanup);
169
-
170
- const orchId = projectKeyFromPath(repo.path);
171
- await ensureOrchestraDir(orchId);
172
-
173
- // Musician points at a window id that will never exist.
174
- const state = makeStateWithMusician(orchId, repo.path, 'working', '@9999');
175
- await writeState(orchId, state);
176
-
177
- let thrown = false;
178
- let transitions: Awaited<ReturnType<typeof pollPermissions>> = [];
179
- try {
180
- transitions = await pollPermissions(state);
181
- } catch {
182
- thrown = true;
183
- }
184
-
185
- expect(thrown).toBe(false);
186
- expect(transitions).toHaveLength(0);
187
- });
188
-
189
- it('emits no transition when state already matches (awaiting + prompt still visible)', async () => {
190
- const cfg = await makeTmpConfig();
191
- cleanups.push(cfg.cleanup);
192
- process.env.NFO_HOME = cfg.path;
193
-
194
- const repo: TmpRepo = await makeTmpRepo();
195
- cleanups.push(repo.cleanup);
196
-
197
- const orchId = projectKeyFromPath(repo.path);
198
- await ensureOrchestraDir(orchId);
199
-
200
- const sess = sessionName(orchId);
201
- sessionsToKill.push(sess);
202
- await createDetachedSession(sess, repo.path, 220, 50);
203
-
204
- const { stdout: winIdRaw } = await execa('tmux', [
205
- 'new-window', '-t', sess, '-n', 'mus-001-noop', '-c', repo.path, '-d',
206
- '-P', '-F', '#{window_id}',
207
- ]);
208
- const winId = winIdRaw.trim();
209
-
210
- // Write the prompt so the detector fires.
211
- await execa('tmux', ['send-keys', '-l', '-t', `${sess}:${winId}`, '--', PERMISSION_PROMPT_TEXT]);
212
- await new Promise((r) => { setTimeout(r, 250); });
213
-
214
- // Musician is ALREADY marked awaiting_permission — no delta to emit.
215
- const state = makeStateWithMusician(orchId, repo.path, 'awaiting_permission', winId);
216
- await writeState(orchId, state);
217
-
218
- const transitions = await pollPermissions(state);
219
-
220
- expect(transitions).toHaveLength(0);
221
- });
222
- });
@@ -1,27 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { statusIcon, statusColor } from '../../src/tui/status-icon.js';
3
- import type { MusicianStatus } from '../../src/state.types.js';
4
-
5
- describe('statusIcon', () => {
6
- it('maps each status to an icon', () => {
7
- expect(statusIcon('working')).toBe('●');
8
- expect(statusIcon('idle')).toBe('◐');
9
- expect(statusIcon('awaiting_permission')).toBe('⚠');
10
- expect(statusIcon('stopped')).toBe('○');
11
- });
12
- });
13
-
14
- describe('statusColor', () => {
15
- it('maps each status to an ink color name', () => {
16
- const colors: Record<MusicianStatus, string> = {
17
- working: statusColor('working'),
18
- idle: statusColor('idle'),
19
- awaiting_permission: statusColor('awaiting_permission'),
20
- stopped: statusColor('stopped'),
21
- };
22
- expect(colors.working).toBe('green');
23
- expect(colors.idle).toBe('yellow');
24
- expect(colors.awaiting_permission).toBe('red');
25
- expect(colors.stopped).toBe('gray');
26
- });
27
- });
@@ -1,113 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import type { Key } from 'ink';
3
- import {
4
- toTerminalMouseScroll,
5
- toTerminalInput,
6
- toTerminalViewportCommand,
7
- } from '../../src/tui/terminal-input.js';
8
-
9
- function makeKey(overrides: Partial<Key> = {}): Key {
10
- return {
11
- upArrow: false,
12
- downArrow: false,
13
- leftArrow: false,
14
- rightArrow: false,
15
- pageDown: false,
16
- pageUp: false,
17
- home: false,
18
- end: false,
19
- return: false,
20
- escape: false,
21
- ctrl: false,
22
- shift: false,
23
- tab: false,
24
- backspace: false,
25
- delete: false,
26
- meta: false,
27
- super: false,
28
- hyper: false,
29
- capsLock: false,
30
- numLock: false,
31
- ...overrides,
32
- };
33
- }
34
-
35
- describe('toTerminalInput', () => {
36
- it('maps enter and arrows to terminal control sequences', () => {
37
- expect(toTerminalInput('', makeKey({ return: true }))).toBe('\r');
38
- expect(toTerminalInput('', makeKey({ upArrow: true }))).toBe('\x1b[A');
39
- expect(toTerminalInput('', makeKey({ downArrow: true }))).toBe('\x1b[B');
40
- });
41
-
42
- it('maps modified enter variants to a literal newline', () => {
43
- expect(toTerminalInput('\r', makeKey({ return: true, shift: true }))).toBe('\n');
44
- expect(toTerminalInput('\r', makeKey({ return: true, meta: true }))).toBe('\n');
45
- });
46
-
47
- it('maps ctrl keys to control characters', () => {
48
- expect(toTerminalInput('c', makeKey({ ctrl: true }))).toBe('\x03');
49
- expect(toTerminalInput('l', makeKey({ ctrl: true }))).toBe('\x0c');
50
- });
51
-
52
- it('maps tab variants and delete sequences', () => {
53
- expect(toTerminalInput('', makeKey({ tab: true }))).toBe('\t');
54
- expect(toTerminalInput('', makeKey({ tab: true, shift: true }))).toBe('\x1b[Z');
55
- expect(toTerminalInput('', makeKey({ delete: true }))).toBe('\x1b[3~');
56
- });
57
-
58
- it('passes through plain input', () => {
59
- expect(toTerminalInput('hello', makeKey())).toBe('hello');
60
- });
61
- });
62
-
63
- describe('toTerminalViewportCommand', () => {
64
- it('maps shift+page keys to viewport scrolling', () => {
65
- expect(toTerminalViewportCommand(makeKey({ shift: true, pageUp: true }))).toEqual({
66
- kind: 'scroll-pages',
67
- pageCount: -1,
68
- });
69
- expect(toTerminalViewportCommand(makeKey({ shift: true, pageDown: true }))).toEqual({
70
- kind: 'scroll-pages',
71
- pageCount: 1,
72
- });
73
- });
74
-
75
- describe('toTerminalMouseScroll', () => {
76
- it('maps SGR mouse wheel events to line scrolling', () => {
77
- expect(toTerminalMouseScroll('[<64;12;8M')).toEqual({
78
- button: 64,
79
- lineCount: -3,
80
- column: 12,
81
- row: 8,
82
- sequence: '\u001b[<64;12;8M',
83
- });
84
- expect(toTerminalMouseScroll('[<65;12;8M')).toEqual({
85
- button: 65,
86
- lineCount: 3,
87
- column: 12,
88
- row: 8,
89
- sequence: '\u001b[<65;12;8M',
90
- });
91
- });
92
-
93
- it('ignores non-wheel mouse sequences', () => {
94
- expect(toTerminalMouseScroll('[<0;12;8M')).toBeNull();
95
- expect(toTerminalMouseScroll('[<64;12;8m')).toBeNull();
96
- expect(toTerminalMouseScroll('hello')).toBeNull();
97
- });
98
- });
99
-
100
- it('maps shift+home/end to top and bottom jumps', () => {
101
- expect(toTerminalViewportCommand(makeKey({ shift: true, home: true }))).toEqual({
102
- kind: 'scroll-top',
103
- });
104
- expect(toTerminalViewportCommand(makeKey({ shift: true, end: true }))).toEqual({
105
- kind: 'scroll-bottom',
106
- });
107
- });
108
-
109
- it('ignores unmodified terminal navigation keys', () => {
110
- expect(toTerminalViewportCommand(makeKey({ pageUp: true }))).toBeNull();
111
- expect(toTerminalViewportCommand(makeKey({ end: true }))).toBeNull();
112
- });
113
- });
@@ -1,54 +0,0 @@
1
- import { describe, it, expect, afterEach, beforeEach } from 'vitest';
2
- import { watchOrchestraState } from '../../src/tui/watch-state.js';
3
- import { makeTmpConfig } from '../helpers/tmp-config.js';
4
- import { ensureOrchestraDir, writeState } from '../../src/state.js';
5
- import { makeInitialState } from '../../src/state.types.js';
6
- import { setOrchestratorSessionId } from '../../src/state-updaters.js';
7
- import type { OrchestraState } from '../../src/state.types.js';
8
-
9
- describe('watchOrchestraState', () => {
10
- const cleanups: Array<() => Promise<void>> = [];
11
- const stops: Array<() => Promise<void>> = [];
12
-
13
- beforeEach(() => { process.env.NFO_HOME = ''; });
14
- afterEach(async () => {
15
- for (const stop of stops) { await stop(); }
16
- stops.length = 0;
17
- for (const c of cleanups) { await c(); }
18
- cleanups.length = 0;
19
- delete process.env.NFO_HOME;
20
- });
21
-
22
- it('emits the current state immediately and again on change', async () => {
23
- const cfg = await makeTmpConfig();
24
- cleanups.push(cfg.cleanup);
25
- process.env.NFO_HOME = cfg.path;
26
- await ensureOrchestraDir('orch-w');
27
- await writeState('orch-w', makeInitialState({
28
- orchestraId: 'orch-w', projectPath: '/tmp/x', permissionLevel: 'supervised',
29
- }));
30
-
31
- const seen: OrchestraState[] = [];
32
- const stop = await watchOrchestraState('orch-w', (s) => { seen.push(s); });
33
- stops.push(stop);
34
-
35
- // initial emit
36
- await waitFor(() => { return seen.length >= 1; });
37
- expect(seen[0].orchestra_id).toBe('orch-w');
38
-
39
- // mutate → expect another emit
40
- await setOrchestratorSessionId('orch-w', 'sess-123');
41
- await waitFor(() => { return seen.some((s) => { return s.orchestrator_session_id === 'sess-123'; }); }, 4000);
42
- expect(seen.some((s) => { return s.orchestrator_session_id === 'sess-123'; })).toBe(true);
43
- });
44
- });
45
-
46
- async function waitFor(pred: () => boolean, timeoutMs = 3000): Promise<void> {
47
- const start = Date.now();
48
- while (!pred()) {
49
- if (Date.now() - start > timeoutMs) {
50
- throw new Error('Timed out');
51
- }
52
- await new Promise((r) => { setTimeout(r, 25); });
53
- }
54
- }
@@ -1,73 +0,0 @@
1
- import { describe, it, expect, afterEach } from 'vitest';
2
- import { execa } from 'execa';
3
- import { mkdtemp, rm } from 'node:fs/promises';
4
- import { existsSync } from 'node:fs';
5
- import { tmpdir } from 'node:os';
6
- import { join } from 'node:path';
7
- import { makeTmpRepo, type TmpRepo } from './helpers/tmp-repo.js';
8
- import { addWorktree, removeWorktree, worktreeExists } from '../src/worktree.js';
9
-
10
- describe('worktree wrapper', () => {
11
- const cleanups: Array<() => Promise<void>> = [];
12
- const dirsToRemove: string[] = [];
13
-
14
- afterEach(async () => {
15
- for (const d of dirsToRemove) {
16
- try { await rm(d, { recursive: true, force: true }); } catch { /* ignore */ }
17
- }
18
- dirsToRemove.length = 0;
19
- for (const c of cleanups) await c();
20
- cleanups.length = 0;
21
- });
22
-
23
- async function track(t: TmpRepo) {
24
- cleanups.push(t.cleanup);
25
- return t;
26
- }
27
-
28
- it('addWorktree creates a worktree on a new branch from HEAD', async () => {
29
- const repo = await track(await makeTmpRepo());
30
- const workArea = await mkdtemp(join(tmpdir(), 'nfo-wt-'));
31
- dirsToRemove.push(workArea);
32
- const path = join(workArea, 'mus-001');
33
-
34
- await addWorktree({ repoRoot: repo.path, path, branch: 'nfo/mus-001' });
35
-
36
- expect(existsSync(path)).toBe(true);
37
- expect(await worktreeExists(repo.path, path)).toBe(true);
38
- });
39
-
40
- it('addWorktree honours baseRef', async () => {
41
- const repo = await track(await makeTmpRepo());
42
- await execa('git', ['commit', '--allow-empty', '-m', 'second'], { cwd: repo.path });
43
- const { stdout: firstSha } = await execa('git', ['rev-parse', 'HEAD~1'], { cwd: repo.path });
44
-
45
- const workArea = await mkdtemp(join(tmpdir(), 'nfo-wt-'));
46
- dirsToRemove.push(workArea);
47
- const path = join(workArea, 'mus-002');
48
-
49
- await addWorktree({
50
- repoRoot: repo.path,
51
- path,
52
- branch: 'nfo/mus-002',
53
- baseRef: firstSha.trim(),
54
- });
55
-
56
- const { stdout: branchSha } = await execa('git', ['rev-parse', 'HEAD'], { cwd: path });
57
- expect(branchSha.trim()).toBe(firstSha.trim());
58
- });
59
-
60
- it('removeWorktree removes the worktree dir and metadata', async () => {
61
- const repo = await track(await makeTmpRepo());
62
- const workArea = await mkdtemp(join(tmpdir(), 'nfo-wt-'));
63
- dirsToRemove.push(workArea);
64
- const path = join(workArea, 'mus-003');
65
-
66
- await addWorktree({ repoRoot: repo.path, path, branch: 'nfo/mus-003' });
67
- expect(await worktreeExists(repo.path, path)).toBe(true);
68
-
69
- await removeWorktree({ repoRoot: repo.path, path });
70
- expect(existsSync(path)).toBe(false);
71
- expect(await worktreeExists(repo.path, path)).toBe(false);
72
- });
73
- });
package/tsconfig.json DELETED
@@ -1,19 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "ESNext",
5
- "moduleResolution": "Bundler",
6
- "outDir": "./dist",
7
- "rootDir": "./src",
8
- "strict": true,
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "forceConsistentCasingInFileNames": true,
12
- "resolveJsonModule": true,
13
- "declaration": false,
14
- "jsx": "react-jsx",
15
- "sourceMap": true
16
- },
17
- "include": ["src/**/*"],
18
- "exclude": ["node_modules", "dist", "tests"]
19
- }
package/vitest.config.ts DELETED
@@ -1,12 +0,0 @@
1
- import { defineConfig } from 'vitest/config';
2
-
3
- export default defineConfig({
4
- test: {
5
- include: ['tests/**/*.test.ts', 'tests/**/*.test.tsx'],
6
- environment: 'node',
7
- testTimeout: 10000,
8
- },
9
- esbuild: {
10
- jsx: 'automatic',
11
- },
12
- });