@vincentt-xr/cli 0.16.0 → 0.18.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 (95) hide show
  1. package/MIGRATION.md +24 -0
  2. package/README.md +94 -1
  3. package/dist/cli/index.js +196 -87
  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 +24 -11
  57. package/dist/preview/api.js +42 -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 +39 -6
  69. package/dist/publish/api.js +80 -9
  70. package/dist/publish/command.d.ts +25 -0
  71. package/dist/publish/command.js +230 -32
  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 +6 -0
  81. package/dist/publish/upload.js +11 -1
  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 +114 -0
  92. package/dist/shared/countLimit.js +176 -0
  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,24 @@
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.0
7
+
8
+ required: false
9
+ No action required for a project made from the Vincentt template. Behaviour that changed:
10
+ - 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`.
11
+ - 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.
12
+ - 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.
13
+ - `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.
14
+ - 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.
15
+ - 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.
16
+ - 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.
17
+ - `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.
18
+ - `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.
19
+ - `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.
20
+
21
+ ## 0.17.0
22
+
23
+ required: false
24
+ 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,13 +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
- import { PreviewConflictError, PreviewRateLimitedError } from "../preview/api.js";
46
+ import { PreviewConflictError, PreviewLimitError, PreviewRateLimitedError, } from "../preview/api.js";
47
+ import { previewLimitRefused, resolveCountLimit, } from "../shared/countLimit.js";
42
48
  import { fetchNext, readCursor, writeCursor } from "../lib/feedback.js";
43
49
  import * as feedbackCopy from "../lib/feedbackCopy.js";
44
50
  import { record as recordInvocation } from "../dx/journal.js";
45
- import { dxDisableCommand, dxEnableCommand, dxSendCommand, parseDxOptions, runDx, } from "../dx/command.js";
51
+ import { dxDisableCommand, dxEnableCommand, dxSendCommand, parseDxOptions, runDx, sinceUsageLines, } from "../dx/command.js";
46
52
  import { httpConsentBackend, runConsentInBrowser } from "../dx/consent.js";
47
53
  import { DX_UNATTENDED_SUNSET, loadEnablement } from "../dx/enablement.js";
48
54
  import { journalEnabled, journalPath, readJournal } from "../dx/journal.js";
@@ -52,14 +58,13 @@ import { FETCH_TIMEOUT_MS, isFresh, updateNotice } from "../dx/updateCheck.js";
52
58
  import { fetchLatest, loadCache, saveCache } from "../dx/updateCheckIo.js";
53
59
  import { verbOf } from "../dx/skeleton.js";
54
60
  import { outdatedCommand } from "../platform/command.js";
55
- import { behindFooterLines, needsRefresh, refreshReleaseCache } from "../platform/footer.js";
61
+ import { behindFooterLines, needsRefresh, refreshReleaseCache, } from "../platform/footer.js";
56
62
  import { loadReleaseCache } from "../platform/releaseCache.js";
57
63
  import { resolveBuiltWith } from "../platform/resolve.js";
58
64
  import { createDirCollision, createSourceFlagRefusal, fromFlagConflict, isHelpInvocation, isVersionInvocation, positionalArgs, refusedFlag, } from "./args.js";
59
65
  import { parseSourceRef } from "../lib/sourceRef.js";
60
66
  import { printCreateSourceFlagRefusal, printFlagConflict, printSourceRefFailure, } from "../lib/sourceRefCopy.js";
61
- import { registerAfterScaffold } from "../lib/initFrom.js";
62
- import { registerProject } from "../lib/projects.js";
67
+ import { registerClonedProject } from "../lib/projects.js";
63
68
  import { sanitizeAppName } from "../lib/initCommand.js";
64
69
  import { resolveOrgForCommand } from "../lib/lifecycle.js";
65
70
  // The console origin serves /cli-auth (the browser target for `vincentt login`). VINCENTT_
@@ -67,7 +72,6 @@ import { resolveOrgForCommand } from "../lib/lifecycle.js";
67
72
  // This is the prod console; vincentt.app is the published-PROJECT apex, not the console, so a
68
73
  // first-run `vincentt login` (no machine config yet) must default here or it opens a dead page.
69
74
  const DEFAULT_CONSOLE_URL = "https://console.vincentt.studio";
70
- const DEFAULT_API_URL = "https://api.vincentt.studio";
71
75
  // ---- arg helpers ------------------------------------------------------------
72
76
  const argv = process.argv.slice(2);
73
77
  function flag(name) {
@@ -107,8 +111,12 @@ async function resolveAuthUrls() {
107
111
  process.env.VINCENTT_EDITOR_URL ??
108
112
  machine.consoleUrl ??
109
113
  DEFAULT_CONSOLE_URL;
110
- const apiUrl = flag("--api") ?? process.env.VINCENTT_API_URL ?? machine.apiUrl ?? DEFAULT_API_URL;
111
- 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;
112
120
  }
113
121
  /** The PKCE loopback + code-exchange login (f1). The raw PAT never rides a redirect query. */
114
122
  async function runLoginFlow() {
@@ -128,13 +136,8 @@ async function login() {
128
136
  whoAmI: async () => {
129
137
  const pat = await currentToken();
130
138
  if (!pat)
131
- return undefined;
132
- try {
133
- return await httpAuthBackend(apiUrl).me(pat);
134
- }
135
- catch {
136
- return undefined;
137
- }
139
+ throw new NotSignedInError();
140
+ return httpAuthBackend(apiUrl).me(pat);
138
141
  },
139
142
  runLogin: runLoginFlow,
140
143
  clearToken: async () => {
@@ -149,13 +152,20 @@ async function login() {
149
152
  async function currentToken() {
150
153
  return process.env.VINCENTT_PAT ?? (await loadMachineConfig()).pat;
151
154
  }
152
- /** 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
+ */
153
159
  async function requireLogin() {
154
- await ensureLoggedIn({
160
+ const ok = await ensureLoggedIn({
155
161
  currentToken,
156
162
  runLogin: login,
157
163
  notify: (m) => console.error(m),
164
+ interactive: Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY),
158
165
  });
166
+ if (!ok)
167
+ process.exitCode = EXIT.NOT_AUTHENTICATED;
168
+ return ok;
159
169
  }
160
170
  async function logout() {
161
171
  const { path: p, hadToken } = await clearMachineToken();
@@ -170,14 +180,7 @@ async function whoami() {
170
180
  const machine = await loadMachineConfig();
171
181
  process.exitCode = await whoamiCommand({
172
182
  print: (l) => console.log(l),
173
- config: async () => {
174
- const apiUrl = process.env.VINCENTT_API_URL ?? machine.apiUrl ?? DEFAULT_API_URL;
175
- const pat = process.env.VINCENTT_PAT ?? machine.pat;
176
- if (!pat)
177
- return undefined;
178
- const cfg = { apiUrl: apiUrl.replace(/\/$/, ""), pat };
179
- return cfg;
180
- },
183
+ config: async () => machineCredentials(machine),
181
184
  getMe,
182
185
  org: {
183
186
  flag: flag("--org"),
@@ -219,35 +222,17 @@ async function init() {
219
222
  ask: interactive ? stdinAsk() : undefined,
220
223
  confirm: interactive ? stdinConfirm() : undefined,
221
224
  register: from
222
- ? (rd, o) => registerAfterScaffold({
223
- ...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
+ }),
224
230
  currentToken,
225
231
  runLogin: async () => {
226
232
  await login();
227
233
  },
228
- resolveOrg: async () => {
229
- const cfg = await resolveConfig(rd.targetDir, { warn: print });
230
- const resolved = await resolveOrgForCommand(cfg, {
231
- flag: flag("--org"),
232
- });
233
- if (!resolved.ok)
234
- return resolved;
235
- return {
236
- ok: true,
237
- name: resolved.org.name,
238
- source: orgSource(),
239
- };
240
- },
241
- // registerProject, NOT createCommand: createCommand is now the
242
- // whole two-half verb, so calling it here would classify and
243
- // scaffold a second time inside the half that already cloned.
244
- create: async ({ name, slug }) => registerProject({
245
- print,
246
- cwd: rd.targetDir,
247
- config: () => resolveConfig(rd.targetDir, { warn: print }),
248
- org: async () => resolveOrgForCommand(await resolveConfig(rd.targetDir, { warn: print }), { flag: flag("--org") }),
249
- }, { name, slug }),
250
- }, o)
234
+ orgSource,
235
+ }, rd, o)
251
236
  : undefined,
252
237
  }, {
253
238
  dir,
@@ -343,6 +328,7 @@ async function create() {
343
328
  await login();
344
329
  },
345
330
  orgSource,
331
+ consoleUrl: (await resolveAuthUrls()).consoleUrl,
346
332
  onLine: (l) => console.log(l),
347
333
  });
348
334
  if (result.exitCode !== 0) {
@@ -356,16 +342,30 @@ async function create() {
356
342
  }
357
343
  }
358
344
  async function link() {
359
- await requireLogin();
345
+ if (!(await requireLogin()))
346
+ return;
360
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);
361
351
  process.exitCode = await linkCommand({
362
352
  print: (l) => console.log(l),
363
353
  cwd,
364
354
  config: () => resolveConfig(cwd, {
365
355
  allowCustomApi: has("--allow-custom-api"),
366
- 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)),
367
358
  }),
368
- }, 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
+ });
369
369
  }
370
370
  async function unlink() {
371
371
  const cwd = projectDir();
@@ -385,13 +385,10 @@ async function unlink() {
385
385
  * BECAUSE they are in an unbound folder depend on that folder having a binding.
386
386
  */
387
387
  async function projects() {
388
- await requireLogin();
388
+ if (!(await requireLogin()))
389
+ return;
389
390
  const machine = await loadMachineConfig();
390
- const config = async () => {
391
- const apiUrl = process.env.VINCENTT_API_URL ?? machine.apiUrl ?? DEFAULT_API_URL;
392
- const pat = process.env.VINCENTT_PAT ?? machine.pat;
393
- return pat ? { apiUrl: apiUrl.replace(/\/$/, ""), pat } : undefined;
394
- };
391
+ const config = async () => machineCredentials(machine);
395
392
  process.exitCode = await listProjectsCommand({
396
393
  print: (l) => console.log(l),
397
394
  config,
@@ -419,7 +416,7 @@ async function outdated() {
419
416
  print: (l) => console.log(l),
420
417
  cwd,
421
418
  now: () => Date.now(),
422
- apiUrl: async () => process.env.VINCENTT_API_URL ?? (await loadMachineConfig()).apiUrl ?? DEFAULT_API_URL,
419
+ apiUrl: async () => machineApiUrl(await loadMachineConfig()),
423
420
  inProject: async () => (await locateProjectBinding(cwd)) !== undefined,
424
421
  projectLine: async () => {
425
422
  const name = await projectDisplayName(cwd);
@@ -446,29 +443,38 @@ async function publish() {
446
443
  return;
447
444
  }
448
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));
449
449
  process.exitCode = await publishCommand({
450
- print: (l) => console.log(l),
451
- 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),
452
453
  confirm: stdinConfirm(),
453
454
  cwd,
454
455
  config: () => resolveConfig(cwd, {
455
456
  allowCustomApi: has("--allow-custom-api"),
456
- warn: (l) => console.log(l),
457
+ warn: say,
457
458
  }),
458
459
  binding: async () => {
459
460
  const located = await locateProjectBinding(cwd);
460
461
  return located
461
- ? { projectId: located.binding.projectId, slug: located.binding.slug }
462
+ ? {
463
+ projectId: located.binding.projectId,
464
+ slug: located.binding.slug,
465
+ dir: located.dir,
466
+ }
462
467
  : undefined;
463
468
  },
464
469
  token: currentToken,
465
- 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),
466
472
  now: () => Date.now(),
467
473
  sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
468
474
  orgName: activeOrgName,
469
475
  orgSlug: async () => (await activeOrgName()) || undefined,
470
476
  consoleUrl: flag("--console") ?? process.env.VINCENTT_CONSOLE_URL,
471
- }, { distDir: flag("--dist"), note: flag("--note"), yes: has("--yes") });
477
+ }, { distDir: flag("--dist"), note: flag("--note"), yes: has("--yes"), json });
472
478
  }
473
479
  /** Flow L30 — the six verbs that are console actions, answered rather than rejected. */
474
480
  async function consoleOnly(verb) {
@@ -491,16 +497,61 @@ async function consoleOnly(verb) {
491
497
  async function preview() {
492
498
  const cwd = projectDir();
493
499
  if (has("--stop")) {
494
- const { message } = await previewStopSignal(cwd);
500
+ const { stopped, message } = await previewStopSignal(cwd);
495
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;
504
+ return;
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;
496
531
  return;
497
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);
498
549
  // The gate is LOCAL and NOTHING IS ATTEMPTED: no spinner, no partial mint, no
499
550
  // session left in `pending`. The command stops before it touches the network.
500
551
  if (!(await currentToken())) {
501
- for (const l of notSignedIn())
552
+ for (const l of notSignedIn(devCommand, localUrl))
502
553
  console.log(l);
503
- process.exitCode = 1;
554
+ process.exitCode = EXIT.NOT_AUTHENTICATED;
504
555
  return;
505
556
  }
506
557
  const filterViewer = flag("--viewer");
@@ -509,10 +560,13 @@ async function preview() {
509
560
  const ui = new PreviewUi({
510
561
  print: (l) => console.log(l),
511
562
  now: () => Date.now(),
512
- targetUrl: `http://localhost:${numFlag("--port") ?? 5173}`,
563
+ targetUrl: localUrl,
513
564
  color,
