diff --git a/.qwen/QWEN.md b/.qwen/QWEN.md new file mode 100644 index 00000000..dd27e3d2 --- /dev/null +++ b/.qwen/QWEN.md @@ -0,0 +1,25 @@ +# Qwen CLI Configuration + +This directory contains ECC's Qwen CLI install template. + +## Runtime Location + +The source `.qwen/` directory in this repository is copied into a user's home-level `~/.qwen/` install root when running: + +```bash +./install.sh --target qwen --profile minimal +``` + +The managed install also writes `~/.qwen/ecc-install-state.json` so future ECC updates and uninstalls can distinguish ECC-owned files from user-owned Qwen configuration. + +## Installed Surface + +The Qwen target installs the same managed manifest modules used by other harness adapters: + +- `rules/` +- `agents/` +- `commands/` +- `skills/` +- `mcp-configs/` + +Hook runtime files are intentionally not selected for Qwen until the Qwen hook/event contract is verified. diff --git a/README.md b/README.md index 3d96f2bb..07afc741 100644 --- a/README.md +++ b/README.md @@ -1084,6 +1084,7 @@ Yes. ECC is cross-platform: - **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). +- **Qwen CLI**: Home-directory selective install adapter for commands, agents, skills, rules, and Qwen config. See [Qwen CLI Adapter Guide](docs/QWEN-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/QWEN-GUIDE.md b/docs/QWEN-GUIDE.md new file mode 100644 index 00000000..5a3ee0bd --- /dev/null +++ b/docs/QWEN-GUIDE.md @@ -0,0 +1,54 @@ +# Qwen CLI Adapter Guide + +ECC can install its managed command, agent, skill, rule, and MCP surfaces into the Qwen CLI home directory. + +## Install + +From the ECC repository root: + +```bash +./install.sh --target qwen --profile minimal +``` + +Preview a larger install before copying files: + +```bash +./install.sh --target qwen --profile full --dry-run +``` + +The Qwen adapter writes into `~/.qwen/` and records managed file ownership in `~/.qwen/ecc-install-state.json`. + +## Installed Layout + +The managed install can populate: + +```text +~/.qwen/ + QWEN.md + agents/ + commands/ + mcp-configs/ + rules/ + skills/ + ecc-install-state.json +``` + +The installer preserves the source layout for rules, so language rule sets stay under paths such as `~/.qwen/rules/common/` and `~/.qwen/rules/typescript/`. + +## Updating + +Rerun the same install command after pulling ECC updates. The installer uses the install-state file to update ECC-managed files without claiming unrelated user files in `~/.qwen/`. + +## Uninstalling + +Use the managed uninstall path rather than deleting the whole Qwen directory: + +```bash +node scripts/uninstall.js --target qwen +``` + +That removes files recorded in `~/.qwen/ecc-install-state.json` and leaves unrelated Qwen configuration alone. + +## Scope + +This target is intentionally narrower than stale PR #1352. It ports the maintainable Qwen install-target intent onto the current selective installer and avoids unverified hook-runtime claims until Qwen's hook/event contract is confirmed. diff --git a/manifests/install-modules.json b/manifests/install-modules.json index ebeb2c22..dfe0929a 100644 --- a/manifests/install-modules.json +++ b/manifests/install-modules.json @@ -13,7 +13,8 @@ "cursor", "antigravity", "codebuddy", - "joycode" + "joycode", + "qwen" ], "dependencies": [], "defaultInstall": true, @@ -35,7 +36,8 @@ "antigravity", "codex", "codebuddy", - "joycode" + "joycode", + "qwen" ], "dependencies": [], "defaultInstall": true, @@ -55,7 +57,8 @@ "antigravity", "opencode", "codebuddy", - "joycode" + "joycode", + "qwen" ], "dependencies": [], "defaultInstall": true, @@ -92,6 +95,7 @@ ".cursor", ".gemini", ".opencode", + ".qwen", "mcp-configs", "scripts/auto-update.js", "scripts/setup-package-manager.js" @@ -104,7 +108,8 @@ "gemini", "opencode", "codebuddy", - "joycode" + "joycode", + "qwen" ], "dependencies": [], "defaultInstall": true, @@ -164,7 +169,8 @@ "codex", "opencode", "codebuddy", - "joycode" + "joycode", + "qwen" ], "dependencies": [ "rules-core", @@ -194,7 +200,8 @@ "codex", "opencode", "codebuddy", - "joycode" + "joycode", + "qwen" ], "dependencies": [ "platform-configs" @@ -234,7 +241,8 @@ "codex", "opencode", "codebuddy", - "joycode" + "joycode", + "qwen" ], "dependencies": [ "platform-configs" @@ -270,7 +278,8 @@ "codex", "opencode", "codebuddy", - "joycode" + "joycode", + "qwen" ], "dependencies": [ "workflow-quality" @@ -298,7 +307,8 @@ "codex", "opencode", "codebuddy", - "joycode" + "joycode", + "qwen" ], "dependencies": [ "platform-configs" @@ -330,7 +340,8 @@ "codex", "opencode", "codebuddy", - "joycode" + "joycode", + "qwen" ], "dependencies": [ "platform-configs" @@ -369,7 +380,8 @@ "codex", "opencode", "codebuddy", - "joycode" + "joycode", + "qwen" ], "dependencies": [ "platform-configs" @@ -393,7 +405,8 @@ "codex", "opencode", "codebuddy", - "joycode" + "joycode", + "qwen" ], "dependencies": [ "business-content" @@ -420,7 +433,8 @@ "codex", "opencode", "codebuddy", - "joycode" + "joycode", + "qwen" ], "dependencies": [ "platform-configs" @@ -475,7 +489,8 @@ "codex", "opencode", "codebuddy", - "joycode" + "joycode", + "qwen" ], "dependencies": [ "platform-configs" @@ -515,7 +530,8 @@ "codex", "opencode", "codebuddy", - "joycode" + "joycode", + "qwen" ], "dependencies": [ "platform-configs" @@ -545,7 +561,8 @@ "codex", "opencode", "codebuddy", - "joycode" + "joycode", + "qwen" ], "dependencies": [ "platform-configs" @@ -575,7 +592,8 @@ "codex", "opencode", "codebuddy", - "joycode" + "joycode", + "qwen" ], "dependencies": [ "platform-configs" @@ -599,7 +617,8 @@ "codex", "opencode", "codebuddy", - "joycode" + "joycode", + "qwen" ], "dependencies": [ "platform-configs" diff --git a/package.json b/package.json index 139ecf2c..e67b02b6 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ ".cursor/", ".gemini/", ".opencode/", + ".qwen/", ".mcp.json", "AGENTS.md", "VERSION", diff --git a/schemas/ecc-install-config.schema.json b/schemas/ecc-install-config.schema.json index 04bda09b..33d1558c 100644 --- a/schemas/ecc-install-config.schema.json +++ b/schemas/ecc-install-config.schema.json @@ -25,7 +25,8 @@ "gemini", "opencode", "codebuddy", - "joycode" + "joycode", + "qwen" ] }, "profile": { diff --git a/schemas/install-modules.schema.json b/schemas/install-modules.schema.json index f2e5da65..012e4868 100644 --- a/schemas/install-modules.schema.json +++ b/schemas/install-modules.schema.json @@ -54,7 +54,8 @@ "gemini", "opencode", "codebuddy", - "joycode" + "joycode", + "qwen" ] } }, diff --git a/scripts/install-apply.js b/scripts/install-apply.js index e1f48236..5cb32134 100755 --- a/scripts/install-apply.js +++ b/scripts/install-apply.js @@ -36,6 +36,7 @@ Targets: 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/ + qwen - Install commands, agents, skills, rules, and Qwen config into ~/.qwen/ Options: --profile Resolve and install a manifest profile diff --git a/scripts/lib/install-manifests.js b/scripts/lib/install-manifests.js index 0f179f25..b5623b11 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', 'joycode']; +const SUPPORTED_INSTALL_TARGETS = ['claude', 'cursor', 'antigravity', 'codex', 'gemini', 'opencode', 'codebuddy', 'joycode', 'qwen']; 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 5b033fe1..a7a39663 100644 --- a/scripts/lib/install-targets/helpers.js +++ b/scripts/lib/install-targets/helpers.js @@ -10,6 +10,7 @@ const PLATFORM_SOURCE_PATH_OWNERS = Object.freeze({ '.joycode': 'joycode', '.opencode': 'opencode', '.codebuddy': 'codebuddy', + '.qwen': 'qwen', }); function normalizeRelativePath(relativePath) { diff --git a/scripts/lib/install-targets/qwen-home.js b/scripts/lib/install-targets/qwen-home.js new file mode 100644 index 00000000..96981d7b --- /dev/null +++ b/scripts/lib/install-targets/qwen-home.js @@ -0,0 +1,10 @@ +const { createInstallTargetAdapter } = require('./helpers'); + +module.exports = createInstallTargetAdapter({ + id: 'qwen-home', + target: 'qwen', + kind: 'home', + rootSegments: ['.qwen'], + installStatePathSegments: ['ecc-install-state.json'], + nativeRootRelativePath: '.qwen', +}); diff --git a/scripts/lib/install-targets/registry.js b/scripts/lib/install-targets/registry.js index 87db5f65..f7c4f44e 100644 --- a/scripts/lib/install-targets/registry.js +++ b/scripts/lib/install-targets/registry.js @@ -6,6 +6,7 @@ const cursorProject = require('./cursor-project'); const geminiProject = require('./gemini-project'); const joycodeProject = require('./joycode-project'); const opencodeHome = require('./opencode-home'); +const qwenHome = require('./qwen-home'); const ADAPTERS = Object.freeze([ claudeHome, @@ -16,6 +17,7 @@ const ADAPTERS = Object.freeze([ opencodeHome, codebuddyProject, joycodeProject, + qwenHome, ]); function listInstallTargetAdapters() { diff --git a/tests/lib/install-manifests.test.js b/tests/lib/install-manifests.test.js index 7c2ee5bc..ea9d5b1f 100644 --- a/tests/lib/install-manifests.test.js +++ b/tests/lib/install-manifests.test.js @@ -246,6 +246,31 @@ function runTests() { assert.ok(plan.operations.length > 0, 'Should include install operations'); })) passed++; else failed++; + if (test('resolves Qwen minimal profile while leaving hooks out', () => { + const homeDir = '/Users/example'; + const plan = resolveInstallPlan({ + profileId: 'minimal', + target: 'qwen', + homeDir, + }); + + assert.deepStrictEqual( + plan.selectedModuleIds, + ['rules-core', 'agents-core', 'commands-core', 'platform-configs', 'workflow-quality'] + ); + assert.deepStrictEqual(plan.skippedModuleIds, []); + assert.strictEqual(plan.targetAdapterId, 'qwen-home'); + assert.strictEqual(plan.targetRoot, path.join(homeDir, '.qwen')); + assert.ok( + plan.operations.some(operation => operation.sourceRelativePath === '.qwen'), + 'Should install Qwen native config' + ); + assert.ok( + !plan.operations.some(operation => operation.destinationPath.includes(`${path.sep}hooks`)), + 'Qwen minimal profile should not install hook runtime files' + ); + })) passed++; else failed++; + if (test('resolves explicit modules with dependency expansion', () => { const plan = resolveInstallPlan({ moduleIds: ['security'] }); assert.ok(plan.selectedModuleIds.includes('security'), 'Should include requested module'); diff --git a/tests/lib/install-targets.test.js b/tests/lib/install-targets.test.js index eac286d9..c5b3b7ce 100644 --- a/tests/lib/install-targets.test.js +++ b/tests/lib/install-targets.test.js @@ -44,6 +44,7 @@ function runTests() { 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'); + assert.ok(targets.includes('qwen'), 'Should include qwen target'); })) passed++; else failed++; if (test('resolves cursor adapter root and install-state path from project root', () => { @@ -525,6 +526,29 @@ function runTests() { assert.ok(byTarget.supports('joycode-project')); })) passed++; else failed++; + if (test('resolves qwen adapter root and install-state path from home dir', () => { + const adapter = getInstallTargetAdapter('qwen'); + const homeDir = '/Users/example'; + const root = adapter.resolveRoot({ homeDir }); + const statePath = adapter.getInstallStatePath({ homeDir }); + + assert.strictEqual(adapter.id, 'qwen-home'); + assert.strictEqual(adapter.target, 'qwen'); + assert.strictEqual(adapter.kind, 'home'); + assert.strictEqual(root, path.join(homeDir, '.qwen')); + assert.strictEqual(statePath, path.join(homeDir, '.qwen', 'ecc-install-state.json')); + })) passed++; else failed++; + + if (test('qwen adapter supports lookup by target and adapter id', () => { + const byTarget = getInstallTargetAdapter('qwen'); + const byId = getInstallTargetAdapter('qwen-home'); + + assert.strictEqual(byTarget.id, 'qwen-home'); + assert.strictEqual(byId.id, 'qwen-home'); + assert.ok(byTarget.supports('qwen')); + assert.ok(byTarget.supports('qwen-home')); + })) passed++; else failed++; + if (test('plans codebuddy rules with flat namespaced filenames', () => { const repoRoot = path.join(__dirname, '..', '..'); const projectRoot = '/workspace/app'; @@ -622,6 +646,69 @@ function runTests() { ); })) passed++; else failed++; + if (test('plans qwen commands, agents, skills, and native config under home root', () => { + const repoRoot = path.join(__dirname, '..', '..'); + const homeDir = '/Users/example'; + + const plan = planInstallTargetScaffold({ + target: 'qwen', + repoRoot, + homeDir, + modules: [ + { + id: 'rules-core', + paths: ['rules'], + }, + { + id: 'agents-core', + paths: ['agents'], + }, + { + id: 'commands-core', + paths: ['commands'], + }, + { + id: 'platform-configs', + paths: ['.qwen', '.gemini', 'mcp-configs'], + }, + { + id: 'workflow-quality', + paths: ['skills/tdd-workflow'], + }, + ], + }); + + assert.strictEqual(plan.adapter.id, 'qwen-home'); + assert.strictEqual(plan.targetRoot, path.join(homeDir, '.qwen')); + assert.strictEqual(plan.installStatePath, path.join(homeDir, '.qwen', 'ecc-install-state.json')); + assert.ok( + plan.operations.some(operation => ( + normalizedRelativePath(operation.sourceRelativePath) === 'rules' + && operation.destinationPath === path.join(homeDir, '.qwen', 'rules') + )), + 'Should preserve rules under ~/.qwen/rules' + ); + assert.ok( + plan.operations.some(operation => ( + normalizedRelativePath(operation.sourceRelativePath) === '.qwen' + && operation.destinationPath === path.join(homeDir, '.qwen') + && operation.strategy === 'sync-root-children' + )), + 'Should sync Qwen native config into ~/.qwen' + ); + assert.ok( + !plan.operations.some(operation => normalizedRelativePath(operation.sourceRelativePath) === '.gemini'), + 'Should skip foreign platform config paths' + ); + assert.ok( + plan.operations.some(operation => ( + normalizedRelativePath(operation.sourceRelativePath) === 'skills/tdd-workflow' + && operation.destinationPath === path.join(homeDir, '.qwen', 'skills', 'tdd-workflow') + )), + 'Should install skills under ~/.qwen/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 5a941cf7..c9aac651 100644 --- a/tests/scripts/install-apply.test.js +++ b/tests/scripts/install-apply.test.js @@ -267,6 +267,40 @@ function runTests() { } })) passed++; else failed++; + if (test('installs Qwen profile through managed home install-state', () => { + const homeDir = createTempDir('install-apply-home-'); + const projectDir = createTempDir('install-apply-project-'); + + try { + const result = run(['--target', 'qwen', '--profile', 'minimal'], { cwd: projectDir, homeDir }); + assert.strictEqual(result.code, 0, result.stderr); + + assert.ok(fs.existsSync(path.join(homeDir, '.qwen', 'QWEN.md'))); + assert.ok(fs.existsSync(path.join(homeDir, '.qwen', 'rules', 'common', 'coding-style.md'))); + assert.ok(fs.existsSync(path.join(homeDir, '.qwen', 'agents', 'architect.md'))); + assert.ok(fs.existsSync(path.join(homeDir, '.qwen', 'commands', 'plan.md'))); + assert.ok(fs.existsSync(path.join(homeDir, '.qwen', 'skills', 'tdd-workflow', 'SKILL.md'))); + assert.ok(fs.existsSync(path.join(homeDir, '.qwen', 'mcp-configs', 'mcp-servers.json'))); + assert.ok(!fs.existsSync(path.join(homeDir, '.qwen', 'hooks'))); + + const statePath = path.join(homeDir, '.qwen', 'ecc-install-state.json'); + const state = readJson(statePath); + assert.strictEqual(state.target.id, 'qwen-home'); + 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('.qwen', 'skills', 'tdd-workflow', 'SKILL.md')) + )), + 'Should record Qwen 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-'); diff --git a/tests/scripts/npm-publish-surface.test.js b/tests/scripts/npm-publish-surface.test.js index ae1ed738..bd4bc1e8 100644 --- a/tests/scripts/npm-publish-surface.test.js +++ b/tests/scripts/npm-publish-surface.test.js @@ -111,6 +111,7 @@ function main() { "scripts/catalog.js", "scripts/consult.js", ".gemini/GEMINI.md", + ".qwen/QWEN.md", ".claude-plugin/plugin.json", ".codex-plugin/plugin.json", "schemas/install-state.schema.json",