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,217 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseInput = parseInput;
4
+ exports.generateSchema = generateSchema;
5
+ function parseInput(input) {
6
+ const originalInput = input.trim();
7
+ let cleanedInput = originalInput;
8
+ let title = 'Schema';
9
+ const todoTitleMatch = cleanedInput.match(/^(todo|tasks?)\s+(?:list\s+for\s+)?(.+?)(?:\s*:|\s*$)/i);
10
+ if (todoTitleMatch) {
11
+ title = `TODO ${todoTitleMatch[2].trim()}`;
12
+ cleanedInput = cleanedInput.replace(/^(todo|tasks?)\s+(?:list\s+for\s+)?[^:]+:\s*/i, '');
13
+ }
14
+ else {
15
+ const archTitleMatch = cleanedInput.match(/^(architecture|structure|kubernetes|k8s|yandex|cloud)\s+(?:production\s+)?(.+?)(?:\s*:|\s*$)/i);
16
+ if (archTitleMatch) {
17
+ const prefix = archTitleMatch[1].charAt(0).toUpperCase() + archTitleMatch[1].slice(1);
18
+ title = `${prefix} ${archTitleMatch[2].trim()}`;
19
+ cleanedInput = cleanedInput.replace(/^(architecture|structure|kubernetes|k8s|yandex|cloud)\s+(?:production\s+)?[^:]+:\s*/i, '');
20
+ }
21
+ else {
22
+ const colonMatch = cleanedInput.match(/^(.+?):\s*(.+)$/);
23
+ if (colonMatch) {
24
+ title = colonMatch[1].trim();
25
+ cleanedInput = colonMatch[2].trim();
26
+ }
27
+ }
28
+ }
29
+ cleanedInput = cleanedInput.replace(/^(todo|tasks?)\s*:?\s*/i, '');
30
+ cleanedInput = cleanedInput.replace(/^(architecture|structure)\s*/i, '');
31
+ cleanedInput = cleanedInput.replace(/^(kubernetes\s+cluster\s+with|kubernetes\s+cluster|kubernetes|k8s)\s*/i, '');
32
+ cleanedInput = cleanedInput.trim();
33
+ if (!cleanedInput || cleanedInput.length === 0) {
34
+ return {
35
+ title: originalInput || 'Schema',
36
+ tree: [],
37
+ };
38
+ }
39
+ const parts = cleanedInput.split(',').map(p => p.trim()).filter(p => p.length > 0);
40
+ const tree = [];
41
+ const pathMap = {};
42
+ const branchNodes = {};
43
+ for (const part of parts) {
44
+ const metadata = extractMetadata(part);
45
+ const name = extractNodeName(part);
46
+ if (part.includes('/')) {
47
+ const pathParts = part.split('/').map(p => p.trim()).filter(p => p.length > 0);
48
+ if (pathParts.length > 0) {
49
+ let currentPath = '';
50
+ let parentNode = null;
51
+ pathParts.forEach((pathPart, index) => {
52
+ const cleanPart = extractNodeName(pathPart);
53
+ const fullPath = currentPath ? `${currentPath}/${cleanPart}` : cleanPart;
54
+ if (!pathMap[fullPath]) {
55
+ const newNode = {
56
+ name: cleanPart,
57
+ children: [],
58
+ };
59
+ if (index === pathParts.length - 1) {
60
+ if (metadata.deadline)
61
+ newNode.deadline = metadata.deadline;
62
+ if (metadata.branch)
63
+ newNode.branch = metadata.branch;
64
+ if (metadata.zone)
65
+ newNode.zone = metadata.zone;
66
+ }
67
+ pathMap[fullPath] = newNode;
68
+ if (parentNode) {
69
+ parentNode.children.push(newNode);
70
+ }
71
+ else {
72
+ tree.push(newNode);
73
+ }
74
+ parentNode = newNode;
75
+ }
76
+ else {
77
+ parentNode = pathMap[fullPath];
78
+ }
79
+ currentPath = fullPath;
80
+ });
81
+ }
82
+ }
83
+ else {
84
+ const newNode = {
85
+ name,
86
+ deadline: metadata.deadline,
87
+ branch: metadata.branch,
88
+ zone: metadata.zone,
89
+ children: [],
90
+ };
91
+ if (metadata.branch) {
92
+ if (!branchNodes[metadata.branch]) {
93
+ const branchNode = {
94
+ name: `Branch: ${metadata.branch}`,
95
+ children: [],
96
+ };
97
+ tree.push(branchNode);
98
+ branchNodes[metadata.branch] = branchNode;
99
+ }
100
+ branchNodes[metadata.branch].children.push(newNode);
101
+ }
102
+ else {
103
+ tree.push(newNode);
104
+ }
105
+ }
106
+ }
107
+ return { title, tree };
108
+ }
109
+ function extractNodeName(text) {
110
+ let cleaned = text.trim();
111
+ cleaned = cleaned.replace(/\[deadline:\s*[^\]]+\]|deadline:\s*[^\s,]+/gi, '').trim();
112
+ cleaned = cleaned.replace(/\[branch:\s*[^\]]+\]|branch:\s*[^\s,]+/gi, '').trim();
113
+ cleaned = cleaned.replace(/\[zone:\s*[^\]]+\]|zone:\s*[^\s,]+/gi, '').trim();
114
+ cleaned = cleaned.replace(/^\[|\]$/g, '').replace(/,\s*$/, '').trim();
115
+ return cleaned;
116
+ }
117
+ function extractMetadata(text) {
118
+ const metadata = {};
119
+ const deadlineMatch = text.match(/\[deadline:\s*([^\]]+)\]|deadline:\s*([^\s,]+)/i);
120
+ if (deadlineMatch) {
121
+ metadata.deadline = (deadlineMatch[1] || deadlineMatch[2]).trim();
122
+ }
123
+ const branchMatch = text.match(/\[branch:\s*([^\]]+)\]|branch:\s*([^\s,]+)/i);
124
+ if (branchMatch) {
125
+ metadata.branch = (branchMatch[1] || branchMatch[2]).trim();
126
+ }
127
+ const zoneMatch = text.match(/\[zone:\s*([^\]]+)\]|zone:\s*([^\s,]+)/i);
128
+ if (zoneMatch) {
129
+ metadata.zone = (zoneMatch[1] || zoneMatch[2]).trim();
130
+ }
131
+ return metadata;
132
+ }
133
+ function generateSchema(parsed) {
134
+ if (parsed.tree.length === 0) {
135
+ return [
136
+ `┌─ ${parsed.title} ────────────────────┐`,
137
+ '│ No items │',
138
+ '└────────────────────────────┘',
139
+ ];
140
+ }
141
+ const lines = [];
142
+ const maxWidth = Math.max(parsed.title.length + 4, calculateMaxWidth(parsed.tree, 0));
143
+ const width = Math.min(maxWidth + 10, 75);
144
+ const titlePadding = width - parsed.title.length - 4;
145
+ lines.push(`┌─ ${parsed.title}${' '.repeat(Math.max(0, titlePadding))}┐`);
146
+ lines.push(`│${' '.repeat(width - 2)}│`);
147
+ parsed.tree.forEach((node, index) => {
148
+ const isLast = index === parsed.tree.length - 1;
149
+ const nodeLines = generateTreeNode(node, '│ ', isLast, width);
150
+ lines.push(...nodeLines);
151
+ });
152
+ lines.push(`│${' '.repeat(width - 2)}│`);
153
+ lines.push('└' + '─'.repeat(width - 2) + '┘');
154
+ return lines;
155
+ }
156
+ function calculateMaxWidth(nodes, depth) {
157
+ let maxWidth = 20;
158
+ nodes.forEach(node => {
159
+ let nodeWidth = node.name.length;
160
+ if (node.deadline)
161
+ nodeWidth += node.deadline.length + 3;
162
+ if (node.branch && !node.name.startsWith('Branch:'))
163
+ nodeWidth += node.branch.length + 3;
164
+ if (node.zone && !node.name.startsWith('Zone:'))
165
+ nodeWidth += node.zone.length + 3;
166
+ nodeWidth += depth * 4;
167
+ if (nodeWidth > maxWidth) {
168
+ maxWidth = nodeWidth;
169
+ }
170
+ if (node.children.length > 0) {
171
+ const childWidth = calculateMaxWidth(node.children, depth + 1);
172
+ if (childWidth > maxWidth) {
173
+ maxWidth = childWidth;
174
+ }
175
+ }
176
+ });
177
+ return maxWidth;
178
+ }
179
+ function generateTreeNode(node, prefix, isLast, width) {
180
+ const lines = [];
181
+ let nodeText = node.name;
182
+ if (node.deadline) {
183
+ nodeText += ` [${node.deadline}]`;
184
+ }
185
+ if (node.branch && !node.name.startsWith('Branch:')) {
186
+ nodeText += ` (${node.branch})`;
187
+ }
188
+ if (node.zone && !node.name.startsWith('Zone:')) {
189
+ nodeText += ` [zone: ${node.zone}]`;
190
+ }
191
+ const connector = isLast ? '└──' : '├──';
192
+ const spacing = isLast ? ' ' : '│ ';
193
+ const borderPadding = 2;
194
+ const prefixLen = prefix.length;
195
+ const connectorLen = connector.length + 1;
196
+ const availableWidth = width - prefixLen - connectorLen - borderPadding;
197
+ let displayText = nodeText;
198
+ if (displayText.length > availableWidth) {
199
+ displayText = displayText.substring(0, availableWidth - 3) + '...';
200
+ }
201
+ else {
202
+ displayText = displayText.padEnd(availableWidth);
203
+ }
204
+ lines.push(`${prefix}${connector} ${displayText} │`);
205
+ if (node.children.length > 0) {
206
+ const childPrefixBase = prefix.replace(/│ $/, spacing);
207
+ node.children.forEach((child, index) => {
208
+ const isChildLast = index === node.children.length - 1;
209
+ const childPrefix = isLast
210
+ ? childPrefixBase.replace(/│/g, ' ')
211
+ : childPrefixBase;
212
+ const childLines = generateTreeNode(child, childPrefix, isChildLast, width);
213
+ lines.push(...childLines);
214
+ });
215
+ }
216
+ return lines;
217
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "roguelike-cli",
3
+ "version": "1.0.0",
4
+ "description": "AI-powered interactive terminal for creating schemas and todo lists",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "rlc": "./bin/rlc"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "ts-node src/index.ts",
12
+ "prepublishOnly": "npm run build"
13
+ },
14
+ "keywords": [
15
+ "todo",
16
+ "schema",
17
+ "terminal",
18
+ "cli",
19
+ "interactive",
20
+ "ai",
21
+ "roguelike"
22
+ ],
23
+ "author": "",
24
+ "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/creative-ventures/roguelike-cli"
28
+ },
29
+ "homepage": "https://www.rlc.rocks",
30
+ "dependencies": {
31
+ "@anthropic-ai/sdk": "^0.20.0",
32
+ "chalk": "^5.3.0",
33
+ "commander": "^11.1.0",
34
+ "inquirer": "^9.2.12",
35
+ "ora": "^7.0.1",
36
+ "readline": "^1.3.0"
37
+ },
38
+ "devDependencies": {
39
+ "@types/inquirer": "^9.0.7",
40
+ "@types/node": "^22.0.0",
41
+ "ts-node": "^10.9.2",
42
+ "typescript": "^5.6.0"
43
+ }
44
+ }
45
+
@@ -0,0 +1,139 @@
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+ import { Config } from '../config/config';
3
+
4
+ export interface GeneratedSchema {
5
+ title: string;
6
+ content: string;
7
+ tree?: any[];
8
+ }
9
+
10
+ export interface ConversationMessage {
11
+ role: 'user' | 'assistant';
12
+ content: string;
13
+ }
14
+
15
+ const SYSTEM_PROMPT = `You are a schema generator. Based on user input, generate EITHER:
16
+
17
+ 1. **BLOCK DIAGRAM** - when user mentions: "schema", "architecture", "infrastructure", "diagram", "system"
18
+ Use box-drawing to create visual blocks with connections:
19
+
20
+ Example:
21
+ \`\`\`
22
+ ┌─────────────────────────────────────────────────────────────┐
23
+ │ Kubernetes Cluster │
24
+ │ │
25
+ │ ┌──────────────────┐ ┌──────────────────┐ │
26
+ │ │ Control Plane │ │ Worker Nodes │ │
27
+ │ │ │◄────►│ │ │
28
+ │ │ - API Server │ │ - Node Pool 1 │ │
29
+ │ │ - Scheduler │ │ - Node Pool 2 │ │
30
+ │ │ - etcd │ │ - GPU Pool │ │
31
+ │ └────────┬─────────┘ └────────┬─────────┘ │
32
+ │ │ │ │
33
+ │ └──────────┬───────────────┘ │
34
+ │ │ │
35
+ │ ┌──────────────────┐│┌──────────────────┐ │
36
+ │ │ PostgreSQL │││ Redis │ │
37
+ │ └──────────────────┘│└──────────────────┘ │
38
+ └─────────────────────────────────────────────────────────────┘
39
+ \`\`\`
40
+
41
+ 2. **TREE STRUCTURE** - when user mentions: "todo", "tasks", "list", "steps", "plan"
42
+ Use tree format:
43
+
44
+ Example:
45
+ \`\`\`
46
+ ├── Phase 1: Setup
47
+ │ ├── Create repository
48
+ │ ├── Setup CI/CD
49
+ │ └── Configure environment
50
+ ├── Phase 2: Development
51
+ │ ├── Backend API
52
+ │ └── Frontend UI
53
+ └── Phase 3: Deploy
54
+ \`\`\`
55
+
56
+ Rules:
57
+ 1. Extract a short title for filename
58
+ 2. If user says "schema" or "architecture" - ALWAYS use BLOCK DIAGRAM format
59
+ 3. If user says "todo" or "tasks" - use TREE format
60
+ 4. Keep context from previous messages
61
+
62
+ Respond with JSON:
63
+ {
64
+ "title": "short-title",
65
+ "format": "block" or "tree",
66
+ "content": "the actual ASCII art schema here"
67
+ }`;
68
+
69
+ export async function generateSchemaWithAI(
70
+ input: string,
71
+ config: Config,
72
+ signal?: AbortSignal,
73
+ history?: ConversationMessage[]
74
+ ): Promise<GeneratedSchema | null> {
75
+ if (!config.apiKey) {
76
+ throw new Error('API key not set. Use config:apiKey=<key> to set it.');
77
+ }
78
+
79
+ const client = new Anthropic({
80
+ apiKey: config.apiKey,
81
+ });
82
+
83
+ // Build messages from history or just the current input
84
+ const messages: { role: 'user' | 'assistant'; content: string }[] = [];
85
+
86
+ if (history && history.length > 0) {
87
+ // Add previous messages for context
88
+ for (const msg of history.slice(0, -1)) { // exclude the last one (current input)
89
+ messages.push({
90
+ role: msg.role,
91
+ content: msg.role === 'assistant'
92
+ ? `Previous schema generated:\n${msg.content}`
93
+ : msg.content
94
+ });
95
+ }
96
+ }
97
+
98
+ // Add current user input
99
+ messages.push({
100
+ role: 'user',
101
+ content: input
102
+ });
103
+
104
+ try {
105
+ const model = config.model || 'claude-sonnet-4-20250514';
106
+ const message = await client.messages.create({
107
+ model: model,
108
+ max_tokens: 2000,
109
+ system: SYSTEM_PROMPT,
110
+ messages: messages,
111
+ });
112
+
113
+ const content = message.content[0];
114
+ if (content.type !== 'text') {
115
+ return null;
116
+ }
117
+
118
+ const text = content.text.trim();
119
+
120
+ let jsonMatch = text.match(/\{[\s\S]*\}/);
121
+ if (!jsonMatch) {
122
+ return null;
123
+ }
124
+
125
+ const parsed = JSON.parse(jsonMatch[0]);
126
+
127
+ // AI now returns ready content
128
+ const schemaContent = parsed.content || '';
129
+
130
+ return {
131
+ title: parsed.title || 'schema',
132
+ content: schemaContent,
133
+ };
134
+ } catch (error: any) {
135
+ console.error('AI Error:', error.message);
136
+ return null;
137
+ }
138
+ }
139
+
@@ -0,0 +1,159 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+ import { Config, saveConfig, initConfig } from '../config/config';
5
+
6
+ function question(query: string): Promise<string> {
7
+ return new Promise((resolve) => {
8
+ process.stdout.write(query);
9
+
10
+ let input = '';
11
+ const onData = (chunk: Buffer) => {
12
+ const str = chunk.toString();
13
+
14
+ for (const char of str) {
15
+ if (char === '\n' || char === '\r') {
16
+ process.stdin.removeListener('data', onData);
17
+ process.stdout.write('\n');
18
+ resolve(input);
19
+ return;
20
+ } else if (char === '\x7f' || char === '\b') {
21
+ // Backspace
22
+ if (input.length > 0) {
23
+ input = input.slice(0, -1);
24
+ process.stdout.write('\b \b');
25
+ }
26
+ } else if (char >= ' ') {
27
+ input += char;
28
+ process.stdout.write(char);
29
+ }
30
+ }
31
+ };
32
+
33
+ process.stdin.on('data', onData);
34
+ });
35
+ }
36
+
37
+ // Recursive copy function
38
+ function copyRecursive(src: string, dest: string): void {
39
+ const stat = fs.statSync(src);
40
+
41
+ if (stat.isDirectory()) {
42
+ if (!fs.existsSync(dest)) {
43
+ fs.mkdirSync(dest, { recursive: true });
44
+ }
45
+
46
+ const entries = fs.readdirSync(src);
47
+ for (const entry of entries) {
48
+ copyRecursive(path.join(src, entry), path.join(dest, entry));
49
+ }
50
+ } else {
51
+ fs.copyFileSync(src, dest);
52
+ }
53
+ }
54
+
55
+ export async function initCommand(): Promise<void> {
56
+ console.log('\n╔═══════════════════════════════════════╗');
57
+ console.log('║ ROGUELIKE CLI INITIALIZATION WIZARD ║');
58
+ console.log('╚═══════════════════════════════════════╝\n');
59
+
60
+ // Get existing config if any
61
+ const existingConfig = await initConfig();
62
+ const oldStoragePath = existingConfig?.storagePath;
63
+
64
+ // 1. Root directory
65
+ const defaultRoot = path.join(os.homedir(), '.rlc', 'notes');
66
+ const rootDirAnswer = await question(`Root directory for notes [${defaultRoot}]: `);
67
+ const rootDir = rootDirAnswer.trim() || defaultRoot;
68
+
69
+ // Check if we need to migrate data
70
+ if (oldStoragePath && oldStoragePath !== rootDir && fs.existsSync(oldStoragePath)) {
71
+ const entries = fs.readdirSync(oldStoragePath);
72
+ if (entries.length > 0) {
73
+ const migrateAnswer = await question(`\nMigrate existing data from ${oldStoragePath}? [Y/n]: `);
74
+ if (migrateAnswer.toLowerCase() !== 'n') {
75
+ if (!fs.existsSync(rootDir)) {
76
+ fs.mkdirSync(rootDir, { recursive: true });
77
+ }
78
+
79
+ console.log(`Migrating data...`);
80
+ for (const entry of entries) {
81
+ const srcPath = path.join(oldStoragePath, entry);
82
+ const destPath = path.join(rootDir, entry);
83
+ copyRecursive(srcPath, destPath);
84
+ }
85
+ console.log(`Migrated ${entries.length} items to ${rootDir}`);
86
+ }
87
+ }
88
+ }
89
+
90
+ if (!fs.existsSync(rootDir)) {
91
+ fs.mkdirSync(rootDir, { recursive: true });
92
+ console.log(`Created directory: ${rootDir}`);
93
+ }
94
+
95
+ // 2. AI Provider selection
96
+ console.log('\nSelect AI Provider:');
97
+ console.log(' 1. Claude Sonnet 4.5');
98
+ console.log(' 2. Claude Opus 4.5');
99
+ console.log(' 3. GPT-4o (latest)');
100
+ console.log(' 4. Gemini 3 Pro');
101
+ console.log(' 5. Grok (latest)');
102
+
103
+ const aiChoice = await question('\nEnter choice [1-5] (default: 1): ');
104
+ const aiProviders = [
105
+ { name: 'claude', model: 'claude-sonnet-4-20250514', apiUrl: 'https://api.anthropic.com' },
106
+ { name: 'claude-opus', model: 'claude-opus-4-20250514', apiUrl: 'https://api.anthropic.com' },
107
+ { name: 'openai', model: 'gpt-4o', apiUrl: 'https://api.openai.com' },
108
+ { name: 'gemini', model: 'gemini-3-pro', apiUrl: 'https://generativelanguage.googleapis.com' },
109
+ { name: 'grok', model: 'grok-beta', apiUrl: 'https://api.x.ai' },
110
+ ];
111
+
112
+ const selectedIndex = parseInt(aiChoice.trim()) - 1 || 0;
113
+ const selectedProvider = aiProviders[selectedIndex] || aiProviders[0];
114
+
115
+ console.log(`Selected: ${selectedProvider.name} (${selectedProvider.model})`);
116
+
117
+ // 3. API Key - reuse existing if not provided
118
+ const existingApiKey = existingConfig?.apiKey || '';
119
+ const hasExistingKey = existingApiKey.length > 0;
120
+ const keyPrompt = hasExistingKey
121
+ ? `\nAPI key for ${selectedProvider.name} [press Enter to keep existing]: `
122
+ : `\nEnter API key for ${selectedProvider.name}: `;
123
+
124
+ const apiKeyInput = await question(keyPrompt);
125
+ const apiKey = apiKeyInput.trim() || existingApiKey;
126
+
127
+ if (!apiKey) {
128
+ console.log('Warning: API key not set. You can set it later with config:apiKey=<key>');
129
+ } else if (apiKeyInput.trim()) {
130
+ console.log('API key saved');
131
+ } else {
132
+ console.log('Using existing API key');
133
+ }
134
+
135
+ // Save config
136
+ const config: Config = {
137
+ aiProvider: selectedProvider.name as any,
138
+ apiKey: apiKey,
139
+ apiUrl: selectedProvider.apiUrl,
140
+ storagePath: rootDir,
141
+ currentPath: rootDir,
142
+ model: selectedProvider.model,
143
+ };
144
+
145
+ saveConfig(config);
146
+
147
+ // Ensure storage directory exists
148
+ if (!fs.existsSync(rootDir)) {
149
+ fs.mkdirSync(rootDir, { recursive: true });
150
+ }
151
+
152
+ console.log('\n╔═══════════════════════════════════════╗');
153
+ console.log('║ INITIALIZATION COMPLETE ║');
154
+ console.log('╚═══════════════════════════════════════╝\n');
155
+ console.log(`Root directory: ${rootDir}`);
156
+ console.log(`AI Provider: ${selectedProvider.name}`);
157
+ console.log(`Model: ${selectedProvider.model}\n`);
158
+ }
159
+
@@ -0,0 +1,69 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+
5
+ export interface Config {
6
+ aiProvider: 'claude' | 'claude-opus' | 'openai' | 'gemini' | 'grok' | 'custom';
7
+ apiKey: string;
8
+ apiUrl?: string;
9
+ storagePath: string;
10
+ currentPath: string;
11
+ model?: string;
12
+ }
13
+
14
+ const CONFIG_FILE = path.join(os.homedir(), '.rlc', 'config.json');
15
+ const DEFAULT_STORAGE = path.join(os.homedir(), '.rlc', 'notes');
16
+
17
+ export async function initConfig(): Promise<Config | null> {
18
+ const configDir = path.dirname(CONFIG_FILE);
19
+
20
+ if (!fs.existsSync(configDir)) {
21
+ fs.mkdirSync(configDir, { recursive: true });
22
+ }
23
+
24
+ if (!fs.existsSync(CONFIG_FILE)) {
25
+ return null;
26
+ }
27
+
28
+ const configData = fs.readFileSync(CONFIG_FILE, 'utf-8');
29
+ const config: Config = JSON.parse(configData);
30
+
31
+ if (!fs.existsSync(config.storagePath)) {
32
+ fs.mkdirSync(config.storagePath, { recursive: true });
33
+ }
34
+
35
+ if (!config.currentPath) {
36
+ config.currentPath = config.storagePath;
37
+ }
38
+
39
+ if (!config.model) {
40
+ config.model = 'claude-sonnet-4-20250514';
41
+ }
42
+
43
+ return config;
44
+ }
45
+
46
+ export function saveConfig(config: Config): void {
47
+ const configDir = path.dirname(CONFIG_FILE);
48
+ if (!fs.existsSync(configDir)) {
49
+ fs.mkdirSync(configDir, { recursive: true });
50
+ }
51
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
52
+ }
53
+
54
+ export function getConfig(): Config {
55
+ if (!fs.existsSync(CONFIG_FILE)) {
56
+ throw new Error('Config not initialized. Run rlc first.');
57
+ }
58
+
59
+ const configData = fs.readFileSync(CONFIG_FILE, 'utf-8');
60
+ return JSON.parse(configData);
61
+ }
62
+
63
+ export function updateConfig(updates: Partial<Config>): Config {
64
+ const config = getConfig();
65
+ const updated = { ...config, ...updates };
66
+ saveConfig(updated);
67
+ return updated;
68
+ }
69
+
package/src/index.ts ADDED
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { startInteractive } from './interactive';
4
+ import { initConfig } from './config/config';
5
+ import { initCommand } from './commands/init';
6
+
7
+ async function main() {
8
+ // Check for "rlc init" command line argument
9
+ const args = process.argv.slice(2);
10
+
11
+ if (args[0] === 'init') {
12
+ await initCommand();
13
+ return;
14
+ }
15
+
16
+ let config = await initConfig();
17
+
18
+ if (!config) {
19
+ console.log('\n╔═══════════════════════════════════════╗');
20
+ console.log('║ ROGUELIKE CLI NOT INITIALIZED ║');
21
+ console.log('╚═══════════════════════════════════════╝\n');
22
+ console.log('Running init wizard...\n');
23
+
24
+ await initCommand();
25
+ config = await initConfig();
26
+
27
+ if (!config) {
28
+ console.log('Initialization failed. Please try again.');
29
+ process.exit(1);
30
+ }
31
+ }
32
+
33
+ await startInteractive(config);
34
+ }
35
+
36
+ main().catch(console.error);
37
+