feat: add Kilo CLI runtime support
This commit is contained in:
@@ -36,6 +36,7 @@ describe('getDirName (Antigravity)', () => {
|
||||
assert.strictEqual(getDirName('claude'), '.claude');
|
||||
assert.strictEqual(getDirName('opencode'), '.opencode');
|
||||
assert.strictEqual(getDirName('gemini'), '.gemini');
|
||||
assert.strictEqual(getDirName('kilo'), '.kilo');
|
||||
assert.strictEqual(getDirName('codex'), '.codex');
|
||||
assert.strictEqual(getDirName('copilot'), '.github');
|
||||
});
|
||||
@@ -97,6 +98,7 @@ describe('getConfigDirFromHome (Antigravity)', () => {
|
||||
test('does not change other runtimes', () => {
|
||||
assert.strictEqual(getConfigDirFromHome('claude', true), "'.claude'");
|
||||
assert.strictEqual(getConfigDirFromHome('gemini', true), "'.gemini'");
|
||||
assert.strictEqual(getConfigDirFromHome('kilo', true), "'.config', 'kilo'");
|
||||
assert.strictEqual(getConfigDirFromHome('copilot', true), "'.copilot'");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,6 +44,7 @@ describe('getDirName (Copilot)', () => {
|
||||
assert.strictEqual(getDirName('claude'), '.claude');
|
||||
assert.strictEqual(getDirName('opencode'), '.opencode');
|
||||
assert.strictEqual(getDirName('gemini'), '.gemini');
|
||||
assert.strictEqual(getDirName('kilo'), '.kilo');
|
||||
assert.strictEqual(getDirName('codex'), '.codex');
|
||||
});
|
||||
});
|
||||
@@ -109,6 +110,7 @@ describe('getConfigDirFromHome (Copilot)', () => {
|
||||
assert.strictEqual(getConfigDirFromHome('opencode', true), "'.config', 'opencode'");
|
||||
assert.strictEqual(getConfigDirFromHome('claude', true), "'.claude'");
|
||||
assert.strictEqual(getConfigDirFromHome('gemini', true), "'.gemini'");
|
||||
assert.strictEqual(getConfigDirFromHome('kilo', true), "'.config', 'kilo'");
|
||||
assert.strictEqual(getConfigDirFromHome('codex', true), "'.codex'");
|
||||
});
|
||||
});
|
||||
@@ -137,8 +139,8 @@ describe('Source code integration (Copilot)', () => {
|
||||
assert.ok(src.includes('--copilot'), 'help text has --copilot option');
|
||||
});
|
||||
|
||||
test('CLI-02: promptRuntime runtimeMap has Copilot as option 5', () => {
|
||||
assert.ok(src.includes("'5': 'copilot'"), 'runtimeMap has 5 -> copilot');
|
||||
test('CLI-02: promptRuntime runtimeMap has Copilot as option 6', () => {
|
||||
assert.ok(src.includes("'6': 'copilot'"), 'runtimeMap has 6 -> copilot');
|
||||
});
|
||||
|
||||
test('CLI-02: promptRuntime allRuntimes array includes copilot', () => {
|
||||
|
||||
252
tests/kilo-install.test.cjs
Normal file
252
tests/kilo-install.test.cjs
Normal file
@@ -0,0 +1,252 @@
|
||||
/**
|
||||
* GSD Tools Tests - Kilo Install Plumbing
|
||||
*
|
||||
* Tests for Kilo runtime directory resolution, config paths,
|
||||
* permission config, and installer source integration.
|
||||
*/
|
||||
|
||||
process.env.GSD_TEST_MODE = '1';
|
||||
|
||||
const { test, describe, beforeEach, afterEach } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
|
||||
const { createTempProject, cleanup } = require('./helpers.cjs');
|
||||
const {
|
||||
getDirName,
|
||||
getGlobalDir,
|
||||
getConfigDirFromHome,
|
||||
resolveKiloConfigPath,
|
||||
configureKiloPermissions,
|
||||
} = require('../bin/install.js');
|
||||
|
||||
describe('getDirName (Kilo)', () => {
|
||||
test('returns .kilo for kilo', () => {
|
||||
assert.strictEqual(getDirName('kilo'), '.kilo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConfigDirFromHome (Kilo)', () => {
|
||||
test('returns .kilo for local installs', () => {
|
||||
assert.strictEqual(getConfigDirFromHome('kilo', false), "'.kilo'");
|
||||
});
|
||||
|
||||
test('returns .config, kilo for global installs', () => {
|
||||
assert.strictEqual(getConfigDirFromHome('kilo', true), "'.config', 'kilo'");
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGlobalDir (Kilo)', () => {
|
||||
let savedEnv;
|
||||
|
||||
beforeEach(() => {
|
||||
savedEnv = {
|
||||
KILO_CONFIG_DIR: process.env.KILO_CONFIG_DIR,
|
||||
KILO_CONFIG: process.env.KILO_CONFIG,
|
||||
XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME,
|
||||
};
|
||||
|
||||
delete process.env.KILO_CONFIG_DIR;
|
||||
delete process.env.KILO_CONFIG;
|
||||
delete process.env.XDG_CONFIG_HOME;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (savedEnv.KILO_CONFIG_DIR === undefined) {
|
||||
delete process.env.KILO_CONFIG_DIR;
|
||||
} else {
|
||||
process.env.KILO_CONFIG_DIR = savedEnv.KILO_CONFIG_DIR;
|
||||
}
|
||||
|
||||
if (savedEnv.KILO_CONFIG === undefined) {
|
||||
delete process.env.KILO_CONFIG;
|
||||
} else {
|
||||
process.env.KILO_CONFIG = savedEnv.KILO_CONFIG;
|
||||
}
|
||||
|
||||
if (savedEnv.XDG_CONFIG_HOME === undefined) {
|
||||
delete process.env.XDG_CONFIG_HOME;
|
||||
} else {
|
||||
process.env.XDG_CONFIG_HOME = savedEnv.XDG_CONFIG_HOME;
|
||||
}
|
||||
});
|
||||
|
||||
test('returns ~/.config/kilo by default', () => {
|
||||
assert.strictEqual(getGlobalDir('kilo'), path.join(os.homedir(), '.config', 'kilo'));
|
||||
});
|
||||
|
||||
test('respects KILO_CONFIG_DIR env var', () => {
|
||||
process.env.KILO_CONFIG_DIR = '~/custom-kilo';
|
||||
assert.strictEqual(getGlobalDir('kilo'), path.join(os.homedir(), 'custom-kilo'));
|
||||
});
|
||||
|
||||
test('falls back to XDG_CONFIG_HOME/kilo', () => {
|
||||
process.env.XDG_CONFIG_HOME = '~/xdg-config';
|
||||
assert.strictEqual(getGlobalDir('kilo'), path.join(os.homedir(), 'xdg-config', 'kilo'));
|
||||
});
|
||||
|
||||
test('ignores KILO_CONFIG file env var and uses XDG/default resolution only', () => {
|
||||
process.env.KILO_CONFIG = '/tmp/ignored-kilo.json';
|
||||
assert.strictEqual(getGlobalDir('kilo'), path.join(os.homedir(), '.config', 'kilo'));
|
||||
});
|
||||
|
||||
test('explicit config-dir overrides env vars', () => {
|
||||
process.env.KILO_CONFIG_DIR = '~/from-env';
|
||||
process.env.XDG_CONFIG_HOME = '~/xdg-config';
|
||||
assert.strictEqual(getGlobalDir('kilo', '/explicit/kilo'), '/explicit/kilo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Kilo config file helpers', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject('gsd-kilo-');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('resolveKiloConfigPath prefers kilo.jsonc when present', () => {
|
||||
const configDir = path.join(tmpDir, '.kilo');
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(configDir, 'kilo.jsonc'), '{\n // comment\n}\n');
|
||||
|
||||
assert.strictEqual(resolveKiloConfigPath(configDir), path.join(configDir, 'kilo.jsonc'));
|
||||
});
|
||||
|
||||
test('resolveKiloConfigPath falls back to kilo.json', () => {
|
||||
const configDir = path.join(tmpDir, '.kilo');
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
|
||||
assert.strictEqual(resolveKiloConfigPath(configDir), path.join(configDir, 'kilo.json'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('configureKiloPermissions', () => {
|
||||
let tmpDir;
|
||||
let configDir;
|
||||
let savedEnv;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject('gsd-kilo-perms-');
|
||||
configDir = path.join(tmpDir, '.config', 'kilo');
|
||||
savedEnv = {
|
||||
KILO_CONFIG_DIR: process.env.KILO_CONFIG_DIR,
|
||||
XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME,
|
||||
};
|
||||
process.env.KILO_CONFIG_DIR = configDir;
|
||||
delete process.env.XDG_CONFIG_HOME;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (savedEnv.KILO_CONFIG_DIR === undefined) {
|
||||
delete process.env.KILO_CONFIG_DIR;
|
||||
} else {
|
||||
process.env.KILO_CONFIG_DIR = savedEnv.KILO_CONFIG_DIR;
|
||||
}
|
||||
|
||||
if (savedEnv.XDG_CONFIG_HOME === undefined) {
|
||||
delete process.env.XDG_CONFIG_HOME;
|
||||
} else {
|
||||
process.env.XDG_CONFIG_HOME = savedEnv.XDG_CONFIG_HOME;
|
||||
}
|
||||
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('writes GSD permissions to kilo.json when config is missing', () => {
|
||||
configureKiloPermissions(true);
|
||||
|
||||
const configPath = path.join(configDir, 'kilo.json');
|
||||
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');
|
||||
});
|
||||
|
||||
test('updates existing kilo.jsonc configs via JSONC parsing', () => {
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
const configPath = path.join(configDir, 'kilo.jsonc');
|
||||
fs.writeFileSync(configPath, '{\n // existing config\n "permission": {\n "bash": "ask",\n },\n}\n');
|
||||
|
||||
configureKiloPermissions(true);
|
||||
|
||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
const gsdPath = `${configDir.replace(/\\/g, '/')}/get-shit-done/*`;
|
||||
|
||||
assert.strictEqual(config.permission.bash, 'ask');
|
||||
assert.strictEqual(config.permission.read[gsdPath], 'allow');
|
||||
assert.strictEqual(config.permission.external_directory[gsdPath], 'allow');
|
||||
});
|
||||
|
||||
test('writes permissions to an explicit config dir argument', () => {
|
||||
const explicitDir = path.join(tmpDir, 'custom-kilo-config');
|
||||
|
||||
configureKiloPermissions(true, explicitDir);
|
||||
|
||||
const configPath = path.join(explicitDir, 'kilo.json');
|
||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
const gsdPath = `${explicitDir.replace(/\\/g, '/')}/get-shit-done/*`;
|
||||
|
||||
assert.strictEqual(config.permission.read[gsdPath], 'allow');
|
||||
assert.strictEqual(config.permission.external_directory[gsdPath], 'allow');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Source code integration (Kilo)', () => {
|
||||
const src = fs.readFileSync(path.join(__dirname, '..', 'bin', 'install.js'), 'utf8');
|
||||
const updateWorkflowSrc = fs.readFileSync(path.join(__dirname, '..', 'get-shit-done', 'workflows', 'update.md'), 'utf8');
|
||||
const reapplyPatchesSrc = fs.readFileSync(path.join(__dirname, '..', 'commands', 'gsd', 'reapply-patches.md'), 'utf8');
|
||||
|
||||
test('--kilo flag parsing exists', () => {
|
||||
assert.ok(src.includes("args.includes('--kilo')"), '--kilo flag parsed');
|
||||
});
|
||||
|
||||
test('help text includes --kilo', () => {
|
||||
assert.ok(src.includes('Install for Kilo only'), 'help text includes Kilo option');
|
||||
});
|
||||
|
||||
test('--all array includes kilo', () => {
|
||||
assert.ok(src.includes("'kilo'"), '--all includes kilo runtime');
|
||||
});
|
||||
|
||||
test('promptRuntime runtimeMap has Kilo as option 4', () => {
|
||||
assert.ok(src.includes("'4': 'kilo'"), 'runtimeMap has 4 -> kilo');
|
||||
});
|
||||
|
||||
test('hooks are skipped for Kilo', () => {
|
||||
assert.ok(src.includes('!isOpencode && !isKilo'), 'hooks skip check includes kilo');
|
||||
});
|
||||
|
||||
test('settings.json write excludes Kilo', () => {
|
||||
assert.ok(src.includes('!isCodex && !isCopilot && !isKilo && !isCursor && !isWindsurf'), 'settings write excludes kilo');
|
||||
});
|
||||
|
||||
test('agent path replacement does not exclude Kilo', () => {
|
||||
assert.ok(src.includes('if (!isCopilot && !isAntigravity)'), 'generic agent path replacement still applies');
|
||||
});
|
||||
|
||||
test('finishInstall passes the actual config dir to Kilo permissions', () => {
|
||||
assert.ok(src.includes('configureKiloPermissions(isGlobal, configDir);'), 'Kilo permission config uses actual install dir');
|
||||
});
|
||||
|
||||
test('uninstall cleans Kilo permissions from the resolved target dir', () => {
|
||||
assert.ok(src.includes('const configPath = resolveKiloConfigPath(targetDir);'), 'Kilo uninstall cleanup uses targetDir');
|
||||
});
|
||||
|
||||
test('update workflow checks preferred custom config dirs before defaults', () => {
|
||||
assert.ok(updateWorkflowSrc.includes('PREFERRED_CONFIG_DIR'), 'workflow tracks preferred config dir');
|
||||
assert.ok(updateWorkflowSrc.includes('kilo.jsonc'), 'workflow infers Kilo from config files');
|
||||
assert.ok(updateWorkflowSrc.includes('ENV_RUNTIME_DIRS'), 'workflow checks env-derived config dirs');
|
||||
});
|
||||
|
||||
test('reapply-patches checks Kilo custom config env vars first', () => {
|
||||
assert.ok(reapplyPatchesSrc.includes('KILO_CONFIG_DIR'), 'reapply-patches checks KILO_CONFIG_DIR');
|
||||
assert.ok(reapplyPatchesSrc.includes('XDG_CONFIG_HOME'), 'reapply-patches checks XDG_CONFIG_HOME');
|
||||
});
|
||||
});
|
||||
@@ -21,13 +21,14 @@ const runtimeMap = {
|
||||
'1': 'claude',
|
||||
'2': 'opencode',
|
||||
'3': 'gemini',
|
||||
'4': 'codex',
|
||||
'5': 'copilot',
|
||||
'6': 'antigravity',
|
||||
'7': 'cursor',
|
||||
'8': 'windsurf'
|
||||
'4': 'kilo',
|
||||
'5': 'codex',
|
||||
'6': 'copilot',
|
||||
'7': 'antigravity',
|
||||
'8': 'cursor',
|
||||
'9': 'windsurf'
|
||||
};
|
||||
const allRuntimes = ['claude', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf'];
|
||||
const allRuntimes = ['claude', 'opencode', 'gemini', 'kilo', 'codex', 'copilot', 'antigravity', 'cursor', 'windsurf'];
|
||||
|
||||
/**
|
||||
* Simulate the parsing logic from promptRuntime without requiring readline.
|
||||
@@ -36,7 +37,7 @@ const allRuntimes = ['claude', 'opencode', 'gemini', 'codex', 'copilot', 'antigr
|
||||
function parseRuntimeInput(input) {
|
||||
input = input.trim() || '1';
|
||||
|
||||
if (input === '9') {
|
||||
if (input === '10') {
|
||||
return allRuntimes;
|
||||
}
|
||||
|
||||
@@ -55,31 +56,33 @@ function parseRuntimeInput(input) {
|
||||
describe('multi-runtime selection parsing', () => {
|
||||
test('single choice returns single runtime', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('1'), ['claude']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('4'), ['codex']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('7'), ['cursor']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('4'), ['kilo']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('5'), ['codex']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('8'), ['cursor']);
|
||||
});
|
||||
|
||||
test('comma-separated choices return multiple runtimes', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('1,4,6'), ['claude', 'codex', 'antigravity']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('1,5,7'), ['claude', 'codex', 'antigravity']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('2,3'), ['opencode', 'gemini']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('2,4'), ['opencode', 'kilo']);
|
||||
});
|
||||
|
||||
test('space-separated choices return multiple runtimes', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('1 4 6'), ['claude', 'codex', 'antigravity']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('5 7'), ['copilot', 'cursor']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('1 5 7'), ['claude', 'codex', 'antigravity']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('6 8'), ['copilot', 'cursor']);
|
||||
});
|
||||
|
||||
test('mixed comma and space separators work', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('1, 4, 6'), ['claude', 'codex', 'antigravity']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('2 , 5'), ['opencode', 'copilot']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('1, 5, 7'), ['claude', 'codex', 'antigravity']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('2 , 6'), ['opencode', 'copilot']);
|
||||
});
|
||||
|
||||
test('single choice for windsurf', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('8'), ['windsurf']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('9'), ['windsurf']);
|
||||
});
|
||||
|
||||
test('choice 9 returns all runtimes', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('9'), allRuntimes);
|
||||
test('choice 10 returns all runtimes', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('10'), allRuntimes);
|
||||
});
|
||||
|
||||
test('empty input defaults to claude', () => {
|
||||
@@ -88,29 +91,29 @@ describe('multi-runtime selection parsing', () => {
|
||||
});
|
||||
|
||||
test('invalid choices are ignored, falls back to claude if all invalid', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('10'), ['claude']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('11'), ['claude']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('0'), ['claude']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('abc'), ['claude']);
|
||||
});
|
||||
|
||||
test('invalid choices mixed with valid are filtered out', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('1,10,4'), ['claude', 'codex']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('1,11,5'), ['claude', 'codex']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('abc 3 xyz'), ['gemini']);
|
||||
});
|
||||
|
||||
test('duplicate choices are deduplicated', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('1,1,1'), ['claude']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('4,4,6,6'), ['codex', 'antigravity']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('5,5,7,7'), ['codex', 'antigravity']);
|
||||
});
|
||||
|
||||
test('preserves selection order', () => {
|
||||
assert.deepStrictEqual(parseRuntimeInput('6,1,4'), ['antigravity', 'claude', 'codex']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('7,2,5'), ['cursor', 'opencode', 'copilot']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('7,1,5'), ['antigravity', 'claude', 'codex']);
|
||||
assert.deepStrictEqual(parseRuntimeInput('8,2,6'), ['cursor', 'opencode', 'copilot']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('install.js source contains multi-select support', () => {
|
||||
test('runtimeMap is defined with all 8 runtimes', () => {
|
||||
test('runtimeMap is defined with all 9 runtimes', () => {
|
||||
for (const [key, name] of Object.entries(runtimeMap)) {
|
||||
assert.ok(
|
||||
installSrc.includes(`'${key}': '${name}'`),
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* Runtime Converter Tests — OpenCode + Gemini
|
||||
* Runtime Converter Tests — OpenCode + Kilo + Gemini
|
||||
*
|
||||
* Tests for small runtime-specific conversion functions from install.js.
|
||||
* Larger runtime test suites (Copilot, Codex, Antigravity) have their own files.
|
||||
*
|
||||
* OpenCode: convertClaudeToOpencodeFrontmatter (agent + command modes)
|
||||
* model: inherit is NOT added (OpenCode doesn't support it — see #1156)
|
||||
* but mode: subagent IS added (required by OpenCode agents).
|
||||
* OpenCode/Kilo: flat-runtime frontmatter converters (agent + command modes)
|
||||
* model: inherit is NOT added (runtime uses its configured default model)
|
||||
* but mode: subagent IS added (required by both runtimes' agents).
|
||||
* Gemini: convertClaudeToGeminiAgent (frontmatter + tool mapping + body escaping)
|
||||
*/
|
||||
|
||||
@@ -16,6 +16,7 @@ const assert = require('node:assert');
|
||||
process.env.GSD_TEST_MODE = '1';
|
||||
const {
|
||||
convertClaudeToOpencodeFrontmatter,
|
||||
convertClaudeToKiloFrontmatter,
|
||||
convertClaudeToGeminiAgent,
|
||||
neutralizeAgentReferences,
|
||||
} = require('../bin/install.js');
|
||||
@@ -52,66 +53,80 @@ allowed-tools:
|
||||
|
||||
Execute the phase plan.`;
|
||||
|
||||
describe('OpenCode agent conversion (isAgent: true)', () => {
|
||||
test('keeps name: field for agents', () => {
|
||||
const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(frontmatter.includes('name: gsd-executor'), 'name: should be preserved for agents');
|
||||
});
|
||||
const flatRuntimeSuites = [
|
||||
{
|
||||
label: 'OpenCode',
|
||||
convert: convertClaudeToOpencodeFrontmatter,
|
||||
configDir: '.config/opencode',
|
||||
},
|
||||
{
|
||||
label: 'Kilo',
|
||||
convert: convertClaudeToKiloFrontmatter,
|
||||
configDir: '.config/kilo',
|
||||
},
|
||||
];
|
||||
|
||||
test('does not add model: inherit (OpenCode does not support it)', () => {
|
||||
const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(!frontmatter.includes('model: inherit'), 'model: inherit should NOT be added — OpenCode throws ProviderModelNotFoundError');
|
||||
});
|
||||
for (const { label, convert, configDir } of flatRuntimeSuites) {
|
||||
describe(`${label} agent conversion (isAgent: true)`, () => {
|
||||
test('keeps name: field for agents', () => {
|
||||
const result = convert(SAMPLE_AGENT, { isAgent: true });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(frontmatter.includes('name: gsd-executor'), 'name: should be preserved for agents');
|
||||
});
|
||||
|
||||
test('adds mode: subagent', () => {
|
||||
const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(frontmatter.includes('mode: subagent'), 'mode: subagent should be added');
|
||||
});
|
||||
test('does not add model: inherit', () => {
|
||||
const result = convert(SAMPLE_AGENT, { isAgent: true });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(!frontmatter.includes('model: inherit'), 'model: inherit should NOT be added');
|
||||
});
|
||||
|
||||
test('strips tools: field', () => {
|
||||
const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(!frontmatter.includes('tools:'), 'tools: should be stripped for agents');
|
||||
assert.ok(!frontmatter.includes('read: true'), 'tools object should not be generated');
|
||||
});
|
||||
test('adds mode: subagent', () => {
|
||||
const result = convert(SAMPLE_AGENT, { isAgent: true });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(frontmatter.includes('mode: subagent'), 'mode: subagent should be added');
|
||||
});
|
||||
|
||||
test('strips skills: array', () => {
|
||||
const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(!frontmatter.includes('skills:'), 'skills: should be stripped');
|
||||
assert.ok(!frontmatter.includes('gsd-executor-workflow'), 'skill entries should be stripped');
|
||||
});
|
||||
test('strips tools: field', () => {
|
||||
const result = convert(SAMPLE_AGENT, { isAgent: true });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(!frontmatter.includes('tools:'), 'tools: should be stripped for agents');
|
||||
assert.ok(!frontmatter.includes('read: true'), 'tools object should not be generated');
|
||||
});
|
||||
|
||||
test('strips color: field', () => {
|
||||
const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(!frontmatter.includes('color:'), 'color: should be stripped for agents');
|
||||
});
|
||||
test('strips skills: array', () => {
|
||||
const result = convert(SAMPLE_AGENT, { isAgent: true });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(!frontmatter.includes('skills:'), 'skills: should be stripped');
|
||||
assert.ok(!frontmatter.includes('gsd-executor-workflow'), 'skill entries should be stripped');
|
||||
});
|
||||
|
||||
test('strips commented hooks block', () => {
|
||||
const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(!frontmatter.includes('# hooks:'), 'commented hooks should be stripped');
|
||||
assert.ok(!frontmatter.includes('PostToolUse'), 'hook content should be stripped');
|
||||
});
|
||||
test('strips color: field', () => {
|
||||
const result = convert(SAMPLE_AGENT, { isAgent: true });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(!frontmatter.includes('color:'), 'color: should be stripped for agents');
|
||||
});
|
||||
|
||||
test('keeps description: field', () => {
|
||||
const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(frontmatter.includes('description: Executes GSD plans'), 'description should be kept');
|
||||
});
|
||||
test('strips commented hooks block', () => {
|
||||
const result = convert(SAMPLE_AGENT, { isAgent: true });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(!frontmatter.includes('# hooks:'), 'commented hooks should be stripped');
|
||||
assert.ok(!frontmatter.includes('PostToolUse'), 'hook content should be stripped');
|
||||
});
|
||||
|
||||
test('preserves body content', () => {
|
||||
const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true });
|
||||
assert.ok(result.includes('<role>'), 'body should be preserved');
|
||||
assert.ok(result.includes('You are a GSD plan executor.'), 'body content should be intact');
|
||||
});
|
||||
test('keeps description: field', () => {
|
||||
const result = convert(SAMPLE_AGENT, { isAgent: true });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(frontmatter.includes('description: Executes GSD plans'), 'description should be kept');
|
||||
});
|
||||
|
||||
test('applies body text replacements', () => {
|
||||
const agentWithClaudePaths = `---
|
||||
test('preserves body content', () => {
|
||||
const result = convert(SAMPLE_AGENT, { isAgent: true });
|
||||
assert.ok(result.includes('<role>'), 'body should be preserved');
|
||||
assert.ok(result.includes('You are a GSD plan executor.'), 'body content should be intact');
|
||||
});
|
||||
|
||||
test('applies body text replacements', () => {
|
||||
const agentWithClaudePaths = `---
|
||||
name: test-agent
|
||||
description: Test
|
||||
tools: Read
|
||||
@@ -120,32 +135,33 @@ tools: Read
|
||||
Read ~/.claude/agent-memory/ for context.
|
||||
Use $HOME/.claude/skills/ for reference.`;
|
||||
|
||||
const result = convertClaudeToOpencodeFrontmatter(agentWithClaudePaths, { isAgent: true });
|
||||
assert.ok(result.includes('~/.config/opencode/agent-memory/'), '~/.claude should be replaced');
|
||||
assert.ok(result.includes('$HOME/.config/opencode/skills/'), '$HOME/.claude should be replaced');
|
||||
});
|
||||
});
|
||||
|
||||
describe('OpenCode command conversion (isAgent: false, default)', () => {
|
||||
test('strips name: field for commands', () => {
|
||||
const result = convertClaudeToOpencodeFrontmatter(SAMPLE_COMMAND);
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(!frontmatter.includes('name:'), 'name: should be stripped for commands');
|
||||
const result = convert(agentWithClaudePaths, { isAgent: true });
|
||||
assert.ok(result.includes(`~/${configDir}/agent-memory/`), '~/.claude should be replaced');
|
||||
assert.ok(result.includes(`$HOME/${configDir}/skills/`), '$HOME/.claude should be replaced');
|
||||
});
|
||||
});
|
||||
|
||||
test('does not add model: or mode: for commands', () => {
|
||||
const result = convertClaudeToOpencodeFrontmatter(SAMPLE_COMMAND);
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(!frontmatter.includes('model:'), 'model: should not be added for commands');
|
||||
assert.ok(!frontmatter.includes('mode:'), 'mode: should not be added for commands');
|
||||
});
|
||||
describe(`${label} command conversion (isAgent: false, default)`, () => {
|
||||
test('strips name: field for commands', () => {
|
||||
const result = convert(SAMPLE_COMMAND);
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(!frontmatter.includes('name:'), 'name: should be stripped for commands');
|
||||
});
|
||||
|
||||
test('keeps description: for commands', () => {
|
||||
const result = convertClaudeToOpencodeFrontmatter(SAMPLE_COMMAND);
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(frontmatter.includes('description:'), 'description should be kept');
|
||||
test('does not add model: or mode: for commands', () => {
|
||||
const result = convert(SAMPLE_COMMAND);
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(!frontmatter.includes('model:'), 'model: should not be added for commands');
|
||||
assert.ok(!frontmatter.includes('mode:'), 'mode: should not be added for commands');
|
||||
});
|
||||
|
||||
test('keeps description: for commands', () => {
|
||||
const result = convert(SAMPLE_COMMAND);
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(frontmatter.includes('description:'), 'description should be kept');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Gemini CLI agent conversion (merged from gemini-config.test.cjs)
|
||||
|
||||
Reference in New Issue
Block a user