vgxness 1.2.1 → 1.3.1

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 (85) hide show
  1. package/README.md +20 -19
  2. package/dist/cli/cli-help.js +4 -7
  3. package/dist/cli/commands/index.js +1 -1
  4. package/dist/cli/commands/interactive-entrypoint-dispatcher.js +150 -0
  5. package/dist/cli/commands/setup-dispatcher.js +11 -8
  6. package/dist/cli/dispatcher.js +1 -8
  7. package/dist/cli/doctor-renderer.js +1 -1
  8. package/dist/cli/index.js +0 -0
  9. package/dist/cli/sdd-renderer.js +7 -7
  10. package/dist/cli/setup-status-renderer.js +1 -0
  11. package/dist/cli/tui/main-menu/index.js +0 -1
  12. package/dist/cli/tui/main-menu/main-menu-controller.js +0 -2
  13. package/dist/cli/tui/main-menu/main-menu-read-model.js +10 -8
  14. package/dist/cli/tui/main-menu/main-menu-render-shape.js +19 -2
  15. package/dist/cli/tui/main-menu/main-menu-state.js +1 -1
  16. package/dist/cli/tui/opentui/code/index.js +210 -0
  17. package/dist/cli/tui/opentui/code/screen.js +107 -0
  18. package/dist/cli/tui/opentui/code/smoke.js +32 -0
  19. package/dist/cli/tui/opentui/main-menu/index.js +3 -0
  20. package/dist/cli/tui/opentui/main-menu/renderer.js +68 -0
  21. package/dist/cli/tui/opentui/main-menu/screen.js +68 -0
  22. package/dist/cli/tui/opentui/main-menu/smoke.js +17 -0
  23. package/dist/cli/tui/opentui/main-menu/view.js +8 -0
  24. package/dist/cli/tui/opentui/setup/index.js +3 -0
  25. package/dist/cli/tui/opentui/setup/renderer.js +87 -0
  26. package/dist/cli/tui/opentui/setup/screen.js +170 -0
  27. package/dist/cli/tui/opentui/setup/smoke.js +42 -0
  28. package/dist/cli/tui/opentui/setup/view.js +12 -0
  29. package/dist/cli/tui/setup/setup-tui-input.js +43 -0
  30. package/dist/cli/tui/setup/setup-tui-read-model.js +4 -4
  31. package/dist/cli/tui/setup/setup-tui-render-shape.js +9 -10
  32. package/dist/cli/tui/setup/setup-tui-state.js +1 -1
  33. package/dist/cli/tui/setup/setup-tui-view-helpers.js +46 -0
  34. package/dist/cli/tui/visual/index.js +0 -2
  35. package/dist/code/runtime/sdd-context.js +2 -2
  36. package/dist/code/tui/approval-actions.js +33 -0
  37. package/dist/code/tui/prompt-mode.js +11 -0
  38. package/dist/code/tui/runtime-events.js +320 -0
  39. package/dist/mcp/validation.js +6 -2
  40. package/dist/orchestrator/natural-language-planner.js +1 -1
  41. package/dist/sdd/sdd-workflow-service.js +1 -25
  42. package/dist/setup/backup-rollback-service.js +2 -2
  43. package/dist/setup/providers/antigravity-setup-adapter.js +1 -1
  44. package/dist/setup/providers/claude-setup-adapter.js +2 -2
  45. package/dist/setup/providers/custom-setup-adapter.js +1 -1
  46. package/dist/setup/providers/opencode-setup-adapter.js +3 -3
  47. package/dist/setup/setup-lifecycle-service.js +6 -6
  48. package/dist/setup/setup-plan.js +3 -3
  49. package/dist/verification/verification-plan-service.js +1 -1
  50. package/docs/architecture.md +43 -42
  51. package/docs/cli.md +141 -133
  52. package/docs/funcionamiento-del-sistema.md +22 -23
  53. package/docs/harness-gap-analysis.md +15 -1
  54. package/docs/prd.md +14 -14
  55. package/docs/vgxcode.md +87 -0
  56. package/docs/vgxness-code.md +6 -4
  57. package/package.json +5 -6
  58. package/dist/cli/commands/dashboard-dispatcher.js +0 -560
  59. package/dist/cli/dashboard-operational-read-models.js +0 -428
  60. package/dist/cli/dashboard-renderer.js +0 -158
  61. package/dist/cli/dashboard-screen-renderers.js +0 -256
  62. package/dist/cli/dashboard-tui-read-model.js +0 -73
  63. package/dist/cli/dashboard-tui-state.js +0 -314
  64. package/dist/cli/guided-main-menu.js +0 -470
  65. package/dist/cli/interactive-dashboard.js +0 -34
  66. package/dist/cli/setup-wizard-read-model.js +0 -72
  67. package/dist/cli/setup-wizard-renderer.js +0 -155
  68. package/dist/cli/setup-wizard-state.js +0 -82
  69. package/dist/cli/tui/dashboard/dashboard-adapter.js +0 -4
  70. package/dist/cli/tui/main-menu/main-menu-app.js +0 -28
  71. package/dist/cli/tui/render-ink-app.js +0 -10
  72. package/dist/cli/tui/setup/screens/applying-screen.js +0 -6
  73. package/dist/cli/tui/setup/screens/cancellation-screen.js +0 -6
  74. package/dist/cli/tui/setup/screens/error-recovery-screen.js +0 -6
  75. package/dist/cli/tui/setup/screens/final-confirmation-screen.js +0 -6
  76. package/dist/cli/tui/setup/screens/opencode-details-screen.js +0 -10
  77. package/dist/cli/tui/setup/screens/plan-review-screen.js +0 -6
  78. package/dist/cli/tui/setup/screens/project-database-screen.js +0 -6
  79. package/dist/cli/tui/setup/screens/provider-screen.js +0 -7
  80. package/dist/cli/tui/setup/screens/result-screen.js +0 -16
  81. package/dist/cli/tui/setup/screens/screen-components.js +0 -103
  82. package/dist/cli/tui/setup/screens/welcome-screen.js +0 -6
  83. package/dist/cli/tui/setup/setup-tui-app.js +0 -113
  84. package/dist/cli/tui/visual/choice-list.js +0 -10
  85. package/dist/cli/tui/visual/layout.js +0 -10
