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.
- package/dist/claude-command.js +6 -1
- package/dist/claude-command.js.map +1 -1
- package/dist/claude-trust.js +46 -0
- package/dist/claude-trust.js.map +1 -0
- package/dist/cli.js +64 -54
- package/dist/cli.js.map +1 -1
- package/dist/commands/restore.js +0 -1
- package/dist/commands/restore.js.map +1 -1
- package/dist/commands/tui.js +6 -4
- package/dist/commands/tui.js.map +1 -1
- package/dist/mcp/handlers.js +5 -0
- package/dist/mcp/handlers.js.map +1 -1
- package/dist/mcp/tool-defs.js +10 -0
- package/dist/mcp/tool-defs.js.map +1 -1
- package/dist/musicians/dismiss.js +1 -1
- package/dist/musicians/dismiss.js.map +1 -1
- package/dist/musicians/roles.js +15 -0
- package/dist/musicians/roles.js.map +1 -0
- package/dist/musicians/spawn.js +53 -18
- package/dist/musicians/spawn.js.map +1 -1
- package/dist/permission.js +14 -8
- package/dist/permission.js.map +1 -1
- package/dist/prompts/musician-role.js +2 -1
- package/dist/prompts/musician-role.js.map +1 -1
- package/dist/prompts/orchestrator-role.js +42 -8
- package/dist/prompts/orchestrator-role.js.map +1 -1
- package/dist/prompts/tool-discipline.js +10 -0
- package/dist/prompts/tool-discipline.js.map +1 -1
- package/dist/tui/{App.js → components/App.js} +20 -20
- package/dist/tui/components/App.js.map +1 -0
- package/dist/tui/components/AppView.js +13 -0
- package/dist/tui/components/AppView.js.map +1 -0
- package/dist/tui/{Auditorium.js → components/Auditorium.js} +2 -2
- package/dist/tui/components/Auditorium.js.map +1 -0
- package/dist/tui/components/ConcertHall.js.map +1 -0
- package/dist/tui/{Help.js → components/Help.js} +0 -8
- package/dist/tui/components/Help.js.map +1 -0
- package/dist/tui/components/OrchestratorPane.js.map +1 -0
- package/dist/tui/components/SidebarHeader.js +6 -0
- package/dist/tui/components/SidebarHeader.js.map +1 -0
- package/dist/tui/{StatusBar.js → components/StatusBar.js} +1 -1
- package/dist/tui/components/StatusBar.js.map +1 -0
- package/package.json +8 -1
- package/assets/agent-screen.png +0 -0
- package/assets/main-screen.png +0 -0
- package/assets/orche-clawd.png +0 -0
- package/dist/tui/App.js.map +0 -1
- package/dist/tui/AppView.js +0 -13
- package/dist/tui/AppView.js.map +0 -1
- package/dist/tui/Auditorium.js.map +0 -1
- package/dist/tui/ConcertHall.js.map +0 -1
- package/dist/tui/Help.js.map +0 -1
- package/dist/tui/OrchestratorPane.js.map +0 -1
- package/dist/tui/SidebarHeader.js +0 -6
- package/dist/tui/SidebarHeader.js.map +0 -1
- package/dist/tui/StatusBar.js.map +0 -1
- package/docs/plans/2026-05-29-nfo-phase-1-bootstrap.md +0 -2152
- package/docs/plans/2026-05-29-nfo-phase-2-mcp-musicians.md +0 -2467
- package/docs/plans/2026-05-29-nfo-phase-3-ink-tui.md +0 -1611
- package/docs/plans/2026-05-29-nfo-phase-4-permission-prompts.md +0 -460
- package/docs/plans/2026-05-29-nfo-phase-5-help-and-notify.md +0 -933
- package/docs/specs/2026-05-29-nfo-design.md +0 -468
- package/src/claude-command.ts +0 -35
- package/src/claude-detect.ts +0 -42
- package/src/cli.ts +0 -164
- package/src/commands/attach.ts +0 -24
- package/src/commands/dashboard-window.ts +0 -33
- package/src/commands/kill.ts +0 -50
- package/src/commands/launch.ts +0 -134
- package/src/commands/list.ts +0 -43
- package/src/commands/mcp-server.ts +0 -18
- package/src/commands/notes.ts +0 -18
- package/src/commands/restore.ts +0 -153
- package/src/commands/tui.tsx +0 -16
- package/src/config.ts +0 -44
- package/src/dashboard.ts +0 -1
- package/src/mcp/config.ts +0 -39
- package/src/mcp/handlers.ts +0 -141
- package/src/mcp/server.ts +0 -50
- package/src/mcp/tool-defs.ts +0 -151
- package/src/musicians/dismiss.ts +0 -60
- package/src/musicians/ids.ts +0 -21
- package/src/musicians/lookup.ts +0 -13
- package/src/musicians/message-log.ts +0 -152
- package/src/musicians/message.ts +0 -99
- package/src/musicians/query.ts +0 -19
- package/src/musicians/spawn.ts +0 -139
- package/src/notes.ts +0 -39
- package/src/notify.ts +0 -62
- package/src/orchestrator/report-back.ts +0 -33
- package/src/permission.ts +0 -30
- package/src/project-key.ts +0 -12
- package/src/prompts/musician-role.ts +0 -22
- package/src/prompts/orchestrator-role.ts +0 -60
- package/src/prompts/tool-discipline.ts +0 -35
- package/src/repo.ts +0 -14
- package/src/shell-quote.ts +0 -7
- package/src/state-updaters.ts +0 -132
- package/src/state.ts +0 -49
- package/src/state.types.ts +0 -67
- package/src/tmux.ts +0 -226
- package/src/tui/App.tsx +0 -532
- package/src/tui/AppView.tsx +0 -96
- package/src/tui/Auditorium.tsx +0 -56
- package/src/tui/ConcertHall.tsx +0 -31
- package/src/tui/Help.tsx +0 -72
- package/src/tui/OrchestratorPane.tsx +0 -98
- package/src/tui/SidebarHeader.tsx +0 -32
- package/src/tui/StatusBar.tsx +0 -44
- package/src/tui/activity-line.ts +0 -16
- package/src/tui/detect-permission.ts +0 -93
- package/src/tui/embedded-session-lifecycle.ts +0 -44
- package/src/tui/embedded-terminal.ts +0 -325
- package/src/tui/format-time.ts +0 -25
- package/src/tui/keymap.ts +0 -104
- package/src/tui/poll-activity.ts +0 -25
- package/src/tui/poll-idle.ts +0 -149
- package/src/tui/poll-permission.ts +0 -50
- package/src/tui/status-icon.ts +0 -35
- package/src/tui/terminal-input.ts +0 -136
- package/src/tui/watch-state.ts +0 -43
- package/src/worktree.ts +0 -41
- package/tests/claude-command.test.ts +0 -30
- package/tests/claude-detect.test.ts +0 -14
- package/tests/commands/attach.test.ts +0 -60
- package/tests/commands/kill.test.ts +0 -66
- package/tests/commands/launch.test.ts +0 -75
- package/tests/commands/list.test.ts +0 -47
- package/tests/commands/notes.test.ts +0 -53
- package/tests/commands/restore.test.ts +0 -126
- package/tests/helpers/tmp-config.ts +0 -16
- package/tests/helpers/tmp-repo.ts +0 -29
- package/tests/integration/orchestrator-spawn.test.ts +0 -108
- package/tests/mcp/handlers.test.ts +0 -163
- package/tests/mcp/tool-defs.test.ts +0 -35
- package/tests/musicians/dismiss.test.ts +0 -102
- package/tests/musicians/message.test.ts +0 -159
- package/tests/musicians/query.test.ts +0 -65
- package/tests/musicians/spawn.test.ts +0 -125
- package/tests/notes.test.ts +0 -56
- package/tests/notify.test.ts +0 -80
- package/tests/orchestrator/report-back.test.ts +0 -18
- package/tests/permission.test.ts +0 -29
- package/tests/project-key.test.ts +0 -33
- package/tests/prompts/tool-discipline.test.ts +0 -25
- package/tests/repo.test.ts +0 -38
- package/tests/state-updaters.test.ts +0 -126
- package/tests/state.test.ts +0 -85
- package/tests/tmux.test.ts +0 -126
- package/tests/tui/AppView.test.tsx +0 -92
- package/tests/tui/Auditorium.test.tsx +0 -67
- package/tests/tui/ConcertHall.test.tsx +0 -22
- package/tests/tui/Help.test.tsx +0 -38
- package/tests/tui/OrchestratorPane.test.ts +0 -30
- package/tests/tui/SidebarHeader.test.tsx +0 -20
- package/tests/tui/StatusBar.test.tsx +0 -51
- package/tests/tui/activity-line.test.ts +0 -21
- package/tests/tui/detect-permission.test.ts +0 -92
- package/tests/tui/embedded-session-lifecycle.test.ts +0 -55
- package/tests/tui/embedded-terminal.test.ts +0 -80
- package/tests/tui/format-time.test.ts +0 -25
- package/tests/tui/keymap.test.ts +0 -93
- package/tests/tui/poll-activity.test.ts +0 -81
- package/tests/tui/poll-idle.test.ts +0 -159
- package/tests/tui/poll-permission.test.ts +0 -222
- package/tests/tui/status-icon.test.ts +0 -27
- package/tests/tui/terminal-input.test.ts +0 -113
- package/tests/tui/watch-state.test.ts +0 -54
- package/tests/worktree.test.ts +0 -73
- package/tsconfig.json +0 -19
- package/vitest.config.ts +0 -12
- /package/dist/tui/{ConcertHall.js → components/ConcertHall.js} +0 -0
- /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
|
-
}
|
package/tests/worktree.test.ts
DELETED
|
@@ -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
|
File without changes
|
|
File without changes
|