mirror of
https://github.com/Heretek-AI/awesome-opencode.git
synced 2026-11-07 10:45:57 -10:00
fix: quieter retarget warnings and plugin/project split in the scout
check-pr no longer flags repo URL changes that GitHub redirects (renames/transfers) or that only add a monorepo path, so bot rename PRs stay clean. The scout files packages built on @opencode/plugin as plugins and other @opencode/* consumers (SDK/client tools) as projects. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
91287042b8
commit
7b219dd82a
3 files changed
+33
-14
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# Weekly discovery of OpenCode V2 plugins on npm that are not in the catalog yet.
|
||||
# Weekly discovery of OpenCode V2 plugins and SDK tools on npm that are not in the catalog yet.
|
||||
# Opens a "Scout" issue; ticking a candidate's box opens a PR (see issues.yml).
|
||||
name: Scout V2 plugins
|
||||
name: Scout V2 packages
|
||||
|
||||
on:
|
||||
schedule:
|
||||
|
||||
+13
-9
@@ -22,7 +22,7 @@ const { getNpmLatest, getNpmWeeklyDownloads, getRepos, rest } = require('./utils
|
||||
const { openPullRequest } = require('./utils/git-pr');
|
||||
const { REPO, ensureLabels, listOpenPulls } = require('./utils/repo');
|
||||
const { buildEntry } = require('./utils/submission');
|
||||
const { suggestTopics } = require('./utils/suggest');
|
||||
const { suggestKind, suggestTopics } = require('./utils/suggest');
|
||||
|
||||
const V2_RELEASED = '2026-09-02';
|
||||
const SEARCHES = ['keywords:opencode-plugin', '@opencode/plugin', 'keywords:opencode'];
|
||||
@@ -144,13 +144,16 @@ async function findCandidates(limit) {
|
||||
function candidateInput(c) {
|
||||
const description = truncate(c.info.description || c.manifest.description || c.pkg.description || '', 600);
|
||||
const text = [c.pkg.name, description, (c.manifest.keywords || []).join(' ')].join(' ');
|
||||
// Packages built on the plugin API are plugins; other @opencode/* consumers (SDK/client) are projects
|
||||
const isPlugin = c.markers.v2.some(m => /^@opencode(-ai)?\/plugin\b/.test(m));
|
||||
const category = isPlugin ? 'plugins' : 'projects';
|
||||
return {
|
||||
name: c.pkg.name,
|
||||
repo: c.source.repo,
|
||||
path: c.source.path,
|
||||
npm: c.pkg.name,
|
||||
category: 'plugins',
|
||||
kind: 'plugin',
|
||||
category,
|
||||
kind: isPlugin ? 'plugin' : suggestKind('projects', text),
|
||||
topics: suggestTopics(text),
|
||||
tagline: truncate(c.manifest.description || description || c.pkg.name, 120),
|
||||
description: description || c.pkg.name
|
||||
@@ -164,10 +167,11 @@ async function run({ dryRun, limit }) {
|
||||
const lines = candidates.map((c, i) => {
|
||||
const id = `c${i + 1}`;
|
||||
data[id] = candidateInput(c);
|
||||
return `- [ ] **${c.pkg.name}** — ${truncate(c.manifest.description || '', 140)} · ⭐ ${c.info.stargazerCount} · ⬇ ${c.downloads}/wk · [repo](${c.source.repo}) · [npm](https://www.npmjs.com/package/${c.pkg.name}) <!-- scout:${id} -->`;
|
||||
const label = data[id].category === 'plugins' ? 'plugin' : data[id].kind;
|
||||
return `- [ ] **${c.pkg.name}** (${label}) — ${truncate(c.manifest.description || '', 140)} · ⭐ ${c.info.stargazerCount} · ⬇ ${c.downloads}/wk · [repo](${c.source.repo}) · [npm](https://www.npmjs.com/package/${c.pkg.name}) <!-- scout:${id} -->`;
|
||||
});
|
||||
const body = [
|
||||
`OpenCode V2 plugins on npm that are not in the catalog yet: published since ${V2_RELEASED}, depending on \`@opencode/*\`, public, licensed, not archived or forked. Ranked by weekly downloads, then stars.`,
|
||||
`OpenCode V2 packages on npm that are not in the catalog yet: published since ${V2_RELEASED}, depending on \`@opencode/*\` (plugins on \`@opencode/plugin\`, other tools on the SDK/client), public, licensed, not archived or forked. Ranked by weekly downloads, then stars.`,
|
||||
'',
|
||||
'**Tick a box to open a pull request for that candidate** (maintainers only; topics are a keyword guess, fix them in the PR). To stop suggesting a package, add it to [`discovery/declined.txt`](https://github.com/Heretek-AI/awesome-opencode/blob/main/discovery/declined.txt).',
|
||||
'',
|
||||
@@ -183,7 +187,7 @@ async function run({ dryRun, limit }) {
|
||||
for (const old of Array.isArray(open) ? open : []) {
|
||||
if (!old.pull_request) await rest(`/repos/${REPO}/issues/${old.number}`, { method: 'PATCH', body: { state: 'closed', state_reason: 'completed' } });
|
||||
}
|
||||
const res = await rest(`/repos/${REPO}/issues`, { method: 'POST', body: { title: `Scout: V2 plugin candidates (${week})`, body, labels: ['scout'] } });
|
||||
const res = await rest(`/repos/${REPO}/issues`, { method: 'POST', body: { title: `Scout: V2 candidates (${week})`, body, labels: ['scout'] } });
|
||||
console.log(`Opened scout issue #${res.data.number} with ${candidates.length} candidate(s)`);
|
||||
}
|
||||
|
||||
@@ -211,11 +215,11 @@ async function accept({ issueNumber, sender, dryRun }) {
|
||||
note = ` → not added: ${result.problems.join('; ').slice(0, 200)}`;
|
||||
} else {
|
||||
const pr = await openPullRequest({
|
||||
branch: `scout/${result.file.replace(/^data\/plugins\/|\.yaml$/g, '')}`,
|
||||
branch: `scout/${result.file.replace(/^data\/[a-z]+\/|\.yaml$/g, '')}`,
|
||||
files: { [result.file]: result.text },
|
||||
title: `docs: add ${input.name} to plugins`,
|
||||
title: `docs: add ${input.name} to ${input.category}`,
|
||||
body: [`Found by the weekly scout in #${issueNumber}.`, '', ...result.notes.map(n => `- ${n}`), '', 'Topics are a keyword guess; adjust them before merging.'].join('\n'),
|
||||
labels: ['scout', 'category: plugins'],
|
||||
labels: ['scout', `category: ${input.category}`],
|
||||
dryRun
|
||||
});
|
||||
note = pr ? ` → #${pr.number}` : ' → (no changes)';
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
|
||||
const path = require('path');
|
||||
const yaml = require('js-yaml');
|
||||
const { CATEGORIES, canonicalKey, checkCatalog } = require('./catalog');
|
||||
const { CATEGORIES, canonicalKey, checkCatalog, parseRepoUrl } = require('./catalog');
|
||||
const { probeCompat } = require('./compat');
|
||||
const { getRepos } = require('./github');
|
||||
|
||||
// Generated files a PR must never edit by hand
|
||||
const GENERATED_FILES = ['README.md', 'dist/registry.json'];
|
||||
@@ -47,6 +48,7 @@ async function analyzePullRequest({ changes, baseEntries, probe = true }) {
|
||||
// Overlay the PR's data changes onto the base catalog
|
||||
const head = new Map(baseEntries.map(e => [e.file, e]));
|
||||
const changed = [];
|
||||
const retargeted = [];
|
||||
for (const change of changes.filter(c => DATA_FILE_RE.test(c.path))) {
|
||||
head.delete(change.path);
|
||||
if (change.status === 'removed') continue;
|
||||
@@ -69,13 +71,26 @@ async function analyzePullRequest({ changes, baseEntries, probe = true }) {
|
||||
if (change.status === 'modified' && change.baseContent) {
|
||||
try {
|
||||
const before = yaml.load(change.baseContent);
|
||||
if (before && before.repo && data.repo && canonicalKey(before) !== canonicalKey(data)) {
|
||||
warnings.push({ file: change.path, kind: 'human', message: `changes this entry's target from ${before.repo} to ${data.repo}. If this is a different project, add a new file instead of reusing an existing filename.` });
|
||||
// Adding or changing `path` within the same repo is not a new target
|
||||
if (before && before.repo && data.repo && canonicalKey({ repo: before.repo }) !== canonicalKey({ repo: data.repo })) {
|
||||
retargeted.push({ file: change.path, from: before.repo, to: data.repo });
|
||||
}
|
||||
} catch { /* base file was already broken */ }
|
||||
}
|
||||
}
|
||||
|
||||
// A changed repo URL is fine when GitHub redirects the old name to the new one (rename/transfer)
|
||||
if (retargeted.length) {
|
||||
const olds = retargeted.map(r => parseRepoUrl(r.from)).filter(p => p && p.host === 'github.com');
|
||||
const resolved = probe ? await getRepos(olds.map(p => ({ owner: p.owner, name: p.name }))).catch(() => new Map()) : new Map();
|
||||
for (const r of retargeted) {
|
||||
const old = parseRepoUrl(r.from);
|
||||
const info = old && resolved.get(`${old.owner}/${old.name}`);
|
||||
if (info && canonicalKey({ repo: `https://github.com/${info.nameWithOwner}` }) === canonicalKey({ repo: r.to })) continue;
|
||||
warnings.push({ file: r.file, kind: 'human', message: `changes this entry's target from ${r.from} to ${r.to}. If this is a different project, add a new file instead of reusing an existing filename.` });
|
||||
}
|
||||
}
|
||||
|
||||
// YAML files outside data/<category>/
|
||||
for (const change of changes) {
|
||||
if (change.path.startsWith('data/') && change.path.endsWith('.yaml') && !DATA_FILE_RE.test(change.path) && !change.path.startsWith('data/examples/') && change.status !== 'removed') {
|
||||
|
||||
Reference in new issue
Block a user