@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,150 @@
1
+ /**
2
+ * The one network read: `GET /platform/versions`.
3
+ *
4
+ * Public and unauthenticated, so no PAT is attached and no binding is consulted
5
+ * for anything but the host. `undefined` ON ANY FAULT — a refusal, a timeout, a
6
+ * 503 from a box with an empty corpus, a body of the wrong shape, a connection
7
+ * reset. Every one of them is "nothing to say", and the unbidden caller's
8
+ * silence on all of them must be byte-identical to the silence of a project with
9
+ * nothing to report. `preview` must not acquire a new way to look broken.
10
+ *
11
+ * The 2s budget is f7's `FETCH_TIMEOUT_MS`, reused rather than re-chosen. A
12
+ * second timeout constant is a second thing to tune and the one nobody finds.
13
+ */
14
+ import http from "node:http";
15
+ import https from "node:https";
16
+ import { CLIENT_HEADER, clientHeaderValue } from "../shared/clientHeader.js";
17
+ import { FETCH_TIMEOUT_MS } from "../dx/updateCheck.js";
18
+ import { readPlatformRelease } from "./releaseCache.js";
19
+ import { platformCheckEnabled } from "./offSwitch.js";
20
+ export const VERSIONS_PATH = "/platform/versions";
21
+ /**
22
+ * Fetch the current set. `undefined` means nothing to say, including on a `304`
23
+ * — the caller already holds that answer in its cache and a 304 is not new
24
+ * information.
25
+ *
26
+ * OFF-SWITCH SITE (the refresh): with the check off no request is made at all.
27
+ */
28
+ export async function fetchRelease(opts) {
29
+ if (!platformCheckEnabled())
30
+ return undefined;
31
+ const fetchImpl = opts.fetchImpl ?? fetch;
32
+ const controller = new AbortController();
33
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
34
+ timer.unref?.();
35
+ const onAbort = () => controller.abort();
36
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
37
+ try {
38
+ const res = await fetchImpl(`${opts.apiUrl.replace(/\/$/, "")}${VERSIONS_PATH}`, {
39
+ signal: controller.signal,
40
+ headers: {
41
+ // The client tag rides along because the existing middleware LOGS it,
42
+ // and logging is not branching: the handler reads no header but
43
+ // `If-None-Match`, so this cannot change the answer. Omitting it would
44
+ // make this one route's requests distinguishable from every other
45
+ // request the CLI makes, which is the opposite of what is wanted.
46
+ [CLIENT_HEADER]: clientHeaderValue(),
47
+ ...(opts.etag ? { "if-none-match": opts.etag } : {}),
48
+ },
49
+ });
50
+ if (!res.ok)
51
+ return undefined;
52
+ const release = readPlatformRelease(await res.json());
53
+ if (release === undefined)
54
+ return undefined;
55
+ const etag = res.headers.get("etag") ?? undefined;
56
+ return etag ? { release, etag } : { release };
57
+ }
58
+ catch {
59
+ return undefined;
60
+ }
61
+ finally {
62
+ clearTimeout(timer);
63
+ opts.signal?.removeEventListener("abort", onAbort);
64
+ }
65
+ }
66
+ /**
67
+ * The same read, on an UNREF'D SOCKET, for the unbidden path only.
68
+ *
69
+ * WHY THIS EXISTS AND IS NOT `fetch`. The design's performance guarantee is
70
+ * structural: `preview`'s time-to-URL must not move, and the mechanism named for
71
+ * it is an `unref`'d refresh. Measured, `unref`'ing the TIMERS is not sufficient
72
+ * — a pending `fetch` keeps its SOCKET as an active handle, and the platform
73
+ * exposes no way to unref it, so a verb run against an endpoint that accepts the
74
+ * connection and never answers waits the full budget before exiting. Against a
75
+ * healthy endpoint nobody notices; against a hung one every command appears to
76
+ * hang, and the delay lands on `preview`, where someone is waiting to open a URL
77
+ * on a phone. That is the exact failure the guarantee was written to exclude.
78
+ *
79
+ * `node:http` with `socket.unref()` exits immediately (measured: 2ms against the
80
+ * same hung endpoint), so the unbidden path uses it and the guarantee holds as a
81
+ * property rather than as an aspiration. `fetchRelease` above keeps `fetch` and
82
+ * its injectable seam for `outdated`, which was ASKED for and may legitimately
83
+ * wait.
84
+ *
85
+ * Best-effort throughout, and `undefined` on every fault — including a body that
86
+ * arrives after the process has already exited, which is the normal case here
87
+ * and is why this writes through a callback rather than returning a promise the
88
+ * caller could await.
89
+ */
90
+ export function fetchReleaseDetached(opts, onResult) {
91
+ if (!platformCheckEnabled())
92
+ return;
93
+ let url;
94
+ try {
95
+ url = new URL(`${opts.apiUrl.replace(/\/$/, "")}${VERSIONS_PATH}`);
96
+ }
97
+ catch {
98
+ return;
99
+ }
100
+ const transport = url.protocol === "https:" ? https : http;
101
+ try {
102
+ const req = transport.request({
103
+ protocol: url.protocol,
104
+ hostname: url.hostname,
105
+ port: url.port,
106
+ path: `${url.pathname}${url.search}`,
107
+ method: "GET",
108
+ headers: {
109
+ [CLIENT_HEADER]: clientHeaderValue(),
110
+ ...(opts.etag ? { "if-none-match": opts.etag } : {}),
111
+ },
112
+ }, (res) => {
113
+ if (res.statusCode !== 200) {
114
+ res.resume();
115
+ return;
116
+ }
117
+ let body = "";
118
+ res.setEncoding("utf8");
119
+ res.on("data", (chunk) => {
120
+ body += chunk;
121
+ // Bounded: this is parsed on a creator's machine off a public route,
122
+ // and an endless body must not grow without limit.
123
+ if (body.length > 256_000)
124
+ req.destroy();
125
+ });
126
+ res.on("end", () => {
127
+ try {
128
+ const release = readPlatformRelease(JSON.parse(body));
129
+ if (release === undefined)
130
+ return;
131
+ const etag = res.headers.etag;
132
+ onResult(typeof etag === "string" ? { release, etag } : { release });
133
+ }
134
+ catch {
135
+ // A malformed body is nothing to say.
136
+ }
137
+ });
138
+ res.on("error", () => { });
139
+ });
140
+ // THE LOAD-BEARING CALL. Without it a pending request is an active handle
141
+ // and the process waits on it.
142
+ req.on("socket", (socket) => socket.unref());
143
+ req.on("error", () => { });
144
+ req.setTimeout(FETCH_TIMEOUT_MS, () => req.destroy());
145
+ req.end();
146
+ }
147
+ catch {
148
+ // A refresh that cannot even be started is nothing to say.
149
+ }
150
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The wire shape. `packages` keys are package identities; `template` is the
3
+ * recorded baseline, and ABSENT means unknown.
4
+ *
5
+ * The field names and casing are the contract — the same name and shape on the
6
+ * publish route and the report route.
7
+ */
8
+ export interface BuiltWith {
9
+ packages: Record<string, string>;
10
+ template?: string;
11
+ }
12
+ export interface ResolveBuiltWithOptions {
13
+ /**
14
+ * The recorded template baseline, injected so the resolver does not have to
15
+ * decide where a binding lives. Omitted → read from the project binding.
16
+ */
17
+ templateVersion?: string;
18
+ /** Test seam: skip the binding read entirely. */
19
+ readBinding?: boolean;
20
+ }
21
+ /**
22
+ * Resolve the version set for `projectDir`, or `undefined` when there is
23
+ * nothing to say.
24
+ *
25
+ * ITERATES THE ALLOWLIST — it never enumerates the creator's dependencies and
26
+ * filters. Only allowlisted paths are opened, so a third-party name is never
27
+ * read into memory in the first place. This is `B-F17-1` as control flow, and it
28
+ * is why the creator-facing disclosure can say "and nothing else" truthfully.
29
+ */
30
+ export declare function resolveBuiltWith(projectDir: string, opts?: ResolveBuiltWithOptions): Promise<BuiltWith | undefined>;
@@ -0,0 +1,128 @@
1
+ /**
2
+ * `builtWith` — what this project resolved on THIS machine, right now.
3
+ *
4
+ * ONE resolver, two callers (the publish body and the dx report body). That is
5
+ * not tidiness: the two payloads carrying byte-identical values for the same
6
+ * project is the mechanical proof there is one resolver and not two that drifted
7
+ * — a second resolver is how one surface starts reporting declared ranges while
8
+ * the other reports installed versions, and neither looks wrong alone.
9
+ *
10
+ * INSTALLED, NEVER DECLARED. The version read is the one in
11
+ * `node_modules/<name>/package.json`, because `^1.2.0` cannot answer "did the
12
+ * fix reach anyone" and `1.4.0` can. A creator whose lockfile resolved 1.4.0
13
+ * under a `^1.2.0` declaration HAS the fix, and recording the range would report
14
+ * them behind while current. (The UPGRADE edits the declared range, because that
15
+ * is what an install resolves from — two different reads of two different
16
+ * things, and a dev unifying them breaks one of the two.)
17
+ *
18
+ * ABSENT vs EMPTY are different facts and both are first-class. `undefined`
19
+ * means resolution did not run or found nothing to say; an object with an empty
20
+ * `packages` map means it ran and found none installed. Nothing is ever
21
+ * backfilled and there is no `"unknown"` sentinel — a valid-looking version
22
+ * string is how a surface ends up rendering `sdk@unknown` as a version and how a
23
+ * comparison ends up running against it.
24
+ */
25
+ import { promises as fs } from "node:fs";
26
+ import path from "node:path";
27
+ import { PLATFORM_PACKAGES } from "./allowlist.js";
28
+ import { loadProjectBinding } from "../shared/config.js";
29
+ /** §9's bounds. Over-bound entries are DROPPED, never an error. */
30
+ const MAX_PACKAGES = 16;
31
+ const MAX_NAME_BYTES = 128;
32
+ const MAX_VERSION_BYTES = 64;
33
+ /**
34
+ * The character class a version string must satisfy WHOLE, matching the api's
35
+ * own `dxCLIVersionRe`. It admits every registry semver — `2.0.0-alpha.3+build.1`,
36
+ * `1.0.0-rc.1+meta.20260908`, `1.0.0-rc-16` — and nothing that can carry a
37
+ * sentence.
38
+ *
39
+ * Printable ASCII was not enough, and the gap was demonstrated rather than
40
+ * argued: control bytes were already dropped, so ANSI and OSC-8 were closed, but
41
+ * punctuation-only injection was wide open. A `version` field in a
42
+ * `node_modules/*​/package.json` is writable by any postinstall script or
43
+ * transitive dependency, this string lands INSIDE the payload block on a consent
44
+ * screen, the CLI's stdout is a coding agent's input, and the sentence below it
45
+ * asserts "and nothing else" — so the attacker's text lands where an agent parses
46
+ * loosely with the platform's own copy vouching for it.
47
+ */
48
+ const VERSION_CLASS = /^[0-9A-Za-z.\-+]+$/;
49
+ /**
50
+ * A version string, or `undefined` when there is nothing safe to say.
51
+ *
52
+ * THE CLASS CHECK RUNS FIRST, on the value AS RECEIVED, and an out-of-class
53
+ * value is dropped WHOLE. Order is the decision: applied after truncation it
54
+ * would store a PREFIX of a hostile value, and `1.4` standing in for `1.4.7` is
55
+ * a wrong fact that looks entirely plausible — a wrong answer to "did the fix
56
+ * reach anyone", and worse than absence. Truncation therefore only ever sees a
57
+ * value already known to be in class, which also makes mid-rune truncation moot.
58
+ *
59
+ * DROPPED, NEVER REFUSED. An out-of-class value omits the entry; the send still
60
+ * succeeds. `file:` and `git+ssh://` installs fall out of class and become
61
+ * ABSENT, which is a first-class state every surface renders honestly, and it is
62
+ * the accepted cost of closing an injection channel on a consent surface.
63
+ */
64
+ function sanitizeVersion(raw) {
65
+ if (!VERSION_CLASS.test(raw))
66
+ return undefined;
67
+ if (Buffer.byteLength(raw, "utf8") > MAX_VERSION_BYTES)
68
+ return undefined;
69
+ return raw;
70
+ }
71
+ /**
72
+ * Read one allowlisted package's installed version.
73
+ *
74
+ * A missing package, an unreadable manifest, a manifest with no `version` and a
75
+ * manifest that is not JSON are all the same answer: this package contributes
76
+ * nothing. None of them is an error, because a project legitimately does not
77
+ * install every platform package.
78
+ */
79
+ async function installedVersion(projectDir, name) {
80
+ try {
81
+ const manifestPath = path.join(projectDir, "node_modules", ...name.split("/"), "package.json");
82
+ const raw = JSON.parse(await fs.readFile(manifestPath, "utf8"));
83
+ if (typeof raw.version !== "string")
84
+ return undefined;
85
+ return sanitizeVersion(raw.version);
86
+ }
87
+ catch {
88
+ return undefined;
89
+ }
90
+ }
91
+ /**
92
+ * Resolve the version set for `projectDir`, or `undefined` when there is
93
+ * nothing to say.
94
+ *
95
+ * ITERATES THE ALLOWLIST — it never enumerates the creator's dependencies and
96
+ * filters. Only allowlisted paths are opened, so a third-party name is never
97
+ * read into memory in the first place. This is `B-F17-1` as control flow, and it
98
+ * is why the creator-facing disclosure can say "and nothing else" truthfully.
99
+ */
100
+ export async function resolveBuiltWith(projectDir, opts = {}) {
101
+ const packages = {};
102
+ for (const pkg of PLATFORM_PACKAGES) {
103
+ if (Object.keys(packages).length >= MAX_PACKAGES)
104
+ break;
105
+ if (Buffer.byteLength(pkg.name, "utf8") > MAX_NAME_BYTES)
106
+ continue;
107
+ const version = await installedVersion(projectDir, pkg.name);
108
+ if (version !== undefined)
109
+ packages[pkg.name] = version;
110
+ }
111
+ let template = opts.templateVersion;
112
+ if (template === undefined && opts.readBinding !== false) {
113
+ try {
114
+ template = (await loadProjectBinding(projectDir))?.template?.version;
115
+ }
116
+ catch {
117
+ template = undefined;
118
+ }
119
+ }
120
+ const cleanTemplate = template === undefined ? undefined : sanitizeVersion(template);
121
+ // Nothing resolved at all → undefined, so the caller OMITS the field rather
122
+ // than sending `{}`. A `{}` says "resolution ran and found nothing", which is
123
+ // a different and specific fact, and a surface that renders the two the same
124
+ // way collapses two of the three states this design exists to distinguish.
125
+ if (Object.keys(packages).length === 0 && cleanTemplate === undefined)
126
+ return undefined;
127
+ return cleanTemplate === undefined ? { packages } : { packages, template: cleanTemplate };
128
+ }
@@ -0,0 +1,56 @@
1
+ import { type DependencyChange, type TrackedState } from "./upgradeReport.js";
2
+ /** The manifest file, read-only: it arrives with the TO tree and is never in its own overwrite set. */
3
+ export declare const MANIFEST_FILE = ".vincentt-template.json";
4
+ /** The first template version carrying a manifest, for the blocked report's copy. */
5
+ export declare const FIRST_MANIFEST_VERSION = "1.5.0";
6
+ export type GitRunner = (args: string[]) => Promise<string | void>;
7
+ /**
8
+ * Validate a recorded FROM ref BEFORE it reaches git.
9
+ *
10
+ * Anything not exactly `MAJOR.MINOR.PATCH` is NO BASELINE — not an error, and
11
+ * not a bare attempt. `../../etc`, `refs/heads/1.4.3`, `1.4.3; rm -rf` and
12
+ * `latest` all resolve to "no baseline" without git being invoked at all.
13
+ */
14
+ export declare function validateFromRef(raw: string | undefined): string | undefined;
15
+ export interface UpgradeDeps {
16
+ projectDir: string;
17
+ /** The TO version — a concrete tag. */
18
+ to: string;
19
+ /**
20
+ * What the dependency half reported. The template half is attempted only when
21
+ * every platform package the project has is accounted for here.
22
+ */
23
+ dependencyOutcome: DependencyOutcome;
24
+ runGit?: GitRunner;
25
+ /** Test seam: substitute the clone with a local directory per ref. */
26
+ fetchTag?: (ref: string) => Promise<string | undefined>;
27
+ /** Test seam for tracked-ness, so the git plumbing is injectable. */
28
+ trackedStateOf?: (projectDir: string, rel: string) => Promise<TrackedState>;
29
+ }
30
+ export interface DependencyOutcome {
31
+ /** Packages the dependency half completed. */
32
+ completed: DependencyChange[];
33
+ /**
34
+ * Packages it did NOT complete. Non-empty means the template half is not
35
+ * attempted — the ordering precondition, expressed as data the report reads
36
+ * rather than as an exit code the procedure may not read.
37
+ */
38
+ outstanding: string[];
39
+ }
40
+ export interface UpgradeOutcome {
41
+ /** The rendered report. The procedure reads THIS, never an exit code. */
42
+ report: string[];
43
+ /** Whether the template half wrote any file. */
44
+ templateAttempted: boolean;
45
+ /** The baseline recorded, if any. Written LAST. */
46
+ baselineWritten?: string;
47
+ }
48
+ /**
49
+ * Run the template half.
50
+ *
51
+ * Every early return renders a report and writes nothing. There is no path
52
+ * through this function that overwrites a file without the report naming it.
53
+ */
54
+ export declare function upgradeTemplate(deps: UpgradeDeps): Promise<UpgradeOutcome>;
55
+ /** Which platform packages this project has installed, for the ordering precondition. */
56
+ export declare function installedPlatformPackages(projectDir: string): Promise<string[]>;