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,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
- });