8.4 KiB
AI Tools A2 Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Install and update AI coding CLIs from Settings → AI Tools, keep the catalog fresh from the newest release, and show real icons.
Architecture: ai-tools.js learns two catalog fields and a third catalog tier (remote). Four small server modules own one job each: ai-tool-jobs.js (the one-at-a-time install/update runner), ai-tool-versions.js (npm-managed check + version badge), ai-tools-sync.js (daily remote catalog), ai-tool-icons.js (favicon cache). routes/ai-tools.js exposes them; the client polls one job endpoint.
Tech Stack: Node/Express (node:test), React + Vite + vitest.
Spec: docs/superpowers/specs/2026-09-28-ai-tools-a2-design.md
Global Constraints
- Commands come only from catalog files;
install/update/launcherare refused inDASHBOARD_AI_TOOLS_DIRfiles; allowed in built-in and remote (release) files. package:^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$; scripturlhttps, last two host labels equal thehomepageUrlhost's.- Every exec is an argv (
execFile/spawn), never a shell string; stdin ignored; job timeout 10 min; log cap 256 KB; one job server-wide (409 EBUSY). - npm is resolved on the login shell's own PATH first (
shellPath()), job envPATH = shellPath() + ":" + process.env.PATH. - npm Update only for npm-managed tools (binary realpath under
npm prefix -g), elseENOTNPM. - Remote catalog only when release tag > running
package.jsonversion;AGENTHUB_AI_TOOLS_SYNC=0|false|offdisables it; stored in<getDataDir()>/runtime/ai-tools-remote/. - Icons: no SVG; ≤ 64 KB; dotted hostname, no IP literal; headers
nosniff,Content-Security-Policy: sandbox; stored in<getDataDir()>/runtime/ai-tool-icons/. - All writes behind
sameOriginGuard. - Every new/edited source file starts with the header ending
@author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>. - Server tests:
npm run test:server; client:cd client && NODE_ENV=test npx vitest run. Never commit or push without the user's explicit ask.
Task 1: Catalog fields, trust tiers, shell PATH, repo URL
Files: Modify server/lib/ai-tools.js, server/lib/shell-path.js, server/lib/update-check.js, eight files in server/data/ai-tools/ (§ 1 table), server/data/ai-tools/README.md. Tests: server/__tests__/ai-tools.test.js, shell-path.test.js, update-check.test.js.
Produces:
-
Tool gains
install: {kind:"npm",package}|{kind:"script",url}|null,update: {kind:"npm"}|{kind:"script"}|{kind:"self",args:string[]}|null;source∈builtin|remote|custom. -
__setCatalogDirs([builtin, remote, custom])(a two-element array keeps meaning[builtin, custom]for the A1 tests). -
view()rows gaininstall,update;view().catalogfromai-tools-sync.catalogMeta()(Task 3; until then{source:"builtin"}). -
shellPath(): string|nullinshell-path.js— the last PATH the shell printed. -
repoUrlFor(root): Promise<string|null>inupdate-check.js. -
Failing tests: bad package name / http url / off-host url /
update:npmwithout npm install /installin custom dir → file skipped;launcherallowed in remote tier; precedence custom › remote › builtin;shellPath()null before hydration, set after;repoUrlForfor a plugin-cache root uses the marketplace manifest. -
Implement; add fields to codex, gemini, qwen-code, opencode, copilot, amp, cursor, claude.
-
npm run test:servergreen.
Task 2: Jobs and versions
Files: Create server/lib/ai-tool-jobs.js, server/lib/ai-tool-versions.js. Tests: server/__tests__/ai-tool-jobs.test.js, ai-tool-versions.test.js.
Produces:
-
ai-tool-versions.js:npmBin(): string|null(which("npm") overshellPath()then PATH);npmManaged(id): Promise<boolean>;versions({fresh}): Promise<{[id]:{npmManaged,installed,latest,updateAvailable}}>(24 h cache);invalidate(id);__setExec(fn)seam (fn(file,args,opts) → Promise<{stdout}>). -
ai-tool-jobs.js:start(id, action)→ job (throwsENOTOOL|ENOINSTALL|EINSTALLED|ENOUPDATE|ENOTINSTALLED|ENOTNPM|EBUSY|ENOTHING);startUpdateAll();current(): Job|null;stepsFor(id, action): Promise<Step[]>whereStep = {argv:string[]} | {script:url};__setSpawn(fn),__setFetch(fn)seams. -
Job end: hydrate,
aiTools.refresh(), install ok ⇒setEnabled(id,true),versions.invalidate(id). -
Failing tests: argv for npm install/update is
[npm,"install","-g","pkg@latest"]with npm from the shell PATH ahead of PATH; self is[detectedPath,...args]; script downloads (≤1 MB, https final URL, same two-label host) and runs["bash", tmp]then deletes it;EBUSYwhile running; log capped at 256 KB keeping the tail; timeout ⇒ failed; EACCES ⇒ hint line; ok install ⇒ enabled; update-all steps = npm-managed with update + self updaters,ENOTHINGwhen none; npm-managed by realpath undernpm prefix -g; version parse from noisy--version; compare viaupdate-check.compareVersions; offline ⇒ no badge. -
Implement.
-
npm run test:servergreen.
Task 3: Remote catalog sync
Files: Create server/lib/ai-tools-sync.js; modify server/index.js (startBackgroundServices: first sync 60 s after start, then every 24 h, unref). Test: server/__tests__/ai-tools-sync.test.js.
Produces: syncOnce({root, fetch?}): Promise<{ok, reason?, tag?}>; remoteDir(): string; catalogMeta(): {source, tag, fetchedAt}; activeRemoteDir(): string|null (null when disabled, absent, or its tag is not newer than the running version) — used by ai-tools.load().
- Failing tests (fake fetch): release not newer ⇒ nothing written; newer ⇒ files validated and written atomically with
meta.json; an invalid file is dropped, not fatal; a failing download keeps the previous set; > 200 entries / > 64 KB file refused;AGENTHUB_AI_TOOLS_SYNC=0⇒{ok:false, reason:"disabled"};activeRemoteDir()null after the running version catches up. - Implement (contents URL =
releaseApiUrl(repo)with/releases/latestreplaced by/contents/server/data/ai-tools?ref=<tag>; each entry'sdownload_url). npm run test:servergreen.
Task 4: Icons
Files: Create server/lib/ai-tool-icons.js. Test: server/__tests__/ai-tool-icons.test.js.
Produces: iconFor(id, {fetch?}): Promise<{type, body}|null> (cache hit, 24 h miss cache); iconDomain(tool): string|null.
- Failing tests: svg refused; > 64 KB refused; IP-literal / port / dotless domain ⇒ null; redirect off-domain refused; cache hit makes no second fetch; miss cached.
- Implement.
Task 5: Routes
Files: Modify server/routes/ai-tools.js, server/openapi-extra/ai-tools.js, regenerate server/openapi.yaml (npm run openapi if present, else the existing generator script). Test: server/__tests__/ai-tools-route.test.js.
- Failing tests:
POST /:id/install202 + job, 400ENOINSTALL, 409EBUSY, cross-origin 403;POST /:id/update409ENOTNPM;POST /update-all400ENOTHING;GET /jobnull then job;GET /versionsshape;GET /:id/iconheaders and 404. - Implement (error code → status map in one helper).
Task 6: Client
Files: Modify client/src/lib/api.ts, client/src/hooks/useAiTools.ts, client/src/components/settings/AiToolsSettings.tsx, client/src/i18n/locales/{en,vi}/settings.json (wherever aiTools.* keys live). Test: client/src/components/settings/__tests__/AiToolsSettings.test.tsx (create or extend).
- Failing tests: Install opens a confirm dialog with
npm install -g <pkg>@latest; confirming POSTs and renders the polled log; polling stops atok; badgev<latest> available;Installed outside npmwhen not npm-managed; Update tools disabled with nothing to do;<img>falls back to the letter on error. - Implement; review any screen snapshot diff before
-u. cd client && NODE_ENV=test npx vitest rungreen;npx tsc --noEmitclean.
Task 7: Docs + browser check
CLAUDE.mdmarker,AGENTS.md,docs/API.md,server/README.md,server/data/ai-tools/README.md, env var list (AGENTHUB_AI_TOOLS_SYNC);bash skills/file-headers/scripts/check-headers.shexits 0.- Browser check on an isolated server (
DASHBOARD_DATA_DIR,TMUX_TMPDIRtemp, port 4850): icons render, Install dialog text, badge. No real install is run without the user's go.