@vincentt-xr/cli 0.1.4 → 0.2.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 +94 -0
- package/dist/cli/args.d.ts +13 -0
- package/dist/cli/args.js +41 -0
- package/dist/cli/index.js +362 -92
- package/dist/lib/backend.d.ts +45 -5
- package/dist/lib/backend.js +99 -18
- package/dist/lib/diagnostics.d.ts +27 -2
- package/dist/lib/diagnostics.js +73 -8
- package/dist/lib/initCommand.d.ts +14 -0
- package/dist/lib/initCommand.js +75 -0
- package/dist/lib/lifecycle.d.ts +73 -15
- package/dist/lib/lifecycle.js +111 -114
- package/dist/lib/org.d.ts +29 -0
- package/dist/lib/org.js +45 -0
- package/dist/lib/projects.d.ts +82 -0
- package/dist/lib/projects.js +217 -0
- package/dist/lib/relayInfo.d.ts +41 -3
- package/dist/lib/relayInfo.js +94 -0
- package/dist/lib/whoami.d.ts +11 -0
- package/dist/lib/whoami.js +38 -0
- package/dist/login/authBackend.d.ts +30 -0
- package/dist/login/authBackend.js +80 -0
- package/dist/login/guard.d.ts +13 -0
- package/dist/login/guard.js +17 -0
- package/dist/login/loginCommand.d.ts +39 -0
- package/dist/login/loginCommand.js +72 -0
- package/dist/login/pkce.d.ts +47 -0
- package/dist/login/pkce.js +106 -0
- package/dist/login/pkceLogin.d.ts +53 -0
- package/dist/login/pkceLogin.js +152 -0
- package/dist/mcp/server.js +47 -20
- package/dist/preview/api.d.ts +84 -0
- package/dist/preview/api.js +174 -0
- package/dist/preview/command.d.ts +96 -0
- package/dist/preview/command.js +276 -0
- package/dist/preview/copy.d.ts +209 -0
- package/dist/preview/copy.js +444 -0
- package/dist/preview/index.d.ts +2 -3
- package/dist/preview/index.js +5 -6
- package/dist/preview/proxy.d.ts +12 -1
- package/dist/preview/proxy.js +17 -7
- package/dist/preview/qr.d.ts +30 -0
- package/dist/preview/qr.js +89 -0
- package/dist/preview/runner.d.ts +155 -30
- package/dist/preview/runner.js +219 -68
- package/dist/preview/stream.d.ts +52 -0
- package/dist/preview/stream.js +171 -0
- package/dist/preview/viewers.d.ts +113 -0
- package/dist/preview/viewers.js +242 -0
- package/dist/relay/server.d.ts +32 -0
- package/dist/relay/server.js +80 -12
- package/dist/relay/store.d.ts +32 -3
- package/dist/relay/store.js +76 -22
- package/dist/relay/testers.d.ts +90 -0
- package/dist/relay/testers.js +144 -0
- package/dist/shared/authError.d.ts +1 -0
- package/dist/shared/authError.js +11 -0
- package/dist/shared/config.d.ts +73 -5
- package/dist/shared/config.js +133 -20
- package/package.json +3 -7
- package/dist/cli/deprecated/harness-mcp.d.ts +0 -2
- package/dist/cli/deprecated/harness-mcp.js +0 -7
- package/dist/cli/deprecated/harness-relay.d.ts +0 -2
- package/dist/cli/deprecated/harness-relay.js +0 -7
- package/dist/cli/deprecated/harness.d.ts +0 -2
- package/dist/cli/deprecated/harness.js +0 -7
- package/dist/login/login.d.ts +0 -34
- package/dist/login/login.js +0 -148
- package/dist/preview/cloudflared.d.ts +0 -13
- package/dist/preview/cloudflared.js +0 -46
- package/dist/preview/tunnel.d.ts +0 -14
- package/dist/preview/tunnel.js +0 -28
package/dist/cli/index.js
CHANGED
|
@@ -5,20 +5,34 @@
|
|
|
5
5
|
//
|
|
6
6
|
// Verb groups:
|
|
7
7
|
// auth login · logout · whoami
|
|
8
|
-
//
|
|
8
|
+
// project init · create · link · unlink
|
|
9
|
+
// dev loop publish · preview
|
|
9
10
|
// diagnostics logs · network · trace
|
|
10
11
|
// reverse feedback (--wait | --watch)
|
|
11
12
|
// servers relay · mcp
|
|
12
13
|
import os from "node:os";
|
|
13
14
|
import path from "node:path";
|
|
14
|
-
import {
|
|
15
|
+
import { runPkceLogin } from "../login/pkceLogin.js";
|
|
16
|
+
import { httpAuthBackend } from "../login/authBackend.js";
|
|
17
|
+
import { loginCommand } from "../login/loginCommand.js";
|
|
15
18
|
import { ensureLoggedIn } from "../login/ensure.js";
|
|
16
|
-
import { clearMachineToken,
|
|
17
|
-
import { getMe } from "../lib/backend.js";
|
|
19
|
+
import { clearMachineToken, loadMachineConfig, locateProjectBinding, resolveConfig, MACHINE_CONFIG_PATH, } from "../shared/config.js";
|
|
20
|
+
import { getMe, ProjectUnreachableError } from "../lib/backend.js";
|
|
21
|
+
import { initCommand } from "../lib/initCommand.js";
|
|
22
|
+
import { linkCommand, printUnreachable, unlinkCommand } from "../lib/projects.js";
|
|
23
|
+
import { whoamiCommand } from "../lib/whoami.js";
|
|
18
24
|
import { diagQuery, diagRead, previewStart, previewStop, previewStopSignal, projectCreate, projectPublish, } from "../lib/lifecycle.js";
|
|
19
|
-
import {
|
|
25
|
+
import { PreviewUi } from "../preview/command.js";
|
|
26
|
+
import { httpRelayClient } from "../lib/diagnostics.js";
|
|
27
|
+
import { readLivePreview } from "../lib/relayInfo.js";
|
|
28
|
+
import { mintRateLimited, notSignedIn, previewConflict, startBlock, } from "../preview/copy.js";
|
|
29
|
+
import { renderTerminalQr, shouldRenderQr } from "../preview/qr.js";
|
|
30
|
+
import { PreviewConflictError, PreviewRateLimitedError } from "../preview/api.js";
|
|
20
31
|
import { fetchNext, readCursor, writeCursor } from "../lib/feedback.js";
|
|
21
|
-
|
|
32
|
+
import { isHelpInvocation, positionalArgs } from "./args.js";
|
|
33
|
+
// The console origin serves /cli-auth (the browser target for `vincentt login`). VINCENTT_
|
|
34
|
+
// CONSOLE_URL overrides for dev/dogfood; VINCENTT_EDITOR_URL is honored as a legacy alias.
|
|
35
|
+
const DEFAULT_CONSOLE_URL = "https://vincentt.app";
|
|
22
36
|
const DEFAULT_API_URL = "https://api.vincentt.studio";
|
|
23
37
|
// ---- arg helpers ------------------------------------------------------------
|
|
24
38
|
const argv = process.argv.slice(2);
|
|
@@ -37,20 +51,54 @@ function projectDir() {
|
|
|
37
51
|
const dir = flag("--dir");
|
|
38
52
|
return dir ? path.resolve(dir) : process.cwd();
|
|
39
53
|
}
|
|
54
|
+
/** The nth bare argument (`vincentt link <id>`), skipping flags and their values. */
|
|
55
|
+
function positional(n) {
|
|
56
|
+
return positionalArgs(argv)[n];
|
|
57
|
+
}
|
|
40
58
|
// ---- auth -------------------------------------------------------------------
|
|
41
|
-
|
|
42
|
-
const
|
|
59
|
+
function resolveAuthUrls() {
|
|
60
|
+
const consoleUrl = flag("--console") ??
|
|
61
|
+
flag("--editor") ??
|
|
62
|
+
process.env.VINCENTT_CONSOLE_URL ??
|
|
63
|
+
process.env.VINCENTT_EDITOR_URL ??
|
|
64
|
+
DEFAULT_CONSOLE_URL;
|
|
43
65
|
const apiUrl = flag("--api") ?? process.env.VINCENTT_API_URL ?? DEFAULT_API_URL;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
66
|
+
return { consoleUrl, apiUrl };
|
|
67
|
+
}
|
|
68
|
+
/** The PKCE loopback + code-exchange login (f1). The raw PAT never rides a redirect query. */
|
|
69
|
+
async function runLoginFlow() {
|
|
70
|
+
const { consoleUrl, apiUrl } = resolveAuthUrls();
|
|
71
|
+
return runPkceLogin({
|
|
72
|
+
consoleUrl,
|
|
48
73
|
apiUrl,
|
|
49
|
-
|
|
50
|
-
onUrl: (url) => console.log(`\nIf your browser didn't open, visit:\n ${url}\n`),
|
|
74
|
+
onProgress: (line) => console.log(line),
|
|
51
75
|
});
|
|
52
|
-
|
|
53
|
-
|
|
76
|
+
}
|
|
77
|
+
/** `vincentt login` [--force] — the L1/L2/L4 transcripts. */
|
|
78
|
+
async function login() {
|
|
79
|
+
const { apiUrl } = resolveAuthUrls();
|
|
80
|
+
const code = await loginCommand({
|
|
81
|
+
print: (line) => console.log(line),
|
|
82
|
+
currentToken,
|
|
83
|
+
whoAmI: async () => {
|
|
84
|
+
const pat = await currentToken();
|
|
85
|
+
if (!pat)
|
|
86
|
+
return undefined;
|
|
87
|
+
try {
|
|
88
|
+
return await httpAuthBackend(apiUrl).me(pat);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
runLogin: runLoginFlow,
|
|
95
|
+
clearToken: async () => {
|
|
96
|
+
await clearMachineToken();
|
|
97
|
+
},
|
|
98
|
+
home: os.homedir(),
|
|
99
|
+
configPath: MACHINE_CONFIG_PATH,
|
|
100
|
+
}, { force: has("--force") });
|
|
101
|
+
process.exitCode = code;
|
|
54
102
|
}
|
|
55
103
|
/** The token that would authenticate a machine call, if any (env wins over file). */
|
|
56
104
|
async function currentToken() {
|
|
@@ -68,41 +116,47 @@ async function logout() {
|
|
|
68
116
|
const { path: p, hadToken } = await clearMachineToken();
|
|
69
117
|
console.log(hadToken ? `✓ Signed out. Token removed from ${p}` : "Already signed out.");
|
|
70
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* `whoami` reads the MACHINE config's API host, never the folder's binding — it
|
|
121
|
+
* answers "who am I", and reading the binding would make it answer a question
|
|
122
|
+
* about the folder instead.
|
|
123
|
+
*/
|
|
71
124
|
async function whoami() {
|
|
72
125
|
const machine = await loadMachineConfig();
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
126
|
+
process.exitCode = await whoamiCommand({
|
|
127
|
+
print: (l) => console.log(l),
|
|
128
|
+
config: async () => {
|
|
129
|
+
const apiUrl = process.env.VINCENTT_API_URL ?? machine.apiUrl ?? DEFAULT_API_URL;
|
|
130
|
+
const pat = process.env.VINCENTT_PAT ?? machine.pat;
|
|
131
|
+
if (!pat)
|
|
132
|
+
return undefined;
|
|
133
|
+
const cfg = { apiUrl: apiUrl.replace(/\/$/, ""), pat };
|
|
134
|
+
return cfg;
|
|
135
|
+
},
|
|
136
|
+
getMe,
|
|
137
|
+
org: {
|
|
138
|
+
flag: flag("--org"),
|
|
139
|
+
env: process.env.VINCENTT_ORG,
|
|
140
|
+
config: machine.activeOrg,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
// ---- project ----------------------------------------------------------------
|
|
145
|
+
async function init() {
|
|
146
|
+
process.exitCode = await initCommand({ print: (l) => console.log(l), cwd: process.cwd() }, { dir: positional(1), skipInstall: has("--no-install") });
|
|
92
147
|
}
|
|
93
|
-
// ---- dev loop ---------------------------------------------------------------
|
|
94
148
|
async function create() {
|
|
95
149
|
await requireLogin();
|
|
96
150
|
const result = await projectCreate(projectDir(), {
|
|
97
151
|
name: flag("--name"),
|
|
98
152
|
slug: flag("--slug"),
|
|
99
|
-
|
|
153
|
+
org: flag("--org"),
|
|
100
154
|
force: has("--force"),
|
|
101
|
-
|
|
155
|
+
allowCustomApi: has("--allow-custom-api"),
|
|
156
|
+
onLine: (l) => console.log(l),
|
|
102
157
|
});
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
process.exitCode = 1;
|
|
158
|
+
if (result.exitCode !== 0) {
|
|
159
|
+
process.exitCode = result.exitCode;
|
|
106
160
|
return;
|
|
107
161
|
}
|
|
108
162
|
// Chain straight into the dev loop when asked.
|
|
@@ -111,68 +165,264 @@ async function create() {
|
|
|
111
165
|
await preview();
|
|
112
166
|
}
|
|
113
167
|
}
|
|
114
|
-
async function
|
|
115
|
-
|
|
116
|
-
|
|
168
|
+
async function link() {
|
|
169
|
+
await requireLogin();
|
|
170
|
+
const cwd = projectDir();
|
|
171
|
+
process.exitCode = await linkCommand({
|
|
172
|
+
print: (l) => console.log(l),
|
|
173
|
+
cwd,
|
|
174
|
+
config: () => resolveConfig(cwd, {
|
|
175
|
+
allowCustomApi: has("--allow-custom-api"),
|
|
176
|
+
warn: (l) => console.log(l),
|
|
177
|
+
}),
|
|
178
|
+
}, positional(1));
|
|
179
|
+
}
|
|
180
|
+
async function unlink() {
|
|
181
|
+
const cwd = projectDir();
|
|
182
|
+
process.exitCode = await unlinkCommand({
|
|
183
|
+
print: (l) => console.log(l),
|
|
184
|
+
cwd,
|
|
185
|
+
// unlink makes no server call, so this is never invoked.
|
|
186
|
+
config: () => resolveConfig(cwd),
|
|
187
|
+
});
|
|
117
188
|
}
|
|
118
189
|
async function publish() {
|
|
119
190
|
await requireLogin();
|
|
120
191
|
const result = await projectPublish(projectDir(), {
|
|
121
192
|
distDir: flag("--dist"),
|
|
122
193
|
note: flag("--note"),
|
|
194
|
+
allowCustomApi: has("--allow-custom-api"),
|
|
195
|
+
warn: (l) => console.log(l),
|
|
123
196
|
});
|
|
124
197
|
console.log(result.message);
|
|
125
198
|
if (result.notBound)
|
|
126
199
|
process.exitCode = 1;
|
|
127
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* `vincentt preview` — Flows L13-L21.
|
|
203
|
+
*
|
|
204
|
+
* The verb does something a creator could easily misread, so the FOLLOW path
|
|
205
|
+
* says what it is doing in its first line: in a folder with a live preview,
|
|
206
|
+
* `vincentt preview` ATTACHES to it and follows. It does not mint, does not
|
|
207
|
+
* print a QR, and does not supersede. A second preview for the same tree is
|
|
208
|
+
* created only when the working tree differs.
|
|
209
|
+
*/
|
|
128
210
|
async function preview() {
|
|
129
|
-
|
|
130
|
-
// via its pidfile — no need to hunt for the cloudflared pid by hand.
|
|
211
|
+
const cwd = projectDir();
|
|
131
212
|
if (has("--stop")) {
|
|
132
|
-
const { message } = await previewStopSignal(
|
|
213
|
+
const { message } = await previewStopSignal(cwd);
|
|
133
214
|
console.log(message);
|
|
134
215
|
return;
|
|
135
216
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (!
|
|
139
|
-
|
|
217
|
+
// The gate is LOCAL and NOTHING IS ATTEMPTED: no spinner, no partial mint, no
|
|
218
|
+
// session left in `pending`. The command stops before it touches the network.
|
|
219
|
+
if (!(await currentToken())) {
|
|
220
|
+
for (const l of notSignedIn())
|
|
221
|
+
console.log(l);
|
|
222
|
+
process.exitCode = 1;
|
|
140
223
|
return;
|
|
141
224
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
225
|
+
const filterViewer = flag("--viewer");
|
|
226
|
+
const filterTester = flag("--tester");
|
|
227
|
+
const color = Boolean(process.stdout.isTTY) && process.env.NO_COLOR === undefined;
|
|
228
|
+
const ui = new PreviewUi({
|
|
229
|
+
print: (l) => console.log(l),
|
|
230
|
+
now: () => Date.now(),
|
|
231
|
+
targetUrl: `http://localhost:${numFlag("--port") ?? 5173}`,
|
|
232
|
+
color,
|
|
233
|
+
filterTester,
|
|
234
|
+
filterViewer,
|
|
235
|
+
});
|
|
236
|
+
// A preview already running for this folder — in ANOTHER process — is
|
|
237
|
+
// FOLLOWED, not superseded. `vincentt preview` in a folder with a live
|
|
238
|
+
// preview attaches to it: it does not mint, does not print a QR, and does not
|
|
239
|
+
// take the link away from whoever is holding a phone.
|
|
240
|
+
//
|
|
241
|
+
// The marker is VALIDATED first. It only ever proved a preview was STARTED,
|
|
242
|
+
// and a crashed CLI, a kill -9 or a reboot all leave it behind — an unvalidated
|
|
243
|
+
// one short-circuited the mint into a dead end whose only cure was deleting a
|
|
244
|
+
// dotfile the creator does not know exists. A stale marker is cleared and
|
|
245
|
+
// treated as absent, which lands on the mint the creator actually asked for.
|
|
246
|
+
const existing = await readLivePreview(cwd);
|
|
247
|
+
if (existing !== undefined) {
|
|
248
|
+
await followPreview(existing, ui, filterTester, filterViewer);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
let ending = null;
|
|
252
|
+
let resolveDone;
|
|
253
|
+
const done = new Promise((r) => {
|
|
254
|
+
resolveDone = r;
|
|
255
|
+
});
|
|
256
|
+
let running;
|
|
147
257
|
try {
|
|
148
|
-
const
|
|
149
|
-
|
|
258
|
+
const started = await previewStart(cwd, (m) => console.error(`[preview] ${m}`), {
|
|
259
|
+
appPort: numFlag("--port"),
|
|
260
|
+
appStdio: has("--verbose") ? "inherit" : "ignore",
|
|
261
|
+
onViewer: (label, uaClass) => ui.viewerSeen(label, uaClass),
|
|
262
|
+
onViewerGone: (label) => ui.viewerGone(label),
|
|
263
|
+
onEvents: (events, tags) => ui.events(events, tags),
|
|
264
|
+
onCapReached: () => ui.capReached(),
|
|
265
|
+
onTunnelState: (state) => ui.tunnel(state),
|
|
266
|
+
onOriginError: () => ui.originError(),
|
|
267
|
+
onOriginRecovered: () => ui.originRecovered(),
|
|
268
|
+
onExpiring: () => ui.checkHardMax(running?.expiresAt ?? ""),
|
|
269
|
+
onEnded: (reason) => {
|
|
270
|
+
ending = reason;
|
|
271
|
+
resolveDone?.();
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
running = started.preview;
|
|
275
|
+
if (!started.started) {
|
|
276
|
+
// Already running IN THIS PROCESS. The mint path never reaches here.
|
|
277
|
+
console.log(`A preview is already running:\n${running.url}`);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
150
280
|
}
|
|
151
|
-
catch {
|
|
152
|
-
|
|
281
|
+
catch (err) {
|
|
282
|
+
for (const l of previewFailureLines(err))
|
|
283
|
+
console.log(l);
|
|
284
|
+
process.exitCode = 1;
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const qr = shouldRenderQr({
|
|
288
|
+
isTTY: Boolean(process.stdout.isTTY),
|
|
289
|
+
noQr: has("--no-qr"),
|
|
290
|
+
columns: process.stdout.columns,
|
|
291
|
+
})
|
|
292
|
+
? await renderTerminalQr(running.url).catch(() => undefined)
|
|
293
|
+
: undefined;
|
|
294
|
+
for (const l of startBlock({
|
|
295
|
+
projectName: await projectDisplayName(cwd),
|
|
296
|
+
orgName: await activeOrgName(),
|
|
297
|
+
targetUrl: `http://localhost:${running.appPort}`,
|
|
298
|
+
url: running.url,
|
|
299
|
+
qr,
|
|
300
|
+
expiresInHours: 12,
|
|
301
|
+
})) {
|
|
302
|
+
console.log(l);
|
|
303
|
+
}
|
|
304
|
+
// The hard-max warning is checked on a slow tick rather than scheduled at a
|
|
305
|
+
// fixed offset: a laptop that slept through the scheduled instant would never
|
|
306
|
+
// fire a one-shot timer, and the 30-minute warning is the one the creator
|
|
307
|
+
// plans a demo around.
|
|
308
|
+
const ceiling = setInterval(() => ui.checkHardMax(running.expiresAt), 60_000);
|
|
309
|
+
ceiling.unref?.();
|
|
310
|
+
const stop = () => {
|
|
311
|
+
ui.stopping();
|
|
312
|
+
void previewStop().then(() => {
|
|
313
|
+
ui.stopped();
|
|
314
|
+
resolveDone?.();
|
|
315
|
+
});
|
|
316
|
+
};
|
|
317
|
+
process.on("SIGINT", stop);
|
|
318
|
+
process.on("SIGTERM", stop);
|
|
319
|
+
await done;
|
|
320
|
+
clearInterval(ceiling);
|
|
321
|
+
if (ending !== null) {
|
|
322
|
+
ui.endedBy(ending, (await activeOrgName()) || "your organization");
|
|
323
|
+
await previewStop();
|
|
324
|
+
process.exitCode = 1;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Flow L16 — follow a preview running in another terminal.
|
|
329
|
+
*
|
|
330
|
+
* It says what it is doing in its FIRST line, because this is the one flow
|
|
331
|
+
* where the CLI's verb does something the creator could easily misread. It
|
|
332
|
+
* polls the relay rather than opening a second tunnel: a second tunnel would be
|
|
333
|
+
* a second session, which is the "two live URLs, one stale" the design forbids.
|
|
334
|
+
*/
|
|
335
|
+
async function followPreview(info, ui, filterTester, filterViewer) {
|
|
336
|
+
console.log("→ Attaching to the preview running in this folder");
|
|
337
|
+
const following = filterTester ?? filterViewer;
|
|
338
|
+
if (following !== undefined) {
|
|
339
|
+
console.log(`✓ Following ${following}`);
|
|
153
340
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
console.log("\nStop: vincentt preview --stop (or Ctrl-C here).");
|
|
157
|
-
if (!p.appReady) {
|
|
158
|
-
console.log(`\nNote: the app on :${p.appPort} isn't responding yet — the URL may be blank until its build finishes.`);
|
|
341
|
+
else {
|
|
342
|
+
console.log("✓ Following this preview");
|
|
159
343
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
process.on("SIGINT", stop);
|
|
168
|
-
process.on("SIGTERM", stop);
|
|
344
|
+
console.log("");
|
|
345
|
+
const relay = httpRelayClient(`http://localhost:${info.relayPort}`);
|
|
346
|
+
let since = -1;
|
|
347
|
+
let warnedMissing = false;
|
|
348
|
+
let stop = false;
|
|
349
|
+
process.on("SIGINT", () => {
|
|
350
|
+
stop = true;
|
|
169
351
|
});
|
|
352
|
+
while (!stop) {
|
|
353
|
+
try {
|
|
354
|
+
const result = await relay.query({ since, tester: filterTester, viewer: filterViewer });
|
|
355
|
+
// An unknown id is a FACT, not an error — and the remedy goes on the same
|
|
356
|
+
// line. Said once rather than on every poll, or a two-second loop becomes
|
|
357
|
+
// a wall of identical advisories.
|
|
358
|
+
if (!warnedMissing &&
|
|
359
|
+
filterTester !== undefined &&
|
|
360
|
+
result.testers.length > 0 &&
|
|
361
|
+
!result.testers.includes(filterTester)) {
|
|
362
|
+
warnedMissing = true;
|
|
363
|
+
console.log(` ! No ${filterTester} on this preview right now. Connected: ${result.testers.join(", ")}.`);
|
|
364
|
+
}
|
|
365
|
+
// Both tags ride ALONGSIDE the events, index-parallel, so an unattributed
|
|
366
|
+
// event is dropped from the terminal rather than printed under an
|
|
367
|
+
// invented id that would read as a real tester.
|
|
368
|
+
result.events.forEach((e, i) => ui.events([e], { tester: result.eventTesters?.[i], viewer: result.eventViewers?.[i] }, { attribute: false }));
|
|
369
|
+
if (result.latestSeq > since)
|
|
370
|
+
since = result.latestSeq;
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
// The preview stopped, or its relay is gone. Either way the follower has
|
|
374
|
+
// nothing left to follow.
|
|
375
|
+
console.log("");
|
|
376
|
+
console.log("✓ The preview is no longer running.");
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
380
|
+
}
|
|
381
|
+
console.log("");
|
|
382
|
+
console.log(" Ctrl-C to stop following. The preview keeps running.");
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* The org the creator is acting in, by the same precedence every other verb
|
|
386
|
+
* uses — flag, then env, then the machine config. Reading only the machine
|
|
387
|
+
* config would print a different org from the one the command acted in.
|
|
388
|
+
*/
|
|
389
|
+
async function activeOrgName() {
|
|
390
|
+
return (flag("--org") ??
|
|
391
|
+
process.env.VINCENTT_ORG ??
|
|
392
|
+
(await loadMachineConfig()).activeOrg ??
|
|
393
|
+
"");
|
|
394
|
+
}
|
|
395
|
+
/** The binding's name is a HINT for display only; the API is the authority. */
|
|
396
|
+
async function projectDisplayName(cwd) {
|
|
397
|
+
const located = await locateProjectBinding(cwd);
|
|
398
|
+
return located?.binding.slug ?? path.basename(cwd);
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Map a mint failure to its screen. Each cause has a DISTINCT first line so the
|
|
402
|
+
* creator never has to read a second, and none of them names the edge, the API
|
|
403
|
+
* or an outage — a creator cannot act on that.
|
|
404
|
+
*/
|
|
405
|
+
function previewFailureLines(err) {
|
|
406
|
+
if (err instanceof PreviewRateLimitedError)
|
|
407
|
+
return mintRateLimited(err.retryAfterSec);
|
|
408
|
+
if (err instanceof PreviewConflictError)
|
|
409
|
+
return previewConflict();
|
|
410
|
+
if (err instanceof ProjectUnreachableError) {
|
|
411
|
+
const out = [];
|
|
412
|
+
printUnreachable((l) => out.push(l), err.message);
|
|
413
|
+
return ["", ...out, ""];
|
|
414
|
+
}
|
|
415
|
+
return ["", `✗ ${err instanceof Error ? err.message : String(err)}`, ""];
|
|
170
416
|
}
|
|
171
417
|
// ---- diagnostics ------------------------------------------------------------
|
|
172
418
|
async function diag(kind) {
|
|
173
419
|
const cwd = projectDir();
|
|
420
|
+
// There is NO `--session`. It filtered on the raw client-supplied key, which
|
|
421
|
+
// made it the one surface that would teach a creator that key exists — and it
|
|
422
|
+
// was the same value the terminal was printing back. `--tester` replaces it.
|
|
174
423
|
const opts = {
|
|
175
|
-
|
|
424
|
+
tester: flag("--tester"),
|
|
425
|
+
viewer: flag("--viewer"),
|
|
176
426
|
since: numFlag("--since"),
|
|
177
427
|
limit: numFlag("--limit"),
|
|
178
428
|
errorsOnly: has("--errors"),
|
|
@@ -227,31 +477,52 @@ async function relay() {
|
|
|
227
477
|
function usage() {
|
|
228
478
|
console.log(`Vincentt CLI — local-first AR dev loop\n\n` +
|
|
229
479
|
`Usage: vincentt <command> [options]\n\n` +
|
|
480
|
+
`Getting started (building a new app with a coding agent)\n` +
|
|
481
|
+
` 1. Confirm a project name with the developer first.\n` +
|
|
482
|
+
` 2. Scaffold: vincentt init <dir> (no account needed)\n` +
|
|
483
|
+
` 3. Register: vincentt create --name "<name>"\n` +
|
|
484
|
+
` 4. Read AGENTS.md, confirm it is ready, and stop — do NOT invent an app.\n` +
|
|
485
|
+
` Wait for the developer to describe what to build.\n` +
|
|
486
|
+
` Full guide: npm view @vincentt-xr/cli readme\n\n` +
|
|
230
487
|
`Auth\n` +
|
|
231
488
|
` login Sign in; save an access token to ~/.vincentt/config.json\n` +
|
|
232
489
|
` logout Remove the saved access token\n` +
|
|
233
|
-
` whoami Show the signed-in account
|
|
490
|
+
` whoami Show the signed-in account\n\n` +
|
|
491
|
+
`Project\n` +
|
|
492
|
+
` init [dir] Scaffold a Vincentt XR app — no account needed\n` +
|
|
493
|
+
` [--no-install]\n` +
|
|
494
|
+
` create Register a project and reserve its address\n` +
|
|
495
|
+
` [--name <s>] [--slug <s>] [--org <s>] [--preview] [--force]\n` +
|
|
496
|
+
` link <project-id> Link this folder to an existing project\n` +
|
|
497
|
+
` unlink Disconnect this folder from its project\n\n` +
|
|
234
498
|
`Dev loop\n` +
|
|
235
|
-
` create Scaffold + bind a project (empty dir → full starter)\n` +
|
|
236
|
-
` [--name <s>] [--slug <s>] [--preview] [--force]\n` +
|
|
237
|
-
` unbind Detach this directory from its project\n` +
|
|
238
499
|
` publish Upload the built dist; go live at <slug>.vincentt.app\n` +
|
|
239
|
-
` preview Serve
|
|
240
|
-
`
|
|
500
|
+
` preview Serve the app at an https link a phone can open\n` +
|
|
501
|
+
` [--port <n>] [--tester <id>] [--no-qr] [--stop]\n\n` +
|
|
241
502
|
`Diagnostics (need a running preview)\n` +
|
|
242
|
-
` logs [--errors] Console logs from the device
|
|
243
|
-
` network fetch/XHR requests [--json]\n` +
|
|
244
|
-
` trace Performance samples [--json]\n
|
|
503
|
+
` logs [--errors] Console logs from the device [--json] [--tester <id>]\n` +
|
|
504
|
+
` network fetch/XHR requests [--json] [--tester <id>]\n` +
|
|
505
|
+
` trace Performance samples [--json] [--tester <id>]\n` +
|
|
506
|
+
` --tester t1 filters to one tester (one tab, across\n` +
|
|
507
|
+
` its refreshes). --viewer <label> filters to a single\n` +
|
|
508
|
+
` connection; one tab opens several.\n\n` +
|
|
245
509
|
`Reverse channel\n` +
|
|
246
510
|
` feedback --wait Block for the next phone annotation, print it, exit\n` +
|
|
247
511
|
` feedback --watch Stream annotations as they arrive\n\n` +
|
|
248
512
|
`Servers\n` +
|
|
249
513
|
` relay Run the diagnostics/annotation relay\n` +
|
|
250
514
|
` mcp Run the MCP adapter (for hosts without a shell)\n\n` +
|
|
251
|
-
`Common options: --dir <path> (project dir, default cwd)\n`
|
|
515
|
+
`Common options: --dir <path> (project dir, default cwd)\n` +
|
|
516
|
+
` --allow-custom-api honor a binding's apiUrl override\n`);
|
|
252
517
|
}
|
|
253
518
|
async function main() {
|
|
254
519
|
const cmd = argv[0];
|
|
520
|
+
// `--help`/`-h` anywhere means "explain, don't act" — never let it fall through to
|
|
521
|
+
// a verb. Otherwise `login --help` runs login, `create --help` scaffolds, etc.
|
|
522
|
+
if (isHelpInvocation(argv)) {
|
|
523
|
+
usage();
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
255
526
|
switch (cmd) {
|
|
256
527
|
case "login":
|
|
257
528
|
return login();
|
|
@@ -259,10 +530,14 @@ async function main() {
|
|
|
259
530
|
return logout();
|
|
260
531
|
case "whoami":
|
|
261
532
|
return whoami();
|
|
533
|
+
case "init":
|
|
534
|
+
return init();
|
|
262
535
|
case "create":
|
|
263
536
|
return create();
|
|
264
|
-
case "
|
|
265
|
-
return
|
|
537
|
+
case "link":
|
|
538
|
+
return link();
|
|
539
|
+
case "unlink":
|
|
540
|
+
return unlink();
|
|
266
541
|
case "publish":
|
|
267
542
|
return publish();
|
|
268
543
|
case "preview":
|
|
@@ -279,11 +554,6 @@ async function main() {
|
|
|
279
554
|
return mcp();
|
|
280
555
|
case "relay":
|
|
281
556
|
return relay();
|
|
282
|
-
case undefined:
|
|
283
|
-
case "-h":
|
|
284
|
-
case "--help":
|
|
285
|
-
usage();
|
|
286
|
-
return;
|
|
287
557
|
default:
|
|
288
558
|
console.error(`Unknown command: ${cmd}\n`);
|
|
289
559
|
usage();
|
package/dist/lib/backend.d.ts
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
import type { ResolvedConfig } from "../shared/config.js";
|
|
2
|
+
/**
|
|
3
|
+
* A project as the Go API returns it. `liveUrl` is SERVER-COMPUTED — the apex
|
|
4
|
+
* differs per environment, so a client that builds `<slug>.<apex>` itself would
|
|
5
|
+
* print the wrong address on staging and dev.
|
|
6
|
+
*/
|
|
7
|
+
export interface ProjectView {
|
|
8
|
+
id: string;
|
|
9
|
+
slug: string;
|
|
10
|
+
name: string;
|
|
11
|
+
orgId: string;
|
|
12
|
+
liveUrl: string;
|
|
13
|
+
publishState: string;
|
|
14
|
+
}
|
|
2
15
|
export interface CreatedProject {
|
|
3
16
|
projectId: string;
|
|
4
17
|
slug: string;
|
|
5
18
|
name: string;
|
|
19
|
+
orgId: string;
|
|
20
|
+
liveUrl: string;
|
|
6
21
|
}
|
|
7
22
|
export interface PublishResult {
|
|
8
23
|
version: number;
|
|
@@ -15,12 +30,28 @@ export declare class BackendError extends Error {
|
|
|
15
30
|
readonly status?: number | undefined;
|
|
16
31
|
constructor(message: string, status?: number | undefined);
|
|
17
32
|
}
|
|
33
|
+
/** The address a caller asked for is not available. Carries the server's suggestions. */
|
|
34
|
+
export declare class SlugTakenError extends BackendError {
|
|
35
|
+
readonly suggestions: string[];
|
|
36
|
+
constructor(suggestions: string[]);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The uniform 404 for every unreachable-project cause. The message is the
|
|
40
|
+
* server's, verbatim: the console shows the same six words, so a creator moving
|
|
41
|
+
* between surfaces sees one product and support can match a screenshot to a log
|
|
42
|
+
* line without a translation table.
|
|
43
|
+
*/
|
|
44
|
+
export declare class ProjectUnreachableError extends BackendError {
|
|
45
|
+
constructor(message: string);
|
|
46
|
+
}
|
|
18
47
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
48
|
+
* Register a project in an org. `slug` is optional and the server NEVER assigns
|
|
49
|
+
* one on a collision — it returns 409 with suggestions and the caller re-asks, so
|
|
50
|
+
* a creator's address is always a name they chose.
|
|
22
51
|
*/
|
|
23
|
-
export declare function createProject(cfg: ResolvedConfig, name: string, slug?: string): Promise<CreatedProject>;
|
|
52
|
+
export declare function createProject(cfg: ResolvedConfig, orgSlug: string, name: string, slug?: string): Promise<CreatedProject>;
|
|
53
|
+
/** Read a project by id. Backs `link`; the org is derived server-side from the row. */
|
|
54
|
+
export declare function getProject(cfg: ResolvedConfig, projectId: string): Promise<ProjectView>;
|
|
24
55
|
export interface MintedTunnel {
|
|
25
56
|
tunnelId: string;
|
|
26
57
|
/** The public host the phone opens: <slug>-<token>.<previewApex>. */
|
|
@@ -48,11 +79,20 @@ export declare function publishUpload(cfg: ResolvedConfig, projectId: string, di
|
|
|
48
79
|
note?: string;
|
|
49
80
|
commitSha?: string;
|
|
50
81
|
}): Promise<PublishResult>;
|
|
82
|
+
export interface MeMembership {
|
|
83
|
+
orgId: string;
|
|
84
|
+
orgSlug: string;
|
|
85
|
+
orgName: string;
|
|
86
|
+
orgKind: "personal" | "team";
|
|
87
|
+
role: string;
|
|
88
|
+
status: string;
|
|
89
|
+
}
|
|
51
90
|
export interface Me {
|
|
52
91
|
id?: string;
|
|
53
92
|
email?: string;
|
|
54
93
|
name?: string;
|
|
55
|
-
|
|
94
|
+
personalOrgId?: string;
|
|
95
|
+
memberships?: MeMembership[];
|
|
56
96
|
}
|
|
57
97
|
/** Resolve the signed-in identity for `vincentt whoami`. Throws on any non-2xx so
|
|
58
98
|
* the caller can degrade to "token present, unverified". */
|