mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -07:00
279 lines
7.5 KiB
JavaScript
279 lines
7.5 KiB
JavaScript
/**
|
|
* @license
|
|
* Copyright 2026 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
const { getAsset } = require('node:sea');
|
|
const process = require('node:process');
|
|
const nodeModule = require('node:module');
|
|
const path = require('node:path');
|
|
const { pathToFileURL } = require('node:url');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const crypto = require('node:crypto');
|
|
|
|
// --- Helper Functions ---
|
|
|
|
/**
|
|
* Strips the "ghost" argument that Node SEA sometimes injects (argv[2] == argv[0]).
|
|
* @param {string[]} argv
|
|
* @param {string} execPath
|
|
* @param {function} resolveFn
|
|
* @returns {boolean} True if an argument was removed.
|
|
*/
|
|
function sanitizeArgv(argv, execPath, resolveFn = path.resolve) {
|
|
if (argv.length > 2) {
|
|
const binaryAbs = execPath;
|
|
const arg2Abs = resolveFn(argv[2]);
|
|
if (binaryAbs === arg2Abs) {
|
|
argv.splice(2, 1);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Sanitizes a string for use in file paths.
|
|
* @param {string} name
|
|
* @returns {string}
|
|
*/
|
|
function getSafeName(name) {
|
|
return (name || 'unknown').toString().replace(/[^a-zA-Z0-9.-]/g, '_');
|
|
}
|
|
|
|
/**
|
|
* Verifies the integrity of the runtime directory against the manifest.
|
|
* @param {string} dir
|
|
* @param {object} manifest
|
|
* @param {object} fsMod
|
|
* @param {object} cryptoMod
|
|
* @returns {boolean}
|
|
*/
|
|
function verifyIntegrity(dir, manifest, fsMod = fs, cryptoMod = crypto) {
|
|
try {
|
|
const calculateHash = (filePath) => {
|
|
const hash = cryptoMod.createHash('sha256');
|
|
const fd = fsMod.openSync(filePath, 'r');
|
|
const buffer = new Uint8Array(65536); // 64KB
|
|
try {
|
|
let bytesRead = 0;
|
|
while (
|
|
(bytesRead = fsMod.readSync(fd, buffer, 0, buffer.length, null)) !== 0
|
|
) {
|
|
hash.update(buffer.subarray(0, bytesRead));
|
|
}
|
|
} finally {
|
|
fsMod.closeSync(fd);
|
|
}
|
|
return hash.digest('hex');
|
|
};
|
|
|
|
if (calculateHash(path.join(dir, 'gemini.mjs')) !== manifest.mainHash)
|
|
return false;
|
|
if (manifest.files) {
|
|
for (const file of manifest.files) {
|
|
if (calculateHash(path.join(dir, file.path)) !== file.hash)
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
} catch (_e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prepares the runtime directory, extracting assets if necessary.
|
|
* @param {object} manifest
|
|
* @param {function} getAssetFn
|
|
* @param {object} deps Dependencies (fs, os, path, processEnv)
|
|
* @returns {string} The path to the prepared runtime directory.
|
|
*/
|
|
function prepareRuntime(manifest, getAssetFn, deps = {}) {
|
|
const fsMod = deps.fs || fs;
|
|
const osMod = deps.os || os;
|
|
const pathMod = deps.path || path;
|
|
const processEnv = deps.processEnv || process.env;
|
|
const processPid = deps.processPid || process.pid;
|
|
const processUid =
|
|
deps.processUid || (process.getuid ? process.getuid() : 'unknown');
|
|
|
|
const version = manifest.version || '0.0.0';
|
|
const safeVersion = getSafeName(version);
|
|
const userInfo = osMod.userInfo();
|
|
const username =
|
|
userInfo.username || processEnv.USER || processUid || 'unknown';
|
|
const safeUsername = getSafeName(username);
|
|
|
|
let tempBase = osMod.tmpdir();
|
|
|
|
if (process.platform === 'win32' && processEnv.LOCALAPPDATA) {
|
|
const appDir = pathMod.join(processEnv.LOCALAPPDATA, 'Google', 'GeminiCLI');
|
|
try {
|
|
if (!fsMod.existsSync(appDir)) {
|
|
fsMod.mkdirSync(appDir, { recursive: true, mode: 0o700 });
|
|
}
|
|
tempBase = appDir;
|
|
} catch (_) {
|
|
// Fallback to tmpdir
|
|
}
|
|
}
|
|
|
|
const finalRuntimeDir = pathMod.join(
|
|
tempBase,
|
|
`gemini-runtime-${safeVersion}-${safeUsername}`,
|
|
);
|
|
|
|
let runtimeDir;
|
|
let useExisting = false;
|
|
|
|
const isSecure = (dir) => {
|
|
try {
|
|
const stat = fsMod.lstatSync(dir);
|
|
if (!stat.isDirectory()) return false;
|
|
if (processUid !== 'unknown' && stat.uid !== processUid) return false;
|
|
// Skip strict permission check on Windows as it's unreliable with standard fs.stat
|
|
if (process.platform !== 'win32' && (stat.mode & 0o777) !== 0o700)
|
|
return false;
|
|
return true;
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
};
|
|
if (fsMod.existsSync(finalRuntimeDir)) {
|
|
if (isSecure(finalRuntimeDir)) {
|
|
if (
|
|
verifyIntegrity(finalRuntimeDir, manifest, fsMod, deps.crypto || crypto)
|
|
) {
|
|
runtimeDir = finalRuntimeDir;
|
|
useExisting = true;
|
|
} else {
|
|
try {
|
|
fsMod.rmSync(finalRuntimeDir, { recursive: true, force: true });
|
|
} catch (_) {}
|
|
}
|
|
} else {
|
|
try {
|
|
fsMod.rmSync(finalRuntimeDir, { recursive: true, force: true });
|
|
} catch (_) {}
|
|
}
|
|
}
|
|
|
|
if (!useExisting) {
|
|
const setupDir = pathMod.join(
|
|
tempBase,
|
|
`gemini-setup-${processPid}-${Date.now()}`,
|
|
);
|
|
|
|
try {
|
|
fsMod.mkdirSync(setupDir, { recursive: true, mode: 0o700 });
|
|
const writeToSetup = (assetKey, relPath) => {
|
|
const content = getAssetFn(assetKey);
|
|
if (!content) return;
|
|
const destPath = pathMod.join(setupDir, relPath);
|
|
const destDir = pathMod.dirname(destPath);
|
|
if (!fsMod.existsSync(destDir))
|
|
fsMod.mkdirSync(destDir, { recursive: true, mode: 0o700 });
|
|
fsMod.writeFileSync(destPath, new Uint8Array(content), {
|
|
mode: 0o755,
|
|
});
|
|
};
|
|
writeToSetup('gemini.mjs', 'gemini.mjs');
|
|
if (manifest.files) {
|
|
for (const file of manifest.files) {
|
|
writeToSetup(file.key, file.path);
|
|
}
|
|
}
|
|
try {
|
|
fsMod.renameSync(setupDir, finalRuntimeDir);
|
|
runtimeDir = finalRuntimeDir;
|
|
} catch (renameErr) {
|
|
if (
|
|
fsMod.existsSync(finalRuntimeDir) &&
|
|
isSecure(finalRuntimeDir) &&
|
|
verifyIntegrity(
|
|
finalRuntimeDir,
|
|
manifest,
|
|
fsMod,
|
|
deps.crypto || crypto,
|
|
)
|
|
) {
|
|
runtimeDir = finalRuntimeDir;
|
|
try {
|
|
fsMod.rmSync(setupDir, { recursive: true, force: true });
|
|
} catch (_) {}
|
|
} else {
|
|
throw renameErr;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error(
|
|
'Fatal Error: Failed to setup secure runtime. Please try running again and if error persists please reinstall.',
|
|
e,
|
|
);
|
|
try {
|
|
fsMod.rmSync(setupDir, { recursive: true, force: true });
|
|
} catch (_) {}
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
return runtimeDir;
|
|
}
|
|
|
|
// --- Main Execution ---
|
|
|
|
async function main(getAssetFn = getAsset) {
|
|
process.env.IS_BINARY = 'true';
|
|
|
|
if (nodeModule.enableCompileCache) {
|
|
nodeModule.enableCompileCache();
|
|
}
|
|
|
|
process.noDeprecation = true;
|
|
|
|
sanitizeArgv(process.argv, process.execPath);
|
|
|
|
const manifestJson = getAssetFn('manifest.json', 'utf8');
|
|
if (!manifestJson) {
|
|
console.error('Fatal Error: Corrupted binary. Please reinstall.');
|
|
process.exit(1);
|
|
}
|
|
|
|
const manifest = JSON.parse(manifestJson);
|
|
|
|
const runtimeDir = prepareRuntime(manifest, getAssetFn, {
|
|
fs,
|
|
os,
|
|
path,
|
|
processEnv: process.env,
|
|
crypto,
|
|
});
|
|
|
|
const mainPath = path.join(runtimeDir, 'gemini.mjs');
|
|
|
|
await import(pathToFileURL(mainPath).href).catch((err) => {
|
|
console.error('Fatal Error: Failed to launch. Please reinstall.', err);
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
// Only execute if this is the main module (standard Node behavior)
|
|
// or if explicitly running as the SEA entry point (heuristic).
|
|
if (require.main === module) {
|
|
main().catch((err) => {
|
|
console.error('Unhandled error in sea-launch:', err);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
sanitizeArgv,
|
|
getSafeName,
|
|
verifyIntegrity,
|
|
prepareRuntime,
|
|
main,
|
|
};
|