honeytree 1.0.7 → 1.0.9
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/package.json +1 -1
- package/src/badge.js +1 -1
- package/src/commands/init.js +17 -1
- package/src/commands/plant.js +1 -2
- package/src/commands/watch.js +6 -6
- package/src/core/progression.js +15 -13
- package/src/core/state.js +25 -5
- package/src/renderers/terminal.js +7 -2
package/package.json
CHANGED
package/src/badge.js
CHANGED
package/src/commands/init.js
CHANGED
|
@@ -2,7 +2,8 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { applyActiveMinutes, applyCommit } from "../core/progression.js";
|
|
6
|
+
import { ensureState, getStateFile, readState, updateState } from "../core/state.js";
|
|
6
7
|
|
|
7
8
|
function getClaudeSettingsPath() {
|
|
8
9
|
const root = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
|
|
@@ -46,6 +47,21 @@ function installClaudeHook() {
|
|
|
46
47
|
export async function init() {
|
|
47
48
|
ensureState();
|
|
48
49
|
|
|
50
|
+
// Plant a starter tree if the forest is empty, otherwise re-save to apply migrations
|
|
51
|
+
const state = readState();
|
|
52
|
+
if (state && state.trees.length === 0) {
|
|
53
|
+
updateState((draft) => applyCommit(draft));
|
|
54
|
+
} else {
|
|
55
|
+
updateState((draft) => {
|
|
56
|
+
// Seed starter wildlife based on tree count if none exist
|
|
57
|
+
if (draft.animals.length === 0 && draft.trees.length >= 3) {
|
|
58
|
+
const seedMinutes = Math.min(draft.trees.length * 6, 240);
|
|
59
|
+
applyActiveMinutes(draft, { minutes: seedMinutes });
|
|
60
|
+
}
|
|
61
|
+
return draft;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
49
65
|
const hook = process.argv.includes("--hook") || process.env.HONEYTREE_ENABLE_CLAUDE_HOOK === "1";
|
|
50
66
|
if (hook) installClaudeHook();
|
|
51
67
|
|
package/src/commands/plant.js
CHANGED
|
@@ -2,9 +2,8 @@ import { applyCommit } from "../core/progression.js";
|
|
|
2
2
|
import { updateState } from "../core/state.js";
|
|
3
3
|
|
|
4
4
|
export async function plant() {
|
|
5
|
-
const width = Number.parseInt(process.env.HONEYTREE_WIDTH ?? "80", 10) || 80;
|
|
6
5
|
const now = new Date();
|
|
7
|
-
const nextState = updateState((state) => applyCommit(state, { now
|
|
6
|
+
const nextState = updateState((state) => applyCommit(state, { now }));
|
|
8
7
|
console.log(
|
|
9
8
|
`Planted ${nextState.trees.at(-1)?.species ?? "tree"} #${nextState.total_commits} (${nextState.trees.length} total trees)`,
|
|
10
9
|
);
|
package/src/commands/watch.js
CHANGED
|
@@ -13,8 +13,8 @@ const POLL_MS = 1_000;
|
|
|
13
13
|
const h = React.createElement;
|
|
14
14
|
|
|
15
15
|
function buildCommandHint(width) {
|
|
16
|
-
const full = "
|
|
17
|
-
const compact = "
|
|
16
|
+
const full = "q: quit · honeytree export · VS Code: Honeytree sidebar · github.com/Varun2009178/honeytree";
|
|
17
|
+
const compact = "q: quit · VS Code sidebar · honeytree export";
|
|
18
18
|
if (width >= full.length + 2) {
|
|
19
19
|
return full;
|
|
20
20
|
}
|
|
@@ -60,7 +60,7 @@ function ForestWatchApp() {
|
|
|
60
60
|
useEffect(() => {
|
|
61
61
|
const activity = createActivityTracker({
|
|
62
62
|
onMinutes(minutes) {
|
|
63
|
-
const nextState = updateState((draft) => applyActiveMinutes(draft, { minutes
|
|
63
|
+
const nextState = updateState((draft) => applyActiveMinutes(draft, { minutes }));
|
|
64
64
|
setState(nextState);
|
|
65
65
|
},
|
|
66
66
|
});
|
|
@@ -72,7 +72,7 @@ function ForestWatchApp() {
|
|
|
72
72
|
activity.markActive();
|
|
73
73
|
const nextState = updateState((draft) => {
|
|
74
74
|
applyActivityPulse(draft);
|
|
75
|
-
return applyFileSave(draft
|
|
75
|
+
return applyFileSave(draft);
|
|
76
76
|
});
|
|
77
77
|
setState(nextState);
|
|
78
78
|
},
|
|
@@ -82,7 +82,7 @@ function ForestWatchApp() {
|
|
|
82
82
|
cwd: process.cwd(),
|
|
83
83
|
lastCommitHash: state.last_commit_hash,
|
|
84
84
|
async onCommit(commitHash) {
|
|
85
|
-
const nextState = updateState((draft) => applyCommit(draft, { commitHash
|
|
85
|
+
const nextState = updateState((draft) => applyCommit(draft, { commitHash }));
|
|
86
86
|
setState(nextState);
|
|
87
87
|
},
|
|
88
88
|
});
|
|
@@ -94,7 +94,7 @@ function ForestWatchApp() {
|
|
|
94
94
|
fileTracker.close().catch?.(() => {});
|
|
95
95
|
gitTracker.stop();
|
|
96
96
|
};
|
|
97
|
-
}, [
|
|
97
|
+
}, []);
|
|
98
98
|
|
|
99
99
|
const frame = useMemo(
|
|
100
100
|
() => renderTerminalFrame(state, width, tick, { now: new Date() }),
|
package/src/core/progression.js
CHANGED
|
@@ -33,22 +33,25 @@ function dayNumber(value) {
|
|
|
33
33
|
return Date.UTC(year, month - 1, day) / 86_400_000;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
export const VIRTUAL_WIDTH = 400;
|
|
37
|
+
|
|
38
|
+
function placeSprite(existing, spriteWidth, key) {
|
|
38
39
|
const half = Math.floor(spriteWidth / 2);
|
|
40
|
+
const gap = spriteWidth + 2;
|
|
39
41
|
const minX = half + 1;
|
|
40
|
-
const maxX = Math.max(minX,
|
|
42
|
+
const maxX = Math.max(minX, VIRTUAL_WIDTH - half - 2);
|
|
41
43
|
|
|
42
|
-
for (let attempt = 0; attempt <
|
|
44
|
+
for (let attempt = 0; attempt < 48; attempt += 1) {
|
|
43
45
|
const rand = seededNumber(`${key}:${attempt}`);
|
|
44
46
|
const x = Math.round(minX + rand * (maxX - minX));
|
|
45
|
-
const collides = existing.some((entry) => Math.abs(entry.x_position - x) <
|
|
46
|
-
if (!collides
|
|
47
|
+
const collides = existing.some((entry) => Math.abs(entry.x_position - x) < gap);
|
|
48
|
+
if (!collides) {
|
|
47
49
|
return x;
|
|
48
50
|
}
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
|
|
53
|
+
// Deterministic even spread as fallback
|
|
54
|
+
return minX + (existing.length * 17) % Math.max(1, maxX - minX + 1);
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
export function getNextMinuteCheckpoint(totalMinutes) {
|
|
@@ -69,7 +72,7 @@ export function updateCommitStreak(currentStreak, lastActiveDate, now = new Date
|
|
|
69
72
|
return 1;
|
|
70
73
|
}
|
|
71
74
|
|
|
72
|
-
export function applyCommit(state, { commitHash = "", now = new Date()
|
|
75
|
+
export function applyCommit(state, { commitHash = "", now = new Date() } = {}) {
|
|
73
76
|
const nextTotal = state.total_commits + 1;
|
|
74
77
|
const species = getTreeSpeciesForCommit(nextTotal);
|
|
75
78
|
const sprite = getTreeSprite(species);
|
|
@@ -80,12 +83,12 @@ export function applyCommit(state, { commitHash = "", now = new Date(), width =
|
|
|
80
83
|
state.trees.push({
|
|
81
84
|
species,
|
|
82
85
|
planted_at: now.toISOString(),
|
|
83
|
-
x_position: placeSprite(state.trees, sprite.width,
|
|
86
|
+
x_position: placeSprite(state.trees, sprite.width, `${species}:${nextTotal}`),
|
|
84
87
|
});
|
|
85
88
|
return state;
|
|
86
89
|
}
|
|
87
90
|
|
|
88
|
-
export function applyFileSave(state, { now = new Date()
|
|
91
|
+
export function applyFileSave(state, { now = new Date() } = {}) {
|
|
89
92
|
state.total_file_saves += 1;
|
|
90
93
|
if (state.total_file_saves % 50 === 0) {
|
|
91
94
|
const type = getGroundElementType(state.total_file_saves);
|
|
@@ -95,7 +98,6 @@ export function applyFileSave(state, { now = new Date(), width = 80 } = {}) {
|
|
|
95
98
|
x_position: placeSprite(
|
|
96
99
|
state.ground_elements,
|
|
97
100
|
sprite.width,
|
|
98
|
-
width,
|
|
99
101
|
`${type}:${state.total_file_saves}`,
|
|
100
102
|
),
|
|
101
103
|
});
|
|
@@ -103,7 +105,7 @@ export function applyFileSave(state, { now = new Date(), width = 80 } = {}) {
|
|
|
103
105
|
return state;
|
|
104
106
|
}
|
|
105
107
|
|
|
106
|
-
export function applyActiveMinutes(state, { minutes = 1, now = new Date()
|
|
108
|
+
export function applyActiveMinutes(state, { minutes = 1, now = new Date() } = {}) {
|
|
107
109
|
const before = state.total_minutes_coded;
|
|
108
110
|
const after = before + minutes;
|
|
109
111
|
state.total_minutes_coded = after;
|
|
@@ -114,7 +116,7 @@ export function applyActiveMinutes(state, { minutes = 1, now = new Date(), width
|
|
|
114
116
|
const sprite = getAnimalSprite(type);
|
|
115
117
|
state.animals.push({
|
|
116
118
|
type,
|
|
117
|
-
x_position: placeSprite(state.animals, sprite.width,
|
|
119
|
+
x_position: placeSprite(state.animals, sprite.width, `${type}:${checkpoint}`),
|
|
118
120
|
direction: state.animals.length % 2 === 0 ? "right" : "left",
|
|
119
121
|
});
|
|
120
122
|
checkpoint += 30;
|
package/src/core/state.js
CHANGED
|
@@ -69,8 +69,30 @@ function normalizeGroundElement(element) {
|
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
const VIRTUAL_WIDTH = 400;
|
|
73
|
+
|
|
74
|
+
function rescalePositions(items) {
|
|
75
|
+
if (!items.length) return items;
|
|
76
|
+
const maxPos = Math.max(...items.map((item) => item.x_position));
|
|
77
|
+
// Old coordinate space topped out around 130 (width=80); new space is 400.
|
|
78
|
+
// If all positions fit in the old range, rescale them.
|
|
79
|
+
if (maxPos > 0 && maxPos < VIRTUAL_WIDTH * 0.45) {
|
|
80
|
+
const scale = VIRTUAL_WIDTH / (maxPos + 10);
|
|
81
|
+
return items.map((item) => ({
|
|
82
|
+
...item,
|
|
83
|
+
x_position: Math.round(item.x_position * scale),
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
return items;
|
|
87
|
+
}
|
|
88
|
+
|
|
72
89
|
export function normalizeState(input = {}) {
|
|
73
90
|
const base = createEmptyState();
|
|
91
|
+
const trees = Array.isArray(input.trees) ? input.trees.map(normalizeTree) : [];
|
|
92
|
+
const animals = Array.isArray(input.animals) ? input.animals.map(normalizeAnimal) : [];
|
|
93
|
+
const ground_elements = Array.isArray(input.ground_elements)
|
|
94
|
+
? input.ground_elements.map(normalizeGroundElement)
|
|
95
|
+
: [];
|
|
74
96
|
return {
|
|
75
97
|
total_commits: Number.isFinite(input.total_commits)
|
|
76
98
|
? input.total_commits
|
|
@@ -85,11 +107,9 @@ export function normalizeState(input = {}) {
|
|
|
85
107
|
? input.last_active_date
|
|
86
108
|
: input.lastActiveDate ?? base.last_active_date,
|
|
87
109
|
last_commit_hash: typeof input.last_commit_hash === "string" ? input.last_commit_hash : "",
|
|
88
|
-
trees:
|
|
89
|
-
animals:
|
|
90
|
-
ground_elements:
|
|
91
|
-
? input.ground_elements.map(normalizeGroundElement)
|
|
92
|
-
: [],
|
|
110
|
+
trees: rescalePositions(trees),
|
|
111
|
+
animals: rescalePositions(animals),
|
|
112
|
+
ground_elements: rescalePositions(ground_elements),
|
|
93
113
|
};
|
|
94
114
|
}
|
|
95
115
|
|
|
@@ -2,6 +2,7 @@ import chalk from "chalk";
|
|
|
2
2
|
|
|
3
3
|
import { getDynamicScene } from "../core/animation.js";
|
|
4
4
|
import { getEnvironmentSnapshot } from "../core/environment.js";
|
|
5
|
+
import { VIRTUAL_WIDTH } from "../core/progression.js";
|
|
5
6
|
import {
|
|
6
7
|
getAnimalSprite,
|
|
7
8
|
getGroundElementSprite,
|
|
@@ -122,14 +123,18 @@ function drawSeasonParticles(buffer, width, environment, tick) {
|
|
|
122
123
|
}
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
function scaleX(virtualX, width) {
|
|
127
|
+
return Math.round((virtualX / VIRTUAL_WIDTH) * width);
|
|
128
|
+
}
|
|
129
|
+
|
|
125
130
|
function drawForest(buffer, width, state, environment, tick) {
|
|
126
131
|
const treeBaseY = SKY_ROWS + FOREST_ROWS - 1;
|
|
127
132
|
for (const tree of state.trees) {
|
|
128
|
-
compositeSprite(buffer, getTreeSprite(tree.species), tree.x_position, treeBaseY, environment.palette);
|
|
133
|
+
compositeSprite(buffer, getTreeSprite(tree.species), scaleX(tree.x_position, width), treeBaseY, environment.palette);
|
|
129
134
|
}
|
|
130
135
|
|
|
131
136
|
for (const element of state.ground_elements) {
|
|
132
|
-
compositeSprite(buffer, getGroundElementSprite(element.type), element.x_position, ART_ROWS - 1, environment.palette);
|
|
137
|
+
compositeSprite(buffer, getGroundElementSprite(element.type), scaleX(element.x_position, width), ART_ROWS - 1, environment.palette);
|
|
133
138
|
}
|
|
134
139
|
|
|
135
140
|
const scene = getDynamicScene(state, width, tick, environment);
|