{"version":3,"file":"migration.d.ts","sourceRoot":"","sources":["../src/migration.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,mBAAmB,EAA2B,MAAM,YAAY,CAAC;AAI/E,MAAM,WAAW,yBAAyB;IACzC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uBAAuB,CAAC,EAAE,MAAM,EAAE,CAAC;IACnC,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,qBAAqB,CAAC,EAAE,MAAM,EAAE,CAAC;CACjC;AAED,MAAM,WAAW,wBAAwB;IACxC,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAC9B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,WAAW,EAAE,MAAM,EAAE,CAAC;CACtB;AAwQD,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGjF;AAED,wBAAsB,2BAA2B,CAAC,OAAO,EAAE;IAC1D,qBAAqB,EAAE,MAAM,CAAC;IAC9B,OAAO,CAAC,EAAE,yBAAyB,CAAC;IACpC,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAC/B,GAAG,OAAO,CAAC,wBAAwB,CAAC,CA+BpC","sourcesContent":["import { constants } from \"node:fs\";\nimport { lstat, mkdir, open, readdir, realpath } from \"node:fs/promises\";\nimport { basename, dirname, join, relative, resolve, sep } from \"node:path\";\nimport { loadSkills } from \"@ch1nyzzz/pi-coding-agent\";\nimport { assertAssetPath } from \"./bundle/schema.ts\";\nimport { atomicWriteFile, sha256 } from \"./storage.ts\";\nimport type { BundleManagedSource, BundleManagedSourceKind } from \"./types.ts\";\n\nconst GLOBAL_CONTEXT_FILES = [\"AGENTS.md\", \"AGENTS.MD\", \"CLAUDE.md\", \"CLAUDE.MD\"];\n\nexport interface EvoBundleMigrationOptions {\n\tagentDirectory?: string;\n\tsystemPromptDirectories?: string[];\n\tskillDirectories?: string[];\n\tmemoryDirectories?: string[];\n\tpreferenceDirectories?: string[];\n}\n\nexport interface EvoBundleMigrationResult {\n\tassets: BundleManagedSource[];\n\tpromptPaths: string[];\n\tskillPaths: string[];\n\tmemoryPaths: string[];\n}\n\ninterface MigrationContext {\n\tbundleSourceDirectory: string;\n\tassetsByTarget: Map<string, BundleManagedSource>;\n\tpromptPaths: string[];\n\tskillPaths: string[];\n\tmemoryPaths: string[];\n}\n\nfunction errorCode(error: unknown): string | undefined {\n\tif (typeof error !== \"object\" || error === null || !(\"code\" in error)) return undefined;\n\treturn typeof error.code === \"string\" ? error.code : undefined;\n}\n\nasync function canonicalDirectory(path: string, optional: boolean): Promise<string | undefined> {\n\tlet pathStat: Awaited<ReturnType<typeof lstat>>;\n\tconst resolvedPath = resolve(path);\n\ttry {\n\t\tpathStat = await lstat(resolvedPath);\n\t} catch (error) {\n\t\tif (optional && errorCode(error) === \"ENOENT\") return undefined;\n\t\tthrow error;\n\t}\n\tif (pathStat.isSymbolicLink()) {\n\t\tthrow new Error(`Evo-Pi migration refuses symbolic-link directory: ${resolvedPath}`);\n\t}\n\tif (!pathStat.isDirectory()) throw new Error(`Evo-Pi migration source is not a directory: ${resolvedPath}`);\n\tconst canonicalPath = await realpath(resolvedPath);\n\tconst confirmedStat = await lstat(resolvedPath);\n\tconst canonicalStat = await lstat(canonicalPath);\n\tif (\n\t\tconfirmedStat.isSymbolicLink() ||\n\t\t!confirmedStat.isDirectory() ||\n\t\tconfirmedStat.dev !== pathStat.dev ||\n\t\tconfirmedStat.ino !== pathStat.ino ||\n\t\tcanonicalStat.dev !== confirmedStat.dev ||\n\t\tcanonicalStat.ino !== confirmedStat.ino\n\t) {\n\t\tthrow new Error(`Evo-Pi migration source directory changed during validation: ${resolvedPath}`);\n\t}\n\treturn canonicalPath;\n}\n\nasync function readDataFile(path: string): Promise<string> {\n\tconst pathStat = await lstat(path);\n\tif (pathStat.isSymbolicLink()) throw new Error(`Evo-Pi migration refuses symbolic link: ${path}`);\n\tif (!pathStat.isFile()) throw new Error(`Evo-Pi migration source is not a regular file: ${path}`);\n\tif ((pathStat.mode & 0o111) !== 0) throw new Error(`Evo-Pi migration refuses executable file: ${path}`);\n\n\tconst handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);\n\ttry {\n\t\tconst before = await handle.stat();\n\t\tif (!before.isFile() || before.dev !== pathStat.dev || before.ino !== pathStat.ino) {\n\t\t\tthrow new Error(`Evo-Pi migration source changed during validation: ${path}`);\n\t\t}\n\t\tconst content = await handle.readFile();\n\t\tconst after = await handle.stat();\n\t\tif (before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs) {\n\t\t\tthrow new Error(`Evo-Pi migration source changed while it was read: ${path}`);\n\t\t}\n\t\treturn new TextDecoder(\"utf-8\", { fatal: true }).decode(content);\n\t} catch (error) {\n\t\tif (error instanceof TypeError) throw new Error(`Evo-Pi migration source is not valid UTF-8: ${path}`);\n\t\tthrow error;\n\t} finally {\n\t\tawait handle.close();\n\t}\n}\n\nasync function copyAsset(\n\tcontext: MigrationContext,\n\tkind: BundleManagedSourceKind,\n\tsourceRoot: string,\n\tsource: string,\n\ttarget: string,\n): Promise<void> {\n\tassertAssetPath(target);\n\tconst canonicalSourceRoot = await canonicalDirectory(sourceRoot, false);\n\tif (!canonicalSourceRoot) throw new Error(`Evo-Pi migration source directory disappeared: ${sourceRoot}`);\n\tconst resolvedSource = resolve(source);\n\tconst relativePath = relative(canonicalSourceRoot, resolvedSource).split(sep).join(\"/\");\n\tif (!relativePath || relativePath === \"..\" || relativePath.startsWith(\"../\")) {\n\t\tthrow new Error(`Evo-Pi migration source is outside its declared root: ${resolvedSource}`);\n\t}\n\tconst expectedParent = dirname(resolvedSource);\n\tif ((await canonicalDirectory(expectedParent, false)) !== expectedParent) {\n\t\tthrow new Error(`Evo-Pi migration source traverses a symbolic link: ${resolvedSource}`);\n\t}\n\tconst existing = context.assetsByTarget.get(target);\n\tif (existing) {\n\t\tif (\n\t\t\texisting.sourceRoot === canonicalSourceRoot &&\n\t\t\texisting.relativePath === relativePath &&\n\t\t\texisting.kind === kind\n\t\t) {\n\t\t\treturn;\n\t\t}\n\t\tthrow new Error(\n\t\t\t`Evo-Pi migration target collision at ${target}: ${join(existing.sourceRoot, existing.relativePath)} and ${resolvedSource}`,\n\t\t);\n\t}\n\tconst content = await readDataFile(resolvedSource);\n\tif ((await canonicalDirectory(expectedParent, false)) !== expectedParent) {\n\t\tthrow new Error(`Evo-Pi migration source parent changed while it was read: ${resolvedSource}`);\n\t}\n\tconst targetPath = join(context.bundleSourceDirectory, target);\n\tawait mkdir(dirname(targetPath), { recursive: true, mode: 0o700 });\n\tawait atomicWriteFile(targetPath, content);\n\tconst asset = {\n\t\tkind,\n\t\tsourceRoot: canonicalSourceRoot,\n\t\trelativePath,\n\t\ttargetPath: target,\n\t\tsourceSha256: sha256(content),\n\t} satisfies BundleManagedSource;\n\tcontext.assetsByTarget.set(target, asset);\n\tif (kind === \"custom-prompt\" || kind === \"append-prompt\" || kind === \"prompt\") {\n\t\tcontext.promptPaths.push(target);\n\t} else if (kind === \"skill\") {\n\t\tcontext.skillPaths.push(target);\n\t} else {\n\t\tcontext.memoryPaths.push(target);\n\t}\n}\n\nasync function copyOptionalFile(\n\tcontext: MigrationContext,\n\tkind: BundleManagedSourceKind,\n\tsourceRoot: string,\n\tsource: string,\n\ttarget: string,\n): Promise<boolean> {\n\ttry {\n\t\tawait lstat(source);\n\t} catch (error) {\n\t\tif (errorCode(error) === \"ENOENT\") return false;\n\t\tthrow error;\n\t}\n\tawait copyAsset(context, kind, sourceRoot, source, target);\n\treturn true;\n}\n\nasync function copyMarkdownDirectory(\n\tcontext: MigrationContext,\n\tkind: \"prompt\" | \"memory\" | \"preference\",\n\tdirectory: string,\n): Promise<void> {\n\tconst resolvedDirectory = await canonicalDirectory(directory, false);\n\tif (!resolvedDirectory) throw new Error(`Evo-Pi migration source directory disappeared: ${directory}`);\n\tconst entries = (await readdir(resolvedDirectory, { withFileTypes: true })).sort((left, right) =>\n\t\tleft.name.localeCompare(right.name),\n\t);\n\tfor (const entry of entries) {\n\t\tconst source = join(resolvedDirectory, entry.name);\n\t\tconst entryStat = await lstat(source);\n\t\tif (entryStat.isSymbolicLink()) throw new Error(`Evo-Pi migration refuses symbolic link: ${source}`);\n\t\tif (!entryStat.isFile() || !entry.name.endsWith(\".md\")) {\n\t\t\tthrow new Error(`Evo-Pi migration ${kind} directory may contain only direct Markdown files: ${source}`);\n\t\t}\n\t\tconst targetDirectory = kind === \"prompt\" ? \"prompts\" : \"memory\";\n\t\tawait copyAsset(context, kind, resolvedDirectory, source, `${targetDirectory}/${entry.name}`);\n\t}\n}\n\nasync function discoverSkillFiles(directory: string, includeRootFiles: boolean): Promise<string[]> {\n\tconst canonicalSkillDirectory = await canonicalDirectory(directory, false);\n\tif (!canonicalSkillDirectory) throw new Error(`Evo-Pi skill directory disappeared: ${directory}`);\n\tconst entries = (await readdir(canonicalSkillDirectory, { withFileTypes: true })).sort((left, right) =>\n\t\tleft.name.localeCompare(right.name),\n\t);\n\tconst skillEntry = entries.find((entry) => entry.name === \"SKILL.md\");\n\tif (skillEntry) {\n\t\tconst skillPath = join(canonicalSkillDirectory, skillEntry.name);\n\t\tconst skillStat = await lstat(skillPath);\n\t\tif (skillStat.isSymbolicLink()) throw new Error(`Evo-Pi migration refuses symbolic link: ${skillPath}`);\n\t\tif (!skillStat.isFile()) throw new Error(`Evo-Pi migration SKILL.md is not a regular file: ${skillPath}`);\n\t\tif (entries.length !== 1) {\n\t\t\tthrow new Error(`Evo-Pi migration refuses non-data skill support files in: ${canonicalSkillDirectory}`);\n\t\t}\n\t\treturn [skillPath];\n\t}\n\n\tconst result: string[] = [];\n\tfor (const entry of entries) {\n\t\tconst source = join(canonicalSkillDirectory, entry.name);\n\t\tconst entryStat = await lstat(source);\n\t\tif (entryStat.isSymbolicLink()) throw new Error(`Evo-Pi migration refuses symbolic link: ${source}`);\n\t\tif (entryStat.isDirectory()) {\n\t\t\tif (entry.name.startsWith(\".\") || entry.name === \"node_modules\") {\n\t\t\t\tthrow new Error(`Evo-Pi migration refuses hidden or dependency directory in skills: ${source}`);\n\t\t\t}\n\t\t\tresult.push(...(await discoverSkillFiles(source, false)));\n\t\t\tcontinue;\n\t\t}\n\t\tif (entryStat.isFile() && includeRootFiles && entry.name.endsWith(\".md\")) {\n\t\t\tresult.push(source);\n\t\t\tcontinue;\n\t\t}\n\t\tthrow new Error(`Evo-Pi migration skill source contains an unsupported or code file: ${source}`);\n\t}\n\treturn result;\n}\n\nasync function copySkillDirectory(context: MigrationContext, directory: string): Promise<void> {\n\tconst resolvedDirectory = await canonicalDirectory(directory, false);\n\tif (!resolvedDirectory) throw new Error(`Evo-Pi migration source directory disappeared: ${directory}`);\n\tconst discovered = (await discoverSkillFiles(resolvedDirectory, true)).map((path) => resolve(path)).sort();\n\tconst loaded = loadSkills({\n\t\tcwd: resolvedDirectory,\n\t\tagentDir: resolvedDirectory,\n\t\tskillPaths: [resolvedDirectory],\n\t\tincludeDefaults: false,\n\t});\n\tif (loaded.diagnostics.length > 0) {\n\t\tconst detail = loaded.diagnostics.map((diagnostic) => `${diagnostic.path}: ${diagnostic.message}`).join(\"; \");\n\t\tthrow new Error(`Evo-Pi migration refuses invalid or ambiguous skills: ${detail}`);\n\t}\n\tconst loadedPaths = loaded.skills.map((skill) => resolve(skill.filePath)).sort();\n\tif (loadedPaths.join(\"\\n\") !== discovered.join(\"\\n\")) {\n\t\tthrow new Error(`Evo-Pi migration could not safely and completely discover skills in: ${resolvedDirectory}`);\n\t}\n\tfor (const skill of loaded.skills.sort((left, right) => left.name.localeCompare(right.name))) {\n\t\tawait copyAsset(context, \"skill\", resolvedDirectory, skill.filePath, `skills/${skill.name}/SKILL.md`);\n\t}\n}\n\nasync function copyConventionalAgentData(context: MigrationContext, agentDirectory: string): Promise<void> {\n\tconst resolvedAgentDirectory = await canonicalDirectory(agentDirectory, false);\n\tif (!resolvedAgentDirectory) throw new Error(`Evo-Pi agent directory disappeared: ${agentDirectory}`);\n\tawait copyOptionalFile(\n\t\tcontext,\n\t\t\"custom-prompt\",\n\t\tresolvedAgentDirectory,\n\t\tjoin(resolvedAgentDirectory, \"SYSTEM.md\"),\n\t\t\"prompts/system.md\",\n\t);\n\tawait copyOptionalFile(\n\t\tcontext,\n\t\t\"append-prompt\",\n\t\tresolvedAgentDirectory,\n\t\tjoin(resolvedAgentDirectory, \"APPEND_SYSTEM.md\"),\n\t\t\"prompts/append-system.md\",\n\t);\n\n\tfor (const name of GLOBAL_CONTEXT_FILES) {\n\t\tif (\n\t\t\tawait copyOptionalFile(\n\t\t\t\tcontext,\n\t\t\t\t\"context\",\n\t\t\t\tresolvedAgentDirectory,\n\t\t\t\tjoin(resolvedAgentDirectory, name),\n\t\t\t\t\"memory/global-context.md\",\n\t\t\t)\n\t\t) {\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tconst conventionalSkills = join(resolvedAgentDirectory, \"skills\");\n\tconst canonicalSkills = await canonicalDirectory(conventionalSkills, true);\n\tif (canonicalSkills) await copySkillDirectory(context, canonicalSkills);\n}\n\nexport function inferAgentDirectoryForEvoRoot(evoRoot: string): string | undefined {\n\tconst resolvedRoot = resolve(evoRoot);\n\treturn basename(resolvedRoot) === \"evo\" ? dirname(resolvedRoot) : undefined;\n}\n\nexport async function migratePiDataToBundleSource(options: {\n\tbundleSourceDirectory: string;\n\tsources?: EvoBundleMigrationOptions;\n\tdefaultAgentDirectory?: string;\n}): Promise<EvoBundleMigrationResult> {\n\tconst bundleSourceDirectory = await canonicalDirectory(options.bundleSourceDirectory, false);\n\tif (!bundleSourceDirectory) {\n\t\tthrow new Error(`Evo-Pi bundle source directory disappeared: ${options.bundleSourceDirectory}`);\n\t}\n\tconst context: MigrationContext = {\n\t\tbundleSourceDirectory,\n\t\tassetsByTarget: new Map(),\n\t\tpromptPaths: [],\n\t\tskillPaths: [],\n\t\tmemoryPaths: [],\n\t};\n\tconst sources = options.sources ?? {};\n\tconst agentDirectory = sources.agentDirectory ?? options.defaultAgentDirectory;\n\tif (agentDirectory) await copyConventionalAgentData(context, agentDirectory);\n\tfor (const directory of sources.systemPromptDirectories ?? []) {\n\t\tawait copyMarkdownDirectory(context, \"prompt\", directory);\n\t}\n\tfor (const directory of sources.skillDirectories ?? []) await copySkillDirectory(context, directory);\n\tfor (const directory of sources.memoryDirectories ?? []) {\n\t\tawait copyMarkdownDirectory(context, \"memory\", directory);\n\t}\n\tfor (const directory of sources.preferenceDirectories ?? []) {\n\t\tawait copyMarkdownDirectory(context, \"preference\", directory);\n\t}\n\treturn {\n\t\tassets: [...context.assetsByTarget.values()],\n\t\tpromptPaths: context.promptPaths,\n\t\tskillPaths: context.skillPaths,\n\t\tmemoryPaths: context.memoryPaths,\n\t};\n}\n"]}