@fractary/codex-cli 0.4.0 → 0.5.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.
- package/README.md +35 -0
- package/dist/cli.cjs +219 -9
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +218 -9
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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';
|
|
@@ -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
|
|
|
@@ -1472,15 +1646,50 @@ function syncCommand() {
|
|
|
1472
1646
|
let plan;
|
|
1473
1647
|
let routingScan;
|
|
1474
1648
|
if (direction === "from-codex") {
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
}
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
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);
|
|
1484
1693
|
}
|
|
1485
1694
|
const planWithRouting = await syncManager.createRoutingAwarePlan(
|
|
1486
1695
|
config.organization,
|