@zenbuild/zenbuild 0.1.0 → 0.2.0
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/index.js +70 -23
- package/lib/anim.js +144 -0
- package/lib/garden.js +121 -0
- package/lib/gates.js +93 -9
- package/lib/quotes-api.js +27 -0
- package/package.json +3 -2
- package/lib/breath.js +0 -37
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 0xendale
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/index.js
CHANGED
|
@@ -1,52 +1,99 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { spawn }
|
|
3
|
-
import { checkNight,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import { breathe }
|
|
7
|
-
import {
|
|
2
|
+
import { spawn } from 'child_process'
|
|
3
|
+
import { checkNight, checkBurnout, pickGate, getHistory,
|
|
4
|
+
hydrationGate, stretchGate, eyeRestGate, gratitudeGate,
|
|
5
|
+
maybePosture, readGratitude } from './lib/gates.js'
|
|
6
|
+
import { breathe, intro, ripple } from './lib/anim.js'
|
|
7
|
+
import { fetchQuote } from './lib/quotes-api.js'
|
|
8
|
+
import { growGarden, showGarden, growthNotice } from './lib/garden.js'
|
|
8
9
|
|
|
10
|
+
const VERSION = '0.2.0'
|
|
9
11
|
const RESET = '\x1b[0m'
|
|
10
12
|
const BOLD = '\x1b[1m'
|
|
11
13
|
const DIM = '\x1b[2m'
|
|
12
14
|
const GREEN = '\x1b[32m'
|
|
13
15
|
const SEP = ` ${DIM}${'─'.repeat(44)}${RESET}`
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
console.log(` ${DIM}"slow down to code faster"${RESET}`)
|
|
21
|
-
console.log(SEP)
|
|
17
|
+
const arg = process.argv[2]
|
|
18
|
+
|
|
19
|
+
if (arg === '--garden') {
|
|
20
|
+
showGarden()
|
|
21
|
+
process.exit(0)
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
if (arg === '--gratitude') {
|
|
25
|
+
const entries = readGratitude()
|
|
26
|
+
if (entries.length === 0) {
|
|
27
|
+
console.log(`\n ${DIM}no gratitude noted yet. it will come.${RESET}\n`)
|
|
28
|
+
} else {
|
|
29
|
+
console.log(`\n ${BOLD}things that worked${RESET}\n`)
|
|
30
|
+
for (const line of entries) {
|
|
31
|
+
const [date, ...rest] = line.split(' — ')
|
|
32
|
+
console.log(` ${DIM}${date.slice(0, 10)}${RESET} ${rest.join(' — ')}`)
|
|
33
|
+
}
|
|
34
|
+
console.log()
|
|
35
|
+
}
|
|
36
|
+
process.exit(0)
|
|
37
|
+
}
|
|
25
38
|
|
|
26
|
-
if (!
|
|
39
|
+
if (!arg) {
|
|
27
40
|
console.log(`\n ${BOLD}Usage:${RESET} zenbuild ${DIM}"<your build command>"${RESET}`)
|
|
28
|
-
console.log(` ${DIM}Example: zenbuild "npm run build"${RESET}
|
|
41
|
+
console.log(` ${DIM}Example: zenbuild "npm run build"${RESET}`)
|
|
42
|
+
console.log(` ${DIM}Also: zenbuild --garden · zenbuild --gratitude${RESET}\n`)
|
|
29
43
|
process.exit(1)
|
|
30
44
|
}
|
|
31
45
|
|
|
32
|
-
|
|
46
|
+
await intro(VERSION)
|
|
47
|
+
console.log(SEP)
|
|
33
48
|
checkNight()
|
|
34
49
|
console.log(SEP)
|
|
35
50
|
await checkBurnout()
|
|
51
|
+
maybePosture()
|
|
36
52
|
console.log(SEP)
|
|
53
|
+
|
|
54
|
+
const quotePromise = fetchQuote() // resolves during the breathing ritual
|
|
37
55
|
await breathe()
|
|
38
56
|
console.log(SEP)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
57
|
+
|
|
58
|
+
const gate = process.stdout.isTTY ? pickGate(getHistory()) : null
|
|
59
|
+
if (gate === 'eye-rest') await eyeRestGate()
|
|
60
|
+
else if (gate === 'hydration') {
|
|
61
|
+
await ripple()
|
|
62
|
+
await hydrationGate()
|
|
63
|
+
}
|
|
64
|
+
else if (gate === 'stretch') await stretchGate()
|
|
65
|
+
else if (gate === 'gratitude') await gratitudeGate()
|
|
66
|
+
if (gate) console.log(SEP)
|
|
67
|
+
|
|
68
|
+
const quote = await quotePromise
|
|
69
|
+
console.log(`\n ${BOLD}✦ "${quote.text}"${RESET}`)
|
|
70
|
+
if (quote.author) console.log(` ${DIM}— ${quote.author}${RESET}`)
|
|
71
|
+
console.log()
|
|
42
72
|
await new Promise(r => setTimeout(r, 1500))
|
|
43
73
|
console.log(SEP)
|
|
44
74
|
console.log(`\n ${GREEN}▶ Okay okay, building now...${RESET}\n`)
|
|
45
75
|
|
|
46
|
-
const child = spawn(
|
|
76
|
+
const child = spawn(arg, { shell: true, stdio: 'inherit' })
|
|
77
|
+
|
|
78
|
+
child.on('close', code => {
|
|
79
|
+
const exitCode = code ?? 0
|
|
80
|
+
if (exitCode === 0) {
|
|
81
|
+
const growth = growGarden()
|
|
82
|
+
if (growth) {
|
|
83
|
+
console.log(`\n ${GREEN}${growthNotice(growth.type, growth.total)}${RESET}`)
|
|
84
|
+
if (growth.completed) {
|
|
85
|
+
console.log(` ${DIM}your garden is complete. it was never about the garden.${RESET}\n`)
|
|
86
|
+
} else {
|
|
87
|
+
console.log(` ${DIM}zenbuild --garden to visit${RESET}\n`)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
console.log(`\n ${DIM}breathe. read the error. it's telling you something.${RESET}\n`)
|
|
92
|
+
}
|
|
93
|
+
process.exit(exitCode)
|
|
94
|
+
})
|
|
47
95
|
|
|
48
|
-
child.on('
|
|
49
|
-
child.on('error', err => {
|
|
96
|
+
child.on('error', err => {
|
|
50
97
|
console.error(`\n ${BOLD}zenbuild:${RESET} could not run command — ${err.message}\n`)
|
|
51
98
|
process.exit(1)
|
|
52
99
|
})
|
package/lib/anim.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// lib/anim.js
|
|
2
|
+
import { stdout } from 'process'
|
|
3
|
+
|
|
4
|
+
const RESET = '\x1b[0m'
|
|
5
|
+
const BOLD = '\x1b[1m'
|
|
6
|
+
const DIM = '\x1b[2m'
|
|
7
|
+
const GREEN = '\x1b[32m'
|
|
8
|
+
|
|
9
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
|
10
|
+
|
|
11
|
+
// ── breathing circle ──────────────────────────────────────────────────────────
|
|
12
|
+
// 5 frames, radius 1..5. Fixed 5-line height and constant line width so the
|
|
13
|
+
// animation can redraw in place with cursor-up.
|
|
14
|
+
|
|
15
|
+
const CIRCLE_FRAMES = [
|
|
16
|
+
[
|
|
17
|
+
' ',
|
|
18
|
+
' ',
|
|
19
|
+
' ◯ ',
|
|
20
|
+
' ',
|
|
21
|
+
' ',
|
|
22
|
+
],
|
|
23
|
+
[
|
|
24
|
+
' ',
|
|
25
|
+
' · ',
|
|
26
|
+
' · ◯ · ',
|
|
27
|
+
' · ',
|
|
28
|
+
' ',
|
|
29
|
+
],
|
|
30
|
+
[
|
|
31
|
+
' · · ',
|
|
32
|
+
' · · ',
|
|
33
|
+
' · ◯ · ',
|
|
34
|
+
' · · ',
|
|
35
|
+
' · · ',
|
|
36
|
+
],
|
|
37
|
+
[
|
|
38
|
+
' ∙ ∙ ∙ ',
|
|
39
|
+
' ∙ ∙ ',
|
|
40
|
+
' ∙ ◯ ∙ ',
|
|
41
|
+
' ∙ ∙ ',
|
|
42
|
+
' ∙ ∙ ∙ ',
|
|
43
|
+
],
|
|
44
|
+
[
|
|
45
|
+
' ∙ ∙ ∙ ',
|
|
46
|
+
' ∙ ∙ ',
|
|
47
|
+
' ∙ ◯ ∙ ',
|
|
48
|
+
' ∙ ∙ ',
|
|
49
|
+
' ∙ ∙ ∙ ',
|
|
50
|
+
],
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
export function circleFrame(radius) {
|
|
54
|
+
const r = Math.min(Math.max(radius, 1), 5)
|
|
55
|
+
return CIRCLE_FRAMES[r - 1].join('\n')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 5 circle lines + blank + label line
|
|
59
|
+
const FRAME_H = 7
|
|
60
|
+
|
|
61
|
+
function drawBreathFrame(radius, label, dim = false) {
|
|
62
|
+
const tint = dim ? DIM : GREEN
|
|
63
|
+
const block = circleFrame(radius)
|
|
64
|
+
.split('\n')
|
|
65
|
+
.map(l => ` ${tint}${l}${RESET}\x1b[K`)
|
|
66
|
+
.join('\n')
|
|
67
|
+
stdout.write(`\x1b[${FRAME_H}A\r` + block + `\n\n ${BOLD}${label.padEnd(12)}${RESET}\x1b[K\n`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function animateBreath() {
|
|
71
|
+
stdout.write('\n'.repeat(FRAME_H))
|
|
72
|
+
for (let r = 1; r <= 5; r++) { // inhale: 4s
|
|
73
|
+
drawBreathFrame(r, 'Inhale...')
|
|
74
|
+
await sleep(800)
|
|
75
|
+
}
|
|
76
|
+
for (let i = 0; i < 4; i++) { // hold: 2s, pulse
|
|
77
|
+
drawBreathFrame(5, 'Hold...', i % 2 === 1)
|
|
78
|
+
await sleep(500)
|
|
79
|
+
}
|
|
80
|
+
for (let r = 5; r >= 1; r--) { // exhale: 4s
|
|
81
|
+
drawBreathFrame(r, 'Exhale...')
|
|
82
|
+
await sleep(800)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function breathe() {
|
|
87
|
+
console.log(`\n 🫁 ${BOLD}Breathe with me for a moment...${RESET}\n`)
|
|
88
|
+
if (!stdout.isTTY) {
|
|
89
|
+
for (const [label, ms] of [['Inhale...', 4000], ['Hold...', 2000], ['Exhale...', 4000]]) {
|
|
90
|
+
console.log(` ${label}`)
|
|
91
|
+
await sleep(ms)
|
|
92
|
+
}
|
|
93
|
+
console.log()
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
await animateBreath()
|
|
97
|
+
console.log()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── animated intro ────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
const TAGLINE = '"slow down to code faster"'
|
|
103
|
+
|
|
104
|
+
export async function intro(version) {
|
|
105
|
+
console.log()
|
|
106
|
+
if (!stdout.isTTY) {
|
|
107
|
+
console.log(` ${DIM}( zen )${RESET}`)
|
|
108
|
+
console.log(` ${DIM}∿∿∿ ◯ ∿∿∿${RESET}`)
|
|
109
|
+
console.log(` ${BOLD}zenbuild v${version}${RESET}`)
|
|
110
|
+
console.log(` ${DIM}${TAGLINE}${RESET}`)
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
const waveFrames = [
|
|
114
|
+
` ${DIM}·∿· · ·∿·${RESET}`,
|
|
115
|
+
` ${DIM}∿∿· ○ ·∿∿${RESET}`,
|
|
116
|
+
` ${DIM}∿∿∿ ◯ ∿∿∿${RESET}`,
|
|
117
|
+
]
|
|
118
|
+
console.log(` ${DIM}( zen )${RESET}`)
|
|
119
|
+
stdout.write('\n')
|
|
120
|
+
for (const frame of waveFrames) {
|
|
121
|
+
stdout.write(`\x1b[1A\r${frame}\x1b[K\n`)
|
|
122
|
+
await sleep(250)
|
|
123
|
+
}
|
|
124
|
+
console.log(` ${BOLD}zenbuild v${version}${RESET}`)
|
|
125
|
+
stdout.write(` ${DIM}`)
|
|
126
|
+
for (const ch of TAGLINE) { // type out, ~30ms/char
|
|
127
|
+
stdout.write(ch)
|
|
128
|
+
await sleep(25)
|
|
129
|
+
}
|
|
130
|
+
stdout.write(`${RESET}\n`)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── water ripple (hydration gate opener) ──────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
export async function ripple() {
|
|
136
|
+
if (!stdout.isTTY) return
|
|
137
|
+
const frames = [' 💧', ' ◦', ' ◦ ◯ ◦', ' ◦ ◯ ◦']
|
|
138
|
+
stdout.write('\n')
|
|
139
|
+
for (const f of frames) {
|
|
140
|
+
stdout.write(`\r\x1b[K ${DIM}${f}${RESET}`)
|
|
141
|
+
await sleep(200)
|
|
142
|
+
}
|
|
143
|
+
stdout.write('\r\x1b[K')
|
|
144
|
+
}
|
package/lib/garden.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// lib/garden.js
|
|
2
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'
|
|
3
|
+
import { homedir } from 'os'
|
|
4
|
+
import { join } from 'path'
|
|
5
|
+
|
|
6
|
+
const RESET = '\x1b[0m'
|
|
7
|
+
const BOLD = '\x1b[1m'
|
|
8
|
+
const DIM = '\x1b[2m'
|
|
9
|
+
|
|
10
|
+
const GARDEN_DIR = join(homedir(), '.zenbuild')
|
|
11
|
+
const GARDEN_FILE = join(GARDEN_DIR, 'garden.json')
|
|
12
|
+
|
|
13
|
+
export const GRID_W = 44
|
|
14
|
+
export const GRID_H = 6
|
|
15
|
+
export const MAX_ELEMENTS = 40
|
|
16
|
+
|
|
17
|
+
const CYCLE = ['stone', 'sprout', 'grass', 'bamboo', 'tree', 'lotus']
|
|
18
|
+
const GLYPHS = {
|
|
19
|
+
stone: '◦', sprout: '🌱', grass: 'ⁿ', bamboo: '┃',
|
|
20
|
+
tree: '🌲', lotus: '✿', koi: '🐟',
|
|
21
|
+
}
|
|
22
|
+
const NOTICES = {
|
|
23
|
+
stone: 'a stone settles in your garden',
|
|
24
|
+
sprout: 'a sprout takes root in your garden',
|
|
25
|
+
grass: 'grass whispers up through the sand',
|
|
26
|
+
bamboo: 'bamboo rises in your garden',
|
|
27
|
+
tree: 'a tree stands quietly in your garden',
|
|
28
|
+
lotus: 'a lotus blooms in your garden',
|
|
29
|
+
koi: 'a koi arrives — rare and unbothered',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── pure core ─────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
export function nextElementType(count) {
|
|
35
|
+
if ((count + 1) % 10 === 0) return 'koi'
|
|
36
|
+
return CYCLE[count % CYCLE.length]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function parseGarden(raw) {
|
|
40
|
+
try {
|
|
41
|
+
const data = JSON.parse(raw)
|
|
42
|
+
if (!Array.isArray(data.elements)) throw new Error('bad shape')
|
|
43
|
+
return {
|
|
44
|
+
elements: data.elements,
|
|
45
|
+
totalBuilds: Number(data.totalBuilds) || 0,
|
|
46
|
+
plantedAt: data.plantedAt || new Date().toISOString(),
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
return { elements: [], totalBuilds: 0, plantedAt: new Date().toISOString() }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function placeElement(elements, rng = Math.random) {
|
|
54
|
+
for (let i = 0; i < 200; i++) {
|
|
55
|
+
const x = Math.floor(rng() * GRID_W)
|
|
56
|
+
const y = Math.floor(rng() * GRID_H)
|
|
57
|
+
if (!elements.some(e => e.x === x && e.y === y)) return { x, y }
|
|
58
|
+
}
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function renderGarden(state) {
|
|
63
|
+
const grid = Array.from({ length: GRID_H }, () => Array(GRID_W).fill(`${DIM}˜${RESET}`))
|
|
64
|
+
for (const e of state.elements) {
|
|
65
|
+
if (e.y >= 0 && e.y < GRID_H && e.x >= 0 && e.x < GRID_W) {
|
|
66
|
+
grid[e.y][e.x] = GLYPHS[e.type] || '◦'
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return grid.map(row => ' ' + row.join('')).join('\n')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function growthNotice(type, total) {
|
|
73
|
+
return `${GLYPHS[type]} ${NOTICES[type]} (${total} element${total === 1 ? '' : 's'})`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── IO shell ──────────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
function loadGarden() {
|
|
79
|
+
try {
|
|
80
|
+
return parseGarden(readFileSync(GARDEN_FILE, 'utf8'))
|
|
81
|
+
} catch {
|
|
82
|
+
return parseGarden('')
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function saveGarden(state, file = GARDEN_FILE) {
|
|
87
|
+
if (!existsSync(GARDEN_DIR)) mkdirSync(GARDEN_DIR, { recursive: true })
|
|
88
|
+
writeFileSync(file, JSON.stringify(state, null, 2))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Adds one element after a successful build. Archives + resets a full garden.
|
|
92
|
+
// Silent null on any failure — the garden never blocks a build.
|
|
93
|
+
export function growGarden(rng = Math.random) {
|
|
94
|
+
try {
|
|
95
|
+
const state = loadGarden()
|
|
96
|
+
const type = nextElementType(state.elements.length)
|
|
97
|
+
const pos = placeElement(state.elements, rng)
|
|
98
|
+
if (!pos) return null
|
|
99
|
+
state.elements.push({ type, x: pos.x, y: pos.y, addedAt: new Date().toISOString() })
|
|
100
|
+
state.totalBuilds += 1
|
|
101
|
+
const completed = state.elements.length >= MAX_ELEMENTS
|
|
102
|
+
if (completed) {
|
|
103
|
+
let n = 1
|
|
104
|
+
while (existsSync(join(GARDEN_DIR, `garden-${n}.json`))) n++
|
|
105
|
+
saveGarden(state, join(GARDEN_DIR, `garden-${n}.json`))
|
|
106
|
+
saveGarden({ elements: [], totalBuilds: state.totalBuilds, plantedAt: new Date().toISOString() })
|
|
107
|
+
} else {
|
|
108
|
+
saveGarden(state)
|
|
109
|
+
}
|
|
110
|
+
return { type, total: state.elements.length, completed }
|
|
111
|
+
} catch {
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function showGarden() {
|
|
117
|
+
const state = loadGarden()
|
|
118
|
+
console.log(`\n ${BOLD}your zen garden${RESET}\n`)
|
|
119
|
+
console.log(renderGarden(state))
|
|
120
|
+
console.log(`\n ${DIM}${state.elements.length} elements · ${state.totalBuilds} mindful builds · planted ${state.plantedAt.slice(0, 10)}${RESET}\n`)
|
|
121
|
+
}
|
package/lib/gates.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// lib/gates.js
|
|
2
2
|
import { createInterface } from 'readline'
|
|
3
|
-
import { readFileSync, writeFileSync,
|
|
3
|
+
import { readFileSync, writeFileSync, appendFileSync,
|
|
4
4
|
mkdirSync, existsSync } from 'fs'
|
|
5
5
|
import { homedir } from 'os'
|
|
6
6
|
import { join } from 'path'
|
|
@@ -15,6 +15,8 @@ const HISTORY_DIR = join(homedir(), '.zenbuild')
|
|
|
15
15
|
const HISTORY_FILE = join(HISTORY_DIR, 'history.json')
|
|
16
16
|
const TEN_MIN_MS = 10 * 60 * 1000
|
|
17
17
|
const MAX_BUILDS = 5
|
|
18
|
+
const ONE_HOUR_MS = 60 * 60 * 1000
|
|
19
|
+
const GRATITUDE_FILE = join(HISTORY_DIR, 'gratitude.log')
|
|
18
20
|
|
|
19
21
|
// ── internal helpers ──────────────────────────────────────────────────────────
|
|
20
22
|
|
|
@@ -62,9 +64,10 @@ export function checkNight() {
|
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
export async function checkBurnout() {
|
|
65
|
-
const now
|
|
66
|
-
const raw
|
|
67
|
-
const
|
|
67
|
+
const now = Date.now()
|
|
68
|
+
const raw = readHistory()
|
|
69
|
+
const pastHour = raw.filter(ts => now - new Date(ts).getTime() < ONE_HOUR_MS)
|
|
70
|
+
const recent = pastHour.filter(ts => now - new Date(ts).getTime() < TEN_MIN_MS)
|
|
68
71
|
|
|
69
72
|
if (recent.length >= MAX_BUILDS) {
|
|
70
73
|
console.log(`\n ${YELLOW}🔥 Five builds in ten minutes.`)
|
|
@@ -76,15 +79,29 @@ export async function checkBurnout() {
|
|
|
76
79
|
process.exit(0)
|
|
77
80
|
}
|
|
78
81
|
|
|
79
|
-
|
|
80
|
-
writeHistory(
|
|
82
|
+
pastHour.push(new Date().toISOString())
|
|
83
|
+
writeHistory(pastHour)
|
|
81
84
|
}
|
|
82
85
|
|
|
83
|
-
export
|
|
84
|
-
|
|
86
|
+
export function getHistory() {
|
|
87
|
+
return readHistory()
|
|
88
|
+
}
|
|
85
89
|
|
|
86
|
-
|
|
90
|
+
// One roll per run. Eye-rest is not random: it fires on every 3rd build in
|
|
91
|
+
// the rolling hour (3rd, 6th, 9th ...) and replaces the roll entirely.
|
|
92
|
+
export function pickGate(history, rng = Math.random, now = Date.now()) {
|
|
93
|
+
const pastHour = history.filter(ts => now - new Date(ts).getTime() < ONE_HOUR_MS)
|
|
94
|
+
if (pastHour.length >= 3 && pastHour.length % 3 === 0) return 'eye-rest'
|
|
95
|
+
const roll = rng()
|
|
96
|
+
if (roll < 0.10) return 'hydration'
|
|
97
|
+
if (roll < 0.20) return 'stretch'
|
|
98
|
+
if (roll < 0.25) return 'gratitude'
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
87
101
|
|
|
102
|
+
// Hydration prompt without the probability roll — pickGate owns probability.
|
|
103
|
+
export async function hydrationGate() {
|
|
104
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
88
105
|
return new Promise(resolve => {
|
|
89
106
|
const ask = () => {
|
|
90
107
|
rl.question(
|
|
@@ -110,3 +127,70 @@ export async function checkHydration() {
|
|
|
110
127
|
ask()
|
|
111
128
|
})
|
|
112
129
|
}
|
|
130
|
+
|
|
131
|
+
const STRETCHES = [
|
|
132
|
+
'roll your neck slowly, side to side',
|
|
133
|
+
'shrug your shoulders up... and let them drop',
|
|
134
|
+
'circle your wrists — builders need their wrists',
|
|
135
|
+
'stand up and reach for the ceiling',
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
export async function stretchGate(rng = Math.random) {
|
|
139
|
+
const s = STRETCHES[Math.floor(rng() * STRETCHES.length)]
|
|
140
|
+
console.log(`\n ${GREEN}🧘 quick stretch: ${s}${RESET}\n`)
|
|
141
|
+
await countdown(15)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function eyeRestGate() {
|
|
145
|
+
console.log(`\n ${GREEN}👀 you've been at this a while.`)
|
|
146
|
+
console.log(` look at something 20 feet away. just 20 seconds.${RESET}\n`)
|
|
147
|
+
await countdown(20)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function formatGratitudeEntry(text, date = new Date()) {
|
|
151
|
+
return `${date.toISOString()} — ${text}\n`
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function appendGratitude(text) {
|
|
155
|
+
try {
|
|
156
|
+
if (!existsSync(HISTORY_DIR)) mkdirSync(HISTORY_DIR, { recursive: true })
|
|
157
|
+
appendFileSync(GRATITUDE_FILE, formatGratitudeEntry(text))
|
|
158
|
+
} catch {} // gratitude never blocks a build
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function gratitudeGate() {
|
|
162
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
163
|
+
return new Promise(resolve => {
|
|
164
|
+
rl.question(`\n ${GREEN}🙏 name one thing that worked today: ${RESET}`, (answer) => {
|
|
165
|
+
rl.close()
|
|
166
|
+
const text = answer.trim()
|
|
167
|
+
if (text) {
|
|
168
|
+
appendGratitude(text)
|
|
169
|
+
console.log(`\n ${DIM}noted. carry it with you.${RESET}`)
|
|
170
|
+
}
|
|
171
|
+
resolve()
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function readGratitude() {
|
|
177
|
+
try {
|
|
178
|
+
return readFileSync(GRATITUDE_FILE, 'utf8').trim().split('\n').filter(Boolean).reverse()
|
|
179
|
+
} catch {
|
|
180
|
+
return []
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const POSTURE_LINES = [
|
|
185
|
+
'🧘 unclench your jaw. drop your shoulders. sit tall.',
|
|
186
|
+
'🧘 feet flat. spine long. screen at eye level.',
|
|
187
|
+
'🧘 relax your grip on the mouse. it already surrendered.',
|
|
188
|
+
]
|
|
189
|
+
|
|
190
|
+
// Non-blocking flavor, not a gate: ~15% chance, one dim line, no pause.
|
|
191
|
+
export function maybePosture(rng = Math.random) {
|
|
192
|
+
if (rng() < 0.15) {
|
|
193
|
+
const line = POSTURE_LINES[Math.floor(rng() * POSTURE_LINES.length)]
|
|
194
|
+
console.log(`\n ${DIM}${line}${RESET}`)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// lib/quotes-api.js
|
|
2
|
+
import { randomQuote } from './snippets.js'
|
|
3
|
+
|
|
4
|
+
const API_URL = 'https://zenquotes.io/api/random'
|
|
5
|
+
|
|
6
|
+
// Never rejects, never blocks longer than timeoutMs: any failure falls back
|
|
7
|
+
// silently to the local pool. fetchImpl is injectable for tests.
|
|
8
|
+
export async function fetchQuote(fetchImpl = globalThis.fetch, timeoutMs = 2000) {
|
|
9
|
+
const ctrl = new AbortController()
|
|
10
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs)
|
|
11
|
+
try {
|
|
12
|
+
const res = await fetchImpl(API_URL, { signal: ctrl.signal })
|
|
13
|
+
if (!res.ok) throw new Error(`status ${res.status}`)
|
|
14
|
+
const data = await res.json()
|
|
15
|
+
const q = data?.[0]?.q
|
|
16
|
+
const a = data?.[0]?.a
|
|
17
|
+
if (typeof q !== 'string' || !q.trim()) throw new Error('bad payload')
|
|
18
|
+
return {
|
|
19
|
+
text: q.trim(),
|
|
20
|
+
author: typeof a === 'string' && a.trim() ? a.trim() : null,
|
|
21
|
+
}
|
|
22
|
+
} catch {
|
|
23
|
+
return { text: randomQuote(), author: null }
|
|
24
|
+
} finally {
|
|
25
|
+
clearTimeout(timer)
|
|
26
|
+
}
|
|
27
|
+
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenbuild/zenbuild",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Slow down to code faster.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/0xendale/zenbuild.git"
|
|
9
9
|
},
|
|
10
|
+
"homepage": "https://github.com/0xendale/zenbuild#readme",
|
|
10
11
|
"bin": {
|
|
11
12
|
"zenbuild": "./index.js"
|
|
12
13
|
},
|
|
13
14
|
"scripts": {
|
|
14
|
-
"test": "node --test
|
|
15
|
+
"test": "node --test"
|
|
15
16
|
},
|
|
16
17
|
"files": [
|
|
17
18
|
"index.js",
|
package/lib/breath.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
// lib/breath.js
|
|
2
|
-
import { stdout } from 'process'
|
|
3
|
-
|
|
4
|
-
const RESET = '\x1b[0m'
|
|
5
|
-
const GREEN = '\x1b[32m'
|
|
6
|
-
const DIM = '\x1b[2m'
|
|
7
|
-
const BOLD = '\x1b[1m'
|
|
8
|
-
const BAR_W = 30
|
|
9
|
-
|
|
10
|
-
function renderBar(filled) {
|
|
11
|
-
return GREEN + '█'.repeat(filled) + DIM + '░'.repeat(BAR_W - filled) + RESET
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function animatePhase(label, durationMs) {
|
|
15
|
-
const stepMs = durationMs / BAR_W
|
|
16
|
-
let filled = 0
|
|
17
|
-
|
|
18
|
-
return new Promise(resolve => {
|
|
19
|
-
const timer = setInterval(() => {
|
|
20
|
-
filled++
|
|
21
|
-
stdout.write(`\r ${BOLD}${label.padEnd(12)}${RESET} [${renderBar(filled)}]`)
|
|
22
|
-
if (filled >= BAR_W) {
|
|
23
|
-
clearInterval(timer)
|
|
24
|
-
stdout.write('\n')
|
|
25
|
-
resolve()
|
|
26
|
-
}
|
|
27
|
-
}, stepMs)
|
|
28
|
-
})
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export async function breathe() {
|
|
32
|
-
console.log(`\n 🫁 ${BOLD}Breathe with me for a moment...${RESET}\n`)
|
|
33
|
-
await animatePhase('Inhale...', 4000)
|
|
34
|
-
await animatePhase('Hold...', 2000)
|
|
35
|
-
await animatePhase('Exhale...', 4000)
|
|
36
|
-
console.log()
|
|
37
|
-
}
|