codexport 0.2.0 → 0.3.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.
Files changed (2) hide show
  1. package/dist/index.js +90 -18
  2. 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.2.0";
14
+ const VERSION = "0.3.0";
15
15
  const DEFAULT_PORT = 17342;
16
16
  const DEFAULT_TIMEOUT_MS = 5_000;
17
17
  const CODEXPORT_DIR = ".codexport";
@@ -21,6 +21,8 @@ const MCPS_LOCAL_FILE = "mcps.local.toml";
21
21
  const LAST_BUNDLE_FILE = "last-bundle.json";
22
22
  const CACHE_BUNDLE_FILE = "bundle.json";
23
23
  const APPLIED_FILES_FILE = "applied-files.json";
24
+ const MCP_MANIFEST_FILE = "mcp-manifest.json";
25
+ const MANAGED_MCP_PACKAGE = "codexport@latest";
24
26
  const INCLUDE_ROOTS = [
25
27
  "AGENTS.md",
26
28
  "RTK.md",
@@ -410,10 +412,31 @@ function rewritePortableConfig(canonical, sourceRoot, sourceEnv = {}) {
410
412
  continue;
411
413
  const server = rawServer;
412
414
  mergeSourceEnvForMcp(name, server, sourceEnv);
413
- rewritePortableMcpServer(name, server, sourceRoot, sourceHome);
415
+ rewriteManagedMcpServer(name, server, sourceRoot, sourceHome);
414
416
  }
415
417
  return stringifyToml(parsed);
416
418
  }
419
+ function buildMcpManifest(canonical, sourceRoot, sourceEnv = {}) {
420
+ let parsed;
421
+ try {
422
+ parsed = parseTomlObject(canonical, "canonical config.toml");
423
+ }
424
+ catch {
425
+ return undefined;
426
+ }
427
+ const mcpServers = parsed.mcp_servers;
428
+ if (!mcpServers || typeof mcpServers !== "object" || Array.isArray(mcpServers))
429
+ return undefined;
430
+ const servers = {};
431
+ for (const [name, rawServer] of Object.entries(mcpServers)) {
432
+ if (!rawServer || typeof rawServer !== "object" || Array.isArray(rawServer))
433
+ continue;
434
+ const server = structuredClone(rawServer);
435
+ mergeSourceEnvForMcp(name, server, sourceEnv);
436
+ servers[name] = server;
437
+ }
438
+ return { version: 1, sourceRoot, sourceEnv, servers };
439
+ }
417
440
  function mergeSourceEnvForMcp(name, server, sourceEnv) {
418
441
  if (name !== "kagi-mcp")
419
442
  return;
@@ -439,25 +462,12 @@ function rewritePortableTableKeys(table, sourceRoot, sourceHome) {
439
462
  table[key] = value;
440
463
  }
441
464
  }
