@vincentt-xr/cli 0.18.1 → 0.18.2
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/MIGRATION.md +9 -0
- package/dist/lib/initCommand.js +3 -1
- package/dist/lib/initFrom.js +1 -1
- package/dist/lib/lifecycle.d.ts +3 -0
- package/dist/lib/lifecycle.js +20 -1
- package/dist/lib/processStartTime.d.ts +12 -0
- package/dist/lib/processStartTime.js +33 -0
- package/dist/lib/relayInfo.d.ts +7 -0
- package/dist/platform/releaseCache.d.ts +7 -0
- package/dist/platform/releaseCache.js +4 -1
- package/dist/preview/copy.js +6 -6
- package/dist/scaffold/index.d.ts +11 -0
- package/dist/scaffold/index.js +35 -3
- package/dist/scaffold/releasedCommit.d.ts +37 -0
- package/dist/scaffold/releasedCommit.js +71 -0
- package/package.json +1 -1
package/MIGRATION.md
CHANGED
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
Per version, newest first. Read by the platform's own version surface and served to
|
|
4
4
|
creators' agents. Accurate or absent, never speculative (B-F5-2).
|
|
5
5
|
|
|
6
|
+
## 0.18.2
|
|
7
|
+
|
|
8
|
+
required: false
|
|
9
|
+
No action required. Behaviour that changed:
|
|
10
|
+
- `init` and `create` read the public release list once and clone the template version it names, then check the cloned commit against the platform's record. A commit that does not match exits 1 and writes nothing. When the platform cannot be reached, or has no commit on record, they clone the `latest` tag as before and print one warning on stderr. Plain `init` still needs no account.
|
|
11
|
+
- `vincentt create` in a folder that already holds an app, with no account and no terminal, now exits 4 (not signed in) with the `vincentt login` remedy, where it exited 1. Every not-signed-in path now exits 4.
|
|
12
|
+
- The six preview ending lines no longer use em-dashes: each reads `✓ Preview stopped.` or `✗ Preview ended.` followed by its cause, for example `✗ Preview ended. It reached the 5 GB ceiling.` A script matching the old wording exactly needs updating.
|
|
13
|
+
- `preview --stop` no longer signals a process that merely reuses the preview's old process id: the preview records its start time, and when the process now holding that id started at a different time, nothing is signalled and the command exits 1 as "nothing stopped", clearing the stale record. A preview started by an older CLI, or on Windows, has no start time on record and stops as before.
|
|
14
|
+
|
|
6
15
|
## 0.18.1
|
|
7
16
|
|
|
8
17
|
required: false
|
package/dist/lib/initCommand.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// `vincentt init [dir]` — scaffold a Vincentt XR app. On the BARE path: NO
|
|
2
|
-
// account, NO
|
|
2
|
+
// account, NO binding written, and no API call beyond the scaffold's anonymous
|
|
3
|
+
// read of the public release list (to verify the template commit; its failure
|
|
4
|
+
// only warns).
|
|
3
5
|
//
|
|
4
6
|
// This is the acquisition wedge: local-first is a smaller room, not a wall, so the
|
|
5
7
|
// template must be reachable without ever touching the auth gate. `create` (which
|
package/dist/lib/initFrom.js
CHANGED
|
@@ -93,7 +93,7 @@ export async function registerAfterScaffold(deps, opts = {}) {
|
|
|
93
93
|
}
|
|
94
94
|
deps.print("");
|
|
95
95
|
deps.print(closing);
|
|
96
|
-
return scaffolded ? EXIT.DELIVERED_NOT_REGISTERED : EXIT.
|
|
96
|
+
return scaffolded ? EXIT.DELIVERED_NOT_REGISTERED : EXIT.NOT_AUTHENTICATED;
|
|
97
97
|
}
|
|
98
98
|
deps.print("");
|
|
99
99
|
deps.print("→ Now registering the project so it gets an address.");
|
package/dist/lib/lifecycle.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { OrgSource } from "./initFrom.js";
|
|
|
2
2
|
import type { Ask } from "../publish/prompt.js";
|
|
3
3
|
import { type ResolvedOrg } from "./org.js";
|
|
4
4
|
import { type RelayClient } from "./diagnostics.js";
|
|
5
|
+
import { type ProcessStartTimeReader } from "./processStartTime.js";
|
|
5
6
|
import { type ResolvedConfig } from "../shared/config.js";
|
|
6
7
|
import { type RunningPreview, type StartPreviewOptions } from "../preview/index.js";
|
|
7
8
|
import type { DiagEventKind, RelayResult } from "@vincentt-xr/harness/events";
|
|
@@ -125,6 +126,8 @@ export interface PreviewStopDeps {
|
|
|
125
126
|
relay?: RelayClient;
|
|
126
127
|
/** Injected so the SIGTERM is observable without signalling a real process. */
|
|
127
128
|
kill?: (pid: number, signal: NodeJS.Signals) => void;
|
|
129
|
+
/** Injected so a recycled pid can be simulated without a real process. */
|
|
130
|
+
processStartTime?: ProcessStartTimeReader;
|
|
128
131
|
}
|
|
129
132
|
/**
|
|
130
133
|
* Stop a preview started by a *different* process (the common case: `vincentt
|
package/dist/lib/lifecycle.js
CHANGED
|
@@ -10,6 +10,7 @@ import { publishCommand } from "../publish/command.js";
|
|
|
10
10
|
import { activeOrgSlug, resolveActiveOrg } from "./org.js";
|
|
11
11
|
import { filterErrors, httpRelayClient, renderResult, } from "./diagnostics.js";
|
|
12
12
|
import { clearRelayInfo, readLivePreview, readRelayInfo, writeRelayInfo, } from "./relayInfo.js";
|
|
13
|
+
import { readProcessStartTime } from "./processStartTime.js";
|
|
13
14
|
import { loadMachineConfig, locateProjectBinding, resolveConfig, } from "../shared/config.js";
|
|
14
15
|
import { startPreview, } from "../preview/index.js";
|
|
15
16
|
// The SHIPPED stopping line, reused rather than reworded: the interrupt path and
|
|
@@ -173,6 +174,7 @@ export async function previewStart(projectCwd, onLog, extra = {}) {
|
|
|
173
174
|
pid: process.pid,
|
|
174
175
|
harnessPath: "/__harness",
|
|
175
176
|
expiresAt: preview.expiresAt,
|
|
177
|
+
pidStartTime: await readProcessStartTime(process.pid),
|
|
176
178
|
});
|
|
177
179
|
return { preview, started: true };
|
|
178
180
|
}
|
|
@@ -199,6 +201,19 @@ function isAlive(pid) {
|
|
|
199
201
|
}
|
|
200
202
|
/** The bound the caller is held to: one query, no retry, isRelayResponding's window. */
|
|
201
203
|
const PRESENCE_TIMEOUT_MS = 700;
|
|
204
|
+
/**
|
|
205
|
+
* True only when the marker recorded a start time and the live process holding
|
|
206
|
+
* that pid PROVABLY started at a different time. A marker from an older CLI, or
|
|
207
|
+
* a start time that cannot be read, proves nothing, so the stop goes ahead as it
|
|
208
|
+
* always did. The relay is deliberately not consulted: QA-F14-06 requires the
|
|
209
|
+
* stop to happen whatever the relay answers.
|
|
210
|
+
*/
|
|
211
|
+
async function isRecycledPid(info, read) {
|
|
212
|
+
if (info.pidStartTime === undefined)
|
|
213
|
+
return false;
|
|
214
|
+
const live = await read(info.pid);
|
|
215
|
+
return live !== undefined && live !== info.pidStartTime;
|
|
216
|
+
}
|
|
202
217
|
/**
|
|
203
218
|
* How many connections are attached, or UNDEFINED when that cannot be
|
|
204
219
|
* established — and undefined is a first-class answer here, not an error.
|
|
@@ -259,7 +274,11 @@ export async function previewStopSignal(projectCwd, deps = {}) {
|
|
|
259
274
|
if (!info) {
|
|
260
275
|
return { stopped: false, message: "No preview is running for this directory." };
|
|
261
276
|
}
|
|
262
|
-
|
|
277
|
+
// A recycled pid is treated exactly like a dead one: nothing is signalled, the
|
|
278
|
+
// relay is not asked, and the stale marker is cleared.
|
|
279
|
+
const previewAlive = info.pid !== undefined &&
|
|
280
|
+
isAlive(info.pid) &&
|
|
281
|
+
!(await isRecycledPid(info, deps.processStartTime ?? readProcessStartTime));
|
|
263
282
|
if (previewAlive) {
|
|
264
283
|
// PRESENCE IS READ BEFORE THE SIGNAL, and the order is the feature. The
|
|
265
284
|
// creator is about to blank someone's screen, so the count has to describe
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type ProcessStartTimeReader = (pid: number) => Promise<string | undefined>;
|
|
2
|
+
/**
|
|
3
|
+
* macOS and Linux both answer `ps -o lstart= -p <pid>` (BSD ps and procps), run
|
|
4
|
+
* under the C locale so the text does not vary with the creator's language.
|
|
5
|
+
* One-second resolution is enough: the process that held the pid had to exit
|
|
6
|
+
* before it could be reused. Windows returns undefined: no marker then carries
|
|
7
|
+
* a start time, and `preview --stop` behaves as it did before the check.
|
|
8
|
+
*
|
|
9
|
+
* Undefined also means "could not read" (no `ps`, a ps without `lstart`), never
|
|
10
|
+
* "different".
|
|
11
|
+
*/
|
|
12
|
+
export declare function readProcessStartTime(pid: number, platform?: NodeJS.Platform): Promise<string | undefined>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// A process's start time, read as an opaque token that is only ever compared for
|
|
2
|
+
// equality. Together with the pid it identifies ONE process: a recycled pid has
|
|
3
|
+
// the same number and a different start time.
|
|
4
|
+
import { execFile } from "node:child_process";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
const execFileP = promisify(execFile);
|
|
7
|
+
/**
|
|
8
|
+
* macOS and Linux both answer `ps -o lstart= -p <pid>` (BSD ps and procps), run
|
|
9
|
+
* under the C locale so the text does not vary with the creator's language.
|
|
10
|
+
* One-second resolution is enough: the process that held the pid had to exit
|
|
11
|
+
* before it could be reused. Windows returns undefined: no marker then carries
|
|
12
|
+
* a start time, and `preview --stop` behaves as it did before the check.
|
|
13
|
+
*
|
|
14
|
+
* Undefined also means "could not read" (no `ps`, a ps without `lstart`), never
|
|
15
|
+
* "different".
|
|
16
|
+
*/
|
|
17
|
+
export async function readProcessStartTime(pid, platform = process.platform) {
|
|
18
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
19
|
+
return undefined;
|
|
20
|
+
if (platform === "win32")
|
|
21
|
+
return undefined;
|
|
22
|
+
try {
|
|
23
|
+
const { stdout } = await execFileP("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
24
|
+
env: { ...process.env, LC_ALL: "C" },
|
|
25
|
+
timeout: 2000,
|
|
26
|
+
});
|
|
27
|
+
const lstart = stdout.trim().replace(/\s+/g, " ");
|
|
28
|
+
return lstart === "" ? undefined : lstart;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
package/dist/lib/relayInfo.d.ts
CHANGED
|
@@ -17,6 +17,13 @@ export interface RelayInfo {
|
|
|
17
17
|
* only assumes is live. Optional: markers written by an older CLI lack it.
|
|
18
18
|
*/
|
|
19
19
|
expiresAt?: string;
|
|
20
|
+
/**
|
|
21
|
+
* The preview process's start time (an opaque token from
|
|
22
|
+
* `readProcessStartTime`), so `preview --stop` can tell the preview from an
|
|
23
|
+
* unrelated process that was later given the same pid. Absent from markers
|
|
24
|
+
* written by an older CLI or on a platform with no reader.
|
|
25
|
+
*/
|
|
26
|
+
pidStartTime?: string;
|
|
20
27
|
}
|
|
21
28
|
export declare function relayInfoPath(cwd: string): string;
|
|
22
29
|
/**
|
|
@@ -8,6 +8,13 @@ export interface ReleaseComponent {
|
|
|
8
8
|
* cannot tell blank from forgotten, so the platform must say which one it is.
|
|
9
9
|
*/
|
|
10
10
|
migration?: ReleaseMigration;
|
|
11
|
+
/**
|
|
12
|
+
* The released commit (40 lowercase hex). ABSENT for rows released before the
|
|
13
|
+
* platform recorded it, and for a value of any other shape: the scaffold
|
|
14
|
+
* verifies a clone against it, so a malformed value must read as "nothing to
|
|
15
|
+
* verify", never as a value to compare.
|
|
16
|
+
*/
|
|
17
|
+
commit?: string;
|
|
11
18
|
}
|
|
12
19
|
export interface ReleaseMigration {
|
|
13
20
|
required: boolean;
|
|
@@ -19,6 +19,7 @@ import path from "node:path";
|
|
|
19
19
|
import { CHECK_TTL_MS } from "../dx/updateCheck.js";
|
|
20
20
|
import { platformCheckEnabled } from "./offSwitch.js";
|
|
21
21
|
const CACHE_FILE = "platform-release.json";
|
|
22
|
+
const COMMIT_SHA = /^[0-9a-f]{40}$/;
|
|
22
23
|
/**
|
|
23
24
|
* Freshness, with a clock that moved backwards treated as STALE rather than as
|
|
24
25
|
* fresh forever. Same rule and same TTL as the CLI update check — reused rather
|
|
@@ -75,13 +76,15 @@ export function readPlatformRelease(raw) {
|
|
|
75
76
|
for (const [name, value] of Object.entries(components)) {
|
|
76
77
|
if (typeof value !== "object" || value === null)
|
|
77
78
|
continue;
|
|
78
|
-
const { version, migration } = value;
|
|
79
|
+
const { version, migration, commit } = value;
|
|
79
80
|
if (typeof version !== "string" || version === "")
|
|
80
81
|
continue;
|
|
81
82
|
const component = { version };
|
|
82
83
|
const note = readMigration(migration);
|
|
83
84
|
if (note !== undefined)
|
|
84
85
|
component.migration = note;
|
|
86
|
+
if (typeof commit === "string" && COMMIT_SHA.test(commit))
|
|
87
|
+
component.commit = commit;
|
|
85
88
|
out[name] = component;
|
|
86
89
|
}
|
|
87
90
|
const parsed = { release, components: out };
|
package/dist/preview/copy.js
CHANGED
|
@@ -331,7 +331,7 @@ export function stoppedElsewhere() {
|
|
|
331
331
|
export function endedDisconnected() {
|
|
332
332
|
return [
|
|
333
333
|
"",
|
|
334
|
-
"✗ Preview ended
|
|
334
|
+
"✗ Preview ended. The connection to Vincentt closed.",
|
|
335
335
|
"",
|
|
336
336
|
" This window can't tell whether that was your network, your machine going to",
|
|
337
337
|
" sleep, or something on our side. Nothing is wrong with your project.",
|
|
@@ -375,7 +375,7 @@ export function hardMaxWarning(clockTime, minutesLeft) {
|
|
|
375
375
|
export function hardMaxReached() {
|
|
376
376
|
return [
|
|
377
377
|
"",
|
|
378
|
-
"✓ Preview stopped
|
|
378
|
+
"✓ Preview stopped. 12-hour maximum reached. The link no longer works.",
|
|
379
379
|
"",
|
|
380
380
|
" Start a new one: vincentt preview",
|
|
381
381
|
" For an address that lasts, publish it: vincentt publish",
|
|
@@ -435,7 +435,7 @@ export function supersededWinner() {
|
|
|
435
435
|
export function endedUnreachable(orgName) {
|
|
436
436
|
return [
|
|
437
437
|
"",
|
|
438
|
-
"✗ Preview ended
|
|
438
|
+
"✗ Preview ended. This project is no longer available to you.",
|
|
439
439
|
"",
|
|
440
440
|
` Your access to ${orgName} may have changed. Check who you are: vincentt whoami`,
|
|
441
441
|
"",
|
|
@@ -458,7 +458,7 @@ export function endedAuthExpired() {
|
|
|
458
458
|
export function endedUnconfirmed() {
|
|
459
459
|
return [
|
|
460
460
|
"",
|
|
461
|
-
"✗ Preview ended
|
|
461
|
+
"✗ Preview ended. Couldn't confirm your access to this project.",
|
|
462
462
|
"",
|
|
463
463
|
" Try again: vincentt preview",
|
|
464
464
|
"",
|
|
@@ -498,7 +498,7 @@ export function endedUnconfirmed() {
|
|
|
498
498
|
export function endedAbandoned() {
|
|
499
499
|
return [
|
|
500
500
|
"",
|
|
501
|
-
"✗ Preview ended
|
|
501
|
+
"✗ Preview ended. This machine stopped checking in with Vincentt.",
|
|
502
502
|
"",
|
|
503
503
|
" The preview needed this computer to stay in touch and it went quiet, so the",
|
|
504
504
|
" link was released. Nothing is wrong with your project.",
|
|
@@ -526,7 +526,7 @@ export function egressSoft() {
|
|
|
526
526
|
export function egressHard() {
|
|
527
527
|
return [
|
|
528
528
|
"",
|
|
529
|
-
"✗ Preview ended
|
|
529
|
+
"✗ Preview ended. It reached the 5 GB ceiling.",
|
|
530
530
|
"",
|
|
531
531
|
" Start a new one: vincentt preview",
|
|
532
532
|
"",
|
package/dist/scaffold/index.d.ts
CHANGED
|
@@ -9,6 +9,11 @@ export declare const TEMPLATE_REPO = "https://github.com/vincentt-xr/v2-template
|
|
|
9
9
|
*
|
|
10
10
|
* A creator pins a known version with `--template 1.0.0`; version tags are
|
|
11
11
|
* immutable, which is what makes that an escape hatch rather than a coin flip.
|
|
12
|
+
*
|
|
13
|
+
* Asked for this ref, the scaffold clones the platform's released version tag
|
|
14
|
+
* instead when the platform records that release's commit, and refuses a clone
|
|
15
|
+
* that lands anywhere else (see `releasedCommit.ts`). The moving tag is cloned
|
|
16
|
+
* only when there is nothing to check it against.
|
|
12
17
|
*/
|
|
13
18
|
export declare const DEFAULT_TEMPLATE_REF = "latest";
|
|
14
19
|
/**
|
|
@@ -51,6 +56,12 @@ export interface ScaffoldOptions {
|
|
|
51
56
|
ref?: string;
|
|
52
57
|
/** Test seam — defaults to spawning real git. */
|
|
53
58
|
runGit?: GitRunner;
|
|
59
|
+
/** API origin for the release lookup. Defaults to the machine's configured one. */
|
|
60
|
+
apiUrl?: string;
|
|
61
|
+
/** Test seam for the release lookup — defaults to global `fetch`. */
|
|
62
|
+
fetchImpl?: typeof fetch;
|
|
63
|
+
/** Where the unverified-template warning goes. Defaults to stderr. */
|
|
64
|
+
warn?: (line: string) => void;
|
|
54
65
|
}
|
|
55
66
|
/**
|
|
56
67
|
* The concrete version tag the clone actually landed on, or `undefined` when the
|
package/dist/scaffold/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { promises as fs } from "node:fs";
|
|
|
8
8
|
import os from "node:os";
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import { promisify } from "node:util";
|
|
11
|
+
import { TemplateCommitMismatchError, UNVERIFIED_TEMPLATE_WARNING, resolveTemplatePin, } from "./releasedCommit.js";
|
|
11
12
|
const execFileAsync = promisify(execFile);
|
|
12
13
|
export const TEMPLATE_REPO = "https://github.com/vincentt-xr/v2-template.git";
|
|
13
14
|
/**
|
|
@@ -20,6 +21,11 @@ export const TEMPLATE_REPO = "https://github.com/vincentt-xr/v2-template.git";
|
|
|
20
21
|
*
|
|
21
22
|
* A creator pins a known version with `--template 1.0.0`; version tags are
|
|
22
23
|
* immutable, which is what makes that an escape hatch rather than a coin flip.
|
|
24
|
+
*
|
|
25
|
+
* Asked for this ref, the scaffold clones the platform's released version tag
|
|
26
|
+
* instead when the platform records that release's commit, and refuses a clone
|
|
27
|
+
* that lands anywhere else (see `releasedCommit.ts`). The moving tag is cloned
|
|
28
|
+
* only when there is nothing to check it against.
|
|
23
29
|
*/
|
|
24
30
|
export const DEFAULT_TEMPLATE_REF = "latest";
|
|
25
31
|
const realGit = async (args) => {
|
|
@@ -128,10 +134,21 @@ export async function scaffoldFromTemplate(targetDir, opts = {}) {
|
|
|
128
134
|
const repo = opts.repo ?? TEMPLATE_REPO;
|
|
129
135
|
const ref = opts.ref ?? DEFAULT_TEMPLATE_REF;
|
|
130
136
|
const runGit = opts.runGit ?? realGit;
|
|
137
|
+
// Only the built-in template has a platform release to check against; a
|
|
138
|
+
// gallery source (`init --from`) is another repository entirely.
|
|
139
|
+
const pin = repo === TEMPLATE_REPO
|
|
140
|
+
? await resolveTemplatePin(ref, DEFAULT_TEMPLATE_REF, {
|
|
141
|
+
apiUrl: opts.apiUrl,
|
|
142
|
+
fetchImpl: opts.fetchImpl,
|
|
143
|
+
})
|
|
144
|
+
: { ref, unverified: false };
|
|
145
|
+
if (pin.unverified) {
|
|
146
|
+
(opts.warn ?? ((line) => process.stderr.write(`${line}\n`)))(UNVERIFIED_TEMPLATE_WARNING);
|
|
147
|
+
}
|
|
131
148
|
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "vincentt-tmpl-"));
|
|
132
149
|
try {
|
|
133
150
|
try {
|
|
134
|
-
await runGit(["clone", "--depth", "1", "--branch", ref, repo, tmp]);
|
|
151
|
+
await runGit(["clone", "--depth", "1", "--branch", pin.ref, repo, tmp]);
|
|
135
152
|
}
|
|
136
153
|
catch (err) {
|
|
137
154
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -140,11 +157,26 @@ export async function scaffoldFromTemplate(targetDir, opts = {}) {
|
|
|
140
157
|
// real cause is a typo'd version). git names the ref in this specific case,
|
|
141
158
|
// so split on that rather than reporting one cause for both.
|
|
142
159
|
if (/(remote branch|Remote branch).*not found|couldn't find remote ref/i.test(msg)) {
|
|
143
|
-
throw new ScaffoldRefError(
|
|
144
|
-
`
|
|
160
|
+
throw new ScaffoldRefError(pin.ref === ref
|
|
161
|
+
? `The template has no version "${ref}". ` +
|
|
162
|
+
`Check the tag exists, or omit --template to use the latest release.`
|
|
163
|
+
: `The template has no tag for its released version "${pin.ref}". ` +
|
|
164
|
+
`Report this to the Vincentt team.`);
|
|
145
165
|
}
|
|
146
166
|
throw new Error(`Could not clone the ${repo} template (check your git access): ${msg}`);
|
|
147
167
|
}
|
|
168
|
+
if (pin.expectedCommit !== undefined) {
|
|
169
|
+
let actual;
|
|
170
|
+
try {
|
|
171
|
+
actual = String((await runGit(["-C", tmp, "rev-parse", "HEAD"])) ?? "").trim();
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
actual = "";
|
|
175
|
+
}
|
|
176
|
+
if (actual !== pin.expectedCommit) {
|
|
177
|
+
throw new TemplateCommitMismatchError(pin.releasedVersion ?? pin.ref, pin.expectedCommit, actual || "(could not be read)");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
148
180
|
// BEFORE the strip below — `.git` is the only place the concrete tag is
|
|
149
181
|
// readable, and the copy into the target deliberately does not carry it.
|
|
150
182
|
// A failure here must not fail the scaffold: the files are correct either
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface TemplatePin {
|
|
2
|
+
/** The tag to clone. */
|
|
3
|
+
ref: string;
|
|
4
|
+
/** Present when the clone must be checked; the clone fails on any other commit. */
|
|
5
|
+
expectedCommit?: string;
|
|
6
|
+
/** The version the platform released, named in the mismatch error. */
|
|
7
|
+
releasedVersion?: string;
|
|
8
|
+
/** True when the default ref is cloned with nothing to check it against. */
|
|
9
|
+
unverified: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface PinOptions {
|
|
12
|
+
/** The API origin. Defaults to the machine's configured one. */
|
|
13
|
+
apiUrl?: string;
|
|
14
|
+
/** Test seam, passed through to `fetchRelease`. */
|
|
15
|
+
fetchImpl?: typeof fetch;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the pin for a scaffold of the built-in template.
|
|
19
|
+
*
|
|
20
|
+
* `defaultRef` is the moving tag. Asked for it, the platform's release version
|
|
21
|
+
* is cloned in its place when that release carries a commit; otherwise the
|
|
22
|
+
* moving tag is cloned as before and the result says it went unverified. An
|
|
23
|
+
* explicit version is checked only when it is the version the platform
|
|
24
|
+
* released, since older version tags are protected by the tag ruleset and the
|
|
25
|
+
* platform holds no commit for them.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveTemplatePin(requested: string, defaultRef: string, opts?: PinOptions): Promise<TemplatePin>;
|
|
28
|
+
/** The one-line stderr warning for a default-ref scaffold with nothing to check. */
|
|
29
|
+
export declare const UNVERIFIED_TEMPLATE_WARNING = "! The template could not be verified against the platform release; cloned the latest tag unchecked.";
|
|
30
|
+
/**
|
|
31
|
+
* The cloned commit is not the one the platform released. Nothing is written
|
|
32
|
+
* and there is no fallback: a moved tag is exactly what this check exists to
|
|
33
|
+
* catch, and retrying `latest` would clone the same suspect tree unchecked.
|
|
34
|
+
*/
|
|
35
|
+
export declare class TemplateCommitMismatchError extends Error {
|
|
36
|
+
constructor(version: string, expected: string, actual: string);
|
|
37
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Which template ref to clone, and which commit it must land on.
|
|
2
|
+
//
|
|
3
|
+
// The template's tags live in a public repo whose admins can move them by hand,
|
|
4
|
+
// so the clone alone proves nothing. The platform records each template
|
|
5
|
+
// release's commit independently (`GET /platform/versions`), and a scaffold of
|
|
6
|
+
// the default ref clones THAT release's version tag and checks the commit.
|
|
7
|
+
import { TEMPLATE_COMPONENT } from "../platform/allowlist.js";
|
|
8
|
+
import { fetchRelease } from "../platform/releaseFetch.js";
|
|
9
|
+
import { loadMachineConfig, machineApiUrl } from "../shared/config.js";
|
|
10
|
+
/** Same shape the scaffold accepts as a concrete version tag. */
|
|
11
|
+
const CONCRETE_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+$/;
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the pin for a scaffold of the built-in template.
|
|
14
|
+
*
|
|
15
|
+
* `defaultRef` is the moving tag. Asked for it, the platform's release version
|
|
16
|
+
* is cloned in its place when that release carries a commit; otherwise the
|
|
17
|
+
* moving tag is cloned as before and the result says it went unverified. An
|
|
18
|
+
* explicit version is checked only when it is the version the platform
|
|
19
|
+
* released, since older version tags are protected by the tag ruleset and the
|
|
20
|
+
* platform holds no commit for them.
|
|
21
|
+
*/
|
|
22
|
+
export async function resolveTemplatePin(requested, defaultRef, opts = {}) {
|
|
23
|
+
const row = await fetchTemplateRow(opts);
|
|
24
|
+
const verifiable = row !== undefined && row.commit !== undefined && CONCRETE_VERSION.test(row.version);
|
|
25
|
+
if (requested === defaultRef) {
|
|
26
|
+
if (!verifiable)
|
|
27
|
+
return { ref: defaultRef, unverified: true };
|
|
28
|
+
return {
|
|
29
|
+
ref: row.version,
|
|
30
|
+
expectedCommit: row.commit,
|
|
31
|
+
releasedVersion: row.version,
|
|
32
|
+
unverified: false,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (verifiable && requested === row.version) {
|
|
36
|
+
return {
|
|
37
|
+
ref: requested,
|
|
38
|
+
expectedCommit: row.commit,
|
|
39
|
+
releasedVersion: row.version,
|
|
40
|
+
unverified: false,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return { ref: requested, unverified: false };
|
|
44
|
+
}
|
|
45
|
+
async function fetchTemplateRow(opts) {
|
|
46
|
+
try {
|
|
47
|
+
const apiUrl = opts.apiUrl ?? machineApiUrl(await loadMachineConfig());
|
|
48
|
+
const fetched = await fetchRelease({ apiUrl, fetchImpl: opts.fetchImpl });
|
|
49
|
+
return fetched?.release.components[TEMPLATE_COMPONENT];
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// An unreadable machine config is the same as an unreachable platform.
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** The one-line stderr warning for a default-ref scaffold with nothing to check. */
|
|
57
|
+
export const UNVERIFIED_TEMPLATE_WARNING = "! The template could not be verified against the platform release; cloned the latest tag unchecked.";
|
|
58
|
+
/**
|
|
59
|
+
* The cloned commit is not the one the platform released. Nothing is written
|
|
60
|
+
* and there is no fallback: a moved tag is exactly what this check exists to
|
|
61
|
+
* catch, and retrying `latest` would clone the same suspect tree unchecked.
|
|
62
|
+
*/
|
|
63
|
+
export class TemplateCommitMismatchError extends Error {
|
|
64
|
+
constructor(version, expected, actual) {
|
|
65
|
+
super(`The template did not match the released version ${version}.\n` +
|
|
66
|
+
` Expected commit ${expected}\n` +
|
|
67
|
+
` Actual commit ${actual}\n\n` +
|
|
68
|
+
` Nothing was written. Report this to the Vincentt team.`);
|
|
69
|
+
this.name = "TemplateCommitMismatchError";
|
|
70
|
+
}
|
|
71
|
+
}
|
package/package.json
CHANGED