@pellux/goodvibes-tui 1.13.0 → 1.14.0
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 +101 -0
- package/README.md +6 -6
- package/bin/goodvibes +6 -0
- package/bin/goodvibes-daemon +6 -0
- package/bin/launcher-support.js +34 -1
- package/docs/foundation-artifacts/operator-contract.json +8076 -2047
- package/package.json +15 -11
- package/scripts/postinstall.js +73 -48
- package/src/cli/bundle-command.ts +5 -5
- package/src/cli/completion.ts +1 -1
- package/src/cli/completions/generate.ts +15 -3
- package/src/cli/doctor.ts +385 -0
- package/src/cli/entrypoint.ts +72 -1
- package/src/cli/help.ts +29 -9
- package/src/cli/hooks-command.ts +74 -0
- package/src/cli/hooks-report.ts +167 -0
- package/src/cli/index.ts +3 -0
- package/src/cli/management.ts +13 -1
- package/src/cli/parser.ts +10 -2
- package/src/cli/plugin-bundles-command.ts +228 -0
- package/src/cli/plugin-command.ts +179 -0
- package/src/cli/service-posture.ts +10 -0
- package/src/cli/status.ts +275 -2
- package/src/cli/tui-startup.ts +119 -1
- package/src/cli/types.ts +3 -1
- package/src/config/index.ts +16 -0
- package/src/config/tui-extension-settings.ts +169 -0
- package/src/core/approval-alert.ts +15 -2
- package/src/core/compaction-receipt.ts +127 -0
- package/src/core/model-routing-chip.ts +82 -0
- package/src/core/permission-mode.ts +89 -0
- package/src/core/rewind-receipt.ts +83 -0
- package/src/core/rewind-turn-anchors.ts +160 -0
- package/src/core/scriptable-statusline.ts +141 -0
- package/src/core/session-auto-titler.ts +110 -0
- package/src/core/stream-event-wiring.ts +20 -0
- package/src/core/system-message-router.ts +1 -1
- package/src/core/terminal-notifier.ts +102 -0
- package/src/core/turn-event-wiring.ts +80 -1
- package/src/input/command-reference.ts +96 -0
- package/src/input/command-registry.ts +25 -0
- package/src/input/commands/channel-pairing.ts +245 -0
- package/src/input/commands/channel-runtime.ts +152 -6
- package/src/input/commands/checkin-runtime.ts +89 -0
- package/src/input/commands/checkpoint-runtime.ts +7 -179
- package/src/input/commands/ci-runtime.ts +207 -0
- package/src/input/commands/cost-attribution-format.ts +52 -0
- package/src/input/commands/cost-runtime.ts +44 -4
- package/src/input/commands/editor-runtime.ts +22 -0
- package/src/input/commands/flags-runtime.ts +328 -0
- package/src/input/commands/health-metrics-format.ts +67 -0
- package/src/input/commands/health-runtime.ts +55 -1
- package/src/input/commands/memory.ts +23 -0
- package/src/input/commands/operator-rpc.ts +95 -0
- package/src/input/commands/palette-runtime.ts +22 -0
- package/src/input/commands/permissions-provenance.ts +264 -0
- package/src/input/commands/permissions-runtime.ts +21 -0
- package/src/input/commands/planning-runtime.ts +28 -1
- package/src/input/commands/platform-access-runtime.ts +2 -97
- package/src/input/commands/principals-runtime.ts +212 -0
- package/src/input/commands/product-runtime.ts +43 -3
- package/src/input/commands/recall-files-config.ts +54 -0
- package/src/input/commands/recall-files-git.ts +57 -0
- package/src/input/commands/recall-files.ts +185 -0
- package/src/input/commands/recall-query.ts +8 -3
- package/src/input/commands/recall-review.ts +2 -2
- package/src/input/commands/recall-shared.ts +13 -0
- package/src/input/commands/relay-runtime.ts +86 -0
- package/src/input/commands/review-runtime.ts +160 -0
- package/src/input/commands/rewind-runtime.ts +362 -0
- package/src/input/commands/schedule-nl.ts +147 -0
- package/src/input/commands/schedule-runtime.ts +29 -10
- package/src/input/commands/secret-runtime.ts +55 -0
- package/src/input/commands/session-content.ts +19 -8
- package/src/input/commands/session-workflow.ts +10 -0
- package/src/input/commands/update-runtime.ts +363 -0
- package/src/input/commands/workstream-attempt-dependents.ts +103 -0
- package/src/input/commands/workstream-attempts.ts +244 -0
- package/src/input/commands/workstream-runtime.ts +164 -26
- package/src/input/commands/worktree-runtime.ts +56 -2
- package/src/input/commands.ts +135 -80
- package/src/input/composer-capture.ts +67 -0
- package/src/input/composer-editor.ts +134 -0
- package/src/input/concealed-input.ts +94 -0
- package/src/input/exec-env-scrub-config.ts +84 -0
- package/src/input/feature-unit-layout.ts +142 -0
- package/src/input/feed-context-factory.ts +2 -0
- package/src/input/handler-feed-routes.ts +17 -0
- package/src/input/handler-feed.ts +3 -0
- package/src/input/handler-modal-routes.ts +5 -2
- package/src/input/handler-shortcuts.ts +37 -0
- package/src/input/handler.ts +24 -2
- package/src/input/keybindings.ts +11 -2
- package/src/input/onboarding/onboarding-feature-units.ts +536 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +40 -6
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +324 -629
- package/src/input/onboarding/onboarding-wizard-steps.ts +5 -5
- package/src/input/onboarding/onboarding-wizard-types.ts +6 -0
- package/src/input/relay-settings-descriptions.ts +35 -0
- package/src/input/sandbox-exec-config.ts +107 -0
- package/src/input/settings-modal-adjustment.ts +4 -7
- package/src/input/settings-modal-data.ts +178 -8
- package/src/input/settings-modal-mutations.ts +41 -13
- package/src/input/settings-modal-types.ts +42 -5
- package/src/input/settings-modal.ts +67 -15
- package/src/input/submission-router.ts +4 -1
- package/src/input/worktree-setup-config.ts +93 -0
- package/src/main.ts +43 -43
- package/src/panels/builtin/development.ts +14 -0
- package/src/panels/diff-review-model.ts +189 -0
- package/src/panels/diff-review-panel.ts +395 -0
- package/src/panels/fleet-panel-format.ts +114 -2
- package/src/panels/fleet-panel.ts +65 -78
- package/src/panels/fleet-read-model.ts +91 -11
- package/src/panels/fleet-stop.ts +19 -4
- package/src/panels/modals/memory-modal.ts +11 -2
- package/src/permissions/hunk-selection.ts +86 -5
- package/src/permissions/prompt.ts +108 -7
- package/src/permissions/sandbox-exec-gate.ts +162 -0
- package/src/renderer/settings-modal-helpers.ts +21 -1
- package/src/renderer/settings-modal.ts +70 -44
- package/src/renderer/shell-surface.ts +23 -1
- package/src/renderer/system-message.ts +4 -0
- package/src/renderer/tool-result-summary.ts +95 -5
- package/src/renderer/ui-factory.ts +22 -0
- package/src/runtime/bootstrap-command-context.ts +15 -1
- package/src/runtime/bootstrap-command-parts.ts +14 -2
- package/src/runtime/bootstrap-core.ts +38 -38
- package/src/runtime/bootstrap-notifier-sync.ts +41 -0
- package/src/runtime/bootstrap-shell.ts +8 -0
- package/src/runtime/bootstrap.ts +21 -23
- package/src/runtime/context-accounting-source.ts +103 -0
- package/src/runtime/conversation-rewind-port.ts +130 -0
- package/src/runtime/fleet-needs-input-push.ts +70 -0
- package/src/runtime/install-self-check-startup.ts +36 -0
- package/src/runtime/install-self-check.ts +149 -0
- package/src/runtime/onboarding/fast-path.ts +93 -0
- package/src/runtime/process-lifecycle.ts +32 -24
- package/src/runtime/relay-reachability-bridge.ts +53 -0
- package/src/runtime/release-artifacts.ts +79 -0
- package/src/runtime/services.ts +38 -37
- package/src/runtime/trust/workspace-registration.ts +184 -0
- package/src/runtime/trust/workspace-trust.ts +202 -0
- package/src/runtime/ui-services.ts +2 -1
- package/src/runtime/update-check.ts +113 -0
- package/src/runtime/workspace-checkpointing.ts +32 -0
- package/src/runtime/workstream-attempts-validation.ts +77 -0
- package/src/runtime/workstream-draft-edits.ts +95 -0
- package/src/runtime/workstream-draft-store.ts +101 -0
- package/src/runtime/workstream-draft-types.ts +42 -0
- package/src/runtime/workstream-services.ts +70 -39
- package/src/shell/blocking-input.ts +29 -0
- package/src/shell/ui-openers.ts +65 -0
- package/src/verification/verification-ledger.ts +56 -2
- package/src/version.ts +1 -1
- package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +0 -130
- package/src/runtime/render-scheduler.ts +0 -90
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import type { UiRuntimeEvents } from '@/runtime/index.ts';
|
|
1
|
+
import type { UiRuntimeEvents, RuntimeEventBus } from '@/runtime/index.ts';
|
|
2
2
|
import { buildPersistedSessionContext, persistConversation } from '@/runtime/index.ts';
|
|
3
|
+
import { buildCompactionReceiptBlock } from './compaction-receipt.ts';
|
|
4
|
+
import { persistTurnAnchors, recordTurnAnchor, summarizeTurnLabel } from './rewind-turn-anchors.ts';
|
|
3
5
|
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
4
6
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
5
7
|
import type { HookDispatcher, HookPhase, HookCategory, HookEventPath } from '@pellux/goodvibes-sdk/platform/hooks';
|
|
@@ -32,6 +34,10 @@ interface TurnProviderRegistry {
|
|
|
32
34
|
/** Minimal config manager surface required by turn-event wiring. */
|
|
33
35
|
interface TurnConfigManager {
|
|
34
36
|
get(key: string): unknown;
|
|
37
|
+
/** Subscribe to a config-key change; returns an unsubscribe. Used to keep the
|
|
38
|
+
* footer permission-mode pill live without polling. Optional so bare test
|
|
39
|
+
* doubles that only implement get() still satisfy the type. */
|
|
40
|
+
subscribe?(key: string, cb: (...args: unknown[]) => void): () => void;
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
/** Minimal system message router surface required by turn-event wiring. */
|
|
@@ -43,6 +49,13 @@ interface TurnSystemMessageRouter {
|
|
|
43
49
|
|
|
44
50
|
export interface WireTurnEventHandlersOptions {
|
|
45
51
|
readonly events: UiRuntimeEvents;
|
|
52
|
+
/**
|
|
53
|
+
* Raw runtime event bus. Needed for domains NOT surfaced by the grouped
|
|
54
|
+
* UiRuntimeEvents feed — specifically the 'compaction' domain, whose
|
|
55
|
+
* mandatory COMPACTION_RECEIPT the transcript renders as a distinct block.
|
|
56
|
+
* Optional so headless/test callers can omit it.
|
|
57
|
+
*/
|
|
58
|
+
readonly runtimeBus?: RuntimeEventBus | null;
|
|
46
59
|
readonly conversation: ConversationManager;
|
|
47
60
|
readonly runtime: { sessionId: string; model: string; provider: string };
|
|
48
61
|
readonly orchestrator: TurnOrchestrator;
|
|
@@ -74,6 +87,14 @@ export interface WireTurnEventHandlersOptions {
|
|
|
74
87
|
* to fall back to).
|
|
75
88
|
*/
|
|
76
89
|
readonly focusTracker?: Pick<FocusTracker, 'shouldAlertWhenUnfocused'> | null;
|
|
90
|
+
/**
|
|
91
|
+
* Optional in-terminal (OSC 9) notifier. When present: a turn finishing emits
|
|
92
|
+
* a 'turn-end' notification, and a delegated agent blocking on human input
|
|
93
|
+
* (AGENT_AWAITING_MESSAGE) emits an 'agent-blocked' notification. Each is
|
|
94
|
+
* gated independently by the notifier's own per-signal config + focus rule.
|
|
95
|
+
* Absent in tests/headless.
|
|
96
|
+
*/
|
|
97
|
+
readonly terminalNotifier?: import('./terminal-notifier.ts').TerminalNotifier | null;
|
|
77
98
|
/**
|
|
78
99
|
* Minimal test seam: injectable clock for controlling Date.now() in tests.
|
|
79
100
|
* Defaults to the real Date.now when absent.
|
|
@@ -114,6 +135,7 @@ export function wireTurnEventHandlers(
|
|
|
114
135
|
events, conversation, runtime, configManager, hookDispatcher, orchestrator, providerRegistry,
|
|
115
136
|
workingDir, homeDirectory, sessionManager, gitStatusProvider,
|
|
116
137
|
lastGitInfoRef, buildSessionContinuityHints, render, webhookNotifier, focusTracker,
|
|
138
|
+
terminalNotifier, runtimeBus, systemMessageRouter,
|
|
117
139
|
_clock = Date.now,
|
|
118
140
|
} = options;
|
|
119
141
|
|
|
@@ -158,6 +180,9 @@ export function wireTurnEventHandlers(
|
|
|
158
180
|
const notifyThreshold = readNotifyAfterSeconds((k) => configManager.get(k as Parameters<typeof configManager.get>[0]));
|
|
159
181
|
// stopReason 'empty_response' signals a non-successful completion.
|
|
160
182
|
const taskStatus: LongTaskStatus = evt.stopReason === 'completed' ? 'ok' : 'fail';
|
|
183
|
+
// In-terminal (OSC 9) turn-end notification — fires on its own per-signal
|
|
184
|
+
// config + focus gate, independent of the long-task duration threshold below.
|
|
185
|
+
terminalNotifier?.notify('turn-end', taskStatus === 'ok' ? 'Turn finished' : 'Turn finished with errors');
|
|
161
186
|
maybeNotifyLongTask({
|
|
162
187
|
elapsedMs: turnElapsedMs,
|
|
163
188
|
status: taskStatus,
|
|
@@ -198,6 +223,22 @@ export function wireTurnEventHandlers(
|
|
|
198
223
|
} catch { /* best-effort */ }
|
|
199
224
|
logger.debug('auto-save on turn:complete failed', { error: summarizeError(e) });
|
|
200
225
|
}
|
|
226
|
+
// Record this turn's rewind anchor: pair the turnId (shared with the
|
|
227
|
+
// workspace checkpoint the turn engine snapshots for this same turn) with
|
|
228
|
+
// the live conversation message count, so a later message-anchored /rewind
|
|
229
|
+
// can truncate the conversation to exactly this boundary — the join key
|
|
230
|
+
// between conversation and files rewind (see core/rewind-turn-anchors.ts).
|
|
231
|
+
try {
|
|
232
|
+
recordTurnAnchor(runtime.sessionId, {
|
|
233
|
+
turnId: evt.turnId,
|
|
234
|
+
label: summarizeTurnLabel(conversation.getLastUserMessage()),
|
|
235
|
+
messageCount: conversation.getMessageCount(),
|
|
236
|
+
at: _clock(),
|
|
237
|
+
});
|
|
238
|
+
// Mirror the anchors to the session's sidecar so message-anchored /rewind
|
|
239
|
+
// survives a resume (the in-memory registry alone is process-local).
|
|
240
|
+
persistTurnAnchors(runtime.sessionId, workingDir);
|
|
241
|
+
} catch { /* best-effort; a rewind-anchor miss must never break the turn */ }
|
|
201
242
|
// Auto-compaction is owned by the SDK Orchestrator's post-turn maintenance
|
|
202
243
|
// (handlePostTurnContextMaintenance), which runs on every turn against the
|
|
203
244
|
// resolved context window (getContextWindowForModel) via the SDK's
|
|
@@ -206,6 +247,16 @@ export function wireTurnEventHandlers(
|
|
|
206
247
|
refreshGit();
|
|
207
248
|
}));
|
|
208
249
|
|
|
250
|
+
// In-terminal (OSC 9) agent-blocked notification: a delegated agent parked
|
|
251
|
+
// waiting for a human message (AGENT_AWAITING_MESSAGE) is "blocked on you".
|
|
252
|
+
// Gated by the notifier's own per-signal config + focus rule. PRIVACY: the
|
|
253
|
+
// short agent id only — never task text.
|
|
254
|
+
if (terminalNotifier) {
|
|
255
|
+
unsubs.push(events.agents.on('AGENT_AWAITING_MESSAGE', (payload) => {
|
|
256
|
+
terminalNotifier.notify('agent-blocked', `agent ${payload.agentId.slice(0, 8)} is waiting for your input`);
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
259
|
+
|
|
209
260
|
unsubs.push(events.tools.on('TOOL_SUCCEEDED', () => {
|
|
210
261
|
refreshGit();
|
|
211
262
|
}));
|
|
@@ -249,5 +300,33 @@ export function wireTurnEventHandlers(
|
|
|
249
300
|
}));
|
|
250
301
|
}
|
|
251
302
|
|
|
303
|
+
// Live footer permission-mode pill: re-render whenever the SDK config
|
|
304
|
+
// surface reports permissions.mode changed. Mode changes are made through
|
|
305
|
+
// configManager.set('permissions.mode', ...) (the SDK-owned surface the
|
|
306
|
+
// PermissionManager reads), so this covers our own Shift+Tab / /plan toggles
|
|
307
|
+
// as well as any other in-process writer. No polling.
|
|
308
|
+
if (typeof configManager.subscribe === 'function') {
|
|
309
|
+
unsubs.push(configManager.subscribe('permissions.mode', () => { render(); }));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Post-compaction receipt (never-silent): the SDK emits a mandatory
|
|
313
|
+
// COMPACTION_RECEIPT on the 'compaction' domain after every automatic (and
|
|
314
|
+
// the manual) compaction. That domain is NOT in the grouped UiRuntimeEvents
|
|
315
|
+
// feed, so we subscribe on the raw bus and render the receipt as a distinct
|
|
316
|
+
// [Compaction] block. Automatic compaction previously left no transcript
|
|
317
|
+
// trace; this makes the audit visible.
|
|
318
|
+
if (runtimeBus) {
|
|
319
|
+
unsubs.push(runtimeBus.on('COMPACTION_RECEIPT', (envelope) => {
|
|
320
|
+
try {
|
|
321
|
+
const payload = envelope.payload;
|
|
322
|
+
if (payload.type !== 'COMPACTION_RECEIPT') return;
|
|
323
|
+
systemMessageRouter.high(buildCompactionReceiptBlock(payload));
|
|
324
|
+
render();
|
|
325
|
+
} catch (err) {
|
|
326
|
+
logger.debug('turn-event-wiring: compaction-receipt render error', { error: String(err) });
|
|
327
|
+
}
|
|
328
|
+
}));
|
|
329
|
+
}
|
|
330
|
+
|
|
252
331
|
return { refreshGit, unsubs, transcriptJournal };
|
|
253
332
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* command-reference.ts — pure renderer for the generated slash-command
|
|
3
|
+
* reference (docs/commands-reference.md).
|
|
4
|
+
*
|
|
5
|
+
* The reference is generated from the live command registry (via
|
|
6
|
+
* {@link categorizeBuiltinCommands} in ./commands.ts), never hand-maintained.
|
|
7
|
+
* This module is deliberately side-effect free so both the generator
|
|
8
|
+
* (scripts/project-surfaces.ts) and the drift-check test import the same
|
|
9
|
+
* rendering logic — the committed file and a fresh render must be byte-identical
|
|
10
|
+
* or the gate fails.
|
|
11
|
+
*/
|
|
12
|
+
import type { SlashCommand } from './command-registry.ts';
|
|
13
|
+
|
|
14
|
+
export interface CategorizedCommand {
|
|
15
|
+
readonly command: SlashCommand;
|
|
16
|
+
readonly category: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Escape the pipe and backslash characters that would break a markdown table cell. */
|
|
20
|
+
function escapeCell(text: string): string {
|
|
21
|
+
return text.replace(/\\/g, '\\\\').replace(/\|/g, '\\|').replace(/\r?\n/g, ' ').trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Stable anchor slug for a category heading (GitHub-style). */
|
|
25
|
+
function slug(text: string): string {
|
|
26
|
+
return text
|
|
27
|
+
.toLowerCase()
|
|
28
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
29
|
+
.trim()
|
|
30
|
+
.replace(/\s+/g, '-');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* renderCommandReferenceMarkdown — deterministic markdown for the full command
|
|
35
|
+
* set. Categories are sorted alphabetically and commands alphabetically by
|
|
36
|
+
* primary name within each category, so the output depends only on the command
|
|
37
|
+
* set and their metadata — not on registration order — which keeps the drift
|
|
38
|
+
* check meaningful (a genuinely new/renamed command moves the bytes; a pure
|
|
39
|
+
* reordering does not).
|
|
40
|
+
*/
|
|
41
|
+
export function renderCommandReferenceMarkdown(entries: readonly CategorizedCommand[]): string {
|
|
42
|
+
const byCategory = new Map<string, SlashCommand[]>();
|
|
43
|
+
for (const { command, category } of entries) {
|
|
44
|
+
const list = byCategory.get(category) ?? [];
|
|
45
|
+
list.push(command);
|
|
46
|
+
byCategory.set(category, list);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const categories = Array.from(byCategory.keys()).sort((a, b) => a.localeCompare(b));
|
|
50
|
+
const total = entries.length;
|
|
51
|
+
|
|
52
|
+
const lines: string[] = [];
|
|
53
|
+
lines.push('<!-- GENERATED FILE — DO NOT EDIT BY HAND.');
|
|
54
|
+
lines.push(' Regenerate with `bun run docs:commands`.');
|
|
55
|
+
lines.push(' Source of truth: the slash-command registry (src/input/commands.ts).');
|
|
56
|
+
lines.push(' A drift check (src/test/release-gates/command-reference-gate.test.ts)');
|
|
57
|
+
lines.push(' fails CI if this file is stale. -->');
|
|
58
|
+
lines.push('');
|
|
59
|
+
lines.push('# Command Reference');
|
|
60
|
+
lines.push('');
|
|
61
|
+
lines.push(
|
|
62
|
+
`GoodVibes ships **${total}** built-in slash commands across **${categories.length}** categories. ` +
|
|
63
|
+
'Every command below is generated directly from the command registry, so this list is always ' +
|
|
64
|
+
'complete and current. Type a command in the composer prefixed with `/`, or press `Ctrl+K` (or ' +
|
|
65
|
+
'run `/palette`) to search them all in the command palette.',
|
|
66
|
+
);
|
|
67
|
+
lines.push('');
|
|
68
|
+
lines.push('## Categories');
|
|
69
|
+
lines.push('');
|
|
70
|
+
for (const category of categories) {
|
|
71
|
+
const count = byCategory.get(category)?.length ?? 0;
|
|
72
|
+
lines.push(`- [${escapeCell(category)}](#${slug(category)}) — ${count}`);
|
|
73
|
+
}
|
|
74
|
+
lines.push('');
|
|
75
|
+
|
|
76
|
+
for (const category of categories) {
|
|
77
|
+
const commands = (byCategory.get(category) ?? []).slice().sort((a, b) => a.name.localeCompare(b.name));
|
|
78
|
+
lines.push(`## ${category}`);
|
|
79
|
+
lines.push('');
|
|
80
|
+
lines.push('| Command | Aliases | Usage | Description |');
|
|
81
|
+
lines.push('| --- | --- | --- | --- |');
|
|
82
|
+
for (const cmd of commands) {
|
|
83
|
+
const name = `\`/${escapeCell(cmd.name)}\``;
|
|
84
|
+
const aliases = (cmd.aliases ?? []).length > 0
|
|
85
|
+
? (cmd.aliases ?? []).map((a) => `\`/${escapeCell(a)}\``).join(', ')
|
|
86
|
+
: '—';
|
|
87
|
+
const usageText = cmd.usage ?? cmd.argsHint ?? '';
|
|
88
|
+
const usage = usageText ? `\`${escapeCell(usageText)}\`` : '—';
|
|
89
|
+
const description = cmd.description ? escapeCell(cmd.description) : '—';
|
|
90
|
+
lines.push(`| ${name} | ${aliases} | ${usage} | ${description} |`);
|
|
91
|
+
}
|
|
92
|
+
lines.push('');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return `${lines.join('\n').trimEnd()}\n`;
|
|
96
|
+
}
|
|
@@ -8,6 +8,7 @@ import type { PermissionRequestHandler } from '@pellux/goodvibes-sdk/platform/pe
|
|
|
8
8
|
import type { SelectionItem, SelectionResult, SelectionAction } from './selection-modal.ts';
|
|
9
9
|
import type { FileUndoManager } from '@pellux/goodvibes-sdk/platform/state';
|
|
10
10
|
import type { WorkspaceCheckpointManager } from '@pellux/goodvibes-sdk/platform/workspace';
|
|
11
|
+
import type { GatewayMethodCatalog } from '@pellux/goodvibes-sdk/platform/control-plane';
|
|
11
12
|
import type { PanelManager, PanelDeepLinkTarget } from '../panels/panel-manager.ts';
|
|
12
13
|
import type { KeybindingsManager } from './keybindings.ts';
|
|
13
14
|
import type { OnboardingWizardMode } from './onboarding/onboarding-wizard.ts';
|
|
@@ -18,10 +19,12 @@ import type { HookApi } from '@pellux/goodvibes-sdk/platform/hooks';
|
|
|
18
19
|
import type { McpApi } from '@pellux/goodvibes-sdk/platform/mcp';
|
|
19
20
|
import type { ProviderApi } from '@pellux/goodvibes-sdk/platform/providers';
|
|
20
21
|
import type { OpsApi } from '@/runtime/index.ts';
|
|
22
|
+
import type { FeatureFlagManager } from '@/runtime/index.ts';
|
|
21
23
|
import type { OperatorClient } from '@/runtime/index.ts';
|
|
22
24
|
import type { PeerClient } from '@/runtime/index.ts';
|
|
23
25
|
import type { DirectTransport } from '@/runtime/index.ts';
|
|
24
26
|
import type { VoiceProviderRegistry, VoiceService } from '@pellux/goodvibes-sdk/platform/voice';
|
|
27
|
+
import type { UiPlatformServices } from '../runtime/ui-services.ts';
|
|
25
28
|
import type {
|
|
26
29
|
CommandWorkspaceShellServices,
|
|
27
30
|
} from '@/runtime/index.ts';
|
|
@@ -67,6 +70,8 @@ export interface CommandUiActions {
|
|
|
67
70
|
renderRequest: () => void;
|
|
68
71
|
print: (text: string) => void;
|
|
69
72
|
exit: () => void;
|
|
73
|
+
/** Open the current composer draft in the user's $EDITOR and load back the edited text. */
|
|
74
|
+
openComposerEditor?: () => void;
|
|
70
75
|
submitInput?: (text: string, content?: import('@pellux/goodvibes-sdk/platform/providers').ContentPart[]) => void;
|
|
71
76
|
submitSpokenInput?: (text: string, content?: import('@pellux/goodvibes-sdk/platform/providers').ContentPart[]) => void;
|
|
72
77
|
stopSpokenOutput?: () => void;
|
|
@@ -96,6 +101,12 @@ export interface CommandUiActions {
|
|
|
96
101
|
*/
|
|
97
102
|
completeEmbeddingProviderSelection?: (providerId: string) => void;
|
|
98
103
|
clearScreen?: () => void;
|
|
104
|
+
/**
|
|
105
|
+
* Begin one line of concealed (masked) composer input for a password-like
|
|
106
|
+
* secret. The typed text is masked in the composer and kept out of input
|
|
107
|
+
* history and the transcript; the plaintext reaches only request.onSubmit.
|
|
108
|
+
*/
|
|
109
|
+
beginConcealedInput?: (request: import('./concealed-input.ts').ConcealedInputRequest) => void;
|
|
99
110
|
activatePlan?: (planId: string, task: string) => void;
|
|
100
111
|
requestPermission?: PermissionRequestHandler;
|
|
101
112
|
/**
|
|
@@ -139,6 +150,12 @@ export interface CommandShellUiOpeners {
|
|
|
139
150
|
openShortcutsOverlay?: () => void;
|
|
140
151
|
getScrollTop?: () => number;
|
|
141
152
|
openPanelPicker?: () => void;
|
|
153
|
+
/**
|
|
154
|
+
* Open the fuzzy command palette — a searchable picker over every registered
|
|
155
|
+
* slash command (generated live from the registry). Bound to Ctrl+K and the
|
|
156
|
+
* /palette command. Selecting a command pre-fills it into the composer.
|
|
157
|
+
*/
|
|
158
|
+
openCommandPalette?: () => void;
|
|
142
159
|
/**
|
|
143
160
|
* Open (and optionally focus) a panel. UX-C focus rule: the command path is
|
|
144
161
|
* "the user is mid-command-flow" — opening a panel this way leaves keyboard
|
|
@@ -226,6 +243,10 @@ export interface CommandWorkspaceUiServices {
|
|
|
226
243
|
keybindingsManager?: KeybindingsManager;
|
|
227
244
|
fileUndoManager?: FileUndoManager;
|
|
228
245
|
workspaceCheckpointManager?: WorkspaceCheckpointManager;
|
|
246
|
+
/** In-process gateway catalog — the /review hunk-revert flow invokes checkpoints.revertHunk(Preview) over it. */
|
|
247
|
+
gatewayMethods?: GatewayMethodCatalog;
|
|
248
|
+
workspaceTrustManager?: import('../runtime/trust/workspace-trust.ts').WorkspaceTrustManager;
|
|
249
|
+
workspaceRegistrationManager?: import('../runtime/trust/workspace-registration.ts').WorkspaceRegistrationManager;
|
|
229
250
|
panelManager?: PanelManager;
|
|
230
251
|
profileManager?: import('@pellux/goodvibes-sdk/platform/profiles').ProfileManager;
|
|
231
252
|
bookmarkManager?: import('@pellux/goodvibes-sdk/platform/bookmarks').BookmarkManager;
|
|
@@ -241,6 +262,8 @@ export interface CommandWorkspaceServices
|
|
|
241
262
|
export interface CommandPlatformConfigServices {
|
|
242
263
|
readonly config: DeepReadonly<GoodVibesConfig>;
|
|
243
264
|
readonly configManager: ConfigManager;
|
|
265
|
+
/** Feature-flag runtime — backs the `/flags` command's listing and runtime toggling. */
|
|
266
|
+
readonly featureFlagManager?: FeatureFlagManager;
|
|
244
267
|
readonly voiceProviderRegistry?: VoiceProviderRegistry;
|
|
245
268
|
readonly voiceService?: VoiceService;
|
|
246
269
|
/** Direct-command consumer (`/search`) alongside the existing agent-tool consumer. */
|
|
@@ -248,6 +271,8 @@ export interface CommandPlatformConfigServices {
|
|
|
248
271
|
/** Direct-command consumer (`/image`) — first production caller of `.generate()`. */
|
|
249
272
|
readonly mediaProviders?: import('@pellux/goodvibes-sdk/platform/media').MediaProviderRegistry;
|
|
250
273
|
readonly artifactStore?: import('@pellux/goodvibes-sdk/platform/artifacts').ArtifactStore;
|
|
274
|
+
/** Background daemon/HTTP-listener/relay controller. Mutable: main.ts patches it in post-bootstrap (see bootstrap-command-parts.ts ordering note). */
|
|
275
|
+
externalServices?: UiPlatformServices['externalServices'];
|
|
251
276
|
}
|
|
252
277
|
|
|
253
278
|
export interface CommandPlatformServices
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* channel-pairing.ts — guided `/channel pair` flow.
|
|
3
|
+
*
|
|
4
|
+
* Adapters and their credential requirements are driven from the SDK's
|
|
5
|
+
* getBuiltinSetupSchema — the same setup-schema source every GoodVibes
|
|
6
|
+
* consumer (TUI, webui, agent) reads, not a hand-maintained list here.
|
|
7
|
+
* Secret (masked) credentials are stored the same secure way onboarding
|
|
8
|
+
* stores them, via persistSecretBackedConfigValue (secret manager + a
|
|
9
|
+
* config reference), so the plaintext never lands in config or the
|
|
10
|
+
* transcript.
|
|
11
|
+
*
|
|
12
|
+
* /channel pair → list adapters + how many credentials are set
|
|
13
|
+
* /channel pair <surface> → show the adapter's declared credentials and
|
|
14
|
+
* prompt (masked) for each missing secret
|
|
15
|
+
* /channel pair <surface> verify → a REAL round-trip: send a test message
|
|
16
|
+
* through the daemon (channels.test.send)
|
|
17
|
+
* and report the actual delivered/error
|
|
18
|
+
* outcome, never a fabricated success.
|
|
19
|
+
*/
|
|
20
|
+
import { getBuiltinSetupSchema } from '@pellux/goodvibes-sdk/platform/channels';
|
|
21
|
+
import type { ChannelSetupFieldDescriptor, ChannelSetupSchema, ChannelSurface } from '@pellux/goodvibes-sdk/platform/channels';
|
|
22
|
+
import type { CommandContext } from '../command-registry.ts';
|
|
23
|
+
import type { ConfigManager, ConfigKey } from '../../config/index.ts';
|
|
24
|
+
import { persistSecretBackedConfigValue } from '../../config/secret-config.ts';
|
|
25
|
+
import { describeOperatorRpcError, getOperatorRpc } from './operator-rpc.ts';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Surfaces pairable through /channel pair — every ChannelSurface except the
|
|
29
|
+
* two internal render surfaces ('tui', 'web'), which are not externally
|
|
30
|
+
* paired channels. The `satisfies Record<...>` shape makes this exhaustive:
|
|
31
|
+
* a new ChannelSurface value that isn't listed here fails to compile.
|
|
32
|
+
*/
|
|
33
|
+
type PairableSurface = Exclude<ChannelSurface, 'tui' | 'web'>;
|
|
34
|
+
const PAIRABLE_SURFACE_SET = {
|
|
35
|
+
slack: true,
|
|
36
|
+
discord: true,
|
|
37
|
+
ntfy: true,
|
|
38
|
+
webhook: true,
|
|
39
|
+
homeassistant: true,
|
|
40
|
+
telegram: true,
|
|
41
|
+
'google-chat': true,
|
|
42
|
+
signal: true,
|
|
43
|
+
whatsapp: true,
|
|
44
|
+
telephony: true,
|
|
45
|
+
imessage: true,
|
|
46
|
+
msteams: true,
|
|
47
|
+
bluebubbles: true,
|
|
48
|
+
mattermost: true,
|
|
49
|
+
matrix: true,
|
|
50
|
+
} satisfies Record<PairableSurface, true>;
|
|
51
|
+
const PAIRABLE_SURFACES: readonly PairableSurface[] = Object.keys(PAIRABLE_SURFACE_SET) as PairableSurface[];
|
|
52
|
+
|
|
53
|
+
function findSchema(surface: string): ChannelSetupSchema | undefined {
|
|
54
|
+
return PAIRABLE_SURFACES.includes(surface as PairableSurface) ? getBuiltinSetupSchema(surface as PairableSurface) : undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The always-present 'enabled' boolean field's config key, if declared. */
|
|
58
|
+
function enabledConfigKey(schema: ChannelSetupSchema): ConfigKey | undefined {
|
|
59
|
+
return schema.fields.find((f) => f.id === 'enabled' && f.kind === 'boolean')?.configKey as ConfigKey | undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function fieldValue(field: ChannelSetupFieldDescriptor, configManager: ConfigManager): string {
|
|
63
|
+
if (!field.configKey) return '';
|
|
64
|
+
const raw = configManager.get(field.configKey as ConfigKey);
|
|
65
|
+
return raw === undefined || raw === null ? '' : String(raw);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A field counts as configured once it has any stored value; 'select' fields always carry a default. */
|
|
69
|
+
function fieldConfigured(field: ChannelSetupFieldDescriptor, configManager: ConfigManager): boolean {
|
|
70
|
+
if (field.kind === 'select' || field.kind === 'boolean') return true;
|
|
71
|
+
return fieldValue(field, configManager).length > 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Credential fields the user must supply — secret (masked) and plain text; booleans/selects are defaulted. */
|
|
75
|
+
function credentialFields(schema: ChannelSetupSchema): ChannelSetupFieldDescriptor[] {
|
|
76
|
+
return schema.fields.filter((field) => field.kind === 'secret' || field.kind === 'string' || field.kind === 'url' || field.kind === 'number');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A field-level hint: the matching secretTarget's detail for secrets, else the placeholder, else the label. */
|
|
80
|
+
function fieldHint(field: ChannelSetupFieldDescriptor, schema: ChannelSetupSchema): string {
|
|
81
|
+
if (field.kind === 'secret' && field.secretTargetId) {
|
|
82
|
+
const target = schema.secretTargets.find((t) => t.id === field.secretTargetId);
|
|
83
|
+
if (target?.detail) return target.envKeys?.length ? `${target.detail} (env: ${target.envKeys.join(', ')})` : target.detail;
|
|
84
|
+
}
|
|
85
|
+
return field.placeholder ? `e.g. ${field.placeholder}` : field.label;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function listAdapters(ctx: CommandContext): void {
|
|
89
|
+
const configManager = ctx.platform.configManager;
|
|
90
|
+
const lines: string[] = ['Channel adapters — pair with /channel pair <surface>:', ''];
|
|
91
|
+
for (const surface of PAIRABLE_SURFACES) {
|
|
92
|
+
const schema = getBuiltinSetupSchema(surface);
|
|
93
|
+
const creds = credentialFields(schema);
|
|
94
|
+
const set = creds.filter((f) => fieldConfigured(f, configManager)).length;
|
|
95
|
+
const enabledKey = enabledConfigKey(schema);
|
|
96
|
+
const enabled = enabledKey !== undefined && configManager.get(enabledKey) === true;
|
|
97
|
+
const state = creds.length === 0 ? 'no credentials' : `${set}/${creds.length} set`;
|
|
98
|
+
lines.push(` ${surface.padEnd(14)} ${schema.label.padEnd(22)} ${state.padEnd(12)} ${enabled ? '(enabled)' : ''}`);
|
|
99
|
+
}
|
|
100
|
+
lines.push('');
|
|
101
|
+
lines.push('Run /channel pair <surface> to see and enter its declared credentials.');
|
|
102
|
+
ctx.print(lines.join('\n'));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function renderSurface(schema: ChannelSetupSchema, configManager: ConfigManager): string[] {
|
|
106
|
+
const lines = [`${schema.label} (${schema.surface})`, schema.description, '', `Setup mode: ${schema.setupMode}`, '', 'Declared credentials:'];
|
|
107
|
+
for (const field of credentialFields(schema)) {
|
|
108
|
+
const configured = fieldConfigured(field, configManager);
|
|
109
|
+
const kindTag = field.kind === 'secret' ? 'secret' : 'value';
|
|
110
|
+
const status = configured ? 'set' : 'not set';
|
|
111
|
+
lines.push(` - ${field.label} (${kindTag}): ${status} — ${fieldHint(field, schema)}`);
|
|
112
|
+
}
|
|
113
|
+
if (schema.externalSteps.length > 0) {
|
|
114
|
+
lines.push('', 'External setup steps:');
|
|
115
|
+
for (const step of schema.externalSteps) lines.push(` - ${step}`);
|
|
116
|
+
}
|
|
117
|
+
return lines;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Chain a masked prompt for each missing secret field, storing each securely. */
|
|
121
|
+
function promptMissingSecrets(
|
|
122
|
+
ctx: CommandContext,
|
|
123
|
+
schema: ChannelSetupSchema,
|
|
124
|
+
missing: readonly ChannelSetupFieldDescriptor[],
|
|
125
|
+
index: number,
|
|
126
|
+
): void {
|
|
127
|
+
const configManager = ctx.platform.configManager;
|
|
128
|
+
if (index >= missing.length) {
|
|
129
|
+
const enabledKey = enabledConfigKey(schema);
|
|
130
|
+
if (enabledKey !== undefined) configManager.setDynamic(enabledKey, true);
|
|
131
|
+
ctx.print(`\n${schema.label} credentials stored${enabledKey !== undefined ? ' and the surface is enabled' : ''}. Verify with: /channel pair ${schema.surface} verify`);
|
|
132
|
+
ctx.renderRequest();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const field = missing[index]!;
|
|
136
|
+
if (!field.configKey) {
|
|
137
|
+
ctx.print(`[pair] ${field.label} has no config key declared; skipping.`);
|
|
138
|
+
promptMissingSecrets(ctx, schema, missing, index + 1);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const configKey = field.configKey as ConfigKey;
|
|
142
|
+
if (!ctx.beginConcealedInput) {
|
|
143
|
+
ctx.print('[pair] Concealed input is unavailable on this surface; set credentials via /config and re-run verify.');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
ctx.print(`[pair] Enter ${field.label} (${field.placeholder || 'value'}) — masked; Enter to store, Esc to stop.`);
|
|
147
|
+
ctx.beginConcealedInput({
|
|
148
|
+
label: field.label,
|
|
149
|
+
onSubmit: (value) => {
|
|
150
|
+
if (value.length === 0) {
|
|
151
|
+
ctx.print(`[pair] ${field.label} left unset.`);
|
|
152
|
+
promptMissingSecrets(ctx, schema, missing, index + 1);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
void persistSecretBackedConfigValue(configManager, ctx.platform.secretsManager, configKey, value)
|
|
156
|
+
.then(() => {
|
|
157
|
+
ctx.print(`[pair] ${field.label} stored securely (hidden).`);
|
|
158
|
+
promptMissingSecrets(ctx, schema, missing, index + 1);
|
|
159
|
+
})
|
|
160
|
+
.catch((error: unknown) => {
|
|
161
|
+
ctx.print(`[pair] Failed to store ${field.label}: ${error instanceof Error ? error.message : String(error)}`);
|
|
162
|
+
promptMissingSecrets(ctx, schema, missing, index + 1);
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
onCancel: () => {
|
|
166
|
+
ctx.print(`[pair] Stopped. Re-run /channel pair ${schema.surface} to finish the remaining credentials.`);
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Verify a surface: report which declared credentials resolve locally, then
|
|
173
|
+
* perform a REAL round-trip through the daemon (channels.test.send) and
|
|
174
|
+
* report the actual delivered/error outcome — never a fabricated success.
|
|
175
|
+
*/
|
|
176
|
+
async function verifySurface(ctx: CommandContext, schema: ChannelSetupSchema): Promise<void> {
|
|
177
|
+
const configManager = ctx.platform.configManager;
|
|
178
|
+
const creds = credentialFields(schema);
|
|
179
|
+
const lines: string[] = [`Verifying ${schema.label} (${schema.surface}):`];
|
|
180
|
+
const missing: ChannelSetupFieldDescriptor[] = [];
|
|
181
|
+
for (const field of creds) {
|
|
182
|
+
const ok = fieldConfigured(field, configManager);
|
|
183
|
+
if (!ok) missing.push(field);
|
|
184
|
+
lines.push(` ${ok ? '[ok] ' : '[fail]'} ${field.label}${ok ? '' : ' — missing'}`);
|
|
185
|
+
}
|
|
186
|
+
lines.push('');
|
|
187
|
+
if (missing.length === 0) {
|
|
188
|
+
lines.push('All declared credentials resolve.');
|
|
189
|
+
} else {
|
|
190
|
+
lines.push(`${missing.length} credential(s) still missing: ${missing.map((f) => f.label).join(', ')}.`);
|
|
191
|
+
lines.push(`Run /channel pair ${schema.surface} to enter them.`);
|
|
192
|
+
}
|
|
193
|
+
ctx.print(lines.join('\n'));
|
|
194
|
+
|
|
195
|
+
const rpc = getOperatorRpc(ctx);
|
|
196
|
+
if (!rpc.available) {
|
|
197
|
+
ctx.print(`\n[verify] Cannot perform a live round-trip: ${rpc.reason}`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
ctx.print(`\n[verify] Sending a real test message through ${schema.surface}...`);
|
|
201
|
+
try {
|
|
202
|
+
const result = await rpc.sdk.operator.invoke('channels.test.send', { surface: schema.surface });
|
|
203
|
+
if (result.delivered) {
|
|
204
|
+
ctx.print(`[verify] delivered:true${result.responseId ? ` (responseId: ${result.responseId})` : ''}${result.address ? ` → ${result.address}` : ''}`);
|
|
205
|
+
} else {
|
|
206
|
+
ctx.print(`[verify] delivered:false — ${result.error ?? 'the daemon returned no error detail'}`);
|
|
207
|
+
}
|
|
208
|
+
} catch (error) {
|
|
209
|
+
ctx.print(`[verify] round-trip request failed: ${describeOperatorRpcError(error)}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Entry point for `/channel pair [...]`. */
|
|
214
|
+
export async function runChannelPairing(args: readonly string[], ctx: CommandContext): Promise<void> {
|
|
215
|
+
const surface = args[0];
|
|
216
|
+
if (!surface) {
|
|
217
|
+
listAdapters(ctx);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const schema = findSchema(surface);
|
|
221
|
+
if (!schema) {
|
|
222
|
+
ctx.print(`Unknown channel "${surface}". Available: ${PAIRABLE_SURFACES.join(', ')}`);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const action = (args[1] ?? '').toLowerCase();
|
|
226
|
+
if (action === 'verify' || action === 'test') {
|
|
227
|
+
await verifySurface(ctx, schema);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const configManager = ctx.platform.configManager;
|
|
232
|
+
ctx.print(renderSurface(schema, configManager).join('\n'));
|
|
233
|
+
const creds = credentialFields(schema);
|
|
234
|
+
const missingSecrets = creds.filter((f) => f.kind === 'secret' && !fieldConfigured(f, configManager));
|
|
235
|
+
const missingText = creds.filter((f) => f.kind !== 'secret' && !fieldConfigured(f, configManager));
|
|
236
|
+
if (missingText.length > 0) {
|
|
237
|
+
ctx.print(`\nNon-secret values to set with /config (or accept defaults): ${missingText.map((f) => `${f.label} → ${f.configKey}`).join('; ')}`);
|
|
238
|
+
}
|
|
239
|
+
if (missingSecrets.length === 0) {
|
|
240
|
+
ctx.print(`\nNo missing secrets. Verify with: /channel pair ${schema.surface} verify`);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
ctx.print(`\nEntering ${missingSecrets.length} secret credential(s) — Esc to stop.`);
|
|
244
|
+
promptMissingSecrets(ctx, schema, missingSecrets, 0);
|
|
245
|
+
}
|