@max-null/dsh-plugin-center 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/meta.js ADDED
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Installed-plugin metadata: resolve each Loader entry's specifier to a
3
+ * package.json, classify its provenance, and read version / description /
4
+ * DSH-compat range. Read-only projection — the Loader stays the authority.
5
+ */
6
+ import { createRequire } from 'node:module';
7
+ import { readFile } from 'node:fs/promises';
8
+ import { dirname, join } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ /** Compact a module specifier into a display name without guessing Loader id shape. */
11
+ export function displayName(specifier) {
12
+ const unscoped = specifier.startsWith('@')
13
+ ? specifier.slice(specifier.indexOf('/') + 1)
14
+ : specifier;
15
+ return unscoped
16
+ .replace(/^cordis:/, '')
17
+ .replace(/^cordis-plugin-/, '')
18
+ .replace(/^dsh-(?:host-|client-)?/, '');
19
+ }
20
+ /** The `@deepseek-ai/dsh*` peer-dependency range, or null when undeclared. */
21
+ function dshCompatRange(pkg) {
22
+ const peers = pkg.peerDependencies ?? {};
23
+ for (const [name, range] of Object.entries(peers)) {
24
+ if (name.startsWith('@deepseek-ai/dsh'))
25
+ return range;
26
+ }
27
+ return null;
28
+ }
29
+ /** Classify provenance from the specifier shape alone (matches the §4.2 design). */
30
+ function classifySource(specifier) {
31
+ if (specifier.startsWith('@deepseek-ai/dsh-'))
32
+ return 'official';
33
+ if (specifier.startsWith('file://') || specifier.startsWith('link:'))
34
+ return 'local';
35
+ if (specifier.startsWith('cordis:'))
36
+ return 'builtin';
37
+ return 'installed';
38
+ }
39
+ /** Process-local cache of resolved packages — stable per run, so resolve once. */
40
+ const packageCache = new Map();
41
+ /**
42
+ * Resolve one Loader entry to its package.json. `file://` specs walk upward to
43
+ * the nearest directory holding a package.json; `cordis:*` builtins have none.
44
+ * Results are cached per (baseUrl, specifier) — the resolution is a pure read
45
+ * and never changes within a process, so the file I/O happens only once.
46
+ * @param baseUrl - profile directory (the cordis.yml anchor, `ctx.baseUrl`).
47
+ * @param specifier - the Loader entry's module specifier.
48
+ * @returns the parsed package.json and its directory, or null when unresolvable.
49
+ */
50
+ export function resolvePackage(baseUrl, specifier) {
51
+ const key = `${baseUrl}\u0000${specifier}`;
52
+ const cached = packageCache.get(key);
53
+ if (cached !== undefined)
54
+ return cached;
55
+ const pending = resolveUncached(baseUrl, specifier);
56
+ packageCache.set(key, pending);
57
+ return pending;
58
+ }
59
+ async function resolveUncached(baseUrl, specifier) {
60
+ if (specifier.startsWith('file://')) {
61
+ let dir = dirname(fileURLToPath(specifier));
62
+ for (let i = 0; i < 12; i++) {
63
+ const path = join(dir, 'package.json');
64
+ try {
65
+ return { pkg: JSON.parse(await readFile(path, 'utf8')), dir };
66
+ }
67
+ catch {
68
+ const parent = dirname(dir);
69
+ if (parent === dir)
70
+ return null;
71
+ dir = parent;
72
+ }
73
+ }
74
+ return null;
75
+ }
76
+ if (specifier.startsWith('cordis:'))
77
+ return null;
78
+ try {
79
+ const require = createRequire(join(baseUrl, 'package.json'));
80
+ const path = require.resolve(`${specifier}/package.json`);
81
+ return { pkg: JSON.parse(await readFile(path, 'utf8')), dir: dirname(path) };
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ }
87
+ /** Build the Remote-ready metadata for one Loader entry. */
88
+ export async function buildInstalledPlugin(baseUrl, entry) {
89
+ const resolved = await resolvePackage(baseUrl, entry.name);
90
+ const source = classifySource(entry.name);
91
+ return {
92
+ entryId: entry.id,
93
+ name: entry.name,
94
+ displayName: displayName(entry.name),
95
+ version: resolved?.pkg.version ?? null,
96
+ description: resolved?.pkg.description ?? null,
97
+ source,
98
+ enabled: !entry.disabled,
99
+ fiberPhase: entry.fiberPhase,
100
+ compatRange: resolved === null ? null : dshCompatRange(resolved.pkg),
101
+ repoUrl: resolved === null ? null : (() => {
102
+ const r = resolved.pkg.repository;
103
+ if (typeof r === 'string')
104
+ return r;
105
+ if (r !== null && typeof r === 'object' && typeof r.url === 'string')
106
+ return r.url;
107
+ return null;
108
+ })(),
109
+ categories: [],
110
+ };
111
+ }
package/dist/rpc.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `PluginCenterRpc` — a private loopback RPC channel exposing the engine to
3
+ * the browser half. The Typert Remote path is closed to third-party packages
4
+ * (api-remotes imports an explicit allowlist of official `./remote` artifacts),
5
+ * so the client calls `ctx.connection.rpc.call('/plugin-center', ...)` instead
6
+ * of `ctx.remote.pluginCenter.*` — the same seam dsh-think-any-lang uses.
7
+ */
8
+ import { Service, type Context } from '@deepseek-ai/cordis';
9
+ export declare class PluginCenterRpc extends Service {
10
+ static inject: string[];
11
+ constructor(ctx: Context);
12
+ }
13
+ export default PluginCenterRpc;
package/dist/rpc.js ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * `PluginCenterRpc` — a private loopback RPC channel exposing the engine to
3
+ * the browser half. The Typert Remote path is closed to third-party packages
4
+ * (api-remotes imports an explicit allowlist of official `./remote` artifacts),
5
+ * so the client calls `ctx.connection.rpc.call('/plugin-center', ...)` instead
6
+ * of `ctx.remote.pluginCenter.*` — the same seam dsh-think-any-lang uses.
7
+ */
8
+ import { Service } from '@deepseek-ai/cordis';
9
+ const CHANNEL = '/plugin-center';
10
+ /** Fold a thrown value into the RpcResult error branch (closed `internal` code). */
11
+ function internal(message) {
12
+ return { ok: false, error: { code: 'internal', message, details: {} } };
13
+ }
14
+ export class PluginCenterRpc extends Service {
15
+ static inject = ['pluginCenter', 'connection'];
16
+ constructor(ctx) {
17
+ super(ctx, 'pluginCenterRpc');
18
+ ctx.connection.rpc.handle(CHANNEL, async (endpoint, payload) => {
19
+ try {
20
+ switch (endpoint) {
21
+ case 'listInstalled':
22
+ return { ok: true, value: await ctx.pluginCenter.listInstalled() };
23
+ case 'listMarket':
24
+ return { ok: true, value: await ctx.pluginCenter.listMarket((payload?.source ?? 'all')) };
25
+ case 'checkUpdates':
26
+ return { ok: true, value: await ctx.pluginCenter.checkUpdates(payload?.since ?? '') };
27
+ case 'install': {
28
+ const spec = payload?.spec;
29
+ if (typeof spec !== 'string' || spec === '')
30
+ return internal('install: spec is required');
31
+ return { ok: true, value: await ctx.pluginCenter.install(spec) };
32
+ }
33
+ case 'update': {
34
+ const name = payload?.name;
35
+ if (typeof name !== 'string' || name === '')
36
+ return internal('update: name is required');
37
+ return { ok: true, value: await ctx.pluginCenter.update(name) };
38
+ }
39
+ case 'debug':
40
+ return { ok: true, value: await ctx.pluginCenter.debug() };
41
+ case 'readVersions':
42
+ return { ok: true, value: await ctx.pluginCenter.readVersions() };
43
+ case 'markRead': {
44
+ const versions = payload?.versions ?? {};
45
+ return { ok: true, value: await ctx.pluginCenter.markRead(versions) };
46
+ }
47
+ default:
48
+ return internal(`unknown endpoint "${endpoint}"`);
49
+ }
50
+ }
51
+ catch (error) {
52
+ return internal(error instanceof Error ? error.message : String(error));
53
+ }
54
+ }, { authority: 'loopback' });
55
+ }
56
+ }
57
+ export default PluginCenterRpc;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Hand-rolled semver comparison for `major.minor.patch[-prerelease]`.
3
+ * Kept dependency-free: the plugin compares local vs remote versions and
4
+ * matches peer-dependency ranges with just these two rules.
5
+ */
6
+ /** A parsed semver (prerelease tag kept verbatim for lexical tie-breaks). */
7
+ export interface Semver {
8
+ major: number;
9
+ minor: number;
10
+ patch: number;
11
+ pre: string;
12
+ }
13
+ /** Parse a `major.minor.patch[-prerelease]` (optional `v` prefix); null when unparsable. */
14
+ export declare function parseVersion(version: string): Semver | null;
15
+ /** Compare two versions: -1 (a<b), 0 (equal), 1 (a>b). Unparsable sides compare equal. */
16
+ export declare function compareVersions(a: string, b: string): number;
17
+ /** Whether `version` satisfies a caret/tilde/range constraint like `^0.1.0-rc.6` or `>=0.0.1-rc.1`. */
18
+ export declare function satisfies(version: string, range: string): boolean;
package/dist/semver.js ADDED
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Hand-rolled semver comparison for `major.minor.patch[-prerelease]`.
3
+ * Kept dependency-free: the plugin compares local vs remote versions and
4
+ * matches peer-dependency ranges with just these two rules.
5
+ */
6
+ /** Parse a `major.minor.patch[-prerelease]` (optional `v` prefix); null when unparsable. */
7
+ export function parseVersion(version) {
8
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/.exec(version);
9
+ if (match === null)
10
+ return null;
11
+ return {
12
+ major: Number(match[1]),
13
+ minor: Number(match[2]),
14
+ patch: Number(match[3]),
15
+ pre: match[4] ?? '',
16
+ };
17
+ }
18
+ /** Compare two versions: -1 (a<b), 0 (equal), 1 (a>b). Unparsable sides compare equal. */
19
+ export function compareVersions(a, b) {
20
+ const pa = parseVersion(a);
21
+ const pb = parseVersion(b);
22
+ if (pa === null || pb === null)
23
+ return 0;
24
+ for (const key of ['major', 'minor', 'patch']) {
25
+ if (pa[key] !== pb[key])
26
+ return pa[key] > pb[key] ? 1 : -1;
27
+ }
28
+ if (pa.pre === pb.pre)
29
+ return 0;
30
+ if (pa.pre === '')
31
+ return 1; // release > prerelease
32
+ if (pb.pre === '')
33
+ return -1;
34
+ return pa.pre > pb.pre ? 1 : -1;
35
+ }
36
+ /** Whether `version` satisfies a caret/tilde/range constraint like `^0.1.0-rc.6` or `>=0.0.1-rc.1`. */
37
+ export function satisfies(version, range) {
38
+ const v = parseVersion(version);
39
+ if (v === null)
40
+ return false;
41
+ const trimmed = range.trim();
42
+ if (trimmed === '*' || trimmed === '')
43
+ return true;
44
+ // `>= x`
45
+ const gte = /^>=\s*(.+)$/.exec(trimmed);
46
+ if (gte !== null)
47
+ return compareVersions(version, gte[1]) >= 0;
48
+ // `^ x.y.z` — same major, at least the stated minor/patch (prerelease-aware)
49
+ const caret = /^\^\s*(.+)$/.exec(trimmed);
50
+ if (caret !== null) {
51
+ const base = parseVersion(caret[1]);
52
+ if (base === null)
53
+ return false;
54
+ if (base.major > 0) {
55
+ return v.major === base.major && compareVersions(version, caret[1]) >= 0;
56
+ }
57
+ // ^0.x.z — same minor
58
+ return v.major === base.major && v.minor === base.minor && compareVersions(version, caret[1]) >= 0;
59
+ }
60
+ // bare version
61
+ const exact = parseVersion(trimmed);
62
+ if (exact !== null)
63
+ return compareVersions(version, trimmed) === 0;
64
+ return false;
65
+ }
@@ -0,0 +1,30 @@
1
+ /** A detected update for one installed plugin. */
2
+ export interface UpdateDigest {
3
+ name: string;
4
+ fromVersion: string;
5
+ toVersion: string;
6
+ changelog: string[];
7
+ compat: 'compatible' | 'incompatible' | 'unknown';
8
+ compatRange: string | null;
9
+ }
10
+ /** Latest published version on the npm registry; null when unreachable/unpublished. */
11
+ export declare function npmLatest(packageName: string): Promise<string | null>;
12
+ /** Commit-message changelog: the reliable source for repos without release notes. */
13
+ export declare function fetchCommitChangelog(repoUrl: string | null, sinceIso: string): Promise<string[]>;
14
+ /**
15
+ * Detect one plugin's update: compare local vs remote version, pull commit
16
+ * changelog since `sinceIso`, and check DSH compatibility against the local
17
+ * DSH version via the plugin's peer-dependency range.
18
+ */
19
+ export declare function detectUpdate(name: string, localVersion: string, repoUrl: string | null, compatRange: string | null, localDshVersion: string, sinceIso: string): Promise<UpdateDigest | null>;
20
+ /**
21
+ * Run pnpm in the profile directory. Output inherits the process stdio (no
22
+ * pipe capture — the host sandbox forbids named-pipe stdio); the exit code is
23
+ * the only result this layer needs.
24
+ * @returns the child exit code, or 1 when spawn itself fails.
25
+ */
26
+ export declare function runPnpm(args: readonly string[], cwd: string): Promise<number>;
27
+ /** Install a package into the web profile, mirroring `dsh plugin add` semantics. */
28
+ export declare function installPlugin(packageSpec: string, profileDir: string): Promise<boolean>;
29
+ /** Update a package to latest, mirroring `dsh plugin add <pkg>` (pnpm installs latest). */
30
+ export declare function updatePlugin(packageName: string, profileDir: string): Promise<boolean>;
package/dist/update.js ADDED
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Update detection, changelog extraction, and install/update execution.
3
+ * npm registry is the primary version source; changelog is commit-history
4
+ * first (many community repos ship no release/tag/CHANGELOG — verified §7.2).
5
+ */
6
+ import { spawn } from 'node:child_process';
7
+ import { compareVersions, satisfies } from "./semver.js";
8
+ const UA = { 'User-Agent': 'dsh-plugin-center' };
9
+ /** Latest published version on the npm registry; null when unreachable/unpublished. */
10
+ export async function npmLatest(packageName) {
11
+ for (const registry of ['https://registry.npmjs.org', 'https://registry.npmmirror.com']) {
12
+ try {
13
+ const res = await fetch(`${registry}/${packageName}/latest`, {
14
+ signal: AbortSignal.timeout(8000),
15
+ });
16
+ if (res.ok)
17
+ return (await res.json()).version ?? null;
18
+ }
19
+ catch { /* next registry */ }
20
+ }
21
+ return null;
22
+ }
23
+ /** Extract owner/repo from a package.json repository field. */
24
+ function repoOf(repoUrl) {
25
+ if (repoUrl === null)
26
+ return null;
27
+ const match = /github\.com[/:]([^/]+)\/([^/.#]+)/.exec(repoUrl);
28
+ if (match === null)
29
+ return null;
30
+ return { owner: match[1], repo: match[2] };
31
+ }
32
+ /** Commit-message changelog: the reliable source for repos without release notes. */
33
+ export async function fetchCommitChangelog(repoUrl, sinceIso) {
34
+ const repo = repoOf(repoUrl);
35
+ if (repo === null)
36
+ return [];
37
+ try {
38
+ const res = await fetch(`https://api.github.com/repos/${repo.owner}/${repo.repo}/commits?since=${encodeURIComponent(sinceIso)}&per_page=20`, { headers: UA, signal: AbortSignal.timeout(15000) });
39
+ if (!res.ok)
40
+ return [];
41
+ const commits = await res.json();
42
+ return commits.map(c => c.commit.message.split('\n')[0]).filter(line => line !== '');
43
+ }
44
+ catch {
45
+ return [];
46
+ }
47
+ }
48
+ /**
49
+ * Detect one plugin's update: compare local vs remote version, pull commit
50
+ * changelog since `sinceIso`, and check DSH compatibility against the local
51
+ * DSH version via the plugin's peer-dependency range.
52
+ */
53
+ export async function detectUpdate(name, localVersion, repoUrl, compatRange, localDshVersion, sinceIso) {
54
+ const latest = await npmLatest(name);
55
+ if (latest === null || compareVersions(latest, localVersion) <= 0)
56
+ return null;
57
+ let compat = 'unknown';
58
+ if (compatRange !== null) {
59
+ compat = satisfies(localDshVersion, compatRange) ? 'compatible' : 'incompatible';
60
+ }
61
+ return {
62
+ name,
63
+ fromVersion: localVersion,
64
+ toVersion: latest,
65
+ changelog: await fetchCommitChangelog(repoUrl, sinceIso),
66
+ compat,
67
+ compatRange,
68
+ };
69
+ }
70
+ /**
71
+ * Run pnpm in the profile directory. Output inherits the process stdio (no
72
+ * pipe capture — the host sandbox forbids named-pipe stdio); the exit code is
73
+ * the only result this layer needs.
74
+ * @returns the child exit code, or 1 when spawn itself fails.
75
+ */
76
+ export function runPnpm(args, cwd) {
77
+ return new Promise((resolve) => {
78
+ let child;
79
+ try {
80
+ child = spawn('pnpm', [...args], { cwd, stdio: 'inherit', shell: false });
81
+ }
82
+ catch {
83
+ resolve(1);
84
+ return;
85
+ }
86
+ child.on('error', () => resolve(1));
87
+ child.on('close', code => resolve(code ?? 1));
88
+ });
89
+ }
90
+ /** Install a package into the web profile, mirroring `dsh plugin add` semantics. */
91
+ export async function installPlugin(packageSpec, profileDir) {
92
+ // `-w` is required: every profile ships a pnpm-workspace.yaml.
93
+ const code = await runPnpm(['add', '-w', packageSpec], profileDir);
94
+ return code === 0;
95
+ }
96
+ /** Update a package to latest, mirroring `dsh plugin add <pkg>` (pnpm installs latest). */
97
+ export async function updatePlugin(packageName, profileDir) {
98
+ const code = await runPnpm(['add', '-w', packageName], profileDir);
99
+ return code === 0;
100
+ }
package/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "@max-null/dsh-plugin-center",
3
+ "version": "0.1.0",
4
+ "description": "Plugin center for DeepSeek Harness — installed metadata, community market, update detection, and What's New",
5
+ "license": "MIT",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "type": "module",
10
+ "main": "dist/index.js",
11
+ "types": "dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ },
17
+ "./client": "./client.js",
18
+ "./cordis.patch.yml": "./cordis.patch.yml",
19
+ "./package.json": "./package.json"
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "client.js",
24
+ "cordis.patch.yml",
25
+ "assets"
26
+ ],
27
+ "dsh": {
28
+ "bundle": {
29
+ "patch": "./cordis.patch.yml"
30
+ },
31
+ "client": {
32
+ "inject": [
33
+ "@deepseek-ai/dsh-client-connection",
34
+ "@deepseek-ai/dsh-client-runtime",
35
+ "@deepseek-ai/dsh-client-locale",
36
+ "@deepseek-ai/dsh-client-ui-settings",
37
+ "@deepseek-ai/dsh-client-ui-conversation",
38
+ "@deepseek-ai/dsh-client-ui-slots",
39
+ "@deepseek-ai/dsh-api-remotes"
40
+ ],
41
+ "platform": "web",
42
+ "immediately": true
43
+ }
44
+ },
45
+ "keywords": [
46
+ "deepseek",
47
+ "harness",
48
+ "dsh",
49
+ "dsh-plugin",
50
+ "deepseek-harness",
51
+ "plugin",
52
+ "market",
53
+ "update",
54
+ "cordis"
55
+ ],
56
+ "repository": {
57
+ "type": "git",
58
+ "url": "git+https://github.com/Max-Null/dsh-plugin-center.git"
59
+ },
60
+ "scripts": {
61
+ "build": "tsc -p tsconfig.json && node build-client.mjs",
62
+ "typecheck": "tsc --noEmit -p tsconfig.json"
63
+ },
64
+ "dependencies": {
65
+ "js-yaml": "^4.1.0"
66
+ },
67
+ "peerDependencies": {
68
+ "@deepseek-ai/cordis": "^4.0.1",
69
+ "@deepseek-ai/cordis-plugin-loader": "^1.0.2",
70
+ "@deepseek-ai/dsh-client-connection": ">=0.0.1-rc.5",
71
+ "@deepseek-ai/dsh-host-apiproxy": ">=0.0.1-rc.5"
72
+ },
73
+ "devDependencies": {
74
+ "@deepseek-ai/cordis": "^4.0.1",
75
+ "@deepseek-ai/cordis-plugin-loader": "^1.0.2",
76
+ "@deepseek-ai/dsh-client-connection": ">=0.0.1-rc.5",
77
+ "@deepseek-ai/dsh-host-apiproxy": ">=0.0.1-rc.5",
78
+ "@types/js-yaml": "^4.0.9",
79
+ "@types/node": "^26.2.0",
80
+ "@types/react": "~18.3.1",
81
+ "esbuild": "^0.24.0",
82
+ "react": "^18.2.0",
83
+ "typescript": "^5.5.0"
84
+ }
85
+ }