rad-coder 1.0.4 → 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
@@ -39,14 +39,30 @@ npx rad-coder https://studio.responsiveads.com/creatives/697b80fcc6e904025f5147a
39
39
  ### Basic Usage
40
40
 
41
41
  ```bash
42
- # Create a new directory for your project
43
- mkdir my-creative
44
- cd my-creative
45
-
46
- # Start rad-coder with your creative ID
42
+ # Start rad-coder with your creative ID (creates a ./<id> folder)
47
43
  npx rad-coder 697b80fcc6e904025f5147a0
48
44
  ```
49
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
+
50
66
  ### With AI Assistants
51
67
 
52
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:
@@ -58,10 +74,11 @@ The generated `AGENTS.md` file contains instructions for AI coding assistants. W
58
74
 
59
75
  ### Workflow
60
76
 
61
- 1. Run `npx rad-coder <creativeId>`
77
+ 1. Run `npx rad-coder <creativeId>` (first time — creates project folder)
62
78
  2. Edit `custom.js` in your favorite editor
63
79
  3. Save the file - browser auto-reloads
64
80
  4. See your changes applied to the creative instantly
81
+ 5. Next session: `cd <creativeId> && npx rad-coder` to continue
65
82
 
66
83
  ## Features
67
84
 
@@ -99,7 +116,7 @@ Instructions for developers who want to modify rad-coder itself.
99
116
 
100
117
  ```bash
101
118
  # Clone the repository
102
- git clone https://github.com/nicatronTg/rad-coder.git
119
+ git clone https://github.com/ResponsiveAds/rad-coder.git
103
120
  cd rad-coder
104
121
 
105
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,6 +1,6 @@
1
1
  {
2
2
  "name": "rad-coder",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
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
@@ -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
  });