@vincentt-xr/cli 0.17.0 → 0.18.1

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 (95) hide show
  1. package/MIGRATION.md +31 -0
  2. package/README.md +94 -1
  3. package/dist/cli/index.js +170 -84
  4. package/dist/cli/usage-text.js +32 -10
  5. package/dist/domain/api.d.ts +10 -8
  6. package/dist/domain/api.js +14 -9
  7. package/dist/domain/command.js +25 -16
  8. package/dist/domain/copy.d.ts +6 -3
  9. package/dist/domain/copy.js +10 -6
  10. package/dist/dx/command.d.ts +24 -4
  11. package/dist/dx/command.js +80 -25
  12. package/dist/dx/consent.js +6 -5
  13. package/dist/dx/journal.d.ts +17 -2
  14. package/dist/dx/journal.js +19 -3
  15. package/dist/dx/report.js +4 -3
  16. package/dist/dx/reportIo.js +6 -5
  17. package/dist/dx/send.js +6 -5
  18. package/dist/lib/backend.d.ts +0 -16
  19. package/dist/lib/backend.js +2 -26
  20. package/dist/lib/detect.d.ts +87 -0
  21. package/dist/lib/detect.js +867 -0
  22. package/dist/lib/diagnostics.js +2 -2
  23. package/dist/lib/feedback.js +1 -1
  24. package/dist/lib/feedbackCopy.js +1 -1
  25. package/dist/lib/initFrom.js +4 -4
  26. package/dist/lib/lifecycle.d.ts +2 -0
  27. package/dist/lib/lifecycle.js +9 -3
  28. package/dist/lib/listProjects.js +2 -1
  29. package/dist/lib/projects.d.ts +37 -2
  30. package/dist/lib/projects.js +86 -21
  31. package/dist/lib/settings.d.ts +113 -0
  32. package/dist/lib/settings.js +284 -0
  33. package/dist/lib/settingsCopy.d.ts +6 -0
  34. package/dist/lib/settingsCopy.js +99 -0
  35. package/dist/lib/settle.d.ts +61 -0
  36. package/dist/lib/settle.js +338 -0
  37. package/dist/lib/template.d.ts +19 -0
  38. package/dist/lib/template.js +73 -0
  39. package/dist/lib/terminalText.d.ts +3 -0
  40. package/dist/lib/terminalText.js +10 -0
  41. package/dist/lib/version.js +5 -11
  42. package/dist/lib/whoami.js +10 -3
  43. package/dist/library/assetsCommand.js +1 -1
  44. package/dist/library/client.js +8 -3
  45. package/dist/library/render.js +8 -6
  46. package/dist/login/authBackend.js +3 -2
  47. package/dist/login/ensure.d.ts +8 -2
  48. package/dist/login/ensure.js +21 -10
  49. package/dist/login/guard.js +5 -4
  50. package/dist/login/loginCommand.d.ts +2 -2
  51. package/dist/login/loginCommand.js +29 -5
  52. package/dist/login/pkce.js +1 -1
  53. package/dist/login/pkceLogin.js +1 -1
  54. package/dist/platform/outdated.js +2 -2
  55. package/dist/platform/upgradeReport.js +16 -12
  56. package/dist/preview/api.d.ts +5 -11
  57. package/dist/preview/api.js +13 -15
  58. package/dist/preview/command.d.ts +29 -0
  59. package/dist/preview/command.js +61 -1
  60. package/dist/preview/copy.d.ts +35 -3
  61. package/dist/preview/copy.js +102 -10
  62. package/dist/preview/proxy.d.ts +38 -1
  63. package/dist/preview/proxy.js +151 -2
  64. package/dist/preview/qrPage.js +1 -1
  65. package/dist/preview/runner.d.ts +25 -13
  66. package/dist/preview/runner.js +127 -26
  67. package/dist/preview/viewers.js +1 -1
  68. package/dist/publish/api.d.ts +22 -5
  69. package/dist/publish/api.js +80 -9
  70. package/dist/publish/command.d.ts +27 -0
  71. package/dist/publish/command.js +261 -50
  72. package/dist/publish/copy.d.ts +78 -5
  73. package/dist/publish/copy.js +271 -37
  74. package/dist/publish/fit.d.ts +15 -0
  75. package/dist/publish/fit.js +103 -0
  76. package/dist/publish/manifest.d.ts +44 -3
  77. package/dist/publish/manifest.js +172 -3
  78. package/dist/publish/prompt.d.ts +4 -0
  79. package/dist/publish/prompt.js +15 -1
  80. package/dist/publish/upload.d.ts +59 -5
  81. package/dist/publish/upload.js +138 -30
  82. package/dist/publish/visibility.d.ts +21 -0
  83. package/dist/publish/visibility.js +68 -0
  84. package/dist/relay/annotations.js +9 -4
  85. package/dist/relay/server.d.ts +21 -0
  86. package/dist/relay/server.js +35 -1
  87. package/dist/shared/authError.d.ts +27 -0
  88. package/dist/shared/authError.js +48 -4
  89. package/dist/shared/config.d.ts +21 -1
  90. package/dist/shared/config.js +39 -8
  91. package/dist/shared/countLimit.d.ts +7 -2
  92. package/dist/shared/countLimit.js +12 -5
  93. package/dist/shared/http.d.ts +23 -0
  94. package/dist/shared/http.js +58 -0
  95. package/package.json +7 -3
