lintcn 0.4.0 → 0.6.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/CHANGELOG.md CHANGED
@@ -1,14 +1,70 @@
1
- ## 0.4.0
1
+ ## 0.6.0
2
+
3
+ 1. **Rules now live in subfolders** — each rule is its own Go package under `.lintcn/{rule_name}/`, replacing the old flat `.lintcn/*.go` layout. This eliminates the need to rename `options.go` and `schema.json` companions — they stay in the subfolder with their original names, and the Go package name matches the folder. `lintcn add` now fetches the entire rule folder automatically.
2
4
 
3
- 1. **Simpler rule imports** — rules now import from `pkg/rule`, `pkg/utils`, etc. instead of `internal/rule`. The `internal/` child-module-path hack is gone. Your `.go` files use clean import paths:
4
- ```go
5
- import (
6
- "github.com/typescript-eslint/tsgolint/pkg/rule"
7
- "github.com/typescript-eslint/tsgolint/pkg/utils"
8
- )
9
5
  ```
6
+ .lintcn/
7
+ no_floating_promises/
8
+ no_floating_promises.go
9
+ no_floating_promises_test.go
10
+ options.go ← original name, no renaming
11
+ schema.json
12
+ my_custom_rule/
13
+ my_custom_rule.go
14
+ ```
15
+
16
+ 2. **`lintcn add` fetches whole folders** — both folder URLs (`/tree/`) and file URLs (`/blob/`) now fetch every `.go` and `.json` file in the rule's directory. Passing a file URL auto-detects the parent folder:
10
17
 
11
- 2. **Simpler codegen** — uses a tsgolint fork with `pkg/runner.Run()`, eliminating all regex surgery on main.go. The generated binary entry point is a 15-line template instead of a patched copy of tsgolint's main.go.
18
+ ```bash
19
+ # folder URL
20
+ lintcn add https://github.com/oxc-project/tsgolint/tree/main/internal/rules/no_floating_promises
21
+
22
+ # file URL — auto-fetches the whole folder
23
+ lintcn add https://github.com/oxc-project/tsgolint/blob/main/internal/rules/await_thenable/await_thenable.go
24
+ ```
25
+
26
+ 3. **Error for flat `.go` files in `.lintcn/`** — if leftover flat files from older versions are detected, lintcn now prints a clear migration error with instructions instead of silently ignoring them.
27
+
28
+ 4. **Reproducible builds with `-trimpath`** — the Go binary is now built with `-trimpath`, stripping absolute paths from the output. Binaries are identical across machines for the same rule content + tsgolint version + platform.
29
+
30
+ 5. **Faster cache hits** — Go version removed from the content hash. The compiled binary is a standalone executable with no Go runtime dependency, so the Go version used to build it doesn't affect correctness. Also excludes `_test.go` files from the hash since tests don't affect the binary.
31
+
32
+ 6. **Go compilation output is live** — `go build` now inherits stdio, so compilation progress and errors stream directly to the terminal instead of being silently captured.
33
+
34
+ 7. **First-build guidance** — on first compile (cold Go cache), lintcn explains the one-time 30s cost and shows which directories to cache in CI:
35
+ ```
36
+ Compiling custom tsgolint binary (first build — may take 30s+ to compile dependencies)...
37
+ Subsequent builds will be fast (~1s). In CI, cache ~/.cache/lintcn/ and GOCACHE (run `go env GOCACHE`).
38
+ ```
39
+
40
+ 8. **GitHub Actions example** — README now includes a copy-paste workflow that caches the compiled binary. Subsequent CI runs take ~12s (vs ~4min cold):
41
+
42
+ ```yaml
43
+ - name: Cache lintcn binary + Go build cache
44
+ uses: actions/cache@v4
45
+ with:
46
+ path: |
47
+ ~/.cache/lintcn
48
+ ~/go/pkg
49
+ key: lintcn-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.lintcn/**/*.go') }}
50
+ restore-keys: lintcn-${{ runner.os }}-${{ runner.arch }}-
51
+ ```
52
+
53
+ ## 0.5.0
54
+
55
+ 1. **Security fix — path traversal in `--tsgolint-version`** — the version flag is now validated against a strict pattern. Previously a value like `../../etc` could escape the cache directory.
56
+
57
+ 2. **Fixed intermittent failures with concurrent `lintcn lint` runs** — build workspaces are now per-content-hash instead of shared. Two processes running simultaneously no longer corrupt each other's build.
58
+
59
+ 3. **Cross-platform tar extraction** — replaced shell `tar` command with the npm `tar` package. Works on Windows without needing system tar.
60
+
61
+ 4. **No more `patch` command required** — tsgolint downloads now use a fork with a clean `internal/runner.Run()` entry point. Zero modifications to existing tsgolint files; upstream syncs will never conflict.
62
+
63
+ 5. **Downloads no longer hang** — 120s timeout on all GitHub tarball downloads.
64
+
65
+ 6. **Fixed broken `.tsgolint` symlink** — `lintcn add` now correctly detects and recreates broken symlinks.
66
+
67
+ ## 0.4.0
12
68
 
13
69
  ## 0.3.0
14
70
 
package/README.md CHANGED
@@ -13,8 +13,11 @@ npm install -D lintcn
13
13
  ## Usage
14
14
 
15
15
  ```bash
