✨ feat(cli): add interactive update command for skill and config updates (#10)
Existing users who update the npm package often forget to update the agent skill files, leaving their AI agent working with stale workflow guidance (sizing rules, color palettes, anti-patterns). The new `update` subcommand solves this by detecting and updating all skill installations automatically. 🔧 Interactive update wizard: - Scan all agent directories (Cursor, Claude Code, Codex CLI) across global and local scopes for existing excalidraw-skill installations - Batch-update found installations with a single Y/n confirmation - Offer to install the skill for detected agents that don't have it yet - Optionally re-apply MCP config (defaults to no for non-breaking updates) 📝 Documentation: - Rewrite README "Updating" section to lead with the interactive command - Add collapsible example session showing the update flow - Retain manual update methods as fallback for each installation path 🎯 Ensures skill files stay in sync with MCP tools across releases, preventing the subtle drift where new tool capabilities exist but the agent's workflow guidance doesn't reference them.
This commit is contained in:
@@ -41,6 +41,7 @@ Click the workspace badge to switch between isolated canvases — each workspace
|
||||
- [Quick Start](#quick-start)
|
||||
- [Configuration](#configuration)
|
||||
- [Verify Installation](#verify-installation)
|
||||
- [Updating](#updating)
|
||||
- [How We Differ from the Official Excalidraw MCP](#how-we-differ-from-the-official-excalidraw-mcp)
|
||||
- [What Changed From Upstream](#what-changed-from-upstream)
|
||||
- [Architecture](#architecture)
|
||||
@@ -280,6 +281,119 @@ open http://localhost:3000
|
||||
|
||||
If the health check fails, see [Troubleshooting](#troubleshooting).
|
||||
|
||||
## Updating
|
||||
|
||||
Already installed a previous version? The interactive update wizard is the easiest way to update everything — MCP server **and** agent skills — in one go.
|
||||
|
||||
### Interactive Update (recommended)
|
||||
|
||||
```bash
|
||||
npx @sanjibdevnath/mcp-excalidraw-local@latest update
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Example session</summary>
|
||||
|
||||
```
|
||||
$ npx @sanjibdevnath/mcp-excalidraw-local@latest update
|
||||
|
||||
Excalidraw MCP — Update v1.2.0
|
||||
|
||||
[1/2] Skill Update
|
||||
|
||||
Found 2 existing skill installation(s):
|
||||
[1] Cursor (global) — ~/.cursor/skills/excalidraw-skill
|
||||
[2] Claude Code (global) — ~/.claude/skills/excalidraw-skill
|
||||
|
||||
Update all 2 installation(s) to v1.2.0? [Y/n]: Y
|
||||
✔ Updated Cursor (global) — ~/.cursor/skills/excalidraw-skill
|
||||
✔ Updated Claude Code (global) — ~/.claude/skills/excalidraw-skill
|
||||
|
||||
2/2 skill(s) updated.
|
||||
|
||||
[2/2] MCP Configuration
|
||||
Re-apply MCP server config? (overwrites existing entry) [y/N]: N
|
||||
MCP config unchanged.
|
||||
|
||||
Update complete! Restart your MCP client to pick up changes.
|
||||
```
|
||||
</details>
|
||||
|
||||
The update wizard:
|
||||
1. **Finds all existing skill installations** across Cursor, Claude Code, and Codex CLI (both global and local scopes)
|
||||
2. **Updates them in-place** with the latest skill files (SKILL.md, cheatsheet, geometric-thinking reference, helper scripts)
|
||||
3. **Offers to install** the skill for any detected agent that doesn't have it yet
|
||||
4. **Optionally re-applies MCP config** if needed
|
||||
|
||||
> **Why this matters:** The agent skill contains workflow guidance, sizing rules, color palettes, and anti-patterns that evolve alongside the MCP tools. Updating the MCP server without updating the skill means your AI agent is working with stale instructions.
|
||||
|
||||
### Manual Update by Installation Method
|
||||
|
||||
If you prefer to update manually, follow the steps for your installation method, then restart your MCP client.
|
||||
|
||||
#### npx users
|
||||
|
||||
If your MCP config uses `npx -y @sanjibdevnath/mcp-excalidraw-local`, npx caches the package locally and won't automatically fetch new versions.
|
||||
|
||||
**Option A — Clear the cache (one-time):**
|
||||
```bash
|
||||
npm cache clean --force
|
||||
```
|
||||
Then restart your MCP client. npx will download the latest version on next launch.
|
||||
|
||||
**Option B — Pin to `@latest` in your MCP config (permanent fix):**
|
||||
|
||||
Update the `args` in your MCP config to include `@latest`:
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"excalidraw-canvas": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@sanjibdevnath/mcp-excalidraw-local@latest"],
|
||||
"env": { "CANVAS_PORT": "3000" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This ensures npx always checks for the newest published version.
|
||||
|
||||
#### From-source users
|
||||
|
||||
```bash
|
||||
cd mcp-excalidraw-local
|
||||
git pull origin main
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
#### Docker users
|
||||
|
||||
```bash
|
||||
docker pull sanjibdevnath/mcp-excalidraw-local:latest
|
||||
docker pull sanjibdevnath/mcp-excalidraw-local-canvas:latest
|
||||
```
|
||||
|
||||
Then recreate your containers (`docker compose up -d` or `docker run` again).
|
||||
|
||||
#### Updating the agent skill manually
|
||||
|
||||
If you skipped the interactive update, copy the skill files yourself:
|
||||
```bash
|
||||
cp -R skills/excalidraw-skill ~/.cursor/skills/excalidraw-skill
|
||||
cp -R skills/excalidraw-skill ~/.claude/skills/excalidraw-skill
|
||||
```
|
||||
|
||||
### Verify the update
|
||||
|
||||
```bash
|
||||
# Check the running version
|
||||
curl -s http://localhost:3000/health
|
||||
|
||||
# Or check the installed package version
|
||||
npx @sanjibdevnath/mcp-excalidraw-local --version
|
||||
```
|
||||
|
||||
## How We Differ from the Official Excalidraw MCP
|
||||
|
||||
Excalidraw now has an [official MCP](https://github.com/excalidraw/excalidraw-mcp) — it's great for quick, prompt-to-diagram generation rendered inline in chat. We solve a different problem.
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"production": "npm run build && npm run canvas",
|
||||
"prepublishOnly": "npm run build",
|
||||
"setup": "node dist/index.js setup",
|
||||
"update": "node dist/index.js update",
|
||||
"type-check": "npx tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
|
||||
+6
-1
@@ -2727,12 +2727,17 @@ if (isMainModule()) {
|
||||
process.stderr.write(`Setup failed: ${(error as Error).message}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
} else if (arg === 'update') {
|
||||
import('./setup.js').then(m => m.runUpdate()).catch(error => {
|
||||
process.stderr.write(`Update failed: ${(error as Error).message}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
} else if (arg === '--help' || arg === '-h' || arg === '--version' || arg === '-v') {
|
||||
const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
||||
if (arg === '--help' || arg === '-h') {
|
||||
process.stdout.write(`${pkg.name} v${pkg.version}\n\nUsage:\n mcp-excalidraw-local Start MCP server (stdio transport)\n mcp-excalidraw-local setup Interactive setup wizard\n mcp-excalidraw-local --help Show this help\n mcp-excalidraw-local --version Show version\n`);
|
||||
process.stdout.write(`${pkg.name} v${pkg.version}\n\nUsage:\n mcp-excalidraw-local Start MCP server (stdio transport)\n mcp-excalidraw-local setup Interactive setup wizard\n mcp-excalidraw-local update Update agent skills and MCP config\n mcp-excalidraw-local --help Show this help\n mcp-excalidraw-local --version Show version\n`);
|
||||
} else {
|
||||
process.stdout.write(`${pkg.version}\n`);
|
||||
}
|
||||
|
||||
+161
@@ -346,6 +346,167 @@ function printManualConfig(): void {
|
||||
`);
|
||||
}
|
||||
|
||||
// ── Update ───────────────────────────────────────────────────
|
||||
|
||||
interface SkillInstallation {
|
||||
agent: AgentDef;
|
||||
scope: 'global' | 'local';
|
||||
path: string;
|
||||
exists: boolean;
|
||||
}
|
||||
|
||||
function getPackageVersion(): string {
|
||||
try {
|
||||
const pkgPath = path.resolve(__dirname, '..', 'package.json');
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
||||
return pkg.version ?? 'unknown';
|
||||
} catch {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
function findExistingSkillInstalls(): SkillInstallation[] {
|
||||
const agents = getAgents();
|
||||
const installs: SkillInstallation[] = [];
|
||||
for (const agent of agents) {
|
||||
for (const scope of ['global', 'local'] as const) {
|
||||
const skillDir = path.join(agent.skillBasePaths[scope], 'excalidraw-skill');
|
||||
installs.push({
|
||||
agent,
|
||||
scope,
|
||||
path: skillDir,
|
||||
exists: fs.existsSync(path.join(skillDir, 'SKILL.md')),
|
||||
});
|
||||
}
|
||||
}
|
||||
return installs;
|
||||
}
|
||||
|
||||
export async function runUpdate(): Promise<void> {
|
||||
if (!process.stdin.isTTY) {
|
||||
process.stderr.write('Error: Update requires an interactive terminal.\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
const version = getPackageVersion();
|
||||
process.stdout.write(`\n ${BOLD}Excalidraw MCP — Update${RESET} ${DIM}v${version}${RESET}\n`);
|
||||
|
||||
try {
|
||||
// ── Phase 1: Detect existing skill installations ──────────
|
||||
heading('1/2', 'Skill Update');
|
||||
|
||||
const allInstalls = findExistingSkillInstalls();
|
||||
const existing = allInstalls.filter(i => i.exists);
|
||||
const missing = allInstalls.filter(i => !i.exists);
|
||||
const detectedAgents = detectInstalledAgents();
|
||||
|
||||
const skillSource = path.resolve(__dirname, '..', 'skills', 'excalidraw-skill');
|
||||
if (!fs.existsSync(skillSource)) {
|
||||
fail(`Skill source not found at ${skillSource}`);
|
||||
fail('This can happen with corrupted installs. Try: npx @sanjibdevnath/mcp-excalidraw-local@latest setup');
|
||||
rl.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (existing.length > 0) {
|
||||
process.stdout.write(`\n Found ${CYAN}${existing.length}${RESET} existing skill installation(s):\n`);
|
||||
existing.forEach((inst, i) => {
|
||||
const label = `${inst.agent.name} (${inst.scope})`;
|
||||
process.stdout.write(` ${CYAN}[${i + 1}]${RESET} ${label} — ${DIM}${inst.path}${RESET}\n`);
|
||||
});
|
||||
|
||||
const doUpdate = await confirm(rl, `\n Update all ${existing.length} installation(s) to v${version}?`);
|
||||
if (doUpdate) {
|
||||
let updated = 0;
|
||||
for (const inst of existing) {
|
||||
try {
|
||||
copyDirSync(skillSource, inst.path);
|
||||
ok(`Updated ${inst.agent.name} (${inst.scope}) — ${inst.path}`);
|
||||
updated++;
|
||||
} catch (err) {
|
||||
fail(`Failed to update ${inst.path}: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
process.stdout.write(`\n ${GREEN}${updated}/${existing.length}${RESET} skill(s) updated.\n`);
|
||||
} else {
|
||||
info(`${DIM}Skipped skill update.${RESET}`);
|
||||
}
|
||||
} else {
|
||||
info('No existing skill installations found.');
|
||||
}
|
||||
|
||||
// Offer to install for detected agents that don't have the skill
|
||||
const agentsWithoutSkill = detectedAgents.filter(agent =>
|
||||
!existing.some(inst => inst.agent.name === agent.name),
|
||||
);
|
||||
|
||||
if (agentsWithoutSkill.length > 0) {
|
||||
process.stdout.write(`\n Agents without the skill:\n`);
|
||||
agentsWithoutSkill.forEach((a, i) => {
|
||||
process.stdout.write(` ${YELLOW}[${i + 1}]${RESET} ${a.name}\n`);
|
||||
});
|
||||
|
||||
const doInstall = await confirm(rl, 'Install the skill for these agents?');
|
||||
if (doInstall) {
|
||||
for (const agent of agentsWithoutSkill) {
|
||||
const scopeAnswer = await ask(rl, `\n ${agent.name} — scope? [G]lobal / [l]ocal: `);
|
||||
const scope = scopeAnswer.trim().toLowerCase() === 'l' ? 'local' : 'global';
|
||||
const destDir = path.join(agent.skillBasePaths[scope], 'excalidraw-skill');
|
||||
|
||||
try {
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
copyDirSync(skillSource, destDir);
|
||||
ok(`Installed to ${destDir}`);
|
||||
} catch (err) {
|
||||
fail(`Failed to install to ${destDir}: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Phase 2: MCP config check ────────────────────────────
|
||||
heading('2/2', 'MCP Configuration');
|
||||
|
||||
const wantConfig = await confirm(rl, 'Re-apply MCP server config? (overwrites existing entry)', false);
|
||||
if (wantConfig) {
|
||||
for (const agent of detectedAgents) {
|
||||
if (agent.mcpConfigType === 'json-file' && agent.mcpConfigPath) {
|
||||
const doIt = await confirm(rl, `${agent.name} — update ${agent.mcpConfigPath}?`);
|
||||
if (doIt) {
|
||||
try {
|
||||
mergeJsonConfig(agent.mcpConfigPath);
|
||||
ok(`Updated 'excalidraw-canvas' in ${agent.mcpConfigPath}`);
|
||||
} catch (err) {
|
||||
fail(`Failed: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
} else if (agent.mcpConfigType === 'cli-command' && agent.mcpCliCommand) {
|
||||
const doIt = await confirm(rl, `${agent.name} — re-register via CLI?`);
|
||||
if (doIt) {
|
||||
try {
|
||||
execSync(agent.mcpCliCommand, { stdio: 'inherit' });
|
||||
ok(`Re-registered 'excalidraw-canvas' via ${agent.name} CLI`);
|
||||
} catch (err) {
|
||||
fail(`CLI registration failed: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
info(`${DIM}MCP config unchanged.${RESET}`);
|
||||
}
|
||||
|
||||
process.stdout.write(`\n ${GREEN}${BOLD}Update complete!${RESET} Restart your MCP client to pick up changes.\n\n`);
|
||||
} finally {
|
||||
rl.close();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Main ─────────────────────────────────────────────────────
|
||||
|
||||
export async function runSetup(): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user