@vincentt-xr/cli 0.16.0 → 0.17.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/dist/cli/index.js +26 -3
- package/dist/preview/api.d.ts +19 -0
- package/dist/preview/api.js +29 -0
- package/dist/publish/api.d.ts +17 -1
- package/dist/publish/command.js +23 -1
- package/dist/shared/countLimit.d.ts +109 -0
- package/dist/shared/countLimit.js +169 -0
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -38,7 +38,8 @@ import { httpRelayClient } from "../lib/diagnostics.js";
|
|
|
38
38
|
import { readLivePreview, relayInfoDir } from "../lib/relayInfo.js";
|
|
39
39
|
import { attachedToRunning, mintRateLimited, notSignedIn, previewConflict, startBlock, } from "../preview/copy.js";
|
|
40
40
|
import { renderTerminalQr, shouldRenderQr } from "../preview/qr.js";
|
|
41
|
-
import { PreviewConflictError, PreviewRateLimitedError } from "../preview/api.js";
|
|
41
|
+
import { PreviewConflictError, PreviewLimitError, PreviewRateLimitedError, } from "../preview/api.js";
|
|
42
|
+
import { previewLimitRefused, resolveCountLimit, } from "../shared/countLimit.js";
|
|
42
43
|
import { fetchNext, readCursor, writeCursor } from "../lib/feedback.js";
|
|
43
44
|
import * as feedbackCopy from "../lib/feedbackCopy.js";
|
|
44
45
|
import { record as recordInvocation } from "../dx/journal.js";
|
|
@@ -584,7 +585,11 @@ async function preview() {
|
|
|
584
585
|
}
|
|
585
586
|
}
|
|
586
587
|
catch (err) {
|
|
587
|
-
|
|
588
|
+
// The local org is the FALLBACK only. A count refusal names the org that
|
|
589
|
+
// owns this project, and the wire's name wins over this one wherever both
|
|
590
|
+
// exist — see resolveCountLimit.
|
|
591
|
+
const localOrg = await localOrgIdentity();
|
|
592
|
+
for (const l of previewFailureLines(err, localOrg))
|
|
588
593
|
console.log(l);
|
|
589
594
|
process.exitCode = 1;
|
|
590
595
|
return;
|
|
@@ -706,6 +711,18 @@ async function activeOrgName() {
|
|
|
706
711
|
(await loadMachineConfig()).activeOrg ??
|
|
707
712
|
"");
|
|
708
713
|
}
|
|
714
|
+
/**
|
|
715
|
+
* The org this machine is pointed at, for a refusal whose body named none.
|
|
716
|
+
*
|
|
717
|
+
* `activeOrg` is a SLUG, so it fills the slug slot and not the name slot. The
|
|
718
|
+
* display name is the API's to give; inventing one from the slug would put a
|
|
719
|
+
* fabricated workspace name on a refusal.
|
|
720
|
+
*/
|
|
721
|
+
async function localOrgIdentity() {
|
|
722
|
+
const slug = await activeOrgName();
|
|
723
|
+
const { consoleUrl } = await resolveAuthUrls();
|
|
724
|
+
return { slug: slug || undefined, consoleUrl };
|
|
725
|
+
}
|
|
709
726
|
/** The binding's name is a HINT for display only; the API is the authority. */
|
|
710
727
|
async function projectDisplayName(cwd) {
|
|
711
728
|
const located = await locateProjectBinding(cwd);
|
|
@@ -744,9 +761,15 @@ async function projectConsoleUrl(cwd, sessionId) {
|
|
|
744
761
|
* creator never has to read a second, and none of them names the edge, the API
|
|
745
762
|
* or an outage — a creator cannot act on that.
|
|
746
763
|
*/
|
|
747
|
-
function previewFailureLines(err) {
|
|
764
|
+
function previewFailureLines(err, local) {
|
|
748
765
|
if (err instanceof PreviewRateLimitedError)
|
|
749
766
|
return mintRateLimited(err.retryAfterSec);
|
|
767
|
+
// The ceiling is the second 409 on the mint route, and it is a DIFFERENT
|
|
768
|
+
// creator situation from the conflict below: nothing is running, nothing was
|
|
769
|
+
// taken down, and the organization is full.
|
|
770
|
+
if (err instanceof PreviewLimitError) {
|
|
771
|
+
return previewLimitRefused(resolveCountLimit(err.detail, local));
|
|
772
|
+
}
|
|
750
773
|
if (err instanceof PreviewConflictError)
|
|
751
774
|
return previewConflict();
|
|
752
775
|
if (err instanceof ProjectUnreachableError) {
|
package/dist/preview/api.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ResolvedConfig } from "../shared/config.js";
|
|
2
|
+
import type { CountLimitDetail } from "../shared/countLimit.js";
|
|
2
3
|
import { BackendError } from "../lib/backend.js";
|
|
3
4
|
/** The mint response. Its key set is the cross-repo pin (contract table 2.11). */
|
|
4
5
|
export interface MintedPreview {
|
|
@@ -27,6 +28,24 @@ export declare class PreviewRateLimitedError extends BackendError {
|
|
|
27
28
|
export declare class PreviewConflictError extends BackendError {
|
|
28
29
|
constructor(message: string);
|
|
29
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* The org is at its concurrent-live-preview ceiling.
|
|
33
|
+
*
|
|
34
|
+
* IT IS A SECOND 409 ON THE MINT ROUTE and it is told from the first by its
|
|
35
|
+
* CODE, never by its status. The two mean opposite things to a creator: a
|
|
36
|
+
* conflict says a preview for this folder is already running and to go watch it;
|
|
37
|
+
* the ceiling says no preview started and the organization is full. Collapsing
|
|
38
|
+
* them would tell a creator at their ceiling to watch a preview that does not
|
|
39
|
+
* exist.
|
|
40
|
+
*
|
|
41
|
+
* It carries the whole detail because the refusal IS the notice: the figures and
|
|
42
|
+
* the org name are the only things that make it actionable, and the CLI cannot
|
|
43
|
+
* obtain either anywhere else.
|
|
44
|
+
*/
|
|
45
|
+
export declare class PreviewLimitError extends BackendError {
|
|
46
|
+
readonly detail: CountLimitDetail;
|
|
47
|
+
constructor(message: string, detail: CountLimitDetail);
|
|
48
|
+
}
|
|
30
49
|
/**
|
|
31
50
|
* The session is gone — `preview_session_ended`, the CLI's terminal branch.
|
|
32
51
|
*
|
package/dist/preview/api.js
CHANGED
|
@@ -23,6 +23,28 @@ export class PreviewConflictError extends BackendError {
|
|
|
23
23
|
this.name = "PreviewConflictError";
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* The org is at its concurrent-live-preview ceiling.
|
|
28
|
+
*
|
|
29
|
+
* IT IS A SECOND 409 ON THE MINT ROUTE and it is told from the first by its
|
|
30
|
+
* CODE, never by its status. The two mean opposite things to a creator: a
|
|
31
|
+
* conflict says a preview for this folder is already running and to go watch it;
|
|
32
|
+
* the ceiling says no preview started and the organization is full. Collapsing
|
|
33
|
+
* them would tell a creator at their ceiling to watch a preview that does not
|
|
34
|
+
* exist.
|
|
35
|
+
*
|
|
36
|
+
* It carries the whole detail because the refusal IS the notice: the figures and
|
|
37
|
+
* the org name are the only things that make it actionable, and the CLI cannot
|
|
38
|
+
* obtain either anywhere else.
|
|
39
|
+
*/
|
|
40
|
+
export class PreviewLimitError extends BackendError {
|
|
41
|
+
detail;
|
|
42
|
+
constructor(message, detail) {
|
|
43
|
+
super(message, 409);
|
|
44
|
+
this.detail = detail;
|
|
45
|
+
this.name = "PreviewLimitError";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
26
48
|
/**
|
|
27
49
|
* The session is gone — `preview_session_ended`, the CLI's terminal branch.
|
|
28
50
|
*
|
|
@@ -104,6 +126,13 @@ export async function mintPreviewSession(cfg, projectId, key, fetchImpl = fetch)
|
|
|
104
126
|
throw new ProjectUnreachableError(body.message ?? "This project is not available to you.");
|
|
105
127
|
}
|
|
106
128
|
if (res.status === 409) {
|
|
129
|
+
// TWO 409s NOW LAND HERE and the CODE is what separates them. Reading the
|
|
130
|
+
// status alone would render the ceiling refusal as "a preview is already
|
|
131
|
+
// running for this folder" — a sentence that is false, that names a
|
|
132
|
+
// preview the creator does not have, and that sends them to watch it.
|
|
133
|
+
if (body.code === "preview_limit") {
|
|
134
|
+
throw new PreviewLimitError(body.message ?? "this organization is at its preview limit", body.detail ?? {});
|
|
135
|
+
}
|
|
107
136
|
throw new PreviewConflictError(body.message ?? "a preview is already running for this folder");
|
|
108
137
|
}
|
|
109
138
|
throw new BackendError(body.message ?? `could not start a preview (${res.status})`, res.status);
|
package/dist/publish/api.d.ts
CHANGED
|
@@ -41,7 +41,15 @@ export interface CommitResult {
|
|
|
41
41
|
releaseNote?: string;
|
|
42
42
|
}
|
|
43
43
|
export interface ApiErrorDetail {
|
|
44
|
-
|
|
44
|
+
/**
|
|
45
|
+
* ⚠ `limit` CARRIES TWO DIFFERENT TYPES on two different codes, and the api
|
|
46
|
+
* owns both. On `publish_too_large` and f16's domain ceiling it is a
|
|
47
|
+
* MAGNITUDE (bytes, a count); on the three count refusals it is the AXIS NAME
|
|
48
|
+
* ("seats" | "published" | "previews" | "storage"). The two never appear on
|
|
49
|
+
* the same body, so the union is read at the arm that knows which code it is
|
|
50
|
+
* handling and never generically.
|
|
51
|
+
*/
|
|
52
|
+
limit?: number | string;
|
|
45
53
|
observed?: number;
|
|
46
54
|
missing?: number;
|
|
47
55
|
mismatched?: number;
|
|
@@ -67,6 +75,14 @@ export interface ApiErrorDetail {
|
|
|
67
75
|
*/
|
|
68
76
|
orgName?: string;
|
|
69
77
|
orgSlug?: string;
|
|
78
|
+
/**
|
|
79
|
+
* The count refusals' two figures — the org's current occupancy on the axis,
|
|
80
|
+
* and the plan's ceiling for it. Both are required for the refusal to
|
|
81
|
+
* discharge B-F10-2: a refusal naming neither is a refusal with no notice in
|
|
82
|
+
* it.
|
|
83
|
+
*/
|
|
84
|
+
count?: number;
|
|
85
|
+
ceiling?: number;
|
|
70
86
|
}
|
|
71
87
|
export declare class PublishApiError extends Error {
|
|
72
88
|
readonly status: number;
|
package/dist/publish/command.js
CHANGED
|
@@ -17,6 +17,7 @@ import { classifyDist } from "./classify.js";
|
|
|
17
17
|
import { createProgress } from "./progress.js";
|
|
18
18
|
import { UploadFailed, uploadAll } from "./upload.js";
|
|
19
19
|
import * as copy from "./copy.js";
|
|
20
|
+
import { publishedLimitRefused, resolveCountLimit, storageCeilingRefused, } from "../shared/countLimit.js";
|
|
20
21
|
import { resolveBuiltWith } from "../platform/resolve.js";
|
|
21
22
|
const COMMIT_RETRIES = 3;
|
|
22
23
|
const COMMIT_RETRY_DELAY_MS = 2000;
|
|
@@ -318,8 +319,20 @@ function openErrorLines(err, address, liveVersion, org) {
|
|
|
318
319
|
if (err.code === "unavailable_plan_limit") {
|
|
319
320
|
return planLimitLines(err, org);
|
|
320
321
|
}
|
|
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
|
+
if (err.code === "published_limit") {
|
|
326
|
+
return publishedLimitRefused(resolveCountLimit(err.detail, org));
|
|
327
|
+
}
|
|
328
|
+
if (err.code === "storage_ceiling") {
|
|
329
|
+
// It takes no detail. The body is `{"limit":"storage"}` and names nothing
|
|
330
|
+
// else by design — the ceiling is an abuse ceiling, not a plan limit.
|
|
331
|
+
return storageCeilingRefused();
|
|
332
|
+
}
|
|
321
333
|
if (err.code === "publish_too_large") {
|
|
322
|
-
|
|
334
|
+
// `limit` is the MAGNITUDE on this code. See ApiErrorDetail.limit.
|
|
335
|
+
return copy.tooLargeFromServer(err.detail.observed ?? 0, numericLimit(err.detail.limit));
|
|
323
336
|
}
|
|
324
337
|
if (err.code === "rate_limited") {
|
|
325
338
|
return copy.rateLimited(err.retryAfterSec ?? 60);
|
|
@@ -339,6 +352,15 @@ function openErrorLines(err, address, liveVersion, org) {
|
|
|
339
352
|
"",
|
|
340
353
|
];
|
|
341
354
|
}
|
|
355
|
+
/**
|
|
356
|
+
* `detail.limit` on a code whose `limit` is a MAGNITUDE. A string there is the
|
|
357
|
+
* axis-name meaning arriving on a body that should not carry it, and it reads as
|
|
358
|
+
* 0 rather than as `NaN` — a bound printed as "NaN MB" is worse than one printed
|
|
359
|
+
* as a figure the copy already handles.
|
|
360
|
+
*/
|
|
361
|
+
function numericLimit(limit) {
|
|
362
|
+
return typeof limit === "number" ? limit : 0;
|
|
363
|
+
}
|
|
342
364
|
function manifestErrorLines(err, distDir) {
|
|
343
365
|
if (err.reason === "no_dist")
|
|
344
366
|
return copy.noDist(distDir);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The detail every count refusal carries. Pinned to the api's `Decision.Err`
|
|
3
|
+
* renderer: `{limit, count, ceiling, orgName, orgSlug, planName}`.
|
|
4
|
+
*
|
|
5
|
+
* On THESE codes `limit` is the AXIS NAME — "seats" | "published" | "previews" |
|
|
6
|
+
* "storage" — and not a magnitude. It is typed as the transport's union rather
|
|
7
|
+
* than narrowed to `string`, because the same detail bag carries the byte-valued
|
|
8
|
+
* `limit` of `publish_too_large`, and a type that disagreed with the transport
|
|
9
|
+
* would push the mismatch to a cast at the call site.
|
|
10
|
+
*
|
|
11
|
+
* NOTHING BELOW READS IT. The axis is already known from the code that selected
|
|
12
|
+
* the renderer, so reading it back would be the renderer asking the wire to
|
|
13
|
+
* confirm which function it is. It is declared so the shape is complete and so a
|
|
14
|
+
* reader can see the field was considered.
|
|
15
|
+
*/
|
|
16
|
+
export interface CountLimitDetail {
|
|
17
|
+
limit?: number | string;
|
|
18
|
+
count?: number;
|
|
19
|
+
ceiling?: number;
|
|
20
|
+
orgName?: string;
|
|
21
|
+
orgSlug?: string;
|
|
22
|
+
planName?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The org as the CLI can otherwise name it — the machine's configured active
|
|
26
|
+
* org. It is the FALLBACK and never the authority: see `resolveOrg`.
|
|
27
|
+
*/
|
|
28
|
+
export interface LocalOrgIdentity {
|
|
29
|
+
name?: string;
|
|
30
|
+
slug?: string;
|
|
31
|
+
consoleUrl?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface ResolvedCountLimit {
|
|
34
|
+
orgName?: string;
|
|
35
|
+
orgSlug?: string;
|
|
36
|
+
planName?: string;
|
|
37
|
+
count: number;
|
|
38
|
+
ceiling: number;
|
|
39
|
+
consoleUrl?: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* THE WIRE WINS over the machine's configured active org.
|
|
43
|
+
*
|
|
44
|
+
* The API names the org that owns the project being acted on; local config names
|
|
45
|
+
* whichever org the CLI was last pointed at, and on a machine with more than one
|
|
46
|
+
* workspace they are routinely different. Naming the local one would print the
|
|
47
|
+
* wrong workspace on the exact screen that is refusing the creator — and the
|
|
48
|
+
* refusal's figures are about the org the wire named, so a mismatched heading
|
|
49
|
+
* makes the numbers read as wrong rather than the name.
|
|
50
|
+
*
|
|
51
|
+
* The local values remain as a fallback for a body that carried neither, which
|
|
52
|
+
* is a refusal from an older api rather than a shape this client accepts.
|
|
53
|
+
*/
|
|
54
|
+
export declare function resolveCountLimit(detail: CountLimitDetail, local?: LocalOrgIdentity): ResolvedCountLimit;
|
|
55
|
+
/**
|
|
56
|
+
* `Acme (acme)` — name and slug when they differ, one of them when they do not,
|
|
57
|
+
* and an empty string when the wire named neither. Matches f10's shipped
|
|
58
|
+
* plan-limit heading so the four plan refusals read as one family.
|
|
59
|
+
*/
|
|
60
|
+
export declare function orgLabel(name?: string, slug?: string): string;
|
|
61
|
+
/**
|
|
62
|
+
* The figures, STACKED AND ALIGNED rather than inline. They are a comparison —
|
|
63
|
+
* the count against the ceiling — and f2's convention puts two values a creator
|
|
64
|
+
* must judge on their own aligned lines. Inline they are a sentence to parse;
|
|
65
|
+
* stacked, an agent and a human read them the same way.
|
|
66
|
+
*/
|
|
67
|
+
export declare function countRows(i: ResolvedCountLimit, axisLabel: string, planSuffix: string): string[];
|
|
68
|
+
/**
|
|
69
|
+
* f4's rule: where a verb does not exist because the act belongs to another
|
|
70
|
+
* surface, say where it lives and link to it — and if the link cannot be built,
|
|
71
|
+
* drop the link and keep the explanation.
|
|
72
|
+
*
|
|
73
|
+
* `path` is the org-scoped page that shows the axis being refused.
|
|
74
|
+
*/
|
|
75
|
+
export declare function consoleLines(i: ResolvedCountLimit, path: string): string[];
|
|
76
|
+
/**
|
|
77
|
+
* The published-project ceiling, refused at open-publish.
|
|
78
|
+
*
|
|
79
|
+
* "One published project" means ONE LIVE ADDRESS AT A TIME, and the copy says
|
|
80
|
+
* so, because the other reading — one project ever published — is what makes a
|
|
81
|
+
* creator believe they have permanently spent their slot on a mistake. Taking a
|
|
82
|
+
* project offline frees it, and that is the sentence that distinguishes the two.
|
|
83
|
+
*
|
|
84
|
+
* It does NOT say the project being published is excluded from the count, even
|
|
85
|
+
* though it is: a creator refused here is publishing a SECOND project, so the
|
|
86
|
+
* exclusion never applies on the screen that would explain it.
|
|
87
|
+
*/
|
|
88
|
+
export declare function publishedLimitRefused(i: ResolvedCountLimit): string[];
|
|
89
|
+
/**
|
|
90
|
+
* The concurrent-preview ceiling, refused at mint.
|
|
91
|
+
*
|
|
92
|
+
* "Still running" is the load-bearing claim and it is stated first among the
|
|
93
|
+
* facts. A creator whose `vincentt preview` just refused has every reason to
|
|
94
|
+
* believe it took something down — the command that failed is the command that
|
|
95
|
+
* starts previews — and B-F10-3's guarantee (a count limit refuses growth and
|
|
96
|
+
* removes nothing) is invisible unless the refusal says it.
|
|
97
|
+
*/
|
|
98
|
+
export declare function previewLimitRefused(i: ResolvedCountLimit): string[];
|
|
99
|
+
/**
|
|
100
|
+
* The storage ceiling. It NAMES NO FIGURE, and the asymmetry with the three
|
|
101
|
+
* count refusals above is deliberate rather than an omission: the ceiling is an
|
|
102
|
+
* abuse ceiling, not a tier limit. It is flat on every plan, never sold, never
|
|
103
|
+
* displayed, and its only audience is an operator. A number here would be the
|
|
104
|
+
* product printing a magnitude a creator cannot act on and was never offered.
|
|
105
|
+
*
|
|
106
|
+
* Support is therefore the whole remedy — there is no console page showing this
|
|
107
|
+
* and no plan that raises it.
|
|
108
|
+
*/
|
|
109
|
+
export declare function storageCeilingRefused(): string[];
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// The three count-limit refusals — seats, published projects, concurrent live
|
|
2
|
+
// previews — as they reach a creator standing at a ceiling.
|
|
3
|
+
//
|
|
4
|
+
// THE REFUSAL IS THE NOTICE. A count limit is evaluated in the same request as
|
|
5
|
+
// the act it refuses, so no earlier moment exists at which a warning could have
|
|
6
|
+
// been delivered (B-F10-2, as amended). The obligation is discharged by the
|
|
7
|
+
// refusal naming the axis, the current count and the ceiling — which means a
|
|
8
|
+
// refusal rendered without its figures does not discharge it at all. That is the
|
|
9
|
+
// whole reason these arms exist rather than the generic `✗ {message}` default.
|
|
10
|
+
//
|
|
11
|
+
// Two prohibitions govern every string below:
|
|
12
|
+
// - NO PRICE, and no plan other than the one in force. f16's inherited rule: a
|
|
13
|
+
// surface tells a creator where they stand, never what to buy.
|
|
14
|
+
// - NO COMMAND that resolves the refusal. An agent's default response to a
|
|
15
|
+
// failure is to run the next thing it is handed, and there is no CLI verb
|
|
16
|
+
// that frees a seat, unpublishes a project or ends someone else's preview.
|
|
17
|
+
// The copy names the act as a human one, in the console.
|
|
18
|
+
const SUPPORT_EMAIL = "hello@vincentt.studio";
|
|
19
|
+
const DEFAULT_CONSOLE_URL = "https://console.vincentt.studio";
|
|
20
|
+
/**
|
|
21
|
+
* THE WIRE WINS over the machine's configured active org.
|
|
22
|
+
*
|
|
23
|
+
* The API names the org that owns the project being acted on; local config names
|
|
24
|
+
* whichever org the CLI was last pointed at, and on a machine with more than one
|
|
25
|
+
* workspace they are routinely different. Naming the local one would print the
|
|
26
|
+
* wrong workspace on the exact screen that is refusing the creator — and the
|
|
27
|
+
* refusal's figures are about the org the wire named, so a mismatched heading
|
|
28
|
+
* makes the numbers read as wrong rather than the name.
|
|
29
|
+
*
|
|
30
|
+
* The local values remain as a fallback for a body that carried neither, which
|
|
31
|
+
* is a refusal from an older api rather than a shape this client accepts.
|
|
32
|
+
*/
|
|
33
|
+
export function resolveCountLimit(detail, local) {
|
|
34
|
+
return {
|
|
35
|
+
orgName: detail.orgName ?? local?.name,
|
|
36
|
+
orgSlug: detail.orgSlug ?? local?.slug,
|
|
37
|
+
planName: detail.planName,
|
|
38
|
+
count: detail.count ?? 0,
|
|
39
|
+
ceiling: detail.ceiling ?? 0,
|
|
40
|
+
consoleUrl: local?.consoleUrl,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* `Acme (acme)` — name and slug when they differ, one of them when they do not,
|
|
45
|
+
* and an empty string when the wire named neither. Matches f10's shipped
|
|
46
|
+
* plan-limit heading so the four plan refusals read as one family.
|
|
47
|
+
*/
|
|
48
|
+
export function orgLabel(name, slug) {
|
|
49
|
+
if (name && slug && name !== slug)
|
|
50
|
+
return `${name} (${slug})`;
|
|
51
|
+
return name || slug || "";
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The figures, STACKED AND ALIGNED rather than inline. They are a comparison —
|
|
55
|
+
* the count against the ceiling — and f2's convention puts two values a creator
|
|
56
|
+
* must judge on their own aligned lines. Inline they are a sentence to parse;
|
|
57
|
+
* stacked, an agent and a human read them the same way.
|
|
58
|
+
*/
|
|
59
|
+
export function countRows(i, axisLabel, planSuffix) {
|
|
60
|
+
const rows = [];
|
|
61
|
+
if (i.orgName || i.orgSlug)
|
|
62
|
+
rows.push(["Organization", orgLabel(i.orgName, i.orgSlug)]);
|
|
63
|
+
if (i.planName)
|
|
64
|
+
rows.push(["Plan", `${i.planName} — ${planSuffix}`]);
|
|
65
|
+
rows.push([axisLabel, `${i.count} of ${i.ceiling} used`]);
|
|
66
|
+
return alignedRows(rows);
|
|
67
|
+
}
|
|
68
|
+
function alignedRows(rows) {
|
|
69
|
+
const width = Math.max(...rows.map((r) => r[0].length));
|
|
70
|
+
return rows.map(([label, value]) => ` ${label.padEnd(width)} ${value}`);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* f4's rule: where a verb does not exist because the act belongs to another
|
|
74
|
+
* surface, say where it lives and link to it — and if the link cannot be built,
|
|
75
|
+
* drop the link and keep the explanation.
|
|
76
|
+
*
|
|
77
|
+
* `path` is the org-scoped page that shows the axis being refused.
|
|
78
|
+
*/
|
|
79
|
+
export function consoleLines(i, path) {
|
|
80
|
+
if (!i.orgSlug)
|
|
81
|
+
return [` Email ${SUPPORT_EMAIL}.`, ""];
|
|
82
|
+
const base = (i.consoleUrl ?? DEFAULT_CONSOLE_URL).replace(/\/$/, "");
|
|
83
|
+
return [` See ${base}/orgs/${i.orgSlug}/${path}, or email`, ` ${SUPPORT_EMAIL}.`, ""];
|
|
84
|
+
}
|
|
85
|
+
// THERE IS NO SEAT ARM HERE, AND THE ABSENCE IS THE DESIGN.
|
|
86
|
+
//
|
|
87
|
+
// `seat_limit` is refused on `POST /orgs/{slug}/invites`, which the CLI does not
|
|
88
|
+
// call and has no verb for: inviting a person is an act whose authorized actor
|
|
89
|
+
// is a human, so it lives in the console (f15's convention). The seat refusal's
|
|
90
|
+
// copy — including the two ways to free a seat — is the console's to render.
|
|
91
|
+
//
|
|
92
|
+
// A renderer for it added here would be an export with no caller, which is worse
|
|
93
|
+
// than nothing: it reads as coverage and cannot fail.
|
|
94
|
+
/**
|
|
95
|
+
* The published-project ceiling, refused at open-publish.
|
|
96
|
+
*
|
|
97
|
+
* "One published project" means ONE LIVE ADDRESS AT A TIME, and the copy says
|
|
98
|
+
* so, because the other reading — one project ever published — is what makes a
|
|
99
|
+
* creator believe they have permanently spent their slot on a mistake. Taking a
|
|
100
|
+
* project offline frees it, and that is the sentence that distinguishes the two.
|
|
101
|
+
*
|
|
102
|
+
* It does NOT say the project being published is excluded from the count, even
|
|
103
|
+
* though it is: a creator refused here is publishing a SECOND project, so the
|
|
104
|
+
* exclusion never applies on the screen that would explain it.
|
|
105
|
+
*/
|
|
106
|
+
export function publishedLimitRefused(i) {
|
|
107
|
+
return [
|
|
108
|
+
"",
|
|
109
|
+
"✗ This organization is already publishing as many projects as its plan allows.",
|
|
110
|
+
"",
|
|
111
|
+
...countRows(i, "Published", "published projects are counted per organization"),
|
|
112
|
+
"",
|
|
113
|
+
" The count is projects with a live address right now, not projects that have",
|
|
114
|
+
" ever been published. Taking one offline in the console frees the slot, and",
|
|
115
|
+
" everything already published stays exactly as it is.",
|
|
116
|
+
"",
|
|
117
|
+
...consoleLines(i, "projects"),
|
|
118
|
+
" Nothing was uploaded.",
|
|
119
|
+
"",
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The concurrent-preview ceiling, refused at mint.
|
|
124
|
+
*
|
|
125
|
+
* "Still running" is the load-bearing claim and it is stated first among the
|
|
126
|
+
* facts. A creator whose `vincentt preview` just refused has every reason to
|
|
127
|
+
* believe it took something down — the command that failed is the command that
|
|
128
|
+
* starts previews — and B-F10-3's guarantee (a count limit refuses growth and
|
|
129
|
+
* removes nothing) is invisible unless the refusal says it.
|
|
130
|
+
*/
|
|
131
|
+
export function previewLimitRefused(i) {
|
|
132
|
+
return [
|
|
133
|
+
"",
|
|
134
|
+
"✗ This organization has as many previews running as its plan allows.",
|
|
135
|
+
"",
|
|
136
|
+
...countRows(i, "Previews", "previews are counted per organization"),
|
|
137
|
+
"",
|
|
138
|
+
" Every preview that was running is still running — this one did not start.",
|
|
139
|
+
" A preview stops when whoever started it stops it, or when it reaches its",
|
|
140
|
+
" 12-hour limit.",
|
|
141
|
+
"",
|
|
142
|
+
...consoleLines(i, "projects"),
|
|
143
|
+
" Nothing was started.",
|
|
144
|
+
"",
|
|
145
|
+
];
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The storage ceiling. It NAMES NO FIGURE, and the asymmetry with the three
|
|
149
|
+
* count refusals above is deliberate rather than an omission: the ceiling is an
|
|
150
|
+
* abuse ceiling, not a tier limit. It is flat on every plan, never sold, never
|
|
151
|
+
* displayed, and its only audience is an operator. A number here would be the
|
|
152
|
+
* product printing a magnitude a creator cannot act on and was never offered.
|
|
153
|
+
*
|
|
154
|
+
* Support is therefore the whole remedy — there is no console page showing this
|
|
155
|
+
* and no plan that raises it.
|
|
156
|
+
*/
|
|
157
|
+
export function storageCeilingRefused() {
|
|
158
|
+
return [
|
|
159
|
+
"",
|
|
160
|
+
"✗ This organization cannot upload more files right now.",
|
|
161
|
+
"",
|
|
162
|
+
" This is not a plan limit and taking a different plan will not change it.",
|
|
163
|
+
"",
|
|
164
|
+
` Email ${SUPPORT_EMAIL} and it can be looked at.`,
|
|
165
|
+
"",
|
|
166
|
+
" Nothing was uploaded.",
|
|
167
|
+
"",
|
|
168
|
+
];
|
|
169
|
+
}
|
package/package.json
CHANGED