1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
|
/**
* `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:<name> an agent pane (\`sl0ppi agent\` looks for agent:main)
// service:<name> a long-running process
// logs:<name> a log tail
// human:<name> 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");
}
|