freertc 0.1.0
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 +246 -0
- package/bin/freertc.mjs +106 -0
- package/package.json +68 -0
- package/public/app.js +2851 -0
- package/public/index.html +821 -0
- package/scripts/d1-schema.sql +44 -0
- package/scripts/dev-server.mjs +129 -0
- package/scripts/non-cloudflare-server.mjs +427 -0
- package/scripts/postinstall-message.mjs +19 -0
- package/scripts/project-bootstrap.mjs +113 -0
- package/scripts/wrangler-install-wizard.mjs +697 -0
- package/src/index.js +690 -0
- package/wrangler.template.jsonc +71 -0
- package/wrangler.workers-dev.jsonc +19 -0
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { createInterface } from 'node:readline/promises';
|
|
8
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
9
|
+
import { PACKAGE_ROOT, ensureProjectFiles, resolveProjectRoot, resolveWranglerCommand } from './project-bootstrap.mjs';
|
|
10
|
+
|
|
11
|
+
const CARGO_BIN = path.join(os.homedir(), '.cargo', 'bin');
|
|
12
|
+
const PATH_WITH_CARGO = `${CARGO_BIN}${path.delimiter}${process.env.PATH || ''}`;
|
|
13
|
+
const WASM_TARGET = 'wasm32-unknown-unknown';
|
|
14
|
+
const ROOT = resolveProjectRoot(process.cwd());
|
|
15
|
+
const WRANGLER_CONFIG = path.join(ROOT, 'wrangler.jsonc');
|
|
16
|
+
const WRANGLER_TEMPLATE = path.join(ROOT, 'wrangler.template.jsonc');
|
|
17
|
+
const D1_SCHEMA_FILE = path.join(ROOT, 'scripts', 'd1-schema.sql');
|
|
18
|
+
|
|
19
|
+
function readProjectName(dir) {
|
|
20
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
21
|
+
if (!fs.existsSync(pkgPath)) return 'worker-app';
|
|
22
|
+
try {
|
|
23
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
24
|
+
return (pkg?.name && String(pkg.name).trim()) || 'worker-app';
|
|
25
|
+
} catch {
|
|
26
|
+
return 'worker-app';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const PROJECT_NAME = readProjectName(ROOT);
|
|
31
|
+
|
|
32
|
+
function run(command, args, { allowFailure = false } = {}) {
|
|
33
|
+
const result = spawnSync(command, args, {
|
|
34
|
+
stdio: 'inherit',
|
|
35
|
+
cwd: ROOT,
|
|
36
|
+
env: { ...process.env, PATH: PATH_WITH_CARGO }
|
|
37
|
+
});
|
|
38
|
+
if (result.status !== 0 && !allowFailure) {
|
|
39
|
+
throw new Error(`Command failed: ${command} ${args.join(' ')}`);
|
|
40
|
+
}
|
|
41
|
+
return result.status === 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function runCapture(command, args, { allowFailure = false } = {}) {
|
|
45
|
+
const result = spawnSync(command, args, {
|
|
46
|
+
stdio: 'pipe',
|
|
47
|
+
encoding: 'utf8',
|
|
48
|
+
cwd: ROOT,
|
|
49
|
+
env: { ...process.env, PATH: PATH_WITH_CARGO }
|
|
50
|
+
});
|
|
51
|
+
if (result.status !== 0 && !allowFailure) {
|
|
52
|
+
const stderr = (result.stderr || '').trim();
|
|
53
|
+
throw new Error(`Command failed: ${command} ${args.join(' ')}${stderr ? `\n${stderr}` : ''}`);
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
ok: result.status === 0,
|
|
57
|
+
status: result.status,
|
|
58
|
+
stdout: result.stdout || '',
|
|
59
|
+
stderr: result.stderr || ''
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function commandExists(command, args = ['--version']) {
|
|
64
|
+
const result = spawnSync(command, args, {
|
|
65
|
+
cwd: ROOT,
|
|
66
|
+
stdio: 'ignore',
|
|
67
|
+
env: { ...process.env, PATH: PATH_WITH_CARGO }
|
|
68
|
+
});
|
|
69
|
+
return result.status === 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function hasWasmTargetInstalled() {
|
|
73
|
+
const sysroot = runCapture('rustc', ['--print', 'sysroot'], { allowFailure: true });
|
|
74
|
+
if (!sysroot.ok) return false;
|
|
75
|
+
const sysrootPath = (sysroot.stdout || '').trim();
|
|
76
|
+
if (!sysrootPath) return false;
|
|
77
|
+
|
|
78
|
+
const targetDir = path.join(sysrootPath, 'lib', 'rustlib', WASM_TARGET);
|
|
79
|
+
return fs.existsSync(targetDir);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function ensureWorkerBuild() {
|
|
83
|
+
if (commandExists('worker-build')) return;
|
|
84
|
+
|
|
85
|
+
if (!commandExists('cargo')) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
'Rust build required by wrangler config but Cargo is missing. Install Rust from https://rustup.rs or switch wrangler main/build to JS (src/index.js).'
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log('Installing worker-build via Cargo...');
|
|
92
|
+
run('cargo', ['install', 'worker-build']);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function ensureWasmTarget() {
|
|
96
|
+
if (!commandExists('rustc')) {
|
|
97
|
+
throw new Error('Rust build required by wrangler config but rustc is missing. Install Rust from https://rustup.rs.');
|
|
98
|
+
}
|
|
99
|
+
if (hasWasmTargetInstalled()) return;
|
|
100
|
+
|
|
101
|
+
if (!commandExists('rustup')) {
|
|
102
|
+
throw new Error(`Missing ${WASM_TARGET} target and rustup is not available. Install rustup then run: rustup target add ${WASM_TARGET}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
console.log(`Installing ${WASM_TARGET} Rust target...`);
|
|
106
|
+
run('rustup', ['target', 'add', WASM_TARGET]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function wranglerConfigUsesWorkerBuild(filePath) {
|
|
110
|
+
if (!fs.existsSync(filePath)) return false;
|
|
111
|
+
const text = fs.readFileSync(filePath, 'utf8');
|
|
112
|
+
return /worker-build/.test(text);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function ensureBuildPrereqsForConfig(filePath) {
|
|
116
|
+
if (!wranglerConfigUsesWorkerBuild(filePath)) return;
|
|
117
|
+
|
|
118
|
+
console.log('\nDetected Rust worker build command in Wrangler config.');
|
|
119
|
+
console.log('Ensuring worker-build and WebAssembly target are available...');
|
|
120
|
+
ensureWorkerBuild();
|
|
121
|
+
ensureWasmTarget();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function getWranglerCommand() {
|
|
125
|
+
return resolveWranglerCommand(ROOT);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Resolved lazily after npm install — do not call before resolveWrangler().
|
|
129
|
+
let WRANGLER = null;
|
|
130
|
+
|
|
131
|
+
function resolveWrangler() {
|
|
132
|
+
WRANGLER = getWranglerCommand();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function runWrangler(args, options = {}) {
|
|
136
|
+
return run(WRANGLER.command, [...WRANGLER.baseArgs, ...args], options);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function runWranglerCapture(args, options = {}) {
|
|
140
|
+
return runCapture(WRANGLER.command, [...WRANGLER.baseArgs, ...args], options);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function isWranglerAuthenticated() {
|
|
144
|
+
const result = spawnSync(WRANGLER.command, [...WRANGLER.baseArgs, 'whoami'], {
|
|
145
|
+
stdio: 'pipe',
|
|
146
|
+
encoding: 'utf8',
|
|
147
|
+
cwd: ROOT
|
|
148
|
+
});
|
|
149
|
+
return result.status === 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function parseFirstDatabaseName(filePath) {
|
|
153
|
+
if (!fs.existsSync(filePath)) return null;
|
|
154
|
+
const jsonc = fs.readFileSync(filePath, 'utf8');
|
|
155
|
+
const match = jsonc.match(/"database_name"\s*:\s*"([^"]+)"/);
|
|
156
|
+
return match ? match[1] : null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function parseFirstDatabaseId(filePath) {
|
|
160
|
+
if (!fs.existsSync(filePath)) return null;
|
|
161
|
+
const jsonc = fs.readFileSync(filePath, 'utf8');
|
|
162
|
+
const match = jsonc.match(/"database_id"\s*:\s*"([^"]+)"/);
|
|
163
|
+
return match ? match[1] : null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function isValidUuid(value) {
|
|
167
|
+
return typeof value === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function patchDbId(text, newDbId) {
|
|
171
|
+
return text.replace(
|
|
172
|
+
/("database_id"\s*:\s*)"[^"]*"/g,
|
|
173
|
+
`$1"${newDbId}"`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function firstUuidFromText(text) {
|
|
178
|
+
if (!text) return null;
|
|
179
|
+
const match = text.match(/[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/i);
|
|
180
|
+
return match ? match[0] : null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function resolveRemoteDbId(dbName) {
|
|
184
|
+
// Try create first. If DB already exists, wrangler typically returns non-zero
|
|
185
|
+
// and we fall back to list lookup.
|
|
186
|
+
const createResult = runWranglerCapture(['d1', 'create', dbName], { allowFailure: true });
|
|
187
|
+
const createOutput = `${createResult.stdout}\n${createResult.stderr}`;
|
|
188
|
+
const createdId = firstUuidFromText(createOutput);
|
|
189
|
+
if (createdId) {
|
|
190
|
+
return createdId;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const listJson = runWranglerCapture(['d1', 'list', '--json'], { allowFailure: true });
|
|
194
|
+
if (listJson.ok) {
|
|
195
|
+
try {
|
|
196
|
+
const parsed = JSON.parse(listJson.stdout);
|
|
197
|
+
if (Array.isArray(parsed)) {
|
|
198
|
+
const found = parsed.find((entry) => entry?.name === dbName || entry?.database_name === dbName);
|
|
199
|
+
const id = found?.uuid || found?.id || found?.database_id;
|
|
200
|
+
if (isValidUuid(id)) return id;
|
|
201
|
+
}
|
|
202
|
+
} catch {
|
|
203
|
+
// fall through to text parsing below
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const listText = runWranglerCapture(['d1', 'list'], { allowFailure: true });
|
|
208
|
+
const line = `${listText.stdout}\n${listText.stderr}`
|
|
209
|
+
.split('\n')
|
|
210
|
+
.find((l) => l.includes(dbName));
|
|
211
|
+
const listedId = firstUuidFromText(line || `${listText.stdout}\n${listText.stderr}`);
|
|
212
|
+
return listedId;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function copyTemplateIfNeeded() {
|
|
216
|
+
if (fs.existsSync(WRANGLER_CONFIG)) {
|
|
217
|
+
return { created: false, source: 'existing' };
|
|
218
|
+
}
|
|
219
|
+
if (!fs.existsSync(WRANGLER_TEMPLATE)) {
|
|
220
|
+
const fallback = `{
|
|
221
|
+
"name": "${PROJECT_NAME}",
|
|
222
|
+
"main": "src/index.js",
|
|
223
|
+
"compatibility_date": "2024-09-23"
|
|
224
|
+
}\n`;
|
|
225
|
+
fs.writeFileSync(WRANGLER_CONFIG, fallback, 'utf8');
|
|
226
|
+
return { created: true, source: 'fallback' };
|
|
227
|
+
}
|
|
228
|
+
fs.copyFileSync(WRANGLER_TEMPLATE, WRANGLER_CONFIG);
|
|
229
|
+
return { created: true, source: 'template' };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function sanitizeDomain(domain) {
|
|
233
|
+
return domain
|
|
234
|
+
.trim()
|
|
235
|
+
.toLowerCase()
|
|
236
|
+
.replace(/^https?:\/\//, '')
|
|
237
|
+
.replace(/\/.*$/, '')
|
|
238
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
239
|
+
.replace(/-+/g, '-')
|
|
240
|
+
.replace(/^-|-$/g, '');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function normalizeHost(value) {
|
|
244
|
+
if (!value || typeof value !== 'string') return null;
|
|
245
|
+
let host = value.trim().toLowerCase();
|
|
246
|
+
if (!host) return null;
|
|
247
|
+
host = host.replace(/^https?:\/\//, '');
|
|
248
|
+
host = host.replace(/\/.*$/, '');
|
|
249
|
+
host = host.replace(/:\d+$/, '');
|
|
250
|
+
return host || null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function firstRouteHostFromWranglerConfig(filePath) {
|
|
254
|
+
if (!fs.existsSync(filePath)) return null;
|
|
255
|
+
const jsonc = fs.readFileSync(filePath, 'utf8');
|
|
256
|
+
const routePatternMatch = jsonc.match(/"pattern"\s*:\s*"([^"]+)"/);
|
|
257
|
+
if (!routePatternMatch) return null;
|
|
258
|
+
const pattern = routePatternMatch[1];
|
|
259
|
+
const host = pattern.split('/')[0];
|
|
260
|
+
return normalizeHost(host);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function dbNameForDomain(domain) {
|
|
264
|
+
const sanitized = sanitizeDomain(domain);
|
|
265
|
+
return sanitized ? `freertc-signal-${sanitized}` : 'freertc-signal';
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function workerNameForDomain(domain) {
|
|
269
|
+
const sanitized = sanitizeDomain(domain);
|
|
270
|
+
if (!sanitized) return PROJECT_NAME;
|
|
271
|
+
return `${PROJECT_NAME}-${sanitized}`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Extract the domain slug from an existing freertc-signal-<domain> DB name.
|
|
275
|
+
// Returns null for placeholder values or plain 'freertc-signal'.
|
|
276
|
+
function domainFromDbName(dbName) {
|
|
277
|
+
if (!dbName) return null;
|
|
278
|
+
const PLACEHOLDERS = ['freertc-signal', 'freertc-signal-your-domain', 'freertc-signal-your_domain'];
|
|
279
|
+
if (PLACEHOLDERS.includes(dbName.toLowerCase())) return null;
|
|
280
|
+
const match = dbName.match(/^freertc-signal-(.+)$/);
|
|
281
|
+
return match ? match[1] : null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Replace all occurrences of a database_name value in wrangler.jsonc text.
|
|
285
|
+
function patchDbName(text, newDbName) {
|
|
286
|
+
return text.replace(
|
|
287
|
+
/("database_name"\s*:\s*)"[^"]*"/g,
|
|
288
|
+
`$1"${newDbName}"`
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function patchVar(text, varName, value) {
|
|
293
|
+
// Replace existing quoted value for the var in any vars block
|
|
294
|
+
const escaped = varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
295
|
+
const re = new RegExp(`("${escaped}"\\s*:\\s*)"[^"]*"`, 'g');
|
|
296
|
+
if (re.test(text)) {
|
|
297
|
+
return text.replace(re, `$1"${value}"`);
|
|
298
|
+
}
|
|
299
|
+
// If not found, inject after RELAY_PEER_ID line (best-effort)
|
|
300
|
+
return text.replace(
|
|
301
|
+
/("RELAY_PEER_ID"\s*:\s*"[^"]*")/g,
|
|
302
|
+
`$1,\n "${varName}": "${value}"`
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function removeVar(text, varName) {
|
|
307
|
+
const escaped = varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
308
|
+
// Remove the line (and trailing comma or leading comma)
|
|
309
|
+
return text
|
|
310
|
+
.replace(new RegExp(`\\s*"${escaped}"\\s*:\\s*"[^"]*",?`, 'g'), '')
|
|
311
|
+
.replace(/,(\s*})/g, '$1'); // clean up trailing commas before closing braces
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function randomSuffix(length = 6) {
|
|
315
|
+
return Math.random().toString(36).slice(2, 2 + length).padEnd(length, '0');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function patchWorkerName(text, newName) {
|
|
319
|
+
// Patch every "name": "..." line (top-level and env.production)
|
|
320
|
+
return text.replace(/^(\s*"name"\s*:\s*)"[^"]*"/gm, `$1"${newName}"`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function parseFirstWorkerName(filePath) {
|
|
324
|
+
if (!fs.existsSync(filePath)) return null;
|
|
325
|
+
const jsonc = fs.readFileSync(filePath, 'utf8');
|
|
326
|
+
const match = jsonc.match(/"name"\s*:\s*"([^"]+)"/);
|
|
327
|
+
return match ? match[1] : null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function modeFromAnswer(answer) {
|
|
331
|
+
const normalized = (answer || '').trim().toLowerCase();
|
|
332
|
+
if (normalized === '1' || normalized === 'dev') return 'dev';
|
|
333
|
+
if (normalized === '2' || normalized === 'deploy') return 'deploy';
|
|
334
|
+
if (normalized === '3' || normalized === 'both') return 'both';
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function modeFromArgs(argv) {
|
|
339
|
+
const args = argv.slice(2);
|
|
340
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
341
|
+
const arg = args[i];
|
|
342
|
+
if (arg === '--mode' && i + 1 < args.length) {
|
|
343
|
+
return modeFromAnswer(args[i + 1]);
|
|
344
|
+
}
|
|
345
|
+
if (arg.startsWith('--mode=')) {
|
|
346
|
+
return modeFromAnswer(arg.split('=')[1]);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function yes(answer, defaultYes = true) {
|
|
353
|
+
const normalized = (answer || '').trim().toLowerCase();
|
|
354
|
+
if (!normalized) return defaultYes;
|
|
355
|
+
return normalized === 'y' || normalized === 'yes';
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function checkHealthUrl(url) {
|
|
359
|
+
const result = spawnSync('curl', ['-fsS', url], {
|
|
360
|
+
stdio: 'pipe',
|
|
361
|
+
encoding: 'utf8'
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
if (result.status === 0) {
|
|
365
|
+
return { ok: true, output: result.stdout || '' };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const output = [result.stdout, result.stderr].filter(Boolean).join('\n');
|
|
369
|
+
return { ok: false, output };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function includesApiKeyMissing(text) {
|
|
373
|
+
if (!text) return false;
|
|
374
|
+
return /api key is missing/i.test(text);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async function main() {
|
|
378
|
+
const rl = createInterface({ input, output });
|
|
379
|
+
const forcedMode = modeFromArgs(process.argv);
|
|
380
|
+
|
|
381
|
+
try {
|
|
382
|
+
const copiedFiles = ensureProjectFiles(ROOT);
|
|
383
|
+
|
|
384
|
+
console.log(`\n${PROJECT_NAME} Wrangler Install Wizard\n`);
|
|
385
|
+
console.log(`Using project root: ${ROOT}`);
|
|
386
|
+
console.log(`Wrangler config path: ${WRANGLER_CONFIG}`);
|
|
387
|
+
console.log(`Wrangler template path: ${WRANGLER_TEMPLATE}`);
|
|
388
|
+
console.log(`Package assets path: ${PACKAGE_ROOT}\n`);
|
|
389
|
+
|
|
390
|
+
if (path.resolve(process.cwd()) !== ROOT) {
|
|
391
|
+
console.log(`Detected project root: ${ROOT}`);
|
|
392
|
+
console.log(`Running commands from project root instead of current directory: ${process.cwd()}\n`);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (copiedFiles.length > 0) {
|
|
396
|
+
console.log('Copied package files into this project:');
|
|
397
|
+
for (const file of copiedFiles) {
|
|
398
|
+
console.log(` - ${file}`);
|
|
399
|
+
}
|
|
400
|
+
console.log('');
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
let mode = forcedMode;
|
|
404
|
+
if (!mode) {
|
|
405
|
+
console.log('Choose setup mode:');
|
|
406
|
+
console.log(' 1) dev (local wrangler dev + local D1 schema)');
|
|
407
|
+
console.log(' 2) deploy (Cloudflare login + remote D1 schema + deploy)');
|
|
408
|
+
console.log(' 3) both (dev + deploy setup)\n');
|
|
409
|
+
|
|
410
|
+
const modeAnswer = await rl.question('Mode [1/2/3]: ');
|
|
411
|
+
mode = modeFromAnswer(modeAnswer);
|
|
412
|
+
} else {
|
|
413
|
+
console.log(`Using setup mode from args: ${mode}`);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (!mode) {
|
|
417
|
+
throw new Error('Invalid mode. Please run the wizard again and choose 1, 2, or 3.');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const needsDev = mode === 'dev' || mode === 'both';
|
|
421
|
+
const needsDeploy = mode === 'deploy' || mode === 'both';
|
|
422
|
+
|
|
423
|
+
console.log('\nStep 1: Ensure project files are present');
|
|
424
|
+
if (copiedFiles.length === 0) {
|
|
425
|
+
console.log('Required worker files already exist in this project.');
|
|
426
|
+
} else {
|
|
427
|
+
console.log('Project bootstrapped from the published freertc package.');
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
resolveWrangler();
|
|
431
|
+
|
|
432
|
+
console.log('\nStep 2: Verify Wrangler CLI is available');
|
|
433
|
+
runWrangler(['--version']);
|
|
434
|
+
console.log(`Using ${WRANGLER.source} wrangler.`);
|
|
435
|
+
|
|
436
|
+
// Create wrangler.jsonc first so we can read existing DB name from it.
|
|
437
|
+
const wranglerInit = copyTemplateIfNeeded();
|
|
438
|
+
if (wranglerInit.created && wranglerInit.source === 'template') {
|
|
439
|
+
console.log('\nCreated wrangler.jsonc from wrangler.template.jsonc.');
|
|
440
|
+
console.log('Edit wrangler.jsonc and replace YOUR_D1_DATABASE_ID before production deploy.');
|
|
441
|
+
}
|
|
442
|
+
if (wranglerInit.created && wranglerInit.source === 'fallback') {
|
|
443
|
+
console.log('\nCreated wrangler.jsonc from fallback defaults (template not found).');
|
|
444
|
+
console.log('Update name/main/compatibility_date and add bindings before deploy.');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
ensureBuildPrereqsForConfig(WRANGLER_CONFIG);
|
|
448
|
+
|
|
449
|
+
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
450
|
+
// Derive D1 database name from domain or existing config.
|
|
451
|
+
console.log('\nStep 3: Configure D1 database name');
|
|
452
|
+
let preferredHealthHost = null;
|
|
453
|
+
const isFirstRun = wranglerInit.created;
|
|
454
|
+
try {
|
|
455
|
+
const existingDbName = parseFirstDatabaseName(WRANGLER_CONFIG);
|
|
456
|
+
const existingDomain = domainFromDbName(existingDbName);
|
|
457
|
+
|
|
458
|
+
let derivedDbName;
|
|
459
|
+
|
|
460
|
+
if (isFirstRun) {
|
|
461
|
+
console.log('Enter your custom domain, or press Enter to use a free workers.dev subdomain.');
|
|
462
|
+
const domainInput = (await rl.question('Domain (example: example.com) [Enter to skip]: ')).trim();
|
|
463
|
+
if (!domainInput) {
|
|
464
|
+
const suffix = randomSuffix();
|
|
465
|
+
const workerName = `${PROJECT_NAME}-${suffix}`;
|
|
466
|
+
derivedDbName = `freertc-signal-${suffix}`;
|
|
467
|
+
console.log(`✓ No domain — using free workers.dev subdomain.`);
|
|
468
|
+
console.log(` Worker name : ${workerName}`);
|
|
469
|
+
console.log(` Database : ${derivedDbName}`);
|
|
470
|
+
let wText = fs.readFileSync(WRANGLER_CONFIG, 'utf8');
|
|
471
|
+
wText = patchWorkerName(wText, workerName);
|
|
472
|
+
fs.writeFileSync(WRANGLER_CONFIG, wText, 'utf8');
|
|
473
|
+
} else {
|
|
474
|
+
derivedDbName = dbNameForDomain(domainInput);
|
|
475
|
+
const workerName = workerNameForDomain(domainInput);
|
|
476
|
+
preferredHealthHost = normalizeHost(domainInput);
|
|
477
|
+
console.log(`✓ Domain-specific database name: ${derivedDbName}`);
|
|
478
|
+
console.log(`✓ Domain-specific worker name: ${workerName}`);
|
|
479
|
+
const customDbName = (await rl.question(`Database name [press Enter for ${derivedDbName}]: `)).trim();
|
|
480
|
+
derivedDbName = customDbName || derivedDbName;
|
|
481
|
+
console.log(`Using database name: ${derivedDbName}`);
|
|
482
|
+
let wText = fs.readFileSync(WRANGLER_CONFIG, 'utf8');
|
|
483
|
+
wText = patchWorkerName(wText, workerName);
|
|
484
|
+
fs.writeFileSync(WRANGLER_CONFIG, wText, 'utf8');
|
|
485
|
+
}
|
|
486
|
+
} else if (existingDomain) {
|
|
487
|
+
// Offer the existing domain-derived name as default
|
|
488
|
+
const dbNamePrompt = `Database name [press Enter for ${existingDbName}]: `;
|
|
489
|
+
const customDbName = (await rl.question(dbNamePrompt)).trim();
|
|
490
|
+
derivedDbName = customDbName || existingDbName;
|
|
491
|
+
console.log(`Using database name: ${derivedDbName}`);
|
|
492
|
+
const workerName = workerNameForDomain(existingDomain);
|
|
493
|
+
let wText = fs.readFileSync(WRANGLER_CONFIG, 'utf8');
|
|
494
|
+
wText = patchWorkerName(wText, workerName);
|
|
495
|
+
fs.writeFileSync(WRANGLER_CONFIG, wText, 'utf8');
|
|
496
|
+
console.log(`✓ Domain-specific worker name: ${workerName}`);
|
|
497
|
+
} else {
|
|
498
|
+
// Existing DB is placeholder or missing domain flavor — always offer to upgrade
|
|
499
|
+
if (existingDbName === 'freertc-signal') {
|
|
500
|
+
console.log(`Current database: ${existingDbName} (placeholder, no domain)`);
|
|
501
|
+
}
|
|
502
|
+
console.log('Database names should follow: freertc-signal-<your-domain>');
|
|
503
|
+
const domainInput = (await rl.question('Domain (example: example.com) [Enter to use free workers.dev]: ')).trim();
|
|
504
|
+
if (!domainInput) {
|
|
505
|
+
const suffix = randomSuffix();
|
|
506
|
+
const workerName = `${PROJECT_NAME}-${suffix}`;
|
|
507
|
+
derivedDbName = `freertc-signal-${suffix}`;
|
|
508
|
+
console.log(`✓ No domain — using free workers.dev subdomain.`);
|
|
509
|
+
console.log(` Worker name : ${workerName}`);
|
|
510
|
+
console.log(` Database : ${derivedDbName}`);
|
|
511
|
+
let wText = fs.readFileSync(WRANGLER_CONFIG, 'utf8');
|
|
512
|
+
wText = patchWorkerName(wText, workerName);
|
|
513
|
+
fs.writeFileSync(WRANGLER_CONFIG, wText, 'utf8');
|
|
514
|
+
} else {
|
|
515
|
+
derivedDbName = dbNameForDomain(domainInput);
|
|
516
|
+
const workerName = workerNameForDomain(domainInput);
|
|
517
|
+
preferredHealthHost = normalizeHost(domainInput);
|
|
518
|
+
console.log(`✓ Domain-specific database name: ${derivedDbName}`);
|
|
519
|
+
console.log(`✓ Domain-specific worker name: ${workerName}`);
|
|
520
|
+
const customDbName = (await rl.question(`Confirm [press Enter for ${derivedDbName}]: `)).trim();
|
|
521
|
+
derivedDbName = customDbName || derivedDbName;
|
|
522
|
+
let wText = fs.readFileSync(WRANGLER_CONFIG, 'utf8');
|
|
523
|
+
wText = patchWorkerName(wText, workerName);
|
|
524
|
+
fs.writeFileSync(WRANGLER_CONFIG, wText, 'utf8');
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Patch DB name and auto-set RELAY_URL from domain.
|
|
529
|
+
const host = preferredHealthHost || normalizeHost(derivedDbName.replace(/^freertc-signal-/, ''));
|
|
530
|
+
const relayWsUrl = host ? `wss://${host}/ws` : null;
|
|
531
|
+
|
|
532
|
+
let wranglerText = fs.readFileSync(WRANGLER_CONFIG, 'utf8');
|
|
533
|
+
wranglerText = patchDbName(wranglerText, derivedDbName);
|
|
534
|
+
if (relayWsUrl) {
|
|
535
|
+
wranglerText = patchVar(wranglerText, 'RELAY_URL', relayWsUrl);
|
|
536
|
+
console.log(`✓ Set RELAY_URL: ${relayWsUrl}`);
|
|
537
|
+
}
|
|
538
|
+
fs.writeFileSync(WRANGLER_CONFIG, wranglerText, 'utf8');
|
|
539
|
+
console.log(`✓ Updated wrangler.jsonc with database name: ${derivedDbName}`);
|
|
540
|
+
|
|
541
|
+
// Federation opt-in
|
|
542
|
+
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
543
|
+
console.log('\nGlobal peer network: contribute your relay to peer.ooo federation?');
|
|
544
|
+
console.log('When enabled, peers across all federated relays can discover each other.');
|
|
545
|
+
const joinGlobal = await rl.question('Join global network at peer.ooo? [Y/n]: ');
|
|
546
|
+
let updatedText = fs.readFileSync(WRANGLER_CONFIG, 'utf8');
|
|
547
|
+
if (yes(joinGlobal, true)) {
|
|
548
|
+
const relayNameAnswer = (await rl.question('Relay display name [press Enter to skip]: ')).trim();
|
|
549
|
+
updatedText = patchVar(updatedText, 'GLOBAL_RELAY_URL', 'wss://peer.ooo/ws');
|
|
550
|
+
if (relayNameAnswer) {
|
|
551
|
+
updatedText = patchVar(updatedText, 'RELAY_NAME', relayNameAnswer);
|
|
552
|
+
}
|
|
553
|
+
console.log('✓ GLOBAL_RELAY_URL set to wss://peer.ooo/ws');
|
|
554
|
+
} else {
|
|
555
|
+
updatedText = removeVar(updatedText, 'GLOBAL_RELAY_URL');
|
|
556
|
+
updatedText = removeVar(updatedText, 'RELAY_NAME');
|
|
557
|
+
console.log('✓ Skipped global network — relay will operate standalone.');
|
|
558
|
+
}
|
|
559
|
+
fs.writeFileSync(WRANGLER_CONFIG, updatedText, 'utf8');
|
|
560
|
+
|
|
561
|
+
// Safety pass: when DB name is domain-based and worker name is still default,
|
|
562
|
+
// normalize to freertc-<domain> before any deploy action.
|
|
563
|
+
const currentWorkerName = parseFirstWorkerName(WRANGLER_CONFIG);
|
|
564
|
+
const domainFromDb = domainFromDbName(derivedDbName);
|
|
565
|
+
if (domainFromDb && currentWorkerName === PROJECT_NAME) {
|
|
566
|
+
const normalizedWorkerName = workerNameForDomain(domainFromDb);
|
|
567
|
+
let normalizeText = fs.readFileSync(WRANGLER_CONFIG, 'utf8');
|
|
568
|
+
normalizeText = patchWorkerName(normalizeText, normalizedWorkerName);
|
|
569
|
+
fs.writeFileSync(WRANGLER_CONFIG, normalizeText, 'utf8');
|
|
570
|
+
console.log(`✓ Normalized worker name from ${PROJECT_NAME} to ${normalizedWorkerName}`);
|
|
571
|
+
}
|
|
572
|
+
} catch (err) {
|
|
573
|
+
console.error('Step 3 error:', err.message);
|
|
574
|
+
throw err;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const dbName = parseFirstDatabaseName(WRANGLER_CONFIG);
|
|
578
|
+
if (!dbName) {
|
|
579
|
+
console.log('\nNo D1 database_name found in wrangler.jsonc.');
|
|
580
|
+
console.log('Please set d1_databases[0].database_name, then rerun this wizard.');
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// For remote operations, database_id must be a real UUID (not placeholder).
|
|
585
|
+
if (needsDeploy) {
|
|
586
|
+
let dbId = parseFirstDatabaseId(WRANGLER_CONFIG);
|
|
587
|
+
if (!isValidUuid(dbId)) {
|
|
588
|
+
console.log('\nStep 4: Configure D1 database ID');
|
|
589
|
+
console.log(`Current database_id is invalid or placeholder: ${dbId || '(missing)'}`);
|
|
590
|
+
console.log(`Creating or resolving remote D1 database: ${dbName}`);
|
|
591
|
+
const resolvedDbId = resolveRemoteDbId(dbName);
|
|
592
|
+
if (!isValidUuid(resolvedDbId)) {
|
|
593
|
+
throw new Error(`Could not resolve database_id for ${dbName}. Run: ${WRANGLER.command} ${[...WRANGLER.baseArgs, 'd1', 'create', dbName].join(' ')} and update wrangler.jsonc.`);
|
|
594
|
+
}
|
|
595
|
+
const current = fs.readFileSync(WRANGLER_CONFIG, 'utf8');
|
|
596
|
+
fs.writeFileSync(WRANGLER_CONFIG, patchDbId(current, resolvedDbId), 'utf8');
|
|
597
|
+
dbId = resolvedDbId;
|
|
598
|
+
console.log(`✓ Updated wrangler.jsonc with database_id: ${dbId}`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (!fs.existsSync(D1_SCHEMA_FILE)) {
|
|
603
|
+
throw new Error('Missing scripts/d1-schema.sql');
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (needsDeploy) {
|
|
607
|
+
console.log('\nStep 5: Cloudflare authentication');
|
|
608
|
+
if (isWranglerAuthenticated()) {
|
|
609
|
+
console.log('Wrangler is already authenticated. Skipping login.');
|
|
610
|
+
} else {
|
|
611
|
+
const doLogin = await rl.question('Not logged in. Run "wrangler login" now? [Y/n]: ');
|
|
612
|
+
if (yes(doLogin, true)) {
|
|
613
|
+
runWrangler(['login']);
|
|
614
|
+
} else {
|
|
615
|
+
console.log('Skipping login. Deploy steps may fail until you authenticate.');
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (needsDev) {
|
|
621
|
+
console.log('\nStep 6: Initialize local D1 schema');
|
|
622
|
+
runWrangler(['d1', 'execute', dbName, '--local', '--file', 'scripts/d1-schema.sql']);
|
|
623
|
+
|
|
624
|
+
const startDevDefaultYes = !needsDeploy;
|
|
625
|
+
const startDevPrompt = startDevDefaultYes
|
|
626
|
+
? 'Start local Wrangler dev server now (freertc dev:cf)? [Y/n]: '
|
|
627
|
+
: 'Start local Wrangler dev server now (freertc dev:cf)? [y/N]: ';
|
|
628
|
+
const startDev = await rl.question(startDevPrompt);
|
|
629
|
+
if (yes(startDev, startDevDefaultYes)) {
|
|
630
|
+
run(process.execPath, [path.join(PACKAGE_ROOT, 'scripts', 'dev-server.mjs')]);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (needsDeploy) {
|
|
635
|
+
console.log('\nStep 7: Initialize remote D1 schema');
|
|
636
|
+
runWrangler(['d1', 'execute', dbName, '--remote', '--file', 'scripts/d1-schema.sql']);
|
|
637
|
+
|
|
638
|
+
const doDeploy = await rl.question('Deploy now (freertc deploy)? [Y/n]: ');
|
|
639
|
+
if (yes(doDeploy, true)) {
|
|
640
|
+
runWrangler(['deploy', '--env', 'production']);
|
|
641
|
+
|
|
642
|
+
console.log('\nStep 8: Verify deployment endpoint (recommended)');
|
|
643
|
+
console.log('Auto-checking /health on detected domain(s)...');
|
|
644
|
+
|
|
645
|
+
const routeHost = firstRouteHostFromWranglerConfig(WRANGLER_CONFIG);
|
|
646
|
+
const hosts = [preferredHealthHost, routeHost].filter(Boolean);
|
|
647
|
+
const uniqueHosts = [...new Set(hosts)];
|
|
648
|
+
|
|
649
|
+
if (uniqueHosts.length === 0) {
|
|
650
|
+
console.log('No custom domain detected in wizard input or wrangler routes.');
|
|
651
|
+
console.log('Set routes in wrangler.jsonc or run manual check: curl -fsS https://<your-domain>/health');
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
for (const host of uniqueHosts) {
|
|
655
|
+
const healthUrl = `https://${host}/health`;
|
|
656
|
+
console.log(`\nChecking ${healthUrl}`);
|
|
657
|
+
const health = checkHealthUrl(healthUrl);
|
|
658
|
+
if (health.ok) {
|
|
659
|
+
console.log('/health response:');
|
|
660
|
+
console.log(health.output.trim() || '(empty body)');
|
|
661
|
+
} else {
|
|
662
|
+
console.log('Health check failed. Raw output:');
|
|
663
|
+
console.log(health.output || '(no output)');
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (includesApiKeyMissing(health.output)) {
|
|
667
|
+
console.log('\nDetected "API key is missing" in response.');
|
|
668
|
+
console.log('This Worker does not require an API key for /health or /ws.');
|
|
669
|
+
console.log('Most likely causes:');
|
|
670
|
+
console.log(' 1) The domain route points to a different service/worker.');
|
|
671
|
+
console.log(' 2) Cloudflare Access/API Shield/WAF on that hostname requires auth headers.');
|
|
672
|
+
console.log(' 3) You deployed a different environment than expected.');
|
|
673
|
+
console.log('Next checks:');
|
|
674
|
+
console.log(' - Confirm route/custom domain is attached to this Worker.');
|
|
675
|
+
console.log(' - Compare workers.dev /health vs custom-domain /health responses.');
|
|
676
|
+
console.log(' - If using --env production, ensure that env is the one attached to routes.');
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
console.log('\nWizard completed successfully.');
|
|
683
|
+
console.log('\nQuick commands:');
|
|
684
|
+
console.log(` ${WRANGLER.command} ${[...WRANGLER.baseArgs, 'd1', 'execute', dbName, '--local', '--file', 'scripts/d1-schema.sql'].join(' ')}`);
|
|
685
|
+
console.log(` ${WRANGLER.command} ${[...WRANGLER.baseArgs, 'd1', 'execute', dbName, '--remote', '--file', 'scripts/d1-schema.sql'].join(' ')}`);
|
|
686
|
+
console.log(' npx freertc dev');
|
|
687
|
+
console.log(' npx freertc dev:cf');
|
|
688
|
+
console.log(' npx freertc deploy');
|
|
689
|
+
} finally {
|
|
690
|
+
rl.close();
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
main().catch((error) => {
|
|
695
|
+
console.error(`\nWizard failed: ${error.message}`);
|
|
696
|
+
process.exit(1);
|
|
697
|
+
});
|