@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.
Files changed (43) hide show
  1. package/README.md +3 -1
  2. package/dist/cli/args.d.ts +9 -0
  3. package/dist/cli/args.js +20 -0
  4. package/dist/cli/index.js +114 -25
  5. package/dist/lib/backend.d.ts +0 -16
  6. package/dist/lib/backend.js +0 -34
  7. package/dist/lib/initCommand.d.ts +18 -1
  8. package/dist/lib/initCommand.js +70 -7
  9. package/dist/lib/lifecycle.d.ts +12 -4
  10. package/dist/lib/lifecycle.js +36 -30
  11. package/dist/lib/version.d.ts +34 -0
  12. package/dist/lib/version.js +82 -0
  13. package/dist/publish/api.d.ts +67 -0
  14. package/dist/publish/api.js +96 -0
  15. package/dist/publish/bounds.d.ts +25 -0
  16. package/dist/publish/bounds.js +53 -0
  17. package/dist/publish/classify.d.ts +51 -0
  18. package/dist/publish/classify.js +138 -0
  19. package/dist/publish/command.d.ts +27 -0
  20. package/dist/publish/command.js +328 -0
  21. package/dist/publish/consoleVerbs.d.ts +17 -0
  22. package/dist/publish/consoleVerbs.js +26 -0
  23. package/dist/publish/copy.d.ts +180 -0
  24. package/dist/publish/copy.js +606 -0
  25. package/dist/publish/defaults.d.ts +16 -0
  26. package/dist/publish/defaults.js +52 -0
  27. package/dist/publish/manifest.d.ts +27 -0
  28. package/dist/publish/manifest.js +81 -0
  29. package/dist/publish/paths.d.ts +25 -0
  30. package/dist/publish/paths.js +78 -0
  31. package/dist/publish/progress.d.ts +17 -0
  32. package/dist/publish/progress.js +45 -0
  33. package/dist/publish/prompt.d.ts +7 -0
  34. package/dist/publish/prompt.js +43 -0
  35. package/dist/publish/qa.f4.fixtures.d.ts +25 -0
  36. package/dist/publish/qa.f4.fixtures.js +140 -0
  37. package/dist/publish/upload.d.ts +30 -0
  38. package/dist/publish/upload.js +93 -0
  39. package/dist/scaffold/index.d.ts +26 -0
  40. package/dist/scaffold/index.js +43 -1
  41. package/package.json +2 -3
  42. package/dist/mcp/server.d.ts +0 -16
  43. 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[];