@quecue/dev-notify 0.1.3 → 0.1.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/dist/setup.js CHANGED
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { randomBytes } from 'node:crypto';
4
4
  import { ConfigError, decodeUtf8, example, parseConfig, ready } from './config.js';
5
5
  import { encodeBundle, nativeStore } from './credentials.js';
6
+ import { getLocale, joinList, t } from './i18n.js';
6
7
  import { terminalPrompts } from './prompts.js';
7
8
  const yes = (text) => /^(y|yes)$/i.test(text.trim());
8
9
  const defaultYes = (text) => !text.trim() || yes(text);
@@ -13,7 +14,7 @@ export async function snapshot(file) {
13
14
  catch (error) {
14
15
  if (error.code === 'ENOENT')
15
16
  return null;
16
- throw new ConfigError('无法读取现有配置,这次没有做任何修改。');
17
+ throw new ConfigError(t('setup.readFailed'));
17
18
  }
18
19
  }
19
20
  function same(left, right) { return left === null ? right === null : right !== null && left.equals(right); }
@@ -25,7 +26,7 @@ export function readRaw(raw) {
25
26
  data = JSON.parse(decodeUtf8(raw));
26
27
  }
27
28
  catch {
28
- throw new ConfigError('现有配置无法解析,这次没有覆盖它。请先修复 JSON。');
29
+ throw new ConfigError(t('setup.parseFailed'));
29
30
  }
30
31
  parseConfig(data, {}, () => ({}));
31
32
  return data;
@@ -49,6 +50,9 @@ export async function commitConfig(file, data, expected, store, values, options
49
50
  target.credentials = { backend: 'keyring', id };
50
51
  parseConfig(target, {}, () => values);
51
52
  }
53
+ else if (options.keepCredentials) {
54
+ parseConfig(target, {}, () => ({}));
55
+ }
52
56
  else {
53
57
  delete target.credentials;
54
58
  parseConfig(target, {});
@@ -60,20 +64,20 @@ export async function commitConfig(file, data, expected, store, values, options
60
64
  lock = await open(lockPath, 'wx', 0o600);
61
65
  }
62
66
  catch {
63
- throw new ConfigError('配置写入锁不可用。请检查权限;如果确认没有向导在运行,可以删除残留的 .setup.lock。');
67
+ throw new ConfigError(t('setup.lockUnavailable'));
64
68
  }
65
69
  const temporary = path.join(path.dirname(file), `.config-${id}.tmp`);
66
70
  let created = false, committed = false, tempCreated = false;
67
71
  try {
68
72
  await lock.close();
69
73
  if (!same(await snapshot(file), expected))
70
- throw new ConfigError('配置在向导期间发生了变化,这次没有覆盖它。请重试。');
74
+ throw new ConfigError(t('setup.changedDuringWizard'));
71
75
  if (values !== null) {
72
76
  store.create(id, values);
73
77
  created = true;
74
78
  const check = store.read(id);
75
79
  if (check === null || JSON.stringify(check) !== JSON.stringify(values))
76
- throw new ConfigError('凭据写入后校验失败,配置没有切换。');
80
+ throw new ConfigError(t('setup.credentialVerifyFailed'));
77
81
  }
78
82
  const stream = await open(temporary, 'wx', 0o600);
79
83
  tempCreated = true;
@@ -85,7 +89,7 @@ export async function commitConfig(file, data, expected, store, values, options
85
89
  await stream.close();
86
90
  }
87
91
  if (!same(await snapshot(file), expected))
88
- throw new ConfigError('配置在保存期间发生了变化,这次没有覆盖它。请重试。');
92
+ throw new ConfigError(t('setup.changedDuringSave'));
89
93
  await (options.replace ?? rename)(temporary, file);
90
94
  committed = true;
91
95
  }
@@ -95,7 +99,7 @@ export async function commitConfig(file, data, expected, store, values, options
95
99
  await removeFile(temporary);
96
100
  }
97
101
  catch {
98
- warn(`临时配置没有清理掉:${path.basename(temporary)}`);
102
+ warn(t('setup.tempCleanupFailed', { name: path.basename(temporary) }));
99
103
  }
100
104
  }