package/MIGRATION.md ADDED
@@ -0,0 +1,31 @@
1
+ # Migration notes
2
+
3
+ Per version, newest first. Read by the platform's own version surface and served to
4
+ creators' agents. Accurate or absent, never speculative (B-F5-2).
5
+
6
+ ## 0.18.1
7
+
8
+ required: false
9
+ No action required. Behaviour that changed:
10
+ - `publish` re-opens the upload when an upload link expires (at most twice) instead of failing. Network errors and S3 5xx are retried with backoff. A refused upload reports "changed while uploading" only when S3 says the bytes do not match; other refusals name the failed file.
11
+ - A publish that fails on the network leaves its upload open, so running `vincentt publish` again sends only the files that have not landed yet (on an API that supports it). Files stream from disk instead of being read into memory.
12
+
13
+ ## 0.18.0
14
+
15
+ required: false
16
+ No action required for a project made from the Vincentt template. Behaviour that changed:
17
+ - An app not made from the template can be linked; `vincentt link` then writes a committed `vincentt.json` describing how it runs. `publish --dist <folder>` resolves against the linked project folder, not the current directory, and refuses a folder that is the project root, lies outside it, or holds a hidden file other than `.well-known` / `.nojekyll`.
18
+ - The preview proxy and the local relay listen on 127.0.0.1 only. The preview proxy gzips text responses (JavaScript, CSS, HTML, JSON, SVG) for clients that accept gzip.
19
+ - Every verb that needs an account now exits 4 (not signed in) with the `vincentt login` remedy, whether no token is stored or the API refuses the stored one; with no stored token `publish`, `preview`, `link`, `projects`, `whoami` and `dx` used to exit 1. A running preview whose heartbeat is refused ends and exits 4. Without a terminal, `link` and `projects` refuse at once instead of waiting on a browser sign-in. `create` in a folder that already holds an app still exits 1.
20
+ - `publish` on a project Vincentt has taken offline over its traffic allowance exits 1 before prompting and uploads nothing; it used to upload the whole build and be refused at the end. `publish` prints `Live at` only for an address that is serving. A published address that serves nothing shows a not-serving checklist, and an unrecognised state defaults its prompt to no.
21
+ - A preview ending names its real cause (for example stopped from another terminal, or abandoned) instead of always reporting a dropped connection. `preview --stop` exits 1 when there was no preview to stop. `domain attach` at the custom-domain limit states the real allowance; a plan with none is told custom domains are part of the paid plans.
22
+ - With no API configured, every verb uses https://api.vincentt.studio; `publish` and `preview` used to fall back to http://localhost:5051. Every API call and upload now gives up after a deadline instead of waiting forever.
23
+ - A publish whose final switch-over answer is lost retries up to three times, then says the version may or may not be live instead of crashing. Ctrl-D at a name or address prompt cancels instead of hanging. Output lines no longer use em-dashes (the preview ending lines are unchanged for now), so a script matching the old wording exactly needs updating.
24
+ - `publish` prints `Live at` only once the public address serves the new `index.html`, printing a `Propagating` line while it waits (up to 90 s). If the wait runs out it still exits 0 and says the version is published but not yet visible. `publish --json` (with `--yes`) prints one JSON object on stdout whose `state` is `live`, `committed_not_visible` or `failed`; the human output moves to stderr.
25
+ - `login` with a stored token checks it before reporting signed in. A refused token runs the sign-in again; when the account cannot be checked the token is kept and `login` exits 1 (it used to exit 0 and print "your account"). `init --from` re-asks for an address when the chosen one is taken, up to three attempts, instead of exiting at the first refusal.
26
+ - `vincentt dx` and `dx --send` look at the last 24 hours of runs by default, not the last 7 days. `--since <duration>` (for example `30m`, `2h`, `7d`) sets the window and the report names it; an unreadable duration exits 1. `--days <n>` still works. `feedback --wait` no longer skips an annotation when several arrive at once; every one is handed over in order.
27
+
28
+ ## 0.17.0
29
+
30
+ required: false
31
+ No action required.
package/README.md CHANGED
@@ -65,7 +65,10 @@ Project
65
65
  if the folder is empty
66
66
  [--name <s>] [--slug <s>] [--org <s>] [--preview]
67
67
  [--force] [--no-install]
68
- link <project-id> Link this directory to an existing project
68
+ link <project-id> Link this directory to an existing project; for an app not
69
+ made from the template, also writes vincentt.json
70
+ [--json] [--dev-command <cmd>] [--dev-port <n>]
71
+ [--build-command <cmd>] [--build-output <folder>]
69
72
  unlink Detach this directory from its project
70
73
  projects List the workspace's projects, their ids, and where each
71
74
  one lives on this machine
@@ -73,8 +76,11 @@ Project
73
76
 
74
77
  Dev loop
75
78
  publish Upload the built dist; go live at <slug>.vincentt.app
79
+ [--dist <folder>] (relative to the project folder)
80
+ [--yes] [--json]
76
81
  preview Serve + tunnel the app for on-device preview
77
82
  (prints a desktop QR page to scan) [--stop]
83
+ [--port <n>] [--dev-command <cmd>]
78
84
 
79
85
  Diagnostics (need a running preview)
80
86
  logs [--errors] Console logs from the device [--json]
@@ -115,6 +121,14 @@ later verbs run without prompting. `init` never reaches that gate at all.
115
121
  there. A count that cannot be read prints no line rather than a guess.
