@vincentt-xr/cli 0.20.0 → 0.21.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 +15 -0
- package/dist/cli/index.js +63 -6
- package/dist/domain/api.js +16 -12
- package/dist/domain/command.js +9 -0
- package/dist/dx/command.js +3 -0
- package/dist/dx/consent.js +10 -14
- 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/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 +8 -5
- package/dist/preview/copy.d.ts +8 -1
- package/dist/preview/copy.js +16 -3
- package/dist/preview/proxy.js +5 -39
- package/dist/preview/runner.d.ts +25 -9
- package/dist/preview/runner.js +42 -15
- package/dist/publish/api.js +14 -14
- package/dist/publish/command.d.ts +3 -0
- package/dist/publish/command.js +71 -7
- package/dist/publish/copy.d.ts +9 -0
- package/dist/publish/copy.js +83 -14
- package/dist/publish/keyScan.d.ts +25 -0
- package/dist/publish/keyScan.js +88 -0
- package/dist/publish/manifest.d.ts +51 -19
- package/dist/publish/manifest.js +246 -63
- package/dist/publish/upload.d.ts +8 -0
- package/dist/publish/upload.js +15 -0
- package/dist/publish/weight.d.ts +23 -0
- package/dist/publish/weight.js +113 -0
- 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 +48 -2
- package/dist/tunnel/client.js +157 -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.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,12 @@ 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
|
+
onRefused: (_reason, minimum, declared) => ended("upgrade_required", {
|
|
260
|
+
minimum: parseMinimum(minimum),
|
|
261
|
+
declared: parseDeclared(declared),
|
|
262
|
+
}),
|
|
246
263
|
});
|
|
247
264
|
tunnel.start();
|
|
248
265
|
// The heartbeat is keepalive AND re-authorization: every beat re-runs
|
|
@@ -252,10 +269,10 @@ export async function startPreview(opts) {
|
|
|
252
269
|
// One beat in flight at a time: a slow api must not stack up requests.
|
|
253
270
|
let beating = false;
|
|
254
271
|
heartbeat = setInterval(() => {
|
|
255
|
-
if (beating)
|
|
272
|
+
if (beating || versionRefused)
|
|
256
273
|
return;
|
|
257
274
|
beating = true;
|
|
258
|
-
void runHeartbeat(config, minted.sessionId, fetchImpl,
|
|
275
|
+
void runHeartbeat(config, minted.sessionId, fetchImpl, ended).finally(() => {
|
|
259
276
|
beating = false;
|
|
260
277
|
});
|
|
261
278
|
}, minted.heartbeatSec * 1000);
|
|
@@ -284,18 +301,24 @@ export async function startPreview(opts) {
|
|
|
284
301
|
}
|
|
285
302
|
}
|
|
286
303
|
/**
|
|
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.
|
|
304
|
+
* Three of the four kinds that reach onControl are endings. `expiring` is NOT:
|
|
305
|
+
* it is the hard-max warning, and it arrives on the same channel. Mapping it to
|
|
306
|
+
* an ending would take away a link that has half an hour left, at exactly the
|
|
307
|
+
* moment the creator is being warned to plan around it, so it returns null and
|
|
308
|
+
* the caller renders the warning instead. (`viewer_gone` never reaches here.)
|
|
292
309
|
*/
|
|
293
310
|
export function controlToReason(kind) {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
311
|
+
switch (kind) {
|
|
312
|
+
case "superseded":
|
|
313
|
+
return "superseded";
|
|
314
|
+
case "revoke":
|
|
315
|
+
return "authz_revoked";
|
|
316
|
+
case "authz_expired":
|
|
317
|
+
return "authz_lapsed";
|
|
318
|
+
case "expiring":
|
|
319
|
+
case "viewer_gone":
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
299
322
|
}
|
|
300
323
|
/**
|
|
301
324
|
* A refused RECONNECT dial → the ending screen it means.
|
|
@@ -346,6 +369,10 @@ export async function runHeartbeat(config, sessionId, fetchImpl, onEnded) {
|
|
|
346
369
|
await heartbeatPreviewSession(config, sessionId, fetchImpl);
|
|
347
370
|
}
|
|
348
371
|
catch (err) {
|
|
372
|
+
if (err instanceof UpgradeRequiredError) {
|
|
373
|
+
onEnded?.("upgrade_required", { minimum: err.minimum, declared: err.declared });
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
349
376
|
if (err instanceof AuthExpiredError) {
|
|
350
377
|
onEnded?.("auth_expired");
|
|
351
378
|
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, 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,11 @@ 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
|
+
? escapeTerminal(err.path)
|
|
347
|
+
: "a file";
|
|
332
348
|
deps.print("");
|
|
333
349
|
deps.print(`✗ Uploading ${failedPath} failed.`);
|
|
334
350
|
deps.print("");
|
|
@@ -365,6 +381,15 @@ async function commitWithHealing(deps, cfg, i, uploadId) {
|
|
|
365
381
|
return 0;
|
|
366
382
|
}
|
|
367
383
|
catch (err) {
|
|
384
|
+
// A version refusal is final: COMMIT_RETRIES is for transient failures,
|
|
385
|
+
// and each retry would be one more refused call.
|
|
386
|
+
if (err instanceof UpgradeRequiredError) {
|
|
387
|
+
return refusedVersion(deps, err, {
|
|
388
|
+
kind: "publish",
|
|
389
|
+
address: i.address,
|
|
390
|
+
stillServing: i.liveVersion,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
368
393
|
// The token can lapse during a long upload. The files are stored and the
|
|
369
394
|
// live address is unchanged, so signing in and publishing again is safe.
|
|
370
395
|
if (err instanceof AuthExpiredError)
|
|
@@ -533,6 +558,21 @@ function openErrorLines(err, address, liveVersion, org) {
|
|
|
533
558
|
function numericLimit(limit) {
|
|
534
559
|
return typeof limit === "number" ? limit : 0;
|
|
535
560
|
}
|
|
561
|
+
/** The refusals about what the output holds, printed after the header and reading line. */
|
|
562
|
+
function outputRefusal(err, output, buildCommand) {
|
|
563
|
+
switch (err.reason) {
|
|
564
|
+
case "secret_path":
|
|
565
|
+
return copy.secretFiles(err.paths, output);
|
|
566
|
+
case "credential_file":
|
|
567
|
+
return copy.credentialFiles(err.paths, output);
|
|
568
|
+
case "file_unreadable":
|
|
569
|
+
return copy.fileUnreadable(err.offendingPath, output);
|
|
570
|
+
case "lfs_pointer":
|
|
571
|
+
return copy.lfsPointers(err.paths, output, buildCommand ?? undefined);
|
|
572
|
+
default:
|
|
573
|
+
return undefined;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
536
576
|
function manifestErrorLines(err, distDir, ctx) {
|
|
537
577
|
if (err.reason === "no_dist")
|
|
538
578
|
return copy.noDist(distDir, ctx.buildCommand);
|
|
@@ -559,6 +599,24 @@ function outputLabelOf(root, distDir, source) {
|
|
|
559
599
|
: "";
|
|
560
600
|
return { folder, sourceNote };
|
|
561
601
|
}
|
|
602
|
+
/** The first `n` bytes of a file, or fewer if it is shorter. */
|
|
603
|
+
async function readFileHead(abs, n) {
|
|
604
|
+
const handle = await fs.open(abs, "r");
|
|
605
|
+
try {
|
|
606
|
+
const buf = Buffer.alloc(n);
|
|
607
|
+
let got = 0;
|
|
608
|
+
while (got < n) {
|
|
609
|
+
const { bytesRead } = await handle.read(buf, got, n - got, got);
|
|
610
|
+
if (bytesRead === 0)
|
|
611
|
+
break;
|
|
612
|
+
got += bytesRead;
|
|
613
|
+
}
|
|
614
|
+
return buf.subarray(0, got);
|
|
615
|
+
}
|
|
616
|
+
finally {
|
|
617
|
+
await handle.close().catch(() => undefined);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
562
620
|
/** Every `.html` in the manifest, for the root-absolute reference check. */
|
|
563
621
|
async function readHtmlFiles(distDir, manifest) {
|
|
564
622
|
const out = [];
|
|
@@ -588,6 +646,12 @@ function addressOf(project) {
|
|
|
588
646
|
return project.liveUrl.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
589
647
|
}
|
|
590
648
|
}
|
|
649
|
+
/** The platform refused this CLI's version. Nothing further is sent. */
|
|
650
|
+
function refusedVersion(deps, err, arm) {
|
|
651
|
+
for (const l of upgradeRefusalLines(err, arm))
|
|
652
|
+
deps.print(l);
|
|
653
|
+
return EXIT.UPGRADE_REQUIRED;
|
|
654
|
+
}
|
|
591
655
|
/** The api refused the token: the sign-in remedy, and the not-signed-in exit code. */
|
|
592
656
|
function authExpired(deps) {
|
|
593
657
|
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,10 @@ 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
|
+
/** Flow U1 — credential files, from the early checks or the hash step. After the header and reading line. */
|
|
180
|
+
export declare function credentialFiles(paths: Offender[], output?: OutputLabel): string[];
|
|
181
|
+
/** Flow U1 — a file that changed, became a link, or could not be read while checked or hashed. */
|
|
182
|
+
export declare function fileUnreadable(filePath: string, output?: OutputLabel): string[];
|
|
174
183
|
/** Flow U1 — Git LFS pointer files. The recorded build.command, verbatim, when there is one. */
|
|
175
184
|
export declare function lfsPointers(paths: Offender[], output?: OutputLabel, buildCommand?: string): string[];
|
|
176
185
|
/** A local path rule the creator's own build broke. Local, before any request. */
|
package/dist/publish/copy.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// NEVER PRINTED, at any point, in any state: the project id, the org id, the
|
|
13
13
|
// uploadId, the PAT, any presigned URL or fragment of one, the bucket, any S3
|
|
14
14
|
// key, or a per-file upload log.
|
|
15
|
-
import {
|
|
15
|
+
import { escapeTerminal } from "../lib/terminalText.js";
|
|
16
16
|
/** The only horizontal rules in any f4 output. They exist here and nowhere else. */
|
|
17
17
|
const RULE = " ─────────────────────────────────────────────────────────────────────";
|
|
18
18
|
const LABELS = {
|
|
@@ -166,11 +166,33 @@ export function pathsFromRootLines(findings, folder = "dist/") {
|
|
|
166
166
|
out.push("", " Vincentt serves this app at the root of its address. A missing path that", " starts with / usually means the build was made for a subpath, such as a", " GitHub Pages project site. Nothing here blocks you.", "");
|
|
167
167
|
return out;
|
|
168
168
|
}
|
|
169
|
+
/** The `Heavy files` section: warning-only, absent when there are no findings. */
|
|
170
|
+
export function heavyFileLines(findings) {
|
|
171
|
+
if (!findings || findings.length === 0)
|
|
172
|
+
return [];
|
|
173
|
+
const out = [" Heavy files (each one downloads to the phone before it can show)", ""];
|
|
174
|
+
for (const f of findings.slice(0, FINDINGS_SHOWN)) {
|
|
175
|
+
const name = escapeTerminal(f.path);
|
|
176
|
+
if (f.kind === "model") {
|
|
177
|
+
out.push(` ! ${name} is ${formatBytes(f.bytes)}`);
|
|
178
|
+
out.push(`${" ".repeat(21)}a model over 10 MB`);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
out.push(` ! ${name} is ${f.width} x ${f.height} px`);
|
|
182
|
+
out.push(`${" ".repeat(21)}an image over 2048 px on its long side`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (findings.length > FINDINGS_SHOWN) {
|
|
186
|
+
out.push(` and ${findings.length - FINDINGS_SHOWN} more`);
|
|
187
|
+
}
|
|
188
|
+
out.push("", " Every phone that opens this app downloads these files over its own", " connection. Compress the model or shrink the image, then build again.", " Nothing here blocks you.", "");
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
169
191
|
const DEFAULT_OUTPUT = { folder: "dist/", sourceNote: "" };
|
|
170
192
|
export function readingLine(fileCount, totalBytes, output = DEFAULT_OUTPUT) {
|
|
171
193
|
const note = output.sourceNote ? ` ${output.sourceNote}` : "";
|
|
172
194
|
// The folder is the creator's build.output or --dist, so it is escaped.
|
|
173
|
-
return `→ Reading ${
|
|
195
|
+
return `→ Reading ${escapeTerminal(output.folder)}${note} … ${fileCount} files · ${formatBytes(totalBytes)}`;
|
|
174
196
|
}
|
|
175
197
|
export function headerLine(projectName, orgName) {
|
|
176
198
|
return orgName ? `→ ${projectName} · ${orgName}` : `→ ${projectName}`;
|
|
@@ -211,6 +233,7 @@ export function firstPublishChecklist(i) {
|
|
|
211
233
|
...findingLines(i.findings, true, i.output?.folder),
|
|
212
234
|
...nothingBlocksYou(),
|
|
213
235
|
...pathsFromRootLines(i.rootAbsolute, i.output?.folder),
|
|
236
|
+
...heavyFileLines(i.heavy),
|
|
214
237
|
` Publishing v${i.nextVersion} · ${i.fileCount} files · ${formatBytes(i.totalBytes)}`,
|
|
215
238
|
"",
|
|
216
239
|
];
|
|
@@ -255,6 +278,7 @@ export function republishChecklist(i) {
|
|
|
255
278
|
"",
|
|
256
279
|
...findingLines(i.findings, false, i.output?.folder),
|
|
257
280
|
...pathsFromRootLines(i.rootAbsolute, i.output?.folder),
|
|
281
|
+
...heavyFileLines(i.heavy),
|
|
258
282
|
];
|
|
259
283
|
}
|
|
260
284
|
/** `[Y/n]` — on a republish an accidental Enter does the thing the creator typed the command to do. */
|
|
@@ -279,6 +303,7 @@ export function offlineChecklist(i) {
|
|
|
279
303
|
"",
|
|
280
304
|
...findingLines(i.findings, false, i.output?.folder),
|
|
281
305
|
...pathsFromRootLines(i.rootAbsolute, i.output?.folder),
|
|
306
|
+
...heavyFileLines(i.heavy),
|
|
282
307
|
];
|
|
283
308
|
}
|
|
284
309
|
export function offlinePrompt(version) {
|
|
@@ -303,6 +328,7 @@ export function notServingChecklist(i, stateKnown) {
|
|
|
303
328
|
"",
|
|
304
329
|
...findingLines(i.findings, false, i.output?.folder),
|
|
305
330
|
...pathsFromRootLines(i.rootAbsolute, i.output?.folder),
|
|
331
|
+
...heavyFileLines(i.heavy),
|
|
306
332
|
];
|
|
307
333
|
}
|
|
308
334
|
/** `[y/N]`: the CLI cannot say what this publish changes, so Enter does nothing. */
|
|
@@ -455,7 +481,7 @@ export function noDist(distDir, buildCommand) {
|
|
|
455
481
|
"",
|
|
456
482
|
" Nothing was uploaded.",
|
|
457
483
|
"",
|
|
458
|
-
].map(
|
|
484
|
+
].map(escapeTerminal);
|
|
459
485
|
}
|
|
460
486
|
export function noIndexHtml(distDir, buildCommand) {
|
|
461
487
|
return [
|
|
@@ -469,7 +495,7 @@ export function noIndexHtml(distDir, buildCommand) {
|
|
|
469
495
|
"",
|
|
470
496
|
" Nothing was uploaded.",
|
|
471
497
|
"",
|
|
472
|
-
].map(
|
|
498
|
+
].map(escapeTerminal);
|
|
473
499
|
}
|
|
474
500
|
/** Flow U1 — the output is the project folder itself. */
|
|
475
501
|
export function outputIsRoot(o) {
|
|
@@ -487,13 +513,13 @@ export function outputIsRoot(o) {
|
|
|
487
513
|
"",
|
|
488
514
|
" Nothing was uploaded.",
|
|
489
515
|
"",
|
|
490
|
-
].map(
|
|
516
|
+
].map(escapeTerminal);
|
|
491
517
|
}
|
|
492
518
|
/** Flow U1 — the output lies outside the project. `shown` is project-relative. */
|
|
493
519
|
export function outputOutside(shown) {
|
|
494
520
|
return [
|
|
495
521
|
"",
|
|
496
|
-
`✗ The build output ${
|
|
522
|
+
`✗ The build output ${escapeTerminal(shown)} is outside this project.`,
|
|
497
523
|
"",
|
|
498
524
|
" Publish uploads a folder inside the project. Set --dist, or build.output in",
|
|
499
525
|
" vincentt.json, to the folder your build writes.",
|
|
@@ -506,7 +532,7 @@ export function outputOutside(shown) {
|
|
|
506
532
|
export function outputInHidden(shown, hidden) {
|
|
507
533
|
return [
|
|
508
534
|
"",
|
|
509
|
-
`✗ The build output ${
|
|
535
|
+
`✗ The build output ${escapeTerminal(shown)} is inside ${hidden}/, not a build.`,
|
|
510
536
|
"",
|
|
511
537
|
" Publishing it would put your repository's history or its Vincentt link at a",
|
|
512
538
|
" public address. Set --dist, or build.output in vincentt.json, to the folder",
|
|
@@ -516,18 +542,30 @@ export function outputInHidden(shown, hidden) {
|
|
|
516
542
|
"",
|
|
517
543
|
];
|
|
518
544
|
}
|
|
545
|
+
/** Where a credential reason starts, unless a longer path pushes it right. */
|
|
546
|
+
const WHY_COLUMN = 40;
|
|
547
|
+
const CREDENTIAL_WHY = {
|
|
548
|
+
name: "named like a credential",
|
|
549
|
+
private_key: "holds a private key",
|
|
550
|
+
service_account: "holds a service-account key",
|
|
551
|
+
authorized_user: "holds a Google sign-in credential",
|
|
552
|
+
};
|
|
519
553
|
function offenderList(paths, folder) {
|
|
520
554
|
// File names and the folder prefix come from the build and the creator's
|
|
521
555
|
// settings, and may carry control characters.
|
|
522
|
-
folder =
|
|
556
|
+
folder = escapeTerminal(folder);
|
|
523
557
|
const shown = paths.slice(0, FINDINGS_SHOWN).map((p) => ({
|
|
524
|
-
path:
|
|
525
|
-
target: p.target === undefined ? undefined :
|
|
558
|
+
path: escapeTerminal(p.path),
|
|
559
|
+
target: p.target === undefined ? undefined : escapeTerminal(p.target),
|
|
560
|
+
why: p.why,
|
|
526
561
|
}));
|
|
527
562
|
const width = Math.max(...shown.map((p) => folder.length + p.path.length));
|
|
563
|
+
const whyColumn = Math.max(WHY_COLUMN, width + 3);
|
|
528
564
|
const out = shown.map((p) => p.target !== undefined
|
|
529
565
|
? ` ${(folder + p.path).padEnd(width)} links to ${p.target}`
|
|
530
|
-
:
|
|
566
|
+
: p.why !== undefined
|
|
567
|
+
? ` ${(folder + p.path).padEnd(whyColumn)}${CREDENTIAL_WHY[p.why]}`
|
|
568
|
+
: ` ${folder}${p.path}`);
|
|
531
569
|
if (paths.length > FINDINGS_SHOWN)
|
|
532
570
|
out.push(` and ${paths.length - FINDINGS_SHOWN} more`);
|
|
533
571
|
return out;
|
|
@@ -540,13 +578,44 @@ export function secretFiles(paths, output = DEFAULT_OUTPUT) {
|
|
|
540
578
|
"",
|
|
541
579
|
...offenderList(paths, output.folder),
|
|
542
580
|
"",
|
|
543
|
-
` Everything in ${
|
|
581
|
+
` Everything in ${escapeTerminal(output.folder)} becomes public at your address. Remove these from the build`,
|
|
544
582
|
" output, rebuild, then publish.",
|
|
545
583
|
"",
|
|
546
584
|
" Nothing was uploaded.",
|
|
547
585
|
"",
|
|
548
586
|
];
|
|
549
587
|
}
|
|
588
|
+
/** Flow U1 — credential files, from the early checks or the hash step. After the header and reading line. */
|
|
589
|
+
export function credentialFiles(paths, output = DEFAULT_OUTPUT) {
|
|
590
|
+
return [
|
|
591
|
+
"",
|
|
592
|
+
"✗ This build holds files that look like private keys.",
|
|
593
|
+
"",
|
|
594
|
+
...offenderList(paths, output.folder),
|
|
595
|
+
"",
|
|
596
|
+
` Everything in ${escapeTerminal(output.folder)} becomes public at your address, and a published version`,
|
|
597
|
+
" cannot be taken back. Move these out of the build output, rebuild, then",
|
|
598
|
+
" publish. A file only named like a key, such as a Keynote .key, needs a new",
|
|
599
|
+
" name.",
|
|
600
|
+
"",
|
|
601
|
+
" Nothing was uploaded.",
|
|
602
|
+
"",
|
|
603
|
+
];
|
|
604
|
+
}
|
|
605
|
+
/** Flow U1 — a file that changed, became a link, or could not be read while checked or hashed. */
|
|
606
|
+
export function fileUnreadable(filePath, output = DEFAULT_OUTPUT) {
|
|
607
|
+
return [
|
|
608
|
+
"",
|
|
609
|
+
`✗ ${escapeTerminal(output.folder + filePath)} changed while publish was reading it.`,
|
|
610
|
+
"",
|
|
611
|
+
" It became a link or a different file, or could not be read, so publish",
|
|
612
|
+
" cannot vouch for what it would upload. Let your build finish, then publish",
|
|
613
|
+
" again.",
|
|
614
|
+
"",
|
|
615
|
+
" Nothing was uploaded.",
|
|
616
|
+
"",
|
|
617
|
+
];
|
|
618
|
+
}
|
|
550
619
|
/** Flow U1 — Git LFS pointer files. The recorded build.command, verbatim, when there is one. */
|
|
551
620
|
export function lfsPointers(paths, output = DEFAULT_OUTPUT, buildCommand) {
|
|
552
621
|
return [
|
|
@@ -560,7 +629,7 @@ export function lfsPointers(paths, output = DEFAULT_OUTPUT, buildCommand) {
|
|
|
560
629
|
"",
|
|
561
630
|
" git lfs pull",
|
|
562
631
|
buildCommand !== undefined
|
|
563
|
-
? ` ${
|
|
632
|
+
? ` ${escapeTerminal(buildCommand)}`
|
|
564
633
|
: " then run your build again",
|
|
565
634
|
"",
|
|
566
635
|
" Nothing was uploaded.",
|
|
@@ -581,7 +650,7 @@ export function badPath(reason, offending) {
|
|
|
581
650
|
"",
|
|
582
651
|
`✗ This build ${why[reason] ?? "has a path we can't publish"}.`,
|
|
583
652
|
"",
|
|
584
|
-
` ${
|
|
653
|
+
` ${escapeTerminal(offending)}`,
|
|
585
654
|
"",
|
|
586
655
|
" Rename it in your source, rebuild, then publish again.",
|
|
587
656
|
"",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** The label is bounded to 40 characters so the carried tail stays finite. */
|
|
2
|
+
export declare const KEY_HEADER: RegExp;
|
|
3
|
+
/**
|
|
4
|
+
* A newline (real, or the two-character `\n` or four-character `\r\n` of a
|
|
5
|
+
* string literal), then a base64 line.
|
|
6
|
+
*/
|
|
7
|
+
export declare const KEY_BODY: RegExp;
|
|
8
|
+
/** Below this offset a header alone is a match. */
|
|
9
|
+
export declare const HEAD_BYTES = 4096;
|
|
10
|
+
/** How far past a header its key body may sit. */
|
|
11
|
+
export declare const BODY_WINDOW = 256;
|
|
12
|
+
export interface KeyScanner {
|
|
13
|
+
/** Feed the next chunk, as latin1. */
|
|
14
|
+
push(chunk: string): void;
|
|
15
|
+
/** Judge any header still waiting on its body window. True when a key was seen. */
|
|
16
|
+
end(): boolean;
|
|
17
|
+
readonly matched: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare function createKeyScanner(): KeyScanner;
|
|
20
|
+
export type CredentialJsonType = "service_account" | "authorized_user";
|
|
21
|
+
/**
|
|
22
|
+
* The top-level `type` of a Google credential JSON, or undefined. A parse
|
|
23
|
+
* failure is "not a match" and its message is discarded: it echoes the input.
|
|
24
|
+
*/
|
|
25
|
+
export declare function credentialJson(bytes: Uint8Array): CredentialJsonType | undefined;
|