@jacebenson/jsn 0.0.10 → 1.0.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.
Files changed (41) hide show
  1. package/README.md +7 -49
  2. package/bin/jsn.js +57 -2
  3. package/package.json +28 -32
  4. package/scripts/install.sh +227 -0
  5. package/scripts/npm-install.js +235 -0
  6. package/scripts/pre-commit-check.sh +61 -0
  7. package/src/app.js +0 -157
  8. package/src/auth.js +0 -283
  9. package/src/cli.js +0 -144
  10. package/src/commands/_ticket.js +0 -256
  11. package/src/commands/auth.js +0 -62
  12. package/src/commands/changes.js +0 -7
  13. package/src/commands/dev/_generic.js +0 -223
  14. package/src/commands/dev/_simple.js +0 -89
  15. package/src/commands/dev/eval.js +0 -17
  16. package/src/commands/dev/flows.js +0 -528
  17. package/src/commands/dev/forms.js +0 -313
  18. package/src/commands/dev/lists.js +0 -233
  19. package/src/commands/dev/logs.js +0 -51
  20. package/src/commands/dev/rest.js +0 -64
  21. package/src/commands/dev/scopes.js +0 -96
  22. package/src/commands/dev/updatesets.js +0 -97
  23. package/src/commands/dev.js +0 -53
  24. package/src/commands/groupmembers.js +0 -39
  25. package/src/commands/grouproles.js +0 -39
  26. package/src/commands/groups.js +0 -57
  27. package/src/commands/incidents.js +0 -7
  28. package/src/commands/profiles.js +0 -79
  29. package/src/commands/records.js +0 -137
  30. package/src/commands/requests.js +0 -7
  31. package/src/commands/setup.js +0 -39
  32. package/src/commands/tasks.js +0 -7
  33. package/src/commands/tickets.js +0 -121
  34. package/src/commands/users.js +0 -57
  35. package/src/commands/version.js +0 -25
  36. package/src/config.js +0 -154
  37. package/src/context.js +0 -62
  38. package/src/errors.js +0 -101
  39. package/src/helpers.js +0 -60
  40. package/src/output.js +0 -410
  41. package/src/sdk.js +0 -357
package/README.md CHANGED
@@ -2,28 +2,17 @@
2
2
 
3
3
  A command-line interface for ServiceNow that follows the Unix philosophy: simple, composable, and scriptable.
4
4
 
5
- ## Versions
6
-
7
- | Version | Language | Branch | Install | Status |
8
- |---------|----------|--------|---------|--------|
9
- | Go | Go | `main` | Binary / `go install` | Stable |
10
- | Node.js | JavaScript (Node 18+) | `nodejs` | `npm install -g` | Active development |
11
-
12
- Both versions share the same CLI interface and are tested against the same ServiceNow PDI.
13
-
14
5
  ## Installation
15
6
 
16
- ### npm (Node.js version cross-platform)
7
+ ### npm (Cross-platformrecommended for Windows)
17
8
 
18
9
  ```bash
19
- npm install -g @jacebenson/jsn@node
10
+ npm install -g @jacebenson/jsn
20
11
  ```
21
12
 
22
- > **Note:** The `@node` dist-tag is required. The `latest` tag currently points to the Go shim wrapper (v1.0.1).
13
+ Works on macOS, Linux, and Windows. The correct binary for your platform is downloaded automatically during install.
23
14
 
24
- No compilation needed. Works on macOS, Linux, and Windows with Node.js 18+.
25
-
26
- ### Download Binary (Go version)
15
+ ### Download Binary
27
16
 
28
17
  ```bash
29
18
  # Download the latest release
@@ -32,7 +21,7 @@ chmod +x jsn
32
21
  sudo mv jsn /usr/local/bin/
33
22
  ```
34
23
 
35
- ### Go Install (Go version)
24
+ ### Go Install
36
25
 
37
26
  ```bash
38
27
  go install github.com/jacebenson/jsn/cmd/jsn@latest
@@ -318,7 +307,7 @@ jsn auth status
318
307
  jsn auth logout
319
308
  ```
