@fractary/codex-cli 0.3.1 → 0.4.1

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/cli.js CHANGED
@@ -4,6 +4,8 @@ import 'url';
4
4
  import * as fs from 'fs/promises';
5
5
  import * as yaml from 'js-yaml';
6
6
  import { ValidationError, PermissionDeniedError, ConfigurationError, CodexError } from '@fractary/codex';
7
+ import * as os from 'os';
8
+ import { spawn } from 'child_process';
7
9
  import { Command } from 'commander';
8
10
  import chalk8 from 'chalk';
9
11
  import * as crypto from 'crypto';
@@ -61,7 +63,7 @@ async function migrateConfig(legacyConfigPath, options) {
61
63
  organization: legacy.organization || legacy.organizationSlug || "default"
62
64
  };
63
65
  if (legacy.cache) {
64
- yamlConfig.cacheDir = legacy.cache.directory || ".codex-cache";
66
+ yamlConfig.cacheDir = legacy.cache.directory || ".fractary/codex/cache";
65
67
  }
66
68
  if (legacy.storage?.providers) {
67
69
  yamlConfig.storage = [];
@@ -170,7 +172,7 @@ async function writeYamlConfig(config, outputPath) {
170
172
  function getDefaultYamlConfig(organization) {
171
173
  return {
172
174
  organization,
173
- cacheDir: ".codex-cache",
175
+ cacheDir: ".fractary/codex/cache",
174
176
  storage: [
175
177
  {
176
178
  type: "local",
@@ -590,6 +592,178 @@ var init_codex_client = __esm({
590
592
  }
591
593
  });
592
594
 
595
+ // src/utils/codex-repository.ts
596
+ var codex_repository_exports = {};
597
+ __export(codex_repository_exports, {
598
+ ensureCodexCloned: () => ensureCodexCloned,
599
+ getCodexRepoUrl: () => getCodexRepoUrl,
600
+ getTempCodexPath: () => getTempCodexPath,
601
+ isValidGitRepo: () => isValidGitRepo
602
+ });
603
+ function spawnAsync(command, args, options) {
604
+ return new Promise((resolve, reject) => {
605
+ const child = spawn(command, args, {
606
+ ...options,
607
+ env: process.env
608
+ });
609
+ let stdout = "";
610
+ let stderr = "";
611
+ child.stdout.on("data", (data) => {
612
+ stdout += data.toString();
613
+ });
614
+ child.stderr.on("data", (data) => {
615
+ stderr += data.toString();
616
+ });
617
+ child.on("error", (error) => {
618
+ reject(error);
619
+ });
620
+ child.on("close", (code) => {
621
+ if (code !== 0) {
622
+ const error = new Error(stderr || `Command exited with code ${code}`);
623
+ error.code = code;
624
+ reject(error);
625
+ } else {
626
+ resolve(stdout);
627
+ }
628
+ });
629
+ });
630
+ }
631
+ function sanitizePathComponent(component) {
632
+ if (!component || typeof component !== "string") {
633
+ throw new Error("Path component must be a non-empty string");
634
+ }
635
+ const sanitized = component.replace(/\.\./g, "").replace(/[/\\]/g, "").trim();
636
+ if (!sanitized) {
637
+ throw new Error(`Invalid path component: ${component}`);
638
+ }
639
+ return sanitized;
640
+ }
641
+ function validateGitHubName(name, type) {
642
+ if (!name || typeof name !== "string") {
643
+ throw new Error(`GitHub ${type} name must be a non-empty string`);
644
+ }
645
+ if (!/^[a-zA-Z0-9._-]+$/.test(name)) {
646
+ throw new Error(`Invalid GitHub ${type} name: ${name}. Must contain only alphanumeric characters, hyphens, underscores, and dots.`);
647
+ }
648
+ if (name.startsWith(".") || name.startsWith("-")) {
649
+ throw new Error(`GitHub ${type} name cannot start with a dot or hyphen: ${name}`);
650
+ }
651
+ }
652
+ function getTempCodexPath(config) {
653
+ const codexRepo = config.codex_repository || "codex";
654
+ const sanitizedOrg = sanitizePathComponent(config.organization);
655
+ const sanitizedRepo = sanitizePathComponent(codexRepo);
656
+ return path4.join(
657
+ os.tmpdir(),
658
+ "fractary-codex-clone",
659
+ `${sanitizedOrg}-${sanitizedRepo}-${process.pid}`
660
+ );
661
+ }
662
+ async function isValidGitRepo(repoPath) {
663
+ try {
664
+ const gitDir = path4.join(repoPath, ".git");
665
+ const stats = await fs.stat(gitDir);
666
+ return stats.isDirectory();
667
+ } catch {
668
+ return false;
669
+ }
670
+ }
671
+ function getCodexRepoUrl(config) {
672
+ const codexRepo = config.codex_repository || "codex";
673
+ validateGitHubName(config.organization, "organization");
674
+ validateGitHubName(codexRepo, "repository");
675
+ return `https://github.com/${config.organization}/${codexRepo}.git`;
676
+ }
677
+ async function execGit(repoPath, args) {
678
+ try {
679
+ const stdout = await spawnAsync("git", args, {
680
+ cwd: repoPath
681
+ });
682
+ return stdout;
683
+ } catch (error) {
684
+ if (error.code === "ENOENT") {
685
+ throw new Error(`Git command not found. Ensure git is installed and in PATH.`);
686
+ }
687
+ if (error.code === "EACCES") {
688
+ throw new Error(`Permission denied accessing repository at ${repoPath}`);
689
+ }
690
+ if (error.code === 128) {
691
+ throw new Error(`Git authentication failed. Check your credentials and repository access.`);
692
+ }
693
+ throw error;
694
+ }
695
+ }
696
+ async function gitClone(url, targetPath, options) {
697
+ const parentDir = path4.dirname(targetPath);
698
+ await fs.mkdir(parentDir, { recursive: true });
699
+ const args = ["clone"];
700
+ if (options?.depth) {
701
+ if (!Number.isInteger(options.depth) || options.depth <= 0) {
702
+ throw new Error(`Invalid depth parameter: ${options.depth}. Must be a positive integer.`);
703
+ }
704
+ args.push("--depth", String(options.depth));
705
+ }
706
+ if (options?.branch) {
707
+ if (!/^[\w\-./]+$/.test(options.branch)) {
708
+ throw new Error(`Invalid branch name: ${options.branch}`);
709
+ }
710
+ args.push("--branch", options.branch);
711
+ }
712
+ args.push("--single-branch");
713
+ args.push(url, targetPath);
714
+ await spawnAsync("git", args, { cwd: parentDir });
715
+ }
716
+ async function gitFetch(repoPath, branch) {
717
+ {
718
+ if (!/^[\w\-./]+$/.test(branch)) {
719
+ throw new Error(`Invalid branch name: ${branch}`);
720
+ }
721
+ await execGit(repoPath, ["fetch", "origin", `${branch}:${branch}`]);
722
+ }
723
+ }
724
+ async function gitCheckout(repoPath, branch) {
725
+ if (!/^[\w\-./]+$/.test(branch)) {
726
+ throw new Error(`Invalid branch name: ${branch}`);
727
+ }
728
+ await execGit(repoPath, ["checkout", branch]);
729
+ }
730
+ async function gitPull(repoPath) {
731
+ await execGit(repoPath, ["pull"]);
732
+ }
733
+ async function ensureCodexCloned(config, options) {
734
+ const tempPath = getTempCodexPath(config);
735
+ const branch = options?.branch || "main";
736
+ if (await isValidGitRepo(tempPath) && !options?.force) {
737
+ try {
738
+ await gitFetch(tempPath, branch);
739
+ await gitCheckout(tempPath, branch);
740
+ await gitPull(tempPath);
741
+ return tempPath;
742
+ } catch (error) {
743
+ console.warn(`Failed to update existing clone: ${error.message}`);
744
+ console.warn(`Removing and cloning fresh...`);
745
+ await fs.rm(tempPath, { recursive: true, force: true });
746
+ }
747
+ }
748
+ const repoUrl = getCodexRepoUrl(config);
749
+ try {
750
+ await fs.rm(tempPath, { recursive: true, force: true });
751
+ } catch (error) {
752
+ console.warn(`Could not remove existing directory ${tempPath}: ${error.message}`);
753
+ }
754
+ await gitClone(repoUrl, tempPath, {
755
+ branch,
756
+ depth: 1
757
+ // Shallow clone for efficiency
758
+ });
759
+ return tempPath;
760
+ }
761
+ var init_codex_repository = __esm({
762
+ "src/utils/codex-repository.ts"() {
763
+ init_esm_shims();
764
+ }
765
+ });
766
+
593
767
  // src/cli.ts
594
768
  init_esm_shims();
595
769
 
@@ -738,24 +912,18 @@ function initCommand() {
738
912
  console.log(chalk8.dim(`Organization: ${chalk8.cyan(org)}
739
913
  `));
740
914
  }
741
- const configDir = path4.join(process.cwd(), ".fractary");
742
- const configPath = path4.join(configDir, "codex.yaml");
915
+ const configDir = path4.join(process.cwd(), ".fractary", "codex");
916
+ const configPath = path4.join(configDir, "config.yaml");
743
917
  const configExists = await fileExists(configPath);
744
- const legacyConfigPath = path4.join(process.cwd(), ".fractary", "plugins", "codex", "config.json");
745
- const legacyExists = await fileExists(legacyConfigPath);
746
918
  if (configExists && !options.force) {
747
- console.log(chalk8.yellow("\u26A0 Configuration already exists at .fractary/codex.yaml"));
919
+ console.log(chalk8.yellow("\u26A0 Configuration already exists at .fractary/codex/config.yaml"));
748
920
  console.log(chalk8.dim("Use --force to overwrite"));
749
921
  process.exit(1);
750
922
  }
751
- if (legacyExists && !configExists) {
752
- console.log(chalk8.yellow("\u26A0 Legacy configuration detected at .fractary/plugins/codex/config.json"));
753
- console.log(chalk8.dim('Run "fractary codex migrate" to upgrade to YAML format\n'));
754
- }
755
923
  console.log("Creating directory structure...");
756
924
  const dirs = [
757
- ".fractary",
758
- ".codex-cache"
925
+ ".fractary/codex",
926
+ ".fractary/codex/cache"
759
927
  ];
760
928
  for (const dir of dirs) {
761
929
  await fs.mkdir(path4.join(process.cwd(), dir), { recursive: true });
@@ -767,12 +935,12 @@ function initCommand() {
767
935
  config.mcp.enabled = true;
768
936
  }
769
937
  await writeYamlConfig(config, configPath);
770
- console.log(chalk8.green("\u2713"), chalk8.dim(".fractary/codex.yaml"));
771
- console.log(chalk8.green("\n\u2713 Codex v3.0 initialized successfully!\n"));
938
+ console.log(chalk8.green("\u2713"), chalk8.dim(".fractary/codex/config.yaml"));
939
+ console.log(chalk8.green("\n\u2713 Codex v4.0 initialized successfully!\n"));
772
940
  console.log(chalk8.bold("Configuration:"));
773
941
  console.log(chalk8.dim(` Organization: ${org}`));
774
- console.log(chalk8.dim(` Cache: .codex-cache/`));
775
- console.log(chalk8.dim(` Config: .fractary/codex.yaml`));
942
+ console.log(chalk8.dim(` Cache: .fractary/codex/cache/`));
943
+ console.log(chalk8.dim(` Config: .fractary/codex/config.yaml`));
776
944
  if (options.mcp) {
777
945
  console.log(chalk8.dim(` MCP Server: Enabled (port 3000)`));
778
946
  }
@@ -782,13 +950,9 @@ function initCommand() {
782
950
  console.log(chalk8.dim(" - HTTP endpoint"));
783
951
  console.log(chalk8.bold("\nNext steps:"));
784
952
  console.log(chalk8.dim(' 1. Set your GitHub token: export GITHUB_TOKEN="your_token"'));
785
- console.log(chalk8.dim(" 2. Edit .fractary/codex.yaml to configure storage providers"));
953
+ console.log(chalk8.dim(" 2. Edit .fractary/codex/config.yaml to configure storage providers"));
786
954
  console.log(chalk8.dim(" 3. Fetch a document: fractary codex fetch codex://org/project/path"));
787
955
  console.log(chalk8.dim(" 4. Check cache: fractary codex cache list"));
788
- if (legacyExists) {
789
- console.log(chalk8.yellow("\n\u26A0 Legacy config detected:"));
790
- console.log(chalk8.dim(' Run "fractary codex migrate" to convert your existing config'));
791
- }
792
956
  } catch (error) {
793
957
  console.error(chalk8.red("Error:"), error.message);
794
958
  process.exit(1);
@@ -1421,7 +1585,7 @@ function syncCommand() {
1421
1585
  let projectName = name;
1422
1586
  if (!projectName) {
1423
1587
  const detected = detectCurrentProject();
1424
- projectName = detected.project || null;
1588
+ projectName = detected.project || void 0;
1425
1589
  }
1426
1590
  if (!projectName) {
1427
1591
  console.error(chalk8.red("Error:"), "Could not determine project name.");
@@ -1444,7 +1608,7 @@ function syncCommand() {
1444
1608
  config: config.sync,
1445
1609
  manifestPath: path4.join(process.cwd(), ".fractary", ".codex-sync-manifest.json")
1446
1610
  });
1447
- const defaultPatterns = config.sync?.include || [
1611
+ const defaultPatterns = [
1448
1612
  "docs/**/*.md",
1449
1613
  "specs/**/*.md",
1450
1614
  ".fractary/standards/**",
@@ -1479,13 +1643,71 @@ function syncCommand() {
1479
1643
  include: includePatterns,
1480
1644
  exclude: excludePatterns
1481
1645
  };
1482
- const plan = await syncManager.createPlan(
1483
- config.organization,
1484
- projectName,
1485
- sourceDir,
1486
- targetFiles,
1487
- syncOptions
1488
- );
1646
+ let plan;
1647
+ let routingScan;
1648
+ if (direction === "from-codex") {
1649
+ let codexRepoPath;
1650
+ try {
1651
+ const { ensureCodexCloned: ensureCodexCloned2 } = await Promise.resolve().then(() => (init_codex_repository(), codex_repository_exports));
1652
+ if (!options.json) {
1653
+ console.log(chalk8.blue("\u2139 Cloning/updating codex repository..."));
1654
+ }
1655
+ codexRepoPath = await ensureCodexCloned2(config, {
1656
+ branch: targetBranch
1657
+ });
1658
+ if (!options.json) {
1659
+ console.log(chalk8.dim(` Codex cloned to: ${codexRepoPath}`));
1660
+ console.log(chalk8.dim(" Scanning for files routing to this project...\n"));
1661
+ } else {
1662
+ console.log(JSON.stringify({
1663
+ info: "Routing-aware sync: cloned codex repository and scanning for files targeting this project",
1664
+ codexPath: codexRepoPath
1665
+ }, null, 2));
1666
+ }
1667
+ } catch (error) {
1668
+ console.error(chalk8.red("Error:"), "Failed to clone codex repository");
1669
+ console.error(chalk8.dim(` ${error.message}`));
1670
+ console.log(chalk8.yellow("\nTroubleshooting:"));
1671
+ if (error.message.includes("Git command not found")) {
1672
+ console.log(chalk8.dim(" Git is not installed or not in PATH."));
1673
+ console.log(chalk8.dim(" Install git: https://git-scm.com/downloads"));
1674
+ } else if (error.message.includes("authentication failed") || error.message.includes("Authentication failed")) {
1675
+ console.log(chalk8.dim(" GitHub authentication is required for private repositories."));
1676
+ console.log(chalk8.dim(" 1. Check auth status: gh auth status"));
1677
+ console.log(chalk8.dim(" 2. Login if needed: gh auth login"));
1678
+ console.log(chalk8.dim(" 3. Or set GITHUB_TOKEN environment variable"));
1679
+ } else if (error.message.includes("Permission denied")) {
1680
+ console.log(chalk8.dim(" Permission denied accessing repository files."));
1681
+ console.log(chalk8.dim(" 1. Check file/directory permissions"));
1682
+ console.log(chalk8.dim(" 2. Ensure you have access to the repository"));
1683
+ } else if (error.message.includes("not found") || error.message.includes("does not exist")) {
1684
+ console.log(chalk8.dim(` Repository not found: ${config.organization}/${config.codex_repository || "codex"}`));
1685
+ console.log(chalk8.dim(" 1. Verify the repository exists on GitHub"));
1686
+ console.log(chalk8.dim(" 2. Check organization and repository names in config"));
1687
+ } else {
1688
+ console.log(chalk8.dim(" 1. Ensure git is installed: git --version"));
1689
+ console.log(chalk8.dim(" 2. Check GitHub auth: gh auth status"));
1690
+ console.log(chalk8.dim(` 3. Verify repo exists: ${config.organization}/${config.codex_repository || "codex"}`));
1691
+ }
1692
+ process.exit(1);
1693
+ }
1694
+ const planWithRouting = await syncManager.createRoutingAwarePlan(
1695
+ config.organization,
1696
+ projectName,
1697
+ codexRepoPath,
1698
+ syncOptions
1699
+ );
1700
+ plan = planWithRouting;
1701
+ routingScan = planWithRouting.routingScan;
1702
+ } else {
1703
+ plan = await syncManager.createPlan(
1704
+ config.organization,
1705
+ projectName,
1706
+ sourceDir,
1707
+ targetFiles,
1708
+ syncOptions
1709
+ );
1710
+ }
1489
1711
  if (plan.totalFiles === 0) {
1490
1712
  if (options.json) {
1491
1713
  console.log(JSON.stringify({
@@ -1548,6 +1770,20 @@ function syncCommand() {
1548
1770
  if (plan.estimatedTime) {
1549
1771
  console.log(` Est. time: ${chalk8.dim(formatDuration(plan.estimatedTime))}`);
1550
1772
  }
1773
+ if (routingScan) {
1774
+ console.log("");
1775
+ console.log(chalk8.bold("Routing Statistics\n"));
1776
+ console.log(` Scanned: ${chalk8.cyan(routingScan.stats.totalScanned.toString())} files`);
1777
+ console.log(` Matched: ${chalk8.cyan(routingScan.stats.totalMatched.toString())} files`);
1778
+ console.log(` Source projects: ${chalk8.cyan(routingScan.stats.sourceProjects.length.toString())}`);
1779
+ if (routingScan.stats.sourceProjects.length > 0) {
1780
+ console.log(chalk8.dim(` ${routingScan.stats.sourceProjects.slice(0, 5).join(", ")}`));
1781
+ if (routingScan.stats.sourceProjects.length > 5) {
1782
+ console.log(chalk8.dim(` ... and ${routingScan.stats.sourceProjects.length - 5} more`));
1783
+ }
1784
+ }
1785
+ console.log(` Scan time: ${chalk8.dim(formatDuration(routingScan.stats.durationMs))}`);
1786
+ }
1551
1787
  console.log("");
1552
1788
  if (plan.conflicts.length > 0) {
1553
1789
  console.log(chalk8.yellow(`\u26A0 ${plan.conflicts.length} conflicts detected:`));