mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-09 01:27:41 -07:00
feat(workspaces): implement wsr command group and hub configuration
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
// File for 'gemini wsr' command
|
||||
import type { CommandModule, Argv } from 'yargs';
|
||||
import { listCommand } from './workspace/list.js';
|
||||
import { createCommand } from './workspace/create.js';
|
||||
import { deleteCommand } from './workspace/delete.js';
|
||||
import { defer } from '../deferred.js';
|
||||
|
||||
export const remoteWorkspaceCommand: CommandModule = {
|
||||
command: 'wsr',
|
||||
describe: 'Manage remote workspaces',
|
||||
builder: (yargs: Argv) =>
|
||||
yargs
|
||||
.command(defer(listCommand, 'wsr'))
|
||||
.command(defer(createCommand, 'wsr'))
|
||||
.command(defer(deleteCommand, 'wsr'))
|
||||
.demandCommand(1, 'You need at least one command before continuing.')
|
||||
.version(false),
|
||||
|
||||
handler: () => {
|
||||
// yargs will automatically show help if no subcommand is provided
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { CommandModule, ArgumentsCamelCase } from 'yargs';
|
||||
import {
|
||||
createWorkspace as performCreateWorkspace,
|
||||
type Config,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { exitCli } from '../utils.js';
|
||||
import chalk from 'chalk';
|
||||
|
||||
interface CreateArgs {
|
||||
config?: Config;
|
||||
name: string;
|
||||
machineType?: string;
|
||||
}
|
||||
|
||||
export async function createWorkspace(
|
||||
args: ArgumentsCamelCase<CreateArgs>,
|
||||
): Promise<void> {
|
||||
if (!args.config) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(chalk.red('Internal error: Config not loaded.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
chalk.yellow(`Requesting creation of workspace "${args.name}"...`),
|
||||
);
|
||||
|
||||
const result = await performCreateWorkspace(
|
||||
args.config,
|
||||
args.name,
|
||||
args.machineType,
|
||||
);
|
||||
|
||||
if (result.type === 'message') {
|
||||
if (result.messageType === 'error') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(chalk.red(result.content));
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(chalk.green(result.content));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const createCommand: CommandModule<object, CreateArgs> = {
|
||||
command: 'create <name>',
|
||||
describe: 'Create a new remote workspace',
|
||||
builder: (yargs) => yargs
|
||||
.positional('name', {
|
||||
type: 'string',
|
||||
describe: 'Name of the workspace',
|
||||
demandOption: true,
|
||||
})
|
||||
.option('machine-type', {
|
||||
type: 'string',
|
||||
describe: 'GCE machine type',
|
||||
}),
|
||||
handler: async (argv) => {
|
||||
await createWorkspace(argv);
|
||||
await exitCli();
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { CommandModule, ArgumentsCamelCase } from 'yargs';
|
||||
import {
|
||||
deleteWorkspace as performDeleteWorkspace,
|
||||
type Config,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { exitCli } from '../utils.js';
|
||||
import chalk from 'chalk';
|
||||
|
||||
interface DeleteArgs {
|
||||
config?: Config;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export async function deleteWorkspace(
|
||||
args: ArgumentsCamelCase<DeleteArgs>,
|
||||
): Promise<void> {
|
||||
if (!args.config) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(chalk.red('Internal error: Config not loaded.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(chalk.yellow(`Deleting workspace "${args.id}"...`));
|
||||
|
||||
const result = await performDeleteWorkspace(args.config, args.id);
|
||||
|
||||
if (result.type === 'message') {
|
||||
if (result.messageType === 'error') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(chalk.red(result.content));
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(chalk.green(result.content));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteCommand: CommandModule<object, DeleteArgs> = {
|
||||
command: 'delete <id>',
|
||||
describe: 'Delete a remote workspace',
|
||||
builder: (yargs) => yargs.positional('id', {
|
||||
type: 'string',
|
||||
describe: 'ID of the workspace to delete',
|
||||
demandOption: true,
|
||||
}),
|
||||
handler: async (argv) => {
|
||||
await deleteWorkspace(argv);
|
||||
await exitCli();
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { CommandModule, ArgumentsCamelCase } from 'yargs';
|
||||
import {
|
||||
listWorkspaces as performListWorkspaces,
|
||||
type Config,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { exitCli } from '../utils.js';
|
||||
import chalk from 'chalk';
|
||||
|
||||
interface ListArgs {
|
||||
config?: Config;
|
||||
}
|
||||
|
||||
export async function listWorkspaces(
|
||||
args: ArgumentsCamelCase<ListArgs>,
|
||||
): Promise<void> {
|
||||
if (!args.config) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(chalk.red('Internal error: Config not loaded.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await performListWorkspaces(args.config);
|
||||
|
||||
if (result.type === 'message') {
|
||||
if (result.messageType === 'error') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(chalk.red(result.content));
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(result.content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const listCommand: CommandModule<object, ListArgs> = {
|
||||
command: 'list',
|
||||
describe: 'List all remote workspaces',
|
||||
handler: async (argv) => {
|
||||
await listWorkspaces(argv);
|
||||
await exitCli();
|
||||
},
|
||||
};
|
||||
@@ -671,6 +671,12 @@ describe('parseArguments', () => {
|
||||
const argv = await parseArguments(settings);
|
||||
expect(argv.isCommand).toBe(true);
|
||||
});
|
||||
|
||||
it('should set isCommand to true for workspace command', async () => {
|
||||
process.argv = ['node', 'script.js', 'workspace', 'list'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
expect(argv.isCommand).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadCliConfig', () => {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { hideBin } from 'yargs/helpers';
|
||||
import process from 'node:process';
|
||||
import * as path from 'node:path';
|
||||
import { mcpCommand } from '../commands/mcp.js';
|
||||
import { remoteWorkspaceCommand as workspaceCommand } from '../commands/workspace.js';
|
||||
import { extensionsCommand } from '../commands/extensions.js';
|
||||
import { skillsCommand } from '../commands/skills.js';
|
||||
import { hooksCommand } from '../commands/hooks.js';
|
||||
@@ -131,6 +132,7 @@ export async function parseArguments(
|
||||
description: 'Run in debug mode (open debug console with F12)',
|
||||
default: false,
|
||||
})
|
||||
.command(workspaceCommand)
|
||||
.command('$0 [query..]', 'Launch Gemini CLI', (yargsInstance) =>
|
||||
yargsInstance
|
||||
.positional('query', {
|
||||
@@ -776,6 +778,7 @@ export async function loadCliConfig(
|
||||
toolCallCommand: settings.tools?.callCommand,
|
||||
mcpServerCommand,
|
||||
mcpServers,
|
||||
workspaces: settings.workspaces,
|
||||
mcpEnablementCallbacks,
|
||||
mcpEnabled,
|
||||
extensionsEnabled,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
|
||||
DEFAULT_MODEL_CONFIGS,
|
||||
type MCPServerConfig,
|
||||
type WorkspaceConfig,
|
||||
type BugCommandSettings,
|
||||
type TelemetrySettings,
|
||||
type AuthType,
|
||||
@@ -169,6 +170,49 @@ const SETTINGS_SCHEMA = {
|
||||
},
|
||||
},
|
||||
|
||||
workspaces: {
|
||||
type: 'object',
|
||||
label: 'Workspaces',
|
||||
category: 'Advanced',
|
||||
requiresRestart: false,
|
||||
default: { hubs: {} } as WorkspaceConfig,
|
||||
description: 'Configuration for remote workspaces.',
|
||||
showInDialog: false,
|
||||
mergeStrategy: MergeStrategy.SHALLOW_MERGE,
|
||||
properties: {
|
||||
hubs: {
|
||||
type: 'object',
|
||||
label: 'Workspace Hubs',
|
||||
category: 'Advanced',
|
||||
requiresRestart: false,
|
||||
default: {},
|
||||
description: 'Configured Workspace Hubs.',
|
||||
mergeStrategy: MergeStrategy.SHALLOW_MERGE,
|
||||
additionalProperties: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: {
|
||||
type: 'string',
|
||||
label: 'Hub URL',
|
||||
category: 'Advanced',
|
||||
requiresRestart: false,
|
||||
default: 'http://localhost:8080',
|
||||
description: 'The URL of the Workspace Hub.',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultHub: {
|
||||
type: 'string',
|
||||
label: 'Default Hub',
|
||||
category: 'Advanced',
|
||||
requiresRestart: false,
|
||||
default: undefined as string | undefined,
|
||||
description: 'The name of the default Workspace Hub to use.',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
policyPaths: pathArraySetting(
|
||||
'Policy Paths',
|
||||
'Additional policy files or directories to load.',
|
||||
|
||||
@@ -63,6 +63,18 @@ export async function runDeferredCommand(settings: MergedSettings) {
|
||||
process.exit(ExitCodes.FATAL_CONFIG_ERROR);
|
||||
}
|
||||
|
||||
if (commandName === 'wsr') {
|
||||
// Inject settings into argv
|
||||
const argvWithSettings = {
|
||||
...deferredCommand.argv,
|
||||
settings,
|
||||
};
|
||||
|
||||
await deferredCommand.handler(argvWithSettings);
|
||||
await runExitCleanup();
|
||||
process.exit(ExitCodes.SUCCESS);
|
||||
}
|
||||
|
||||
// Inject settings into argv
|
||||
const argvWithSettings = {
|
||||
...deferredCommand.argv,
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { WorkspaceHubClient } from '@google/gemini-cli-core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
export async function runRemoteCommand(args: string[]): Promise<void> {
|
||||
const command = args[0];
|
||||
const hubUrl =
|
||||
process.env['GEMINI_WORKSPACE_HUB_URL'] || 'http://localhost:8080';
|
||||
const client = new WorkspaceHubClient(hubUrl);
|
||||
|
||||
try {
|
||||
if (command === 'list' || command === 'ls') {
|
||||
const workspaces = await client.listWorkspaces();
|
||||
if (workspaces.length === 0) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('No active workspaces found.');
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(chalk.bold('Active Workspaces:'));
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
'------------------------------------------------------------',
|
||||
);
|
||||
for (const ws of workspaces) {
|
||||
const statusColor = ws.status === 'READY' ? chalk.green : chalk.yellow;
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`${chalk.cyan(ws.name.padEnd(20))} | ${statusColor(ws.status.padEnd(12))} | ${ws.id}`,
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
'------------------------------------------------------------',
|
||||
);
|
||||
} else if (command === 'create') {
|
||||
const name = args[1];
|
||||
if (!name) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
chalk.red(
|
||||
'Error: Workspace name is required. Usage: wsr create <name>',
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
chalk.yellow(`Requesting creation of workspace "${name}"...`),
|
||||
);
|
||||
const ws = await client.createWorkspace(name);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(chalk.green(`✅ Workspace created successfully!`));
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`${chalk.bold('ID:')} ${ws.id}`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`${chalk.bold('Name:')} ${ws.name}`);
|
||||
} else if (command === 'delete' || command === 'rm') {
|
||||
const id = args[1];
|
||||
if (!id) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
chalk.red('Error: Workspace ID is required. Usage: wsr delete <id>'),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(chalk.yellow(`Deleting workspace "${id}"...`));
|
||||
await client.deleteWorkspace(id);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(chalk.green(`✅ Workspace deleted successfully.`));
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Usage: wsr <command> [args]');
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Commands: list, create, delete');
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(chalk.red('Remote command failed:'), message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -58,6 +58,7 @@ import { skillsCommand } from '../ui/commands/skillsCommand.js';
|
||||
import { settingsCommand } from '../ui/commands/settingsCommand.js';
|
||||
import { shellsCommand } from '../ui/commands/shellsCommand.js';
|
||||
import { vimCommand } from '../ui/commands/vimCommand.js';
|
||||
import { workspaceSlashCommand } from '../ui/commands/workspaceCommand.js';
|
||||
import { setupGithubCommand } from '../ui/commands/setupGithubCommand.js';
|
||||
import { terminalSetupCommand } from '../ui/commands/terminalSetupCommand.js';
|
||||
import { upgradeCommand } from '../ui/commands/upgradeCommand.js';
|
||||
@@ -223,6 +224,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
|
||||
settingsCommand,
|
||||
shellsCommand,
|
||||
vimCommand,
|
||||
workspaceSlashCommand,
|
||||
setupGithubCommand,
|
||||
terminalSetupCommand,
|
||||
...(this.config?.getContentGeneratorConfig()?.authType ===
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type { SlashCommand, CommandContext } from './types.js';
|
||||
import { CommandKind } from './types.js';
|
||||
import type { MessageActionReturn } from '@google/gemini-cli-core';
|
||||
import { WorkspaceHubClient } from '@google/gemini-cli-core';
|
||||
|
||||
const listAction = async (
|
||||
|
||||
_context: CommandContext,
|
||||
): Promise<void | MessageActionReturn> => {
|
||||
const hubUrl =
|
||||
process.env['GEMINI_WORKSPACE_HUB_URL'] || 'http://localhost:8080';
|
||||
const client = new WorkspaceHubClient(hubUrl);
|
||||
|
||||
try {
|
||||
const workspaces = await client.listWorkspaces();
|
||||
|
||||
if (workspaces.length === 0) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: 'No active workspaces found.',
|
||||
};
|
||||
}
|
||||
|
||||
let content = 'Active Workspaces:\n';
|
||||
content += '------------------------------------------------------------\n';
|
||||
for (const ws of workspaces) {
|
||||
content += `${ws.name.padEnd(20)} | ${ws.status.padEnd(12)} | ${ws.id}\n`;
|
||||
}
|
||||
content += '------------------------------------------------------------';
|
||||
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content,
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const message = (error as Error).message;
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: `Failed to list workspaces: ${message}`,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const listCommand: SlashCommand = {
|
||||
name: 'list',
|
||||
altNames: ['ls'],
|
||||
description: 'List remote workspaces',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: (context) => listAction(context),
|
||||
};
|
||||
|
||||
const createCommand: SlashCommand = {
|
||||
name: 'create',
|
||||
description: 'Create a new remote workspace',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (
|
||||
context: CommandContext,
|
||||
args: string,
|
||||
): Promise<MessageActionReturn> => {
|
||||
const name = args.trim();
|
||||
if (!name) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Workspace name is required. Usage: /workspace create <name>',
|
||||
};
|
||||
}
|
||||
|
||||
const hubUrl =
|
||||
process.env['GEMINI_WORKSPACE_HUB_URL'] || 'http://localhost:8080';
|
||||
const client = new WorkspaceHubClient(hubUrl);
|
||||
|
||||
try {
|
||||
context.ui.addItem({
|
||||
type: 'info',
|
||||
text: `Requesting creation of workspace "${name}"...`,
|
||||
});
|
||||
const ws = await client.createWorkspace(name);
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: `✅ Workspace created successfully!\nID: ${ws.id}\nName: ${ws.name}\nGCE: ${ws.instance_name}`,
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const message = (error as Error).message;
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: `Failed to create workspace: ${message}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const deleteCommand: SlashCommand = {
|
||||
name: 'delete',
|
||||
altNames: ['rm'],
|
||||
description: 'Delete a remote workspace',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (
|
||||
context: CommandContext,
|
||||
args: string,
|
||||
): Promise<MessageActionReturn> => {
|
||||
const id = args.trim();
|
||||
if (!id) {
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: 'Workspace ID is required. Usage: /workspace delete <id>',
|
||||
};
|
||||
}
|
||||
|
||||
const hubUrl =
|
||||
process.env['GEMINI_WORKSPACE_HUB_URL'] || 'http://localhost:8080';
|
||||
const client = new WorkspaceHubClient(hubUrl);
|
||||
|
||||
try {
|
||||
context.ui.addItem({
|
||||
type: 'info',
|
||||
text: `Deleting workspace "${id}"...`,
|
||||
});
|
||||
await client.deleteWorkspace(id);
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
content: `✅ Workspace ${id} deleted successfully.`,
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const message = (error as Error).message;
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'error',
|
||||
content: `Failed to delete workspace: ${message}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const workspaceSlashCommand: SlashCommand = {
|
||||
name: 'workspace',
|
||||
description: 'Manage remote workspaces',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: false,
|
||||
subCommands: [listCommand, createCommand, deleteCommand],
|
||||
action: async (context: CommandContext) => listAction(context),
|
||||
};
|
||||
Reference in New Issue
Block a user