Files
修雨 df1e33442b fix(skill): embed skill sources in the binary and cover qoder dirs in upgrade refresh
Two distribution defects left installed skills stale (issue PeterGuy326#8):

1. skill setup resolved its source from cwd / exe-adjacent checkouts, so
   an upgraded binary happily re-installed whatever stale checkout it was
   run from, and agents kept routing 'create robot' into the chat dead end.
   The skills/mono and skills/multi trees are now embedded (go:embed) and
   setup defaults to that copy, materialized into ~/.dws/skills/<mode> —
   upgrading the binary upgrades what setup installs. --source and
   DWS_SKILL_SOURCE stay as explicit overrides, and an override that has
   no skill root errors out instead of silently falling back.

2. dws upgrade's skill refresh (knownSkillDirs) skipped ~/.qoder and
   ~/.qoderwork, so Qoder/QoderWork hosts never got refreshed routing.
   Added both to the dir table and its seven mirrors (npm install.js,
   install.sh/ps1, install-skills.sh, homebrew template, package test,
   verify-package-managers.sh).

setup-side detection and --target qoder/qoderwork already work on this
branch; this closes the remaining upgrade-side and source-side gaps.
2026-06-12 17:57:48 +08:00

199 lines
6.2 KiB
JavaScript
Executable File

#!/usr/bin/env node
"use strict";
const fs = require("fs");
const os = require("os");
const path = require("path");
const childProcess = require("child_process");
// Canonical list: keep scripts/install.sh, scripts/install.ps1, scripts/install-skills.sh in sync.
const AGENT_DIRS = [
".agents/skills",
".claude/skills",
".cursor/skills",
".qoder/skills",
".qoderwork/skills",
".gemini/skills",
".codex/skills",
".github/skills",
".windsurf/skills",
".augment/skills",
".cline/skills",
".amp/skills",
".kiro/skills",
".trae/skills",
".openclaw/skills",
".hermes/skills",
];
const PLATFORM_MAP = {
"darwin-x64": "dws-darwin-amd64.tar.gz",
"darwin-arm64": "dws-darwin-arm64.tar.gz",
"linux-x64": "dws-linux-amd64.tar.gz",
"linux-arm64": "dws-linux-arm64.tar.gz",
"win32-x64": "dws-windows-amd64.zip",
"win32-arm64": "dws-windows-arm64.zip",
};
function run(command, args) {
childProcess.execFileSync(command, args, { stdio: "inherit" });
}
function ensureCleanDir(dir) {
fs.rmSync(dir, { recursive: true, force: true });
fs.mkdirSync(dir, { recursive: true });
}
function findBinary(root) {
const entries = fs.readdirSync(root, { withFileTypes: true });
for (const entry of entries) {
const entryPath = path.join(root, entry.name);
if (entry.isDirectory()) {
const nested = findBinary(entryPath);
if (nested) {
return nested;
}
continue;
}
if (entry.name === "dws" || entry.name === "dws.exe") {
return entryPath;
}
}
return "";
}
function extractArchive(archivePath, destDir) {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "dws-npm-bin-"));
try {
if (archivePath.endsWith(".tar.gz")) {
run("tar", ["-xzf", archivePath, "-C", tmpDir]);
} else if (process.platform === "win32") {
run("powershell.exe", [
"-NoLogo",
"-NoProfile",
"-Command",
`Expand-Archive -Path '${archivePath.replace(/'/g, "''")}' -DestinationPath '${tmpDir.replace(/'/g, "''")}' -Force`,
]);
} else {
run("unzip", ["-q", archivePath, "-d", tmpDir]);
}
const binaryPath = findBinary(tmpDir);
if (!binaryPath) {
throw new Error(`dws binary not found in archive ${archivePath}`);
}
ensureCleanDir(destDir);
const targetName = process.platform === "win32" ? "dws.exe" : "dws";
const targetPath = path.join(destDir, targetName);
fs.copyFileSync(binaryPath, targetPath);
if (process.platform !== "win32") {
fs.chmodSync(targetPath, 0o755);
}
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
}
function extractSkills(zipPath, destDir) {
ensureCleanDir(destDir);
if (process.platform === "win32") {
run("powershell.exe", [
"-NoLogo",
"-NoProfile",
"-Command",
`Expand-Archive -Path '${zipPath.replace(/'/g, "''")}' -DestinationPath '${destDir.replace(/'/g, "''")}' -Force`,
]);
return;
}
run("unzip", ["-q", zipPath, "-d", destDir]);
}
function copyChildren(srcDir, destDir) {
fs.mkdirSync(destDir, { recursive: true });
for (const entry of fs.readdirSync(srcDir)) {
fs.cpSync(path.join(srcDir, entry), path.join(destDir, entry), { recursive: true, force: true });
}
}
function installSkillsToHomes(skillRoot) {
const homeDir = os.homedir();
let installed = 0;
AGENT_DIRS.forEach((agentDir, index) => {
const baseDir = path.join(homeDir, agentDir);
const parentGate = path.dirname(baseDir);
if (index > 0 && !fs.existsSync(parentGate)) {
return;
}
const destDir = path.join(baseDir, "dws");
fs.rmSync(destDir, { recursive: true, force: true });
copyChildren(skillRoot, destDir);
installed += 1;
});
if (installed === 0) {
copyChildren(skillRoot, path.join(homeDir, ".agents", "skills", "dws"));
}
}
// cacheUserSkills copies the mono and multi trees out of the freshly extracted
// dws-skills.zip into ~/.dws/skills/{mono,multi}/ so that `dws skill setup`
// can fall back to a user-local cache when --source is not provided. mono is
// already installed into agent homes by installSkillsToHomes; the cache is
// purely a source-of-truth for the setup command.
function cacheUserSkills(extractedSkillsRoot) {
const cacheBase = path.join(os.homedir(), ".dws", "skills");
const monoSource = fs.existsSync(path.join(extractedSkillsRoot, "mono", "SKILL.md"))
? path.join(extractedSkillsRoot, "mono")
: extractedSkillsRoot;
const monoCache = path.join(cacheBase, "mono");
fs.rmSync(monoCache, { recursive: true, force: true });
copyChildren(monoSource, monoCache);
const multiSource = path.join(extractedSkillsRoot, "multi");
if (fs.existsSync(multiSource) && fs.statSync(multiSource).isDirectory()) {
const multiCache = path.join(cacheBase, "multi");
fs.rmSync(multiCache, { recursive: true, force: true });
copyChildren(multiSource, multiCache);
}
}
function main() {
const packageRoot = __dirname;
const assetsDir = path.join(packageRoot, "assets");
const vendorDir = path.join(packageRoot, "vendor");
// Extract dws-skills.zip into a staging directory so we can split mono/
// (installed to agent homes) from multi/ (cached for later setup use).
const skillsStaging = path.join(packageRoot, "share", "skills");
const assetName = PLATFORM_MAP[`${process.platform}-${process.arch}`];
if (!assetName) {
throw new Error(`unsupported platform: ${process.platform}/${process.arch}`);
}
const archivePath = path.join(assetsDir, assetName);
const skillsPath = path.join(assetsDir, "dws-skills.zip");
if (!fs.existsSync(archivePath)) {
throw new Error(`missing platform archive: ${archivePath}`);
}
if (!fs.existsSync(skillsPath)) {
throw new Error(`missing skills archive: ${skillsPath}`);
}
extractArchive(archivePath, vendorDir);
extractSkills(skillsPath, skillsStaging);
// For backward compatibility, the zip root carries a copy of mono content
// (SKILL.md + references/ + scripts/). Prefer the explicit mono/ subdir
// when present; fall back to the staging root otherwise.
const monoRoot = fs.existsSync(path.join(skillsStaging, "mono", "SKILL.md"))
? path.join(skillsStaging, "mono")
: skillsStaging;
installSkillsToHomes(monoRoot);
cacheUserSkills(skillsStaging);
}
main();