rad-coder 1.0.1 → 1.0.3
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/bin/cli.js +227 -44
- package/package.json +1 -1
- package/server/index.js +314 -37
- package/server/tui.js +282 -0
package/bin/cli.js
CHANGED
|
@@ -13,66 +13,249 @@ function extractCreativeId(input) {
|
|
|
13
13
|
return urlMatch ? urlMatch[1] : input;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Prompt user with a question and arrow-key selectable choices
|
|
18
|
+
* @param {string} question - The question to ask
|
|
19
|
+
* @param {string[]} choices - Array of choices
|
|
20
|
+
* @returns {Promise<number>} - The index of the selected choice (0-based)
|
|
21
|
+
*/
|
|
22
|
+
function promptUser(question, choices) {
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
let selected = 0;
|
|
25
|
+
|
|
26
|
+
// Print question
|
|
27
|
+
console.log(`\n${question}\n`);
|
|
28
|
+
|
|
29
|
+
function draw() {
|
|
30
|
+
// Move cursor up to overwrite previous menu lines
|
|
31
|
+
if (selected !== -1) {
|
|
32
|
+
process.stdout.write(`\x1B[${choices.length}A`);
|
|
33
|
+
}
|
|
34
|
+
for (let i = 0; i < choices.length; i++) {
|
|
35
|
+
process.stdout.write('\x1B[2K'); // clear line
|
|
36
|
+
if (i === selected) {
|
|
37
|
+
process.stdout.write(` \x1B[36m\x1B[1m❯ ${choices[i]}\x1B[0m\n`);
|
|
38
|
+
} else {
|
|
39
|
+
process.stdout.write(` ${choices[i]}\n`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Initial draw (set selected to -1 so it doesn't move cursor up first time)
|
|
45
|
+
const initial = selected;
|
|
46
|
+
selected = -1;
|
|
47
|
+
// Print placeholder lines first
|
|
48
|
+
for (let i = 0; i < choices.length; i++) {
|
|
49
|
+
process.stdout.write('\n');
|
|
50
|
+
}
|
|
51
|
+
// Move back up and draw properly
|
|
52
|
+
process.stdout.write(`\x1B[${choices.length}A`);
|
|
53
|
+
selected = initial;
|
|
54
|
+
draw();
|
|
55
|
+
|
|
56
|
+
// Enable raw mode for keypress detection
|
|
57
|
+
if (process.stdin.isTTY) {
|
|
58
|
+
process.stdin.setRawMode(true);
|
|
59
|
+
}
|
|
60
|
+
process.stdin.resume();
|
|
61
|
+
|
|
62
|
+
function onData(data) {
|
|
63
|
+
const key = data.toString();
|
|
64
|
+
|
|
65
|
+
// Ctrl+C
|
|
66
|
+
if (key === '\x03') {
|
|
67
|
+
cleanup();
|
|
68
|
+
process.exit(0);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Up arrow
|
|
73
|
+
if (key === '\x1B[A') {
|
|
74
|
+
selected = Math.max(0, selected - 1);
|
|
75
|
+
draw();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Down arrow
|
|
80
|
+
if (key === '\x1B[B') {
|
|
81
|
+
selected = Math.min(choices.length - 1, selected + 1);
|
|
82
|
+
draw();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Enter
|
|
87
|
+
if (key === '\r' || key === '\n') {
|
|
88
|
+
cleanup();
|
|
89
|
+
resolve(selected);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Number keys for quick select
|
|
94
|
+
const num = parseInt(key, 10);
|
|
95
|
+
if (num >= 1 && num <= choices.length) {
|
|
96
|
+
selected = num - 1;
|
|
97
|
+
draw();
|
|
98
|
+
cleanup();
|
|
99
|
+
resolve(selected);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function cleanup() {
|
|
105
|
+
process.stdin.removeListener('data', onData);
|
|
106
|
+
if (process.stdin.isTTY) {
|
|
107
|
+
process.stdin.setRawMode(false);
|
|
108
|
+
}
|
|
109
|
+
process.stdin.pause();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
process.stdin.on('data', onData);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Get creative ID from command line argument (skip flags)
|
|
117
|
+
const args = process.argv.slice(2);
|
|
118
|
+
let input = null;
|
|
119
|
+
let editorFlag = null;
|
|
120
|
+
let noEditor = false;
|
|
121
|
+
|
|
122
|
+
for (const arg of args) {
|
|
123
|
+
if (arg.startsWith('--editor=')) {
|
|
124
|
+
editorFlag = arg.split('=')[1];
|
|
125
|
+
} else if (arg === '--no-editor') {
|
|
126
|
+
noEditor = true;
|
|
127
|
+
} else if (!arg.startsWith('--')) {
|
|
128
|
+
input = arg;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Pass editor preferences via environment variables
|
|
133
|
+
if (editorFlag) {
|
|
134
|
+
process.env.RAD_CODER_EDITOR = editorFlag;
|
|
135
|
+
}
|
|
136
|
+
if (noEditor) {
|
|
137
|
+
process.env.RAD_CODER_NO_EDITOR = '1';
|
|
138
|
+
}
|
|
139
|
+
|
|
18
140
|
const creativeId = extractCreativeId(input);
|
|
19
141
|
|
|
20
142
|
if (!creativeId) {
|
|
21
|
-
console.log('Usage: npx rad-coder <creativeId or previewUrl>');
|
|
143
|
+
console.log('Usage: npx rad-coder <creativeId or previewUrl> [options]');
|
|
144
|
+
console.log('');
|
|
145
|
+
console.log('Options:');
|
|
146
|
+
console.log(' --editor=<cmd> Set code editor command (default: code)');
|
|
147
|
+
console.log(' --no-editor Don\'t auto-open code editor');
|
|
22
148
|
console.log('');
|
|
23
149
|
console.log('Examples:');
|
|
24
150
|
console.log(' npx rad-coder 697b80fcc6e904025f5147a0');
|
|
25
151
|
console.log(' npx rad-coder https://studio.responsiveads.com/creatives/697b80fcc6e904025f5147a0/preview');
|
|
152
|
+
console.log(' npx rad-coder 697b80fcc6e904025f5147a0 --editor=cursor');
|
|
26
153
|
process.exit(1);
|
|
27
154
|
}
|
|
28
155
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
} else {
|
|
39
|
-
// Create or use a folder with the creative ID
|
|
40
|
-
userDir = path.join(cwd, creativeId);
|
|
41
|
-
if (!fs.existsSync(userDir)) {
|
|
42
|
-
fs.mkdirSync(userDir);
|
|
43
|
-
console.log(`Created folder: ./${creativeId}`);
|
|
44
|
-
} else {
|
|
156
|
+
async function main() {
|
|
157
|
+
// Determine the target directory
|
|
158
|
+
const cwd = process.cwd();
|
|
159
|
+
const currentDirName = path.basename(cwd);
|
|
160
|
+
let userDir;
|
|
161
|
+
|
|
162
|
+
if (currentDirName === creativeId) {
|
|
163
|
+
// Already in the correct folder
|
|
164
|
+
userDir = cwd;
|
|
45
165
|
console.log(`Using existing folder: ./${creativeId}`);
|
|
166
|
+
} else {
|
|
167
|
+
// Create or use a folder with the creative ID
|
|
168
|
+
userDir = path.join(cwd, creativeId);
|
|
169
|
+
if (!fs.existsSync(userDir)) {
|
|
170
|
+
fs.mkdirSync(userDir);
|
|
171
|
+
console.log(`Created folder: ./${creativeId}`);
|
|
172
|
+
} else {
|
|
173
|
+
console.log(`Using existing folder: ./${creativeId}`);
|
|
174
|
+
}
|
|
46
175
|
}
|
|
47
|
-
}
|
|
48
176
|
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
177
|
+
// Set environment variables for the server
|
|
178
|
+
process.env.RAD_CODER_USER_DIR = userDir;
|
|
179
|
+
process.env.RAD_CODER_PACKAGE_DIR = packageRoot;
|
|
180
|
+
|
|
181
|
+
// Import server module to fetch creative config
|
|
182
|
+
const { fetchCreativeConfig, startServer } = require('../server/index.js');
|
|
183
|
+
|
|
184
|
+
// Fetch creative config first to check for customjs
|
|
185
|
+
const config = await fetchCreativeConfig(creativeId);
|
|
186
|
+
|
|
187
|
+
const customJsPath = path.join(userDir, 'custom.js');
|
|
188
|
+
const customJsExists = fs.existsSync(customJsPath);
|
|
189
|
+
const hasCreativeCustomJs = config.customjs && config.customjs.trim().length > 0;
|
|
190
|
+
|
|
191
|
+
// Handle custom.js file creation/update
|
|
192
|
+
if (hasCreativeCustomJs) {
|
|
193
|
+
if (!customJsExists) {
|
|
194
|
+
// custom.js doesn't exist - ask user what to use
|
|
195
|
+
const choice = await promptUser(
|
|
196
|
+
'Found customJS in this creative. What would you like to use?',
|
|
197
|
+
[
|
|
198
|
+
'Use customJS from the creative (recommended)',
|
|
199
|
+
'Start with blank template'
|
|
200
|
+
]
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
if (choice === 0) {
|
|
204
|
+
// Use customjs from creative
|
|
205
|
+
fs.writeFileSync(customJsPath, config.customjs, 'utf-8');
|
|
206
|
+
console.log(' Created custom.js (from creative)');
|
|
207
|
+
} else {
|
|
208
|
+
// Use template
|
|
209
|
+
const templatePath = path.join(packageRoot, 'templates', 'custom.js');
|
|
210
|
+
if (fs.existsSync(templatePath)) {
|
|
211
|
+
fs.copyFileSync(templatePath, customJsPath);
|
|
212
|
+
console.log(' Created custom.js (from template)');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
// custom.js exists - ask user if they want to overwrite
|
|
217
|
+
const choice = await promptUser(
|
|
218
|
+
'Found customJS in this creative. Your custom.js already exists.',
|
|
219
|
+
[
|
|
220
|
+
'Keep existing custom.js',
|
|
221
|
+
'Overwrite with customJS from creative'
|
|
222
|
+
]
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
if (choice === 1) {
|
|
226
|
+
// Overwrite with creative's customjs
|
|
227
|
+
fs.writeFileSync(customJsPath, config.customjs, 'utf-8');
|
|
228
|
+
console.log(' Overwrote custom.js with creative\'s customJS');
|
|
229
|
+
} else {
|
|
230
|
+
console.log(' Keeping existing custom.js');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
// No customjs in creative - use template if custom.js doesn't exist
|
|
235
|
+
if (!customJsExists) {
|
|
236
|
+
const templatePath = path.join(packageRoot, 'templates', 'custom.js');
|
|
237
|
+
if (fs.existsSync(templatePath)) {
|
|
238
|
+
fs.copyFileSync(templatePath, customJsPath);
|
|
239
|
+
console.log(' Created custom.js (from template)');
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Copy AGENTS.md if it doesn't exist
|
|
245
|
+
const agentsMdPath = path.join(userDir, 'AGENTS.md');
|
|
246
|
+
if (!fs.existsSync(agentsMdPath)) {
|
|
247
|
+
const templatePath = path.join(packageRoot, 'templates', 'AGENTS.md');
|
|
61
248
|
if (fs.existsSync(templatePath)) {
|
|
62
|
-
fs.copyFileSync(templatePath,
|
|
63
|
-
console.log(
|
|
64
|
-
filesCreated = true;
|
|
249
|
+
fs.copyFileSync(templatePath, agentsMdPath);
|
|
250
|
+
console.log(' Created AGENTS.md');
|
|
65
251
|
}
|
|
66
252
|
}
|
|
67
|
-
});
|
|
68
253
|
|
|
69
|
-
|
|
70
|
-
|
|
254
|
+
// Start the server with pre-fetched config
|
|
255
|
+
await startServer(config);
|
|
71
256
|
}
|
|
72
257
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
process.
|
|
76
|
-
|
|
77
|
-
// Run the server
|
|
78
|
-
require('../server/index.js');
|
|
258
|
+
main().catch((err) => {
|
|
259
|
+
console.error('Error:', err.message);
|
|
260
|
+
process.exit(1);
|
|
261
|
+
});
|
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -5,6 +5,50 @@ const chokidar = require('chokidar');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const http = require('http');
|
|
8
|
+
const { spawn } = require('child_process');
|
|
9
|
+
const TUI = require('./tui');
|
|
10
|
+
|
|
11
|
+
// ============================================================
|
|
12
|
+
// TUI Instance & Logging
|
|
13
|
+
// ============================================================
|
|
14
|
+
|
|
15
|
+
let tui = null;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Log a message — routes to TUI scroll area when active, otherwise console.log
|
|
19
|
+
*/
|
|
20
|
+
function log(message) {
|
|
21
|
+
if (tui && !tui.destroyed) {
|
|
22
|
+
tui.log(message);
|
|
23
|
+
} else {
|
|
24
|
+
console.log(message);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Open the user's code editor
|
|
30
|
+
*/
|
|
31
|
+
function openEditor() {
|
|
32
|
+
const editorCmd = process.env.RAD_CODER_EDITOR
|
|
33
|
+
|| process.env.VISUAL
|
|
34
|
+
|| process.env.EDITOR
|
|
35
|
+
|| 'code';
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const child = spawn(editorCmd, [userDir], {
|
|
39
|
+
detached: true,
|
|
40
|
+
stdio: 'ignore',
|
|
41
|
+
shell: process.platform === 'win32'
|
|
42
|
+
});
|
|
43
|
+
child.unref();
|
|
44
|
+
child.on('error', (err) => {
|
|
45
|
+
log(` ✗ Could not open editor "${editorCmd}": ${err.message}`);
|
|
46
|
+
});
|
|
47
|
+
log(` Editor (${editorCmd}) opened`);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
log(` ✗ Could not open editor "${editorCmd}": ${err.message}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
8
52
|
|
|
9
53
|
// ============================================================
|
|
10
54
|
// Directory Configuration
|
|
@@ -19,26 +63,33 @@ const packageDir = process.env.RAD_CODER_PACKAGE_DIR || path.join(__dirname, '..
|
|
|
19
63
|
// CLI Argument Parsing
|
|
20
64
|
// ============================================================
|
|
21
65
|
|
|
22
|
-
|
|
66
|
+
// Check if we're being required as a module (from cli.js) or run directly
|
|
67
|
+
const isModule = require.main !== module;
|
|
23
68
|
|
|
24
|
-
|
|
25
|
-
console.error('\n Usage: npx rad-coder <creativeId or previewUrl>\n');
|
|
26
|
-
console.error(' Examples:');
|
|
27
|
-
console.error(' npx rad-coder 697b80fcc6e904025f5147a0');
|
|
28
|
-
console.error(' npx rad-coder https://studio.responsiveads.com/creatives/697b80fcc6e904025f5147a0/preview\n');
|
|
29
|
-
process.exit(1);
|
|
30
|
-
}
|
|
69
|
+
let creativeId = null;
|
|
31
70
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
*/
|
|
35
|
-
function extractCreativeId(input) {
|
|
36
|
-
// If it's a URL, extract the ID
|
|
37
|
-
const urlMatch = input.match(/creatives\/([a-f0-9]+)/i);
|
|
38
|
-
return urlMatch ? urlMatch[1] : input;
|
|
39
|
-
}
|
|
71
|
+
if (!isModule) {
|
|
72
|
+
const input = process.argv[2];
|
|
40
73
|
|
|
41
|
-
|
|
74
|
+
if (!input) {
|
|
75
|
+
console.error('\n Usage: npx rad-coder <creativeId or previewUrl>\n');
|
|
76
|
+
console.error(' Examples:');
|
|
77
|
+
console.error(' npx rad-coder 697b80fcc6e904025f5147a0');
|
|
78
|
+
console.error(' npx rad-coder https://studio.responsiveads.com/creatives/697b80fcc6e904025f5147a0/preview\n');
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Extract creative ID from URL or use directly
|
|
84
|
+
*/
|
|
85
|
+
function extractCreativeIdLocal(input) {
|
|
86
|
+
// If it's a URL, extract the ID
|
|
87
|
+
const urlMatch = input.match(/creatives\/([a-f0-9]+)/i);
|
|
88
|
+
return urlMatch ? urlMatch[1] : input;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
creativeId = extractCreativeIdLocal(input);
|
|
92
|
+
}
|
|
42
93
|
|
|
43
94
|
// ============================================================
|
|
44
95
|
// Fetch Creative Config from Studio Preview Page
|
|
@@ -46,14 +97,68 @@ const creativeId = extractCreativeId(input);
|
|
|
46
97
|
|
|
47
98
|
let creativeConfig = null;
|
|
48
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Extract a JSON object from HTML using balanced bracket parsing
|
|
102
|
+
* @param {string} html - The HTML content
|
|
103
|
+
* @param {string} startMarker - The marker to find (e.g., 'window.creative = ')
|
|
104
|
+
* @returns {object|null} - Parsed JSON object or null
|
|
105
|
+
*/
|
|
106
|
+
function extractJsonObject(html, startMarker) {
|
|
107
|
+
const startIdx = html.indexOf(startMarker);
|
|
108
|
+
if (startIdx === -1) return null;
|
|
109
|
+
|
|
110
|
+
const jsonStart = startIdx + startMarker.length;
|
|
111
|
+
let braceCount = 0;
|
|
112
|
+
let inString = false;
|
|
113
|
+
let escapeNext = false;
|
|
114
|
+
let endIdx = jsonStart;
|
|
115
|
+
|
|
116
|
+
for (let i = jsonStart; i < html.length; i++) {
|
|
117
|
+
const char = html[i];
|
|
118
|
+
|
|
119
|
+
if (escapeNext) {
|
|
120
|
+
escapeNext = false;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (char === '\\') {
|
|
125
|
+
escapeNext = true;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (char === '"' && !inString) {
|
|
130
|
+
inString = true;
|
|
131
|
+
} else if (char === '"' && inString) {
|
|
132
|
+
inString = false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!inString) {
|
|
136
|
+
if (char === '{') braceCount++;
|
|
137
|
+
if (char === '}') braceCount--;
|
|
138
|
+
|
|
139
|
+
if (braceCount === 0 && char === '}') {
|
|
140
|
+
endIdx = i + 1;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const jsonStr = html.substring(jsonStart, endIdx);
|
|
148
|
+
return JSON.parse(jsonStr);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
49
154
|
/**
|
|
50
155
|
* Fetch and parse creative configuration from studio preview page
|
|
51
156
|
*/
|
|
52
157
|
async function fetchCreativeConfig(creativeId) {
|
|
53
158
|
const previewUrl = `https://studio.responsiveads.com/creatives/${creativeId}/preview`;
|
|
54
159
|
|
|
55
|
-
|
|
56
|
-
|
|
160
|
+
log(` Fetching creative config from studio...`);
|
|
161
|
+
log(` URL: ${previewUrl}\n`);
|
|
57
162
|
|
|
58
163
|
try {
|
|
59
164
|
const response = await fetch(previewUrl);
|
|
@@ -67,6 +172,13 @@ async function fetchCreativeConfig(creativeId) {
|
|
|
67
172
|
const creativeIdMatch = html.match(/window\.creativeId\s*=\s*['"]([^'"]+)['"]/);
|
|
68
173
|
const extractedCreativeId = creativeIdMatch ? creativeIdMatch[1] : creativeId;
|
|
69
174
|
|
|
175
|
+
// Extract window.creative object to get customjs
|
|
176
|
+
let customjs = null;
|
|
177
|
+
const creativeObj = extractJsonObject(html, 'window.creative = ');
|
|
178
|
+
if (creativeObj && creativeObj.config && creativeObj.config.customjs) {
|
|
179
|
+
customjs = creativeObj.config.customjs;
|
|
180
|
+
}
|
|
181
|
+
|
|
70
182
|
// Extract flowlines - try multiple patterns
|
|
71
183
|
let flowlines;
|
|
72
184
|
|
|
@@ -187,11 +299,13 @@ async function fetchCreativeConfig(creativeId) {
|
|
|
187
299
|
name: f.name,
|
|
188
300
|
sizes: f.flowline?.sizes || [],
|
|
189
301
|
isFluid: f.fullyFluid
|
|
190
|
-
}))
|
|
302
|
+
})),
|
|
303
|
+
// Custom JS from the creative (if available)
|
|
304
|
+
customjs: customjs
|
|
191
305
|
};
|
|
192
306
|
|
|
193
307
|
} catch (error) {
|
|
194
|
-
|
|
308
|
+
log(`\n ✗ Failed to fetch creative config: ${error.message}\n`);
|
|
195
309
|
process.exit(1);
|
|
196
310
|
}
|
|
197
311
|
}
|
|
@@ -211,11 +325,11 @@ const clients = new Set();
|
|
|
211
325
|
|
|
212
326
|
wss.on('connection', (ws) => {
|
|
213
327
|
clients.add(ws);
|
|
214
|
-
|
|
328
|
+
log(' Browser connected for hot-reload');
|
|
215
329
|
|
|
216
330
|
ws.on('close', () => {
|
|
217
331
|
clients.delete(ws);
|
|
218
|
-
|
|
332
|
+
log(' Browser disconnected');
|
|
219
333
|
});
|
|
220
334
|
});
|
|
221
335
|
|
|
@@ -271,27 +385,31 @@ const watcher = chokidar.watch(customJsWatchPath, {
|
|
|
271
385
|
ignoreInitial: true
|
|
272
386
|
});
|
|
273
387
|
|
|
274
|
-
watcher.on('change', (
|
|
275
|
-
|
|
276
|
-
|
|
388
|
+
watcher.on('change', (changedPath) => {
|
|
389
|
+
log(` File changed: ${path.basename(changedPath)}`);
|
|
390
|
+
log(' Reloading browsers...');
|
|
277
391
|
broadcastReload();
|
|
278
392
|
});
|
|
279
393
|
|
|
280
394
|
watcher.on('error', (error) => {
|
|
281
|
-
|
|
395
|
+
log(` Watcher error: ${error.message}`);
|
|
282
396
|
});
|
|
283
397
|
|
|
284
398
|
// ============================================================
|
|
285
399
|
// Start Server
|
|
286
400
|
// ============================================================
|
|
287
401
|
|
|
288
|
-
async function start() {
|
|
402
|
+
async function start(prefetchedConfig = null) {
|
|
289
403
|
console.log('\n========================================');
|
|
290
404
|
console.log(' RAD Coder - ResponsiveAds Creative Tester');
|
|
291
405
|
console.log('========================================\n');
|
|
292
406
|
|
|
293
|
-
//
|
|
294
|
-
|
|
407
|
+
// Use pre-fetched config if provided, otherwise fetch it
|
|
408
|
+
if (prefetchedConfig) {
|
|
409
|
+
creativeConfig = prefetchedConfig;
|
|
410
|
+
} else {
|
|
411
|
+
creativeConfig = await fetchCreativeConfig(creativeId);
|
|
412
|
+
}
|
|
295
413
|
|
|
296
414
|
console.log(' Creative Config:');
|
|
297
415
|
console.log(` - Creative ID: ${creativeConfig.creativeId}`);
|
|
@@ -299,6 +417,7 @@ async function start() {
|
|
|
299
417
|
console.log(` - Flowline ID: ${creativeConfig.flowlineId}`);
|
|
300
418
|
console.log(` - Sizes: ${creativeConfig.sizes.join(', ')}`);
|
|
301
419
|
console.log(` - Is Fluid: ${creativeConfig.isFluid}`);
|
|
420
|
+
console.log(` - Has CustomJS: ${creativeConfig.customjs ? 'Yes' : 'No'}`);
|
|
302
421
|
|
|
303
422
|
if (creativeConfig.allFlowlines.length > 1) {
|
|
304
423
|
console.log(`\n Available Flowlines (${creativeConfig.allFlowlines.length}):`);
|
|
@@ -315,7 +434,6 @@ async function start() {
|
|
|
315
434
|
console.log(` Test page: http://${host}:${port}/test.html`);
|
|
316
435
|
console.log(`\n Working directory: ${userDir}`);
|
|
317
436
|
console.log(' Edit custom.js and save to hot-reload\n');
|
|
318
|
-
console.log(' Press Ctrl+C to stop\n');
|
|
319
437
|
|
|
320
438
|
// Small delay to ensure server is fully ready before opening browser
|
|
321
439
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
@@ -324,18 +442,175 @@ async function start() {
|
|
|
324
442
|
try {
|
|
325
443
|
const open = (await import('open')).default;
|
|
326
444
|
await open(`http://${host}:${port}/test.html`);
|
|
327
|
-
console.log(' Browser opened automatically
|
|
445
|
+
console.log(' Browser opened automatically');
|
|
328
446
|
} catch (err) {
|
|
329
|
-
console.log(
|
|
330
|
-
console.log(` Please open http://${host}:${port}/test.html manually
|
|
447
|
+
console.log(` Could not auto-open browser: ${err.message}`);
|
|
448
|
+
console.log(` Please open http://${host}:${port}/test.html manually`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Auto-open editor (unless --no-editor)
|
|
452
|
+
if (!process.env.RAD_CODER_NO_EDITOR) {
|
|
453
|
+
openEditor();
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Start interactive TUI (only if stdin is a TTY)
|
|
457
|
+
if (process.stdin.isTTY) {
|
|
458
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
459
|
+
startInteractiveMenu();
|
|
460
|
+
} else {
|
|
461
|
+
console.log(' Press Ctrl+C to stop\n');
|
|
331
462
|
}
|
|
332
463
|
});
|
|
333
464
|
}
|
|
334
465
|
|
|
335
|
-
|
|
466
|
+
// ============================================================
|
|
467
|
+
// Interactive Menu
|
|
468
|
+
// ============================================================
|
|
469
|
+
|
|
470
|
+
function getMainMenuItems() {
|
|
471
|
+
const items = [
|
|
472
|
+
{ label: 'Open Browser', id: 'open-browser' },
|
|
473
|
+
{ label: 'Open Editor', id: 'open-editor' },
|
|
474
|
+
];
|
|
475
|
+
|
|
476
|
+
if (creativeConfig && creativeConfig.allFlowlines.length > 1) {
|
|
477
|
+
items.push({ label: 'Switch Flowline', id: 'switch-flowline', description: `(${creativeConfig.flowlineName})` });
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
items.push(
|
|
481
|
+
{ label: 'Server Status', id: 'status' },
|
|
482
|
+
{ label: 'Clear Logs', id: 'clear' },
|
|
483
|
+
{ label: 'Restart Server', id: 'restart' },
|
|
484
|
+
{ label: 'Stop Server', id: 'stop' },
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
return items;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function getFlowlineMenuItems() {
|
|
491
|
+
const items = [{ label: '← Back', id: 'back' }];
|
|
492
|
+
creativeConfig.allFlowlines.forEach((fl, i) => {
|
|
493
|
+
const marker = fl.id === creativeConfig.flowlineId ? ' ✓' : '';
|
|
494
|
+
items.push({ label: `${fl.name}${marker}`, id: `flowline-${i}`, flowlineIndex: i });
|
|
495
|
+
});
|
|
496
|
+
return items;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function startInteractiveMenu() {
|
|
500
|
+
tui = new TUI();
|
|
501
|
+
|
|
502
|
+
let inSubMenu = false;
|
|
503
|
+
|
|
504
|
+
function handleSelect(item) {
|
|
505
|
+
// Sub-menu: flowline selection
|
|
506
|
+
if (inSubMenu) {
|
|
507
|
+
if (item.id === 'back') {
|
|
508
|
+
inSubMenu = false;
|
|
509
|
+
tui.updateMenu(getMainMenuItems());
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
// Switch flowline
|
|
513
|
+
const fl = creativeConfig.allFlowlines[item.flowlineIndex];
|
|
514
|
+
if (fl) {
|
|
515
|
+
creativeConfig.flowlineId = fl.id;
|
|
516
|
+
creativeConfig.flowlineName = fl.name;
|
|
517
|
+
creativeConfig.sizes = fl.sizes || [];
|
|
518
|
+
creativeConfig.isFluid = fl.isFluid || false;
|
|
519
|
+
log(` Switched to flowline: ${fl.name}`);
|
|
520
|
+
broadcastReload();
|
|
521
|
+
}
|
|
522
|
+
inSubMenu = false;
|
|
523
|
+
tui.updateMenu(getMainMenuItems());
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Main menu actions
|
|
528
|
+
switch (item.id) {
|
|
529
|
+
case 'open-browser': {
|
|
530
|
+
const { port, host } = creativeConfig.server;
|
|
531
|
+
import('open').then(mod => {
|
|
532
|
+
mod.default(`http://${host}:${port}/test.html`);
|
|
533
|
+
log(' Browser opened');
|
|
534
|
+
}).catch(err => {
|
|
535
|
+
log(` Could not open browser: ${err.message}`);
|
|
536
|
+
});
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
case 'open-editor':
|
|
541
|
+
openEditor();
|
|
542
|
+
break;
|
|
543
|
+
|
|
544
|
+
case 'switch-flowline':
|
|
545
|
+
inSubMenu = true;
|
|
546
|
+
tui.updateMenu(getFlowlineMenuItems());
|
|
547
|
+
break;
|
|
548
|
+
|
|
549
|
+
case 'status': {
|
|
550
|
+
const { port, host } = creativeConfig.server;
|
|
551
|
+
log('');
|
|
552
|
+
log(' ── Server Status ──────────────────');
|
|
553
|
+
log(` Creative ID : ${creativeConfig.creativeId}`);
|
|
554
|
+
log(` Flowline : ${creativeConfig.flowlineName}`);
|
|
555
|
+
log(` Flowline ID : ${creativeConfig.flowlineId}`);
|
|
556
|
+
log(` Sizes : ${creativeConfig.sizes.join(', ') || 'N/A'}`);
|
|
557
|
+
log(` Is Fluid : ${creativeConfig.isFluid}`);
|
|
558
|
+
log(` Server : http://${host}:${port}`);
|
|
559
|
+
log(` Directory : ${userDir}`);
|
|
560
|
+
log(` Browsers : ${clients.size} connected`);
|
|
561
|
+
log(' ───────────────────────────────────');
|
|
562
|
+
log('');
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
case 'clear':
|
|
567
|
+
tui.clearLogs();
|
|
568
|
+
break;
|
|
569
|
+
|
|
570
|
+
case 'restart': {
|
|
571
|
+
log(' Restarting server...');
|
|
572
|
+
const { port, host } = creativeConfig.server;
|
|
573
|
+
server.close(() => {
|
|
574
|
+
server.listen(port, host, () => {
|
|
575
|
+
log(` Server restarted on http://${host}:${port}`);
|
|
576
|
+
broadcastReload();
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
case 'stop':
|
|
583
|
+
gracefulShutdown();
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
tui.init({
|
|
589
|
+
menuItems: getMainMenuItems(),
|
|
590
|
+
onSelect: handleSelect,
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// ============================================================
|
|
595
|
+
// Module Exports & Startup
|
|
596
|
+
// ============================================================
|
|
597
|
+
|
|
598
|
+
// Export functions for use by cli.js
|
|
599
|
+
module.exports = {
|
|
600
|
+
fetchCreativeConfig,
|
|
601
|
+
startServer: start
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
// Only auto-start if run directly (not required as module)
|
|
605
|
+
if (!isModule) {
|
|
606
|
+
start();
|
|
607
|
+
}
|
|
336
608
|
|
|
337
609
|
// Graceful shutdown
|
|
338
|
-
|
|
610
|
+
function gracefulShutdown() {
|
|
611
|
+
if (tui) {
|
|
612
|
+
tui.destroy();
|
|
613
|
+
}
|
|
339
614
|
console.log('\n Shutting down...');
|
|
340
615
|
watcher.close();
|
|
341
616
|
wss.close();
|
|
@@ -343,4 +618,6 @@ process.on('SIGINT', () => {
|
|
|
343
618
|
console.log(' Server stopped\n');
|
|
344
619
|
process.exit(0);
|
|
345
620
|
});
|
|
346
|
-
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
process.on('SIGINT', gracefulShutdown);
|
package/server/tui.js
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal UI Module
|
|
3
|
+
*
|
|
4
|
+
* Provides an interactive arrow-key menu pinned at the bottom of the terminal
|
|
5
|
+
* with a scrolling log area above it. Uses raw ANSI escape codes — zero dependencies.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const readline = require('readline');
|
|
9
|
+
|
|
10
|
+
// ANSI escape helpers
|
|
11
|
+
const ESC = '\x1B';
|
|
12
|
+
const CSI = `${ESC}[`;
|
|
13
|
+
|
|
14
|
+
const ansi = {
|
|
15
|
+
clearScreen: `${CSI}2J`,
|
|
16
|
+
clearLine: `${CSI}2K`,
|
|
17
|
+
cursorTo: (row, col) => `${CSI}${row};${col}H`,
|
|
18
|
+
cursorSave: `${ESC}7`,
|
|
19
|
+
cursorRestore: `${ESC}8`,
|
|
20
|
+
scrollRegion: (top, bottom) => `${CSI}${top};${bottom}r`,
|
|
21
|
+
resetScrollRegion: `${CSI}r`,
|
|
22
|
+
showCursor: `${CSI}?25h`,
|
|
23
|
+
hideCursor: `${CSI}?25l`,
|
|
24
|
+
bold: `${CSI}1m`,
|
|
25
|
+
dim: `${CSI}2m`,
|
|
26
|
+
cyan: `${CSI}36m`,
|
|
27
|
+
green: `${CSI}32m`,
|
|
28
|
+
yellow: `${CSI}33m`,
|
|
29
|
+
inverse: `${CSI}7m`,
|
|
30
|
+
reset: `${CSI}0m`,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
class TUI {
|
|
34
|
+
constructor() {
|
|
35
|
+
this.menuItems = [];
|
|
36
|
+
this.selectedIndex = 0;
|
|
37
|
+
this.onSelect = null;
|
|
38
|
+
this.destroyed = false;
|
|
39
|
+
this.menuHeight = 0; // computed from items + header + border lines
|
|
40
|
+
this._boundKeyHandler = this._handleKeypress.bind(this);
|
|
41
|
+
this._boundResize = this._handleResize.bind(this);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Initialize the TUI
|
|
46
|
+
* @param {Object} options
|
|
47
|
+
* @param {Array<{label: string, description?: string}>} options.menuItems
|
|
48
|
+
* @param {Function} options.onSelect - callback(item, index)
|
|
49
|
+
*/
|
|
50
|
+
init(options) {
|
|
51
|
+
this.menuItems = options.menuItems || [];
|
|
52
|
+
this.onSelect = options.onSelect || (() => {});
|
|
53
|
+
this.selectedIndex = 0;
|
|
54
|
+
this.menuHeight = this.menuItems.length + 3; // items + header + top/bottom borders
|
|
55
|
+
|
|
56
|
+
// Set raw mode for keypress detection
|
|
57
|
+
if (process.stdin.isTTY) {
|
|
58
|
+
process.stdin.setRawMode(true);
|
|
59
|
+
process.stdin.resume();
|
|
60
|
+
process.stdin.on('data', this._boundKeyHandler);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Handle terminal resize
|
|
64
|
+
process.stdout.on('resize', this._boundResize);
|
|
65
|
+
|
|
66
|
+
// Initial draw
|
|
67
|
+
this._setupScreen();
|
|
68
|
+
this._drawMenu();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Set up ANSI scrolling region (log area = top, menu = bottom)
|
|
73
|
+
*/
|
|
74
|
+
_setupScreen() {
|
|
75
|
+
const rows = process.stdout.rows || 24;
|
|
76
|
+
const logBottom = rows - this.menuHeight;
|
|
77
|
+
|
|
78
|
+
// Set scrolling region to the top portion only
|
|
79
|
+
process.stdout.write(ansi.scrollRegion(1, logBottom));
|
|
80
|
+
|
|
81
|
+
// Position cursor in the log area
|
|
82
|
+
process.stdout.write(ansi.cursorTo(logBottom, 1));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Handle terminal resize
|
|
87
|
+
*/
|
|
88
|
+
_handleResize() {
|
|
89
|
+
if (this.destroyed) return;
|
|
90
|
+
this._setupScreen();
|
|
91
|
+
this._drawMenu();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Handle raw keypress data
|
|
96
|
+
*/
|
|
97
|
+
_handleKeypress(data) {
|
|
98
|
+
if (this.destroyed) return;
|
|
99
|
+
|
|
100
|
+
const key = data.toString();
|
|
101
|
+
|
|
102
|
+
// Ctrl+C
|
|
103
|
+
if (key === '\x03') {
|
|
104
|
+
this.destroy();
|
|
105
|
+
process.emit('SIGINT');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Up arrow
|
|
110
|
+
if (key === `${CSI}A`) {
|
|
111
|
+
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
112
|
+
this._drawMenu();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Down arrow
|
|
117
|
+
if (key === `${CSI}B`) {
|
|
118
|
+
this.selectedIndex = Math.min(this.menuItems.length - 1, this.selectedIndex + 1);
|
|
119
|
+
this._drawMenu();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Enter
|
|
124
|
+
if (key === '\r' || key === '\n') {
|
|
125
|
+
const item = this.menuItems[this.selectedIndex];
|
|
126
|
+
if (item && this.onSelect) {
|
|
127
|
+
this.onSelect(item, this.selectedIndex);
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Number keys 1-9 for quick select
|
|
133
|
+
const num = parseInt(key, 10);
|
|
134
|
+
if (num >= 1 && num <= this.menuItems.length) {
|
|
135
|
+
this.selectedIndex = num - 1;
|
|
136
|
+
this._drawMenu();
|
|
137
|
+
const item = this.menuItems[this.selectedIndex];
|
|
138
|
+
if (item && this.onSelect) {
|
|
139
|
+
this.onSelect(item, this.selectedIndex);
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Draw the menu at the bottom of the terminal
|
|
147
|
+
*/
|
|
148
|
+
_drawMenu() {
|
|
149
|
+
if (this.destroyed) return;
|
|
150
|
+
|
|
151
|
+
const rows = process.stdout.rows || 24;
|
|
152
|
+
const cols = process.stdout.cols || 80;
|
|
153
|
+
const menuStartRow = rows - this.menuHeight + 1;
|
|
154
|
+
|
|
155
|
+
// Save cursor position (in log area)
|
|
156
|
+
let output = ansi.cursorSave;
|
|
157
|
+
output += ansi.hideCursor;
|
|
158
|
+
|
|
159
|
+
// Draw top border
|
|
160
|
+
output += ansi.cursorTo(menuStartRow, 1);
|
|
161
|
+
output += ansi.clearLine;
|
|
162
|
+
output += `${ansi.dim}${'─'.repeat(Math.min(cols, 60))}${ansi.reset}`;
|
|
163
|
+
|
|
164
|
+
// Draw header
|
|
165
|
+
output += ansi.cursorTo(menuStartRow + 1, 1);
|
|
166
|
+
output += ansi.clearLine;
|
|
167
|
+
output += `${ansi.bold} ↑↓ Navigate Enter Select 1-${this.menuItems.length} Quick Select${ansi.reset}`;
|
|
168
|
+
|
|
169
|
+
// Draw menu items
|
|
170
|
+
for (let i = 0; i < this.menuItems.length; i++) {
|
|
171
|
+
const row = menuStartRow + 2 + i;
|
|
172
|
+
const item = this.menuItems[i];
|
|
173
|
+
const isSelected = i === this.selectedIndex;
|
|
174
|
+
|
|
175
|
+
output += ansi.cursorTo(row, 1);
|
|
176
|
+
output += ansi.clearLine;
|
|
177
|
+
|
|
178
|
+
if (isSelected) {
|
|
179
|
+
output += `${ansi.cyan}${ansi.bold} ❯ ${item.label}${ansi.reset}`;
|
|
180
|
+
if (item.description) {
|
|
181
|
+
output += `${ansi.dim} ${item.description}${ansi.reset}`;
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
output += ` ${item.label}`;
|
|
185
|
+
if (item.description) {
|
|
186
|
+
output += `${ansi.dim} ${item.description}${ansi.reset}`;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Restore cursor position (back to log area)
|
|
192
|
+
output += ansi.cursorRestore;
|
|
193
|
+
output += ansi.showCursor;
|
|
194
|
+
|
|
195
|
+
process.stdout.write(output);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Update menu items (e.g., for sub-menus)
|
|
200
|
+
*/
|
|
201
|
+
updateMenu(items) {
|
|
202
|
+
const oldHeight = this.menuHeight;
|
|
203
|
+
this.menuItems = items;
|
|
204
|
+
this.selectedIndex = 0;
|
|
205
|
+
this.menuHeight = items.length + 3;
|
|
206
|
+
|
|
207
|
+
if (this.menuHeight !== oldHeight) {
|
|
208
|
+
// Clear old menu area
|
|
209
|
+
const rows = process.stdout.rows || 24;
|
|
210
|
+
const oldMenuStart = rows - oldHeight + 1;
|
|
211
|
+
let clear = '';
|
|
212
|
+
for (let i = oldMenuStart; i <= rows; i++) {
|
|
213
|
+
clear += ansi.cursorTo(i, 1) + ansi.clearLine;
|
|
214
|
+
}
|
|
215
|
+
process.stdout.write(clear);
|
|
216
|
+
|
|
217
|
+
// Reconfigure scroll region
|
|
218
|
+
this._setupScreen();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
this._drawMenu();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Add a log message to the scrolling log area
|
|
226
|
+
*/
|
|
227
|
+
log(message) {
|
|
228
|
+
if (this.destroyed) {
|
|
229
|
+
console.log(message);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const rows = process.stdout.rows || 24;
|
|
234
|
+
const logBottom = rows - this.menuHeight;
|
|
235
|
+
|
|
236
|
+
// Save cursor, move to bottom of log area, print message (scrolls within region)
|
|
237
|
+
let output = ansi.cursorSave;
|
|
238
|
+
output += ansi.cursorTo(logBottom, 1);
|
|
239
|
+
output += '\n' + message;
|
|
240
|
+
output += ansi.cursorRestore;
|
|
241
|
+
|
|
242
|
+
process.stdout.write(output);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Clear the log area
|
|
247
|
+
*/
|
|
248
|
+
clearLogs() {
|
|
249
|
+
const rows = process.stdout.rows || 24;
|
|
250
|
+
const logBottom = rows - this.menuHeight;
|
|
251
|
+
|
|
252
|
+
let output = '';
|
|
253
|
+
for (let i = 1; i <= logBottom; i++) {
|
|
254
|
+
output += ansi.cursorTo(i, 1) + ansi.clearLine;
|
|
255
|
+
}
|
|
256
|
+
output += ansi.cursorTo(1, 1);
|
|
257
|
+
process.stdout.write(output);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Destroy the TUI and restore terminal state
|
|
262
|
+
*/
|
|
263
|
+
destroy() {
|
|
264
|
+
if (this.destroyed) return;
|
|
265
|
+
this.destroyed = true;
|
|
266
|
+
|
|
267
|
+
// Remove listeners
|
|
268
|
+
process.stdin.removeListener('data', this._boundKeyHandler);
|
|
269
|
+
process.stdout.removeListener('resize', this._boundResize);
|
|
270
|
+
|
|
271
|
+
// Restore terminal
|
|
272
|
+
process.stdout.write(ansi.resetScrollRegion);
|
|
273
|
+
process.stdout.write(ansi.showCursor);
|
|
274
|
+
|
|
275
|
+
if (process.stdin.isTTY) {
|
|
276
|
+
process.stdin.setRawMode(false);
|
|
277
|
+
}
|
|
278
|
+
process.stdin.pause();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
module.exports = TUI;
|