rad-coder 1.0.1 → 1.0.2
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 +133 -41
- package/package.json +1 -1
- package/server/index.js +111 -23
package/bin/cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const fs = require('fs');
|
|
5
|
+
const readline = require('readline');
|
|
5
6
|
|
|
6
7
|
// Get the package root directory
|
|
7
8
|
const packageRoot = path.join(__dirname, '..');
|
|
@@ -13,6 +14,41 @@ function extractCreativeId(input) {
|
|
|
13
14
|
return urlMatch ? urlMatch[1] : input;
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Prompt user with a question and choices
|
|
19
|
+
* @param {string} question - The question to ask
|
|
20
|
+
* @param {string[]} choices - Array of choices
|
|
21
|
+
* @returns {Promise<number>} - The index of the selected choice (0-based)
|
|
22
|
+
*/
|
|
23
|
+
function promptUser(question, choices) {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
const rl = readline.createInterface({
|
|
26
|
+
input: process.stdin,
|
|
27
|
+
output: process.stdout
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
console.log(`\n${question}`);
|
|
31
|
+
choices.forEach((choice, index) => {
|
|
32
|
+
console.log(` [${index + 1}] ${choice}`);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const ask = () => {
|
|
36
|
+
rl.question('\nChoice (enter number): ', (answer) => {
|
|
37
|
+
const num = parseInt(answer, 10);
|
|
38
|
+
if (num >= 1 && num <= choices.length) {
|
|
39
|
+
rl.close();
|
|
40
|
+
resolve(num - 1);
|
|
41
|
+
} else {
|
|
42
|
+
console.log(`Please enter a number between 1 and ${choices.length}`);
|
|
43
|
+
ask();
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
ask();
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
16
52
|
// Get creative ID from command line argument
|
|
17
53
|
const input = process.argv[2];
|
|
18
54
|
const creativeId = extractCreativeId(input);
|
|
@@ -26,53 +62,109 @@ if (!creativeId) {
|
|
|
26
62
|
process.exit(1);
|
|
27
63
|
}
|
|
28
64
|
|
|
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 {
|
|
65
|
+
async function main() {
|
|
66
|
+
// Determine the target directory
|
|
67
|
+
const cwd = process.cwd();
|
|
68
|
+
const currentDirName = path.basename(cwd);
|
|
69
|
+
let userDir;
|
|
70
|
+
|
|
71
|
+
if (currentDirName === creativeId) {
|
|
72
|
+
// Already in the correct folder
|
|
73
|
+
userDir = cwd;
|
|
45
74
|
console.log(`Using existing folder: ./${creativeId}`);
|
|
75
|
+
} else {
|
|
76
|
+
// Create or use a folder with the creative ID
|
|
77
|
+
userDir = path.join(cwd, creativeId);
|
|
78
|
+
if (!fs.existsSync(userDir)) {
|
|
79
|
+
fs.mkdirSync(userDir);
|
|
80
|
+
console.log(`Created folder: ./${creativeId}`);
|
|
81
|
+
} else {
|
|
82
|
+
console.log(`Using existing folder: ./${creativeId}`);
|
|
83
|
+
}
|
|
46
84
|
}
|
|
47
|
-
}
|
|
48
85
|
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
86
|
+
// Set environment variables for the server
|
|
87
|
+
process.env.RAD_CODER_USER_DIR = userDir;
|
|
88
|
+
process.env.RAD_CODER_PACKAGE_DIR = packageRoot;
|
|
89
|
+
|
|
90
|
+
// Import server module to fetch creative config
|
|
91
|
+
const { fetchCreativeConfig, startServer } = require('../server/index.js');
|
|
92
|
+
|
|
93
|
+
// Fetch creative config first to check for customjs
|
|
94
|
+
const config = await fetchCreativeConfig(creativeId);
|
|
95
|
+
|
|
96
|
+
const customJsPath = path.join(userDir, 'custom.js');
|
|
97
|
+
const customJsExists = fs.existsSync(customJsPath);
|
|
98
|
+
const hasCreativeCustomJs = config.customjs && config.customjs.trim().length > 0;
|
|
99
|
+
|
|
100
|
+
// Handle custom.js file creation/update
|
|
101
|
+
if (hasCreativeCustomJs) {
|
|
102
|
+
if (!customJsExists) {
|
|
103
|
+
// custom.js doesn't exist - ask user what to use
|
|
104
|
+
const choice = await promptUser(
|
|
105
|
+
'Found customJS in this creative. What would you like to use?',
|
|
106
|
+
[
|
|
107
|
+
'Use customJS from the creative (recommended)',
|
|
108
|
+
'Start with blank template'
|
|
109
|
+
]
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
if (choice === 0) {
|
|
113
|
+
// Use customjs from creative
|
|
114
|
+
fs.writeFileSync(customJsPath, config.customjs, 'utf-8');
|
|
115
|
+
console.log(' Created custom.js (from creative)');
|
|
116
|
+
} else {
|
|
117
|
+
// Use template
|
|
118
|
+
const templatePath = path.join(packageRoot, 'templates', 'custom.js');
|
|
119
|
+
if (fs.existsSync(templatePath)) {
|
|
120
|
+
fs.copyFileSync(templatePath, customJsPath);
|
|
121
|
+
console.log(' Created custom.js (from template)');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
// custom.js exists - ask user if they want to overwrite
|
|
126
|
+
const choice = await promptUser(
|
|
127
|
+
'Found customJS in this creative. Your custom.js already exists.',
|
|
128
|
+
[
|
|
129
|
+
'Keep existing custom.js',
|
|
130
|
+
'Overwrite with customJS from creative'
|
|
131
|
+
]
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
if (choice === 1) {
|
|
135
|
+
// Overwrite with creative's customjs
|
|
136
|
+
fs.writeFileSync(customJsPath, config.customjs, 'utf-8');
|
|
137
|
+
console.log(' Overwrote custom.js with creative\'s customJS');
|
|
138
|
+
} else {
|
|
139
|
+
console.log(' Keeping existing custom.js');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
// No customjs in creative - use template if custom.js doesn't exist
|
|
144
|
+
if (!customJsExists) {
|
|
145
|
+
const templatePath = path.join(packageRoot, 'templates', 'custom.js');
|
|
146
|
+
if (fs.existsSync(templatePath)) {
|
|
147
|
+
fs.copyFileSync(templatePath, customJsPath);
|
|
148
|
+
console.log(' Created custom.js (from template)');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Copy AGENTS.md if it doesn't exist
|
|
154
|
+
const agentsMdPath = path.join(userDir, 'AGENTS.md');
|
|
155
|
+
if (!fs.existsSync(agentsMdPath)) {
|
|
156
|
+
const templatePath = path.join(packageRoot, 'templates', 'AGENTS.md');
|
|
61
157
|
if (fs.existsSync(templatePath)) {
|
|
62
|
-
fs.copyFileSync(templatePath,
|
|
63
|
-
console.log(
|
|
64
|
-
filesCreated = true;
|
|
158
|
+
fs.copyFileSync(templatePath, agentsMdPath);
|
|
159
|
+
console.log(' Created AGENTS.md');
|
|
65
160
|
}
|
|
66
161
|
}
|
|
67
|
-
});
|
|
68
162
|
|
|
69
|
-
|
|
70
|
-
|
|
163
|
+
// Start the server with pre-fetched config
|
|
164
|
+
await startServer(config);
|
|
71
165
|
}
|
|
72
166
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
process.
|
|
76
|
-
|
|
77
|
-
// Run the server
|
|
78
|
-
require('../server/index.js');
|
|
167
|
+
main().catch((err) => {
|
|
168
|
+
console.error('Error:', err.message);
|
|
169
|
+
process.exit(1);
|
|
170
|
+
});
|
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -19,26 +19,33 @@ const packageDir = process.env.RAD_CODER_PACKAGE_DIR || path.join(__dirname, '..
|
|
|
19
19
|
// CLI Argument Parsing
|
|
20
20
|
// ============================================================
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (!input) {
|
|
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
|
-
}
|
|
22
|
+
// Check if we're being required as a module (from cli.js) or run directly
|
|
23
|
+
const isModule = require.main !== module;
|
|
31
24
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
25
|
+
let creativeId = null;
|
|
26
|
+
|
|
27
|
+
if (!isModule) {
|
|
28
|
+
const input = process.argv[2];
|
|
29
|
+
|
|
30
|
+
if (!input) {
|
|
31
|
+
console.error('\n Usage: npx rad-coder <creativeId or previewUrl>\n');
|
|
32
|
+
console.error(' Examples:');
|
|
33
|
+
console.error(' npx rad-coder 697b80fcc6e904025f5147a0');
|
|
34
|
+
console.error(' npx rad-coder https://studio.responsiveads.com/creatives/697b80fcc6e904025f5147a0/preview\n');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Extract creative ID from URL or use directly
|
|
40
|
+
*/
|
|
41
|
+
function extractCreativeIdLocal(input) {
|
|
42
|
+
// If it's a URL, extract the ID
|
|
43
|
+
const urlMatch = input.match(/creatives\/([a-f0-9]+)/i);
|
|
44
|
+
return urlMatch ? urlMatch[1] : input;
|
|
45
|
+
}
|
|
40
46
|
|
|
41
|
-
|
|
47
|
+
creativeId = extractCreativeIdLocal(input);
|
|
48
|
+
}
|
|
42
49
|
|
|
43
50
|
// ============================================================
|
|
44
51
|
// Fetch Creative Config from Studio Preview Page
|
|
@@ -46,6 +53,60 @@ const creativeId = extractCreativeId(input);
|
|
|
46
53
|
|
|
47
54
|
let creativeConfig = null;
|
|
48
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Extract a JSON object from HTML using balanced bracket parsing
|
|
58
|
+
* @param {string} html - The HTML content
|
|
59
|
+
* @param {string} startMarker - The marker to find (e.g., 'window.creative = ')
|
|
60
|
+
* @returns {object|null} - Parsed JSON object or null
|
|
61
|
+
*/
|
|
62
|
+
function extractJsonObject(html, startMarker) {
|
|
63
|
+
const startIdx = html.indexOf(startMarker);
|
|
64
|
+
if (startIdx === -1) return null;
|
|
65
|
+
|
|
66
|
+
const jsonStart = startIdx + startMarker.length;
|
|
67
|
+
let braceCount = 0;
|
|
68
|
+
let inString = false;
|
|
69
|
+
let escapeNext = false;
|
|
70
|
+
let endIdx = jsonStart;
|
|
71
|
+
|
|
72
|
+
for (let i = jsonStart; i < html.length; i++) {
|
|
73
|
+
const char = html[i];
|
|
74
|
+
|
|
75
|
+
if (escapeNext) {
|
|
76
|
+
escapeNext = false;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (char === '\\') {
|
|
81
|
+
escapeNext = true;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (char === '"' && !inString) {
|
|
86
|
+
inString = true;
|
|
87
|
+
} else if (char === '"' && inString) {
|
|
88
|
+
inString = false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!inString) {
|
|
92
|
+
if (char === '{') braceCount++;
|
|
93
|
+
if (char === '}') braceCount--;
|
|
94
|
+
|
|
95
|
+
if (braceCount === 0 && char === '}') {
|
|
96
|
+
endIdx = i + 1;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const jsonStr = html.substring(jsonStart, endIdx);
|
|
104
|
+
return JSON.parse(jsonStr);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
49
110
|
/**
|
|
50
111
|
* Fetch and parse creative configuration from studio preview page
|
|
51
112
|
*/
|
|
@@ -67,6 +128,13 @@ async function fetchCreativeConfig(creativeId) {
|
|
|
67
128
|
const creativeIdMatch = html.match(/window\.creativeId\s*=\s*['"]([^'"]+)['"]/);
|
|
68
129
|
const extractedCreativeId = creativeIdMatch ? creativeIdMatch[1] : creativeId;
|
|
69
130
|
|
|
131
|
+
// Extract window.creative object to get customjs
|
|
132
|
+
let customjs = null;
|
|
133
|
+
const creativeObj = extractJsonObject(html, 'window.creative = ');
|
|
134
|
+
if (creativeObj && creativeObj.config && creativeObj.config.customjs) {
|
|
135
|
+
customjs = creativeObj.config.customjs;
|
|
136
|
+
}
|
|
137
|
+
|
|
70
138
|
// Extract flowlines - try multiple patterns
|
|
71
139
|
let flowlines;
|
|
72
140
|
|
|
@@ -187,7 +255,9 @@ async function fetchCreativeConfig(creativeId) {
|
|
|
187
255
|
name: f.name,
|
|
188
256
|
sizes: f.flowline?.sizes || [],
|
|
189
257
|
isFluid: f.fullyFluid
|
|
190
|
-
}))
|
|
258
|
+
})),
|
|
259
|
+
// Custom JS from the creative (if available)
|
|
260
|
+
customjs: customjs
|
|
191
261
|
};
|
|
192
262
|
|
|
193
263
|
} catch (error) {
|
|
@@ -285,13 +355,17 @@ watcher.on('error', (error) => {
|
|
|
285
355
|
// Start Server
|
|
286
356
|
// ============================================================
|
|
287
357
|
|
|
288
|
-
async function start() {
|
|
358
|
+
async function start(prefetchedConfig = null) {
|
|
289
359
|
console.log('\n========================================');
|
|
290
360
|
console.log(' RAD Coder - ResponsiveAds Creative Tester');
|
|
291
361
|
console.log('========================================\n');
|
|
292
362
|
|
|
293
|
-
//
|
|
294
|
-
|
|
363
|
+
// Use pre-fetched config if provided, otherwise fetch it
|
|
364
|
+
if (prefetchedConfig) {
|
|
365
|
+
creativeConfig = prefetchedConfig;
|
|
366
|
+
} else {
|
|
367
|
+
creativeConfig = await fetchCreativeConfig(creativeId);
|
|
368
|
+
}
|
|
295
369
|
|
|
296
370
|
console.log(' Creative Config:');
|
|
297
371
|
console.log(` - Creative ID: ${creativeConfig.creativeId}`);
|
|
@@ -299,6 +373,7 @@ async function start() {
|
|
|
299
373
|
console.log(` - Flowline ID: ${creativeConfig.flowlineId}`);
|
|
300
374
|
console.log(` - Sizes: ${creativeConfig.sizes.join(', ')}`);
|
|
301
375
|
console.log(` - Is Fluid: ${creativeConfig.isFluid}`);
|
|
376
|
+
console.log(` - Has CustomJS: ${creativeConfig.customjs ? 'Yes' : 'No'}`);
|
|
302
377
|
|
|
303
378
|
if (creativeConfig.allFlowlines.length > 1) {
|
|
304
379
|
console.log(`\n Available Flowlines (${creativeConfig.allFlowlines.length}):`);
|
|
@@ -332,7 +407,20 @@ async function start() {
|
|
|
332
407
|
});
|
|
333
408
|
}
|
|
334
409
|
|
|
335
|
-
|
|
410
|
+
// ============================================================
|
|
411
|
+
// Module Exports & Startup
|
|
412
|
+
// ============================================================
|
|
413
|
+
|
|
414
|
+
// Export functions for use by cli.js
|
|
415
|
+
module.exports = {
|
|
416
|
+
fetchCreativeConfig,
|
|
417
|
+
startServer: start
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
// Only auto-start if run directly (not required as module)
|
|
421
|
+
if (!isModule) {
|
|
422
|
+
start();
|
|
423
|
+
}
|
|
336
424
|
|
|
337
425
|
// Graceful shutdown
|
|
338
426
|
process.on('SIGINT', () => {
|