@codebend3r/gale 0.2.0 → 0.2.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.
package/README.md CHANGED
@@ -27,4 +27,4 @@ console.log(result.errored);
27
27
  console.log(result.results);
28
28
  ```
29
29
 
30
- See the full documentation at [github.com/LyricalString/gale](https://github.com/LyricalString/gale).
30
+ See the full documentation at [github.com/codebend3r/gale](https://github.com/codebend3r/gale).
Binary file
Binary file
package/bin/gale.cjs ADDED
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ /**
5
+ * Launches the bundled Gale binary for this platform.
6
+ *
7
+ * A Node script rather than a shell script so the same launcher works on
8
+ * Windows, where npm wraps it in a .cmd shim. Everything is forwarded: the
9
+ * arguments, stdin/stdout/stderr, the exit code, and any terminating signal.
10
+ */
11
+
12
+ const { spawnSync } = require("node:child_process");
13
+ const { existsSync } = require("node:fs");
14
+ const path = require("node:path");
15
+
16
+ const { binaryFileName, resolveTarget, supportedPlatforms } = require("../platform.cjs");
17
+
18
+ const target = resolveTarget(process.platform, process.arch);
19
+
20
+ if (!target) {
21
+ process.stderr.write(`Unsupported platform: ${process.platform}-${process.arch}\n`);
22
+ process.stderr.write(`Supported: ${supportedPlatforms().join(", ")}\n`);
23
+ process.stderr.write("Build from source instead: cargo install gale-lint\n");
24
+ process.exit(1);
25
+ }
26
+
27
+ const binary = path.join(__dirname, target, binaryFileName(process.platform));
28
+
29
+ if (!existsSync(binary)) {
30
+ process.stderr.write(`Gale binary missing for ${target} at ${binary}\n`);
31
+ process.stderr.write(`This package should ship prebuilt binaries for ${target}.\n`);
32
+ process.stderr.write("Try reinstalling the package, or build from source:\n");
33
+ process.stderr.write(" cargo install gale-lint\n");
34
+ process.exit(1);
35
+ }
36
+
37
+ const result = spawnSync(binary, process.argv.slice(2), {
38
+ stdio: "inherit",
39
+ windowsHide: true,
40
+ });
41
+
42
+ if (result.error) {
43
+ process.stderr.write(`Failed to run ${binary}: ${result.error.message}\n`);
44
+ process.exit(1);
45
+ }
46
+
47
+ if (result.signal) {
48
+ // Re-raise so the parent sees the same signal the linter died from.
49
+ process.kill(process.pid, result.signal);
50
+ }
51
+
52
+ process.exit(result.status ?? 1);
Binary file
Binary file
package/index.d.ts ADDED
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Type declarations for @codebend3r/gale.
3
+ *
4
+ * The shapes mirror Stylelint's public API so a project can swap the import
5
+ * without touching its call sites. Fields Gale never populates are typed as
6
+ * they are in Stylelint but are always empty.
7
+ */
8
+
9
+ export type Severity = "error" | "warning";
10
+
11
+ export type FormatterType = "string" | "json" | "compact" | "verbose" | "tap" | "unix";
12
+
13
+ /** A single problem reported for a source. */
14
+ export interface Warning {
15
+ line: number;
16
+ column: number;
17
+ endLine?: number;
18
+ endColumn?: number;
19
+ rule: string;
20
+ severity: Severity;
21
+ /** The message with ` (rule-name)` appended, as Stylelint prints it. */
22
+ text: string;
23
+ /** Present when the rule's `url` secondary option is set. */
24
+ url?: string;
25
+ }
26
+
27
+ /** The result of linting one source. */
28
+ export interface LintResult {
29
+ source: string;
30
+ warnings: Warning[];
31
+ /** Always empty: Gale reports no deprecations. */
32
+ deprecations: unknown[];
33
+ /** Always empty: Gale does not validate rule options. */
34
+ invalidOptionWarnings: unknown[];
35
+ /** Always empty: parse errors surface as `parse-error` warnings. */
36
+ parseErrors: unknown[];
37
+ errored: boolean;
38
+ ignored: boolean;
39
+ }
40
+
41
+ export interface MaxWarningsExceeded {
42
+ maxWarnings: number;
43
+ foundWarnings: number;
44
+ }
45
+
46
+ /** What `lint()` resolves to. */
47
+ export interface LinterResult {
48
+ cwd: string;
49
+ results: LintResult[];
50
+ errored: boolean;
51
+ /** The formatted report, in the formatter requested (JSON by default). */
52
+ report: string;
53
+ /** The fixed source when `fix` and `code` were both given. */
54
+ code?: string;
55
+ maxWarningsExceeded?: MaxWarningsExceeded;
56
+ /** Always empty: Gale exposes no per-rule metadata. */
57
+ ruleMetadata: Record<string, never>;
58
+ }
59
+
60
+ export type FormatterFunction = (
61
+ results: LintResult[],
62
+ returnValue: { cwd: string; results: LintResult[]; errored: boolean },
63
+ ) => string;
64
+
65
+ /** Rule settings in any of Stylelint's accepted shapes. */
66
+ export type RuleSetting = boolean | null | string | number | unknown[];
67
+
68
+ export interface Config {
69
+ extends?: string | string[];
70
+ plugins?: string[];
71
+ rules?: Record<string, RuleSetting>;
72
+ overrides?: unknown[];
73
+ ignoreFiles?: string | string[];
74
+ ignorePatterns?: string | string[];
75
+ customSyntax?: string;
76
+ defaultSeverity?: Severity;
77
+ ignoreDisables?: boolean;
78
+ reportNeedlessDisables?: boolean;
79
+ reportInvalidScopeDisables?: boolean;
80
+ reportDescriptionlessDisables?: boolean;
81
+ reportUnscopedDisables?: boolean;
82
+ allowEmptyInput?: boolean;
83
+ quiet?: boolean;
84
+ fix?: boolean | "strict" | "lax";
85
+ cache?: boolean;
86
+ cacheLocation?: string;
87
+ [key: string]: unknown;
88
+ }
89
+
90
+ export interface LinterOptions {
91
+ /** Glob pattern(s) for files to lint. */
92
+ files?: string | string[];
93
+ /** CSS source to lint instead of files. */
94
+ code?: string;
95
+ /** Virtual filename for `code`, used for syntax detection. */
96
+ codeFilename?: string;
97
+ /** Inline config object. */
98
+ config?: Config;
99
+ /** Path to a config file. */
100
+ configFile?: string;
101
+ fix?: boolean | "strict" | "lax";
102
+ formatter?: FormatterType | FormatterFunction;
103
+ quiet?: boolean;
104
+ cache?: boolean;
105
+ cacheLocation?: string;
106
+ maxWarnings?: number;
107
+ allowEmptyInput?: boolean;
108
+ ignorePath?: string;
109
+ ignoreDisables?: boolean;
110
+ reportNeedlessDisables?: boolean;
111
+ reportInvalidScopeDisables?: boolean;
112
+ reportDescriptionlessDisables?: boolean;
113
+ /** Working directory; defaults to `process.cwd()`. */
114
+ cwd?: string;
115
+ }
116
+
117
+ /** Lint files or a code string and resolve with a Stylelint-shaped result. */
118
+ export function lint(options?: LinterOptions): Promise<LinterResult>;
119
+
120
+ /** Resolve the effective config for a file, or `undefined` when none applies. */
121
+ export function resolveConfig(
122
+ filePath: string,
123
+ options?: { configFile?: string; cwd?: string },
124
+ ): Promise<Config | undefined>;
125
+
126
+ /** Promise-based formatter functions keyed by name, like `stylelint.formatters`. */
127
+ export const formatters: {
128
+ readonly json: Promise<FormatterFunction>;
129
+ readonly string: Promise<FormatterFunction>;
130
+ readonly compact: Promise<FormatterFunction>;
131
+ readonly verbose: Promise<FormatterFunction>;
132
+ readonly tap: Promise<FormatterFunction>;
133
+ readonly unix: Promise<FormatterFunction>;
134
+ };
135
+
136
+ /**
137
+ * Compatibility stub. Gale runs built-in Rust rules and cannot execute a
138
+ * JavaScript rule; calling this warns and returns an inert plugin object.
139
+ */
140
+ export function createPlugin(
141
+ ruleName: string,
142
+ ruleFunction: (...args: unknown[]) => unknown,
143
+ ): { ruleName: string; rule: (...args: unknown[]) => unknown };
144
+
145
+ declare const _default: {
146
+ lint: typeof lint;
147
+ formatters: typeof formatters;
148
+ resolveConfig: typeof resolveConfig;
149
+ createPlugin: typeof createPlugin;
150
+ };
151
+
152
+ export default _default;
package/index.mjs CHANGED
@@ -12,6 +12,8 @@ import { tmpdir } from "node:os";
12
12
  import { randomBytes } from "node:crypto";
13
13
  import { fileURLToPath } from "node:url";
14
14
 
15
+ import { binaryFileName, resolveTarget } from "./platform.cjs";
16
+
15
17
  const __filename = fileURLToPath(import.meta.url);
16
18
  const __dirname = dirname(__filename);
17
19
 
@@ -20,10 +22,20 @@ const __dirname = dirname(__filename);
20
22
  // ---------------------------------------------------------------------------
21
23
 
22
24
  function findBinary() {
23
- // 1. Check the bin/ directory within the npm package
24
- const localBin = join(__dirname, "bin", "gale");
25
- if (existsSync(localBin)) {
26
- return localBin;
25
+ // 0. An explicit override wins. Test suites and monorepos use this to point
26
+ // at a freshly built binary instead of the bundled one.
27
+ const override = process.env.GALE_BINARY;
28
+ if (override) {
29
+ return override;
30
+ }
31
+
32
+ // 1. The binary bundled for this platform inside the npm package
33
+ const target = resolveTarget(process.platform, process.arch);
34
+ if (target) {
35
+ const bundled = join(__dirname, "bin", target, binaryFileName(process.platform));
36
+ if (existsSync(bundled)) {
37
+ return bundled;
38
+ }
27
39
  }
28
40
 
29
41
  // 2. Fall back to gale on PATH
@@ -122,9 +134,14 @@ function parseJsonOutput(jsonString) {
122
134
  warnings: (entry.warnings || []).map((w) => ({
123
135
  line: w.line,
124
136
  column: w.column,
137
+ endLine: w.endLine,
138
+ endColumn: w.endColumn,
125
139
  rule: w.rule,
126
140
  severity: w.severity || "warning",
127
141
  text: w.text,
142
+ // Only present when the rule's `url` secondary option is set, as in
143
+ // Stylelint, where an undefined url is dropped by JSON.stringify.
144
+ ...(w.url !== undefined ? { url: w.url } : {}),
128
145
  })),
129
146
  deprecations: [],
130
147
  invalidOptionWarnings: [],
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@codebend3r/gale",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "An extremely fast CSS linter, written in Rust. Drop-in replacement for Stylelint.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "https://github.com/LyricalString/gale"
8
+ "url": "https://github.com/codebend3r/gale"
9
9
  },
10
- "homepage": "https://github.com/LyricalString/gale",
11
- "bugs": "https://github.com/LyricalString/gale/issues",
10
+ "homepage": "https://github.com/codebend3r/gale",
11
+ "bugs": "https://github.com/codebend3r/gale/issues",
12
12
  "keywords": [
13
13
  "css",
14
14
  "linter",
@@ -22,24 +22,28 @@
22
22
  ],
23
23
  "type": "module",
24
24
  "main": "index.mjs",
25
+ "types": "index.d.ts",
25
26
  "exports": {
26
27
  ".": {
28
+ "types": "./index.d.ts",
27
29
  "import": "./index.mjs",
28
30
  "require": "./index.cjs"
29
31
  }
30
32
  },
31
33
  "bin": {
32
- "gale": "bin/gale"
34
+ "gale": "bin/gale.cjs"
33
35
  },
34
36
  "files": [
35
37
  "bin/",
36
38
  "index.cjs",
39
+ "index.d.ts",
37
40
  "index.mjs",
41
+ "platform.cjs",
38
42
  "test.mjs",
39
43
  "README.md"
40
44
  ],
41
45
  "engines": {
42
- "node": ">=16.0.0"
46
+ "node": ">=20.0.0"
43
47
  },
44
48
  "scripts": {
45
49
  "test": "node test.mjs"
package/platform.cjs ADDED
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Maps a Node.js platform and architecture to the Rust target whose binary
5
+ * ships inside this package, and names the binary for that platform.
6
+ *
7
+ * Shared by the `gale` launcher and the programmatic API. CommonJS so the
8
+ * launcher can require it synchronously on every supported Node version.
9
+ */
10
+
11
+ const TARGETS = {
12
+ "darwin-arm64": "aarch64-apple-darwin",
13
+ "darwin-x64": "x86_64-apple-darwin",
14
+ "linux-arm64": "aarch64-unknown-linux-gnu",
15
+ "linux-x64": "x86_64-unknown-linux-gnu",
16
+ "win32-arm64": "aarch64-pc-windows-msvc",
17
+ "win32-x64": "x86_64-pc-windows-msvc",
18
+ };
19
+
20
+ /**
21
+ * The Rust target triple for a platform/arch pair, or `null` when Gale does
22
+ * not ship a binary for it.
23
+ *
24
+ * @param {string} platform - `process.platform`
25
+ * @param {string} arch - `process.arch`
26
+ * @returns {string | null}
27
+ */
28
+ function resolveTarget(platform, arch) {
29
+ return TARGETS[`${platform}-${arch}`] ?? null;
30
+ }
31
+
32
+ /**
33
+ * The file name of the Gale binary on a platform.
34
+ *
35
+ * @param {string} platform - `process.platform`
36
+ * @returns {string}
37
+ */
38
+ function binaryFileName(platform) {
39
+ return platform === "win32" ? "gale.exe" : "gale";
40
+ }
41
+
42
+ /** Every `platform-arch` pair a binary ships for, for error messages. */
43
+ function supportedPlatforms() {
44
+ return Object.keys(TARGETS);
45
+ }
46
+
47
+ exports.resolveTarget = resolveTarget;
48
+ exports.binaryFileName = binaryFileName;
49
+ exports.supportedPlatforms = supportedPlatforms;
package/test.mjs CHANGED
@@ -8,18 +8,45 @@
8
8
  * Requires a working gale binary (either in npm/bin/ or on PATH).
9
9
  */
10
10
 
11
+ import { createRequire } from "node:module";
12
+
11
13
  import { lint, formatters, resolveConfig, createPlugin } from "./index.mjs";
12
14
 
15
+ const require = createRequire(import.meta.url);
16
+
13
17
  let passed = 0;
14
18
  let failed = 0;
19
+ let current = "";
20
+
21
+ // Dots reporter: one "." per passing assertion, "F" per failure. Failure
22
+ // details are printed on their own line so the dots stay readable.
23
+ function section(name) {
24
+ current = name;
25
+ }
26
+
27
+ function fail(message) {
28
+ process.stdout.write("F");
29
+ console.error(`\n FAIL [${current}] ${message}`);
30
+ failed++;
31
+ }
15
32
 
16
33
  function assert(condition, message) {
17
34
  if (condition) {
18
- console.log(` PASS: ${message}`);
35
+ process.stdout.write(".");
19
36
  passed++;
20
37
  } else {
21
- console.error(` FAIL: ${message}`);
22
- failed++;
38
+ fail(message);
39
+ }
40
+ }
41
+
42
+ // Runs fn with console.warn muted, for APIs that warn by design.
43
+ function quietly(fn) {
44
+ const warn = console.warn;
45
+ console.warn = () => {};
46
+ try {
47
+ return fn();
48
+ } finally {
49
+ console.warn = warn;
23
50
  }
24
51
  }
25
52
 
@@ -28,7 +55,7 @@ function assert(condition, message) {
28
55
  // ---------------------------------------------------------------------------
29
56
 
30
57
  async function testLintCodeEmptyBlock() {
31
- console.log("\nTest 1: lint({ code: 'a {}' })");
58
+ section("Test 1: lint({ code: 'a {}' })");
32
59
 
33
60
  try {
34
61
  const result = await lint({ code: "a {}" });
@@ -50,8 +77,7 @@ async function testLintCodeEmptyBlock() {
50
77
  assert(typeof first.ignored === "boolean", "first result has ignored boolean");
51
78
  }
52
79
  } catch (err) {
53
- console.error(` ERROR: ${err.message}`);
54
- failed++;
80
+ fail(`ERROR: ${err.message}`);
55
81
  }
56
82
  }
57
83
 
@@ -60,7 +86,7 @@ async function testLintCodeEmptyBlock() {
60
86
  // ---------------------------------------------------------------------------
61
87
 
62
88
  async function testLintCodeWithConfig() {
63
- console.log("\nTest 2: lint({ code: 'a { color: pink; }', config: { rules: { 'color-named': 'never' } } })");
89
+ section("Test 2: lint({ code: 'a { color: pink; }', config: { rules: { 'color-named': 'never' } } })");
64
90
 
65
91
  try {
66
92
  const result = await lint({
@@ -86,13 +112,10 @@ async function testLintCodeWithConfig() {
86
112
  w.rule === "color-named",
87
113
  `warning rule is "color-named" (got "${w.rule}")`,
88
114
  );
89
- } else {
90
- console.log(" INFO: No warnings returned (gale may not flag this without config).");
91
115
  }
92
116
  }
93
117
  } catch (err) {
94
- console.error(` ERROR: ${err.message}`);
95
- failed++;
118
+ fail(`ERROR: ${err.message}`);
96
119
  }
97
120
  }
98
121
 
@@ -101,7 +124,7 @@ async function testLintCodeWithConfig() {
101
124
  // ---------------------------------------------------------------------------
102
125
 
103
126
  async function testResolveConfig() {
104
- console.log("\nTest 3: resolveConfig('test.css')");
127
+ section("Test 3: resolveConfig('test.css')");
105
128
 
106
129
  try {
107
130
  const config = await resolveConfig("test.css");
@@ -111,8 +134,7 @@ async function testResolveConfig() {
111
134
  "resolveConfig returns object or undefined",
112
135
  );
113
136
  } catch (err) {
114
- console.error(` ERROR: ${err.message}`);
115
- failed++;
137
+ fail(`ERROR: ${err.message}`);
116
138
  }
117
139
  }
118
140
 
@@ -121,7 +143,7 @@ async function testResolveConfig() {
121
143
  // ---------------------------------------------------------------------------
122
144
 
123
145
  async function testFormattersJson() {
124
- console.log("\nTest 4: formatters.json resolves to a function");
146
+ section("Test 4: formatters.json resolves to a function");
125
147
 
126
148
  try {
127
149
  const jsonFormatter = await formatters.json;
@@ -144,8 +166,7 @@ async function testFormattersJson() {
144
166
  const parsed = JSON.parse(output);
145
167
  assert(Array.isArray(parsed), "JSON formatter output is parseable as array");
146
168
  } catch (err) {
147
- console.error(` ERROR: ${err.message}`);
148
- failed++;
169
+ fail(`ERROR: ${err.message}`);
149
170
  }
150
171
  }
151
172
 
@@ -154,9 +175,9 @@ async function testFormattersJson() {
154
175
  // ---------------------------------------------------------------------------
155
176
 
156
177
  async function testCreatePlugin() {
157
- console.log("\nTest 5: createPlugin returns stub");
178
+ section("Test 5: createPlugin returns stub");
158
179
 
159
- const plugin = createPlugin("my-rule", () => {});
180
+ const plugin = quietly(() => createPlugin("my-rule", () => {}));
160
181
  assert(plugin.ruleName === "my-rule", 'plugin.ruleName is "my-rule"');
161
182
  assert(typeof plugin.rule === "function", "plugin.rule is a function");
162
183
  }
@@ -166,7 +187,7 @@ async function testCreatePlugin() {
166
187
  // ---------------------------------------------------------------------------
167
188
 
168
189
  async function testLinterResultShape() {
169
- console.log("\nTest 6: LinterResult has correct shape");
190
+ section("Test 6: LinterResult has correct shape");
170
191
 
171
192
  try {
172
193
  const result = await lint({ code: "a { color: red; }" });
@@ -179,8 +200,39 @@ async function testLinterResultShape() {
179
200
  assert("maxWarningsExceeded" in result, "result has maxWarningsExceeded key");
180
201
  assert("code" in result, "result has code key");
181
202
  } catch (err) {
182
- console.error(` ERROR: ${err.message}`);
183
- failed++;
203
+ fail(`ERROR: ${err.message}`);
204
+ }
205
+ }
206
+
207
+ // ---------------------------------------------------------------------------
208
+ // Test 7: CommonJS entry point bridges to the ESM implementation
209
+ // ---------------------------------------------------------------------------
210
+
211
+ async function testCommonJsEntry() {
212
+ section("Test 7: require('./index.cjs') exposes the same API");
213
+
214
+ try {
215
+ const cjs = require("./index.cjs");
216
+
217
+ assert(typeof cjs.lint === "function", "cjs.lint is a function");
218
+ assert(typeof cjs.resolveConfig === "function", "cjs.resolveConfig is a function");
219
+ assert(typeof cjs.createPlugin === "function", "cjs.createPlugin is a function");
220
+ assert(cjs.default === cjs, "cjs.default points back at module.exports");
221
+
222
+ const jsonFormatter = await cjs.formatters.json;
223
+ assert(typeof jsonFormatter === "function", "cjs.formatters.json resolves to a function");
224
+
225
+ const result = await cjs.lint({
226
+ code: "a {}",
227
+ config: { rules: { "block-no-empty": true } },
228
+ });
229
+ assert(Array.isArray(result.results), "cjs.lint returns results");
230
+ assert(
231
+ result.results[0]?.warnings[0]?.rule === "block-no-empty",
232
+ "cjs.lint reports block-no-empty",
233
+ );
234
+ } catch (err) {
235
+ fail(`ERROR: ${err.message}`);
184
236
  }
185
237
  }
186
238
 
@@ -189,16 +241,15 @@ async function testLinterResultShape() {
189
241
  // ---------------------------------------------------------------------------
190
242
 
191
243
  async function main() {
192
- console.log("=== @codebend3r/gale programmatic API tests ===");
193
-
194
244
  await testLintCodeEmptyBlock();
195
245
  await testLintCodeWithConfig();
196
246
  await testResolveConfig();
197
247
  await testFormattersJson();
198
248
  await testCreatePlugin();
199
249
  await testLinterResultShape();
250
+ await testCommonJsEntry();
200
251
 
201
- console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`);
252
+ console.log(`\n${passed} passed, ${failed} failed (Node ${process.versions.node})`);
202
253
 