16
- # Add a rule by URL
17
- npx lintcn add https://github.com/user/repo/blob/main/rules/no_unhandled_error.go
16
+ # Add a rule folder from tsgolint
17
+ npx lintcn add https://github.com/oxc-project/tsgolint/tree/main/internal/rules/no_floating_promises
18
+
19
+ # Add by file URL (auto-fetches the whole folder)
20
+ npx lintcn add https://github.com/oxc-project/tsgolint/blob/main/internal/rules/await_thenable/await_thenable.go
18
21
 
19
22
  # Lint your project
20
23
  npx lintcn lint
@@ -26,21 +29,33 @@ npx lintcn lint --tsconfig tsconfig.build.json
26
29
  npx lintcn list
27
30
 
28
31
  # Remove a rule
29
- npx lintcn remove no-unhandled-error
32
+ npx lintcn remove no-floating-promises
33
+
34
+ # Clean cached tsgolint source + binaries
35
+ npx lintcn clean
30
36
  ```
31
37
 
38
+ Browse all 50+ available built-in rules in the [tsgolint rules directory](https://github.com/oxc-project/tsgolint/tree/main/internal/rules).
39
+
32
40
  ## How it works
33
41
 
34
- Rules live as `.go` files in `.lintcn/` at your project root. You own the source — edit, customize, delete.
42
+ Each rule lives in its own subfolder under `.lintcn/`. You own the source — edit, customize, delete.
35
43
 
36
44
  ```
37
45
  my-project/
38
46
  ├── .lintcn/
39
- │ ├── .gitignore ← ignores generated Go files
40
- │ ├── no_unhandled_error.go ← your rule (committed)
41
- └── no_unhandled_error_test.go its tests (committed)
47
+ │ ├── .gitignore ← ignores generated Go files
48
+ │ ├── no_floating_promises/
49
+ │ ├── no_floating_promises.go rule source (committed)
50
+ │ │ ├── no_floating_promises_test.go ← tests (committed)
51
+ │ │ ├── options.go ← rule options struct
52
+ │ │ └── schema.json ← options schema
53
+ │ ├── await_thenable/
54
+ │ │ ├── await_thenable.go
55
+ │ │ └── await_thenable_test.go
56
+ │ └── my_custom_rule/
57
+ │ └── my_custom_rule.go
42
58
  ├── src/
43
- │ ├── index.ts
44
59
  │ └── ...
45
60
  ├── tsconfig.json
46
61
  └── package.json
@@ -48,22 +63,32 @@ my-project/
48
63
 
49
64
  When you run `npx lintcn lint`, the CLI:
50
65
 
51
- 1. Scans `.lintcn/*.go` for rule definitions
52
- 2. Generates a Go workspace with all 50+ built-in tsgolint rules + your custom rules
66
+ 1. Scans `.lintcn/*/` subfolders for rule definitions
67
+ 2. Generates a Go workspace with your custom rules
53
68
  3. Compiles a custom binary (cached — rebuilds only when rules change)
54
69
  4. Runs the binary against your project
55
70
 
