feat(electrobun): project wizard phases 1-5 (WIP)

- sanitize.ts: input sanitization (trim, control chars, path traversal)
- provider-scanner.ts: detect Claude/Codex/Ollama/Gemini availability
- model-fetcher.ts: live model lists from 4 provider APIs
- ModelConfigPanel.svelte: per-provider config (thinking, effort, sandbox, temperature)
- WizardStep1-3.svelte: split wizard into composable steps
- CustomDropdown/Checkbox/Radio: themed UI components
- provider-handlers.ts: provider.scan + provider.models RPC
- Wire providers into wizard step 3 (live detection + model lists)
- Replace native selects in 5 settings panels with CustomDropdown
This commit is contained in:
Hibryda 2026-03-23 13:05:07 +01:00
parent b7fc3a0f9b
commit d4014a193d
25 changed files with 2112 additions and 759 deletions

View file

@ -1,5 +1,5 @@
/**
* Git RPC handlers branch listing, clone operations.
* Git RPC handlers branch listing, clone, probe, and template scaffolding.
*/
import path from "path";
@ -98,5 +98,118 @@ export function createGitHandlers() {
});
});
},
"git.probe": async ({ url }: { url: string }) => {
if (!url || (!url.includes("/") && !url.includes(":"))) {
return { ok: false, branches: [], defaultBranch: '', error: "Invalid URL" };
}
return new Promise<{
ok: boolean; branches: string[]; defaultBranch: string; error?: string;
}>((resolve) => {
const proc = spawn("git", ["ls-remote", "--heads", "--symref", url], {
stdio: "pipe",
timeout: 15_000,
});
let stdout = "";
let stderr = "";
proc.stdout?.on("data", (chunk: Buffer) => { stdout += chunk.toString(); });
proc.stderr?.on("data", (chunk: Buffer) => { stderr += chunk.toString(); });
proc.on("close", (code) => {
if (code !== 0) {
resolve({ ok: false, branches: [], defaultBranch: '', error: stderr.trim() || 'Probe failed' });
return;
}
const branches: string[] = [];
let defaultBranch = 'main';
for (const line of stdout.split("\n")) {
// Parse symref for HEAD
const symMatch = line.match(/^ref: refs\/heads\/(\S+)\s+HEAD/);
if (symMatch) { defaultBranch = symMatch[1]; continue; }
// Parse branch refs
const refMatch = line.match(/\trefs\/heads\/(.+)$/);
if (refMatch && !branches.includes(refMatch[1])) {
branches.push(refMatch[1]);
}
}
resolve({ ok: true, branches, defaultBranch });
});
proc.on("error", (err) => {
resolve({ ok: false, branches: [], defaultBranch: '', error: err.message });
});
});
},
"project.createFromTemplate": async ({
templateId,
targetDir,
projectName,
}: {
templateId: string;
targetDir: string;
projectName: string;
}) => {
const resolved = path.resolve(targetDir.replace(/^~/, process.env.HOME ?? ""));
const projectDir = path.join(resolved, projectName);
// Don't overwrite existing directory
try {
fs.statSync(projectDir);
return { ok: false, path: '', error: "Directory already exists" };
} catch {
// Expected
}
try {
fs.mkdirSync(projectDir, { recursive: true });
const scaffolds: Record<string, Record<string, string>> = {
blank: {
'README.md': `# ${projectName}\n`,
'.gitignore': 'node_modules/\ndist/\n.env\n',
},
'web-app': {
'index.html': `<!DOCTYPE html>\n<html lang="en">\n<head>\n <meta charset="UTF-8" />\n <meta name="viewport" content="width=device-width, initial-scale=1.0" />\n <title>${projectName}</title>\n <link rel="stylesheet" href="style.css" />\n</head>\n<body>\n <h1>${projectName}</h1>\n <script src="main.js"></script>\n</body>\n</html>\n`,
'style.css': `body {\n font-family: system-ui, sans-serif;\n margin: 2rem;\n}\n`,
'main.js': `console.log('${projectName} loaded');\n`,
'README.md': `# ${projectName}\n\nA web application.\n`,
'.gitignore': 'node_modules/\ndist/\n.env\n',
},
'api-server': {
'index.ts': `const server = Bun.serve({\n port: 3000,\n fetch(req) {\n const url = new URL(req.url);\n if (url.pathname === '/health') {\n return Response.json({ status: 'ok' });\n }\n return Response.json({ message: 'Hello from ${projectName}' });\n },\n});\nconsole.log(\`Server running on \${server.url}\`);\n`,
'README.md': `# ${projectName}\n\nA Bun HTTP API server.\n\n## Run\n\n\`\`\`bash\nbun run index.ts\n\`\`\`\n`,
'.gitignore': 'node_modules/\ndist/\n.env\n',
'package.json': `{\n "name": "${projectName}",\n "version": "0.1.0",\n "scripts": {\n "start": "bun run index.ts",\n "dev": "bun --watch index.ts"\n }\n}\n`,
},
'cli-tool': {
'cli.ts': `#!/usr/bin/env bun\nconst args = process.argv.slice(2);\nif (args.includes('--help') || args.includes('-h')) {\n console.log('Usage: ${projectName} [options]');\n console.log(' --help, -h Show this help');\n console.log(' --version Show version');\n process.exit(0);\n}\nif (args.includes('--version')) {\n console.log('${projectName} 0.1.0');\n process.exit(0);\n}\nconsole.log('Hello from ${projectName}!');\n`,
'README.md': `# ${projectName}\n\nA command-line tool.\n\n## Run\n\n\`\`\`bash\nbun run cli.ts --help\n\`\`\`\n`,
'.gitignore': 'node_modules/\ndist/\n.env\n',
},
};
const files = scaffolds[templateId] ?? scaffolds['blank'];
for (const [name, content] of Object.entries(files)) {
fs.writeFileSync(path.join(projectDir, name), content);
}
// Initialize git repo
try {
execSync('git init', { cwd: projectDir, timeout: 5000 });
} catch {
// Non-fatal — project created without git
}
return { ok: true, path: projectDir };
} catch (err) {
const error = err instanceof Error ? err.message : String(err);
return { ok: false, path: '', error };
}
},
};
}

View file

@ -0,0 +1,30 @@
/**
* Provider RPC handlers scanning + model fetching.
*/
import { scanAllProviders } from '../provider-scanner.ts';
import { fetchModelsForProvider } from '../model-fetcher.ts';
export function createProviderHandlers() {
return {
'provider.scan': async () => {
try {
const providers = await scanAllProviders();
return { providers };
} catch (err) {
console.error('[provider.scan]', err);
return { providers: [] };
}
},
'provider.models': async ({ provider }: { provider: string }) => {
try {
const models = await fetchModelsForProvider(provider);
return { models };
} catch (err) {
console.error('[provider.models]', err);
return { models: [] };
}
},
};
}