203
254
  if (failed > 0) {
204
255
  process.exit(1);
package/bin/gale DELETED
@@ -1,54 +0,0 @@
1
- #!/bin/sh
2
-
3
- set -eu
4
-
5
- platform="$(uname -s)-$(uname -m)"
6
-
7
- case "$platform" in
8
- Darwin-arm64)
9
- target="aarch64-apple-darwin"
10
- ;;
11
- Darwin-x86_64)
12
- target="x86_64-apple-darwin"
13
- ;;
14
- Linux-aarch64|Linux-arm64)
15
- target="aarch64-unknown-linux-gnu"
16
- ;;
17
- Linux-x86_64)
18
- target="x86_64-unknown-linux-gnu"
19
- ;;
20
- *)
21
- echo "Unsupported platform: $platform" >&2
22
- echo "Supported: Darwin-arm64, Darwin-x86_64, Linux-aarch64, Linux-x86_64" >&2
23
- exit 1
24
- ;;
25
- esac
26
-
27
- script="$0"
28
-
29
- while [ -L "$script" ]; do
30
- script_dir="$(CDPATH= cd "$(dirname "$script")" && pwd)"
31
- link="$(readlink "$script")"
32
-
33
- case "$link" in
34
- /*)
35
- script="$link"
36
- ;;
37
- *)
38
- script="$script_dir/$link"
39
- ;;
40
- esac
41
- done
42
-
43
- bin_dir="$(CDPATH= cd "$(dirname "$script")" && pwd)"
44
- binary="$bin_dir/$target/gale"
45
-
46
- if [ ! -x "$binary" ]; then
47
- echo "Gale binary missing for $target at $binary" >&2
48
- echo "This package should ship prebuilt binaries for $target." >&2
49
- echo "Try reinstalling the package, or build from source:" >&2
50
- echo " cargo install gale-lint" >&2
51
- exit 1
52
- fi
53
-
54
- exec "$binary" "$@"