@pellux/goodvibes-tui 1.13.1 → 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 +96 -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 +33 -33
- 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/gateway-verbs.ts +0 -47
- package/src/runtime/render-scheduler.ts +0 -90
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
2
|
+
import { PermissionManager, createPermissionConfigReader } from '@pellux/goodvibes-sdk/platform/permissions';
|
|
3
|
+
import type { PermissionCategory, PermissionCheckResult } from '@pellux/goodvibes-sdk/platform/permissions';
|
|
4
|
+
import { PolicyRuntimeState, createFeatureFlagManager } from '@/runtime/index.ts';
|
|
5
|
+
import type { FlagState } from '@/runtime/index.ts';
|
|
6
|
+
import { getProviderIdFromModel } from '../config/provider-model.ts';
|
|
7
|
+
import type { CliCommandOutput } from './types.ts';
|
|
8
|
+
import type { GoodVibesCliOutputFormat } from './types.ts';
|
|
9
|
+
import { buildHooksValidation } from './hooks-report.ts';
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// `goodvibes doctor <explain|routing|hooks>` — the diagnostician subcommands.
|
|
13
|
+
// Each one explains a decision the platform already makes, reading the LIVE
|
|
14
|
+
// config and reusing the platform's OWN decision machinery. None of them
|
|
15
|
+
// changes config or fires a real action:
|
|
16
|
+
// explain — walks the real PermissionManager to say why a tool/command
|
|
17
|
+
// would be allowed / asked / denied under the current mode.
|
|
18
|
+
// routing — prints which model/provider serves which role, from live config.
|
|
19
|
+
// hooks — lists registered hooks, from where, and their validation status
|
|
20
|
+
// (reuses the hooks-validate core in hooks-report.ts).
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
export interface DoctorSubcommandOptions {
|
|
24
|
+
readonly subcommand: string;
|
|
25
|
+
readonly args: readonly string[];
|
|
26
|
+
readonly configManager: ConfigManager;
|
|
27
|
+
readonly workingDirectory: string;
|
|
28
|
+
readonly homeDirectory: string;
|
|
29
|
+
readonly outputFormat?: GoodVibesCliOutputFormat;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The tool names the platform's PermissionManager recognizes (from its TOOL_CATEGORIES/TOOL_CONFIG_KEYS). */
|
|
33
|
+
const KNOWN_TOOLS = new Set([
|
|
34
|
+
'read', 'find', 'fetch', 'analyze', 'inspect', 'state', 'registry', 'goodvibes_context', 'repo_map',
|
|
35
|
+
'write', 'edit', 'goodvibes_settings', 'exec', 'agent', 'delegate', 'workflow', 'mcp',
|
|
36
|
+
]);
|
|
37
|
+
/** Tools whose primary display/decision argument is a filesystem path rather than a command. */
|
|
38
|
+
const PATH_TOOLS = new Set(['read', 'write', 'edit', 'find', 'fetch', 'analyze', 'inspect', 'state', 'registry']);
|
|
39
|
+
|
|
40
|
+
/** The permission-mode → human label map (matches the doctor/status wording). */
|
|
41
|
+
function permissionModeLabel(mode: unknown): string {
|
|
42
|
+
if (mode === 'prompt') return 'Ask before powerful actions';
|
|
43
|
+
if (mode === 'allow-all') return 'Allow everything';
|
|
44
|
+
if (mode === 'plan') return 'Plan only (read-only)';
|
|
45
|
+
if (mode === 'accept-edits') return 'Auto-accept file edits';
|
|
46
|
+
if (mode === 'custom') return 'Custom rules';
|
|
47
|
+
return String(mode ?? 'unknown');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Turn the `explain` argument list into the tool+args shape the
|
|
52
|
+
* PermissionManager evaluates. A bare recognized tool name is taken as that
|
|
53
|
+
* tool (with any remainder as its path/command arg); anything else is treated
|
|
54
|
+
* as a shell command run through the `exec` tool.
|
|
55
|
+
*/
|
|
56
|
+
function resolveExplainTarget(args: readonly string[]): { tool: string; args: Record<string, unknown>; interpretation: string } {
|
|
57
|
+
const first = args[0] ?? '';
|
|
58
|
+
if (KNOWN_TOOLS.has(first)) {
|
|
59
|
+
const remainder = args.slice(1).join(' ').trim();
|
|
60
|
+
if (remainder.length === 0) {
|
|
61
|
+
return { tool: first, args: {}, interpretation: `recognized tool "${first}" (no target argument)` };
|
|
62
|
+
}
|
|
63
|
+
if (first === 'exec') {
|
|
64
|
+
return { tool: 'exec', args: { command: remainder }, interpretation: `tool "exec" running command: ${remainder}` };
|
|
65
|
+
}
|
|
66
|
+
if (PATH_TOOLS.has(first)) {
|
|
67
|
+
return { tool: first, args: { path: remainder }, interpretation: `tool "${first}" targeting path: ${remainder}` };
|
|
68
|
+
}
|
|
69
|
+
return { tool: first, args: { command: remainder }, interpretation: `tool "${first}" with argument: ${remainder}` };
|
|
70
|
+
}
|
|
71
|
+
const command = args.join(' ').trim();
|
|
72
|
+
return { tool: 'exec', args: { command }, interpretation: `shell command run through the "exec" tool: ${command}` };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The ordered permission layers the PermissionManager consults, for the explainer's narrative. */
|
|
76
|
+
const LAYER_NAMES: readonly string[] = [
|
|
77
|
+
'Auto-approve override (behavior.autoApprove / --no-worries-just-vibes)',
|
|
78
|
+
'Session permission mode (permissions.mode)',
|
|
79
|
+
'Runtime policy engine (feature flag: permissions-policy-engine)',
|
|
80
|
+
'Per-tool config rule (permissions.tools.<tool>)',
|
|
81
|
+
'Default read auto-approve (prompt mode)',
|
|
82
|
+
'Session approval cache (remembered [A] decisions)',
|
|
83
|
+
'Human approval prompt (Human-in-the-Loop)',
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Which layer index (into LAYER_NAMES) actually decided, inferred from the
|
|
88
|
+
* PermissionManager's own sourceLayer + reasonCode. The verdict itself is the
|
|
89
|
+
* platform's; this only positions the marker in the narrative.
|
|
90
|
+
*/
|
|
91
|
+
function decidedLayerIndex(result: PermissionCheckResult, mode: string, autoApprove: boolean, category: PermissionCategory): number {
|
|
92
|
+
switch (result.sourceLayer) {
|
|
93
|
+
case 'user_prompt': return 6;
|
|
94
|
+
case 'session_override': return 5;
|
|
95
|
+
case 'safety_check':
|
|
96
|
+
case 'managed_policy': return 2;
|
|
97
|
+
case 'runtime_mode': return 1;
|
|
98
|
+
case 'config_policy':
|
|
99
|
+
if (result.reasonCode === 'config_allow') {
|
|
100
|
+
if (autoApprove) return 0;
|
|
101
|
+
if (mode === 'custom') return 3;
|
|
102
|
+
if (category === 'read') return 4;
|
|
103
|
+
return 3;
|
|
104
|
+
}
|
|
105
|
+
return 3; // config_deny → custom per-tool rule
|
|
106
|
+
default: return 6;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The authoritative ALLOW / ASK / DENY verdict from a PermissionManager result. */
|
|
111
|
+
function verdictOf(result: PermissionCheckResult): 'ALLOW' | 'ASK' | 'DENY' {
|
|
112
|
+
if (result.sourceLayer === 'user_prompt') return 'ASK';
|
|
113
|
+
return result.approved ? 'ALLOW' : 'DENY';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** A plain-language "because" line keyed off the platform's own reasonCode. */
|
|
117
|
+
function reasonExplanation(result: PermissionCheckResult, mode: string, toolKey: string): string {
|
|
118
|
+
switch (result.reasonCode) {
|
|
119
|
+
case 'config_allow':
|
|
120
|
+
if (mode === 'custom') return `The per-tool rule permissions.tools.${toolKey} is set to "allow".`;
|
|
121
|
+
if (result.sourceLayer === 'config_policy' && result.analysis.classification === 'read') return 'Read-class tools are auto-approved in the default "prompt" mode.';
|
|
122
|
+
return 'Auto-approve is active (behavior.autoApprove), so this call is approved without a prompt.';
|
|
123
|
+
case 'config_deny': return `The per-tool rule permissions.tools.${toolKey} is set to "deny".`;
|
|
124
|
+
case 'mode_allow_all':
|
|
125
|
+
if (mode === 'plan') return 'Plan mode allows read-class tools.';
|
|
126
|
+
if (mode === 'accept-edits') return 'Accept-edits mode allows read-class tools.';
|
|
127
|
+
return 'Permission mode is "allow-all" — every tool call is auto-approved.';
|
|
128
|
+
case 'mode_denied': return 'The active permission mode denies this tool class.';
|
|
129
|
+
case 'plan_mode': return 'Plan mode refuses mutating / execute / delegate tools and steers the model to present a plan instead.';
|
|
130
|
+
case 'mode_accept_edits': return 'Accept-edits mode auto-approves file write/edit tools.';
|
|
131
|
+
case 'managed_policy_allow': return 'A runtime policy rule allowed this call.';
|
|
132
|
+
case 'managed_policy_deny': return 'A runtime policy rule denied this call.';
|
|
133
|
+
case 'safety_guardrail': return 'A safety guardrail blocked this call.';
|
|
134
|
+
case 'session_cached_allow': return 'A remembered session decision ([A] allow-always) approves this call.';
|
|
135
|
+
case 'session_cached_deny': return 'A remembered session decision denies this call.';
|
|
136
|
+
case 'user_approved':
|
|
137
|
+
case 'user_denied': return 'This reaches a Human-in-the-Loop approval prompt — the outcome depends on your response at the prompt.';
|
|
138
|
+
default: return 'Decided by the platform permission machinery.';
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Stable per-tool config key for the explain narrative (mirrors the SDK's TOOL_CONFIG_KEYS defaults). */
|
|
143
|
+
function toolConfigKey(tool: string): string {
|
|
144
|
+
const map: Record<string, string> = {
|
|
145
|
+
goodvibes_context: 'state', goodvibes_settings: 'write', repo_map: 'read',
|
|
146
|
+
};
|
|
147
|
+
return map[tool] ?? tool;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function explain(options: DoctorSubcommandOptions): Promise<CliCommandOutput> {
|
|
151
|
+
const json = options.outputFormat === 'json';
|
|
152
|
+
const { args } = options;
|
|
153
|
+
if (args.length === 0) {
|
|
154
|
+
return { output: 'Usage: goodvibes doctor explain <tool-name | shell command>', exitCode: 2 };
|
|
155
|
+
}
|
|
156
|
+
const config = options.configManager;
|
|
157
|
+
const target = resolveExplainTarget(args);
|
|
158
|
+
const mode = String(config.get('permissions.mode') ?? 'prompt');
|
|
159
|
+
const autoApprove = config.get('behavior.autoApprove') === true;
|
|
160
|
+
|
|
161
|
+
// Reuse the platform's OWN PermissionManager. A requestPermission handler that
|
|
162
|
+
// returns a denial lets the machinery run to its real terminal layer without
|
|
163
|
+
// actually prompting; sourceLayer 'user_prompt' is then reported as ASK.
|
|
164
|
+
// Feature flags come from live config so the policy engine layer is walked
|
|
165
|
+
// exactly when the running app would walk it.
|
|
166
|
+
const featureFlags = createFeatureFlagManager();
|
|
167
|
+
featureFlags.loadFromConfig({ flags: (config.getCategory('featureFlags') as Record<string, FlagState>) ?? {} });
|
|
168
|
+
const policyEngineOn = featureFlags.isEnabled('permissions-policy-engine');
|
|
169
|
+
const policyRuntimeState = new PolicyRuntimeState();
|
|
170
|
+
const manager = new PermissionManager(
|
|
171
|
+
async () => ({ approved: false, remember: false }),
|
|
172
|
+
createPermissionConfigReader(config),
|
|
173
|
+
policyRuntimeState,
|
|
174
|
+
null,
|
|
175
|
+
featureFlags,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const category = manager.getCategory(target.tool, target.args);
|
|
179
|
+
const result = await manager.checkDetailed(target.tool, target.args);
|
|
180
|
+
const verdict = verdictOf(result);
|
|
181
|
+
const decidedIdx = decidedLayerIndex(result, mode, autoApprove, category);
|
|
182
|
+
const toolKey = toolConfigKey(target.tool);
|
|
183
|
+
const because = reasonExplanation(result, mode, toolKey);
|
|
184
|
+
|
|
185
|
+
if (json) {
|
|
186
|
+
return {
|
|
187
|
+
output: JSON.stringify({
|
|
188
|
+
subject: args.join(' '),
|
|
189
|
+
tool: target.tool,
|
|
190
|
+
category,
|
|
191
|
+
args: target.args,
|
|
192
|
+
mode,
|
|
193
|
+
autoApprove,
|
|
194
|
+
policyEngine: policyEngineOn ? 'enabled' : 'disabled',
|
|
195
|
+
verdict,
|
|
196
|
+
decidedLayer: LAYER_NAMES[decidedIdx],
|
|
197
|
+
sourceLayer: result.sourceLayer,
|
|
198
|
+
reasonCode: result.reasonCode,
|
|
199
|
+
because,
|
|
200
|
+
analysis: result.analysis,
|
|
201
|
+
}, null, 2),
|
|
202
|
+
exitCode: 0,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const a = result.analysis;
|
|
207
|
+
const lines: string[] = [
|
|
208
|
+
'GoodVibes doctor — explain permission decision',
|
|
209
|
+
` subject : ${args.join(' ')}`,
|
|
210
|
+
` resolved as : ${target.interpretation}`,
|
|
211
|
+
` tool : ${target.tool} (category: ${category})`,
|
|
212
|
+
` args : ${JSON.stringify(target.args)}`,
|
|
213
|
+
'',
|
|
214
|
+
'Current settings:',
|
|
215
|
+
` mode : ${mode} (${permissionModeLabel(mode)})`,
|
|
216
|
+
` autoApprove : ${autoApprove ? 'yes (behavior.autoApprove)' : 'no'}`,
|
|
217
|
+
` policyEngine: ${policyEngineOn ? 'enabled' : 'disabled'} (feature flag permissions-policy-engine)`,
|
|
218
|
+
'',
|
|
219
|
+
'Analysis (from the platform analyzer):',
|
|
220
|
+
` risk : ${a.riskLevel.toUpperCase()} (${a.classification})`,
|
|
221
|
+
...(a.surface || a.blastRadius ? [` surface : ${a.surface ?? 'generic'}${a.blastRadius ? ` radius=${a.blastRadius}` : ''}`] : []),
|
|
222
|
+
` summary : ${a.summary}`,
|
|
223
|
+
...a.reasons.slice(0, 3).map((r) => ` reason : ${r}`),
|
|
224
|
+
'',
|
|
225
|
+
'Layers walked (priority order — the platform PermissionManager decides; this shows the order it walks):',
|
|
226
|
+
...LAYER_NAMES.map((name, i) => ` ${i === decidedIdx ? '▶' : ' '} ${i + 1}. ${name}${i === decidedIdx ? ' ← DECIDED HERE' : ''}`),
|
|
227
|
+
'',
|
|
228
|
+
`Decision: ${verdict}`,
|
|
229
|
+
` decided by : ${result.sourceLayer} (${result.reasonCode})`,
|
|
230
|
+
` because : ${because}`,
|
|
231
|
+
];
|
|
232
|
+
if (policyEngineOn) {
|
|
233
|
+
lines.push(' note : the runtime policy engine is enabled, but a one-shot diagnostic loads no promoted policy bundle, so its rule set is empty here.');
|
|
234
|
+
}
|
|
235
|
+
return { output: lines.join('\n'), exitCode: 0 };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** One routed role: which provider+model the live config assigns to it, and the keys that carry it. */
|
|
239
|
+
interface RoutingRole {
|
|
240
|
+
readonly role: string;
|
|
241
|
+
readonly enabled: boolean | null;
|
|
242
|
+
readonly provider: string;
|
|
243
|
+
readonly model: string;
|
|
244
|
+
readonly keys: readonly string[];
|
|
245
|
+
readonly note?: string;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function buildRoutingRoles(config: ConfigManager): RoutingRole[] {
|
|
249
|
+
const mainModel = String(config.get('provider.model') ?? '');
|
|
250
|
+
const mainProvider = getProviderIdFromModel(mainModel);
|
|
251
|
+
const emptyProvider = (p: string): string => (p.length > 0 ? p : `(inherits main: ${mainProvider})`);
|
|
252
|
+
|
|
253
|
+
const toolProvider = String(config.get('tools.llmProvider') ?? '');
|
|
254
|
+
const toolModel = String(config.get('tools.llmModel') ?? '');
|
|
255
|
+
const helperProvider = String(config.get('helper.globalProvider') ?? '');
|
|
256
|
+
const helperModel = String(config.get('helper.globalModel') ?? '');
|
|
257
|
+
const ttsProvider = String(config.get('tts.llmProvider') ?? '');
|
|
258
|
+
const ttsModel = String(config.get('tts.llmModel') ?? '');
|
|
259
|
+
|
|
260
|
+
return [
|
|
261
|
+
{
|
|
262
|
+
role: 'Conversation (main model)',
|
|
263
|
+
enabled: null,
|
|
264
|
+
provider: mainProvider,
|
|
265
|
+
model: mainModel || '(unset)',
|
|
266
|
+
keys: ['provider.model', 'provider.reasoningEffort'],
|
|
267
|
+
note: `reasoningEffort=${String(config.get('provider.reasoningEffort') ?? '')}`,
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
role: 'Embeddings',
|
|
271
|
+
enabled: null,
|
|
272
|
+
provider: String(config.get('provider.embeddingProvider') ?? '') || `(inherits main: ${mainProvider})`,
|
|
273
|
+
model: '(provider default)',
|
|
274
|
+
keys: ['provider.embeddingProvider'],
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
role: 'Tool LLM (grunt tool work)',
|
|
278
|
+
enabled: config.get('tools.llmEnabled') === true,
|
|
279
|
+
provider: emptyProvider(toolProvider),
|
|
280
|
+
model: toolModel || '(unset)',
|
|
281
|
+
keys: ['tools.llmEnabled', 'tools.llmProvider', 'tools.llmModel'],
|
|
282
|
+
...(config.get('tools.llmEnabled') === true ? {} : { note: 'disabled — no tool-LLM route active' }),
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
role: 'Helper (cache/compaction/commit-message/etc.)',
|
|
286
|
+
enabled: config.get('helper.enabled') === true,
|
|
287
|
+
provider: emptyProvider(helperProvider),
|
|
288
|
+
model: helperModel || '(unset)',
|
|
289
|
+
keys: ['helper.enabled', 'helper.globalProvider', 'helper.globalModel'],
|
|
290
|
+
...(config.get('helper.enabled') === true ? {} : { note: 'disabled — helper tasks stay on the main model' }),
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
role: 'Narration LLM (TTS phrasing)',
|
|
294
|
+
enabled: null,
|
|
295
|
+
provider: emptyProvider(ttsProvider),
|
|
296
|
+
model: ttsModel || '(unset)',
|
|
297
|
+
keys: ['tts.llmProvider', 'tts.llmModel'],
|
|
298
|
+
note: `engine=${String(config.get('tts.provider') ?? '(unset)')}`,
|
|
299
|
+
},
|
|
300
|
+
];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function routing(options: DoctorSubcommandOptions): CliCommandOutput {
|
|
304
|
+
const config = options.configManager;
|
|
305
|
+
const roles = buildRoutingRoles(config);
|
|
306
|
+
if (options.outputFormat === 'json') {
|
|
307
|
+
return { output: JSON.stringify({ roles }, null, 2), exitCode: 0 };
|
|
308
|
+
}
|
|
309
|
+
const lines: string[] = [
|
|
310
|
+
'GoodVibes doctor — model / provider routing (live config)',
|
|
311
|
+
'',
|
|
312
|
+
];
|
|
313
|
+
for (const role of roles) {
|
|
314
|
+
const state = role.enabled === null ? '' : role.enabled ? ' [on]' : ' [off]';
|
|
315
|
+
lines.push(` ${role.role}${state}`);
|
|
316
|
+
lines.push(` provider : ${role.provider}`);
|
|
317
|
+
lines.push(` model : ${role.model}`);
|
|
318
|
+
if (role.note) lines.push(` note : ${role.note}`);
|
|
319
|
+
lines.push(` from : ${role.keys.join(', ')}`);
|
|
320
|
+
lines.push('');
|
|
321
|
+
}
|
|
322
|
+
return { output: lines.join('\n').replace(/\n+$/, ''), exitCode: 0 };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function hooks(options: DoctorSubcommandOptions): CliCommandOutput {
|
|
326
|
+
const report = buildHooksValidation(options.configManager);
|
|
327
|
+
if (options.outputFormat === 'json') {
|
|
328
|
+
return {
|
|
329
|
+
output: JSON.stringify({
|
|
330
|
+
path: report.path,
|
|
331
|
+
present: report.present,
|
|
332
|
+
valid: report.valid,
|
|
333
|
+
reason: report.reason ?? null,
|
|
334
|
+
topLevelIssues: report.topLevelIssues,
|
|
335
|
+
hooks: report.checks.map((c) => ({ pattern: c.pattern, index: c.index, name: c.name, type: c.type, source: report.path, ok: c.ok, reason: c.reason ?? null, contract: c.contract ? { authority: c.contract.authority, executionMode: c.contract.executionMode } : null })),
|
|
336
|
+
chains: report.chains,
|
|
337
|
+
}, null, 2),
|
|
338
|
+
exitCode: report.valid ? 0 : 1,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const lines: string[] = [
|
|
343
|
+
'GoodVibes doctor — registered hooks',
|
|
344
|
+
` source file : ${report.path}`,
|
|
345
|
+
];
|
|
346
|
+
if (!report.present) {
|
|
347
|
+
lines.push(' no hooks file present — nothing registered.');
|
|
348
|
+
return { output: lines.join('\n'), exitCode: 0 };
|
|
349
|
+
}
|
|
350
|
+
if (report.reason && report.checks.length === 0 && report.topLevelIssues.length === 0) {
|
|
351
|
+
lines.push(` FAIL: ${report.reason}`);
|
|
352
|
+
return { output: lines.join('\n'), exitCode: 1 };
|
|
353
|
+
}
|
|
354
|
+
lines.push(` registered : ${report.checks.length} hook(s), ${report.chains.accepted} chain(s) accepted`);
|
|
355
|
+
lines.push(` validation : ${report.passCount} pass, ${report.failCount} problem(s)`);
|
|
356
|
+
lines.push('');
|
|
357
|
+
for (const issue of report.topLevelIssues) lines.push(` FAIL (file): ${issue}`);
|
|
358
|
+
for (const check of report.checks) {
|
|
359
|
+
const status = check.ok ? 'PASS' : 'FAIL';
|
|
360
|
+
lines.push(` [${status}] ${check.pattern} #${check.index} ${check.name} (type: ${check.type})`);
|
|
361
|
+
lines.push(` from: ${report.path}`);
|
|
362
|
+
if (check.ok && check.contract) {
|
|
363
|
+
lines.push(` contract: ${check.contract.authority}/${check.contract.executionMode} (deny=${check.contract.canDeny ? 'yes' : 'no'})`);
|
|
364
|
+
} else if (!check.ok) {
|
|
365
|
+
lines.push(` reason: ${check.reason}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (report.chains.declared > 0 || report.chains.accepted > 0) {
|
|
369
|
+
lines.push('');
|
|
370
|
+
lines.push(` chains: ${report.chains.declared} declared, ${report.chains.accepted} accepted by the loader`);
|
|
371
|
+
}
|
|
372
|
+
lines.push('');
|
|
373
|
+
lines.push(report.valid ? ' result: all registered hooks are valid.' : ` result: ${report.failCount} problem(s) found.`);
|
|
374
|
+
return { output: lines.join('\n'), exitCode: report.valid ? 0 : 1 };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Route a `doctor <sub>` invocation. Returns null when <sub> is not a doctor subcommand (caller renders the classic doctor report). */
|
|
378
|
+
export async function handleDoctorSubcommand(options: DoctorSubcommandOptions): Promise<CliCommandOutput | null> {
|
|
379
|
+
switch (options.subcommand) {
|
|
380
|
+
case 'explain': return explain(options);
|
|
381
|
+
case 'routing': return routing(options);
|
|
382
|
+
case 'hooks': return hooks(options);
|
|
383
|
+
default: return null;
|
|
384
|
+
}
|
|
385
|
+
}
|
package/src/cli/entrypoint.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
applyRuntimeConfigValue,
|
|
13
13
|
applyRuntimeFeatureFlagOverrides,
|
|
14
14
|
buildCliStatusSnapshot,
|
|
15
|
+
handleDoctorSubcommand,
|
|
15
16
|
handleGoodVibesCliCommand,
|
|
16
17
|
parseGoodVibesCli,
|
|
17
18
|
renderCliStatus,
|
|
@@ -21,7 +22,14 @@ import {
|
|
|
21
22
|
renderGoodVibesVersion,
|
|
22
23
|
renderOnboardingCliStatus,
|
|
23
24
|
} from './index.ts';
|
|
24
|
-
import { buildCliServicePosture } from './service-posture.ts';
|
|
25
|
+
import { buildCliServicePosture, getGoodVibesPackageRoot, resolveGoodVibesDaemonExecutable } from './service-posture.ts';
|
|
26
|
+
import { runInstallSelfCheck } from '../runtime/install-self-check.ts';
|
|
27
|
+
import { readPersistedWorkspaceTrust } from '../runtime/trust/workspace-trust.ts';
|
|
28
|
+
import { WorkspaceRegistrationManager } from '../runtime/trust/workspace-registration.ts';
|
|
29
|
+
import type { CliWorkspaceStatus, CliSandboxStatus, CliRelayStatus } from './status.ts';
|
|
30
|
+
import { detectSandboxAvailability, probeSandboxHost } from '@pellux/goodvibes-sdk/platform/tools/exec/sandbox';
|
|
31
|
+
import { createFeatureFlagManager } from '@/runtime/index.ts';
|
|
32
|
+
import type { FlagState } from '@/runtime/index.ts';
|
|
25
33
|
import { ensureGoodvibesGitignore } from './ensure-goodvibes-gitignore.ts';
|
|
26
34
|
|
|
27
35
|
type ShellEntrypointOwnership = {
|
|
@@ -126,6 +134,21 @@ export async function prepareShellCliRuntime(
|
|
|
126
134
|
process.exit(2);
|
|
127
135
|
}
|
|
128
136
|
|
|
137
|
+
if (cli.command === 'doctor' && ['explain', 'routing', 'hooks'].includes(cli.commandArgs[0] ?? '')) {
|
|
138
|
+
const result = await handleDoctorSubcommand({
|
|
139
|
+
subcommand: cli.commandArgs[0]!,
|
|
140
|
+
args: cli.commandArgs.slice(1),
|
|
141
|
+
configManager,
|
|
142
|
+
workingDirectory: bootstrapWorkingDir,
|
|
143
|
+
homeDirectory: bootstrapHomeDirectory,
|
|
144
|
+
outputFormat: cli.flags.outputFormat,
|
|
145
|
+
});
|
|
146
|
+
if (result) {
|
|
147
|
+
console.log(result.output);
|
|
148
|
+
process.exit(result.exitCode);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
129
152
|
if (cli.command === 'status' || cli.command === 'doctor' || (cli.command === 'onboarding' && cli.commandArgs[0] === 'status')) {
|
|
130
153
|
const shellPaths = createShellPathService({
|
|
131
154
|
workingDirectory: bootstrapWorkingDir,
|
|
@@ -140,6 +163,45 @@ export async function prepareShellCliRuntime(
|
|
|
140
163
|
workingDirectory: bootstrapWorkingDir,
|
|
141
164
|
homeDirectory: bootstrapHomeDirectory,
|
|
142
165
|
});
|
|
166
|
+
// Read-only workspace posture for the report: the trust reader never
|
|
167
|
+
// persists (no grandfathering side effect), and registration resolve() is
|
|
168
|
+
// read-only, so `status`/`doctor` observe state without mutating it.
|
|
169
|
+
const trustView = readPersistedWorkspaceTrust(shellPaths);
|
|
170
|
+
const registrationEvaluation = await new WorkspaceRegistrationManager({ shellPaths }).evaluate();
|
|
171
|
+
const workspaceStatus: CliWorkspaceStatus = {
|
|
172
|
+
trustLevel: trustView.level,
|
|
173
|
+
trustGrandfathered: trustView.grandfathered,
|
|
174
|
+
registrationStatus: registrationEvaluation.status,
|
|
175
|
+
registrationRoot: registrationEvaluation.root,
|
|
176
|
+
registeredBy: registrationEvaluation.coveredBy,
|
|
177
|
+
viaWorktreeLink: registrationEvaluation.viaWorktreeLink,
|
|
178
|
+
registrationBroad: registrationEvaluation.broad,
|
|
179
|
+
};
|
|
180
|
+
// Honest per-command exec sandbox posture: the host probe (a bwrap spawn on
|
|
181
|
+
// Linux, a no-op elsewhere) is what makes "available" trustworthy.
|
|
182
|
+
const sandboxAvailability = detectSandboxAvailability(probeSandboxHost());
|
|
183
|
+
const sandboxFeatureFlags = createFeatureFlagManager();
|
|
184
|
+
sandboxFeatureFlags.loadFromConfig({
|
|
185
|
+
flags: (configManager.getCategory('featureFlags') as Record<string, FlagState>) ?? {},
|
|
186
|
+
});
|
|
187
|
+
const sandboxStatus: CliSandboxStatus = {
|
|
188
|
+
configEnabled: Boolean(configManager.getCategory('sandbox').enabled),
|
|
189
|
+
featureEnabled: sandboxFeatureFlags.isEnabled('exec-sandbox'),
|
|
190
|
+
available: sandboxAvailability.available,
|
|
191
|
+
backend: sandboxAvailability.backend,
|
|
192
|
+
reason: sandboxAvailability.reason,
|
|
193
|
+
networkIsolationGuaranteed: sandboxAvailability.networkIsolationGuaranteed,
|
|
194
|
+
};
|
|
195
|
+
// Reuses sandboxFeatureFlags (already loaded from the same featureFlags config category)
|
|
196
|
+
// for the relay-connect gate rather than constructing a second FeatureFlagManager.
|
|
197
|
+
const relayCategory = configManager.getCategory('relay');
|
|
198
|
+
const relayStatus: CliRelayStatus = {
|
|
199
|
+
configEnabled: relayCategory.enabled === true,
|
|
200
|
+
featureEnabled: sandboxFeatureFlags.isEnabled('relay-connect'),
|
|
201
|
+
url: String(relayCategory.url ?? ''),
|
|
202
|
+
rendezvousId: String(relayCategory.rendezvousId ?? ''),
|
|
203
|
+
requireStepUpForMutations: relayCategory.requireStepUpForMutations === true,
|
|
204
|
+
};
|
|
143
205
|
const statusOptions = {
|
|
144
206
|
configManager,
|
|
145
207
|
workingDirectory: bootstrapWorkingDir,
|
|
@@ -154,8 +216,17 @@ export async function prepareShellCliRuntime(
|
|
|
154
216
|
operatorTokenPresent: existsSync(operatorTokenPath),
|
|
155
217
|
},
|
|
156
218
|
service,
|
|
219
|
+
install: runInstallSelfCheck({
|
|
220
|
+
execPath: process.execPath,
|
|
221
|
+
packageRoot: getGoodVibesPackageRoot(),
|
|
222
|
+
daemon: resolveGoodVibesDaemonExecutable(),
|
|
223
|
+
fileExists: existsSync,
|
|
224
|
+
}),
|
|
157
225
|
doctor: cli.command === 'doctor',
|
|
158
226
|
outputFormat: cli.flags.outputFormat,
|
|
227
|
+
workspace: workspaceStatus,
|
|
228
|
+
sandbox: sandboxStatus,
|
|
229
|
+
relay: relayStatus,
|
|
159
230
|
};
|
|
160
231
|
const snapshot = buildCliStatusSnapshot(statusOptions);
|
|
161
232
|
console.log(cli.command === 'onboarding'
|
package/src/cli/help.ts
CHANGED
|
@@ -36,6 +36,9 @@ export function renderGoodVibesHelp(binary = 'goodvibes'): string {
|
|
|
36
36
|
' service Inspect/manage daemon service lifecycle',
|
|
37
37
|
' status Print config, provider, service, and onboarding posture',
|
|
38
38
|
' doctor Print status plus setup warnings',
|
|
39
|
+
' doctor explain <target> Explain why a tool/command would be allowed/asked/denied',
|
|
40
|
+
' doctor routing Print which model/provider serves which role',
|
|
41
|
+
' doctor hooks List registered hooks, their source, and validation status',
|
|
39
42
|
' onboarding [status] Open onboarding in the TUI, or print onboarding status',
|
|
40
43
|
' models [provider] List/use/pin selectable models and recent model history',
|
|
41
44
|
' providers List/inspect/use provider config/auth posture',
|
|
@@ -48,9 +51,12 @@ export function renderGoodVibesHelp(binary = 'goodvibes'): string {
|
|
|
48
51
|
' surfaces Inspect/check/enable/disable browser/listener/external surfaces',
|
|
49
52
|
' listener test Test HTTP listener/webhook readiness',
|
|
50
53
|
' control-plane status Inspect daemon auth, local admin, tokens, and ports',
|
|
51
|
-
' bundle export|inspect|import',
|
|
52
|
-
' Move
|
|
54
|
+
' support-bundle export|inspect|import',
|
|
55
|
+
' Move a support snapshot (config + diagnostics + posture)',
|
|
53
56
|
' remote|bridge Inspect remote runner/node posture',
|
|
57
|
+
' hooks validate Validate hooks.json against the hook loader schema',
|
|
58
|
+
' plugin init|validate Scaffold a plugin or validate a plugin directory',
|
|
59
|
+
' plugin bundles ... Browse/install/list SHA-256-pinned capability bundles',
|
|
54
60
|
' completion <shell> Generate shell completion script',
|
|
55
61
|
' help [command] Print this help or command-specific help',
|
|
56
62
|
' version Print version',
|
|
@@ -133,9 +139,9 @@ const COMMAND_HELP: Record<string, CommandHelp> = {
|
|
|
133
139
|
examples: ['status', 'status --json'],
|
|
134
140
|
},
|
|
135
141
|
doctor: {
|
|
136
|
-
usage: ['doctor', 'doctor --json'],
|
|
137
|
-
summary: 'Print status plus
|
|
138
|
-
examples: ['doctor', 'doctor --json'],
|
|
142
|
+
usage: ['doctor', 'doctor --json', 'doctor explain <tool|command>', 'doctor routing', 'doctor hooks'],
|
|
143
|
+
summary: 'Print status plus setup warnings, or explain a platform decision: explain (why a tool/command would be allowed/asked/denied under the current mode), routing (which model/provider serves which role), hooks (registered hooks, their source, and validation status).',
|
|
144
|
+
examples: ['doctor', 'doctor explain "rm -rf build"', 'doctor explain write ./src/app.ts', 'doctor routing', 'doctor hooks --json'],
|
|
139
145
|
},
|
|
140
146
|
providers: {
|
|
141
147
|
usage: ['providers [list]', 'providers current', 'providers inspect <provider>', 'providers use <provider> [modelRegistryKey]'],
|
|
@@ -187,10 +193,10 @@ const COMMAND_HELP: Record<string, CommandHelp> = {
|
|
|
187
193
|
summary: 'Inspect daemon control-plane bind posture, reachability, local auth, bootstrap credentials, and operator tokens.',
|
|
188
194
|
examples: ['control-plane status', 'control-plane status --json'],
|
|
189
195
|
},
|
|
190
|
-
bundle: {
|
|
191
|
-
usage: ['bundle export [path]', 'bundle inspect <path>', 'bundle import <path>'],
|
|
192
|
-
summary: 'Export, inspect, or import
|
|
193
|
-
examples: ['bundle export goodvibes-bundle.json', 'bundle inspect goodvibes-bundle.json'],
|
|
196
|
+
'support-bundle': {
|
|
197
|
+
usage: ['support-bundle export [path]', 'support-bundle inspect <path>', 'support-bundle import <path>'],
|
|
198
|
+
summary: 'Export, inspect, or import a support snapshot (redacted config + diagnostics + posture). Aliases: bundle, bundles. Distinct from `plugin bundles` (capability bundles).',
|
|
199
|
+
examples: ['support-bundle export goodvibes-bundle.json', 'support-bundle inspect goodvibes-bundle.json'],
|
|
194
200
|
},
|
|
195
201
|
pair: {
|
|
196
202
|
usage: ['pair', 'qrcode'],
|
|
@@ -222,6 +228,16 @@ const COMMAND_HELP: Record<string, CommandHelp> = {
|
|
|
222
228
|
summary: 'Inspect remote runner/node posture and bridge readiness.',
|
|
223
229
|
examples: ['remote', 'bridge'],
|
|
224
230
|
},
|
|
231
|
+
hooks: {
|
|
232
|
+
usage: ['hooks validate'],
|
|
233
|
+
summary: 'Validate the configured hooks.json against the hook loader schema — per-hook pass/fail with plain reasons, nonzero exit if any hook is invalid.',
|
|
234
|
+
examples: ['hooks validate', 'hooks validate --json'],
|
|
235
|
+
},
|
|
236
|
+
plugin: {
|
|
237
|
+
usage: ['plugin init <name> [directory]', 'plugin validate <path>', 'plugin bundles browse <index-url-or-file>', 'plugin bundles install <ref> --sha256 <pin>', 'plugin bundles list'],
|
|
238
|
+
summary: 'Scaffold a minimal valid plugin (manifest.json + entry file), validate a plugin directory against the plugin loader checks, or browse/install/list SHA-256-pinned capability bundles from a governed marketplace index. Bundle installs are never unpinned — a missing or mismatched --sha256 is refused, not overridable.',
|
|
239
|
+
examples: ['plugin init my-plugin', 'plugin validate .goodvibes/plugins/my-plugin', 'plugin bundles browse ./marketplace-index.json', 'plugin bundles install ./bundle.json --sha256 <64-hex> --yes'],
|
|
240
|
+
},
|
|
225
241
|
};
|
|
226
242
|
|
|
227
243
|
const HELP_ALIASES: Record<string, string> = {
|
|
@@ -238,12 +254,16 @@ const HELP_ALIASES: Record<string, string> = {
|
|
|
238
254
|
webhook: 'listener',
|
|
239
255
|
controlplane: 'control-plane',
|
|
240
256
|
cp: 'control-plane',
|
|
257
|
+
bundle: 'support-bundle',
|
|
258
|
+
bundles: 'support-bundle',
|
|
241
259
|
qrcode: 'pair',
|
|
242
260
|
qr: 'pair',
|
|
243
261
|
daemon: 'serve',
|
|
244
262
|
server: 'serve',
|
|
245
263
|
services: 'service',
|
|
246
264
|
bridge: 'remote',
|
|
265
|
+
hook: 'hooks',
|
|
266
|
+
plugins: 'plugin',
|
|
247
267
|
};
|
|
248
268
|
|
|
249
269
|
function normalizeHelpTopic(topic: string): string {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { buildHooksValidation } from './hooks-report.ts';
|
|
2
|
+
import type { CliCommandOutput, CliCommandRuntime } from './types.ts';
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// `goodvibes hooks validate` — validate the user's hooks.json against the
|
|
6
|
+
// hooks loader's REAL schema. The acceptance verdict comes from
|
|
7
|
+
// buildHooksValidation (src/cli/hooks-report.ts), which runs the SDK's own
|
|
8
|
+
// HookDispatcher.loadFromFile and getHookPointContract — the same machinery
|
|
9
|
+
// `goodvibes doctor hooks` reuses. This command only reads and reports.
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
export async function handleHooksCommand(runtime: CliCommandRuntime): Promise<CliCommandOutput> {
|
|
13
|
+
const [sub = 'validate'] = runtime.cli.commandArgs;
|
|
14
|
+
const json = runtime.cli.flags.outputFormat === 'json';
|
|
15
|
+
if (sub !== 'validate') {
|
|
16
|
+
return { output: 'Usage: goodvibes hooks validate', exitCode: 2 };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const report = buildHooksValidation(runtime.configManager);
|
|
20
|
+
const { path } = report;
|
|
21
|
+
|
|
22
|
+
if (!report.present) {
|
|
23
|
+
const text = [
|
|
24
|
+
'GoodVibes hooks validation',
|
|
25
|
+
` file: ${path}`,
|
|
26
|
+
' no hooks file present — nothing to validate.',
|
|
27
|
+
].join('\n');
|
|
28
|
+
return { output: json ? JSON.stringify({ path, present: false, hooks: [], valid: true }, null, 2) : text, exitCode: 0 };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (report.reason && report.checks.length === 0 && report.topLevelIssues.length === 0) {
|
|
32
|
+
const text = ['GoodVibes hooks validation', ` file: ${path}`, ` FAIL: ${report.reason}`].join('\n');
|
|
33
|
+
return { output: json ? JSON.stringify({ path, present: true, valid: false, reason: report.reason }, null, 2) : text, exitCode: 1 };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { checks, topLevelIssues, chains, passCount, failCount, valid } = report;
|
|
37
|
+
|
|
38
|
+
if (json) {
|
|
39
|
+
return {
|
|
40
|
+
output: JSON.stringify({
|
|
41
|
+
path,
|
|
42
|
+
present: true,
|
|
43
|
+
valid,
|
|
44
|
+
topLevelIssues,
|
|
45
|
+
hooks: checks.map((c) => ({ pattern: c.pattern, index: c.index, name: c.name, ok: c.ok, reason: c.reason ?? null })),
|
|
46
|
+
chains: { declared: chains.declared, accepted: chains.accepted },
|
|
47
|
+
}, null, 2),
|
|
48
|
+
exitCode: valid ? 0 : 1,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const lines: string[] = [
|
|
53
|
+
'GoodVibes hooks validation',
|
|
54
|
+
` file: ${path}`,
|
|
55
|
+
` hooks: ${checks.length} total, ${passCount} pass, ${checks.length - passCount} fail`,
|
|
56
|
+
];
|
|
57
|
+
for (const issue of topLevelIssues) lines.push(` FAIL: ${issue}`);
|
|
58
|
+
for (const check of checks) {
|
|
59
|
+
const label = `${check.pattern} #${check.index} ${check.name}`;
|
|
60
|
+
if (check.ok) {
|
|
61
|
+
const c = check.contract;
|
|
62
|
+
const detail = c ? ` (contract: ${c.authority}/${c.executionMode})` : '';
|
|
63
|
+
lines.push(` PASS ${label}${detail}`);
|
|
64
|
+
} else {
|
|
65
|
+
lines.push(` FAIL ${label} — ${check.reason}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (chains.declared > 0 || chains.accepted > 0) {
|
|
69
|
+
lines.push(` chains: ${chains.declared} declared, ${chains.accepted} accepted by the loader`);
|
|
70
|
+
}
|
|
71
|
+
lines.push(valid ? ' result: all hooks are valid.' : ` result: ${failCount} problem(s) found.`);
|
|
72
|
+
|
|
73
|
+
return { output: lines.join('\n'), exitCode: valid ? 0 : 1 };
|
|
74
|
+
}
|