@pellux/goodvibes-tui 0.19.32 → 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.
Files changed (39) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +4 -2
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/audio/spoken-turn-model-routing.ts +117 -0
  6. package/src/input/command-registry.ts +2 -0
  7. package/src/input/commands/cloudflare-runtime.ts +343 -0
  8. package/src/input/commands/tts-runtime.ts +288 -7
  9. package/src/input/commands.ts +2 -0
  10. package/src/input/feed-context-factory.ts +1 -0
  11. package/src/input/handler-feed.ts +6 -0
  12. package/src/input/handler-modal-routes.ts +23 -10
  13. package/src/input/handler-modal-token-routes.ts +9 -0
  14. package/src/input/handler-onboarding-cloudflare.ts +391 -0
  15. package/src/input/handler-onboarding.ts +33 -0
  16. package/src/input/handler-picker-routes.ts +1 -1
  17. package/src/input/handler.ts +4 -1
  18. package/src/input/model-picker-types.ts +125 -0
  19. package/src/input/model-picker.ts +144 -134
  20. package/src/input/onboarding/onboarding-wizard-apply.ts +81 -0
  21. package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +449 -0
  22. package/src/input/onboarding/onboarding-wizard-cloudflare.ts +199 -0
  23. package/src/input/onboarding/onboarding-wizard-constants.ts +7 -0
  24. package/src/input/onboarding/onboarding-wizard-steps.ts +6 -6
  25. package/src/input/onboarding/onboarding-wizard-types.ts +8 -0
  26. package/src/input/settings-modal-types.ts +2 -1
  27. package/src/input/settings-modal.ts +30 -8
  28. package/src/main.ts +12 -1
  29. package/src/renderer/buffer.ts +40 -2
  30. package/src/renderer/compositor.ts +25 -17
  31. package/src/renderer/model-picker-overlay.ts +70 -0
  32. package/src/renderer/settings-modal-helpers.ts +1 -0
  33. package/src/runtime/bootstrap-command-parts.ts +4 -0
  34. package/src/runtime/cloudflare-control-plane.ts +328 -0
  35. package/src/runtime/onboarding/derivation.ts +25 -0
  36. package/src/runtime/onboarding/snapshot.ts +2 -0
  37. package/src/runtime/onboarding/types.ts +5 -1
  38. package/src/shell/ui-openers.ts +21 -2
  39. package/src/version.ts +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,29 @@ All notable changes to GoodVibes TUI.
4
4
 
5
5
  ---
6
6
 
7
+ ## [0.19.34] — 2026-04-26
8
+
9
+ ### Added
10
+ - Cloudflare onboarding support for optional Workers/Queues batch and remote daemon/control-plane provisioning.
11
+ - `/cloudflare` (`/cf`) runtime command for status, token requirements, bootstrap-token creation, discovery, validation, provisioning, verification, disable, and setup entry.
12
+ - Cloudflare settings category in `/settings`, including SDK `cloudflare.*` and `batch.*` keys.
13
+ - Documentation for Cloudflare token setup, daemon routes, onboarding fields, batch modes, and secret references.
14
+
15
+ ### Changed
16
+ - Updated `@pellux/goodvibes-sdk` to `0.25.10` for SDK-owned Cloudflare daemon routes, token creation, discovery, validation, provisioning, and config persistence.
17
+ - Onboarding Cloudflare provisioning failures are reported as warnings so local daemon usage is not blocked when optional Cloudflare setup needs follow-up.
18
+
19
+ ## [0.19.33] — 2026-04-26
20
+
21
+ ### Changed
22
+ - `/config-tts` now opens an interactive TTS configuration modal instead of only printing provider and voice lists.
23
+ - `/config-tts providers` and `/config-tts voices [provider]` open selectable provider/voice pickers in the TUI and persist the selected SDK config keys.
24
+ - `/config-tts llm` opens the model picker for a separate `/tts` response-model override; `/tts` still defaults to the current chat provider/model when no override is configured.
25
+
26
+ ### Fixed
27
+ - `/tts` now honors configured `tts.llmProvider`/`tts.llmModel` for that spoken turn without changing the main chat model or global provider settings.
28
+ - Changing the TTS provider clears the provider-specific voice selection so a stale voice id is not reused against a different provider.
29
+
7
30
  ## [0.19.32] — 2026-04-25
