mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-24 13:01:29 -07:00
feat(workspaces): implement workspace container image and initial hub api
This commit is contained in:
50
packages/workspace-manager/docker/Dockerfile
Normal file
50
packages/workspace-manager/docker/Dockerfile
Normal file
@@ -0,0 +1,50 @@
|
||||
# Copyright 2026 Google LLC
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
FROM node:20-slim
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git \
|
||||
curl \
|
||||
rsync \
|
||||
vim \
|
||||
tmux \
|
||||
procps \
|
||||
build-essential \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install GitHub CLI
|
||||
RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \
|
||||
&& chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg \
|
||||
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
|
||||
&& apt-get update \
|
||||
&& apt-get install gh -y
|
||||
|
||||
# Install Rust (to install shpool)
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
|
||||
# Install shpool
|
||||
RUN cargo install shpool \
|
||||
&& mv /root/.cargo/bin/shpool /usr/local/bin/shpool
|
||||
|
||||
# Install global dev tools
|
||||
RUN npm install -g tsx eslint vitest typescript prettier @google/gemini-cli@nightly
|
||||
|
||||
# Create workspace directory
|
||||
WORKDIR /home/node/workspace
|
||||
RUN chown -R node:node /home/node/workspace
|
||||
|
||||
# Entrypoint script
|
||||
COPY --chown=node:node entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
||||
USER node
|
||||
|
||||
# Environment variables
|
||||
ENV GEMINI_CLI_WORKSPACE=1
|
||||
ENV PATH=$PATH:/usr/local/bin
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||
CMD ["/bin/bash"]
|
||||
21
packages/workspace-manager/docker/entrypoint.sh
Normal file
21
packages/workspace-manager/docker/entrypoint.sh
Normal file
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
# Copyright 2026 Google LLC
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# Ensure GH_TOKEN is set from memory-only mount if available
|
||||
if [ -f /dev/shm/.gh_token ]; then
|
||||
export GH_TOKEN=$(cat /dev/shm/.gh_token)
|
||||
echo "GitHub token injected from memory."
|
||||
fi
|
||||
|
||||
# Start shpool daemon in the background
|
||||
/usr/local/bin/shpool daemon &
|
||||
|
||||
# Restore ~/.gemini settings if they are provided in a mount or PD
|
||||
# (Assuming PD is mounted at /home/node/persistent_home for now)
|
||||
if [ -d /home/node/persistent_home/.gemini ]; then
|
||||
rsync -a /home/node/persistent_home/.gemini/ /home/node/.gemini/
|
||||
fi
|
||||
|
||||
# Execute the CMD passed to docker
|
||||
exec "$@"
|
||||
27
packages/workspace-manager/package.json
Normal file
27
packages/workspace-manager/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-workspace-manager",
|
||||
"version": "0.1.0",
|
||||
"description": "Workspace Hub for Gemini CLI",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"start": "node dist/index.js",
|
||||
"dev": "tsx src/index.ts",
|
||||
"test": "vitest run",
|
||||
"build": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google-cloud/compute": "^4.10.0",
|
||||
"@google-cloud/firestore": "^7.11.0",
|
||||
"express": "^4.21.2",
|
||||
"uuid": "^13.0.0",
|
||||
"zod": "^3.25.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"typescript": "^5.7.0",
|
||||
"vitest": "^3.2.0"
|
||||
}
|
||||
}
|
||||
125
packages/workspace-manager/src/index.ts
Normal file
125
packages/workspace-manager/src/index.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
import type { Request, Response, RequestHandler } from 'express';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Firestore } from '@google-cloud/firestore';
|
||||
|
||||
interface WorkspaceData {
|
||||
owner_id: string;
|
||||
name: string;
|
||||
instance_name: string;
|
||||
status: string;
|
||||
machine_type: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// Initialize Firestore
|
||||
const firestore = new Firestore();
|
||||
|
||||
const PORT = process.env.PORT || 8080;
|
||||
|
||||
app.get('/health', (_req: Request, res: Response) => {
|
||||
res.send({ status: 'ok' });
|
||||
});
|
||||
|
||||
/**
|
||||
* List all workspaces for the authenticated user
|
||||
*/
|
||||
const listWorkspaces: RequestHandler = async (_req, res) => {
|
||||
try {
|
||||
const ownerId = 'default-user'; // TODO: Get from OAuth/IAP headers
|
||||
const snapshot = await firestore
|
||||
.collection('workspaces')
|
||||
.where('owner_id', '==', ownerId)
|
||||
.get();
|
||||
|
||||
const workspaces = snapshot.docs.map((doc) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const data = doc.data() as WorkspaceData;
|
||||
return {
|
||||
id: doc.id,
|
||||
...data,
|
||||
};
|
||||
});
|
||||
|
||||
res.json(workspaces);
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
res.status(500).json({ error: message });
|
||||
}
|
||||
};
|
||||
|
||||
app.get('/workspaces', listWorkspaces);
|
||||
|
||||
/**
|
||||
* Create a new workspace (GCE VM)
|
||||
*/
|
||||
const createWorkspace: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||
const body = req.body as Record<string, unknown>;
|
||||
const name = typeof body['name'] === 'string' ? body['name'] : 'unnamed';
|
||||
const machineType =
|
||||
typeof body['machineType'] === 'string'
|
||||
? body['machineType']
|
||||
: 'e2-standard-4';
|
||||
|
||||
const ownerId = 'default-user'; // TODO: Get from OAuth/IAP headers
|
||||
const workspaceId = uuidv4();
|
||||
const instanceName = `workspace-${workspaceId.slice(0, 8)}`;
|
||||
|
||||
const workspaceData: WorkspaceData = {
|
||||
owner_id: ownerId,
|
||||
name,
|
||||
instance_name: instanceName,
|
||||
status: 'PROVISIONING',
|
||||
machine_type: machineType,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
await firestore
|
||||
.collection('workspaces')
|
||||
.doc(workspaceId)
|
||||
.set(workspaceData);
|
||||
|
||||
res.status(201).json({ id: workspaceId, ...workspaceData });
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
res.status(500).json({ error: message });
|
||||
}
|
||||
};
|
||||
|
||||
app.post('/workspaces', createWorkspace);
|
||||
|
||||
/**
|
||||
* Delete a workspace
|
||||
*/
|
||||
const deleteWorkspace: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
if (!id) {
|
||||
res.status(400).json({ error: 'Workspace ID is required' });
|
||||
return;
|
||||
}
|
||||
await firestore.collection('workspaces').doc(id).delete();
|
||||
res.status(204).send();
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
res.status(500).json({ error: message });
|
||||
}
|
||||
};
|
||||
|
||||
app.delete('/workspaces/:id', deleteWorkspace);
|
||||
|
||||
app.listen(PORT, () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Workspace Hub listening on port ${PORT}`);
|
||||
});
|
||||
10
packages/workspace-manager/tsconfig.json
Normal file
10
packages/workspace-manager/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"composite": false
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "**/*.test.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user