@quecue/dev-notify 0.1.0 → 0.1.2

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 CHANGED
@@ -1,5 +1,18 @@
1
1
  # 更新记录
2
2
 
3
+ ## 0.1.2 — 2026-11-03
4
+
5
+ ### 改进
6
+
7
+ - `dev-notify update` 安装成功后自动重新安装绑定当前配置的 Claude Code hooks,并重启正在运行的回复桥接;新增 `--no-post` 跳过这些维护步骤。
8
+ - `integration claude-code install` / `remove` 支持 `--yes`,供自动化或 `update` 的维护流程使用。
9
+
10
+ ## 0.1.1 — 2026-11-03
11
+
12
+ ### 新增
13
+
14
+ - 新增 `dev-notify update`:检查 npm 最新版本,确认后调用 npm 全局安装;`--check` 只检查不安装,`--yes` 跳过确认。实际的完整性校验、registry 配置和原生依赖解析仍由 npm 完成。
15
+
3
16
  ## 0.1.0 — 2026-11-03
4
17
 
5
18
  ### 改进
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  跨平台开发任务通知工具,使用 **TypeScript / Node.js**,通过 npm 分发,不需要独立可执行文件或其他脚本运行时。通用通知不依赖 Claude Code、Unity 或项目目录。
4
4
 
5
- **npm 包名为 `@quecue/dev-notify`。`0.1.0` 提供 Claude Code 回复、原生 PC 与 Telegram 双端答题及结果反馈。** 版本变更与升级说明见 [CHANGELOG.md](CHANGELOG.md);公开版本以 npm registry 为准。
5
+ **npm 包名为 `@quecue/dev-notify`。`0.1.2` 提供 Claude Code 回复、原生 PC 与 Telegram 双端答题及结果反馈。** 版本变更与升级说明见 [CHANGELOG.md](CHANGELOG.md);公开版本以 npm registry 为准。
6
6
 
7
7
  ## 安装与使用
8
8
 
@@ -40,6 +40,17 @@ node dist/cli.js send --title "分段预览" --content-file result.txt --dry-run
40
40
 
41
41
  正式包的安装、升级和卸载由 npm 负责。**卸载前建议先清理凭据**:npm 卸载不会自动删除用户凭据。请先交互执行 `dev-notify credentials clear`,再卸载。该命令只删除当前配置引用的 Node 凭据,不会动其他账号、其他配置或旧版原型凭据。
42
42
 
43
+ 已安装的版本可以用 `update` 升级;它只负责检查版本并调用 npm,不会自己下载或替换文件:
44
+
45
+ ```sh
46
+ dev-notify update # 检查并在确认后运行 npm install -g
47
+ dev-notify update --check # 只检查,不安装
48
+ dev-notify update --yes # 跳过确认(适合脚本)
49
+ dev-notify update --no-post # 只更新包,不执行 hooks/桥接维护
50
+ ```
51
+
52
+ 实际的完整性校验、registry 配置、权限处理和原生依赖解析仍由 npm 完成。安装成功后,`update` 会自动重新安装绑定当前配置的 Claude Code hooks(已存在且配置匹配时),并重启正在运行的回复桥接;不需要时可用 `--no-post` 跳过。
53
+
43
54
  ## 已提供的功能
44
55
 
45
56
  - Telegram、飞书双渠道通知,按渲染后的长度分段,不会悄悄截断内容。
@@ -92,6 +103,8 @@ dev-notify integration claude-code status
92
103
  dev-notify integration claude-code remove
