Browse Source

fix(opencode): adapt to V2 skill draft API removal (#2106)

OpenCode V2 removed SkillDraft.source() in 1113adfd5e (#41622): the
skill service now stores values only, and filesystem scanning moved to
the config side. The plugin's draft.source({type:'directory'}) call
threw 'draft.source is not a function', which killed the entire V2
plugin activation generation. Because V2 gates model.list on
PluginSupervisor.flush (23b0688a7f, #41783), that failure left flush
pending forever and the TUI showed no providers or models
('Model catalog initialization timed out', /api/model 503).

Register each skills/<name>/SKILL.md as a native Skill.Info object via
draft.add({id, name, description, location, content}) instead, matching
the {list, add, update, remove} draft API and the pattern used by
V2's built-in skill plugin. Wrap skill registration, hook registration,
and the context hook callback in try/catch so a future V2 API change
degrades to a logged error instead of taking down the whole generation
again. session.hook('context') payload shape is unchanged and keeps
working.

Verified end-to-end on opencode2 v0.0.0-beta-17595: 24 skills listed
via /api/skill (all superpowers skills present), /api/model returns
83 models across 3 providers, no 'failed to reload plugins' in server
logs. V1 path untouched.
GoldJohnKing 2 tháng trước cách đây
mục cha
commit
6e31f3f7db
1 tập tin đã thay đổi với 49 bổ sung và 16 xóa
  1. 49 16
      .opencode/plugins/superpowers.js

+ 49 - 16
.opencode/plugins/superpowers.js

@@ -142,31 +142,64 @@ export const SuperpowersPlugin = async ({ client, directory }) => {
 /**
  * V2 Setup Function (default.setup)
  *
- * Called by V2 PluginSupervisor (packages/core/src/plugin/).
+ * Called by V2 PluginSupervisor (packages/core/src/plugin/supervisor.ts).
  * Performs two things:
  *
- * 1. Registers the skills directory natively via ctx.skill.transform().
+ * 1. Registers every skills/<name>/SKILL.md as a native Skill.Info object
+ *    via ctx.skill.transform((draft) => draft.add(info)).
+ *    V2 removed the old draft.source() directory registration; the draft API
+ *    is now { list, add, update, remove } where add() decodes plain objects:
+ *    { id, name, description?, slash?, autoinvoke?, location, content }.
+ *    See packages/core/src/plugin/skill.ts and packages/schema/src/skill.ts.
  * 2. Injects bootstrap context via ctx.session.hook("context"), the V2
  *    equivalent of V1's experimental.chat.messages.transform.
  */
 async function setup(ctx) {
-  // 1. Register skills
-  await ctx.skill.transform((draft) => {
-    draft.source({
-      type: 'directory',
-      path: superpowersSkillsDir,
+  // 1. Register skills (one transform; one draft.add per skill)
+  try {
+    const skills = [];
+    if (fs.existsSync(superpowersSkillsDir)) {
+      for (const entry of fs.readdirSync(superpowersSkillsDir, { withFileTypes: true })) {
+        if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
+        const skillPath = path.join(superpowersSkillsDir, entry.name, 'SKILL.md');
+        if (!fs.existsSync(skillPath)) continue;
+        const { frontmatter, content } = extractAndStripFrontmatter(fs.readFileSync(skillPath, 'utf8'));
+        skills.push({
+          id: entry.name,
+          name: frontmatter.name || entry.name,
+          ...(frontmatter.description ? { description: frontmatter.description } : {}),
+          location: skillPath,
+          content,
+        });
+      }
+    }
+    await ctx.skill.transform((draft) => {
+      for (const skill of skills) draft.add(skill);
     });
-  });
+  } catch (err) {
+    // Never break plugin activation: one failing plugin takes down the whole
+    // V2 generation (including provider/catalog plugins => no models in TUI).
+    console.error('[superpowers] skill registration failed:', err);
+  }
 
   // 2. Inject bootstrap into first user message via V2 session context hook
-  await ctx.session.hook('context', (event) => {
-    const bootstrap = getBootstrapContent();
-    if (!bootstrap || !event.messages || !event.messages.length) return;
-    const firstUser = event.messages.find(m => m.role === 'user');
-    if (!firstUser || !firstUser.content || !firstUser.content.length) return;
-    if (firstUser.content.some(p => p.type === 'text' && p.text && p.text.includes('EXTREMELY_IMPORTANT'))) return;
-    firstUser.content.unshift({ type: 'text', text: bootstrap });
-  });
+  try {
+    await ctx.session.hook('context', (event) => {
+      try {
+        const bootstrap = getBootstrapContent();
+        if (!bootstrap || !event.messages || !event.messages.length) return;
+        const firstUser = event.messages.find(m => m.role === 'user');
+        if (!firstUser || !firstUser.content || !firstUser.content.length) return;
+        if (firstUser.content.some(p => p.type === 'text' && p.text && p.text.includes('EXTREMELY_IMPORTANT'))) return;
+        firstUser.content.unshift({ type: 'text', text: bootstrap });
+      } catch (err) {
+        // Never let hook callback errors break the request pipeline.
+        console.error('[superpowers] context hook failed:', err);
+      }
+    });
+  } catch (err) {
+    console.error('[superpowers] session hook registration failed:', err);
+  }
 }
 
 /**