@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
package/dist/dx/skeleton.js
CHANGED
|
@@ -20,9 +20,24 @@ const ELIDED = "<v>";
|
|
|
20
20
|
* on-disk journal and any report built from it. Lowercase kebab-case is exactly
|
|
21
21
|
* what slugs and project names are made of, so no character-class check catches
|
|
22
22
|
* this — only a list of the verbs that actually exist.
|
|
23
|
+
*
|
|
24
|
+
* THIS SET MUST COVER EVERY DISPATCHED VERB, and a missing one is not a leak but
|
|
25
|
+
* a mis-bucketing: `journal.ts` records `verbOf(argv)`, which answers `<verb>`
|
|
26
|
+
* for anything absent here, so every invocation of a missing verb clusters under
|
|
27
|
+
* the placeholder and its friction is invisible. That also broke `lastContext()`
|
|
28
|
+
* — `report.ts` skips entries whose verb is `report`, which never matched while
|
|
29
|
+
* `report` was absent, so the context line a creator consents to could be the
|
|
30
|
+
* skeleton of their own `report` invocation rather than the command before it.
|
|
31
|
+
*
|
|
32
|
+
* Adding a verb CANNOT widen what a positional carries: `skeleton()` consults
|
|
33
|
+
* this set only when `out.length === 0`, so it reaches the FIRST token only, and
|
|
34
|
+
* every later positional is elided regardless. The historical leak lived in the
|
|
35
|
+
* first token.
|
|
23
36
|
*/
|
|
24
37
|
const KNOWN_VERBS = new Set([
|
|
38
|
+
"assets",
|
|
25
39
|
"create",
|
|
40
|
+
"domain",
|
|
26
41
|
"dx",
|
|
27
42
|
"feedback",
|
|
28
43
|
"init",
|
|
@@ -31,9 +46,12 @@ const KNOWN_VERBS = new Set([
|
|
|
31
46
|
"logout",
|
|
32
47
|
"logs",
|
|
33
48
|
"network",
|
|
49
|
+
"outdated",
|
|
34
50
|
"preview",
|
|
51
|
+
"projects",
|
|
35
52
|
"publish",
|
|
36
53
|
"relay",
|
|
54
|
+
"report",
|
|
37
55
|
"trace",
|
|
38
56
|
"unlink",
|
|
39
57
|
"version",
|
|
@@ -56,6 +74,7 @@ const KNOWN_FLAGS = new Set([
|
|
|
56
74
|
"--json",
|
|
57
75
|
"--limit",
|
|
58
76
|
"--name",
|
|
77
|
+
"--no-context",
|
|
59
78
|
"--no-install",
|
|
60
79
|
"--no-qr",
|
|
61
80
|
"--note",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The verbs unbidden output may not contain.
|
|
3
|
+
*
|
|
4
|
+
* Unbidden output lands in an agent's transcript, and an agent that reads an
|
|
5
|
+
* instruction will simply obey it. Naming that a command exists is information;
|
|
6
|
+
* telling a caller to transmit — or to grant itself permission to — is not. A
|
|
7
|
+
* REFUSAL may name both, because a refusal is solicited
|
|
8
|
+
* (D-A-refusal-is-solicited-output), so this list constrains only output the
|
|
9
|
+
* caller did not ask for.
|
|
10
|
+
*
|
|
11
|
+
* It is exported rather than restated at each site because every additional
|
|
12
|
+
* consumer is another chance for the lists to drift apart, and a guard that has
|
|
13
|
+
* quietly stopped matching its sibling still passes.
|
|
14
|
+
*
|
|
15
|
+
* `please`, `should` and `must` are not verbs. They are here because they are
|
|
16
|
+
* how an imperative gets smuggled past a verb-only list ("you should enable
|
|
17
|
+
* this"), and the guard is about directing rather than about grammar.
|
|
18
|
+
*/
|
|
19
|
+
export declare const UNBIDDEN_FORBIDDEN_VERBS: readonly ["run", "send", "report", "submit", "share", "try", "use", "execute", "enable", "please", "should", "must"];
|
|
20
|
+
export type UnbiddenForbiddenVerb = (typeof UNBIDDEN_FORBIDDEN_VERBS)[number];
|
|
21
|
+
/**
|
|
22
|
+
* Word-boundary, case-insensitive — the form every consumer needs, built here so
|
|
23
|
+
* a consumer cannot get the boundary or the flags subtly wrong.
|
|
24
|
+
*/
|
|
25
|
+
export declare function unbiddenVerbPattern(verb: string): RegExp;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The verbs unbidden output may not contain.
|
|
3
|
+
*
|
|
4
|
+
* Unbidden output lands in an agent's transcript, and an agent that reads an
|
|
5
|
+
* instruction will simply obey it. Naming that a command exists is information;
|
|
6
|
+
* telling a caller to transmit — or to grant itself permission to — is not. A
|
|
7
|
+
* REFUSAL may name both, because a refusal is solicited
|
|
8
|
+
* (D-A-refusal-is-solicited-output), so this list constrains only output the
|
|
9
|
+
* caller did not ask for.
|
|
10
|
+
*
|
|
11
|
+
* It is exported rather than restated at each site because every additional
|
|
12
|
+
* consumer is another chance for the lists to drift apart, and a guard that has
|
|
13
|
+
* quietly stopped matching its sibling still passes.
|
|
14
|
+
*
|
|
15
|
+
* `please`, `should` and `must` are not verbs. They are here because they are
|
|
16
|
+
* how an imperative gets smuggled past a verb-only list ("you should enable
|
|
17
|
+
* this"), and the guard is about directing rather than about grammar.
|
|
18
|
+
*/
|
|
19
|
+
export const UNBIDDEN_FORBIDDEN_VERBS = Object.freeze([
|
|
20
|
+
"run",
|
|
21
|
+
"send",
|
|
22
|
+
"report",
|
|
23
|
+
"submit",
|
|
24
|
+
"share",
|
|
25
|
+
"try",
|
|
26
|
+
"use",
|
|
27
|
+
"execute",
|
|
28
|
+
"enable",
|
|
29
|
+
"please",
|
|
30
|
+
"should",
|
|
31
|
+
"must",
|
|
32
|
+
]);
|
|
33
|
+
/**
|
|
34
|
+
* Word-boundary, case-insensitive — the form every consumer needs, built here so
|
|
35
|
+
* a consumer cannot get the boundary or the flags subtly wrong.
|
|
36
|
+
*/
|
|
37
|
+
export function unbiddenVerbPattern(verb) {
|
|
38
|
+
return new RegExp(`\\b${verb}\\b`, "i");
|
|
39
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every exit code the CLI returns. B-F12-1: APPEND-ONLY — a code may be added,
|
|
3
|
+
* an existing code's meaning never changes. Removing or repurposing one breaks
|
|
4
|
+
* grounding that already shipped into a creator's repository and is never read
|
|
5
|
+
* back, so the platform cannot warn about it or repair it.
|
|
6
|
+
*/
|
|
7
|
+
export declare const EXIT: {
|
|
8
|
+
/** The command did what it was asked. */
|
|
9
|
+
readonly OK: 0;
|
|
10
|
+
/** The command failed and nothing lasting was produced. */
|
|
11
|
+
readonly FAILED: 1;
|
|
12
|
+
/**
|
|
13
|
+
* The project source was written by this invocation, but no address was
|
|
14
|
+
* reserved: no account on this machine, and no browser to get one. The
|
|
15
|
+
* creator holds a working local project; only registration is deferred.
|
|
16
|
+
*/
|
|
17
|
+
readonly DELIVERED_NOT_REGISTERED: 3;
|
|
18
|
+
/** A platform call needed an account and the command did not answer for it. */
|
|
19
|
+
readonly NOT_AUTHENTICATED: 4;
|
|
20
|
+
/** A bounded wait elapsed with nothing handed over. */
|
|
21
|
+
readonly WINDOW_ELAPSED: 64;
|
|
22
|
+
/** The channel ended before anything was handed over. */
|
|
23
|
+
readonly CHANNEL_ENDED: 65;
|
|
24
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every exit code the CLI returns. B-F12-1: APPEND-ONLY — a code may be added,
|
|
3
|
+
* an existing code's meaning never changes. Removing or repurposing one breaks
|
|
4
|
+
* grounding that already shipped into a creator's repository and is never read
|
|
5
|
+
* back, so the platform cannot warn about it or repair it.
|
|
6
|
+
*/
|
|
7
|
+
export const EXIT = {
|
|
8
|
+
/** The command did what it was asked. */
|
|
9
|
+
OK: 0,
|
|
10
|
+
/** The command failed and nothing lasting was produced. */
|
|
11
|
+
FAILED: 1,
|
|
12
|
+
/**
|
|
13
|
+
* The project source was written by this invocation, but no address was
|
|
14
|
+
* reserved: no account on this machine, and no browser to get one. The
|
|
15
|
+
* creator holds a working local project; only registration is deferred.
|
|
16
|
+
*/
|
|
17
|
+
DELIVERED_NOT_REGISTERED: 3,
|
|
18
|
+
/** A platform call needed an account and the command did not answer for it. */
|
|
19
|
+
NOT_AUTHENTICATED: 4,
|
|
20
|
+
/** A bounded wait elapsed with nothing handed over. */
|
|
21
|
+
WINDOW_ELAPSED: 64,
|
|
22
|
+
/** The channel ended before anything was handed over. */
|
|
23
|
+
CHANNEL_ENDED: 65,
|
|
24
|
+
};
|
|
25
|
+
// 2 is deliberately unallocated. Shells conventionally use 2 for misuse of a
|
|
26
|
+
// builtin, and a CLI that returns it invites a caller to misread a shell error
|
|
27
|
+
// as ours. The gap is recorded here so a later change does not fill it by
|
|
28
|
+
// looking for the lowest free number.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { type ScaffoldOutcome } from "../scaffold/index.js";
|
|
1
2
|
import type { RegisterDeps } from "./initFrom.js";
|
|
2
3
|
import { type SourceRef } from "./sourceRef.js";
|
|
3
4
|
import type { Ask, Confirm } from "../publish/prompt.js";
|
|
4
5
|
export interface InitDeps {
|
|
5
6
|
print: (line: string) => void;
|
|
6
7
|
cwd: string;
|
|
7
|
-
scaffold?: (targetDir: string, opts: ScaffoldCall) => Promise<
|
|
8
|
+
scaffold?: (targetDir: string, opts: ScaffoldCall) => Promise<ScaffoldOutcome>;
|
|
8
9
|
install?: (targetDir: string) => Promise<string>;
|
|
9
10
|
isEmpty?: (targetDir: string) => Promise<boolean>;
|
|
10
11
|
/** Free-text prompt. Absent → not a TTY, so nothing may be asked. */
|
|
@@ -20,6 +21,7 @@ export interface InitDeps {
|
|
|
20
21
|
register?: (deps: Pick<RegisterDeps, "print" | "targetDir" | "dirLabel" | "ask">, opts: {
|
|
21
22
|
name?: string;
|
|
22
23
|
slug?: string;
|
|
24
|
+
templateVersion?: string;
|
|
23
25
|
}) => Promise<number>;
|
|
24
26
|
}
|
|
25
27
|
/**
|
package/dist/lib/initCommand.js
CHANGED
|
@@ -93,6 +93,10 @@ export async function initCommand(deps, opts = {}) {
|
|
|
93
93
|
const scaffold = deps.scaffold ??
|
|
94
94
|
((dir, o) => scaffoldFromTemplate(dir, o));
|
|
95
95
|
try {
|
|
96
|
+
// The result is deliberately discarded here. `init` writes no
|
|
97
|
+
// `.vincentt/project.json` at all on the bare path (that is the whole
|
|
98
|
+
// no-account wedge), and its only registering path is `--from`, whose
|
|
99
|
+
// baseline is refused below for naming a different repository.
|
|
96
100
|
await scaffold(target, opts.from
|
|
97
101
|
? { repo: templateCloneURL(opts.from.owner, opts.from.repo), ref: opts.from.tag }
|
|
98
102
|
: { ref: opts.template });
|
|
@@ -164,6 +168,12 @@ export async function initCommand(deps, opts = {}) {
|
|
|
164
168
|
const register = deps.register;
|
|
165
169
|
if (!register)
|
|
166
170
|
return 0;
|
|
171
|
+
// NO baseline on the `--from` path, and that is not an oversight. A gallery
|
|
172
|
+
// source is a different repository, so a version tag in it names that repo's
|
|
173
|
+
// release and not a v2-template version — recording it would hand a later
|
|
174
|
+
// upgrade a baseline that resolves against the wrong remote entirely, which is
|
|
175
|
+
// worse than the no-baseline path it takes instead. `register` is unreachable
|
|
176
|
+
// unless `--from` was given, so this call never carries one.
|
|
167
177
|
const code = await register({ print: deps.print, targetDir: target, dirLabel: label, ask: deps.ask }, { name: opts.name, slug: opts.slug });
|
|
168
178
|
if (code !== 0)
|
|
169
179
|
return code;
|
package/dist/lib/initFrom.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// be wrong.
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
import { SlugTakenError } from "./backend.js";
|
|
13
|
+
import { EXIT } from "./exitCodes.js";
|
|
13
14
|
import { slugify } from "./slug.js";
|
|
14
15
|
import { formatSourceRef } from "./sourceRef.js";
|
|
15
16
|
/** How many times the address prompt re-asks before falling back to the printed block. */
|
|
@@ -70,7 +71,9 @@ export async function registerAfterScaffold(deps, opts = {}) {
|
|
|
70
71
|
if (!token) {
|
|
71
72
|
if (!deps.ask) {
|
|
72
73
|
deps.print("");
|
|
73
|
-
deps.print(
|
|
74
|
+
deps.print(scaffolded
|
|
75
|
+
? "→ Not signed in, so no address is reserved yet."
|
|
76
|
+
: "✗ Not signed in, and signing in needs a browser.");
|
|
74
77
|
deps.print("");
|
|
75
78
|
if (scaffolded)
|
|
76
79
|
reassure(deps.print, deps.dirLabel);
|
|
@@ -81,9 +84,16 @@ export async function registerAfterScaffold(deps, opts = {}) {
|
|
|
81
84
|
deps.print("");
|
|
82
85
|
deps.print(" Then finish:");
|
|
83
86
|
resume(deps.print, deps.dirLabel, resumeCommand, needsCd);
|
|
87
|
+
if (scaffolded) {
|
|
88
|
+
deps.print("");
|
|
89
|
+
// B-F2-9(a): the open route must be named in something the creator's
|
|
90
|
+
// agent reads. On a non-TTY this output IS that artifact — the TTY
|
|
91
|
+
// branch's login prompt, which carries the same line, is unreachable here.
|
|
92
|
+
deps.print(" Or don't — the local dev loop needs no account.");
|
|
93
|
+
}
|
|
84
94
|
deps.print("");
|
|
85
95
|
deps.print(closing);
|
|
86
|
-
return
|
|
96
|
+
return scaffolded ? EXIT.DELIVERED_NOT_REGISTERED : EXIT.FAILED;
|
|
87
97
|
}
|
|
88
98
|
deps.print("");
|
|
89
99
|
deps.print("→ Now registering the project so it gets an address.");
|
package/dist/lib/lifecycle.d.ts
CHANGED
|
@@ -36,7 +36,11 @@ export interface CreateOptions {
|
|
|
36
36
|
export interface CreateResult {
|
|
37
37
|
/** The full transcript, exactly as the CLI prints it. */
|
|
38
38
|
message: string;
|
|
39
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Non-zero when the address was not reserved. Code 1 means nothing was created
|
|
41
|
+
* (already linked, address taken, no org); code 3 means the project source was
|
|
42
|
+
* written but registration was deferred for want of an account.
|
|
43
|
+
*/
|
|
40
44
|
exitCode: number;
|
|
41
45
|
}
|
|
42
46
|
/**
|
package/dist/lib/projects.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type FolderCase } from "./classifyTarget.js";
|
|
2
2
|
import { type OrgSource } from "./initFrom.js";
|
|
3
3
|
import type { ScaffoldCall } from "./initCommand.js";
|
|
4
|
+
import { type ScaffoldOutcome } from "../scaffold/index.js";
|
|
4
5
|
import type { Ask } from "../publish/prompt.js";
|
|
5
6
|
import { type ResolvedConfig } from "../shared/config.js";
|
|
6
7
|
export interface ProjectCommandDeps {
|
|
@@ -33,7 +34,7 @@ export interface CreateCommandDeps extends ProjectCommandDeps {
|
|
|
33
34
|
mkdir?: (dir: string) => Promise<void>;
|
|
34
35
|
/** Resolve a path through its symlinks, for the classify→write re-check. */
|
|
35
36
|
realpath?: (dir: string) => Promise<string>;
|
|
36
|
-
scaffold?: (dir: string, opts: ScaffoldCall) => Promise<
|
|
37
|
+
scaffold?: (dir: string, opts: ScaffoldCall) => Promise<ScaffoldOutcome>;
|
|
37
38
|
install?: (dir: string) => Promise<string>;
|
|
38
39
|
detectPm?: (dir: string) => Promise<string>;
|
|
39
40
|
/** Free-text prompt. Absent → not a TTY, so nothing may be asked. */
|
|
@@ -60,6 +61,12 @@ export interface CreateCommandOptions {
|
|
|
60
61
|
* bounded re-ask). See the throw site.
|
|
61
62
|
*/
|
|
62
63
|
rethrowSlugTaken?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* The concrete template version the scaffold above this call resolved, recorded
|
|
66
|
+
* into the binding as the drift baseline. Absent when nothing was scaffolded
|
|
67
|
+
* (registering a folder that already holds an app) or when resolution failed.
|
|
68
|
+
*/
|
|
69
|
+
templateVersion?: string;
|
|
63
70
|
}
|
|
64
71
|
/**
|
|
65
72
|
* `vincentt create` — a TWO-HALF verb with one seam.
|
package/dist/lib/projects.js
CHANGED
|
@@ -8,6 +8,7 @@ import { promises as fs } from "node:fs";
|
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { BackendError, SlugTakenError, createProject, getProject, } from "./backend.js";
|
|
10
10
|
import { classifyTarget } from "./classifyTarget.js";
|
|
11
|
+
import { EXIT } from "./exitCodes.js";
|
|
11
12
|
import { registerAfterScaffold } from "./initFrom.js";
|
|
12
13
|
import { slugify } from "./slug.js";
|
|
13
14
|
import { detectPackageManager, installDependencies, scaffoldFromTemplate, } from "../scaffold/index.js";
|
|
@@ -85,6 +86,10 @@ export async function createCommand(deps, opts = {}) {
|
|
|
85
86
|
let installed = true;
|
|
86
87
|
let pm = "npm";
|
|
87
88
|
let actedOn = dirLabel;
|
|
89
|
+
// Case 2 (an app already on disk) leaves this absent: this command did not
|
|
90
|
+
// scaffold that tree, so it knows nothing about which template version it came
|
|
91
|
+
// from, and inventing one is the wrong-baseline failure.
|
|
92
|
+
let templateVersion;
|
|
88
93
|
if (folderCase === "empty") {
|
|
89
94
|
const wrote = await scaffoldInto(deps, target, dirLabel, opts);
|
|
90
95
|
if (!wrote.ok)
|
|
@@ -92,6 +97,7 @@ export async function createCommand(deps, opts = {}) {
|
|
|
92
97
|
scaffolded = true;
|
|
93
98
|
installed = wrote.installed;
|
|
94
99
|
pm = wrote.pm;
|
|
100
|
+
templateVersion = wrote.templateVersion;
|
|
95
101
|
}
|
|
96
102
|
else {
|
|
97
103
|
// Case 2 writes too — a binding and a .gitignore line — so it runs the same
|
|
@@ -156,6 +162,7 @@ export async function createCommand(deps, opts = {}) {
|
|
|
156
162
|
dir: opts.dir,
|
|
157
163
|
name,
|
|
158
164
|
slug,
|
|
165
|
+
templateVersion,
|
|
159
166
|
rethrowSlugTaken: true,
|
|
160
167
|
}),
|
|
161
168
|
},
|
|
@@ -169,7 +176,10 @@ export async function createCommand(deps, opts = {}) {
|
|
|
169
176
|
name: opts.name ?? path.basename(target),
|
|
170
177
|
slug: opts.slug ?? slugify(opts.name ?? path.basename(target)),
|
|
171
178
|
});
|
|
172
|
-
|
|
179
|
+
// The gate is DELIVERY, not zero. A run that wrote the source and deferred
|
|
180
|
+
// only the address still delivered a project, so it still prints how to run
|
|
181
|
+
// it. The endings that stay exit 1 delivered nothing and correctly skip this.
|
|
182
|
+
if (code !== 0 && code !== EXIT.DELIVERED_NOT_REGISTERED)
|
|
173
183
|
return code;
|
|
174
184
|
if (scaffolded) {
|
|
175
185
|
// This reader has never run the dev server, so the block that fits is the one
|
|
@@ -187,7 +197,10 @@ export async function createCommand(deps, opts = {}) {
|
|
|
187
197
|
for (const line of alignedSteps(steps))
|
|
188
198
|
deps.print(line);
|
|
189
199
|
}
|
|
190
|
-
|
|
200
|
+
// Printing the steps must not launder the ending: a delivered-not-registered
|
|
201
|
+
// run still reports 3, so a caller never reads success for an unregistered
|
|
202
|
+
// project.
|
|
203
|
+
return code;
|
|
191
204
|
}
|
|
192
205
|
/** Two columns, aligned on the widest command, so the block reads as a table. */
|
|
193
206
|
function alignedSteps(steps) {
|
|
@@ -273,8 +286,9 @@ async function scaffoldInto(deps, target, dirLabel, opts) {
|
|
|
273
286
|
// which it could — which is exactly what `--template`/`--from` being refused at
|
|
274
287
|
// the parser makes structurally true, and what the install below rests on.
|
|
275
288
|
const scaffold = deps.scaffold ?? ((dir, o) => scaffoldFromTemplate(dir, o));
|
|
289
|
+
let scaffoldResult;
|
|
276
290
|
try {
|
|
277
|
-
await scaffold(target, {});
|
|
291
|
+
scaffoldResult = await scaffold(target, {});
|
|
278
292
|
}
|
|
279
293
|
catch (err) {
|
|
280
294
|
// No `✓ Created`, so no registration is attempted, and this arm carries
|
|
@@ -300,7 +314,7 @@ async function scaffoldInto(deps, target, dirLabel, opts) {
|
|
|
300
314
|
const pm = await detect(target).catch(() => "npm");
|
|
301
315
|
deps.print("");
|
|
302
316
|
deps.print(`✓ Created ${dirLabel}`);
|
|
303
|
-
return { ok: true, installed, pm };
|
|
317
|
+
return { ok: true, installed, pm, templateVersion: scaffoldResult?.templateVersion };
|
|
304
318
|
}
|
|
305
319
|
/**
|
|
306
320
|
* The registration half — the binding guard's remainder, the slug derivation, the
|
|
@@ -348,6 +362,9 @@ export async function registerProject(deps, opts = {}) {
|
|
|
348
362
|
await writeProjectBinding(target, {
|
|
349
363
|
projectId: created.projectId,
|
|
350
364
|
slug: created.slug,
|
|
365
|
+
// Omitted, never `latest`: absence is the honest no-baseline state, and it is
|
|
366
|
+
// the one that makes a later upgrade report rather than overwrite.
|
|
367
|
+
...(opts.templateVersion ? { template: { version: opts.templateVersion } } : {}),
|
|
351
368
|
});
|
|
352
369
|
deps.print(`✓ Created ${created.name} in ${org.org.name}`);
|
|
353
370
|
deps.print(` Address reserved: ${created.liveUrl}`);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The packages the platform publishes, frozen.
|
|
3
|
+
*
|
|
4
|
+
* This list is what makes `B-F17-1` a structural property rather than a promise:
|
|
5
|
+
* the resolver ITERATES it and reads only the manifests it names, so a
|
|
6
|
+
* third-party package name is never in the CLI's memory — not read, not
|
|
7
|
+
* filtered, not dropped. "Filter the creator's dependency graph" produces the
|
|
8
|
+
* same wire payload today and leaks the whole graph the first time someone
|
|
9
|
+
* spreads the intermediate value into a log line or a report.
|
|
10
|
+
*
|
|
11
|
+
* ORDER IS PART OF THE CONTRACT. Every surface that renders a version set
|
|
12
|
+
* renders it in this order — the notice's rows, `outdated`'s sections, and the
|
|
13
|
+
* report's one-line summary — so two surfaces can never disagree about
|
|
14
|
+
* sequence, and the wire field's key order is stable for the byte-identity
|
|
15
|
+
* assertion across the publish and report payloads.
|
|
16
|
+
*/
|
|
17
|
+
export interface PlatformPackage {
|
|
18
|
+
/** The package identity — the string in the creator's own dependency manifest. */
|
|
19
|
+
name: string;
|
|
20
|
+
/**
|
|
21
|
+
* The short name used only where a person reads three names in one pass (the
|
|
22
|
+
* payload preview's summary line). The WIRE always carries `name`: an agent
|
|
23
|
+
* handed `sdk` cannot find it in a `package.json`.
|
|
24
|
+
*/
|
|
25
|
+
short: string;
|
|
26
|
+
}
|
|
27
|
+
export declare const PLATFORM_PACKAGES: readonly PlatformPackage[];
|
|
28
|
+
/**
|
|
29
|
+
* The template's short name. It is NOT a package: it has no `node_modules`
|
|
30
|
+
* entry, it was copied rather than installed, and its version comes from the
|
|
31
|
+
* project binding. It is named here so the surfaces that render it last are
|
|
32
|
+
* reading one constant rather than each spelling it.
|
|
33
|
+
*/
|
|
34
|
+
export declare const TEMPLATE_COMPONENT = "template";
|
|
35
|
+
export declare function isPlatformPackage(name: string): boolean;
|
|
36
|
+
export declare function shortNameOf(name: string): string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The packages the platform publishes, frozen.
|
|
3
|
+
*
|
|
4
|
+
* This list is what makes `B-F17-1` a structural property rather than a promise:
|
|
5
|
+
* the resolver ITERATES it and reads only the manifests it names, so a
|
|
6
|
+
* third-party package name is never in the CLI's memory — not read, not
|
|
7
|
+
* filtered, not dropped. "Filter the creator's dependency graph" produces the
|
|
8
|
+
* same wire payload today and leaks the whole graph the first time someone
|
|
9
|
+
* spreads the intermediate value into a log line or a report.
|
|
10
|
+
*
|
|
11
|
+
* ORDER IS PART OF THE CONTRACT. Every surface that renders a version set
|
|
12
|
+
* renders it in this order — the notice's rows, `outdated`'s sections, and the
|
|
13
|
+
* report's one-line summary — so two surfaces can never disagree about
|
|
14
|
+
* sequence, and the wire field's key order is stable for the byte-identity
|
|
15
|
+
* assertion across the publish and report payloads.
|
|
16
|
+
*/
|
|
17
|
+
export const PLATFORM_PACKAGES = Object.freeze([
|
|
18
|
+
Object.freeze({ name: "@vincentt-xr/sdk", short: "sdk" }),
|
|
19
|
+
Object.freeze({ name: "@vincentt-xr/harness", short: "harness" }),
|
|
20
|
+
Object.freeze({ name: "@vincentt-xr/analytics", short: "analytics" }),
|
|
21
|
+
]);
|
|
22
|
+
/**
|
|
23
|
+
* The template's short name. It is NOT a package: it has no `node_modules`
|
|
24
|
+
* entry, it was copied rather than installed, and its version comes from the
|
|
25
|
+
* project binding. It is named here so the surfaces that render it last are
|
|
26
|
+
* reading one constant rather than each spelling it.
|
|
27
|
+
*/
|
|
28
|
+
export const TEMPLATE_COMPONENT = "template";
|
|
29
|
+
export function isPlatformPackage(name) {
|
|
30
|
+
return PLATFORM_PACKAGES.some((p) => p.name === name);
|
|
31
|
+
}
|
|
32
|
+
export function shortNameOf(name) {
|
|
33
|
+
return PLATFORM_PACKAGES.find((p) => p.name === name)?.short ?? name;
|
|
34
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { PlatformRelease } from "./releaseCache.js";
|
|
2
|
+
export interface BehindRow {
|
|
3
|
+
/** The package identity — the string in the creator's own manifest. */
|
|
4
|
+
name: string;
|
|
5
|
+
installed: string;
|
|
6
|
+
current: string;
|
|
7
|
+
/** Whether the platform authored a note. NEVER the note itself. */
|
|
8
|
+
hasMigrationNote: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface BehindNotice {
|
|
11
|
+
/** The release identifier, stated because this is what the notice fires once per. */
|
|
12
|
+
release: string;
|
|
13
|
+
rows: BehindRow[];
|
|
14
|
+
/** How many rows carry an authored note. Counted, never quoted. */
|
|
15
|
+
noteCount: number;
|
|
16
|
+
}
|
|
17
|
+
export interface InstalledSet {
|
|
18
|
+
/** Keyed by package identity. */
|
|
19
|
+
packages: Record<string, string>;
|
|
20
|
+
/** The recorded template baseline. Absent = not recorded. */
|
|
21
|
+
template?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Build the notice struct, or `undefined` when there is nothing to say.
|
|
25
|
+
*
|
|
26
|
+
* A component the composed set does not mention is NOT BEHIND and is not
|
|
27
|
+
* reported: on the day this ships every component is absent, which is neither
|
|
28
|
+
* behind nor up to date, and describing it would put a row of nothing in front
|
|
29
|
+
* of every creator. Absent is absent — never inferred, never rendered as a
|
|
30
|
+
* value, never dressed up as a state.
|
|
31
|
+
*/
|
|
32
|
+
export declare function behindFor(installed: InstalledSet, current: PlatformRelease | undefined): BehindNotice | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Whether this machine has already announced this release.
|
|
35
|
+
*
|
|
36
|
+
* Equality on the identifier, and the announced value is READ FROM THE CACHE
|
|
37
|
+
* rather than derived. Time governs how often we ask; the fact governs whether
|
|
38
|
+
* we speak — so the second run today is silent, the fortieth is silent, and so
|
|
39
|
+
* is tomorrow's, until the identifier changes.
|
|
40
|
+
*/
|
|
41
|
+
export declare function alreadyAnnounced(announcedRelease: string | undefined, release: string): boolean;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The comparison: this project's versions against what the platform publishes.
|
|
3
|
+
*
|
|
4
|
+
* THE OUTPUT TYPE CARRIES NO COMMAND STRING, NO URL, AND NO NOTE BODY. That is
|
|
5
|
+
* the primary guard on the unbidden notice and it is structural: an instruction
|
|
6
|
+
* is UNRENDERABLE rather than merely unwritten, because the renderer is never
|
|
7
|
+
* handed anything it could instruct with. A prose guard over the rendered lines
|
|
8
|
+
* is the second layer, not the first — a single-regex guard on exactly this
|
|
9
|
+
* class shipped once and six phrasings walked past it.
|
|
10
|
+
*
|
|
11
|
+
* `hasMigrationNote` is a BOOLEAN for the same reason. A count says "there are
|
|
12
|
+
* instructions somewhere" and leaves the reader to ask for them; the body would
|
|
13
|
+
* say "here are the instructions", which is the sentence an autonomous caller
|
|
14
|
+
* acts on.
|
|
15
|
+
*
|
|
16
|
+
* NOTHING BRANCHES ON A VERSION. Equality decides behind-or-current, and the
|
|
17
|
+
* release identifier is compared for equality only. There is no ordering
|
|
18
|
+
* comparison anywhere in this module: `YYYY.MM.N` is an identifier, and an
|
|
19
|
+
* installed version is evidence rather than authorization.
|
|
20
|
+
*/
|
|
21
|
+
import { PLATFORM_PACKAGES, TEMPLATE_COMPONENT } from "./allowlist.js";
|
|
22
|
+
/**
|
|
23
|
+
* Build the notice struct, or `undefined` when there is nothing to say.
|
|
24
|
+
*
|
|
25
|
+
* A component the composed set does not mention is NOT BEHIND and is not
|
|
26
|
+
* reported: on the day this ships every component is absent, which is neither
|
|
27
|
+
* behind nor up to date, and describing it would put a row of nothing in front
|
|
28
|
+
* of every creator. Absent is absent — never inferred, never rendered as a
|
|
29
|
+
* value, never dressed up as a state.
|
|
30
|
+
*/
|
|
31
|
+
export function behindFor(installed, current) {
|
|
32
|
+
if (current === undefined)
|
|
33
|
+
return undefined;
|
|
34
|
+
const rows = [];
|
|
35
|
+
// ALLOWLIST ORDER, not object order, so every surface renders the same
|
|
36
|
+
// sequence and two surfaces can never disagree.
|
|
37
|
+
for (const pkg of PLATFORM_PACKAGES) {
|
|
38
|
+
const have = installed.packages[pkg.name];
|
|
39
|
+
const component = current.components[pkg.name];
|
|
40
|
+
if (have === undefined || component === undefined)
|
|
41
|
+
continue;
|
|
42
|
+
if (have === component.version)
|
|
43
|
+
continue;
|
|
44
|
+
rows.push({
|
|
45
|
+
name: pkg.name,
|
|
46
|
+
installed: have,
|
|
47
|
+
current: component.version,
|
|
48
|
+
hasMigrationNote: component.migration !== undefined,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
// The template last, and it is not a package: printed bare, which is also how
|
|
52
|
+
// it distinguishes itself as the one entry that is not a dependency.
|
|
53
|
+
const templateCurrent = current.components[TEMPLATE_COMPONENT];
|
|
54
|
+
if (installed.template !== undefined &&
|
|
55
|
+
templateCurrent !== undefined &&
|
|
56
|
+
installed.template !== templateCurrent.version) {
|
|
57
|
+
rows.push({
|
|
58
|
+
name: TEMPLATE_COMPONENT,
|
|
59
|
+
installed: installed.template,
|
|
60
|
+
current: templateCurrent.version,
|
|
61
|
+
hasMigrationNote: templateCurrent.migration !== undefined,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (rows.length === 0)
|
|
65
|
+
return undefined;
|
|
66
|
+
return {
|
|
67
|
+
release: current.release,
|
|
68
|
+
rows,
|
|
69
|
+
noteCount: rows.filter((r) => r.hasMigrationNote).length,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Whether this machine has already announced this release.
|
|
74
|
+
*
|
|
75
|
+
* Equality on the identifier, and the announced value is READ FROM THE CACHE
|
|
76
|
+
* rather than derived. Time governs how often we ask; the fact governs whether
|
|
77
|
+
* we speak — so the second run today is silent, the fortieth is silent, and so
|
|
78
|
+
* is tomorrow's, until the identifier changes.
|
|
79
|
+
*/
|
|
80
|
+
export function alreadyAnnounced(announcedRelease, release) {
|
|
81
|
+
return announcedRelease === release;
|
|
82
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { InstalledSet } from "./behind.js";
|
|
2
|
+
export interface OutdatedDeps {
|
|
3
|
+
print: (line: string) => void;
|
|
4
|
+
cwd: string;
|
|
5
|
+
now: () => number;
|
|
6
|
+
/** The API origin, and the base of the address printed to the creator. */
|
|
7
|
+
apiUrl: () => Promise<string>;
|
|
8
|
+
/** `Name · Workspace` when this folder is a project; undefined outside one. */
|
|
9
|
+
projectLine: () => Promise<string | undefined>;
|
|
10
|
+
/** Whether this folder resolves a project binding. */
|
|
11
|
+
inProject: () => Promise<boolean>;
|
|
12
|
+
fetchImpl?: typeof fetch;
|
|
13
|
+
}
|
|
14
|
+
export declare function outdatedCommand(deps: OutdatedDeps): Promise<number>;
|
|
15
|
+
/** Nothing installed and no baseline → the flat "what is current" answer still prints. */
|
|
16
|
+
export declare function hasAnyInstalled(installed: InstalledSet): boolean;
|