Files
pi-agent-integrated/scripts/migrate-profile.mjs
T

103 lines
3.9 KiB
JavaScript

import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { isAbsolute, join, relative, resolve, sep } from "node:path";
import { agentDir, ensureProfile, rootDir, skillsHomeDir } from "./profile.mjs";
const args = process.argv.slice(2);
const force = args.includes("--force");
const dryRun = args.includes("--dry-run");
function valueAfter(flag, fallback) {
const index = args.indexOf(flag);
return index >= 0 && args[index + 1] ? resolve(args[index + 1]) : fallback;
}
const sourceAgentDir = valueAfter("--from", join(homedir(), ".pi", "agent"));
const sourceAgentsDir = valueAfter("--skills-from", join(homedir(), ".agents"));
ensureProfile({ quiet: true });
function copyContents(source, destination, label) {
if (!existsSync(source)) {
console.log(`Skip ${label}: source does not exist (${source})`);
return { copied: 0, skipped: 0 };
}
mkdirSync(destination, { recursive: true });
let copied = 0;
let skipped = 0;
for (const entry of readdirSync(source, { withFileTypes: true })) {
const sourcePath = join(source, entry.name);
const destinationPath = join(destination, entry.name);
if (existsSync(destinationPath) && !force) {
skipped++;
console.log(`Skip existing: ${destinationPath}`);
continue;
}
console.log(`${dryRun ? "Would copy" : "Copy"}: ${sourcePath} -> ${destinationPath}`);
if (!dryRun) cpSync(sourcePath, destinationPath, { recursive: true, force });
copied++;
}
return { copied, skipped };
}
function isInsideApp(path) {
const rel = relative(rootDir, resolve(path));
return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
}
function isRemotePackageSource(source) {
return source.startsWith("npm:") || source.startsWith("git:") || /^[a-z]+:\/\//i.test(source);
}
function sanitizeMigratedSettings() {
const settingsPath = join(agentDir, "settings.json");
if (!existsSync(settingsPath)) return { changed: false, removed: [] };
let settings;
try {
settings = JSON.parse(readFileSync(settingsPath, "utf8"));
} catch {
console.log(`Skip settings cleanup: invalid JSON (${settingsPath})`);
return { changed: false, removed: [] };
}
if (!settings || typeof settings !== "object" || Array.isArray(settings)) {
console.log(`Skip settings cleanup: expected a JSON object (${settingsPath})`);
return { changed: false, removed: [] };
}
const removed = [];
for (const key of ["extensions", "skills", "prompts", "themes"]) {
if (!Array.isArray(settings[key])) continue;
settings[key] = settings[key].filter((entry) => {
if (typeof entry !== "string" || !isAbsolute(entry) || isInsideApp(entry)) return true;
removed.push(`${key}:${entry}`);
return false;
});
}
if (Array.isArray(settings.packages)) {
settings.packages = settings.packages.filter((entry) => {
const source = typeof entry === "string" ? entry : entry?.source;
if (typeof source !== "string" || isRemotePackageSource(source)) return true;
const resolvedSource = resolve(agentDir, source);
if (isInsideApp(resolvedSource)) return true;
removed.push(`packages:${source}`);
return false;
});
}
if (removed.length > 0) {
console.log(`Remove ${removed.length} external resource reference(s) from migrated settings.`);
for (const entry of removed) console.log(` ${entry}`);
if (!dryRun) writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
}
return { changed: removed.length > 0, removed };
}
const agentResult = copyContents(sourceAgentDir, agentDir, "Pi agent profile");
const skillsResult = copyContents(sourceAgentsDir, join(skillsHomeDir, ".agents"), "global skills profile");
const settingsCleanup = sanitizeMigratedSettings();
console.log(JSON.stringify({ dryRun, force, agent: agentResult, skills: skillsResult, settingsCleanup }, null, 2));