@@ -0,0 +1,210 @@
1
+ import { createCliRenderer } from '@opentui/core';
2
+ import { spawn } from 'node:child_process';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { ApprovalDecisionWriter, getApprovalActionsState } from '../../../../code/tui/approval-actions.js';
6
+ import { buildSafetyReadModel, parseVgxcodeJsonl, parseVgxcodeJsonlLine } from '../../../../code/tui/runtime-events.js';
7
+ import { parsePromptSubmission, toggleReadOnlyMode } from '../../../../code/tui/prompt-mode.js';
8
+ import { createVgxcodeScreen } from './screen.js';
9
+ async function main() {
10
+ const project = process.argv[2] ?? 'vgxness';
11
+ const parsedStdin = await readPipedRuntimeEvents();
12
+ const state = {
13
+ project,
14
+ prompt: '',
15
+ submittedPrompt: '',
16
+ events: [...parsedStdin.events],
17
+ errors: [...parsedStdin.errors],
18
+ status: parsedStdin.errors.length > 0 ? 'error' : parsedStdin.events.length > 0 ? 'completed' : 'idle',
19
+ source: parsedStdin.events.length > 0 || parsedStdin.errors.length > 0 ? 'stdin' : 'interactive',
20
+ mode: 'inspect',
21
+ childStdinOpen: false,
22
+ decisionWriter: undefined,
23
+ };
24
+ const renderer = await createCliRenderer({
25
+ exitOnCtrlC: true,
26
+ clearOnShutdown: true,
27
+ screenMode: 'alternate-screen',
28
+ consoleMode: 'disabled',
29
+ });
30
+ const render = () => {
31
+ const current = renderer.root.getRenderable('vgxcode-screen');
32
+ if (current)
33
+ renderer.root.remove('vgxcode-screen');
34
+ const safety = buildSafetyReadModel(state.events);
35
+ renderer.root.add(createVgxcodeScreen({
36
+ ...state,
37
+ approvalActionsEnabled: getApprovalActionsState({
38
+ source: state.source,
39
+ status: state.status,
40
+ mode: state.mode,
41
+ childStdinOpen: state.childStdinOpen,
42
+ pendingApprovalCount: safety.pendingApprovalCount,
43
+ ...(safety.latestPendingApproval === undefined ? {} : { latestPendingApproval: safety.latestPendingApproval }),
44
+ }).enabled,
45
+ }));
46
+ renderer.requestRender();
47
+ };
48
+ render();
49
+ if (state.source === 'interactive') {
50
+ renderer.keyInput.on('keypress', (key) => {
51
+ if (key.ctrl && key.name === 'c')
52
+ return;
53
+ if (state.status === 'running') {
54
+ if (key.name === 'a' || key.sequence === 'a')
55
+ writePendingApprovalDecision(state, 'approved', render);
56
+ if (key.name === 'd' || key.sequence === 'd')
57
+ writePendingApprovalDecision(state, 'denied', render);
58
+ return;
59
+ }
60
+ if (key.name === 'tab') {
61
+ state.mode = toggleReadOnlyMode(state.mode);
62
+ render();
63
+ return;
64
+ }
65
+ if (key.name === 'return' || key.name === 'enter') {
66
+ void runReadOnlyBridge(state, render);
67
+ return;
68
+ }
69
+ if (key.name === 'backspace' || key.name === 'delete') {
70
+ state.prompt = state.prompt.slice(0, -1);
71
+ render();
72
+ return;
73
+ }
74
+ const text = printableKeyText(key.sequence);
75
+ if (text !== '') {
76
+ state.prompt += text;
77
+ render();
78
+ }
79
+ });
80
+ }
81
+ }
82
+ async function readPipedRuntimeEvents() {
83
+ if (process.stdin.isTTY)
84
+ return { events: [], errors: [] };
85
+ let input = '';
86
+ for await (const chunk of process.stdin)
87
+ input += String(chunk);
88
+ return parseVgxcodeJsonl(input);
89
+ }
90
+ async function runReadOnlyBridge(state, render) {
91
+ const submission = parsePromptSubmission(state.prompt, state.mode);
92
+ const prompt = submission.prompt;
93
+ if (prompt === '') {
94
+ state.status = 'error';
95
+ state.errors = ['Enter a prompt before running inspect, plan, craft-preview, or craft. Prefix with /inspect, /plan, /craft-preview, or /craft; press Tab to switch inspect/plan.'];
96
+ render();
97
+ return;
98
+ }
99
+ state.mode = submission.mode;
100
+ state.submittedPrompt = prompt;
101
+ state.prompt = '';
102
+ state.status = 'running';
103
+ state.errors = [];
104
+ state.events = [];
105
+ state.childStdinOpen = false;
106
+ state.decisionWriter = undefined;
107
+ render();
108
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '../../../../..');
109
+ const approvalCapableCraft = state.mode === 'craft';
110
+ const child = spawn('bun', commandArgsForMode(state.mode, prompt), {
111
+ cwd: root,
112
+ stdio: approvalCapableCraft ? ['pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
113
+ env: process.env,
114
+ });
115
+ state.childStdinOpen = approvalCapableCraft && child.stdin !== null;
116
+ state.decisionWriter = approvalCapableCraft && child.stdin !== null ? new ApprovalDecisionWriter(child.stdin) : undefined;
117
+ if (child.stdout === null || child.stderr === null) {
118
+ state.status = 'error';
119
+ state.childStdinOpen = false;
120
+ state.decisionWriter = undefined;
121
+ state.errors.push('Failed to start root CLI with JSONL output streams.');
122
+ render();
123
+ return;
124
+ }
125
+ child.stdin?.on('close', () => {
126
+ state.childStdinOpen = false;
127
+ render();
128
+ });
129
+ child.stdin?.on('error', () => {
130
+ state.childStdinOpen = false;
131
+ render();
132
+ });
133
+ let lineNumber = 0;
134
+ let stdoutBuffer = '';
135
+ let stderr = '';
136
+ child.stdout.setEncoding('utf8');
137
+ child.stdout.on('data', (chunk) => {
138
+ stdoutBuffer += chunk;
139
+ const lines = stdoutBuffer.split(/\r?\n/);
140
+ stdoutBuffer = lines.pop() ?? '';
141
+ for (const line of lines) {
142
+ lineNumber += 1;
143
+ const parsed = parseVgxcodeJsonlLine(line, lineNumber);
144
+ if (parsed.event)
145
+ state.events.push(parsed.event);
146
+ if (parsed.error)
147
+ state.errors.push(parsed.error);
148
+ }
149
+ render();
150
+ });
151
+ child.stderr.setEncoding('utf8');
152
+ child.stderr.on('data', (chunk) => {
153
+ stderr += chunk;
154
+ });
155
+ child.on('error', (error) => {
156
+ state.status = 'error';
157
+ state.childStdinOpen = false;
158
+ state.errors.push(`Failed to start root CLI: ${error.message}`);
159
+ render();
160
+ });
161
+ child.on('close', (code, signal) => {
162
+ state.childStdinOpen = false;
163
+ state.decisionWriter = undefined;
164
+ if (stdoutBuffer.trim() !== '') {
165
+ lineNumber += 1;
166
+ const parsed = parseVgxcodeJsonlLine(stdoutBuffer, lineNumber);
167
+ if (parsed.event)
168
+ state.events.push(parsed.event);
169
+ if (parsed.error)
170
+ state.errors.push(parsed.error);
171
+ }
172
+ state.status = state.errors.length > 0 ? 'error' : 'completed';
173
+ if (code !== 0 && !(state.mode === 'craft-preview' && code === 3)) {
174
+ const detail = stderr.trim() === '' ? `signal ${signal ?? 'none'}` : stderr.trim();
175
+ state.status = 'error';
176
+ state.errors.push(`Root Bun CLI ${state.mode} failed (exit ${code ?? 'signal'}): ${detail}`);
177
+ }
178
+ render();
179
+ });
180
+ }
181
+ function commandArgsForMode(mode, prompt) {
182
+ const args = ['run', '--silent', 'cli:bun', '--', 'code', mode, prompt, '--events-jsonl'];
183
+ if (mode === 'craft')
184
+ args.push('--approval-channel', 'stdio');
185
+ return args;
186
+ }
187
+ function writePendingApprovalDecision(state, status, render) {
188
+ const model = buildSafetyReadModel(state.events);
189
+ const actions = getApprovalActionsState({
190
+ source: state.source,
191
+ status: state.status,
192
+ mode: state.mode,
193
+ childStdinOpen: state.childStdinOpen,
194
+ pendingApprovalCount: model.pendingApprovalCount,
195
+ ...(model.latestPendingApproval === undefined ? {} : { latestPendingApproval: model.latestPendingApproval }),
196
+ });
197
+ if (!actions.enabled)
198
+ return;
199
+ const wrote = state.decisionWriter?.write(actions.approval, status, status === 'approved' ? 'Approved from vgxcode.' : 'Denied from vgxcode.') ?? false;
200
+ if (!wrote)
201
+ return;
202
+ render();
203
+ }
204
+ function printableKeyText(sequence) {
205
+ if (sequence.length !== 1)
206
+ return '';
207
+ const code = sequence.charCodeAt(0);
208
+ return code >= 32 && code !== 127 ? sequence : '';
209
+ }
210
+ await main();
@@ -0,0 +1,107 @@
1
+ import { Box, Text } from '@opentui/core';
2
+ import { buildSafetyReadModel, summarizeEvent } from '../../../../code/tui/runtime-events.js';
3
+ export function createVgxcodeScreen(input) {
4
+ const latest = input.events.at(-1);
5
+ const transcript = input.events.filter((event) => event.type === 'provider.message').map(summarizeEvent);
6
+ const activity = input.events.filter((event) => event.type !== 'provider.message').map(summarizeEvent);
7
+ const safety = buildSafetyReadModel(input.events);
8
+ const status = input.errors.length > 0 ? 'error' : input.status;
9
+ const latestText = latest ? ` Last event: ${latest.type}` : '';
10
+ const modeDescription = describeMode(input.mode);
11
+ const promptLabel = input.source === 'stdin' ? 'piped JSONL replay (safe)' : `${input.mode} prompt (${modeDescription})`;
12
+ const submittedText = input.submittedPrompt === '' ? 'Last submitted: none' : `Last submitted (${input.mode}): ${input.submittedPrompt}`;
13
+ const promptText = input.source === 'stdin'
14
+ ? 'stdin JSONL detected; not spawning root CLI.'
15
+ : `Mode: ${input.mode} (${modeDescription}) State: ${status}\n${submittedText}\n> ${input.prompt}${input.status === 'running' ? ' [running]' : ''}`;
16
+ const errorText = input.errors.length === 0 ? '' : `\n\nErrors (${input.errors.length}):\n${input.errors.map((error) => `! ${error}`).join('\n')}`;
17
+ const safetyText = formatSafetyDetails(safety);
18
+ return Box({
19
+ id: 'vgxcode-screen',
20
+ flexDirection: 'column',
21
+ width: '100%',
22
+ height: '100%',
23
+ padding: 1,
24
+ gap: 1,
25
+ backgroundColor: '#080B12',
26
+ }, Box({
27
+ border: true,
28
+ borderStyle: 'rounded',
29
+ borderColor: '#7DD3FC',
30
+ paddingX: 1,
31
+ title: ' vgxcode ',
32
+ }, Text({ content: `Project: ${input.project} UI: OpenTUI/Bun experimental Source: ${input.source} State: ${status}${latestText}`, fg: '#E0F2FE', wrapMode: 'word' })), Box({
33
+ border: true,
34
+ borderStyle: 'rounded',
35
+ borderColor: '#A78BFA',
36
+ paddingX: 1,
37
+ title: ` Prompt — ${promptLabel} `,
38
+ }, Text({ content: promptText, fg: '#F8FAFC', wrapMode: 'word' })), Box({
39
+ flexDirection: 'row',
40
+ flexGrow: 1,
41
+ gap: 1,
42
+ }, Box({
43
+ border: true,
44
+ borderStyle: 'rounded',
45
+ borderColor: '#22C55E',
46
+ flexGrow: 2,
47
+ padding: 1,
48
+ title: ' Transcript ',
49
+ }, Text({ content: transcript.length === 0 ? 'No assistant output yet.' : transcript.join('\n'), fg: '#DCFCE7', wrapMode: 'word' })), Box({
50
+ border: true,
51
+ borderStyle: 'rounded',
52
+ borderColor: '#F59E0B',
53
+ flexGrow: 1,
54
+ padding: 1,
55
+ title: ' Activity ',
56
+ }, Text({ content: `${activity.length === 0 ? 'No runtime activity yet.' : activity.join('\n')}${errorText}`, fg: input.errors.length > 0 ? '#FECACA' : '#FEF3C7', wrapMode: 'word' }))), Box({
57
+ border: true,
58
+ borderStyle: 'rounded',
59
+ borderColor: '#38BDF8',
60
+ paddingX: 1,
61
+ title: ` Safety details — ${input.mode === 'craft' ? 'approval-capable craft' : 'preview/read-only'} `,
62
+ }, Text({ content: safetyText, fg: '#E0F2FE', wrapMode: 'word' })), Box({
63
+ border: true,
64
+ borderStyle: 'rounded',
65
+ borderColor: '#475569',
66
+ paddingX: 1,
67
+ title: ' Shortcuts ',
68
+ }, Text({ content: formatFooter(input.approvalActionsEnabled === true), fg: '#CBD5E1', wrapMode: 'word' })));
69
+ }
70
+ function formatSafetyDetails(safety) {
71
+ const approvalText = safety.latestPendingApproval
72
+ ? `! Pending approvals: ${safety.pendingApprovalCount} (latest: ${safety.latestPendingApproval.toolName})`
73
+ : `✓ Pending approvals: ${safety.pendingApprovalCount}`;
74
+ const decisionText = safety.latestApprovalDecision
75
+ ? `${approvalDecisionCue(safety.latestApprovalDecision.final)} Latest approval decision: ${safety.latestApprovalDecision.final} — ${safety.latestApprovalDecision.toolName}: ${safety.latestApprovalDecision.reason}`
76
+ : '· Latest approval decision: none';
77
+ const previewText = safety.latestApprovalPreview
78
+ ? `! Approval preview: ${safety.latestApprovalPreview.toolName} (${safety.latestApprovalPreview.capability}) — Approve disabled, Deny disabled. No mutation executed.${safety.latestApprovalPreview.targetPath ? ` Target: ${safety.latestApprovalPreview.targetPath}.` : ''}`
79
+ : '· Approval preview: none';
80
+ const changedFilesText = safety.changedFiles.length > 0
81
+ ? `! Changed files (${safety.changedFiles.length}): ${safety.changedFiles.join(', ')}`
82
+ : '✓ Changed files: none reported';
83
+ const diffText = safety.diffAvailability === 'filenames-only'
84
+ ? '· Diff preview: unavailable in current event stream; only changed filenames were reported.'
85
+ : safety.diffAvailability === 'preview' && safety.latestDiffPreview
86
+ ? formatDiffPreview(safety.latestDiffPreview)
87
+ : '· Diff preview: no changed files reported.';
88
+ return `${approvalText}\n${decisionText}\n${previewText}\n${changedFilesText}\n${diffText}\n· Craft is approval-capable and may mutate after you approve. Craft-preview is preview-only and never sends approval decisions.`;
89
+ }
90
+ function describeMode(mode) {
91
+ if (mode === 'craft')
92
+ return 'approval-capable, mutating after approval';
93
+ if (mode === 'craft-preview')
94
+ return 'preview-only, no mutation executed';
95
+ return 'read-only';
96
+ }
97
+ function formatFooter(approvalActionsEnabled) {
98
+ const approvalText = approvalActionsEnabled ? '[a] Approve pending [d] Deny pending' : 'Approve/Deny hidden until a live craft run has a pending approval';
99
+ return `[Enter] Run mode [Tab] Toggle inspect/plan Prefix: /inspect, /plan, /craft, or /craft-preview [Backspace] Edit ${approvalText} [Ctrl+C] Quit.`;
100
+ }
101
+ function formatDiffPreview(preview) {
102
+ const metadata = `· Diff preview: ${preview.files.length} files, ${preview.bodyBytes}/${preview.originalBytes} bytes, sha256:${preview.hash.slice(0, 12)}${preview.truncated ? ', truncated' : ''}${preview.redacted ? ', redacted' : ''}`;
103
+ return `${metadata}\n${preview.body}`;
104
+ }
105
+ function approvalDecisionCue(final) {
106
+ return final === 'allow' ? '✓' : '✕';
107
+ }
@@ -0,0 +1,32 @@
1
+ import { createTestRenderer } from '@opentui/core/testing';
2
+ import { parseVgxcodeJsonl, sampleEvents } from '../../../../code/tui/runtime-events.js';
3
+ import { createVgxcodeScreen } from './screen.js';
4
+ const input = await readPipedInput();
5
+ const parsed = input === '' ? { events: [], errors: [] } : parseVgxcodeJsonl(input);
6
+ if (parsed.errors.length > 0) {
7
+ throw new Error(`Smoke input contained invalid events: ${parsed.errors.join('; ')}`);
8
+ }
9
+ const events = parsed.events.length > 0 ? parsed.events : sampleEvents();
10
+ const setup = await createTestRenderer({ width: 140, height: 36 });
11
+ try {
12
+ setup.renderer.root.add(createVgxcodeScreen({ project: 'vgxness-smoke', events, prompt: 'smoke test prompt', submittedPrompt: 'previous smoke prompt', status: 'completed', errors: [], source: 'interactive', mode: 'inspect' }));
13
+ await setup.renderOnce();
14
+ const frame = setup.captureCharFrame();
15
+ const requiredText = ['vgxcode', 'vgxness-smoke', 'State: completed', 'Mode: inspect', 'Last submitted', 'Prompt', 'Transcript', 'Activity', 'Safety details', 'hidden until a live craft run', '/craft-preview'];
16
+ const missing = requiredText.filter((text) => !frame.includes(text));
17
+ if (missing.length > 0) {
18
+ throw new Error(`Smoke frame is missing expected text: ${missing.join(', ')}`);
19
+ }
20
+ console.log('vgxcode OpenTUI smoke passed');
21
+ }
22
+ finally {
23
+ setup.renderer.destroy();
24
+ }
25
+ async function readPipedInput() {
26
+ if (process.stdin.isTTY)
27
+ return '';
28
+ let input = '';
29
+ for await (const chunk of process.stdin)
30
+ input += String(chunk);
31
+ return input;
32
+ }
@@ -0,0 +1,3 @@
1
+ export * from './renderer.js';
2
+ export * from './screen.js';
3
+ export * from './view.js';
@@ -0,0 +1,68 @@
1
+ import { createCliRenderer } from '@opentui/core';
2
+ import { mainMenuActionFromInput, reduceMainMenuState } from '../../main-menu/main-menu-actions.js';
3
+ import { createMainMenuState } from '../../main-menu/main-menu-state.js';
4
+ import { createTuiViewport } from '../../visual/viewport.js';
5
+ import { createOpenTuiMainMenuScreen } from './screen.js';
6
+ export async function renderOpenTuiMainMenu(options = {}) {
7
+ const width = terminalWidth(options.stdout);
8
+ let state = options.initialState ?? createMainMenuState(width === undefined ? {} : { width });
9
+ const renderer = await createCliRenderer({
10
+ ...(options.stdin === undefined ? {} : { stdin: options.stdin }),
11
+ ...(options.stdout === undefined ? {} : { stdout: options.stdout }),
12
+ exitOnCtrlC: true,
13
+ clearOnShutdown: true,
14
+ screenMode: 'alternate-screen',
15
+ consoleMode: 'disabled',
16
+ });
17
+ await new Promise((resolve) => {
18
+ let finished = false;
19
+ const finish = (result) => {
20
+ if (finished)
21
+ return;
22
+ finished = true;
23
+ options.onResult?.(result);
24
+ renderer.destroy();
25
+ resolve();
26
+ };
27
+ const render = () => {
28
+ const current = renderer.root.getRenderable('main-menu-screen');
29
+ if (current)
30
+ renderer.root.remove('main-menu-screen');
31
+ renderer.root.add(createOpenTuiMainMenuScreen(state));
32
+ renderer.requestRender();
33
+ };
34
+ renderer.on('destroy', () => {
35
+ if (!finished)
36
+ resolve();
37
+ });
38
+ renderer.on('resize', () => {
39
+ state = { ...state, viewport: createTuiViewport(renderer.terminalWidth, state.viewport.width) };
40
+ render();
41
+ });
42
+ renderer.keyInput.on('keypress', (key) => {
43
+ if (key.ctrl && key.name === 'c')
44
+ return;
45
+ const action = mainMenuActionFromInput(key.sequence, keyToMainMenuInputKey(key));
46
+ if (action === undefined)
47
+ return;
48
+ const reduced = reduceMainMenuState(state, action);
49
+ state = reduced.state;
50
+ if (reduced.result !== undefined)
51
+ finish(reduced.result);
52
+ else
53
+ render();
54
+ });
55
+ render();
56
+ });
57
+ }
58
+ function terminalWidth(stdout) {
59
+ return stdout?.columns;
60
+ }
61
+ function keyToMainMenuInputKey(key) {
62
+ return {
63
+ upArrow: key.name === 'up',
64
+ downArrow: key.name === 'down',
65
+ return: key.name === 'return' || key.name === 'enter',
66
+ escape: key.name === 'escape',
67
+ };
68
+ }
@@ -0,0 +1,68 @@
1
+ import { Box, Text } from '@opentui/core';
2
+ import { tuiBadges } from '../../visual/badges.js';
3
+ import { buildMainMenuViewModel } from '../../main-menu/main-menu-read-model.js';
4
+ import { formatMainMenuOptions } from './view.js';
5
+ export function createOpenTuiMainMenuScreen(state) {
6
+ const vm = buildMainMenuViewModel(state);
7
+ return Box({
8
+ id: 'main-menu-screen',
9
+ flexDirection: 'column',
10
+ width: '100%',
11
+ height: '100%',
12
+ padding: 1,
13
+ gap: 1,
14
+ backgroundColor: '#080B12',
15
+ }, Box({
16
+ border: true,
17
+ borderStyle: 'rounded',
18
+ borderColor: '#7DD3FC',
19
+ paddingX: 1,
20
+ title: ' VGXNESS ',
21
+ }, Text({ content: `${vm.title}\n${vm.subtitle} ${tuiBadges.readOnly}\n${vm.contextLines.join('\n')}`, fg: '#E0F2FE', wrapMode: 'word' })), Box({
22
+ border: true,
23
+ borderStyle: 'rounded',
24
+ borderColor: '#14B8A6',
25
+ paddingX: 1,
26
+ title: ` ${vm.statusSnapshot.title} ${vm.statusSnapshot.badges.join(' ')} `,
27
+ }, Text({ content: vm.statusSnapshot.lines.join('\n'), fg: '#CCFBF1', wrapMode: 'word' })), Box({
28
+ flexDirection: state.viewport.mode === 'narrow' ? 'column' : 'row',
29
+ flexGrow: 1,
30
+ gap: 1,
31
+ }, Box({
32
+ border: true,
33
+ borderStyle: 'rounded',
34
+ borderColor: '#A78BFA',
35
+ flexGrow: 1,
36
+ padding: 1,
37
+ title: ' Menu ',
38
+ }, Text({ content: formatMainMenuOptions(vm.options), fg: '#F8FAFC', wrapMode: 'word' })), Box({
39
+ border: true,
40
+ borderStyle: 'rounded',
41
+ borderColor: '#22C55E',
42
+ flexGrow: 1,
43
+ padding: 1,
44
+ title: ` ${vm.detail.title}${vm.detail.badges.length === 0 ? '' : ` ${vm.detail.badges.join(' ')}`} `,
45
+ }, Text({ content: vm.detail.lines.join('\n'), fg: '#DCFCE7', wrapMode: 'word' }))), Box({
46
+ border: true,
47
+ borderStyle: 'rounded',
48
+ borderColor: '#F59E0B',
49
+ paddingX: 1,
50
+ title: ` Safety ${tuiBadges.noProviderWrites} `,
51
+ }, Text({ content: vm.safetyLines.join('\n'), fg: '#FEF3C7', wrapMode: 'word' })), ...(vm.helpLines.length === 0
52
+ ? []
53
+ : [
54
+ Box({
55
+ border: true,
56
+ borderStyle: 'rounded',
57
+ borderColor: '#38BDF8',
58
+ paddingX: 1,
59
+ title: ` Help ${tuiBadges.readOnly} `,
60
+ }, Text({ content: vm.helpLines.join('\n'), fg: '#E0F2FE', wrapMode: 'word' })),
61
+ ]), Box({
62
+ border: true,
63
+ borderStyle: 'rounded',
64
+ borderColor: '#475569',
65
+ paddingX: 1,
66
+ title: ' Shortcuts ',
67
+ }, Text({ content: vm.footer, fg: '#CBD5E1', wrapMode: 'word' })));
68
+ }
@@ -0,0 +1,17 @@
1
+ import { createTestRenderer } from '@opentui/core/testing';
2
+ import { createMainMenuState } from '../../main-menu/main-menu-state.js';
3
+ import { createOpenTuiMainMenuScreen } from './screen.js';
4
+ const setup = await createTestRenderer({ width: 120, height: 32 });
5
+ try {
6
+ setup.renderer.root.add(createOpenTuiMainMenuScreen(createMainMenuState({ focusedOptionId: 'setup', helpVisible: true, width: 120 })));
7
+ await setup.renderOnce();
8
+ const frame = setup.captureCharFrame();
9
+ const requiredText = ['VGXNESS Main Menu', 'Installation', 'Requires confirmation', 'No provider writes', 'final confirmation', 'q/Esc'];
10
+ const missing = requiredText.filter((text) => !frame.includes(text));
11
+ if (missing.length > 0)
12
+ throw new Error(`Main menu smoke frame is missing expected text: ${missing.join(', ')}`);
13
+ console.log('main-menu OpenTUI smoke passed');
14
+ }
15
+ finally {
16
+ setup.renderer.destroy();
17
+ }
@@ -0,0 +1,8 @@
1
+ import { formatBadges, tuiBadges } from '../../visual/badges.js';
2
+ export function formatMainMenuOptions(options) {
3
+ return options.map(formatMainMenuOption).join('\n');
4
+ }
5
+ function formatMainMenuOption(option) {
6
+ const badges = formatBadges([...(option.focused ? [tuiBadges.focused] : []), ...option.badges]);
7
+ return `${option.focused ? '›' : ' '} ${option.label}${badges.length === 0 ? '' : ` ${badges}`} — ${option.description}`;
8
+ }
@@ -0,0 +1,3 @@
1
+ export { renderOpenTuiSetup } from './renderer.js';
2
+ export { createOpenTuiSetupScreen } from './screen.js';
3
+ export { formatOpenTuiSetupScreen } from './view.js';
@@ -0,0 +1,87 @@
1
+ import { createCliRenderer } from '@opentui/core';
2
+ import { createSetupTuiController } from '../../setup/setup-tui-controller.js';
3
+ import { setupTuiActionFromInput } from '../../setup/setup-tui-input.js';
4
+ import { createSetupTuiState } from '../../setup/setup-tui-state.js';
5
+ import { createOpenTuiSetupScreen } from './screen.js';
6
+ export async function renderOpenTuiSetup(options) {
7
+ let state = options.initialState ?? createSetupTuiState({ selections: options.runtime.selections });
8
+ const renderer = await createCliRenderer({
9
+ ...(options.stdin === undefined ? {} : { stdin: options.stdin }),
10
+ ...(options.stdout === undefined ? {} : { stdout: options.stdout }),
11
+ exitOnCtrlC: true,
12
+ clearOnShutdown: true,
13
+ screenMode: 'alternate-screen',
14
+ consoleMode: 'disabled',
15
+ });
16
+ await new Promise((resolve) => {
17
+ let finished = false;
18
+ let dispatching = false;
19
+ const finish = () => {
20
+ if (finished)
21
+ return;
22
+ finished = true;
23
+ renderer.destroy();
24
+ resolve();
25
+ };
26
+ const render = () => {
27
+ const current = renderer.root.getRenderable('setup-screen');
28
+ if (current)
29
+ renderer.root.remove('setup-screen');
30
+ renderer.root.add(createOpenTuiSetupScreen(state, renderer.terminalWidth));
31
+ renderer.requestRender();
32
+ options.onState?.(state);
33
+ };
34
+ const dispatch = async (action) => {
35
+ dispatching = true;
36
+ try {
37
+ const controller = await createSetupTuiController({ state, services: options.services, runtime: options.runtime }).dispatch(action);
38
+ state = controller.state;
39
+ render();
40
+ if (state.screen === 'cancelled' && action.type === 'cancel')
41
+ finish();
42
+ }
43
+ finally {
44
+ dispatching = false;
45
+ }
46
+ };
47
+ renderer.on('destroy', () => {
48
+ if (!finished)
49
+ resolve();
50
+ });
51
+ renderer.on('resize', render);
52
+ renderer.keyInput.on('keypress', (key) => {
53
+ if (key.ctrl && key.name === 'c')
54
+ return;
55
+ if (dispatching)
56
+ return;
57
+ if (isCloseKey(key) && (state.screen === 'cancelled' || state.screen === 'result' || state.screen === 'error-recovery')) {
58
+ finish();
59
+ return;
60
+ }
61
+ const action = setupTuiActionFromInput(key.sequence, keyToSetupTuiInputKey(key), state);
62
+ if (action !== undefined)
63
+ void dispatch(action);
64
+ });
65
+ void createSetupTuiController({ state, services: options.services, runtime: options.runtime })
66
+ .load()
67
+ .then((controller) => {
68
+ if (finished)
69
+ return;
70
+ state = controller.state;
71
+ render();
72
+ });
73
+ });
74
+ }
75
+ function isCloseKey(key) {
76
+ return key.name === 'escape' || key.sequence === 'q';
77
+ }
78
+ function keyToSetupTuiInputKey(key) {
79
+ return {
80
+ upArrow: key.name === 'up',
81
+ downArrow: key.name === 'down',
82
+ return: key.name === 'return' || key.name === 'enter',
83
+ escape: key.name === 'escape',
84
+ tab: key.name === 'tab',
85
+ shift: key.shift === true,
86
+ };
87
+ }