@aaayeaaa-p1ws/dsh-plugin-auto-update 0.0.0-stage → 1.1.1

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.
@@ -0,0 +1,48 @@
1
+ // 重启桌面端 DSH。
2
+ //
3
+ // Host 是 Electron 主进程的子进程,拿不到 `app.relaunch()`;唯一可靠的办法是让一个
4
+ // **独立**的 powershell 等我们退出后把主进程杀掉再重新拉起 exe——它必须脱离我们的进程树,
5
+ // 否则 Stop-Process 会把这条命令自己也带走。
6
+ import { spawn } from 'node:child_process';
7
+
8
+ /** 当前进程是不是桌面端 Host:exe 名 + 父进程号都像桌面端才算。 */
9
+ export function desktopLaunchInfo() {
10
+ const exe = typeof process.execPath === 'string' ? process.execPath : '';
11
+ const base = exe.replace(/\\/g, '/').split('/').pop().toLowerCase();
12
+ const isDesktop = base === 'deepseek harness.exe' || base === 'deepseek-harness.exe';
13
+ const mainPid = Number(process.ppid) || 0;
14
+ return { exe, isDesktop, mainPid };
15
+ }
16
+
17
+ /**
18
+ * 排一次「退出 → 重启」。
19
+ * @param {{ delayMs?: number }} [options] 先等多久再动手(给 HTTP 响应留时间)
20
+ * @returns {{ ok: boolean, reason?: string, pid?: number, mainPid?: number, exe?: string }}
21
+ */
22
+ export function scheduleDesktopRestart(options = {}) {
23
+ const info = desktopLaunchInfo();
24
+ if (!info.isDesktop) return { ok: false, reason: 'not-desktop', exe: info.exe };
25
+ if (!Number.isInteger(info.mainPid) || info.mainPid <= 0) return { ok: false, reason: 'no-parent', exe: info.exe };
26
+ const delayMs = Math.max(200, Math.min(20000, Number(options.delayMs) || 900));
27
+ const exeQuoted = `'${info.exe.replace(/'/g, "''")}'`;
28
+ const script = [
29
+ "$ErrorActionPreference = 'SilentlyContinue'",
30
+ `Start-Sleep -Milliseconds ${delayMs}`,
31
+ `$target = ${info.mainPid}`,
32
+ 'try { Stop-Process -Id $target -Force -ErrorAction Stop } catch { }',
33
+ 'for ($i = 0; $i -lt 80; $i++) { if (-not (Get-Process -Id $target -ErrorAction SilentlyContinue)) { break }; Start-Sleep -Milliseconds 250 }',
34
+ 'Start-Sleep -Milliseconds 1200',
35
+ `Start-Process -FilePath ${exeQuoted}`,
36
+ ].join('; ');
37
+ try {
38
+ const child = spawn('powershell.exe', ['-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-Command', script], {
39
+ detached: true,
40
+ stdio: 'ignore',
41
+ windowsHide: true,
42
+ });
43
+ child.unref();
44
+ return { ok: true, pid: child.pid, mainPid: info.mainPid, exe: info.exe };
45
+ } catch (error) {
46
+ return { ok: false, reason: error instanceof Error ? error.message : String(error), exe: info.exe };
47
+ }
48
+ }
package/lib/store.mjs ADDED
@@ -0,0 +1,112 @@
1
+ // 用户的更新设置(GUI 里改的那份),存在 ~/.dsh/auto-update/config.json。
2
+ // 与 cordis.patch.yml 里的 config 合并:patch 优先做默认,GUI 写下的值覆盖它。
3
+ import { readFile, writeFile, mkdir, rename } from 'node:fs/promises';
4
+ import { dirname } from 'node:path';
5
+
6
+ export const CONFIG_DEFAULTS = {
7
+ /** startup = 每次打开 DSH 查;daily = 每天固定时间查;interval = 每隔 N 小时查 */
8
+ mode: 'startup',
9
+ /** mode=daily 时的时刻,本地时间 HH:MM */
10
+ dailyAt: '09:30',
11
+ /** mode=interval 时间隔小时数 */
12
+ intervalHours: 6,
13
+ /** 每天第一次打开时弹一次更新结果 */
14
+ popupEnabled: true,
15
+ /** 总开关 */
16
+ enabled: true,
17
+ /** 是否直接安装(关掉就只检查) */
18
+ autoApply: true,
19
+ };
20
+
21
+ const MODES = new Set(['startup', 'daily', 'interval']);
22
+
23
+ export function normalizeConfig(input, base) {
24
+ const src = input && typeof input === 'object' ? input : {};
25
+ const fallback = base && typeof base === 'object' ? base : CONFIG_DEFAULTS;
26
+ const mode = MODES.has(src.mode) ? src.mode : fallback.mode;
27
+ const dailyAtRaw = typeof src.dailyAt === 'string' ? src.dailyAt.trim() : fallback.dailyAt;
28
+ const dailyAt = /^([01]\d|2[0-3]):([0-5]\d)$/.test(dailyAtRaw) ? dailyAtRaw : '09:30';
29
+ const intervalHours = clamp(Number(src.intervalHours), fallback.intervalHours, 1, 48);
30
+ const popupEnabled = typeof src.popupEnabled === 'boolean' ? src.popupEnabled : fallback.popupEnabled !== false;
31
+ const enabled = typeof src.enabled === 'boolean' ? src.enabled : fallback.enabled !== false;
32
+ const autoApply = typeof src.autoApply === 'boolean' ? src.autoApply : fallback.autoApply !== false;
33
+ return { mode, dailyAt, intervalHours, popupEnabled, enabled, autoApply };
34
+ }
35
+
36
+ function clamp(value, fallback, min, max) {
37
+ const n = Number.isFinite(value) ? value : Number(fallback);
38
+ if (!Number.isFinite(n)) return min;
39
+ return Math.max(min, Math.min(max, n));
40
+ }
41
+
42
+ export async function readConfigFile(path) {
43
+ try {
44
+ const text = await readFile(path, 'utf8');
45
+ const parsed = JSON.parse(text);
46
+ return normalizeConfig(parsed, CONFIG_DEFAULTS);
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ export async function writeConfigFile(path, value) {
53
+ const next = normalizeConfig(value, CONFIG_DEFAULTS);
54
+ try {
55
+ await mkdir(dirname(path), { recursive: true });
56
+ const tmp = `${path}.tmp`;
57
+ await writeFile(tmp, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
58
+ await rename(tmp, path);
59
+ return next;
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ /** 本地日期的 HH:MM 分钟数。 */
66
+ export function minutesOfDay(date = new Date()) {
67
+ return (date.getHours() || 0) * 60 + (date.getMinutes() || 0);
68
+ }
69
+
70
+ /** 'HH:MM' → 分钟数 */
71
+ export function parseDailyAt(text) {
72
+ const m = /^([01]\d|2[0-3]):([0-5]\d)$/.exec(String(text || ''));
73
+ return m ? Number(m[1]) * 60 + Number(m[2]) : 9 * 60 + 30;
74
+ }
75
+
76
+ /**
77
+ * tick 这一轮该不该动手(boot 那一轮另有逻辑)。
78
+ * @returns {{ due: boolean, why: string }}
79
+ */
80
+ export function tickDue(config, state, now = new Date()) {
81
+ if (config.mode === 'daily') {
82
+ const today = localDay(now);
83
+ if (state.lastDay === today) return { due: false, why: `今天(${today})已经检查过` };
84
+ const target = parseDailyAt(config.dailyAt);
85
+ if (minutesOfDay(now) < target) return { due: false, why: `今天 ${config.dailyAt} 之后才检查` };
86
+ return { due: true, why: `每天 ${config.dailyAt} 的定时检查` };
87
+ }
88
+ if (config.mode === 'interval') {
89
+ const last = Date.parse(state.lastCheckAt || '');
90
+ if (!Number.isFinite(last)) return { due: true, why: '还没检查过' };
91
+ const gapMs = config.intervalHours * 3600 * 1000;
92
+ if (now.getTime() - last < gapMs) {
93
+ const at = new Date(last + gapMs);
94
+ return { due: false, why: `每 ${config.intervalHours} 小时一次,下次 ${localTime(at)}` };
95
+ }
96
+ return { due: true, why: `每 ${config.intervalHours} 小时的定时检查` };
97
+ }
98
+ return { due: false, why: '启动时检查模式(tick 只补欠账)' };
99
+ }
100
+
101
+ export function localDay(date = new Date()) {
102
+ const d = date instanceof Date ? date : new Date(date);
103
+ const pad = (n) => String(n).padStart(2, '0');
104
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
105
+ }
106
+
107
+ export function localTime(value) {
108
+ const date = value instanceof Date ? value : new Date(value);
109
+ if (!(date instanceof Date) || Number.isNaN(date.getTime())) return typeof value === 'string' ? value : '?';
110
+ const pad = (n) => String(n).padStart(2, '0');
111
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
112
+ }
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Plugin auto update",
4
+ "description": "Checks for plugin updates on every DSH launch and installs them in the background. A popup reports the results and can disable a busy plugin, update it and restart DSH. Schedule and update log live in Settings → Plugin auto update."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "插件自动更新",
4
+ "description": "每次打开 DSH 时在后台检查插件更新并安装;装完弹窗汇报结果,被占用的插件可一键停用、更新并重启 DSH。检查时机与更新日志在「设置 → 插件自动更新」。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,58 @@
1
1
  {
2
2
  "name": "@aaayeaaa-p1ws/dsh-plugin-auto-update",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.1.1",
4
+ "type": "module",
5
+ "description": "Keeps your DSH plugins current: checks the plugin market on every launch, installs updates in the background, and reports what changed. Adds a settings page for the schedule and an update log, plus one-click recovery when a plugin is in use.",
6
+ "keywords": [
7
+ "dsh",
8
+ "dsh-plugin",
9
+ "deepseek-harness",
10
+ "cordis",
11
+ "plugin",
12
+ "auto-update",
13
+ "updater",
14
+ "settings"
15
+ ],
16
+ "license": "MIT",
17
+ "author": "aaayeaaa-P1WS",
18
+ "engines": {
19
+ "node": ">=20"
20
+ },
21
+ "exports": {
22
+ ".": "./index.js",
23
+ "./client": "./client.js",
24
+ "./locale/*.json": "./locale/*.json",
25
+ "./icon": "./icon.svg"
26
+ },
27
+ "files": [
28
+ "index.js",
29
+ "client.js",
30
+ "lib/*.mjs",
31
+ "cordis.patch.yml",
32
+ "locale/*.json",
33
+ "icon.svg",
34
+ "README.md",
35
+ "LICENSE"
36
+ ],
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/aaayeaaa-P1WS/dsh-plugin-auto-update.git"
40
+ },
41
+ "homepage": "https://github.com/aaayeaaa-P1WS/dsh-plugin-auto-update#readme",
42
+ "bugs": {
43
+ "url": "https://github.com/aaayeaaa-P1WS/dsh-plugin-auto-update/issues"
44
+ },
45
+ "dsh": {
46
+ "bundle": {
47
+ "patch": "./cordis.patch.yml"
48
+ },
49
+ "client": {
50
+ "platform": "web",
51
+ "immediately": true,
52
+ "inject": []
53
+ },
54
+ "engines": {
55
+ "dsh": ">=0.2.2-rc.1"
56
+ }
57
+ }
58
+ }