copilot-proxy-web 1.0.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/bin/run-web.js ADDED
@@ -0,0 +1,548 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const path = require("path");
5
+ const os = require("os");
6
+ const fs = require("fs");
7
+ const dns = require("dns");
8
+ const http = require("http");
9
+ const https = require("https");
10
+ const { spawn, spawnSync } = require("child_process");
11
+ const { execSync } = require("child_process");
12
+ const {
13
+ normalizePath,
14
+ extractHostname,
15
+ resolveHostnameWithDomain,
16
+ inferDomainFromHostname,
17
+ formatCheckLine,
18
+ parseRouteCreatedRecord,
19
+ } = require("../lib/cloudflare-utils");
20
+ const { createDaemonStateStore } = require("../lib/daemon-state");
21
+ const { createDaemonService } = require("../lib/daemon-service");
22
+ const { createCloudflareStateStore } = require("../lib/cloudflare-state");
23
+ const { createCloudflareService } = require("../lib/cloudflare-service");
24
+ const { createCloudflareSetupService } = require("../lib/cloudflare-setup-service");
25
+ const { createCloudflareSetupDeps } = require("../lib/cloudflare-setup-deps");
26
+
27
+ const DEFAULTS = {
28
+ port: "3000",
29
+ log: "/tmp/copilot-proxy/",
30
+ ptyCols: "80",
31
+ ptyRows: "24",
32
+ noDefaultSession: true,
33
+ noStdin: true,
34
+ noStdout: true,
35
+ };
36
+
37
+ const argv = process.argv.slice(2);
38
+ let subcommand = "run";
39
+ if (argv.length > 0 && !argv[0].startsWith("-")) {
40
+ subcommand = argv[0];
41
+ const allowed = new Set([
42
+ "run",
43
+ "start",
44
+ "stop",
45
+ "status",
46
+ "check",
47
+ "cloudflare",
48
+ "wc",
49
+ ]);
50
+ if (!allowed.has(subcommand)) {
51
+ console.error(`Unknown subcommand: ${subcommand}`);
52
+ console.error("Available: run | start | stop | status | check | cloudflare | wc");
53
+ process.exit(1);
54
+ }
55
+ argv.shift();
56
+ }
57
+
58
+ const passthrough = [];
59
+ const overrides = {
60
+ port: null,
61
+ log: null,
62
+ ptyCols: null,
63
+ ptyRows: null,
64
+ noDefaultSession: null,
65
+ noStdin: null,
66
+ noStdout: null,
67
+ };
68
+
69
+ function takeValue(i) {
70
+ if (i + 1 >= argv.length) {
71
+ throw new Error(`Missing value for ${argv[i]}`);
72
+ }
73
+ return argv[i + 1];
74
+ }
75
+
76
+ for (let i = 0; i < argv.length; i += 1) {
77
+ const arg = argv[i];
78
+
79
+ if (arg === "--help" || arg === "-h") {
80
+ console.log(`Usage:
81
+ npx copilot-proxy-web [run|start|stop|status|check] [overrides] [extra flags]
82
+ npx copilot-proxy-web cloudflare <setup|start|stop|status|check|diagnose|clean|token> [flags]
83
+ npx copilot-proxy-web wc [ws client flags]
84
+
85
+ Overrides:
86
+ --port <n> (default: ${DEFAULTS.port})
87
+ --log <path> (default: ${DEFAULTS.log})
88
+ --pty-cols <n> (default: ${DEFAULTS.ptyCols})
89
+ --pty-rows <n> (default: ${DEFAULTS.ptyRows})
90
+ --auth-token <tok> (or set AUTH_TOKEN env var)
91
+ --default-session (removes --no-default-session)
92
+ --stdin (removes --no-stdin)
93
+ --stdout (removes --no-stdout)
94
+
95
+ WS client flags (wc):
96
+ --url URL WebSocket endpoint (wss://.../ws)
97
+ --sessionId ID Session id to attach
98
+ --token TOKEN AUTH_TOKEN (or set AUTH_TOKEN env)
99
+ --origin ORIGIN Set Origin header (for servers that require it)
100
+ --header KEY:VALUE Add custom request header (repeatable)
101
+ --cf-access-id ID Cloudflare Access Client ID
102
+ --cf-access-secret ID Cloudflare Access Client Secret
103
+ --text TEXT Send a text command (adds CR on server)
104
+ --keys KEYS Send raw keys
105
+ --resize COLSxROWS Send resize event once
106
+ --no-stdin Do not forward stdin
107
+
108
+ Cloudflare flags:
109
+ setup --hostname <host> (required) Tunnel hostname (e.g. proxy.example.com)
110
+ setup --domain <zone> (optional) DNS zone guard (e.g. example.com). If omitted, auto-infer from hostname (last two labels).
111
+ setup --tunnel-name <name> (default: copilot-proxy-web)
112
+ setup --port <n> (default: ${DEFAULTS.port})
113
+ setup --service <http|https> (override scheme for localhost target)
114
+ setup --service-url <url> (override full service target, e.g. https://127.0.0.1:3000/health)
115
+ setup --path <path> (match on hostname + path, e.g. /path01)
116
+ token set --token <TOKEN>
117
+ status/diagnose --local-url <url> (override local health check URL)
118
+ status/diagnose --local-path <path> (override local path; builds http://127.0.0.1:<port><path>)
119
+ `);
120
+ process.exit(0);
121
+ }
122
+
123
+ if (arg === "--port") {
124
+ overrides.port = takeValue(i);
125
+ i += 1;
126
+ continue;
127
+ }
128
+ if (arg === "--log") {
129
+ overrides.log = takeValue(i);
130
+ i += 1;
131
+ continue;
132
+ }
133
+ if (arg === "--pty-cols") {
134
+ overrides.ptyCols = takeValue(i);
135
+ i += 1;
136
+ continue;
137
+ }
138
+ if (arg === "--pty-rows") {
139
+ overrides.ptyRows = takeValue(i);
140
+ i += 1;
141
+ continue;
142
+ }
143
+
144
+ if (arg === "--default-session") {
145
+ overrides.noDefaultSession = false;
146
+ continue;
147
+ }
148
+ if (arg === "--no-default-session") {
149
+ overrides.noDefaultSession = true;
150
+ continue;
151
+ }
152
+ if (arg === "--stdin") {
153
+ overrides.noStdin = false;
154
+ continue;
155
+ }
156
+ if (arg === "--no-stdin") {
157
+ overrides.noStdin = true;
158
+ continue;
159
+ }
160
+ if (arg === "--stdout") {
161
+ overrides.noStdout = false;
162
+ continue;
163
+ }
164
+ if (arg === "--no-stdout") {
165
+ overrides.noStdout = true;
166
+ continue;
167
+ }
168
+
169
+ passthrough.push(arg);
170
+ }
171
+
172
+ const port = overrides.port ?? DEFAULTS.port;
173
+ const log = overrides.log ?? DEFAULTS.log;
174
+ const ptyCols = overrides.ptyCols ?? DEFAULTS.ptyCols;
175
+ const ptyRows = overrides.ptyRows ?? DEFAULTS.ptyRows;
176
+ const noDefaultSession =
177
+ overrides.noDefaultSession ?? DEFAULTS.noDefaultSession;
178
+ const noStdin = overrides.noStdin ?? DEFAULTS.noStdin;
179
+ const noStdout = overrides.noStdout ?? DEFAULTS.noStdout;
180
+
181
+ function getPassthroughValue(flag) {
182
+ const idx = passthrough.indexOf(flag);
183
+ if (idx >= 0 && idx + 1 < passthrough.length) {
184
+ return passthrough[idx + 1];
185
+ }
186
+ return null;
187
+ }
188
+
189
+ function hasAuthConfigured() {
190
+ if (process.env.AUTH_TOKEN) return true;
191
+ return Boolean(getPassthroughValue("--auth-token"));
192
+ }
193
+
194
+ function resolveHostForPrint() {
195
+ return getPassthroughValue("--host") || "127.0.0.1";
196
+ }
197
+
198
+ function resolveApiPortForPrint() {
199
+ return getPassthroughValue("--api-port") || port;
200
+ }
201
+
202
+ function getCloudflareArg(flag) {
203
+ const actions = new Set([
204
+ "setup",
205
+ "start",
206
+ "stop",
207
+ "status",
208
+ "check",
209
+ "diagnose",
210
+ "clean",
211
+ "token",
212
+ ]);
213
+ let start = 0;
214
+ if (argv.length > 0 && actions.has(argv[0])) start = 1;
215
+ for (let i = start; i < argv.length; i += 1) {
216
+ if (argv[i] === flag && i + 1 < argv.length) return argv[i + 1];
217
+ }
218
+ return null;
219
+ }
220
+
221
+ const homeRoot =
222
+ process.env.COPILOT_PROXY_HOME || path.join(os.homedir(), ".copilot-proxy-web");
223
+ const pidFile = path.join(homeRoot, "proxy.pid");
224
+ const stateFile = path.join(homeRoot, "state.json");
225
+ const cfRoot = path.join(homeRoot, "cloudflare");
226
+ const cfPidFile = path.join(cfRoot, "cloudflared.pid");
227
+ const cfStateFile = path.join(cfRoot, "state.json");
228
+ const cfConfigFile = path.join(cfRoot, "config.yml");
229
+ const cfTokenFile = path.join(cfRoot, "token.txt");
230
+ const daemonStateStore = createDaemonStateStore({
231
+ fs,
232
+ homeRoot,
233
+ pidFile,
234
+ stateFile,
235
+ });
236
+ const cfStateStore = createCloudflareStateStore({
237
+ fs,
238
+ paths: {
239
+ cfRoot,
240
+ cfPidFile,
241
+ cfStateFile,
242
+ cfConfigFile,
243
+ cfTokenFile,
244
+ },
245
+ });
246
+
247
+ function isProcessAlive(pid) {
248
+ if (!pid) return false;
249
+ try {
250
+ process.kill(pid, 0);
251
+ return true;
252
+ } catch {
253
+ return false;
254
+ }
255
+ }
256
+
257
+ function sleep(ms) {
258
+ const shared = new Int32Array(new SharedArrayBuffer(4));
259
+ Atomics.wait(shared, 0, 0, ms);
260
+ }
261
+
262
+ function buildArgs() {
263
+ const args = [
264
+ path.join(__dirname, "..", "copilot-proxy.js"),
265
+ "--web",
266
+ "--port",
267
+ port,
268
+ "--shell",
269
+ "--log",
270
+ log,
271
+ "--pty-cols",
272
+ ptyCols,
273
+ "--pty-rows",
274
+ ptyRows,
275
+ ];
276
+
277
+ if (noDefaultSession) args.push("--no-default-session");
278
+ if (noStdin) args.push("--no-stdin");
279
+ if (noStdout) args.push("--no-stdout");
280
+
281
+ args.push(...passthrough);
282
+ return args;
283
+ }
284
+
285
+ function restoreTerminal() {
286
+ if (process.stdin.isTTY) {
287
+ try {
288
+ process.stdin.setRawMode(false);
289
+ } catch {
290
+ // ignore
291
+ }
292
+ if (process.platform !== "win32") {
293
+ try {
294
+ execSync("stty sane", { stdio: "inherit" });
295
+ } catch {
296
+ // ignore
297
+ }
298
+ }
299
+ }
300
+ }
301
+
302
+ function runForeground() {
303
+ return daemonService.runForeground();
304
+ }
305
+
306
+ function startBackground() {
307
+ return daemonService.startBackground();
308
+ }
309
+
310
+ function stopBackground() {
311
+ return daemonService.stopBackground();
312
+ }
313
+
314
+ function status() {
315
+ return daemonService.status();
316
+ }
317
+
318
+ function check() {
319
+ return daemonService.check();
320
+ }
321
+
322
+ function findInPath(bin) {
323
+ const pathEnv = process.env.PATH || "";
324
+ const parts = pathEnv.split(path.delimiter);
325
+ for (const dir of parts) {
326
+ if (!dir) continue;
327
+ const candidate = path.join(dir, bin);
328
+ try {
329
+ fs.accessSync(candidate, fs.constants.X_OK);
330
+ return candidate;
331
+ } catch {
332
+ // continue
333
+ }
334
+ }
335
+ return null;
336
+ }
337
+
338
+ function resolveCloudflaredBin() {
339
+ const envBin = process.env.CLOUDFLARED_BIN;
340
+ if (envBin) return envBin;
341
+ const inPath = findInPath("cloudflared");
342
+ if (inPath) return inPath;
343
+ const common = ["/opt/homebrew/bin/cloudflared", "/usr/local/bin/cloudflared", "/usr/bin/cloudflared"];
344
+ for (const candidate of common) {
345
+ try {
346
+ fs.accessSync(candidate, fs.constants.X_OK);
347
+ return candidate;
348
+ } catch {
349
+ // continue
350
+ }
351
+ }
352
+ return "cloudflared";
353
+ }
354
+
355
+ function ensureCloudflaredOrExit() {
356
+ const bin = resolveCloudflaredBin();
357
+ const res = spawnSync(bin, ["--version"], { stdio: "ignore" });
358
+ if (res.error) {
359
+ console.error("cloudflared not found.");
360
+ console.error("Install:");
361
+ console.error(" https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/");
362
+ console.error(" https://github.com/cloudflare/cloudflared");
363
+ console.error("Or set CLOUDFLARED_BIN=/full/path/to/cloudflared");
364
+ process.exit(1);
365
+ }
366
+ return bin;
367
+ }
368
+
369
+ const daemonService = createDaemonService({
370
+ daemonStateStore,
371
+ buildArgs,
372
+ spawnFn: spawn,
373
+ isProcessAlive,
374
+ sleepFn: sleep,
375
+ processLike: process,
376
+ consoleLike: console,
377
+ httpLike: http,
378
+ resolveHostForPrint,
379
+ resolveApiPortForPrint,
380
+ hasAuthConfigured,
381
+ port,
382
+ log,
383
+ ptyCols,
384
+ ptyRows,
385
+ noDefaultSession,
386
+ noStdin,
387
+ noStdout,
388
+ passthrough,
389
+ restoreTerminal,
390
+ });
391
+
392
+ const cloudflareSetupDeps = createCloudflareSetupDeps({
393
+ fs,
394
+ path,
395
+ os,
396
+ dns,
397
+ spawnSync,
398
+ cfRoot,
399
+ cfConfigFile,
400
+ });
401
+
402
+ const cloudflareSetupService = createCloudflareSetupService({
403
+ extractHostname,
404
+ inferDomainFromHostname,
405
+ resolveHostnameWithDomain,
406
+ ensureCloudflaredOrExit,
407
+ port,
408
+ cfStateStore,
409
+ resolveTunnelId: cloudflareSetupDeps.resolveTunnelId,
410
+ findCredentialsForTunnel: cloudflareSetupDeps.findCredentialsForTunnel,
411
+ readLatestCredentialsFile: cloudflareSetupDeps.readLatestCredentialsFile,
412
+ hasOriginCert: cloudflareSetupDeps.hasOriginCert,
413
+ lookupDnsAddresses: cloudflareSetupDeps.lookupDnsAddresses,
414
+ findDnsRouteLine: cloudflareSetupDeps.findDnsRouteLine,
415
+ writeCloudflareConfigWithService: cloudflareSetupDeps.writeCloudflareConfigWithService,
416
+ parseRouteCreatedRecord,
417
+ spawnFn: spawn,
418
+ consoleLike: console,
419
+ processLike: process,
420
+ formatCheckLine,
421
+ cfConfigFile,
422
+ });
423
+
424
+ const cloudflareService = createCloudflareService({
425
+ ensureCloudflaredOrExit,
426
+ cfStateStore,
427
+ cfConfigFile,
428
+ cfTokenFile,
429
+ fsLike: fs,
430
+ spawnFn: spawn,
431
+ isProcessAlive,
432
+ sleepFn: sleep,
433
+ processLike: process,
434
+ consoleLike: console,
435
+ httpsLike: https,
436
+ httpLike: http,
437
+ getCloudflareArg,
438
+ normalizePath,
439
+ port,
440
+ });
441
+
442
+ function cloudflareSetup(cfArgs) {
443
+ return cloudflareSetupService.cloudflareSetup(cfArgs);
444
+ }
445
+
446
+ function cloudflareStart() {
447
+ return cloudflareService.cloudflareStart();
448
+ }
449
+
450
+ function cloudflareStop() {
451
+ return cloudflareService.cloudflareStop();
452
+ }
453
+
454
+ function cloudflareStatus() {
455
+ return cloudflareService.cloudflareStatus();
456
+ }
457
+
458
+ function cloudflareCheck() {
459
+ return cloudflareService.cloudflareCheck();
460
+ }
461
+
462
+ function cloudflareDiagnose() {
463
+ return cloudflareService.cloudflareDiagnose();
464
+ }
465
+
466
+ function cloudflareClean() {
467
+ return cloudflareService.cloudflareClean();
468
+ }
469
+
470
+ function cloudflareToken(action, tokenValue) {
471
+ return cloudflareService.cloudflareToken(action, tokenValue);
472
+ }
473
+
474
+ function runWsClient() {
475
+ const clientPath = path.join(__dirname, "wss-client.js");
476
+ const res = spawnSync(process.execPath, [clientPath, ...argv], {
477
+ stdio: "inherit",
478
+ });
479
+ if (res.error) {
480
+ console.error(res.error?.message || "Failed to start WS client.");
481
+ process.exit(1);
482
+ }
483
+ process.exit(res.status ?? 0);
484
+ }
485
+
486
+ if (subcommand === "run") {
487
+ runForeground();
488
+ } else if (subcommand === "start") {
489
+ startBackground();
490
+ } else if (subcommand === "stop") {
491
+ stopBackground();
492
+ } else if (subcommand === "status") {
493
+ status();
494
+ } else if (subcommand === "check") {
495
+ check();
496
+ } else if (subcommand === "cloudflare") {
497
+ const action = argv[0];
498
+ if (action === "--help" || action === "-h") {
499
+ console.log(`Usage:
500
+ npx copilot-proxy-web cloudflare <setup|start|stop|status|check|diagnose|clean|token> [flags]
501
+
502
+ Setup:
503
+ --hostname <host> (required) Tunnel hostname (e.g. proxy.example.com)
504
+ --domain <zone> (optional) DNS zone guard (e.g. example.com). If omitted, auto-infer from hostname (last two labels).
505
+ --tunnel-name <name> (default: copilot-proxy-web)
506
+ --port <n> (default: ${port})
507
+
508
+ Token:
509
+ cloudflare token set --token <TOKEN>
510
+ cloudflare token get
511
+ cloudflare token clear
512
+ `);
513
+ process.exit(0);
514
+ }
515
+ const cfArgs = argv.slice(1);
516
+ if (!action) {
517
+ console.error("Usage: npx copilot-proxy-web cloudflare <setup|start|stop|status|check>");
518
+ process.exit(1);
519
+ }
520
+ if (action === "setup") {
521
+ cloudflareSetup(cfArgs);
522
+ } else if (action === "start") {
523
+ cloudflareStart();
524
+ } else if (action === "stop") {
525
+ cloudflareStop();
526
+ } else if (action === "status") {
527
+ cloudflareStatus();
528
+ } else if (action === "check") {
529
+ cloudflareCheck();
530
+ } else if (action === "diagnose") {
531
+ cloudflareDiagnose();
532
+ } else if (action === "clean") {
533
+ cloudflareClean();
534
+ } else if (action === "token") {
535
+ const tokenAction = cfArgs[0];
536
+ let tokenValue = null;
537
+ const tokenIdx = cfArgs.indexOf("--token");
538
+ if (tokenIdx >= 0 && tokenIdx + 1 < cfArgs.length) {
539
+ tokenValue = cfArgs[tokenIdx + 1];
540
+ }
541
+ cloudflareToken(tokenAction, tokenValue);
542
+ } else {
543
+ console.error(`Unknown cloudflare command: ${action}`);
544
+ process.exit(1);
545
+ }
546
+ } else if (subcommand === "wc") {
547
+ runWsClient();
548
+ }