mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 12:10:49 +00:00
Add installer changes
This commit is contained in:
parent
e765ccf303
commit
2158478894
13 changed files with 674 additions and 21 deletions
18
installer/.gitignore
vendored
Normal file
18
installer/.gitignore
vendored
Normal 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
254
installer/build.js
Normal 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();
|
||||
46
installer/generate-certs.js
Normal file
46
installer/generate-certs.js
Normal 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
22
installer/package.json
Normal 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": {}
|
||||
}
|
||||
128
installer/scripts/darwin_postinstall.sh
Normal file
128
installer/scripts/darwin_postinstall.sh
Normal 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
|
||||
28
installer/scripts/darwin_preinstall.sh
Normal file
28
installer/scripts/darwin_preinstall.sh
Normal 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
|
||||
49
installer/scripts/darwin_uninstall.sh
Normal file
49
installer/scripts/darwin_uninstall.sh
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue