@vincentt-xr/cli 0.16.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 +196 -87
- 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 +24 -11
- package/dist/preview/api.js +42 -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 +39 -6
- package/dist/publish/api.js +80 -9
- package/dist/publish/command.d.ts +25 -0
- package/dist/publish/command.js +230 -32
- 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 +114 -0
- package/dist/shared/countLimit.js +176 -0
- package/dist/shared/http.d.ts +23 -0
- package/dist/shared/http.js +58 -0
- package/package.json +7 -3
package/dist/shared/config.js
CHANGED
|
@@ -12,6 +12,7 @@ import { randomBytes } from "node:crypto";
|
|
|
12
12
|
import { promises as fs } from "node:fs";
|
|
13
13
|
import os from "node:os";
|
|
14
14
|
import path from "node:path";
|
|
15
|
+
import { NotSignedInError } from "./authError.js";
|
|
15
16
|
import { recordLocation } from "./recordLocation.js";
|
|
16
17
|
export const MACHINE_CONFIG_PATH = path.join(os.homedir(), ".vincentt", "config.json");
|
|
17
18
|
const BINDING_DIR = ".vincentt";
|
|
@@ -216,9 +217,41 @@ async function ensureGitignored(cwd, pattern) {
|
|
|
216
217
|
// A missing/unwritable .gitignore must not fail project creation.
|
|
217
218
|
}
|
|
218
219
|
}
|
|
219
|
-
|
|
220
|
+
/**
|
|
221
|
+
* Every environment variable the CLI reads a credential from. They are removed
|
|
222
|
+
* from the environment of the creator's dev command, which is repo-controlled.
|
|
223
|
+
* A credential read added to this file without a name here fails a unit test.
|
|
224
|
+
*/
|
|
225
|
+
export const CLI_CREDENTIAL_ENV = ["VINCENTT_PAT"];
|
|
226
|
+
export function withoutCliCredentials(env) {
|
|
227
|
+
const out = { ...env };
|
|
228
|
+
// Compared upper-cased: Windows environment names are case-insensitive.
|
|
229
|
+
for (const key of Object.keys(out)) {
|
|
230
|
+
if (CLI_CREDENTIAL_ENV.includes(key.toUpperCase()))
|
|
231
|
+
delete out[key];
|
|
232
|
+
}
|
|
233
|
+
return out;
|
|
234
|
+
}
|
|
235
|
+
export const DEFAULT_API_URL = "https://api.vincentt.studio";
|
|
236
|
+
/**
|
|
237
|
+
* The API host the person running the command configured: an explicit flag, then
|
|
238
|
+
* VINCENTT_API_URL, then the machine config, then the default. Every verb reads
|
|
239
|
+
* its host through here, so a token is never sent to one host by one verb and to
|
|
240
|
+
* another by the next. A binding's override is layered on top by decideApiUrl.
|
|
241
|
+
*/
|
|
242
|
+
export function configuredApiUrl(input) {
|
|
243
|
+
return strip(input.flagApiUrl || input.envApiUrl || input.machineApiUrl || DEFAULT_API_URL);
|
|
244
|
+
}
|
|
245
|
+
/** configuredApiUrl from this process's environment and a machine config already read. */
|
|
246
|
+
export function machineApiUrl(machine, flagApiUrl) {
|
|
247
|
+
return configuredApiUrl({
|
|
248
|
+
flagApiUrl,
|
|
249
|
+
envApiUrl: process.env.VINCENTT_API_URL,
|
|
250
|
+
machineApiUrl: machine.apiUrl,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
220
253
|
export function decideApiUrl(input) {
|
|
221
|
-
const configuredUrl = input
|
|
254
|
+
const configuredUrl = configuredApiUrl(input);
|
|
222
255
|
const binding = input.bindingApiUrl;
|
|
223
256
|
if (!binding || strip(binding) === strip(configuredUrl)) {
|
|
224
257
|
return { apiUrl: configuredUrl, configuredUrl };
|
|
@@ -255,10 +288,8 @@ export async function resolveConfig(cwd, opts = {}) {
|
|
|
255
288
|
}
|
|
256
289
|
}
|
|
257
290
|
const pat = process.env.VINCENTT_PAT ?? machine.pat;
|
|
258
|
-
if (!pat)
|
|
259
|
-
throw new
|
|
260
|
-
`or set the VINCENTT_PAT env var.`);
|
|
261
|
-
}
|
|
291
|
+
if (!pat)
|
|
292
|
+
throw new NotSignedInError();
|
|
262
293
|
return { apiUrl: strip(decided.apiUrl), pat };
|
|
263
294
|
}
|
|
264
295
|
const BINDING_REL_PATH = `${BINDING_DIR}/${BINDING_FILE}`;
|
|
@@ -292,8 +323,8 @@ export function apiUrlAdvisory(decided, ctx) {
|
|
|
292
323
|
` your machine config:${pad} ${decided.configuredUrl}`,
|
|
293
324
|
"",
|
|
294
325
|
" Your access token is sent with every request, so it would go to",
|
|
295
|
-
` ${hostOf(decided.ignoredBindingApiUrl)}. A binding file travels with a repository
|
|
296
|
-
" anyone who can commit to it can set this.",
|
|
326
|
+
` ${hostOf(decided.ignoredBindingApiUrl)}. A binding file travels with a repository,`,
|
|
327
|
+
" so anyone who can commit to it can set this.",
|
|
297
328
|
"",
|
|
298
329
|
" Ignoring the binding and using your machine config.",
|
|
299
330
|
" To use the binding's API on purpose, re-run with --allow-custom-api.",
|
|
@@ -0,0 +1,114 @@
|
|
|
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 or at commit.
|
|
78
|
+
*
|
|
79
|
+
* Commit re-checks because two publishes opened in the same org can both be
|
|
80
|
+
* admitted and race to go live; the loser is refused after its bytes were
|
|
81
|
+
* uploaded. The api keeps those bytes as a stored version, so the last line
|
|
82
|
+
* changes to say so and nothing else does.
|
|
83
|
+
*
|
|
84
|
+
* "One published project" means ONE LIVE ADDRESS AT A TIME, and the copy says
|
|
85
|
+
* so, because the other reading — one project ever published — is what makes a
|
|
86
|
+
* creator believe they have permanently spent their slot on a mistake. Taking a
|
|
87
|
+
* project offline frees it, and that is the sentence that distinguishes the two.
|
|
88
|
+
*
|
|
89
|
+
* It does NOT say the project being published is excluded from the count, even
|
|
90
|
+
* though it is: a creator refused here is publishing a SECOND project, so the
|
|
91
|
+
* exclusion never applies on the screen that would explain it.
|
|
92
|
+
*/
|
|
93
|
+
export declare function publishedLimitRefused(i: ResolvedCountLimit, stage?: "open" | "commit"): string[];
|
|
94
|
+
/**
|
|
95
|
+
* The concurrent-preview ceiling, refused at mint.
|
|
96
|
+
*
|
|
97
|
+
* "Still running" is the load-bearing claim and it is stated first among the
|
|
98
|
+
* facts. A creator whose `vincentt preview` just refused has every reason to
|
|
99
|
+
* believe it took something down — the command that failed is the command that
|
|
100
|
+
* starts previews — and B-F10-3's guarantee (a count limit refuses growth and
|
|
101
|
+
* removes nothing) is invisible unless the refusal says it.
|
|
102
|
+
*/
|
|
103
|
+
export declare function previewLimitRefused(i: ResolvedCountLimit): string[];
|
|
104
|
+
/**
|
|
105
|
+
* The storage ceiling. It NAMES NO FIGURE, and the asymmetry with the three
|
|
106
|
+
* count refusals above is deliberate rather than an omission: the ceiling is an
|
|
107
|
+
* abuse ceiling, not a tier limit. It is flat on every plan, never sold, never
|
|
108
|
+
* displayed, and its only audience is an operator. A number here would be the
|
|
109
|
+
* product printing a magnitude a creator cannot act on and was never offered.
|
|
110
|
+
*
|
|
111
|
+
* Support is therefore the whole remedy — there is no console page showing this
|
|
112
|
+
* and no plan that raises it.
|
|
113
|
+
*/
|
|
114
|
+
export declare function storageCeilingRefused(): string[];
|
|
@@ -0,0 +1,176 @@
|
|
|
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 or at commit.
|
|
96
|
+
*
|
|
97
|
+
* Commit re-checks because two publishes opened in the same org can both be
|
|
98
|
+
* admitted and race to go live; the loser is refused after its bytes were
|
|
99
|
+
* uploaded. The api keeps those bytes as a stored version, so the last line
|
|
100
|
+
* changes to say so and nothing else does.
|
|
101
|
+
*
|
|
102
|
+
* "One published project" means ONE LIVE ADDRESS AT A TIME, and the copy says
|
|
103
|
+
* so, because the other reading — one project ever published — is what makes a
|
|
104
|
+
* creator believe they have permanently spent their slot on a mistake. Taking a
|
|
105
|
+
* project offline frees it, and that is the sentence that distinguishes the two.
|
|
106
|
+
*
|
|
107
|
+
* It does NOT say the project being published is excluded from the count, even
|
|
108
|
+
* though it is: a creator refused here is publishing a SECOND project, so the
|
|
109
|
+
* exclusion never applies on the screen that would explain it.
|
|
110
|
+
*/
|
|
111
|
+
export function publishedLimitRefused(i, stage = "open") {
|
|
112
|
+
return [
|
|
113
|
+
"",
|
|
114
|
+
"✗ This organization is already publishing as many projects as its plan allows.",
|
|
115
|
+
"",
|
|
116
|
+
...countRows(i, "Published", "published projects are counted per organization"),
|
|
117
|
+
"",
|
|
118
|
+
" The count is projects with a live address right now, not projects that have",
|
|
119
|
+
" ever been published. Taking one offline in the console frees the slot, and",
|
|
120
|
+
" everything already published stays exactly as it is.",
|
|
121
|
+
"",
|
|
122
|
+
...consoleLines(i, "projects"),
|
|
123
|
+
stage === "commit"
|
|
124
|
+
? " Your files were uploaded and kept, but nothing went live."
|
|
125
|
+
: " Nothing was uploaded.",
|
|
126
|
+
"",
|
|
127
|
+
];
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The concurrent-preview ceiling, refused at mint.
|
|
131
|
+
*
|
|
132
|
+
* "Still running" is the load-bearing claim and it is stated first among the
|
|
133
|
+
* facts. A creator whose `vincentt preview` just refused has every reason to
|
|
134
|
+
* believe it took something down — the command that failed is the command that
|
|
135
|
+
* starts previews — and B-F10-3's guarantee (a count limit refuses growth and
|
|
136
|
+
* removes nothing) is invisible unless the refusal says it.
|
|
137
|
+
*/
|
|
138
|
+
export function previewLimitRefused(i) {
|
|
139
|
+
return [
|
|
140
|
+
"",
|
|
141
|
+
"✗ This organization has as many previews running as its plan allows.",
|
|
142
|
+
"",
|
|
143
|
+
...countRows(i, "Previews", "previews are counted per organization"),
|
|
144
|
+
"",
|
|
145
|
+
" Every preview that was running is still running. This one did not start.",
|
|
146
|
+
" A preview stops when whoever started it stops it, or when it reaches its",
|
|
147
|
+
" 12-hour limit.",
|
|
148
|
+
"",
|
|
149
|
+
...consoleLines(i, "projects"),
|
|
150
|
+
" Nothing was started.",
|
|
151
|
+
"",
|
|
152
|
+
];
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The storage ceiling. It NAMES NO FIGURE, and the asymmetry with the three
|
|
156
|
+
* count refusals above is deliberate rather than an omission: the ceiling is an
|
|
157
|
+
* abuse ceiling, not a tier limit. It is flat on every plan, never sold, never
|
|
158
|
+
* displayed, and its only audience is an operator. A number here would be the
|
|
159
|
+
* product printing a magnitude a creator cannot act on and was never offered.
|
|
160
|
+
*
|
|
161
|
+
* Support is therefore the whole remedy — there is no console page showing this
|
|
162
|
+
* and no plan that raises it.
|
|
163
|
+
*/
|
|
164
|
+
export function storageCeilingRefused() {
|
|
165
|
+
return [
|
|
166
|
+
"",
|
|
167
|
+
"✗ This organization cannot upload more files right now.",
|
|
168
|
+
"",
|
|
169
|
+
" This is not a plan limit and taking a different plan will not change it.",
|
|
170
|
+
"",
|
|
171
|
+
` Email ${SUPPORT_EMAIL} and it can be looked at.`,
|
|
172
|
+
"",
|
|
173
|
+
" Nothing was uploaded.",
|
|
174
|
+
"",
|
|
175
|
+
];
|
|
176
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** A request that did not finish inside its deadline. Nothing is known about its outcome. */
|
|
2
|
+
export declare class RequestTimeout extends Error {
|
|
3
|
+
readonly ms: number;
|
|
4
|
+
constructor(ms: number);
|
|
5
|
+
}
|
|
6
|
+
export declare const API_DEADLINE_MS = 30000;
|
|
7
|
+
/**
|
|
8
|
+
* Wrap a fetch so each call is aborted after `ms`. The deadline covers the body
|
|
9
|
+
* read too: the timer is not cleared when headers arrive, and aborting a request
|
|
10
|
+
* whose body was already read is a no-op. The timer is unref'd so it never holds
|
|
11
|
+
* the process open. A caller's own signal still aborts the request.
|
|
12
|
+
*/
|
|
13
|
+
export declare function withDeadline(fetchImpl: typeof fetch, ms: number): typeof fetch;
|
|
14
|
+
/**
|
|
15
|
+
* The global fetch with the api deadline. Looked up per call, never captured, so
|
|
16
|
+
* a test that stubs the global still reaches its stub.
|
|
17
|
+
*/
|
|
18
|
+
export declare const apiFetch: typeof fetch;
|
|
19
|
+
/**
|
|
20
|
+
* A request abandoned by the network or by its deadline, as opposed to a
|
|
21
|
+
* response the server sent. Its outcome on the server is unknown.
|
|
22
|
+
*/
|
|
23
|
+
export declare function isTransportError(err: unknown): boolean;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// The one fetch wrapper with a deadline. Every api call goes through apiFetch, so
|
|
2
|
+
// a stalled server or a dead network ends in a bounded error instead of a hang
|
|
3
|
+
// (including the Ctrl-C path, which ends the preview session with an api call).
|
|
4
|
+
/** A request that did not finish inside its deadline. Nothing is known about its outcome. */
|
|
5
|
+
export class RequestTimeout extends Error {
|
|
6
|
+
ms;
|
|
7
|
+
constructor(ms) {
|
|
8
|
+
super(`the request did not finish within ${Math.round(ms / 1000)}s`);
|
|
9
|
+
this.ms = ms;
|
|
10
|
+
this.name = "RequestTimeout";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export const API_DEADLINE_MS = 30_000;
|
|
14
|
+
/**
|
|
15
|
+
* Wrap a fetch so each call is aborted after `ms`. The deadline covers the body
|
|
16
|
+
* read too: the timer is not cleared when headers arrive, and aborting a request
|
|
17
|
+
* whose body was already read is a no-op. The timer is unref'd so it never holds
|
|
18
|
+
* the process open. A caller's own signal still aborts the request.
|
|
19
|
+
*/
|
|
20
|
+
export function withDeadline(fetchImpl, ms) {
|
|
21
|
+
return (async (input, init = {}) => {
|
|
22
|
+
const controller = new AbortController();
|
|
23
|
+
const timer = setTimeout(() => controller.abort(new RequestTimeout(ms)), ms);
|
|
24
|
+
timer.unref?.();
|
|
25
|
+
const outer = init.signal;
|
|
26
|
+
if (outer) {
|
|
27
|
+
if (outer.aborted)
|
|
28
|
+
controller.abort(outer.reason);
|
|
29
|
+
else
|
|
30
|
+
outer.addEventListener("abort", () => controller.abort(outer.reason), {
|
|
31
|
+
once: true,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
return await fetchImpl(input, { ...init, signal: controller.signal });
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
if (controller.signal.reason instanceof RequestTimeout)
|
|
39
|
+
throw controller.signal.reason;
|
|
40
|
+
throw err;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The global fetch with the api deadline. Looked up per call, never captured, so
|
|
46
|
+
* a test that stubs the global still reaches its stub.
|
|
47
|
+
*/
|
|
48
|
+
export const apiFetch = withDeadline((input, init) => globalThis.fetch(input, init), API_DEADLINE_MS);
|
|
49
|
+
/**
|
|
50
|
+
* A request abandoned by the network or by its deadline, as opposed to a
|
|
51
|
+
* response the server sent. Its outcome on the server is unknown.
|
|
52
|
+
*/
|
|
53
|
+
export function isTransportError(err) {
|
|
54
|
+
if (err instanceof RequestTimeout)
|
|
55
|
+
return true;
|
|
56
|
+
// undici reports a refused, reset or unresolvable connection as a TypeError.
|
|
57
|
+
return err instanceof TypeError || (err instanceof Error && err.name === "AbortError");
|
|
58
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincentt-xr/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Vincentt CLI — the vincentt command-line dev loop (login, create, publish, preview, diagnostics, feedback, relay)",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"files": [
|
|
8
|
-
"dist"
|
|
8
|
+
"dist",
|
|
9
|
+
"MIGRATION.md"
|
|
9
10
|
],
|
|
10
11
|
"exports": {
|
|
11
12
|
"./preview": {
|
|
@@ -20,11 +21,14 @@
|
|
|
20
21
|
"bin": {
|
|
21
22
|
"vincentt": "./dist/cli/index.js"
|
|
22
23
|
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
23
27
|
"dependencies": {
|
|
24
28
|
"qrcode": "^1.5.4",
|
|
25
29
|
"ws": "^8.18.0",
|
|
26
30
|
"zod": "^3.23.8",
|
|
27
|
-
"@vincentt-xr/harness": "1.
|
|
31
|
+
"@vincentt-xr/harness": "1.6.0"
|
|
28
32
|
},
|
|
29
33
|
"devDependencies": {
|
|
30
34
|
"@types/qrcode": "^1.5.5",
|