honeytree 1.0.7 → 1.0.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "honeytree",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "A living pixel art forest that grows while you code",
5
5
  "type": "module",
6
6
  "bin": {
package/src/badge.js CHANGED
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
  import { readState } from "./core/state.js";
5
5
  import { buildTerminalScene } from "./renderers/terminal.js";
6
6
 
7
- const CELL = 6;
7
+ const CELL = 8;
8
8
  const BG = "#0d1117";
9
9
  const TEXT_PAD = 18;
10
10
 
@@ -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 { ensureState, getStateFile } from "../core/state.js";
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
 
@@ -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, width }));
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
  );
@@ -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 = "init: honeytree init · watch: honeydew watch";
17
- const compact = "honeytree init · honeydew watch";
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, width }));
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, { width });
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, width }));
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
- }, [width]);
97
+ }, []);
98
98
 
99
99
  const frame = useMemo(
100
100
  () => renderTerminalFrame(state, width, tick, { now: new Date() }),
@@ -33,22 +33,25 @@ function dayNumber(value) {
33
33
  return Date.UTC(year, month - 1, day) / 86_400_000;
34
34
  }
35
35
 
36
- function placeSprite(existing, spriteWidth, width, key) {
37
- const safeWidth = Math.max(width, 40);
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, safeWidth - half - 2);
42
+ const maxX = Math.max(minX, VIRTUAL_WIDTH - half - 2);
41
43
 
42
- for (let attempt = 0; attempt < 24; attempt += 1) {
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) < half + 2);
46
- if (!collides || existing.length > Math.floor(safeWidth / 4)) {
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
- return minX + (existing.length * 7) % Math.max(1, maxX - minX + 1);
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(), width = 80 } = {}) {
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, width, `${species}:${nextTotal}`),
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(), width = 80 } = {}) {
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(), width = 80 } = {}) {
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, width, `${type}:${checkpoint}`),
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: 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
- : [],
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);