docs-cache 0.1.4 → 0.3.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 CHANGED
@@ -19,7 +19,7 @@ Documentation is cached in a gitignored location, exposed to agent and tool targ
19
19
  - **Fast**: Local cache avoids network roundtrips after sync.
20
20
  - **Flexible**: Cache full repos or just the subdirectories you need.
21
21
 
22
- > **Note**: Sources are downloaded to a local cache. If you provide a `targetDir`, `docs-cache` creates a symlink or copy from the cache to that target directory. The target should be outside `.docs`.
22
+ > **Note**: Sources are downloaded to a local cache. If you provide a `targetDir`, `docs-cache` creates a symlink or copy from the cache to that target directory.
23
23
 
24
24
  ## Usage
25
25
 
@@ -73,26 +73,60 @@ npx docs-cache clean
73
73
 
74
74
  ### Options
75
75
 
76
- | Field | Type | Description |
77
- | ---------- | ------- | ---------------------------------------- |
78
- | `cacheDir` | string | Directory for cache, defaults to `.docs` |
79
- | `index` | boolean | Write `index.json` summary file |
80
- | `sources` | array | List of repositories to sync |
81
- | `defaults` | object | Default settings for all sources |
76
+ **Top-level**
82
77
 
83
- **Source Options:**
78
+ | Field | Details | Required |
79
+ | ---------- | -------------------------------------- | -------- |
80
+ | `cacheDir` | Directory for cache. Default: `.docs`. | Optional |
81
+ | `sources` | List of repositories to sync. | Required |
82
+ | `defaults` | Default settings for all sources. | Optional |
84
83
 
85
- - `repo`: Git URL
86
- - `ref`: Branch, tag, or commit
87
- - `include`: Glob patterns to copy, defaults to `"**/*.{md,mdx,markdown,mkd,txt,rst,adoc,asciidoc}"`,
88
- - `exclude`: Glob patterns to skip
89
- - `targetDir`: Optional path where files should be symlinked/copied to, outside `.docs`
90
- - `targetMode`: Defaults to `symlink` on Unix and `copy` on Windows
91
- - `required`: Whether missing sources should fail in offline/strict runs
92
- - `maxBytes`: Maximum total bytes to materialize for the source
93
- - `maxFiles`: Maximum total files to materialize for the source
84
+ <details>
85
+ <summary>Show default and source options</summary>
94
86
 
95
- > **Note**: Sources are always downloaded to `.docs/<id>/`. If you provide a `targetDir`, `docs-cache` will create a symlink or copy pointing from the cache to that target directory. The target should be outside `.docs`.
87
+ ### Default options
88
+
89
+ All fields in `defaults` apply to all sources unless overridden per-source.
90
+
91
+ | Field | Details |
92
+ | ------------ | --------------------------------------------------------------------------------------------------------- |
93
+ | `ref` | Branch, tag, or commit. Default: `"HEAD"`. |
94
+ | `mode` | Cache mode. Default: `"materialize"`. |
95
+ | `include` | Glob patterns to copy. Default: `["**/*.{md,mdx,markdown,mkd,txt,rst,adoc,asciidoc}"]`. |
96
+ | `targetMode` | How to link or copy from the cache to the destination. Default: `"symlink"` on Unix, `"copy"` on Windows. |
97
+ | `depth` | Git clone depth. Default: `1`. |
98
+ | `required` | Whether missing sources should fail. Default: `true`. |
99
+ | `maxBytes` | Maximum total bytes to materialize. Default: `200000000` (200 MB). |
100
+ | `maxFiles` | Maximum total files to materialize. |
101
+ | `allowHosts` | Allowed Git hosts. Default: `["github.com", "gitlab.com"]`. |
102
+ | `toc` | Generate per-source `TOC.md`. Default: `true`. Supports `true`, `false`, or a format (`"tree"`, `"compressed"`). |
103
+
104
+ ### Source options
105
+
106
+ #### Required
107
+
108
+ | Field | Details |
109
+ | ------ | --------------------------------- |
110
+ | `repo` | Git URL. |
111
+ | `id` | Unique identifier for the source. |
112
+
113
+ #### Optional
114
+
115
+ | Field | Details |
116
+ | ------------ | ---------------------------------------------------------------- |
117
+ | `ref` | Branch, tag, or commit. |
118
+ | `include` | Glob patterns to copy. |
119
+ | `exclude` | Glob patterns to skip. |
120
+ | `targetDir` | Path where files should be symlinked/copied to, outside `.docs`. |
121
+ | `targetMode` | How to link or copy from the cache to the destination. |
122
+ | `required` | Whether missing sources should fail. |
123
+ | `maxBytes` | Maximum total bytes to materialize. |
124
+ | `maxFiles` | Maximum total files to materialize. |
125
+ | `toc` | Generate per-source `TOC.md`. Supports `true`, `false`, or a format (`"tree"`, `"compressed"`). |
126
+
127
+ > **Note**: Sources are always downloaded to `.docs/<id>/`. If you provide a `targetDir`, `docs-cache` will create a symlink or copy pointing from the cache to that target directory. The target should be outside `.docs`. Git operation timeout is configured via the `--timeout-ms` CLI flag, not as a per-source configuration option.
128
+
129
+ </details>
96
130
 
97
131
  ## NPM Integration
98
132
 
@@ -101,7 +135,7 @@ Use `postinstall` to ensure documentation is available locally immediately after
101
135
  ```json
102
136
  {
103
137
  "scripts": {
104
- "postinstall": "npx docs-cache sync"
138
+ "postinstall": "npx docs-cache sync --prune"
105
139
  }
106
140
  }
107
141
  ```
@@ -1,3 +1,3 @@
1
- import{readFile as D,writeFile as b,access as k}from"node:fs/promises";import l from"node:path";import{v,D as P,a as E,s as F,w as I,b as U,r as S}from"../shared/docs-cache.D4Fth4X8.mjs";import{e as A}from"../shared/docs-cache.DgrUGcWv.mjs";import{r as N}from"../shared/docs-cache.D9_kM5zq.mjs";import{r as O}from"../shared/docs-cache.BSvQNKuf.mjs";import"zod";import"node:process";import"cac";import"picocolors";const u=async o=>{try{return await k(o),!0}catch{return!1}},y="package.json",x=async o=>{const i=await D(o,"utf8"),e=JSON.parse(i),t=e["docs-cache"];return t?{parsed:e,config:v(t)}:{parsed:e,config:null}},J=async o=>{if(o){const t=S(o);return{resolvedPath:t,mode:l.basename(t)===y?"package":"config"}}const i=S();if(await u(i))return{resolvedPath:i,mode:"config"};const e=l.resolve(process.cwd(),y);return await u(e)&&(await x(e)).config?{resolvedPath:e,mode:"package"}:{resolvedPath:i,mode:"config"}},T=async o=>{const i=await J(o.configPath),e=i.resolvedPath;let t=P,a=null,g=null,d=!1;if(await u(e))if(i.mode==="package"){const r=await x(e);g=r.parsed,a=r.config,t=a??P,d=!!a}else{const r=await D(e,"utf8");a=JSON.parse(r.toString()),t=v(a),d=!0}const C="https://raw.githubusercontent.com/fbosch/docs-cache/main/docs.config.schema.json",p=new Set(t.sources.map(r=>r.id)),m=[],f=o.entries.map(r=>{const n=O(r.repo),w=r.id||n.inferredId;if(!w)throw new Error("Unable to infer id. Provide an explicit id.");const c=E(w,"source id");return p.has(c)?(m.push(c),null):(p.add(c),r.targetDir&&N(e,r.targetDir),{id:c,repo:n.repoUrl,...r.targetDir?{targetDir:r.targetDir}:{},...n.ref?{ref:n.ref}:{}})}).filter(Boolean);if(f.length===0)throw new Error("All sources already exist in config.");const s={$schema:C,sources:[...t.sources,...f]};if(a?.cacheDir&&(s.cacheDir=a.cacheDir),a?.index!==void 0&&(s.index=a.index),a?.defaults&&(s.defaults=a.defaults),i.mode==="package"){const r=g??{};r["docs-cache"]=F(s),await b(e,`${JSON.stringify(r,null,2)}
2
- `,"utf8")}else await I(e,s);const h=d?null:await A(l.dirname(e),a?.cacheDir??U);return{configPath:e,sources:f,skipped:m,created:!0,gitignoreUpdated:h?.updated??!1,gitignorePath:h?.gitignorePath??null}};export{T as addSources};
1
+ import{readFile as D,writeFile as k,access as E}from"node:fs/promises";import l from"node:path";import{v,D as P,a as A,s as F,w as I,b as U,r as S}from"../shared/docs-cache.BWEcxcrg.mjs";import{e as N}from"../shared/docs-cache.C96VehGu.mjs";import{r as O}from"../shared/docs-cache.DDgb7yxQ.mjs";import{r as x}from"../shared/docs-cache.BSvQNKuf.mjs";import"zod";import"node:process";import"cac";import"picocolors";const u=async o=>{try{return await E(o),!0}catch{return!1}},y="package.json",C=async o=>{const i=await D(o,"utf8"),e=JSON.parse(i),a=e["docs-cache"];return a?{parsed:e,config:v(a)}:{parsed:e,config:null}},J=async o=>{if(o){const a=S(o);return{resolvedPath:a,mode:l.basename(a)===y?"package":"config"}}const i=S();if(await u(i))return{resolvedPath:i,mode:"config"};const e=l.resolve(process.cwd(),y);return await u(e)&&(await C(e)).config?{resolvedPath:e,mode:"package"}:{resolvedPath:i,mode:"config"}},T=async o=>{const i=await J(o.configPath),e=i.resolvedPath;let a=P,t=null,g=null,f=!1;if(await u(e))if(i.mode==="package"){const r=await C(e);g=r.parsed,t=r.config,a=t??P,f=!!t}else{const r=await D(e,"utf8");t=JSON.parse(r.toString()),a=v(t),f=!0}const b="https://raw.githubusercontent.com/fbosch/docs-cache/main/docs.config.schema.json",p=new Set(a.sources.map(r=>r.id)),m=[],d=o.entries.map(r=>{const n=x(r.repo),w=r.id||n.inferredId;if(!w)throw new Error("Unable to infer id. Provide an explicit id.");const c=A(w,"source id");return p.has(c)?(m.push(c),null):(p.add(c),r.targetDir&&O(e,r.targetDir),{id:c,repo:n.repoUrl,...r.targetDir?{targetDir:r.targetDir}:{},...n.ref?{ref:n.ref}:{}})}).filter(Boolean);if(d.length===0)throw new Error("All sources already exist in config.");const s={$schema:b,sources:[...a.sources,...d]};if(t?.cacheDir&&(s.cacheDir=t.cacheDir),t?.defaults&&(s.defaults=t.defaults),i.mode==="package"){const r=g??{};r["docs-cache"]=F(s),await k(e,`${JSON.stringify(r,null,2)}
2
+ `,"utf8")}else await I(e,s);const h=f?null:await N(l.dirname(e),t?.cacheDir??U);return{configPath:e,sources:d,skipped:m,created:!0,gitignoreUpdated:h?.updated??!1,gitignorePath:h?.gitignorePath??null}};export{T as addSources};
3
3
  //# sourceMappingURL=add.mjs.map
@@ -0,0 +1,2 @@
1
+ import{rm as n,readdir as o,stat as m}from"node:fs/promises";import u from"node:path";import{r as f,e as w}from"../shared/docs-cache.kK1DPQIQ.mjs";import"node:os";const c=async t=>{try{const e=await o(t,{withFileTypes:!0});let r=0;for(const a of e){const i=u.join(t,a.name);if(a.isDirectory())r+=await c(i);else{const s=await m(i);r+=s.size}}return r}catch{return 0}},h=async t=>{try{return(await o(t)).length}catch{return 0}},p=async()=>{const t=f();if(!await w(t))return{removed:!1,cacheDir:t};const e=await h(t),r=await c(t);return await n(t,{recursive:!0,force:!0}),{removed:!0,cacheDir:t,repoCount:e,bytesFreed:r}};export{p as cleanGitCache};
2
+ //# sourceMappingURL=clean-git-cache.mjs.map
@@ -1,2 +1,2 @@
1
- import{rm as o,access as i}from"node:fs/promises";import{l as s,b as m}from"../shared/docs-cache.D4Fth4X8.mjs";import{a as n}from"../shared/docs-cache.D9_kM5zq.mjs";import"node:path";import"zod";import"node:process";import"cac";import"picocolors";const f=async r=>{try{return await i(r),!0}catch{return!1}},p=async r=>{const{config:t,resolvedPath:c}=await s(r.configPath),a=n(c,t.cacheDir??m,r.cacheDirOverride),e=await f(a);return e&&await o(a,{recursive:!0,force:!0}),{cacheDir:a,removed:e}};export{p as cleanCache};
1
+ import{rm as o,access as i}from"node:fs/promises";import{l as s,b as m}from"../shared/docs-cache.BWEcxcrg.mjs";import{b as n}from"../shared/docs-cache.DDgb7yxQ.mjs";import"node:path";import"zod";import"node:process";import"cac";import"picocolors";const f=async r=>{try{return await i(r),!0}catch{return!1}},p=async r=>{const{config:t,resolvedPath:c}=await s(r.configPath),a=n(c,t.cacheDir??m,r.cacheDirOverride),e=await f(a);return e&&await o(a,{recursive:!0,force:!0}),{cacheDir:a,removed:e}};export{p as cleanCache};
2
2
  //# sourceMappingURL=clean.mjs.map
@@ -1,3 +1,3 @@
1
- import{readFile as y,writeFile as N,access as S}from"node:fs/promises";import r from"node:path";import{confirm as V,isCancel as G,select as J,text as U}from"@clack/prompts";import{c as _,b as n,s as L,w as T}from"../shared/docs-cache.D4Fth4X8.mjs";import{g as H,e as F}from"../shared/docs-cache.DgrUGcWv.mjs";import"zod";import"../shared/docs-cache.D9_kM5zq.mjs";import"node:process";import"cac";import"picocolors";const h=async c=>{try{return await S(c),!0}catch{return!1}},M=async(c,s={})=>{const x=s.confirm??V,l=s.isCancel??G,O=s.select??J,$=s.text??U,f=c.cwd??process.cwd(),d=r.resolve(f,_),i=r.resolve(f,"package.json"),g=[];if(await h(d)&&g.push(d),await h(i)){const t=await y(i,"utf8");JSON.parse(t)["docs-cache"]&&g.push(i)}if(g.length>0)throw new Error(`Config already exists at ${g.join(", ")}. Init aborted.`);let C=!1;if(await h(i)){const t=await y(i,"utf8");if(!JSON.parse(t)["docs-cache"]){const o=await O({message:"Config location",options:[{value:"config",label:"docs.config.json"},{value:"package",label:"package.json"}],initialValue:"config"});if(l(o))throw new Error("Init cancelled.");C=o==="package"}}const k=C?i:d,v=c.cacheDirOverride??n,u=await $({message:"Cache directory",initialValue:v});if(l(u))throw new Error("Init cancelled.");const A=u||n,b=await x({message:"Generate index.json (summary of cached sources + paths for tools)",initialValue:!1});if(l(b))throw new Error("Init cancelled.");const D=await H(f,A);let j=!1;if(D.entry&&!D.hasEntry){const t=await x({message:"Add cache directory to .gitignore",initialValue:!0});if(l(t))throw new Error("Init cancelled.");j=t}const a={configPath:k,cacheDir:u,index:b,gitignore:j},e=r.resolve(f,a.configPath);if(r.basename(e)==="package.json"){const t=await y(e,"utf8"),o=JSON.parse(t);if(o["docs-cache"])throw new Error(`docs-cache config already exists in ${e}.`);const p={$schema:"https://raw.githubusercontent.com/fbosch/docs-cache/main/docs.config.schema.json",sources:[]},E=a.cacheDir||n;E!==n&&(p.cacheDir=E),a.index&&(p.index=!0),o["docs-cache"]=L(p),await N(e,`${JSON.stringify(o,null,2)}
2
- `,"utf8");const P=a.gitignore?await F(r.dirname(e),E):null;return{configPath:e,created:!0,gitignoreUpdated:P?.updated??!1,gitignorePath:P?.gitignorePath??null}}if(await h(e))throw new Error(`Config already exists at ${e}.`);const w={$schema:"https://raw.githubusercontent.com/fbosch/docs-cache/main/docs.config.schema.json",sources:[]},m=a.cacheDir||n;m!==n&&(w.cacheDir=m),a.index&&(w.index=!0),await T(e,w);const I=a.gitignore?await F(r.dirname(e),m):null;return{configPath:e,created:!0,gitignoreUpdated:I?.updated??!1,gitignorePath:I?.gitignorePath??null}};export{M as initConfig};
1
+ import{readFile as C,writeFile as N,access as S}from"node:fs/promises";import c from"node:path";import{confirm as V,isCancel as G,select as J,text as U}from"@clack/prompts";import{c as _,b as r,s as L,w as T}from"../shared/docs-cache.BWEcxcrg.mjs";import{g as H,e as k}from"../shared/docs-cache.C96VehGu.mjs";import"zod";import"../shared/docs-cache.DDgb7yxQ.mjs";import"node:process";import"cac";import"picocolors";const h=async n=>{try{return await S(n),!0}catch{return!1}},M=async(n,s={})=>{const y=s.confirm??V,l=s.isCancel??G,x=s.select??J,F=s.text??U,f=n.cwd??process.cwd(),d=c.resolve(f,_),i=c.resolve(f,"package.json"),g=[];if(await h(d)&&g.push(d),await h(i)){const e=await C(i,"utf8");JSON.parse(e)["docs-cache"]&&g.push(i)}if(g.length>0)throw new Error(`Config already exists at ${g.join(", ")}. Init aborted.`);let b=!1;if(await h(i)){const e=await C(i,"utf8");if(!JSON.parse(e)["docs-cache"]){const o=await x({message:"Config location",options:[{value:"config",label:"docs.config.json"},{value:"package",label:"package.json"}],initialValue:"config"});if(l(o))throw new Error("Init cancelled.");b=o==="package"}}const $=b?i:d,v=n.cacheDirOverride??r,u=await F({message:"Cache directory",initialValue:v});if(l(u))throw new Error("Init cancelled.");const A=u||r,D=await y({message:"Generate TOC.md (table of contents with links to all documentation)",initialValue:!0});if(l(D))throw new Error("Init cancelled.");const I=await H(f,A);let P=!1;if(I.entry&&!I.hasEntry){const e=await y({message:"Add cache directory to .gitignore",initialValue:!0});if(l(e))throw new Error("Init cancelled.");P=e}const a={configPath:$,cacheDir:u,toc:D,gitignore:P},t=c.resolve(f,a.configPath);if(c.basename(t)==="package.json"){const e=await C(t,"utf8"),o=JSON.parse(e);if(o["docs-cache"])throw new Error(`docs-cache config already exists in ${t}.`);const p={$schema:"https://raw.githubusercontent.com/fbosch/docs-cache/main/docs.config.schema.json",sources:[]},E=a.cacheDir||r;E!==r&&(p.cacheDir=E),a.toc||(p.defaults={toc:!1}),o["docs-cache"]=L(p),await N(t,`${JSON.stringify(o,null,2)}
2
+ `,"utf8");const O=a.gitignore?await k(c.dirname(t),E):null;return{configPath:t,created:!0,gitignoreUpdated:O?.updated??!1,gitignorePath:O?.gitignorePath??null}}if(await h(t))throw new Error(`Config already exists at ${t}.`);const w={$schema:"https://raw.githubusercontent.com/fbosch/docs-cache/main/docs.config.schema.json",sources:[]},m=a.cacheDir||r;m!==r&&(w.cacheDir=m),a.toc||(w.defaults={toc:!1}),await T(t,w);const j=a.gitignore?await k(c.dirname(t),m):null;return{configPath:t,created:!0,gitignoreUpdated:j?.updated??!1,gitignorePath:j?.gitignorePath??null}};export{M as initConfig};
3
3
  //# sourceMappingURL=init.mjs.map