101
105
  if (created && !committed) {
@@ -103,40 +107,42 @@ export async function commitConfig(file, data, expected, store, values, options
103
107
  store.remove(id);
104
108
  }
105
109
  catch {
106
- warn(`配置没有切换,新凭据也未能清理。引用 ID:${id}`);
110
+ warn(t('setup.credentialCleanupFailed', { id }));
107
111
  }
108
112
  }
109
113
  try {
110
114
  await removeFile(lockPath);
111
115
  }
112
116
  catch {
113
- warn('配置写入锁没有清理掉。请确认没有向导在运行,再删除 .setup.lock。');
117
+ warn(t('setup.lockCleanupFailed'));
114
118
  }
115
119
  }
116
- if (old?.backend === 'keyring') {
120
+ if (!options.keepCredentials && old?.backend === 'keyring') {
117
121
  try {
118
122
  store.remove(old.id);
119
123
  }
120
124
  catch {
121
- warn(`配置已保存,但旧凭据没有清理掉。引用 ID:${old.id}`);
125
+ warn(t('setup.oldCredentialCleanupFailed', { id: old.id }));
122
126
  }
123
127
  }
124
- else if (old?.backend === 'system') {
125
- warn('旧版原型的旧凭据仍然保留,由旧版工具继续管理,避免两个实现互相误删。');
128
+ else if (!options.keepCredentials && old?.backend === 'system') {
129
+ warn(t('setup.oldSystemRetained'));
126
130
  }
127
131
  }
128
132
  export async function wizard(file, options = {}) {
129
133
  const prompts = options.prompts ?? terminalPrompts();
130
134
  const before = await snapshot(file), data = readRaw(before);
131
135
  const { out } = prompts;
132
- out(`配置路径:${file}`);
133
- out('Token、webhook 和签名密钥都不会回显,将保存到 Windows Credential Manager / macOS Keychain;配置文件只保留引用。');
136
+ if (before === null)
137
+ data.language = getLocale();
138
+ out(t('setup.path', { file }));
139
+ out(t('setup.secretsNote'));
134
140
  const telegramEnabled = data.telegram?.enabled !== false;
135
141
  const feishuEnabled = data.feishu?.enabled !== false;
136
142
  const current = before === null ? ''
137
143
  : telegramEnabled && feishuEnabled ? '3'
138
144
  : telegramEnabled ? '1' : feishuEnabled ? '2' : '';
139
- const entered = await prompts.text(`选择通知渠道:1 Telegram,2 飞书,3 两者${current ? ` [${current}]` : ''}(输入其他内容取消):`);
145
+ const entered = await prompts.text(t('setup.channelPrompt', { current: current ? ` [${current}]` : '' }));
140
146
  const selection = entered || current;
141
147
  if (!['1', '2', '3'].includes(selection))
142
148
  return false;
@@ -151,13 +157,13 @@ export async function wizard(file, options = {}) {
151
157
  previous = existing;
152
158
  }
153
159
  catch {
154
- out('当前凭据暂时读不到,可能是条目缺失、凭据库锁定或权限不足。');
155
- if (!yes(await prompts.text('要重新输入所选渠道的凭据吗?[y/N] ')))
160
+ out(t('setup.credentialUnreadable'));
161
+ if (!yes(await prompts.text(t('setup.reenterPrompt'))))
156
162
  return false;
157
163
  }
158
164
  }
159
165
  else if (reference?.backend === 'system')
160
- out('检测到旧版原型保存的凭据。本次会重新输入并保存为 Node 版凭据,旧凭据保持不变。');
166
+ out(t('setup.oldSystemDetected'));
161
167
  data.telegram ??= {};
162
168
  data.feishu ??= {};
163
169
  data.telegram.enabled = selection !== '2';
@@ -165,12 +171,13 @@ export async function wizard(file, options = {}) {
165
171
  const values = {};
166
172
  const readValue = async (label, key, required) => {
167
173
  while (true) {
168
- const hint = previous[key] ? '留空保留当前值' : '不会从环境变量自动填充';
169
- const optional = required ? '' : '可选;';
170
- const answer = await prompts.secret(`${label}(隐藏输入,不回显;${optional}${hint}${required ? '' : ';输入 - 清除'}):`);
174
+ const prompt = required
175
+ ? previous[key] ? 'setup.secretRequiredKeep' : 'setup.secretRequiredNew'
176
+ : previous[key] ? 'setup.secretOptionalKeep' : 'setup.secretOptionalNew';
177
+ const answer = await prompts.secret(t(prompt, { label }));
171
178
  const value = !required && answer === '-' ? '' : answer || previous[key] || '';
172
179
  if (required && !value) {
173
- out('这一项不能为空,请重新输入;按 Ctrl+C 可退出。');
180
+ out(t('setup.requiredBlank'));
174
181
  continue;
175
182
  }
176
183
  try {
@@ -180,15 +187,15 @@ export async function wizard(file, options = {}) {
180
187
  return value;
181
188
  }
182
189
  catch {
183
- out('格式不正确,请重新输入(输入内容不会显示)。');
190
+ out(t('setup.invalidFormat'));
184
191
  }
185
192
  }
186
193
  };
187
194
  if (data.telegram.enabled) {
188
195
  values.telegram_token = await readValue('Telegram Bot Token', 'telegram_token', true);
189
- out('请先让接收人给 Bot 发一条消息,然后填写数字 chat_id;这里不会自动获取或配对接收人。');
196
+ out(t('setup.telegramChatInstruction'));
190
197
  while (true) {
191
- const entered = await prompts.text('接收人 chat_id,多个用逗号分隔(留空保留现有名单):');
198
+ const entered = await prompts.text(t('setup.telegramChatPrompt'));
192
199
  const ids = entered ? entered.split(',').map(item => item.trim()) : data.telegram.chat_ids ?? [];
193
200
  try {
194
201
  if (!ids.length)
@@ -198,13 +205,13 @@ export async function wizard(file, options = {}) {
198
205
  break;
199
206
  }
200
207
  catch {
201
- out('请填写至少一个有效的数字 chat_id;按 Ctrl+C 可退出。');
208
+ out(t('setup.telegramChatInvalid'));
202
209
  }
203
210
  }
204
211
  }
205
212
  if (data.feishu.enabled) {
206
- values.feishu_webhook = await readValue('飞书 webhook', 'feishu_webhook', true);
207
- values.feishu_secret = await readValue('飞书签名密钥', 'feishu_secret', false);
213
+ values.feishu_webhook = await readValue(t('setup.labelFeishuWebhook'), 'feishu_webhook', true);
214
+ values.feishu_secret = await readValue(t('setup.labelFeishuSecret'), 'feishu_secret', false);
208
215
  }
209
216
  encodeBundle(values);
210
217
  const preview = { ...data, credentials: { backend: 'keyring', id: '0'.repeat(32) } };
@@ -212,70 +219,81 @@ export async function wizard(file, options = {}) {
212
219
  const env = options.env ?? process.env;
213
220
  const channels = [];
214
221
  if (data.telegram.enabled)
215
- channels.push(`Telegram(${data.telegram.chat_ids.length} 个接收人)`);
222
+ channels.push(t('setup.channelTelegram', { count: data.telegram.chat_ids.length }));
216
223
  if (data.feishu.enabled)
217
- channels.push('飞书');
218
- out(`将启用:${channels.join('、')}`);
219
- out('环境变量的优先级更高,空值也会覆盖这里的设置;测试通知使用最终生效的配置。');
224
+ channels.push(t('setup.channelFeishu'));
225
+ out(t('setup.channelsEnabled', { channels: joinList(channels) }));
226
+ out(t('setup.envPriority'));
220
227
  const overrides = [data.telegram.token_env ?? 'TELEGRAM_BOT_TOKEN', data.feishu.webhook_env ?? 'FEISHU_WEBHOOK',
221
228
  data.feishu.secret_env ?? 'FEISHU_SECRET', 'TELEGRAM_CHAT_ID', 'TELEGRAM_CHAT_IDS'].filter(name => Object.hasOwn(env, name));
222
229
  if (overrides.length)
223
- out('以下环境变量正在覆盖配置:' + overrides.join('、'));
230
+ out(t('setup.envOverrides', { names: joinList(overrides) }));
224
231
  if (reference?.backend === 'keyring')
225
- out('保存成功后会删除旧凭据;如果其他配置也引用了同一个凭据,需要一并更新。');
226
- out('没有选中的渠道会被停用;新凭据只用于本次选中的渠道。');
232
+ out(t('setup.oldDeleteWarning'));
233
+ out(t('setup.disabledChannels'));
227
234
  for (const line of options.notes ?? [])
228
235
  out(line);
229
- const actions = ['保存配置和系统凭据'];
236
+ const actions = [t('setup.actionSave')];
230
237
  if (options.testSend)
231
- actions.push('发送固定内容的测试通知');
238
+ actions.push(t('setup.actionTest'));
232
239
  if (options.afterSave)
233
- actions.push('安装 Claude Code 通知 hooks');
234
- if (!defaultYes(await prompts.text(`将${actions.join('、')}。继续?[Y/n] `)))
240
+ actions.push(t('setup.actionHooks'));
241
+ if (!defaultYes(await prompts.text(t('setup.continue', { actions: joinList(actions) }))))
235
242
  return false;
236
243
  await commitConfig(file, data, before, store, values, { warn: out });
237
- out('配置和系统凭据已保存。');
244
+ out(t('setup.saved'));
238
245
  if (options.testSend) {
239
246
  try {
240
247
  const effective = parseConfig(preview, env, () => values);
241
248
  const ok = await options.testSend(effective);
242
- out(ok ? '测试通知发送成功。' : '配置已保存。测试通知没有发送成功,请运行 dev-notify doctor 检查。');
249
+ out(ok ? t('setup.testSuccess') : t('setup.testFailed'));
243
250
  }
244
251
  catch {
245
- out('配置已保存。测试通知没有发送成功,可能是环境变量覆盖或网络问题;请运行 dev-notify doctor 检查。');
252
+ out(t('setup.testFailedEnv'));
246
253
  }
247
254
  }
248
255
  else
249
- out('已跳过测试通知。');
256
+ out(t('setup.testSkipped'));
250
257
  if (options.afterSave) {
251
258
  try {
252
259
  await options.afterSave();
253
260
  }
254
261
  catch (error) {
255
- out(`配置已保存。Claude Code hooks 没有安装完成:${error instanceof ConfigError ? error.message : '请运行 integration claude-code status 检查'}`);
262
+ out(t('setup.hooksFailed', { reason: error instanceof ConfigError ? error.message : t('setup.hooksCheckHint') }));
256
263
  }
257
264
  }
258
265
  try {
259
266
  if (ready(parseConfig(preview, env, () => values), 'telegram')) {
260
- out('如需在 Telegram 中回复 Claude,请运行:dev-notify reply setup');
267
+ out(t('setup.replyHint'));
261
268
  }
262
269
  }
263
270
  catch { /* 环境覆盖无效时上方测试分支已报告。 */ }
264
271
  return true;
265
272
  }
273
+ export async function setConfigLanguage(file, language, warn) {
274
+ const before = await snapshot(file);
275
+ if (before === null)
276
+ throw new ConfigError(t('config.languageNoConfig', { file, language }));
277
+ const data = readRaw(before);
278
+ if ((data.language ?? 'zh') === language)
279
+ return false;
280
+ data.language = language;
281
+ await commitConfig(file, data, before, { read: () => null, create: () => { }, remove: () => { } }, null, { keepCredentials: true, warn });
282
+ return true;
283
+ }
266
284
  export async function clearCredentials(file, prompts, store) {
267
285
  const before = await snapshot(file), data = readRaw(before);
268
286
  const reference = data.credentials;
269
287
  if (!reference) {
270
- prompts.out('当前配置没有系统凭据引用,无需清理。');
288
+ prompts.out(t('setup.clearNoReference'));
271
289
  return true;
272
290
  }
273
291
  if (reference.backend !== 'keyring')
274
- throw new ConfigError('这是旧版原型保存的凭据;Node 版不会跨实现删除,请用原工具处理。');
275
- prompts.out('将删除当前配置引用的系统凭据,并从配置中移除引用。环境变量不受影响;如果其他配置共享同一个凭据,它们也会失效。');
276
- if (!yes(await prompts.text('确认清除吗?[y/N] ')))
292
+ throw new ConfigError(t('setup.clearOldBackend'));
293
+ prompts.out(t('setup.clearWarning'));
294
+ if (!yes(await prompts.text(t('setup.clearConfirm'))))
277
295
  return false;
278
296
  await commitConfig(file, data, before, store ?? nativeStore(), null, { warn: prompts.out });
279
- prompts.out('凭据引用已移除。如果系统条目删除失败,上方会显示提示。');
297
+ prompts.out(t('setup.cleared'));
280
298
  return true;
281
299
  }
package/dist/update.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  import { ConfigError } from './config.js';
3
+ import { t } from './i18n.js';
3
4
  import { terminalPrompts } from './prompts.js';
4
5
  const NAME = '@quecue/dev-notify';
5
6
  const VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(alpha|beta|rc)\.(0|[1-9]\d*))?$/;
@@ -12,7 +13,7 @@ function npmRunner(args, capture) {
12
13
  shell: process.platform === 'win32',
13
14
  });
14
15
  if (result.error)
15
- throw new ConfigError('无法运行 npm。请确认已安装 npm,并且它在 PATH 中。');
16
+ throw new ConfigError(t('update.npmUnavailable'));
16
17
  return { status: result.status, stdout: result.stdout ?? '', stderr: result.stderr ?? '' };
17
18
  }
18
19
  async function fetchLatest() {
@@ -25,25 +26,25 @@ async function fetchLatest() {
25
26
  });
26
27
  }
