Conversation
Closes #29 - Add AiTool type and AI_TOOLS registry with CLI metadata per tool - Persist tool selection from setup to .mex/config.json - Load saved aiTools in findConfig and pass through MexConfig - Sync now dispatches to the correct CLI (claude, opencode run, codex) - Tools without a CLI (Cursor, Windsurf, Copilot) fall back to prompts mode - Replace hardcoded "Claude" strings in sync UI and cli help text - Add tests for config persistence (save/load aiTools)
af89487 to
3a53d7c
Compare
There was a problem hiding this comment.
Pull request overview
This PR makes mex sync tool-agnostic by persisting the user’s AI tool selection from mex setup and dispatching sync to the correct CLI tool (or falling back to prompt-only mode when no supported CLI is available), addressing Issue #29.
Changes:
- Add persisted AI tool selection (
aiTools) to the mex config model and save/load it via.mex/config.json. - Update
mex syncto select an installed CLI tool from the configured set and run it interactively, otherwise fall back to “show prompts”. - Add/extend tests for config persistence and update CLI help text to remove Claude-specific wording.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| test/config.test.ts | Adds tests for loading/saving persisted aiTools selection. |
| src/types.ts | Introduces AiTool/AI_TOOLS metadata and extends MexConfig with aiTools. |
| src/sync/index.ts | Implements tool selection + dispatch to the selected tool’s CLI (or prompt fallback). |
| src/setup/index.ts | Persists tool choice from setup into mex config. |
| src/config.ts | Loads/saves aiTools from a persisted JSON config file. |
| src/cli.ts | Updates command help text to be tool-agnostic (“AI” vs “Claude”). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| aiTools?: AiTool[]; | ||
| } | ||
|
|
||
| function loadAiTools(scaffoldRoot: string): AiTool[] { | ||
| const configPath = resolve(scaffoldRoot, CONFIG_FILE); | ||
| if (!existsSync(configPath)) return []; | ||
| try { | ||
| const raw = JSON.parse(readFileSync(configPath, "utf-8")) as MexPersistedConfig; | ||
| return Array.isArray(raw.aiTools) ? raw.aiTools : []; |
There was a problem hiding this comment.
loadAiTools() only checks that aiTools is an array, but doesn’t validate each entry. If config.json contains an unknown string (or non-string), later code like AI_TOOLS[t] in sync can be undefined and cause a runtime crash when reading meta.cli. Consider filtering to known tools (e.g., Object.keys(AI_TOOLS)) and dropping invalid entries when loading.
| aiTools?: AiTool[]; | |
| } | |
| function loadAiTools(scaffoldRoot: string): AiTool[] { | |
| const configPath = resolve(scaffoldRoot, CONFIG_FILE); | |
| if (!existsSync(configPath)) return []; | |
| try { | |
| const raw = JSON.parse(readFileSync(configPath, "utf-8")) as MexPersistedConfig; | |
| return Array.isArray(raw.aiTools) ? raw.aiTools : []; | |
| aiTools?: unknown; | |
| } | |
| function isAiTool(value: unknown): value is AiTool { | |
| return typeof value === "string"; | |
| } | |
| function loadAiTools(scaffoldRoot: string): AiTool[] { | |
| const configPath = resolve(scaffoldRoot, CONFIG_FILE); | |
| if (!existsSync(configPath)) return []; | |
| try { | |
| const raw = JSON.parse(readFileSync(configPath, "utf-8")) as MexPersistedConfig; | |
| return Array.isArray(raw.aiTools) ? raw.aiTools.filter(isAiTool) : []; |
| const CONFIG_FILE = "config.json"; | ||
|
|
||
| interface MexPersistedConfig { | ||
| aiTools?: AiTool[]; | ||
| } | ||
|
|
||
| function loadAiTools(scaffoldRoot: string): AiTool[] { | ||
| const configPath = resolve(scaffoldRoot, CONFIG_FILE); | ||
| if (!existsSync(configPath)) return []; | ||
| try { | ||
| const raw = JSON.parse(readFileSync(configPath, "utf-8")) as MexPersistedConfig; |
There was a problem hiding this comment.
Config persistence is resolved relative to scaffoldRoot (resolve(scaffoldRoot, "config.json")). When findScaffoldRoot() returns projectRoot (the context/ layout), this becomes <projectRoot>/config.json, which is a very generic filename and can easily collide with an app’s own config.json (and it also differs from the PR description of .mex/config.json). Consider always reading/writing under a mex-specific path (e.g. <projectRoot>/.mex/config.json or a hidden mex-specific filename) regardless of scaffold layout.
| export function saveAiTools(scaffoldRoot: string, tools: AiTool[]): void { | ||
| const configPath = resolve(scaffoldRoot, CONFIG_FILE); | ||
| let existing: MexPersistedConfig = {}; | ||
| if (existsSync(configPath)) { | ||
| try { | ||
| existing = JSON.parse(readFileSync(configPath, "utf-8")) as MexPersistedConfig; | ||
| } catch { /* start fresh */ } | ||
| } | ||
| existing.aiTools = tools; | ||
| mkdirSync(dirname(configPath), { recursive: true }); | ||
| writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n"); |
There was a problem hiding this comment.
saveAiTools() assumes JSON.parse() returns an object. If config.json is valid JSON but not an object (e.g. an array/number/string), existing.aiTools = tools will throw at runtime. Consider guarding existing to a plain object (non-null, typeof object, not Array) before assigning, and also consider de-duplicating tools to avoid repeated entries from multi-select input.
| function hasCliTool(cmd: string): boolean { | ||
| try { | ||
| execSync(`which ${cmd}`, { stdio: "ignore" }); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } |
There was a problem hiding this comment.
hasCliTool() uses execSync(which ${cmd}), which is shell-based and not portable (e.g. Windows typically uses where). Even though cmd currently comes from constants, using execFileSync/spawnSync with shell:false (and selecting which vs where by platform) would make this safer and more reliable across environments.
| // Persist tool selection | ||
| if (selectedTools.length > 0 && !dryRun) { | ||
| const mexDir = resolve(projectRoot, ".mex"); | ||
| saveAiTools(mexDir, selectedTools); | ||
| } | ||
|
|
||
| return selectedClaude; |
There was a problem hiding this comment.
MexConfig now requires aiTools, but later in this file the config object passed into runScan is still created as { projectRoot, scaffoldRoot: mexDir } (missing aiTools), which will fail npm run typecheck and makes the types inconsistent. Consider including aiTools there (e.g. selectedTools/[]) or changing runScan to accept a narrower config type if it only needs projectRoot.
Summary
Closes #29
mex syncno longer hardcodes Claude Code as the only AI toolmex setupis now persisted to.mex/config.jsonclaude,opencode run,codex)Test plan
mex setupand select OpenCode → verify.mex/config.jsonhas"aiTools": ["opencode"]mex sync→ verify it shows "OpenCode" in the menu instead of "Claude"mex setupwith multiple tools → verify sync lets you pick between themmex syncwith no CLI tool configured → verify it falls back to prompts mode