@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/README.md +38 -13
- package/dist/cli.cjs +268 -31
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +267 -31
- package/dist/cli.js.map +1 -1
- package/package.json +2 -2
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
|
|
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
|
|
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, "
|
|
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
|
|
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
|
|
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
|
|
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 ||
|
|
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 =
|
|
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
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
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:`));
|