go-touch-grass 1.0.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 touch-grass contributors
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/README.md ADDED
@@ -0,0 +1,283 @@
1
+ <div align="center">
2
+
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="./touch_grass_logo_dark.png" />
5
+ <source media="(prefers-color-scheme: light)" srcset="./toucn_grass_logo.png" />
6
+ <img src="./touch_grass_logo.png" alt="go-touch-grass CLI logo — a mouse cursor touching grass blades, representing developer break reminders" width="280" />
7
+ </picture>
8
+
9
+ # go-touch-grass
10
+
11
+ ### A sarcastic CLI that reminds developers to go outside, touch grass, and stop staring at screens
12
+
13
+ Track your outdoor streak, earn milestones, and share your grass-touching stats on Twitter/X, LinkedIn, and Instagram — all from your terminal.
14
+
15
+ [![npm version](https://img.shields.io/npm/v/go-touch-grass.svg?style=flat-square)](https://www.npmjs.com/package/go-touch-grass)
16
+ [![npm downloads](https://img.shields.io/npm/dm/go-touch-grass.svg?style=flat-square)](https://www.npmjs.com/package/go-touch-grass)
17
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](./LICENSE)
18
+ [![Node.js Version](https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg?style=flat-square)](https://nodejs.org/)
19
+
20
+ [**Get Started**](#-installation) · [**Features**](#-features) · [**Usage**](#-usage) · [**Commands**](#-command-reference)
21
+
22
+ </div>
23
+
24
+ ---
25
+
26
+ ## What is go-touch-grass?
27
+
28
+ **go-touch-grass** is a zero-config Node.js CLI tool that nudges developers to take outdoor breaks. Run one command and you'll see colorful ASCII art, get roasted with a sarcastic message, receive a 10-minute outdoor assignment, and optionally share your streak on social media. It tracks your daily grass-touching habit locally and rewards consistency with milestone achievements.
29
+
30
+ ```bash
31
+ npx go-touch-grass
32
+ ```
33
+
34
+ > **One command. One break. Every day.**
35
+
36
+ ---
37
+
38
+ ## 📦 Installation
39
+
40
+ ### Global install (recommended for daily use)
41
+
42
+ ```bash
43
+ npm install -g go-touch-grass
44
+ go-touch-grass
45
+ ```
46
+
47
+ ### One-time execution (zero install)
48
+
49
+ ```bash
50
+ npx go-touch-grass
51
+ ```
52
+
53
+ ### Local project dependency
54
+
55
+ ```bash
56
+ npm install go-touch-grass
57
+ npx go-touch-grass
58
+ ```
59
+
60
+ ---
61
+
62
+ ## 🎯 Features
63
+
64
+ | Feature | Description |
65
+ | ------------------------------ | -------------------------------------------------------------------------------------------------------- |
66
+ | **31 Sarcastic Messages** | A different quip every time — all about your screen-addicted life choices |
67
+ | **3 ASCII Art Scenes** | Meadow, park, and mountain environments rendered in your terminal |
68
+ | **Persistent Streak Tracking** | Your history lives in `~/.config/go-touch-grass/` (Linux/Mac) or `%APPDATA%\\go-touch-grass\\` (Windows) |
69
+ | **Milestone Achievements** | Unlock special messages at 1, 5, 10, 25, 50, and 100 touches |
70
+ | **10-Minute Countdown Timer** | Optional animated progress bar for your outdoor assignment |
71
+ | **Social Media Sharing** | Post your streak to **Twitter/X**, **LinkedIn**, or **Instagram** |
72
+ | **8.5 kB Package Size** | Tiny footprint — optimized for `npx` cold starts |
73
+ | **ESM-Native** | Pure ES modules, no CommonJS |
74
+
75
+ ---
76
+
77
+ ## 🚀 Usage
78
+
79
+ ### Full experience
80
+
81
+ ```bash
82
+ npx go-touch-grass
83
+ ```
84
+
85
+ Runs the complete flow:
86
+
87
+ 1. Displays a random ASCII art outdoor scene
88
+ 2. Delivers a sarcastic message about your screen time
89
+ 3. Shows your current streak, total touches, and longest streak
90
+ 4. Assigns a 10-minute outdoor break with optional countdown
91
+ 5. Offers to share your achievement on social media
92
+
93
+ ### Check your streak stats
94
+
95
+ ```bash
96
+ npx go-touch-grass --streak
97
+ # or
98
+ npx go-touch-grass -s
99
+ ```
100
+
101
+ ### Share to social media
102
+
103
+ ```bash
104
+ npx go-touch-grass --share
105
+ # or
106
+ npx go-touch-grass -S
107
+ ```
108
+
109
+ Choose your platform interactively:
110
+
111
+ - **Twitter/X** — Tweet your grass touching streak
112
+ - **LinkedIn** — Share your touching grass acheivements, professionally
113
+ - **Instagram** — Copy text for a post
114
+ - **Skip** — Keep it private
115
+
116
+ ### Skip optional features
117
+
118
+ ```bash
119
+ npx go-touch-grass --noTimer # Skip the countdown timer
120
+ npx go-touch-grass --noShare # Skip the social media prompt
121
+ npx go-touch-grass --noTimer --noShare # Minimalist mode
122
+ ```
123
+
124
+ ### Show help
125
+
126
+ ```bash
127
+ npx go-touch-grass --help
128
+ ```
129
+
130
+ ---
131
+
132
+ ## 📸 Example Output
133
+
134
+ ```
135
+ ^ * *
136
+ /|\ *
137
+ / | \
138
+ ___/ | \___
139
+ ( touch )
140
+ \ grass /
141
+ \ /
142
+ ~~~~~~~~~~~~~~~~~~~~
143
+
144
+ ────────────────────────────────────────────────────────────
145
+ >>> You have been in dark mode for 14 hours. The sun has better rendering.
146
+
147
+ ────────────────────────────────────────────────────────────
148
+ streak: 5 days | total: 42 touches | longest: 12 days
149
+
150
+ ────────────────────────────────────────────────────────────
151
+
152
+ ╭─────────────────────────────────╮
153
+ │ │
154
+ │ ⏱ YOUR OUTDOOR ASSIGNMENT │
155
+ │ │
156
+ │ Duration: 10 minutes │
157
+ │ Return by: 3:45 PM │
158
+ │ │
159
+ │ Do NOT touch your keyboard. │
160
+ │ Do NOT check Slack. │
161
+ │ Touch grass. Breathe air. │
162
+ │ │
163
+ ╰─────────────────────────────────╯
164
+
165
+ ? Share your achievement on social media? (y/N)
166
+ ```
167
+
168
+ ---
169
+
170
+ ## 📊 Social Sharing Examples
171
+
172
+ Each share message is randomized. Here are some examples:
173
+
174
+ ```
175
+ 🌿 just touched grass ✅ you should too! try: npx go-touch-grass
176
+
177
+ 🔥 day 7 of touching grass as a developer. it gets easier. npx go-touch-grass
178
+
179
+ 💪 7 day grass-touching streak 🌿 my productivity is actually up??? npx go-touch-grass
180
+
181
+ 🧠 went outside. the solution was there the whole time. literally in the air. npx go-touch-grass
182
+
183
+ ☀️ 30 consecutive days of touching grass. my therapist is proud. npx go-touch-grass
184
+ ```
185
+
186
+ ---
187
+
188
+ ## 🗂️ Data Storage
189
+
190
+ Streak data is stored locally — no cloud, no telemetry, no accounts.
191
+
192
+ | Platform | Config path |
193
+ | ----------------- | ---------------------------------------- |
194
+ | **Linux / macOS** | `~/.config/go-touch-grass/config.json` |
195
+ | **Windows** | `%APPDATA%\\go-touch-grass\\config.json` |
196
+
197
+ <details>
198
+ <summary>Example config file</summary>
199
+
200
+ ```json
201
+ {
202
+ "count": 42,
203
+ "lastVisit": "2026-03-01",
204
+ "currentStreak": 7,
205
+ "longestStreak": 12
206
+ }
207
+ ```
208
+
209
+ </details>
210
+
211
+ ---
212
+
213
+ ## 🎯 Command Reference
214
+
215
+ | Command | Alias | Description |
216
+ | ------------------------------ | ----- | ------------------------ |
217
+ | `npx go-touch-grass` | — | Run the full experience |
218
+ | `npx go-touch-grass --streak` | `-s` | Show streak stats |
219
+ | `npx go-touch-grass --share` | `-S` | Open social sharing menu |
220
+ | `npx go-touch-grass --noTimer` | — | Skip the countdown timer |
221
+ | `npx go-touch-grass --noShare` | — | Skip social media prompt |
222
+ | `npx go-touch-grass --help` | — | Print help text |
223
+ | `npx go-touch-grass --version` | — | Print version number |
224
+
225
+ ---
226
+
227
+ ## 💡 Tips
228
+
229
+ - **Only one touch per day is counted.** Running it multiple times on the same day won't inflate your stats.
230
+ - **Streaks reset after one missed day.** Consistency is the point.
231
+ - **Vary your sharing platform.** LinkedIn for professional audiences, Twitter for unhinged honesty.
232
+ - **Use `--noTimer` when you're short on time.** The outdoor assignment is optional.
233
+ - **Pipe-friendly.** `go-go-go-touch-grass | tee grass.log` works for logging your sessions.
234
+
235
+ ---
236
+
237
+ ## 📚 How It Works Internally
238
+
239
+ 1. Compares today's date to your last visit
240
+ 2. Increments your streak if it's a new day (idempotent on repeat runs)
241
+ 3. Renders a random ASCII art outdoor scene
242
+ 4. Picks one of 31 sarcastic messages
243
+ 5. Displays your streak, total touches, and longest streak
244
+ 6. Assigns a 10-minute outdoor break
245
+ 7. Optionally runs an animated countdown timer
246
+ 8. Offers social sharing to Twitter/X, LinkedIn, or Instagram
247
+ 9. Exits — time to touch grass
248
+
249
+ ---
250
+
251
+ ## ⚙️ Requirements
252
+
253
+ - **Node.js 20 or higher**
254
+ - A terminal emulator
255
+ - Access to the outdoors (grass optional, parks acceptable)
256
+
257
+ ---
258
+
259
+ ## 🤝 Contributing
260
+
261
+ Contributions are welcome from developers who go outside sometimes.
262
+
263
+ - **Bug reports:** [Open an issue](https://github.com/lexCoder2/touch-grass-js/issues)
264
+ - **Feature requests:** [Start a discussion](https://github.com/lexCoder2/touch-grass-js/discussions)
265
+ - **Pull requests:** Reviewed between outdoor breaks
266
+
267
+ ---
268
+
269
+ ## 📄 License
270
+
271
+ [MIT](./LICENSE) © 2026 go-touch-grass contributors
272
+
273
+ ---
274
+
275
+ <div align="center">
276
+
277
+ **Made with ☀️ and 🌿 for developers who need a reminder to go outside**
278
+
279
+ Your code will still be there in 10 minutes. Your mental health might not be.
280
+
281
+ [**⬆ Back to top**](#go-touch-grass)
282
+
283
+ </div>
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env node
2
+
3
+ import meow from 'meow';
4
+ import chalk from 'chalk';
5
+ import { getArt } from '../src/art.js';
6
+ import { getRandomMessage } from '../src/messages.js';
7
+ import { incrementStreak, getStreakData, showStreakStats } from '../src/streak.js';
8
+ import { showReturnTime, runCountdown, showTimerPrompt } from '../src/timer.js';
9
+ import { openShareDialog } from '../src/share.js';
10
+ import {
11
+ printHeader,
12
+ printMessage,
13
+ printStreakBadge,
14
+ printSeparator,
15
+ clearOnExit,
16
+ } from '../src/ui.js';
17
+ import { confirm, select } from '@inquirer/prompts';
18
+
19
+ const cli = meow(
20
+ `
21
+ Usage
22
+ $ npx go-touch-grass [options]
23
+
24
+ Options
25
+ --streak, -s Show your grass-touching streak stats
26
+ --share, -S Share your achievement to social media
27
+ --noTimer Skip the countdown timer
28
+ --noShare Skip the social media share prompt
29
+ --help Show this help
30
+ --version Show version
31
+
32
+ Examples
33
+ $ npx go-touch-grass
34
+ $ npx go-touch-grass --streak
35
+ $ npx go-touch-grass --share
36
+ $ npx go-touch-grass --noShare
37
+ `,
38
+ {
39
+ importMeta: import.meta,
40
+ flags: {
41
+ streak: {
42
+ type: 'boolean',
43
+ shortFlag: 's',
44
+ default: false,
45
+ },
46
+ share: {
47
+ type: 'boolean',
48
+ shortFlag: 'S',
49
+ default: false,
50
+ },
51
+ noTimer: {
52
+ type: 'boolean',
53
+ default: false,
54
+ },
55
+ noShare: {
56
+ type: 'boolean',
57
+ default: false,
58
+ },
59
+ },
60
+ }
61
+ );
62
+
63
+ async function main() {
64
+ clearOnExit();
65
+
66
+ // --streak flag: show stats and exit
67
+ if (cli.flags.streak) {
68
+ console.log(showStreakStats(chalk));
69
+ process.exit(0);
70
+ }
71
+
72
+ // --share flag: go straight to share (ask for platform)
73
+ if (cli.flags.share) {
74
+ const streakData = getStreakData();
75
+ try {
76
+ const platform = await select({
77
+ message: 'Which platform would you like to shame your followers on?',
78
+ choices: [
79
+ { name: '🐦 Twitter/X', value: 'twitter' },
80
+ { name: '💼 LinkedIn', value: 'linkedin' },
81
+ { name: '📸 Instagram', value: 'instagram' },
82
+ { name: '🚫 Actually, nevermind', value: 'none' },
83
+ ],
84
+ });
85
+ if (platform !== 'none') {
86
+ await openShareDialog(streakData, platform);
87
+ }
88
+ } catch {
89
+ // Non-TTY, default to twitter
90
+ await openShareDialog(streakData, 'twitter');
91
+ }
92
+ process.exit(0);
93
+ }
94
+
95
+ // Main experience flow
96
+ const art = getArt();
97
+ printHeader(art);
98
+ printSeparator();
99
+
100
+ const message = getRandomMessage();
101
+ printMessage(message);
102
+
103
+ printSeparator();
104
+
105
+ const streakResult = incrementStreak();
106
+ printStreakBadge(streakResult, streakResult.milestone);
107
+
108
+ printSeparator();
109
+
110
+ // Show timer experience
111
+ showReturnTime();
112
+
113
+ // Offer countdown if TTY
114
+ if (!cli.flags.noTimer) {
115
+ const wantCountdown = await showTimerPrompt();
116
+ if (wantCountdown) {
117
+ await runCountdown(10);
118
+ console.log(chalk.bold.green('\n✓ Welcome back! Now go work on something.\n'));
119
+ }
120
+ }
121
+
122
+ // Offer to share (unless --noShare is set)
123
+ if (!cli.flags.noShare) {
124
+ try {
125
+ const wantShare = await confirm({
126
+ message: 'Share your achievement on social media?',
127
+ default: false,
128
+ });
129
+
130
+ if (wantShare) {
131
+ const platform = await select({
132
+ message: 'Which platform would you like to shame your followers on?',
133
+ choices: [
134
+ { name: '🐦 Twitter/X', value: 'twitter' },
135
+ { name: '💼 LinkedIn', value: 'linkedin' },
136
+ { name: '📸 Instagram', value: 'instagram' },
137
+ { name: '🚫 Never mind', value: 'none' },
138
+ ],
139
+ });
140
+
141
+ if (platform !== 'none') {
142
+ await openShareDialog(streakResult, platform);
143
+ }
144
+ }
145
+ } catch {
146
+ // Non-TTY, skip
147
+ }
148
+ }
149
+
150
+ console.log(chalk.dim('\nRespect. Now go touch that grass.\n'));
151
+ process.exit(0);
152
+ }
153
+
154
+ main();
package/logo.svg ADDED
@@ -0,0 +1,93 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 520 600" width="520" height="600" role="img" aria-label="go-touch-grass logo: a mouse cursor clicking on grass blades inside a circle">
2
+ <title>go-touch-grass — CLI reminder for developers to go outside</title>
3
+ <desc>Logo for go-touch-grass: a mouse cursor pointer touching green grass blades inside a white circle. A sarcastic Node.js CLI tool that reminds developers to take outdoor breaks.</desc>
4
+ <defs>
5
+ <!-- Grass gradients -->
6
+ <linearGradient id="grass-bright" x1="0" y1="0" x2="0" y2="1">
7
+ <stop offset="0%" stop-color="#8BC34A"/>
8
+ <stop offset="100%" stop-color="#4CAF50"/>
9
+ </linearGradient>
10
+ <linearGradient id="grass-mid" x1="0" y1="0" x2="0" y2="1">
11
+ <stop offset="0%" stop-color="#7CB342"/>
12
+ <stop offset="100%" stop-color="#388E3C"/>
13
+ </linearGradient>
14
+ <linearGradient id="grass-dark" x1="0" y1="0" x2="0" y2="1">
15
+ <stop offset="0%" stop-color="#689F38"/>
16
+ <stop offset="100%" stop-color="#2E7D32"/>
17
+ </linearGradient>
18
+ <linearGradient id="ground-fill" x1="0" y1="0" x2="0" y2="1">
19
+ <stop offset="0%" stop-color="#8BC34A"/>
20
+ <stop offset="60%" stop-color="#7CB342"/>
21
+ <stop offset="100%" stop-color="#689F38"/>
22
+ </linearGradient>
23
+ <linearGradient id="cursor-body" x1="0" y1="0" x2="1" y2="1">
24
+ <stop offset="0%" stop-color="#1B3A5C"/>
25
+ <stop offset="100%" stop-color="#263850"/>
26
+ </linearGradient>
27
+ <!-- Circle shadow -->
28
+ <filter id="circle-shadow" x="-10%" y="-5%" width="120%" height="120%">
29
+ <feDropShadow dx="0" dy="4" stdDeviation="12" flood-color="#000" flood-opacity="0.1"/>
30
+ </filter>
31
+ <!-- Subtle inner shadow for depth -->
32
+ <filter id="inner-glow" x="-5%" y="-5%" width="110%" height="110%">
33
+ <feDropShadow dx="0" dy="1" stdDeviation="2" flood-color="#000" flood-opacity="0.06"/>
34
+ </filter>
35
+ </defs>
36
+
37
+ <!-- Background circle with shadow -->
38
+ <circle cx="260" cy="250" r="195" fill="white" filter="url(#circle-shadow)"/>
39
+ <!-- Subtle border ring -->
40
+ <circle cx="260" cy="250" r="193" fill="none" stroke="#E0E0E0" stroke-width="1.5" opacity="0.5"/>
41
+
42
+ <!-- === GRASS CLUSTER === -->
43
+ <!-- Ground strip / base -->
44
+ <ellipse cx="240" cy="370" rx="120" ry="18" fill="url(#ground-fill)" opacity="0.9"/>
45
+
46
+ <!-- Back grass blades (lighter, behind cursor) -->
47
+ <!-- Back-left short blade -->
48
+ <path d="M 175 365 Q 160 320 170 280 Q 175 260 185 245"
49
+ stroke="url(#grass-bright)" stroke-width="18" fill="none" stroke-linecap="round"
50
+ filter="url(#inner-glow)"/>
51
+ <!-- Back-right blade -->
52
+ <path d="M 290 360 Q 310 310 300 260 Q 295 240 285 225"
53
+ stroke="url(#grass-bright)" stroke-width="16" fill="none" stroke-linecap="round"
54
+ filter="url(#inner-glow)"/>
55
+
56
+ <!-- Mid grass blades -->
57
+ <!-- Mid-left tall blade curving left -->
58
+ <path d="M 200 368 Q 175 300 185 240 Q 190 210 205 185"
59
+ stroke="url(#grass-mid)" stroke-width="20" fill="none" stroke-linecap="round"
60
+ filter="url(#inner-glow)"/>
61
+ <!-- Mid-center blade (tallest, straight up) -->
62
+ <path d="M 235 370 Q 230 290 235 225 Q 238 195 245 170"
63
+ stroke="url(#grass-mid)" stroke-width="22" fill="none" stroke-linecap="round"
64
+ filter="url(#inner-glow)"/>
65
+ <!-- Mid-right blade curving right -->
66
+ <path d="M 265 365 Q 285 300 275 245 Q 270 220 260 200"
67
+ stroke="url(#grass-mid)" stroke-width="18" fill="none" stroke-linecap="round"
68
+ filter="url(#inner-glow)"/>
69
+
70
+ <!-- Front grass blades (darkest, in front) -->
71
+ <!-- Front-left blade -->
72
+ <path d="M 210 372 Q 195 330 205 280 Q 210 255 220 235"
73
+ stroke="url(#grass-dark)" stroke-width="15" fill="none" stroke-linecap="round"/>
74
+ <!-- Front-center-right small blade -->
75
+ <path d="M 250 372 Q 260 335 255 300 Q 252 280 248 265"
76
+ stroke="url(#grass-dark)" stroke-width="14" fill="none" stroke-linecap="round"/>
77
+
78
+ <!-- === MOUSE CURSOR === -->
79
+ <!-- The cursor pointer — pointing down-left, touching the grass -->
80
+ <g transform="translate(280, 160) rotate(15)">
81
+ <!-- Cursor arrow shape -->
82
+ <path d="M 0 0 L 0 120 L 30 95 L 50 140 L 68 132 L 48 87 L 82 82 Z"
83
+ fill="url(#cursor-body)" stroke="#1B3A5C" stroke-width="7" stroke-linejoin="round" stroke-linecap="round"/>
84
+ <!-- Inner highlight for depth -->
85
+ <path d="M 10 20 L 10 105 L 34 85 L 52 127 L 60 124 L 42 82 L 70 78 Z"
86
+ fill="none" stroke="#3D5A80" stroke-width="1.5" stroke-linejoin="round" opacity="0.4"/>
87
+ </g>
88
+
89
+ <!-- === TEXT: touch-grass.js === -->
90
+ <text x="260" y="530" font-family="'Segoe UI', 'Helvetica Neue', 'Arial', sans-serif"
91
+ font-size="56" font-weight="800" fill="#4CAF50" text-anchor="middle"
92
+ letter-spacing="-0.5">go-touch-grass</text>
93
+ </svg>
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "go-touch-grass",
3
+ "version": "1.0.0",
4
+ "description": "A Fun CLI reminder for developers to go outside and touch grass. Persistent streak tracking, ASCII art scenes, emoji-packed social sharing, and optional countdown timer.",
5
+ "homepage": "https://github.com/lexCoder2/touch-grass-js#readme",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/lexCoder2/touch-grass-js.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/lexCoder2/touch-grass-js/issues"
12
+ },
13
+ "author": "Alex Rodriguez <hi@iarodriguez.com>",
14
+ "type": "module",
15
+ "bin": {
16
+ "go-touch-grass": "./bin/touch-grass.js"
17
+ },
18
+ "files": [
19
+ "bin/",
20
+ "src/",
21
+ "LICENSE",
22
+ "README.md",
23
+ "logo.svg"
24
+ ],
25
+ "engines": {
26
+ "node": ">=20"
27
+ },
28
+ "scripts": {
29
+ "start": "node bin/touch-grass.js",
30
+ "lint": "node --check src/*.js bin/*.js"
31
+ },
32
+ "keywords": [
33
+ "cli",
34
+ "fun",
35
+ "grass",
36
+ "developer-health",
37
+ "humor",
38
+ "productivity",
39
+ "outdoors",
40
+ "streak",
41
+ "sarcasm",
42
+ "terminal",
43
+ "wellness",
44
+ "break-reminder",
45
+ "burnout-prevention",
46
+ "mental-health",
47
+ "work-life-balance",
48
+ "social-sharing",
49
+ "twitter",
50
+ "linkedin",
51
+ "instagram",
52
+ "nodejs",
53
+ "npm",
54
+ "npx",
55
+ "developers"
56
+ ],
57
+ "license": "MIT",
58
+ "dependencies": {
59
+ "chalk": "^5.4.1",
60
+ "conf": "^15.1.0",
61
+ "meow": "^14.0.0",
62
+ "open": "^11.0.0",
63
+ "@inquirer/prompts": "^8.3.0",
64
+ "boxen": "^8.0.1",
65
+ "log-update": "^7.2.0"
66
+ }
67
+ }
package/src/art.js ADDED
@@ -0,0 +1,42 @@
1
+ const scenes = [
2
+ {
3
+ title: '~ The Meadow ~',
4
+ scene: ` \\ | / * *
5
+ \\ | / * *
6
+ ____\\_|_/____
7
+ | \\|/ | The sun is out.
8
+ | | Your screen is not.
9
+ |___________|
10
+ |I|I|I|I|I|I|
11
+ /\\/\\/\\/\\/\\/\\/\\
12
+ ~~~~~~~~~~~~~~~~~`,
13
+ },
14
+ {
15
+ title: '~ The Park ~',
16
+ scene: ` ^ * *
17
+ /|\\ *
18
+ / | \\
19
+ ___/ | \\___
20
+ ( touch )
21
+ \\ grass /
22
+ \\ /
23
+ ~~~~~~~~~~~~~~~~~~~~`,
24
+ },
25
+ {
26
+ title: '~ The Mountain ~',
27
+ scene: ` *
28
+ /|\\
29
+ / | \\
30
+ / | \\
31
+ / | \\
32
+ ~~~~~~~~~~~~~~~~~~~~
33
+ go. outside. now.`,
34
+ },
35
+ ];
36
+
37
+ export function getArt(variant = null) {
38
+ const index = variant !== null ? variant : Math.floor(Math.random() * scenes.length);
39
+ return scenes[index];
40
+ }
41
+
42
+ export default scenes;
@@ -0,0 +1,40 @@
1
+ const messages = [
2
+ 'Congratulations. You have successfully remembered that outside exists.',
3
+ 'Your IDE will still be there. Regrettably.',
4
+ 'The compiler says: skill issue. Grass says: come here.',
5
+ 'Stack Overflow has been there for you. Have YOU been there for vitamin D?',
6
+ 'Breaking: local developer discovers world beyond 80-column terminal.',
7
+ 'You have been in dark mode for 14 hours. The sun has better rendering.',
8
+ 'Your PR will wait. Your mitochondria will not.',
9
+ '404: outdoor time not found. Initiating emergency protocol.',
10
+ "Today's forecast: touch grass, high of 72°F, zero merge conflicts.",
11
+ 'npm install outside completed successfully. 0 vulnerabilities.',
12
+ 'git push yourself. Outside. Now.',
13
+ 'ERROR: Too much terminal. SOLUTION: Unplug face from monitor.',
14
+ 'Doctors hate this one weird trick: going outside.',
15
+ "You've read more man pages today than a park ranger does in a year.",
16
+ 'The grass is always greener outside. Mostly because you are never there.',
17
+ "Your linter found 0 issues. Your therapist found several.",
18
+ "You can't merge a PR if you're outside. Sounds like a personal problem.",
19
+ 'BREAKING: Grass has better documentation than Stack Overflow.',
20
+ 'Tree.render() is working perfectly. Better than your last deploy.',
21
+ "Touching grass: The only bug fix that actually heals you.",
22
+ 'Chlorophyll? More like Chloro-FEEL the sunshine.',
23
+ "Your keyboard: still broken after 12 hours. Grass: still free.",
24
+ 'Ctrl+Alt+Delete your schedule and go outside.',
25
+ "Sunscreen applied. Jira ticket: still haunting you. Go outside anyway.",
26
+ 'Roses are red, violets are blue, touch grass right now or you might lose your mind too.',
27
+ 'System.out.println("I went outside and it was weird but nice");',
28
+ 'Type: grass. Import: fresh air. Compile: success. Status: HUMAN.',
29
+ "If you can't find the bug, the bug is probably behind you (it's a tree).",
30
+ 'Your standup can wait. Your Vitamin D levels cannot.',
31
+ "New error: `touch: grass not found`. Seek grass immediately.",
32
+ 'DEBUG: why do I feel better outside? ANS: because you are finally alive.',
33
+ ];
34
+
35
+
36
+ export function getRandomMessage() {
37
+ return messages[Math.floor(Math.random() * messages.length)];
38
+ }
39
+
40
+ export default messages;
package/src/share.js ADDED
@@ -0,0 +1,59 @@
1
+ import open from 'open';
2
+
3
+ const SOCIAL_TEMPLATES = [
4
+ '🌿 just touched grass ✅ you should too! try: npx go-touch-grass',
5
+ '🌱 i went outside. actually outside. grass was real. npx go-touch-grass if you dare',
6
+ '🔥 day {streak} of touching grass as a developer. it gets easier. npx go-touch-grass',
7
+ '⚡ my outdoor streak is {streak} days and i feel things. horrifying. npx go-touch-grass',
8
+ '☀️ went outside today. no screens. no PRs. grass. 10/10 recommend. npx go-touch-grass',
9
+ '🏕️ {streak} consecutive days of touching grass. my therapist is proud. npx go-touch-grass',
10
+ '🌍 breaking: developer discovers photosynthesis exists. it\'s called touching grass. npx go-touch-grass',
11
+ '💪 {streak} day grass-touching streak 🌿 my productivity is actually up??? npx go-touch-grass',
12
+ '🎉 hit {streak} touches! my vitamin D levels have entered the chat. npx go-touch-grass',
13
+ '🚀 just proved i can escape my desk for 10 minutes. npx go-touch-grass',
14
+ '😎 {streak} touches deep into my grass-touching era. you could join. npx go-touch-grass',
15
+ '🌟 pro tip: touching grass is the best debugging strategy. npx go-touch-grass',
16
+ '🧠 went outside. the solution was there the whole time. literally in the air. npx go-touch-grass',
17
+ '✨ {streak} days of touching grass and my posture is actually improving??? npx go-touch-grass',
18
+ ];
19
+
20
+ export function buildShareText(streakData) {
21
+ const template = SOCIAL_TEMPLATES[Math.floor(Math.random() * SOCIAL_TEMPLATES.length)];
22
+ return template
23
+ .replace('{count}', streakData.count || 0)
24
+ .replace('{streak}', streakData.currentStreak || 1);
25
+ }
26
+
27
+ export function buildTwitterUrl(text) {
28
+ const encoded = encodeURIComponent(text);
29
+ return `https://twitter.com/intent/tweet?text=${encoded}`;
30
+ }
31
+
32
+ export function buildLinkedInUrl(text) {
33
+ const encoded = encodeURIComponent(text);
34
+ return `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent('https://github.com/lexCoder2/touch-grass-js')}&summary=${encoded}`;
35
+ }
36
+
37
+ export function buildInstagramShareText(text) {
38
+ // Instagram doesn't have a direct URL intent, so we return the text for manual sharing
39
+ return text;
40
+ }
41
+
42
+ export async function openShareDialog(streakData, platform) {
43
+ const shareText = buildShareText(streakData);
44
+ console.log('\n📱 Here\'s what we\'ll share:\n');
45
+ console.log(` "${shareText}"\n`);
46
+
47
+ if (platform === 'twitter') {
48
+ const url = buildTwitterUrl(shareText);
49
+ console.log('Opening Twitter/X... go shame your followers into being healthy.\n');
50
+ await open(url);
51
+ } else if (platform === 'linkedin') {
52
+ const url = buildLinkedInUrl(shareText);
53
+ console.log('Opening LinkedIn... time to network your outdoor achievements.\n');
54
+ await open(url);
55
+ } else if (platform === 'instagram') {
56
+ console.log('Copy the text above and paste it into your Instagram post caption.\n');
57
+ console.log('✨ Pro tip: Add some grass/nature photos to really sell it!\n');
58
+ }
59
+ }
package/src/streak.js ADDED
@@ -0,0 +1,123 @@
1
+ import Conf from 'conf';
2
+ import boxen from 'boxen';
3
+
4
+ const config = new Conf({
5
+ projectName: 'go-touch-grass',
6
+ schema: {
7
+ count: {
8
+ type: 'number',
9
+ default: 0,
10
+ minimum: 0,
11
+ },
12
+ lastVisit: {
13
+ type: 'string',
14
+ default: '',
15
+ },
16
+ longestStreak: {
17
+ type: 'number',
18
+ default: 0,
19
+ minimum: 0,
20
+ },
21
+ currentStreak: {
22
+ type: 'number',
23
+ default: 0,
24
+ minimum: 0,
25
+ },
26
+ },
27
+ });
28
+
29
+ const MILESTONES = {
30
+ 1: 'First time?! Bold move. Let\'s see if you survive.',
31
+ 5: 'Five times. You\'re basically a park ranger now.',
32
+ 10: 'Ten times. Your tan is visible from space.',
33
+ 25: '25 touches. You have surpassed all other developers.',
34
+ 50: '50 touches. Consider going professional.',
35
+ 100: '100 TOUCHES. You are no longer a developer. You are nature.',
36
+ };
37
+
38
+ function getTodayDate() {
39
+ return new Date().toISOString().split('T')[0];
40
+ }
41
+
42
+ function isYesterday(dateStr) {
43
+ const today = new Date();
44
+ const yesterday = new Date(today);
45
+ yesterday.setDate(yesterday.getDate() - 1);
46
+ return dateStr === yesterday.toISOString().split('T')[0];
47
+ }
48
+
49
+ export function incrementStreak() {
50
+ const today = getTodayDate();
51
+ const lastVisit = config.get('lastVisit') || '';
52
+ let currentStreak = config.get('currentStreak') || 0;
53
+ let count = config.get('count') || 0;
54
+ let longestStreak = config.get('longestStreak') || 0;
55
+ let milestone = null;
56
+
57
+ // Only increment count if it's a new day
58
+ if (lastVisit !== today) {
59
+ count += 1;
60
+
61
+ // Update streak logic
62
+ if (isYesterday(lastVisit)) {
63
+ // Streak continues
64
+ currentStreak += 1;
65
+ } else {
66
+ // Streak broken or first time
67
+ currentStreak = 1;
68
+ }
69
+
70
+ // Update longest streak
71
+ if (currentStreak > longestStreak) {
72
+ longestStreak = currentStreak;
73
+ }
74
+
75
+ // Check for milestones
76
+ if (MILESTONES[count]) {
77
+ milestone = MILESTONES[count];
78
+ }
79
+
80
+ // Save to config
81
+ config.set({
82
+ count,
83
+ lastVisit: today,
84
+ currentStreak,
85
+ longestStreak,
86
+ });
87
+ }
88
+
89
+ return {
90
+ count,
91
+ lastVisit: today,
92
+ currentStreak,
93
+ longestStreak,
94
+ milestone,
95
+ };
96
+ }
97
+
98
+ export function getStreakData() {
99
+ return {
100
+ count: config.get('count'),
101
+ lastVisit: config.get('lastVisit'),
102
+ currentStreak: config.get('currentStreak'),
103
+ longestStreak: config.get('longestStreak'),
104
+ };
105
+ }
106
+
107
+ export function resetStreak() {
108
+ config.clear();
109
+ }
110
+
111
+ export function showStreakStats(chalk) {
112
+ const data = getStreakData();
113
+ const statsBox = boxen(
114
+ `🌿 GRASS TOUCHING STATS\n\n Total touches: ${chalk.cyan(data.count)}\n Current streak: ${chalk.cyan(data.currentStreak)} days\n Longest streak: ${chalk.cyan(data.longestStreak)} days\n Last touched: ${chalk.cyan(data.lastVisit || 'never')}`,
115
+ {
116
+ padding: 1,
117
+ margin: 1,
118
+ borderStyle: 'round',
119
+ borderColor: 'green',
120
+ }
121
+ );
122
+ return statsBox;
123
+ }
package/src/timer.js ADDED
@@ -0,0 +1,72 @@
1
+ import chalk from 'chalk';
2
+ import boxen from 'boxen';
3
+ import logUpdate from 'log-update';
4
+ import { confirm } from '@inquirer/prompts';
5
+
6
+ export function showReturnTime() {
7
+ const returnTime = new Date(Date.now() + 10 * 60 * 1000);
8
+ const timeStr = returnTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
9
+
10
+ const timerBox = boxen(
11
+ `⏱ YOUR OUTDOOR ASSIGNMENT\n\nDuration: ${chalk.yellow('10 minutes')}\nReturn by: ${chalk.cyan(timeStr)}\n\n${chalk.dim('Do NOT touch your keyboard.')}\n${chalk.dim('Do NOT check Slack.')}\n${chalk.dim('Touch grass. Breathe air.')}`,
12
+ {
13
+ padding: 1,
14
+ margin: 1,
15
+ borderStyle: 'round',
16
+ borderColor: 'yellow',
17
+ }
18
+ );
19
+
20
+ console.log('\n' + timerBox + '\n');
21
+ }
22
+
23
+ function buildProgressBar(done, total, width = 30) {
24
+ const filledWidth = Math.floor((done / total) * width);
25
+ const emptyWidth = width - filledWidth;
26
+ return '█'.repeat(filledWidth) + '░'.repeat(emptyWidth);
27
+ }
28
+
29
+ export async function runCountdown(minutes = 10) {
30
+ return new Promise((resolve) => {
31
+ const totalSeconds = minutes * 60;
32
+ let remaining = totalSeconds;
33
+
34
+ const interval = setInterval(() => {
35
+ const mins = Math.floor(remaining / 60).toString().padStart(2, '0');
36
+ const secs = (remaining % 60).toString().padStart(2, '0');
37
+ const bar = buildProgressBar(totalSeconds - remaining, totalSeconds);
38
+
39
+ logUpdate(
40
+ chalk.green(`\n ⏱ GO OUTSIDE TIMER\n\n`) +
41
+ chalk.yellow(` ${mins}:${secs} remaining\n\n`) +
42
+ chalk.gray(` ${bar}\n\n`) +
43
+ chalk.dim(` Press Ctrl+C to admit defeat\n`)
44
+ );
45
+
46
+ remaining--;
47
+ if (remaining < 0) {
48
+ clearInterval(interval);
49
+ logUpdate.done();
50
+ resolve();
51
+ }
52
+ }, 1000);
53
+ });
54
+ }
55
+
56
+ export async function showTimerPrompt() {
57
+ // Skip in non-TTY environments
58
+ if (!process.stdout.isTTY) {
59
+ return false;
60
+ }
61
+
62
+ try {
63
+ const wantCountdown = await confirm({
64
+ message: 'Count down 10 minutes for you?',
65
+ default: false,
66
+ });
67
+ return wantCountdown;
68
+ } catch {
69
+ // If prompt fails (e.g., in piped context), return false
70
+ return false;
71
+ }
72
+ }
package/src/ui.js ADDED
@@ -0,0 +1,64 @@
1
+ import chalk from 'chalk';
2
+
3
+ // Color palette
4
+ const colors = {
5
+ grass: chalk.hex('#4CAF50'),
6
+ sun: chalk.hex('#FFD700'),
7
+ sky: chalk.hex('#87CEEB'),
8
+ dirt: chalk.hex('#8B4513'),
9
+ accent: chalk.bold.white,
10
+ dim: chalk.dim,
11
+ error: chalk.bold.red,
12
+ streak: chalk.bold.cyan,
13
+ warning: chalk.yellow,
14
+ };
15
+
16
+ export function printHeader(art) {
17
+ let colored = art.scene;
18
+ // Color grass tildes green
19
+ colored = colored.replace(/~/g, colors.grass('~'));
20
+ // Color asterisks gold
21
+ colored = colored.replace(/\*/g, colors.sun('*'));
22
+ // Color caret (tree) green
23
+ colored = colored.replace(/\^/g, colors.grass('^'));
24
+
25
+ console.log('\n' + colored + '\n');
26
+ }
27
+
28
+ export function printMessage(message) {
29
+ const prefix = colors.warning('>>>');
30
+ console.log(`${prefix} ${colors.accent(message)}\n`);
31
+ }
32
+
33
+ export function printStreakBadge(data, milestone) {
34
+ const streakLine = ` ${colors.streak(`streak: ${data.currentStreak} days`)} | ${colors.streak(`total: ${data.count} touches`)} | ${colors.streak(`longest: ${data.longestStreak} days`)}`;
35
+ console.log(streakLine);
36
+
37
+ if (milestone) {
38
+ console.log(`\n ${colors.warning('✨')} ${colors.accent(milestone)}\n`);
39
+ }
40
+ }
41
+
42
+ export function printSeparator() {
43
+ console.log(colors.dim('─'.repeat(60)));
44
+ }
45
+
46
+ export function printSuccess(text) {
47
+ console.log(` ${chalk.green('✓')} ${text}`);
48
+ }
49
+
50
+ export function clearOnExit() {
51
+ process.on('SIGINT', () => {
52
+ process.exit(0);
53
+ });
54
+ }
55
+
56
+ export default {
57
+ colors,
58
+ printHeader,
59
+ printMessage,
60
+ printStreakBadge,
61
+ printSeparator,
62
+ printSuccess,
63
+ clearOnExit,
64
+ };