Add installer changes

This commit is contained in:
Reinier Criel 2025-11-19 09:46:09 -08:00
parent e765ccf303
commit 2158478894
13 changed files with 674 additions and 21 deletions

18
installer/.gitignore vendored Normal file
View file

@ -0,0 +1,18 @@
# Installer build artifacts
dist/
node_modules/
# macOS specific
*.pkg
*.dmg
.DS_Store
# Temporary certificate files during development
*.pem
*.key
certs/
# Build logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*

254
installer/build.js Normal file
View file

@ -0,0 +1,254 @@
#!/usr/bin/env node
/**
* Build script for creating standalone Safe Chain binaries
* Uses esbuild to bundle ES modules, then @yao-pkg/pkg to create executable
*/
import { exec } from 'node:child_process';
import { promisify } from 'node:util';
import { existsSync, mkdirSync, copyFileSync, writeFileSync, rmSync, readFileSync, readdirSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import * as esbuild from 'esbuild';
const execAsync = promisify(exec);
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT_DIR = join(__dirname, '..');
const DIST_DIR = join(__dirname, 'dist');
const BUNDLE_DIR = join(DIST_DIR, 'bundle');
const SAFE_CHAIN_DIR = join(ROOT_DIR, 'packages/safe-chain');
/**
* Parse command line arguments
*/
function parseArgs() {
const args = process.argv.slice(2);
const platform = args.find(arg => arg.startsWith('--platform='))?.split('=')[1] || 'macos';
return { platform };
}
/**
* Ensure dist directory exists
*/
function ensureDistDirectory() {
if (!existsSync(DIST_DIR)) {
mkdirSync(DIST_DIR, { recursive: true });
}
if (!existsSync(BUNDLE_DIR)) {
mkdirSync(BUNDLE_DIR, { recursive: true });
}
}
/**
* Bundle ES modules using esbuild
* This converts ES modules to CommonJS that pkg can handle
*/
async function bundleWithEsbuild() {
console.log('Bundling with esbuild...');
const entryPoint = join(SAFE_CHAIN_DIR, 'bin/safe-chain.js');
const outputFile = join(BUNDLE_DIR, 'safe-chain-bundled.cjs');
await esbuild.build({
entryPoints: [entryPoint],
bundle: true,
platform: 'node',
target: 'node20',
format: 'cjs',
outfile: outputFile,
external: [
// Keep node: protocol imports external
'node:*',
],
loader: {
'.json': 'json', // Inline JSON files
},
banner: {
js: `// Polyfill for import.meta.url in CommonJS
var __filename = __filename || (() => {
try {
return require('url').fileURLToPath(__filename);
} catch (e) {
return __filename;
}
})();
var __dirname = __dirname || require('path').dirname(__filename);
var import_meta_url = typeof __filename !== 'undefined' ? require('url').pathToFileURL(__filename).href : undefined;
`,
},
define: {
'import.meta.url': 'import_meta_url',
},
minify: false, // Keep readable for debugging
sourcemap: false,
});
console.log('✓ Bundle created at:', outputFile);
return outputFile;
}
/**
* Build macOS binary and installer
*/
async function buildMacOS() {
console.log('Building macOS binary...');
// Step 1: Bundle with esbuild
const bundledFile = await bundleWithEsbuild();
// Step 2: Package with pkg
const targetPlatform = 'node20-macos-arm64';
const outputPath = join(DIST_DIR, 'safe-chain-macos-arm64');
// Copy shell-integration files to a staging directory with the structure we want in /snapshot
const stagingDir = join(DIST_DIR, 'staging');
const stagingShellInt = join(stagingDir, 'src/shell-integration');
// Clean and create staging directory
if (existsSync(stagingDir)) {
rmSync(stagingDir, { recursive: true });
}
mkdirSync(stagingShellInt, { recursive: true });
// Copy shell-integration directory
const shellIntegrationSrc = join(SAFE_CHAIN_DIR, 'src/shell-integration');
// Helper to copy directory recursively
const copyDir = (src, dest) => {
if (!existsSync(dest)) {
mkdirSync(dest, { recursive: true });
}
const entries = readdirSync(src, { withFileTypes: true });
for (const entry of entries) {
const srcPath = join(src, entry.name);
const destPath = join(dest, entry.name);
if (entry.isDirectory()) {
copyDir(srcPath, destPath);
} else {
copyFileSync(srcPath, destPath);
}
}
};
copyDir(shellIntegrationSrc, stagingShellInt);
const pkgArgs = [
bundledFile,
'--target', targetPlatform,
'--output', outputPath,
'--compress', 'GZip',
// Include contents of staging/src - files will be at /snapshot/src/shell-integration/
`--assets=${join(stagingDir, 'src')}/**/*`,
];
console.log(`Running: npx @yao-pkg/pkg ${pkgArgs.join(' ')}`);
// Use spawn instead of execFile to avoid issues with glob expansion
const { spawn } = await import('node:child_process');
const pkgProcess = spawn('npx', ['@yao-pkg/pkg', ...pkgArgs], {
cwd: __dirname,
stdio: 'inherit'
});
await new Promise((resolve, reject) => {
pkgProcess.on('close', (code) => {
if (code !== 0) {
reject(new Error(`pkg failed with code ${code}`));
} else {
resolve();
}
});
}); console.log('✓ Binary created at:', outputPath);
// Create installer package
await createMacOSInstaller(outputPath);
}
/**
* Create macOS installer with certificate installation
*/
async function createMacOSInstaller(binaryPath) {
console.log('Creating macOS installer package...');
const installerDir = join(DIST_DIR, 'macos-installer');
const scriptsDir = join(installerDir, 'scripts');
const resourcesDir = join(installerDir, 'resources');
// Create directory structure
mkdirSync(scriptsDir, { recursive: true });
mkdirSync(resourcesDir, { recursive: true });
// Copy binary to resources
const binaryDestination = join(resourcesDir, 'safe-chain');
copyFileSync(binaryPath, binaryDestination);
// Read installer scripts from separate files
const scriptsSourceDir = join(__dirname, 'scripts');
const preinstallScript = readFileSync(join(scriptsSourceDir, 'darwin_preinstall.sh'), 'utf8');
const postinstallScript = readFileSync(join(scriptsSourceDir, 'darwin_postinstall.sh'), 'utf8');
const uninstallScript = readFileSync(join(scriptsSourceDir, 'darwin_uninstall.sh'), 'utf8');
// Write scripts to installer directory
writeFileSync(join(scriptsDir, 'preinstall'), preinstallScript, { mode: 0o755 });
writeFileSync(join(scriptsDir, 'postinstall'), postinstallScript, { mode: 0o755 });
writeFileSync(join(installerDir, 'uninstall.sh'), uninstallScript, { mode: 0o755 });
console.log('✓ macOS installer package created at:', installerDir);
console.log('');
console.log('To create a .pkg installer, run:');
console.log(` cd ${installerDir}`);
console.log(' pkgbuild --root resources --scripts scripts --identifier com.aikido.safe-chain --version 1.0.0 --install-location /tmp/safe-chain-install SafeChain.pkg');
}
/**
* Build Linux binary and installer
*/
async function buildLinux() {
// TODO: Implement Linux binary creation
}
/**
* Build Windows binary and installer
*/
async function buildWindows() {
// TODO: Implement Windows binary creation
}
/**
* Main build function
*/
async function build() {
const { platform } = parseArgs();
console.log('=== Safe Chain Installer Builder ===');
console.log(`Platform: ${platform}`);
console.log('');
ensureDistDirectory();
try {
switch (platform) {
case 'macos':
await buildMacOS();
break;
case 'linux':
await buildLinux();
break;
case 'windows':
await buildWindows();
break;
default:
console.error(`Unknown platform: ${platform}`);
console.error('Valid options: macos, linux, windows');
process.exit(1);
}
} catch (error) {
process.exit(1);
}
}
// Run the build
build();

View file

@ -0,0 +1,46 @@
#!/usr/bin/env node
/**
* Wrapper script for certificate generation during installer build
* This re-exports the certificate generation functionality from the main package
*/
import { generateCACertificate } from '../packages/safe-chain/src/registryProxy/certUtils.js';
import { writeFileSync, mkdirSync } from 'node:fs';
import { join } from 'node:path';
/**
* Generate certificate files (simple version for installer build)
* For the full CLI version with nice output, use: safe-chain generate-cert
*
* @param {string} outputDir - Directory to save certificate files
* @returns {Promise<{certPath: string, keyPath: string}>}
*/
export async function generateCertificates(outputDir) {
console.log('Generating Safe Chain CA certificate...');
mkdirSync(outputDir, { recursive: true });
const { cert, key } = generateCACertificate();
const certPath = join(outputDir, 'ca-cert.pem');
const keyPath = join(outputDir, 'ca-key.pem');
writeFileSync(certPath, cert);
writeFileSync(keyPath, key);
console.log('✓ Certificate generated:');
console.log(` Certificate: ${certPath}`);
console.log(` Private Key: ${keyPath}`);
return { certPath, keyPath };
}
// CLI usage - when run directly
if (import.meta.url === `file://${process.argv[1]}`) {
const outputDir = process.argv[2] || './certs';
generateCertificates(outputDir).catch(error => {
console.error('Error generating certificates:', error);
process.exit(1);
});
}

22
installer/package.json Normal file
View file

@ -0,0 +1,22 @@
{
"name": "@aikidosec/safe-chain-installer",
"version": "1.0.0",
"description": "Installer for Aikido Safe Chain - creates standalone binaries and installs system certificates",
"private": true,
"type": "module",
"scripts": {
"build": "node build.js",
"build:macos": "node build.js --platform=macos",
"build:linux": "node build.js --platform=linux",
"build:windows": "node build.js --platform=windows",
"build:all": "node build.js --platform=all"
},
"keywords": [],
"author": "Aikido Security",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@yao-pkg/pkg": "^5.15.0",
"esbuild": "^0.24.0"
},
"devDependencies": {}
}

