Skip to content

Commit 950a2d5

Browse files
authored
fix: Windows install + config docs (closes #32, #34) (#35)
* fix(npm): use PowerShell Expand-Archive on Windows Windows lacks the unzip command, so the postinstall extraction silently failed and stacklit.exe never landed in npm/bin/. Switch to Expand-Archive (ships with Windows 10+) for .zip on win32, surface extraction output via stdio inherit, and fail loudly when the binary is missing after extraction. Closes #32 * docs: align config docs with .stacklitrc.json The README and USAGE guide showed a TOML schema, but the loader in internal/config reads .stacklitrc.json. Update both docs to JSON so the example matches the file the tool actually loads. Closes #34 * fix(npm): harden Windows install against quoting and silent failures Address PR review feedback from Copilot: - Use execFileSync instead of execSync for all extraction commands so paths are passed as args, never interpolated into a shell string. The original PowerShell command wrapped paths in single quotes inside a double-quoted string, so a single quote in __dirname (e.g. C:\\Users\\O'Connor\\...) would have broken parsing or altered the command. - On Windows, hand the archive and bin paths to PowerShell via env vars ($env:STACKLIT_ARCHIVE / $env:STACKLIT_BINDIR) and use -LiteralPath so the script body has no string interpolation at all. - Move archive.tmp cleanup into a finally so a failed extraction doesn't leave the temp file behind. - Set process.exitCode = 1 in the top-level catch so npm postinstall reports a non-zero status when extraction fails or the binary is missing afterward. Download failures still return silently, matching the existing 'let the bin script show a helpful error' behavior. * chore: revert unintended stacklit.json regeneration The post-commit hook regenerated stacklit.json (new merkle hash and timestamp) when committing the npm fix. That regeneration is unrelated to this PR's scope, so revert it to keep the diff focused on npm/install.js.
2 parents 0ce9ee1 + 56772cd commit 950a2d5

3 files changed

Lines changed: 73 additions & 42 deletions

File tree

README.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -267,16 +267,18 @@ stacklit setup cursor # configure Cursor + MCP
267267
```
268268
269269
<details>
270-
<summary>Configuration (stacklit.toml)</summary>
270+
<summary>Configuration (.stacklitrc.json)</summary>
271271
272-
```toml
273-
ignore = ["vendor/", "generated/"]
274-
max_depth = 3
275-
276-
[output]
277-
json = "stacklit.json"
278-
mermaid = "DEPENDENCIES.md"
279-
html = "stacklit.html"
272+
```json
273+
{
274+
"ignore": ["vendor/", "generated/"],
275+
"max_depth": 3,
276+
"output": {
277+
"json": "stacklit.json",
278+
"mermaid": "DEPENDENCIES.md",
279+
"html": "stacklit.html"
280+
}
281+
}
280282
```
281283

282284
</details>

USAGE.md

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -152,28 +152,24 @@ The server auto-reloads when `stacklit.json` changes on disk.
152152

153153
## Configuration
154154

155-
Create `stacklit.toml` in your project root (optional):
155+
Create `.stacklitrc.json` in your project root (optional):
156156

157-
```toml
158-
# Paths to ignore on top of .gitignore
159-
ignore = ["vendor/", "generated/", "*.pb.go"]
160-
161-
# Module detection depth (default: 4)
162-
max_depth = 3
163-
164-
# Max modules before collapsing (default: 200)
165-
max_modules = 150
166-
167-
# Max exports per module (default: 10)
168-
max_exports = 15
169-
170-
# Output file names
171-
[output]
172-
json = "stacklit.json"
173-
mermaid = "DEPENDENCIES.md"
174-
html = "stacklit.html"
157+
```json
158+
{
159+
"ignore": ["vendor/", "generated/", "*.pb.go"],
160+
"max_depth": 3,
161+
"max_modules": 150,
162+
"max_exports": 15,
163+
"output": {
164+
"json": "stacklit.json",
165+
"mermaid": "DEPENDENCIES.md",
166+
"html": "stacklit.html"
167+
}
168+
}
175169
```
176170

171+
Keys: `ignore` (extra paths on top of `.gitignore`), `max_depth` (module detection depth, default 4), `max_modules` (collapse threshold, default 200), `max_exports` (per module, default 10), and `output` (override generated file names).
172+
177173
---
178174

179175
## Reading stacklit.json

npm/install.js

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ const os = require('os');
22
const fs = require('fs');
33
const path = require('path');
44
const https = require('https');
5-
const { execSync } = require('child_process');
5+
const { execFileSync } = require('child_process');
66

77
const VERSION = '0.3.0';
88
const REPO = 'glincker/stacklit';
@@ -77,25 +77,58 @@ async function install() {
7777
return;
7878
}
7979

80-
// Extract
81-
if (archivePath.endsWith('.zip') || url.endsWith('.zip')) {
82-
execSync(`unzip -o "${archivePath}" stacklit.exe -d "${binDir}"`, { stdio: 'ignore' });
83-
} else {
84-
execSync(`tar -xzf "${archivePath}" -C "${binDir}" stacklit`, { stdio: 'ignore' });
85-
}
86-
87-
// Make executable
88-
if (os.platform() !== 'win32') {
89-
fs.chmodSync(binPath, 0o755);
80+
try {
81+
// Extract. execFileSync passes args directly to the binary, so no shell
82+
// quoting of paths is needed. On Windows we hand paths to PowerShell via
83+
// env vars so a single quote in __dirname (e.g. C:\Users\O'Connor\...)
84+
// cannot terminate the script string.
85+
if (url.endsWith('.zip')) {
86+
if (os.platform() === 'win32') {
87+
execFileSync(
88+
'powershell',
89+
[
90+
'-NoProfile',
91+
'-ExecutionPolicy', 'Bypass',
92+
'-Command',
93+
'Expand-Archive -Force -LiteralPath $env:STACKLIT_ARCHIVE -DestinationPath $env:STACKLIT_BINDIR',
94+
],
95+
{
96+
stdio: 'inherit',
97+
env: { ...process.env, STACKLIT_ARCHIVE: archivePath, STACKLIT_BINDIR: binDir },
98+
}
99+
);
100+
} else {
101+
execFileSync('unzip', ['-o', archivePath, 'stacklit.exe', '-d', binDir], { stdio: 'inherit' });
102+
}
103+
} else {
104+
execFileSync('tar', ['-xzf', archivePath, '-C', binDir, 'stacklit'], { stdio: 'inherit' });
105+
}
106+
107+
if (!fs.existsSync(binPath)) {
108+
throw new Error(`extraction completed but ${binPath} is missing`);
109+
}
110+
111+
if (os.platform() !== 'win32') {
112+
fs.chmodSync(binPath, 0o755);
113+
}
114+
} finally {
115+
if (fs.existsSync(archivePath)) {
116+
try {
117+
fs.unlinkSync(archivePath);
118+
} catch (cleanupErr) {
119+
// best-effort cleanup; warn but don't mask the original failure
120+
console.warn(`Could not remove ${archivePath}: ${cleanupErr.message}`);
121+
}
122+
}
90123
}
91124

92-
// Cleanup
93-
fs.unlinkSync(archivePath);
94-
95125
console.log('stacklit installed successfully.');
96126
}
97127

98128
install().catch((err) => {
99129
console.error('Installation failed:', err.message);
100130
console.error('You can install manually: go install github.com/glincker/stacklit/cmd/stacklit@latest');
131+
// Surface the failure to npm so postinstall doesn't appear to succeed when
132+
// the binary is actually missing or extraction broke.
133+
process.exitCode = 1;
101134
});

0 commit comments

Comments
 (0)