honeytree 1.0.5 → 1.0.7

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,388 @@
1
+ const BLOCK = "█";
2
+
3
+ export const TREE_SPECIES = [
4
+ "sapling",
5
+ "birch",
6
+ "oak",
7
+ "cherry",
8
+ "pine",
9
+ "willow",
10
+ "ancient_oak",
11
+ ];
12
+
13
+ export const ANIMAL_TYPES = ["butterfly", "rabbit", "fox", "deer", "owl"];
14
+
15
+ export const LLAMA_POSES = ["walking", "sitting", "happy"];
16
+
17
+ export const GROUND_ELEMENT_TYPES = ["flower", "mushroom", "rock", "tall_grass"];
18
+
19
+ function parseSprite(template, legend) {
20
+ const lines = template
21
+ .trim()
22
+ .split("\n")
23
+ .map((line) => line.replace(/\r/g, ""));
24
+ const width = Math.max(...lines.map((line) => line.length));
25
+ const rows = lines
26
+ .map((line) => line.padEnd(width, " "))
27
+ .map((line) =>
28
+ Array.from(line, (token) => {
29
+ if (token === " ") {
30
+ return null;
31
+ }
32
+ return legend[token] ?? null;
33
+ }),
34
+ )
35
+ .reverse();
36
+
37
+ return {
38
+ rows,
39
+ width,
40
+ height: rows.length,
41
+ };
42
+ }
43
+
44
+ export function materializeSprite(sprite, palette, char = BLOCK) {
45
+ return {
46
+ width: sprite.width,
47
+ height: sprite.height,
48
+ rows: sprite.rows.map((row) =>
49
+ row.map((token) => {
50
+ if (!token) {
51
+ return [" ", null];
52
+ }
53
+ return [char, palette[token] ?? token];
54
+ }),
55
+ ),
56
+ };
57
+ }
58
+
59
+ export function getTreeSpeciesForCommit(totalCommits) {
60
+ if (totalCommits <= 5) return "sapling";
61
+ if (totalCommits <= 15) return "birch";
62
+ if (totalCommits <= 30) return "oak";
63
+ if (totalCommits <= 50) return "cherry";
64
+ if (totalCommits <= 75) return "pine";
65
+ if (totalCommits <= 100) return "willow";
66
+ return "ancient_oak";
67
+ }
68
+
69
+ export function getGroundElementType(totalFileSaves) {
70
+ const tier = Math.max(0, Math.floor(totalFileSaves / 50) - 1);
71
+ return GROUND_ELEMENT_TYPES[tier % GROUND_ELEMENT_TYPES.length];
72
+ }
73
+
74
+ export function getAnimalTypeForMinutes(totalMinutesCoded) {
75
+ if (totalMinutesCoded >= 480) return "owl";
76
+ if (totalMinutesCoded >= 240) return "deer";
77
+ if (totalMinutesCoded >= 120) return "fox";
78
+ if (totalMinutesCoded >= 60) return "rabbit";
79
+ return "butterfly";
80
+ }
81
+
82
+ const TREE_SPRITES = {
83
+ sapling: parseSprite(
84
+ `
85
+ l
86
+ lL
87
+ t
88
+ `,
89
+ {
90
+ l: "leaf",
91
+ L: "leafLight",
92
+ t: "trunk",
93
+ },
94
+ ),
95
+ birch: parseSprite(
96
+ `
97
+ ll
98
+ lLL
99
+ llLLl
100
+ bb
101
+ bB
102
+ `,
103
+ {
104
+ l: "leaf",
105
+ L: "leafLight",
106
+ b: "birchBark",
107
+ B: "birchShadow",
108
+ },
109
+ ),
110
+ oak: parseSprite(
111
+ `
112
+ dd
113
+ dllll
114
+ dllLLlld
115
+ llllll
116
+ tt
117
+ tTT
118
+ `,
119
+ {
120
+ d: "leafDark",
121
+ l: "leaf",
122
+ L: "leafLight",
123
+ t: "trunk",
124
+ T: "trunkLight",
125
+ },
126
+ ),
127
+ cherry: parseSprite(
128
+ `
129
+ pp
130
+ pPPPP
131
+ pPPpPPP
132
+ PPPPP
133
+ tt
134
+ tTT
135
+ `,
136
+ {
137
+ p: "petal",
138
+ P: "petalLight",
139
+ t: "trunk",
140
+ T: "trunkLight",
141
+ },
142
+ ),
143
+ pine: parseSprite(
144
+ `
145
+ p
146
+ pPp
147
+ pPPPp
148
+ pPPPPPp
149
+ PPPPP
150
+ tT
151
+ tT
152
+ `,
153
+ {
154
+ p: "pineDark",
155
+ P: "pine",
156
+ t: "trunkDark",
157
+ T: "trunk",
158
+ },
159
+ ),
160
+ willow: parseSprite(
161
+ `
162
+ llll
163
+ llLLLLll
164
+ llLllllLll
165
+ ll ll
166
+ ll ll
167
+ tTT
168
+ tTT
169
+ `,
170
+ {
171
+ l: "leaf",
172
+ L: "leafLight",
173
+ t: "trunk",
174
+ T: "trunkLight",
175
+ },
176
+ ),
177
+ ancient_oak: parseSprite(
178
+ `
179
+ dd
180
+ ddllllld
181
+ dllLLLllld
182
+ ddllLLLllld
183
+ llllllll
184
+ mmm
185
+ tTTT
186
+ ttTTtt
187
+ `,
188
+ {
189
+ d: "leafDark",
190
+ l: "leaf",
191
+ L: "leafLight",
192
+ m: "moss",
193
+ t: "trunkDark",
194
+ T: "trunk",
195
+ },
196
+ ),
197
+ };
198
+
199
+ const ANIMAL_SPRITES = {
200
+ butterfly: parseSprite(
201
+ `
202
+ w w
203
+ W
204
+ `,
205
+ {
206
+ w: "wing",
207
+ W: "wingLight",
208
+ },
209
+ ),
210
+ rabbit: parseSprite(
211
+ `
212
+ ee
213
+ rrre
214
+ rr
215
+ `,
216
+ {
217
+ r: "rabbit",
218
+ e: "rabbitLight",
219
+ },
220
+ ),
221
+ fox: parseSprite(
222
+ `
223
+ ff
224
+ ffff
225
+ fllf
226
+ `,
227
+ {
228
+ f: "fox",
229
+ l: "foxLight",
230
+ },
231
+ ),
232
+ deer: parseSprite(
233
+ `
234
+ aa
235
+ dddd
236
+ d dd
237
+ `,
238
+ {
239
+ a: "deerLight",
240
+ d: "deer",
241
+ },
242
+ ),
243
+ owl: parseSprite(
244
+ `
245
+ oo
246
+ OooO
247
+ tt
248
+ `,
249
+ {
250
+ o: "owl",
251
+ O: "owlLight",
252
+ t: "trunkDark",
253
+ },
254
+ ),
255
+ };
256
+
257
+ const LLAMA_SPRITES = {
258
+ walking: parseSprite(
259
+ `
260
+ ee
261
+ lllN
262
+ llll
263
+ l l
264
+ `,
265
+ {
266
+ e: "llamaLight",
267
+ l: "llama",
268
+ N: "llamaNose",
269
+ },
270
+ ),
271
+ sitting: parseSprite(
272
+ `
273
+ ee
274
+ lllN
275
+ lll
276
+ ll
277
+ `,
278
+ {
279
+ e: "llamaLight",
280
+ l: "llama",
281
+ N: "llamaNose",
282
+ },
283
+ ),
284
+ happy: parseSprite(
285
+ `
286
+ ee
287
+ lllN
288
+ llll
289
+ l l
290
+ `,
291
+ {
292
+ e: "llamaLight",
293
+ l: "llama",
294
+ N: "llamaNose",
295
+ },
296
+ ),
297
+ };
298
+
299
+ const GROUND_SPRITES = {
300
+ flower: parseSprite(
301
+ `
302
+ c
303
+ FfF
304
+ g
305
+ `,
306
+ {
307
+ c: "flowerCenter",
308
+ F: "flower",
309
+ f: "flowerLight",
310
+ g: "grassLight",
311
+ },
312
+ ),
313
+ mushroom: parseSprite(
314
+ `
315
+ MMM
316
+ s
317
+ `,
318
+ {
319
+ M: "mushroomCap",
320
+ s: "mushroomStem",
321
+ },
322
+ ),
323
+ rock: parseSprite(
324
+ `
325
+ rr
326
+ rrrr
327
+ `,
328
+ {
329
+ r: "stone",
330
+ },
331
+ ),
332
+ tall_grass: parseSprite(
333
+ `
334
+ gg
335
+ ggg
336
+ `,
337
+ {
338
+ g: "grass",
339
+ },
340
+ ),
341
+ };
342
+
343
+ export function getTreeSprite(species) {
344
+ const sprite = TREE_SPRITES[species];
345
+ if (!sprite) {
346
+ throw new Error(`Unknown tree species: ${species}`);
347
+ }
348
+ return sprite;
349
+ }
350
+
351
+ export function getAnimalSprite(type) {
352
+ const sprite = ANIMAL_SPRITES[type];
353
+ if (!sprite) {
354
+ throw new Error(`Unknown animal type: ${type}`);
355
+ }
356
+ return sprite;
357
+ }
358
+
359
+ export function getGroundElementSprite(type) {
360
+ const sprite = GROUND_SPRITES[type];
361
+ if (!sprite) {
362
+ throw new Error(`Unknown ground element type: ${type}`);
363
+ }
364
+ return sprite;
365
+ }
366
+
367
+ export function getLlamaSprite(pose = "walking") {
368
+ const sprite = LLAMA_SPRITES[pose];
369
+ if (!sprite) {
370
+ throw new Error(`Unknown llama pose: ${pose}`);
371
+ }
372
+ return sprite;
373
+ }
374
+
375
+ export function getRandomGroundElement() {
376
+ return GROUND_ELEMENT_TYPES[Math.floor(Math.random() * GROUND_ELEMENT_TYPES.length)];
377
+ }
378
+
379
+ export function getSpriteWidth(kind, type) {
380
+ if (kind === "tree") {
381
+ return getTreeSprite(type).width;
382
+ }
383
+ if (kind === "animal") {
384
+ return getAnimalSprite(type).width;
385
+ }
386
+ return getGroundElementSprite(type).width;
387
+ }
388
+
@@ -0,0 +1,173 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+
6
+ const STATE_DIR_NAME = ".honeytree";
7
+ const STATE_FILE_NAME = "state.json";
8
+ const LEGACY_DIR_NAME = ".honeydew";
9
+ const LEGACY_FILE_NAME = "forest.json";
10
+
11
+ function todayString(now = new Date()) {
12
+ return now.toISOString().slice(0, 10);
13
+ }
14
+
15
+ function resolveStateDir() {
16
+ return process.env.HONEYTREE_DIR || path.join(os.homedir(), STATE_DIR_NAME);
17
+ }
18
+
19
+ function resolveLegacyDir() {
20
+ return process.env.HONEYDEW_DIR || path.join(os.homedir(), LEGACY_DIR_NAME);
21
+ }
22
+
23
+ export function getStateDir() {
24
+ return resolveStateDir();
25
+ }
26
+
27
+ export function getStateFile() {
28
+ return path.join(resolveStateDir(), STATE_FILE_NAME);
29
+ }
30
+
31
+ export function getLegacyForestFile() {
32
+ return path.join(resolveLegacyDir(), LEGACY_FILE_NAME);
33
+ }
34
+
35
+ export function createEmptyState(now = new Date()) {
36
+ return {
37
+ total_commits: 0,
38
+ total_file_saves: 0,
39
+ total_minutes_coded: 0,
40
+ current_streak: 0,
41
+ last_active_date: todayString(now),
42
+ last_commit_hash: "",
43
+ trees: [],
44
+ animals: [],
45
+ ground_elements: [],
46
+ };
47
+ }
48
+
49
+ function normalizeTree(tree) {
50
+ return {
51
+ species: tree?.species ?? tree?.type ?? "sapling",
52
+ planted_at: tree?.planted_at ?? tree?.plantedAt ?? new Date().toISOString(),
53
+ x_position: Number.isFinite(tree?.x_position) ? tree.x_position : tree?.x ?? 12,
54
+ };
55
+ }
56
+
57
+ function normalizeAnimal(animal) {
58
+ return {
59
+ type: animal?.type ?? "butterfly",
60
+ x_position: Number.isFinite(animal?.x_position) ? animal.x_position : animal?.x ?? 10,
61
+ direction: animal?.direction === "left" ? "left" : "right",
62
+ };
63
+ }
64
+
65
+ function normalizeGroundElement(element) {
66
+ return {
67
+ type: element?.type ?? "flower",
68
+ x_position: Number.isFinite(element?.x_position) ? element.x_position : element?.x ?? 8,
69
+ };
70
+ }
71
+
72
+ export function normalizeState(input = {}) {
73
+ const base = createEmptyState();
74
+ return {
75
+ total_commits: Number.isFinite(input.total_commits)
76
+ ? input.total_commits
77
+ : input.totalPrompts ?? base.total_commits,
78
+ total_file_saves: Number.isFinite(input.total_file_saves) ? input.total_file_saves : 0,
79
+ total_minutes_coded: Number.isFinite(input.total_minutes_coded) ? input.total_minutes_coded : 0,
80
+ current_streak: Number.isFinite(input.current_streak)
81
+ ? input.current_streak
82
+ : input.streak ?? base.current_streak,
83
+ last_active_date:
84
+ typeof input.last_active_date === "string"
85
+ ? input.last_active_date
86
+ : input.lastActiveDate ?? base.last_active_date,
87
+ last_commit_hash: typeof input.last_commit_hash === "string" ? input.last_commit_hash : "",
88
+ trees: Array.isArray(input.trees) ? input.trees.map(normalizeTree) : [],
89
+ animals: Array.isArray(input.animals) ? input.animals.map(normalizeAnimal) : [],
90
+ ground_elements: Array.isArray(input.ground_elements)
91
+ ? input.ground_elements.map(normalizeGroundElement)
92
+ : [],
93
+ };
94
+ }
95
+
96
+ export function migrateOldForest(oldForest = {}) {
97
+ const migrated = createEmptyState();
98
+ migrated.total_commits = oldForest.totalPrompts ?? oldForest.trees?.length ?? 0;
99
+ migrated.current_streak = oldForest.streak ?? 0;
100
+ migrated.last_active_date = oldForest.lastActiveDate ?? todayString();
101
+ migrated.trees = Array.isArray(oldForest.trees) ? oldForest.trees.map(normalizeTree) : [];
102
+ return migrated;
103
+ }
104
+
105
+ function readJson(filePath) {
106
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
107
+ }
108
+
109
+ function backupCorruptState(filePath) {
110
+ const dir = path.dirname(filePath);
111
+ const backupFile = path.join(
112
+ dir,
113
+ `${path.basename(filePath, ".json")}.bak.${Date.now()}.json`,
114
+ );
115
+ try {
116
+ fs.renameSync(filePath, backupFile);
117
+ } catch {}
118
+ }
119
+
120
+ export function readState() {
121
+ const stateFile = getStateFile();
122
+ try {
123
+ if (fs.existsSync(stateFile)) {
124
+ return normalizeState(readJson(stateFile));
125
+ }
126
+ } catch {
127
+ backupCorruptState(stateFile);
128
+ return createEmptyState();
129
+ }
130
+
131
+ const legacyFile = getLegacyForestFile();
132
+ try {
133
+ if (fs.existsSync(legacyFile)) {
134
+ const migrated = migrateOldForest(readJson(legacyFile));
135
+ writeState(migrated);
136
+ return migrated;
137
+ }
138
+ } catch {}
139
+
140
+ return null;
141
+ }
142
+
143
+ export function writeState(state) {
144
+ const directory = getStateDir();
145
+ const stateFile = getStateFile();
146
+ const tmpFile = path.join(
147
+ directory,
148
+ `state.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`,
149
+ );
150
+ fs.mkdirSync(directory, { recursive: true });
151
+ fs.writeFileSync(tmpFile, JSON.stringify(normalizeState(state), null, 2));
152
+ fs.renameSync(tmpFile, stateFile);
153
+ }
154
+
155
+ export function ensureState() {
156
+ const state = readState();
157
+ if (state) {
158
+ return state;
159
+ }
160
+ const fresh = createEmptyState();
161
+ writeState(fresh);
162
+ return fresh;
163
+ }
164
+
165
+ export function updateState(mutator) {
166
+ const current = ensureState();
167
+ const draft = globalThis.structuredClone ? structuredClone(current) : JSON.parse(JSON.stringify(current));
168
+ const next = mutator(draft) ?? draft;
169
+ const normalized = normalizeState(next);
170
+ writeState(normalized);
171
+ return normalized;
172
+ }
173
+
package/src/markdown.js CHANGED
@@ -1,58 +1,28 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
 
