@vincentt-xr/cli 0.15.2 → 0.17.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 (83) hide show
  1. package/dist/cli/args.js +1 -0
  2. package/dist/cli/index.js +198 -3
  3. package/dist/cli/usage-text.js +27 -0
  4. package/dist/cli/verb-surface.d.ts +3 -1
  5. package/dist/cli/verb-surface.js +12 -0
  6. package/dist/domain/api.d.ts +51 -0
  7. package/dist/domain/api.js +145 -0
  8. package/dist/domain/command.d.ts +75 -0
  9. package/dist/domain/command.js +467 -0
  10. package/dist/domain/copy.d.ts +334 -0
  11. package/dist/domain/copy.js +952 -0
  12. package/dist/domain/hostname.d.ts +57 -0
  13. package/dist/domain/hostname.js +116 -0
  14. package/dist/domain/names.d.ts +78 -0
  15. package/dist/domain/names.js +113 -0
  16. package/dist/domain/routedVerbs.d.ts +10 -0
  17. package/dist/domain/routedVerbs.js +30 -0
  18. package/dist/domain/wire.d.ts +140 -0
  19. package/dist/domain/wire.js +174 -0
  20. package/dist/dx/command.d.ts +13 -4
  21. package/dist/dx/command.js +26 -5
  22. package/dist/dx/report.d.ts +39 -7
  23. package/dist/dx/report.js +71 -20
  24. package/dist/dx/send.d.ts +22 -4
  25. package/dist/dx/send.js +29 -8
  26. package/dist/dx/skeleton.js +19 -0
  27. package/dist/dx/unbiddenVerbs.d.ts +25 -0
  28. package/dist/dx/unbiddenVerbs.js +39 -0
  29. package/dist/lib/initCommand.d.ts +3 -1
  30. package/dist/lib/initCommand.js +10 -0
  31. package/dist/lib/projects.d.ts +8 -1
  32. package/dist/lib/projects.js +12 -2
  33. package/dist/platform/allowlist.d.ts +36 -0
  34. package/dist/platform/allowlist.js +34 -0
  35. package/dist/platform/behind.d.ts +41 -0
  36. package/dist/platform/behind.js +82 -0
  37. package/dist/platform/command.d.ts +16 -0
  38. package/dist/platform/command.js +90 -0
  39. package/dist/platform/footer.d.ts +60 -0
  40. package/dist/platform/footer.js +101 -0
  41. package/dist/platform/notice.d.ts +29 -0
  42. package/dist/platform/notice.js +66 -0
  43. package/dist/platform/offSwitch.d.ts +19 -0
  44. package/dist/platform/offSwitch.js +21 -0
  45. package/dist/platform/outdated.d.ts +25 -0
  46. package/dist/platform/outdated.js +246 -0
  47. package/dist/platform/releaseCache.d.ts +69 -0
  48. package/dist/platform/releaseCache.js +140 -0
  49. package/dist/platform/releaseFetch.d.ts +51 -0
  50. package/dist/platform/releaseFetch.js +150 -0
  51. package/dist/platform/resolve.d.ts +30 -0
  52. package/dist/platform/resolve.js +128 -0
  53. package/dist/platform/templateUpgrade.d.ts +56 -0
  54. package/dist/platform/templateUpgrade.js +476 -0
  55. package/dist/platform/upgradeReport.d.ts +132 -0
  56. package/dist/platform/upgradeReport.js +258 -0
  57. package/dist/platform/versionSummary.d.ts +82 -0
  58. package/dist/platform/versionSummary.js +181 -0
  59. package/dist/platform/zonePath.d.ts +154 -0
  60. package/dist/platform/zonePath.js +286 -0
  61. package/dist/platform/zones.d.ts +64 -0
  62. package/dist/platform/zones.js +79 -0
  63. package/dist/preview/api.d.ts +19 -0
  64. package/dist/preview/api.js +29 -0
  65. package/dist/publish/api.d.ts +32 -5
  66. package/dist/publish/api.js +19 -5
  67. package/dist/publish/command.d.ts +10 -0
  68. package/dist/publish/command.js +30 -2
  69. package/dist/publish/copy.d.ts +14 -1
  70. package/dist/publish/copy.js +32 -3
  71. package/dist/scaffold/index.d.ts +50 -3
  72. package/dist/scaffold/index.js +61 -1
  73. package/dist/shared/config.d.ts +16 -0
  74. package/dist/shared/config.js +9 -1
  75. package/dist/shared/countLimit.d.ts +109 -0
  76. package/dist/shared/countLimit.js +169 -0
  77. package/package.json +1 -1
  78. package/dist/dx/qa.f7.fixtures.d.ts +0 -73
  79. package/dist/dx/qa.f7.fixtures.js +0 -58
  80. package/dist/publish/qa.f4.fixtures.d.ts +0 -25
  81. package/dist/publish/qa.f4.fixtures.js +0 -140
  82. package/dist/relay/qa.f12.fixtures.d.ts +0 -154
  83. package/dist/relay/qa.f12.fixtures.js +0 -358
