Browse Source

feat: support disabling plugin via OH_MY_OPENCODE_SLIM_DISABLE

alvinreal 1 month ago
parent
commit
711413725a
10 changed files with 158 additions and 4 deletions
  1. 12 0
      README.md
  2. 8 0
      docs/configuration.md
  3. 3 3
      src/agents/index.test.ts
  4. 1 1
      src/config/constants.ts
  5. 34 0
      src/index.test.ts
  6. 6 0
      src/index.ts
  7. 47 0
      src/tui.test.ts
  8. 3 0
      src/tui.ts
  9. 31 0
      src/utils/env.test.ts
  10. 13 0
      src/utils/env.ts

+ 12 - 0
README.md

@@ -161,6 +161,18 @@ The default generated configuration includes both `openai` and `opencode-go` pre
 
 To use custom providers or a mixed-provider setup, use **[Configuration](docs/configuration.md)** for the full reference. If you want a ready-made starting point, check the **[Author's Preset](docs/authors-preset.md)** and **[$30 Preset](docs/thirty-dollars-preset.md)** - the `$30` preset is the best cheap setup.
 
+### Temporarily Disable the Plugin
+
+Set `OH_MY_OPENCODE_SLIM_DISABLE=1` when starting OpenCode to make the plugin
+return without registering agents, tools, MCPs, hooks, Companion, or the TUI
+sidebar:
+
+```bash
+OH_MY_OPENCODE_SLIM_DISABLE=1 opencode
+```
+
+Truthy values are `1`, `true`, `yes`, and `on`.
+
 ### ✅ Verify Your Setup
 
 After installation and authentication, verify all agents are configured and responding:

+ 8 - 0
docs/configuration.md

@@ -18,6 +18,14 @@ Complete reference for all configuration files and options in oh-my-opencode-sli
 Set `OPENCODE_CONFIG_DIR` to use a custom user config directory instead of
 `~/.config/opencode`; install and runtime config discovery both honor it.
 
+Set `OH_MY_OPENCODE_SLIM_DISABLE` to `1`, `true`, `yes`, or `on` to make
+oh-my-opencode-slim return during startup without registering agents, tools,
+MCPs, hooks, Companion, or the TUI sidebar. This is a temporary escape hatch:
+
+```bash
+OH_MY_OPENCODE_SLIM_DISABLE=1 opencode
+```
+
 If OmO-slim detects an invalid plugin config for the current project, the TUI sidebar shows a warning. Run `oh-my-opencode-slim doctor` from your project root for full diagnostics.
 
 ---

+ 3 - 3
src/agents/index.test.ts

@@ -501,9 +501,9 @@ describe('getAgentConfigs', () => {
     const configs = getAgentConfigs();
     expect(configs.orchestrator).toBeDefined();
     expect(configs.explorer).toBeDefined();
-    // orchestrator has no hardcoded default model; resolved at runtime via
-    // chat.message hook when _modelArray is configured, or left to the user
-    expect(configs.explorer.model).toBeDefined();
+    // Agents have no hardcoded default model; OpenCode resolves them from the
+    // global/session model unless users override per-agent models.
+    expect(configs.explorer.model).toBeUndefined();
   });
 
   test('includes description in SDK config', () => {

+ 1 - 1
src/config/constants.ts

@@ -84,4 +84,4 @@ export const STABLE_POLLS_THRESHOLD = 3;
 
 /** Agents that are disabled by default. Users must explicitly enable them
  *  by removing from disabled_agents and configuring an appropriate model. */
-export const DEFAULT_DISABLED_AGENTS: string[] = ['observer'];
+export const DEFAULT_DISABLED_AGENTS: string[] = ['observer'];

+ 34 - 0
src/index.test.ts

@@ -0,0 +1,34 @@
+import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
+import plugin from './index';
+
+describe('plugin env disable', () => {
+  let originalEnv: typeof process.env;
+
+  beforeEach(() => {
+    originalEnv = { ...process.env };
+  });
+
+  afterEach(() => {
+    process.env = originalEnv;
+  });
+
+  test('returns empty hooks without reading plugin context', async () => {
+    process.env.OH_MY_OPENCODE_SLIM_DISABLE = '1';
+
+    const ctx = new Proxy(
+      {},
+      {
+        get(_target, property) {
+          throw new Error(`disabled plugin read ctx.${String(property)}`);
+        },
+      },
+    );
+
+    const hooks = await plugin(ctx as Parameters<typeof plugin>[0]);
+
+    expect(hooks).toEqual({});
+    expect(hooks.config).toBeUndefined();
+    expect(hooks.event).toBeUndefined();
+    expect(hooks.tool).toBeUndefined();
+  });
+});

+ 6 - 0
src/index.ts

@@ -55,6 +55,7 @@ import {
   createDisplayNameMentionRewriter,
   resolveRuntimeAgentName,
 } from './utils';
+import { isPluginDisabledByEnv } from './utils/env';
 import { initLogger, log } from './utils/logger';
 import { SubagentDepthTracker } from './utils/subagent-depth';
 import { collapseSystemInPlace } from './utils/system-collapse';
@@ -115,6 +116,11 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
   const sessionId = new Date().toISOString().replace(/[-:]/g, '').slice(0, 15);
   initLogger(sessionId);
 
+  if (isPluginDisabledByEnv()) {
+    log('[plugin] disabled by OH_MY_OPENCODE_SLIM_DISABLE');
+    return {};
+  }
+
   // Declare variables that must survive the try/catch for the return
   // closure. These are set inside the try block.
   let config: ReturnType<typeof loadPluginConfig>;

+ 47 - 0
src/tui.test.ts

@@ -6,6 +6,7 @@ import {
   getSidebarAgentNames,
   readConfigInvalid,
   splitSidebarModelId,
+  default as tuiPlugin,
 } from './tui';
 import type { TuiSnapshot } from './tui-state';
 
@@ -119,3 +120,49 @@ describe('readConfigInvalid', () => {
     }
   });
 });