8
31
 
9
32
  ### Added
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![CI](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml/badge.svg)](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
- [![Version](https://img.shields.io/badge/version-0.19.32-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
5
+ [![Version](https://img.shields.io/badge/version-0.19.34-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
6
6
 
7
7
  A terminal-native AI coding, operations, automation, knowledge, and integration console with a typed runtime, omnichannel surfaces, structured memory/knowledge, and a raw ANSI renderer.
8
8
 
@@ -989,6 +989,7 @@ These systems are first-class runtime families.
989
989
  - `web-search`: provider-backed search with verbosity control, evidence shaping, optional source fetching, and normalized results across DuckDuckGo, SearXNG, Brave, Exa, Firecrawl, Tavily, and Perplexity
990
990
  - `artifacts`: durable file/object storage for markdown, text, JSON, CSV, spreadsheets, PDFs, images, audio, video, and generated outputs, with metadata, content access, and delivery reuse
991
991
  - `voice`: provider-backed TTS/STT/realtime negotiation with OpenAI, ElevenLabs, Deepgram, Google, Microsoft, and Vydra; the TUI also supports live `/tts` spoken output through streaming TTS providers and local `mpv`/`ffplay` playback
992
+ - `cloudflare`: optional Workers/Queues batch execution and remote control-plane provisioning through SDK daemon routes; local immediate daemon behavior remains the default until Cloudflare and a batch mode are enabled
992
993
  - `multimodal`: unified image/audio/video/document analysis with packet building and optional knowledge write-back
993
994
 
994
995
  These surfaces are exposed through the daemon/API as well as the TUI panels and commands, giving future web and companion clients the same backend runtime.
@@ -1269,7 +1270,8 @@ Those pieces cover conversation-noise routing, panel-health/performance budgets,
1269
1270
  | `/notify [action]` | `/ntf` | Manage webhook notifications (ntfy.sh): add, remove, list, clear, test |
1270
1271
  | `/voice [action]` | — | Review optional voice posture and export/inspect voice bundles |
1271
1272
  | `/tts <prompt>` | — | Submit a normal prompt and play the assistant response through live TTS |
1272
- | `/config-tts [action]` | `/tts-config` | Configure TTS provider, voice, and optional spoken-turn LLM overrides |
1273
+ | `/config-tts [action]` | `/tts-config` | Open TTS configuration for provider, voice, and optional `/tts` response-model override |
1274
+ | `/cloudflare [action]` | `/cf` | Configure optional Cloudflare Workers/Queues batch and remote control-plane provisioning |
1273
1275
  | `/diff [target]` | `/d` | Show unified diff: session, head, working, staged, or a git ref |
1274
1276
  | `/mcp [tools]` | — | List connected MCP servers and their tools |
1275
1277
  | `/help [command]` | `/h`, `/?` | Show available commands and keyboard shortcuts |
@@ -3,7 +3,7 @@
3
3
  "product": {
4
4
  "id": "goodvibes",
5
5
  "surface": "operator",
6
- "version": "0.25.8"
6
+ "version": "0.25.10"
7
7
  },
8
8
  "auth": {
9
9
  "modes": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-tui",
3
- "version": "0.19.32",
3
+ "version": "0.19.34",
4
4
  "description": "Terminal-native GoodVibes product for coding, operations, automation, knowledge, channels, and daemon-backed control-plane workflows.",
5
5
  "type": "module",
6
6
  "main": "src/main.ts",
@@ -91,7 +91,7 @@
91
91
  "@anthropic-ai/vertex-sdk": "^0.16.0",
92
92
  "@ast-grep/napi": "^0.42.0",
93
93
  "@aws/bedrock-token-generator": "^1.1.0",
94
- "@pellux/goodvibes-sdk": "0.25.8",
94
+ "@pellux/goodvibes-sdk": "0.25.10",
95
95
  "bash-language-server": "^5.6.0",
96
96
  "fuse.js": "^7.1.0",
97
97
  "graphql": "^16.13.2",
@@ -0,0 +1,117 @@
1
+ import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config/manager';
2
+ import type { ModelDefinition, ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers/registry';
3
+ import type { ContentPart } from '@pellux/goodvibes-sdk/platform/providers/interface';
4
+ import type { Orchestrator, OrchestratorUserInputOptions } from '../core/orchestrator.ts';
5
+
6
+ const SPOKEN_TURN_SOURCE = 'tts';
7
+
8
+ type RunTurn = (
9
+ text: string,
10
+ content?: ContentPart[],
11
+ options?: OrchestratorUserInputOptions,
12
+ ) => Promise<void>;
13
+
14
+ type PatchableOrchestrator = {
15
+ runTurn?: RunTurn;
16
+ setCoreServices: (services: { providerRegistry?: ProviderRegistry }) => void;
17
+ };
18
+
19
+ export interface SpokenTurnModelRoutingOptions {
20
+ readonly orchestrator: Orchestrator;
21
+ readonly providerRegistry: ProviderRegistry;
22
+ readonly configManager: Pick<ConfigManager, 'get'>;
23
+ readonly notify?: (message: string) => void;
24
+ }
25
+
26
+ export function createSpokenTurnInputOptions(): OrchestratorUserInputOptions {
27
+ return {
28
+ origin: {
29
+ source: SPOKEN_TURN_SOURCE,
30
+ surface: 'tui',
31
+ metadata: { spokenOutput: true },
32
+ },
33
+ };
34
+ }
35
+
36
+ export function attachSpokenTurnModelRouting(options: SpokenTurnModelRoutingOptions): () => void {
37
+ const target = options.orchestrator as unknown as PatchableOrchestrator;
38
+ const originalRunTurn = target.runTurn?.bind(options.orchestrator);
39
+ if (!originalRunTurn) return () => {};
40
+
41
+ target.runTurn = async (text, content, inputOptions) => {
42
+ if (!isSpokenTurn(inputOptions)) {
43
+ await originalRunTurn(text, content, inputOptions);
44
+ return;
45
+ }
46
+
47
+ const override = resolveSpokenTurnModelOverride({
48
+ providerRegistry: options.providerRegistry,
49
+ configManager: options.configManager,
50
+ notify: options.notify,
51
+ });
52
+ if (!override) {
53
+ await originalRunTurn(text, content, inputOptions);
54
+ return;
55
+ }
56
+
57
+ const routedRegistry = createRoutedProviderRegistry(options.providerRegistry, override);
58
+ target.setCoreServices({ providerRegistry: routedRegistry });
59
+ try {
60
+ await originalRunTurn(text, content, inputOptions);
61
+ } finally {
62
+ target.setCoreServices({ providerRegistry: options.providerRegistry });
63
+ }
64
+ };
65
+
66
+ return () => {
67
+ target.runTurn = originalRunTurn;
68
+ target.setCoreServices({ providerRegistry: options.providerRegistry });
69
+ };
70
+ }
71
+
72
+ export function resolveSpokenTurnModelOverride(options: {
73
+ readonly providerRegistry: Pick<ProviderRegistry, 'listModels' | 'getCurrentModel'>;
74
+ readonly configManager: Pick<ConfigManager, 'get'>;
75
+ readonly notify?: (message: string) => void;
76
+ }): ModelDefinition | null {
77
+ const modelRef = readConfigString(options.configManager, 'tts.llmModel');
78
+ if (!modelRef) return null;
79
+
80
+ const providerId = readConfigString(options.configManager, 'tts.llmProvider');
81
+ const current = options.providerRegistry.getCurrentModel();
82
+ const model = options.providerRegistry.listModels().find((candidate) => {
83
+ const refMatches = candidate.registryKey === modelRef || candidate.id === modelRef;
84
+ const providerMatches = !providerId || candidate.provider === providerId;
85
+ return refMatches && providerMatches;
86
+ });
87
+
88
+ if (!model) {
89
+ options.notify?.(`[TTS] Configured TTS LLM '${modelRef}' was not found; using current chat model.`);
90
+ return null;
91
+ }
92
+ if (model.selectable === false) {
93
+ options.notify?.(`[TTS] Configured TTS LLM '${modelRef}' is not selectable; using current chat model.`);
94
+ return null;
95
+ }
96
+ if ((model.registryKey ?? model.id) === (current.registryKey ?? current.id)) return null;
97
+ return model;
98
+ }
99
+
100
+ function isSpokenTurn(options: OrchestratorUserInputOptions | undefined): boolean {
101
+ return options?.origin?.source === SPOKEN_TURN_SOURCE
102
+ || options?.origin?.metadata?.['spokenOutput'] === true;
103
+ }
104
+
105
+ function createRoutedProviderRegistry(providerRegistry: ProviderRegistry, model: ModelDefinition): ProviderRegistry {
106
+ return new Proxy(providerRegistry, {
107
+ get(target, prop, receiver) {
108
+ if (prop === 'getCurrentModel') return () => model;
109
+ const value = Reflect.get(target, prop, receiver);
110
+ return typeof value === 'function' ? value.bind(target) : value;
111
+ },
112
+ });
113
+ }
114
+
115
+ function readConfigString(configManager: Pick<ConfigManager, 'get'>, key: 'tts.llmProvider' | 'tts.llmModel'): string {
116
+ return String(configManager.get(key) ?? '').trim();
117
+ }
@@ -79,6 +79,8 @@ export interface CommandShellUiOpeners {
79
79
  reloadSystemPrompt?: () => string;
80
80
  openOnboardingWizard?: (modeOrOptions?: OnboardingWizardMode | OpenOnboardingWizardOptions) => void;
81
81
  openModelPicker?: () => void;
82
+ openModelPickerWithTarget?: (target: import('./model-picker.ts').ModelPickerTarget) => boolean;
83
+ openProviderModelPickerWithTarget?: (target: import('./model-picker.ts').ModelPickerTarget) => boolean;
82
84
  openProviderPicker?: () => void;
83
85
  openContextInspector?: () => void;
84
86
  openBookmarkModal?: () => void;
@@ -0,0 +1,343 @@
1
+ import {
2
+ CLOUDFLARE_COMPONENT_IDS,
3
+ CLOUDFLARE_COMPONENT_LABELS,
4
+ DEFAULT_CLOUDFLARE_COMPONENT_SELECTION,
5
+ CloudflareDaemonRouteError,
6
+ createCloudflareDaemonClient,
7
+ type CloudflareComponent,
8
+ type CloudflareComponentSelection,
9
+ type CloudflareDaemonClient,
10
+ type CloudflareProvisionStep,
11
+ } from '../../runtime/cloudflare-control-plane.ts';
12
+ import type { CommandContext, CommandRegistry } from '../command-registry.ts';
13
+ import { requireShellPaths } from './runtime-services.ts';
14
+
15
+ interface ParsedCloudflareArgs {
16
+ readonly positional: readonly string[];
17
+ readonly flags: ReadonlyMap<string, readonly string[]>;
18
+ }
19
+
20
+ export function registerCloudflareRuntimeCommands(registry: CommandRegistry): void {
21
+ registry.register({
22
+ name: 'cloudflare',
23
+ aliases: ['cf'],
24
+ description: 'Inspect and manage optional Cloudflare batch/control-plane integration through daemon SDK routes',
25
+ usage: '[status|setup|requirements|create-token|discover|validate|provision|verify|disable] [flags]',
26
+ async handler(args, ctx) {
27
+ const subcommand = (args[0] ?? 'status').toLowerCase();
28
+ const parsed = parseCloudflareArgs(args.slice(1));
29
+ if (subcommand === 'setup' || subcommand === 'onboarding') {
30
+ ctx.openOnboardingWizard?.({ mode: 'edit', reset: true });
31
+ ctx.print('Opening onboarding wizard. Select the Cloudflare batch capability to configure Cloudflare.');
32
+ return;
33
+ }
34
+
35
+ let client: CloudflareDaemonClient;
36
+ try {
37
+ client = createCloudflareClient(ctx);
38
+ } catch (error) {
39
+ ctx.print(`Cloudflare command unavailable: ${formatCloudflareError(error)}`);
40
+ return;
41
+ }
42
+
43
+ try {
44
+ if (subcommand === 'status' || subcommand === 'show') {
45
+ const status = await client.status();
46
+ ctx.print([
47
+ 'Cloudflare Status',
48
+ ` enabled: ${status.enabled ? 'yes' : 'no'}`,
49
+ ` ready: ${status.ready ? 'yes' : 'no'}`,
50
+ ` account: ${status.config.accountId || '(not set)'}`,
51
+ ` token ref: ${status.config.apiTokenRef || '(CLOUDFLARE_API_TOKEN fallback)'}`,
52
+ ` worker: ${status.config.workerName || '(not set)'}`,
53
+ ` worker URL: ${status.config.workerBaseUrl || '(not set)'}`,
54
+ ` queue: ${status.config.queueName || '(not set)'}`,
55
+ ` DLQ: ${status.config.deadLetterQueueName || '(not set)'}`,
56
+ ` batch mode: ${String(ctx.platform.configManager.get('batch.mode') ?? 'off')}`,
57
+ ` queue backend: ${String(ctx.platform.configManager.get('batch.queueBackend') ?? 'local')}`,
58
+ ...status.warnings.map((warning) => ` warning: ${warning}`),
59
+ ].join('\n'));
60
+ return;
61
+ }
62
+
63
+ if (subcommand === 'requirements') {
64
+ const result = await client.tokenRequirements({
65
+ components: componentsFromArgs(parsed),
66
+ includeBootstrap: true,
67
+ });
68
+ ctx.print([
69
+ 'Cloudflare Token Requirements',
70
+ ` components: ${formatComponents(result.components)}`,
71
+ ' permissions:',
72
+ ...result.permissions.map((permission) => ` ${permission.scope}: ${permission.permission} (${permission.component}) - ${permission.reason}`),
73
+ ...(result.bootstrapToken.instructions.length > 0
74
+ ? [' bootstrap token:', ...result.bootstrapToken.instructions.map((line) => ` ${line}`)]
75
+ : []),
76
+ ].join('\n'));
77
+ return;
78
+ }
79
+
80
+ if (subcommand === 'create-token') {
81
+ const bootstrapToken = getFlag(parsed, 'bootstrap-token') || readTokenEnv(parsed, 'bootstrap-env');
82
+ if (!bootstrapToken) {
83
+ ctx.print('Usage: /cloudflare create-token --account <account-id> --bootstrap-token <token> or --bootstrap-env <env-name>');
84
+ return;
85
+ }
86
+ const result = await client.createOperationalToken({
87
+ components: componentsFromArgs(parsed),
88
+ ...optionalString('accountId', getFlag(parsed, 'account') || getFlag(parsed, 'account-id')),
89
+ ...optionalString('zoneId', getFlag(parsed, 'zone-id')),
90
+ ...optionalString('zoneName', getFlag(parsed, 'zone') || getFlag(parsed, 'zone-name')),
91
+ bootstrapToken,
92
+ storeApiToken: true,
93
+ persistConfig: true,
94
+ });
95
+ ctx.print([
96
+ 'Cloudflare Operational Token Created',
97
+ ` token: ${result.tokenName}${result.tokenId ? ` (${result.tokenId})` : ''}`,
98
+ ` account: ${result.accountId}`,
99
+ ` zone: ${result.zoneId || '(none)'}`,
100
+ ` stored ref: ${result.apiTokenRef ?? '(not stored)'}`,
101
+ ' revoke or expire the temporary bootstrap token after validation.',
102
+ ].join('\n'));
103
+ return;
104
+ }
105
+
106
+ if (subcommand === 'discover') {
107
+ const result = await client.discover({
108
+ ...cloudflareAuthInput(parsed),
109
+ components: componentsFromArgs(parsed),
110
+ ...optionalString('zoneId', getFlag(parsed, 'zone-id')),
111
+ ...optionalString('zoneName', getFlag(parsed, 'zone') || getFlag(parsed, 'zone-name')),
112
+ includeResources: !hasFlag(parsed, 'fast'),
113
+ });
114
+ ctx.print([
115
+ 'Cloudflare Discovery',
116
+ ` token source: ${result.tokenSource}`,
117
+ ` accounts: ${result.accounts.length}`,
118
+ ...result.accounts.slice(0, 12).map((account) => ` account ${account.id}: ${account.name}`),
119
+ ` zones: ${result.zones.length}`,
120
+ ...result.zones.slice(0, 12).map((zone) => ` zone ${zone.id}: ${zone.name}${zone.status ? ` (${zone.status})` : ''}`),
121
+ ` worker subdomain: ${result.workerSubdomain || '(not detected)'}`,
122
+ ` queues: ${result.queues?.length ?? 0}`,
123
+ ` KV namespaces: ${result.kvNamespaces?.length ?? 0}`,
124
+ ` R2 buckets: ${result.r2Buckets?.length ?? 0}`,
125
+ ...result.warnings.map((warning) => ` warning: ${warning}`),
126
+ ].join('\n'));
127
+ return;
128
+ }
129
+
130
+ if (subcommand === 'validate') {
131
+ const result = await client.validate(cloudflareAuthInput(parsed));
132
+ ctx.print([
133
+ 'Cloudflare Token Validation',
134
+ ` ok: ${result.ok ? 'yes' : 'no'}`,
135
+ ` token source: ${result.tokenSource}`,
136
+ result.account ? ` account: ${result.account.name} (${result.account.id})` : ' account: not resolved',
137
+ ].join('\n'));
138
+ return;
139
+ }
140
+
141
+ if (subcommand === 'provision') {
142
+ const result = await client.provision({
143
+ ...cloudflareAuthInput(parsed),
144
+ components: componentsFromArgs(parsed),
145
+ ...optionalString('accountId', getFlag(parsed, 'account') || getFlag(parsed, 'account-id')),
146
+ ...optionalString('zoneId', getFlag(parsed, 'zone-id')),
147
+ ...optionalString('zoneName', getFlag(parsed, 'zone') || getFlag(parsed, 'zone-name')),
148
+ ...optionalString('daemonBaseUrl', getFlag(parsed, 'daemon-url')),
149
+ ...optionalString('daemonHostname', getFlag(parsed, 'daemon-hostname')),
150
+ ...optionalString('workerName', getFlag(parsed, 'worker-name')),
151
+ ...optionalString('workerSubdomain', getFlag(parsed, 'worker-subdomain')),
152
+ ...optionalString('workerHostname', getFlag(parsed, 'worker-hostname')),
153
+ ...optionalString('workerBaseUrl', getFlag(parsed, 'worker-url')),
154
+ ...optionalString('queueName', getFlag(parsed, 'queue') || getFlag(parsed, 'queue-name')),
155
+ ...optionalString('deadLetterQueueName', getFlag(parsed, 'dlq') || getFlag(parsed, 'dead-letter-queue')),
156
+ ...optionalBatchMode(readBatchMode(parsed)),
157
+ persistConfig: true,
158
+ verify: !hasFlag(parsed, 'no-verify'),
159
+ storeApiToken: !hasFlag(parsed, 'no-store-token'),
160
+ enableWorkersDev: !hasFlag(parsed, 'no-workers-dev'),
161
+ });
162
+ ctx.print([
163
+ 'Cloudflare Provisioning',
164
+ ` ok: ${result.ok ? 'yes' : 'no'}`,
165
+ ...(result.worker ? [` worker: ${result.worker.name}${result.worker.baseUrl ? ` at ${result.worker.baseUrl}` : ''}`] : []),
166
+ ...(result.queues ? [` queues: ${result.queues.queueName}; DLQ ${result.queues.deadLetterQueueName}`] : []),
167
+ ...formatProvisionSteps(result.steps),
168
+ ].join('\n'));
169
+ return;
170
+ }
171
+
172
+ if (subcommand === 'verify') {
173
+ const result = await client.verify({
174
+ ...optionalString('workerBaseUrl', getFlag(parsed, 'worker-url')),
175
+ ...optionalString('workerClientToken', getFlag(parsed, 'worker-token')),
176
+ ...optionalString('workerClientTokenRef', getFlag(parsed, 'worker-token-ref')),
177
+ });
178
+ ctx.print([
179
+ 'Cloudflare Verification',
180
+ ` ok: ${result.ok ? 'yes' : 'no'}`,
181
+ ` worker health: ${result.workerHealth.ok ? 'ok' : 'failed'} (HTTP ${result.workerHealth.status})${result.workerHealth.error ? ` - ${result.workerHealth.error}` : ''}`,
182
+ ...(result.daemonBatchProxy ? [` daemon batch proxy: ${result.daemonBatchProxy.ok ? 'ok' : 'failed'} (HTTP ${result.daemonBatchProxy.status})${result.daemonBatchProxy.error ? ` - ${result.daemonBatchProxy.error}` : ''}`] : []),
183
+ ].join('\n'));
184
+ return;
185
+ }
186
+
187
+ if (subcommand === 'disable') {
188
+ const result = await client.disable({
189
+ ...cloudflareAuthInput(parsed),
190
+ ...optionalString('workerName', getFlag(parsed, 'worker-name')),
191
+ disableWorkerSubdomain: hasFlag(parsed, 'disable-worker-subdomain'),
192
+ disableCron: !hasFlag(parsed, 'keep-cron'),
193
+ persistConfig: true,
194
+ });
195
+ ctx.print([
196
+ 'Cloudflare Disabled',
197
+ ` ok: ${result.ok ? 'yes' : 'no'}`,
198
+ ...formatProvisionSteps(result.steps),
199
+ ].join('\n'));
200
+ return;
201
+ }
202
+
203
+ ctx.print('Usage: /cloudflare [status|setup|requirements|create-token|discover|validate|provision|verify|disable] [flags]');
204
+ } catch (error) {
205
+ ctx.print(`Cloudflare ${subcommand} failed: ${formatCloudflareError(error)}`);
206
+ }
207
+ },
208
+ });
209
+ }
210
+
211
+ function createCloudflareClient(ctx: CommandContext): CloudflareDaemonClient {
212
+ const shellPaths = requireShellPaths(ctx);
213
+ return createCloudflareDaemonClient({
214
+ configManager: ctx.platform.configManager,
215
+ homeDirectory: shellPaths.homeDirectory,
216
+ });
217
+ }
218
+
219
+ function parseCloudflareArgs(args: readonly string[]): ParsedCloudflareArgs {
220
+ const positional: string[] = [];
221
+ const flags = new Map<string, string[]>();
222
+ for (let index = 0; index < args.length; index += 1) {
223
+ const arg = args[index]!;
224
+ if (!arg.startsWith('--')) {
225
+ positional.push(arg);
226
+ continue;
227
+ }
228
+ const raw = arg.slice(2);
229
+ const equalsIndex = raw.indexOf('=');
230
+ if (equalsIndex >= 0) {
231
+ const key = raw.slice(0, equalsIndex);
232
+ const value = raw.slice(equalsIndex + 1);
233
+ flags.set(key, [...(flags.get(key) ?? []), value]);
234
+ continue;
235
+ }
236
+ const next = args[index + 1];
237
+ if (next && !next.startsWith('--')) {
238
+ flags.set(raw, [...(flags.get(raw) ?? []), next]);
239
+ index += 1;
240
+ } else {
241
+ flags.set(raw, [...(flags.get(raw) ?? []), 'true']);
242
+ }
243
+ }
244
+ return { positional, flags };
245
+ }
246
+
247
+ function hasFlag(args: ParsedCloudflareArgs, key: string): boolean {
248
+ return args.flags.has(key);
249
+ }
250
+
251
+ function getFlag(args: ParsedCloudflareArgs, key: string): string {
252
+ const values = args.flags.get(key);
253
+ const value = values?.[values.length - 1] ?? '';
254
+ return value === 'true' ? '' : value.trim();
255
+ }
256
+
257
+ function getFlagValues(args: ParsedCloudflareArgs, key: string): readonly string[] {
258
+ return args.flags.get(key)?.map((value) => value.trim()).filter(Boolean) ?? [];
259
+ }
260
+
261
+ function componentsFromArgs(args: ParsedCloudflareArgs): Record<CloudflareComponent, boolean> {
262
+ const components: Record<CloudflareComponent, boolean> = { ...DEFAULT_CLOUDFLARE_COMPONENT_SELECTION };
263
+ if (hasFlag(args, 'all') || hasFlag(args, 'advanced')) {
264
+ for (const component of CLOUDFLARE_COMPONENT_IDS) components[component] = true;
265
+ }
266
+ for (const raw of [...args.positional, ...getFlagValues(args, 'component')]) {
267
+ const normalized = normalizeComponent(raw);
268
+ if (normalized) components[normalized] = true;
269
+ }
270
+ for (const raw of getFlagValues(args, 'no-component')) {
271
+ const normalized = normalizeComponent(raw);
272
+ if (normalized) components[normalized] = false;
273
+ }
274
+ return components;
275
+ }
276
+
277
+ function normalizeComponent(value: string): CloudflareComponent | null {
278
+ const normalized = value.trim().toLowerCase().replace(/[-_]/g, '');
279
+ for (const component of CLOUDFLARE_COMPONENT_IDS) {
280
+ if (component.toLowerCase() === normalized) return component;
281
+ }
282
+ if (normalized === 'workerscript' || normalized === 'worker') return 'workers';
283
+ if (normalized === 'queue') return 'queues';
284
+ if (normalized === 'tunnel') return 'zeroTrustTunnel';
285
+ if (normalized === 'access') return 'zeroTrustAccess';
286
+ if (normalized === 'domain' || normalized === 'hostname') return 'dns';
287
+ if (normalized === 'do' || normalized === 'durableobject') return 'durableObjects';
288
+ if (normalized === 'secret' || normalized === 'secrets') return 'secretsStore';
289
+ return null;
290
+ }
291
+
292
+ function formatComponents(components: CloudflareComponentSelection): string {
293
+ const selected = CLOUDFLARE_COMPONENT_IDS
294
+ .filter((component) => components[component] === true)
295
+ .map((component) => CLOUDFLARE_COMPONENT_LABELS[component]);
296
+ return selected.length > 0 ? selected.join(', ') : 'none';
297
+ }
298
+
299
+ function cloudflareAuthInput(args: ParsedCloudflareArgs): {
300
+ readonly accountId?: string;
301
+ readonly apiToken?: string;
302
+ readonly apiTokenRef?: string;
303
+ } {
304
+ const token = getFlag(args, 'token') || readTokenEnv(args, 'token-env');
305
+ const tokenRef = getFlag(args, 'token-ref');
306
+ return {
307
+ ...optionalString('accountId', getFlag(args, 'account') || getFlag(args, 'account-id')),
308
+ ...(token ? { apiToken: token } : tokenRef ? { apiTokenRef: tokenRef } : {}),
309
+ };
310
+ }
311
+
312
+ function readTokenEnv(args: ParsedCloudflareArgs, key: string): string {
313
+ const envName = getFlag(args, key);
314
+ if (!envName) return '';
315
+ return process.env[envName] ?? '';
316
+ }
317
+
318
+ function readBatchMode(args: ParsedCloudflareArgs): 'off' | 'explicit' | 'eligible-by-default' | undefined {
319
+ const value = getFlag(args, 'batch-mode');
320
+ if (value === 'off' || value === 'explicit' || value === 'eligible-by-default') return value;
321
+ return undefined;
322
+ }
323
+
324
+ function optionalString<K extends string>(key: K, value: string): Partial<Record<K, string>> {
325
+ return value.trim().length > 0 ? { [key]: value.trim() } as Partial<Record<K, string>> : {};
326
+ }
327
+
328
+ function optionalBatchMode(value: ReturnType<typeof readBatchMode>): { readonly batchMode?: 'off' | 'explicit' | 'eligible-by-default' } {
329
+ return value ? { batchMode: value } : {};
330
+ }
331
+
332
+ function formatProvisionSteps(steps: readonly CloudflareProvisionStep[]): string[] {
333
+ return steps.length > 0
334
+ ? steps.map((step) => ` ${step.status}: ${step.name}${step.message ? ` - ${step.message}` : ''}`)
335
+ : [' no steps returned'];
336
+ }
337
+
338
+ function formatCloudflareError(error: unknown): string {
339
+ if (error instanceof CloudflareDaemonRouteError) {
340
+ return `${error.message} (HTTP ${error.status}, ${error.code})`;
341
+ }
342
+ return error instanceof Error ? error.message : String(error);
343
+ }