@vincentt-xr/cli 0.18.1 → 0.18.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/MIGRATION.md CHANGED
@@ -3,6 +3,15 @@
3
3
  Per version, newest first. Read by the platform's own version surface and served to
4
4
  creators' agents. Accurate or absent, never speculative (B-F5-2).
5
5
 
6
+ ## 0.18.2
7
+
8
+ required: false
9
+ No action required. Behaviour that changed:
10
+ - `init` and `create` read the public release list once and clone the template version it names, then check the cloned commit against the platform's record. A commit that does not match exits 1 and writes nothing. When the platform cannot be reached, or has no commit on record, they clone the `latest` tag as before and print one warning on stderr. Plain `init` still needs no account.
11
+ - `vincentt create` in a folder that already holds an app, with no account and no terminal, now exits 4 (not signed in) with the `vincentt login` remedy, where it exited 1. Every not-signed-in path now exits 4.
12
+ - The six preview ending lines no longer use em-dashes: each reads `✓ Preview stopped.` or `✗ Preview ended.` followed by its cause, for example `✗ Preview ended. It reached the 5 GB ceiling.` A script matching the old wording exactly needs updating.
13
+ - `preview --stop` no longer signals a process that merely reuses the preview's old process id: the preview records its start time, and when the process now holding that id started at a different time, nothing is signalled and the command exits 1 as "nothing stopped", clearing the stale record. A preview started by an older CLI, or on Windows, has no start time on record and stops as before.
14
+
6
15
  ## 0.18.1
7
16
 
8
17
  required: false
@@ -1,5 +1,7 @@
1
1
  // `vincentt init [dir]` — scaffold a Vincentt XR app. On the BARE path: NO
2
- // account, NO network call to the API, NO binding written.
2
+ // account, NO binding written, and no API call beyond the scaffold's anonymous
3
+ // read of the public release list (to verify the template commit; its failure
4
+ // only warns).
3
5
  //
4
6
  // This is the acquisition wedge: local-first is a smaller room, not a wall, so the
5
7
  // template must be reachable without ever touching the auth gate. `create` (which
@@ -93,7 +93,7 @@ export async function registerAfterScaffold(deps, opts = {}) {
93
93
  }
94
94
  deps.print("");
95
95
  deps.print(closing);
96
- return scaffolded ? EXIT.DELIVERED_NOT_REGISTERED : EXIT.FAILED;
96
+ return scaffolded ? EXIT.DELIVERED_NOT_REGISTERED : EXIT.NOT_AUTHENTICATED;
97
97
  }
98
98
  deps.print("");
99
99
  deps.print("→ Now registering the project so it gets an address.");
@@ -2,6 +2,7 @@ import type { OrgSource } from "./initFrom.js";
2
2
  import type { Ask } from "../publish/prompt.js";
3
3
  import { type ResolvedOrg } from "./org.js";
4
4
  import { type RelayClient } from "./diagnostics.js";
5
+ import { type ProcessStartTimeReader } from "./processStartTime.js";
5
6
  import { type ResolvedConfig } from "../shared/config.js";
6
7
  import { type RunningPreview, type StartPreviewOptions } from "../preview/index.js";
7
8
  import type { DiagEventKind, RelayResult } from "@vincentt-xr/harness/events";
@@ -125,6 +126,8 @@ export interface PreviewStopDeps {
125
126
  relay?: RelayClient;
126
127
  /** Injected so the SIGTERM is observable without signalling a real process. */
127
128
  kill?: (pid: number, signal: NodeJS.Signals) => void;
129
+ /** Injected so a recycled pid can be simulated without a real process. */
130
+ processStartTime?: ProcessStartTimeReader;
128
131
  }
