@afffun/codexbot 1.0.74 → 1.0.75

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afffun/codexbot",
3
- "version": "1.0.74",
3
+ "version": "1.0.75",
4
4
  "description": "Thin npm bootstrap CLI for installing and operating Codexbot nodes",
5
5
  "type": "module",
6
6
  "author": "john88188 <john88188@outlook.com>",
@@ -3,36 +3,11 @@ import fs from 'node:fs/promises';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { spawn } from 'node:child_process';
6
- import readline from 'node:readline/promises';
7
6
 
8
7
  import { buildInstallLicenseClaimUrl, getDefaultDistributionConfig } from './config_service.mjs';
8
+ import { createPrompter, promptUntilValid, trim } from './input_contract_service.mjs';
9
9
  import { resolveInstalledControlLayout } from './installed_layout_service.mjs';
10
10
 
11
- function trim(value, fallback = '') {
12
- const text = String(value ?? '').trim();
13
- return text || String(fallback ?? '').trim();
14
- }
15
-
16
- function createPrompter({ stdin, stdout } = {}) {
17
- const input = stdin || process.stdin;
18
- const output = stdout || process.stdout;
19
- if (!input?.isTTY || !output?.isTTY) {
20
- return {
21
- interactive: false,
22
- ask: async () => '',
23
- close: async () => {},
24
- };
25
- }
26
- const rl = readline.createInterface({ input, output });
27
- return {
28
- interactive: true,
29
- ask: async (prompt) => trim(await rl.question(prompt)),
30
- close: async () => {
31
- rl.close();
32
- },
33
- };
34
- }
35
-
36
11
  function runChildCapture(spawnFn, command, args, options = {}) {
37
12
  return new Promise((resolve, reject) => {
38
13
  const child = spawnFn(command, args, {
@@ -128,9 +103,13 @@ export function createNpmDistributionActivationService(deps = {}) {
128
103
  if (Boolean(options.nonInteractive) || !prompter.interactive) {
129
104
  throw new Error('license key is required');
130
105
  }
131
- const entered = trim(await prompter.ask('License key: '));
132
- if (!entered) throw new Error('license key is required');
133
- return entered;
106
+ return promptUntilValid(prompter, {
107
+ logger,
108
+ prompt: 'License key: ',
109
+ normalize: trim,
110
+ validate: (value) => Boolean(value),
111
+ invalidMessage: 'License key is required.',
112
+ });
134
113
  } finally {
135
114
  await prompter.close().catch(() => {});
136
115
  }
@@ -1,6 +1,4 @@
1
- function trim(value) {
2
- return String(value ?? '').trim();
3
- }
1
+ import { normalizeConnectorChoice, trim } from './input_contract_service.mjs';
4
2
 
5
3
  function readOptionValue(argv, index, name) {
6
4
  if (index + 1 >= argv.length) {
@@ -94,7 +92,10 @@ function parseBootstrapOptions(argv = []) {
94
92
  continue;
95
93
  }
96
94
  if (token === '--connector') {
97
- options.connector = readOptionValue(argv, i, token);
95
+ options.connector = normalizeConnectorChoice(readOptionValue(argv, i, token));
96
+ if (!options.connector) {
97
+ throw new Error('invalid value for --connector (expected telegram, wecom, or skip)');
98
+ }
98
99
  i += 1;
99
100
  continue;
100
101
  }
@@ -0,0 +1,68 @@
1
+ import readline from 'node:readline/promises';
2
+
3
+ export function trim(value, fallback = '') {
4
+ const text = String(value ?? '').trim();
5
+ return text || String(fallback ?? '').trim();
6
+ }
7
+
8
+ export function normalizeConnectorChoice(value) {
9
+ const raw = trim(value).toLowerCase();
10
+ if (raw === '1') return 'telegram';
11
+ if (raw === '2') return 'wecom';
12
+ if (raw === '3') return 'skip';
13
+ if (raw === 'telegram') return 'telegram';
14
+ if (raw === 'wecom') return 'wecom';
15
+ if (raw === 'skip' || raw === 'none') return 'skip';
16
+ return '';
17
+ }
18
+
19
+ export function normalizeList(input) {
20
+ return Array.from(new Set(
21
+ String(input || '')
22
+ .split(/[,\n]/)
23
+ .map((item) => trim(item))
24
+ .filter(Boolean),
25
+ ));
26
+ }
27
+
28
+ export function normalizeYesNo(input, fallback = false) {
29
+ const raw = trim(input).toLowerCase();
30
+ if (!raw) return Boolean(fallback);
31
+ if (['y', 'yes', '1', 'true'].includes(raw)) return true;
32
+ if (['n', 'no', '0', 'false'].includes(raw)) return false;
33
+ return Boolean(fallback);
34
+ }
35
+
36
+ export function createPrompter({ stdin, stdout } = {}) {
37
+ const input = stdin || process.stdin;
38
+ const output = stdout || process.stdout;
39
+ if (!input?.isTTY || !output?.isTTY) {
40
+ return {
41
+ interactive: false,
42
+ ask: async () => '',
43
+ close: async () => {},
44
+ };
45
+ }
46
+ const rl = readline.createInterface({ input, output });
47
+ return {
48
+ interactive: true,
49
+ ask: async (prompt) => trim(await rl.question(prompt)),
50
+ close: async () => {
51
+ rl.close();
52
+ },
53
+ };
54
+ }
55
+
56
+ export async function promptUntilValid(prompter, {
57
+ logger = console,
58
+ prompt,
59
+ normalize = trim,
60
+ validate = (value) => Boolean(value),
61
+ invalidMessage = 'Invalid input. Please try again.',
62
+ } = {}) {
63
+ while (true) {
64
+ const value = normalize(await prompter.ask(prompt));
65
+ if (validate(value)) return value;
66
+ logger?.log?.(invalidMessage);
67
+ }
68
+ }
@@ -1,36 +1,23 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
- import readline from 'node:readline/promises';
4
+ import {
5
+ createPrompter,
6
+ normalizeConnectorChoice,
7
+ normalizeList,
8
+ normalizeYesNo,
9
+ promptUntilValid,
10
+ trim,
11
+ } from './input_contract_service.mjs';
5
12
 
6
- function trim(value, fallback = '') {
7
- const text = String(value ?? '').trim();
8
- return text || String(fallback ?? '').trim();
9
- }
10
-
11
- function normalizeConnectorChoice(value) {
12
- const raw = trim(value).toLowerCase();
13
- if (raw === 'telegram') return 'telegram';
14
- if (raw === 'wecom') return 'wecom';
15
- if (raw === 'skip' || raw === 'none') return 'skip';
16
- return '';
17
- }
18
-
19
- function normalizeList(input) {
20
- return Array.from(new Set(
21
- String(input || '')
22
- .split(/[,\n]/)
23
- .map((item) => trim(item))
24
- .filter(Boolean),
25
- ));
26
- }
27
-
28
- function normalizeYesNo(input, fallback = false) {
29
- const raw = trim(input).toLowerCase();
30
- if (!raw) return Boolean(fallback);
31
- if (['y', 'yes', '1', 'true'].includes(raw)) return true;
32
- if (['n', 'no', '0', 'false'].includes(raw)) return false;
33
- return Boolean(fallback);
13
+ async function promptForConnectorChoice(prompter, logger) {
14
+ return promptUntilValid(prompter, {
15
+ logger,
16
+ prompt: 'Choose connector [1=telegram, 2=wecom, 3=skip] (default: 3): ',
17
+ normalize: normalizeConnectorChoice,
18
+ validate: (value) => Boolean(value),
19
+ invalidMessage: 'Invalid connector selection. Enter 1, 2, 3, telegram, wecom, or skip.',
20
+ });
34
21
  }
35
22
 
36
23
  function buildConnectorCapabilities(type) {
@@ -211,26 +198,6 @@ function buildWecomSeed(nowIso, {
211
198
  };
212
199
  }
213
200
 
214
- function createPrompter({ stdin, stdout } = {}) {
215
- const input = stdin || process.stdin;
216
- const output = stdout || process.stdout;
217
- if (!input?.isTTY || !output?.isTTY) {
218
- return {
219
- interactive: false,
220
- ask: async () => '',
221
- close: async () => {},
222
- };
223
- }
224
- const rl = readline.createInterface({ input, output });
225
- return {
226
- interactive: true,
227
- ask: async (prompt) => trim(await rl.question(prompt)),
228
- close: async () => {
229
- rl.close();
230
- },
231
- };
232
- }
233
-
234
201
  async function ensureReadableFile(fsPromises, filePath, label) {
235
202
  const target = trim(filePath);
236
203
  if (!target) return '';
@@ -325,7 +292,7 @@ export function createNpmDistributionInstallSeedService(deps = {}) {
325
292
  }
326
293
  if (!connector && !nonInteractive) {
327
294
  logger.log('==> Remote control channel setup');
328
- connector = normalizeConnectorChoice(await prompter.ask('Choose connector [telegram/wecom/skip] (default: skip): ')) || 'skip';
295
+ connector = await promptForConnectorChoice(prompter, logger);
329
296
  }
330
297
  if (!connector) connector = 'skip';
331
298
 
@@ -338,8 +305,28 @@ export function createNpmDistributionInstallSeedService(deps = {}) {
338
305
  }
339
306
 
340
307
  if (connector === 'telegram') {
341
- const telegramBotToken = trim(options.telegramBotToken || (!nonInteractive ? await prompter.ask('Telegram bot token: ') : ''));
342
- const telegramChatIds = normalizeList(options.telegramChatIds || (!nonInteractive ? await prompter.ask('Telegram allowed chat IDs (comma-separated): ') : ''));
308
+ const telegramBotToken = trim(options.telegramBotToken || (
309
+ !nonInteractive
310
+ ? await promptUntilValid(prompter, {
311
+ logger,
312
+ prompt: 'Telegram bot token: ',
313
+ normalize: trim,
314
+ validate: (value) => Boolean(value),
315
+ invalidMessage: 'Telegram bot token is required.',
316
+ })
317
+ : ''
318
+ ));
319
+ const telegramChatIds = normalizeList(options.telegramChatIds || (
320
+ !nonInteractive
321
+ ? await promptUntilValid(prompter, {
322
+ logger,
323
+ prompt: 'Telegram allowed chat IDs (comma-separated): ',
324
+ normalize: normalizeList,
325
+ validate: (value) => Array.isArray(value) && value.length > 0,
326
+ invalidMessage: 'Provide at least one Telegram chat ID.',
327
+ })
328
+ : ''
329
+ ));
343
330
  if (!telegramBotToken || telegramChatIds.length === 0) {
344
331
  throw new Error('Telegram connector requires bot token and at least one allowed chat ID');
345
332
  }
@@ -356,8 +343,28 @@ export function createNpmDistributionInstallSeedService(deps = {}) {
356
343
  result.summary.connector = 'telegram';
357
344
  result.summary.connectorSeeded = true;
358
345
  } else if (connector === 'wecom') {
359
- const wecomBotId = trim(options.wecomBotId || (!nonInteractive ? await prompter.ask('WeCom bot ID: ') : ''));
360
- const wecomSecret = trim(options.wecomSecret || (!nonInteractive ? await prompter.ask('WeCom bot secret: ') : ''));
346
+ const wecomBotId = trim(options.wecomBotId || (
347
+ !nonInteractive
348
+ ? await promptUntilValid(prompter, {
349
+ logger,
350
+ prompt: 'WeCom bot ID: ',
351
+ normalize: trim,
352
+ validate: (value) => Boolean(value),
353
+ invalidMessage: 'WeCom bot ID is required.',
354
+ })
355
+ : ''
356
+ ));
357
+ const wecomSecret = trim(options.wecomSecret || (
358
+ !nonInteractive
359
+ ? await promptUntilValid(prompter, {
360
+ logger,
361
+ prompt: 'WeCom bot secret: ',
362
+ normalize: trim,
363
+ validate: (value) => Boolean(value),
364
+ invalidMessage: 'WeCom bot secret is required.',
365
+ })
366
+ : ''
367
+ ));
361
368
  const wecomTencentDocsApiKey = trim(options.wecomTencentDocsApiKey || (!nonInteractive ? await prompter.ask('Tencent Docs API key (optional, blank to skip): ') : ''));
362
369
  if (!wecomBotId || !wecomSecret) {
363
370
  throw new Error('WeCom connector requires bot ID and secret');