116
122
  - `npx vincentt publish` — build (`pnpm build`), then upload the built `dist/` and
117
123
  return the live `<slug>.vincentt.app` URL. Nothing builds server-side.
124
+ The CDN takes a few seconds to switch to a new version, so after the upload
125
+ `publish` prints a `→ Propagating` line and checks the address until it
126
+ serves the `index.html` just uploaded. Only then does it print `✓ Live at`. It
127
+ waits up to 90 seconds; past that it exits 0 and says the version is published
128
+ but not yet visible, because the publish did happen and publishing again would
129
+ not help. `--json` (with `--yes`) puts the human output on stderr and prints one
130
+ object on stdout whose `state` is `live` or `committed_not_visible` (with
131
+ `version`, `liveUrl` and `waitedMs`), or `failed` (with `exitCode`).
118
132
 
119
133
  In a directory that is not linked to anything, `npx vincentt projects` lists the
120
134
  workspace's projects with their ids, which is what `vincentt link <project-id>`
@@ -140,3 +154,82 @@ deleting `~/.vincentt/locations.ndjson` costs nothing but the shortcut.
140
154
 
141
155
  `vincentt` is a project devDependency in the scaffold, so invoke it as
142
156
  `npx vincentt <verb>` and it resolves to the local copy — no global install.
157
+
158
+ ## An app that isn't built on the Vincentt template
159
+
160
+ Any app with a local dev server and a static build output can join: create the
161
+ project in the console, then run `vincentt link <project-id>` in the app's folder.
162
+ (`vincentt create` also registers a non-empty folder whose `package.json` already
163
+ lists a `@vincentt-xr/*` package, and does the same.)
164
+
165
+ Besides the gitignored binding, `link` reads how the app runs from
166
+ `package.json`, the lockfile's name and at most one bundler config. **It runs
167
+ nothing**: no script, no config evaluation, no install. It writes what it found to
168
+ `vincentt.json` at the project root:
169
+
170
+ ```json
171
+ {
172
+ "$schema": "https://vincentt.studio/schema/vincentt.json",
173
+ "dev": { "command": "npm run serve", "port": 8080 },
174
+ "build": { "command": "npm run build", "output": "dist" }
175
+ }
176
+ ```
177
+
178
+ - `dev.command` is what `vincentt preview` starts, and `dev.port` is the port it
179
+ forwards to. `build.output` is the folder `vincentt publish` uploads.
180
+ `build.command` is recorded and printed, never run: publish uploads a folder
181
+ that is already built.
182
+ - **Commit it**, so every clone previews and publishes the same way. It holds no
183
+ project id, account, URL or credential.
184
+ - **A present file is the whole truth.** A field missing from it is refused by
185
+ name (`vincentt.json has no dev.port.`); defaults apply only when there is no
186
+ file at all. A flag (`--port`, `--dev-command`, `--dist`) overrides one field
187
+ for one run.
188
+ - `vincentt.json` runs a command on every collaborator's machine at `preview`,
189
+ like a `package.json` script. Review changes to it in pull requests. The
190
+ command runs without the CLI's access token in its environment.
191
+ - When detection cannot find a field, `link` still succeeds and prints the
192
+ `vincentt.json` edit and the one-run flag. In a terminal it asks for each value
193
+ first. `vincentt link <id> --json` prints the result for a program to read, and
194
+ never asks.
195
+ - `--dist` and `build.output` are relative to the project folder (the one that
196
+ holds `.vincentt/project.json`), not the directory the command runs in.
197
+ - A project made from the Vincentt template is recognised and gets no
198
+ `vincentt.json`: its defaults are `npm run dev`, port 5173 and `dist`.
199
+
200
+ ### Diagnostics for an app that isn't built on the Vincentt template
201
+
202
+ `vincentt preview` works without this. Adding it makes `vincentt logs`, `network` and
203
+ `trace` report what happens on the phone, and lets the console reach the preview.
204
+
205
+ 1. Install the harness:
206
+
207
+ ```sh
208
+ npm i @vincentt-xr/harness
209
+ ```
210
+
211
+ 2. Add this line at the top of your entry file, before anything else runs:
212
+
213
+ ```js
214
+ import "@vincentt-xr/harness/auto";
215
+ ```
216
+
217
+ That is the whole change. It needs no React and no build setting.
218
+
219
+ The line switches itself on only on a Vincentt preview address (`*.vincentt.dev`). Everywhere
220
+ else, including your published address, GitHub Pages, Vercel, Netlify and `localhost`, it does
221
+ nothing at all: no network request, no page change, no console output.
222
+
223
+ Do not add it to a project made from the Vincentt template; the template already includes the
224
+ harness.
225
+
226
+ ### What publish checks first
227
+
228
+ Before any upload, `vincentt publish` refuses an output that is the project
229
+ folder itself or lies outside it, holds any name starting with `.` (other than
230
+ `.well-known/` and `.nojekyll` at its root), holds a symlink that resolves outside
231
+ the project or onto a hidden path, or holds Git LFS pointer files instead of the
232
+ assets (`git lfs pull`, then rebuild). A `<script src>`, stylesheet, image or media
233
+ reference that starts with `/` and is not in the output is reported as a warning:
234
+ Vincentt serves the app at the root of its address, so a build made for a subpath
235
+ (a GitHub Pages project site) will not find those files.
package/dist/cli/index.js CHANGED
@@ -18,10 +18,11 @@ import { runPkceLogin } from "../login/pkceLogin.js";
18
18
  import { httpAuthBackend } from "../login/authBackend.js";