129
132
  /**
130
133
  * Stop a preview started by a *different* process (the common case: `vincentt
@@ -10,6 +10,7 @@ import { publishCommand } from "../publish/command.js";
10
10
  import { activeOrgSlug, resolveActiveOrg } from "./org.js";
11
11
  import { filterErrors, httpRelayClient, renderResult, } from "./diagnostics.js";
12
12
  import { clearRelayInfo, readLivePreview, readRelayInfo, writeRelayInfo, } from "./relayInfo.js";
13
+ import { readProcessStartTime } from "./processStartTime.js";
13
14
  import { loadMachineConfig, locateProjectBinding, resolveConfig, } from "../shared/config.js";
14
15
  import { startPreview, } from "../preview/index.js";
15
16
  // The SHIPPED stopping line, reused rather than reworded: the interrupt path and
@@ -173,6 +174,7 @@ export async function previewStart(projectCwd, onLog, extra = {}) {
173
174
  pid: process.pid,
174
175
  harnessPath: "/__harness",
175
176
  expiresAt: preview.expiresAt,
177
+ pidStartTime: await readProcessStartTime(process.pid),
176
178
  });
177
179
  return { preview, started: true };
178
180
  }
@@ -199,6 +201,19 @@ function isAlive(pid) {
199
201
  }
200
202
  /** The bound the caller is held to: one query, no retry, isRelayResponding's window. */
201
203
  const PRESENCE_TIMEOUT_MS = 700;
204
+ /**
205
+ * True only when the marker recorded a start time and the live process holding
206
+ * that pid PROVABLY started at a different time. A marker from an older CLI, or
207
+ * a start time that cannot be read, proves nothing, so the stop goes ahead as it
208
+ * always did. The relay is deliberately not consulted: QA-F14-06 requires the
209
+ * stop to happen whatever the relay answers.
210
+ */
211
+ async function isRecycledPid(info, read) {
212
+ if (info.pidStartTime === undefined)
213
+ return false;
214
+ const live = await read(info.pid);
215
+ return live !== undefined && live !== info.pidStartTime;
216
+ }
202
217
  /**
203
218
  * How many connections are attached, or UNDEFINED when that cannot be
204
219
  * established — and undefined is a first-class answer here, not an error.
@@ -259,7 +274,11 @@ export async function previewStopSignal(projectCwd, deps = {}) {
259
274
  if (!info) {
260
275
  return { stopped: false, message: "No preview is running for this directory." };
261
276
  }
262
- const previewAlive = info.pid !== undefined && isAlive(info.pid);
277
+ // A recycled pid is treated exactly like a dead one: nothing is signalled, the
278
+ // relay is not asked, and the stale marker is cleared.
279
+ const previewAlive = info.pid !== undefined &&
280
+ isAlive(info.pid) &&
281
+ !(await isRecycledPid(info, deps.processStartTime ?? readProcessStartTime));
263
282
  if (previewAlive) {
264
283
  // PRESENCE IS READ BEFORE THE SIGNAL, and the order is the feature. The
265
284
  // creator is about to blank someone's screen, so the count has to describe
@@ -0,0 +1,12 @@
1
+ export type ProcessStartTimeReader = (pid: number) => Promise<string | undefined>;
2
+ /**
3
+ * macOS and Linux both answer `ps -o lstart= -p <pid>` (BSD ps and procps), run
4
+ * under the C locale so the text does not vary with the creator's language.
5
+ * One-second resolution is enough: the process that held the pid had to exit
6
+ * before it could be reused. Windows returns undefined: no marker then carries
7
+ * a start time, and `preview --stop` behaves as it did before the check.
8
+ *
9
+ * Undefined also means "could not read" (no `ps`, a ps without `lstart`), never
10
+ * "different".
11
+ */
12
+ export declare function readProcessStartTime(pid: number, platform?: NodeJS.Platform): Promise<string | undefined>;
@@ -0,0 +1,33 @@
1
+ // A process's start time, read as an opaque token that is only ever compared for
2
+ // equality. Together with the pid it identifies ONE process: a recycled pid has
3
+ // the same number and a different start time.
4
+ import { execFile } from "node:child_process";
5
+ import { promisify } from "node:util";
6
+ const execFileP = promisify(execFile);
7
+ /**
8
+ * macOS and Linux both answer `ps -o lstart= -p <pid>` (BSD ps and procps), run
9
+ * under the C locale so the text does not vary with the creator's language.
10
+ * One-second resolution is enough: the process that held the pid had to exit
11
+ * before it could be reused. Windows returns undefined: no marker then carries
12
+ * a start time, and `preview --stop` behaves as it did before the check.
13
+ *
14
+ * Undefined also means "could not read" (no `ps`, a ps without `lstart`), never
15
+ * "different".
16
+ */
17
+ export async function readProcessStartTime(pid, platform = process.platform) {
18
+ if (!Number.isInteger(pid) || pid <= 0)
19
+ return undefined;
20
+ if (platform === "win32")
21
+ return undefined;
22
+ try {
23
+ const { stdout } = await execFileP("ps", ["-o", "lstart=", "-p", String(pid)], {
24
+ env: { ...process.env, LC_ALL: "C" },
25
+ timeout: 2000,
26
+ });
27
+ const lstart = stdout.trim().replace(/\s+/g, " ");
28
+ return lstart === "" ? undefined : lstart;
29
+ }
30
+ catch {
31
+ return undefined;
32
+ }
33
+ }
@@ -17,6 +17,13 @@ export interface RelayInfo {
17
17
  * only assumes is live. Optional: markers written by an older CLI lack it.
18
18
  */
19
19
  expiresAt?: string;
20
+ /**
21
+ * The preview process's start time (an opaque token from
22
+ * `readProcessStartTime`), so `preview --stop` can tell the preview from an
23
+ * unrelated process that was later given the same pid. Absent from markers
24
+ * written by an older CLI or on a platform with no reader.
25
+ */
26
+ pidStartTime?: string;
20
27
  }
