@afffun/codexbot 1.0.71

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/README.md ADDED
@@ -0,0 +1,14 @@
1
+ # Codexbot npm CLI
2
+
3
+ This package is the thin npm-distributed entrypoint for Codexbot.
4
+
5
+ It does not embed the full runtime payload, systemd units, or native sidecars.
6
+ Instead it exposes explicit commands such as:
7
+
8
+ - `codexbot install`
9
+ - `codexbot upgrade`
10
+ - `codexbot auth status`
11
+ - `codexbot auth login`
12
+
13
+ `install` and `upgrade` fetch the signed release package through the existing
14
+ Codexbot update pipeline, then apply it with the official installer.
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runCodexbotCli } from '../src/entrypoint.mjs';
4
+
5
+ const exitCode = await runCodexbotCli(process.argv.slice(2));
6
+ process.exit(exitCode);
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@afffun/codexbot",
3
+ "version": "1.0.71",
4
+ "description": "Thin npm bootstrap CLI for installing and operating Codexbot nodes",
5
+ "type": "module",
6
+ "author": "john88188 <john88188@outlook.com>",
7
+ "license": "ISC",
8
+ "bin": {
9
+ "codexbot": "./bin/codexbot.mjs"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "files": [
15
+ "bin/",
16
+ "src/",
17
+ "README.md"
18
+ ],
19
+ "engines": {
20
+ "node": ">=20"
21
+ }
22
+ }
@@ -0,0 +1,147 @@
1
+ function trim(value) {
2
+ return String(value ?? '').trim();
3
+ }
4
+
5
+ function readOptionValue(argv, index, name) {
6
+ if (index + 1 >= argv.length) {
7
+ throw new Error(`missing value for ${name}`);
8
+ }
9
+ return String(argv[index + 1] || '').trim();
10
+ }
11
+
12
+ function parseBootstrapOptions(argv = []) {
13
+ const options = {
14
+ channel: '',
15
+ updateBaseUrl: '',
16
+ manifestUrl: '',
17
+ publicKeyUrl: '',
18
+ installRemoteUrl: '',
19
+ publicKeyFile: '',
20
+ token: '',
21
+ allowHttp: false,
22
+ noSignature: false,
23
+ forwardedArgs: [],
24
+ };
25
+
26
+ for (let i = 0; i < argv.length; i += 1) {
27
+ const token = trim(argv[i]);
28
+ if (!token) continue;
29
+ if (token === '--channel') {
30
+ options.channel = readOptionValue(argv, i, token);
31
+ i += 1;
32
+ continue;
33
+ }
34
+ if (token === '--update-base-url') {
35
+ options.updateBaseUrl = readOptionValue(argv, i, token);
36
+ i += 1;
37
+ continue;
38
+ }
39
+ if (token === '--manifest-url') {
40
+ options.manifestUrl = readOptionValue(argv, i, token);
41
+ i += 1;
42
+ continue;
43
+ }
44
+ if (token === '--public-key-url') {
45
+ options.publicKeyUrl = readOptionValue(argv, i, token);
46
+ i += 1;
47
+ continue;
48
+ }
49
+ if (token === '--install-remote-url') {
50
+ options.installRemoteUrl = readOptionValue(argv, i, token);
51
+ i += 1;
52
+ continue;
53
+ }
54
+ if (token === '--public-key-file') {
55
+ options.publicKeyFile = readOptionValue(argv, i, token);
56
+ i += 1;
57
+ continue;
58
+ }
59
+ if (token === '--token' || token === '--auth-token') {
60
+ options.token = readOptionValue(argv, i, token);
61
+ i += 1;
62
+ continue;
63
+ }
64
+ if (token === '--allow-http') {
65
+ options.allowHttp = true;
66
+ options.forwardedArgs.push(token);
67
+ continue;
68
+ }
69
+ if (token === '--no-signature') {
70
+ options.noSignature = true;
71
+ options.forwardedArgs.push(token);
72
+ continue;
73
+ }
74
+ options.forwardedArgs.push(token);
75
+ }
76
+
77
+ return options;
78
+ }
79
+
80
+ function parseAuthOptions(argv = []) {
81
+ const options = {
82
+ layoutProfile: '',
83
+ appDir: '',
84
+ controlEnvFile: '',
85
+ controlUser: '',
86
+ codexHomeDir: '',
87
+ };
88
+ for (let i = 0; i < argv.length; i += 1) {
89
+ const token = trim(argv[i]);
90
+ if (!token) continue;
91
+ if (token === '--layout-profile') {
92
+ options.layoutProfile = readOptionValue(argv, i, token);
93
+ i += 1;
94
+ continue;
95
+ }
96
+ if (token === '--app-dir') {
97
+ options.appDir = readOptionValue(argv, i, token);
98
+ i += 1;
99
+ continue;
100
+ }
101
+ if (token === '--control-env-file') {
102
+ options.controlEnvFile = readOptionValue(argv, i, token);
103
+ i += 1;
104
+ continue;
105
+ }
106
+ if (token === '--control-user') {
107
+ options.controlUser = readOptionValue(argv, i, token);
108
+ i += 1;
109
+ continue;
110
+ }
111
+ if (token === '--codex-home-dir') {
112
+ options.codexHomeDir = readOptionValue(argv, i, token);
113
+ i += 1;
114
+ continue;
115
+ }
116
+ throw new Error(`unknown auth arg: ${token}`);
117
+ }
118
+ return options;
119
+ }
120
+
121
+ export function parseCliArgs(argv = []) {
122
+ const args = Array.isArray(argv) ? argv.map((item) => String(item || '')) : [];
123
+ const command = trim(args[0]).toLowerCase();
124
+ if (!command || command === 'help' || command === '--help' || command === '-h') {
125
+ return { action: 'help' };
126
+ }
127
+ if (command === 'version' || command === '--version' || command === '-v') {
128
+ return { action: 'version' };
129
+ }
130
+ if (command === 'doctor') {
131
+ return { action: 'doctor', options: parseAuthOptions(args.slice(1)) };
132
+ }
133
+ if (command === 'install') {
134
+ return { action: 'install', options: parseBootstrapOptions(args.slice(1)) };
135
+ }
136
+ if (command === 'upgrade') {
137
+ return { action: 'upgrade', options: parseBootstrapOptions(args.slice(1)) };
138
+ }
139
+ if (command === 'auth') {
140
+ const sub = trim(args[1]).toLowerCase() || 'status';
141
+ if (!['status', 'login', 'login-link', 'logout'].includes(sub)) {
142
+ throw new Error(`unknown auth subcommand: ${sub}`);
143
+ }
144
+ return { action: 'auth', mode: sub, options: parseAuthOptions(args.slice(2)) };
145
+ }
146
+ throw new Error(`unknown command: ${command}`);
147
+ }
@@ -0,0 +1,111 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawn } from 'node:child_process';
4
+ import { resolveInstalledControlLayout } from './installed_layout_service.mjs';
5
+
6
+ function runChild(spawnFn, command, args, options = {}) {
7
+ return new Promise((resolve, reject) => {
8
+ const child = spawnFn(command, args, {
9
+ stdio: 'inherit',
10
+ ...options,
11
+ });
12
+ child.on('error', reject);
13
+ child.on('exit', (code, signal) => {
14
+ if (signal) {
15
+ reject(new Error(`${command} exited via signal ${signal}`));
16
+ return;
17
+ }
18
+ resolve(Number(code || 0));
19
+ });
20
+ });
21
+ }
22
+
23
+ function trim(value, fallback = '') {
24
+ const text = String(value ?? '').trim();
25
+ return text || String(fallback ?? '').trim();
26
+ }
27
+
28
+ function mapAuthModeToCodexArgs(mode) {
29
+ if (mode === 'status') return ['login', 'status'];
30
+ if (mode === 'login') return ['login', '--device-auth'];
31
+ if (mode === 'login-link') return ['login'];
32
+ if (mode === 'logout') return ['logout'];
33
+ throw new Error(`unsupported auth mode: ${mode}`);
34
+ }
35
+
36
+ export function createNpmDistributionAuthService(deps = {}) {
37
+ const fsSync = deps.fsSync || fs;
38
+ const pathModule = deps.pathModule || path;
39
+ const spawnFn = deps.spawnFn || spawn;
40
+ const processImpl = deps.processImpl || process;
41
+ const logger = deps.logger || console;
42
+
43
+ function resolveControlLayout(options = {}) {
44
+ const layout = resolveInstalledControlLayout({
45
+ fsSync,
46
+ pathModule,
47
+ env: processImpl.env,
48
+ profile: options.layoutProfile,
49
+ appDir: options.appDir,
50
+ controlEnvFile: options.controlEnvFile,
51
+ controlUser: options.controlUser,
52
+ codexHomeDir: options.codexHomeDir,
53
+ });
54
+ if (!fsSync.existsSync(layout.codexBinPath)) {
55
+ throw new Error(`installed codex binary not found: ${layout.codexBinPath}`);
56
+ }
57
+ return layout;
58
+ }
59
+
60
+ async function runAuthCommand(mode, options = {}) {
61
+ const layout = resolveControlLayout(options);
62
+ const codexArgs = mapAuthModeToCodexArgs(mode);
63
+ const currentUser = trim(layout.currentUser, trim(processImpl.env.USER || processImpl.env.LOGNAME, ''));
64
+ const isRoot = typeof processImpl.getuid === 'function' ? processImpl.getuid() === 0 : false;
65
+ const envArgs = ['env', `CODEX_HOME=${layout.codexHomeDir}`, `PATH=${pathModule.dirname(layout.codexBinPath)}:${processImpl.env.PATH || ''}`, layout.codexBinPath, ...codexArgs];
66
+ logger.log(`==> Run Codex auth command for ${layout.profile} install as ${layout.controlUser}`);
67
+ if (isRoot) {
68
+ return runChild(spawnFn, 'sudo', ['-u', layout.controlUser, ...envArgs]);
69
+ }
70
+ if (currentUser && currentUser === layout.controlUser) {
71
+ return runChild(spawnFn, layout.codexBinPath, codexArgs, {
72
+ env: {
73
+ ...processImpl.env,
74
+ CODEX_HOME: layout.codexHomeDir,
75
+ PATH: `${pathModule.dirname(layout.codexBinPath)}:${processImpl.env.PATH || ''}`,
76
+ },
77
+ });
78
+ }
79
+ return runChild(spawnFn, 'sudo', ['-u', layout.controlUser, ...envArgs]);
80
+ }
81
+
82
+ function formatDoctorReport(options = {}) {
83
+ const layout = resolveControlLayout(options);
84
+ return [
85
+ `profile: ${layout.profile}`,
86
+ `app_dir: ${layout.appDir}`,
87
+ `control_env_file: ${layout.controlEnvFile}`,
88
+ `control_user: ${layout.controlUser}`,
89
+ `codex_home: ${layout.codexHomeDir}`,
90
+ `codex_bin: ${layout.codexBinPath}`,
91
+ ].join('\n');
92
+ }
93
+
94
+ function resolveInstalledVersion(options = {}) {
95
+ const layout = resolveControlLayout(options);
96
+ const packageJsonPath = pathModule.join(layout.appDir, 'package.json');
97
+ if (!fsSync.existsSync(packageJsonPath)) return '';
98
+ try {
99
+ const pkg = JSON.parse(fsSync.readFileSync(packageJsonPath, 'utf8'));
100
+ return String(pkg.version || '').trim();
101
+ } catch {
102
+ return '';
103
+ }
104
+ }
105
+
106
+ return {
107
+ runAuthCommand,
108
+ formatDoctorReport,
109
+ resolveInstalledVersion,
110
+ };
111
+ }
@@ -0,0 +1,62 @@
1
+ const DEFAULT_UPDATE_BASE_URL = 'https://codexbotupdate.afffun.com';
2
+ const DEFAULT_CHANNEL = 'stable';
3
+
4
+ function trim(value, fallback = '') {
5
+ const text = String(value ?? '').trim();
6
+ return text || String(fallback ?? '').trim();
7
+ }
8
+
9
+ function normalizeBaseUrl(value, fallback = DEFAULT_UPDATE_BASE_URL) {
10
+ return trim(value, fallback).replace(/\/+$/g, '');
11
+ }
12
+
13
+ function normalizeChannel(value, fallback = DEFAULT_CHANNEL) {
14
+ const text = trim(value, fallback);
15
+ if (!/^[a-zA-Z0-9._-]+$/.test(text)) {
16
+ throw new Error(`invalid channel: ${text}`);
17
+ }
18
+ return text;
19
+ }
20
+
21
+ function assertHttps(value, { allowHttp = false, label = 'url' } = {}) {
22
+ const url = new URL(String(value || '').trim());
23
+ if (!allowHttp && url.protocol !== 'https:') {
24
+ throw new Error(`${label} must use https unless --allow-http is set`);
25
+ }
26
+ return url.toString();
27
+ }
28
+
29
+ export function getDefaultDistributionConfig(env = process.env) {
30
+ return {
31
+ updateBaseUrl: normalizeBaseUrl(env.CODEXBOT_UPDATE_BASE_URL, DEFAULT_UPDATE_BASE_URL),
32
+ channel: normalizeChannel(env.CODEXBOT_INSTALL_CHANNEL, DEFAULT_CHANNEL),
33
+ authToken: trim(env.CODEXBOT_UPDATE_TOKEN || env.CODEX_REMOTE_UPDATE_AUTH_TOKEN, ''),
34
+ };
35
+ }
36
+
37
+ export function buildRemoteInstallUrls({
38
+ updateBaseUrl = DEFAULT_UPDATE_BASE_URL,
39
+ channel = DEFAULT_CHANNEL,
40
+ manifestUrl = '',
41
+ publicKeyUrl = '',
42
+ installRemoteUrl = '',
43
+ allowHttp = false,
44
+ } = {}) {
45
+ const resolvedBaseUrl = normalizeBaseUrl(updateBaseUrl, DEFAULT_UPDATE_BASE_URL);
46
+ const resolvedChannel = normalizeChannel(channel, DEFAULT_CHANNEL);
47
+ const root = `${resolvedBaseUrl}/v1/channels/${resolvedChannel}`;
48
+ const urls = {
49
+ updateBaseUrl: resolvedBaseUrl,
50
+ channel: resolvedChannel,
51
+ manifestUrl: trim(manifestUrl) || `${root}/manifest.json`,
52
+ publicKeyUrl: trim(publicKeyUrl) || `${root}/packages/latest/update-public.pem`,
53
+ installRemoteUrl: trim(installRemoteUrl) || `${root}/packages/latest/install-remote.sh`,
54
+ };
55
+ return {
56
+ updateBaseUrl: assertHttps(urls.updateBaseUrl, { allowHttp, label: 'update base url' }),
57
+ channel: urls.channel,
58
+ manifestUrl: assertHttps(urls.manifestUrl, { allowHttp, label: 'manifest url' }),
59
+ publicKeyUrl: assertHttps(urls.publicKeyUrl, { allowHttp, label: 'public key url' }),
60
+ installRemoteUrl: assertHttps(urls.installRemoteUrl, { allowHttp, label: 'install-remote url' }),
61
+ };
62
+ }
@@ -0,0 +1,78 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { parseCliArgs } from './args_service.mjs';
4
+ import { createNpmDistributionInstallService } from './install_service.mjs';
5
+ import { createNpmDistributionAuthService } from './auth_service.mjs';
6
+
7
+ function readPackageVersion() {
8
+ const packageJsonPath = new URL('../package.json', import.meta.url);
9
+ const raw = fs.readFileSync(packageJsonPath, 'utf8');
10
+ const pkg = JSON.parse(raw);
11
+ return String(pkg.version || '').trim();
12
+ }
13
+
14
+ function renderUsage() {
15
+ return [
16
+ 'Usage:',
17
+ ' codexbot install [bootstrap options] [install-remote flags...]',
18
+ ' codexbot upgrade [bootstrap options] [install-remote flags...]',
19
+ ' codexbot auth status|login|login-link|logout [layout overrides]',
20
+ ' codexbot doctor [layout overrides]',
21
+ ' codexbot version',
22
+ '',
23
+ 'Bootstrap options:',
24
+ ' --channel <stable|canary|...>',
25
+ ' --update-base-url <https://codexbotupdate.example.com>',
26
+ ' --manifest-url <url>',
27
+ ' --public-key-url <url>',
28
+ ' --install-remote-url <url>',
29
+ ' --public-key-file </path/to/update-public.pem>',
30
+ ' --token <update server token>',
31
+ ' --allow-http',
32
+ '',
33
+ 'Layout overrides:',
34
+ ' --layout-profile <split|legacy>',
35
+ ' --app-dir <path>',
36
+ ' --control-env-file <path>',
37
+ ' --control-user <user>',
38
+ ' --codex-home-dir <path>',
39
+ ].join('\n');
40
+ }
41
+
42
+ export async function runCodexbotCli(argv = [], deps = {}) {
43
+ const logger = deps.logger || console;
44
+ const installService = deps.installService || createNpmDistributionInstallService({ logger });
45
+ const authService = deps.authService || createNpmDistributionAuthService({ logger });
46
+ const cli = parseCliArgs(argv);
47
+ if (cli.action === 'help') {
48
+ logger.log(renderUsage());
49
+ return 0;
50
+ }
51
+ if (cli.action === 'version') {
52
+ const packageVersion = readPackageVersion();
53
+ let installedVersion = '';
54
+ try {
55
+ installedVersion = authService.resolveInstalledVersion({});
56
+ } catch {
57
+ installedVersion = '';
58
+ }
59
+ logger.log(installedVersion
60
+ ? `codexbot npm cli ${packageVersion}\ninstalled runtime ${installedVersion}`
61
+ : `codexbot npm cli ${packageVersion}`);
62
+ return 0;
63
+ }
64
+ if (cli.action === 'doctor') {
65
+ logger.log(authService.formatDoctorReport(cli.options || {}));
66
+ return 0;
67
+ }
68
+ if (cli.action === 'install') {
69
+ return installService.runInstallCommand(cli.options || {});
70
+ }
71
+ if (cli.action === 'upgrade') {
72
+ return installService.runUpgradeCommand(cli.options || {});
73
+ }
74
+ if (cli.action === 'auth') {
75
+ return authService.runAuthCommand(cli.mode, cli.options || {});
76
+ }
77
+ throw new Error(`unsupported action: ${cli.action}`);
78
+ }
@@ -0,0 +1,26 @@
1
+ function stripWrappingQuotes(value) {
2
+ const text = String(value ?? '');
3
+ if (text.length >= 2) {
4
+ const first = text[0];
5
+ const last = text[text.length - 1];
6
+ if ((first === '"' && last === '"') || (first === '\'' && last === '\'')) {
7
+ return text.slice(1, -1);
8
+ }
9
+ }
10
+ return text;
11
+ }
12
+
13
+ export function parseEnvText(text = '') {
14
+ const out = {};
15
+ for (const rawLine of String(text || '').split(/\r?\n/)) {
16
+ const line = rawLine.trim();
17
+ if (!line || line.startsWith('#')) continue;
18
+ const index = line.indexOf('=');
19
+ if (index <= 0) continue;
20
+ const key = line.slice(0, index).trim();
21
+ if (!key) continue;
22
+ const value = stripWrappingQuotes(line.slice(index + 1).trim());
23
+ out[key] = value;
24
+ }
25
+ return out;
26
+ }
@@ -0,0 +1,108 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { spawn } from 'node:child_process';
5
+ import { buildRemoteInstallUrls, getDefaultDistributionConfig } from './config_service.mjs';
6
+
7
+ function trim(value, fallback = '') {
8
+ const text = String(value ?? '').trim();
9
+ return text || String(fallback ?? '').trim();
10
+ }
11
+
12
+ async function ensureOkResponse(response, label) {
13
+ if (!response.ok) {
14
+ throw new Error(`${label} download failed: ${response.status} ${response.statusText}`);
15
+ }
16
+ return response;
17
+ }
18
+
19
+ async function writeResponseToFile(response, filePath, fsPromises) {
20
+ const buffer = Buffer.from(await response.arrayBuffer());
21
+ await fsPromises.writeFile(filePath, buffer);
22
+ }
23
+
24
+ function runChild(spawnFn, command, args, options = {}) {
25
+ return new Promise((resolve, reject) => {
26
+ const child = spawnFn(command, args, {
27
+ stdio: 'inherit',
28
+ ...options,
29
+ });
30
+ child.on('error', reject);
31
+ child.on('exit', (code, signal) => {
32
+ if (signal) {
33
+ reject(new Error(`${command} exited via signal ${signal}`));
34
+ return;
35
+ }
36
+ resolve(Number(code || 0));
37
+ });
38
+ });
39
+ }
40
+
41
+ export function createNpmDistributionInstallService(deps = {}) {
42
+ const fetchFn = deps.fetchFn || fetch;
43
+ const fsPromises = deps.fsPromises || fs;
44
+ const osModule = deps.osModule || os;
45
+ const pathModule = deps.pathModule || path;
46
+ const spawnFn = deps.spawnFn || spawn;
47
+ const processImpl = deps.processImpl || process;
48
+ const logger = deps.logger || console;
49
+
50
+ async function runRemoteInstall({
51
+ mode,
52
+ options = {},
53
+ } = {}) {
54
+ 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);
64
+ const tempDir = await fsPromises.mkdtemp(pathModule.join(osModule.tmpdir(), 'codexbot-npm-'));
65
+ const tempInstallPath = pathModule.join(tempDir, 'install-remote.sh');
66
+ const tempKeyPath = pathModule.join(tempDir, 'update-public.pem');
67
+ const headers = authToken ? { Authorization: `Bearer ${authToken}` } : {};
68
+ try {
69
+ logger.log(`==> Fetch install entry from ${urls.installRemoteUrl}`);
70
+ const installResponse = await ensureOkResponse(await fetchFn(urls.installRemoteUrl, { headers }), 'install-remote');
71
+ await writeResponseToFile(installResponse, tempInstallPath, fsPromises);
72
+ await fsPromises.chmod(tempInstallPath, 0o755);
73
+
74
+ const publicKeyFile = trim(options.publicKeyFile, '');
75
+ const resolvedPublicKeyFile = publicKeyFile || tempKeyPath;
76
+ if (!publicKeyFile) {
77
+ logger.log(`==> Fetch update public key from ${urls.publicKeyUrl}`);
78
+ const keyResponse = await ensureOkResponse(await fetchFn(urls.publicKeyUrl, { headers }), 'update public key');
79
+ await writeResponseToFile(keyResponse, tempKeyPath, fsPromises);
80
+ }
81
+
82
+ const args = [
83
+ tempInstallPath,
84
+ '--manifest-url', urls.manifestUrl,
85
+ '--public-key-file', resolvedPublicKeyFile,
86
+ '--channel', urls.channel,
87
+ '--mode', trim(mode, 'auto'),
88
+ ...(authToken ? ['--auth-token', authToken] : []),
89
+ ...(Array.isArray(options.forwardedArgs) ? options.forwardedArgs : []),
90
+ ];
91
+ const isRoot = typeof processImpl.getuid === 'function' ? processImpl.getuid() === 0 : false;
92
+ const command = isRoot ? 'bash' : 'sudo';
93
+ const commandArgs = isRoot ? args : ['bash', ...args];
94
+ return await runChild(spawnFn, command, commandArgs);
95
+ } finally {
96
+ await fsPromises.rm(tempDir, { recursive: true, force: true }).catch(() => {});
97
+ }
98
+ }
99
+
100
+ return {
101
+ runInstallCommand(options = {}) {
102
+ return runRemoteInstall({ mode: 'install', options });
103
+ },
104
+ runUpgradeCommand(options = {}) {
105
+ return runRemoteInstall({ mode: 'upgrade', options });
106
+ },
107
+ };
108
+ }
@@ -0,0 +1,86 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { parseEnvText } from './env_file_service.mjs';
4
+
5
+ const SPLIT_DEFAULTS = Object.freeze({
6
+ profile: 'split',
7
+ appDir: '/usr/local/lib/codexbot',
8
+ controlEnvFile: '/etc/codexbot/control.env',
9
+ controlUser: 'codexbotd',
10
+ codexHomeDir: '/var/lib/codexbot/control/home/.codex',
11
+ });
12
+
13
+ const LEGACY_DEFAULTS = Object.freeze({
14
+ profile: 'legacy',
15
+ appDir: '/home/codexbot/codexbot-telegram',
16
+ controlEnvFile: '/home/codexbot/codexbot-telegram/.env',
17
+ controlUser: 'codexbot',
18
+ codexHomeDir: '/home/codexbot/codexbot-home/.codex',
19
+ });
20
+
21
+ function trim(value, fallback = '') {
22
+ const text = String(value ?? '').trim();
23
+ return text || String(fallback ?? '').trim();
24
+ }
25
+
26
+ function exists(fsSync, target) {
27
+ try {
28
+ return fsSync.existsSync(target);
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ function readEnvFile(fsSync, filePath) {
35
+ try {
36
+ if (!exists(fsSync, filePath)) return {};
37
+ return parseEnvText(fsSync.readFileSync(filePath, 'utf8'));
38
+ } catch {
39
+ return {};
40
+ }
41
+ }
42
+
43
+ function resolveProfile(fsSync, explicitProfile, explicitEnvFile) {
44
+ const profile = trim(explicitProfile).toLowerCase();
45
+ if (profile === 'split' || profile === 'legacy') return profile;
46
+ if (trim(explicitEnvFile).endsWith('/control.env')) return 'split';
47
+ if (trim(explicitEnvFile).endsWith('/.env')) return 'legacy';
48
+ if (exists(fsSync, SPLIT_DEFAULTS.controlEnvFile) || exists(fsSync, SPLIT_DEFAULTS.appDir)) return 'split';
49
+ return 'legacy';
50
+ }
51
+
52
+ export function resolveInstalledControlLayout({
53
+ fsSync = fs,
54
+ pathModule = path,
55
+ env = process.env,
56
+ profile = '',
57
+ appDir = '',
58
+ controlEnvFile = '',
59
+ controlUser = '',
60
+ codexHomeDir = '',
61
+ } = {}) {
62
+ const resolvedProfile = resolveProfile(fsSync, profile, controlEnvFile);
63
+ const defaults = resolvedProfile === 'split' ? SPLIT_DEFAULTS : LEGACY_DEFAULTS;
64
+ const envFile = trim(controlEnvFile, defaults.controlEnvFile);
65
+ const fileEnv = readEnvFile(fsSync, envFile);
66
+ const resolvedAppDir = pathModule.resolve(trim(appDir || fileEnv.CODEXBOT_APP_DIR || fileEnv.CODEXBOT_SYSTEM_ROOT, defaults.appDir));
67
+ const resolvedControlUser = trim(controlUser || fileEnv.CODEXBOT_CONTROL_USER, defaults.controlUser);
68
+ const resolvedCodexHomeDir = pathModule.resolve(trim(
69
+ codexHomeDir
70
+ || fileEnv.CODEXBOT_CONTROL_CODEX_HOME_DIR
71
+ || fileEnv.CODEX_HOME
72
+ || fileEnv.CODEXBOT_CONTROL_HOME && `${fileEnv.CODEXBOT_CONTROL_HOME}/.codex`,
73
+ defaults.codexHomeDir,
74
+ ));
75
+ const codexBinPath = pathModule.join(resolvedAppDir, 'node_modules', '.bin', 'codex');
76
+ return {
77
+ profile: resolvedProfile,
78
+ appDir: resolvedAppDir,
79
+ controlEnvFile: envFile,
80
+ controlUser: resolvedControlUser,
81
+ codexHomeDir: resolvedCodexHomeDir,
82
+ codexBinPath,
83
+ env: fileEnv,
84
+ currentUser: trim(env.USER || env.LOGNAME || ''),
85
+ };
86
+ }