dsh-plugin-toolkit 0.7.3 → 0.8.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/CHANGELOG.md +16 -0
- package/client.js +161 -0
- package/cordis.patch.yml +6 -0
- package/index.js +111 -0
- package/package.json +11 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
## 0.8.0
|
|
2
|
+
|
|
3
|
+
The toolkit can now be installed as a plugin, because a tool that cannot be
|
|
4
|
+
installed the way its users install things is asking them to take its word.
|
|
5
|
+
|
|
6
|
+
- A "Plugin doctor" tab in the right sidebar: name the folder that holds a
|
|
7
|
+
plugin's `package.json`, and it runs the same checks the market runs
|
|
8
|
+
(`doctor` plus the pack list) and says which one would fail.
|
|
9
|
+
- `index.js` registers `GET /toolkit/check?dir=`, read-only, same-origin only,
|
|
10
|
+
and answers with `{ok, dir, checks:[{check, ok, message}]}`.
|
|
11
|
+
- `cordis.patch.yml` and `dsh.bundle` in `package.json`, so `dsh plugin add
|
|
12
|
+
dsh-plugin-toolkit` installs the panel; the CLI keeps working from the same
|
|
13
|
+
package.
|
|
14
|
+
- The panel is tested the way a user meets it: `mountClient` on the real
|
|
15
|
+
`client.js`, with a stub answering the route the host half registers.
|
|
16
|
+
|
|
1
17
|
## 0.7.3
|
|
2
18
|
|
|
3
19
|
The scaffold was two minor versions behind the tool that wrote it.
|
package/client.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: 'dsh-plugin-toolkit',
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
const module = { exports: {} };
|
|
5
|
+
const React = require('react');
|
|
6
|
+
const { useState, useCallback } = React;
|
|
7
|
+
|
|
8
|
+
const NS = 'dsh-plugin-toolkit';
|
|
9
|
+
const KIND = 'toolkit';
|
|
10
|
+
|
|
11
|
+
const COPY = {
|
|
12
|
+
en: {
|
|
13
|
+
tabTitle: 'Plugin doctor',
|
|
14
|
+
title: 'Does this plugin survive an install?',
|
|
15
|
+
hint: 'The checks the market runs, on the folder you name.',
|
|
16
|
+
placeholder: '/path/to/your-plugin',
|
|
17
|
+
check: 'Check',
|
|
18
|
+
checking: 'Checking…',
|
|
19
|
+
pass: 'Everything the market checks passes.',
|
|
20
|
+
fail: 'This would not install cleanly.',
|
|
21
|
+
noDir: 'Name the folder that holds the plugin\'s package.json.',
|
|
22
|
+
failed: 'Could not run the checks',
|
|
23
|
+
},
|
|
24
|
+
zh: {
|
|
25
|
+
tabTitle: '插件体检',
|
|
26
|
+
title: '这个插件装上去还能用吗?',
|
|
27
|
+
hint: '市场会跑的检查,对着你填的文件夹跑一遍。',
|
|
28
|
+
placeholder: '/path/to/your-plugin',
|
|
29
|
+
check: '检查',
|
|
30
|
+
checking: '检查中…',
|
|
31
|
+
pass: '市场会检查的项都通过了。',
|
|
32
|
+
fail: '这样装上去会出问题。',
|
|
33
|
+
noDir: '请填写放着 package.json 的插件文件夹。',
|
|
34
|
+
failed: '检查没能跑起来',
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const h = React.createElement;
|
|
39
|
+
|
|
40
|
+
const CSS = [
|
|
41
|
+
'.tk-panel{padding:16px;display:flex;flex-direction:column;gap:10px;font-size:13px}',
|
|
42
|
+
'.tk-title{margin:0;font-size:14px;font-weight:600}',
|
|
43
|
+
'.tk-hint{margin:0;font-size:12px;color:var(--dsw-alias-label-tertiary,#888)}',
|
|
44
|
+
'.tk-form{display:flex;gap:8px}',
|
|
45
|
+
'.tk-input{flex:1;min-width:0;height:28px;padding:0 9px;font:inherit;font-size:12px;',
|
|
46
|
+
'border:0.5px solid var(--dsw-alias-border-l2,#ccc);border-radius:6px;',
|
|
47
|
+
'background:var(--dsw-alias-bg-layer-1,#fff);color:inherit}',
|
|
48
|
+
'.tk-input:focus-visible{outline:1px solid var(--dsw-alias-state-business-primary,#4176e6);outline-offset:-1px}',
|
|
49
|
+
'.tk-button{height:28px;padding:0 12px;border-radius:6px;border:0;cursor:pointer;font:inherit;',
|
|
50
|
+
'background:var(--dsw-alias-state-business-primary,#4176e6);color:#fff}',
|
|
51
|
+
'.tk-button:disabled{opacity:.6;cursor:default}',
|
|
52
|
+
'.tk-failed{margin:0;font-size:12px;color:var(--dsw-alias-state-error-primary,#c0392b)}',
|
|
53
|
+
'.tk-verdict{margin:0;font-size:12px;font-weight:600}',
|
|
54
|
+
'.tk-verdict[data-ok="true"]{color:var(--dsw-alias-state-success-primary,#2f855a)}',
|
|
55
|
+
'.tk-verdict[data-ok="false"]{color:var(--dsw-alias-state-warn-primary,#b7791f)}',
|
|
56
|
+
'.tk-rows{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:4px}',
|
|
57
|
+
'.tk-row{display:flex;gap:8px;align-items:baseline;padding:5px 8px;border-radius:6px;',
|
|
58
|
+
'background:var(--dsw-alias-bg-layer-1,#fafafa)}',
|
|
59
|
+
'.tk-mark{width:12px;flex:none;font-weight:600}',
|
|
60
|
+
'.tk-row[data-ok="true"] .tk-mark{color:var(--dsw-alias-state-success-primary,#2f855a)}',
|
|
61
|
+
'.tk-row[data-ok="false"] .tk-mark{color:var(--dsw-alias-state-error-primary,#c0392b)}',
|
|
62
|
+
'.tk-body{display:flex;flex-direction:column;gap:1px;min-width:0}',
|
|
63
|
+
'.tk-check{font-size:11px;text-transform:uppercase;letter-spacing:.04em;',
|
|
64
|
+
'color:var(--dsw-alias-label-tertiary,#888)}',
|
|
65
|
+
'.tk-message{font-size:12px;overflow-wrap:anywhere}',
|
|
66
|
+
].join('');
|
|
67
|
+
|
|
68
|
+
function injectStyles(document) {
|
|
69
|
+
if (document.getElementById('dsh-plugin-toolkit:styles') !== null) return;
|
|
70
|
+
const style = document.createElement('style');
|
|
71
|
+
style.id = 'dsh-plugin-toolkit:styles';
|
|
72
|
+
style.textContent = CSS;
|
|
73
|
+
document.head.appendChild(style);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function Row({ entry }) {
|
|
77
|
+
return h(
|
|
78
|
+
'li',
|
|
79
|
+
{ className: 'tk-row', 'data-ok': entry.ok === true ? 'true' : 'false' },
|
|
80
|
+
h('span', { className: 'tk-mark', 'aria-hidden': 'true' }, entry.ok === true ? '✓' : '✗'),
|
|
81
|
+
h('span', { className: 'tk-body' },
|
|
82
|
+
h('span', { className: 'tk-check' }, entry.check),
|
|
83
|
+
h('span', { className: 'tk-message' }, entry.message)),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function Panel() {
|
|
88
|
+
const [dir, setDir] = useState('');
|
|
89
|
+
const [result, setResult] = useState(null);
|
|
90
|
+
const [busy, setBusy] = useState(false);
|
|
91
|
+
const [failed, setFailed] = useState(null);
|
|
92
|
+
const tr = (key) => {
|
|
93
|
+
const language = COPY[document.documentElement.lang === 'zh' ? 'zh' : 'en'];
|
|
94
|
+
return language[key] ?? key;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const check = useCallback(() => {
|
|
98
|
+
if (dir.trim().length === 0) {
|
|
99
|
+
setFailed(tr('noDir'));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
setBusy(true);
|
|
103
|
+
setFailed(null);
|
|
104
|
+
fetch(`/toolkit/check?dir=${encodeURIComponent(dir.trim())}`)
|
|
105
|
+
.then((response) => (response.ok === true ? response.json() : Promise.reject(new Error(String(response.status)))))
|
|
106
|
+
.then((payload) => setResult(payload))
|
|
107
|
+
.catch((error) => setFailed(`${tr('failed')} · ${error.message}`))
|
|
108
|
+
.finally(() => setBusy(false));
|
|
109
|
+
}, [dir]);
|
|
110
|
+
|
|
111
|
+
return h('div', { className: 'tk-panel' },
|
|
112
|
+
h('h2', { className: 'tk-title' }, tr('title')),
|
|
113
|
+
h('p', { className: 'tk-hint' }, tr('hint')),
|
|
114
|
+
h('div', { className: 'tk-form' },
|
|
115
|
+
h('input', {
|
|
116
|
+
className: 'tk-input',
|
|
117
|
+
type: 'text',
|
|
118
|
+
value: dir,
|
|
119
|
+
placeholder: tr('placeholder'),
|
|
120
|
+
'aria-label': tr('placeholder'),
|
|
121
|
+
onChange: (event) => setDir(event.target.value),
|
|
122
|
+
onKeyDown: (event) => { if (event.key === 'Enter') check(); },
|
|
123
|
+
}),
|
|
124
|
+
h('button', {
|
|
125
|
+
className: 'tk-button',
|
|
126
|
+
type: 'button',
|
|
127
|
+
disabled: busy === true,
|
|
128
|
+
onClick: check,
|
|
129
|
+
}, busy === true ? tr('checking') : tr('check'))),
|
|
130
|
+
failed !== null ? h('p', { className: 'tk-failed', role: 'status' }, failed) : null,
|
|
131
|
+
result !== null
|
|
132
|
+
? h('div', { className: 'tk-result' },
|
|
133
|
+
h('p', { className: 'tk-verdict', 'data-ok': result.ok === true ? 'true' : 'false', role: 'status' },
|
|
134
|
+
result.ok === true ? tr('pass') : tr('fail')),
|
|
135
|
+
h('ul', { className: 'tk-rows' }, result.checks.map((entry) => h(Row, { key: entry.check, entry }))))
|
|
136
|
+
: null);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function apply(ctx) {
|
|
140
|
+
injectStyles(document);
|
|
141
|
+
ctx.locale.register(NS, COPY);
|
|
142
|
+
ctx.inject(['sidebarRightTabs', 'sidebarRight'], (tabCtx) => {
|
|
143
|
+
if (tabCtx.slots === undefined || tabCtx.sidebarRightTabs === undefined) return;
|
|
144
|
+
tabCtx.effect(() => tabCtx.slots.inject('sidebar.right.pane.tab', () => tabCtx.slots.register({
|
|
145
|
+
name: 'sidebar.right.pane.tab',
|
|
146
|
+
key: NS,
|
|
147
|
+
locale: NS,
|
|
148
|
+
}, Panel)));
|
|
149
|
+
tabCtx.effect(() => tabCtx.sidebarRightTabs.register({
|
|
150
|
+
id: NS,
|
|
151
|
+
kind: KIND,
|
|
152
|
+
priority: 'extension',
|
|
153
|
+
title: () => 'Plugin doctor',
|
|
154
|
+
}));
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = { apply, name: 'dsh-plugin-toolkit' };
|
|
159
|
+
return module.exports;
|
|
160
|
+
},
|
|
161
|
+
});
|
package/cordis.patch.yml
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The host half: a panel is a promise that something checked your plugin, so
|
|
3
|
+
* something has to do the checking on the side that can read files.
|
|
4
|
+
*
|
|
5
|
+
* Read-only by design. The panel passes a directory, this runs the same
|
|
6
|
+
* `doctor` and pack-list checks the CLI runs, and answers with what failed and
|
|
7
|
+
* the command that fixes it. Nothing here writes to the workspace.
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
import { doctor } from './lib/doctor.mjs';
|
|
13
|
+
import { verify } from './lib/verify.mjs';
|
|
14
|
+
|
|
15
|
+
export const name = 'dsh-plugin-toolkit';
|
|
16
|
+
|
|
17
|
+
const LIMIT = 400;
|
|
18
|
+
|
|
19
|
+
function sendJson(res, status, body) {
|
|
20
|
+
const text = JSON.stringify(body);
|
|
21
|
+
res.statusCode = status;
|
|
22
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
23
|
+
res.setHeader('Content-Length', Buffer.byteLength(text));
|
|
24
|
+
res.end(text);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readDir(value) {
|
|
28
|
+
if (typeof value !== 'string' || value.trim().length === 0) return null;
|
|
29
|
+
const resolved = path.resolve(value.trim());
|
|
30
|
+
if (resolved.length > LIMIT) return null;
|
|
31
|
+
try {
|
|
32
|
+
if (fs.statSync(resolved).isDirectory() === false) return null;
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return resolved;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Same-origin only: this route has no authentication of its own. */
|
|
40
|
+
function sameOrigin(req) {
|
|
41
|
+
const origin = req.headers !== undefined ? req.headers.origin : undefined;
|
|
42
|
+
if (typeof origin !== 'string' || origin.length === 0) return true;
|
|
43
|
+
const host = req.headers.host;
|
|
44
|
+
try {
|
|
45
|
+
return new URL(origin).host === host;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function checkPlugin(dir) {
|
|
52
|
+
const target = readDir(dir);
|
|
53
|
+
if (target === null) {
|
|
54
|
+
return { ok: false, dir: null, checks: [{ check: 'dir', ok: false, message: 'that is not a directory' }] };
|
|
55
|
+
}
|
|
56
|
+
const checks = [];
|
|
57
|
+
const report = doctor(target);
|
|
58
|
+
if (report.failures.length === 0) {
|
|
59
|
+
checks.push({ check: 'manifest', ok: true, message: `${report.manifest.name}@${report.manifest.version}` });
|
|
60
|
+
}
|
|
61
|
+
for (const failure of report.failures) {
|
|
62
|
+
checks.push({ check: failure.check, ok: false, message: failure.message });
|
|
63
|
+
}
|
|
64
|
+
// The pack list is the check `doctor` cannot make: it reads the working copy,
|
|
65
|
+
// so a `files` entry that forgets client.js passes it and ships a plugin with
|
|
66
|
+
// no panel.
|
|
67
|
+
const packed = await verify({ dir: target, mount: false });
|
|
68
|
+
const known = new Set(checks.map((entry) => entry.check));
|
|
69
|
+
if (packed.ok === true) {
|
|
70
|
+
checks.push({ check: 'tarball', ok: true, message: `${packed.files.length} file(s)` });
|
|
71
|
+
} else {
|
|
72
|
+
for (const failure of packed.failures ?? []) {
|
|
73
|
+
// `verify` re-runs doctor first; do not say the same thing twice.
|
|
74
|
+
if (known.has(failure.check) === true) continue;
|
|
75
|
+
checks.push({ check: failure.check ?? packed.stage, ok: false, message: failure.message });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { ok: checks.every((entry) => entry.ok === true), dir: target, checks };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function apply(ctx) {
|
|
82
|
+
const checkHandler = async (req, res) => {
|
|
83
|
+
if (req.method !== 'GET') return sendJson(res, 405, { error: 'method-not-allowed' });
|
|
84
|
+
if (sameOrigin(req) === false) return sendJson(res, 403, { error: 'cross-origin' });
|
|
85
|
+
const url = new URL(req.url, 'http://localhost');
|
|
86
|
+
try {
|
|
87
|
+
return sendJson(res, 200, await checkPlugin(url.searchParams.get('dir')));
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return sendJson(res, 500, { error: 'check-failed', message: String(error && error.message) });
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
let installed = false;
|
|
94
|
+
const installRoutes = (serviceCtx) => {
|
|
95
|
+
if (installed) return;
|
|
96
|
+
const server = serviceCtx !== null && serviceCtx !== undefined ? serviceCtx.webServer : undefined;
|
|
97
|
+
if (server === null || server === undefined || typeof server.register !== 'function') return;
|
|
98
|
+
server.register({ kind: 'exact', path: '/toolkit/check', handler: checkHandler });
|
|
99
|
+
installed = true;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const defer = (deps, callback) => {
|
|
103
|
+
try {
|
|
104
|
+
if (typeof ctx.inject === 'function') ctx.inject(deps, callback);
|
|
105
|
+
} catch {
|
|
106
|
+
// The service may already be mounted on this context.
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
defer(['webServer'], (serviceCtx) => installRoutes(serviceCtx));
|
|
110
|
+
installRoutes(ctx);
|
|
111
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Preflight, release and test DeepSeek Harness plugins: checks what the host needs, waits for npm to actually serve the version, and keeps the market index honest.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,12 +13,16 @@
|
|
|
13
13
|
"files": [
|
|
14
14
|
"bin/",
|
|
15
15
|
"lib/",
|
|
16
|
+
"index.js",
|
|
17
|
+
"client.js",
|
|
18
|
+
"cordis.patch.yml",
|
|
16
19
|
"README.md",
|
|
17
20
|
"LICENSE",
|
|
18
21
|
"CHANGELOG.md"
|
|
19
22
|
],
|
|
20
23
|
"engines": {
|
|
21
|
-
"node": ">=18"
|
|
24
|
+
"node": ">=18",
|
|
25
|
+
"dsh": ">=0.2.0"
|
|
22
26
|
},
|
|
23
27
|
"keywords": [
|
|
24
28
|
"dsh-plugin",
|
|
@@ -65,5 +69,10 @@
|
|
|
65
69
|
"jsdom": "^26.1.0",
|
|
66
70
|
"react": "^19.3.1",
|
|
67
71
|
"react-dom": "^19.3.1"
|
|
72
|
+
},
|
|
73
|
+
"dsh": {
|
|
74
|
+
"bundle": {
|
|
75
|
+
"patch": "./cordis.patch.yml"
|
|
76
|
+
}
|
|
68
77
|
}
|
|
69
78
|
}
|