shingan-lint 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +64 -0
- package/bin/shingan.js +66 -0
- package/package.json +56 -0
- package/scripts/postinstall.js +186 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 hatyibei
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# shingan-lint
|
|
2
|
+
|
|
3
|
+
> AI Agent Workflow Static Analyzer — `npx`-installable wrapper for [Shingan](https://github.com/hatyibei/shingan).
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
# zero-install one-shot
|
|
7
|
+
npx shingan-lint analyze --format adk-go --input ./agents
|
|
8
|
+
|
|
9
|
+
# project-local
|
|
10
|
+
pnpm add -D shingan-lint
|
|
11
|
+
pnpm exec shingan analyze --format json --input ./testdata/buggy.json
|
|
12
|
+
|
|
13
|
+
# global
|
|
14
|
+
npm install -g shingan-lint
|
|
15
|
+
shingan analyze --since main
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## What it does
|
|
19
|
+
|
|
20
|
+
Shingan detects 20 classes of design-time bugs in AI agent workflows **before they ship**:
|
|
21
|
+
|
|
22
|
+
- **Infinite loops** (`loop_guard`, `cycle_detection`)
|
|
23
|
+
- **Cost explosions** (`cost_estimation`, `retry_storm`, `max_parallel_branches`)
|
|
24
|
+
- **PII / prompt injection** (`pii_leak_scanner`, `prompt_injection_sink`, `secret_in_prompt_template`)
|
|
25
|
+
- **Code-execution risk** (`eval_missing`, `dynamic_node_construction`)
|
|
26
|
+
- **Misconfiguration** (`deprecated_model`, `model_card_mismatch`, `temperature_misuse`)
|
|
27
|
+
- **Architectural smells** (`circular_dep_agents`, `unbounded_tool_arg`, `error_handler_checker`, `redundant_llm_call`, `unreachable_node`, `missing_eval_dataset`)
|
|
28
|
+
|
|
29
|
+
Supported frameworks: **LangGraph** (Python), **ADK-Go** (Google), **n8n** / **SamuraiAI** / generic JSON.
|
|
30
|
+
|
|
31
|
+
## How this package works
|
|
32
|
+
|
|
33
|
+
`shingan-lint` is a thin Node wrapper. On `npm install` it downloads the platform-specific Go binary from the matching GitHub Release, verifies its SHA-256 against `checksums.txt`, and installs it under `~/.cache/shingan-lint/v<version>/`. Subsequent `shingan` invocations spawn the cached binary directly — no Node overhead at runtime.
|
|
34
|
+
|
|
35
|
+
| Platform | Supported |
|
|
36
|
+
|---|---|
|
|
37
|
+
| Linux x64 / arm64 | ✅ |
|
|
38
|
+
| macOS Intel / Apple Silicon | ✅ |
|
|
39
|
+
| Windows x64 / arm64 | ✅ |
|
|
40
|
+
|
|
41
|
+
If your platform isn't listed, install via Go directly:
|
|
42
|
+
```bash
|
|
43
|
+
go install github.com/hatyibei/shingan/cmd/shingan@v0.6.0
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Environment variables
|
|
47
|
+
|
|
48
|
+
| var | purpose |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `SHINGAN_SKIP_POSTINSTALL=1` | skip the download step (air-gapped CI; you provide the binary yourself) |
|
|
51
|
+
| `SHINGAN_CACHE_DIR=/some/dir` | override `~/.cache` for the binary cache |
|
|
52
|
+
| `SHINGAN_DOWNLOAD_BASE=https://mirror/...` | mirror base URL (corporate proxies) |
|
|
53
|
+
|
|
54
|
+
## Documentation
|
|
55
|
+
|
|
56
|
+
Full rule list, ADRs, LSP setup, and contributing guide live in the main repository:
|
|
57
|
+
- [README](https://github.com/hatyibei/shingan)
|
|
58
|
+
- [Rule authoring guide](https://github.com/hatyibei/shingan/blob/main/docs/rule-authoring.md)
|
|
59
|
+
- [LSP integration](https://github.com/hatyibei/shingan/blob/main/docs/lsp.md)
|
|
60
|
+
- [ADRs](https://github.com/hatyibei/shingan/blob/main/shingan-adr.md)
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
MIT — see [LICENSE](https://github.com/hatyibei/shingan/blob/main/LICENSE).
|
package/bin/shingan.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// shingan CLI shim. Spawns the platform-specific Go binary downloaded
|
|
3
|
+
// by postinstall.js into ~/.cache/shingan-lint/v<version>/<binary>.
|
|
4
|
+
// Forwards every argv element + stdio + exit code, so `npx shingan
|
|
5
|
+
// analyze --input ./testdata` behaves identically to running the
|
|
6
|
+
// native binary directly.
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const { spawn } = require('child_process');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
|
|
15
|
+
const PACKAGE_VERSION = require('../package.json').version;
|
|
16
|
+
|
|
17
|
+
function platformTag() {
|
|
18
|
+
const p = process.platform;
|
|
19
|
+
const a = process.arch;
|
|
20
|
+
if (p === 'darwin' && a === 'arm64') return { os: 'darwin', arch: 'arm64', exe: 'shingan' };
|
|
21
|
+
if (p === 'darwin' && a === 'x64') return { os: 'darwin', arch: 'amd64', exe: 'shingan' };
|
|
22
|
+
if (p === 'linux' && a === 'arm64') return { os: 'linux', arch: 'arm64', exe: 'shingan' };
|
|
23
|
+
if (p === 'linux' && a === 'x64') return { os: 'linux', arch: 'amd64', exe: 'shingan' };
|
|
24
|
+
if (p === 'win32' && a === 'arm64') return { os: 'windows', arch: 'arm64', exe: 'shingan.exe' };
|
|
25
|
+
if (p === 'win32' && a === 'x64') return { os: 'windows', arch: 'amd64', exe: 'shingan.exe' };
|
|
26
|
+
throw new Error(`shingan-lint: unsupported platform/arch combination: ${p}/${a}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function cacheDir() {
|
|
30
|
+
const base = process.env.SHINGAN_CACHE_DIR ||
|
|
31
|
+
process.env.XDG_CACHE_HOME ||
|
|
32
|
+
path.join(os.homedir(), '.cache');
|
|
33
|
+
return path.join(base, 'shingan-lint', `v${PACKAGE_VERSION}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function binaryPath() {
|
|
37
|
+
const t = platformTag();
|
|
38
|
+
return path.join(cacheDir(), t.exe);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function main() {
|
|
42
|
+
const bin = binaryPath();
|
|
43
|
+
if (!fs.existsSync(bin)) {
|
|
44
|
+
console.error(
|
|
45
|
+
`shingan-lint: binary not found at ${bin}\n` +
|
|
46
|
+
`Run \`npm rebuild shingan-lint\` (or \`pnpm rebuild shingan-lint\`) to re-trigger postinstall, ` +
|
|
47
|
+
`or download manually from https://github.com/hatyibei/shingan/releases/tag/v${PACKAGE_VERSION}`
|
|
48
|
+
);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
const child = spawn(bin, process.argv.slice(2), { stdio: 'inherit' });
|
|
52
|
+
child.on('error', (err) => {
|
|
53
|
+
console.error(`shingan-lint: failed to exec ${bin}: ${err.message}`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
});
|
|
56
|
+
child.on('exit', (code, signal) => {
|
|
57
|
+
if (signal) {
|
|
58
|
+
// Mirror the signal: re-raise so shells see e.g. 130 for SIGINT.
|
|
59
|
+
process.kill(process.pid, signal);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
process.exit(code === null ? 1 : code);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "shingan-lint",
|
|
3
|
+
"version": "0.6.1",
|
|
4
|
+
"description": "AI Agent Workflow Static Analyzer — detect infinite loops, cost explosions, PII leaks, prompt injection sinks before execution. CLI wrapper that downloads the platform-specific Go binary from GitHub Releases.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai-agent",
|
|
7
|
+
"workflow",
|
|
8
|
+
"static-analysis",
|
|
9
|
+
"lint",
|
|
10
|
+
"linter",
|
|
11
|
+
"langgraph",
|
|
12
|
+
"adk",
|
|
13
|
+
"samurai",
|
|
14
|
+
"llm",
|
|
15
|
+
"prompt-injection",
|
|
16
|
+
"shingan"
|
|
17
|
+
],
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"homepage": "https://github.com/hatyibei/shingan",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/hatyibei/shingan.git",
|
|
23
|
+
"directory": "npm"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/hatyibei/shingan/issues"
|
|
27
|
+
},
|
|
28
|
+
"author": "hatyibei <hathibei7@gmail.com>",
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18"
|
|
31
|
+
},
|
|
32
|
+
"os": [
|
|
33
|
+
"darwin",
|
|
34
|
+
"linux",
|
|
35
|
+
"win32"
|
|
36
|
+
],
|
|
37
|
+
"cpu": [
|
|
38
|
+
"x64",
|
|
39
|
+
"arm64"
|
|
40
|
+
],
|
|
41
|
+
"bin": {
|
|
42
|
+
"shingan": "./bin/shingan.js"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"postinstall": "node ./scripts/postinstall.js"
|
|
46
|
+
},
|
|
47
|
+
"files": [
|
|
48
|
+
"bin/shingan.js",
|
|
49
|
+
"scripts/postinstall.js",
|
|
50
|
+
"README.md",
|
|
51
|
+
"LICENSE"
|
|
52
|
+
],
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"tar": "^7.4.3"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// shingan-lint postinstall: downloads the platform-specific Go binary
|
|
3
|
+
// from the matching GitHub Release tag, verifies it via the
|
|
4
|
+
// checksums.txt sha256 entry, extracts it from the goreleaser tarball,
|
|
5
|
+
// and installs it under ~/.cache/shingan-lint/v<version>/.
|
|
6
|
+
//
|
|
7
|
+
// Skipped silently when SHINGAN_SKIP_POSTINSTALL=1 (CI mirroring use
|
|
8
|
+
// cases like air-gapped builds where the binary is provided
|
|
9
|
+
// externally).
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
const crypto = require('crypto');
|
|
17
|
+
const { pipeline } = require('stream/promises');
|
|
18
|
+
const tar = require('tar');
|
|
19
|
+
|
|
20
|
+
const PACKAGE_VERSION = require('../package.json').version;
|
|
21
|
+
|
|
22
|
+
function platformTag() {
|
|
23
|
+
const p = process.platform;
|
|
24
|
+
const a = process.arch;
|
|
25
|
+
if (p === 'darwin' && a === 'arm64') return { os: 'darwin', arch: 'arm64', ext: 'tar.gz', exe: 'shingan' };
|
|
26
|
+
if (p === 'darwin' && a === 'x64') return { os: 'darwin', arch: 'amd64', ext: 'tar.gz', exe: 'shingan' };
|
|
27
|
+
if (p === 'linux' && a === 'arm64') return { os: 'linux', arch: 'arm64', ext: 'tar.gz', exe: 'shingan' };
|
|
28
|
+
if (p === 'linux' && a === 'x64') return { os: 'linux', arch: 'amd64', ext: 'tar.gz', exe: 'shingan' };
|
|
29
|
+
if (p === 'win32' && a === 'arm64') return { os: 'windows', arch: 'arm64', ext: 'zip', exe: 'shingan.exe' };
|
|
30
|
+
if (p === 'win32' && a === 'x64') return { os: 'windows', arch: 'amd64', ext: 'zip', exe: 'shingan.exe' };
|
|
31
|
+
throw new Error(`shingan-lint: unsupported platform/arch: ${p}/${a}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function cacheDir() {
|
|
35
|
+
const base = process.env.SHINGAN_CACHE_DIR ||
|
|
36
|
+
process.env.XDG_CACHE_HOME ||
|
|
37
|
+
path.join(os.homedir(), '.cache');
|
|
38
|
+
return path.join(base, 'shingan-lint', `v${PACKAGE_VERSION}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function archiveName(tag) {
|
|
42
|
+
return `shingan_${PACKAGE_VERSION}_${tag.os}_${tag.arch}.${tag.ext}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function releaseURL(tag) {
|
|
46
|
+
const base = process.env.SHINGAN_DOWNLOAD_BASE ||
|
|
47
|
+
`https://github.com/hatyibei/shingan/releases/download/v${PACKAGE_VERSION}`;
|
|
48
|
+
return `${base}/${archiveName(tag)}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function checksumURL() {
|
|
52
|
+
const base = process.env.SHINGAN_DOWNLOAD_BASE ||
|
|
53
|
+
`https://github.com/hatyibei/shingan/releases/download/v${PACKAGE_VERSION}`;
|
|
54
|
+
return `${base}/checksums.txt`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function fetchBuffer(url) {
|
|
58
|
+
// Node 18+ has global fetch. Follow redirects (GitHub returns 302 to S3).
|
|
59
|
+
const res = await fetch(url, { redirect: 'follow' });
|
|
60
|
+
if (!res.ok) {
|
|
61
|
+
throw new Error(`download ${url} failed: HTTP ${res.status}`);
|
|
62
|
+
}
|
|
63
|
+
const ab = await res.arrayBuffer();
|
|
64
|
+
return Buffer.from(ab);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function fetchText(url) {
|
|
68
|
+
const buf = await fetchBuffer(url);
|
|
69
|
+
return buf.toString('utf8');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function sha256(buf) {
|
|
73
|
+
return crypto.createHash('sha256').update(buf).digest('hex');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function findExpectedHash(checksumsText, archiveName) {
|
|
77
|
+
// checksums.txt format: "<hash> <filename>" per line
|
|
78
|
+
for (const line of checksumsText.split('\n')) {
|
|
79
|
+
const parts = line.trim().split(/\s+/);
|
|
80
|
+
if (parts.length === 2 && parts[1] === archiveName) {
|
|
81
|
+
return parts[0];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function extractTar(buf, dest, tag) {
|
|
88
|
+
// Write to temp file so `tar` can stream it back.
|
|
89
|
+
const tmpFile = path.join(dest, `_archive.${tag.ext}`);
|
|
90
|
+
fs.writeFileSync(tmpFile, buf);
|
|
91
|
+
try {
|
|
92
|
+
if (tag.ext === 'tar.gz') {
|
|
93
|
+
await tar.x({ file: tmpFile, cwd: dest, strict: true });
|
|
94
|
+
} else if (tag.ext === 'zip') {
|
|
95
|
+
// Minimal zip extraction without an extra dependency: shell out.
|
|
96
|
+
const { execFileSync } = require('child_process');
|
|
97
|
+
execFileSync('powershell.exe', [
|
|
98
|
+
'-NoProfile',
|
|
99
|
+
'-Command',
|
|
100
|
+
`Expand-Archive -Path "${tmpFile}" -DestinationPath "${dest}" -Force`,
|
|
101
|
+
], { stdio: 'inherit' });
|
|
102
|
+
} else {
|
|
103
|
+
throw new Error(`unknown archive ext: ${tag.ext}`);
|
|
104
|
+
}
|
|
105
|
+
} finally {
|
|
106
|
+
fs.unlinkSync(tmpFile);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function main() {
|
|
111
|
+
if (process.env.SHINGAN_SKIP_POSTINSTALL === '1') {
|
|
112
|
+
console.log('shingan-lint: SHINGAN_SKIP_POSTINSTALL=1 set, skipping binary download.');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let tag;
|
|
117
|
+
try {
|
|
118
|
+
tag = platformTag();
|
|
119
|
+
} catch (e) {
|
|
120
|
+
console.error(`shingan-lint: ${e.message}`);
|
|
121
|
+
console.error(`Supported: darwin/arm64, darwin/x64, linux/arm64, linux/x64, win32/arm64, win32/x64.`);
|
|
122
|
+
console.error(`If your platform is unsupported, install via: go install github.com/hatyibei/shingan/cmd/shingan@v${PACKAGE_VERSION}`);
|
|
123
|
+
process.exit(0); // exit 0 so CI installs don't break — user gets actionable error on first invoke
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const dest = cacheDir();
|
|
127
|
+
const binPath = path.join(dest, tag.exe);
|
|
128
|
+
if (fs.existsSync(binPath)) {
|
|
129
|
+
console.log(`shingan-lint: binary already cached at ${binPath}`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
134
|
+
|
|
135
|
+
const archive = archiveName(tag);
|
|
136
|
+
const url = releaseURL(tag);
|
|
137
|
+
console.log(`shingan-lint: downloading ${archive} from ${url}`);
|
|
138
|
+
|
|
139
|
+
let archiveBuf;
|
|
140
|
+
try {
|
|
141
|
+
archiveBuf = await fetchBuffer(url);
|
|
142
|
+
} catch (e) {
|
|
143
|
+
console.error(`shingan-lint: download failed: ${e.message}`);
|
|
144
|
+
console.error(`If the release was just published, GitHub may need a few seconds to propagate.`);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Verify checksum if checksums.txt is reachable. Soft-fail when the
|
|
149
|
+
// checksum file is missing (e.g. early-stage release without
|
|
150
|
+
// goreleaser's checksum step), so users still get a usable binary
|
|
151
|
+
// — at the cost of trust on first install.
|
|
152
|
+
try {
|
|
153
|
+
const checksumsText = await fetchText(checksumURL());
|
|
154
|
+
const expected = findExpectedHash(checksumsText, archive);
|
|
155
|
+
if (expected) {
|
|
156
|
+
const actual = sha256(archiveBuf);
|
|
157
|
+
if (actual !== expected) {
|
|
158
|
+
throw new Error(`sha256 mismatch: expected ${expected}, got ${actual}`);
|
|
159
|
+
}
|
|
160
|
+
console.log(`shingan-lint: sha256 verified (${expected.slice(0, 12)}…)`);
|
|
161
|
+
} else {
|
|
162
|
+
console.warn(`shingan-lint: ${archive} not found in checksums.txt — proceeding without verification`);
|
|
163
|
+
}
|
|
164
|
+
} catch (e) {
|
|
165
|
+
console.warn(`shingan-lint: checksum verification skipped: ${e.message}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
await extractTar(archiveBuf, dest, tag);
|
|
169
|
+
|
|
170
|
+
if (!fs.existsSync(binPath)) {
|
|
171
|
+
console.error(`shingan-lint: binary ${tag.exe} missing after extraction in ${dest}.`);
|
|
172
|
+
console.error(`Archive may have a different layout than expected. Files extracted:`);
|
|
173
|
+
for (const f of fs.readdirSync(dest)) console.error(` ${f}`);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Ensure executable bit (no-op on Windows).
|
|
178
|
+
try { fs.chmodSync(binPath, 0o755); } catch (_) {}
|
|
179
|
+
|
|
180
|
+
console.log(`shingan-lint: installed ${tag.exe} → ${binPath}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
main().catch((err) => {
|
|
184
|
+
console.error(`shingan-lint: postinstall failed: ${err.stack || err.message}`);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
});
|