4
- import { readForest } from "./state.js";
5
- import { renderPlainText, getWiltFactor } from "./renderer.js";
4
+ import { getEnvironmentSnapshot } from "./core/environment.js";
5
+ import { readState } from "./core/state.js";
6
+ import { renderTerminalFrame } from "./renderers/terminal.js";
6
7
 
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}**`);
8
+ export function buildMarkdown(state) {
9
+ const environment = getEnvironmentSnapshot(new Date(), state.current_streak);
10
+ const art = renderTerminalFrame(state, 80, 0, { color: false });
41
11
 
42
12
  const lines = [
43
13
  `<div align="center">`,
44
14
  ``,
45
15
  `[![honeytree](./honeytree-badge.svg)](https://github.com/Varun2009178/honeytree)`,
46
16
  ``,
47
- statParts.join(" · "),
17
+ `**${state.trees.length} trees** · **${state.animals.length} animals** · **${state.current_streak} day streak** · **${environment.season.label} ${environment.weather.label}**`,
48
18
  ``,
49
19
  "```",
50
20
  art,
51
21
  "```",
52
22
  ``,
53
- `${forest.totalPrompts} prompts over ${days} day${days === 1 ? "" : "s"}`,
23
+ `${state.total_commits} commits · ${state.total_file_saves} saves · ${state.total_minutes_coded} active minutes`,
54
24
  ``,
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>`,
25
+ `<sub>Grown with <a href="https://github.com/Varun2009178/honeytree">honeytree</a> — a living pixel forest that grows while you code</sub>`,
56
26
  ``,
57
27
  `</div>`,
58
28
  ``,
@@ -62,16 +32,15 @@ function buildMarkdown(forest) {
62
32
  }
63
33
 
64
34
  export async function generateForestMd() {
65
- const forest = readForest();
66
- if (!forest) {
67
- console.error('No forest found. Run "honeytree init" first.');
35
+ const state = readState();
36
+ if (!state) {
37
+ console.error('No Honeytree state found. Run "honeytree init" first.');
68
38
  process.exit(1);
69
39
  }
70
40
 
71
- const md = buildMarkdown(forest);
41
+ const md = buildMarkdown(state);
72
42
  const outPath = path.resolve("FOREST.md");
73
43
  fs.writeFileSync(outPath, md);
74
44
 
75
45
  console.log(`Written to ${outPath}`);
76
- console.log("Tip: run `honeytree badge` to generate the badge SVG too.");
77
46
  }