rad-coder 1.0.3 → 1.0.5

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/README.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  A development environment for testing ResponsiveAds creative custom JavaScript with hot-reload.
4
4
 
5
+
6
+
7
+ https://github.com/user-attachments/assets/ce7515c5-0920-4f02-b430-6af69fc2d44d
8
+
9
+
10
+
5
11
  ## Quick Start
6
12
 
7
13
  ```bash
@@ -33,14 +39,30 @@ npx rad-coder https://studio.responsiveads.com/creatives/697b80fcc6e904025f5147a
33
39
  ### Basic Usage
34
40
 
35
41
  ```bash
36
- # Create a new directory for your project
37
- mkdir my-creative
38
- cd my-creative
39
-
40
- # Start rad-coder with your creative ID
42
+ # Start rad-coder with your creative ID (creates a ./<id> folder)
41
43
  npx rad-coder 697b80fcc6e904025f5147a0
42
44
  ```
43
45
 
46
+ ### Continue Working
47
+
48
+ Next time, just `cd` into the project folder and run without arguments:
49
+
50
+ ```bash
51
+ cd 697b80fcc6e904025f5147a0
52
+ npx rad-coder
53
+ ```
54
+
55
+ The CLI auto-detects the creative from `.rad-coder.json` (or the folder name). Your local `custom.js` is used as-is — no prompts.
56
+
57
+ ### Options
58
+
59
+ ```bash
60
+ npx rad-coder <id> --reset # Overwrite local custom.js with the remote version
61
+ npx rad-coder <id> --fresh # Delete folder and start from scratch
62
+ npx rad-coder <id> --editor=cursor # Use a specific editor
63
+ npx rad-coder <id> --no-editor # Don't auto-open editor
64
+ ```
65
+
44
66
  ### With AI Assistants
45
67
 
46
68
  The generated `AGENTS.md` file contains instructions for AI coding assistants. When using VS Code with Copilot or other AI tools, they can read this file to understand:
@@ -52,10 +74,11 @@ The generated `AGENTS.md` file contains instructions for AI coding assistants. W
52
74
 
53
75
  ### Workflow
54
76
 
55
- 1. Run `npx rad-coder <creativeId>`
77
+ 1. Run `npx rad-coder <creativeId>` (first time — creates project folder)
56
78
  2. Edit `custom.js` in your favorite editor
57
79
  3. Save the file - browser auto-reloads
58
80
  4. See your changes applied to the creative instantly
81
+ 5. Next session: `cd <creativeId> && npx rad-coder` to continue
59
82
 
60
83
  ## Features
61
84
 
@@ -93,7 +116,7 @@ Instructions for developers who want to modify rad-coder itself.
93
116
 
94
117
  ```bash
95
118
  # Clone the repository
96
- git clone https://github.com/nicatronTg/rad-coder.git
119
+ git clone https://github.com/ResponsiveAds/rad-coder.git
97
120
  cd rad-coder
98
121
 
99
122
  # Install dependencies
package/bin/cli.js CHANGED
@@ -118,12 +118,18 @@ const args = process.argv.slice(2);
118
118
  let input = null;
119
119
  let editorFlag = null;
120
120
  let noEditor = false;
121
+ let resetFlag = false;
122
+ let freshFlag = false;
121
123
 
