honeytree 1.1.6 → 1.2.1

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.
@@ -0,0 +1,30 @@
1
+ // Canonical tree-variety definitions for the CLI forest.
2
+ // Each variety maps to one or more renderable "species" specs: { type, variant }.
3
+ // `type` indexes SPRITES in sprites.js; `variant: "ancient"` routes through
4
+ // getAncientSprite (the type is then a base placeholder).
5
+ export const VARIETIES = [
6
+ { key: "standard", threshold: 0, label: "Standard", species: [{ type: "birch" }, { type: "willow" }] },
7
+ { key: "cherry", threshold: 1, label: "Cherry Blossom", species: [{ type: "cherry_blossom" }] },
8
+ { key: "pine", threshold: 5, label: "Pine", species: [{ type: "pine" }] },
9
+ { key: "oak", threshold: 10, label: "Oak", species: [{ type: "oak" }] },
10
+ { key: "ancient", threshold: 25, label: "Ancient", species: [{ type: "oak", variant: "ancient" }] },
11
+ { key: "mythic", threshold: 50, label: "Mythic", species: [{ type: "mythic" }] },
12
+ ];
13
+
14
+ // Build the species pool from the set of unlocked variety keys.
15
+ // `standard` is always included.
16
+ export function unlockedPool(unlockedKeys = []) {
17
+ const keys = new Set(["standard", ...unlockedKeys]);
18
+ const pool = [];
19
+ for (const v of VARIETIES) {
20
+ if (keys.has(v.key)) pool.push(...v.species);
21
+ }
22
+ return pool;
23
+ }
24
+
25
+ const STANDARD_SPECIES = VARIETIES[0].species;
26
+
27
+ export function pickSpecies(pool) {
28
+ const list = Array.isArray(pool) && pool.length > 0 ? pool : STANDARD_SPECIES;
29
+ return list[Math.floor(Math.random() * list.length)];
30
+ }
@@ -0,0 +1,24 @@
1
+ import fs from "node:fs";
2
+ import React from "react";
3
+ import { render } from "ink";
4
+
5
+ import ForestApp from "./components/ForestApp.js";
6
+
7
+ export function createForestWatcher(filePath, onChange) {
8
+ try {
9
+ const stat = fs.statSync(filePath);
10
+ if (!stat.isFile()) return null;
11
+
12
+ const watcher = fs.watch(filePath, onChange);
13
+ watcher.on("error", () => {
14
+ try { watcher.close(); } catch {}
15
+ });
16
+ return watcher;
17
+ } catch {
18
+ return null;
19
+ }
20
+ }
21
+
22
+ export async function viewer() {
23
+ render(React.createElement(ForestApp));
24
+ }
package/src/markdown.js DELETED
@@ -1,77 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
-
4
- import { readForest } from "./state.js";
5
- import { renderPlainText, getWiltFactor } from "./renderer.js";
6
-
7
- function getBiomeLabel(count) {
8
- if (count < 10) return "clearing";
9
- if (count < 25) return "grove";
10
- if (count < 50) return "woodland";
11
- if (count < 100) return "old growth";
12
- return "ancient forest";
13
- }
14
-
15
- function getDayCount(createdAt) {
16
- const created = new Date(createdAt).getTime();
17
- const diff = Date.now() - created;
18
- return Math.max(1, Math.floor(diff / (24 * 60 * 60 * 1000)) + 1);
19
- }
20
-
21
- function buildMarkdown(forest) {
22
- const count = forest.trees.length;
23
- const streak = forest.streak || 0;
24
- const biome = getBiomeLabel(count);
25
- const wilt = getWiltFactor(forest.lastActiveDate);
26
- const days = getDayCount(forest.createdAt);
27
- const width = Math.min(forest.viewerWidth || 60, 80);
28
-
29
- const art = renderPlainText(forest, width);
30
-
31
- const statParts = [`**${count} tree${count === 1 ? "" : "s"}**`];
32
- if (wilt > 0) {
33
- const a = new Date(forest.lastActiveDate + "T00:00:00");
34
- const b = new Date(new Date().toISOString().slice(0, 10) + "T00:00:00");
35
- const idle = Math.round((b - a) / (24 * 60 * 60 * 1000));
36
- statParts.push(`**wilting (${idle}d idle)**`);
37
- } else if (streak > 0) {
38
- statParts.push(`**${streak}-day streak**`);
39
- }
40
- statParts.push(`**${biome}**`);
41
-
42
- const lines = [
43
- `<div align="center">`,
44
- ``,
45
- `[![honeytree](./honeytree-badge.svg)](https://github.com/Varun2009178/honeytree)`,
46
- ``,
47
- statParts.join(" · "),
48
- ``,
49
- "```",
50
- art,
51
- "```",
52
- ``,
53
- `${forest.totalPrompts} prompts over ${days} day${days === 1 ? "" : "s"}`,
54
- ``,
55
- `<sub>Grown with <a href="https://github.com/Varun2009178/honeytree">honeytree</a> — a forest that grows in your terminal every time you use Claude Code</sub>`,
56
- ``,
57
- `</div>`,
58
- ``,
59
- ];
60
-
61
- return lines.join("\n");
62
- }
63
-
64
- export async function generateForestMd() {
65
- const forest = readForest();
66
- if (!forest) {
67
- console.error('No forest found. Run "honeytree init" first.');
68
- process.exit(1);
69
- }
70
-
71
- const md = buildMarkdown(forest);
72
- const outPath = path.resolve("FOREST.md");
73
- fs.writeFileSync(outPath, md);
74
-
75
- console.log(`Written to ${outPath}`);
76
- console.log("Tip: run `honeytree badge` to generate the badge SVG too.");
77
- }
package/src/viewer.js DELETED
@@ -1,234 +0,0 @@
1
- import fs from "node:fs";
2
-
3
- import { renderFrame } from "./renderer.js";
4
- import { getForestFile, readForest, writeForest } from "./state.js";
5
- import { migrateLayout } from "./migrate.js";
6
- import { getVirtualWidth } from "./plant.js";
7
-
8
- function writeAnsi(code) {
9
- process.stdout.write(code);
10
- }
11
-
12
- function clearScreen() {
13
- writeAnsi("\x1b[2J\x1b[H");
14
- }
15
-
16
- function hideCursor() {
17
- writeAnsi("\x1b[?25l");
18
- }
19
-
20
- function showCursor() {
21
- writeAnsi("\x1b[?25h");
22
- }
23
-
24
- function moveHome() {
25
- writeAnsi("\x1b[H");
26
- }
27
-
28
- function delay(ms) {
29
- return new Promise((resolve) => setTimeout(resolve, ms));
30
- }
31
-
32
- export async function viewer() {
33
- const forestFile = getForestFile();
34
- let forest = readForest();
35
-
36
- if (!forest || !fs.existsSync(forestFile)) {
37
- console.error('No forest found. Run "honeytree init" first.');
38
- process.exit(1);
39
- }
40
-
41
- // Migrate old layouts on first view
42
- if (forest && (!forest.layoutVersion || forest.layoutVersion < 2)) {
43
- const termWidth = process.stdout.columns || 80;
44
- migrateLayout(forest, termWidth);
45
- // Will be written to disk by syncWidth below
46
- }
47
-
48
- // Save terminal width so plant knows how wide to spread trees
49
- let ignoreNextChange = false;
50
- function syncWidth() {
51
- const cols = process.stdout.columns || 80;
52
- if (forest.viewerWidth !== cols) {
53
- forest.viewerWidth = cols;
54
- ignoreNextChange = true;
55
- writeForest(forest);
56
- }
57
- }
58
-
59
- let lastMaxId = forest.trees.reduce((max, tree) => Math.max(max, tree.id), 0);
60
- let lastTotalPrompts = forest.totalPrompts;
61
- let animating = false;
62
-
63
- let viewportX = forest.viewportX || 0;
64
- const PAN_STEP = 4;
65
-
66
- function getViewportWidth() {
67
- return process.stdout.columns || 80;
68
- }
69
-
70
- function clampViewport(x) {
71
- const vw = getVirtualWidth(forest.trees.length, getViewportWidth());
72
- return Math.max(0, Math.min(x, Math.max(0, vw - getViewportWidth())));
73
- }
74
-
75
- function renderForest(forest, twinkleSeed = 0) {
76
- clearScreen();
77
- const termWidth = process.stdout.columns || 80;
78
- const vw = getVirtualWidth(forest.trees.length, termWidth);
79
- process.stdout.write(renderFrame(forest, termWidth, {
80
- twinkleSeed,
81
- viewportX,
82
- virtualWidth: vw,
83
- }));
84
- }
85
-
86
- async function animateNewTree(forest, newTreeId) {
87
- const tree = forest.trees.find((entry) => entry.id === newTreeId);
88
- if (!tree) {
89
- renderForest(forest);
90
- return;
91
- }
92
-
93
- const originalGrowth = tree.growth;
94
- const frames = [0.12, 0.32, 0.6, originalGrowth].filter(
95
- (value, index, values) => value <= originalGrowth && values.indexOf(value) === index,
96
- );
97
-
98
- for (let index = 0; index < frames.length; index += 1) {
99
- tree.growth = frames[index];
100
- renderForest(forest, index);
101
- await delay(120);
102
- }
103
-
104
- tree.growth = originalGrowth;
105
- renderForest(forest);
106
- }
107
-
108
- syncWidth();
109
- hideCursor();
110
- clearScreen();
111
- renderForest(forest);
112
-
113
- const cleanup = () => {
114
- // Persist viewport position for next session
115
- forest.viewportX = viewportX;
116
- ignoreNextChange = true;
117
- writeForest(forest);
118
- showCursor();
119
- clearScreen();
120
- console.log(
121
- `Forest summary: ${forest.trees.length} trees across ${forest.totalPrompts} prompts`,
122
- );
123
- process.exit(0);
124
- };
125
-
126
- process.on("SIGINT", cleanup);
127
- process.on("SIGTERM", cleanup);
128
- process.stdout.on("resize", () => {
129
- syncWidth();
130
- clearScreen();
131
- renderForest(forest);
132
- });
133
-
134
- // Enable raw mode for keypress handling
135
- if (process.stdin.isTTY) {
136
- process.stdin.setRawMode(true);
137
- process.stdin.resume();
138
- process.stdin.on("data", (data) => {
139
- const key = data.toString();
140
- // Ctrl+C or q to quit
141
- if (key === "\x03" || key === "q") {
142
- cleanup();
143
- return;
144
- }
145
- // Left arrow: \x1b[D
146
- if (key === "\x1b[D") {
147
- viewportX = clampViewport(viewportX - PAN_STEP);
148
- forest.viewportX = viewportX;
149
- renderForest(forest);
150
- return;
151
- }
152
- // Right arrow: \x1b[C
153
- if (key === "\x1b[C") {
154
- viewportX = clampViewport(viewportX + PAN_STEP);
155
- forest.viewportX = viewportX;
156
- renderForest(forest);
157
- return;
158
- }
159
- });
160
- }
161
-
162
- // Check for changes — used by both fs.watch and polling fallback
163
- async function checkForUpdates() {
164
- if (animating) return;
165
-
166
- if (ignoreNextChange) {
167
- ignoreNextChange = false;
168
- return;
169
- }
170
-
171
- const updated = readForest();
172
- if (!updated) return;
173
-
174
- // Only re-render if something actually changed
175
- if (updated.totalPrompts === lastTotalPrompts) return;
176
-
177
- const nextMaxId = updated.trees.reduce((max, tree) => Math.max(max, tree.id), 0);
178
- forest = updated;
179
- lastTotalPrompts = forest.totalPrompts;
180
-
181
- if (nextMaxId > lastMaxId) {
182
- lastMaxId = nextMaxId;
183
- // Center viewport on the new tree
184
- const newTree = forest.trees.find((t) => t.id === nextMaxId);
185
- if (newTree) {
186
- const termWidth = getViewportWidth();
187
- viewportX = clampViewport(newTree.x - Math.floor(termWidth / 2));
188
- }
189
- animating = true;
190
- await animateNewTree(forest, nextMaxId);
191
- animating = false;
192
- } else {
193
- renderForest(forest);
194
- }
195
- }
196
-
197
- // fs.watch can drop events on macOS after atomic renames, so
198
- // use it for fast response but also poll as a reliable fallback
199
- function startWatcher() {
200
- try {
201
- const watcher = fs.watch(forestFile, () => {
202
- checkForUpdates();
203
- });
204
- watcher.on("error", () => {});
205
- return watcher;
206
- } catch {
207
- return null;
208
- }
209
- }
210
-
211
- let watcher = startWatcher();
212
-
213
- // Poll every 800ms as fallback — cheap since it only reads if mtime changed
214
- let lastMtime = 0;
215
- try {
216
- lastMtime = fs.statSync(forestFile).mtimeMs;
217
- } catch {}
218
-
219
- setInterval(() => {
220
- try {
221
- const mtime = fs.statSync(forestFile).mtimeMs;
222
- if (mtime !== lastMtime) {
223
- lastMtime = mtime;
224
- checkForUpdates();
225
-
226
- // Re-establish watcher in case rename killed it
227
- if (watcher) {
228
- try { watcher.close(); } catch {}
229
- }
230
- watcher = startWatcher();
231
- }
232
- } catch {}
233
- }, 800);
234
- }