21
28
  export declare function relayInfoPath(cwd: string): string;
22
29
  /**
@@ -8,6 +8,13 @@ export interface ReleaseComponent {
8
8
  * cannot tell blank from forgotten, so the platform must say which one it is.
9
9
  */
10
10
  migration?: ReleaseMigration;
11
+ /**
12
+ * The released commit (40 lowercase hex). ABSENT for rows released before the
13
+ * platform recorded it, and for a value of any other shape: the scaffold
14
+ * verifies a clone against it, so a malformed value must read as "nothing to
15
+ * verify", never as a value to compare.
16
+ */
17
+ commit?: string;
11
18
  }
12
19
  export interface ReleaseMigration {
13
20
  required: boolean;
@@ -19,6 +19,7 @@ import path from "node:path";
19
19
  import { CHECK_TTL_MS } from "../dx/updateCheck.js";
20
20
  import { platformCheckEnabled } from "./offSwitch.js";
21
21
  const CACHE_FILE = "platform-release.json";
22
+ const COMMIT_SHA = /^[0-9a-f]{40}$/;
22
23
  /**
23
24
  * Freshness, with a clock that moved backwards treated as STALE rather than as
24
25
  * fresh forever. Same rule and same TTL as the CLI update check — reused rather
@@ -75,13 +76,15 @@ export function readPlatformRelease(raw) {
75
76
  for (const [name, value] of Object.entries(components)) {
76
77
  if (typeof value !== "object" || value === null)
77
78
  continue;
78
- const { version, migration } = value;
79
+ const { version, migration, commit } = value;
79
80
  if (typeof version !== "string" || version === "")
80
81
  continue;
81
82
  const component = { version };
82
83
  const note = readMigration(migration);
83
84
  if (note !== undefined)
84
85
  component.migration = note;
86
+ if (typeof commit === "string" && COMMIT_SHA.test(commit))
87
+ component.commit = commit;
85
88
  out[name] = component;
86
89
  }
87
90
  const parsed = { release, components: out };
@@ -331,7 +331,7 @@ export function stoppedElsewhere() {
331
331
  export function endedDisconnected() {
332
332
  return [
333
333
  "",
334
- "✗ Preview ended — the connection to Vincentt closed.",
334
+ "✗ Preview ended. The connection to Vincentt closed.",
335
335
  "",
336
336
  " This window can't tell whether that was your network, your machine going to",
337
337
  " sleep, or something on our side. Nothing is wrong with your project.",
@@ -375,7 +375,7 @@ export function hardMaxWarning(clockTime, minutesLeft) {
375
375
  export function hardMaxReached() {
376
376
  return [
377
377
  "",
378
- "✓ Preview stopped — 12-hour maximum reached. The link no longer works.",
378
+ "✓ Preview stopped. 12-hour maximum reached. The link no longer works.",
379
379
  "",
380
380
  " Start a new one: vincentt preview",
381
381
  " For an address that lasts, publish it: vincentt publish",
@@ -435,7 +435,7 @@ export function supersededWinner() {
435
435
  export function endedUnreachable(orgName) {
436
436
  return [
437
437
  "",
438
- "✗ Preview ended — this project is no longer available to you.",
438
+ "✗ Preview ended. This project is no longer available to you.",
439
439
  "",
440
440
  ` Your access to ${orgName} may have changed. Check who you are: vincentt whoami`,
441
441
  "",
@@ -458,7 +458,7 @@ export function endedAuthExpired() {
458
458
  export function endedUnconfirmed() {
459
459
  return [
460
460
  "",
461
- "✗ Preview ended — couldn't confirm your access to this project.",
461
+ "✗ Preview ended. Couldn't confirm your access to this project.",
462
462
  "",
463
463
  " Try again: vincentt preview",
464
464
  "",
@@ -498,7 +498,7 @@ export function endedUnconfirmed() {
498
498
  export function endedAbandoned() {
499
499
  return [
500
500
  "",
501
- "✗ Preview ended — this machine stopped checking in with Vincentt.",
501
+ "✗ Preview ended. This machine stopped checking in with Vincentt.",
502
502
  "",
503
503
  " The preview needed this computer to stay in touch and it went quiet, so the",
504
504
  " link was released. Nothing is wrong with your project.",
@@ -526,7 +526,7 @@ export function egressSoft() {
526
526
  export function egressHard() {
527
527
  return [
528
528
  "",
529
- "✗ Preview ended — it reached the 5 GB ceiling.",
529
+ "✗ Preview ended. It reached the 5 GB ceiling.",
530
530
  "",
531
531
  " Start a new one: vincentt preview",
532
532
  "",
@@ -9,6 +9,11 @@ export declare const TEMPLATE_REPO = "https://github.com/vincentt-xr/v2-template
9
9
  *
10
10
  * A creator pins a known version with `--template 1.0.0`; version tags are
11
11
  * immutable, which is what makes that an escape hatch rather than a coin flip.
12
+ *
13
+ * Asked for this ref, the scaffold clones the platform's released version tag
14
+ * instead when the platform records that release's commit, and refuses a clone
15
+ * that lands anywhere else (see `releasedCommit.ts`). The moving tag is cloned
16
+ * only when there is nothing to check it against.
12
17
  */
13
18
  export declare const DEFAULT_TEMPLATE_REF = "latest";
14
19
  /**
@@ -51,6 +56,12 @@ export interface ScaffoldOptions {
51
56
  ref?: string;
52
57
  /** Test seam — defaults to spawning real git. */
53
58
  runGit?: GitRunner;
59
+ /** API origin for the release lookup. Defaults to the machine's configured one. */
60
+ apiUrl?: string;
61
+ /** Test seam for the release lookup — defaults to global `fetch`. */
62
+ fetchImpl?: typeof fetch;
63
+ /** Where the unverified-template warning goes. Defaults to stderr. */
64
+ warn?: (line: string) => void;
54
65
  }