@@ -1,2 +1,2 @@
1
- import{readdir as p,rm as f,access as h}from"node:fs/promises";import u from"node:path";import{l as d,b as D}from"../shared/docs-cache.D4Fth4X8.mjs";import{a as v}from"../shared/docs-cache.D9_kM5zq.mjs";import"zod";import"node:process";import"cac";import"picocolors";const w=async t=>{try{return await h(t),!0}catch{return!1}},l=async t=>{const{config:c,resolvedPath:s,sources:a}=await d(t.configPath),e=v(s,c.cacheDir??D,t.cacheDirOverride);if(!await w(e))return{cacheDir:e,removed:[],kept:a.map(r=>r.id)};const n=new Set(a.map(r=>r.id)),m=await p(e,{withFileTypes:!0}),o=[];for(const r of m){if(!r.isDirectory())continue;const i=r.name;n.has(i)||i.startsWith(".tmp-")||(await f(u.join(e,i),{recursive:!0,force:!0}),o.push(i))}return{cacheDir:e,removed:o,kept:a.map(r=>r.id)}};export{l as pruneCache};
1
+ import{readdir as p,rm as f,access as h}from"node:fs/promises";import u from"node:path";import{l as d,b as D}from"../shared/docs-cache.BWEcxcrg.mjs";import{b as v}from"../shared/docs-cache.DDgb7yxQ.mjs";import"zod";import"node:process";import"cac";import"picocolors";const w=async t=>{try{return await h(t),!0}catch{return!1}},l=async t=>{const{config:c,resolvedPath:s,sources:a}=await d(t.configPath),e=v(s,c.cacheDir??D,t.cacheDirOverride);if(!await w(e))return{cacheDir:e,removed:[],kept:a.map(r=>r.id)};const n=new Set(a.map(r=>r.id)),m=await p(e,{withFileTypes:!0}),o=[];for(const r of m){if(!r.isDirectory())continue;const i=r.name;n.has(i)||i.startsWith(".tmp-")||(await f(u.join(e,i),{recursive:!0,force:!0}),o.push(i))}return{cacheDir:e,removed:o,kept:a.map(r=>r.id)}};export{l as pruneCache};
2
2
  //# sourceMappingURL=prune.mjs.map
@@ -1,3 +1,3 @@
1
- import{readFile as w,writeFile as N,rm as $,access as k}from"node:fs/promises";import v from"node:path";import{v as D,D as E,s as I,w as x,r as P}from"../shared/docs-cache.D4Fth4X8.mjs";import{r as F}from"../shared/docs-cache.D9_kM5zq.mjs";import{r as O}from"../shared/docs-cache.BSvQNKuf.mjs";import"zod";import"node:process";import"cac";import"picocolors";const d=async s=>{try{return await k(s),!0}catch{return!1}},y="package.json",S=async s=>{const a=await w(s,"utf8"),o=JSON.parse(a),t=o["docs-cache"];return t?{parsed:o,config:D(t)}:{parsed:o,config:null}},U=async s=>{if(s){const t=P(s);return{resolvedPath:t,mode:v.basename(t)===y?"package":"config"}}const a=P();if(await d(a))return{resolvedPath:a,mode:"config"};const o=v.resolve(process.cwd(),y);return await d(o)&&(await S(o)).config?{resolvedPath:o,mode:"package"}:{resolvedPath:a,mode:"config"}},b=async s=>{if(s.ids.length===0)throw new Error("No sources specified to remove.");const a=await U(s.configPath),o=a.resolvedPath;let t=E,r=null,f=null;if(await d(o))if(a.mode==="package"){const e=await S(o);if(f=e.parsed,r=e.config,!r)throw new Error(`Missing docs-cache config in ${o}.`);t=r}else{const e=await w(o,"utf8");r=JSON.parse(e.toString()),t=D(r)}else throw new Error(`Config not found at ${o}.`);const u=new Map(t.sources.map(e=>[e.id,e])),g=new Map(t.sources.map(e=>[e.repo,e])),c=new Set,l=[];for(const e of s.ids){if(u.has(e)){c.add(e);continue}const i=O(e);if(i.repoUrl&&g.has(i.repoUrl)){const p=g.get(i.repoUrl);p&&c.add(p.id);continue}if(i.inferredId&&u.has(i.inferredId)){c.add(i.inferredId);continue}l.push(e)}const C=t.sources.filter(e=>!c.has(e.id)),h=t.sources.filter(e=>c.has(e.id)).map(e=>e.id),M=t.sources.filter(e=>c.has(e.id));if(h.length===0)throw new Error("No matching sources found to remove.");const n={$schema:r?.$schema??"https://raw.githubusercontent.com/fbosch/docs-cache/main/docs.config.schema.json",sources:C};if(r?.cacheDir&&(n.cacheDir=r.cacheDir),r?.index!==void 0&&(n.index=r.index),r?.defaults&&(n.defaults=r.defaults),r?.targetMode&&(n.targetMode=r.targetMode),a.mode==="package"){const e=f??{};e["docs-cache"]=I(n),await N(o,`${JSON.stringify(e,null,2)}
2
- `,"utf8")}else await x(o,n);const m=[];for(const e of M){if(!e.targetDir)continue;const i=F(o,e.targetDir);await $(i,{recursive:!0,force:!0}),m.push({id:e.id,targetDir:i})}return{configPath:o,removed:h,missing:l,targetsRemoved:m}};export{b as removeSources};
1
+ import{readFile as w,writeFile as N,rm as $,access as k}from"node:fs/promises";import v from"node:path";import{v as D,D as E,s as F,w as I,r as P}from"../shared/docs-cache.BWEcxcrg.mjs";import{r as O}from"../shared/docs-cache.DDgb7yxQ.mjs";import{r as U}from"../shared/docs-cache.BSvQNKuf.mjs";import"zod";import"node:process";import"cac";import"picocolors";const f=async a=>{try{return await k(a),!0}catch{return!1}},y="package.json",S=async a=>{const s=await w(a,"utf8"),o=JSON.parse(s),r=o["docs-cache"];return r?{parsed:o,config:D(r)}:{parsed:o,config:null}},b=async a=>{if(a){const r=P(a);return{resolvedPath:r,mode:v.basename(r)===y?"package":"config"}}const s=P();if(await f(s))return{resolvedPath:s,mode:"config"};const o=v.resolve(process.cwd(),y);return await f(o)&&(await S(o)).config?{resolvedPath:o,mode:"package"}:{resolvedPath:s,mode:"config"}},J=async a=>{if(a.ids.length===0)throw new Error("No sources specified to remove.");const s=await b(a.configPath),o=s.resolvedPath;let r=E,t=null,d=null;if(await f(o))if(s.mode==="package"){const e=await S(o);if(d=e.parsed,t=e.config,!t)throw new Error(`Missing docs-cache config in ${o}.`);r=t}else{const e=await w(o,"utf8");t=JSON.parse(e.toString()),r=D(t)}else throw new Error(`Config not found at ${o}.`);const u=new Map(r.sources.map(e=>[e.id,e])),g=new Map(r.sources.map(e=>[e.repo,e])),n=new Set,l=[];for(const e of a.ids){if(u.has(e)){n.add(e);continue}const i=U(e);if(i.repoUrl&&g.has(i.repoUrl)){const p=g.get(i.repoUrl);p&&n.add(p.id);continue}if(i.inferredId&&u.has(i.inferredId)){n.add(i.inferredId);continue}l.push(e)}const C=r.sources.filter(e=>!n.has(e.id)),h=r.sources.filter(e=>n.has(e.id)).map(e=>e.id),M=r.sources.filter(e=>n.has(e.id));if(h.length===0)throw new Error("No matching sources found to remove.");const c={$schema:t?.$schema??"https://raw.githubusercontent.com/fbosch/docs-cache/main/docs.config.schema.json",sources:C};if(t?.cacheDir&&(c.cacheDir=t.cacheDir),t?.defaults&&(c.defaults=t.defaults),t?.targetMode&&(c.targetMode=t.targetMode),s.mode==="package"){const e=d??{};e["docs-cache"]=F(c),await N(o,`${JSON.stringify(e,null,2)}
2
+ `,"utf8")}else await I(o,c);const m=[];for(const e of M){if(!e.targetDir)continue;const i=O(o,e.targetDir);await $(i,{recursive:!0,force:!0}),m.push({id:e.id,targetDir:i})}return{configPath:o,removed:h,missing:l,targetsRemoved:m}};export{J as removeSources};
3
3
  //# sourceMappingURL=remove.mjs.map
@@ -1,2 +1,2 @@
1
- import{access as E}from"node:fs/promises";import a from"picocolors";import{u as o,s as u,a as w,g as D}from"../shared/docs-cache.D9_kM5zq.mjs";import{l as v,b as x}from"../shared/docs-cache.D4Fth4X8.mjs";import{resolveLockPath as C,readLock as P}from"../lock.mjs";import"node:process";import"cac";import"node:path";import"zod";const h=async s=>{try{return await E(s),!0}catch{return!1}},$=async s=>{const{config:e,resolvedPath:t,sources:n}=await v(s.configPath),r=w(t,e.cacheDir??x,s.cacheDirOverride),l=await h(r),c=C(t),i=await h(c);let d=!1,f=null;if(i)try{f=await P(c),d=!0}catch{d=!1}const g=await Promise.all(n.map(async m=>{const p=D(r,m.id),k=await h(p.sourceDir),y=f?.sources?.[m.id]??null;return{id:m.id,docsPath:p.sourceDir,docsExists:k,lockEntry:y}}));return{configPath:t,cacheDir:r,cacheDirExists:l,lockPath:c,lockExists:i,lockValid:d,sources:g}},L=s=>{const e=o.path(s.cacheDir),t=s.cacheDirExists?a.green("present"):a.red("missing"),n=s.lockExists?s.lockValid?a.green("valid"):a.red("invalid"):a.yellow("missing");if(o.header("Cache",`${e} (${t})`),o.header("Lock",`docs.lock (${n})`),s.sources.length===0){o.line(),o.line(`${u.warn} No sources configured.`);return}o.line();for(const r of s.sources){const l=r.docsExists?u.success:u.error,c=r.lockEntry?a.green("locked"):a.yellow("new"),i=o.hash(r.lockEntry?.resolvedCommit);o.item(l,r.id.padEnd(20),`${c.padEnd(10)} ${i}`)}};export{$ as getStatus,L as printStatus};
1
+ import{access as E}from"node:fs/promises";import a from"picocolors";import{u as o,a as u,b as w,g as D}from"../shared/docs-cache.DDgb7yxQ.mjs";import{l as v,b as x}from"../shared/docs-cache.BWEcxcrg.mjs";import{resolveLockPath as C,readLock as P}from"../lock.mjs";import"node:process";import"cac";import"node:path";import"zod";const h=async r=>{try{return await E(r),!0}catch{return!1}},L=async r=>{const{config:i,resolvedPath:t,sources:n}=await v(r.configPath),s=w(t,i.cacheDir??x,r.cacheDirOverride),l=await h(s),c=C(t),e=await h(c);let d=!1,f=null;if(e)try{f=await P(c),d=!0}catch{d=!1}const g=await Promise.all(n.map(async m=>{const p=D(s,m.id),k=await h(p.sourceDir),y=f?.sources?.[m.id]??null;return{id:m.id,docsPath:p.sourceDir,docsExists:k,lockEntry:y}}));return{configPath:t,cacheDir:s,cacheDirExists:l,lockPath:c,lockExists:e,lockValid:d,sources:g}},b=r=>{const i=o.path(r.cacheDir),t=r.cacheDirExists?a.green("present"):a.red("missing"),n=r.lockExists?r.lockValid?a.green("valid"):a.red("invalid"):a.yellow("missing");if(o.header("Cache",`${i} (${t})`),o.header("Lock",`docs.lock (${n})`),r.sources.length===0){o.line(),o.line(`${u.warn} No sources configured.`);return}o.line();for(const s of r.sources){const l=s.docsExists?u.success:u.error,c=s.lockEntry?a.green("locked"):a.yellow("new"),e=o.hash(s.lockEntry?.resolvedCommit);o.item(l,s.id.padEnd(20),`${c.padEnd(10)} ${e}`)}};export{L as getStatus,b as printStatus};
2
2
  //# sourceMappingURL=status.mjs.map
