-
Notifications
You must be signed in to change notification settings - Fork 489
feat(settings): add interactive setup wizard for initial configuration #204
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,9 @@ | ||
| import fs from 'fs'; | ||
| import os from 'os'; | ||
| 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. */ | ||
|
|
@@ -14,6 +16,17 @@ export function mutateSettings(fn: (settings: Settings) => void): Settings { | |
|
|
||
| const app = new Hono(); | ||
|
|
||
| function expandHomePath(input?: string): string | undefined { | ||
| if (!input) return input; | ||
| const home = process.env.HOME || os.homedir(); | ||
| if (!home) return input; | ||
| if (input === '~') return home; | ||
| if (input.startsWith('~/')) return path.join(home, input.slice(2)); | ||
| if (input === '$HOME') return home; | ||
| if (input.startsWith('$HOME/')) return path.join(home, input.slice(6)); | ||
| return input; | ||
| } | ||
|
|
||
| // GET /api/settings | ||
| app.get('/api/settings', (c) => { | ||
| return c.json(getSettings()); | ||
|
|
@@ -29,4 +42,59 @@ 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; | ||
|
|
||
| if (settings.workspace?.path) { | ||
| settings.workspace.path = expandHomePath(settings.workspace.path); | ||
| } | ||
| if (settings.agents) { | ||
| for (const agent of Object.values(settings.agents)) { | ||
| if (agent.working_directory) { | ||
| agent.working_directory = expandHomePath(agent.working_directory) || agent.working_directory; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // 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); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Create workspace directory | ||
| const workspacePath = settings.workspace?.path; | ||
| if (workspacePath) { | ||
| fs.mkdirSync(workspacePath, { recursive: true }); | ||
greptile-apps[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Filesystem errors surface as unhandled exceptions The 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; | ||
There was a problem hiding this comment.
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) andAGENTS.mdwill 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: