rad-coder 1.0.0 → 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.
Files changed (3) hide show
  1. package/bin/cli.js +156 -27
  2. package/package.json +1 -1
  3. package/server/index.js +114 -26
package/bin/cli.js CHANGED
@@ -2,40 +2,169 @@
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, '..');
8
9
 
9
- // Get user's current working directory
10
- const userDir = process.cwd();
11
-
12
- // Files to copy to user's directory on first run
13
- const filesToCopy = [
14
- { template: 'custom.js', target: 'custom.js' },
15
- { template: 'AGENTS.md', target: 'AGENTS.md' }
16
- ];
17
-
18
- // Copy template files if they don't exist
19
- let filesCreated = false;
20
- filesToCopy.forEach(({ template, target }) => {
21
- const targetPath = path.join(userDir, target);
22
- if (!fs.existsSync(targetPath)) {
23
- const templatePath = path.join(packageRoot, 'templates', template);
10
+ // Extract creative ID from input (can be a direct ID or a preview URL)
11
+ function extractCreativeId(input) {
12
+ if (!input) return null;
13
+ const urlMatch = input.match(/creatives\/([a-f0-9]+)/i);
14
+ return urlMatch ? urlMatch[1] : input;
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
+
52
+ // Get creative ID from command line argument
53
+ const input = process.argv[2];
54
+ const creativeId = extractCreativeId(input);
55
+
56
+ if (!creativeId) {
57
+ console.log('Usage: npx rad-coder <creativeId or previewUrl>');
58
+ console.log('');
59
+ console.log('Examples:');
60
+ console.log(' npx rad-coder 697b80fcc6e904025f5147a0');
61
+ console.log(' npx rad-coder https://studio.responsiveads.com/creatives/697b80fcc6e904025f5147a0/preview');
62
+ process.exit(1);
63
+ }
64
+
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;
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
+ }
84
+ }
85
+
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');
24
157
  if (fs.existsSync(templatePath)) {
25
- fs.copyFileSync(templatePath, targetPath);
26
- console.log(` Created ${target}`);
27
- filesCreated = true;
158
+ fs.copyFileSync(templatePath, agentsMdPath);
159
+ console.log(' Created AGENTS.md');
28
160
  }
29
161
  }
30
- });
31
162
 
32
- if (filesCreated) {
33
- console.log('');
163
+ // Start the server with pre-fetched config
164
+ await startServer(config);
34
165
  }
35
166
 
36
- // Set environment variables for the server
37
- process.env.RAD_CODER_USER_DIR = userDir;
38
- process.env.RAD_CODER_PACKAGE_DIR = packageRoot;
39
-
40
- // Run the server
41
- require('../server/index.js');
167
+ main().catch((err) => {
168
+ console.error('Error:', err.message);
169
+ process.exit(1);
170
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rad-coder",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Development environment for testing ResponsiveAds creative custom JS with hot-reload",
5
5
  "bin": {
6
6
  "rad-coder": "./bin/cli.js"
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
- const input = process.argv[2];
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
- * Extract creative ID from URL or use directly
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
- }
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
- const creativeId = extractCreativeId(input);
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
 
@@ -174,9 +242,9 @@ async function fetchCreativeConfig(creativeId) {
174
242
  flowlineName: fl.name || 'Unknown',
175
243
  sizes: sizes,
176
244
  isFluid: fl.fullyFluid || false,
177
- adSource: '//publish.responsiveads.com/ads/',
178
- flSource: '//publish.responsiveads.com/flowlines/',
179
- radicalScript: 'https://publish.responsiveads.com/libs/radical.r8.min.js',
245
+ adSource: '//edit.responsiveads.com/ads/',
246
+ flSource: '//edit.responsiveads.com/flowlines/',
247
+ radicalScript: 'https://studio.responsiveads.com/js/libs/radical.min.js',
180
248
  server: {
181
249
  port: 3000,
182
250
  host: 'localhost'
@@ -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
- // Fetch creative config from studio
294
- creativeConfig = await fetchCreativeConfig(creativeId);
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
- start();
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', () => {