93
104
  ```
94
105
 
106
+ `install` / `remove` 可加 `--yes` 跳过确认,供自动化或 `update` 的维护流程使用。
107
+
95
108
  源码验证时将上述 `dev-notify` 替换为 `node dist/cli.js`。安装可用 `--config <绝对或相对路径>` 绑定指定通知配置;执行时将其解析为绝对路径并保存到 hook 参数。自定义 `DEV_NOTIFY_HOME` 的使用者应在安装时设置该变量。
96
109
 
97
110
  - **配置范围**:默认用户级 `~/.claude/settings.json`,设置 `CLAUDE_CONFIG_DIR` 时使用该绝对目录下的 `settings.json`,覆盖该 Claude Code 配置目录下的全部项目。不会扫描或修改每个项目的设置。
package/SECURITY.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # 安全边界(Node/npm)
2
2
 
3
- 当前源码为 `0.1.0`,npm 包名为 `@quecue/dev-notify`;发布版本以 npm registry 为准,源码更新不等同于已发布。本项目尚未完成安全审计或双平台实机验收。
3
+ 当前源码为 `0.1.2`,npm 包名为 `@quecue/dev-notify`;发布版本以 npm registry 为准,源码更新不等同于已发布。本项目尚未完成安全审计或双平台实机验收。
4
4
 
5
5
  ## 凭据与权限
6
6
 
@@ -61,11 +61,13 @@ function owned(hook) {
61
61
  && hook.args[4] === '--config' && typeof hook.args[5] === 'string';
62
62
  }
63
63
  function describe(file, data) {
64
- const result = { file, stop: 0, notification: 0, lifecycle: 0, interaction: 0, others: 0, disabled: data.disableAllHooks === true };
64
+ const result = { file, stop: 0, notification: 0, lifecycle: 0, interaction: 0, others: 0, disabled: data.disableAllHooks === true, configs: [] };
65
65
  for (const event of EVENTS)
66
66
  for (const group of data.hooks?.[event] ?? [])
67
67
  for (const hook of group.hooks) {
68
68
  if (owned(hook)) {
69
+ if (!result.configs.includes(hook.args[5]))
70
+ result.configs.push(hook.args[5]);
69
71
  if (event === 'Stop')
70
72
  result.stop++;
71
73
  else if (event === 'Notification')
package/dist/cli.js CHANGED
@@ -13,7 +13,10 @@ import { claudeCodeStatus, claudeSettingsPath, configureClaudeCode } from './cla
13
13
  import { dispatchClaudeHook, readHookInput } from './claude-hook.js';
14
14
  import { accessReplies, configureReplies, disableReplies } from './reply-state.js';
15
15
  import { startBridge, stopBridge, bridgeStatus, runBridge } from './reply-bridge.js';
16
- const HELP = `dev-notify 0.1.0 — 跨平台开发任务通知
16
+ import { updatePackage } from './update.js';
17
+ import { postUpdate } from './maintenance.js';
18
+ export const VERSION = '0.1.2';
19
+ const HELP = `dev-notify ${VERSION} — 跨平台开发任务通知
17
20
 
18
21
  命令:
19
22
  send --title 标题 [--content 正文 | --content-file 文件 | --stdin]
@@ -26,9 +29,11 @@ const HELP = `dev-notify 0.1.0 — 跨平台开发任务通知
26
29
  credentials clear 移除当前 Node 凭据引用和条目(需要确认)
27
30
  paths 显示配置路径(不会写入文件)
28
31
  doctor / status 离线检查(不会发送消息)
32
+ update [--check] [--yes] [--no-post]
33
+ 检查并更新到 npm 最新版本(确认后调用 npm)
29
34
  integration list 列出可用集成
30
35
  integration claude-code install / status / remove
31
- 单独安装、查询或移除用户级 hooks
36
+ 单独安装、查询或移除用户级 hooks(install/remove 可用 --yes)
32
37
  reply setup 授权可信的 Telegram 私聊用户
33
38
  reply start / status / stop
34
39
  启动、查询或停止回复桥接
@@ -64,7 +69,7 @@ export async function main(argv = process.argv.slice(2), overrides = {}) {
64
69
  title: { type: 'string' }, content: { type: 'string' }, 'content-file': { type: 'string' }, stdin: { type: 'boolean' },
65
70
  channel: { type: 'string' }, level: { type: 'string' }, footer: { type: 'string' }, 'dry-run': { type: 'boolean' },
66
71
  template: { type: 'boolean' }, yes: { type: 'boolean' },
67
- 'no-test': { type: 'boolean' }, 'no-hooks': { type: 'boolean' },
72
+ 'no-test': { type: 'boolean' }, 'no-hooks': { type: 'boolean' }, check: { type: 'boolean' }, 'no-post': { type: 'boolean' },
68
73
  } });
69
74
  }
