@vincentt-xr/cli 0.20.0 → 0.22.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/MIGRATION.md +30 -0
- package/dist/cli/index.js +86 -12
- package/dist/domain/api.js +16 -12
- package/dist/domain/command.js +9 -0
- package/dist/dx/command.d.ts +74 -18
- package/dist/dx/command.js +140 -86
- package/dist/dx/consent.d.ts +25 -6
- package/dist/dx/consent.js +64 -35
- package/dist/dx/enablement.d.ts +63 -11
- package/dist/dx/enablement.js +93 -21
- package/dist/dx/report.d.ts +4 -24
- package/dist/dx/report.js +32 -25
- package/dist/dx/reportIo.js +6 -4
- package/dist/dx/send.js +6 -4
- package/dist/lib/backend.js +5 -10
- package/dist/lib/exitCodes.d.ts +6 -0
- package/dist/lib/exitCodes.js +6 -0
- package/dist/lib/terminalText.d.ts +6 -0
- package/dist/lib/terminalText.js +10 -0
- package/dist/lib/version.d.ts +28 -2
- package/dist/lib/version.js +30 -3
- package/dist/lib/whoami.js +3 -0
- package/dist/library/assetsCommand.js +4 -0
- package/dist/library/client.js +6 -8
- package/dist/login/authBackend.js +18 -15
- package/dist/login/loginCommand.js +31 -4
- package/dist/login/pkceLogin.d.ts +10 -0
- package/dist/login/pkceLogin.js +24 -1
- package/dist/platform/releaseFetch.js +36 -69
- package/dist/preview/api.js +9 -12
- package/dist/preview/command.d.ts +3 -2
- package/dist/preview/command.js +10 -5
- package/dist/preview/copy.d.ts +13 -1
- package/dist/preview/copy.js +31 -3
- package/dist/preview/proxy.js +5 -39
- package/dist/preview/runner.d.ts +31 -9
- package/dist/preview/runner.js +43 -15
- package/dist/publish/api.js +14 -14
- package/dist/publish/command.d.ts +3 -0
- package/dist/publish/command.js +74 -7
- package/dist/publish/copy.d.ts +15 -0
- package/dist/publish/copy.js +109 -19
- package/dist/publish/keyScan.d.ts +25 -0
- package/dist/publish/keyScan.js +88 -0
- package/dist/publish/manifest.d.ts +60 -19
- package/dist/publish/manifest.js +290 -71
- package/dist/publish/upload.d.ts +18 -0
- package/dist/publish/upload.js +38 -0
- package/dist/publish/weight.d.ts +23 -0
- package/dist/publish/weight.js +113 -0
- package/dist/shared/config.d.ts +6 -6
- package/dist/shared/credentialNames.d.ts +17 -0
- package/dist/shared/credentialNames.js +72 -0
- package/dist/shared/http.d.ts +34 -0
- package/dist/shared/http.js +98 -2
- package/dist/shared/upgradeError.d.ts +62 -0
- package/dist/shared/upgradeError.js +215 -0
- package/dist/tunnel/caps.d.ts +19 -0
- package/dist/tunnel/caps.js +52 -0
- package/dist/tunnel/cbor.d.ts +39 -12
- package/dist/tunnel/cbor.js +84 -10
- package/dist/tunnel/client.d.ts +56 -2
- package/dist/tunnel/client.js +160 -44
- package/dist/tunnel/frame.d.ts +1 -1
- package/dist/tunnel/frame.js +8 -4
- package/dist/tunnel/index.d.ts +3 -2
- package/dist/tunnel/index.js +3 -2
- package/package.json +4 -3
package/dist/preview/runner.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type ControlKind, type TunnelState, type UAClass } from "../tunnel/inde
|
|
|
2
2
|
import type { DiagEvent } from "@vincentt-xr/harness/events";
|
|
3
3
|
import { type EventTags } from "../relay/server.js";
|
|
4
4
|
import { type ResolvedConfig } from "../shared/config.js";
|
|
5
|
+
import { type UpgradeFacts } from "../shared/upgradeError.js";
|
|
5
6
|
/** The path a 404 advisory names: the request's own path, without its query. */
|
|
6
7
|
export declare function requestPathOf(url: string | undefined): string;
|
|
7
8
|
/**
|
|
@@ -31,7 +32,25 @@ export type PreviewEndReason = "stopped"
|
|
|
31
32
|
* elapsed without the client getting back): same remedy, different diagnosis,
|
|
32
33
|
* and the copy explains the bound only at the moment it bites.
|
|
33
34
|
*/
|
|
34
|
-
| "grace_cap" | "authz_revoked"
|
|
35
|
+
| "grace_cap" | "authz_revoked"
|
|
36
|
+
/**
|
|
37
|
+
* The edge drained the session because the platform stopped confirming it,
|
|
38
|
+
* and said so (CONTROL `authz_expired`). Final: a new preview is a new link,
|
|
39
|
+
* so nothing is re-minted and nothing redials.
|
|
40
|
+
*/
|
|
41
|
+
| "authz_lapsed"
|
|
42
|
+
/**
|
|
43
|
+
* The platform refused this CLI's version, on the heartbeat (426) or on a
|
|
44
|
+
* dial (410 + `X-Vincentt-Refusal: cli_upgrade_required`). One screen for
|
|
45
|
+
* both. No further platform call is made, the DELETE included.
|
|
46
|
+
*/
|
|
47
|
+
| "upgrade_required"
|
|
48
|
+
/**
|
|
49
|
+
* The edge broke the agreed tunnel wire (a malformed 101 caps header, a
|
|
50
|
+
* CONTROL frame this connection cannot read, or a fatal OPEN). Final: a redial reaches the
|
|
51
|
+
* same edge, so the tunnel client has already stopped dialling.
|
|
52
|
+
*/
|
|
53
|
+
| "protocol_violation"
|
|
35
54
|
/**
|
|
36
55
|
* The 30-minute heartbeat-silence flip. NEVER reported as `hard_max`: a
|
|
37
56
|
* session ended at minute 30 has not reached its 12-hour ceiling, and saying
|
|
@@ -130,8 +149,11 @@ export interface StartPreviewOptions {
|
|
|
130
149
|
onOriginError?: (label: string, code: string) => void;
|
|
131
150
|
/** Their dev server answered again. Re-arms the advisory; prints NOTHING. */
|
|
132
151
|
onOriginRecovered?: () => void;
|
|
133
|
-
/**
|
|
134
|
-
|
|
152
|
+
/**
|
|
153
|
+
* The session ended for a reason the creator did not cause. `refusal` carries
|
|
154
|
+
* the validated floor facts on `upgrade_required` only.
|
|
155
|
+
*/
|
|
156
|
+
onEnded?: (reason: PreviewEndReason, refusal?: UpgradeFacts) => void;
|
|
135
157
|
/** The hard max is approaching. A WARNING, never an ending (L20b). */
|
|
136
158
|
onExpiring?: () => void;
|
|
137
159
|
/** Injected for tests. */
|
|
@@ -154,11 +176,11 @@ export interface RunningPreview {
|
|
|
154
176
|
}
|
|
155
177
|
export declare function startPreview(opts: StartPreviewOptions): Promise<RunningPreview>;
|
|
156
178
|
/**
|
|
157
|
-
*
|
|
158
|
-
* hard-max warning, and it arrives on the same channel. Mapping it to
|
|
159
|
-
* would take away a link that has half an hour left, at exactly the
|
|
160
|
-
* creator is being warned to plan around it
|
|
161
|
-
* renders the warning instead.
|
|
179
|
+
* Three of the four kinds that reach onControl are endings. `expiring` is NOT:
|
|
180
|
+
* it is the hard-max warning, and it arrives on the same channel. Mapping it to
|
|
181
|
+
* an ending would take away a link that has half an hour left, at exactly the
|
|
182
|
+
* moment the creator is being warned to plan around it, so it returns null and
|
|
183
|
+
* the caller renders the warning instead. (`viewer_gone` never reaches here.)
|
|
162
184
|
*/
|
|
163
185
|
export declare function controlToReason(kind: ControlKind): PreviewEndReason | null;
|
|
164
186
|
/**
|
|
@@ -199,7 +221,7 @@ export declare function refusalToReason(status: number): PreviewEndReason | null
|
|
|
199
221
|
* reason maps to its own screen. A 410 with no reason gets the cause-free
|
|
200
222
|
* ending. See the note on PreviewEndedError.
|
|
201
223
|
*/
|
|
202
|
-
export declare function runHeartbeat(config: ResolvedConfig, sessionId: string, fetchImpl: typeof fetch, onEnded?: (reason: PreviewEndReason) => void): Promise<void>;
|
|
224
|
+
export declare function runHeartbeat(config: ResolvedConfig, sessionId: string, fetchImpl: typeof fetch, onEnded?: (reason: PreviewEndReason, refusal?: UpgradeFacts) => void): Promise<void>;
|
|
203
225
|
/**
|
|
204
226
|
* The API's `endReason` → the CLI's ending screen.
|
|
205
227
|
*
|
package/dist/preview/runner.js
CHANGED
|
@@ -26,6 +26,7 @@ import { withoutCliCredentials } from "../shared/config.js";
|
|
|
26
26
|
import { ProjectUnreachableError } from "../lib/backend.js";
|
|
27
27
|
import { AuthExpiredError } from "../shared/authError.js";
|
|
28
28
|
import { apiFetch } from "../shared/http.js";
|
|
29
|
+
import { UpgradeRequiredError, parseDeclared, parseMinimum, } from "../shared/upgradeError.js";
|
|
29
30
|
import { terminalSafe } from "../lib/terminalText.js";
|
|
30
31
|
/** The path a 404 advisory names: the request's own path, without its query. */
|
|
31
32
|
export function requestPathOf(url) {
|
|
@@ -63,6 +64,17 @@ export async function startPreview(opts) {
|
|
|
63
64
|
let heartbeat;
|
|
64
65
|
let minted;
|
|
65
66
|
let stopped = false;
|
|
67
|
+
// Set once the platform refuses this CLI's version: every later platform
|
|
68
|
+
// call would be refused too, so none is made.
|
|
69
|
+
let versionRefused = false;
|
|
70
|
+
const ended = (reason, refusal) => {
|
|
71
|
+
if (reason === "upgrade_required") {
|
|
72
|
+
versionRefused = true;
|
|
73
|
+
if (heartbeat !== undefined)
|
|
74
|
+
clearInterval(heartbeat);
|
|
75
|
+
}
|
|
76
|
+
opts.onEnded?.(reason, refusal);
|
|
77
|
+
};
|
|
66
78
|
const stop = async () => {
|
|
67
79
|
if (stopped)
|
|
68
80
|
return;
|
|
@@ -84,8 +96,9 @@ export async function startPreview(opts) {
|
|
|
84
96
|
// best-effort in intent even though it is in effect: the 12-hour ceiling
|
|
85
97
|
// and the edge's 60-second fail-closed deadline both reap a session whose
|
|
86
98
|
// DELETE never landed.
|
|
87
|
-
if (minted)
|
|
99
|
+
if (minted && !versionRefused) {
|
|
88
100
|
await endPreviewSession(config, minted.sessionId, fetchImpl);
|
|
101
|
+
}
|
|
89
102
|
};
|
|
90
103
|
try {
|
|
91
104
|
// The MINT comes first. Everything below it costs the creator a dev-server
|
|
@@ -231,7 +244,7 @@ export async function startPreview(opts) {
|
|
|
231
244
|
opts.onExpiring?.();
|
|
232
245
|
return;
|
|
233
246
|
}
|
|
234
|
-
|
|
247
|
+
ended(reason);
|
|
235
248
|
},
|
|
236
249
|
onOriginError: opts.onOriginError,
|
|
237
250
|
onStateChange: opts.onTunnelState,
|
|
@@ -241,8 +254,13 @@ export async function startPreview(opts) {
|
|
|
241
254
|
onReconnectRefused: (status) => {
|
|
242
255
|
const reason = refusalToReason(status);
|
|
243
256
|
if (reason !== null)
|
|
244
|
-
|
|
257
|
+
ended(reason);
|
|
245
258
|
},
|
|
259
|
+
onProtocolViolation: () => ended("protocol_violation"),
|
|
260
|
+
onRefused: (_reason, minimum, declared) => ended("upgrade_required", {
|
|
261
|
+
minimum: parseMinimum(minimum),
|
|
262
|
+
declared: parseDeclared(declared),
|
|
263
|
+
}),
|
|
246
264
|
});
|
|
247
265
|
tunnel.start();
|
|
248
266
|
// The heartbeat is keepalive AND re-authorization: every beat re-runs
|
|
@@ -252,10 +270,10 @@ export async function startPreview(opts) {
|
|
|
252
270
|
// One beat in flight at a time: a slow api must not stack up requests.
|
|
253
271
|
let beating = false;
|
|
254
272
|
heartbeat = setInterval(() => {
|
|
255
|
-
if (beating)
|
|
273
|
+
if (beating || versionRefused)
|
|
256
274
|
return;
|
|
257
275
|
beating = true;
|
|
258
|
-
void runHeartbeat(config, minted.sessionId, fetchImpl,
|
|
276
|
+
void runHeartbeat(config, minted.sessionId, fetchImpl, ended).finally(() => {
|
|
259
277
|
beating = false;
|
|
260
278
|
});
|
|
261
279
|
}, minted.heartbeatSec * 1000);
|
|
@@ -284,18 +302,24 @@ export async function startPreview(opts) {
|
|
|
284
302
|
}
|
|
285
303
|
}
|
|
286
304
|
/**
|
|
287
|
-
*
|
|
288
|
-
* hard-max warning, and it arrives on the same channel. Mapping it to
|
|
289
|
-
* would take away a link that has half an hour left, at exactly the
|
|
290
|
-
* creator is being warned to plan around it
|
|
291
|
-
* renders the warning instead.
|
|
305
|
+
* Three of the four kinds that reach onControl are endings. `expiring` is NOT:
|
|
306
|
+
* it is the hard-max warning, and it arrives on the same channel. Mapping it to
|
|
307
|
+
* an ending would take away a link that has half an hour left, at exactly the
|
|
308
|
+
* moment the creator is being warned to plan around it, so it returns null and
|
|
309
|
+
* the caller renders the warning instead. (`viewer_gone` never reaches here.)
|
|
292
310
|
*/
|
|
293
311
|
export function controlToReason(kind) {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
312
|
+
switch (kind) {
|
|
313
|
+
case "superseded":
|
|
314
|
+
return "superseded";
|
|
315
|
+
case "revoke":
|
|
316
|
+
return "authz_revoked";
|
|
317
|
+
case "authz_expired":
|
|
318
|
+
return "authz_lapsed";
|
|
319
|
+
case "expiring":
|
|
320
|
+
case "viewer_gone":
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
299
323
|
}
|
|
300
324
|
/**
|
|
301
325
|
* A refused RECONNECT dial → the ending screen it means.
|
|
@@ -346,6 +370,10 @@ export async function runHeartbeat(config, sessionId, fetchImpl, onEnded) {
|
|
|
346
370
|
await heartbeatPreviewSession(config, sessionId, fetchImpl);
|
|
347
371
|
}
|
|
348
372
|
catch (err) {
|
|
373
|
+
if (err instanceof UpgradeRequiredError) {
|
|
374
|
+
onEnded?.("upgrade_required", { minimum: err.minimum, declared: err.declared });
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
349
377
|
if (err instanceof AuthExpiredError) {
|
|
350
378
|
onEnded?.("auth_expired");
|
|
351
379
|
return;
|
package/dist/publish/api.js
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
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";
|
|
9
8
|
import { rejectAuthExpired } from "../shared/authError.js";
|
|
10
|
-
import { apiFetch } from "../shared/http.js";
|
|
9
|
+
import { apiFetch, apiRequest } from "../shared/http.js";
|
|
10
|
+
import { rejectUpgradeRequired } from "../shared/upgradeError.js";
|
|
11
11
|
/**
|
|
12
12
|
* The api's publish states, `domain.PublishState*`. apiPublishStates.test.ts
|
|
13
13
|
* checks this list against the committed api capture, so a state the api adds
|
|
@@ -82,8 +82,12 @@ export class PublishApiError extends Error {
|
|
|
82
82
|
this.name = "PublishApiError";
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* Rejects with UpgradeRequiredError on a 426 and AuthExpiredError on a 401;
|
|
87
|
+
* every other status becomes a PublishApiError.
|
|
88
|
+
*/
|
|
86
89
|
async function readError(res) {
|
|
90
|
+
await rejectUpgradeRequired(res);
|
|
87
91
|
rejectAuthExpired(res);
|
|
88
92
|
let body = {};
|
|
89
93
|
try {
|
|
@@ -95,14 +99,10 @@ async function readError(res) {
|
|
|
95
99
|
const retryAfter = res.headers.get("retry-after");
|
|
96
100
|
return new PublishApiError(res.status, body.code ?? `http_${res.status}`, body.message ?? `request failed (${res.status})`, body.detail ?? {}, retryAfter ? Number(retryAfter) : undefined);
|
|
97
101
|
}
|
|
98
|
-
|
|
99
|
-
return { ...clientHeaders(), authorization: `Bearer ${cfg.pat}` };
|
|
100
|
-
}
|
|
102
|
+
const AUTHENTICATED = { authenticated: true };
|
|
101
103
|
/** The ONE call before the checklist. liveVersion + livePublishedAt ride it, by design. */
|
|
102
104
|
export async function readProject(cfg, projectId, fetchImpl = apiFetch) {
|
|
103
|
-
const res = await
|
|
104
|
-
headers: authHeaders(cfg),
|
|
105
|
-
});
|
|
105
|
+
const res = await apiRequest(cfg, `/projects/${encodeURIComponent(projectId)}`, {}, AUTHENTICATED, fetchImpl);
|
|
106
106
|
if (!res.ok)
|
|
107
107
|
throw await readError(res);
|
|
108
108
|
const raw = (await res.json());
|
|
@@ -137,15 +137,15 @@ export async function readProject(cfg, projectId, fetchImpl = apiFetch) {
|
|
|
137
137
|
* and specific fact.
|
|
138
138
|
*/
|
|
139
139
|
export async function openUpload(cfg, projectId, files, note, fetchImpl = apiFetch, builtWith) {
|
|
140
|
-
const res = await
|
|
140
|
+
const res = await apiRequest(cfg, `/projects/${encodeURIComponent(projectId)}/publishes`, {
|
|
141
141
|
method: "POST",
|
|
142
|
-
headers: {
|
|
142
|
+
headers: { "content-type": "application/json" },
|
|
143
143
|
body: JSON.stringify({
|
|
144
144
|
files,
|
|
145
145
|
...(note ? { note } : {}),
|
|
146
146
|
...(builtWith ? { builtWith } : {}),
|
|
147
147
|
}),
|
|
148
|
-
});
|
|
148
|
+
}, AUTHENTICATED, fetchImpl);
|
|
149
149
|
if (!res.ok)
|
|
150
150
|
throw await readError(res);
|
|
151
151
|
const body = (await res.json());
|
|
@@ -159,7 +159,7 @@ export async function openUpload(cfg, projectId, files, note, fetchImpl = apiFet
|
|
|
159
159
|
};
|
|
160
160
|
}
|
|
161
161
|
export async function commitUpload(cfg, projectId, uploadId, fetchImpl = apiFetch) {
|
|
162
|
-
const res = await
|
|
162
|
+
const res = await apiRequest(cfg, `/projects/${encodeURIComponent(projectId)}/publishes/${encodeURIComponent(uploadId)}/commit`, { method: "POST" }, AUTHENTICATED, fetchImpl);
|
|
163
163
|
if (!res.ok)
|
|
164
164
|
throw await readError(res);
|
|
165
165
|
let raw;
|
|
@@ -174,7 +174,7 @@ export async function commitUpload(cfg, projectId, uploadId, fetchImpl = apiFetc
|
|
|
174
174
|
/** Abandon an open upload. Idempotent server-side; a failure here must never mask the real error. */
|
|
175
175
|
export async function abandonUpload(cfg, projectId, uploadId, fetchImpl = apiFetch) {
|
|
176
176
|
try {
|
|
177
|
-
await
|
|
177
|
+
await apiRequest(cfg, `/projects/${encodeURIComponent(projectId)}/publishes/${encodeURIComponent(uploadId)}`, { method: "DELETE" }, AUTHENTICATED, fetchImpl);
|
|
178
178
|
}
|
|
179
179
|
catch {
|
|
180
180
|
// Best-effort cleanup: the TTL reaps the row either way.
|
|
@@ -3,6 +3,7 @@ export declare const MAX_REOPENS = 2;
|
|
|
3
3
|
import type { ResolvedConfig } from "../shared/config.js";
|
|
4
4
|
import { type BuiltWith } from "../platform/resolve.js";
|
|
5
5
|
import { type VincenttJson } from "../lib/settings.js";
|
|
6
|
+
import { type ReadHead } from "./weight.js";
|
|
6
7
|
export interface PublishDeps {
|
|
7
8
|
print: (line: string) => void;
|
|
8
9
|
/** Raw write, for the redrawn progress line only. */
|
|
@@ -42,6 +43,8 @@ export interface PublishDeps {
|
|
|
42
43
|
probeFetch?: typeof fetch;
|
|
43
44
|
/** Where the --json document goes (stdout). Defaults to print. */
|
|
44
45
|
emit?: (line: string) => void;
|
|
46
|
+
/** The Heavy files section's image read, by output-relative path. Injected for tests; defaults to a read under the output folder. */
|
|
47
|
+
readHead?: ReadHead;
|
|
45
48
|
}
|
|
46
49
|
export interface PublishOptions {
|
|
47
50
|
distDir?: string;
|
package/dist/publish/command.js
CHANGED
|
@@ -15,19 +15,22 @@ import { buildManifest, ManifestError } from "./manifest.js";
|
|
|
15
15
|
import { checkBounds } from "./bounds.js";
|
|
16
16
|
import { classifyDist } from "./classify.js";
|
|
17
17
|
import { createProgress } from "./progress.js";
|
|
18
|
-
import { UploadExpired, UploadFailed, uploadAll } from "./upload.js";
|
|
18
|
+
import { UploadExpired, UploadFailed, UploadTargetRejected, UploadChecksumMismatch, uploadAll, } from "./upload.js";
|
|
19
|
+
import { escapeTerminal } from "../lib/terminalText.js";
|
|
19
20
|
/** Re-opens after an expired upload link, per publish, before giving up. */
|
|
20
21
|
export const MAX_REOPENS = 2;
|
|
21
22
|
import { waitUntilServing } from "./visibility.js";
|
|
22
23
|
import * as copy from "./copy.js";
|
|
23
24
|
import { publishedLimitRefused, resolveCountLimit, storageCeilingRefused, } from "../shared/countLimit.js";
|
|
24
25
|
import { AuthExpiredError, authExpiredLines } from "../shared/authError.js";
|
|
26
|
+
import { UpgradeRequiredError, upgradeRefusalLines, } from "../shared/upgradeError.js";
|
|
25
27
|
import { EXIT } from "../lib/exitCodes.js";
|
|
26
28
|
import { apiFetch, isTransportError } from "../shared/http.js";
|
|
27
29
|
import { resolveBuiltWith } from "../platform/resolve.js";
|
|
28
30
|
import { readSettings, resolveSettings, SettingsError, TEMPLATE_DEFAULTS, } from "../lib/settings.js";
|
|
29
31
|
import { settingsRefusal } from "../lib/settingsCopy.js";
|
|
30
32
|
import { rootAbsoluteFindings } from "./fit.js";
|
|
33
|
+
import { heavyFileFindings } from "./weight.js";
|
|
31
34
|
const COMMIT_RETRIES = 3;
|
|
32
35
|
const COMMIT_RETRY_DELAY_MS = 2000;
|
|
33
36
|
export async function publishCommand(deps, opts = {}) {
|
|
@@ -91,13 +94,11 @@ async function runPublish(deps, opts, onCommitted) {
|
|
|
91
94
|
}
|
|
92
95
|
catch (err) {
|
|
93
96
|
if (err instanceof ManifestError) {
|
|
94
|
-
|
|
97
|
+
const block = outputRefusal(err, output, buildCommand);
|
|
98
|
+
if (block !== undefined) {
|
|
95
99
|
deps.print(copy.headerLine(await projectLabel(deps, binding.slug), await orgOf(deps)));
|
|
96
100
|
deps.print(copy.readingLine(err.stats?.files ?? 0, err.stats?.bytes ?? 0, output));
|
|
97
|
-
const
|
|
98
|
-
? copy.secretFiles(err.paths, output)
|
|
99
|
-
: copy.lfsPointers(err.paths, output, buildCommand ?? undefined);
|
|
100
|
-
for (const l of lines)
|
|
101
|
+
for (const l of block)
|
|
101
102
|
deps.print(l);
|
|
102
103
|
return 1;
|
|
103
104
|
}
|
|
@@ -129,6 +130,11 @@ async function runPublish(deps, opts, onCommitted) {
|
|
|
129
130
|
project = await readProject(cfg, binding.projectId, fetchImpl);
|
|
130
131
|
}
|
|
131
132
|
catch (err) {
|
|
133
|
+
if (err instanceof UpgradeRequiredError) {
|
|
134
|
+
deps.print(copy.headerLine(await projectLabel(deps, binding.slug), await orgOf(deps)));
|
|
135
|
+
deps.print(copy.readingLine(manifest.files.length, manifest.totalBytes, output));
|
|
136
|
+
return refusedVersion(deps, err, { kind: "publish" });
|
|
137
|
+
}
|
|
132
138
|
if (err instanceof AuthExpiredError)
|
|
133
139
|
return authExpired(deps);
|
|
134
140
|
if (err instanceof PublishApiError && err.status === 404) {
|
|
@@ -172,6 +178,8 @@ async function runPublish(deps, opts, onCommitted) {
|
|
|
172
178
|
const manifestPaths = new Set(manifest.files.map((f) => f.path));
|
|
173
179
|
const findings = await classifyDist(distDir, manifestPaths, (abs) => fs.readFile(abs, "utf8"), (dir, rel) => path.join(dir, ...rel.split("/")), { expectedHost: address });
|
|
174
180
|
const rootAbsolute = rootAbsoluteFindings(await readHtmlFiles(distDir, manifest), manifestPaths);
|
|
181
|
+
// Warning-only: a fault in this section never changes the exit code.
|
|
182
|
+
const heavy = await heavyFileFindings(manifest.files, deps.readHead ?? ((rel, n) => readFileHead(path.join(distDir, ...rel.split("/")), n))).catch(() => []);
|
|
175
183
|
const checklistInput = {
|
|
176
184
|
projectName: project.name,
|
|
177
185
|
orgName,
|
|
@@ -182,6 +190,7 @@ async function runPublish(deps, opts, onCommitted) {
|
|
|
182
190
|
findings,
|
|
183
191
|
output,
|
|
184
192
|
rootAbsolute,
|
|
193
|
+
heavy,
|
|
185
194
|
};
|
|
186
195
|
let promptLine;
|
|
187
196
|
let defaultYes;
|
|
@@ -261,6 +270,9 @@ async function runPublish(deps, opts, onCommitted) {
|
|
|
261
270
|
async function runUpload(deps, cfg, i) {
|
|
262
271
|
const open = () => openUpload(cfg, i.projectId, i.manifest.files, i.note, i.fetchImpl, i.builtWith);
|
|
263
272
|
const openFailed = (err) => {
|
|
273
|
+
if (err instanceof UpgradeRequiredError) {
|
|
274
|
+
return refusedVersion(deps, err, { kind: "publish", address: i.address });
|
|
275
|
+
}
|
|
264
276
|
if (err instanceof AuthExpiredError)
|
|
265
277
|
return authExpired(deps);
|
|
266
278
|
for (const l of openErrorLines(err, i.address, i.liveVersion, i.org))
|
|
@@ -328,7 +340,12 @@ async function runUpload(deps, cfg, i) {
|
|
|
328
340
|
deps.print(l);
|
|
329
341
|
return 1;
|
|
330
342
|
}
|
|
331
|
-
const failedPath = err instanceof UploadFailed ||
|
|
343
|
+
const failedPath = err instanceof UploadFailed ||
|
|
344
|
+
err instanceof UploadExpired ||
|
|
345
|
+
err instanceof UploadTargetRejected ||
|
|
346
|
+
err instanceof UploadChecksumMismatch
|
|
347
|
+
? escapeTerminal(err.path)
|
|
348
|
+
: "a file";
|
|
332
349
|
deps.print("");
|
|
333
350
|
deps.print(`✗ Uploading ${failedPath} failed.`);
|
|
334
351
|
deps.print("");
|
|
@@ -365,6 +382,15 @@ async function commitWithHealing(deps, cfg, i, uploadId) {
|
|
|
365
382
|
return 0;
|
|
366
383
|
}
|
|
367
384
|
catch (err) {
|
|
385
|
+
// A version refusal is final: COMMIT_RETRIES is for transient failures,
|
|
386
|
+
// and each retry would be one more refused call.
|
|
387
|
+
if (err instanceof UpgradeRequiredError) {
|
|
388
|
+
return refusedVersion(deps, err, {
|
|
389
|
+
kind: "publish",
|
|
390
|
+
address: i.address,
|
|
391
|
+
stillServing: i.liveVersion,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
368
394
|
// The token can lapse during a long upload. The files are stored and the
|
|
369
395
|
// live address is unchanged, so signing in and publishing again is safe.
|
|
370
396
|
if (err instanceof AuthExpiredError)
|
|
@@ -533,6 +559,23 @@ function openErrorLines(err, address, liveVersion, org) {
|
|
|
533
559
|
function numericLimit(limit) {
|
|
534
560
|
return typeof limit === "number" ? limit : 0;
|
|
535
561
|
}
|
|
562
|
+
/** The refusals about what the output holds, printed after the header and reading line. */
|
|
563
|
+
function outputRefusal(err, output, buildCommand) {
|
|
564
|
+
switch (err.reason) {
|
|
565
|
+
case "secret_path":
|
|
566
|
+
return copy.secretFiles(err.paths, output);
|
|
567
|
+
case "path_escaped":
|
|
568
|
+
return copy.escapedFiles(err.paths, output);
|
|
569
|
+
case "credential_file":
|
|
570
|
+
return copy.credentialFiles(err.paths, output);
|
|
571
|
+
case "file_unreadable":
|
|
572
|
+
return copy.fileUnreadable(err.offendingPath, output);
|
|
573
|
+
case "lfs_pointer":
|
|
574
|
+
return copy.lfsPointers(err.paths, output, buildCommand ?? undefined);
|
|
575
|
+
default:
|
|
576
|
+
return undefined;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
536
579
|
function manifestErrorLines(err, distDir, ctx) {
|
|
537
580
|
if (err.reason === "no_dist")
|
|
538
581
|
return copy.noDist(distDir, ctx.buildCommand);
|
|
@@ -559,6 +602,24 @@ function outputLabelOf(root, distDir, source) {
|
|
|
559
602
|
: "";
|
|
560
603
|
return { folder, sourceNote };
|
|
561
604
|
}
|
|
605
|
+
/** The first `n` bytes of a file, or fewer if it is shorter. */
|
|
606
|
+
async function readFileHead(abs, n) {
|
|
607
|
+
const handle = await fs.open(abs, "r");
|
|
608
|
+
try {
|
|
609
|
+
const buf = Buffer.alloc(n);
|
|
610
|
+
let got = 0;
|
|
611
|
+
while (got < n) {
|
|
612
|
+
const { bytesRead } = await handle.read(buf, got, n - got, got);
|
|
613
|
+
if (bytesRead === 0)
|
|
614
|
+
break;
|
|
615
|
+
got += bytesRead;
|
|
616
|
+
}
|
|
617
|
+
return buf.subarray(0, got);
|
|
618
|
+
}
|
|
619
|
+
finally {
|
|
620
|
+
await handle.close().catch(() => undefined);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
562
623
|
/** Every `.html` in the manifest, for the root-absolute reference check. */
|
|
563
624
|
async function readHtmlFiles(distDir, manifest) {
|
|
564
625
|
const out = [];
|
|
@@ -588,6 +649,12 @@ function addressOf(project) {
|
|
|
588
649
|
return project.liveUrl.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
589
650
|
}
|
|
590
651
|
}
|
|
652
|
+
/** The platform refused this CLI's version. Nothing further is sent. */
|
|
653
|
+
function refusedVersion(deps, err, arm) {
|
|
654
|
+
for (const l of upgradeRefusalLines(err, arm))
|
|
655
|
+
deps.print(l);
|
|
656
|
+
return EXIT.UPGRADE_REQUIRED;
|
|
657
|
+
}
|
|
591
658
|
/** The api refused the token: the sign-in remedy, and the not-signed-in exit code. */
|
|
592
659
|
function authExpired(deps) {
|
|
593
660
|
for (const l of authExpiredLines())
|
package/dist/publish/copy.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { FileFindings } from "./classify.js";
|
|
2
2
|
import type { BoundViolation } from "./bounds.js";
|
|
3
3
|
import type { RootAbsoluteFinding } from "./fit.js";
|
|
4
|
+
import type { HeavyFinding } from "./weight.js";
|
|
4
5
|
import type { Offender } from "./manifest.js";
|
|
5
6
|
export declare function truncate(value: string, max?: number): string;
|
|
6
7
|
/** Bytes as the creator reads them. Decimal MB, matching what a build tool prints. */
|
|
@@ -46,6 +47,8 @@ export interface ChecklistInput {
|
|
|
46
47
|
output?: OutputLabel;
|
|
47
48
|
/** Root-absolute references that do not resolve. The section is absent when empty. */
|
|
48
49
|
rootAbsolute?: RootAbsoluteFinding[];
|
|
50
|
+
/** Models and images over the weight limits. The section is absent when empty. */
|
|
51
|
+
heavy?: HeavyFinding[];
|
|
49
52
|
}
|
|
50
53
|
/**
|
|
51
54
|
* The `Paths from the root` section: a second section of the checklist, same
|
|
@@ -53,6 +56,8 @@ export interface ChecklistInput {
|
|
|
53
56
|
* empty heading. One explanation for the whole section.
|
|
54
57
|
*/
|
|
55
58
|
export declare function pathsFromRootLines(findings: RootAbsoluteFinding[] | undefined, folder?: string): string[];
|
|
59
|
+
/** The `Heavy files` section: warning-only, absent when there are no findings. */
|
|
60
|
+
export declare function heavyFileLines(findings: HeavyFinding[] | undefined): string[];
|
|
56
61
|
/**
|
|
57
62
|
* The output folder as the creator reads it, `dist/` for the template. The
|
|
58
63
|
* source is appended only when it is not the default, so the template's
|
|
@@ -171,6 +176,16 @@ export declare function outputOutside(shown: string): string[];
|
|
|
171
176
|
export declare function outputInHidden(shown: string, hidden: string): string[];
|
|
172
177
|
/** Flow U1 — hidden names or symlinks that would leak. After the header and reading line. */
|
|
173
178
|
export declare function secretFiles(paths: Offender[], output?: OutputLabel): string[];
|
|
179
|
+
/**
|
|
180
|
+
* Flow U1 — files whose folder resolved outside the output after the walk: a
|
|
181
|
+
* folder was swapped for a link while publish read it. The whole publish is
|
|
182
|
+
* refused, never just the file. After the header and reading line.
|
|
183
|
+
*/
|
|
184
|
+
export declare function escapedFiles(paths: Offender[], output?: OutputLabel): string[];
|
|
185
|
+
/** Flow U1 — credential files, from the early checks or the hash step. After the header and reading line. */
|
|
186
|
+
export declare function credentialFiles(paths: Offender[], output?: OutputLabel): string[];
|
|
187
|
+
/** Flow U1 — a file that changed, became a link, or could not be read while checked or hashed. */
|
|
188
|
+
export declare function fileUnreadable(filePath: string, output?: OutputLabel): string[];
|
|
174
189
|
/** Flow U1 — Git LFS pointer files. The recorded build.command, verbatim, when there is one. */
|
|
175
190
|
export declare function lfsPointers(paths: Offender[], output?: OutputLabel, buildCommand?: string): string[];
|
|
176
191
|
/** A local path rule the creator's own build broke. Local, before any request. */
|