@pellux/goodvibes-tui 0.19.24 → 0.19.26

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 (76) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +5 -5
  3. package/bin/goodvibes +10 -0
  4. package/bin/goodvibes-daemon +10 -0
  5. package/docs/foundation-artifacts/operator-contract.json +1 -1
  6. package/package.json +3 -2
  7. package/src/cli/bundle-command.ts +225 -0
  8. package/src/cli/completion.ts +90 -0
  9. package/src/cli/config-overrides.ts +159 -0
  10. package/src/cli/endpoints.ts +63 -0
  11. package/src/cli/entrypoint.ts +169 -0
  12. package/src/cli/help.ts +301 -0
  13. package/src/cli/index.ts +11 -0
  14. package/src/cli/management-commands.ts +426 -0
  15. package/src/cli/management.ts +719 -0
  16. package/src/cli/network-posture.ts +46 -0
  17. package/src/cli/package-verification.ts +119 -0
  18. package/src/cli/parser.ts +369 -0
  19. package/src/cli/provider-classification.ts +107 -0
  20. package/src/cli/redaction.ts +105 -0
  21. package/src/cli/service-command.ts +45 -0
  22. package/src/cli/service-posture.ts +247 -0
  23. package/src/cli/status.ts +382 -0
  24. package/src/cli/surface-command.ts +248 -0
  25. package/src/cli/tui-startup.ts +32 -0
  26. package/src/cli/types.ts +69 -0
  27. package/src/cli-flags.ts +18 -55
  28. package/src/config/index.ts +1 -1
  29. package/src/config/secrets.ts +44 -0
  30. package/src/daemon/cli.ts +62 -11
  31. package/src/input/command-registry.ts +3 -0
  32. package/src/input/commands/guidance-runtime.ts +9 -4
  33. package/src/input/commands/local-runtime.ts +21 -7
  34. package/src/input/commands/local-setup.ts +31 -38
  35. package/src/input/commands/onboarding-runtime.ts +14 -0
  36. package/src/input/commands/runtime-services.ts +9 -0
  37. package/src/input/commands.ts +2 -0
  38. package/src/input/feed-context-factory.ts +8 -1
  39. package/src/input/handler-feed.ts +13 -8
  40. package/src/input/handler-interactions.ts +266 -0
  41. package/src/input/handler-modal-stack.ts +23 -3
  42. package/src/input/handler-modal-token-routes.ts +23 -1
  43. package/src/input/handler-onboarding.ts +696 -0
  44. package/src/input/handler-picker-routes.ts +15 -7
  45. package/src/input/handler-ui-state.ts +58 -0
  46. package/src/input/handler.ts +120 -246
  47. package/src/input/onboarding/handler-onboarding-routes.ts +105 -0
  48. package/src/input/onboarding/onboarding-wizard-apply.ts +211 -0
  49. package/src/input/onboarding/onboarding-wizard-constants.ts +148 -0
  50. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +712 -0
  51. package/src/input/onboarding/onboarding-wizard-helpers.ts +218 -0
  52. package/src/input/onboarding/onboarding-wizard-rules.ts +224 -0
  53. package/src/input/onboarding/onboarding-wizard-state.ts +354 -0
  54. package/src/input/onboarding/onboarding-wizard-steps.ts +642 -0
  55. package/src/input/onboarding/onboarding-wizard-types.ts +170 -0
  56. package/src/input/onboarding/onboarding-wizard.ts +594 -0
  57. package/src/main.ts +32 -39
  58. package/src/panels/builtin/operations.ts +0 -10
  59. package/src/panels/index.ts +0 -1
  60. package/src/renderer/conversation-overlays.ts +6 -0
  61. package/src/renderer/help-overlay.ts +1 -1
  62. package/src/renderer/onboarding/onboarding-wizard.ts +533 -0
  63. package/src/runtime/bootstrap-core.ts +1 -0
  64. package/src/runtime/bootstrap.ts +123 -0
  65. package/src/runtime/onboarding/apply.ts +685 -0
  66. package/src/runtime/onboarding/derivation.ts +495 -0
  67. package/src/runtime/onboarding/index.ts +7 -0
  68. package/src/runtime/onboarding/markers.ts +161 -0
  69. package/src/runtime/onboarding/snapshot.ts +400 -0
  70. package/src/runtime/onboarding/state.ts +140 -0
  71. package/src/runtime/onboarding/types.ts +402 -0
  72. package/src/runtime/onboarding/verify.ts +233 -0
  73. package/src/runtime/ui-services.ts +16 -0
  74. package/src/shell/ui-openers.ts +12 -2
  75. package/src/version.ts +1 -1
  76. package/src/panels/welcome-panel.ts +0 -64
