Browse Source

fix(skill-sync): break custom skills import cycle

Alvin Unreal 1 month ago
parent
commit
e3b85d0ff9

+ 73 - 0
src/cli/custom-skills-registry.ts

@@ -0,0 +1,73 @@
+/**
+ * A custom skill bundled in this repository.
+ * Unlike npx-installed skills, these are copied from src/skills/ to the OpenCode skills directory
+ */
+export interface CustomSkill {
+  /** Skill name (folder name) */
+  name: string;
+  /** Human-readable description */
+  description: string;
+  /** List of agents that should auto-allow this skill */
+  allowedAgents: string[];
+  /** Source path in this repo (relative to project root) */
+  sourcePath: string;
+}
+
+/**
+ * Registry of custom skills bundled in this repository.
+ */
+export const CUSTOM_SKILLS: CustomSkill[] = [
+  {
+    name: 'simplify',
+    description: 'Code simplification and readability-focused refactoring',
+    allowedAgents: ['oracle'],
+    sourcePath: 'src/skills/simplify',
+  },
+  {
+    name: 'codemap',
+    description: 'Repository understanding and hierarchical codemap generation',
+    allowedAgents: ['orchestrator'],
+    sourcePath: 'src/skills/codemap',
+  },
+  {
+    name: 'clonedeps',
+    description: 'Clone important dependency source for local inspection',
+    allowedAgents: ['orchestrator'],
+    sourcePath: 'src/skills/clonedeps',
+  },
+  {
+    name: 'deepwork',
+    description:
+      'Heavy/complex coding sessions and large modifications workflow',
+    allowedAgents: ['orchestrator'],
+    sourcePath: 'src/skills/deepwork',
+  },
+  {
+    name: 'reflect',
+    description:
+      'Review repeated work and suggest reusable workflow improvements',
+    allowedAgents: ['orchestrator'],
+    sourcePath: 'src/skills/reflect',
+  },
+  {
+    name: 'oh-my-opencode-slim',
+    description:
+      'Configure, customize, and safely improve oh-my-opencode-slim setups',
+    allowedAgents: ['orchestrator'],
+    sourcePath: 'src/skills/oh-my-opencode-slim',
+  },
+  {
+    name: 'release-smoke-test',
+    description:
+      'Validate packed release candidates and bugfixes before public publish',
+    allowedAgents: ['orchestrator'],
+    sourcePath: 'src/skills/release-smoke-test',
+  },
+  {
+    name: 'worktrees',
+    description:
+      'Manage Git worktrees as OMO safe isolated coding lanes for complex/risky/parallel work',
+    allowedAgents: ['orchestrator'],
+    sourcePath: 'src/skills/worktrees',
+  },
+];

+ 2 - 73
src/cli/custom-skills.ts

@@ -1,80 +1,9 @@
 import { join } from 'node:path';
 import { join } from 'node:path';
 import { fileURLToPath } from 'node:url';
 import { fileURLToPath } from 'node:url';
+import { CUSTOM_SKILLS, type CustomSkill } from './custom-skills-registry';
 import { getConfigDir } from './paths';
 import { getConfigDir } from './paths';
 
 
