tools and skills, etc in the containers

This commit is contained in:
2026-02-24 01:24:28 +00:00
parent 071e2decc3
commit d6ffe43a11
16 changed files with 996 additions and 66 deletions
+44 -10
View File
@@ -2,7 +2,7 @@ import type { ServerWebSocket } from 'bun';
import { mkdirSync, statSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { getHomeDir } from '@@/data-path';
import { getHomeDir, getGlobalSkillsDir, getGlobalToolsDir, getGlobalExtensionsDir, getUserSkillsDir, getUserToolsDir } from '@@/data-path';
import { syncUserPiConfig } from '@@/api/server-settings/sync-user-pi-config';
import { getUsers } from 'officerdb';
@@ -101,7 +101,20 @@ const ensureDockerImage = () => {
dockerImageReady = true;
};
const startDockerSidecar = (port: number, homeDir: string, userId: number, username: string): { dockerId: string } => {
// Check whether a container already has the officer resource mounts.
// We test for the global skills dir as a proxy for all mounts being present.
const containerHasResourceMounts = (dockerId: string): boolean => {
const dockerPath = Bun.which('docker') ?? 'docker';
const result = Bun.spawnSync({
cmd: [dockerPath, 'inspect', '--format', '{{range .Mounts}}{{.Source}}\n{{end}}', dockerId],
stdout: 'pipe',
stderr: 'ignore',
});
if (result.exitCode !== 0) return false;
return result.stdout.toString().includes(getGlobalSkillsDir());
};
const startDockerSidecar = (port: number, homeDir: string, userId: number, username: string, email: string): { dockerId: string } => {
ensureDockerImage();
const dockerPath = Bun.which('docker') ?? 'docker';
const dockerId = `officer-terminal-${userId}`;
@@ -144,10 +157,13 @@ const startDockerSidecar = (port: number, homeDir: string, userId: number, usern
`TERMINAL_UID=${uid}`,
'-e',
`TERMINAL_GID=${gid}`,
'-v',
`${homeDir}:${containerHome}`,
'-w',
containerHome,
'-v', `${homeDir}:${containerHome}`,
'-v', `${getGlobalSkillsDir()}:/officer/skills:ro`,
'-v', `${getGlobalToolsDir()}:/officer/tools:ro`,
'-v', `${getGlobalExtensionsDir()}:/officer/extensions:ro`,
'-v', `${getUserSkillsDir(email)}:/officer/user/skills:ro`,
'-v', `${getUserToolsDir(email)}:/officer/user/tools:ro`,
'-w', containerHome,
tag,
],
stdout: 'inherit',
@@ -211,17 +227,35 @@ const dockerStart = (dockerId: string) => {
};
export const ensureDockerContainer = async (email: string, userId: number, homeDir: string, username: string) => {
// Ensure user-specific resource dirs exist before mounting (Docker creates them as root if missing)
mkdirSync(getUserSkillsDir(email), { recursive: true });
mkdirSync(getUserToolsDir(email), { recursive: true });
const map = await loadContainerMap();
const existing = map[email];
if (existing && dockerContainerRunning(existing.dockerId)) return existing;
if (existing && dockerContainerRunning(existing.dockerId)) {
// Recreate if resource mounts are missing (e.g. first run after feature was added)
if (!containerHasResourceMounts(existing.dockerId)) {
console.log(`[terminal] recreating container for ${email} — resource mounts missing`);
stopDockerSidecar(existing.dockerId);
} else {
return existing;
}
}
if (existing && dockerContainerExists(existing.dockerId)) {
if (dockerStart(existing.dockerId)) return existing;
stopDockerSidecar(existing.dockerId);
if (!containerHasResourceMounts(existing.dockerId)) {
stopDockerSidecar(existing.dockerId);
} else if (dockerStart(existing.dockerId)) {
return existing;
} else {
stopDockerSidecar(existing.dockerId);
}
}
const port = existing?.port ?? getAvailablePort(map, userId);
const docker = startDockerSidecar(port, homeDir, userId, username);
const docker = startDockerSidecar(port, homeDir, userId, username, email);
const next = { userId, email, dockerId: docker.dockerId, port };
map[email] = next;
await saveContainerMap(map);