@quecue/dev-notify 0.1.0-alpha.1 → 0.1.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/dist/setup.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { readFile, mkdir, open, rename, unlink } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { randomBytes } from 'node:crypto';
4
- import { ConfigError, decodeUtf8, example, parseConfig } from './config.js';
4
+ import { ConfigError, decodeUtf8, example, parseConfig, ready } from './config.js';
5
5
  import { encodeBundle, nativeStore } from './credentials.js';
6
6
  import { terminalPrompts } from './prompts.js';
7
7
  const yes = (text) => /^(y|yes)$/i.test(text.trim());
8
+ const defaultYes = (text) => !text.trim() || yes(text);
8
9
  export async function snapshot(file) {
9
10
  try {
10
11
  return await readFile(file);
@@ -12,7 +13,7 @@ export async function snapshot(file) {
12
13
  catch (error) {
13
14
  if (error.code === 'ENOENT')
14
15
  return null;
15
- throw new ConfigError('无法读取现有配置,未修改');
16
+ throw new ConfigError('无法读取现有配置,这次没有做任何修改。');
16
17
  }
17
18
  }
18
19
  function same(left, right) { return left === null ? right === null : right !== null && left.equals(right); }
@@ -24,7 +25,7 @@ export function readRaw(raw) {
24
25
  data = JSON.parse(decodeUtf8(raw));
25
26
  }
26
27
  catch {
27
- throw new ConfigError('现有配置损坏,未覆盖;请先修复 JSON');
28
+ throw new ConfigError('现有配置无法解析,这次没有覆盖它。请先修复 JSON。');
28
29
  }
29
30
  parseConfig(data, {}, () => ({}));
30
31
  return data;
@@ -59,20 +60,20 @@ export async function commitConfig(file, data, expected, store, values, options
59
60
  lock = await open(lockPath, 'wx', 0o600);
60
61
  }
61
62
  catch {
62
- throw new ConfigError('配置写入锁不可用;请检查权限,或确认没有运行中的向导后处理残留 .setup.lock');
63
+ throw new ConfigError('配置写入锁不可用。请检查权限;如果确认没有向导在运行,可以删除残留的 .setup.lock。');
63
64
  }
64
65
  const temporary = path.join(path.dirname(file), `.config-${id}.tmp`);
65
66
  let created = false, committed = false, tempCreated = false;
66
67
  try {
67
68
  await lock.close();
68
69
  if (!same(await snapshot(file), expected))
69
- throw new ConfigError('配置在向导期间已变化,未覆盖,请重试');
70
+ throw new ConfigError('配置在向导期间发生了变化,这次没有覆盖它。请重试。');
70
71
  if (values !== null) {
71
72
  store.create(id, values);
72
73
  created = true;
73
74
  const check = store.read(id);
74
75
  if (check === null || JSON.stringify(check) !== JSON.stringify(values))
75
- throw new ConfigError('凭据写入校验失败,未切换配置');
76
+ throw new ConfigError('凭据写入后校验失败,配置没有切换。');
76
77
  }
77
78
  const stream = await open(temporary, 'wx', 0o600);
78
79
  tempCreated = true;
@@ -84,7 +85,7 @@ export async function commitConfig(file, data, expected, store, values, options
84
85
  await stream.close();
85
86
  }
86
87
  if (!same(await snapshot(file), expected))
87
- throw new ConfigError('配置在保存期间已变化,未覆盖,请重试');
88
+ throw new ConfigError('配置在保存期间发生了变化,这次没有覆盖它。请重试。');
88
89
  await (options.replace ?? rename)(temporary, file);
89
90
  committed = true;
90
91
  }
@@ -94,7 +95,7 @@ export async function commitConfig(file, data, expected, store, values, options
94
95
  await removeFile(temporary);
95
96
  }
96
97
  catch {
97
- warn(`临时配置清理失败:${path.basename(temporary)}`);
98
+ warn(`临时配置没有清理掉:${path.basename(temporary)}`);
98
99
  }
99
100
  }
100
101
  if (created && !committed) {
@@ -102,14 +103,14 @@ export async function commitConfig(file, data, expected, store, values, options
102
103
  store.remove(id);
103
104
  }
104
105
  catch {
105
- warn(`配置未切换,但新凭据清理失败;引用 ID:${id}`);
106
+ warn(`配置没有切换,新凭据也未能清理。引用 ID:${id}`);
106
107
  }
107
108
  }
108
109
  try {
109
110
  await removeFile(lockPath);
110
111
  }
111
112
  catch {
112
- warn('配置写入锁清理失败,请确认没有向导运行后处理 .setup.lock');
113
+ warn('配置写入锁没有清理掉。请确认没有向导在运行,再删除 .setup.lock。');
113
114
  }
114
115
  }
