@afffun/codexbot 1.0.71 → 1.0.72

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/bin/codexbot.mjs CHANGED
@@ -1,6 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { runCodexbotCli } from '../src/entrypoint.mjs';
3
+ import { formatCliError, runCodexbotCli } from '../src/entrypoint.mjs';
4
4
 
5
- const exitCode = await runCodexbotCli(process.argv.slice(2));
6
- process.exit(exitCode);
5
+ try {
6
+ const exitCode = await runCodexbotCli(process.argv.slice(2));
7
+ process.exit(exitCode);
8
+ } catch (err) {
9
+ process.stderr.write(`${formatCliError(err)}\n`);
10
+ process.exit(1);
11
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afffun/codexbot",
3
- "version": "1.0.71",
3
+ "version": "1.0.72",
4
4
  "description": "Thin npm bootstrap CLI for installing and operating Codexbot nodes",
5
5
  "type": "module",
6
6
  "author": "john88188 <john88188@outlook.com>",
@@ -12,6 +12,7 @@ function readOptionValue(argv, index, name) {
12
12
  function parseBootstrapOptions(argv = []) {
13
13
  const options = {
14
14
  channel: '',
15
+ installBaseUrl: '',
15
16
  updateBaseUrl: '',
16
17
  manifestUrl: '',
17
18
  publicKeyUrl: '',
@@ -20,6 +21,21 @@ function parseBootstrapOptions(argv = []) {
20
21
  token: '',
21
22
  allowHttp: false,
22
23
  noSignature: false,
24
+ nonInteractive: false,
25
+ connector: '',
26
+ telegramBotToken: '',
27
+ telegramChatIds: '',
28
+ wecomBotId: '',
29
+ wecomSecret: '',
30
+ wecomTencentDocsApiKey: '',
31
+ openAiApiKey: '',
32
+ deferLicenseActivation: false,
33
+ licenseFile: '',
34
+ licensePublicKeyFile: '',
35
+ licenseServerPublicKeyFile: '',
36
+ seedEnvFile: '',
37
+ seedConnectorsStateFile: '',
38
+ seedConnectorsCredentialsFile: '',
23
39
  forwardedArgs: [],
24
40
  };
25
41
 
@@ -31,6 +47,11 @@ function parseBootstrapOptions(argv = []) {
31
47
  i += 1;
32
48
  continue;
33
49
  }
50
+ if (token === '--install-base-url') {
51
+ options.installBaseUrl = readOptionValue(argv, i, token);
52
+ i += 1;
53
+ continue;
54
+ }
34
55
  if (token === '--update-base-url') {
35
56
  options.updateBaseUrl = readOptionValue(argv, i, token);
36
57
  i += 1;
@@ -66,6 +87,79 @@ function parseBootstrapOptions(argv = []) {
66
87
  options.forwardedArgs.push(token);
67
88
  continue;
68
89
  }
90
+ if (token === '--non-interactive' || token === '--no-prompt') {
91
+ options.nonInteractive = true;
92
+ continue;
93
+ }
94
+ if (token === '--connector') {
95
+ options.connector = readOptionValue(argv, i, token);
96
+ i += 1;
97
+ continue;
98
+ }
99
+ if (token === '--telegram-bot-token') {
100
+ options.telegramBotToken = readOptionValue(argv, i, token);
101
+ i += 1;
102
+ continue;
103
+ }
104
+ if (token === '--telegram-chat-ids') {
105
+ options.telegramChatIds = readOptionValue(argv, i, token);
106
+ i += 1;
107
+ continue;
108
+ }
109
+ if (token === '--wecom-bot-id') {
110
+ options.wecomBotId = readOptionValue(argv, i, token);
111
+ i += 1;
112
+ continue;
113
+ }
114
+ if (token === '--wecom-secret') {
115
+ options.wecomSecret = readOptionValue(argv, i, token);
116
+ i += 1;
117
+ continue;
118
+ }
119
+ if (token === '--wecom-tencent-docs-api-key') {
120
+ options.wecomTencentDocsApiKey = readOptionValue(argv, i, token);
121
+ i += 1;
122
+ continue;
123
+ }
124
+ if (token === '--openai-api-key') {
125
+ options.openAiApiKey = readOptionValue(argv, i, token);
126
+ i += 1;
127
+ continue;
128
+ }
129
+ if (token === '--defer-license-activation') {
130
+ options.deferLicenseActivation = true;
131
+ continue;
132
+ }
133
+ if (token === '--license-file') {
134
+ options.licenseFile = readOptionValue(argv, i, token);
135
+ i += 1;
136
+ continue;
137
+ }
138
+ if (token === '--license-public-key-file') {
139
+ options.licensePublicKeyFile = readOptionValue(argv, i, token);
140
+ i += 1;
141
+ continue;
142
+ }
143
+ if (token === '--license-server-public-key-file') {
144
+ options.licenseServerPublicKeyFile = readOptionValue(argv, i, token);
145
+ i += 1;
146
+ continue;
147
+ }
148
+ if (token === '--seed-env-file') {
149
+ options.seedEnvFile = readOptionValue(argv, i, token);
150
+ i += 1;
151
+ continue;
152
+ }
153
+ if (token === '--seed-connectors-state-file') {
154
+ options.seedConnectorsStateFile = readOptionValue(argv, i, token);
155
+ i += 1;
156
+ continue;
157
+ }
158
+ if (token === '--seed-connectors-credentials-file') {
159
+ options.seedConnectorsCredentialsFile = readOptionValue(argv, i, token);
160
+ i += 1;
161
+ continue;
162
+ }
69
163
  if (token === '--no-signature') {
70
164
  options.noSignature = true;
71
165
  options.forwardedArgs.push(token);
@@ -1,3 +1,4 @@
1
+ const DEFAULT_INSTALL_BASE_URL = 'https://codexbotinstall.afffun.com';
1
2
  const DEFAULT_UPDATE_BASE_URL = 'https://codexbotupdate.afffun.com';
2
3
  const DEFAULT_CHANNEL = 'stable';
3
4
 
@@ -28,12 +29,51 @@ function assertHttps(value, { allowHttp = false, label = 'url' } = {}) {
28
29
 
29
30
  export function getDefaultDistributionConfig(env = process.env) {
30
31
  return {
32
+ installBaseUrl: normalizeBaseUrl(env.CODEXBOT_INSTALL_BASE_URL, DEFAULT_INSTALL_BASE_URL),
31
33
  updateBaseUrl: normalizeBaseUrl(env.CODEXBOT_UPDATE_BASE_URL, DEFAULT_UPDATE_BASE_URL),
32
34
  channel: normalizeChannel(env.CODEXBOT_INSTALL_CHANNEL, DEFAULT_CHANNEL),
33
35
  authToken: trim(env.CODEXBOT_UPDATE_TOKEN || env.CODEX_REMOTE_UPDATE_AUTH_TOKEN, ''),
34
36
  };
35
37
  }
36
38
 
39
+ export function buildPublicInstallUrls({
40
+ installBaseUrl = DEFAULT_INSTALL_BASE_URL,
41
+ updateBaseUrl = DEFAULT_UPDATE_BASE_URL,
42
+ channel = DEFAULT_CHANNEL,
43
+ manifestUrl = '',
44
+ publicKeyUrl = '',
45
+ installRemoteUrl = '',
46
+ runtimeManifestUrl = '',
47
+ packageDownloadBaseUrl = '',
48
+ allowHttp = false,
49
+ } = {}) {
50
+ const resolvedInstallBaseUrl = normalizeBaseUrl(installBaseUrl, DEFAULT_INSTALL_BASE_URL);
51
+ const resolvedUpdateBaseUrl = normalizeBaseUrl(updateBaseUrl, DEFAULT_UPDATE_BASE_URL);
52
+ const resolvedChannel = normalizeChannel(channel, DEFAULT_CHANNEL);
53
+ const publicRoot = `${resolvedInstallBaseUrl}/v1/install/${resolvedChannel}`;
54
+ const updateRoot = `${resolvedUpdateBaseUrl}/v1/channels/${resolvedChannel}`;
55
+ const urls = {
56
+ installBaseUrl: resolvedInstallBaseUrl,
57
+ updateBaseUrl: resolvedUpdateBaseUrl,
58
+ channel: resolvedChannel,
59
+ manifestUrl: trim(manifestUrl) || `${publicRoot}/manifest.json`,
60
+ publicKeyUrl: trim(publicKeyUrl) || `${publicRoot}/packages/latest/update-public.pem`,
61
+ installRemoteUrl: trim(installRemoteUrl) || `${publicRoot}/packages/latest/install-remote.sh`,
62
+ runtimeManifestUrl: trim(runtimeManifestUrl) || `${updateRoot}/manifest.json`,
63
+ packageDownloadBaseUrl: trim(packageDownloadBaseUrl) || `${publicRoot}/packages`,
64
+ };
65
+ return {
66
+ installBaseUrl: assertHttps(urls.installBaseUrl, { allowHttp, label: 'install base url' }),
67
+ updateBaseUrl: assertHttps(urls.updateBaseUrl, { allowHttp, label: 'update base url' }),
68
+ channel: urls.channel,
69
+ manifestUrl: assertHttps(urls.manifestUrl, { allowHttp, label: 'manifest url' }),
70
+ publicKeyUrl: assertHttps(urls.publicKeyUrl, { allowHttp, label: 'public key url' }),
71
+ installRemoteUrl: assertHttps(urls.installRemoteUrl, { allowHttp, label: 'install-remote url' }),
72
+ runtimeManifestUrl: assertHttps(urls.runtimeManifestUrl, { allowHttp, label: 'runtime manifest url' }),
73
+ packageDownloadBaseUrl: assertHttps(urls.packageDownloadBaseUrl, { allowHttp, label: 'package download base url' }),
74
+ };
75
+ }
76
+
37
77
  export function buildRemoteInstallUrls({
38
78
  updateBaseUrl = DEFAULT_UPDATE_BASE_URL,
39
79
  channel = DEFAULT_CHANNEL,
@@ -22,12 +22,27 @@ function renderUsage() {
22
22
  '',
23
23
  'Bootstrap options:',
24
24
  ' --channel <stable|canary|...>',
25
+ ' --install-base-url <https://codexbotinstall.example.com>',
25
26
  ' --update-base-url <https://codexbotupdate.example.com>',
27
+ ' install defaults to an interactive seed wizard unless --non-interactive is used.',
28
+ ' --connector <telegram|wecom|skip>',
29
+ ' --telegram-bot-token <token>',
30
+ ' --telegram-chat-ids <id1,id2,...>',
31
+ ' --wecom-bot-id <botId>',
32
+ ' --wecom-secret <secret>',
33
+ ' --wecom-tencent-docs-api-key <apiKey>',
34
+ ' --openai-api-key <key>',
35
+ ' --license-file </path/to/license.json>',
36
+ ' --license-public-key-file </path/to/license-public.pem>',
37
+ ' --license-server-public-key-file </path/to/license-server-public.pem>',
38
+ ' --defer-license-activation',
39
+ ' --non-interactive',
40
+ ' advanced seed flags such as --seed-env-file remain supported for automation',
26
41
  ' --manifest-url <url>',
27
42
  ' --public-key-url <url>',
28
43
  ' --install-remote-url <url>',
29
44
  ' --public-key-file </path/to/update-public.pem>',
30
- ' --token <update server token>',
45
+ ' --token <protected update server token>',
31
46
  ' --allow-http',
32
47
  '',
33
48
  'Layout overrides:',
@@ -76,3 +91,9 @@ export async function runCodexbotCli(argv = [], deps = {}) {
76
91
  }
77
92
  throw new Error(`unsupported action: ${cli.action}`);
78
93
  }
94
+
95
+ export function formatCliError(err) {
96
+ const message = String(err?.message || err || '').trim();
97
+ if (!message) return 'codexbot failed';
98
+ return message;
99
+ }
@@ -0,0 +1,411 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import readline from 'node:readline/promises';
5
+
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);
34
+ }
35
+
36
+ function buildConnectorCapabilities(type) {
37
+ if (type === 'telegram') {
38
+ return {
39
+ supportsEdit: true,
40
+ supportsButtons: true,
41
+ supportsAudioIn: true,
42
+ supportsAudioOut: true,
43
+ supportsFileUpload: true,
44
+ supportsStreaming: true,
45
+ supportsWebSocket: false,
46
+ supportsWebhook: true,
47
+ supportsPolling: true,
48
+ supportsOAuth: false,
49
+ };
50
+ }
51
+ if (type === 'wecom') {
52
+ return {
53
+ supportsEdit: false,
54
+ supportsButtons: false,
55
+ supportsAudioIn: true,
56
+ supportsAudioOut: false,
57
+ supportsFileUpload: true,
58
+ supportsStreaming: true,
59
+ supportsWebSocket: true,
60
+ supportsWebhook: false,
61
+ supportsPolling: false,
62
+ supportsOAuth: false,
63
+ };
64
+ }
65
+ return {};
66
+ }
67
+
68
+ function buildTelegramSeed(nowIso, {
69
+ botToken,
70
+ allowedChatIds,
71
+ baseUrl = 'https://api.telegram.org',
72
+ fileBaseUrl = 'https://api.telegram.org/file',
73
+ } = {}) {
74
+ const connectorId = 'telegram-primary';
75
+ const configured = Boolean(trim(botToken) && allowedChatIds.length > 0);
76
+ const updatedAt = nowIso();
77
+ return {
78
+ connectorId,
79
+ descriptor: {
80
+ connectorId,
81
+ type: 'telegram',
82
+ name: connectorId,
83
+ displayName: 'Telegram',
84
+ source: 'managed',
85
+ managed: true,
86
+ status: configured ? 'authorized' : 'draft',
87
+ mode: 'polling',
88
+ capabilities: buildConnectorCapabilities('telegram'),
89
+ config: {
90
+ baseUrl,
91
+ fileBaseUrl,
92
+ },
93
+ binding: {
94
+ allowedChats: allowedChatIds,
95
+ },
96
+ runtime: {
97
+ desiredEnabled: true,
98
+ configured,
99
+ authModes: ['bot_token'],
100
+ },
101
+ health: {
102
+ state: configured ? 'idle' : 'misconfigured',
103
+ lastError: configured ? '' : 'missing telegram bot token or allowed chats',
104
+ },
105
+ auth: {
106
+ supportedModes: ['bot_token'],
107
+ mode: configured ? 'bot_token' : '',
108
+ state: configured ? 'configured' : 'unconfigured',
109
+ configured,
110
+ credentialRef: `cred_${connectorId}`,
111
+ storedKeys: configured ? ['botToken'] : [],
112
+ redirectUri: '',
113
+ authorizeUrl: '',
114
+ scopes: [],
115
+ tokenExchangeConfigured: false,
116
+ hasAccessToken: false,
117
+ hasRefreshToken: false,
118
+ lastAuthorizedAt: configured ? updatedAt : '',
119
+ lastError: configured ? '' : 'missing telegram bot token or allowed chats',
120
+ lastErrorAt: configured ? '' : updatedAt,
121
+ },
122
+ createdAt: updatedAt,
123
+ updatedAt,
124
+ },
125
+ credentials: {
126
+ connectorId,
127
+ credentialRef: `cred_${connectorId}`,
128
+ authMode: 'bot_token',
129
+ values: configured ? { botToken: trim(botToken) } : {},
130
+ oauth: {},
131
+ updatedAt,
132
+ },
133
+ };
134
+ }
135
+
136
+ function buildWecomSeed(nowIso, {
137
+ botId,
138
+ secret,
139
+ tencentDocsApiKey = '',
140
+ websocketUrl = 'wss://openws.work.weixin.qq.com',
141
+ } = {}) {
142
+ const connectorId = 'wecom-primary';
143
+ const configured = Boolean(trim(botId) && trim(secret));
144
+ const updatedAt = nowIso();
145
+ const storedKeys = [];
146
+ if (trim(botId)) storedKeys.push('botId');
147
+ if (trim(secret)) storedKeys.push('secret');
148
+ if (trim(tencentDocsApiKey)) storedKeys.push('tencentDocsApiKey');
149
+ return {
150
+ connectorId,
151
+ descriptor: {
152
+ connectorId,
153
+ type: 'wecom',
154
+ name: connectorId,
155
+ displayName: 'WeCom',
156
+ source: 'managed',
157
+ managed: true,
158
+ status: configured ? 'authorized' : 'draft',
159
+ mode: 'websocket',
160
+ capabilities: buildConnectorCapabilities('wecom'),
161
+ config: {
162
+ botId: trim(botId),
163
+ websocketUrl,
164
+ dmPolicy: 'open',
165
+ groupPolicy: 'open',
166
+ sendThinkingMessage: false,
167
+ tencentDocsEnabled: Boolean(trim(tencentDocsApiKey)),
168
+ },
169
+ binding: {},
170
+ runtime: {
171
+ desiredEnabled: true,
172
+ configured,
173
+ authModes: ['bot_secret'],
174
+ },
175
+ health: {
176
+ state: configured ? 'idle' : 'misconfigured',
177
+ lastError: configured ? '' : 'missing fields: botId, secret',
178
+ },
179
+ auth: {
180
+ supportedModes: ['bot_secret'],
181
+ mode: configured ? 'bot_secret' : '',
182
+ state: configured ? 'configured' : 'unconfigured',
183
+ configured,
184
+ credentialRef: `cred_${connectorId}`,
185
+ storedKeys,
186
+ redirectUri: '',
187
+ authorizeUrl: '',
188
+ scopes: [],
189
+ tokenExchangeConfigured: false,
190
+ hasAccessToken: false,
191
+ hasRefreshToken: false,
192
+ lastAuthorizedAt: configured ? updatedAt : '',
193
+ lastError: configured ? '' : 'missing fields: botId, secret',
194
+ lastErrorAt: configured ? '' : updatedAt,
195
+ },
196
+ createdAt: updatedAt,
197
+ updatedAt,
198
+ },
199
+ credentials: {
200
+ connectorId,
201
+ credentialRef: `cred_${connectorId}`,
202
+ authMode: 'bot_secret',
203
+ values: {
204
+ ...(trim(botId) ? { botId: trim(botId) } : {}),
205
+ ...(trim(secret) ? { secret: trim(secret) } : {}),
206
+ ...(trim(tencentDocsApiKey) ? { tencentDocsApiKey: trim(tencentDocsApiKey) } : {}),
207
+ },
208
+ oauth: {},
209
+ updatedAt,
210
+ },
211
+ };
212
+ }
213
+
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
+ async function ensureReadableFile(fsPromises, filePath, label) {
235
+ const target = trim(filePath);
236
+ if (!target) return '';
237
+ try {
238
+ await fsPromises.access(target);
239
+ } catch {
240
+ throw new Error(`${label} not found: ${target}`);
241
+ }
242
+ return target;
243
+ }
244
+
245
+ function renderEnvFile(values = {}) {
246
+ const lines = Object.entries(values)
247
+ .filter(([, value]) => trim(value))
248
+ .map(([key, value]) => `${key}=${String(value)}`);
249
+ return lines.length ? `${lines.join('\n')}\n` : '';
250
+ }
251
+
252
+ export function createNpmDistributionInstallSeedService(deps = {}) {
253
+ const fsPromises = deps.fsPromises || fs;
254
+ const osModule = deps.osModule || os;
255
+ const pathModule = deps.pathModule || path;
256
+ const processImpl = deps.processImpl || process;
257
+ const logger = deps.logger || console;
258
+ const nowIso = deps.nowIso || (() => new Date().toISOString());
259
+ const promptFactory = deps.promptFactory || createPrompter;
260
+
261
+ async function prepareInstallSeed(options = {}) {
262
+ const explicitSeedFiles = Boolean(
263
+ trim(options.seedEnvFile)
264
+ || trim(options.seedConnectorsStateFile)
265
+ || trim(options.seedConnectorsCredentialsFile),
266
+ );
267
+ const prompter = promptFactory({
268
+ stdin: processImpl.stdin,
269
+ stdout: processImpl.stdout,
270
+ });
271
+ const nonInteractive = Boolean(options.nonInteractive) || !prompter.interactive;
272
+ const tempDir = await fsPromises.mkdtemp(pathModule.join(osModule.tmpdir(), 'codexbot-install-seed-'));
273
+ const cleanup = async () => {
274
+ await prompter.close().catch(() => {});
275
+ await fsPromises.rm(tempDir, { recursive: true, force: true }).catch(() => {});
276
+ };
277
+
278
+ try {
279
+ const result = {
280
+ seedEnvFile: trim(options.seedEnvFile),
281
+ seedConnectorsStateFile: trim(options.seedConnectorsStateFile),
282
+ seedConnectorsCredentialsFile: trim(options.seedConnectorsCredentialsFile),
283
+ seedLicenseFile: '',
284
+ seedLicensePublicKeyFile: '',
285
+ seedLicenseServerPublicKeyFile: '',
286
+ deferLicenseActivation: Boolean(options.deferLicenseActivation),
287
+ summary: {
288
+ connector: 'skip',
289
+ connectorSeeded: false,
290
+ openAiSeeded: false,
291
+ licenseDeferred: false,
292
+ },
293
+ cleanup,
294
+ };
295
+
296
+ if (explicitSeedFiles) {
297
+ result.seedLicenseFile = trim(options.licenseFile);
298
+ result.seedLicensePublicKeyFile = trim(options.licensePublicKeyFile);
299
+ result.seedLicenseServerPublicKeyFile = trim(options.licenseServerPublicKeyFile);
300
+ if (!result.deferLicenseActivation
301
+ && !result.seedLicenseFile
302
+ && !result.seedLicensePublicKeyFile
303
+ && !result.seedLicenseServerPublicKeyFile) {
304
+ result.deferLicenseActivation = true;
305
+ }
306
+ result.summary.licenseDeferred = result.deferLicenseActivation;
307
+ return result;
308
+ }
309
+
310
+ let connector = normalizeConnectorChoice(options.connector);
311
+ const hasExplicitConnectorArgs = Boolean(
312
+ connector
313
+ || trim(options.telegramBotToken)
314
+ || trim(options.telegramChatIds)
315
+ || trim(options.wecomBotId)
316
+ || trim(options.wecomSecret)
317
+ || trim(options.wecomTencentDocsApiKey),
318
+ );
319
+ if (!connector && hasExplicitConnectorArgs) {
320
+ connector = trim(options.wecomBotId || options.wecomSecret || options.wecomTencentDocsApiKey) ? 'wecom' : 'telegram';
321
+ }
322
+ if (!connector && !nonInteractive) {
323
+ logger.log('==> Remote control channel setup');
324
+ connector = normalizeConnectorChoice(await prompter.ask('Choose connector [telegram/wecom/skip] (default: skip): ')) || 'skip';
325
+ }
326
+ if (!connector) connector = 'skip';
327
+
328
+ const openAiApiKey = trim(options.openAiApiKey || (!nonInteractive ? await prompter.ask('OPENAI_API_KEY (optional, blank to skip): ') : ''));
329
+ if (openAiApiKey) {
330
+ const envFile = pathModule.join(tempDir, 'seed.env');
331
+ await fsPromises.writeFile(envFile, renderEnvFile({ OPENAI_API_KEY: openAiApiKey }), 'utf8');
332
+ result.seedEnvFile = envFile;
333
+ result.summary.openAiSeeded = true;
334
+ }
335
+
336
+ if (connector === 'telegram') {
337
+ const telegramBotToken = trim(options.telegramBotToken || (!nonInteractive ? await prompter.ask('Telegram bot token: ') : ''));
338
+ const telegramChatIds = normalizeList(options.telegramChatIds || (!nonInteractive ? await prompter.ask('Telegram allowed chat IDs (comma-separated): ') : ''));
339
+ if (!telegramBotToken || telegramChatIds.length === 0) {
340
+ throw new Error('Telegram connector requires bot token and at least one allowed chat ID');
341
+ }
342
+ const seed = buildTelegramSeed(nowIso, {
343
+ botToken: telegramBotToken,
344
+ allowedChatIds: telegramChatIds,
345
+ });
346
+ const stateFile = pathModule.join(tempDir, 'connectors.json');
347
+ const credentialsFile = pathModule.join(tempDir, 'connectors.credentials.json');
348
+ await fsPromises.writeFile(stateFile, `${JSON.stringify({ connectors: { [seed.connectorId]: seed.descriptor } }, null, 2)}\n`, 'utf8');
349
+ await fsPromises.writeFile(credentialsFile, `${JSON.stringify({ connectors: { [seed.connectorId]: seed.credentials } }, null, 2)}\n`, 'utf8');
350
+ result.seedConnectorsStateFile = stateFile;
351
+ result.seedConnectorsCredentialsFile = credentialsFile;
352
+ result.summary.connector = 'telegram';
353
+ result.summary.connectorSeeded = true;
354
+ } else if (connector === 'wecom') {
355
+ const wecomBotId = trim(options.wecomBotId || (!nonInteractive ? await prompter.ask('WeCom bot ID: ') : ''));
356
+ const wecomSecret = trim(options.wecomSecret || (!nonInteractive ? await prompter.ask('WeCom bot secret: ') : ''));
357
+ const wecomTencentDocsApiKey = trim(options.wecomTencentDocsApiKey || (!nonInteractive ? await prompter.ask('Tencent Docs API key (optional, blank to skip): ') : ''));
358
+ if (!wecomBotId || !wecomSecret) {
359
+ throw new Error('WeCom connector requires bot ID and secret');
360
+ }
361
+ const seed = buildWecomSeed(nowIso, {
362
+ botId: wecomBotId,
363
+ secret: wecomSecret,
364
+ tencentDocsApiKey: wecomTencentDocsApiKey,
365
+ });
366
+ const stateFile = pathModule.join(tempDir, 'connectors.json');
367
+ const credentialsFile = pathModule.join(tempDir, 'connectors.credentials.json');
368
+ await fsPromises.writeFile(stateFile, `${JSON.stringify({ connectors: { [seed.connectorId]: seed.descriptor } }, null, 2)}\n`, 'utf8');
369
+ await fsPromises.writeFile(credentialsFile, `${JSON.stringify({ connectors: { [seed.connectorId]: seed.credentials } }, null, 2)}\n`, 'utf8');
370
+ result.seedConnectorsStateFile = stateFile;
371
+ result.seedConnectorsCredentialsFile = credentialsFile;
372
+ result.summary.connector = 'wecom';
373
+ result.summary.connectorSeeded = true;
374
+ }
375
+
376
+ let licenseFile = trim(options.licenseFile);
377
+ let licensePublicKeyFile = trim(options.licensePublicKeyFile);
378
+ let licenseServerPublicKeyFile = trim(options.licenseServerPublicKeyFile);
379
+ if (!result.deferLicenseActivation && !licenseFile && !licensePublicKeyFile && !licenseServerPublicKeyFile && !nonInteractive) {
380
+ const provideLicense = normalizeYesNo(await prompter.ask('Provide license materials now? [y/N]: '), false);
381
+ if (provideLicense) {
382
+ licenseFile = await prompter.ask('Path to license.json: ');
383
+ licensePublicKeyFile = await prompter.ask('Path to license-public.pem: ');
384
+ licenseServerPublicKeyFile = await prompter.ask('Path to license-server-public.pem: ');
385
+ } else {
386
+ result.deferLicenseActivation = true;
387
+ }
388
+ } else if (!licenseFile && !licensePublicKeyFile && !licenseServerPublicKeyFile) {
389
+ result.deferLicenseActivation = true;
390
+ }
391
+
392
+ const licenseProvided = Boolean(licenseFile || licensePublicKeyFile || licenseServerPublicKeyFile);
393
+ if (licenseProvided) {
394
+ result.seedLicenseFile = await ensureReadableFile(fsPromises, licenseFile, 'license file');
395
+ result.seedLicensePublicKeyFile = await ensureReadableFile(fsPromises, licensePublicKeyFile, 'license public key file');
396
+ result.seedLicenseServerPublicKeyFile = await ensureReadableFile(fsPromises, licenseServerPublicKeyFile, 'license server public key file');
397
+ result.deferLicenseActivation = false;
398
+ }
399
+
400
+ result.summary.licenseDeferred = result.deferLicenseActivation;
401
+ return result;
402
+ } catch (error) {
403
+ await cleanup();
404
+ throw error;
405
+ }
406
+ }
407
+
408
+ return {
409
+ prepareInstallSeed,
410
+ };
411
+ }
@@ -2,16 +2,79 @@ import fs from 'node:fs/promises';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { spawn } from 'node:child_process';
5
- import { buildRemoteInstallUrls, getDefaultDistributionConfig } from './config_service.mjs';
5
+ import {
6
+ buildPublicInstallUrls,
7
+ buildRemoteInstallUrls,
8
+ getDefaultDistributionConfig,
9
+ } from './config_service.mjs';
10
+ import { createNpmDistributionInstallSeedService } from './install_seed_service.mjs';
6
11
 
7
12
  function trim(value, fallback = '') {
8
13
  const text = String(value ?? '').trim();
9
14
  return text || String(fallback ?? '').trim();
10
15
  }
11
16
 
12
- async function ensureOkResponse(response, label) {
17
+ function buildAuthFailureMessage({
18
+ label,
19
+ status,
20
+ statusText,
21
+ installBaseUrl,
22
+ channel,
23
+ updateBaseUrl,
24
+ authTokenPresent,
25
+ distributionMode = 'upgrade',
26
+ } = {}) {
27
+ const lines = [
28
+ `${label} download failed: ${status} ${statusText}`,
29
+ ];
30
+ if (status === 401 || status === 403) {
31
+ lines.push('');
32
+ if (distributionMode === 'install') {
33
+ lines.push('The public Codexbot install facade rejected this request or could not proxy it upstream.');
34
+ lines.push('Retry with one of the following:');
35
+ lines.push(`- codexbot install --channel ${channel || 'stable'}`);
36
+ lines.push(`- codexbot install --install-base-url '${installBaseUrl || 'https://codexbotinstall.afffun.com'}' --update-base-url '${updateBaseUrl || 'https://codexbotupdate.afffun.com'}'`);
37
+ lines.push('- Verify the install facade is configured with upstream access to the protected update server.');
38
+ if (authTokenPresent) {
39
+ lines.push('- A token was provided, but public install should normally not require it; verify the install facade is not pointing at a protected route.');
40
+ }
41
+ } else {
42
+ lines.push('The Codexbot npm package is public, but the protected update channel still requires an update token.');
43
+ lines.push('Retry with one of the following:');
44
+ lines.push(`- codexbot upgrade --channel ${channel || 'stable'} --token '<UPDATE_SERVER_AUTH_TOKEN>'`);
45
+ lines.push(`- export CODEXBOT_UPDATE_TOKEN='<UPDATE_SERVER_AUTH_TOKEN>' && codexbot upgrade --channel ${channel || 'stable'}`);
46
+ lines.push(`- target update base: ${updateBaseUrl || 'https://codexbotupdate.afffun.com'}`);
47
+ if (authTokenPresent) {
48
+ lines.push('- A token was provided, but the server rejected it. Check that the token is valid for this update channel.');
49
+ } else {
50
+ lines.push('- No update token was provided for this request.');
51
+ }
52
+ }
53
+ }
54
+ return lines.join('\n');
55
+ }
56
+
57
+ async function ensureOkResponse(response, {
58
+ label,
59
+ installBaseUrl,
60
+ channel,
61
+ updateBaseUrl,
62
+ authTokenPresent = false,
63
+ distributionMode = 'upgrade',
64
+ } = {}) {
13
65
  if (!response.ok) {
14
- throw new Error(`${label} download failed: ${response.status} ${response.statusText}`);
66
+ const error = new Error(buildAuthFailureMessage({
67
+ label,
68
+ status: response.status,
69
+ statusText: response.statusText,
70
+ installBaseUrl,
71
+ channel,
72
+ updateBaseUrl,
73
+ authTokenPresent,
74
+ distributionMode,
75
+ }));
76
+ error.code = `HTTP_${response.status}`;
77
+ throw error;
15
78
  }
16
79
  return response;
17
80
  }
@@ -46,53 +109,113 @@ export function createNpmDistributionInstallService(deps = {}) {
46
109
  const spawnFn = deps.spawnFn || spawn;
47
110
  const processImpl = deps.processImpl || process;
48
111
  const logger = deps.logger || console;
112
+ const installSeedService = deps.installSeedService || createNpmDistributionInstallSeedService({
113
+ fsPromises,
114
+ osModule,
115
+ pathModule,
116
+ processImpl,
117
+ logger,
118
+ });
49
119
 
50
120
  async function runRemoteInstall({
51
121
  mode,
52
122
  options = {},
53
123
  } = {}) {
54
124
  const defaults = getDefaultDistributionConfig(processImpl.env);
55
- const urls = buildRemoteInstallUrls({
56
- updateBaseUrl: options.updateBaseUrl || defaults.updateBaseUrl,
57
- channel: options.channel || defaults.channel,
58
- manifestUrl: options.manifestUrl,
59
- publicKeyUrl: options.publicKeyUrl,
60
- installRemoteUrl: options.installRemoteUrl,
61
- allowHttp: Boolean(options.allowHttp),
62
- });
63
- const authToken = trim(options.token, defaults.authToken);
125
+ const distributionMode = trim(mode, 'auto') === 'install' ? 'install' : 'upgrade';
126
+ const urls = distributionMode === 'install'
127
+ ? buildPublicInstallUrls({
128
+ installBaseUrl: options.installBaseUrl || defaults.installBaseUrl,
129
+ updateBaseUrl: options.updateBaseUrl || defaults.updateBaseUrl,
130
+ channel: options.channel || defaults.channel,
131
+ manifestUrl: options.manifestUrl,
132
+ publicKeyUrl: options.publicKeyUrl,
133
+ installRemoteUrl: options.installRemoteUrl,
134
+ allowHttp: Boolean(options.allowHttp),
135
+ })
136
+ : buildRemoteInstallUrls({
137
+ updateBaseUrl: options.updateBaseUrl || defaults.updateBaseUrl,
138
+ channel: options.channel || defaults.channel,
139
+ manifestUrl: options.manifestUrl,
140
+ publicKeyUrl: options.publicKeyUrl,
141
+ installRemoteUrl: options.installRemoteUrl,
142
+ allowHttp: Boolean(options.allowHttp),
143
+ });
144
+ let installSeed = null;
145
+ const effectiveOptions = { ...options };
146
+ if (distributionMode === 'install') {
147
+ installSeed = await installSeedService.prepareInstallSeed(options);
148
+ effectiveOptions.seedEnvFile = trim(installSeed.seedEnvFile, effectiveOptions.seedEnvFile);
149
+ effectiveOptions.seedConnectorsStateFile = trim(installSeed.seedConnectorsStateFile, effectiveOptions.seedConnectorsStateFile);
150
+ effectiveOptions.seedConnectorsCredentialsFile = trim(installSeed.seedConnectorsCredentialsFile, effectiveOptions.seedConnectorsCredentialsFile);
151
+ effectiveOptions.licenseFile = trim(installSeed.seedLicenseFile, effectiveOptions.licenseFile);
152
+ effectiveOptions.licensePublicKeyFile = trim(installSeed.seedLicensePublicKeyFile, effectiveOptions.licensePublicKeyFile);
153
+ effectiveOptions.licenseServerPublicKeyFile = trim(installSeed.seedLicenseServerPublicKeyFile, effectiveOptions.licenseServerPublicKeyFile);
154
+ effectiveOptions.deferLicenseActivation = Boolean(installSeed.deferLicenseActivation || effectiveOptions.deferLicenseActivation);
155
+ }
156
+ const authToken = trim(effectiveOptions.token, defaults.authToken);
64
157
  const tempDir = await fsPromises.mkdtemp(pathModule.join(osModule.tmpdir(), 'codexbot-npm-'));
65
158
  const tempInstallPath = pathModule.join(tempDir, 'install-remote.sh');
66
159
  const tempKeyPath = pathModule.join(tempDir, 'update-public.pem');
67
160
  const headers = authToken ? { Authorization: `Bearer ${authToken}` } : {};
68
161
  try {
69
162
  logger.log(`==> Fetch install entry from ${urls.installRemoteUrl}`);
70
- const installResponse = await ensureOkResponse(await fetchFn(urls.installRemoteUrl, { headers }), 'install-remote');
163
+ const installResponse = await ensureOkResponse(await fetchFn(urls.installRemoteUrl, { headers }), {
164
+ label: 'install-remote',
165
+ installBaseUrl: urls.installBaseUrl,
166
+ channel: urls.channel,
167
+ updateBaseUrl: urls.updateBaseUrl,
168
+ authTokenPresent: Boolean(authToken),
169
+ distributionMode,
170
+ });
71
171
  await writeResponseToFile(installResponse, tempInstallPath, fsPromises);
72
172
  await fsPromises.chmod(tempInstallPath, 0o755);
73
173
 
74
- const publicKeyFile = trim(options.publicKeyFile, '');
174
+ const publicKeyFile = trim(effectiveOptions.publicKeyFile, '');
75
175
  const resolvedPublicKeyFile = publicKeyFile || tempKeyPath;
76
176
  if (!publicKeyFile) {
77
177
  logger.log(`==> Fetch update public key from ${urls.publicKeyUrl}`);
78
- const keyResponse = await ensureOkResponse(await fetchFn(urls.publicKeyUrl, { headers }), 'update public key');
178
+ const keyResponse = await ensureOkResponse(await fetchFn(urls.publicKeyUrl, { headers }), {
179
+ label: 'update public key',
180
+ installBaseUrl: urls.installBaseUrl,
181
+ channel: urls.channel,
182
+ updateBaseUrl: urls.updateBaseUrl,
183
+ authTokenPresent: Boolean(authToken),
184
+ distributionMode,
185
+ });
79
186
  await writeResponseToFile(keyResponse, tempKeyPath, fsPromises);
80
187
  }
81
188
 
82
189
  const args = [
83
190
  tempInstallPath,
84
191
  '--manifest-url', urls.manifestUrl,
192
+ ...(distributionMode === 'install'
193
+ ? ['--runtime-remote-update-manifest-url', urls.runtimeManifestUrl]
194
+ : []),
195
+ ...(distributionMode === 'install'
196
+ ? ['--package-download-base-url', urls.packageDownloadBaseUrl]
197
+ : []),
85
198
  '--public-key-file', resolvedPublicKeyFile,
86
199
  '--channel', urls.channel,
87
200
  '--mode', trim(mode, 'auto'),
88
201
  ...(authToken ? ['--auth-token', authToken] : []),
89
- ...(Array.isArray(options.forwardedArgs) ? options.forwardedArgs : []),
202
+ ...(effectiveOptions.deferLicenseActivation ? ['--defer-license-activation'] : []),
203
+ ...(trim(effectiveOptions.seedEnvFile) ? ['--seed-env-file', trim(effectiveOptions.seedEnvFile)] : []),
204
+ ...(trim(effectiveOptions.seedConnectorsStateFile) ? ['--seed-connectors-state-file', trim(effectiveOptions.seedConnectorsStateFile)] : []),
205
+ ...(trim(effectiveOptions.seedConnectorsCredentialsFile) ? ['--seed-connectors-credentials-file', trim(effectiveOptions.seedConnectorsCredentialsFile)] : []),
206
+ ...(trim(effectiveOptions.licenseFile) ? ['--seed-license-file', trim(effectiveOptions.licenseFile)] : []),
207
+ ...(trim(effectiveOptions.licensePublicKeyFile) ? ['--seed-license-public-key-file', trim(effectiveOptions.licensePublicKeyFile)] : []),
208
+ ...(trim(effectiveOptions.licenseServerPublicKeyFile) ? ['--seed-license-server-public-key-file', trim(effectiveOptions.licenseServerPublicKeyFile)] : []),
209
+ ...(Array.isArray(effectiveOptions.forwardedArgs) ? effectiveOptions.forwardedArgs : []),
90
210
  ];
91
211
  const isRoot = typeof processImpl.getuid === 'function' ? processImpl.getuid() === 0 : false;
92
212
  const command = isRoot ? 'bash' : 'sudo';
93
213
  const commandArgs = isRoot ? args : ['bash', ...args];
94
214
  return await runChild(spawnFn, command, commandArgs);
95
215
  } finally {
216
+ if (installSeed?.cleanup) {
217
+ await installSeed.cleanup().catch(() => {});
218
+ }
96
219
  await fsPromises.rm(tempDir, { recursive: true, force: true }).catch(() => {});
97
220
  }
98
221
  }