320
309
 
321
- Credentials are stored in `~/.config/servicenow/credentials/` (file-based, OS keychain coming soon).
310
+ Credentials are securely stored in your OS keychain (or file fallback at `~/.config/servicenow/credentials/`).
322
311
 
323
312
  ## Environment Variables
324
313
 
@@ -342,8 +331,6 @@ jsn incidents list
342
331
 
343
332
  ## Shell Completion
344
333
 
345
- > **Note:** Shell completion is available in the Go version only.
346
-
347
334
  ```bash
348
335
  # Bash
349
336
  source <(jsn completion bash)
@@ -394,39 +381,10 @@ Error (usage): Instance URL required. Set via --instance flag, SERVICENOW_INSTAN
394
381
  - `--instance` flag
395
382
  - `SERVICENOW_INSTANCE_URL` environment variable
396
383
 
397
- ## Development
398
-
399
- This repository maintains two parallel implementations:
400
-
401
- - **`main`** — Go implementation (stable)
402
- - **`nodejs`** — Node.js implementation (active development)
403
-
404
- Both branches share the same CLI interface and are kept in sync for feature parity.
405
-
406
- ### Node.js version
407
-
408
- ```bash
409
- git checkout nodejs
410
- npm install
411
- npm test # Run tests
412
- npm run lint # Run ESLint
413
- npm run start # Run CLI locally
414
- ```
415
-
416
- ### Releasing
417
-
418
- ```bash
419
- # From nodejs branch — creates node-v* tag and publishes to npm
420
- npm run release -- patch
421
-
422
- # From main branch — creates go-v* tag and builds binaries
423
- npm run release -- patch
424
- ```
425
-
426
384
  ## Contributing
427
385
 
428
386
  1. Fork the repository
429
- 2. Create a feature branch (target the appropriate branch: `main` or `nodejs`)
387
+ 2. Create a feature branch
430
388
  3. Make your changes
431
389
  4. Add tests
432
390
  5. Submit a pull request
package/bin/jsn.js CHANGED
@@ -1,5 +1,60 @@
1
1
  #!/usr/bin/env node
2
+ /**
3
+ * JSN CLI binary wrapper
4
+ *
5
+ * This script finds the downloaded platform-specific binary and spawns it,
6
+ * forwarding all arguments, stdin, stdout, and stderr.
7
+ */
2
8
 
3
- import { cli } from '../src/cli.js';
9
+ const { spawn } = require('child_process');
10
+ const path = require('path');
11
+ const fs = require('fs');
12
+ const os = require('os');
4
13
 
5
- cli.parse();
14
+ function getBinaryPath() {
15
+ const binaryDir = path.join(__dirname, '..', 'binary');
16
+ const binaryName = os.platform() === 'win32' ? 'jsn.exe' : 'jsn';
17
+ const binaryPath = path.join(binaryDir, binaryName);
18
+
19
+ if (fs.existsSync(binaryPath)) {
20
+ return binaryPath;
21
+ }
22
+
23
+ // Fallback: check if running from source/build
24
+ const devPath = path.join(__dirname, '..', 'jsn');
25
+ if (fs.existsSync(devPath)) {
26
+ return devPath;
27
+ }
28
+
29
+ // Another fallback for dev
30
+ const devPath2 = path.join(__dirname, '..', 'jsn.exe');
31
+ if (fs.existsSync(devPath2)) {
32
+ return devPath2;
33
+ }
34
+
35
+ console.error('Error: JSN binary not found.');
36
+ console.error('');
37
+ console.error('The binary should have been downloaded during npm install.');
38
+ console.error('Try reinstalling the package:');
39
+ console.error(' npm uninstall -g @jacebenson/jsn');
40
+ console.error(' npm install -g @jacebenson/jsn');
41
+ console.error('');
42
+ console.error('Or download manually from:');
43
+ console.error(' https://github.com/jacebenson/jsn/releases');
44
+ process.exit(1);
45
+ }
46
+
47
+ const binaryPath = getBinaryPath();
48
+ const child = spawn(binaryPath, process.argv.slice(2), {
49
+ stdio: 'inherit',
50
+ windowsHide: true,
51
+ });
52
+
53
+ child.on('exit', (code) => {
54
+ process.exit(code ?? 0);
55
+ });
56
+
57
+ child.on('error', (err) => {
58
+ console.error(`Failed to start JSN: ${err.message}`);
59
+ process.exit(1);
60
+ });
package/package.json CHANGED
@@ -1,44 +1,40 @@
1
1
  {
2
2
  "name": "@jacebenson/jsn",
3
- "version": "0.0.10",
3
+ "version": "1.0.2",
4
4
  "description": "A command-line interface for ServiceNow that follows the Unix philosophy: simple, composable, and scriptable.",
5
- "type": "module",
5
+ "homepage": "https://github.com/jacebenson/jsn#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/jacebenson/jsn/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/jacebenson/jsn.git"
12
+ },
13
+ "license": "MIT",
14
+ "author": "Jace Benson",
15
+ "type": "commonjs",
16
+ "files": [
17
+ "bin",
18
+ "scripts",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
6
22
  "bin": {
7
23
  "jsn": "bin/jsn.js"
8
24
  },
9
- "main": "src/cli.js",
10
25
  "scripts": {
11
- "test": "node --test $(find test -name '*.test.js')",
12
- "lint": "npx eslint src/ bin/ test/",
13
- "start": "node bin/jsn.js",
14
- "tag-reminder": "node scripts/tag-reminder.js",
15
- "release": "node scripts/release.js"
26
+ "postinstall": "node scripts/npm-install.js"
16
27
  },
17
- "keywords": [
18
- "servicenow",
19
- "cli",
20
- "rest-api",
21
- "automation"
22
- ],
23
- "author": "Jace Benson",
24
- "license": "MIT",
25
28
  "engines": {
26
- "node": ">=18.0.0"
27
- },
28
- "dependencies": {
29
- "@inquirer/prompts": "^7.5.1",
30
- "chalk": "^5.4.1",
31
- "cli-table3": "^0.6.5",
32
- "yargs": "^17.7.2"
33
- },
34
- "devDependencies": {
35
- "@eslint/js": "^10.0.1",
36
- "globals": "^17.6.0"
29
+ "node": ">=14"
37
30
  },
38
- "files": [
39
- "bin/jsn.js",
40
- "src/",
41
- "README.md",
42
- "LICENSE"
31
+ "os": [
32
+ "darwin",
33
+ "linux",
34
+ "win32"
35
+ ],
36
+ "cpu": [
37
+ "x64",
38
+ "arm64"
43
39
  ]
44
40
  }
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env bash
2
+ # JSN CLI Installer
3
+ # Installs the latest JSN release from GitHub
4
+ #
5
+ # Usage:
6
+ # curl -fsSL https://jsn.jace.pro/install | bash
7
+ #
8
+ # Environment:
9
+ # JSN_VERSION Install specific version (e.g., 0.4.1)
10
+ # NO_COLOR Disable colored output (https://no-color.org)
11
+
12
+ set -euo pipefail
13
+
14
+ REPO="jacebenson/jsn"
15
+ BINARY_NAME="jsn"
16
+ INSTALL_DIR="${JSN_INSTALL_DIR:-}"
17
+ VERSION="${JSN_VERSION:-}"
18
+
19
+ # Colors - respect NO_COLOR (https://no-color.org)
20
+ if [[ -z "${NO_COLOR:-}" ]] && [[ -t 1 ]]; then
21
+ RED='\033[0;31m'
22
+ GREEN='\033[0;32m'
23
+ YELLOW='\033[1;33m'
24
+ NC='\033[0m'
25
+ BOLD='\033[1m'
26
+ else
27
+ RED=''
28
+ GREEN=''
29
+ YELLOW=''
30
+ NC=''
31
+ BOLD=''
32
+ fi
33
+
34
+ info() { echo -e "${GREEN}✓${NC} $1"; }
35
+ step() { echo -e "${BOLD}→${NC} $1"; }
36
+ warn() { echo -e "${YELLOW}⚠${NC} $1"; }
37
+ error() { echo -e "${RED}✗${NC} $1" >&2; exit 1; }
38
+
39
+ detect_platform() {
40
+ local os arch
41
+
42
+ os=$(uname -s | tr '[:upper:]' '[:lower:]')
43
+ case "$os" in
44
+ linux) PLATFORM="linux" ;;
45
+ darwin) PLATFORM="darwin" ;;
46
+ mingw*|msys*|cygwin*)
47
+ PLATFORM="windows"
48
+ BINARY_NAME="jsn.exe"
49
+ ;;
50
+ *) error "Unsupported OS: $os" ;;
51
+ esac
52
+
53
+ arch=$(uname -m)
54
+ case "$arch" in
55
+ x86_64|amd64) ARCH="amd64" ;;
56
+ arm64|aarch64) ARCH="arm64" ;;
57
+ armv7l) ARCH="arm" ;;
58
+ *) error "Unsupported architecture: $arch" ;;
59
+ esac
60
+ }
61
+
62
+ get_latest_version() {
63
+ # Use redirect URL instead of API to avoid rate limits
64
+ local url
65
+ url=$(curl -fsSL -o /dev/null -w '%{url_effective}' "https://github.com/${REPO}/releases/latest" 2>/dev/null) || true
66
+ local version="${url##*/}"
67
+ version="${version#v}"
68
+
69
+ if [[ ! $version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
70
+ error "Could not determine latest version. Check your network connection."
71
+ fi
72
+
73
+ echo "$version"
74
+ }
75
+
76
+ setup_install_dir() {
77
+ if [[ -z "$INSTALL_DIR" ]]; then
78
+ if [[ -w "/usr/local/bin" ]]; then
79
+ INSTALL_DIR="/usr/local/bin"
80
+ else
81
+ INSTALL_DIR="$HOME/.local/bin"
82
+ fi
83
+ fi
84
+ mkdir -p "$INSTALL_DIR"
85
+ }
86
+
87
+ download_and_install() {
88
+ local version="$1"
89
+ local tmp_dir
90
+ tmp_dir=$(mktemp -d)
91
+ trap "rm -rf '$tmp_dir'" EXIT
92
+
93
+ # Determine archive extension
94
+ local ext
95
+ if [[ "$PLATFORM" == "windows" ]]; then
96
+ ext="zip"
97
+ else
98
+ ext="tar.gz"
99
+ fi
100
+
101
+ local filename="jsn_v${version}_${PLATFORM}_${ARCH}.${ext}"
102
+ local url="https://github.com/${REPO}/releases/download/v${version}/${filename}"
103
+
104
+ step "Downloading JSN v${version} for ${PLATFORM}/${ARCH}..."
105
+ if ! curl -fsSL "$url" -o "${tmp_dir}/${filename}"; then
106
+ error "Download failed from $url"
107
+ fi
108
+
109
+ step "Extracting..."
110
+ cd "$tmp_dir"
111
+ if [[ "$PLATFORM" == "windows" ]]; then
112
+ unzip -q "$filename"
113
+ else
114
+ tar -xzf "$filename"
115
+ fi
116
+
117
+ # Find binary using shell globbing (Windows find is different)
118
+ local binary_file=""
119
+ for f in jsn jsn.exe jsn_*; do
120
+ if [[ -f "$f" ]] && [[ "$f" != "*.tar.gz" ]] && [[ "$f" != "*.zip" ]]; then
121
+ binary_file="$f"
122
+ break
123
+ fi
124
+ done
125
+
126
+ if [[ -z "$binary_file" ]]; then
127
+ error "Could not find binary in archive"
128
+ ls -la
129
+ exit 1
130
+ fi
131
+
132
+ step "Installing to $INSTALL_DIR..."
133
+ if [[ -w "$INSTALL_DIR" ]]; then
134
+ mv "$binary_file" "$INSTALL_DIR/$BINARY_NAME"
135
+ else
136
+ mv "$binary_file" "$INSTALL_DIR/$BINARY_NAME" 2>/dev/null || {
137
+ warn "Need sudo access to install to $INSTALL_DIR"
138
+ sudo mv "$binary_file" "$INSTALL_DIR/$BINARY_NAME"
139
+ }
140
+ fi
141
+
142
+ chmod +x "$INSTALL_DIR/$BINARY_NAME" 2>/dev/null || true
143
+ }
144
+
145
+ setup_path() {
146
+ # Check if already in PATH
147
+ if [[ ":$PATH:" == *":$INSTALL_DIR:"* ]]; then
148
+ return 0
149
+ fi
150
+
151
+ # Determine shell config file
152
+ local shell_rc=""
153
+ if [[ "$PLATFORM" == "windows" ]]; then
154
+ # On Windows, always use .bashrc for Git Bash
155
+ shell_rc="$HOME/.bashrc"
156
+ else
157
+ case "${SHELL:-}" in
158
+ */zsh) shell_rc="$HOME/.zshrc" ;;
159
+ */bash) shell_rc="$HOME/.bashrc" ;;
160
+ *) shell_rc="$HOME/.profile" ;;
161
+ esac
162
+ fi
163
+
164
+ # Check if already in config
165
+ if [[ -f "$shell_rc" ]] && grep -qF "$INSTALL_DIR" "$shell_rc" 2>/dev/null; then
166
+ return 0
167
+ fi
168
+
169
+ step "Adding $INSTALL_DIR to PATH in $shell_rc"
170
+ echo "" >> "$shell_rc"
171
+ echo "# Added by JSN installer" >> "$shell_rc"
172
+ echo "export PATH=\"$INSTALL_DIR:\$PATH\"" >> "$shell_rc"
173
+ info "Added to $shell_rc"
174
+ warn "Run: source $shell_rc"
175
+
176
+ # On Windows, also warn about CMD/PowerShell
177
+ if [[ "$PLATFORM" == "windows" ]]; then
178
+ echo ""
179
+ warn "For Command Prompt or PowerShell, add this to your PATH manually:"
180
+ echo " $INSTALL_DIR"
181
+ echo " setx PATH \"%PATH%;$INSTALL_DIR\""
182
+ fi
183
+ }
184
+
185
+ verify_install() {
186
+ local installed_version
187
+ if installed_version=$("$INSTALL_DIR/$BINARY_NAME" version 2>/dev/null | tail -1); then
188
+ info "${installed_version}"
189
+ return 0
190
+ fi
191
+ error "Installation failed - JSN not working"
192
+ }
193
+
194
+ main() {
195
+ step "Installing JSN..."
196
+
197
+ detect_platform
198
+
199
+ if [[ -n "$VERSION" ]]; then
200
+ # Strip leading 'v' if present
201
+ VERSION="${VERSION#v}"
202
+ if [[ ! $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
203
+ error "Invalid version format: $VERSION (expected: 0.4.1)"
204
+ fi
205
+ else
206
+ VERSION=$(get_latest_version)
207
+ fi
208
+
209
+ setup_install_dir
210
+ download_and_install "$VERSION"
211
+ setup_path
212
+ verify_install "$PLATFORM"
213
+
214
+ echo ""
215
+ info "JSN installed successfully!"
216
+ echo ""
217
+ echo " Run 'jsn' to get started (setup will run automatically on first use)"
218
+ echo ""
219
+
220
+ if [[ "$PLATFORM" == "windows" ]]; then
221
+ warn "Windows users: Add $INSTALL_DIR to your PATH manually:"
222
+ echo " setx PATH \"%PATH%;$INSTALL_DIR\""
223
+ echo ""
224
+ fi
225
+ }
226
+
227
+ main "$@"
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * JSN npm postinstall script
4
+ *
5
+ * Downloads the correct platform-specific binary from GitHub releases
6
+ * and places it in the `binary/` directory for the wrapper to find.
7
+ */
8
+
9
+ const https = require('https');
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const os = require('os');
13
+ const { execSync } = require('child_process');
14
+
15
+ const REPO = 'jacebenson/jsn';
16
+ const BINARY_DIR = path.join(__dirname, '..', 'binary');
17
+
18
+ const PLATFORM_MAP = {
19
+ darwin: 'darwin',
20
+ linux: 'linux',
21
+ win32: 'windows',
22
+ };
23
+
24
+ const ARCH_MAP = {
25
+ x64: 'amd64',
26
+ arm64: 'arm64',
27
+ };
28
+
29
+ function detectPlatform() {
30
+ const platform = os.platform();
31
+ const arch = os.arch();
32
+
33
+ const mappedPlatform = PLATFORM_MAP[platform];
34
+ const mappedArch = ARCH_MAP[arch];
35
+
36
+ if (!mappedPlatform) {
37
+ throw new Error(
38
+ `Unsupported platform: ${platform}. ` +
39
+ `Supported platforms: ${Object.keys(PLATFORM_MAP).join(', ')}`
40
+ );
41
+ }
42
+
43
+ if (!mappedArch) {
44
+ throw new Error(
45
+ `Unsupported architecture: ${arch}. ` +
46
+ `Supported architectures: ${Object.keys(ARCH_MAP).join(', ')}`
47
+ );
48
+ }
49
+
50
+ // Windows arm64 can run amd64 binaries, so fall back
51
+ if (mappedPlatform === 'windows' && mappedArch === 'arm64') {
52
+ console.log(
53
+ 'Warning: Windows arm64 build not available, using amd64 binary.'
54
+ );
55
+ return { platform: 'windows', arch: 'amd64', fallback: true };
56
+ }
57
+
58
+ return { platform: mappedPlatform, arch: mappedArch };
59
+ }
60
+
61
+ function getVersion() {
62
+ // npm sets this during install
63
+ if (process.env.npm_package_version) {
64
+ return process.env.npm_package_version;
65
+ }
66
+
67
+ // Fallback: read package.json
68
+ const pkgPath = path.join(__dirname, '..', 'package.json');
69
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
70
+ return pkg.version;
71
+ }
72
+
73
+ function downloadFile(url, dest) {
74
+ return new Promise((resolve, reject) => {
75
+ const file = fs.createWriteStream(dest);
76
+ https
77
+ .get(url, { timeout: 30000 }, (response) => {
78
+ if (response.statusCode === 301 || response.statusCode === 302) {
79
+ // Follow redirect
80
+ return downloadFile(response.headers.location, dest)
81
+ .then(resolve)
82
+ .catch(reject);
83
+ }
84
+
85
+ if (response.statusCode !== 200) {
86
+ reject(
87
+ new Error(
88
+ `Download failed: HTTP ${response.statusCode} for ${url}`
89
+ )
90
+ );
91
+ return;
92
+ }
93
+
94
+ response.pipe(file);
95
+ file.on('finish', () => {
96
+ file.close(resolve);
97
+ });
98
+ })
99
+ .on('error', (err) => {
100
+ fs.unlink(dest, () => {});
101
+ reject(err);
102
+ })
103
+ .on('timeout', () => {
104
+ fs.unlink(dest, () => {});
105
+ reject(new Error('Download timeout'));
106
+ });
107
+ });
108
+ }
109
+
110
+ function extractArchive(archivePath, destDir) {
111
+ const ext = path.extname(archivePath);
112
+ const isWindows = os.platform() === 'win32';
113
+
114
+ if (archivePath.endsWith('.zip')) {
115
+ // Use PowerShell on Windows, unzip elsewhere
116
+ if (isWindows) {
117
+ execSync(
118
+ `powershell -Command "Expand-Archive -Path '${archivePath}' -DestinationPath '${destDir}' -Force"`,
119
+ { stdio: 'inherit' }
120
+ );
121
+ } else {
122
+ execSync(`unzip -q -o "${archivePath}" -d "${destDir}"`, {
123
+ stdio: 'inherit',
124
+ });
125
+ }
126
+ } else if (archivePath.endsWith('.tar.gz')) {
127
+ execSync(`tar -xzf "${archivePath}" -C "${destDir}"`, {
128
+ stdio: 'inherit',
129
+ });
130
+ } else {
131
+ throw new Error(`Unknown archive format: ${archivePath}`);
132
+ }
133
+ }
134
+
135
+ function findBinary(extractDir) {
136
+ const binaryName = os.platform() === 'win32' ? 'jsn.exe' : 'jsn';
137
+
138
+ // The archive contains just the binary directly
139
+ const directPath = path.join(extractDir, binaryName);
140
+ if (fs.existsSync(directPath)) {
141
+ return directPath;
142
+ }
143
+
144
+ // Sometimes archives have nested directories
145
+ const entries = fs.readdirSync(extractDir);
146
+ for (const entry of entries) {
147
+ const entryPath = path.join(extractDir, entry);
148
+ const stat = fs.statSync(entryPath);
149
+ if (stat.isDirectory()) {
150
+ const nestedPath = path.join(entryPath, binaryName);
151
+ if (fs.existsSync(nestedPath)) {
152
+ return nestedPath;
153
+ }
154
+ }
155
+ }
156
+
157
+ throw new Error(
158
+ `Could not find ${binaryName} in extracted archive. Contents: ${entries.join(', ')}`
159
+ );
160
+ }
161
+
162
+ async function main() {
163
+ // Skip if running in development (from git repo)
164
+ if (fs.existsSync(path.join(__dirname, '..', 'go.mod'))) {
165
+ console.log('Development environment detected. Skipping binary download.');
166
+ console.log('Build the binary manually: go build ./cmd/jsn');
167
+ return;
168
+ }
169
+
170
+ const { platform, arch } = detectPlatform();
171
+ const version = getVersion();
172
+
173
+ // Development placeholder version
174
+ if (version === '0.0.0') {
175
+ console.log('Warning: version is 0.0.0, skipping binary download.');
176
+ console.log('This is expected when installing from source.');
177
+ return;
178
+ }
179
+
180
+ const ext = platform === 'windows' ? 'zip' : 'tar.gz';
181
+ const filename = `jsn_v${version}_${platform}_${arch}.${ext}`;
182
+ const url = `https://github.com/${REPO}/releases/download/v${version}/${filename}`;
183
+
184
+ console.log(`Downloading JSN v${version} for ${platform}/${arch}...`);
185
+ console.log(` ${url}`);
186
+
187
+ // Ensure binary directory exists
188
+ if (!fs.existsSync(BINARY_DIR)) {
189
+ fs.mkdirSync(BINARY_DIR, { recursive: true });
190
+ }
191
+
192
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jsn-install-'));
193
+ const archivePath = path.join(tmpDir, filename);
194
+
195
+ try {
196
+ await downloadFile(url, archivePath);
197
+ console.log('Extracting...');
198
+ extractArchive(archivePath, tmpDir);
199
+
200
+ const extractedBinary = findBinary(tmpDir);
201
+ const binaryName = platform === 'windows' ? 'jsn.exe' : 'jsn';
202
+ const finalPath = path.join(BINARY_DIR, binaryName);
203
+
204
+ // Move binary to final location
205
+ fs.copyFileSync(extractedBinary, finalPath);
206
+
207
+ // Make executable on Unix
208
+ if (platform !== 'windows') {
209
+ fs.chmodSync(finalPath, 0o755);
210
+ }
211
+
212
+ console.log(`Installed JSN binary to ${finalPath}`);
213
+ } catch (err) {
214
+ console.error(`Error: ${err.message}`);
215
+ console.error('');
216
+ console.error('Failed to download the JSN binary.');
217
+ console.error('You can still use the CLI if you have a binary installed manually.');
218
+ console.error('Download from: https://github.com/jacebenson/jsn/releases');
219
+
220
+ // Don't fail the npm install — the user can fix this later
221
+ process.exit(0);
222
+ } finally {
223
+ // Cleanup temp directory
224
+ try {
225
+ fs.rmSync(tmpDir, { recursive: true, force: true });
226
+ } catch {
227
+ // Ignore cleanup errors
228
+ }
229
+ }
230
+ }
231
+
232
+ main().catch((err) => {
233
+ console.error(`Unexpected error: ${err.message}`);
234
+ process.exit(0); // Don't fail npm install
235
+ });