roguelike-cli 1.3.0 → 1.3.1
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 +3 -1
- package/dist/interactive/commands.js +141 -10
- package/dist/interactive/startup.js +2 -2
- package/dist/storage/profile.js +47 -0
- package/package.json +1 -1
- package/src/interactive/commands.ts +161 -11
- package/src/interactive/startup.ts +2 -2
- package/src/storage/profile.ts +65 -0
package/README.md
CHANGED
|
@@ -95,11 +95,13 @@ You are now level 2!
|
|
|
95
95
|
| Command | Description |
|
|
96
96
|
|---------|-------------|
|
|
97
97
|
| `done` | Complete task (recursive, earns XP) |
|
|
98
|
+
| `undo` | Undo last done (restores XP) |
|
|
98
99
|
| `deadline <date>` | Set deadline |
|
|
99
100
|
| `boss` | Toggle boss status (3x XP) |
|
|
100
|
-
| `block [
|
|
101
|
+
| `block [node]` | Block by task or text reason |
|
|
101
102
|
| `unblock` | Remove blocked status |
|
|
102
103
|
| `status` | Show task details |
|
|
104
|
+
| `check` | Show overdue/upcoming deadlines |
|
|
103
105
|
|
|
104
106
|
### Gamification
|
|
105
107
|
|
|
@@ -428,6 +428,13 @@ async function processCommand(input, currentPath, config, signal, rl) {
|
|
|
428
428
|
// Update profile with XP and achievements
|
|
429
429
|
const depth = getDepth(currentPath, config.storagePath);
|
|
430
430
|
const taskResult = (0, profile_1.completeTask)(result.xpGained, nodeConfig.isBoss || false, depth, nodeConfig.createdAt);
|
|
431
|
+
// Save to undo history
|
|
432
|
+
(0, profile_1.addToUndoHistory)({
|
|
433
|
+
path: currentPath,
|
|
434
|
+
xpLost: result.xpGained,
|
|
435
|
+
wasBoss: nodeConfig.isBoss || false,
|
|
436
|
+
timestamp: new Date().toISOString(),
|
|
437
|
+
});
|
|
431
438
|
let output = `\n=== TASK COMPLETED ===\n`;
|
|
432
439
|
output += `\nTasks completed: ${result.tasksCompleted}`;
|
|
433
440
|
if (result.bossesDefeated > 0) {
|
|
@@ -445,6 +452,30 @@ async function processCommand(input, currentPath, config, signal, rl) {
|
|
|
445
452
|
}
|
|
446
453
|
}
|
|
447
454
|
output += '\n';
|
|
455
|
+
output += '[Type "undo" to revert]';
|
|
456
|
+
return wrapResult({ output });
|
|
457
|
+
}
|
|
458
|
+
// Undo command
|
|
459
|
+
if (command === 'undo') {
|
|
460
|
+
const lastUndo = (0, profile_1.getLastUndo)();
|
|
461
|
+
if (!lastUndo) {
|
|
462
|
+
return wrapResult({ output: 'Nothing to undo.' });
|
|
463
|
+
}
|
|
464
|
+
// Revert the task status
|
|
465
|
+
const nodeConfig = (0, nodeConfig_1.readNodeConfig)(lastUndo.path);
|
|
466
|
+
if (nodeConfig) {
|
|
467
|
+
(0, nodeConfig_1.writeNodeConfig)(lastUndo.path, {
|
|
468
|
+
...nodeConfig,
|
|
469
|
+
status: 'open',
|
|
470
|
+
completedAt: undefined,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
// Revert profile stats
|
|
474
|
+
const result = (0, profile_1.performUndo)();
|
|
475
|
+
let output = `\n=== UNDO ===\n`;
|
|
476
|
+
output += `\nReverted: ${path.basename(lastUndo.path)}`;
|
|
477
|
+
output += `\n${result.message}`;
|
|
478
|
+
output += '\n';
|
|
448
479
|
return wrapResult({ output });
|
|
449
480
|
}
|
|
450
481
|
// Deadline command
|
|
@@ -482,19 +513,48 @@ async function processCommand(input, currentPath, config, signal, rl) {
|
|
|
482
513
|
});
|
|
483
514
|
return wrapResult({ output: newIsBoss ? 'Marked as BOSS task (3x XP)' : 'Removed BOSS status' });
|
|
484
515
|
}
|
|
485
|
-
// Block command
|
|
516
|
+
// Block command - block <node> or block "reason"
|
|
486
517
|
if (command === 'block') {
|
|
487
518
|
const nodeConfig = (0, nodeConfig_1.readNodeConfig)(currentPath);
|
|
488
519
|
if (!nodeConfig) {
|
|
489
520
|
return wrapResult({ output: 'No task at current location.' });
|
|
490
521
|
}
|
|
491
|
-
const
|
|
522
|
+
const arg = parts.length > 1 ? parts.slice(1).join(' ') : undefined;
|
|
523
|
+
let blockedBy = nodeConfig.blockedBy || [];
|
|
524
|
+
let blockMessage = 'Task marked as blocked';
|
|
525
|
+
if (arg) {
|
|
526
|
+
// Check if it's a path to another node
|
|
527
|
+
const potentialPath = path.join(currentPath, '..', arg);
|
|
528
|
+
const absolutePath = path.isAbsolute(arg) ? arg : potentialPath;
|
|
529
|
+
if (fs.existsSync(absolutePath) && fs.statSync(absolutePath).isDirectory()) {
|
|
530
|
+
// It's a node path
|
|
531
|
+
const blockerConfig = (0, nodeConfig_1.readNodeConfig)(absolutePath);
|
|
532
|
+
const blockerName = blockerConfig?.name || path.basename(absolutePath);
|
|
533
|
+
blockedBy = [...blockedBy, absolutePath];
|
|
534
|
+
blockMessage = `Blocked by: ${blockerName}`;
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
// Check sibling folder
|
|
538
|
+
const siblingPath = path.join(path.dirname(currentPath), arg);
|
|
539
|
+
if (fs.existsSync(siblingPath) && fs.statSync(siblingPath).isDirectory()) {
|
|
540
|
+
const blockerConfig = (0, nodeConfig_1.readNodeConfig)(siblingPath);
|
|
541
|
+
const blockerName = blockerConfig?.name || arg;
|
|
542
|
+
blockedBy = [...blockedBy, siblingPath];
|
|
543
|
+
blockMessage = `Blocked by: ${blockerName}`;
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
// It's a text reason
|
|
547
|
+
blockedBy = [...blockedBy, arg];
|
|
548
|
+
blockMessage = `Blocked: ${arg}`;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
492
552
|
(0, nodeConfig_1.writeNodeConfig)(currentPath, {
|
|
493
553
|
...nodeConfig,
|
|
494
554
|
status: 'blocked',
|
|
495
|
-
blockedBy
|
|
555
|
+
blockedBy,
|
|
496
556
|
});
|
|
497
|
-
return wrapResult({ output:
|
|
557
|
+
return wrapResult({ output: blockMessage });
|
|
498
558
|
}
|
|
499
559
|
// Unblock command
|
|
500
560
|
if (command === 'unblock') {
|
|
@@ -509,6 +569,67 @@ async function processCommand(input, currentPath, config, signal, rl) {
|
|
|
509
569
|
});
|
|
510
570
|
return wrapResult({ output: 'Task unblocked' });
|
|
511
571
|
}
|
|
572
|
+
// Check command - show overdue/upcoming deadlines
|
|
573
|
+
if (command === 'check') {
|
|
574
|
+
const checkDeadlines = (dirPath, results) => {
|
|
575
|
+
if (!fs.existsSync(dirPath))
|
|
576
|
+
return;
|
|
577
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
578
|
+
for (const entry of entries) {
|
|
579
|
+
if (!entry.isDirectory() || entry.name.startsWith('.'))
|
|
580
|
+
continue;
|
|
581
|
+
const nodePath = path.join(dirPath, entry.name);
|
|
582
|
+
const cfg = (0, nodeConfig_1.readNodeConfig)(nodePath);
|
|
583
|
+
if (cfg && cfg.deadline && cfg.status !== 'done') {
|
|
584
|
+
const deadlineDate = new Date(cfg.deadline);
|
|
585
|
+
const today = new Date();
|
|
586
|
+
today.setHours(0, 0, 0, 0);
|
|
587
|
+
deadlineDate.setHours(0, 0, 0, 0);
|
|
588
|
+
const diffDays = Math.ceil((deadlineDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
|
|
589
|
+
if (diffDays <= 3) { // Show tasks due within 3 days or overdue
|
|
590
|
+
results.push({
|
|
591
|
+
path: nodePath,
|
|
592
|
+
name: cfg.name,
|
|
593
|
+
deadline: formatDeadline(cfg.deadline),
|
|
594
|
+
status: diffDays < 0 ? 'OVERDUE' : diffDays === 0 ? 'TODAY' : 'SOON',
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
checkDeadlines(nodePath, results);
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
const results = [];
|
|
602
|
+
checkDeadlines(config.storagePath, results);
|
|
603
|
+
if (results.length === 0) {
|
|
604
|
+
return wrapResult({ output: 'No upcoming deadlines within 3 days.' });
|
|
605
|
+
}
|
|
606
|
+
const lines = ['', '=== DEADLINE CHECK ===', ''];
|
|
607
|
+
const overdue = results.filter(r => r.status === 'OVERDUE');
|
|
608
|
+
const today = results.filter(r => r.status === 'TODAY');
|
|
609
|
+
const soon = results.filter(r => r.status === 'SOON');
|
|
610
|
+
if (overdue.length > 0) {
|
|
611
|
+
lines.push('OVERDUE:');
|
|
612
|
+
for (const r of overdue) {
|
|
613
|
+
lines.push(` ! ${r.name} (${r.deadline})`);
|
|
614
|
+
}
|
|
615
|
+
lines.push('');
|
|
616
|
+
}
|
|
617
|
+
if (today.length > 0) {
|
|
618
|
+
lines.push('DUE TODAY:');
|
|
619
|
+
for (const r of today) {
|
|
620
|
+
lines.push(` * ${r.name}`);
|
|
621
|
+
}
|
|
622
|
+
lines.push('');
|
|
623
|
+
}
|
|
624
|
+
if (soon.length > 0) {
|
|
625
|
+
lines.push('UPCOMING:');
|
|
626
|
+
for (const r of soon) {
|
|
627
|
+
lines.push(` - ${r.name} (${r.deadline})`);
|
|
628
|
+
}
|
|
629
|
+
lines.push('');
|
|
630
|
+
}
|
|
631
|
+
return wrapResult({ output: lines.join('\n') });
|
|
632
|
+
}
|
|
512
633
|
// Status command - show current task status
|
|
513
634
|
if (command === 'status') {
|
|
514
635
|
const nodeConfig = (0, nodeConfig_1.readNodeConfig)(currentPath);
|
|
@@ -531,7 +652,15 @@ async function processCommand(input, currentPath, config, signal, rl) {
|
|
|
531
652
|
lines.push(`Completed: ${new Date(nodeConfig.completedAt).toLocaleDateString()}`);
|
|
532
653
|
}
|
|
533
654
|
if (nodeConfig.blockedBy && nodeConfig.blockedBy.length > 0) {
|
|
534
|
-
|
|
655
|
+
// Format blockedBy - show names if paths exist
|
|
656
|
+
const blockerNames = nodeConfig.blockedBy.map(b => {
|
|
657
|
+
if (fs.existsSync(b)) {
|
|
658
|
+
const cfg = (0, nodeConfig_1.readNodeConfig)(b);
|
|
659
|
+
return cfg?.name || path.basename(b);
|
|
660
|
+
}
|
|
661
|
+
return b;
|
|
662
|
+
});
|
|
663
|
+
lines.push(`Blocked by: ${blockerNames.join(', ')}`);
|
|
535
664
|
}
|
|
536
665
|
lines.push('');
|
|
537
666
|
return wrapResult({ output: lines.join('\n') });
|
|
@@ -850,11 +979,13 @@ Navigation:
|
|
|
850
979
|
Task Management:
|
|
851
980
|
mkdir <name> Create new task
|
|
852
981
|
done Mark current task as completed (recursive)
|
|
982
|
+
undo Undo last done (restores XP)
|
|
853
983
|
deadline <date> Set deadline (today, tomorrow, +3d, Jan 15)
|
|
854
984
|
boss Toggle boss/milestone status (3x XP)
|
|
855
|
-
block [
|
|
985
|
+
block [node] Block by task (or text reason)
|
|
856
986
|
unblock Remove blocked status
|
|
857
987
|
status Show current task details
|
|
988
|
+
check Show overdue/upcoming deadlines
|
|
858
989
|
|
|
859
990
|
File Operations:
|
|
860
991
|
cp <src> <dest> Copy file or folder
|
|
@@ -885,11 +1016,11 @@ Clipboard:
|
|
|
885
1016
|
<cmd> | pbcopy Copy output (macOS)
|
|
886
1017
|
<cmd> | clip Copy output (Windows)
|
|
887
1018
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1019
|
+
Examples:
|
|
1020
|
+
block backend-api Block current task by sibling task
|
|
1021
|
+
block "waiting for design" Block with text reason
|
|
891
1022
|
deadline +3d Due in 3 days
|
|
892
|
-
|
|
1023
|
+
check See all upcoming deadlines
|
|
893
1024
|
|
|
894
1025
|
www.rlc.rocks
|
|
895
1026
|
`.trim()
|
|
@@ -20,8 +20,8 @@ const ASCII_ART = [
|
|
|
20
20
|
'╚═════════════════════════╝',
|
|
21
21
|
'',
|
|
22
22
|
' Navigation: ls, cd, tree, pwd, open',
|
|
23
|
-
' Tasks: done, deadline, boss, block
|
|
24
|
-
' Gamification: stats, achievements, map',
|
|
23
|
+
' Tasks: done, undo, deadline, boss, block',
|
|
24
|
+
' Gamification: stats, achievements, map, check',
|
|
25
25
|
'',
|
|
26
26
|
' TAB to autocomplete, | pbcopy to copy',
|
|
27
27
|
' Workflow: <description> -> refine -> save',
|
package/dist/storage/profile.js
CHANGED
|
@@ -43,6 +43,9 @@ exports.addXP = addXP;
|
|
|
43
43
|
exports.completeTask = completeTask;
|
|
44
44
|
exports.formatStats = formatStats;
|
|
45
45
|
exports.formatAchievements = formatAchievements;
|
|
46
|
+
exports.addToUndoHistory = addToUndoHistory;
|
|
47
|
+
exports.getLastUndo = getLastUndo;
|
|
48
|
+
exports.performUndo = performUndo;
|
|
46
49
|
const fs = __importStar(require("fs"));
|
|
47
50
|
const path = __importStar(require("path"));
|
|
48
51
|
const os = __importStar(require("os"));
|
|
@@ -127,6 +130,7 @@ function createDefaultProfile() {
|
|
|
127
130
|
currentStreak: 0,
|
|
128
131
|
longestStreak: 0,
|
|
129
132
|
achievements: [],
|
|
133
|
+
undoHistory: [],
|
|
130
134
|
stats: {
|
|
131
135
|
completedByDay: {},
|
|
132
136
|
createdAt: new Date().toISOString(),
|
|
@@ -274,3 +278,46 @@ function formatAchievements() {
|
|
|
274
278
|
lines.push('');
|
|
275
279
|
return lines.join('\n');
|
|
276
280
|
}
|
|
281
|
+
function addToUndoHistory(entry) {
|
|
282
|
+
const profile = readProfile();
|
|
283
|
+
// Keep only last 10 undo entries
|
|
284
|
+
profile.undoHistory = profile.undoHistory || [];
|
|
285
|
+
profile.undoHistory.unshift(entry);
|
|
286
|
+
if (profile.undoHistory.length > 10) {
|
|
287
|
+
profile.undoHistory = profile.undoHistory.slice(0, 10);
|
|
288
|
+
}
|
|
289
|
+
saveProfile(profile);
|
|
290
|
+
}
|
|
291
|
+
function getLastUndo() {
|
|
292
|
+
const profile = readProfile();
|
|
293
|
+
if (!profile.undoHistory || profile.undoHistory.length === 0) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
return profile.undoHistory[0];
|
|
297
|
+
}
|
|
298
|
+
function performUndo() {
|
|
299
|
+
const profile = readProfile();
|
|
300
|
+
if (!profile.undoHistory || profile.undoHistory.length === 0) {
|
|
301
|
+
return { success: false, entry: null, message: 'Nothing to undo.' };
|
|
302
|
+
}
|
|
303
|
+
const entry = profile.undoHistory.shift();
|
|
304
|
+
// Subtract XP
|
|
305
|
+
profile.totalXP = Math.max(0, profile.totalXP - entry.xpLost);
|
|
306
|
+
profile.tasksCompleted = Math.max(0, profile.tasksCompleted - 1);
|
|
307
|
+
if (entry.wasBoss) {
|
|
308
|
+
profile.bossesDefeated = Math.max(0, profile.bossesDefeated - 1);
|
|
309
|
+
}
|
|
310
|
+
// Update level
|
|
311
|
+
profile.level = levelFromXP(profile.totalXP);
|
|
312
|
+
// Update daily stats
|
|
313
|
+
const today = new Date().toISOString().split('T')[0];
|
|
314
|
+
if (profile.stats.completedByDay[today]) {
|
|
315
|
+
profile.stats.completedByDay[today] = Math.max(0, profile.stats.completedByDay[today] - 1);
|
|
316
|
+
}
|
|
317
|
+
saveProfile(profile);
|
|
318
|
+
return {
|
|
319
|
+
success: true,
|
|
320
|
+
entry,
|
|
321
|
+
message: `Undo: -${entry.xpLost} XP`
|
|
322
|
+
};
|
|
323
|
+
}
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@ import { Config } from '../config/config';
|
|
|
6
6
|
import { listSchemas, navigateToNode, getTree } from '../storage/storage';
|
|
7
7
|
import { saveSchemaFile, writeNodeConfig, readNodeConfig, calculateXP, saveMapFile, NodeConfig } from '../storage/nodeConfig';
|
|
8
8
|
import { generateSchemaWithAI, generateDungeonMapWithAI } from '../ai/claude';
|
|
9
|
-
import { completeTask, formatStats, formatAchievements, readProfile } from '../storage/profile';
|
|
9
|
+
import { completeTask, formatStats, formatAchievements, readProfile, addToUndoHistory, performUndo, getLastUndo } from '../storage/profile';
|
|
10
10
|
|
|
11
11
|
// Parse tree ASCII art and create folder structure
|
|
12
12
|
function createFoldersFromTree(rootPath: string, treeContent: string): void {
|
|
@@ -505,6 +505,14 @@ export async function processCommand(
|
|
|
505
505
|
nodeConfig.createdAt
|
|
506
506
|
);
|
|
507
507
|
|
|
508
|
+
// Save to undo history
|
|
509
|
+
addToUndoHistory({
|
|
510
|
+
path: currentPath,
|
|
511
|
+
xpLost: result.xpGained,
|
|
512
|
+
wasBoss: nodeConfig.isBoss || false,
|
|
513
|
+
timestamp: new Date().toISOString(),
|
|
514
|
+
});
|
|
515
|
+
|
|
508
516
|
let output = `\n=== TASK COMPLETED ===\n`;
|
|
509
517
|
output += `\nTasks completed: ${result.tasksCompleted}`;
|
|
510
518
|
if (result.bossesDefeated > 0) {
|
|
@@ -524,6 +532,35 @@ export async function processCommand(
|
|
|
524
532
|
}
|
|
525
533
|
}
|
|
526
534
|
|
|
535
|
+
output += '\n';
|
|
536
|
+
output += '[Type "undo" to revert]';
|
|
537
|
+
|
|
538
|
+
return wrapResult({ output });
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Undo command
|
|
542
|
+
if (command === 'undo') {
|
|
543
|
+
const lastUndo = getLastUndo();
|
|
544
|
+
if (!lastUndo) {
|
|
545
|
+
return wrapResult({ output: 'Nothing to undo.' });
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Revert the task status
|
|
549
|
+
const nodeConfig = readNodeConfig(lastUndo.path);
|
|
550
|
+
if (nodeConfig) {
|
|
551
|
+
writeNodeConfig(lastUndo.path, {
|
|
552
|
+
...nodeConfig,
|
|
553
|
+
status: 'open',
|
|
554
|
+
completedAt: undefined,
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Revert profile stats
|
|
559
|
+
const result = performUndo();
|
|
560
|
+
|
|
561
|
+
let output = `\n=== UNDO ===\n`;
|
|
562
|
+
output += `\nReverted: ${path.basename(lastUndo.path)}`;
|
|
563
|
+
output += `\n${result.message}`;
|
|
527
564
|
output += '\n';
|
|
528
565
|
|
|
529
566
|
return wrapResult({ output });
|
|
@@ -574,22 +611,51 @@ export async function processCommand(
|
|
|
574
611
|
return wrapResult({ output: newIsBoss ? 'Marked as BOSS task (3x XP)' : 'Removed BOSS status' });
|
|
575
612
|
}
|
|
576
613
|
|
|
577
|
-
// Block command
|
|
614
|
+
// Block command - block <node> or block "reason"
|
|
578
615
|
if (command === 'block') {
|
|
579
616
|
const nodeConfig = readNodeConfig(currentPath);
|
|
580
617
|
if (!nodeConfig) {
|
|
581
618
|
return wrapResult({ output: 'No task at current location.' });
|
|
582
619
|
}
|
|
583
620
|
|
|
584
|
-
const
|
|
621
|
+
const arg = parts.length > 1 ? parts.slice(1).join(' ') : undefined;
|
|
622
|
+
let blockedBy: string[] = nodeConfig.blockedBy || [];
|
|
623
|
+
let blockMessage = 'Task marked as blocked';
|
|
624
|
+
|
|
625
|
+
if (arg) {
|
|
626
|
+
// Check if it's a path to another node
|
|
627
|
+
const potentialPath = path.join(currentPath, '..', arg);
|
|
628
|
+
const absolutePath = path.isAbsolute(arg) ? arg : potentialPath;
|
|
629
|
+
|
|
630
|
+
if (fs.existsSync(absolutePath) && fs.statSync(absolutePath).isDirectory()) {
|
|
631
|
+
// It's a node path
|
|
632
|
+
const blockerConfig = readNodeConfig(absolutePath);
|
|
633
|
+
const blockerName = blockerConfig?.name || path.basename(absolutePath);
|
|
634
|
+
blockedBy = [...blockedBy, absolutePath];
|
|
635
|
+
blockMessage = `Blocked by: ${blockerName}`;
|
|
636
|
+
} else {
|
|
637
|
+
// Check sibling folder
|
|
638
|
+
const siblingPath = path.join(path.dirname(currentPath), arg);
|
|
639
|
+
if (fs.existsSync(siblingPath) && fs.statSync(siblingPath).isDirectory()) {
|
|
640
|
+
const blockerConfig = readNodeConfig(siblingPath);
|
|
641
|
+
const blockerName = blockerConfig?.name || arg;
|
|
642
|
+
blockedBy = [...blockedBy, siblingPath];
|
|
643
|
+
blockMessage = `Blocked by: ${blockerName}`;
|
|
644
|
+
} else {
|
|
645
|
+
// It's a text reason
|
|
646
|
+
blockedBy = [...blockedBy, arg];
|
|
647
|
+
blockMessage = `Blocked: ${arg}`;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
585
651
|
|
|
586
652
|
writeNodeConfig(currentPath, {
|
|
587
653
|
...nodeConfig,
|
|
588
654
|
status: 'blocked',
|
|
589
|
-
blockedBy
|
|
655
|
+
blockedBy,
|
|
590
656
|
});
|
|
591
657
|
|
|
592
|
-
return wrapResult({ output:
|
|
658
|
+
return wrapResult({ output: blockMessage });
|
|
593
659
|
}
|
|
594
660
|
|
|
595
661
|
// Unblock command
|
|
@@ -608,6 +674,80 @@ export async function processCommand(
|
|
|
608
674
|
return wrapResult({ output: 'Task unblocked' });
|
|
609
675
|
}
|
|
610
676
|
|
|
677
|
+
// Check command - show overdue/upcoming deadlines
|
|
678
|
+
if (command === 'check') {
|
|
679
|
+
const checkDeadlines = (dirPath: string, results: { path: string; name: string; deadline: string; status: string }[]) => {
|
|
680
|
+
if (!fs.existsSync(dirPath)) return;
|
|
681
|
+
|
|
682
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
683
|
+
for (const entry of entries) {
|
|
684
|
+
if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
|
|
685
|
+
|
|
686
|
+
const nodePath = path.join(dirPath, entry.name);
|
|
687
|
+
const cfg = readNodeConfig(nodePath);
|
|
688
|
+
|
|
689
|
+
if (cfg && cfg.deadline && cfg.status !== 'done') {
|
|
690
|
+
const deadlineDate = new Date(cfg.deadline);
|
|
691
|
+
const today = new Date();
|
|
692
|
+
today.setHours(0, 0, 0, 0);
|
|
693
|
+
deadlineDate.setHours(0, 0, 0, 0);
|
|
694
|
+
|
|
695
|
+
const diffDays = Math.ceil((deadlineDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
|
|
696
|
+
|
|
697
|
+
if (diffDays <= 3) { // Show tasks due within 3 days or overdue
|
|
698
|
+
results.push({
|
|
699
|
+
path: nodePath,
|
|
700
|
+
name: cfg.name,
|
|
701
|
+
deadline: formatDeadline(cfg.deadline),
|
|
702
|
+
status: diffDays < 0 ? 'OVERDUE' : diffDays === 0 ? 'TODAY' : 'SOON',
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
checkDeadlines(nodePath, results);
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
const results: { path: string; name: string; deadline: string; status: string }[] = [];
|
|
712
|
+
checkDeadlines(config.storagePath, results);
|
|
713
|
+
|
|
714
|
+
if (results.length === 0) {
|
|
715
|
+
return wrapResult({ output: 'No upcoming deadlines within 3 days.' });
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const lines: string[] = ['', '=== DEADLINE CHECK ===', ''];
|
|
719
|
+
|
|
720
|
+
const overdue = results.filter(r => r.status === 'OVERDUE');
|
|
721
|
+
const today = results.filter(r => r.status === 'TODAY');
|
|
722
|
+
const soon = results.filter(r => r.status === 'SOON');
|
|
723
|
+
|
|
724
|
+
if (overdue.length > 0) {
|
|
725
|
+
lines.push('OVERDUE:');
|
|
726
|
+
for (const r of overdue) {
|
|
727
|
+
lines.push(` ! ${r.name} (${r.deadline})`);
|
|
728
|
+
}
|
|
729
|
+
lines.push('');
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (today.length > 0) {
|
|
733
|
+
lines.push('DUE TODAY:');
|
|
734
|
+
for (const r of today) {
|
|
735
|
+
lines.push(` * ${r.name}`);
|
|
736
|
+
}
|
|
737
|
+
lines.push('');
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
if (soon.length > 0) {
|
|
741
|
+
lines.push('UPCOMING:');
|
|
742
|
+
for (const r of soon) {
|
|
743
|
+
lines.push(` - ${r.name} (${r.deadline})`);
|
|
744
|
+
}
|
|
745
|
+
lines.push('');
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
return wrapResult({ output: lines.join('\n') });
|
|
749
|
+
}
|
|
750
|
+
|
|
611
751
|
// Status command - show current task status
|
|
612
752
|
if (command === 'status') {
|
|
613
753
|
const nodeConfig = readNodeConfig(currentPath);
|
|
@@ -635,7 +775,15 @@ export async function processCommand(
|
|
|
635
775
|
}
|
|
636
776
|
|
|
637
777
|
if (nodeConfig.blockedBy && nodeConfig.blockedBy.length > 0) {
|
|
638
|
-
|
|
778
|
+
// Format blockedBy - show names if paths exist
|
|
779
|
+
const blockerNames = nodeConfig.blockedBy.map(b => {
|
|
780
|
+
if (fs.existsSync(b)) {
|
|
781
|
+
const cfg = readNodeConfig(b);
|
|
782
|
+
return cfg?.name || path.basename(b);
|
|
783
|
+
}
|
|
784
|
+
return b;
|
|
785
|
+
});
|
|
786
|
+
lines.push(`Blocked by: ${blockerNames.join(', ')}`);
|
|
639
787
|
}
|
|
640
788
|
|
|
641
789
|
lines.push('');
|
|
@@ -1015,11 +1163,13 @@ Navigation:
|
|
|
1015
1163
|
Task Management:
|
|
1016
1164
|
mkdir <name> Create new task
|
|
1017
1165
|
done Mark current task as completed (recursive)
|
|
1166
|
+
undo Undo last done (restores XP)
|
|
1018
1167
|
deadline <date> Set deadline (today, tomorrow, +3d, Jan 15)
|
|
1019
1168
|
boss Toggle boss/milestone status (3x XP)
|
|
1020
|
-
block [
|
|
1169
|
+
block [node] Block by task (or text reason)
|
|
1021
1170
|
unblock Remove blocked status
|
|
1022
1171
|
status Show current task details
|
|
1172
|
+
check Show overdue/upcoming deadlines
|
|
1023
1173
|
|
|
1024
1174
|
File Operations:
|
|
1025
1175
|
cp <src> <dest> Copy file or folder
|
|
@@ -1050,11 +1200,11 @@ Clipboard:
|
|
|
1050
1200
|
<cmd> | pbcopy Copy output (macOS)
|
|
1051
1201
|
<cmd> | clip Copy output (Windows)
|
|
1052
1202
|
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1203
|
+
Examples:
|
|
1204
|
+
block backend-api Block current task by sibling task
|
|
1205
|
+
block "waiting for design" Block with text reason
|
|
1056
1206
|
deadline +3d Due in 3 days
|
|
1057
|
-
|
|
1207
|
+
check See all upcoming deadlines
|
|
1058
1208
|
|
|
1059
1209
|
www.rlc.rocks
|
|
1060
1210
|
`.trim()
|
|
@@ -18,8 +18,8 @@ const ASCII_ART = [
|
|
|
18
18
|
'╚═════════════════════════╝',
|
|
19
19
|
'',
|
|
20
20
|
' Navigation: ls, cd, tree, pwd, open',
|
|
21
|
-
' Tasks: done, deadline, boss, block
|
|
22
|
-
' Gamification: stats, achievements, map',
|
|
21
|
+
' Tasks: done, undo, deadline, boss, block',
|
|
22
|
+
' Gamification: stats, achievements, map, check',
|
|
23
23
|
'',
|
|
24
24
|
' TAB to autocomplete, | pbcopy to copy',
|
|
25
25
|
' Workflow: <description> -> refine -> save',
|
package/src/storage/profile.ts
CHANGED
|
@@ -9,6 +9,13 @@ export interface Achievement {
|
|
|
9
9
|
unlockedAt?: string;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
export interface UndoEntry {
|
|
13
|
+
path: string;
|
|
14
|
+
xpLost: number;
|
|
15
|
+
wasBoss: boolean;
|
|
16
|
+
timestamp: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
12
19
|
export interface Profile {
|
|
13
20
|
totalXP: number;
|
|
14
21
|
level: number;
|
|
@@ -18,6 +25,7 @@ export interface Profile {
|
|
|
18
25
|
longestStreak: number;
|
|
19
26
|
lastCompletionDate?: string;
|
|
20
27
|
achievements: string[];
|
|
28
|
+
undoHistory: UndoEntry[];
|
|
21
29
|
stats: {
|
|
22
30
|
completedByDay: Record<string, number>;
|
|
23
31
|
createdAt: string;
|
|
@@ -114,6 +122,7 @@ function createDefaultProfile(): Profile {
|
|
|
114
122
|
currentStreak: 0,
|
|
115
123
|
longestStreak: 0,
|
|
116
124
|
achievements: [],
|
|
125
|
+
undoHistory: [],
|
|
117
126
|
stats: {
|
|
118
127
|
completedByDay: {},
|
|
119
128
|
createdAt: new Date().toISOString(),
|
|
@@ -279,3 +288,59 @@ export function formatAchievements(): string {
|
|
|
279
288
|
return lines.join('\n');
|
|
280
289
|
}
|
|
281
290
|
|
|
291
|
+
export function addToUndoHistory(entry: UndoEntry): void {
|
|
292
|
+
const profile = readProfile();
|
|
293
|
+
|
|
294
|
+
// Keep only last 10 undo entries
|
|
295
|
+
profile.undoHistory = profile.undoHistory || [];
|
|
296
|
+
profile.undoHistory.unshift(entry);
|
|
297
|
+
if (profile.undoHistory.length > 10) {
|
|
298
|
+
profile.undoHistory = profile.undoHistory.slice(0, 10);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
saveProfile(profile);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function getLastUndo(): UndoEntry | null {
|
|
305
|
+
const profile = readProfile();
|
|
306
|
+
if (!profile.undoHistory || profile.undoHistory.length === 0) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
return profile.undoHistory[0];
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function performUndo(): { success: boolean; entry: UndoEntry | null; message: string } {
|
|
313
|
+
const profile = readProfile();
|
|
314
|
+
|
|
315
|
+
if (!profile.undoHistory || profile.undoHistory.length === 0) {
|
|
316
|
+
return { success: false, entry: null, message: 'Nothing to undo.' };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const entry = profile.undoHistory.shift()!;
|
|
320
|
+
|
|
321
|
+
// Subtract XP
|
|
322
|
+
profile.totalXP = Math.max(0, profile.totalXP - entry.xpLost);
|
|
323
|
+
profile.tasksCompleted = Math.max(0, profile.tasksCompleted - 1);
|
|
324
|
+
|
|
325
|
+
if (entry.wasBoss) {
|
|
326
|
+
profile.bossesDefeated = Math.max(0, profile.bossesDefeated - 1);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Update level
|
|
330
|
+
profile.level = levelFromXP(profile.totalXP);
|
|
331
|
+
|
|
332
|
+
// Update daily stats
|
|
333
|
+
const today = new Date().toISOString().split('T')[0];
|
|
334
|
+
if (profile.stats.completedByDay[today]) {
|
|
335
|
+
profile.stats.completedByDay[today] = Math.max(0, profile.stats.completedByDay[today] - 1);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
saveProfile(profile);
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
success: true,
|
|
342
|
+
entry,
|
|
343
|
+
message: `Undo: -${entry.xpLost} XP`
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|