@vincentt-xr/cli 0.1.5 → 0.3.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 +14 -0
- package/dist/cli/args.js +51 -0
- package/dist/cli/index.js +416 -100
- package/dist/lib/backend.d.ts +44 -20
- package/dist/lib/backend.js +98 -51
- 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 +85 -19
- package/dist/lib/lifecycle.js +133 -130
- 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 +52 -25
- 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/publish/api.d.ts +67 -0
- package/dist/publish/api.js +96 -0
- package/dist/publish/bounds.d.ts +25 -0
- package/dist/publish/bounds.js +53 -0
- package/dist/publish/classify.d.ts +51 -0
- package/dist/publish/classify.js +138 -0
- package/dist/publish/command.d.ts +27 -0
- package/dist/publish/command.js +328 -0
- package/dist/publish/consoleVerbs.d.ts +17 -0
- package/dist/publish/consoleVerbs.js +26 -0
- package/dist/publish/copy.d.ts +180 -0
- package/dist/publish/copy.js +606 -0
- package/dist/publish/defaults.d.ts +16 -0
- package/dist/publish/defaults.js +52 -0
- package/dist/publish/manifest.d.ts +27 -0
- package/dist/publish/manifest.js +81 -0
- package/dist/publish/paths.d.ts +25 -0
- package/dist/publish/paths.js +78 -0
- package/dist/publish/progress.d.ts +17 -0
- package/dist/publish/progress.js +45 -0
- package/dist/publish/prompt.d.ts +4 -0
- package/dist/publish/prompt.js +31 -0
- package/dist/publish/qa.f4.fixtures.d.ts +25 -0
- package/dist/publish/qa.f4.fixtures.js +140 -0
- package/dist/publish/upload.d.ts +30 -0
- package/dist/publish/upload.js +93 -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 +2 -3
- 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,37 @@
|
|
|
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";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
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";
|
|
24
|
+
import { diagQuery, diagRead, previewStart, previewStop, previewStopSignal, projectCreate, } from "../lib/lifecycle.js";
|
|
25
|
+
import { publishCommand } from "../publish/command.js";
|
|
26
|
+
import { stdinConfirm } from "../publish/prompt.js";
|
|
27
|
+
import { consoleVerbCommand, isConsoleOnlyVerb } from "../publish/consoleVerbs.js";
|
|
28
|
+
import { PreviewUi } from "../preview/command.js";
|
|
29
|
+
import { httpRelayClient } from "../lib/diagnostics.js";
|
|
30
|
+
import { readLivePreview } from "../lib/relayInfo.js";
|
|
31
|
+
import { mintRateLimited, notSignedIn, previewConflict, startBlock, } from "../preview/copy.js";
|
|
32
|
+
import { renderTerminalQr, shouldRenderQr } from "../preview/qr.js";
|
|
33
|
+
import { PreviewConflictError, PreviewRateLimitedError } from "../preview/api.js";
|
|
20
34
|
import { fetchNext, readCursor, writeCursor } from "../lib/feedback.js";
|
|
21
|
-
|
|
35
|
+
import { isHelpInvocation, positionalArgs, refusedFlag } from "./args.js";
|
|
36
|
+
// The console origin serves /cli-auth (the browser target for `vincentt login`). VINCENTT_
|
|
37
|
+
// CONSOLE_URL overrides for dev/dogfood; VINCENTT_EDITOR_URL is honored as a legacy alias.
|
|
38
|
+
const DEFAULT_CONSOLE_URL = "https://vincentt.app";
|
|
22
39
|
const DEFAULT_API_URL = "https://api.vincentt.studio";
|
|
23
40
|
// ---- arg helpers ------------------------------------------------------------
|
|
24
41
|
const argv = process.argv.slice(2);
|
|
@@ -37,20 +54,54 @@ function projectDir() {
|
|
|
37
54
|
const dir = flag("--dir");
|
|
38
55
|
return dir ? path.resolve(dir) : process.cwd();
|
|
39
56
|
}
|
|
57
|
+
/** The nth bare argument (`vincentt link <id>`), skipping flags and their values. */
|
|
58
|
+
function positional(n) {
|
|
59
|
+
return positionalArgs(argv)[n];
|
|
60
|
+
}
|
|
40
61
|
// ---- auth -------------------------------------------------------------------
|
|
41
|
-
|
|
42
|
-
const
|
|
62
|
+
function resolveAuthUrls() {
|
|
63
|
+
const consoleUrl = flag("--console") ??
|
|
64
|
+
flag("--editor") ??
|
|
65
|
+
process.env.VINCENTT_CONSOLE_URL ??
|
|
66
|
+
process.env.VINCENTT_EDITOR_URL ??
|
|
67
|
+
DEFAULT_CONSOLE_URL;
|
|
43
68
|
const apiUrl = flag("--api") ?? process.env.VINCENTT_API_URL ?? DEFAULT_API_URL;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
69
|
+
return { consoleUrl, apiUrl };
|
|
70
|
+
}
|
|
71
|
+
/** The PKCE loopback + code-exchange login (f1). The raw PAT never rides a redirect query. */
|
|
72
|
+
async function runLoginFlow() {
|
|
73
|
+
const { consoleUrl, apiUrl } = resolveAuthUrls();
|
|
74
|
+
return runPkceLogin({
|
|
75
|
+
consoleUrl,
|
|
48
76
|
apiUrl,
|
|
49
|
-
|
|
50
|
-
onUrl: (url) => console.log(`\nIf your browser didn't open, visit:\n ${url}\n`),
|
|
77
|
+
onProgress: (line) => console.log(line),
|
|
51
78
|
});
|
|
52
|
-
|
|
53
|
-
|
|
79
|
+
}
|
|
80
|
+
/** `vincentt login` [--force] — the L1/L2/L4 transcripts. */
|
|
81
|
+
async function login() {
|
|
82
|
+
const { apiUrl } = resolveAuthUrls();
|
|
83
|
+
const code = await loginCommand({
|
|
84
|
+
print: (line) => console.log(line),
|
|
85
|
+
currentToken,
|
|
86
|
+
whoAmI: async () => {
|
|
87
|
+
const pat = await currentToken();
|
|
88
|
+
if (!pat)
|
|
89
|
+
return undefined;
|
|
90
|
+
try {
|
|
91
|
+
return await httpAuthBackend(apiUrl).me(pat);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
runLogin: runLoginFlow,
|
|
98
|
+
clearToken: async () => {
|
|
99
|
+
await clearMachineToken();
|
|
100
|
+
},
|
|
101
|
+
home: os.homedir(),
|
|
102
|
+
configPath: MACHINE_CONFIG_PATH,
|
|
103
|
+
}, { force: has("--force") });
|
|
104
|
+
process.exitCode = code;
|
|
54
105
|
}
|
|
55
106
|
/** The token that would authenticate a machine call, if any (env wins over file). */
|
|
56
107
|
async function currentToken() {
|
|
@@ -68,41 +119,47 @@ async function logout() {
|
|
|
68
119
|
const { path: p, hadToken } = await clearMachineToken();
|
|
69
120
|
console.log(hadToken ? `✓ Signed out. Token removed from ${p}` : "Already signed out.");
|
|
70
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* `whoami` reads the MACHINE config's API host, never the folder's binding — it
|
|
124
|
+
* answers "who am I", and reading the binding would make it answer a question
|
|
125
|
+
* about the folder instead.
|
|
126
|
+
*/
|
|
71
127
|
async function whoami() {
|
|
72
128
|
const machine = await loadMachineConfig();
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
129
|
+
process.exitCode = await whoamiCommand({
|
|
130
|
+
print: (l) => console.log(l),
|
|
131
|
+
config: async () => {
|
|
132
|
+
const apiUrl = process.env.VINCENTT_API_URL ?? machine.apiUrl ?? DEFAULT_API_URL;
|
|
133
|
+
const pat = process.env.VINCENTT_PAT ?? machine.pat;
|
|
134
|
+
if (!pat)
|
|
135
|
+
return undefined;
|
|
136
|
+
const cfg = { apiUrl: apiUrl.replace(/\/$/, ""), pat };
|
|
137
|
+
return cfg;
|
|
138
|
+
},
|
|
139
|
+
getMe,
|
|
140
|
+
org: {
|
|
141
|
+
flag: flag("--org"),
|
|
142
|
+
env: process.env.VINCENTT_ORG,
|
|
143
|
+
config: machine.activeOrg,
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
// ---- project ----------------------------------------------------------------
|
|
148
|
+
async function init() {
|
|
149
|
+
process.exitCode = await initCommand({ print: (l) => console.log(l), cwd: process.cwd() }, { dir: positional(1), skipInstall: has("--no-install") });
|
|
92
150
|
}
|
|
93
|
-
// ---- dev loop ---------------------------------------------------------------
|
|
94
151
|
async function create() {
|
|
95
152
|
await requireLogin();
|
|
96
153
|
const result = await projectCreate(projectDir(), {
|
|
97
154
|
name: flag("--name"),
|
|
98
155
|
slug: flag("--slug"),
|
|
99
|
-
|
|
156
|
+
org: flag("--org"),
|
|
100
157
|
force: has("--force"),
|
|
101
|
-
|
|
158
|
+
allowCustomApi: has("--allow-custom-api"),
|
|
159
|
+
onLine: (l) => console.log(l),
|
|
102
160
|
});
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
process.exitCode = 1;
|
|
161
|
+
if (result.exitCode !== 0) {
|
|
162
|
+
process.exitCode = result.exitCode;
|
|
106
163
|
return;
|
|
107
164
|
}
|
|
108
165
|
// Chain straight into the dev loop when asked.
|
|
@@ -111,68 +168,301 @@ async function create() {
|
|
|
111
168
|
await preview();
|
|
112
169
|
}
|
|
113
170
|
}
|
|
114
|
-
async function
|
|
115
|
-
const { path: p, hadBinding } = await clearProjectBinding(projectDir());
|
|
116
|
-
console.log(hadBinding ? `✓ Unbound. Removed ${p}` : "This directory has no project binding.");
|
|
117
|
-
}
|
|
118
|
-
async function publish() {
|
|
171
|
+
async function link() {
|
|
119
172
|
await requireLogin();
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
173
|
+
const cwd = projectDir();
|
|
174
|
+
process.exitCode = await linkCommand({
|
|
175
|
+
print: (l) => console.log(l),
|
|
176
|
+
cwd,
|
|
177
|
+
config: () => resolveConfig(cwd, {
|
|
178
|
+
allowCustomApi: has("--allow-custom-api"),
|
|
179
|
+
warn: (l) => console.log(l),
|
|
180
|
+
}),
|
|
181
|
+
}, positional(1));
|
|
182
|
+
}
|
|
183
|
+
async function unlink() {
|
|
184
|
+
const cwd = projectDir();
|
|
185
|
+
process.exitCode = await unlinkCommand({
|
|
186
|
+
print: (l) => console.log(l),
|
|
187
|
+
cwd,
|
|
188
|
+
// unlink makes no server call, so this is never invoked.
|
|
189
|
+
config: () => resolveConfig(cwd),
|
|
123
190
|
});
|
|
124
|
-
|
|
125
|
-
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* `vincentt publish` — Flows L22-L31.
|
|
194
|
+
*
|
|
195
|
+
* The whole gate lives here: the checklist, the permanence disclosure, the
|
|
196
|
+
* confirmation, the upload and the live URL. There is no browser step in
|
|
197
|
+
* publishing, not on the first publish, not ever.
|
|
198
|
+
*/
|
|
199
|
+
async function publish() {
|
|
200
|
+
const refused = refusedFlag(argv);
|
|
201
|
+
if (refused) {
|
|
202
|
+
console.log(`✗ There's no \`${refused}\` option.`);
|
|
203
|
+
console.log("");
|
|
204
|
+
console.log(" The link-preview check reports what it found; it never edits your");
|
|
205
|
+
console.log(" source. Fix the tags in your own files, rebuild, then publish again.");
|
|
126
206
|
process.exitCode = 1;
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const cwd = projectDir();
|
|
210
|
+
process.exitCode = await publishCommand({
|
|
211
|
+
print: (l) => console.log(l),
|
|
212
|
+
write: (s) => process.stdout.write(s),
|
|
213
|
+
confirm: stdinConfirm(),
|
|
214
|
+
cwd,
|
|
215
|
+
config: () => resolveConfig(cwd, {
|
|
216
|
+
allowCustomApi: has("--allow-custom-api"),
|
|
217
|
+
warn: (l) => console.log(l),
|
|
218
|
+
}),
|
|
219
|
+
binding: async () => {
|
|
220
|
+
const located = await locateProjectBinding(cwd);
|
|
221
|
+
return located
|
|
222
|
+
? { projectId: located.binding.projectId, slug: located.binding.slug }
|
|
223
|
+
: undefined;
|
|
224
|
+
},
|
|
225
|
+
token: currentToken,
|
|
226
|
+
isTTY: Boolean(process.stdout.isTTY) && Boolean(process.stdin.isTTY),
|
|
227
|
+
now: () => Date.now(),
|
|
228
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
229
|
+
orgName: activeOrgName,
|
|
230
|
+
}, { distDir: flag("--dist"), note: flag("--note"), yes: has("--yes") });
|
|
127
231
|
}
|
|
232
|
+
/** Flow L30 — the six verbs that are console actions, answered rather than rejected. */
|
|
233
|
+
async function consoleOnly(verb) {
|
|
234
|
+
const cwd = projectDir();
|
|
235
|
+
process.exitCode = await consoleVerbCommand({
|
|
236
|
+
print: (l) => console.log(l),
|
|
237
|
+
projectId: async () => (await locateProjectBinding(cwd))?.binding.projectId,
|
|
238
|
+
consoleUrl: flag("--console") ?? process.env.VINCENTT_CONSOLE_URL,
|
|
239
|
+
}, verb);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* `vincentt preview` — Flows L13-L21.
|
|
243
|
+
*
|
|
244
|
+
* The verb does something a creator could easily misread, so the FOLLOW path
|
|
245
|
+
* says what it is doing in its first line: in a folder with a live preview,
|
|
246
|
+
* `vincentt preview` ATTACHES to it and follows. It does not mint, does not
|
|
247
|
+
* print a QR, and does not supersede. A second preview for the same tree is
|
|
248
|
+
* created only when the working tree differs.
|
|
249
|
+
*/
|
|
128
250
|
async function preview() {
|
|
129
|
-
|
|
130
|
-
// via its pidfile — no need to hunt for the cloudflared pid by hand.
|
|
251
|
+
const cwd = projectDir();
|
|
131
252
|
if (has("--stop")) {
|
|
132
|
-
const { message } = await previewStopSignal(
|
|
253
|
+
const { message } = await previewStopSignal(cwd);
|
|
133
254
|
console.log(message);
|
|
134
255
|
return;
|
|
135
256
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (!
|
|
139
|
-
|
|
257
|
+
// The gate is LOCAL and NOTHING IS ATTEMPTED: no spinner, no partial mint, no
|
|
258
|
+
// session left in `pending`. The command stops before it touches the network.
|
|
259
|
+
if (!(await currentToken())) {
|
|
260
|
+
for (const l of notSignedIn())
|
|
261
|
+
console.log(l);
|
|
262
|
+
process.exitCode = 1;
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const filterViewer = flag("--viewer");
|
|
266
|
+
const filterTester = flag("--tester");
|
|
267
|
+
const color = Boolean(process.stdout.isTTY) && process.env.NO_COLOR === undefined;
|
|
268
|
+
const ui = new PreviewUi({
|
|
269
|
+
print: (l) => console.log(l),
|
|
270
|
+
now: () => Date.now(),
|
|
271
|
+
targetUrl: `http://localhost:${numFlag("--port") ?? 5173}`,
|
|
272
|
+
color,
|
|
273
|
+
filterTester,
|
|
274
|
+
filterViewer,
|
|
275
|
+
});
|
|
276
|
+
// A preview already running for this folder — in ANOTHER process — is
|
|
277
|
+
// FOLLOWED, not superseded. `vincentt preview` in a folder with a live
|
|
278
|
+
// preview attaches to it: it does not mint, does not print a QR, and does not
|
|
279
|
+
// take the link away from whoever is holding a phone.
|
|
280
|
+
//
|
|
281
|
+
// The marker is VALIDATED first. It only ever proved a preview was STARTED,
|
|
282
|
+
// and a crashed CLI, a kill -9 or a reboot all leave it behind — an unvalidated
|
|
283
|
+
// one short-circuited the mint into a dead end whose only cure was deleting a
|
|
284
|
+
// dotfile the creator does not know exists. A stale marker is cleared and
|
|
285
|
+
// treated as absent, which lands on the mint the creator actually asked for.
|
|
286
|
+
const existing = await readLivePreview(cwd);
|
|
287
|
+
if (existing !== undefined) {
|
|
288
|
+
await followPreview(existing, ui, filterTester, filterViewer);
|
|
140
289
|
return;
|
|
141
290
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
291
|
+
let ending = null;
|
|
292
|
+
let resolveDone;
|
|
293
|
+
const done = new Promise((r) => {
|
|
294
|
+
resolveDone = r;
|
|
295
|
+
});
|
|
296
|
+
let running;
|
|
147
297
|
try {
|
|
148
|
-
const
|
|
149
|
-
|
|
298
|
+
const started = await previewStart(cwd, (m) => console.error(`[preview] ${m}`), {
|
|
299
|
+
appPort: numFlag("--port"),
|
|
300
|
+
appStdio: has("--verbose") ? "inherit" : "ignore",
|
|
301
|
+
onViewer: (label, uaClass) => ui.viewerSeen(label, uaClass),
|
|
302
|
+
onViewerGone: (label) => ui.viewerGone(label),
|
|
303
|
+
onEvents: (events, tags) => ui.events(events, tags),
|
|
304
|
+
onCapReached: () => ui.capReached(),
|
|
305
|
+
onTunnelState: (state) => ui.tunnel(state),
|
|
306
|
+
onOriginError: () => ui.originError(),
|
|
307
|
+
onOriginRecovered: () => ui.originRecovered(),
|
|
308
|
+
onExpiring: () => ui.checkHardMax(running?.expiresAt ?? ""),
|
|
309
|
+
onEnded: (reason) => {
|
|
310
|
+
ending = reason;
|
|
311
|
+
resolveDone?.();
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
running = started.preview;
|
|
315
|
+
if (!started.started) {
|
|
316
|
+
// Already running IN THIS PROCESS. The mint path never reaches here.
|
|
317
|
+
console.log(`A preview is already running:\n${running.url}`);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
catch (err) {
|
|
322
|
+
for (const l of previewFailureLines(err))
|
|
323
|
+
console.log(l);
|
|
324
|
+
process.exitCode = 1;
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const qr = shouldRenderQr({
|
|
328
|
+
isTTY: Boolean(process.stdout.isTTY),
|
|
329
|
+
noQr: has("--no-qr"),
|
|
330
|
+
columns: process.stdout.columns,
|
|
331
|
+
})
|
|
332
|
+
? await renderTerminalQr(running.url).catch(() => undefined)
|
|
333
|
+
: undefined;
|
|
334
|
+
for (const l of startBlock({
|
|
335
|
+
projectName: await projectDisplayName(cwd),
|
|
336
|
+
orgName: await activeOrgName(),
|
|
337
|
+
targetUrl: `http://localhost:${running.appPort}`,
|
|
338
|
+
url: running.url,
|
|
339
|
+
qr,
|
|
340
|
+
expiresInHours: 12,
|
|
341
|
+
})) {
|
|
342
|
+
console.log(l);
|
|
343
|
+
}
|
|
344
|
+
// The hard-max warning is checked on a slow tick rather than scheduled at a
|
|
345
|
+
// fixed offset: a laptop that slept through the scheduled instant would never
|
|
346
|
+
// fire a one-shot timer, and the 30-minute warning is the one the creator
|
|
347
|
+
// plans a demo around.
|
|
348
|
+
const ceiling = setInterval(() => ui.checkHardMax(running.expiresAt), 60_000);
|
|
349
|
+
ceiling.unref?.();
|
|
350
|
+
const stop = () => {
|
|
351
|
+
ui.stopping();
|
|
352
|
+
void previewStop().then(() => {
|
|
353
|
+
ui.stopped();
|
|
354
|
+
resolveDone?.();
|
|
355
|
+
});
|
|
356
|
+
};
|
|
357
|
+
process.on("SIGINT", stop);
|
|
358
|
+
process.on("SIGTERM", stop);
|
|
359
|
+
await done;
|
|
360
|
+
clearInterval(ceiling);
|
|
361
|
+
if (ending !== null) {
|
|
362
|
+
ui.endedBy(ending, (await activeOrgName()) || "your organization");
|
|
363
|
+
await previewStop();
|
|
364
|
+
process.exitCode = 1;
|
|
150
365
|
}
|
|
151
|
-
|
|
152
|
-
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Flow L16 — follow a preview running in another terminal.
|
|
369
|
+
*
|
|
370
|
+
* It says what it is doing in its FIRST line, because this is the one flow
|
|
371
|
+
* where the CLI's verb does something the creator could easily misread. It
|
|
372
|
+
* polls the relay rather than opening a second tunnel: a second tunnel would be
|
|
373
|
+
* a second session, which is the "two live URLs, one stale" the design forbids.
|
|
374
|
+
*/
|
|
375
|
+
async function followPreview(info, ui, filterTester, filterViewer) {
|
|
376
|
+
console.log("→ Attaching to the preview running in this folder");
|
|
377
|
+
const following = filterTester ?? filterViewer;
|
|
378
|
+
if (following !== undefined) {
|
|
379
|
+
console.log(`✓ Following ${following}`);
|
|
153
380
|
}
|
|
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.`);
|
|
381
|
+
else {
|
|
382
|
+
console.log("✓ Following this preview");
|
|
159
383
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
process.on("SIGINT", stop);
|
|
168
|
-
process.on("SIGTERM", stop);
|
|
384
|
+
console.log("");
|
|
385
|
+
const relay = httpRelayClient(`http://localhost:${info.relayPort}`);
|
|
386
|
+
let since = -1;
|
|
387
|
+
let warnedMissing = false;
|
|
388
|
+
let stop = false;
|
|
389
|
+
process.on("SIGINT", () => {
|
|
390
|
+
stop = true;
|
|
169
391
|
});
|
|
392
|
+
while (!stop) {
|
|
393
|
+
try {
|
|
394
|
+
const result = await relay.query({ since, tester: filterTester, viewer: filterViewer });
|
|
395
|
+
// An unknown id is a FACT, not an error — and the remedy goes on the same
|
|
396
|
+
// line. Said once rather than on every poll, or a two-second loop becomes
|
|
397
|
+
// a wall of identical advisories.
|
|
398
|
+
if (!warnedMissing &&
|
|
399
|
+
filterTester !== undefined &&
|
|
400
|
+
result.testers.length > 0 &&
|
|
401
|
+
!result.testers.includes(filterTester)) {
|
|
402
|
+
warnedMissing = true;
|
|
403
|
+
console.log(` ! No ${filterTester} on this preview right now. Connected: ${result.testers.join(", ")}.`);
|
|
404
|
+
}
|
|
405
|
+
// Both tags ride ALONGSIDE the events, index-parallel, so an unattributed
|
|
406
|
+
// event is dropped from the terminal rather than printed under an
|
|
407
|
+
// invented id that would read as a real tester.
|
|
408
|
+
result.events.forEach((e, i) => ui.events([e], { tester: result.eventTesters?.[i], viewer: result.eventViewers?.[i] }, { attribute: false }));
|
|
409
|
+
if (result.latestSeq > since)
|
|
410
|
+
since = result.latestSeq;
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
// The preview stopped, or its relay is gone. Either way the follower has
|
|
414
|
+
// nothing left to follow.
|
|
415
|
+
console.log("");
|
|
416
|
+
console.log("✓ The preview is no longer running.");
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
420
|
+
}
|
|
421
|
+
console.log("");
|
|
422
|
+
console.log(" Ctrl-C to stop following. The preview keeps running.");
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* The org the creator is acting in, by the same precedence every other verb
|
|
426
|
+
* uses — flag, then env, then the machine config. Reading only the machine
|
|
427
|
+
* config would print a different org from the one the command acted in.
|
|
428
|
+
*/
|
|
429
|
+
async function activeOrgName() {
|
|
430
|
+
return (flag("--org") ??
|
|
431
|
+
process.env.VINCENTT_ORG ??
|
|
432
|
+
(await loadMachineConfig()).activeOrg ??
|
|
433
|
+
"");
|
|
434
|
+
}
|
|
435
|
+
/** The binding's name is a HINT for display only; the API is the authority. */
|
|
436
|
+
async function projectDisplayName(cwd) {
|
|
437
|
+
const located = await locateProjectBinding(cwd);
|
|
438
|
+
return located?.binding.slug ?? path.basename(cwd);
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Map a mint failure to its screen. Each cause has a DISTINCT first line so the
|
|
442
|
+
* creator never has to read a second, and none of them names the edge, the API
|
|
443
|
+
* or an outage — a creator cannot act on that.
|
|
444
|
+
*/
|
|
445
|
+
function previewFailureLines(err) {
|
|
446
|
+
if (err instanceof PreviewRateLimitedError)
|
|
447
|
+
return mintRateLimited(err.retryAfterSec);
|
|
448
|
+
if (err instanceof PreviewConflictError)
|
|
449
|
+
return previewConflict();
|
|
450
|
+
if (err instanceof ProjectUnreachableError) {
|
|
451
|
+
const out = [];
|
|
452
|
+
printUnreachable((l) => out.push(l), err.message);
|
|
453
|
+
return ["", ...out, ""];
|
|
454
|
+
}
|
|
455
|
+
return ["", `✗ ${err instanceof Error ? err.message : String(err)}`, ""];
|
|
170
456
|
}
|
|
171
457
|
// ---- diagnostics ------------------------------------------------------------
|
|
172
458
|
async function diag(kind) {
|
|
173
459
|
const cwd = projectDir();
|
|
460
|
+
// There is NO `--session`. It filtered on the raw client-supplied key, which
|
|
461
|
+
// made it the one surface that would teach a creator that key exists — and it
|
|
462
|
+
// was the same value the terminal was printing back. `--tester` replaces it.
|
|
174
463
|
const opts = {
|
|
175
|
-
|
|
464
|
+
tester: flag("--tester"),
|
|
465
|
+
viewer: flag("--viewer"),
|
|
176
466
|
since: numFlag("--since"),
|
|
177
467
|
limit: numFlag("--limit"),
|
|
178
468
|
errorsOnly: has("--errors"),
|
|
@@ -227,31 +517,53 @@ async function relay() {
|
|
|
227
517
|
function usage() {
|
|
228
518
|
console.log(`Vincentt CLI — local-first AR dev loop\n\n` +
|
|
229
519
|
`Usage: vincentt <command> [options]\n\n` +
|
|
520
|
+
`Getting started (building a new app with a coding agent)\n` +
|
|
521
|
+
` 1. Confirm a project name with the developer first.\n` +
|
|
522
|
+
` 2. Scaffold: vincentt init <dir> (no account needed)\n` +
|
|
523
|
+
` 3. Register: vincentt create --name "<name>"\n` +
|
|
524
|
+
` 4. Read AGENTS.md, confirm it is ready, and stop — do NOT invent an app.\n` +
|
|
525
|
+
` Wait for the developer to describe what to build.\n` +
|
|
526
|
+
` Full guide: npm view @vincentt-xr/cli readme\n\n` +
|
|
230
527
|
`Auth\n` +
|
|
231
528
|
` login Sign in; save an access token to ~/.vincentt/config.json\n` +
|
|
232
529
|
` logout Remove the saved access token\n` +
|
|
233
|
-
` whoami Show the signed-in account
|
|
530
|
+
` whoami Show the signed-in account\n\n` +
|
|
531
|
+
`Project\n` +
|
|
532
|
+
` init [dir] Scaffold a Vincentt XR app — no account needed\n` +
|
|
533
|
+
` [--no-install]\n` +
|
|
534
|
+
` create Register a project and reserve its address\n` +
|
|
535
|
+
` [--name <s>] [--slug <s>] [--org <s>] [--preview] [--force]\n` +
|
|
536
|
+
` link <project-id> Link this folder to an existing project\n` +
|
|
537
|
+
` unlink Disconnect this folder from its project\n\n` +
|
|
234
538
|
`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
539
|
` publish Upload the built dist; go live at <slug>.vincentt.app\n` +
|
|
239
|
-
`
|
|
240
|
-
`
|
|
540
|
+
` [--dist <path>] [--note <s>] [--yes]\n` +
|
|
541
|
+
` preview Serve the app at an https link a phone can open\n` +
|
|
542
|
+
` [--port <n>] [--tester <id>] [--no-qr] [--stop]\n\n` +
|
|
241
543
|
`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
|
|
544
|
+
` logs [--errors] Console logs from the device [--json] [--tester <id>]\n` +
|
|
545
|
+
` network fetch/XHR requests [--json] [--tester <id>]\n` +
|
|
546
|
+
` trace Performance samples [--json] [--tester <id>]\n` +
|
|
547
|
+
` --tester t1 filters to one tester (one tab, across\n` +
|
|
548
|
+
` its refreshes). --viewer <label> filters to a single\n` +
|
|
549
|
+
` connection; one tab opens several.\n\n` +
|
|
245
550
|
`Reverse channel\n` +
|
|
246
551
|
` feedback --wait Block for the next phone annotation, print it, exit\n` +
|
|
247
552
|
` feedback --watch Stream annotations as they arrive\n\n` +
|
|
248
553
|
`Servers\n` +
|
|
249
554
|
` relay Run the diagnostics/annotation relay\n` +
|
|
250
555
|
` mcp Run the MCP adapter (for hosts without a shell)\n\n` +
|
|
251
|
-
`Common options: --dir <path> (project dir, default cwd)\n`
|
|
556
|
+
`Common options: --dir <path> (project dir, default cwd)\n` +
|
|
557
|
+
` --allow-custom-api honor a binding's apiUrl override\n`);
|
|
252
558
|
}
|
|
253
559
|
async function main() {
|
|
254
560
|
const cmd = argv[0];
|
|
561
|
+
// `--help`/`-h` anywhere means "explain, don't act" — never let it fall through to
|
|
562
|
+
// a verb. Otherwise `login --help` runs login, `create --help` scaffolds, etc.
|
|
563
|
+
if (isHelpInvocation(argv)) {
|
|
564
|
+
usage();
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
255
567
|
switch (cmd) {
|
|
256
568
|
case "login":
|
|
257
569
|
return login();
|
|
@@ -259,10 +571,14 @@ async function main() {
|
|
|
259
571
|
return logout();
|
|
260
572
|
case "whoami":
|
|
261
573
|
return whoami();
|
|
574
|
+
case "init":
|
|
575
|
+
return init();
|
|
262
576
|
case "create":
|
|
263
577
|
return create();
|
|
264
|
-
case "
|
|
265
|
-
return
|
|
578
|
+
case "link":
|
|
579
|
+
return link();
|
|
580
|
+
case "unlink":
|
|
581
|
+
return unlink();
|
|
266
582
|
case "publish":
|
|
267
583
|
return publish();
|
|
268
584
|
case "preview":
|
|
@@ -279,12 +595,12 @@ async function main() {
|
|
|
279
595
|
return mcp();
|
|
280
596
|
case "relay":
|
|
281
597
|
return relay();
|
|
282
|
-
case undefined:
|
|
283
|
-
case "-h":
|
|
284
|
-
case "--help":
|
|
285
|
-
usage();
|
|
286
|
-
return;
|
|
287
598
|
default:
|
|
599
|
+
// Flow L30: rolling back and taking an address offline are console
|
|
600
|
+
// actions, and a bare "unknown command" here would be the product failing
|
|
601
|
+
// to answer a question it knows the answer to.
|
|
602
|
+
if (cmd !== undefined && isConsoleOnlyVerb(cmd))
|
|
603
|
+
return consoleOnly(cmd);
|
|
288
604
|
console.error(`Unknown command: ${cmd}\n`);
|
|
289
605
|
usage();
|
|
290
606
|
process.exitCode = 1;
|