View file

@ -0,0 +1,128 @@
#!/bin/bash
set -e
echo "Installing Safe Chain..."
# The binary is installed to the location specified by --install-location
# which is passed as $3 (installation volume/mountpoint)
INSTALL_LOCATION="${3}/tmp/safe-chain-install"
# Install binary
mkdir -p /usr/local/bin
cp "${INSTALL_LOCATION}/safe-chain" /usr/local/bin/safe-chain
chmod +x /usr/local/bin/safe-chain
# Setup certificate directory in user's home
# The proxy will use ~/.safe-chain/certs/ so we need to ensure it exists
# and install the certificate from there
# Get the actual user (not root) who invoked the installer
# When using 'installer' command, SUDO_USER is not set, so we use the console user
ACTUAL_USER=$(stat -f '%Su' /dev/console)
# Get the home directory of the actual user
USER_HOME=$(eval echo "~${ACTUAL_USER}")
CERT_DIR="${USER_HOME}/.safe-chain/certs"
mkdir -p "${CERT_DIR}"
# Set ownership immediately after creating directory
chown -R "${ACTUAL_USER}:staff" "${USER_HOME}/.safe-chain"
# Generate certificate if it doesn't exist
# This ensures the same cert is used by both the proxy and system trust store
if [ ! -f "${CERT_DIR}/ca-cert.pem" ]; then
echo "Generating Safe Chain CA certificate..."
# Run as the actual user with their HOME set, not root
sudo -u "${ACTUAL_USER}" HOME="${USER_HOME}" /usr/local/bin/safe-chain generate-cert --output "${CERT_DIR}"
fi
# Set correct ownership (important since installer runs as root)
# Do this AFTER generating certificates so they get the right ownership too
chown -R "${ACTUAL_USER}:staff" "${USER_HOME}/.safe-chain"
# Install certificate in system trust store
echo "Installing Safe Chain CA certificate in system trust store..."
if [ -f "${CERT_DIR}/ca-cert.pem" ]; then
security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${CERT_DIR}/ca-cert.pem" || true
echo "✓ Certificate installed in system trust store"
else
echo "⚠ Warning: Could not find certificate to install"
exit 1
fi
# Start safe-chain as a background service
echo "Starting Safe Chain proxy service..."
# Create LaunchAgent for auto-start on login
LAUNCH_AGENT_DIR="${USER_HOME}/Library/LaunchAgents"
mkdir -p "${LAUNCH_AGENT_DIR}"
PLIST_PATH="${LAUNCH_AGENT_DIR}/com.aikido.safe-chain.plist"
cat > "${PLIST_PATH}" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.aikido.safe-chain</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/safe-chain</string>
<string>run</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>HTTPS_PROXY</key>
<string>http://localhost:8080</string>
<key>GLOBAL_AGENT_HTTP_PROXY</key>
<string>http://localhost:8080</string>
<key>NODE_EXTRA_CA_CERTS</key>
<string>${CERT_DIR}/ca-cert.pem</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>${USER_HOME}/.safe-chain/safe-chain.log</string>
<key>StandardErrorPath</key>
<string>${USER_HOME}/.safe-chain/safe-chain.error.log</string>
</dict>
</plist>
EOF
# Set correct ownership for plist
chown "${ACTUAL_USER}:staff" "${PLIST_PATH}"
# Set correct ownership for plist
chown "${ACTUAL_USER}:staff" "${PLIST_PATH}"
# Load the LaunchAgent to start the service now
# Need to run as the actual user, not root
sudo -u "${ACTUAL_USER}" launchctl load "${PLIST_PATH}" 2>/dev/null || true
# Give it a moment to start
sleep 2
# Set system-wide environment variables so all processes can use the proxy
# These affect all processes for the user, not just the LaunchAgent
echo "Setting system-wide proxy environment variables..."
sudo -u "${ACTUAL_USER}" launchctl setenv HTTPS_PROXY "http://localhost:8080"
sudo -u "${ACTUAL_USER}" launchctl setenv GLOBAL_AGENT_HTTP_PROXY "http://localhost:8080"
sudo -u "${ACTUAL_USER}" launchctl setenv NODE_EXTRA_CA_CERTS "${CERT_DIR}/ca-cert.pem"
echo "✓ Safe Chain installed successfully!"
echo ""
echo "Safe Chain is now running as a background service."
echo "It will automatically start on login."
echo ""
echo "Logs are available at:"
echo " ${USER_HOME}/.safe-chain/safe-chain.log"
echo ""
echo "To manually control the service:"
echo " Stop: launchctl unload ~/Library/LaunchAgents/com.aikido.safe-chain.plist"
echo " Start: launchctl load ~/Library/LaunchAgents/com.aikido.safe-chain.plist"
echo ""
echo "You can now use npm, pip, yarn without any additional configuration!"
echo "Package installations will be automatically scanned for malware."
exit 0

