mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-15 22:33:05 -07:00
feat(workspaces): transform workspaces feature into a distributable extension
This commit is contained in:
@@ -1,80 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { spawnSync, spawn } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import readline from 'readline';
|
||||
import { runOrchestrator } from '../scripts/orchestrator.ts';
|
||||
import { runWorker } from '../scripts/worker.ts';
|
||||
import { ProviderFactory } from '../scripts/providers/ProviderFactory.ts';
|
||||
|
||||
vi.mock('child_process');
|
||||
vi.mock('fs');
|
||||
vi.mock('readline');
|
||||
vi.mock('../scripts/providers/ProviderFactory.ts');
|
||||
|
||||
describe('Workspace Tooling Matrix', () => {
|
||||
const mockSettings = {
|
||||
maintainer: {
|
||||
workspace: {
|
||||
projectId: 'test-project',
|
||||
zone: 'us-west1-a',
|
||||
remoteWorkDir: '/home/node/dev/main'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const mockProvider = {
|
||||
provision: vi.fn().mockResolvedValue(0),
|
||||
ensureReady: vi.fn().mockResolvedValue(0),
|
||||
setup: vi.fn().mockResolvedValue(0),
|
||||
exec: vi.fn().mockResolvedValue(0),
|
||||
getExecOutput: vi.fn().mockResolvedValue({ status: 0, stdout: '', stderr: '' }),
|
||||
sync: vi.fn().mockResolvedValue(0),
|
||||
getStatus: vi.fn().mockResolvedValue({ name: 'test-instance', status: 'RUNNING' }),
|
||||
stop: vi.fn().mockResolvedValue(0)
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockSettings));
|
||||
vi.mocked(ProviderFactory.getProvider).mockReturnValue(mockProvider as any);
|
||||
|
||||
vi.mocked(spawnSync).mockImplementation((cmd: any) => {
|
||||
if (cmd === 'gh') return { status: 0, stdout: Buffer.from('test-branch\n') } as any;
|
||||
return { status: 0, stdout: Buffer.from('') } as any;
|
||||
});
|
||||
|
||||
vi.mocked(spawn).mockImplementation(() => {
|
||||
return {
|
||||
stdout: { pipe: vi.fn(), on: vi.fn() },
|
||||
stderr: { pipe: vi.fn(), on: vi.fn() },
|
||||
on: vi.fn((event, cb) => { if (event === 'close') cb(0); }),
|
||||
pid: 1234
|
||||
} as any;
|
||||
});
|
||||
|
||||
vi.spyOn(process, 'chdir').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
describe('Implement Playbook', () => {
|
||||
it('should create a branch and run research/implementation', async () => {
|
||||
await runOrchestrator(['456', 'implement'], {});
|
||||
|
||||
expect(mockProvider.exec).toHaveBeenCalledWith(expect.stringContaining('git worktree add'), expect.any(Object));
|
||||
expect(mockProvider.exec).toHaveBeenCalledWith(expect.stringContaining('tmux new-session'), expect.any(Object));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Fix Playbook', () => {
|
||||
it('should launch the agentic fix-pr skill', async () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
await runWorker(['123', 'test-branch', '/path/policy', 'fix']);
|
||||
|
||||
const spawnSyncCalls = vi.mocked(spawnSync).mock.calls;
|
||||
const fixCall = spawnSyncCalls.find(call =>
|
||||
JSON.stringify(call).includes("activate the 'fix-pr' skill")
|
||||
);
|
||||
expect(fixCall).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,89 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { spawnSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import readline from 'readline';
|
||||
import { runOrchestrator } from '../scripts/orchestrator.ts';
|
||||
import { runSetup } from '../scripts/setup.ts';
|
||||
import { ProviderFactory } from '../scripts/providers/ProviderFactory.ts';
|
||||
|
||||
vi.mock('child_process');
|
||||
vi.mock('fs');
|
||||
vi.mock('readline');
|
||||
vi.mock('../scripts/providers/ProviderFactory.ts');
|
||||
|
||||
describe('Workspace Orchestration (Refactored)', () => {
|
||||
const mockSettings = {
|
||||
maintainer: {
|
||||
workspace: {
|
||||
projectId: 'test-project',
|
||||
zone: 'us-west1-a',
|
||||
remoteWorkDir: '/home/node/dev/main'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const mockProvider = {
|
||||
provision: vi.fn().mockResolvedValue(0),
|
||||
ensureReady: vi.fn().mockResolvedValue(0),
|
||||
setup: vi.fn().mockResolvedValue(0),
|
||||
exec: vi.fn().mockResolvedValue(0),
|
||||
getExecOutput: vi.fn().mockResolvedValue({ status: 0, stdout: '', stderr: '' }),
|
||||
sync: vi.fn().mockResolvedValue(0),
|
||||
getStatus: vi.fn().mockResolvedValue({ name: 'test-instance', status: 'RUNNING' }),
|
||||
stop: vi.fn().mockResolvedValue(0)
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockSettings));
|
||||
vi.mocked(fs.writeFileSync).mockReturnValue(undefined as any);
|
||||
|
||||
// Explicitly set the mock return value for each test
|
||||
vi.mocked(ProviderFactory.getProvider).mockReturnValue(mockProvider as any);
|
||||
|
||||
vi.mocked(spawnSync).mockImplementation((cmd: any) => {
|
||||
if (cmd === 'gh') return { status: 0, stdout: Buffer.from('test-branch\n') } as any;
|
||||
return { status: 0, stdout: Buffer.from('') } as any;
|
||||
});
|
||||
|
||||
vi.spyOn(process, 'chdir').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
describe('orchestrator.ts', () => {
|
||||
it('should wake the worker and execute remote commands', async () => {
|
||||
await runOrchestrator(['123'], { USER: 'testuser' });
|
||||
|
||||
expect(mockProvider.ensureReady).toHaveBeenCalled();
|
||||
expect(mockProvider.exec).toHaveBeenCalledWith(expect.stringContaining('git worktree add'), expect.any(Object));
|
||||
});
|
||||
});
|
||||
|
||||
describe('setup.ts', () => {
|
||||
const mockInterface = {
|
||||
question: vi.fn(),
|
||||
close: vi.fn()
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(readline.createInterface).mockReturnValue(mockInterface as any);
|
||||
});
|
||||
|
||||
it('should use the provider to configure SSH and sync scripts', async () => {
|
||||
mockInterface.question
|
||||
.mockImplementationOnce((q, cb) => cb('test-project'))
|
||||
.mockImplementationOnce((q, cb) => cb('us-west1-a'))
|
||||
.mockImplementationOnce((q, cb) => cb('.internal')) // dnsSuffix
|
||||
.mockImplementationOnce((q, cb) => cb('n')) // sync auth
|
||||
.mockImplementationOnce((q, cb) => cb('n')) // scoped token
|
||||
.mockImplementationOnce((q, cb) => cb('n')); // clone
|
||||
|
||||
// Ensure mockProvider is returned
|
||||
vi.mocked(ProviderFactory.getProvider).mockReturnValue(mockProvider as any);
|
||||
|
||||
await runSetup({ USER: 'testuser' });
|
||||
|
||||
expect(mockProvider.setup).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { spawnSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import { runFixPlaybook } from '../../scripts/playbooks/fix.ts';
|
||||
|
||||
vi.mock('child_process');
|
||||
vi.mock('fs');
|
||||
|
||||
describe('Fix Playbook', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(spawnSync).mockReturnValue({ status: 0 } as any);
|
||||
});
|
||||
|
||||
it('should launch the agentic fix-pr skill via spawnSync', async () => {
|
||||
const status = await runFixPlaybook('123', '/tmp/target', '/path/policy', '/path/gemini');
|
||||
|
||||
expect(status).toBe(0);
|
||||
const spawnCalls = vi.mocked(spawnSync).mock.calls;
|
||||
|
||||
expect(spawnCalls.some(c => JSON.stringify(c).includes("activate the 'fix-pr' skill"))).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,33 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { spawnSync, spawn } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import { runReadyPlaybook } from '../../scripts/playbooks/ready.ts';
|
||||
|
||||
vi.mock('child_process');
|
||||
vi.mock('fs');
|
||||
|
||||
describe('Ready Playbook', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(fs.mkdirSync).mockReturnValue(undefined as any);
|
||||
vi.mocked(fs.writeFileSync).mockReturnValue(undefined as any);
|
||||
vi.mocked(fs.createWriteStream).mockReturnValue({ pipe: vi.fn() } as any);
|
||||
|
||||
vi.mocked(spawn).mockImplementation(() => {
|
||||
return {
|
||||
stdout: { pipe: vi.fn(), on: vi.fn() },
|
||||
stderr: { pipe: vi.fn(), on: vi.fn() },
|
||||
on: vi.fn((event, cb) => { if (event === 'close') cb(0); })
|
||||
} as any;
|
||||
});
|
||||
});
|
||||
|
||||
it('should register and run clean, preflight, and conflict checks', async () => {
|
||||
runReadyPlaybook('123', '/tmp/target', '/path/policy', '/path/gemini');
|
||||
|
||||
const spawnCalls = vi.mocked(spawn).mock.calls;
|
||||
|
||||
expect(spawnCalls.some(c => c[0].includes('npm run clean'))).toBe(true);
|
||||
expect(spawnCalls.some(c => c[0].includes('git fetch origin main'))).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,36 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { spawn } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import { runReviewPlaybook } from '../../scripts/playbooks/review.ts';
|
||||
|
||||
vi.mock('child_process');
|
||||
vi.mock('fs');
|
||||
|
||||
describe('Review Playbook', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(fs.mkdirSync).mockReturnValue(undefined as any);
|
||||
vi.mocked(fs.writeFileSync).mockReturnValue(undefined as any);
|
||||
vi.mocked(fs.createWriteStream).mockReturnValue({ pipe: vi.fn() } as any);
|
||||
|
||||
vi.mocked(spawn).mockImplementation(() => {
|
||||
return {
|
||||
stdout: { pipe: vi.fn(), on: vi.fn() },
|
||||
stderr: { pipe: vi.fn(), on: vi.fn() },
|
||||
on: vi.fn((event, cb) => { if (event === 'close') cb(0); })
|
||||
} as any;
|
||||
});
|
||||
});
|
||||
|
||||
it('should register and run build, ci, and review tasks', async () => {
|
||||
// We don't await because TaskRunner uses setInterval and we'd need to mock timers
|
||||
// but we can check if spawn was called with the right commands.
|
||||
runReviewPlaybook('123', '/tmp/target', '/path/policy', '/path/gemini');
|
||||
|
||||
const spawnCalls = vi.mocked(spawn).mock.calls;
|
||||
|
||||
expect(spawnCalls.some(c => c[0].includes('npm ci'))).toBe(true);
|
||||
expect(spawnCalls.some(c => c[0].includes('gh pr checks'))).toBe(true);
|
||||
expect(spawnCalls.some(c => c[0].includes("activate the 'review-pr' skill"))).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,64 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { spawnSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { GceCosProvider } from '../scripts/providers/GceCosProvider.ts';
|
||||
|
||||
vi.mock('child_process');
|
||||
vi.mock('fs');
|
||||
|
||||
describe('GceCosProvider', () => {
|
||||
const mockConfig = {
|
||||
projectId: 'test-project',
|
||||
zone: 'us-west1-a',
|
||||
instanceName: 'test-instance',
|
||||
repoRoot: '/test-root'
|
||||
};
|
||||
|
||||
let provider: GceCosProvider;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
provider = new GceCosProvider(mockConfig.projectId, mockConfig.zone, mockConfig.instanceName, mockConfig.repoRoot);
|
||||
|
||||
vi.mocked(spawnSync).mockReturnValue({ status: 0, stdout: Buffer.from(''), stderr: Buffer.from('') } as any);
|
||||
});
|
||||
|
||||
it('should provision an instance with COS image and startup script', async () => {
|
||||
await provider.provision();
|
||||
|
||||
const calls = vi.mocked(spawnSync).mock.calls;
|
||||
const createCall = calls.find(c => c[1].includes('create'));
|
||||
|
||||
expect(createCall).toBeDefined();
|
||||
expect(createCall![1]).toContain('cos-stable');
|
||||
expect(createCall![1]).toContain('test-instance');
|
||||
});
|
||||
|
||||
it('should attempt direct SSH and fallback to IAP on failure', async () => {
|
||||
// Fail direct SSH
|
||||
vi.mocked(spawnSync)
|
||||
.mockReturnValueOnce({ status: 1, stdout: Buffer.from(''), stderr: Buffer.from('fail') } as any) // direct
|
||||
.mockReturnValueOnce({ status: 0, stdout: Buffer.from('ok'), stderr: Buffer.from('') } as any); // IAP
|
||||
|
||||
const result = await provider.exec('echo 1');
|
||||
|
||||
expect(result).toBe(0);
|
||||
const calls = vi.mocked(spawnSync).mock.calls;
|
||||
expect(calls[0][0]).toBe('ssh');
|
||||
expect(calls[1][1]).toContain('--tunnel-through-iap');
|
||||
});
|
||||
|
||||
it('should sync files with IAP fallback', async () => {
|
||||
// Fail direct rsync
|
||||
vi.mocked(spawnSync)
|
||||
.mockReturnValueOnce({ status: 1 } as any) // direct
|
||||
.mockReturnValueOnce({ status: 0 } as any); // IAP
|
||||
|
||||
await provider.sync('./local', '/remote');
|
||||
|
||||
const calls = vi.mocked(spawnSync).mock.calls;
|
||||
expect(calls[0][0]).toBe('rsync');
|
||||
expect(calls[1][1]).toContain('gcloud compute ssh --project test-project --zone us-west1-a --tunnel-through-iap --quiet');
|
||||
});
|
||||
});
|
||||
@@ -301,8 +301,8 @@ and full builds) to a dedicated, high-performance GCP worker.
|
||||
await provider.exec(`sudo chmod -R 777 ${workspaceRoot}`);
|
||||
|
||||
// 1. Sync Scripts & Policies
|
||||
await provider.sync('.gemini/skills/workspaces/scripts/', `${persistentScripts}/`, { delete: true, sudo: true });
|
||||
await provider.sync('.gemini/skills/workspaces/policy.toml', `${workspaceRoot}/policies/workspace-policy.toml`, { sudo: true });
|
||||
await provider.sync('extensions/workspaces/scripts/', `${persistentScripts}/`, { delete: true, sudo: true });
|
||||
await provider.sync('extensions/workspaces/policies/workspace-policy.toml', `${workspaceRoot}/policies/workspace-policy.toml`, { sudo: true });
|
||||
|
||||
// 2. Initialize Remote Gemini Config with Auth
|
||||
console.log('⚙️ Initializing remote Gemini configuration...');
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: workspaces
|
||||
description: Expertise in managing and utilizing Gemini Workspaces for high-performance remote development tasks.
|
||||
---
|
||||
|
||||
# Gemini Workspaces Skill
|
||||
|
||||
This skill enables the agent to utilize **Gemini Workspaces**—a high-performance, persistent remote development platform. It allows the agent to move intensive tasks (PR reviews, complex repairs, full builds) from the local environment to a dedicated cloud worker.
|
||||
|
||||
## 🛠️ Key Capabilities
|
||||
1. **Persistent Execution**: Jobs run in remote `tmux` sessions. Disconnecting or crashing the local terminal does not stop the remote work.
|
||||
2. **Parallel Infrastructure**: The agent can launch a heavy task (like a full build or CI run) in a workspace while continuing to assist the user locally.
|
||||
3. **Behavioral Fidelity**: Remote workers have full tool access (Git, Node, Docker, etc.) and high-performance compute, allowing the agent to provide behavioral proofs of its work.
|
||||
|
||||
## 📋 Instructions for the Agent
|
||||
|
||||
### When to use Workspaces
|
||||
- **Intensive Tasks**: Full preflight runs, large-scale refactors, or deep PR reviews.
|
||||
- **Persistent Logic**: When a task is expected to take longer than a few minutes and needs to survive local connection drops.
|
||||
- **Environment Isolation**: When you need a clean, high-performance environment to verify a fix without polluting the user's local machine.
|
||||
|
||||
### How to use Workspaces
|
||||
1. **Setup**: If the user hasn't initialized their environment, instruct them to run `npm run workspace:setup`.
|
||||
2. **Launch**: Use the `workspace` command to start a playbook:
|
||||
```bash
|
||||
npm run workspace <PR_NUMBER> [action]
|
||||
```
|
||||
- Actions: `review` (default), `fix`, `ready`.
|
||||
3. **Check Status**: See global state and active sessions with `npm run workspace:status`, or deep-dive into specific PR logs with `npm run workspace:check <PR_NUMBER>`.
|
||||
4. **Cleanup**:
|
||||
- **Bulk**: Clear all sessions/worktrees with `npm run workspace:clean-all`.
|
||||
- **Surgical**: Kill a specific PR task with `npm run workspace:kill <PR_NUMBER> <action>`.
|
||||
5. **Fleet**: Manage VM lifecycle with `npm run workspace:fleet [stop|provision|list]`.
|
||||
|
||||
## ⚠️ Important Constraints
|
||||
- **Absolute Paths**: Always use absolute paths (e.g., `/mnt/disks/data/...`) when orchestrating remote commands.
|
||||
- **npx tsx**: When running scripts manually from the skill directory, always prefix with `npx tsx` to ensure dependencies are available.
|
||||
- **Be Behavioral**: Prioritize results from live execution (behavioral proofs) over static reading.
|
||||
- **Multi-tasking**: Remind the user they can continue chatting in the main window while the heavy workspace task runs in the separate terminal window.
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "workspaces",
|
||||
"version": "0.1.0",
|
||||
"description": "High-performance remote development workspaces for Gemini CLI.",
|
||||
"author": "Google Gemini Team",
|
||||
"license": "Apache-2.0",
|
||||
"skills": [
|
||||
"extensions/workspaces/skills/SKILL.md"
|
||||
],
|
||||
"contextFileName": "extensions/workspaces/docs/GEMINI.md"
|
||||
}
|
||||
+53
-55
@@ -1,79 +1,77 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Unified Workspaces Entry Point (Local)
|
||||
*
|
||||
* Central CLI for managing Gemini Workspaces.
|
||||
* Usage: scripts/workspaces.ts <command> [args]
|
||||
*/
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { spawnSync } from 'child_process';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const REPO_ROOT = path.resolve(__dirname, '..');
|
||||
|
||||
const commands: Record<string, string> = {
|
||||
setup: '.gemini/skills/workspaces/scripts/setup.ts',
|
||||
shell: '.gemini/skills/workspaces/scripts/orchestrator.ts shell',
|
||||
check: '.gemini/skills/workspaces/scripts/check.ts',
|
||||
'clean-all': '.gemini/skills/workspaces/scripts/clean.ts',
|
||||
kill: '.gemini/skills/workspaces/scripts/clean.ts',
|
||||
fleet: '.gemini/skills/workspaces/scripts/fleet.ts',
|
||||
status: '.gemini/skills/workspaces/scripts/status.ts',
|
||||
attach: '.gemini/skills/workspaces/scripts/attach.ts',
|
||||
logs: '.gemini/skills/workspaces/scripts/logs.ts',
|
||||
'setup': 'extensions/workspaces/scripts/setup.ts',
|
||||
'shell': 'extensions/workspaces/scripts/orchestrator.ts shell',
|
||||
'check': 'extensions/workspaces/scripts/check.ts',
|
||||
'clean-all': 'extensions/workspaces/scripts/clean.ts',
|
||||
'kill': 'extensions/workspaces/scripts/clean.ts',
|
||||
'fleet': 'extensions/workspaces/scripts/fleet.ts',
|
||||
'status': 'extensions/workspaces/scripts/status.ts',
|
||||
'attach': 'extensions/workspaces/scripts/attach.ts',
|
||||
'logs': 'extensions/workspaces/scripts/logs.ts',
|
||||
};
|
||||
|
||||
function printUsage() {
|
||||
console.log('Gemini Workspaces Management CLI');
|
||||
console.log(
|
||||
'\nUsage: scripts/workspaces.ts <command> [args] [--open foreground|tab|window]',
|
||||
);
|
||||
console.log('\nCommands:');
|
||||
console.log(
|
||||
' setup Initialize or reconfigure your remote worker',
|
||||
);
|
||||
console.log(' <pr-number> [action] Launch a PR task (review, fix, ready)');
|
||||
console.log(' shell [id] Open an ad-hoc interactive session');
|
||||
console.log(' status See worker and session overview');
|
||||
console.log(' check <pr-number> Deep-dive into PR logs');
|
||||
console.log(' kill <pr-number> <act> Surgical removal of a task');
|
||||
console.log(' clean-all Full remote cleanup');
|
||||
console.log(' fleet <action> Manage VM life cycle (stop, provision)');
|
||||
process.exit(1);
|
||||
console.log('Gemini Workspaces Management CLI');
|
||||
console.log('\nUsage: scripts/workspaces.ts <command> [args] [--open foreground|tab|window]');
|
||||
console.log('\nCommands:');
|
||||
console.log(' setup Initialize or reconfigure your remote worker');
|
||||
console.log(' <pr-number> [action] Launch a PR task (review, fix, ready)');
|
||||
console.log(' shell [id] Open an ad-hoc interactive session');
|
||||
console.log(' status See worker and session overview');
|
||||
console.log(' check <pr-number> Deep-dive into PR logs');
|
||||
console.log(' kill <pr-number> <act> Surgical removal of a task');
|
||||
console.log(' clean-all Full remote cleanup');
|
||||
console.log(' fleet <action> Manage VM life cycle (stop, provision)');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const cmd = args[0];
|
||||
const args = process.argv.slice(2);
|
||||
const cmd = args[0];
|
||||
|
||||
if (!cmd || cmd === '--help' || cmd === '-h') {
|
||||
printUsage();
|
||||
}
|
||||
if (!cmd || cmd === '--help' || cmd === '-h') {
|
||||
printUsage();
|
||||
}
|
||||
|
||||
let scriptPath = commands[cmd];
|
||||
let finalArgs = args.slice(1);
|
||||
let scriptPath = commands[cmd];
|
||||
let finalArgs = args.slice(1);
|
||||
|
||||
// Default: If it's a number, it's a PR orchestrator task
|
||||
if (!scriptPath && /^\d+$/.test(cmd)) {
|
||||
scriptPath = '.gemini/skills/workspaces/scripts/orchestrator.ts';
|
||||
finalArgs = args; // Pass the PR number as the first arg
|
||||
}
|
||||
// Default: If it's a number, it's a PR orchestrator task
|
||||
if (!scriptPath && /^\d+$/.test(cmd)) {
|
||||
scriptPath = 'extensions/workspaces/scripts/orchestrator.ts';
|
||||
finalArgs = args; // Pass the PR number as the first arg
|
||||
}
|
||||
|
||||
if (!scriptPath) {
|
||||
console.error(`❌ Unknown command: ${cmd}`);
|
||||
printUsage();
|
||||
}
|
||||
if (!scriptPath) {
|
||||
console.error(`❌ Unknown command: ${cmd}`);
|
||||
printUsage();
|
||||
}
|
||||
|
||||
const [realScript, ...internalArgs] = scriptPath.split(' ');
|
||||
const fullScriptPath = path.join(REPO_ROOT, realScript);
|
||||
const [realScript, ...internalArgs] = scriptPath.split(' ');
|
||||
const fullScriptPath = path.join(REPO_ROOT, realScript);
|
||||
|
||||
const result = spawnSync(
|
||||
'npx',
|
||||
['tsx', fullScriptPath, ...internalArgs, ...finalArgs],
|
||||
{ stdio: 'inherit' },
|
||||
);
|
||||
const result = spawnSync('npx', [
|
||||
'tsx',
|
||||
fullScriptPath,
|
||||
...internalArgs,
|
||||
...finalArgs
|
||||
], { stdio: 'inherit' });
|
||||
|
||||
process.exit(result.status ?? 0);
|
||||
process.exit(result.status ?? 0);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
|
||||
Reference in New Issue
Block a user