115
116
  if (old?.backend === 'keyring') {
@@ -117,11 +118,11 @@ export async function commitConfig(file, data, expected, store, values, options
117
118
  store.remove(old.id);
118
119
  }
119
120
  catch {
120
- warn(`配置已保存,但旧凭据清理失败;引用 ID:${old.id}`);
121
+ warn(`配置已保存,但旧凭据没有清理掉。引用 ID:${old.id}`);
121
122
  }
122
123
  }
123
124
  else if (old?.backend === 'system') {
124
- warn('Python 原型旧凭据未删除;仍由原型管理,避免跨实现误删');
125
+ warn('旧版原型的旧凭据仍然保留,由旧版工具继续管理,避免两个实现互相误删。');
125
126
  }
126
127
  }
127
128
  export async function wizard(file, options = {}) {
@@ -129,11 +130,14 @@ export async function wizard(file, options = {}) {
129
130
  const before = await snapshot(file), data = readRaw(before);
130
131
  const { out } = prompts;
131
132
  out(`配置路径:${file}`);
132
- out('密钥隐藏输入并存入 Windows Credential Manager / macOS Keychain;JSON 只保存引用。');
133
- out('不安装 hooks,不启用远程控制;只有额外同意测试时才发送消息。');
134
- if (before && !yes(await prompts.text('已有配置,继续编辑?[y/N] ')))
135
- return false;
136
- const selection = await prompts.text('选择渠道:1 Telegram,2 飞书,3 两者;其他输入取消:');
133
+ out('Token、webhook 和签名密钥都不会回显,将保存到 Windows Credential Manager / macOS Keychain;配置文件只保留引用。');
134
+ const telegramEnabled = data.telegram?.enabled !== false;
135
+ const feishuEnabled = data.feishu?.enabled !== false;
136
+ const current = before === null ? ''
137
+ : telegramEnabled && feishuEnabled ? '3'
138
+ : telegramEnabled ? '1' : feishuEnabled ? '2' : '';
139
+ const entered = await prompts.text(`选择通知渠道:1 Telegram,2 飞书,3 两者${current ? ` [${current}]` : ''}(输入其他内容取消):`);
140
+ const selection = entered || current;
137
141
  if (!['1', '2', '3'].includes(selection))
138
142
  return false;
139
143
  const store = options.store ?? nativeStore();
@@ -147,13 +151,13 @@ export async function wizard(file, options = {}) {
147
151
  previous = existing;
148
152
  }
149
153
  catch {
150
- out('原凭据无法读取,可能缺失、锁定或权限不足。');
151
- if (!yes(await prompts.text('放弃旧值,重新输入所选渠道凭据?[y/N] ')))
154
+ out('当前凭据暂时读不到,可能是条目缺失、凭据库锁定或权限不足。');
155
+ if (!yes(await prompts.text('要重新输入所选渠道的凭据吗?[y/N] ')))
152
156
  return false;
153
157
  }
154
158
  }
155
159
  else if (reference?.backend === 'system')
156
- out('检测到 Python 原型凭据;本次重新输入保存,旧凭据不读取、不删除。');
160
+ out('检测到旧版原型保存的凭据。本次会重新输入并保存为 Node 版凭据,旧凭据保持不变。');
157
161
  data.telegram ??= {};
158
162
  data.feishu ??= {};
159
163
  data.telegram.enabled = selection !== '2';
@@ -161,11 +165,12 @@ export async function wizard(file, options = {}) {
161
165
  const values = {};
162
166
  const readValue = async (label, key, required) => {
163
167
  while (true) {
164
- const hint = previous[key] ? '留空保留已存值' : '不从环境变量自动复制';
165
- const answer = await prompts.secret(`${label}(${hint}${required ? '' : ';输入 - 清除'}):`);
168
+ const hint = previous[key] ? '留空保留当前值' : '不会从环境变量自动填充';
169
+ const optional = required ? '' : '可选;';
170
+ const answer = await prompts.secret(`${label}(隐藏输入,不回显;${optional}${hint}${required ? '' : ';输入 - 清除'}):`);
166
171
  const value = !required && answer === '-' ? '' : answer || previous[key] || '';
167
172
  if (required && !value) {
168
- out('此项不能为空,请重试,或 Ctrl+C 取消。');
173
+ out('这一项不能为空,请重新输入;按 Ctrl+C 可退出。');
169
174
  continue;
170
175
  }
171
176
  try {
@@ -175,15 +180,15 @@ export async function wizard(file, options = {}) {
175
180
  return value;
176
181
  }
177
182
  catch {
178
- out('格式不正确,请检查后重新输入;不会显示秘密。');
183
+ out('格式不正确,请重新输入(输入内容不会显示)。');
179
184
  }
180
185
  }
181
186
  };
182
187
  if (data.telegram.enabled) {
183
188
  values.telegram_token = await readValue('Telegram Bot Token', 'telegram_token', true);
184
- out('接收人需要先启动 Bot;本轮手动填写数字 chat_id,不启动 getUpdates 或自动配对。');
189
+ out('请先让接收人给 Bot 发一条消息,然后填写数字 chat_id;这里不会自动获取或配对接收人。');
185
190
  while (true) {
186
- const entered = await prompts.text('接收人 chat_id,多个用逗号分隔(留空保留原名单):');
191
+ const entered = await prompts.text('接收人 chat_id,多个用逗号分隔(留空保留现有名单):');
187
192
  const ids = entered ? entered.split(',').map(item => item.trim()) : data.telegram.chat_ids ?? [];
188
193
  try {
189
194
  if (!ids.length)
@@ -193,58 +198,84 @@ export async function wizard(file, options = {}) {
193
198
  break;
194
199
  }
195
200
  catch {
196
- out('请填写至少一个有效数字 chat_id,或 Ctrl+C 取消。');
201
+ out('请填写至少一个有效的数字 chat_id;按 Ctrl+C 可退出。');
197
202
  }
198
203
  }
199
204
  }
200
205
  if (data.feishu.enabled) {
201
206
  values.feishu_webhook = await readValue('飞书 webhook', 'feishu_webhook', true);
202
- values.feishu_secret = await readValue('飞书签名密钥(可选)', 'feishu_secret', false);
207
+ values.feishu_secret = await readValue('飞书签名密钥', 'feishu_secret', false);
203
208
  }
204
209
  encodeBundle(values);
205
210
  const preview = { ...data, credentials: { backend: 'keyring', id: '0'.repeat(32) } };
206
211
  parseConfig(preview, {}, () => values);
207
212
  const env = options.env ?? process.env;
208
- out(`将启用:${data.telegram.enabled ? `Telegram(${data.telegram.chat_ids.length} 个接收人)` : ''}${data.feishu.enabled ? ' 飞书' : ''}`);
209
- out('环境变量仍优先(含显式空值);测试通知会使用最终生效的配置,不一定是刚输入的值。');
213
+ const channels = [];
214
+ if (data.telegram.enabled)
215
+ channels.push(`Telegram(${data.telegram.chat_ids.length} 个接收人)`);
216
+ if (data.feishu.enabled)
217
+ channels.push('飞书');
218
+ out(`将启用:${channels.join('、')}`);
219
+ out('环境变量的优先级更高,空值也会覆盖这里的设置;测试通知使用最终生效的配置。');
210
220
  const overrides = [data.telegram.token_env ?? 'TELEGRAM_BOT_TOKEN', data.feishu.webhook_env ?? 'FEISHU_WEBHOOK',
211
221
  data.feishu.secret_env ?? 'FEISHU_SECRET', 'TELEGRAM_CHAT_ID', 'TELEGRAM_CHAT_IDS'].filter(name => Object.hasOwn(env, name));
212
222
  if (overrides.length)
213
- out('当前存在环境覆盖:' + overrides.join('、'));
223
+ out('以下环境变量正在覆盖配置:' + overrides.join('、'));
214
224
  if (reference?.backend === 'keyring')
215
- out('保存成功后删除旧凭据;共享旧引用 ID 的配置副本也需更新。');
216
- out('未选中的渠道会禁用;新条目只保存所选渠道凭据。');
217
- if (!yes(await prompts.text('确认保存配置和系统凭据?[y/N] ')))
225
+ out('保存成功后会删除旧凭据;如果其他配置也引用了同一个凭据,需要一并更新。');
226
+ out('没有选中的渠道会被停用;新凭据只用于本次选中的渠道。');
227
+ for (const line of options.notes ?? [])
228
+ out(line);
229
+ const actions = ['保存配置和系统凭据'];
230
+ if (options.testSend)
231
+ actions.push('发送固定内容的测试通知');
232
+ if (options.afterSave)
233
+ actions.push('安装 Claude Code 通知 hooks');
234
+ if (!defaultYes(await prompts.text(`将${actions.join('、')}。继续?[Y/n] `)))
218
235
  return false;
219
236
  await commitConfig(file, data, before, store, values, { warn: out });
220
237
  out('配置和系统凭据已保存。');
221
- if (options.testSend && yes(await prompts.text('发送固定内容的测试通知到生效的接收目标?[y/N] '))) {
238
+ if (options.testSend) {
222
239
  try {
223
240
  const effective = parseConfig(preview, env, () => values);
224
241
  const ok = await options.testSend(effective);
225
- out(ok ? '测试通知发送成功。' : '配置已保存,但测试通知失败或没有可用渠道;请运行 doctor 检查。');
242
+ out(ok ? '测试通知发送成功。' : '配置已保存。测试通知没有发送成功,请运行 dev-notify doctor 检查。');
226
243
  }
227
244
  catch {
228
- out('配置已保存,但环境覆盖或网络导致测试失败;请运行 doctor 检查。');
245
+ out('配置已保存。测试通知没有发送成功,可能是环境变量覆盖或网络问题;请运行 dev-notify doctor 检查。');
229
246
  }
230
247
  }
231
248
  else
232
- out('未发送测试通知。');
249
+ out('已跳过测试通知。');
250
+ if (options.afterSave) {
251
+ try {
252
+ await options.afterSave();
253
+ }
254
+ catch (error) {
255
+ out(`配置已保存。Claude Code hooks 没有安装完成:${error instanceof ConfigError ? error.message : '请运行 integration claude-code status 检查'}`);
256
+ }
257
+ }
258
+ try {
259
+ if (ready(parseConfig(preview, env, () => values), 'telegram')) {
260
+ out('如需在 Telegram 中回复 Claude,请运行:dev-notify reply setup');
261
+ }
262
+ }
263
+ catch { /* 环境覆盖无效时上方测试分支已报告。 */ }
233
264
  return true;
234
265
  }
235
266
  export async function clearCredentials(file, prompts, store) {
236
267
  const before = await snapshot(file), data = readRaw(before);
237
268
  const reference = data.credentials;
238
269
  if (!reference) {
239
- prompts.out('此配置没有系统凭据引用,未修改。');
270
+ prompts.out('当前配置没有系统凭据引用,无需清理。');
240
271
  return true;
241
272
  }
242
273
  if (reference.backend !== 'keyring')
243
- throw new ConfigError('这是 Python 原型的凭据,Node 不执行跨实现删除');
244
- prompts.out('将移除当前配置的系统凭据引用并删除其条目;环境变量不变,共享引用的配置副本将失效。');
245
- if (!yes(await prompts.text('确认清除?[y/N] ')))
274
+ throw new ConfigError('这是旧版原型保存的凭据;Node 版不会跨实现删除,请用原工具处理。');
275
+ prompts.out('将删除当前配置引用的系统凭据,并从配置中移除引用。环境变量不受影响;如果其他配置共享同一个凭据,它们也会失效。');
276
+ if (!yes(await prompts.text('确认清除吗?[y/N] ')))
246
277
  return false;
247
278
  await commitConfig(file, data, before, store ?? nativeStore(), null, { warn: prompts.out });
248
- prompts.out('凭据引用已移除;如删除条目失败,上方会显示警告。');
279
+ prompts.out('凭据引用已移除。如果系统条目删除失败,上方会显示提示。');
249
280
  return true;
250
281
  }
package/package.json CHANGED
@@ -1,8 +1,16 @@
1
1
  {
2
2
  "name": "@quecue/dev-notify",
3
- "version": "0.1.0-alpha.1",
3
+ "version": "0.1.0",
4
4
  "description": "跨平台开发任务通知工具,支持 Telegram 与飞书",
5
5
  "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/QueCue/dev-notify.git"
9
+ },
10
+ "homepage": "https://github.com/QueCue/dev-notify#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/QueCue/dev-notify/issues"
13
+ },
6
14
  "type": "module",
7
15
  "engines": {
8
16
  "node": ">=22.0.0"
@@ -14,6 +22,7 @@
14
22
  "dist/*.js",
15
23
  "README.md",
16
24
  "SECURITY.md",
25
+ "CHANGELOG.md",
17
26
  "LICENSE"
18
27
  ],
19
28
  "scripts": {