@vincentt-xr/cli 0.2.0 → 0.4.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/README.md +3 -1
- package/dist/cli/args.d.ts +9 -0
- package/dist/cli/args.js +20 -0
- package/dist/cli/index.js +114 -25
- package/dist/lib/backend.d.ts +0 -16
- package/dist/lib/backend.js +0 -34
- package/dist/lib/initCommand.d.ts +18 -1
- package/dist/lib/initCommand.js +70 -7
- package/dist/lib/lifecycle.d.ts +12 -4
- package/dist/lib/lifecycle.js +36 -30
- package/dist/lib/version.d.ts +34 -0
- package/dist/lib/version.js +82 -0
- package/dist/publish/api.d.ts +67 -0
- package/dist/publish/api.js +96 -0
- package/dist/publish/bounds.d.ts +25 -0
- package/dist/publish/bounds.js +53 -0
- package/dist/publish/classify.d.ts +51 -0
- package/dist/publish/classify.js +138 -0
- package/dist/publish/command.d.ts +27 -0
- package/dist/publish/command.js +328 -0
- package/dist/publish/consoleVerbs.d.ts +17 -0
- package/dist/publish/consoleVerbs.js +26 -0
- package/dist/publish/copy.d.ts +180 -0
- package/dist/publish/copy.js +606 -0
- package/dist/publish/defaults.d.ts +16 -0
- package/dist/publish/defaults.js +52 -0
- package/dist/publish/manifest.d.ts +27 -0
- package/dist/publish/manifest.js +81 -0
- package/dist/publish/paths.d.ts +25 -0
- package/dist/publish/paths.js +78 -0
- package/dist/publish/progress.d.ts +17 -0
- package/dist/publish/progress.js +45 -0
- package/dist/publish/prompt.d.ts +7 -0
- package/dist/publish/prompt.js +43 -0
- package/dist/publish/qa.f4.fixtures.d.ts +25 -0
- package/dist/publish/qa.f4.fixtures.js +140 -0
- package/dist/publish/upload.d.ts +30 -0
- package/dist/publish/upload.js +93 -0
- package/dist/scaffold/index.d.ts +26 -0
- package/dist/scaffold/index.js +43 -1
- package/package.json +2 -3
- package/dist/mcp/server.d.ts +0 -16
- package/dist/mcp/server.js +0 -215
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
// `vincentt publish` — Flows L22-L31.
|
|
2
|
+
//
|
|
3
|
+
// The shape: build the manifest LOCALLY, check the bounds LOCALLY, read the
|
|
4
|
+
// project ONCE, show the checklist, ask, then open → PUT direct to S3 → commit.
|
|
5
|
+
// Nothing is sent before the creator answers. `--yes` skips the prompt on every
|
|
6
|
+
// publish including the first; it never skips the checklist, so the permanence
|
|
7
|
+
// disclosure still reaches an unattended operator's transcript.
|
|
8
|
+
//
|
|
9
|
+
// The IO boundary (print, prompt, fetch, disk, clock, TTY) is injected so every
|
|
10
|
+
// branch is assertable without a real API, a real terminal or a real S3.
|
|
11
|
+
import { promises as fs } from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { PublishApiError, abandonUpload, commitUpload, openUpload, readProject, } from "./api.js";
|
|
14
|
+
import { buildManifest, ManifestError } from "./manifest.js";
|
|
15
|
+
import { checkBounds } from "./bounds.js";
|
|
16
|
+
import { classifyDist } from "./classify.js";
|
|
17
|
+
import { createProgress } from "./progress.js";
|
|
18
|
+
import { UploadFailed, uploadAll } from "./upload.js";
|
|
19
|
+
import * as copy from "./copy.js";
|
|
20
|
+
const COMMIT_RETRIES = 3;
|
|
21
|
+
const COMMIT_RETRY_DELAY_MS = 2000;
|
|
22
|
+
export async function publishCommand(deps, opts = {}) {
|
|
23
|
+
// The gate is LOCAL and NOTHING IS ATTEMPTED — no request, no partial upload.
|
|
24
|
+
if (!(await deps.token())) {
|
|
25
|
+
for (const l of copy.notSignedIn())
|
|
26
|
+
deps.print(l);
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
const binding = await deps.binding();
|
|
30
|
+
if (!binding) {
|
|
31
|
+
deps.print("✗ This folder isn't linked to a project.");
|
|
32
|
+
deps.print("");
|
|
33
|
+
deps.print(" vincentt link <project-id> (copy it from the console)");
|
|
34
|
+
deps.print(" vincentt create Register a new project for this folder");
|
|
35
|
+
return 1;
|
|
36
|
+
}
|
|
37
|
+
const distDir = path.resolve(deps.cwd, opts.distDir ?? "dist");
|
|
38
|
+
// Everything below the manifest build happens before a single request. The
|
|
39
|
+
// creator learns about a too-large build in the same second they would
|
|
40
|
+
// otherwise have started a ten-minute upload.
|
|
41
|
+
let manifest;
|
|
42
|
+
try {
|
|
43
|
+
manifest = await buildManifest(distDir);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
if (err instanceof ManifestError) {
|
|
47
|
+
for (const l of manifestErrorLines(err, distDir))
|
|
48
|
+
deps.print(l);
|
|
49
|
+
return 1;
|
|
50
|
+
}
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
const violation = checkBounds(manifest);
|
|
54
|
+
if (violation) {
|
|
55
|
+
deps.print(copy.headerLine(await projectLabel(deps, binding.slug), await orgOf(deps)));
|
|
56
|
+
deps.print(copy.readingLine(manifest.files.length, manifest.totalBytes));
|
|
57
|
+
for (const l of copy.tooLarge(violation))
|
|
58
|
+
deps.print(l);
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
61
|
+
const cfg = await deps.config();
|
|
62
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
63
|
+
// ONE call before the checklist. liveVersion and livePublishedAt ride it, so
|
|
64
|
+
// the republish line costs no second round trip.
|
|
65
|
+
let project;
|
|
66
|
+
try {
|
|
67
|
+
project = await readProject(cfg, binding.projectId, fetchImpl);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
if (err instanceof PublishApiError && err.status === 404) {
|
|
71
|
+
deps.print(`✗ ${err.message}`);
|
|
72
|
+
deps.print("");
|
|
73
|
+
deps.print(" This folder is linked to a project that can't be reached with your current");
|
|
74
|
+
deps.print(" account. Check who you are: vincentt whoami");
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
const address = addressOf(project);
|
|
80
|
+
const isFirstPublish = project.firstPublishedAt === null;
|
|
81
|
+
const nextVersion = (project.highestVersion ?? 0) + 1;
|
|
82
|
+
const orgName = await orgOf(deps);
|
|
83
|
+
// A non-interactive stdin with no --yes must REFUSE rather than assume yes.
|
|
84
|
+
if (!opts.yes && !deps.isTTY) {
|
|
85
|
+
deps.print("");
|
|
86
|
+
deps.print("✗ Publishing needs a confirmation, and this isn't an interactive terminal.");
|
|
87
|
+
deps.print("");
|
|
88
|
+
deps.print(" Re-run it interactively, or pass --yes: vincentt publish --yes");
|
|
89
|
+
deps.print("");
|
|
90
|
+
deps.print(" Nothing was uploaded.");
|
|
91
|
+
deps.print("");
|
|
92
|
+
return 1;
|
|
93
|
+
}
|
|
94
|
+
const findings = await classifyDist(distDir, new Set(manifest.files.map((f) => f.path)), (abs) => fs.readFile(abs, "utf8"), (dir, rel) => path.join(dir, ...rel.split("/")), { expectedHost: address });
|
|
95
|
+
const checklistInput = {
|
|
96
|
+
projectName: project.name,
|
|
97
|
+
orgName,
|
|
98
|
+
address,
|
|
99
|
+
fileCount: manifest.files.length,
|
|
100
|
+
totalBytes: manifest.totalBytes,
|
|
101
|
+
nextVersion,
|
|
102
|
+
findings,
|
|
103
|
+
};
|
|
104
|
+
let promptLine;
|
|
105
|
+
let defaultYes;
|
|
106
|
+
if (isFirstPublish) {
|
|
107
|
+
for (const l of copy.firstPublishChecklist(checklistInput))
|
|
108
|
+
deps.print(l);
|
|
109
|
+
promptLine = copy.firstPublishPrompt(address, nextVersion);
|
|
110
|
+
defaultYes = false;
|
|
111
|
+
}
|
|
112
|
+
else if (project.publishState === "unpublished") {
|
|
113
|
+
for (const l of copy.offlineChecklist(checklistInput))
|
|
114
|
+
deps.print(l);
|
|
115
|
+
promptLine = copy.offlinePrompt(nextVersion);
|
|
116
|
+
// This publish has a second consequence the creator did not ask for, so an
|
|
117
|
+
// accidental Enter must do nothing.
|
|
118
|
+
defaultYes = false;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
for (const l of copy.republishChecklist({
|
|
122
|
+
...checklistInput,
|
|
123
|
+
liveVersion: project.liveVersion ?? nextVersion - 1,
|
|
124
|
+
livePublishedAt: project.livePublishedAt ?? undefined,
|
|
125
|
+
now: deps.now(),
|
|
126
|
+
})) {
|
|
127
|
+
deps.print(l);
|
|
128
|
+
}
|
|
129
|
+
promptLine = copy.republishPrompt(nextVersion);
|
|
130
|
+
defaultYes = true;
|
|
131
|
+
}
|
|
132
|
+
// --yes suppresses the PROMPT, not the REPORT. The findings block above is
|
|
133
|
+
// byte-identical on both paths, because stdout is the only channel that
|
|
134
|
+
// reaches an agent.
|
|
135
|
+
if (!opts.yes) {
|
|
136
|
+
const confirmed = await deps.confirm(promptLine, defaultYes);
|
|
137
|
+
if (!confirmed) {
|
|
138
|
+
for (const l of copy.declined())
|
|
139
|
+
deps.print(l);
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return runUpload(deps, cfg, {
|
|
144
|
+
projectId: binding.projectId,
|
|
145
|
+
distDir,
|
|
146
|
+
manifest,
|
|
147
|
+
note: opts.note,
|
|
148
|
+
address,
|
|
149
|
+
liveVersion: project.liveVersion ?? undefined,
|
|
150
|
+
fetchImpl,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
async function runUpload(deps, cfg, i) {
|
|
154
|
+
let opened;
|
|
155
|
+
try {
|
|
156
|
+
opened = await openUpload(cfg, i.projectId, i.manifest.files, i.note, i.fetchImpl);
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
for (const l of openErrorLines(err, i.address, i.liveVersion))
|
|
160
|
+
deps.print(l);
|
|
161
|
+
return 1;
|
|
162
|
+
}
|
|
163
|
+
deps.print(copy.uploadStart(i.manifest.files.length));
|
|
164
|
+
const progress = createProgress({
|
|
165
|
+
isTTY: deps.isTTY,
|
|
166
|
+
write: deps.write,
|
|
167
|
+
print: deps.print,
|
|
168
|
+
now: deps.now,
|
|
169
|
+
});
|
|
170
|
+
const startedAt = deps.now();
|
|
171
|
+
try {
|
|
172
|
+
await uploadAll({
|
|
173
|
+
distDir: i.distDir,
|
|
174
|
+
targets: opened.uploads,
|
|
175
|
+
manifest: i.manifest.files,
|
|
176
|
+
onProgress: (p) => progress.update(p),
|
|
177
|
+
fetchImpl: i.fetchImpl,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
progress.finish();
|
|
182
|
+
await abandonUpload(cfg, i.projectId, opened.uploadId, i.fetchImpl);
|
|
183
|
+
if (err instanceof UploadFailed && err.badDigest) {
|
|
184
|
+
for (const l of copy.badDigest(err.path, i.address, i.liveVersion))
|
|
185
|
+
deps.print(l);
|
|
186
|
+
return 1;
|
|
187
|
+
}
|
|
188
|
+
const failedPath = err instanceof UploadFailed ? err.path : "a file";
|
|
189
|
+
deps.print("");
|
|
190
|
+
deps.print(`✗ Uploading ${failedPath} failed.`);
|
|
191
|
+
deps.print("");
|
|
192
|
+
deps.print(i.liveVersion === undefined
|
|
193
|
+
? ` Your live address is unchanged — ${i.address}.`
|
|
194
|
+
: ` Your live address is unchanged — ${i.address} is still serving v${i.liveVersion}.`);
|
|
195
|
+
deps.print("");
|
|
196
|
+
deps.print(" Publish again: vincentt publish");
|
|
197
|
+
deps.print("");
|
|
198
|
+
return 1;
|
|
199
|
+
}
|
|
200
|
+
progress.finish();
|
|
201
|
+
deps.print(copy.uploadDone(i.manifest.files.length, i.manifest.totalBytes, deps.now() - startedAt));
|
|
202
|
+
deps.print(copy.finishing());
|
|
203
|
+
return commitWithHealing(deps, cfg, i, opened.uploadId);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* The commit, with the healing retry. A 502 publish_flip_failed is retried
|
|
207
|
+
* automatically — up to 3 attempts, 2s apart — because commit is idempotent on
|
|
208
|
+
* uploadId and the retry is exactly what the healing path was built for.
|
|
209
|
+
* Surfacing a 502 to a creator whose bytes are all safely stored and whose
|
|
210
|
+
* publish will succeed on the next call would be manufacturing an incident.
|
|
211
|
+
*/
|
|
212
|
+
async function commitWithHealing(deps, cfg, i, uploadId) {
|
|
213
|
+
let announcedSlow = false;
|
|
214
|
+
for (let attempt = 1; attempt <= COMMIT_RETRIES; attempt++) {
|
|
215
|
+
try {
|
|
216
|
+
const result = await commitUpload(cfg, i.projectId, uploadId, i.fetchImpl);
|
|
217
|
+
for (const l of copy.liveAt(result.liveUrl, result.version))
|
|
218
|
+
deps.print(l);
|
|
219
|
+
return 0;
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
if (!(err instanceof PublishApiError))
|
|
223
|
+
throw err;
|
|
224
|
+
if (err.code === "publish_flip_failed" || err.status === 502) {
|
|
225
|
+
if (attempt === COMMIT_RETRIES) {
|
|
226
|
+
for (const l of copy.flipFailed(i.address, i.liveVersion))
|
|
227
|
+
deps.print(l);
|
|
228
|
+
return 1;
|
|
229
|
+
}
|
|
230
|
+
// The `!` line prints only if a retry happens, and it says nothing about
|
|
231
|
+
// a failure — from the creator's position nothing has failed yet.
|
|
232
|
+
if (!announcedSlow) {
|
|
233
|
+
deps.print(copy.takingLonger());
|
|
234
|
+
announcedSlow = true;
|
|
235
|
+
}
|
|
236
|
+
await deps.sleep(COMMIT_RETRY_DELAY_MS);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
for (const l of commitErrorLines(err, i.address, i.liveVersion))
|
|
240
|
+
deps.print(l);
|
|
241
|
+
return 1;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return 1;
|
|
245
|
+
}
|
|
246
|
+
function commitErrorLines(err, address, liveVersion) {
|
|
247
|
+
switch (err.code) {
|
|
248
|
+
case "publish_expired":
|
|
249
|
+
return copy.publishExpired(address, liveVersion);
|
|
250
|
+
case "publish_contended":
|
|
251
|
+
return copy.publishContended();
|
|
252
|
+
case "publish_incomplete":
|
|
253
|
+
// The response carries COUNTS, never paths, so these are the CLI's own
|
|
254
|
+
// strings about its own manifest.
|
|
255
|
+
return copy.publishIncomplete(address, { missing: err.detail.missing, mismatched: err.detail.mismatched }, liveVersion);
|
|
256
|
+
default:
|
|
257
|
+
return [
|
|
258
|
+
"",
|
|
259
|
+
`✗ ${err.message}`,
|
|
260
|
+
"",
|
|
261
|
+
liveVersion === undefined
|
|
262
|
+
? ` Your live address is unchanged — ${address}.`
|
|
263
|
+
: ` Your live address is unchanged — ${address} is still serving v${liveVersion}.`,
|
|
264
|
+
"",
|
|
265
|
+
" Publish again: vincentt publish",
|
|
266
|
+
"",
|
|
267
|
+
];
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function openErrorLines(err, address, liveVersion) {
|
|
271
|
+
if (!(err instanceof PublishApiError)) {
|
|
272
|
+
return [
|
|
273
|
+
"",
|
|
274
|
+
`✗ ${err instanceof Error ? err.message : String(err)}`,
|
|
275
|
+
"",
|
|
276
|
+
" Nothing was uploaded.",
|
|
277
|
+
"",
|
|
278
|
+
];
|
|
279
|
+
}
|
|
280
|
+
if (err.code === "publish_too_large") {
|
|
281
|
+
return copy.tooLargeFromServer(err.detail.observed ?? 0, err.detail.limit ?? 0);
|
|
282
|
+
}
|
|
283
|
+
if (err.code === "rate_limited") {
|
|
284
|
+
return copy.rateLimited(err.retryAfterSec ?? 60);
|
|
285
|
+
}
|
|
286
|
+
if (err.code === "manifest_invalid") {
|
|
287
|
+
return copy.badPath(err.detail.reason ?? "", err.detail.path ?? "");
|
|
288
|
+
}
|
|
289
|
+
return [
|
|
290
|
+
"",
|
|
291
|
+
`✗ ${err.message}`,
|
|
292
|
+
"",
|
|
293
|
+
liveVersion === undefined
|
|
294
|
+
? ` Your live address is unchanged — ${address}.`
|
|
295
|
+
: ` Your live address is unchanged — ${address} is still serving v${liveVersion}.`,
|
|
296
|
+
"",
|
|
297
|
+
" Nothing was uploaded.",
|
|
298
|
+
"",
|
|
299
|
+
];
|
|
300
|
+
}
|
|
301
|
+
function manifestErrorLines(err, distDir) {
|
|
302
|
+
if (err.reason === "no_dist")
|
|
303
|
+
return copy.noDist(distDir);
|
|
304
|
+
if (err.reason === "no_index_html")
|
|
305
|
+
return copy.noIndexHtml(distDir);
|
|
306
|
+
return copy.badPath(err.reason, err.offendingPath);
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* The address as a creator reads it: the host, never the scheme. `liveUrl` is
|
|
310
|
+
* server-computed (the apex differs per environment), so it is the authority and
|
|
311
|
+
* the slug is only a fallback.
|
|
312
|
+
*/
|
|
313
|
+
function addressOf(project) {
|
|
314
|
+
if (!project.liveUrl)
|
|
315
|
+
return project.slug;
|
|
316
|
+
try {
|
|
317
|
+
return new URL(project.liveUrl).host;
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
return project.liveUrl.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
async function orgOf(deps) {
|
|
324
|
+
return (await deps.orgName?.()) ?? "";
|
|
325
|
+
}
|
|
326
|
+
async function projectLabel(deps, slug) {
|
|
327
|
+
return slug || path.basename(deps.cwd);
|
|
328
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare function isConsoleOnlyVerb(verb: string): boolean;
|
|
2
|
+
export interface ConsoleVerbDeps {
|
|
3
|
+
print: (line: string) => void;
|
|
4
|
+
/** The bound project id, or undefined for an unlinked folder. */
|
|
5
|
+
projectId: () => Promise<string | undefined>;
|
|
6
|
+
consoleUrl?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The deep link goes to the exact tab, built from the binding the CLI already
|
|
10
|
+
* holds — a creator who typed `vincentt rollback` wants to be one click from
|
|
11
|
+
* rolling back.
|
|
12
|
+
*
|
|
13
|
+
* If the folder is NOT linked the last two lines are dropped and the block ends
|
|
14
|
+
* at the reason: the CLI does not know which project they meant, and guessing a
|
|
15
|
+
* console URL would be worse than none.
|
|
16
|
+
*/
|
|
17
|
+
export declare function consoleVerbCommand(deps: ConsoleVerbDeps, verb: string): Promise<number>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Flow L30 — rollback, unpublish, republish, versions, revert and take-offline
|
|
2
|
+
// are NOT CLI verbs, and the CLI says where they are.
|
|
3
|
+
//
|
|
4
|
+
// This is ARGUMENT PARSING. Exit 1, no network call, nothing looked up.
|
|
5
|
+
import { CONSOLE_ONLY_VERBS, consoleVerb } from "./copy.js";
|
|
6
|
+
const DEFAULT_CONSOLE_URL = "https://console.vincentt.app";
|
|
7
|
+
export function isConsoleOnlyVerb(verb) {
|
|
8
|
+
return CONSOLE_ONLY_VERBS.includes(verb);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The deep link goes to the exact tab, built from the binding the CLI already
|
|
12
|
+
* holds — a creator who typed `vincentt rollback` wants to be one click from
|
|
13
|
+
* rolling back.
|
|
14
|
+
*
|
|
15
|
+
* If the folder is NOT linked the last two lines are dropped and the block ends
|
|
16
|
+
* at the reason: the CLI does not know which project they meant, and guessing a
|
|
17
|
+
* console URL would be worse than none.
|
|
18
|
+
*/
|
|
19
|
+
export async function consoleVerbCommand(deps, verb) {
|
|
20
|
+
const projectId = await deps.projectId();
|
|
21
|
+
const base = (deps.consoleUrl ?? DEFAULT_CONSOLE_URL).replace(/\/$/, "");
|
|
22
|
+
const url = projectId ? `${base}/projects/${projectId}/versions` : undefined;
|
|
23
|
+
for (const line of consoleVerb(verb, url))
|
|
24
|
+
deps.print(line);
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type { FileFindings } from "./classify.js";
|
|
2
|
+
import type { BoundViolation } from "./bounds.js";
|
|
3
|
+
export declare function truncate(value: string, max?: number): string;
|
|
4
|
+
/** Bytes as the creator reads them. Decimal MB, matching what a build tool prints. */
|
|
5
|
+
export declare function formatBytes(bytes: number): string;
|
|
6
|
+
/** The limit, printed as a plain number and never as policy. */
|
|
7
|
+
export declare function formatLimit(bytes: number): string;
|
|
8
|
+
export declare function relativeTime(iso: string, now: number): string;
|
|
9
|
+
/**
|
|
10
|
+
* Flow L25. Three registers of copy because the platform's confidence genuinely
|
|
11
|
+
* differs:
|
|
12
|
+
* - TEMPLATE-DEFAULT is flat and assertive. The CLI matched a string in a
|
|
13
|
+
* corpus it ships; it knows.
|
|
14
|
+
* - LIKELY-DEFAULT is hedged. The heuristic will be wrong sometimes, and a
|
|
15
|
+
* creator whose experience is genuinely called "AR" must not be told they
|
|
16
|
+
* made a mistake.
|
|
17
|
+
* - The dead og:image is the only ✗ the checklist ever emits, and it earns it:
|
|
18
|
+
* the manifest either contains the path or it does not.
|
|
19
|
+
*
|
|
20
|
+
* BOTH default tiers print the offending value. "Still the template default"
|
|
21
|
+
* without the string is a claim the creator has to go and verify; with it they
|
|
22
|
+
* either recognise it instantly or spot that we are wrong.
|
|
23
|
+
*
|
|
24
|
+
* A FINDING IS NEVER A COUNT. No "2 issues found", no summary line, and no exit
|
|
25
|
+
* code that varies with findings.
|
|
26
|
+
*/
|
|
27
|
+
export declare function findingLines(byFile: FileFindings[], withSubtitle: boolean): string[];
|
|
28
|
+
/**
|
|
29
|
+
* Mandatory and always present. A screen with a red ✗ on it reads as a gate
|
|
30
|
+
* unless it explicitly says otherwise, and a creator who believes the platform
|
|
31
|
+
* is gating their publish on our opinion of their <meta> tags has learned
|
|
32
|
+
* something false about who owns the dist.
|
|
33
|
+
*/
|
|
34
|
+
export declare function nothingBlocksYou(): string[];
|
|
35
|
+
export interface ChecklistInput {
|
|
36
|
+
projectName: string;
|
|
37
|
+
orgName: string;
|
|
38
|
+
address: string;
|
|
39
|
+
fileCount: number;
|
|
40
|
+
totalBytes: number;
|
|
41
|
+
nextVersion: number;
|
|
42
|
+
findings: FileFindings[];
|
|
43
|
+
}
|
|
44
|
+
export declare function readingLine(fileCount: number, totalBytes: number): string;
|
|
45
|
+
export declare function headerLine(projectName: string, orgName: string): string;
|
|
46
|
+
/**
|
|
47
|
+
* Flow L22 — the single most consequential screen in the product, and its ORDER
|
|
48
|
+
* is the design.
|
|
49
|
+
*
|
|
50
|
+
* The address block is FIRST and it is fenced: a creator who reads nothing else
|
|
51
|
+
* reads the address and the sentence under it. The findings come AFTER, never
|
|
52
|
+
* before — the address is the irreversible thing and the metadata is fixable
|
|
53
|
+
* forever, so putting a red ✗ above the permanence block would make the eye land
|
|
54
|
+
* on the recoverable problem and skim the unrecoverable one.
|
|
55
|
+
*/
|
|
56
|
+
export declare function firstPublishChecklist(i: ChecklistInput): string[];
|
|
57
|
+
/** The prompt line restates the address AND the version. `[y/N]` — an accidental Enter does nothing. */
|
|
58
|
+
export declare function firstPublishPrompt(address: string, version: number): string;
|
|
59
|
+
/**
|
|
60
|
+
* Declining. Glyphless, exit 0, zero network calls beyond the project read.
|
|
61
|
+
* Declining is not a failure and is not an error; it is the prompt working.
|
|
62
|
+
*
|
|
63
|
+
* The second line is the one that matters — a creator who backed out because the
|
|
64
|
+
* address was wrong needs to know the address is still theirs to change.
|
|
65
|
+
*/
|
|
66
|
+
export declare function declined(): string[];
|
|
67
|
+
export interface RepublishInput extends ChecklistInput {
|
|
68
|
+
liveVersion: number;
|
|
69
|
+
/** ISO8601 from the SAME project read — no second call to render one parenthetical. */
|
|
70
|
+
livePublishedAt?: string;
|
|
71
|
+
now: number;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Flow L24. No fence, no permanence block, no disclosure: the address is already
|
|
75
|
+
* permanent and the creator already knows, and presenting a frozen fact as a
|
|
76
|
+
* choice is worse than not presenting it at all.
|
|
77
|
+
*
|
|
78
|
+
* `(v5, published 3 days ago)` is rendered from the project read ALONE —
|
|
79
|
+
* liveVersion and livePublishedAt both ride GET /projects/{id}.
|
|
80
|
+
*/
|
|
81
|
+
export declare function republishChecklist(i: RepublishInput): string[];
|
|
82
|
+
/** `[Y/n]` — on a republish an accidental Enter does the thing the creator typed the command to do. */
|
|
83
|
+
export declare function republishPrompt(version: number): string;
|
|
84
|
+
/**
|
|
85
|
+
* A publish onto an offline address brings it online, because publish always
|
|
86
|
+
* goes live. The creator must be told: they took it offline on purpose and might
|
|
87
|
+
* not intend to reverse that, so the default reverts to [y/N].
|
|
88
|
+
*/
|
|
89
|
+
export declare function offlineChecklist(i: ChecklistInput): string[];
|
|
90
|
+
export declare function offlinePrompt(version: number): string;
|
|
91
|
+
/**
|
|
92
|
+
* f1's line verbatim, with f4's reason. Note what is ABSENT, deliberately: f3's
|
|
93
|
+
* "your local dev loop doesn't need one" paragraph. It is the right copy for
|
|
94
|
+
* preview and the wrong copy here — a creator trying to publish is not looking
|
|
95
|
+
* for a smaller room, and offering one at this gate reads as a deflection.
|
|
96
|
+
*/
|
|
97
|
+
export declare function notSignedIn(): string[];
|
|
98
|
+
/**
|
|
99
|
+
* Loud, local, instant, and it carries the real number. This fires before a
|
|
100
|
+
* single byte moves and before a single request is made.
|
|
101
|
+
*
|
|
102
|
+
* The two numbers are STACKED AND ALIGNED — the judgement the creator must make
|
|
103
|
+
* is a comparison, and prose defeats it. The three largest files are named
|
|
104
|
+
* because that is almost always the entire diagnosis: one uncompressed asset is
|
|
105
|
+
* nearly always the cause.
|
|
106
|
+
*
|
|
107
|
+
* The limit is printed as a number, never as policy. No "on your plan", no
|
|
108
|
+
* "upgrade" — it is a correctness limit and not a quota, and copy implying
|
|
109
|
+
* otherwise would be the first sentence of a billing model this feature does not
|
|
110
|
+
* have.
|
|
111
|
+
*/
|
|
112
|
+
export declare function tooLarge(v: BoundViolation): string[];
|
|
113
|
+
/** No dist at all, or no index.html at its root. Zero network calls. */
|
|
114
|
+
export declare function noDist(distDir: string): string[];
|
|
115
|
+
export declare function noIndexHtml(distDir: string): string[];
|
|
116
|
+
/** A local path rule the creator's own build broke. Local, before any request. */
|
|
117
|
+
export declare function badPath(reason: string, offending: string): string[];
|
|
118
|
+
/** The one redrawn line in f4. A bounded progress bar with a known total is not a stream. */
|
|
119
|
+
export declare function progressBar(done: number, total: number, bytesDone: number, bytesTotal: number): string;
|
|
120
|
+
/** The non-TTY degrade: periodic plain lines, at most one per 2s. */
|
|
121
|
+
export declare function progressPlain(done: number, total: number, bytesDone: number): string;
|
|
122
|
+
export declare function uploadStart(fileCount: number): string;
|
|
123
|
+
export declare function uploadDone(fileCount: number, totalBytes: number, ms: number): string;
|
|
124
|
+
/** Covers the commit. The pause between the last byte and the live URL is real. */
|
|
125
|
+
export declare function finishing(): string;
|
|
126
|
+
/**
|
|
127
|
+
* The payload of the entire command: the live URL, full and clickable, with the
|
|
128
|
+
* version in parentheses. The caching sentence is one line and ALWAYS present —
|
|
129
|
+
* a creator refreshing a phone that has the old page in memory will see the old
|
|
130
|
+
* page, decide publish is broken, and publish again.
|
|
131
|
+
*/
|
|
132
|
+
export declare function liveAt(liveUrl: string, version: number): string[];
|
|
133
|
+
/** Prints only if a retry is needed. It says nothing about a failure, because nothing has failed yet. */
|
|
134
|
+
export declare function takingLonger(): string;
|
|
135
|
+
/**
|
|
136
|
+
* The cause is NOT named. Every non-healing arm of the commit state machine
|
|
137
|
+
* returns this identically by design, and the CLI must not invent a distinction
|
|
138
|
+
* the wire does not carry. "Had to be restarted" is honest about what happened
|
|
139
|
+
* and is correct for all three arms.
|
|
140
|
+
*/
|
|
141
|
+
export declare function publishExpired(address: string, liveVersion?: number): string[];
|
|
142
|
+
/** All three retries failed. "Nothing was lost" is the true and load-bearing sentence. */
|
|
143
|
+
export declare function flipFailed(address: string, liveVersion?: number): string[];
|
|
144
|
+
/**
|
|
145
|
+
* Two sentences, no explanation of version allocation. The real case is a
|
|
146
|
+
* teammate and an agent publishing in the same second, and "someone else
|
|
147
|
+
* published this project" tells the creator to check with a person before
|
|
148
|
+
* re-running — the correct next action, and not something a retry loop can do
|
|
149
|
+
* for them.
|
|
150
|
+
*/
|
|
151
|
+
export declare function publishContended(): string[];
|
|
152
|
+
/**
|
|
153
|
+
* The commit found fewer objects than the manifest declared. The response
|
|
154
|
+
* carries COUNTS, never paths, so the CLI diffs against its OWN manifest and
|
|
155
|
+
* prints its own strings.
|
|
156
|
+
*/
|
|
157
|
+
export declare function publishIncomplete(address: string, counts: {
|
|
158
|
+
missing?: number;
|
|
159
|
+
mismatched?: number;
|
|
160
|
+
}, liveVersion?: number): string[];
|
|
161
|
+
/** A file's bytes changed under the CLI between hashing and PUT. Names the file. */
|
|
162
|
+
export declare function badDigest(filePath: string, address: string, liveVersion?: number): string[];
|
|
163
|
+
export declare function tooLargeFromServer(observed: number, limit: number): string[];
|
|
164
|
+
export declare function rateLimited(retryAfterSec: number): string[];
|
|
165
|
+
/**
|
|
166
|
+
* The same block serves rollback, unpublish, republish, versions, revert and
|
|
167
|
+
* take-offline. These are the words a creator will actually type, and every one
|
|
168
|
+
* of them lands on one screen that names the real home. A bare "unknown command"
|
|
169
|
+
* here would be the product failing to answer a question it knows the answer to.
|
|
170
|
+
*
|
|
171
|
+
* The reason is given in one clause and it is the honest one. Not "not
|
|
172
|
+
* supported", not "coming soon" — neither is true.
|
|
173
|
+
*
|
|
174
|
+
* IF THE FOLDER IS NOT LINKED THE LAST TWO LINES ARE DROPPED. The CLI does not
|
|
175
|
+
* know which project they meant, and guessing a console URL would be worse than
|
|
176
|
+
* none.
|
|
177
|
+
*/
|
|
178
|
+
export declare function consoleVerb(verb: string, versionsUrl?: string): string[];
|
|
179
|
+
/** The six verbs that land on that block. */
|
|
180
|
+
export declare const CONSOLE_ONLY_VERBS: readonly string[];
|