70
75
  catch {
@@ -76,13 +81,13 @@ export async function main(argv = process.argv.slice(2), overrides = {}) {
76
81
  return 0;
77
82
  }
78
83
  if (values.version) {
79
- context.out('dev-notify 0.1.0');
84
+ context.out(`dev-notify ${VERSION}`);
80
85
  return 0;
81
86
  }
82
87
  const [command, action, integrationAction] = positionals;
83
88
  const allowed = {
84
89
  send: ['title', 'content', 'content-file', 'stdin', 'channel', 'level', 'footer', 'dry-run'],
85
- setup: ['template', 'yes', 'no-test', 'no-hooks'], paths: [], doctor: [], status: [], integration: [], credentials: [], reply: [],
90
+ setup: ['template', 'yes', 'no-test', 'no-hooks'], paths: [], doctor: [], status: [], integration: ['yes'], credentials: [], reply: [], update: ['check', 'yes', 'no-post'],
86
91
  };
87
92
  if (!command || !Object.hasOwn(allowed, command) || positionals.length > (command === 'integration' ? 3 : command === 'credentials' || command === 'reply' ? 2 : 1)
88
93
  || Object.keys(values).some(key => key !== 'config' && !allowed[command].includes(key)))
@@ -93,6 +98,14 @@ export async function main(argv = process.argv.slice(2), overrides = {}) {
93
98
  context.out(JSON.stringify({ config: file, data: paths.data }, null, 2));
94
99
  return 0;
95
100
  }
101
+ if (command === 'update') {
102
+ return await updatePackage(VERSION, {
103
+ out: context.out, prompts: context.prompts,
104
+ check: !!values.check, yes: !!values.yes,
105
+ post: values['no-post'] ? undefined
106
+ : () => postUpdate({ config: file, env: context.env, out: context.out, err: context.err }),
107
+ });
108
+ }
96
109
  if (command === 'integration') {
97
110
  if (action === 'list' && !integrationAction) {
98
111
  context.out('claude-code:支持通知、Telegram 回复和结构化问题交互。通知 hooks 与回复权限分开,默认只发送通知,不接收远程输入。');
@@ -112,13 +125,15 @@ export async function main(argv = process.argv.slice(2), overrides = {}) {
112
125
  : '这里只检查用户级文件。是否实际加载还受项目和托管策略影响,请在 Claude Code 的 /hooks 中确认。');
113
126
  return status.stop === 1 && status.notification === 1 && status.lifecycle === 3 && status.interaction === 5 && !status.disabled ? 0 : 1;
114
127
  }
115
- const prompts = context.prompts ?? terminalPrompts();
128
+ const prompts = context.prompts ?? (values.yes
129
+ ? { out: context.out, text: async () => '', secret: async () => '' }
130
+ : terminalPrompts());
116
131
  if (integrationAction === 'install') {
117
132
  const config = await loadConfig(file, true, context.env);
118
133
  if (!ready(config, 'telegram') && !ready(config, 'feishu'))
119
134
  throw new ConfigError('请先运行 setup 配置至少一个通知渠道,再安装 Claude Code hooks。');
120
135
  }
121
- return await configureClaudeCode(file, integrationAction, prompts, { env: context.env }) ? 0 : 130;
136
+ return await configureClaudeCode(file, integrationAction, prompts, { env: context.env, confirm: values.yes ? false : undefined }) ? 0 : 130;
122
137
  }
123
138
  if (command === 'reply') {
124
139
  if (!['setup', 'start', 'status', 'stop', 'disable', 'run'].includes(action))
@@ -0,0 +1,100 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { fileURLToPath } from 'node:url';
3
+ import path from 'node:path';
4
+ import { setTimeout as delay } from 'node:timers/promises';
5
+ import { ConfigError } from './config.js';
6
+ import { claudeCodeStatus } from './claude-code.js';
7
+ import { bridgeStatus } from './reply-bridge.js';
8
+ function runCli(args) {
9
+ const cli = fileURLToPath(new URL('./cli.js', import.meta.url));
10
+ const result = spawnSync(process.execPath, [cli, ...args], { stdio: 'inherit', timeout: 300000 });
11
+ if (result.error)
12
+ throw new ConfigError('无法运行更新后的 dev-notify。请重新打开终端后运行 dev-notify --version 检查。');
13
+ return result.status ?? 1;
14
+ }
15
+ function reason(error) {
16
+ return error instanceof ConfigError ? error.message : '请运行 dev-notify integration claude-code status 或 dev-notify reply status 检查。';
17
+ }
18
+ const normalize = (file) => process.platform === 'win32' ? path.resolve(file).toLowerCase() : path.resolve(file);
19
+ export async function postUpdate(options) {
20
+ const run = options.run ?? runCli;
21
+ const hooks = options.hooks ?? (() => claudeCodeStatus({ env: options.env }));
22
+ const bridge = options.bridge ?? (() => bridgeStatus(options.config));
23
+ const wait = options.wait ?? delay;
24
+ let failed = false;
25
+ let hooksStatus = null;
26
+ try {
27
+ hooksStatus = await hooks();
28
+ }
29
+ catch (error) {
30
+ options.err(`无法检查 Claude Code hooks:${reason(error)}`);
31
+ failed = true;
32
+ }
33
+ if (hooksStatus) {
34
+ const count = hooksStatus.stop + hooksStatus.notification + hooksStatus.lifecycle + hooksStatus.interaction;
35
+ if (!count)
36
+ options.out('未检测到本工具的 Claude Code hooks,跳过。');
37
+ else if (!hooksStatus.configs.some(file => normalize(file) === normalize(options.config))) {
38
+ options.out('Claude Code hooks 绑定的是其他通知配置,跳过自动更新;如需更新,请用原来的 --config 运行 integration claude-code install。');
39
+ }
40
+ else {
41
+ options.out('更新 Claude Code hooks ...');
42
+ try {
43
+ if (run(['integration', 'claude-code', 'install', '--yes', '--config', options.config]) === 0)
44
+ options.out('Claude Code hooks 已更新。');
45
+ else {
46
+ options.err('Claude Code hooks 更新失败。');
47
+ failed = true;
48
+ }
49
+ }
50
+ catch (error) {
51
+ options.err(`Claude Code hooks 更新失败:${reason(error)}`);
52
+ failed = true;
53
+ }
54
+ }
55
+ }
56
+ let bridgeState = null;
57
+ try {
58
+ bridgeState = await bridge();
59
+ }
60
+ catch (error) {
61
+ options.err(`无法检查回复桥接:${reason(error)}`);
62
+ failed = true;
63
+ }
64
+ if (bridgeState?.running) {
65
+ options.out('重启回复桥接 ...');
66
+ try {
67
+ // 0 表示已停止或本来未运行;1 表示停止请求已登记,下面等待进程释放锁。
68
+ run(['reply', 'stop', '--config', options.config]);
69
+ let stillRunning = true;
70
+ for (let i = 0; i < 20; i++) {
71
+ try {
72
+ stillRunning = (await bridge()).running;
73
+ }
74
+ catch {
75
+ break;
76
+ }
77
+ if (!stillRunning)
78
+ break;
79
+ await wait(500);
80
+ }
81
+ if (stillRunning) {
82
+ options.err('回复桥接仍在退出,请稍后运行 dev-notify reply start。');
83
+ failed = true;
84
+ }
85
+ else if (run(['reply', 'start', '--config', options.config]) === 0)
86
+ options.out('回复桥接已重启。');
87
+ else {
88
+ options.err('回复桥接启动失败。');
89
+ failed = true;
90
+ }
91
+ }
92
+ catch (error) {
93
+ options.err(`回复桥接重启失败:${reason(error)}`);
94
+ failed = true;
95
+ }
96
+ }
97
+ else if (bridgeState)
98
+ options.out('未检测到运行中的回复桥接,跳过。');
99
+ return failed ? 1 : 0;
100
+ }
package/dist/update.js ADDED
@@ -0,0 +1,104 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { ConfigError } from './config.js';
3
+ import { terminalPrompts } from './prompts.js';
4
+ const NAME = '@quecue/dev-notify';
5
+ const VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(alpha|beta|rc)\.(0|[1-9]\d*))?$/;
6
+ function npmRunner(args, capture) {
7
+ // Windows 的 npm 是 .cmd;Node 不能直接 spawn,需要经过 shell。参数均为固定值。
8
+ const result = spawnSync('npm', args, {
9
+ encoding: 'utf8',
10
+ stdio: capture ? 'pipe' : 'inherit',
11
+ timeout: capture ? 30000 : 300000,
12
+ shell: process.platform === 'win32',
13
+ });
14
+ if (result.error)
15
+ throw new ConfigError('无法运行 npm。请确认已安装 npm,并且它在 PATH 中。');
16
+ return { status: result.status, stdout: result.stdout ?? '', stderr: result.stderr ?? '' };
17
+ }
18
+ async function fetchLatest() {
19
+ let response;
20
+ try {
21
+ response = await fetch(`https://registry.npmjs.org/${NAME.replace('/', '%2F')}/latest?t=${Date.now()}`, {
22
+ redirect: 'error',
23
+ signal: AbortSignal.timeout(10000),
24
+ headers: { 'Cache-Control': 'no-cache, no-store', Accept: 'application/json' },
25
+ });
26
+ }
27
+ catch {
28
+ throw new ConfigError('无法检查最新版本。请检查网络后重试。');
29
+ }
30
+ if (!response.ok)
31
+ throw new ConfigError(`无法检查最新版本(HTTP ${response.status})。请检查网络后重试。`);
32
+ let version;
33
+ try {
34
+ version = (await response.json())?.version;
35
+ }
36
+ catch {
37
+ throw new ConfigError('npm registry 返回的版本信息无法解析。');
38
+ }
39
+ if (typeof version !== 'string' || !VERSION.test(version))
40
+ throw new ConfigError('npm registry 返回的版本号格式不受支持。');
41
+ return version;
42
+ }
43
+ function parts(version) {
44
+ const match = VERSION.exec(version);
45
+ if (!match)
46
+ throw new ConfigError('版本号格式不受支持。');
47
+ const rank = match[4] === 'alpha' ? 1 : match[4] === 'beta' ? 2 : match[4] === 'rc' ? 3 : 4;
48
+ return [Number(match[1]), Number(match[2]), Number(match[3]), rank, Number(match[5] ?? 0)];
49
+ }
50
+ export function compareVersions(left, right) {
51
+ const a = parts(left), b = parts(right);
52
+ for (let i = 0; i < a.length; i++)
53
+ if (a[i] !== b[i])
54
+ return a[i] > b[i] ? 1 : -1;
55
+ return 0;
56
+ }
57
+ async function confirmUpdate(latest, options) {
58
+ if (options.yes)
59
+ return true;
60
+ if (!options.prompts && (!process.stdin.isTTY || !process.stdout.isTTY)) {
61
+ throw new ConfigError('更新需要在交互终端中确认;非交互场景请使用 dev-notify update --yes。');
62
+ }
63
+ const prompts = options.prompts ?? terminalPrompts();
64
+ return /^(y|yes)$/i.test(await prompts.text(`更新到 ${latest}?[y/N] `));
65
+ }
66
+ export async function updatePackage(current, options) {
67
+ if (!VERSION.test(current))
68
+ throw new ConfigError('当前版本号格式不受支持,无法检查更新。');
69
+ const latest = await (options.latest ?? fetchLatest)();
70
+ if (!VERSION.test(latest))
71
+ throw new ConfigError('npm registry 返回的版本号格式不受支持。');
72
+ if (compareVersions(latest, current) <= 0) {
73
+ options.out(`已是最新版本 ${current}。`);
74
+ return 0;
75
+ }
76
+ options.out(`当前版本 ${current},npm 最新版本 ${latest}。`);
77
+ if (options.check) {
78
+ options.out('运行 dev-notify update 更新。');
79
+ return 0;
80
+ }
81
+ if (options.post)
82
+ options.out('更新完成后会自动重新安装绑定当前配置的 Claude Code hooks,并重启正在运行的回复桥接;可用 --no-post 跳过。');
83
+ if (!await confirmUpdate(latest, options)) {
84
+ options.out('已取消,没有更新。');
85
+ return 130;
86
+ }
87
+ options.out(`运行 npm install -g ${NAME}@${latest} ...`);
88
+ const result = (options.runner ?? npmRunner)(['install', '-g', `${NAME}@${latest}`], false);
89
+ if (result.status !== 0)
90
+ throw new ConfigError(`更新没有完成。请检查 npm 权限,或手动运行 npm install -g ${NAME}@latest。`);
91
+ options.out(`已更新到 ${latest}。请重新打开终端后运行 dev-notify --version 确认。`);
92
+ if (options.post) {
93
+ const code = await options.post();
94
+ if (code !== 0) {
95
+ options.out('更新已完成,但后续维护步骤没有全部成功;请按上面的提示检查。');
96
+ return code;
97
+ }
98
+ options.out('更新后的维护步骤已完成。');
99
+ }
100
+ else {
101
+ options.out('如有 Claude Code hooks,请运行 dev-notify integration claude-code install;如使用远程回复,请重启桥接。');
102
+ }
103
+ return 0;
104
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quecue/dev-notify",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "跨平台开发任务通知工具,支持 Telegram 与飞书",
5
5
  "license": "MIT",
6
6
  "repository": {