55
66
  /**
56
67
  * The concrete version tag the clone actually landed on, or `undefined` when the
@@ -8,6 +8,7 @@ import { promises as fs } from "node:fs";
8
8
  import os from "node:os";
9
9
  import path from "node:path";
10
10
  import { promisify } from "node:util";
11
+ import { TemplateCommitMismatchError, UNVERIFIED_TEMPLATE_WARNING, resolveTemplatePin, } from "./releasedCommit.js";
11
12
  const execFileAsync = promisify(execFile);
12
13
  export const TEMPLATE_REPO = "https://github.com/vincentt-xr/v2-template.git";
13
14
  /**
@@ -20,6 +21,11 @@ export const TEMPLATE_REPO = "https://github.com/vincentt-xr/v2-template.git";
20
21
  *
21
22
  * A creator pins a known version with `--template 1.0.0`; version tags are
22
23
  * immutable, which is what makes that an escape hatch rather than a coin flip.
24
+ *
25
+ * Asked for this ref, the scaffold clones the platform's released version tag
26
+ * instead when the platform records that release's commit, and refuses a clone
27
+ * that lands anywhere else (see `releasedCommit.ts`). The moving tag is cloned
28
+ * only when there is nothing to check it against.
23
29
  */
24
30
  export const DEFAULT_TEMPLATE_REF = "latest";
