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 +24 -7
- package/bin/cli.js +97 -46
- package/package.json +1 -1
- package/server/index.js +52 -4
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
|
-
#
|
|
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/
|
|
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
|
-
|
|
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 (
|
|
204
|
+
if (inProjectDir) {
|
|
163
205
|
// Already in the correct folder
|
|
164
206
|
userDir = cwd;
|
|
165
|
-
console.log(`Using existing
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
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
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
});
|