Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/sync-plugins.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TURSO_DATABASE_URL: ${{ secrets.TURSO_DATABASE_URL }}
TURSO_AUTH_TOKEN: ${{ secrets.TURSO_AUTH_TOKEN }}
# 探测安装方式:新收录的仓库 + 超过 7 天没探过的重探(作者可能发布 npm、
# GitHub Release tarball 或补 prepare)。每个有效 bundle 最多请求一次 Releases API。
# 探测安装方式:新收录、上次探测后有 push,或超过 7 天没探过的仓库。
# 作者可能发布 npm / Release 或补 prepare每个有效 bundle 最多请求一次 Releases API。
- run: pnpm probe:install
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down
53 changes: 53 additions & 0 deletions scripts/lib/install-metadata.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import assert from "node:assert/strict";
import test from "node:test";

import { createClient } from "@libsql/client";

import {
installProbeFilter,
resolveLiveInstallDisplay,
} from "../../src/lib/install-metadata.mjs";

test("a repository pushed after its fresh install probe is selected again", async (t) => {
const client = createClient({ url: "file::memory:" });
t.after(() => client.close());
await client.execute(`CREATE TABLE plugins (
full_name TEXT, is_present INTEGER, is_offtopic INTEGER,
pushed_at TEXT, install_probed_at TEXT, stars INTEGER
)`);
await client.batch(
[
["pushed/repo", "2026-08-16T01:00:00.000Z", "2026-08-15T01:00:00.000Z"],
["quiet/repo", "2026-08-14T01:00:00.000Z", "2026-08-15T01:00:00.000Z"],
].map(([fullName, pushedAt, probedAt], stars) => ({
sql: "INSERT INTO plugins VALUES (?, 1, 0, ?, ?, ?)",
args: [fullName, pushedAt, probedAt, stars],
})),
"write",
);

const filter = installProbeFilter({
only: [],
rederive: false,
all: false,
staleDays: 7,
now: Date.parse("2026-08-16T02:00:00.000Z"),
});
const selected = await client.execute({
sql: `SELECT full_name FROM plugins
WHERE is_present = 1 AND is_offtopic = 0${filter.sql}
ORDER BY stars DESC`,
args: filter.args,
});
assert.deepEqual(selected.rows.map((row) => String(row.full_name)), ["pushed/repo"]);
});

test("a live null never falls back to a stale editorial install command", () => {
const display = resolveLiveInstallDisplay({
liveCuratedCmd: null,
automaticCmd: null,
automaticKind: null,
editorialCmd: "dsh plugin add old-release.tgz",
});
assert.deepEqual(display, { installCmd: null, installKind: null });
});
11 changes: 11 additions & 0 deletions scripts/lib/install.test.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";

import {
Expand Down Expand Up @@ -264,3 +265,13 @@ test("preserves npm, build-required, git, and not-installable fallbacks", () =>
);
assert.equal(deriveInstall(installFacts({ hasBundle: false })).kind, "not-installable");
});

test("probe preserves an intentional curated pin even if it equals an earlier automatic command", async () => {
// String equality cannot establish provenance: an operator may deliberately keep that exact pin.
// Guard the write statement itself so probing may refresh install_cmd_auto but never install_cmd.
const source = await readFile(new URL("../probe-install.mjs", import.meta.url), "utf8");
const update = source.match(/sql: `UPDATE plugins SET([\s\S]*?)WHERE full_name = \?`/);
assert.ok(update, "probe UPDATE statement not found");
assert.doesNotMatch(update[1], /(?:^|[,\s])install_cmd\s*=/m);
assert.match(update[1], /(?:^|[,\s])install_cmd_auto\s*=/m);
});
18 changes: 7 additions & 11 deletions scripts/probe-install.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* 探测每个收录仓库「到底怎么装」,把事实与推导结论写回 Turso plugins 表。
*
* 用法:
* pnpm probe:install # 探测所有从未探过 / 超过 7 天没探的仓库
* pnpm probe:install # 探测新仓库、最近有 push 或超过 7 天没探的仓库
* pnpm probe:install --stale-days 30 # 换个新鲜度阈值
* pnpm probe:install --all # 无视新鲜度,全部重探
* pnpm probe:install --only owner/repo # 只探一个(可重复传)
Expand All @@ -29,6 +29,7 @@ import {
mergeReleaseProbe,
probeTimestamp,
} from "./lib/github-release-probe.mjs";
import { installProbeFilter } from "../src/lib/install-metadata.mjs";

