@pellux/goodvibes-tui 0.19.33 → 0.19.34
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 +12 -0
- package/README.md +3 -1
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +2 -2
- package/src/input/command-registry.ts +1 -0
- package/src/input/commands/cloudflare-runtime.ts +343 -0
- 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 +81 -0
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +449 -0
- package/src/input/onboarding/onboarding-wizard-cloudflare.ts +199 -0
- package/src/input/onboarding/onboarding-wizard-constants.ts +7 -0
- 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 +1 -0
- package/src/runtime/cloudflare-control-plane.ts +328 -0
- package/src/runtime/onboarding/derivation.ts +25 -0
- 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
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import { CLOUDFLARE_COMPONENT_IDS, DEFAULT_CLOUDFLARE_COMPONENT_SELECTION } from '../../runtime/cloudflare-control-plane.ts';
|
|
2
|
+
import { normalizeText } from './onboarding-wizard-helpers.ts';
|
|
3
|
+
import type { OnboardingWizardController } from './onboarding-wizard.ts';
|
|
4
|
+
import type { OnboardingWizardFieldDefinition, OnboardingWizardStepDefinition } from './onboarding-wizard-types.ts';
|
|
5
|
+
import {
|
|
6
|
+
CLOUDFLARE_BATCH_MODE_OPTIONS,
|
|
7
|
+
CLOUDFLARE_PROVISION_OPTIONS,
|
|
8
|
+
CLOUDFLARE_SETUP_SOURCE_OPTIONS,
|
|
9
|
+
CLOUDFLARE_YES_NO_OPTIONS,
|
|
10
|
+
cloudflareComponentFieldId,
|
|
11
|
+
cloudflareComponentLabel,
|
|
12
|
+
getCloudflareBatchMode,
|
|
13
|
+
getCloudflareComponentSelection,
|
|
14
|
+
getCloudflareSetupSource,
|
|
15
|
+
} from './onboarding-wizard-cloudflare.ts';
|
|
16
|
+
|
|
17
|
+
export function buildCloudflareStep(controller: OnboardingWizardController): OnboardingWizardStepDefinition {
|
|
18
|
+
const config = controller.runtimeSnapshot?.config.cloudflare;
|
|
19
|
+
const batch = controller.runtimeSnapshot?.config.batch;
|
|
20
|
+
const enabledDefault = controller.isCapabilitySelected('cloudflare-batch') || config?.enabled === true;
|
|
21
|
+
const enabled = controller.getBooleanFieldValue('cloudflare.enabled', enabledDefault);
|
|
22
|
+
const components = getCloudflareComponentSelection(controller);
|
|
23
|
+
const componentCount = Object.values(components).filter(Boolean).length;
|
|
24
|
+
const setupSource = getCloudflareSetupSource(controller);
|
|
25
|
+
const batchMode = getCloudflareBatchMode(controller);
|
|
26
|
+
const bind = controller.runtimeSnapshot?.bindSettings.controlPlane;
|
|
27
|
+
const defaultDaemonBaseUrl = normalizeText(config?.daemonBaseUrl)
|
|
28
|
+
|| `http://${bind?.host && bind.host !== '0.0.0.0' && bind.host !== '::' ? bind.host : '127.0.0.1'}:${bind?.port ?? 3421}`;
|
|
29
|
+
const resultMessage = controller.textState.get('cloudflare.action-status') ?? 'No Cloudflare daemon action has run in this wizard session.';
|
|
30
|
+
const fields: OnboardingWizardFieldDefinition[] = [
|
|
31
|
+
{
|
|
32
|
+
kind: 'checklist',
|
|
33
|
+
id: 'cloudflare.enabled',
|
|
34
|
+
label: 'Enable Cloudflare integration',
|
|
35
|
+
hint: 'Turns on GoodVibes Cloudflare config. Batch execution remains opt-in through the batch mode below.',
|
|
36
|
+
defaultValue: enabledDefault,
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
if (enabled) {
|
|
41
|
+
fields.push(
|
|
42
|
+
{
|
|
43
|
+
kind: 'radio',
|
|
44
|
+
id: 'cloudflare.batch-mode',
|
|
45
|
+
label: 'Batch mode',
|
|
46
|
+
hint: 'Controls when daemon work uses the batch path. Off keeps normal immediate behavior.',
|
|
47
|
+
options: CLOUDFLARE_BATCH_MODE_OPTIONS,
|
|
48
|
+
defaultValue: batch?.mode ?? 'off',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
kind: 'radio',
|
|
52
|
+
id: 'cloudflare.free-tier-mode',
|
|
53
|
+
label: 'Free-tier guardrails',
|
|
54
|
+
hint: 'Keep conservative queue-operation limits visible for free-tier Cloudflare accounts.',
|
|
55
|
+
options: CLOUDFLARE_YES_NO_OPTIONS,
|
|
56
|
+
defaultValue: config?.freeTierMode === false ? 'no' : 'yes',
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
for (const component of CLOUDFLARE_COMPONENT_IDS) {
|
|
61
|
+
const advanced = component !== 'workers' && component !== 'queues';
|
|
62
|
+
fields.push({
|
|
63
|
+
kind: 'checklist',
|
|
64
|
+
id: cloudflareComponentFieldId(component),
|
|
65
|
+
label: `${cloudflareComponentLabel(component)}${advanced ? ' (advanced)' : ''}`,
|
|
66
|
+
hint: cloudflareComponentHint(component),
|
|
67
|
+
defaultValue: config?.enabled === true
|
|
68
|
+
? component === 'workers' || component === 'queues'
|
|
69
|
+
: DEFAULT_CLOUDFLARE_COMPONENT_SELECTION[component],
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fields.push(
|
|
74
|
+
{
|
|
75
|
+
kind: 'radio',
|
|
76
|
+
id: 'cloudflare.setup-source',
|
|
77
|
+
label: 'Cloudflare token setup path',
|
|
78
|
+
hint: 'Choose whether the daemon SDK route creates/stores an operational token, uses an existing token, or only saves settings.',
|
|
79
|
+
options: CLOUDFLARE_SETUP_SOURCE_OPTIONS,
|
|
80
|
+
defaultValue: getCloudflareSetupSource(controller),
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
if (setupSource === 'bootstrap-token') {
|
|
85
|
+
fields.push({
|
|
86
|
+
kind: 'masked',
|
|
87
|
+
id: 'cloudflare.bootstrap-token',
|
|
88
|
+
label: 'Temporary bootstrap token',
|
|
89
|
+
hint: 'Used once by the SDK route to create a narrower GoodVibes operational token. It is never persisted.',
|
|
90
|
+
placeholder: 'temporary Cloudflare token',
|
|
91
|
+
defaultValue: '',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (setupSource === 'bootstrap-env') {
|
|
95
|
+
fields.push({
|
|
96
|
+
kind: 'text',
|
|
97
|
+
id: 'cloudflare.bootstrap-env-name',
|
|
98
|
+
label: 'Bootstrap token environment variable',
|
|
99
|
+
hint: 'The TUI reads this environment variable once and passes the value to the SDK token-create route. It is not persisted.',
|
|
100
|
+
placeholder: 'GOODVIBES_CLOUDFLARE_BOOTSTRAP_TOKEN',
|
|
101
|
+
defaultValue: 'GOODVIBES_CLOUDFLARE_BOOTSTRAP_TOKEN',
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
if (setupSource === 'operational-token') {
|
|
105
|
+
fields.push({
|
|
106
|
+
kind: 'masked',
|
|
107
|
+
id: 'cloudflare.operational-token',
|
|
108
|
+
label: 'Final Cloudflare API token',
|
|
109
|
+
hint: 'A fully-created operational token. Provisioning can store it as goodvibes://secrets/goodvibes/CLOUDFLARE_API_TOKEN.',
|
|
110
|
+
placeholder: 'Cloudflare API token',
|
|
111
|
+
defaultValue: '',
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (setupSource === 'operational-env') {
|
|
115
|
+
fields.push({
|
|
116
|
+
kind: 'text',
|
|
117
|
+
id: 'cloudflare.operational-env-name',
|
|
118
|
+
label: 'Operational token environment variable',
|
|
119
|
+
hint: 'Defaults to CLOUDFLARE_API_TOKEN. The SDK can also resolve goodvibes://secrets/env/<name>.',
|
|
120
|
+
placeholder: 'CLOUDFLARE_API_TOKEN',
|
|
121
|
+
defaultValue: config?.apiTokenRef?.startsWith('goodvibes://secrets/env/')
|
|
122
|
+
? decodeURIComponent(config.apiTokenRef.slice('goodvibes://secrets/env/'.length))
|
|
123
|
+
: 'CLOUDFLARE_API_TOKEN',
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
fields.push(
|
|
128
|
+
{
|
|
129
|
+
kind: 'text',
|
|
130
|
+
id: 'cloudflare.account-id',
|
|
131
|
+
label: 'Cloudflare account id',
|
|
132
|
+
hint: 'Required for validation, token creation, discovery, and provisioning. Discovery can list accounts when the token permits it.',
|
|
133
|
+
placeholder: 'account id',
|
|
134
|
+
defaultValue: config?.accountId ?? '',
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
kind: 'text',
|
|
138
|
+
id: 'cloudflare.zone-id',
|
|
139
|
+
label: 'Zone id',
|
|
140
|
+
hint: 'Optional unless DNS or Access hostname automation is selected. Use the zone that owns the chosen hostname.',
|
|
141
|
+
placeholder: 'optional zone id',
|
|
142
|
+
defaultValue: config?.zoneId ?? '',
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
kind: 'text',
|
|
146
|
+
id: 'cloudflare.zone-name',
|
|
147
|
+
label: 'Zone name',
|
|
148
|
+
hint: 'Optional unless DNS or Access hostname automation is selected. Example: example.com for goodvibes.example.com.',
|
|
149
|
+
placeholder: 'example.com',
|
|
150
|
+
defaultValue: config?.zoneName ?? '',
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
kind: 'text',
|
|
154
|
+
id: 'cloudflare.daemon-base-url',
|
|
155
|
+
label: 'Daemon base URL for Worker calls',
|
|
156
|
+
hint: 'The URL Cloudflare Worker/Tunnel uses to reach the GoodVibes daemon. 127.0.0.1 only works for local verification, not remote Cloudflare calls.',
|
|
157
|
+
placeholder: 'https://daemon.example.com or http://127.0.0.1:3421',
|
|
158
|
+
defaultValue: defaultDaemonBaseUrl,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
kind: 'text',
|
|
162
|
+
id: 'cloudflare.daemon-hostname',
|
|
163
|
+
label: 'Daemon hostname',
|
|
164
|
+
hint: 'Optional hostname used by Tunnel, Access, and DNS automation. Leave blank to infer it from daemon base URL when possible.',
|
|
165
|
+
placeholder: 'daemon.example.com',
|
|
166
|
+
defaultValue: config?.daemonHostname ?? '',
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
kind: 'text',
|
|
170
|
+
id: 'cloudflare.worker-name',
|
|
171
|
+
label: 'Worker name',
|
|
172
|
+
hint: 'Cloudflare Worker script name to create or update.',
|
|
173
|
+
placeholder: 'goodvibes-batch-worker',
|
|
174
|
+
defaultValue: config?.workerName || 'goodvibes-batch-worker',
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
kind: 'text',
|
|
178
|
+
id: 'cloudflare.worker-subdomain',
|
|
179
|
+
label: 'workers.dev subdomain',
|
|
180
|
+
hint: 'Optional workers.dev subdomain. If unavailable or blank, a custom route can still be used later.',
|
|
181
|
+
placeholder: 'account-subdomain',
|
|
182
|
+
defaultValue: config?.workerSubdomain ?? '',
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
kind: 'text',
|
|
186
|
+
id: 'cloudflare.worker-hostname',
|
|
187
|
+
label: 'Worker custom hostname',
|
|
188
|
+
hint: 'Optional custom hostname for Worker DNS/route automation.',
|
|
189
|
+
placeholder: 'goodvibes.example.com',
|
|
190
|
+
defaultValue: config?.workerHostname ?? '',
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
kind: 'text',
|
|
194
|
+
id: 'cloudflare.worker-base-url',
|
|
195
|
+
label: 'Worker base URL',
|
|
196
|
+
hint: 'Optional existing Worker URL. Provisioning fills this when it can infer the workers.dev URL.',
|
|
197
|
+
placeholder: 'https://goodvibes-batch-worker.account.workers.dev',
|
|
198
|
+
defaultValue: config?.workerBaseUrl ?? '',
|
|
199
|
+
},
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
if (components.queues) {
|
|
203
|
+
fields.push(
|
|
204
|
+
{
|
|
205
|
+
kind: 'text',
|
|
206
|
+
id: 'cloudflare.queue-name',
|
|
207
|
+
label: 'Queue name',
|
|
208
|
+
hint: 'Cloudflare Queue used for GoodVibes batch job signals.',
|
|
209
|
+
placeholder: 'goodvibes-batch',
|
|
210
|
+
defaultValue: config?.queueName || 'goodvibes-batch',
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
kind: 'text',
|
|
214
|
+
id: 'cloudflare.dead-letter-queue-name',
|
|
215
|
+
label: 'Dead-letter queue name',
|
|
216
|
+
hint: 'Cloudflare dead-letter queue for failed batch signals.',
|
|
217
|
+
placeholder: 'goodvibes-batch-dlq',
|
|
218
|
+
defaultValue: config?.deadLetterQueueName || 'goodvibes-batch-dlq',
|
|
219
|
+
},
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (components.zeroTrustTunnel) {
|
|
224
|
+
fields.push(
|
|
225
|
+
{
|
|
226
|
+
kind: 'text',
|
|
227
|
+
id: 'cloudflare.tunnel-name',
|
|
228
|
+
label: 'Tunnel name',
|
|
229
|
+
hint: 'Cloudflare Tunnel name to create or reuse.',
|
|
230
|
+
placeholder: 'goodvibes-daemon',
|
|
231
|
+
defaultValue: config?.tunnelName || 'goodvibes-daemon',
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
kind: 'text',
|
|
235
|
+
id: 'cloudflare.tunnel-id',
|
|
236
|
+
label: 'Existing tunnel id',
|
|
237
|
+
hint: 'Optional existing Tunnel id. Leave blank to let provisioning create or discover one by name.',
|
|
238
|
+
placeholder: 'optional tunnel id',
|
|
239
|
+
defaultValue: config?.tunnelId ?? '',
|
|
240
|
+
},
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (components.kv) {
|
|
245
|
+
fields.push(
|
|
246
|
+
{
|
|
247
|
+
kind: 'text',
|
|
248
|
+
id: 'cloudflare.kv-namespace-name',
|
|
249
|
+
label: 'KV namespace name',
|
|
250
|
+
hint: 'Cloudflare KV namespace for optional batch/runtime state.',
|
|
251
|
+
placeholder: 'goodvibes-runtime',
|
|
252
|
+
defaultValue: config?.kvNamespaceName || 'goodvibes-runtime',
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
kind: 'text',
|
|
256
|
+
id: 'cloudflare.kv-namespace-id',
|
|
257
|
+
label: 'Existing KV namespace id',
|
|
258
|
+
hint: 'Optional existing KV namespace id.',
|
|
259
|
+
placeholder: 'optional KV id',
|
|
260
|
+
defaultValue: config?.kvNamespaceId ?? '',
|
|
261
|
+
},
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (components.durableObjects) {
|
|
266
|
+
fields.push(
|
|
267
|
+
{
|
|
268
|
+
kind: 'text',
|
|
269
|
+
id: 'cloudflare.do-namespace-name',
|
|
270
|
+
label: 'Durable Object namespace name',
|
|
271
|
+
hint: 'Durable Object namespace expected by the Worker when this advanced component is selected.',
|
|
272
|
+
placeholder: 'GoodVibesCoordinator',
|
|
273
|
+
defaultValue: config?.durableObjectNamespaceName || 'GoodVibesCoordinator',
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
kind: 'text',
|
|
277
|
+
id: 'cloudflare.do-namespace-id',
|
|
278
|
+
label: 'Durable Object namespace id',
|
|
279
|
+
hint: 'Optional existing Durable Object namespace id.',
|
|
280
|
+
placeholder: 'optional Durable Object id',
|
|
281
|
+
defaultValue: config?.durableObjectNamespaceId ?? '',
|
|
282
|
+
},
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (components.r2) {
|
|
287
|
+
fields.push({
|
|
288
|
+
kind: 'text',
|
|
289
|
+
id: 'cloudflare.r2-bucket-name',
|
|
290
|
+
label: 'R2 bucket name',
|
|
291
|
+
hint: 'R2 Standard bucket for optional batch artifacts.',
|
|
292
|
+
placeholder: 'goodvibes-artifacts',
|
|
293
|
+
defaultValue: config?.r2BucketName || 'goodvibes-artifacts',
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (components.secretsStore) {
|
|
298
|
+
fields.push(
|
|
299
|
+
{
|
|
300
|
+
kind: 'text',
|
|
301
|
+
id: 'cloudflare.secrets-store-name',
|
|
302
|
+
label: 'Secrets Store name',
|
|
303
|
+
hint: 'Cloudflare Secrets Store name for optional account-level secrets.',
|
|
304
|
+
placeholder: 'goodvibes',
|
|
305
|
+
defaultValue: config?.secretsStoreName || 'goodvibes',
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
kind: 'text',
|
|
309
|
+
id: 'cloudflare.secrets-store-id',
|
|
310
|
+
label: 'Secrets Store id',
|
|
311
|
+
hint: 'Optional existing Cloudflare Secrets Store id.',
|
|
312
|
+
placeholder: 'optional Secrets Store id',
|
|
313
|
+
defaultValue: config?.secretsStoreId ?? '',
|
|
314
|
+
},
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
fields.push(
|
|
319
|
+
{
|
|
320
|
+
kind: 'text',
|
|
321
|
+
id: 'cloudflare.worker-cron',
|
|
322
|
+
label: 'Worker cron',
|
|
323
|
+
hint: 'Cron trigger installed on the Worker for batch scheduler ticks. Leave blank to skip cron automation.',
|
|
324
|
+
placeholder: '*/5 * * * *',
|
|
325
|
+
defaultValue: config?.workerCron || '*/5 * * * *',
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
kind: 'text',
|
|
329
|
+
id: 'cloudflare.max-queue-ops-per-day',
|
|
330
|
+
label: 'Max queue ops per day',
|
|
331
|
+
hint: 'Free-tier queue-operation budget used for local warnings.',
|
|
332
|
+
placeholder: '10000',
|
|
333
|
+
defaultValue: String(config?.maxQueueOpsPerDay ?? 10000),
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
kind: 'radio',
|
|
337
|
+
id: 'cloudflare.provision-on-apply',
|
|
338
|
+
label: 'Provision Cloudflare on final apply',
|
|
339
|
+
hint: 'If yes, final Apply calls SDK daemon routes to create/update resources and verify them. Failure is reported as a warning; settings still save.',
|
|
340
|
+
options: CLOUDFLARE_PROVISION_OPTIONS,
|
|
341
|
+
defaultValue: 'no',
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
kind: 'status',
|
|
345
|
+
id: 'cloudflare.action-status',
|
|
346
|
+
label: 'Last Cloudflare daemon action',
|
|
347
|
+
hint: resultMessage,
|
|
348
|
+
defaultValue: resultMessage,
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
kind: 'action',
|
|
352
|
+
id: 'cloudflare.requirements',
|
|
353
|
+
action: 'cloudflare-token-requirements',
|
|
354
|
+
label: 'Show token requirements',
|
|
355
|
+
hint: 'Calls the daemon SDK route and displays the required token permissions for the selected components.',
|
|
356
|
+
defaultValue: 'Action',
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
kind: 'action',
|
|
360
|
+
id: 'cloudflare.create-token',
|
|
361
|
+
action: 'cloudflare-create-operational-token',
|
|
362
|
+
label: 'Create operational token from bootstrap token',
|
|
363
|
+
hint: 'Uses a pasted or environment bootstrap token once. The SDK stores the generated operational token as a goodvibes:// secret.',
|
|
364
|
+
defaultValue: 'Action',
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
kind: 'action',
|
|
368
|
+
id: 'cloudflare.discover',
|
|
369
|
+
action: 'cloudflare-discover',
|
|
370
|
+
label: 'Discover accounts, zones, and resources',
|
|
371
|
+
hint: 'Calls the daemon SDK route using the configured or supplied token and summarizes discoverable Cloudflare resources.',
|
|
372
|
+
defaultValue: 'Action',
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
kind: 'action',
|
|
376
|
+
id: 'cloudflare.validate',
|
|
377
|
+
action: 'cloudflare-validate',
|
|
378
|
+
label: 'Validate Cloudflare token',
|
|
379
|
+
hint: 'Calls the daemon SDK route to validate account access with the configured or supplied token.',
|
|
380
|
+
defaultValue: 'Action',
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
kind: 'action',
|
|
384
|
+
id: 'cloudflare.provision',
|
|
385
|
+
action: 'cloudflare-provision',
|
|
386
|
+
label: 'Provision and verify now',
|
|
387
|
+
hint: 'Calls the daemon SDK route immediately with the current wizard values. This creates/updates selected Cloudflare resources.',
|
|
388
|
+
defaultValue: 'Action',
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
kind: 'action',
|
|
392
|
+
id: 'cloudflare.verify',
|
|
393
|
+
action: 'cloudflare-verify',
|
|
394
|
+
label: 'Verify Worker now',
|
|
395
|
+
hint: 'Calls the daemon SDK route to verify Worker health and daemon batch proxy readiness.',
|
|
396
|
+
defaultValue: 'Action',
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
kind: 'action',
|
|
400
|
+
id: 'cloudflare.disable',
|
|
401
|
+
action: 'cloudflare-disable',
|
|
402
|
+
label: 'Disable Cloudflare integration',
|
|
403
|
+
hint: 'Calls the daemon SDK route to disable local Cloudflare usage and return the batch queue backend to local behavior.',
|
|
404
|
+
defaultValue: 'Action',
|
|
405
|
+
},
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
id: 'cloudflare',
|
|
411
|
+
title: 'Cloudflare batch setup',
|
|
412
|
+
shortLabel: 'Cloudflare',
|
|
413
|
+
description: 'Optional Cloudflare Workers and Queues setup. GoodVibes uses local immediate daemon behavior unless Cloudflare and a batch mode are enabled.',
|
|
414
|
+
summaryTitle: 'Cloudflare summary',
|
|
415
|
+
summaryLines: [
|
|
416
|
+
`Enabled: ${enabled ? 'yes' : 'no'}`,
|
|
417
|
+
`Batch mode: ${enabled ? batchMode : 'off'}`,
|
|
418
|
+
`Components: ${enabled ? componentCount : 0} selected`,
|
|
419
|
+
`Token setup: ${enabled ? setupSource : 'not used'}`,
|
|
420
|
+
`Provision on final apply: ${enabled ? controller.getStringFieldValue('cloudflare.provision-on-apply', 'no') : 'no'}`,
|
|
421
|
+
],
|
|
422
|
+
fields,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function cloudflareComponentHint(component: string): string {
|
|
427
|
+
switch (component) {
|
|
428
|
+
case 'workers':
|
|
429
|
+
return 'Deploy the GoodVibes Worker used for batch signals and optional public ingress.';
|
|
430
|
+
case 'queues':
|
|
431
|
+
return 'Create Cloudflare Queue and dead-letter queue resources for batch job signals.';
|
|
432
|
+
case 'zeroTrustTunnel':
|
|
433
|
+
return 'Create or reuse a Cloudflare Tunnel so Cloudflare can reach the daemon through a controlled path.';
|
|
434
|
+
case 'zeroTrustAccess':
|
|
435
|
+
return 'Configure Cloudflare Access application/service-token protection around the daemon hostname.';
|
|
436
|
+
case 'dns':
|
|
437
|
+
return 'Create DNS records for selected custom hostnames. Requires a Cloudflare-managed zone.';
|
|
438
|
+
case 'kv':
|
|
439
|
+
return 'Create or reuse KV for optional Worker-side state.';
|
|
440
|
+
case 'durableObjects':
|
|
441
|
+
return 'Use Durable Objects for advanced Worker coordination where supported.';
|
|
442
|
+
case 'secretsStore':
|
|
443
|
+
return 'Create or reuse a Cloudflare Secrets Store for optional account-level secrets.';
|
|
444
|
+
case 'r2':
|
|
445
|
+
return 'Create or reuse an R2 Standard bucket for optional batch artifacts.';
|
|
446
|
+
default:
|
|
447
|
+
return 'Optional Cloudflare component.';
|
|
448
|
+
}
|
|
449
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CLOUDFLARE_COMPONENT_IDS,
|
|
3
|
+
CLOUDFLARE_COMPONENT_LABELS,
|
|
4
|
+
DEFAULT_CLOUDFLARE_COMPONENT_SELECTION,
|
|
5
|
+
type CloudflareBatchMode,
|
|
6
|
+
type CloudflareComponent,
|
|
7
|
+
type CloudflareComponentSelection,
|
|
8
|
+
type CloudflareProvisionRequest,
|
|
9
|
+
} from '../../runtime/cloudflare-control-plane.ts';
|
|
10
|
+
import { buildGoodVibesSecretRef, normalizeText } from './onboarding-wizard-helpers.ts';
|
|
11
|
+
import type { OnboardingWizardController } from './onboarding-wizard.ts';
|
|
12
|
+
import type { OnboardingWizardRadioOption } from './onboarding-wizard-types.ts';
|
|
13
|
+
|
|
14
|
+
export const CLOUDFLARE_SETUP_SOURCE_OPTIONS: readonly OnboardingWizardRadioOption[] = [
|
|
15
|
+
{
|
|
16
|
+
id: 'save-only',
|
|
17
|
+
label: 'Save settings only',
|
|
18
|
+
hint: 'Persist Cloudflare fields without passing a token to the daemon. Provision later from the Cloudflare command or settings.',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'bootstrap-token',
|
|
22
|
+
label: 'Paste temporary bootstrap token',
|
|
23
|
+
hint: 'Use a short-lived Cloudflare token once. The SDK creates and stores a narrower GoodVibes operational token.',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'bootstrap-env',
|
|
27
|
+
label: 'Read bootstrap token from environment',
|
|
28
|
+
hint: 'Read a temporary token from an environment variable and pass it once to the SDK. The value is not stored.',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'operational-token',
|
|
32
|
+
label: 'Paste final operational token',
|
|
33
|
+
hint: 'Use a token you already created. The SDK can store it as a GoodVibes secret during provisioning.',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'operational-env',
|
|
37
|
+
label: 'Use final token from environment',
|
|
38
|
+
hint: 'Use an environment-backed token reference such as CLOUDFLARE_API_TOKEN.',
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
export const CLOUDFLARE_BATCH_MODE_OPTIONS: readonly OnboardingWizardRadioOption[] = [
|
|
43
|
+
{
|
|
44
|
+
id: 'off',
|
|
45
|
+
label: 'Off',
|
|
46
|
+
hint: 'Keep daemon requests on the immediate local path. Cloudflare resource settings can still be saved.',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'explicit',
|
|
50
|
+
label: 'Explicit batch only',
|
|
51
|
+
hint: 'Only requests explicitly marked for batch execution use the configured batch path.',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'eligible-by-default',
|
|
55
|
+
label: 'Eligible requests batch by default',
|
|
56
|
+
hint: 'Batch-capable daemon work can use the configured batch path unless the caller opts out.',
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
export const CLOUDFLARE_YES_NO_OPTIONS: readonly OnboardingWizardRadioOption[] = [
|
|
61
|
+
{ id: 'yes', label: 'Yes', hint: 'Enable this behavior.' },
|
|
62
|
+
{ id: 'no', label: 'No', hint: 'Leave this behavior off.' },
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
export const CLOUDFLARE_PROVISION_OPTIONS: readonly OnboardingWizardRadioOption[] = [
|
|
66
|
+
{
|
|
67
|
+
id: 'no',
|
|
68
|
+
label: 'No, save configuration only',
|
|
69
|
+
hint: 'Final Apply saves the settings. Use the Cloudflare command or this wizard later to provision resources.',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'yes',
|
|
73
|
+
label: 'Yes, create or update Cloudflare resources',
|
|
74
|
+
hint: 'Final Apply asks the daemon SDK route to create/update selected Cloudflare resources and verify the Worker when possible.',
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
export type CloudflareSetupSource =
|
|
79
|
+
| 'save-only'
|
|
80
|
+
| 'bootstrap-token'
|
|
81
|
+
| 'bootstrap-env'
|
|
82
|
+
| 'operational-token'
|
|
83
|
+
| 'operational-env';
|
|
84
|
+
|
|
85
|
+
export function cloudflareComponentFieldId(component: CloudflareComponent): string {
|
|
86
|
+
return `cloudflare.component.${component}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function cloudflareComponentLabel(component: CloudflareComponent): string {
|
|
90
|
+
return CLOUDFLARE_COMPONENT_LABELS[component];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function isCloudflareConfigured(controller: OnboardingWizardController): boolean {
|
|
94
|
+
const config = controller.runtimeSnapshot?.config.cloudflare;
|
|
95
|
+
if (!config) return false;
|
|
96
|
+
return config.enabled
|
|
97
|
+
|| normalizeText(config.accountId).length > 0
|
|
98
|
+
|| normalizeText(config.apiTokenRef).length > 0
|
|
99
|
+
|| normalizeText(config.workerBaseUrl).length > 0
|
|
100
|
+
|| normalizeText(config.workerName).length > 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function shouldShowCloudflareStep(controller: OnboardingWizardController): boolean {
|
|
104
|
+
return controller.isCapabilitySelected('cloudflare-batch') || isCloudflareConfigured(controller);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function getCloudflareSetupSource(controller: OnboardingWizardController): CloudflareSetupSource {
|
|
108
|
+
const configuredTokenRef = controller.runtimeSnapshot?.config.cloudflare.apiTokenRef ?? '';
|
|
109
|
+
const defaultValue = configuredTokenRef.startsWith('goodvibes://secrets/env/') ? 'operational-env' : 'save-only';
|
|
110
|
+
const value = controller.getStringFieldValue('cloudflare.setup-source', defaultValue);
|
|
111
|
+
if (
|
|
112
|
+
value === 'bootstrap-token'
|
|
113
|
+
|| value === 'bootstrap-env'
|
|
114
|
+
|| value === 'operational-token'
|
|
115
|
+
|| value === 'operational-env'
|
|
116
|
+
|| value === 'save-only'
|
|
117
|
+
) {
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
return 'save-only';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function getCloudflareComponentSelection(controller: OnboardingWizardController): Record<CloudflareComponent, boolean> {
|
|
124
|
+
const selected: Record<CloudflareComponent, boolean> = { ...DEFAULT_CLOUDFLARE_COMPONENT_SELECTION };
|
|
125
|
+
const configured = controller.runtimeSnapshot?.config.cloudflare;
|
|
126
|
+
for (const component of CLOUDFLARE_COMPONENT_IDS) {
|
|
127
|
+
const fallback = configured?.enabled === true
|
|
128
|
+
? component === 'workers' || component === 'queues'
|
|
129
|
+
: DEFAULT_CLOUDFLARE_COMPONENT_SELECTION[component];
|
|
130
|
+
selected[component] = controller.getBooleanFieldValue(cloudflareComponentFieldId(component), fallback);
|
|
131
|
+
}
|
|
132
|
+
return selected;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function getSelectedCloudflareComponents(controller: OnboardingWizardController): CloudflareComponentSelection {
|
|
136
|
+
return getCloudflareComponentSelection(controller);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function getCloudflareBatchMode(controller: OnboardingWizardController): CloudflareBatchMode {
|
|
140
|
+
const value = controller.getStringFieldValue('cloudflare.batch-mode', controller.runtimeSnapshot?.config.batch.mode ?? 'off');
|
|
141
|
+
return value === 'explicit' || value === 'eligible-by-default' ? value : 'off';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function buildCloudflareApiTokenRef(envName: string): string {
|
|
145
|
+
const normalized = normalizeText(envName) || 'CLOUDFLARE_API_TOKEN';
|
|
146
|
+
return `goodvibes://secrets/env/${encodeURIComponent(normalized)}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function buildCloudflareProvisionRequest(controller: OnboardingWizardController, options: {
|
|
150
|
+
readonly includeTransientSecrets?: boolean;
|
|
151
|
+
} = {}): CloudflareProvisionRequest {
|
|
152
|
+
const components = getCloudflareComponentSelection(controller);
|
|
153
|
+
const setupSource = getCloudflareSetupSource(controller);
|
|
154
|
+
const accountId = controller.getStringFieldValue('cloudflare.account-id', controller.runtimeSnapshot?.config.cloudflare.accountId ?? '');
|
|
155
|
+
const zoneId = controller.getStringFieldValue('cloudflare.zone-id', controller.runtimeSnapshot?.config.cloudflare.zoneId ?? '');
|
|
156
|
+
const zoneName = controller.getStringFieldValue('cloudflare.zone-name', controller.runtimeSnapshot?.config.cloudflare.zoneName ?? '');
|
|
157
|
+
const apiToken = setupSource === 'operational-token' && options.includeTransientSecrets
|
|
158
|
+
? controller.getStringFieldValue('cloudflare.operational-token', '')
|
|
159
|
+
: '';
|
|
160
|
+
const apiTokenRef = setupSource === 'operational-env'
|
|
161
|
+
? buildCloudflareApiTokenRef(controller.getStringFieldValue('cloudflare.operational-env-name', 'CLOUDFLARE_API_TOKEN'))
|
|
162
|
+
: controller.runtimeSnapshot?.config.cloudflare.apiTokenRef ?? '';
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
components,
|
|
166
|
+
...(accountId ? { accountId } : {}),
|
|
167
|
+
...(zoneId ? { zoneId } : {}),
|
|
168
|
+
...(zoneName ? { zoneName } : {}),
|
|
169
|
+
...(apiToken ? { apiToken, storeApiToken: true } : {}),
|
|
170
|
+
...(!apiToken && apiTokenRef ? { apiTokenRef } : {}),
|
|
171
|
+
workerName: controller.getStringFieldValue('cloudflare.worker-name', controller.runtimeSnapshot?.config.cloudflare.workerName ?? 'goodvibes-batch-worker'),
|
|
172
|
+
workerSubdomain: controller.getStringFieldValue('cloudflare.worker-subdomain', controller.runtimeSnapshot?.config.cloudflare.workerSubdomain ?? ''),
|
|
173
|
+
workerHostname: controller.getStringFieldValue('cloudflare.worker-hostname', controller.runtimeSnapshot?.config.cloudflare.workerHostname ?? ''),
|
|
174
|
+
workerBaseUrl: controller.getStringFieldValue('cloudflare.worker-base-url', controller.runtimeSnapshot?.config.cloudflare.workerBaseUrl ?? ''),
|
|
175
|
+
daemonBaseUrl: controller.getStringFieldValue('cloudflare.daemon-base-url', controller.runtimeSnapshot?.config.cloudflare.daemonBaseUrl ?? ''),
|
|
176
|
+
daemonHostname: controller.getStringFieldValue('cloudflare.daemon-hostname', controller.runtimeSnapshot?.config.cloudflare.daemonHostname ?? ''),
|
|
177
|
+
queueName: controller.getStringFieldValue('cloudflare.queue-name', controller.runtimeSnapshot?.config.cloudflare.queueName ?? 'goodvibes-batch'),
|
|
178
|
+
deadLetterQueueName: controller.getStringFieldValue('cloudflare.dead-letter-queue-name', controller.runtimeSnapshot?.config.cloudflare.deadLetterQueueName ?? 'goodvibes-batch-dlq'),
|
|
179
|
+
tunnelName: controller.getStringFieldValue('cloudflare.tunnel-name', controller.runtimeSnapshot?.config.cloudflare.tunnelName ?? 'goodvibes-daemon'),
|
|
180
|
+
tunnelId: controller.getStringFieldValue('cloudflare.tunnel-id', controller.runtimeSnapshot?.config.cloudflare.tunnelId ?? ''),
|
|
181
|
+
kvNamespaceName: controller.getStringFieldValue('cloudflare.kv-namespace-name', controller.runtimeSnapshot?.config.cloudflare.kvNamespaceName ?? 'goodvibes-runtime'),
|
|
182
|
+
kvNamespaceId: controller.getStringFieldValue('cloudflare.kv-namespace-id', controller.runtimeSnapshot?.config.cloudflare.kvNamespaceId ?? ''),
|
|
183
|
+
durableObjectNamespaceName: controller.getStringFieldValue('cloudflare.do-namespace-name', controller.runtimeSnapshot?.config.cloudflare.durableObjectNamespaceName ?? 'GoodVibesCoordinator'),
|
|
184
|
+
durableObjectNamespaceId: controller.getStringFieldValue('cloudflare.do-namespace-id', controller.runtimeSnapshot?.config.cloudflare.durableObjectNamespaceId ?? ''),
|
|
185
|
+
r2BucketName: controller.getStringFieldValue('cloudflare.r2-bucket-name', controller.runtimeSnapshot?.config.cloudflare.r2BucketName ?? 'goodvibes-artifacts'),
|
|
186
|
+
secretsStoreName: controller.getStringFieldValue('cloudflare.secrets-store-name', controller.runtimeSnapshot?.config.cloudflare.secretsStoreName ?? 'goodvibes'),
|
|
187
|
+
secretsStoreId: controller.getStringFieldValue('cloudflare.secrets-store-id', controller.runtimeSnapshot?.config.cloudflare.secretsStoreId ?? ''),
|
|
188
|
+
workerCron: controller.getStringFieldValue('cloudflare.worker-cron', controller.runtimeSnapshot?.config.cloudflare.workerCron ?? '*/5 * * * *'),
|
|
189
|
+
enableWorkersDev: true,
|
|
190
|
+
queueJobPayloads: false,
|
|
191
|
+
persistConfig: true,
|
|
192
|
+
verify: true,
|
|
193
|
+
batchMode: getCloudflareBatchMode(controller),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function buildCloudflareOperationalTokenRef(): string {
|
|
198
|
+
return buildGoodVibesSecretRef('CLOUDFLARE_API_TOKEN');
|
|
199
|
+
}
|
|
@@ -6,6 +6,7 @@ export const STEP_ORDER: readonly OnboardingWizardStepId[] = [
|
|
|
6
6
|
'network',
|
|
7
7
|
'access',
|
|
8
8
|
'external-services',
|
|
9
|
+
'cloudflare',
|
|
9
10
|
'provider-access',
|
|
10
11
|
'default-model',
|
|
11
12
|
'experience',
|
|
@@ -43,6 +44,12 @@ export const DEFAULT_CAPABILITIES: readonly OnboardingStep1CapabilityItem[] = [
|
|
|
43
44
|
selected: false,
|
|
44
45
|
detail: 'Enable setup screens for Slack, Discord, Telegram, Teams, Matrix, and other app surfaces you choose.',
|
|
45
46
|
},
|
|
47
|
+
{
|
|
48
|
+
id: 'cloudflare-batch',
|
|
49
|
+
label: 'Use Cloudflare for batch or remote daemon work',
|
|
50
|
+
selected: false,
|
|
51
|
+
detail: 'Optionally configure Cloudflare Workers and Queues for explicit or eligible background batch jobs. Immediate local daemon behavior stays the default unless enabled.',
|
|
52
|
+
},
|
|
46
53
|
];
|
|
47
54
|
|
|
48
55
|
export const REASONING_OPTIONS: readonly OnboardingWizardRadioOption[] = [
|