/** * `sl0ppi init` — opt a directory in as a project. * * Writes `.sl0ppi/layout.kdl`, the pane fragment sl0ppi wraps in a tab. The * template is mostly comments on purpose: the vocabulary and the * `start_suspended` habit are the two things a newcomer cannot guess, and both * are cheap to explain at the point of use. */ import { existsSync, mkdirSync, writeFileSync } from "node:fs"; import { basename, join, resolve } from "node:path"; import { LAYOUT_FILE, MARKER_DIR } from "../core/discover.ts"; export const TEMPLATE = `// sl0ppi project layout. // // PANE NODES ONLY. sl0ppi wraps these in the tab that carries the project's // identity (its \`purpose\`) and cwd, so do not add \`layout {}\` or \`tab {}\` here. // \`sl0ppi print\` shows the composed result. // // Purposes are how tooling finds a pane. sl0ppi owns the vocabulary; the // terminal stores the string and never interprets it: // // agent: an agent pane (\`sl0ppi agent\` looks for agent:main) // service: a long-running process // logs: a log tail // human: a pane meant for a person // // start_suspended stages a command without running it: the pane and the layout // exist, and the command starts on a keypress. Keep it on anything expensive, // so \`sl0ppi up\` across a whole root does not launch a dozen servers at once. // // KDL nodes need \`;\` between them on a single line. Just use one line per node. pane purpose="agent:main" start_suspended=true command="pi" // A fuller example, for when one pane stops being enough: // // pane split_direction="vertical" { // pane purpose="agent:main" start_suspended=true command="pi" // pane split_direction="horizontal" { // pane purpose="service:web" start_suspended=true command="npm" { // args "run" "dev" // } // pane purpose="logs:app" start_suspended=true command="tail" { // args "-f" "log/dev.log" // } // } // } `; export interface InitResult { path: string; created: boolean; /** Set when an existing file was left alone. */ skipped?: string; } export function init(dir: string, options: { force?: boolean } = {}): InitResult { const target = resolve(dir); if (!existsSync(target)) throw new Error(`no such directory: ${target}`); const markerDir = join(target, MARKER_DIR); const path = join(markerDir, LAYOUT_FILE); if (existsSync(path) && !options.force) { return { path, created: false, skipped: "already exists (use --force to overwrite)" }; } mkdirSync(markerDir, { recursive: true }); writeFileSync(path, TEMPLATE); return { path, created: true }; } export function initMessage(result: InitResult, dir: string): string { if (!result.created) return `${result.path}: ${result.skipped}`; const name = basename(resolve(dir)); return [ `created ${result.path}`, ``, `${name} is now a project. From its root directory:`, ` sl0ppi up open a tab for it (and every other project there)`, ` sl0ppi ls see what is open`, ].join("\n"); }