fix(workspaces): clean up distractors and fix ESM runtime errors

This commit is contained in:
mkorwel
2026-03-23 11:15:57 -07:00
parent b2fe426d90
commit 9a19cd67ea
14 changed files with 646 additions and 455 deletions
+31 -16
View File
@@ -1,12 +1,12 @@
/**
* Workspace Attach Utility (Local)
*
* Re-attaches to a running tmux session inside the container on the worker.
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import path from 'path';
import fs from 'fs';
import { spawnSync } from 'child_process';
import { fileURLToPath } from 'url';
import path from 'node:path';
import fs from 'node:fs';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { ProviderFactory } from './providers/ProviderFactory.ts';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -14,19 +14,22 @@ const REPO_ROOT = path.resolve(__dirname, '../../../..');
const q = (str: string) => `'${str.replace(/'/g, "'\\''")}'`;
export async function runAttach(args: string[], env: NodeJS.ProcessEnv = process.env) {
export async function runAttach(
args: string[],
env: NodeJS.ProcessEnv = process.env,
) {
const prNumber = args[0];
const action = args[1] || 'review';
const isLocal = args.includes('--local');
if (!prNumber) {
console.error('Usage: npm run workspace:attach <PR_NUMBER> [action] [--local]');
console.error('Usage: workspace attach <PR_NUMBER> [action] [--local]');
return 1;
}
const settingsPath = path.join(REPO_ROOT, '.gemini/workspaces/settings.json');
if (!fs.existsSync(settingsPath)) {
console.error('❌ Settings not found. Run "npm run workspace:setup" first.');
console.error('❌ Settings not found. Run "workspace setup" first.');
return 1;
}
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
@@ -38,18 +41,30 @@ export async function runAttach(args: string[], env: NodeJS.ProcessEnv = process
const { projectId, zone } = config;
const targetVM = `gcli-workspace-${env.USER || 'mattkorwel'}`;
const provider = ProviderFactory.getProvider({ projectId, zone, instanceName: targetVM });
const provider = ProviderFactory.getProvider({
projectId,
zone,
instanceName: targetVM,
});
const sessionName = `workspace-${prNumber}-${action}`;
const containerAttach = `sudo docker exec -it maintainer-worker sh -c ${q(`tmux attach-session -t ${sessionName}`)}`;
const finalSSH = provider.getRunCommand(containerAttach, { interactive: true });
const finalSSH = provider.getRunCommand(containerAttach, {
interactive: true,
});
console.log(`🔗 Attaching to session: ${sessionName}...`);
const isWithinGemini = !!env.GEMINI_CLI || !!env.GEMINI_SESSION_ID || !!env.GCLI_SESSION_ID;
const isWithinGemini =
!!env.GEMINI_CLI || !!env.GEMINI_SESSION_ID || !!env.GCLI_SESSION_ID;
if (isWithinGemini && !isLocal) {
const tempCmdPath = path.join(process.env.TMPDIR || '/tmp', `workspace-attach-${prNumber}.sh`);
fs.writeFileSync(tempCmdPath, `#!/bin/bash\n${finalSSH}\nrm "$0"`, { mode: 0o755 });
const tempCmdPath = path.join(
process.env.TMPDIR || '/tmp',
`workspace-attach-${prNumber}.sh`,
);
fs.writeFileSync(tempCmdPath, `#!/bin/bash\n${finalSSH}\nrm "$0"`, {
mode: 0o755,
});
const appleScript = `
on run argv
+44 -15
View File
@@ -1,13 +1,21 @@
import { spawnSync } from 'child_process';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { ProviderFactory } from './providers/ProviderFactory.ts';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, '../../../..');
export async function runChecker(args: string[], env: NodeJS.ProcessEnv = process.env) {
export async function runChecker(
args: string[],
env: NodeJS.ProcessEnv = process.env,
) {
const prNumber = args[0];
if (!prNumber) {
console.error('Usage: npm run review:check <PR_NUMBER>');
@@ -16,7 +24,7 @@ export async function runChecker(args: string[], env: NodeJS.ProcessEnv = proces
const settingsPath = path.join(REPO_ROOT, '.gemini/workspaces/settings.json');
if (!fs.existsSync(settingsPath)) {
console.error('❌ Settings not found. Run "npm run workspace:setup" first.');
console.error('❌ Settings not found. Run "workspace setup" first.');
return 1;
}
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
@@ -27,11 +35,21 @@ export async function runChecker(args: string[], env: NodeJS.ProcessEnv = proces
}
const { projectId, zone, remoteWorkDir } = config;
const targetVM = `gcli-workspace-${env.USER || 'mattkorwel'}`;
const provider = ProviderFactory.getProvider({ projectId, zone, instanceName: targetVM });
const provider = ProviderFactory.getProvider({
projectId,
zone,
instanceName: targetVM,
});
console.log(`🔍 Checking remote status for PR #${prNumber} on ${targetVM}...`);
console.log(
`🔍 Checking remote status for PR #${prNumber} on ${targetVM}...`,
);
const branchView = spawnSync('gh', ['pr', 'view', prNumber, '--json', 'headRefName', '-q', '.headRefName'], { shell: true });
const branchView = spawnSync(
'gh',
['pr', 'view', prNumber, '--json', 'headRefName', '-q', '.headRefName'],
{ shell: true },
);
const branchName = branchView.stdout.toString().trim();
const logDir = `${remoteWorkDir}/${branchName}/.gemini/logs/review-${prNumber}`;
@@ -41,13 +59,20 @@ export async function runChecker(args: string[], env: NodeJS.ProcessEnv = proces
console.log('\n--- Task Status ---');
for (const task of tasks) {
const exitFile = `${logDir}/${task}.exit`;
const checkExit = await provider.getExecOutput(`[ -f ${exitFile} ] && cat ${exitFile}`, { wrapContainer: 'maintainer-worker' });
const checkExit = await provider.getExecOutput(
`[ -f ${exitFile} ] && cat ${exitFile}`,
{ wrapContainer: 'maintainer-worker' },
);
if (checkExit.status === 0 && checkExit.stdout.trim()) {
const code = checkExit.stdout.trim();
console.log(` ${code === '0' ? '✅' : '❌'} ${task.padEnd(10)}: ${code === '0' ? 'SUCCESS' : `FAILED (exit ${code})`}`);
console.log(
` ${code === '0' ? '✅' : '❌'} ${task.padEnd(10)}: ${code === '0' ? 'SUCCESS' : `FAILED (exit ${code})`}`,
);
} else {
const checkRunning = await provider.exec(`[ -f ${logDir}/${task}.log ]`, { wrapContainer: 'maintainer-worker' });
const checkRunning = await provider.exec(`[ -f ${logDir}/${task}.log ]`, {
wrapContainer: 'maintainer-worker',
});
if (checkRunning === 0) {
console.log(`${task.padEnd(10)}: RUNNING`);
} else {
@@ -58,9 +83,13 @@ export async function runChecker(args: string[], env: NodeJS.ProcessEnv = proces
}
if (allDone) {
console.log('\n✨ All remote tasks complete. You can now synthesize the results.');
console.log(
'\n✨ All remote tasks complete. You can now synthesize the results.',
);
} else {
console.log('\n⏳ Some tasks are still in progress. Check again in a few minutes.');
console.log(
'\n⏳ Some tasks are still in progress. Check again in a few minutes.',
);
}
return 0;
}
+61 -29
View File
@@ -1,20 +1,22 @@
/**
* Universal Workspace Cleanup (Local)
*
* Surgical or full cleanup of sessions and worktrees on the GCE worker.
* Refactored to use WorkerProvider for container compatibility.
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
import readline from 'readline';
import path from 'node:path';
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import readline from 'node:readline';
import { ProviderFactory } from './providers/ProviderFactory.ts';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, '../../../..');
async function confirm(question: string): Promise<boolean> {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) => {
rl.question(`${question} (y/n): `, (answer) => {
rl.close();
@@ -23,13 +25,16 @@ async function confirm(question: string): Promise<boolean> {
});
}
export async function runCleanup(args: string[], env: NodeJS.ProcessEnv = process.env) {
export async function runCleanup(
args: string[],
env: NodeJS.ProcessEnv = process.env,
) {
const prNumber = args[0];
const action = args[1];
const settingsPath = path.join(REPO_ROOT, '.gemini/workspaces/settings.json');
if (!fs.existsSync(settingsPath)) {
console.error('❌ Settings not found. Run "npm run workspace:setup" first.');
console.error('❌ Settings not found. Run "workspace setup" first.');
return 1;
}
@@ -42,54 +47,81 @@ export async function runCleanup(args: string[], env: NodeJS.ProcessEnv = proces
const { projectId, zone } = config;
const targetVM = `gcli-workspace-${env.USER || 'mattkorwel'}`;
const provider = ProviderFactory.getProvider({ projectId, zone, instanceName: targetVM });
const provider = ProviderFactory.getProvider({
projectId,
zone,
instanceName: targetVM,
});
if (prNumber && action) {
const sessionName = `workspace-${prNumber}-${action}`;
const worktreePath = `/home/node/.workspaces/worktrees/${sessionName}`;
console.log(`🧹 Surgically removing session and worktree for ${prNumber}-${action}...`);
console.log(
`🧹 Surgically removing session and worktree for ${prNumber}-${action}...`,
);
// Kill specific tmux session inside container
await provider.exec(`tmux kill-session -t ${sessionName} 2>/dev/null`, { wrapContainer: 'maintainer-worker' });
await provider.exec(`tmux kill-session -t ${sessionName} 2>/dev/null`, {
wrapContainer: 'maintainer-worker',
});
// Remove specific worktree inside container
await provider.exec(`cd /home/node/.workspaces/main && git worktree remove -f ${worktreePath} 2>/dev/null && git worktree prune`, { wrapContainer: 'maintainer-worker' });
await provider.exec(
`cd /home/node/.workspaces/main && git worktree remove -f ${worktreePath} 2>/dev/null && git worktree prune`,
{ wrapContainer: 'maintainer-worker' },
);
console.log(`✅ Cleaned up ${prNumber}-${action}.`);
return 0;
}
// --- Bulk Cleanup ---
console.log(`⚠️ DANGER: You are about to perform a BULK cleanup on ${targetVM}.`);
const confirmed = await confirm(' Are you sure you want to kill ALL sessions and worktrees?');
console.log(
`⚠️ DANGER: You are about to perform a BULK cleanup on ${targetVM}.`,
);
const confirmed = await confirm(
' Are you sure you want to kill ALL sessions and worktrees?',
);
if (!confirmed) {
console.log('❌ Cleanup cancelled.');
return 0;
console.log('❌ Cleanup cancelled.');
return 0;
}
console.log(`🧹 Starting BULK cleanup...`);
// 1. Standard Cleanup
console.log(' - Killing ALL remote tmux sessions...');
await provider.exec(`tmux kill-server`, { wrapContainer: 'maintainer-worker' });
await provider.exec(`tmux kill-server`, {
wrapContainer: 'maintainer-worker',
});
console.log(' - Cleaning up Docker resources...');
await provider.exec(`sudo docker rm -f maintainer-worker || true`);
await provider.exec(`sudo docker system prune -af --volumes`);
console.log(' - Cleaning up ALL Git Worktrees...');
await provider.exec(`cd /home/node/.workspaces/main && git worktree prune && rm -rf /home/node/.workspaces/worktrees/*`, { wrapContainer: 'maintainer-worker' });
await provider.exec(
`cd /home/node/.workspaces/main && git worktree prune && rm -rf /home/node/.workspaces/worktrees/*`,
{ wrapContainer: 'maintainer-worker' },
);
console.log('✅ Remote environment cleared.');
// 2. Full Wipe Option
const shouldWipe = await confirm('\nWould you like to COMPLETELY wipe the remote workspace (main clone)?');
const shouldWipe = await confirm(
'\nWould you like to COMPLETELY wipe the remote workspace (main clone)?',
);
if (shouldWipe) {
console.log(`🔥 Wiping /home/node/.workspaces/main...`);
await provider.exec(`rm -rf /home/node/.workspaces/main && mkdir -p /home/node/.workspaces/main`, { wrapContainer: 'maintainer-worker' });
console.log('✅ Remote hub wiped. You will need to run npm run workspace:setup again.');
await provider.exec(
`rm -rf /home/node/.workspaces/main && mkdir -p /home/node/.workspaces/main`,
{ wrapContainer: 'maintainer-worker' },
);
console.log(
'✅ Remote hub wiped. You will need to run workspace setup again.',
);
}
return 0;
}
+59 -32
View File
@@ -1,12 +1,12 @@
/**
* Workspace Fleet Manager
*
* Manages dynamic GCP workers for workspaces tasks.
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { spawnSync } from 'child_process';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { ProviderFactory } from './providers/ProviderFactory.ts';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -22,7 +22,9 @@ function getProjectId(): string {
try {
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
return settings.workspace?.projectId;
} catch (e) {}
} catch {
// Ignore
}
}
return process.env.GOOGLE_CLOUD_PROJECT || '';
}
@@ -30,36 +32,47 @@ function getProjectId(): string {
async function listWorkers() {
const projectId = getProjectId();
if (!projectId) {
console.error('❌ Project ID not found. Run "npm run workspace:setup" first.');
console.error('❌ Project ID not found. Run "workspace setup" first.');
return;
}
console.log(`🔍 Listing Workspace Workers for ${USER} in ${projectId}...`);
spawnSync('gcloud', [
'compute', 'instances', 'list',
'--project', projectId,
'--filter', `name~^${INSTANCE_PREFIX}`,
'--format', 'table(name,zone,status,networkInterfaces[0].networkIP:label=INTERNAL_IP,creationTimestamp)'
], { stdio: 'inherit' });
spawnSync(
'gcloud',
[
'compute',
'instances',
'list',
'--project',
projectId,
'--filter',
`name~^${INSTANCE_PREFIX}`,
'--format',
'table(name,zone,status,networkInterfaces[0].networkIP:label=INTERNAL_IP,creationTimestamp)',
],
{ stdio: 'inherit' },
);
}
async function provisionWorker() {
const projectId = getProjectId();
if (!projectId) {
console.error('❌ Project ID not found. Run "npm run workspace:setup" first.');
console.error('❌ Project ID not found. Run "workspace setup" first.');
return;
}
const provider = ProviderFactory.getProvider({
projectId: projectId,
zone: DEFAULT_ZONE,
instanceName: INSTANCE_PREFIX
const provider = ProviderFactory.getProvider({
projectId: projectId,
zone: DEFAULT_ZONE,
instanceName: INSTANCE_PREFIX,
});
const status = await provider.getStatus();
if (status.status !== 'UNKNOWN' && status.status !== 'ERROR') {
console.log(`✅ Worker ${INSTANCE_PREFIX} already exists and is ${status.status}.`);
console.log(
`✅ Worker ${INSTANCE_PREFIX} already exists and is ${status.status}.`,
);
return;
}
@@ -68,12 +81,12 @@ async function provisionWorker() {
async function stopWorker() {
const projectId = getProjectId();
const provider = ProviderFactory.getProvider({
projectId: projectId,
zone: DEFAULT_ZONE,
instanceName: INSTANCE_PREFIX
const provider = ProviderFactory.getProvider({
projectId: projectId,
zone: DEFAULT_ZONE,
instanceName: INSTANCE_PREFIX,
});
console.log(`🛑 Stopping workspace worker: ${INSTANCE_PREFIX}...`);
await provider.stop();
}
@@ -81,14 +94,28 @@ async function stopWorker() {
async function rebuildWorker() {
const projectId = getProjectId();
console.log(`🔥 Rebuilding worker ${INSTANCE_PREFIX}...`);
const knownHostsPath = path.join(REPO_ROOT, '.gemini/workspaces_known_hosts');
if (fs.existsSync(knownHostsPath)) {
console.log(` - Clearing isolated known_hosts...`);
fs.unlinkSync(knownHostsPath);
console.log(` - Clearing isolated known_hosts...`);
fs.unlinkSync(knownHostsPath);
}
spawnSync('gcloud', ['compute', 'instances', 'delete', INSTANCE_PREFIX, '--project', projectId, '--zone', DEFAULT_ZONE, '--quiet'], { stdio: 'inherit' });
spawnSync(
'gcloud',
[
'compute',
'instances',
'delete',
INSTANCE_PREFIX,
'--project',
projectId,
'--zone',
DEFAULT_ZONE,
'--quiet',
],
{ stdio: 'inherit' },
);
await provisionWorker();
}
+13 -10
View File
@@ -1,12 +1,12 @@
/**
* Workspace Log Tailer (Local)
*
* Tails the latest remote logs for a specific job.
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { spawnSync } from 'child_process';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, '../../../..');
@@ -14,9 +14,9 @@ const REPO_ROOT = path.resolve(__dirname, '../../../..');
export async function runLogs(args: string[]) {
const prNumber = args[0];
const action = args[1] || 'review';
if (!prNumber) {
console.error('Usage: npm run workspace:logs <PR_NUMBER> [action]');
console.error('Usage: workspace logs <PR_NUMBER> [action]');
return 1;
}
@@ -42,7 +42,10 @@ export async function runLogs(args: string[]) {
tail -f "$latest_log"
`;
spawnSync(`ssh -F ${sshConfigPath} ${remoteHost} ${JSON.stringify(tailCmd)}`, { stdio: 'inherit', shell: true });
spawnSync(
`ssh -F ${sshConfigPath} ${remoteHost} ${JSON.stringify(tailCmd)}`,
{ stdio: 'inherit', shell: true },
);
return 0;
}
+93 -60
View File
@@ -1,63 +1,71 @@
/**
* Workspace Orchestrator (Local)
*
* Central coordination of remote tasks.
* Wakes workers, prepares worktrees, and launches tmux sessions.
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { spawnSync } from 'child_process';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { ProviderFactory } from './providers/ProviderFactory.ts';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, '../../../..');
function q(str: string) {
return `'${str.replace(/'/g, "'\\''")}'`;
return `'${str.replace(/'/g, "'\\''")}'`;
}
export async function runOrchestrator(args: string[], env: NodeJS.ProcessEnv = process.env) {
export async function runOrchestrator(
args: string[],
env: NodeJS.ProcessEnv = process.env,
) {
let prNumber = args[0];
let action = args[1] || 'review';
// Handle "shell" mode: npm run workspace:shell [identifier]
// Handle "shell" mode: workspace shell [identifier]
const isShellMode = prNumber === 'shell';
if (isShellMode) {
prNumber = args[1] || `adhoc-${Math.floor(Math.random() * 10000)}`;
action = 'shell';
prNumber = args[1] || `adhoc-${Math.floor(Math.random() * 10000)}`;
action = 'shell';
}
if (!prNumber) {
console.error('❌ Usage: npm run workspace <PR_NUMBER> [action] OR npm run workspace:shell [identifier]');
console.error(
'❌ Usage: workspace <PR_NUMBER> [action] OR workspace shell [identifier]',
);
return 1;
}
// 1. Load Settings
const settingsPath = path.join(REPO_ROOT, '.gemini/workspaces/settings.json');
if (!fs.existsSync(settingsPath)) {
console.error('❌ Workspace settings not found. Run "npm run workspace:setup" first.');
console.error(
'❌ Workspace settings not found. Run "workspace setup" first.',
);
return 1;
}
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
const config = settings.workspace;
const targetVM = `gcli-workspace-${env.USER || 'mattkorwel'}`;
const provider = ProviderFactory.getProvider({ projectId: config.projectId, zone: config.zone, instanceName: targetVM });
const provider = ProviderFactory.getProvider({
projectId: config.projectId,
zone: config.zone,
instanceName: targetVM,
});
// 2. Wake Worker & Verify Container
await provider.ensureReady();
// Retrieve the remote user to ensure we run git commands correctly
const whoamiRes = await provider.getExecOutput('whoami');
const remoteUser = whoamiRes.stdout.trim();
await provider.getExecOutput('whoami');
// Paths - Unified across host and container
const hostWorkspaceRoot = `/home/node/.workspaces`;
const hostWorkDir = `${hostWorkspaceRoot}/main`;
const containerHome = '/home/node';
const containerWorkspaceRoot = `/home/node/.workspaces`;
const remotePolicyPath = `${containerWorkspaceRoot}/policies/workspace-policy.toml`;
const persistentScripts = `${containerWorkspaceRoot}/scripts`;
const sessionName = `workspace-${prNumber}-${action}`;
@@ -65,40 +73,42 @@ export async function runOrchestrator(args: string[], env: NodeJS.ProcessEnv = p
const hostWorktreeDir = `${hostWorkspaceRoot}/worktrees/${sessionName}`;
// 3. Remote Context Setup (Executed on HOST for permission simplicity)
console.log(`🚀 Preparing remote environment for ${action} on ${isShellMode ? 'branch/id' : '#'}${prNumber}...`);
console.log(
`🚀 Preparing remote environment for ${action} on ${isShellMode ? 'branch/id' : '#'}${prNumber}...`,
);
// FIX: Use the host path to check for existence
const check = await provider.getExecOutput(`ls -d ${hostWorktreeDir}/.git`);
// FIX: Ensure container user (node) owns the workspaces directories
console.log(' - Synchronizing container permissions...');
await provider.exec(`sudo chown -R 1000:1000 /home/node/.workspaces`);
if (check.status !== 0) {
console.log(` - Provisioning isolated git worktree for ${prNumber}...`);
if (check.status !== 0) {
console.log(` - Provisioning isolated git worktree for ${prNumber}...`);
// We run these on the host. Since setup might have left the repo root-owned, we use sudo.
// We use environment variables to bypass safe.directory checks on a read-only filesystem.
const gitEnv = `GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=safe.directory GIT_CONFIG_VALUE_0=${hostWorkDir}`;
// We run these on the host. Since setup might have left the repo root-owned, we use sudo.
// We use environment variables to bypass safe.directory checks on a read-only filesystem.
const gitEnv = `GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=safe.directory GIT_CONFIG_VALUE_0=${hostWorkDir}`;
const gitFetch = isShellMode
const gitFetch = isShellMode
? `sudo ${gitEnv} git -C ${hostWorkDir} fetch --quiet origin`
: `sudo ${gitEnv} git -C ${hostWorkDir} fetch --quiet upstream pull/${prNumber}/head`;
const gitTarget = isShellMode ? 'FETCH_HEAD' : 'FETCH_HEAD';
const gitTarget = isShellMode ? 'FETCH_HEAD' : 'FETCH_HEAD';
const setupCmd = `
sudo mkdir -p ${hostWorkspaceRoot}/worktrees && \
sudo chown chronos:chronos ${hostWorkspaceRoot}/worktrees && \
${gitFetch} && \
sudo ${gitEnv} git -C ${hostWorkDir} worktree add --quiet -f ${hostWorktreeDir} ${gitTarget} 2>&1 && \
sudo chown -R 1000:1000 ${hostWorkspaceRoot}
`;
const setupCmd = `
sudo mkdir -p ${hostWorkspaceRoot}/worktrees && \
sudo chown chronos:chronos ${hostWorkspaceRoot}/worktrees && \
${gitFetch} && \
sudo ${gitEnv} git -C ${hostWorkDir} worktree add --quiet -f ${hostWorktreeDir} ${gitTarget} 2>&1 && \
sudo chown -R 1000:1000 ${hostWorkspaceRoot}
`;
const setupRes = await provider.getExecOutput(setupCmd);
if (setupRes.status !== 0) {
console.error(' ❌ Failed to provision remote worktree.');
console.error(' STDOUT:', setupRes.stdout);
console.error(' STDERR:', setupRes.stderr);
return 1;
console.error(' ❌ Failed to provision remote worktree.');
console.error(' STDOUT:', setupRes.stdout);
console.error(' STDERR:', setupRes.stderr);
return 1;
}
console.log(' ✅ Worktree provisioned successfully.');
} else {
@@ -107,13 +117,19 @@ if (check.status !== 0) {
// AUTH: Dynamically retrieve credentials from host-side config/disk
const remoteConfigPath = `${hostWorkspaceRoot}/gemini-cli-config/.gemini/settings.json`;
const remoteSettingsRes = await provider.getExecOutput(`cat ${remoteConfigPath}`);
const remoteSettingsRes = await provider.getExecOutput(
`cat ${remoteConfigPath}`,
);
const remoteSettingsJson = remoteSettingsRes.stdout.trim();
const apiKeyRes = await provider.getExecOutput(`cat ${remoteConfigPath} | grep apiKey | cut -d '\"' -f 4`);
const apiKeyRes = await provider.getExecOutput(
`cat ${remoteConfigPath} | grep apiKey | cut -d '"' -f 4`,
);
const remoteApiKey = apiKeyRes.stdout.trim();
const ghTokenRes = await provider.getExecOutput(`cat ${hostWorkspaceRoot}/.gh_token`);
const ghTokenRes = await provider.getExecOutput(
`cat ${hostWorkspaceRoot}/.gh_token`,
);
const remoteGhToken = ghTokenRes.stdout.trim();
// AUTH: Inject credentials and settings directly into the worktree
@@ -126,19 +142,23 @@ GEMINI_AUTO_UPDATE=0
GEMINI_SANDBOX=workspace
GEMINI_HOST=${targetVM}
`.trim();
await provider.exec(`sudo docker exec maintainer-worker sh -c ${q(`echo ${q(dotEnvContent)} > ${remoteWorktreeDir}/.env`)}`);
await provider.exec(
`sudo docker exec maintainer-worker sh -c ${q(`echo ${q(dotEnvContent)} > ${remoteWorktreeDir}/.env`)}`,
);
// Also inject the settings.json into the worktree's .gemini folder for maximum reliability
await provider.exec(`sudo docker exec maintainer-worker sh -c ${q(`mkdir -p ${remoteWorktreeDir}/.gemini && echo ${q(remoteSettingsJson)} > ${remoteWorktreeDir}/.gemini/settings.json`)}`);
await provider.exec(
`sudo docker exec maintainer-worker sh -c ${q(`mkdir -p ${remoteWorktreeDir}/.gemini && echo ${q(remoteSettingsJson)} > ${remoteWorktreeDir}/.gemini/settings.json`)}`,
);
// 4. Execution Logic
// In shell mode, we just start gemini. In action mode, we run the entrypoint.
const remoteWorker = isShellMode
const remoteWorker = isShellMode
? `gemini`
: `tsx ${persistentScripts}/entrypoint.ts ${prNumber} . ${remotePolicyPath} ${action}`;
const authEnv = `-e GEMINI_AUTO_UPDATE=0 ${remoteApiKey ? `-e GEMINI_API_KEY=${remoteApiKey} ` : ''}${remoteGhToken ? `-e GITHUB_TOKEN=${remoteGhToken} -e GH_TOKEN=${remoteGhToken} ` : ''}`;
// PERSISTENCE: Wrap the entire execution in a tmux session inside the container
// We HIDE the tmux status bar to reduce visual noise
const tmuxStyle = `
@@ -147,25 +167,37 @@ GEMINI_HOST=${targetVM}
const tmuxCmd = `tmux new-session -A -s ${sessionName} ${q(`${tmuxStyle} cd ${remoteWorktreeDir} && ${remoteWorker}; exec $SHELL`)}`;
const containerWrap = `sudo docker exec -it -e COLORTERM=truecolor -e TERM=xterm-256color ${authEnv}maintainer-worker sh -c ${q(tmuxCmd)}`;
const finalSSH = provider.getRunCommand(containerWrap, { interactive: true });
const isWithinGemini = !!env.GEMINI_CLI || !!env.GEMINI_SESSION_ID || !!env.GCLI_SESSION_ID;
const isWithinGemini =
!!env.GEMINI_CLI || !!env.GEMINI_SESSION_ID || !!env.GCLI_SESSION_ID;
// 1.5 Handle --open override
const openIdx = args.indexOf('--open');
let terminalTarget = config.terminalTarget || 'tab';
if (openIdx !== -1 && args[openIdx + 1]) {
terminalTarget = args[openIdx + 1];
terminalTarget = args[openIdx + 1];
}
const forceMainTerminal = terminalTarget === 'foreground';
if (!forceMainTerminal && isWithinGemini && env.TERM_PROGRAM === 'iTerm.app') {
const tempCmdPath = path.join(process.env.TMPDIR || '/tmp', `workspace-ssh-${prNumber}.sh`);
fs.writeFileSync(tempCmdPath, `#!/bin/bash\n${finalSSH}\nrm "$0"`, { mode: 0o755 });
if (
!forceMainTerminal &&
isWithinGemini &&
env.TERM_PROGRAM === 'iTerm.app'
) {
const tempCmdPath = path.join(
process.env.TMPDIR || '/tmp',
`workspace-ssh-${prNumber}.sh`,
);
fs.writeFileSync(tempCmdPath, `#!/bin/bash\n${finalSSH}\nrm "$0"`, {
mode: 0o755,
});
const appleScript = terminalTarget === 'window' ? `
const appleScript =
terminalTarget === 'window'
? `
on run argv
tell application "iTerm"
set newWindow to (create window with default profile)
@@ -175,7 +207,8 @@ GEMINI_HOST=${targetVM}
activate
end tell
end run
` : `
`
: `
on run argv
tell application "iTerm"
tell current window
@@ -5,10 +5,10 @@
*/
/**
* WorkspaceProvider interface defines the contract for different remote
* WorkerProvider interface defines the contract for different remote
* execution environments (GCE, Workstations, etc.).
*/
export interface WorkspaceProvider {
export interface WorkerProvider {
/**
* Provisions the underlying infrastructure.
*/
@@ -37,12 +37,19 @@ export interface WorkspaceProvider {
/**
* Executes a command on the workspace and returns the output.
*/
getExecOutput(command: string, options?: ExecOptions): Promise<{ status: number; stdout: string; stderr: string }>;
getExecOutput(
command: string,
options?: ExecOptions,
): Promise<{ status: number; stdout: string; stderr: string }>;
/**
* Synchronizes local files to the workspace.
*/
sync(localPath: string, remotePath: string, options?: SyncOptions): Promise<number>;
sync(
localPath: string,
remotePath: string,
options?: SyncOptions,
): Promise<number>;
/**
* Returns the status of the workspace.
@@ -4,14 +4,20 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { spawnSync } from 'child_process';
import path from 'path';
import fs from 'fs';
import os from 'os';
import { WorkspaceProvider, SetupOptions, ExecOptions, SyncOptions, WorkspaceStatus } from './BaseProvider.ts';
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import fs from 'node:fs';
import os from 'node:os';
import {
type WorkerProvider,
type SetupOptions,
type ExecOptions,
type SyncOptions,
type WorkspaceStatus,
} from './BaseProvider.ts';
import { GceConnectionManager } from './GceConnectionManager.ts';
export class GceCosProvider implements WorkspaceProvider {
export class GceCosProvider implements WorkerProvider {
private projectId: string;
private zone: string;
private instanceName: string;
@@ -20,46 +26,143 @@ export class GceCosProvider implements WorkspaceProvider {
private sshAlias = 'gcli-worker';
private conn: GceConnectionManager;
constructor(projectId: string, zone: string, instanceName: string, repoRoot: string) {
constructor(
projectId: string,
zone: string,
instanceName: string,
repoRoot: string,
) {
this.projectId = projectId;
this.zone = zone;
this.instanceName = instanceName;
const workspacesDir = path.join(repoRoot, '.gemini/workspaces');
if (!fs.existsSync(workspacesDir)) fs.mkdirSync(workspacesDir, { recursive: true });
if (!fs.existsSync(workspacesDir))
fs.mkdirSync(workspacesDir, { recursive: true });
this.sshConfigPath = path.join(workspacesDir, 'ssh_config');
this.knownHostsPath = path.join(workspacesDir, 'known_hosts');
this.conn = new GceConnectionManager(projectId, zone, instanceName);
}
async provision(): Promise<number> {
const imageUri = 'us-docker.pkg.dev/gemini-code-dev/gemini-cli/maintainer:latest';
const imageUri =
'us-docker.pkg.dev/gemini-code-dev/gemini-cli/maintainer:latest';
const region = this.zone.split('-').slice(0, 2).join('-');
const vpcName = 'iap-vpc';
const subnetName = 'iap-subnet';
console.log(`🏗️ Ensuring "Magic" Network Infrastructure in ${this.projectId}...`);
console.log(
`🏗️ Ensuring "Magic" Network Infrastructure in ${this.projectId}...`,
);
const vpcCheck = spawnSync('gcloud', ['compute', 'networks', 'describe', vpcName, '--project', this.projectId], { stdio: 'pipe' });
const vpcCheck = spawnSync(
'gcloud',
['compute', 'networks', 'describe', vpcName, '--project', this.projectId],
{ stdio: 'pipe' },
);
if (vpcCheck.status !== 0) {
spawnSync('gcloud', ['compute', 'networks', 'create', vpcName, '--project', this.projectId, '--subnet-mode=custom'], { stdio: 'inherit' });
spawnSync(
'gcloud',
[
'compute',
'networks',
'create',
vpcName,
'--project',
this.projectId,
'--subnet-mode=custom',
],
{ stdio: 'inherit' },
);
}
const subnetCheck = spawnSync('gcloud', ['compute', 'networks', 'subnets', 'describe', subnetName, '--project', this.projectId, '--region', region], { stdio: 'pipe' });
const subnetCheck = spawnSync(
'gcloud',
[
'compute',
'networks',
'subnets',
'describe',
subnetName,
'--project',
this.projectId,
'--region',
region,
],
{ stdio: 'pipe' },
);
if (subnetCheck.status !== 0) {
spawnSync('gcloud', ['compute', 'networks', 'subnets', 'create', subnetName,
'--project', this.projectId, '--network', vpcName, '--region', region,
'--range=10.0.0.0/24', '--enable-private-ip-google-access'], { stdio: 'inherit' });
spawnSync(
'gcloud',
[
'compute',
'networks',
'subnets',
'create',
subnetName,
'--project',
this.projectId,
'--network',
vpcName,
'--region',
region,
'--range=10.0.0.0/24',
'--enable-private-ip-google-access',
],
{ stdio: 'inherit' },
);
} else {
spawnSync('gcloud', ['compute', 'networks', 'subnets', 'update', subnetName, '--project', this.projectId, '--region', region, '--enable-private-ip-google-access'], { stdio: 'pipe' });
spawnSync(
'gcloud',
[
'compute',
'networks',
'subnets',
'update',
subnetName,
'--project',
this.projectId,
'--region',
region,
'--enable-private-ip-google-access',
],
{ stdio: 'pipe' },
);
}
const fwCheck = spawnSync('gcloud', ['compute', 'firewall-rules', 'describe', 'allow-corporate-ssh', '--project', this.projectId], { stdio: 'pipe' });
const fwCheck = spawnSync(
'gcloud',
[
'compute',
'firewall-rules',
'describe',
'allow-corporate-ssh',
'--project',
this.projectId,
],
{ stdio: 'pipe' },
);
if (fwCheck.status !== 0) {
spawnSync('gcloud', ['compute', 'firewall-rules', 'create', 'allow-corporate-ssh',
'--project', this.projectId, '--network', vpcName, '--allow=tcp:22', '--source-ranges=0.0.0.0/0'], { stdio: 'inherit' });
spawnSync(
'gcloud',
[
'compute',
'firewall-rules',
'create',
'allow-corporate-ssh',
'--project',
this.projectId,
'--network',
vpcName,
'--allow=tcp:22',
'--source-ranges=0.0.0.0/0',
],
{ stdio: 'inherit' },
);
}
console.log(`🚀 Provisioning GCE COS worker: ${this.instanceName} (Unified Workspace Setup)...`);
console.log(
`🚀 Provisioning GCE COS worker: ${this.instanceName} (Unified Workspace Setup)...`,
);
const startupScriptContent = `#!/bin/bash
set -e
@@ -112,32 +215,55 @@ export class GceCosProvider implements WorkspaceProvider {
echo "✅ Unified Workspace is active."
`;
const tmpScriptPath = path.join(os.tmpdir(), `gcli-startup-${Date.now()}.sh`);
const tmpScriptPath = path.join(
os.tmpdir(),
`gcli-startup-${Date.now()}.sh`,
);
fs.writeFileSync(tmpScriptPath, startupScriptContent);
const result = spawnSync('gcloud', [
'compute', 'instances', 'create', this.instanceName,
'--project', this.projectId,
'--zone', this.zone,
'--machine-type', 'n2-standard-8',
'--image-family', 'cos-stable',
'--image-project', 'cos-cloud',
'--boot-disk-size', '10GB',
'--boot-disk-type', 'pd-balanced',
'--create-disk', `name=${this.instanceName}-data,size=200,type=pd-balanced,device-name=data,auto-delete=yes`,
'--metadata-from-file', `startup-script=${tmpScriptPath}`,
'--metadata', 'enable-oslogin=TRUE',
'--network-interface', `network=${vpcName},subnet=${subnetName},no-address`,
'--scopes', 'https://www.googleapis.com/auth/cloud-platform',
'--quiet'
], { stdio: 'inherit' });
const result = spawnSync(
'gcloud',
[
'compute',
'instances',
'create',
this.instanceName,
'--project',
this.projectId,
'--zone',
this.zone,
'--machine-type',
'n2-standard-8',
'--image-family',
'cos-stable',
'--image-project',
'cos-cloud',
'--boot-disk-size',
'10GB',
'--boot-disk-type',
'pd-balanced',
'--create-disk',
`name=${this.instanceName}-data,size=200,type=pd-balanced,device-name=data,auto-delete=yes`,
'--metadata-from-file',
`startup-script=${tmpScriptPath}`,
'--metadata',
'enable-oslogin=TRUE',
'--network-interface',
`network=${vpcName},subnet=${subnetName},no-address`,
'--scopes',
'https://www.googleapis.com/auth/cloud-platform',
'--quiet',
],
{ stdio: 'inherit' },
);
fs.unlinkSync(tmpScriptPath);
if (result.status === 0) {
console.log('⏳ Waiting for OS Login and SSH to initialize (this takes ~45s)...');
await new Promise(r => setTimeout(r, 45000));
console.log(
'⏳ Waiting for OS Login and SSH to initialize (this takes ~45s)...',
);
await new Promise((r) => setTimeout(r, 45000));
}
return result.status ?? 1;
@@ -146,47 +272,68 @@ export class GceCosProvider implements WorkspaceProvider {
async ensureReady(): Promise<number> {
const status = await this.getStatus();
if (status.status !== 'RUNNING') {
console.log(`⚠️ Worker ${this.instanceName} is ${status.status}. Waking it up...`);
const res = spawnSync('gcloud', [
'compute', 'instances', 'start', this.instanceName,
'--project', this.projectId,
'--zone', this.zone
], { stdio: 'inherit' });
console.log(
`⚠️ Worker ${this.instanceName} is ${status.status}. Waking it up...`,
);
const res = spawnSync(
'gcloud',
[
'compute',
'instances',
'start',
this.instanceName,
'--project',
this.projectId,
'--zone',
this.zone,
],
{ stdio: 'inherit' },
);
if (res.status !== 0) return res.status ?? 1;
console.log('⏳ Waiting for boot...');
await new Promise(r => setTimeout(r, 20000));
await new Promise((r) => setTimeout(r, 20000));
}
// NEW: Verify the container is actually running AND up to date
console.log(' - Verifying remote container health and image version...');
const containerCheck = await this.getExecOutput('sudo docker ps -q --filter "name=maintainer-worker"');
const containerCheck = await this.getExecOutput(
'sudo docker ps -q --filter "name=maintainer-worker"',
);
let needsUpdate = false;
if (containerCheck.status === 0 && containerCheck.stdout.trim()) {
// Check if the volume mounts are correct by checking for files inside .workspaces/main
const mountCheck = await this.getExecOutput('sudo docker exec maintainer-worker ls -A /home/node/.workspaces/main');
if (mountCheck.status !== 0 || !mountCheck.stdout.trim()) {
console.log(' ⚠️ Remote container has incorrect or empty mounts. Triggering refresh...');
needsUpdate = true;
} else {
// Check if the running image is stale
const imageUri = 'us-docker.pkg.dev/gemini-code-dev/gemini-cli/maintainer:latest';
const tmuxCheck = await this.getExecOutput('sudo docker exec maintainer-worker which tmux');
if (tmuxCheck.status !== 0) {
console.log(' ⚠️ Remote container is stale (missing tmux). Triggering update...');
needsUpdate = true;
}
}
} else {
// Check if the volume mounts are correct by checking for files inside .workspaces/main
const mountCheck = await this.getExecOutput(
'sudo docker exec maintainer-worker ls -A /home/node/.workspaces/main',
);
if (mountCheck.status !== 0 || !mountCheck.stdout.trim()) {
console.log(
' ⚠️ Remote container has incorrect or empty mounts. Triggering refresh...',
);
needsUpdate = true;
} else {
// Check if the running image is stale
const tmuxCheck = await this.getExecOutput(
'sudo docker exec maintainer-worker which tmux',
);
if (tmuxCheck.status !== 0) {
console.log(
' ⚠️ Remote container is stale (missing tmux). Triggering update...',
);
needsUpdate = true;
}
}
} else {
needsUpdate = true;
}
if (needsUpdate) {
console.log(' ⚠️ Container missing or stale. Attempting refresh...');
const imageUri = 'us-docker.pkg.dev/gemini-code-dev/gemini-cli/maintainer:latest';
// Ensure data mount is available before running
const recoverCmd = `
console.log(' ⚠️ Container missing or stale. Attempting refresh...');
const imageUri =
'us-docker.pkg.dev/gemini-code-dev/gemini-cli/maintainer:latest';
// Ensure data mount is available before running
const recoverCmd = `
(mountpoint -q /mnt/disks/data || sudo mount /dev/disk/by-id/google-data /mnt/disks/data) && \
sudo docker pull ${imageUri} && \
(sudo docker rm -f maintainer-worker || true) && \
@@ -196,12 +343,14 @@ export class GceCosProvider implements WorkspaceProvider {
-v ~/.config/gh:/home/node/.config/gh:rw \
${imageUri} /bin/bash -c "while true; do sleep 1000; done"
`;
const recoverRes = await this.exec(recoverCmd);
if (recoverRes !== 0) {
console.error(' ❌ Critical: Failed to refresh maintainer container.');
return 1;
}
console.log(' ✅ Container refreshed.');
const recoverRes = await this.exec(recoverCmd);
if (recoverRes !== 0) {
console.error(
' ❌ Critical: Failed to refresh maintainer container.',
);
return 1;
}
console.log(' ✅ Container refreshed.');
}
return 0;
@@ -227,23 +376,31 @@ Host ${this.sshAlias}
fs.writeFileSync(this.sshConfigPath, sshEntry);
console.log(` ✅ Created project SSH config: ${this.sshConfigPath}`);
console.log(' - Verifying direct connection (may trigger corporate SSO prompt)...');
console.log(
' - Verifying direct connection (may trigger corporate SSO prompt)...',
);
const res = this.conn.run('echo 1');
if (res.status !== 0) {
console.error('\n❌ All connection attempts failed. Please ensure you have "gcert" and IAP permissions.');
return 1;
console.error(
'\n❌ All connection attempts failed. Please ensure you have "gcert" and IAP permissions.',
);
return 1;
}
console.log(' ✅ Connection verified. Waiting 10s for remote disk initialization...');
await new Promise(r => setTimeout(r, 10000));
console.log(
' ✅ Connection verified. Waiting 10s for remote disk initialization...',
);
await new Promise((r) => setTimeout(r, 10000));
return 0;
}
getRunCommand(command: string, options: ExecOptions = {}): string {
let finalCmd = command;
if (options.wrapContainer) {
finalCmd = `sudo docker exec ${options.interactive ? '-it' : ''} ${options.cwd ? `-w ${options.cwd}` : ''} ${options.wrapContainer} sh -c ${this.quote(command)}`;
finalCmd = `sudo docker exec ${options.interactive ? '-it' : ''} ${options.cwd ? `-w ${options.cwd}` : ''} ${options.wrapContainer} sh -c ${this.quote(command)}`;
}
return this.conn.getRunCommand(finalCmd, { interactive: options.interactive });
return this.conn.getRunCommand(finalCmd, {
interactive: options.interactive,
});
}
async exec(command: string, options: ExecOptions = {}): Promise<number> {
@@ -251,27 +408,47 @@ Host ${this.sshAlias}
return res.status;
}
async getExecOutput(command: string, options: ExecOptions = {}): Promise<{ status: number; stdout: string; stderr: string }> {
async getExecOutput(
command: string,
options: ExecOptions = {},
): Promise<{ status: number; stdout: string; stderr: string }> {
let finalCmd = command;
if (options.wrapContainer) {
finalCmd = `sudo docker exec ${options.interactive ? '-it' : ''} ${options.cwd ? `-w ${options.cwd}` : ''} ${options.wrapContainer} sh -c ${this.quote(command)}`;
finalCmd = `sudo docker exec ${options.interactive ? '-it' : ''} ${options.cwd ? `-w ${options.cwd}` : ''} ${options.wrapContainer} sh -c ${this.quote(command)}`;
}
return this.conn.run(finalCmd, { interactive: options.interactive, stdio: options.interactive ? 'inherit' : 'pipe' });
return this.conn.run(finalCmd, {
interactive: options.interactive,
stdio: options.interactive ? 'inherit' : 'pipe',
});
}
async sync(localPath: string, remotePath: string, options: SyncOptions = {}): Promise<number> {
async sync(
localPath: string,
remotePath: string,
options: SyncOptions = {},
): Promise<number> {
console.log(`📦 Syncing ${localPath} to remote:${remotePath}...`);
return this.conn.sync(localPath, remotePath, options);
}
async getStatus(): Promise<WorkspaceStatus> {
const res = spawnSync('gcloud', [
'compute', 'instances', 'describe', this.instanceName,
'--project', this.projectId,
'--zone', this.zone,
'--format', 'json(name,status,networkInterfaces[0].networkIP)'
], { stdio: 'pipe' });
const res = spawnSync(
'gcloud',
[
'compute',
'instances',
'describe',
this.instanceName,
'--project',
this.projectId,
'--zone',
this.zone,
'--format',
'json(name,status,networkInterfaces[0].networkIP)',
],
{ stdio: 'pipe' },
);
if (res.status !== 0) {
return { name: this.instanceName, status: 'UNKNOWN' };
@@ -282,19 +459,28 @@ Host ${this.sshAlias}
return {
name: data.name,
status: data.status,
internalIp: data.networkInterfaces?.[0]?.networkIP
internalIp: data.networkInterfaces?.[0]?.networkIP,
};
} catch (e) {
return { name: this.instanceName, status: 'ERROR' };
} catch {
return { name: this.instanceName, status: 'UNKNOWN' };
}
}
async stop(): Promise<number> {
const res = spawnSync('gcloud', [
'compute', 'instances', 'stop', this.instanceName,
'--project', this.projectId,
'--zone', this.zone
], { stdio: 'inherit' });
const res = spawnSync(
'gcloud',
[
'compute',
'instances',
'stop',
this.instanceName,
'--project',
this.projectId,
'--zone',
this.zone,
],
{ stdio: 'inherit' },
);
return res.status ?? 1;
}
@@ -5,16 +5,25 @@
*/
import { GceCosProvider } from './GceCosProvider.ts';
import { WorkspaceProvider } from './BaseProvider.ts';
import path from 'path';
import { fileURLToPath } from 'url';
import type { WorkerProvider } from './BaseProvider.ts';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, '../../../../..');
export class ProviderFactory {
static getProvider(config: { projectId: string; zone: string; instanceName: string }): WorkspaceProvider {
static getProvider(config: {
projectId: string;
zone: string;
instanceName: string;
}): WorkerProvider {
// Currently we only have GceCosProvider, but this is where we'd branch
return new GceCosProvider(config.projectId, config.zone, config.instanceName, REPO_ROOT);
return new GceCosProvider(
config.projectId,
config.zone,
config.instanceName,
REPO_ROOT,
);
}
}
+25 -14
View File
@@ -1,11 +1,11 @@
/**
* Workspace Status Inspector (Local)
*
* Orchestrates remote status retrieval via the WorkerProvider.
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
import path from 'node:path';
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { ProviderFactory } from './providers/ProviderFactory.ts';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -14,7 +14,7 @@ const REPO_ROOT = path.resolve(__dirname, '../../../..');
async function runStatus(env: NodeJS.ProcessEnv = process.env) {
const settingsPath = path.join(REPO_ROOT, '.gemini/workspaces/settings.json');
if (!fs.existsSync(settingsPath)) {
console.error('❌ Settings not found. Run "npm run workspace:setup" first.');
console.error('❌ Settings not found. Run "workspace setup" first.');
return 1;
}
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
@@ -26,11 +26,17 @@ async function runStatus(env: NodeJS.ProcessEnv = process.env) {
const { projectId, zone } = config;
const targetVM = `gcli-workspace-${env.USER || 'mattkorwel'}`;
const provider = ProviderFactory.getProvider({ projectId, zone, instanceName: targetVM });
const provider = ProviderFactory.getProvider({
projectId,
zone,
instanceName: targetVM,
});
console.log(`\n🛰️ Workspace Mission Control: ${targetVM}`);
console.log(`--------------------------------------------------------------------------------`);
console.log(
`--------------------------------------------------------------------------------`,
);
const status = await provider.getStatus();
console.log(` - VM State: ${status.status}`);
console.log(` - Internal IP: ${status.internalIp || 'N/A'}`);
@@ -38,11 +44,14 @@ async function runStatus(env: NodeJS.ProcessEnv = process.env) {
if (status.status === 'RUNNING') {
console.log(`\n🧵 Active Sessions (tmux):`);
// We fetch the list of sessions from INSIDE the container
const tmuxRes = await provider.getExecOutput('tmux list-sessions -F "#S" 2>/dev/null', { wrapContainer: 'maintainer-worker' });
const tmuxRes = await provider.getExecOutput(
'tmux list-sessions -F "#S" 2>/dev/null',
{ wrapContainer: 'maintainer-worker' },
);
if (tmuxRes.status === 0 && tmuxRes.stdout.trim()) {
const sessions = tmuxRes.stdout.trim().split('\n');
sessions.forEach(s => {
sessions.forEach((s) => {
if (s.startsWith('workspace-')) {
console.log(`${s}`);
} else {
@@ -54,7 +63,9 @@ async function runStatus(env: NodeJS.ProcessEnv = process.env) {
}
}
console.log(`--------------------------------------------------------------------------------\n`);
console.log(
`--------------------------------------------------------------------------------\n`,
);
return 0;
}