@@ -0,0 +1,174 @@
1
+ // The f16 custom-domain wire shapes, and the ONE parser every renderer reads
2
+ // through.
3
+ //
4
+ // EVERY field is read defensively, for the reason lib/diagnostics.ts and
5
+ // library/wire.ts both record: the CLI and the API are versioned separately on a
6
+ // creator's machine, so a field a renderer reads unguarded is a field whose
7
+ // removal from the API crashes the command. A missing optional field degrades to
8
+ // a missing LINE, never to an exception.
9
+ //
10
+ // Two fields are read with more care than the rest, and the care is the point:
11
+ //
12
+ // • `records` is PRINTED VERBATIM. No client constructs a record
13
+ // (implementation.md § Wire fields). The parser therefore carries values
14
+ // through untouched and derives exactly one thing — the relative name — which
15
+ // it derives from the FQDN the API sent rather than by assembling a name from
16
+ // parts.
17
+ // • `observed` is A STRING, NEVER STRUCTURED. The wire says so explicitly, and
18
+ // the reason is that structure invites a client to re-render and lose the
19
+ // fidelity that is the whole point of the diagnosis ruling. It is read as a
20
+ // string and printed as a string; there is no parse step to lose it in.
21
+ /**
22
+ * The closed state enum (implementation.md § Wire fields).
23
+ *
24
+ * `DomainState` is a union of the seven the API can send. An unrecognized state
25
+ * is NOT coerced into one of these — `parseDomain` keeps the raw string, because
26
+ * a newer API sending an eighth state must render as itself rather than as a
27
+ * plausible neighbour. Rendering a state we do not know as `pending` would tell a
28
+ * creator nothing is wrong when something might be.
29
+ */
30
+ export const DOMAIN_STATES = [
31
+ "pending",
32
+ "verified",
33
+ "live",
34
+ "failing",
35
+ "held",
36
+ "cert_failed",
37
+ "detached",
38
+ ];
39
+ export function isDomainState(s) {
40
+ return DOMAIN_STATES.includes(s);
41
+ }
42
+ /**
43
+ * The closed findings enum (how-it-works.md Scenario 4). Each member carries an
44
+ * `observed` string.
45
+ *
46
+ * The severity is NOT on the wire as a field the CLI trusts blindly — see
47
+ * `findingSeverity` in copy.ts, which maps the enum to blocking/advisory. That
48
+ * mapping lives with the copy because the exit code follows the glyph, and the
49
+ * glyph is a copy decision.
50
+ */
51
+ // TAKEN FROM THE PRODUCER, not from the design record's prose: these are the
52
+ // exact strings `domain.DomainFinding` emits (api internal/domain/customdomain.go).
53
+ // The record's Scenario 4 describes the same set in narrative form and does not
54
+ // spell every constant, so the Go constants are authoritative for the wire.
55
+ export const FINDING_CODES = [
56
+ "TOKEN_MISSING",
57
+ "TOKEN_MISMATCH",
58
+ "TOKEN_STALE_PRESENT",
59
+ "WRONG_RECORD_TYPE",
60
+ "CNAME_WRONG_TARGET",
61
+ "CONFLICTING_RECORDS",
62
+ "UNEXPECTED_AAAA",
63
+ "PROXY_IN_FRONT",
64
+ "CAA_BLOCKS_ISSUANCE",
65
+ "STALE_ACME_CHALLENGE",
66
+ // D-44. The producer spells this WITHOUT a `CERT_` prefix; an earlier draft of
67
+ // this client invented one and would have rendered the finding as an unknown
68
+ // code — printing it, but with the enum string as its title and no remedy.
69
+ "VALIDATION_RECORD_GONE",
70
+ "MALFORMED_ZONE",
71
+ ];
72
+ /**
73
+ * D-44's finding, as a NAMED CONSTANT rather than a literal at each site.
74
+ *
75
+ * Two places branch on it — the severity table and the record re-print — and
76
+ * they were briefly spelled differently, which printed the finding while
77
+ * silently omitting the record that IS its remedy. A shared symbol makes that
78
+ * particular disagreement unexpressible.
79
+ */
80
+ export const VALIDATION_RECORD_GONE = "VALIDATION_RECORD_GONE";
81
+ function str(o, k) {
82
+ return typeof o[k] === "string" ? o[k] : undefined;
83
+ }
84
+ function num(o, k) {
85
+ return typeof o[k] === "number" ? o[k] : undefined;
86
+ }
87
+ function obj(raw) {
88
+ return typeof raw === "object" && raw !== null ? raw : {};
89
+ }
90
+ /**
91
+ * Parse one record. `type`, `name` and `value` are all required: a record
92
+ * missing any of the three cannot be created at a registrar, and printing a
93
+ * partial row would send a creator to paste a blank into a form.
94
+ */
95
+ export function parseRecord(raw) {
96
+ const o = obj(raw);
97
+ const type = str(o, "type");
98
+ const name = str(o, "name");
99
+ const value = str(o, "value");
100
+ if (!type || !name || !value)
101
+ return undefined;
102
+ return { type, name, value, ttl: num(o, "ttl") };
103
+ }
104
+ /**
105
+ * Parse one finding. A finding with no `observed` is DROPPED rather than
106
+ * rendered, because the shape's whole contract is that it reports what was seen
107
+ * — and a finding that restates the instructions is the thing the design refuses.
108
+ * Dropping is visible: `parseDomain` counts what it dropped.
109
+ */
110
+ export function parseFinding(raw) {
111
+ const o = obj(raw);
112
+ const code = str(o, "code");
113
+ const observed = str(o, "observed");
114
+ if (!code || observed === undefined)
115
+ return undefined;
116
+ return {
117
+ code,
118
+ observed,
119
+ expected: str(o, "expected"),
120
+ severity: str(o, "severity"),
121
+ };
122
+ }
123
+ /**
124
+ * Parse a domain row.
125
+ *
126
+ * `domain` is the only structurally required field: without a hostname there is
127
+ * nothing to render a row about. Everything else degrades to an absent line.
128
+ */
129
+ export function parseDomain(raw) {
130
+ const o = obj(raw);
131
+ const domain = str(o, "domain");
132
+ if (!domain)
133
+ return undefined;
134
+ const rawRecords = Array.isArray(o.records) ? o.records : [];
135
+ const records = rawRecords
136
+ .map(parseRecord)
137
+ .filter((r) => r !== undefined);
138
+ const rawFindings = Array.isArray(o.findings) ? o.findings : [];
139
+ const findings = rawFindings
140
+ .map(parseFinding)
141
+ .filter((f) => f !== undefined);
142
+ const rawState = str(o, "state") ?? "";
143
+ // `holdsAt` is null unless `failing`. A JSON null must read as ABSENT, not as
144
+ // the string "null" — `str` already refuses a non-string, which is the guard.
145
+ return {
146
+ domain: {
147
+ domain,
148
+ rawState,
149
+ state: isDomainState(rawState) ? rawState : undefined,
150
+ projectId: str(o, "projectId"),
151
+ projectName: str(o, "projectName"),
152
+ records,
153
+ findings,
154
+ holdsAt: str(o, "holdsAt"),
155
+ attachedAt: str(o, "attachedAt"),
156
+ permanentAddress: str(o, "permanentAddress"),
157
+ },
158
+ droppedFindings: rawFindings.length - findings.length,
159
+ };
160
+ }
161
+ /** Parse the list envelope. A row that cannot be parsed is skipped, never faked. */
162
+ export function parseDomainList(raw) {
163
+ const o = obj(raw);
164
+ const rows = Array.isArray(o.domains) ? o.domains : [];
165
+ return {
166
+ domains: rows
167
+ .map(parseDomain)
168
+ .filter((p) => p !== undefined)
169
+ .map((p) => p.domain),
170
+ count: num(o, "count"),
171
+ limit: num(o, "limit"),
172
+ planName: str(o, "planName"),
173
+ };
174
+ }
@@ -2,6 +2,7 @@ import type { ConsentDecision } from "./consent.js";
2
2
  import type { Enablement } from "./enablement.js";
