dsh-mishu 0.5.1 → 0.5.4

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/README.md CHANGED
@@ -72,6 +72,8 @@
72
72
 
73
73
  **设置**:点页面顶部的"设置":语言、各类别提前几天提醒、GitHub(开关、token 从哪来、测试连接、在 GitHub 上标已读的方式)、邮箱账户(添加、测试连接、现在收一次、编辑、删除)。飞书即将接入。
74
74
 
75
+ **重启 DSH**:页面顶部"设置"后面的按键。重装或升级插件后点它,桌面版会按正常方式退出(有任务在跑时会先问你),再自动打开。用 `dsh` 命令启动的 DSH 没法从页面重启(新的登录地址只会打印在那个终端里),按键会告诉你回终端按 Ctrl+C 再运行一次。Windows 和 Linux 桌面版的重启还没在真机上测过。
76
+
75
77
  **文字命令**:
76
78
 
77
79
  ```
@@ -196,6 +198,8 @@ Cards can be dragged: onto "Done for you" to mark done, onto "Keeping for you" t
196
198
 
197
199
  **Settings**: the "Settings" tab at the top of the page: language, warning days per category, GitHub (on/off, where the token comes from, connection test, how to mark read), mail accounts (add, test, check now, edit, delete). Feishu / Lark is coming.
198
200
 
201
+ **Restart DSH**: the button after "Settings" at the top of the page. After reinstalling or upgrading plugins, click it: the desktop app quits the normal way (asking first if a task is running) and opens again. DSH started with the `dsh` command cannot be restarted from the page (its new sign-in address is printed only in that terminal), so the button tells you to press Ctrl+C there and run it again. Restarting the Windows and Linux desktop apps is not yet tested on real machines.
202
+
199
203
  **Text commands**:
200
204
 
201
205
  ```
package/lib/client.js CHANGED
@@ -60,6 +60,10 @@ var mishuApi = {
60
60
  add: (lang, item) => post("/add", { lang, item }),
61
61
  ask: (lang, ask) => post("/ask", { lang, ask })
62
62
  };
63
+ var restartApi = {
64
+ info: (lang) => post("/restart/info", { lang }),
65
+ restart: (lang) => post("/restart", { lang })
66
+ };
63
67
  var settingsApi = {
64
68
  load: (lang) => post("/settings", { lang }),
65
69
  save: (lang, settings) => post("/settings/save", { lang, settings }),
@@ -134,6 +138,10 @@ var zh = {
134
138
  resize: "\u62D6\u52A8\u8C03\u6574\u5BBD\u5EA6\uFF0C\u53CC\u51FB\u6062\u590D\u7B49\u5BBD",
135
139
  tabBoard: "\u770B\u677F",
136
140
  tabSettings: "\u8BBE\u7F6E",
141
+ restart: "\u91CD\u542F DSH",
142
+ restartTip: "\u91CD\u88C5\u6216\u5347\u7EA7\u63D2\u4EF6\u540E\uFF0C\u53EF\u4EE5\u7528\u8FD9\u4E2A\u6309\u952E\u91CD\u542F\u672C DSH",
143
+ restartConfirm: "\u91CD\u542F DSH\uFF1F\u5B89\u88C5\u63D2\u4EF6\u6216\u8005\u63D2\u4EF6\u66F4\u65B0\u540E\u9700\u8981\u91CD\u542F DSH\u3002",
144
+ restarting: "\u6B63\u5728\u91CD\u542F\u2026\u7A97\u53E3\u4F1A\u5173\u6389\u518D\u81EA\u52A8\u6253\u5F00",
137
145
  all: "\u5168\u90E8",
138
146
  sourceGithub: "GitHub",
139
147
  sourceMail: "\u90AE\u4EF6",
@@ -258,6 +266,10 @@ var en = {
258
266
  resize: "Drag to resize; double-click to make them equal",
259
267
  tabBoard: "Board",
260
268
  tabSettings: "Settings",
269
+ restart: "Restart DSH",
270
+ restartTip: "After reinstalling or upgrading plugins, use this to restart DSH",
271
+ restartConfirm: "Restart DSH? DSH needs a restart after you install or update plugins.",
272
+ restarting: "Restarting\u2026 the window closes and opens again",
261
273
  all: "All",
262
274
  sourceGithub: "GitHub",
263
275
  sourceMail: "Mail",
@@ -876,7 +888,10 @@ function MishuBoard({ locale, openChat: openChat2 }) {
876
888
  );
877
889
  if (board === void 0) {
878
890
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mishu-page", children: [
879
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mishu-head", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h1", { children: t.title }) }),
891
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mishu-head", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mishu-title", children: [
892
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h1", { children: t.title }),
893
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RestartButton, { lang, locale })
894
+ ] }) }),
880
895
  error === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mishu-empty", children: t.loading }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mishu-error", children: [
881
896
  t.failed,
882
897
  error
@@ -920,7 +935,8 @@ function MishuBoard({ locale, openChat: openChat2 }) {
920
935
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mishu-tabs", role: "tablist", children: [
921
936
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", role: "tab", "aria-selected": tab === "board", className: tab === "board" ? "active" : "", onClick: () => setTab("board"), children: t.tabBoard }),
922
937
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", role: "tab", "aria-selected": tab === "settings", className: tab === "settings" ? "active" : "", onClick: () => setTab("settings"), children: t.tabSettings })
923
- ] })
938
+ ] }),
939
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RestartButton, { lang, locale })
924
940
  ] }),
