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.
- package/README.md +20 -19
- package/dist/cli/cli-help.js +4 -7
- package/dist/cli/commands/index.js +1 -1
- package/dist/cli/commands/interactive-entrypoint-dispatcher.js +150 -0
- package/dist/cli/commands/setup-dispatcher.js +11 -8
- package/dist/cli/dispatcher.js +1 -8
- package/dist/cli/doctor-renderer.js +1 -1
- package/dist/cli/index.js +0 -0
- package/dist/cli/sdd-renderer.js +7 -7
- package/dist/cli/setup-status-renderer.js +1 -0
- package/dist/cli/tui/main-menu/index.js +0 -1
- package/dist/cli/tui/main-menu/main-menu-controller.js +0 -2
- package/dist/cli/tui/main-menu/main-menu-read-model.js +10 -8
- package/dist/cli/tui/main-menu/main-menu-render-shape.js +19 -2
- package/dist/cli/tui/main-menu/main-menu-state.js +1 -1
- package/dist/cli/tui/opentui/code/index.js +210 -0
- package/dist/cli/tui/opentui/code/screen.js +107 -0
- package/dist/cli/tui/opentui/code/smoke.js +32 -0
- package/dist/cli/tui/opentui/main-menu/index.js +3 -0
- package/dist/cli/tui/opentui/main-menu/renderer.js +68 -0
- package/dist/cli/tui/opentui/main-menu/screen.js +68 -0
- package/dist/cli/tui/opentui/main-menu/smoke.js +17 -0
- package/dist/cli/tui/opentui/main-menu/view.js +8 -0
- package/dist/cli/tui/opentui/setup/index.js +3 -0
- package/dist/cli/tui/opentui/setup/renderer.js +87 -0
- package/dist/cli/tui/opentui/setup/screen.js +170 -0
- package/dist/cli/tui/opentui/setup/smoke.js +42 -0
- package/dist/cli/tui/opentui/setup/view.js +12 -0
- package/dist/cli/tui/setup/setup-tui-input.js +43 -0
- package/dist/cli/tui/setup/setup-tui-read-model.js +4 -4
- package/dist/cli/tui/setup/setup-tui-render-shape.js +9 -10
- package/dist/cli/tui/setup/setup-tui-state.js +1 -1
- package/dist/cli/tui/setup/setup-tui-view-helpers.js +46 -0
- package/dist/cli/tui/visual/index.js +0 -2
- package/dist/code/runtime/sdd-context.js +2 -2
- package/dist/code/tui/approval-actions.js +33 -0
- package/dist/code/tui/prompt-mode.js +11 -0
- package/dist/code/tui/runtime-events.js +320 -0
- package/dist/mcp/validation.js +6 -2
- package/dist/orchestrator/natural-language-planner.js +1 -1
- package/dist/sdd/sdd-workflow-service.js +1 -25
- package/dist/setup/backup-rollback-service.js +2 -2
- package/dist/setup/providers/antigravity-setup-adapter.js +1 -1
- package/dist/setup/providers/claude-setup-adapter.js +2 -2
- package/dist/setup/providers/custom-setup-adapter.js +1 -1
- package/dist/setup/providers/opencode-setup-adapter.js +3 -3
- package/dist/setup/setup-lifecycle-service.js +6 -6
- package/dist/setup/setup-plan.js +3 -3
- package/dist/verification/verification-plan-service.js +1 -1
- package/docs/architecture.md +43 -42
- package/docs/cli.md +141 -133
- package/docs/funcionamiento-del-sistema.md +22 -23
- package/docs/harness-gap-analysis.md +15 -1
- package/docs/prd.md +14 -14
- package/docs/vgxcode.md +87 -0
- package/docs/vgxness-code.md +6 -4
- package/package.json +5 -6
- package/dist/cli/commands/dashboard-dispatcher.js +0 -560
- package/dist/cli/dashboard-operational-read-models.js +0 -428
- package/dist/cli/dashboard-renderer.js +0 -158
- package/dist/cli/dashboard-screen-renderers.js +0 -256
- package/dist/cli/dashboard-tui-read-model.js +0 -73
- package/dist/cli/dashboard-tui-state.js +0 -314
- package/dist/cli/guided-main-menu.js +0 -470
- package/dist/cli/interactive-dashboard.js +0 -34
- package/dist/cli/setup-wizard-read-model.js +0 -72
- package/dist/cli/setup-wizard-renderer.js +0 -155
- package/dist/cli/setup-wizard-state.js +0 -82
- package/dist/cli/tui/dashboard/dashboard-adapter.js +0 -4
- package/dist/cli/tui/main-menu/main-menu-app.js +0 -28
- package/dist/cli/tui/render-ink-app.js +0 -10
- package/dist/cli/tui/setup/screens/applying-screen.js +0 -6
- package/dist/cli/tui/setup/screens/cancellation-screen.js +0 -6
- package/dist/cli/tui/setup/screens/error-recovery-screen.js +0 -6
- package/dist/cli/tui/setup/screens/final-confirmation-screen.js +0 -6
- package/dist/cli/tui/setup/screens/opencode-details-screen.js +0 -10
- package/dist/cli/tui/setup/screens/plan-review-screen.js +0 -6
- package/dist/cli/tui/setup/screens/project-database-screen.js +0 -6
- package/dist/cli/tui/setup/screens/provider-screen.js +0 -7
- package/dist/cli/tui/setup/screens/result-screen.js +0 -16
- package/dist/cli/tui/setup/screens/screen-components.js +0 -103
- package/dist/cli/tui/setup/screens/welcome-screen.js +0 -6
- package/dist/cli/tui/setup/setup-tui-app.js +0 -113
- package/dist/cli/tui/visual/choice-list.js +0 -10
- package/dist/cli/tui/visual/layout.js +0 -10
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { Box, Text } from '@opentui/core';
|
|
2
|
+
import { buildSetupTuiViewModel } from '../../setup/setup-tui-read-model.js';
|
|
3
|
+
import { formatFooter, setupFooterHints } from '../../setup/setup-tui-view-helpers.js';
|
|
4
|
+
export function createOpenTuiSetupScreen(state, width) {
|
|
5
|
+
const vm = buildSetupTuiViewModel(state);
|
|
6
|
+
const screen = setupScreenContent(state, vm, width);
|
|
7
|
+
return Box({
|
|
8
|
+
id: 'setup-screen',
|
|
9
|
+
flexDirection: 'column',
|
|
10
|
+
width: '100%',
|
|
11
|
+
height: '100%',
|
|
12
|
+
padding: 1,
|
|
13
|
+
backgroundColor: '#080B12',
|
|
14
|
+
gap: 1,
|
|
15
|
+
}, headerPanel(vm), Box({ flexDirection: width < 100 ? 'column' : 'row', flexGrow: 1, gap: 1 }, setupPanel({ title: screen.leftTitle, content: screen.left, borderColor: '#A78BFA', grow: 1 }), setupPanel({ title: screen.rightTitle, content: screen.right, borderColor: '#22C55E', grow: 1 })), setupPanel({ title: ' Safety ', content: screen.safety, borderColor: state.screen === 'final-confirmation' ? '#F59E0B' : '#38BDF8' }), ...(screen.help.length === 0 ? [] : [setupPanel({ title: ' Help [read-only] ', content: screen.help, borderColor: '#38BDF8' })]), Box({ border: true, borderStyle: 'rounded', borderColor: '#475569', paddingX: 1, title: ' Shortcuts ' }, Text({ content: screen.footer, fg: '#CBD5E1', wrapMode: 'word' })));
|
|
16
|
+
}
|
|
17
|
+
function headerPanel(vm) {
|
|
18
|
+
return Box({ border: true, borderStyle: 'rounded', borderColor: '#7DD3FC', paddingX: 1, title: ' VGXNESS Setup ' }, Text({
|
|
19
|
+
content: `${vm.title}\n${vm.progressLabel}\nProject: ${vm.projectLabel} · Provider: ${vm.providerLabel} · Database: ${vm.databaseLabel}`,
|
|
20
|
+
fg: '#E0F2FE',
|
|
21
|
+
wrapMode: 'word',
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
function setupPanel(input) {
|
|
25
|
+
return Box({
|
|
26
|
+
border: true,
|
|
27
|
+
borderStyle: 'rounded',
|
|
28
|
+
borderColor: input.borderColor,
|
|
29
|
+
paddingX: 1,
|
|
30
|
+
title: input.title,
|
|
31
|
+
...(input.grow === undefined ? {} : { flexGrow: input.grow }),
|
|
32
|
+
}, Text({ content: input.content.join('\n'), fg: '#F8FAFC', wrapMode: 'word' }));
|
|
33
|
+
}
|
|
34
|
+
function setupScreenContent(state, vm, width) {
|
|
35
|
+
const footer = footerForScreen(state.screen, width);
|
|
36
|
+
const safety = safetyLines(vm, state.screen);
|
|
37
|
+
const help = state.helpVisible ? vm.helpLines : [];
|
|
38
|
+
switch (state.screen) {
|
|
39
|
+
case 'welcome':
|
|
40
|
+
return {
|
|
41
|
+
leftTitle: ' Flow ',
|
|
42
|
+
left: ['1. Choose storage', '2. Choose provider', '3. Review OpenCode details', '4. Read-only plan preview', '5. Final confirmation', '6. Result and next steps'],
|
|
43
|
+
rightTitle: ' Current context ',
|
|
44
|
+
right: previewLines(vm),
|
|
45
|
+
safety,
|
|
46
|
+
help,
|
|
47
|
+
footer,
|
|
48
|
+
};
|
|
49
|
+
case 'project-database':
|
|
50
|
+
return {
|
|
51
|
+
leftTitle: ' Database choices ',
|
|
52
|
+
left: choiceLines(vm.databaseChoices),
|
|
53
|
+
rightTitle: focusedChoiceTitle(vm.databaseChoices, 'Database details'),
|
|
54
|
+
right: [...focusedChoiceDetails(vm.databaseChoices), `Mode: ${vm.databaseLabel}`, `Path: ${vm.databasePathLabel}`, `Source: ${vm.databaseSourceLabel}`],
|
|
55
|
+
safety,
|
|
56
|
+
help,
|
|
57
|
+
footer,
|
|
58
|
+
};
|
|
59
|
+
case 'provider':
|
|
60
|
+
return {
|
|
61
|
+
leftTitle: ' Provider choices ',
|
|
62
|
+
left: choiceLines(vm.providerChoices),
|
|
63
|
+
rightTitle: focusedChoiceTitle(vm.providerChoices, 'Provider details'),
|
|
64
|
+
right: [...focusedChoiceDetails(vm.providerChoices), vm.providerInstallabilityLabel],
|
|
65
|
+
safety,
|
|
66
|
+
help,
|
|
67
|
+
footer,
|
|
68
|
+
};
|
|
69
|
+
case 'opencode-details':
|
|
70
|
+
return {
|
|
71
|
+
leftTitle: ' OpenCode controls ',
|
|
72
|
+
left: [...choiceLines(vm.scopeChoices), '', ...choiceLines(vm.installModeChoices)].filter(Boolean),
|
|
73
|
+
rightTitle: ' Plan target ',
|
|
74
|
+
right: [`Scope: ${vm.scopeLabel}`, `Install mode: ${vm.installModeLabel}`, `Action: ${vm.opencodeActionLabel}`, `Target: ${vm.targetPathLabel}`, `Agents: ${vm.agentReadinessLabel}`],
|
|
75
|
+
safety,
|
|
76
|
+
help,
|
|
77
|
+
footer,
|
|
78
|
+
};
|
|
79
|
+
case 'plan-review':
|
|
80
|
+
return {
|
|
81
|
+
leftTitle: ' Read-only plan ',
|
|
82
|
+
left: planSummaryLines(vm),
|
|
83
|
+
rightTitle: ' Blockers / next ',
|
|
84
|
+
right: blockerAndNextLines(vm),
|
|
85
|
+
safety,
|
|
86
|
+
help,
|
|
87
|
+
footer,
|
|
88
|
+
};
|
|
89
|
+
case 'final-confirmation':
|
|
90
|
+
return {
|
|
91
|
+
leftTitle: ' Final confirmation ',
|
|
92
|
+
left: ['! This is the first write-capable step.', 'Press Enter only if the target and backups look correct.', 'Esc/q cancels without writing.', '', ...planSummaryLines(vm)],
|
|
93
|
+
rightTitle: ' Before apply ',
|
|
94
|
+
right: blockerAndNextLines(vm),
|
|
95
|
+
safety,
|
|
96
|
+
help,
|
|
97
|
+
footer,
|
|
98
|
+
};
|
|
99
|
+
case 'applying':
|
|
100
|
+
return { leftTitle: ' Applying ', left: ['Applying confirmed OpenCode setup...', 'Waiting for the apply operation to finish.'], rightTitle: ' Status ', right: previewLines(vm), safety, help, footer };
|
|
101
|
+
case 'result':
|
|
102
|
+
return { leftTitle: ' Result ', left: ['Setup finished.', ...vm.nextCommands.map((command) => `Next: ${command}`)], rightTitle: ' Summary ', right: previewLines(vm), safety, help, footer };
|
|
103
|
+
case 'error-recovery':
|
|
104
|
+
return { leftTitle: ' Recovery ', left: ['! Setup hit an error.', 'No unconfirmed provider config write was performed.', 'Inspect `vgxness setup plan`, resolve blockers, then retry.'], rightTitle: ' Context ', right: previewLines(vm), safety, help, footer };
|
|
105
|
+
case 'cancelled':
|
|
106
|
+
return { leftTitle: ' Cancelled ', left: ['Setup was cancelled.', 'No provider config was written.', 'No agent seeding was performed.'], rightTitle: ' Context ', right: previewLines(vm), safety, help, footer };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function choiceLines(choices) {
|
|
110
|
+
if (choices.length === 0)
|
|
111
|
+
return ['No editable choices on this step.'];
|
|
112
|
+
return choices.map((choice) => `${choice.focused ? '›' : ' '} ${choice.label}${choiceBadges(choice)}\n ${choice.description}`);
|
|
113
|
+
}
|
|
114
|
+
function choiceBadges(choice) {
|
|
115
|
+
const badges = [...(choice.selected ? ['[selected]'] : []), ...(choice.focused ? ['[focused]'] : []), ...choice.badges];
|
|
116
|
+
return badges.length === 0 ? '' : ` ${badges.join(' ')}`;
|
|
117
|
+
}
|
|
118
|
+
function focusedChoiceTitle(choices, fallback) {
|
|
119
|
+
return ` ${choices.find((choice) => choice.focused)?.label ?? fallback} `;
|
|
120
|
+
}
|
|
121
|
+
function focusedChoiceDetails(choices) {
|
|
122
|
+
const focused = choices.find((choice) => choice.focused) ?? choices.find((choice) => choice.selected) ?? choices[0];
|
|
123
|
+
if (focused === undefined)
|
|
124
|
+
return ['No focused choice.'];
|
|
125
|
+
return [focused.description, focused.badges.length === 0 ? 'Badges: none' : `Badges: ${focused.badges.join(' ')}`];
|
|
126
|
+
}
|
|
127
|
+
function previewLines(vm) {
|
|
128
|
+
return [
|
|
129
|
+
`Readiness: ${vm.readinessBadge} ${vm.readinessLabel}`,
|
|
130
|
+
`Provider: ${vm.providerLabel}`,
|
|
131
|
+
`Database: ${vm.databaseLabel}`,
|
|
132
|
+
`Path: ${vm.databasePathLabel}`,
|
|
133
|
+
`Target: ${vm.targetPathLabel}`,
|
|
134
|
+
`Backups: ${vm.backupLabel.replace(/^Backup(?: planned)?:\s*/u, '')}`,
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
function planSummaryLines(vm) {
|
|
138
|
+
return [
|
|
139
|
+
`Provider: ${vm.providerLabel}`,
|
|
140
|
+
`Database: ${vm.databaseLabel}`,
|
|
141
|
+
`Target: ${vm.targetPathLabel}`,
|
|
142
|
+
`Agents: ${vm.agentReadinessLabel}`,
|
|
143
|
+
vm.backupLabel,
|
|
144
|
+
...vm.plannedActions.slice(0, 3).map((action) => `${action.safetyBadge} ${action.label}`),
|
|
145
|
+
];
|
|
146
|
+
}
|
|
147
|
+
function blockerAndNextLines(vm) {
|
|
148
|
+
return [
|
|
149
|
+
...(vm.blockers.length === 0 ? ['Blockers: none'] : vm.blockers.map((blocker) => `! ${blocker}`)),
|
|
150
|
+
...(vm.warnings.length === 0 ? ['Warnings: none'] : vm.warnings.map((warning) => `! ${warning}`)),
|
|
151
|
+
'',
|
|
152
|
+
...vm.nextCommands.slice(0, 3).map((command) => `Next: ${command}`),
|
|
153
|
+
].filter(Boolean);
|
|
154
|
+
}
|
|
155
|
+
function safetyLines(vm, screen) {
|
|
156
|
+
return [
|
|
157
|
+
screen === 'final-confirmation' ? '! Final confirmation can apply OpenCode setup.' : 'Read-only until final confirmation.',
|
|
158
|
+
vm.footerSafetyLabel,
|
|
159
|
+
vm.canAutoApply ? 'Apply is available only from final confirmation.' : 'Automatic apply is currently unavailable for this selection.',
|
|
160
|
+
];
|
|
161
|
+
}
|
|
162
|
+
function footerForScreen(screen, width) {
|
|
163
|
+
if (screen === 'final-confirmation')
|
|
164
|
+
return formatFooter([setupFooterHints.confirmApply, setupFooterHints.back, setupFooterHints.cancel], width);
|
|
165
|
+
if (screen === 'project-database' || screen === 'provider' || screen === 'opencode-details')
|
|
166
|
+
return formatFooter([setupFooterHints.select, setupFooterHints.continue, setupFooterHints.back, setupFooterHints.cancel], width);
|
|
167
|
+
if (screen === 'result' || screen === 'error-recovery' || screen === 'cancelled')
|
|
168
|
+
return formatFooter([setupFooterHints.close], width);
|
|
169
|
+
return formatFooter([setupFooterHints.continue, setupFooterHints.back, setupFooterHints.cancel], width);
|
|
170
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createTestRenderer } from '@opentui/core/testing';
|
|
2
|
+
import { createSetupTuiState } from '../../setup/setup-tui-state.js';
|
|
3
|
+
import { createOpenTuiSetupScreen } from './screen.js';
|
|
4
|
+
const setup = await createTestRenderer({ width: 160, height: 70 });
|
|
5
|
+
try {
|
|
6
|
+
setup.renderer.root.add(createOpenTuiSetupScreen(createSetupTuiState({
|
|
7
|
+
screen: 'final-confirmation',
|
|
8
|
+
plan: {
|
|
9
|
+
version: 1,
|
|
10
|
+
kind: 'setup-plan',
|
|
11
|
+
status: 'ready',
|
|
12
|
+
project: 'vgxness',
|
|
13
|
+
workspaceRoot: '/tmp/project',
|
|
14
|
+
db: { mode: 'global', path: '/tmp/db.sqlite', source: 'flag' },
|
|
15
|
+
provider: 'opencode',
|
|
16
|
+
opencode: { scope: 'user', action: 'merge', targetPath: '/tmp/.config/opencode/opencode.json', installsAgents: true, agentNames: ['vgxness-manager'] },
|
|
17
|
+
actions: [
|
|
18
|
+
{
|
|
19
|
+
id: 'opencode-merge',
|
|
20
|
+
description: 'Merge OpenCode config with mcp.vgxness.',
|
|
21
|
+
mutating: false,
|
|
22
|
+
targetPath: '/tmp/.config/opencode/opencode.json',
|
|
23
|
+
backupRequired: true,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
conflicts: [],
|
|
27
|
+
backupsPlanned: [{ targetPath: '/tmp/.config/opencode/opencode.json', reason: 'Existing OpenCode config would be backed up before merge.' }],
|
|
28
|
+
safety: { mutating: false, writesProviderConfig: false, requiresConfirmationForApply: true },
|
|
29
|
+
nextCommands: ['vgxness setup apply --yes', 'vgxness doctor'],
|
|
30
|
+
},
|
|
31
|
+
}), 120));
|
|
32
|
+
await setup.renderOnce();
|
|
33
|
+
const frame = setup.captureCharFrame();
|
|
34
|
+
const requiredText = ['VGXNESS Setup Assistant', 'Final confirmation', 'confirm and apply', 'will write after confirm', 'opencode.json'];
|
|
35
|
+
const missing = requiredText.filter((text) => !frame.includes(text));
|
|
36
|
+
if (missing.length > 0)
|
|
37
|
+
throw new Error(`Setup OpenTUI smoke frame is missing expected text: ${missing.join(', ')}`);
|
|
38
|
+
console.log('setup OpenTUI smoke passed');
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
setup.renderer.destroy();
|
|
42
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { buildSetupTuiViewModel } from '../../setup/setup-tui-read-model.js';
|
|
2
|
+
import { renderSetupTuiShape } from '../../setup/setup-tui-render-shape.js';
|
|
3
|
+
export function formatOpenTuiSetupScreen(state, width = 100) {
|
|
4
|
+
return renderSetupTuiShape({
|
|
5
|
+
screen: state.screen,
|
|
6
|
+
viewModel: buildSetupTuiViewModel(state),
|
|
7
|
+
...(state.result === undefined ? {} : { result: state.result }),
|
|
8
|
+
...(state.error === undefined ? {} : { error: state.error }),
|
|
9
|
+
width,
|
|
10
|
+
color: false,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { navigationIntentFromInput } from '../keymap.js';
|
|
2
|
+
export function setupTuiActionFromInput(input, key, state) {
|
|
3
|
+
if (key.upArrow === true)
|
|
4
|
+
return { type: 'focus-previous-choice' };
|
|
5
|
+
if (key.downArrow === true)
|
|
6
|
+
return { type: 'focus-next-choice' };
|
|
7
|
+
if (key.escape === true)
|
|
8
|
+
return state.helpVisible ? { type: 'toggle-help' } : { type: 'cancel' };
|
|
9
|
+
if (key.tab === true && key.shift === true)
|
|
10
|
+
return { type: 'back' };
|
|
11
|
+
if (key.tab === true)
|
|
12
|
+
return { type: 'continue' };
|
|
13
|
+
const raw = key.return === true ? '\n' : input;
|
|
14
|
+
const intent = navigationIntentFromInput(raw);
|
|
15
|
+
if (intent === 'cancel')
|
|
16
|
+
return state.helpVisible ? { type: 'toggle-help' } : { type: 'cancel' };
|
|
17
|
+
if (intent === 'help')
|
|
18
|
+
return { type: 'toggle-help' };
|
|
19
|
+
if (intent === 'up')
|
|
20
|
+
return { type: 'focus-previous-choice' };
|
|
21
|
+
if (intent === 'down')
|
|
22
|
+
return { type: 'focus-next-choice' };
|
|
23
|
+
if (intent === 'select')
|
|
24
|
+
return { type: 'select-focused-choice' };
|
|
25
|
+
if (intent === 'back')
|
|
26
|
+
return { type: 'back' };
|
|
27
|
+
if (intent !== 'next')
|
|
28
|
+
return undefined;
|
|
29
|
+
if (key.return === true && isEditableChoiceScreen(state))
|
|
30
|
+
return { type: 'select-focused-choice' };
|
|
31
|
+
if (state.screen === 'plan-review')
|
|
32
|
+
return { type: 'request-apply' };
|
|
33
|
+
if (state.screen === 'final-confirmation')
|
|
34
|
+
return { type: 'confirm-apply' };
|
|
35
|
+
return { type: 'continue' };
|
|
36
|
+
}
|
|
37
|
+
function isEditableChoiceScreen(state) {
|
|
38
|
+
if (state.focusedChoiceId === undefined)
|
|
39
|
+
return false;
|
|
40
|
+
if (state.screen === 'project-database' || state.screen === 'provider')
|
|
41
|
+
return true;
|
|
42
|
+
return state.screen === 'opencode-details' && state.selections.provider === 'opencode';
|
|
43
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { badgeLabels, compactPath } from './
|
|
1
|
+
import { badgeLabels, compactPath } from './setup-tui-view-helpers.js';
|
|
2
2
|
export function buildSetupTuiViewModel(state) {
|
|
3
3
|
return setupTuiViewModelFromPlan(state.plan, state.status, state);
|
|
4
4
|
}
|
|
@@ -57,7 +57,7 @@ export function setupTuiViewModelFromPlan(plan, status, state) {
|
|
|
57
57
|
warnings,
|
|
58
58
|
blockers,
|
|
59
59
|
backupLabel: backups.length === 0 ? 'Backup: none planned' : `Backup planned: ${backups.join('; ')}`,
|
|
60
|
-
nextCommands: plan?.nextCommands ?? ['
|
|
60
|
+
nextCommands: plan?.nextCommands ?? ['vgxness setup plan'],
|
|
61
61
|
canAutoApply: plan?.provider === 'opencode' && plan.status === 'ready' && state?.selections.provider !== 'none' && plan.opencode !== undefined,
|
|
62
62
|
safetyWarning: 'Final confirmation is required before any provider config write. OpenCode config may be modified and backed up.',
|
|
63
63
|
frameLabel: 'VGXNESS Setup Assistant workspace',
|
|
@@ -108,7 +108,7 @@ function previewDetailLines(input) {
|
|
|
108
108
|
...actions,
|
|
109
109
|
input.backups.length === 0 ? 'Backups: none planned' : `Backups: ${input.backups.join('; ')}`,
|
|
110
110
|
...(input.warnings.length === 0 ? ['Warnings: none'] : input.warnings.map((warning) => `Warning: ${warning}`)),
|
|
111
|
-
...(input.blockers.length === 0 ? ['Blockers: none'] : input.blockers.map((blocker) => `
|
|
111
|
+
...(input.blockers.length === 0 ? ['Blockers: none'] : input.blockers.map((blocker) => `Blocked because: ${blocker}`)),
|
|
112
112
|
];
|
|
113
113
|
}
|
|
114
114
|
function helpLines(screen) {
|
|
@@ -145,7 +145,7 @@ function agentReadinessLabels(plan, status) {
|
|
|
145
145
|
if (status?.agents.status === 'blocked')
|
|
146
146
|
return {
|
|
147
147
|
label: 'Agents need attention',
|
|
148
|
-
detail: `${status.agents.blocker ?? 'Required agents are not ready.'} Next: ${status.agents.nextAction ?? '
|
|
148
|
+
detail: `${status.agents.blocker ?? 'Required agents are not ready.'} Next: ${status.agents.nextAction ?? 'vgxness agents seed --scope project'}`,
|
|
149
149
|
};
|
|
150
150
|
return { label: 'Agent readiness preview', detail: `Required agents: ${names.join(', ')}. Preview/status screens do not seed agents.` };
|
|
151
151
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { badgeLabels, compactPath, formatFooter, setupFooterHints, wrapLabel } from './
|
|
1
|
+
import { badgeLabels, compactPath, formatFooter, setupFooterHints, wrapLabel } from './setup-tui-view-helpers.js';
|
|
2
2
|
export function renderSetupTuiShape(input) {
|
|
3
3
|
const width = input.width ?? 80;
|
|
4
4
|
const lines = [input.viewModel.title, ...screenLines(input, width)];
|
|
@@ -59,11 +59,10 @@ function screenLines(input, width) {
|
|
|
59
59
|
'Read-only plan review',
|
|
60
60
|
'Plan summary ' + badgeLabels.readOnly,
|
|
61
61
|
...planLines(vm, width),
|
|
62
|
-
|
|
63
|
-
'Restart next step: Restart OpenCode after confirmed setup so it reloads MCP configuration.',
|
|
62
|
+
...nextCommandLines(vm),
|
|
64
63
|
'Review only: this screen does not write provider config.',
|
|
65
|
-
vm.canAutoApply ? 'Press Enter to continue to final confirmation.' : '
|
|
66
|
-
], width, [setupFooterHints.finalConfirmation, setupFooterHints.back, setupFooterHints.cancel]);
|
|
64
|
+
vm.canAutoApply ? 'Press Enter to continue to final confirmation.' : 'Press Enter to show manual next steps; no automatic apply is available.',
|
|
65
|
+
], width, vm.canAutoApply ? [setupFooterHints.finalConfirmation, setupFooterHints.back, setupFooterHints.cancel] : [setupFooterHints.continue, setupFooterHints.back, setupFooterHints.cancel]);
|
|
67
66
|
case 'final-confirmation':
|
|
68
67
|
return workspaceLines(vm, [
|
|
69
68
|
'Final confirmation',
|
|
@@ -84,7 +83,7 @@ function screenLines(input, width) {
|
|
|
84
83
|
badgeLabels.error,
|
|
85
84
|
`Error: ${input.error ?? 'Unknown setup error'}`,
|
|
86
85
|
'No unconfirmed provider config write was performed.',
|
|
87
|
-
'Next: inspect `
|
|
86
|
+
'Next: inspect `vgxness setup plan`, resolve blockers, then retry.',
|
|
88
87
|
footer([setupFooterHints.close], width),
|
|
89
88
|
];
|
|
90
89
|
case 'cancelled':
|
|
@@ -106,7 +105,7 @@ function planLines(vm, width) {
|
|
|
106
105
|
vm.backupLabel,
|
|
107
106
|
...vm.plannedActions.map((action) => `Action: ${action.safetyBadge} ${action.label}${action.targetPathLabel === undefined ? '' : ` (${compactPath(action.targetPathLabel, width - 12)})`}`),
|
|
108
107
|
...vm.warnings.map((warning) => `Warning: ${warning}`),
|
|
109
|
-
...vm.blockers.map((blocker) => `Blocked: ${blocker}`),
|
|
108
|
+
...vm.blockers.map((blocker) => `Blocked because: ${blocker}`),
|
|
110
109
|
];
|
|
111
110
|
}
|
|
112
111
|
function workspaceLines(vm, lines, width, hints = [setupFooterHints.continue, setupFooterHints.back, setupFooterHints.cancel]) {
|
|
@@ -155,11 +154,11 @@ function resultLines(result, vm, width) {
|
|
|
155
154
|
}
|
|
156
155
|
function resultNextCommands(vm) {
|
|
157
156
|
const filtered = vm.nextCommands.filter((command) => !/^vgx(?:ness)? setup apply\b/.test(command));
|
|
158
|
-
const withDoctor = filtered.some((command) => /\bdoctor\b/.test(command)) ? filtered : [...filtered, '
|
|
157
|
+
const withDoctor = filtered.some((command) => /\bdoctor\b/.test(command)) ? filtered : [...filtered, 'vgxness doctor'];
|
|
159
158
|
return withDoctor.some((command) => /Restart OpenCode/i.test(command)) ? withDoctor : [...withDoctor, 'Restart OpenCode and verify the vgxness MCP server is visible.'];
|
|
160
159
|
}
|
|
161
|
-
function
|
|
162
|
-
return vm.nextCommands.
|
|
160
|
+
function nextCommandLines(vm) {
|
|
161
|
+
return ['Next commands:', ...vm.nextCommands.map((command) => `Next: ${command}`)];
|
|
163
162
|
}
|
|
164
163
|
function footer(hints, width) {
|
|
165
164
|
return formatFooter(hints, width);
|
|
@@ -81,7 +81,7 @@ export function reduceSetupTuiState(state, action) {
|
|
|
81
81
|
result: {
|
|
82
82
|
status: 'failed',
|
|
83
83
|
message: action.message,
|
|
84
|
-
recovery: ['Inspect the setup plan with `
|
|
84
|
+
recovery: ['Inspect the setup plan with `vgxness setup plan`.', 'Resolve blockers before retrying.'],
|
|
85
85
|
writesOccurred: false,
|
|
86
86
|
},
|
|
87
87
|
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { formatTuiFooter } from '../visual/footer.js';
|
|
2
|
+
export const badgeLabels = {
|
|
3
|
+
recommended: '[recommended]',
|
|
4
|
+
selected: '[selected]',
|
|
5
|
+
focused: '[focused]',
|
|
6
|
+
warning: '[warning]',
|
|
7
|
+
error: '[error]',
|
|
8
|
+
writeAfterConfirm: '[will write after confirm]',
|
|
9
|
+
readOnly: '[read-only]',
|
|
10
|
+
manual: '[manual]',
|
|
11
|
+
deferred: '[deferred]',
|
|
12
|
+
cancelled: '[cancelled]',
|
|
13
|
+
pending: '[pending]',
|
|
14
|
+
success: '[success]',
|
|
15
|
+
};
|
|
16
|
+
export const setupFooterHints = {
|
|
17
|
+
continue: { key: 'Enter', label: 'continue' },
|
|
18
|
+
select: { key: 'Enter/Space', label: 'select focused row' },
|
|
19
|
+
finalConfirmation: { key: 'Enter', label: 'final confirmation' },
|
|
20
|
+
confirmApply: { key: 'Enter', label: 'confirm and apply' },
|
|
21
|
+
help: { key: '?/h', label: 'help' },
|
|
22
|
+
back: { key: 'Shift+Tab', label: 'back' },
|
|
23
|
+
cancel: { key: 'q/Esc', label: 'cancel' },
|
|
24
|
+
close: { key: 'q/Esc', label: 'close' },
|
|
25
|
+
};
|
|
26
|
+
export function compactPath(path, width) {
|
|
27
|
+
if (width <= 0 || path.length <= width)
|
|
28
|
+
return path;
|
|
29
|
+
const basename = path.split(/[\\/]/).filter(Boolean).pop() ?? path;
|
|
30
|
+
if (basename.length + 4 >= width)
|
|
31
|
+
return `.../${basename}`;
|
|
32
|
+
const prefixWidth = Math.max(1, width - basename.length - 5);
|
|
33
|
+
return `${path.slice(0, prefixWidth)}.../${basename}`;
|
|
34
|
+
}
|
|
35
|
+
export function wrapLabel(value, width) {
|
|
36
|
+
if (width <= 0 || value.length <= width)
|
|
37
|
+
return value;
|
|
38
|
+
const protectedPhrases = ['final confirmation', 'confirm and apply', 'not installable', 'read-only', 'requires confirmation', 'will write after confirm', 'error', 'cancelled'];
|
|
39
|
+
const phrase = protectedPhrases.find((candidate) => value.includes(candidate));
|
|
40
|
+
if (phrase !== undefined)
|
|
41
|
+
return `${value.slice(0, Math.max(0, width - phrase.length - 5)).trim()} ... ${phrase}`.trim();
|
|
42
|
+
return `${value.slice(0, Math.max(0, width - 1)).trim()}…`;
|
|
43
|
+
}
|
|
44
|
+
export function formatFooter(hints, width) {
|
|
45
|
+
return formatTuiFooter(hints, width);
|
|
46
|
+
}
|
|
@@ -84,8 +84,6 @@ export function validateSddReadiness(context) {
|
|
|
84
84
|
if (context.next.status === 'complete')
|
|
85
85
|
return { ok: true };
|
|
86
86
|
const present = context.status.phases.find((candidate) => candidate.phase === context.phase)?.present === true;
|
|
87
|
-
if (present)
|
|
88
|
-
return { ok: true };
|
|
89
87
|
const blockedPrerequisites = activeBlockedPrerequisites(context);
|
|
90
88
|
if (blockedPrerequisites.length > 0)
|
|
91
89
|
return {
|
|
@@ -100,6 +98,8 @@ export function validateSddReadiness(context) {
|
|
|
100
98
|
recommendedAction: `Accept or restore prerequisites before running ${context.phase}.`,
|
|
101
99
|
},
|
|
102
100
|
};
|
|
101
|
+
if (present)
|
|
102
|
+
return { ok: true };
|
|
103
103
|
if (context.status.nextReadyPhase === context.phase)
|
|
104
104
|
return { ok: true };
|
|
105
105
|
return {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export function getApprovalActionsState(input) {
|
|
2
|
+
if (input.source !== 'interactive' || input.status !== 'running' || input.mode !== 'craft' || !input.childStdinOpen)
|
|
3
|
+
return { enabled: false };
|
|
4
|
+
const approval = input.latestPendingApproval;
|
|
5
|
+
if (input.pendingApprovalCount < 1 || approval?.approvalId === undefined || approval.toolCallId === undefined)
|
|
6
|
+
return { enabled: false };
|
|
7
|
+
return { enabled: true, approval };
|
|
8
|
+
}
|
|
9
|
+
export class ApprovalDecisionWriter {
|
|
10
|
+
#sink;
|
|
11
|
+
#writtenKeys = new Set();
|
|
12
|
+
constructor(sink) {
|
|
13
|
+
this.#sink = sink;
|
|
14
|
+
}
|
|
15
|
+
write(approval, status, reason) {
|
|
16
|
+
if (approval?.approvalId === undefined || approval.toolCallId === undefined)
|
|
17
|
+
return false;
|
|
18
|
+
if (this.#sink.writableEnded === true || this.#sink.destroyed === true)
|
|
19
|
+
return false;
|
|
20
|
+
const key = `${approval.approvalId}:${approval.toolCallId}`;
|
|
21
|
+
if (this.#writtenKeys.has(key))
|
|
22
|
+
return false;
|
|
23
|
+
this.#sink.write(`${JSON.stringify({
|
|
24
|
+
type: 'approval.decision',
|
|
25
|
+
approvalId: approval.approvalId,
|
|
26
|
+
toolCallId: approval.toolCallId,
|
|
27
|
+
status,
|
|
28
|
+
reason,
|
|
29
|
+
})}\n`);
|
|
30
|
+
this.#writtenKeys.add(key);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function parsePromptSubmission(input, currentMode) {
|
|
2
|
+
const trimmed = input.trim();
|
|
3
|
+
const prefixMatch = /^(\/(inspect|plan|craft|craft-preview))(?:\s+|$)/iu.exec(trimmed);
|
|
4
|
+
if (!prefixMatch)
|
|
5
|
+
return { mode: currentMode, prompt: trimmed };
|
|
6
|
+
const mode = prefixMatch[2]?.toLowerCase();
|
|
7
|
+
return { mode, prompt: trimmed.slice(prefixMatch[0].length).trim() };
|
|
8
|
+
}
|
|
9
|
+
export function toggleReadOnlyMode(mode) {
|
|
10
|
+
return mode === 'inspect' ? 'plan' : 'inspect';
|
|
11
|
+
}
|