View file

@ -0,0 +1,28 @@
#!/bin/bash
set -e
echo "Preparing to install Safe Chain..."
USER_HOME="${HOME}"
if [ -z "${USER_HOME}" ]; then
USER_HOME=~
fi
# Stop existing service if running
PLIST_PATH="${USER_HOME}/Library/LaunchAgents/com.aikido.safe-chain.plist"
if [ -f "${PLIST_PATH}" ]; then
echo "Stopping existing Safe Chain service..."
launchctl unload "${PLIST_PATH}" 2>/dev/null || true
fi
# Clear any existing environment variables from previous installation
launchctl unsetenv HTTPS_PROXY 2>/dev/null || true
launchctl unsetenv GLOBAL_AGENT_HTTP_PROXY 2>/dev/null || true
launchctl unsetenv NODE_EXTRA_CA_CERTS 2>/dev/null || true
# Remove old binary if exists
if [ -f /usr/local/bin/safe-chain ]; then
rm -f /usr/local/bin/safe-chain
fi
exit 0

View file

@ -0,0 +1,49 @@
#!/bin/bash
set -e
echo "Uninstalling Safe Chain..."
USER_HOME="${HOME}"
if [ -z "${USER_HOME}" ]; then
USER_HOME=~
fi
# Stop and unload the LaunchAgent
PLIST_PATH="${USER_HOME}/Library/LaunchAgents/com.aikido.safe-chain.plist"
if [ -f "${PLIST_PATH}" ]; then
echo "Stopping Safe Chain service..."
launchctl unload "${PLIST_PATH}" 2>/dev/null || true
rm -f "${PLIST_PATH}"
fi
# Remove system-wide environment variables
echo "Removing proxy environment variables..."
launchctl unsetenv HTTPS_PROXY 2>/dev/null || true
launchctl unsetenv GLOBAL_AGENT_HTTP_PROXY 2>/dev/null || true
launchctl unsetenv NODE_EXTRA_CA_CERTS 2>/dev/null || true
# Remove binary
rm -f /usr/local/bin/safe-chain
# Remove certificate from system keychain
CERT_PATH="${USER_HOME}/.safe-chain/certs/ca-cert.pem"
if [ -f "${CERT_PATH}" ]; then
echo "Removing certificate from system trust store..."
# Find and delete the certificate by common name
security delete-certificate -c "safe-chain proxy" /Library/Keychains/System.keychain 2>/dev/null || true
fi
# Optionally remove the .safe-chain directory (commented out to preserve user data)
# echo "Remove ~/.safe-chain directory? (y/N)"
# read -r response
# if [[ "$response" =~ ^[Yy]$ ]]; then
# rm -rf "${USER_HOME}/.safe-chain"
# echo "✓ Configuration and certificates removed"
# fi
echo "✓ Safe Chain uninstalled successfully!"
echo ""
echo "Note: Certificate and configuration files in ~/.safe-chain were preserved."
echo "To remove them manually: rm -rf ~/.safe-chain"
exit 0

