roguelike-cli 1.2.6 → 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 +156 -208
- package/dist/ai/claude.js +53 -42
- package/dist/interactive/commands.js +514 -181
- package/dist/interactive/startup.js +6 -3
- package/dist/storage/nodeConfig.js +35 -1
- package/dist/storage/profile.js +323 -0
- package/package.json +1 -1
- package/src/ai/claude.ts +60 -43
- package/src/interactive/commands.ts +620 -187
- package/src/interactive/startup.ts +6 -3
- package/src/storage/nodeConfig.ts +48 -2
- package/src/storage/profile.ts +346 -0
|
@@ -4,40 +4,38 @@ import * as readline from 'readline';
|
|
|
4
4
|
import { execSync } from 'child_process';
|
|
5
5
|
import { Config } from '../config/config';
|
|
6
6
|
import { listSchemas, navigateToNode, getTree } from '../storage/storage';
|
|
7
|
-
import { saveSchemaFile, writeNodeConfig } from '../storage/nodeConfig';
|
|
8
|
-
import { generateSchemaWithAI } from '../ai/claude';
|
|
7
|
+
import { saveSchemaFile, writeNodeConfig, readNodeConfig, calculateXP, saveMapFile, NodeConfig } from '../storage/nodeConfig';
|
|
8
|
+
import { generateSchemaWithAI, generateDungeonMapWithAI } from '../ai/claude';
|
|
9
|
+
import { completeTask, formatStats, formatAchievements, readProfile, addToUndoHistory, performUndo, getLastUndo } from '../storage/profile';
|
|
9
10
|
|
|
10
11
|
// Parse tree ASCII art and create folder structure
|
|
11
12
|
function createFoldersFromTree(rootPath: string, treeContent: string): void {
|
|
12
|
-
// Create root folder
|
|
13
13
|
if (!fs.existsSync(rootPath)) {
|
|
14
14
|
fs.mkdirSync(rootPath, { recursive: true });
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
// Parse tree lines
|
|
18
17
|
const lines = treeContent.split('\n');
|
|
19
18
|
const stack: { path: string; indent: number }[] = [{ path: rootPath, indent: -1 }];
|
|
20
19
|
|
|
21
20
|
for (const line of lines) {
|
|
22
|
-
// Skip empty lines
|
|
23
21
|
if (!line.trim()) continue;
|
|
24
22
|
|
|
25
|
-
// Extract node name from tree line
|
|
26
|
-
// Patterns: "├── Name", "└── Name", "│ ├── Name", etc.
|
|
27
23
|
const match = line.match(/^([\s│]*)[├└]──\s*(.+)$/);
|
|
28
24
|
if (!match) continue;
|
|
29
25
|
|
|
30
26
|
const prefix = match[1];
|
|
31
27
|
let nodeName = match[2].trim();
|
|
32
|
-
|
|
33
|
-
// Calculate indent level (each │ or space block = 1 level)
|
|
34
28
|
const indent = Math.floor(prefix.replace(/│/g, ' ').length / 4);
|
|
35
29
|
|
|
36
|
-
//
|
|
30
|
+
// Extract metadata from node name
|
|
31
|
+
const isBoss = /\[BOSS\]/i.test(nodeName) || /\[MILESTONE\]/i.test(nodeName);
|
|
32
|
+
const deadlineMatch = nodeName.match(/\[(?:DUE|DEADLINE):\s*([^\]]+)\]/i);
|
|
33
|
+
const deadline = deadlineMatch ? deadlineMatch[1].trim() : undefined;
|
|
34
|
+
|
|
35
|
+
// Clean node name
|
|
37
36
|
nodeName = nodeName.replace(/\s*\([^)]*\)\s*/g, '').trim();
|
|
38
37
|
nodeName = nodeName.replace(/\s*\[[^\]]*\]\s*/g, '').trim();
|
|
39
38
|
|
|
40
|
-
// Create safe folder name
|
|
41
39
|
const safeName = nodeName
|
|
42
40
|
.toLowerCase()
|
|
43
41
|
.replace(/[^a-z0-9]+/g, '-')
|
|
@@ -45,7 +43,6 @@ function createFoldersFromTree(rootPath: string, treeContent: string): void {
|
|
|
45
43
|
|
|
46
44
|
if (!safeName) continue;
|
|
47
45
|
|
|
48
|
-
// Pop stack until we find parent
|
|
49
46
|
while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
|
|
50
47
|
stack.pop();
|
|
51
48
|
}
|
|
@@ -53,25 +50,29 @@ function createFoldersFromTree(rootPath: string, treeContent: string): void {
|
|
|
53
50
|
const parentPath = stack[stack.length - 1].path;
|
|
54
51
|
const folderPath = path.join(parentPath, safeName);
|
|
55
52
|
|
|
56
|
-
// Create folder
|
|
57
53
|
if (!fs.existsSync(folderPath)) {
|
|
58
54
|
fs.mkdirSync(folderPath, { recursive: true });
|
|
59
55
|
}
|
|
60
56
|
|
|
61
|
-
//
|
|
57
|
+
// Calculate depth for XP
|
|
58
|
+
const depth = stack.length;
|
|
59
|
+
|
|
62
60
|
writeNodeConfig(folderPath, {
|
|
63
61
|
name: nodeName,
|
|
62
|
+
status: 'open',
|
|
63
|
+
xp: calculateXP(depth, isBoss),
|
|
64
|
+
isBoss,
|
|
65
|
+
deadline,
|
|
64
66
|
createdAt: new Date().toISOString(),
|
|
65
67
|
updatedAt: new Date().toISOString(),
|
|
66
68
|
});
|
|
67
69
|
|
|
68
|
-
// Push to stack
|
|
69
70
|
stack.push({ path: folderPath, indent });
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
// Generate dungeon map visualization from folder structure
|
|
74
|
-
function generateDungeonMap(dirPath: string): string {
|
|
75
|
+
function generateDungeonMap(dirPath: string, config: Config): string {
|
|
75
76
|
if (!fs.existsSync(dirPath)) {
|
|
76
77
|
return 'Directory does not exist.';
|
|
77
78
|
}
|
|
@@ -86,32 +87,33 @@ function generateDungeonMap(dirPath: string): string {
|
|
|
86
87
|
const lines: string[] = [];
|
|
87
88
|
const roomWidth = 20;
|
|
88
89
|
const roomsPerRow = 2;
|
|
89
|
-
const wall = '
|
|
90
|
+
const wall = '#';
|
|
90
91
|
const door = '+';
|
|
91
92
|
const task = '*';
|
|
92
93
|
const milestone = '@';
|
|
94
|
+
const done = 'x';
|
|
95
|
+
const blocked = '!';
|
|
93
96
|
|
|
94
|
-
// Group folders into rows of 2
|
|
95
97
|
const rows: typeof folders[] = [];
|
|
96
98
|
for (let i = 0; i < folders.length; i += roomsPerRow) {
|
|
97
99
|
rows.push(folders.slice(i, i + roomsPerRow));
|
|
98
100
|
}
|
|
99
101
|
|
|
100
|
-
|
|
102
|
+
lines.push('');
|
|
101
103
|
lines.push(' ' + wall.repeat(roomWidth * roomsPerRow + 3));
|
|
102
104
|
|
|
103
105
|
rows.forEach((row, rowIndex) => {
|
|
104
|
-
// Room content
|
|
105
106
|
for (let line = 0; line < 6; line++) {
|
|
106
107
|
let rowStr = ' ' + wall;
|
|
107
108
|
|
|
108
109
|
row.forEach((folder, colIndex) => {
|
|
109
|
-
const
|
|
110
|
+
const folderPath = path.join(dirPath, folder.name);
|
|
111
|
+
const nodeConfig = readNodeConfig(folderPath);
|
|
112
|
+
const name = (nodeConfig?.name || folder.name).replace(/-/g, ' ');
|
|
110
113
|
const displayName = name.length > roomWidth - 4
|
|
111
114
|
? name.substring(0, roomWidth - 7) + '...'
|
|
112
115
|
: name;
|
|
113
116
|
|
|
114
|
-
// Get sub-items
|
|
115
117
|
const subPath = path.join(dirPath, folder.name);
|
|
116
118
|
const subEntries = fs.existsSync(subPath)
|
|
117
119
|
? fs.readdirSync(subPath, { withFileTypes: true })
|
|
@@ -119,25 +121,32 @@ function generateDungeonMap(dirPath: string): string {
|
|
|
119
121
|
: [];
|
|
120
122
|
|
|
121
123
|
if (line === 0) {
|
|
122
|
-
// Empty line
|
|
123
124
|
rowStr += ' '.repeat(roomWidth - 1) + wall;
|
|
124
125
|
} else if (line === 1) {
|
|
125
|
-
|
|
126
|
+
const statusIcon = nodeConfig?.status === 'done' ? '[DONE]'
|
|
127
|
+
: nodeConfig?.isBoss ? '[BOSS]'
|
|
128
|
+
: '';
|
|
129
|
+
const title = statusIcon ? `${statusIcon}` : `[${displayName}]`;
|
|
130
|
+
const padding = roomWidth - title.length - 1;
|
|
131
|
+
rowStr += ' ' + title + ' '.repeat(Math.max(0, padding - 1)) + wall;
|
|
132
|
+
} else if (line === 2 && !nodeConfig?.isBoss) {
|
|
126
133
|
const title = `[${displayName}]`;
|
|
127
134
|
const padding = roomWidth - title.length - 1;
|
|
128
135
|
rowStr += ' ' + title + ' '.repeat(Math.max(0, padding - 1)) + wall;
|
|
129
136
|
} else if (line >= 2 && line <= 4) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const subName = subEntries[itemIndex].name.replace(/-/g, ' ');
|
|
137
|
+
const itemIndex = nodeConfig?.isBoss ? line - 2 : line - 3;
|
|
138
|
+
if (itemIndex >= 0 && itemIndex < subEntries.length) {
|
|
139
|
+
const subConfig = readNodeConfig(path.join(subPath, subEntries[itemIndex].name));
|
|
140
|
+
const subName = (subConfig?.name || subEntries[itemIndex].name).replace(/-/g, ' ');
|
|
134
141
|
const shortName = subName.length > roomWidth - 6
|
|
135
142
|
? subName.substring(0, roomWidth - 9) + '...'
|
|
136
143
|
: subName;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
144
|
+
|
|
145
|
+
let marker = task;
|
|
146
|
+
if (subConfig?.status === 'done') marker = done;
|
|
147
|
+
else if (subConfig?.status === 'blocked') marker = blocked;
|
|
148
|
+
else if (subConfig?.isBoss) marker = milestone;
|
|
149
|
+
|
|
141
150
|
const itemStr = `${marker} ${shortName}`;
|
|
142
151
|
const itemPadding = roomWidth - itemStr.length - 1;
|
|
143
152
|
rowStr += ' ' + itemStr + ' '.repeat(Math.max(0, itemPadding - 1)) + wall;
|
|
@@ -145,11 +154,9 @@ function generateDungeonMap(dirPath: string): string {
|
|
|
145
154
|
rowStr += ' '.repeat(roomWidth - 1) + wall;
|
|
146
155
|
}
|
|
147
156
|
} else {
|
|
148
|
-
// Empty line
|
|
149
157
|
rowStr += ' '.repeat(roomWidth - 1) + wall;
|
|
150
158
|
}
|
|
151
159
|
|
|
152
|
-
// Add door between rooms
|
|
153
160
|
if (colIndex < row.length - 1 && line === 3) {
|
|
154
161
|
rowStr = rowStr.slice(0, -1) + door + door + door;
|
|
155
162
|
} else if (colIndex < row.length - 1) {
|
|
@@ -157,7 +164,6 @@ function generateDungeonMap(dirPath: string): string {
|
|
|
157
164
|
}
|
|
158
165
|
});
|
|
159
166
|
|
|
160
|
-
// Fill empty space if odd number of rooms
|
|
161
167
|
if (row.length < roomsPerRow) {
|
|
162
168
|
rowStr += ' '.repeat(roomWidth) + wall;
|
|
163
169
|
}
|
|
@@ -165,16 +171,14 @@ function generateDungeonMap(dirPath: string): string {
|
|
|
165
171
|
lines.push(rowStr);
|
|
166
172
|
}
|
|
167
173
|
|
|
168
|
-
// Bottom border with doors to next row
|
|
169
174
|
if (rowIndex < rows.length - 1) {
|
|
170
175
|
let borderStr = ' ' + wall.repeat(Math.floor(roomWidth / 2)) + door;
|
|
171
176
|
borderStr += wall.repeat(roomWidth - 1) + door;
|
|
172
177
|
borderStr += wall.repeat(Math.floor(roomWidth / 2) + 1);
|
|
173
178
|
lines.push(borderStr);
|
|
174
179
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
corridorStr += ' '.repeat(roomWidth - 1) + '│';
|
|
180
|
+
let corridorStr = ' ' + ' '.repeat(Math.floor(roomWidth / 2)) + '|';
|
|
181
|
+
corridorStr += ' '.repeat(roomWidth - 1) + '|';
|
|
178
182
|
lines.push(corridorStr);
|
|
179
183
|
|
|
180
184
|
borderStr = ' ' + wall.repeat(Math.floor(roomWidth / 2)) + door;
|
|
@@ -184,23 +188,111 @@ function generateDungeonMap(dirPath: string): string {
|
|
|
184
188
|
}
|
|
185
189
|
});
|
|
186
190
|
|
|
187
|
-
// Bottom border
|
|
188
191
|
lines.push(' ' + wall.repeat(roomWidth * roomsPerRow + 3));
|
|
189
192
|
|
|
190
|
-
// Legend
|
|
191
193
|
lines.push('');
|
|
192
|
-
lines.push(`Legend: ${task} Task ${milestone} Milestone ${
|
|
194
|
+
lines.push(`Legend: ${task} Task ${done} Done ${milestone} Boss/Milestone ${blocked} Blocked ${door} Door`);
|
|
193
195
|
|
|
194
196
|
return lines.join('\n');
|
|
195
197
|
}
|
|
196
198
|
|
|
199
|
+
// Parse human-readable date
|
|
200
|
+
function parseDeadline(input: string): string | null {
|
|
201
|
+
const lower = input.toLowerCase().trim();
|
|
202
|
+
const today = new Date();
|
|
203
|
+
|
|
204
|
+
if (lower === 'today') {
|
|
205
|
+
return today.toISOString().split('T')[0];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (lower === 'tomorrow') {
|
|
209
|
+
today.setDate(today.getDate() + 1);
|
|
210
|
+
return today.toISOString().split('T')[0];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// +Nd format (e.g., +3d, +7d)
|
|
214
|
+
const plusDaysMatch = lower.match(/^\+(\d+)d$/);
|
|
215
|
+
if (plusDaysMatch) {
|
|
216
|
+
today.setDate(today.getDate() + parseInt(plusDaysMatch[1]));
|
|
217
|
+
return today.toISOString().split('T')[0];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Try parsing as date
|
|
221
|
+
const parsed = new Date(input);
|
|
222
|
+
if (!isNaN(parsed.getTime())) {
|
|
223
|
+
return parsed.toISOString().split('T')[0];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Format deadline for display
|
|
230
|
+
function formatDeadline(deadline: string): string {
|
|
231
|
+
const deadlineDate = new Date(deadline);
|
|
232
|
+
const today = new Date();
|
|
233
|
+
today.setHours(0, 0, 0, 0);
|
|
234
|
+
deadlineDate.setHours(0, 0, 0, 0);
|
|
235
|
+
|
|
236
|
+
const diffDays = Math.ceil((deadlineDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
|
|
237
|
+
|
|
238
|
+
if (diffDays < 0) return `OVERDUE ${Math.abs(diffDays)}d`;
|
|
239
|
+
if (diffDays === 0) return 'TODAY';
|
|
240
|
+
if (diffDays === 1) return 'tomorrow';
|
|
241
|
+
if (diffDays <= 7) return `${diffDays}d left`;
|
|
242
|
+
|
|
243
|
+
return deadlineDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Get depth of current path relative to storage root
|
|
247
|
+
function getDepth(currentPath: string, storagePath: string): number {
|
|
248
|
+
const relative = path.relative(storagePath, currentPath);
|
|
249
|
+
if (!relative) return 0;
|
|
250
|
+
return relative.split(path.sep).length;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Mark node as done recursively
|
|
254
|
+
function markDoneRecursive(nodePath: string, storagePath: string): { xpGained: number; tasksCompleted: number; bossesDefeated: number } {
|
|
255
|
+
let result = { xpGained: 0, tasksCompleted: 0, bossesDefeated: 0 };
|
|
256
|
+
|
|
257
|
+
const config = readNodeConfig(nodePath);
|
|
258
|
+
if (!config || config.status === 'done') {
|
|
259
|
+
return result;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// First, mark all children as done
|
|
263
|
+
const entries = fs.readdirSync(nodePath, { withFileTypes: true });
|
|
264
|
+
for (const entry of entries) {
|
|
265
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
266
|
+
const childResult = markDoneRecursive(path.join(nodePath, entry.name), storagePath);
|
|
267
|
+
result.xpGained += childResult.xpGained;
|
|
268
|
+
result.tasksCompleted += childResult.tasksCompleted;
|
|
269
|
+
result.bossesDefeated += childResult.bossesDefeated;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Mark this node as done
|
|
274
|
+
const depth = getDepth(nodePath, storagePath);
|
|
275
|
+
const xp = config.xp || calculateXP(depth, config.isBoss || false);
|
|
276
|
+
|
|
277
|
+
writeNodeConfig(nodePath, {
|
|
278
|
+
...config,
|
|
279
|
+
status: 'done',
|
|
280
|
+
completedAt: new Date().toISOString(),
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
result.xpGained += xp;
|
|
284
|
+
result.tasksCompleted += 1;
|
|
285
|
+
if (config.isBoss) result.bossesDefeated += 1;
|
|
286
|
+
|
|
287
|
+
return result;
|
|
288
|
+
}
|
|
289
|
+
|
|
197
290
|
export interface CommandResult {
|
|
198
291
|
output?: string;
|
|
199
292
|
newPath?: string;
|
|
200
293
|
reloadConfig?: boolean;
|
|
201
294
|
}
|
|
202
295
|
|
|
203
|
-
// Pending schema waiting to be saved
|
|
204
296
|
export interface PendingSchema {
|
|
205
297
|
title: string;
|
|
206
298
|
content: string;
|
|
@@ -208,25 +300,21 @@ export interface PendingSchema {
|
|
|
208
300
|
tree?: any[];
|
|
209
301
|
}
|
|
210
302
|
|
|
211
|
-
// Conversation history for AI context
|
|
212
303
|
export interface ConversationMessage {
|
|
213
304
|
role: 'user' | 'assistant';
|
|
214
305
|
content: string;
|
|
215
306
|
}
|
|
216
307
|
|
|
217
|
-
// Session state for dialog mode
|
|
218
308
|
export interface SessionState {
|
|
219
309
|
pending: PendingSchema | null;
|
|
220
310
|
history: ConversationMessage[];
|
|
221
311
|
}
|
|
222
312
|
|
|
223
|
-
// Global session state
|
|
224
313
|
export const sessionState: SessionState = {
|
|
225
314
|
pending: null,
|
|
226
315
|
history: []
|
|
227
316
|
};
|
|
228
317
|
|
|
229
|
-
// Format items in columns like native ls
|
|
230
318
|
function formatColumns(items: string[], termWidth: number = 80): string {
|
|
231
319
|
if (items.length === 0) return '';
|
|
232
320
|
|
|
@@ -242,7 +330,6 @@ function formatColumns(items: string[], termWidth: number = 80): string {
|
|
|
242
330
|
return rows.join('\n');
|
|
243
331
|
}
|
|
244
332
|
|
|
245
|
-
// Copy to clipboard (cross-platform)
|
|
246
333
|
function copyToClipboard(text: string): void {
|
|
247
334
|
const platform = process.platform;
|
|
248
335
|
try {
|
|
@@ -251,7 +338,6 @@ function copyToClipboard(text: string): void {
|
|
|
251
338
|
} else if (platform === 'win32') {
|
|
252
339
|
execSync('clip', { input: text });
|
|
253
340
|
} else {
|
|
254
|
-
// Linux - try xclip or xsel
|
|
255
341
|
try {
|
|
256
342
|
execSync('xclip -selection clipboard', { input: text });
|
|
257
343
|
} catch {
|
|
@@ -259,11 +345,10 @@ function copyToClipboard(text: string): void {
|
|
|
259
345
|
}
|
|
260
346
|
}
|
|
261
347
|
} catch (e) {
|
|
262
|
-
// Silently fail
|
|
348
|
+
// Silently fail
|
|
263
349
|
}
|
|
264
350
|
}
|
|
265
351
|
|
|
266
|
-
// Helper function for recursive copy
|
|
267
352
|
function copyRecursive(src: string, dest: string): void {
|
|
268
353
|
const stat = fs.statSync(src);
|
|
269
354
|
|
|
@@ -283,6 +368,81 @@ function copyRecursive(src: string, dest: string): void {
|
|
|
283
368
|
}
|
|
284
369
|
}
|
|
285
370
|
|
|
371
|
+
// Build tree with status and deadline info
|
|
372
|
+
function getTreeWithStatus(
|
|
373
|
+
dirPath: string,
|
|
374
|
+
prefix: string = '',
|
|
375
|
+
isRoot: boolean = true,
|
|
376
|
+
maxDepth: number = 10,
|
|
377
|
+
currentDepth: number = 0,
|
|
378
|
+
showFiles: boolean = false
|
|
379
|
+
): string[] {
|
|
380
|
+
const lines: string[] = [];
|
|
381
|
+
|
|
382
|
+
if (!fs.existsSync(dirPath)) {
|
|
383
|
+
return lines;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (currentDepth >= maxDepth) {
|
|
387
|
+
return lines;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
391
|
+
const dirs = entries.filter(e => e.isDirectory() && !e.name.startsWith('.'));
|
|
392
|
+
const files = showFiles ? entries.filter(e => e.isFile() && !e.name.startsWith('.')) : [];
|
|
393
|
+
|
|
394
|
+
const allItems = [...dirs, ...files];
|
|
395
|
+
|
|
396
|
+
allItems.forEach((entry, index) => {
|
|
397
|
+
const isLast = index === allItems.length - 1;
|
|
398
|
+
const connector = isLast ? '└── ' : '├── ';
|
|
399
|
+
const nextPrefix = isLast ? ' ' : '│ ';
|
|
400
|
+
|
|
401
|
+
if (entry.isDirectory()) {
|
|
402
|
+
const nodePath = path.join(dirPath, entry.name);
|
|
403
|
+
const config = readNodeConfig(nodePath);
|
|
404
|
+
|
|
405
|
+
let displayName = config?.name || entry.name;
|
|
406
|
+
const tags: string[] = [];
|
|
407
|
+
|
|
408
|
+
// Add status tag
|
|
409
|
+
if (config?.status === 'done') {
|
|
410
|
+
tags.push('DONE');
|
|
411
|
+
} else if (config?.status === 'blocked') {
|
|
412
|
+
tags.push('BLOCKED');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Add boss tag
|
|
416
|
+
if (config?.isBoss) {
|
|
417
|
+
tags.push('BOSS');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Add deadline tag
|
|
421
|
+
if (config?.deadline && config.status !== 'done') {
|
|
422
|
+
tags.push(formatDeadline(config.deadline));
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const tagStr = tags.length > 0 ? ` [${tags.join('] [')}]` : '';
|
|
426
|
+
|
|
427
|
+
lines.push(`${prefix}${connector}${displayName}/${tagStr}`);
|
|
428
|
+
|
|
429
|
+
const childLines = getTreeWithStatus(
|
|
430
|
+
nodePath,
|
|
431
|
+
prefix + nextPrefix,
|
|
432
|
+
false,
|
|
433
|
+
maxDepth,
|
|
434
|
+
currentDepth + 1,
|
|
435
|
+
showFiles
|
|
436
|
+
);
|
|
437
|
+
lines.push(...childLines);
|
|
438
|
+
} else {
|
|
439
|
+
lines.push(`${prefix}${connector}${entry.name}`);
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
return lines;
|
|
444
|
+
}
|
|
445
|
+
|
|
286
446
|
export async function processCommand(
|
|
287
447
|
input: string,
|
|
288
448
|
currentPath: string,
|
|
@@ -290,7 +450,6 @@ export async function processCommand(
|
|
|
290
450
|
signal?: AbortSignal,
|
|
291
451
|
rl?: readline.Interface
|
|
292
452
|
): Promise<CommandResult> {
|
|
293
|
-
// Check for clipboard pipe
|
|
294
453
|
const clipboardPipe = /\s*\|\s*(pbcopy|copy|clip)\s*$/i;
|
|
295
454
|
const shouldCopy = clipboardPipe.test(input);
|
|
296
455
|
const cleanInput = input.replace(clipboardPipe, '').trim();
|
|
@@ -298,7 +457,6 @@ export async function processCommand(
|
|
|
298
457
|
const parts = cleanInput.split(' ').filter(p => p.length > 0);
|
|
299
458
|
const command = parts[0].toLowerCase();
|
|
300
459
|
|
|
301
|
-
// Helper to wrap result with clipboard copy
|
|
302
460
|
const wrapResult = (result: CommandResult): CommandResult => {
|
|
303
461
|
if (shouldCopy && result.output) {
|
|
304
462
|
copyToClipboard(result.output);
|
|
@@ -313,9 +471,350 @@ export async function processCommand(
|
|
|
313
471
|
return wrapResult({ output: `Roguelike CLI v${pkg.version}` });
|
|
314
472
|
}
|
|
315
473
|
|
|
474
|
+
// Stats command
|
|
475
|
+
if (command === 'stats') {
|
|
476
|
+
return wrapResult({ output: formatStats() });
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Achievements command
|
|
480
|
+
if (command === 'achievements' || command === 'ach') {
|
|
481
|
+
return wrapResult({ output: formatAchievements() });
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Done command - mark current node as completed
|
|
485
|
+
if (command === 'done') {
|
|
486
|
+
const nodeConfig = readNodeConfig(currentPath);
|
|
487
|
+
|
|
488
|
+
if (!nodeConfig) {
|
|
489
|
+
return wrapResult({ output: 'No task at current location. Navigate to a task first.' });
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (nodeConfig.status === 'done') {
|
|
493
|
+
return wrapResult({ output: 'This task is already completed.' });
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Mark done recursively
|
|
497
|
+
const result = markDoneRecursive(currentPath, config.storagePath);
|
|
498
|
+
|
|
499
|
+
// Update profile with XP and achievements
|
|
500
|
+
const depth = getDepth(currentPath, config.storagePath);
|
|
501
|
+
const taskResult = completeTask(
|
|
502
|
+
result.xpGained,
|
|
503
|
+
nodeConfig.isBoss || false,
|
|
504
|
+
depth,
|
|
505
|
+
nodeConfig.createdAt
|
|
506
|
+
);
|
|
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
|
+
|
|
516
|
+
let output = `\n=== TASK COMPLETED ===\n`;
|
|
517
|
+
output += `\nTasks completed: ${result.tasksCompleted}`;
|
|
518
|
+
if (result.bossesDefeated > 0) {
|
|
519
|
+
output += `\nBosses defeated: ${result.bossesDefeated}`;
|
|
520
|
+
}
|
|
521
|
+
output += `\n+${result.xpGained} XP`;
|
|
522
|
+
|
|
523
|
+
if (taskResult.levelUp) {
|
|
524
|
+
output += `\n\n*** LEVEL UP! ***`;
|
|
525
|
+
output += `\nYou are now level ${taskResult.newLevel}!`;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (taskResult.newAchievements.length > 0) {
|
|
529
|
+
output += `\n\n=== NEW ACHIEVEMENTS ===`;
|
|
530
|
+
for (const ach of taskResult.newAchievements) {
|
|
531
|
+
output += `\n[x] ${ach.name}: ${ach.description}`;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
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}`;
|
|
564
|
+
output += '\n';
|
|
565
|
+
|
|
566
|
+
return wrapResult({ output });
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Deadline command
|
|
570
|
+
if (command === 'deadline') {
|
|
571
|
+
if (parts.length < 2) {
|
|
572
|
+
return wrapResult({ output: 'Usage: deadline <date>\nExamples: deadline today, deadline tomorrow, deadline +3d, deadline Jan 15' });
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const dateStr = parts.slice(1).join(' ');
|
|
576
|
+
const deadline = parseDeadline(dateStr);
|
|
577
|
+
|
|
578
|
+
if (!deadline) {
|
|
579
|
+
return wrapResult({ output: `Could not parse date: ${dateStr}` });
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const nodeConfig = readNodeConfig(currentPath);
|
|
583
|
+
if (!nodeConfig) {
|
|
584
|
+
return wrapResult({ output: 'No task at current location.' });
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
writeNodeConfig(currentPath, {
|
|
588
|
+
...nodeConfig,
|
|
589
|
+
deadline,
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
return wrapResult({ output: `Deadline set: ${formatDeadline(deadline)}` });
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Boss command - mark as boss/milestone
|
|
596
|
+
if (command === 'boss' || command === 'milestone') {
|
|
597
|
+
const nodeConfig = readNodeConfig(currentPath);
|
|
598
|
+
if (!nodeConfig) {
|
|
599
|
+
return wrapResult({ output: 'No task at current location.' });
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const newIsBoss = !nodeConfig.isBoss;
|
|
603
|
+
const depth = getDepth(currentPath, config.storagePath);
|
|
604
|
+
|
|
605
|
+
writeNodeConfig(currentPath, {
|
|
606
|
+
...nodeConfig,
|
|
607
|
+
isBoss: newIsBoss,
|
|
608
|
+
xp: calculateXP(depth, newIsBoss),
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
return wrapResult({ output: newIsBoss ? 'Marked as BOSS task (3x XP)' : 'Removed BOSS status' });
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Block command - block <node> or block "reason"
|
|
615
|
+
if (command === 'block') {
|
|
616
|
+
const nodeConfig = readNodeConfig(currentPath);
|
|
617
|
+
if (!nodeConfig) {
|
|
618
|
+
return wrapResult({ output: 'No task at current location.' });
|
|
619
|
+
}
|
|
620
|
+
|
|
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
|
+
}
|
|
651
|
+
|
|
652
|
+
writeNodeConfig(currentPath, {
|
|
653
|
+
...nodeConfig,
|
|
654
|
+
status: 'blocked',
|
|
655
|
+
blockedBy,
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
return wrapResult({ output: blockMessage });
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Unblock command
|
|
662
|
+
if (command === 'unblock') {
|
|
663
|
+
const nodeConfig = readNodeConfig(currentPath);
|
|
664
|
+
if (!nodeConfig) {
|
|
665
|
+
return wrapResult({ output: 'No task at current location.' });
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
writeNodeConfig(currentPath, {
|
|
669
|
+
...nodeConfig,
|
|
670
|
+
status: 'open',
|
|
671
|
+
blockedBy: [],
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
return wrapResult({ output: 'Task unblocked' });
|
|
675
|
+
}
|
|
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
|
+
|
|
751
|
+
// Status command - show current task status
|
|
752
|
+
if (command === 'status') {
|
|
753
|
+
const nodeConfig = readNodeConfig(currentPath);
|
|
754
|
+
if (!nodeConfig) {
|
|
755
|
+
return wrapResult({ output: 'No task at current location.' });
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const lines: string[] = [
|
|
759
|
+
'',
|
|
760
|
+
`Task: ${nodeConfig.name}`,
|
|
761
|
+
`Status: ${nodeConfig.status.toUpperCase()}`,
|
|
762
|
+
`XP: ${nodeConfig.xp}`,
|
|
763
|
+
];
|
|
764
|
+
|
|
765
|
+
if (nodeConfig.isBoss) {
|
|
766
|
+
lines.push('Type: BOSS');
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (nodeConfig.deadline) {
|
|
770
|
+
lines.push(`Deadline: ${formatDeadline(nodeConfig.deadline)}`);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (nodeConfig.completedAt) {
|
|
774
|
+
lines.push(`Completed: ${new Date(nodeConfig.completedAt).toLocaleDateString()}`);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
if (nodeConfig.blockedBy && nodeConfig.blockedBy.length > 0) {
|
|
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(', ')}`);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
lines.push('');
|
|
790
|
+
|
|
791
|
+
return wrapResult({ output: lines.join('\n') });
|
|
792
|
+
}
|
|
793
|
+
|
|
316
794
|
// Map command - dungeon visualization
|
|
317
795
|
if (command === 'map') {
|
|
318
|
-
|
|
796
|
+
// Check for --ai flag to use AI generation
|
|
797
|
+
if (parts.includes('--ai') || parts.includes('-a')) {
|
|
798
|
+
const treeLines = getTreeWithStatus(currentPath, '', true, 10, 0, false);
|
|
799
|
+
const treeContent = treeLines.join('\n');
|
|
800
|
+
|
|
801
|
+
if (!treeContent) {
|
|
802
|
+
return wrapResult({ output: 'No tasks to visualize.' });
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const mapContent = await generateDungeonMapWithAI(treeContent, config, signal);
|
|
806
|
+
|
|
807
|
+
if (mapContent) {
|
|
808
|
+
// Save map to file
|
|
809
|
+
const folderName = path.basename(currentPath);
|
|
810
|
+
saveMapFile(currentPath, folderName + '-map', mapContent);
|
|
811
|
+
return wrapResult({ output: mapContent + '\n\n[Map saved as .rlc.map]' });
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
return wrapResult({ output: 'Could not generate AI map. Using default.' });
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const dungeonMap = generateDungeonMap(currentPath, config);
|
|
319
818
|
return wrapResult({ output: dungeonMap });
|
|
320
819
|
}
|
|
321
820
|
|
|
@@ -331,7 +830,15 @@ export async function processCommand(
|
|
|
331
830
|
if (entry.name.startsWith('.')) continue;
|
|
332
831
|
|
|
333
832
|
if (entry.isDirectory()) {
|
|
334
|
-
|
|
833
|
+
const nodePath = path.join(currentPath, entry.name);
|
|
834
|
+
const config = readNodeConfig(nodePath);
|
|
835
|
+
let suffix = '/';
|
|
836
|
+
|
|
837
|
+
if (config?.status === 'done') suffix = '/ [DONE]';
|
|
838
|
+
else if (config?.status === 'blocked') suffix = '/ [BLOCKED]';
|
|
839
|
+
else if (config?.isBoss) suffix = '/ [BOSS]';
|
|
840
|
+
|
|
841
|
+
items.push(entry.name + suffix);
|
|
335
842
|
} else {
|
|
336
843
|
items.push(entry.name);
|
|
337
844
|
}
|
|
@@ -348,7 +855,6 @@ export async function processCommand(
|
|
|
348
855
|
if (command === 'tree') {
|
|
349
856
|
const showFiles = parts.includes('-A') || parts.includes('--all');
|
|
350
857
|
|
|
351
|
-
// Parse depth: --depth=N or -d N
|
|
352
858
|
let maxDepth = 10;
|
|
353
859
|
const depthFlag = parts.find(p => p.startsWith('--depth='));
|
|
354
860
|
if (depthFlag) {
|
|
@@ -360,20 +866,18 @@ export async function processCommand(
|
|
|
360
866
|
}
|
|
361
867
|
}
|
|
362
868
|
|
|
363
|
-
const treeLines =
|
|
869
|
+
const treeLines = getTreeWithStatus(currentPath, '', true, maxDepth, 0, showFiles);
|
|
364
870
|
if (treeLines.length === 0) {
|
|
365
871
|
return wrapResult({ output: 'No items found.' });
|
|
366
872
|
}
|
|
367
873
|
return wrapResult({ output: treeLines.join('\n') });
|
|
368
874
|
}
|
|
369
875
|
|
|
370
|
-
// Handle navigation without 'cd' command
|
|
876
|
+
// Handle navigation without 'cd' command
|
|
371
877
|
if (/^\.{2,}$/.test(command)) {
|
|
372
|
-
// Count dots: .. = 1 level, ... = 2 levels, .... = 3 levels, etc
|
|
373
878
|
const levels = command.length - 1;
|
|
374
879
|
let targetPath = currentPath;
|
|
375
880
|
|
|
376
|
-
// Already at root?
|
|
377
881
|
if (targetPath === config.storagePath) {
|
|
378
882
|
return { output: 'Already at root.' };
|
|
379
883
|
}
|
|
@@ -381,14 +885,12 @@ export async function processCommand(
|
|
|
381
885
|
for (let i = 0; i < levels; i++) {
|
|
382
886
|
const parentPath = path.dirname(targetPath);
|
|
383
887
|
|
|
384
|
-
// Stop at storage root
|
|
385
888
|
if (targetPath === config.storagePath || parentPath.length < config.storagePath.length) {
|
|
386
889
|
break;
|
|
387
890
|
}
|
|
388
891
|
|
|
389
892
|
targetPath = parentPath;
|
|
390
893
|
|
|
391
|
-
// If we reached root, stop
|
|
392
894
|
if (targetPath === config.storagePath) {
|
|
393
895
|
break;
|
|
394
896
|
}
|
|
@@ -455,7 +957,6 @@ export async function processCommand(
|
|
|
455
957
|
fs.renameSync(sourcePath, destPath);
|
|
456
958
|
return { output: `Moved: ${source} -> ${dest}` };
|
|
457
959
|
} catch (error: any) {
|
|
458
|
-
// If rename fails (cross-device), copy then delete
|
|
459
960
|
try {
|
|
460
961
|
copyRecursive(sourcePath, destPath);
|
|
461
962
|
fs.rmSync(sourcePath, { recursive: true, force: true });
|
|
@@ -469,7 +970,6 @@ export async function processCommand(
|
|
|
469
970
|
if (command === 'open') {
|
|
470
971
|
const { exec } = require('child_process');
|
|
471
972
|
|
|
472
|
-
// open or open . - open current folder in system file manager
|
|
473
973
|
if (parts.length < 2 || parts[1] === '.') {
|
|
474
974
|
exec(`open "${currentPath}"`);
|
|
475
975
|
return { output: `Opening: ${currentPath}` };
|
|
@@ -478,18 +978,15 @@ export async function processCommand(
|
|
|
478
978
|
const name = parts.slice(1).join(' ');
|
|
479
979
|
const targetPath = path.join(currentPath, name);
|
|
480
980
|
|
|
481
|
-
// Check if target exists
|
|
482
981
|
if (fs.existsSync(targetPath)) {
|
|
483
982
|
const stat = fs.statSync(targetPath);
|
|
484
983
|
|
|
485
984
|
if (stat.isDirectory()) {
|
|
486
|
-
// It's a folder, open in file manager
|
|
487
985
|
exec(`open "${targetPath}"`);
|
|
488
986
|
return { output: `Opening: ${targetPath}` };
|
|
489
987
|
}
|
|
490
988
|
|
|
491
989
|
if (stat.isFile()) {
|
|
492
|
-
// It's a file, show its content (supports | pbcopy)
|
|
493
990
|
const content = fs.readFileSync(targetPath, 'utf-8');
|
|
494
991
|
return wrapResult({ output: content });
|
|
495
992
|
}
|
|
@@ -545,12 +1042,10 @@ export async function processCommand(
|
|
|
545
1042
|
|
|
546
1043
|
const target = parts.slice(1).join(' ');
|
|
547
1044
|
|
|
548
|
-
// Handle cd .., cd ..., cd ...., etc
|
|
549
1045
|
if (/^\.{2,}$/.test(target)) {
|
|
550
1046
|
const levels = target.length - 1;
|
|
551
1047
|
let targetPath = currentPath;
|
|
552
1048
|
|
|
553
|
-
// Already at root?
|
|
554
1049
|
if (targetPath === config.storagePath) {
|
|
555
1050
|
return { output: 'Already at root.' };
|
|
556
1051
|
}
|
|
@@ -558,14 +1053,12 @@ export async function processCommand(
|
|
|
558
1053
|
for (let i = 0; i < levels; i++) {
|
|
559
1054
|
const parentPath = path.dirname(targetPath);
|
|
560
1055
|
|
|
561
|
-
// Stop at storage root
|
|
562
1056
|
if (targetPath === config.storagePath || parentPath.length < config.storagePath.length) {
|
|
563
1057
|
break;
|
|
564
1058
|
}
|
|
565
1059
|
|
|
566
1060
|
targetPath = parentPath;
|
|
567
1061
|
|
|
568
|
-
// If we reached root, stop
|
|
569
1062
|
if (targetPath === config.storagePath) {
|
|
570
1063
|
break;
|
|
571
1064
|
}
|
|
@@ -574,14 +1067,12 @@ export async function processCommand(
|
|
|
574
1067
|
return { newPath: targetPath, output: '' };
|
|
575
1068
|
}
|
|
576
1069
|
|
|
577
|
-
// Handle paths like "cd bank/account" or "cd ../other"
|
|
578
1070
|
if (target.includes('/')) {
|
|
579
1071
|
let targetPath = currentPath;
|
|
580
1072
|
const pathParts = target.split('/');
|
|
581
1073
|
|
|
582
1074
|
for (const part of pathParts) {
|
|
583
1075
|
if (/^\.{2,}$/.test(part)) {
|
|
584
|
-
// Handle .., ..., ...., etc in path
|
|
585
1076
|
const levels = part.length - 1;
|
|
586
1077
|
for (let i = 0; i < levels; i++) {
|
|
587
1078
|
if (targetPath === config.storagePath) break;
|
|
@@ -656,121 +1147,71 @@ Storage: ${config.storagePath}
|
|
|
656
1147
|
|
|
657
1148
|
if (command === 'help') {
|
|
658
1149
|
return wrapResult({
|
|
659
|
-
output: `
|
|
660
|
-
|
|
661
|
-
ls - List all schemas, todos, and notes
|
|
662
|
-
tree - Show directory tree structure
|
|
663
|
-
tree -A - Show tree with files
|
|
664
|
-
tree --depth=N - Limit tree depth (e.g., --depth=2)
|
|
665
|
-
map - Dungeon map visualization
|
|
666
|
-
cd <node> - Navigate into a node
|
|
667
|
-
cd .. - Go back to parent
|
|
668
|
-
pwd - Show current path
|
|
669
|
-
open - Open current folder in Finder
|
|
670
|
-
open <folder> - Open specific folder in Finder
|
|
671
|
-
mkdir <name> - Create new folder
|
|
672
|
-
cp <src> <dest> - Copy file or folder
|
|
673
|
-
mv <src> <dest> - Move/rename file or folder
|
|
674
|
-
rm <name> - Delete file
|
|
675
|
-
rm -rf <name> - Delete folder recursively
|
|
676
|
-
config - Show configuration
|
|
677
|
-
config:apiKey=<key> - Set API key
|
|
678
|
-
v, version - Show version
|
|
679
|
-
<description> - Create schema/todo (AI generates preview)
|
|
680
|
-
save - Save pending schema to disk
|
|
681
|
-
cancel - Discard pending schema
|
|
682
|
-
clean - Show items to delete in current folder
|
|
683
|
-
clean --yes - Delete all items in current folder
|
|
684
|
-
exit/quit - Exit the program
|
|
685
|
-
|
|
686
|
-
Clipboard:
|
|
687
|
-
ls | pbcopy - Copy output to clipboard (macOS)
|
|
688
|
-
tree | pbcopy - Works with any command
|
|
689
|
-
config | copy - Alternative for Windows
|
|
690
|
-
|
|
691
|
-
Workflow:
|
|
692
|
-
1. Type description (e.g., "todo: deploy app")
|
|
693
|
-
2. AI generates schema preview
|
|
694
|
-
3. Refine with more instructions if needed
|
|
695
|
-
4. Type "save" to save or "cancel" to discard
|
|
696
|
-
|
|
697
|
-
Examples:
|
|
1150
|
+
output: `
|
|
1151
|
+
=== ROGUELIKE CLI ===
|
|
698
1152
|
|
|
699
|
-
|
|
1153
|
+
Navigation:
|
|
1154
|
+
ls List tasks and files
|
|
1155
|
+
tree Show task tree with status
|
|
1156
|
+
tree -A Include files
|
|
1157
|
+
tree --depth=N Limit tree depth
|
|
1158
|
+
cd <task> Navigate into task
|
|
1159
|
+
cd .., ... Go back 1 or 2 levels
|
|
1160
|
+
pwd Show current path
|
|
1161
|
+
open Open folder in Finder
|
|
700
1162
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
1163
|
+
Task Management:
|
|
1164
|
+
mkdir <name> Create new task
|
|
1165
|
+
done Mark current task as completed (recursive)
|
|
1166
|
+
undo Undo last done (restores XP)
|
|
1167
|
+
deadline <date> Set deadline (today, tomorrow, +3d, Jan 15)
|
|
1168
|
+
boss Toggle boss/milestone status (3x XP)
|
|
1169
|
+
block [node] Block by task (or text reason)
|
|
1170
|
+
unblock Remove blocked status
|
|
1171
|
+
status Show current task details
|
|
1172
|
+
check Show overdue/upcoming deadlines
|
|
710
1173
|
|
|
711
|
-
|
|
1174
|
+
File Operations:
|
|
1175
|
+
cp <src> <dest> Copy file or folder
|
|
1176
|
+
mv <src> <dest> Move/rename
|
|
1177
|
+
rm <name> Delete file
|
|
1178
|
+
rm -rf <name> Delete folder
|
|
712
1179
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
│ │ (VM) │ │ (VM) │ │
|
|
719
|
-
│ └────────┬─────────┘ └──────────────────┘ │
|
|
720
|
-
│ │ │
|
|
721
|
-
│ ├──────────────────┬─────────────────┐ │
|
|
722
|
-
│ │ │ │ │
|
|
723
|
-
│ ┌────────▼────────┐ ┌─────▼──────┐ ┌──────▼────────┐ │
|
|
724
|
-
│ │ PostgreSQL │ │ Redis │ │ Cloudflare │ │
|
|
725
|
-
│ │ (Existing DB) │ │ Cluster │ │ R2 Storage │ │
|
|
726
|
-
│ └─────────────────┘ └────────────┘ └───────────────┘ │
|
|
727
|
-
└─────────────────────────────────────────────────────────────┘
|
|
1180
|
+
Gamification:
|
|
1181
|
+
stats Show XP, level, streaks
|
|
1182
|
+
achievements Show achievement list
|
|
1183
|
+
map Dungeon map view
|
|
1184
|
+
map --ai AI-generated dungeon map
|
|
728
1185
|
|
|
729
|
-
|
|
1186
|
+
Schema Generation:
|
|
1187
|
+
<description> AI generates todo/schema preview
|
|
1188
|
+
save Save pending schema
|
|
1189
|
+
cancel Discard pending schema
|
|
730
1190
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
├── redis │
|
|
739
|
-
│ ├── cache-cluster │
|
|
740
|
-
│ └── session-store │
|
|
741
|
-
└── database │
|
|
742
|
-
├── postgres-primary │
|
|
743
|
-
└── postgres-replica │
|
|
744
|
-
│ │
|
|
745
|
-
└───────────────────────────────────────────────────────────────┘
|
|
1191
|
+
Utility:
|
|
1192
|
+
init Setup wizard
|
|
1193
|
+
config Show settings
|
|
1194
|
+
clean --yes Clear current folder
|
|
1195
|
+
v, version Show version
|
|
1196
|
+
help This help
|
|
1197
|
+
exit, quit Exit
|
|
746
1198
|
|
|
747
|
-
|
|
1199
|
+
Clipboard:
|
|
1200
|
+
<cmd> | pbcopy Copy output (macOS)
|
|
1201
|
+
<cmd> | clip Copy output (Windows)
|
|
748
1202
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
│ │ │ │ │ │
|
|
755
|
-
│ │ primary-pod │ │ cache-pod-1 │ │
|
|
756
|
-
│ │ replica-pod-1│ │ cache-pod-2 │ │
|
|
757
|
-
│ │ replica-pod-2│ │ │ │
|
|
758
|
-
│ └──────┬───────┘ └──────┬───────┘ │
|
|
759
|
-
│ │ │ │
|
|
760
|
-
│ └──────────┬───────────┘ │
|
|
761
|
-
│ │ │
|
|
762
|
-
│ ┌───────▼────────┐ │
|
|
763
|
-
│ │ worker-zones │ │
|
|
764
|
-
│ │ zone-1 │ │
|
|
765
|
-
│ │ zone-2 │ │
|
|
766
|
-
│ └────────────────┘ │
|
|
767
|
-
└─────────────────────────────────────────────────────────────┘
|
|
1203
|
+
Examples:
|
|
1204
|
+
block backend-api Block current task by sibling task
|
|
1205
|
+
block "waiting for design" Block with text reason
|
|
1206
|
+
deadline +3d Due in 3 days
|
|
1207
|
+
check See all upcoming deadlines
|
|
768
1208
|
|
|
769
|
-
www.rlc.rocks
|
|
1209
|
+
www.rlc.rocks
|
|
1210
|
+
`.trim()
|
|
770
1211
|
});
|
|
771
1212
|
}
|
|
772
1213
|
|
|
773
|
-
// Save command
|
|
1214
|
+
// Save command
|
|
774
1215
|
if (command === 'save') {
|
|
775
1216
|
if (!sessionState.pending) {
|
|
776
1217
|
return wrapResult({ output: 'Nothing to save. Create a schema first.' });
|
|
@@ -780,17 +1221,14 @@ www.rlc.rocks`
|
|
|
780
1221
|
const safeName = pending.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
781
1222
|
|
|
782
1223
|
if (pending.format === 'tree') {
|
|
783
|
-
// Create folder structure from tree ASCII art
|
|
784
1224
|
const rootPath = path.join(currentPath, safeName);
|
|
785
1225
|
createFoldersFromTree(rootPath, pending.content);
|
|
786
1226
|
|
|
787
|
-
// Clear session
|
|
788
1227
|
sessionState.pending = null;
|
|
789
1228
|
sessionState.history = [];
|
|
790
1229
|
|
|
791
1230
|
return wrapResult({ output: `Created todo folder: ${safeName}/` });
|
|
792
1231
|
} else {
|
|
793
|
-
// Save as .rlc.schema file
|
|
794
1232
|
const schemaPath = saveSchemaFile(
|
|
795
1233
|
currentPath,
|
|
796
1234
|
pending.title,
|
|
@@ -798,7 +1236,6 @@ www.rlc.rocks`
|
|
|
798
1236
|
);
|
|
799
1237
|
const filename = path.basename(schemaPath);
|
|
800
1238
|
|
|
801
|
-
// Clear session
|
|
802
1239
|
sessionState.pending = null;
|
|
803
1240
|
sessionState.history = [];
|
|
804
1241
|
|
|
@@ -806,7 +1243,7 @@ www.rlc.rocks`
|
|
|
806
1243
|
}
|
|
807
1244
|
}
|
|
808
1245
|
|
|
809
|
-
// Cancel command
|
|
1246
|
+
// Cancel command
|
|
810
1247
|
if (command === 'cancel') {
|
|
811
1248
|
if (!sessionState.pending) {
|
|
812
1249
|
return wrapResult({ output: 'Nothing to cancel.' });
|
|
@@ -818,7 +1255,7 @@ www.rlc.rocks`
|
|
|
818
1255
|
return wrapResult({ output: 'Discarded pending schema.' });
|
|
819
1256
|
}
|
|
820
1257
|
|
|
821
|
-
// Clean command
|
|
1258
|
+
// Clean command
|
|
822
1259
|
if (command === 'clean') {
|
|
823
1260
|
const entries = fs.readdirSync(currentPath);
|
|
824
1261
|
const toDelete = entries.filter(e => !e.startsWith('.'));
|
|
@@ -827,7 +1264,6 @@ www.rlc.rocks`
|
|
|
827
1264
|
return wrapResult({ output: 'Directory is already empty.' });
|
|
828
1265
|
}
|
|
829
1266
|
|
|
830
|
-
// Check for --yes flag to skip confirmation
|
|
831
1267
|
if (!parts.includes('--yes') && !parts.includes('-y')) {
|
|
832
1268
|
return wrapResult({
|
|
833
1269
|
output: `Will delete ${toDelete.length} items:\n${toDelete.join('\n')}\n\nRun "clean --yes" to confirm.`
|
|
@@ -842,10 +1278,9 @@ www.rlc.rocks`
|
|
|
842
1278
|
return wrapResult({ output: `Deleted ${toDelete.length} items.` });
|
|
843
1279
|
}
|
|
844
1280
|
|
|
845
|
-
// AI generation
|
|
1281
|
+
// AI generation
|
|
846
1282
|
const fullInput = cleanInput;
|
|
847
1283
|
|
|
848
|
-
// Add user message to history
|
|
849
1284
|
sessionState.history.push({ role: 'user', content: fullInput });
|
|
850
1285
|
|
|
851
1286
|
const schema = await generateSchemaWithAI(fullInput, config, signal, sessionState.history);
|
|
@@ -855,7 +1290,6 @@ www.rlc.rocks`
|
|
|
855
1290
|
}
|
|
856
1291
|
|
|
857
1292
|
if (schema) {
|
|
858
|
-
// Store in pending
|
|
859
1293
|
sessionState.pending = {
|
|
860
1294
|
title: schema.title,
|
|
861
1295
|
content: schema.content,
|
|
@@ -863,7 +1297,6 @@ www.rlc.rocks`
|
|
|
863
1297
|
tree: schema.tree
|
|
864
1298
|
};
|
|
865
1299
|
|
|
866
|
-
// Add assistant response to history
|
|
867
1300
|
sessionState.history.push({ role: 'assistant', content: schema.content });
|
|
868
1301
|
|
|
869
1302
|
const safeName = schema.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|