925
941
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mishu-sources", children: [
926
942
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { children: [
@@ -1166,6 +1182,33 @@ function AddForm({ lang, locale, onAdded }) {
1166
1182
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mishu-hint", children: t.chatHint })
1167
1183
  ] });
1168
1184
  }
1185
+ function RestartButton({ lang, locale }) {
1186
+ const t = TEXT[lang];
1187
+ const [why, setWhy] = (0, import_react3.useState)();
1188
+ const [status, setStatus] = (0, import_react3.useState)();
1189
+ (0, import_react3.useEffect)(() => {
1190
+ void restartApi.info(locale()).then((r) => {
1191
+ if (r.ok && !r.supported) setWhy(r.why);
1192
+ });
1193
+ }, [locale]);
1194
+ const restart = async () => {
1195
+ if (why !== void 0) {
1196
+ setStatus({ ok: false, text: why });
1197
+ return;
1198
+ }
1199
+ if (!window.confirm(t.restartConfirm)) return;
1200
+ setStatus({ ok: true, text: t.restarting });
1201
+ const r = await restartApi.restart(locale());
1202
+ if (!r.ok) setStatus({ ok: false, text: r.message });
1203
+ };
1204
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
1205
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", className: "mishu-btn", title: why === void 0 ? t.restartTip : `${t.restartTip}
1206
+ ${why}`, disabled: status?.ok === true, onClick: () => {
1207
+ void restart();
1208
+ }, children: t.restart }),
1209
+ status === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: status.ok ? "mishu-hint" : "mishu-hint mishu-warn", children: status.text })
1210
+ ] });
1211
+ }
1169
1212
 
1170
1213
  // src/client/styles.ts
1171
1214
  var v = (name, fallback) => `var(--dsw-alias-${name}, ${fallback})`;
package/lib/core/index.js CHANGED
@@ -23,6 +23,7 @@ export { ImapFetcher } from "./mail/imap.js";
23
23
  export { judgeMail } from "./mail/rules.js";
24
24
  export { describeMailError, MAIL_SOURCE, mailReportKey, syncMail } from "./mail/sync.js";
25
25
  export { findProgram } from "./exec.js";
26
+ export { restartPlan, scheduleRestart } from "./restart.js";
26
27
  export { MemorySecrets, secretEnvName, secretFromEnv, systemSecrets } from "./secrets.js";
27
28
  export { deleteMailAccount, effectiveSettings, mailAccounts, saveMailAccount, saveSettings } from "./settings.js";
28
29
  export const DEFAULT_HOME = '~/.mishu';
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Restarting DSH after plugins were reinstalled or upgraded.
3
+ *
4
+ * In the desktop app, DSH runs as a child of the app's main process
5
+ * (Electron as Node, `ELECTRON_RUN_AS_NODE=1`). The `dsh` command runs on
6
+ * the same binary the same way, so the test is the parent: only when the
7
+ * parent process IS this app binary is there an app to restart (from a
8
+ * terminal the parent is a shell, and `__CFBundleIdentifier` names the
9
+ * terminal app — never trusted). A restart asks the app to
10
+ * quit the normal way — so it still asks first when a task is running — and
11
+ * opens it again once it is gone. That is done by a small detached helper,
12
+ * because this process ends with the app.
13
+ *
14
+ * macOS AppleScript `quit`, then `open -b <bundle id>` (tested)
15
+ * Windows `taskkill /PID` without /F (a close request), then the app again
16
+ * Linux SIGTERM to the app, then the app again
17
+ *
18
+ * DSH started with the `dsh` command is not restarted from here: its web
19
+ * address (with a new sign-in token) is printed in that terminal, which a
20
+ * detached replacement could not reach. The page says what to do instead.
21
+ */
22
+ import { execFileSync, spawn } from 'node:child_process';
23
+ import { readlinkSync } from 'node:fs';
24
+ import { tmpdir } from 'node:os';
25
+ import { join } from 'node:path';
26
+ import { findProgram } from "./exec.js";
27
+ import { bi } from "./i18n.js";
28
+ /** `<x>.app` holding a macOS executable, if any. */
29
+ function appBundle(execPath) {
30
+ const at = execPath.indexOf('.app/');
31
+ return at < 0 ? undefined : execPath.slice(0, at + 4);
32
+ }
33
+ function parentExecutable(ppid, platform) {
34
+ try {
35
+ if (platform === 'linux')
36
+ return readlinkSync(`/proc/${ppid}/exe`);
37
+ if (platform === 'win32') {
38
+ const ps = findProgram('powershell');
39
+ return ps === undefined ? undefined : execFileSync(ps, ['-NoProfile', '-NonInteractive', '-Command', `(Get-Process -Id ${ppid}).Path`], { timeout: 10_000, windowsHide: true }).toString().trim();
40
+ }
41
+ return execFileSync('/bin/ps', ['-o', 'comm=', '-p', String(ppid)], { timeout: 5000 }).toString().trim();
42
+ }
43
+ catch {
44
+ return undefined;
45
+ }
46
+ }
47
+ function bundleIdOf(app) {
48
+ try {
49
+ return execFileSync('/usr/bin/defaults', ['read', `${app}/Contents/Info`, 'CFBundleIdentifier'], { timeout: 5000 }).toString().trim();
50
+ }
51
+ catch {
52
+ return undefined;
53
+ }
54
+ }
55
+ function here() {
56
+ const facts = { env: process.env, platform: process.platform, execPath: process.execPath, ppid: process.ppid, electron: process.versions.electron, parentExe: undefined, bundleId: undefined };
57
+ if (facts.electron === undefined || facts.env.ELECTRON_RUN_AS_NODE !== '1' || facts.ppid <= 1)
58
+ return facts;
59
+ facts.parentExe = parentExecutable(facts.ppid, facts.platform);
60
+ const app = facts.platform === 'darwin' ? appBundle(facts.execPath) : undefined;
61
+ if (app !== undefined)
62
+ facts.bundleId = bundleIdOf(app);
63
+ return facts;
64
+ }
65
+ function samePath(a, b, platform) {
66
+ return a !== undefined && (platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b);
67
+ }
68
+ const FROM_TERMINAL = bi('这个 DSH 是用 dsh 命令启动的,小秘书没法替它重启:回到运行 dsh 的那个终端按 Ctrl+C,再运行同一条命令。', 'This DSH was started with the dsh command, so the secretary cannot restart it: press Ctrl+C in the terminal running dsh, then run the same command again.');
69
+ /** How DSH can be restarted from here, or why it cannot. */
70
+ export function restartPlan(facts = here()) {
71
+ const desktop = facts.electron !== undefined && facts.env.ELECTRON_RUN_AS_NODE === '1' && facts.ppid > 1 && samePath(facts.parentExe, facts.execPath, facts.platform);
72
+ if (!desktop)
73
+ return { kind: 'none', why: FROM_TERMINAL };
74
+ if (facts.platform === 'darwin') {
75
+ const bundleId = facts.bundleId ?? '';
76
+ if (/^[\w.-]+$/.test(bundleId))
77
+ return { kind: 'macos-app', bundleId, appPid: facts.ppid };
78
+ return { kind: 'none', why: bi('没找到桌面版的应用标识,没法替它重启。请退出 DSH 再打开。', 'Could not tell which app this is, so it cannot be restarted from here. Quit DSH and open it again.') };
79
+ }
80
+ if (facts.platform === 'win32' || facts.platform === 'linux')
81
+ return { kind: 'desktop-app', platform: facts.platform, exe: facts.execPath, appPid: facts.ppid };
82
+ return { kind: 'none', why: bi('这个系统上没法替桌面版重启。请退出 DSH 再打开。', 'The desktop app cannot be restarted from here on this system. Quit DSH and open it again.') };
83
+ }
84
+ const MAC_TOOLS = { osascript: '/usr/bin/osascript', open: '/usr/bin/open' };
85
+ /**
86
+ * Source of the detached helper: ask the app to quit, wait until it is gone
87
+ * (giving up quietly when you cancel the quit), open it again, and check it
88
+ * came back. Every step is noted in `log`, the only account of a restart
89
+ * once this process is gone.
90
+ *
91
+ * The helper itself runs as Node (`ELECTRON_RUN_AS_NODE=1`), and whatever it
92
+ * launches inherits its environment — `open` included ("Opened applications
93
+ * inherit environment variables", open(1)). So the app is always started
94
+ * without that variable; with it, the app starts as bare Node and exits.
95
+ */
96
+ export function restartHelperSource(plan, log, tools = MAC_TOOLS) {
97
+ const mac = plan.kind === 'macos-app';
98
+ const app = mac ? `application id ${JSON.stringify(plan.bundleId)}` : '';
99
+ const quit = mac
100
+ ? `spawnSync(${JSON.stringify(tools.osascript)}, ['-e', ${JSON.stringify(`tell ${app} to quit`)}], { timeout: 180000 })`
101
+ : plan.platform === 'win32'
102
+ ? `spawnSync('taskkill', ['/PID', String(pid)], { windowsHide: true })`
103
+ : `process.kill(pid, 'SIGTERM')`;
104
+ // Answers whether the app runs again; only macOS can ask, elsewhere the launch is trusted.
105
+ const running = mac
106
+ ? `String(spawnSync(${JSON.stringify(tools.osascript)}, ['-e', ${JSON.stringify(`${app} is running`)}], { timeout: 10000 }).stdout).trim() === 'true'`
107
+ : 'true';
108
+ const open = mac
109
+ ? `const r = spawnSync(${JSON.stringify(tools.open)}, ['-b', ${JSON.stringify(plan.bundleId)}], { env: clean, timeout: 60000 }); note('open exited ' + r.status + (String(r.stderr || '').trim() ? ': ' + String(r.stderr).trim() : ''))`
110
+ : `spawn(${JSON.stringify(plan.exe)}, [], { detached: true, stdio: 'ignore', env: clean }).unref()`;
111
+ return [
112
+ "const { spawn, spawnSync } = require('node:child_process')",
113
+ "const fs = require('node:fs')",
114
+ `const pid = ${plan.appPid}`,
115
+ `const log = ${JSON.stringify(log)}`,
116
+ "const clean = Object.fromEntries(Object.entries(process.env).filter(([k]) => k !== 'ELECTRON_RUN_AS_NODE'))",
117
+ "const note = (line) => { try { fs.appendFileSync(log, new Date().toISOString() + ' ' + line + '\\n') } catch {} }",
118
+ 'const sleep = (ms) => new Promise(r => setTimeout(r, ms))',
119
+ "const alive = () => { try { process.kill(pid, 0); return true } catch (e) { return e.code === 'EPERM' } }",
120
+ `const running = () => { try { return ${running} } catch { return false } }`,
121
+ `const open = () => { try { ${open} } catch (e) { note('could not open the app: ' + (e && e.message)) } }`,
122
+ 'const main = async () => {',
123
+ // Let the page receive its answer before the app starts closing.
124
+ ' await sleep(400)',
125
+ " note('asking the app (pid ' + pid + ') to quit')",
126
+ ` try { ${quit} } catch (e) { note('quit request failed: ' + (e && e.message)) }`,
127
+ ' const until = Date.now() + 180000',
128
+ ' while (alive() && Date.now() < until) await sleep(300)',
129
+ " if (alive()) { note('the app is still running (the quit was cancelled?); not reopening'); return }",
130
+ ' await sleep(800)',
131
+ ' for (let attempt = 1; attempt <= 2; attempt++) {',
132
+ " note('opening the app (attempt ' + attempt + ')')",
133
+ ' open()',
134
+ ' const by = Date.now() + 20000',
135
+ ' while (!running() && Date.now() < by) await sleep(500)',
136
+ " if (running()) { note('the app is running again'); return }",
137
+ ' }',
138
+ " note('the app did not come back; open it yourself')",
139
+ '}',
140
+ "main().catch(e => note('helper failed: ' + (e && e.stack || e)))",
141
+ ].join('\n');
142
+ }
143
+ /** Start the helper and return where it logs. Runs with this process's Node (Electron as Node in the app). */
144
+ export function scheduleRestart(plan) {
145
+ const log = join(tmpdir(), `dsh-mishu-restart-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}.log`);
146
+ const helper = spawn(process.execPath, ['-e', restartHelperSource(plan, log)], { detached: true, stdio: 'ignore', env: process.env, windowsHide: true });
147
+ helper.unref();
148
+ return { log };
149
+ }
package/lib/dsh/api.js CHANGED
@@ -240,6 +240,18 @@ export function apiHandlers(runtime, clock = Date.now) {
240
240
  }
241
241
  return { view: await settingsView(lang) };
242
242
  },
243
+ /** Whether the page's "restart DSH" button can work here, and if not, what to do instead. */
244
+ '/restart/info': async (body) => {
245
+ const plan = runtime.restarter.plan();
246
+ return plan.kind === 'none' ? { supported: false, why: plan.why[langOf(body)] } : { supported: true };
247
+ },
248
+ '/restart': async (body) => {
249
+ const plan = runtime.restarter.plan();
250
+ if (plan.kind === 'none')
251
+ throw new ApiError(plan.why[langOf(body)]);
252
+ const { log } = runtime.restarter.schedule(plan);
253
+ return { restarting: true, log };
254
+ },
243
255
  '/mail/sync': async (body) => {
244
256
  const lang = langOf(body);
245
257
  const account = runtime.accounts().find(a => a.id === Number(body.id));
@@ -4,7 +4,7 @@
4
4
  * what you set on the settings page on top), the keychain, and one sync of
5
5
  * every source at a time.
6
6
  */
7
- import { dayOf, detectLang, effectiveSettings, GitHubClient, ImapFetcher, mailAccounts, mailReportKey, renderBrief, resolveGithubToken, secretAccount, secretFromEnv, syncGithub, syncMail, systemSecrets, tickManual, bi, } from "../core/index.js";
7
+ import { dayOf, detectLang, effectiveSettings, GitHubClient, ImapFetcher, mailAccounts, mailReportKey, renderBrief, resolveGithubToken, secretAccount, secretFromEnv, syncGithub, syncMail, systemSecrets, tickManual, bi, restartPlan, scheduleRestart, } from "../core/index.js";
8
8
  export class Runtime {
9
9
  store;
10
10
  config;
@@ -12,6 +12,8 @@ export class Runtime {
12
12
  fetcher;
13
13
  logger;
14
14
  syncing;
15
+ /** How DSH restarts; tests swap it so they never restart anything. */
16
+ restarter = { plan: () => restartPlan(), schedule: scheduleRestart };
15
17
  constructor(config, logger, secrets = systemSecrets(), fetcher = new ImapFetcher()) {
16
18
  this.config = config;
17
19
  this.logger = logger;
@@ -22,6 +22,7 @@ export { ImapFetcher } from './mail/imap.ts';
22
22
  export { judgeMail } from './mail/rules.ts';
23
23
  export { describeMailError, MAIL_SOURCE, mailReportKey, syncMail, type MailSyncReport } from './mail/sync.ts';
24
24
  export { findProgram } from './exec.ts';
25
+ export { restartPlan, scheduleRestart, type RestartPlan } from './restart.ts';
25
26
  export { MemorySecrets, secretEnvName, secretFromEnv, systemSecrets, type SecretStatus, type SecretStore } from './secrets.ts';
26
27
  export { deleteMailAccount, effectiveSettings, mailAccounts, saveMailAccount, saveSettings, type Defaults, type UserSettings } from './settings.ts';
27
28
  export declare const DEFAULT_HOME = "~/.mishu";
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Restarting DSH after plugins were reinstalled or upgraded.
3
+ *
4
+ * In the desktop app, DSH runs as a child of the app's main process
5
+ * (Electron as Node, `ELECTRON_RUN_AS_NODE=1`). The `dsh` command runs on
6
+ * the same binary the same way, so the test is the parent: only when the
7
+ * parent process IS this app binary is there an app to restart (from a
8
+ * terminal the parent is a shell, and `__CFBundleIdentifier` names the
9
+ * terminal app — never trusted). A restart asks the app to
10
+ * quit the normal way — so it still asks first when a task is running — and
11
+ * opens it again once it is gone. That is done by a small detached helper,
12
+ * because this process ends with the app.
13
+ *
14
+ * macOS AppleScript `quit`, then `open -b <bundle id>` (tested)
15
+ * Windows `taskkill /PID` without /F (a close request), then the app again
16
+ * Linux SIGTERM to the app, then the app again
17
+ *
18
+ * DSH started with the `dsh` command is not restarted from here: its web
19
+ * address (with a new sign-in token) is printed in that terminal, which a
20
+ * detached replacement could not reach. The page says what to do instead.
21
+ */
22
+ import { type Bi } from './i18n.ts';
23
+ export type RestartPlan = {
24
+ kind: 'macos-app';
25
+ bundleId: string;
26
+ appPid: number;
27
+ } | {
28
+ kind: 'desktop-app';
29
+ platform: 'win32' | 'linux';
30
+ exe: string;
31
+ appPid: number;
32
+ } | {
33
+ kind: 'none';
34
+ why: Bi;
35
+ };
36
+ export interface ProcessFacts {
37
+ env: NodeJS.ProcessEnv;
38
+ platform: NodeJS.Platform;
39
+ execPath: string;
40
+ ppid: number;
41
+ electron: string | undefined;
42
+ /** The parent process's executable, when it can be read. */
43
+ parentExe: string | undefined;
44
+ /** macOS: CFBundleIdentifier from the Info.plist of the .app holding `execPath`. */
45
+ bundleId: string | undefined;
46
+ }
47
+ /** How DSH can be restarted from here, or why it cannot. */
48
+ export declare function restartPlan(facts?: ProcessFacts): RestartPlan;
49
+ /** The programs the macOS helper runs; tests point them at stand-ins. */
50
+ export interface MacTools {
51
+ osascript: string;
52
+ open: string;
53
+ }
54
+ /**
55
+ * Source of the detached helper: ask the app to quit, wait until it is gone
56
+ * (giving up quietly when you cancel the quit), open it again, and check it
57
+ * came back. Every step is noted in `log`, the only account of a restart
58
+ * once this process is gone.
59
+ *
60
+ * The helper itself runs as Node (`ELECTRON_RUN_AS_NODE=1`), and whatever it
61
+ * launches inherits its environment — `open` included ("Opened applications
62
+ * inherit environment variables", open(1)). So the app is always started
63
+ * without that variable; with it, the app starts as bare Node and exits.
64
+ */
65
+ export declare function restartHelperSource(plan: Exclude<RestartPlan, {
66
+ kind: 'none';
67
+ }>, log: string, tools?: MacTools): string;
68
+ /** Start the helper and return where it logs. Runs with this process's Node (Electron as Node in the app). */
69
+ export declare function scheduleRestart(plan: Exclude<RestartPlan, {
70
+ kind: 'none';
71
+ }>): {
72
+ log: string;
73
+ };
@@ -4,7 +4,7 @@
4
4
  * what you set on the settings page on top), the keychain, and one sync of
5
5
  * every source at a time.
6
6
  */
7
- import { type Defaults, type Lang, type MailAccount, type MailFetcher, type MailSyncReport, type MishuStore, type SecretStore, type TokenResult } from '../core/index.ts';
7
+ import { type Defaults, type Lang, type MailAccount, type MailFetcher, type MailSyncReport, type MishuStore, type RestartPlan, type SecretStore, type TokenResult } from '../core/index.ts';
8
8
  import type { MishuConfig } from './config.ts';
9
9
  interface Logger {
10
10
  info(message: string): void;
@@ -17,6 +17,15 @@ export declare class Runtime {
17
17
  readonly fetcher: MailFetcher;
18
18
  private readonly logger;
19
19
  private syncing;
20
+ /** How DSH restarts; tests swap it so they never restart anything. */
21
+ restarter: {
22
+ plan(): RestartPlan;
23
+ schedule(plan: Exclude<RestartPlan, {
24
+ kind: 'none';
25
+ }>): {
26
+ log: string;
27
+ };
28
+ };
20
29
  constructor(config: MishuConfig, logger: Logger, secrets?: SecretStore, fetcher?: MailFetcher);
21
30
  defaults(): Defaults;
22
31
  settings(): Defaults;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mishu",
3
- "version": "0.5.1",
3
+ "version": "0.5.4",
4
4
  "description": "DSH secretary (小秘书) for DeepSeek Harness (dsh): watches GitHub notifications and the dated things you tell it (bills, renewals, documents, birthdays), closes what is already resolved, and keeps a short list of what actually needs you. Bilingual (中文 / English), macOS / Windows / Linux. · DSH 小秘书:提醒、通知、待办、消息、收件箱、秘书。GitHub 通知按当前状态分诊,交代一句就记下到期和周期事项。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",