+
+describe('tui plugin env disable', () => {
+  let originalEnv: typeof process.env;
+
+  beforeEach(() => {
+    originalEnv = { ...process.env };
+  });
+
+  afterEach(() => {
+    process.env = originalEnv;
+  });
+
+  test('does not perform setup when plugin is disabled by env', async () => {
+    process.env.OH_MY_OPENCODE_SLIM_DISABLE = '1';
+
+    let disposeRegistered = false;
+    let renderRequested = false;
+    let registered = false;
+    await tuiPlugin.tui(
+      {
+        lifecycle: {
+          onDispose: () => {
+            disposeRegistered = true;
+          },
+        },
+        renderer: {
+          requestRender: () => {
+            renderRequested = true;
+          },
+        },
+        slots: {
+          register: () => {
+            registered = true;
+          },
+        },
+        theme: { current: {} },
+      } as unknown as Parameters<typeof tuiPlugin.tui>[0],
+      {},
+      { version: 'test' } as Parameters<typeof tuiPlugin.tui>[2],
+    );
+
+    expect(registered).toBe(false);
+    expect(disposeRegistered).toBe(false);
+    expect(renderRequested).toBe(false);
+  });
+});

+ 3 - 0
src/tui.ts

@@ -8,6 +8,7 @@ import {
   readTuiSnapshotAsync,
   type TuiSnapshot,
 } from './tui-state';
+import { isPluginDisabledByEnv } from './utils/env';
 
 const PLUGIN_NAME = 'oh-my-opencode-slim';
 const CONFIG_WARNING_COLOR = 'orange';
@@ -212,6 +213,8 @@ export function readConfigInvalid(directory: string): boolean {
 const plugin: TuiPluginModule & { id: string } = {
   id: `${PLUGIN_NAME}:tui`,
   tui: async (api, _options, meta) => {
+    if (isPluginDisabledByEnv()) return;
+
     const version = meta.version ?? (await readPackageVersion()) ?? 'dev';
     let configDirectory = getTuiDirectory(api);
     let configInvalid = readConfigInvalid(configDirectory);

+ 31 - 0
src/utils/env.test.ts

@@ -0,0 +1,31 @@
+import { describe, expect, test } from 'bun:test';
+import {
+  isPluginDisabledByEnv,
+  isTruthyEnvValue,
+  PLUGIN_DISABLE_ENV,
+} from './env';
+
+describe('isTruthyEnvValue', () => {
+  test.each(['1', 'true', 'TRUE', ' yes ', 'on'])('%p is truthy', (value) => {
+    expect(isTruthyEnvValue(value)).toBe(true);
+  });
+
+  test.each([
+    undefined,
+    '',
+    '0',
+    'false',
+    'no',
+    'off',
+    'anything',
+  ])('%p is not truthy', (value) => {
+    expect(isTruthyEnvValue(value)).toBe(false);
+  });
+});
+
+describe('isPluginDisabledByEnv', () => {
+  test('reads OH_MY_OPENCODE_SLIM_DISABLE', () => {
+    expect(isPluginDisabledByEnv({ [PLUGIN_DISABLE_ENV]: '1' })).toBe(true);
+    expect(isPluginDisabledByEnv({ [PLUGIN_DISABLE_ENV]: '0' })).toBe(false);
+  });
+});

+ 13 - 0
src/utils/env.ts

@@ -0,0 +1,13 @@
+const TRUTHY_ENV_VALUES = new Set(['1', 'true', 'yes', 'on']);
+
+export const PLUGIN_DISABLE_ENV = 'OH_MY_OPENCODE_SLIM_DISABLE';
+
+export function isTruthyEnvValue(value: string | undefined): boolean {
+  return TRUTHY_ENV_VALUES.has(value?.trim().toLowerCase() ?? '');
+}
+
+export function isPluginDisabledByEnv(
+  env: NodeJS.ProcessEnv = process.env,
+): boolean {
+  return isTruthyEnvValue(env[PLUGIN_DISABLE_ENV]);
+}