@@ -0,0 +1,354 @@
1
+ import type { ModelPickerTarget } from '../model-picker.ts';
2
+ import type { OnboardingStep1CapabilityItem } from '../../runtime/onboarding/index.ts';
3
+ import { DEFAULT_CAPABILITIES, NETWORK_HOST_FIELD_IDS } from './onboarding-wizard-constants.ts';
4
+ import { areSelectionsEqual, clamp, cloneSelection, getExternalSurfaceSetupFieldSpec, isMalformedGoodVibesSecretReferenceValue, isValidHostValue, normalizeText } from './onboarding-wizard-helpers.ts';
5
+ import type { OnboardingWizardController } from './onboarding-wizard.ts';
6
+ import type { OnboardingWizardFieldDefinition, OnboardingWizardModelSelection, OnboardingWizardStepDefinition } from './onboarding-wizard-types.ts';
7
+
8
+ export function getToggleFieldCount(controller: OnboardingWizardController, stepIndex: number): number {
9
+ const step = controller.steps[stepIndex];
10
+ if (!step) return 0;
11
+ return step.fields.filter((field) => field.kind === 'checklist' || field.kind === 'acknowledgement').length;
12
+ }
13
+
14
+ export function getCompletedToggleCount(controller: OnboardingWizardController, stepIndex: number): number {
15
+ const step = controller.steps[stepIndex];
16
+ if (!step) return 0;
17
+
18
+ return step.fields.filter((field) => (
19
+ (field.kind === 'checklist' || field.kind === 'acknowledgement')
20
+ && (controller.getFieldValue(field) as boolean)
21
+ )).length;
22
+ }
23
+
24
+ export function getStepFieldCount(controller: OnboardingWizardController, stepIndex: number): number {
25
+ return controller.steps[stepIndex]?.fields.length ?? 0;
26
+ }
27
+
28
+ export function getCompletedFieldCount(controller: OnboardingWizardController, stepIndex: number): number {
29
+ const step = controller.steps[stepIndex];
30
+ if (!step) return 0;
31
+ return step.fields.filter((field) => controller.isFieldSatisfied(field)).length;
32
+ }
33
+
34
+ export function isStepDirty(controller: OnboardingWizardController, stepIndex: number): boolean {
35
+ const stepId = controller.steps[stepIndex]?.id;
36
+ return stepId ? controller.dirtyStepIds.has(stepId) : false;
37
+ }
38
+
39
+ export function isFieldDirty(controller: OnboardingWizardController, fieldId: string): boolean {
40
+ const field = controller.getFieldById(fieldId);
41
+ return field ? controller.isFieldDirtyByDefinition(field) : false;
42
+ }
43
+
44
+ export function getBlockingFieldLabels(controller: OnboardingWizardController): readonly string[] {
45
+ const labels: string[] = [];
46
+ if (controller.hydrationPending) {
47
+ labels.push('Loading: Current runtime settings are still being collected.');
48
+ return labels;
49
+ }
50
+ if (controller.hydrationError !== null) {
51
+ labels.push(`Loading: Current runtime settings could not be collected: ${controller.hydrationError}`);
52
+ return labels;
53
+ }
54
+
55
+ for (const step of controller.steps) {
56
+ for (const field of step.fields) {
57
+ if (field.kind === 'acknowledgement' && field.required && !controller.isFieldSatisfied(field)) {
58
+ labels.push(`${step.shortLabel}: ${field.label}`);
59
+ }
60
+ const validationError = controller.getFieldValidationError(step, field);
61
+ if (validationError) labels.push(validationError);
62
+ }
63
+ }
64
+ return labels;
65
+ }
66
+
67
+ export function getFieldValidationError(
68
+ controller: OnboardingWizardController,
69
+ step: OnboardingWizardStepDefinition,
70
+ field: OnboardingWizardFieldDefinition,
71
+ ): string | null {
72
+ if (field.kind !== 'text' && field.kind !== 'masked') return null;
73
+
74
+ const value = normalizeText(controller.getFieldValue(field) as string);
75
+ const required = field.required === true || controller.isRequiredExternalSetupField(field.id);
76
+ if (required && value.length === 0) {
77
+ return `${step.shortLabel}: ${field.label} is required.`;
78
+ }
79
+
80
+ if (field.id === 'accounts.admin-username') {
81
+ const existing = controller.runtimeSnapshot?.auth.snapshot.users.find((user) => user.username === value);
82
+ if (controller.hasBootstrapCredentialPresent() && existing) {
83
+ return `${step.shortLabel}: ${field.label} must be a new username so the wizard can replace bootstrap credentials.`;
84
+ }
85
+ if (existing && !existing.roles.includes('admin')) {
86
+ return `${step.shortLabel}: ${field.label} must be a new username or an existing admin user.`;
87
+ }
88
+ }
89
+
90
+ if (field.kind === 'masked' && isMalformedGoodVibesSecretReferenceValue(value)) {
91
+ return `${step.shortLabel}: ${field.label} must be a secret value or a goodvibes://secrets/... reference.`;
92
+ }
93
+
94
+ if (NETWORK_HOST_FIELD_IDS.has(field.id)) {
95
+ if (!isValidHostValue(value)) {
96
+ return `${step.shortLabel}: ${field.label} must be a host or IP address, not a URL.`;
97
+ }
98
+ return null;
99
+ }
100
+
101
+ if (field.id === 'network.service-port' || field.id === 'network.browser-port' || field.id === 'network.webhook-port') {
102
+ const parsed = controller.parseIntegerFieldValue(field.id, Number.parseInt(field.defaultValue, 10));
103
+ if (parsed === null || parsed < 1 || parsed > 65535) {
104
+ return `${step.shortLabel}: ${field.label} must be a port number from 1 to 65535.`;
105
+ }
106
+ return null;
107
+ }
108
+
109
+ if (field.kind !== 'text') return null;
110
+ const setupField = getExternalSurfaceSetupFieldSpec(field.id);
111
+ if (setupField?.valueType !== 'number') return null;
112
+ const parsed = controller.parseIntegerFieldValue(field.id, Number.parseInt(field.defaultValue, 10));
113
+ if (parsed === null) {
114
+ return `${step.shortLabel}: ${field.label} must be a number.`;
115
+ }
116
+ if (setupField.min !== undefined && parsed < setupField.min) {
117
+ return `${step.shortLabel}: ${field.label} must be at least ${setupField.min}.`;
118
+ }
119
+ if (setupField.max !== undefined && parsed > setupField.max) {
120
+ return `${step.shortLabel}: ${field.label} must be at most ${setupField.max}.`;
121
+ }
122
+ return null;
123
+ }
124
+
125
+ export function getFieldById(controller: OnboardingWizardController, fieldId: string): OnboardingWizardFieldDefinition | null {
126
+ for (const step of controller.steps) {
127
+ const field = step.fields.find((entry) => entry.id === fieldId);
128
+ if (field) return field;
129
+ }
130
+ return null;
131
+ }
132
+
133
+ export function ensureSelectionVisible(controller: OnboardingWizardController, visibleFields: number): void {
134
+ const total = controller.currentStep.fields.length;
135
+ if (total === 0) {
136
+ controller.scrollOffsets[controller.stepIndex] = 0;
137
+ controller.selectedFieldIndices[controller.stepIndex] = 0;
138
+ return;
139
+ }
140
+
141
+ const clampedSelection = clamp(controller.selectedFieldIndices[controller.stepIndex] ?? 0, 0, total - 1);
142
+ const maxStart = Math.max(0, total - visibleFields);
143
+ let nextOffset = clamp(controller.scrollOffsets[controller.stepIndex] ?? 0, 0, maxStart);
144
+
145
+ if (clampedSelection < nextOffset) nextOffset = clampedSelection;
146
+ if (clampedSelection >= nextOffset + visibleFields) nextOffset = clampedSelection - visibleFields + 1;
147
+
148
+ controller.selectedFieldIndices[controller.stepIndex] = clampedSelection;
149
+ controller.scrollOffsets[controller.stepIndex] = clamp(nextOffset, 0, maxStart);
150
+ }
151
+
152
+ export function reconcileStepCursor(controller: OnboardingWizardController, stepIndex: number): void {
153
+ const total = controller.steps[stepIndex]?.fields.length ?? 0;
154
+ if (total === 0) {
155
+ controller.scrollOffsets[stepIndex] = 0;
156
+ controller.selectedFieldIndices[stepIndex] = 0;
157
+ return;
158
+ }
159
+
160
+ controller.selectedFieldIndices[stepIndex] = clamp(controller.selectedFieldIndices[stepIndex] ?? 0, 0, total - 1);
161
+ controller.scrollOffsets[stepIndex] = clamp(controller.scrollOffsets[stepIndex] ?? 0, 0, total - 1);
162
+ }
163
+
164
+ export function resetValuesFromCurrentDefinitions(controller: OnboardingWizardController): void {
165
+ controller.toggleState.clear();
166
+ controller.baselineToggleState.clear();
167
+ controller.radioState.clear();
168
+ controller.baselineRadioState.clear();
169
+ controller.textState.clear();
170
+ controller.baselineTextState.clear();
171
+ controller.modelSelectionState.clear();
172
+ controller.baselineModelSelectionState.clear();
173
+ controller.touchedActionFields.clear();
174
+ controller.dirtyStepIds.clear();
175
+ controller.pendingAction = null;
176
+
177
+ for (const step of controller.steps) {
178
+ for (const field of step.fields) {
179
+ if (field.kind === 'status') continue;
180
+
181
+ if (field.kind === 'checklist' || field.kind === 'acknowledgement') {
182
+ controller.toggleState.set(field.id, field.defaultValue);
183
+ controller.baselineToggleState.set(field.id, field.defaultValue);
184
+ continue;
185
+ }
186
+
187
+ if (field.kind === 'radio') {
188
+ controller.radioState.set(field.id, field.defaultValue);
189
+ controller.baselineRadioState.set(field.id, field.defaultValue);
190
+ continue;
191
+ }
192
+
193
+ if (field.kind === 'text' || field.kind === 'masked') {
194
+ controller.textState.set(field.id, field.defaultValue);
195
+ controller.baselineTextState.set(field.id, field.defaultValue);
196
+ continue;
197
+ }
198
+
199
+ if (field.kind === 'action') continue;
200
+
201
+ controller.modelSelectionState.set(field.target, cloneSelection(field.defaultSelection));
202
+ controller.baselineModelSelectionState.set(field.target, cloneSelection(field.defaultSelection));
203
+ }
204
+ }
205
+
206
+ for (let index = 0; index < controller.steps.length; index += 1) {
207
+ controller.reconcileStepCursor(index);
208
+ }
209
+ }
210
+
211
+ export function reconcileStateWithCurrentDefinitions(controller: OnboardingWizardController): void {
212
+ const nextToggleKeys = new Set<string>();
213
+ const nextRadioKeys = new Set<string>();
214
+ const nextTextKeys = new Set<string>();
215
+ const nextModelTargets = new Set<ModelPickerTarget>();
216
+
217
+ for (const step of controller.steps) {
218
+ for (const field of step.fields) {
219
+ if (field.kind === 'status') continue;
220
+
221
+ if (field.kind === 'checklist' || field.kind === 'acknowledgement') {
222
+ nextToggleKeys.add(field.id);
223
+ if (!controller.toggleState.has(field.id)) controller.toggleState.set(field.id, field.defaultValue);
224
+ if (!controller.baselineToggleState.has(field.id)) controller.baselineToggleState.set(field.id, field.defaultValue);
225
+ continue;
226
+ }
227
+
228
+ if (field.kind === 'radio') {
229
+ nextRadioKeys.add(field.id);
230
+ if (!controller.radioState.has(field.id)) controller.radioState.set(field.id, field.defaultValue);
231
+ if (!controller.baselineRadioState.has(field.id)) controller.baselineRadioState.set(field.id, field.defaultValue);
232
+ continue;
233
+ }
234
+
235
+ if (field.kind === 'text' || field.kind === 'masked') {
236
+ nextTextKeys.add(field.id);
237
+ if (!controller.textState.has(field.id)) controller.textState.set(field.id, field.defaultValue);
238
+ if (!controller.baselineTextState.has(field.id)) controller.baselineTextState.set(field.id, field.defaultValue);
239
+ continue;
240
+ }
241
+
242
+ if (field.kind === 'action') continue;
243
+
244
+ nextModelTargets.add(field.target);
245
+ if (!controller.modelSelectionState.has(field.target)) {
246
+ controller.modelSelectionState.set(field.target, cloneSelection(field.defaultSelection));
247
+ }
248
+ if (!controller.baselineModelSelectionState.has(field.target)) {
249
+ controller.baselineModelSelectionState.set(field.target, cloneSelection(field.defaultSelection));
250
+ }
251
+ }
252
+ }
253
+
254
+ for (const key of [...controller.toggleState.keys()]) {
255
+ if (!nextToggleKeys.has(key)) controller.toggleState.delete(key);
256
+ }
257
+ for (const key of [...controller.baselineToggleState.keys()]) {
258
+ if (!nextToggleKeys.has(key)) controller.baselineToggleState.delete(key);
259
+ }
260
+ for (const key of [...controller.radioState.keys()]) {
261
+ if (!nextRadioKeys.has(key)) controller.radioState.delete(key);
262
+ }
263
+ for (const key of [...controller.baselineRadioState.keys()]) {
264
+ if (!nextRadioKeys.has(key)) controller.baselineRadioState.delete(key);
265
+ }
266
+ for (const key of [...controller.textState.keys()]) {
267
+ if (!nextTextKeys.has(key)) controller.textState.delete(key);
268
+ }
269
+ for (const key of [...controller.baselineTextState.keys()]) {
270
+ if (!nextTextKeys.has(key)) controller.baselineTextState.delete(key);
271
+ }
272
+ for (const key of [...controller.modelSelectionState.keys()]) {
273
+ if (!nextModelTargets.has(key)) controller.modelSelectionState.delete(key);
274
+ }
275
+ for (const key of [...controller.baselineModelSelectionState.keys()]) {
276
+ if (!nextModelTargets.has(key)) controller.baselineModelSelectionState.delete(key);
277
+ }
278
+ }
279
+
280
+ export function recalculateDirtyState(controller: OnboardingWizardController): void {
281
+ controller.reconcileStateWithCurrentDefinitions();
282
+ controller.dirtyStepIds.clear();
283
+
284
+ for (const step of controller.steps) {
285
+ if (step.fields.some((field) => controller.isFieldDirtyByDefinition(field))) {
286
+ controller.dirtyStepIds.add(step.id);
287
+ }
288
+ }
289
+ }
290
+
291
+ export function isFieldDirtyByDefinition(controller: OnboardingWizardController, field: OnboardingWizardFieldDefinition): boolean {
292
+ if (field.kind === 'checklist' || field.kind === 'acknowledgement') {
293
+ return (controller.toggleState.get(field.id) ?? field.defaultValue)
294
+ !== (controller.baselineToggleState.get(field.id) ?? field.defaultValue);
295
+ }
296
+
297
+ if (field.kind === 'radio') {
298
+ return (controller.radioState.get(field.id) ?? field.defaultValue)
299
+ !== (controller.baselineRadioState.get(field.id) ?? field.defaultValue);
300
+ }
301
+
302
+ if (field.kind === 'text' || field.kind === 'masked') {
303
+ return (controller.textState.get(field.id) ?? field.defaultValue)
304
+ !== (controller.baselineTextState.get(field.id) ?? field.defaultValue);
305
+ }
306
+
307
+ if (field.kind === 'status' || field.kind === 'action') return false;
308
+
309
+ return !areSelectionsEqual(
310
+ controller.modelSelectionState.get(field.target) ?? field.defaultSelection,
311
+ controller.baselineModelSelectionState.get(field.target) ?? field.defaultSelection,
312
+ );
313
+ }
314
+
315
+ export function isFieldSatisfied(controller: OnboardingWizardController, field: OnboardingWizardFieldDefinition): boolean {
316
+ if (field.kind === 'checklist' || field.kind === 'acknowledgement') {
317
+ if (field.kind === 'acknowledgement' && !field.required) return true;
318
+ return Boolean(controller.getFieldValue(field));
319
+ }
320
+
321
+ if (field.kind === 'radio') return true;
322
+
323
+ if (field.kind === 'text' || field.kind === 'masked') {
324
+ return normalizeText(controller.getFieldValue(field) as string).length > 0;
325
+ }
326
+
327
+ if (field.kind === 'status' || field.kind === 'action') return true;
328
+
329
+ const selection = controller.getFieldValue(field) as OnboardingWizardModelSelection;
330
+ return selection.providerId.length > 0 || selection.modelId.length > 0;
331
+ }
332
+
333
+ export function getCurrentCapabilities(controller: OnboardingWizardController): readonly OnboardingStep1CapabilityItem[] {
334
+ return controller.runtimeDerived.step1Capabilities.length > 0
335
+ ? controller.runtimeDerived.step1Capabilities
336
+ : DEFAULT_CAPABILITIES;
337
+ }
338
+
339
+ export function getCapabilitySelectionState(controller: OnboardingWizardController): readonly OnboardingStep1CapabilityItem[] {
340
+ return controller.getCurrentCapabilities().map((capability) => ({
341
+ ...capability,
342
+ selected: controller.toggleState.get(`capabilities.${capability.id}`) ?? capability.selected,
343
+ }));
344
+ }
345
+
346
+ export function hasExistingAccessState(controller: OnboardingWizardController): boolean {
347
+ const auth = controller.runtimeSnapshot?.auth.snapshot;
348
+ return controller.mode !== 'new'
349
+ || (controller.runtimeSnapshot?.subscriptions.active.length ?? 0) > 0
350
+ || (controller.runtimeSnapshot?.subscriptions.pending.length ?? 0) > 0
351
+ || (auth?.userCount ?? 0) > 0
352
+ || (auth?.sessionCount ?? 0) > 0
353
+ || Boolean(auth?.bootstrapCredentialPresent);
354
+ }