-/**
- * A custom skill bundled in this repository.
- * Unlike npx-installed skills, these are copied from src/skills/ to the OpenCode skills directory
- */
-export interface CustomSkill {
-  /** Skill name (folder name) */
-  name: string;
-  /** Human-readable description */
-  description: string;
-  /** List of agents that should auto-allow this skill */
-  allowedAgents: string[];
-  /** Source path in this repo (relative to project root) */
-  sourcePath: string;
-}
-
-/**
- * Registry of custom skills bundled in this repository.
- */
-export const CUSTOM_SKILLS: CustomSkill[] = [
-  {
-    name: 'simplify',
-    description: 'Code simplification and readability-focused refactoring',
-    allowedAgents: ['oracle'],
-    sourcePath: 'src/skills/simplify',
-  },
-  {
-    name: 'codemap',
-    description: 'Repository understanding and hierarchical codemap generation',
-    allowedAgents: ['orchestrator'],
-    sourcePath: 'src/skills/codemap',
-  },
-  {
-    name: 'clonedeps',
-    description: 'Clone important dependency source for local inspection',
-    allowedAgents: ['orchestrator'],
-    sourcePath: 'src/skills/clonedeps',
-  },
-  {
-    name: 'deepwork',
-    description:
-      'Heavy/complex coding sessions and large modifications workflow',
-    allowedAgents: ['orchestrator'],
-    sourcePath: 'src/skills/deepwork',
-  },
-  {
-    name: 'reflect',
-    description:
-      'Review repeated work and suggest reusable workflow improvements',
-    allowedAgents: ['orchestrator'],
-    sourcePath: 'src/skills/reflect',
-  },
-  {
-    name: 'oh-my-opencode-slim',
-    description:
-      'Configure, customize, and safely improve oh-my-opencode-slim setups',
-    allowedAgents: ['orchestrator'],
-    sourcePath: 'src/skills/oh-my-opencode-slim',
-  },
-  {
-    name: 'release-smoke-test',
-    description:
-      'Validate packed release candidates and bugfixes before public publish',
-    allowedAgents: ['orchestrator'],
-    sourcePath: 'src/skills/release-smoke-test',
-  },
-  {
-    name: 'worktrees',
-    description:
-      'Manage Git worktrees as OMO safe isolated coding lanes for complex/risky/parallel work',
-    allowedAgents: ['orchestrator'],
-    sourcePath: 'src/skills/worktrees',
-  },
-];
+export { CUSTOM_SKILLS, type CustomSkill };
 
 
 /**
 /**
  * Get the target directory for custom skills installation.
  * Get the target directory for custom skills installation.

+ 31 - 26
src/hooks/auto-update-checker/skill-sync.ts

@@ -1,16 +1,21 @@
 import * as crypto from 'node:crypto';
 import * as crypto from 'node:crypto';
-import * as fs from 'node:fs';
 import {
 import {
   copyFileSync,
   copyFileSync,
   existsSync,
   existsSync,
   lstatSync,
   lstatSync,
   mkdirSync,
   mkdirSync,
   readdirSync,
   readdirSync,
+  readFileSync,
+  readlinkSync,
   renameSync,
   renameSync,
+  rmSync,
+  statSync,
+  unlinkSync,
+  writeFileSync,
 } from 'node:fs';
 } from 'node:fs';
 import * as os from 'node:os';
 import * as os from 'node:os';
 import * as path from 'node:path';
 import * as path from 'node:path';
-import { CUSTOM_SKILLS } from '../../cli/custom-skills';
+import { CUSTOM_SKILLS } from '../../cli/custom-skills-registry';
 import { getConfigDir } from '../../cli/paths';
 import { getConfigDir } from '../../cli/paths';
 import { log } from '../../utils/logger';
 import { log } from '../../utils/logger';
 
 
@@ -185,10 +190,10 @@ export function computeDirectoryHash(dirPath: string): string {
     hash.update(String(entry.mode & 0o7777));
     hash.update(String(entry.mode & 0o7777));
     hash.update('\0');
     hash.update('\0');
     if (entry.kind === 'file') {
     if (entry.kind === 'file') {
-      const content = fs.readFileSync(entry.absolutePath);
+      const content = readFileSync(entry.absolutePath);
       hash.update(content);
       hash.update(content);
     } else if (entry.kind === 'symlink') {
     } else if (entry.kind === 'symlink') {
-      hash.update(fs.readlinkSync(entry.absolutePath));
+      hash.update(readlinkSync(entry.absolutePath));
     }
     }
   }
   }
 
 
@@ -227,7 +232,7 @@ export function acquireLock(lockDir: string): boolean {
         time: Date.now(),
         time: Date.now(),
         token: PROCESS_TOKEN,
         token: PROCESS_TOKEN,
       };
       };
-      fs.writeFileSync(metadataPath, JSON.stringify(metadata), 'utf-8');
+      writeFileSync(metadataPath, JSON.stringify(metadata), 'utf-8');
     } catch {
     } catch {
       // Ignored
       // Ignored
     }
     }
@@ -250,7 +255,7 @@ export function acquireLock(lockDir: string): boolean {
 
 
     if (existsSync(metadataPath)) {
     if (existsSync(metadataPath)) {
       try {
       try {
-        const content = fs.readFileSync(metadataPath, 'utf-8');
+        const content = readFileSync(metadataPath, 'utf-8');
         const metadata = JSON.parse(content);
         const metadata = JSON.parse(content);
         ageMs = Date.now() - metadata.time;
         ageMs = Date.now() - metadata.time;
 
 
@@ -277,7 +282,7 @@ export function acquireLock(lockDir: string): boolean {
         shouldSteal = true;
         shouldSteal = true;
       }
       }
     } else {
     } else {
-      const stat = fs.statSync(lockDir);
+      const stat = statSync(lockDir);
       ageMs = Date.now() - stat.mtimeMs;
       ageMs = Date.now() - stat.mtimeMs;
       if (ageMs > 30000) {
       if (ageMs > 30000) {
         shouldSteal = true;
         shouldSteal = true;
@@ -287,7 +292,7 @@ export function acquireLock(lockDir: string): boolean {
     if (!shouldSteal) return false;
     if (!shouldSteal) return false;
 
 
     log(`[skill-sync] Stealing/recovering lock directory.`);
     log(`[skill-sync] Stealing/recovering lock directory.`);
-    fs.rmSync(lockDir, { recursive: true, force: true });
+    rmSync(lockDir, { recursive: true, force: true });
     mkdirSync(lockDir);
     mkdirSync(lockDir);
     writeMetadata();
     writeMetadata();
     ACQUIRED_LOCKS.add(path.resolve(lockDir));
     ACQUIRED_LOCKS.add(path.resolve(lockDir));
@@ -309,7 +314,7 @@ export function releaseLock(lockDir: string): void {
 
 
     if (existsSync(metadataPath)) {
     if (existsSync(metadataPath)) {
       try {
       try {
-        const content = fs.readFileSync(metadataPath, 'utf-8');
+        const content = readFileSync(metadataPath, 'utf-8');
         const metadata = JSON.parse(content);
         const metadata = JSON.parse(content);
         if (
         if (
           metadata.host === os.hostname() &&
           metadata.host === os.hostname() &&
@@ -330,7 +335,7 @@ export function releaseLock(lockDir: string): void {
 
 
     if (isOurLock) {
     if (isOurLock) {
       if (existsSync(lockDir)) {
       if (existsSync(lockDir)) {
-        fs.rmSync(lockDir, { recursive: true, force: true });
+        rmSync(lockDir, { recursive: true, force: true });
       }
       }
     } else if (existsSync(lockDir)) {
     } else if (existsSync(lockDir)) {
       log(
       log(
@@ -377,7 +382,7 @@ function atomicReplaceDir(sourceDir: string, destDir: string): void {
     renameSync(stagingDir, destDir);
     renameSync(stagingDir, destDir);
 
 
     if (backupCreated) {
     if (backupCreated) {
-      fs.rmSync(backupDir, { recursive: true, force: true });
+      rmSync(backupDir, { recursive: true, force: true });
     }
     }
   } catch (err) {
   } catch (err) {
     log(
     log(
@@ -388,7 +393,7 @@ function atomicReplaceDir(sourceDir: string, destDir: string): void {
     if (backupCreated) {
     if (backupCreated) {
       try {
       try {
         if (existsSync(destDir)) {
         if (existsSync(destDir)) {
-          fs.rmSync(destDir, { recursive: true, force: true });
+          rmSync(destDir, { recursive: true, force: true });
         }
         }
         renameSync(backupDir, destDir);
         renameSync(backupDir, destDir);
       } catch (rollbackErr) {
       } catch (rollbackErr) {
@@ -401,7 +406,7 @@ function atomicReplaceDir(sourceDir: string, destDir: string): void {
 
 
     try {
     try {
       if (existsSync(stagingDir)) {
       if (existsSync(stagingDir)) {
-        fs.rmSync(stagingDir, { recursive: true, force: true });
+        rmSync(stagingDir, { recursive: true, force: true });
       }
       }
     } catch {}
     } catch {}
 
 
@@ -480,7 +485,7 @@ function recoverOrphanArtifacts(
 
 
     for (const backup of backups) {
     for (const backup of backups) {
       try {
       try {
-        fs.rmSync(backup, { recursive: true, force: true });
+        rmSync(backup, { recursive: true, force: true });
       } catch (err) {
       } catch (err) {
         log(`[skill-sync] Failed to clean up backup folder ${backup}:`, err);
         log(`[skill-sync] Failed to clean up backup folder ${backup}:`, err);
       }
       }
@@ -489,7 +494,7 @@ function recoverOrphanArtifacts(
 
 
   for (const staging of stagings) {
   for (const staging of stagings) {
     try {
     try {
-      fs.rmSync(staging, { recursive: true, force: true });
+      rmSync(staging, { recursive: true, force: true });
     } catch (err) {
     } catch (err) {
       log(`[skill-sync] Failed to clean up staging folder ${staging}:`, err);
       log(`[skill-sync] Failed to clean up staging folder ${staging}:`, err);
     }
     }
@@ -518,7 +523,7 @@ function removeManagedStagedPath(
 
 
     if (isUnderRoot) {
     if (isUnderRoot) {
       if (existsSync(absoluteStagedPath)) {
       if (existsSync(absoluteStagedPath)) {
-        fs.rmSync(absoluteStagedPath, { recursive: true, force: true });
+        rmSync(absoluteStagedPath, { recursive: true, force: true });
         log(
         log(
           `[skill-sync] Safely cleaned up staged path for ${skillName}: ${absoluteStagedPath}`,
           `[skill-sync] Safely cleaned up staged path for ${skillName}: ${absoluteStagedPath}`,
         );
         );
@@ -588,7 +593,7 @@ export function syncBundledSkillsFromPackage(
   try {
   try {
     const pkgJsonPath = path.join(packageRoot, 'package.json');
     const pkgJsonPath = path.join(packageRoot, 'package.json');
     if (existsSync(pkgJsonPath)) {
     if (existsSync(pkgJsonPath)) {
-      const content = fs.readFileSync(pkgJsonPath, 'utf-8');
+      const content = readFileSync(pkgJsonPath, 'utf-8');
       const pkg = JSON.parse(content);
       const pkg = JSON.parse(content);
       if (pkg.version) {
       if (pkg.version) {
         packageVersion = pkg.version;
         packageVersion = pkg.version;
@@ -639,7 +644,7 @@ export function syncBundledSkillsFromPackage(
 
 
     if (existsSync(manifestPath)) {
     if (existsSync(manifestPath)) {
       try {
       try {
-        const content = fs.readFileSync(manifestPath, 'utf-8');
+        const content = readFileSync(manifestPath, 'utf-8');
         const parsed = JSON.parse(content);
         const parsed = JSON.parse(content);
         if (validateManifest(parsed)) {
         if (validateManifest(parsed)) {
           manifest = parsed;
           manifest = parsed;
@@ -768,7 +773,7 @@ export function syncBundledSkillsFromPackage(
                   skill.name,
                   skill.name,
                 );
                 );
                 if (existsSync(stagedSkillDir)) {
                 if (existsSync(stagedSkillDir)) {
-                  fs.rmSync(stagedSkillDir, { recursive: true, force: true });
+                  rmSync(stagedSkillDir, { recursive: true, force: true });
                 }
                 }
                 mkdirSync(stagedSkillDir, { recursive: true });
                 mkdirSync(stagedSkillDir, { recursive: true });
                 copyDirRecursive(sourcePath, stagedSkillDir);
                 copyDirRecursive(sourcePath, stagedSkillDir);
@@ -941,7 +946,7 @@ export function syncBundledSkillsFromPackage(
                     );
                     );
                   }
                   }
                   if (existsSync(stagedSkillDir)) {
                   if (existsSync(stagedSkillDir)) {
-                    fs.rmSync(stagedSkillDir, { recursive: true, force: true });
+                    rmSync(stagedSkillDir, { recursive: true, force: true });
                   }
                   }
                   mkdirSync(stagedSkillDir, { recursive: true });
                   mkdirSync(stagedSkillDir, { recursive: true });
                   copyDirRecursive(sourcePath, stagedSkillDir);
                   copyDirRecursive(sourcePath, stagedSkillDir);
@@ -1009,7 +1014,7 @@ export function syncBundledSkillsFromPackage(
                     );
                     );
                   }
                   }
                   if (existsSync(stagedSkillDir)) {
                   if (existsSync(stagedSkillDir)) {
-                    fs.rmSync(stagedSkillDir, { recursive: true, force: true });
+                    rmSync(stagedSkillDir, { recursive: true, force: true });
                   }
                   }
                   mkdirSync(stagedSkillDir, { recursive: true });
                   mkdirSync(stagedSkillDir, { recursive: true });
                   copyDirRecursive(sourcePath, stagedSkillDir);
                   copyDirRecursive(sourcePath, stagedSkillDir);
@@ -1104,7 +1109,7 @@ export function syncBundledSkillsFromPackage(
                 skill.name,
                 skill.name,
               );
               );
               if (existsSync(stagedSkillDir)) {
               if (existsSync(stagedSkillDir)) {
-                fs.rmSync(stagedSkillDir, { recursive: true, force: true });
+                rmSync(stagedSkillDir, { recursive: true, force: true });
               }
               }
               mkdirSync(stagedSkillDir, { recursive: true });
               mkdirSync(stagedSkillDir, { recursive: true });
               copyDirRecursive(sourcePath, stagedSkillDir);
               copyDirRecursive(sourcePath, stagedSkillDir);
@@ -1143,18 +1148,18 @@ export function syncBundledSkillsFromPackage(
     manifest.updatedAt = new Date().toISOString();
     manifest.updatedAt = new Date().toISOString();
     const tempManifestPath = `${manifestPath}.${Math.random().toString(36).slice(2, 9)}.tmp`;
     const tempManifestPath = `${manifestPath}.${Math.random().toString(36).slice(2, 9)}.tmp`;
     try {
     try {
-      fs.writeFileSync(
+      writeFileSync(
         tempManifestPath,
         tempManifestPath,
         JSON.stringify(manifest, null, 2),
         JSON.stringify(manifest, null, 2),
         'utf-8',
         'utf-8',
       );
       );
-      fs.renameSync(tempManifestPath, manifestPath);
+      renameSync(tempManifestPath, manifestPath);
     } catch (err) {
     } catch (err) {
       log('[skill-sync] Failed to write skills manifest atomically:', err);
       log('[skill-sync] Failed to write skills manifest atomically:', err);
       manifestWriteFailed = true;
       manifestWriteFailed = true;
       try {
       try {
-        if (fs.existsSync(tempManifestPath)) {
-          fs.unlinkSync(tempManifestPath);
+        if (existsSync(tempManifestPath)) {
+          unlinkSync(tempManifestPath);
         }
         }
       } catch {}
       } catch {}
     }
     }