@@ -1,8 +1,9 @@
1
- import{createHash as L,randomBytes as me}from"node:crypto";import{rm as $,mkdtemp as N,writeFile as X,mkdir as R,access as J,rename as j,open as B,lstat as he,symlink as we,cp as pe,readFile as H}from"node:fs/promises";import w from"node:path";import M from"picocolors";import{t as _,r as U,D as ge,g as ye,u as v,s as O,a as Se}from"../shared/docs-cache.D9_kM5zq.mjs";import{a as z,l as De,D as ve,b as xe}from"../shared/docs-cache.D4Fth4X8.mjs";import{execFile as K}from"node:child_process";import Ce,{tmpdir as q}from"node:os";import{promisify as V}from"node:util";import{writeLock as Pe,resolveLockPath as Ee,readLock as Me}from"../lock.mjs";import{M as G,v as Z}from"./verify.mjs";import{createWriteStream as Q,createReadStream as Oe,constants as ee}from"node:fs";import{pipeline as $e}from"node:stream/promises";import te from"fast-glob";const Te=/^(https?:\/\/)([^@]+)@/i,I=e=>e.replace(Te,"$1***@"),ke=V(K),Fe=3e4,Ie=new Set(["file:","ftp:","data:","javascript:"]),be=e=>{try{const r=new URL(e);if(Ie.has(r.protocol))throw new Error(`Blocked protocol '${r.protocol}' in repo URL '${I(e)}'.`)}catch(r){if(r instanceof TypeError)return;throw r}},Re=e=>{if(be(e),e.startsWith("git@")){const r=e.indexOf("@"),t=e.indexOf(":",r+1);return t===-1?null:e.slice(r+1,t)||null}try{const r=new URL(e);return r.protocol!=="https:"&&r.protocol!=="ssh:"?null:r.hostname||null}catch{return null}},re=(e,r)=>{const t=Re(e);if(!t)throw new Error(`Unsupported repo URL '${I(e)}'. Use HTTPS or SSH.`);const i=t.toLowerCase();if(!r.map(o=>o.toLowerCase()).includes(i))throw new Error(`Host '${t}' is not in allowHosts for '${I(e)}'.`)},oe=e=>{const r=e.trim().split(`
2
- `).filter(Boolean);return r.length===0?null:r[0].split(/\s+/)[0]||null},_e=async e=>{re(e.repo,e.allowHosts);const{stdout:r}=await ke("git",["ls-remote",e.repo,e.ref],{timeout:e.timeoutMs??Fe,maxBuffer:1024*1024}),t=oe(r);if(!t)throw new Error(`Unable to resolve ref '${e.ref}' for ${I(e.repo)}.`);return{repo:e.repo,ref:e.ref,resolvedCommit:t}},se=V(K),ie=3e4,A=async(e,r)=>{await se("git",["-c","core.hooksPath=/dev/null","-c","submodule.recurse=false","-c","protocol.file.allow=never","-c","protocol.ext.allow=never",...e],{cwd:r?.cwd,timeout:r?.timeoutMs??ie,maxBuffer:1024*1024,env:{PATH:process.env.PATH,HOME:process.env.HOME,USER:process.env.USER,USERPROFILE:process.env.USERPROFILE,TMPDIR:process.env.TMPDIR,TMP:process.env.TMP,TEMP:process.env.TEMP,SYSTEMROOT:process.env.SYSTEMROOT,WINDIR:process.env.WINDIR,SSH_AUTH_SOCK:process.env.SSH_AUTH_SOCK,SSH_AGENT_PID:process.env.SSH_AGENT_PID,HTTP_PROXY:process.env.HTTP_PROXY,HTTPS_PROXY:process.env.HTTPS_PROXY,NO_PROXY:process.env.NO_PROXY,GIT_TERMINAL_PROMPT:"0",GIT_CONFIG_NOSYSTEM:"1",GIT_CONFIG_NOGLOBAL:"1",...process.platform==="win32"?{}:{GIT_ASKPASS:"/bin/false"}}})},Ae=async(e,r,t,i)=>{const o=w.join(t,"archive.tar");await A(["archive","--remote",e,"--format=tar","--output",o,r],{timeoutMs:i}),await se("tar",["-xf",o,"-C",t],{timeout:i??ie,maxBuffer:1024*1024}),await $(o,{force:!0})},Le=e=>{if(!e||e.length===0)return!1;for(const r of e)if(!r||r.includes("**"))return!1;return!0},Ne=e=>{if(!e)return[];const r=e.map(t=>{const i=t.replace(/\\/g,"/"),o=i.indexOf("*");return(o===-1?i:i.slice(0,o)).replace(/\/+$|\/$/,"")});return Array.from(new Set(r.filter(t=>t.length>0)))},je=async(e,r)=>{const t=/^[0-9a-f]{7,40}$/i.test(e.ref),i=Le(e.include),o=["clone","--no-checkout","--filter=blob:none","--depth",String(e.depth),"--recurse-submodules=no","--no-tags"];if(i&&o.push("--sparse"),t||(o.push("--single-branch"),e.ref!=="HEAD"&&o.push("--branch",e.ref)),o.push(e.repo,r),await A(o,{timeoutMs:e.timeoutMs}),i){const s=Ne(e.include);s.length>0&&await A(["-C",r,"sparse-checkout","set",...s],{timeoutMs:e.timeoutMs})}await A(["-C",r,"checkout","--detach",e.resolvedCommit],{timeoutMs:e.timeoutMs})},Be=async e=>{const r=await N(w.join(q(),`docs-cache-${e.sourceId}-`));try{return await Ae(e.repo,e.resolvedCommit,r,e.timeoutMs),r}catch(t){throw await $(r,{recursive:!0,force:!0}),t}},He=async e=>{z(e.sourceId,"sourceId");try{const r=await Be(e);return{repoDir:r,cleanup:async()=>{await $(r,{recursive:!0,force:!0})}}}catch{const r=await N(w.join(q(),`docs-cache-${e.sourceId}-`));try{return await je(e,r),{repoDir:r,cleanup:async()=>{await $(r,{recursive:!0,force:!0})}}}catch(t){throw await $(r,{recursive:!0,force:!0}),t}}},Ue=async e=>{const r=new Map(e.sources.map(u=>[u.id,u])),t={};for(const[u,m]of Object.entries(e.lock.sources)){const l=r.get(u),h=l?.targetDir?_(U(e.configPath,l.targetDir)):void 0;t[u]={repo:m.repo,ref:m.ref,resolvedCommit:m.resolvedCommit,bytes:m.bytes,fileCount:m.fileCount,manifestSha256:m.manifestSha256,updatedAt:m.updatedAt,cachePath:_(w.join(e.cacheDir,u)),...h?{targetDir:h}:{}}}const i={generatedAt:new Date().toISOString(),cacheDir:_(e.cacheDir),sources:t},o=w.join(e.cacheDir,ge),s=`${JSON.stringify(i,null,2)}
3
- `;await X(o,s,"utf8")},F=e=>_(e),Y=Number(process.env.DOCS_CACHE_STREAM_THRESHOLD_MB??"2"),ze=Number.isFinite(Y)&&Y>0?Math.floor(Y*1024*1024):1024*1024,Ge=(e,r)=>{const t=w.resolve(e);if(!w.resolve(r).startsWith(t+w.sep))throw new Error(`Path traversal detected: ${r}`)},ae=async e=>{try{return await B(e,ee.O_RDONLY|ee.O_NOFOLLOW)}catch(r){const t=r.code;if(t==="ELOOP")return null;if(t==="EINVAL"||t==="ENOSYS"||t==="ENOTSUP")return(await he(e)).isSymbolicLink()?null:await B(e,"r");throw r}},Ye=async(e,r=5e3)=>{const t=Date.now();for(;Date.now()-t<r;)try{const i=await B(e,"wx");return{release:async()=>{await i.close(),await $(e,{force:!0})}}}catch(i){if(i.code!=="EEXIST")throw i;await new Promise(o=>setTimeout(o,100))}throw new Error(`Failed to acquire lock for ${e}.`)},We=async e=>{z(e.sourceId,"sourceId");const r=ye(e.cacheDir,e.sourceId);await R(e.cacheDir,{recursive:!0});const t=await N(w.join(e.cacheDir,`.tmp-${e.sourceId}-`));let i=null;const o=async()=>{const s=i;!s||s.closed||s.destroyed||await new Promise(u=>{const m=()=>{s.off("close",l),s.off("error",h),u()},l=()=>m(),h=()=>m();s.once("close",l),s.once("error",h);try{s.end()}catch{m()}})};try{const s=await te(e.include,{cwd:e.repoDir,ignore:[".git/**",...e.exclude??[]],dot:!0,onlyFiles:!0,followSymbolicLinks:!1});s.sort((f,c)=>F(f).localeCompare(F(c)));const u=new Set;for(const f of s)u.add(w.dirname(f));await Promise.all(Array.from(u,f=>R(w.join(t,f),{recursive:!0})));let m=0,l=0;const h=Math.max(1,Math.min(s.length,Math.max(8,Math.min(128,Ce.cpus().length*8)))),C=w.join(t,G),g=Q(C,{encoding:"utf8"});i=g;const x=L("sha256"),y=async f=>new Promise((c,p)=>{const d=P=>{g.off("drain",a),p(P)},a=()=>{g.off("error",d),c()};g.once("error",d),g.write(f)?(g.off("error",d),c()):g.once("drain",a)});for(let f=0;f<s.length;f+=h){const c=s.slice(f,f+h),p=await Promise.all(c.map(async d=>{const a=F(d),P=w.join(e.repoDir,d),D=await ae(P);if(!D)return null;try{const k=await D.stat();if(!k.isFile())return null;const T=w.join(t,d);if(Ge(t,T),k.size>=ze){const E=Oe(P,{fd:D.fd,autoClose:!1}),de=Q(T);await $e(E,de)}else{const E=await D.readFile();await X(T,E)}return{path:a,size:k.size}}finally{await D.close()}}));for(const d of p){if(!d)continue;if(e.maxFiles!==void 0&&l+1>e.maxFiles)throw new Error(`Materialized content exceeds maxFiles (${e.maxFiles}).`);if(m+=d.size,m>e.maxBytes)throw new Error(`Materialized content exceeds maxBytes (${e.maxBytes}).`);const a=`${JSON.stringify(d)}
4
- `;x.update(a),await y(a),l+=1}}await new Promise((f,c)=>{g.end(()=>f()),g.once("error",c)});const n=x.digest("hex"),S=async f=>{try{return await J(f),!0}catch{return!1}};return await(async(f,c)=>{const p=await Ye(`${c}.lock`);try{const d=await S(c),a=`${c}.bak-${me(8).toString("hex")}`;d&&await j(c,a);try{await j(f,c)}catch(P){if(d)try{await j(a,c)}catch(D){const k=D instanceof Error?D.message:String(D);process.stderr.write(`Warning: Failed to restore backup: ${k}
5
- `)}throw P}d&&await $(a,{recursive:!0,force:!0})}finally{await p.release()}})(t,r.sourceDir),{bytes:m,fileCount:l,manifestSha256:n}}catch(s){try{await o()}catch{}throw await $(t,{recursive:!0,force:!0}),s}},Xe=async e=>{z(e.sourceId,"sourceId");const r=await te(e.include,{cwd:e.repoDir,ignore:[".git/**",...e.exclude??[]],dot:!0,onlyFiles:!0,followSymbolicLinks:!1});r.sort((s,u)=>F(s).localeCompare(F(u)));let t=0,i=0;const o=L("sha256");for(const s of r){const u=F(s),m=w.join(e.repoDir,s),l=await ae(m);if(l)try{const h=await l.stat();if(!h.isFile())continue;if(e.maxFiles!==void 0&&i+1>e.maxFiles)throw new Error(`Materialized content exceeds maxFiles (${e.maxFiles}).`);if(t+=h.size,t>e.maxBytes)throw new Error(`Materialized content exceeds maxBytes (${e.maxBytes}).`);const C=`${JSON.stringify({path:u,size:h.size})}
6
- `;o.update(C),i+=1}finally{await l.close()}}return{bytes:t,fileCount:i,manifestSha256:o.digest("hex")}},Je=async(e,r)=>{await r.rm(e,{recursive:!0,force:!0})},W=async e=>{const r=e.deps??{cp:pe,mkdir:R,rm:$,symlink:we,stderr:process.stderr},t=w.dirname(e.targetDir);await r.mkdir(t,{recursive:!0}),await Je(e.targetDir,r);const i=process.platform==="win32"?"copy":"symlink";if((e.mode??i)==="copy"){await r.cp(e.sourceDir,e.targetDir,{recursive:!0});return}const o=process.platform==="win32"?"junction":"dir";try{await r.symlink(e.sourceDir,e.targetDir,o)}catch(s){const u=s.code;if(u&&new Set(["EPERM","EACCES","ENOTSUP","EINVAL"]).has(u)){if(e.explicitTargetMode){const m=s instanceof Error?s.message:String(s);r.stderr.write(`Warning: Failed to create symlink at ${e.targetDir}. Falling back to copy. ${m}
7
- `)}await r.cp(e.sourceDir,e.targetDir,{recursive:!0});return}throw s}},Ke=e=>{if(e<1024)return`${e} B`;const r=["KB","MB","GB","TB"];let t=e,i=-1;for(;t>=1024&&i<r.length-1;)t/=1024,i+=1;return`${t.toFixed(1)} ${r[i]}`},b=async e=>{try{return await J(e),!0}catch{return!1}},ne=async(e,r)=>{const t=w.join(e,r);return await b(t)?await b(w.join(t,G)):!1},ce=e=>{if(!e||e.length===0)return[];const r=e.map(t=>t.trim()).filter(t=>t.length>0);return Array.from(new Set(r)).sort()},qe=e=>{const r={include:ce(e.include),exclude:ce(e.exclude)},t=L("sha256");return t.update(JSON.stringify(r)),t.digest("hex")},le=async(e,r={})=>{const{config:t,resolvedPath:i,sources:o}=await De(e.configPath),s=t.defaults??ve.defaults,u=Se(i,t.cacheDir??xe,e.cacheDirOverride),m=Ee(i),l=await b(m);let h=null;l&&(h=await Me(m));const C=r.resolveRemoteCommit??_e,g=e.sourceFilter?.length?o.filter(y=>e.sourceFilter?.includes(y.id)):o,x=await Promise.all(g.map(async y=>{const n=h?.sources?.[y.id],S=y.include??s.include,f=y.exclude,c=qe({include:S,exclude:f});if(e.offline){const P=await ne(u,y.id);return{id:y.id,repo:n?.repo??y.repo,ref:n?.ref??y.ref??s.ref,resolvedCommit:n?.resolvedCommit??"offline",lockCommit:n?.resolvedCommit??null,lockRulesSha256:n?.rulesSha256,status:n&&P?"up-to-date":"missing",bytes:n?.bytes,fileCount:n?.fileCount,manifestSha256:n?.manifestSha256,rulesSha256:c}}const p=await C({repo:y.repo,ref:y.ref,allowHosts:s.allowHosts,timeoutMs:e.timeoutMs}),d=n?.resolvedCommit===p.resolvedCommit&&n?.rulesSha256===c,a=n?d?"up-to-date":"changed":"missing";return{id:y.id,repo:p.repo,ref:p.ref,resolvedCommit:p.resolvedCommit,lockCommit:n?.resolvedCommit??null,lockRulesSha256:n?.rulesSha256,status:a,bytes:n?.bytes,fileCount:n?.fileCount,manifestSha256:n?.manifestSha256,rulesSha256:c}}));return{config:t,configPath:i,cacheDir:u,lockPath:m,lockExists:l,lockData:h,results:x,sources:g,defaults:s}},Ve=async()=>{const e=w.resolve(process.cwd(),"package.json");try{const r=await H(e,"utf8"),t=JSON.parse(r.toString());return typeof t.version=="string"?t.version:"0.0.0"}catch{}try{const r=await H(new URL("../package.json",import.meta.url),"utf8"),t=JSON.parse(r.toString());return typeof t.version=="string"?t.version:"0.0.0"}catch{}try{const r=await H(new URL("../../package.json",import.meta.url),"utf8"),t=JSON.parse(r.toString());return typeof t.version=="string"?t.version:"0.0.0"}catch{return"0.0.0"}},Ze=async(e,r)=>{const t=await Ve(),i=new Date().toISOString(),o={...r?.sources??{}};for(const s of e.results){const u=o[s.id];o[s.id]={repo:s.repo,ref:s.ref,resolvedCommit:s.resolvedCommit,bytes:s.bytes??u?.bytes??0,fileCount:s.fileCount??u?.fileCount??0,manifestSha256:s.manifestSha256??u?.manifestSha256??s.resolvedCommit,rulesSha256:s.rulesSha256??u?.rulesSha256,updatedAt:i}}return{version:1,generatedAt:i,toolVersion:t,sources:o}},ue=async(e,r={})=>{const t=process.hrtime.bigint();let i=0;const o=await le(e,r);await R(o.cacheDir,{recursive:!0});const s=o.lockData,u=o.results.filter(l=>{const h=o.sources.find(C=>C.id===l.id);return l.status==="missing"&&(h?.required??!0)});if(e.failOnMiss&&u.length>0)throw new Error(`Missing required source(s): ${u.map(l=>l.id).join(", ")}.`);if(!e.lockOnly){const l=o.defaults,h=r.fetchSource??He,C=r.materializeSource??We,g=async(n,S)=>{const f=n?.length?o.results.filter(c=>n.includes(c.id)):o.results;return(await Promise.all(f.map(async c=>{const p=o.sources.find(a=>a.id===c.id);if(!p)return null;const d=await ne(o.cacheDir,c.id);return S||c.status!=="up-to-date"||!d?{result:c,source:p}:null}))).filter(Boolean)},x=async()=>{await Promise.all(o.sources.map(async n=>{if(!n.targetDir)return;const S=U(o.configPath,n.targetDir);await b(S)||await W({sourceDir:w.join(o.cacheDir,n.id),targetDir:S,mode:n.targetMode??l.targetMode,explicitTargetMode:n.targetMode!==void 0})}))},y=async n=>{const S=e.concurrency??4;let f=0;const c=async()=>{const p=n[f];if(!p||!p.source)return;f+=1;const{result:d,source:a}=p,P=o.lockData?.sources?.[a.id];e.json||v.step("Fetching",a.id);const D=await h({sourceId:a.id,repo:a.repo,ref:a.ref,resolvedCommit:d.resolvedCommit,cacheDir:o.cacheDir,depth:a.depth??l.depth,include:a.include??l.include,timeoutMs:e.timeoutMs});try{const k=w.join(o.cacheDir,a.id,G);if(d.status!=="up-to-date"&&P?.manifestSha256&&await b(k)){const E=await Xe({sourceId:a.id,repoDir:D.repoDir,cacheDir:o.cacheDir,include:a.include??l.include,exclude:a.exclude,maxBytes:a.maxBytes??l.maxBytes,maxFiles:a.maxFiles??l.maxFiles});if(E.manifestSha256===P.manifestSha256){d.bytes=E.bytes,d.fileCount=E.fileCount,d.manifestSha256=E.manifestSha256,d.status="up-to-date",e.json||v.item(O.success,a.id,"no content changes"),await c();return}}const T=await C({sourceId:a.id,repoDir:D.repoDir,cacheDir:o.cacheDir,include:a.include??l.include,exclude:a.exclude,maxBytes:a.maxBytes??l.maxBytes,maxFiles:a.maxFiles??l.maxFiles});if(a.targetDir){const E=U(o.configPath,a.targetDir);await W({sourceDir:w.join(o.cacheDir,a.id),targetDir:E,mode:a.targetMode??l.targetMode,explicitTargetMode:a.targetMode!==void 0})}d.bytes=T.bytes,d.fileCount=T.fileCount,d.manifestSha256=T.manifestSha256,e.json||v.item(O.success,a.id,`synced ${T.fileCount} files`)}finally{await D.cleanup()}await c()};await Promise.all(Array.from({length:Math.min(S,n.length)},c))};if(e.offline)await x();else{const n=await g();await y(n),await x()}if(!e.offline){const n=(await Z({configPath:o.configPath,cacheDirOverride:o.cacheDir})).results.filter(S=>!S.ok);if(n.length>0){const S=await g(n.map(c=>c.id),!0);S.length>0&&(await y(S),await x());const f=(await Z({configPath:o.configPath,cacheDirOverride:o.cacheDir})).results.filter(c=>!c.ok);if(f.length>0&&(i+=1,!e.json)){const c=f.map(p=>`${p.id} (${p.issues.join("; ")})`).join(", ");v.line(`${O.warn} Verify failed for ${f.length} source(s): ${c}`)}}}}const m=await Ze(o,s);if(await Pe(o.lockPath,m),!e.json){const l=Number(process.hrtime.bigint()-t)/1e6,h=o.results.reduce((g,x)=>g+(x.bytes??0),0),C=o.results.reduce((g,x)=>g+(x.fileCount??0),0);v.line(`${O.info} Completed in ${l.toFixed(0)}ms \xB7 ${Ke(h)} \xB7 ${C} files${i?` \xB7 ${i} warning${i===1?"":"s"}`:""}`)}return o.config.index&&await Ue({cacheDir:o.cacheDir,configPath:o.configPath,lock:m,sources:o.sources}),o.lockExists=!0,o},fe=e=>{const r={upToDate:e.results.filter(t=>t.status==="up-to-date").length,changed:e.results.filter(t=>t.status==="changed").length,missing:e.results.filter(t=>t.status==="missing").length};if(e.results.length===0){v.line(`${O.info} No sources to sync.`);return}v.line(`${O.info} ${e.results.length} sources (${r.upToDate} up-to-date, ${r.changed} changed, ${r.missing} missing)`);for(const t of e.results){const i=v.hash(t.resolvedCommit),o=v.hash(t.lockCommit),s=!!t.lockRulesSha256&&!!t.rulesSha256&&t.lockRulesSha256!==t.rulesSha256;if(t.status==="up-to-date"){v.item(O.success,t.id,`${M.dim("up-to-date")} ${M.gray(i)}`);continue}if(t.status==="changed"){if(t.lockCommit===t.resolvedCommit&&s){v.item(O.warn,t.id,`${M.dim("rules changed")} ${M.gray(i)}`);continue}v.item(O.warn,t.id,`${M.dim("changed")} ${M.gray(o)} ${M.dim("->")} ${M.gray(i)}`);continue}v.item(O.warn,t.id,`${M.dim("missing")} ${M.gray(i)}`)}},Qe={__proto__:null,getSyncPlan:le,printSyncPlan:fe,runSync:ue};export{W as a,fe as b,ue as c,re as e,oe as p,I as r,Qe as s};
1
+ import{createHash as _,randomBytes as ye}from"node:crypto";import{rm as x,mkdtemp as N,mkdir as b,writeFile as q,access as A,rename as B,open as U,lstat as Se,symlink as ve,cp as De,readFile as L}from"node:fs/promises";import g from"node:path";import P from"picocolors";import{g as $e,t as z,r as G,D as Ce,u as C,a as E,b as xe}from"../shared/docs-cache.DDgb7yxQ.mjs";import{a as Y,l as Me,D as Pe,b as Ee}from"../shared/docs-cache.BWEcxcrg.mjs";import{execFile as K}from"node:child_process";import Oe,{tmpdir as V}from"node:os";import{pathToFileURL as Te}from"node:url";import{promisify as Q}from"node:util";import{e as ke,r as Z}from"../shared/docs-cache.kK1DPQIQ.mjs";import{writeLock as Fe,resolveLockPath as be,readLock as Ie}from"../lock.mjs";import{M as W,v as ee}from"./verify.mjs";import{createWriteStream as te,createReadStream as Re,constants as re}from"node:fs";import{pipeline as je}from"node:stream/promises";import oe from"fast-glob";const _e=/^(https?:\/\/)([^@]+)@/i,R=e=>e.replace(_e,"$1***@"),Ae=Q(K),Le=3e4,He=new Set(["file:","ftp:","data:","javascript:"]),Ne=e=>{try{const r=new URL(e);if(He.has(r.protocol))throw new Error(`Blocked protocol '${r.protocol}' in repo URL '${R(e)}'.`)}catch(r){if(r instanceof TypeError)return;throw r}},Be=e=>{if(Ne(e),e.startsWith("git@")){const r=e.indexOf("@"),t=e.indexOf(":",r+1);return t===-1?null:e.slice(r+1,t)||null}try{const r=new URL(e);return r.protocol!=="https:"&&r.protocol!=="ssh:"?null:r.hostname||null}catch{return null}},se=(e,r)=>{const t=Be(e);if(!t)throw new Error(`Unsupported repo URL '${R(e)}'. Use HTTPS or SSH.`);const s=t.toLowerCase();if(!r.map(o=>o.toLowerCase()).includes(s))throw new Error(`Host '${t}' is not in allowHosts for '${R(e)}'.`)},ie=e=>{const r=e.trim().split(`
2
+ `).filter(Boolean);return r.length===0?null:r[0].split(/\s+/)[0]||null},Ue=async e=>{se(e.repo,e.allowHosts);const{stdout:r}=await Ae("git",["ls-remote",e.repo,e.ref],{timeout:e.timeoutMs??Le,maxBuffer:1024*1024}),t=ie(r);if(!t)throw new Error(`Unable to resolve ref '${e.ref}' for ${R(e.repo)}.`);return{repo:e.repo,ref:e.ref,resolvedCommit:t}},ae=Q(K),ne=12e4,T=async(e,r)=>{const t=["-c","core.hooksPath=/dev/null","-c","submodule.recurse=false","-c","protocol.ext.allow=never"];r?.allowFileProtocol?t.push("-c","protocol.file.allow=always"):t.push("-c","protocol.file.allow=never"),await ae("git",[...t,...e],{cwd:r?.cwd,timeout:r?.timeoutMs??ne,maxBuffer:10*1024*1024,env:{PATH:process.env.PATH,HOME:process.env.HOME,USER:process.env.USER,USERPROFILE:process.env.USERPROFILE,TMPDIR:process.env.TMPDIR,TMP:process.env.TMP,TEMP:process.env.TEMP,SYSTEMROOT:process.env.SYSTEMROOT,WINDIR:process.env.WINDIR,SSH_AUTH_SOCK:process.env.SSH_AUTH_SOCK,SSH_AGENT_PID:process.env.SSH_AGENT_PID,HTTP_PROXY:process.env.HTTP_PROXY,HTTPS_PROXY:process.env.HTTPS_PROXY,NO_PROXY:process.env.NO_PROXY,GIT_TERMINAL_PROMPT:"0",GIT_CONFIG_NOSYSTEM:"1",GIT_CONFIG_NOGLOBAL:"1",...process.platform==="win32"?{}:{GIT_ASKPASS:"/bin/false"}}})},ze=e=>_("sha256").update(e).digest("hex").substring(0,16),Ge=e=>{const r=ze(e);return g.join(Z(),r)},Ye=async e=>{try{return await T(["rev-parse","--git-dir"],{cwd:e}),!0}catch{return!1}},We=async(e,r,t,s)=>{const o=g.join(t,"archive.tar");await T(["archive","--remote",e,"--format=tar","--output",o,r],{timeoutMs:s}),await ae("tar",["-xf",o,"-C",t],{timeout:s??ne,maxBuffer:1024*1024}),await x(o,{force:!0})},ce=e=>{if(!e||e.length===0)return!1;for(const r of e)if(!r||r.includes("**"))return!1;return!0},le=e=>{if(!e)return[];const r=e.map(t=>{const s=t.replace(/\\/g,"/"),o=s.indexOf("*");return(o===-1?s:s.slice(0,o)).replace(/\/+$|\/$/,"")});return Array.from(new Set(r.filter(t=>t.length>0)))},ue=async(e,r)=>{const t=/^[0-9a-f]{7,40}$/i.test(e.ref),s=ce(e.include),o=["clone","--no-checkout","--filter=blob:none","--depth",String(e.depth),"--recurse-submodules=no","--no-tags"];if(s&&o.push("--sparse"),t||(o.push("--single-branch"),e.ref!=="HEAD"&&o.push("--branch",e.ref)),o.push(e.repo,r),await T(o,{timeoutMs:e.timeoutMs}),s){const i=le(e.include);i.length>0&&await T(["-C",r,"sparse-checkout","set",...i],{timeoutMs:e.timeoutMs})}await T(["-C",r,"checkout","--quiet","--detach",e.resolvedCommit],{timeoutMs:e.timeoutMs})},Je=async(e,r)=>{const t=Ge(e.repo),s=await ke(t),o=/^[0-9a-f]{7,40}$/i.test(e.ref),i=ce(e.include),c=Z();if(await b(c,{recursive:!0}),s&&await Ye(t))try{const u=["fetch","origin"];if(o)u.push("--depth",String(e.depth));else{const D=e.ref==="HEAD"?"HEAD":`${e.ref}:refs/remotes/origin/${e.ref}`;u.push(D,"--depth",String(e.depth))}await T(["-C",t,...u],{timeoutMs:e.timeoutMs})}catch{await x(t,{recursive:!0,force:!0}),await ue(e,t)}else s&&await x(t,{recursive:!0,force:!0}),await ue(e,t);await b(r,{recursive:!0});const a=["clone","--no-checkout","--filter=blob:none","--depth",String(e.depth),"--recurse-submodules=no","--no-tags"];i&&a.push("--sparse"),o||(a.push("--single-branch"),e.ref!=="HEAD"&&a.push("--branch",e.ref));const n=Te(t).href;if(a.push(n,r),await T(a,{timeoutMs:e.timeoutMs,allowFileProtocol:!0}),i){const u=le(e.include);u.length>0&&await T(["-C",r,"sparse-checkout","set",...u],{timeoutMs:e.timeoutMs})}await T(["-C",r,"checkout","--quiet","--detach",e.resolvedCommit],{timeoutMs:e.timeoutMs})},Xe=async e=>{const r=await N(g.join(V(),`docs-cache-${e.sourceId}-`));try{return await We(e.repo,e.resolvedCommit,r,e.timeoutMs),r}catch(t){throw await x(r,{recursive:!0,force:!0}),t}},qe=async e=>{Y(e.sourceId,"sourceId");try{const r=await Xe(e);return{repoDir:r,cleanup:async()=>{await x(r,{recursive:!0,force:!0})}}}catch{const r=await N(g.join(V(),`docs-cache-${e.sourceId}-`));try{return await Je(e,r),{repoDir:r,cleanup:async()=>{await x(r,{recursive:!0,force:!0})}}}catch(t){throw await x(r,{recursive:!0,force:!0}),t}}},H=e=>z(e),J=Number(process.env.DOCS_CACHE_STREAM_THRESHOLD_MB??"2"),Ke=Number.isFinite(J)&&J>0?Math.floor(J*1024*1024):1024*1024,Ve=(e,r)=>{const t=g.resolve(e);if(!g.resolve(r).startsWith(t+g.sep))throw new Error(`Path traversal detected: ${r}`)},fe=async e=>{try{return await U(e,re.O_RDONLY|re.O_NOFOLLOW)}catch(r){const t=r.code;if(t==="ELOOP")return null;if(t==="EINVAL"||t==="ENOSYS"||t==="ENOTSUP")return(await Se(e)).isSymbolicLink()?null:await U(e,"r");throw r}},Qe=async(e,r=5e3)=>{const t=Date.now();for(;Date.now()-t<r;)try{const s=await U(e,"wx");return{release:async()=>{await s.close(),await x(e,{force:!0})}}}catch(s){if(s.code!=="EEXIST")throw s;await new Promise(o=>setTimeout(o,100))}throw new Error(`Failed to acquire lock for ${e}.`)},Ze=async e=>{Y(e.sourceId,"sourceId");const r=$e(e.cacheDir,e.sourceId);await b(e.cacheDir,{recursive:!0});const t=await N(g.join(e.cacheDir,`.tmp-${e.sourceId}-`));let s=null;const o=async()=>{const i=s;!i||i.closed||i.destroyed||await new Promise(c=>{const a=()=>{i.off("close",n),i.off("error",u),c()},n=()=>a(),u=()=>a();i.once("close",n),i.once("error",u);try{i.end()}catch{a()}})};try{const i=(await oe(e.include,{cwd:e.repoDir,ignore:[".git/**",...e.exclude??[]],dot:!0,onlyFiles:!0,followSymbolicLinks:!1})).map(f=>({relativePath:f,normalized:H(f)})).sort((f,h)=>f.normalized.localeCompare(h.normalized)),c=new Set;for(const{relativePath:f}of i)c.add(g.dirname(f));await Promise.all(Array.from(c,f=>b(g.join(t,f),{recursive:!0})));let a=0,n=0;const u=Math.max(1,Math.min(i.length,Math.max(8,Math.min(128,Oe.cpus().length*8)))),D=g.join(t,W),w=te(D,{encoding:"utf8"});s=w;const $=_("sha256"),S=async f=>new Promise((h,m)=>{const p=l=>{w.off("drain",d),m(l)},d=()=>{w.off("error",p),h()};w.once("error",p),w.write(f)?(w.off("error",p),h()):w.once("drain",d)});for(let f=0;f<i.length;f+=u){const h=i.slice(f,f+u),m=await Promise.all(h.map(async p=>{const d=g.join(e.repoDir,p.relativePath),l=await fe(d);if(!l)return null;try{const M=await l.stat();if(!M.isFile())return null;const O=g.join(t,p.relativePath);if(Ve(t,O),M.size>=Ke){const I=Re(d,{fd:l.fd,autoClose:!1}),k=te(O);await je(I,k)}else{const I=await l.readFile();await q(O,I)}return{path:p.normalized,size:M.size}}finally{await l.close()}}));for(const p of m){if(!p)continue;if(e.maxFiles!==void 0&&n+1>e.maxFiles)throw new Error(`Materialized content exceeds maxFiles (${e.maxFiles}).`);if(a+=p.size,a>e.maxBytes)throw new Error(`Materialized content exceeds maxBytes (${e.maxBytes}).`);const d=`${JSON.stringify(p)}
3
+ `;$.update(d),await S(d),n+=1}}await new Promise((f,h)=>{w.end(()=>f()),w.once("error",h)});const y=$.digest("hex"),v=async f=>{try{return await A(f),!0}catch{return!1}};return await(async(f,h)=>{const m=await Qe(`${h}.lock`);try{const p=await v(h),d=`${h}.bak-${ye(8).toString("hex")}`;p&&await B(h,d);try{await B(f,h)}catch(l){if(p)try{await B(d,h)}catch(M){const O=M instanceof Error?M.message:String(M);process.stderr.write(`Warning: Failed to restore backup: ${O}
4
+ `)}throw l}p&&await x(d,{recursive:!0,force:!0})}finally{await m.release()}})(t,r.sourceDir),{bytes:a,fileCount:n,manifestSha256:y}}catch(i){try{await o()}catch{}throw await x(t,{recursive:!0,force:!0}),i}},et=async e=>{Y(e.sourceId,"sourceId");const r=await oe(e.include,{cwd:e.repoDir,ignore:[".git/**",...e.exclude??[]],dot:!0,onlyFiles:!0,followSymbolicLinks:!1});r.sort((i,c)=>H(i).localeCompare(H(c)));let t=0,s=0;const o=_("sha256");for(const i of r){const c=H(i),a=g.join(e.repoDir,i),n=await fe(a);if(n)try{const u=await n.stat();if(!u.isFile())continue;if(e.maxFiles!==void 0&&s+1>e.maxFiles)throw new Error(`Materialized content exceeds maxFiles (${e.maxFiles}).`);if(t+=u.size,t>e.maxBytes)throw new Error(`Materialized content exceeds maxBytes (${e.maxBytes}).`);const D=`${JSON.stringify({path:c,size:u.size})}
5
+ `;o.update(D),s+=1}finally{await n.close()}}return{bytes:t,fileCount:s,manifestSha256:o.digest("hex")}},tt=async(e,r)=>{await r.rm(e,{recursive:!0,force:!0})},X=async e=>{const r=e.deps??{cp:De,mkdir:b,rm:x,symlink:ve,stderr:process.stderr},t=g.dirname(e.targetDir);await r.mkdir(t,{recursive:!0}),await tt(e.targetDir,r);const s=process.platform==="win32"?"copy":"symlink";if((e.mode??s)==="copy"){await r.cp(e.sourceDir,e.targetDir,{recursive:!0});return}const o=process.platform==="win32"?"junction":"dir";try{await r.symlink(e.sourceDir,e.targetDir,o)}catch(i){const c=i.code;if(c&&new Set(["EPERM","EACCES","ENOTSUP","EINVAL"]).has(c)){if(e.explicitTargetMode){const a=i instanceof Error?i.message:String(i);r.stderr.write(`Warning: Failed to create symlink at ${e.targetDir}. Falling back to copy. ${a}
6
+ `)}await r.cp(e.sourceDir,e.targetDir,{recursive:!0});return}throw i}},rt=e=>{const r={dirs:new Map,files:[]};for(const t of e){const s=t.split("/").filter(Boolean);if(s.length===0)continue;let o=r;for(const c of s.slice(0,-1)){let a=o.dirs.get(c);a||(a={dirs:new Map,files:[]},o.dirs.set(c,a)),o=a}const i=s[s.length-1];o.files.push({name:i,path:t})}return r},me=(e,r,t)=>{const s=" ".repeat(r),o=Array.from(e.dirs.keys()).sort(),i=[...e.files].sort((c,a)=>c.name.localeCompare(a.name));for(const c of o){t.push(`${s}- ${c}/`);const a=e.dirs.get(c);a&&me(a,r+1,t)}for(const c of i)t.push(`${s}- [${c.name}](./${c.path})`)},ot=(e,r,t)=>{const s=[...e].sort((a,n)=>a.localeCompare(n)),o=new Map;for(const a of s){const n=a.lastIndexOf("/"),u=n===-1?"":a.substring(0,n),D=n===-1?a:a.substring(n+1),w=o.get(u);w?w.push(D):o.set(u,[D])}const i=Array.from(o.keys()).sort(),c=[];c.push(`[${t}]`);for(const a of i){const n=o.get(a);if(!n)continue;const u=n.join(",");a===""?c.push(`root:{${u}}`):c.push(`${a}:{${u}}`)}r.push(c.join("|"))},st=(e,r="compressed")=>{const t=[];if(r==="tree"){t.push(`# ${e.id} - Documentation`),t.push(""),t.push("## Files"),t.push("");const s=rt(e.files);me(s,0,t)}else{const s=`${e.id} Docs Index`;ot(e.files,t,s)}return t.push(""),t.join(`
7
+ `)},it=async e=>{const r=g.join(e,".manifest.jsonl");try{const t=await L(r,"utf8"),s=[];for(const o of t.split(`
8
+ `))if(o.trim()){const i=JSON.parse(o);i.path&&s.push(i.path)}return s}catch{return[]}},at=async e=>{const r=new Map(e.sources.map(s=>[s.id,s])),t=new Map((e.results??[]).map(s=>[s.id,s]));for(const[s,o]of Object.entries(e.lock.sources)){const i=r.get(s);i?.targetDir&&z(G(e.configPath,i.targetDir));const c=g.join(e.cacheDir,s);try{await A(c)}catch{continue}const a=await it(c),n={id:s,repo:o.repo,ref:o.ref,resolvedCommit:o.resolvedCommit,fileCount:o.fileCount,cachePath:z(g.join(e.cacheDir,s)),files:a},u=i?.toc,D=u!==!1;let w="compressed";typeof u=="string"&&(w=u);const $=g.join(c,Ce);if(D){if(t.get(s)?.status==="up-to-date")try{await A($);continue}catch{}const S=st(n,w);await q($,S,"utf8")}else try{await x($,{force:!0})}catch{}}},nt=e=>{if(e<1024)return`${e} B`;const r=["KB","MB","GB","TB"];let t=e,s=-1;for(;t>=1024&&s<r.length-1;)t/=1024,s+=1;return`${t.toFixed(1)} ${r[s]}`},j=async e=>{try{return await A(e),!0}catch{return!1}},de=async(e,r)=>{const t=g.join(e,r);return await j(t)?await j(g.join(t,W)):!1},he=e=>{if(!e||e.length===0)return[];const r=e.map(t=>t.trim()).filter(t=>t.length>0);return Array.from(new Set(r)).sort()},ct=e=>{const r={include:he(e.include),exclude:he(e.exclude)},t=_("sha256");return t.update(JSON.stringify(r)),t.digest("hex")},pe=async(e,r={})=>{const{config:t,resolvedPath:s,sources:o}=await Me(e.configPath),i=t.defaults??Pe.defaults,c=xe(s,t.cacheDir??Ee,e.cacheDirOverride),a=be(s),n=await j(a);let u=null;n&&(u=await Ie(a));const D=r.resolveRemoteCommit??Ue,w=e.sourceFilter?.length?o.filter(S=>e.sourceFilter?.includes(S.id)):o,$=await Promise.all(w.map(async S=>{const y=u?.sources?.[S.id],v=S.include??i.include,f=S.exclude,h=ct({include:v,exclude:f});if(e.offline){const l=await de(c,S.id);return{id:S.id,repo:y?.repo??S.repo,ref:y?.ref??S.ref??i.ref,resolvedCommit:y?.resolvedCommit??"offline",lockCommit:y?.resolvedCommit??null,lockRulesSha256:y?.rulesSha256,status:y&&l?"up-to-date":"missing",bytes:y?.bytes,fileCount:y?.fileCount,manifestSha256:y?.manifestSha256,rulesSha256:h}}const m=await D({repo:S.repo,ref:S.ref,allowHosts:i.allowHosts,timeoutMs:e.timeoutMs}),p=y?.resolvedCommit===m.resolvedCommit&&y?.rulesSha256===h,d=y?p?"up-to-date":"changed":"missing";return{id:S.id,repo:m.repo,ref:m.ref,resolvedCommit:m.resolvedCommit,lockCommit:y?.resolvedCommit??null,lockRulesSha256:y?.rulesSha256,status:d,bytes:y?.bytes,fileCount:y?.fileCount,manifestSha256:y?.manifestSha256,rulesSha256:h}}));return{config:t,configPath:s,cacheDir:c,lockPath:a,lockExists:n,lockData:u,results:$,sources:w,defaults:i}},lt=async()=>{const e=g.resolve(process.cwd(),"package.json");try{const r=await L(e,"utf8"),t=JSON.parse(r.toString());return typeof t.version=="string"?t.version:"0.0.0"}catch{}try{const r=await L(new URL("../package.json",import.meta.url),"utf8"),t=JSON.parse(r.toString());return typeof t.version=="string"?t.version:"0.0.0"}catch{}try{const r=await L(new URL("../../package.json",import.meta.url),"utf8"),t=JSON.parse(r.toString());return typeof t.version=="string"?t.version:"0.0.0"}catch{return"0.0.0"}},ut=async(e,r)=>{const t=await lt(),s=new Date().toISOString(),o={...r?.sources??{}};for(const i of e.results){const c=o[i.id];o[i.id]={repo:i.repo,ref:i.ref,resolvedCommit:i.resolvedCommit,bytes:i.bytes??c?.bytes??0,fileCount:i.fileCount??c?.fileCount??0,manifestSha256:i.manifestSha256??c?.manifestSha256??i.resolvedCommit,rulesSha256:i.rulesSha256??c?.rulesSha256,updatedAt:s}}return{version:1,generatedAt:s,toolVersion:t,sources:o}},we=async(e,r={})=>{const t=process.hrtime.bigint();let s=0;const o=await pe(e,r);await b(o.cacheDir,{recursive:!0});const i=o.lockData,c=o.results.filter(n=>{const u=o.sources.find(D=>D.id===n.id);return n.status==="missing"&&(u?.required??!0)});if(e.failOnMiss&&c.length>0)throw new Error(`Missing required source(s): ${c.map(n=>n.id).join(", ")}.`);if(!e.lockOnly){const n=o.defaults,u=r.fetchSource??qe,D=r.materializeSource??Ze,w=new Map,$=async(v,f)=>{const h=v?.length?o.results.filter(m=>v.includes(m.id)):o.results;return(await Promise.all(h.map(async m=>{const p=o.sources.find(l=>l.id===m.id);if(!p)return null;if(f)return{result:m,source:p};let d=w.get(m.id);return d===void 0&&(d=await de(o.cacheDir,m.id),w.set(m.id,d)),m.status!=="up-to-date"||!d?{result:m,source:p}:null}))).filter(Boolean)},S=async()=>{await Promise.all(o.sources.map(async v=>{if(!v.targetDir)return;const f=G(o.configPath,v.targetDir);await j(f)||await X({sourceDir:g.join(o.cacheDir,v.id),targetDir:f,mode:v.targetMode??n.targetMode,explicitTargetMode:v.targetMode!==void 0})}))},y=async v=>{const f=e.concurrency??4;let h=0;const m=async()=>{const p=v[h];if(!p||!p.source)return;h+=1;const{result:d,source:l}=p,M=o.lockData?.sources?.[l.id];e.json||C.step("Fetching",l.id);const O=await u({sourceId:l.id,repo:l.repo,ref:l.ref,resolvedCommit:d.resolvedCommit,cacheDir:o.cacheDir,depth:l.depth??n.depth,include:l.include??n.include,timeoutMs:e.timeoutMs});try{const I=g.join(o.cacheDir,l.id,W);if(d.status!=="up-to-date"&&M?.manifestSha256&&await j(I)){const F=await et({sourceId:l.id,repoDir:O.repoDir,cacheDir:o.cacheDir,include:l.include??n.include,exclude:l.exclude,maxBytes:l.maxBytes??n.maxBytes,maxFiles:l.maxFiles??n.maxFiles});if(F.manifestSha256===M.manifestSha256){d.bytes=F.bytes,d.fileCount=F.fileCount,d.manifestSha256=F.manifestSha256,d.status="up-to-date",e.json||C.item(E.success,l.id,"no content changes"),await m();return}}const k=await D({sourceId:l.id,repoDir:O.repoDir,cacheDir:o.cacheDir,include:l.include??n.include,exclude:l.exclude,maxBytes:l.maxBytes??n.maxBytes,maxFiles:l.maxFiles??n.maxFiles});if(l.targetDir){const F=G(o.configPath,l.targetDir);await X({sourceDir:g.join(o.cacheDir,l.id),targetDir:F,mode:l.targetMode??n.targetMode,explicitTargetMode:l.targetMode!==void 0})}d.bytes=k.bytes,d.fileCount=k.fileCount,d.manifestSha256=k.manifestSha256,e.json||C.item(E.success,l.id,`synced ${k.fileCount} files`)}finally{await O.cleanup()}await m()};await Promise.all(Array.from({length:Math.min(f,v.length)},m))};if(e.offline)await S();else{const v=await $();await y(v),await S()}if(!e.offline){const v=(await ee({configPath:o.configPath,cacheDirOverride:o.cacheDir})).results.filter(f=>!f.ok);if(v.length>0){const f=await $(v.map(m=>m.id),!0);f.length>0&&(await y(f),await S());const h=(await ee({configPath:o.configPath,cacheDirOverride:o.cacheDir})).results.filter(m=>!m.ok);if(h.length>0&&(s+=1,!e.json)){const m=h.map(p=>`${p.id} (${p.issues.join("; ")})`).join(", ");C.line(`${E.warn} Verify failed for ${h.length} source(s): ${m}`)}}}}const a=await ut(o,i);if(await Fe(o.lockPath,a),!e.json){const n=Number(process.hrtime.bigint()-t)/1e6,u=o.results.reduce((w,$)=>w+($.bytes??0),0),D=o.results.reduce((w,$)=>w+($.fileCount??0),0);C.line(`${E.info} Completed in ${n.toFixed(0)}ms \xB7 ${nt(u)} \xB7 ${D} files${s?` \xB7 ${s} warning${s===1?"":"s"}`:""}`)}return await at({cacheDir:o.cacheDir,configPath:o.configPath,lock:a,sources:o.sources,results:o.results}),o.lockExists=!0,o},ge=e=>{const r={upToDate:e.results.filter(t=>t.status==="up-to-date").length,changed:e.results.filter(t=>t.status==="changed").length,missing:e.results.filter(t=>t.status==="missing").length};if(e.results.length===0){C.line(`${E.info} No sources to sync.`);return}C.line(`${E.info} ${e.results.length} sources (${r.upToDate} up-to-date, ${r.changed} changed, ${r.missing} missing)`);for(const t of e.results){const s=C.hash(t.resolvedCommit),o=C.hash(t.lockCommit),i=!!t.lockRulesSha256&&!!t.rulesSha256&&t.lockRulesSha256!==t.rulesSha256;if(t.status==="up-to-date"){C.item(E.success,t.id,`${P.dim("up-to-date")} ${P.gray(s)}`);continue}if(t.status==="changed"){if(t.lockCommit===t.resolvedCommit&&i){C.item(E.warn,t.id,`${P.dim("rules changed")} ${P.gray(s)}`);continue}C.item(E.warn,t.id,`${P.dim("changed")} ${P.gray(o)} ${P.dim("->")} ${P.gray(s)}`);continue}C.item(E.warn,t.id,`${P.dim("missing")} ${P.gray(s)}`)}},ft={__proto__:null,getSyncPlan:pe,printSyncPlan:ge,runSync:we};export{X as a,ge as b,we as c,se as e,ie as p,R as r,ft as s};
8
9
  //# sourceMappingURL=sync.mjs.map
