@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,169 @@
1
+ // The three count-limit refusals — seats, published projects, concurrent live
2
+ // previews — as they reach a creator standing at a ceiling.
3
+ //
4
+ // THE REFUSAL IS THE NOTICE. A count limit is evaluated in the same request as
5
+ // the act it refuses, so no earlier moment exists at which a warning could have
6
+ // been delivered (B-F10-2, as amended). The obligation is discharged by the
7
+ // refusal naming the axis, the current count and the ceiling — which means a
8
+ // refusal rendered without its figures does not discharge it at all. That is the
9
+ // whole reason these arms exist rather than the generic `✗ {message}` default.
10
+ //
11
+ // Two prohibitions govern every string below:
12
+ // - NO PRICE, and no plan other than the one in force. f16's inherited rule: a
13
+ // surface tells a creator where they stand, never what to buy.
14
+ // - NO COMMAND that resolves the refusal. An agent's default response to a
15
+ // failure is to run the next thing it is handed, and there is no CLI verb
16
+ // that frees a seat, unpublishes a project or ends someone else's preview.
17
+ // The copy names the act as a human one, in the console.
18
+ const SUPPORT_EMAIL = "hello@vincentt.studio";
19
+ const DEFAULT_CONSOLE_URL = "https://console.vincentt.studio";
20
+ /**
21
+ * THE WIRE WINS over the machine's configured active org.
22
+ *
23
+ * The API names the org that owns the project being acted on; local config names
24
+ * whichever org the CLI was last pointed at, and on a machine with more than one
25
+ * workspace they are routinely different. Naming the local one would print the
26
+ * wrong workspace on the exact screen that is refusing the creator — and the
27
+ * refusal's figures are about the org the wire named, so a mismatched heading
28
+ * makes the numbers read as wrong rather than the name.
29
+ *
30
+ * The local values remain as a fallback for a body that carried neither, which
31
+ * is a refusal from an older api rather than a shape this client accepts.
32
+ */
33
+ export function resolveCountLimit(detail, local) {
34
+ return {
35
+ orgName: detail.orgName ?? local?.name,
36
+ orgSlug: detail.orgSlug ?? local?.slug,
37
+ planName: detail.planName,
38
+ count: detail.count ?? 0,
39
+ ceiling: detail.ceiling ?? 0,
40
+ consoleUrl: local?.consoleUrl,
41
+ };
42
+ }
43
+ /**
44
+ * `Acme (acme)` — name and slug when they differ, one of them when they do not,
45
+ * and an empty string when the wire named neither. Matches f10's shipped
46
+ * plan-limit heading so the four plan refusals read as one family.
47
+ */
48
+ export function orgLabel(name, slug) {
49
+ if (name && slug && name !== slug)
50
+ return `${name} (${slug})`;
51
+ return name || slug || "";
52
+ }
53
+ /**
54
+ * The figures, STACKED AND ALIGNED rather than inline. They are a comparison —
55
+ * the count against the ceiling — and f2's convention puts two values a creator
56
+ * must judge on their own aligned lines. Inline they are a sentence to parse;
57
+ * stacked, an agent and a human read them the same way.
58
+ */
59
+ export function countRows(i, axisLabel, planSuffix) {
60
+ const rows = [];
61
+ if (i.orgName || i.orgSlug)
62
+ rows.push(["Organization", orgLabel(i.orgName, i.orgSlug)]);
63
+ if (i.planName)
64
+ rows.push(["Plan", `${i.planName} — ${planSuffix}`]);
65
+ rows.push([axisLabel, `${i.count} of ${i.ceiling} used`]);
66
+ return alignedRows(rows);
67
+ }
68
+ function alignedRows(rows) {
69
+ const width = Math.max(...rows.map((r) => r[0].length));
70
+ return rows.map(([label, value]) => ` ${label.padEnd(width)} ${value}`);
71
+ }
72
+ /**
73
+ * f4's rule: where a verb does not exist because the act belongs to another
74
+ * surface, say where it lives and link to it — and if the link cannot be built,
75
+ * drop the link and keep the explanation.
76
+ *
77
+ * `path` is the org-scoped page that shows the axis being refused.
78
+ */
79
+ export function consoleLines(i, path) {
80
+ if (!i.orgSlug)
81
+ return [` Email ${SUPPORT_EMAIL}.`, ""];
82
+ const base = (i.consoleUrl ?? DEFAULT_CONSOLE_URL).replace(/\/$/, "");
83
+ return [` See ${base}/orgs/${i.orgSlug}/${path}, or email`, ` ${SUPPORT_EMAIL}.`, ""];
84
+ }
85
+ // THERE IS NO SEAT ARM HERE, AND THE ABSENCE IS THE DESIGN.
86
+ //
87
+ // `seat_limit` is refused on `POST /orgs/{slug}/invites`, which the CLI does not
88
+ // call and has no verb for: inviting a person is an act whose authorized actor
89
+ // is a human, so it lives in the console (f15's convention). The seat refusal's
90
+ // copy — including the two ways to free a seat — is the console's to render.
91
+ //
92
+ // A renderer for it added here would be an export with no caller, which is worse
93
+ // than nothing: it reads as coverage and cannot fail.
94
+ /**
95
+ * The published-project ceiling, refused at open-publish.
96
+ *
97
+ * "One published project" means ONE LIVE ADDRESS AT A TIME, and the copy says
98
+ * so, because the other reading — one project ever published — is what makes a
99
+ * creator believe they have permanently spent their slot on a mistake. Taking a
100
+ * project offline frees it, and that is the sentence that distinguishes the two.
101
+ *
102
+ * It does NOT say the project being published is excluded from the count, even
103
+ * though it is: a creator refused here is publishing a SECOND project, so the
104
+ * exclusion never applies on the screen that would explain it.
105
+ */
106
+ export function publishedLimitRefused(i) {
107
+ return [
108
+ "",
109
+ "✗ This organization is already publishing as many projects as its plan allows.",
110
+ "",
111
+ ...countRows(i, "Published", "published projects are counted per organization"),
112
+ "",
113
+ " The count is projects with a live address right now, not projects that have",
114
+ " ever been published. Taking one offline in the console frees the slot, and",
115
+ " everything already published stays exactly as it is.",
116
+ "",
117
+ ...consoleLines(i, "projects"),
118
+ " Nothing was uploaded.",
119
+ "",
120
+ ];
121
+ }
122
+ /**
123
+ * The concurrent-preview ceiling, refused at mint.
124
+ *
125
+ * "Still running" is the load-bearing claim and it is stated first among the
126
+ * facts. A creator whose `vincentt preview` just refused has every reason to
127
+ * believe it took something down — the command that failed is the command that
128
+ * starts previews — and B-F10-3's guarantee (a count limit refuses growth and
129
+ * removes nothing) is invisible unless the refusal says it.
130
+ */
131
+ export function previewLimitRefused(i) {
132
+ return [
133
+ "",
134
+ "✗ This organization has as many previews running as its plan allows.",
135
+ "",
136
+ ...countRows(i, "Previews", "previews are counted per organization"),
137
+ "",
138
+ " Every preview that was running is still running — this one did not start.",
139
+ " A preview stops when whoever started it stops it, or when it reaches its",
140
+ " 12-hour limit.",
141
+ "",
142
+ ...consoleLines(i, "projects"),
143
+ " Nothing was started.",
144
+ "",
145
+ ];
146
+ }
147
+ /**
148
+ * The storage ceiling. It NAMES NO FIGURE, and the asymmetry with the three
149
+ * count refusals above is deliberate rather than an omission: the ceiling is an
150
+ * abuse ceiling, not a tier limit. It is flat on every plan, never sold, never
151
+ * displayed, and its only audience is an operator. A number here would be the
152
+ * product printing a magnitude a creator cannot act on and was never offered.
153
+ *
154
+ * Support is therefore the whole remedy — there is no console page showing this
155
+ * and no plan that raises it.
156
+ */
157
+ export function storageCeilingRefused() {
158
+ return [
159
+ "",
160
+ "✗ This organization cannot upload more files right now.",
161
+ "",
162
+ " This is not a plan limit and taking a different plan will not change it.",
163
+ "",
164
+ ` Email ${SUPPORT_EMAIL} and it can be looked at.`,
165
+ "",
166
+ " Nothing was uploaded.",
167
+ "",
168
+ ];
169
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincentt-xr/cli",
3
- "version": "0.15.2",
3
+ "version": "0.17.0",
4
4
  "description": "Vincentt CLI — the vincentt command-line dev loop (login, create, publish, preview, diagnostics, feedback, relay)",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -1,73 +0,0 @@
1
- /**
2
- * QA fixtures for the dx-agent-consent delta (f7).
3
- *
4
- * Declared in the change's qa.md `## Fixtures` section. All three are
5
- * machine-config state, which f7 has never had a fixture for — its existing
6
- * fixtures (F7_JOURNAL_FRICTION, F7_REPORT_EXPIRED, …) are journal-side or
7
- * server-side.
8
- *
9
- * F7_CONFIG_SIGNED_IN is written by driving the REAL writeMachineConfig(), never
10
- * by hand-writing JSON. A hand-written config asserts against a shape the CLI
11
- * never produces — the f1 orgs/memberships failure at file scope, where a mock
12
- * of state the API could actually produce drifted from the API and stayed green.
13
- *
14
- * These are QA-owned (qa.* prefix) and deliberately separate from dev's test
15
- * files in the same package.
16
- */
17
- import type { JournalEntry } from "./journal.js";
18
- /** The honest baseline config, as the CLI itself writes it. */
19
- export declare const F7_CONFIG_SIGNED_IN: {
20
- readonly apiUrl: "https://api.test";
21
- readonly consoleUrl: "https://console.test";
22
- readonly pat: "vct_alice";
23
- readonly activeOrg: "alice";
24
- };
25
- /**
26
- * The instant every fixture is dated against. 145 days after the grant in
27
- * F7_CONFIG_ENABLED, and before DX_UNATTENDED_SUNSET.
28
- */
29
- export declare const F7_NOW: number;
30
- /** 145 days before F7_NOW. The AGE is the point — see qa.md on F7_CONFIG_ENABLED. */
31
- export declare const F7_ENABLED_AT = "2026-04-01T00:00:00.000Z";
32
- /**
33
- * The single pinned human-facing shape. Every human surface must render this
34
- * byte-for-byte; the age is the load-bearing half, so it is pinned as a literal
35
- * rather than computed (a computed expectation would follow a broken
36
- * implementation wherever it went).
37
- */
38
- export declare const F7_PINNED_ENABLEMENT_LINE = "Unattended send: ENABLED since 2026-04-01 (145 days)";
39
- /** F7_CONFIG_SIGNED_IN + durable enablement, aged. */
40
- export declare const F7_CONFIG_ENABLED: {
41
- readonly dxSendEnabled: true;
42
- readonly dxSendEnabledAt: "2026-04-01T00:00:00.000Z";
43
- readonly apiUrl: "https://api.test";
44
- readonly consoleUrl: "https://console.test";
45
- readonly pat: "vct_alice";
46
- readonly activeOrg: "alice";
47
- };
48
- /**
49
- * Every shape the enablement read must treat as DISABLED.
50
- *
51
- * Rows B, C, E, F, G are TRUTHY IN JAVASCRIPT — `if (cfg.dxSendEnabled)` passes
52
- * every one of them. That is the entire reason this fixture exists, and why the
53
- * read is specified as `=== true`. The worst is B ("false"): what a person
54
- * hand-editing this file writes when trying to turn the feature OFF.
55
- *
56
- * `raw` rows (K, L) are written as bytes rather than as an object, because the
57
- * property under test is that an UNPARSEABLE config fails closed instead of
58
- * throwing through into an unrelated verb.
59
- */
60
- export interface MalformedRow {
61
- id: string;
62
- why: string;
63
- /** A config object to serialize, or undefined when `raw` carries the bytes. */
64
- patch?: Record<string, unknown>;
65
- /** Literal file bytes, for the rows that are not valid JSON at all. */
66
- raw?: string;
67
- }
68
- export declare const F7_CONFIG_MALFORMED_ENABLEMENT: MalformedRow[];
69
- /**
70
- * F7_JOURNAL_FRICTION, as the record declares it: three runs of the same verb in
71
- * a few minutes, which is a repetition cluster.
72
- */
73
- export declare function f7JournalFriction(now?: number): JournalEntry[];
@@ -1,58 +0,0 @@
1
- /** The honest baseline config, as the CLI itself writes it. */
2
- export const F7_CONFIG_SIGNED_IN = {
3
- apiUrl: "https://api.test",
4
- consoleUrl: "https://console.test",
5
- pat: "vct_alice",
6
- activeOrg: "alice",
7
- };
8
- /**
9
- * The instant every fixture is dated against. 145 days after the grant in
10
- * F7_CONFIG_ENABLED, and before DX_UNATTENDED_SUNSET.
11
- */
12
- export const F7_NOW = Date.parse("2026-08-24T12:00:00.000Z");
13
- /** 145 days before F7_NOW. The AGE is the point — see qa.md on F7_CONFIG_ENABLED. */
14
- export const F7_ENABLED_AT = "2026-04-01T00:00:00.000Z";
15
- /**
16
- * The single pinned human-facing shape. Every human surface must render this
17
- * byte-for-byte; the age is the load-bearing half, so it is pinned as a literal
18
- * rather than computed (a computed expectation would follow a broken
19
- * implementation wherever it went).
20
- */
21
- export const F7_PINNED_ENABLEMENT_LINE = "Unattended send: ENABLED since 2026-04-01 (145 days)";
22
- /** F7_CONFIG_SIGNED_IN + durable enablement, aged. */
23
- export const F7_CONFIG_ENABLED = {
24
- ...F7_CONFIG_SIGNED_IN,
25
- dxSendEnabled: true,
26
- dxSendEnabledAt: F7_ENABLED_AT,
27
- };
28
- export const F7_CONFIG_MALFORMED_ENABLEMENT = [
29
- { id: "A", why: 'truthy STRING "true"', patch: { dxSendEnabled: "true", dxSendEnabledAt: F7_ENABLED_AT } },
30
- { id: "B", why: 'truthy in JS, means false to a human', patch: { dxSendEnabled: "false", dxSendEnabledAt: F7_ENABLED_AT } },
31
- { id: "C", why: 'truthy in JS ("0")', patch: { dxSendEnabled: "0", dxSendEnabledAt: F7_ENABLED_AT } },
32
- { id: "D", why: "truthy, not the boolean (1)", patch: { dxSendEnabled: 1, dxSendEnabledAt: F7_ENABLED_AT } },
33
- { id: "E", why: "truthy ({} — what a refactor grouping the fields produces)", patch: { dxSendEnabled: {}, dxSendEnabledAt: F7_ENABLED_AT } },
34
- { id: "F", why: "truthy ([])", patch: { dxSendEnabled: [], dxSendEnabledAt: F7_ENABLED_AT } },
35
- { id: "G", why: 'truthy ("garbage")', patch: { dxSendEnabled: "garbage", dxSendEnabledAt: F7_ENABLED_AT } },
36
- { id: "H", why: "key absent entirely", patch: {} },
37
- { id: "I", why: "enabled true, dxSendEnabledAt absent", patch: { dxSendEnabled: true } },
38
- { id: "J", why: 'enabled true, dxSendEnabledAt "not-a-date"', patch: { dxSendEnabled: true, dxSendEnabledAt: "not-a-date" } },
39
- { id: "K", why: "file truncated mid-JSON", raw: '{"pat":"vct_alice","dxSendEn' },
40
- { id: "L", why: "file empty (0 bytes)", raw: "" },
41
- ];
42
- /**
43
- * F7_JOURNAL_FRICTION, as the record declares it: three runs of the same verb in
44
- * a few minutes, which is a repetition cluster.
45
- */
46
- export function f7JournalFriction(now = F7_NOW) {
47
- const entry = (minutesAgo) => ({
48
- ts: new Date(now - minutesAgo * 60_000).toISOString(),
49
- v: "0.7.0",
50
- verb: "preview",
51
- skel: "preview",
52
- exit: 0,
53
- ms: 800,
54
- err: null,
55
- wt: "abc123",
56
- });
57
- return [entry(3), entry(2), entry(1)];
58
- }
@@ -1,25 +0,0 @@
1
- export type DistFixtureName = "CLEAN" | "TEMPLATE" | "MISSING_META" | "DEAD_IMAGE" | "MULTI_HTML" | "ADVERSARIAL" | "NO_DIST" | "OVERSIZE";
2
- /** The template defaults corpus the CLI ships, used verbatim by TEMPLATE. */
3
- export declare const TEMPLATE_TITLE = "Vite + React";
4
- export declare const TEMPLATE_DESCRIPTION = "Vite + React + TypeScript starter";
5
- /**
6
- * distFixture materializes one named tree under a fresh temp dir and returns
7
- * its cwd. The caller removes it.
8
- */
9
- export declare function distFixture(name: DistFixtureName): Promise<string>;
10
- export declare function removeFixture(cwd: string): Promise<void>;
11
- /**
12
- * The SHARED ADVERSARIAL ROWS. This list is the fixture the API's Go table test
13
- * and this suite both read; row-count parity is asserted in both runners.
14
- *
15
- * `reason` is the closed-enum value the API answers with. A row whose reason
16
- * differs between the two runners is a validator that disagrees with itself
17
- * across a repo boundary, which shows up only as a publish that cannot succeed.
18
- */
19
- export interface AdversarialRow {
20
- name: string;
21
- path: string;
22
- /** "" means the row must be ACCEPTED. */
23
- reason: string;
24
- }
25
- export declare function adversarialRows(): AdversarialRow[];
@@ -1,140 +0,0 @@
1
- // QA f4 · the eight on-disk `dist/` trees the catalog names (4C / 4E).
2
- //
3
- // A distFixture is a REAL DIRECTORY, not a mock: the checklist parses HTML off
4
- // disk and the manifest hashes bytes off disk, so a fixture that stubbed either
5
- // would be asserting against a shape the product never reads.
6
- //
7
- // `ADVERSARIAL` is shared BYTE-FOR-BYTE with the API's validateManifest table —
8
- // one set of rows, read by both the Go table test and this vitest suite. A row
9
- // present in one runner and absent in the other fails the parity assertion, or
10
- // the shared fixture degrades into two fixtures with one name (f3's QA-F3-G11
11
- // lesson).
12
- import { promises as fs } from "node:fs";
13
- import os from "node:os";
14
- import path from "node:path";
15
- /** The template defaults corpus the CLI ships, used verbatim by TEMPLATE. */
16
- export const TEMPLATE_TITLE = "Vite + React";
17
- export const TEMPLATE_DESCRIPTION = "Vite + React + TypeScript starter";
18
- function html(opts) {
19
- const parts = ["<!doctype html><html><head>"];
20
- if (opts.title !== undefined)
21
- parts.push(`<title>${opts.title}</title>`);
22
- if (opts.ogTitle !== undefined) {
23
- parts.push(`<meta property="og:title" content="${opts.ogTitle}">`);
24
- }
25
- if (opts.description !== undefined) {
26
- parts.push(`<meta name="description" content="${opts.description}">`);
27
- }
28
- if (opts.ogImage !== undefined) {
29
- parts.push(`<meta property="og:image" content="${opts.ogImage}">`);
30
- }
31
- parts.push("</head><body><div id=\"root\"></div></body></html>");
32
- return parts.join("");
33
- }
34
- /**
35
- * distFixture materializes one named tree under a fresh temp dir and returns
36
- * its cwd. The caller removes it.
37
- */
38
- export async function distFixture(name) {
39
- const cwd = await fs.mkdtemp(path.join(os.tmpdir(), `vct-f4-${name.toLowerCase()}-`));
40
- const dist = path.join(cwd, "dist");
41
- if (name === "NO_DIST") {
42
- // The ABSENCE of a dist/ directory IS the fixture.
43
- return cwd;
44
- }
45
- await fs.mkdir(dist, { recursive: true });
46
- switch (name) {
47
- case "CLEAN": {
48
- // A real title, description and og:image; og.png PRESENT; 6 files.
49
- await fs.writeFile(path.join(dist, "index.html"), html({
50
- title: "Harbour Wing — an AR walk",
51
- description: "A guided AR tour of the Harbour Wing collection.",
52
- ogImage: "/og.png",
53
- }));
54
- await fs.writeFile(path.join(dist, "og.png"), "png-bytes-clean");
55
- await fs.mkdir(path.join(dist, "assets"), { recursive: true });
56
- await fs.writeFile(path.join(dist, "assets", "app-a1b2c3.js"), "export const a=1;");
57
- await fs.writeFile(path.join(dist, "assets", "app-d4e5f6.css"), "body{margin:0}");
58
- await fs.writeFile(path.join(dist, "favicon.ico"), "ico-bytes");
59
- await fs.writeFile(path.join(dist, "model.glb"), "glb-bytes");
60
- return cwd;
61
- }
62
- case "TEMPLATE": {
63
- // The template's own strings, untouched.
64
- await fs.writeFile(path.join(dist, "index.html"), html({
65
- title: TEMPLATE_TITLE,
66
- description: TEMPLATE_DESCRIPTION,
67
- ogImage: "/og.png",
68
- }));
69
- await fs.writeFile(path.join(dist, "og.png"), "png-bytes-template");
70
- return cwd;
71
- }
72
- case "MISSING_META": {
73
- // A title ONLY — no description, no og:image.
74
- await fs.writeFile(path.join(dist, "index.html"), html({ title: "Harbour Wing — an AR walk" }));
75
- return cwd;
76
- }
77
- case "DEAD_IMAGE": {
78
- // og:image references /og.png, which is ABSENT from the tree. This is the
79
- // ONLY fixture that produces a ✗.
80
- await fs.writeFile(path.join(dist, "index.html"), html({
81
- title: "Harbour Wing — an AR walk",
82
- description: "A guided AR tour of the Harbour Wing collection.",
83
- ogImage: "/og.png",
84
- }));
85
- return cwd;
86
- }
87
- case "MULTI_HTML": {
88
- await fs.writeFile(path.join(dist, "index.html"), html({
89
- title: "Harbour Wing — an AR walk",
90
- description: "A guided AR tour of the Harbour Wing collection.",
91
- ogImage: "/og.png",
92
- }));
93
- await fs.writeFile(path.join(dist, "og.png"), "png-bytes-multi");
94
- // gallery.html has its OWN, DIFFERING findings.
95
- await fs.writeFile(path.join(dist, "gallery.html"), html({ title: TEMPLATE_TITLE }));
96
- return cwd;
97
- }
98
- case "ADVERSARIAL": {
99
- // Paths only, and NO index.html at the root. The tree cannot literally
100
- // contain "../escape.js" — the ROWS are the fixture, and they are shared
101
- // with the Go table test through adversarialRows() below.
102
- await fs.mkdir(path.join(dist, "sub"), { recursive: true });
103
- await fs.writeFile(path.join(dist, "sub", "index.html"), html({ title: "nested" }));
104
- return cwd;
105
- }
106
- case "OVERSIZE": {
107
- // The bound variants are driven by SYNTHETIC MANIFEST ROWS rather than
108
- // real bytes: writing 612 MB to a temp dir on every run would make the
109
- // suite unusable, and the bound is checked against declared sizes before
110
- // a byte moves — which is exactly the property under test.
111
- await fs.writeFile(path.join(dist, "index.html"), html({ title: "big" }));
112
- return cwd;
113
- }
114
- }
115
- return cwd;
116
- }
117
- export async function removeFixture(cwd) {
118
- await fs.rm(cwd, { recursive: true, force: true });
119
- }
120
- export function adversarialRows() {
121
- return [
122
- { name: "absolute", path: "/a.js", reason: "absolute_path" },
123
- { name: "traversal", path: "../a.js", reason: "path_traversal" },
124
- { name: "traversal_after_normalize", path: "a/../../b.js", reason: "path_traversal" },
125
- { name: "backslash", path: "a\\b.js", reason: "backslash" },
126
- { name: "empty_segment", path: "a//b.js", reason: "empty_segment" },
127
- { name: "over_1024_bytes", path: `${"a".repeat(1025)}.js`, reason: "path_too_long" },
128
- { name: "control_char", path: `a${String.fromCharCode(7)}b.js`, reason: "control_char" },
129
- { name: "percent", path: "a%20b.js", reason: "bad_encoding" },
130
- { name: "percent_2e2e", path: "a%2e%2e/b.js", reason: "bad_encoding" },
131
- { name: "reserved_platform", path: "_platform/x.js", reason: "reserved_path" },
132
- { name: "reserved_platform2", path: "_platform2/x.js", reason: "reserved_path" },
133
- { name: "reserved_leading_underscore", path: "_anything/x.js", reason: "reserved_path" },
134
- { name: "dot_segment", path: ".", reason: "path_traversal" },
135
- { name: "dotdot_segment", path: "..", reason: "path_traversal" },
136
- // THE ACCEPTED ROW. Without one the table passes on a validator that
137
- // rejects everything.
138
- { name: "nested_leading_underscore_is_ALLOWED", path: "a/_b.js", reason: "" },
139
- ];
140
- }
@@ -1,154 +0,0 @@
1
- import { type ChildProcessByStdio } from "node:child_process";
2
- import type { Readable } from "node:stream";
3
- import { AnnotationStore } from "./annotations.js";
4
- import { type RelayHandle } from "./server.js";
5
- /** src/relay → packages/cli. The built entry is the one a creator runs. */
6
- export declare const CLI_ENTRY: string;
7
- export interface F12Tree {
8
- dir: string;
9
- cleanup: () => Promise<void>;
10
- }
11
- /**
12
- * A mkdtemp project root with `.vincentt/`, a written relay.json, and a project
13
- * BINDING.
14
- *
15
- * The binding is what makes the tree a tree: `relayInfoDir` resolves through
16
- * `locateProjectBinding`, which walks up from the cwd looking for
17
- * `.vincentt/project.json`. Without it a subdirectory of the project is a
18
- * DIFFERENT tree with its own cursor at 0 — which is precisely the
19
- * re-deliver-everything bug feedback.ts's header comment describes, so a fixture
20
- * that omitted the binding could not express the tree-anchoring case at all.
21
- */
22
- export declare function F12_TREE(opts?: {
23
- relayPort: number;
24
- pid?: number;
25
- harnessPath?: string;
26
- }): Promise<F12Tree>;
27
- /** A fresh store over the tree, nothing on disk. readCursor ⇒ 0. */
28
- export declare function F12_STORE_EMPTY(projectDir: string): AnnotationStore;
29
- /** A valid 1×1 PNG data URL — the real screenshot shape `add()` decodes. */
30
- export declare const PNG_1X1: string;
31
- /**
32
- * F12_STORE_EMPTY plus three real `add()` calls ⇒ seq 1,2,3. `add()` is the same
33
- * function the real POST path calls, so driving it exercises the real delivery
34
- * path from one function inward.
35
- */
36
- export declare function F12_STORE_BATCH_3(projectDir: string, messages?: readonly string[]): Promise<AnnotationStore>;
37
- export interface F12Relay {
38
- handle: RelayHandle;
39
- port: number;
40
- /** Everything the relay wrote to its `onLog` seam. */
41
- logs: string[];
42
- /**
43
- * The `onAnnotation` notifications, one per accepted POST, carrying only the
44
- * resolved tester. This is the seam `preview/runner.ts` wires to the preview
45
- * terminal's `annotation received` line (Flow C8) — it is NOT `onLog`, so a
46
- * case that watched `logs` for arrivals would see none and wrongly conclude
47
- * the line was never wired.
48
- */
49
- arrivals: Array<{
50
- tester?: string;
51
- viewer?: string;
52
- }>;
53
- /** The rendered Flow C8 stream lines, as the preview terminal would print them. */
54
- streamLines: string[];
55
- close: () => Promise<void>;
56
- }
57
- /**
58
- * A real relay on a free port with an injectable store. Cases drive it through
59
- * REAL HTTP rather than by calling handlers directly: the exit contract is
60
- * defined in terms of status codes on a wire, and a handler-level call would not
61
- * observe a 410 the socket never sent.
62
- */
63
- export declare function F12_RELAY_LIVE(projectDir: string, store?: AnnotationStore): Promise<F12Relay>;
64
- /**
65
- * A tree whose relay.json names a port nothing listens on and a pid that is NOT
66
- * alive. The input to the liveness probe's dead arm.
67
- */
68
- export declare function F12_RELAY_DEAD(): Promise<F12Tree>;
69
- /** Write `.vincentt/annotations/offset` = n. n===null writes nothing (absent). */
70
- export declare function F12_CURSOR_AT(tree: string, n: number | null): Promise<void>;
71
- /** Raw cursor bytes, or null when the file is absent. */
72
- export declare function readCursorRaw(tree: string): Promise<string | null>;
73
- export interface HostileRow {
74
- row: string;
75
- payload: string;
76
- attacks: string;
77
- }
78
- export declare const F12_HOSTILE_MESSAGES: readonly HostileRow[];
79
- export interface F12TwoTrees {
80
- a: {
81
- tree: F12Tree;
82
- relay: F12Relay;
83
- store: AnnotationStore;
84
- };
85
- b: {
86
- tree: F12Tree;
87
- relay: F12Relay;
88
- store: AnnotationStore;
89
- };
90
- cleanup: () => Promise<void>;
91
- }
92
- /**
93
- * Two trees, two relays on two ports, two stores, two cursors. The
94
- * PROJECT-isolation fixture (QA-F12-G7). Deliberately NOT a tenant fixture —
95
- * these are one creator's two checkouts, the only multiplicity f12 has.
96
- */
97
- export declare function F12_TWO_TREES(): Promise<F12TwoTrees>;
98
- /** stdin ignored, stdout + stderr on their own pipes. */
99
- type PipedChild = ChildProcessByStdio<null, Readable, Readable>;
100
- export interface CliRun {
101
- child: PipedChild;
102
- /** Raw stdout BYTES — never merged with stderr. */
103
- stdout: () => Buffer;
104
- /** Raw stderr BYTES — never merged with stdout. */
105
- stderr: () => Buffer;
106
- out: () => string;
107
- err: () => string;
108
- /** Resolves with the OS-reported exit status. */
109
- exit: Promise<{
110
- code: number | null;
111
- signal: NodeJS.Signals | null;
112
- }>;
113
- exited: () => boolean;
114
- kill: (sig?: NodeJS.Signals) => void;
115
- }
116
- /**
117
- * Spawn the BUILT CLI with stdout/stderr on SEPARATE pipes and expose the real
118
- * exit status. `stdio: pipe` is load-bearing twice over: it keeps the two streams
119
- * distinguishable (QA-F12-C1) and it makes stdout a PIPE, which is the
120
- * asynchronous-write case the cursor invariant exists for (§3.1).
121
- */
122
- export declare function runCli(args: readonly string[], opts?: {
123
- cwd: string;
124
- env?: Record<string, string | undefined>;
125
- }): CliRun;
126
- /** Spawn, wait for exit, return everything. */
127
- export declare function runCliToEnd(args: readonly string[], opts: {
128
- cwd: string;
129
- env?: Record<string, string | undefined>;
130
- }): Promise<{
131
- code: number | null;
132
- signal: NodeJS.Signals | null;
133
- stdout: Buffer;
134
- stderr: Buffer;
135
- out: string;
136
- err: string;
137
- }>;
138
- /**
139
- * A throwaway HOME, so the dx journal (and therefore the dx nudge) starts empty.
140
- *
141
- * The nudge is journal-state-dependent and fires on the Nth recent run, so two
142
- * otherwise-identical invocations differ in stderr purely because one came later.
143
- * Any case comparing two runs byte-for-byte must isolate this or it is asserting
144
- * on run order rather than on the property it names.
145
- */
146
- export declare function freshHome(): Promise<string>;
147
- export declare function sleep(ms: number): Promise<void>;
148
- /** Wait until `cond()` or the deadline. Returns whether it became true. */
149
- export declare function until(cond: () => boolean, timeoutMs?: number, stepMs?: number): Promise<boolean>;
150
- /** Non-empty `\n`-split fragments of a raw stdout capture. */
151
- export declare function ndjsonLines(stdout: Buffer): string[];
152
- /** The Annotation key set §4.2 pins on stdout. */
153
- export declare const ANNOTATION_KEYS: readonly ["id", "seq", "createdAt", "projectDir", "message", "screenshotPath", "spec", "sessionId"];
154
- export {};