feat(installer): support pi package remove and update
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { access, cp, lstat, mkdir, realpath, rm, stat, symlink, chmod, unlink } from "node:fs/promises";
|
||||
import { access, cp, lstat, mkdir, readFile, realpath, rm, stat, symlink, chmod, unlink } from "node:fs/promises";
|
||||
import { constants, existsSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import path from "node:path";
|
||||
@@ -253,6 +253,55 @@ export async function findInstalledSuperpowers(clientId, cwd = process.cwd()) {
|
||||
return found;
|
||||
}
|
||||
|
||||
function piPackageSettingsPath(scope, repoRoot) {
|
||||
if (scope === "packageLocal") return path.join(repoRoot, ".pi", "settings.json");
|
||||
return path.join(homedir(), ".pi", "agent", "settings.json");
|
||||
}
|
||||
|
||||
async function readJsonIfExists(filePath) {
|
||||
try {
|
||||
return JSON.parse(await readFile(filePath, "utf8"));
|
||||
} catch (error) {
|
||||
if (error?.code === "ENOENT") return null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function isPiPackageInstalled({ scope, repoRoot }) {
|
||||
const settingsPath = piPackageSettingsPath(scope, repoRoot);
|
||||
const settings = await readJsonIfExists(settingsPath);
|
||||
if (!Array.isArray(settings?.packages)) return false;
|
||||
const settingsDir = path.dirname(settingsPath);
|
||||
const expected = path.resolve(repoRoot);
|
||||
return settings.packages.some((packagePath) => path.resolve(settingsDir, packagePath) === expected);
|
||||
}
|
||||
|
||||
export function isBootstrapInstalled(action, target) {
|
||||
const scriptsDir = path.join(target, "scripts");
|
||||
if (action === "pnpm-install") return existsSync(path.join(scriptsDir, "node_modules"));
|
||||
if (action === "web-automation") {
|
||||
return existsSync(path.join(scriptsDir, "node_modules"))
|
||||
&& existsSync(path.join(scriptsDir, "node_modules", ".bin", "cloakbrowser"));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function piPackageCommand({ action, repoRoot, piInstallArg = "" }) {
|
||||
if (["install", "update", "reinstall"].includes(action)) {
|
||||
const args = ["install"];
|
||||
if (piInstallArg) args.push(piInstallArg);
|
||||
args.push(repoRoot);
|
||||
return ["pi", args];
|
||||
}
|
||||
if (action === "remove") {
|
||||
const args = ["remove"];
|
||||
if (piInstallArg) args.push(piInstallArg);
|
||||
args.push(repoRoot);
|
||||
return ["pi", args];
|
||||
}
|
||||
throw new Error(`pi package mode does not support action: ${action}`);
|
||||
}
|
||||
|
||||
export function requiredSuperpowersFor(actions) {
|
||||
const required = new Set();
|
||||
for (const [skillName, action] of Object.entries(actions || {})) {
|
||||
@@ -274,9 +323,59 @@ export async function buildOperationPlan({ selections, repoRoot = process.cwd(),
|
||||
const scopeInfo = selection.skillsRoot ? { skillsRoot: selection.skillsRoot } : resolveClientScope(clientId, scope, repoRoot);
|
||||
const skillsRoot = scopeInfo.skillsRoot;
|
||||
if (clientId === "pi" && scopeInfo.packageMode) {
|
||||
operations.push({ kind: "pi-package", clientId, scope, action: selection.action || "install", repoRoot, piInstallArg: scopeInfo.piInstallArg || "" });
|
||||
operations.push({ kind: "bootstrap", clientId, scope, skill: "atlassian", action: "pnpm-install", target: path.join(repoRoot, "pi-package", "skills", "atlassian") });
|
||||
operations.push({ kind: "bootstrap", clientId, scope, skill: "web-automation", action: "web-automation", target: path.join(repoRoot, "pi-package", "skills", "web-automation") });
|
||||
const action = selection.action || "install";
|
||||
if (action === "skip") continue;
|
||||
if (["update", "reinstall"].includes(action)) {
|
||||
operations.push({ kind: "sync-pi-package", clientId, scope, action: "sync", repoRoot });
|
||||
}
|
||||
const packageInstalled = await isPiPackageInstalled({ scope, repoRoot });
|
||||
if (action === "remove") {
|
||||
operations.push({
|
||||
kind: "pi-package",
|
||||
clientId,
|
||||
scope,
|
||||
action,
|
||||
repoRoot,
|
||||
piInstallArg: scopeInfo.piInstallArg || "",
|
||||
status: packageInstalled ? undefined : "skipped",
|
||||
details: packageInstalled ? "" : "not installed in Pi settings",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
operations.push({
|
||||
kind: "pi-package",
|
||||
clientId,
|
||||
scope,
|
||||
action,
|
||||
repoRoot,
|
||||
piInstallArg: scopeInfo.piInstallArg || "",
|
||||
status: packageInstalled && action === "install" ? "skipped" : undefined,
|
||||
details: packageInstalled && action === "install" ? "already installed in Pi settings" : "",
|
||||
});
|
||||
for (const skillName of Object.keys(SKILLS)) {
|
||||
if (getSkillSource(skillName, clientId, repoRoot)) {
|
||||
operations.push({ kind: "package-skill", clientId, scope, skill: skillName, action: "included", status: "included", details: "included in Pi package" });
|
||||
}
|
||||
}
|
||||
for (const bootstrap of [
|
||||
{ skill: "atlassian", action: "pnpm-install" },
|
||||
{ skill: "web-automation", action: "web-automation" },
|
||||
]) {
|
||||
const target = path.join(repoRoot, "pi-package", "skills", bootstrap.skill);
|
||||
const installed = isBootstrapInstalled(bootstrap.action, target);
|
||||
const skipBootstrap = action === "install" && installed;
|
||||
operations.push({
|
||||
kind: "bootstrap",
|
||||
clientId,
|
||||
scope,
|
||||
skill: bootstrap.skill,
|
||||
action: bootstrap.action,
|
||||
displayAction: "bootstrap-deps",
|
||||
target,
|
||||
status: skipBootstrap ? "skipped" : undefined,
|
||||
details: skipBootstrap ? "runtime dependencies already installed" : target,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const installed = await detectInstalledSkills({ skillsRoot, clientId, repoRoot });
|
||||
@@ -326,7 +425,7 @@ export async function buildOperationPlan({ selections, repoRoot = process.cwd(),
|
||||
client: op.clientId,
|
||||
scope: op.scope,
|
||||
item: op.skill || op.helper || op.kind,
|
||||
action: op.action,
|
||||
action: op.displayAction || op.action,
|
||||
status: op.status || "planned",
|
||||
details: op.details || op.target || "",
|
||||
});
|
||||
@@ -399,18 +498,14 @@ function requireNode20() {
|
||||
|
||||
export async function executeOperation(op) {
|
||||
if (op.action === "unsupported" || op.status === "skipped") return { ...op, status: "skipped" };
|
||||
if (op.kind === "package-skill") return { ...op, status: "included" };
|
||||
if (op.kind === "sync-pi-package") {
|
||||
runCommand(path.join(op.repoRoot, "scripts", "sync-pi-package-skills.sh"), [], { cwd: op.repoRoot });
|
||||
return { ...op, status: "ok" };
|
||||
}
|
||||
if (op.kind === "pi-package") {
|
||||
if (op.action !== "install" && op.action !== "update" && op.action !== "reinstall") {
|
||||
throw new Error(`pi package mode does not support action: ${op.action}`);
|
||||
}
|
||||
const args = ["install"];
|
||||
if (op.piInstallArg) args.push(op.piInstallArg);
|
||||
args.push(op.repoRoot);
|
||||
runCommand("pi", args, { cwd: op.repoRoot });
|
||||
const [command, args] = piPackageCommand(op);
|
||||
runCommand(command, args, { cwd: op.repoRoot });
|
||||
return { ...op, status: "ok" };
|
||||
}
|
||||
if (op.kind === "skill") {
|
||||
|
||||
Reference in New Issue
Block a user