19
19
  import { loginCommand } from "../login/loginCommand.js";
20
20
  import { ensureLoggedIn } from "../login/ensure.js";
21
- import { clearMachineToken, loadMachineConfig, locateProjectBinding, resolveConfig, writeDxEnablement, MACHINE_CONFIG_PATH, } from "../shared/config.js";
21
+ import { clearMachineToken, loadMachineConfig, locateProjectBinding, machineApiUrl, resolveConfig, writeDxEnablement, MACHINE_CONFIG_PATH, } from "../shared/config.js";
22
22
  import { fileLocationStore } from "../shared/locationStore.js";
23
23
  import { getMe, listProjects, ProjectUnreachableError } from "../lib/backend.js";
24
24
  import { EXIT } from "../lib/exitCodes.js";
25
+ import { AuthExpiredError, authExpiredLines, isAuthRefused, NOT_SIGNED_IN_LINES, NotSignedInError, } from "../shared/authError.js";
25
26
  import { listProjectsCommand } from "../lib/listProjects.js";
26
27
  import { initCommand } from "../lib/initCommand.js";
27
28
  import { linkCommand, printUnreachable, unlinkCommand } from "../lib/projects.js";
@@ -36,14 +37,18 @@ import { USAGE_TEXT } from "./usage-text.js";
36
37
  import { PreviewUi } from "../preview/command.js";
37
38
  import { httpRelayClient } from "../lib/diagnostics.js";
38
39
  import { readLivePreview, relayInfoDir } from "../lib/relayInfo.js";
39
- import { attachedToRunning, mintRateLimited, notSignedIn, previewConflict, startBlock, } from "../preview/copy.js";
40
+ import { attachedToRunning, mintRateLimited, noDiagnosticsStatic, notSignedIn, previewConflict, settingsBanner, startBlock, } from "../preview/copy.js";
41
+ import { isPortListening } from "../preview/net.js";
42
+ import { readSettings, resolveSettings, SettingsError, TEMPLATE_DEFAULTS, validateField, } from "../lib/settings.js";
43
+ import { settingsRefusal, sourceWords } from "../lib/settingsCopy.js";
44
+ import { listsHarness } from "../lib/template.js";
40
45
  import { renderTerminalQr, shouldRenderQr } from "../preview/qr.js";
41
46
  import { PreviewConflictError, PreviewLimitError, PreviewRateLimitedError, } from "../preview/api.js";
42
47
  import { previewLimitRefused, resolveCountLimit, } from "../shared/countLimit.js";
43
48
  import { fetchNext, readCursor, writeCursor } from "../lib/feedback.js";
44
49
  import * as feedbackCopy from "../lib/feedbackCopy.js";
45
50
  import { record as recordInvocation } from "../dx/journal.js";
46
- import { dxDisableCommand, dxEnableCommand, dxSendCommand, parseDxOptions, runDx, } from "../dx/command.js";
51
+ import { dxDisableCommand, dxEnableCommand, dxSendCommand, parseDxOptions, runDx, sinceUsageLines, } from "../dx/command.js";
47
52
  import { httpConsentBackend, runConsentInBrowser } from "../dx/consent.js";
48
53
  import { DX_UNATTENDED_SUNSET, loadEnablement } from "../dx/enablement.js";
49
54
  import { journalEnabled, journalPath, readJournal } from "../dx/journal.js";
@@ -53,14 +58,13 @@ import { FETCH_TIMEOUT_MS, isFresh, updateNotice } from "../dx/updateCheck.js";
53
58
  import { fetchLatest, loadCache, saveCache } from "../dx/updateCheckIo.js";
54
59
  import { verbOf } from "../dx/skeleton.js";
55
60
  import { outdatedCommand } from "../platform/command.js";
56
- import { behindFooterLines, needsRefresh, refreshReleaseCache } from "../platform/footer.js";
61
+ import { behindFooterLines, needsRefresh, refreshReleaseCache, } from "../platform/footer.js";
57
62
  import { loadReleaseCache } from "../platform/releaseCache.js";
58
63
  import { resolveBuiltWith } from "../platform/resolve.js";
59
64
  import { createDirCollision, createSourceFlagRefusal, fromFlagConflict, isHelpInvocation, isVersionInvocation, positionalArgs, refusedFlag, } from "./args.js";
60
65
  import { parseSourceRef } from "../lib/sourceRef.js";
61
66
  import { printCreateSourceFlagRefusal, printFlagConflict, printSourceRefFailure, } from "../lib/sourceRefCopy.js";
62
- import { registerAfterScaffold } from "../lib/initFrom.js";
63
- import { registerProject } from "../lib/projects.js";
67
+ import { registerClonedProject } from "../lib/projects.js";
64
68
  import { sanitizeAppName } from "../lib/initCommand.js";
65
69
  import { resolveOrgForCommand } from "../lib/lifecycle.js";
66
70
  // The console origin serves /cli-auth (the browser target for `vincentt login`). VINCENTT_
@@ -68,7 +72,6 @@ import { resolveOrgForCommand } from "../lib/lifecycle.js";
68
72
  // This is the prod console; vincentt.app is the published-PROJECT apex, not the console, so a