@@ -1,2 +1,2 @@
1
- import{stat as v,access as k}from"node:fs/promises";import g from"node:path";import{a as _,r as C,u as l,s as m}from"../shared/docs-cache.D9_kM5zq.mjs";import{l as E,b as j}from"../shared/docs-cache.D4Fth4X8.mjs";import{createReadStream as M}from"node:fs";import z from"node:readline";const d=".manifest.jsonl",A=async function*(s){const a=g.join(s,d),r=M(a,{encoding:"utf8"}),e=z.createInterface({input:r,crlfDelay:1/0});try{for await(const u of e){const f=u.trim();f&&(yield JSON.parse(f))}}finally{e.close(),r.destroy()}},p=async s=>{try{return await k(s),!0}catch{return!1}},w=async s=>{const{config:a,resolvedPath:r,sources:e}=await E(s.configPath),u=_(r,a.cacheDir??j,s.cacheDirOverride),f=async(i,o)=>{if(!await p(i))return{ok:!1,issues:[o==="source"?"missing source directory":"missing target directory"]};try{let t=0,n=0;for await(const h of A(i)){const y=g.join(i,h.path);if(!await p(y)){t+=1;continue}(await v(y)).size!==h.size&&(n+=1)}const c=[];return t>0&&c.push(o==="source"?`missing files: ${t}`:`target missing files: ${t}`),n>0&&c.push(o==="source"?`size mismatch: ${n}`:`target size mismatch: ${n}`),{ok:c.length===0,issues:c}}catch{return{ok:!1,issues:[o==="source"?"missing manifest":"missing target manifest"]}}},D=await Promise.all(e.map(async i=>{const o=g.join(u,i.id),t=[...(await f(o,"source")).issues];if(i.targetDir&&i.targetMode==="copy"){const n=C(r,i.targetDir),c=await f(n,"target");t.push(...c.issues)}return{id:i.id,ok:t.length===0,issues:t}}));return{cacheDir:u,results:D}},I=s=>{const a=s.results.filter(e=>e.ok).length,r=s.results.length-a;if(s.results.length===0){l.line(`${m.warn} No sources to verify.`);return}l.line(`${m.info} Verified ${s.results.length} sources (${a} ok, ${r} failed)`);for(const e of s.results)e.ok?l.item(m.success,e.id):l.item(m.warn,e.id,e.issues.join(", "))},N={__proto__:null,printVerify:I,verifyCache:w};export{d as M,N as a,w as v};
1
+ import{stat as $,access as N}from"node:fs/promises";import h from"node:path";import{b as v,r as k,u as l,a as m}from"../shared/docs-cache.DDgb7yxQ.mjs";import{l as O,b as T}from"../shared/docs-cache.BWEcxcrg.mjs";import{createReadStream as _}from"node:fs";import C from"node:readline";const p=".manifest.jsonl",I=async function*(s){const o=h.join(s,p),a=_(o,{encoding:"utf8"}),t=C.createInterface({input:a,crlfDelay:1/0});try{for await(const u of t){const f=u.trim();f&&(yield JSON.parse(f))}}finally{t.close(),a.destroy()}},j=async s=>{try{return await N(s),!0}catch{return!1}},w=async s=>{const{config:o,resolvedPath:a,sources:t}=await O(s.configPath),u=v(a,o.cacheDir??T,s.cacheDirOverride),f=async(r,n)=>{if(!await j(r))return{ok:!1,issues:[n==="source"?"missing source directory":"missing target directory"]};try{let e=0,i=0;for await(const g of I(r)){const E=h.join(r,g.path);try{(await $(E)).size!==g.size&&(i+=1)}catch(y){const d=y.code;if(d==="ENOENT"||d==="ENOTDIR"){e+=1;continue}throw y}}const c=[];return e>0&&c.push(n==="source"?`missing files: ${e}`:`target missing files: ${e}`),i>0&&c.push(n==="source"?`size mismatch: ${i}`:`target size mismatch: ${i}`),{ok:c.length===0,issues:c}}catch(e){const i=e.code;if(i==="ENOENT"||i==="ENOTDIR")return{ok:!1,issues:[n==="source"?"missing manifest":"missing target manifest"]};throw e}},D=await Promise.all(t.map(async r=>{const n=h.join(u,r.id),e=[...(await f(n,"source")).issues];if(r.targetDir&&r.targetMode==="copy"){const i=k(a,r.targetDir),c=await f(i,"target");e.push(...c.issues)}return{id:r.id,ok:e.length===0,issues:e}}));return{cacheDir:u,results:D}},M=s=>{const o=s.results.filter(t=>t.ok).length,a=s.results.length-o;if(s.results.length===0){l.line(`${m.warn} No sources to verify.`);return}l.line(`${m.info} Verified ${s.results.length} sources (${o} ok, ${a} failed)`);for(const t of s.results)t.ok?l.item(m.success,t.id):l.item(m.warn,t.id,t.issues.join(", "))},z={__proto__:null,printVerify:M,verifyCache:w};export{p as M,z as a,w as v};
2
2
  //# sourceMappingURL=verify.mjs.map
