@vincentt-xr/cli 0.17.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 +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 +25 -0
  71. package/dist/publish/command.js +211 -35
  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 +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
@@ -23,6 +23,7 @@ import { stateMatches } from "../login/pkce.js";
23
23
  import { getFreePort } from "../preview/net.js";
24
24
  import { authFailureMessage } from "../shared/authError.js";
25
25
  import { clientHeaders } from "../shared/clientHeader.js";
26
+ import { apiFetch } from "../shared/http.js";
26
27
  import { readAccountConsent } from "./enablement.js";
27
28
  async function safeText(res) {
28
29
  try {
@@ -50,7 +51,7 @@ export function httpConsentBackend() {
50
51
  const strip = (u) => u.replace(/\/$/, "");
51
52
  return {
52
53
  async read(apiUrl, pat, now) {
53
- const res = await fetch(`${strip(apiUrl)}/me`, {
54
+ const res = await apiFetch(`${strip(apiUrl)}/me`, {
54
55
  headers: { ...clientHeaders(), authorization: `Bearer ${pat}` },
55
56
  });
56
57
  if (!res.ok) {
@@ -59,7 +60,7 @@ export function httpConsentBackend() {
59
60
  return readAccountConsent(unwrapMe(await res.json()), now);
60
61
  },
61
62
  async grant(apiUrl, pat, now) {
62
- const res = await fetch(`${strip(apiUrl)}/me/dx-consent`, {
63
+ const res = await apiFetch(`${strip(apiUrl)}/me/dx-consent`, {
63
64
  method: "POST",
64
65
  headers: { ...clientHeaders(), authorization: `Bearer ${pat}` },
65
66
  });
@@ -69,7 +70,7 @@ export function httpConsentBackend() {
69
70
  return readAccountConsent(unwrapMe(await res.json()), now);
70
71
  },
71
72
  async revoke(apiUrl, pat) {
72
- const res = await fetch(`${strip(apiUrl)}/me/dx-consent`, {
73
+ const res = await apiFetch(`${strip(apiUrl)}/me/dx-consent`, {
73
74
  method: "DELETE",
74
75
  headers: { ...clientHeaders(), authorization: `Bearer ${pat}` },
75
76
  });
@@ -114,7 +115,7 @@ export function parseConsentCallback(params, expectedState, stateMatches) {
114
115
  return {
115
116
  ok: false,
116
117
  reason: "state_mismatch",
117
- message: "State mismatch — ignoring an unexpected callback.",
118
+ message: "State mismatch. Ignoring an unexpected callback.",
118
119
  };
119
120
  }
120
121
  const result = params.get("result");
@@ -165,7 +166,7 @@ export async function runConsentInBrowser(opts) {
165
166
  value: result.decision,
166
167
  page: {
167
168
  status: 200,
168
- html: browserPage(granted, granted ? "Reports allowed" : "Not enabled", "Return to your terminal — you can close this tab."),
169
+ html: browserPage(granted, granted ? "Reports allowed" : "Not enabled", "Return to your terminal. You can close this tab."),
169
170
  },
170
171
  };
171
172
  }
@@ -47,5 +47,20 @@ export declare function buildEntry(input: RecordInput): JournalEntry;
47
47
  * turned into a failed command.
48
48
  */
49
49
  export declare function record(input: RecordInput): void;
50
- /** Read the journal oldest-first. Malformed lines are skipped, not fatal. */
51
- export declare function readJournal(): JournalEntry[];
50
+ export interface JournalWindow {
51
+ /** Epoch ms. Entries stamped before this are dropped. */
52
+ sinceMs: number;
53
+ }
54
+ /**
55
+ * Keep only entries stamped at or after the cutoff. An entry whose timestamp
56
+ * does not parse is dropped: it cannot be placed inside any window.
57
+ */
58
+ export declare function withinWindow(entries: JournalEntry[], window: JournalWindow): JournalEntry[];
59
+ /**
60
+ * Read the journal oldest-first. Malformed lines are skipped, not fatal.
61
+ *
62
+ * With a window, only entries inside it are returned. Without one, the whole
63
+ * file is: the size trim is the only other bound, so how far back an unwindowed
64
+ * read reaches depends on how much the CLI was used, not on when.
65
+ */
66
+ export declare function readJournal(window?: JournalWindow): JournalEntry[];
@@ -105,8 +105,24 @@ export function record(input) {
105
105
  // Instrumentation must not break the tool it measures.
106
106
  }
107
107
  }
108
- /** Read the journal oldest-first. Malformed lines are skipped, not fatal. */
109
- export function readJournal() {
108
+ /**
109
+ * Keep only entries stamped at or after the cutoff. An entry whose timestamp
110
+ * does not parse is dropped: it cannot be placed inside any window.
111
+ */
112
+ export function withinWindow(entries, window) {
113
+ return entries.filter((e) => {
114
+ const t = Date.parse(e.ts);
115
+ return Number.isFinite(t) && t >= window.sinceMs;
116
+ });
117
+ }
118
+ /**
119
+ * Read the journal oldest-first. Malformed lines are skipped, not fatal.
120
+ *
121
+ * With a window, only entries inside it are returned. Without one, the whole
122
+ * file is: the size trim is the only other bound, so how far back an unwindowed
123
+ * read reaches depends on how much the CLI was used, not on when.
124
+ */
125
+ export function readJournal(window) {
110
126
  let raw;
111
127
  try {
112
128
  raw = fs.readFileSync(journalPath(), "utf8");
@@ -125,5 +141,5 @@ export function readJournal() {
125
141
  // A torn line from a killed process is not a reason to lose the rest.
126
142
  }
127
143
  }
128
- return out;
144
+ return window === undefined ? out : withinWindow(out, window);
129
145
  }
package/dist/dx/report.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { enablementLine } from "./enablement.js";
2
2
  import { DXSendError, platformTag } from "./send.js";
3
3
  import { resolveBuiltWith } from "../platform/resolve.js";
4
+ import { EXIT } from "../lib/exitCodes.js";
4
5
  import { renderPayloadWithVersionSummary, summarizeBuiltWith, versionDisclosureLines, } from "../platform/versionSummary.js";
5
6
  /** Matches the server's MaxDXNote. Checked here so a refusal costs no round trip. */
6
7
  export const MAX_NOTE = 2000;
@@ -93,7 +94,7 @@ export function renderObservation(report) {
93
94
  "",
94
95
  ...renderPayloadWithVersionSummary(report, report.builtWith).map((l) => ` ${l}`),
95
96
  "",
96
- " YOUR NOTE IS SENT EXACTLY AS WRITTEN — check it for paths, URLs,",
97
+ " YOUR NOTE IS SENT EXACTLY AS WRITTEN. Check it for paths, URLs,",
97
98
  " tokens, or customer names before sending.",
98
99
  // Entry 2: it explains the one anomalous line above, so it sits directly
99
100
  // under the warning where the reader's eye lands next — above the context
@@ -153,7 +154,7 @@ export async function reportCommand(deps, options) {
153
154
  }
154
155
  if (auth === undefined) {
155
156
  deps.print("Not signed in. Run `vincentt login` to send a report.");
156
- return 1;
157
+ return EXIT.NOT_AUTHENTICATED;
157
158
  }
158
159
  const context = options.noContext ? undefined : lastContext(deps.entries());
159
160
  // RESOLVED AFTER THE GATE AND IMMEDIATELY BEFORE THE BUILD.
@@ -199,7 +200,7 @@ export async function reportCommand(deps, options) {
199
200
  try {
200
201
  const res = await deps.post(auth.apiUrl, auth.pat, report);
201
202
  deps.print(`Sent. Reference ${res.ref}.`);
202
- deps.print("Thank you — this is the kind of thing we cannot detect on our own.");
203
+ deps.print("Thank you. This is the kind of thing we cannot detect on our own.");
203
204
  return 0;
204
205
  }
205
206
  catch (err) {
@@ -1,5 +1,7 @@
1
1
  import { DXSendError } from "./send.js";
2
- export async function postObservationReport(apiUrl, pat, report, fetchImpl = fetch) {
2
+ import { AUTH_EXPIRED_MESSAGE } from "../shared/authError.js";
3
+ import { apiFetch } from "../shared/http.js";
4
+ export async function postObservationReport(apiUrl, pat, report, fetchImpl = apiFetch) {
3
5
  const res = await fetchImpl(`${apiUrl}/dx-reports`, {
4
6
  method: "POST",
5
7
  headers: { "content-type": "application/json", authorization: `Bearer ${pat}` },
@@ -14,14 +16,13 @@ export async function postObservationReport(apiUrl, pat, report, fetchImpl = fet
14
16
  catch {
15
17
  // A non-JSON body is not more informative than the status.
16
18
  }
17
- if (res.status === 401) {
18
- throw new DXSendError("Your access token is no longer valid — run `vincentt login`.", 401);
19
- }
19
+ if (res.status === 401)
20
+ throw new DXSendError(AUTH_EXPIRED_MESSAGE, 401);
20
21
  if (res.status === 429) {
21
22
  // Distinguished from the generic path because it is the one refusal the
22
23
  // creator can act on by simply waiting, and a bare "not accepted" would
23
24
  // read as their report being rejected on its merits.
24
- throw new DXSendError("Too many reports from this account just now — try again shortly.", 429);
25
+ throw new DXSendError("Too many reports from this account just now. Try again shortly.", 429);
25
26
  }
26
27
  throw new DXSendError(detail !== "" ? detail : `The report was not accepted (${res.status}).`, res.status);
27
28
  }
package/dist/dx/send.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { AUTH_EXPIRED_MESSAGE } from "../shared/authError.js";
2
+ import { apiFetch } from "../shared/http.js";
1
3
  import { renderPayloadWithVersionSummary, summarizeBuiltWith, versionDisclosureLines, } from "../platform/versionSummary.js";
2
4
  /**
3
5
  * The server bounds a report at 50 findings and refuses the whole thing past
@@ -79,7 +81,7 @@ export function renderPayload(report) {
79
81
  // creator to consent on a false claim — the one thing this screen exists to
80
82
  // prevent. So the sentence changes when a note is present.
81
83
  if (hasNote) {
82
- lines.push(" The generated fields carry no paths, flag values, or error messages.", " YOUR NOTE IS SENT EXACTLY AS WRITTEN — check it for paths, URLs,", " tokens, or customer names before sending.");
84
+ lines.push(" The generated fields carry no paths, flag values, or error messages.", " YOUR NOTE IS SENT EXACTLY AS WRITTEN. Check it for paths, URLs,", " tokens, or customer names before sending.");
83
85
  }
84
86
  else {
85
87
  lines.push(" No paths, no flag values, no error messages, and nothing about which", " project this was.");
@@ -102,7 +104,7 @@ export class DXSendError extends Error {
102
104
  /**
103
105
  * POST the report. The caller has already shown the payload and had it accepted.
104
106
  */
105
- export async function postReport(apiUrl, pat, report, fetchImpl = fetch) {
107
+ export async function postReport(apiUrl, pat, report, fetchImpl = apiFetch) {
106
108
  const res = await fetchImpl(`${apiUrl}/dx-reports`, {
107
109
  method: "POST",
108
110
  headers: { "content-type": "application/json", authorization: `Bearer ${pat}` },
@@ -117,9 +119,8 @@ export async function postReport(apiUrl, pat, report, fetchImpl = fetch) {
117
119
  catch {
118
120
  // A non-JSON body is not more informative than the status.
119
121
  }
120
- if (res.status === 401) {
121
- throw new DXSendError("Your access token is no longer valid — run `vincentt login`.", 401);
122
- }
122
+ if (res.status === 401)
123
+ throw new DXSendError(AUTH_EXPIRED_MESSAGE, 401);
123
124
  // A 400 here means the payload this CLI built failed the server's own
124
125
  // grammar. Surfaced verbatim rather than softened: it is a bug in this CLI,
125
126
  // and the creator is the one who can report it.
@@ -56,22 +56,6 @@ export declare function createProject(cfg: ResolvedConfig, orgSlug: string, name
56
56
  export declare function listProjects(cfg: ResolvedConfig, orgSlug: string): Promise<ProjectView[]>;
57
57
  /** Read a project by id. Backs `link`; the org is derived server-side from the row. */
58
58
  export declare function getProject(cfg: ResolvedConfig, projectId: string): Promise<ProjectView>;
59
- export interface MintedTunnel {
60
- tunnelId: string;
61
- /** The public host the phone opens: <slug>-<token>.<previewApex>. */
62
- hostname: string;
63
- /** cloudflared run token — `cloudflared tunnel run --token <this>`. */
64
- runToken: string;
65
- /** DNS record id, passed back on reap. */
66
- dnsRecordId: string;
67
- }
68
- /** Mint a per-session dev tunnel for `localPort` (the backend holds CF creds). */
69
- export declare function mintTunnel(cfg: ResolvedConfig, projectId: string, localPort: number): Promise<MintedTunnel>;
70
- /** Tear down a session tunnel (DNS route + tunnel). Best-effort; never throws. */
71
- export declare function reapTunnel(cfg: ResolvedConfig, projectId: string, ref: {
72
- tunnelId: string;
73
- dnsRecordId?: string;
74
- }): Promise<void>;
75
59
  /** Recursively list files under `dir`, returning dir-relative POSIX paths. */
76
60
  export declare function walkDir(dir: string, base?: string): Promise<string[]>;
77
61
  export interface MeMembership {
@@ -8,6 +8,7 @@ import { execFile } from "node:child_process";
8
8
  import { promisify } from "node:util";
9
9
  import { clientHeaders } from "../shared/clientHeader.js";
10
10
  import { authFailureMessage } from "../shared/authError.js";
11
+ import { apiFetch } from "../shared/http.js";
11
12
  const execFileAsync = promisify(execFile);
12
13
  export class BackendError extends Error {
13
14
  status;
@@ -18,7 +19,7 @@ export class BackendError extends Error {
18
19
  }
19
20
  }
20
21
  async function authFetch(cfg, pathname, init = {}) {
21
- return fetch(`${cfg.apiUrl}${pathname}`, {
22
+ return apiFetch(`${cfg.apiUrl}${pathname}`, {
22
23
  ...init,
23
24
  headers: {
24
25
  ...(init.headers ?? {}),
@@ -159,31 +160,6 @@ function parseProjectView(raw, what) {
159
160
  publishState: str("publishState") ?? "never",
160
161
  };
161
162
  }
162
- /** Mint a per-session dev tunnel for `localPort` (the backend holds CF creds). */
163
- export async function mintTunnel(cfg, projectId, localPort) {
164
- const res = await authFetch(cfg, `/projects/${projectId}/tunnel`, {
165
- method: "POST",
166
- headers: { "content-type": "application/json" },
167
- body: JSON.stringify({ port: localPort }),
168
- });
169
- if (!res.ok) {
170
- throw new BackendError(authFailureMessage("tunnel mint failed", res.status, await safeText(res)), res.status);
171
- }
172
- return (await res.json());
173
- }
174
- /** Tear down a session tunnel (DNS route + tunnel). Best-effort; never throws. */
175
- export async function reapTunnel(cfg, projectId, ref) {
176
- try {
177
- await authFetch(cfg, `/projects/${projectId}/tunnel/reap`, {
178
- method: "POST",
179
- headers: { "content-type": "application/json" },
180
- body: JSON.stringify(ref),
181
- });
182
- }
183
- catch {
184
- // Reap is a cleanup courtesy — a failure must never crash the preview teardown.
185
- }
186
- }
187
163
  /** Recursively list files under `dir`, returning dir-relative POSIX paths. */
188
164
  export async function walkDir(dir, base = dir) {
189
165
  const out = [];
@@ -0,0 +1,87 @@
1
+ import type { SettingsField } from "./settings.js";
2
+ export declare const DETECT_MAX_BYTES: number;
3
+ /** `notFile`: it exists but is not a regular file (a FIFO would block the read). */
4
+ export type ReadResult = {
5
+ text: string;
6
+ } | {
7
+ missing: true;
8
+ } | {
9
+ tooLarge: true;
10
+ } | {
11
+ notFile: true;
12
+ };
13
+ export interface DetectProbes {
14
+ /** Read a repo-relative file, at most DETECT_MAX_BYTES. */
15
+ readText: (rel: string) => Promise<ReadResult>;
16
+ exists: (rel: string) => Promise<boolean>;
17
+ }
18
+ export interface DetectedField {
19
+ value: string | number;
20
+ evidence: string;
21
+ }
22
+ export interface DetectResult {
23
+ /** Exactly the files that were read or looked for by name, in report order. */
24
+ read: string[];
25
+ fields: Partial<Record<SettingsField, DetectedField>>;
26
+ missing: {
27
+ field: SettingsField;
28
+ reason: string;
29
+ }[];
30
+ pm: PackageManager;
31
+ }
32
+ export type PackageManager = "npm" | "pnpm" | "yarn" | "bun";
33
+ /** One simple command of a script body: its words, wrappers and env assignments removed. */
34
+ interface Command {
35
+ words: string[];
36
+ /** The command as written, for evidence. */
37
+ text: string;
38
+ }
39
+ /** Split a script body on `&&`, `||`, `;`, `|` and `&`, honouring quotes. Linear. */
40
+ export declare function splitScript(body: string): Command[];
41
+ type Tok = {
42
+ t: "ident";
43
+ v: string;
44
+ } | {
45
+ t: "str";
46
+ v: string;
47
+ } | {
48
+ t: "num";
49
+ v: string;
50
+ } | {
51
+ t: "tpl";
52
+ v: string;
53
+ interpolated: boolean;
54
+ } | {
55
+ t: "p";
56
+ v: string;
57
+ } | {
58
+ t: "re";
59
+ };
60
+ /** A JS/TS tokenizer just good enough to find object keys. One pass, no backtracking. */
61
+ export declare function tokenize(src: string): Tok[];
62
+ export type ConfigValue = {
63
+ kind: "object";
64
+ spread: boolean;
65
+ } | {
66
+ kind: "number";
67
+ value: number;
68
+ } | {
69
+ kind: "string";
70
+ value: string;
71
+ } | {
72
+ kind: "dirname";
73
+ fn: "resolve" | "join";
74
+ args: string[];
75
+ } | {
76
+ kind: "expr";
77
+ };
78
+ /**
79
+ * Every object key whose objects nest DIRECTLY from an anonymous root, keyed by
80
+ * its dotted path (`devServer.port`, `build.outDir`). A key inside a function
81
+ * body, an array, a call argument that is not the config itself, or any value
82
+ * expression is not recorded: its value is judged as "an expression", which
83
+ * detection reports as not found.
84
+ */
85
+ export declare function scanConfigKeys(src: string): Map<string, ConfigValue[]>;
86
+ export declare function detect(probes: DetectProbes): Promise<DetectResult>;
87
+ export {};