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.
@@ -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
+
@@ -0,0 +1,5 @@
1
+ export function sleep(ms: number): Promise<void> {
2
+ return new Promise(resolve => setTimeout(resolve, ms));
3
+ }
4
+
5
+