Skip to content

Commit da54a75

Browse files
committed
refactor: rename bootstrap to stub for clarity
- Rename bootstrap.mts to stub.mts (more accurate name) - Update rollup config to build stub.cjs instead of bootstrap.cjs - Rename npm script from build:sea:internal:bootstrap to build:sea:stub - Update build-sea.mjs to reference stub instead of bootstrap - Clarify in comments that we use yao-pkg, not Node 24 native SEA
1 parent 6cca79d commit da54a75

File tree

5 files changed

+219
-17
lines changed

5 files changed

+219
-17
lines changed

.config/rollup.sea.config.mjs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
2-
* Rollup configuration for building SEA bootstrap thin wrapper.
3-
* Compiles TypeScript bootstrap to CommonJS for Node.js SEA compatibility.
2+
* Rollup configuration for building SEA stub thin wrapper.
3+
* Compiles TypeScript stub to CommonJS for Node.js SEA compatibility.
44
*/
55

66
import path from 'node:path'
@@ -25,10 +25,10 @@ const MIN_NODE_VERSION = semver.major(maintainedNodeVersions[2])
2525

2626
export default {
2727
input:
28-
process.env.SEA_BOOTSTRAP || path.join(rootDir, 'src/sea/bootstrap.mts'),
28+
process.env.SEA_STUB || path.join(rootDir, 'src/sea/stub.mts'),
2929
output: {
3030
file:
31-
process.env.SEA_OUTPUT || path.join(rootDir, 'dist/sea/bootstrap.cjs'),
31+
process.env.SEA_OUTPUT || path.join(rootDir, 'dist/sea/stub.cjs'),
3232
format: 'cjs',
3333
interop: 'auto',
3434
},

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
"build:dist:types": "node scripts/build.mjs --types-only",
4040
"build:ink": "node scripts/build.mjs --ink-only",
4141
"build:sea": "node scripts/build-sea.mjs",
42-
"build:sea:internal:bootstrap": "rollup -c .config/rollup.sea.config.mjs",
42+
"build:sea:stub": "rollup -c .config/rollup.sea.config.mjs",
4343
"publish:sea": "node scripts/publish-sea.mjs",
4444
"publish:sea:github": "node scripts/publish-sea.mjs --skip-npm",
4545
"publish:sea:npm": "node scripts/publish-sea.mjs --skip-github",

scripts/build-sea.mjs

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
/**
22
* Build script for creating self-executable Socket CLI applications.
3-
* Uses Node.js Single Executable Application (SEA) feature.
3+
* Uses yao-pkg (enhanced fork of vercel/pkg) to create single executables.
44
*
5-
* IMPORTANT: This builds a THIN WRAPPER that downloads @socketsecurity/cli on first use.
5+
* NOTE: We use "SEA" as a general term for single executable applications,
6+
* but this uses yao-pkg, not Node.js 24's native SEA feature.
7+
*
8+
* IMPORTANT: This builds a THIN STUB that downloads @socketsecurity/cli on first use.
69
* The binary contains only:
7-
* - Node.js runtime
8-
* - Bootstrap code to download/execute @socketsecurity/cli
10+
* - Node.js runtime (embedded by yao-pkg)
11+
* - Stub code to download/execute @socketsecurity/cli
912
* - No actual CLI implementation
1013
*
1114
* The real Socket CLI code lives in the @socketsecurity/cli npm package,
@@ -19,7 +22,7 @@
1922
* Usage:
2023
* - Build all platforms: npm run build:sea
2124
* - Build specific platform: npm run build:sea -- --platform=darwin --arch=x64
22-
* - Use advanced bootstrap: npm run build:sea -- --advanced
25+
* - Use advanced stub: npm run build:sea -- --advanced
2326
*/
2427

2528
import crypto from 'node:crypto'
@@ -464,23 +467,23 @@ async function buildTarget(target, options) {
464467
)
465468
console.log('(Actual CLI will be downloaded from npm on first use)')
466469

467-
// Use the thin bootstrap for minimal size.
470+
// Use the thin stub for minimal size.
468471
const tsEntryPoint = normalizePath(
469-
path.join(__dirname, '..', 'src', 'sea', 'bootstrap.mts'),
472+
path.join(__dirname, '..', 'src', 'sea', 'stub.mts'),
470473
)
471474

472475
// Ensure output directory exists.
473476
await fs.mkdir(outputDir, { recursive: true })
474477

475-
// Build the bootstrap with Rollup to CommonJS for SEA.
476-
const entryPoint = normalizePath(path.join(outputDir, 'bootstrap.cjs'))
477-
console.log('Building bootstrap...')
478+
// Build the stub with Rollup to CommonJS for yao-pkg.
479+
const entryPoint = normalizePath(path.join(outputDir, 'stub.cjs'))
480+
console.log('Building stub...')
478481

479482
// Set environment variables for the rollup config.
480-
process.env['SEA_BOOTSTRAP'] = tsEntryPoint
483+
process.env['SEA_STUB'] = tsEntryPoint
481484
process.env['SEA_OUTPUT'] = entryPoint
482485

483-
await spawn('pnpm', ['run', 'build:sea:internal:bootstrap'], {
486+
await spawn('pnpm', ['run', 'build:sea:stub'], {
484487
stdio: 'inherit',
485488
})
486489

src/sea/install.mts

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
/**
2+
* @fileoverview NPM postinstall script for Socket CLI binary distribution.
3+
* Downloads the appropriate platform-specific binary from GitHub releases.
4+
* This runs when users install the `socket` npm package.
5+
*/
6+
7+
import { createWriteStream, existsSync } from 'node:fs'
8+
import { chmod, rename, unlink } from 'node:fs/promises'
9+
import https from 'node:https'
10+
import os from 'node:os'
11+
import path from 'node:path'
12+
import { fileURLToPath } from 'node:url'
13+
14+
const __filename = fileURLToPath(import.meta.url)
15+
const __dirname = path.dirname(__filename)
16+
17+
// Binary naming constants
18+
const BINARY_NAME = 'socket'
19+
const GITHUB_ORG = 'SocketDev'
20+
const GITHUB_REPO = 'socket-cli'
21+
22+
// Map Node.js platform/arch to our binary naming convention
23+
const PLATFORM_MAP: Record<string, string> = {
24+
darwin: 'darwin', // macOS
25+
linux: 'linux',
26+
win32: 'win32' // Windows
27+
}
28+
29+
const ARCH_MAP: Record<string, string> = {
30+
arm64: 'arm64',
31+
x64: 'x64'
32+
}
33+
34+
/**
35+
* Get the binary filename for the current platform.
36+
*/
37+
function getBinaryName(): string {
38+
const platform = PLATFORM_MAP[os.platform()]
39+
const arch = ARCH_MAP[os.arch()]
40+
41+
if (!platform || !arch) {
42+
throw new Error(
43+
`Unsupported platform: ${os.platform()} ${os.arch()}. ` +
44+
`Please install @socketsecurity/cli directly instead.`
45+
)
46+
}
47+
48+
const extension = os.platform() === 'win32' ? '.exe' : ''
49+
return `socket-${platform}-${arch}${extension}`
50+
}
51+
52+
/**
53+
* Get package version from package.json.
54+
*/
55+
async function getPackageVersion(): Promise<string> {
56+
// In production, this will be in npm-package/package.json
57+
const packagePath = path.join(__dirname, '..', '..', 'npm-package', 'package.json')
58+
if (existsSync(packagePath)) {
59+
const { default: pkg } = await import(packagePath, { with: { type: 'json' } })
60+
return pkg.version
61+
}
62+
63+
// Fallback for development
64+
const devPackagePath = path.join(__dirname, '..', '..', '..', 'package.json')
65+
const { default: pkg } = await import(devPackagePath, { with: { type: 'json' } })
66+
return pkg.version
67+
}
68+
69+
/**
70+
* Download a file from a URL with redirect handling.
71+
*/
72+
async function downloadFile(url: string, destPath: string): Promise<void> {
73+
return new Promise((resolve, reject) => {
74+
const file = createWriteStream(destPath)
75+
76+
const request = https.get(
77+
url,
78+
{
79+
headers: {
80+
'User-Agent': 'socket-cli-installer'
81+
}
82+
},
83+
response => {
84+
// Handle redirects
85+
if (response.statusCode === 301 || response.statusCode === 302) {
86+
const redirectUrl = response.headers.location
87+
if (!redirectUrl) {
88+
file.close()
89+
unlink(destPath).catch(() => {})
90+
reject(new Error('Redirect without location header'))
91+
return
92+
}
93+
94+
file.close()
95+
downloadFile(redirectUrl, destPath).then(resolve, reject)
96+
return
97+
}
98+
99+
// Check for successful response
100+
if (response.statusCode !== 200) {
101+
file.close()
102+
unlink(destPath).catch(() => {})
103+
reject(new Error(
104+
`Failed to download binary: HTTP ${response.statusCode}`
105+
))
106+
return
107+
}
108+
109+
// Pipe response to file
110+
response.pipe(file)
111+
112+
file.on('finish', () => {
113+
file.close(() => resolve())
114+
})
115+
}
116+
)
117+
118+
request.on('error', err => {
119+
file.close()
120+
unlink(destPath).catch(() => {})
121+
reject(err)
122+
})
123+
})
124+
}
125+
126+
/**
127+
* Get the download URL for the binary.
128+
*/
129+
async function getBinaryUrl(): Promise<string> {
130+
const version = await getPackageVersion()
131+
const binaryName = getBinaryName()
132+
133+
// GitHub releases URL pattern
134+
return `https://github.com/${GITHUB_ORG}/${GITHUB_REPO}/releases/download/v${version}/${binaryName}`
135+
}
136+
137+
/**
138+
* Install the platform-specific binary.
139+
*/
140+
async function install(): Promise<void> {
141+
try {
142+
const binaryName = getBinaryName()
143+
const targetName = BINARY_NAME + (os.platform() === 'win32' ? '.exe' : '')
144+
const binaryPath = path.join(__dirname, '..', '..', 'npm-package', targetName)
145+
146+
// For development, use local path
147+
const devBinaryPath = path.join(__dirname, targetName)
148+
const finalPath = existsSync(path.dirname(binaryPath)) ? binaryPath : devBinaryPath
149+
150+
// Check if binary already exists
151+
if (existsSync(finalPath)) {
152+
console.log('Socket CLI binary already installed.')
153+
return
154+
}
155+
156+
console.log(`Downloading Socket CLI for ${os.platform()}-${os.arch()}...`)
157+
158+
const url = await getBinaryUrl()
159+
const tempPath = `${finalPath}.download`
160+
161+
// Download the binary
162+
await downloadFile(url, tempPath)
163+
164+
// Make executable on Unix-like systems
165+
if (os.platform() !== 'win32') {
166+
await chmod(tempPath, 0o755)
167+
}
168+
169+
// Atomic rename to final location
170+
await rename(tempPath, finalPath)
171+
172+
console.log('✓ Socket CLI installed successfully!')
173+
console.log(` Binary: ${finalPath}`)
174+
console.log(` Run 'socket --help' to get started.`)
175+
} catch (error) {
176+
const message = error instanceof Error ? error.message : String(error)
177+
178+
console.error(`Failed to install Socket CLI binary: ${message}`)
179+
console.error('')
180+
console.error('You can try:')
181+
console.error(' 1. Installing from source: npm install -g @socketsecurity/cli')
182+
console.error(' 2. Downloading manually from: https://github.com/SocketDev/socket-cli/releases')
183+
console.error('')
184+
console.error('For help, visit: https://github.com/SocketDev/socket-cli/issues')
185+
186+
// Don't fail the npm install - allow fallback to source
187+
process.exitCode = 0
188+
}
189+
}
190+
191+
// Run if this is the main module
192+
if (import.meta.url === `file://${process.argv[1]}`) {
193+
install().catch(error => {
194+
console.error('Unexpected error:', error)
195+
process.exitCode = 0 // Still don't fail npm install
196+
})
197+
}
198+
199+
export { install, getBinaryName, getBinaryUrl }
File renamed without changes.

0 commit comments

Comments
 (0)