const CONCURRENCY = 8;
const DEFAULT_STALE_DAYS = 7;
Expand Down Expand Up @@ -217,16 +218,11 @@ let sql = `SELECT full_name, pkg_name, pkg_version, pkg_private, has_bundle, has
release_asset_size, release_asset_digest, release_etag, install_probed_at
FROM plugins WHERE is_present = 1 AND is_offtopic = 0`;
const args = [];
if (only.length) {
sql += ` AND lower(full_name) IN (${only.map(() => "?").join(",")})`;
args.push(...only.map((s) => s.toLowerCase()));
} else if (rederive) {
sql += ` AND install_probed_at IS NOT NULL`;
} else if (!all) {
const cutoff = new Date(Date.now() - staleDays * 86400_000).toISOString();
sql += ` AND (install_probed_at IS NULL OR install_probed_at < ?)`;
args.push(cutoff);
}
// sync:db 已在本任务前刷新 pushed_at。仓库刚有 push 时不等满 7 天,下一轮就重探;
// 没有活动的仓库仍按 staleDays 兜底,避免每天扫描全部安装元数据。
const filter = installProbeFilter({ only, rederive, all, staleDays });
sql += filter.sql;
args.push(...filter.args);
sql += ` ORDER BY stars DESC`;

const rows = (await client.execute({ sql, args })).rows;
Expand Down
13 changes: 9 additions & 4 deletions src/app/[locale]/plugins/[owner]/[repo]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { getPluginDetail } from "@/lib/plugins-db";
import { isLocale, type Locale } from "@/i18n/config";
import { pageAlternates, SITE_URL } from "@/lib/site";
import { ShareCardBox } from "@/components/share-card-box";
import { resolveLiveInstallDisplay } from "@/lib/install-metadata.mjs";

type Params = Promise<{ locale: string; owner: string; repo: string }>;

Expand Down Expand Up @@ -73,12 +74,16 @@ export default async function PluginDetailPage({
const live = plugin.i18n[loc];
const intro = live?.intro ?? editorial?.intro?.[loc];
const highlights = live?.highlights ?? editorial?.highlights?.[loc];
// 安装命令三个来源,优先级递减:运营人工核对 → 构建期生成物 → 按 package.json/npm 推导。
// 安装命令只读实时库:运营人工核对 → 按 package.json/npm 推导。安装目标会过期,
// DB 明确返回 null 时不能再让构建期快照复活一条已撤销的旧命令;DB 不可用时
// getPluginDetail 会把安装结论整体置空并引导用户看仓库 README。
// 都没有时不再拿 fullName 硬拼 `github:` —— 仓库名不足以推出安装方式,编出来的命令
// 对索引仓库、非组合包、未构建的 TS 包一律是错的(判定见 scripts/lib/install.mjs)。
const curatedCmd = plugin.installCmd ?? editorial?.installCmd ?? null;
const installCmd = curatedCmd ?? plugin.installCmdAuto;
const installKind = curatedCmd ? "curated" : plugin.installKind;
const { installCmd, installKind } = resolveLiveInstallDisplay({
liveCuratedCmd: plugin.installCmd,
automaticCmd: plugin.installCmdAuto,
automaticKind: plugin.installKind,
});
const description =
live?.description ??
localizePluginDescription(plugin.fullName, locale, plugin.description);
Expand Down
37 changes: 37 additions & 0 deletions src/lib/install-metadata.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const DAY_MS = 86_400_000;

/** Build the mutually exclusive scope filter used by the install probe query. */
export function installProbeFilter({
only,
rederive,
all,
staleDays,
now = Date.now(),
}) {
if (only.length) {
return {
sql: ` AND lower(full_name) IN (${only.map(() => "?").join(",")})`,
args: only.map((name) => name.toLowerCase()),
};
}
if (rederive) return { sql: " AND install_probed_at IS NOT NULL", args: [] };
if (all) return { sql: "", args: [] };

const cutoff = new Date(now - staleDays * DAY_MS).toISOString();
return {
sql: " AND (install_probed_at IS NULL OR install_probed_at < ? OR pushed_at > install_probed_at)",
args: [cutoff],
};
}

/** Resolve install UI strictly from live database fields, never from an editorial snapshot. */
export function resolveLiveInstallDisplay({
liveCuratedCmd,
automaticCmd,
automaticKind,
}) {
return {
installCmd: liveCuratedCmd ?? automaticCmd,
installKind: liveCuratedCmd ? "curated" : automaticKind,
};
}