codexport 0.1.5 → 0.1.7

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 -17
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { Command, Option } from "commander";
3
3
  import chokidar from "chokidar";
4
4
  import { createHash, randomBytes } from "node:crypto";
5
5
  import { createServer, request } from "node:http";
6
- import { lstat, mkdir, readFile, readdir, readlink, rename, rm, stat, symlink, writeFile } from "node:fs/promises";
6
+ import { chmod, lstat, mkdir, readFile, readdir, readlink, rename, rm, stat, symlink, writeFile } from "node:fs/promises";
7
7
  import { existsSync } from "node:fs";
8
8
  import { realpathSync } from "node:fs";
9
9
  import { fileURLToPath } from "node:url";
@@ -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.5";
14
+ const VERSION = "0.1.7";
15
15
  const DEFAULT_PORT = 17342;
16
16
  const DEFAULT_TIMEOUT_MS = 5_000;
17
17
  const CODEXPORT_DIR = ".codexport";
@@ -114,6 +114,31 @@ async function writeJsonAtomic(filePath, value) {
114
114
  await writeFile(tmpPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
115
115
  await rename(tmpPath, filePath);
116
116
  }
117
+ async function writeFileReplacingExisting(filePath, content, options) {
118
+ try {
119
+ await writeFile(filePath, content, options);
120
+ return;
121
+ }
122
+ catch (error) {
123
+ if (!isPermissionError(error))
124
+ throw error;
125
+ }
126
+ if (await pathExists(filePath)) {
127
+ try {
128
+ await chmod(filePath, 0o666);
129
+ }
130
+ catch (error) {
131
+ if (!isPermissionError(error))
132
+ throw error;
133
+ }
134
+ await rm(filePath, { force: true });
135
+ }
136
+ await writeFile(filePath, content, options);
137
+ }
138
+ function isPermissionError(error) {
139
+ const code = error.code;
140
+ return code === "EACCES" || code === "EPERM";
141
+ }
117
142
  function parseTomlObject(text, filePath) {
118
143
  try {
119
144
  const parsed = parseToml(text);
@@ -390,19 +415,21 @@ function rewritePortableTableKeys(table, sourceRoot, sourceHome) {
390
415
  function rewritePortableMcpServer(_name, server, sourceRoot, sourceHome) {
391
416
  if (typeof server.url === "string")
392
417
  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
- }
418
+ const command = typeof server.command === "string" ? server.command : undefined;
419
+ const args = Array.isArray(server.args) ? server.args : [];
420
+ const launcher = command ? portableMcpLauncher(_name, command, args, sourceHome) : undefined;
421
+ if (launcher) {
422
+ server.command = launcher.command;
423
+ server.args = launcher.args.map((arg) => rewritePortablePath(arg, sourceRoot, sourceHome));
424
+ }
425
+ else if (command && isAbsoluteAnyPlatform(command)) {
426
+ server.enabled = false;
400
427
  }
401
- if (typeof server.command === "string") {
402
- server.command = rewritePortableCommand(server.command, sourceRoot);
428
+ else if (command) {
429
+ server.command = rewritePortableCommand(command, sourceRoot);
403
430
  }
404
- if (Array.isArray(server.args)) {
405
- server.args = server.args.map((arg) => typeof arg === "string" ? rewritePortablePath(arg, sourceRoot, sourceHome) : arg);
431
+ if (!launcher && args.length) {
432
+ server.args = args.map((arg) => typeof arg === "string" ? rewritePortablePath(arg, sourceRoot, sourceHome) : arg);
406
433
  }
407
434
  if (server.env && typeof server.env === "object" && !Array.isArray(server.env)) {
408
435
  for (const [key, value] of Object.entries(server.env)) {
@@ -412,6 +439,34 @@ function rewritePortableMcpServer(_name, server, sourceRoot, sourceHome) {
412
439
  }
413
440
  }
414
441
  }
442
+ function portableMcpLauncher(name, command, args, sourceHome) {
443
+ const commandName = basenameAnyPlatform(command);
444
+ if (commandName === "npx" || commandName === "bunx" || commandName === "uvx") {
445
+ return allStrings(args) ? { command: commandName, args: args } : undefined;
446
+ }
447
+ const nodePackage = nodePackageFromServer(command, args) ?? workspacePackageFromServer(command, args, sourceHome);
448
+ if (nodePackage) {
449
+ return { command: "npx", args: ["-y", nodePackage.packageName, ...nodePackage.remainingArgs] };
450
+ }
451
+ const npmPackage = npmPackageForPortableMcp(name, commandName);
452
+ if (npmPackage) {
453
+ const remainingArgs = allStrings(args) ? args : [];
454
+ return { command: "npx", args: ["-y", npmPackage, ...remainingArgs] };
455
+ }
456
+ return undefined;
457
+ }
458
+ function npmPackageForPortableMcp(name, commandName) {
459
+ const knownPackages = {
460
+ "dora": "dora",
461
+ "kagi-mcp": "kagi-mcp",
462
+ "opensrc-mcp": "opensrc-mcp",
463
+ "opensrc-mcp-stdio": "opensrc-mcp",
464
+ "perplexity-webui": "perplexity-webui-mcp",
465
+ "perplexity-webui-mcp": "perplexity-webui-mcp",
466
+ "reddit-mcp-buddy": "reddit-mcp-buddy"
467
+ };
468
+ return knownPackages[name] ?? knownPackages[commandName];
469
+ }
415
470
  function rewritePortableCommand(command, sourceRoot) {
416
471
  const sourceRelative = rewriteSourceRootPath(command, sourceRoot);
417
472
  if (sourceRelative !== command)
@@ -432,13 +487,31 @@ function nodePackageFromServer(command, args) {
432
487
  const [entrypoint, ...remainingArgs] = args;
433
488
  if (typeof entrypoint !== "string" || !isAbsoluteAnyPlatform(entrypoint))
434
489
  return undefined;
435
- if (!remainingArgs.every((arg) => typeof arg === "string"))
490
+ if (!allStrings(remainingArgs))
436
491
  return undefined;
437
492
  const packageName = packageNameFromNodeModulesPath(entrypoint);
438
493
  if (!packageName)
439
494
  return undefined;
440
495
  return { packageName, remainingArgs: remainingArgs };
441
496
  }
497
+ function workspacePackageFromServer(command, args, sourceHome) {
498
+ if (basenameAnyPlatform(command) !== "node")
499
+ return undefined;
500
+ const [entrypoint, ...remainingArgs] = args;
501
+ if (!sourceHome || typeof entrypoint !== "string" || !isAbsoluteAnyPlatform(entrypoint) || !allStrings(remainingArgs))
502
+ return undefined;
503
+ const normalizedEntry = normalizePathForCompare(entrypoint);
504
+ const workspacePrefix = `${normalizePathForCompare(sourceHome)}/workspace/`;
505
+ if (!normalizedEntry.startsWith(workspacePrefix))
506
+ return undefined;
507
+ const packageName = normalizedEntry.slice(workspacePrefix.length).split("/")[0];
508
+ if (!packageName)
509
+ return undefined;
510
+ return { packageName, remainingArgs: remainingArgs };
511
+ }
512
+ function allStrings(values) {
513
+ return values.every((value) => typeof value === "string");
514
+ }
442
515
  function packageNameFromNodeModulesPath(value) {
443
516
  const parts = normalizePathForCompare(value).split("/");
444
517
  const nodeModulesIndex = parts.lastIndexOf("node_modules");
@@ -556,7 +629,7 @@ async function applyBundle(ctx, bundle) {
556
629
  await ensureDir(path.dirname(target));
557
630
  if (file.path === "config.toml")
558
631
  continue;
559
- await writeFile(target, decodeFile(file), { mode: file.mode });
632
+ await writeFileReplacingExisting(target, decodeFile(file), { mode: file.mode });
560
633
  }
561
634
  const configEntry = bundle.files.find((file) => file.path === "config.toml");
562
635
  if (configEntry) {
@@ -568,7 +641,7 @@ async function applyBundle(ctx, bundle) {
568
641
  const backupPath = `${configPath}.codexport-backup-${new Date().toISOString().replace(/[:.]/g, "-")}`;
569
642
  await writeFile(backupPath, await readFile(configPath));
570
643
  }
571
- await writeFile(configPath, generated, "utf8");
644
+ await writeFileReplacingExisting(configPath, generated, "utf8");
572
645
  }
573
646
  const localSkillsDir = path.join(ctx.stateDir, "skills");
574
647
  if (await pathExists(localSkillsDir)) {
@@ -589,7 +662,7 @@ async function copyDirectory(source, target) {
589
662
  }
590
663
  else if (entry.isFile()) {
591
664
  await ensureDir(path.dirname(targetPath));
592
- await writeFile(targetPath, await readFile(sourcePath));
665
+ await writeFileReplacingExisting(targetPath, await readFile(sourcePath));
593
666
  }
594
667
  else if (entry.isSymbolicLink()) {
595
668
  const linkTarget = await readlink(sourcePath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexport",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
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",
@@ -24,7 +24,7 @@
24
24
  "automation"
25
25
  ],
26
26
  "bin": {
27
- "codexport": "./dist/index.js"
27
+ "codexport": "dist/index.js"
28
28
  },
29
29
  "files": [
30
30
  "dist",