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,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
|
+
|
package/src/ai/claude.ts
ADDED
|
@@ -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
|
+
|