nfo-cli 0.0.4-improve-prompting → 0.0.6-a89844d-dev

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