514
565
  filterTester,
515
566
  filterViewer,
567
+ devCommand,
568
+ portSource: settings.devPort.source,
569
+ harnessListed,
516
570
  });
517
571
  // A preview already running for this folder — in ANOTHER process — is
518
572
  // FOLLOWED, not superseded. `vincentt preview` in a folder with a live
@@ -552,7 +606,9 @@ async function preview() {
552
606
  let running;
553
607
  try {
554
608
  const started = await previewStart(cwd, (m) => console.error(`[preview] ${m}`), {
555
- appPort: numFlag("--port"),
609
+ appPort: devPort,
610
+ devCommand,
611
+ onHiddenPath: (p) => ui.hiddenPath(p),
556
612
  appStdio: has("--verbose") ? "inherit" : "ignore",
557
613
  onViewer: (label, uaClass) => ui.viewerSeen(label, uaClass),
558
614
  onViewerGone: (label) => ui.viewerGone(label),
@@ -584,9 +640,13 @@ async function preview() {
584
640
  }
585
641
  }
586
642
  catch (err) {
587
- for (const l of previewFailureLines(err))
643
+ // The local org is the FALLBACK only. A count refusal names the org that
644
+ // owns this project, and the wire's name wins over this one wherever both
645
+ // exist — see resolveCountLimit.
646
+ const localOrg = await localOrgIdentity();
647
+ for (const l of previewFailureLines(err, localOrg))
588
648
  console.log(l);
589
- process.exitCode = 1;
649
+ process.exitCode = err instanceof AuthExpiredError ? EXIT.NOT_AUTHENTICATED : 1;
590
650
  return;
591
651
  }
592
652
  const qr = shouldRenderQr({
@@ -627,9 +687,24 @@ async function preview() {
627
687
  if (ending !== null) {
628
688
  ui.endedBy(ending, (await activeOrgName()) || "your workspace");
629
689
  await previewStop();
630
- process.exitCode = 1;
690
+ process.exitCode = ending === "auth_expired" ? EXIT.NOT_AUTHENTICATED : 1;
631
691
  }
632
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
+ }
633
708
  /**
634
709
  * Flow L16 — follow a preview running in another terminal.
635
710
  *
@@ -652,7 +727,7 @@ async function followPreview(info, ui, filterTester, filterViewer) {
652
727
  console.log("✓ Following this preview");
653
728
  }
654
729
  console.log("");
655
- const relay = httpRelayClient(`http://localhost:${info.relayPort}`);
730
+ const relay = httpRelayClient(`http://127.0.0.1:${info.relayPort}`);
656
731
  let since = -1;
657
732
  let warnedMissing = false;
658
733
  let stop = false;
@@ -706,6 +781,18 @@ async function activeOrgName() {
706
781
  (await loadMachineConfig()).activeOrg ??
707
782
  "");
708
783
  }
784
+ /**
785
+ * The org this machine is pointed at, for a refusal whose body named none.
786
+ *
787
+ * `activeOrg` is a SLUG, so it fills the slug slot and not the name slot. The
788
+ * display name is the API's to give; inventing one from the slug would put a
789
+ * fabricated workspace name on a refusal.
790
+ */
791
+ async function localOrgIdentity() {
792
+ const slug = await activeOrgName();
793
+ const { consoleUrl } = await resolveAuthUrls();
794
+ return { slug: slug || undefined, consoleUrl };
795
+ }
709
796
  /** The binding's name is a HINT for display only; the API is the authority. */
710
797
  async function projectDisplayName(cwd) {
711
798
  const located = await locateProjectBinding(cwd);
@@ -744,9 +831,17 @@ async function projectConsoleUrl(cwd, sessionId) {
744
831
  * creator never has to read a second, and none of them names the edge, the API
745
832
  * or an outage — a creator cannot act on that.
746
833
  */
747
- function previewFailureLines(err) {
834
+ function previewFailureLines(err, local) {
835
+ if (err instanceof AuthExpiredError)
836
+ return authExpiredLines();
748
837
  if (err instanceof PreviewRateLimitedError)
749
838
  return mintRateLimited(err.retryAfterSec);
839
+ // The ceiling is the second 409 on the mint route, and it is a DIFFERENT
840
+ // creator situation from the conflict below: nothing is running, nothing was
841
+ // taken down, and the organization is full.
842
+ if (err instanceof PreviewLimitError) {
843
+ return previewLimitRefused(resolveCountLimit(err.detail, local));
844
+ }
750
845
  if (err instanceof PreviewConflictError)
751
846
  return previewConflict();
752
847
  if (err instanceof ProjectUnreachableError) {
@@ -1034,6 +1129,12 @@ async function dx() {
1034
1129
  : await dxDisableCommand(deps);
1035
1130
  return;
1036
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
+ }
1037
1138
  // Read strictly and independently of auth: a truncated config must disable the
1038
1139
  // relaxation, not throw through it.
1039
1140
  const enablement = await loadEnablement(now);
@@ -1199,7 +1300,7 @@ async function assets() {
1199
1300
  * the act does not exist: certificates renew on their own.
1200
1301
  */
1201
1302
  async function domain() {
1202
- const { domainCommand, resolveSub, } = await import("../domain/command.js");
1303
+ const { domainCommand, resolveSub } = await import("../domain/command.js");
1203
1304
  const domainCopy = await import("../domain/copy.js");
1204
1305
  const api = await import("../domain/api.js");
1205
1306
  const sub = positional(1);
@@ -1223,11 +1324,7 @@ async function domain() {
1223
1324
  }
1224
1325
  const cwd = projectDir();
1225
1326
  const machine = await loadMachineConfig();
1226
- const config = async () => {
1227
- const apiUrl = process.env.VINCENTT_API_URL ?? machine.apiUrl ?? DEFAULT_API_URL;
1228
- const pat = process.env.VINCENTT_PAT ?? machine.pat;
1229
- return pat ? { apiUrl: apiUrl.replace(/\/$/, ""), pat } : undefined;
1230
- };
1327
+ const config = async () => machineCredentials(machine);
1231
1328
  process.exitCode = await domainCommand({
1232
1329
  print: (l) => console.log(l),
1233
1330
  config,
@@ -1445,9 +1542,7 @@ async function announceBehind() {
1445
1542
  }
1446
1543
  if (needsRefresh(loadReleaseCache(), Date.now())) {
1447
1544
  refreshReleaseCache({
1448
- apiUrl: process.env.VINCENTT_API_URL ??
1449
- (await loadMachineConfig()).apiUrl ??
1450
- DEFAULT_API_URL,
1545
+ apiUrl: machineApiUrl(await loadMachineConfig()),
1451
1546
  now: () => Date.now(),
1452
1547
  });
1453
1548
  }
@@ -1483,6 +1578,20 @@ main()
1483
1578
  journal(Number(process.exitCode ?? 0));
1484
1579
  })
1485
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
+ }
1486
1595
  console.error(`\n✗ ${err instanceof Error ? err.message : String(err)}`);
1487
1596
  journal(1, err);
1488
1597
  process.exit(1);