27
28
  catch {
28
- throw new ConfigError('无法检查最新版本。请检查网络后重试。');
29
+ throw new ConfigError(t('update.checkNetwork'));
29
30
  }
30
31
  if (!response.ok)
31
- throw new ConfigError(`无法检查最新版本(HTTP ${response.status})。请检查网络后重试。`);
32
+ throw new ConfigError(t('update.checkHttp', { status: response.status }));
32
33
  let version;
33
34
  try {
34
35
  version = (await response.json())?.version;
35
36
  }
36
37
  catch {
37
- throw new ConfigError('npm registry 返回的版本信息无法解析。');
38
+ throw new ConfigError(t('update.parseRegistry'));
38
39
  }
39
40
  if (typeof version !== 'string' || !VERSION.test(version))
40
- throw new ConfigError('npm registry 返回的版本号格式不受支持。');
41
+ throw new ConfigError(t('update.registryVersion'));
41
42
  return version;
42
43
  }
43
44
  function parts(version) {
44
45
  const match = VERSION.exec(version);
45
46
  if (!match)
46
- throw new ConfigError('版本号格式不受支持。');
47
+ throw new ConfigError(t('update.versionFormat'));
47
48
  const rank = match[4] === 'alpha' ? 1 : match[4] === 'beta' ? 2 : match[4] === 'rc' ? 3 : 4;
48
49
  return [Number(match[1]), Number(match[2]), Number(match[3]), rank, Number(match[5] ?? 0)];
49
50
  }