122
124
  for (const arg of args) {
123
125
  if (arg.startsWith('--editor=')) {
124
126
  editorFlag = arg.split('=')[1];
125
127
  } else if (arg === '--no-editor') {
126
128
  noEditor = true;
129
+ } else if (arg === '--reset') {
130
+ resetFlag = true;
131
+ } else if (arg === '--fresh') {
132
+ freshFlag = true;
127
133
  } else if (!arg.startsWith('--')) {
128
134
  input = arg;
129
135
  }
@@ -137,7 +143,33 @@ if (noEditor) {
137
143
  process.env.RAD_CODER_NO_EDITOR = '1';
138
144
  }
139
145
 
140
- const creativeId = extractCreativeId(input);
146
+ let creativeId = extractCreativeId(input);
147
+
148
+ // Auto-detect creative ID when no argument is given
149
+ if (!creativeId) {
150
+ const cwd = process.cwd();
151
+
152
+ // 1. Check for .rad-coder.json in current directory
153
+ const configPath = path.join(cwd, '.rad-coder.json');
154
+ if (fs.existsSync(configPath)) {
155
+ try {
156
+ const saved = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
157
+ if (saved.creativeId) {
158
+ creativeId = saved.creativeId;
159
+ console.log(`Detected creative from .rad-coder.json: ${creativeId}`);
160
+ }
161
+ } catch (_) { /* ignore malformed config */ }
162
+ }
163
+
164
+ // 2. Check if current directory name looks like a creative ID (24-char hex)
165
+ if (!creativeId) {
166
+ const dirName = path.basename(cwd);
167
+ if (/^[a-f0-9]{24}$/i.test(dirName)) {
168
+ creativeId = dirName;
169
+ console.log(`Detected creative from folder name: ${creativeId}`);
170
+ }
171
+ }
172
+ }
141
173
 
142
174
  if (!creativeId) {
143
175
  console.log('Usage: npx rad-coder <creativeId or previewUrl> [options]');
@@ -145,11 +177,16 @@ if (!creativeId) {
145
177
  console.log('Options:');
146
178
  console.log(' --editor=<cmd> Set code editor command (default: code)');
147
179
  console.log(' --no-editor Don\'t auto-open code editor');
180
+ console.log(' --reset Overwrite local custom.js with remote version');
181
+ console.log(' --fresh Delete local folder and start from scratch');
148
182
  console.log('');
149
183
  console.log('Examples:');
150
184
  console.log(' npx rad-coder 697b80fcc6e904025f5147a0');
151
185
  console.log(' npx rad-coder https://studio.responsiveads.com/creatives/697b80fcc6e904025f5147a0/preview');
152
186
  console.log(' npx rad-coder 697b80fcc6e904025f5147a0 --editor=cursor');
187
+ console.log('');
188
+ console.log('Continue working (from inside a project folder):');
189
+ console.log(' cd 697b80fcc6e904025f5147a0 && npx rad-coder');
153
190
  process.exit(1);
154
191
  }
155
192
 
@@ -158,22 +195,36 @@ async function main() {
158
195
  const cwd = process.cwd();
159
196
  const currentDirName = path.basename(cwd);
160
197
  let userDir;
198
+ let isNewProject = false;
199
+
200
+ // Check if a .rad-coder.json exists in cwd (we're inside a project folder)
201
+ const cwdConfigPath = path.join(cwd, '.rad-coder.json');
202
+ const inProjectDir = fs.existsSync(cwdConfigPath) || currentDirName === creativeId;
161
203
 
162
- if (currentDirName === creativeId) {
204
+ if (inProjectDir) {
163
205
  // Already in the correct folder
164
206
  userDir = cwd;
165
- console.log(`Using existing folder: ./${creativeId}`);
207
+ console.log(`Using existing project: ${userDir}`);
166
208
  } else {
167
209
  // Create or use a folder with the creative ID
168
210
  userDir = path.join(cwd, creativeId);
169
211
  if (!fs.existsSync(userDir)) {
170
212
  fs.mkdirSync(userDir);
213
+ isNewProject = true;
171
214
  console.log(`Created folder: ./${creativeId}`);
172
215
  } else {
173
216
  console.log(`Using existing folder: ./${creativeId}`);
174
217
  }
175
218
  }
176
219
 
220
+ // Handle --fresh: delete folder and recreate
221
+ if (freshFlag && fs.existsSync(userDir)) {
222
+ fs.rmSync(userDir, { recursive: true, force: true });
223
+ fs.mkdirSync(userDir);
224
+ isNewProject = true;
225
+ console.log(` Fresh start: deleted and recreated ./${creativeId}`);
226
+ }
227
+
177
228
  // Set environment variables for the server
178
229
  process.env.RAD_CODER_USER_DIR = userDir;
179
230
  process.env.RAD_CODER_PACKAGE_DIR = packageRoot;
@@ -189,56 +240,40 @@ async function main() {
189
240
  const hasCreativeCustomJs = config.customjs && config.customjs.trim().length > 0;
190
241
 
191
242
  // 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
- }
243
+ if (resetFlag && hasCreativeCustomJs) {
244
+ // --reset: overwrite local custom.js with remote version
245
+ fs.writeFileSync(customJsPath, config.customjs, 'utf-8');
246
+ console.log(' Reset custom.js (from creative)');
247
+ } else if (customJsExists) {
248
+ // custom.js already exists — use it silently (zero-friction repeat run)
249
+ console.log(' Using existing custom.js');
250
+ } else if (hasCreativeCustomJs) {
251
+ // First run with remote customjs available — prompt
252
+ const choice = await promptUser(
253
+ 'Found customJS in this creative. What would you like to use?',
254
+ [
255
+ 'Use customJS from the creative (recommended)',
256
+ 'Start with blank template'
257
+ ]
258
+ );
259
+
260
+ if (choice === 0) {
261
+ fs.writeFileSync(customJsPath, config.customjs, 'utf-8');
262
+ console.log(' Created custom.js (from creative)');
215
263
  } 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
264
  const templatePath = path.join(packageRoot, 'templates', 'custom.js');
237
265
  if (fs.existsSync(templatePath)) {
238
266
  fs.copyFileSync(templatePath, customJsPath);
239
267
  console.log(' Created custom.js (from template)');
240
268
  }
241
269
  }
270
+ } else {
271
+ // No customjs in creative — use template if custom.js doesn't exist
272
+ const templatePath = path.join(packageRoot, 'templates', 'custom.js');
273
+ if (fs.existsSync(templatePath)) {
274
+ fs.copyFileSync(templatePath, customJsPath);
275
+ console.log(' Created custom.js (from template)');
276
+ }
242
277
  }
243
278
 
244
279
  // Copy AGENTS.md if it doesn't exist
@@ -251,6 +286,22 @@ async function main() {
251
286
  }
252
287
  }
253
288
 
289
+ // Save .rad-coder.json config for future no-arg runs
290
+ const radCoderConfigPath = path.join(userDir, '.rad-coder.json');
291
+ const savedConfig = {
292
+ creativeId: config.creativeId,
293
+ flowlineId: config.flowlineId,
294
+ flowlineName: config.flowlineName,
295
+ createdAt: fs.existsSync(radCoderConfigPath)
296
+ ? JSON.parse(fs.readFileSync(radCoderConfigPath, 'utf-8')).createdAt
297
+ : new Date().toISOString(),
298
+ updatedAt: new Date().toISOString()
299
+ };
300
+ fs.writeFileSync(radCoderConfigPath, JSON.stringify(savedConfig, null, 2) + '\n', 'utf-8');
301
+ if (isNewProject) {
302
+ console.log(' Created .rad-coder.json');
303
+ }
304
+
254
305
  // Start the server with pre-fetched config
255
306
  await startServer(config);
256
307
  }
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "rad-coder",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Development environment for testing ResponsiveAds creative custom JS with hot-reload",
5
5
  "bin": {
6
- "rad-coder": "./bin/cli.js"
6
+ "rad-coder": "bin/cli.js"
7
7
  },
8
8
  "scripts": {
9
9
  "dev": "node bin/cli.js",
@@ -26,7 +26,7 @@
26
26
  ],
27
27
  "repository": {
28
28
  "type": "git",
29
- "url": "https://github.com/ResponsiveAds/rad-coder"
29
+ "url": "git+https://github.com/ResponsiveAds/rad-coder.git"
30
30
  },
31
31
  "author": "ResponsiveAds",
32
32
  "license": "MIT",
package/public/test.html CHANGED
@@ -87,23 +87,8 @@
87
87
  }
