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,132 +0,0 @@
1
- import { readState, writeState } from './state.js';
2
- import type {
3
- ArchivedMusician,
4
- Musician,
5
- MusicianReport,
6
- MusicianStatus,
7
- OrchestraState,
8
- } from './state.types.js';
9
-
10
- async function update(
11
- orchestraId: string,
12
- mutator: (s: OrchestraState) => void,
13
- ): Promise<void> {
14
- const state = await readState(orchestraId);
15
- if (!state) {
16
- throw new Error(`Unknown orchestra: ${orchestraId}`);
17
- }
18
- mutator(state);
19
- await writeState(orchestraId, state);
20
- }
21
-
22
- export async function addMusician(orchestraId: string, m: Musician): Promise<void> {
23
- await update(orchestraId, (s) => { s.musicians.push(m); });
24
- }
25
-
26
- export async function setMusicianStatus(
27
- orchestraId: string,
28
- musicianId: string,
29
- status: MusicianStatus,
30
- pendingPermission?: string | null,
31
- ): Promise<void> {
32
- await update(orchestraId, (s) => {
33
- const m = s.musicians.find((mu) => { return mu.id === musicianId; });
34
- if (!m) {
35
- throw new Error(`Unknown musician: ${musicianId}`);
36
- }
37
- m.status = status;
38
- if (pendingPermission !== undefined) {
39
- m.pending_permission = pendingPermission;
40
- }
41
- });
42
- }
43
-
44
- export async function setMusicianClaudeSessionId(
45
- orchestraId: string,
46
- musicianId: string,
47
- sessionId: string,
48
- ): Promise<void> {
49
- await update(orchestraId, (s) => {
50
- const m = s.musicians.find((mu) => { return mu.id === musicianId; });
51
- if (!m) {
52
- throw new Error(`Unknown musician: ${musicianId}`);
53
- }
54
- m.claude_session_id = sessionId;
55
- });
56
- }
57
-
58
- export async function setMusicianTmuxWindowId(
59
- orchestraId: string,
60
- musicianId: string,
61
- tmuxWindowId: string,
62
- ): Promise<void> {
63
- await update(orchestraId, (s) => {
64
- const m = s.musicians.find((mu) => { return mu.id === musicianId; });
65
- if (!m) {
66
- throw new Error(`Unknown musician: ${musicianId}`);
67
- }
68
- m.tmux_window_id = tmuxWindowId;
69
- });
70
- }
71
-
72
- export async function touchMusicianActivity(
73
- orchestraId: string,
74
- musicianId: string,
75
- timestamp?: string,
76
- ): Promise<void> {
77
- const ts = timestamp ?? new Date().toISOString();
78
- await update(orchestraId, (s) => {
79
- const m = s.musicians.find((mu) => { return mu.id === musicianId; });
80
- if (!m) {
81
- throw new Error(`Unknown musician: ${musicianId}`);
82
- }
83
- m.last_activity = ts;
84
- });
85
- }
86
-
87
- export async function setMusicianLatestReport(
88
- orchestraId: string,
89
- musicianId: string,
90
- report: MusicianReport | null,
91
- ): Promise<void> {
92
- await update(orchestraId, (s) => {
93
- const m = s.musicians.find((mu) => { return mu.id === musicianId; });
94
- if (!m) {
95
- throw new Error(`Unknown musician: ${musicianId}`);
96
- }
97
- m.latest_report = report;
98
- });
99
- }
100
-
101
- export async function setOrchestratorSessionId(
102
- orchestraId: string,
103
- sessionId: string,
104
- ): Promise<void> {
105
- await update(orchestraId, (s) => { s.orchestrator_session_id = sessionId; });
106
- }
107
-
108
- export interface ArchiveArgs {
109
- summary: string | null;
110
- dismissedAt?: string;
111
- }
112
-
113
- export async function archiveMusician(
114
- orchestraId: string,
115
- musicianId: string,
116
- args: ArchiveArgs,
117
- ): Promise<void> {
118
- await update(orchestraId, (s) => {
119
- const idx = s.musicians.findIndex((mu) => { return mu.id === musicianId; });
120
- if (idx === -1) {
121
- throw new Error(`Unknown musician: ${musicianId}`);
122
- }
123
- const [m] = s.musicians.splice(idx, 1);
124
- const archived: ArchivedMusician = {
125
- ...m,
126
- status: 'stopped',
127
- dismissed_at: args.dismissedAt ?? new Date().toISOString(),
128
- summary: args.summary,
129
- };
130
- s.archived_musicians.push(archived);
131
- });
132
- }
package/src/state.ts DELETED
@@ -1,49 +0,0 @@
1
- import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
2
- import { existsSync } from 'node:fs';
3
- import { join, dirname } from 'node:path';
4
- import lockfile from 'proper-lockfile';
5
- import {
6
- notesDir,
7
- logsDir,
8
- messageLogsDir,
9
- worktreesDir,
10
- archiveDir,
11
- stateFile,
12
- orchestraDir,
13
- } from './config.js';
14
- import type { OrchestraState } from './state.types.js';
15
-
16
- export async function ensureOrchestraDir(projectKey: string): Promise<void> {
17
- await mkdir(orchestraDir(projectKey), { recursive: true });
18
- await mkdir(notesDir(projectKey), { recursive: true });
19
- await mkdir(logsDir(projectKey), { recursive: true });
20
- await mkdir(messageLogsDir(projectKey), { recursive: true });
21
- await mkdir(worktreesDir(projectKey), { recursive: true });
22
- await mkdir(archiveDir(projectKey), { recursive: true });
23
- }
24
-
25
- export async function readState(projectKey: string): Promise<OrchestraState | null> {
26
- const file = stateFile(projectKey);
27
- if (!existsSync(file)) return null;
28
- const buf = await readFile(file, 'utf8');
29
- return JSON.parse(buf) as OrchestraState;
30
- }
31
-
32
- export async function writeState(projectKey: string, state: OrchestraState): Promise<void> {
33
- const file = stateFile(projectKey);
34
- await mkdir(dirname(file), { recursive: true });
35
-
36
- // proper-lockfile needs the target file to exist before it can lock it.
37
- if (!existsSync(file)) {
38
- await writeFile(file, '{}', 'utf8');
39
- }
40
-
41
- const release = await lockfile.lock(file, { retries: { retries: 5, minTimeout: 50 } });
42
- try {
43
- const tmp = `${file}.tmp.${process.pid}.${Date.now()}`;
44
- await writeFile(tmp, JSON.stringify(state, null, 2), 'utf8');
45
- await rename(tmp, file);
46
- } finally {
47
- await release();
48
- }
49
- }
@@ -1,67 +0,0 @@
1
- import type { PermissionLevel } from "./permission.js";
2
-
3
- export type MusicianStatus =
4
- | "working"
5
- | "idle"
6
- | "awaiting_permission"
7
- | "stopped";
8
- export type SubagentModel = "sonnet" | "haiku";
9
-
10
- export interface MusicianReport {
11
- summary: string;
12
- next_steps: string | null;
13
- reported_at: string;
14
- }
15
-
16
- export interface Musician {
17
- id: string;
18
- name: string;
19
- task_summary: string;
20
- status: MusicianStatus;
21
- pending_permission?: string | null;
22
- tmux_window_id: string;
23
- claude_session_id: string | null;
24
- worktree_path: string | null;
25
- branch: string | null;
26
- spawned_at: string;
27
- last_activity: string;
28
- latest_report?: MusicianReport | null;
29
- model?: SubagentModel;
30
- }
31
-
32
- export interface ArchivedMusician extends Musician {
33
- dismissed_at: string;
34
- summary: string | null;
35
- }
36
-
37
- export interface OrchestraState {
38
- version: number;
39
- orchestra_id: string;
40
- project_path: string;
41
- created_at: string;
42
- permission_level: PermissionLevel;
43
- notify_on_permission?: boolean;
44
- orchestrator_session_id: string | null;
45
- musicians: Musician[];
46
- archived_musicians: ArchivedMusician[];
47
- }
48
-
49
- export function makeInitialState(args: {
50
- orchestraId: string;
51
- projectPath: string;
52
- permissionLevel: PermissionLevel;
53
- notifyOnPermission?: boolean;
54
- }): OrchestraState {
55
- const now = new Date().toISOString();
56
- return {
57
- version: 1,
58
- orchestra_id: args.orchestraId,
59
- project_path: args.projectPath,
60
- created_at: now,
61
- permission_level: args.permissionLevel,
62
- notify_on_permission: args.notifyOnPermission ?? false,
63
- orchestrator_session_id: null,
64
- musicians: [],
65
- archived_musicians: [],
66
- };
67
- }
package/src/tmux.ts DELETED
@@ -1,226 +0,0 @@
1
- import { execa } from 'execa';
2
- import { DASHBOARD_WINDOW_NAME } from './dashboard.js';
3
-
4
- const NFO_SESSION_MATCH = '#{m:^nfo-,#{session_name}}';
5
- const EMBED_SESSION_SUFFIX = '-embed';
6
- const REQUIRED_TERMINAL_FEATURES = [
7
- 'xterm*:extkeys',
8
- 'screen*:extkeys',
9
- 'tmux*:extkeys',
10
- ];
11
-
12
- export function sessionName(projectKey: string): string {
13
- return `nfo-${projectKey}`;
14
- }
15
-
16
- export function embeddedSessionName(projectKey: string): string {
17
- return `${sessionName(projectKey)}${EMBED_SESSION_SUFFIX}`;
18
- }
19
-
20
- export async function sessionExists(name: string): Promise<boolean> {
21
- const result = await execa('tmux', ['has-session', '-t', name], { reject: false });
22
- return result.exitCode === 0;
23
- }
24
-
25
- export async function createDetachedSession(
26
- name: string,
27
- cwd: string,
28
- width = 220,
29
- height = 50,
30
- ): Promise<void> {
31
- await execa('tmux', ['new-session', '-d', '-s', name, '-c', cwd, '-x', String(width), '-y', String(height)]);
32
- }
33
-
34
- export async function createDetachedWindow(
35
- session: string,
36
- windowName: string,
37
- cwd: string,
38
- command?: string,
39
- ): Promise<string> {
40
- const args = [
41
- 'new-window',
42
- '-t',
43
- session,
44
- '-n',
45
- windowName,
46
- '-c',
47
- cwd,
48
- '-d',
49
- '-P',
50
- '-F',
51
- '#{pane_id}',
52
- ];
53
- if (command) {
54
- args.push(command);
55
- }
56
- const { stdout } = await execa('tmux', args);
57
- return stdout.trim();
58
- }
59
-
60
- export async function killSession(name: string): Promise<void> {
61
- await execa('tmux', ['kill-session', '-t', name], { reject: false });
62
- }
63
-
64
- export async function createLinkedSession(
65
- sourceSession: string,
66
- linkedSession: string,
67
- cwd: string,
68
- ): Promise<void> {
69
- await execa('tmux', ['new-session', '-d', '-t', sourceSession, '-s', linkedSession, '-c', cwd]);
70
- }
71
-
72
- export async function ensureEmbeddedSession(
73
- sourceSession: string,
74
- linkedSession: string,
75
- cwd: string,
76
- ): Promise<void> {
77
- if (!(await sessionExists(linkedSession))) {
78
- await createLinkedSession(sourceSession, linkedSession, cwd);
79
- }
80
- await ensureNfoSessionUi(linkedSession);
81
- await selectWindow(linkedSession, '0');
82
- }
83
-
84
- export async function attachSession(name: string): Promise<void> {
85
- // Inherits stdio so the user's terminal becomes the tmux client.
86
- await execa('tmux', ['attach-session', '-t', name], { stdio: 'inherit' });
87
- }
88
-
89
- export async function detachCurrentClient(): Promise<void> {
90
- await execa('tmux', ['detach-client']);
91
- }
92
-
93
- export async function splitWindowHorizontal(
94
- target: string,
95
- columns: number,
96
- command?: string,
97
- ): Promise<void> {
98
- // Use -l (absolute column count) rather than -p (percent) because percent
99
- // requires an attached client with a known terminal size.
100
- const args = ['split-window', '-h', '-l', String(columns), '-t', target];
101
- if (command) args.push(command);
102
- await execa('tmux', args);
103
- }
104
-
105
- export async function sendKeys(target: string, text: string, withEnter: boolean): Promise<void> {
106
- // Use -l (literal) to avoid keystroke interpretation.
107
- await execa('tmux', ['send-keys', '-l', '-t', target, '--', text]);
108
- if (withEnter) {
109
- await execa('tmux', ['send-keys', '-t', target, 'Enter']);
110
- }
111
- }
112
-
113
- export async function respawnPane(target: string, command: string): Promise<void> {
114
- await execa('tmux', ['respawn-pane', '-k', '-t', target, command]);
115
- }
116
-
117
- export async function capturePane(target: string, lines: number): Promise<string> {
118
- const { stdout } = await execa('tmux', [
119
- 'capture-pane',
120
- '-p',
121
- '-t',
122
- target,
123
- '-S',
124
- `-${lines}`,
125
- ]);
126
- return stdout;
127
- }
128
-
129
- export async function captureVisiblePane(target: string): Promise<string> {
130
- const { stdout } = await execa('tmux', [
131
- 'capture-pane',
132
- '-p',
133
- '-N',
134
- '-t',
135
- target,
136
- '-S',
137
- '0',
138
- '-E',
139
- '-',
140
- ]);
141
- return stdout;
142
- }
143
-
144
- export async function setSessionOption(name: string, option: string, value: string): Promise<void> {
145
- await execa('tmux', ['set-option', '-t', name, option, value]);
146
- }
147
-
148
- export async function setPaneOption(target: string, option: string, value: string): Promise<void> {
149
- await execa('tmux', ['set-option', '-p', '-t', target, option, value]);
150
- }
151
-
152
- function parseArrayOptionValue(line: string, option: string): string {
153
- return line.replace(new RegExp(`^${option}\\[\\d+\\]\\s+`, 'u'), '');
154
- }
155
-
156
- async function ensureSessionTerminalFeatures(
157
- name: string,
158
- features: string[],
159
- ): Promise<void> {
160
- const { stdout } = await execa('tmux', ['show-options', '-t', name, 'terminal-features']);
161
- const configured = new Set(
162
- stdout
163
- .split('\n')
164
- .map((line) => { return line.trim(); })
165
- .filter((line) => { return line.length > 0; })
166
- .map((line) => { return parseArrayOptionValue(line, 'terminal-features'); }),
167
- );
168
-
169
- for (const feature of features) {
170
- if (configured.has(feature)) {
171
- continue;
172
- }
173
- await execa('tmux', ['set-option', '-as', '-t', name, 'terminal-features', `,${feature}`]);
174
- }
175
- }
176
-
177
- async function getRootBindingLine(key: string): Promise<string | null> {
178
- const { stdout } = await execa('tmux', ['list-keys', '-T', 'root']);
179
- const line = stdout
180
- .split('\n')
181
- .map((entry) => { return entry.trim(); })
182
- .find((entry) => { return entry.startsWith(`bind-key -T root ${key} `); });
183
- return line ?? null;
184
- }
185
-
186
- async function ensureRootKeyBinding(
187
- key: string,
188
- nfoCommand: string,
189
- ): Promise<void> {
190
- const existing = await getRootBindingLine(key);
191
- if (existing && !existing.includes(NFO_SESSION_MATCH)) {
192
- // Respect user-defined bindings that are not NFO-managed.
193
- return;
194
- }
195
- await execa('tmux', [
196
- 'bind-key',
197
- '-n',
198
- key,
199
- 'if-shell',
200
- '-F',
201
- NFO_SESSION_MATCH,
202
- nfoCommand,
203
- `send-keys ${key}`,
204
- ]);
205
- }
206
-
207
- export async function ensureNfoNavigationBindings(): Promise<void> {
208
- await ensureRootKeyBinding('F6', `select-window -t :${DASHBOARD_WINDOW_NAME}`);
209
- await ensureRootKeyBinding('F7', 'select-window -t :0');
210
- }
211
-
212
- export async function ensureNfoSessionUi(name: string): Promise<void> {
213
- await setSessionOption(name, 'mouse', 'on');
214
- await setSessionOption(name, 'status', 'off');
215
- await setSessionOption(name, 'extended-keys', 'on');
216
- await ensureSessionTerminalFeatures(name, REQUIRED_TERMINAL_FEATURES);
217
- await ensureNfoNavigationBindings();
218
- }
219
-
220
- export async function selectWindow(name: string, windowTarget: string): Promise<void> {
221
- await execa('tmux', ['select-window', '-t', `${name}:${windowTarget}`]);
222
- }
223
-
224
- export async function selectPane(target: string): Promise<void> {
225
- await execa('tmux', ['select-pane', '-t', target]);
226
- }