Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion packages/server/src/routes/settings.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import fs from 'fs';
import path from 'path';
import { Hono } from 'hono';
import { Settings } from '@tinyclaw/core';
import { SETTINGS_FILE, getSettings } from '@tinyclaw/core';
import { SETTINGS_FILE, TINYCLAW_HOME, getSettings, ensureAgentDirectory, copyDirSync, SCRIPT_DIR } from '@tinyclaw/core';
import { log } from '@tinyclaw/core';

/** Read, mutate, and persist settings.json atomically. */
Expand Down Expand Up @@ -29,4 +30,48 @@ app.put('/api/settings', async (c) => {
return c.json({ ok: true, settings: merged });
});

// POST /api/setup — run initial setup (write settings + create directories)
app.post('/api/setup', async (c) => {
const settings = (await c.req.json()) as Settings;

// Write settings.json
fs.mkdirSync(path.dirname(SETTINGS_FILE), { recursive: true });
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2) + '\n');
log('INFO', '[API] Setup: settings.json written');

// Create TINYCLAW_HOME directories
fs.mkdirSync(path.join(TINYCLAW_HOME, 'logs'), { recursive: true });
fs.mkdirSync(path.join(TINYCLAW_HOME, 'files'), { recursive: true });

// Copy template files into TINYCLAW_HOME
const templateItems = ['.claude', 'heartbeat.md', 'AGENTS.md'];
for (const item of templateItems) {
const srcPath = path.join(SCRIPT_DIR, item);
const destPath = path.join(TINYCLAW_HOME, item);
if (fs.existsSync(srcPath)) {
if (fs.statSync(srcPath).isDirectory()) {
copyDirSync(srcPath, destPath);
} else {
fs.copyFileSync(srcPath, destPath);
}
}
}
Comment on lines +70 to +81
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Template files unconditionally overwritten on re-run

The "Run Setup" button on the settings page can trigger this endpoint again on an already-configured system. Since there's no existence check on destPath, files like .claude (which often holds custom Claude configuration) and AGENTS.md will be silently overwritten with the bundled templates, discarding any user modifications.

Consider guarding the copy with an !fs.existsSync(destPath) check, or at least documenting this destructive behaviour:

if (fs.existsSync(srcPath) && !fs.existsSync(destPath)) {
    if (fs.statSync(srcPath).isDirectory()) {
        copyDirSync(srcPath, destPath);
    } else {
        fs.copyFileSync(srcPath, destPath);
    }
}


// Create workspace directory
const workspacePath = settings.workspace?.path;
if (workspacePath) {
fs.mkdirSync(workspacePath, { recursive: true });
}

// Create agent directories
if (settings.agents) {
for (const agent of Object.values(settings.agents)) {
ensureAgentDirectory(agent.working_directory);
}
}

log('INFO', '[API] Setup complete');
return c.json({ ok: true, settings });
});
Comment on lines +46 to +98
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filesystem errors surface as unhandled exceptions

The POST /api/setup handler performs several fs.* operations (mkdirSync, copyFileSync, etc.) with no surrounding try-catch. If any of these fail — due to permission denied, disk full, or a missing parent directory — Hono will propagate the exception as an unhandled 500, and the client's apiFetch will only see "Internal Server Error" rather than a descriptive message.

Wrapping the side-effectful block in a try-catch lets you return a structured error that the UI can display to the user:

try {
    // ... all fs.mkdirSync / copyFileSync / ensureAgentDirectory calls ...
} catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    log('ERROR', `[API] Setup failed: ${msg}`);
    return c.json({ ok: false, error: msg }, 500);
}


export default app;
Loading