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,340 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PRICING_AS_OF,
|
|
3
|
+
PRICING_TABLE_VERSION,
|
|
4
|
+
REFERENCE_MODEL,
|
|
5
|
+
referencePricing,
|
|
6
|
+
wastePerTokenUsd
|
|
7
|
+
} from "./chunk-O7ZE6CFI.js";
|
|
8
|
+
import {
|
|
9
|
+
requirePro
|
|
10
|
+
} from "./chunk-GXASKZ4Z.js";
|
|
11
|
+
import {
|
|
12
|
+
runRules
|
|
13
|
+
} from "./chunk-WKFGRNYK.js";
|
|
14
|
+
import {
|
|
15
|
+
COST_LINE_OVERLAPPED,
|
|
16
|
+
costLabel,
|
|
17
|
+
costLineAtRisk,
|
|
18
|
+
costLineInvalidates,
|
|
19
|
+
costLineWasted,
|
|
20
|
+
costSummary,
|
|
21
|
+
formatUsdPer1k
|
|
22
|
+
} from "./chunk-VWE5TDL5.js";
|
|
23
|
+
import {
|
|
24
|
+
resolveConfigArg,
|
|
25
|
+
resolveSources,
|
|
26
|
+
toPosixRelative
|
|
27
|
+
} from "./chunk-MW7BXKPR.js";
|
|
28
|
+
import {
|
|
29
|
+
DEFAULT_RULE_VOLATILITY,
|
|
30
|
+
isRuleId
|
|
31
|
+
} from "./chunk-4GHSUBAY.js";
|
|
32
|
+
import {
|
|
33
|
+
CACHELINT_VERSION,
|
|
34
|
+
COST_MODEL_VERSION
|
|
35
|
+
} from "./chunk-WPJYUQMU.js";
|
|
36
|
+
import {
|
|
37
|
+
ConfigError,
|
|
38
|
+
SourceError,
|
|
39
|
+
costUnavailableWarning,
|
|
40
|
+
diagnosticsExitCode,
|
|
41
|
+
redactSecrets
|
|
42
|
+
} from "./chunk-XKRPGMZT.js";
|
|
43
|
+
|
|
44
|
+
// src/commands/lint.ts
|
|
45
|
+
import { extname, resolve as pathResolve } from "path";
|
|
46
|
+
|
|
47
|
+
// src/rules/packs.ts
|
|
48
|
+
var KNOWN_PACK_IDS = /* @__PURE__ */ new Set(["anthropic-2026-q1"]);
|
|
49
|
+
|
|
50
|
+
// src/cost/estimate.ts
|
|
51
|
+
var CHARS_PER_TOKEN = 4;
|
|
52
|
+
function usdPer1kCalls(tokens, pricing) {
|
|
53
|
+
return tokens * wastePerTokenUsd(pricing) * 1e3;
|
|
54
|
+
}
|
|
55
|
+
function heuristicTokens(chars) {
|
|
56
|
+
return Math.ceil(chars / CHARS_PER_TOKEN);
|
|
57
|
+
}
|
|
58
|
+
function metricsOf(normalized) {
|
|
59
|
+
const lines = normalized === "" ? [] : normalized.slice(0, -1).split("\n");
|
|
60
|
+
const lineLengths = lines.map((l) => l.length);
|
|
61
|
+
const lineStarts = new Array(lineLengths.length);
|
|
62
|
+
let at = 0;
|
|
63
|
+
for (let i = 0; i < lineLengths.length; i++) {
|
|
64
|
+
lineStarts[i] = at;
|
|
65
|
+
at += lineLengths[i] + 1;
|
|
66
|
+
}
|
|
67
|
+
return { totalLength: normalized.length, lineLengths, lineStarts };
|
|
68
|
+
}
|
|
69
|
+
function normalizedOffsetFor(m, line, column) {
|
|
70
|
+
if (m.lineLengths.length === 0) return 0;
|
|
71
|
+
const idx = line - 1;
|
|
72
|
+
if (idx < 0) return 0;
|
|
73
|
+
if (idx >= m.lineLengths.length) return m.totalLength;
|
|
74
|
+
return m.lineStarts[idx] + Math.min(Math.max(column - 1, 0), m.lineLengths[idx]);
|
|
75
|
+
}
|
|
76
|
+
function volatilityOf(d) {
|
|
77
|
+
if (d.volatility !== void 0) return d.volatility;
|
|
78
|
+
return isRuleId(d.rule) ? DEFAULT_RULE_VOLATILITY[d.rule] : "regeneration";
|
|
79
|
+
}
|
|
80
|
+
function estimateCosts(diagnostics, normalizedTextByFile, pricing = referencePricing()) {
|
|
81
|
+
const metricsByFile = /* @__PURE__ */ new Map();
|
|
82
|
+
const metricsFor = (file) => {
|
|
83
|
+
const cached = metricsByFile.get(file);
|
|
84
|
+
if (cached !== void 0) return cached;
|
|
85
|
+
const text = normalizedTextByFile.get(file);
|
|
86
|
+
if (text === void 0) return null;
|
|
87
|
+
const m = metricsOf(text);
|
|
88
|
+
metricsByFile.set(file, m);
|
|
89
|
+
return m;
|
|
90
|
+
};
|
|
91
|
+
const computed = diagnostics.map((d) => {
|
|
92
|
+
const m = metricsFor(d.file);
|
|
93
|
+
if (m === null) return null;
|
|
94
|
+
const offset = normalizedOffsetFor(m, d.line, d.column);
|
|
95
|
+
const remainingChars = Math.max(0, m.totalLength - offset);
|
|
96
|
+
return {
|
|
97
|
+
file: d.file,
|
|
98
|
+
volatility: volatilityOf(d),
|
|
99
|
+
offset,
|
|
100
|
+
fromHereTokens: heuristicTokens(remainingChars)
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
const earliest = /* @__PURE__ */ new Map();
|
|
104
|
+
for (let i = 0; i < computed.length; i++) {
|
|
105
|
+
const c = computed[i];
|
|
106
|
+
if (c === null || c === void 0) continue;
|
|
107
|
+
const key = `${c.file}\0${c.volatility}`;
|
|
108
|
+
const prev = earliest.get(key);
|
|
109
|
+
if (prev === void 0 || c.offset < computed[prev].offset) earliest.set(key, i);
|
|
110
|
+
}
|
|
111
|
+
const perDiagnostic = computed.map((c, i) => {
|
|
112
|
+
if (c === null) return null;
|
|
113
|
+
const counts = earliest.get(`${c.file}\0${c.volatility}`) === i;
|
|
114
|
+
return {
|
|
115
|
+
fromHereTokens: c.fromHereTokens,
|
|
116
|
+
volatility: c.volatility,
|
|
117
|
+
countsTowardTotal: counts,
|
|
118
|
+
...c.volatility === "per-call" ? { wastedUsdPer1kCalls: usdPer1kCalls(c.fromHereTokens, pricing) } : {}
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
const perFileTokens = /* @__PURE__ */ new Map();
|
|
122
|
+
for (const i of earliest.values()) {
|
|
123
|
+
const c = computed[i];
|
|
124
|
+
const agg = perFileTokens.get(c.file) ?? { wasted: 0, atRisk: 0 };
|
|
125
|
+
if (c.volatility === "per-call") agg.wasted = c.fromHereTokens;
|
|
126
|
+
else agg.atRisk = c.fromHereTokens;
|
|
127
|
+
perFileTokens.set(c.file, agg);
|
|
128
|
+
}
|
|
129
|
+
const perFile = /* @__PURE__ */ new Map();
|
|
130
|
+
let totalWasted = 0;
|
|
131
|
+
let totalAtRisk = 0;
|
|
132
|
+
for (const file of [...perFileTokens.keys()].sort()) {
|
|
133
|
+
const { wasted, atRisk } = perFileTokens.get(file);
|
|
134
|
+
perFile.set(file, {
|
|
135
|
+
wastedTokens: wasted,
|
|
136
|
+
wastedUsdPer1kCalls: usdPer1kCalls(wasted, pricing),
|
|
137
|
+
atRiskTokens: atRisk
|
|
138
|
+
});
|
|
139
|
+
totalWasted += wasted;
|
|
140
|
+
totalAtRisk += atRisk;
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
perDiagnostic,
|
|
144
|
+
perFile,
|
|
145
|
+
totals: {
|
|
146
|
+
wastedTokens: totalWasted,
|
|
147
|
+
wastedUsdPer1kCalls: usdPer1kCalls(totalWasted, pricing),
|
|
148
|
+
atRiskTokens: totalAtRisk
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/commands/lint.ts
|
|
154
|
+
var GLOB_CHARS = /[*?[\]{}]/;
|
|
155
|
+
function lint(args = {}) {
|
|
156
|
+
const root = pathResolve(args.projectRoot ?? process.cwd());
|
|
157
|
+
const { config, configPath } = resolveConfigArg(args.config, root);
|
|
158
|
+
const packLoaded = resolvePack(args.pack);
|
|
159
|
+
const cliPaths = args.paths ?? [];
|
|
160
|
+
const scanPaths = cliPaths.length > 0 ? [...cliPaths] : config.prompt_globs ?? [];
|
|
161
|
+
if (scanPaths.length === 0) {
|
|
162
|
+
throw new SourceError({
|
|
163
|
+
code: "SRC_NO_PATHS",
|
|
164
|
+
message: "no paths to lint: pass file/glob arguments, or set `prompt_globs` in cachelint.config",
|
|
165
|
+
hint: 'e.g. `cachelint lint prompts/system.md` or add `prompt_globs: ["prompts/**/*"]` to your config.'
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
const promptScoped = computePromptScopedSet(cliPaths, config, root);
|
|
169
|
+
const resolution = resolveSources({
|
|
170
|
+
paths: scanPaths,
|
|
171
|
+
projectRoot: root,
|
|
172
|
+
required: false,
|
|
173
|
+
...config.exclude !== void 0 ? { exclude: config.exclude } : {},
|
|
174
|
+
...config.max_file_bytes !== void 0 ? { maxFileBytes: config.max_file_bytes } : {},
|
|
175
|
+
...config.max_total_bytes !== void 0 ? { maxTotalBytes: config.max_total_bytes } : {}
|
|
176
|
+
});
|
|
177
|
+
const inputs = resolution.sources.map((s) => ({
|
|
178
|
+
file: s.path,
|
|
179
|
+
raw: s.raw,
|
|
180
|
+
ext: extname(s.path).toLowerCase(),
|
|
181
|
+
bomByte: s.bomByte,
|
|
182
|
+
mixedLineEndings: s.mixedLineEndings,
|
|
183
|
+
promptScoped: promptScoped.has(s.path)
|
|
184
|
+
}));
|
|
185
|
+
const diagnostics = runRules(inputs, {
|
|
186
|
+
settings: {
|
|
187
|
+
...config.disable !== void 0 ? { disable: config.disable } : {},
|
|
188
|
+
...config.rules !== void 0 ? { rules: config.rules } : {}
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
const exitCode = diagnosticsExitCode(diagnostics, { strict: args.strict === true });
|
|
192
|
+
let cost;
|
|
193
|
+
if (args.cost !== false) {
|
|
194
|
+
try {
|
|
195
|
+
const normalizedTextByFile = new Map(resolution.sources.map((s) => [s.path, s.normalized]));
|
|
196
|
+
const estimate = estimateCosts(diagnostics, normalizedTextByFile);
|
|
197
|
+
if (estimate.perDiagnostic.length !== diagnostics.length) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
`cost estimate misaligned: ${estimate.perDiagnostic.length} per-diagnostic entries for ${diagnostics.length} diagnostics`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
const normalizedCharsByFile = {};
|
|
203
|
+
for (const path of [...normalizedTextByFile.keys()].sort()) {
|
|
204
|
+
normalizedCharsByFile[path] = (normalizedTextByFile.get(path) ?? "").length;
|
|
205
|
+
}
|
|
206
|
+
cost = {
|
|
207
|
+
costModelVersion: COST_MODEL_VERSION,
|
|
208
|
+
pricing: { tableVersion: PRICING_TABLE_VERSION, asOf: PRICING_AS_OF, referenceModel: REFERENCE_MODEL },
|
|
209
|
+
perDiagnostic: estimate.perDiagnostic,
|
|
210
|
+
totals: estimate.totals,
|
|
211
|
+
normalizedCharsByFile
|
|
212
|
+
};
|
|
213
|
+
} catch (err) {
|
|
214
|
+
resolution.warnings.push(costUnavailableWarning(err));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
version: CACHELINT_VERSION,
|
|
219
|
+
configPath,
|
|
220
|
+
packLoaded,
|
|
221
|
+
diagnostics,
|
|
222
|
+
warnings: resolution.warnings,
|
|
223
|
+
exitCode,
|
|
224
|
+
...cost !== void 0 ? { cost } : {}
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function resolvePack(pack) {
|
|
228
|
+
if (pack === void 0) return null;
|
|
229
|
+
requirePro("pack", `the --pack ${pack} bug-pattern pack`);
|
|
230
|
+
if (!KNOWN_PACK_IDS.has(pack)) {
|
|
231
|
+
throw new ConfigError({
|
|
232
|
+
code: "PRO_UNKNOWN_PACK",
|
|
233
|
+
message: `unknown pack "${pack}"`,
|
|
234
|
+
hint: `Known packs: ${[...KNOWN_PACK_IDS].join(", ")}.`
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
return pack;
|
|
238
|
+
}
|
|
239
|
+
function computePromptScopedSet(cliPaths, config, root) {
|
|
240
|
+
const set = /* @__PURE__ */ new Set();
|
|
241
|
+
for (const p of cliPaths) {
|
|
242
|
+
if (!GLOB_CHARS.test(p)) {
|
|
243
|
+
set.add(toPosixRelative(pathResolve(root, p), root));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (config.prompt_globs && config.prompt_globs.length > 0) {
|
|
247
|
+
const r = resolveSources({ paths: config.prompt_globs, projectRoot: root, required: false });
|
|
248
|
+
for (const s of r.sources) set.add(s.path);
|
|
249
|
+
}
|
|
250
|
+
return set;
|
|
251
|
+
}
|
|
252
|
+
var COLORS = {
|
|
253
|
+
red: "\x1B[31m",
|
|
254
|
+
yellow: "\x1B[33m",
|
|
255
|
+
dim: "\x1B[2m",
|
|
256
|
+
bold: "\x1B[1m",
|
|
257
|
+
reset: "\x1B[0m"
|
|
258
|
+
};
|
|
259
|
+
function renderLintHuman(result, opts = {}) {
|
|
260
|
+
const color = opts.color === true;
|
|
261
|
+
const c = (code, s) => color ? code + s + COLORS.reset : s;
|
|
262
|
+
const lines = [];
|
|
263
|
+
if (result.packLoaded !== null) {
|
|
264
|
+
lines.push(c(COLORS.dim, `note pack "${result.packLoaded}" loaded (no additional rules in this build yet)`));
|
|
265
|
+
}
|
|
266
|
+
for (const w of result.warnings) {
|
|
267
|
+
const where = w.path ? `${w.path}: ` : "";
|
|
268
|
+
lines.push(`${c(COLORS.dim, "note")} ${where}${w.message}`);
|
|
269
|
+
}
|
|
270
|
+
if ((result.warnings.length > 0 || result.packLoaded !== null) && result.diagnostics.length > 0) lines.push("");
|
|
271
|
+
const label = result.cost !== void 0 ? costLabel({ model: result.cost.pricing.referenceModel }, result.cost.pricing.asOf) : "";
|
|
272
|
+
let errors = 0;
|
|
273
|
+
let warns = 0;
|
|
274
|
+
for (let i = 0; i < result.diagnostics.length; i++) {
|
|
275
|
+
const d = result.diagnostics[i];
|
|
276
|
+
if (d.severity === "error") errors++;
|
|
277
|
+
else warns++;
|
|
278
|
+
const level = d.severity === "error" ? c(COLORS.red, "error ") : c(COLORS.yellow, "warning");
|
|
279
|
+
lines.push(`${c(COLORS.bold, `${d.file}:${d.line}:${d.column}`)} ${level} ${d.rule} ${d.message}`);
|
|
280
|
+
if (d.snippet) lines.push(` ${c(COLORS.dim, "\u203A " + d.snippet)}`);
|
|
281
|
+
if (d.fixHint) lines.push(` ${c(COLORS.dim, "fix:")} ${d.fixHint}`);
|
|
282
|
+
if (d.whyUrl) lines.push(` ${c(COLORS.dim, "why:")} ${d.whyUrl}`);
|
|
283
|
+
const dc = result.cost?.perDiagnostic[i] ?? null;
|
|
284
|
+
if (dc !== null) {
|
|
285
|
+
if (!dc.countsTowardTotal) {
|
|
286
|
+
lines.push(` ${c(COLORS.dim, COST_LINE_OVERLAPPED)}`);
|
|
287
|
+
} else if (dc.volatility === "per-call") {
|
|
288
|
+
lines.push(` ${c(COLORS.dim, costLineInvalidates(dc.fromHereTokens))}`);
|
|
289
|
+
const usd = dc.wastedUsdPer1kCalls !== void 0 ? formatUsdPer1k(dc.wastedUsdPer1kCalls) : null;
|
|
290
|
+
if (usd !== null) lines.push(` ${c(COLORS.dim, costLineWasted(usd, label))}`);
|
|
291
|
+
} else {
|
|
292
|
+
lines.push(` ${c(COLORS.dim, costLineAtRisk(dc.fromHereTokens))}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (result.diagnostics.length === 0) {
|
|
297
|
+
lines.push(c(COLORS.dim, "cachelint: no problems found."));
|
|
298
|
+
} else {
|
|
299
|
+
lines.push("");
|
|
300
|
+
const parts = [];
|
|
301
|
+
if (errors > 0) parts.push(`${errors} error${errors === 1 ? "" : "s"}`);
|
|
302
|
+
if (warns > 0) parts.push(`${warns} warning${warns === 1 ? "" : "s"}`);
|
|
303
|
+
const total = result.diagnostics.length;
|
|
304
|
+
lines.push(`cachelint: ${total} problem${total === 1 ? "" : "s"} (${parts.join(", ")})`);
|
|
305
|
+
if (result.cost !== void 0) {
|
|
306
|
+
const summary = costSummary(result.cost.totals, label);
|
|
307
|
+
if (summary !== null) lines.push(`cachelint: ${summary}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return redactSecrets(lines.join("\n"));
|
|
311
|
+
}
|
|
312
|
+
function renderLintJson(result) {
|
|
313
|
+
return redactSecrets(
|
|
314
|
+
JSON.stringify(
|
|
315
|
+
{
|
|
316
|
+
version: result.version,
|
|
317
|
+
configPath: result.configPath,
|
|
318
|
+
packLoaded: result.packLoaded,
|
|
319
|
+
exitCode: result.exitCode,
|
|
320
|
+
diagnostics: result.diagnostics,
|
|
321
|
+
warnings: result.warnings,
|
|
322
|
+
// R4: present only when cost was computed — `--no-cost` omits the
|
|
323
|
+
// block by construction (it's absent from the result itself).
|
|
324
|
+
...result.cost !== void 0 ? { cost: result.cost } : {}
|
|
325
|
+
},
|
|
326
|
+
null,
|
|
327
|
+
2
|
|
328
|
+
)
|
|
329
|
+
) + "\n";
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export {
|
|
333
|
+
KNOWN_PACK_IDS,
|
|
334
|
+
usdPer1kCalls,
|
|
335
|
+
heuristicTokens,
|
|
336
|
+
estimateCosts,
|
|
337
|
+
lint,
|
|
338
|
+
renderLintHuman,
|
|
339
|
+
renderLintJson
|
|
340
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// src/rules/ids.ts
|
|
2
|
+
var RULE_IDS = ["R001", "R002", "R003", "R004", "R005"];
|
|
3
|
+
var DEFAULT_RULE_SEVERITY = {
|
|
4
|
+
R001: "warn",
|
|
5
|
+
R002: "warn",
|
|
6
|
+
R003: "warn",
|
|
7
|
+
R004: "error",
|
|
8
|
+
R005: "error"
|
|
9
|
+
};
|
|
10
|
+
var DEFAULT_RULE_VOLATILITY = {
|
|
11
|
+
R001: "per-call",
|
|
12
|
+
R002: "per-call",
|
|
13
|
+
R003: "regeneration",
|
|
14
|
+
R004: "regeneration",
|
|
15
|
+
R005: "regeneration"
|
|
16
|
+
};
|
|
17
|
+
var RULE_SEVERITY_VALUES = ["off", "warn", "error"];
|
|
18
|
+
function isRuleId(s) {
|
|
19
|
+
return RULE_IDS.includes(s);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
RULE_IDS,
|
|
24
|
+
DEFAULT_RULE_SEVERITY,
|
|
25
|
+
DEFAULT_RULE_VOLATILITY,
|
|
26
|
+
RULE_SEVERITY_VALUES,
|
|
27
|
+
isRuleId
|
|
28
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// src/license/store.ts
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { readFileSync, writeFileSync, mkdirSync, rmSync, existsSync } from "fs";
|
|
5
|
+
var ENV_VAR = "CACHELINT_LICENSE_KEY";
|
|
6
|
+
function licenseFilePath() {
|
|
7
|
+
if (process.platform === "win32") {
|
|
8
|
+
const appData = process.env["APPDATA"] ?? join(homedir(), "AppData", "Roaming");
|
|
9
|
+
return join(appData, "cachelint", "license");
|
|
10
|
+
}
|
|
11
|
+
const xdg = process.env["XDG_CONFIG_HOME"];
|
|
12
|
+
const base = xdg !== void 0 && xdg.trim() !== "" ? xdg : join(homedir(), ".config");
|
|
13
|
+
return join(base, "cachelint", "license");
|
|
14
|
+
}
|
|
15
|
+
function readStoredLicense() {
|
|
16
|
+
const env = process.env[ENV_VAR];
|
|
17
|
+
if (env !== void 0 && env.trim() !== "") return { token: env.trim(), source: `env ${ENV_VAR}` };
|
|
18
|
+
const path = licenseFilePath();
|
|
19
|
+
if (!existsSync(path)) return null;
|
|
20
|
+
try {
|
|
21
|
+
const token = readFileSync(path, "utf8").trim();
|
|
22
|
+
if (token === "") return null;
|
|
23
|
+
return { token, source: path };
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function writeStoredLicense(token) {
|
|
29
|
+
const path = licenseFilePath();
|
|
30
|
+
mkdirSync(join(path, ".."), { recursive: true });
|
|
31
|
+
writeFileSync(path, token.trim() + "\n", { encoding: "utf8", mode: 384 });
|
|
32
|
+
return path;
|
|
33
|
+
}
|
|
34
|
+
function clearStoredLicense() {
|
|
35
|
+
const path = licenseFilePath();
|
|
36
|
+
if (!existsSync(path)) return false;
|
|
37
|
+
rmSync(path, { force: true });
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
function hasEnvLicense() {
|
|
41
|
+
const env = process.env[ENV_VAR];
|
|
42
|
+
return env !== void 0 && env.trim() !== "";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export {
|
|
46
|
+
licenseFilePath,
|
|
47
|
+
readStoredLicense,
|
|
48
|
+
writeStoredLicense,
|
|
49
|
+
clearStoredLicense,
|
|
50
|
+
hasEnvLicense
|
|
51
|
+
};
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import {
|
|
2
|
+
bundleHash,
|
|
3
|
+
resolveSources
|
|
4
|
+
} from "./chunk-MW7BXKPR.js";
|
|
5
|
+
import {
|
|
6
|
+
CACHELINT_VERSION,
|
|
7
|
+
NORMALIZATION_POLICY_VERSION,
|
|
8
|
+
RULE_TABLE_VERSION
|
|
9
|
+
} from "./chunk-WPJYUQMU.js";
|
|
10
|
+
import {
|
|
11
|
+
LockCorruptError,
|
|
12
|
+
SourceError
|
|
13
|
+
} from "./chunk-XKRPGMZT.js";
|
|
14
|
+
|
|
15
|
+
// src/commands/bundles.ts
|
|
16
|
+
import { resolve as pathResolve } from "path";
|
|
17
|
+
function computeBundles(args) {
|
|
18
|
+
const root = pathResolve(args.projectRoot);
|
|
19
|
+
const cfg = args.config;
|
|
20
|
+
const warnings = [];
|
|
21
|
+
const byName = /* @__PURE__ */ new Map();
|
|
22
|
+
const add = (b) => {
|
|
23
|
+
if (byName.has(b.name)) {
|
|
24
|
+
throw new SourceError({
|
|
25
|
+
code: "BUNDLE_NAME_COLLISION",
|
|
26
|
+
message: `two bundles resolve to the same name "${b.name}"`,
|
|
27
|
+
hint: "Rename a config bundle, or don't list a file path that collides with a config bundle name."
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
byName.set(b.name, b);
|
|
31
|
+
};
|
|
32
|
+
const cliPaths = args.paths ?? [];
|
|
33
|
+
const oneFilePaths = cliPaths.length > 0 ? [...cliPaths] : cfg.prompt_globs ?? [];
|
|
34
|
+
if (oneFilePaths.length > 0) {
|
|
35
|
+
const r = resolveSources({
|
|
36
|
+
paths: oneFilePaths,
|
|
37
|
+
projectRoot: root,
|
|
38
|
+
required: false,
|
|
39
|
+
...cfg.exclude !== void 0 ? { exclude: cfg.exclude } : {},
|
|
40
|
+
...cfg.max_file_bytes !== void 0 ? { maxFileBytes: cfg.max_file_bytes } : {},
|
|
41
|
+
...cfg.max_total_bytes !== void 0 ? { maxTotalBytes: cfg.max_total_bytes } : {}
|
|
42
|
+
});
|
|
43
|
+
warnings.push(...r.warnings);
|
|
44
|
+
for (const s of r.sources) {
|
|
45
|
+
add({ name: s.path, files: [{ path: s.path, sha256: s.sha256 }], stablePrefixHash: bundleHash([s.sha256]) });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
for (const b of cfg.bundles ?? []) {
|
|
49
|
+
const files = [];
|
|
50
|
+
for (const entry of b.files) {
|
|
51
|
+
const r = resolveSources({
|
|
52
|
+
paths: [entry],
|
|
53
|
+
projectRoot: root,
|
|
54
|
+
required: true,
|
|
55
|
+
name: b.name,
|
|
56
|
+
...cfg.exclude !== void 0 ? { exclude: cfg.exclude } : {},
|
|
57
|
+
...cfg.max_file_bytes !== void 0 ? { maxFileBytes: cfg.max_file_bytes } : {},
|
|
58
|
+
...cfg.max_total_bytes !== void 0 ? { maxTotalBytes: cfg.max_total_bytes } : {}
|
|
59
|
+
});
|
|
60
|
+
warnings.push(...r.warnings);
|
|
61
|
+
if (r.sources.length === 0) {
|
|
62
|
+
throw new SourceError({
|
|
63
|
+
code: "BUNDLE_ENTRY_EMPTY",
|
|
64
|
+
message: `bundle "${b.name}": entry "${entry}" matched no readable text files`,
|
|
65
|
+
hint: "Fix the path/glob, or remove it from the bundle."
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
for (const s of r.sources) files.push({ path: s.path, sha256: s.sha256 });
|
|
69
|
+
}
|
|
70
|
+
add({ name: b.name, files, stablePrefixHash: bundleHash(files.map((f) => f.sha256)) });
|
|
71
|
+
}
|
|
72
|
+
if (byName.size === 0) {
|
|
73
|
+
throw new SourceError({
|
|
74
|
+
code: "NO_BUNDLES",
|
|
75
|
+
message: "nothing to hash: pass file/glob arguments, or set `prompt_globs` / `bundles` in cachelint.config",
|
|
76
|
+
hint: 'e.g. `cachelint hash prompts/system.md`, or add `bundles: [{ name: "system", files: [...] }]`.'
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return { bundles: [...byName.values()].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0), warnings };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/lockfile/format.ts
|
|
83
|
+
import { writeFileSync, renameSync, rmSync, readFileSync, mkdirSync, existsSync } from "fs";
|
|
84
|
+
import { dirname, basename, join } from "path";
|
|
85
|
+
var LOCKFILE_FORMAT_VERSION = 1;
|
|
86
|
+
var DEFAULT_LOCKFILE_NAME = "cachelint.lock";
|
|
87
|
+
function makeLockfile(bundles) {
|
|
88
|
+
const sorted = [...bundles].sort((a, b) => cmpCodepoint(a.name, b.name));
|
|
89
|
+
return {
|
|
90
|
+
version: LOCKFILE_FORMAT_VERSION,
|
|
91
|
+
cachelintVersion: CACHELINT_VERSION,
|
|
92
|
+
normalizationPolicyVersion: NORMALIZATION_POLICY_VERSION,
|
|
93
|
+
ruleTableVersion: RULE_TABLE_VERSION,
|
|
94
|
+
bundles: sorted
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function canonicalJsonStringify(value) {
|
|
98
|
+
const json = JSON.stringify(sortKeysDeep(value), jsonReplacerForNumbers, 2);
|
|
99
|
+
let out = "";
|
|
100
|
+
for (let i = 0; i < json.length; i++) {
|
|
101
|
+
const code = json.charCodeAt(i);
|
|
102
|
+
out += code <= 127 ? json.charAt(i) : "\\u" + code.toString(16).padStart(4, "0");
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
function serializeLockfile(lock) {
|
|
107
|
+
return canonicalJsonStringify(lock);
|
|
108
|
+
}
|
|
109
|
+
function jsonReplacerForNumbers(_key, val) {
|
|
110
|
+
if (typeof val === "number" && !Number.isInteger(val)) {
|
|
111
|
+
throw new LockCorruptError(`refusing to serialize a non-integer number (${val}) into the lockfile`);
|
|
112
|
+
}
|
|
113
|
+
return val;
|
|
114
|
+
}
|
|
115
|
+
function sortKeysDeep(v) {
|
|
116
|
+
if (Array.isArray(v)) return v.map(sortKeysDeep);
|
|
117
|
+
if (v !== null && typeof v === "object") {
|
|
118
|
+
const src = v;
|
|
119
|
+
const out = {};
|
|
120
|
+
for (const k of Object.keys(src).sort(cmpCodepoint)) out[k] = sortKeysDeep(src[k]);
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
return v;
|
|
124
|
+
}
|
|
125
|
+
function cmpCodepoint(a, b) {
|
|
126
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
127
|
+
}
|
|
128
|
+
function parseLockfile(text, label = DEFAULT_LOCKFILE_NAME) {
|
|
129
|
+
let data;
|
|
130
|
+
try {
|
|
131
|
+
data = JSON.parse(text);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
throw new LockCorruptError(
|
|
134
|
+
`${label} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
135
|
+
"Re-generate it with `cachelint hash \u2026` and commit the result."
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
139
|
+
throw new LockCorruptError(`${label} is not an object`);
|
|
140
|
+
}
|
|
141
|
+
const o = data;
|
|
142
|
+
const version = o["version"];
|
|
143
|
+
if (typeof version !== "number") throw new LockCorruptError(`${label}: missing/invalid "version"`);
|
|
144
|
+
if (version !== LOCKFILE_FORMAT_VERSION) {
|
|
145
|
+
throw new LockCorruptError(
|
|
146
|
+
`${label}: lockfile format version ${version} is not supported by this cachelint (expects ${LOCKFILE_FORMAT_VERSION})`,
|
|
147
|
+
"Upgrade or downgrade cachelint, or re-run `cachelint hash` with this version and commit."
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
const cachelintVersion = typeof o["cachelintVersion"] === "string" ? o["cachelintVersion"] : "";
|
|
151
|
+
const npv = o["normalizationPolicyVersion"];
|
|
152
|
+
const rtv = o["ruleTableVersion"];
|
|
153
|
+
if (typeof npv !== "number" || typeof rtv !== "number") {
|
|
154
|
+
throw new LockCorruptError(`${label}: missing/invalid policy version fields`);
|
|
155
|
+
}
|
|
156
|
+
const bundlesRaw = o["bundles"];
|
|
157
|
+
if (!Array.isArray(bundlesRaw)) throw new LockCorruptError(`${label}: "bundles" must be an array`);
|
|
158
|
+
const bundles = bundlesRaw.map((b, i) => parseBundle(b, `${label}: bundles[${i}]`));
|
|
159
|
+
return {
|
|
160
|
+
version,
|
|
161
|
+
cachelintVersion,
|
|
162
|
+
normalizationPolicyVersion: npv,
|
|
163
|
+
ruleTableVersion: rtv,
|
|
164
|
+
bundles
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function parseBundle(b, where) {
|
|
168
|
+
if (typeof b !== "object" || b === null || Array.isArray(b)) throw new LockCorruptError(`${where}: not an object`);
|
|
169
|
+
const o = b;
|
|
170
|
+
const name = o["name"];
|
|
171
|
+
const stablePrefixHash = o["stablePrefixHash"];
|
|
172
|
+
const filesRaw = o["files"];
|
|
173
|
+
if (typeof name !== "string" || name.length === 0) throw new LockCorruptError(`${where}: missing "name"`);
|
|
174
|
+
if (!isSha256Hex(stablePrefixHash)) throw new LockCorruptError(`${where}: missing/invalid "stablePrefixHash"`);
|
|
175
|
+
if (!Array.isArray(filesRaw)) throw new LockCorruptError(`${where}: "files" must be an array`);
|
|
176
|
+
const files = filesRaw.map((f, i) => {
|
|
177
|
+
if (typeof f !== "object" || f === null) throw new LockCorruptError(`${where}.files[${i}]: not an object`);
|
|
178
|
+
const fo = f;
|
|
179
|
+
const p = fo["path"];
|
|
180
|
+
const h = fo["sha256"];
|
|
181
|
+
if (typeof p !== "string" || p.length === 0) throw new LockCorruptError(`${where}.files[${i}]: missing "path"`);
|
|
182
|
+
if (!isSha256Hex(h)) throw new LockCorruptError(`${where}.files[${i}]: missing/invalid "sha256"`);
|
|
183
|
+
return { path: p, sha256: h };
|
|
184
|
+
});
|
|
185
|
+
return { name, files, stablePrefixHash };
|
|
186
|
+
}
|
|
187
|
+
function isSha256Hex(v) {
|
|
188
|
+
return typeof v === "string" && /^[0-9a-f]{64}$/.test(v);
|
|
189
|
+
}
|
|
190
|
+
function readLockfileIfExists(path) {
|
|
191
|
+
let text;
|
|
192
|
+
try {
|
|
193
|
+
text = readFileSync(path, "utf8");
|
|
194
|
+
} catch (err) {
|
|
195
|
+
if (err?.code === "ENOENT") return null;
|
|
196
|
+
throw new LockCorruptError(`cannot read ${basename(path)}: ${err instanceof Error ? err.message : String(err)}`);
|
|
197
|
+
}
|
|
198
|
+
return parseLockfile(text, basename(path));
|
|
199
|
+
}
|
|
200
|
+
function writeLockfileAtomic(path, lock) {
|
|
201
|
+
const text = serializeLockfile(lock);
|
|
202
|
+
const dir = dirname(path);
|
|
203
|
+
mkdirSync(dir, { recursive: true });
|
|
204
|
+
const tmp = join(dir, `.${basename(path)}.${process.pid}.tmp`);
|
|
205
|
+
const bak = join(dir, `.${basename(path)}.${process.pid}.bak`);
|
|
206
|
+
writeFileSync(tmp, text, { encoding: "utf8" });
|
|
207
|
+
try {
|
|
208
|
+
renameSync(tmp, path);
|
|
209
|
+
return text;
|
|
210
|
+
} catch {
|
|
211
|
+
}
|
|
212
|
+
let movedAside = false;
|
|
213
|
+
try {
|
|
214
|
+
if (existsSync(path)) {
|
|
215
|
+
renameSync(path, bak);
|
|
216
|
+
movedAside = true;
|
|
217
|
+
}
|
|
218
|
+
renameSync(tmp, path);
|
|
219
|
+
if (movedAside) rmSync(bak, { force: true });
|
|
220
|
+
return text;
|
|
221
|
+
} catch (err) {
|
|
222
|
+
if (movedAside && existsSync(bak) && !existsSync(path)) {
|
|
223
|
+
try {
|
|
224
|
+
renameSync(bak, path);
|
|
225
|
+
} catch {
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
rmSync(tmp, { force: true });
|
|
229
|
+
throw new LockCorruptError(
|
|
230
|
+
`could not write ${basename(path)} atomically: ${err instanceof Error ? err.message : String(err)}`,
|
|
231
|
+
"Check write permissions on the directory; the previous cachelint.lock (if any) was preserved."
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export {
|
|
237
|
+
computeBundles,
|
|
238
|
+
LOCKFILE_FORMAT_VERSION,
|
|
239
|
+
DEFAULT_LOCKFILE_NAME,
|
|
240
|
+
makeLockfile,
|
|
241
|
+
canonicalJsonStringify,
|
|
242
|
+
serializeLockfile,
|
|
243
|
+
parseLockfile,
|
|
244
|
+
readLockfileIfExists,
|
|
245
|
+
writeLockfileAtomic
|
|
246
|
+
};
|