@methiu/raigal 0.16.2

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.
@@ -0,0 +1,383 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createHash } from "node:crypto";
4
+ import fs from "node:fs";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ import { Readable } from "node:stream";
8
+ import { pipeline } from "node:stream/promises";
9
+ import { fileURLToPath } from "node:url";
10
+ import AdmZip from "adm-zip";
11
+ import * as tar from "tar";
12
+
13
+ const THIS_FILE = fileURLToPath(import.meta.url);
14
+ const PACKAGE_ROOT = path.resolve(path.dirname(THIS_FILE), "..");
15
+ const TOOLS_BIN_DIR = path.join(PACKAGE_ROOT, "tools", "bin");
16
+ const TOOLS_ANALYZERS_DIR = path.join(PACKAGE_ROOT, "tools", "analyzers");
17
+ const USER_AGENT = "aislop-installer";
18
+ const DOWNLOAD_ATTEMPTS = 3;
19
+ const RETRYABLE_HTTP_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]);
20
+
21
+ // Pinned Roslyn analyzer packages whose rules feed the C# lint engine.
22
+ // (AsyncFixer01-03, MA0040/42/45, IDISP001: see RELEVANT_IDS in lint/dotnet.ts).
23
+ // Bundling their assemblies lets `roslynator analyze --analyzer-assemblies` cover
24
+ // projects that don't reference these analyzers themselves. NuGet catalog hashes
25
+ // prevent floating versions or altered archives from placing unverified assemblies.
26
+ // IDisposableAnalyzers is single-assembly and its IDISP rules are on by default
27
+ // (unlike CA2000, which is disabled by default in the SDK analyzers), so it gives
28
+ // type-accurate "created but never disposed" detection with no curated type list.
29
+ //
30
+ // Roslynator.Analyzers is intentionally NOT bundled: its RCS rules aren't in
31
+ // RELEVANT_IDS, and its nupkg flattens dependency assemblies under analyzers/dotnet/cs
32
+ // with prefixed names that break naive extraction.
33
+ const ANALYZER_PACKAGES = [
34
+ {
35
+ id: "AsyncFixer",
36
+ version: "2.1.0",
37
+ sha512: "9exIvSbeCBqtmVeSC6hX9CD9UxtNaWWFbPPte+J3QqjYi2jOP2nt8keuehOrFBeTJXj2j/9PHx99tFltpWpV5Q==",
38
+ },
39
+ {
40
+ id: "Meziantou.Analyzer",
41
+ version: "3.0.123",
42
+ sha512: "NbIeci8/kG8/EWnK1Fs0Clt5BHNNGMGd48CScxGHkPkVX5D4DgSFbOFCELLdJaIvNkS7mkTCBHxqON8PqEZ+sQ==",
43
+ },
44
+ {
45
+ id: "IDisposableAnalyzers",
46
+ version: "4.0.8",
47
+ sha512: "hvk7xMUBwUzUsGaL66zFTsOAp17hKj2XO0Y0rNIwts3iZ0PGF1M6YTAFiQ0tGnJxzsA8oWczc3ZTfaxj91Xwfw==",
48
+ },
49
+ ];
50
+
51
+ const PLATFORM_KEY = `${process.platform}-${process.arch}`;
52
+
53
+ const TOOL_DEFINITIONS = [
54
+ {
55
+ name: "ruff",
56
+ repo: "astral-sh/ruff",
57
+ version: "0.15.4",
58
+ tag: "0.15.4",
59
+ binaryName: "ruff",
60
+ assets: {
61
+ "darwin-arm64": ["ruff-aarch64-apple-darwin.tar.gz"],
62
+ "darwin-x64": ["ruff-x86_64-apple-darwin.tar.gz"],
63
+ "linux-arm64": ["ruff-aarch64-unknown-linux-gnu.tar.gz"],
64
+ "linux-x64": ["ruff-x86_64-unknown-linux-gnu.tar.gz"],
65
+ "win32-arm64": ["ruff-aarch64-pc-windows-msvc.zip"],
66
+ "win32-x64": ["ruff-x86_64-pc-windows-msvc.zip"],
67
+ },
68
+ sha256: {
69
+ "darwin-arm64": "2d63cc9fd12c9cc3b524563bbeb50470cf3f68f3194002228a417a53a2a56164",
70
+ "darwin-x64": "f40e16784c867b60850fbe96a2cccd123589c90d6db71ad8ade62efdeabccc84",
71
+ "linux-arm64": "7e436cedadb1bac0166448b05c0b5d69bb1d7879b0b26696bfc198ebdffb7b2f",
72
+ "linux-x64": "6e24501f753416bc84456383ccf62239889ab9fec8318549db9bee791612bd85",
73
+ "win32-arm64": "7de874b0d667fe04c2cd15629c19baff6dfff55e1fd99dfb14cb9850b09e7a20",
74
+ "win32-x64": "ca4db783ce3a1b942e67aa4002ca9f3c6ff1b150a85cb4ca1345c4299ad12a0f",
75
+ },
76
+ },
77
+ {
78
+ name: "golangci-lint",
79
+ repo: "golangci/golangci-lint",
80
+ version: "2.10.1",
81
+ tag: "v2.10.1",
82
+ binaryName: "golangci-lint",
83
+ assets: {
84
+ "darwin-arm64": [
85
+ "golangci-lint-2.10.1-darwin-arm64.tar.gz",
86
+ "golangci-lint-v2.10.1-darwin-arm64.tar.gz",
87
+ ],
88
+ "darwin-x64": [
89
+ "golangci-lint-2.10.1-darwin-amd64.tar.gz",
90
+ "golangci-lint-v2.10.1-darwin-amd64.tar.gz",
91
+ ],
92
+ "linux-arm64": [
93
+ "golangci-lint-2.10.1-linux-arm64.tar.gz",
94
+ "golangci-lint-v2.10.1-linux-arm64.tar.gz",
95
+ ],
96
+ "linux-x64": [
97
+ "golangci-lint-2.10.1-linux-amd64.tar.gz",
98
+ "golangci-lint-v2.10.1-linux-amd64.tar.gz",
99
+ ],
100
+ "win32-arm64": [
101
+ "golangci-lint-2.10.1-windows-arm64.zip",
102
+ "golangci-lint-v2.10.1-windows-arm64.zip",
103
+ ],
104
+ "win32-x64": [
105
+ "golangci-lint-2.10.1-windows-amd64.zip",
106
+ "golangci-lint-v2.10.1-windows-amd64.zip",
107
+ ],
108
+ },
109
+ sha256: {
110
+ "darwin-arm64": "03bfadf67e52b441b7ec21305e501c717df93c959836d66c7f97312654acb297",
111
+ "darwin-x64": "66fb0da81b8033b477f97eea420d4b46b230ca172b8bb87c6610109f3772b6b6",
112
+ "linux-arm64": "6652b42ae02915eb2f9cb2a2e0cac99514c8eded8388d88ae3e06e1a52c00de8",
113
+ "linux-x64": "dfa775874cf0561b404a02a8f4481fc69b28091da95aa697259820d429b09c99",
114
+ "win32-arm64": "636ab790c8dcea8034aa34aba6031ca3893d68f7eda000460ab534341fadbab1",
115
+ "win32-x64": "c60c87695e79db8e320f0e5be885059859de52bb5ee5f11be5577828570bc2a3",
116
+ },
117
+ },
118
+ ];
119
+
120
+ const isWindows = process.platform === "win32";
121
+ const withExecutableExtension = (name) => (isWindows ? `${name}.exe` : name);
122
+
123
+ const info = (message) => console.error(`[aislop] ${message}`);
124
+ const warn = (message) => console.error(`[aislop] ${message}`);
125
+
126
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
127
+
128
+ const downloadError = (message, retryable) =>
129
+ Object.assign(new Error(message), { retryable });
130
+
131
+ const downloadFile = async (url, destination) => {
132
+ let lastError = null;
133
+ for (let attempt = 1; attempt <= DOWNLOAD_ATTEMPTS; attempt += 1) {
134
+ try {
135
+ const response = await fetch(url, {
136
+ headers: { "User-Agent": USER_AGENT },
137
+ });
138
+ if (response.ok && response.body) {
139
+ await pipeline(
140
+ Readable.fromWeb(response.body),
141
+ fs.createWriteStream(destination),
142
+ );
143
+ return;
144
+ }
145
+ const message = `Failed to download ${url} (${response.status})`;
146
+ lastError = downloadError(message, RETRYABLE_HTTP_STATUSES.has(response.status));
147
+ if (!lastError.retryable) throw lastError;
148
+ } catch (error) {
149
+ lastError = error instanceof Error ? error : new Error(String(error));
150
+ if (lastError.retryable === false || attempt === DOWNLOAD_ATTEMPTS) break;
151
+ }
152
+ await sleep(400 * attempt);
153
+ }
154
+ throw lastError ?? new Error(`Failed to download ${url}`);
155
+ };
156
+
157
+ const extractArchive = async (archivePath, extractDir) => {
158
+ if (archivePath.endsWith(".tar.gz")) {
159
+ await tar.x({ file: archivePath, cwd: extractDir });
160
+ return;
161
+ }
162
+ if (archivePath.endsWith(".zip")) {
163
+ const zip = new AdmZip(archivePath);
164
+ zip.extractAllTo(extractDir, true);
165
+ return;
166
+ }
167
+ throw new Error(`Unsupported archive format for ${archivePath}`);
168
+ };
169
+
170
+ const getTagCandidates = (tag) => {
171
+ if (tag.startsWith("v")) {
172
+ return [tag, tag.slice(1)];
173
+ }
174
+ return [tag, `v${tag}`];
175
+ };
176
+
177
+ const getAssetUrls = (tool, assetName) =>
178
+ getTagCandidates(tool.tag).map(
179
+ (tag) =>
180
+ `https://github.com/${tool.repo}/releases/download/${tag}/${assetName}`,
181
+ );
182
+
183
+ const downloadFromCandidates = async (urls, archivePath) => {
184
+ const failures = [];
185
+ for (const url of urls) {
186
+ try {
187
+ await downloadFile(url, archivePath);
188
+ return url;
189
+ } catch (error) {
190
+ failures.push(error instanceof Error ? error.message : String(error));
191
+ }
192
+ }
193
+ throw new Error(
194
+ `Could not download from candidate URLs: ${failures.join(" | ")}`,
195
+ );
196
+ };
197
+
198
+ const findBinary = (rootDir, binaryName) => {
199
+ const queue = [rootDir];
200
+ while (queue.length > 0) {
201
+ const current = queue.shift();
202
+ if (!current) continue;
203
+ const entries = fs.readdirSync(current, { withFileTypes: true });
204
+ for (const entry of entries) {
205
+ const fullPath = path.join(current, entry.name);
206
+ if (entry.isDirectory()) {
207
+ queue.push(fullPath);
208
+ continue;
209
+ }
210
+ if (entry.name === binaryName) return fullPath;
211
+ }
212
+ }
213
+ return null;
214
+ };
215
+
216
+ const digestOf = async (filePath, algorithm, encoding) => {
217
+ const hash = createHash(algorithm);
218
+ for await (const chunk of fs.createReadStream(filePath)) hash.update(chunk);
219
+ return hash.digest(encoding);
220
+ };
221
+
222
+ const installTool = async (tool) => {
223
+ const assetNames = tool.assets[PLATFORM_KEY];
224
+ if (!assetNames || assetNames.length === 0) {
225
+ warn(`No bundled ${tool.name} build for ${PLATFORM_KEY}; skipping.`);
226
+ return false;
227
+ }
228
+
229
+ const destinationBinary = path.join(
230
+ TOOLS_BIN_DIR,
231
+ withExecutableExtension(tool.binaryName),
232
+ );
233
+ if (fs.existsSync(destinationBinary)) {
234
+ info(`${tool.name} already present.`);
235
+ return true;
236
+ }
237
+
238
+ const tempDir = fs.mkdtempSync(
239
+ path.join(os.tmpdir(), `aislop-${tool.name}-`),
240
+ );
241
+ const archivePath = path.join(tempDir, assetNames[0]);
242
+ const extractDir = path.join(tempDir, "extract");
243
+ fs.mkdirSync(extractDir, { recursive: true });
244
+
245
+ try {
246
+ const candidateUrls = assetNames.flatMap((assetName) =>
247
+ getAssetUrls(tool, assetName),
248
+ );
249
+ info(`Downloading ${tool.name} ${tool.version}...`);
250
+ await downloadFromCandidates(candidateUrls, archivePath);
251
+ const actualSha256 = await digestOf(archivePath, "sha256", "hex");
252
+ if (actualSha256 !== tool.sha256[PLATFORM_KEY]) {
253
+ throw new Error(`SHA-256 mismatch for ${tool.name} ${tool.version}`);
254
+ }
255
+ await extractArchive(archivePath, extractDir);
256
+
257
+ const extractedBinary = findBinary(
258
+ extractDir,
259
+ withExecutableExtension(tool.binaryName),
260
+ );
261
+ if (!extractedBinary) {
262
+ throw new Error(
263
+ `Unable to locate ${tool.binaryName} in extracted archive`,
264
+ );
265
+ }
266
+
267
+ fs.mkdirSync(TOOLS_BIN_DIR, { recursive: true });
268
+ fs.copyFileSync(extractedBinary, destinationBinary);
269
+ if (!isWindows) fs.chmodSync(destinationBinary, 0o755);
270
+
271
+ info(`Installed bundled ${tool.name} at ${destinationBinary}`);
272
+ return true;
273
+ } finally {
274
+ fs.rmSync(tempDir, { recursive: true, force: true });
275
+ }
276
+ };
277
+
278
+ // One analyzer DLL per basename, preferring the newest Roslyn-versioned subfolder
279
+ // (e.g. analyzers/dotnet/roslyn4.7/cs over roslyn3.8) so we don't load duplicates.
280
+ const pickAnalyzerEntries = (zip) => {
281
+ const dllRe = /(?:^|\/)analyzers\/.*\/cs\/[^/]+\.dll$/i;
282
+ const byBasename = new Map();
283
+ for (const entry of zip.getEntries()) {
284
+ if (entry.isDirectory || !dllRe.test(entry.entryName)) continue;
285
+ const basename = path.posix.basename(entry.entryName);
286
+ const existing = byBasename.get(basename);
287
+ if (!existing || entry.entryName > existing.entryName) byBasename.set(basename, entry);
288
+ }
289
+ return [...byBasename.values()];
290
+ };
291
+
292
+ const installAnalyzerPackage = async (
293
+ { id: packageId, version, sha512 },
294
+ destinationDirectory,
295
+ ) => {
296
+ const idLower = packageId.toLowerCase();
297
+ const url = `https://api.nuget.org/v3-flatcontainer/${idLower}/${version}/${idLower}.${version}.nupkg`;
298
+
299
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `aislop-${idLower}-`));
300
+ const nupkgPath = path.join(tempDir, `${idLower}.${version}.nupkg`);
301
+ try {
302
+ await downloadFile(url, nupkgPath);
303
+ const actualSha512 = await digestOf(nupkgPath, "sha512", "base64");
304
+ if (actualSha512 !== sha512) {
305
+ throw new Error(`SHA-512 mismatch for ${packageId} ${version}`);
306
+ }
307
+ const entries = pickAnalyzerEntries(new AdmZip(nupkgPath));
308
+ if (entries.length === 0) throw new Error(`no analyzer assemblies inside ${packageId}`);
309
+ for (const entry of entries) {
310
+ fs.writeFileSync(
311
+ path.join(destinationDirectory, path.posix.basename(entry.entryName)),
312
+ entry.getData(),
313
+ );
314
+ }
315
+ info(`Bundled ${entries.length} analyzer assembly(ies) from ${packageId} ${version}`);
316
+ return true;
317
+ } finally {
318
+ fs.rmSync(tempDir, { recursive: true, force: true });
319
+ }
320
+ };
321
+
322
+ const installAnalyzers = async () => {
323
+ const stagingDirectory = `${TOOLS_ANALYZERS_DIR}-${process.pid}`;
324
+ fs.rmSync(stagingDirectory, { recursive: true, force: true });
325
+ fs.mkdirSync(stagingDirectory, { recursive: true });
326
+ try {
327
+ for (const analyzerPackage of ANALYZER_PACKAGES) {
328
+ await installAnalyzerPackage(analyzerPackage, stagingDirectory);
329
+ }
330
+ fs.rmSync(TOOLS_ANALYZERS_DIR, { recursive: true, force: true });
331
+ fs.renameSync(stagingDirectory, TOOLS_ANALYZERS_DIR);
332
+ } catch (error) {
333
+ fs.rmSync(stagingDirectory, { recursive: true, force: true });
334
+ throw error;
335
+ }
336
+ };
337
+
338
+ const main = async () => {
339
+ const failures = [];
340
+ for (const tool of TOOL_DEFINITIONS) {
341
+ try {
342
+ const installed = await installTool(tool);
343
+ if (!installed) {
344
+ failures.push(`${tool.name}: unsupported platform`);
345
+ }
346
+ } catch (error) {
347
+ const message = error instanceof Error ? error.message : String(error);
348
+ failures.push(`${tool.name}: ${message}`);
349
+ warn(`Failed to install ${tool.name}: ${message}`);
350
+ }
351
+ }
352
+
353
+ if (failures.length > 0) {
354
+ warn("Some bundled tools could not be installed:");
355
+ for (const failure of failures) {
356
+ warn(` - ${failure}`);
357
+ }
358
+ warn(
359
+ "raigal will still run, but coverage for those tools may be reduced until installation succeeds.",
360
+ );
361
+ process.exitCode = 1;
362
+ return;
363
+ }
364
+
365
+ await installAnalyzers();
366
+
367
+ printNextSteps();
368
+ };
369
+
370
+ const printNextSteps = () => {
371
+ if (process.env.CI) return;
372
+ info("Installed. Next:");
373
+ info(" raigal scan score this repo for AI slop");
374
+ info(" raigal agent run a coding agent and auto-scan its work");
375
+ info(" Gate every PR for your team, free → https://raigal.dev");
376
+ };
377
+
378
+ main().catch((error) => {
379
+ warn(
380
+ `tool installation failed: ${error instanceof Error ? error.message : String(error)}`,
381
+ );
382
+ process.exitCode = 1;
383
+ });
@@ -0,0 +1,10 @@
1
+ if (!Promise.withResolvers) {
2
+ Promise.withResolvers = function () {
3
+ let resolve, reject;
4
+ const promise = new Promise((res, rej) => {
5
+ resolve = res;
6
+ reject = rej;
7
+ });
8
+ return { promise, resolve, reject };
9
+ };
10
+ }
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const ROOT = path.resolve(__dirname, "..");
8
+ const LICENSE_PATH = path.join(ROOT, "LICENSE");
9
+
10
+ const PLACEHOLDER_TOKEN = "[LICENCE TEXT TO BE SUPPLIED BY COUNSEL]";
11
+
12
+ if (!fs.existsSync(LICENSE_PATH)) {
13
+ console.error("Release guard error: LICENSE file is missing.");
14
+ process.exit(1);
15
+ }
16
+
17
+ const content = fs.readFileSync(LICENSE_PATH, "utf-8");
18
+
19
+ if (content.includes(PLACEHOLDER_TOKEN)) {
20
+ console.error(
21
+ "Release guard error: LICENSE still contains placeholder token:\n" +
22
+ ` "${PLACEHOLDER_TOKEN}"\n` +
23
+ "Counsel must provide approved proprietary licence terms before release.",
24
+ );
25
+ process.exit(1);
26
+ }
27
+
28
+ console.log("Release guard passed: LICENSE has been replaced with final legal terms.");
29
+ process.exit(0);
@@ -0,0 +1,45 @@
1
+ # tools/jb
2
+
3
+ This directory holds raigal's bundled ReSharper/JetBrains settings for the
4
+ `jb inspectcode` pass. The settings file is passed via `--settings` when
5
+ `resolveBundledJbSettings()` finds it, allowing raigal to layer controlled
6
+ suppressions and patterns on top of the jb defaults.
7
+
8
+ ## raigal.DotSettings
9
+
10
+ Currently ships one entry:
11
+
12
+ - **InconsistentNaming suppression** (`DO_NOT_SHOW`): The `InconsistentNaming`
13
+ inspection binds to machine-global ReSharper configuration and ignores
14
+ solution-level settings, making its output unreliable and noisy in CLI usage.
15
+ raigal force-suppresses it via this settings file so the inspection count is
16
+ deterministic across machines.
17
+
18
+ ## TODO: SSR slop-pattern starter set (deferred)
19
+
20
+ The following Structural Search and Replace (SSR) patterns are planned but not
21
+ yet authored. They must be created interactively in Rider's Structural Search UI
22
+ (ReSharper | Tools | Pattern Catalog), verified to fire against real C# code,
23
+ then exported and merged into `raigal.DotSettings`. SSR blobs are opaque enough
24
+ that hand-writing them silently no-ops; authoring in Rider is required.
25
+
26
+ Patterns to add (each at severity WARNING):
27
+
28
+ 1. Redundant local-then-return: `var $x$ = $expr$; return $x$;` where `$x$`
29
+ is a local used exactly twice.
30
+ 2. Empty catch block: `try { $body$ } catch { }`.
31
+ 3. Single-variable string interpolation: `$"{$x$}"` where `$x$` is any
32
+ expression (should be written as `$x$.ToString()` or cast directly).
33
+ 4. Redundant null-guard duplicating a non-null contract.
34
+
35
+ Authoring and verification procedure:
36
+
37
+ 1. In Rider, open ReSharper | Tools | Pattern Catalog | Add and define each
38
+ pattern with its placeholders and severity WARNING.
39
+ 2. Export the Solution-team-shared settings and copy the resulting
40
+ `/Default/PatternsAndTemplates/...` entries into `raigal.DotSettings`.
41
+ 3. Verify each pattern actually fires: build a tiny C# fixture containing one
42
+ violation of each, then run
43
+ `jb inspectcode <fixture> --format=Xml --output=ssr.xml --caches-home=<temp> --settings=tools/jb/raigal.DotSettings`
44
+ and confirm each authored pattern appears as both an `<IssueType>` and an
45
+ `<Issue>` in `ssr.xml`. Do not ship a pattern that does not fire.
@@ -0,0 +1,3 @@
1
+ <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
2
+ <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=InconsistentNaming/@EntryIndexedValue">DO_NOT_SHOW</s:String>
3
+ </wpf:ResourceDictionary>