@traice/codex-collector 0.1.0 → 0.1.2
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 +49 -11
- package/cli.mjs +8 -11
- package/collector.mjs +242 -203
- package/install.mjs +154 -132
- package/package.json +1 -1
package/install.mjs
CHANGED
|
@@ -1,71 +1,81 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { createHash, randomBytes } from
|
|
3
|
-
import { execFileSync, spawnSync } from
|
|
4
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, copyFileSync } from
|
|
5
|
-
import { homedir, hostname, userInfo } from
|
|
6
|
-
import { resolve } from
|
|
7
|
-
import { fileURLToPath } from
|
|
8
|
-
|
|
9
|
-
const DEFAULT_OUT_DIR =
|
|
10
|
-
const LEGACY_INSTALL_CONFIG =
|
|
11
|
-
const DEFAULT_WORKSPACE_SLUG =
|
|
12
|
-
const DEFAULT_WORKSPACE_NAME =
|
|
13
|
-
const DEFAULT_KEY_NAME =
|
|
14
|
-
const BEGIN_MARKER =
|
|
15
|
-
const END_MARKER =
|
|
16
|
-
const LEGACY_BEGIN_MARKER =
|
|
17
|
-
const LEGACY_END_MARKER =
|
|
18
|
-
const LAUNCH_AGENT_LABEL =
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, copyFileSync } from "node:fs";
|
|
5
|
+
import { homedir, hostname, userInfo } from "node:os";
|
|
6
|
+
import { resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_OUT_DIR = "~/.traice/internal-spend-codex";
|
|
10
|
+
const LEGACY_INSTALL_CONFIG = ".traice-internal-spend/install.json";
|
|
11
|
+
const DEFAULT_WORKSPACE_SLUG = "internal-spend-poc";
|
|
12
|
+
const DEFAULT_WORKSPACE_NAME = "Internal Spend POC";
|
|
13
|
+
const DEFAULT_KEY_NAME = "internal-spend-codex-device";
|
|
14
|
+
const BEGIN_MARKER = "# BEGIN trAIce Internal Spend Codex OTel";
|
|
15
|
+
const END_MARKER = "# END trAIce Internal Spend Codex OTel";
|
|
16
|
+
const LEGACY_BEGIN_MARKER = "# BEGIN trAIce Codex OTel POC";
|
|
17
|
+
const LEGACY_END_MARKER = "# END trAIce Codex OTel POC";
|
|
18
|
+
const LAUNCH_AGENT_LABEL = "com.traice.internal-spend-codex-collector";
|
|
19
19
|
|
|
20
20
|
const args = parseArgs(process.argv.slice(2));
|
|
21
|
-
|
|
21
|
+
const inheritedApiKey = process.env.TRAICE_API_KEY;
|
|
22
|
+
loadEnvFile(resolve("apps/web/.env"));
|
|
22
23
|
|
|
23
|
-
const outDir = resolveHome(args.get(
|
|
24
|
-
const installPath = resolve(outDir,
|
|
24
|
+
const outDir = resolveHome(args.get("out") ?? DEFAULT_OUT_DIR);
|
|
25
|
+
const installPath = resolve(outDir, "install.json");
|
|
25
26
|
const serverUrl = normalizeUrl(
|
|
26
|
-
args.get(
|
|
27
|
-
process.env.TRAICE_API_URL ??
|
|
28
|
-
process.env.NEXT_PUBLIC_APP_URL ??
|
|
29
|
-
'http://127.0.0.1:3003',
|
|
27
|
+
args.get("server-url") ?? process.env.TRAICE_API_URL ?? process.env.NEXT_PUBLIC_APP_URL ?? "http://127.0.0.1:3003",
|
|
30
28
|
);
|
|
31
|
-
const workspaceSlug = args.get(
|
|
32
|
-
const workspaceName = args.get(
|
|
33
|
-
const keyName = args.get(
|
|
34
|
-
const codexHome = resolveHome(args.get(
|
|
35
|
-
const listenHost = args.get(
|
|
36
|
-
const listenPort = parsePort(args.get(
|
|
37
|
-
const patchCodexConfig = args.get(
|
|
38
|
-
const installLaunchAgent = args.get(
|
|
39
|
-
const employeeEmail =
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
29
|
+
const workspaceSlug = args.get("workspace-slug") ?? DEFAULT_WORKSPACE_SLUG;
|
|
30
|
+
const workspaceName = args.get("workspace-name") ?? DEFAULT_WORKSPACE_NAME;
|
|
31
|
+
const keyName = args.get("key-name") ?? DEFAULT_KEY_NAME;
|
|
32
|
+
const codexHome = resolveHome(args.get("codex-home") ?? process.env.CODEX_HOME ?? "~/.codex");
|
|
33
|
+
const listenHost = args.get("listen-host") ?? "127.0.0.1";
|
|
34
|
+
const listenPort = parsePort(args.get("listen-port") ?? "4318");
|
|
35
|
+
const patchCodexConfig = (args.get("patch-codex-config") ?? args.get("patch-settings")) !== "false";
|
|
36
|
+
const installLaunchAgent = args.get("launch-agent") === "true" || args.get("start-service") === "true";
|
|
37
|
+
const employeeEmail =
|
|
38
|
+
args.get("employee-email") ??
|
|
39
|
+
process.env.TRAICE_EMPLOYEE_EMAIL ??
|
|
40
|
+
gitConfig("user.email") ??
|
|
41
|
+
`${userInfo().username}@local.dev`;
|
|
42
|
+
const employeeName =
|
|
43
|
+
args.get("employee-name") ?? process.env.TRAICE_EMPLOYEE_NAME ?? gitConfig("user.name") ?? userInfo().username;
|
|
44
|
+
const teamName = args.get("team-name") ?? process.env.TRAICE_TEAM_NAME ?? "Engineering";
|
|
45
|
+
const sourcePrincipal = args.get("source-principal") ?? `${hostname()}:${userInfo().username}`;
|
|
46
|
+
const seatMonthlyUsd = parseOptionalMoney(args.get("seat-monthly-usd") ?? process.env.TRAICE_SEAT_MONTHLY_USD);
|
|
44
47
|
const providedApiKey = readApiKey();
|
|
48
|
+
const backfillHours = parseBackfillHours(args.get("backfill-hours") ?? "all");
|
|
45
49
|
|
|
46
|
-
mkdirSync(outDir, { recursive: true });
|
|
50
|
+
mkdirSync(outDir, { recursive: true, mode: 0o700 });
|
|
51
|
+
try {
|
|
52
|
+
chmodSync(outDir, 0o700);
|
|
53
|
+
} catch {
|
|
54
|
+
// Best effort on non-POSIX filesystems. Windows inherits the user-profile ACL.
|
|
55
|
+
}
|
|
47
56
|
|
|
48
57
|
const previousInstall = readJsonIfExists(installPath) ?? readLegacyInstallIfDefaultOut();
|
|
49
|
-
const previousApiKey = typeof previousInstall?.apiKey ===
|
|
58
|
+
const previousApiKey = typeof previousInstall?.apiKey === "string" ? previousInstall.apiKey : null;
|
|
50
59
|
|
|
51
60
|
let prisma = null;
|
|
52
61
|
|
|
53
62
|
try {
|
|
54
|
-
const
|
|
63
|
+
const reusableApiKey = providedApiKey || previousApiKey;
|
|
64
|
+
const setup = reusableApiKey ? providedApiKeySetup(reusableApiKey, !providedApiKey) : await localDevSetup();
|
|
55
65
|
|
|
56
66
|
const installConfig = {
|
|
57
|
-
version:
|
|
67
|
+
version: 2,
|
|
58
68
|
installedAt: new Date().toISOString(),
|
|
59
69
|
serverUrl,
|
|
60
70
|
apiKey: setup.key.raw,
|
|
61
71
|
apiKeyPrefix: setup.key.prefix,
|
|
62
72
|
workspaceId: setup.workspace.id,
|
|
63
73
|
workspaceSlug: setup.workspace.slug,
|
|
64
|
-
sourceKey:
|
|
65
|
-
sourceName:
|
|
66
|
-
sourceKind:
|
|
67
|
-
tool:
|
|
68
|
-
category:
|
|
74
|
+
sourceKey: "codex-local",
|
|
75
|
+
sourceName: "Codex local collector",
|
|
76
|
+
sourceKind: "codex_otel",
|
|
77
|
+
tool: "codex",
|
|
78
|
+
category: "coding_agent",
|
|
69
79
|
employeeEmail,
|
|
70
80
|
employeeName,
|
|
71
81
|
teamName,
|
|
@@ -75,7 +85,7 @@ try {
|
|
|
75
85
|
listenHost,
|
|
76
86
|
listenPort,
|
|
77
87
|
pollIntervalMs: 3000,
|
|
78
|
-
backfillHours
|
|
88
|
+
backfillHours,
|
|
79
89
|
includeZeroOutput: false,
|
|
80
90
|
};
|
|
81
91
|
|
|
@@ -88,10 +98,10 @@ try {
|
|
|
88
98
|
|
|
89
99
|
const configResult = patchCodexConfig
|
|
90
100
|
? patchUserCodexConfig({ codexHome, listenHost, listenPort })
|
|
91
|
-
: { status:
|
|
101
|
+
: { status: "skipped", path: resolve(codexHome, "config.toml") };
|
|
92
102
|
const launchAgentResult = installLaunchAgent
|
|
93
103
|
? installUserLaunchAgent({ installPath, outDir })
|
|
94
|
-
: { status:
|
|
104
|
+
: { status: "skipped" };
|
|
95
105
|
|
|
96
106
|
console.log(
|
|
97
107
|
JSON.stringify(
|
|
@@ -115,41 +125,43 @@ try {
|
|
|
115
125
|
await prisma?.$disconnect();
|
|
116
126
|
}
|
|
117
127
|
|
|
118
|
-
function providedApiKeySetup(rawApiKey) {
|
|
128
|
+
function providedApiKeySetup(rawApiKey, reused) {
|
|
119
129
|
return {
|
|
120
|
-
mode:
|
|
130
|
+
mode: "provided_api_key",
|
|
121
131
|
workspace: { id: null, slug: workspaceSlug, name: workspaceName, plan: null },
|
|
122
132
|
key: {
|
|
123
133
|
raw: rawApiKey,
|
|
124
134
|
prefix: rawApiKey.slice(0, 12),
|
|
125
|
-
reused
|
|
135
|
+
reused,
|
|
126
136
|
},
|
|
127
137
|
};
|
|
128
138
|
}
|
|
129
139
|
|
|
130
140
|
function readLegacyInstallIfDefaultOut() {
|
|
131
|
-
if (args.get(
|
|
141
|
+
if (args.get("out")) return null;
|
|
132
142
|
return readJsonIfExists(resolve(LEGACY_INSTALL_CONFIG));
|
|
133
143
|
}
|
|
134
144
|
|
|
135
145
|
async function localDevSetup() {
|
|
136
146
|
if (!process.env.POSTGRES_PRISMA_URL || !process.env.POSTGRES_URL_NON_POOLING) {
|
|
137
|
-
console.error(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
147
|
+
console.error(
|
|
148
|
+
[
|
|
149
|
+
"Missing API key and local database environment.",
|
|
150
|
+
"For a normal install, set TRAICE_API_KEY or pass --api-key-stdin.",
|
|
151
|
+
"For local repo development, check apps/web/.env for POSTGRES_PRISMA_URL and POSTGRES_URL_NON_POOLING.",
|
|
152
|
+
].join("\n"),
|
|
153
|
+
);
|
|
142
154
|
process.exit(2);
|
|
143
155
|
}
|
|
144
156
|
|
|
145
|
-
const { PrismaClient } = await import(
|
|
157
|
+
const { PrismaClient } = await import("@prisma/client");
|
|
146
158
|
prisma = new PrismaClient();
|
|
147
159
|
const workspace = await prisma.workspace.upsert({
|
|
148
160
|
where: { slug: workspaceSlug },
|
|
149
161
|
create: {
|
|
150
162
|
name: workspaceName,
|
|
151
163
|
slug: workspaceSlug,
|
|
152
|
-
plan:
|
|
164
|
+
plan: "TEAM",
|
|
153
165
|
captureSamplesOptIn: false,
|
|
154
166
|
internalSpendEnabled: true,
|
|
155
167
|
},
|
|
@@ -162,12 +174,12 @@ async function localDevSetup() {
|
|
|
162
174
|
|
|
163
175
|
const key = await ensureApiKey(workspace.id, keyName, previousApiKey);
|
|
164
176
|
await ensureInternalSource(workspace.id, seatMonthlyUsd);
|
|
165
|
-
if (process.env.DEV_AUTH_ENABLED ===
|
|
166
|
-
await ensureDevMembership(workspace.id, args.get(
|
|
177
|
+
if (process.env.DEV_AUTH_ENABLED === "true") {
|
|
178
|
+
await ensureDevMembership(workspace.id, args.get("dev-user") ?? "admin@local.dev");
|
|
167
179
|
}
|
|
168
180
|
|
|
169
181
|
return {
|
|
170
|
-
mode:
|
|
182
|
+
mode: "local_dev_bootstrap",
|
|
171
183
|
workspace: { id: workspace.id, slug: workspace.slug, name: workspace.name, plan: workspace.plan },
|
|
172
184
|
key,
|
|
173
185
|
};
|
|
@@ -178,7 +190,7 @@ async function ensureDevMembership(workspaceId, email) {
|
|
|
178
190
|
where: { email },
|
|
179
191
|
create: {
|
|
180
192
|
email,
|
|
181
|
-
name: email.split(
|
|
193
|
+
name: email.split("@")[0],
|
|
182
194
|
emailVerified: new Date(),
|
|
183
195
|
},
|
|
184
196
|
update: {
|
|
@@ -188,35 +200,36 @@ async function ensureDevMembership(workspaceId, email) {
|
|
|
188
200
|
|
|
189
201
|
await prisma.workspaceUser.upsert({
|
|
190
202
|
where: { workspaceId_userId: { workspaceId, userId: user.id } },
|
|
191
|
-
create: { workspaceId, userId: user.id, role:
|
|
192
|
-
update: { role:
|
|
203
|
+
create: { workspaceId, userId: user.id, role: "OWNER" },
|
|
204
|
+
update: { role: "OWNER" },
|
|
193
205
|
});
|
|
194
206
|
}
|
|
195
207
|
|
|
196
208
|
async function ensureInternalSource(workspaceId, monthlySeatUsd) {
|
|
197
|
-
const metadata =
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
209
|
+
const metadata =
|
|
210
|
+
monthlySeatUsd > 0
|
|
211
|
+
? {
|
|
212
|
+
subscription: {
|
|
213
|
+
monthlySeatUsd,
|
|
214
|
+
allocation: "monthly_seat_commitment",
|
|
215
|
+
note: "Subscription commitment is shown separately from per-request usage cost.",
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
: undefined;
|
|
206
219
|
|
|
207
220
|
await prisma.internalSource.upsert({
|
|
208
|
-
where: { workspaceId_key: { workspaceId, key:
|
|
221
|
+
where: { workspaceId_key: { workspaceId, key: "codex-local" } },
|
|
209
222
|
create: {
|
|
210
223
|
workspaceId,
|
|
211
|
-
key:
|
|
212
|
-
name:
|
|
213
|
-
kind:
|
|
224
|
+
key: "codex-local",
|
|
225
|
+
name: "Codex local collector",
|
|
226
|
+
kind: "codex_otel",
|
|
214
227
|
...(metadata ? { metadata } : {}),
|
|
215
228
|
},
|
|
216
229
|
update: {
|
|
217
|
-
name:
|
|
218
|
-
kind:
|
|
219
|
-
status:
|
|
230
|
+
name: "Codex local collector",
|
|
231
|
+
kind: "codex_otel",
|
|
232
|
+
status: "active",
|
|
220
233
|
...(metadata ? { metadata } : {}),
|
|
221
234
|
},
|
|
222
235
|
});
|
|
@@ -247,49 +260,49 @@ async function ensureApiKey(workspaceId, name, previousRawKey) {
|
|
|
247
260
|
|
|
248
261
|
function patchUserCodexConfig({ codexHome, listenHost, listenPort }) {
|
|
249
262
|
mkdirSync(codexHome, { recursive: true });
|
|
250
|
-
const configPath = resolve(codexHome,
|
|
251
|
-
const current = existsSync(configPath) ? readFileSync(configPath,
|
|
263
|
+
const configPath = resolve(codexHome, "config.toml");
|
|
264
|
+
const current = existsSync(configPath) ? readFileSync(configPath, "utf8") : "";
|
|
252
265
|
const block = [
|
|
253
266
|
BEGIN_MARKER,
|
|
254
|
-
|
|
267
|
+
"[otel]",
|
|
255
268
|
'environment = "traice-device"',
|
|
256
|
-
|
|
269
|
+
"log_user_prompt = false",
|
|
257
270
|
`exporter = { otlp-http = { endpoint = "http://${listenHost}:${listenPort}/v1/logs", protocol = "json" } }`,
|
|
258
271
|
'trace_exporter = "none"',
|
|
259
272
|
'metrics_exporter = "none"',
|
|
260
273
|
END_MARKER,
|
|
261
|
-
|
|
262
|
-
].join(
|
|
274
|
+
"",
|
|
275
|
+
].join("\n");
|
|
263
276
|
|
|
264
277
|
if (current.includes(BEGIN_MARKER) || current.includes(LEGACY_BEGIN_MARKER)) {
|
|
265
278
|
const begin = current.includes(BEGIN_MARKER) ? BEGIN_MARKER : LEGACY_BEGIN_MARKER;
|
|
266
279
|
const end = current.includes(BEGIN_MARKER) ? END_MARKER : LEGACY_END_MARKER;
|
|
267
|
-
const pattern = new RegExp(`${escapeRegExp(begin)}[\\s\\S]*?${escapeRegExp(end)}\\n?`,
|
|
280
|
+
const pattern = new RegExp(`${escapeRegExp(begin)}[\\s\\S]*?${escapeRegExp(end)}\\n?`, "m");
|
|
268
281
|
writeFileSync(configPath, current.replace(pattern, block));
|
|
269
|
-
return { status:
|
|
282
|
+
return { status: "updated", path: configPath };
|
|
270
283
|
}
|
|
271
284
|
|
|
272
285
|
if (/^\s*\[otel\]\s*$/m.test(current)) {
|
|
273
286
|
return {
|
|
274
|
-
status:
|
|
287
|
+
status: "blocked_existing_otel",
|
|
275
288
|
path: configPath,
|
|
276
|
-
message:
|
|
289
|
+
message: "Existing [otel] table found; left it unchanged.",
|
|
277
290
|
};
|
|
278
291
|
}
|
|
279
292
|
|
|
280
293
|
const next = current.trimEnd().length > 0 ? `${current.trimEnd()}\n\n${block}` : block;
|
|
281
294
|
writeFileSync(configPath, next);
|
|
282
|
-
return { status:
|
|
295
|
+
return { status: "added", path: configPath };
|
|
283
296
|
}
|
|
284
297
|
|
|
285
298
|
function installUserLaunchAgent({ installPath, outDir }) {
|
|
286
|
-
const launchAgentsDir = resolveHome(
|
|
299
|
+
const launchAgentsDir = resolveHome("~/Library/LaunchAgents");
|
|
287
300
|
mkdirSync(launchAgentsDir, { recursive: true });
|
|
288
301
|
const plistPath = resolve(launchAgentsDir, `${LAUNCH_AGENT_LABEL}.plist`);
|
|
289
302
|
const nodePath = process.execPath;
|
|
290
303
|
const collectorPath = copyCollectorForService(outDir);
|
|
291
|
-
const logPath = resolve(outDir,
|
|
292
|
-
const errPath = resolve(outDir,
|
|
304
|
+
const logPath = resolve(outDir, "collector.log");
|
|
305
|
+
const errPath = resolve(outDir, "collector.err.log");
|
|
293
306
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
294
307
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
295
308
|
<plist version="1.0">
|
|
@@ -321,21 +334,21 @@ function installUserLaunchAgent({ installPath, outDir }) {
|
|
|
321
334
|
writeFileSync(plistPath, plist);
|
|
322
335
|
|
|
323
336
|
const guiTarget = `gui/${process.getuid()}`;
|
|
324
|
-
spawnSync(
|
|
325
|
-
const result = spawnSync(
|
|
337
|
+
spawnSync("launchctl", ["bootout", guiTarget, plistPath], { stdio: "ignore" });
|
|
338
|
+
const result = spawnSync("launchctl", ["bootstrap", guiTarget, plistPath], { encoding: "utf8" });
|
|
326
339
|
if (result.status !== 0) {
|
|
327
340
|
return {
|
|
328
|
-
status:
|
|
341
|
+
status: "written_not_loaded",
|
|
329
342
|
path: plistPath,
|
|
330
|
-
error: (result.stderr || result.stdout ||
|
|
343
|
+
error: (result.stderr || result.stdout || "").trim(),
|
|
331
344
|
};
|
|
332
345
|
}
|
|
333
|
-
return { status:
|
|
346
|
+
return { status: "loaded", path: plistPath, logPath, errPath };
|
|
334
347
|
}
|
|
335
348
|
|
|
336
349
|
function copyCollectorForService(outDir) {
|
|
337
|
-
const sourcePath = fileURLToPath(new URL(
|
|
338
|
-
const targetPath = resolve(outDir,
|
|
350
|
+
const sourcePath = fileURLToPath(new URL("./collector.mjs", import.meta.url));
|
|
351
|
+
const targetPath = resolve(outDir, "collector.mjs");
|
|
339
352
|
copyFileSync(sourcePath, targetPath);
|
|
340
353
|
try {
|
|
341
354
|
chmodSync(targetPath, 0o755);
|
|
@@ -346,24 +359,26 @@ function copyCollectorForService(outDir) {
|
|
|
346
359
|
}
|
|
347
360
|
|
|
348
361
|
function readApiKey() {
|
|
349
|
-
if (args.get(
|
|
350
|
-
return readFileSync(0,
|
|
362
|
+
if (args.get("api-key-stdin") === "true") {
|
|
363
|
+
return readFileSync(0, "utf8").trim();
|
|
351
364
|
}
|
|
352
|
-
|
|
365
|
+
// A repo-local .env is loaded for database development only. It must not
|
|
366
|
+
// silently replace a device's saved production credential on reinstall.
|
|
367
|
+
return args.get("api-key") ?? inheritedApiKey;
|
|
353
368
|
}
|
|
354
369
|
|
|
355
370
|
function parseArgs(argv) {
|
|
356
371
|
const parsed = new Map();
|
|
357
372
|
for (let i = 0; i < argv.length; i += 1) {
|
|
358
373
|
const arg = argv[i];
|
|
359
|
-
if (!arg.startsWith(
|
|
374
|
+
if (!arg.startsWith("--")) continue;
|
|
360
375
|
const key = arg.slice(2);
|
|
361
376
|
const next = argv[i + 1];
|
|
362
|
-
if (next && !next.startsWith(
|
|
377
|
+
if (next && !next.startsWith("--")) {
|
|
363
378
|
parsed.set(key, next);
|
|
364
379
|
i += 1;
|
|
365
380
|
} else {
|
|
366
|
-
parsed.set(key,
|
|
381
|
+
parsed.set(key, "true");
|
|
367
382
|
}
|
|
368
383
|
}
|
|
369
384
|
return parsed;
|
|
@@ -371,9 +386,9 @@ function parseArgs(argv) {
|
|
|
371
386
|
|
|
372
387
|
function gitConfig(key) {
|
|
373
388
|
try {
|
|
374
|
-
const value = execFileSync(
|
|
375
|
-
encoding:
|
|
376
|
-
stdio: [
|
|
389
|
+
const value = execFileSync("git", ["config", "--global", key], {
|
|
390
|
+
encoding: "utf8",
|
|
391
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
377
392
|
}).trim();
|
|
378
393
|
return value || null;
|
|
379
394
|
} catch {
|
|
@@ -383,9 +398,9 @@ function gitConfig(key) {
|
|
|
383
398
|
|
|
384
399
|
function loadEnvFile(path) {
|
|
385
400
|
if (!existsSync(path)) return;
|
|
386
|
-
for (const line of readFileSync(path,
|
|
401
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
387
402
|
const trimmed = line.trim();
|
|
388
|
-
if (!trimmed || trimmed.startsWith(
|
|
403
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
389
404
|
const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
390
405
|
if (!match) continue;
|
|
391
406
|
const [, key, rawValue] = match;
|
|
@@ -395,10 +410,7 @@ function loadEnvFile(path) {
|
|
|
395
410
|
}
|
|
396
411
|
|
|
397
412
|
function unquoteEnvValue(value) {
|
|
398
|
-
if (
|
|
399
|
-
(value.startsWith('"') && value.endsWith('"')) ||
|
|
400
|
-
(value.startsWith("'") && value.endsWith("'"))
|
|
401
|
-
) {
|
|
413
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
402
414
|
return value.slice(1, -1);
|
|
403
415
|
}
|
|
404
416
|
return value;
|
|
@@ -407,19 +419,19 @@ function unquoteEnvValue(value) {
|
|
|
407
419
|
function readJsonIfExists(path) {
|
|
408
420
|
if (!existsSync(path)) return null;
|
|
409
421
|
try {
|
|
410
|
-
return JSON.parse(readFileSync(path,
|
|
422
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
411
423
|
} catch {
|
|
412
424
|
return null;
|
|
413
425
|
}
|
|
414
426
|
}
|
|
415
427
|
|
|
416
428
|
function normalizeUrl(value) {
|
|
417
|
-
return String(value).replace(/\/+$/,
|
|
429
|
+
return String(value).replace(/\/+$/, "");
|
|
418
430
|
}
|
|
419
431
|
|
|
420
432
|
function resolveHome(value) {
|
|
421
|
-
if (value ===
|
|
422
|
-
if (value.startsWith(
|
|
433
|
+
if (value === "~") return homedir();
|
|
434
|
+
if (value.startsWith("~/")) return resolve(homedir(), value.slice(2));
|
|
423
435
|
return resolve(value);
|
|
424
436
|
}
|
|
425
437
|
|
|
@@ -433,8 +445,8 @@ function parsePort(value) {
|
|
|
433
445
|
}
|
|
434
446
|
|
|
435
447
|
function parseOptionalMoney(value) {
|
|
436
|
-
if (value == null || value ===
|
|
437
|
-
const parsed = Number(String(value).replace(/[$,]/g,
|
|
448
|
+
if (value == null || value === "") return 0;
|
|
449
|
+
const parsed = Number(String(value).replace(/[$,]/g, ""));
|
|
438
450
|
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
439
451
|
console.error(`Invalid USD amount: ${value}`);
|
|
440
452
|
process.exit(2);
|
|
@@ -442,8 +454,18 @@ function parseOptionalMoney(value) {
|
|
|
442
454
|
return parsed;
|
|
443
455
|
}
|
|
444
456
|
|
|
457
|
+
function parseBackfillHours(value) {
|
|
458
|
+
if (["all", "full", "infinity"].includes(String(value).toLowerCase())) return "all";
|
|
459
|
+
const parsed = Number(value);
|
|
460
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
461
|
+
console.error(`Invalid backfill hours: ${value}. Use a non-negative number or "all".`);
|
|
462
|
+
process.exit(2);
|
|
463
|
+
}
|
|
464
|
+
return parsed;
|
|
465
|
+
}
|
|
466
|
+
|
|
445
467
|
function generateApiKey() {
|
|
446
|
-
const raw = `lm_live_${randomBytes(24).toString(
|
|
468
|
+
const raw = `lm_live_${randomBytes(24).toString("hex")}`;
|
|
447
469
|
return {
|
|
448
470
|
raw,
|
|
449
471
|
prefix: raw.slice(0, 12),
|
|
@@ -452,18 +474,18 @@ function generateApiKey() {
|
|
|
452
474
|
}
|
|
453
475
|
|
|
454
476
|
function hashApiKey(raw) {
|
|
455
|
-
return createHash(
|
|
477
|
+
return createHash("sha256").update(raw).digest("hex");
|
|
456
478
|
}
|
|
457
479
|
|
|
458
480
|
function escapeRegExp(value) {
|
|
459
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g,
|
|
481
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
460
482
|
}
|
|
461
483
|
|
|
462
484
|
function escapeXml(value) {
|
|
463
485
|
return String(value)
|
|
464
|
-
.replace(/&/g,
|
|
465
|
-
.replace(/</g,
|
|
466
|
-
.replace(/>/g,
|
|
467
|
-
.replace(/"/g,
|
|
468
|
-
.replace(/'/g,
|
|
486
|
+
.replace(/&/g, "&")
|
|
487
|
+
.replace(/</g, "<")
|
|
488
|
+
.replace(/>/g, ">")
|
|
489
|
+
.replace(/"/g, """)
|
|
490
|
+
.replace(/'/g, "'");
|
|
469
491
|
}
|