@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/README.md
CHANGED
|
@@ -153,6 +153,41 @@ Options:
|
|
|
153
153
|
--parallel <n> Number of parallel sync operations (default: 3)
|
|
154
154
|
```
|
|
155
155
|
|
|
156
|
+
**Routing-Aware Sync (v4.1+):**
|
|
157
|
+
|
|
158
|
+
When using `--from-codex` direction, the sync command uses **routing-aware file discovery** to find all files across the entire codex that should sync to your project based on `codex_sync_include` frontmatter patterns.
|
|
159
|
+
|
|
160
|
+
How it works:
|
|
161
|
+
1. Clones the entire codex repository to a temporary directory (`/tmp/fractary-codex-clone/`)
|
|
162
|
+
2. Scans ALL markdown files in the codex recursively
|
|
163
|
+
3. Evaluates `codex_sync_include` patterns in each file's frontmatter
|
|
164
|
+
4. Returns only files that match your project name or pattern
|
|
165
|
+
|
|
166
|
+
**Example frontmatter in source files:**
|
|
167
|
+
```yaml
|
|
168
|
+
---
|
|
169
|
+
codex_sync_include: ['*'] # Syncs to ALL projects
|
|
170
|
+
codex_sync_include: ['lake-*', 'api-*'] # Syncs to lake-* and api-* projects
|
|
171
|
+
codex_sync_exclude: ['*-test'] # Except *-test projects
|
|
172
|
+
---
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Trade-offs:**
|
|
176
|
+
- **Efficient for discovery**: Finds all relevant files across hundreds of projects
|
|
177
|
+
- **Inefficient for execution**: Clones entire codex repository (uses shallow clone for speed)
|
|
178
|
+
- **Best practice**: Use `codex://` URI references + cache purging for most workflows; use sync occasionally when needed
|
|
179
|
+
|
|
180
|
+
**Recommended workflow:**
|
|
181
|
+
```bash
|
|
182
|
+
# Primary: Reference files via codex:// URIs (no sync needed)
|
|
183
|
+
fractary-codex fetch codex://org/project/path/file.md
|
|
184
|
+
|
|
185
|
+
# When you need latest versions: Purge cache
|
|
186
|
+
fractary-codex cache clear --pattern "codex://org/project/*"
|
|
187
|
+
|
|
188
|
+
# Then MCP will re-fetch fresh content automatically
|
|
189
|
+
```
|
|
190
|
+
|
|
156
191
|
### `types` - Type Registry Management
|
|
157
192
|
|
|
158
193
|
Manage custom artifact types for classification and caching.
|
package/dist/cli.cjs
CHANGED
|
@@ -5,6 +5,8 @@ var fs = require('fs/promises');
|
|
|
5
5
|
var path3 = require('path');
|
|
6
6
|
var yaml = require('js-yaml');
|
|
7
7
|
var codex = require('@fractary/codex');
|
|
8
|
+
var os = require('os');
|
|
9
|
+
var child_process = require('child_process');
|
|
8
10
|
var commander = require('commander');
|
|
9
11
|
var chalk8 = require('chalk');
|
|
10
12
|
var crypto = require('crypto');
|
|
@@ -32,6 +34,7 @@ function _interopNamespace(e) {
|
|
|
32
34
|
var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
|
|
33
35
|
var path3__namespace = /*#__PURE__*/_interopNamespace(path3);
|
|
34
36
|
var yaml__namespace = /*#__PURE__*/_interopNamespace(yaml);
|
|
37
|
+
var os__namespace = /*#__PURE__*/_interopNamespace(os);
|
|
35
38
|
var chalk8__default = /*#__PURE__*/_interopDefault(chalk8);
|
|
36
39
|
var crypto__namespace = /*#__PURE__*/_interopNamespace(crypto);
|
|
37
40
|
|
|
@@ -619,6 +622,178 @@ var init_codex_client = __esm({
|
|
|
619
622
|
}
|
|
620
623
|
});
|
|
621
624
|
|
|
625
|
+
// src/utils/codex-repository.ts
|
|
626
|
+
var codex_repository_exports = {};
|
|
627
|
+
__export(codex_repository_exports, {
|
|
628
|
+
ensureCodexCloned: () => ensureCodexCloned,
|
|
629
|
+
getCodexRepoUrl: () => getCodexRepoUrl,
|
|
630
|
+
getTempCodexPath: () => getTempCodexPath,
|
|
631
|
+
isValidGitRepo: () => isValidGitRepo
|
|
632
|
+
});
|
|
633
|
+
function spawnAsync(command, args, options) {
|
|
634
|
+
return new Promise((resolve, reject) => {
|
|
635
|
+
const child = child_process.spawn(command, args, {
|
|
636
|
+
...options,
|
|
637
|
+
env: process.env
|
|
638
|
+
});
|
|
639
|
+
let stdout = "";
|
|
640
|
+
let stderr = "";
|
|
641
|
+
child.stdout.on("data", (data) => {
|
|
642
|
+
stdout += data.toString();
|
|
643
|
+
});
|
|
644
|
+
child.stderr.on("data", (data) => {
|
|
645
|
+
stderr += data.toString();
|
|
646
|
+
});
|
|
647
|
+
child.on("error", (error) => {
|
|
648
|
+
reject(error);
|
|
649
|
+
});
|
|
650
|
+
child.on("close", (code) => {
|
|
651
|
+
if (code !== 0) {
|
|
652
|
+
const error = new Error(stderr || `Command exited with code ${code}`);
|
|
653
|
+
error.code = code;
|
|
654
|
+
reject(error);
|
|
655
|
+
} else {
|
|
656
|
+
resolve(stdout);
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
function sanitizePathComponent(component) {
|
|
662
|
+
if (!component || typeof component !== "string") {
|
|
663
|
+
throw new Error("Path component must be a non-empty string");
|
|
664
|
+
}
|
|
665
|
+
const sanitized = component.replace(/\.\./g, "").replace(/[/\\]/g, "").trim();
|
|
666
|
+
if (!sanitized) {
|
|
667
|
+
throw new Error(`Invalid path component: ${component}`);
|
|
668
|
+
}
|
|
669
|
+
return sanitized;
|
|
670
|
+
}
|
|
671
|
+
function validateGitHubName(name, type) {
|
|
672
|
+
if (!name || typeof name !== "string") {
|
|
673
|
+
throw new Error(`GitHub ${type} name must be a non-empty string`);
|
|
674
|
+
}
|
|
675
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(name)) {
|
|
676
|
+
throw new Error(`Invalid GitHub ${type} name: ${name}. Must contain only alphanumeric characters, hyphens, underscores, and dots.`);
|
|
677
|
+
}
|
|
678
|
+
if (name.startsWith(".") || name.startsWith("-")) {
|
|
679
|
+
throw new Error(`GitHub ${type} name cannot start with a dot or hyphen: ${name}`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
function getTempCodexPath(config) {
|
|
683
|
+
const codexRepo = config.codex_repository || "codex";
|
|
684
|
+
const sanitizedOrg = sanitizePathComponent(config.organization);
|
|
685
|
+
const sanitizedRepo = sanitizePathComponent(codexRepo);
|
|
686
|
+
return path3__namespace.join(
|
|
687
|
+
os__namespace.tmpdir(),
|
|
688
|
+
"fractary-codex-clone",
|
|
689
|
+
`${sanitizedOrg}-${sanitizedRepo}-${process.pid}`
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
async function isValidGitRepo(repoPath) {
|
|
693
|
+
try {
|
|
694
|
+
const gitDir = path3__namespace.join(repoPath, ".git");
|
|
695
|
+
const stats = await fs__namespace.stat(gitDir);
|
|
696
|
+
return stats.isDirectory();
|
|
697
|
+
} catch {
|
|
698
|
+
return false;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
function getCodexRepoUrl(config) {
|
|
702
|
+
const codexRepo = config.codex_repository || "codex";
|
|
703
|
+
validateGitHubName(config.organization, "organization");
|
|
704
|
+
validateGitHubName(codexRepo, "repository");
|
|
705
|
+
return `https://github.com/${config.organization}/${codexRepo}.git`;
|
|
706
|
+
}
|
|
707
|
+
async function execGit(repoPath, args) {
|
|
708
|
+
try {
|
|
709
|
+
const stdout = await spawnAsync("git", args, {
|
|
710
|
+
cwd: repoPath
|
|
711
|
+
});
|
|
712
|
+
return stdout;
|
|
713
|
+
} catch (error) {
|
|
714
|
+
if (error.code === "ENOENT") {
|
|
715
|
+
throw new Error(`Git command not found. Ensure git is installed and in PATH.`);
|
|
716
|
+
}
|
|
717
|
+
if (error.code === "EACCES") {
|
|
718
|
+
throw new Error(`Permission denied accessing repository at ${repoPath}`);
|
|
719
|
+
}
|
|
720
|
+
if (error.code === 128) {
|
|
721
|
+
throw new Error(`Git authentication failed. Check your credentials and repository access.`);
|
|
722
|
+
}
|
|
723
|
+
throw error;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
async function gitClone(url, targetPath, options) {
|
|
727
|
+
const parentDir = path3__namespace.dirname(targetPath);
|
|
728
|
+
await fs__namespace.mkdir(parentDir, { recursive: true });
|
|
729
|
+
const args = ["clone"];
|
|
730
|
+
if (options?.depth) {
|
|
731
|
+
if (!Number.isInteger(options.depth) || options.depth <= 0) {
|
|
732
|
+
throw new Error(`Invalid depth parameter: ${options.depth}. Must be a positive integer.`);
|
|
733
|
+
}
|
|
734
|
+
args.push("--depth", String(options.depth));
|
|
735
|
+
}
|
|
736
|
+
if (options?.branch) {
|
|
737
|
+
if (!/^[\w\-./]+$/.test(options.branch)) {
|
|
738
|
+
throw new Error(`Invalid branch name: ${options.branch}`);
|
|
739
|
+
}
|
|
740
|
+
args.push("--branch", options.branch);
|
|
741
|
+
}
|
|
742
|
+
args.push("--single-branch");
|
|
743
|
+
args.push(url, targetPath);
|
|
744
|
+
await spawnAsync("git", args, { cwd: parentDir });
|
|
745
|
+
}
|
|
746
|
+
async function gitFetch(repoPath, branch) {
|
|
747
|
+
{
|
|
748
|
+
if (!/^[\w\-./]+$/.test(branch)) {
|
|
749
|
+
throw new Error(`Invalid branch name: ${branch}`);
|
|
750
|
+
}
|
|
751
|
+
await execGit(repoPath, ["fetch", "origin", `${branch}:${branch}`]);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
async function gitCheckout(repoPath, branch) {
|
|
755
|
+
if (!/^[\w\-./]+$/.test(branch)) {
|
|
756
|
+
throw new Error(`Invalid branch name: ${branch}`);
|
|
757
|
+
}
|
|
758
|
+
await execGit(repoPath, ["checkout", branch]);
|
|
759
|
+
}
|
|
760
|
+
async function gitPull(repoPath) {
|
|
761
|
+
await execGit(repoPath, ["pull"]);
|
|
762
|
+
}
|
|
763
|
+
async function ensureCodexCloned(config, options) {
|
|
764
|
+
const tempPath = getTempCodexPath(config);
|
|
765
|
+
const branch = options?.branch || "main";
|
|
766
|
+
if (await isValidGitRepo(tempPath) && !options?.force) {
|
|
767
|
+
try {
|
|
768
|
+
await gitFetch(tempPath, branch);
|
|
769
|
+
await gitCheckout(tempPath, branch);
|
|
770
|
+
await gitPull(tempPath);
|
|
771
|
+
return tempPath;
|
|
772
|
+
} catch (error) {
|
|
773
|
+
console.warn(`Failed to update existing clone: ${error.message}`);
|
|
774
|
+
console.warn(`Removing and cloning fresh...`);
|
|
775
|
+
await fs__namespace.rm(tempPath, { recursive: true, force: true });
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
const repoUrl = getCodexRepoUrl(config);
|
|
779
|
+
try {
|
|
780
|
+
await fs__namespace.rm(tempPath, { recursive: true, force: true });
|
|
781
|
+
} catch (error) {
|
|
782
|
+
console.warn(`Could not remove existing directory ${tempPath}: ${error.message}`);
|
|
783
|
+
}
|
|
784
|
+
await gitClone(repoUrl, tempPath, {
|
|
785
|
+
branch,
|
|
786
|
+
depth: 1
|
|
787
|
+
// Shallow clone for efficiency
|
|
788
|
+
});
|
|
789
|
+
return tempPath;
|
|
790
|
+
}
|
|
791
|
+
var init_codex_repository = __esm({
|
|
792
|
+
"src/utils/codex-repository.ts"() {
|
|
793
|
+
init_cjs_shims();
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
|
|
622
797
|
// src/cli.ts
|
|
623
798
|
init_cjs_shims();
|
|
624
799
|
|
|
@@ -1501,15 +1676,50 @@ function syncCommand() {
|
|
|
1501
1676
|
let plan;
|
|
1502
1677
|
let routingScan;
|
|
1503
1678
|
if (direction === "from-codex") {
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
}
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1679
|
+
let codexRepoPath;
|
|
1680
|
+
try {
|
|
1681
|
+
const { ensureCodexCloned: ensureCodexCloned2 } = await Promise.resolve().then(() => (init_codex_repository(), codex_repository_exports));
|
|
1682
|
+
if (!options.json) {
|
|
1683
|
+
console.log(chalk8__default.default.blue("\u2139 Cloning/updating codex repository..."));
|
|
1684
|
+
}
|
|
1685
|
+
codexRepoPath = await ensureCodexCloned2(config, {
|
|
1686
|
+
branch: targetBranch
|
|
1687
|
+
});
|
|
1688
|
+
if (!options.json) {
|
|
1689
|
+
console.log(chalk8__default.default.dim(` Codex cloned to: ${codexRepoPath}`));
|
|
1690
|
+
console.log(chalk8__default.default.dim(" Scanning for files routing to this project...\n"));
|
|
1691
|
+
} else {
|
|
1692
|
+
console.log(JSON.stringify({
|
|
1693
|
+
info: "Routing-aware sync: cloned codex repository and scanning for files targeting this project",
|
|
1694
|
+
codexPath: codexRepoPath
|
|
1695
|
+
}, null, 2));
|
|
1696
|
+
}
|
|
1697
|
+
} catch (error) {
|
|
1698
|
+
console.error(chalk8__default.default.red("Error:"), "Failed to clone codex repository");
|
|
1699
|
+
console.error(chalk8__default.default.dim(` ${error.message}`));
|
|
1700
|
+
console.log(chalk8__default.default.yellow("\nTroubleshooting:"));
|
|
1701
|
+
if (error.message.includes("Git command not found")) {
|
|
1702
|
+
console.log(chalk8__default.default.dim(" Git is not installed or not in PATH."));
|
|
1703
|
+
console.log(chalk8__default.default.dim(" Install git: https://git-scm.com/downloads"));
|
|
1704
|
+
} else if (error.message.includes("authentication failed") || error.message.includes("Authentication failed")) {
|
|
1705
|
+
console.log(chalk8__default.default.dim(" GitHub authentication is required for private repositories."));
|
|
1706
|
+
console.log(chalk8__default.default.dim(" 1. Check auth status: gh auth status"));
|
|
1707
|
+
console.log(chalk8__default.default.dim(" 2. Login if needed: gh auth login"));
|
|
1708
|
+
console.log(chalk8__default.default.dim(" 3. Or set GITHUB_TOKEN environment variable"));
|
|
1709
|
+
} else if (error.message.includes("Permission denied")) {
|
|
1710
|
+
console.log(chalk8__default.default.dim(" Permission denied accessing repository files."));
|
|
1711
|
+
console.log(chalk8__default.default.dim(" 1. Check file/directory permissions"));
|
|
1712
|
+
console.log(chalk8__default.default.dim(" 2. Ensure you have access to the repository"));
|
|
1713
|
+
} else if (error.message.includes("not found") || error.message.includes("does not exist")) {
|
|
1714
|
+
console.log(chalk8__default.default.dim(` Repository not found: ${config.organization}/${config.codex_repository || "codex"}`));
|
|
1715
|
+
console.log(chalk8__default.default.dim(" 1. Verify the repository exists on GitHub"));
|
|
1716
|
+
console.log(chalk8__default.default.dim(" 2. Check organization and repository names in config"));
|
|
1717
|
+
} else {
|
|
1718
|
+
console.log(chalk8__default.default.dim(" 1. Ensure git is installed: git --version"));
|
|
1719
|
+
console.log(chalk8__default.default.dim(" 2. Check GitHub auth: gh auth status"));
|
|
1720
|
+
console.log(chalk8__default.default.dim(` 3. Verify repo exists: ${config.organization}/${config.codex_repository || "codex"}`));
|
|
1721
|
+
}
|
|
1722
|
+
process.exit(1);
|
|
1513
1723
|
}
|
|
1514
1724
|
const planWithRouting = await syncManager.createRoutingAwarePlan(
|
|
1515
1725
|
config.organization,
|