@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.
- package/dist/cli/args.js +1 -0
- package/dist/cli/index.js +198 -3
- package/dist/cli/usage-text.js +27 -0
- package/dist/cli/verb-surface.d.ts +3 -1
- package/dist/cli/verb-surface.js +12 -0
- package/dist/domain/api.d.ts +51 -0
- package/dist/domain/api.js +145 -0
- package/dist/domain/command.d.ts +75 -0
- package/dist/domain/command.js +467 -0
- package/dist/domain/copy.d.ts +334 -0
- package/dist/domain/copy.js +952 -0
- package/dist/domain/hostname.d.ts +57 -0
- package/dist/domain/hostname.js +116 -0
- package/dist/domain/names.d.ts +78 -0
- package/dist/domain/names.js +113 -0
- package/dist/domain/routedVerbs.d.ts +10 -0
- package/dist/domain/routedVerbs.js +30 -0
- package/dist/domain/wire.d.ts +140 -0
- package/dist/domain/wire.js +174 -0
- package/dist/dx/command.d.ts +13 -4
- package/dist/dx/command.js +26 -5
- package/dist/dx/report.d.ts +39 -7
- package/dist/dx/report.js +71 -20
- package/dist/dx/send.d.ts +22 -4
- package/dist/dx/send.js +29 -8
- package/dist/dx/skeleton.js +19 -0
- package/dist/dx/unbiddenVerbs.d.ts +25 -0
- package/dist/dx/unbiddenVerbs.js +39 -0
- package/dist/lib/initCommand.d.ts +3 -1
- package/dist/lib/initCommand.js +10 -0
- package/dist/lib/projects.d.ts +8 -1
- package/dist/lib/projects.js +12 -2
- package/dist/platform/allowlist.d.ts +36 -0
- package/dist/platform/allowlist.js +34 -0
- package/dist/platform/behind.d.ts +41 -0
- package/dist/platform/behind.js +82 -0
- package/dist/platform/command.d.ts +16 -0
- package/dist/platform/command.js +90 -0
- package/dist/platform/footer.d.ts +60 -0
- package/dist/platform/footer.js +101 -0
- package/dist/platform/notice.d.ts +29 -0
- package/dist/platform/notice.js +66 -0
- package/dist/platform/offSwitch.d.ts +19 -0
- package/dist/platform/offSwitch.js +21 -0
- package/dist/platform/outdated.d.ts +25 -0
- package/dist/platform/outdated.js +246 -0
- package/dist/platform/releaseCache.d.ts +69 -0
- package/dist/platform/releaseCache.js +140 -0
- package/dist/platform/releaseFetch.d.ts +51 -0
- package/dist/platform/releaseFetch.js +150 -0
- package/dist/platform/resolve.d.ts +30 -0
- package/dist/platform/resolve.js +128 -0
- package/dist/platform/templateUpgrade.d.ts +56 -0
- package/dist/platform/templateUpgrade.js +476 -0
- package/dist/platform/upgradeReport.d.ts +132 -0
- package/dist/platform/upgradeReport.js +258 -0
- package/dist/platform/versionSummary.d.ts +82 -0
- package/dist/platform/versionSummary.js +181 -0
- package/dist/platform/zonePath.d.ts +154 -0
- package/dist/platform/zonePath.js +286 -0
- package/dist/platform/zones.d.ts +64 -0
- package/dist/platform/zones.js +79 -0
- package/dist/preview/api.d.ts +19 -0
- package/dist/preview/api.js +29 -0
- package/dist/publish/api.d.ts +32 -5
- package/dist/publish/api.js +19 -5
- package/dist/publish/command.d.ts +10 -0
- package/dist/publish/command.js +30 -2
- package/dist/publish/copy.d.ts +14 -1
- package/dist/publish/copy.js +32 -3
- package/dist/scaffold/index.d.ts +50 -3
- package/dist/scaffold/index.js +61 -1
- package/dist/shared/config.d.ts +16 -0
- package/dist/shared/config.js +9 -1
- package/dist/shared/countLimit.d.ts +109 -0
- package/dist/shared/countLimit.js +169 -0
- package/package.json +1 -1
- package/dist/dx/qa.f7.fixtures.d.ts +0 -73
- package/dist/dx/qa.f7.fixtures.js +0 -58
- package/dist/publish/qa.f4.fixtures.d.ts +0 -25
- package/dist/publish/qa.f4.fixtures.js +0 -140
- package/dist/relay/qa.f12.fixtures.d.ts +0 -154
- 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[]>;
|