devgrowth 1.0.0 → 1.1.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/README.md +135 -90
- package/package.json +52 -48
- package/src/commands/config.js +54 -50
- package/src/commands/dashboard.js +13 -0
- package/src/commands/history.js +75 -73
- package/src/commands/log.js +40 -36
- package/src/commands/login.js +127 -0
- package/src/commands/logout.js +26 -0
- package/src/commands/milestone.js +54 -51
- package/src/commands/review.js +62 -57
- package/src/commands/start.js +97 -91
- package/src/commands/status.js +55 -53
- package/src/commands/sync.js +29 -0
- package/src/core/apiClient.js +46 -0
- package/src/core/auth.js +34 -0
- package/src/core/banner.js +293 -293
- package/src/core/configManager.js +57 -70
- package/src/core/eventLog.js +112 -0
- package/src/core/logger.js +62 -62
- package/src/core/milestones.js +62 -72
- package/src/core/prompt.js +58 -0
- package/src/core/rebuild.js +37 -0
- package/src/core/schedule.js +1 -70
- package/src/core/stats.js +43 -130
- package/src/core/storage.js +52 -33
- package/src/core/sync.js +101 -0
- package/src/index.js +221 -152
package/src/commands/start.js
CHANGED
|
@@ -1,91 +1,97 @@
|
|
|
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
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
console.log('
|
|
31
|
-
console.log(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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 * as eventLog from '../core/eventLog.js';
|
|
8
|
+
import * as sync from '../core/sync.js';
|
|
9
|
+
import { printBanner, VERSION } from '../core/banner.js';
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
|
|
12
|
+
export async function startCommand(options = {}) {
|
|
13
|
+
const config = await configManager.getConfig();
|
|
14
|
+
const today = schedule.getTodaySkill(config);
|
|
15
|
+
|
|
16
|
+
// Compact banner only — `start` is the focus moment, not a stats dashboard.
|
|
17
|
+
if (options.banner !== false) {
|
|
18
|
+
await printBanner({
|
|
19
|
+
compact: true,
|
|
20
|
+
context: { version: VERSION, initialized: true, skill: today.skill, focus: today.focus }
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (today.skill === 'rest') {
|
|
25
|
+
console.log(chalk.green('☀️ Sunday is rest day. System is still alive. Come back tomorrow at 9 PM.'));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (today.skill === 'review') {
|
|
30
|
+
console.log(chalk.blue('📋 Saturday — Weekly Review Day'));
|
|
31
|
+
console.log('Open your log file and read all 5 logs from this week.');
|
|
32
|
+
console.log('Ask: Did I show up? What pattern do I see? What needs more time next week?');
|
|
33
|
+
console.log(chalk.gray('Run `devgrowth review` when you are done.'));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const isLight = options.light || false;
|
|
38
|
+
const now = schedule.getSessionDate(new Date());
|
|
39
|
+
|
|
40
|
+
console.log(chalk.bold(`\n🎯 Tonight: ${today.skill.toUpperCase()} — ${today.focus}`));
|
|
41
|
+
console.log(chalk.gray(`Resource: ${today.resource || 'N/A'}`));
|
|
42
|
+
|
|
43
|
+
if (isLight) {
|
|
44
|
+
console.log(chalk.yellow('\n🟡 Light session mode — timer skipped.'));
|
|
45
|
+
console.log('Just write your log and close.\n');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Warm-up prompt
|
|
50
|
+
console.log(chalk.cyan('\n⏱️ Warm up: Read yesterday\'s log (2 min)...'));
|
|
51
|
+
|
|
52
|
+
// Open resource
|
|
53
|
+
if (today.url) {
|
|
54
|
+
console.log(chalk.gray(`Opening ${today.url}...`));
|
|
55
|
+
try {
|
|
56
|
+
await browser.openUrl(today.url);
|
|
57
|
+
} catch {
|
|
58
|
+
console.log(chalk.yellow(`⚠️ Could not open the browser (offline?). Visit manually: ${today.url}`));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Start timer
|
|
63
|
+
const duration = config.session?.durationMinutes || 20;
|
|
64
|
+
console.log(chalk.bold(`\n🔥 Deep work: ${duration} minutes`));
|
|
65
|
+
console.log(chalk.gray('Press Ctrl+C to interrupt\n'));
|
|
66
|
+
|
|
67
|
+
const timerResult = await timer.start(duration * 60, {
|
|
68
|
+
onTick: (remaining) => {
|
|
69
|
+
const mins = Math.floor(remaining / 60);
|
|
70
|
+
const secs = remaining % 60;
|
|
71
|
+
const line = `${chalk.yellow('⏳')} ${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')} remaining`;
|
|
72
|
+
process.stdout.write(`\r\x1b[K${line}`);
|
|
73
|
+
},
|
|
74
|
+
onWarning: () => {
|
|
75
|
+
console.log(chalk.yellow('\n⚠️ 1 minute left. Wrap up.'));
|
|
76
|
+
},
|
|
77
|
+
onComplete: () => {
|
|
78
|
+
console.log(chalk.green('\n✅ Session complete!'));
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
process.stdout.write('\n');
|
|
83
|
+
|
|
84
|
+
if (timerResult.interrupted) {
|
|
85
|
+
console.log(chalk.red('\n⏹️ Session interrupted. Saving partial log...'));
|
|
86
|
+
await logger.writeLogEntry(now, today.skill, today.resource, duration, 'Interrupted', 'Session was interrupted.');
|
|
87
|
+
await stats.recordSession(now, today.skill, 0, 'Interrupted');
|
|
88
|
+
await eventLog.recordSession(now, {
|
|
89
|
+
skill: today.skill, resource: today.resource, duration, status: 'Interrupted', message: 'Session was interrupted.'
|
|
90
|
+
});
|
|
91
|
+
await sync.trySync();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log(chalk.green('\n📝 Time to log. Run:'));
|
|
96
|
+
console.log(chalk.bold(' devgrowth log "Today I learned ___. Tomorrow I will ___."\n'));
|
|
97
|
+
}
|
package/src/commands/status.js
CHANGED
|
@@ -1,53 +1,55 @@
|
|
|
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
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
console.log();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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 * as sync from '../core/sync.js';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
|
|
8
|
+
const DAYS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
|
9
|
+
|
|
10
|
+
export async function statusCommand() {
|
|
11
|
+
await sync.trySync();
|
|
12
|
+
const config = await configManager.getConfig();
|
|
13
|
+
const status = await stats.getStatus();
|
|
14
|
+
const now = new Date();
|
|
15
|
+
const weekStart = schedule.getWeekStart(now);
|
|
16
|
+
|
|
17
|
+
console.log(chalk.bold(`\nWeek ${schedule.getWeekNumber(now)} (${schedule.formatShortDate(weekStart)} – ${schedule.formatShortDate(addDays(weekStart, 6))})`));
|
|
18
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
19
|
+
|
|
20
|
+
// The whole week lives in a single log file keyed by the Monday's year/month
|
|
21
|
+
const logYear = weekStart.getFullYear();
|
|
22
|
+
const logMonth = String(weekStart.getMonth() + 1).padStart(2, '0');
|
|
23
|
+
const logs = await logger.readWeekLogs(logYear, logMonth, `week${schedule.getWeekNumber(now)}.md`);
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < 7; i++) {
|
|
26
|
+
const dayDate = addDays(weekStart, i);
|
|
27
|
+
const dayKey = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'][i];
|
|
28
|
+
const dayConfig = config.schedule[dayKey];
|
|
29
|
+
|
|
30
|
+
const dayStr = schedule.formatShortDate(dayDate);
|
|
31
|
+
// Match against the markdown header to avoid false positives
|
|
32
|
+
const headerPattern = new RegExp(`^## ${DAYS[i]}, ${dayStr}`, 'm');
|
|
33
|
+
const hasLog = headerPattern.test(logs);
|
|
34
|
+
const emoji = hasLog ? '✅' : (isToday(dayDate, now) ? '⬜' : '◻');
|
|
35
|
+
const label = hasLog ? chalk.green(dayConfig.skill) : chalk.gray(dayConfig.skill);
|
|
36
|
+
|
|
37
|
+
console.log(`${emoji} ${DAYS[i].padEnd(3)} ${label.padEnd(10)} ${hasLog ? chalk.dim('Logged') : (isToday(dayDate, now) ? chalk.yellow('Today') : '—')}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
41
|
+
console.log(`${status.currentWeek.completed}/5 sessions this week | Streak: ${chalk.bold(status.currentStreakWeeks)} weeks | Longest: ${status.longestStreakWeeks}`);
|
|
42
|
+
console.log();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function addDays(date, days) {
|
|
46
|
+
const result = new Date(date);
|
|
47
|
+
result.setDate(result.getDate() + days);
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isToday(d1, d2) {
|
|
52
|
+
return d1.getFullYear() === d2.getFullYear() &&
|
|
53
|
+
d1.getMonth() === d2.getMonth() &&
|
|
54
|
+
d1.getDate() === d2.getDate();
|
|
55
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import * as auth from '../core/auth.js';
|
|
3
|
+
import * as eventLog from '../core/eventLog.js';
|
|
4
|
+
import * as sync from '../core/sync.js';
|
|
5
|
+
|
|
6
|
+
async function showStatus() {
|
|
7
|
+
const session = await auth.getAuth();
|
|
8
|
+
if (!session) {
|
|
9
|
+
console.log('Not logged in. Run `devgrowth login` to sync across devices.');
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const state = await eventLog.getSyncState();
|
|
13
|
+
console.log(chalk.bold('Sync'));
|
|
14
|
+
console.log(` Account: ${session.email}`);
|
|
15
|
+
console.log(` Server: ${session.apiUrl}`);
|
|
16
|
+
console.log(` Pending: ${state.outbox.length} change(s) not yet uploaded`);
|
|
17
|
+
console.log(` Last synced: ${state.lastSyncAt ? new Date(state.lastSyncAt).toLocaleString() : 'never'}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function syncCommand(subcommand) {
|
|
21
|
+
if (subcommand === 'status') return showStatus();
|
|
22
|
+
if (subcommand !== undefined) {
|
|
23
|
+
console.error(chalk.red(`Unknown subcommand: ${subcommand}`));
|
|
24
|
+
console.log('Usage: devgrowth sync [status]');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
const { pushed, pulled } = await sync.sync();
|
|
28
|
+
console.log(chalk.green(`✓ Synced: ${pushed} uploaded, ${pulled} downloaded.`));
|
|
29
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Minimal client for the devgrowth-cloud API, on Node 18+'s built-in fetch.
|
|
2
|
+
|
|
3
|
+
export class ApiError extends Error {
|
|
4
|
+
constructor(status, code, message, details) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.details = details;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* `request(apiUrl, 'POST', '/v1/events', { token, body, timeoutMs })`.
|
|
14
|
+
* Resolves to the parsed JSON body (null for 204). Throws ApiError, with
|
|
15
|
+
* status 0 and code `network_error` when the server can't be reached in time.
|
|
16
|
+
*/
|
|
17
|
+
export async function request(apiUrl, method, apiPath, { token, body, timeoutMs = 10000 } = {}) {
|
|
18
|
+
const headers = {};
|
|
19
|
+
if (token) headers.authorization = `Bearer ${token}`;
|
|
20
|
+
if (body !== undefined) headers['content-type'] = 'application/json';
|
|
21
|
+
|
|
22
|
+
const controller = new AbortController();
|
|
23
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
24
|
+
let res;
|
|
25
|
+
try {
|
|
26
|
+
res = await fetch(`${apiUrl.replace(/\/+$/, '')}${apiPath}`, {
|
|
27
|
+
method,
|
|
28
|
+
headers,
|
|
29
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
30
|
+
signal: controller.signal
|
|
31
|
+
});
|
|
32
|
+
} catch (err) {
|
|
33
|
+
const reason = err.name === 'AbortError' ? 'timed out' : (err.cause?.code || err.message);
|
|
34
|
+
throw new ApiError(0, 'network_error', `Could not reach ${apiUrl} (${reason})`);
|
|
35
|
+
} finally {
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (res.status === 204) return null;
|
|
40
|
+
const data = await res.json().catch(() => null);
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
const err = data?.error;
|
|
43
|
+
throw new ApiError(res.status, err?.code ?? 'http_error', err?.message ?? `Request failed (${res.status})`, err?.details);
|
|
44
|
+
}
|
|
45
|
+
return data;
|
|
46
|
+
}
|
package/src/core/auth.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as storage from './storage.js';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
// ~/.devgrowth/auth.json: { apiUrl, token, userId, deviceId, email }.
|
|
5
|
+
// The token is a credential, so the file is readable by its owner only.
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_API_URL = 'http://localhost:3000';
|
|
8
|
+
|
|
9
|
+
function authPath() {
|
|
10
|
+
return path.join(storage.getDevGrowthDir(), 'auth.json');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function getAuth() {
|
|
14
|
+
const file = authPath();
|
|
15
|
+
return (await storage.fileExists(file)) ? await storage.readJson(file) : null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function saveAuth(auth) {
|
|
19
|
+
await storage.writeJson(authPath(), auth, { mode: 0o600 });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function clearAuth() {
|
|
23
|
+
await storage.removePath(authPath());
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function isLoggedIn() {
|
|
27
|
+
return (await getAuth()) !== null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Where to find the server: flag, then env, then the saved login, then localhost. */
|
|
31
|
+
export async function resolveApiUrl(flag) {
|
|
32
|
+
const saved = await getAuth();
|
|
33
|
+
return (flag || process.env.DEVGROWTH_API_URL || saved?.apiUrl || DEFAULT_API_URL).replace(/\/+$/, '');
|
|
34
|
+
}
|