25
31
  const realGit = async (args) => {
@@ -128,10 +134,21 @@ export async function scaffoldFromTemplate(targetDir, opts = {}) {
128
134
  const repo = opts.repo ?? TEMPLATE_REPO;
129
135
  const ref = opts.ref ?? DEFAULT_TEMPLATE_REF;
130
136
  const runGit = opts.runGit ?? realGit;
137
+ // Only the built-in template has a platform release to check against; a
138
+ // gallery source (`init --from`) is another repository entirely.
139
+ const pin = repo === TEMPLATE_REPO
140
+ ? await resolveTemplatePin(ref, DEFAULT_TEMPLATE_REF, {
141
+ apiUrl: opts.apiUrl,
142
+ fetchImpl: opts.fetchImpl,
143
+ })
144
+ : { ref, unverified: false };
145
+ if (pin.unverified) {
146
+ (opts.warn ?? ((line) => process.stderr.write(`${line}\n`)))(UNVERIFIED_TEMPLATE_WARNING);
147
+ }
131
148
  const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "vincentt-tmpl-"));
132
149
  try {
133
150
  try {
134
- await runGit(["clone", "--depth", "1", "--branch", ref, repo, tmp]);
151
+ await runGit(["clone", "--depth", "1", "--branch", pin.ref, repo, tmp]);
135
152
  }
136
153
  catch (err) {
137
154
  const msg = err instanceof Error ? err.message : String(err);
@@ -140,11 +157,26 @@ export async function scaffoldFromTemplate(targetDir, opts = {}) {
140
157
  // real cause is a typo'd version). git names the ref in this specific case,
141
158
  // so split on that rather than reporting one cause for both.
142
159
  if (/(remote branch|Remote branch).*not found|couldn't find remote ref/i.test(msg)) {
143
- throw new ScaffoldRefError(`The template has no version "${ref}". ` +
144
- `Check the tag exists, or omit --template to use the latest release.`);
160
+ throw new ScaffoldRefError(pin.ref === ref
161
+ ? `The template has no version "${ref}". ` +
162
+ `Check the tag exists, or omit --template to use the latest release.`
163
+ : `The template has no tag for its released version "${pin.ref}". ` +
164
+ `Report this to the Vincentt team.`);
145
165
  }
146
166
  throw new Error(`Could not clone the ${repo} template (check your git access): ${msg}`);
147
167
  }
168
+ if (pin.expectedCommit !== undefined) {
169
+ let actual;
170
+ try {
171
+ actual = String((await runGit(["-C", tmp, "rev-parse", "HEAD"])) ?? "").trim();
172
+ }
173
+ catch {
174
+ actual = "";
175
+ }
176
+ if (actual !== pin.expectedCommit) {
177
+ throw new TemplateCommitMismatchError(pin.releasedVersion ?? pin.ref, pin.expectedCommit, actual || "(could not be read)");
178
+ }
179
+ }
148
180
  // BEFORE the strip below — `.git` is the only place the concrete tag is
149
181
  // readable, and the copy into the target deliberately does not carry it.
150
182
  // A failure here must not fail the scaffold: the files are correct either
@@ -0,0 +1,37 @@
1
+ export interface TemplatePin {
2
+ /** The tag to clone. */
3
+ ref: string;
4
+ /** Present when the clone must be checked; the clone fails on any other commit. */
5
+ expectedCommit?: string;
6
+ /** The version the platform released, named in the mismatch error. */
7
+ releasedVersion?: string;
8
+ /** True when the default ref is cloned with nothing to check it against. */
9
+ unverified: boolean;
10
+ }
11
+ export interface PinOptions {
12
+ /** The API origin. Defaults to the machine's configured one. */
13
+ apiUrl?: string;
14
+ /** Test seam, passed through to `fetchRelease`. */
15
+ fetchImpl?: typeof fetch;
16
+ }
17
+ /**
18
+ * Resolve the pin for a scaffold of the built-in template.
19
+ *
20
+ * `defaultRef` is the moving tag. Asked for it, the platform's release version
21
+ * is cloned in its place when that release carries a commit; otherwise the
22
+ * moving tag is cloned as before and the result says it went unverified. An
23
+ * explicit version is checked only when it is the version the platform
24
+ * released, since older version tags are protected by the tag ruleset and the
25
+ * platform holds no commit for them.
26
+ */
27
+ export declare function resolveTemplatePin(requested: string, defaultRef: string, opts?: PinOptions): Promise<TemplatePin>;
28
+ /** The one-line stderr warning for a default-ref scaffold with nothing to check. */
29
+ export declare const UNVERIFIED_TEMPLATE_WARNING = "! The template could not be verified against the platform release; cloned the latest tag unchecked.";
30
+ /**
31
+ * The cloned commit is not the one the platform released. Nothing is written
32
+ * and there is no fallback: a moved tag is exactly what this check exists to
33
+ * catch, and retrying `latest` would clone the same suspect tree unchecked.
34
+ */
35
+ export declare class TemplateCommitMismatchError extends Error {
36
+ constructor(version: string, expected: string, actual: string);
37
+ }
@@ -0,0 +1,71 @@
1
+ // Which template ref to clone, and which commit it must land on.
2
+ //
3
+ // The template's tags live in a public repo whose admins can move them by hand,
4
+ // so the clone alone proves nothing. The platform records each template
5
+ // release's commit independently (`GET /platform/versions`), and a scaffold of
6
+ // the default ref clones THAT release's version tag and checks the commit.
7
+ import { TEMPLATE_COMPONENT } from "../platform/allowlist.js";
8
+ import { fetchRelease } from "../platform/releaseFetch.js";
9
+ import { loadMachineConfig, machineApiUrl } from "../shared/config.js";
10
+ /** Same shape the scaffold accepts as a concrete version tag. */
11
+ const CONCRETE_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+$/;
12
+ /**
13
+ * Resolve the pin for a scaffold of the built-in template.
14
+ *
15
+ * `defaultRef` is the moving tag. Asked for it, the platform's release version
16
+ * is cloned in its place when that release carries a commit; otherwise the
17
+ * moving tag is cloned as before and the result says it went unverified. An
18
+ * explicit version is checked only when it is the version the platform
19
+ * released, since older version tags are protected by the tag ruleset and the
20
+ * platform holds no commit for them.
21
+ */
22
+ export async function resolveTemplatePin(requested, defaultRef, opts = {}) {
23
+ const row = await fetchTemplateRow(opts);
24
+ const verifiable = row !== undefined && row.commit !== undefined && CONCRETE_VERSION.test(row.version);
25
+ if (requested === defaultRef) {
26
+ if (!verifiable)
27
+ return { ref: defaultRef, unverified: true };
28
+ return {
29
+ ref: row.version,
30
+ expectedCommit: row.commit,
31
+ releasedVersion: row.version,
32
+ unverified: false,
33
+ };
34
+ }
35
+ if (verifiable && requested === row.version) {
36
+ return {
37
+ ref: requested,
38
+ expectedCommit: row.commit,
39
+ releasedVersion: row.version,
40
+ unverified: false,
41
+ };
42
+ }
43
+ return { ref: requested, unverified: false };
44
+ }
45
+ async function fetchTemplateRow(opts) {
46
+ try {
47
+ const apiUrl = opts.apiUrl ?? machineApiUrl(await loadMachineConfig());
48
+ const fetched = await fetchRelease({ apiUrl, fetchImpl: opts.fetchImpl });
49
+ return fetched?.release.components[TEMPLATE_COMPONENT];
50
+ }
51
+ catch {
52
+ // An unreadable machine config is the same as an unreachable platform.
53
+ return undefined;
54
+ }
55
+ }
56
+ /** The one-line stderr warning for a default-ref scaffold with nothing to check. */
57
+ export const UNVERIFIED_TEMPLATE_WARNING = "! The template could not be verified against the platform release; cloned the latest tag unchecked.";
58
+ /**
59
+ * The cloned commit is not the one the platform released. Nothing is written
60
+ * and there is no fallback: a moved tag is exactly what this check exists to
61
+ * catch, and retrying `latest` would clone the same suspect tree unchecked.
62
+ */
63
+ export class TemplateCommitMismatchError extends Error {
64
+ constructor(version, expected, actual) {
65
+ super(`The template did not match the released version ${version}.\n` +
66
+ ` Expected commit ${expected}\n` +
67
+ ` Actual commit ${actual}\n\n` +
68
+ ` Nothing was written. Report this to the Vincentt team.`);
69
+ this.name = "TemplateCommitMismatchError";
70
+ }
71
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincentt-xr/cli",
3
- "version": "0.18.1",
3
+ "version": "0.18.2",
4
4
  "description": "Vincentt CLI — the vincentt command-line dev loop (login, create, publish, preview, diagnostics, feedback, relay)",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",