@fractary/codex 0.7.1 → 0.8.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/dist/index.cjs CHANGED
@@ -6,6 +6,7 @@ var child_process = require('child_process');
6
6
  var zod = require('zod');
7
7
  var yaml = require('js-yaml');
8
8
  var fs2 = require('fs/promises');
9
+ var util = require('util');
9
10
 
10
11
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
11
12
 
@@ -843,6 +844,16 @@ var DirectionalSyncSchema = zod.z.object({
843
844
  default_to_codex: zod.z.array(zod.z.string()).optional(),
844
845
  default_from_codex: zod.z.array(zod.z.string()).optional()
845
846
  });
847
+ var ArchiveProjectConfigSchema = zod.z.object({
848
+ enabled: zod.z.boolean(),
849
+ handler: zod.z.enum(["s3", "r2", "gcs", "local"]),
850
+ bucket: zod.z.string().optional(),
851
+ prefix: zod.z.string().optional(),
852
+ patterns: zod.z.array(zod.z.string()).optional()
853
+ });
854
+ var ArchiveConfigSchema = zod.z.object({
855
+ projects: zod.z.record(ArchiveProjectConfigSchema)
856
+ });
846
857
  var CodexConfigSchema = zod.z.object({
847
858
  organizationSlug: zod.z.string(),
848
859
  directories: zod.z.object({
@@ -852,7 +863,9 @@ var CodexConfigSchema = zod.z.object({
852
863
  }).optional(),
853
864
  rules: SyncRulesSchema.optional(),
854
865
  // Directional sync configuration
855
- sync: DirectionalSyncSchema.optional()
866
+ sync: DirectionalSyncSchema.optional(),
867
+ // Archive configuration
868
+ archive: ArchiveConfigSchema.optional()
856
869
  }).strict();
857
870
  function parseMetadata(content, options = {}) {
858
871
  const { strict = true, normalize = true } = options;
@@ -977,11 +990,17 @@ function getDefaultRules() {
977
990
  defaultExclude: []
978
991
  };
979
992
  }
993
+ function getDefaultArchiveConfig() {
994
+ return {
995
+ projects: {}
996
+ };
997
+ }
980
998
  function getDefaultConfig(orgSlug) {
981
999
  return {
982
1000
  organizationSlug: orgSlug,
983
1001
  directories: getDefaultDirectories(orgSlug),
984
- rules: getDefaultRules()
1002
+ rules: getDefaultRules(),
1003
+ archive: getDefaultArchiveConfig()
985
1004
  };
986
1005
  }
987
1006
 
@@ -1703,6 +1722,190 @@ var HttpStorage = class {
1703
1722
  function createHttpStorage(options) {
1704
1723
  return new HttpStorage(options);
1705
1724
  }
1725
+ var execFileAsync = util.promisify(child_process.execFile);
1726
+ async function execFileNoThrow(command, args = [], options) {
1727
+ try {
1728
+ const { stdout, stderr } = await execFileAsync(command, args, {
1729
+ ...options,
1730
+ maxBuffer: options?.maxBuffer || 1024 * 1024 * 10
1731
+ // 10MB default
1732
+ });
1733
+ return {
1734
+ stdout: stdout || "",
1735
+ stderr: stderr || "",
1736
+ exitCode: 0
1737
+ };
1738
+ } catch (error) {
1739
+ const exitCode = typeof error.exitCode === "number" ? error.exitCode : 1;
1740
+ return {
1741
+ stdout: error.stdout || "",
1742
+ stderr: error.stderr || error.message || "",
1743
+ exitCode
1744
+ };
1745
+ }
1746
+ }
1747
+
1748
+ // src/storage/s3-archive.ts
1749
+ var S3ArchiveStorage = class {
1750
+ name = "s3-archive";
1751
+ type = "s3-archive";
1752
+ projects;
1753
+ fractaryCli;
1754
+ constructor(options = {}) {
1755
+ this.projects = options.projects || {};
1756
+ this.fractaryCli = options.fractaryCli || "fractary";
1757
+ }
1758
+ /**
1759
+ * Check if this provider can handle the reference
1760
+ *
1761
+ * S3 Archive provider handles references that:
1762
+ * 1. Are for the current project (same org/project)
1763
+ * 2. Have archive enabled in config
1764
+ * 3. Match configured patterns (if specified)
1765
+ */
1766
+ canHandle(reference) {
1767
+ if (!reference.isCurrentProject) {
1768
+ return false;
1769
+ }
1770
+ const projectKey = `${reference.org}/${reference.project}`;
1771
+ const config = this.projects[projectKey];
1772
+ if (!config || !config.enabled) {
1773
+ return false;
1774
+ }
1775
+ if (config.patterns && config.patterns.length > 0) {
1776
+ return this.matchesPatterns(reference.path, config.patterns);
1777
+ }
1778
+ return true;
1779
+ }
1780
+ /**
1781
+ * Fetch content from S3 archive via fractary-file CLI
1782
+ */
1783
+ async fetch(reference, options) {
1784
+ const opts = mergeFetchOptions(options);
1785
+ const projectKey = `${reference.org}/${reference.project}`;
1786
+ const config = this.projects[projectKey];
1787
+ if (!config) {
1788
+ throw new Error(`No archive config for project: ${projectKey}`);
1789
+ }
1790
+ const archivePath = this.calculateArchivePath(reference, config);
1791
+ try {
1792
+ const result = await execFileNoThrow(
1793
+ this.fractaryCli,
1794
+ [
1795
+ "file",
1796
+ "read",
1797
+ "--remote-path",
1798
+ archivePath,
1799
+ "--handler",
1800
+ config.handler,
1801
+ ...config.bucket ? ["--bucket", config.bucket] : []
1802
+ ],
1803
+ {
1804
+ timeout: opts.timeout
1805
+ }
1806
+ );
1807
+ if (result.exitCode !== 0) {
1808
+ throw new Error(`fractary-file read failed: ${result.stderr}`);
1809
+ }
1810
+ const content = Buffer.from(result.stdout);
1811
+ return {
1812
+ content,
1813
+ contentType: detectContentType(reference.path),
1814
+ size: content.length,
1815
+ source: "s3-archive",
1816
+ metadata: {
1817
+ archivePath,
1818
+ bucket: config.bucket,
1819
+ handler: config.handler
1820
+ }
1821
+ };
1822
+ } catch (error) {
1823
+ const message = error instanceof Error ? error.message : String(error);
1824
+ throw new Error(`Failed to fetch from archive: ${message}`);
1825
+ }
1826
+ }
1827
+ /**
1828
+ * Check if archived file exists
1829
+ *
1830
+ * Note: This currently downloads the file to check existence.
1831
+ * TODO: Optimize by using fractary-file 'stat' or 'head' command when available
1832
+ * to avoid downloading full file for existence checks.
1833
+ */
1834
+ async exists(reference, options) {
1835
+ const projectKey = `${reference.org}/${reference.project}`;
1836
+ const config = this.projects[projectKey];
1837
+ if (!config) {
1838
+ return false;
1839
+ }
1840
+ try {
1841
+ await this.fetch(reference, { ...options, timeout: 5e3 });
1842
+ return true;
1843
+ } catch {
1844
+ return false;
1845
+ }
1846
+ }
1847
+ /**
1848
+ * Calculate archive path from reference
1849
+ *
1850
+ * Pattern: {prefix}/{type}/{org}/{project}/{original-path}
1851
+ *
1852
+ * Examples (with default prefix "archive/"):
1853
+ * specs/WORK-123.md → archive/specs/org/project/specs/WORK-123.md
1854
+ * docs/api.md → archive/docs/org/project/docs/api.md
1855
+ *
1856
+ * Examples (with custom prefix "archived-docs/"):
1857
+ * specs/WORK-123.md → archived-docs/specs/org/project/specs/WORK-123.md
1858
+ */
1859
+ calculateArchivePath(reference, config) {
1860
+ const type = this.detectType(reference.path);
1861
+ const prefix = config.prefix || "archive/";
1862
+ const trimmedPrefix = prefix.trim();
1863
+ if (!trimmedPrefix) {
1864
+ throw new Error("Archive prefix cannot be empty or whitespace-only");
1865
+ }
1866
+ const normalizedPrefix = trimmedPrefix.endsWith("/") ? trimmedPrefix : `${trimmedPrefix}/`;
1867
+ return `${normalizedPrefix}${type}/${reference.org}/${reference.project}/${reference.path}`;
1868
+ }
1869
+ /**
1870
+ * Detect artifact type from path
1871
+ *
1872
+ * Used to organize archives by type
1873
+ */
1874
+ detectType(path6) {
1875
+ if (path6.startsWith("specs/")) return "specs";
1876
+ if (path6.startsWith("docs/")) return "docs";
1877
+ if (path6.includes("/logs/")) return "logs";
1878
+ return "misc";
1879
+ }
1880
+ /**
1881
+ * Check if path matches any of the patterns
1882
+ *
1883
+ * Supports glob-style patterns:
1884
+ * - specs/** (all files in specs/)
1885
+ * - *.md (all markdown files)
1886
+ * - docs/*.md (markdown files in docs/)
1887
+ */
1888
+ matchesPatterns(path6, patterns) {
1889
+ for (const pattern of patterns) {
1890
+ if (this.matchesPattern(path6, pattern)) {
1891
+ return true;
1892
+ }
1893
+ }
1894
+ return false;
1895
+ }
1896
+ /**
1897
+ * Check if path matches a single pattern
1898
+ */
1899
+ matchesPattern(path6, pattern) {
1900
+ const DOUBLE_STAR = "\0DOUBLE_STAR\0";
1901
+ let regexPattern = pattern.replace(/\*\*/g, DOUBLE_STAR);
1902
+ regexPattern = regexPattern.replace(/[.[\](){}+^$|\\]/g, "\\$&");
1903
+ regexPattern = regexPattern.replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]");
1904
+ regexPattern = regexPattern.replace(new RegExp(DOUBLE_STAR, "g"), ".*");
1905
+ const regex = new RegExp(`^${regexPattern}$`);
1906
+ return regex.test(path6);
1907
+ }
1908
+ };
1706
1909
 
1707
1910
  // src/storage/manager.ts
1708
1911
  var StorageManager = class {
@@ -1712,7 +1915,10 @@ var StorageManager = class {
1712
1915
  this.providers.set("local", new LocalStorage(config.local));
1713
1916
  this.providers.set("github", new GitHubStorage(config.github));
1714
1917
  this.providers.set("http", new HttpStorage(config.http));
1715
- this.priority = config.priority || ["local", "github", "http"];
1918
+ if (config.s3Archive) {
1919
+ this.providers.set("s3-archive", new S3ArchiveStorage(config.s3Archive));
1920
+ }
1921
+ this.priority = config.priority || (config.s3Archive ? ["local", "s3-archive", "github", "http"] : ["local", "github", "http"]);
1716
1922
  }
1717
1923
  /**
1718
1924
  * Register a custom storage provider