roguelike-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL.md +108 -0
- package/bin/rlc +4 -0
- package/dist/ai/claude.js +115 -0
- package/dist/commands/init.js +175 -0
- package/dist/config/config.js +85 -0
- package/dist/index.js +29 -0
- package/dist/interactive/commands.js +552 -0
- package/dist/interactive/index.js +180 -0
- package/dist/interactive/startup.js +38 -0
- package/dist/storage/nodeConfig.js +109 -0
- package/dist/storage/storage.js +155 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/schemaParser.js +217 -0
- package/package.json +45 -0
- package/src/ai/claude.ts +139 -0
- package/src/commands/init.ts +159 -0
- package/src/config/config.ts +69 -0
- package/src/index.ts +37 -0
- package/src/interactive/commands.ts +625 -0
- package/src/interactive/index.ts +174 -0
- package/src/interactive/startup.ts +38 -0
- package/src/storage/nodeConfig.ts +109 -0
- package/src/storage/storage.ts +143 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/schemaParser.ts +259 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import * as readline from 'readline';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { showStartupAnimation } from './startup';
|
|
5
|
+
import { processCommand } from './commands';
|
|
6
|
+
import { Config, initConfig } from '../config/config';
|
|
7
|
+
|
|
8
|
+
function getCompletions(currentPath: string): string[] {
|
|
9
|
+
if (!fs.existsSync(currentPath)) {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
|
14
|
+
const completions: string[] = [];
|
|
15
|
+
|
|
16
|
+
for (const entry of entries) {
|
|
17
|
+
if (entry.name.startsWith('.')) continue;
|
|
18
|
+
|
|
19
|
+
completions.push(entry.name);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return completions;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function startInteractive(initialConfig: Config): Promise<void> {
|
|
26
|
+
await showStartupAnimation();
|
|
27
|
+
|
|
28
|
+
let config = initialConfig;
|
|
29
|
+
|
|
30
|
+
// Use object to hold currentPath so closures always get the current value
|
|
31
|
+
const state = {
|
|
32
|
+
currentPath: config.currentPath
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Completer function that uses state.currentPath
|
|
36
|
+
const completer = (line: string): [string[], string] => {
|
|
37
|
+
try {
|
|
38
|
+
const trimmed = line.trim();
|
|
39
|
+
if (!trimmed) {
|
|
40
|
+
return [[], ''];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Find the last word using regex to get exact position
|
|
44
|
+
const lastWordMatch = line.match(/(\S+)$/);
|
|
45
|
+
if (!lastWordMatch) {
|
|
46
|
+
return [[], line];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const lastWord = lastWordMatch[1];
|
|
50
|
+
const lastWordStart = lastWordMatch.index!;
|
|
51
|
+
const prefix = line.substring(lastWordStart);
|
|
52
|
+
|
|
53
|
+
const parts = trimmed.split(/\s+/);
|
|
54
|
+
const command = parts[0]?.toLowerCase() || '';
|
|
55
|
+
|
|
56
|
+
// Only autocomplete if we're in a command that takes a path argument
|
|
57
|
+
const commandsThatNeedCompletion = ['cd', 'open', 'cp', 'rm', 'mkdir'];
|
|
58
|
+
|
|
59
|
+
if (commandsThatNeedCompletion.includes(command) && parts.length > 1) {
|
|
60
|
+
const completions = getCompletions(state.currentPath);
|
|
61
|
+
const hits = completions.filter((c) => c.toLowerCase().startsWith(lastWord.toLowerCase()));
|
|
62
|
+
|
|
63
|
+
return [hits.length > 0 ? hits : [], prefix];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// If we're typing a command name itself
|
|
67
|
+
if (parts.length === 1) {
|
|
68
|
+
const commandCompletions = ['ls', 'cd', 'mkdir', 'open', 'cp', 'rm', 'tree', 'pwd', 'init', 'config', 'help', 'save', 'cancel', 'clean', 'exit', 'quit', '..', '...'];
|
|
69
|
+
const hits = commandCompletions.filter((c) => c.toLowerCase().startsWith(lastWord.toLowerCase()));
|
|
70
|
+
return [hits.length > 0 ? hits : commandCompletions, prefix];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return [[], line];
|
|
74
|
+
} catch (e) {
|
|
75
|
+
// Return empty completion on any error to prevent crash
|
|
76
|
+
return [[], line];
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const rl = readline.createInterface({
|
|
81
|
+
input: process.stdin,
|
|
82
|
+
output: process.stdout,
|
|
83
|
+
prompt: '> ',
|
|
84
|
+
completer: completer,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
let isProcessingCommand = false;
|
|
88
|
+
let currentCommandAbortController: AbortController | null = null;
|
|
89
|
+
|
|
90
|
+
// Override SIGINT to cancel commands instead of exiting
|
|
91
|
+
process.on('SIGINT', () => {
|
|
92
|
+
if (isProcessingCommand) {
|
|
93
|
+
// Cancel current command but stay in program
|
|
94
|
+
if (currentCommandAbortController) {
|
|
95
|
+
currentCommandAbortController.abort();
|
|
96
|
+
currentCommandAbortController = null;
|
|
97
|
+
}
|
|
98
|
+
isProcessingCommand = false;
|
|
99
|
+
console.log('\n^C');
|
|
100
|
+
rl.prompt();
|
|
101
|
+
} else {
|
|
102
|
+
// Clear the line if not processing
|
|
103
|
+
rl.write('\x1B[2K\r> ');
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
rl.on('line', async (input: string) => {
|
|
108
|
+
const trimmed = input.trim();
|
|
109
|
+
|
|
110
|
+
if (trimmed === 'exit' || trimmed === 'quit') {
|
|
111
|
+
rl.close();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (trimmed === '') {
|
|
116
|
+
rl.prompt();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Create abort controller for this command
|
|
121
|
+
currentCommandAbortController = new AbortController();
|
|
122
|
+
isProcessingCommand = true;
|
|
123
|
+
|
|
124
|
+
const abortController = currentCommandAbortController;
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const result = await processCommand(trimmed, state.currentPath, config, abortController.signal);
|
|
128
|
+
|
|
129
|
+
if (abortController.signal.aborted) {
|
|
130
|
+
rl.prompt();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (result.reloadConfig) {
|
|
135
|
+
const newConfig = await initConfig();
|
|
136
|
+
if (newConfig) {
|
|
137
|
+
config = newConfig;
|
|
138
|
+
state.currentPath = config.currentPath;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (result.newPath) {
|
|
143
|
+
state.currentPath = result.newPath;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (result.output) {
|
|
147
|
+
console.log(result.output);
|
|
148
|
+
}
|
|
149
|
+
} catch (error: any) {
|
|
150
|
+
if (error.name === 'AbortError' || abortController.signal.aborted) {
|
|
151
|
+
console.log('\nCommand cancelled.');
|
|
152
|
+
} else {
|
|
153
|
+
console.error(`Error: ${error.message}`);
|
|
154
|
+
}
|
|
155
|
+
} finally {
|
|
156
|
+
isProcessingCommand = false;
|
|
157
|
+
currentCommandAbortController = null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
rl.prompt();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
rl.on('close', () => {
|
|
164
|
+
if (process.stdin.isTTY) {
|
|
165
|
+
process.stdin.setRawMode(false);
|
|
166
|
+
}
|
|
167
|
+
console.log('\nGoodbye!');
|
|
168
|
+
process.exit(0);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
rl.prompt();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { sleep } from '../utils';
|
|
2
|
+
|
|
3
|
+
const ASCII_ART = [
|
|
4
|
+
'',
|
|
5
|
+
' |',
|
|
6
|
+
' |',
|
|
7
|
+
' + \\',
|
|
8
|
+
' \\.G_.*=.',
|
|
9
|
+
' `(#\'/.\|',
|
|
10
|
+
' .>\' (_--.',
|
|
11
|
+
' _=/d ,^\\',
|
|
12
|
+
'~~ \\)-\' \'',
|
|
13
|
+
' / |',
|
|
14
|
+
' \' \'',
|
|
15
|
+
'',
|
|
16
|
+
'╔═════════════════════════╗',
|
|
17
|
+
'║ Roguelike CLI v1.0 ║',
|
|
18
|
+
'║ www.rlc.rocks ║',
|
|
19
|
+
'╚═════════════════════════╝',
|
|
20
|
+
'',
|
|
21
|
+
' Commands: ls, cd, mkdir, open, cp, mv, rm, tree, pwd, clean',
|
|
22
|
+
' TAB to autocomplete, | pbcopy to copy output',
|
|
23
|
+
'',
|
|
24
|
+
' Workflow: <description> -> refine -> save',
|
|
25
|
+
' init - setup, config - settings, help - examples',
|
|
26
|
+
'',
|
|
27
|
+
' Ready...',
|
|
28
|
+
'',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export async function showStartupAnimation(): Promise<void> {
|
|
32
|
+
for (const line of ASCII_ART) {
|
|
33
|
+
console.log(line);
|
|
34
|
+
await sleep(15);
|
|
35
|
+
}
|
|
36
|
+
await sleep(100);
|
|
37
|
+
}
|
|
38
|
+
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
export interface NodeConfig {
|
|
5
|
+
name: string;
|
|
6
|
+
deadline?: string;
|
|
7
|
+
branch?: string;
|
|
8
|
+
zone?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
createdAt: string;
|
|
11
|
+
updatedAt: string;
|
|
12
|
+
metadata?: Record<string, any>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const CONFIG_FILE = '.rlc.json';
|
|
16
|
+
|
|
17
|
+
export function readNodeConfig(nodePath: string): NodeConfig | null {
|
|
18
|
+
const configPath = path.join(nodePath, CONFIG_FILE);
|
|
19
|
+
|
|
20
|
+
if (!fs.existsSync(configPath)) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const data = fs.readFileSync(configPath, 'utf-8');
|
|
26
|
+
return JSON.parse(data);
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function writeNodeConfig(nodePath: string, config: NodeConfig): void {
|
|
33
|
+
if (!fs.existsSync(nodePath)) {
|
|
34
|
+
fs.mkdirSync(nodePath, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const configPath = path.join(nodePath, CONFIG_FILE);
|
|
38
|
+
const existing = readNodeConfig(nodePath);
|
|
39
|
+
|
|
40
|
+
const updated: NodeConfig = {
|
|
41
|
+
...existing,
|
|
42
|
+
...config,
|
|
43
|
+
updatedAt: new Date().toISOString(),
|
|
44
|
+
createdAt: existing?.createdAt || new Date().toISOString(),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
fs.writeFileSync(configPath, JSON.stringify(updated, null, 2), 'utf-8');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createNode(
|
|
51
|
+
parentPath: string,
|
|
52
|
+
name: string,
|
|
53
|
+
options?: {
|
|
54
|
+
deadline?: string;
|
|
55
|
+
branch?: string;
|
|
56
|
+
zone?: string;
|
|
57
|
+
description?: string;
|
|
58
|
+
metadata?: Record<string, any>;
|
|
59
|
+
}
|
|
60
|
+
): string {
|
|
61
|
+
const safeName = name
|
|
62
|
+
.toLowerCase()
|
|
63
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
64
|
+
.replace(/^-+|-+$/g, '');
|
|
65
|
+
|
|
66
|
+
const nodePath = path.join(parentPath, safeName);
|
|
67
|
+
|
|
68
|
+
const config: NodeConfig = {
|
|
69
|
+
name,
|
|
70
|
+
deadline: options?.deadline,
|
|
71
|
+
branch: options?.branch,
|
|
72
|
+
zone: options?.zone,
|
|
73
|
+
description: options?.description,
|
|
74
|
+
createdAt: new Date().toISOString(),
|
|
75
|
+
updatedAt: new Date().toISOString(),
|
|
76
|
+
metadata: options?.metadata,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
writeNodeConfig(nodePath, config);
|
|
80
|
+
|
|
81
|
+
return nodePath;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Save schema content to .rlc.schema file
|
|
85
|
+
export function saveSchemaFile(dirPath: string, filename: string, content: string): string {
|
|
86
|
+
if (!fs.existsSync(dirPath)) {
|
|
87
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const safeName = filename
|
|
91
|
+
.toLowerCase()
|
|
92
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
93
|
+
.replace(/^-+|-+$/g, '');
|
|
94
|
+
|
|
95
|
+
const schemaPath = path.join(dirPath, `${safeName}.rlc.schema`);
|
|
96
|
+
fs.writeFileSync(schemaPath, content, 'utf-8');
|
|
97
|
+
|
|
98
|
+
return schemaPath;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Read schema file
|
|
102
|
+
export function readSchemaFile(filePath: string): string | null {
|
|
103
|
+
if (!fs.existsSync(filePath)) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
108
|
+
}
|
|
109
|
+
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { readNodeConfig, NodeConfig } from './nodeConfig';
|
|
4
|
+
|
|
5
|
+
export interface SchemaInfo {
|
|
6
|
+
name: string;
|
|
7
|
+
path: string;
|
|
8
|
+
config?: NodeConfig | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function listSchemas(dirPath: string): SchemaInfo[] {
|
|
12
|
+
if (!fs.existsSync(dirPath)) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const items: SchemaInfo[] = [];
|
|
17
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
18
|
+
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
21
|
+
|
|
22
|
+
// Skip hidden files
|
|
23
|
+
if (entry.name.startsWith('.')) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (entry.isDirectory()) {
|
|
28
|
+
const config = readNodeConfig(fullPath);
|
|
29
|
+
items.push({
|
|
30
|
+
name: entry.name,
|
|
31
|
+
path: fullPath,
|
|
32
|
+
config,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return items.sort((a, b) => a.name.localeCompare(b.name));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function navigateToNode(currentPath: string, nodeName: string): string | null {
|
|
41
|
+
const schemas = listSchemas(currentPath);
|
|
42
|
+
const node = schemas.find(s => s.name.toLowerCase() === nodeName.toLowerCase());
|
|
43
|
+
|
|
44
|
+
if (!node) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (fs.statSync(node.path).isDirectory()) {
|
|
49
|
+
return node.path;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return node.path;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function readSchema(nodePath: string): string | null {
|
|
56
|
+
// Read node config and return its info
|
|
57
|
+
const config = readNodeConfig(nodePath);
|
|
58
|
+
|
|
59
|
+
if (!config) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let output = `${config.name}`;
|
|
64
|
+
if (config.deadline) output += `\nDeadline: ${config.deadline}`;
|
|
65
|
+
if (config.branch) output += `\nBranch: ${config.branch}`;
|
|
66
|
+
if (config.zone) output += `\nZone: ${config.zone}`;
|
|
67
|
+
if (config.description) output += `\n${config.description}`;
|
|
68
|
+
|
|
69
|
+
return output;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getTree(dirPath: string, prefix = '', isLast = true, maxDepth = 10, currentDepth = 0, showFiles = false): string[] {
|
|
73
|
+
if (currentDepth >= maxDepth) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const lines: string[] = [];
|
|
78
|
+
|
|
79
|
+
// Get items - either just folders or all entries
|
|
80
|
+
let items: { name: string; path: string; isDir: boolean; config?: any }[] = [];
|
|
81
|
+
|
|
82
|
+
if (showFiles) {
|
|
83
|
+
// Show all files and folders
|
|
84
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
85
|
+
for (const entry of entries) {
|
|
86
|
+
if (entry.name.startsWith('.')) continue;
|
|
87
|
+
|
|
88
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
89
|
+
const config = entry.isDirectory() ? readNodeConfig(fullPath) : null;
|
|
90
|
+
|
|
91
|
+
items.push({
|
|
92
|
+
name: entry.name,
|
|
93
|
+
path: fullPath,
|
|
94
|
+
isDir: entry.isDirectory(),
|
|
95
|
+
config
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
// Sort: folders first, then files
|
|
99
|
+
items.sort((a, b) => {
|
|
100
|
+
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
|
101
|
+
return a.name.localeCompare(b.name);
|
|
102
|
+
});
|
|
103
|
+
} else {
|
|
104
|
+
// Only folders (original behavior)
|
|
105
|
+
const schemas = listSchemas(dirPath);
|
|
106
|
+
items = schemas.map(s => ({
|
|
107
|
+
name: s.name,
|
|
108
|
+
path: s.path,
|
|
109
|
+
isDir: true,
|
|
110
|
+
config: s.config
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
items.forEach((item, index) => {
|
|
115
|
+
const isItemLast = index === items.length - 1;
|
|
116
|
+
const connector = isItemLast ? '└──' : '├──';
|
|
117
|
+
const currentPrefix = prefix + (isLast ? ' ' : '│ ');
|
|
118
|
+
|
|
119
|
+
let displayName = item.name;
|
|
120
|
+
|
|
121
|
+
// Add file indicator
|
|
122
|
+
if (!item.isDir) {
|
|
123
|
+
displayName = `* ${displayName}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (item.config?.deadline) {
|
|
127
|
+
displayName += ` [${item.config.deadline}]`;
|
|
128
|
+
}
|
|
129
|
+
if (item.config?.branch) {
|
|
130
|
+
displayName += ` (${item.config.branch})`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
lines.push(`${prefix}${connector} ${displayName}`);
|
|
134
|
+
|
|
135
|
+
if (item.isDir) {
|
|
136
|
+
const childLines = getTree(item.path, currentPrefix, isItemLast, maxDepth, currentDepth + 1, showFiles);
|
|
137
|
+
lines.push(...childLines);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return lines;
|
|
142
|
+
}
|
|
143
|
+
|