@vincentt-xr/cli 0.9.0 → 0.10.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 +40 -13
- package/dist/cli/args.d.ts +12 -0
- package/dist/cli/args.js +40 -0
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +296 -95
- package/dist/cli/usage-text.d.ts +1 -0
- package/dist/cli/usage-text.js +97 -0
- package/dist/cli/verb-surface.d.ts +24 -0
- package/dist/cli/verb-surface.js +49 -0
- package/dist/dx/clusters.d.ts +0 -17
- package/dist/dx/clusters.js +50 -1
- package/dist/lib/backend.d.ts +11 -0
- package/dist/lib/backend.js +23 -0
- package/dist/lib/classifyTarget.d.ts +42 -0
- package/dist/lib/classifyTarget.js +83 -0
- package/dist/lib/feedback.d.ts +46 -5
- package/dist/lib/feedback.js +76 -19
- package/dist/lib/feedbackCopy.d.ts +70 -0
- package/dist/lib/feedbackCopy.js +163 -0
- package/dist/lib/initFrom.d.ts +26 -4
- package/dist/lib/initFrom.js +65 -41
- package/dist/lib/lifecycle.d.ts +28 -3
- package/dist/lib/lifecycle.js +99 -7
- package/dist/lib/listProjects.d.ts +41 -0
- package/dist/lib/listProjects.js +77 -0
- package/dist/lib/projects.d.ts +64 -8
- package/dist/lib/projects.js +316 -24
- package/dist/lib/sourceRefCopy.d.ts +19 -0
- package/dist/lib/sourceRefCopy.js +36 -0
- package/dist/preview/command.d.ts +11 -0
- package/dist/preview/command.js +19 -1
- package/dist/preview/copy.d.ts +7 -0
- package/dist/preview/copy.js +8 -1
- package/dist/preview/runner.d.ts +6 -0
- package/dist/preview/runner.js +1 -0
- package/dist/preview/stream.d.ts +11 -0
- package/dist/preview/stream.js +13 -0
- package/dist/relay/annotations.d.ts +28 -4
- package/dist/relay/annotations.js +23 -7
- package/dist/relay/qa.f12.fixtures.d.ts +154 -0
- package/dist/relay/qa.f12.fixtures.js +358 -0
- package/dist/relay/server.d.ts +41 -3
- package/dist/relay/server.js +90 -32
- package/package.json +2 -2
- package/dist/library/qa.f5-b32.spec.d.ts +0 -1
- package/dist/library/qa.f5-b32.spec.js +0 -202
- package/dist/library/qa.f5-cli.spec.d.ts +0 -1
- package/dist/library/qa.f5-cli.spec.js +0 -433
- package/dist/library/qa.f5-contract.spec.d.ts +0 -1
- package/dist/library/qa.f5-contract.spec.js +0 -206
- package/dist/relay/injectors.d.ts +0 -29
- package/dist/relay/injectors.js +0 -72
package/dist/cli/index.js
CHANGED
|
@@ -19,7 +19,8 @@ import { httpAuthBackend } from "../login/authBackend.js";
|
|
|
19
19
|
import { loginCommand } from "../login/loginCommand.js";
|
|
20
20
|
import { ensureLoggedIn } from "../login/ensure.js";
|
|
21
21
|
import { clearMachineToken, loadMachineConfig, locateProjectBinding, resolveConfig, writeDxEnablement, MACHINE_CONFIG_PATH, } from "../shared/config.js";
|
|
22
|
-
import { getMe, ProjectUnreachableError } from "../lib/backend.js";
|
|
22
|
+
import { getMe, listProjects, ProjectUnreachableError } from "../lib/backend.js";
|
|
23
|
+
import { listProjectsCommand } from "../lib/listProjects.js";
|
|
23
24
|
import { initCommand } from "../lib/initCommand.js";
|
|
24
25
|
import { linkCommand, printUnreachable, unlinkCommand } from "../lib/projects.js";
|
|
25
26
|
import { whoamiCommand } from "../lib/whoami.js";
|
|
@@ -28,13 +29,16 @@ import { diagQuery, diagRead, previewStart, previewStop, previewStopSignal, proj
|
|
|
28
29
|
import { publishCommand } from "../publish/command.js";
|
|
29
30
|
import { stdinAsk, stdinConfirm } from "../publish/prompt.js";
|
|
30
31
|
import { consoleVerbCommand, isConsoleOnlyVerb } from "../publish/consoleVerbs.js";
|
|
32
|
+
import { resolveVerb } from "./verb-surface.js";
|
|
33
|
+
import { USAGE_TEXT } from "./usage-text.js";
|
|
31
34
|
import { PreviewUi } from "../preview/command.js";
|
|
32
35
|
import { httpRelayClient } from "../lib/diagnostics.js";
|
|
33
|
-
import { readLivePreview } from "../lib/relayInfo.js";
|
|
36
|
+
import { readLivePreview, relayInfoDir } from "../lib/relayInfo.js";
|
|
34
37
|
import { attachedToRunning, mintRateLimited, notSignedIn, previewConflict, startBlock, } from "../preview/copy.js";
|
|
35
38
|
import { renderTerminalQr, shouldRenderQr } from "../preview/qr.js";
|
|
36
39
|
import { PreviewConflictError, PreviewRateLimitedError } from "../preview/api.js";
|
|
37
40
|
import { fetchNext, readCursor, writeCursor } from "../lib/feedback.js";
|
|
41
|
+
import * as feedbackCopy from "../lib/feedbackCopy.js";
|
|
38
42
|
import { record as recordInvocation } from "../dx/journal.js";
|
|
39
43
|
import { dxDisableCommand, dxEnableCommand, dxSendCommand, parseDxOptions, runDx, } from "../dx/command.js";
|
|
40
44
|
import { DX_UNATTENDED_SUNSET, loadEnablement } from "../dx/enablement.js";
|
|
@@ -42,11 +46,12 @@ import { journalEnabled, journalPath, readJournal } from "../dx/journal.js";
|
|
|
42
46
|
import { postReport } from "../dx/send.js";
|
|
43
47
|
import { nudgeFor } from "../dx/nudge.js";
|
|
44
48
|
import { verbOf } from "../dx/skeleton.js";
|
|
45
|
-
import { fromFlagConflict, isHelpInvocation, isVersionInvocation, positionalArgs, refusedFlag, } from "./args.js";
|
|
49
|
+
import { createSourceFlagRefusal, fromFlagConflict, isHelpInvocation, isVersionInvocation, positionalArgs, refusedFlag, } from "./args.js";
|
|
46
50
|
import { parseSourceRef } from "../lib/sourceRef.js";
|
|
47
|
-
import { printFlagConflict, printSourceRefFailure } from "../lib/sourceRefCopy.js";
|
|
51
|
+
import { printCreateSourceFlagRefusal, printFlagConflict, printSourceRefFailure, } from "../lib/sourceRefCopy.js";
|
|
48
52
|
import { registerAfterScaffold } from "../lib/initFrom.js";
|
|
49
|
-
import {
|
|
53
|
+
import { registerProject } from "../lib/projects.js";
|
|
54
|
+
import { sanitizeAppName } from "../lib/initCommand.js";
|
|
50
55
|
import { resolveOrgForCommand } from "../lib/lifecycle.js";
|
|
51
56
|
// The console origin serves /cli-auth (the browser target for `vincentt login`). VINCENTT_
|
|
52
57
|
// CONSOLE_URL overrides for dev/dogfood; VINCENTT_EDITOR_URL is honored as a legacy alias.
|
|
@@ -75,6 +80,15 @@ function projectDir() {
|
|
|
75
80
|
function positional(n) {
|
|
76
81
|
return positionalArgs(argv)[n];
|
|
77
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* The value of a `--flag=value` spelling, which `flag()` cannot see at all (it is
|
|
85
|
+
* a bare `argv.indexOf`). Only used to echo the creator's own argument back into a
|
|
86
|
+
* refusal's remedy, so both spellings produce a copy-pasteable line.
|
|
87
|
+
*/
|
|
88
|
+
function valueOfEqualsFlag(args, name) {
|
|
89
|
+
const hit = args.find((a) => a.startsWith(`${name}=`));
|
|
90
|
+
return hit?.slice(name.length + 1) || undefined;
|
|
91
|
+
}
|
|
78
92
|
// ---- auth -------------------------------------------------------------------
|
|
79
93
|
async function resolveAuthUrls() {
|
|
80
94
|
const machine = await loadMachineConfig();
|
|
@@ -204,7 +218,9 @@ async function init() {
|
|
|
204
218
|
},
|
|
205
219
|
resolveOrg: async () => {
|
|
206
220
|
const cfg = await resolveConfig(rd.targetDir, { warn: print });
|
|
207
|
-
const resolved = await resolveOrgForCommand(cfg, {
|
|
221
|
+
const resolved = await resolveOrgForCommand(cfg, {
|
|
222
|
+
flag: flag("--org"),
|
|
223
|
+
});
|
|
208
224
|
if (!resolved.ok)
|
|
209
225
|
return resolved;
|
|
210
226
|
return {
|
|
@@ -213,7 +229,10 @@ async function init() {
|
|
|
213
229
|
source: orgSource(),
|
|
214
230
|
};
|
|
215
231
|
},
|
|
216
|
-
|
|
232
|
+
// registerProject, NOT createCommand: createCommand is now the
|
|
233
|
+
// whole two-half verb, so calling it here would classify and
|
|
234
|
+
// scaffold a second time inside the half that already cloned.
|
|
235
|
+
create: async ({ name, slug }) => registerProject({
|
|
217
236
|
print,
|
|
218
237
|
cwd: rd.targetDir,
|
|
219
238
|
config: () => resolveConfig(rd.targetDir, { warn: print }),
|
|
@@ -239,13 +258,54 @@ function orgSource() {
|
|
|
239
258
|
return "active";
|
|
240
259
|
}
|
|
241
260
|
async function create() {
|
|
242
|
-
|
|
261
|
+
// NO requireLogin() here, deliberately. The scaffold half must not depend on a
|
|
262
|
+
// session: `create` in an empty folder writes the open template, and a gate at
|
|
263
|
+
// the top of the verb would put it behind the auth gate. The session is
|
|
264
|
+
// consulted BELOW the seam, inside the verb. `link()` three lines down still
|
|
265
|
+
// calls requireLogin, correctly — do not copy it up here.
|
|
266
|
+
const print = (l) => console.log(l);
|
|
267
|
+
const dirLabel = positional(1) ?? ".";
|
|
268
|
+
// The parser refusal runs FIRST, before anything is read or written. `flag()` is
|
|
269
|
+
// a global argv scan with no per-verb gate, so a single natural copy of
|
|
270
|
+
// `template: flag("--template")` into this function would make `create` an
|
|
271
|
+
// unauthenticated arbitrary-ref clone-and-execute, and nothing would go red.
|
|
272
|
+
const refusedSource = createSourceFlagRefusal(argv);
|
|
273
|
+
if (refusedSource) {
|
|
274
|
+
printCreateSourceFlagRefusal(print, refusedSource, {
|
|
275
|
+
dir: dirLabel,
|
|
276
|
+
value: flag(refusedSource) ?? valueOfEqualsFlag(argv, refusedSource),
|
|
277
|
+
});
|
|
278
|
+
process.exitCode = 1;
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const raw = positional(1);
|
|
282
|
+
let dir;
|
|
283
|
+
if (raw !== undefined) {
|
|
284
|
+
const safe = sanitizeAppName(raw);
|
|
285
|
+
if (safe === null) {
|
|
286
|
+
print(`✗ "${raw.trim()}" is not a folder name.`);
|
|
287
|
+
print("");
|
|
288
|
+
print(" Use a plain name (my-ar-app), or `vincentt create` for this folder.");
|
|
289
|
+
process.exitCode = 1;
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
dir = safe;
|
|
293
|
+
}
|
|
294
|
+
const interactive = Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
|
|
243
295
|
const result = await projectCreate(projectDir(), {
|
|
244
296
|
name: flag("--name"),
|
|
245
297
|
slug: flag("--slug"),
|
|
246
298
|
org: flag("--org"),
|
|
247
299
|
force: has("--force"),
|
|
248
300
|
allowCustomApi: has("--allow-custom-api"),
|
|
301
|
+
dir,
|
|
302
|
+
skipInstall: has("--no-install"),
|
|
303
|
+
ask: interactive ? stdinAsk() : undefined,
|
|
304
|
+
currentToken,
|
|
305
|
+
runLogin: async () => {
|
|
306
|
+
await login();
|
|
307
|
+
},
|
|
308
|
+
orgSource,
|
|
249
309
|
onLine: (l) => console.log(l),
|
|
250
310
|
});
|
|
251
311
|
if (result.exitCode !== 0) {
|
|
@@ -279,6 +339,34 @@ async function unlink() {
|
|
|
279
339
|
config: () => resolveConfig(cwd),
|
|
280
340
|
});
|
|
281
341
|
}
|
|
342
|
+
/**
|
|
343
|
+
* `vincentt projects` — which projects does this workspace have?
|
|
344
|
+
*
|
|
345
|
+
* Reads the MACHINE config and the org precedence chain, never the folder's
|
|
346
|
+
* binding, for the same reason `whoami` does: it answers a question about the
|
|
347
|
+
* workspace. A folder-derived default would make the one verb a creator runs
|
|
348
|
+
* BECAUSE they are in an unbound folder depend on that folder having a binding.
|
|
349
|
+
*/
|
|
350
|
+
async function projects() {
|
|
351
|
+
await requireLogin();
|
|
352
|
+
const machine = await loadMachineConfig();
|
|
353
|
+
const config = async () => {
|
|
354
|
+
const apiUrl = process.env.VINCENTT_API_URL ?? machine.apiUrl ?? DEFAULT_API_URL;
|
|
355
|
+
const pat = process.env.VINCENTT_PAT ?? machine.pat;
|
|
356
|
+
return pat ? { apiUrl: apiUrl.replace(/\/$/, ""), pat } : undefined;
|
|
357
|
+
};
|
|
358
|
+
process.exitCode = await listProjectsCommand({
|
|
359
|
+
print: (l) => console.log(l),
|
|
360
|
+
config,
|
|
361
|
+
org: async () => {
|
|
362
|
+
const cfg = await config();
|
|
363
|
+
if (!cfg)
|
|
364
|
+
return { ok: false, message: "Not signed in. Run `vincentt login`." };
|
|
365
|
+
return resolveOrgForCommand(cfg, { flag: flag("--org") });
|
|
366
|
+
},
|
|
367
|
+
list: listProjects,
|
|
368
|
+
}, { json: has("--json") });
|
|
369
|
+
}
|
|
282
370
|
/**
|
|
283
371
|
* `vincentt publish` — Flows L22-L31.
|
|
284
372
|
*
|
|
@@ -417,6 +505,7 @@ async function preview() {
|
|
|
417
505
|
}
|
|
418
506
|
},
|
|
419
507
|
onCapReached: () => ui.capReached(),
|
|
508
|
+
onAnnotation: (tags) => ui.annotation(tags),
|
|
420
509
|
onTunnelState: (state) => ui.tunnel(state),
|
|
421
510
|
onOriginError: () => ui.originError(),
|
|
422
511
|
onOriginRecovered: () => ui.originRecovered(),
|
|
@@ -451,6 +540,7 @@ async function preview() {
|
|
|
451
540
|
orgName: await activeOrgName(),
|
|
452
541
|
targetUrl: `http://localhost:${running.appPort}`,
|
|
453
542
|
url: running.url,
|
|
543
|
+
consoleUrl: await projectConsoleUrl(cwd),
|
|
454
544
|
qr,
|
|
455
545
|
expiresInHours: 12,
|
|
456
546
|
})) {
|
|
@@ -560,6 +650,22 @@ async function projectDisplayName(cwd) {
|
|
|
560
650
|
const located = await locateProjectBinding(cwd);
|
|
561
651
|
return located?.binding.slug ?? path.basename(cwd);
|
|
562
652
|
}
|
|
653
|
+
/**
|
|
654
|
+
* The project's page in the console, or undefined for an unlinked folder.
|
|
655
|
+
*
|
|
656
|
+
* The path is the BARE-ID route (`/projects/<id>`), the same shape
|
|
657
|
+
* `consoleVerbCommand` deep-links to. The console redirects it to the org-scoped
|
|
658
|
+
* address, so the CLI does not need the org slug and cannot print a stale one — a
|
|
659
|
+
* folder can outlive the org binding the machine config happens to hold.
|
|
660
|
+
*/
|
|
661
|
+
async function projectConsoleUrl(cwd) {
|
|
662
|
+
const located = await locateProjectBinding(cwd);
|
|
663
|
+
if (!located)
|
|
664
|
+
return undefined;
|
|
665
|
+
const cfg = await loadMachineConfig();
|
|
666
|
+
const base = (cfg.consoleUrl ?? DEFAULT_CONSOLE_URL).replace(/\/$/, "");
|
|
667
|
+
return `${base}/projects/${located.binding.projectId}`;
|
|
668
|
+
}
|
|
563
669
|
/**
|
|
564
670
|
* Map a mint failure to its screen. Each cause has a DISTINCT first line so the
|
|
565
671
|
* creator never has to read a second, and none of them names the edge, the API
|
|
@@ -598,28 +704,179 @@ async function diag(kind) {
|
|
|
598
704
|
}
|
|
599
705
|
}
|
|
600
706
|
// ---- reverse channel --------------------------------------------------------
|
|
707
|
+
/**
|
|
708
|
+
* `vincentt feedback` — the reverse channel's consumer, and the one verb whose
|
|
709
|
+
* EXIT CODE is the contract.
|
|
710
|
+
*
|
|
711
|
+
* | exit | meaning | stdout | cursor |
|
|
712
|
+
* |------|------------------------------------------|-----------|-----------|
|
|
713
|
+
* | 0 | one or more annotations handed over | NDJSON | advanced |
|
|
714
|
+
* | 64 | the bounded window elapsed, nothing new | *(empty)* | untouched |
|
|
715
|
+
* | 65 | the preview session has ended | *(empty)* | untouched |
|
|
716
|
+
* | 1 | usage error, or an unexpected fault | *(empty)* | untouched |
|
|
717
|
+
*
|
|
718
|
+
* A caller never parses stdout to decide whether to re-park: empty stdout is
|
|
719
|
+
* never ambiguous, because 64 and 65 differ. 64 and 65 are NOT errors — a
|
|
720
|
+
* recipe must be able to loop on 64 without also swallowing real failures.
|
|
721
|
+
*
|
|
722
|
+
* 64/65 sit above the band generic tooling overloads, below the shell-reserved
|
|
723
|
+
* 126, and deliberately avoid 124: GNU `timeout` emits 124, and a recipe that
|
|
724
|
+
* wraps this command in `timeout` would otherwise be unable to tell its own
|
|
725
|
+
* timeout from ours.
|
|
726
|
+
*
|
|
727
|
+
* The contract is APPEND-ONLY, permanently: a code may be added, an existing
|
|
728
|
+
* code's meaning never changes, a flag once accepted stays accepted, and
|
|
729
|
+
* stdout's one-JSON-per-line grammar never changes shape.
|
|
730
|
+
*/
|
|
601
731
|
async function feedback() {
|
|
602
732
|
const cwd = projectDir();
|
|
603
733
|
const timeoutMs = (numFlag("--timeout") ?? 300) * 1000;
|
|
734
|
+
const verbose = has("--verbose");
|
|
604
735
|
const watch = has("--watch");
|
|
605
736
|
if (!watch && !has("--wait")) {
|
|
606
|
-
console.error(
|
|
737
|
+
console.error(feedbackCopy.USAGE);
|
|
607
738
|
process.exitCode = 1;
|
|
608
739
|
return;
|
|
609
740
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
741
|
+
// process.exit() would truncate a pending pipe write, so every path here
|
|
742
|
+
// assigns exitCode and RETURNS — the loop drains on its own.
|
|
743
|
+
process.exitCode = watch
|
|
744
|
+
? await feedbackWatch(cwd, timeoutMs, verbose)
|
|
745
|
+
: await feedbackWait(cwd, timeoutMs, verbose);
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* The drain. `--wait` hands over EVERY pending annotation and then exits, not
|
|
749
|
+
* exactly one: a creator who walked the room and sent three is one hand-off, and
|
|
750
|
+
* three round trips would be three wakes for one thought.
|
|
751
|
+
*
|
|
752
|
+
* The first park uses the full window; every subsequent probe asks "is there
|
|
753
|
+
* another RIGHT NOW" with `timeoutMs: 0` and must not block, or the batch
|
|
754
|
+
* silently costs one window per annotation.
|
|
755
|
+
*/
|
|
756
|
+
async function feedbackWait(cwd, timeoutMs, verbose) {
|
|
757
|
+
for (const l of feedbackCopy.waiting())
|
|
758
|
+
console.error(l);
|
|
759
|
+
let after = await readCursor(cwd);
|
|
760
|
+
let handed = 0;
|
|
761
|
+
for (;;) {
|
|
762
|
+
const result = await fetchNext(cwd, {
|
|
763
|
+
after,
|
|
764
|
+
timeoutMs: handed === 0 ? timeoutMs : 0,
|
|
765
|
+
});
|
|
766
|
+
if (result.kind === "empty") {
|
|
767
|
+
if (handed > 0)
|
|
768
|
+
break;
|
|
769
|
+
for (const l of feedbackCopy.windowElapsed(timeoutMs))
|
|
770
|
+
console.error(l);
|
|
771
|
+
return 64;
|
|
772
|
+
}
|
|
773
|
+
if (result.kind === "closed") {
|
|
774
|
+
if (handed > 0)
|
|
775
|
+
break;
|
|
776
|
+
for (const l of feedbackCopy.sessionEnded(handed))
|
|
777
|
+
console.error(l);
|
|
778
|
+
if (verbose) {
|
|
779
|
+
for (const l of feedbackCopy.sessionEndedDetail(result.reason, await relayInfoDir(cwd), after)) {
|
|
780
|
+
console.error(l);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return 65;
|
|
784
|
+
}
|
|
785
|
+
await handOver(cwd, result.annotation);
|
|
786
|
+
after = result.annotation.seq;
|
|
787
|
+
handed += 1;
|
|
788
|
+
}
|
|
789
|
+
// A session that ended MID-BATCH is still a successful hand-off: exiting 65
|
|
790
|
+
// here would tell a polling host to stop after it had just been given work.
|
|
791
|
+
// The un-drained tail is not skipped — the cursor names only what was printed.
|
|
792
|
+
for (const l of feedbackCopy.handedOver(handed))
|
|
793
|
+
console.error(l);
|
|
794
|
+
return 0;
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* `--watch` streams for a human and does not return on an elapsed window — it
|
|
798
|
+
* re-arms. It DOES return 65 when the session ends, rather than looping forever
|
|
799
|
+
* against a channel that can never speak again.
|
|
800
|
+
*
|
|
801
|
+
* `--timeout` is accepted here (a flag once accepted is accepted forever) and
|
|
802
|
+
* names the internal re-arm interval rather than a bounded window.
|
|
803
|
+
*/
|
|
804
|
+
async function feedbackWatch(cwd, timeoutMs, verbose) {
|
|
805
|
+
for (const l of feedbackCopy.watching())
|
|
806
|
+
console.error(l);
|
|
807
|
+
// A human stopping a watch is not a failure, so it ends 0 — and the ending
|
|
808
|
+
// says the PREVIEW is still live, because what stopped is this command.
|
|
809
|
+
//
|
|
810
|
+
// An interrupt between a completed write and its cursor advance leaves the
|
|
811
|
+
// cursor behind, which re-delivers that annotation on the next run. That is
|
|
812
|
+
// at-least-once working as designed, and the safe direction.
|
|
813
|
+
process.on("SIGINT", () => {
|
|
814
|
+
for (const l of feedbackCopy.stoppedWatching())
|
|
815
|
+
console.error(l);
|
|
816
|
+
process.exit(0);
|
|
817
|
+
});
|
|
818
|
+
let after = await readCursor(cwd);
|
|
819
|
+
let handed = 0;
|
|
820
|
+
// A `--timeout 0` here would make the re-arm a busy loop against the relay
|
|
821
|
+
// rather than a parked request. The flag stays ACCEPTED (it always has been)
|
|
822
|
+
// and the floor is applied silently: a watch's window is an implementation
|
|
823
|
+
// detail of its re-arm, not a contract the caller reads back.
|
|
824
|
+
const armMs = Math.max(timeoutMs, 1_000);
|
|
825
|
+
for (;;) {
|
|
826
|
+
const result = await fetchNext(cwd, { after, timeoutMs: armMs });
|
|
827
|
+
if (result.kind === "empty")
|
|
617
828
|
continue;
|
|
829
|
+
if (result.kind === "closed") {
|
|
830
|
+
// `{N}` is what THIS invocation handed over, which for a watch that ran
|
|
831
|
+
// for an hour is not 0. The ending is C4's, exit code included: a watch
|
|
832
|
+
// against a dead channel would otherwise loop forever.
|
|
833
|
+
for (const l of feedbackCopy.sessionEnded(handed))
|
|
834
|
+
console.error(l);
|
|
835
|
+
if (verbose) {
|
|
836
|
+
for (const l of feedbackCopy.sessionEndedDetail(result.reason, await relayInfoDir(cwd), after)) {
|
|
837
|
+
console.error(l);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
return 65;
|
|
618
841
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
842
|
+
const a = result.annotation;
|
|
843
|
+
await handOver(cwd, a);
|
|
844
|
+
// The human's copy, on stderr, in the second fidelity. stdout stays the
|
|
845
|
+
// exact NDJSON, so `feedback --watch > out.ndjson` is still clean.
|
|
846
|
+
console.error("");
|
|
847
|
+
for (const l of feedbackCopy.watchEntry({
|
|
848
|
+
createdAt: a.createdAt,
|
|
849
|
+
tester: a.sessionId,
|
|
850
|
+
message: a.message,
|
|
851
|
+
screenshotPath: a.screenshotPath,
|
|
852
|
+
}, process.stderr.columns)) {
|
|
853
|
+
console.error(l);
|
|
854
|
+
}
|
|
855
|
+
after = a.seq;
|
|
856
|
+
handed += 1;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* THE ORDERING THAT MAKES DELIVERY AT-LEAST-ONCE.
|
|
861
|
+
*
|
|
862
|
+
* The cursor advances only inside write's completion callback, having fired
|
|
863
|
+
* without error. On a PIPE — the consumer case this channel exists for —
|
|
864
|
+
* `write()` is ASYNCHRONOUS and returns before the bytes reach the OS.
|
|
865
|
+
* Advancing on return and then being killed loses the annotation with the
|
|
866
|
+
* cursor already past it: at-most-once, silently, with no way for the creator to
|
|
867
|
+
* learn their words were gone.
|
|
868
|
+
*
|
|
869
|
+
* `console.log` is barred from this path for the same reason — it offers no
|
|
870
|
+
* completion callback, so there is no point at which the cursor may legally
|
|
871
|
+
* advance. There is no `stdout.flush()`; Node streams have no such method, and
|
|
872
|
+
* the callback is the only correct signal.
|
|
873
|
+
*/
|
|
874
|
+
async function handOver(cwd, a) {
|
|
875
|
+
const line = JSON.stringify(a) + "\n";
|
|
876
|
+
await new Promise((resolve, reject) => {
|
|
877
|
+
process.stdout.write(line, (err) => (err ? reject(err) : resolve()));
|
|
878
|
+
});
|
|
879
|
+
await writeCursor(cwd, a.seq);
|
|
623
880
|
}
|
|
624
881
|
// ---- meta -------------------------------------------------------------------
|
|
625
882
|
/**
|
|
@@ -746,7 +1003,7 @@ async function assets() {
|
|
|
746
1003
|
// Not signed in — the exit-4 screen, because we did not answer. The library is
|
|
747
1004
|
// a platform call, and the creator's PROJECT is unaffected either way.
|
|
748
1005
|
const { unreachableLines } = await import("../library/render.js");
|
|
749
|
-
for (const l of unreachableLines("auth",
|
|
1006
|
+
for (const l of unreachableLines("auth", 'vincentt assets search "<what you want>"')) {
|
|
750
1007
|
console.error(l);
|
|
751
1008
|
}
|
|
752
1009
|
process.exitCode = 4;
|
|
@@ -803,70 +1060,9 @@ async function relay() {
|
|
|
803
1060
|
});
|
|
804
1061
|
}
|
|
805
1062
|
// ---- dispatch ---------------------------------------------------------------
|
|
806
|
-
function usage() {
|
|
807
|
-
console.log(
|
|
808
|
-
|
|
809
|
-
`Getting started (building a new app with a coding agent)\n` +
|
|
810
|
-
` 1. Confirm a project name with the developer first.\n` +
|
|
811
|
-
` 2. Scaffold: vincentt init <dir> (no account needed)\n` +
|
|
812
|
-
` 3. Register: vincentt create --name "<name>"\n` +
|
|
813
|
-
` 4. Read AGENTS.md, confirm it is ready, and stop — do NOT invent an app.\n` +
|
|
814
|
-
` Wait for the developer to describe what to build.\n` +
|
|
815
|
-
` Full guide: npm view @vincentt-xr/cli readme\n\n` +
|
|
816
|
-
`Auth\n` +
|
|
817
|
-
` login Sign in; save an access token to ~/.vincentt/config.json\n` +
|
|
818
|
-
` [--api <url>] [--console <url>] to target a non-prod env\n` +
|
|
819
|
-
` logout Remove the saved access token\n` +
|
|
820
|
-
` whoami Show the signed-in account\n\n` +
|
|
821
|
-
`Project\n` +
|
|
822
|
-
` init [dir] Scaffold a Vincentt XR app — no account needed\n` +
|
|
823
|
-
` [--no-install] [--template <version>]\n` +
|
|
824
|
-
` [--from <owner>/<repo>@<tag>] Clone a gallery template\n` +
|
|
825
|
-
` and register it [--name <s>] [--slug <s>]\n` +
|
|
826
|
-
` create Register a project and reserve its address\n` +
|
|
827
|
-
` [--name <s>] [--slug <s>] [--org <s>] [--preview] [--force]\n` +
|
|
828
|
-
` link <project-id> Link this folder to an existing project\n` +
|
|
829
|
-
` unlink Disconnect this folder from its project\n\n` +
|
|
830
|
-
`Dev loop\n` +
|
|
831
|
-
` publish Upload the built dist; go live at <slug>.vincentt.app\n` +
|
|
832
|
-
` [--dist <path>] [--note <s>] [--yes]\n` +
|
|
833
|
-
` preview Serve the app at an https link a phone can open\n` +
|
|
834
|
-
` [--port <n>] [--tester <id>] [--no-qr] [--stop]\n` +
|
|
835
|
-
` Re-running prints the live link; --restart mints a new one.\n\n` +
|
|
836
|
-
`Diagnostics (need a running preview)\n` +
|
|
837
|
-
` logs [--errors] Console logs from the device [--json] [--tester <id>]\n` +
|
|
838
|
-
` network fetch/XHR requests [--json] [--tester <id>]\n` +
|
|
839
|
-
` trace Performance samples [--json] [--tester <id>]\n` +
|
|
840
|
-
` --tester t1 filters to one tester (one tab, across\n` +
|
|
841
|
-
` its refreshes). --viewer <label> filters to a single\n` +
|
|
842
|
-
` connection; one tab opens several.\n\n` +
|
|
843
|
-
`Asset library\n` +
|
|
844
|
-
` assets search "<query>" Find curated assets [--kind prop|frame]\n` +
|
|
845
|
-
` [--render 3d-clay] [--palette sage] [--limit 20] [--json]\n` +
|
|
846
|
-
` assets show <id> [<id>…] Everything known about an asset [--json]\n` +
|
|
847
|
-
` assets add <id> [<id>…] Print the URLs to reference from your source\n` +
|
|
848
|
-
` Writes nothing. [--download [--dir <path>] [--force]]\n` +
|
|
849
|
-
` [--json]\n` +
|
|
850
|
-
` Ids look like lib_7f3a2c67d4e5b6a7; search returns\n` +
|
|
851
|
-
` them. Reference only ids the tool returned.\n\n` +
|
|
852
|
-
`Reverse channel\n` +
|
|
853
|
-
` feedback --wait Block for the next phone annotation, print it, exit\n` +
|
|
854
|
-
` feedback --watch Stream annotations as they arrive\n\n` +
|
|
855
|
-
`Servers\n` +
|
|
856
|
-
` relay Run the diagnostics/annotation relay\n\n` +
|
|
857
|
-
`Meta\n` +
|
|
858
|
-
` version CLI version, and the API this machine resolves to\n` +
|
|
859
|
-
` dx Where this dev loop fought you [--days <n>] [--json]\n` +
|
|
860
|
-
` Reads a local journal of your own runs. Local by default.\n` +
|
|
861
|
-
` --send reports the findings to the Vincentt team; it\n` +
|
|
862
|
-
` shows the exact payload and asks first. [--note <text>]\n` +
|
|
863
|
-
` dx enable Allow reports to be sent from this machine without asking.\n` +
|
|
864
|
-
` Asks you to confirm in your terminal. Stays on until you\n` +
|
|
865
|
-
` disable it, and is shown on \`vincentt dx\` and\n` +
|
|
866
|
-
` \`vincentt version\` while on.\n` +
|
|
867
|
-
` dx disable Turn that off again.\n\n` +
|
|
868
|
-
`Common options: --dir <path> (project dir, default cwd)\n` +
|
|
869
|
-
` --allow-custom-api honor a binding's apiUrl override\n`);
|
|
1063
|
+
export function usage() {
|
|
1064
|
+
console.log(USAGE_TEXT);
|
|
1065
|
+
return USAGE_TEXT;
|
|
870
1066
|
}
|
|
871
1067
|
async function main() {
|
|
872
1068
|
const cmd = argv[0];
|
|
@@ -881,7 +1077,19 @@ async function main() {
|
|
|
881
1077
|
usage();
|
|
882
1078
|
return;
|
|
883
1079
|
}
|
|
884
|
-
|
|
1080
|
+
const verb = resolveVerb(cmd);
|
|
1081
|
+
if (verb === undefined) {
|
|
1082
|
+
// Flow L30: rolling back and taking an address offline are console
|
|
1083
|
+
// actions, and a bare "unknown command" here would be the product failing
|
|
1084
|
+
// to answer a question it knows the answer to.
|
|
1085
|
+
if (cmd !== undefined && isConsoleOnlyVerb(cmd))
|
|
1086
|
+
return consoleOnly(cmd);
|
|
1087
|
+
console.error(`Unknown command: ${cmd}\n`);
|
|
1088
|
+
usage();
|
|
1089
|
+
process.exitCode = 1;
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
switch (verb) {
|
|
885
1093
|
case "version":
|
|
886
1094
|
return version();
|
|
887
1095
|
case "login":
|
|
@@ -898,6 +1106,8 @@ async function main() {
|
|
|
898
1106
|
return link();
|
|
899
1107
|
case "unlink":
|
|
900
1108
|
return unlink();
|
|
1109
|
+
case "projects":
|
|
1110
|
+
return projects();
|
|
901
1111
|
case "publish":
|
|
902
1112
|
return publish();
|
|
903
1113
|
case "preview":
|
|
@@ -916,15 +1126,6 @@ async function main() {
|
|
|
916
1126
|
return assets();
|
|
917
1127
|
case "dx":
|
|
918
1128
|
return dx();
|
|
919
|
-
default:
|
|
920
|
-
// Flow L30: rolling back and taking an address offline are console
|
|
921
|
-
// actions, and a bare "unknown command" here would be the product failing
|
|
922
|
-
// to answer a question it knows the answer to.
|
|
923
|
-
if (cmd !== undefined && isConsoleOnlyVerb(cmd))
|
|
924
|
-
return consoleOnly(cmd);
|
|
925
|
-
console.error(`Unknown command: ${cmd}\n`);
|
|
926
|
-
usage();
|
|
927
|
-
process.exitCode = 1;
|
|
928
1129
|
}
|
|
929
1130
|
}
|
|
930
1131
|
// Every invocation is journaled here, at the one site that sees all of them —
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const USAGE_TEXT: string;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// The help text, in its own module so it can be rendered without importing the
|
|
2
|
+
// CLI entrypoint — `cli/index.ts` calls `main()` at module scope, so importing it
|
|
3
|
+
// runs the CLI, writes the dx journal and sets process.exitCode.
|
|
4
|
+
export const USAGE_TEXT = `Vincentt CLI — local-first AR dev loop\n\n` +
|
|
5
|
+
`Usage: vincentt <command> [options]\n\n` +
|
|
6
|
+
`Getting started (building a new app with a coding agent)\n` +
|
|
7
|
+
` 1. Confirm a project name with the developer first.\n` +
|
|
8
|
+
` 2. Create: vincentt create <name> (scaffolds if the folder is empty)\n` +
|
|
9
|
+
` 3. The scaffold writes the agent contract into the project root. Read it,\n` +
|
|
10
|
+
` confirm the project is ready, and stop — do NOT invent an app.\n` +
|
|
11
|
+
` Wait for the developer to describe what to build.\n` +
|
|
12
|
+
` Full guide: npm view @vincentt-xr/cli readme\n\n` +
|
|
13
|
+
`Auth\n` +
|
|
14
|
+
` login Sign in; save an access token to ~/.vincentt/config.json\n` +
|
|
15
|
+
` [--api <url>] [--console <url>] to target a non-prod env\n` +
|
|
16
|
+
` logout Remove the saved access token\n` +
|
|
17
|
+
` whoami Show the signed-in account\n\n` +
|
|
18
|
+
`Project\n` +
|
|
19
|
+
` init [dir] Scaffold a Vincentt XR app — no account needed, ever\n` +
|
|
20
|
+
` [--no-install] [--template <version>]\n` +
|
|
21
|
+
` [--from <owner>/<repo>@<tag>] Clone a gallery template\n` +
|
|
22
|
+
` and register it [--name <s>] [--slug <s>]\n` +
|
|
23
|
+
` create [name] Register a project and reserve its address; scaffolds first\n` +
|
|
24
|
+
` if the folder is empty [--name <s>] [--slug <s>]\n` +
|
|
25
|
+
` [--org <s>] [--preview] [--force] [--no-install]\n` +
|
|
26
|
+
` link <project-id> Link this folder to an existing project\n` +
|
|
27
|
+
` unlink Disconnect this folder from its project\n` +
|
|
28
|
+
` projects List the workspace's projects and their ids [--json]\n` +
|
|
29
|
+
` [--org <s>] to list a workspace other than the active one\n\n` +
|
|
30
|
+
`Dev loop\n` +
|
|
31
|
+
` publish Upload the built dist; go live at <slug>.vincentt.app\n` +
|
|
32
|
+
` [--dist <path>] [--note <s>] [--yes]\n` +
|
|
33
|
+
` preview Serve the app at an https link a phone can open\n` +
|
|
34
|
+
` [--port <n>] [--tester <id>] [--no-qr] [--stop]\n` +
|
|
35
|
+
` Re-running prints the live link; --restart mints a new one.\n\n` +
|
|
36
|
+
`Diagnostics (need a running preview)\n` +
|
|
37
|
+
` logs [--errors] Console logs from the device [--json] [--tester <id>]\n` +
|
|
38
|
+
` network fetch/XHR requests [--json] [--tester <id>]\n` +
|
|
39
|
+
` trace Performance samples [--json] [--tester <id>]\n` +
|
|
40
|
+
` --tester t1 filters to one tester (one tab, across\n` +
|
|
41
|
+
` its refreshes). --viewer <label> filters to a single\n` +
|
|
42
|
+
` connection; one tab opens several.\n\n` +
|
|
43
|
+
`Asset library\n` +
|
|
44
|
+
` assets search "<query>" Find curated assets [--kind prop|frame]\n` +
|
|
45
|
+
` [--render 3d-clay] [--palette sage] [--limit 20] [--json]\n` +
|
|
46
|
+
` assets show <id> [<id>…] Everything known about an asset [--json]\n` +
|
|
47
|
+
` assets add <id> [<id>…] Print the URLs to reference from your source\n` +
|
|
48
|
+
` Writes nothing. [--download [--dir <path>] [--force]]\n` +
|
|
49
|
+
` [--json]\n` +
|
|
50
|
+
` Ids look like lib_7f3a2c67d4e5b6a7; search returns\n` +
|
|
51
|
+
` them. Reference only ids the tool returned.\n\n` +
|
|
52
|
+
// Help text is SOLICITED output, so an instruction would be permitted here —
|
|
53
|
+
// and it is still written as facts, because these exact words get quoted into
|
|
54
|
+
// a creator's own agent instructions, where they are unbidden by the time
|
|
55
|
+
// anything acts on them.
|
|
56
|
+
//
|
|
57
|
+
// Every line below is load-bearing. The exit codes are named HERE because a
|
|
58
|
+
// reader who has to look them up elsewhere guesses instead. "Blocking costs
|
|
59
|
+
// nothing while it waits" is the sentence that kills the poll-on-a-timer
|
|
60
|
+
// pattern — that pattern exists because a blocked command is assumed
|
|
61
|
+
// expensive and polling cheap, which is exactly backwards. "the same command
|
|
62
|
+
// may be run again immediately" teaches the re-park loop without naming a
|
|
63
|
+
// host, a shell, or any background mechanism, and "nothing is missed and
|
|
64
|
+
// nothing is re-read" forecloses an invented seen-set or a `--since` flag
|
|
65
|
+
// that does not exist.
|
|
66
|
+
//
|
|
67
|
+
// DELIBERATELY ABSENT: any product name; any shell idiom; any mention of a
|
|
68
|
+
// scheduled task EVEN TO FORBID ONE (naming a bad pattern is how a reader
|
|
69
|
+
// learns it exists); any "recommended for AI" framing.
|
|
70
|
+
`Reverse channel — feedback sent from the phone\n` +
|
|
71
|
+
` feedback --wait Block until feedback arrives, print every annotation\n` +
|
|
72
|
+
` waiting as one JSON object per line, then exit.\n` +
|
|
73
|
+
` [--timeout <seconds>, default 300]\n` +
|
|
74
|
+
` Exit 0 one or more annotations printed\n` +
|
|
75
|
+
` Exit 64 the window passed with nothing waiting\n` +
|
|
76
|
+
` Exit 65 the preview ended; nothing more can arrive\n` +
|
|
77
|
+
` Blocking costs nothing while it waits. On 64 the same\n` +
|
|
78
|
+
` command may be run again immediately; nothing is\n` +
|
|
79
|
+
` missed and nothing is re-read. On 65 a preview has to\n` +
|
|
80
|
+
` be running before there is anything to wait on.\n` +
|
|
81
|
+
` feedback --watch The same stream, printed as it arrives, without exiting.\n` +
|
|
82
|
+
` For a person watching a terminal.\n\n` +
|
|
83
|
+
`Servers\n` +
|
|
84
|
+
` relay Run the diagnostics/annotation relay\n\n` +
|
|
85
|
+
`Meta\n` +
|
|
86
|
+
` version CLI version, and the API this machine resolves to\n` +
|
|
87
|
+
` dx Where this dev loop fought you [--days <n>] [--json]\n` +
|
|
88
|
+
` Reads a local journal of your own runs. Local by default.\n` +
|
|
89
|
+
` --send reports the findings to the Vincentt team; it\n` +
|
|
90
|
+
` shows the exact payload and asks first. [--note <text>]\n` +
|
|
91
|
+
` dx enable Allow reports to be sent from this machine without asking.\n` +
|
|
92
|
+
` Asks you to confirm in your terminal. Stays on until you\n` +
|
|
93
|
+
` disable it, and is shown on \`vincentt dx\` and\n` +
|
|
94
|
+
` \`vincentt version\` while on.\n` +
|
|
95
|
+
` dx disable Turn that off again.\n\n` +
|
|
96
|
+
`Common options: --dir <path> (project dir, default cwd)\n` +
|
|
97
|
+
` --allow-custom-api honor a binding's apiUrl override\n`;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const PINNED_VERBS: readonly ["version", "login", "logout", "whoami", "init", "create", "link", "unlink", "publish", "preview", "logs", "network", "trace", "feedback", "relay", "assets", "dx", "projects"];
|
|
2
|
+
export type PinnedVerb = (typeof PINNED_VERBS)[number];
|
|
3
|
+
export declare const VERB_HANDLERS: {
|
|
4
|
+
readonly version: true;
|
|
5
|
+
readonly login: true;
|
|
6
|
+
readonly logout: true;
|
|
7
|
+
readonly whoami: true;
|
|
8
|
+
readonly init: true;
|
|
9
|
+
readonly create: true;
|
|
10
|
+
readonly link: true;
|
|
11
|
+
readonly unlink: true;
|
|
12
|
+
readonly publish: true;
|
|
13
|
+
readonly preview: true;
|
|
14
|
+
readonly logs: true;
|
|
15
|
+
readonly network: true;
|
|
16
|
+
readonly trace: true;
|
|
17
|
+
readonly feedback: true;
|
|
18
|
+
readonly relay: true;
|
|
19
|
+
readonly assets: true;
|
|
20
|
+
readonly dx: true;
|
|
21
|
+
readonly projects: true;
|
|
22
|
+
};
|
|
23
|
+
export type DispatchVerb = keyof typeof VERB_HANDLERS;
|
|
24
|
+
export declare function resolveVerb(cmd: string | undefined): DispatchVerb | undefined;
|