docs-cache 0.5.2 → 0.5.4

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
@@ -102,7 +102,7 @@ These fields can be set in `defaults` and are inherited by every source unless o
102
102
  | `ignoreHidden` | Skip hidden files and directories (dotfiles). Default: `false`. |
103
103
  | `allowHosts` | Allowed Git hosts. Default: `["github.com", "gitlab.com", "visualstudio.com"]`. |
104
104
  | `toc` | Generate per-source `TOC.md`. Default: `true`. Supports `true`, `false`, or a format: `"tree"` (human readable), `"compressed"` |
105
- | `unwrapSingleRootDir` | If the materialized output is nested under a single directory, unwrap it (recursively). Default: `false`. |
105
+ | `unwrapSingleRootDir` | If the materialized output is nested under a single directory, unwrap it (recursively). Default: `true`. |
106
106
 
107
107
  ### Source options
108
108
 
@@ -121,7 +121,7 @@ const resolveMaterializeParams = (params) => ({
121
121
  ...params,
122
122
  exclude: params.exclude ?? [],
123
123
  ignoreHidden: params.ignoreHidden ?? false,
124
- unwrapSingleRootDir: params.unwrapSingleRootDir ?? false,
124
+ unwrapSingleRootDir: params.unwrapSingleRootDir ?? true,
125
125
  json: params.json ?? false,
126
126
  progressThrottleMs: params.progressThrottleMs ?? 120
127
127
  });
@@ -19,7 +19,7 @@ export const DEFAULT_CONFIG = {
19
19
  ignoreHidden: false,
20
20
  allowHosts: ["github.com", "gitlab.com", "visualstudio.com"],
21
21
  toc: true,
22
- unwrapSingleRootDir: false
22
+ unwrapSingleRootDir: true
23
23
  },
24
24
  sources: []
25
25
  };
@@ -177,35 +177,54 @@ const ensureCommitAvailable = async (repoPath, commit, options) => {
177
177
  logger: options?.logger
178
178
  });
179
179
  };
