@vincentt-xr/cli 0.1.1 → 0.1.3
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/dist/cli/index.js +25 -5
- package/dist/lib/lifecycle.d.ts +14 -0
- package/dist/lib/lifecycle.js +51 -1
- package/dist/lib/relayInfo.d.ts +3 -0
- package/dist/preview/proxy.js +10 -1
- package/dist/preview/qrPage.d.ts +4 -0
- package/dist/preview/qrPage.js +62 -0
- package/dist/preview/runner.d.ts +11 -3
- package/dist/preview/runner.js +23 -8
- package/package.json +3 -1
package/dist/cli/index.js
CHANGED
|
@@ -14,7 +14,8 @@ import path from "node:path";
|
|
|
14
14
|
import { runLogin } from "../login/login.js";
|
|
15
15
|
import { clearMachineToken, clearProjectBinding, loadMachineConfig, } from "../shared/config.js";
|
|
16
16
|
import { getMe } from "../lib/backend.js";
|
|
17
|
-
import { diagQuery, diagRead, previewStart, previewStop, projectCreate, projectPublish, } from "../lib/lifecycle.js";
|
|
17
|
+
import { diagQuery, diagRead, previewStart, previewStop, previewStopSignal, projectCreate, projectPublish, } from "../lib/lifecycle.js";
|
|
18
|
+
import { writeQrPage } from "../preview/qrPage.js";
|
|
18
19
|
import { fetchNext, readCursor, writeCursor } from "../lib/feedback.js";
|
|
19
20
|
const DEFAULT_EDITOR_URL = "https://editor.vincentt.studio";
|
|
20
21
|
const DEFAULT_API_URL = "https://api.vincentt.studio";
|
|
@@ -110,15 +111,33 @@ async function publish() {
|
|
|
110
111
|
process.exitCode = 1;
|
|
111
112
|
}
|
|
112
113
|
async function preview() {
|
|
114
|
+
// `--stop` reaps a preview started by another process (e.g. a backgrounded run)
|
|
115
|
+
// via its pidfile — no need to hunt for the cloudflared pid by hand.
|
|
116
|
+
if (has("--stop")) {
|
|
117
|
+
const { message } = await previewStopSignal(projectDir());
|
|
118
|
+
console.log(message);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
113
121
|
const { preview: p, started } = await previewStart(projectDir(), (m) => console.error(`[preview] ${m}`));
|
|
114
122
|
if (!started) {
|
|
115
123
|
console.log(`A preview is already running:\n${p.url}`);
|
|
116
124
|
return;
|
|
117
125
|
}
|
|
118
|
-
console.log(
|
|
119
|
-
console.log(
|
|
126
|
+
console.log("\nLive preview:");
|
|
127
|
+
console.log(` Open on your phone: ${p.url}`);
|
|
128
|
+
// A scannable QR in the terminal is unreliable across emulators, so hand back a
|
|
129
|
+
// desktop QR page to open and scan instead. Best-effort — a preview is still
|
|
130
|
+
// usable via the URL above if writing the page fails.
|
|
131
|
+
try {
|
|
132
|
+
const qrUrl = await writeQrPage(projectDir(), p.url);
|
|
133
|
+
console.log(` Show QR (desktop): ${qrUrl}`);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
/* non-fatal: the URL above is enough to preview */
|
|
137
|
+
}
|
|
138
|
+
console.log("\nDiagnostics: vincentt logs | network | trace");
|
|
120
139
|
console.log("Feedback: vincentt feedback --wait");
|
|
121
|
-
console.log("\
|
|
140
|
+
console.log("\nStop: vincentt preview --stop (or Ctrl-C here).");
|
|
122
141
|
if (!p.appReady) {
|
|
123
142
|
console.log(`\nNote: the app on :${p.appPort} isn't responding yet — the URL may be blank until its build finishes.`);
|
|
124
143
|
}
|
|
@@ -201,7 +220,8 @@ function usage() {
|
|
|
201
220
|
` [--name <s>] [--slug <s>] [--preview] [--force]\n` +
|
|
202
221
|
` unbind Detach this directory from its project\n` +
|
|
203
222
|
` publish Upload the built dist; go live at <slug>.vincentt.app\n` +
|
|
204
|
-
` preview Serve + tunnel the app for on-device preview\n
|
|
223
|
+
` preview Serve + tunnel the app for on-device preview\n` +
|
|
224
|
+
` (prints a desktop QR page to scan) [--stop]\n\n` +
|
|
205
225
|
`Diagnostics (need a running preview)\n` +
|
|
206
226
|
` logs [--errors] Console logs from the device [--json]\n` +
|
|
207
227
|
` network fetch/XHR requests [--json]\n` +
|
package/dist/lib/lifecycle.d.ts
CHANGED
|
@@ -37,6 +37,20 @@ export interface PreviewStartResult {
|
|
|
37
37
|
}
|
|
38
38
|
export declare function previewStart(projectCwd: string, onLog: (msg: string) => void): Promise<PreviewStartResult>;
|
|
39
39
|
export declare function previewStop(): Promise<boolean>;
|
|
40
|
+
export interface PreviewStopOutcome {
|
|
41
|
+
stopped: boolean;
|
|
42
|
+
message: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Stop a preview started by a *different* process (the common case: `vincentt
|
|
46
|
+
* preview` runs in the background and the agent later wants it down). Reads the
|
|
47
|
+
* pidfile and signals the recorded pids rather than the in-process `activePreview`.
|
|
48
|
+
* SIGTERM to the preview process runs its graceful handler (which reaps the tunnel
|
|
49
|
+
* + backend DNS route); if that process is already gone, its cloudflared child may
|
|
50
|
+
* be orphaned to init, so kill the recorded tunnel pid directly. Either way, clear
|
|
51
|
+
* the (now stale) relay info.
|
|
52
|
+
*/
|
|
53
|
+
export declare function previewStopSignal(projectCwd: string): Promise<PreviewStopOutcome>;
|
|
40
54
|
export interface DiagQueryOptions {
|
|
41
55
|
sessionId?: string;
|
|
42
56
|
since?: number;
|
package/dist/lib/lifecycle.js
CHANGED
|
@@ -110,7 +110,7 @@ export async function projectPublish(projectCwd, opts = {}) {
|
|
|
110
110
|
commitSha,
|
|
111
111
|
});
|
|
112
112
|
return {
|
|
113
|
-
message: `Published v${result.version}. Live at ${result.liveUrl}
|
|
113
|
+
message: `Published v${result.version}. Live at ${result.liveUrl}`,
|
|
114
114
|
notBound: false,
|
|
115
115
|
version: result.version,
|
|
116
116
|
liveUrl: result.liveUrl,
|
|
@@ -135,6 +135,7 @@ export async function previewStart(projectCwd, onLog) {
|
|
|
135
135
|
relayPort: preview.relayPort,
|
|
136
136
|
url: preview.url,
|
|
137
137
|
pid: process.pid,
|
|
138
|
+
tunnelPid: preview.tunnelPid,
|
|
138
139
|
harnessPath: "/__harness",
|
|
139
140
|
});
|
|
140
141
|
return { preview, started: true };
|
|
@@ -151,6 +152,55 @@ export async function previewStop() {
|
|
|
151
152
|
await clearRelayInfo(cwd);
|
|
152
153
|
return true;
|
|
153
154
|
}
|
|
155
|
+
function isAlive(pid) {
|
|
156
|
+
try {
|
|
157
|
+
process.kill(pid, 0); // signal 0 = existence check, kills nothing
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Stop a preview started by a *different* process (the common case: `vincentt
|
|
166
|
+
* preview` runs in the background and the agent later wants it down). Reads the
|
|
167
|
+
* pidfile and signals the recorded pids rather than the in-process `activePreview`.
|
|
168
|
+
* SIGTERM to the preview process runs its graceful handler (which reaps the tunnel
|
|
169
|
+
* + backend DNS route); if that process is already gone, its cloudflared child may
|
|
170
|
+
* be orphaned to init, so kill the recorded tunnel pid directly. Either way, clear
|
|
171
|
+
* the (now stale) relay info.
|
|
172
|
+
*/
|
|
173
|
+
export async function previewStopSignal(projectCwd) {
|
|
174
|
+
const info = await readRelayInfo(projectCwd);
|
|
175
|
+
if (!info) {
|
|
176
|
+
return { stopped: false, message: "No preview is running for this directory." };
|
|
177
|
+
}
|
|
178
|
+
const previewAlive = info.pid !== undefined && isAlive(info.pid);
|
|
179
|
+
const reaped = [];
|
|
180
|
+
if (previewAlive) {
|
|
181
|
+
try {
|
|
182
|
+
process.kill(info.pid, "SIGTERM");
|
|
183
|
+
reaped.push("preview process");
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
/* raced with its own exit — fall through to tunnel cleanup */
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// Only force-kill the tunnel directly when no live preview process will reap it.
|
|
190
|
+
if (!previewAlive && info.tunnelPid !== undefined && isAlive(info.tunnelPid)) {
|
|
191
|
+
try {
|
|
192
|
+
process.kill(info.tunnelPid, "SIGKILL");
|
|
193
|
+
reaped.push("orphaned tunnel");
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
/* already gone */
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
await clearRelayInfo(projectCwd);
|
|
200
|
+
return reaped.length > 0
|
|
201
|
+
? { stopped: true, message: `Stopped preview (${reaped.join(", ")}).` }
|
|
202
|
+
: { stopped: false, message: "No live preview process found; cleared stale relay info." };
|
|
203
|
+
}
|
|
154
204
|
/** Resolve the relay client for a project's running preview, or throw a hint. */
|
|
155
205
|
async function relayForProject(projectCwd, override) {
|
|
156
206
|
if (override)
|
package/dist/lib/relayInfo.d.ts
CHANGED
|
@@ -5,6 +5,9 @@ export interface RelayInfo {
|
|
|
5
5
|
url: string;
|
|
6
6
|
/** The preview process pid, so a stale file can be detected. */
|
|
7
7
|
pid: number;
|
|
8
|
+
/** The cloudflared child pid, so `preview --stop` can reap an orphaned tunnel
|
|
9
|
+
* even after the preview process itself is gone (reparented to init). */
|
|
10
|
+
tunnelPid?: number;
|
|
8
11
|
/** WS/HTTP path prefix routed to the relay (default /__harness). */
|
|
9
12
|
harnessPath: string;
|
|
10
13
|
}
|
package/dist/preview/proxy.js
CHANGED
|
@@ -17,7 +17,16 @@ export function proxyWeb(req, res, targetPort) {
|
|
|
17
17
|
method: req.method,
|
|
18
18
|
headers: localHeaders(req.headers, targetPort),
|
|
19
19
|
}, (proxyRes) => {
|
|
20
|
-
|
|
20
|
+
// The preview tunnel is a *proxied* Cloudflare hostname, so its CDN edge
|
|
21
|
+
// caches static extensions (.js/.css) by default whenever the origin sends
|
|
22
|
+
// no Cache-Control — and esbuild's dev serve sends none. That made a phone
|
|
23
|
+
// reload after every edit serve a stale bundle. Force no-store on every
|
|
24
|
+
// preview response so neither the CDN nor the device caches it, and drop the
|
|
25
|
+
// validators that would otherwise drive a 304 back to a cached copy.
|
|
26
|
+
const headers = { ...proxyRes.headers, "cache-control": "no-store" };
|
|
27
|
+
delete headers.etag;
|
|
28
|
+
delete headers["last-modified"];
|
|
29
|
+
res.writeHead(proxyRes.statusCode ?? 502, headers);
|
|
21
30
|
proxyRes.pipe(res);
|
|
22
31
|
});
|
|
23
32
|
proxyReq.on("error", () => {
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Full HTML for the QR page — inline SVG QR + the tappable URL, no external assets. */
|
|
2
|
+
export declare function buildQrPage(url: string): Promise<string>;
|
|
3
|
+
/** Write the QR page under .vincentt/ and return a file:// URL to open on the desktop. */
|
|
4
|
+
export declare function writeQrPage(projectCwd: string, url: string): Promise<string>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// A tiny self-contained QR page for the preview URL. Rendering a QR in the terminal
|
|
2
|
+
// is unreliable (font/rows vary by emulator, long tunnel URLs don't scan), so instead
|
|
3
|
+
// `vincentt preview` writes this page and prints a file:// link: the developer opens
|
|
4
|
+
// it on their desktop and scans the big SVG QR with their phone. Written under the
|
|
5
|
+
// gitignored .vincentt/ dir, so it never rides a commit.
|
|
6
|
+
import { promises as fs } from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { pathToFileURL } from "node:url";
|
|
9
|
+
import QRCode from "qrcode";
|
|
10
|
+
const VINCENTT_DIR = ".vincentt";
|
|
11
|
+
const QR_FILE = "preview-qr.html";
|
|
12
|
+
/** Full HTML for the QR page — inline SVG QR + the tappable URL, no external assets. */
|
|
13
|
+
export async function buildQrPage(url) {
|
|
14
|
+
// Medium ECC + a quiet margin keeps a ~50-char tunnel URL comfortably scannable.
|
|
15
|
+
const svg = await QRCode.toString(url, {
|
|
16
|
+
type: "svg",
|
|
17
|
+
margin: 2,
|
|
18
|
+
errorCorrectionLevel: "M",
|
|
19
|
+
});
|
|
20
|
+
const safe = escapeHtml(url);
|
|
21
|
+
return `<!doctype html>
|
|
22
|
+
<html lang="en">
|
|
23
|
+
<head>
|
|
24
|
+
<meta charset="utf-8" />
|
|
25
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
26
|
+
<title>Scan to preview — ${safe}</title>
|
|
27
|
+
<style>
|
|
28
|
+
:root { color-scheme: light dark; }
|
|
29
|
+
body { margin:0; min-height:100vh; display:grid; place-items:center;
|
|
30
|
+
font:16px/1.5 system-ui, sans-serif; background:#fff; color:#111; }
|
|
31
|
+
@media (prefers-color-scheme: dark){ body{ background:#0b0b0c; color:#f4f4f5; } }
|
|
32
|
+
.card { text-align:center; padding:2rem; max-width:min(90vw,420px); }
|
|
33
|
+
.qr { width:min(72vw,320px); aspect-ratio:1; margin:0 auto 1.25rem; }
|
|
34
|
+
.qr svg { width:100%; height:100%; }
|
|
35
|
+
h1 { font-size:1.05rem; font-weight:600; margin:0 0 .35rem; }
|
|
36
|
+
p { margin:0 0 1.25rem; opacity:.7; }
|
|
37
|
+
a.url { display:inline-block; word-break:break-all; font:500 .95rem/1.4 ui-monospace,monospace;
|
|
38
|
+
color:#2563eb; text-decoration:none; }
|
|
39
|
+
@media (prefers-color-scheme: dark){ a.url{ color:#60a5fa; } }
|
|
40
|
+
</style>
|
|
41
|
+
</head>
|
|
42
|
+
<body>
|
|
43
|
+
<div class="card">
|
|
44
|
+
<div class="qr">${svg}</div>
|
|
45
|
+
<h1>Scan to open the preview</h1>
|
|
46
|
+
<p>Point your phone's camera at the code.</p>
|
|
47
|
+
<a class="url" href="${safe}">${safe}</a>
|
|
48
|
+
</div>
|
|
49
|
+
</body>
|
|
50
|
+
</html>
|
|
51
|
+
`;
|
|
52
|
+
}
|
|
53
|
+
/** Write the QR page under .vincentt/ and return a file:// URL to open on the desktop. */
|
|
54
|
+
export async function writeQrPage(projectCwd, url) {
|
|
55
|
+
const p = path.join(projectCwd, VINCENTT_DIR, QR_FILE);
|
|
56
|
+
await fs.mkdir(path.dirname(p), { recursive: true });
|
|
57
|
+
await fs.writeFile(p, await buildQrPage(url), "utf8");
|
|
58
|
+
return pathToFileURL(p).href;
|
|
59
|
+
}
|
|
60
|
+
function escapeHtml(s) {
|
|
61
|
+
return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] ?? c);
|
|
62
|
+
}
|
package/dist/preview/runner.d.ts
CHANGED
|
@@ -16,7 +16,9 @@ export interface StartPreviewOptions {
|
|
|
16
16
|
/** stdio for the app dev serve. "ignore" (default) keeps an MCP server's stdout
|
|
17
17
|
* clean; a CLI can pass "inherit" to surface build output. */
|
|
18
18
|
appStdio?: "ignore" | "inherit";
|
|
19
|
-
/** How long to wait for cloudflared to register before giving up (default
|
|
19
|
+
/** How long to wait for cloudflared to register before giving up (default 90s).
|
|
20
|
+
* Generous because on a network that blocks the QUIC edge, cloudflared spends
|
|
21
|
+
* ~20-30s failing QUIC before it falls back to the http2 edge, which does work. */
|
|
20
22
|
registerTimeoutMs?: number;
|
|
21
23
|
/** How long to wait for the app to serve before returning anyway (default 8s).
|
|
22
24
|
* Kept short so an MCP tool call stays within strict clients' ~30s timeout;
|
|
@@ -32,6 +34,9 @@ export interface RunningPreview {
|
|
|
32
34
|
relayPort: number;
|
|
33
35
|
/** The app dev-serve port the tunnel ultimately serves. */
|
|
34
36
|
appPort: number;
|
|
37
|
+
/** The cloudflared child pid, recorded so a separate `preview --stop` can reap
|
|
38
|
+
* an orphaned tunnel by pid. Undefined if the spawn never yielded one. */
|
|
39
|
+
tunnelPid?: number;
|
|
35
40
|
/** Whether the app was responding when we returned (false = URL may be blank). */
|
|
36
41
|
appReady: boolean;
|
|
37
42
|
/** Tear down tunnel (+ DNS route), app dev serve, front proxy, and relay. */
|
|
@@ -40,6 +45,9 @@ export interface RunningPreview {
|
|
|
40
45
|
export declare function startPreview(opts: StartPreviewOptions): Promise<RunningPreview>;
|
|
41
46
|
/**
|
|
42
47
|
* Resolve once cloudflared reports a live edge connection, reject if it exits
|
|
43
|
-
* first or never registers.
|
|
48
|
+
* first or never registers. On failure the rejection carries cloudflared's own
|
|
49
|
+
* recent output — otherwise "did not register" hides the real cause (an invalid
|
|
50
|
+
* run token, or a network that blocks the QUIC edge, the two common culprits).
|
|
51
|
+
* `onLine` streams each log line out live. Exported for unit testing.
|
|
44
52
|
*/
|
|
45
|
-
export declare function waitForRegister(tunnel: ChildProcess, timeoutMs: number): Promise<void>;
|
|
53
|
+
export declare function waitForRegister(tunnel: ChildProcess, timeoutMs: number, onLine?: (line: string) => void): Promise<void>;
|
package/dist/preview/runner.js
CHANGED
|
@@ -16,7 +16,7 @@ import { startSessionTunnel } from "./tunnel.js";
|
|
|
16
16
|
import { proxyWeb, proxyWs } from "./proxy.js";
|
|
17
17
|
import { getFreePort, isPortListening, waitForApp } from "./net.js";
|
|
18
18
|
export async function startPreview(opts) {
|
|
19
|
-
const { projectCwd, harnessPath = "/__harness", devCommand = ["npm", "run", "dev"], appStdio = "ignore", registerTimeoutMs =
|
|
19
|
+
const { projectCwd, harnessPath = "/__harness", devCommand = ["npm", "run", "dev"], appStdio = "ignore", registerTimeoutMs = 90_000, appReadyTimeoutMs = 8_000, onLog = () => undefined, } = opts;
|
|
20
20
|
// Resolve ports. The app port defaults to 5173; the relay + front ports are
|
|
21
21
|
// harness-internal, so auto-pick free ones (the relay port is returned for the
|
|
22
22
|
// MCP diag_* tools to point at) instead of colliding on fixed defaults.
|
|
@@ -70,14 +70,14 @@ export async function startPreview(opts) {
|
|
|
70
70
|
tunnel = spawn(cloudflaredBin, ["tunnel", "run", "--token", session.runToken], {
|
|
71
71
|
stdio: ["ignore", "pipe", "pipe"],
|
|
72
72
|
});
|
|
73
|
-
await waitForRegister(tunnel, registerTimeoutMs);
|
|
73
|
+
await waitForRegister(tunnel, registerTimeoutMs, (line) => onLog(`[cloudflared] ${line}`));
|
|
74
74
|
// Don't hand back a live URL that serves a blank page: wait for the app to
|
|
75
75
|
// actually respond. Non-fatal — a slow build still returns, flagged not-ready.
|
|
76
76
|
const appReady = reuseApp ? true : await waitForApp(appPort, appReadyTimeoutMs);
|
|
77
77
|
if (!appReady) {
|
|
78
78
|
onLog(`app on :${appPort} isn't responding yet — the URL may be blank until it builds`);
|
|
79
79
|
}
|
|
80
|
-
return { url: session.url, relayPort, appPort, appReady, stop };
|
|
80
|
+
return { url: session.url, relayPort, appPort, tunnelPid: tunnel.pid, appReady, stop };
|
|
81
81
|
}
|
|
82
82
|
catch (err) {
|
|
83
83
|
await stop();
|
|
@@ -86,11 +86,15 @@ export async function startPreview(opts) {
|
|
|
86
86
|
}
|
|
87
87
|
/**
|
|
88
88
|
* Resolve once cloudflared reports a live edge connection, reject if it exits
|
|
89
|
-
* first or never registers.
|
|
89
|
+
* first or never registers. On failure the rejection carries cloudflared's own
|
|
90
|
+
* recent output — otherwise "did not register" hides the real cause (an invalid
|
|
91
|
+
* run token, or a network that blocks the QUIC edge, the two common culprits).
|
|
92
|
+
* `onLine` streams each log line out live. Exported for unit testing.
|
|
90
93
|
*/
|
|
91
|
-
export function waitForRegister(tunnel, timeoutMs) {
|
|
94
|
+
export function waitForRegister(tunnel, timeoutMs, onLine) {
|
|
92
95
|
return new Promise((resolve, reject) => {
|
|
93
96
|
let settled = false;
|
|
97
|
+
const recent = [];
|
|
94
98
|
const finish = (fn) => {
|
|
95
99
|
if (settled)
|
|
96
100
|
return;
|
|
@@ -98,14 +102,25 @@ export function waitForRegister(tunnel, timeoutMs) {
|
|
|
98
102
|
clearTimeout(timer);
|
|
99
103
|
fn();
|
|
100
104
|
};
|
|
105
|
+
const tail = () => {
|
|
106
|
+
const lines = recent.slice(-15).join("\n").trim();
|
|
107
|
+
return lines ? `\ncloudflared output:\n${lines}` : "";
|
|
108
|
+
};
|
|
101
109
|
const scan = (buf) => {
|
|
102
|
-
|
|
110
|
+
const text = String(buf);
|
|
111
|
+
for (const line of text.split(/\r?\n/)) {
|
|
112
|
+
if (!line.trim())
|
|
113
|
+
continue;
|
|
114
|
+
recent.push(line);
|
|
115
|
+
onLine?.(line);
|
|
116
|
+
}
|
|
117
|
+
if (/Registered tunnel connection|Connection [^ ]+ registered/.test(text)) {
|
|
103
118
|
finish(resolve);
|
|
104
119
|
}
|
|
105
120
|
};
|
|
106
121
|
tunnel.stdout?.on("data", scan);
|
|
107
122
|
tunnel.stderr?.on("data", scan);
|
|
108
|
-
tunnel.on("exit", (code) => finish(() => reject(new Error(`cloudflared exited before registering (code ${code})
|
|
109
|
-
const timer = setTimeout(() => finish(() => reject(new Error(`cloudflared did not register within ${timeoutMs}ms
|
|
123
|
+
tunnel.on("exit", (code) => finish(() => reject(new Error(`cloudflared exited before registering (code ${code}).${tail()}`))));
|
|
124
|
+
const timer = setTimeout(() => finish(() => reject(new Error(`cloudflared did not register within ${timeoutMs}ms.${tail()}`))), timeoutMs);
|
|
110
125
|
});
|
|
111
126
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincentt-xr/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Vincentt CLI — the vincentt command-line dev loop (login, create, publish, preview, diagnostics, feedback, relay, mcp)",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -26,11 +26,13 @@
|
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
28
28
|
"cloudflared": "^0.7.1",
|
|
29
|
+
"qrcode": "^1.5.4",
|
|
29
30
|
"ws": "^8.18.0",
|
|
30
31
|
"zod": "^3.23.8",
|
|
31
32
|
"@vincentt-xr/harness": "1.0.0"
|
|
32
33
|
},
|
|
33
34
|
"devDependencies": {
|
|
35
|
+
"@types/qrcode": "^1.5.5",
|
|
34
36
|
"@types/ws": "^8.5.13"
|
|
35
37
|
},
|
|
36
38
|
"scripts": {
|