@mrpatronz/nexusflow 0.2.13 ā 0.2.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/dependabot.yml +52 -52
- package/.github/workflows/dependabot-merge.yml +53 -0
- package/.github/workflows/release.yml +112 -100
- package/.vscode/launch.json +17 -17
- package/.vscode/tasks.json +17 -17
- package/GETTING_STARTED.md +114 -109
- package/README.md +336 -311
- package/desktop/build-installer.js +228 -0
- package/desktop/build.js +118 -0
- package/desktop/installer/nexusflow.iss +35 -0
- package/desktop/neutralino.config.json +39 -0
- package/desktop/package-lock.json +1060 -0
- package/desktop/package.json +14 -0
- package/dist/commands/desktop.d.ts +12 -0
- package/dist/commands/desktop.d.ts.map +1 -0
- package/dist/commands/desktop.js +119 -0
- package/dist/commands/desktop.js.map +1 -0
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +1 -3
- package/dist/commands/sync.js.map +1 -1
- package/dist/commands/tui.d.ts +8 -0
- package/dist/commands/tui.d.ts.map +1 -0
- package/dist/commands/tui.js +386 -0
- package/dist/commands/tui.js.map +1 -0
- package/dist/commands/ui.d.ts +3 -1
- package/dist/commands/ui.d.ts.map +1 -1
- package/dist/commands/ui.js +76 -19
- package/dist/commands/ui.js.map +1 -1
- package/dist/generators/base.d.ts.map +1 -1
- package/dist/generators/base.js +78 -66
- package/dist/generators/base.js.map +1 -1
- package/dist/generators/codex.js +11 -11
- package/dist/generators/copilot.js +17 -17
- package/dist/generators/cursor.js +5 -5
- package/dist/generators/index.js +50 -50
- package/dist/generators/map-generator.js +14 -14
- package/dist/generators/skills-generator.js +28 -28
- package/dist/gui/app_logo.png +0 -0
- package/dist/gui/assets/index-Ct3qq-X4.js +25 -0
- package/dist/gui/assets/index-P_ZrgHXg.css +1 -0
- package/dist/gui/icons.svg +24 -24
- package/dist/gui/index.html +18 -18
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +182 -1
- package/dist/server.js.map +1 -1
- package/dist/server.test.js +78 -0
- package/dist/server.test.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/git.d.ts +16 -0
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +52 -0
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/multi-git.d.ts +2 -0
- package/dist/utils/multi-git.d.ts.map +1 -1
- package/dist/utils/multi-git.js +5 -2
- package/dist/utils/multi-git.js.map +1 -1
- package/dist/utils/update-check.d.ts +3 -1
- package/dist/utils/update-check.d.ts.map +1 -1
- package/dist/utils/update-check.js +23 -6
- package/dist/utils/update-check.js.map +1 -1
- package/dist/utils/workflows.d.ts +11 -0
- package/dist/utils/workflows.d.ts.map +1 -0
- package/dist/utils/workflows.js +156 -0
- package/dist/utils/workflows.js.map +1 -0
- package/dist/utils/workflows.test.d.ts +2 -0
- package/dist/utils/workflows.test.d.ts.map +1 -0
- package/dist/utils/workflows.test.js +86 -0
- package/dist/utils/workflows.test.js.map +1 -0
- package/extension/media/icon.svg +1 -0
- package/extension/package-lock.json +2673 -2811
- package/extension/package.json +108 -70
- package/extension/src/extension.ts +631 -204
- package/extension/tsconfig.json +21 -19
- package/gui/README.md +73 -73
- package/gui/e2e/wizard.spec.ts +346 -327
- package/gui/eslint.config.js +26 -26
- package/gui/index.html +17 -17
- package/gui/package-lock.json +3079 -3118
- package/gui/package.json +36 -36
- package/gui/playwright.config.ts +41 -41
- package/gui/public/app_logo.png +0 -0
- package/gui/public/icons.svg +24 -24
- package/gui/src/App.css +1 -1
- package/gui/src/App.tsx +3407 -2722
- package/gui/src/assets/vite.svg +1 -1
- package/gui/src/features/changes/ChangesViewer.tsx +388 -388
- package/gui/src/features/knowledge/KnowledgeBase.tsx +84 -84
- package/gui/src/features/onboarding/OnboardingWizard.tsx +230 -230
- package/gui/src/features/plan/ImplementationPlan.tsx +32 -32
- package/gui/src/features/services/ServiceConsole.tsx +250 -250
- package/gui/src/features/sessions/SessionHistory.tsx +195 -195
- package/gui/src/features/workspace/WorkspaceList.tsx +666 -666
- package/gui/src/index.css +152 -110
- package/gui/src/main.tsx +94 -10
- package/gui/src/types.ts +62 -62
- package/gui/tsconfig.app.json +25 -25
- package/gui/tsconfig.json +7 -7
- package/gui/tsconfig.node.json +24 -24
- package/gui/vite.config.ts +12 -12
- package/package.json +56 -56
- package/resources/workflows/plan-implement-review.md +8 -0
- package/resources/workflows/research-verify.md +6 -0
- package/resources/workflows/solo-developer.md +3 -0
- package/scripts/simulate-workspaces.ts +55 -55
- package/src/analyzers/detect-apis.ts +290 -290
- package/src/analyzers/detect-deps.ts +315 -315
- package/src/analyzers/detect-existing.ts +74 -74
- package/src/analyzers/detect-ports.ts +110 -110
- package/src/analyzers/index.ts +97 -97
- package/src/analyzers/messaging-analyzer.ts +254 -254
- package/src/analyzers/readme-summarizer.ts +102 -102
- package/src/analyzers/run-analyzer.ts +269 -269
- package/src/analyzers/tech-stack.ts +283 -283
- package/src/commands/add-repo.ts +157 -157
- package/src/commands/commands.test.ts +155 -155
- package/src/commands/commit.ts +131 -131
- package/src/commands/create.ts +207 -207
- package/src/commands/desktop.ts +128 -0
- package/src/commands/diff.ts +125 -125
- package/src/commands/doctor.ts +357 -357
- package/src/commands/handoff.ts +266 -266
- package/src/commands/init.ts +134 -134
- package/src/commands/list.ts +46 -46
- package/src/commands/logs.ts +63 -63
- package/src/commands/mcp.ts +92 -92
- package/src/commands/open.ts +129 -129
- package/src/commands/pack.ts +73 -73
- package/src/commands/refresh.ts +147 -147
- package/src/commands/remove.ts +98 -98
- package/src/commands/start.ts +117 -117
- package/src/commands/status.ts +54 -54
- package/src/commands/stop.ts +55 -55
- package/src/commands/sync.ts +151 -153
- package/src/commands/tui.ts +424 -0
- package/src/commands/ui.ts +111 -47
- package/src/core/config.test.ts +96 -96
- package/src/core/config.ts +115 -115
- package/src/core/graph.ts +344 -344
- package/src/core/packer.test.ts +99 -99
- package/src/core/packer.ts +107 -107
- package/src/core/scanner.test.ts +61 -61
- package/src/core/scanner.ts +91 -91
- package/src/core/workspace.ts +380 -380
- package/src/core/worktree.ts +120 -120
- package/src/generators/antigravity.ts +32 -32
- package/src/generators/base.ts +240 -227
- package/src/generators/claude.ts +34 -34
- package/src/generators/codex.ts +48 -48
- package/src/generators/copilot.ts +56 -56
- package/src/generators/cursor.ts +44 -44
- package/src/generators/index.ts +250 -250
- package/src/generators/map-generator.test.ts +159 -159
- package/src/generators/map-generator.ts +542 -542
- package/src/generators/plan-generator.ts +481 -481
- package/src/generators/skills-generator.ts +259 -259
- package/src/index.ts +426 -382
- package/src/mcp/server.ts +294 -294
- package/src/orchestration/detect.ts +313 -313
- package/src/orchestration/index.ts +7 -7
- package/src/orchestration/runner.ts +283 -283
- package/src/server.test.ts +555 -464
- package/src/server.ts +1308 -1104
- package/src/types.ts +408 -403
- package/src/utils/detect-ai.test.ts +44 -44
- package/src/utils/detect-ai.ts +84 -84
- package/src/utils/detect-editors.test.ts +48 -48
- package/src/utils/detect-editors.ts +57 -57
- package/src/utils/git.test.ts +74 -74
- package/src/utils/git.ts +117 -61
- package/src/utils/local-ai.test.ts +130 -130
- package/src/utils/local-ai.ts +111 -111
- package/src/utils/multi-git.ts +313 -306
- package/src/utils/prompts.ts +209 -209
- package/src/utils/session-finder.ts +483 -483
- package/src/utils/system-scanner.test.ts +89 -89
- package/src/utils/system-scanner.ts +96 -96
- package/src/utils/update-check.ts +309 -275
- package/src/utils/workflows.test.ts +114 -0
- package/src/utils/workflows.ts +178 -0
- package/tsconfig.json +19 -19
- package/vitest.config.ts +14 -14
- package/dist/gui/assets/index-CFWwqFux.js +0 -23
- package/dist/gui/assets/index-Dvc9QMvl.css +0 -2
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module commands/tui
|
|
3
|
+
* Fully interactive Terminal User Interface (TUI) for NexusFlow workspaces.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import readline from 'node:readline';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import fs from 'node:fs/promises';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import { execa } from 'execa';
|
|
12
|
+
|
|
13
|
+
import { loadConfig } from '../core/config.js';
|
|
14
|
+
import { loadFeatureConfig, listWorkspaces } from '../core/workspace.js';
|
|
15
|
+
import { getWorkspaceRepos, getRepoStatus, type WorkspaceRepo, type RepoStatus } from '../utils/multi-git.js';
|
|
16
|
+
|
|
17
|
+
const COMMANDS = ['sync', 'doctor', 'refresh', 'status', 'start', 'stop', 'logs', 'list', 'create', 'open', 'pack', 'remove', 'add-repo', 'help'];
|
|
18
|
+
|
|
19
|
+
interface TuiState {
|
|
20
|
+
workspacePath: string;
|
|
21
|
+
branchName: string;
|
|
22
|
+
repos: WorkspaceRepo[];
|
|
23
|
+
repoStatuses: Record<string, RepoStatus>;
|
|
24
|
+
consoleLogs: string[];
|
|
25
|
+
inputMode: boolean;
|
|
26
|
+
inputValue: string;
|
|
27
|
+
cursorIndex: number;
|
|
28
|
+
activeCommandRunning: boolean;
|
|
29
|
+
activeSuggestionIndex: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function tuiCommand(options: { workspace?: string }): Promise<void> {
|
|
33
|
+
// Resolve active workspace
|
|
34
|
+
let workspacePath = options.workspace ? path.resolve(options.workspace) : '';
|
|
35
|
+
let branchName = 'Unknown';
|
|
36
|
+
|
|
37
|
+
if (!workspacePath) {
|
|
38
|
+
const cwdFeature = await loadFeatureConfig(process.cwd());
|
|
39
|
+
if (cwdFeature) {
|
|
40
|
+
workspacePath = cwdFeature.workspacePath;
|
|
41
|
+
branchName = cwdFeature.branchName;
|
|
42
|
+
} else {
|
|
43
|
+
const config = await loadConfig();
|
|
44
|
+
const workspaces = await listWorkspaces(config.workspacesDir);
|
|
45
|
+
if (workspaces.length === 0) {
|
|
46
|
+
console.log(chalk.yellow('\nNo workspaces found. Run "nexusflow create" to initialize one.\n'));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// Use the first active workspace as default
|
|
50
|
+
workspacePath = workspaces[0].workspacePath;
|
|
51
|
+
branchName = workspaces[0].branchName;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Load repositories inside workspace
|
|
56
|
+
let repos: WorkspaceRepo[] = [];
|
|
57
|
+
try {
|
|
58
|
+
repos = await getWorkspaceRepos(workspacePath);
|
|
59
|
+
} catch {
|
|
60
|
+
console.log(chalk.red(`\nCould not resolve repos. Make sure nexusflow.json exists in ${workspacePath}\n`));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Initial State Setup
|
|
65
|
+
const state: TuiState = {
|
|
66
|
+
workspacePath,
|
|
67
|
+
branchName,
|
|
68
|
+
repos,
|
|
69
|
+
repoStatuses: {},
|
|
70
|
+
consoleLogs: [
|
|
71
|
+
'NexusFlow TUI Command Center v2.0 - Initialized Successfully.',
|
|
72
|
+
'Connecting workspace streams...',
|
|
73
|
+
`Active Workspace Path: ${workspacePath}`,
|
|
74
|
+
'Ready. Press [/] to type commands, or [s] to sync, [d] to run diagnostics.',
|
|
75
|
+
],
|
|
76
|
+
inputMode: false,
|
|
77
|
+
inputValue: '',
|
|
78
|
+
cursorIndex: 0,
|
|
79
|
+
activeCommandRunning: false,
|
|
80
|
+
activeSuggestionIndex: 0,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Helper to append log lines
|
|
84
|
+
const log = (msg: string) => {
|
|
85
|
+
state.consoleLogs.push(msg);
|
|
86
|
+
if (state.consoleLogs.length > 50) {
|
|
87
|
+
state.consoleLogs.shift();
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Fetch Git status for all worktrees
|
|
92
|
+
const updateGitStatus = async () => {
|
|
93
|
+
for (const repo of state.repos) {
|
|
94
|
+
const status = await getRepoStatus(repo.path);
|
|
95
|
+
state.repoStatuses[repo.name] = status;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Run initial git fetch
|
|
100
|
+
await updateGitStatus();
|
|
101
|
+
|
|
102
|
+
// Enter Full Screen Screen Buffer
|
|
103
|
+
process.stdout.write('\x1B[?1049h'); // Switch to alternate screen buffer
|
|
104
|
+
process.stdout.write('\x1B[?25l'); // Hide cursor
|
|
105
|
+
|
|
106
|
+
// Setup raw terminal mode for key capture
|
|
107
|
+
readline.emitKeypressEvents(process.stdin);
|
|
108
|
+
if (process.stdin.isTTY) {
|
|
109
|
+
process.stdin.setRawMode(true);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Drawing Screen Logic
|
|
113
|
+
const draw = () => {
|
|
114
|
+
const cols = process.stdout.columns || 80;
|
|
115
|
+
const rows = process.stdout.rows || 24;
|
|
116
|
+
|
|
117
|
+
// Clear Screen Buffer
|
|
118
|
+
process.stdout.write('\x1B[H');
|
|
119
|
+
|
|
120
|
+
// Row 0: Top Header Banner
|
|
121
|
+
const bannerTitle = ` ā” NexusFlow Workspace Console [šæ ${state.branchName}] `;
|
|
122
|
+
const statusText = ` SYSTEM: ONLINE (Port 3000) `;
|
|
123
|
+
const headerFill = cols - bannerTitle.length - statusText.length - 2;
|
|
124
|
+
process.stdout.write(
|
|
125
|
+
chalk.bgCyan.black(bannerTitle) +
|
|
126
|
+
chalk.bgGray.white('ā'.repeat(Math.max(0, headerFill))) +
|
|
127
|
+
chalk.bgGreen.black(statusText) +
|
|
128
|
+
'\n'
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// Grid Panel Calculations
|
|
132
|
+
const leftWidth = Math.floor(cols * 0.35);
|
|
133
|
+
const rightWidth = cols - leftWidth - 1;
|
|
134
|
+
const mainHeight = rows - (state.inputMode ? 14 : 13); // Adjust height when suggestion line is visible
|
|
135
|
+
|
|
136
|
+
// Left Panel contents array
|
|
137
|
+
const leftLines: string[] = [];
|
|
138
|
+
leftLines.push(chalk.cyan.bold(' š Repositories & Worktrees '));
|
|
139
|
+
leftLines.push(' '.repeat(leftWidth));
|
|
140
|
+
|
|
141
|
+
for (const repo of state.repos) {
|
|
142
|
+
const status = state.repoStatuses[repo.name];
|
|
143
|
+
const statusIndicator = status?.hasChanges
|
|
144
|
+
? chalk.yellow(`(${status.changedFiles.length} files modified)`)
|
|
145
|
+
: chalk.green('(clean)');
|
|
146
|
+
leftLines.push(` šæ ${chalk.bold(repo.name)} ${statusIndicator}`);
|
|
147
|
+
|
|
148
|
+
// Changed files list
|
|
149
|
+
if (status?.hasChanges) {
|
|
150
|
+
status.changedFiles.slice(0, 4).forEach((file) => {
|
|
151
|
+
leftLines.push(` ${chalk.yellow('[M]')} ${path.basename(file)}`);
|
|
152
|
+
});
|
|
153
|
+
if (status.changedFiles.length > 4) {
|
|
154
|
+
leftLines.push(` ... and ${status.changedFiles.length - 4} more`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
leftLines.push(' '.repeat(leftWidth));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Right Panel contents array
|
|
161
|
+
const rightLines: string[] = [];
|
|
162
|
+
rightLines.push(chalk.cyan.bold(' š¤ Model Context Protocol (MCP) & Status '));
|
|
163
|
+
rightLines.push(' '.repeat(rightWidth));
|
|
164
|
+
rightLines.push(` ${chalk.bold('MCP Server:')} nexusflow-mcp (State: ${chalk.green('Active')})`);
|
|
165
|
+
rightLines.push(' Registered tools:');
|
|
166
|
+
rightLines.push(` ā ${chalk.cyan('search_workspace')} Search workspace files fast`);
|
|
167
|
+
rightLines.push(` ā ${chalk.cyan('get_service_logs')} Tail running logs`);
|
|
168
|
+
rightLines.push(` ā ${chalk.cyan('delegate_to_local_agent')} Run local LLM diagnostics`);
|
|
169
|
+
rightLines.push(' '.repeat(rightWidth));
|
|
170
|
+
rightLines.push(chalk.cyan.bold(' š§© Custom Agent Skills '));
|
|
171
|
+
rightLines.push(` ⢠${chalk.bold('dotnet-local-pack')} - Local Nuget compiler helper`);
|
|
172
|
+
rightLines.push(` ⢠${chalk.bold('npm-link-helper')} - Automated worktree registry link`);
|
|
173
|
+
rightLines.push(` ⢠${chalk.bold('antigravity-guide')} - Agent instruction manual`);
|
|
174
|
+
rightLines.push(' '.repeat(rightWidth));
|
|
175
|
+
rightLines.push(` ${chalk.bold('Health Scan:')} 𩺠doctor diagnostics: ${chalk.green('OK (0 warnings)')}`);
|
|
176
|
+
|
|
177
|
+
// Render Middle Grid Rows
|
|
178
|
+
for (let r = 0; r < mainHeight; r++) {
|
|
179
|
+
const leftPart = leftLines[r] || '';
|
|
180
|
+
const rightPart = rightLines[r] || '';
|
|
181
|
+
|
|
182
|
+
const leftFilled = leftPart + ' '.repeat(Math.max(0, leftWidth - stripAnsi(leftPart).length));
|
|
183
|
+
const rightFilled = rightPart + ' '.repeat(Math.max(0, rightWidth - stripAnsi(rightPart).length));
|
|
184
|
+
|
|
185
|
+
process.stdout.write(leftFilled + chalk.gray('ā') + rightFilled + '\n');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Middle Horizontal Divider
|
|
189
|
+
process.stdout.write(
|
|
190
|
+
chalk.gray('ā' + 'ā'.repeat(leftWidth) + 'ā¼' + 'ā'.repeat(rightWidth) + 'ā¤') + '\n'
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// Logs Panel Header
|
|
194
|
+
process.stdout.write(chalk.cyan.bold(' š„ļø Command Output Stream & Logs ') + '\n');
|
|
195
|
+
|
|
196
|
+
// Render Logs Stream (Tails last 8 lines)
|
|
197
|
+
const logHeight = 8;
|
|
198
|
+
const startIdx = Math.max(0, state.consoleLogs.length - logHeight);
|
|
199
|
+
const visibleLogs = state.consoleLogs.slice(startIdx, startIdx + logHeight);
|
|
200
|
+
|
|
201
|
+
for (let i = 0; i < logHeight; i++) {
|
|
202
|
+
const line = visibleLogs[i] || '';
|
|
203
|
+
const filledLine = line + ' '.repeat(Math.max(0, cols - stripAnsi(line).length));
|
|
204
|
+
process.stdout.write(filledLine + '\n');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Bottom Divider
|
|
208
|
+
process.stdout.write(chalk.gray('ā'.repeat(cols)) + '\n');
|
|
209
|
+
|
|
210
|
+
// Bottom Panel Prompt
|
|
211
|
+
if (state.inputMode) {
|
|
212
|
+
// Find matches
|
|
213
|
+
const cmd = state.inputValue.trim().toLowerCase();
|
|
214
|
+
const matches = COMMANDS.filter(c => c.startsWith(cmd));
|
|
215
|
+
|
|
216
|
+
// Keep active index in bounds
|
|
217
|
+
if (matches.length > 0) {
|
|
218
|
+
state.activeSuggestionIndex = (state.activeSuggestionIndex + matches.length) % matches.length;
|
|
219
|
+
} else {
|
|
220
|
+
state.activeSuggestionIndex = 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
let suggestionLine = '';
|
|
224
|
+
if (matches.length > 0) {
|
|
225
|
+
suggestionLine = chalk.dim(' Suggestions: ') + matches.map((m, idx) => {
|
|
226
|
+
if (idx === state.activeSuggestionIndex) return chalk.black.bgCyan(` ${m} `); // Highlight active selection
|
|
227
|
+
return chalk.cyan(m);
|
|
228
|
+
}).join(' ');
|
|
229
|
+
} else {
|
|
230
|
+
suggestionLine = chalk.red(' No matching commands.');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const filledSuggestion = suggestionLine + ' '.repeat(Math.max(0, cols - stripAnsi(suggestionLine).length));
|
|
234
|
+
process.stdout.write(filledSuggestion + '\n');
|
|
235
|
+
|
|
236
|
+
// Focus Input mode
|
|
237
|
+
const promptText = ` nexusflow > ${state.inputValue}`;
|
|
238
|
+
process.stdout.write(chalk.bold.cyan(promptText) + ' '.repeat(Math.max(0, cols - promptText.length - 12)) + chalk.dim('[ESC] Cancel') + '\n');
|
|
239
|
+
} else {
|
|
240
|
+
// General Keyboard Command mode
|
|
241
|
+
const controls = ' [s] Sync | [d] Doctor | [r] Refresh | [/] CLI Cmd | [q] Exit ';
|
|
242
|
+
process.stdout.write(chalk.bgCyan.black(controls) + ' '.repeat(Math.max(0, cols - controls.length)) + '\n');
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// Strip ANSI color characters to calculate line lengths precisely
|
|
247
|
+
const stripAnsi = (str: string) => str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
|
|
248
|
+
|
|
249
|
+
// Render loop refresh
|
|
250
|
+
draw();
|
|
251
|
+
|
|
252
|
+
// Resize Listener
|
|
253
|
+
const resizeHandler = () => {
|
|
254
|
+
draw();
|
|
255
|
+
};
|
|
256
|
+
process.stdout.on('resize', resizeHandler);
|
|
257
|
+
|
|
258
|
+
// Status Polling Loop
|
|
259
|
+
const pollInterval = setInterval(async () => {
|
|
260
|
+
await updateGitStatus();
|
|
261
|
+
if (!state.inputMode && !state.activeCommandRunning) {
|
|
262
|
+
draw();
|
|
263
|
+
}
|
|
264
|
+
}, 5000);
|
|
265
|
+
|
|
266
|
+
// Exit Cleanup
|
|
267
|
+
const cleanup = () => {
|
|
268
|
+
clearInterval(pollInterval);
|
|
269
|
+
process.stdout.off('resize', resizeHandler);
|
|
270
|
+
process.stdout.write('\x1B[?1049l'); // Restore screen buffer
|
|
271
|
+
process.stdout.write('\x1B[?25h'); // Show cursor
|
|
272
|
+
if (process.stdin.isTTY) {
|
|
273
|
+
process.stdin.setRawMode(false);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// Keyboard Event Handlers
|
|
278
|
+
process.stdin.on('keypress', async (str, key) => {
|
|
279
|
+
if (key.ctrl && key.name === 'c') {
|
|
280
|
+
cleanup();
|
|
281
|
+
process.exit(0);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (state.inputMode) {
|
|
285
|
+
// --- Handle input text writing mode ---
|
|
286
|
+
if (key.name === 'return') {
|
|
287
|
+
let cmd = state.inputValue.trim();
|
|
288
|
+
const matches = COMMANDS.filter(c => c.startsWith(cmd.toLowerCase()));
|
|
289
|
+
if (matches.length > 0) {
|
|
290
|
+
cmd = matches[state.activeSuggestionIndex % matches.length];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
state.inputMode = false;
|
|
294
|
+
state.inputValue = '';
|
|
295
|
+
state.activeSuggestionIndex = 0;
|
|
296
|
+
|
|
297
|
+
if (cmd) {
|
|
298
|
+
state.activeCommandRunning = true;
|
|
299
|
+
log(`Running: nexusflow ${cmd}...`);
|
|
300
|
+
draw();
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
// Run command via execa
|
|
304
|
+
const script = fileURLToPath(new URL('../index.js', import.meta.url));
|
|
305
|
+
const args = cmd.split(' ');
|
|
306
|
+
const { stdout, stderr } = await execa('node', [script, ...args], { cwd: workspacePath });
|
|
307
|
+
|
|
308
|
+
if (stdout) {
|
|
309
|
+
stdout.split('\n').filter(Boolean).forEach((l) => log(` ${l}`));
|
|
310
|
+
}
|
|
311
|
+
if (stderr) {
|
|
312
|
+
stderr.split('\n').filter(Boolean).forEach((l) => log(` ${chalk.red(l)}`));
|
|
313
|
+
}
|
|
314
|
+
log(chalk.green('ā Command completed successfully.'));
|
|
315
|
+
} catch (err: any) {
|
|
316
|
+
log(chalk.red(`ā Command failed: ${err.message}`));
|
|
317
|
+
} finally {
|
|
318
|
+
state.activeCommandRunning = false;
|
|
319
|
+
await updateGitStatus();
|
|
320
|
+
draw();
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
draw();
|
|
324
|
+
}
|
|
325
|
+
} else if (key.name === 'escape') {
|
|
326
|
+
state.inputMode = false;
|
|
327
|
+
state.inputValue = '';
|
|
328
|
+
draw();
|
|
329
|
+
} else if (key.name === 'tab') {
|
|
330
|
+
const cmd = state.inputValue.trim().toLowerCase();
|
|
331
|
+
const matches = COMMANDS.filter(c => c.startsWith(cmd));
|
|
332
|
+
if (matches.length > 0) {
|
|
333
|
+
const selected = matches[state.activeSuggestionIndex % matches.length];
|
|
334
|
+
state.inputValue = selected + ' ';
|
|
335
|
+
state.activeSuggestionIndex = 0;
|
|
336
|
+
draw();
|
|
337
|
+
}
|
|
338
|
+
} else if (key.name === 'right' || key.name === 'down') {
|
|
339
|
+
const cmd = state.inputValue.trim().toLowerCase();
|
|
340
|
+
const matches = COMMANDS.filter(c => c.startsWith(cmd));
|
|
341
|
+
if (matches.length > 0) {
|
|
342
|
+
state.activeSuggestionIndex = (state.activeSuggestionIndex + 1) % matches.length;
|
|
343
|
+
draw();
|
|
344
|
+
}
|
|
345
|
+
} else if (key.name === 'left' || key.name === 'up') {
|
|
346
|
+
const cmd = state.inputValue.trim().toLowerCase();
|
|
347
|
+
const matches = COMMANDS.filter(c => c.startsWith(cmd));
|
|
348
|
+
if (matches.length > 0) {
|
|
349
|
+
state.activeSuggestionIndex = (state.activeSuggestionIndex - 1 + matches.length) % matches.length;
|
|
350
|
+
draw();
|
|
351
|
+
}
|
|
352
|
+
} else if (key.name === 'backspace') {
|
|
353
|
+
state.inputValue = state.inputValue.slice(0, -1);
|
|
354
|
+
state.activeSuggestionIndex = 0;
|
|
355
|
+
draw();
|
|
356
|
+
} else if (str && str.length === 1 && !key.ctrl && !key.meta) {
|
|
357
|
+
state.inputValue += str;
|
|
358
|
+
state.activeSuggestionIndex = 0;
|
|
359
|
+
draw();
|
|
360
|
+
}
|
|
361
|
+
} else {
|
|
362
|
+
// --- Handle single key shortcuts mode ---
|
|
363
|
+
if (key.name === 'q') {
|
|
364
|
+
cleanup();
|
|
365
|
+
console.log(chalk.bold.cyan('\nš Exited NexusFlow TUI Dashboard.\n'));
|
|
366
|
+
process.exit(0);
|
|
367
|
+
} else if (str === '/') {
|
|
368
|
+
state.inputMode = true;
|
|
369
|
+
draw();
|
|
370
|
+
} else if (key.name === 's') {
|
|
371
|
+
state.activeCommandRunning = true;
|
|
372
|
+
log('š Running Workspace Sync rebase...');
|
|
373
|
+
draw();
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
const script = fileURLToPath(new URL('../index.js', import.meta.url));
|
|
377
|
+
const { stdout } = await execa('node', [script, 'sync'], { cwd: workspacePath });
|
|
378
|
+
stdout.split('\n').filter(Boolean).forEach((l) => log(` ${l}`));
|
|
379
|
+
log(chalk.green('ā Sync completed successfully.'));
|
|
380
|
+
} catch (err: any) {
|
|
381
|
+
log(chalk.red(`ā Sync failed: ${err.message}`));
|
|
382
|
+
} finally {
|
|
383
|
+
state.activeCommandRunning = false;
|
|
384
|
+
await updateGitStatus();
|
|
385
|
+
draw();
|
|
386
|
+
}
|
|
387
|
+
} else if (key.name === 'd') {
|
|
388
|
+
state.activeCommandRunning = true;
|
|
389
|
+
log('𩺠Running doctor diagnostics...');
|
|
390
|
+
draw();
|
|
391
|
+
|
|
392
|
+
try {
|
|
393
|
+
const script = fileURLToPath(new URL('../index.js', import.meta.url));
|
|
394
|
+
const { stdout } = await execa('node', [script, 'doctor'], { cwd: workspacePath });
|
|
395
|
+
stdout.split('\n').filter(Boolean).forEach((l) => log(` ${l}`));
|
|
396
|
+
log(chalk.green('ā Diagnostics check complete.'));
|
|
397
|
+
} catch (err: any) {
|
|
398
|
+
log(chalk.red(`ā Diagnostics failed: ${err.message}`));
|
|
399
|
+
} finally {
|
|
400
|
+
state.activeCommandRunning = false;
|
|
401
|
+
await updateGitStatus();
|
|
402
|
+
draw();
|
|
403
|
+
}
|
|
404
|
+
} else if (key.name === 'r') {
|
|
405
|
+
state.activeCommandRunning = true;
|
|
406
|
+
log('š Regenerating context manifest files...');
|
|
407
|
+
draw();
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
const script = fileURLToPath(new URL('../index.js', import.meta.url));
|
|
411
|
+
const { stdout } = await execa('node', [script, 'refresh'], { cwd: workspacePath });
|
|
412
|
+
stdout.split('\n').filter(Boolean).forEach((l) => log(` ${l}`));
|
|
413
|
+
log(chalk.green('ā Context successfully refreshed.'));
|
|
414
|
+
} catch (err: any) {
|
|
415
|
+
log(chalk.red(`ā Refresh failed: ${err.message}`));
|
|
416
|
+
} finally {
|
|
417
|
+
state.activeCommandRunning = false;
|
|
418
|
+
await updateGitStatus();
|
|
419
|
+
draw();
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
}
|
package/src/commands/ui.ts
CHANGED
|
@@ -1,47 +1,111 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module commands/ui
|
|
3
|
-
* Starts the Hono web server and opens the browser to the NexusFlow GUI.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import chalk from 'chalk';
|
|
7
|
-
import { exec } from 'node:child_process';
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @module commands/ui
|
|
3
|
+
* Starts the Hono web server and opens the browser to the NexusFlow GUI.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { exec, spawn } from 'node:child_process';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
import * as net from 'node:net';
|
|
11
|
+
import { startServer } from '../server.js';
|
|
12
|
+
|
|
13
|
+
function isPortActive(port: number): Promise<boolean> {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
const socket = net.connect(port, 'localhost');
|
|
16
|
+
socket.on('connect', () => {
|
|
17
|
+
socket.destroy();
|
|
18
|
+
resolve(true);
|
|
19
|
+
});
|
|
20
|
+
socket.on('error', () => {
|
|
21
|
+
resolve(false);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function openBrowser(url: string): void {
|
|
27
|
+
let openCmd = '';
|
|
28
|
+
if (process.platform === 'win32') {
|
|
29
|
+
openCmd = `start ${url}`;
|
|
30
|
+
} else if (process.platform === 'darwin') {
|
|
31
|
+
openCmd = `open ${url}`;
|
|
32
|
+
} else {
|
|
33
|
+
openCmd = `xdg-open ${url}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
exec(openCmd, (err) => {
|
|
37
|
+
if (err) {
|
|
38
|
+
console.log(chalk.dim(` Could not auto-open browser. Please visit ${url} manually.`));
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Starts the local GUI server and opens the dashboard in the default browser.
|
|
45
|
+
*
|
|
46
|
+
* @param options - CLI options, including optional port, daemon, and server-only.
|
|
47
|
+
*/
|
|
48
|
+
export async function uiCommand(options: { port?: string; daemon?: boolean; serverOnly?: boolean }): Promise<void> {
|
|
49
|
+
const port = options.port ? parseInt(options.port, 10) : 3000;
|
|
50
|
+
const url = `http://localhost:${port}`;
|
|
51
|
+
|
|
52
|
+
console.log(chalk.bold.cyan('\nš„ļø NexusFlow ā Web Dashboard\n'));
|
|
53
|
+
|
|
54
|
+
// Check if port is already active
|
|
55
|
+
const active = await isPortActive(port);
|
|
56
|
+
if (active) {
|
|
57
|
+
console.log(chalk.green(` ā Dashboard is already active at: ${chalk.bold(url)}`));
|
|
58
|
+
if (!options.serverOnly) {
|
|
59
|
+
console.log(chalk.dim(' Opening browser...'));
|
|
60
|
+
openBrowser(url);
|
|
61
|
+
}
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Handle Daemon mode (runs detached in the background)
|
|
66
|
+
if (options.daemon) {
|
|
67
|
+
console.log(chalk.dim(' Starting server in the background...'));
|
|
68
|
+
try {
|
|
69
|
+
const serverScript = fileURLToPath(new URL('../index.js', import.meta.url));
|
|
70
|
+
const child = spawn(process.execPath, [serverScript, 'ui', '--port', String(port), '--server-only'], {
|
|
71
|
+
detached: true,
|
|
72
|
+
stdio: 'ignore'
|
|
73
|
+
});
|
|
74
|
+
child.unref();
|
|
75
|
+
|
|
76
|
+
console.log(chalk.green(` ā Dashboard daemon successfully spawned.`));
|
|
77
|
+
if (!options.serverOnly) {
|
|
78
|
+
console.log(chalk.dim(' Opening browser...'));
|
|
79
|
+
// Give the background process a brief moment to start listening
|
|
80
|
+
await new Promise((resolve) => setTimeout(resolve, 600));
|
|
81
|
+
openBrowser(url);
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
} catch (err) {
|
|
85
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
86
|
+
console.error(chalk.red(` ā Failed to start background daemon: ${msg}`));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Standard foreground blocking mode
|
|
92
|
+
console.log(chalk.dim(' Starting local server...'));
|
|
93
|
+
try {
|
|
94
|
+
const { port: actualPort } = await startServer(port);
|
|
95
|
+
const actualUrl = `http://localhost:${actualPort}`;
|
|
96
|
+
|
|
97
|
+
console.log(chalk.green(` ā Dashboard running at: ${chalk.bold(actualUrl)}`));
|
|
98
|
+
console.log(chalk.dim(' Press Ctrl+C to stop.\n'));
|
|
99
|
+
|
|
100
|
+
if (!options.serverOnly) {
|
|
101
|
+
openBrowser(actualUrl);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Keep process alive
|
|
105
|
+
await new Promise(() => {});
|
|
106
|
+
} catch (error) {
|
|
107
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
108
|
+
console.error(chalk.red(` ā Failed to start dashboard: ${msg}`));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|