diff --git a/package.json b/package.json index fae9ff292..340fe8602 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,6 @@ "update": "node scripts/update.mts", "// Setup": "", "setup": "node scripts/setup.mts", - "preinstall": "node scripts/bootstrap-firewall-deps.mts", "postinstall": "node scripts/setup.mts --install --quiet", "prepare": "husky", "pretest": "pnpm run build:cli" @@ -74,6 +73,7 @@ "@pnpm/lockfile.detect-dep-types": "catalog:", "@pnpm/lockfile.fs": "catalog:", "@pnpm/logger": "catalog:", + "@sinclair/typebox": "catalog:", "@socketregistry/hyrious__bun.lockb": "catalog:", "@socketregistry/indent-string": "catalog:", "@socketregistry/is-interactive": "catalog:", diff --git a/packages/cli/src/cli-entry.mts b/packages/cli/src/cli-entry.mts index 3be9d39d4..e706ef9ed 100755 --- a/packages/cli/src/cli-entry.mts +++ b/packages/cli/src/cli-entry.mts @@ -2,7 +2,7 @@ // Set global Socket theme for consistent CLI branding. import { isError } from '@socketsecurity/lib/errors' -import { setTheme } from '@socketsecurity/lib/themes' +import { setTheme } from '@socketsecurity/lib/themes/context' setTheme('socket') import { promises as fs } from 'node:fs' @@ -214,8 +214,9 @@ void (async () => { try { logger.error('Fatal error:', err) } catch { - // Fallback to console if logger fails. - console.error('Fatal error:', err) + // Last-ditch fallback when logger itself throws — the catch + // ensures we still report the original error before exit. + console.error('Fatal error:', err) // # socket-hook: allow logger } // Track CLI error for fatal exceptions. @@ -234,8 +235,8 @@ process.on('uncaughtException', async err => { try { logger.error('Uncaught exception:', err) } catch { - // Fallback to console if logger fails. - console.error('Uncaught exception:', err) + // Last-ditch fallback when logger itself throws. + console.error('Uncaught exception:', err) // # socket-hook: allow logger } // Track CLI error for uncaught exception. @@ -248,7 +249,8 @@ process.on('uncaughtException', async err => { try { logger.error('Error in uncaughtException handler:', e) } catch { - console.error('Error in uncaughtException handler:', e) + // Last-ditch fallback when logger itself throws. + console.error('Error in uncaughtException handler:', e) // # socket-hook: allow logger } } finally { // eslint-disable-next-line n/no-process-exit @@ -262,8 +264,8 @@ process.on('unhandledRejection', async (reason, promise) => { try { logger.error('Unhandled rejection at:', promise, 'reason:', reason) } catch { - // Fallback to console if logger fails. - console.error('Unhandled rejection at:', promise, 'reason:', reason) + // Last-ditch fallback when logger itself throws. + console.error('Unhandled rejection at:', promise, 'reason:', reason) // # socket-hook: allow logger } // Track CLI error for unhandled rejection. @@ -277,7 +279,8 @@ process.on('unhandledRejection', async (reason, promise) => { try { logger.error('Error in unhandledRejection handler:', e) } catch { - console.error('Error in unhandledRejection handler:', e) + // Last-ditch fallback when logger itself throws. + console.error('Error in unhandledRejection handler:', e) // # socket-hook: allow logger } } finally { // eslint-disable-next-line n/no-process-exit diff --git a/packages/cli/src/utils/terminal/ascii-header.mts b/packages/cli/src/utils/terminal/ascii-header.mts index 78c14a8c8..cb83ec2f8 100644 --- a/packages/cli/src/utils/terminal/ascii-header.mts +++ b/packages/cli/src/utils/terminal/ascii-header.mts @@ -7,12 +7,14 @@ import colors from 'yoctocolors-cjs' -import { applyShimmer } from '@socketsecurity/lib/effects/text-shimmer' +import { configToSpec, frameColors } from '@socketsecurity/lib/effects/shimmer' +import { colorsToAnsi } from '@socketsecurity/lib/effects/shimmer-terminal' import type { - ShimmerColorGradient, - ShimmerState, -} from '@socketsecurity/lib/effects/text-shimmer' + Palette, + RGB, + ShimmerSpec, +} from '@socketsecurity/lib/effects/shimmer' /** * Color themes for header styling. @@ -93,60 +95,72 @@ function applyHexColor(text: string, hexColor: string): string { return `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m` } +/** + * Pick the brighter of two RGB colors. Used to compose two shimmer + * waves into one frame: each wave's `frameColors[i]` is computed + * independently, then merged so the brighter highlight wins per char. + * Treats luminance as the simple sum of channels — fine here because + * both waves share base + highlight palettes. + */ +function brighterRgb(a: RGB, b: RGB): RGB { + return a[0] + a[1] + a[2] >= b[0] + b[1] + b[2] ? a : b +} + /** * Render ASCII logo with shimmer effect for given frame. - * Uses socket-registry's applyShimmer with theme color gradients. - * Features dual shimmer waves and slanted diagonal movement. + * + * Uses socket-lib's @socketsecurity/lib/effects/shimmer engine + * (5.26.1+). Builds two ShimmerSpecs per line — primary + secondary + * offset by 35 frames — and merges their per-char colors with + * `brighterRgb` so the dual-wave look is preserved. Each line gets a + * `slantOffset = i * 4` added to the frame counter, producing a + * diagonal wave across the logo. Applies bold via ANSI before the + * shimmer's truecolor escape so terminals render the highlight bold. */ export function renderShimmerFrame( frame: number, theme: HeaderTheme = 'default', ): string { - const themeGradient = THEME_COLORS_RGB[ - theme - ] as unknown as ShimmerColorGradient + const themePalette = THEME_COLORS_RGB[theme] as unknown as Palette - // Apply shimmer to each line of the ASCII logo with slanted offset. const lines: string[] = [] for (let i = 0; i < ASCII_LOGO.length; i++) { const line = ASCII_LOGO[i]! + const lineLength = line.length - // Apply bold formatting first so applyShimmer can detect and preserve it. - const boldLine = `\x1b[1m${line}\x1b[0m` - - // Create slanted shimmer by offsetting each line's frame position. - // This creates a diagonal wave effect across the logo. + // Slant the wave by offsetting each line's frame counter — same + // 4-frame-per-row delta as the previous implementation. const slantOffset = i * 4 - - // Primary shimmer wave. - const shimmerState1: ShimmerState = { - currentDir: 'ltr', - mode: 'ltr', - speed: 0.25, - step: frame + slantOffset, - } - - // Secondary shimmer wave (offset to create dual wave effect). - const shimmerState2: ShimmerState = { - currentDir: 'ltr', - mode: 'ltr', - speed: 0.25, - step: frame + slantOffset + 35, - } - - // Apply first shimmer pass (will detect and preserve bold). - const shimmered1 = applyShimmer(boldLine, shimmerState1, { - color: themeGradient, - direction: 'ltr', - }) - - // Apply second shimmer pass for dual wave effect. - const shimmered2 = applyShimmer(shimmered1, shimmerState2, { - color: themeGradient, - direction: 'ltr', - }) - - lines.push(shimmered2) + const speed = 0.25 + + // Build the shimmer spec once and reuse for both waves — the + // spec is frame-independent (positionAt is a closure over speed + // + textLength + direction). The two waves differ only in the + // frame counter passed to `frameColors`. + const spec: ShimmerSpec = configToSpec( + { + color: themePalette, + dir: 'ltr', + speed, + }, + lineLength, + ) + + // Compute per-char colors for both waves and merge. + const primaryColors = frameColors(spec, lineLength, frame + slantOffset) + const secondaryColors = frameColors( + spec, + lineLength, + frame + slantOffset + 35, + ) + const merged: RGB[] = primaryColors.map((c, idx) => + brighterRgb(c, secondaryColors[idx]!), + ) + + // Render to ANSI truecolor + wrap in bold for the brighter look + // the previous implementation produced. \x1b[1m turns bold on, + // colorsToAnsi emits per-char truecolor codes, \x1b[0m resets. + lines.push(`\x1b[1m${colorsToAnsi(line, merged)}\x1b[0m`) } return lines.join('\n') diff --git a/packages/cli/test/unit/constants/paths.test.mts b/packages/cli/test/unit/constants/paths.test.mts index d33785833..b152450e3 100644 --- a/packages/cli/test/unit/constants/paths.test.mts +++ b/packages/cli/test/unit/constants/paths.test.mts @@ -142,7 +142,9 @@ describe('paths constants', () => { it('getBinCliPath returns path to CLI entry point', () => { const result = getBinCliPath() - expect(result).toContain('cli.js') + // The bundle entry is `dist/index.js` (was `dist/cli.js` before + // the unified-build rename in src/constants/paths.mts). + expect(result).toContain('dist/index.js') }) it('getDistPath returns distPath', () => { diff --git a/packages/package-builder/templates/socketaddon-main/index.mjs b/packages/package-builder/templates/socketaddon-main/index.mjs index 8f30b2c2a..406673855 100644 --- a/packages/package-builder/templates/socketaddon-main/index.mjs +++ b/packages/package-builder/templates/socketaddon-main/index.mjs @@ -106,10 +106,30 @@ function loadNativeAddon() { } if (buildOutDir) { + // First: look for a sibling-package-style layout + // `socketaddon-iocraft-/iocraft.node` next to the + // main package. const siblingPath = join(buildOutDir, `socketaddon-iocraft-${platformId}`, 'iocraft.node') if (existsSync(siblingPath)) { return require(siblingPath) } + // Second: look inside the main package's bundled + // `node_modules/@socketaddon/iocraft-/iocraft.node`. + // This is where pnpm leaves the optionalDependency when it's + // installed into the file: package's local node_modules but + // not lifted into the consumer's .pnpm store (which happens + // for `file:` deps that declare optionalDependencies). + const bundledPath = join( + buildOutDir, + 'socketaddon-iocraft', + 'node_modules', + '@socketaddon', + `iocraft-${platformId}`, + 'iocraft.node', + ) + if (existsSync(bundledPath)) { + return require(bundledPath) + } } throw new Error('Not in development build structure') diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1d8add5e3..b51d574ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -257,7 +257,7 @@ overrides: '@octokit/graphql': 9.0.1 '@octokit/request-error': 7.0.0 '@sigstore/sign': 4.1.0 - '@socketsecurity/lib': 5.24.0 + '@socketsecurity/lib': 5.26.1 aggregate-error: npm:@socketregistry/aggregate-error@^1.0.15 ansi-regex: 6.2.2 brace-expansion: 5.0.5 @@ -378,6 +378,9 @@ importers: '@pnpm/logger': specifier: 'catalog:' version: 1001.0.0 + '@sinclair/typebox': + specifier: 'catalog:' + version: 0.34.49 '@socketregistry/hyrious__bun.lockb': specifier: 'catalog:' version: 1.0.19 @@ -397,8 +400,8 @@ importers: specifier: 'catalog:' version: 3.0.1 '@socketsecurity/lib': - specifier: 5.24.0 - version: 5.24.0(typescript@5.9.3) + specifier: 5.26.1 + version: 5.26.1(typescript@5.9.3) '@socketsecurity/registry': specifier: 'catalog:' version: 2.0.2(typescript@5.9.3) @@ -571,24 +574,14 @@ importers: specifier: 'catalog:' version: 4.1.8 - .claude/hooks/auth-rotation-reminder: - dependencies: - '@socketsecurity/lib': - specifier: 5.24.0 - version: 5.24.0(typescript@5.9.3) - devDependencies: - '@types/node': - specifier: 'catalog:' - version: 24.9.2 - .claude/hooks/check-new-deps: dependencies: '@socketregistry/packageurl-js': specifier: 'catalog:' version: 1.4.2 '@socketsecurity/lib': - specifier: 5.24.0 - version: 5.24.0(typescript@5.9.3) + specifier: 5.26.1 + version: 5.26.1(typescript@5.9.3) '@socketsecurity/sdk': specifier: 'catalog:' version: 4.0.1 @@ -597,14 +590,6 @@ importers: specifier: 24.9.2 version: 24.9.2 - .claude/hooks/logger-guard: - devDependencies: - '@types/node': - specifier: 'catalog:' - version: 24.9.2 - - .claude/hooks/path-guard: {} - .claude/hooks/public-surface-reminder: devDependencies: '@types/node': @@ -614,18 +599,14 @@ importers: .claude/hooks/setup-security-tools: dependencies: '@socketsecurity/lib': - specifier: 5.24.0 - version: 5.24.0(typescript@5.9.3) - - .claude/hooks/stale-process-sweeper: {} - - .claude/hooks/token-guard: {} + specifier: 5.26.1 + version: 5.26.1(typescript@5.9.3) .claude/hooks/token-hygiene: devDependencies: '@socketsecurity/lib': - specifier: 5.24.0 - version: 5.24.0(typescript@5.9.3) + specifier: 5.26.1 + version: 5.26.1(typescript@5.9.3) '@types/node': specifier: 24.9.2 version: 24.9.2 @@ -642,8 +623,8 @@ importers: specifier: 'catalog:' version: 0.34.49 '@socketsecurity/lib': - specifier: 5.24.0 - version: 5.24.0(typescript@5.9.3) + specifier: 5.26.1 + version: 5.26.1(typescript@5.9.3) magic-string: specifier: 'catalog:' version: 0.30.19 @@ -699,8 +680,8 @@ importers: specifier: 'catalog:' version: 3.0.1 '@socketsecurity/lib': - specifier: 5.24.0 - version: 5.24.0(typescript@5.9.3) + specifier: 5.26.1 + version: 5.26.1(typescript@5.9.3) '@socketsecurity/registry': specifier: 'catalog:' version: 2.0.2(typescript@5.9.3) @@ -816,8 +797,8 @@ importers: packages/package-builder: dependencies: '@socketsecurity/lib': - specifier: 5.24.0 - version: 5.24.0(typescript@5.9.3) + specifier: 5.26.1 + version: 5.26.1(typescript@5.9.3) build-infra: specifier: workspace:* version: link:../build-infra @@ -2198,7 +2179,6 @@ packages: '@socketaddon/iocraft@file:packages/package-builder/build/dev/out/socketaddon-iocraft': resolution: {directory: packages/package-builder/build/dev/out/socketaddon-iocraft, type: directory} - engines: {node: '>=18'} '@socketregistry/es-set-tostringtag@1.0.10': resolution: {integrity: sha512-btXmvw1JpA8WtSoXx9mTapo9NAyIDKRRzK84i48d8zc0X09M6ORfobVnHbgwhXf7CFhkRzhYrHG9dqbI9vpELQ==} @@ -2257,8 +2237,8 @@ packages: resolution: {integrity: sha512-kLKdSqi4W7SDSm5z+wYnfVRnZCVhxzbzuKcdOZSrcHoEGOT4Gl844uzoaML+f5eiQMxY+nISiETwRph/aXrIaQ==} engines: {node: 18.20.7 || ^20.18.3 || >=22.14.0} - '@socketsecurity/lib@5.24.0': - resolution: {integrity: sha512-4Yar8oo4N12ESoNt/i2PNf08HRABUC0OcfUfwzIF3xjq89E5VMDN+aeOtnn6Oo4Y6u3TiuZRG7NgEBZ83LQ1Lw==} + '@socketsecurity/lib@5.26.1': + resolution: {integrity: sha512-ppyhOC/vPBY0gZVRtzNpmf/8nodILERmNV8J62pzVo7GvAB9S1q0/k/vSF2tor2NJEDtim+m6N+J5qtQ0mR99A==} engines: {node: '>=22', pnpm: '>=11.0.0-rc.0'} peerDependencies: typescript: '>=5.0.0' @@ -5749,7 +5729,7 @@ snapshots: pony-cause: 2.1.11 yaml: 2.8.1 - '@socketsecurity/lib@5.24.0(typescript@5.9.3)': + '@socketsecurity/lib@5.26.1(typescript@5.9.3)': optionalDependencies: typescript: 5.9.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b1961450a..e2e0a555b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -48,7 +48,7 @@ catalog: '@socketregistry/packageurl-js': 1.4.2 '@socketregistry/yocto-spinner': 1.0.25 '@socketsecurity/config': 3.0.1 - '@socketsecurity/lib': 5.24.0 + '@socketsecurity/lib': 5.26.1 '@socketsecurity/registry': 2.0.2 '@socketsecurity/sdk': 4.0.1 '@types/adm-zip': 0.5.7 @@ -165,6 +165,15 @@ minimumReleaseAgeExclude: - '@socketregistry/*' - '@socketsecurity/*' +# Refuse transitive dependencies declared via git/tarball/local-tarball +# specs — an npm package shouldn't be allowed to drag in a git URL we +# don't control (bypasses npm registry validation, no provenance, no +# soak window). Direct git deps are still allowed (the test suite at +# pnpm/pkg-manager/core/test/install/blockExoticSubdeps.ts confirms +# this). pnpm's current default is `false`; declared explicitly so a +# future flip can't silently change install behavior. +blockExoticSubdeps: true + # Dependency overrides (migrated from package.json pnpm.overrides). overrides: '@octokit/graphql': 'catalog:' diff --git a/scripts/check.mts b/scripts/check.mts index b2115b5b7..675445127 100644 --- a/scripts/check.mts +++ b/scripts/check.mts @@ -380,6 +380,45 @@ async function main(): Promise { } } + // Run path-hygiene check (1 path, 1 reference). See + // .claude/skills/path-guard/ + .claude/hooks/path-guard/. + if (runAll) { + if (!quiet) { + logger.log('') + logger.progress('Running path-hygiene check (1 path, 1 reference)') + } + // Resolve the gate path against scripts/ so this runner works + // when invoked from any cwd (root or a workspace package dir). + const gatePath = path.join(scriptsDir, 'check-paths.mts') + const repoRoot = path.dirname(scriptsDir) + const pathHygieneResult = await spawn('node', [gatePath, '--quiet'], { + cwd: repoRoot, + shell: WIN32, + stdio: 'pipe', + stdioString: true, + }) + if (pathHygieneResult.code !== 0) { + if (!quiet) { + logger.clearLine() + logger.error( + 'Path-hygiene check failed — rerun with --explain for details', + ) + } + if (pathHygieneResult.stdout) { + logger.log(pathHygieneResult.stdout) + } + if (pathHygieneResult.stderr) { + logger.error(pathHygieneResult.stderr) + } + process.exitCode = pathHygieneResult.code + return + } + if (!quiet) { + logger.clearLine() + logger.success('Path-hygiene check passed') + } + } + if (!quiet) { logger.log('') logger.success('All checks passed') diff --git a/scripts/power-state.mts b/scripts/power-state.mts new file mode 100644 index 000000000..2d3270065 --- /dev/null +++ b/scripts/power-state.mts @@ -0,0 +1,165 @@ +/** + * @fileoverview Detect whether the host is currently on AC power + * (vs battery). Used by long-running build/test scripts to size + * timeouts adaptively — laptops on battery throttle CPU hard + * (especially macOS), and a static timeout that fits AC will kill + * an otherwise-healthy run on battery. + * + * Two paths, in priority order: + * + * 1. `node:smol-power` — when running inside a node-smol binary + * that ships the smol_power native binding (socket-btm's custom + * Node distribution). Pure C++ syscalls, sub-millisecond. + * + * 2. Shellout fallback — system Node doesn't have node:smol-power. + * Each platform has a different mechanism: + * * macOS: `pmset -g batt` parses "AC Power" / "Battery Power" + * * Linux: reads /sys/class/power_supply//online + * (no shellout, just open/read syscalls) + * * Windows: PowerShell `Get-CimInstance Win32_Battery` + * + * On detection failure we conservatively assume AC — the downstream + * timeout becomes the shorter / more aggressive value, which is + * appropriate for build servers and headless CI (those environments + * are expected to run at full speed). + * + * Returns a Promise so callers don't block the event loop on shellout + * paths. + * + * Byte-identical across the fleet via socket-repo-template's + * sync-scaffolding (IDENTICAL_FILES). + */ + +import { existsSync, promises as fs } from 'node:fs' +import path from 'node:path' +import process from 'node:process' + +import { spawn } from '@socketsecurity/lib/spawn' + +// Probe for node:smol-power. Lives in socket-btm's node-smol binary. +// Wrapped in try/catch so this file is safe to import on system Node +// where the module doesn't exist. +let _smolPower: { isOnAcPower: () => boolean } | undefined +async function getSmolPower(): Promise { + if (_smolPower !== undefined) { + return _smolPower + } + try { + const mod = await import('node:smol-power') + _smolPower = mod + return _smolPower + } catch { + _smolPower = undefined + return undefined + } +} + +async function detectMacOs(): Promise { + try { + // `pmset -g batt` on macOS prints lines like + // Now drawing from 'AC Power' + // Now drawing from 'Battery Power' + // Match the AC variant; everything else (battery, unknown) is + // treated as not-AC. + const result = await spawn('pmset', ['-g', 'batt'], { + stdio: ['ignore', 'pipe', 'ignore'], + }) + return /AC Power/.test(result.stdout || '') + } catch { + return true + } +} + +async function detectLinux(): Promise { + // Linux exposes power state under /sys/class/power_supply. Each + // AC adapter is its own dir (`AC`, `ADP1`, `AC0`, `ACAD`, …) + // with an `online` file holding "1" when power is connected. + // Containers and headless servers often have no power_supply + // tree at all — treat that as AC since those environments are + // expected to run at full speed. + const psDir = '/sys/class/power_supply' + if (!existsSync(psDir)) { + return true + } + try { + const entries = await fs.readdir(psDir) + for (const entry of entries) { + const onlineFile = path.join(psDir, entry, 'online') + if (!existsSync(onlineFile)) { + continue + } + try { + const value = await fs.readFile(onlineFile, 'utf8') + if (value.trim() === '1') { + return true + } + } catch { + // Unreadable entry — skip; another entry may report. + } + } + } catch { + // Directory enumeration failed — fall through to AC. + return true + } + return false +} + +async function detectWindows(): Promise { + try { + // Windows: query the battery status via PowerShell + CIM. + // `Win32_Battery.BatteryStatus`: + // 1 = Discharging (battery) + // 2 = On AC, not charging or fully charged + // 3..5 = Various battery states + // 6 = AC + charging + // Desktops with no battery return an empty result; treat as AC. + const result = await spawn( + 'powershell.exe', + [ + '-NoProfile', + '-Command', + '(Get-CimInstance -ClassName Win32_Battery).BatteryStatus', + ], + { stdio: ['ignore', 'pipe', 'ignore'] }, + ) + const trimmed = (result.stdout || '').trim() + if (trimmed === '') { + return true + } + const status = Number.parseInt(trimmed, 10) + if (Number.isNaN(status)) { + return true + } + return status === 2 || status === 6 + } catch { + return true + } +} + +/** + * Returns `true` if the host is on AC power. Conservative on + * detection failure (returns `true`) — callers using this for + * timeout sizing prefer a longer timeout to a too-short one. + * + * Prefers the native binding (`node:smol-power`) when running + * inside a node-smol binary; falls back to a per-platform path + * (shellout on macOS / Windows, direct sysfs reads on Linux) on + * system Node. + */ +export async function isOnAcPower(): Promise { + const native = await getSmolPower() + if (native) { + return native.isOnAcPower() + } + if (process.platform === 'darwin') { + return await detectMacOs() + } + if (process.platform === 'linux') { + return await detectLinux() + } + if (process.platform === 'win32') { + return await detectWindows() + } + // Unsupported platform; conservative default. + return true +}