nfo-cli 0.0.4-improve-prompting → 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 (159) 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 +0 -0
  6. package/dist/mcp/handlers.js +5 -0
  7. package/dist/mcp/handlers.js.map +1 -1
  8. package/dist/mcp/tool-defs.js +10 -0
  9. package/dist/mcp/tool-defs.js.map +1 -1
  10. package/dist/musicians/dismiss.js +1 -1
  11. package/dist/musicians/dismiss.js.map +1 -1
  12. package/dist/musicians/roles.js +15 -0
  13. package/dist/musicians/roles.js.map +1 -0
  14. package/dist/musicians/spawn.js +53 -18
  15. package/dist/musicians/spawn.js.map +1 -1
  16. package/dist/permission.js +6 -0
  17. package/dist/permission.js.map +1 -1
  18. package/dist/prompts/musician-role.js +2 -1
  19. package/dist/prompts/musician-role.js.map +1 -1
  20. package/dist/prompts/orchestrator-role.js +18 -6
  21. package/dist/prompts/orchestrator-role.js.map +1 -1
  22. package/dist/prompts/tool-discipline.js +7 -3
  23. package/dist/prompts/tool-discipline.js.map +1 -1
  24. package/package.json +8 -1
  25. package/assets/agent-screen.png +0 -0
  26. package/assets/main-screen.png +0 -0
  27. package/assets/orche-clawd.png +0 -0
  28. package/dist/tui/App.js +0 -428
  29. package/dist/tui/App.js.map +0 -1
  30. package/dist/tui/AppView.js +0 -13
  31. package/dist/tui/AppView.js.map +0 -1
  32. package/dist/tui/Auditorium.js +0 -17
  33. package/dist/tui/Auditorium.js.map +0 -1
  34. package/dist/tui/ConcertHall.js +0 -11
  35. package/dist/tui/ConcertHall.js.map +0 -1
  36. package/dist/tui/Help.js +0 -49
  37. package/dist/tui/Help.js.map +0 -1
  38. package/dist/tui/OrchestratorPane.js +0 -34
  39. package/dist/tui/OrchestratorPane.js.map +0 -1
  40. package/dist/tui/SidebarHeader.js +0 -6
  41. package/dist/tui/SidebarHeader.js.map +0 -1
  42. package/dist/tui/StatusBar.js +0 -6
  43. package/dist/tui/StatusBar.js.map +0 -1
  44. package/docs/plans/2026-05-29-nfo-phase-1-bootstrap.md +0 -2152
  45. package/docs/plans/2026-05-29-nfo-phase-2-mcp-musicians.md +0 -2467
  46. package/docs/plans/2026-05-29-nfo-phase-3-ink-tui.md +0 -1611
  47. package/docs/plans/2026-05-29-nfo-phase-4-permission-prompts.md +0 -460
  48. package/docs/plans/2026-05-29-nfo-phase-5-help-and-notify.md +0 -933
  49. package/docs/specs/2026-05-29-nfo-design.md +0 -468
  50. package/plan-explorer-musician-hardening.md +0 -56
  51. package/src/claude-command.ts +0 -35
  52. package/src/claude-detect.ts +0 -42
  53. package/src/cli.ts +0 -197
  54. package/src/commands/attach.ts +0 -24
  55. package/src/commands/dashboard-window.ts +0 -33
  56. package/src/commands/kill.ts +0 -50
  57. package/src/commands/launch.ts +0 -134
  58. package/src/commands/list.ts +0 -43
  59. package/src/commands/mcp-server.ts +0 -18
  60. package/src/commands/notes.ts +0 -18
  61. package/src/commands/restore.ts +0 -153
  62. package/src/commands/tui.tsx +0 -22
  63. package/src/config.ts +0 -44
  64. package/src/dashboard.ts +0 -1
  65. package/src/mcp/config.ts +0 -39
  66. package/src/mcp/handlers.ts +0 -141
  67. package/src/mcp/server.ts +0 -50
  68. package/src/mcp/tool-defs.ts +0 -151
  69. package/src/musicians/dismiss.ts +0 -60
  70. package/src/musicians/ids.ts +0 -21
  71. package/src/musicians/lookup.ts +0 -13
  72. package/src/musicians/message-log.ts +0 -152
  73. package/src/musicians/message.ts +0 -99
  74. package/src/musicians/query.ts +0 -19
  75. package/src/musicians/spawn.ts +0 -139
  76. package/src/notes.ts +0 -39
  77. package/src/notify.ts +0 -62
  78. package/src/orchestrator/report-back.ts +0 -33
  79. package/src/permission.ts +0 -30
  80. package/src/project-key.ts +0 -12
  81. package/src/prompts/musician-role.ts +0 -22
  82. package/src/prompts/orchestrator-role.ts +0 -84
  83. package/src/prompts/tool-discipline.ts +0 -41
  84. package/src/repo.ts +0 -14
  85. package/src/shell-quote.ts +0 -7
  86. package/src/state-updaters.ts +0 -132
  87. package/src/state.ts +0 -49
  88. package/src/state.types.ts +0 -67
  89. package/src/tmux.ts +0 -226
  90. package/src/tui/activity-line.ts +0 -16
  91. package/src/tui/components/App.tsx +0 -534
  92. package/src/tui/components/AppView.tsx +0 -98
  93. package/src/tui/components/Auditorium.tsx +0 -56
  94. package/src/tui/components/ConcertHall.tsx +0 -31
  95. package/src/tui/components/Help.tsx +0 -63
  96. package/src/tui/components/OrchestratorPane.tsx +0 -98
  97. package/src/tui/components/SidebarHeader.tsx +0 -34
  98. package/src/tui/components/StatusBar.tsx +0 -42
  99. package/src/tui/detect-permission.ts +0 -93
  100. package/src/tui/embedded-session-lifecycle.ts +0 -44
  101. package/src/tui/embedded-terminal.ts +0 -325
  102. package/src/tui/format-time.ts +0 -25
  103. package/src/tui/keymap.ts +0 -104
  104. package/src/tui/poll-activity.ts +0 -25
  105. package/src/tui/poll-idle.ts +0 -149
  106. package/src/tui/poll-permission.ts +0 -50
  107. package/src/tui/status-icon.ts +0 -35
  108. package/src/tui/terminal-input.ts +0 -136
  109. package/src/tui/watch-state.ts +0 -43
  110. package/src/worktree.ts +0 -41
  111. package/tests/claude-command.test.ts +0 -30
  112. package/tests/claude-detect.test.ts +0 -14
  113. package/tests/commands/attach.test.ts +0 -60
  114. package/tests/commands/kill.test.ts +0 -66
  115. package/tests/commands/launch.test.ts +0 -75
  116. package/tests/commands/list.test.ts +0 -47
  117. package/tests/commands/notes.test.ts +0 -53
  118. package/tests/commands/restore.test.ts +0 -126
  119. package/tests/helpers/tmp-config.ts +0 -16
  120. package/tests/helpers/tmp-repo.ts +0 -29
  121. package/tests/integration/orchestrator-spawn.test.ts +0 -108
  122. package/tests/mcp/handlers.test.ts +0 -163
  123. package/tests/mcp/tool-defs.test.ts +0 -35
  124. package/tests/musicians/dismiss.test.ts +0 -102
  125. package/tests/musicians/message.test.ts +0 -159
  126. package/tests/musicians/query.test.ts +0 -65
  127. package/tests/musicians/spawn.test.ts +0 -125
  128. package/tests/notes.test.ts +0 -56
  129. package/tests/notify.test.ts +0 -80
  130. package/tests/orchestrator/report-back.test.ts +0 -18
  131. package/tests/permission.test.ts +0 -39
  132. package/tests/project-key.test.ts +0 -33
  133. package/tests/prompts/tool-discipline.test.ts +0 -25
  134. package/tests/repo.test.ts +0 -38
  135. package/tests/state-updaters.test.ts +0 -126
  136. package/tests/state.test.ts +0 -85
  137. package/tests/tmux.test.ts +0 -126
  138. package/tests/tui/AppView.test.tsx +0 -92
  139. package/tests/tui/Auditorium.test.tsx +0 -67
  140. package/tests/tui/ConcertHall.test.tsx +0 -22
  141. package/tests/tui/Help.test.tsx +0 -38
  142. package/tests/tui/OrchestratorPane.test.ts +0 -30
  143. package/tests/tui/SidebarHeader.test.tsx +0 -20
  144. package/tests/tui/StatusBar.test.tsx +0 -51
  145. package/tests/tui/activity-line.test.ts +0 -21
  146. package/tests/tui/detect-permission.test.ts +0 -92
  147. package/tests/tui/embedded-session-lifecycle.test.ts +0 -55
  148. package/tests/tui/embedded-terminal.test.ts +0 -80
  149. package/tests/tui/format-time.test.ts +0 -25
  150. package/tests/tui/keymap.test.ts +0 -93
  151. package/tests/tui/poll-activity.test.ts +0 -81
  152. package/tests/tui/poll-idle.test.ts +0 -159
  153. package/tests/tui/poll-permission.test.ts +0 -222
  154. package/tests/tui/status-icon.test.ts +0 -27
  155. package/tests/tui/terminal-input.test.ts +0 -113
  156. package/tests/tui/watch-state.test.ts +0 -54
  157. package/tests/worktree.test.ts +0 -73
  158. package/tsconfig.json +0 -19
  159. package/vitest.config.ts +0 -12
@@ -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
- });