diff --git a/bin/install.js b/bin/install.js index 9d5a58c4e..55a79a64e 100755 --- a/bin/install.js +++ b/bin/install.js @@ -3985,8 +3985,14 @@ function configureOpencodePermissions(isGlobal = true) { } } + // OpenCode also allows a top-level string permission like "allow". + // In that case, path-specific permission entries are unnecessary. + if (typeof config.permission === 'string') { + return; + } + // Ensure permission structure exists - if (!config.permission) { + if (!config.permission || typeof config.permission !== 'object') { config.permission = {}; } @@ -5207,6 +5213,7 @@ if (process.env.GSD_TEST_MODE) { validateHookFields, installSdk, promptSdk, + configureOpencodePermissions, }; } else { diff --git a/tests/opencode-permissions.test.cjs b/tests/opencode-permissions.test.cjs new file mode 100644 index 000000000..a50745b0e --- /dev/null +++ b/tests/opencode-permissions.test.cjs @@ -0,0 +1,72 @@ +/** + * Regression tests for OpenCode permission config handling. + * + * Ensures the installer does not crash when opencode.json uses the valid + * top-level string form: "permission": "allow". + */ + +const { test, describe, afterEach } = require('node:test'); +const assert = require('node:assert'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +process.env.GSD_TEST_MODE = '1'; +const { configureOpencodePermissions } = require('../bin/install.js'); + +const envKeys = ['OPENCODE_CONFIG_DIR', 'OPENCODE_CONFIG', 'XDG_CONFIG_HOME']; +const originalEnv = Object.fromEntries(envKeys.map(key => [key, process.env[key]])); + +function restoreEnv(snapshot) { + for (const key of envKeys) { + if (snapshot[key] === undefined) { + delete process.env[key]; + } else { + process.env[key] = snapshot[key]; + } + } +} + +function createOpencodeConfigDir() { + return fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-opencode-')); +} + +afterEach(() => { + restoreEnv(originalEnv); +}); + +describe('configureOpencodePermissions', () => { + test('does not crash or rewrite top-level string permissions', () => { + const configDir = createOpencodeConfigDir(); + const configPath = path.join(configDir, 'opencode.json'); + const original = JSON.stringify({ + $schema: 'https://opencode.ai/config.json', + permission: 'allow', + skills: { paths: ['/tmp/skills'] }, + }, null, 2) + '\n'; + + fs.writeFileSync(configPath, original); + process.env.OPENCODE_CONFIG_DIR = configDir; + + assert.doesNotThrow(() => configureOpencodePermissions(true)); + assert.strictEqual(fs.readFileSync(configPath, 'utf8'), original); + + fs.rmSync(configDir, { recursive: true, force: true }); + }); + + test('adds path-specific read and external_directory permissions for object configs', () => { + const configDir = createOpencodeConfigDir(); + const configPath = path.join(configDir, 'opencode.json'); + fs.writeFileSync(configPath, JSON.stringify({ permission: {} }, null, 2) + '\n'); + process.env.OPENCODE_CONFIG_DIR = configDir; + + configureOpencodePermissions(true); + + const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + const gsdPath = `${configDir.replace(/\\/g, '/')}/get-shit-done/*`; + assert.strictEqual(config.permission.read[gsdPath], 'allow'); + assert.strictEqual(config.permission.external_directory[gsdPath], 'allow'); + + fs.rmSync(configDir, { recursive: true, force: true }); + }); +});