@afffun/codexbot 1.0.71 → 1.0.73
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 +8 -3
- package/package.json +1 -1
- package/src/activation_service.mjs +262 -0
- package/src/args_service.mjs +177 -3
- package/src/config_service.mjs +51 -0
- package/src/entrypoint.mjs +38 -1
- package/src/install_seed_service.mjs +417 -0
- package/src/install_service.mjs +160 -17
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
|
-
|
|
6
|
-
process.
|
|
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
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import fsSyncDefault from 'node:fs';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import readline from 'node:readline/promises';
|
|
7
|
+
|
|
8
|
+
import { buildInstallLicenseClaimUrl, getDefaultDistributionConfig } from './config_service.mjs';
|
|
9
|
+
import { resolveInstalledControlLayout } from './installed_layout_service.mjs';
|
|
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
|
+
function runChildCapture(spawnFn, command, args, options = {}) {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const child = spawnFn(command, args, {
|
|
39
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
40
|
+
...options,
|
|
41
|
+
});
|
|
42
|
+
let stdout = '';
|
|
43
|
+
let stderr = '';
|
|
44
|
+
child.stdout?.on('data', (chunk) => {
|
|
45
|
+
stdout += Buffer.from(chunk).toString('utf8');
|
|
46
|
+
});
|
|
47
|
+
child.stderr?.on('data', (chunk) => {
|
|
48
|
+
stderr += Buffer.from(chunk).toString('utf8');
|
|
49
|
+
});
|
|
50
|
+
child.on('error', reject);
|
|
51
|
+
child.on('exit', (code, signal) => {
|
|
52
|
+
if (signal) {
|
|
53
|
+
reject(new Error(`${command} exited via signal ${signal}`));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
resolve({ code: Number(code || 0), stdout, stderr });
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function runChildInherit(spawnFn, command, args, options = {}) {
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
const child = spawnFn(command, args, {
|
|
64
|
+
stdio: 'inherit',
|
|
65
|
+
...options,
|
|
66
|
+
});
|
|
67
|
+
child.on('error', reject);
|
|
68
|
+
child.on('exit', (code, signal) => {
|
|
69
|
+
if (signal) {
|
|
70
|
+
reject(new Error(`${command} exited via signal ${signal}`));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
resolve(Number(code || 0));
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function resolveControlLayout({ fsSync, pathModule, processImpl, options }) {
|
|
79
|
+
return resolveInstalledControlLayout({
|
|
80
|
+
fsSync,
|
|
81
|
+
pathModule,
|
|
82
|
+
env: processImpl.env,
|
|
83
|
+
profile: options.layoutProfile,
|
|
84
|
+
appDir: options.appDir,
|
|
85
|
+
controlEnvFile: options.controlEnvFile,
|
|
86
|
+
controlUser: options.controlUser,
|
|
87
|
+
codexHomeDir: options.codexHomeDir,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function readJsonResponse(response) {
|
|
92
|
+
const text = await response.text();
|
|
93
|
+
try {
|
|
94
|
+
return text ? JSON.parse(text) : {};
|
|
95
|
+
} catch {
|
|
96
|
+
return {};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function buildClaimFailureMessage(error, claimUrl) {
|
|
101
|
+
const lines = [trim(error?.message, 'license claim failed')];
|
|
102
|
+
lines.push('');
|
|
103
|
+
lines.push('The node remains installed but activation is still pending.');
|
|
104
|
+
lines.push(`Retry with: codexbot activate --license-key '<your-license-key>' --license-claim-url '${claimUrl}'`);
|
|
105
|
+
lines.push('Or complete activation manually after obtaining an activation package.');
|
|
106
|
+
return lines.join('\n');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function createNpmDistributionActivationService(deps = {}) {
|
|
110
|
+
const fsPromises = deps.fsPromises || fs;
|
|
111
|
+
const fsSync = deps.fsSync || fsSyncDefault;
|
|
112
|
+
const osModule = deps.osModule || os;
|
|
113
|
+
const pathModule = deps.pathModule || path;
|
|
114
|
+
const spawnFn = deps.spawnFn || spawn;
|
|
115
|
+
const processImpl = deps.processImpl || process;
|
|
116
|
+
const fetchFn = deps.fetchFn || fetch;
|
|
117
|
+
const logger = deps.logger || console;
|
|
118
|
+
const promptFactory = deps.promptFactory || createPrompter;
|
|
119
|
+
|
|
120
|
+
async function resolveLicenseKey(options = {}) {
|
|
121
|
+
const explicit = trim(options.licenseKey);
|
|
122
|
+
if (explicit) return explicit;
|
|
123
|
+
const prompter = promptFactory({
|
|
124
|
+
stdin: processImpl.stdin,
|
|
125
|
+
stdout: processImpl.stdout,
|
|
126
|
+
});
|
|
127
|
+
try {
|
|
128
|
+
if (Boolean(options.nonInteractive) || !prompter.interactive) {
|
|
129
|
+
throw new Error('license key is required');
|
|
130
|
+
}
|
|
131
|
+
const entered = trim(await prompter.ask('License key: '));
|
|
132
|
+
if (!entered) throw new Error('license key is required');
|
|
133
|
+
return entered;
|
|
134
|
+
} finally {
|
|
135
|
+
await prompter.close().catch(() => {});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function readInstalledNodeIdentity(layout, options = {}) {
|
|
140
|
+
const scriptPath = pathModule.join(layout.appDir, 'scripts', 'codexbot_node_identity.sh');
|
|
141
|
+
const isRoot = typeof processImpl.getuid === 'function' ? processImpl.getuid() === 0 : false;
|
|
142
|
+
const command = isRoot ? 'bash' : 'sudo';
|
|
143
|
+
const commandArgs = isRoot
|
|
144
|
+
? [scriptPath, '--control-env-file', layout.controlEnvFile]
|
|
145
|
+
: ['bash', scriptPath, '--control-env-file', layout.controlEnvFile];
|
|
146
|
+
const result = await runChildCapture(spawnFn, command, commandArgs);
|
|
147
|
+
if (result.code !== 0) {
|
|
148
|
+
throw new Error(trim(result.stderr || result.stdout, 'failed to read node identity'));
|
|
149
|
+
}
|
|
150
|
+
let payload = {};
|
|
151
|
+
try {
|
|
152
|
+
payload = JSON.parse(result.stdout);
|
|
153
|
+
} catch {
|
|
154
|
+
throw new Error('node identity output is not valid JSON');
|
|
155
|
+
}
|
|
156
|
+
return payload;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function fetchActivationPackage({ claimUrl, licenseKey, identity }) {
|
|
160
|
+
const response = await fetchFn(claimUrl, {
|
|
161
|
+
method: 'POST',
|
|
162
|
+
headers: {
|
|
163
|
+
'content-type': 'application/json',
|
|
164
|
+
'user-agent': 'codexbot-npm-cli/1.0',
|
|
165
|
+
},
|
|
166
|
+
body: JSON.stringify({
|
|
167
|
+
licenseKey,
|
|
168
|
+
fingerprint: trim(identity?.machineFingerprint),
|
|
169
|
+
node: {
|
|
170
|
+
id: trim(identity?.nodeId),
|
|
171
|
+
...(identity?.payload && typeof identity.payload === 'object' && !Array.isArray(identity.payload) ? identity.payload : {}),
|
|
172
|
+
},
|
|
173
|
+
}),
|
|
174
|
+
});
|
|
175
|
+
const payload = await readJsonResponse(response);
|
|
176
|
+
if (!response.ok) {
|
|
177
|
+
const err = new Error(trim(payload?.error?.message || payload?.message, `license claim http ${response.status}`));
|
|
178
|
+
err.code = trim(payload?.error?.code || payload?.code, `HTTP_${response.status}`);
|
|
179
|
+
err.statusCode = response.status;
|
|
180
|
+
err.payload = payload;
|
|
181
|
+
throw err;
|
|
182
|
+
}
|
|
183
|
+
return payload;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function writeActivationPackage(tempDir, activation = {}) {
|
|
187
|
+
const licenseFile = pathModule.join(tempDir, 'license.json');
|
|
188
|
+
const licensePublicKeyFile = pathModule.join(tempDir, 'license-public.pem');
|
|
189
|
+
const licenseServerPublicKeyFile = pathModule.join(tempDir, 'license-server-public.pem');
|
|
190
|
+
await fsPromises.writeFile(licenseFile, `${JSON.stringify(activation.license || {}, null, 2)}\n`, 'utf8');
|
|
191
|
+
await fsPromises.writeFile(licensePublicKeyFile, `${trim(activation.licensePublicKey)}\n`, 'utf8');
|
|
192
|
+
await fsPromises.writeFile(licenseServerPublicKeyFile, `${trim(activation.licenseServerPublicKey)}\n`, 'utf8');
|
|
193
|
+
return {
|
|
194
|
+
licenseFile,
|
|
195
|
+
licensePublicKeyFile,
|
|
196
|
+
licenseServerPublicKeyFile,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function runLocalActivationScript(layout, files) {
|
|
201
|
+
const scriptPath = pathModule.join(layout.appDir, 'scripts', 'codexbot_activate.sh');
|
|
202
|
+
const isRoot = typeof processImpl.getuid === 'function' ? processImpl.getuid() === 0 : false;
|
|
203
|
+
const baseArgs = [
|
|
204
|
+
scriptPath,
|
|
205
|
+
'--control-env-file', layout.controlEnvFile,
|
|
206
|
+
'--license-file', files.licenseFile,
|
|
207
|
+
'--license-public-key-file', files.licensePublicKeyFile,
|
|
208
|
+
'--license-server-public-key-file', files.licenseServerPublicKeyFile,
|
|
209
|
+
'--start',
|
|
210
|
+
];
|
|
211
|
+
const command = isRoot ? 'bash' : 'sudo';
|
|
212
|
+
const commandArgs = isRoot ? baseArgs : ['bash', ...baseArgs];
|
|
213
|
+
return runChildInherit(spawnFn, command, commandArgs);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function claimAndActivate(options = {}) {
|
|
217
|
+
const defaults = getDefaultDistributionConfig(processImpl.env);
|
|
218
|
+
const licenseKey = await resolveLicenseKey(options);
|
|
219
|
+
const claimUrl = buildInstallLicenseClaimUrl({
|
|
220
|
+
installBaseUrl: options.installBaseUrl || defaults.installBaseUrl,
|
|
221
|
+
claimUrl: options.licenseClaimUrl || defaults.licenseClaimUrl,
|
|
222
|
+
allowHttp: Boolean(options.allowHttp),
|
|
223
|
+
});
|
|
224
|
+
const layout = resolveControlLayout({
|
|
225
|
+
fsSync,
|
|
226
|
+
pathModule,
|
|
227
|
+
processImpl,
|
|
228
|
+
options,
|
|
229
|
+
});
|
|
230
|
+
const tempDir = await fsPromises.mkdtemp(pathModule.join(osModule.tmpdir(), 'codexbot-activation-'));
|
|
231
|
+
try {
|
|
232
|
+
logger.log('==> Read node identity');
|
|
233
|
+
const identity = await readInstalledNodeIdentity(layout, options);
|
|
234
|
+
logger.log(`==> Claim activation package from ${claimUrl}`);
|
|
235
|
+
const claim = await fetchActivationPackage({ claimUrl, licenseKey, identity });
|
|
236
|
+
const activation = claim?.activation || {};
|
|
237
|
+
if (!activation.license || !trim(activation.licensePublicKey) || !trim(activation.licenseServerPublicKey)) {
|
|
238
|
+
throw new Error('claim response is missing activation package files');
|
|
239
|
+
}
|
|
240
|
+
const files = await writeActivationPackage(tempDir, activation);
|
|
241
|
+
logger.log('==> Activate installed node');
|
|
242
|
+
const exitCode = await runLocalActivationScript(layout, files);
|
|
243
|
+
if (exitCode !== 0) {
|
|
244
|
+
throw new Error(`activation script exited with code ${exitCode}`);
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
ok: true,
|
|
248
|
+
claim,
|
|
249
|
+
};
|
|
250
|
+
} catch (error) {
|
|
251
|
+
const wrapped = new Error(buildClaimFailureMessage(error, claimUrl));
|
|
252
|
+
wrapped.cause = error;
|
|
253
|
+
throw wrapped;
|
|
254
|
+
} finally {
|
|
255
|
+
await fsPromises.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
claimAndActivate,
|
|
261
|
+
};
|
|
262
|
+
}
|
package/src/args_service.mjs
CHANGED
|
@@ -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,23 @@ 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
|
+
licenseKey: '',
|
|
34
|
+
licenseClaimUrl: '',
|
|
35
|
+
licenseFile: '',
|
|
36
|
+
licensePublicKeyFile: '',
|
|
37
|
+
licenseServerPublicKeyFile: '',
|
|
38
|
+
seedEnvFile: '',
|
|
39
|
+
seedConnectorsStateFile: '',
|
|
40
|
+
seedConnectorsCredentialsFile: '',
|
|
23
41
|
forwardedArgs: [],
|
|
24
42
|
};
|
|
25
43
|
|
|
@@ -31,6 +49,11 @@ function parseBootstrapOptions(argv = []) {
|
|
|
31
49
|
i += 1;
|
|
32
50
|
continue;
|
|
33
51
|
}
|
|
52
|
+
if (token === '--install-base-url') {
|
|
53
|
+
options.installBaseUrl = readOptionValue(argv, i, token);
|
|
54
|
+
i += 1;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
34
57
|
if (token === '--update-base-url') {
|
|
35
58
|
options.updateBaseUrl = readOptionValue(argv, i, token);
|
|
36
59
|
i += 1;
|
|
@@ -66,6 +89,89 @@ function parseBootstrapOptions(argv = []) {
|
|
|
66
89
|
options.forwardedArgs.push(token);
|
|
67
90
|
continue;
|
|
68
91
|
}
|
|
92
|
+
if (token === '--non-interactive' || token === '--no-prompt') {
|
|
93
|
+
options.nonInteractive = true;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (token === '--connector') {
|
|
97
|
+
options.connector = readOptionValue(argv, i, token);
|
|
98
|
+
i += 1;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (token === '--telegram-bot-token') {
|
|
102
|
+
options.telegramBotToken = readOptionValue(argv, i, token);
|
|
103
|
+
i += 1;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (token === '--telegram-chat-ids') {
|
|
107
|
+
options.telegramChatIds = readOptionValue(argv, i, token);
|
|
108
|
+
i += 1;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (token === '--wecom-bot-id') {
|
|
112
|
+
options.wecomBotId = readOptionValue(argv, i, token);
|
|
113
|
+
i += 1;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (token === '--wecom-secret') {
|
|
117
|
+
options.wecomSecret = readOptionValue(argv, i, token);
|
|
118
|
+
i += 1;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (token === '--wecom-tencent-docs-api-key') {
|
|
122
|
+
options.wecomTencentDocsApiKey = readOptionValue(argv, i, token);
|
|
123
|
+
i += 1;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (token === '--openai-api-key') {
|
|
127
|
+
options.openAiApiKey = readOptionValue(argv, i, token);
|
|
128
|
+
i += 1;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (token === '--defer-license-activation') {
|
|
132
|
+
options.deferLicenseActivation = true;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (token === '--license-key') {
|
|
136
|
+
options.licenseKey = readOptionValue(argv, i, token);
|
|
137
|
+
i += 1;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (token === '--license-claim-url') {
|
|
141
|
+
options.licenseClaimUrl = readOptionValue(argv, i, token);
|
|
142
|
+
i += 1;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (token === '--license-file') {
|
|
146
|
+
options.licenseFile = readOptionValue(argv, i, token);
|
|
147
|
+
i += 1;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (token === '--license-public-key-file') {
|
|
151
|
+
options.licensePublicKeyFile = readOptionValue(argv, i, token);
|
|
152
|
+
i += 1;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (token === '--license-server-public-key-file') {
|
|
156
|
+
options.licenseServerPublicKeyFile = readOptionValue(argv, i, token);
|
|
157
|
+
i += 1;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (token === '--seed-env-file') {
|
|
161
|
+
options.seedEnvFile = readOptionValue(argv, i, token);
|
|
162
|
+
i += 1;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (token === '--seed-connectors-state-file') {
|
|
166
|
+
options.seedConnectorsStateFile = readOptionValue(argv, i, token);
|
|
167
|
+
i += 1;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (token === '--seed-connectors-credentials-file') {
|
|
171
|
+
options.seedConnectorsCredentialsFile = readOptionValue(argv, i, token);
|
|
172
|
+
i += 1;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
69
175
|
if (token === '--no-signature') {
|
|
70
176
|
options.noSignature = true;
|
|
71
177
|
options.forwardedArgs.push(token);
|
|
@@ -77,7 +183,7 @@ function parseBootstrapOptions(argv = []) {
|
|
|
77
183
|
return options;
|
|
78
184
|
}
|
|
79
185
|
|
|
80
|
-
function
|
|
186
|
+
function parseLayoutOptions(argv = []) {
|
|
81
187
|
const options = {
|
|
82
188
|
layoutProfile: '',
|
|
83
189
|
appDir: '',
|
|
@@ -118,6 +224,71 @@ function parseAuthOptions(argv = []) {
|
|
|
118
224
|
return options;
|
|
119
225
|
}
|
|
120
226
|
|
|
227
|
+
function parseActivationOptions(argv = []) {
|
|
228
|
+
const options = {
|
|
229
|
+
...parseLayoutOptions([]),
|
|
230
|
+
installBaseUrl: '',
|
|
231
|
+
licenseClaimUrl: '',
|
|
232
|
+
allowHttp: false,
|
|
233
|
+
nonInteractive: false,
|
|
234
|
+
licenseKey: '',
|
|
235
|
+
};
|
|
236
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
237
|
+
const token = trim(argv[i]);
|
|
238
|
+
if (!token) continue;
|
|
239
|
+
if (token === '--license-key') {
|
|
240
|
+
options.licenseKey = readOptionValue(argv, i, token);
|
|
241
|
+
i += 1;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (token === '--install-base-url') {
|
|
245
|
+
options.installBaseUrl = readOptionValue(argv, i, token);
|
|
246
|
+
i += 1;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (token === '--license-claim-url') {
|
|
250
|
+
options.licenseClaimUrl = readOptionValue(argv, i, token);
|
|
251
|
+
i += 1;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (token === '--allow-http') {
|
|
255
|
+
options.allowHttp = true;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (token === '--non-interactive' || token === '--no-prompt') {
|
|
259
|
+
options.nonInteractive = true;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (token === '--layout-profile') {
|
|
263
|
+
options.layoutProfile = readOptionValue(argv, i, token);
|
|
264
|
+
i += 1;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (token === '--app-dir') {
|
|
268
|
+
options.appDir = readOptionValue(argv, i, token);
|
|
269
|
+
i += 1;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (token === '--control-env-file') {
|
|
273
|
+
options.controlEnvFile = readOptionValue(argv, i, token);
|
|
274
|
+
i += 1;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (token === '--control-user') {
|
|
278
|
+
options.controlUser = readOptionValue(argv, i, token);
|
|
279
|
+
i += 1;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (token === '--codex-home-dir') {
|
|
283
|
+
options.codexHomeDir = readOptionValue(argv, i, token);
|
|
284
|
+
i += 1;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
throw new Error(`unknown activate arg: ${token}`);
|
|
288
|
+
}
|
|
289
|
+
return options;
|
|
290
|
+
}
|
|
291
|
+
|
|
121
292
|
export function parseCliArgs(argv = []) {
|
|
122
293
|
const args = Array.isArray(argv) ? argv.map((item) => String(item || '')) : [];
|
|
123
294
|
const command = trim(args[0]).toLowerCase();
|
|
@@ -128,7 +299,7 @@ export function parseCliArgs(argv = []) {
|
|
|
128
299
|
return { action: 'version' };
|
|
129
300
|
}
|
|
130
301
|
if (command === 'doctor') {
|
|
131
|
-
return { action: 'doctor', options:
|
|
302
|
+
return { action: 'doctor', options: parseLayoutOptions(args.slice(1)) };
|
|
132
303
|
}
|
|
133
304
|
if (command === 'install') {
|
|
134
305
|
return { action: 'install', options: parseBootstrapOptions(args.slice(1)) };
|
|
@@ -136,12 +307,15 @@ export function parseCliArgs(argv = []) {
|
|
|
136
307
|
if (command === 'upgrade') {
|
|
137
308
|
return { action: 'upgrade', options: parseBootstrapOptions(args.slice(1)) };
|
|
138
309
|
}
|
|
310
|
+
if (command === 'activate') {
|
|
311
|
+
return { action: 'activate', options: parseActivationOptions(args.slice(1)) };
|
|
312
|
+
}
|
|
139
313
|
if (command === 'auth') {
|
|
140
314
|
const sub = trim(args[1]).toLowerCase() || 'status';
|
|
141
315
|
if (!['status', 'login', 'login-link', 'logout'].includes(sub)) {
|
|
142
316
|
throw new Error(`unknown auth subcommand: ${sub}`);
|
|
143
317
|
}
|
|
144
|
-
return { action: 'auth', mode: sub, options:
|
|
318
|
+
return { action: 'auth', mode: sub, options: parseLayoutOptions(args.slice(2)) };
|
|
145
319
|
}
|
|
146
320
|
throw new Error(`unknown command: ${command}`);
|
|
147
321
|
}
|
package/src/config_service.mjs
CHANGED
|
@@ -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,9 +29,59 @@ 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, ''),
|
|
36
|
+
licenseClaimUrl: trim(env.CODEXBOT_LICENSE_CLAIM_URL, ''),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function buildInstallLicenseClaimUrl({
|
|
41
|
+
installBaseUrl = DEFAULT_INSTALL_BASE_URL,
|
|
42
|
+
claimUrl = '',
|
|
43
|
+
allowHttp = false,
|
|
44
|
+
} = {}) {
|
|
45
|
+
const resolvedInstallBaseUrl = normalizeBaseUrl(installBaseUrl, DEFAULT_INSTALL_BASE_URL);
|
|
46
|
+
const resolvedClaimUrl = trim(claimUrl) || `${resolvedInstallBaseUrl}/v1/install/license/claim`;
|
|
47
|
+
return assertHttps(resolvedClaimUrl, { allowHttp, label: 'license claim url' });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function buildPublicInstallUrls({
|
|
51
|
+
installBaseUrl = DEFAULT_INSTALL_BASE_URL,
|
|
52
|
+
updateBaseUrl = DEFAULT_UPDATE_BASE_URL,
|
|
53
|
+
channel = DEFAULT_CHANNEL,
|
|
54
|
+
manifestUrl = '',
|
|
55
|
+
publicKeyUrl = '',
|
|
56
|
+
installRemoteUrl = '',
|
|
57
|
+
runtimeManifestUrl = '',
|
|
58
|
+
packageDownloadBaseUrl = '',
|
|
59
|
+
allowHttp = false,
|
|
60
|
+
} = {}) {
|
|
61
|
+
const resolvedInstallBaseUrl = normalizeBaseUrl(installBaseUrl, DEFAULT_INSTALL_BASE_URL);
|
|
62
|
+
const resolvedUpdateBaseUrl = normalizeBaseUrl(updateBaseUrl, DEFAULT_UPDATE_BASE_URL);
|
|
63
|
+
const resolvedChannel = normalizeChannel(channel, DEFAULT_CHANNEL);
|
|
64
|
+
const publicRoot = `${resolvedInstallBaseUrl}/v1/install/${resolvedChannel}`;
|
|
65
|
+
const updateRoot = `${resolvedUpdateBaseUrl}/v1/channels/${resolvedChannel}`;
|
|
66
|
+
const urls = {
|
|
67
|
+
installBaseUrl: resolvedInstallBaseUrl,
|
|
68
|
+
updateBaseUrl: resolvedUpdateBaseUrl,
|
|
69
|
+
channel: resolvedChannel,
|
|
70
|
+
manifestUrl: trim(manifestUrl) || `${publicRoot}/manifest.json`,
|
|
71
|
+
publicKeyUrl: trim(publicKeyUrl) || `${publicRoot}/packages/latest/update-public.pem`,
|
|
72
|
+
installRemoteUrl: trim(installRemoteUrl) || `${publicRoot}/packages/latest/install-remote.sh`,
|
|
73
|
+
runtimeManifestUrl: trim(runtimeManifestUrl) || `${updateRoot}/manifest.json`,
|
|
74
|
+
packageDownloadBaseUrl: trim(packageDownloadBaseUrl) || `${publicRoot}/packages`,
|
|
75
|
+
};
|
|
76
|
+
return {
|
|
77
|
+
installBaseUrl: assertHttps(urls.installBaseUrl, { allowHttp, label: 'install base url' }),
|
|
78
|
+
updateBaseUrl: assertHttps(urls.updateBaseUrl, { allowHttp, label: 'update base url' }),
|
|
79
|
+
channel: urls.channel,
|
|
80
|
+
manifestUrl: assertHttps(urls.manifestUrl, { allowHttp, label: 'manifest url' }),
|
|
81
|
+
publicKeyUrl: assertHttps(urls.publicKeyUrl, { allowHttp, label: 'public key url' }),
|
|
82
|
+
installRemoteUrl: assertHttps(urls.installRemoteUrl, { allowHttp, label: 'install-remote url' }),
|
|
83
|
+
runtimeManifestUrl: assertHttps(urls.runtimeManifestUrl, { allowHttp, label: 'runtime manifest url' }),
|
|
84
|
+
packageDownloadBaseUrl: assertHttps(urls.packageDownloadBaseUrl, { allowHttp, label: 'package download base url' }),
|
|
34
85
|
};
|
|
35
86
|
}
|
|
36
87
|
|
package/src/entrypoint.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { parseCliArgs } from './args_service.mjs';
|
|
4
|
+
import { createNpmDistributionActivationService } from './activation_service.mjs';
|
|
4
5
|
import { createNpmDistributionInstallService } from './install_service.mjs';
|
|
5
6
|
import { createNpmDistributionAuthService } from './auth_service.mjs';
|
|
6
7
|
|
|
@@ -16,18 +17,43 @@ function renderUsage() {
|
|
|
16
17
|
'Usage:',
|
|
17
18
|
' codexbot install [bootstrap options] [install-remote flags...]',
|
|
18
19
|
' codexbot upgrade [bootstrap options] [install-remote flags...]',
|
|
20
|
+
' codexbot activate [activation options]',
|
|
19
21
|
' codexbot auth status|login|login-link|logout [layout overrides]',
|
|
20
22
|
' codexbot doctor [layout overrides]',
|
|
21
23
|
' codexbot version',
|
|
22
24
|
'',
|
|
23
25
|
'Bootstrap options:',
|
|
24
26
|
' --channel <stable|canary|...>',
|
|
27
|
+
' --install-base-url <https://codexbotinstall.example.com>',
|
|
25
28
|
' --update-base-url <https://codexbotupdate.example.com>',
|
|
29
|
+
' install defaults to an interactive seed wizard unless --non-interactive is used.',
|
|
30
|
+
' --connector <telegram|wecom|skip>',
|
|
31
|
+
' --telegram-bot-token <token>',
|
|
32
|
+
' --telegram-chat-ids <id1,id2,...>',
|
|
33
|
+
' --wecom-bot-id <botId>',
|
|
34
|
+
' --wecom-secret <secret>',
|
|
35
|
+
' --wecom-tencent-docs-api-key <apiKey>',
|
|
36
|
+
' --openai-api-key <key>',
|
|
37
|
+
' --license-key <key>',
|
|
38
|
+
' install will auto-claim and activate when a license key is provided.',
|
|
39
|
+
' --license-file </path/to/license.json>',
|
|
40
|
+
' --license-public-key-file </path/to/license-public.pem>',
|
|
41
|
+
' --license-server-public-key-file </path/to/license-server-public.pem>',
|
|
42
|
+
' --defer-license-activation',
|
|
43
|
+
' --non-interactive',
|
|
44
|
+
' advanced seed flags such as --seed-env-file remain supported for automation',
|
|
26
45
|
' --manifest-url <url>',
|
|
27
46
|
' --public-key-url <url>',
|
|
28
47
|
' --install-remote-url <url>',
|
|
29
48
|
' --public-key-file </path/to/update-public.pem>',
|
|
30
|
-
' --token <update server token>',
|
|
49
|
+
' --token <protected update server token>',
|
|
50
|
+
' --allow-http',
|
|
51
|
+
'',
|
|
52
|
+
'Activation options:',
|
|
53
|
+
' --license-key <key>',
|
|
54
|
+
' --install-base-url <https://codexbotinstall.example.com>',
|
|
55
|
+
' --license-claim-url <https://codexbotinstall.example.com/v1/install/license/claim>',
|
|
56
|
+
' --non-interactive',
|
|
31
57
|
' --allow-http',
|
|
32
58
|
'',
|
|
33
59
|
'Layout overrides:',
|
|
@@ -42,6 +68,7 @@ function renderUsage() {
|
|
|
42
68
|
export async function runCodexbotCli(argv = [], deps = {}) {
|
|
43
69
|
const logger = deps.logger || console;
|
|
44
70
|
const installService = deps.installService || createNpmDistributionInstallService({ logger });
|
|
71
|
+
const activationService = deps.activationService || createNpmDistributionActivationService({ logger });
|
|
45
72
|
const authService = deps.authService || createNpmDistributionAuthService({ logger });
|
|
46
73
|
const cli = parseCliArgs(argv);
|
|
47
74
|
if (cli.action === 'help') {
|
|
@@ -71,8 +98,18 @@ export async function runCodexbotCli(argv = [], deps = {}) {
|
|
|
71
98
|
if (cli.action === 'upgrade') {
|
|
72
99
|
return installService.runUpgradeCommand(cli.options || {});
|
|
73
100
|
}
|
|
101
|
+
if (cli.action === 'activate') {
|
|
102
|
+
await activationService.claimAndActivate(cli.options || {});
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
74
105
|
if (cli.action === 'auth') {
|
|
75
106
|
return authService.runAuthCommand(cli.mode, cli.options || {});
|
|
76
107
|
}
|
|
77
108
|
throw new Error(`unsupported action: ${cli.action}`);
|
|
78
109
|
}
|
|
110
|
+
|
|
111
|
+
export function formatCliError(err) {
|
|
112
|
+
const message = String(err?.message || err || '').trim();
|
|
113
|
+
if (!message) return 'codexbot failed';
|
|
114
|
+
return message;
|
|
115
|
+
}
|
|
@@ -0,0 +1,417 @@
|
|
|
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
|
+
licenseKey: trim(options.licenseKey),
|
|
287
|
+
deferLicenseActivation: Boolean(options.deferLicenseActivation),
|
|
288
|
+
summary: {
|
|
289
|
+
connector: 'skip',
|
|
290
|
+
connectorSeeded: false,
|
|
291
|
+
openAiSeeded: false,
|
|
292
|
+
licenseDeferred: false,
|
|
293
|
+
},
|
|
294
|
+
cleanup,
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
if (explicitSeedFiles) {
|
|
298
|
+
if (result.licenseKey) {
|
|
299
|
+
throw new Error('license key cannot be combined with explicit license seed files');
|
|
300
|
+
}
|
|
301
|
+
result.seedLicenseFile = trim(options.licenseFile);
|
|
302
|
+
result.seedLicensePublicKeyFile = trim(options.licensePublicKeyFile);
|
|
303
|
+
result.seedLicenseServerPublicKeyFile = trim(options.licenseServerPublicKeyFile);
|
|
304
|
+
if (!result.deferLicenseActivation
|
|
305
|
+
&& !result.seedLicenseFile
|
|
306
|
+
&& !result.seedLicensePublicKeyFile
|
|
307
|
+
&& !result.seedLicenseServerPublicKeyFile) {
|
|
308
|
+
result.deferLicenseActivation = true;
|
|
309
|
+
}
|
|
310
|
+
result.summary.licenseDeferred = result.deferLicenseActivation;
|
|
311
|
+
return result;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
let connector = normalizeConnectorChoice(options.connector);
|
|
315
|
+
const hasExplicitConnectorArgs = Boolean(
|
|
316
|
+
connector
|
|
317
|
+
|| trim(options.telegramBotToken)
|
|
318
|
+
|| trim(options.telegramChatIds)
|
|
319
|
+
|| trim(options.wecomBotId)
|
|
320
|
+
|| trim(options.wecomSecret)
|
|
321
|
+
|| trim(options.wecomTencentDocsApiKey),
|
|
322
|
+
);
|
|
323
|
+
if (!connector && hasExplicitConnectorArgs) {
|
|
324
|
+
connector = trim(options.wecomBotId || options.wecomSecret || options.wecomTencentDocsApiKey) ? 'wecom' : 'telegram';
|
|
325
|
+
}
|
|
326
|
+
if (!connector && !nonInteractive) {
|
|
327
|
+
logger.log('==> Remote control channel setup');
|
|
328
|
+
connector = normalizeConnectorChoice(await prompter.ask('Choose connector [telegram/wecom/skip] (default: skip): ')) || 'skip';
|
|
329
|
+
}
|
|
330
|
+
if (!connector) connector = 'skip';
|
|
331
|
+
|
|
332
|
+
const openAiApiKey = trim(options.openAiApiKey || (!nonInteractive ? await prompter.ask('OPENAI_API_KEY (optional, blank to skip): ') : ''));
|
|
333
|
+
if (openAiApiKey) {
|
|
334
|
+
const envFile = pathModule.join(tempDir, 'seed.env');
|
|
335
|
+
await fsPromises.writeFile(envFile, renderEnvFile({ OPENAI_API_KEY: openAiApiKey }), 'utf8');
|
|
336
|
+
result.seedEnvFile = envFile;
|
|
337
|
+
result.summary.openAiSeeded = true;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
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): ') : ''));
|
|
343
|
+
if (!telegramBotToken || telegramChatIds.length === 0) {
|
|
344
|
+
throw new Error('Telegram connector requires bot token and at least one allowed chat ID');
|
|
345
|
+
}
|
|
346
|
+
const seed = buildTelegramSeed(nowIso, {
|
|
347
|
+
botToken: telegramBotToken,
|
|
348
|
+
allowedChatIds: telegramChatIds,
|
|
349
|
+
});
|
|
350
|
+
const stateFile = pathModule.join(tempDir, 'connectors.json');
|
|
351
|
+
const credentialsFile = pathModule.join(tempDir, 'connectors.credentials.json');
|
|
352
|
+
await fsPromises.writeFile(stateFile, `${JSON.stringify({ connectors: { [seed.connectorId]: seed.descriptor } }, null, 2)}\n`, 'utf8');
|
|
353
|
+
await fsPromises.writeFile(credentialsFile, `${JSON.stringify({ connectors: { [seed.connectorId]: seed.credentials } }, null, 2)}\n`, 'utf8');
|
|
354
|
+
result.seedConnectorsStateFile = stateFile;
|
|
355
|
+
result.seedConnectorsCredentialsFile = credentialsFile;
|
|
356
|
+
result.summary.connector = 'telegram';
|
|
357
|
+
result.summary.connectorSeeded = true;
|
|
358
|
+
} 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: ') : ''));
|
|
361
|
+
const wecomTencentDocsApiKey = trim(options.wecomTencentDocsApiKey || (!nonInteractive ? await prompter.ask('Tencent Docs API key (optional, blank to skip): ') : ''));
|
|
362
|
+
if (!wecomBotId || !wecomSecret) {
|
|
363
|
+
throw new Error('WeCom connector requires bot ID and secret');
|
|
364
|
+
}
|
|
365
|
+
const seed = buildWecomSeed(nowIso, {
|
|
366
|
+
botId: wecomBotId,
|
|
367
|
+
secret: wecomSecret,
|
|
368
|
+
tencentDocsApiKey: wecomTencentDocsApiKey,
|
|
369
|
+
});
|
|
370
|
+
const stateFile = pathModule.join(tempDir, 'connectors.json');
|
|
371
|
+
const credentialsFile = pathModule.join(tempDir, 'connectors.credentials.json');
|
|
372
|
+
await fsPromises.writeFile(stateFile, `${JSON.stringify({ connectors: { [seed.connectorId]: seed.descriptor } }, null, 2)}\n`, 'utf8');
|
|
373
|
+
await fsPromises.writeFile(credentialsFile, `${JSON.stringify({ connectors: { [seed.connectorId]: seed.credentials } }, null, 2)}\n`, 'utf8');
|
|
374
|
+
result.seedConnectorsStateFile = stateFile;
|
|
375
|
+
result.seedConnectorsCredentialsFile = credentialsFile;
|
|
376
|
+
result.summary.connector = 'wecom';
|
|
377
|
+
result.summary.connectorSeeded = true;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
let licenseKey = result.licenseKey;
|
|
381
|
+
let licenseFile = trim(options.licenseFile);
|
|
382
|
+
let licensePublicKeyFile = trim(options.licensePublicKeyFile);
|
|
383
|
+
let licenseServerPublicKeyFile = trim(options.licenseServerPublicKeyFile);
|
|
384
|
+
if (licenseKey && (licenseFile || licensePublicKeyFile || licenseServerPublicKeyFile)) {
|
|
385
|
+
throw new Error('license key cannot be combined with direct license file inputs');
|
|
386
|
+
}
|
|
387
|
+
if (!licenseKey && !licenseFile && !licensePublicKeyFile && !licenseServerPublicKeyFile && !nonInteractive) {
|
|
388
|
+
licenseKey = trim(await prompter.ask('License key (optional, blank to skip and activate later): '));
|
|
389
|
+
}
|
|
390
|
+
if (!licenseKey && !licenseFile && !licensePublicKeyFile && !licenseServerPublicKeyFile) {
|
|
391
|
+
result.deferLicenseActivation = true;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (licenseKey) {
|
|
395
|
+
result.licenseKey = licenseKey;
|
|
396
|
+
result.deferLicenseActivation = true;
|
|
397
|
+
}
|
|
398
|
+
const licenseProvided = Boolean(licenseFile || licensePublicKeyFile || licenseServerPublicKeyFile);
|
|
399
|
+
if (licenseProvided) {
|
|
400
|
+
result.seedLicenseFile = await ensureReadableFile(fsPromises, licenseFile, 'license file');
|
|
401
|
+
result.seedLicensePublicKeyFile = await ensureReadableFile(fsPromises, licensePublicKeyFile, 'license public key file');
|
|
402
|
+
result.seedLicenseServerPublicKeyFile = await ensureReadableFile(fsPromises, licenseServerPublicKeyFile, 'license server public key file');
|
|
403
|
+
result.deferLicenseActivation = false;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
result.summary.licenseDeferred = result.deferLicenseActivation;
|
|
407
|
+
return result;
|
|
408
|
+
} catch (error) {
|
|
409
|
+
await cleanup();
|
|
410
|
+
throw error;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
prepareInstallSeed,
|
|
416
|
+
};
|
|
417
|
+
}
|
package/src/install_service.mjs
CHANGED
|
@@ -2,16 +2,80 @@ 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 {
|
|
5
|
+
import {
|
|
6
|
+
buildPublicInstallUrls,
|
|
7
|
+
buildRemoteInstallUrls,
|
|
8
|
+
getDefaultDistributionConfig,
|
|
9
|
+
} from './config_service.mjs';
|
|
10
|
+
import { createNpmDistributionActivationService } from './activation_service.mjs';
|
|
11
|
+
import { createNpmDistributionInstallSeedService } from './install_seed_service.mjs';
|
|
6
12
|
|
|
7
13
|
function trim(value, fallback = '') {
|
|
8
14
|
const text = String(value ?? '').trim();
|
|
9
15
|
return text || String(fallback ?? '').trim();
|
|
10
16
|
}
|
|
11
17
|
|
|
12
|
-
|
|
18
|
+
function buildAuthFailureMessage({
|
|
19
|
+
label,
|
|
20
|
+
status,
|
|
21
|
+
statusText,
|
|
22
|
+
installBaseUrl,
|
|
23
|
+
channel,
|
|
24
|
+
updateBaseUrl,
|
|
25
|
+
authTokenPresent,
|
|
26
|
+
distributionMode = 'upgrade',
|
|
27
|
+
} = {}) {
|
|
28
|
+
const lines = [
|
|
29
|
+
`${label} download failed: ${status} ${statusText}`,
|
|
30
|
+
];
|
|
31
|
+
if (status === 401 || status === 403) {
|
|
32
|
+
lines.push('');
|
|
33
|
+
if (distributionMode === 'install') {
|
|
34
|
+
lines.push('The public Codexbot install facade rejected this request or could not proxy it upstream.');
|
|
35
|
+
lines.push('Retry with one of the following:');
|
|
36
|
+
lines.push(`- codexbot install --channel ${channel || 'stable'}`);
|
|
37
|
+
lines.push(`- codexbot install --install-base-url '${installBaseUrl || 'https://codexbotinstall.afffun.com'}' --update-base-url '${updateBaseUrl || 'https://codexbotupdate.afffun.com'}'`);
|
|
38
|
+
lines.push('- Verify the install facade is configured with upstream access to the protected update server.');
|
|
39
|
+
if (authTokenPresent) {
|
|
40
|
+
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.');
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
lines.push('The Codexbot npm package is public, but the protected update channel still requires an update token.');
|
|
44
|
+
lines.push('Retry with one of the following:');
|
|
45
|
+
lines.push(`- codexbot upgrade --channel ${channel || 'stable'} --token '<UPDATE_SERVER_AUTH_TOKEN>'`);
|
|
46
|
+
lines.push(`- export CODEXBOT_UPDATE_TOKEN='<UPDATE_SERVER_AUTH_TOKEN>' && codexbot upgrade --channel ${channel || 'stable'}`);
|
|
47
|
+
lines.push(`- target update base: ${updateBaseUrl || 'https://codexbotupdate.afffun.com'}`);
|
|
48
|
+
if (authTokenPresent) {
|
|
49
|
+
lines.push('- A token was provided, but the server rejected it. Check that the token is valid for this update channel.');
|
|
50
|
+
} else {
|
|
51
|
+
lines.push('- No update token was provided for this request.');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return lines.join('\n');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function ensureOkResponse(response, {
|
|
59
|
+
label,
|
|
60
|
+
installBaseUrl,
|
|
61
|
+
channel,
|
|
62
|
+
updateBaseUrl,
|
|
63
|
+
authTokenPresent = false,
|
|
64
|
+
distributionMode = 'upgrade',
|
|
65
|
+
} = {}) {
|
|
13
66
|
if (!response.ok) {
|
|
14
|
-
|
|
67
|
+
const error = new Error(buildAuthFailureMessage({
|
|
68
|
+
label,
|
|
69
|
+
status: response.status,
|
|
70
|
+
statusText: response.statusText,
|
|
71
|
+
installBaseUrl,
|
|
72
|
+
channel,
|
|
73
|
+
updateBaseUrl,
|
|
74
|
+
authTokenPresent,
|
|
75
|
+
distributionMode,
|
|
76
|
+
}));
|
|
77
|
+
error.code = `HTTP_${response.status}`;
|
|
78
|
+
throw error;
|
|
15
79
|
}
|
|
16
80
|
return response;
|
|
17
81
|
}
|
|
@@ -46,53 +110,132 @@ export function createNpmDistributionInstallService(deps = {}) {
|
|
|
46
110
|
const spawnFn = deps.spawnFn || spawn;
|
|
47
111
|
const processImpl = deps.processImpl || process;
|
|
48
112
|
const logger = deps.logger || console;
|
|
113
|
+
const installSeedService = deps.installSeedService || createNpmDistributionInstallSeedService({
|
|
114
|
+
fsPromises,
|
|
115
|
+
osModule,
|
|
116
|
+
pathModule,
|
|
117
|
+
processImpl,
|
|
118
|
+
logger,
|
|
119
|
+
});
|
|
120
|
+
const activationService = deps.activationService || createNpmDistributionActivationService({
|
|
121
|
+
fsPromises,
|
|
122
|
+
pathModule,
|
|
123
|
+
spawnFn,
|
|
124
|
+
processImpl,
|
|
125
|
+
fetchFn,
|
|
126
|
+
logger,
|
|
127
|
+
});
|
|
49
128
|
|
|
50
129
|
async function runRemoteInstall({
|
|
51
130
|
mode,
|
|
52
131
|
options = {},
|
|
53
132
|
} = {}) {
|
|
54
133
|
const defaults = getDefaultDistributionConfig(processImpl.env);
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
134
|
+
const distributionMode = trim(mode, 'auto') === 'install' ? 'install' : 'upgrade';
|
|
135
|
+
const urls = distributionMode === 'install'
|
|
136
|
+
? buildPublicInstallUrls({
|
|
137
|
+
installBaseUrl: options.installBaseUrl || defaults.installBaseUrl,
|
|
138
|
+
updateBaseUrl: options.updateBaseUrl || defaults.updateBaseUrl,
|
|
139
|
+
channel: options.channel || defaults.channel,
|
|
140
|
+
manifestUrl: options.manifestUrl,
|
|
141
|
+
publicKeyUrl: options.publicKeyUrl,
|
|
142
|
+
installRemoteUrl: options.installRemoteUrl,
|
|
143
|
+
allowHttp: Boolean(options.allowHttp),
|
|
144
|
+
})
|
|
145
|
+
: buildRemoteInstallUrls({
|
|
146
|
+
updateBaseUrl: options.updateBaseUrl || defaults.updateBaseUrl,
|
|
147
|
+
channel: options.channel || defaults.channel,
|
|
148
|
+
manifestUrl: options.manifestUrl,
|
|
149
|
+
publicKeyUrl: options.publicKeyUrl,
|
|
150
|
+
installRemoteUrl: options.installRemoteUrl,
|
|
151
|
+
allowHttp: Boolean(options.allowHttp),
|
|
152
|
+
});
|
|
153
|
+
let installSeed = null;
|
|
154
|
+
const effectiveOptions = { ...options };
|
|
155
|
+
if (distributionMode === 'install') {
|
|
156
|
+
installSeed = await installSeedService.prepareInstallSeed(options);
|
|
157
|
+
effectiveOptions.seedEnvFile = trim(installSeed.seedEnvFile, effectiveOptions.seedEnvFile);
|
|
158
|
+
effectiveOptions.seedConnectorsStateFile = trim(installSeed.seedConnectorsStateFile, effectiveOptions.seedConnectorsStateFile);
|
|
159
|
+
effectiveOptions.seedConnectorsCredentialsFile = trim(installSeed.seedConnectorsCredentialsFile, effectiveOptions.seedConnectorsCredentialsFile);
|
|
160
|
+
effectiveOptions.licenseFile = trim(installSeed.seedLicenseFile, effectiveOptions.licenseFile);
|
|
161
|
+
effectiveOptions.licensePublicKeyFile = trim(installSeed.seedLicensePublicKeyFile, effectiveOptions.licensePublicKeyFile);
|
|
162
|
+
effectiveOptions.licenseServerPublicKeyFile = trim(installSeed.seedLicenseServerPublicKeyFile, effectiveOptions.licenseServerPublicKeyFile);
|
|
163
|
+
effectiveOptions.licenseKey = trim(installSeed.licenseKey, effectiveOptions.licenseKey);
|
|
164
|
+
effectiveOptions.deferLicenseActivation = Boolean(installSeed.deferLicenseActivation || effectiveOptions.deferLicenseActivation);
|
|
165
|
+
}
|
|
166
|
+
const authToken = trim(effectiveOptions.token, defaults.authToken);
|
|
64
167
|
const tempDir = await fsPromises.mkdtemp(pathModule.join(osModule.tmpdir(), 'codexbot-npm-'));
|
|
65
168
|
const tempInstallPath = pathModule.join(tempDir, 'install-remote.sh');
|
|
66
169
|
const tempKeyPath = pathModule.join(tempDir, 'update-public.pem');
|
|
67
170
|
const headers = authToken ? { Authorization: `Bearer ${authToken}` } : {};
|
|
68
171
|
try {
|
|
69
172
|
logger.log(`==> Fetch install entry from ${urls.installRemoteUrl}`);
|
|
70
|
-
const installResponse = await ensureOkResponse(await fetchFn(urls.installRemoteUrl, { headers }),
|
|
173
|
+
const installResponse = await ensureOkResponse(await fetchFn(urls.installRemoteUrl, { headers }), {
|
|
174
|
+
label: 'install-remote',
|
|
175
|
+
installBaseUrl: urls.installBaseUrl,
|
|
176
|
+
channel: urls.channel,
|
|
177
|
+
updateBaseUrl: urls.updateBaseUrl,
|
|
178
|
+
authTokenPresent: Boolean(authToken),
|
|
179
|
+
distributionMode,
|
|
180
|
+
});
|
|
71
181
|
await writeResponseToFile(installResponse, tempInstallPath, fsPromises);
|
|
72
182
|
await fsPromises.chmod(tempInstallPath, 0o755);
|
|
73
183
|
|
|
74
|
-
const publicKeyFile = trim(
|
|
184
|
+
const publicKeyFile = trim(effectiveOptions.publicKeyFile, '');
|
|
75
185
|
const resolvedPublicKeyFile = publicKeyFile || tempKeyPath;
|
|
76
186
|
if (!publicKeyFile) {
|
|
77
187
|
logger.log(`==> Fetch update public key from ${urls.publicKeyUrl}`);
|
|
78
|
-
const keyResponse = await ensureOkResponse(await fetchFn(urls.publicKeyUrl, { headers }),
|
|
188
|
+
const keyResponse = await ensureOkResponse(await fetchFn(urls.publicKeyUrl, { headers }), {
|
|
189
|
+
label: 'update public key',
|
|
190
|
+
installBaseUrl: urls.installBaseUrl,
|
|
191
|
+
channel: urls.channel,
|
|
192
|
+
updateBaseUrl: urls.updateBaseUrl,
|
|
193
|
+
authTokenPresent: Boolean(authToken),
|
|
194
|
+
distributionMode,
|
|
195
|
+
});
|
|
79
196
|
await writeResponseToFile(keyResponse, tempKeyPath, fsPromises);
|
|
80
197
|
}
|
|
81
198
|
|
|
82
199
|
const args = [
|
|
83
200
|
tempInstallPath,
|
|
84
201
|
'--manifest-url', urls.manifestUrl,
|
|
202
|
+
...(distributionMode === 'install'
|
|
203
|
+
? ['--runtime-remote-update-manifest-url', urls.runtimeManifestUrl]
|
|
204
|
+
: []),
|
|
205
|
+
...(distributionMode === 'install'
|
|
206
|
+
? ['--package-download-base-url', urls.packageDownloadBaseUrl]
|
|
207
|
+
: []),
|
|
85
208
|
'--public-key-file', resolvedPublicKeyFile,
|
|
86
209
|
'--channel', urls.channel,
|
|
87
210
|
'--mode', trim(mode, 'auto'),
|
|
88
211
|
...(authToken ? ['--auth-token', authToken] : []),
|
|
89
|
-
...(
|
|
212
|
+
...(effectiveOptions.deferLicenseActivation ? ['--defer-license-activation'] : []),
|
|
213
|
+
...(trim(effectiveOptions.seedEnvFile) ? ['--seed-env-file', trim(effectiveOptions.seedEnvFile)] : []),
|
|
214
|
+
...(trim(effectiveOptions.seedConnectorsStateFile) ? ['--seed-connectors-state-file', trim(effectiveOptions.seedConnectorsStateFile)] : []),
|
|
215
|
+
...(trim(effectiveOptions.seedConnectorsCredentialsFile) ? ['--seed-connectors-credentials-file', trim(effectiveOptions.seedConnectorsCredentialsFile)] : []),
|
|
216
|
+
...(trim(effectiveOptions.licenseFile) ? ['--seed-license-file', trim(effectiveOptions.licenseFile)] : []),
|
|
217
|
+
...(trim(effectiveOptions.licensePublicKeyFile) ? ['--seed-license-public-key-file', trim(effectiveOptions.licensePublicKeyFile)] : []),
|
|
218
|
+
...(trim(effectiveOptions.licenseServerPublicKeyFile) ? ['--seed-license-server-public-key-file', trim(effectiveOptions.licenseServerPublicKeyFile)] : []),
|
|
219
|
+
...(Array.isArray(effectiveOptions.forwardedArgs) ? effectiveOptions.forwardedArgs : []),
|
|
90
220
|
];
|
|
91
221
|
const isRoot = typeof processImpl.getuid === 'function' ? processImpl.getuid() === 0 : false;
|
|
92
222
|
const command = isRoot ? 'bash' : 'sudo';
|
|
93
223
|
const commandArgs = isRoot ? args : ['bash', ...args];
|
|
94
|
-
|
|
224
|
+
const exitCode = await runChild(spawnFn, command, commandArgs);
|
|
225
|
+
if (distributionMode === 'install' && exitCode === 0 && trim(effectiveOptions.licenseKey)) {
|
|
226
|
+
await activationService.claimAndActivate({
|
|
227
|
+
licenseKey: effectiveOptions.licenseKey,
|
|
228
|
+
installBaseUrl: urls.installBaseUrl,
|
|
229
|
+
licenseClaimUrl: effectiveOptions.licenseClaimUrl,
|
|
230
|
+
allowHttp: Boolean(effectiveOptions.allowHttp),
|
|
231
|
+
nonInteractive: true,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return exitCode;
|
|
95
235
|
} finally {
|
|
236
|
+
if (installSeed?.cleanup) {
|
|
237
|
+
await installSeed.cleanup().catch(() => {});
|
|
238
|
+
}
|
|
96
239
|
await fsPromises.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
97
240
|
}
|
|
98
241
|
}
|