diff options
| author | Adam Malczewski <[email protected]> | 2026-05-20 15:04:26 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-05-20 15:04:26 +0900 |
| commit | adc8bd185b54935e7a31aae04da3175b7989927a (patch) | |
| tree | 031fa53009a4708ce0cc15c0e3dcb2f2a73cf2d9 /packages/core/src/config/loader.ts | |
| parent | 52af4ca33b1f83b8f863a6267d447944f55e729d (diff) | |
| download | dispatch-adc8bd185b54935e7a31aae04da3175b7989927a.tar.gz dispatch-adc8bd185b54935e7a31aae04da3175b7989927a.zip | |
feat: phase 3 — config, skills, model groups, task list, and sidebar UI
- Config system: TOML-based dispatch.toml with hot-reload via chokidar
- Model/key resolution: tag-based model selection, key fallback chains
- Skills system: directory loader with TOML frontmatter, agent mappings
- Task list tool: add/update/list/get operations with WebSocket events
- API routes: GET /config, /skills, /skills/:name, /models, /models/resolve
- Frontend: sidebar with model status, task list, config viewer, skills browser, permission log
- Sliding sidebar animation using CSS transitions (not Svelte transitions)
Diffstat (limited to 'packages/core/src/config/loader.ts')
| -rw-r--r-- | packages/core/src/config/loader.ts | 119 |
1 files changed, 22 insertions, 97 deletions
diff --git a/packages/core/src/config/loader.ts b/packages/core/src/config/loader.ts index 0e58ad2..3b4d733 100644 --- a/packages/core/src/config/loader.ts +++ b/packages/core/src/config/loader.ts @@ -1,25 +1,12 @@ import { readFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; +import { parse } from "smol-toml"; import type { PermissionRule, Ruleset } from "../permission/index.js"; +import type { DispatchConfig } from "../types/index.js"; +import { validateConfig } from "./schema.js"; -// Strip inline comments that appear outside of quoted strings. -// Handles both single- and double-quoted values. -function stripInlineComment(line: string): string { - // Walk character by character; track whether we're inside quotes. - let inQuote: string | null = null; - for (let i = 0; i < line.length; i++) { - const ch = line[i]; - if (inQuote) { - if (ch === inQuote) inQuote = null; - } else if (ch === '"' || ch === "'") { - inQuote = ch; - } else if (ch === "#") { - return line.slice(0, i).trimEnd(); - } - } - return line; -} +const DEFAULT_CONFIG: DispatchConfig = { permissions: {} }; const VALID_ACTIONS = new Set(["allow", "deny", "ask"]); @@ -29,89 +16,27 @@ function validateAction(raw: string): "allow" | "deny" | "ask" { return "ask"; } -export interface DispatchConfig { - permissions: Record<string, string | Record<string, string>>; -} - -// Load dispatch.yaml from the given directory +// Load dispatch.toml from the given directory export function loadConfig(dir: string): DispatchConfig { - const yamlPath = join(dir, "dispatch.yaml"); + const tomlPath = join(dir, "dispatch.toml"); + let raw: unknown; try { - const content = readFileSync(yamlPath, "utf-8"); - return parseYaml(content); - } catch { - return { permissions: {} }; - } -} - -function expandHome(value: string): string { - const home = homedir(); - return value.replace(/^\$HOME(?=[\/\\]|$)/, home).replace(/^~(?=[\/\\]|$)/, home); -} - -function stripQuotes(s: string): string { - if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { - return s.slice(1, -1); - } - return s; -} - -// Parse simple YAML for the permissions structure -function parseYaml(content: string): DispatchConfig { - const permissions: Record<string, string | Record<string, string>> = {}; - const lines = content.split("\n"); - - let inPermissions = false; - let currentKey: string | null = null; - - for (const raw of lines) { - // Skip comments and blank lines - const trimmed = raw.trimEnd(); - const stripped = trimmed.trimStart(); - if (stripped === "" || stripped.startsWith("#")) continue; - - const indent = trimmed.length - stripped.length; - - if (indent === 0) { - // Top-level key - inPermissions = stripped.startsWith("permissions:"); - currentKey = null; - continue; - } - - if (!inPermissions) continue; - - if (indent === 2) { - // permission key line: " key: value" or " key:" - const colonIdx = stripped.indexOf(":"); - if (colonIdx === -1) continue; - const key = stripQuotes(stripped.slice(0, colonIdx).trim()); - const valueRaw = stripInlineComment(stripped.slice(colonIdx + 1).trim()).trim(); - if (valueRaw === "" || valueRaw === null) { - // nested map follows - currentKey = key; - permissions[currentKey] = {}; - } else { - // inline value - const value = stripQuotes(valueRaw); - permissions[key] = value; - currentKey = null; - } - continue; - } - - if (indent >= 4 && currentKey !== null) { - // sub-key line: " "pattern": action" - const colonIdx = stripped.indexOf(":"); - if (colonIdx === -1) continue; - const pattern = expandHome(stripQuotes(stripped.slice(0, colonIdx).trim())); - const actionRaw = stripQuotes(stripInlineComment(stripped.slice(colonIdx + 1).trim()).trim()); - const action = validateAction(actionRaw); - (permissions[currentKey] as Record<string, string>)[pattern] = action; + const content = readFileSync(tomlPath, "utf-8"); + raw = parse(content); + } catch (err: unknown) { + if (err instanceof Error && (err as NodeJS.ErrnoException).code === "ENOENT") { + // File doesn't exist — return empty default + return DEFAULT_CONFIG; } + console.warn(`dispatch: failed to parse dispatch.toml: ${err instanceof Error ? err.message : String(err)}`); + throw err; } - return { permissions }; + const { config, errors } = validateConfig(raw); + for (const e of errors) { + console.warn(`dispatch: config warning at ${e.path}: ${e.message}`); + } + return config; } // Convert the config's permission block to a Ruleset @@ -126,8 +51,8 @@ export function configToRuleset(config: DispatchConfig): Ruleset { } else { for (const [rawPattern, rawAction] of Object.entries(value)) { const pattern = rawPattern - .replace(/^\$HOME(?=[\/\\]|$)/, home) - .replace(/^~(?=[\/\\]|$)/, home); + .replace(/^\$HOME(?=[/\\]|$)/, home) + .replace(/^~(?=[/\\]|$)/, home); const action = validateAction(rawAction); rules.push({ permission, pattern, action }); } |