package/dist/cli.mjs CHANGED
@@ -1,15 +1,16 @@
1
- import w from"node:path";import a from"node:process";import l from"picocolors";import{p as j,E as f,s as g,u as r}from"./shared/docs-cache.D9_kM5zq.mjs";import"cac";const u="docs-cache",O=`
1
+ import y from"node:path";import s from"node:process";import g from"picocolors";import{p as O,s as $,E as f,a as l,u as r}from"./shared/docs-cache.DDgb7yxQ.mjs";import"cac";const u="docs-cache",D=`
2
2
  Usage: ${u} <command> [options]
3
3
 
4
4
  Commands:
5
- add Add sources to the config (supports github:org/repo#ref)
6
- remove Remove sources from the config and targets
7
- sync Synchronize cache with config
8
- status Show cache status
9
- clean Remove cache
10
- prune Remove unused data
11
- verify Validate cache integrity
12
- init Create a new config interactively
5
+ add Add sources to the config (supports github:org/repo#ref)
6
+ remove Remove sources from the config and targets
7
+ sync Synchronize cache with config
8
+ status Show cache status
9
+ clean Remove project cache
10
+ clean-cache Clear global git cache
11
+ prune Remove unused data
12
+ verify Validate cache integrity
13
+ init Create a new config interactively
13
14
 
14
15
  Global options:
15
16
  --source <repo> (add only)
@@ -23,14 +24,16 @@ Global options:
23
24
  --concurrency <n>
24
25
  --json
25
26
  --timeout-ms <n>
26
- `,m=()=>{a.stdout.write(O.trimStart())},p=e=>{a.stderr.write(`${g.error} ${e}
27
- `)},D=e=>{const t=e.findIndex(i=>!i.startsWith("-")),h=t===-1?[]:e.slice(t+1),d=[];let s=-1;const n=new Set(["--config","--cache-dir","--concurrency","--timeout-ms"]);for(let i=0;i<h.length;i+=1){const o=h[i];if(o==="--source"){const c=h[i+1];if(!c||c.startsWith("-"))throw new Error("--source expects a value.");d.push({repo:c}),s=d.length-1,i+=1;continue}if(o==="--target"||o==="--target-dir"){const c=h[i+1];if(!c||c.startsWith("-"))throw new Error("--target expects a value.");if(s===-1)throw new Error("--target must follow a --source entry.");d[s].targetDir=c,i+=1;continue}if(n.has(o)){i+=1;continue}o.startsWith("--")||(d.push({repo:o}),s=d.length-1)}return d},S=async(e,t,h,d)=>{if(e==="add"){const{addSources:s}=await import("./chunks/add.mjs"),{runSync:n}=await import("./chunks/sync.mjs").then(function(c){return c.s}),i=D(d);if(i.length===0)throw new Error("Usage: docs-cache add [--source <repo> --target <dir>] <repo...>");const o=await s({configPath:t.config,entries:i});if(t.offline?t.json||r.line(`${g.warn} Offline: skipped sync`):await n({configPath:t.config,cacheDirOverride:t.cacheDir,json:t.json,lockOnly:t.lockOnly,offline:t.offline,failOnMiss:t.failOnMiss,sourceFilter:o.sources.map(c=>c.id),timeoutMs:t.timeoutMs}),t.json)a.stdout.write(`${JSON.stringify(o,null,2)}
28
- `);else{for(const c of o.sources){const y=c.repo.replace(/^https?:\/\//,"").replace(/\.git$/,""),v=c.targetDir?` ${l.dim("->")} ${l.magenta(c.targetDir)}`:"";r.item(g.success,c.id,`${l.blue(y)}${v}`)}o.skipped?.length&&r.line(`${g.warn} Skipped ${o.skipped.length} existing source${o.skipped.length===1?"":"s"}: ${o.skipped.join(", ")}`),r.line(`${g.info} Updated ${l.gray(w.relative(a.cwd(),o.configPath)||"docs.config.json")}`),o.gitignoreUpdated&&o.gitignorePath&&r.line(`${g.info} Updated ${l.gray(r.path(o.gitignorePath))}`)}return}if(e==="remove"){const{removeSources:s}=await import("./chunks/remove.mjs"),{pruneCache:n}=await import("./chunks/prune.mjs");if(h.length===0)throw new Error("Usage: docs-cache remove <id...>");const i=await s({configPath:t.config,ids:h});if(t.json)a.stdout.write(`${JSON.stringify(i,null,2)}
29
- `);else{if(i.removed.length>0&&r.line(`${g.success} Removed ${i.removed.length} source${i.removed.length===1?"":"s"}: ${i.removed.join(", ")}`),i.missing.length>0&&r.line(`${g.warn} Missing ${i.missing.length} source${i.missing.length===1?"":"s"}: ${i.missing.join(", ")}`),i.targetsRemoved.length>0){const o=i.targetsRemoved.map(c=>`${c.id} -> ${r.path(c.targetDir)}`).join(", ");r.line(`${g.success} Removed ${i.targetsRemoved.length} target${i.targetsRemoved.length===1?"":"s"}: ${o}`)}r.line(`${g.info} Updated ${l.gray(w.relative(a.cwd(),i.configPath)||"docs.config.json")}`)}t.prune&&await n({configPath:t.config,cacheDirOverride:t.cacheDir,json:t.json});return}if(e==="status"){const{getStatus:s,printStatus:n}=await import("./chunks/status.mjs"),i=await s({configPath:t.config,cacheDirOverride:t.cacheDir,json:t.json});t.json?a.stdout.write(`${JSON.stringify(i,null,2)}
30
- `):n(i);return}if(e==="clean"){const{cleanCache:s}=await import("./chunks/clean.mjs"),n=await s({configPath:t.config,cacheDirOverride:t.cacheDir,json:t.json});t.json?a.stdout.write(`${JSON.stringify(n,null,2)}
31
- `):n.removed?r.line(`${g.success} Removed cache at ${r.path(n.cacheDir)}`):r.line(`${g.info} Cache already missing at ${r.path(n.cacheDir)}`);return}if(e==="prune"){const{pruneCache:s}=await import("./chunks/prune.mjs"),n=await s({configPath:t.config,cacheDirOverride:t.cacheDir,json:t.json});t.json?a.stdout.write(`${JSON.stringify(n,null,2)}
32
- `):n.removed.length===0?r.line(`${g.info} No cache entries to prune.`):r.line(`${g.success} Pruned ${n.removed.length} cache entr${n.removed.length===1?"y":"ies"}: ${n.removed.join(", ")}`);return}if(e==="sync"){const{printSyncPlan:s,runSync:n}=await import("./chunks/sync.mjs").then(function(o){return o.s}),i=await n({configPath:t.config,cacheDirOverride:t.cacheDir,json:t.json,lockOnly:t.lockOnly,offline:t.offline,failOnMiss:t.failOnMiss,timeoutMs:t.timeoutMs});t.json?a.stdout.write(`${JSON.stringify(i,null,2)}
33
- `):s(i);return}if(e==="verify"){const{printVerify:s,verifyCache:n}=await import("./chunks/verify.mjs").then(function(o){return o.a}),i=await n({configPath:t.config,cacheDirOverride:t.cacheDir,json:t.json});t.json?a.stdout.write(`${JSON.stringify(i,null,2)}
34
- `):s(i),i.results.some(o=>!o.ok)&&a.exit(f.FatalError);return}if(e==="init"){const{initConfig:s}=await import("./chunks/init.mjs");if(t.config)throw new Error("Init does not accept --config. Use the project root.");const n=await s({cacheDirOverride:t.cacheDir,json:t.json});t.json?a.stdout.write(`${JSON.stringify(n,null,2)}
35
- `):(r.line(`${g.success} Wrote ${l.gray(r.path(n.configPath))}`),n.gitignoreUpdated&&n.gitignorePath&&r.line(`${g.info} Updated ${l.gray(r.path(n.gitignorePath))}`));return}r.line(`${u} ${e}: not implemented yet.`)};async function P(){try{a.on("uncaughtException",$),a.on("unhandledRejection",$);const e=j(),t=e.rawArgs;e.help&&(m(),a.exit(f.Success)),e.command||(m(),a.exit(f.InvalidArgument)),e.command!=="add"&&e.command!=="remove"&&e.positionals.length>0&&(p(`${u}: unexpected arguments.`),m(),a.exit(f.InvalidArgument)),e.command!=="add"&&e.options.targetDir&&(p(`${u}: --target-dir is only valid for add.`),m(),a.exit(f.InvalidArgument)),await S(e.command,e.options,e.positionals,e.rawArgs)}catch(e){$(e)}}function $(e){const t=e.message||String(e);p(t),a.exit(f.FatalError)}export{u as CLI_NAME,P as main};
27
+ --silent
28
+ `,p=()=>{s.stdout.write(D.trimStart())},m=n=>{s.stderr.write(`${l.error} ${n}
29
+ `)},S=n=>{const t=n.findIndex(e=>!e.startsWith("-")),h=t===-1?[]:n.slice(t+1),d=[];let a=-1;const i=new Set(["--config","--cache-dir","--concurrency","--timeout-ms"]);for(let e=0;e<h.length;e+=1){const o=h[e];if(o==="--source"){const c=h[e+1];if(!c||c.startsWith("-"))throw new Error("--source expects a value.");d.push({repo:c}),a=d.length-1,e+=1;continue}if(o==="--target"||o==="--target-dir"){const c=h[e+1];if(!c||c.startsWith("-"))throw new Error("--target expects a value.");if(a===-1)throw new Error("--target must follow a --source entry.");d[a].targetDir=c,e+=1;continue}if(i.has(o)){e+=1;continue}o.startsWith("--")||(d.push({repo:o}),a=d.length-1)}return d},P=async(n,t,h,d)=>{if(n==="add"){const{addSources:a}=await import("./chunks/add.mjs"),{runSync:i}=await import("./chunks/sync.mjs").then(function(c){return c.s}),e=S(d);if(e.length===0)throw new Error("Usage: docs-cache add [--source <repo> --target <dir>] <repo...>");const o=await a({configPath:t.config,entries:e});if(t.offline?t.json||r.line(`${l.warn} Offline: skipped sync`):await i({configPath:t.config,cacheDirOverride:t.cacheDir,json:t.json,lockOnly:t.lockOnly,offline:t.offline,failOnMiss:t.failOnMiss,sourceFilter:o.sources.map(c=>c.id),timeoutMs:t.timeoutMs}),t.json)s.stdout.write(`${JSON.stringify(o,null,2)}
30
+ `);else{for(const c of o.sources){const v=c.repo.replace(/^https?:\/\//,"").replace(/\.git$/,""),j=c.targetDir?` ${g.dim("->")} ${g.magenta(c.targetDir)}`:"";r.item(l.success,c.id,`${g.blue(v)}${j}`)}o.skipped?.length&&r.line(`${l.warn} Skipped ${o.skipped.length} existing source${o.skipped.length===1?"":"s"}: ${o.skipped.join(", ")}`),r.line(`${l.info} Updated ${g.gray(y.relative(s.cwd(),o.configPath)||"docs.config.json")}`),o.gitignoreUpdated&&o.gitignorePath&&r.line(`${l.info} Updated ${g.gray(r.path(o.gitignorePath))}`)}return}if(n==="remove"){const{removeSources:a}=await import("./chunks/remove.mjs"),{pruneCache:i}=await import("./chunks/prune.mjs");if(h.length===0)throw new Error("Usage: docs-cache remove <id...>");const e=await a({configPath:t.config,ids:h});if(t.json)s.stdout.write(`${JSON.stringify(e,null,2)}
31
+ `);else{if(e.removed.length>0&&r.line(`${l.success} Removed ${e.removed.length} source${e.removed.length===1?"":"s"}: ${e.removed.join(", ")}`),e.missing.length>0&&r.line(`${l.warn} Missing ${e.missing.length} source${e.missing.length===1?"":"s"}: ${e.missing.join(", ")}`),e.targetsRemoved.length>0){const o=e.targetsRemoved.map(c=>`${c.id} -> ${r.path(c.targetDir)}`).join(", ");r.line(`${l.success} Removed ${e.targetsRemoved.length} target${e.targetsRemoved.length===1?"":"s"}: ${o}`)}r.line(`${l.info} Updated ${g.gray(y.relative(s.cwd(),e.configPath)||"docs.config.json")}`)}t.prune&&await i({configPath:t.config,cacheDirOverride:t.cacheDir,json:t.json});return}if(n==="status"){const{getStatus:a,printStatus:i}=await import("./chunks/status.mjs"),e=await a({configPath:t.config,cacheDirOverride:t.cacheDir,json:t.json});t.json?s.stdout.write(`${JSON.stringify(e,null,2)}
32
+ `):i(e);return}if(n==="clean"){const{cleanCache:a}=await import("./chunks/clean.mjs"),i=await a({configPath:t.config,cacheDirOverride:t.cacheDir,json:t.json});t.json?s.stdout.write(`${JSON.stringify(i,null,2)}
33
+ `):i.removed?r.line(`${l.success} Removed cache at ${r.path(i.cacheDir)}`):r.line(`${l.info} Cache already missing at ${r.path(i.cacheDir)}`);return}if(n==="clean-cache"){const{cleanGitCache:a}=await import("./chunks/clean-git-cache.mjs"),i=await a();if(t.json)s.stdout.write(`${JSON.stringify(i,null,2)}
34
+ `);else if(i.removed){const e=i.bytesFreed!==void 0?`${(i.bytesFreed/1024/1024).toFixed(2)} MB`:"unknown size",o=i.repoCount!==void 0?` (${i.repoCount} cached repositor${i.repoCount===1?"y":"ies"})`:"";r.line(`${l.success} Cleared global git cache${o}: ${e} freed`),r.line(`${l.info} Cache location: ${r.path(i.cacheDir)}`)}else r.line(`${l.info} Global git cache already empty at ${r.path(i.cacheDir)}`);return}if(n==="prune"){const{pruneCache:a}=await import("./chunks/prune.mjs"),i=await a({configPath:t.config,cacheDirOverride:t.cacheDir,json:t.json});t.json?s.stdout.write(`${JSON.stringify(i,null,2)}
35
+ `):i.removed.length===0?r.line(`${l.info} No cache entries to prune.`):r.line(`${l.success} Pruned ${i.removed.length} cache entr${i.removed.length===1?"y":"ies"}: ${i.removed.join(", ")}`);return}if(n==="sync"){const{printSyncPlan:a,runSync:i}=await import("./chunks/sync.mjs").then(function(o){return o.s}),e=await i({configPath:t.config,cacheDirOverride:t.cacheDir,json:t.json,lockOnly:t.lockOnly,offline:t.offline,failOnMiss:t.failOnMiss,timeoutMs:t.timeoutMs});t.json?s.stdout.write(`${JSON.stringify(e,null,2)}
36
+ `):a(e);return}if(n==="verify"){const{printVerify:a,verifyCache:i}=await import("./chunks/verify.mjs").then(function(o){return o.a}),e=await i({configPath:t.config,cacheDirOverride:t.cacheDir,json:t.json});t.json?s.stdout.write(`${JSON.stringify(e,null,2)}
37
+ `):a(e),e.results.some(o=>!o.ok)&&s.exit(f.FatalError);return}if(n==="init"){const{initConfig:a}=await import("./chunks/init.mjs");if(t.config)throw new Error("Init does not accept --config. Use the project root.");const i=await a({cacheDirOverride:t.cacheDir,json:t.json});t.json?s.stdout.write(`${JSON.stringify(i,null,2)}
38
+ `):(r.line(`${l.success} Wrote ${g.gray(r.path(i.configPath))}`),i.gitignoreUpdated&&i.gitignorePath&&r.line(`${l.info} Updated ${g.gray(r.path(i.gitignorePath))}`));return}r.line(`${u} ${n}: not implemented yet.`)};async function C(){try{s.on("uncaughtException",w),s.on("unhandledRejection",w);const n=O(),t=n.rawArgs;$(n.options.silent),n.help&&(p(),s.exit(f.Success)),n.command||(p(),s.exit(f.InvalidArgument)),n.command!=="add"&&n.command!=="remove"&&n.positionals.length>0&&(m(`${u}: unexpected arguments.`),p(),s.exit(f.InvalidArgument)),n.command!=="add"&&n.options.targetDir&&(m(`${u}: --target-dir is only valid for add.`),p(),s.exit(f.InvalidArgument)),await P(n.command,n.options,n.positionals,n.rawArgs)}catch(n){w(n)}}function w(n){const t=n.message||String(n);m(t),s.exit(f.FatalError)}export{u as CLI_NAME,C as main};
36
39
  //# sourceMappingURL=cli.mjs.map
@@ -0,0 +1,3 @@
1
+ import{writeFile as I,readFile as T,access as z}from"node:fs/promises";import h from"node:path";import{z as i}from"zod";import{r as L}from"./docs-cache.DDgb7yxQ.mjs";const b=i.enum(["symlink","copy"]),M=i.enum(["materialize"]),D=i.enum(["tree","compressed"]),U=i.object({type:i.enum(["commit","manifest"]),value:i.string().nullable()}).strict(),_=i.object({ref:i.string().min(1),mode:M,include:i.array(i.string().min(1)).min(1),targetMode:b.optional(),depth:i.number().min(1),required:i.boolean(),maxBytes:i.number().min(1),maxFiles:i.number().min(1).optional(),allowHosts:i.array(i.string().min(1)).min(1),toc:i.union([i.boolean(),D]).optional()}).strict(),P=i.object({id:i.string().min(1),repo:i.string().min(1),targetDir:i.string().min(1).optional(),targetMode:b.optional(),ref:i.string().min(1).optional(),mode:M.optional(),depth:i.number().min(1).optional(),include:i.array(i.string().min(1)).optional(),exclude:i.array(i.string().min(1)).optional(),required:i.boolean().optional(),maxBytes:i.number().min(1).optional(),maxFiles:i.number().min(1).optional(),integrity:U.optional(),toc:i.union([i.boolean(),D]).optional()}).strict(),J=i.object({$schema:i.string().min(1).optional(),cacheDir:i.string().min(1).optional(),targetMode:b.optional(),defaults:_.partial().optional(),sources:i.array(P)}).strict(),R=/^[a-zA-Z0-9_-]+$/,G=new Set([".","..","CON","PRN","AUX","NUL","COM1","LPT1"]),F=(e,t)=>{if(typeof e!="string"||e.length===0)throw new Error(`${t} must be a non-empty string.`);if(e.length>200)throw new Error(`${t} exceeds maximum length of 200.`);if(!R.test(e))throw new Error(`${t} must contain only alphanumeric characters, hyphens, and underscores.`);if(G.has(e.toUpperCase()))throw new Error(`${t} uses reserved name '${e}'.`);return e},j="docs.config.json",v=".docs",A="package.json",V=process.platform==="win32"?"copy":"symlink",g={cacheDir:v,defaults:{ref:"HEAD",mode:"materialize",include:["**/*.{md,mdx,markdown,mkd,txt,rst,adoc,asciidoc}"],targetMode:V,depth:1,required:!0,maxBytes:2e8,allowHosts:["github.com","gitlab.com"],toc:!0},sources:[]},X=(e,t)=>!e||!t?e===t:e.length!==t.length?!1:e.every((r,o)=>r===t[o]),S=e=>typeof e=="object"&&e!==null&&!Array.isArray(e),C=(e,t)=>{const r={};for(const[o,d]of Object.entries(e)){const n=t[o];if(Array.isArray(d)&&Array.isArray(n)){X(d,n)||(r[o]=d);continue}if(S(d)&&S(n)){const c=C(d,n);Object.keys(c).length>0&&(r[o]=c);continue}d!==n&&(r[o]=d)}return r},Z=e=>{const t={...g,$schema:e.$schema,defaults:{...g.defaults,...e.targetMode?{targetMode:e.targetMode}:void 0}},r=C(e,t),o={$schema:r.$schema,cacheDir:r.cacheDir,targetMode:r.targetMode,defaults:r.defaults,sources:e.sources};return(!o.defaults||Object.keys(o.defaults).length===0)&&delete o.defaults,o},p=e=>typeof e=="object"&&e!==null&&!Array.isArray(e),l=(e,t)=>{if(typeof e!="string"||e.length===0)throw new Error(`${t} must be a non-empty string.`);return e},k=(e,t)=>{if(typeof e!="boolean")throw new Error(`${t} must be a boolean.`);return e},K=(e,t)=>{if(typeof e!="number"||Number.isNaN(e))throw new Error(`${t} must be a number.`);return e},f=(e,t)=>{const r=K(e,t);if(r<1)throw new Error(`${t} must be greater than zero.`);return r},y=(e,t)=>{if(!Array.isArray(e)||e.length===0)throw new Error(`${t} must be a non-empty array of strings.`);for(const r of e)if(typeof r!="string"||r.length===0)throw new Error(`${t} must contain non-empty strings.`);return e},q=(e,t)=>{const r=l(e,t);if(r!=="symlink"&&r!=="copy")throw new Error(`${t} must be "symlink" or "copy".`);return r},B=(e,t)=>{if(e!=="materialize")throw new Error(`${t} must be "materialize".`);return e},Q=(e,t)=>{if(!p(e))throw new Error(`${t} must be an object.`);const r=e.type;if(r!=="commit"&&r!=="manifest")throw new Error(`${t}.type must be "commit" or "manifest".`);const o=e.value;if(typeof o!="string"&&o!==null)throw new Error(`${t}.value must be a string or null.`);return{type:r,value:o}},N=e=>{if(!p(e))throw new Error("Config must be a JSON object.");const t=J.safeParse(e);if(!t.success){const s=t.error.issues.map(a=>`${a.path.join(".")||"config"} ${a.message}`).join("; ");throw new Error(`Config does not match schema: ${s}.`)}const r=e.cacheDir?l(e.cacheDir,"cacheDir"):v,o=e.defaults,d=e.targetMode!==void 0?q(e.targetMode,"targetMode"):void 0,n=g.defaults;let c=n;if(o!==void 0){if(!p(o))throw new Error("defaults must be an object.");c={ref:o.ref!==void 0?l(o.ref,"defaults.ref"):n.ref,mode:o.mode!==void 0?B(o.mode,"defaults.mode"):n.mode,include:o.include!==void 0?y(o.include,"defaults.include"):n.include,targetMode:o.targetMode!==void 0?q(o.targetMode,"defaults.targetMode"):d??n.targetMode,depth:o.depth!==void 0?f(o.depth,"defaults.depth"):n.depth,required:o.required!==void 0?k(o.required,"defaults.required"):n.required,maxBytes:o.maxBytes!==void 0?f(o.maxBytes,"defaults.maxBytes"):n.maxBytes,maxFiles:o.maxFiles!==void 0?f(o.maxFiles,"defaults.maxFiles"):n.maxFiles,allowHosts:o.allowHosts!==void 0?y(o.allowHosts,"defaults.allowHosts"):n.allowHosts,toc:o.toc!==void 0?o.toc:n.toc}}else d!==void 0&&(c={...n,targetMode:d});if(!Array.isArray(e.sources))throw new Error("sources must be an array.");const m=e.sources.map((s,a)=>{if(!p(s))throw new Error(`sources[${a}] must be an object.`);const u={id:F(s.id,`sources[${a}].id`),repo:l(s.repo,`sources[${a}].repo`)};if(s.targetDir!==void 0&&(u.targetDir=l(s.targetDir,`sources[${a}].targetDir`)),s.targetMode!==void 0){const $=l(s.targetMode,`sources[${a}].targetMode`);if($!=="symlink"&&$!=="copy")throw new Error(`sources[${a}].targetMode must be "symlink" or "copy".`);u.targetMode=$}return s.ref!==void 0&&(u.ref=l(s.ref,`sources[${a}].ref`)),s.mode!==void 0&&(u.mode=B(s.mode,`sources[${a}].mode`)),s.depth!==void 0&&(u.depth=f(s.depth,`sources[${a}].depth`)),s.include!==void 0&&(u.include=y(s.include,`sources[${a}].include`)),s.exclude!==void 0&&(u.exclude=y(s.exclude,`sources[${a}].exclude`)),s.required!==void 0&&(u.required=k(s.required,`sources[${a}].required`)),s.maxBytes!==void 0&&(u.maxBytes=f(s.maxBytes,`sources[${a}].maxBytes`)),s.maxFiles!==void 0&&(u.maxFiles=f(s.maxFiles,`sources[${a}].maxFiles`)),s.integrity!==void 0&&(u.integrity=Q(s.integrity,`sources[${a}].integrity`)),s.toc!==void 0&&(u.toc=s.toc),u}),E=new Set,w=[];for(const s of m)E.has(s.id)&&w.push(s.id),E.add(s.id);if(w.length>0)throw new Error(`Duplicate source IDs found: ${w.join(", ")}. Each source must have a unique ID.`);return{cacheDir:r,targetMode:d,defaults:c,sources:m}},W=e=>{const t=e.defaults??g.defaults;return e.sources.map(r=>({id:r.id,repo:r.repo,targetDir:r.targetDir,targetMode:r.targetMode??t.targetMode,ref:r.ref??t.ref,mode:r.mode??t.mode,depth:r.depth??t.depth,include:r.include??t.include,exclude:r.exclude,required:r.required??t.required,maxBytes:r.maxBytes??t.maxBytes,maxFiles:r.maxFiles??t.maxFiles,integrity:r.integrity,toc:r.toc??t.toc}))},O=e=>e?h.resolve(e):h.resolve(process.cwd(),j),Y=()=>h.resolve(process.cwd(),A),H=async e=>{try{return await z(e),!0}catch{return!1}},x=async(e,t)=>{let r;try{r=await T(e,"utf8")}catch(c){const m=c instanceof Error?c.message:String(c);throw new Error(`Failed to read config at ${e}: ${m}`)}let o;try{o=JSON.parse(r)}catch(c){const m=c instanceof Error?c.message:String(c);throw new Error(`Invalid JSON in ${e}: ${m}`)}const d=t==="package"?o?.["docs-cache"]:o;if(t==="package"&&d===void 0)throw new Error(`Missing docs-cache config in ${e}.`);const n=N(d);for(const c of n.sources)c.targetDir&&L(e,c.targetDir);return{config:n,resolvedPath:e,sources:W(n)}},ee=async(e,t)=>{const r=`${JSON.stringify(t,null,2)}
2
+ `;await I(e,r,"utf8")},te=async e=>{const t=O(e),r=h.basename(t)===A;if(e)return x(t,r?"package":"config");if(await H(t))return x(t,"config");const o=Y();if(await H(o))try{return await x(o,"package")}catch{}throw new Error(`No docs.config.json found at ${t} and no docs-cache config in ${o}.`)};export{g as D,F as a,v as b,j as c,te as l,O as r,Z as s,N as v,ee as w};
3
+ //# sourceMappingURL=docs-cache.BWEcxcrg.mjs.map
@@ -1,5 +1,5 @@
1
- import{readFile as o,writeFile as f,access as P}from"node:fs/promises";import a from"node:path";import{t as p}from"./docs-cache.D9_kM5zq.mjs";const g=async n=>{try{return await P(n),!0}catch{return!1}},l=n=>{const r=n.trim();if(!r||r.startsWith("#")||r.startsWith("!"))return"";let t=r.replace(/^\//,"");return t=t.replace(/^\.\//,""),t=t.replace(/\/+$/,""),p(t)},d=(n,r)=>{const t=a.isAbsolute(r)?a.resolve(r):a.resolve(n,r),e=a.relative(n,t);return e===".."||e.startsWith(`..${a.sep}`)||a.isAbsolute(e)?null:e.length===0?".":e},u=async(n,r)=>{const t=a.resolve(n,".gitignore"),e=d(n,r);if(!e)return{gitignorePath:t,entry:null,hasEntry:!1};const i=l(e);if(!i)return{gitignorePath:t,entry:null,hasEntry:!1};let s="";await g(t)&&(s=await o(t,"utf8"));const h=s.split(/\r?\n/),c=new Set(h.map(y=>l(y)).filter(Boolean));return{gitignorePath:t,entry:`${i}/`,hasEntry:c.has(i)}},w=async(n,r)=>{const t=await u(n,r);if(!t.entry)return{updated:!1,gitignorePath:t.gitignorePath,entry:null};if(t.hasEntry)return{updated:!1,gitignorePath:t.gitignorePath,entry:t.entry};let e="";await g(t.gitignorePath)&&(e=await o(t.gitignorePath,"utf8"));const i=e.length===0||e.endsWith(`
1
+ import{readFile as o,writeFile as f,access as P}from"node:fs/promises";import a from"node:path";import{t as p}from"./docs-cache.DDgb7yxQ.mjs";const g=async n=>{try{return await P(n),!0}catch{return!1}},l=n=>{const r=n.trim();if(!r||r.startsWith("#")||r.startsWith("!"))return"";let t=r.replace(/^\//,"");return t=t.replace(/^\.\//,""),t=t.replace(/\/+$/,""),p(t)},d=(n,r)=>{const t=a.isAbsolute(r)?a.resolve(r):a.resolve(n,r),e=a.relative(n,t);return e===".."||e.startsWith(`..${a.sep}`)||a.isAbsolute(e)?null:e.length===0?".":e},u=async(n,r)=>{const t=a.resolve(n,".gitignore"),e=d(n,r);if(!e)return{gitignorePath:t,entry:null,hasEntry:!1};const i=l(e);if(!i)return{gitignorePath:t,entry:null,hasEntry:!1};let s="";await g(t)&&(s=await o(t,"utf8"));const h=s.split(/\r?\n/),c=new Set(h.map(y=>l(y)).filter(Boolean));return{gitignorePath:t,entry:`${i}/`,hasEntry:c.has(i)}},w=async(n,r)=>{const t=await u(n,r);if(!t.entry)return{updated:!1,gitignorePath:t.gitignorePath,entry:null};if(t.hasEntry)return{updated:!1,gitignorePath:t.gitignorePath,entry:t.entry};let e="";await g(t.gitignorePath)&&(e=await o(t.gitignorePath,"utf8"));const i=e.length===0||e.endsWith(`
2
2
  `)?"":`
3
3
  `,s=`${e}${i}${t.entry}
4
4
  `;return await f(t.gitignorePath,s,"utf8"),{updated:!0,gitignorePath:t.gitignorePath,entry:t.entry}};export{w as e,u as g};
5
- //# sourceMappingURL=docs-cache.DgrUGcWv.mjs.map
5
+ //# sourceMappingURL=docs-cache.C96VehGu.mjs.map
@@ -0,0 +1,6 @@
1
+ import p from"node:process";import d from"cac";import i from"node:path";import s from"picocolors";var a=(o=>(o[o.Success=0]="Success",o[o.FatalError=1]="FatalError",o[o.InvalidArgument=9]="InvalidArgument",o))(a||{});const m=["add","remove","sync","status","clean","clean-cache","prune","verify","init"],h=(o=p.argv)=>{try{const r=d("docs-cache");r.option("--source <repo>","Source repo (add only)").option("--target <dir>","Target directory for source (add only)").option("--config <path>","Path to config file").option("--cache-dir <path>","Override cache directory").option("--offline","Disable network access").option("--fail-on-miss","Fail when required sources are missing").option("--lock-only","Update lock without materializing files").option("--prune","Prune cache on remove").option("--target-dir <path>","Target directory for add").option("--concurrency <n>","Concurrency limit").option("--json","Output JSON").option("--timeout-ms <n>","Network timeout in milliseconds").option("--silent","Suppress non-error output").help();const e=r.parse(o,{run:!1}),t=e.args[0];if(t&&!m.includes(t))throw new Error(`Unknown command '${t}'.`);const n={config:e.options.config,cacheDir:e.options.cacheDir,offline:!!e.options.offline,failOnMiss:!!e.options.failOnMiss,lockOnly:!!e.options.lockOnly,prune:!!e.options.prune,targetDir:e.options.targetDir,concurrency:e.options.concurrency?Number(e.options.concurrency):void 0,json:!!e.options.json,timeoutMs:e.options.timeoutMs?Number(e.options.timeoutMs):void 0,silent:!!e.options.silent};if(n.concurrency!==void 0&&n.concurrency<1)throw new Error("--concurrency must be a positive number.");if(n.timeoutMs!==void 0&&n.timeoutMs<1)throw new Error("--timeout-ms must be a positive number.");const l=o.slice(2);return{command:t??null,options:n,positionals:e.args.slice(1),rawArgs:l,help:!!e.options.help}}catch(r){const e=r instanceof Error?r.message:String(r);console.error(e),p.exit(a.InvalidArgument)}},f="TOC.md",u=o=>o.replace(/\\/g,"/"),g=(o,r)=>{const e=i.dirname(i.resolve(o)),t=i.resolve(e,r),n=i.relative(e,t);if(n===".."||n.startsWith(`..${i.sep}`)||i.isAbsolute(n))throw new Error(`targetDir '${r}' escapes project directory. Must be within ${e}.`);if(u(n).split("/").filter(Boolean).includes(".git"))throw new Error("targetDir cannot be within .git directory.");return t},y=(o,r,e)=>{if(e)return i.resolve(e);const t=i.dirname(o);return i.resolve(t,r)},w=(o,r)=>{i.join(o,"repos");const e=i.join(o,r);return{cacheDir:o,sourceDir:e}},v={error:s.red("\u2716"),success:s.green("\u2714"),info:s.blue("\u2139"),warn:s.yellow("\u26A0")};let c=!1;const b=o=>{c=o},E={path:o=>{const r=i.relative(process.cwd(),o),e=r.length<o.length?r:o;return u(e)},hash:o=>o?o.slice(0,7):"-",pad:(o,r)=>o.padEnd(r),line:(o="")=>{c||process.stdout.write(`${o}
2
+ `)},header:(o,r)=>{c||process.stdout.write(`${s.blue("\u2139")} ${o.padEnd(10)} ${r}
3
+ `)},item:(o,r,e)=>{if(c)return;const t=s.bold(r),n=e?s.gray(e):"";process.stdout.write(` ${o} ${t} ${n}
4
+ `)},step:(o,r,e)=>{if(c)return;const t=s.cyan("\u2192");process.stdout.write(` ${t} ${o} ${s.bold(r)}${e?` ${s.dim(e)}`:""}
5
+ `)}};export{f as D,a as E,v as a,y as b,w as g,h as p,g as r,b as s,u as t,E as u};
6
+ //# sourceMappingURL=docs-cache.DDgb7yxQ.mjs.map
@@ -0,0 +1,2 @@
1
+ import{access as s}from"node:fs/promises";import{homedir as a}from"node:os";import e from"node:path";const c=()=>{const r=a();switch(process.platform){case"darwin":return e.join(r,"Library","Caches");case"win32":return process.env.LOCALAPPDATA||e.join(r,"AppData","Local");default:return process.env.XDG_CACHE_HOME||e.join(r,".cache")}},t=()=>process.env.DOCS_CACHE_GIT_DIR||e.join(c(),"docs-cache-git"),o=async r=>{try{return await s(r),!0}catch{return!1}};export{o as e,t as r};
2
+ //# sourceMappingURL=docs-cache.kK1DPQIQ.mjs.map
package/package.json CHANGED
@@ -1,94 +1,91 @@
1
1
  {
2
- "name": "docs-cache",
3
- "private": false,
4
- "type": "module",
5
- "version": "0.1.4",
6
- "packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748",
7
- "description": "CLI for deterministic local caching of external documentation for agents and tools",
8
- "author": "Frederik Bosch",
9
- "license": "MIT",
10
- "homepage": "https://github.com/fbosch/docs-cache#readme",
11
- "repository": {
12
- "type": "git",
13
- "url": "https://github.com/fbosch/docs-cache.git"
14
- },
15
- "bugs": {
16
- "url": "https://github.com/fbosch/docs-cache/issues"
17
- },
18
- "keywords": [
19
- "docs",
20
- "documentation",
21
- "cache",
22
- "agent",
23
- "ai",
24
- "git",
25
- "cli"
26
- ],
27
- "sideEffects": false,
28
- "engines": {
29
- "node": ">=18"
30
- },
31
- "bin": {
32
- "docs-cache": "./bin/docs-cache.mjs"
33
- },
34
- "files": [
35
- "bin",
36
- "dist/cli.mjs",
37
- "dist/chunks/*.mjs",
38
- "dist/lock.mjs",
39
- "dist/shared/*.mjs",
40
- "README.md",
41
- "LICENSE"
42
- ],
43
- "scripts": {
44
- "build": "unbuild",
45
- "dev": "unbuild --stub",
46
- "lint": "biome check .",
47
- "prepublishOnly": "pnpm audit --audit-level=high && pnpm build && pnpm size && pnpm schema:build",
48
- "release": "pnpm run lint && pnpm run typecheck && bumpp && pnpm publish --access public",
49
- "test": "pnpm build && node --test",
50
- "test:coverage": "pnpm build && c8 --include dist --exclude bin --reporter=text node --test",
51
- "bench": "pnpm build && node scripts/benchmarks/run.mjs",
52
- "schema:build": "node scripts/generate-schema.mjs",
53
- "size": "size-limit",
54
- "test:watch": "node --test --watch",
55
- "typecheck": "tsc --noEmit",
56
- "prepare": "simple-git-hooks"
57
- },
58
- "dependencies": {
59
- "@clack/prompts": "^1.0.0",
60
- "cac": "^6.7.14",
61
- "fast-glob": "^3.3.2",
62
- "picocolors": "^1.1.1",
63
- "picomatch": "^2.3.1",
64
- "zod": "^4.3.6"
65
- },
66
- "devDependencies": {
67
- "@biomejs/biome": "^2.3.8",
68
- "@size-limit/file": "^11.2.0",
69
- "@types/node": "^24.2.1",
70
- "bumpp": "^10.3.2",
71
- "c8": "^10.1.3",
72
- "jiti": "^2.5.1",
73
- "lint-staged": "^16.2.7",
74
- "simple-git-hooks": "^2.13.1",
75
- "size-limit": "^11.2.0",
76
- "tinybench": "^6.0.0",
77
- "typescript": "^5.9.3",
78
- "unbuild": "^3.6.1"
79
- },
80
- "size-limit": [
81
- {
82
- "path": "dist/cli.mjs",
83
- "limit": "10 kB"
84
- }
85
- ],
86
- "simple-git-hooks": {
87
- "pre-commit": "pnpm lint-staged"
88
- },
89
- "lint-staged": {
90
- "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": [
91
- "biome check --write --no-errors-on-unmatched"
92
- ]
93
- }
94
- }
2
+ "name": "docs-cache",
3
+ "private": false,
4
+ "type": "module",
5
+ "version": "0.3.0",
6
+ "description": "CLI for deterministic local caching of external documentation for agents and tools",
7
+ "author": "Frederik Bosch",
8
+ "license": "MIT",
9
+ "homepage": "https://github.com/fbosch/docs-cache#readme",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/fbosch/docs-cache.git"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/fbosch/docs-cache/issues"
16
+ },
17
+ "keywords": [
18
+ "docs",
19
+ "documentation",
20
+ "cache",
21
+ "agent",
22
+ "ai",
23
+ "git",
24
+ "cli"
25
+ ],
26
+ "sideEffects": false,
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "bin": {
31
+ "docs-cache": "./bin/docs-cache.mjs"
32
+ },
33
+ "files": [
34
+ "bin",
35
+ "dist/cli.mjs",
36
+ "dist/chunks/*.mjs",
37
+ "dist/lock.mjs",
38
+ "dist/shared/*.mjs",
39
+ "README.md",
40
+ "LICENSE"
41
+ ],
42
+ "dependencies": {
43
+ "@clack/prompts": "^1.0.0",
44
+ "cac": "^6.7.14",
45
+ "fast-glob": "^3.3.2",
46
+ "picocolors": "^1.1.1",
47
+ "picomatch": "^2.3.1",
48
+ "zod": "^4.3.6"
49
+ },
50
+ "devDependencies": {
51
+ "@biomejs/biome": "^2.3.8",
52
+ "@size-limit/file": "^11.2.0",
53
+ "@types/node": "^24.2.1",
54
+ "bumpp": "^10.3.2",
55
+ "c8": "^10.1.3",
56
+ "jiti": "^2.5.1",
57
+ "lint-staged": "^16.2.7",
58
+ "simple-git-hooks": "^2.13.1",
59
+ "size-limit": "^11.2.0",
60
+ "tinybench": "^6.0.0",
61
+ "typescript": "^5.9.3",
62
+ "unbuild": "^3.6.1"
63
+ },
64
+ "size-limit": [
65
+ {
66
+ "path": "dist/cli.mjs",
67
+ "limit": "10 kB"
68
+ }
69
+ ],
70
+ "simple-git-hooks": {
71
+ "pre-commit": "pnpm lint-staged"
72
+ },
73
+ "lint-staged": {
74
+ "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": [
75
+ "biome check --write --no-errors-on-unmatched"
76
+ ]
77
+ },
78
+ "scripts": {
79
+ "build": "unbuild",
80
+ "dev": "unbuild --stub",
81
+ "lint": "biome check .",
82
+ "release": "pnpm run lint && pnpm run typecheck && bumpp && pnpm publish --access public",
83
+ "test": "pnpm build && node --test",
84
+ "test:coverage": "pnpm build && c8 --include dist --exclude bin --reporter=text node --test",
85
+ "bench": "pnpm build && node scripts/benchmarks/run.mjs",
86
+ "schema:build": "node scripts/generate-schema.mjs",
87
+ "size": "size-limit",
88
+ "test:watch": "node --test --watch",
89
+ "typecheck": "tsc --noEmit"
90
+ }
91
+ }
@@ -1,3 +0,0 @@
1
- import{writeFile as I,readFile as z,access as T}from"node:fs/promises";import h from"node:path";import{z as o}from"zod";import{r as L}from"./docs-cache.D9_kM5zq.mjs";const $=o.enum(["symlink","copy"]),F=o.enum(["materialize"]),U=o.object({type:o.enum(["commit","manifest"]),value:o.string().nullable()}).strict(),_=o.object({ref:o.string().min(1),mode:F,include:o.array(o.string().min(1)).min(1),targetMode:$.optional(),depth:o.number().min(1),required:o.boolean(),maxBytes:o.number().min(1),maxFiles:o.number().min(1).optional(),allowHosts:o.array(o.string().min(1)).min(1)}).strict(),P=o.object({id:o.string().min(1),repo:o.string().min(1),targetDir:o.string().min(1).optional(),targetMode:$.optional(),ref:o.string().min(1).optional(),mode:F.optional(),depth:o.number().min(1).optional(),include:o.array(o.string().min(1)).optional(),exclude:o.array(o.string().min(1)).optional(),required:o.boolean().optional(),maxBytes:o.number().min(1).optional(),maxFiles:o.number().min(1).optional(),integrity:U.optional()}).strict(),J=o.object({$schema:o.string().min(1).optional(),cacheDir:o.string().min(1).optional(),targetMode:$.optional(),index:o.boolean().optional(),defaults:_.partial().optional(),sources:o.array(P)}).strict(),R=/^[a-zA-Z0-9_-]+$/,G=new Set([".","..","CON","PRN","AUX","NUL","COM1","LPT1"]),A=(e,r)=>{if(typeof e!="string"||e.length===0)throw new Error(`${r} must be a non-empty string.`);if(e.length>200)throw new Error(`${r} exceeds maximum length of 200.`);if(!R.test(e))throw new Error(`${r} must contain only alphanumeric characters, hyphens, and underscores.`);if(G.has(e.toUpperCase()))throw new Error(`${r} uses reserved name '${e}'.`);return e},j="docs.config.json",b=".docs",S="package.json",V=process.platform==="win32"?"copy":"symlink",f={cacheDir:b,index:!1,defaults:{ref:"HEAD",mode:"materialize",include:["**/*.{md,mdx,markdown,mkd,txt,rst,adoc,asciidoc}"],targetMode:V,depth:1,required:!0,maxBytes:2e8,allowHosts:["github.com","gitlab.com"]},sources:[]},X=(e,r)=>!e||!r?e===r:e.length!==r.length?!1:e.every((t,n)=>t===r[n]),C=e=>typeof e=="object"&&e!==null&&!Array.isArray(e),k=(e,r)=>{const t={};for(const[n,s]of Object.entries(e)){const d=r[n];if(Array.isArray(s)&&Array.isArray(d)){X(s,d)||(t[n]=s);continue}if(C(s)&&C(d)){const a=k(s,d);Object.keys(a).length>0&&(t[n]=a);continue}s!==d&&(t[n]=s)}return t},Z=e=>{const r={...f,$schema:e.$schema,defaults:{...f.defaults,...e.targetMode?{targetMode:e.targetMode}:void 0}},t=k(e,r),n={$schema:t.$schema,cacheDir:t.cacheDir,index:t.index,targetMode:t.targetMode,defaults:t.defaults,sources:e.sources};return(!n.defaults||Object.keys(n.defaults).length===0)&&delete n.defaults,n},p=e=>typeof e=="object"&&e!==null&&!Array.isArray(e),m=(e,r)=>{if(typeof e!="string"||e.length===0)throw new Error(`${r} must be a non-empty string.`);return e},v=(e,r)=>{if(typeof e!="boolean")throw new Error(`${r} must be a boolean.`);return e},K=(e,r)=>{if(typeof e!="number"||Number.isNaN(e))throw new Error(`${r} must be a number.`);return e},g=(e,r)=>{const t=K(e,r);if(t<1)throw new Error(`${r} must be greater than zero.`);return t},y=(e,r)=>{if(!Array.isArray(e)||e.length===0)throw new Error(`${r} must be a non-empty array of strings.`);for(const t of e)if(typeof t!="string"||t.length===0)throw new Error(`${r} must contain non-empty strings.`);return e},q=(e,r)=>{const t=m(e,r);if(t!=="symlink"&&t!=="copy")throw new Error(`${r} must be "symlink" or "copy".`);return t},B=(e,r)=>{if(e!=="materialize")throw new Error(`${r} must be "materialize".`);return e},Q=(e,r)=>{if(!p(e))throw new Error(`${r} must be an object.`);const t=e.type;if(t!=="commit"&&t!=="manifest")throw new Error(`${r}.type must be "commit" or "manifest".`);const n=e.value;if(typeof n!="string"&&n!==null)throw new Error(`${r}.value must be a string or null.`);return{type:t,value:n}},N=e=>{if(!p(e))throw new Error("Config must be a JSON object.");const r=J.safeParse(e);if(!r.success){const i=r.error.issues.map(c=>`${c.path.join(".")||"config"} ${c.message}`).join("; ");throw new Error(`Config does not match schema: ${i}.`)}const t=e.cacheDir?m(e.cacheDir,"cacheDir"):b,n=e.index!==void 0?v(e.index,"index"):f.index??!1,s=e.defaults,d=e.targetMode!==void 0?q(e.targetMode,"targetMode"):void 0,a=f.defaults;let l=a;if(s!==void 0){if(!p(s))throw new Error("defaults must be an object.");l={ref:s.ref!==void 0?m(s.ref,"defaults.ref"):a.ref,mode:s.mode!==void 0?B(s.mode,"defaults.mode"):a.mode,include:s.include!==void 0?y(s.include,"defaults.include"):a.include,targetMode:s.targetMode!==void 0?q(s.targetMode,"defaults.targetMode"):d??a.targetMode,depth:s.depth!==void 0?g(s.depth,"defaults.depth"):a.depth,required:s.required!==void 0?v(s.required,"defaults.required"):a.required,maxBytes:s.maxBytes!==void 0?g(s.maxBytes,"defaults.maxBytes"):a.maxBytes,maxFiles:s.maxFiles!==void 0?g(s.maxFiles,"defaults.maxFiles"):a.maxFiles,allowHosts:s.allowHosts!==void 0?y(s.allowHosts,"defaults.allowHosts"):a.allowHosts}}else d!==void 0&&(l={...a,targetMode:d});if(!Array.isArray(e.sources))throw new Error("sources must be an array.");const M=e.sources.map((i,c)=>{if(!p(i))throw new Error(`sources[${c}] must be an object.`);const u={id:A(i.id,`sources[${c}].id`),repo:m(i.repo,`sources[${c}].repo`)};if(i.targetDir!==void 0&&(u.targetDir=m(i.targetDir,`sources[${c}].targetDir`)),i.targetMode!==void 0){const x=m(i.targetMode,`sources[${c}].targetMode`);if(x!=="symlink"&&x!=="copy")throw new Error(`sources[${c}].targetMode must be "symlink" or "copy".`);u.targetMode=x}return i.ref!==void 0&&(u.ref=m(i.ref,`sources[${c}].ref`)),i.mode!==void 0&&(u.mode=B(i.mode,`sources[${c}].mode`)),i.depth!==void 0&&(u.depth=g(i.depth,`sources[${c}].depth`)),i.include!==void 0&&(u.include=y(i.include,`sources[${c}].include`)),i.exclude!==void 0&&(u.exclude=y(i.exclude,`sources[${c}].exclude`)),i.required!==void 0&&(u.required=v(i.required,`sources[${c}].required`)),i.maxBytes!==void 0&&(u.maxBytes=g(i.maxBytes,`sources[${c}].maxBytes`)),i.maxFiles!==void 0&&(u.maxFiles=g(i.maxFiles,`sources[${c}].maxFiles`)),i.integrity!==void 0&&(u.integrity=Q(i.integrity,`sources[${c}].integrity`)),u}),D=new Set,w=[];for(const i of M)D.has(i.id)&&w.push(i.id),D.add(i.id);if(w.length>0)throw new Error(`Duplicate source IDs found: ${w.join(", ")}. Each source must have a unique ID.`);return{cacheDir:t,targetMode:d,index:n,defaults:l,sources:M}},W=e=>{const r=e.defaults??f.defaults;return e.sources.map(t=>({id:t.id,repo:t.repo,targetDir:t.targetDir,targetMode:t.targetMode??r.targetMode,ref:t.ref??r.ref,mode:t.mode??r.mode,depth:t.depth??r.depth,include:t.include??r.include,exclude:t.exclude,required:t.required??r.required,maxBytes:t.maxBytes??r.maxBytes,maxFiles:t.maxFiles??r.maxFiles,integrity:t.integrity}))},O=e=>e?h.resolve(e):h.resolve(process.cwd(),j),Y=()=>h.resolve(process.cwd(),S),H=async e=>{try{return await T(e),!0}catch{return!1}},E=async(e,r)=>{let t;try{t=await z(e,"utf8")}catch(a){const l=a instanceof Error?a.message:String(a);throw new Error(`Failed to read config at ${e}: ${l}`)}let n;try{n=JSON.parse(t)}catch(a){const l=a instanceof Error?a.message:String(a);throw new Error(`Invalid JSON in ${e}: ${l}`)}const s=r==="package"?n?.["docs-cache"]:n;if(r==="package"&&s===void 0)throw new Error(`Missing docs-cache config in ${e}.`);const d=N(s);for(const a of d.sources)a.targetDir&&L(e,a.targetDir);return{config:d,resolvedPath:e,sources:W(d)}},ee=async(e,r)=>{const t=`${JSON.stringify(r,null,2)}
2
- `;await I(e,t,"utf8")},re=async e=>{const r=O(e),t=h.basename(r)===S;if(e)return E(r,t?"package":"config");if(await H(r))return E(r,"config");const n=Y();if(await H(n))try{return await E(n,"package")}catch{}throw new Error(`No docs.config.json found at ${r} and no docs-cache config in ${n}.`)};export{f as D,A as a,b,j as c,re as l,O as r,Z as s,N as v,ee as w};
3
- //# sourceMappingURL=docs-cache.D4Fth4X8.mjs.map
@@ -1,6 +0,0 @@
1
- import u from"node:process";import d from"cac";import n from"node:path";import s from"picocolors";var c=(o=>(o[o.Success=0]="Success",o[o.FatalError=1]="FatalError",o[o.InvalidArgument=9]="InvalidArgument",o))(c||{});const m=["add","remove","sync","status","clean","prune","verify","init"],h=(o=u.argv)=>{try{const r=d("docs-cache");r.option("--source <repo>","Source repo (add only)").option("--target <dir>","Target directory for source (add only)").option("--config <path>","Path to config file").option("--cache-dir <path>","Override cache directory").option("--offline","Disable network access").option("--fail-on-miss","Fail when required sources are missing").option("--lock-only","Update lock without materializing files").option("--prune","Prune cache on remove").option("--target-dir <path>","Target directory for add").option("--concurrency <n>","Concurrency limit").option("--json","Output JSON").option("--timeout-ms <n>","Network timeout in milliseconds").help();const e=r.parse(o,{run:!1}),t=e.args[0];if(t&&!m.includes(t))throw new Error(`Unknown command '${t}'.`);const i={config:e.options.config,cacheDir:e.options.cacheDir,offline:!!e.options.offline,failOnMiss:!!e.options.failOnMiss,lockOnly:!!e.options.lockOnly,prune:!!e.options.prune,targetDir:e.options.targetDir,concurrency:e.options.concurrency?Number(e.options.concurrency):void 0,json:!!e.options.json,timeoutMs:e.options.timeoutMs?Number(e.options.timeoutMs):void 0};if(i.concurrency!==void 0&&i.concurrency<1)throw new Error("--concurrency must be a positive number.");if(i.timeoutMs!==void 0&&i.timeoutMs<1)throw new Error("--timeout-ms must be a positive number.");const l=o.slice(2);return{command:t??null,options:i,positionals:e.args.slice(1),rawArgs:l,help:!!e.options.help}}catch(r){const e=r instanceof Error?r.message:String(r);console.error(e),u.exit(c.InvalidArgument)}},p="index.json",a=o=>o.replace(/\\/g,"/"),g=(o,r)=>{const e=n.dirname(n.resolve(o)),t=n.resolve(e,r),i=n.relative(e,t);if(i===".."||i.startsWith(`..${n.sep}`)||n.isAbsolute(i))throw new Error(`targetDir '${r}' escapes project directory. Must be within ${e}.`);if(a(i).split("/").filter(Boolean).includes(".git"))throw new Error("targetDir cannot be within .git directory.");return t},f=(o,r,e)=>{if(e)return n.resolve(e);const t=n.dirname(o);return n.resolve(t,r)},y=(o,r)=>{n.join(o,"repos");const e=n.join(o,r),t=n.join(o,p);return{cacheDir:o,sourceDir:e,indexPath:t}},w={error:s.red("\u2716"),success:s.green("\u2714"),info:s.blue("\u2139"),warn:s.yellow("\u26A0")},v={path:o=>{const r=n.relative(process.cwd(),o),e=r.length<o.length?r:o;return a(e)},hash:o=>o?o.slice(0,7):"-",pad:(o,r)=>o.padEnd(r),line:(o="")=>process.stdout.write(`${o}
2
- `),header:(o,r)=>{process.stdout.write(`${s.blue("\u2139")} ${o.padEnd(10)} ${r}
3
- `)},item:(o,r,e)=>{const t=s.bold(r),i=e?s.gray(e):"";process.stdout.write(` ${o} ${t} ${i}
4
- `)},step:(o,r,e)=>{const t=s.cyan("\u2192");process.stdout.write(` ${t} ${o} ${s.bold(r)}${e?` ${s.dim(e)}`:""}
5
- `)}};export{p as D,c as E,f as a,y as g,h as p,g as r,w as s,a as t,v as u};
6
- //# sourceMappingURL=docs-cache.D9_kM5zq.mjs.map