import { cpSync, existsSync, lstatSync, mkdirSync, rmSync, unlinkSync, } from 'node:fs'; import { join } from 'node:path'; export interface BundledSkillsPathInput { isPackaged: boolean; resourcesPath: string; appPath: string; } export interface EnsureBundledCourseSkillsOptions { managedConfigDir: string; sourceDir?: string; } export const BUNDLED_COURSE_SKILL_IDS = [ 'agent-browser', 'deploy-publish-check', 'designer-design-spec', 'dev-build-test', 'game-assets', 'marketing-launch-story', 'nianxxgame-skill', 'partner-agent-showcase', 'pm-project-plan', 'product-demo-prototype', 'ui-ux-course-quality', 'youth-plain-language', 'youth-ai-product-course', ] as const; const RETIRED_COURSE_SKILL_IDS = ['course-stage-review', 'student-growth-logger'] as const; const LEGACY_SUPERPOWERS_ARTIFACTS = [ 'plugins/superpowers-niancode.js', 'plugins/superpowers.js', 'superpowers-active.json', 'superpowers-bundles', ] as const; export function resolveBundledCourseSkillsDir(input: BundledSkillsPathInput): string { return input.isPackaged ? join(input.resourcesPath, 'course-skills') : join(input.appPath, '.opencode', 'skills'); } export function resolveBundledAgentBrowserPluginPath(input: BundledSkillsPathInput): string { return join( resolveBundledCourseSkillsDir(input), 'agent-browser', '.opencode', 'plugins', 'niancode-agent-browser.js', ); } export function getManagedOpencodeConfigDir(userDataDir: string): string { return join(userDataDir, 'opencode', 'niancode-config'); } function copyDirectorySync(sourceDir: string, targetDir: string): void { cpSync(sourceDir, targetDir, { recursive: true, force: true, filter: (sourcePath, targetPath) => { // Node 24.14's Windows override path can corrupt non-ASCII destinations // when replacing a file. Public unlink keeps force-overwrite semantics. if (lstatSync(sourcePath).isFile() && existsSync(targetPath)) { unlinkSync(targetPath); } return true; }, }); } export function removeLegacySuperpowersArtifacts(managedConfigDir: string): void { for (const artifact of LEGACY_SUPERPOWERS_ARTIFACTS) { rmSync(join(managedConfigDir, artifact), { recursive: true, force: true }); } } export function ensureBundledCourseSkills(options: EnsureBundledCourseSkillsOptions): boolean { const sourceDir = options.sourceDir?.trim(); if (!sourceDir || !existsSync(sourceDir)) { return false; } const targetSkillsDir = join(options.managedConfigDir, 'skills'); mkdirSync(targetSkillsDir, { recursive: true }); for (const skillId of RETIRED_COURSE_SKILL_IDS) { rmSync(join(targetSkillsDir, skillId), { recursive: true, force: true }); } copyDirectorySync(sourceDir, targetSkillsDir); return true; } export function ensureBundledAgentBrowserPlugin(options: { managedConfigDir: string; sourcePath?: string; }): boolean { const sourcePath = options.sourcePath?.trim(); if (!sourcePath || !existsSync(sourcePath)) return false; const targetPluginsDir = join(options.managedConfigDir, 'plugins'); mkdirSync(targetPluginsDir, { recursive: true }); cpSync(sourcePath, join(targetPluginsDir, 'niancode-agent-browser.js'), { force: true }); return true; }