trimference 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/BENCHMARK.md +39 -0
- package/CHANGELOG.md +41 -0
- package/CONTRIBUTING.md +43 -0
- package/DEMO.md +106 -0
- package/LICENSE +21 -0
- package/README.md +268 -0
- package/SECURITY.md +17 -0
- package/action.yml +182 -0
- package/dist/baseline.d.ts +13 -0
- package/dist/baseline.d.ts.map +1 -0
- package/dist/baseline.js +23 -0
- package/dist/baseline.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +245 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +31 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +127 -0
- package/dist/config.js.map +1 -0
- package/dist/ecoLLM.d.ts +33 -0
- package/dist/ecoLLM.d.ts.map +1 -0
- package/dist/ecoLLM.js +98 -0
- package/dist/ecoLLM.js.map +1 -0
- package/dist/impact.d.ts +22 -0
- package/dist/impact.d.ts.map +1 -0
- package/dist/impact.js +80 -0
- package/dist/impact.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/models.d.ts +21 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +58 -0
- package/dist/models.js.map +1 -0
- package/dist/prComment.d.ts +36 -0
- package/dist/prComment.d.ts.map +1 -0
- package/dist/prComment.js +45 -0
- package/dist/prComment.js.map +1 -0
- package/dist/reporters/impactTracker.d.ts +21 -0
- package/dist/reporters/impactTracker.d.ts.map +1 -0
- package/dist/reporters/impactTracker.js +54 -0
- package/dist/reporters/impactTracker.js.map +1 -0
- package/dist/reporters/jsonReporter.d.ts +4 -0
- package/dist/reporters/jsonReporter.d.ts.map +1 -0
- package/dist/reporters/jsonReporter.js +34 -0
- package/dist/reporters/jsonReporter.js.map +1 -0
- package/dist/reporters/markdownReporter.d.ts +4 -0
- package/dist/reporters/markdownReporter.d.ts.map +1 -0
- package/dist/reporters/markdownReporter.js +97 -0
- package/dist/reporters/markdownReporter.js.map +1 -0
- package/dist/reporters/sarifReporter.d.ts +4 -0
- package/dist/reporters/sarifReporter.d.ts.map +1 -0
- package/dist/reporters/sarifReporter.js +63 -0
- package/dist/reporters/sarifReporter.js.map +1 -0
- package/dist/reporters/terminalReporter.d.ts +10 -0
- package/dist/reporters/terminalReporter.d.ts.map +1 -0
- package/dist/reporters/terminalReporter.js +80 -0
- package/dist/reporters/terminalReporter.js.map +1 -0
- package/dist/rules/agentLoopWithoutBudget.d.ts +3 -0
- package/dist/rules/agentLoopWithoutBudget.d.ts.map +1 -0
- package/dist/rules/agentLoopWithoutBudget.js +105 -0
- package/dist/rules/agentLoopWithoutBudget.js.map +1 -0
- package/dist/rules/expensiveModelSimpleTask.d.ts +3 -0
- package/dist/rules/expensiveModelSimpleTask.d.ts.map +1 -0
- package/dist/rules/expensiveModelSimpleTask.js +119 -0
- package/dist/rules/expensiveModelSimpleTask.js.map +1 -0
- package/dist/rules/frequentCron.d.ts +3 -0
- package/dist/rules/frequentCron.d.ts.map +1 -0
- package/dist/rules/frequentCron.js +64 -0
- package/dist/rules/frequentCron.js.map +1 -0
- package/dist/rules/helpers.d.ts +65 -0
- package/dist/rules/helpers.d.ts.map +1 -0
- package/dist/rules/helpers.js +383 -0
- package/dist/rules/helpers.js.map +1 -0
- package/dist/rules/hugeContext.d.ts +3 -0
- package/dist/rules/hugeContext.d.ts.map +1 -0
- package/dist/rules/hugeContext.js +74 -0
- package/dist/rules/hugeContext.js.map +1 -0
- package/dist/rules/imageGenerationLoop.d.ts +3 -0
- package/dist/rules/imageGenerationLoop.d.ts.map +1 -0
- package/dist/rules/imageGenerationLoop.js +72 -0
- package/dist/rules/imageGenerationLoop.js.map +1 -0
- package/dist/rules/index.d.ts +14 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +24 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/missingRateLimit.d.ts +3 -0
- package/dist/rules/missingRateLimit.d.ts.map +1 -0
- package/dist/rules/missingRateLimit.js +78 -0
- package/dist/rules/missingRateLimit.js.map +1 -0
- package/dist/rules/noLlmCache.d.ts +3 -0
- package/dist/rules/noLlmCache.d.ts.map +1 -0
- package/dist/rules/noLlmCache.js +71 -0
- package/dist/rules/noLlmCache.js.map +1 -0
- package/dist/rules/noTokenLimit.d.ts +3 -0
- package/dist/rules/noTokenLimit.d.ts.map +1 -0
- package/dist/rules/noTokenLimit.js +59 -0
- package/dist/rules/noTokenLimit.js.map +1 -0
- package/dist/rules/repeatedEmbeddings.d.ts +3 -0
- package/dist/rules/repeatedEmbeddings.d.ts.map +1 -0
- package/dist/rules/repeatedEmbeddings.js +81 -0
- package/dist/rules/repeatedEmbeddings.js.map +1 -0
- package/dist/rules/sequentialLlmCalls.d.ts +3 -0
- package/dist/rules/sequentialLlmCalls.d.ts.map +1 -0
- package/dist/rules/sequentialLlmCalls.js +69 -0
- package/dist/rules/sequentialLlmCalls.js.map +1 -0
- package/dist/scanner.d.ts +23 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +217 -0
- package/dist/scanner.js.map +1 -0
- package/dist/types.d.ts +93 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -0
- package/package.json +75 -0
package/action.yml
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
name: "Trimference"
|
|
2
|
+
description: "Review LLM application code for missing efficiency safeguards."
|
|
3
|
+
author: "Trimference contributors"
|
|
4
|
+
branding:
|
|
5
|
+
icon: "wind"
|
|
6
|
+
color: "green"
|
|
7
|
+
|
|
8
|
+
inputs:
|
|
9
|
+
path:
|
|
10
|
+
description: "Path to scan"
|
|
11
|
+
required: false
|
|
12
|
+
default: "."
|
|
13
|
+
min-severity:
|
|
14
|
+
description: "Minimum severity to report (low | medium | high)"
|
|
15
|
+
required: false
|
|
16
|
+
default: "low"
|
|
17
|
+
format:
|
|
18
|
+
description: "Report format (markdown | json)"
|
|
19
|
+
required: false
|
|
20
|
+
default: "markdown"
|
|
21
|
+
comment:
|
|
22
|
+
description: "Post or update a Trimference comment on pull requests"
|
|
23
|
+
required: false
|
|
24
|
+
default: "false"
|
|
25
|
+
baseline:
|
|
26
|
+
description: "Optional Trimference baseline path, relative to the scanned path"
|
|
27
|
+
required: false
|
|
28
|
+
default: ""
|
|
29
|
+
fail-on:
|
|
30
|
+
description: "Fail after reporting at this severity (none | low | medium | high)"
|
|
31
|
+
required: false
|
|
32
|
+
default: "none"
|
|
33
|
+
changed-since:
|
|
34
|
+
description: "Optional local Git ref; scan only files changed since it"
|
|
35
|
+
required: false
|
|
36
|
+
default: ""
|
|
37
|
+
|
|
38
|
+
runs:
|
|
39
|
+
using: "composite"
|
|
40
|
+
steps:
|
|
41
|
+
- name: Run Trimference
|
|
42
|
+
id: trimference
|
|
43
|
+
shell: bash
|
|
44
|
+
env:
|
|
45
|
+
INPUT_PATH: ${{ inputs.path }}
|
|
46
|
+
INPUT_MIN_SEVERITY: ${{ inputs.min-severity }}
|
|
47
|
+
INPUT_FORMAT: ${{ inputs.format }}
|
|
48
|
+
INPUT_BASELINE: ${{ inputs.baseline }}
|
|
49
|
+
INPUT_CHANGED_SINCE: ${{ inputs.changed-since }}
|
|
50
|
+
run: |
|
|
51
|
+
REPORT="${RUNNER_TEMP:-.}/trimference-report.md"
|
|
52
|
+
|
|
53
|
+
# Always generate a Markdown report file for the job summary and any PR
|
|
54
|
+
# comment. Markdown reports include every finding.
|
|
55
|
+
EXTRA_ARGS=()
|
|
56
|
+
if [ -n "$INPUT_BASELINE" ]; then
|
|
57
|
+
EXTRA_ARGS+=(--baseline "$INPUT_BASELINE")
|
|
58
|
+
fi
|
|
59
|
+
if [ -n "$INPUT_CHANGED_SINCE" ]; then
|
|
60
|
+
EXTRA_ARGS+=(--changed-since "$INPUT_CHANGED_SINCE")
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
npx --yes trimference@0.3.0 scan \
|
|
64
|
+
--path "$INPUT_PATH" \
|
|
65
|
+
--min-severity "$INPUT_MIN_SEVERITY" \
|
|
66
|
+
--markdown \
|
|
67
|
+
--output "$REPORT" \
|
|
68
|
+
"${EXTRA_ARGS[@]}"
|
|
69
|
+
|
|
70
|
+
# The CLI skips writing the file when there are no scannable files;
|
|
71
|
+
# fall back to a minimal report so the summary and comment steps work.
|
|
72
|
+
if [ ! -f "$REPORT" ]; then
|
|
73
|
+
printf '# Trimference Report\n\nNo scannable files were found under `%s`.\n' \
|
|
74
|
+
"$INPUT_PATH" > "$REPORT"
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# Print the report to the workflow log in the requested display format.
|
|
78
|
+
if [ "$INPUT_FORMAT" = "json" ]; then
|
|
79
|
+
npx --yes trimference@0.3.0 scan \
|
|
80
|
+
--path "$INPUT_PATH" \
|
|
81
|
+
--min-severity "$INPUT_MIN_SEVERITY" \
|
|
82
|
+
--json \
|
|
83
|
+
"${EXTRA_ARGS[@]}" || true
|
|
84
|
+
else
|
|
85
|
+
cat "$REPORT"
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
# Publish the report to the job summary.
|
|
89
|
+
if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
|
|
90
|
+
cat "$REPORT" >> "$GITHUB_STEP_SUMMARY"
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
echo "report-path=$REPORT" >> "$GITHUB_OUTPUT"
|
|
94
|
+
|
|
95
|
+
- name: Post or update PR comment
|
|
96
|
+
if: ${{ inputs.comment == 'true' && github.event_name == 'pull_request' }}
|
|
97
|
+
uses: actions/github-script@v7
|
|
98
|
+
env:
|
|
99
|
+
REPORT_PATH: ${{ steps.trimference.outputs.report-path }}
|
|
100
|
+
with:
|
|
101
|
+
github-token: ${{ github.token }}
|
|
102
|
+
script: |
|
|
103
|
+
// Mirrors src/prComment.ts (buildCommentBody). Keep the two in sync.
|
|
104
|
+
const fs = require("fs");
|
|
105
|
+
const MARKER = "<!-- trimference-report -->";
|
|
106
|
+
const LEGACY_MARKER = "<!-- ecolint-ai-report -->";
|
|
107
|
+
const MAX_LENGTH = 60000;
|
|
108
|
+
|
|
109
|
+
const report = fs.readFileSync(process.env.REPORT_PATH, "utf8").trimEnd();
|
|
110
|
+
|
|
111
|
+
let body;
|
|
112
|
+
if (report.length <= MAX_LENGTH) {
|
|
113
|
+
body = `${MARKER}\n${report}\n`;
|
|
114
|
+
} else {
|
|
115
|
+
const slice = report.slice(0, MAX_LENGTH);
|
|
116
|
+
const lastNewline = slice.lastIndexOf("\n");
|
|
117
|
+
const truncated = (lastNewline > 0 ? slice.slice(0, lastNewline) : slice).trimEnd();
|
|
118
|
+
const note =
|
|
119
|
+
"> ⚠️ This report was truncated to fit a PR comment. " +
|
|
120
|
+
"See the full Trimference report in the workflow **job summary**.";
|
|
121
|
+
body = `${MARKER}\n${truncated}\n\n---\n\n${note}\n`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const { owner, repo } = context.repo;
|
|
125
|
+
const issue_number = context.issue.number;
|
|
126
|
+
|
|
127
|
+
const comments = await github.paginate(github.rest.issues.listComments, {
|
|
128
|
+
owner,
|
|
129
|
+
repo,
|
|
130
|
+
issue_number,
|
|
131
|
+
per_page: 100,
|
|
132
|
+
});
|
|
133
|
+
const existing = comments.find(
|
|
134
|
+
(c) => c.body && (c.body.includes(MARKER) || c.body.includes(LEGACY_MARKER)),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
if (existing) {
|
|
138
|
+
await github.rest.issues.updateComment({
|
|
139
|
+
owner,
|
|
140
|
+
repo,
|
|
141
|
+
comment_id: existing.id,
|
|
142
|
+
body,
|
|
143
|
+
});
|
|
144
|
+
} else {
|
|
145
|
+
await github.rest.issues.createComment({
|
|
146
|
+
owner,
|
|
147
|
+
repo,
|
|
148
|
+
issue_number,
|
|
149
|
+
body,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
- name: PR comment skipped (not a pull request)
|
|
154
|
+
if: ${{ inputs.comment == 'true' && github.event_name != 'pull_request' }}
|
|
155
|
+
shell: bash
|
|
156
|
+
run: |
|
|
157
|
+
echo "Trimference: PR comments are only available on pull_request events." \
|
|
158
|
+
"The report was written to the job summary instead."
|
|
159
|
+
|
|
160
|
+
- name: Enforce Trimference threshold
|
|
161
|
+
if: ${{ inputs.fail-on != 'none' }}
|
|
162
|
+
shell: bash
|
|
163
|
+
env:
|
|
164
|
+
INPUT_PATH: ${{ inputs.path }}
|
|
165
|
+
INPUT_MIN_SEVERITY: ${{ inputs.min-severity }}
|
|
166
|
+
INPUT_BASELINE: ${{ inputs.baseline }}
|
|
167
|
+
INPUT_FAIL_ON: ${{ inputs.fail-on }}
|
|
168
|
+
INPUT_CHANGED_SINCE: ${{ inputs.changed-since }}
|
|
169
|
+
run: |
|
|
170
|
+
EXTRA_ARGS=()
|
|
171
|
+
if [ -n "$INPUT_BASELINE" ]; then
|
|
172
|
+
EXTRA_ARGS+=(--baseline "$INPUT_BASELINE")
|
|
173
|
+
fi
|
|
174
|
+
if [ -n "$INPUT_CHANGED_SINCE" ]; then
|
|
175
|
+
EXTRA_ARGS+=(--changed-since "$INPUT_CHANGED_SINCE")
|
|
176
|
+
fi
|
|
177
|
+
npx --yes trimference@0.3.0 scan \
|
|
178
|
+
--path "$INPUT_PATH" \
|
|
179
|
+
--min-severity "$INPUT_MIN_SEVERITY" \
|
|
180
|
+
--summary \
|
|
181
|
+
--fail-on "$INPUT_FAIL_ON" \
|
|
182
|
+
"${EXTRA_ARGS[@]}"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Finding } from "./types.js";
|
|
2
|
+
export declare const BASELINE_VERSION = 1;
|
|
3
|
+
export type TrimferenceBaseline = {
|
|
4
|
+
version: typeof BASELINE_VERSION;
|
|
5
|
+
fingerprints: string[];
|
|
6
|
+
};
|
|
7
|
+
/** @deprecated Use TrimferenceBaseline. */
|
|
8
|
+
export type EcoLintBaseline = TrimferenceBaseline;
|
|
9
|
+
/** Read and validate a Trimference baseline file. */
|
|
10
|
+
export declare function readBaseline(filePath: string): Promise<string[]>;
|
|
11
|
+
/** Write the current findings as accepted baseline debt. */
|
|
12
|
+
export declare function writeBaseline(filePath: string, findings: Finding[]): Promise<void>;
|
|
13
|
+
//# sourceMappingURL=baseline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"baseline.d.ts","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAG1C,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAElC,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,OAAO,gBAAgB,CAAC;IACjC,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB,CAAC;AAEF,2CAA2C;AAC3C,MAAM,MAAM,eAAe,GAAG,mBAAmB,CAAC;AAElD,qDAAqD;AACrD,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAatE;AAED,4DAA4D;AAC5D,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,OAAO,EAAE,GAClB,OAAO,CAAC,IAAI,CAAC,CAMf"}
|
package/dist/baseline.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import { findingFingerprint } from "./scanner.js";
|
|
3
|
+
export const BASELINE_VERSION = 1;
|
|
4
|
+
/** Read and validate a Trimference baseline file. */
|
|
5
|
+
export async function readBaseline(filePath) {
|
|
6
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
7
|
+
const parsed = JSON.parse(raw);
|
|
8
|
+
if (parsed.version !== BASELINE_VERSION ||
|
|
9
|
+
!Array.isArray(parsed.fingerprints) ||
|
|
10
|
+
!parsed.fingerprints.every((item) => typeof item === "string")) {
|
|
11
|
+
throw new Error(`Invalid baseline at ${filePath}. Expected version ${BASELINE_VERSION}.`);
|
|
12
|
+
}
|
|
13
|
+
return parsed.fingerprints;
|
|
14
|
+
}
|
|
15
|
+
/** Write the current findings as accepted baseline debt. */
|
|
16
|
+
export async function writeBaseline(filePath, findings) {
|
|
17
|
+
const baseline = {
|
|
18
|
+
version: BASELINE_VERSION,
|
|
19
|
+
fingerprints: [...new Set(findings.map(findingFingerprint))].sort(),
|
|
20
|
+
};
|
|
21
|
+
await fs.writeFile(filePath, `${JSON.stringify(baseline, null, 2)}\n`, "utf8");
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=baseline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"baseline.js","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;AAEzC,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAElD,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAUlC,qDAAqD;AACrD,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,QAAgB;IACjD,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAiC,CAAC;IAC/D,IACE,MAAM,CAAC,OAAO,KAAK,gBAAgB;QACnC,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC;QACnC,CAAC,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,EAC9D,CAAC;QACD,MAAM,IAAI,KAAK,CACb,uBAAuB,QAAQ,sBAAsB,gBAAgB,GAAG,CACzE,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC,YAAY,CAAC;AAC7B,CAAC;AAED,4DAA4D;AAC5D,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,QAAgB,EAChB,QAAmB;IAEnB,MAAM,QAAQ,GAAwB;QACpC,OAAO,EAAE,gBAAgB;QACzB,YAAY,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE;KACpE,CAAC;IACF,MAAM,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AACjF,CAAC"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { promises as fs, readFileSync } from "node:fs";
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import pc from "picocolors";
|
|
9
|
+
import { readBaseline, writeBaseline } from "./baseline.js";
|
|
10
|
+
import { SEVERITY_WEIGHT } from "./impact.js";
|
|
11
|
+
import { CONFIG_FILENAME, SAMPLE_CONFIG, loadConfig } from "./config.js";
|
|
12
|
+
import { scan } from "./scanner.js";
|
|
13
|
+
import { renderTerminalReport, } from "./reporters/terminalReporter.js";
|
|
14
|
+
import { renderJsonReport } from "./reporters/jsonReporter.js";
|
|
15
|
+
import { renderSarifReport } from "./reporters/sarifReporter.js";
|
|
16
|
+
import { renderMarkdownReport } from "./reporters/markdownReporter.js";
|
|
17
|
+
/** Read the package version so `--version` always matches package.json. */
|
|
18
|
+
function readVersion() {
|
|
19
|
+
try {
|
|
20
|
+
const pkgPath = fileURLToPath(new URL("../package.json", import.meta.url));
|
|
21
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
22
|
+
return pkg.version ?? "0.0.0";
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return "0.0.0";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const VALID_SEVERITIES = ["low", "medium", "high"];
|
|
29
|
+
const VALID_PROVIDERS = [
|
|
30
|
+
"openai",
|
|
31
|
+
"anthropic",
|
|
32
|
+
"google",
|
|
33
|
+
"mistral",
|
|
34
|
+
"unknown",
|
|
35
|
+
];
|
|
36
|
+
const execFileAsync = promisify(execFile);
|
|
37
|
+
function parseMaxFindings(value) {
|
|
38
|
+
const n = Number(value);
|
|
39
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
40
|
+
throw new Error(`Invalid --max-findings "${value}". Expected a non-negative integer (0 = show all).`);
|
|
41
|
+
}
|
|
42
|
+
return n;
|
|
43
|
+
}
|
|
44
|
+
function parseSeverity(value) {
|
|
45
|
+
const lower = value.toLowerCase();
|
|
46
|
+
if (!VALID_SEVERITIES.includes(lower)) {
|
|
47
|
+
throw new Error(`Invalid --min-severity "${value}". Expected one of: ${VALID_SEVERITIES.join(", ")}.`);
|
|
48
|
+
}
|
|
49
|
+
return lower;
|
|
50
|
+
}
|
|
51
|
+
function parseFailOn(value) {
|
|
52
|
+
if (value.toLowerCase() === "none")
|
|
53
|
+
return "none";
|
|
54
|
+
return parseSeverity(value);
|
|
55
|
+
}
|
|
56
|
+
function parseProvider(value) {
|
|
57
|
+
const lower = value.toLowerCase();
|
|
58
|
+
if (!VALID_PROVIDERS.includes(lower)) {
|
|
59
|
+
throw new Error(`Invalid --provider "${value}". Expected one of: ${VALID_PROVIDERS.join(", ")}.`);
|
|
60
|
+
}
|
|
61
|
+
return lower;
|
|
62
|
+
}
|
|
63
|
+
function renderFor(format, result, terminalOptions = {}) {
|
|
64
|
+
switch (format) {
|
|
65
|
+
case "json":
|
|
66
|
+
return renderJsonReport(result);
|
|
67
|
+
case "markdown":
|
|
68
|
+
return renderMarkdownReport(result);
|
|
69
|
+
case "sarif":
|
|
70
|
+
return renderSarifReport(result);
|
|
71
|
+
case "terminal":
|
|
72
|
+
return renderTerminalReport(result, terminalOptions);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function runScan(options) {
|
|
76
|
+
const targetPath = options.path ?? ".";
|
|
77
|
+
// Fail clearly if the path does not exist.
|
|
78
|
+
const stat = await fs.stat(path.resolve(targetPath)).catch(() => null);
|
|
79
|
+
if (!stat) {
|
|
80
|
+
console.error(pc.red(`Error: path does not exist: ${targetPath}`));
|
|
81
|
+
process.exitCode = 1;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// Load optional config; CLI flags take precedence over config values.
|
|
85
|
+
const config = await loadConfig(targetPath);
|
|
86
|
+
const minSeverity = options.minSeverity
|
|
87
|
+
? parseSeverity(options.minSeverity)
|
|
88
|
+
: config.minSeverity ?? "low";
|
|
89
|
+
const provider = options.provider
|
|
90
|
+
? parseProvider(options.provider)
|
|
91
|
+
: config.provider;
|
|
92
|
+
const failOn = parseFailOn(options.failOn ?? config.failOn ?? "none");
|
|
93
|
+
const baselineSetting = options.writeBaseline
|
|
94
|
+
? undefined
|
|
95
|
+
: options.baseline ?? config.baseline;
|
|
96
|
+
const baselinePath = baselineSetting
|
|
97
|
+
? path.resolve(stat.isDirectory() ? targetPath : path.dirname(targetPath), baselineSetting)
|
|
98
|
+
: undefined;
|
|
99
|
+
const baselineKeys = baselinePath ? await readBaseline(baselinePath) : undefined;
|
|
100
|
+
const includedPaths = options.changedSince
|
|
101
|
+
? await changedPaths(targetPath, stat.isDirectory(), options.changedSince)
|
|
102
|
+
: undefined;
|
|
103
|
+
const result = await scan({
|
|
104
|
+
path: targetPath,
|
|
105
|
+
minSeverity,
|
|
106
|
+
provider,
|
|
107
|
+
ignoredRules: config.ignoredRules,
|
|
108
|
+
ignoredPaths: config.ignoredPaths,
|
|
109
|
+
baselineKeys,
|
|
110
|
+
includedPaths,
|
|
111
|
+
});
|
|
112
|
+
if (result.summary.filesScanned === 0) {
|
|
113
|
+
console.error(pc.yellow(options.changedSince
|
|
114
|
+
? `No changed scannable files found since "${options.changedSince}" under "${targetPath}".`
|
|
115
|
+
: `No scannable files found under "${targetPath}". Supported: .ts, .tsx, .js, .jsx, .mjs, .cjs, .py`));
|
|
116
|
+
// Not an error exit — just nothing to do.
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (options.writeBaseline) {
|
|
120
|
+
const target = path.resolve(stat.isDirectory() ? targetPath : path.dirname(targetPath), options.writeBaseline);
|
|
121
|
+
await writeBaseline(target, result.findings);
|
|
122
|
+
console.log(pc.green(`Trimference: wrote baseline to ${target} (${result.findings.length} findings).`));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const format = options.json
|
|
126
|
+
? "json"
|
|
127
|
+
: options.sarif
|
|
128
|
+
? "sarif"
|
|
129
|
+
: options.markdown
|
|
130
|
+
? "markdown"
|
|
131
|
+
: "terminal";
|
|
132
|
+
// When writing to a file, default to markdown unless json is explicitly set.
|
|
133
|
+
// File reports (markdown/JSON) always include every finding.
|
|
134
|
+
if (options.output) {
|
|
135
|
+
const fileFormat = options.json
|
|
136
|
+
? "json"
|
|
137
|
+
: options.sarif
|
|
138
|
+
? "sarif"
|
|
139
|
+
: "markdown";
|
|
140
|
+
const content = renderFor(fileFormat, result);
|
|
141
|
+
await fs.writeFile(options.output, content, "utf8");
|
|
142
|
+
console.log(pc.green(`Trimference: wrote ${fileFormat} report to ${options.output} ` +
|
|
143
|
+
`(${result.summary.totalFindings} findings).`));
|
|
144
|
+
setFailureExitCode(result, failOn);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const terminalOptions = {
|
|
148
|
+
summary: options.summary,
|
|
149
|
+
maxFindings: options.maxFindings
|
|
150
|
+
? parseMaxFindings(options.maxFindings)
|
|
151
|
+
: undefined,
|
|
152
|
+
};
|
|
153
|
+
console.log(renderFor(format, result, terminalOptions));
|
|
154
|
+
setFailureExitCode(result, failOn);
|
|
155
|
+
}
|
|
156
|
+
async function changedPaths(targetPath, targetIsDirectory, reference) {
|
|
157
|
+
const absoluteTarget = path.resolve(targetPath);
|
|
158
|
+
const gitCwd = targetIsDirectory ? absoluteTarget : path.dirname(absoluteTarget);
|
|
159
|
+
try {
|
|
160
|
+
const { stdout: rootOutput } = await execFileAsync("git", ["-C", gitCwd, "rev-parse", "--show-toplevel"], { encoding: "utf8" });
|
|
161
|
+
const repoRoot = rootOutput.trim();
|
|
162
|
+
const { stdout } = await execFileAsync("git", [
|
|
163
|
+
"-C",
|
|
164
|
+
repoRoot,
|
|
165
|
+
"diff",
|
|
166
|
+
"--name-only",
|
|
167
|
+
"--diff-filter=ACMR",
|
|
168
|
+
`${reference}...HEAD`,
|
|
169
|
+
"--",
|
|
170
|
+
], { encoding: "utf8" });
|
|
171
|
+
const targetRoot = targetIsDirectory ? absoluteTarget : path.dirname(absoluteTarget);
|
|
172
|
+
return stdout
|
|
173
|
+
.split(/\r?\n/)
|
|
174
|
+
.filter(Boolean)
|
|
175
|
+
.map((relative) => path.resolve(repoRoot, relative))
|
|
176
|
+
.filter((absolute) => targetIsDirectory
|
|
177
|
+
? !path.relative(targetRoot, absolute).startsWith("..")
|
|
178
|
+
: absolute === absoluteTarget)
|
|
179
|
+
.map((absolute) => path.relative(targetRoot, absolute).split(path.sep).join("/"));
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
183
|
+
throw new Error(`Could not determine files changed since "${reference}". ` +
|
|
184
|
+
`Ensure the Git reference is available locally. ${message}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function setFailureExitCode(result, failOn) {
|
|
188
|
+
if (failOn === "none")
|
|
189
|
+
return;
|
|
190
|
+
if (result.findings.some((finding) => SEVERITY_WEIGHT[finding.severity] >= SEVERITY_WEIGHT[failOn])) {
|
|
191
|
+
process.exitCode = 2;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async function runInit() {
|
|
195
|
+
const target = path.resolve(CONFIG_FILENAME);
|
|
196
|
+
const exists = await fs
|
|
197
|
+
.stat(target)
|
|
198
|
+
.then(() => true)
|
|
199
|
+
.catch(() => false);
|
|
200
|
+
if (exists) {
|
|
201
|
+
console.error(pc.yellow(`Config already exists at ${target}. Not overwriting.`));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
await fs.writeFile(target, SAMPLE_CONFIG, "utf8");
|
|
205
|
+
console.log(pc.green(`Created ${target}`));
|
|
206
|
+
}
|
|
207
|
+
async function main() {
|
|
208
|
+
const program = new Command();
|
|
209
|
+
program
|
|
210
|
+
.name("trimference")
|
|
211
|
+
.description("Review LLM application code for missing efficiency safeguards.")
|
|
212
|
+
.version(readVersion());
|
|
213
|
+
program
|
|
214
|
+
.command("scan")
|
|
215
|
+
.description("Review a codebase for missing AI efficiency safeguards")
|
|
216
|
+
.option("-p, --path <path>", "Path to scan", ".")
|
|
217
|
+
.option("--json", "Output findings as JSON")
|
|
218
|
+
.option("--markdown", "Output findings as Markdown")
|
|
219
|
+
.option("--sarif", "Output SARIF 2.1.0 for code scanning")
|
|
220
|
+
.option("-o, --output <file>", "Write the report to a file")
|
|
221
|
+
.option("--min-severity <level>", "Minimum severity to report (low | medium | high). Overrides config.")
|
|
222
|
+
.option("--provider <name>", "Provider hint for model suggestions (openai | anthropic | google | mistral). Overrides config.")
|
|
223
|
+
.option("--max-findings <number>", "Max detailed findings to show in the terminal (0 = show all). Default 8.")
|
|
224
|
+
.option("--summary", "Show only the high-level summary (no detailed findings).")
|
|
225
|
+
.option("--baseline <file>", "Hide findings recorded in a baseline file (path is relative to --path).")
|
|
226
|
+
.option("--write-baseline <file>", "Write current findings to a baseline file relative to --path, then exit.")
|
|
227
|
+
.option("--fail-on <level>", "Exit 2 when findings meet this severity (none | low | medium | high).")
|
|
228
|
+
.option("--changed-since <git-ref>", "Scan only files changed since a local Git reference.")
|
|
229
|
+
.action(async (opts) => {
|
|
230
|
+
await runScan(opts);
|
|
231
|
+
});
|
|
232
|
+
program
|
|
233
|
+
.command("init")
|
|
234
|
+
.description(`Create a sample ${CONFIG_FILENAME} config file`)
|
|
235
|
+
.action(async () => {
|
|
236
|
+
await runInit();
|
|
237
|
+
});
|
|
238
|
+
await program.parseAsync(process.argv);
|
|
239
|
+
}
|
|
240
|
+
main().catch((err) => {
|
|
241
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
242
|
+
console.error(pc.red(`Trimference error: ${message}`));
|
|
243
|
+
process.exitCode = 1;
|
|
244
|
+
});
|
|
245
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Severity } from "./types.js";
|
|
2
|
+
import type { Provider } from "./models.js";
|
|
3
|
+
/**
|
|
4
|
+
* Optional on-disk config. The CLI works without it; when present it provides
|
|
5
|
+
* defaults that CLI flags override.
|
|
6
|
+
*/
|
|
7
|
+
export type TrimferenceFileConfig = {
|
|
8
|
+
minSeverity?: Severity;
|
|
9
|
+
ignoredRules?: string[];
|
|
10
|
+
ignoredPaths?: string[];
|
|
11
|
+
provider?: Provider;
|
|
12
|
+
baseline?: string;
|
|
13
|
+
failOn?: Severity | "none";
|
|
14
|
+
};
|
|
15
|
+
/** @deprecated Use TrimferenceFileConfig. */
|
|
16
|
+
export type EcoLintFileConfig = TrimferenceFileConfig;
|
|
17
|
+
export declare const SCANNABLE_EXTENSIONS: string[];
|
|
18
|
+
export declare const IGNORE_GLOBS: string[];
|
|
19
|
+
export declare const DEFAULT_CONFIG: Required<TrimferenceFileConfig>;
|
|
20
|
+
export declare const CONFIG_FILENAME = "trimference.config.json";
|
|
21
|
+
export declare const LEGACY_CONFIG_FILENAME = "ecolint.config.json";
|
|
22
|
+
/** Contents written by `trimference init`. */
|
|
23
|
+
export declare const SAMPLE_CONFIG = "{\n \"minSeverity\": \"low\",\n \"ignoredRules\": [],\n \"ignoredPaths\": [],\n \"provider\": \"openai\",\n \"failOn\": \"none\"\n}\n";
|
|
24
|
+
/**
|
|
25
|
+
* Load `trimference.config.json`, searching the scan path first and then the
|
|
26
|
+
* current working directory. The former `ecolint.config.json` name is accepted
|
|
27
|
+
* as a migration fallback. Returns an empty object (not defaults) when no file
|
|
28
|
+
* exists, so callers can tell "unset" from "set to default".
|
|
29
|
+
*/
|
|
30
|
+
export declare function loadConfig(scanPath: string): Promise<TrimferenceFileConfig>;
|
|
31
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C;;;GAGG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,WAAW,CAAC,EAAE,QAAQ,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,QAAQ,GAAG,MAAM,CAAC;CAC5B,CAAC;AAEF,6CAA6C;AAC7C,MAAM,MAAM,iBAAiB,GAAG,qBAAqB,CAAC;AAEtD,eAAO,MAAM,oBAAoB,UAQhC,CAAC;AAEF,eAAO,MAAM,YAAY,UAWxB,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,QAAQ,CAAC,qBAAqB,CAO1D,CAAC;AAEF,eAAO,MAAM,eAAe,4BAA4B,CAAC;AACzD,eAAO,MAAM,sBAAsB,wBAAwB,CAAC;AAE5D,8CAA8C;AAC9C,eAAO,MAAM,aAAa,+IAOzB,CAAC;AAWF;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAmBjF"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export const SCANNABLE_EXTENSIONS = [
|
|
4
|
+
".ts",
|
|
5
|
+
".tsx",
|
|
6
|
+
".js",
|
|
7
|
+
".jsx",
|
|
8
|
+
".mjs",
|
|
9
|
+
".cjs",
|
|
10
|
+
".py",
|
|
11
|
+
];
|
|
12
|
+
export const IGNORE_GLOBS = [
|
|
13
|
+
"**/node_modules/**",
|
|
14
|
+
"**/.git/**",
|
|
15
|
+
"**/dist/**",
|
|
16
|
+
"**/build/**",
|
|
17
|
+
"**/.next/**",
|
|
18
|
+
"**/coverage/**",
|
|
19
|
+
"**/*.lock",
|
|
20
|
+
"**/package-lock.json",
|
|
21
|
+
"**/pnpm-lock.yaml",
|
|
22
|
+
"**/yarn.lock",
|
|
23
|
+
];
|
|
24
|
+
export const DEFAULT_CONFIG = {
|
|
25
|
+
minSeverity: "low",
|
|
26
|
+
ignoredRules: [],
|
|
27
|
+
ignoredPaths: [],
|
|
28
|
+
provider: "unknown",
|
|
29
|
+
baseline: "",
|
|
30
|
+
failOn: "none",
|
|
31
|
+
};
|
|
32
|
+
export const CONFIG_FILENAME = "trimference.config.json";
|
|
33
|
+
export const LEGACY_CONFIG_FILENAME = "ecolint.config.json";
|
|
34
|
+
/** Contents written by `trimference init`. */
|
|
35
|
+
export const SAMPLE_CONFIG = `{
|
|
36
|
+
"minSeverity": "low",
|
|
37
|
+
"ignoredRules": [],
|
|
38
|
+
"ignoredPaths": [],
|
|
39
|
+
"provider": "openai",
|
|
40
|
+
"failOn": "none"
|
|
41
|
+
}
|
|
42
|
+
`;
|
|
43
|
+
const VALID_SEVERITIES = ["low", "medium", "high"];
|
|
44
|
+
const VALID_PROVIDERS = [
|
|
45
|
+
"openai",
|
|
46
|
+
"anthropic",
|
|
47
|
+
"google",
|
|
48
|
+
"mistral",
|
|
49
|
+
"unknown",
|
|
50
|
+
];
|
|
51
|
+
/**
|
|
52
|
+
* Load `trimference.config.json`, searching the scan path first and then the
|
|
53
|
+
* current working directory. The former `ecolint.config.json` name is accepted
|
|
54
|
+
* as a migration fallback. Returns an empty object (not defaults) when no file
|
|
55
|
+
* exists, so callers can tell "unset" from "set to default".
|
|
56
|
+
*/
|
|
57
|
+
export async function loadConfig(scanPath) {
|
|
58
|
+
const candidates = [...new Set([
|
|
59
|
+
path.resolve(scanPath, CONFIG_FILENAME),
|
|
60
|
+
path.resolve(scanPath, LEGACY_CONFIG_FILENAME),
|
|
61
|
+
path.resolve(process.cwd(), CONFIG_FILENAME),
|
|
62
|
+
path.resolve(process.cwd(), LEGACY_CONFIG_FILENAME),
|
|
63
|
+
])];
|
|
64
|
+
for (const candidate of candidates) {
|
|
65
|
+
const raw = await fs.readFile(candidate, "utf8").catch(() => null);
|
|
66
|
+
if (raw === null)
|
|
67
|
+
continue;
|
|
68
|
+
try {
|
|
69
|
+
return normalizeConfig(JSON.parse(raw), candidate);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
73
|
+
throw new Error(`Invalid config at ${candidate}: ${message}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return {};
|
|
77
|
+
}
|
|
78
|
+
function normalizeConfig(input, source) {
|
|
79
|
+
if (typeof input !== "object" || input === null) {
|
|
80
|
+
throw new Error("expected a JSON object");
|
|
81
|
+
}
|
|
82
|
+
const obj = input;
|
|
83
|
+
const config = {};
|
|
84
|
+
if (obj.minSeverity !== undefined) {
|
|
85
|
+
if (!VALID_SEVERITIES.includes(obj.minSeverity)) {
|
|
86
|
+
throw new Error(`minSeverity must be one of ${VALID_SEVERITIES.join(", ")}`);
|
|
87
|
+
}
|
|
88
|
+
config.minSeverity = obj.minSeverity;
|
|
89
|
+
}
|
|
90
|
+
if (obj.ignoredRules !== undefined) {
|
|
91
|
+
if (!isStringArray(obj.ignoredRules)) {
|
|
92
|
+
throw new Error("ignoredRules must be an array of strings");
|
|
93
|
+
}
|
|
94
|
+
config.ignoredRules = obj.ignoredRules;
|
|
95
|
+
}
|
|
96
|
+
if (obj.ignoredPaths !== undefined) {
|
|
97
|
+
if (!isStringArray(obj.ignoredPaths)) {
|
|
98
|
+
throw new Error("ignoredPaths must be an array of strings");
|
|
99
|
+
}
|
|
100
|
+
config.ignoredPaths = obj.ignoredPaths;
|
|
101
|
+
}
|
|
102
|
+
if (obj.provider !== undefined) {
|
|
103
|
+
if (!VALID_PROVIDERS.includes(obj.provider)) {
|
|
104
|
+
throw new Error(`provider must be one of ${VALID_PROVIDERS.join(", ")}`);
|
|
105
|
+
}
|
|
106
|
+
config.provider = obj.provider;
|
|
107
|
+
}
|
|
108
|
+
if (obj.baseline !== undefined) {
|
|
109
|
+
if (typeof obj.baseline !== "string") {
|
|
110
|
+
throw new Error("baseline must be a string path");
|
|
111
|
+
}
|
|
112
|
+
config.baseline = obj.baseline;
|
|
113
|
+
}
|
|
114
|
+
if (obj.failOn !== undefined) {
|
|
115
|
+
if (obj.failOn !== "none" &&
|
|
116
|
+
!VALID_SEVERITIES.includes(obj.failOn)) {
|
|
117
|
+
throw new Error(`failOn must be none or one of ${VALID_SEVERITIES.join(", ")}`);
|
|
118
|
+
}
|
|
119
|
+
config.failOn = obj.failOn;
|
|
120
|
+
}
|
|
121
|
+
void source;
|
|
122
|
+
return config;
|
|
123
|
+
}
|
|
124
|
+
function isStringArray(value) {
|
|
125
|
+
return Array.isArray(value) && value.every((v) => typeof v === "string");
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=config.js.map
|