feat(skill): rename skill to reverse-engineer-anything
This commit is contained in:
parent
34252f55e9
commit
9356634ccb
|
|
@ -28,7 +28,7 @@
|
|||
<li><span><span class="tsd-kind-parameter">providerEnvironment</span>: <a href="../types/application_Setup.SetupProviderEnvironment.html" class="tsd-signature-type tsd-kind-type-alias">SetupProviderEnvironment</a></span></li></ul></div>
|
||||
<h4 class="tsd-returns-title">Returns <span class="tsd-signature-keyword">readonly</span> <a href="../interfaces/application_Setup.SetupAction.html" class="tsd-signature-type tsd-kind-interface">SetupAction</a><span class="tsd-signature-symbol">[]</span></h4><aside class="tsd-sources">
|
||||
<ul>
|
||||
<li>Defined in <a href="https://github.com/morluto/rea/blob/main/src/application/SetupPlan.ts#L8">src/application/SetupPlan.ts:8</a></li></ul></aside></div></li></ul></section></div>
|
||||
<li>Defined in <a href="https://github.com/morluto/rea/blob/main/src/application/SetupPlan.ts#L9">src/application/SetupPlan.ts:9</a></li></ul></aside></div></li></ul></section></div>
|
||||
<div class="col-sidebar">
|
||||
<div class="page-menu">
|
||||
<div class="tsd-navigation settings">
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
<li><span><span class="tsd-kind-parameter">home</span>: <span class="tsd-signature-type">string</span></span></li></ul></div>
|
||||
<h4 class="tsd-returns-title">Returns <span class="tsd-signature-type">Promise</span><span class="tsd-signature-symbol"><</span><span class="tsd-signature-type">boolean</span><span class="tsd-signature-symbol">></span></h4><aside class="tsd-sources">
|
||||
<ul>
|
||||
<li>Defined in <a href="https://github.com/morluto/rea/blob/main/src/application/SetupSkill.ts#L26">src/application/SetupSkill.ts:26</a></li></ul></aside></div></li></ul></section></div>
|
||||
<li>Defined in <a href="https://github.com/morluto/rea/blob/main/src/application/SetupSkill.ts#L34">src/application/SetupSkill.ts:34</a></li></ul></aside></div></li></ul></section></div>
|
||||
<div class="col-sidebar">
|
||||
<div class="page-menu">
|
||||
<div class="tsd-navigation settings">
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
<li><span><span class="tsd-kind-parameter">home</span>: <span class="tsd-signature-type">string</span></span></li></ul></div>
|
||||
<h4 class="tsd-returns-title">Returns <span class="tsd-signature-type">Promise</span><span class="tsd-signature-symbol"><</span><span class="tsd-signature-type">"failed"</span> <span class="tsd-signature-symbol">|</span> <span class="tsd-signature-type">"unchanged"</span> <span class="tsd-signature-symbol">|</span> <span class="tsd-signature-type">"installed"</span><span class="tsd-signature-symbol">></span></h4><aside class="tsd-sources">
|
||||
<ul>
|
||||
<li>Defined in <a href="https://github.com/morluto/rea/blob/main/src/application/SetupSkill.ts#L38">src/application/SetupSkill.ts:38</a></li></ul></aside></div></li></ul></section></div>
|
||||
<li>Defined in <a href="https://github.com/morluto/rea/blob/main/src/application/SetupSkill.ts#L50">src/application/SetupSkill.ts:50</a></li></ul></aside></div></li></ul></section></div>
|
||||
<div class="col-sidebar">
|
||||
<div class="page-menu">
|
||||
<div class="tsd-navigation settings">
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
"client": "2.0.0-beta.4",
|
||||
"core": "2.0.0-beta.4"
|
||||
},
|
||||
"skill_version": "20"
|
||||
"skill_version": "21"
|
||||
},
|
||||
"tools": {
|
||||
"total": 95,
|
||||
|
|
|
|||
|
|
@ -18,12 +18,12 @@ const lock = JSON.parse(
|
|||
await readFile(join(root, "package-lock.json"), "utf8"),
|
||||
);
|
||||
const skill = await readFile(
|
||||
join(root, "skills/rea-analysis/SKILL.md"),
|
||||
join(root, "skills/reverse-engineer-anything/SKILL.md"),
|
||||
"utf8",
|
||||
);
|
||||
const skillVersion = /^\s{2}version:\s*"([^"]+)"\s*$/mu.exec(skill)?.[1];
|
||||
if (skillVersion === undefined)
|
||||
throw new Error("Missing rea-analysis skill metadata version");
|
||||
throw new Error("Missing reverse-engineer-anything skill metadata version");
|
||||
const versionOf = (name) => {
|
||||
const value = lock.packages?.[`node_modules/${name}`]?.version;
|
||||
if (typeof value !== "string")
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ for (const argument of arguments_)
|
|||
throw new Error(`Unknown skill metadata option: ${argument}`);
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const path = join(root, "skills/rea-analysis/SKILL.md");
|
||||
const path = join(root, "skills/reverse-engineer-anything/SKILL.md");
|
||||
const current = await readFile(path, "utf8");
|
||||
const { CATALOG_IDENTITY } = await import(
|
||||
`${pathToFileURL(join(root, "dist/catalogIdentity.js")).href}?${String(Date.now())}`
|
||||
|
|
|
|||
|
|
@ -756,14 +756,18 @@ try {
|
|||
!mcpRegistration.includes("add-mcp npx -y rea-agents@latest mcp --name rea")
|
||||
)
|
||||
throw new Error("Incur mcp add did not register the latest npx command");
|
||||
const skillPath = join(home, ".agents/skills/rea-analysis/SKILL.md");
|
||||
const skillPath = join(
|
||||
home,
|
||||
".agents/skills/reverse-engineer-anything/SKILL.md",
|
||||
);
|
||||
const legacySkillPath = join(home, ".agents/skills/rea-analysis/SKILL.md");
|
||||
const siblingSkillPath = join(home, ".agents/skills/unrelated/SKILL.md");
|
||||
if (supportedSetupHost) {
|
||||
await mkdir(join(home, ".agents/skills/rea-analysis"), {
|
||||
recursive: true,
|
||||
});
|
||||
await mkdir(join(home, ".agents/skills/unrelated"), { recursive: true });
|
||||
await writeFile(skillPath, "stale managed skill\n");
|
||||
await writeFile(legacySkillPath, "stale managed skill\n");
|
||||
await writeFile(siblingSkillPath, "unrelated skill\n");
|
||||
}
|
||||
const plannedExecution = await runWithStatus(
|
||||
|
|
@ -776,7 +780,7 @@ try {
|
|||
const plannedClaudeConfig = await readFile(claudeConfig, "utf8");
|
||||
const plannedCodexConfig = await readFile(codexTarget, "utf8");
|
||||
const plannedCursorConfig = await readFile(cursorConfig, "utf8");
|
||||
const plannedSkill = await readFile(skillPath, "utf8");
|
||||
const plannedSkill = await readFile(legacySkillPath, "utf8");
|
||||
if (
|
||||
planned.status !== "needs_confirmation" ||
|
||||
plannedExecution.status !== 1 ||
|
||||
|
|
@ -856,17 +860,18 @@ try {
|
|||
throw new Error("packaged setup did not preserve config symlinks");
|
||||
const skill = await readFile(skillPath, "utf8");
|
||||
const canonicalSkill = await readFile(
|
||||
join(root, "skills/rea-analysis/SKILL.md"),
|
||||
join(root, "skills/reverse-engineer-anything/SKILL.md"),
|
||||
"utf8",
|
||||
);
|
||||
if (skill !== canonicalSkill)
|
||||
throw new Error("packaged skill did not match its canonical source");
|
||||
if (
|
||||
(await readFile(`${skillPath}.rea.backup`, "utf8")) !==
|
||||
(await pathExists(legacySkillPath)) ||
|
||||
(await readFile(`${legacySkillPath}.rea.backup`, "utf8")) !==
|
||||
"stale managed skill\n" ||
|
||||
(await readFile(siblingSkillPath, "utf8")) !== "unrelated skill\n"
|
||||
)
|
||||
throw new Error("packaged stale-skill upgrade was not isolated");
|
||||
throw new Error("packaged skill-name migration was not isolated");
|
||||
const alignedDoctorExecution = await runWithStatus(
|
||||
cli,
|
||||
["doctor", "--json"],
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
name: rea-analysis
|
||||
name: reverse-engineer-anything
|
||||
description: Reverse engineer native, Electron/JavaScript, and web applications with REA. Connect static artifacts to passive runtime evidence, explain how features work, and build a version tailored to the user's project.
|
||||
metadata:
|
||||
version: "20"
|
||||
version: "21"
|
||||
tool_count: 95
|
||||
catalog_digest: "b3db06bedadc1ad21a6cfe76f58a9c6cebbf9bce4f9663733b6bab07a57203c3"
|
||||
---
|
||||
|
|
@ -101,9 +101,9 @@ Notes is only a documentation example. Never select an app unless the user names
|
|||
|
||||
## Ensure REA is ready
|
||||
|
||||
1. Run `npx -y rea-agents doctor`.
|
||||
1. Run `npx -y rea-agents@latest doctor`.
|
||||
2. If setup is needed, tell the user REA needs to install its local binary-analysis tools. Do not lead with implementation details or assume the user knows reverse-engineering products.
|
||||
3. Before installing external software, obtain approval and identify what will be installed. If deeper analysis needs Hopper, describe it as REA's local analysis engine and note that it is a separate Mac app with its own license. Then run `npx -y rea-agents setup --yes`.
|
||||
3. Before installing external software, obtain approval and identify what will be installed. If deeper analysis needs Hopper, describe it as REA's local analysis engine and note that it is a separate Mac app with its own license. Then run `npx -y rea-agents@latest setup --yes`.
|
||||
4. If macOS or an installer requests human input, tell the user exactly what needs attention. After they finish, rerun setup and doctor.
|
||||
5. If setup registers a new MCP server, tell the user to restart their agent to load all REA tools. Direct CLI commands remain available before restart.
|
||||
|
||||
|
|
@ -3,6 +3,7 @@ import type {
|
|||
SetupClient,
|
||||
SetupProviderEnvironment,
|
||||
} from "./Setup.js";
|
||||
import { PRODUCT_IDENTITY } from "../identity.js";
|
||||
|
||||
/** Build the complete setup mutation plan before approval. */
|
||||
export const setupPlan = (
|
||||
|
|
@ -42,8 +43,9 @@ export const setupPlan = (
|
|||
? [
|
||||
{
|
||||
kind: "install_skill" as const,
|
||||
target: "~/.agents/skills/rea-analysis/SKILL.md",
|
||||
detail: "Install or update the bundled REA analysis skill.",
|
||||
target: `~/.agents/skills/${PRODUCT_IDENTITY.skillName}/SKILL.md`,
|
||||
detail:
|
||||
"Install or update the bundled REA reverse-engineering skill.",
|
||||
external: false,
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,18 +1,26 @@
|
|||
import { mkdir, readFile, rm } from "node:fs/promises";
|
||||
import { lstat, mkdir, readFile, rm } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import writeFileAtomic from "write-file-atomic";
|
||||
|
||||
import { PRODUCT_IDENTITY } from "../identity.js";
|
||||
|
||||
const skillDestination = (home: string, name: string): string =>
|
||||
join(home, ".agents/skills", name, "SKILL.md");
|
||||
|
||||
const readOptionalText = async (path: string): Promise<string | undefined> => {
|
||||
try {
|
||||
return await readFile(path, "utf8");
|
||||
} catch (cause: unknown) {
|
||||
if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
|
||||
return undefined;
|
||||
throw cause;
|
||||
}
|
||||
};
|
||||
|
||||
const canonicalSkillFiles = async (
|
||||
home: string,
|
||||
): Promise<{ readonly content: string; readonly destination: string }> => ({
|
||||
destination: join(
|
||||
home,
|
||||
".agents/skills",
|
||||
PRODUCT_IDENTITY.skillName,
|
||||
"SKILL.md",
|
||||
),
|
||||
destination: skillDestination(home, PRODUCT_IDENTITY.skillName),
|
||||
content: await readFile(
|
||||
new URL(
|
||||
`../../skills/${PRODUCT_IDENTITY.skillName}/SKILL.md`,
|
||||
|
|
@ -28,7 +36,11 @@ export const canonicalSkillNeedsInstall = async (
|
|||
): Promise<boolean> => {
|
||||
try {
|
||||
const { content, destination } = await canonicalSkillFiles(home);
|
||||
return (await readFile(destination, "utf8")) !== content;
|
||||
if ((await readOptionalText(destination)) !== content) return true;
|
||||
for (const name of PRODUCT_IDENTITY.legacySkillNames)
|
||||
if ((await readOptionalText(skillDestination(home, name))) !== undefined)
|
||||
return true;
|
||||
return false;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -38,45 +50,80 @@ export const canonicalSkillNeedsInstall = async (
|
|||
export const installCanonicalSkill = async (
|
||||
home: string,
|
||||
): Promise<"installed" | "unchanged" | "failed"> => {
|
||||
let destination = join(
|
||||
home,
|
||||
".agents/skills",
|
||||
PRODUCT_IDENTITY.skillName,
|
||||
"SKILL.md",
|
||||
);
|
||||
let destination = skillDestination(home, PRODUCT_IDENTITY.skillName);
|
||||
let backup = `${destination}.rea.backup`;
|
||||
let original: string | undefined;
|
||||
let canonicalChanged = false;
|
||||
const removedLegacy: Array<{
|
||||
readonly content: string;
|
||||
readonly destination: string;
|
||||
}> = [];
|
||||
try {
|
||||
const canonical = await canonicalSkillFiles(home);
|
||||
destination = canonical.destination;
|
||||
backup = `${destination}.rea.backup`;
|
||||
const { content } = canonical;
|
||||
original = await readFile(destination, "utf8").catch(() => undefined);
|
||||
if (original === content) return "unchanged";
|
||||
original = await readOptionalText(destination);
|
||||
const legacy: Array<{
|
||||
readonly content: string;
|
||||
readonly destination: string;
|
||||
}> = [];
|
||||
for (const name of PRODUCT_IDENTITY.legacySkillNames) {
|
||||
const legacyDestination = skillDestination(home, name);
|
||||
const legacyContent = await readOptionalText(legacyDestination);
|
||||
if (legacyContent !== undefined) {
|
||||
if ((await lstat(dirname(legacyDestination))).isSymbolicLink())
|
||||
throw new Error("legacy skill directory is a symbolic link");
|
||||
legacy.push({ content: legacyContent, destination: legacyDestination });
|
||||
}
|
||||
}
|
||||
if (original === content && legacy.length === 0) return "unchanged";
|
||||
await mkdir(dirname(destination), { recursive: true });
|
||||
if (original !== undefined)
|
||||
if (original !== undefined && original !== content)
|
||||
await writeFileAtomic(backup, original, {
|
||||
encoding: "utf8",
|
||||
mode: 0o600,
|
||||
});
|
||||
await writeFileAtomic(destination, content, {
|
||||
encoding: "utf8",
|
||||
mode: 0o600,
|
||||
});
|
||||
canonicalChanged = original !== content;
|
||||
if (canonicalChanged)
|
||||
await writeFileAtomic(destination, content, {
|
||||
encoding: "utf8",
|
||||
mode: 0o600,
|
||||
});
|
||||
if ((await readFile(destination, "utf8")) !== content)
|
||||
throw new Error("skill readback mismatch");
|
||||
for (const legacySkill of legacy) {
|
||||
await writeFileAtomic(
|
||||
`${legacySkill.destination}.rea.backup`,
|
||||
legacySkill.content,
|
||||
{ encoding: "utf8", mode: 0o600 },
|
||||
);
|
||||
await rm(legacySkill.destination, { force: true });
|
||||
removedLegacy.push(legacySkill);
|
||||
}
|
||||
return "installed";
|
||||
} catch {
|
||||
try {
|
||||
if (original === undefined) await rm(destination, { force: true });
|
||||
else
|
||||
if (canonicalChanged && original === undefined)
|
||||
await rm(destination, { force: true });
|
||||
else if (canonicalChanged && original !== undefined)
|
||||
await writeFileAtomic(destination, original, {
|
||||
encoding: "utf8",
|
||||
mode: 0o600,
|
||||
});
|
||||
} catch {
|
||||
// The backup remains beside the skill for explicit operator recovery.
|
||||
// The canonical backup remains beside the skill for operator recovery.
|
||||
}
|
||||
for (const legacySkill of removedLegacy)
|
||||
try {
|
||||
await mkdir(dirname(legacySkill.destination), { recursive: true });
|
||||
await writeFileAtomic(legacySkill.destination, legacySkill.content, {
|
||||
encoding: "utf8",
|
||||
mode: 0o600,
|
||||
});
|
||||
} catch {
|
||||
// The legacy backup remains beside the skill for operator recovery.
|
||||
}
|
||||
return "failed";
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -87,12 +87,7 @@ export const systemUninstallHost = (
|
|||
): UninstallHost => ({
|
||||
clients: () => Promise.resolve(supportedClients(home)),
|
||||
removeClient: (client) => removeClient(client, fileSystem),
|
||||
removeSkill: () =>
|
||||
removeManagedPath(
|
||||
join(home, ".agents/skills", PRODUCT_IDENTITY.skillName),
|
||||
"skill",
|
||||
fileSystem,
|
||||
),
|
||||
removeSkill: () => removeManagedSkills(home, fileSystem),
|
||||
purgeData: async () => [
|
||||
await removeManagedPath(join(home, ".rea/cache"), "cache", fileSystem),
|
||||
await removeManagedPath(join(home, ".rea/state"), "state", fileSystem),
|
||||
|
|
@ -262,6 +257,36 @@ const removeManagedPath = async (
|
|||
}
|
||||
};
|
||||
|
||||
const removeManagedSkills = async (
|
||||
home: string,
|
||||
fileSystem: UninstallFileSystem,
|
||||
): Promise<UninstallItem> => {
|
||||
const results: UninstallItem[] = [];
|
||||
for (const name of [
|
||||
PRODUCT_IDENTITY.skillName,
|
||||
...PRODUCT_IDENTITY.legacySkillNames,
|
||||
])
|
||||
results.push(
|
||||
await removeManagedPath(
|
||||
join(home, ".agents/skills", name),
|
||||
"skill",
|
||||
fileSystem,
|
||||
),
|
||||
);
|
||||
const failed = results.find(({ status }) => status === "failed");
|
||||
if (failed !== undefined) return failed;
|
||||
const retained = results.find(({ status }) => status === "retained");
|
||||
if (retained !== undefined) return retained;
|
||||
const removed = results.filter(({ status }) => status === "removed");
|
||||
if (removed.length > 0)
|
||||
return item(
|
||||
"skill",
|
||||
"removed",
|
||||
removed.map(({ detail }) => detail).join(" "),
|
||||
);
|
||||
return item("skill", "skipped", "No managed REA skill installation exists.");
|
||||
};
|
||||
|
||||
const item = (
|
||||
name: string,
|
||||
status: UninstallItem["status"],
|
||||
|
|
|
|||
|
|
@ -5,5 +5,5 @@ export const PACKAGE_METADATA = {
|
|||
serverSdkVersion: "2.0.0-beta.4",
|
||||
clientSdkVersion: "2.0.0-beta.4",
|
||||
coreSdkVersion: "2.0.0-beta.4",
|
||||
skillVersion: "20",
|
||||
skillVersion: "21",
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ export const PRODUCT_IDENTITY = {
|
|||
cliBinary: "rea",
|
||||
mcpCommand: `npx -y ${PACKAGE_METADATA.name}@latest mcp`,
|
||||
mcpServerKey: "rea",
|
||||
skillName: "rea-analysis",
|
||||
skillName: "reverse-engineer-anything",
|
||||
legacySkillNames: ["rea-analysis"],
|
||||
skillVersion: PACKAGE_METADATA.skillVersion,
|
||||
configFileName: "rea.json",
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -146,9 +146,14 @@ describe("agent lifecycle", () => {
|
|||
const home = await mkdtemp(join(tmpdir(), "rea-uninstall-"));
|
||||
roots.push(home);
|
||||
const cursor = join(home, ".cursor/mcp.json");
|
||||
const skill = join(home, ".agents/skills/rea-analysis/SKILL.md");
|
||||
const skill = join(
|
||||
home,
|
||||
".agents/skills/reverse-engineer-anything/SKILL.md",
|
||||
);
|
||||
const legacySkill = join(home, ".agents/skills/rea-analysis/SKILL.md");
|
||||
await mkdir(dirname(cursor), { recursive: true });
|
||||
await mkdir(dirname(skill), { recursive: true });
|
||||
await mkdir(dirname(legacySkill), { recursive: true });
|
||||
await writeFile(
|
||||
cursor,
|
||||
JSON.stringify({
|
||||
|
|
@ -159,6 +164,7 @@ describe("agent lifecycle", () => {
|
|||
}),
|
||||
);
|
||||
await writeFile(skill, "managed");
|
||||
await writeFile(legacySkill, "legacy managed");
|
||||
await mkdir(join(home, ".rea"), { recursive: true });
|
||||
await import("node:fs/promises").then(({ symlink }) =>
|
||||
symlink(home, join(home, ".rea/cache")),
|
||||
|
|
@ -168,6 +174,8 @@ describe("agent lifecycle", () => {
|
|||
expect(JSON.parse(await readFile(cursor, "utf8"))).toEqual({
|
||||
mcpServers: { other: { command: "other" } },
|
||||
});
|
||||
await expect(readFile(skill, "utf8")).rejects.toThrow();
|
||||
await expect(readFile(legacySkill, "utf8")).rejects.toThrow();
|
||||
expect(first.items).toContainEqual(
|
||||
expect.objectContaining({
|
||||
name: "cache",
|
||||
|
|
@ -300,7 +308,7 @@ describe("agent lifecycle", () => {
|
|||
it("reports a managed-path removal failure", async () => {
|
||||
const home = await mkdtemp(join(tmpdir(), "rea-uninstall-remove-"));
|
||||
roots.push(home);
|
||||
const skillRoot = join(home, ".agents/skills/rea-analysis");
|
||||
const skillRoot = join(home, ".agents/skills/reverse-engineer-anything");
|
||||
await mkdir(skillRoot, { recursive: true });
|
||||
const result = await runUninstall(
|
||||
false,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,11 @@
|
|||
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import {
|
||||
mkdtemp,
|
||||
mkdir,
|
||||
readFile,
|
||||
rm,
|
||||
symlink,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
|
@ -19,7 +26,10 @@ describe("canonical skill transaction", () => {
|
|||
it("backs up and upgrades a stale managed skill without touching siblings", async () => {
|
||||
const home = await mkdtemp(join(tmpdir(), "rea-skill-test-"));
|
||||
roots.push(home);
|
||||
const destination = join(home, ".agents/skills/rea-analysis/SKILL.md");
|
||||
const destination = join(
|
||||
home,
|
||||
".agents/skills/reverse-engineer-anything/SKILL.md",
|
||||
);
|
||||
const sibling = join(home, ".agents/skills/unrelated/SKILL.md");
|
||||
await mkdir(dirname(destination), { recursive: true });
|
||||
await mkdir(dirname(sibling), { recursive: true });
|
||||
|
|
@ -31,7 +41,7 @@ describe("canonical skill transaction", () => {
|
|||
expect(await readFile(`${destination}.rea.backup`, "utf8")).toBe(
|
||||
"stale managed skill\n",
|
||||
);
|
||||
expect(await readFile(destination, "utf8")).toContain('version: "20"');
|
||||
expect(await readFile(destination, "utf8")).toContain('version: "21"');
|
||||
expect(await readFile(destination, "utf8")).toContain(
|
||||
`tool_count: ${String(TOOL_CONTRACTS.length)}`,
|
||||
);
|
||||
|
|
@ -39,4 +49,49 @@ describe("canonical skill transaction", () => {
|
|||
expect(await canonicalSkillNeedsInstall(home)).toBe(false);
|
||||
expect(await installCanonicalSkill(home)).toBe("unchanged");
|
||||
});
|
||||
|
||||
it("backs up and retires the legacy skill name", async () => {
|
||||
const home = await mkdtemp(join(tmpdir(), "rea-skill-test-"));
|
||||
roots.push(home);
|
||||
const legacy = join(home, ".agents/skills/rea-analysis/SKILL.md");
|
||||
const destination = join(
|
||||
home,
|
||||
".agents/skills/reverse-engineer-anything/SKILL.md",
|
||||
);
|
||||
await mkdir(dirname(legacy), { recursive: true });
|
||||
await writeFile(legacy, "legacy managed skill\n");
|
||||
|
||||
expect(await canonicalSkillNeedsInstall(home)).toBe(true);
|
||||
expect(await installCanonicalSkill(home)).toBe("installed");
|
||||
expect(await readFile(destination, "utf8")).toContain(
|
||||
"name: reverse-engineer-anything",
|
||||
);
|
||||
expect(await readFile(`${legacy}.rea.backup`, "utf8")).toBe(
|
||||
"legacy managed skill\n",
|
||||
);
|
||||
await expect(readFile(legacy, "utf8")).rejects.toThrow();
|
||||
expect(await canonicalSkillNeedsInstall(home)).toBe(false);
|
||||
});
|
||||
|
||||
it("does not migrate a legacy skill through a directory symlink", async () => {
|
||||
const home = await mkdtemp(join(tmpdir(), "rea-skill-test-"));
|
||||
roots.push(home);
|
||||
const skillsRoot = join(home, ".agents/skills");
|
||||
const external = join(home, "external-skill");
|
||||
const legacyRoot = join(skillsRoot, "rea-analysis");
|
||||
const legacy = join(legacyRoot, "SKILL.md");
|
||||
const destination = join(skillsRoot, "reverse-engineer-anything/SKILL.md");
|
||||
await mkdir(skillsRoot, { recursive: true });
|
||||
await mkdir(external, { recursive: true });
|
||||
await writeFile(join(external, "SKILL.md"), "external skill\n");
|
||||
await symlink(
|
||||
external,
|
||||
legacyRoot,
|
||||
process.platform === "win32" ? "junction" : "dir",
|
||||
);
|
||||
|
||||
expect(await installCanonicalSkill(home)).toBe("failed");
|
||||
expect(await readFile(legacy, "utf8")).toBe("external skill\n");
|
||||
await expect(readFile(destination, "utf8")).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue