devgrowth 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 EhDohWah
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,90 @@
1
+ # DevGrowth CLI
2
+
3
+ Zero-friction developer growth CLI for Saw Eh Doh Wah.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install
9
+ npm link
10
+ ```
11
+
12
+ ## Windows Notification Setup
13
+
14
+ ```powershell
15
+ # Requires devgrowth to be globally available (via npm link or installed globally)
16
+ .\scripts\install-windows.ps1
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```bash
22
+ devgrowth # Splash banner + command list
23
+ devgrowth banner # Banner with current stats
24
+ devgrowth banner --compact # Single-line banner
25
+ devgrowth init # Initialize configuration
26
+ devgrowth start # Start tonight's session
27
+ devgrowth start --light # Low-energy mode (skip timer)
28
+ devgrowth start --no-banner # Skip the startup banner
29
+ devgrowth log "..." # Write session log
30
+ devgrowth log "..." --light # Mark as light session
31
+ devgrowth status # Show weekly progress
32
+ devgrowth review # Weekly review (auto-saves review section)
33
+ devgrowth milestone list # Show achievement grid
34
+ devgrowth milestone check laravel m1 formRequests # Check off milestone
35
+ devgrowth milestone progress laravel # Show milestone progress
36
+ devgrowth history # Browse current week logs
37
+ devgrowth history --week 32 # View a specific week
38
+ devgrowth history --week 1 --year 2027 # View week across year boundaries
39
+ devgrowth history --skill laravel # Filter by skill
40
+ devgrowth history --search "Form Request" # Full-text search
41
+ devgrowth config show # View configuration
42
+ devgrowth config set <key> <value> # Update configuration
43
+ devgrowth config edit # Open config in default editor
44
+ ```
45
+
46
+ ## Banner
47
+
48
+ Running `devgrowth` with no arguments shows a splash screen: the `devGrowth`
49
+ wordmark in block letters with the turtle riding beside it, then tonight's
50
+ skill, weekly progress, and your streak. `devgrowth start` shows a one-line
51
+ version of it.
52
+
53
+ ```
54
+ █ ▄▀▀▀▄ ▄ █    __   _
55
+ ▄▄▄█ ▄▄▄ ▄ ▄ █ ▄ ▄▄ ▄▄▄ ▄ ▄ ▄█▄ █ ▄▄   / ♯ # \ /・_)
56
+ █ █ █▄▄▄█ █ █ █ ▀▀█ █▀ █ █ █ ▄ █ █ █▀ █ (ソ♯ # ♯ #Y /
57
+ ▀▄▄▄█ ▀▄▄▄ ▀▄▀ ▀▄▄▄█ █ ▀▄▄▄▀ █▀ ▀█ █▄▄ █ █  (_)――-(_)′
58
+
59
+ Saw Eh Doh Wah@DESKTOP-BOD610M · v1.0.0
60
+ ──────────────────────────────────────────────────────────────────────────
61
+ Tonight laravel — Deepen
62
+ Resource Laravel Daily
63
+ Session 21:00 · 20 min
64
+ Week 36 ░░░░░ 0/5 sessions
65
+ Streak 0 weeks (best: 0)
66
+ Totals 1 session · 0 min
67
+ Data ~/.devgrowth
68
+ ```
69
+
70
+ The banner is suppressed automatically when output is not a TTY, so piping and
71
+ scheduled runs stay clean:
72
+
73
+ ```bash
74
+ devgrowth config show | jq . # no banner
75
+ ```
76
+
77
+ | Control | Effect |
78
+ | --- | --- |
79
+ | `DEVGROWTH_NO_BANNER=1` | Never show the banner |
80
+ | `DEVGROWTH_BANNER=1` | Show it even when piped |
81
+ | `--no-banner` | Skip it for one `devgrowth start` |
82
+ | `NO_COLOR=1` | Banner without color (handled by chalk) |
83
+
84
+ Terminals narrower than the 74-column header automatically fall back to the
85
+ one-line banner, so the layout never wraps.
86
+
87
+ ## Troubleshooting
88
+
89
+ - If `devgrowth` command is not found after `npm install`, run `npm link` from the project directory.
90
+ - Windows install script requires the CLI to be in PATH. Ensure `npm link` has been run first.
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { program } from '../src/index.js';
3
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "devgrowth",
3
+ "version": "1.0.0",
4
+ "description": "Zero-friction developer growth CLI",
5
+ "type": "module",
6
+ "keywords": [
7
+ "cli",
8
+ "productivity",
9
+ "habit-tracker",
10
+ "developer-growth",
11
+ "learning"
12
+ ],
13
+ "homepage": "https://github.com/EhDohWah/devgrowth#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/EhDohWah/devgrowth/issues"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/EhDohWah/devgrowth.git"
20
+ },
21
+ "license": "MIT",
22
+ "author": "EhDohWah <ehdohwah007@gmail.com>",
23
+ "files": [
24
+ "bin",
25
+ "src",
26
+ "scripts"
27
+ ],
28
+ "bin": {
29
+ "devgrowth": "bin/devgrowth.js"
30
+ },
31
+ "scripts": {
32
+ "test": "NODE_OPTIONS='--experimental-vm-modules' jest --coverage",
33
+ "test:win": "set NODE_OPTIONS=--experimental-vm-modules && jest --coverage"
34
+ },
35
+ "dependencies": {
36
+ "chalk": "^5.3.0",
37
+ "commander": "^11.1.0",
38
+ "fs-extra": "^11.2.0",
39
+ "node-notifier": "^10.0.1",
40
+ "open": "^9.1.0"
41
+ },
42
+ "devDependencies": {
43
+ "jest": "^29.7.0"
44
+ },
45
+ "engines": {
46
+ "node": ">=18.0.0"
47
+ }
48
+ }
@@ -0,0 +1,28 @@
1
+ #Requires -RunAsAdministrator
2
+
3
+ param(
4
+ [string]$DevGrowthPath
5
+ )
6
+
7
+ $taskName = "DevGrowthNotify"
8
+
9
+ # Resolve devgrowth executable path
10
+ $devgrowth = $null
11
+ if ($DevGrowthPath) {
12
+ $devgrowth = Get-Command $DevGrowthPath -ErrorAction SilentlyContinue
13
+ } else {
14
+ $devgrowth = Get-Command devgrowth -ErrorAction SilentlyContinue
15
+ }
16
+
17
+ if (-not $devgrowth) {
18
+ Write-Error "devgrowth command not found in PATH. Install the CLI first (e.g. run 'npm link' in the project directory)."
19
+ exit 1
20
+ }
21
+
22
+ $action = New-ScheduledTaskAction -Execute $devgrowth.Source -Argument "notify"
23
+ $trigger = New-ScheduledTaskTrigger -Daily -At "20:55"
24
+ $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
25
+
26
+ Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Settings $settings -Force
27
+
28
+ Write-Host "Scheduled task '$taskName' created. Runs 'devgrowth notify' daily at 20:55."
@@ -0,0 +1,50 @@
1
+ import * as configManager from '../core/configManager.js';
2
+ import * as storage from '../core/storage.js';
3
+ import * as path from 'path';
4
+ import open from 'open';
5
+ import chalk from 'chalk';
6
+
7
+ function parseValue(raw) {
8
+ if (raw === 'true') return true;
9
+ if (raw === 'false') return false;
10
+ if (/^-?\d+$/.test(raw)) return parseInt(raw, 10);
11
+ if (/^-?\d*\.\d+$/.test(raw)) return parseFloat(raw);
12
+ return raw;
13
+ }
14
+
15
+ export async function configCommand(subcommand, args = []) {
16
+ switch (subcommand) {
17
+ case 'show': {
18
+ const config = await configManager.getConfig();
19
+ console.log(JSON.stringify(config, null, 2));
20
+ break;
21
+ }
22
+ case 'set': {
23
+ const [keyPath, value] = args;
24
+ if (!keyPath || value === undefined) {
25
+ console.error(chalk.red('Usage: devgrowth config set <key.path> <value>'));
26
+ process.exit(1);
27
+ }
28
+ const parsed = parseValue(value);
29
+ await configManager.setConfigValue(keyPath, parsed);
30
+ console.log(chalk.green(`Updated ${keyPath} = ${JSON.stringify(parsed)}`));
31
+ break;
32
+ }
33
+ case 'edit': {
34
+ const configPath = path.join(storage.getDevGrowthDir(), 'config.json');
35
+ try {
36
+ await open(configPath);
37
+ console.log('Opening config in default editor...');
38
+ } catch (err) {
39
+ console.error(chalk.red(`Could not open editor: ${err.message}`));
40
+ console.log(chalk.yellow(`Fallback: Open this file manually:\n ${configPath}`));
41
+ }
42
+ break;
43
+ }
44
+ default: {
45
+ console.error(chalk.red(`Unknown subcommand: ${subcommand}`));
46
+ console.log('Usage: devgrowth config <show|set|edit>');
47
+ process.exit(1);
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,73 @@
1
+ import * as logger from '../core/logger.js';
2
+ import * as schedule from '../core/schedule.js';
3
+ import * as storage from '../core/storage.js';
4
+ import * as path from 'path';
5
+ import chalk from 'chalk';
6
+
7
+ function getDateOfISOWeek(w, y) {
8
+ const simple = new Date(Date.UTC(y, 0, 1 + (w - 1) * 7));
9
+ const dow = simple.getUTCDay();
10
+ const ISOweekStart = new Date(simple);
11
+ if (dow <= 4) {
12
+ ISOweekStart.setUTCDate(simple.getUTCDate() - simple.getUTCDay() + 1);
13
+ } else {
14
+ ISOweekStart.setUTCDate(simple.getUTCDate() + 8 - simple.getUTCDay());
15
+ }
16
+ return ISOweekStart;
17
+ }
18
+
19
+ export async function historyCommand(options) {
20
+ const now = new Date();
21
+ let year;
22
+ let month;
23
+ let weekFile;
24
+
25
+ if (options.week) {
26
+ const weekNum = parseInt(options.week, 10);
27
+ const requestedYear = options.year ? parseInt(options.year, 10) : now.getFullYear();
28
+ const mon = getDateOfISOWeek(weekNum, requestedYear);
29
+ // The week may start in the previous year (e.g. ISO week 1 of 2026 begins
30
+ // Dec 29, 2025) — key the file by the Monday's actual year/month.
31
+ year = mon.getUTCFullYear();
32
+ month = String(mon.getUTCMonth() + 1).padStart(2, '0');
33
+ weekFile = `week${weekNum}.md`;
34
+ } else {
35
+ // Current week: key by this week's Monday so month boundaries don't split logs
36
+ const weekStart = schedule.getWeekStart(now);
37
+ year = weekStart.getFullYear();
38
+ month = String(weekStart.getMonth() + 1).padStart(2, '0');
39
+ weekFile = schedule.getCurrentWeekFileName(now);
40
+ }
41
+
42
+ const logs = await logger.readWeekLogs(year, month, weekFile);
43
+
44
+ console.log(chalk.bold(`\nHistory \u2014 ${weekFile.replace('.md', '')}`));
45
+ console.log(chalk.gray('\u2500'.repeat(50)));
46
+
47
+ let entries = logger.parseLogEntries(logs);
48
+
49
+ if (options.skill) {
50
+ const skillLower = options.skill.toLowerCase();
51
+ entries = entries.filter(entry => {
52
+ const block = logs.split(/\n(?=## )/).find(b => b.includes(entry.date));
53
+ return block && block.toLowerCase().includes(`**skill:** ${skillLower}`);
54
+ });
55
+ }
56
+
57
+ if (options.search) {
58
+ const searchLower = options.search.toLowerCase();
59
+ entries = entries.filter(entry => {
60
+ const block = logs.split(/\n(?=## )/).find(b => b.includes(entry.date));
61
+ return block && block.toLowerCase().includes(searchLower);
62
+ });
63
+ }
64
+
65
+ if (entries.length === 0) {
66
+ console.log(chalk.yellow('No logs found.'));
67
+ } else {
68
+ for (const entry of entries) {
69
+ console.log(`${chalk.cyan(entry.date)} \u2014 ${entry.message}`);
70
+ }
71
+ }
72
+ console.log();
73
+ }
@@ -0,0 +1,31 @@
1
+ import * as configManager from '../core/configManager.js';
2
+ import * as storage from '../core/storage.js';
3
+ import { DEFAULT_MILESTONES } from '../core/milestones.js';
4
+ import * as path from 'path';
5
+
6
+ export async function initCommand() {
7
+ const dir = storage.getDevGrowthDir();
8
+ const configPath = path.join(dir, 'config.json');
9
+ const exists = await storage.fileExists(configPath);
10
+
11
+ if (exists) {
12
+ console.log(`✓ DevGrowth already initialized at ${dir}`);
13
+ return;
14
+ }
15
+
16
+ await configManager.createDefaultConfig();
17
+ await storage.writeJson(path.join(dir, 'stats.json'), {
18
+ currentStreakWeeks: 0,
19
+ longestStreakWeeks: 0,
20
+ totalSessions: 0,
21
+ totalMinutesDeepWork: 0,
22
+ currentWeek: { completed: 0, total: 5, status: 'not_started' },
23
+ weeklyHistory: [],
24
+ lastSessionDate: null,
25
+ lastFinalizedWeek: null
26
+ });
27
+ await storage.writeJson(path.join(dir, 'milestones.json'), DEFAULT_MILESTONES);
28
+
29
+ console.log(`✓ DevGrowth initialized at ${dir}`);
30
+ console.log(' Run `devgrowth start` to begin your first session.');
31
+ }
@@ -0,0 +1,36 @@
1
+ import * as configManager from '../core/configManager.js';
2
+ import * as schedule from '../core/schedule.js';
3
+ import * as logger from '../core/logger.js';
4
+ import * as stats from '../core/stats.js';
5
+ import chalk from 'chalk';
6
+
7
+ export async function logCommand(message, options = {}) {
8
+ if (!message || message.trim().length === 0) {
9
+ console.error(chalk.red('Error: Log message required.'));
10
+ console.log('Usage: devgrowth log "Today I learned ___. Tomorrow I will ___."');
11
+ process.exit(1);
12
+ }
13
+
14
+ if (message.length < 10) {
15
+ console.log(chalk.yellow('⚠️ Warning: Log message is very short, but saving anyway.'));
16
+ }
17
+
18
+ const config = await configManager.getConfig();
19
+ const today = schedule.getTodaySkill(config);
20
+ const now = schedule.getSessionDate(new Date());
21
+ const isLight = options.light || false;
22
+ const status = isLight ? 'Light' : 'Completed';
23
+ const duration = config.session?.durationMinutes || 20;
24
+
25
+ await logger.writeLogEntry(now, today.skill, today.resource, duration, status, message);
26
+ const updatedStats = await stats.recordSession(now, today.skill, duration, status);
27
+
28
+ const weekFile = schedule.getWeekFileName(now);
29
+ const weekStart = schedule.getWeekStart(now);
30
+ const logYear = weekStart.getFullYear();
31
+ const logMonth = String(weekStart.getMonth() + 1).padStart(2, '0');
32
+ console.log(chalk.green('✓ Log saved.'));
33
+ console.log(chalk.gray(` File: ~/.devgrowth/logs/${logYear}/${logMonth}/${weekFile}`));
34
+ console.log(chalk.gray(` Week progress: ${updatedStats.currentWeek.completed}/5 sessions`));
35
+ console.log(chalk.gray(` Streak: ${updatedStats.currentStreakWeeks} weeks (longest: ${updatedStats.longestStreakWeeks})`));
36
+ }
@@ -0,0 +1,51 @@
1
+ import * as milestones from '../core/milestones.js';
2
+ import chalk from 'chalk';
3
+
4
+ export async function milestoneCommand(subcommand, args = []) {
5
+ switch (subcommand) {
6
+ case 'list': {
7
+ const data = await milestones.getMilestones();
8
+ for (const [skill, config] of Object.entries(data)) {
9
+ console.log(chalk.bold(`\n${skill.toUpperCase()}`));
10
+ console.log(chalk.gray('\u2500'.repeat(40)));
11
+ for (const [monthKey, monthData] of Object.entries(config)) {
12
+ console.log(chalk.cyan(` ${monthKey.toUpperCase()}: ${monthData.title}`));
13
+ for (const [itemKey, done] of Object.entries(monthData.items)) {
14
+ const emoji = done ? '\u2705' : '\u2B1C';
15
+ console.log(` ${emoji} ${itemKey}`);
16
+ }
17
+ }
18
+ }
19
+ console.log();
20
+ break;
21
+ }
22
+ case 'check': {
23
+ const [skill, month, item] = args;
24
+ if (!skill || !month || !item) {
25
+ console.error(chalk.red('Usage: devgrowth milestone check <skill> <month> <item>'));
26
+ process.exit(1);
27
+ }
28
+ await milestones.checkMilestone(skill, month, item);
29
+ console.log(chalk.green(`\u2705 Milestone checked: ${skill} ${month} ${item}`));
30
+ break;
31
+ }
32
+ case 'progress': {
33
+ const [skill] = args;
34
+ if (!skill) {
35
+ console.error(chalk.red('Usage: devgrowth milestone progress <skill>'));
36
+ process.exit(1);
37
+ }
38
+ const progress = await milestones.getProgress(skill);
39
+ const barLength = 20;
40
+ const filled = Math.round((progress.completed / progress.total) * barLength);
41
+ const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barLength - filled);
42
+ console.log(`${chalk.bold(skill.toUpperCase())} ${bar} ${progress.percentage}% (${progress.completed}/${progress.total})`);
43
+ break;
44
+ }
45
+ default: {
46
+ console.error(chalk.red(`Unknown subcommand: ${subcommand}`));
47
+ console.log('Usage: devgrowth milestone <list|check|progress>');
48
+ process.exit(1);
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,23 @@
1
+ import * as configManager from '../core/configManager.js';
2
+ import * as schedule from '../core/schedule.js';
3
+ import * as notifier from '../core/notifier.js';
4
+
5
+ export async function notifyCommand() {
6
+ const config = await configManager.getConfig();
7
+
8
+ if (config.notification?.enabled === false) {
9
+ return;
10
+ }
11
+
12
+ const today = schedule.getTodaySkill(config);
13
+
14
+ if (today.skill === 'rest') {
15
+ return;
16
+ }
17
+
18
+ const minutes = config.notification?.reminderMinutesBefore ?? 5;
19
+ const skillName = today.skill.charAt(0).toUpperCase() + today.skill.slice(1);
20
+ const focusText = today.focus ? ` ${today.focus}` : '';
21
+ const message = `DevGrowth session in ${minutes} minutes — Tonight: ${skillName}${focusText}`;
22
+ await notifier.send('DevGrowth', message);
23
+ }
@@ -0,0 +1,57 @@
1
+ import * as logger from '../core/logger.js';
2
+ import * as stats from '../core/stats.js';
3
+ import * as storage from '../core/storage.js';
4
+ import * as schedule from '../core/schedule.js';
5
+ import * as path from 'path';
6
+ import chalk from 'chalk';
7
+
8
+ export async function reviewCommand() {
9
+ const now = new Date();
10
+ // Key the log file by this week's Monday so month/year boundaries don't split logs
11
+ const weekStart = schedule.getWeekStart(now);
12
+ const year = weekStart.getFullYear();
13
+ const month = String(weekStart.getMonth() + 1).padStart(2, '0');
14
+ const weekFile = schedule.getCurrentWeekFileName(now);
15
+
16
+ const logs = await logger.readWeekLogs(year, month, weekFile);
17
+
18
+ console.log(chalk.bold('\nWeekly Review'));
19
+ console.log(chalk.gray('\u2500'.repeat(50)));
20
+
21
+ const entries = logger.parseLogEntries(logs);
22
+ if (entries.length === 0) {
23
+ console.log(chalk.yellow('No logs found for this week.'));
24
+ } else {
25
+ for (const entry of entries) {
26
+ console.log(`${chalk.cyan(entry.date)} \u2014 ${entry.message}`);
27
+ }
28
+ }
29
+
30
+ console.log(chalk.gray('\u2500'.repeat(50)));
31
+ console.log(chalk.bold('Review Questions:'));
32
+ console.log('1. What was your biggest win this week?');
33
+ console.log('2. What blocked you?');
34
+ console.log('3. What will you focus on next week?');
35
+ console.log();
36
+
37
+ const result = await stats.finalizeWeek(now);
38
+ if (result.justFinalized) {
39
+ console.log(chalk.green(`Week finalized! Streak: ${result.currentStreakWeeks} weeks | Longest: ${result.longestStreakWeeks} weeks`));
40
+ } else {
41
+ console.log(chalk.yellow(`Week already finalized. Streak unchanged: ${result.currentStreakWeeks} weeks (longest: ${result.longestStreakWeeks})`));
42
+ }
43
+ console.log();
44
+
45
+ // Append review block to the week's markdown file (only on first finalize)
46
+ if (result.justFinalized) {
47
+ const filePath = path.join(storage.getDevGrowthDir(), 'logs', String(year), month, weekFile);
48
+ const exists = await storage.fileExists(filePath);
49
+ if (exists) {
50
+ let content = await storage.readFile(filePath);
51
+ const weekNum = schedule.getWeekNumber(now);
52
+ const reviewBlock = `## Week ${weekNum} Review\n- **Date:** ${schedule.formatDate(now)}\n- **Sessions:** ${result.currentWeek?.completed || entries.length}\n- **Streak:** ${result.currentStreakWeeks} weeks (longest: ${result.longestStreakWeeks})\n- **Biggest Win:** _\n- **Blocker:** _\n- **Next Week Focus:** _\n\n`;
53
+ content += reviewBlock;
54
+ await storage.writeFile(filePath, content);
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,91 @@
1
+ import * as configManager from '../core/configManager.js';
2
+ import * as schedule from '../core/schedule.js';
3
+ import * as timer from '../core/timer.js';
4
+ import * as browser from '../core/browser.js';
5
+ import * as logger from '../core/logger.js';
6
+ import * as stats from '../core/stats.js';
7
+ import { printBanner, VERSION } from '../core/banner.js';
8
+ import chalk from 'chalk';
9
+
10
+ export async function startCommand(options = {}) {
11
+ const config = await configManager.getConfig();
12
+ const today = schedule.getTodaySkill(config);
13
+
14
+ // Compact banner only — `start` is the focus moment, not a stats dashboard.
15
+ if (options.banner !== false) {
16
+ await printBanner({
17
+ compact: true,
18
+ context: { version: VERSION, initialized: true, skill: today.skill, focus: today.focus }
19
+ });
20
+ }
21
+
22
+ if (today.skill === 'rest') {
23
+ console.log(chalk.green('☀️ Sunday is rest day. System is still alive. Come back tomorrow at 9 PM.'));
24
+ return;
25
+ }
26
+
27
+ if (today.skill === 'review') {
28
+ console.log(chalk.blue('📋 Saturday — Weekly Review Day'));
29
+ console.log('Open your log file and read all 5 logs from this week.');
30
+ console.log('Ask: Did I show up? What pattern do I see? What needs more time next week?');
31
+ console.log(chalk.gray('Run `devgrowth review` when you are done.'));
32
+ return;
33
+ }
34
+
35
+ const isLight = options.light || false;
36
+ const now = schedule.getSessionDate(new Date());
37
+
38
+ console.log(chalk.bold(`\n🎯 Tonight: ${today.skill.toUpperCase()} — ${today.focus}`));
39
+ console.log(chalk.gray(`Resource: ${today.resource || 'N/A'}`));
40
+
41
+ if (isLight) {
42
+ console.log(chalk.yellow('\n🟡 Light session mode — timer skipped.'));
43
+ console.log('Just write your log and close.\n');
44
+ return;
45
+ }
46
+
47
+ // Warm-up prompt
48
+ console.log(chalk.cyan('\n⏱️ Warm up: Read yesterday\'s log (2 min)...'));
49
+
50
+ // Open resource
51
+ if (today.url) {
52
+ console.log(chalk.gray(`Opening ${today.url}...`));
53
+ try {
54
+ await browser.openUrl(today.url);
55
+ } catch {
56
+ console.log(chalk.yellow(`⚠️ Could not open the browser (offline?). Visit manually: ${today.url}`));
57
+ }
58
+ }
59
+
60
+ // Start timer
61
+ const duration = config.session?.durationMinutes || 20;
62
+ console.log(chalk.bold(`\n🔥 Deep work: ${duration} minutes`));
63
+ console.log(chalk.gray('Press Ctrl+C to interrupt\n'));
64
+
65
+ const timerResult = await timer.start(duration * 60, {
66
+ onTick: (remaining) => {
67
+ const mins = Math.floor(remaining / 60);
68
+ const secs = remaining % 60;
69
+ const line = `${chalk.yellow('⏳')} ${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')} remaining`;
70
+ process.stdout.write(`\r\x1b[K${line}`);
71
+ },
72
+ onWarning: () => {
73
+ console.log(chalk.yellow('\n⚠️ 1 minute left. Wrap up.'));
74
+ },
75
+ onComplete: () => {
76
+ console.log(chalk.green('\n✅ Session complete!'));
77
+ }
78
+ });
79
+
80
+ process.stdout.write('\n');
81
+
82
+ if (timerResult.interrupted) {
83
+ console.log(chalk.red('\n⏹️ Session interrupted. Saving partial log...'));
84
+ await logger.writeLogEntry(now, today.skill, today.resource, duration, 'Interrupted', 'Session was interrupted.');
85
+ await stats.recordSession(now, today.skill, 0, 'Interrupted');
86
+ return;
87
+ }
88
+
89
+ console.log(chalk.green('\n📝 Time to log. Run:'));
90
+ console.log(chalk.bold(' devgrowth log "Today I learned ___. Tomorrow I will ___."\n'));
91
+ }
@@ -0,0 +1,53 @@
1
+ import * as configManager from '../core/configManager.js';
2
+ import * as schedule from '../core/schedule.js';
3
+ import * as stats from '../core/stats.js';
4
+ import * as logger from '../core/logger.js';
5
+ import chalk from 'chalk';
6
+
7
+ const DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
8
+
9
+ export async function statusCommand() {
10
+ const config = await configManager.getConfig();
11
+ const status = await stats.getStatus();
12
+ const now = new Date();
13
+ const weekStart = schedule.getWeekStart(now);
14
+
15
+ console.log(chalk.bold(`\nWeek ${schedule.getWeekNumber(now)} (${schedule.formatShortDate(weekStart)} – ${schedule.formatShortDate(addDays(weekStart, 6))})`));
16
+ console.log(chalk.gray('─'.repeat(50)));
17
+
18
+ // The whole week lives in a single log file keyed by the Monday's year/month
19
+ const logYear = weekStart.getFullYear();
20
+ const logMonth = String(weekStart.getMonth() + 1).padStart(2, '0');
21
+ const logs = await logger.readWeekLogs(logYear, logMonth, `week${schedule.getWeekNumber(now)}.md`);
22
+
23
+ for (let i = 0; i < 7; i++) {
24
+ const dayDate = addDays(weekStart, i);
25
+ const dayKey = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'][i];
26
+ const dayConfig = config.schedule[dayKey];
27
+
28
+ const dayStr = schedule.formatShortDate(dayDate);
29
+ // Match against the markdown header to avoid false positives
30
+ const headerPattern = new RegExp(`^## ${DAYS[i]}, ${dayStr}`, 'm');
31
+ const hasLog = headerPattern.test(logs);
32
+ const emoji = hasLog ? '✅' : (isToday(dayDate, now) ? '⬜' : '◻');
33
+ const label = hasLog ? chalk.green(dayConfig.skill) : chalk.gray(dayConfig.skill);
34
+
35
+ console.log(`${emoji} ${DAYS[i].padEnd(3)} ${label.padEnd(10)} ${hasLog ? chalk.dim('Logged') : (isToday(dayDate, now) ? chalk.yellow('Today') : '—')}`);
36
+ }
37
+
38
+ console.log(chalk.gray('─'.repeat(50)));
39
+ console.log(`${status.currentWeek.completed}/5 sessions this week | Streak: ${chalk.bold(status.currentStreakWeeks)} weeks | Longest: ${status.longestStreakWeeks}`);
40
+ console.log();
41
+ }
42
+
43
+ function addDays(date, days) {
44
+ const result = new Date(date);
45
+ result.setDate(result.getDate() + days);
46
+ return result;
47
+ }
48
+
49
+ function isToday(d1, d2) {
50
+ return d1.getFullYear() === d2.getFullYear() &&
51
+ d1.getMonth() === d2.getMonth() &&
52
+ d1.getDate() === d2.getDate();
53
+ }