habithive-code 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/setup.js ADDED
@@ -0,0 +1,106 @@
1
+ // habithive-code — one-command full-stack setup.
2
+ // 1. opencode — official npm package (installed if missing) [required]
3
+ // 2. oh-my-opencode — pinned version wired into config (npm fetches it) [required]
4
+ // 3. habithive — plugin wired into tui.json [required]
5
+ // 4. playwright MCP — wired into the opencode config [extra]
6
+ // 5. openspec CLI — @fission-ai/openspec (npm) [extra]
7
+ // 6. graphify CLI — graphifyy (PyPI, via uv/pip) [extra]
8
+ // 7. bundled skills — karpathy-guidelines (MIT) + graphify → config/skills [extra]
9
+ // Nothing is bundled/redistributed except the two skills under skills/. Every
10
+ // tool is installed from its official source. Use `habithive setup --minimal` to
11
+ // skip the extras (4-7).
12
+
13
+ import { spawnSync } from "node:child_process";
14
+ import { createInterface } from "node:readline";
15
+ import { mkdirSync, cpSync, existsSync, readdirSync } from "node:fs";
16
+ import { join } from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+ import { setMode, ensureMcp, OMO_SPEC, PATHS } from "./config.js";
19
+
20
+ const c = { reset: "\x1b[0m", cyan: "\x1b[36m", green: "\x1b[32m", yellow: "\x1b[33m", dim: "\x1b[2m", red: "\x1b[31m" };
21
+ const say = (m) => process.stdout.write(m + "\n");
22
+ const step = (m) => say(`${c.cyan}▸${c.reset} ${m}`);
23
+ const ok = (m) => say(`${c.green}✓${c.reset} ${m}`);
24
+ const warn = (m) => say(`${c.yellow}!${c.reset} ${m}`);
25
+ const win = process.platform === "win32";
26
+
27
+ function has(cmd) {
28
+ return spawnSync(cmd, ["--version"], { stdio: "ignore", shell: win }).status === 0;
29
+ }
30
+ function run(cmd, args) {
31
+ return spawnSync(cmd, args, { stdio: "inherit", shell: win }).status === 0;
32
+ }
33
+ async function confirm(q, yes) {
34
+ if (yes || !process.stdin.isTTY) return true;
35
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
36
+ const a = await new Promise((r) => rl.question(`${q} ${c.dim}[Y/n]${c.reset} `, r));
37
+ rl.close();
38
+ return !/^n/i.test(a.trim());
39
+ }
40
+
41
+ // Install the graphify (graphifyy on PyPI) Python CLI — prefer uv, fall back to pip.
42
+ function installGraphify() {
43
+ if (has("uv")) return run("uv", ["tool", "install", "--upgrade", "graphifyy"]);
44
+ const py = has("python3") ? "python3" : has("python") ? "python" : null;
45
+ if (!py) return false;
46
+ return run(py, ["-m", "pip", "install", "-U", "graphifyy"]) ||
47
+ run(py, ["-m", "pip", "install", "-U", "--break-system-packages", "graphifyy"]);
48
+ }
49
+
50
+ // Copy the skills bundled with this package into ~/.config/opencode/skills/.
51
+ function installSkills() {
52
+ const srcDir = fileURLToPath(new URL("../skills", import.meta.url));
53
+ if (!existsSync(srcDir)) return [];
54
+ const dest = join(PATHS.CONFIG_DIR, "skills");
55
+ mkdirSync(dest, { recursive: true });
56
+ const names = readdirSync(srcDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
57
+ for (const n of names) cpSync(join(srcDir, n), join(dest, n), { recursive: true });
58
+ return names;
59
+ }
60
+
61
+ export async function runSetup({ yes = false, mode = "complex", minimal = false } = {}) {
62
+ say(`\n${c.cyan}habithive-code setup${c.reset} ${c.dim}— opencode + oh-my-opencode + habithive${minimal ? "" : " + tools + skills"}${c.reset}\n`);
63
+
64
+ // 1) opencode (required)
65
+ if (has("opencode")) ok("opencode already installed");
66
+ else {
67
+ warn("opencode not found");
68
+ if (!(await confirm("Install opencode now (npm i -g opencode-ai)?", yes))) {
69
+ say(`${c.red}aborted${c.reset} — install opencode first, then re-run: habithive setup`);
70
+ return 1;
71
+ }
72
+ step("installing opencode…");
73
+ if (!run("npm", ["i", "-g", "opencode-ai"])) { say(`${c.red}✗ opencode install failed${c.reset}`); return 1; }
74
+ ok("opencode installed");
75
+ }
76
+
77
+ // 2)+3) wire oh-my-opencode (pinned) + habithive, default complex
78
+ mkdirSync(PATHS.CONFIG_DIR, { recursive: true });
79
+ step(`wiring oh-my-opencode (${OMO_SPEC}) + habithive…`);
80
+ setMode(mode);
81
+ ok(`config wired (${mode} mode)`);
82
+
83
+ if (!minimal) {
84
+ // 4) playwright MCP
85
+ if (ensureMcp("playwright", { type: "local", enabled: true, command: "npx", args: ["@playwright/mcp@latest"] }))
86
+ ok("playwright MCP added"); else ok("playwright MCP already configured");
87
+
88
+ // 5) openspec CLI
89
+ if (has("openspec")) ok("openspec already installed");
90
+ else { step("installing openspec (@fission-ai/openspec)…"); run("npm", ["i", "-g", "@fission-ai/openspec"]) ? ok("openspec installed") : warn("openspec install skipped/failed"); }
91
+
92
+ // 6) graphify CLI (graphifyy on PyPI)
93
+ if (has("graphify")) ok("graphify already installed");
94
+ else { step("installing graphify (graphifyy via uv/pip)…"); installGraphify() ? ok("graphify installed") : warn("graphify install skipped/failed (needs uv or pip)"); }
95
+
96
+ // 7) bundled skills → config/skills (loaded by oh-my-opencode in complex mode)
97
+ const skills = installSkills();
98
+ if (skills.length) ok(`skills installed: ${skills.join(", ")}`);
99
+ }
100
+
101
+ say(`\n${c.green}done.${c.reset} start with: ${c.cyan}habithive${c.reset}`);
102
+ say(`${c.dim} habithive complex mode, new session`);
103
+ say(` habithive simple vanilla opencode`);
104
+ say(` habithive -c | -s <id> continue last / specific session${c.reset}\n`);
105
+ return 0;
106
+ }