@pellux/goodvibes-tui 0.19.33 → 0.19.35
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/CHANGELOG.md +23 -0
- package/README.md +6 -3
- package/docs/foundation-artifacts/operator-contract.json +284 -112
- package/package.json +2 -2
- package/src/cli/management.ts +2 -2
- package/src/input/command-registry.ts +1 -0
- package/src/input/commands/cloudflare-runtime.ts +370 -0
- package/src/input/commands/local-auth-runtime.ts +4 -4
- package/src/input/commands/tts-runtime.ts +93 -10
- package/src/input/commands.ts +2 -0
- package/src/input/feed-context-factory.ts +1 -0
- package/src/input/handler-feed.ts +6 -0
- package/src/input/handler-modal-routes.ts +23 -10
- package/src/input/handler-modal-token-routes.ts +9 -0
- package/src/input/handler-onboarding-cloudflare.ts +391 -0
- package/src/input/handler-onboarding.ts +33 -0
- package/src/input/handler-picker-routes.ts +1 -1
- package/src/input/handler.ts +4 -1
- package/src/input/model-picker-types.ts +125 -0
- package/src/input/model-picker.ts +144 -135
- package/src/input/onboarding/onboarding-wizard-apply.ts +85 -0
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +494 -0
- package/src/input/onboarding/onboarding-wizard-cloudflare.ts +204 -0
- package/src/input/onboarding/onboarding-wizard-constants.ts +12 -1
- package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +117 -0
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +3 -41
- package/src/input/onboarding/onboarding-wizard-steps.ts +6 -6
- package/src/input/onboarding/onboarding-wizard-types.ts +8 -0
- package/src/input/settings-modal-types.ts +2 -1
- package/src/input/settings-modal.ts +30 -8
- package/src/renderer/buffer.ts +40 -2
- package/src/renderer/compositor.ts +25 -17
- package/src/renderer/model-picker-overlay.ts +70 -0
- package/src/renderer/settings-modal-helpers.ts +9 -0
- package/src/runtime/cloudflare-control-plane.ts +349 -0
- package/src/runtime/onboarding/apply.ts +9 -8
- package/src/runtime/onboarding/derivation.ts +26 -1
- package/src/runtime/onboarding/snapshot.ts +2 -0
- package/src/runtime/onboarding/types.ts +5 -1
- package/src/shell/ui-openers.ts +10 -1
- package/src/version.ts +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TerminalBuffer } from './buffer.ts';
|
|
2
2
|
import { DiffEngine } from './diff.ts';
|
|
3
|
-
import { type Line, createStyledCell } from '../types/grid.ts';
|
|
3
|
+
import { type Line, createEmptyCell, createStyledCell } from '../types/grid.ts';
|
|
4
4
|
import { getDisplayWidth } from '../utils/terminal-width.ts';
|
|
5
5
|
import type { SearchManager } from '../input/search.ts';
|
|
6
6
|
|
|
@@ -78,7 +78,7 @@ export class Compositor {
|
|
|
78
78
|
if (!this.backBuffer) {
|
|
79
79
|
this.backBuffer = new TerminalBuffer(width, height);
|
|
80
80
|
} else {
|
|
81
|
-
this.backBuffer.reset(width, height);
|
|
81
|
+
this.backBuffer.reset(width, height, this.frontBuffer);
|
|
82
82
|
}
|
|
83
83
|
const newBuffer = this.backBuffer;
|
|
84
84
|
|
|
@@ -154,19 +154,31 @@ export class Compositor {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
const panelStartX = sepX + 1;
|
|
157
|
+
const clearPanelRemainder = (fromX = 0) => {
|
|
158
|
+
for (let x = Math.max(0, fromX); x < panelWidth; x++) {
|
|
159
|
+
newBuffer.setCell(panelStartX + x, screenY, createEmptyCell());
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
const drawPanelLine = (panelLine: Line | undefined) => {
|
|
163
|
+
if (panelLine === undefined) {
|
|
164
|
+
clearPanelRemainder();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const limit = Math.min(panelLine.length, panelWidth);
|
|
168
|
+
for (let x = 0; x < limit; x++) {
|
|
169
|
+
const cell = panelLine[x];
|
|
170
|
+
if (cell !== undefined) {
|
|
171
|
+
newBuffer.setCell(panelStartX + x, screenY, cell);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
clearPanelRemainder(limit);
|
|
175
|
+
};
|
|
157
176
|
|
|
158
177
|
if (!hasBottomPane) {
|
|
159
178
|
// --- Single pane mode ---
|
|
160
179
|
// viewport row 0 → workspace bar, viewport rows 1+ → panel content
|
|
161
180
|
const panelLine = i === 0 ? p.workspaceBar : p.topContent[i - 1];
|
|
162
|
-
|
|
163
|
-
for (let x = 0; x < panelWidth; x++) {
|
|
164
|
-
const cell = panelLine[x];
|
|
165
|
-
if (cell !== undefined) {
|
|
166
|
-
newBuffer.setCell(panelStartX + x, screenY, cell);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
181
|
+
drawPanelLine(panelLine);
|
|
170
182
|
} else {
|
|
171
183
|
// --- Two pane mode ---
|
|
172
184
|
// Row layout (by viewport row i):
|
|
@@ -203,13 +215,8 @@ export class Compositor {
|
|
|
203
215
|
panelLine = p.bottomContent?.[i - (hSepRow + 2)];
|
|
204
216
|
}
|
|
205
217
|
|
|
206
|
-
if (
|
|
207
|
-
|
|
208
|
-
const cell = panelLine[x];
|
|
209
|
-
if (cell !== undefined) {
|
|
210
|
-
newBuffer.setCell(panelStartX + x, screenY, cell);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
218
|
+
if (i !== hSepRow) {
|
|
219
|
+
drawPanelLine(panelLine);
|
|
213
220
|
}
|
|
214
221
|
}
|
|
215
222
|
}
|
|
@@ -269,6 +276,7 @@ export class Compositor {
|
|
|
269
276
|
// Swap: back (just written) becomes the new front reference; old front becomes the next back
|
|
270
277
|
const swap = this.frontBuffer;
|
|
271
278
|
this.frontBuffer = this.backBuffer;
|
|
279
|
+
this.frontBuffer.clearDirty();
|
|
272
280
|
this.backBuffer = swap;
|
|
273
281
|
}
|
|
274
282
|
}
|
|
@@ -34,6 +34,10 @@ const MODE_TITLES: Record<string, string> = {
|
|
|
34
34
|
*/
|
|
35
35
|
export const MODEL_PICKER_CHROME_LINES = 7;
|
|
36
36
|
|
|
37
|
+
const renderCache = new WeakMap<ModelPickerModal, { key: string; lines: Line[] }>();
|
|
38
|
+
const objectIds = new WeakMap<object, number>();
|
|
39
|
+
let nextObjectId = 1;
|
|
40
|
+
|
|
37
41
|
function putRowText(line: Line, startX: number, maxWidth: number, text: string, fg: string, bg = '', bold = false, dim = false): void {
|
|
38
42
|
putOverlayText(line, startX, maxWidth, text, { fg, bg, bold, dim });
|
|
39
43
|
}
|
|
@@ -51,6 +55,10 @@ export function renderModelPickerOverlay(
|
|
|
51
55
|
maxVisible = 20,
|
|
52
56
|
viewportHeight?: number,
|
|
53
57
|
): Line[] {
|
|
58
|
+
const cacheKey = getRenderCacheKey(picker, width, maxVisible, viewportHeight);
|
|
59
|
+
const cached = renderCache.get(picker);
|
|
60
|
+
if (cached?.key === cacheKey) return cached.lines;
|
|
61
|
+
|
|
54
62
|
const lines: Line[] = [];
|
|
55
63
|
const metrics = getOverlaySurfaceMetrics(width, viewportHeight ?? 24, {
|
|
56
64
|
chromeRows: MODEL_PICKER_CHROME_LINES,
|
|
@@ -399,5 +407,67 @@ export function renderModelPickerOverlay(
|
|
|
399
407
|
putRowText(footerLine, layout.margin + 2, contentW, fitDisplay(truncateDisplay(hints, contentW), contentW), mutedFg, '', false, true);
|
|
400
408
|
lines.push(footerLine);
|
|
401
409
|
|
|
410
|
+
renderCache.set(picker, { key: cacheKey, lines });
|
|
402
411
|
return lines;
|
|
403
412
|
}
|
|
413
|
+
|
|
414
|
+
function getRenderCacheKey(
|
|
415
|
+
picker: ModelPickerModal,
|
|
416
|
+
width: number,
|
|
417
|
+
maxVisible: number,
|
|
418
|
+
viewportHeight: number | undefined,
|
|
419
|
+
): string {
|
|
420
|
+
const base = [
|
|
421
|
+
width,
|
|
422
|
+
maxVisible,
|
|
423
|
+
viewportHeight ?? '',
|
|
424
|
+
picker.mode,
|
|
425
|
+
picker.target,
|
|
426
|
+
picker.query,
|
|
427
|
+
picker.searchFocused ? 1 : 0,
|
|
428
|
+
picker.selectedIndex,
|
|
429
|
+
picker.scrollOffset,
|
|
430
|
+
picker.categoryFilter,
|
|
431
|
+
picker.capabilityFilter,
|
|
432
|
+
picker.availableOnly ? 1 : 0,
|
|
433
|
+
picker.benchmarkSort,
|
|
434
|
+
picker.groupBy,
|
|
435
|
+
keyForSet(picker.pinnedIds),
|
|
436
|
+
keyForSet(picker.configuredProviders),
|
|
437
|
+
];
|
|
438
|
+
|
|
439
|
+
if (picker.mode === 'model') {
|
|
440
|
+
const filtered = picker.getFilteredModels();
|
|
441
|
+
const selected = filtered[picker.selectedIndex];
|
|
442
|
+
base.push(objectId(picker.models), objectId(filtered), filtered.length, selected?.registryKey ?? selected?.id ?? '');
|
|
443
|
+
} else if (picker.mode === 'provider') {
|
|
444
|
+
const filteredProviders = picker.getFilteredProviders();
|
|
445
|
+
base.push(objectId(picker.providers), objectId(filteredProviders), filteredProviders.length, keyForMap(picker.configuredViaMap));
|
|
446
|
+
} else if (picker.mode === 'effort') {
|
|
447
|
+
base.push(objectId(picker.effortLevels), picker.effortLevels.join('\u001f'), picker.pendingModel?.registryKey ?? picker.pendingModel?.id ?? '');
|
|
448
|
+
} else if (picker.mode === 'contextCap') {
|
|
449
|
+
base.push(picker.contextCapQuery, picker.contextCapPendingModel?.registryKey ?? picker.contextCapPendingModel?.id ?? '');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return base.join('\u001e');
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function objectId(value: object): number {
|
|
456
|
+
const existing = objectIds.get(value);
|
|
457
|
+
if (existing !== undefined) return existing;
|
|
458
|
+
const next = nextObjectId++;
|
|
459
|
+
objectIds.set(value, next);
|
|
460
|
+
return next;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function keyForSet(values: ReadonlySet<string>): string {
|
|
464
|
+
return values.size === 0 ? '' : [...values].sort().join('\u001f');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function keyForMap(values: ReadonlyMap<string, string | undefined>): string {
|
|
468
|
+
if (values.size === 0) return '';
|
|
469
|
+
return [...values.entries()]
|
|
470
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
471
|
+
.map(([key, value]) => `${key}\u001d${value ?? ''}`)
|
|
472
|
+
.join('\u001f');
|
|
473
|
+
}
|
|
@@ -76,6 +76,7 @@ export const CATEGORY_LABELS: Record<(typeof SETTINGS_CATEGORIES)[number], strin
|
|
|
76
76
|
mcp: 'MCP',
|
|
77
77
|
sandbox: 'Sandbox',
|
|
78
78
|
surfaces: 'Surfaces',
|
|
79
|
+
cloudflare: 'Cloudflare',
|
|
79
80
|
danger: 'Danger',
|
|
80
81
|
tools: 'Tools',
|
|
81
82
|
flags: 'Flags',
|
|
@@ -138,6 +139,14 @@ export const SETTING_LABELS: Partial<Record<string, string>> = {
|
|
|
138
139
|
'surfaces.ntfy.remoteTopic': 'ntfy Daemon-Only Remote Topic',
|
|
139
140
|
'surfaces.ntfy.token': 'ntfy Token',
|
|
140
141
|
'surfaces.ntfy.defaultPriority': 'ntfy Default Priority',
|
|
142
|
+
'surfaces.homeassistant.enabled': 'Home Assistant Enabled',
|
|
143
|
+
'surfaces.homeassistant.instanceUrl': 'Home Assistant URL',
|
|
144
|
+
'surfaces.homeassistant.accessToken': 'Home Assistant Access Token',
|
|
145
|
+
'surfaces.homeassistant.webhookSecret': 'Home Assistant Webhook Secret',
|
|
146
|
+
'surfaces.homeassistant.defaultConversationId': 'Home Assistant Conversation ID',
|
|
147
|
+
'surfaces.homeassistant.deviceId': 'Home Assistant Device ID',
|
|
148
|
+
'surfaces.homeassistant.deviceName': 'Home Assistant Device Name',
|
|
149
|
+
'surfaces.homeassistant.eventType': 'Home Assistant Event Type',
|
|
141
150
|
};
|
|
142
151
|
|
|
143
152
|
export function getSettingLabel(entry: SettingEntry): string {
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { getOrCreateCompanionToken } from '@pellux/goodvibes-sdk/platform/pairing/companion-token';
|
|
3
|
+
import type { ConfigManager } from '../config/index.ts';
|
|
4
|
+
|
|
5
|
+
export const CLOUDFLARE_COMPONENT_IDS = [
|
|
6
|
+
'workers',
|
|
7
|
+
'queues',
|
|
8
|
+
'zeroTrustTunnel',
|
|
9
|
+
'zeroTrustAccess',
|
|
10
|
+
'dns',
|
|
11
|
+
'kv',
|
|
12
|
+
'durableObjects',
|
|
13
|
+
'secretsStore',
|
|
14
|
+
'r2',
|
|
15
|
+
] as const;
|
|
16
|
+
|
|
17
|
+
export type CloudflareComponent = typeof CLOUDFLARE_COMPONENT_IDS[number];
|
|
18
|
+
export type CloudflareComponentSelection = Partial<Record<CloudflareComponent, boolean>>;
|
|
19
|
+
export type CloudflareBatchMode = 'off' | 'explicit' | 'eligible-by-default';
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_CLOUDFLARE_COMPONENT_SELECTION: Readonly<Record<CloudflareComponent, boolean>> = {
|
|
22
|
+
workers: true,
|
|
23
|
+
queues: true,
|
|
24
|
+
zeroTrustTunnel: false,
|
|
25
|
+
zeroTrustAccess: false,
|
|
26
|
+
dns: false,
|
|
27
|
+
kv: false,
|
|
28
|
+
durableObjects: false,
|
|
29
|
+
secretsStore: false,
|
|
30
|
+
r2: false,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const CLOUDFLARE_COMPONENT_LABELS: Readonly<Record<CloudflareComponent, string>> = {
|
|
34
|
+
workers: 'Workers',
|
|
35
|
+
queues: 'Queues',
|
|
36
|
+
zeroTrustTunnel: 'Zero Trust Tunnel',
|
|
37
|
+
zeroTrustAccess: 'Zero Trust Access',
|
|
38
|
+
dns: 'DNS hostname',
|
|
39
|
+
kv: 'KV',
|
|
40
|
+
durableObjects: 'Durable Objects',
|
|
41
|
+
secretsStore: 'Secrets Store',
|
|
42
|
+
r2: 'R2 artifacts',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export interface CloudflareProvisionStep {
|
|
46
|
+
readonly name: string;
|
|
47
|
+
readonly status: 'ok' | 'skipped' | 'warning';
|
|
48
|
+
readonly message?: string;
|
|
49
|
+
readonly resourceId?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface CloudflareControlPlaneStatus {
|
|
53
|
+
readonly enabled: boolean;
|
|
54
|
+
readonly ready: boolean;
|
|
55
|
+
readonly configured: Record<string, boolean>;
|
|
56
|
+
readonly config: Record<string, unknown>;
|
|
57
|
+
readonly warnings: readonly string[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface CloudflareTokenRequirement {
|
|
61
|
+
readonly component: CloudflareComponent | 'bootstrap';
|
|
62
|
+
readonly scope: 'account' | 'zone' | 'user' | 'r2';
|
|
63
|
+
readonly permission: string;
|
|
64
|
+
readonly alternatives?: readonly string[];
|
|
65
|
+
readonly reason: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface CloudflareTokenRequirementsResult {
|
|
69
|
+
readonly ok: true;
|
|
70
|
+
readonly components: Readonly<Record<CloudflareComponent, boolean>>;
|
|
71
|
+
readonly permissions: readonly CloudflareTokenRequirement[];
|
|
72
|
+
readonly bootstrapToken: {
|
|
73
|
+
readonly requiredForSdkCreation: boolean;
|
|
74
|
+
readonly storeInGoodVibes: false;
|
|
75
|
+
readonly instructions: readonly string[];
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface CloudflareValidateResult {
|
|
80
|
+
readonly ok: boolean;
|
|
81
|
+
readonly account?: {
|
|
82
|
+
readonly id: string;
|
|
83
|
+
readonly name: string;
|
|
84
|
+
readonly type?: string;
|
|
85
|
+
};
|
|
86
|
+
readonly tokenSource: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface CloudflareOperationalTokenResult {
|
|
90
|
+
readonly ok: true;
|
|
91
|
+
readonly tokenId?: string;
|
|
92
|
+
readonly tokenName: string;
|
|
93
|
+
readonly tokenSource: 'bootstrap';
|
|
94
|
+
readonly apiTokenRef?: string;
|
|
95
|
+
readonly generatedToken?: string;
|
|
96
|
+
readonly accountId: string;
|
|
97
|
+
readonly zoneId?: string;
|
|
98
|
+
readonly permissions: readonly CloudflareTokenRequirement[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface CloudflareDiscoverResult {
|
|
102
|
+
readonly ok: true;
|
|
103
|
+
readonly tokenSource: string;
|
|
104
|
+
readonly accounts: ReadonlyArray<{ readonly id: string; readonly name: string; readonly type?: string }>;
|
|
105
|
+
readonly selectedAccount?: { readonly id: string; readonly name: string; readonly type?: string };
|
|
106
|
+
readonly zones: ReadonlyArray<{ readonly id: string; readonly name: string; readonly status?: string; readonly type?: string }>;
|
|
107
|
+
readonly selectedZone?: { readonly id: string; readonly name: string; readonly status?: string; readonly type?: string };
|
|
108
|
+
readonly workerSubdomain?: string;
|
|
109
|
+
readonly queues?: ReadonlyArray<{ readonly queue_id?: string; readonly queue_name?: string }>;
|
|
110
|
+
readonly kvNamespaces?: ReadonlyArray<{ readonly id?: string; readonly title?: string }>;
|
|
111
|
+
readonly durableObjectNamespaces?: ReadonlyArray<{ readonly id?: string; readonly name?: string; readonly class?: string }>;
|
|
112
|
+
readonly r2Buckets?: ReadonlyArray<{ readonly name?: string; readonly storage_class?: string }>;
|
|
113
|
+
readonly secretsStores?: ReadonlyArray<{ readonly id: string; readonly name: string }>;
|
|
114
|
+
readonly tunnels?: ReadonlyArray<{ readonly id?: string; readonly name?: string; readonly status?: string }>;
|
|
115
|
+
readonly accessApplications?: ReadonlyArray<{ readonly id?: string; readonly name?: string; readonly domain?: string; readonly type?: string }>;
|
|
116
|
+
readonly warnings: readonly string[];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface CloudflareProvisionResult {
|
|
120
|
+
readonly ok: boolean;
|
|
121
|
+
readonly dryRun: false;
|
|
122
|
+
readonly steps: readonly CloudflareProvisionStep[];
|
|
123
|
+
readonly account?: { readonly id: string; readonly name: string };
|
|
124
|
+
readonly worker?: { readonly name: string; readonly baseUrl?: string; readonly subdomain?: string; readonly hostname?: string; readonly cron?: string };
|
|
125
|
+
readonly queues?: { readonly queueName: string; readonly queueId: string; readonly deadLetterQueueName: string; readonly deadLetterQueueId: string; readonly consumerId?: string };
|
|
126
|
+
readonly tunnel?: { readonly id: string; readonly name: string; readonly hostname?: string; readonly tokenRef?: string };
|
|
127
|
+
readonly access?: { readonly appId?: string; readonly serviceTokenId?: string; readonly serviceTokenRef?: string };
|
|
128
|
+
readonly dns?: {
|
|
129
|
+
readonly zoneId: string;
|
|
130
|
+
readonly zoneName?: string;
|
|
131
|
+
readonly records: ReadonlyArray<{ readonly id?: string; readonly name?: string; readonly type?: string; readonly content?: string }>;
|
|
132
|
+
};
|
|
133
|
+
readonly kv?: { readonly namespaceName: string; readonly namespaceId: string };
|
|
134
|
+
readonly durableObjects?: { readonly namespaceName: string; readonly namespaceId?: string };
|
|
135
|
+
readonly r2?: { readonly bucketName: string; readonly storageClass: 'Standard' };
|
|
136
|
+
readonly secretsStore?: { readonly storeName: string; readonly storeId: string };
|
|
137
|
+
readonly verification?: CloudflareVerifyResult;
|
|
138
|
+
readonly generatedSecrets?: {
|
|
139
|
+
readonly workerClientToken?: string;
|
|
140
|
+
readonly tunnelToken?: string;
|
|
141
|
+
readonly accessServiceTokenClientId?: string;
|
|
142
|
+
readonly accessServiceTokenClientSecret?: string;
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface CloudflareVerifyResult {
|
|
147
|
+
readonly ok: boolean;
|
|
148
|
+
readonly workerHealth: {
|
|
149
|
+
readonly ok: boolean;
|
|
150
|
+
readonly status: number;
|
|
151
|
+
readonly error?: string;
|
|
152
|
+
};
|
|
153
|
+
readonly daemonBatchProxy?: {
|
|
154
|
+
readonly ok: boolean;
|
|
155
|
+
readonly status: number;
|
|
156
|
+
readonly error?: string;
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface CloudflareDisableResult {
|
|
161
|
+
readonly ok: boolean;
|
|
162
|
+
readonly steps: readonly CloudflareProvisionStep[];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface CloudflareTokenRequirementsRequest {
|
|
166
|
+
readonly components?: CloudflareComponentSelection;
|
|
167
|
+
readonly includeBootstrap?: boolean;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface CloudflareOperationalTokenRequest extends CloudflareTokenRequirementsRequest {
|
|
171
|
+
readonly accountId?: string;
|
|
172
|
+
readonly zoneId?: string;
|
|
173
|
+
readonly zoneName?: string;
|
|
174
|
+
readonly bootstrapToken?: string;
|
|
175
|
+
readonly tokenName?: string;
|
|
176
|
+
readonly expiresOn?: string;
|
|
177
|
+
readonly persistConfig?: boolean;
|
|
178
|
+
readonly storeApiToken?: boolean;
|
|
179
|
+
readonly returnGeneratedToken?: boolean;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface CloudflareValidateRequest {
|
|
183
|
+
readonly accountId?: string;
|
|
184
|
+
readonly apiToken?: string;
|
|
185
|
+
readonly apiTokenRef?: string;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface CloudflareDiscoverRequest extends CloudflareValidateRequest {
|
|
189
|
+
readonly components?: CloudflareComponentSelection;
|
|
190
|
+
readonly zoneId?: string;
|
|
191
|
+
readonly zoneName?: string;
|
|
192
|
+
readonly includeResources?: boolean;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface CloudflareProvisionRequest extends CloudflareDiscoverRequest {
|
|
196
|
+
readonly workerName?: string;
|
|
197
|
+
readonly workerSubdomain?: string;
|
|
198
|
+
readonly workerHostname?: string;
|
|
199
|
+
readonly workerBaseUrl?: string;
|
|
200
|
+
readonly daemonBaseUrl?: string;
|
|
201
|
+
readonly daemonHostname?: string;
|
|
202
|
+
readonly queueName?: string;
|
|
203
|
+
readonly deadLetterQueueName?: string;
|
|
204
|
+
readonly tunnelName?: string;
|
|
205
|
+
readonly tunnelId?: string;
|
|
206
|
+
readonly tunnelServiceUrl?: string;
|
|
207
|
+
readonly tunnelTokenRef?: string;
|
|
208
|
+
readonly accessAppId?: string;
|
|
209
|
+
readonly accessServiceTokenId?: string;
|
|
210
|
+
readonly accessServiceTokenRef?: string;
|
|
211
|
+
readonly kvNamespaceName?: string;
|
|
212
|
+
readonly kvNamespaceId?: string;
|
|
213
|
+
readonly durableObjectNamespaceName?: string;
|
|
214
|
+
readonly durableObjectNamespaceId?: string;
|
|
215
|
+
readonly r2BucketName?: string;
|
|
216
|
+
readonly secretsStoreName?: string;
|
|
217
|
+
readonly secretsStoreId?: string;
|
|
218
|
+
readonly workerCron?: string;
|
|
219
|
+
readonly operatorToken?: string;
|
|
220
|
+
readonly operatorTokenRef?: string;
|
|
221
|
+
readonly workerClientToken?: string;
|
|
222
|
+
readonly workerClientTokenRef?: string;
|
|
223
|
+
readonly storeApiToken?: boolean;
|
|
224
|
+
readonly storeOperatorToken?: boolean;
|
|
225
|
+
readonly storeWorkerClientToken?: boolean;
|
|
226
|
+
readonly returnGeneratedSecrets?: boolean;
|
|
227
|
+
readonly enableWorkersDev?: boolean;
|
|
228
|
+
readonly queueJobPayloads?: boolean;
|
|
229
|
+
readonly verify?: boolean;
|
|
230
|
+
readonly persistConfig?: boolean;
|
|
231
|
+
readonly batchMode?: CloudflareBatchMode;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export interface CloudflareVerifyRequest {
|
|
235
|
+
readonly workerBaseUrl?: string;
|
|
236
|
+
readonly workerClientToken?: string;
|
|
237
|
+
readonly workerClientTokenRef?: string;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export interface CloudflareDisableRequest extends CloudflareValidateRequest {
|
|
241
|
+
readonly workerName?: string;
|
|
242
|
+
readonly disableWorkerSubdomain?: boolean;
|
|
243
|
+
readonly disableCron?: boolean;
|
|
244
|
+
readonly persistConfig?: boolean;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export class CloudflareDaemonRouteError extends Error {
|
|
248
|
+
readonly status: number;
|
|
249
|
+
readonly code: string;
|
|
250
|
+
|
|
251
|
+
constructor(message: string, status: number, code: string) {
|
|
252
|
+
super(message);
|
|
253
|
+
this.name = 'CloudflareDaemonRouteError';
|
|
254
|
+
this.status = status;
|
|
255
|
+
this.code = code;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export interface CloudflareDaemonClient {
|
|
260
|
+
status(): Promise<CloudflareControlPlaneStatus>;
|
|
261
|
+
tokenRequirements(input?: CloudflareTokenRequirementsRequest): Promise<CloudflareTokenRequirementsResult>;
|
|
262
|
+
createOperationalToken(input: CloudflareOperationalTokenRequest): Promise<CloudflareOperationalTokenResult>;
|
|
263
|
+
discover(input?: CloudflareDiscoverRequest): Promise<CloudflareDiscoverResult>;
|
|
264
|
+
validate(input?: CloudflareValidateRequest): Promise<CloudflareValidateResult>;
|
|
265
|
+
provision(input: CloudflareProvisionRequest): Promise<CloudflareProvisionResult>;
|
|
266
|
+
verify(input?: CloudflareVerifyRequest): Promise<CloudflareVerifyResult>;
|
|
267
|
+
disable(input?: CloudflareDisableRequest): Promise<CloudflareDisableResult>;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export interface CloudflareDaemonClientOptions {
|
|
271
|
+
readonly configManager: Pick<ConfigManager, 'get'>;
|
|
272
|
+
readonly homeDirectory: string;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function connectHostForBindHost(host: string): string {
|
|
276
|
+
if (host === '0.0.0.0' || host === '::' || host.trim().length === 0) return '127.0.0.1';
|
|
277
|
+
return host;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function hostForUrl(host: string): string {
|
|
281
|
+
return host.includes(':') && !host.startsWith('[') ? `[${host}]` : host;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function resolveCloudflareDaemonBaseUrl(configManager: Pick<ConfigManager, 'get'>): string {
|
|
285
|
+
const configuredBaseUrl = String(configManager.get('controlPlane.baseUrl' as never) ?? '').trim();
|
|
286
|
+
if (configuredBaseUrl) return configuredBaseUrl.replace(/\/+$/, '');
|
|
287
|
+
const host = hostForUrl(connectHostForBindHost(String(configManager.get('controlPlane.host' as never) ?? '127.0.0.1')));
|
|
288
|
+
const portValue = Number(configManager.get('controlPlane.port' as never) ?? 3421);
|
|
289
|
+
const port = Number.isFinite(portValue) && portValue > 0 ? portValue : 3421;
|
|
290
|
+
return `http://${host}:${port}`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function buildDefaultCloudflareDaemonBaseUrl(configManager: Pick<ConfigManager, 'get'>): string {
|
|
294
|
+
return resolveCloudflareDaemonBaseUrl(configManager);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function readDaemonToken(homeDirectory: string): string {
|
|
298
|
+
const daemonHomeDir = join(homeDirectory, '.goodvibes', 'daemon');
|
|
299
|
+
return getOrCreateCompanionToken('tui', { daemonHomeDir }).token;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function readJsonResponse<T>(response: Response): Promise<T> {
|
|
303
|
+
const text = await response.text();
|
|
304
|
+
const body = text.trim().length > 0 ? JSON.parse(text) as unknown : {};
|
|
305
|
+
if (!response.ok) {
|
|
306
|
+
const record = body && typeof body === 'object' ? body as Record<string, unknown> : {};
|
|
307
|
+
const message = typeof record.error === 'string' ? record.error : `Cloudflare daemon route failed with HTTP ${response.status}`;
|
|
308
|
+
const code = typeof record.code === 'string' ? record.code : 'CLOUDFLARE_DAEMON_ROUTE_ERROR';
|
|
309
|
+
throw new CloudflareDaemonRouteError(message, response.status, code);
|
|
310
|
+
}
|
|
311
|
+
return body as T;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function createCloudflareDaemonClient(options: CloudflareDaemonClientOptions): CloudflareDaemonClient {
|
|
315
|
+
const baseUrl = resolveCloudflareDaemonBaseUrl(options.configManager);
|
|
316
|
+
const token = readDaemonToken(options.homeDirectory);
|
|
317
|
+
|
|
318
|
+
const requestJson = async <T>(path: string, init: RequestInit = {}): Promise<T> => {
|
|
319
|
+
const headers = new Headers(init.headers);
|
|
320
|
+
headers.set('Authorization', `Bearer ${token}`);
|
|
321
|
+
if (init.body !== undefined) headers.set('Content-Type', 'application/json');
|
|
322
|
+
const response = await fetch(`${baseUrl}${path}`, { ...init, headers });
|
|
323
|
+
return await readJsonResponse<T>(response);
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const postJson = <T>(path: string, body: unknown): Promise<T> => requestJson<T>(path, {
|
|
327
|
+
method: 'POST',
|
|
328
|
+
body: JSON.stringify(body ?? {}),
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
status: () => requestJson<CloudflareControlPlaneStatus>('/api/cloudflare/status'),
|
|
333
|
+
tokenRequirements: (input = {}) => postJson<CloudflareTokenRequirementsResult>('/api/cloudflare/token/requirements', input),
|
|
334
|
+
createOperationalToken: (input) => postJson<CloudflareOperationalTokenResult>('/api/cloudflare/token/create', input),
|
|
335
|
+
discover: (input = {}) => postJson<CloudflareDiscoverResult>('/api/cloudflare/discover', input),
|
|
336
|
+
validate: (input = {}) => postJson<CloudflareValidateResult>('/api/cloudflare/validate', input),
|
|
337
|
+
provision: (input) => postJson<CloudflareProvisionResult>('/api/cloudflare/provision', input),
|
|
338
|
+
verify: (input = {}) => postJson<CloudflareVerifyResult>('/api/cloudflare/verify', input),
|
|
339
|
+
disable: (input = {}) => postJson<CloudflareDisableResult>('/api/cloudflare/disable', input),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function normalizeCloudflareComponents(selection: CloudflareComponentSelection | undefined): Record<CloudflareComponent, boolean> {
|
|
344
|
+
const result: Record<CloudflareComponent, boolean> = { ...DEFAULT_CLOUDFLARE_COMPONENT_SELECTION };
|
|
345
|
+
for (const component of CLOUDFLARE_COMPONENT_IDS) {
|
|
346
|
+
if (typeof selection?.[component] === 'boolean') result[component] = selection[component] === true;
|
|
347
|
+
}
|
|
348
|
+
return result;
|
|
349
|
+
}
|
|
@@ -353,25 +353,26 @@ function buildAuthRollbackAction(
|
|
|
353
353
|
): RollbackAction {
|
|
354
354
|
validateAuthOperation(deps, operation);
|
|
355
355
|
const auth = deps.auth!;
|
|
356
|
+
const mutable = auth as unknown as MutableAuthManager;
|
|
356
357
|
const username = operation.username.trim();
|
|
357
358
|
const before = auth.inspect();
|
|
358
359
|
const existingUser = before.users.find((user) => user.username === username);
|
|
359
|
-
const
|
|
360
|
+
const existingSessionFingerprints = new Set(before.sessions
|
|
360
361
|
.filter((session) => session.username === username)
|
|
361
|
-
.map((session) => session.
|
|
362
|
+
.map((session) => session.tokenFingerprint));
|
|
362
363
|
const userStoreSnapshot = existsSync(before.userStorePath) ? readFileSync(before.userStorePath, 'utf-8') : null;
|
|
363
364
|
const bootstrapCredentialSnapshot = existsSync(before.bootstrapCredentialPath)
|
|
364
365
|
? readFileSync(before.bootstrapCredentialPath, 'utf-8')
|
|
365
366
|
: null;
|
|
366
367
|
const bootstrapCredential = parseBootstrapCredential(bootstrapCredentialSnapshot);
|
|
367
|
-
const beforeSessions =
|
|
368
|
+
const beforeSessions = mutable.sessions instanceof Map
|
|
369
|
+
? [...mutable.sessions.entries()].map(([token, session]) => [token, { ...session }] as const)
|
|
370
|
+
: [];
|
|
368
371
|
|
|
369
372
|
return () => {
|
|
370
|
-
const mutable = auth as unknown as MutableAuthManager;
|
|
371
|
-
|
|
372
373
|
for (const session of auth.inspect().sessions) {
|
|
373
|
-
if (session.username === username && !
|
|
374
|
-
auth.revokeSession(session.
|
|
374
|
+
if (session.username === username && !existingSessionFingerprints.has(session.tokenFingerprint)) {
|
|
375
|
+
auth.revokeSession(session.tokenFingerprint);
|
|
375
376
|
}
|
|
376
377
|
}
|
|
377
378
|
|
|
@@ -402,7 +403,7 @@ function buildAuthRollbackAction(
|
|
|
402
403
|
|
|
403
404
|
if (mutable.sessions instanceof Map) {
|
|
404
405
|
mutable.sessions.clear();
|
|
405
|
-
for (const session of beforeSessions) mutable.sessions.set(
|
|
406
|
+
for (const [token, session] of beforeSessions) mutable.sessions.set(token, session);
|
|
406
407
|
}
|
|
407
408
|
};
|
|
408
409
|
}
|
|
@@ -238,6 +238,15 @@ function hasExternalIntegrations(snapshot: OnboardingSnapshotState): boolean {
|
|
|
238
238
|
|| countConfiguredSurfaceKinds(snapshot) > 0;
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
+
function hasCloudflareBatch(snapshot: OnboardingSnapshotState): boolean {
|
|
242
|
+
return snapshot.config.cloudflare.enabled
|
|
243
|
+
|| snapshot.config.batch.queueBackend === 'cloudflare'
|
|
244
|
+
|| snapshot.config.batch.mode !== 'off'
|
|
245
|
+
|| snapshot.config.cloudflare.accountId.trim().length > 0
|
|
246
|
+
|| snapshot.config.cloudflare.apiTokenRef.trim().length > 0
|
|
247
|
+
|| snapshot.config.cloudflare.workerBaseUrl.trim().length > 0;
|
|
248
|
+
}
|
|
249
|
+
|
|
241
250
|
function describeLocalTuiOnly(snapshot: OnboardingSnapshotState): string {
|
|
242
251
|
if (!hasAnyServerEnabled(snapshot)) {
|
|
243
252
|
return 'Use GoodVibes only in this terminal. No browser access, background service, HTTP listener, external app surface, or network setup.';
|
|
@@ -272,12 +281,20 @@ function describeExternalIntegrations(snapshot: OnboardingSnapshotState): string
|
|
|
272
281
|
]).size;
|
|
273
282
|
|
|
274
283
|
if (integrationCount === 0) {
|
|
275
|
-
return 'Enable setup screens for Slack, Discord, Telegram, Teams, Matrix, and other app surfaces you choose.';
|
|
284
|
+
return 'Enable setup screens for Slack, Discord, Telegram, Home Assistant, Teams, Matrix, and other app surfaces you choose.';
|
|
276
285
|
}
|
|
277
286
|
|
|
278
287
|
return `Review and configure ${integrationCount} detected external app, service, or surface integration signal(s).`;
|
|
279
288
|
}
|
|
280
289
|
|
|
290
|
+
function describeCloudflareBatch(snapshot: OnboardingSnapshotState): string {
|
|
291
|
+
if (hasCloudflareBatch(snapshot)) {
|
|
292
|
+
return 'Review Cloudflare Workers/Queues batch processing, token storage, and optional remote daemon provisioning settings.';
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return 'Optionally configure Cloudflare Workers and Queues for explicit or eligible background batch jobs. Immediate local daemon behavior stays the default unless enabled.';
|
|
296
|
+
}
|
|
297
|
+
|
|
281
298
|
function getAcknowledgementAccepted(
|
|
282
299
|
snapshot: OnboardingSnapshotState,
|
|
283
300
|
target: OnboardingAcknowledgementTarget,
|
|
@@ -346,6 +363,12 @@ export function deriveStep1Capabilities(
|
|
|
346
363
|
selected: hasExternalIntegrations(snapshot),
|
|
347
364
|
detail: describeExternalIntegrations(snapshot),
|
|
348
365
|
},
|
|
366
|
+
{
|
|
367
|
+
id: 'cloudflare-batch',
|
|
368
|
+
label: 'Use Cloudflare for batch or remote daemon work',
|
|
369
|
+
selected: hasCloudflareBatch(snapshot),
|
|
370
|
+
detail: describeCloudflareBatch(snapshot),
|
|
371
|
+
},
|
|
349
372
|
];
|
|
350
373
|
}
|
|
351
374
|
|
|
@@ -360,6 +383,7 @@ export function deriveStep1CapabilityFlags(
|
|
|
360
383
|
readonly httpListener: boolean;
|
|
361
384
|
readonly web: boolean;
|
|
362
385
|
readonly surfaces: boolean;
|
|
386
|
+
readonly cloudflare: boolean;
|
|
363
387
|
} {
|
|
364
388
|
return {
|
|
365
389
|
providers: hasConfiguredProviderState(snapshot) || hasCustomizedProviderRouting(snapshot),
|
|
@@ -372,6 +396,7 @@ export function deriveStep1CapabilityFlags(
|
|
|
372
396
|
httpListener: snapshot.bindSettings.httpListenerEnabled,
|
|
373
397
|
web: snapshot.bindSettings.web.enabled,
|
|
374
398
|
surfaces: countConfiguredSurfaceKinds(snapshot) > 0,
|
|
399
|
+
cloudflare: hasCloudflareBatch(snapshot),
|
|
375
400
|
};
|
|
376
401
|
}
|
|
377
402
|
|
|
@@ -41,6 +41,8 @@ function buildConfigSnapshot(
|
|
|
41
41
|
surfaces: config.getCategory('surfaces'),
|
|
42
42
|
service: config.getCategory('service'),
|
|
43
43
|
featureFlags: config.getCategory('featureFlags'),
|
|
44
|
+
batch: config.getCategory('batch'),
|
|
45
|
+
cloudflare: config.getCategory('cloudflare'),
|
|
44
46
|
};
|
|
45
47
|
}
|
|
46
48
|
|
|
@@ -47,6 +47,8 @@ export interface OnboardingConfigSnapshot {
|
|
|
47
47
|
readonly surfaces: GoodVibesConfig['surfaces'];
|
|
48
48
|
readonly service: GoodVibesConfig['service'];
|
|
49
49
|
readonly featureFlags: GoodVibesConfig['featureFlags'];
|
|
50
|
+
readonly batch: GoodVibesConfig['batch'];
|
|
51
|
+
readonly cloudflare: GoodVibesConfig['cloudflare'];
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
export interface OnboardingProviderRoutingSnapshot {
|
|
@@ -196,7 +198,8 @@ export type OnboardingStep1CapabilityId =
|
|
|
196
198
|
| 'browser-access'
|
|
197
199
|
| 'network-access'
|
|
198
200
|
| 'webhook-events'
|
|
199
|
-
| 'external-integrations'
|
|
201
|
+
| 'external-integrations'
|
|
202
|
+
| 'cloudflare-batch';
|
|
200
203
|
|
|
201
204
|
export interface OnboardingStep1CapabilityItem {
|
|
202
205
|
readonly id: OnboardingStep1CapabilityId;
|
|
@@ -214,6 +217,7 @@ export interface OnboardingStep1CapabilityFlags {
|
|
|
214
217
|
readonly httpListener: boolean;
|
|
215
218
|
readonly web: boolean;
|
|
216
219
|
readonly surfaces: boolean;
|
|
220
|
+
readonly cloudflare: boolean;
|
|
217
221
|
}
|
|
218
222
|
|
|
219
223
|
export interface OnboardingAcknowledgementState {
|