@@ -58,54 +59,54 @@ async function confirmUpdate(latest, options) {
58
59
  if (options.yes)
59
60
  return true;
60
61
  if (!options.prompts && (!process.stdin.isTTY || !process.stdout.isTTY)) {
61
- throw new ConfigError('更新需要在交互终端中确认;非交互场景请使用 dev-notify update --yes。');
62
+ throw new ConfigError(t('update.terminalRequired'));
62
63
  }
63
64
  const prompts = options.prompts ?? terminalPrompts();
64
- return /^(y|yes)$/i.test(await prompts.text(`更新到 ${latest}?[y/N] `));
65
+ return /^(y|yes)$/i.test(await prompts.text(t('update.confirm', { latest })));
65
66
  }
66
67
  async function runMaintenance(options, updated) {
67
68
  if (!options.post) {
68
69
  if (updated)
69
- options.out('如有 Claude Code hooks,请运行 dev-notify integration claude-code install;如使用远程回复,请重启桥接。');
70
+ options.out(t('update.manualPost'));
70
71
  return 0;
71
72
  }
72
73
  const code = await options.post();
73
74
  if (code !== 0) {
74
- options.out(updated ? '更新已完成,但后续维护步骤没有全部成功;请按上面的提示检查。' : '维护步骤没有全部成功;请按上面的提示检查。');
75
+ options.out(t(updated ? 'update.partialUpdated' : 'update.partial'));
75
76
  return code;
76
77
  }
77
- options.out(updated ? '更新后的维护步骤已完成。' : '维护步骤已完成。');
78
+ options.out(t(updated ? 'update.postDoneUpdated' : 'update.postDone'));
78
79
  return 0;
79
80
  }
