@pellux/goodvibes-tui 1.16.0 → 1.17.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 +95 -9
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +1091 -271
- package/package.json +7 -7
- package/src/cli/completions/generate.ts +5 -4
- package/src/cli/config-overrides.ts +51 -11
- package/src/cli/doctor.ts +4 -5
- package/src/cli/entrypoint.ts +13 -8
- package/src/cli/help.ts +7 -6
- package/src/cli/launch-auto-update.ts +214 -0
- package/src/cli/parser.ts +5 -0
- package/src/cli/status.ts +13 -0
- package/src/cli/surface-command.ts +5 -5
- package/src/cli/tui-startup.ts +60 -97
- package/src/cli/types.ts +2 -0
- package/src/config/index.ts +1 -1
- package/src/config/tui-extension-settings.ts +33 -0
- package/src/core/composer-state.ts +1 -1
- package/src/core/conversation-line-cache.ts +3 -3
- package/src/core/conversation-rendering.ts +4 -4
- package/src/core/conversation.ts +2 -2
- package/src/core/long-task-notifier.ts +7 -7
- package/src/core/stream-event-wiring.ts +8 -6
- package/src/core/system-message-noise.ts +1 -1
- package/src/core/system-message-router.ts +6 -6
- package/src/core/turn-cancellation.ts +1 -1
- package/src/core/turn-event-wiring.ts +8 -8
- package/src/daemon/cli.ts +20 -5
- package/src/daemon/lifecycle.ts +41 -0
- package/src/daemon/service-commands.ts +5 -5
- package/src/export/cost-utils.ts +128 -6
- package/src/input/autocomplete.ts +2 -2
- package/src/input/command-registry.ts +34 -9
- package/src/input/commands/ci-runtime.ts +5 -1
- package/src/input/commands/codebase-runtime.ts +6 -6
- package/src/input/commands/cost-runtime.ts +1 -1
- package/src/input/commands/health-runtime.ts +2 -2
- package/src/input/commands/intelligence-runtime.ts +1 -1
- package/src/input/commands/local-auth-runtime.ts +1 -1
- package/src/input/commands/local-provider-runtime.ts +49 -10
- package/src/input/commands/local-runtime.ts +53 -5
- package/src/input/commands/memory.ts +2 -2
- package/src/input/commands/operator-panel-runtime.ts +5 -5
- package/src/input/commands/permissions-provenance.ts +1 -1
- package/src/input/commands/permissions-runtime.ts +99 -8
- package/src/input/commands/planning-runtime.ts +3 -3
- package/src/input/commands/platform-sandbox-runtime.ts +1 -1
- package/src/input/commands/provider-accounts-runtime.ts +71 -6
- package/src/input/commands/provider.ts +7 -5
- package/src/input/commands/recall-files-git.ts +13 -51
- package/src/input/commands/recall-files.ts +8 -11
- package/src/input/commands/recall-review.ts +2 -2
- package/src/input/commands/relay-runtime.ts +1 -1
- package/src/input/commands/remote-runtime.ts +1 -1
- package/src/input/commands/runtime-services.ts +4 -4
- package/src/input/commands/schedule-runtime.ts +14 -7
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-workflow.ts +2 -2
- package/src/input/commands/settings-sync-runtime.ts +2 -2
- package/src/input/commands/share-runtime.ts +1 -1
- package/src/input/commands/shell-core.ts +46 -68
- package/src/input/commands/subscription-runtime.ts +1 -1
- package/src/input/commands/update-runtime.ts +153 -96
- package/src/input/commands/work-plan-runtime.ts +1 -1
- package/src/input/commands/workstream-runtime.ts +2 -2
- package/src/input/commands.ts +0 -2
- package/src/input/config-modal-types.ts +3 -3
- package/src/input/config-modal.ts +10 -11
- package/src/input/feature-unit-layout.ts +93 -82
- package/src/input/feed-context-factory.ts +2 -2
- package/src/input/handler-command-route.ts +2 -2
- package/src/input/handler-content-actions.ts +1 -1
- package/src/input/handler-feed-routes.ts +7 -7
- package/src/input/handler-feed.ts +4 -4
- package/src/input/handler-interactions.ts +3 -2
- package/src/input/handler-modal-routes.ts +12 -4
- package/src/input/handler-modal-token-routes.ts +1 -1
- package/src/input/handler-onboarding-daemon-adopt.ts +2 -2
- package/src/input/handler-onboarding.ts +3 -2
- package/src/input/handler-picker-routes.ts +26 -13
- package/src/input/handler-shortcuts.ts +6 -6
- package/src/input/handler.ts +2 -2
- package/src/input/keybindings.ts +2 -2
- package/src/input/onboarding/onboarding-feature-units.ts +34 -27
- package/src/input/onboarding/onboarding-provider-key-fields.ts +112 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +22 -21
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +17 -0
- package/src/input/onboarding/onboarding-wizard-network-adopt.ts +2 -2
- package/src/input/onboarding/onboarding-wizard-steps.ts +30 -42
- package/src/input/panel-integration-actions.ts +2 -2
- package/src/input/panel-mouse-geometry.ts +1 -1
- package/src/input/panel-paste-flood-guard.ts +1 -1
- package/src/input/provider-key-intake.ts +167 -0
- package/src/input/sandbox-exec-config.ts +2 -2
- package/src/input/selection-modal.ts +1 -1
- package/src/input/settings-modal-activation.ts +5 -0
- package/src/input/settings-modal-adjustment.ts +4 -31
- package/src/input/settings-modal-data.ts +22 -21
- package/src/input/settings-modal-mutations.ts +12 -68
- package/src/input/settings-modal-types.ts +23 -16
- package/src/input/settings-modal.ts +74 -55
- package/src/main.ts +48 -48
- package/src/panels/agent-inspector-shared.ts +4 -4
- package/src/panels/base-panel.ts +2 -2
- package/src/panels/builtin/agent.ts +4 -4
- package/src/panels/builtin/development.ts +4 -4
- package/src/panels/builtin/knowledge.ts +1 -1
- package/src/panels/builtin/operations.ts +29 -10
- package/src/panels/builtin/session.ts +5 -5
- package/src/panels/builtin/shared.ts +4 -4
- package/src/panels/builtin-modals.ts +10 -10
- package/src/panels/builtin-panels.ts +1 -1
- package/src/panels/cost-tracker-panel.ts +124 -7
- package/src/panels/diff-panel.ts +5 -5
- package/src/panels/eval-registry.ts +2 -2
- package/src/panels/fleet-deep-link.ts +1 -1
- package/src/panels/fleet-panel-format.ts +44 -8
- package/src/panels/fleet-panel.ts +16 -16
- package/src/panels/fleet-read-model.ts +52 -13
- package/src/panels/fleet-steer.ts +2 -2
- package/src/panels/fleet-stop.ts +3 -3
- package/src/panels/fleet-tabs.ts +13 -14
- package/src/panels/fleet-transcript.ts +5 -5
- package/src/panels/git-panel.ts +2 -2
- package/src/panels/index.ts +4 -1
- package/src/panels/local-auth-panel.ts +1 -1
- package/src/panels/modals/hooks-modal.ts +1 -1
- package/src/panels/modals/keybindings-modal.ts +2 -2
- package/src/panels/modals/knowledge-modal.ts +1 -1
- package/src/panels/modals/marketplace-modal.ts +4 -4
- package/src/panels/modals/memory-modal.ts +4 -4
- package/src/panels/modals/modal-surface-helpers.ts +1 -1
- package/src/panels/modals/modal-theme.ts +1 -1
- package/src/panels/modals/pairing-modal.ts +2 -2
- package/src/panels/modals/planning-modal.ts +2 -2
- package/src/panels/modals/plugins-modal.ts +1 -1
- package/src/panels/modals/policy-modal.ts +1 -1
- package/src/panels/modals/security-modal.ts +1 -1
- package/src/panels/modals/skills-modal.ts +1 -1
- package/src/panels/modals/work-plan-modal.ts +3 -3
- package/src/panels/notifications-feed.ts +127 -0
- package/src/panels/notifications-panel.ts +160 -0
- package/src/panels/panel-manager.ts +9 -9
- package/src/panels/plugins-panel.ts +4 -4
- package/src/panels/project-planning-answer-actions.ts +1 -1
- package/src/panels/provider-health-routes.ts +2 -2
- package/src/panels/skills-panel.ts +4 -5
- package/src/panels/token-budget-panel.ts +11 -3
- package/src/panels/types.ts +5 -5
- package/src/permissions/broker-approval-card.ts +201 -0
- package/src/permissions/hunk-selection.ts +28 -4
- package/src/permissions/prompt.ts +244 -29
- package/src/renderer/autocomplete-overlay.ts +107 -54
- package/src/renderer/code-block.ts +2 -2
- package/src/renderer/compaction-history-modal.ts +1 -1
- package/src/renderer/compaction-preview.ts +1 -1
- package/src/renderer/compaction-quality.ts +1 -1
- package/src/renderer/compositor.ts +2 -2
- package/src/renderer/config-modal.ts +1 -1
- package/src/renderer/conversation-overlays.ts +1 -1
- package/src/renderer/footer-tips.ts +3 -3
- package/src/renderer/hint-grammar.ts +1 -1
- package/src/renderer/modal-factory.ts +1 -1
- package/src/renderer/model-picker-overlay.ts +1 -1
- package/src/renderer/model-workspace.ts +7 -2
- package/src/renderer/onboarding/onboarding-wizard.ts +22 -8
- package/src/renderer/panel-composite.ts +2 -2
- package/src/renderer/panel-workspace-bar.ts +1 -1
- package/src/renderer/process-indicator.ts +1 -1
- package/src/renderer/selection-modal-overlay.ts +102 -24
- package/src/renderer/session-picker-modal.ts +2 -2
- package/src/renderer/settings-modal-helpers.ts +14 -5
- package/src/renderer/settings-modal.ts +127 -39
- package/src/renderer/shell-surface.ts +3 -0
- package/src/renderer/terminal-bg-probe.ts +1 -1
- package/src/renderer/theme.ts +3 -3
- package/src/renderer/tool-call.ts +1 -1
- package/src/renderer/tool-result-summary.ts +1 -1
- package/src/renderer/turn-injection.ts +7 -6
- package/src/renderer/ui-factory.ts +6 -1
- package/src/runtime/bootstrap-command-context.ts +4 -1
- package/src/runtime/bootstrap-command-parts.ts +6 -4
- package/src/runtime/bootstrap-core.ts +35 -35
- package/src/runtime/bootstrap-hook-bridge.ts +1 -1
- package/src/runtime/bootstrap-shell.ts +6 -5
- package/src/runtime/bootstrap.ts +34 -32
- package/src/runtime/channel-composition.ts +46 -0
- package/src/runtime/code-index-services.ts +3 -4
- package/src/runtime/daemon-attach-notices.ts +120 -0
- package/src/runtime/durability-services.ts +55 -0
- package/src/runtime/embedded-service-factories.ts +40 -0
- package/src/runtime/feature-settings.ts +147 -0
- package/src/runtime/index.ts +4 -21
- package/src/runtime/legacy-daemon-migration.ts +29 -19
- package/src/runtime/memory-fold.ts +1 -1
- package/src/runtime/notification-dispatch.ts +110 -0
- package/src/runtime/onboarding/apply.ts +3 -29
- package/src/runtime/onboarding/fast-path.ts +39 -6
- package/src/runtime/onboarding/provider-key-capture.ts +126 -0
- package/src/runtime/onboarding/snapshot.ts +0 -1
- package/src/runtime/onboarding/types.ts +9 -3
- package/src/runtime/provider-fallback.ts +55 -0
- package/src/runtime/provider-optimizer-wiring.ts +55 -0
- package/src/runtime/release-artifacts.ts +19 -103
- package/src/runtime/resume-notice.ts +1 -1
- package/src/runtime/services.ts +81 -82
- package/src/runtime/session-continuity-hints.ts +38 -0
- package/src/runtime/surface-feature-flags.ts +40 -9
- package/src/runtime/terminal-output-guard.ts +2 -2
- package/src/runtime/trust/workspace-registration.ts +10 -2
- package/src/runtime/trust/workspace-trust.ts +58 -31
- package/src/runtime/ui-services.ts +3 -1
- package/src/runtime/update-check.ts +15 -69
- package/src/runtime/workstream-services.ts +2 -2
- package/src/shell/blocking-input.ts +102 -6
- package/src/shell/ui-openers.ts +38 -6
- package/src/utils/format-duration.ts +1 -1
- package/src/verification/verification-ledger.ts +21 -4
- package/src/version.ts +1 -1
- package/src/input/commands/flags-runtime.ts +0 -328
|
@@ -10,6 +10,7 @@ import type { AutomationManager } from '@pellux/goodvibes-sdk/platform/automatio
|
|
|
10
10
|
import type { AutomationJob } from '@pellux/goodvibes-sdk/platform/automation';
|
|
11
11
|
import type { AutomationScheduleDefinition } from '@pellux/goodvibes-sdk/platform/automation';
|
|
12
12
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
13
|
+
import { buildAutomationEmptyState } from '@pellux/goodvibes-sdk/platform/runtime/feature-announcements';
|
|
13
14
|
import { parseNaturalLanguageSchedule } from './schedule-nl.ts';
|
|
14
15
|
import type {
|
|
15
16
|
AutomationExecutionPolicy,
|
|
@@ -106,13 +107,19 @@ export function registerScheduleRuntimeCommands(registry: CommandRegistry): void
|
|
|
106
107
|
if (!sub || sub === 'list') {
|
|
107
108
|
const jobs = manager.listJobs();
|
|
108
109
|
if (jobs.length === 0) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
110
|
+
// The SDK owns the presence + title of the how-to-create-your-first-
|
|
111
|
+
// routine empty state (shown while automation is on and idle). Its
|
|
112
|
+
// body points at a command name this registry doesn't have, so we
|
|
113
|
+
// render its title with THIS surface's real /schedule commands — a
|
|
114
|
+
// command pointer that actually exists in the registry.
|
|
115
|
+
const emptyState = buildAutomationEmptyState({ enabled: true, routineCount: jobs.length });
|
|
116
|
+
ctx.print([
|
|
117
|
+
emptyState ? emptyState.title : 'No automation jobs.',
|
|
118
|
+
'Automation is on and idle. Create your first routine with:',
|
|
119
|
+
' /schedule add cron "*/30 * * * *" "check build status"',
|
|
120
|
+
' /schedule add every 15m "summarize open PRs"',
|
|
121
|
+
' /schedule add at 2026-04-10T09:00:00 "send release reminder"',
|
|
122
|
+
].join('\n'));
|
|
116
123
|
return;
|
|
117
124
|
}
|
|
118
125
|
const lines = ['Automation jobs:', ''];
|
|
@@ -16,7 +16,7 @@ export function registerServicesRuntimeCommands(registry: CommandRegistry): void
|
|
|
16
16
|
const sub = args[0] ?? 'open';
|
|
17
17
|
const shellPaths = requireShellPaths(ctx);
|
|
18
18
|
if (sub === 'open' || sub === 'panel') {
|
|
19
|
-
ctx.openModal?.('services-modal'); //
|
|
19
|
+
ctx.openModal?.('services-modal'); // services panel -> config modal
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
22
22
|
const svcRegistry = requireServiceRegistry(ctx);
|
|
@@ -89,7 +89,7 @@ export function reopenPanelsFromReturnContext(ctx: CommandContext, summary: Sess
|
|
|
89
89
|
const reopened: string[] = [];
|
|
90
90
|
const movedToModal: string[] = [];
|
|
91
91
|
for (const panelId of summary.openPanels.slice(0, 4)) {
|
|
92
|
-
//
|
|
92
|
+
// (the purge): a MIGRATE-TO-MODAL id has no panel to restore — a modal
|
|
93
93
|
// is not part of the saved panel layout. Skip it (don't pop a modal
|
|
94
94
|
// mid-resume) and note it once, rather than firing openModal + revealing an
|
|
95
95
|
// empty workspace during resume.
|
|
@@ -283,7 +283,7 @@ export async function handleSessionWorkflowCommand(args: string[], ctx: CommandC
|
|
|
283
283
|
},
|
|
284
284
|
});
|
|
285
285
|
// Hydrate the footer's token counters from the resumed (+ journal-replayed)
|
|
286
|
-
// history now, before ctx.renderRequest() below
|
|
286
|
+
// history now, before ctx.renderRequest() below.
|
|
287
287
|
ctx.session.hydrateSessionUsage?.();
|
|
288
288
|
|
|
289
289
|
if (meta.model) {
|
|
@@ -32,7 +32,7 @@ export function registerSettingsSyncRuntimeCommands(registry: CommandRegistry):
|
|
|
32
32
|
const shellPaths = requireShellPaths(ctx);
|
|
33
33
|
const controlPlaneConfigDir = ctx.platform.configManager.getControlPlaneConfigDir();
|
|
34
34
|
const sub = (args[0] ?? '').toLowerCase();
|
|
35
|
-
//
|
|
35
|
+
// item 3 (report-vs-modal front doors): settings-sync has a full
|
|
36
36
|
// modal surface (settings-sync-modal), so the bare command now opens it
|
|
37
37
|
// — the old bare/`review` transcript report moved to an explicit
|
|
38
38
|
// `report` subcommand (scriptability preserved: /settings-sync report).
|
|
@@ -41,7 +41,7 @@ export function registerSettingsSyncRuntimeCommands(registry: CommandRegistry):
|
|
|
41
41
|
return;
|
|
42
42
|
}
|
|
43
43
|
if (sub === 'panel' || sub === 'open') {
|
|
44
|
-
ctx.openModal?.('settings-sync-modal'); //
|
|
44
|
+
ctx.openModal?.('settings-sync-modal'); // settings-sync panel -> config modal
|
|
45
45
|
return;
|
|
46
46
|
}
|
|
47
47
|
if (sub === 'report' || sub === 'review') {
|
|
@@ -115,7 +115,7 @@ export function registerShareRuntimeCommands(registry: CommandRegistry): void {
|
|
|
115
115
|
);
|
|
116
116
|
// The exported document has no field for "this cost is a placeholder" —
|
|
117
117
|
// omit `cost` entirely rather than embed a misleading 0 when the model
|
|
118
|
-
// never resolved to a real price
|
|
118
|
+
// never resolved to a real price.
|
|
119
119
|
const costIsUnpriced = !isModelPriced(sessionModel);
|
|
120
120
|
if (costIsUnpriced) {
|
|
121
121
|
ctx.print(`Note: cost omitted from export — no pricing data for model "${sessionModel}".`);
|
|
@@ -6,6 +6,7 @@ import { executeWriteQuit } from './quit-shared.ts';
|
|
|
6
6
|
import { compactConversation, requireKeybindingsManager, requireProviderApi, requireSessionMemoryStore } from './runtime-services.ts';
|
|
7
7
|
import { buildCompactionPreview, buildCompactionAfterNotice, buildPinUsageText, buildPinSuccessText } from '../../renderer/compaction-preview.ts';
|
|
8
8
|
import { buildCompactionHistoryText } from '../../renderer/compaction-history-modal.ts';
|
|
9
|
+
import { ensureProviderKeyThenSelect } from '../provider-key-intake.ts';
|
|
9
10
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
10
11
|
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
11
12
|
|
|
@@ -32,11 +33,15 @@ export function registerShellCoreCommands(registry: CommandRegistry): void {
|
|
|
32
33
|
const modelId = args[0];
|
|
33
34
|
try {
|
|
34
35
|
const selected = await providerApi.selectModel(modelId);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
ctx
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
// Capture the key for an unconfigured provider inline before the
|
|
37
|
+
// selection is announced, so the switch lands ready to chat.
|
|
38
|
+
ensureProviderKeyThenSelect(ctx, selected.providerId, () => {
|
|
39
|
+
ctx.session.runtime.model = selected.registryKey;
|
|
40
|
+
ctx.session.runtime.provider = selected.providerId;
|
|
41
|
+
ctx.platform.configManager.set('provider.model', selected.registryKey);
|
|
42
|
+
ctx.print(`Switched to model: ${selected.displayName} (${selected.providerId})`);
|
|
43
|
+
void providerApi.recordModelUsage(selected.registryKey).catch((err) => { logger.debug('model usage record failed', { err }); });
|
|
44
|
+
});
|
|
40
45
|
} catch (e) {
|
|
41
46
|
ctx.print(`Error: ${summarizeError(e)}`);
|
|
42
47
|
}
|
|
@@ -112,75 +117,48 @@ export function registerShellCoreCommands(registry: CommandRegistry): void {
|
|
|
112
117
|
registry.register({
|
|
113
118
|
name: 'help',
|
|
114
119
|
aliases: ['h', '?'],
|
|
115
|
-
description: '
|
|
116
|
-
argsHint: '[command]',
|
|
120
|
+
description: 'Browse every command with its description; picking one runs it',
|
|
117
121
|
handler(_args, ctx) {
|
|
122
|
+
// Generated live from the command registry — never a hand-maintained
|
|
123
|
+
// list — so /help can never drift from the real command set. Category
|
|
124
|
+
// labels come from the same single source of truth as the generated
|
|
125
|
+
// reference and the palette (ctx.getCommandCategories; see its doc for
|
|
126
|
+
// why it is threaded through the context rather than imported here).
|
|
127
|
+
const commands = registry
|
|
128
|
+
.getAll()
|
|
129
|
+
.slice()
|
|
130
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
118
131
|
if (ctx.openSelection) {
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
{
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
{ id: '/compact', label: '/compact', detail: 'Summarize to free context', category: 'Conversation' },
|
|
132
|
-
{ id: '/export', label: '/export [file]', detail: 'Export as markdown', category: 'Conversation' },
|
|
133
|
-
{ id: '/title', label: '/title [text]', detail: 'Show or set title', category: 'Conversation' },
|
|
134
|
-
{ id: '/save', label: '/save [name]', detail: 'Save session', category: 'Conversation' },
|
|
135
|
-
{ id: '/load', label: '/load <name>', detail: 'Load session', category: 'Conversation' },
|
|
136
|
-
{ id: '/sessions', label: '/sessions', detail: 'List saved sessions', category: 'Conversation' },
|
|
137
|
-
{ id: '/session', label: '/session', detail: 'Current session info', category: 'Conversation' },
|
|
138
|
-
{ id: '/session list', label: '/session list', detail: 'List all sessions', category: 'Conversation' },
|
|
139
|
-
{ id: '/session rename', label: '/session rename <name>', detail: 'Rename current session', category: 'Conversation' },
|
|
140
|
-
{ id: '/resume', label: '/resume [id]', detail: 'Resume a previous session (picker when no id)', category: 'Conversation' },
|
|
141
|
-
{ id: '/session resume', label: '/session resume <id>', detail: 'Load and resume a session', category: 'Conversation' },
|
|
142
|
-
{ id: '/session fork', label: '/session fork [name]', detail: 'Fork current session to new ID', category: 'Conversation' },
|
|
143
|
-
{ id: '/session save', label: '/session save [name]', detail: 'Force-save current session', category: 'Conversation' },
|
|
144
|
-
{ id: '/session info', label: '/session info [id]', detail: 'Show session details', category: 'Conversation' },
|
|
145
|
-
{ id: '/session export', label: '/session export <id> [format]', detail: 'Export session as markdown/text', category: 'Conversation' },
|
|
146
|
-
{ id: '/session search', label: '/session search <query>', detail: 'Search across all sessions', category: 'Conversation' },
|
|
147
|
-
{ id: '/session delete', label: '/session delete <id>', detail: 'Delete a session', category: 'Conversation' },
|
|
148
|
-
{ id: '/undo', label: '/undo', detail: 'Remove last turn', category: 'Conversation' },
|
|
149
|
-
{ id: '/redo', label: '/redo', detail: 'Restore undone turn', category: 'Conversation' },
|
|
150
|
-
{ id: '/retry', label: '/retry [text]', detail: 'Re-send last message', category: 'Conversation' },
|
|
151
|
-
{ id: '/fork', label: '/fork [name]', detail: 'Snapshot conversation as a named branch', category: 'Conversation' },
|
|
152
|
-
{ id: '/branch', label: '/branch [name]', detail: 'List branches or switch to one', category: 'Conversation' },
|
|
153
|
-
{ id: '/merge', label: '/merge <name>', detail: 'Append messages from a branch', category: 'Conversation' },
|
|
154
|
-
{ id: '/template', label: '/template', detail: 'Browse templates', category: 'Templates' },
|
|
155
|
-
{ id: '/template save', label: '/template save <name>', detail: 'Save prompt as template', category: 'Templates' },
|
|
156
|
-
{ id: '/template use', label: '/template use <name>', detail: 'Execute template', category: 'Templates' },
|
|
157
|
-
{ id: '/tools', label: '/tools', detail: 'List available tools', category: 'Tools & System' },
|
|
158
|
-
{ id: '/search', label: '/search <query> [--limit <n>]', detail: 'Search the web and show ranked results', category: 'Tools & System' },
|
|
159
|
-
{ id: '/imagine', label: '/imagine <prompt>', detail: 'Generate an image from a prompt', category: 'Tools & System' },
|
|
160
|
-
{ id: '/paste', label: '/paste', detail: 'Insert clipboard text or image into the prompt', category: 'Tools & System' },
|
|
161
|
-
{ id: '/shortcuts', label: '/shortcuts', detail: 'View keyboard shortcuts reference', category: 'Tools & System' },
|
|
162
|
-
{ id: '/commands', label: '/commands', detail: 'Browse all commands in a scrollable list', category: 'Tools & System' },
|
|
163
|
-
{ id: '/secrets', label: '/secrets set|link|get|test|list|delete', detail: 'Manage encrypted and provider-backed secrets', category: 'Tools & System' },
|
|
164
|
-
{ id: '/help', label: '/help', detail: 'This help', category: 'Tools & System' },
|
|
165
|
-
{ id: '/quit', label: '/quit', detail: 'Exit', category: 'Tools & System' },
|
|
166
|
-
{ id: '/wq', label: '/wq', detail: 'Commit all git changes and then exit', category: 'Tools & System' },
|
|
167
|
-
];
|
|
168
|
-
// UX-C: every item here is a command, and picking one RUNS it — label
|
|
132
|
+
const categoryByName = ctx.getCommandCategories?.() ?? new Map<string, string>();
|
|
133
|
+
const items: SelectionItem[] = commands.map((cmd) => {
|
|
134
|
+
const argsHint = cmd.argsHint ?? cmd.usage;
|
|
135
|
+
const aliases = cmd.aliases ?? [];
|
|
136
|
+
return {
|
|
137
|
+
id: cmd.name,
|
|
138
|
+
label: argsHint ? `/${cmd.name} ${argsHint}` : `/${cmd.name}`,
|
|
139
|
+
detail: aliases.length > 0 ? `${cmd.description} [aka ${aliases.map((a) => `/${a}`).join(', ')}]` : cmd.description,
|
|
140
|
+
category: categoryByName.get(cmd.name) ?? 'Other',
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
// every item here is a command, and picking one RUNS it — label
|
|
169
144
|
// the verb "Run" (matching the slash-command palette) instead of the
|
|
170
145
|
// generic "Select" default.
|
|
171
146
|
ctx.openSelection('Help — Commands', items, { allowSearch: true, primaryVerbLabel: 'Run' }, (result) => {
|
|
172
147
|
if (!result) return;
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
const parts = command.slice(1).trim().split(/\s+/);
|
|
176
|
-
const name = parts[0];
|
|
177
|
-
const cmdArgs = parts.slice(1);
|
|
178
|
-
void (ctx.executeCommand?.(name, cmdArgs) ?? registry.execute(name, cmdArgs, ctx));
|
|
179
|
-
}
|
|
148
|
+
const name = result.item.id;
|
|
149
|
+
void (ctx.executeCommand?.(name, []) ?? registry.execute(name, [], ctx));
|
|
180
150
|
});
|
|
181
151
|
return;
|
|
182
152
|
}
|
|
183
|
-
ctx.print(
|
|
153
|
+
ctx.print(
|
|
154
|
+
[
|
|
155
|
+
'Commands:',
|
|
156
|
+
...commands.map((cmd) => {
|
|
157
|
+
const argsHint = cmd.argsHint ?? cmd.usage;
|
|
158
|
+
return ` /${cmd.name}${argsHint ? ` ${argsHint}` : ''} — ${cmd.description}`;
|
|
159
|
+
}),
|
|
160
|
+
].join('\n'),
|
|
161
|
+
);
|
|
184
162
|
},
|
|
185
163
|
});
|
|
186
164
|
|
|
@@ -214,7 +192,7 @@ export function registerShellCoreCommands(registry: CommandRegistry): void {
|
|
|
214
192
|
description: 'Summarize conversation to free context window',
|
|
215
193
|
async handler(_args, ctx) {
|
|
216
194
|
const messages = ctx.session.conversationManager.getMessagesForLLM();
|
|
217
|
-
//
|
|
195
|
+
// contextWindow IS reachable from CommandContext — the same
|
|
218
196
|
// call compactConversation() (runtime-services.ts) already makes to scale
|
|
219
197
|
// its own behaviour by window size. Previously hardcoded to 0 here, which
|
|
220
198
|
// silently suppressed buildCompactionPreview()'s capacity-% clause even
|
|
@@ -230,7 +208,7 @@ export function registerShellCoreCommands(registry: CommandRegistry): void {
|
|
|
230
208
|
const result = await compactConversation(ctx);
|
|
231
209
|
if (result) {
|
|
232
210
|
// Post-compact notice: uses real CompactionEvent figures, plus an
|
|
233
|
-
// out-of-band quality-score grade line when one was computed (
|
|
211
|
+
// out-of-band quality-score grade line when one was computed ().
|
|
234
212
|
ctx.print(buildCompactionAfterNotice({ event: result.event, pinnedMemoryCount, qualityScore: result.qualityScore }));
|
|
235
213
|
} else {
|
|
236
214
|
ctx.print('[Context] Compact complete.');
|
|
@@ -300,7 +278,7 @@ export function registerShellCoreCommands(registry: CommandRegistry): void {
|
|
|
300
278
|
ctx.session.runtime.debugMode = !ctx.session.runtime.debugMode;
|
|
301
279
|
ctx.print(`Debug mode: ${ctx.session.runtime.debugMode ? 'ON' : 'OFF'}`);
|
|
302
280
|
// Honest quiet counter for direct terminal writes the guard intercepted
|
|
303
|
-
// (routed here instead of spamming the transcript). (
|
|
281
|
+
// (routed here instead of spamming the transcript). (item 1a.)
|
|
304
282
|
const intercepted = ctx.session.runtime.terminalWritesIntercepted ?? 0;
|
|
305
283
|
if (intercepted > 0) {
|
|
306
284
|
ctx.print(`Terminal writes intercepted this session: ${intercepted} (details in the activity log)`);
|
|
@@ -80,7 +80,7 @@ export function registerSubscriptionRuntimeCommands(registry: CommandRegistry):
|
|
|
80
80
|
async handler(args, ctx) {
|
|
81
81
|
const shellPaths = requireShellPaths(ctx);
|
|
82
82
|
if (args.length === 0 && ctx.openModal) {
|
|
83
|
-
ctx.openModal('subscription-modal'); //
|
|
83
|
+
ctx.openModal('subscription-modal'); // subscription panel -> config modal
|
|
84
84
|
return;
|
|
85
85
|
}
|
|
86
86
|
const sub = (args[0] ?? 'review').toLowerCase();
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `/update` — a real self-update path for binary installs
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* `/update` — a real self-update path for binary installs. The
|
|
3
|
+
* download-verify-swap mechanics are the SDK's canonical update policy
|
|
4
|
+
* module (platform/runtime/self-update — hoisted from this file's
|
|
5
|
+
* semantics), the same mechanism the daemon's hourly loop and
|
|
6
|
+
* scripts/install.sh follow: one update mechanism everywhere. This file owns
|
|
7
|
+
* only the /update UX: install-kind gating, target selection, and the
|
|
8
|
+
* printed report.
|
|
6
9
|
*
|
|
7
10
|
* Subcommands:
|
|
8
11
|
* /update [check] — resolve the latest release tag and report
|
|
@@ -12,9 +15,17 @@
|
|
|
12
15
|
* app and daemon binaries, and refresh the
|
|
13
16
|
* sqlite-vec native addon in lockstep so the
|
|
14
17
|
* vector index never goes stale beside a new
|
|
15
|
-
* binary.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
+
* binary. Every swap parks the outgoing file
|
|
19
|
+
* at `<path>.previous`, so the replaced
|
|
20
|
+
* version is always kept. For any other
|
|
21
|
+
* install kind, prints the exact command to
|
|
22
|
+
* run instead — it never attempts a swap it
|
|
23
|
+
* can't do safely.
|
|
24
|
+
* /update rollback — exchange each installed file with its kept
|
|
25
|
+
* `.previous` counterpart: one command back to
|
|
26
|
+
* the version that ran before the last update
|
|
27
|
+
* (and, being an exchange, one more command
|
|
28
|
+
* forward again).
|
|
18
29
|
* /update review — install/subscription/sandbox posture,
|
|
19
30
|
* unrelated to the update mechanics above.
|
|
20
31
|
* /update bundle export|inspect <path> — portable posture bundle, as before.
|
|
@@ -26,9 +37,16 @@
|
|
|
26
37
|
* (scripts/release.ts) does not publish separate stable/preview channels,
|
|
27
38
|
* so there is no real channel selection to wire it to.
|
|
28
39
|
*/
|
|
29
|
-
import {
|
|
40
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
30
41
|
import { dirname, join } from 'node:path';
|
|
31
42
|
import { spawnSync } from 'node:child_process';
|
|
43
|
+
import {
|
|
44
|
+
applyVerifiedUpdate,
|
|
45
|
+
realUpdateFileIo,
|
|
46
|
+
rollbackKeptPrevious,
|
|
47
|
+
type UpdateFileIo,
|
|
48
|
+
type UpdateTarget,
|
|
49
|
+
} from '@pellux/goodvibes-sdk/platform/runtime/self-update';
|
|
32
50
|
import type { CommandRegistry } from '../command-registry.ts';
|
|
33
51
|
import { VERSION } from '../../version.ts';
|
|
34
52
|
import { listBuiltinSubscriptionProviders } from '@pellux/goodvibes-sdk/platform/config';
|
|
@@ -37,8 +55,6 @@ import {
|
|
|
37
55
|
parseChecksumFile,
|
|
38
56
|
resolveArtifactNames,
|
|
39
57
|
resolveSqliteVecAsset,
|
|
40
|
-
sha256,
|
|
41
|
-
verifyChecksum,
|
|
42
58
|
} from '../../runtime/release-artifacts.ts';
|
|
43
59
|
import {
|
|
44
60
|
compareVersions,
|
|
@@ -66,47 +82,13 @@ async function downloadText(fetchImpl: UpdateFetchLike, url: string): Promise<st
|
|
|
66
82
|
return await response.text();
|
|
67
83
|
}
|
|
68
84
|
|
|
69
|
-
async function downloadBuffer(fetchImpl: UpdateFetchLike, url: string): Promise<Buffer> {
|
|
70
|
-
const response = await fetchImpl(url);
|
|
71
|
-
if (!response.ok) {
|
|
72
|
-
throw new Error(`download failed (${response.status}) for ${url}`);
|
|
73
|
-
}
|
|
74
|
-
return Buffer.from(await response.arrayBuffer());
|
|
75
|
-
}
|
|
76
|
-
|
|
77
85
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* semantics) instead of executing a half-written file. Same approach as
|
|
82
|
-
* scripts/install.sh's `mv -f "$WORKDIR/$artifact" "$target"`.
|
|
86
|
+
* Suffix under which every swap keeps the file it replaced — re-exported
|
|
87
|
+
* from the SDK's canonical update policy module so rollback and swap share
|
|
88
|
+
* one definition everywhere.
|
|
83
89
|
*/
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
writeFileSync(tempPath, buffer);
|
|
87
|
-
if (process.platform !== 'win32') {
|
|
88
|
-
chmodSync(tempPath, 0o755);
|
|
89
|
-
}
|
|
90
|
-
renameSync(tempPath, targetPath);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Writes the sqlite-vec native addon into place with the same temp-write +
|
|
95
|
-
* atomic-rename discipline as the binaries, so a running process never
|
|
96
|
-
* dlopen()s a half-written extension. The addon's parent directory
|
|
97
|
-
* (`<execDir>/lib/sqlite-vec-<os>-<arch>/`) is created first because a fresh
|
|
98
|
-
* install may not have a `lib/` tree yet. A shared library needs no execute
|
|
99
|
-
* bit, so this uses 0o644 rather than the binaries' 0o755.
|
|
100
|
-
*/
|
|
101
|
-
function writeAddonAtomically(targetPath: string, buffer: Buffer): void {
|
|
102
|
-
mkdirSync(dirname(targetPath), { recursive: true });
|
|
103
|
-
const tempPath = `${targetPath}.update-download`;
|
|
104
|
-
writeFileSync(tempPath, buffer);
|
|
105
|
-
if (process.platform !== 'win32') {
|
|
106
|
-
chmodSync(tempPath, 0o644);
|
|
107
|
-
}
|
|
108
|
-
renameSync(tempPath, targetPath);
|
|
109
|
-
}
|
|
90
|
+
export { PREVIOUS_FILE_SUFFIX } from '@pellux/goodvibes-sdk/platform/runtime/self-update';
|
|
91
|
+
import { PREVIOUS_FILE_SUFFIX } from '@pellux/goodvibes-sdk/platform/runtime/self-update';
|
|
110
92
|
|
|
111
93
|
export interface DaemonServiceRestartInfo {
|
|
112
94
|
readonly managed: boolean;
|
|
@@ -169,19 +151,19 @@ export interface ApplyUpdateOptions {
|
|
|
169
151
|
readonly print: (line: string) => void;
|
|
170
152
|
readonly configManager: { get(key: string): unknown };
|
|
171
153
|
readonly runCommand?: RunCommand;
|
|
172
|
-
/** Injectable so tests
|
|
173
|
-
readonly
|
|
174
|
-
/** Injectable so tests can observe/skip writing the sqlite-vec addon. */
|
|
175
|
-
readonly writeAddon?: (targetPath: string, buffer: Buffer) => void;
|
|
176
|
-
readonly fileExists?: (path: string) => boolean;
|
|
154
|
+
/** Injectable filesystem seam (the SDK's UpdateFileIo) so tests observe swaps in memory. */
|
|
155
|
+
readonly io?: UpdateFileIo;
|
|
177
156
|
}
|
|
178
157
|
|
|
179
158
|
/**
|
|
180
|
-
* The real self-update path
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
159
|
+
* The real self-update path, delegating the download-verify-swap mechanics
|
|
160
|
+
* to the SDK's canonical update policy module (applyVerifiedUpdate — the
|
|
161
|
+
* same mechanism the daemon's hourly loop uses). For a binary install:
|
|
162
|
+
* resolve the latest tag, compare to the running version, and if newer,
|
|
163
|
+
* download + checksum-verify EVERY artifact before swapping any one (so a
|
|
164
|
+
* checksum failure never leaves a mismatched pair installed), then
|
|
165
|
+
* atomically swap each in place with the outgoing file kept at
|
|
166
|
+
* `<path>.previous`. For any other install kind, never attempts a swap — it
|
|
185
167
|
* prints the exact command for that install method instead.
|
|
186
168
|
*/
|
|
187
169
|
export async function applyUpdate(options: ApplyUpdateOptions): Promise<void> {
|
|
@@ -211,22 +193,10 @@ export async function applyUpdate(options: ApplyUpdateOptions): Promise<void> {
|
|
|
211
193
|
options.print(`Update available: ${latestTag} (running v${normalizeVersion(options.currentVersion)}). Downloading and verifying...`);
|
|
212
194
|
|
|
213
195
|
const baseUrl = releaseDownloadBaseUrl(latestTag);
|
|
214
|
-
const
|
|
215
|
-
const checksums = parseChecksumFile(checksumText);
|
|
216
|
-
|
|
196
|
+
const io = options.io ?? realUpdateFileIo;
|
|
217
197
|
const appBinaryPath = options.execPath;
|
|
218
198
|
const daemonBinaryPath = join(dirname(appBinaryPath), 'goodvibes-daemon');
|
|
219
|
-
const
|
|
220
|
-
const daemonBinaryPresent = fileExists(daemonBinaryPath);
|
|
221
|
-
|
|
222
|
-
const appBuffer = await downloadBuffer(options.fetchImpl, `${baseUrl}/${artifacts.app}`);
|
|
223
|
-
verifyChecksum(artifacts.app, sha256(appBuffer), checksums.get(artifacts.app));
|
|
224
|
-
|
|
225
|
-
let daemonBuffer: Buffer | null = null;
|
|
226
|
-
if (daemonBinaryPresent) {
|
|
227
|
-
daemonBuffer = await downloadBuffer(options.fetchImpl, `${baseUrl}/${artifacts.daemon}`);
|
|
228
|
-
verifyChecksum(artifacts.daemon, sha256(daemonBuffer), checksums.get(artifacts.daemon));
|
|
229
|
-
}
|
|
199
|
+
const daemonBinaryPresent = io.exists(daemonBinaryPath);
|
|
230
200
|
|
|
231
201
|
// The sqlite-vec native addon travels with the binaries: refresh it in the
|
|
232
202
|
// same download-verify-swap pass so /update never leaves a new binary beside a
|
|
@@ -237,28 +207,36 @@ export async function applyUpdate(options: ApplyUpdateOptions): Promise<void> {
|
|
|
237
207
|
// swap), while an absent entry means the target predates the addon and is
|
|
238
208
|
// skipped rather than blocking an otherwise-valid binary update. On macOS the
|
|
239
209
|
// file is refreshed for consistency even though the platform blocks extension
|
|
240
|
-
// loading.
|
|
210
|
+
// loading. This inclusion decision needs one manifest pre-read; the swap
|
|
211
|
+
// itself re-verifies every included artifact inside applyVerifiedUpdate.
|
|
212
|
+
const checksumText = await downloadText(options.fetchImpl, `${baseUrl}/${CHECKSUM_MANIFEST_NAME}`);
|
|
213
|
+
const checksums = parseChecksumFile(checksumText);
|
|
241
214
|
const addon = resolveSqliteVecAsset(options.platform, options.arch);
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
if (addon && addonExpected !== undefined) {
|
|
245
|
-
addonBuffer = await downloadBuffer(options.fetchImpl, `${baseUrl}/${addon.assetName}`);
|
|
246
|
-
verifyChecksum(addon.assetName, sha256(addonBuffer), addonExpected);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// All downloads verified before any write — an update must not apply partially.
|
|
250
|
-
const swap = options.swap ?? swapBinaryAtomically;
|
|
251
|
-
swap(appBinaryPath, appBuffer);
|
|
252
|
-
if (daemonBuffer) {
|
|
253
|
-
swap(daemonBinaryPath, daemonBuffer);
|
|
254
|
-
}
|
|
255
|
-
const addonTargetPath = addon && addonBuffer
|
|
215
|
+
const addonIncluded = addon !== null && checksums.get(addon.assetName) !== undefined;
|
|
216
|
+
const addonTargetPath = addon && addonIncluded
|
|
256
217
|
? join(dirname(appBinaryPath), 'lib', addon.dirName, addon.fileName)
|
|
257
218
|
: null;
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
219
|
+
|
|
220
|
+
const targets: UpdateTarget[] = [
|
|
221
|
+
{ label: 'app binary', path: appBinaryPath, assetName: artifacts.app, executable: true },
|
|
222
|
+
...(daemonBinaryPresent
|
|
223
|
+
? [{ label: 'daemon binary', path: daemonBinaryPath, assetName: artifacts.daemon, executable: true }]
|
|
224
|
+
: []),
|
|
225
|
+
...(addon && addonTargetPath
|
|
226
|
+
? [{ label: 'vector addon', path: addonTargetPath, assetName: addon.assetName, executable: false }]
|
|
227
|
+
: []),
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
// One mechanism everywhere: downloads + verifies ALL targets before any
|
|
231
|
+
// write, then swaps each atomically with the outgoing file kept at
|
|
232
|
+
// `<path>.previous`.
|
|
233
|
+
await applyVerifiedUpdate({
|
|
234
|
+
fetchImpl: options.fetchImpl,
|
|
235
|
+
downloadBaseUrl: baseUrl,
|
|
236
|
+
targets,
|
|
237
|
+
io,
|
|
238
|
+
platform: options.platform,
|
|
239
|
+
});
|
|
262
240
|
|
|
263
241
|
const serviceInfo = detectDaemonServiceManaged(options.platform, options.configManager, options.runCommand);
|
|
264
242
|
|
|
@@ -279,6 +257,70 @@ export async function applyUpdate(options: ApplyUpdateOptions): Promise<void> {
|
|
|
279
257
|
);
|
|
280
258
|
}
|
|
281
259
|
|
|
260
|
+
export interface RollbackUpdateOptions {
|
|
261
|
+
readonly execPath: string;
|
|
262
|
+
readonly platform: NodeJS.Platform;
|
|
263
|
+
readonly arch: string;
|
|
264
|
+
readonly print: (line: string) => void;
|
|
265
|
+
readonly configManager: { get(key: string): unknown };
|
|
266
|
+
readonly runCommand?: RunCommand;
|
|
267
|
+
/** Injectable filesystem seam (the SDK's UpdateFileIo) so tests observe renames in memory. */
|
|
268
|
+
readonly io?: UpdateFileIo;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* One-command rollback to the version that ran before the last update,
|
|
273
|
+
* delegating the exchange mechanics to the SDK's rollbackKeptPrevious (the
|
|
274
|
+
* same module the swap uses): every installed file (app binary, daemon
|
|
275
|
+
* binary, vector addon) that has a kept `.previous` counterpart is EXCHANGED
|
|
276
|
+
* with it — the previous version becomes live, and the version being rolled
|
|
277
|
+
* back is itself kept at `.previous`, so a second `/update rollback` rolls
|
|
278
|
+
* forward again. Files without a kept counterpart are reported and left
|
|
279
|
+
* untouched; nothing is downloaded.
|
|
280
|
+
*/
|
|
281
|
+
export function rollbackUpdate(options: RollbackUpdateOptions): void {
|
|
282
|
+
const installKind: InstallKind = detectInstallKind(options.execPath);
|
|
283
|
+
if (installKind !== 'binary') {
|
|
284
|
+
options.print(
|
|
285
|
+
[
|
|
286
|
+
`This install is not a self-updatable binary install (detected: ${installKind === 'bun-global-package' ? 'bun/npm package install' : 'running from source'}), so there is no kept previous binary to roll back to.`,
|
|
287
|
+
`Install a specific version with your package manager instead, e.g.: ${fallbackUpdateCommand(installKind)}`,
|
|
288
|
+
].join('\n'),
|
|
289
|
+
);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const io = options.io ?? realUpdateFileIo;
|
|
294
|
+
const addon = resolveSqliteVecAsset(options.platform, options.arch);
|
|
295
|
+
const targets = [
|
|
296
|
+
{ label: 'app binary', path: options.execPath },
|
|
297
|
+
{ label: 'daemon binary', path: join(dirname(options.execPath), 'goodvibes-daemon') },
|
|
298
|
+
...(addon ? [{ label: 'vector addon', path: join(dirname(options.execPath), 'lib', addon.dirName, addon.fileName) }] : []),
|
|
299
|
+
];
|
|
300
|
+
|
|
301
|
+
const result = rollbackKeptPrevious(targets, io);
|
|
302
|
+
if (result.restored.length === 0) {
|
|
303
|
+
options.print(
|
|
304
|
+
`No previous version is kept beside this install (nothing at ${options.execPath}${PREVIOUS_FILE_SUFFIX}). ` +
|
|
305
|
+
'The previous version is kept from the next update onward.',
|
|
306
|
+
);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const serviceInfo = detectDaemonServiceManaged(options.platform, options.configManager, options.runCommand);
|
|
311
|
+
options.print(
|
|
312
|
+
[
|
|
313
|
+
'Rolled back to the previously installed version.',
|
|
314
|
+
...result.restored.map((target) => ` ${target.label}: ${target.path} (the replaced version is kept at ${target.path}${PREVIOUS_FILE_SUFFIX})`),
|
|
315
|
+
'',
|
|
316
|
+
'Restart goodvibes to run the restored version.',
|
|
317
|
+
serviceInfo.managed
|
|
318
|
+
? `The daemon is managed by systemd — restart it with: ${serviceInfo.restartCommand}`
|
|
319
|
+
: 'The daemon restarts automatically the next time goodvibes launches.',
|
|
320
|
+
].join('\n'),
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
282
324
|
interface UpdateBundle {
|
|
283
325
|
readonly version: 1;
|
|
284
326
|
readonly exportedAt: number;
|
|
@@ -304,8 +346,8 @@ export function registerUpdateCommand(registry: CommandRegistry): void {
|
|
|
304
346
|
registry.register({
|
|
305
347
|
name: 'update',
|
|
306
348
|
aliases: ['upgrade'],
|
|
307
|
-
description: 'Check for a newer GoodVibes release and, for binary installs, download/verify/apply it',
|
|
308
|
-
usage: '[check|apply|review|bundle export <path>|bundle inspect <path>]',
|
|
349
|
+
description: 'Check for a newer GoodVibes release and, for binary installs, download/verify/apply it or roll back to the kept previous version',
|
|
350
|
+
usage: '[check|apply|rollback|review|bundle export <path>|bundle inspect <path>]',
|
|
309
351
|
async handler(args, ctx) {
|
|
310
352
|
const sub = args[0] ?? 'check';
|
|
311
353
|
|
|
@@ -340,6 +382,21 @@ export function registerUpdateCommand(registry: CommandRegistry): void {
|
|
|
340
382
|
return;
|
|
341
383
|
}
|
|
342
384
|
|
|
385
|
+
if (sub === 'rollback') {
|
|
386
|
+
try {
|
|
387
|
+
rollbackUpdate({
|
|
388
|
+
execPath: process.execPath,
|
|
389
|
+
platform: process.platform,
|
|
390
|
+
arch: process.arch,
|
|
391
|
+
print: ctx.print,
|
|
392
|
+
configManager: ctx.platform.configManager,
|
|
393
|
+
});
|
|
394
|
+
} catch (error) {
|
|
395
|
+
ctx.print(`Rollback failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
396
|
+
}
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
343
400
|
if (sub === 'review') {
|
|
344
401
|
const installKind = detectInstallKind(process.execPath);
|
|
345
402
|
const subscriptions = requireSubscriptionManager(ctx);
|
|
@@ -358,7 +415,7 @@ export function registerUpdateCommand(registry: CommandRegistry): void {
|
|
|
358
415
|
` built-in subscription providers: ${builtinProviders.length}${builtinProviders.length > 0 ? ` (${builtinProviders.join(', ')})` : ''}`,
|
|
359
416
|
` active subscriptions: ${activeSubscriptions.length}${activeSubscriptions.length > 0 ? ` (${activeSubscriptions.join(', ')})` : ''}`,
|
|
360
417
|
` sandbox profile: ${sandboxProfile}`,
|
|
361
|
-
' use /update check to look for a newer release, /update apply to install it',
|
|
418
|
+
' use /update check to look for a newer release, /update apply to install it, /update rollback to return to the kept previous version',
|
|
362
419
|
].join('\n'),
|
|
363
420
|
);
|
|
364
421
|
return;
|
|
@@ -406,7 +463,7 @@ export function registerUpdateCommand(registry: CommandRegistry): void {
|
|
|
406
463
|
}
|
|
407
464
|
}
|
|
408
465
|
|
|
409
|
-
ctx.print('Usage: /update [check|apply|review|bundle export <path>|bundle inspect <path>]');
|
|
466
|
+
ctx.print('Usage: /update [check|apply|rollback|review|bundle export <path>|bundle inspect <path>]');
|
|
410
467
|
},
|
|
411
468
|
});
|
|
412
469
|
}
|
|
@@ -25,7 +25,7 @@ function getStore(ctx: import('../command-registry.ts').CommandContext): WorkPla
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
function openPanel(ctx: import('../command-registry.ts').CommandContext): void {
|
|
28
|
-
//
|
|
28
|
+
// work-plan migrated to the 'work-plan' modal — open it via the seam.
|
|
29
29
|
openModalCommand(ctx, 'work-plan-modal');
|
|
30
30
|
}
|
|
31
31
|
|