Makelore 2.0 initial clean snapshot
This commit is contained in:
292
electron/opencode/superpowers.ts
Normal file
292
electron/opencode/superpowers.ts
Normal file
@@ -0,0 +1,292 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import {
|
||||
cpSync,
|
||||
existsSync,
|
||||
lstatSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
renameSync,
|
||||
rmSync,
|
||||
unlinkSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs';
|
||||
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
||||
import { retryTransientFilesystemOperation } from './filesystem-retry';
|
||||
|
||||
export interface BundledSuperpowersPathInput {
|
||||
isPackaged: boolean;
|
||||
resourcesPath: string;
|
||||
appPath: string;
|
||||
}
|
||||
|
||||
export interface EnsureBundledSuperpowersOptions {
|
||||
managedConfigDir: string;
|
||||
sourceDir?: string;
|
||||
}
|
||||
|
||||
export interface EnsureBundledCourseSkillsOptions {
|
||||
managedConfigDir: string;
|
||||
sourceDir?: string;
|
||||
}
|
||||
|
||||
export interface EnsureBundledCourseAgentsOptions {
|
||||
managedConfigDir: string;
|
||||
sourceDir?: string;
|
||||
}
|
||||
|
||||
const WRAPPER_PLUGIN_NAME = 'superpowers-niancode.js';
|
||||
const SUPERPOWERS_REPO_DIR = 'superpowers';
|
||||
const SUPERPOWERS_BUNDLES_DIR = 'superpowers-bundles';
|
||||
const SUPERPOWERS_ACTIVE_MANIFEST = 'superpowers-active.json';
|
||||
export const BUNDLED_COURSE_SKILL_IDS = [
|
||||
'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;
|
||||
|
||||
export function resolveBundledSuperpowersDir(input: BundledSuperpowersPathInput): string {
|
||||
const resourcesDir = input.isPackaged
|
||||
? join(input.resourcesPath, 'resources')
|
||||
: join(input.appPath, 'resources');
|
||||
|
||||
return join(resourcesDir, 'skills', SUPERPOWERS_REPO_DIR);
|
||||
}
|
||||
|
||||
export function resolveBundledCourseSkillsDir(input: BundledSuperpowersPathInput): string {
|
||||
return input.isPackaged
|
||||
? join(input.resourcesPath, 'course-skills')
|
||||
: join(input.appPath, '.opencode', 'skills');
|
||||
}
|
||||
|
||||
export function resolveBundledCourseAgentsDir(input: BundledSuperpowersPathInput): string {
|
||||
return input.isPackaged
|
||||
? join(input.resourcesPath, 'course-agents')
|
||||
: join(input.appPath, '.opencode', 'agent');
|
||||
}
|
||||
|
||||
export function getManagedOpencodeConfigDir(userDataDir: string): string {
|
||||
return join(userDataDir, 'opencode', 'niancode-config');
|
||||
}
|
||||
|
||||
function readBundledSuperpowersVersion(sourceDir: string): string {
|
||||
const packagePath = join(sourceDir, 'package.json');
|
||||
const packageJson = JSON.parse(retryTransientFilesystemOperation(
|
||||
() => readFileSync(packagePath, 'utf8'),
|
||||
)) as { version?: unknown };
|
||||
if (typeof packageJson.version !== 'string' || !packageJson.version.trim()) {
|
||||
throw new Error(`Bundled Superpowers package has no valid version: ${packagePath}`);
|
||||
}
|
||||
return packageJson.version.trim();
|
||||
}
|
||||
|
||||
function toSafeBundleName(version: string): string {
|
||||
const safeVersion = version.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
|
||||
if (!safeVersion) {
|
||||
throw new Error(`Bundled Superpowers version cannot form a safe directory name: ${version}`);
|
||||
}
|
||||
return safeVersion;
|
||||
}
|
||||
|
||||
function isCompleteBundle(bundleDir: string, expectedVersion: string): boolean {
|
||||
try {
|
||||
if (!existsSync(join(bundleDir, '.opencode', 'plugins', 'superpowers.js'))
|
||||
|| !existsSync(join(bundleDir, 'skills', 'using-superpowers', 'SKILL.md'))) {
|
||||
return false;
|
||||
}
|
||||
return readBundledSuperpowersVersion(bundleDir) === expectedVersion;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveManifestBundle(
|
||||
managedConfigDir: string,
|
||||
bundlesDir: string,
|
||||
expectedVersion: string,
|
||||
): string | null {
|
||||
try {
|
||||
const manifest = JSON.parse(retryTransientFilesystemOperation(
|
||||
() => readFileSync(join(managedConfigDir, SUPERPOWERS_ACTIVE_MANIFEST), 'utf8'),
|
||||
)) as { version?: unknown; directory?: unknown };
|
||||
if (manifest.version !== expectedVersion || typeof manifest.directory !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bundlesRoot = resolve(bundlesDir);
|
||||
const bundleDir = resolve(bundlesDir, manifest.directory);
|
||||
const relativeBundlePath = relative(bundlesRoot, bundleDir);
|
||||
if (!relativeBundlePath
|
||||
|| relativeBundlePath === '..'
|
||||
|| relativeBundlePath.startsWith(`..${sep}`)
|
||||
|| isAbsolute(relativeBundlePath)) {
|
||||
return null;
|
||||
}
|
||||
return isCompleteBundle(bundleDir, expectedVersion) ? bundleDir : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function installImmutableBundle(
|
||||
sourceDir: string,
|
||||
bundlesDir: string,
|
||||
safeBundleName: string,
|
||||
expectedVersion: string,
|
||||
): string {
|
||||
retryTransientFilesystemOperation(() => mkdirSync(bundlesDir, { recursive: true }));
|
||||
const stagingDir = join(
|
||||
bundlesDir,
|
||||
`.staging-${safeBundleName}-${process.pid}-${randomUUID()}`,
|
||||
);
|
||||
|
||||
try {
|
||||
retryTransientFilesystemOperation(() => copyDirectorySync(sourceDir, stagingDir));
|
||||
if (!isCompleteBundle(stagingDir, expectedVersion)) {
|
||||
throw new Error(`Bundled Superpowers staging copy is incomplete: ${stagingDir}`);
|
||||
}
|
||||
|
||||
const preferredDir = join(bundlesDir, safeBundleName);
|
||||
if (isCompleteBundle(preferredDir, expectedVersion)) {
|
||||
return preferredDir;
|
||||
}
|
||||
|
||||
const selectedDir = existsSync(preferredDir)
|
||||
? join(bundlesDir, `${safeBundleName}-${randomUUID()}`)
|
||||
: preferredDir;
|
||||
try {
|
||||
retryTransientFilesystemOperation(() => renameSync(stagingDir, selectedDir));
|
||||
} catch (error) {
|
||||
if (!isCompleteBundle(selectedDir, expectedVersion)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return selectedDir;
|
||||
} finally {
|
||||
if (existsSync(stagingDir)) {
|
||||
try {
|
||||
retryTransientFilesystemOperation(() => rmSync(stagingDir, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
}));
|
||||
} catch {
|
||||
// Startup cleanup is best-effort; preserve the installation result or error.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function writeSelectedBundleFiles(
|
||||
managedConfigDir: string,
|
||||
bundlesDir: string,
|
||||
bundleDir: string,
|
||||
version: string,
|
||||
): void {
|
||||
const targetPluginsDir = join(managedConfigDir, 'plugins');
|
||||
retryTransientFilesystemOperation(() => mkdirSync(targetPluginsDir, { recursive: true }));
|
||||
|
||||
const pluginPath = join(bundleDir, '.opencode', 'plugins', 'superpowers.js');
|
||||
let pluginImport = relative(targetPluginsDir, pluginPath).split(sep).join('/');
|
||||
if (!pluginImport.startsWith('.')) pluginImport = `./${pluginImport}`;
|
||||
|
||||
retryTransientFilesystemOperation(() => writeFileSync(
|
||||
join(targetPluginsDir, WRAPPER_PLUGIN_NAME),
|
||||
`export { SuperpowersPlugin } from '${pluginImport}';\n`,
|
||||
'utf8',
|
||||
));
|
||||
retryTransientFilesystemOperation(() => writeFileSync(
|
||||
join(managedConfigDir, SUPERPOWERS_ACTIVE_MANIFEST),
|
||||
`${JSON.stringify({
|
||||
version,
|
||||
directory: relative(bundlesDir, bundleDir),
|
||||
}, null, 2)}\n`,
|
||||
'utf8',
|
||||
));
|
||||
}
|
||||
|
||||
export function ensureBundledSuperpowersPlugin(options: EnsureBundledSuperpowersOptions): boolean {
|
||||
const sourceDir = options.sourceDir?.trim();
|
||||
if (!sourceDir) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const sourcePluginPath = join(sourceDir, '.opencode', 'plugins', 'superpowers.js');
|
||||
const sourceSkillsDir = join(sourceDir, 'skills');
|
||||
if (!existsSync(sourcePluginPath) || !existsSync(sourceSkillsDir)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const version = readBundledSuperpowersVersion(sourceDir);
|
||||
const safeBundleName = toSafeBundleName(version);
|
||||
const bundlesDir = join(options.managedConfigDir, SUPERPOWERS_BUNDLES_DIR);
|
||||
const preferredDir = join(bundlesDir, safeBundleName);
|
||||
const bundleDir = resolveManifestBundle(
|
||||
options.managedConfigDir,
|
||||
bundlesDir,
|
||||
version,
|
||||
) ?? (isCompleteBundle(preferredDir, version)
|
||||
? preferredDir
|
||||
: installImmutableBundle(sourceDir, bundlesDir, safeBundleName, version));
|
||||
|
||||
writeSelectedBundleFiles(
|
||||
options.managedConfigDir,
|
||||
bundlesDir,
|
||||
bundleDir,
|
||||
version,
|
||||
);
|
||||
|
||||
return 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 ensureBundledCourseAgents(options: EnsureBundledCourseAgentsOptions): boolean {
|
||||
const sourceDir = options.sourceDir?.trim();
|
||||
if (!sourceDir || !existsSync(sourceDir)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const targetAgentDir = join(options.managedConfigDir, 'agent');
|
||||
mkdirSync(targetAgentDir, { recursive: true });
|
||||
copyDirectorySync(sourceDir, targetAgentDir);
|
||||
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user