56
- ## Writing a rule
71
+ You can run `lintcn lint` from any subdirectory — it walks up to find `.lintcn/` and lints the cwd project.
72
+
73
+ ## Writing custom rules
74
+
75
+ To help AI agents write and modify rules, install the lintcn skill:
76
+
77
+ ```bash
78
+ npx skills add remorses/lintcn
79
+ ```
57
80
 
58
- Every rule is a Go file with `package lintcn` that exports a `rule.Rule` variable.
81
+ This gives your AI agent the full tsgolint rule API reference AST visitors, type checker, reporting, fixes, and testing patterns.
59
82
 
60
- Here's a rule that errors when you discard the return value of a function that returns `Error | T` enforcing the [errore](https://errore.org) pattern:
83
+ Every rule lives in a subfolder under `.lintcn/` with the package name matching the folder:
61
84
 
62
85
  ```go
86
+ // .lintcn/no_unhandled_error/no_unhandled_error.go
87
+
63
88
  // lintcn:name no-unhandled-error
64
89
  // lintcn:description Disallow discarding Error-typed return values
65
90
 
66
- package lintcn
91
+ package no_unhandled_error
67
92
 
68
93
  import (
69
94
  "github.com/microsoft/typescript-go/shim/ast"
@@ -134,7 +159,7 @@ void getUser("id")
134
159
  ```json
135
160
  {
136
161
  "devDependencies": {
137
- "lintcn": "0.1.0"
162
+ "lintcn": "0.5.0"
138
163
  }
139
164
  }
140
165
  ```
@@ -145,17 +170,45 @@ Each lintcn release bundles a specific tsgolint version. Updating lintcn can cha
145
170
  2. Run `npx lintcn build` after updating to verify your rules still compile
146
171
  3. Fix any compilation errors before committing
147
172
 
148
- You can test against an unreleased tsgolint version without updating lintcn:
149
-
150
- ```bash
151
- npx lintcn lint --tsgolint-version v0.10.0
173
+ ## CI Setup
174
+
175
+ The first `lintcn lint` compiles a custom Go binary (~30s). Subsequent runs use the cached binary (<1s). Cache `~/.cache/lintcn/` and Go's build cache to keep CI fast.
176
+
177
+ ```yaml
178
+ # .github/workflows/lint.yml
179
+ name: Lint
180
+ on: [push, pull_request]
181
+
182
+ jobs:
183
+ lint:
184
+ runs-on: ubuntu-latest
185
+ steps:
186
+ - uses: actions/checkout@v4
187
+
188
+ - uses: actions/setup-node@v4
189
+ with:
190
+ node-version: 22
191
+
192
+ - name: Cache lintcn binary + Go build cache
193
+ uses: actions/cache@v4
194
+ with:
195
+ path: |
196
+ ~/.cache/lintcn
197
+ ~/go/pkg
198
+ key: lintcn-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.lintcn/**/*.go') }}
199
+ restore-keys: |
200
+ lintcn-${{ runner.os }}-${{ runner.arch }}-
201
+
202
+ - run: npm ci
203
+ - run: npx lintcn lint
152
204
  ```
153
205
 
206
+ The cache key includes a hash of your rule files — when rules change, the binary is recompiled. The `restore-keys` fallback ensures Go's build cache is still used even when rules change, so recompilation takes ~1s instead of 30s.
207
+
154
208
  ## Prerequisites
155
209
 
156
210
  - **Node.js** — for the CLI
157
- - **Go 1.26+** — for compiling rules (`go.dev/dl`)
158
- - **Git** — for cloning tsgolint source on first build
211
+ - **Go** — for compiling rules (`go.dev/dl`)
159
212
 
160
213
  Go is only needed for `lintcn lint` / `lintcn build`. Adding and listing rules works without Go.
161
214
 
package/dist/cache.d.ts CHANGED
@@ -1,9 +1,13 @@
1
- export declare const DEFAULT_TSGOLINT_VERSION = "a93604379da2631b70332a65bc47eb5ced689a3b";
1
+ export declare const DEFAULT_TSGOLINT_VERSION = "e945641eabec22993eda3e7c101692e80417e0ea";
2
+ /** Validate version string to prevent path traversal attacks.
3
+ * Only allows alphanumeric chars, dots, underscores, and hyphens. */
4
+ export declare function validateVersion(version: string): void;
2
5
  export declare function getCacheDir(): string;