88
88
 
89
89
  .ad-container {
90
- padding: 20px;
91
- display: flex;
92
- justify-content: center;
93
- align-items: flex-start;
94
- }
95
-
96
- .ad-wrapper {
97
- background: #fff;
98
- border-radius: 8px;
99
- overflow: hidden;
100
- box-shadow: 0 10px 40px rgba(0,0,0,0.3);
101
90
  }
102
91
 
103
- .inner-content {
104
- width: 100%;
105
- min-height: 300px;
106
- }
107
92
 
108
93
  /* Toast notification for reload */
109
94
  .toast {
@@ -180,6 +165,8 @@
180
165
  document.getElementById('creative-id').textContent = 'Loading...';
181
166
 
182
167
  const config = await fetchConfigWithRetry();
168
+ //not sure why we get URL that does not work https://edit.responsiveads.com/flowlines
169
+ config.flSource = 'https://s3-eu-west-1.amazonaws.com/publish.responsiveads.com/flowlines/';
183
170
 
184
171
  // Update UI
185
172
  document.getElementById('creative-id').textContent = `ID: ${config.creativeId}`;
@@ -231,7 +218,7 @@
231
218
  tracking: false,
232
219
  isFluid: config.isFluid,
233
220
  screenshot: false,
234
- crossOrigin: true,
221
+ crossOrigin: false,
235
222
  flSource: config.flSource,
236
223
  config: {
237
224
  _default: {
package/server/index.js CHANGED
@@ -315,7 +315,15 @@ async function fetchCreativeConfig(creativeId) {
315
315
  // ============================================================
316
316
 
317
317
  const app = express();
318
- const server = http.createServer(app);
318
+ const server = http.createServer({ maxHeaderSize: 65536 }, app);
319
+
320
+ // Track active HTTP sockets so shutdown can force-close stragglers.
321
+ const activeSockets = new Set();
322
+ server.on('connection', (socket) => {
323
+ activeSockets.add(socket);
324
+ socket.on('close', () => activeSockets.delete(socket));
325
+ });
326
+ let isShuttingDown = false;
319
327
 
320
328
  // WebSocket server for hot-reload
321
329
  const wss = new WebSocketServer({ server });
@@ -608,13 +616,53 @@ if (!isModule) {
608
616
 
609
617
  // Graceful shutdown
610
618
  function gracefulShutdown() {
619
+ if (isShuttingDown) {
620
+ console.log('\n Force exiting...\n');
621
+ process.exit(130);
622
+ }
623
+ isShuttingDown = true;
624
+
611
625
  if (tui) {
612
626
  tui.destroy();
613
627
  }
614
628
  console.log('\n Shutting down...');
615
- watcher.close();
616
- wss.close();
617
- server.close(() => {
629
+ console.log(' Press Ctrl+C again to force exit');
630
+
631
+ // Ensure websocket clients do not keep the process alive.
632
+ clients.forEach((client) => {
633
+ try {
634
+ client.terminate();
635
+ } catch (_) {
636
+ // Ignore client termination failures during shutdown.
637
+ }
638
+ });
639
+
640
+ if (typeof server.closeIdleConnections === 'function') {
641
+ server.closeIdleConnections();
642
+ }
643
+
644
+ if (typeof server.closeAllConnections === 'function') {
645
+ server.closeAllConnections();
646
+ }
647
+
648
+ const forceTimer = setTimeout(() => {
649
+ activeSockets.forEach((socket) => {
650
+ try {
651
+ socket.destroy();
652
+ } catch (_) {
653
+ // Ignore socket destroy failures during forced shutdown.
654
+ }
655
+ });
656
+ console.log(' Forced shutdown: closed remaining connections');
657
+ process.exit(0);
658
+ }, 2000);
659
+
660
+ Promise.allSettled([
661
+ Promise.resolve().then(() => watcher.close()),
662
+ new Promise((resolve) => wss.close(resolve)),
663
+ new Promise((resolve) => server.close(resolve)),
664
+ ]).finally(() => {
665
+ clearTimeout(forceTimer);
618
666
  console.log(' Server stopped\n');
619
667
  process.exit(0);
620
668
  });
@@ -4,6 +4,9 @@ You are an agent writing only JS for a responsive creative. The creative was bui
4
4
 
5
5
  To do this you can only edit the `custom.js` file in this directory. When you edit and save this file the creative will be automatically loaded on the test page: http://localhost:3000/test.html. The code you wrote in `custom.js` will be applied to the creative.
6
6
 
7
+ Use the http://localhost:3000/test.html URL to open creative in the browser. Inspect the HTML dom so that you can use IDs from elements inside the custom.js code. Alo use the browser to test the code and make sure there are no console.log errors.
8
+
9
+
7
10
  Use modern JS standards and code practices.
8
11
 
9
12
  We can use custom in situations when we want to add extra interactivity to our responsive creative. Use the Radical API to access elements added from the editor, update their behavior, and add custom functionalities to your ad.
@@ -16,6 +19,32 @@ You can use all available JavaScript functions to manipulate element position an
16
19
 
17
20
  This document outlines the specific implementation patterns and lifecycle hooks for the Radical API. Use this guide to programmatically control elements, manage dynamic data (DCO), and handle cross-window interactions in ResponsiveAds creatives.
18
21
 
22
+ Use this as a good starting point always call functions from `!rad.getMergedContent().inEditor` check to prevent messing up the editor code.
23
+
24
+ ```
25
+ var rad = Radical.getAdByWindow(window);
26
+ var container = rad.getContainer();
27
+
28
+ var inScreenshot = window.location.href.indexOf('preview?screenshot=1') > -1 ? true : false;
29
+
30
+
31
+ if (!rad.getMergedContent().inEditor) {
32
+ rad.onLoad(onAdLoaded);
33
+ rad.onBeforeRender(onBeforeRender);
34
+ rad.onRender(onAdRender);
35
+
36
+ }
37
+ function onAdLoaded() {}
38
+ function onBeforeRender(arg) {
39
+ console.log('onBeforeRender', arg);
40
+ }
41
+ function onAdRender() {
42
+ console.log('onAdRender');
43
+ const el = rad.getElementById('a2');
44
+ console.log(el);
45
+ }
46
+ ```
47
+
19
48
  ## 1. Initializing the Controller
20
49
 
21
50
  Every script must first reference the ad instance and its container.