mirror of
https://github.com/steel-dev/cli.git
synced 2026-09-14 20:47:34 +08:00
296 lines
6.9 KiB
TypeScript
296 lines
6.9 KiB
TypeScript
import * as fs from 'node:fs';
|
|
import * as os from 'node:os';
|
|
import * as path from 'node:path';
|
|
import {spawnSync} from 'node:child_process';
|
|
|
|
const DEFAULT_CONFIG_DIR = path.join(os.homedir(), '.config', 'steel');
|
|
const DEFAULT_REPO_URL = 'https://github.com/steel-dev/steel-browser.git';
|
|
|
|
export type CommandRunOptions = {
|
|
cwd?: string;
|
|
env?: NodeJS.ProcessEnv;
|
|
stdio?: 'ignore' | 'inherit';
|
|
};
|
|
|
|
export type CommandRunResult = {
|
|
status: number | null;
|
|
error?: NodeJS.ErrnoException | Error;
|
|
};
|
|
|
|
export type CommandRunner = (
|
|
command: string,
|
|
args: string[],
|
|
options?: CommandRunOptions,
|
|
) => CommandRunResult;
|
|
|
|
export type ComposeCommand = {
|
|
command: string;
|
|
baseArgs: string[];
|
|
};
|
|
|
|
type LocalDevContext = {
|
|
configDirectory?: string;
|
|
repoUrl?: string;
|
|
runner?: CommandRunner;
|
|
environment?: NodeJS.ProcessEnv;
|
|
};
|
|
|
|
type InstallLocalBrowserRuntimeOptions = LocalDevContext & {
|
|
verbose?: boolean;
|
|
};
|
|
|
|
type StartLocalBrowserRuntimeOptions = LocalDevContext & {
|
|
port?: number;
|
|
verbose?: boolean;
|
|
skipDockerCheck?: boolean;
|
|
};
|
|
|
|
type StopLocalBrowserRuntimeOptions = LocalDevContext & {
|
|
verbose?: boolean;
|
|
};
|
|
|
|
function defaultRunner(
|
|
command: string,
|
|
args: string[],
|
|
options: CommandRunOptions = {},
|
|
): CommandRunResult {
|
|
const result = spawnSync(command, args, {
|
|
cwd: options.cwd,
|
|
env: options.env,
|
|
stdio: options.stdio || 'ignore',
|
|
});
|
|
|
|
return {
|
|
status: result.status,
|
|
error: result.error as NodeJS.ErrnoException | undefined,
|
|
};
|
|
}
|
|
|
|
function isSuccess(result: CommandRunResult): boolean {
|
|
return result.status === 0;
|
|
}
|
|
|
|
export function getLocalBrowserRepoPath(
|
|
configDirectory = DEFAULT_CONFIG_DIR,
|
|
repoUrl = DEFAULT_REPO_URL,
|
|
): string {
|
|
const repoName = path.basename(repoUrl, '.git');
|
|
return path.join(configDirectory, repoName);
|
|
}
|
|
|
|
export function deriveLocalApiPort(
|
|
environment: NodeJS.ProcessEnv = process.env,
|
|
explicitPort?: number,
|
|
): string {
|
|
if (explicitPort && Number.isInteger(explicitPort) && explicitPort > 0) {
|
|
return String(explicitPort);
|
|
}
|
|
|
|
const configuredApiUrl =
|
|
environment.STEEL_BROWSER_API_URL?.trim() ||
|
|
environment.STEEL_LOCAL_API_URL?.trim();
|
|
if (!configuredApiUrl) {
|
|
return '3000';
|
|
}
|
|
|
|
try {
|
|
const parsedUrl = new URL(configuredApiUrl);
|
|
if (parsedUrl.port) {
|
|
return parsedUrl.port;
|
|
}
|
|
|
|
if (parsedUrl.protocol === 'http:') {
|
|
return '80';
|
|
}
|
|
|
|
if (parsedUrl.protocol === 'https:') {
|
|
return '443';
|
|
}
|
|
} catch {
|
|
const portMatch = configuredApiUrl.match(/:(\d+)(?:\/|$)/);
|
|
if (portMatch?.[1]) {
|
|
return portMatch[1];
|
|
}
|
|
}
|
|
|
|
return '3000';
|
|
}
|
|
|
|
export function isDockerRunning(
|
|
runner: CommandRunner = defaultRunner,
|
|
): boolean {
|
|
const result = runner('docker', ['info'], {stdio: 'ignore'});
|
|
return isSuccess(result);
|
|
}
|
|
|
|
export function resolveComposeCommand(
|
|
runner: CommandRunner = defaultRunner,
|
|
): ComposeCommand | null {
|
|
const dockerComposeV1 = runner('docker-compose', ['version'], {
|
|
stdio: 'ignore',
|
|
});
|
|
if (isSuccess(dockerComposeV1)) {
|
|
return {command: 'docker-compose', baseArgs: []};
|
|
}
|
|
|
|
const dockerComposeV2 = runner('docker', ['compose', 'version'], {
|
|
stdio: 'ignore',
|
|
});
|
|
if (isSuccess(dockerComposeV2)) {
|
|
return {command: 'docker', baseArgs: ['compose']};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function cloneLocalBrowserRepo(
|
|
configDirectory: string,
|
|
repoUrl: string,
|
|
runner: CommandRunner,
|
|
verbose = false,
|
|
): string {
|
|
const repoPath = getLocalBrowserRepoPath(configDirectory, repoUrl);
|
|
|
|
fs.mkdirSync(configDirectory, {recursive: true});
|
|
const cloneResult = runner('git', ['clone', repoUrl], {
|
|
cwd: configDirectory,
|
|
stdio: verbose ? 'inherit' : 'ignore',
|
|
});
|
|
|
|
if (!isSuccess(cloneResult) || !fs.existsSync(repoPath)) {
|
|
throw new Error(
|
|
`Failed to clone local browser runtime repository (${repoUrl}).`,
|
|
);
|
|
}
|
|
|
|
return repoPath;
|
|
}
|
|
|
|
export function installLocalBrowserRuntime(
|
|
options: InstallLocalBrowserRuntimeOptions = {},
|
|
): {repoPath: string; repoUrl: string; installed: boolean} {
|
|
const configDirectory = options.configDirectory || DEFAULT_CONFIG_DIR;
|
|
const repoUrl = options.repoUrl || DEFAULT_REPO_URL;
|
|
const runner = options.runner || defaultRunner;
|
|
const repoPath = getLocalBrowserRepoPath(configDirectory, repoUrl);
|
|
|
|
if (fs.existsSync(repoPath)) {
|
|
return {
|
|
repoPath,
|
|
repoUrl,
|
|
installed: false,
|
|
};
|
|
}
|
|
|
|
const clonedRepoPath = cloneLocalBrowserRepo(
|
|
configDirectory,
|
|
repoUrl,
|
|
runner,
|
|
options.verbose,
|
|
);
|
|
|
|
return {
|
|
repoPath: clonedRepoPath,
|
|
repoUrl,
|
|
installed: true,
|
|
};
|
|
}
|
|
|
|
export function startLocalBrowserRuntime(
|
|
options: StartLocalBrowserRuntimeOptions = {},
|
|
): {repoPath: string; apiPort: string; composeCommand: ComposeCommand} {
|
|
const configDirectory = options.configDirectory || DEFAULT_CONFIG_DIR;
|
|
const repoUrl = options.repoUrl || DEFAULT_REPO_URL;
|
|
const environment = options.environment || process.env;
|
|
const runner = options.runner || defaultRunner;
|
|
const verbose = options.verbose || false;
|
|
const repoPath = getLocalBrowserRepoPath(configDirectory, repoUrl);
|
|
|
|
if (!fs.existsSync(repoPath)) {
|
|
throw new Error(
|
|
'Local browser runtime is not installed. Run `steel dev install` first.',
|
|
);
|
|
}
|
|
|
|
if (!options.skipDockerCheck && !isDockerRunning(runner)) {
|
|
throw new Error('Docker is not running. Start Docker and try again.');
|
|
}
|
|
|
|
const composeCommand = resolveComposeCommand(runner);
|
|
if (!composeCommand) {
|
|
throw new Error(
|
|
'Could not find Docker Compose. Install `docker compose` (v2) or `docker-compose`.',
|
|
);
|
|
}
|
|
|
|
const apiPort = deriveLocalApiPort(environment, options.port);
|
|
const composeArgs = [
|
|
...composeCommand.baseArgs,
|
|
'-f',
|
|
'docker-compose.yml',
|
|
'up',
|
|
'-d',
|
|
];
|
|
const composeResult = runner(composeCommand.command, composeArgs, {
|
|
cwd: repoPath,
|
|
env: {
|
|
...environment,
|
|
API_PORT: apiPort,
|
|
},
|
|
stdio: verbose ? 'inherit' : 'ignore',
|
|
});
|
|
|
|
if (!isSuccess(composeResult)) {
|
|
throw new Error('Failed to start local Steel Browser runtime.');
|
|
}
|
|
|
|
return {
|
|
repoPath,
|
|
apiPort,
|
|
composeCommand,
|
|
};
|
|
}
|
|
|
|
export function stopLocalBrowserRuntime(
|
|
options: StopLocalBrowserRuntimeOptions = {},
|
|
): {repoPath: string; composeCommand: ComposeCommand} {
|
|
const configDirectory = options.configDirectory || DEFAULT_CONFIG_DIR;
|
|
const repoUrl = options.repoUrl || DEFAULT_REPO_URL;
|
|
const runner = options.runner || defaultRunner;
|
|
const repoPath = getLocalBrowserRepoPath(configDirectory, repoUrl);
|
|
|
|
if (!fs.existsSync(repoPath)) {
|
|
throw new Error(
|
|
'Local browser runtime is not installed. Run `steel dev install` first.',
|
|
);
|
|
}
|
|
|
|
const composeCommand = resolveComposeCommand(runner);
|
|
if (!composeCommand) {
|
|
throw new Error(
|
|
'Could not find Docker Compose. Install `docker compose` (v2) or `docker-compose`.',
|
|
);
|
|
}
|
|
|
|
const composeArgs = [
|
|
...composeCommand.baseArgs,
|
|
'-f',
|
|
'docker-compose.yml',
|
|
'down',
|
|
];
|
|
const composeResult = runner(composeCommand.command, composeArgs, {
|
|
cwd: repoPath,
|
|
env: process.env,
|
|
stdio: options.verbose ? 'inherit' : 'ignore',
|
|
});
|
|
|
|
if (!isSuccess(composeResult)) {
|
|
throw new Error('Failed to stop local Steel Browser runtime.');
|
|
}
|
|
|
|
return {
|
|
repoPath,
|
|
composeCommand,
|
|
};
|
|
}
|