3
6
  export declare function getTsgolintSourceDir(version: string): string;
4
7
  export declare function getBinDir(): string;
5
8
  export declare function getBinaryPath(contentHash: string): string;
6
- export declare function getBuildDir(): string;
9
+ /** Per-hash build directory to avoid races between concurrent lintcn processes. */
10
+ export declare function getBuildDir(contentHash: string): string;
7
11
  export declare function ensureTsgolintSource(version: string): Promise<string>;
8
12
  export declare function cachedBinaryExists(contentHash: string): boolean;
9
13
  //# sourceMappingURL=cache.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAiBA,eAAO,MAAM,wBAAwB,6CAA6C,CAAA;AAQlF,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAqCD,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA2D3E;AAED,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAQ/D"}
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAoBA,eAAO,MAAM,wBAAwB,6CAA6C,CAAA;AAUlF;sEACsE;AACtE,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAOrD;AAED,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,mFAAmF;AACnF,wBAAgB,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAEvD;AA0DD,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAuE3E;AAED,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAQ/D"}
package/dist/cache.js CHANGED
@@ -4,21 +4,33 @@
4
4
  //
5
5
  // Cache layout:
6
6
  // ~/.cache/lintcn/tsgolint/<version>/ — extracted source (read-only)
7
+ // ~/.cache/lintcn/build/<content-hash>/ — per-hash build workspace (no race)
7
8
  // ~/.cache/lintcn/bin/<content-hash> — compiled binaries
8
9
  import fs from 'node:fs';
9
10
  import os from 'node:os';
10
11
  import path from 'node:path';
12
+ import crypto from 'node:crypto';
11
13
  import { pipeline } from 'node:stream/promises';
14
+ import { extract } from 'tar';
12
15
  import { execAsync } from "./exec.js";
13
16
  // Pinned tsgolint fork commit — updated with each lintcn release.
14
- // Uses remorses/tsgolint fork which exposes pkg/runner.Run() and
15
- // moves internal/ packages to pkg/ for clean external imports.
16
- export const DEFAULT_TSGOLINT_VERSION = 'a93604379da2631b70332a65bc47eb5ced689a3b';
17
+ // Uses remorses/tsgolint fork which adds internal/runner.Run().
18
+ // Only 1 commit on top of upstream zero modifications to existing files.
19
+ export const DEFAULT_TSGOLINT_VERSION = 'e945641eabec22993eda3e7c101692e80417e0ea';
17
20
  // Pinned typescript-go base commit from microsoft/typescript-go (before patches).
18
21
  // Patches from tsgolint/patches/ are applied on top during setup.
19
- // This is the upstream commit the tsgolint submodule was forked from.
20
22
  // Must be updated when DEFAULT_TSGOLINT_VERSION changes.
21
23
  const TYPESCRIPT_GO_COMMIT = '1b7eabe122e1575a0df9c77eccdf4e063c623224';
24
+ // Strict pattern for version strings — prevents path traversal via ../
25
+ const VERSION_PATTERN = /^[a-zA-Z0-9._-]+$/;
26
+ /** Validate version string to prevent path traversal attacks.
27
+ * Only allows alphanumeric chars, dots, underscores, and hyphens. */
28
+ export function validateVersion(version) {
29
+ if (!VERSION_PATTERN.test(version)) {
30
+ throw new Error(`Invalid tsgolint version "${version}". ` +
31
+ 'Version must only contain alphanumeric characters, dots, underscores, and hyphens.');
32
+ }
33
+ }
22
34
  export function getCacheDir() {
23
35
  return path.join(os.homedir(), '.cache', 'lintcn');
24
36
  }
