@vincentt-xr/cli 0.11.0 → 0.12.1
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/index.js +37 -0
- package/dist/dx/updateCheck.d.ts +55 -0
- package/dist/dx/updateCheck.js +98 -0
- package/dist/dx/updateCheckIo.d.ts +18 -0
- package/dist/dx/updateCheckIo.js +78 -0
- package/dist/lib/backend.js +6 -1
- package/dist/library/client.js +2 -1
- package/dist/login/authBackend.js +3 -2
- package/dist/publish/api.js +2 -1
- package/dist/shared/clientHeader.d.ts +19 -0
- package/dist/shared/clientHeader.js +72 -0
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -45,6 +45,8 @@ import { DX_UNATTENDED_SUNSET, loadEnablement } from "../dx/enablement.js";
|
|
|
45
45
|
import { journalEnabled, journalPath, readJournal } from "../dx/journal.js";
|
|
46
46
|
import { postReport } from "../dx/send.js";
|
|
47
47
|
import { nudgeFor } from "../dx/nudge.js";
|
|
48
|
+
import { FETCH_TIMEOUT_MS, isFresh, updateNotice } from "../dx/updateCheck.js";
|
|
49
|
+
import { fetchLatest, loadCache, saveCache } from "../dx/updateCheckIo.js";
|
|
48
50
|
import { verbOf } from "../dx/skeleton.js";
|
|
49
51
|
import { createSourceFlagRefusal, fromFlagConflict, isHelpInvocation, isVersionInvocation, positionalArgs, refusedFlag, } from "./args.js";
|
|
50
52
|
import { parseSourceRef } from "../lib/sourceRef.js";
|
|
@@ -1173,6 +1175,41 @@ function journal(exitCode, error) {
|
|
|
1173
1175
|
catch {
|
|
1174
1176
|
// A hint that can break a publish is not worth having.
|
|
1175
1177
|
}
|
|
1178
|
+
// The update notice prints from the LAST run's cached answer and never waits
|
|
1179
|
+
// on the registry, so no verb pays for this. The consequence is that a fresh
|
|
1180
|
+
// install stays quiet on its first run and speaks on the next one — which is
|
|
1181
|
+
// the right trade: a creator who just installed is by definition current, and
|
|
1182
|
+
// a notice is worth nothing if it costs a second on every publish.
|
|
1183
|
+
try {
|
|
1184
|
+
const cached = loadCache();
|
|
1185
|
+
for (const l of updateNotice(cliVersion(), cached?.latest))
|
|
1186
|
+
console.error(l);
|
|
1187
|
+
if (!isFresh(cached, Date.now()))
|
|
1188
|
+
refreshUpdateCache();
|
|
1189
|
+
}
|
|
1190
|
+
catch {
|
|
1191
|
+
// Same rule as the nudge: never break a run over a footnote.
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Refresh the cached `latest` without holding the process open.
|
|
1196
|
+
*
|
|
1197
|
+
* `unref` is the load-bearing call. Without it a 2s registry request keeps the
|
|
1198
|
+
* event loop alive after the verb is done, so every command appears to hang for
|
|
1199
|
+
* up to two seconds — a CLI that feels slower in exchange for an occasional
|
|
1200
|
+
* notice is a bad trade, and the delay would land on `preview`, where someone is
|
|
1201
|
+
* waiting to open a URL on a phone.
|
|
1202
|
+
*/
|
|
1203
|
+
function refreshUpdateCache() {
|
|
1204
|
+
const timer = setTimeout(() => { }, FETCH_TIMEOUT_MS + 500);
|
|
1205
|
+
timer.unref?.();
|
|
1206
|
+
void fetchLatest()
|
|
1207
|
+
.then((latest) => {
|
|
1208
|
+
if (latest !== undefined)
|
|
1209
|
+
saveCache({ latest, at: Date.now() });
|
|
1210
|
+
})
|
|
1211
|
+
.catch(() => { })
|
|
1212
|
+
.finally(() => clearTimeout(timer));
|
|
1176
1213
|
}
|
|
1177
1214
|
main()
|
|
1178
1215
|
.then(() => {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The update notice: a one-line fact, printed after a run, saying a newer CLI
|
|
3
|
+
* exists.
|
|
4
|
+
*
|
|
5
|
+
* The CLI is installed globally (`npm install -g @vincentt-xr/cli`), so there is
|
|
6
|
+
* one copy per machine and nothing about a project ever updates it. The install
|
|
7
|
+
* instruction in the scaffolded contract fires only when the binary is MISSING,
|
|
8
|
+
* which makes it a first-install path and never an upgrade path: a creator who
|
|
9
|
+
* installed in July keeps running July's CLI indefinitely, and the failures that
|
|
10
|
+
* follow a wire change read as platform bugs.
|
|
11
|
+
*
|
|
12
|
+
* STATES A FACT, NEVER AN INSTRUCTION — the same rule as the dx nudge, for the
|
|
13
|
+
* same reason. This line lands in an agent's transcript, where the CLI's stdout
|
|
14
|
+
* is the agent's input, and `npm install -g ...@latest` is a directive an
|
|
15
|
+
* autonomous caller will simply obey. An agent that updates the binary mid-run
|
|
16
|
+
* has swapped the tool underneath itself and may need a sudo the human never
|
|
17
|
+
* approved. Naming that a newer version exists is information the caller can act
|
|
18
|
+
* on; telling it to install is a decision that is not the CLI's to make.
|
|
19
|
+
*/
|
|
20
|
+
/** How long a registry answer is trusted. One notice a day, not one per run. */
|
|
21
|
+
export declare const CHECK_TTL_MS: number;
|
|
22
|
+
/** Registry request budget. A slow npm must never slow a verb down. */
|
|
23
|
+
export declare const FETCH_TIMEOUT_MS = 2000;
|
|
24
|
+
export interface UpdateCache {
|
|
25
|
+
/** The `latest` dist-tag as last seen. */
|
|
26
|
+
latest: string;
|
|
27
|
+
/** When it was fetched (epoch ms). */
|
|
28
|
+
at: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* True when `latest` is strictly newer than `current`.
|
|
32
|
+
*
|
|
33
|
+
* Prerelease suffixes are deliberately ignored beyond the numeric core: the
|
|
34
|
+
* comparison only decides whether to print one line, and a creator running
|
|
35
|
+
* `0.11.0-rc.1` against a `0.11.0` latest is not someone to nag.
|
|
36
|
+
*/
|
|
37
|
+
export declare function isNewer(current: string, latest: string): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Decide whether a cached answer is still usable. A cache written in the future
|
|
40
|
+
* (a clock that moved) is treated as stale rather than trusted forever.
|
|
41
|
+
*/
|
|
42
|
+
export declare function isFresh(cache: UpdateCache | undefined, now: number): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Read a cache object defensively. STRICT about types for the same reason the
|
|
45
|
+
* enablement reader is: a hand-edited or half-written file must degrade to "no
|
|
46
|
+
* cache" rather than to a bogus comparison.
|
|
47
|
+
*/
|
|
48
|
+
export declare function readCache(raw: unknown): UpdateCache | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* The notice. Empty when there is nothing to say.
|
|
51
|
+
*
|
|
52
|
+
* Pure, so the transcript is testable without a network, a home directory, or a
|
|
53
|
+
* published package.
|
|
54
|
+
*/
|
|
55
|
+
export declare function updateNotice(current: string, latest: string | undefined): string[];
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The update notice: a one-line fact, printed after a run, saying a newer CLI
|
|
3
|
+
* exists.
|
|
4
|
+
*
|
|
5
|
+
* The CLI is installed globally (`npm install -g @vincentt-xr/cli`), so there is
|
|
6
|
+
* one copy per machine and nothing about a project ever updates it. The install
|
|
7
|
+
* instruction in the scaffolded contract fires only when the binary is MISSING,
|
|
8
|
+
* which makes it a first-install path and never an upgrade path: a creator who
|
|
9
|
+
* installed in July keeps running July's CLI indefinitely, and the failures that
|
|
10
|
+
* follow a wire change read as platform bugs.
|
|
11
|
+
*
|
|
12
|
+
* STATES A FACT, NEVER AN INSTRUCTION — the same rule as the dx nudge, for the
|
|
13
|
+
* same reason. This line lands in an agent's transcript, where the CLI's stdout
|
|
14
|
+
* is the agent's input, and `npm install -g ...@latest` is a directive an
|
|
15
|
+
* autonomous caller will simply obey. An agent that updates the binary mid-run
|
|
16
|
+
* has swapped the tool underneath itself and may need a sudo the human never
|
|
17
|
+
* approved. Naming that a newer version exists is information the caller can act
|
|
18
|
+
* on; telling it to install is a decision that is not the CLI's to make.
|
|
19
|
+
*/
|
|
20
|
+
/** How long a registry answer is trusted. One notice a day, not one per run. */
|
|
21
|
+
export const CHECK_TTL_MS = 24 * 60 * 60 * 1000;
|
|
22
|
+
/** Registry request budget. A slow npm must never slow a verb down. */
|
|
23
|
+
export const FETCH_TIMEOUT_MS = 2000;
|
|
24
|
+
/**
|
|
25
|
+
* Parse a semver-ish version into comparable parts. Returns undefined for
|
|
26
|
+
* anything unrecognized, which callers treat as "cannot compare" rather than as
|
|
27
|
+
* zero — a version that failed to parse must not read as older than everything.
|
|
28
|
+
*/
|
|
29
|
+
function parts(v) {
|
|
30
|
+
const m = /^(\d+)\.(\d+)\.(\d+)/.exec(v.trim());
|
|
31
|
+
if (m === null)
|
|
32
|
+
return undefined;
|
|
33
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* True when `latest` is strictly newer than `current`.
|
|
37
|
+
*
|
|
38
|
+
* Prerelease suffixes are deliberately ignored beyond the numeric core: the
|
|
39
|
+
* comparison only decides whether to print one line, and a creator running
|
|
40
|
+
* `0.11.0-rc.1` against a `0.11.0` latest is not someone to nag.
|
|
41
|
+
*/
|
|
42
|
+
export function isNewer(current, latest) {
|
|
43
|
+
const c = parts(current);
|
|
44
|
+
const l = parts(latest);
|
|
45
|
+
if (c === undefined || l === undefined)
|
|
46
|
+
return false;
|
|
47
|
+
for (let i = 0; i < 3; i++) {
|
|
48
|
+
if (l[i] > c[i])
|
|
49
|
+
return true;
|
|
50
|
+
if (l[i] < c[i])
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Decide whether a cached answer is still usable. A cache written in the future
|
|
57
|
+
* (a clock that moved) is treated as stale rather than trusted forever.
|
|
58
|
+
*/
|
|
59
|
+
export function isFresh(cache, now) {
|
|
60
|
+
if (cache === undefined)
|
|
61
|
+
return false;
|
|
62
|
+
if (!Number.isFinite(cache.at))
|
|
63
|
+
return false;
|
|
64
|
+
const age = now - cache.at;
|
|
65
|
+
return age >= 0 && age < CHECK_TTL_MS;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Read a cache object defensively. STRICT about types for the same reason the
|
|
69
|
+
* enablement reader is: a hand-edited or half-written file must degrade to "no
|
|
70
|
+
* cache" rather than to a bogus comparison.
|
|
71
|
+
*/
|
|
72
|
+
export function readCache(raw) {
|
|
73
|
+
if (typeof raw !== "object" || raw === null)
|
|
74
|
+
return undefined;
|
|
75
|
+
const { latest, at } = raw;
|
|
76
|
+
if (typeof latest !== "string" || latest === "")
|
|
77
|
+
return undefined;
|
|
78
|
+
if (typeof at !== "number" || !Number.isFinite(at))
|
|
79
|
+
return undefined;
|
|
80
|
+
return { latest, at };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The notice. Empty when there is nothing to say.
|
|
84
|
+
*
|
|
85
|
+
* Pure, so the transcript is testable without a network, a home directory, or a
|
|
86
|
+
* published package.
|
|
87
|
+
*/
|
|
88
|
+
export function updateNotice(current, latest) {
|
|
89
|
+
if (latest === undefined)
|
|
90
|
+
return [];
|
|
91
|
+
if (!isNewer(current, latest))
|
|
92
|
+
return [];
|
|
93
|
+
return [
|
|
94
|
+
"",
|
|
95
|
+
` ! A newer vincentt is available (${current} → ${latest}).`,
|
|
96
|
+
" Installed globally as @vincentt-xr/cli.",
|
|
97
|
+
];
|
|
98
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type UpdateCache } from "./updateCheck.js";
|
|
2
|
+
export declare function cachePath(): string;
|
|
3
|
+
/** Read the cached answer. Any fault reads as "no cache". */
|
|
4
|
+
export declare function loadCache(): UpdateCache | undefined;
|
|
5
|
+
/**
|
|
6
|
+
* Write the cached answer. Best-effort and non-atomic, unlike the machine
|
|
7
|
+
* config: this file holds no secret and no state a creator would miss, so a
|
|
8
|
+
* torn write costs one extra registry call rather than a lost token.
|
|
9
|
+
*/
|
|
10
|
+
export declare function saveCache(cache: UpdateCache): void;
|
|
11
|
+
/**
|
|
12
|
+
* Ask the registry for the `latest` dist-tag. Undefined on any fault.
|
|
13
|
+
*
|
|
14
|
+
* The Accept header requests npm's abbreviated document, which is a fraction of
|
|
15
|
+
* the full package metadata — the full one for a package with many versions is
|
|
16
|
+
* megabytes, and this runs on someone's laptop after every verb.
|
|
17
|
+
*/
|
|
18
|
+
export declare function fetchLatest(signal?: AbortSignal): Promise<string | undefined>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The I/O half of the update notice: the cache file and the registry fetch.
|
|
3
|
+
*
|
|
4
|
+
* Split from the decision logic in updateCheck.ts so the transcript and the
|
|
5
|
+
* comparison stay testable without a network or a home directory.
|
|
6
|
+
*
|
|
7
|
+
* Every operation here is best-effort. A creator's `publish` must not fail, slow
|
|
8
|
+
* down, or print a stack trace because the npm registry was unreachable or
|
|
9
|
+
* ~/.vincentt was read-only — the worst outcome of this whole feature going dark
|
|
10
|
+
* is that nobody is told about an update, which is exactly where they were
|
|
11
|
+
* before it existed.
|
|
12
|
+
*/
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { FETCH_TIMEOUT_MS, readCache } from "./updateCheck.js";
|
|
17
|
+
const CACHE_FILE = "update-check.json";
|
|
18
|
+
/** The npm registry's package document, filtered to the dist-tags it serves. */
|
|
19
|
+
const REGISTRY_URL = "https://registry.npmjs.org/@vincentt-xr%2Fcli";
|
|
20
|
+
export function cachePath() {
|
|
21
|
+
return path.join(os.homedir(), ".vincentt", CACHE_FILE);
|
|
22
|
+
}
|
|
23
|
+
/** Read the cached answer. Any fault reads as "no cache". */
|
|
24
|
+
export function loadCache() {
|
|
25
|
+
try {
|
|
26
|
+
return readCache(JSON.parse(fs.readFileSync(cachePath(), "utf8")));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Write the cached answer. Best-effort and non-atomic, unlike the machine
|
|
34
|
+
* config: this file holds no secret and no state a creator would miss, so a
|
|
35
|
+
* torn write costs one extra registry call rather than a lost token.
|
|
36
|
+
*/
|
|
37
|
+
export function saveCache(cache) {
|
|
38
|
+
try {
|
|
39
|
+
fs.mkdirSync(path.dirname(cachePath()), { recursive: true });
|
|
40
|
+
fs.writeFileSync(cachePath(), JSON.stringify(cache) + "\n", "utf8");
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// A cache that cannot be written just means the next run asks again.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Ask the registry for the `latest` dist-tag. Undefined on any fault.
|
|
48
|
+
*
|
|
49
|
+
* The Accept header requests npm's abbreviated document, which is a fraction of
|
|
50
|
+
* the full package metadata — the full one for a package with many versions is
|
|
51
|
+
* megabytes, and this runs on someone's laptop after every verb.
|
|
52
|
+
*/
|
|
53
|
+
export async function fetchLatest(signal) {
|
|
54
|
+
const controller = new AbortController();
|
|
55
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
56
|
+
const onAbort = () => controller.abort();
|
|
57
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
58
|
+
try {
|
|
59
|
+
const res = await fetch(REGISTRY_URL, {
|
|
60
|
+
signal: controller.signal,
|
|
61
|
+
headers: {
|
|
62
|
+
Accept: "application/vnd.npm.install-v1+json",
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
if (!res.ok)
|
|
66
|
+
return undefined;
|
|
67
|
+
const body = (await res.json());
|
|
68
|
+
const latest = body["dist-tags"]?.latest;
|
|
69
|
+
return typeof latest === "string" && latest !== "" ? latest : undefined;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
clearTimeout(timer);
|
|
76
|
+
signal?.removeEventListener("abort", onAbort);
|
|
77
|
+
}
|
|
78
|
+
}
|
package/dist/lib/backend.js
CHANGED
|
@@ -6,6 +6,7 @@ import { promises as fs } from "node:fs";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { execFile } from "node:child_process";
|
|
8
8
|
import { promisify } from "node:util";
|
|
9
|
+
import { clientHeaders } from "../shared/clientHeader.js";
|
|
9
10
|
import { authFailureMessage } from "../shared/authError.js";
|
|
10
11
|
const execFileAsync = promisify(execFile);
|
|
11
12
|
export class BackendError extends Error {
|
|
@@ -19,7 +20,11 @@ export class BackendError extends Error {
|
|
|
19
20
|
async function authFetch(cfg, pathname, init = {}) {
|
|
20
21
|
return fetch(`${cfg.apiUrl}${pathname}`, {
|
|
21
22
|
...init,
|
|
22
|
-
headers: {
|
|
23
|
+
headers: {
|
|
24
|
+
...(init.headers ?? {}),
|
|
25
|
+
...clientHeaders(),
|
|
26
|
+
authorization: `Bearer ${cfg.pat}`,
|
|
27
|
+
},
|
|
23
28
|
});
|
|
24
29
|
}
|
|
25
30
|
async function safeText(res) {
|
package/dist/library/client.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// CLI-supplied URL reaches a fetch. That is a security control, not a style choice
|
|
13
13
|
// (D-add-prints-urls degraded the no-invented-URLs rule from prevention to
|
|
14
14
|
// detection, and this is the one arm that stayed prevention).
|
|
15
|
+
import { clientHeaders } from "../shared/clientHeader.js";
|
|
15
16
|
import { parseBatchResponse, parseFetchResponse, parseSearchResponse, } from "./wire.js";
|
|
16
17
|
/** The API said no, with a code we can act on. */
|
|
17
18
|
export class LibraryError extends Error {
|
|
@@ -61,7 +62,7 @@ async function readJson(res) {
|
|
|
61
62
|
*/
|
|
62
63
|
export function httpLibraryClient(cfg) {
|
|
63
64
|
const base = cfg.apiUrl.replace(/\/$/, "");
|
|
64
|
-
const auth = { authorization: `Bearer ${cfg.pat}` };
|
|
65
|
+
const auth = { ...clientHeaders(), authorization: `Bearer ${cfg.pat}` };
|
|
65
66
|
const call = async (pathname, init) => {
|
|
66
67
|
try {
|
|
67
68
|
return await fetch(`${base}${pathname}`, {
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// POST /access-tokens/exchange {code, code_verifier} → {token} (raw PAT, once, in body)
|
|
8
8
|
// GET /me Bearer <PAT> → identity + orgs (confirm)
|
|
9
9
|
import { authFailureMessage } from "../shared/authError.js";
|
|
10
|
+
import { clientHeaders } from "../shared/clientHeader.js";
|
|
10
11
|
class AuthError extends Error {
|
|
11
12
|
status;
|
|
12
13
|
constructor(message, status) {
|
|
@@ -30,7 +31,7 @@ export function httpAuthBackend(apiBase) {
|
|
|
30
31
|
async exchange(code, codeVerifier) {
|
|
31
32
|
const res = await fetch(`${base}/access-tokens/exchange`, {
|
|
32
33
|
method: "POST",
|
|
33
|
-
headers: { "content-type": "application/json" },
|
|
34
|
+
headers: { ...clientHeaders(), "content-type": "application/json" },
|
|
34
35
|
body: JSON.stringify({ code, code_verifier: codeVerifier }),
|
|
35
36
|
});
|
|
36
37
|
if (!res.ok) {
|
|
@@ -43,7 +44,7 @@ export function httpAuthBackend(apiBase) {
|
|
|
43
44
|
},
|
|
44
45
|
async me(pat) {
|
|
45
46
|
const res = await fetch(`${base}/me`, {
|
|
46
|
-
headers: { authorization: `Bearer ${pat}` },
|
|
47
|
+
headers: { ...clientHeaders(), authorization: `Bearer ${pat}` },
|
|
47
48
|
});
|
|
48
49
|
if (!res.ok) {
|
|
49
50
|
throw new AuthError(authFailureMessage("Identity check failed", res.status, await safeText(res)), res.status);
|
package/dist/publish/api.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
//
|
|
6
6
|
// The bytes do NOT transit the API. Open returns one presigned PUT per file,
|
|
7
7
|
// each signed for one exact key; the CLI PUTs direct to S3 and then commits.
|
|
8
|
+
import { clientHeaders } from "../shared/clientHeader.js";
|
|
8
9
|
export class PublishApiError extends Error {
|
|
9
10
|
status;
|
|
10
11
|
code;
|
|
@@ -31,7 +32,7 @@ async function readError(res) {
|
|
|
31
32
|
return new PublishApiError(res.status, body.code ?? `http_${res.status}`, body.message ?? `request failed (${res.status})`, body.detail ?? {}, retryAfter ? Number(retryAfter) : undefined);
|
|
32
33
|
}
|
|
33
34
|
function authHeaders(cfg) {
|
|
34
|
-
return { authorization: `Bearer ${cfg.pat}` };
|
|
35
|
+
return { ...clientHeaders(), authorization: `Bearer ${cfg.pat}` };
|
|
35
36
|
}
|
|
36
37
|
/** The ONE call before the checklist. liveVersion + livePublishedAt ride it, by design. */
|
|
37
38
|
export async function readProject(cfg, projectId, fetchImpl = fetch) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** The header name. Canonical casing; HTTP header matching is case-insensitive. */
|
|
2
|
+
export declare const CLIENT_HEADER = "X-Vincentt-Client";
|
|
3
|
+
export declare function clientVersion(): string;
|
|
4
|
+
/**
|
|
5
|
+
* The header value: `cli/<version>`.
|
|
6
|
+
*
|
|
7
|
+
* The `cli/` prefix is what keeps the field readable when a second first-party
|
|
8
|
+
* client (the harness, a future console agent) starts reporting on the same
|
|
9
|
+
* header — a bare version would be ambiguous the moment it is not alone.
|
|
10
|
+
*/
|
|
11
|
+
export declare function clientHeaderValue(): string;
|
|
12
|
+
/**
|
|
13
|
+
* The header, as a spreadable object. Callers merge this into their existing
|
|
14
|
+
* headers rather than assembling the name themselves, so the format stays in one
|
|
15
|
+
* place.
|
|
16
|
+
*/
|
|
17
|
+
export declare function clientHeaders(): Record<string, string>;
|
|
18
|
+
/** Reset the memo. Test seam only. */
|
|
19
|
+
export declare function resetClientVersionCache(): void;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The client-identity header every API request carries.
|
|
3
|
+
*
|
|
4
|
+
* The platform could not previously tell which CLI was calling it. That makes
|
|
5
|
+
* two ordinary questions unanswerable: how far behind the installed base
|
|
6
|
+
* actually is, and whether a wire change is safe to make yet. Both are questions
|
|
7
|
+
* about a population, and no amount of reading one creator's terminal answers
|
|
8
|
+
* them.
|
|
9
|
+
*
|
|
10
|
+
* It is also the seam a minimum-version refusal would need. That is deliberately
|
|
11
|
+
* NOT built here — a floor that strands someone mid-publish is worth having only
|
|
12
|
+
* once a contract has actually broken, and it needs this measurement first to
|
|
13
|
+
* know where to sit.
|
|
14
|
+
*
|
|
15
|
+
* `X-Vincentt-Client` is the platform's namespace, which the EDGE strips from
|
|
16
|
+
* viewer traffic (see isViewerRoutingHeader in the api repo): a device must not
|
|
17
|
+
* be able to forge what a creator's tooling reports about itself. This is the
|
|
18
|
+
* other side of that rule — the CLI is a first-party client talking to the API
|
|
19
|
+
* directly, not a viewer, so it is the legitimate origin of the value rather
|
|
20
|
+
* than something to be filtered out.
|
|
21
|
+
*/
|
|
22
|
+
import { createRequire } from "node:module";
|
|
23
|
+
/** The header name. Canonical casing; HTTP header matching is case-insensitive. */
|
|
24
|
+
export const CLIENT_HEADER = "X-Vincentt-Client";
|
|
25
|
+
/**
|
|
26
|
+
* The running package's version, read from the manifest rather than baked in by
|
|
27
|
+
* the build — a hardcoded string is one forgotten edit away from a CLI that
|
|
28
|
+
* misreports itself, which is worse than reporting nothing. `src/shared/` and
|
|
29
|
+
* the emitted `dist/shared/` sit at the same depth, so one relative path serves
|
|
30
|
+
* both.
|
|
31
|
+
*/
|
|
32
|
+
function readVersion() {
|
|
33
|
+
try {
|
|
34
|
+
const manifest = createRequire(import.meta.url)("../../package.json");
|
|
35
|
+
return manifest.version ?? "unknown";
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return "unknown";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Cached: the manifest cannot change within a process, and this is read on every
|
|
43
|
+
* outbound request.
|
|
44
|
+
*/
|
|
45
|
+
let cached;
|
|
46
|
+
export function clientVersion() {
|
|
47
|
+
if (cached === undefined)
|
|
48
|
+
cached = readVersion();
|
|
49
|
+
return cached;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The header value: `cli/<version>`.
|
|
53
|
+
*
|
|
54
|
+
* The `cli/` prefix is what keeps the field readable when a second first-party
|
|
55
|
+
* client (the harness, a future console agent) starts reporting on the same
|
|
56
|
+
* header — a bare version would be ambiguous the moment it is not alone.
|
|
57
|
+
*/
|
|
58
|
+
export function clientHeaderValue() {
|
|
59
|
+
return `cli/${clientVersion()}`;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The header, as a spreadable object. Callers merge this into their existing
|
|
63
|
+
* headers rather than assembling the name themselves, so the format stays in one
|
|
64
|
+
* place.
|
|
65
|
+
*/
|
|
66
|
+
export function clientHeaders() {
|
|
67
|
+
return { [CLIENT_HEADER]: clientHeaderValue() };
|
|
68
|
+
}
|
|
69
|
+
/** Reset the memo. Test seam only. */
|
|
70
|
+
export function resetClientVersionCache() {
|
|
71
|
+
cached = undefined;
|
|
72
|
+
}
|
package/package.json
CHANGED