@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.
Files changed (52) hide show
  1. package/README.md +40 -13
  2. package/dist/cli/args.d.ts +12 -0
  3. package/dist/cli/args.js +40 -0
  4. package/dist/cli/index.d.ts +1 -1
  5. package/dist/cli/index.js +296 -95
  6. package/dist/cli/usage-text.d.ts +1 -0
  7. package/dist/cli/usage-text.js +97 -0
  8. package/dist/cli/verb-surface.d.ts +24 -0
  9. package/dist/cli/verb-surface.js +49 -0
  10. package/dist/dx/clusters.d.ts +0 -17
  11. package/dist/dx/clusters.js +50 -1
  12. package/dist/lib/backend.d.ts +11 -0
  13. package/dist/lib/backend.js +23 -0
  14. package/dist/lib/classifyTarget.d.ts +42 -0
  15. package/dist/lib/classifyTarget.js +83 -0
  16. package/dist/lib/feedback.d.ts +46 -5
  17. package/dist/lib/feedback.js +76 -19
  18. package/dist/lib/feedbackCopy.d.ts +70 -0
  19. package/dist/lib/feedbackCopy.js +163 -0
  20. package/dist/lib/initFrom.d.ts +26 -4
  21. package/dist/lib/initFrom.js +65 -41
  22. package/dist/lib/lifecycle.d.ts +28 -3
  23. package/dist/lib/lifecycle.js +99 -7
  24. package/dist/lib/listProjects.d.ts +41 -0
  25. package/dist/lib/listProjects.js +77 -0
  26. package/dist/lib/projects.d.ts +64 -8
  27. package/dist/lib/projects.js +316 -24
  28. package/dist/lib/sourceRefCopy.d.ts +19 -0
  29. package/dist/lib/sourceRefCopy.js +36 -0
  30. package/dist/preview/command.d.ts +11 -0
  31. package/dist/preview/command.js +19 -1
  32. package/dist/preview/copy.d.ts +7 -0
  33. package/dist/preview/copy.js +8 -1
  34. package/dist/preview/runner.d.ts +6 -0
  35. package/dist/preview/runner.js +1 -0
  36. package/dist/preview/stream.d.ts +11 -0
  37. package/dist/preview/stream.js +13 -0
  38. package/dist/relay/annotations.d.ts +28 -4
  39. package/dist/relay/annotations.js +23 -7
  40. package/dist/relay/qa.f12.fixtures.d.ts +154 -0
  41. package/dist/relay/qa.f12.fixtures.js +358 -0
  42. package/dist/relay/server.d.ts +41 -3
  43. package/dist/relay/server.js +90 -32
  44. package/package.json +2 -2
  45. package/dist/library/qa.f5-b32.spec.d.ts +0 -1
  46. package/dist/library/qa.f5-b32.spec.js +0 -202
  47. package/dist/library/qa.f5-cli.spec.d.ts +0 -1
  48. package/dist/library/qa.f5-cli.spec.js +0 -433
  49. package/dist/library/qa.f5-contract.spec.d.ts +0 -1
  50. package/dist/library/qa.f5-contract.spec.js +0 -206
  51. package/dist/relay/injectors.d.ts +0 -29
  52. 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 { createCommand } from "../lib/projects.js";
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, { flag: flag("--org") });
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
- create: async ({ name, slug }) => createCommand({
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
- await requireLogin();
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("Usage: vincentt feedback --wait | --watch [--timeout <seconds>]");
737
+ console.error(feedbackCopy.USAGE);
607
738
  process.exitCode = 1;
608
739
  return;
609
740
  }
610
- do {
611
- const after = await readCursor(cwd);
612
- const annotation = await fetchNext(cwd, { after, timeoutMs });
613
- if (!annotation) {
614
- // 204 re-arm signal: --wait exits empty, --watch loops.
615
- if (!watch)
616
- return;
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
- // Print BEFORE advancing the cursor → at-least-once (a crash re-delivers).
620
- console.log(JSON.stringify(annotation));
621
- await writeCursor(cwd, annotation.seq);
622
- } while (watch);
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", "vincentt assets search \"<what you want>\"")) {
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(`Vincentt CLI — local-first AR dev loop\n\n` +
808
- `Usage: vincentt <command> [options]\n\n` +
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
- switch (cmd) {
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;