@token-dashboard/codex-usage-uploader 0.1.0

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/src/install.js ADDED
@@ -0,0 +1,101 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { CLI_NAME } from './constants.js';
6
+ import { saveRuntimeConfig } from './runtime-config.js';
7
+
8
+ export function findPackageRoot(startUrl = import.meta.url) {
9
+ let current = path.dirname(fileURLToPath(startUrl));
10
+ while (true) {
11
+ const candidate = path.join(current, 'package.json');
12
+ if (fs.existsSync(candidate)) {
13
+ return current;
14
+ }
15
+ const parent = path.dirname(current);
16
+ if (parent === current) {
17
+ throw new Error('package.json not found from current runtime');
18
+ }
19
+ current = parent;
20
+ }
21
+ }
22
+
23
+ export function resolveSelfPackageSpec(packageRoot, explicitSpec) {
24
+ if (explicitSpec) return explicitSpec;
25
+ return `file:${packageRoot}`;
26
+ }
27
+
28
+ export function installCurrentPackage(runtime, { packageRoot, packageSpec, nodePath = process.execPath } = {}) {
29
+ const resolvedSpec = resolveSelfPackageSpec(packageRoot, packageSpec);
30
+ const appRoot = runtime.appRoot;
31
+ const stagingDir = path.join(appRoot, `.staging-${Date.now()}`);
32
+ fs.mkdirSync(stagingDir, { recursive: true });
33
+ fs.writeFileSync(
34
+ path.join(stagingDir, 'package.json'),
35
+ `${JSON.stringify({ private: true, name: 'codex-usage-uploader-runtime' }, null, 2)}\n`,
36
+ );
37
+
38
+ const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
39
+ const result = spawnSync(
40
+ npmCommand,
41
+ ['install', '--no-save', '--omit=dev', '--no-package-lock', resolvedSpec],
42
+ {
43
+ cwd: stagingDir,
44
+ stdio: 'inherit',
45
+ env: { ...process.env, npm_config_fund: 'false', npm_config_audit: 'false' },
46
+ },
47
+ );
48
+ if (result.error) {
49
+ throw new Error(`failed to execute ${npmCommand}: ${result.error.message}`);
50
+ }
51
+ if (result.status !== 0) {
52
+ throw new Error(`npm install failed for ${resolvedSpec}`);
53
+ }
54
+
55
+ const sourcePkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
56
+ const installedPackageRoot = path.join(stagingDir, 'node_modules', ...String(sourcePkg.name).split('/'));
57
+ const installedPkg = JSON.parse(fs.readFileSync(path.join(installedPackageRoot, 'package.json'), 'utf8'));
58
+ const binField = installedPkg.bin;
59
+ const binRel = typeof binField === 'string'
60
+ ? binField
61
+ : binField?.[CLI_NAME] ?? Object.values(binField ?? {})[0];
62
+ if (!binRel) {
63
+ throw new Error('installed package does not expose the expected CLI binary');
64
+ }
65
+
66
+ const currentDir = runtime.currentAppDir;
67
+ fs.rmSync(currentDir, { recursive: true, force: true });
68
+ fs.mkdirSync(path.dirname(currentDir), { recursive: true });
69
+ fs.renameSync(stagingDir, currentDir);
70
+
71
+ runtime.nodePath = nodePath;
72
+ runtime.packageSpec = resolvedSpec;
73
+ runtime.entryFile = path.join(currentDir, 'node_modules', ...String(sourcePkg.name).split('/'), binRel);
74
+ writeLocalWrapper(runtime);
75
+ ensureHomeBinLink(runtime);
76
+ saveRuntimeConfig(runtime);
77
+ return runtime;
78
+ }
79
+
80
+ export function writeLocalWrapper(runtime) {
81
+ fs.mkdirSync(runtime.localBinDir, { recursive: true });
82
+ const script = `#!/bin/sh
83
+ set -eu
84
+ CONFIG_FILE="\${CODEX_USAGE_UPLOADER_CONFIG:-${runtime.configFile}}"
85
+ exec "${runtime.nodePath}" "${runtime.entryFile}" --config-file "$CONFIG_FILE" "$@"
86
+ `;
87
+ fs.writeFileSync(runtime.localBinPath, script, { mode: 0o755 });
88
+ fs.chmodSync(runtime.localBinPath, 0o755);
89
+ }
90
+
91
+ export function ensureHomeBinLink(runtime) {
92
+ fs.mkdirSync(path.dirname(runtime.homeBinLink), { recursive: true });
93
+ try {
94
+ if (fs.existsSync(runtime.homeBinLink) || fs.lstatSync(runtime.homeBinLink).isSymbolicLink()) {
95
+ fs.rmSync(runtime.homeBinLink, { force: true });
96
+ }
97
+ } catch {
98
+ // ignore
99
+ }
100
+ fs.symlinkSync(runtime.localBinPath, runtime.homeBinLink);
101
+ }
package/src/launchd.js ADDED
@@ -0,0 +1,151 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { spawnSync } from 'node:child_process';
4
+
5
+ export function buildLaunchdPlist(runtime) {
6
+ const args = [
7
+ runtime.nodePath,
8
+ runtime.entryFile,
9
+ '--config-file',
10
+ runtime.configFile,
11
+ 'run',
12
+ ];
13
+ return `<?xml version="1.0" encoding="UTF-8"?>
14
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
15
+ <plist version="1.0">
16
+ <dict>
17
+ <key>Label</key>
18
+ <string>${escapeXml(runtime.launchdLabel)}</string>
19
+ <key>ProgramArguments</key>
20
+ <array>
21
+ ${args.map((item) => ` <string>${escapeXml(item)}</string>`).join('\n')}
22
+ </array>
23
+ <key>RunAtLoad</key>
24
+ <true/>
25
+ <key>KeepAlive</key>
26
+ <true/>
27
+ <key>ProcessType</key>
28
+ <string>Background</string>
29
+ <key>WorkingDirectory</key>
30
+ <string>${escapeXml(runtime.installRoot)}</string>
31
+ <key>StandardOutPath</key>
32
+ <string>${escapeXml(runtime.stdoutLogPath)}</string>
33
+ <key>StandardErrorPath</key>
34
+ <string>${escapeXml(runtime.stderrLogPath)}</string>
35
+ <key>EnvironmentVariables</key>
36
+ <dict>
37
+ <key>NODE_NO_WARNINGS</key>
38
+ <string>1</string>
39
+ </dict>
40
+ </dict>
41
+ </plist>
42
+ `;
43
+ }
44
+
45
+ function escapeXml(value) {
46
+ return String(value)
47
+ .replaceAll('&', '&amp;')
48
+ .replaceAll('<', '&lt;')
49
+ .replaceAll('>', '&gt;')
50
+ .replaceAll('"', '&quot;')
51
+ .replaceAll("'", '&apos;');
52
+ }
53
+
54
+ export function parseLaunchctlPrint(output) {
55
+ const state = output.match(/^\s*state = ([^\n]+)$/m)?.[1]?.trim() ?? null;
56
+ const pidRaw = output.match(/^\s*pid = (\d+)$/m)?.[1] ?? null;
57
+ const exitRaw = output.match(/^\s*last exit code = (-?\d+)$/m)?.[1] ?? null;
58
+ return {
59
+ loaded: true,
60
+ running: state === 'running' || pidRaw != null,
61
+ pid: pidRaw ? Number(pidRaw) : null,
62
+ state,
63
+ lastExitCode: exitRaw ? Number(exitRaw) : null,
64
+ };
65
+ }
66
+
67
+ export class LaunchdServiceManager {
68
+ constructor(runtime) {
69
+ this.runtime = runtime;
70
+ }
71
+
72
+ ensureMacos() {
73
+ if (process.platform !== 'darwin') {
74
+ throw new Error('launchd management is only supported on macOS.');
75
+ }
76
+ }
77
+
78
+ domainTarget() {
79
+ return `gui/${process.getuid()}`;
80
+ }
81
+
82
+ serviceTarget() {
83
+ return `${this.domainTarget()}/${this.runtime.launchdLabel}`;
84
+ }
85
+
86
+ run(args, { check = true } = {}) {
87
+ const result = spawnSync(args[0], args.slice(1), { encoding: 'utf8' });
88
+ if (check && result.status !== 0) {
89
+ throw new Error(result.stderr?.trim() || result.stdout?.trim() || `command failed: ${args.join(' ')}`);
90
+ }
91
+ return result;
92
+ }
93
+
94
+ ensurePlist() {
95
+ this.ensureMacos();
96
+ if (!this.runtime.entryFile || !fs.existsSync(this.runtime.entryFile)) {
97
+ throw new Error(`installed runtime entry not found: ${this.runtime.entryFile || '(empty)'}`);
98
+ }
99
+ fs.mkdirSync(path.dirname(this.runtime.plistPath), { recursive: true });
100
+ fs.mkdirSync(path.dirname(this.runtime.stdoutLogPath), { recursive: true });
101
+ fs.writeFileSync(this.runtime.plistPath, buildLaunchdPlist(this.runtime));
102
+ }
103
+
104
+ start() {
105
+ this.ensurePlist();
106
+ this.stop();
107
+ this.run(['launchctl', 'bootstrap', this.domainTarget(), this.runtime.plistPath]);
108
+ this.run(['launchctl', 'kickstart', '-k', this.serviceTarget()], { check: false });
109
+ }
110
+
111
+ stop() {
112
+ this.ensureMacos();
113
+ this.run(['launchctl', 'bootout', this.domainTarget(), this.runtime.plistPath], { check: false });
114
+ }
115
+
116
+ restart() {
117
+ this.start();
118
+ }
119
+
120
+ status() {
121
+ this.ensureMacos();
122
+ const result = this.run(['launchctl', 'print', this.serviceTarget()], { check: false });
123
+ if (result.status !== 0) {
124
+ return {
125
+ loaded: false,
126
+ running: false,
127
+ pid: null,
128
+ state: null,
129
+ lastExitCode: null,
130
+ };
131
+ }
132
+ return parseLaunchctlPrint(result.stdout);
133
+ }
134
+
135
+ uninstall() {
136
+ this.stop();
137
+ if (fs.existsSync(this.runtime.plistPath)) {
138
+ fs.unlinkSync(this.runtime.plistPath);
139
+ }
140
+ if (fs.existsSync(this.runtime.homeBinLink) && fs.lstatSync(this.runtime.homeBinLink).isSymbolicLink()) {
141
+ try {
142
+ const target = fs.realpathSync.native(this.runtime.homeBinLink);
143
+ if (target === this.runtime.localBinPath) {
144
+ fs.unlinkSync(this.runtime.homeBinLink);
145
+ }
146
+ } catch {
147
+ fs.unlinkSync(this.runtime.homeBinLink);
148
+ }
149
+ }
150
+ }
151
+ }
package/src/parser.js ADDED
@@ -0,0 +1,180 @@
1
+ import { computeEventUid, deriveRepoName, isoToEpochMs, stableStringify } from './utils.js';
2
+
3
+ export class RolloutParser {
4
+ constructor(collectorIdentity, relpath, initialState = {}) {
5
+ this.collectorIdentity = collectorIdentity;
6
+ this.relpath = relpath;
7
+ this.currentSession = initialState.current_session ?? null;
8
+ this.currentTurn = initialState.current_turn ?? null;
9
+ }
10
+
11
+ exportState() {
12
+ return {
13
+ current_session: this.currentSession,
14
+ current_turn: this.currentTurn,
15
+ };
16
+ }
17
+
18
+ normalizeSession(payload) {
19
+ if (!payload?.id) return null;
20
+ const git = payload.git && typeof payload.git === 'object' ? payload.git : {};
21
+ const cwd = payload.cwd ?? null;
22
+ const repositoryUrl = git.repository_url ?? null;
23
+ return {
24
+ sessionId: String(payload.id),
25
+ sessionTimestamp: isoToEpochMs(payload.timestamp),
26
+ cwd,
27
+ originator: payload.originator ?? null,
28
+ source: payload.source ?? null,
29
+ cliVersion: payload.cli_version ?? null,
30
+ modelProvider: payload.model_provider ?? null,
31
+ repoName: deriveRepoName(repositoryUrl, cwd),
32
+ gitBranch: git.branch ?? null,
33
+ gitCommitHash: git.commit_hash ?? null,
34
+ repositoryUrl,
35
+ };
36
+ }
37
+
38
+ normalizeTurn(payload, timestampMs) {
39
+ const turnId = payload?.turn_id;
40
+ const sessionId = this.currentSession?.sessionId;
41
+ if (!turnId || !sessionId) return null;
42
+ const sandbox = payload.sandbox_policy && typeof payload.sandbox_policy === 'object'
43
+ ? payload.sandbox_policy
44
+ : {};
45
+ const writableRoots = Array.isArray(sandbox.writable_roots) ? sandbox.writable_roots : null;
46
+ const collaborationMode = payload.collaboration_mode && typeof payload.collaboration_mode === 'object'
47
+ ? payload.collaboration_mode.mode
48
+ : null;
49
+ return {
50
+ turnId: String(turnId),
51
+ sessionId: String(sessionId),
52
+ eventTimestamp: timestampMs,
53
+ cwd: payload.cwd ?? null,
54
+ currentDate: payload.current_date ?? null,
55
+ timezone: payload.timezone ?? null,
56
+ approvalPolicy: payload.approval_policy ?? null,
57
+ sandboxPolicyType: sandbox.type ?? null,
58
+ sandboxNetworkAccess: sandbox.network_access ?? null,
59
+ sandboxWritableRootsJson: writableRoots ? stableStringify(writableRoots) : null,
60
+ sandboxPolicyJson: Object.keys(sandbox).length ? stableStringify(sandbox) : null,
61
+ model: payload.model ?? null,
62
+ personality: payload.personality ?? null,
63
+ collaborationMode: collaborationMode ?? null,
64
+ effort: payload.effort ?? null,
65
+ summary: payload.summary ?? null,
66
+ truncationPolicyJson: payload.truncation_policy != null
67
+ ? stableStringify(payload.truncation_policy)
68
+ : null,
69
+ };
70
+ }
71
+
72
+ normalizeTokenEvent(payload, timestampMs, lineNo) {
73
+ if (!this.currentSession || timestampMs == null) return null;
74
+ const info = payload?.info;
75
+ if (!info || typeof info !== 'object') return null;
76
+ const totalUsage = info.total_token_usage && typeof info.total_token_usage === 'object'
77
+ ? info.total_token_usage
78
+ : {};
79
+ const lastUsage = info.last_token_usage && typeof info.last_token_usage === 'object'
80
+ ? info.last_token_usage
81
+ : {};
82
+ const rateLimits = payload.rate_limits && typeof payload.rate_limits === 'object'
83
+ ? payload.rate_limits
84
+ : {};
85
+ const primary = rateLimits.primary && typeof rateLimits.primary === 'object' ? rateLimits.primary : {};
86
+ const secondary = rateLimits.secondary && typeof rateLimits.secondary === 'object' ? rateLimits.secondary : {};
87
+ const session = this.currentSession;
88
+ const turn = this.currentTurn ?? {};
89
+ return {
90
+ eventUid: computeEventUid(this.collectorIdentity.collectorId, this.relpath, lineNo),
91
+ sessionId: session.sessionId,
92
+ turnId: turn.turnId ?? null,
93
+ sourceFileRelpath: this.relpath,
94
+ lineNo,
95
+ timestamp: timestampMs,
96
+ model: turn.model ?? null,
97
+ cwd: turn.cwd ?? session.cwd ?? null,
98
+ timezone: turn.timezone ?? null,
99
+ approvalPolicy: turn.approvalPolicy ?? null,
100
+ sandboxPolicyType: turn.sandboxPolicyType ?? null,
101
+ source: session.source ?? null,
102
+ originator: session.originator ?? null,
103
+ cliVersion: session.cliVersion ?? null,
104
+ repositoryUrl: session.repositoryUrl ?? null,
105
+ repoName: session.repoName ?? null,
106
+ gitBranch: session.gitBranch ?? null,
107
+ gitCommitHash: session.gitCommitHash ?? null,
108
+ totalInputTokens: totalUsage.input_tokens ?? null,
109
+ totalCachedInputTokens: totalUsage.cached_input_tokens ?? null,
110
+ totalOutputTokens: totalUsage.output_tokens ?? null,
111
+ totalReasoningOutputTokens: totalUsage.reasoning_output_tokens ?? null,
112
+ totalTokens: totalUsage.total_tokens ?? null,
113
+ lastInputTokens: lastUsage.input_tokens ?? null,
114
+ lastCachedInputTokens: lastUsage.cached_input_tokens ?? null,
115
+ lastOutputTokens: lastUsage.output_tokens ?? null,
116
+ lastReasoningOutputTokens: lastUsage.reasoning_output_tokens ?? null,
117
+ lastTotalTokens: lastUsage.total_tokens ?? null,
118
+ modelContextWindow: info.model_context_window ?? null,
119
+ rateLimitPlanType: rateLimits.plan_type ?? null,
120
+ primaryUsedPercent: primary.used_percent ?? null,
121
+ primaryWindowMinutes: primary.window_minutes ?? null,
122
+ primaryResetsAt: primary.resets_at ?? null,
123
+ secondaryUsedPercent: secondary.used_percent ?? null,
124
+ secondaryWindowMinutes: secondary.window_minutes ?? null,
125
+ secondaryResetsAt: secondary.resets_at ?? null,
126
+ credits: normalizeCredits(rateLimits.credits),
127
+ rawRateLimitsJson: Object.keys(rateLimits).length ? stableStringify(rateLimits) : null,
128
+ };
129
+ }
130
+
131
+ processLine(lineNo, line) {
132
+ let record;
133
+ try {
134
+ record = JSON.parse(line);
135
+ } catch {
136
+ return { sessions: [], turns: [], events: [] };
137
+ }
138
+
139
+ const recordType = record.type;
140
+ const payload = record.payload ?? {};
141
+ const timestampMs = isoToEpochMs(record.timestamp);
142
+ const sessions = [];
143
+ const turns = [];
144
+ const events = [];
145
+
146
+ if (recordType === 'session_meta') {
147
+ const normalized = this.normalizeSession(payload);
148
+ if (normalized) {
149
+ this.currentSession = normalized;
150
+ sessions.push(normalized);
151
+ }
152
+ } else if (recordType === 'turn_context') {
153
+ const normalized = this.normalizeTurn(payload, timestampMs);
154
+ if (normalized) {
155
+ this.currentTurn = normalized;
156
+ turns.push(normalized);
157
+ }
158
+ } else if (recordType === 'event_msg' && payload.type === 'token_count') {
159
+ const normalized = this.normalizeTokenEvent(payload, timestampMs, lineNo);
160
+ if (normalized) {
161
+ events.push(normalized);
162
+ }
163
+ }
164
+
165
+ return { sessions, turns, events };
166
+ }
167
+ }
168
+
169
+ function normalizeCredits(value) {
170
+ if (typeof value === 'number' && Number.isFinite(value)) {
171
+ return value;
172
+ }
173
+ if (value && typeof value === 'object') {
174
+ const balance = value.balance;
175
+ if (typeof balance === 'number' && Number.isFinite(balance)) {
176
+ return balance;
177
+ }
178
+ }
179
+ return null;
180
+ }
@@ -0,0 +1,142 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {
4
+ CLI_NAME,
5
+ DEFAULT_APP_ROOT,
6
+ DEFAULT_BACKEND_URL,
7
+ DEFAULT_CODEX_AUTH_PATH,
8
+ DEFAULT_CONFIG_FILE,
9
+ DEFAULT_CURRENT_APP_DIR,
10
+ DEFAULT_HOME_BIN_LINK,
11
+ DEFAULT_INSTALL_ROOT,
12
+ DEFAULT_LAUNCHD_LABEL,
13
+ DEFAULT_LOCAL_BIN_DIR,
14
+ DEFAULT_LOCAL_BIN_PATH,
15
+ DEFAULT_LOG_DIR,
16
+ DEFAULT_PLIST_PATH,
17
+ DEFAULT_SESSIONS_DIR,
18
+ DEFAULT_STATE_DB,
19
+ DEFAULT_STDERR_LOG_PATH,
20
+ DEFAULT_STDOUT_LOG_PATH,
21
+ STATUS_ONLINE_THRESHOLD_SECONDS,
22
+ } from './constants.js';
23
+ import { stableStringify } from './utils.js';
24
+
25
+ export function defaultRuntimeConfig(configFile = DEFAULT_CONFIG_FILE) {
26
+ const installRoot = path.dirname(configFile);
27
+ const appRoot = path.join(installRoot, 'app');
28
+ const currentAppDir = path.join(appRoot, 'current');
29
+ return {
30
+ configFile,
31
+ installRoot,
32
+ appRoot,
33
+ currentAppDir,
34
+ backendUrl: DEFAULT_BACKEND_URL,
35
+ intervalSeconds: 30,
36
+ codexAuthPath: DEFAULT_CODEX_AUTH_PATH,
37
+ sessionsDir: DEFAULT_SESSIONS_DIR,
38
+ stateDbPath: path.join(installRoot, path.basename(DEFAULT_STATE_DB)),
39
+ nodePath: process.execPath,
40
+ entryFile: '',
41
+ packageSpec: '',
42
+ localBinDir: path.join(installRoot, 'bin'),
43
+ localBinPath: path.join(installRoot, 'bin', CLI_NAME),
44
+ homeBinLink: DEFAULT_HOME_BIN_LINK,
45
+ stdoutLogPath: path.join(installRoot, 'logs', path.basename(DEFAULT_STDOUT_LOG_PATH)),
46
+ stderrLogPath: path.join(installRoot, 'logs', path.basename(DEFAULT_STDERR_LOG_PATH)),
47
+ launchdLabel: DEFAULT_LAUNCHD_LABEL,
48
+ plistPath: DEFAULT_PLIST_PATH,
49
+ };
50
+ }
51
+
52
+ export function loadRuntimeConfig(configFile = DEFAULT_CONFIG_FILE) {
53
+ const runtime = defaultRuntimeConfig(configFile);
54
+ try {
55
+ const payload = JSON.parse(fs.readFileSync(configFile, 'utf8'));
56
+ if (!payload || typeof payload !== 'object') return runtime;
57
+ return normalizeRuntimeConfig({ ...runtime, ...payload, configFile });
58
+ } catch {
59
+ return runtime;
60
+ }
61
+ }
62
+
63
+ export function normalizeRuntimeConfig(runtime) {
64
+ const installRoot = runtime.installRoot || DEFAULT_INSTALL_ROOT;
65
+ const appRoot = runtime.appRoot || path.join(installRoot, 'app');
66
+ const currentAppDir = runtime.currentAppDir || path.join(appRoot, 'current');
67
+ const localBinDir = runtime.localBinDir || DEFAULT_LOCAL_BIN_DIR;
68
+ return {
69
+ ...runtime,
70
+ configFile: runtime.configFile || DEFAULT_CONFIG_FILE,
71
+ installRoot,
72
+ appRoot,
73
+ currentAppDir,
74
+ backendUrl: runtime.backendUrl?.trim() ? runtime.backendUrl.replace(/\/+$/, '') : null,
75
+ intervalSeconds: Number(runtime.intervalSeconds) || 30,
76
+ codexAuthPath: runtime.codexAuthPath || DEFAULT_CODEX_AUTH_PATH,
77
+ sessionsDir: runtime.sessionsDir || DEFAULT_SESSIONS_DIR,
78
+ stateDbPath: runtime.stateDbPath || path.join(installRoot, 'state.sqlite'),
79
+ nodePath: runtime.nodePath || process.execPath,
80
+ entryFile: runtime.entryFile || '',
81
+ packageSpec: runtime.packageSpec || '',
82
+ localBinDir,
83
+ localBinPath: runtime.localBinPath || path.join(localBinDir, CLI_NAME),
84
+ homeBinLink: runtime.homeBinLink || DEFAULT_HOME_BIN_LINK,
85
+ stdoutLogPath: runtime.stdoutLogPath || path.join(installRoot, 'logs', 'stdout.log'),
86
+ stderrLogPath: runtime.stderrLogPath || path.join(installRoot, 'logs', 'stderr.log'),
87
+ launchdLabel: runtime.launchdLabel || DEFAULT_LAUNCHD_LABEL,
88
+ plistPath: runtime.plistPath || DEFAULT_PLIST_PATH,
89
+ };
90
+ }
91
+
92
+ export function saveRuntimeConfig(runtime) {
93
+ const normalized = normalizeRuntimeConfig(runtime);
94
+ fs.mkdirSync(path.dirname(normalized.configFile), { recursive: true });
95
+ fs.mkdirSync(path.dirname(normalized.stdoutLogPath), { recursive: true });
96
+ fs.writeFileSync(normalized.configFile, `${stableStringify({
97
+ backendUrl: normalized.backendUrl,
98
+ intervalSeconds: normalized.intervalSeconds,
99
+ codexAuthPath: normalized.codexAuthPath,
100
+ sessionsDir: normalized.sessionsDir,
101
+ stateDbPath: normalized.stateDbPath,
102
+ installRoot: normalized.installRoot,
103
+ appRoot: normalized.appRoot,
104
+ currentAppDir: normalized.currentAppDir,
105
+ nodePath: normalized.nodePath,
106
+ entryFile: normalized.entryFile,
107
+ packageSpec: normalized.packageSpec,
108
+ localBinDir: normalized.localBinDir,
109
+ localBinPath: normalized.localBinPath,
110
+ homeBinLink: normalized.homeBinLink,
111
+ stdoutLogPath: normalized.stdoutLogPath,
112
+ stderrLogPath: normalized.stderrLogPath,
113
+ launchdLabel: normalized.launchdLabel,
114
+ plistPath: normalized.plistPath,
115
+ })}\n`);
116
+ return normalized;
117
+ }
118
+
119
+ export function mergeRuntimeConfig(configFile, overrides = {}) {
120
+ const base = loadRuntimeConfig(configFile);
121
+ return normalizeRuntimeConfig({ ...base, ...overrides, configFile });
122
+ }
123
+
124
+ export function formatStatusOutput(runtime, status) {
125
+ return {
126
+ configExists: fs.existsSync(runtime.configFile),
127
+ loaded: status.loaded,
128
+ running: status.running,
129
+ pid: status.pid,
130
+ state: status.state,
131
+ lastExitCode: status.lastExitCode,
132
+ backendUrl: runtime.backendUrl,
133
+ intervalSeconds: runtime.intervalSeconds,
134
+ configFile: runtime.configFile,
135
+ stateDbPath: runtime.stateDbPath,
136
+ stdoutLogPath: runtime.stdoutLogPath,
137
+ stderrLogPath: runtime.stderrLogPath,
138
+ plistPath: runtime.plistPath,
139
+ label: runtime.launchdLabel,
140
+ onlineThresholdSeconds: STATUS_ONLINE_THRESHOLD_SECONDS,
141
+ };
142
+ }