3
3
  import type { JournalEntry } from "./journal.js";
4
4
  import type { WireReport } from "./send.js";
5
+ import { type BuiltWith } from "../platform/resolve.js";
5
6
  export interface DxDeps {
6
7
  entries: () => JournalEntry[];
7
8
  print: (line: string) => void;
@@ -159,13 +160,21 @@ export interface DxSendDeps extends DxDeps {
159
160
  apiUrl: string;
160
161
  pat: string;
161
162
  }) => Promise<Enablement>;
163
+ /**
164
+ * The version set. Injectable so a fixed set — or a REJECTING promise, the arm
165
+ * proving a fault omits — can be driven with no filesystem. Defaults to the one
166
+ * shared resolver against `cwd`.
167
+ */
168
+ builtWith?: () => Promise<BuiltWith | undefined>;
169
+ cwd?: string;
162
170
  }
163
171
  /**
164
172
  * `vincentt dx --send`.
165
173
  *
166
- * The order is the consent model and is not an implementation detail: find the
167
- * friction, BUILD the payload, SHOW it in full, ask, and only then send. A
168
- * creator who cannot see what leaves their machine before it leaves has not
169
- * agreed to anything.
174
+ * The order is the consent model and is not an implementation detail. A creator
175
+ * who cannot see what leaves their machine before it leaves has not agreed to
176
+ * anything:
177
+ *
178
+ * friction → GATE (three arms) → auth → RESOLVE → build → SHOW → ask → post
170
179
  */
