Ver Fonte

feat(config): allow notification-only plugin updates

Expose the autoUpdate toggle so users can opt out of background installs while still getting update notifications.
Gustavo Caiano há 4 meses atrás
pai
commit
0dec84a91b

+ 19 - 0
docs/configuration.md

@@ -99,6 +99,7 @@ Presets can also be switched at runtime without restarting using the `/preset` c
 | `agents.<customAgent>.orchestratorPrompt` | string | — | Exact `@agent` block injected into the orchestrator prompt; must start with `@<agent-name>` |
 | `agents.<agent>.displayName` | string | — | Custom user-facing alias for the agent in the active config |
 | `showStartupToast` | boolean | `true` | Show the startup activation toast (`oh-my-opencode-slim is active`) when OpenCode starts |
+| `autoUpdate` | boolean | `true` | Automatically install plugin updates in the background; set to `false` for notification-only mode |
 | `multiplexer.type` | string | `"none"` | Multiplexer mode: `auto`, `tmux`, `zellij`, or `none` |
 | `multiplexer.layout` | string | `"main-vertical"` | Layout preset: `main-vertical`, `main-horizontal`, `tiled`, `even-horizontal`, `even-vertical` |
 | `multiplexer.main_pane_size` | number | `60` | Main pane size as percentage (20–80) |
@@ -139,6 +140,24 @@ appears when the plugin activates.
 }
 ```
 
+### Manual Update Mode
+
+Set `autoUpdate` to `false` if you want update notifications without automatic
+`bun install` runs.
+
+```jsonc
+{
+  "autoUpdate": false
+}
+```
+
+When enabled, this is notification-only mode: you'll see that a new version is
+available, but the plugin won't install it automatically.
+
+> Pinned plugin entries in `opencode.json` (for example
+> `"oh-my-opencode-slim@1.0.1"`) are the true version lock. Those stay pinned
+> regardless of `autoUpdate`.
+
 ### Agent Display Names
 
 Use `displayName` to give an agent a user-facing alias while keeping the

+ 4 - 0
oh-my-opencode-slim.schema.json

@@ -23,6 +23,10 @@
       "description": "Show the startup activation toast when OpenCode starts. Defaults to true.",
       "type": "boolean"
     },
+    "autoUpdate": {
+      "description": "Disable automatic installation of plugin updates when false. Defaults to true.",
+      "type": "boolean"
+    },
     "manualPlan": {
       "type": "object",
       "properties": {

+ 15 - 0
src/config/loader.test.ts

@@ -95,6 +95,21 @@ describe('loadPluginConfig', () => {
     expect(config.showStartupToast).toBe(false);
   });
 
+  test('loads autoUpdate flag when configured', () => {
+    const projectDir = path.join(tempDir, 'project');
+    const projectConfigDir = path.join(projectDir, '.opencode');
+    fs.mkdirSync(projectConfigDir, { recursive: true });
+    fs.writeFileSync(
+      path.join(projectConfigDir, 'oh-my-opencode-slim.json'),
+      JSON.stringify({
+        autoUpdate: false,
+      }),
+    );
+
+    const config = loadPluginConfig(projectDir);
+    expect(config.autoUpdate).toBe(false);
+  });
+
   test('loads manual plan structure when configured', () => {
     const projectDir = path.join(tempDir, 'project');
     const projectConfigDir = path.join(projectDir, '.opencode');

+ 6 - 0
src/config/schema.ts

@@ -280,6 +280,12 @@ export const PluginConfigSchema = z
       .describe(
         'Show the startup activation toast when OpenCode starts. Defaults to true.',
       ),
+    autoUpdate: z
+      .boolean()
+      .optional()
+      .describe(
+        'Disable automatic installation of plugin updates when false. Defaults to true.',
+      ),
     manualPlan: ManualPlanSchema.optional(),
     presets: z.record(z.string(), PresetSchema).optional(),
     agents: z.record(z.string(), AgentOverrideConfigSchema).optional(),

+ 32 - 0
src/hooks/auto-update-checker/index.test.ts

@@ -187,6 +187,38 @@ describe('auto-update-checker/index', () => {
     });
   });
 
+  test('shows notification-only toast when auto-update is disabled', async () => {
+    checkerMocks.findPluginEntry.mockImplementation(() => ({
+      pinnedVersion: null,
+      isPinned: false,
+    }));
+    checkerMocks.getCachedVersion.mockImplementation(() => '0.9.1');
+    checkerMocks.getLatestVersion.mockImplementation(async () => '0.9.11');
+
+    const { createAutoUpdateCheckerHook } = await import(
+      `./index?test=${importCounter++}`
+    );
+    const { ctx, showToast } = createCtx();
+
+    const hook = createAutoUpdateCheckerHook(ctx as never, {
+      showStartupToast: false,
+      autoUpdate: false,
+    });
+    hook.event({ event: { type: 'session.created', properties: {} } });
+    await waitForCalls(showToast);
+
+    expect(showToast).toHaveBeenCalledWith({
+      body: {
+        title: 'OMO-Slim 0.9.11',
+        message: 'v0.9.11 available. Auto-update is disabled.',
+        variant: 'info',
+        duration: 8000,
+      },
+    });
+    expect(cacheMocks.preparePackageUpdate).not.toHaveBeenCalled();
+    expect(crossSpawnMock).not.toHaveBeenCalled();
+  });
+
   test('shows prepare failure toast and skips installation when active install cannot be resolved', async () => {
     checkerMocks.findPluginEntry.mockImplementation(() => ({
       pinnedVersion: null,

+ 2 - 2
src/index.ts

@@ -2,7 +2,6 @@ import type { Plugin } from '@opencode-ai/plugin';
 import { createAgents, getAgentConfigs, getDisabledAgents } from './agents';
 import { buildOrchestratorPrompt } from './agents/orchestrator';
 import { loadPluginConfig, type MultiplexerConfig } from './config';
-import { collapseSystemInPlace } from './utils/system-collapse';
 import { parseList } from './config/agent-mcps';
 import { CouncilManager } from './council';
 import {
@@ -35,6 +34,7 @@ import {
 import { resolveRuntimeAgentName, rewriteDisplayNameMentions } from './utils';
 import { initLogger, log } from './utils/logger';
 import { SubagentDepthTracker } from './utils/subagent-depth';
+import { collapseSystemInPlace } from './utils/system-collapse';
 
 /**
  * Best-effort log to opencode's app logger.
@@ -208,7 +208,7 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
     // Initialize auto-update checker hook
     autoUpdateChecker = createAutoUpdateCheckerHook(ctx, {
       showStartupToast: config.showStartupToast ?? true,
-      autoUpdate: true,
+      autoUpdate: config.autoUpdate ?? true,
     });
 
     // Initialize phase reminder hook for workflow compliance

+ 3 - 1
src/interview/interview.test.ts

@@ -1618,7 +1618,9 @@ describe('renderInterviewPage', () => {
     expect(html).toContain(
       "window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });",
     );
-    expect(html).toContain('overlayText.textContent = "Submitting Answers...";');
+    expect(html).toContain(
+      'overlayText.textContent = "Submitting Answers...";',
+    );
     expect(html).toContain('scrollToTop();');
   });
 });

+ 1 - 1
src/utils/system-collapse.test.ts

@@ -1,4 +1,4 @@
-import { expect, describe, test } from 'bun:test';
+import { describe, expect, test } from 'bun:test';
 import { collapseSystemInPlace } from './system-collapse';
 
 /**