From c7c1e366256efb233cfa796d2625e5980e87199c Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Mon, 11 May 2026 10:58:24 -0400 Subject: [PATCH] feat: add JoyCode install target --- README.md | 1 + docs/JOYCODE-GUIDE.md | 55 ++++++++++++ manifests/install-modules.json | 54 ++++++++---- schemas/ecc-install-config.schema.json | 3 +- schemas/install-modules.schema.json | 3 +- scripts/install-apply.js | 5 ++ scripts/lib/install-manifests.js | 2 +- scripts/lib/install-targets/helpers.js | 1 + .../lib/install-targets/joycode-project.js | 50 +++++++++++ scripts/lib/install-targets/registry.js | 2 + tests/lib/install-targets.test.js | 86 +++++++++++++++++++ tests/scripts/install-apply.test.js | 34 ++++++++ 12 files changed, 275 insertions(+), 21 deletions(-) create mode 100644 docs/JOYCODE-GUIDE.md create mode 100644 scripts/lib/install-targets/joycode-project.js diff --git a/README.md b/README.md index 9bff6c07..3d96f2bb 100644 --- a/README.md +++ b/README.md @@ -1083,6 +1083,7 @@ Yes. ECC is cross-platform: - **OpenCode**: Full plugin support in `.opencode/`. See [OpenCode Support](#opencode-support). - **Codex**: First-class support for both macOS app and CLI, with adapter drift guards and SessionStart fallback. See PR [#257](https://github.com/affaan-m/everything-claude-code/pull/257). - **Antigravity**: Tightly integrated setup for workflows, skills, and flattened rules in `.agent/`. See [Antigravity Guide](docs/ANTIGRAVITY-GUIDE.md). +- **JoyCode / CodeBuddy**: Project-local selective install adapters for commands, agents, skills, and flattened rules. See [JoyCode Adapter Guide](docs/JOYCODE-GUIDE.md). - **Non-native harnesses**: Manual fallback path for Grok and similar interfaces. See [Manual Adaptation Guide](docs/MANUAL-ADAPTATION-GUIDE.md). - **Claude Code**: Native — this is the primary target. diff --git a/docs/JOYCODE-GUIDE.md b/docs/JOYCODE-GUIDE.md new file mode 100644 index 00000000..596816fd --- /dev/null +++ b/docs/JOYCODE-GUIDE.md @@ -0,0 +1,55 @@ +# JoyCode Adapter Guide + +JoyCode can consume ECC through the selective installer. The adapter installs shared ECC commands, agents, skills, and flattened rules into a project-local `.joycode/` directory. + +## Install + +Preview the install plan: + +```bash +node scripts/install-plan.js --target joycode --profile full +``` + +Apply it to the current project: + +```bash +node scripts/install-apply.js --target joycode --profile full +``` + +For a smaller install, select modules explicitly: + +```bash +node scripts/install-apply.js --target joycode --modules rules-core,commands-core,workflow-quality +``` + +## Layout + +The project adapter writes managed files under: + +```text +.joycode/ + agents/ + commands/ + rules/ + skills/ + mcp-configs/ + scripts/ + ecc-install-state.json +``` + +Rules are flattened into namespaced filenames so a JoyCode project does not receive nested rule directories such as `rules/common/coding-style.md`. Commands, agents, and skills keep the same structure they use elsewhere in ECC. +The full profile also includes shared MCP and setup helper files that other ECC project-local adapters use. + +## Uninstall + +Use ECC's managed uninstall path instead of deleting files by hand: + +```bash +node scripts/uninstall.js --target joycode +``` + +The uninstall command reads `.joycode/ecc-install-state.json` and removes only files that ECC installed. User-created JoyCode files are preserved. + +## Source PR + +This adapter salvages the useful project-local JoyCode intent from stale PR #1429 while replacing the standalone shell installer with ECC's current install-state and uninstall machinery. diff --git a/manifests/install-modules.json b/manifests/install-modules.json index d0ae8ef3..ebeb2c22 100644 --- a/manifests/install-modules.json +++ b/manifests/install-modules.json @@ -12,7 +12,8 @@ "claude", "cursor", "antigravity", - "codebuddy" + "codebuddy", + "joycode" ], "dependencies": [], "defaultInstall": true, @@ -33,7 +34,8 @@ "cursor", "antigravity", "codex", - "codebuddy" + "codebuddy", + "joycode" ], "dependencies": [], "defaultInstall": true, @@ -52,7 +54,8 @@ "cursor", "antigravity", "opencode", - "codebuddy" + "codebuddy", + "joycode" ], "dependencies": [], "defaultInstall": true, @@ -100,7 +103,8 @@ "codex", "gemini", "opencode", - "codebuddy" + "codebuddy", + "joycode" ], "dependencies": [], "defaultInstall": true, @@ -159,7 +163,8 @@ "antigravity", "codex", "opencode", - "codebuddy" + "codebuddy", + "joycode" ], "dependencies": [ "rules-core", @@ -188,7 +193,8 @@ "antigravity", "codex", "opencode", - "codebuddy" + "codebuddy", + "joycode" ], "dependencies": [ "platform-configs" @@ -227,7 +233,8 @@ "antigravity", "codex", "opencode", - "codebuddy" + "codebuddy", + "joycode" ], "dependencies": [ "platform-configs" @@ -262,7 +269,8 @@ "antigravity", "codex", "opencode", - "codebuddy" + "codebuddy", + "joycode" ], "dependencies": [ "workflow-quality" @@ -289,7 +297,8 @@ "antigravity", "codex", "opencode", - "codebuddy" + "codebuddy", + "joycode" ], "dependencies": [ "platform-configs" @@ -320,7 +329,8 @@ "antigravity", "codex", "opencode", - "codebuddy" + "codebuddy", + "joycode" ], "dependencies": [ "platform-configs" @@ -358,7 +368,8 @@ "antigravity", "codex", "opencode", - "codebuddy" + "codebuddy", + "joycode" ], "dependencies": [ "platform-configs" @@ -381,7 +392,8 @@ "antigravity", "codex", "opencode", - "codebuddy" + "codebuddy", + "joycode" ], "dependencies": [ "business-content" @@ -407,7 +419,8 @@ "cursor", "codex", "opencode", - "codebuddy" + "codebuddy", + "joycode" ], "dependencies": [ "platform-configs" @@ -461,7 +474,8 @@ "antigravity", "codex", "opencode", - "codebuddy" + "codebuddy", + "joycode" ], "dependencies": [ "platform-configs" @@ -500,7 +514,8 @@ "antigravity", "codex", "opencode", - "codebuddy" + "codebuddy", + "joycode" ], "dependencies": [ "platform-configs" @@ -529,7 +544,8 @@ "antigravity", "codex", "opencode", - "codebuddy" + "codebuddy", + "joycode" ], "dependencies": [ "platform-configs" @@ -558,7 +574,8 @@ "antigravity", "codex", "opencode", - "codebuddy" + "codebuddy", + "joycode" ], "dependencies": [ "platform-configs" @@ -581,7 +598,8 @@ "antigravity", "codex", "opencode", - "codebuddy" + "codebuddy", + "joycode" ], "dependencies": [ "platform-configs" diff --git a/schemas/ecc-install-config.schema.json b/schemas/ecc-install-config.schema.json index cb866aa3..04bda09b 100644 --- a/schemas/ecc-install-config.schema.json +++ b/schemas/ecc-install-config.schema.json @@ -24,7 +24,8 @@ "codex", "gemini", "opencode", - "codebuddy" + "codebuddy", + "joycode" ] }, "profile": { diff --git a/schemas/install-modules.schema.json b/schemas/install-modules.schema.json index fbaa7faf..f2e5da65 100644 --- a/schemas/install-modules.schema.json +++ b/schemas/install-modules.schema.json @@ -53,7 +53,8 @@ "codex", "gemini", "opencode", - "codebuddy" + "codebuddy", + "joycode" ] } }, diff --git a/scripts/install-apply.js b/scripts/install-apply.js index 3acfe228..e1f48236 100755 --- a/scripts/install-apply.js +++ b/scripts/install-apply.js @@ -31,6 +31,11 @@ Targets: claude (default) - Install ECC into ~/.claude/ with managed rules/skills under rules/ecc and skills/ecc cursor - Install rules, hooks, and bundled Cursor configs to ./.cursor/ antigravity - Install rules, workflows, skills, and agents to ./.agent/ + codex - Install shared agents/config into ~/.codex/ + gemini - Install project-local Gemini config into ./.gemini/ + opencode - Install shared commands/hooks/config into ~/.opencode/ + codebuddy - Install commands, agents, skills, and flattened rules into ./.codebuddy/ + joycode - Install commands, agents, skills, and flattened rules into ./.joycode/ Options: --profile Resolve and install a manifest profile diff --git a/scripts/lib/install-manifests.js b/scripts/lib/install-manifests.js index 631eb111..0f179f25 100644 --- a/scripts/lib/install-manifests.js +++ b/scripts/lib/install-manifests.js @@ -4,7 +4,7 @@ const path = require('path'); const { getInstallTargetAdapter, planInstallTargetScaffold } = require('./install-targets/registry'); const DEFAULT_REPO_ROOT = path.join(__dirname, '../..'); -const SUPPORTED_INSTALL_TARGETS = ['claude', 'cursor', 'antigravity', 'codex', 'gemini', 'opencode', 'codebuddy']; +const SUPPORTED_INSTALL_TARGETS = ['claude', 'cursor', 'antigravity', 'codex', 'gemini', 'opencode', 'codebuddy', 'joycode']; const COMPONENT_FAMILY_PREFIXES = { baseline: 'baseline:', language: 'lang:', diff --git a/scripts/lib/install-targets/helpers.js b/scripts/lib/install-targets/helpers.js index bb9a0648..5b033fe1 100644 --- a/scripts/lib/install-targets/helpers.js +++ b/scripts/lib/install-targets/helpers.js @@ -7,6 +7,7 @@ const PLATFORM_SOURCE_PATH_OWNERS = Object.freeze({ '.codex': 'codex', '.cursor': 'cursor', '.gemini': 'gemini', + '.joycode': 'joycode', '.opencode': 'opencode', '.codebuddy': 'codebuddy', }); diff --git a/scripts/lib/install-targets/joycode-project.js b/scripts/lib/install-targets/joycode-project.js new file mode 100644 index 00000000..8faf3515 --- /dev/null +++ b/scripts/lib/install-targets/joycode-project.js @@ -0,0 +1,50 @@ +const path = require('path'); + +const { + createFlatRuleOperations, + createInstallTargetAdapter, + isForeignPlatformPath, +} = require('./helpers'); + +module.exports = createInstallTargetAdapter({ + id: 'joycode-project', + target: 'joycode', + kind: 'project', + rootSegments: ['.joycode'], + installStatePathSegments: ['ecc-install-state.json'], + nativeRootRelativePath: '.joycode', + planOperations(input, adapter) { + const modules = Array.isArray(input.modules) + ? input.modules + : (input.module ? [input.module] : []); + const { + repoRoot, + projectRoot, + homeDir, + } = input; + const planningInput = { + repoRoot, + projectRoot, + homeDir, + }; + const targetRoot = adapter.resolveRoot(planningInput); + + return modules.flatMap(module => { + const paths = Array.isArray(module.paths) ? module.paths : []; + return paths + .filter(p => !isForeignPlatformPath(p, adapter.target)) + .flatMap(sourceRelativePath => { + if (sourceRelativePath === 'rules') { + return createFlatRuleOperations({ + moduleId: module.id, + repoRoot, + sourceRelativePath, + destinationDir: path.join(targetRoot, 'rules'), + }); + } + + return [adapter.createScaffoldOperation(module.id, sourceRelativePath, planningInput)]; + }); + }); + }, +}); diff --git a/scripts/lib/install-targets/registry.js b/scripts/lib/install-targets/registry.js index 64838608..87db5f65 100644 --- a/scripts/lib/install-targets/registry.js +++ b/scripts/lib/install-targets/registry.js @@ -4,6 +4,7 @@ const codebuddyProject = require('./codebuddy-project'); const codexHome = require('./codex-home'); const cursorProject = require('./cursor-project'); const geminiProject = require('./gemini-project'); +const joycodeProject = require('./joycode-project'); const opencodeHome = require('./opencode-home'); const ADAPTERS = Object.freeze([ @@ -14,6 +15,7 @@ const ADAPTERS = Object.freeze([ geminiProject, opencodeHome, codebuddyProject, + joycodeProject, ]); function listInstallTargetAdapters() { diff --git a/tests/lib/install-targets.test.js b/tests/lib/install-targets.test.js index fe9bc9b2..eac286d9 100644 --- a/tests/lib/install-targets.test.js +++ b/tests/lib/install-targets.test.js @@ -43,6 +43,7 @@ function runTests() { assert.ok(targets.includes('gemini'), 'Should include gemini target'); assert.ok(targets.includes('opencode'), 'Should include opencode target'); assert.ok(targets.includes('codebuddy'), 'Should include codebuddy target'); + assert.ok(targets.includes('joycode'), 'Should include joycode target'); })) passed++; else failed++; if (test('resolves cursor adapter root and install-state path from project root', () => { @@ -501,6 +502,29 @@ function runTests() { assert.ok(byTarget.supports('codebuddy-project')); })) passed++; else failed++; + if (test('resolves joycode adapter root and install-state path from project root', () => { + const adapter = getInstallTargetAdapter('joycode'); + const projectRoot = '/workspace/app'; + const root = adapter.resolveRoot({ projectRoot }); + const statePath = adapter.getInstallStatePath({ projectRoot }); + + assert.strictEqual(adapter.id, 'joycode-project'); + assert.strictEqual(adapter.target, 'joycode'); + assert.strictEqual(adapter.kind, 'project'); + assert.strictEqual(root, path.join(projectRoot, '.joycode')); + assert.strictEqual(statePath, path.join(projectRoot, '.joycode', 'ecc-install-state.json')); + })) passed++; else failed++; + + if (test('joycode adapter supports lookup by target and adapter id', () => { + const byTarget = getInstallTargetAdapter('joycode'); + const byId = getInstallTargetAdapter('joycode-project'); + + assert.strictEqual(byTarget.id, 'joycode-project'); + assert.strictEqual(byId.id, 'joycode-project'); + assert.ok(byTarget.supports('joycode')); + assert.ok(byTarget.supports('joycode-project')); + })) passed++; else failed++; + if (test('plans codebuddy rules with flat namespaced filenames', () => { const repoRoot = path.join(__dirname, '..', '..'); const projectRoot = '/workspace/app'; @@ -536,6 +560,68 @@ function runTests() { ); })) passed++; else failed++; + if (test('plans joycode commands, agents, skills, and flattened rules', () => { + const repoRoot = path.join(__dirname, '..', '..'); + const projectRoot = '/workspace/app'; + + const plan = planInstallTargetScaffold({ + target: 'joycode', + repoRoot, + projectRoot, + modules: [ + { + id: 'rules-core', + paths: ['rules'], + }, + { + id: 'agents-core', + paths: ['agents'], + }, + { + id: 'commands-core', + paths: ['commands'], + }, + { + id: 'workflow-quality', + paths: ['skills/tdd-workflow'], + }, + ], + }); + + assert.strictEqual(plan.adapter.id, 'joycode-project'); + assert.strictEqual(plan.targetRoot, path.join(projectRoot, '.joycode')); + assert.strictEqual(plan.installStatePath, path.join(projectRoot, '.joycode', 'ecc-install-state.json')); + + assert.ok( + plan.operations.some(operation => ( + normalizedRelativePath(operation.sourceRelativePath) === 'rules/common/coding-style.md' + && operation.destinationPath === path.join(projectRoot, '.joycode', 'rules', 'common-coding-style.md') + )), + 'Should flatten common rules into namespaced files for joycode' + ); + assert.ok( + plan.operations.some(operation => ( + normalizedRelativePath(operation.sourceRelativePath) === 'agents' + && operation.destinationPath === path.join(projectRoot, '.joycode', 'agents') + )), + 'Should install agents under .joycode/agents' + ); + assert.ok( + plan.operations.some(operation => ( + normalizedRelativePath(operation.sourceRelativePath) === 'commands' + && operation.destinationPath === path.join(projectRoot, '.joycode', 'commands') + )), + 'Should install commands under .joycode/commands' + ); + assert.ok( + plan.operations.some(operation => ( + normalizedRelativePath(operation.sourceRelativePath) === 'skills/tdd-workflow' + && operation.destinationPath === path.join(projectRoot, '.joycode', 'skills', 'tdd-workflow') + )), + 'Should install skills under .joycode/skills' + ); + })) passed++; else failed++; + if (test('exposes validate and planOperations on codebuddy adapter', () => { const codebuddyAdapter = getInstallTargetAdapter('codebuddy'); diff --git a/tests/scripts/install-apply.test.js b/tests/scripts/install-apply.test.js index fcedff0b..5a941cf7 100644 --- a/tests/scripts/install-apply.test.js +++ b/tests/scripts/install-apply.test.js @@ -233,6 +233,40 @@ function runTests() { } })) passed++; else failed++; + if (test('installs JoyCode profile through managed install-state', () => { + const homeDir = createTempDir('install-apply-home-'); + const projectDir = createTempDir('install-apply-project-'); + + try { + const result = run(['--target', 'joycode', '--profile', 'minimal'], { cwd: projectDir, homeDir }); + assert.strictEqual(result.code, 0, result.stderr); + + assert.ok(fs.existsSync(path.join(projectDir, '.joycode', 'rules', 'common-coding-style.md'))); + assert.ok(!fs.existsSync(path.join(projectDir, '.joycode', 'rules', 'common', 'coding-style.md'))); + assert.ok(fs.existsSync(path.join(projectDir, '.joycode', 'agents', 'architect.md'))); + assert.ok(fs.existsSync(path.join(projectDir, '.joycode', 'commands', 'plan.md'))); + assert.ok(fs.existsSync(path.join(projectDir, '.joycode', 'skills', 'tdd-workflow', 'SKILL.md'))); + assert.ok(fs.existsSync(path.join(projectDir, '.joycode', 'mcp-configs', 'mcp-servers.json'))); + assert.ok(!fs.existsSync(path.join(projectDir, '.joycode', 'hooks'))); + + const statePath = path.join(projectDir, '.joycode', 'ecc-install-state.json'); + const state = readJson(statePath); + assert.strictEqual(state.target.id, 'joycode-project'); + assert.deepStrictEqual(state.request.modules, []); + assert.strictEqual(state.request.profile, 'minimal'); + assert.ok(state.resolution.selectedModules.includes('workflow-quality')); + assert.ok( + state.operations.some(operation => ( + operation.destinationPath.endsWith(path.join('.joycode', 'skills', 'tdd-workflow', 'SKILL.md')) + )), + 'Should record JoyCode skill file operation' + ); + } finally { + cleanup(homeDir); + cleanup(projectDir); + } + })) passed++; else failed++; + if (test('supports dry-run without mutating the target project', () => { const homeDir = createTempDir('install-apply-home-'); const projectDir = createTempDir('install-apply-project-');