@vincentt-xr/cli 0.17.0 → 0.18.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 +24 -0
- package/README.md +94 -1
- package/dist/cli/index.js +170 -84
- package/dist/cli/usage-text.js +32 -10
- package/dist/domain/api.d.ts +10 -8
- package/dist/domain/api.js +14 -9
- package/dist/domain/command.js +25 -16
- package/dist/domain/copy.d.ts +6 -3
- package/dist/domain/copy.js +10 -6
- package/dist/dx/command.d.ts +24 -4
- package/dist/dx/command.js +80 -25
- package/dist/dx/consent.js +6 -5
- package/dist/dx/journal.d.ts +17 -2
- package/dist/dx/journal.js +19 -3
- package/dist/dx/report.js +4 -3
- package/dist/dx/reportIo.js +6 -5
- package/dist/dx/send.js +6 -5
- package/dist/lib/backend.d.ts +0 -16
- package/dist/lib/backend.js +2 -26
- package/dist/lib/detect.d.ts +87 -0
- package/dist/lib/detect.js +867 -0
- package/dist/lib/diagnostics.js +2 -2
- package/dist/lib/feedback.js +1 -1
- package/dist/lib/feedbackCopy.js +1 -1
- package/dist/lib/initFrom.js +4 -4
- package/dist/lib/lifecycle.d.ts +2 -0
- package/dist/lib/lifecycle.js +9 -3
- package/dist/lib/listProjects.js +2 -1
- package/dist/lib/projects.d.ts +37 -2
- package/dist/lib/projects.js +86 -21
- package/dist/lib/settings.d.ts +113 -0
- package/dist/lib/settings.js +284 -0
- package/dist/lib/settingsCopy.d.ts +6 -0
- package/dist/lib/settingsCopy.js +99 -0
- package/dist/lib/settle.d.ts +61 -0
- package/dist/lib/settle.js +338 -0
- package/dist/lib/template.d.ts +19 -0
- package/dist/lib/template.js +73 -0
- package/dist/lib/terminalText.d.ts +3 -0
- package/dist/lib/terminalText.js +10 -0
- package/dist/lib/version.js +5 -11
- package/dist/lib/whoami.js +10 -3
- package/dist/library/assetsCommand.js +1 -1
- package/dist/library/client.js +8 -3
- package/dist/library/render.js +8 -6
- package/dist/login/authBackend.js +3 -2
- package/dist/login/ensure.d.ts +8 -2
- package/dist/login/ensure.js +21 -10
- package/dist/login/guard.js +5 -4
- package/dist/login/loginCommand.d.ts +2 -2
- package/dist/login/loginCommand.js +29 -5
- package/dist/login/pkce.js +1 -1
- package/dist/login/pkceLogin.js +1 -1
- package/dist/platform/outdated.js +2 -2
- package/dist/platform/upgradeReport.js +16 -12
- package/dist/preview/api.d.ts +5 -11
- package/dist/preview/api.js +13 -15
- package/dist/preview/command.d.ts +29 -0
- package/dist/preview/command.js +61 -1
- package/dist/preview/copy.d.ts +35 -3
- package/dist/preview/copy.js +102 -10
- package/dist/preview/proxy.d.ts +38 -1
- package/dist/preview/proxy.js +151 -2
- package/dist/preview/qrPage.js +1 -1
- package/dist/preview/runner.d.ts +25 -13
- package/dist/preview/runner.js +127 -26
- package/dist/preview/viewers.js +1 -1
- package/dist/publish/api.d.ts +22 -5
- package/dist/publish/api.js +80 -9
- package/dist/publish/command.d.ts +25 -0
- package/dist/publish/command.js +211 -35
- package/dist/publish/copy.d.ts +78 -5
- package/dist/publish/copy.js +271 -37
- package/dist/publish/fit.d.ts +15 -0
- package/dist/publish/fit.js +103 -0
- package/dist/publish/manifest.d.ts +44 -3
- package/dist/publish/manifest.js +172 -3
- package/dist/publish/prompt.d.ts +4 -0
- package/dist/publish/prompt.js +15 -1
- package/dist/publish/upload.d.ts +6 -0
- package/dist/publish/upload.js +11 -1
- package/dist/publish/visibility.d.ts +21 -0
- package/dist/publish/visibility.js +68 -0
- package/dist/relay/annotations.js +9 -4
- package/dist/relay/server.d.ts +21 -0
- package/dist/relay/server.js +35 -1
- package/dist/shared/authError.d.ts +27 -0
- package/dist/shared/authError.js +48 -4
- package/dist/shared/config.d.ts +21 -1
- package/dist/shared/config.js +39 -8
- package/dist/shared/countLimit.d.ts +7 -2
- package/dist/shared/countLimit.js +12 -5
- package/dist/shared/http.d.ts +23 -0
- package/dist/shared/http.js +58 -0
- package/package.json +7 -3
package/dist/publish/command.js
CHANGED
|
@@ -10,25 +10,67 @@
|
|
|
10
10
|
// branch is assertable without a real API, a real terminal or a real S3.
|
|
11
11
|
import { promises as fs } from "node:fs";
|
|
12
12
|
import path from "node:path";
|
|
13
|
-
import { PublishApiError, abandonUpload, commitUpload, openUpload, readProject, } from "./api.js";
|
|
13
|
+
import { MalformedCommitResponse, PublishApiError, abandonUpload, commitUpload, openUpload, readProject, } from "./api.js";
|
|
14
14
|
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
18
|
import { UploadFailed, uploadAll } from "./upload.js";
|
|
19
|
+
import { waitUntilServing } from "./visibility.js";
|
|
19
20
|
import * as copy from "./copy.js";
|
|
20
21
|
import { publishedLimitRefused, resolveCountLimit, storageCeilingRefused, } from "../shared/countLimit.js";
|
|
22
|
+
import { AuthExpiredError, authExpiredLines } from "../shared/authError.js";
|
|
23
|
+
import { EXIT } from "../lib/exitCodes.js";
|
|
24
|
+
import { apiFetch, isTransportError } from "../shared/http.js";
|
|
21
25
|
import { resolveBuiltWith } from "../platform/resolve.js";
|
|
26
|
+
import { readSettings, resolveSettings, SettingsError, TEMPLATE_DEFAULTS, } from "../lib/settings.js";
|
|
27
|
+
import { settingsRefusal } from "../lib/settingsCopy.js";
|
|
28
|
+
import { rootAbsoluteFindings } from "./fit.js";
|
|
22
29
|
const COMMIT_RETRIES = 3;
|
|
23
30
|
const COMMIT_RETRY_DELAY_MS = 2000;
|
|
24
31
|
export async function publishCommand(deps, opts = {}) {
|
|
32
|
+
let committed;
|
|
33
|
+
const code = await runPublish(deps, opts, (r) => {
|
|
34
|
+
committed = r;
|
|
35
|
+
});
|
|
36
|
+
if (opts.json) {
|
|
37
|
+
const report = code === 0 && committed ? committed : { state: "failed", exitCode: code };
|
|
38
|
+
(deps.emit ?? deps.print)(JSON.stringify(report));
|
|
39
|
+
}
|
|
40
|
+
return code;
|
|
41
|
+
}
|
|
42
|
+
async function runPublish(deps, opts, onCommitted) {
|
|
43
|
+
// The binding lookup is local. Its directory is the project root: the settings
|
|
44
|
+
// file is read there and a relative output or --dist resolves against it,
|
|
45
|
+
// never against the working directory.
|
|
46
|
+
const binding = await deps.binding();
|
|
47
|
+
const root = binding?.dir ?? deps.cwd;
|
|
48
|
+
// Settings resolve first, before the sign-in gate and before any request.
|
|
49
|
+
let settings;
|
|
50
|
+
let hasFile;
|
|
51
|
+
try {
|
|
52
|
+
const file = await (deps.readSettings ?? readSettings)(root);
|
|
53
|
+
hasFile = file !== undefined;
|
|
54
|
+
settings = resolveSettings({ buildOutput: opts.distDir }, file, TEMPLATE_DEFAULTS, [
|
|
55
|
+
"build",
|
|
56
|
+
]);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
if (err instanceof SettingsError) {
|
|
60
|
+
for (const l of settingsRefusal(err, "publish"))
|
|
61
|
+
deps.print(l);
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
// `undefined` → the template's own command; `null` → a file records none.
|
|
67
|
+
const buildCommand = settings.buildCommand?.value ?? (hasFile ? null : undefined);
|
|
25
68
|
// The gate is LOCAL and NOTHING IS ATTEMPTED — no request, no partial upload.
|
|
26
69
|
if (!(await deps.token())) {
|
|
27
70
|
for (const l of copy.notSignedIn())
|
|
28
71
|
deps.print(l);
|
|
29
|
-
return
|
|
72
|
+
return EXIT.NOT_AUTHENTICATED;
|
|
30
73
|
}
|
|
31
|
-
const binding = await deps.binding();
|
|
32
74
|
if (!binding) {
|
|
33
75
|
deps.print("✗ This folder isn't linked to a project.");
|
|
34
76
|
deps.print("");
|
|
@@ -36,18 +78,34 @@ export async function publishCommand(deps, opts = {}) {
|
|
|
36
78
|
deps.print(" vincentt create Register a new project for this folder");
|
|
37
79
|
return 1;
|
|
38
80
|
}
|
|
39
|
-
const distDir = path.resolve(
|
|
81
|
+
const distDir = path.resolve(root, settings.buildOutput.value);
|
|
82
|
+
const output = outputLabelOf(root, distDir, settings.buildOutput.source);
|
|
40
83
|
// Everything below the manifest build happens before a single request. The
|
|
41
84
|
// creator learns about a too-large build in the same second they would
|
|
42
85
|
// otherwise have started a ten-minute upload.
|
|
43
86
|
let manifest;
|
|
44
87
|
try {
|
|
45
|
-
manifest = await buildManifest(distDir);
|
|
88
|
+
manifest = await buildManifest(distDir, { repoRoot: root });
|
|
46
89
|
}
|
|
47
90
|
catch (err) {
|
|
48
91
|
if (err instanceof ManifestError) {
|
|
49
|
-
|
|
92
|
+
if (err.reason === "secret_path" || err.reason === "lfs_pointer") {
|
|
93
|
+
deps.print(copy.headerLine(await projectLabel(deps, binding.slug), await orgOf(deps)));
|
|
94
|
+
deps.print(copy.readingLine(err.stats?.files ?? 0, err.stats?.bytes ?? 0, output));
|
|
95
|
+
const lines = err.reason === "secret_path"
|
|
96
|
+
? copy.secretFiles(err.paths, output)
|
|
97
|
+
: copy.lfsPointers(err.paths, output, buildCommand ?? undefined);
|
|
98
|
+
for (const l of lines)
|
|
99
|
+
deps.print(l);
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
const origin = {
|
|
103
|
+
value: settings.buildOutput.value,
|
|
104
|
+
source: settings.buildOutput.source,
|
|
105
|
+
};
|
|
106
|
+
for (const l of manifestErrorLines(err, distDir, { root, origin, buildCommand })) {
|
|
50
107
|
deps.print(l);
|
|
108
|
+
}
|
|
51
109
|
return 1;
|
|
52
110
|
}
|
|
53
111
|
throw err;
|
|
@@ -55,13 +113,13 @@ export async function publishCommand(deps, opts = {}) {
|
|
|
55
113
|
const violation = checkBounds(manifest);
|
|
56
114
|
if (violation) {
|
|
57
115
|
deps.print(copy.headerLine(await projectLabel(deps, binding.slug), await orgOf(deps)));
|
|
58
|
-
deps.print(copy.readingLine(manifest.files.length, manifest.totalBytes));
|
|
116
|
+
deps.print(copy.readingLine(manifest.files.length, manifest.totalBytes, output));
|
|
59
117
|
for (const l of copy.tooLarge(violation))
|
|
60
118
|
deps.print(l);
|
|
61
119
|
return 1;
|
|
62
120
|
}
|
|
63
121
|
const cfg = await deps.config();
|
|
64
|
-
const fetchImpl = deps.fetchImpl ??
|
|
122
|
+
const fetchImpl = deps.fetchImpl ?? apiFetch;
|
|
65
123
|
// ONE call before the checklist. liveVersion and livePublishedAt ride it, so
|
|
66
124
|
// the republish line costs no second round trip.
|
|
67
125
|
let project;
|
|
@@ -69,6 +127,8 @@ export async function publishCommand(deps, opts = {}) {
|
|
|
69
127
|
project = await readProject(cfg, binding.projectId, fetchImpl);
|
|
70
128
|
}
|
|
71
129
|
catch (err) {
|
|
130
|
+
if (err instanceof AuthExpiredError)
|
|
131
|
+
return authExpired(deps);
|
|
72
132
|
if (err instanceof PublishApiError && err.status === 404) {
|
|
73
133
|
deps.print(`✗ ${err.message}`);
|
|
74
134
|
deps.print("");
|
|
@@ -82,6 +142,20 @@ export async function publishCommand(deps, opts = {}) {
|
|
|
82
142
|
const isFirstPublish = project.firstPublishedAt === null;
|
|
83
143
|
const nextVersion = (project.highestVersion ?? 0) + 1;
|
|
84
144
|
const orgName = await orgOf(deps);
|
|
145
|
+
// Refused here, before the prompt and before a byte moves: the api refuses
|
|
146
|
+
// every publish of a held project, and it used to do so only at commit.
|
|
147
|
+
if (project.publishState === "held_by_platform") {
|
|
148
|
+
deps.print("");
|
|
149
|
+
deps.print(copy.headerLine(project.name, orgName));
|
|
150
|
+
deps.print(copy.readingLine(manifest.files.length, manifest.totalBytes, output));
|
|
151
|
+
for (const l of copy.heldByPlatform(address, {
|
|
152
|
+
orgSlug: (await deps.orgSlug?.()) || undefined,
|
|
153
|
+
consoleUrl: deps.consoleUrl,
|
|
154
|
+
})) {
|
|
155
|
+
deps.print(l);
|
|
156
|
+
}
|
|
157
|
+
return 1;
|
|
158
|
+
}
|
|
85
159
|
// A non-interactive stdin with no --yes must REFUSE rather than assume yes.
|
|
86
160
|
if (!opts.yes && !deps.isTTY) {
|
|
87
161
|
deps.print("");
|
|
@@ -93,7 +167,9 @@ export async function publishCommand(deps, opts = {}) {
|
|
|
93
167
|
deps.print("");
|
|
94
168
|
return 1;
|
|
95
169
|
}
|
|
96
|
-
const
|
|
170
|
+
const manifestPaths = new Set(manifest.files.map((f) => f.path));
|
|
171
|
+
const findings = await classifyDist(distDir, manifestPaths, (abs) => fs.readFile(abs, "utf8"), (dir, rel) => path.join(dir, ...rel.split("/")), { expectedHost: address });
|
|
172
|
+
const rootAbsolute = rootAbsoluteFindings(await readHtmlFiles(distDir, manifest), manifestPaths);
|
|
97
173
|
const checklistInput = {
|
|
98
174
|
projectName: project.name,
|
|
99
175
|
orgName,
|
|
@@ -102,6 +178,8 @@ export async function publishCommand(deps, opts = {}) {
|
|
|
102
178
|
totalBytes: manifest.totalBytes,
|
|
103
179
|
nextVersion,
|
|
104
180
|
findings,
|
|
181
|
+
output,
|
|
182
|
+
rootAbsolute,
|
|
105
183
|
};
|
|
106
184
|
let promptLine;
|
|
107
185
|
let defaultYes;
|
|
@@ -119,6 +197,16 @@ export async function publishCommand(deps, opts = {}) {
|
|
|
119
197
|
// accidental Enter must do nothing.
|
|
120
198
|
defaultYes = false;
|
|
121
199
|
}
|
|
200
|
+
else if (project.publishState !== "live") {
|
|
201
|
+
// not_serving, or a state newer than this CLI. Only `live` may say "Live at".
|
|
202
|
+
const known = project.publishState === "not_serving";
|
|
203
|
+
for (const l of copy.notServingChecklist(checklistInput, known))
|
|
204
|
+
deps.print(l);
|
|
205
|
+
promptLine = known
|
|
206
|
+
? copy.republishPrompt(nextVersion)
|
|
207
|
+
: copy.unknownStatePrompt(nextVersion);
|
|
208
|
+
defaultYes = known;
|
|
209
|
+
}
|
|
122
210
|
else {
|
|
123
211
|
for (const l of copy.republishChecklist({
|
|
124
212
|
...checklistInput,
|
|
@@ -154,12 +242,18 @@ export async function publishCommand(deps, opts = {}) {
|
|
|
154
242
|
address,
|
|
155
243
|
liveVersion: project.liveVersion ?? undefined,
|
|
156
244
|
fetchImpl,
|
|
245
|
+
// The S3 leg sets its own per-file deadline, sized to the file.
|
|
246
|
+
uploadFetch: deps.fetchImpl ?? ((input, init) => globalThis.fetch(input, init)),
|
|
157
247
|
builtWith,
|
|
158
248
|
org: {
|
|
159
249
|
name: orgName || undefined,
|
|
160
250
|
slug: (await deps.orgSlug?.()) || undefined,
|
|
161
251
|
consoleUrl: deps.consoleUrl,
|
|
162
252
|
},
|
|
253
|
+
probeFetch: deps.probeFetch ??
|
|
254
|
+
deps.fetchImpl ??
|
|
255
|
+
((input, init) => globalThis.fetch(input, init)),
|
|
256
|
+
onCommitted,
|
|
163
257
|
});
|
|
164
258
|
}
|
|
165
259
|
async function runUpload(deps, cfg, i) {
|
|
@@ -168,6 +262,8 @@ async function runUpload(deps, cfg, i) {
|
|
|
168
262
|
opened = await openUpload(cfg, i.projectId, i.manifest.files, i.note, i.fetchImpl, i.builtWith);
|
|
169
263
|
}
|
|
170
264
|
catch (err) {
|
|
265
|
+
if (err instanceof AuthExpiredError)
|
|
266
|
+
return authExpired(deps);
|
|
171
267
|
for (const l of openErrorLines(err, i.address, i.liveVersion, i.org))
|
|
172
268
|
deps.print(l);
|
|
173
269
|
return 1;
|
|
@@ -186,7 +282,7 @@ async function runUpload(deps, cfg, i) {
|
|
|
186
282
|
targets: opened.uploads,
|
|
187
283
|
manifest: i.manifest.files,
|
|
188
284
|
onProgress: (p) => progress.update(p),
|
|
189
|
-
fetchImpl: i.
|
|
285
|
+
fetchImpl: i.uploadFetch,
|
|
190
286
|
});
|
|
191
287
|
}
|
|
192
288
|
catch (err) {
|
|
@@ -202,8 +298,8 @@ async function runUpload(deps, cfg, i) {
|
|
|
202
298
|
deps.print(`✗ Uploading ${failedPath} failed.`);
|
|
203
299
|
deps.print("");
|
|
204
300
|
deps.print(i.liveVersion === undefined
|
|
205
|
-
? ` Your live address is unchanged
|
|
206
|
-
: ` Your live address is unchanged
|
|
301
|
+
? ` Your live address is unchanged: ${i.address}.`
|
|
302
|
+
: ` Your live address is unchanged: ${i.address} is still serving v${i.liveVersion}.`);
|
|
207
303
|
deps.print("");
|
|
208
304
|
deps.print(" Publish again: vincentt publish");
|
|
209
305
|
deps.print("");
|
|
@@ -215,30 +311,41 @@ async function runUpload(deps, cfg, i) {
|
|
|
215
311
|
return commitWithHealing(deps, cfg, i, opened.uploadId);
|
|
216
312
|
}
|
|
217
313
|
/**
|
|
218
|
-
* The commit, with the healing retry.
|
|
219
|
-
* automatically
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
314
|
+
* The commit, with the healing retry. Commit is idempotent on uploadId, so two
|
|
315
|
+
* failures are retried automatically, up to 3 attempts with the wait doubling
|
|
316
|
+
* from 2s: a 502 publish_flip_failed (what the healing path was built for), and
|
|
317
|
+
* a request whose answer never arrived (a dropped connection, a deadline, a 2xx
|
|
318
|
+
* body that is not a commit response). Surfacing either to a creator whose bytes
|
|
319
|
+
* are all safely stored would be manufacturing an incident.
|
|
320
|
+
*
|
|
321
|
+
* When the last attempt's answer never arrived, the outcome is unknown and the
|
|
322
|
+
* copy says so: the version may already be live.
|
|
223
323
|
*/
|
|
224
324
|
async function commitWithHealing(deps, cfg, i, uploadId) {
|
|
225
325
|
let announcedSlow = false;
|
|
226
326
|
for (let attempt = 1; attempt <= COMMIT_RETRIES; attempt++) {
|
|
227
327
|
try {
|
|
228
328
|
const result = await commitUpload(cfg, i.projectId, uploadId, i.fetchImpl);
|
|
229
|
-
|
|
230
|
-
deps.print(l);
|
|
329
|
+
await reportVisibility(deps, i, result);
|
|
231
330
|
return 0;
|
|
232
331
|
}
|
|
233
332
|
catch (err) {
|
|
234
|
-
|
|
235
|
-
|
|
333
|
+
// The token can lapse during a long upload. The files are stored and the
|
|
334
|
+
// live address is unchanged, so signing in and publishing again is safe.
|
|
335
|
+
if (err instanceof AuthExpiredError)
|
|
336
|
+
return authExpired(deps);
|
|
337
|
+
const unanswered = err instanceof MalformedCommitResponse || isTransportError(err);
|
|
236
338
|
// The plan limit is a 409 and falls past this branch deliberately. It is
|
|
237
339
|
// the interlock refusing, not the flip failing, and retrying it three
|
|
238
340
|
// times would be the retry loop the refusal exists to stop.
|
|
239
|
-
|
|
341
|
+
const flipFailed = err instanceof PublishApiError &&
|
|
342
|
+
(err.code === "publish_flip_failed" || err.status === 502);
|
|
343
|
+
if (unanswered || flipFailed) {
|
|
240
344
|
if (attempt === COMMIT_RETRIES) {
|
|
241
|
-
|
|
345
|
+
const lines = unanswered
|
|
346
|
+
? copy.commitOutcomeUnknown(i.address, i.liveVersion)
|
|
347
|
+
: copy.flipFailed(i.address, i.liveVersion);
|
|
348
|
+
for (const l of lines)
|
|
242
349
|
deps.print(l);
|
|
243
350
|
return 1;
|
|
244
351
|
}
|
|
@@ -248,9 +355,11 @@ async function commitWithHealing(deps, cfg, i, uploadId) {
|
|
|
248
355
|
deps.print(copy.takingLonger());
|
|
249
356
|
announcedSlow = true;
|
|
250
357
|
}
|
|
251
|
-
await deps.sleep(COMMIT_RETRY_DELAY_MS);
|
|
358
|
+
await deps.sleep(COMMIT_RETRY_DELAY_MS * 2 ** (attempt - 1));
|
|
252
359
|
continue;
|
|
253
360
|
}
|
|
361
|
+
if (!(err instanceof PublishApiError))
|
|
362
|
+
throw err;
|
|
254
363
|
for (const l of commitErrorLines(err, i.address, i.liveVersion, i.org))
|
|
255
364
|
deps.print(l);
|
|
256
365
|
return 1;
|
|
@@ -258,6 +367,32 @@ async function commitWithHealing(deps, cfg, i, uploadId) {
|
|
|
258
367
|
}
|
|
259
368
|
return 1;
|
|
260
369
|
}
|
|
370
|
+
/**
|
|
371
|
+
* "Live" is printed only once the public address serves this commit's index.html.
|
|
372
|
+
* Running out the wait is still exit 0: the commit happened, only the edge lags.
|
|
373
|
+
*/
|
|
374
|
+
async function reportVisibility(deps, i, result) {
|
|
375
|
+
const index = i.manifest.files.find((f) => f.path === "index.html");
|
|
376
|
+
const { visible, waitedMs } = index
|
|
377
|
+
? await waitUntilServing(result.liveUrl, index.sha256, {
|
|
378
|
+
fetchImpl: i.probeFetch,
|
|
379
|
+
now: deps.now,
|
|
380
|
+
sleep: deps.sleep,
|
|
381
|
+
onWaiting: () => deps.print(copy.propagating(result.liveUrl, result.version)),
|
|
382
|
+
})
|
|
383
|
+
: { visible: true, waitedMs: 0 };
|
|
384
|
+
const lines = visible
|
|
385
|
+
? copy.liveAt(result.liveUrl, result.version)
|
|
386
|
+
: copy.notYetVisible(result.liveUrl, result.version);
|
|
387
|
+
for (const l of lines)
|
|
388
|
+
deps.print(l);
|
|
389
|
+
i.onCommitted({
|
|
390
|
+
state: visible ? "live" : "committed_not_visible",
|
|
391
|
+
version: result.version,
|
|
392
|
+
liveUrl: result.liveUrl,
|
|
393
|
+
waitedMs,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
261
396
|
/**
|
|
262
397
|
* The plan-limit refusal, from the detail the API sends. It is reachable from
|
|
263
398
|
* open AND from commit, because the interlock lives at the port and every flip
|
|
@@ -284,6 +419,10 @@ function commitErrorLines(err, address, liveVersion, org) {
|
|
|
284
419
|
switch (err.code) {
|
|
285
420
|
case "unavailable_plan_limit":
|
|
286
421
|
return planLimitLines(err, org);
|
|
422
|
+
case "published_limit":
|
|
423
|
+
// Two opens admitted in the same org race to go live; commit re-checks and
|
|
424
|
+
// refuses the loser, whose version the api keeps stored.
|
|
425
|
+
return publishedLimitRefused(resolveCountLimit(err.detail, org), "commit");
|
|
287
426
|
case "publish_expired":
|
|
288
427
|
return copy.publishExpired(address, liveVersion);
|
|
289
428
|
case "publish_contended":
|
|
@@ -298,8 +437,8 @@ function commitErrorLines(err, address, liveVersion, org) {
|
|
|
298
437
|
`✗ ${err.message}`,
|
|
299
438
|
"",
|
|
300
439
|
liveVersion === undefined
|
|
301
|
-
? ` Your live address is unchanged
|
|
302
|
-
: ` Your live address is unchanged
|
|
440
|
+
? ` Your live address is unchanged: ${address}.`
|
|
441
|
+
: ` Your live address is unchanged: ${address} is still serving v${liveVersion}.`,
|
|
303
442
|
"",
|
|
304
443
|
" Publish again: vincentt publish",
|
|
305
444
|
"",
|
|
@@ -319,12 +458,10 @@ function openErrorLines(err, address, liveVersion, org) {
|
|
|
319
458
|
if (err.code === "unavailable_plan_limit") {
|
|
320
459
|
return planLimitLines(err, org);
|
|
321
460
|
}
|
|
322
|
-
// The two count/ceiling refusals of this change. Both land at OPEN — before a
|
|
323
|
-
// presigned URL is minted and before a byte moves — which is why they are here
|
|
324
|
-
// and not in commitErrorLines: neither is reachable from commit.
|
|
325
461
|
if (err.code === "published_limit") {
|
|
326
|
-
return publishedLimitRefused(resolveCountLimit(err.detail, org));
|
|
462
|
+
return publishedLimitRefused(resolveCountLimit(err.detail, org), "open");
|
|
327
463
|
}
|
|
464
|
+
// Refused only at open, before a presigned URL is minted.
|
|
328
465
|
if (err.code === "storage_ceiling") {
|
|
329
466
|
// It takes no detail. The body is `{"limit":"storage"}` and names nothing
|
|
330
467
|
// else by design — the ceiling is an abuse ceiling, not a plan limit.
|
|
@@ -345,8 +482,8 @@ function openErrorLines(err, address, liveVersion, org) {
|
|
|
345
482
|
`✗ ${err.message}`,
|
|
346
483
|
"",
|
|
347
484
|
liveVersion === undefined
|
|
348
|
-
? ` Your live address is unchanged
|
|
349
|
-
: ` Your live address is unchanged
|
|
485
|
+
? ` Your live address is unchanged: ${address}.`
|
|
486
|
+
: ` Your live address is unchanged: ${address} is still serving v${liveVersion}.`,
|
|
350
487
|
"",
|
|
351
488
|
" Nothing was uploaded.",
|
|
352
489
|
"",
|
|
@@ -361,13 +498,46 @@ function openErrorLines(err, address, liveVersion, org) {
|
|
|
361
498
|
function numericLimit(limit) {
|
|
362
499
|
return typeof limit === "number" ? limit : 0;
|
|
363
500
|
}
|
|
364
|
-
function manifestErrorLines(err, distDir) {
|
|
501
|
+
function manifestErrorLines(err, distDir, ctx) {
|
|
365
502
|
if (err.reason === "no_dist")
|
|
366
|
-
return copy.noDist(distDir);
|
|
503
|
+
return copy.noDist(distDir, ctx.buildCommand);
|
|
367
504
|
if (err.reason === "no_index_html")
|
|
368
|
-
return copy.noIndexHtml(distDir);
|
|
505
|
+
return copy.noIndexHtml(distDir, ctx.buildCommand);
|
|
506
|
+
if (err.reason === "output_is_repo_root")
|
|
507
|
+
return copy.outputIsRoot(ctx.origin);
|
|
508
|
+
if (err.reason === "output_in_hidden") {
|
|
509
|
+
return copy.outputInHidden(path.relative(ctx.root, distDir).split(path.sep).join("/"), err.offendingPath);
|
|
510
|
+
}
|
|
511
|
+
if (err.reason === "output_outside_repo") {
|
|
512
|
+
return copy.outputOutside(path.relative(ctx.root, distDir).split(path.sep).join("/"));
|
|
513
|
+
}
|
|
369
514
|
return copy.badPath(err.reason, err.offendingPath);
|
|
370
515
|
}
|
|
516
|
+
/** `dist/` for the default; the resolved folder and its source otherwise. */
|
|
517
|
+
function outputLabelOf(root, distDir, source) {
|
|
518
|
+
const rel = path.relative(root, distDir).split(path.sep).join("/");
|
|
519
|
+
const folder = `${rel || "."}/`;
|
|
520
|
+
const sourceNote = source === "vincentt.json"
|
|
521
|
+
? "(from vincentt.json)"
|
|
522
|
+
: source === "flag"
|
|
523
|
+
? "(from --dist)"
|
|
524
|
+
: "";
|
|
525
|
+
return { folder, sourceNote };
|
|
526
|
+
}
|
|
527
|
+
/** Every `.html` in the manifest, for the root-absolute reference check. */
|
|
528
|
+
async function readHtmlFiles(distDir, manifest) {
|
|
529
|
+
const out = [];
|
|
530
|
+
for (const f of manifest.files) {
|
|
531
|
+
if (!f.path.toLowerCase().endsWith(".html"))
|
|
532
|
+
continue;
|
|
533
|
+
const html = await fs
|
|
534
|
+
.readFile(path.join(distDir, ...f.path.split("/")), "utf8")
|
|
535
|
+
.catch(() => undefined);
|
|
536
|
+
if (html !== undefined)
|
|
537
|
+
out.push({ file: f.path, html });
|
|
538
|
+
}
|
|
539
|
+
return out;
|
|
540
|
+
}
|
|
371
541
|
/**
|
|
372
542
|
* The address as a creator reads it: the host, never the scheme. `liveUrl` is
|
|
373
543
|
* server-computed (the apex differs per environment), so it is the authority and
|
|
@@ -383,6 +553,12 @@ function addressOf(project) {
|
|
|
383
553
|
return project.liveUrl.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
384
554
|
}
|
|
385
555
|
}
|
|
556
|
+
/** The api refused the token: the sign-in remedy, and the not-signed-in exit code. */
|
|
557
|
+
function authExpired(deps) {
|
|
558
|
+
for (const l of authExpiredLines())
|
|
559
|
+
deps.print(l);
|
|
560
|
+
return EXIT.NOT_AUTHENTICATED;
|
|
561
|
+
}
|
|
386
562
|
async function orgOf(deps) {
|
|
387
563
|
return (await deps.orgName?.()) ?? "";
|
|
388
564
|
}
|
package/dist/publish/copy.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { FileFindings } from "./classify.js";
|
|
2
2
|
import type { BoundViolation } from "./bounds.js";
|
|
3
|
+
import type { RootAbsoluteFinding } from "./fit.js";
|
|
4
|
+
import type { Offender } from "./manifest.js";
|
|
3
5
|
export declare function truncate(value: string, max?: number): string;
|
|
4
6
|
/** Bytes as the creator reads them. Decimal MB, matching what a build tool prints. */
|
|
5
7
|
export declare function formatBytes(bytes: number): string;
|
|
@@ -24,7 +26,7 @@ export declare function relativeTime(iso: string, now: number): string;
|
|
|
24
26
|
* A FINDING IS NEVER A COUNT. No "2 issues found", no summary line, and no exit
|
|
25
27
|
* code that varies with findings.
|
|
26
28
|
*/
|
|
27
|
-
export declare function findingLines(byFile: FileFindings[], withSubtitle: boolean): string[];
|
|
29
|
+
export declare function findingLines(byFile: FileFindings[], withSubtitle: boolean, folder?: string): string[];
|
|
28
30
|
/**
|
|
29
31
|
* Mandatory and always present. A screen with a red ✗ on it reads as a gate
|
|
30
32
|
* unless it explicitly says otherwise, and a creator who believes the platform
|
|
@@ -40,8 +42,29 @@ export interface ChecklistInput {
|
|
|
40
42
|
totalBytes: number;
|
|
41
43
|
nextVersion: number;
|
|
42
44
|
findings: FileFindings[];
|
|
45
|
+
/** Absent for the template's default `dist/`. */
|
|
46
|
+
output?: OutputLabel;
|
|
47
|
+
/** Root-absolute references that do not resolve. The section is absent when empty. */
|
|
48
|
+
rootAbsolute?: RootAbsoluteFinding[];
|
|
43
49
|
}
|
|
44
|
-
|
|
50
|
+
/**
|
|
51
|
+
* The `Paths from the root` section: a second section of the checklist, same
|
|
52
|
+
* geometry as the link preview. Absent when there are no findings, never an
|
|
53
|
+
* empty heading. One explanation for the whole section.
|
|
54
|
+
*/
|
|
55
|
+
export declare function pathsFromRootLines(findings: RootAbsoluteFinding[] | undefined, folder?: string): string[];
|
|
56
|
+
/**
|
|
57
|
+
* The output folder as the creator reads it, `dist/` for the template. The
|
|
58
|
+
* source is appended only when it is not the default, so the template's
|
|
59
|
+
* transcript is unchanged.
|
|
60
|
+
*/
|
|
61
|
+
export interface OutputLabel {
|
|
62
|
+
/** Project-relative, with a trailing slash: `dist/`, `build/`. */
|
|
63
|
+
folder: string;
|
|
64
|
+
/** `(from vincentt.json)` / `(from --dist)`, or empty for the default. */
|
|
65
|
+
sourceNote: string;
|
|
66
|
+
}
|
|
67
|
+
export declare function readingLine(fileCount: number, totalBytes: number, output?: OutputLabel): string;
|
|
45
68
|
export declare function headerLine(projectName: string, orgName: string): string;
|
|
46
69
|
/**
|
|
47
70
|
* Flow L22 — the single most consequential screen in the product, and its ORDER
|
|
@@ -88,6 +111,28 @@ export declare function republishPrompt(version: number): string;
|
|
|
88
111
|
*/
|
|
89
112
|
export declare function offlineChecklist(i: ChecklistInput): string[];
|
|
90
113
|
export declare function offlinePrompt(version: number): string;
|
|
114
|
+
/**
|
|
115
|
+
* The address has published before but serves nothing now (`not_serving`), or
|
|
116
|
+
* the api reports a state this CLI does not know. Neither may say "Live at":
|
|
117
|
+
* the creator would share an address that answers with nothing.
|
|
118
|
+
*/
|
|
119
|
+
export declare function notServingChecklist(i: ChecklistInput, stateKnown: boolean): string[];
|
|
120
|
+
/** `[y/N]`: the CLI cannot say what this publish changes, so Enter does nothing. */
|
|
121
|
+
export declare function unknownStatePrompt(version: number): string;
|
|
122
|
+
/**
|
|
123
|
+
* The project is `held_by_platform`: Vincentt took the org's addresses offline
|
|
124
|
+
* over its traffic allowance, and the api refuses every publish until they are
|
|
125
|
+
* restored. Refused BEFORE the upload, because uploading a whole build only to
|
|
126
|
+
* be refused at commit is the retry loop this copy exists to stop.
|
|
127
|
+
*
|
|
128
|
+
* The project read carries no figures, so this names none. It shares
|
|
129
|
+
* planLimitRefused's voice: the platform is the actor, the remedy is outside
|
|
130
|
+
* the CLI, and nothing was lost.
|
|
131
|
+
*/
|
|
132
|
+
export declare function heldByPlatform(address: string, org?: {
|
|
133
|
+
orgSlug?: string;
|
|
134
|
+
consoleUrl?: string;
|
|
135
|
+
}): string[];
|
|
91
136
|
/**
|
|
92
137
|
* f1's line verbatim, with f4's reason. Note what is ABSENT, deliberately: f3's
|
|
93
138
|
* "your local dev loop doesn't need one" paragraph. It is the right copy for
|
|
@@ -110,9 +155,24 @@ export declare function notSignedIn(): string[];
|
|
|
110
155
|
* have.
|
|
111
156
|
*/
|
|
112
157
|
export declare function tooLarge(v: BoundViolation): string[];
|
|
113
|
-
|
|
114
|
-
export declare function
|
|
115
|
-
|
|
158
|
+
export declare function noDist(distDir: string, buildCommand?: string | null): string[];
|
|
159
|
+
export declare function noIndexHtml(distDir: string, buildCommand?: string | null): string[];
|
|
160
|
+
/** How the output was named, for the leak refusals' "why" sentence. */
|
|
161
|
+
export interface OutputOrigin {
|
|
162
|
+
/** The value as given: `build.output` or `--dist`. */
|
|
163
|
+
value: string;
|
|
164
|
+
source: "vincentt.json" | "flag" | "default";
|
|
165
|
+
}
|
|
166
|
+
/** Flow U1 — the output is the project folder itself. */
|
|
167
|
+
export declare function outputIsRoot(o: OutputOrigin): string[];
|
|
168
|
+
/** Flow U1 — the output lies outside the project. `shown` is project-relative. */
|
|
169
|
+
export declare function outputOutside(shown: string): string[];
|
|
170
|
+
/** Flow U1 — the output is, or sits inside, `.git` or `.vincentt`. `shown` is project-relative. */
|
|
171
|
+
export declare function outputInHidden(shown: string, hidden: string): string[];
|
|
172
|
+
/** Flow U1 — hidden names or symlinks that would leak. After the header and reading line. */
|
|
173
|
+
export declare function secretFiles(paths: Offender[], output?: OutputLabel): string[];
|
|
174
|
+
/** Flow U1 — Git LFS pointer files. The recorded build.command, verbatim, when there is one. */
|
|
175
|
+
export declare function lfsPointers(paths: Offender[], output?: OutputLabel, buildCommand?: string): string[];
|
|
116
176
|
/** A local path rule the creator's own build broke. Local, before any request. */
|
|
117
177
|
export declare function badPath(reason: string, offending: string): string[];
|
|
118
178
|
/** The one redrawn line in f4. A bounded progress bar with a known total is not a stream. */
|
|
@@ -130,6 +190,13 @@ export declare function finishing(): string;
|
|
|
130
190
|
* page, decide publish is broken, and publish again.
|
|
131
191
|
*/
|
|
132
192
|
export declare function liveAt(liveUrl: string, version: number): string[];
|
|
193
|
+
/** Prints once the commit succeeded and the address is still serving the previous bytes. */
|
|
194
|
+
export declare function propagating(liveUrl: string, version: number): string;
|
|
195
|
+
/**
|
|
196
|
+
* The wait ran out. The publish DID commit, so this is a success, and it says so
|
|
197
|
+
* plainly: a creator told "not yet" without that will publish again.
|
|
198
|
+
*/
|
|
199
|
+
export declare function notYetVisible(liveUrl: string, version: number): string[];
|
|
133
200
|
/** Prints only if a retry is needed. It says nothing about a failure, because nothing has failed yet. */
|
|
134
201
|
export declare function takingLonger(): string;
|
|
135
202
|
export interface PlanLimitInput {
|
|
@@ -170,6 +237,12 @@ export declare function planLimitRefused(i: PlanLimitInput): string[];
|
|
|
170
237
|
export declare function publishExpired(address: string, liveVersion?: number): string[];
|
|
171
238
|
/** All three retries failed. "Nothing was lost" is the true and load-bearing sentence. */
|
|
172
239
|
export declare function flipFailed(address: string, liveVersion?: number): string[];
|
|
240
|
+
/**
|
|
241
|
+
* The commit's answer never arrived (the connection dropped or the api stopped
|
|
242
|
+
* answering), so whether the address switched is unknown. It says so, rather than
|
|
243
|
+
* claiming either outcome, and names the one check that settles it.
|
|
244
|
+
*/
|
|
245
|
+
export declare function commitOutcomeUnknown(address: string, liveVersion?: number): string[];
|
|
173
246
|
/**
|
|
174
247
|
* Two sentences, no explanation of version allocation. The real case is a
|
|
175
248
|
* teammate and an agent publishing in the same second, and "someone else
|