@wjm200509/dsh-plugin-version-watch 0.0.0-stage → 0.1.3
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/LICENSE +21 -0
- package/README.md +48 -3
- package/icon.svg +6 -0
- package/lib/client.js +613 -0
- package/lib/index.js +657 -0
- package/lib/lockfile.js +100 -0
- package/lib/spec.js +175 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +52 -4
- package/patch.yml +6 -0
package/lib/lockfile.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The small part of a pnpm lockfile the version watch needs: the resolved
|
|
3
|
+
* dependency strings of the profile's root importer. Each Git-installed
|
|
4
|
+
* dependency's string carries the exact commit pnpm unpacked, which is the
|
|
5
|
+
* only local record of what actually runs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse the root importer's dependency map from pnpm-lock.yaml text.
|
|
10
|
+
*
|
|
11
|
+
* Only `importers:` and the `.` importer are read, so registry hashes or
|
|
12
|
+
* package metadata elsewhere in the file cannot be mistaken for a version.
|
|
13
|
+
* @param {string} text - lockfile contents.
|
|
14
|
+
* @returns {Map<string, { specifier: string | null, version: string | null }>}
|
|
15
|
+
*/
|
|
16
|
+
export function parseImporterDependencies(text) {
|
|
17
|
+
const lines = String(text ?? "").split(/\r?\n/);
|
|
18
|
+
const start = lines.findIndex((line) => /^importers:\s*$/.test(line));
|
|
19
|
+
if (start < 0) return new Map();
|
|
20
|
+
let end = lines.length;
|
|
21
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
22
|
+
const line = lines[index];
|
|
23
|
+
if (line.trim() === "" || line.trim().startsWith("#")) continue;
|
|
24
|
+
if (!/^\s/.test(line)) {
|
|
25
|
+
end = index;
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const dependencies = new Map();
|
|
31
|
+
let root = false;
|
|
32
|
+
let section = false;
|
|
33
|
+
let pendingKey = null;
|
|
34
|
+
|
|
35
|
+
const unquote = (value) => value.trim().replace(/^['"]|['"]$/g, "");
|
|
36
|
+
const header = (line) => {
|
|
37
|
+
const match = line.match(/^(\s*)(.+?):\s*$/);
|
|
38
|
+
return match === null ? null : { indent: match[1].length, key: unquote(match[2]) };
|
|
39
|
+
};
|
|
40
|
+
const value = (line) => {
|
|
41
|
+
const match = line.match(/^(\s*)(.+?):\s+(.*)$/);
|
|
42
|
+
return match === null ? null : { indent: match[1].length, key: unquote(match[2]), value: unquote(match[3]) };
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
for (let index = start + 1; index < end; index += 1) {
|
|
46
|
+
const line = lines[index];
|
|
47
|
+
if (line.trim() === "" || line.trim().startsWith("#")) continue;
|
|
48
|
+
const pair = value(line);
|
|
49
|
+
const head = pair === null ? header(line) : null;
|
|
50
|
+
const indent = pair === null ? head?.indent : pair.indent;
|
|
51
|
+
if (indent === undefined) continue;
|
|
52
|
+
|
|
53
|
+
if (indent === 2) {
|
|
54
|
+
root = false;
|
|
55
|
+
section = false;
|
|
56
|
+
pendingKey = null;
|
|
57
|
+
root = (pair?.key ?? head?.key) === ".";
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (!root) continue;
|
|
61
|
+
|
|
62
|
+
if (!section && indent === 4 && /^(?:dev|optional)?dependencies$/.test(pair?.key ?? head?.key ?? "")) {
|
|
63
|
+
section = true;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (!section) continue;
|
|
67
|
+
|
|
68
|
+
if (indent === 6) {
|
|
69
|
+
// A dependency row may carry an inline value (`name: version`) or a nested map.
|
|
70
|
+
if (pair !== null) {
|
|
71
|
+
dependencies.set(pair.key, { specifier: pair.value, version: null });
|
|
72
|
+
} else {
|
|
73
|
+
pendingKey = head.key;
|
|
74
|
+
dependencies.set(pendingKey, { specifier: null, version: null });
|
|
75
|
+
}
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (indent === 8) {
|
|
80
|
+
const key = pair?.key ?? head?.key;
|
|
81
|
+
const entry = pendingKey === null ? null : dependencies.get(pendingKey);
|
|
82
|
+
if (entry === null || entry === undefined) continue;
|
|
83
|
+
if (key === "specifier" && pair !== null) entry.specifier = pair.value;
|
|
84
|
+
if (key === "version" && pair !== null) entry.version = pair.value;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return dependencies;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Extract the exact Git commit a resolved dependency string names, if any.
|
|
92
|
+
* @param {string | null | undefined} resolution - the importer's `version:` value.
|
|
93
|
+
* @returns {string | null}
|
|
94
|
+
*/
|
|
95
|
+
export function commitFromResolution(resolution) {
|
|
96
|
+
if (typeof resolution !== "string") return null;
|
|
97
|
+
const match = resolution.match(/(?:^|[/#&])([0-9a-f]{40})(?:[&#]|$)/i);
|
|
98
|
+
return match === null ? null : match[1].toLowerCase();
|
|
99
|
+
}
|
|
100
|
+
|
package/lib/spec.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency-spec interpretation for the plugin version watch.
|
|
3
|
+
*
|
|
4
|
+
* The profile's `package.json` records what `pnpm add` saved: a registry
|
|
5
|
+
* name/range, a GitHub shortcut (`github:owner/repo#path:subdir`), a Git URL,
|
|
6
|
+
* a tarball URL, or a local path. The version watch needs three things from
|
|
7
|
+
* that spec: which check applies, which repository (if any) to ask for the
|
|
8
|
+
* latest commit, and the package subdirectory whose `package.json` carries
|
|
9
|
+
* the published version.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** A 40-character Git object name. */
|
|
13
|
+
const COMMIT = /^[0-9a-f]{40}$/i;
|
|
14
|
+
|
|
15
|
+
/** Hosts whose `https://host/owner/repo` form names a Git repository. */
|
|
16
|
+
const GIT_HOSTS = /(?:^|\.)(?:github|gitlab|bitbucket|gitee|codeberg)\.(?:com|org)$/i;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Split the first `#` fragment off a spec.
|
|
20
|
+
* @param {string} spec - raw saved spec.
|
|
21
|
+
* @returns {{ base: string, fragment: string }}
|
|
22
|
+
*/
|
|
23
|
+
function takeFragment(spec) {
|
|
24
|
+
const hash = spec.indexOf("#");
|
|
25
|
+
return hash < 0 ? { base: spec, fragment: "" } : { base: spec.slice(0, hash), fragment: spec.slice(hash + 1) };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Read the fragment parts pnpm understands.
|
|
30
|
+
* @param {string} fragment - text after the first `#`, possibly `ref&path:x` or `ref#path:x`.
|
|
31
|
+
* @returns {{ ref: string | null, subdir: string | null, semver: string | null, pinned: string | null }}
|
|
32
|
+
*/
|
|
33
|
+
function parseFragment(fragment) {
|
|
34
|
+
let ref = null;
|
|
35
|
+
let subdir = null;
|
|
36
|
+
let semver = null;
|
|
37
|
+
let pinned = null;
|
|
38
|
+
for (const part of String(fragment).split(/[&#]/).filter(Boolean)) {
|
|
39
|
+
const lower = part.toLowerCase();
|
|
40
|
+
if (lower.startsWith("path:")) {
|
|
41
|
+
subdir = part.slice(5).replace(/^\/+/, "").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
42
|
+
if (subdir === "") subdir = null;
|
|
43
|
+
} else if (lower.startsWith("semver:")) {
|
|
44
|
+
semver = part.slice(7);
|
|
45
|
+
} else if (COMMIT.test(part)) {
|
|
46
|
+
pinned = part.toLowerCase();
|
|
47
|
+
} else if (ref === null) {
|
|
48
|
+
ref = part;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { ref, subdir, semver, pinned };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Classify one recorded dependency spec.
|
|
56
|
+
* @param {string} raw - spec saved in the profile manifest.
|
|
57
|
+
* @returns {{
|
|
58
|
+
* kind: "git" | "registry" | "local" | "tarball" | "unknown",
|
|
59
|
+
* repoUrl?: string | null,
|
|
60
|
+
* ref?: string | null,
|
|
61
|
+
* subdir?: string | null,
|
|
62
|
+
* semver?: string | null,
|
|
63
|
+
* pinned?: string | null,
|
|
64
|
+
* }}
|
|
65
|
+
*/
|
|
66
|
+
export function parseDependencySpec(raw) {
|
|
67
|
+
const spec = String(raw ?? "").trim();
|
|
68
|
+
if (spec === "") return { kind: "unknown" };
|
|
69
|
+
if (/^(?:file|link):/i.test(spec)) return { kind: "local" };
|
|
70
|
+
if (/^(?:~|\.{1,2}\/|\/)/.test(spec) || /^[a-zA-Z]:[\\/]/.test(spec)) return { kind: "local" };
|
|
71
|
+
|
|
72
|
+
if (/^github:/i.test(spec)) {
|
|
73
|
+
const { base, fragment } = takeFragment(spec.slice(7));
|
|
74
|
+
return gitShortcut(base, fragment);
|
|
75
|
+
}
|
|
76
|
+
if (!spec.startsWith("@") && /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:#|$)/.test(spec)) {
|
|
77
|
+
const { base, fragment } = takeFragment(spec);
|
|
78
|
+
return gitShortcut(base, fragment);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (/^(?:git\+)?(?:ssh|https?|git):/i.test(spec) || /^git@/.test(spec)) {
|
|
82
|
+
const { base, fragment } = takeFragment(spec);
|
|
83
|
+
const url = base.replace(/^git\+/i, "");
|
|
84
|
+
const known = /^git@/.test(url) || /^(?:ssh|git):/i.test(url) || /\.git$/i.test(url) || gitHostUrl(url);
|
|
85
|
+
if (known) {
|
|
86
|
+
return { kind: "git", repoUrl: url, ...parseFragment(fragment) };
|
|
87
|
+
}
|
|
88
|
+
return { kind: "tarball" };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { kind: "registry" };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* `owner/repo` GitHub shorthand.
|
|
96
|
+
* @param {string} base - repository part without the `github:` prefix or fragment.
|
|
97
|
+
* @param {string} fragment - fragment text.
|
|
98
|
+
* @returns {{ kind: "git", repoUrl: string | null, ref: string | null, subdir: string | null, semver: string | null, pinned: string | null }}
|
|
99
|
+
*/
|
|
100
|
+
function gitShortcut(base, fragment) {
|
|
101
|
+
const match = base.match(/^([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
102
|
+
return {
|
|
103
|
+
kind: "git",
|
|
104
|
+
repoUrl: match === null ? null : `https://github.com/${match[1]}/${match[2]}.git`,
|
|
105
|
+
...parseFragment(fragment),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Whether an https URL points at a repository rather than a tarball.
|
|
111
|
+
* @param {string} url - candidate URL.
|
|
112
|
+
* @returns {boolean}
|
|
113
|
+
*/
|
|
114
|
+
function gitHostUrl(url) {
|
|
115
|
+
try {
|
|
116
|
+
const parsed = new URL(url);
|
|
117
|
+
if (!/^https?:/.test(parsed.protocol)) return false;
|
|
118
|
+
if (!GIT_HOSTS.test(parsed.hostname)) return false;
|
|
119
|
+
const segments = parsed.pathname.replace(/^\/+/, "").split("/").filter(Boolean);
|
|
120
|
+
return segments.length === 2;
|
|
121
|
+
} catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Hide credentials a saved spec may carry in an http(s) URL.
|
|
128
|
+
* @param {string} spec - recorded spec.
|
|
129
|
+
* @returns {string}
|
|
130
|
+
*/
|
|
131
|
+
export function redactSource(spec) {
|
|
132
|
+
return String(spec ?? "").replace(/^(\w+:\/\/)([^/@]+)@/, "$1");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Shorten a 40-character commit for display.
|
|
137
|
+
* @param {string | null | undefined} commit - object name.
|
|
138
|
+
* @returns {string | null}
|
|
139
|
+
*/
|
|
140
|
+
export function shortCommit(commit) {
|
|
141
|
+
return typeof commit === "string" && COMMIT.test(commit) ? commit.slice(0, 7) : (commit ?? null);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The raw URL of a package's `package.json` at one commit, on GitHub.
|
|
146
|
+
* @param {string | null} repoUrl - repository URL.
|
|
147
|
+
* @param {string} commit - 40-character commit.
|
|
148
|
+
* @param {string | null} subdir - package directory inside the repository.
|
|
149
|
+
* @returns {string | null}
|
|
150
|
+
*/
|
|
151
|
+
export function githubRawManifestUrl(repoUrl, commit, subdir) {
|
|
152
|
+
if (typeof repoUrl !== "string" || typeof commit !== "string" || !COMMIT.test(commit)) return null;
|
|
153
|
+
let host;
|
|
154
|
+
let pathname;
|
|
155
|
+
const ssh = repoUrl.match(/^git@([^:]+):(.+)$/);
|
|
156
|
+
if (ssh !== null) {
|
|
157
|
+
host = ssh[1].toLowerCase();
|
|
158
|
+
pathname = ssh[2];
|
|
159
|
+
} else {
|
|
160
|
+
try {
|
|
161
|
+
const parsed = new URL(repoUrl);
|
|
162
|
+
host = parsed.hostname.toLowerCase();
|
|
163
|
+
pathname = parsed.pathname;
|
|
164
|
+
} catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (host !== "github.com") return null;
|
|
169
|
+
const repoPath = pathname.replace(/^\/+/, "").replace(/\.git$/i, "");
|
|
170
|
+
const [owner, name] = repoPath.split("/");
|
|
171
|
+
if (owner === undefined || name === undefined || owner === "" || name === "") return null;
|
|
172
|
+
const encode = (part) => part.split("/").filter(Boolean).map(encodeURIComponent).join("/");
|
|
173
|
+
const directory = subdir ? `${encode(subdir)}/` : "";
|
|
174
|
+
return `https://raw.githubusercontent.com/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/${encodeURIComponent(commit)}/${directory}package.json`;
|
|
175
|
+
}
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wjm200509/dsh-plugin-version-watch",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Version watch for installed DSH plugins: list installed versions, check Git and npm sources for updates, and update them live from Settings without restarting.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": "JiaMingWang-CN",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/JiaMingWang-CN/dsh-bundles#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/JiaMingWang-CN/dsh-bundles",
|
|
12
|
+
"directory": "dsh-plugin-version-watch"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"dsh",
|
|
16
|
+
"dsh-plugin",
|
|
17
|
+
"deepseek-harness",
|
|
18
|
+
"plugin-updates",
|
|
19
|
+
"version-watch"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "node --test"
|
|
23
|
+
},
|
|
24
|
+
"exports": {
|
|
25
|
+
".": "./lib/index.js",
|
|
26
|
+
"./client": "./lib/client.js",
|
|
27
|
+
"./locale/*.json": "./locale/*.json",
|
|
28
|
+
"./icon": "./icon.svg"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"lib/index.js",
|
|
32
|
+
"lib/client.js",
|
|
33
|
+
"lib/lockfile.js",
|
|
34
|
+
"lib/spec.js",
|
|
35
|
+
"patch.yml",
|
|
36
|
+
"locale/*.json",
|
|
37
|
+
"icon.svg",
|
|
38
|
+
"README.md"
|
|
39
|
+
],
|
|
40
|
+
"engines": {
|
|
41
|
+
"dsh": "^0.1.7-rc.2 || ^0.2.0-rc.2 || ^0.2.1-alpha.2 || ^0.2.2-rc.1"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"dsh": {
|
|
47
|
+
"client": {
|
|
48
|
+
"platform": "web"
|
|
49
|
+
},
|
|
50
|
+
"bundle": {
|
|
51
|
+
"patch": "./patch.yml"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
package/patch.yml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Plugin version watch: a Settings page that monitors installed plugin versions
|
|
2
|
+
# and updates them in place (Git sources are removed and reinstalled at the
|
|
3
|
+
# latest commit, registry sources use the plugin manager's update path).
|
|
4
|
+
- insert:
|
|
5
|
+
- id: version-watch
|
|
6
|
+
name: "@wjm200509/dsh-plugin-version-watch"
|