69
73
  // first-run `vincentt login` (no machine config yet) must default here or it opens a dead page.
70
74
  const DEFAULT_CONSOLE_URL = "https://console.vincentt.studio";
71
- const DEFAULT_API_URL = "https://api.vincentt.studio";
72
75
  // ---- arg helpers ------------------------------------------------------------
73
76
  const argv = process.argv.slice(2);
74
77
  function flag(name) {
@@ -108,8 +111,12 @@ async function resolveAuthUrls() {
108
111
  process.env.VINCENTT_EDITOR_URL ??
109
112
  machine.consoleUrl ??
110
113
  DEFAULT_CONSOLE_URL;
111
- const apiUrl = flag("--api") ?? process.env.VINCENTT_API_URL ?? machine.apiUrl ?? DEFAULT_API_URL;
112
- return { consoleUrl, apiUrl };
114
+ return { consoleUrl, apiUrl: machineApiUrl(machine, flag("--api")) };
115
+ }
116
+ /** The machine's own host and token, never the folder's binding. Undefined when signed out. */
117
+ function machineCredentials(machine) {
118
+ const pat = process.env.VINCENTT_PAT ?? machine.pat;
119
+ return pat ? { apiUrl: machineApiUrl(machine), pat } : undefined;
113
120
  }
114
121
  /** The PKCE loopback + code-exchange login (f1). The raw PAT never rides a redirect query. */
115
122
  async function runLoginFlow() {
@@ -129,13 +136,8 @@ async function login() {
129
136
  whoAmI: async () => {
130
137
  const pat = await currentToken();
131
138
  if (!pat)
132
- return undefined;
133
- try {
134
- return await httpAuthBackend(apiUrl).me(pat);
135
- }
136
- catch {
137
- return undefined;
138
- }
139
+ throw new NotSignedInError();
140
+ return httpAuthBackend(apiUrl).me(pat);
139
141
  },
140
142
  runLogin: runLoginFlow,
141
143
  clearToken: async () => {
@@ -150,13 +152,20 @@ async function login() {
150
152
  async function currentToken() {
151
153
  return process.env.VINCENTT_PAT ?? (await loadMachineConfig()).pat;
152
154
  }
153
- /** Guard an auth-requiring verb: log in inline on first run instead of erroring. */
155
+ /**
156
+ * Guard an auth-requiring verb: log in inline at a terminal, refuse at once
157
+ * without one. False means the caller must stop; the exit code is already set.
158
+ */
154
159
  async function requireLogin() {
155
- await ensureLoggedIn({
160
+ const ok = await ensureLoggedIn({
156
161
  currentToken,
157
162
  runLogin: login,
158
163
  notify: (m) => console.error(m),
164
+ interactive: Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY),
159
165
  });
166
+ if (!ok)
167
+ process.exitCode = EXIT.NOT_AUTHENTICATED;
168
+ return ok;
160
169
  }
161
170
  async function logout() {
162
171
  const { path: p, hadToken } = await clearMachineToken();
@@ -171,14 +180,7 @@ async function whoami() {
171
180
  const machine = await loadMachineConfig();
172
181
  process.exitCode = await whoamiCommand({
173
182
  print: (l) => console.log(l),
174
- config: async () => {
175
- const apiUrl = process.env.VINCENTT_API_URL ?? machine.apiUrl ?? DEFAULT_API_URL;
176
- const pat = process.env.VINCENTT_PAT ?? machine.pat;
177
- if (!pat)
178
- return undefined;
179
- const cfg = { apiUrl: apiUrl.replace(/\/$/, ""), pat };
180
- return cfg;
181
- },
183
+ config: async () => machineCredentials(machine),
182
184
  getMe,
183
185
  org: {
184
186
  flag: flag("--org"),
@@ -220,35 +222,17 @@ async function init() {
220
222
  ask: interactive ? stdinAsk() : undefined,
221
223
  confirm: interactive ? stdinConfirm() : undefined,
222
224
  register: from
223
- ? (rd, o) => registerAfterScaffold({
224
- ...rd,
225
+ ? (rd, o) => registerClonedProject({
226
+ config: (dir) => resolveConfig(dir, { warn: print }),
227
+ org: async (dir) => resolveOrgForCommand(await resolveConfig(dir, { warn: print }), {
228
+ flag: flag("--org"),
229
+ }),
225
230
  currentToken,
226
231
  runLogin: async () => {
227
232
  await login();
228
233
  },
229
- resolveOrg: async () => {
230
- const cfg = await resolveConfig(rd.targetDir, { warn: print });
231
- const resolved = await resolveOrgForCommand(cfg, {
232
- flag: flag("--org"),
233
- });
234
- if (!resolved.ok)
235
- return resolved;
236
- return {
237
- ok: true,
238
- name: resolved.org.name,
239
- source: orgSource(),
240
- };
241
- },
242
- // registerProject, NOT createCommand: createCommand is now the
243
- // whole two-half verb, so calling it here would classify and
244
- // scaffold a second time inside the half that already cloned.
245
- create: async ({ name, slug }) => registerProject({
246
- print,
247
- cwd: rd.targetDir,
248
- config: () => resolveConfig(rd.targetDir, { warn: print }),
249
- org: async () => resolveOrgForCommand(await resolveConfig(rd.targetDir, { warn: print }), { flag: flag("--org") }),
250
- }, { name, slug }),
251
- }, o)
234
+ orgSource,
235
+ }, rd, o)
252
236
  : undefined,
253
237
  }, {
254
238
  dir,
@@ -344,6 +328,7 @@ async function create() {
344
328
  await login();
345
329
  },
346
330
  orgSource,
331
+ consoleUrl: (await resolveAuthUrls()).consoleUrl,
347
332
  onLine: (l) => console.log(l),
348
333
  });
349
334
  if (result.exitCode !== 0) {
@@ -357,16 +342,30 @@ async function create() {
357
342
  }
358
343
  }
359
344
  async function link() {
360
- await requireLogin();
345
+ if (!(await requireLogin()))
346
+ return;
361
347
  const cwd = projectDir();
348
+ const json = has("--json");
349
+ const interactive = Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
350
+ const value = (name) => flag(name) ?? valueOfEqualsFlag(argv, name);
362
351
  process.exitCode = await linkCommand({
363
352
  print: (l) => console.log(l),
364
353
  cwd,
365
354
  config: () => resolveConfig(cwd, {
366
355
  allowCustomApi: has("--allow-custom-api"),
367
- warn: (l) => console.log(l),
356
+ // Under --json stdout carries the one JSON object only.
357
+ warn: (l) => (json ? console.error(l) : console.log(l)),
368
358
  }),
369
- }, positional(1));
359
+ }, positional(1), {
360
+ json,
361
+ ask: interactive && !json ? stdinAsk() : undefined,
362
+ flags: {
363
+ devCommand: value("--dev-command"),
364
+ devPort: value("--dev-port"),
365
+ buildCommand: value("--build-command"),
366
+ buildOutput: value("--build-output"),
367
+ },
368
+ });
370
369
  }
371
370
  async function unlink() {
372
371
  const cwd = projectDir();
@@ -386,13 +385,10 @@ async function unlink() {
386
385
  * BECAUSE they are in an unbound folder depend on that folder having a binding.
387
386
  */
388
387
  async function projects() {
389
- await requireLogin();
388
+ if (!(await requireLogin()))
389
+ return;
390
390
  const machine = await loadMachineConfig();
391
- const config = async () => {
392
- const apiUrl = process.env.VINCENTT_API_URL ?? machine.apiUrl ?? DEFAULT_API_URL;
393
- const pat = process.env.VINCENTT_PAT ?? machine.pat;
394
- return pat ? { apiUrl: apiUrl.replace(/\/$/, ""), pat } : undefined;
395
- };
391
+ const config = async () => machineCredentials(machine);
396
392
  process.exitCode = await listProjectsCommand({
397
393
  print: (l) => console.log(l),
398
394
  config,
@@ -420,7 +416,7 @@ async function outdated() {
420
416
  print: (l) => console.log(l),
421
417
  cwd,
422
418
  now: () => Date.now(),
423
- apiUrl: async () => process.env.VINCENTT_API_URL ?? (await loadMachineConfig()).apiUrl ?? DEFAULT_API_URL,
419
+ apiUrl: async () => machineApiUrl(await loadMachineConfig()),
424
420
  inProject: async () => (await locateProjectBinding(cwd)) !== undefined,
425
421
  projectLine: async () => {
426
422
  const name = await projectDisplayName(cwd);
@@ -447,29 +443,38 @@ async function publish() {
447
443
  return;
448
444
  }
449
445
  const cwd = projectDir();
446
+ // Under --json stdout carries the one JSON object only; everything else goes to stderr.
447
+ const json = has("--json");
448
+ const say = (l) => (json ? console.error(l) : console.log(l));
450
449
  process.exitCode = await publishCommand({
451
- print: (l) => console.log(l),
452
- write: (s) => process.stdout.write(s),
450
+ print: say,
451
+ write: (s) => (json ? process.stderr : process.stdout).write(s),
452
+ emit: (l) => console.log(l),
453
453
  confirm: stdinConfirm(),
454
454
  cwd,
455
455
  config: () => resolveConfig(cwd, {
456
456
  allowCustomApi: has("--allow-custom-api"),
457
- warn: (l) => console.log(l),
457
+ warn: say,
458
458
  }),
459
459
  binding: async () => {
460
460
  const located = await locateProjectBinding(cwd);
461
461
  return located
462
- ? { projectId: located.binding.projectId, slug: located.binding.slug }
462
+ ? {
463
+ projectId: located.binding.projectId,
464
+ slug: located.binding.slug,
465
+ dir: located.dir,
466
+ }
463
467
  : undefined;
464
468
  },
465
469
  token: currentToken,
466
- isTTY: Boolean(process.stdout.isTTY) && Boolean(process.stdin.isTTY),
470
+ // A prompt would land in the JSON document, so --json needs --yes.
471
+ isTTY: !json && Boolean(process.stdout.isTTY) && Boolean(process.stdin.isTTY),
467
472
  now: () => Date.now(),
468
473
  sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
469
474
  orgName: activeOrgName,
470
475
  orgSlug: async () => (await activeOrgName()) || undefined,
471
476
  consoleUrl: flag("--console") ?? process.env.VINCENTT_CONSOLE_URL,
472
- }, { distDir: flag("--dist"), note: flag("--note"), yes: has("--yes") });
477
+ }, { distDir: flag("--dist"), note: flag("--note"), yes: has("--yes"), json });
473
478
  }
474
479
  /** Flow L30 — the six verbs that are console actions, answered rather than rejected. */
475
480
  async function consoleOnly(verb) {
@@ -492,16 +497,61 @@ async function consoleOnly(verb) {
492
497
  async function preview() {
493
498
  const cwd = projectDir();
494
499
  if (has("--stop")) {
495
- const { message } = await previewStopSignal(cwd);
500
+ const { stopped, message } = await previewStopSignal(cwd);
496
501
  console.log(message);
502
+ // Non-zero when nothing was stopped, so a script can tell "stopped" from "there was nothing".
503
+ process.exitCode = stopped ? EXIT.OK : EXIT.FAILED;
497
504
  return;
498
505
  }
506
+ // Settings resolve first and locally: a refusal touches nothing, and the
507
+ // banner is the disclosure of the dev command before anything runs.
508
+ const settingsRoot = (await locateProjectBinding(cwd))?.dir ?? cwd;
509
+ const portFlag = flag("--port") ?? valueOfEqualsFlag(argv, "--port");
510
+ const devCommandFlag = flag("--dev-command") ?? valueOfEqualsFlag(argv, "--dev-command");
511
+ const flagRefusal = previewFlagRefusal(portFlag, devCommandFlag);
512
+ if (flagRefusal) {
513
+ for (const l of flagRefusal)
514
+ console.log(l);
515
+ process.exitCode = 1;
516
+ return;
517
+ }
518
+ let settings;
519
+ try {
520
+ settings = resolveSettings({
521
+ devCommand: devCommandFlag,
522
+ devPort: portFlag !== undefined ? Number(portFlag) : undefined,
523
+ }, await readSettings(settingsRoot), TEMPLATE_DEFAULTS, ["dev"]);
524
+ }
525
+ catch (err) {
526
+ if (!(err instanceof SettingsError))
527
+ throw err;
528
+ for (const l of settingsRefusal(err, "preview"))
529
+ console.log(l);
530
+ process.exitCode = 1;
531
+ return;
532
+ }
533
+ const devCommand = settings.devCommand.value;
534
+ const devPort = settings.devPort.value;
535
+ const localUrl = `http://localhost:${devPort}`;
536
+ for (const l of settingsBanner({
537
+ devCommand,
538
+ devCommandSource: sourceWords(settings.devCommand),
539
+ devPort,
540
+ devPortSource: sourceWords(settings.devPort),
541
+ reused: await isPortListening(devPort),
542
+ })) {
543
+ console.log(l);
544
+ }
545
+ const harnessListed = await listsHarness(settingsRoot);
546
+ if (!harnessListed)
547
+ for (const l of noDiagnosticsStatic())
548
+ console.log(l);
499
549
  // The gate is LOCAL and NOTHING IS ATTEMPTED: no spinner, no partial mint, no
500
550
  // session left in `pending`. The command stops before it touches the network.
501
551
  if (!(await currentToken())) {
502
- for (const l of notSignedIn())
552
+ for (const l of notSignedIn(devCommand, localUrl))
503
553
  console.log(l);
504
- process.exitCode = 1;
554
+ process.exitCode = EXIT.NOT_AUTHENTICATED;
505
555
  return;
506
556
  }
507
557
  const filterViewer = flag("--viewer");
@@ -510,10 +560,13 @@ async function preview() {
510
560
  const ui = new PreviewUi({
511
561
  print: (l) => console.log(l),
512
562
  now: () => Date.now(),
513
- targetUrl: `http://localhost:${numFlag("--port") ?? 5173}`,
563
+ targetUrl: localUrl,
514
564
  color,
515
565
  filterTester,
516
566
  filterViewer,
567
+ devCommand,
568
+ portSource: settings.devPort.source,
569
+ harnessListed,
517
570
  });
518
571
  // A preview already running for this folder — in ANOTHER process — is
519
572
  // FOLLOWED, not superseded. `vincentt preview` in a folder with a live
@@ -553,7 +606,9 @@ async function preview() {
553
606
  let running;
554
607
  try {
555
608
  const started = await previewStart(cwd, (m) => console.error(`[preview] ${m}`), {
556
- appPort: numFlag("--port"),
609
+ appPort: devPort,
610
+ devCommand,
611
+ onHiddenPath: (p) => ui.hiddenPath(p),
557
612
  appStdio: has("--verbose") ? "inherit" : "ignore",
558
613
  onViewer: (label, uaClass) => ui.viewerSeen(label, uaClass),
559
614
  onViewerGone: (label) => ui.viewerGone(label),
@@ -591,7 +646,7 @@ async function preview() {
591
646
  const localOrg = await localOrgIdentity();
592
647
  for (const l of previewFailureLines(err, localOrg))
593
648
  console.log(l);
594
- process.exitCode = 1;
649
+ process.exitCode = err instanceof AuthExpiredError ? EXIT.NOT_AUTHENTICATED : 1;
595
650
  return;
596
651
  }
597
652
  const qr = shouldRenderQr({
@@ -632,9 +687,24 @@ async function preview() {
632
687
  if (ending !== null) {
633
688
  ui.endedBy(ending, (await activeOrgName()) || "your workspace");
634
689
  await previewStop();
635
- process.exitCode = 1;
690
+ process.exitCode = ending === "auth_expired" ? EXIT.NOT_AUTHENTICATED : 1;
636
691
  }
637
692
  }
693
+ /** A `--port` or `--dev-command` value the schema refuses, before anything runs. */
694
+ function previewFlagRefusal(port, devCommand) {
695
+ const portMessage = port === undefined
696
+ ? undefined
697
+ : validateField("dev.port", /^\d+$/.test(port) ? Number(port) : Number.NaN);
698
+ const commandMessage = devCommand === undefined ? undefined : validateField("dev.command", devCommand);
699
+ const bad = portMessage
700
+ ? `--port ${portMessage}`
701
+ : commandMessage
702
+ ? `--dev-command ${commandMessage}`
703
+ : undefined;
704
+ if (!bad)
705
+ return undefined;
706
+ return ["", `✗ ${bad}.`, "", " Nothing was started.", ""];
707
+ }
638
708
  /**
639
709
  * Flow L16 — follow a preview running in another terminal.
640
710
  *
@@ -657,7 +727,7 @@ async function followPreview(info, ui, filterTester, filterViewer) {
657
727
  console.log("✓ Following this preview");
658
728
  }
659
729
  console.log("");
660
- const relay = httpRelayClient(`http://localhost:${info.relayPort}`);
730
+ const relay = httpRelayClient(`http://127.0.0.1:${info.relayPort}`);
661
731
  let since = -1;
662
732
  let warnedMissing = false;
663
733
  let stop = false;
@@ -762,6 +832,8 @@ async function projectConsoleUrl(cwd, sessionId) {
762
832
  * or an outage — a creator cannot act on that.
763
833
  */
764
834
  function previewFailureLines(err, local) {
835
+ if (err instanceof AuthExpiredError)
836
+ return authExpiredLines();
765
837
  if (err instanceof PreviewRateLimitedError)
766
838
  return mintRateLimited(err.retryAfterSec);
767
839
  // The ceiling is the second 409 on the mint route, and it is a DIFFERENT
@@ -1057,6 +1129,12 @@ async function dx() {
1057
1129
  : await dxDisableCommand(deps);
1058
1130
  return;
1059
1131
  }
1132
+ if (options.sinceInvalid !== undefined) {
1133
+ for (const l of sinceUsageLines(options.sinceInvalid))
1134
+ console.error(l);
1135
+ process.exitCode = EXIT.FAILED;
1136
+ return;
1137
+ }
1060
1138
  // Read strictly and independently of auth: a truncated config must disable the
1061
1139
  // relaxation, not throw through it.
1062
1140
  const enablement = await loadEnablement(now);
@@ -1222,7 +1300,7 @@ async function assets() {
1222
1300
  * the act does not exist: certificates renew on their own.
1223
1301
  */
1224
1302
  async function domain() {
1225
- const { domainCommand, resolveSub, } = await import("../domain/command.js");
1303
+ const { domainCommand, resolveSub } = await import("../domain/command.js");
1226
1304
  const domainCopy = await import("../domain/copy.js");
1227
1305
  const api = await import("../domain/api.js");
1228
1306
  const sub = positional(1);
@@ -1246,11 +1324,7 @@ async function domain() {
1246
1324
  }
1247
1325
  const cwd = projectDir();
1248
1326
  const machine = await loadMachineConfig();
1249
- const config = async () => {
1250
- const apiUrl = process.env.VINCENTT_API_URL ?? machine.apiUrl ?? DEFAULT_API_URL;
1251
- const pat = process.env.VINCENTT_PAT ?? machine.pat;
1252
- return pat ? { apiUrl: apiUrl.replace(/\/$/, ""), pat } : undefined;
1253
- };
1327
+ const config = async () => machineCredentials(machine);
1254
1328
  process.exitCode = await domainCommand({
1255
1329
  print: (l) => console.log(l),
1256
1330
  config,
@@ -1468,9 +1542,7 @@ async function announceBehind() {
1468
1542
  }
1469
1543
  if (needsRefresh(loadReleaseCache(), Date.now())) {
1470
1544
  refreshReleaseCache({
1471
- apiUrl: process.env.VINCENTT_API_URL ??
1472
- (await loadMachineConfig()).apiUrl ??
1473
- DEFAULT_API_URL,
1545
+ apiUrl: machineApiUrl(await loadMachineConfig()),
1474
1546
  now: () => Date.now(),
1475
1547
  });
1476
1548
  }
@@ -1506,6 +1578,20 @@ main()
1506
1578
  journal(Number(process.exitCode ?? 0));
1507
1579
  })
1508
1580
  .catch((err) => {
1581
+ // A signed-out or refused token that no verb handled still exits with the
1582
+ // not-signed-in code and the login remedy, so an agent has one code to branch on.
1583
+ if (err instanceof NotSignedInError) {
1584
+ for (const l of NOT_SIGNED_IN_LINES)
1585
+ console.error(l);
1586
+ journal(EXIT.NOT_AUTHENTICATED, err);
1587
+ process.exit(EXIT.NOT_AUTHENTICATED);
1588
+ }
1589
+ if (isAuthRefused(err)) {
1590
+ for (const l of authExpiredLines())
1591
+ console.error(l);
1592
+ journal(EXIT.NOT_AUTHENTICATED, err);
1593
+ process.exit(EXIT.NOT_AUTHENTICATED);
1594
+ }
1509
1595
  console.error(`\n✗ ${err instanceof Error ? err.message : String(err)}`);
1510
1596
  journal(1, err);
1511
1597
  process.exit(1);