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 +21 -0
- package/README.md +90 -0
- package/bin/devgrowth.js +3 -0
- package/package.json +48 -0
- package/scripts/install-windows.ps1 +28 -0
- package/src/commands/config.js +50 -0
- package/src/commands/history.js +73 -0
- package/src/commands/init.js +31 -0
- package/src/commands/log.js +36 -0
- package/src/commands/milestone.js +51 -0
- package/src/commands/notify.js +23 -0
- package/src/commands/review.js +57 -0
- package/src/commands/start.js +91 -0
- package/src/commands/status.js +53 -0
- package/src/core/banner.js +293 -0
- package/src/core/browser.js +6 -0
- package/src/core/configManager.js +70 -0
- package/src/core/logger.js +62 -0
- package/src/core/milestones.js +72 -0
- package/src/core/notifier.js +18 -0
- package/src/core/schedule.js +70 -0
- package/src/core/stats.js +130 -0
- package/src/core/storage.js +33 -0
- package/src/core/timer.js +72 -0
- package/src/index.js +152 -0
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.
|
package/bin/devgrowth.js
ADDED
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
|
+
}
|