@vincentt-xr/cli 0.15.1 → 0.16.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 +177 -4
- 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/clusters.js +10 -1
- 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/exitCodes.d.ts +24 -0
- package/dist/lib/exitCodes.js +28 -0
- package/dist/lib/initCommand.d.ts +3 -1
- package/dist/lib/initCommand.js +10 -0
- package/dist/lib/initFrom.js +12 -2
- package/dist/lib/lifecycle.d.ts +5 -1
- package/dist/lib/projects.d.ts +8 -1
- package/dist/lib/projects.js +21 -4
- 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/publish/api.d.ts +15 -4
- package/dist/publish/api.js +19 -5
- package/dist/publish/command.d.ts +10 -0
- package/dist/publish/command.js +7 -1
- 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/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
|
@@ -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,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 { resolveBuiltWith } from "../platform/resolve.js";
|
|
20
21
|
const COMMIT_RETRIES = 3;
|
|
21
22
|
const COMMIT_RETRY_DELAY_MS = 2000;
|
|
22
23
|
export async function publishCommand(deps, opts = {}) {
|
|
@@ -140,6 +141,10 @@ export async function publishCommand(deps, opts = {}) {
|
|
|
140
141
|
return 0;
|
|
141
142
|
}
|
|
142
143
|
}
|
|
144
|
+
// Resolved AFTER the confirmation and never before it: a refused publish must
|
|
145
|
+
// read nothing about the creator's installed packages. A fault here yields
|
|
146
|
+
// undefined and the field is omitted — it can never fail a publish.
|
|
147
|
+
const builtWith = await (deps.builtWith ?? (() => resolveBuiltWith(deps.cwd)))().catch(() => undefined);
|
|
143
148
|
return runUpload(deps, cfg, {
|
|
144
149
|
projectId: binding.projectId,
|
|
145
150
|
distDir,
|
|
@@ -148,6 +153,7 @@ export async function publishCommand(deps, opts = {}) {
|
|
|
148
153
|
address,
|
|
149
154
|
liveVersion: project.liveVersion ?? undefined,
|
|
150
155
|
fetchImpl,
|
|
156
|
+
builtWith,
|
|
151
157
|
org: {
|
|
152
158
|
name: orgName || undefined,
|
|
153
159
|
slug: (await deps.orgSlug?.()) || undefined,
|
|
@@ -158,7 +164,7 @@ export async function publishCommand(deps, opts = {}) {
|
|
|
158
164
|
async function runUpload(deps, cfg, i) {
|
|
159
165
|
let opened;
|
|
160
166
|
try {
|
|
161
|
-
opened = await openUpload(cfg, i.projectId, i.manifest.files, i.note, i.fetchImpl);
|
|
167
|
+
opened = await openUpload(cfg, i.projectId, i.manifest.files, i.note, i.fetchImpl, i.builtWith);
|
|
162
168
|
}
|
|
163
169
|
catch (err) {
|
|
164
170
|
for (const l of openErrorLines(err, i.address, i.liveVersion, i.org))
|
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
|
package/package.json
CHANGED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* QA fixtures for the dx-agent-consent delta (f7).
|
|
3
|
-
*
|
|
4
|
-
* Declared in the change's qa.md `## Fixtures` section. All three are
|
|
5
|
-
* machine-config state, which f7 has never had a fixture for — its existing
|
|
6
|
-
* fixtures (F7_JOURNAL_FRICTION, F7_REPORT_EXPIRED, …) are journal-side or
|
|
7
|
-
* server-side.
|
|
8
|
-
*
|
|
9
|
-
* F7_CONFIG_SIGNED_IN is written by driving the REAL writeMachineConfig(), never
|
|
10
|
-
* by hand-writing JSON. A hand-written config asserts against a shape the CLI
|
|
11
|
-
* never produces — the f1 orgs/memberships failure at file scope, where a mock
|
|
12
|
-
* of state the API could actually produce drifted from the API and stayed green.
|
|
13
|
-
*
|
|
14
|
-
* These are QA-owned (qa.* prefix) and deliberately separate from dev's test
|
|
15
|
-
* files in the same package.
|
|
16
|
-
*/
|
|
17
|
-
import type { JournalEntry } from "./journal.js";
|
|
18
|
-
/** The honest baseline config, as the CLI itself writes it. */
|
|
19
|
-
export declare const F7_CONFIG_SIGNED_IN: {
|
|
20
|
-
readonly apiUrl: "https://api.test";
|
|
21
|
-
readonly consoleUrl: "https://console.test";
|
|
22
|
-
readonly pat: "vct_alice";
|
|
23
|
-
readonly activeOrg: "alice";
|
|
24
|
-
};
|
|
25
|
-
/**
|
|
26
|
-
* The instant every fixture is dated against. 145 days after the grant in
|
|
27
|
-
* F7_CONFIG_ENABLED, and before DX_UNATTENDED_SUNSET.
|
|
28
|
-
*/
|
|
29
|
-
export declare const F7_NOW: number;
|
|
30
|
-
/** 145 days before F7_NOW. The AGE is the point — see qa.md on F7_CONFIG_ENABLED. */
|
|
31
|
-
export declare const F7_ENABLED_AT = "2026-04-01T00:00:00.000Z";
|
|
32
|
-
/**
|
|
33
|
-
* The single pinned human-facing shape. Every human surface must render this
|
|
34
|
-
* byte-for-byte; the age is the load-bearing half, so it is pinned as a literal
|
|
35
|
-
* rather than computed (a computed expectation would follow a broken
|
|
36
|
-
* implementation wherever it went).
|
|
37
|
-
*/
|
|
38
|
-
export declare const F7_PINNED_ENABLEMENT_LINE = "Unattended send: ENABLED since 2026-04-01 (145 days)";
|
|
39
|
-
/** F7_CONFIG_SIGNED_IN + durable enablement, aged. */
|
|
40
|
-
export declare const F7_CONFIG_ENABLED: {
|
|
41
|
-
readonly dxSendEnabled: true;
|
|
42
|
-
readonly dxSendEnabledAt: "2026-04-01T00:00:00.000Z";
|
|
43
|
-
readonly apiUrl: "https://api.test";
|
|
44
|
-
readonly consoleUrl: "https://console.test";
|
|
45
|
-
readonly pat: "vct_alice";
|
|
46
|
-
readonly activeOrg: "alice";
|
|
47
|
-
};
|
|
48
|
-
/**
|
|
49
|
-
* Every shape the enablement read must treat as DISABLED.
|
|
50
|
-
*
|
|
51
|
-
* Rows B, C, E, F, G are TRUTHY IN JAVASCRIPT — `if (cfg.dxSendEnabled)` passes
|
|
52
|
-
* every one of them. That is the entire reason this fixture exists, and why the
|
|
53
|
-
* read is specified as `=== true`. The worst is B ("false"): what a person
|
|
54
|
-
* hand-editing this file writes when trying to turn the feature OFF.
|
|
55
|
-
*
|
|
56
|
-
* `raw` rows (K, L) are written as bytes rather than as an object, because the
|
|
57
|
-
* property under test is that an UNPARSEABLE config fails closed instead of
|
|
58
|
-
* throwing through into an unrelated verb.
|
|
59
|
-
*/
|
|
60
|
-
export interface MalformedRow {
|
|
61
|
-
id: string;
|
|
62
|
-
why: string;
|
|
63
|
-
/** A config object to serialize, or undefined when `raw` carries the bytes. */
|
|
64
|
-
patch?: Record<string, unknown>;
|
|
65
|
-
/** Literal file bytes, for the rows that are not valid JSON at all. */
|
|
66
|
-
raw?: string;
|
|
67
|
-
}
|
|
68
|
-
export declare const F7_CONFIG_MALFORMED_ENABLEMENT: MalformedRow[];
|
|
69
|
-
/**
|
|
70
|
-
* F7_JOURNAL_FRICTION, as the record declares it: three runs of the same verb in
|
|
71
|
-
* a few minutes, which is a repetition cluster.
|
|
72
|
-
*/
|
|
73
|
-
export declare function f7JournalFriction(now?: number): JournalEntry[];
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/** The honest baseline config, as the CLI itself writes it. */
|
|
2
|
-
export const F7_CONFIG_SIGNED_IN = {
|
|
3
|
-
apiUrl: "https://api.test",
|
|
4
|
-
consoleUrl: "https://console.test",
|
|
5
|
-
pat: "vct_alice",
|
|
6
|
-
activeOrg: "alice",
|
|
7
|
-
};
|
|
8
|
-
/**
|
|
9
|
-
* The instant every fixture is dated against. 145 days after the grant in
|
|
10
|
-
* F7_CONFIG_ENABLED, and before DX_UNATTENDED_SUNSET.
|
|
11
|
-
*/
|
|
12
|
-
export const F7_NOW = Date.parse("2026-08-24T12:00:00.000Z");
|
|
13
|
-
/** 145 days before F7_NOW. The AGE is the point — see qa.md on F7_CONFIG_ENABLED. */
|
|
14
|
-
export const F7_ENABLED_AT = "2026-04-01T00:00:00.000Z";
|
|
15
|
-
/**
|
|
16
|
-
* The single pinned human-facing shape. Every human surface must render this
|
|
17
|
-
* byte-for-byte; the age is the load-bearing half, so it is pinned as a literal
|
|
18
|
-
* rather than computed (a computed expectation would follow a broken
|
|
19
|
-
* implementation wherever it went).
|
|
20
|
-
*/
|
|
21
|
-
export const F7_PINNED_ENABLEMENT_LINE = "Unattended send: ENABLED since 2026-04-01 (145 days)";
|
|
22
|
-
/** F7_CONFIG_SIGNED_IN + durable enablement, aged. */
|
|
23
|
-
export const F7_CONFIG_ENABLED = {
|
|
24
|
-
...F7_CONFIG_SIGNED_IN,
|
|
25
|
-
dxSendEnabled: true,
|
|
26
|
-
dxSendEnabledAt: F7_ENABLED_AT,
|
|
27
|
-
};
|
|
28
|
-
export const F7_CONFIG_MALFORMED_ENABLEMENT = [
|
|
29
|
-
{ id: "A", why: 'truthy STRING "true"', patch: { dxSendEnabled: "true", dxSendEnabledAt: F7_ENABLED_AT } },
|
|
30
|
-
{ id: "B", why: 'truthy in JS, means false to a human', patch: { dxSendEnabled: "false", dxSendEnabledAt: F7_ENABLED_AT } },
|
|
31
|
-
{ id: "C", why: 'truthy in JS ("0")', patch: { dxSendEnabled: "0", dxSendEnabledAt: F7_ENABLED_AT } },
|
|
32
|
-
{ id: "D", why: "truthy, not the boolean (1)", patch: { dxSendEnabled: 1, dxSendEnabledAt: F7_ENABLED_AT } },
|
|
33
|
-
{ id: "E", why: "truthy ({} — what a refactor grouping the fields produces)", patch: { dxSendEnabled: {}, dxSendEnabledAt: F7_ENABLED_AT } },
|
|
34
|
-
{ id: "F", why: "truthy ([])", patch: { dxSendEnabled: [], dxSendEnabledAt: F7_ENABLED_AT } },
|
|
35
|
-
{ id: "G", why: 'truthy ("garbage")', patch: { dxSendEnabled: "garbage", dxSendEnabledAt: F7_ENABLED_AT } },
|
|
36
|
-
{ id: "H", why: "key absent entirely", patch: {} },
|
|
37
|
-
{ id: "I", why: "enabled true, dxSendEnabledAt absent", patch: { dxSendEnabled: true } },
|
|
38
|
-
{ id: "J", why: 'enabled true, dxSendEnabledAt "not-a-date"', patch: { dxSendEnabled: true, dxSendEnabledAt: "not-a-date" } },
|
|
39
|
-
{ id: "K", why: "file truncated mid-JSON", raw: '{"pat":"vct_alice","dxSendEn' },
|
|
40
|
-
{ id: "L", why: "file empty (0 bytes)", raw: "" },
|
|
41
|
-
];
|
|
42
|
-
/**
|
|
43
|
-
* F7_JOURNAL_FRICTION, as the record declares it: three runs of the same verb in
|
|
44
|
-
* a few minutes, which is a repetition cluster.
|
|
45
|
-
*/
|
|
46
|
-
export function f7JournalFriction(now = F7_NOW) {
|
|
47
|
-
const entry = (minutesAgo) => ({
|
|
48
|
-
ts: new Date(now - minutesAgo * 60_000).toISOString(),
|
|
49
|
-
v: "0.7.0",
|
|
50
|
-
verb: "preview",
|
|
51
|
-
skel: "preview",
|
|
52
|
-
exit: 0,
|
|
53
|
-
ms: 800,
|
|
54
|
-
err: null,
|
|
55
|
-
wt: "abc123",
|
|
56
|
-
});
|
|
57
|
-
return [entry(3), entry(2), entry(1)];
|
|
58
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
export type DistFixtureName = "CLEAN" | "TEMPLATE" | "MISSING_META" | "DEAD_IMAGE" | "MULTI_HTML" | "ADVERSARIAL" | "NO_DIST" | "OVERSIZE";
|
|
2
|
-
/** The template defaults corpus the CLI ships, used verbatim by TEMPLATE. */
|
|
3
|
-
export declare const TEMPLATE_TITLE = "Vite + React";
|
|
4
|
-
export declare const TEMPLATE_DESCRIPTION = "Vite + React + TypeScript starter";
|
|
5
|
-
/**
|
|
6
|
-
* distFixture materializes one named tree under a fresh temp dir and returns
|
|
7
|
-
* its cwd. The caller removes it.
|
|
8
|
-
*/
|
|
9
|
-
export declare function distFixture(name: DistFixtureName): Promise<string>;
|
|
10
|
-
export declare function removeFixture(cwd: string): Promise<void>;
|
|
11
|
-
/**
|
|
12
|
-
* The SHARED ADVERSARIAL ROWS. This list is the fixture the API's Go table test
|
|
13
|
-
* and this suite both read; row-count parity is asserted in both runners.
|
|
14
|
-
*
|
|
15
|
-
* `reason` is the closed-enum value the API answers with. A row whose reason
|
|
16
|
-
* differs between the two runners is a validator that disagrees with itself
|
|
17
|
-
* across a repo boundary, which shows up only as a publish that cannot succeed.
|
|
18
|
-
*/
|
|
19
|
-
export interface AdversarialRow {
|
|
20
|
-
name: string;
|
|
21
|
-
path: string;
|
|
22
|
-
/** "" means the row must be ACCEPTED. */
|
|
23
|
-
reason: string;
|
|
24
|
-
}
|
|
25
|
-
export declare function adversarialRows(): AdversarialRow[];
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
// QA f4 · the eight on-disk `dist/` trees the catalog names (4C / 4E).
|
|
2
|
-
//
|
|
3
|
-
// A distFixture is a REAL DIRECTORY, not a mock: the checklist parses HTML off
|
|
4
|
-
// disk and the manifest hashes bytes off disk, so a fixture that stubbed either
|
|
5
|
-
// would be asserting against a shape the product never reads.
|
|
6
|
-
//
|
|
7
|
-
// `ADVERSARIAL` is shared BYTE-FOR-BYTE with the API's validateManifest table —
|
|
8
|
-
// one set of rows, read by both the Go table test and this vitest suite. A row
|
|
9
|
-
// present in one runner and absent in the other fails the parity assertion, or
|
|
10
|
-
// the shared fixture degrades into two fixtures with one name (f3's QA-F3-G11
|
|
11
|
-
// lesson).
|
|
12
|
-
import { promises as fs } from "node:fs";
|
|
13
|
-
import os from "node:os";
|
|
14
|
-
import path from "node:path";
|
|
15
|
-
/** The template defaults corpus the CLI ships, used verbatim by TEMPLATE. */
|
|
16
|
-
export const TEMPLATE_TITLE = "Vite + React";
|
|
17
|
-
export const TEMPLATE_DESCRIPTION = "Vite + React + TypeScript starter";
|
|
18
|
-
function html(opts) {
|
|
19
|
-
const parts = ["<!doctype html><html><head>"];
|
|
20
|
-
if (opts.title !== undefined)
|
|
21
|
-
parts.push(`<title>${opts.title}</title>`);
|
|
22
|
-
if (opts.ogTitle !== undefined) {
|
|
23
|
-
parts.push(`<meta property="og:title" content="${opts.ogTitle}">`);
|
|
24
|
-
}
|
|
25
|
-
if (opts.description !== undefined) {
|
|
26
|
-
parts.push(`<meta name="description" content="${opts.description}">`);
|
|
27
|
-
}
|
|
28
|
-
if (opts.ogImage !== undefined) {
|
|
29
|
-
parts.push(`<meta property="og:image" content="${opts.ogImage}">`);
|
|
30
|
-
}
|
|
31
|
-
parts.push("</head><body><div id=\"root\"></div></body></html>");
|
|
32
|
-
return parts.join("");
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* distFixture materializes one named tree under a fresh temp dir and returns
|
|
36
|
-
* its cwd. The caller removes it.
|
|
37
|
-
*/
|
|
38
|
-
export async function distFixture(name) {
|
|
39
|
-
const cwd = await fs.mkdtemp(path.join(os.tmpdir(), `vct-f4-${name.toLowerCase()}-`));
|
|
40
|
-
const dist = path.join(cwd, "dist");
|
|
41
|
-
if (name === "NO_DIST") {
|
|
42
|
-
// The ABSENCE of a dist/ directory IS the fixture.
|
|
43
|
-
return cwd;
|
|
44
|
-
}
|
|
45
|
-
await fs.mkdir(dist, { recursive: true });
|
|
46
|
-
switch (name) {
|
|
47
|
-
case "CLEAN": {
|
|
48
|
-
// A real title, description and og:image; og.png PRESENT; 6 files.
|
|
49
|
-
await fs.writeFile(path.join(dist, "index.html"), html({
|
|
50
|
-
title: "Harbour Wing — an AR walk",
|
|
51
|
-
description: "A guided AR tour of the Harbour Wing collection.",
|
|
52
|
-
ogImage: "/og.png",
|
|
53
|
-
}));
|
|
54
|
-
await fs.writeFile(path.join(dist, "og.png"), "png-bytes-clean");
|
|
55
|
-
await fs.mkdir(path.join(dist, "assets"), { recursive: true });
|
|
56
|
-
await fs.writeFile(path.join(dist, "assets", "app-a1b2c3.js"), "export const a=1;");
|
|
57
|
-
await fs.writeFile(path.join(dist, "assets", "app-d4e5f6.css"), "body{margin:0}");
|
|
58
|
-
await fs.writeFile(path.join(dist, "favicon.ico"), "ico-bytes");
|
|
59
|
-
await fs.writeFile(path.join(dist, "model.glb"), "glb-bytes");
|
|
60
|
-
return cwd;
|
|
61
|
-
}
|
|
62
|
-
case "TEMPLATE": {
|
|
63
|
-
// The template's own strings, untouched.
|
|
64
|
-
await fs.writeFile(path.join(dist, "index.html"), html({
|
|
65
|
-
title: TEMPLATE_TITLE,
|
|
66
|
-
description: TEMPLATE_DESCRIPTION,
|
|
67
|
-
ogImage: "/og.png",
|
|
68
|
-
}));
|
|
69
|
-
await fs.writeFile(path.join(dist, "og.png"), "png-bytes-template");
|
|
70
|
-
return cwd;
|
|
71
|
-
}
|
|
72
|
-
case "MISSING_META": {
|
|
73
|
-
// A title ONLY — no description, no og:image.
|
|
74
|
-
await fs.writeFile(path.join(dist, "index.html"), html({ title: "Harbour Wing — an AR walk" }));
|
|
75
|
-
return cwd;
|
|
76
|
-
}
|
|
77
|
-
case "DEAD_IMAGE": {
|
|
78
|
-
// og:image references /og.png, which is ABSENT from the tree. This is the
|
|
79
|
-
// ONLY fixture that produces a ✗.
|
|
80
|
-
await fs.writeFile(path.join(dist, "index.html"), html({
|
|
81
|
-
title: "Harbour Wing — an AR walk",
|
|
82
|
-
description: "A guided AR tour of the Harbour Wing collection.",
|
|
83
|
-
ogImage: "/og.png",
|
|
84
|
-
}));
|
|
85
|
-
return cwd;
|
|
86
|
-
}
|
|
87
|
-
case "MULTI_HTML": {
|
|
88
|
-
await fs.writeFile(path.join(dist, "index.html"), html({
|
|
89
|
-
title: "Harbour Wing — an AR walk",
|
|
90
|
-
description: "A guided AR tour of the Harbour Wing collection.",
|
|
91
|
-
ogImage: "/og.png",
|
|
92
|
-
}));
|
|
93
|
-
await fs.writeFile(path.join(dist, "og.png"), "png-bytes-multi");
|
|
94
|
-
// gallery.html has its OWN, DIFFERING findings.
|
|
95
|
-
await fs.writeFile(path.join(dist, "gallery.html"), html({ title: TEMPLATE_TITLE }));
|
|
96
|
-
return cwd;
|
|
97
|
-
}
|
|
98
|
-
case "ADVERSARIAL": {
|
|
99
|
-
// Paths only, and NO index.html at the root. The tree cannot literally
|
|
100
|
-
// contain "../escape.js" — the ROWS are the fixture, and they are shared
|
|
101
|
-
// with the Go table test through adversarialRows() below.
|
|
102
|
-
await fs.mkdir(path.join(dist, "sub"), { recursive: true });
|
|
103
|
-
await fs.writeFile(path.join(dist, "sub", "index.html"), html({ title: "nested" }));
|
|
104
|
-
return cwd;
|
|
105
|
-
}
|
|
106
|
-
case "OVERSIZE": {
|
|
107
|
-
// The bound variants are driven by SYNTHETIC MANIFEST ROWS rather than
|
|
108
|
-
// real bytes: writing 612 MB to a temp dir on every run would make the
|
|
109
|
-
// suite unusable, and the bound is checked against declared sizes before
|
|
110
|
-
// a byte moves — which is exactly the property under test.
|
|
111
|
-
await fs.writeFile(path.join(dist, "index.html"), html({ title: "big" }));
|
|
112
|
-
return cwd;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return cwd;
|
|
116
|
-
}
|
|
117
|
-
export async function removeFixture(cwd) {
|
|
118
|
-
await fs.rm(cwd, { recursive: true, force: true });
|
|
119
|
-
}
|
|
120
|
-
export function adversarialRows() {
|
|
121
|
-
return [
|
|
122
|
-
{ name: "absolute", path: "/a.js", reason: "absolute_path" },
|
|
123
|
-
{ name: "traversal", path: "../a.js", reason: "path_traversal" },
|
|
124
|
-
{ name: "traversal_after_normalize", path: "a/../../b.js", reason: "path_traversal" },
|
|
125
|
-
{ name: "backslash", path: "a\\b.js", reason: "backslash" },
|
|
126
|
-
{ name: "empty_segment", path: "a//b.js", reason: "empty_segment" },
|
|
127
|
-
{ name: "over_1024_bytes", path: `${"a".repeat(1025)}.js`, reason: "path_too_long" },
|
|
128
|
-
{ name: "control_char", path: `a${String.fromCharCode(7)}b.js`, reason: "control_char" },
|
|
129
|
-
{ name: "percent", path: "a%20b.js", reason: "bad_encoding" },
|
|
130
|
-
{ name: "percent_2e2e", path: "a%2e%2e/b.js", reason: "bad_encoding" },
|
|
131
|
-
{ name: "reserved_platform", path: "_platform/x.js", reason: "reserved_path" },
|
|
132
|
-
{ name: "reserved_platform2", path: "_platform2/x.js", reason: "reserved_path" },
|
|
133
|
-
{ name: "reserved_leading_underscore", path: "_anything/x.js", reason: "reserved_path" },
|
|
134
|
-
{ name: "dot_segment", path: ".", reason: "path_traversal" },
|
|
135
|
-
{ name: "dotdot_segment", path: "..", reason: "path_traversal" },
|
|
136
|
-
// THE ACCEPTED ROW. Without one the table passes on a validator that
|
|
137
|
-
// rejects everything.
|
|
138
|
-
{ name: "nested_leading_underscore_is_ALLOWED", path: "a/_b.js", reason: "" },
|
|
139
|
-
];
|
|
140
|
-
}
|