80
81
  export async function updatePackage(current, options) {
81
82
  if (!VERSION.test(current))
82
- throw new ConfigError('当前版本号格式不受支持,无法检查更新。');
83
+ throw new ConfigError(t('update.currentFormat'));
83
84
  const latest = await (options.latest ?? fetchLatest)();
84
85
  if (!VERSION.test(latest))
85
- throw new ConfigError('npm registry 返回的版本号格式不受支持。');
86
+ throw new ConfigError(t('update.registryVersion'));
86
87
  if (compareVersions(latest, current) <= 0) {
87
- options.out(`已是最新版本 ${current}。`);
88
+ options.out(t('update.upToDate', { version: current }));
88
89
  if (options.check)
89
90
  return 0;
90
91
  if (options.post)
91
- options.out('将执行 Claude Code hooks 和回复桥接维护。');
92
+ options.out(t('update.maintenanceNote'));
92
93
  return await runMaintenance(options, false);
93
94
  }
94
- options.out(`当前版本 ${current},npm 最新版本 ${latest}。`);
95
+ options.out(t('update.versions', { current, latest }));
95
96
  if (options.check) {
96
- options.out('运行 dev-notify update 更新。');
97
+ options.out(t('update.checkHint'));
97
98
  return 0;
98
99
  }
99
100
  if (options.post)
100
- options.out('更新完成后会自动重新安装绑定当前配置的 Claude Code hooks,并重启正在运行的回复桥接;可用 --no-post 跳过。');
101
+ options.out(t('update.postNote'));
101
102
  if (!await confirmUpdate(latest, options)) {
102
- options.out('已取消,没有更新。');
103
+ options.out(t('update.cancelled'));
103
104
  return 130;
104
105
  }
105
- options.out(`运行 npm install -g ${NAME}@${latest} ...`);
106
+ options.out(t('update.running', { name: NAME, latest }));
106
107
  const result = (options.runner ?? npmRunner)(['install', '-g', `${NAME}@${latest}`], false);
107
108
  if (result.status !== 0)
108
- throw new ConfigError(`更新没有完成。请检查 npm 权限,或手动运行 npm install -g ${NAME}@latest。`);
109
- options.out(`已更新到 ${latest}。请重新打开终端后运行 dev-notify --version 确认。`);
109
+ throw new ConfigError(t('update.failed', { name: NAME }));
110
+ options.out(t('update.updated', { latest }));
110
111
  return await runMaintenance(options, true);
111
112
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quecue/dev-notify",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "跨平台开发任务通知工具,支持 Telegram 与飞书",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -21,8 +21,11 @@
21
21
  "files": [
22
22
  "dist/*.js",
23
23
  "README.md",
24
+ "README.en.md",
24
25
  "SECURITY.md",
26
+ "SECURITY.en.md",
25
27
  "CHANGELOG.md",
28
+ "CHANGELOG.en.md",
26
29
  "LICENSE"
27
30
  ],
28
31
  "scripts": {