View file

@ -4,13 +4,15 @@
"type": "module",
"workspaces": [
"packages/*",
"test/e2e"
"test/e2e",
"installer"
],
"scripts": {
"test": "npm run test --workspace=packages/safe-chain --workspace=packages/safe-chain-bun",
"test:e2e": "npm run test --workspace=test/e2e",
"lint": "npm run lint --workspace=packages/safe-chain",
"typecheck": "npm run typecheck --workspace=packages/safe-chain"
"typecheck": "npm run typecheck --workspace=packages/safe-chain",
"build:installer": "npm run build --workspace=installer"
},
"repository": {
"type": "git",

View file

@ -2,11 +2,15 @@
import chalk from "chalk";
import { createRequire } from "module";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import { ui } from "../src/environment/userInteraction.js";
import { setup } from "../src/shell-integration/setup.js";
import { teardown } from "../src/shell-integration/teardown.js";
import { setupCi } from "../src/shell-integration/setup-ci.js";
import { runCommand } from "../src/agent/runCommand.js";
import { generateCertCommand } from "../src/agent/generateCert.js";
if (process.argv.length < 3) {
ui.writeError("No command provided. Please provide a command to execute.");
@ -32,6 +36,10 @@ if (command === "setup") {
// Pass remaining arguments to runCommand
const runArgs = process.argv.slice(3);
runCommand(runArgs);
} else if (command === "generate-cert") {
// Pass remaining arguments to generateCertCommand
const certArgs = process.argv.slice(3);
generateCertCommand(certArgs);
} else if (command === "--version" || command === "-v" || command === "-v") {
ui.writeInformation(`Current safe-chain version: ${getVersion()}`);
} else {
@ -52,8 +60,8 @@ function writeHelp() {
`Available commands: ${chalk.cyan("setup")}, ${chalk.cyan(
"teardown"
)}, ${chalk.cyan("setup-ci")}, ${chalk.cyan("run")}, ${chalk.cyan(
"help"
)}, ${chalk.cyan("--version")}`
"generate-cert"
)}, ${chalk.cyan("help")}, ${chalk.cyan("--version")}`
);
ui.emptyLine();
ui.writeInformation(
@ -74,7 +82,12 @@ function writeHelp() {
ui.writeInformation(
`- ${chalk.cyan(
"safe-chain run"
)}: Run the proxy as a standalone service. Options: --all (default), --js, --py, --ecosystem=<type>`
)}: Run the proxy as a standalone service. Sets system-wide proxy environment variables. Options: --verbose`
);
ui.writeInformation(
`- ${chalk.cyan(
"safe-chain generate-cert"
)}: Generate CA certificate for MITM proxy. Options: --output <directory>`
);
ui.writeInformation(
`- ${chalk.cyan(
@ -85,7 +98,15 @@ function writeHelp() {
}
function getVersion() {
const require = createRequire(import.meta.url);
const packageJson = require("../package.json");
return packageJson.version;
try {
// Try to load package.json from the expected location
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const packageJsonPath = join(__dirname, '../package.json');
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
return packageJson.version;
} catch (error) {
// Fallback for bundled version
return '1.0.0';
}
}

View file

@ -64,5 +64,18 @@
"type": "git",
"url": "git+https://github.com/AikidoSec/safe-chain.git",
"directory": "packages/safe-chain"
},
"pkg": {
"assets": [
"src/**/*.js",
"node_modules/**/*"
],
"targets": [
"node20-macos-arm64",
"node20-macos-x64",
"node20-linux-x64",
"node20-linux-arm64",
"node20-win-x64"
]
}
}

View file

@ -0,0 +1,60 @@
/**
* Generate certificate command for Safe Chain
* Creates CA certificate and key for MITM proxy
*/
import { generateCACertificate } from "../registryProxy/certUtils.js";
import { writeFileSync, mkdirSync } from "node:fs";
import { join } from "node:path";
import { homedir } from "node:os";
import { ui } from "../environment/userInteraction.js";
import chalk from "chalk";
/**
* Generate certificate command
* @param {string[]} args - Command line arguments
*/
export async function generateCertCommand(args) {
// Parse output directory from --output flag
let outputDir = join(homedir(), ".safe-chain");
for (let i = 0; i < args.length; i++) {
if (args[i] === "--output" && args[i + 1]) {
outputDir = args[i + 1];
break;
}
}
try {
ui.writeInformation(chalk.bold("Generating Safe Chain CA certificate..."));
ui.emptyLine();
// Create output directory
mkdirSync(outputDir, { recursive: true });
// Generate certificate
const { cert, key } = generateCACertificate();
// Write certificate and key files
const certPath = join(outputDir, "ca-cert.pem");
const keyPath = join(outputDir, "ca-key.pem");
writeFileSync(certPath, cert);
writeFileSync(keyPath, key);
ui.writeInformation(chalk.green("✓") + " Certificate generated successfully!");
ui.emptyLine();
ui.writeInformation(chalk.bold("Files created:"));
ui.writeInformation(` Certificate: ${chalk.cyan(certPath)}`);
ui.writeInformation(` Private Key: ${chalk.cyan(keyPath)}`);
ui.emptyLine();
ui.writeInformation(chalk.dim("To install this certificate in your system trust store:"));
ui.writeInformation(chalk.dim(" macOS: sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain " + certPath));
ui.writeInformation(chalk.dim(" Linux: sudo cp " + certPath + " /usr/local/share/ca-certificates/ && sudo update-ca-certificates"));
ui.writeInformation(chalk.dim(" Windows: certutil -addstore -f ROOT " + certPath));
ui.emptyLine();
} catch (/** @type {any} */ error) {
ui.writeError(`Failed to generate certificate: ${error.message}`);
process.exit(1);
}
}

View file

@ -4,8 +4,6 @@ import chalk from "chalk";
import { initializeCliArguments } from "../config/cliArguments.js";
import { writeProxyState, clearProxyState } from "./proxyState.js";
import { getCaCertPath } from "../registryProxy/certUtils.js";
import { setup } from "../shell-integration/setup.js";
import { teardown } from "../shell-integration/teardown.js";
/**
* Run the Safe Chain proxy as a standalone service
@ -26,9 +24,9 @@ export async function runCommand(args) {
// Initialize logging from args
initializeCliArguments(processedArgs);
// Automatically set up shell integration
await setup();
ui.emptyLine();
// Note: We no longer call setup() here because the installer sets up
// system-wide proxy environment variables via LaunchAgent on macOS
// or systemd on Linux. The certificate is also installed at install time.
const service = new StandaloneProxyService({
autoVerify: false
@ -54,11 +52,14 @@ export async function runCommand(args) {
ui.writeInformation(` PID: ${chalk.cyan(process.pid)}`);
ui.emptyLine();
ui.writeInformation(chalk.bold("How to Use:"));
ui.writeInformation(chalk.dim(" Restart your terminal, then run package managers normally:"));
ui.writeInformation(chalk.cyan(" npm install <package>"));
ui.writeInformation(chalk.cyan(" yarn add <package>"));
ui.writeInformation(chalk.cyan(" pip3 install <package>"));
ui.writeInformation(chalk.bold("Environment Variables Set:"));
ui.writeInformation(` ${chalk.cyan("HTTPS_PROXY")}: http://localhost:${port}`);
ui.writeInformation(` ${chalk.cyan("GLOBAL_AGENT_HTTP_PROXY")}: http://localhost:${port}`);
ui.writeInformation(` ${chalk.cyan("NODE_EXTRA_CA_CERTS")}: ${getCaCertPath()}`);
ui.emptyLine();
ui.writeInformation(chalk.bold("Package managers will use the proxy automatically."));
ui.writeInformation(chalk.dim(" No shell wrappers or aliases needed."));
ui.emptyLine();
ui.writeInformation(
@ -104,9 +105,8 @@ export async function runCommand(args) {
try {
await service.stop();
// Remove shell integration
ui.emptyLine();
await teardown();
// Note: We no longer call teardown() here because the environment
// variables are managed by the system service (LaunchAgent/systemd)
process.exit(0);
} catch (/** @type {any} */ error) {

View file

@ -116,3 +116,15 @@ function generateCa() {
certificate: cert,
};
}
/**
* Generate CA certificate and return as PEM strings
* @returns {{cert: string, key: string}}
*/
export function generateCACertificate() {
const { privateKey, certificate } = generateCa();
return {
cert: forge.pki.certificateToPem(certificate),
key: forge.pki.privateKeyToPem(privateKey),
};
}