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.
- package/LICENSE +21 -0
- package/README.md +39 -147
- package/bin/honeydew.js +70 -6
- package/package.json +11 -8
- package/src/animation-keyframes.js +890 -0
- package/src/animation.js +151 -0
- package/src/ascii-tree.js +21 -0
- package/src/auth.js +83 -0
- package/src/components/ForestApp.js +404 -0
- package/src/components/ForestScene.js +26 -0
- package/src/components/LiveTree.js +14 -0
- package/src/components/StatsBar.js +70 -0
- package/src/components/TreeInfoPopup.js +28 -0
- package/src/components/UnlockCelebration.js +40 -0
- package/src/growth.js +32 -0
- package/src/init.js +48 -2
- package/src/plant-real.js +129 -0
- package/src/plant.js +39 -17
- package/src/renderer.js +131 -12
- package/src/rewards.js +119 -0
- package/src/session.js +34 -0
- package/src/sprites.js +72 -6
- package/src/stdin.js +28 -0
- package/src/sync.js +44 -0
- package/src/transcript.js +62 -0
- package/src/turn.js +54 -0
- package/src/varieties.js +30 -0
- package/src/viewer2d.js +24 -0
- package/src/markdown.js +0 -77
- package/src/viewer.js +0 -234
package/src/varieties.js
ADDED
|
@@ -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
|
+
}
|
package/src/viewer2d.js
ADDED
|
@@ -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
|
-
`[](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
|
-
}
|