171
180
  export declare function dxSendCommand(deps: DxSendDeps, options: DxOptions): Promise<number>;
@@ -2,6 +2,7 @@ import { findClusters, rankClusters } from "./clusters.js";
2
2
  import { enablementLine } from "./enablement.js";
3
3
  import { journalEnabled, journalPath, readJournal } from "./journal.js";
4
4
  import { buildReport, renderPayload } from "./send.js";
5
+ import { resolveBuiltWith } from "../platform/resolve.js";
5
6
  const DEFAULT_DAYS = 7;
6
7
  export function parseDxOptions(args) {
7
8
  const i = args.indexOf("--days");
@@ -321,10 +322,11 @@ export async function dxDisableCommand(deps) {
321
322
  /**
322
323
  * `vincentt dx --send`.
323
324
  *
324
- * The order is the consent model and is not an implementation detail: find the
325
- * friction, BUILD the payload, SHOW it in full, ask, and only then send. A
326
- * creator who cannot see what leaves their machine before it leaves has not
327
- * agreed to anything.
325
+ * The order is the consent model and is not an implementation detail. A creator
326
+ * who cannot see what leaves their machine before it leaves has not agreed to
327
+ * anything:
328
+ *
329
+ * friction → GATE (three arms) → auth → RESOLVE → build → SHOW → ask → post
328
330
  */
329
331
  export async function dxSendCommand(deps, options) {
330
332
  if (!deps.enabled()) {
@@ -416,7 +418,26 @@ export async function dxSendCommand(deps, options) {
416
418
  deps.print("Not signed in. Run `vincentt login` to send a report.");
417
419
  return 1;
418
420
  }
419
- const report = buildReport(clusters, deps.cliVersion(), options.note);
421
+ // RESOLVED AFTER THE GATE AND IMMEDIATELY BEFORE THE BUILD.
422
+ //
423
+ // This is deliberately the INVERSE of publish, which resolves AFTER its
424
+ // confirmation (`publish/command.ts`), and the two must not be "unified": on
425
+ // publish the version set is never shown, so resolving before the prompt would
426
+ // mean a REFUSED publish had read the creator's `node_modules` for nothing.
427
+ // Here the set IS part of what is being consented to, so a value not resolved
428
+ // cannot be shown and a value not shown cannot be consented to. One rule covers
429
+ // both: resolve at the last moment before the value is needed, and never
430
+ // earlier. On publish that moment is the POST; here it is the SHOW.
431
+ //
432
+ // The gate is already passed, so a refused send has read nothing at all.
433
+ //
434
+ // The `.catch` belongs HERE and not in the resolver: the resolver's `undefined`
435
+ // already means "nothing to say", so swallowing inside it would make a genuine
436
+ // fault indistinguishable from a legitimate empty tree inside the sole
437
+ // producer. At this call site the meaning is exact — a fault yields the same
438
+ // omission an empty project yields, and neither can fail a send.
439
+ const builtWith = await (deps.builtWith ?? (() => resolveBuiltWith(deps.cwd ?? process.cwd())))().catch(() => undefined);
440
+ const report = buildReport(clusters, deps.cliVersion(), options.note, builtWith);
420
441
  // Above the payload, on the unattended path only: name that this is enabled,
421
442
  // how old the grant is, WHERE it was granted, and that nothing is going to ask.
422
443
  const unattended = !interactive && enablement.enabled;
@@ -21,6 +21,7 @@
21
21
  */
22
22
  import type { Enablement } from "./enablement.js";
23
23
  import type { JournalEntry } from "./journal.js";
24
+ import { type BuiltWith } from "../platform/resolve.js";
24
25
  /** An observation as it goes on the wire. Mirrors the API's dxObservationRequest. */
25
26
  export interface WireObservation {
26
27
  note: string;
@@ -29,6 +30,14 @@ export interface WireObservation {
29
30
  export interface WireObservationReport {
30
31
  cliVersion: string;
31
32
  platform: string;
33
+ /**
34
+ * What the CLI resolved on this machine at report time. On the ROOT, not on
35
+ * the observation: it is a fact about the machine that produced the report,
36
+ * not about any one carrier, and putting it per-carrier would both be false
37
+ * and multiply it on the wire by the carrier count. Absent when nothing
38
+ * resolved.
39
+ */
40
+ builtWith?: BuiltWith;
32
41
  findings: never[];
33
42
  observations: WireObservation[];
34
43
  }
@@ -62,14 +71,28 @@ export declare function parseReportOptions(args: string[]): ReportOptions;
62
71
  * this run, and none of those is a reason to refuse to send what someone typed.
63
72
  */
64
73
  export declare function lastContext(entries: JournalEntry[]): string | undefined;
65
- export declare function buildObservationReport(note: string, cliVersion: string, context?: string): WireObservationReport;
74
+ /**
75
+ * The wire payload. PURE and SYNCHRONOUS, which is the property its tests rest
76
+ * on — `builtWith` is a PARAMETER, not a dep, so the async resolution and the
77
+ * `.catch(() => undefined)` that makes a fault an omission stay the caller's.
78
+ */
79
+ export declare function buildObservationReport(note: string, cliVersion: string, context?: string, builtWith?: BuiltWith): WireObservationReport;
66
80
  /**
67
81
  * Render the payload for the creator to read BEFORE it is sent.
68
82
  *
69
- * As in `send.ts`, this is the payload formatted, not a summary of it. The note
70
- * warning is unconditional here, where it is conditional there: an observation
71
- * is ALWAYS a note, so there is no path through this screen that does not need
72
- * it.
83
+ * As in `send.ts`, this is the payload formatted, WITH EXACTLY ONE FIELD
84
+ * SUMMARIZED: `builtWith` renders as a single `versions:` line at its own
85
+ * position inside the block, and no second field may join it. The exemption's
86
+ * reasoning and the three properties bounding it live in
87
+ * `platform/versionSummary.ts` rather than being restated at each screen, so the
88
+ * two cannot come to describe it differently.
89
+ *
90
+ * `renderPayload` (`send.ts`) is the other consent screen and carries the
91
+ * identical exemption. NEITHER MAY BE UPDATED WITHOUT THE OTHER.
92
+ *
93
+ * The note warning is unconditional here, where it is conditional there: an
94
+ * observation is ALWAYS a note, so there is no path through this screen that
95
+ * does not need it.
73
96
  */
74
97
  export declare function renderObservation(report: WireObservationReport): string[];
75
98
  export interface ReportDeps {
@@ -92,11 +115,20 @@ export interface ReportDeps {
92
115
  apiUrl: string;
93
116
  pat: string;
94
117
  }) => Promise<Enablement>;
118
+ /**
119
+ * The version set. Injectable so a fixed set — or a REJECTING promise, the arm
120
+ * proving a fault omits — can be driven with no filesystem. Defaults to the one
121
+ * shared resolver against `cwd`.
122
+ */
123
+ builtWith?: () => Promise<BuiltWith | undefined>;
124
+ cwd?: string;
95
125
  }
96
126
  /**
97
127
  * `vincentt report`.
98
128
  *
99
- * Same order as `dx --send`, for the same reason: build the payload, SHOW it,
100
- * ask, and only then send.
129
+ * Same order as `dx --send`, for the same reason — a creator who cannot see what
130
+ * leaves their machine before it leaves has not agreed to anything:
131
+ *
132
+ * bound-check → GATE (three arms) → auth → RESOLVE → build → SHOW → ask → post
101
133
  */
102
134
  export declare function reportCommand(deps: ReportDeps, options: ReportOptions): Promise<number>;
package/dist/dx/report.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { enablementLine } from "./enablement.js";
2
2
  import { DXSendError, platformTag } from "./send.js";
3
+ import { resolveBuiltWith } from "../platform/resolve.js";
4
+ import { renderPayloadWithVersionSummary, summarizeBuiltWith, versionDisclosureLines, } from "../platform/versionSummary.js";
3
5
  /** Matches the server's MaxDXNote. Checked here so a refusal costs no round trip. */
4
6
  export const MAX_NOTE = 2000;
5
7
  /**
@@ -39,38 +41,64 @@ export function lastContext(entries) {
39
41
  }
40
42
  return undefined;
41
43
  }
42
- export function buildObservationReport(note, cliVersion, context) {
44
+ /**
45
+ * The wire payload. PURE and SYNCHRONOUS, which is the property its tests rest
46
+ * on — `builtWith` is a PARAMETER, not a dep, so the async resolution and the
47
+ * `.catch(() => undefined)` that makes a fault an omission stay the caller's.
48
+ */
49
+ export function buildObservationReport(note, cliVersion, context, builtWith) {
43
50
  const o = { note: note.trim() };
44
51
  if (context !== undefined && context !== "")
45
52
  o.context = context;
46
- return {
47
- cliVersion,
48
- platform: platformTag(),
49
- // Always present and always empty: the server accepts a report with either
50
- // carrier, and sending the key explicitly says "no findings" rather than
51
- // leaving the shape to be inferred from an absent field.
52
- findings: [],
53
- observations: [o],
54
- };
53
+ // THE SUMMARY IS THE AUTHORITY ON WHETHER THE FIELD IS SENT — a version set the
54
+ // consent screen cannot render may not go on the wire. See
55
+ // `summarizeBuiltWith`.
56
+ const sendable = summarizeBuiltWith(builtWith);
57
+ // Always present and always empty: the server accepts a report with either
58
+ // carrier, and sending the key explicitly says "no findings" rather than
59
+ // leaving the shape to be inferred from an absent field.
60
+ //
61
+ // `builtWith` sits between `platform` and `findings` — the position the consent
62
+ // screen summarizes it at, and a sibling of the other two machine facts.
63
+ return sendable === undefined
64
+ ? { cliVersion, platform: platformTag(), findings: [], observations: [o] }
65
+ : {
66
+ cliVersion,
67
+ platform: platformTag(),
68
+ builtWith: sendable,
69
+ findings: [],
70
+ observations: [o],
71
+ };
55
72
  }
56
73
  /**
57
74
  * Render the payload for the creator to read BEFORE it is sent.
58
75
  *
59
- * As in `send.ts`, this is the payload formatted, not a summary of it. The note
60
- * warning is unconditional here, where it is conditional there: an observation
61
- * is ALWAYS a note, so there is no path through this screen that does not need
62
- * it.
76
+ * As in `send.ts`, this is the payload formatted, WITH EXACTLY ONE FIELD
77
+ * SUMMARIZED: `builtWith` renders as a single `versions:` line at its own
78
+ * position inside the block, and no second field may join it. The exemption's
79
+ * reasoning and the three properties bounding it live in
80
+ * `platform/versionSummary.ts` rather than being restated at each screen, so the
81
+ * two cannot come to describe it differently.
82
+ *
83
+ * `renderPayload` (`send.ts`) is the other consent screen and carries the
84
+ * identical exemption. NEITHER MAY BE UPDATED WITHOUT THE OTHER.
85
+ *
86
+ * The note warning is unconditional here, where it is conditional there: an
87
+ * observation is ALWAYS a note, so there is no path through this screen that
88
+ * does not need it.
63
89
  */
64
90
  export function renderObservation(report) {
65
91
  return [
66
92
  "This is what would be sent:",
67
93
  "",
68
- ...JSON.stringify(report, null, 2)
69
- .split("\n")
70
- .map((l) => ` ${l}`),
94
+ ...renderPayloadWithVersionSummary(report, report.builtWith).map((l) => ` ${l}`),
71
95
  "",
72
96
  " YOUR NOTE IS SENT EXACTLY AS WRITTEN — check it for paths, URLs,",
73
97
  " tokens, or customer names before sending.",
98
+ // Entry 2: it explains the one anomalous line above, so it sits directly
99
+ // under the warning where the reader's eye lands next — above the context
100
+ // line, which would otherwise make them hunt for the explanation.
101
+ ...versionDisclosureLines(report.builtWith),
74
102
  " The context line is the shape of your last command, with values removed.",
75
103
  " Your journal stays on this machine.",
76
104
  ];
@@ -78,8 +106,10 @@ export function renderObservation(report) {
78
106
  /**
79
107
  * `vincentt report`.
80
108
  *
81
- * Same order as `dx --send`, for the same reason: build the payload, SHOW it,
82
- * ask, and only then send.
109
+ * Same order as `dx --send`, for the same reason — a creator who cannot see what
110
+ * leaves their machine before it leaves has not agreed to anything:
111
+ *
112
+ * bound-check → GATE (three arms) → auth → RESOLVE → build → SHOW → ask → post
83
113
  */
84
114
  export async function reportCommand(deps, options) {
85
115
  const note = options.note.trim();
@@ -126,7 +156,28 @@ export async function reportCommand(deps, options) {
126
156
  return 1;
127
157
  }
128
158
  const context = options.noContext ? undefined : lastContext(deps.entries());
129
- const report = buildObservationReport(note, deps.cliVersion(), context);
159
+ // RESOLVED AFTER THE GATE AND IMMEDIATELY BEFORE THE BUILD.
160
+ //
161
+ // This is deliberately the INVERSE of publish, which resolves AFTER its
162
+ // confirmation (`publish/command.ts`), and the two must not be "unified": on
163
+ // publish the version set is never shown, so it rides along with bytes the
164
+ // creator already decided to upload and resolving before the prompt would mean
165
+ // a REFUSED publish had read their `node_modules` for nothing. Here the set IS
166
+ // part of what is being consented to, so a value not resolved cannot be shown
167
+ // and a value not shown cannot be consented to — resolving after the ask would
168
+ // transmit a field the creator never saw. One rule covers both: resolve at the
169
+ // last moment before the value is needed, and never earlier. On publish that
170
+ // moment is the POST; here it is the SHOW.
171
+ //
172
+ // The gate is already passed, so a refused report has read nothing at all.
173
+ //
174
+ // The `.catch` belongs HERE and not in the resolver: the resolver's `undefined`
175
+ // already means "nothing to say", so swallowing inside it would make a genuine
176
+ // fault indistinguishable from a legitimate empty tree inside the sole
177
+ // producer. At this call site the meaning is exact — a fault yields the same
178
+ // omission an empty project yields, and neither can fail a report.
179
+ const builtWith = await (deps.builtWith ?? (() => resolveBuiltWith(deps.cwd ?? process.cwd())))().catch(() => undefined);
180
+ const report = buildObservationReport(note, deps.cliVersion(), context, builtWith);
130
181
  const unattended = !interactive && enablement.enabled;
131
182
  if (unattended) {
132
183
  const line = enablementLine(enablement, deps.now());
package/dist/dx/send.d.ts CHANGED
@@ -16,6 +16,7 @@
16
16
  * that needs server-side scrubbing was built wrong.
17
17
  */
18
18
  import type { Cluster } from "./clusters.js";
19
+ import type { BuiltWith } from "../platform/resolve.js";
19
20
  /** A finding as it goes on the wire. Mirrors the API's dxFindingRequest. */
20
21
  export interface WireFinding {
21
22
  kind: "error" | "repetition";
@@ -29,6 +30,13 @@ export interface WireFinding {
29
30
  export interface WireReport {
30
31
  cliVersion: string;
31
32
  platform: string;
33
+ /**
34
+ * What the CLI resolved on this machine at send time. A sibling of
35
+ * `cliVersion` and `platform` at the ROOT: it is a fact about the machine that
36
+ * produced the report, exactly as those two are, and it describes the whole
37
+ * report rather than any one finding. Absent when nothing resolved.
38
+ */
39
+ builtWith?: BuiltWith;
32
40
  findings: WireFinding[];
33
41
  }
34
42
  /**
@@ -45,14 +53,24 @@ export declare function platformTag(): string;
45
53
  * function reads counts and timings, never values. If a future journal field
46
54
  * carries content, it must not be added to this mapping without the same
47
55
  * scrutiny the skeleton got.
56
+ *
57
+ * `builtWith` is a PARAMETER, not a dep: this function stays synchronous and
58
+ * pure, which is the property its tests rest on. The async resolution — and the
59
+ * `.catch(() => undefined)` that makes a fault an omission — is the caller's.
48
60
  */
49
- export declare function buildReport(clusters: Cluster[], cliVersion: string, note?: string): WireReport;
61
+ export declare function buildReport(clusters: Cluster[], cliVersion: string, note?: string, builtWith?: BuiltWith): WireReport;
50
62
  /**
51
63
  * Render the payload for the creator to read BEFORE it is sent.
52
64
  *
53
- * This is not a summary of the payload — it is the payload, formatted. A preview
54
- * that paraphrased what would be sent would be exactly the wrong thing to show
55
- * someone deciding whether to send it.
65
+ * This is the payload, formatted, WITH EXACTLY ONE FIELD SUMMARIZED: `builtWith`
66
+ * renders as a single `versions:` line at its own position inside the block.
67
+ * Every other field is its literal bytes, and no second field may join it. The
68
+ * exemption's reasoning and the three properties that bound it are in
69
+ * `platform/versionSummary.ts` — read there rather than restated here, so the two
70
+ * consent screens cannot come to describe it differently.
71
+ *
72
+ * `renderObservation` (`report.ts`) is the other consent screen and carries the
73
+ * identical exemption. NEITHER MAY BE UPDATED WITHOUT THE OTHER.
56
74
  */
57
75
  export declare function renderPayload(report: WireReport): string[];
58
76
  export interface SendResult {
package/dist/dx/send.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { renderPayloadWithVersionSummary, summarizeBuiltWith, versionDisclosureLines, } from "../platform/versionSummary.js";
1
2
  /**
2
3
  * The server bounds a report at 50 findings and refuses the whole thing past
3
4
  * that, so the worst clusters are sent rather than the request rejected. They
@@ -16,8 +17,12 @@ export function platformTag() {
16
17
  * function reads counts and timings, never values. If a future journal field
17
18
  * carries content, it must not be added to this mapping without the same
18
19
  * scrutiny the skeleton got.
20
+ *
21
+ * `builtWith` is a PARAMETER, not a dep: this function stays synchronous and
22
+ * pure, which is the property its tests rest on. The async resolution — and the
23
+ * `.catch(() => undefined)` that makes a fault an omission — is the caller's.
19
24
  */
20
- export function buildReport(clusters, cliVersion, note) {
25
+ export function buildReport(clusters, cliVersion, note, builtWith) {
21
26
  const findings = clusters.slice(0, MAX_SENT_FINDINGS).map((c) => {
22
27
  const classes = [
23
28
  ...new Set(c.entries.map((e) => e.err).filter((e) => e !== null)),
@@ -38,23 +43,35 @@ export function buildReport(clusters, cliVersion, note) {
38
43
  if (note !== undefined && note.trim() !== "" && findings.length > 0) {
39
44
  findings[0].note = note.trim();
40
45
  }
41
- return { cliVersion, platform: platformTag(), findings };
46
+ // THE SUMMARY IS THE AUTHORITY ON WHETHER THE FIELD IS SENT. A version set the
47
+ // summary cannot render is one the creator would never see, and a field shown
48
+ // nowhere may not go on the wire — see `summarizeBuiltWith`.
49
+ const sendable = summarizeBuiltWith(builtWith);
50
+ // `builtWith` sits between `platform` and `findings`, which is where the
51
+ // consent screen renders its summary and where the api reads it.
52
+ return sendable === undefined
53
+ ? { cliVersion, platform: platformTag(), findings }
54
+ : { cliVersion, platform: platformTag(), builtWith: sendable, findings };
42
55
  }
43
56
  /**
44
57
  * Render the payload for the creator to read BEFORE it is sent.
45
58
  *
46
- * This is not a summary of the payload — it is the payload, formatted. A preview
47
- * that paraphrased what would be sent would be exactly the wrong thing to show
48
- * someone deciding whether to send it.
59
+ * This is the payload, formatted, WITH EXACTLY ONE FIELD SUMMARIZED: `builtWith`
60
+ * renders as a single `versions:` line at its own position inside the block.
61
+ * Every other field is its literal bytes, and no second field may join it. The
62
+ * exemption's reasoning and the three properties that bound it are in
63
+ * `platform/versionSummary.ts` — read there rather than restated here, so the two
64
+ * consent screens cannot come to describe it differently.
65
+ *
66
+ * `renderObservation` (`report.ts`) is the other consent screen and carries the
67
+ * identical exemption. NEITHER MAY BE UPDATED WITHOUT THE OTHER.
49
68
  */
50
69
  export function renderPayload(report) {
51
70
  const hasNote = report.findings.some((f) => f.note !== undefined && f.note !== "");
52
71
  const lines = [
53
72
  "This is what would be sent:",
54
73
  "",
55
- ...JSON.stringify(report, null, 2)
56
- .split("\n")
57
- .map((l) => ` ${l}`),
74
+ ...renderPayloadWithVersionSummary(report, report.builtWith).map((l) => ` ${l}`),
58
75
  "",
59
76
  ];
60
77
  // The generated fields carry shape only. The NOTE does not, and saying
@@ -67,6 +84,10 @@ export function renderPayload(report) {
67
84
  else {
68
85
  lines.push(" No paths, no flag values, no error messages, and nothing about which", " project this was.");
69
86
  }
87
+ // Entry 2 of the disclosure stack: it explains the one anomalous line on the
88
+ // screen above, so it goes directly under the scope sentence a reader's eye
89
+ // lands on next. Absent entirely when nothing resolved.
90
+ lines.push(...versionDisclosureLines(report.builtWith));
70
91
  lines.push(" Your journal stays on this machine.");
71
92
  return lines;
72
93
  }
@@ -20,9 +20,24 @@ const ELIDED = "<v>";
20
20
  * on-disk journal and any report built from it. Lowercase kebab-case is exactly
21
21
  * what slugs and project names are made of, so no character-class check catches
22
22
  * this — only a list of the verbs that actually exist.
23
+ *
24
+ * THIS SET MUST COVER EVERY DISPATCHED VERB, and a missing one is not a leak but
25
+ * a mis-bucketing: `journal.ts` records `verbOf(argv)`, which answers `<verb>`
26
+ * for anything absent here, so every invocation of a missing verb clusters under
27
+ * the placeholder and its friction is invisible. That also broke `lastContext()`
28
+ * — `report.ts` skips entries whose verb is `report`, which never matched while
29
+ * `report` was absent, so the context line a creator consents to could be the
30
+ * skeleton of their own `report` invocation rather than the command before it.
31
+ *
32
+ * Adding a verb CANNOT widen what a positional carries: `skeleton()` consults
33
+ * this set only when `out.length === 0`, so it reaches the FIRST token only, and
34
+ * every later positional is elided regardless. The historical leak lived in the
35
+ * first token.
23
36
  */
24
37
  const KNOWN_VERBS = new Set([
38
+ "assets",
25
39
  "create",
40
+ "domain",
26
41
  "dx",
27
42
  "feedback",
28
43
  "init",
@@ -31,9 +46,12 @@ const KNOWN_VERBS = new Set([
31
46
  "logout",
32
47
  "logs",
33
48
  "network",
49
+ "outdated",
34
50
  "preview",
51
+ "projects",
35
52
  "publish",
36
53
  "relay",
54
+ "report",
37
55
  "trace",
38
56
  "unlink",
39
57
  "version",
@@ -56,6 +74,7 @@ const KNOWN_FLAGS = new Set([
56
74
  "--json",
57
75
  "--limit",
58
76
  "--name",
77
+ "--no-context",
59
78
  "--no-install",
60
79
  "--no-qr",
61
80
  "--note",