@vincentt-xr/cli 0.15.2 → 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/args.js +1 -0
- package/dist/cli/index.js +198 -3
- package/dist/cli/usage-text.js +27 -0
- package/dist/cli/verb-surface.d.ts +3 -1
- package/dist/cli/verb-surface.js +12 -0
- package/dist/domain/api.d.ts +51 -0
- package/dist/domain/api.js +145 -0
- package/dist/domain/command.d.ts +75 -0
- package/dist/domain/command.js +467 -0
- package/dist/domain/copy.d.ts +334 -0
- package/dist/domain/copy.js +952 -0
- package/dist/domain/hostname.d.ts +57 -0
- package/dist/domain/hostname.js +116 -0
- package/dist/domain/names.d.ts +78 -0
- package/dist/domain/names.js +113 -0
- package/dist/domain/routedVerbs.d.ts +10 -0
- package/dist/domain/routedVerbs.js +30 -0
- package/dist/domain/wire.d.ts +140 -0
- package/dist/domain/wire.js +174 -0
- package/dist/dx/command.d.ts +13 -4
- package/dist/dx/command.js +26 -5
- package/dist/dx/report.d.ts +39 -7
- package/dist/dx/report.js +71 -20
- package/dist/dx/send.d.ts +22 -4
- package/dist/dx/send.js +29 -8
- package/dist/dx/skeleton.js +19 -0
- package/dist/dx/unbiddenVerbs.d.ts +25 -0
- package/dist/dx/unbiddenVerbs.js +39 -0
- package/dist/lib/initCommand.d.ts +3 -1
- package/dist/lib/initCommand.js +10 -0
- package/dist/lib/projects.d.ts +8 -1
- package/dist/lib/projects.js +12 -2
- package/dist/platform/allowlist.d.ts +36 -0
- package/dist/platform/allowlist.js +34 -0
- package/dist/platform/behind.d.ts +41 -0
- package/dist/platform/behind.js +82 -0
- package/dist/platform/command.d.ts +16 -0
- package/dist/platform/command.js +90 -0
- package/dist/platform/footer.d.ts +60 -0
- package/dist/platform/footer.js +101 -0
- package/dist/platform/notice.d.ts +29 -0
- package/dist/platform/notice.js +66 -0
- package/dist/platform/offSwitch.d.ts +19 -0
- package/dist/platform/offSwitch.js +21 -0
- package/dist/platform/outdated.d.ts +25 -0
- package/dist/platform/outdated.js +246 -0
- package/dist/platform/releaseCache.d.ts +69 -0
- package/dist/platform/releaseCache.js +140 -0
- package/dist/platform/releaseFetch.d.ts +51 -0
- package/dist/platform/releaseFetch.js +150 -0
- package/dist/platform/resolve.d.ts +30 -0
- package/dist/platform/resolve.js +128 -0
- package/dist/platform/templateUpgrade.d.ts +56 -0
- package/dist/platform/templateUpgrade.js +476 -0
- package/dist/platform/upgradeReport.d.ts +132 -0
- package/dist/platform/upgradeReport.js +258 -0
- package/dist/platform/versionSummary.d.ts +82 -0
- package/dist/platform/versionSummary.js +181 -0
- package/dist/platform/zonePath.d.ts +154 -0
- package/dist/platform/zonePath.js +286 -0
- package/dist/platform/zones.d.ts +64 -0
- package/dist/platform/zones.js +79 -0
- package/dist/preview/api.d.ts +19 -0
- package/dist/preview/api.js +29 -0
- package/dist/publish/api.d.ts +32 -5
- package/dist/publish/api.js +19 -5
- package/dist/publish/command.d.ts +10 -0
- package/dist/publish/command.js +30 -2
- package/dist/publish/copy.d.ts +14 -1
- package/dist/publish/copy.js +32 -3
- package/dist/scaffold/index.d.ts +50 -3
- package/dist/scaffold/index.js +61 -1
- package/dist/shared/config.d.ts +16 -0
- package/dist/shared/config.js +9 -1
- package/dist/shared/countLimit.d.ts +109 -0
- package/dist/shared/countLimit.js +169 -0
- package/package.json +1 -1
- package/dist/dx/qa.f7.fixtures.d.ts +0 -73
- package/dist/dx/qa.f7.fixtures.js +0 -58
- package/dist/publish/qa.f4.fixtures.d.ts +0 -25
- package/dist/publish/qa.f4.fixtures.js +0 -140
- package/dist/relay/qa.f12.fixtures.d.ts +0 -154
- package/dist/relay/qa.f12.fixtures.js +0 -358
package/dist/publish/api.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ResolvedConfig } from "../shared/config.js";
|
|
2
2
|
import type { ManifestEntry } from "./manifest.js";
|
|
3
|
+
import type { BuiltWith } from "../platform/resolve.js";
|
|
3
4
|
export interface PublishProject {
|
|
4
5
|
id: string;
|
|
5
6
|
slug: string;
|
|
@@ -40,7 +41,15 @@ export interface CommitResult {
|
|
|
40
41
|
releaseNote?: string;
|
|
41
42
|
}
|
|
42
43
|
export interface ApiErrorDetail {
|
|
43
|
-
|
|
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;
|
|
44
53
|
observed?: number;
|
|
45
54
|
missing?: number;
|
|
46
55
|
mismatched?: number;
|
|
@@ -66,6 +75,14 @@ export interface ApiErrorDetail {
|
|
|
66
75
|
*/
|
|
67
76
|
orgName?: string;
|
|
68
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;
|
|
69
86
|
}
|
|
70
87
|
export declare class PublishApiError extends Error {
|
|
71
88
|
readonly status: number;
|
|
@@ -77,11 +94,21 @@ export declare class PublishApiError extends Error {
|
|
|
77
94
|
/** The ONE call before the checklist. liveVersion + livePublishedAt ride it, by design. */
|
|
78
95
|
export declare function readProject(cfg: ResolvedConfig, projectId: string, fetchImpl?: typeof fetch): Promise<PublishProject>;
|
|
79
96
|
/**
|
|
80
|
-
* Open an upload. The request body carries `files` (path/size/sha256)
|
|
81
|
-
* optional `note`, AND NOTHING ELSE — there is no
|
|
82
|
-
* field a parsed title, description or image URL
|
|
97
|
+
* Open an upload. The request body carries `files` (path/size/sha256), an
|
|
98
|
+
* optional `note`, and an optional `builtWith`, AND NOTHING ELSE — there is no
|
|
99
|
+
* field for a MIME type and no field a parsed title, description or image URL
|
|
100
|
+
* could land in.
|
|
101
|
+
*
|
|
102
|
+
* `builtWith` CANNOT FAIL A PUBLISH. It is evidence and never authorization: it
|
|
103
|
+
* is computed on a machine the platform does not control, so nothing may branch
|
|
104
|
+
* on it, and the API validates it in its own pass after the manifest, dropping
|
|
105
|
+
* over-bound entries rather than refusing. An instrument that can break the
|
|
106
|
+
* product's most important write path is worse than an instrument with a gap —
|
|
107
|
+
* which is why it is omitted entirely when nothing resolved rather than sent as
|
|
108
|
+
* an empty object. `{}` means "resolution ran and found nothing", a different
|
|
109
|
+
* and specific fact.
|
|
83
110
|
*/
|
|
84
|
-
export declare function openUpload(cfg: ResolvedConfig, projectId: string, files: ManifestEntry[], note: string | undefined, fetchImpl?: typeof fetch): Promise<OpenedUpload>;
|
|
111
|
+
export declare function openUpload(cfg: ResolvedConfig, projectId: string, files: ManifestEntry[], note: string | undefined, fetchImpl?: typeof fetch, builtWith?: BuiltWith): Promise<OpenedUpload>;
|
|
85
112
|
export declare function commitUpload(cfg: ResolvedConfig, projectId: string, uploadId: string, fetchImpl?: typeof fetch): Promise<CommitResult>;
|
|
86
113
|
/** Abandon an open upload. Idempotent server-side; a failure here must never mask the real error. */
|
|
87
114
|
export declare function abandonUpload(cfg: ResolvedConfig, projectId: string, uploadId: string, fetchImpl?: typeof fetch): Promise<void>;
|
package/dist/publish/api.js
CHANGED
|
@@ -58,15 +58,29 @@ export async function readProject(cfg, projectId, fetchImpl = fetch) {
|
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
60
|
/**
|
|
61
|
-
* Open an upload. The request body carries `files` (path/size/sha256)
|
|
62
|
-
* optional `note`, AND NOTHING ELSE — there is no
|
|
63
|
-
* field a parsed title, description or image URL
|
|
61
|
+
* Open an upload. The request body carries `files` (path/size/sha256), an
|
|
62
|
+
* optional `note`, and an optional `builtWith`, AND NOTHING ELSE — there is no
|
|
63
|
+
* field for a MIME type and no field a parsed title, description or image URL
|
|
64
|
+
* could land in.
|
|
65
|
+
*
|
|
66
|
+
* `builtWith` CANNOT FAIL A PUBLISH. It is evidence and never authorization: it
|
|
67
|
+
* is computed on a machine the platform does not control, so nothing may branch
|
|
68
|
+
* on it, and the API validates it in its own pass after the manifest, dropping
|
|
69
|
+
* over-bound entries rather than refusing. An instrument that can break the
|
|
70
|
+
* product's most important write path is worse than an instrument with a gap —
|
|
71
|
+
* which is why it is omitted entirely when nothing resolved rather than sent as
|
|
72
|
+
* an empty object. `{}` means "resolution ran and found nothing", a different
|
|
73
|
+
* and specific fact.
|
|
64
74
|
*/
|
|
65
|
-
export async function openUpload(cfg, projectId, files, note, fetchImpl = fetch) {
|
|
75
|
+
export async function openUpload(cfg, projectId, files, note, fetchImpl = fetch, builtWith) {
|
|
66
76
|
const res = await fetchImpl(`${cfg.apiUrl}/projects/${encodeURIComponent(projectId)}/publishes`, {
|
|
67
77
|
method: "POST",
|
|
68
78
|
headers: { ...authHeaders(cfg), "content-type": "application/json" },
|
|
69
|
-
body: JSON.stringify({
|
|
79
|
+
body: JSON.stringify({
|
|
80
|
+
files,
|
|
81
|
+
...(note ? { note } : {}),
|
|
82
|
+
...(builtWith ? { builtWith } : {}),
|
|
83
|
+
}),
|
|
70
84
|
});
|
|
71
85
|
if (!res.ok)
|
|
72
86
|
throw await readError(res);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ResolvedConfig } from "../shared/config.js";
|
|
2
|
+
import { type BuiltWith } from "../platform/resolve.js";
|
|
2
3
|
export interface PublishDeps {
|
|
3
4
|
print: (line: string) => void;
|
|
4
5
|
/** Raw write, for the redrawn progress line only. */
|
|
@@ -21,6 +22,15 @@ export interface PublishDeps {
|
|
|
21
22
|
/** The active org's slug, for the usage deep link on a plan-limit refusal. */
|
|
22
23
|
orgSlug?: () => Promise<string | undefined>;
|
|
23
24
|
consoleUrl?: string;
|
|
25
|
+
/**
|
|
26
|
+
* What this project resolved on this machine, attached to the version row.
|
|
27
|
+
*
|
|
28
|
+
* Injected so the publish path can be tested without a `node_modules` tree,
|
|
29
|
+
* and defaulted to f17's SHARED resolver — the same one the report path calls,
|
|
30
|
+
* because two payloads carrying byte-identical values for one project is the
|
|
31
|
+
* mechanical proof there is one resolver and not two that drifted.
|
|
32
|
+
*/
|
|
33
|
+
builtWith?: () => Promise<BuiltWith | undefined>;
|
|
24
34
|
}
|
|
25
35
|
export interface PublishOptions {
|
|
26
36
|
distDir?: string;
|
package/dist/publish/command.js
CHANGED
|
@@ -17,6 +17,8 @@ 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";
|
|
21
|
+
import { resolveBuiltWith } from "../platform/resolve.js";
|
|
20
22
|
const COMMIT_RETRIES = 3;
|
|
21
23
|
const COMMIT_RETRY_DELAY_MS = 2000;
|
|
22
24
|
export async function publishCommand(deps, opts = {}) {
|
|
@@ -140,6 +142,10 @@ export async function publishCommand(deps, opts = {}) {
|
|
|
140
142
|
return 0;
|
|
141
143
|
}
|
|
142
144
|
}
|
|
145
|
+
// Resolved AFTER the confirmation and never before it: a refused publish must
|
|
146
|
+
// read nothing about the creator's installed packages. A fault here yields
|
|
147
|
+
// undefined and the field is omitted — it can never fail a publish.
|
|
148
|
+
const builtWith = await (deps.builtWith ?? (() => resolveBuiltWith(deps.cwd)))().catch(() => undefined);
|
|
143
149
|
return runUpload(deps, cfg, {
|
|
144
150
|
projectId: binding.projectId,
|
|
145
151
|
distDir,
|
|
@@ -148,6 +154,7 @@ export async function publishCommand(deps, opts = {}) {
|
|
|
148
154
|
address,
|
|
149
155
|
liveVersion: project.liveVersion ?? undefined,
|
|
150
156
|
fetchImpl,
|
|
157
|
+
builtWith,
|
|
151
158
|
org: {
|
|
152
159
|
name: orgName || undefined,
|
|
153
160
|
slug: (await deps.orgSlug?.()) || undefined,
|
|
@@ -158,7 +165,7 @@ export async function publishCommand(deps, opts = {}) {
|
|
|
158
165
|
async function runUpload(deps, cfg, i) {
|
|
159
166
|
let opened;
|
|
160
167
|
try {
|
|
161
|
-
opened = await openUpload(cfg, i.projectId, i.manifest.files, i.note, i.fetchImpl);
|
|
168
|
+
opened = await openUpload(cfg, i.projectId, i.manifest.files, i.note, i.fetchImpl, i.builtWith);
|
|
162
169
|
}
|
|
163
170
|
catch (err) {
|
|
164
171
|
for (const l of openErrorLines(err, i.address, i.liveVersion, i.org))
|
|
@@ -312,8 +319,20 @@ function openErrorLines(err, address, liveVersion, org) {
|
|
|
312
319
|
if (err.code === "unavailable_plan_limit") {
|
|
313
320
|
return planLimitLines(err, org);
|
|
314
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
|
+
}
|
|
315
333
|
if (err.code === "publish_too_large") {
|
|
316
|
-
|
|
334
|
+
// `limit` is the MAGNITUDE on this code. See ApiErrorDetail.limit.
|
|
335
|
+
return copy.tooLargeFromServer(err.detail.observed ?? 0, numericLimit(err.detail.limit));
|
|
317
336
|
}
|
|
318
337
|
if (err.code === "rate_limited") {
|
|
319
338
|
return copy.rateLimited(err.retryAfterSec ?? 60);
|
|
@@ -333,6 +352,15 @@ function openErrorLines(err, address, liveVersion, org) {
|
|
|
333
352
|
"",
|
|
334
353
|
];
|
|
335
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
|
+
}
|
|
336
364
|
function manifestErrorLines(err, distDir) {
|
|
337
365
|
if (err.reason === "no_dist")
|
|
338
366
|
return copy.noDist(distDir);
|
package/dist/publish/copy.d.ts
CHANGED
|
@@ -205,5 +205,18 @@ export declare function rateLimited(retryAfterSec: number): string[];
|
|
|
205
205
|
* none.
|
|
206
206
|
*/
|
|
207
207
|
export declare function consoleVerb(verb: string, versionsUrl?: string): string[];
|
|
208
|
-
/**
|
|
208
|
+
/**
|
|
209
|
+
* The verbs that land on that block — f4's and f10's six, plus f15's five.
|
|
210
|
+
*
|
|
211
|
+
* NONE OF THESE GAINS A HANDLER. They are routed to an answer precisely because
|
|
212
|
+
* they do not exist: f15 deliberately gives the CLI no checkout verb, since the
|
|
213
|
+
* authorized actor is a human reading consequence copy in a browser and a verb
|
|
214
|
+
* whose whole contribution is printing a URL is a browser flow with a shell in
|
|
215
|
+
* front of it.
|
|
216
|
+
*
|
|
217
|
+
* Routing them rather than ignoring them is about f15's actual actor. A human
|
|
218
|
+
* who types `vincentt upgrade` and gets "unrecognised command" shrugs and reads
|
|
219
|
+
* the help. AN AGENT RETRIES, or guesses a neighbouring verb, and neither
|
|
220
|
+
* terminates.
|
|
221
|
+
*/
|
|
209
222
|
export declare const CONSOLE_ONLY_VERBS: readonly string[];
|
package/dist/publish/copy.js
CHANGED
|
@@ -476,9 +476,19 @@ export function planLimitRefused(i) {
|
|
|
476
476
|
"",
|
|
477
477
|
...alignedRows(rows),
|
|
478
478
|
"",
|
|
479
|
+
// f15 FALSIFIED THE OLD SENTENCE. It said restoring was "a manual action by a
|
|
480
|
+
// Vincentt operator" — true until a customer could pay for it themselves, and
|
|
481
|
+
// false the moment checkout shipped.
|
|
482
|
+
//
|
|
483
|
+
// "cannot be done from the CLI" STAYS, and is the load-bearing half: an agent
|
|
484
|
+
// told only that publishing failed hunts for a flag, and naming the remedy as
|
|
485
|
+
// something outside this tool is what stops the loop.
|
|
486
|
+
//
|
|
487
|
+
// The restore is deliberately not promised as instant — it is about two
|
|
488
|
+
// minutes, and copy that promises instant is copy that lies on a slow tick.
|
|
479
489
|
` Published addresses were taken offline${takenOn} because the organization`,
|
|
480
|
-
" went over its monthly traffic allowance.
|
|
481
|
-
"
|
|
490
|
+
" went over its monthly traffic allowance. Taking a paid plan restores them",
|
|
491
|
+
" automatically, in the console — it cannot be done from the CLI.",
|
|
482
492
|
"",
|
|
483
493
|
" Every version is still stored and the address is still yours.",
|
|
484
494
|
"",
|
|
@@ -681,7 +691,20 @@ export function consoleVerb(verb, versionsUrl) {
|
|
|
681
691
|
}
|
|
682
692
|
return out;
|
|
683
693
|
}
|
|
684
|
-
/**
|
|
694
|
+
/**
|
|
695
|
+
* The verbs that land on that block — f4's and f10's six, plus f15's five.
|
|
696
|
+
*
|
|
697
|
+
* NONE OF THESE GAINS A HANDLER. They are routed to an answer precisely because
|
|
698
|
+
* they do not exist: f15 deliberately gives the CLI no checkout verb, since the
|
|
699
|
+
* authorized actor is a human reading consequence copy in a browser and a verb
|
|
700
|
+
* whose whole contribution is printing a URL is a browser flow with a shell in
|
|
701
|
+
* front of it.
|
|
702
|
+
*
|
|
703
|
+
* Routing them rather than ignoring them is about f15's actual actor. A human
|
|
704
|
+
* who types `vincentt upgrade` and gets "unrecognised command" shrugs and reads
|
|
705
|
+
* the help. AN AGENT RETRIES, or guesses a neighbouring verb, and neither
|
|
706
|
+
* terminates.
|
|
707
|
+
*/
|
|
685
708
|
export const CONSOLE_ONLY_VERBS = [
|
|
686
709
|
"rollback",
|
|
687
710
|
"unpublish",
|
|
@@ -689,4 +712,10 @@ export const CONSOLE_ONLY_VERBS = [
|
|
|
689
712
|
"versions",
|
|
690
713
|
"revert",
|
|
691
714
|
"take-offline",
|
|
715
|
+
// f15 — checkout is console-only, by design.
|
|
716
|
+
"upgrade",
|
|
717
|
+
"billing",
|
|
718
|
+
"plan",
|
|
719
|
+
"subscribe",
|
|
720
|
+
"pay",
|
|
692
721
|
];
|
package/dist/scaffold/index.d.ts
CHANGED
|
@@ -11,8 +11,14 @@ export declare const TEMPLATE_REPO = "https://github.com/vincentt-xr/v2-template
|
|
|
11
11
|
* immutable, which is what makes that an escape hatch rather than a coin flip.
|
|
12
12
|
*/
|
|
13
13
|
export declare const DEFAULT_TEMPLATE_REF = "latest";
|
|
14
|
-
/**
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Injectable git runner (real git by default; a fake in tests avoids the network).
|
|
16
|
+
*
|
|
17
|
+
* Resolves to git's stdout. A fake that returns nothing still satisfies the type,
|
|
18
|
+
* so the tag resolution below treats a void result the same as empty output —
|
|
19
|
+
* unresolved rather than a crash.
|
|
20
|
+
*/
|
|
21
|
+
export type GitRunner = (args: string[]) => Promise<string | void>;
|
|
16
22
|
/**
|
|
17
23
|
* True when `dir` has no app yet (no package.json) → safe to scaffold into.
|
|
18
24
|
* A directory that already holds an app is left untouched (project_create just binds).
|
|
@@ -46,12 +52,53 @@ export interface ScaffoldOptions {
|
|
|
46
52
|
/** Test seam — defaults to spawning real git. */
|
|
47
53
|
runGit?: GitRunner;
|
|
48
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* The concrete version tag the clone actually landed on, or `undefined` when the
|
|
57
|
+
* commit carries none.
|
|
58
|
+
*
|
|
59
|
+
* `git describe --tags` is the obvious call and it is WRONG here: against a clone
|
|
60
|
+
* of the moving tag it answers `latest`, which is the exact non-information this
|
|
61
|
+
* resolution exists to replace. `tag --points-at HEAD` lists every tag on the
|
|
62
|
+
* cloned commit instead — `latest` alongside the version tag it was force-moved
|
|
63
|
+
* onto — and the version tag is selected by shape.
|
|
64
|
+
*
|
|
65
|
+
* When several concrete versions share a commit (a re-tagged release), the
|
|
66
|
+
* HIGHEST is recorded: the tags name the same tree, so any is a correct baseline,
|
|
67
|
+
* and picking by precedence rather than by git's output order keeps the recorded
|
|
68
|
+
* value stable across clones.
|
|
69
|
+
*/
|
|
70
|
+
export declare function pickConcreteVersionTag(tagOutput: string | void): string | undefined;
|
|
71
|
+
/** What a scaffold resolved. `templateVersion` absent = it could not be established. */
|
|
72
|
+
export interface ScaffoldResult {
|
|
73
|
+
/**
|
|
74
|
+
* The concrete version tag the clone landed on. ABSENT rather than `latest`
|
|
75
|
+
* when unresolvable: a moving tag recorded as a baseline is worse than no
|
|
76
|
+
* baseline, because a later upgrade would trust it and widen what it replaces.
|
|
77
|
+
*/
|
|
78
|
+
templateVersion?: string;
|
|
79
|
+
/** The ref that was ASKED for — `latest` on the default path. Diagnostic only. */
|
|
80
|
+
requestedRef?: string;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The shape an INJECTED scaffold may return. `void` is permitted, and reads as
|
|
84
|
+
* "this fake resolved no version" — which is the no-baseline state, already a
|
|
85
|
+
* first-class value here.
|
|
86
|
+
*
|
|
87
|
+
* Deliberately looser than what `scaffoldFromTemplate` itself returns: the real
|
|
88
|
+
* function always reports, while a caller's test double has nothing to report
|
|
89
|
+
* and must not be forced to invent a version to satisfy a type.
|
|
90
|
+
*/
|
|
91
|
+
export type ScaffoldOutcome = ScaffoldResult | void;
|
|
49
92
|
/**
|
|
50
93
|
* Scaffold the template into `targetDir`. Clones to a temp dir, strips `.git`,
|
|
51
94
|
* copies the files in (never overwriting anything already there), then inits a
|
|
52
95
|
* fresh repo. Throws a clear error if the clone fails (e.g. no git access).
|
|
96
|
+
*
|
|
97
|
+
* Returns what it resolved. The requested ref is not the answer: `latest` is
|
|
98
|
+
* force-updated by the template's release workflow, so recording it preserves
|
|
99
|
+
* nothing — every project from any date would report the same string.
|
|
53
100
|
*/
|
|
54
|
-
export declare function scaffoldFromTemplate(targetDir: string, opts?: ScaffoldOptions): Promise<
|
|
101
|
+
export declare function scaffoldFromTemplate(targetDir: string, opts?: ScaffoldOptions): Promise<ScaffoldResult>;
|
|
55
102
|
export type PackageManager = "pnpm" | "yarn" | "npm";
|
|
56
103
|
/**
|
|
57
104
|
* Pick the package manager the scaffolded template is set up for, by its lockfile.
|
package/dist/scaffold/index.js
CHANGED
|
@@ -23,7 +23,8 @@ export const TEMPLATE_REPO = "https://github.com/vincentt-xr/v2-template.git";
|
|
|
23
23
|
*/
|
|
24
24
|
export const DEFAULT_TEMPLATE_REF = "latest";
|
|
25
25
|
const realGit = async (args) => {
|
|
26
|
-
await execFileAsync("git", args);
|
|
26
|
+
const { stdout } = await execFileAsync("git", args);
|
|
27
|
+
return stdout;
|
|
27
28
|
};
|
|
28
29
|
/**
|
|
29
30
|
* True when `dir` has no app yet (no package.json) → safe to scaffold into.
|
|
@@ -71,10 +72,57 @@ export class ScaffoldRefError extends Error {
|
|
|
71
72
|
this.name = "ScaffoldRefError";
|
|
72
73
|
}
|
|
73
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* A concrete `MAJOR.MINOR.PATCH` version tag, anchored at both ends.
|
|
77
|
+
*
|
|
78
|
+
* Anchored because a template repo carries tags a version compare cannot use:
|
|
79
|
+
* `latest` itself, and anything merely version-SHAPED (`1.4.3-extra`). Recording
|
|
80
|
+
* one of those is the defect this whole resolution exists to remove.
|
|
81
|
+
*/
|
|
82
|
+
const CONCRETE_VERSION_TAG = /^[0-9]+\.[0-9]+\.[0-9]+$/;
|
|
83
|
+
/**
|
|
84
|
+
* The concrete version tag the clone actually landed on, or `undefined` when the
|
|
85
|
+
* commit carries none.
|
|
86
|
+
*
|
|
87
|
+
* `git describe --tags` is the obvious call and it is WRONG here: against a clone
|
|
88
|
+
* of the moving tag it answers `latest`, which is the exact non-information this
|
|
89
|
+
* resolution exists to replace. `tag --points-at HEAD` lists every tag on the
|
|
90
|
+
* cloned commit instead — `latest` alongside the version tag it was force-moved
|
|
91
|
+
* onto — and the version tag is selected by shape.
|
|
92
|
+
*
|
|
93
|
+
* When several concrete versions share a commit (a re-tagged release), the
|
|
94
|
+
* HIGHEST is recorded: the tags name the same tree, so any is a correct baseline,
|
|
95
|
+
* and picking by precedence rather than by git's output order keeps the recorded
|
|
96
|
+
* value stable across clones.
|
|
97
|
+
*/
|
|
98
|
+
export function pickConcreteVersionTag(tagOutput) {
|
|
99
|
+
if (!tagOutput)
|
|
100
|
+
return undefined;
|
|
101
|
+
const versions = tagOutput
|
|
102
|
+
.split(/\r?\n/)
|
|
103
|
+
.map((line) => line.trim())
|
|
104
|
+
.filter((line) => CONCRETE_VERSION_TAG.test(line));
|
|
105
|
+
if (versions.length === 0)
|
|
106
|
+
return undefined;
|
|
107
|
+
return versions.sort(compareVersionDesc)[0];
|
|
108
|
+
}
|
|
109
|
+
function compareVersionDesc(a, b) {
|
|
110
|
+
const pa = a.split(".").map(Number);
|
|
111
|
+
const pb = b.split(".").map(Number);
|
|
112
|
+
for (let i = 0; i < 3; i++) {
|
|
113
|
+
if (pa[i] !== pb[i])
|
|
114
|
+
return pb[i] - pa[i];
|
|
115
|
+
}
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
74
118
|
/**
|
|
75
119
|
* Scaffold the template into `targetDir`. Clones to a temp dir, strips `.git`,
|
|
76
120
|
* copies the files in (never overwriting anything already there), then inits a
|
|
77
121
|
* fresh repo. Throws a clear error if the clone fails (e.g. no git access).
|
|
122
|
+
*
|
|
123
|
+
* Returns what it resolved. The requested ref is not the answer: `latest` is
|
|
124
|
+
* force-updated by the template's release workflow, so recording it preserves
|
|
125
|
+
* nothing — every project from any date would report the same string.
|
|
78
126
|
*/
|
|
79
127
|
export async function scaffoldFromTemplate(targetDir, opts = {}) {
|
|
80
128
|
const repo = opts.repo ?? TEMPLATE_REPO;
|
|
@@ -97,6 +145,17 @@ export async function scaffoldFromTemplate(targetDir, opts = {}) {
|
|
|
97
145
|
}
|
|
98
146
|
throw new Error(`Could not clone the ${repo} template (check your git access): ${msg}`);
|
|
99
147
|
}
|
|
148
|
+
// BEFORE the strip below — `.git` is the only place the concrete tag is
|
|
149
|
+
// readable, and the copy into the target deliberately does not carry it.
|
|
150
|
+
// A failure here must not fail the scaffold: the files are correct either
|
|
151
|
+
// way, and the honest outcome is a missing baseline, not a missing app.
|
|
152
|
+
let templateVersion;
|
|
153
|
+
try {
|
|
154
|
+
templateVersion = pickConcreteVersionTag(await runGit(["-C", tmp, "tag", "--points-at", "HEAD"]));
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
templateVersion = undefined;
|
|
158
|
+
}
|
|
100
159
|
await fs.rm(path.join(tmp, ".git"), { recursive: true, force: true });
|
|
101
160
|
// Copy template contents in without clobbering anything already in the target.
|
|
102
161
|
for (const entry of await fs.readdir(tmp)) {
|
|
@@ -110,6 +169,7 @@ export async function scaffoldFromTemplate(targetDir, opts = {}) {
|
|
|
110
169
|
if (!(await isGitRepo(targetDir))) {
|
|
111
170
|
await runGit(["-C", targetDir, "init"]);
|
|
112
171
|
}
|
|
172
|
+
return { templateVersion, requestedRef: ref };
|
|
113
173
|
}
|
|
114
174
|
finally {
|
|
115
175
|
await fs.rm(tmp, { recursive: true, force: true });
|
package/dist/shared/config.d.ts
CHANGED
|
@@ -18,6 +18,20 @@ export interface MachineConfig {
|
|
|
18
18
|
*/
|
|
19
19
|
dxSendEnabledAt?: string;
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* What the project was scaffolded from — the drift baseline a later upgrade
|
|
23
|
+
* compares against.
|
|
24
|
+
*
|
|
25
|
+
* The whole object is ABSENT when the scaffold could not resolve a concrete
|
|
26
|
+
* version, and absent is a first-class state every reader must handle. The
|
|
27
|
+
* alternative — recording the moving `latest` tag — is strictly worse than
|
|
28
|
+
* absence: a reader cannot tell it apart from a real baseline, so it would be
|
|
29
|
+
* trusted, and an upgrade trusting a wrong baseline widens what it overwrites.
|
|
30
|
+
*/
|
|
31
|
+
export interface TemplateProvenance {
|
|
32
|
+
/** A concrete `MAJOR.MINOR.PATCH` tag. Never `latest`, never a branch. */
|
|
33
|
+
version: string;
|
|
34
|
+
}
|
|
21
35
|
export interface ProjectBinding {
|
|
22
36
|
/** The backend Project id this working tree publishes to. */
|
|
23
37
|
projectId: string;
|
|
@@ -25,6 +39,8 @@ export interface ProjectBinding {
|
|
|
25
39
|
slug: string;
|
|
26
40
|
/** Optional per-project API override; else the machine config's apiUrl. */
|
|
27
41
|
apiUrl?: string;
|
|
42
|
+
/** Absent = no recorded baseline. See TemplateProvenance. */
|
|
43
|
+
template?: TemplateProvenance;
|
|
28
44
|
}
|
|
29
45
|
export declare const MACHINE_CONFIG_PATH: string;
|
|
30
46
|
export declare function projectBindingPath(cwd: string): string;
|
package/dist/shared/config.js
CHANGED
|
@@ -176,7 +176,15 @@ async function isGitRoot(dir) {
|
|
|
176
176
|
export async function writeProjectBinding(cwd, binding) {
|
|
177
177
|
await fs.mkdir(path.join(cwd, BINDING_DIR), { recursive: true });
|
|
178
178
|
const p = projectBindingPath(cwd);
|
|
179
|
-
|
|
179
|
+
// A caller that writes a whole binding without a `template` (`link`, a re-bind)
|
|
180
|
+
// must not DROP a baseline the scaffold recorded: the tree it describes is
|
|
181
|
+
// unchanged by a re-bind, and a lost baseline downgrades the next upgrade to
|
|
182
|
+
// the reporting path with nothing on screen to explain why.
|
|
183
|
+
const existing = binding.template
|
|
184
|
+
? undefined
|
|
185
|
+
: (await readJsonIfExists(p))?.template;
|
|
186
|
+
const next = existing ? { ...binding, template: existing } : binding;
|
|
187
|
+
await fs.writeFile(p, JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
180
188
|
await ensureGitignored(cwd, `${BINDING_DIR}/`);
|
|
181
189
|
// `create` and `link` WRITE a binding rather than resolve one, so they never
|
|
182
190
|
// pass through the resolver's hook. Between the two, every way a folder comes
|
|
@@ -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[];
|