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.
- package/README.md +22 -169
- package/bin/honeydew.js +9 -12
- package/package.json +15 -8
- package/src/badge.js +42 -68
- package/src/commands/export.js +23 -0
- package/src/commands/init.js +54 -0
- package/src/commands/plant.js +12 -0
- package/src/commands/stats.js +48 -0
- package/src/commands/watch.js +117 -0
- package/src/core/animation.js +135 -0
- package/src/core/environment.js +324 -0
- package/src/core/progression.js +129 -0
- package/src/core/sprites.js +388 -0
- package/src/core/state.js +173 -0
- package/src/markdown.js +13 -44
- package/src/renderers/terminal.js +225 -0
- package/src/tracker/activity.js +43 -0
- package/src/tracker/files.js +44 -0
- package/src/tracker/git.js +77 -0
- package/src/init.js +0 -86
- package/src/plant.js +0 -101
- package/src/renderer.js +0 -302
- package/src/sprites.js +0 -245
- package/src/state.js +0 -53
- package/src/viewer.js +0 -169
|
@@ -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 {
|
|
5
|
-
import {
|
|
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
|
|
8
|
-
|
|
9
|
-
|
|
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
|
`[](https://github.com/Varun2009178/honeytree)`,
|
|
46
16
|
``,
|
|
47
|
-
|
|
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
|
-
`${
|
|
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
|
|
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
|
|
66
|
-
if (!
|
|
67
|
-
console.error('No
|
|
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(
|
|
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
|
}
|