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,136 +0,0 @@
1
- import type { Key } from 'ink';
2
-
3
- export type TerminalViewportCommand =
4
- | { kind: 'scroll-pages'; pageCount: number }
5
- | { kind: 'scroll-top' }
6
- | { kind: 'scroll-bottom' };
7
-
8
- export interface TerminalMouseScroll {
9
- button: number;
10
- lineCount: number;
11
- column: number;
12
- row: number;
13
- sequence: string;
14
- }
15
-
16
- const sgrMouseSequence =
17
- /^(?:\u001b)?\[<(?<button>\d+);(?<column>\d+);(?<row>\d+)(?<suffix>[Mm])$/u;
18
-
19
- function toControlCharacter(input: string): string | null {
20
- if (input.length !== 1) {
21
- return null;
22
- }
23
-
24
- const code = input.toUpperCase().charCodeAt(0);
25
- if (code >= 64 && code <= 95) {
26
- return String.fromCharCode(code - 64);
27
- }
28
-
29
- return null;
30
- }
31
-
32
- export function toTerminalViewportCommand(
33
- key: Key,
34
- ): TerminalViewportCommand | null {
35
- if (!key.shift) {
36
- return null;
37
- }
38
- if (key.pageUp) {
39
- return { kind: 'scroll-pages', pageCount: -1 };
40
- }
41
- if (key.pageDown) {
42
- return { kind: 'scroll-pages', pageCount: 1 };
43
- }
44
- if (key.home) {
45
- return { kind: 'scroll-top' };
46
- }
47
- if (key.end) {
48
- return { kind: 'scroll-bottom' };
49
- }
50
-
51
- return null;
52
- }
53
-
54
- export function toTerminalMouseScroll(input: string): TerminalMouseScroll | null {
55
- const match = sgrMouseSequence.exec(input);
56
- if (!match?.groups) {
57
- return null;
58
- }
59
-
60
- if (match.groups.suffix !== 'M') {
61
- return null;
62
- }
63
-
64
- const button = Number(match.groups.button);
65
- const column = Number(match.groups.column);
66
- const row = Number(match.groups.row);
67
- if (
68
- !Number.isFinite(button)
69
- || !Number.isFinite(column)
70
- || !Number.isFinite(row)
71
- || (button & 64) === 0
72
- ) {
73
- return null;
74
- }
75
-
76
- return {
77
- button,
78
- lineCount: (button & 1) === 0 ? -3 : 3,
79
- column,
80
- row,
81
- sequence: `\u001b[<${button};${column};${row}${match.groups.suffix}`,
82
- };
83
- }
84
-
85
- export function toTerminalInput(input: string, key: Key): string | null {
86
- if (key.return) {
87
- return key.shift || key.meta ? '\n' : '\r';
88
- }
89
- if (key.tab) {
90
- return key.shift ? '\x1b[Z' : '\t';
91
- }
92
- if (key.backspace) {
93
- return '\x7f';
94
- }
95
- if (key.delete) {
96
- return '\x1b[3~';
97
- }
98
- if (key.escape) {
99
- return '\x1b';
100
- }
101
- if (key.upArrow) {
102
- return '\x1b[A';
103
- }
104
- if (key.downArrow) {
105
- return '\x1b[B';
106
- }
107
- if (key.rightArrow) {
108
- return '\x1b[C';
109
- }
110
- if (key.leftArrow) {
111
- return '\x1b[D';
112
- }
113
- if (key.home) {
114
- return '\x1b[H';
115
- }
116
- if (key.end) {
117
- return '\x1b[F';
118
- }
119
- if (key.pageUp) {
120
- return '\x1b[5~';
121
- }
122
- if (key.pageDown) {
123
- return '\x1b[6~';
124
- }
125
- if (key.ctrl) {
126
- return toControlCharacter(input);
127
- }
128
- if (key.meta && input.length > 0) {
129
- return `\x1b${input}`;
130
- }
131
- if (input.length > 0) {
132
- return input;
133
- }
134
-
135
- return null;
136
- }
@@ -1,43 +0,0 @@
1
- import chokidar from 'chokidar';
2
- import { stateFile } from '../config.js';
3
- import { readState } from '../state.js';
4
- import type { OrchestraState } from '../state.types.js';
5
-
6
- export type StopWatching = () => Promise<void>;
7
-
8
- /**
9
- * Watch an orchestra's state.json and invoke `onChange` with the parsed state:
10
- * once immediately, then on every file change. A chokidar watcher handles the
11
- * common case; a 1s poll fallback covers filesystems without reliable inotify.
12
- * Reads that fail mid-write (partial JSON) are swallowed — the next event wins.
13
- */
14
- export async function watchOrchestraState(
15
- orchestraId: string,
16
- onChange: (state: OrchestraState) => void,
17
- ): Promise<StopWatching> {
18
- const file = stateFile(orchestraId);
19
-
20
- async function emit(): Promise<void> {
21
- try {
22
- const state = await readState(orchestraId);
23
- if (state) {
24
- onChange(state);
25
- }
26
- } catch {
27
- // partial write / transient read error — ignore, next tick re-reads
28
- }
29
- }
30
-
31
- await emit();
32
-
33
- const watcher = chokidar.watch(file, { ignoreInitial: true });
34
- watcher.on('change', () => { void emit(); });
35
- watcher.on('add', () => { void emit(); });
36
-
37
- const poll = setInterval(() => { void emit(); }, 1000);
38
-
39
- return async () => {
40
- clearInterval(poll);
41
- await watcher.close();
42
- };
43
- }
package/src/worktree.ts DELETED
@@ -1,41 +0,0 @@
1
- import { execa } from 'execa';
2
-
3
- export interface AddWorktreeArgs {
4
- repoRoot: string;
5
- path: string;
6
- branch: string;
7
- baseRef?: string;
8
- }
9
-
10
- export async function addWorktree(args: AddWorktreeArgs): Promise<void> {
11
- const cmdArgs = ['worktree', 'add', '-b', args.branch, args.path];
12
- if (args.baseRef) {
13
- cmdArgs.push(args.baseRef);
14
- }
15
- await execa('git', cmdArgs, { cwd: args.repoRoot });
16
- }
17
-
18
- export interface RemoveWorktreeArgs {
19
- repoRoot: string;
20
- path: string;
21
- force?: boolean;
22
- }
23
-
24
- export async function removeWorktree(args: RemoveWorktreeArgs): Promise<void> {
25
- const cmdArgs = ['worktree', 'remove'];
26
- if (args.force) {
27
- cmdArgs.push('--force');
28
- }
29
- cmdArgs.push(args.path);
30
- await execa('git', cmdArgs, { cwd: args.repoRoot, reject: false });
31
- }
32
-
33
- export async function worktreeExists(repoRoot: string, path: string): Promise<boolean> {
34
- const { stdout } = await execa('git', ['worktree', 'list', '--porcelain'], { cwd: repoRoot });
35
- const lines = stdout.split('\n');
36
- return lines.some((l) => { return l === `worktree ${path}`; });
37
- }
38
-
39
- export async function deleteBranch(repoRoot: string, branch: string): Promise<void> {
40
- await execa('git', ['branch', '-D', branch], { cwd: repoRoot, reject: false });
41
- }
@@ -1,30 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { buildClaudeCommand } from '../src/claude-command.js';
3
-
4
- describe('buildClaudeCommand', () => {
5
- it('quotes paths and appends the initial prompt as a positional argument', () => {
6
- const command = buildClaudeCommand({
7
- flags: ['--permission-mode', 'acceptEdits'],
8
- mcpConfigPath: '/tmp/with spaces/mcp-config.json',
9
- promptFile: '/tmp/with spaces/musician-prompt.md',
10
- model: 'haiku',
11
- prompt: "fix the bug in it's startup path",
12
- });
13
-
14
- expect(command).toBe(
15
- "'claude' '--permission-mode' 'acceptEdits' '--mcp-config' '/tmp/with spaces/mcp-config.json' '--append-system-prompt-file' '/tmp/with spaces/musician-prompt.md' '--model' 'haiku' 'fix the bug in it'\"'\"'s startup path'",
16
- );
17
- });
18
-
19
- it('includes resume sessions without requiring a prompt', () => {
20
- const command = buildClaudeCommand({
21
- flags: [],
22
- mcpConfigPath: '/tmp/mcp-config.json',
23
- resumeSessionId: 'session-123',
24
- });
25
-
26
- expect(command).toBe(
27
- "'claude' '--resume' 'session-123' '--mcp-config' '/tmp/mcp-config.json'",
28
- );
29
- });
30
- });
@@ -1,14 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { detectClaude } from '../src/claude-detect.js';
3
-
4
- describe('detectClaude', () => {
5
- it('returns a parsed version object when claude is installed', async () => {
6
- const info = await detectClaude();
7
- expect(info).toMatchObject({
8
- version: expect.stringMatching(/^\d+\.\d+\.\d+$/),
9
- major: expect.any(Number),
10
- minor: expect.any(Number),
11
- patch: expect.any(Number),
12
- });
13
- });
14
- });
@@ -1,60 +0,0 @@
1
- import { describe, it, expect, afterEach, beforeEach } from 'vitest';
2
- import { execa } from 'execa';
3
- import { attachOrRestore } from '../../src/commands/attach.js';
4
- import { makeTmpRepo, type TmpRepo } from '../helpers/tmp-repo.js';
5
- import { makeTmpConfig } from '../helpers/tmp-config.js';
6
- import { ensureOrchestraDir, writeState } from '../../src/state.js';
7
- import { makeInitialState } from '../../src/state.types.js';
8
- import { projectKeyFromPath } from '../../src/project-key.js';
9
- import { createDetachedSession, killSession, sessionName } from '../../src/tmux.js';
10
-
11
- describe('attachOrRestore', () => {
12
- const cleanups: Array<() => Promise<void>> = [];
13
- const sessionsToKill: string[] = [];
14
-
15
- beforeEach(() => { process.env.NFO_HOME = ''; });
16
-
17
- afterEach(async () => {
18
- for (const s of sessionsToKill) {
19
- try { await killSession(s); } catch { /* ignore */ }
20
- }
21
- sessionsToKill.length = 0;
22
- for (const c of cleanups) await c();
23
- cleanups.length = 0;
24
- delete process.env.NFO_HOME;
25
- });
26
-
27
- it('migrates a legacy split session to a dedicated dashboard window', async () => {
28
- const repo: TmpRepo = await makeTmpRepo();
29
- cleanups.push(repo.cleanup);
30
- const cfg = await makeTmpConfig();
31
- cleanups.push(cfg.cleanup);
32
- process.env.NFO_HOME = cfg.path;
33
-
34
- const orchestraId = projectKeyFromPath(repo.path);
35
- await ensureOrchestraDir(orchestraId);
36
- await writeState(orchestraId, makeInitialState({
37
- orchestraId,
38
- projectPath: repo.path,
39
- permissionLevel: 'supervised',
40
- }));
41
-
42
- const name = sessionName(orchestraId);
43
- sessionsToKill.push(name);
44
- await createDetachedSession(name, repo.path);
45
- await execa('tmux', ['split-window', '-h', '-t', `${name}:0`, '-c', repo.path]);
46
-
47
- const result = await attachOrRestore(orchestraId, true);
48
- expect(result.action).toBe('attached');
49
-
50
- const { stdout: paneCount } = await execa('tmux', [
51
- 'list-panes', '-t', `${name}:0`, '-F', '#{pane_index}',
52
- ]);
53
- expect(paneCount.trim().split('\n').length).toBe(1);
54
-
55
- const { stdout: windows } = await execa('tmux', [
56
- 'list-windows', '-t', name, '-F', '#{window_name}',
57
- ]);
58
- expect(windows).toContain('nfo-dashboard');
59
- });
60
- });
@@ -1,66 +0,0 @@
1
- import { describe, it, expect, afterEach, beforeEach } from 'vitest';
2
- import { killOrchestra } from '../../src/commands/kill.js';
3
- import { makeTmpRepo, type TmpRepo } from '../helpers/tmp-repo.js';
4
- import { makeTmpConfig } from '../helpers/tmp-config.js';
5
- import { ensureOrchestraDir, writeState, readState } from '../../src/state.js';
6
- import { makeInitialState } from '../../src/state.types.js';
7
- import { projectKeyFromPath } from '../../src/project-key.js';
8
- import { sessionExists, killSession, createDetachedSession, sessionName } from '../../src/tmux.js';
9
- import { existsSync, readdirSync } from 'node:fs';
10
- import { archiveDir } from '../../src/config.js';
11
-
12
- describe('killOrchestra', () => {
13
- const cleanups: Array<() => Promise<void>> = [];
14
- const sessionsToKill: string[] = [];
15
-
16
- beforeEach(() => { process.env.NFO_HOME = ''; });
17
-
18
- afterEach(async () => {
19
- for (const s of sessionsToKill) {
20
- try { await killSession(s); } catch { /* ignore */ }
21
- }
22
- sessionsToKill.length = 0;
23
- for (const c of cleanups) await c();
24
- cleanups.length = 0;
25
- delete process.env.NFO_HOME;
26
- });
27
-
28
- it('throws when orchestra is unknown', async () => {
29
- const cfg = await makeTmpConfig();
30
- cleanups.push(cfg.cleanup);
31
- process.env.NFO_HOME = cfg.path;
32
- await expect(killOrchestra('does-not-exist', { yes: true })).rejects.toThrow(/Unknown orchestra/);
33
- });
34
-
35
- it('kills tmux session and archives state.json when -y', async () => {
36
- const repo: TmpRepo = await makeTmpRepo();
37
- cleanups.push(repo.cleanup);
38
- const cfg = await makeTmpConfig();
39
- cleanups.push(cfg.cleanup);
40
- process.env.NFO_HOME = cfg.path;
41
-
42
- const orchestraId = projectKeyFromPath(repo.path);
43
- await ensureOrchestraDir(orchestraId);
44
- await writeState(orchestraId, makeInitialState({
45
- orchestraId,
46
- projectPath: repo.path,
47
- permissionLevel: 'supervised',
48
- }));
49
-
50
- const name = sessionName(orchestraId);
51
- sessionsToKill.push(name);
52
- await createDetachedSession(name, repo.path, 220, 50);
53
-
54
- expect(await sessionExists(name)).toBe(true);
55
-
56
- await killOrchestra(orchestraId, { yes: true });
57
-
58
- expect(await sessionExists(name)).toBe(false);
59
- // state.json gone (moved to archive/)
60
- const reloaded = await readState(orchestraId);
61
- expect(reloaded).toBeNull();
62
- // archive/ has at least one state-<ts>.json
63
- const archived = readdirSync(archiveDir(orchestraId));
64
- expect(archived.some(f => /^state-\d+\.json$/.test(f))).toBe(true);
65
- });
66
- });
@@ -1,75 +0,0 @@
1
- import { describe, it, expect, afterEach, beforeEach } from 'vitest';
2
- import { existsSync, readFileSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- import { createOrchestra } from '../../src/commands/launch.js';
5
- import { makeTmpRepo, type TmpRepo } from '../helpers/tmp-repo.js';
6
- import { makeTmpConfig } from '../helpers/tmp-config.js';
7
- import { readState } from '../../src/state.js';
8
- import { projectKeyFromPath } from '../../src/project-key.js';
9
- import { sessionExists, killSession, sessionName } from '../../src/tmux.js';
10
- import { orchestraDir } from '../../src/config.js';
11
-
12
- describe('launch in a repo with no prior orchestra', () => {
13
- const cleanups: Array<() => Promise<void>> = [];
14
- const sessionsToKill: string[] = [];
15
-
16
- beforeEach(() => {
17
- process.env.NFO_HOME = '';
18
- });
19
-
20
- afterEach(async () => {
21
- for (const s of sessionsToKill) {
22
- try { await killSession(s); } catch { /* ignore */ }
23
- }
24
- sessionsToKill.length = 0;
25
- for (const c of cleanups) await c();
26
- cleanups.length = 0;
27
- delete process.env.NFO_HOME;
28
- });
29
-
30
- it('creates an orchestra and a tmux session', async () => {
31
- const repo: TmpRepo = await makeTmpRepo();
32
- cleanups.push(repo.cleanup);
33
- const cfg = await makeTmpConfig();
34
- cleanups.push(cfg.cleanup);
35
- process.env.NFO_HOME = cfg.path;
36
-
37
- const orchestraId = projectKeyFromPath(repo.path);
38
- const result = await createOrchestra({
39
- repoRoot: repo.path,
40
- orchestraId,
41
- permissionLevel: 'supervised',
42
- dryRun: true,
43
- });
44
-
45
- expect(result.action).toBe('created');
46
- expect(result.orchestraId).toBe(orchestraId);
47
- sessionsToKill.push(sessionName(result.orchestraId));
48
-
49
- const state = await readState(result.orchestraId);
50
- expect(state).not.toBeNull();
51
- expect(state!.project_path).toBe(repo.path);
52
- expect(state!.permission_level).toBe('supervised');
53
- expect(await sessionExists(sessionName(result.orchestraId))).toBe(true);
54
-
55
- const mcpCfg = join(orchestraDir(result.orchestraId), 'mcp-config.json');
56
- expect(existsSync(mcpCfg)).toBe(true);
57
- const parsed = JSON.parse(readFileSync(mcpCfg, 'utf8'));
58
- expect(parsed.mcpServers.nfo.command).toBe('nfo');
59
- expect(parsed.mcpServers.nfo.args).toEqual(['mcp-server', '--orchestra-id', result.orchestraId]);
60
-
61
- const { execa } = await import('execa');
62
- const { stdout: paneCount } = await execa('tmux', [
63
- 'list-panes', '-t', `${sessionName(result.orchestraId)}:0`, '-F', '#{pane_index}',
64
- ]);
65
- expect(paneCount.trim().split('\n').length).toBe(1);
66
- const { stdout: windows } = await execa('tmux', [
67
- 'list-windows', '-t', sessionName(result.orchestraId), '-F', '#{window_name}',
68
- ]);
69
- expect(windows).toContain('nfo-dashboard');
70
- const { stdout: status } = await execa('tmux', [
71
- 'show-options', '-t', sessionName(result.orchestraId), 'status',
72
- ]);
73
- expect(status.trim()).toBe('status off');
74
- });
75
- });
@@ -1,47 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { listOrchestras } from '../../src/commands/list.js';
3
- import { ensureOrchestraDir, writeState } from '../../src/state.js';
4
- import { makeInitialState } from '../../src/state.types.js';
5
- import { makeTmpConfig } from '../helpers/tmp-config.js';
6
-
7
- describe('listOrchestras', () => {
8
- const cleanups: Array<() => Promise<void>> = [];
9
-
10
- beforeEach(() => { process.env.NFO_HOME = ''; });
11
- afterEach(async () => {
12
- for (const c of cleanups) await c();
13
- cleanups.length = 0;
14
- delete process.env.NFO_HOME;
15
- });
16
-
17
- it('returns empty array when no orchestras exist', async () => {
18
- const tmp = await makeTmpConfig();
19
- cleanups.push(tmp.cleanup);
20
- process.env.NFO_HOME = tmp.path;
21
- expect(await listOrchestras()).toEqual([]);
22
- });
23
-
24
- it('lists all orchestras with summary info', async () => {
25
- const tmp = await makeTmpConfig();
26
- cleanups.push(tmp.cleanup);
27
- process.env.NFO_HOME = tmp.path;
28
-
29
- await ensureOrchestraDir('aaa-one');
30
- await writeState('aaa-one', makeInitialState({
31
- orchestraId: 'aaa-one',
32
- projectPath: '/tmp/one',
33
- permissionLevel: 'supervised',
34
- }));
35
- await ensureOrchestraDir('bbb-two');
36
- await writeState('bbb-two', makeInitialState({
37
- orchestraId: 'bbb-two',
38
- projectPath: '/tmp/two',
39
- permissionLevel: 'autonomous',
40
- }));
41
-
42
- const list = await listOrchestras();
43
- expect(list).toHaveLength(2);
44
- const ids = list.map(o => o.id).sort();
45
- expect(ids).toEqual(['aaa-one', 'bbb-two']);
46
- });
47
- });
@@ -1,53 +0,0 @@
1
- import { describe, it, expect, afterEach, beforeEach } from 'vitest';
2
- import { openNotes } from '../../src/commands/notes.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
-
9
- describe('openNotes', () => {
10
- const cleanups: Array<() => Promise<void>> = [];
11
- const originalEditor = process.env.EDITOR;
12
-
13
- beforeEach(() => { process.env.NFO_HOME = ''; });
14
-
15
- afterEach(async () => {
16
- for (const c of cleanups) await c();
17
- cleanups.length = 0;
18
- delete process.env.NFO_HOME;
19
- if (originalEditor === undefined) {
20
- delete process.env.EDITOR;
21
- } else {
22
- process.env.EDITOR = originalEditor;
23
- }
24
- });
25
-
26
- it('throws when orchestra is unknown', async () => {
27
- const cfg = await makeTmpConfig();
28
- cleanups.push(cfg.cleanup);
29
- process.env.NFO_HOME = cfg.path;
30
- await expect(openNotes('nope-doesnt-exist')).rejects.toThrow(/Unknown orchestra/);
31
- });
32
-
33
- it('invokes $EDITOR with the notes directory path', async () => {
34
- const repo: TmpRepo = await makeTmpRepo();
35
- cleanups.push(repo.cleanup);
36
- const cfg = await makeTmpConfig();
37
- cleanups.push(cfg.cleanup);
38
- process.env.NFO_HOME = cfg.path;
39
- // Use `true` as EDITOR — it accepts any args and exits 0 with no side effects.
40
- process.env.EDITOR = 'true';
41
-
42
- const orchestraId = projectKeyFromPath(repo.path);
43
- await ensureOrchestraDir(orchestraId);
44
- await writeState(orchestraId, makeInitialState({
45
- orchestraId,
46
- projectPath: repo.path,
47
- permissionLevel: 'supervised',
48
- }));
49
-
50
- // Should resolve without throwing — `true` consumes the arg and exits cleanly.
51
- await expect(openNotes(orchestraId)).resolves.toBeUndefined();
52
- });
53
- });