180
- const isSparseEligible = (include) => {
181
- if (!include || include.length === 0) {
182
- return false;
180
+ const patternHasGlob = (pattern) => pattern.includes("*") || pattern.includes("?") || pattern.includes("[");
181
+ const normalizeSparsePatterns = (include) => (include ?? []).map((pattern) => pattern.replace(/\\/g, "/")).filter(Boolean);
182
+ const isDirectoryLiteral = (pattern) => pattern.endsWith("/");
183
+ const toNoConePattern = (pattern) => {
184
+ if (!patternHasGlob(pattern) && isDirectoryLiteral(pattern)) {
185
+ return pattern.endsWith("/") ? pattern : `${pattern}/`;
186
+ }
187
+ return pattern;
188
+ };
189
+ const resolveSparseSpec = (include) => {
190
+ const normalized = normalizeSparsePatterns(include);
191
+ if (normalized.length === 0) {
192
+ return { enabled: false, mode: "cone", patterns: [] };
183
193
  }
184
- for (const pattern of include) {
185
- if (!pattern || pattern.includes("**")) {
186
- return false;
194
+ const conePaths = [];
195
+ let coneEligible = true;
196
+ for (const pattern of normalized) {
197
+ if (pattern.includes("**")) {
198
+ coneEligible = false;
199
+ break;
187
200
  }
201
+ if (patternHasGlob(pattern)) {
202
+ coneEligible = false;
203
+ break;
204
+ }
205
+ if (isDirectoryLiteral(pattern)) {
206
+ conePaths.push(pattern.replace(/\/+$/, ""));
207
+ continue;
208
+ }
209
+ coneEligible = false;
210
+ break;
188
211
  }
189
- return true;
190
- };
191
- const extractSparsePaths = (include) => {
192
- if (!include) {
193
- return [];
212
+ const uniquePaths = Array.from(new Set(conePaths.filter(Boolean)));
213
+ if (coneEligible && uniquePaths.length > 0) {
214
+ return { enabled: true, mode: "cone", patterns: uniquePaths };
194
215
  }
195
- const paths = include.map((pattern) => {
196
- const normalized = pattern.replace(/\\/g, "/");
197
- const starIndex = normalized.indexOf("*");
198
- const base = starIndex === -1 ? normalized : normalized.slice(0, starIndex);
199
- return base.replace(/\/+$|\/$/, "");
200
- });
201
- return Array.from(new Set(paths.filter((value) => value.length > 0)));
216
+ return {
217
+ enabled: true,
218
+ mode: "no-cone",
219
+ patterns: normalized.map(toNoConePattern)
220
+ };
202
221
  };
203
222
  const cloneRepo = async (params, outDir) => {
204
223
  if (params.offline) {
205
224
  throw new Error(`Cannot clone ${params.repo} while offline.`);
206
225
  }
207
226
  const isCommitRef = /^[0-9a-f]{7,40}$/i.test(params.ref);
208
- const useSparse = isSparseEligible(params.include);
227
+ const sparseSpec = resolveSparseSpec(params.include);
209
228
  const buildCloneArgs = () => {
210
229
  const cloneArgs2 = [
211
230
  "clone",
@@ -218,7 +237,7 @@ const cloneRepo = async (params, outDir) => {
218
237
  return cloneArgs2;
219
238
  };
220
239
  const cloneArgs = buildCloneArgs();
221
- if (useSparse) {
240
+ if (sparseSpec.enabled) {
222
241
  cloneArgs.push("--sparse");
223
242
  }
224
243
  if (!isCommitRef) {
@@ -239,14 +258,16 @@ const cloneRepo = async (params, outDir) => {
239
258
  logger: params.logger,
240
259
  offline: params.offline
241
260
  });
242
- if (useSparse) {
243
- const sparsePaths = extractSparsePaths(params.include);
244
- if (sparsePaths.length > 0) {
245
- await git(["-C", outDir, "sparse-checkout", "set", ...sparsePaths], {
246
- timeoutMs: params.timeoutMs,
247
- logger: params.logger
248
- });
261
+ if (sparseSpec.enabled) {
262
+ const sparseArgs = ["-C", outDir, "sparse-checkout", "set"];
263
+ if (sparseSpec.mode === "no-cone") {
264
+ sparseArgs.push("--no-cone");
249
265
  }
266
+ sparseArgs.push(...sparseSpec.patterns);
267
+ await git(sparseArgs, {
268
+ timeoutMs: params.timeoutMs,
269
+ logger: params.logger
270
+ });
250
271
  }
251
272
  await git(
252
273
  ["-C", outDir, "checkout", "--quiet", "--detach", params.resolvedCommit],
@@ -281,9 +302,14 @@ const addWorktreeFromCache = async (params, cachePath, outDir) => {
281
302
  allowFileProtocol: true
282
303
  }
283
304
  );
284
- const sparsePaths = isSparseEligible(params.include) ? extractSparsePaths(params.include) : [];
285
- if (sparsePaths.length > 0) {
286
- await git(["-C", outDir, "sparse-checkout", "set", ...sparsePaths], {
305
+ const sparseSpec = resolveSparseSpec(params.include);
306
+ if (sparseSpec.enabled) {
307
+ const sparseArgs = ["-C", outDir, "sparse-checkout", "set"];
308
+ if (sparseSpec.mode === "no-cone") {
309
+ sparseArgs.push("--no-cone");
310
+ }
311
+ sparseArgs.push(...sparseSpec.patterns);
312
+ await git(sparseArgs, {
287
313
  timeoutMs: params.timeoutMs,
288
314
  logger: params.logger,
289
315
  allowFileProtocol: true
@@ -380,7 +406,7 @@ const cloneOrUpdateRepo = async (params, outDir) => {
380
406
  const cacheExists = await exists(cachePath);
381
407
  const cacheValid = cacheExists && await isValidGitRepo(cachePath);
382
408
  const isCommitRef = /^[0-9a-f]{7,40}$/i.test(params.ref);
383
- const useSparse = isSparseEligible(params.include);
409
+ const sparseSpec = resolveSparseSpec(params.include);
384
410
  let usedCache = cacheValid;
385
411
  let worktreeUsed = false;
386
412
  const cacheRoot = resolveGitCacheDir();
@@ -410,7 +436,7 @@ const cloneOrUpdateRepo = async (params, outDir) => {
410
436
  if (await isPartialClone(cachePath)) {
411
437
  localCloneArgs.splice(2, 0, "--filter=blob:none");
412
438
  }
413
- if (useSparse) {
439
+ if (sparseSpec.enabled) {
414
440
  localCloneArgs.push("--sparse");
415
441
  }
416
442
  if (!isCommitRef) {
@@ -428,15 +454,17 @@ const cloneOrUpdateRepo = async (params, outDir) => {
428
454
  progressLogger: params.progressLogger,
429
455
  forceProgress: Boolean(params.progressLogger)
430
456
  });
431
- if (useSparse) {
432
- const sparsePaths = extractSparsePaths(params.include);
433
- if (sparsePaths.length > 0) {
434
- await git(["-C", outDir, "sparse-checkout", "set", ...sparsePaths], {
435
- timeoutMs: params.timeoutMs,
436
- allowFileProtocol: true,
437
- logger: params.logger
438
- });
457
+ if (sparseSpec.enabled) {
458
+ const sparseArgs = ["-C", outDir, "sparse-checkout", "set"];
459
+ if (sparseSpec.mode === "no-cone") {
460
+ sparseArgs.push("--no-cone");
439
461
  }
462
+ sparseArgs.push(...sparseSpec.patterns);
463
+ await git(sparseArgs, {
464
+ timeoutMs: params.timeoutMs,
465
+ allowFileProtocol: true,
466
+ logger: params.logger
467
+ });
440
468
  }
441
469
  await ensureCommitAvailable(outDir, params.resolvedCommit, {
442
470
  timeoutMs: params.timeoutMs,
@@ -1,4 +1,5 @@
1
- const SAFE_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
1
+ const INVALID_ID_PATTERN = /[<>:"/\\|?*]/;
2
+ const TRAILING_DOT_SPACE_PATTERN = /[.\s]+$/;
2
3
  const MAX_ID_LENGTH = 200;
3
4
  const RESERVED_NAMES = /* @__PURE__ */ new Set([
4
5
  ".",
@@ -14,15 +15,28 @@ export const assertSafeSourceId = (value, label) => {
14
15
  if (typeof value !== "string" || value.length === 0) {
15
16
  throw new Error(`${label} must be a non-empty string.`);
16
17
  }
18
+ if (value.trim().length === 0) {
19
+ throw new Error(`${label} must not be blank.`);
20
+ }
17
21
  if (value.length > MAX_ID_LENGTH) {
18
22
  throw new Error(`${label} exceeds maximum length of ${MAX_ID_LENGTH}.`);
19
23
  }
20
- if (!SAFE_ID_PATTERN.test(value)) {
24
+ for (const char of value) {
25
+ const code = char.codePointAt(0);
26
+ if (code !== void 0 && (code <= 31 || code === 127)) {
27
+ throw new Error(`${label} must not contain control characters.`);
28
+ }
29
+ }
30
+ if (TRAILING_DOT_SPACE_PATTERN.test(value)) {
31
+ throw new Error(`${label} must not end with dots or spaces.`);
32
+ }
33
+ if (INVALID_ID_PATTERN.test(value) || value.includes("\0")) {
21
34
  throw new Error(
22
- `${label} must contain only alphanumeric characters, hyphens, and underscores.`
35
+ `${label} must not contain path separators or reserved characters (< > : " / \\ | ? *).`
23
36
  );
24
37
  }
25
- if (RESERVED_NAMES.has(value.toUpperCase())) {
38
+ const normalized = value.replace(/[.\s]+$/g, "");
39
+ if (RESERVED_NAMES.has(normalized.toUpperCase())) {
26
40
  throw new Error(`${label} uses reserved name '${value}'.`);
27
41
  }
28
42
  return value;
package/package.json CHANGED
@@ -1,136 +1,139 @@
1
1
  {
2
- "name": "docs-cache",
3
- "private": false,
4
- "type": "module",
5
- "version": "0.5.2",
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/esm/**/*.mjs",
37
- "dist/esm/**/*.d.ts",
38
- "dist/lock.mjs",
39
- "dist/shared/*.mjs",
40
- "README.md",
41
- "LICENSE"
42
- ],
43
- "imports": {
44
- "#cache/*": {
45
- "types": "./dist/esm/cache/*.d.ts",
46
- "default": "./dist/esm/cache/*.mjs"
47
- },
48
- "#cli/*": {
49
- "types": "./dist/esm/cli/*.d.ts",
50
- "default": "./dist/esm/cli/*.mjs"
51
- },
52
- "#commands/*": {
53
- "types": "./dist/esm/commands/*.d.ts",
54
- "default": "./dist/esm/commands/*.mjs"
55
- },
56
- "#core/*": {
57
- "types": "./dist/esm/*.d.ts",
58
- "default": "./dist/esm/*.mjs"
59
- },
60
- "#config": {
61
- "types": "./dist/esm/config/index.d.ts",
62
- "default": "./dist/esm/config/index.mjs"
63
- },
64
- "#config/*": {
65
- "types": "./dist/esm/config/*.d.ts",
66
- "default": "./dist/esm/config/*.mjs"
67
- },
68
- "#git/*": {
69
- "types": "./dist/esm/git/*.d.ts",
70
- "default": "./dist/esm/git/*.mjs"
71
- },
72
- "#types/*": {
73
- "types": "./dist/esm/types/*.d.ts",
74
- "default": "./dist/esm/types/*.mjs"
75
- }
76
- },
77
- "dependencies": {
78
- "@clack/prompts": "^1.0.0",
79
- "cac": "^6.7.14",
80
- "cli-truncate": "^4.0.0",
81
- "execa": "^9.6.1",
82
- "fast-glob": "^3.3.2",
83
- "log-update": "^7.0.2",
84
- "picocolors": "^1.1.1",
85
- "picomatch": "^4.0.3",
86
- "zod": "^4.3.6"
87
- },
88
- "devDependencies": {
89
- "@biomejs/biome": "^2.3.14",
90
- "@size-limit/file": "^12.0.0",
91
- "@types/node": "^25.2.0",
92
- "bumpp": "^10.3.2",
93
- "c8": "^10.1.3",
94
- "jiti": "^2.5.1",
95
- "lint-staged": "^16.2.7",
96
- "simple-git-hooks": "^2.13.1",
97
- "size-limit": "^12.0.0",
98
- "tinybench": "^6.0.0",
99
- "ts-complex": "^1.0.0",
100
- "typescript": "^5.9.3",
101
- "unbuild": "^3.6.1"
102
- },
103
- "size-limit": [
104
- {
105
- "path": "dist/cli.mjs",
106
- "limit": "10 kB"
107
- }
108
- ],
109
- "complexity": {
110
- "maxCyclomatic": 20,
111
- "minMaintainability": 60,
112
- "top": 10
113
- },
114
- "simple-git-hooks": {
115
- "pre-commit": "pnpm lint-staged && pnpm typecheck"
116
- },
117
- "lint-staged": {
118
- "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": [
119
- "biome check --write --no-errors-on-unmatched"
120
- ]
121
- },
122
- "scripts": {
123
- "build": "unbuild",
124
- "dev": "unbuild --stub",
125
- "lint": "biome check .",
126
- "release": "pnpm run lint && pnpm run typecheck && bumpp && pnpm publish --access public",
127
- "test": "pnpm build && node --test",
128
- "test:coverage": "pnpm build && c8 --include dist --exclude bin --reporter=text node --test",
129
- "bench": "pnpm build && node scripts/benchmarks/run.mjs",
130
- "complexity": "node scripts/complexity/run.mjs",
131
- "schema:build": "node scripts/generate-schema.mjs",
132
- "size": "size-limit",
133
- "test:watch": "node --test --watch",
134
- "typecheck": "tsc --noEmit"
135
- }
136
- }
2
+ "name": "docs-cache",
3
+ "private": false,
4
+ "type": "module",
5
+ "version": "0.5.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/esm/**/*.mjs",
38
+ "dist/esm/**/*.d.ts",
39
+ "dist/lock.mjs",
40
+ "dist/shared/*.mjs",
41
+ "README.md",
42
+ "LICENSE"
43
+ ],
44
+ "scripts": {
45
+ "build": "unbuild",
46
+ "dev": "unbuild --stub",
47
+ "lint": "biome check .",
48
+ "prepublishOnly": "pnpm audit --audit-level=high && pnpm build && pnpm size && pnpm schema:build",
49
+ "release": "pnpm run lint && pnpm run typecheck && bumpp && pnpm publish --access public",
50
+ "test": "pnpm build && node --test",
51
+ "test:coverage": "pnpm build && c8 --include dist --exclude bin --reporter=text node --test",
52
+ "bench": "pnpm build && node scripts/benchmarks/run.mjs",
53
+ "complexity": "node scripts/complexity/run.mjs",
54
+ "schema:build": "node scripts/generate-schema.mjs",
55
+ "size": "size-limit",
56
+ "test:watch": "node --test --watch",
57
+ "typecheck": "tsc --noEmit",
58
+ "prepare": "simple-git-hooks"
59
+ },
60
+ "imports": {
61
+ "#cache/*": {
62
+ "types": "./dist/esm/cache/*.d.ts",
63
+ "default": "./dist/esm/cache/*.mjs"
64
+ },
65
+ "#cli/*": {
66
+ "types": "./dist/esm/cli/*.d.ts",
67
+ "default": "./dist/esm/cli/*.mjs"
68
+ },
69
+ "#commands/*": {
70
+ "types": "./dist/esm/commands/*.d.ts",
71
+ "default": "./dist/esm/commands/*.mjs"
72
+ },
73
+ "#core/*": {
74
+ "types": "./dist/esm/*.d.ts",
75
+ "default": "./dist/esm/*.mjs"
76
+ },
77
+ "#config": {
78
+ "types": "./dist/esm/config/index.d.ts",
79
+ "default": "./dist/esm/config/index.mjs"
80
+ },
81
+ "#config/*": {
82
+ "types": "./dist/esm/config/*.d.ts",
83
+ "default": "./dist/esm/config/*.mjs"
84
+ },
85
+ "#git/*": {
86
+ "types": "./dist/esm/git/*.d.ts",
87
+ "default": "./dist/esm/git/*.mjs"
88
+ },
89
+ "#types/*": {
90
+ "types": "./dist/esm/types/*.d.ts",
91
+ "default": "./dist/esm/types/*.mjs"
92
+ }
93
+ },
94
+ "dependencies": {
95
+ "@clack/prompts": "^1.0.0",
96
+ "cac": "^6.7.14",
97
+ "cli-truncate": "^4.0.0",
98
+ "execa": "^9.6.1",
99
+ "fast-glob": "^3.3.2",
100
+ "log-update": "^7.0.2",
101
+ "picocolors": "^1.1.1",
102
+ "picomatch": "^4.0.3",
103
+ "zod": "^4.3.6"
104
+ },
105
+ "devDependencies": {
106
+ "@biomejs/biome": "^2.3.14",
107
+ "@size-limit/file": "^12.0.0",
108
+ "@types/node": "^25.2.0",
109
+ "bumpp": "^10.3.2",
110
+ "c8": "^10.1.3",
111
+ "jiti": "^2.5.1",
112
+ "lint-staged": "^16.2.7",
113
+ "simple-git-hooks": "^2.13.1",
114
+ "size-limit": "^12.0.0",
115
+ "tinybench": "^6.0.0",
116
+ "ts-complex": "^1.0.0",
117
+ "typescript": "^5.9.3",
118
+ "unbuild": "^3.6.1"
119
+ },
120
+ "size-limit": [
121
+ {
122
+ "path": "dist/cli.mjs",
123
+ "limit": "10 kB"
124
+ }
125
+ ],
126
+ "complexity": {
127
+ "maxCyclomatic": 20,
128
+ "minMaintainability": 60,
129
+ "top": 10
130
+ },
131
+ "simple-git-hooks": {
132
+ "pre-commit": "pnpm lint-staged && pnpm typecheck"
133
+ },
134
+ "lint-staged": {
135
+ "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": [
136
+ "biome check --write --no-errors-on-unmatched"
137
+ ]
138
+ }
139
+ }