442
- function rewritePortableMcpServer(_name, server, sourceRoot, sourceHome) {
465
+ function rewriteManagedMcpServer(name, server, sourceRoot, sourceHome) {
443
466
  if (typeof server.url === "string")
444
467
  return;
445
- const command = typeof server.command === "string" ? server.command : undefined;
446
468
  const args = Array.isArray(server.args) ? server.args : [];
447
- const launcher = command && mcpHasRequiredPortableEnv(_name, command, server) ? portableMcpLauncher(_name, command, args, sourceHome, server) : undefined;
448
- if (launcher) {
449
- server.command = launcher.command;
450
- server.args = launcher.args.map((arg) => rewritePortablePath(arg, sourceRoot, sourceHome));
451
- }
452
- else if (command && isAbsoluteAnyPlatform(command)) {
453
- server.enabled = false;
454
- }
455
- else if (command) {
456
- server.command = rewritePortableCommand(command, sourceRoot);
457
- }
458
- if (!launcher && args.length) {
459
- server.args = args.map((arg) => typeof arg === "string" ? rewritePortablePath(arg, sourceRoot, sourceHome) : arg);
460
- }
469
+ server.command = "npx";
470
+ server.args = ["-y", MANAGED_MCP_PACKAGE, "mcp", "run", name];
461
471
  if (server.env && typeof server.env === "object" && !Array.isArray(server.env)) {
462
472
  for (const [key, value] of Object.entries(server.env)) {
463
473
  if (typeof value === "string") {
@@ -691,6 +701,10 @@ async function applyBundle(ctx, bundle) {
691
701
  if (configEntry) {
692
702
  const canonicalConfig = decodeFile(configEntry).toString("utf8");
693
703
  const localMcpText = await readTextIfExists(path.join(ctx.stateDir, MCPS_LOCAL_FILE));
704
+ const manifest = buildMcpManifest(canonicalConfig, bundle.sourceRoot, bundle.sourceEnv);
705
+ if (manifest) {
706
+ await writeJsonAtomic(path.join(ctx.stateDir, MCP_MANIFEST_FILE), manifest);
707
+ }
694
708
  const generated = mergeTomlText(canonicalConfig, localMcpText, { ...localConfig, codexDir: ctx.codexDir }, bundle.sourceRoot, bundle.sourceEnv);
695
709
  const configPath = path.join(ctx.codexDir, "config.toml");
696
710
  if (await pathExists(configPath)) {
@@ -708,6 +722,43 @@ async function applyBundle(ctx, bundle) {
708
722
  await writeLocalConfig(ctx, { ...localConfig, lastRevision: bundle.revision, codexDir: ctx.codexDir });
709
723
  await writeJsonAtomic(path.join(ctx.stateDir, APPLIED_FILES_FILE), bundle.files.map((file) => file.path));
710
724
  }
725
+ async function commandMcpRun(ctx, name) {
726
+ const manifest = await readJsonIfExists(path.join(ctx.stateDir, MCP_MANIFEST_FILE));
727
+ if (!manifest?.servers?.[name]) {
728
+ throw new CliError(`No managed MCP named ${name}. Run codexport sync --apply first.`, 1);
729
+ }
730
+ const localConfig = await readLocalConfig(ctx);
731
+ const sourceHome = inferHomeFromCodexDir(manifest.sourceRoot);
732
+ const server = structuredClone(manifest.servers[name]);
733
+ mergeSourceEnvForMcp(name, server, manifest.sourceEnv ?? {});
734
+ rewritePortableTableKeys(server, manifest.sourceRoot, sourceHome);
735
+ if (typeof server.url === "string") {
736
+ throw new CliError(`MCP ${name} is URL-based and should not be launched through codexport mcp run.`, 2);
737
+ }
738
+ const command = typeof server.command === "string" ? expandPathVariables(server.command, { ...localConfig, codexDir: ctx.codexDir }) : undefined;
739
+ const args = Array.isArray(server.args)
740
+ ? server.args.map((arg) => typeof arg === "string" ? expandPathVariables(rewritePortablePath(arg, manifest.sourceRoot, sourceHome), { ...localConfig, codexDir: ctx.codexDir }) : String(arg))
741
+ : [];
742
+ if (!command)
743
+ throw new CliError(`MCP ${name} has no command.`, 1);
744
+ const launcher = mcpHasRequiredPortableEnv(name, command, server)
745
+ ? portableMcpLauncher(name, command, args, sourceHome, server)
746
+ : undefined;
747
+ const runCommandName = launcher?.command ?? rewritePortableCommand(command, manifest.sourceRoot);
748
+ const runArgs = launcher?.args ?? args;
749
+ const childEnv = { ...process.env, ...portableServerEnv(server, manifest.sourceRoot, sourceHome, { ...localConfig, codexDir: ctx.codexDir }) };
750
+ await runCommandWithEnv(runCommandName, runArgs, childEnv);
751
+ }
752
+ function portableServerEnv(server, sourceRoot, sourceHome, localConfig) {
753
+ const env = server.env && typeof server.env === "object" && !Array.isArray(server.env) ? server.env : {};
754
+ const out = {};
755
+ for (const [key, value] of Object.entries(env)) {
756
+ if (typeof value === "string") {
757
+ out[key] = expandPathVariables(rewritePortablePath(value, sourceRoot, sourceHome), localConfig);
758
+ }
759
+ }
760
+ return out;
761
+ }
711
762
  async function copyDirectory(source, target) {
712
763
  await ensureDir(target);
713
764
  for (const entry of await readdir(source, { withFileTypes: true })) {
@@ -749,6 +800,23 @@ function runCommand(command, args) {
749
800
  });
750
801
  });
751
802
  }
803
+ function runCommandWithEnv(command, args, env) {
804
+ return new Promise((resolve, reject) => {
805
+ const child = spawn(command, args, { stdio: "inherit", env });
806
+ child.on("error", (error) => {
807
+ const message = error.code === "ENOENT"
808
+ ? `MCP launcher program not found: ${command}. Install it on this follower or add it to PATH.`
809
+ : asError(error).message;
810
+ reject(new CliError(message, 1));
811
+ });
812
+ child.on("exit", (code) => {
813
+ if (code === 0)
814
+ resolve();
815
+ else
816
+ reject(new CliError(`${command} ${args.join(" ")} exited with ${code}`, code ?? 1));
817
+ });
818
+ });
819
+ }
752
820
  async function installMasterService(ctx, port, dryRun) {
753
821
  const command = `${process.execPath} ${realpathSync(fileURLToPath(import.meta.url))} master serve --port ${port}`;
754
822
  if (platform() === "linux") {
@@ -1016,6 +1084,10 @@ async function main(argv) {
1016
1084
  program.command("apply")
1017
1085
  .description("Apply the last staged bundle.")
1018
1086
  .action(async (_options, command) => commandApply(contextFromCommand(command)));
1087
+ const mcp = program.command("mcp").description("Run managed follower MCP launchers.");
1088
+ mcp.command("run <name>")
1089
+ .description("Run a synced MCP through codexport's managed launcher.")
1090
+ .action(async (name, _options, command) => commandMcpRun(contextFromCommand(command), name));
1019
1091
  const hook = program.command("hook").description("Manage follower Codex hooks.");
1020
1092
  hook.command("install")
1021
1093
  .description("Install a follower-only Codex SessionStart sync hook.")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexport",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "sync a canonical Codex setup from one master machine to follower machines",
5
5
  "author": "Microck <contact@micr.dev>",
6
6
  "license": "MIT",