devgrowth 1.0.1 → 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 -111
- 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/log.js
CHANGED
|
@@ -1,36 +1,40 @@
|
|
|
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
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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 * as eventLog from '../core/eventLog.js';
|
|
6
|
+
import * as sync from '../core/sync.js';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
|
|
9
|
+
export async function logCommand(message, options = {}) {
|
|
10
|
+
if (!message || message.trim().length === 0) {
|
|
11
|
+
console.error(chalk.red('Error: Log message required.'));
|
|
12
|
+
console.log('Usage: devgrowth log "Today I learned ___. Tomorrow I will ___."');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (message.length < 10) {
|
|
17
|
+
console.log(chalk.yellow('⚠️ Warning: Log message is very short, but saving anyway.'));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const config = await configManager.getConfig();
|
|
21
|
+
const today = schedule.getTodaySkill(config);
|
|
22
|
+
const now = schedule.getSessionDate(new Date());
|
|
23
|
+
const isLight = options.light || false;
|
|
24
|
+
const status = isLight ? 'Light' : 'Completed';
|
|
25
|
+
const duration = config.session?.durationMinutes || 20;
|
|
26
|
+
|
|
27
|
+
await logger.writeLogEntry(now, today.skill, today.resource, duration, status, message);
|
|
28
|
+
const updatedStats = await stats.recordSession(now, today.skill, duration, status);
|
|
29
|
+
await eventLog.recordSession(now, { skill: today.skill, resource: today.resource, duration, status, message });
|
|
30
|
+
await sync.trySync();
|
|
31
|
+
|
|
32
|
+
const weekFile = schedule.getWeekFileName(now);
|
|
33
|
+
const weekStart = schedule.getWeekStart(now);
|
|
34
|
+
const logYear = weekStart.getFullYear();
|
|
35
|
+
const logMonth = String(weekStart.getMonth() + 1).padStart(2, '0');
|
|
36
|
+
console.log(chalk.green('✓ Log saved.'));
|
|
37
|
+
console.log(chalk.gray(` File: ~/.devgrowth/logs/${logYear}/${logMonth}/${weekFile}`));
|
|
38
|
+
console.log(chalk.gray(` Week progress: ${updatedStats.currentWeek.completed}/5 sessions`));
|
|
39
|
+
console.log(chalk.gray(` Streak: ${updatedStats.currentStreakWeeks} weeks (longest: ${updatedStats.longestStreakWeeks})`));
|
|
40
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import * as os from 'os';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import * as coreEvents from 'devgrowth-core/events';
|
|
5
|
+
import * as auth from '../core/auth.js';
|
|
6
|
+
import * as apiClient from '../core/apiClient.js';
|
|
7
|
+
import * as prompt from '../core/prompt.js';
|
|
8
|
+
import * as eventLog from '../core/eventLog.js';
|
|
9
|
+
import * as sync from '../core/sync.js';
|
|
10
|
+
import * as stats from '../core/stats.js';
|
|
11
|
+
import * as milestones from '../core/milestones.js';
|
|
12
|
+
import * as configManager from '../core/configManager.js';
|
|
13
|
+
import * as storage from '../core/storage.js';
|
|
14
|
+
|
|
15
|
+
async function hasLocalHistory() {
|
|
16
|
+
const dir = storage.getDevGrowthDir();
|
|
17
|
+
if ((await eventLog.readAllEvents()).length > 0) return true;
|
|
18
|
+
if ((await storage.listDir(path.join(dir, 'logs'))).length > 0) return true;
|
|
19
|
+
return (await stats.getStats()).totalSessions > 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Copy ~/.devgrowth (minus older backups and the credential) to ~/.devgrowth/backups/<timestamp>. */
|
|
23
|
+
async function backupLocalData() {
|
|
24
|
+
const dir = storage.getDevGrowthDir();
|
|
25
|
+
const dest = path.join(dir, 'backups', new Date().toISOString().replace(/[:.]/g, '-'));
|
|
26
|
+
for (const entry of await storage.listDir(dir)) {
|
|
27
|
+
if (entry === 'backups' || entry === 'auth.json') continue;
|
|
28
|
+
await storage.copyPath(path.join(dir, entry), path.join(dest, entry));
|
|
29
|
+
}
|
|
30
|
+
return dest;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* First login on this device:
|
|
35
|
+
* - Empty account: this device's current stats, milestones and config become
|
|
36
|
+
* the account's starting point (a `baseline` event), then everything syncs up.
|
|
37
|
+
* - Account already has data: this device adopts it. Local data is backed up,
|
|
38
|
+
* then replaced by the account's history (stats, milestones, config and
|
|
39
|
+
* Markdown logs are rebuilt from the synced events).
|
|
40
|
+
*
|
|
41
|
+
* All local preparation (the baseline event, or backup + reset) happens before
|
|
42
|
+
* the first network sync, so if that sync fails nothing is lost: the next
|
|
43
|
+
* command's trySync() pushes the queued baseline or pulls the history.
|
|
44
|
+
*/
|
|
45
|
+
async function connectDevice(me) {
|
|
46
|
+
if (me.schemaVersion > coreEvents.SCHEMA_VERSION) {
|
|
47
|
+
console.log(chalk.yellow('⚠️ The server has data from a newer devgrowth. Update with: npm install -g devgrowth'));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (me.eventCount === 0) {
|
|
51
|
+
let config = null;
|
|
52
|
+
try { config = await configManager.getConfig(); } catch { /* not initialized: no config to share */ }
|
|
53
|
+
await eventLog.recordEvent('baseline', {
|
|
54
|
+
stats: await stats.getStats(),
|
|
55
|
+
milestones: await milestones.getMilestones(),
|
|
56
|
+
config
|
|
57
|
+
});
|
|
58
|
+
const result = await firstSync({});
|
|
59
|
+
if (result) {
|
|
60
|
+
console.log(chalk.green(`✓ Uploaded this device's history (${result.pushed} events). Other devices will pick it up when they log in.`));
|
|
61
|
+
}
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const dir = storage.getDevGrowthDir();
|
|
66
|
+
if (await hasLocalHistory()) {
|
|
67
|
+
const backup = await backupLocalData();
|
|
68
|
+
console.log(chalk.gray(` Backed up this device's previous data to ${backup}`));
|
|
69
|
+
}
|
|
70
|
+
await eventLog.resetLocalLog();
|
|
71
|
+
for (const name of ['stats.json', 'milestones.json', 'logs']) {
|
|
72
|
+
await storage.removePath(path.join(dir, name));
|
|
73
|
+
}
|
|
74
|
+
// The local config.json is replaced by the account's, not pushed over it.
|
|
75
|
+
const result = await firstSync({ detectConfigDrift: false });
|
|
76
|
+
if (!(await storage.fileExists(path.join(dir, 'config.json')))) {
|
|
77
|
+
await configManager.createDefaultConfig();
|
|
78
|
+
}
|
|
79
|
+
if (result) console.log(chalk.green(`✓ Synced ${result.pulled} events from your account.`));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function firstSync(options) {
|
|
83
|
+
try {
|
|
84
|
+
return await sync.sync(options);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.log(chalk.yellow(`⚠️ Logged in, but the first sync failed (${err.message}). It will retry on your next command, or run \`devgrowth sync\`.`));
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function authenticate(kind, options) {
|
|
92
|
+
const existing = await auth.getAuth();
|
|
93
|
+
if (existing) {
|
|
94
|
+
throw new Error(`Already logged in as ${existing.email}. Run \`devgrowth logout\` first.`);
|
|
95
|
+
}
|
|
96
|
+
const apiUrl = await auth.resolveApiUrl(options.api);
|
|
97
|
+
const email = options.email || await prompt.ask('Email: ');
|
|
98
|
+
if (!email) throw new Error('Email is required.');
|
|
99
|
+
const password = await prompt.askHidden('Password: ');
|
|
100
|
+
if (!password) throw new Error('Password is required.');
|
|
101
|
+
|
|
102
|
+
const endpoint = kind === 'register' ? '/v1/auth/register' : '/v1/auth/login';
|
|
103
|
+
const body = await apiClient.request(apiUrl, 'POST', endpoint, {
|
|
104
|
+
body: { email, password, deviceName: os.hostname() || 'cli', kind: 'cli' }
|
|
105
|
+
});
|
|
106
|
+
// Ask whether the account is empty *before* saving the login: if this fails,
|
|
107
|
+
// we must not end up logged in without the first-login step (a device with
|
|
108
|
+
// no baseline would later rebuild its stats without its pre-sync history).
|
|
109
|
+
let me;
|
|
110
|
+
try {
|
|
111
|
+
me = await apiClient.request(apiUrl, 'GET', '/v1/me', { token: body.token });
|
|
112
|
+
} catch (err) {
|
|
113
|
+
await apiClient.request(apiUrl, 'POST', '/v1/auth/logout', { token: body.token, timeoutMs: 3000 }).catch(() => {});
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
await auth.saveAuth({ apiUrl, token: body.token, userId: body.userId, deviceId: body.deviceId, email });
|
|
117
|
+
console.log(chalk.green(`✓ ${kind === 'register' ? 'Account created. ' : ''}Logged in as ${email} on ${apiUrl}`));
|
|
118
|
+
await connectDevice(me);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function loginCommand(options = {}) {
|
|
122
|
+
await authenticate('login', options);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function registerCommand(options = {}) {
|
|
126
|
+
await authenticate('register', options);
|
|
127
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import * as auth from '../core/auth.js';
|
|
3
|
+
import * as apiClient from '../core/apiClient.js';
|
|
4
|
+
import * as eventLog from '../core/eventLog.js';
|
|
5
|
+
import * as sync from '../core/sync.js';
|
|
6
|
+
|
|
7
|
+
export async function logoutCommand() {
|
|
8
|
+
const session = await auth.getAuth();
|
|
9
|
+
if (!session) {
|
|
10
|
+
console.log('Not logged in.');
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
// Push anything pending first so it isn't stranded on this device.
|
|
14
|
+
await sync.trySync();
|
|
15
|
+
const { outbox } = await eventLog.getSyncState();
|
|
16
|
+
try {
|
|
17
|
+
await apiClient.request(session.apiUrl, 'POST', '/v1/auth/logout', { token: session.token, timeoutMs: 3000 });
|
|
18
|
+
} catch {
|
|
19
|
+
// Offline or already revoked: forgetting the token locally is what matters.
|
|
20
|
+
}
|
|
21
|
+
await auth.clearAuth();
|
|
22
|
+
console.log(chalk.green(`✓ Logged out of ${session.email}. Your local data stays on this device.`));
|
|
23
|
+
if (outbox.length > 0) {
|
|
24
|
+
console.log(chalk.yellow(`⚠️ ${outbox.length} change(s) could not be synced. Log in again while online to upload them.`));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -1,51 +1,54 @@
|
|
|
1
|
-
import * as milestones from '../core/milestones.js';
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
1
|
+
import * as milestones from '../core/milestones.js';
|
|
2
|
+
import * as sync from '../core/sync.js';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
export async function milestoneCommand(subcommand, args = []) {
|
|
6
|
+
if (['list', 'check', 'progress'].includes(subcommand)) await sync.trySync();
|
|
7
|
+
switch (subcommand) {
|
|
8
|
+
case 'list': {
|
|
9
|
+
const data = await milestones.getMilestones();
|
|
10
|
+
for (const [skill, config] of Object.entries(data)) {
|
|
11
|
+
console.log(chalk.bold(`\n${skill.toUpperCase()}`));
|
|
12
|
+
console.log(chalk.gray('\u2500'.repeat(40)));
|
|
13
|
+
for (const [monthKey, monthData] of Object.entries(config)) {
|
|
14
|
+
console.log(chalk.cyan(` ${monthKey.toUpperCase()}: ${monthData.title}`));
|
|
15
|
+
for (const [itemKey, done] of Object.entries(monthData.items)) {
|
|
16
|
+
const emoji = done ? '\u2705' : '\u2B1C';
|
|
17
|
+
console.log(` ${emoji} ${itemKey}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
console.log();
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
case 'check': {
|
|
25
|
+
const [skill, month, item] = args;
|
|
26
|
+
if (!skill || !month || !item) {
|
|
27
|
+
console.error(chalk.red('Usage: devgrowth milestone check <skill> <month> <item>'));
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
await milestones.checkMilestone(skill, month, item);
|
|
31
|
+
await sync.trySync();
|
|
32
|
+
console.log(chalk.green(`\u2705 Milestone checked: ${skill} ${month} ${item}`));
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
case 'progress': {
|
|
36
|
+
const [skill] = args;
|
|
37
|
+
if (!skill) {
|
|
38
|
+
console.error(chalk.red('Usage: devgrowth milestone progress <skill>'));
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
const progress = await milestones.getProgress(skill);
|
|
42
|
+
const barLength = 20;
|
|
43
|
+
const filled = Math.round((progress.completed / progress.total) * barLength);
|
|
44
|
+
const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barLength - filled);
|
|
45
|
+
console.log(`${chalk.bold(skill.toUpperCase())} ${bar} ${progress.percentage}% (${progress.completed}/${progress.total})`);
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
default: {
|
|
49
|
+
console.error(chalk.red(`Unknown subcommand: ${subcommand}`));
|
|
50
|
+
console.log('Usage: devgrowth milestone <list|check|progress>');
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/commands/review.js
CHANGED
|
@@ -1,57 +1,62 @@
|
|
|
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
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
console.log('
|
|
34
|
-
console.log('
|
|
35
|
-
console.log();
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
await storage.
|
|
55
|
-
|
|
56
|
-
|
|
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 eventLog from '../core/eventLog.js';
|
|
6
|
+
import * as sync from '../core/sync.js';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
|
|
10
|
+
export async function reviewCommand() {
|
|
11
|
+
await sync.trySync();
|
|
12
|
+
const now = new Date();
|
|
13
|
+
// Key the log file by this week's Monday so month/year boundaries don't split logs
|
|
14
|
+
const weekStart = schedule.getWeekStart(now);
|
|
15
|
+
const year = weekStart.getFullYear();
|
|
16
|
+
const month = String(weekStart.getMonth() + 1).padStart(2, '0');
|
|
17
|
+
const weekFile = schedule.getCurrentWeekFileName(now);
|
|
18
|
+
|
|
19
|
+
const logs = await logger.readWeekLogs(year, month, weekFile);
|
|
20
|
+
|
|
21
|
+
console.log(chalk.bold('\nWeekly Review'));
|
|
22
|
+
console.log(chalk.gray('\u2500'.repeat(50)));
|
|
23
|
+
|
|
24
|
+
const entries = logger.parseLogEntries(logs);
|
|
25
|
+
if (entries.length === 0) {
|
|
26
|
+
console.log(chalk.yellow('No logs found for this week.'));
|
|
27
|
+
} else {
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
console.log(`${chalk.cyan(entry.date)} \u2014 ${entry.message}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log(chalk.gray('\u2500'.repeat(50)));
|
|
34
|
+
console.log(chalk.bold('Review Questions:'));
|
|
35
|
+
console.log('1. What was your biggest win this week?');
|
|
36
|
+
console.log('2. What blocked you?');
|
|
37
|
+
console.log('3. What will you focus on next week?');
|
|
38
|
+
console.log();
|
|
39
|
+
|
|
40
|
+
const result = await stats.finalizeWeek(now);
|
|
41
|
+
if (result.justFinalized) {
|
|
42
|
+
await eventLog.recordReview(now);
|
|
43
|
+
console.log(chalk.green(`Week finalized! Streak: ${result.currentStreakWeeks} weeks | Longest: ${result.longestStreakWeeks} weeks`));
|
|
44
|
+
} else {
|
|
45
|
+
console.log(chalk.yellow(`Week already finalized. Streak unchanged: ${result.currentStreakWeeks} weeks (longest: ${result.longestStreakWeeks})`));
|
|
46
|
+
}
|
|
47
|
+
console.log();
|
|
48
|
+
|
|
49
|
+
// Append review block to the week's markdown file (only on first finalize)
|
|
50
|
+
if (result.justFinalized) {
|
|
51
|
+
const filePath = path.join(storage.getDevGrowthDir(), 'logs', String(year), month, weekFile);
|
|
52
|
+
const exists = await storage.fileExists(filePath);
|
|
53
|
+
if (exists) {
|
|
54
|
+
let content = await storage.readFile(filePath);
|
|
55
|
+
const weekNum = schedule.getWeekNumber(now);
|
|
56
|
+
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`;
|
|
57
|
+
content += reviewBlock;
|
|
58
|
+
await storage.writeFile(filePath, content);
|
|
59
|
+
}
|
|
60
|
+
await sync.trySync();
|
|
61
|
+
}
|
|
62
|
+
}
|