codexport 0.1.4 → 0.1.5
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/dist/index.js +174 -9
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { homedir, platform } from "node:os";
|
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
import { spawn } from "node:child_process";
|
|
13
13
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
14
|
-
const VERSION = "0.1.
|
|
14
|
+
const VERSION = "0.1.5";
|
|
15
15
|
const DEFAULT_PORT = 17342;
|
|
16
16
|
const DEFAULT_TIMEOUT_MS = 5_000;
|
|
17
17
|
const CODEXPORT_DIR = ".codexport";
|
|
@@ -336,8 +336,8 @@ function extractTomlTableNames(text, prefix) {
|
|
|
336
336
|
}
|
|
337
337
|
return names;
|
|
338
338
|
}
|
|
339
|
-
function mergeTomlText(canonical, localMcpText, localConfig) {
|
|
340
|
-
const expandedCanonical = expandPathVariables(canonical, localConfig);
|
|
339
|
+
function mergeTomlText(canonical, localMcpText, localConfig, sourceRoot) {
|
|
340
|
+
const expandedCanonical = expandPathVariables(rewritePortableConfig(canonical, sourceRoot), localConfig);
|
|
341
341
|
if (!localMcpText?.trim())
|
|
342
342
|
return expandedCanonical;
|
|
343
343
|
const canonicalMcps = extractTomlTableNames(canonical, "mcp_servers");
|
|
@@ -349,10 +349,171 @@ function mergeTomlText(canonical, localMcpText, localConfig) {
|
|
|
349
349
|
}
|
|
350
350
|
return `${expandedCanonical.trimEnd()}\n\n# Follower-local MCP overlay from ~/.codexport/mcps.local.toml\n${localMcpText.trim()}\n`;
|
|
351
351
|
}
|
|
352
|
+
function rewritePortableConfig(canonical, sourceRoot) {
|
|
353
|
+
let parsed;
|
|
354
|
+
try {
|
|
355
|
+
parsed = parseTomlObject(canonical, "canonical config.toml");
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
return canonical;
|
|
359
|
+
}
|
|
360
|
+
const sourceHome = inferHomeFromCodexDir(sourceRoot);
|
|
361
|
+
const mcpServers = parsed.mcp_servers;
|
|
362
|
+
if (!mcpServers || typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
|
|
363
|
+
rewritePortableTableKeys(parsed, sourceRoot, sourceHome);
|
|
364
|
+
return stringifyToml(parsed);
|
|
365
|
+
}
|
|
366
|
+
rewritePortableTableKeys(parsed, sourceRoot, sourceHome);
|
|
367
|
+
for (const [name, rawServer] of Object.entries(mcpServers)) {
|
|
368
|
+
if (!rawServer || typeof rawServer !== "object" || Array.isArray(rawServer))
|
|
369
|
+
continue;
|
|
370
|
+
const server = rawServer;
|
|
371
|
+
rewritePortableMcpServer(name, server, sourceRoot, sourceHome);
|
|
372
|
+
}
|
|
373
|
+
return stringifyToml(parsed);
|
|
374
|
+
}
|
|
375
|
+
function rewritePortableTableKeys(table, sourceRoot, sourceHome) {
|
|
376
|
+
const rewritten = {};
|
|
377
|
+
for (const [key, value] of Object.entries(table)) {
|
|
378
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
379
|
+
rewritePortableTableKeys(value, sourceRoot, sourceHome);
|
|
380
|
+
}
|
|
381
|
+
rewritten[rewritePortablePath(key, sourceRoot, sourceHome)] = value;
|
|
382
|
+
}
|
|
383
|
+
for (const key of Object.keys(table)) {
|
|
384
|
+
delete table[key];
|
|
385
|
+
}
|
|
386
|
+
for (const [key, value] of Object.entries(rewritten)) {
|
|
387
|
+
table[key] = value;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function rewritePortableMcpServer(_name, server, sourceRoot, sourceHome) {
|
|
391
|
+
if (typeof server.url === "string")
|
|
392
|
+
return;
|
|
393
|
+
if (typeof server.command === "string" && Array.isArray(server.args)) {
|
|
394
|
+
const nodePackage = nodePackageFromServer(server.command, server.args);
|
|
395
|
+
if (nodePackage) {
|
|
396
|
+
server.command = "npx";
|
|
397
|
+
server.args = ["-y", nodePackage.packageName, ...nodePackage.remainingArgs.map((arg) => rewritePortablePath(arg, sourceRoot, sourceHome))];
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (typeof server.command === "string") {
|
|
402
|
+
server.command = rewritePortableCommand(server.command, sourceRoot);
|
|
403
|
+
}
|
|
404
|
+
if (Array.isArray(server.args)) {
|
|
405
|
+
server.args = server.args.map((arg) => typeof arg === "string" ? rewritePortablePath(arg, sourceRoot, sourceHome) : arg);
|
|
406
|
+
}
|
|
407
|
+
if (server.env && typeof server.env === "object" && !Array.isArray(server.env)) {
|
|
408
|
+
for (const [key, value] of Object.entries(server.env)) {
|
|
409
|
+
if (typeof value === "string") {
|
|
410
|
+
server.env[key] = rewritePortablePath(value, sourceRoot, sourceHome);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
function rewritePortableCommand(command, sourceRoot) {
|
|
416
|
+
const sourceRelative = rewriteSourceRootPath(command, sourceRoot);
|
|
417
|
+
if (sourceRelative !== command)
|
|
418
|
+
return sourceRelative;
|
|
419
|
+
if (!isAbsoluteAnyPlatform(command))
|
|
420
|
+
return command;
|
|
421
|
+
return basenameAnyPlatform(command);
|
|
422
|
+
}
|
|
423
|
+
function rewritePortablePath(value, sourceRoot, sourceHome) {
|
|
424
|
+
const sourceRootPath = rewriteSourceRootPath(value, sourceRoot);
|
|
425
|
+
if (sourceRootPath !== value)
|
|
426
|
+
return sourceRootPath;
|
|
427
|
+
return rewriteSourceHomePath(value, sourceHome);
|
|
428
|
+
}
|
|
429
|
+
function nodePackageFromServer(command, args) {
|
|
430
|
+
if (basenameAnyPlatform(command) !== "node")
|
|
431
|
+
return undefined;
|
|
432
|
+
const [entrypoint, ...remainingArgs] = args;
|
|
433
|
+
if (typeof entrypoint !== "string" || !isAbsoluteAnyPlatform(entrypoint))
|
|
434
|
+
return undefined;
|
|
435
|
+
if (!remainingArgs.every((arg) => typeof arg === "string"))
|
|
436
|
+
return undefined;
|
|
437
|
+
const packageName = packageNameFromNodeModulesPath(entrypoint);
|
|
438
|
+
if (!packageName)
|
|
439
|
+
return undefined;
|
|
440
|
+
return { packageName, remainingArgs: remainingArgs };
|
|
441
|
+
}
|
|
442
|
+
function packageNameFromNodeModulesPath(value) {
|
|
443
|
+
const parts = normalizePathForCompare(value).split("/");
|
|
444
|
+
const nodeModulesIndex = parts.lastIndexOf("node_modules");
|
|
445
|
+
if (nodeModulesIndex === -1)
|
|
446
|
+
return undefined;
|
|
447
|
+
const first = parts[nodeModulesIndex + 1];
|
|
448
|
+
if (!first)
|
|
449
|
+
return undefined;
|
|
450
|
+
if (first.startsWith("@")) {
|
|
451
|
+
const second = parts[nodeModulesIndex + 2];
|
|
452
|
+
return second ? `${first}/${second}` : undefined;
|
|
453
|
+
}
|
|
454
|
+
return first;
|
|
455
|
+
}
|
|
456
|
+
function inferHomeFromCodexDir(sourceRoot) {
|
|
457
|
+
if (!sourceRoot || basenameAnyPlatform(sourceRoot) !== ".codex")
|
|
458
|
+
return undefined;
|
|
459
|
+
const normalized = normalizePathForCompare(sourceRoot);
|
|
460
|
+
return normalized.slice(0, -"/.codex".length);
|
|
461
|
+
}
|
|
462
|
+
function rewriteSourceRootPath(value, sourceRoot) {
|
|
463
|
+
if (!sourceRoot || !isAbsoluteAnyPlatform(value))
|
|
464
|
+
return value;
|
|
465
|
+
const normalizedSourceRoot = normalizePathForCompare(sourceRoot);
|
|
466
|
+
const normalizedValue = normalizePathForCompare(value);
|
|
467
|
+
if (normalizedValue === normalizedSourceRoot)
|
|
468
|
+
return "${codexDir}";
|
|
469
|
+
if (!normalizedValue.startsWith(`${normalizedSourceRoot}/`))
|
|
470
|
+
return value;
|
|
471
|
+
const relative = normalizedValue.slice(normalizedSourceRoot.length + 1);
|
|
472
|
+
return `\${codexDir}/${relative}`;
|
|
473
|
+
}
|
|
474
|
+
function rewriteSourceHomePath(value, sourceHome) {
|
|
475
|
+
if (!sourceHome || !isAbsoluteAnyPlatform(value))
|
|
476
|
+
return value;
|
|
477
|
+
const normalizedSourceHome = normalizePathForCompare(sourceHome);
|
|
478
|
+
const normalizedValue = normalizePathForCompare(value);
|
|
479
|
+
if (normalizedValue === normalizedSourceHome)
|
|
480
|
+
return "${home}";
|
|
481
|
+
if (!normalizedValue.startsWith(`${normalizedSourceHome}/`))
|
|
482
|
+
return value;
|
|
483
|
+
const relative = normalizedValue.slice(normalizedSourceHome.length + 1);
|
|
484
|
+
return `\${home}/${relative}`;
|
|
485
|
+
}
|
|
486
|
+
function isAbsoluteAnyPlatform(value) {
|
|
487
|
+
return path.posix.isAbsolute(value) || path.win32.isAbsolute(value);
|
|
488
|
+
}
|
|
489
|
+
function basenameAnyPlatform(value) {
|
|
490
|
+
return value.includes("\\") ? path.win32.basename(value) : path.posix.basename(value);
|
|
491
|
+
}
|
|
492
|
+
function dirnameAnyPlatform(value) {
|
|
493
|
+
return value.includes("\\") || /^[A-Za-z]:\//.test(value) ? path.win32.dirname(value) : path.posix.dirname(value);
|
|
494
|
+
}
|
|
495
|
+
function normalizePathForCompare(value) {
|
|
496
|
+
return value.replace(/\\/g, "/").replace(/\/+$/g, "");
|
|
497
|
+
}
|
|
498
|
+
async function bundleForCurrentRevision(ctx, local, meta, timeoutMs) {
|
|
499
|
+
const cached = await readJsonIfExists(path.join(ctx.stateDir, CACHE_BUNDLE_FILE));
|
|
500
|
+
if (cached?.revision === meta.revision) {
|
|
501
|
+
verifyBundle(cached);
|
|
502
|
+
return cached;
|
|
503
|
+
}
|
|
504
|
+
if (!local.masterUrl)
|
|
505
|
+
throw new CliError("This machine is not enrolled. Run codexport follower join first.", 1);
|
|
506
|
+
const bundle = await fetchBundle(local.masterUrl, timeoutMs);
|
|
507
|
+
if (bundle.revision !== meta.revision)
|
|
508
|
+
throw new CliError("Master changed revision during sync. Retry.", 1);
|
|
509
|
+
await writeCachedBundle(ctx, bundle);
|
|
510
|
+
return bundle;
|
|
511
|
+
}
|
|
352
512
|
function expandPathVariables(text, localConfig) {
|
|
513
|
+
const configuredCodexDir = localConfig.codexDir ?? path.join(homedir(), ".codex");
|
|
353
514
|
const variables = {
|
|
354
|
-
home: homedir(),
|
|
355
|
-
codexDir:
|
|
515
|
+
home: basenameAnyPlatform(configuredCodexDir) === ".codex" ? dirnameAnyPlatform(configuredCodexDir) : homedir(),
|
|
516
|
+
codexDir: configuredCodexDir,
|
|
356
517
|
...(localConfig.pathVariables ?? {})
|
|
357
518
|
};
|
|
358
519
|
return text.replace(/\$\{([A-Za-z0-9_]+)\}/g, (match, name) => {
|
|
@@ -401,7 +562,7 @@ async function applyBundle(ctx, bundle) {
|
|
|
401
562
|
if (configEntry) {
|
|
402
563
|
const canonicalConfig = decodeFile(configEntry).toString("utf8");
|
|
403
564
|
const localMcpText = await readTextIfExists(path.join(ctx.stateDir, MCPS_LOCAL_FILE));
|
|
404
|
-
const generated = mergeTomlText(canonicalConfig, localMcpText, localConfig);
|
|
565
|
+
const generated = mergeTomlText(canonicalConfig, localMcpText, { ...localConfig, codexDir: ctx.codexDir }, bundle.sourceRoot);
|
|
405
566
|
const configPath = path.join(ctx.codexDir, "config.toml");
|
|
406
567
|
if (await pathExists(configPath)) {
|
|
407
568
|
const backupPath = `${configPath}.codexport-backup-${new Date().toISOString().replace(/[:.]/g, "-")}`;
|
|
@@ -618,12 +779,16 @@ async function commandSync(ctx, options) {
|
|
|
618
779
|
throw new CliError("Stored master fingerprint does not match the reachable master. Refusing to sync; re-enroll or reset trust explicitly.", 1);
|
|
619
780
|
}
|
|
620
781
|
if (local.lastRevision === meta.revision) {
|
|
782
|
+
if (options.apply) {
|
|
783
|
+
const bundle = await bundleForCurrentRevision(ctx, local, meta, options.timeoutMs);
|
|
784
|
+
await applyBundle(ctx, bundle);
|
|
785
|
+
print(ctx, { status: "applied", revision: bundle.revision, files: bundle.files.length });
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
621
788
|
print(ctx, { status: "current", revision: meta.revision });
|
|
622
789
|
return;
|
|
623
790
|
}
|
|
624
|
-
const bundle = await
|
|
625
|
-
if (bundle.revision !== meta.revision)
|
|
626
|
-
throw new CliError("Master changed revision during sync. Retry.", 1);
|
|
791
|
+
const bundle = await bundleForCurrentRevision(ctx, local, meta, options.timeoutMs);
|
|
627
792
|
await writeCachedBundle(ctx, bundle);
|
|
628
793
|
if (options.apply) {
|
|
629
794
|
await applyBundle(ctx, bundle);
|