fix(workspace): align clawd + bootstrap

This commit is contained in:
Peter Steinberger
2026-01-06 19:50:06 +01:00
parent bdf597eb95
commit d07e78855c
13 changed files with 105 additions and 33 deletions

View File

@@ -36,4 +36,17 @@ describe("ensureAgentWorkspace", () => {
await ensureAgentWorkspace({ dir, ensureBootstrapFiles: true });
expect(await fs.readFile(agentsPath, "utf-8")).toBe("custom");
});
it("does not recreate BOOTSTRAP.md once workspace exists", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-ws-"));
const agentsPath = path.join(dir, "AGENTS.md");
const bootstrapPath = path.join(dir, "BOOTSTRAP.md");
await fs.writeFile(agentsPath, "custom", "utf-8");
await fs.rm(bootstrapPath, { force: true });
await ensureAgentWorkspace({ dir, ensureBootstrapFiles: true });
await expect(fs.stat(bootstrapPath)).rejects.toBeDefined();
});
});

View File

@@ -222,6 +222,21 @@ export async function ensureAgentWorkspace(params?: {
const userPath = path.join(dir, DEFAULT_USER_FILENAME);
const bootstrapPath = path.join(dir, DEFAULT_BOOTSTRAP_FILENAME);
const isBrandNewWorkspace = await (async () => {
const paths = [agentsPath, soulPath, toolsPath, identityPath, userPath];
const existing = await Promise.all(
paths.map(async (p) => {
try {
await fs.access(p);
return true;
} catch {
return false;
}
}),
);
return existing.every((v) => !v);
})();
const agentsTemplate = await loadTemplate(
DEFAULT_AGENTS_FILENAME,
DEFAULT_AGENTS_TEMPLATE,
@@ -252,7 +267,9 @@ export async function ensureAgentWorkspace(params?: {
await writeFileIfMissing(toolsPath, toolsTemplate);
await writeFileIfMissing(identityPath, identityTemplate);
await writeFileIfMissing(userPath, userTemplate);
await writeFileIfMissing(bootstrapPath, bootstrapTemplate);
if (isBrandNewWorkspace) {
await writeFileIfMissing(bootstrapPath, bootstrapTemplate);
}
return {
dir,

View File

@@ -268,9 +268,9 @@ describe("doctor", () => {
parsed: {
gateway: { mode: "local", bind: "loopback" },
agent: {
workspace: "/Users/steipete/clawdbot",
workspace: "/Users/steipete/clawd",
sandbox: {
workspaceRoot: "/Users/steipete/clawdbot/sandboxes",
workspaceRoot: "/Users/steipete/clawd/sandboxes",
docker: {
image: "clawdbot-sandbox",
containerPrefix: "clawdbot-sbx",
@@ -282,9 +282,9 @@ describe("doctor", () => {
config: {
gateway: { mode: "local", bind: "loopback" },
agent: {
workspace: "/Users/steipete/clawdbot",
workspace: "/Users/steipete/clawd",
sandbox: {
workspaceRoot: "/Users/steipete/clawdbot/sandboxes",
workspaceRoot: "/Users/steipete/clawd/sandboxes",
docker: {
image: "clawdbot-sandbox",
containerPrefix: "clawdbot-sbx",
@@ -365,8 +365,8 @@ describe("doctor", () => {
const sandbox = agent.sandbox as Record<string, unknown>;
const docker = sandbox.docker as Record<string, unknown>;
expect(agent.workspace).toBe("/Users/steipete/clawdbot");
expect(sandbox.workspaceRoot).toBe("/Users/steipete/clawdbot/sandboxes");
expect(agent.workspace).toBe("/Users/steipete/clawd");
expect(sandbox.workspaceRoot).toBe("/Users/steipete/clawd/sandboxes");
expect(docker.image).toBe("clawdbot-sandbox");
expect(docker.containerPrefix).toBe("clawdbot-sbx");
});

View File

@@ -247,15 +247,28 @@ async function noteSecurityWarnings(cfg: ClawdbotConfig) {
}
}
function replacePathSegment(
function normalizeDefaultWorkspacePath(
value: string | undefined,
from: string,
to: string,
): string | undefined {
if (!value) return value;
const pattern = new RegExp(`(^|[\\/])${from}([\\/]|$)`, "g");
if (!pattern.test(value)) return value;
return value.replace(pattern, `$1${to}$2`);
const resolved = resolveUserPath(value);
const home = os.homedir();
const next = [
["clawdis", "clawd"],
["clawdbot", "clawd"],
].reduce((acc, [from, to]) => {
const fromPrefix = path.join(home, from);
if (acc === fromPrefix) return path.join(home, to);
const withSep = `${fromPrefix}${path.sep}`;
if (acc.startsWith(withSep)) {
return path.join(home, to).concat(acc.slice(fromPrefix.length));
}
return acc;
}, resolved);
return next === resolved ? value : next;
}
function replaceLegacyName(value: string | undefined): string | undefined {
@@ -556,11 +569,7 @@ function normalizeLegacyConfigValues(cfg: ClawdbotConfig): {
let next: ClawdbotConfig = cfg;
const workspace = cfg.agent?.workspace;
const updatedWorkspace = replacePathSegment(
replacePathSegment(workspace, "clawdis", "clawdbot"),
"clawd",
"clawdbot",
);
const updatedWorkspace = normalizeDefaultWorkspacePath(workspace);
if (updatedWorkspace && updatedWorkspace !== workspace) {
next = {
...next,
@@ -573,11 +582,7 @@ function normalizeLegacyConfigValues(cfg: ClawdbotConfig): {
}
const workspaceRoot = cfg.agent?.sandbox?.workspaceRoot;
const updatedWorkspaceRoot = replacePathSegment(
replacePathSegment(workspaceRoot, "clawdis", "clawdbot"),
"clawd",
"clawdbot",
);
const updatedWorkspaceRoot = normalizeDefaultWorkspacePath(workspaceRoot);
if (updatedWorkspaceRoot && updatedWorkspaceRoot !== workspaceRoot) {
next = {
...next,

View File

@@ -223,10 +223,11 @@ export async function openUrl(url: string): Promise<boolean> {
export async function ensureWorkspaceAndSessions(
workspaceDir: string,
runtime: RuntimeEnv,
options?: { skipBootstrap?: boolean },
) {
const ws = await ensureAgentWorkspace({
dir: workspaceDir,
ensureBootstrapFiles: true,
ensureBootstrapFiles: !options?.skipBootstrap,
});
runtime.log(`Workspace OK: ${ws.dir}`);
const sessionsDir = resolveSessionTranscriptsDir();

View File

@@ -219,7 +219,9 @@ export async function runNonInteractiveOnboarding(
nextConfig = applyWizardMetadata(nextConfig, { command: "onboard", mode });
await writeConfigFile(nextConfig);
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
await ensureWorkspaceAndSessions(workspaceDir, runtime);
await ensureWorkspaceAndSessions(workspaceDir, runtime, {
skipBootstrap: Boolean(nextConfig.agent?.skipBootstrap),
});
if (opts.installDaemon) {
const service = resolveGatewayService();

View File

@@ -74,7 +74,7 @@ export async function setupCommand(
const ws = await ensureAgentWorkspace({
dir: workspace,
ensureBootstrapFiles: true,
ensureBootstrapFiles: !next.agent?.skipBootstrap,
});
runtime.log(`Workspace OK: ${ws.dir}`);

View File

@@ -604,7 +604,9 @@ export async function runOnboardingWizard(
await writeConfigFile(nextConfig);
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
await ensureWorkspaceAndSessions(workspaceDir, runtime);
await ensureWorkspaceAndSessions(workspaceDir, runtime, {
skipBootstrap: Boolean(nextConfig.agent?.skipBootstrap),
});
nextConfig = await setupSkills(nextConfig, workspaceDir, runtime, prompter);
nextConfig = applyWizardMetadata(nextConfig, { command: "onboard", mode });