cachelint 0.1.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/LICENSE +27 -0
- package/LICENSE-PRO.md +67 -0
- package/README.md +298 -0
- package/dist/check-2ONJCYKP.js +23 -0
- package/dist/chunk-2JSY5BQA.js +340 -0
- package/dist/chunk-4GHSUBAY.js +28 -0
- package/dist/chunk-4LB5LF2P.js +51 -0
- package/dist/chunk-BUZCVKMZ.js +246 -0
- package/dist/chunk-DYQPXD3G.js +92 -0
- package/dist/chunk-GXASKZ4Z.js +182 -0
- package/dist/chunk-JZU4PCTS.js +80 -0
- package/dist/chunk-MW7BXKPR.js +593 -0
- package/dist/chunk-O7ZE6CFI.js +32 -0
- package/dist/chunk-SQ4IAMTX.js +65 -0
- package/dist/chunk-UUVSBJ62.js +77 -0
- package/dist/chunk-VWE5TDL5.js +91 -0
- package/dist/chunk-WKFGRNYK.js +494 -0
- package/dist/chunk-WPJYUQMU.js +26 -0
- package/dist/chunk-XKRPGMZT.js +197 -0
- package/dist/chunk-ZSEZQ422.js +286 -0
- package/dist/cli.d.ts +46 -0
- package/dist/cli.js +199 -0
- package/dist/gate-63NKQRXL.js +16 -0
- package/dist/hash-XUTGK6SB.js +15 -0
- package/dist/index.d.ts +1200 -0
- package/dist/index.js +184 -0
- package/dist/license-GFDBTQHG.js +19 -0
- package/dist/lint-PQHIDWOY.js +19 -0
- package/dist/sarif-EQ6VJNGN.js +63 -0
- package/dist/step-summary-NKD4N7AY.js +160 -0
- package/dist/store-KZIQANIT.js +14 -0
- package/package.json +85 -0
- package/schema/cachelint.config.schema.json +114 -0
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RULE_IDS,
|
|
3
|
+
RULE_SEVERITY_VALUES
|
|
4
|
+
} from "./chunk-4GHSUBAY.js";
|
|
5
|
+
import {
|
|
6
|
+
ConfigError,
|
|
7
|
+
SourceError
|
|
8
|
+
} from "./chunk-XKRPGMZT.js";
|
|
9
|
+
|
|
10
|
+
// src/sources/normalize.ts
|
|
11
|
+
function normalizeText(input) {
|
|
12
|
+
if (input.length === 0) return "";
|
|
13
|
+
let s = input;
|
|
14
|
+
if (s.charCodeAt(0) === 65279) s = s.slice(1);
|
|
15
|
+
if (s.includes("\r")) {
|
|
16
|
+
s = s.replace(/\r\n?/g, "\n");
|
|
17
|
+
}
|
|
18
|
+
if (/[ \t]+\n|[ \t]+$/.test(s)) {
|
|
19
|
+
const lines = s.split("\n");
|
|
20
|
+
for (let i = 0; i < lines.length; i++) {
|
|
21
|
+
const line = lines[i];
|
|
22
|
+
if (line === void 0) continue;
|
|
23
|
+
let end = line.length;
|
|
24
|
+
while (end > 0) {
|
|
25
|
+
const ch = line.charCodeAt(end - 1);
|
|
26
|
+
if (ch === 32 || ch === 9) end--;
|
|
27
|
+
else break;
|
|
28
|
+
}
|
|
29
|
+
if (end !== line.length) lines[i] = line.slice(0, end);
|
|
30
|
+
}
|
|
31
|
+
s = lines.join("\n");
|
|
32
|
+
}
|
|
33
|
+
if (s.length === 0) return "";
|
|
34
|
+
if (s.endsWith("\n")) {
|
|
35
|
+
let end = s.length;
|
|
36
|
+
while (end > 1 && s.charCodeAt(end - 2) === 10) end--;
|
|
37
|
+
if (end !== s.length) s = s.slice(0, end);
|
|
38
|
+
} else {
|
|
39
|
+
s = s + "\n";
|
|
40
|
+
}
|
|
41
|
+
return s;
|
|
42
|
+
}
|
|
43
|
+
function hasMixedLineEndings(input) {
|
|
44
|
+
let hasCRLF = false;
|
|
45
|
+
let hasBareLF = false;
|
|
46
|
+
let hasBareCR = false;
|
|
47
|
+
for (let i = 0; i < input.length; i++) {
|
|
48
|
+
const c = input.charCodeAt(i);
|
|
49
|
+
if (c === 13) {
|
|
50
|
+
if (i + 1 < input.length && input.charCodeAt(i + 1) === 10) {
|
|
51
|
+
hasCRLF = true;
|
|
52
|
+
i++;
|
|
53
|
+
} else {
|
|
54
|
+
hasBareCR = true;
|
|
55
|
+
}
|
|
56
|
+
} else if (c === 10) {
|
|
57
|
+
hasBareLF = true;
|
|
58
|
+
}
|
|
59
|
+
if (hasCRLF && hasBareLF || hasCRLF && hasBareCR || hasBareCR && hasBareLF) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
function hasBomByte(input) {
|
|
66
|
+
return input.length > 0 && input.charCodeAt(0) === 65279;
|
|
67
|
+
}
|
|
68
|
+
function toPosixRelative(absPath, projectRoot) {
|
|
69
|
+
const root = projectRoot.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
70
|
+
const target = absPath.replace(/\\/g, "/");
|
|
71
|
+
if (root.length === 0) return target;
|
|
72
|
+
const lowerRoot = root.toLowerCase();
|
|
73
|
+
const lowerTarget = target.toLowerCase();
|
|
74
|
+
if (lowerTarget === lowerRoot) return ".";
|
|
75
|
+
const prefix = lowerRoot.endsWith("/") ? lowerRoot : lowerRoot + "/";
|
|
76
|
+
if (lowerTarget.startsWith(prefix)) {
|
|
77
|
+
return target.slice(prefix.length);
|
|
78
|
+
}
|
|
79
|
+
return target;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/config/schema.ts
|
|
83
|
+
import { z } from "zod";
|
|
84
|
+
var nonEmptyString = z.string().min(1, "must not be empty");
|
|
85
|
+
var bundleSchema = z.strictObject({
|
|
86
|
+
/** Stable bundle name — appears in `cachelint.lock` and in diagnostics. */
|
|
87
|
+
name: nonEmptyString,
|
|
88
|
+
/** Files in this bundle, in the exact order they're concatenated for hashing. */
|
|
89
|
+
files: z.array(nonEmptyString).min(1, "a bundle needs at least one file")
|
|
90
|
+
}).describe("A named, ordered group of files hashed together as one cache prefix.");
|
|
91
|
+
var proSchema = z.strictObject({
|
|
92
|
+
/** Cap on `--exact` count-tokens API calls per run (default 100). */
|
|
93
|
+
exact_limit: z.number().int().positive().optional(),
|
|
94
|
+
/** Provider bug-pattern packs to load, e.g. `["anthropic-2026-q1"]`. */
|
|
95
|
+
packs: z.array(nonEmptyString).optional()
|
|
96
|
+
}).describe("Pro-tier settings (only consulted when a Pro feature is invoked with a valid license).");
|
|
97
|
+
var configSchema = z.strictObject({
|
|
98
|
+
/** Optional `$schema` ref so editors can pick up the JSON Schema. Ignored at runtime. */
|
|
99
|
+
$schema: z.string().optional(),
|
|
100
|
+
/**
|
|
101
|
+
* Globs identifying the files cachelint treats as "prompt content files"
|
|
102
|
+
* for `cachelint lint` (when no explicit paths are given) and for scoping
|
|
103
|
+
* rule R003 (`unsorted-json-stringify`), which never runs against arbitrary
|
|
104
|
+
* source — only against files in this list or already in `cachelint.lock`.
|
|
105
|
+
*/
|
|
106
|
+
prompt_globs: z.array(nonEmptyString).optional(),
|
|
107
|
+
/** Extra ignore globs, applied on top of the built-in secret-file deny-list. */
|
|
108
|
+
exclude: z.array(nonEmptyString).optional(),
|
|
109
|
+
/**
|
|
110
|
+
* Rule ids to disable globally. An unknown id is rejected (the error lists
|
|
111
|
+
* the valid ids). Equivalent to setting that rule to `"off"` in `rules`.
|
|
112
|
+
*/
|
|
113
|
+
disable: z.array(z.enum(RULE_IDS)).optional(),
|
|
114
|
+
/**
|
|
115
|
+
* Per-rule severity overrides. `"off"` ≡ listing the rule in `disable`.
|
|
116
|
+
* Unknown rule ids and unknown severities are both rejected.
|
|
117
|
+
*/
|
|
118
|
+
rules: z.partialRecord(z.enum(RULE_IDS), z.enum(RULE_SEVERITY_VALUES)).optional(),
|
|
119
|
+
/** Named multi-file logical bundles for `cachelint hash` / `check`. */
|
|
120
|
+
bundles: z.array(bundleSchema).optional(),
|
|
121
|
+
/** Per-file size cap in bytes (default 5 MiB). */
|
|
122
|
+
max_file_bytes: z.number().int().positive().optional(),
|
|
123
|
+
/** Cumulative size cap across all resolved files in bytes (default 100 MiB). */
|
|
124
|
+
max_total_bytes: z.number().int().positive().optional(),
|
|
125
|
+
/** Pro-tier settings. */
|
|
126
|
+
pro: proSchema.optional()
|
|
127
|
+
}).describe("cachelint configuration (cachelint.config.json / .yaml / .yml).");
|
|
128
|
+
function defineConfig(config) {
|
|
129
|
+
return config;
|
|
130
|
+
}
|
|
131
|
+
var JSON_SCHEMA = z.toJSONSchema(configSchema, { target: "draft-2020-12" });
|
|
132
|
+
function configJsonSchema() {
|
|
133
|
+
return JSON.parse(JSON.stringify(JSON_SCHEMA));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/config/load.ts
|
|
137
|
+
import { readFileSync, existsSync, statSync } from "fs";
|
|
138
|
+
import { resolve as pathResolve, isAbsolute, basename, extname } from "path";
|
|
139
|
+
import { parse as parseYaml, YAMLParseError } from "yaml";
|
|
140
|
+
import "zod";
|
|
141
|
+
var CONFIG_FILENAMES = [
|
|
142
|
+
"cachelint.config.json",
|
|
143
|
+
"cachelint.config.yaml",
|
|
144
|
+
"cachelint.config.yml"
|
|
145
|
+
];
|
|
146
|
+
var CODE_CONFIG_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".js", ".cjs", ".mjs", ".cts", ".mts"]);
|
|
147
|
+
function loadConfig(opts = {}) {
|
|
148
|
+
const root = pathResolve(opts.projectRoot ?? process.cwd());
|
|
149
|
+
const filePath = opts.explicitPath !== void 0 ? resolveExplicit(opts.explicitPath, root) : discover(root);
|
|
150
|
+
if (filePath === null) {
|
|
151
|
+
return { config: {}, path: null };
|
|
152
|
+
}
|
|
153
|
+
const rel = toPosixRelative(filePath, root);
|
|
154
|
+
rejectCodeConfig(filePath, rel);
|
|
155
|
+
let text;
|
|
156
|
+
try {
|
|
157
|
+
text = readFileSync(filePath, "utf8");
|
|
158
|
+
} catch (err) {
|
|
159
|
+
throw new ConfigError({
|
|
160
|
+
code: "CFG_UNREADABLE",
|
|
161
|
+
message: `cannot read config file ${rel}`,
|
|
162
|
+
file: rel,
|
|
163
|
+
cause: err
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
const data = parseConfigText(text, rel);
|
|
167
|
+
const config = validate(data, rel);
|
|
168
|
+
return { config, path: rel };
|
|
169
|
+
}
|
|
170
|
+
function parseConfig(data, sourceLabel = "<config>") {
|
|
171
|
+
return validate(data, sourceLabel);
|
|
172
|
+
}
|
|
173
|
+
function resolveConfigArg(arg, projectRoot) {
|
|
174
|
+
if (arg === void 0) {
|
|
175
|
+
const loaded = loadConfig({ projectRoot });
|
|
176
|
+
return { config: loaded.config, configPath: loaded.path };
|
|
177
|
+
}
|
|
178
|
+
if (typeof arg === "string") {
|
|
179
|
+
const loaded = loadConfig({ projectRoot, explicitPath: arg });
|
|
180
|
+
return { config: loaded.config, configPath: loaded.path };
|
|
181
|
+
}
|
|
182
|
+
return { config: parseConfig(arg, "<config object>"), configPath: null };
|
|
183
|
+
}
|
|
184
|
+
function resolveExplicit(explicitPath, root) {
|
|
185
|
+
const abs = isAbsolute(explicitPath) ? explicitPath : pathResolve(root, explicitPath);
|
|
186
|
+
const rel = toPosixRelative(abs, root);
|
|
187
|
+
if (!existsSync(abs)) {
|
|
188
|
+
throw new ConfigError({
|
|
189
|
+
code: "CFG_NOT_FOUND",
|
|
190
|
+
message: `config file not found: ${explicitPath}`,
|
|
191
|
+
file: rel,
|
|
192
|
+
hint: "Check the --config path, or omit it to auto-discover cachelint.config.{json,yaml,yml}."
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
if (!statSync(abs).isFile()) {
|
|
197
|
+
throw new ConfigError({
|
|
198
|
+
code: "CFG_NOT_FILE",
|
|
199
|
+
message: `config path is not a regular file: ${explicitPath}`,
|
|
200
|
+
file: rel
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
} catch (err) {
|
|
204
|
+
if (err instanceof ConfigError) throw err;
|
|
205
|
+
throw new ConfigError({ code: "CFG_UNREADABLE", message: `cannot stat config file ${rel}`, file: rel, cause: err });
|
|
206
|
+
}
|
|
207
|
+
return abs;
|
|
208
|
+
}
|
|
209
|
+
function discover(root) {
|
|
210
|
+
const found = CONFIG_FILENAMES.map((name) => pathResolve(root, name)).filter((p) => existsSync(p));
|
|
211
|
+
if (found.length === 0) return null;
|
|
212
|
+
if (found.length > 1) {
|
|
213
|
+
throw new ConfigError({
|
|
214
|
+
code: "CFG_AMBIGUOUS",
|
|
215
|
+
message: `multiple config files found: ${found.map((p) => basename(p)).join(", ")}`,
|
|
216
|
+
hint: "Keep exactly one of cachelint.config.{json,yaml,yml}."
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
return found[0];
|
|
220
|
+
}
|
|
221
|
+
function rejectCodeConfig(absPath, rel) {
|
|
222
|
+
const ext = extname(absPath).toLowerCase();
|
|
223
|
+
if (CODE_CONFIG_EXTENSIONS.has(ext)) {
|
|
224
|
+
throw new ConfigError({
|
|
225
|
+
code: "CFG_CODE_NOT_ALLOWED",
|
|
226
|
+
message: `executable config (${ext}) is not supported: ${rel}`,
|
|
227
|
+
file: rel,
|
|
228
|
+
hint: "Use cachelint.config.json or cachelint.config.yaml \u2014 config is data, not code. For dynamic config, call the library API."
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function parseConfigText(text, rel) {
|
|
233
|
+
const isJson = rel.toLowerCase().endsWith(".json");
|
|
234
|
+
if (isJson) {
|
|
235
|
+
try {
|
|
236
|
+
return JSON.parse(text);
|
|
237
|
+
} catch (err) {
|
|
238
|
+
throw new ConfigError({
|
|
239
|
+
code: "CFG_PARSE",
|
|
240
|
+
message: `invalid JSON in ${rel}: ${err instanceof Error ? err.message : String(err)}`,
|
|
241
|
+
file: rel
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
const doc = parseYaml(text);
|
|
247
|
+
return doc ?? {};
|
|
248
|
+
} catch (err) {
|
|
249
|
+
if (err instanceof YAMLParseError) {
|
|
250
|
+
throw new ConfigError({
|
|
251
|
+
code: "CFG_PARSE",
|
|
252
|
+
message: `invalid YAML in ${rel}: ${err.message}`,
|
|
253
|
+
file: rel
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
throw new ConfigError({
|
|
257
|
+
code: "CFG_PARSE",
|
|
258
|
+
message: `cannot parse ${rel}: ${err instanceof Error ? err.message : String(err)}`,
|
|
259
|
+
file: rel
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function validate(data, sourceLabel) {
|
|
264
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
265
|
+
throw new ConfigError({
|
|
266
|
+
code: "CFG_INVALID",
|
|
267
|
+
message: `config in ${sourceLabel} must be a mapping/object, got ${describeJsonType(data)}`,
|
|
268
|
+
file: sourceLabel
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
const result = configSchema.safeParse(data);
|
|
272
|
+
if (!result.success) {
|
|
273
|
+
throw new ConfigError({
|
|
274
|
+
code: "CFG_INVALID",
|
|
275
|
+
message: `invalid config in ${sourceLabel}:
|
|
276
|
+
${formatZodIssues(result.error)}`,
|
|
277
|
+
file: sourceLabel,
|
|
278
|
+
hint: "See the published JSON Schema (schema/cachelint.config.schema.json) for the full shape."
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
return result.data;
|
|
282
|
+
}
|
|
283
|
+
function describeJsonType(v) {
|
|
284
|
+
if (v === null) return "null";
|
|
285
|
+
if (Array.isArray(v)) return "array";
|
|
286
|
+
return typeof v;
|
|
287
|
+
}
|
|
288
|
+
function formatZodIssues(err) {
|
|
289
|
+
return err.issues.map((issue) => {
|
|
290
|
+
const path = issue.path.length > 0 ? issue.path.map(String).join(".") : "(root)";
|
|
291
|
+
return ` - ${path}: ${issue.message}`;
|
|
292
|
+
}).join("\n");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/sources/resolve.ts
|
|
296
|
+
import { readFileSync as readFileSync2, realpathSync, statSync as statSync2 } from "fs";
|
|
297
|
+
import { resolve as pathResolve2, isAbsolute as isAbsolute2 } from "path";
|
|
298
|
+
import { globSync } from "fs";
|
|
299
|
+
|
|
300
|
+
// src/sources/hash.ts
|
|
301
|
+
import { createHash } from "crypto";
|
|
302
|
+
function sha256Hex(text) {
|
|
303
|
+
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
304
|
+
}
|
|
305
|
+
function bundleHash(perSourceHashes) {
|
|
306
|
+
const buf = perSourceHashes.join("\n") + (perSourceHashes.length > 0 ? "\n" : "");
|
|
307
|
+
return sha256Hex(buf);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// src/sources/resolve.ts
|
|
311
|
+
var DEFAULT_MAX_FILE_BYTES = 5 * 1024 * 1024;
|
|
312
|
+
var DEFAULT_MAX_TOTAL_BYTES = 100 * 1024 * 1024;
|
|
313
|
+
var BINARY_HEAD_BYTES = 8 * 1024;
|
|
314
|
+
var SECRET_FILE_PATTERNS = [
|
|
315
|
+
// Env files in any directory: `.env`, `.env.local`, `.env.production`, …
|
|
316
|
+
{ pattern: ".env", kind: "basenameStartsWith" },
|
|
317
|
+
// Common private-key file extensions.
|
|
318
|
+
{ pattern: ".pem", kind: "endsWith" },
|
|
319
|
+
{ pattern: ".key", kind: "endsWith" },
|
|
320
|
+
{ pattern: ".p8", kind: "endsWith" },
|
|
321
|
+
{ pattern: ".p12", kind: "endsWith" },
|
|
322
|
+
{ pattern: ".pfx", kind: "endsWith" },
|
|
323
|
+
{ pattern: ".cer", kind: "endsWith" },
|
|
324
|
+
{ pattern: ".p7b", kind: "endsWith" },
|
|
325
|
+
{ pattern: ".keystore", kind: "endsWith" },
|
|
326
|
+
// SSH / GPG / netrc / config files.
|
|
327
|
+
{ pattern: "id_rsa", kind: "basenameStartsWith" },
|
|
328
|
+
{ pattern: "id_ed25519", kind: "basenameStartsWith" },
|
|
329
|
+
{ pattern: "id_ecdsa", kind: "basenameStartsWith" },
|
|
330
|
+
{ pattern: ".ssh/config", kind: "endsWith" },
|
|
331
|
+
{ pattern: ".netrc", kind: "basename" },
|
|
332
|
+
{ pattern: ".npmrc", kind: "basename" },
|
|
333
|
+
// Common cloud-credentials locations.
|
|
334
|
+
{ pattern: ".aws/credentials", kind: "endsWith" },
|
|
335
|
+
{ pattern: ".aws/config", kind: "endsWith" },
|
|
336
|
+
{ pattern: "gcloud/", kind: "contains" },
|
|
337
|
+
{ pattern: "application_default_credentials.json", kind: "basename" },
|
|
338
|
+
{ pattern: "kubeconfig", kind: "basename" },
|
|
339
|
+
{ pattern: ".docker/config.json", kind: "endsWith" },
|
|
340
|
+
{ pattern: "credentials", kind: "basename" },
|
|
341
|
+
// GitHub/GH tokens often stored locally.
|
|
342
|
+
{ pattern: "gh_token", kind: "basenameStartsWith" }
|
|
343
|
+
];
|
|
344
|
+
function resolveSources(input) {
|
|
345
|
+
const root = input.projectRoot ?? process.cwd();
|
|
346
|
+
const absRoot = pathResolve2(root);
|
|
347
|
+
const maxFileBytes = input.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
|
|
348
|
+
const maxTotalBytes = input.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES;
|
|
349
|
+
const extraExclude = input.exclude ?? [];
|
|
350
|
+
const warnings = [];
|
|
351
|
+
const sources = [];
|
|
352
|
+
let cumulative = 0;
|
|
353
|
+
const expanded = expandPaths(input.paths, absRoot);
|
|
354
|
+
if (expanded.length === 0) {
|
|
355
|
+
if (input.required === true) {
|
|
356
|
+
throw new SourceError({
|
|
357
|
+
code: "SRC_EMPTY",
|
|
358
|
+
message: `no files matched: ${input.paths.join(", ")}`,
|
|
359
|
+
hint: "Check the glob, or mark this source `required: false` if matching nothing is OK."
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
warnings.push({
|
|
363
|
+
code: "GLOB_EMPTY",
|
|
364
|
+
message: `no files matched: ${input.paths.join(", ")}`
|
|
365
|
+
});
|
|
366
|
+
return { sources, warnings };
|
|
367
|
+
}
|
|
368
|
+
let nonBinaryCount = 0;
|
|
369
|
+
for (const absPath of expanded) {
|
|
370
|
+
const posix = toPosixRelative(absPath, absRoot);
|
|
371
|
+
const denyMatch = matchesSecretPattern(posix);
|
|
372
|
+
if (denyMatch !== null) {
|
|
373
|
+
warnings.push({
|
|
374
|
+
code: "SKIPPED_SECRET",
|
|
375
|
+
message: `excluded (matched secret pattern "${denyMatch}")`,
|
|
376
|
+
path: posix
|
|
377
|
+
});
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (extraExclude.some((g) => globMatchesPath(g, posix))) {
|
|
381
|
+
warnings.push({ code: "SKIPPED_EXCLUDED", message: "excluded by config", path: posix });
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
let st;
|
|
385
|
+
let realAbs;
|
|
386
|
+
try {
|
|
387
|
+
realAbs = realpathSync(absPath);
|
|
388
|
+
st = statSync2(realAbs);
|
|
389
|
+
} catch (err) {
|
|
390
|
+
throw new SourceError({
|
|
391
|
+
code: "SRC_UNREADABLE",
|
|
392
|
+
message: `cannot stat ${posix}`,
|
|
393
|
+
path: posix,
|
|
394
|
+
cause: err
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
if (!isInsideRoot(realAbs, absRoot)) {
|
|
398
|
+
throw new SourceError({
|
|
399
|
+
code: "SRC_OUT_OF_ROOT",
|
|
400
|
+
message: `${posix} resolves outside the project root via symlink`,
|
|
401
|
+
path: posix,
|
|
402
|
+
hint: "Symlinks pointing outside the project root are refused \u2014 adjust the link or move the file in."
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
if (!st.isFile()) {
|
|
406
|
+
throw new SourceError({
|
|
407
|
+
code: "SRC_NOT_FILE",
|
|
408
|
+
message: `${posix} is not a regular file (device / socket / fifo)`,
|
|
409
|
+
path: posix
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
if (st.size > maxFileBytes) {
|
|
413
|
+
throw new SourceError({
|
|
414
|
+
code: "SRC_TOO_LARGE",
|
|
415
|
+
message: `${posix} is ${st.size} bytes; exceeds maxFileBytes ${maxFileBytes}`,
|
|
416
|
+
path: posix,
|
|
417
|
+
hint: "Raise maxFileBytes in cachelint.config, or split the source."
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
cumulative += st.size;
|
|
421
|
+
if (cumulative > maxTotalBytes) {
|
|
422
|
+
throw new SourceError({
|
|
423
|
+
code: "SRC_TOTAL_TOO_LARGE",
|
|
424
|
+
message: `cumulative bytes ${cumulative} would exceed maxTotalBytes ${maxTotalBytes}`,
|
|
425
|
+
hint: "Raise maxTotalBytes in cachelint.config, or scope the glob more tightly."
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
let bytes;
|
|
429
|
+
try {
|
|
430
|
+
bytes = readFileSync2(realAbs);
|
|
431
|
+
} catch (err) {
|
|
432
|
+
throw new SourceError({
|
|
433
|
+
code: "SRC_UNREADABLE",
|
|
434
|
+
message: `cannot read ${posix}`,
|
|
435
|
+
path: posix,
|
|
436
|
+
cause: err
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
if (bytes.length > maxFileBytes) {
|
|
440
|
+
throw new SourceError({
|
|
441
|
+
code: "SRC_TOO_LARGE",
|
|
442
|
+
message: `${posix} read as ${bytes.length} bytes; exceeds maxFileBytes ${maxFileBytes}`,
|
|
443
|
+
path: posix,
|
|
444
|
+
hint: "Raise maxFileBytes in cachelint.config, or split the source. (statSync under-reported its size \u2014 this can happen with /proc-style files.)"
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
if (bytes.length !== st.size) cumulative += bytes.length - st.size;
|
|
448
|
+
if (cumulative > maxTotalBytes) {
|
|
449
|
+
throw new SourceError({
|
|
450
|
+
code: "SRC_TOTAL_TOO_LARGE",
|
|
451
|
+
message: `cumulative bytes ${cumulative} would exceed maxTotalBytes ${maxTotalBytes}`,
|
|
452
|
+
hint: "Raise maxTotalBytes in cachelint.config, or scope the glob more tightly."
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
if (looksBinary(bytes)) {
|
|
456
|
+
warnings.push({ code: "SKIPPED_BINARY", message: "skipped: binary content", path: posix });
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
let raw;
|
|
460
|
+
try {
|
|
461
|
+
raw = bytes.toString("utf8");
|
|
462
|
+
} catch (err) {
|
|
463
|
+
warnings.push({ code: "SKIPPED_NON_UTF8", message: "skipped: not valid UTF-8", path: posix });
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
const normalized = normalizeText(raw);
|
|
467
|
+
const sha = sha256Hex(normalized);
|
|
468
|
+
sources.push({
|
|
469
|
+
name: input.name ?? posix,
|
|
470
|
+
path: posix,
|
|
471
|
+
raw,
|
|
472
|
+
normalized,
|
|
473
|
+
sha256: sha,
|
|
474
|
+
bytes: st.size,
|
|
475
|
+
bomByte: hasBomByte(raw),
|
|
476
|
+
mixedLineEndings: hasMixedLineEndings(raw)
|
|
477
|
+
});
|
|
478
|
+
nonBinaryCount++;
|
|
479
|
+
}
|
|
480
|
+
if (input.required === true && nonBinaryCount === 0) {
|
|
481
|
+
throw new SourceError({
|
|
482
|
+
code: "SRC_NO_TEXT",
|
|
483
|
+
message: `no text files resolved from required source ${input.paths.join(", ")} (all matches were binary or excluded)`
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
return { sources, warnings };
|
|
487
|
+
}
|
|
488
|
+
function expandPaths(paths, absRoot) {
|
|
489
|
+
const all = /* @__PURE__ */ new Set();
|
|
490
|
+
for (const p of paths) {
|
|
491
|
+
if (isGlob(p)) {
|
|
492
|
+
const matches = globSync(p, { cwd: absRoot });
|
|
493
|
+
for (const m of matches) {
|
|
494
|
+
all.add(isAbsolute2(m) ? m : pathResolve2(absRoot, m));
|
|
495
|
+
}
|
|
496
|
+
} else {
|
|
497
|
+
const abs = isAbsolute2(p) ? p : pathResolve2(absRoot, p);
|
|
498
|
+
all.add(abs);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return [...all].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
502
|
+
}
|
|
503
|
+
function isGlob(s) {
|
|
504
|
+
return /[*?[\]{}]/.test(s);
|
|
505
|
+
}
|
|
506
|
+
function globMatchesPath(pattern, path) {
|
|
507
|
+
let re = "";
|
|
508
|
+
let i = 0;
|
|
509
|
+
while (i < pattern.length) {
|
|
510
|
+
const c = pattern[i] ?? "";
|
|
511
|
+
if (c === "*") {
|
|
512
|
+
if (pattern[i + 1] === "*") {
|
|
513
|
+
re += ".*";
|
|
514
|
+
i += 2;
|
|
515
|
+
} else {
|
|
516
|
+
re += "[^/]*";
|
|
517
|
+
i++;
|
|
518
|
+
}
|
|
519
|
+
} else if (c === "?") {
|
|
520
|
+
re += "[^/]";
|
|
521
|
+
i++;
|
|
522
|
+
} else if (c === "{") {
|
|
523
|
+
const end = pattern.indexOf("}", i);
|
|
524
|
+
if (end === -1) {
|
|
525
|
+
re += "\\{";
|
|
526
|
+
i++;
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
const alts = pattern.slice(i + 1, end).split(",").map((s) => s.replace(/[.+^$()|\\]/g, "\\$&"));
|
|
530
|
+
re += `(?:${alts.join("|")})`;
|
|
531
|
+
i = end + 1;
|
|
532
|
+
} else if (/[.+^$()|\\]/.test(c)) {
|
|
533
|
+
re += "\\" + c;
|
|
534
|
+
i++;
|
|
535
|
+
} else {
|
|
536
|
+
re += c;
|
|
537
|
+
i++;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
try {
|
|
541
|
+
return new RegExp("^" + re + "$").test(path);
|
|
542
|
+
} catch {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
function isInsideRoot(target, root) {
|
|
547
|
+
const norm = (p) => p.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
548
|
+
const t = norm(target);
|
|
549
|
+
const r = norm(root);
|
|
550
|
+
return t === r || t.startsWith(r + "/");
|
|
551
|
+
}
|
|
552
|
+
function matchesSecretPattern(posixPath) {
|
|
553
|
+
const lower = posixPath.toLowerCase();
|
|
554
|
+
const basename2 = lower.split("/").pop() ?? "";
|
|
555
|
+
for (const { pattern, kind } of SECRET_FILE_PATTERNS) {
|
|
556
|
+
const p = pattern.toLowerCase();
|
|
557
|
+
switch (kind) {
|
|
558
|
+
case "endsWith":
|
|
559
|
+
if (lower.endsWith(p)) return pattern;
|
|
560
|
+
break;
|
|
561
|
+
case "basename":
|
|
562
|
+
if (basename2 === p) return pattern;
|
|
563
|
+
break;
|
|
564
|
+
case "basenameStartsWith":
|
|
565
|
+
if (basename2.startsWith(p)) return pattern;
|
|
566
|
+
break;
|
|
567
|
+
case "contains":
|
|
568
|
+
if (lower.includes(p)) return pattern;
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
function looksBinary(buf) {
|
|
575
|
+
const limit = Math.min(buf.length, BINARY_HEAD_BYTES);
|
|
576
|
+
for (let i = 0; i < limit; i++) {
|
|
577
|
+
if (buf[i] === 0) return true;
|
|
578
|
+
}
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export {
|
|
583
|
+
toPosixRelative,
|
|
584
|
+
configSchema,
|
|
585
|
+
defineConfig,
|
|
586
|
+
configJsonSchema,
|
|
587
|
+
CONFIG_FILENAMES,
|
|
588
|
+
loadConfig,
|
|
589
|
+
parseConfig,
|
|
590
|
+
resolveConfigArg,
|
|
591
|
+
bundleHash,
|
|
592
|
+
resolveSources
|
|
593
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// src/cost/pricing.ts
|
|
2
|
+
var PRICING_TABLE_VERSION = 1;
|
|
3
|
+
var PRICING_AS_OF = "2026-06";
|
|
4
|
+
var REFERENCE_MODEL = "claude-sonnet-4-6";
|
|
5
|
+
var TABLE = Object.freeze([
|
|
6
|
+
{ model: "claude-opus-4-8", baseInputPerMTok: 5, cacheReadPerMTok: 0.5, minCacheablePrefixTokens: 4096 },
|
|
7
|
+
{ model: "claude-opus-4-7", baseInputPerMTok: 5, cacheReadPerMTok: 0.5, minCacheablePrefixTokens: 4096 },
|
|
8
|
+
{ model: "claude-opus-4-6", baseInputPerMTok: 5, cacheReadPerMTok: 0.5, minCacheablePrefixTokens: 4096 },
|
|
9
|
+
{ model: "claude-sonnet-4-6", baseInputPerMTok: 3, cacheReadPerMTok: 0.3, minCacheablePrefixTokens: 2048 },
|
|
10
|
+
{ model: "claude-haiku-4-5", baseInputPerMTok: 1, cacheReadPerMTok: 0.1, minCacheablePrefixTokens: 4096 },
|
|
11
|
+
{ model: "claude-fable-5", baseInputPerMTok: 10, cacheReadPerMTok: 1, minCacheablePrefixTokens: 2048 }
|
|
12
|
+
]);
|
|
13
|
+
function getModelPricing(model) {
|
|
14
|
+
return TABLE.find((p) => p.model === model) ?? null;
|
|
15
|
+
}
|
|
16
|
+
function referencePricing() {
|
|
17
|
+
const p = getModelPricing(REFERENCE_MODEL);
|
|
18
|
+
if (p === null) throw new Error(`cachelint: reference model ${REFERENCE_MODEL} missing from pricing table`);
|
|
19
|
+
return p;
|
|
20
|
+
}
|
|
21
|
+
function wastePerTokenUsd(p) {
|
|
22
|
+
return (p.baseInputPerMTok - p.cacheReadPerMTok) / 1e6;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
PRICING_TABLE_VERSION,
|
|
27
|
+
PRICING_AS_OF,
|
|
28
|
+
REFERENCE_MODEL,
|
|
29
|
+
getModelPricing,
|
|
30
|
+
referencePricing,
|
|
31
|
+
wastePerTokenUsd
|
|
32
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_LOCKFILE_NAME,
|
|
3
|
+
computeBundles,
|
|
4
|
+
makeLockfile,
|
|
5
|
+
writeLockfileAtomic
|
|
6
|
+
} from "./chunk-BUZCVKMZ.js";
|
|
7
|
+
import {
|
|
8
|
+
resolveConfigArg,
|
|
9
|
+
toPosixRelative
|
|
10
|
+
} from "./chunk-MW7BXKPR.js";
|
|
11
|
+
import {
|
|
12
|
+
CACHELINT_VERSION
|
|
13
|
+
} from "./chunk-WPJYUQMU.js";
|
|
14
|
+
import {
|
|
15
|
+
redactSecrets
|
|
16
|
+
} from "./chunk-XKRPGMZT.js";
|
|
17
|
+
|
|
18
|
+
// src/commands/hash.ts
|
|
19
|
+
import { resolve as pathResolve } from "path";
|
|
20
|
+
function hash(args = {}) {
|
|
21
|
+
const root = pathResolve(args.projectRoot ?? process.cwd());
|
|
22
|
+
const { config, configPath } = resolveConfigArg(args.config, root);
|
|
23
|
+
const { bundles, warnings } = computeBundles({
|
|
24
|
+
...args.paths !== void 0 ? { paths: args.paths } : {},
|
|
25
|
+
config,
|
|
26
|
+
projectRoot: root
|
|
27
|
+
});
|
|
28
|
+
const lockfile = makeLockfile(bundles);
|
|
29
|
+
const lockPath = pathResolve(root, args.out ?? DEFAULT_LOCKFILE_NAME);
|
|
30
|
+
const text = writeLockfileAtomic(lockPath, lockfile);
|
|
31
|
+
return { lockfile, text, path: lockPath, configPath, warnings, bundleCount: bundles.length };
|
|
32
|
+
}
|
|
33
|
+
function renderHashHuman(r, projectRoot = process.cwd()) {
|
|
34
|
+
const lockDisplay = toPosixRelative(r.path, pathResolve(projectRoot));
|
|
35
|
+
const lines = [];
|
|
36
|
+
for (const w of r.warnings) lines.push(`note ${w.path ? w.path + ": " : ""}${w.message}`);
|
|
37
|
+
lines.push(`cachelint: wrote ${r.bundleCount} bundle${r.bundleCount === 1 ? "" : "s"} to ${lockDisplay}`);
|
|
38
|
+
for (const b of r.lockfile.bundles) {
|
|
39
|
+
const files = b.files.length === 1 ? b.files[0].path : `${b.files.length} files`;
|
|
40
|
+
lines.push(` ${b.name} ${b.stablePrefixHash.slice(0, 12)} (${files})`);
|
|
41
|
+
}
|
|
42
|
+
return redactSecrets(lines.join("\n"));
|
|
43
|
+
}
|
|
44
|
+
function renderHashJson(r, projectRoot = process.cwd()) {
|
|
45
|
+
return redactSecrets(
|
|
46
|
+
JSON.stringify(
|
|
47
|
+
{
|
|
48
|
+
version: CACHELINT_VERSION,
|
|
49
|
+
configPath: r.configPath,
|
|
50
|
+
lockfilePath: toPosixRelative(r.path, pathResolve(projectRoot)),
|
|
51
|
+
bundleCount: r.bundleCount,
|
|
52
|
+
bundles: r.lockfile.bundles,
|
|
53
|
+
warnings: r.warnings
|
|
54
|
+
},
|
|
55
|
+
null,
|
|
56
|
+
2
|
|
57
|
+
)
|
|
58
|
+
) + "\n";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export {
|
|
62
|
+
hash,
|
|
63
|
+
renderHashHuman,
|
|
64
|
+
renderHashJson
|
|
65
|
+
};
|