@@ -31,24 +43,47 @@ export function getBinDir() {
31
43
  export function getBinaryPath(contentHash) {
32
44
  return path.join(getBinDir(), contentHash);
33
45
  }
34
- export function getBuildDir() {
35
- return path.join(getCacheDir(), 'build');
46
+ /** Per-hash build directory to avoid races between concurrent lintcn processes. */
47
+ export function getBuildDir(contentHash) {
48
+ return path.join(getCacheDir(), 'build', contentHash);
36
49
  }
37
50
  /** Download a tarball from URL and extract it to targetDir.
51
+ * Uses the `tar` npm package for cross-platform support (no shell tar needed).
38
52
  * GitHub tarballs have a top-level directory like `repo-ref/`,
39
53
  * so we strip the first path component during extraction. */
40
54
  async function downloadAndExtract(url, targetDir) {
41
- const response = await fetch(url);
55
+ const controller = new AbortController();
56
+ const timeout = setTimeout(() => {
57
+ controller.abort(new Error(`Download timed out after 120s: ${url}`));
58
+ }, 120_000);
59
+ let response;
60
+ try {
61
+ response = await fetch(url, { signal: controller.signal });
62
+ }
63
+ finally {
64
+ clearTimeout(timeout);
65
+ }
42
66
  if (!response.ok || !response.body) {
43
67
  throw new Error(`Failed to download ${url}: ${response.status} ${response.statusText}`);
44
68
  }
45
- fs.mkdirSync(targetDir, { recursive: true });
46
- const tmpTarGz = path.join(os.tmpdir(), `lintcn-${Date.now()}.tar.gz`);
47
- const fileStream = fs.createWriteStream(tmpTarGz);
48
- // @ts-ignore ReadableStream vs NodeJS.ReadableStream mismatch
49
- await pipeline(response.body, fileStream);
50
- await execAsync('tar', ['xzf', tmpTarGz, '--strip-components=1', '-C', targetDir]);
51
- fs.rmSync(tmpTarGz, { force: true });
69
+ // download to temp file with random suffix to avoid collisions
70
+ const tmpTarGz = path.join(os.tmpdir(), `lintcn-${crypto.randomBytes(8).toString('hex')}.tar.gz`);
71
+ try {
72
+ const fileStream = fs.createWriteStream(tmpTarGz);
73
+ // @ts-ignore ReadableStream vs NodeJS.ReadableStream mismatch
74
+ await pipeline(response.body, fileStream);
75
+ // extract with npm tar package (cross-platform, no shell tar needed)
76
+ fs.mkdirSync(targetDir, { recursive: true });
77
+ await extract({
78
+ file: tmpTarGz,
79
+ cwd: targetDir,
80
+ strip: 1,
81
+ });
82
+ }
83
+ finally {
84
+ // always clean up temp file
85
+ fs.rmSync(tmpTarGz, { force: true });
86
+ }
52
87
  }
53
88
  /** Apply git-format patches using `patch -p1` (no git required).
54
89
  * Patches are standard unified diff format, `patch` ignores the git metadata. */
@@ -63,12 +98,17 @@ async function applyPatches(patchesDir, targetDir) {
63
98
  return patches.length;
64
99
  }
65
100
  export async function ensureTsgolintSource(version) {
101
+ validateVersion(version);
66
102
  const sourceDir = getTsgolintSourceDir(version);
67
103
  const readyMarker = path.join(sourceDir, '.lintcn-ready');
68
104
  if (fs.existsSync(readyMarker)) {
69
105
  return sourceDir;
70
106
  }
71
- // clean up any partial previous attempt so we start fresh
107
+ // Use a temp directory for the download, then atomic rename on success.
108
+ // This prevents concurrent processes from seeing partial state, and
109
+ // avoids the "non-empty dir on retry" problem.
110
+ const tmpDir = path.join(getCacheDir(), 'tsgolint', `.tmp-${version}-${crypto.randomBytes(4).toString('hex')}`);
111
+ // clean up any partial previous attempt
72
112
  if (fs.existsSync(sourceDir)) {
73
113
  fs.rmSync(sourceDir, { recursive: true });
74
114
  }
@@ -76,14 +116,14 @@ export async function ensureTsgolintSource(version) {
76
116
  // download tsgolint fork tarball
77
117
  console.log(`Downloading tsgolint@${version.slice(0, 8)}...`);
78
118
  const tsgolintUrl = `https://github.com/remorses/tsgolint/archive/${version}.tar.gz`;
79
- await downloadAndExtract(tsgolintUrl, sourceDir);
119
+ await downloadAndExtract(tsgolintUrl, tmpDir);
80
120
  // download typescript-go from microsoft (base commit before patches)
81
- const tsGoDir = path.join(sourceDir, 'typescript-go');
121
+ const tsGoDir = path.join(tmpDir, 'typescript-go');
82
122
  console.log('Downloading typescript-go...');
83
123
  const tsGoUrl = `https://github.com/microsoft/typescript-go/archive/${TYPESCRIPT_GO_COMMIT}.tar.gz`;
84
124
  await downloadAndExtract(tsGoUrl, tsGoDir);
85
125
  // apply tsgolint's patches to typescript-go
86
- const patchesDir = path.join(sourceDir, 'patches');
126
+ const patchesDir = path.join(tmpDir, 'patches');
87
127
  if (fs.existsSync(patchesDir)) {
88
128
  const count = await applyPatches(patchesDir, tsGoDir);
89
129
  if (count > 0) {
@@ -91,7 +131,7 @@ export async function ensureTsgolintSource(version) {
91
131
  }
92
132
  }
93
133
  // copy internal/collections from typescript-go (required by tsgolint, done by `just init`)
94
- const collectionsDir = path.join(sourceDir, 'internal', 'collections');
134
+ const collectionsDir = path.join(tmpDir, 'internal', 'collections');
95
135
  const tsGoCollections = path.join(tsGoDir, 'internal', 'collections');
96
136
  if (fs.existsSync(tsGoCollections)) {
97
137
  fs.mkdirSync(collectionsDir, { recursive: true });
@@ -103,13 +143,16 @@ export async function ensureTsgolintSource(version) {
103
143
  }
104
144
  }
105
145
  // write ready marker
106
- fs.writeFileSync(readyMarker, new Date().toISOString());
146
+ fs.writeFileSync(path.join(tmpDir, '.lintcn-ready'), new Date().toISOString());
147
+ // atomic rename: move completed dir to final location
148
+ fs.mkdirSync(path.dirname(sourceDir), { recursive: true });
149
+ fs.renameSync(tmpDir, sourceDir);
107
150
  console.log('tsgolint source ready');
108
151
  }
109
152
  catch (err) {
110
- // clean up partial download so next run starts fresh
111
- if (fs.existsSync(sourceDir)) {
112
- fs.rmSync(sourceDir, { recursive: true });
153
+ // clean up partial temp directory
154
+ if (fs.existsSync(tmpDir)) {
155
+ fs.rmSync(tmpDir, { recursive: true });
113
156
  }
114
157
  throw err;
115
158
  }
package/dist/cli.js CHANGED
@@ -7,16 +7,17 @@ import { addRule } from "./commands/add.js";
7
7
  import { lint, buildBinary } from "./commands/lint.js";
8
8
  import { listRules } from "./commands/list.js";
9
9
  import { removeRule } from "./commands/remove.js";
10
+ import { clean } from "./commands/clean.js";
10
11
  import { DEFAULT_TSGOLINT_VERSION } from "./cache.js";
11
12
  const require = createRequire(import.meta.url);
12
13
  const packageJson = require('../package.json');
13
14
  const cli = goke('lintcn');
14
15
  cli
15
- .command('add <url>', 'Add a rule by URL. Fetches the .go file and copies it into .lintcn/')
16
- .example('# Add a rule from GitHub')
17
- .example('lintcn add https://github.com/user/repo/blob/main/rules/no_floating_promises.go')
18
- .example('# Add from raw URL')
19
- .example('lintcn add https://raw.githubusercontent.com/user/repo/main/rules/no_unused_result.go')
16
+ .command('add <url>', 'Add a rule by GitHub URL. Fetches the whole folder into .lintcn/{rule}/')
17
+ .example('# Add a rule folder')
18
+ .example('lintcn add https://github.com/oxc-project/tsgolint/tree/main/internal/rules/no_floating_promises')
19
+ .example('# Add by file URL (auto-fetches the whole folder)')
20
+ .example('lintcn add https://github.com/oxc-project/tsgolint/blob/main/internal/rules/await_thenable/await_thenable.go')
20
21
  .action(async (url) => {
21
22
  await addRule(url);
22
23
  });
@@ -67,6 +68,11 @@ cli
67
68
  const binaryPath = await buildBinary({ rebuild: !!options.rebuild, tsgolintVersion });
68
69
  console.log(binaryPath);
69
70
  });
71
+ cli
72
+ .command('clean', 'Remove cached tsgolint source and compiled binaries to free disk space')
73
+ .action(() => {
74
+ clean();
75
+ });
70
76
  cli.help();
71
77
  cli.version(packageJson.version);
72
78
  cli.parse();
package/dist/codegen.d.ts CHANGED
@@ -1,9 +1,7 @@
1
1
  import type { RuleMetadata } from './discover.ts';
2
2
  /** Generate .lintcn/go.work and .lintcn/go.mod for editor/gopls support. */
3
3
  export declare function generateEditorGoFiles(lintcnDir: string): void;
4
- /** Generate build workspace for compiling the custom binary.
5
- * With pkg/runner.Run(), the generated main.go is a static template —
6
- * no regex surgery or file copying needed. */
4
+ /** Generate build workspace for compiling the custom binary. */
7
5
  export declare function generateBuildWorkspace({ buildDir, tsgolintDir, lintcnDir, rules, }: {
8
6
  buildDir: string;
9
7
  tsgolintDir: string;
@@ -1 +1 @@
1
- {"version":3,"file":"codegen.d.ts","sourceRoot":"","sources":["../src/codegen.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AA2BjD,4EAA4E;AAC5E,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAkC7D;AAED;;+CAE+C;AAC/C,wBAAgB,sBAAsB,CAAC,EACrC,QAAQ,EACR,WAAW,EACX,SAAS,EACT,KAAK,GACN,EAAE;IACD,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,YAAY,EAAE,CAAA;CACtB,GAAG,IAAI,CA2CP"}
1
+ {"version":3,"file":"codegen.d.ts","sourceRoot":"","sources":["../src/codegen.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AA4BjD,4EAA4E;AAC5E,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAmC7D;AAED,gEAAgE;AAChE,wBAAgB,sBAAsB,CAAC,EACrC,QAAQ,EACR,WAAW,EACX,SAAS,EACT,KAAK,GACN,EAAE;IACD,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,YAAY,EAAE,CAAA;CACtB,GAAG,IAAI,CA2CP"}
package/dist/codegen.js CHANGED
@@ -1,11 +1,12 @@
1
1
  // Generate Go workspace files for building a custom tsgolint binary.
2
- // With the fork (remorses/tsgolint) exposing pkg/runner.Run(), codegen is
3
- // minimal: a 10-line main.go template + go.work with shim replaces.
4
- // No regex surgery, no file copying, no fragile string manipulation.
2
+ // Uses internal/runner.Run() from the fork — codegen is just a static
3
+ // main.go template + go.work with shim replaces.
4
+ //
5
+ // Key: module names must be child paths of github.com/typescript-eslint/tsgolint
6
+ // so Go allows importing internal/ packages across the module boundary.
5
7
  import fs from 'node:fs';
6
8
  import path from 'node:path';
7
9
  // Shim modules that need replace directives in go.work.
8
- // These redirect module paths to local directories inside the tsgolint source.
9
10
  const SHIM_MODULES = [
10
11
  'ast',
11
12
  'bundled',
@@ -22,6 +23,7 @@ const SHIM_MODULES = [
22
23
  'vfs/cachedvfs',
23
24
  'vfs/osvfs',
24
25
  ];
26
+ const TSGOLINT_MODULE = 'github.com/typescript-eslint/tsgolint';
25
27
  function generateReplaceDirectives(tsgolintRelPath) {
26
28
  return SHIM_MODULES.map((mod) => {
27
29
  return `\tgithub.com/microsoft/typescript-go/shim/${mod} => ${tsgolintRelPath}/shim/${mod}`;
@@ -41,8 +43,9 @@ replace (
41
43
  ${generateReplaceDirectives('./.tsgolint')}
42
44
  )
43
45
  `;
44
- // No child-path hack neededpkg/ is public, any module name works
45
- const goMod = `module lintcn-rules
46
+ // Module name is a child path of tsgolintthis is required so Go allows
47
+ // importing internal/ packages across the module boundary in a workspace.
48
+ const goMod = `module ${TSGOLINT_MODULE}/lintcn-rules
46
49
 
47
50
  go 1.26
48
51
  `;
@@ -59,9 +62,7 @@ go.sum
59
62
  fs.writeFileSync(gitignorePath, gitignore);
60
63
  }
61
64
  }
62
- /** Generate build workspace for compiling the custom binary.
63
- * With pkg/runner.Run(), the generated main.go is a static template —
64
- * no regex surgery or file copying needed. */
65
+ /** Generate build workspace for compiling the custom binary. */
65
66
  export function generateBuildWorkspace({ buildDir, tsgolintDir, lintcnDir, rules, }) {
66
67
  fs.mkdirSync(path.join(buildDir, 'wrapper'), { recursive: true });
67
68
  // symlink tsgolint source
@@ -91,21 +92,37 @@ ${generateReplaceDirectives('./tsgolint')}
91
92
  )
92
93
  `;
93
94
  fs.writeFileSync(path.join(buildDir, 'go.work'), goWork);
94
- // wrapper/go.modsimple module name, no child-path hack needed
95
- const wrapperGoMod = `module lintcn-wrapper
95
+ // wrapper module child path of tsgolint for internal/ access
96
+ const wrapperGoMod = `module ${TSGOLINT_MODULE}/lintcn-wrapper
96
97
 
97
98
  go 1.26
98
99
  `;
99
100
  fs.writeFileSync(path.join(buildDir, 'wrapper', 'go.mod'), wrapperGoMod);
100
- // wrapper/main.go — simple template, no regex or string surgery
101
+ // wrapper/main.go — static template
101
102
  const mainGo = generateMainGo(rules);
102
103
  fs.writeFileSync(path.join(buildDir, 'wrapper', 'main.go'), mainGo);
103
104
  }
104
- /** Generate a minimal main.go that imports user rules and calls runner.Run().
105
- * This is a static template no copying or patching of tsgolint source. */
105
+ /** Sanitize a package name into a valid Go identifier for use as an import alias.
106
+ * Replaces hyphens/dots with underscores, prepends _ if starts with a digit. */
107
+ function toGoAlias(pkg) {
108
+ let alias = pkg.replace(/[^a-zA-Z0-9_]/g, '_');
109
+ if (/^[0-9]/.test(alias)) {
110
+ alias = '_' + alias;
111
+ }
112
+ return alias;
113
+ }
114
+ /** Generate main.go that imports user rules and calls internal/runner.Run().
115
+ * Each rule subfolder is its own Go package, imported by package name. */
106
116
  function generateMainGo(rules) {
117
+ // Deduplicate imports by package name (in case a subfolder has multiple rules)
118
+ const uniquePackages = [...new Set(rules.map((r) => { return r.packageName; }))];
119
+ const imports = uniquePackages.map((pkg) => {
120
+ const alias = toGoAlias(pkg);
121
+ return `\t${alias} "${TSGOLINT_MODULE}/lintcn-rules/${pkg}"`;
122
+ }).join('\n');
107
123
  const ruleEntries = rules.map((r) => {
108
- return `\t\tlintcn.${r.varName},`;
124
+ const alias = toGoAlias(r.packageName);
125
+ return `\t\t${alias}.${r.varName},`;
109
126
  }).join('\n');
110
127
  return `// Code generated by lintcn. DO NOT EDIT.
111
128
  package main
@@ -113,9 +130,9 @@ package main
113
130
  import (
114
131
  \t"os"
115
132
 
116
- \t"github.com/typescript-eslint/tsgolint/pkg/rule"
117
- \t"github.com/typescript-eslint/tsgolint/pkg/runner"
118
- \tlintcn "lintcn-rules"
133
+ \t"${TSGOLINT_MODULE}/internal/rule"
134
+ \t"${TSGOLINT_MODULE}/internal/runner"
135
+ ${imports}
119
136
  )
120
137
 
121
138
  func main() {
@@ -1 +1 @@
1
- {"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../../src/commands/add.ts"],"names":[],"mappings":"AA+EA,wBAAsB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqDxD"}
1
+ {"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../../src/commands/add.ts"],"names":[],"mappings":"AAwHA,wBAAsB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA4ExD"}