@reticlehq/vite-plugin 3.7.0 → 3.8.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.
@@ -14,3 +14,5 @@ export interface AnnounceIo {
14
14
  * throw inside a signal handler does not degrade, it takes the shutdown with it.
15
15
  */
16
16
  export declare function announceDevServer(entry: DevServerEntry, home?: string, io?: AnnounceIo): () => void;
17
+ /** The URL to announce: the dev proxy's when one runs this server, else the one Vite reports. */
18
+ export declare function announcedUrl(env: Readonly<Record<string, string | undefined>>, reported: string | undefined, port: number): string;
package/dist/announce.js CHANGED
@@ -58,3 +58,16 @@ export function announceDevServer(entry, home = stateHome(), io = nodeIo) {
58
58
  }
59
59
  };
60
60
  }
61
+ /**
62
+ * The environment variable a dev proxy sets to the URL the app is served on. portless runs each app,
63
+ * and each git worktree of it, at `https://<branch>.<app>.localhost`, while the dev server listens on
64
+ * a port of its own. The page's origin is the proxy URL, so that is what the daemon must match.
65
+ */
66
+ const DEV_PROXY_URL_ENV = 'PORTLESS_URL';
67
+ /** The URL to announce: the dev proxy's when one runs this server, else the one Vite reports. */
68
+ export function announcedUrl(env, reported, port) {
69
+ const proxied = env[DEV_PROXY_URL_ENV];
70
+ if (proxied !== undefined && proxied.length > 0)
71
+ return proxied;
72
+ return reported ?? `http://localhost:${String(port)}/`;
73
+ }
package/dist/index.cjs CHANGED
@@ -4122,126 +4122,7 @@ var coerce = {
4122
4122
  };
4123
4123
  var NEVER = INVALID;
4124
4124
 
4125
- // ../../../core/dist/wire/constants/event-types.js
4126
- var EventType = {
4127
- DOM_ADDED: "dom.added",
4128
- DOM_REMOVED: "dom.removed",
4129
- DOM_ATTR: "dom.attr",
4130
- DOM_TEXT: "dom.text",
4131
- NET_REQUEST: "net.request",
4132
- NET_PENDING: "net.pending",
4133
- /** An SSE (EventSource) or WebSocket frame — a message on a long-lived streaming connection. */
4134
- NET_STREAM: "net.stream",
4135
- /** A web-perf metric a screenshot can't verify: LCP, cumulative layout shift, or a long task. */
4136
- PERF: "perf",
4137
- ROUTE_CHANGE: "route.change",
4138
- CONSOLE_LOG: "console.log",
4139
- CONSOLE_WARN: "console.warn",
4140
- CONSOLE_ERROR: "console.error",
4141
- CONSOLE_INFO: "console.info",
4142
- CONSOLE_DEBUG: "console.debug",
4143
- ERROR_UNCAUGHT: "error.uncaught",
4144
- VISIBLE_SHOWN: "visible.shown",
4145
- ANIM_START: "anim.start",
4146
- ANIM_END: "anim.end",
4147
- SCROLL_POSITION: "scroll.position",
4148
- REVEAL_SHOWN: "reveal.shown",
4149
- SIGNAL: "signal",
4150
- STATE_CHANGE: "state.change",
4151
- /** a write to localStorage/sessionStorage/cookies — `data: { area, key, old?, new? }` (values redacted). */
4152
- STORAGE_CHANGE: "storage.change",
4153
- /** page-level visibility/focus health (distinct from element-level VISIBLE_*). */
4154
- PAGE_HEALTH: "page.health",
4155
- /**
4156
- * synthetic: the page called window.open, so the consequence of what was just clicked may live in
4157
- * another browsing context this one cannot observe (an OAuth popup is the archetype).
4158
- * `data: { href }` — the URL the page asked to open, when it named one.
4159
- */
4160
- CONTEXT_OPENED: "context.opened",
4161
- /**
4162
- * The app opened a native `alert`/`confirm`/`prompt` while Reticle was driving it.
4163
- *
4164
- * Recorded because Reticle ANSWERS these rather than letting them block — a native dialog halts
4165
- * the main thread, and the SDK's own message pump is on that thread, so one `confirm` behind a
4166
- * driven click made the tab permanently unresponsive with no recovery from inside the session.
4167
- * Answering silently would trade a wedge for an invisible one, so the question the app asked, and
4168
- * the answer given, ride out as an event.
4169
- */
4170
- DIALOG_OPENED: "dialog.opened",
4171
- /** aggregated React commits over a throttle window (dev builds) — `data: { commits }`. Commit storms /
4172
- * wasted re-renders show up here without a per-render flood. */
4173
- RENDER_COMMIT: "render.commit",
4174
- /** element focus moved — `data: { to, from, toBody }`. Focus dropping to body after an act is a regression. */
4175
- FOCUS_CHANGE: "focus.change",
4176
- /**
4177
- * a form field's value moved — `data: { field, kind, value?, redacted?, length }`.
4178
- *
4179
- * The DOM observer never saw this: `value` is in its attribute allowlist, but React and every
4180
- * controlled input set the PROPERTY, so `MutationObserver` does not fire. `value` is omitted and
4181
- * `redacted` set for a password, a sensitive name, or a payment autocomplete hint; `length` is
4182
- * always present, because "it was wiped" is assertable without carrying anybody's data.
4183
- */
4184
- FIELD_CHANGE: "field.change",
4185
- /** browser → bridge: a human recording compiled in-page. */
4186
- FLOW_RECORDED: "flow.recorded",
4187
- /** synthetic: browser transport queue overflowed; events were dropped. `data: { dropped: number }`. */
4188
- TRANSPORT_OVERFLOW: "transport.overflow",
4189
- /**
4190
- * synthetic: a per-channel cap truncated a batch (e.g. a DOM mutation flood). `data: { channel, dropped }`.
4191
- * Marks downstream rollups/envelopes as built on incomplete data — a ledger that lies at scale is worse
4192
- * than no ledger, so truncation is never silent.
4193
- */
4194
- TRUNCATED: "truncated",
4195
- /**
4196
- * synthetic: the SDK detected a region it CANNOT observe (a cross-origin iframe, a closed shadow root).
4197
- * `data: { kind: BlindSpotKind, count }`. Surfaced on results as `coverage: partial` so a green never
4198
- * implies it saw everything.
4199
- */
4200
- BLIND_SPOT: "blind-spot",
4201
- /** synthetic: the SDK ITSELF failed (an observer threw). `data: { site, message, errorType }`.
4202
- * Rides the existing bridge — no outbound request. See browser/observers/sdk-failure.ts. */
4203
- SDK_FAILED: "sdk.failed",
4204
- /**
4205
- * synthetic (driven only): CDP/Playwright-authoritative network detail for a response the in-page
4206
- * fetch/XHR wrapper also saw — full response headers + authoritative status/mimeType the page-side
4207
- * wrapper can't reach. `data: { url, method?, status, headers, resourceType? }`. Merged onto the
4208
- * matching in-page NET_REQUEST so the driven view never loses fidelity to an outside-in tool.
4209
- */
4210
- NET_DETAIL: "net.detail",
4211
- /**
4212
- * Live-control: browser → bridge. A human acted on the presenter panel.
4213
- * `data: { kind: HumanControlKind; text?: string }`. Rides the existing EventMessage.
4214
- */
4215
- HUMAN_CONTROL: "human.control",
4216
- /**
4217
- * Human review: browser → bridge. A human pinned a mistake to an element on the running page
4218
- * (the "annotate the bug where you see it" loop). `data` narrows to HumanMarkDataSchema — a note
4219
- * plus a re-resolvable element anchor (and its source file:line when the framework stamped one) so
4220
- * the agent that drains the mark knows exactly which element and which source to fix.
4221
- */
4222
- HUMAN_MARK: "human.mark",
4223
- /**
4224
- * The app produced a FILE — a Blob handed to `URL.createObjectURL`, usually saved by clicking an
4225
- * anchor with `download`. `data: { filename?, mimeType, bytes, lines?, preview? }`. The one artifact
4226
- * class no outside-the-browser tool can inspect: it never crosses the network, so there is no
4227
- * request to intercept. See `observers/download.ts` for the defect that motivated it.
4228
- */
4229
- DOWNLOAD: "download",
4230
- /**
4231
- * Browser -> bridge: a person used the HUD — pressed a control, or the HUD changed how it sits.
4232
- * `data` narrows to HudUseDataSchema: names only. Telemetry, not evidence: the session takes it
4233
- * off the wire before the buffer, so no verdict, settle or agent read ever sees it.
4234
- */
4235
- HUD_USED: "hud.used"
4236
- };
4237
-
4238
- // ../../../core/dist/wire/constants/constants.js
4239
- var RETICLE_DEFAULT_PORT = 4400;
4240
- var RETICLE_WS_PATH = "/reticle";
4241
- var RETICLE_CLIENT_HOST = "localhost";
4242
- function bridgeWsUrl(port = RETICLE_DEFAULT_PORT, host = RETICLE_CLIENT_HOST) {
4243
- return `ws://${host}:${String(port)}${RETICLE_WS_PATH}`;
4244
- }
4125
+ // ../../../core/dist/wire/constants/reticle-env.js
4245
4126
  var ReticleEnv = {
4246
4127
  /** "0"/"false"/"off" turns off telemetry and every optional outbound call; DO_NOT_TRACK too. */
4247
4128
  TELEMETRY: "RETICLE_TELEMETRY",
@@ -4369,8 +4250,148 @@ var ReticleEnv = {
4369
4250
  /** The platform host. Read through `cloudUrlFrom()`, which also accepts `URL`. */
4370
4251
  CLOUD_URL: "RETICLE_CLOUD_URL",
4371
4252
  /** The short name for `CLOUD_URL`, which people type first. `CLOUD_URL` wins when both are set. */
4372
- URL: "RETICLE_URL"
4253
+ URL: "RETICLE_URL",
4254
+ /** "0"/"false"/"off" stops `reticle hook prompt` writing prompts, on this whole machine. */
4255
+ PROMPT_CAPTURE: "RETICLE_PROMPT_CAPTURE",
4256
+ /** "0"/"false"/"off" stops the daemon's background sync, on this whole machine. */
4257
+ BACKGROUND_SYNC: "RETICLE_BACKGROUND_SYNC"
4373
4258
  };
4259
+
4260
+ // ../../../core/dist/wire/constants/event-types.js
4261
+ var EventType = {
4262
+ DOM_ADDED: "dom.added",
4263
+ DOM_REMOVED: "dom.removed",
4264
+ DOM_ATTR: "dom.attr",
4265
+ DOM_TEXT: "dom.text",
4266
+ NET_REQUEST: "net.request",
4267
+ NET_PENDING: "net.pending",
4268
+ /** An SSE (EventSource) or WebSocket frame — a message on a long-lived streaming connection. */
4269
+ NET_STREAM: "net.stream",
4270
+ /** A web-perf metric a screenshot can't verify: LCP, cumulative layout shift, or a long task. */
4271
+ PERF: "perf",
4272
+ ROUTE_CHANGE: "route.change",
4273
+ CONSOLE_LOG: "console.log",
4274
+ CONSOLE_WARN: "console.warn",
4275
+ CONSOLE_ERROR: "console.error",
4276
+ CONSOLE_INFO: "console.info",
4277
+ CONSOLE_DEBUG: "console.debug",
4278
+ ERROR_UNCAUGHT: "error.uncaught",
4279
+ VISIBLE_SHOWN: "visible.shown",
4280
+ ANIM_START: "anim.start",
4281
+ ANIM_END: "anim.end",
4282
+ SCROLL_POSITION: "scroll.position",
4283
+ REVEAL_SHOWN: "reveal.shown",
4284
+ SIGNAL: "signal",
4285
+ STATE_CHANGE: "state.change",
4286
+ /** a write to localStorage/sessionStorage/cookies — `data: { area, key, old?, new? }` (values redacted). */
4287
+ STORAGE_CHANGE: "storage.change",
4288
+ /** page-level visibility/focus health (distinct from element-level VISIBLE_*). */
4289
+ PAGE_HEALTH: "page.health",
4290
+ /**
4291
+ * synthetic: the page called window.open, so the consequence of what was just clicked may live in
4292
+ * another browsing context this one cannot observe (an OAuth popup is the archetype).
4293
+ * `data: { href }` — the URL the page asked to open, when it named one.
4294
+ */
4295
+ CONTEXT_OPENED: "context.opened",
4296
+ /**
4297
+ * The app opened a native `alert`/`confirm`/`prompt` while Reticle was driving it.
4298
+ *
4299
+ * Recorded because Reticle ANSWERS these rather than letting them block — a native dialog halts
4300
+ * the main thread, and the SDK's own message pump is on that thread, so one `confirm` behind a
4301
+ * driven click made the tab permanently unresponsive with no recovery from inside the session.
4302
+ * Answering silently would trade a wedge for an invisible one, so the question the app asked, and
4303
+ * the answer given, ride out as an event.
4304
+ */
4305
+ DIALOG_OPENED: "dialog.opened",
4306
+ /** aggregated React commits over a throttle window (dev builds) — `data: { commits }`. Commit storms /
4307
+ * wasted re-renders show up here without a per-render flood. */
4308
+ RENDER_COMMIT: "render.commit",
4309
+ /** element focus moved — `data: { to, from, toBody }`. Focus dropping to body after an act is a regression. */
4310
+ FOCUS_CHANGE: "focus.change",
4311
+ /**
4312
+ * a form field's value moved — `data: { field, kind, value?, checked?, previous?, previousChecked?, redacted?, length }`.
4313
+ *
4314
+ * The DOM observer never saw this: `value` is in its attribute allowlist, but React and every
4315
+ * controlled input set the PROPERTY, so `MutationObserver` does not fire. `value` is omitted and
4316
+ * `redacted` set for a password, a sensitive name, or a payment autocomplete hint; `length` is
4317
+ * always present, because "it was wiped" is assertable without carrying anybody's data.
4318
+ * `checked` is the checkbox/radio property (`value` there is the HTML value, usually `"on"`).
4319
+ * `kind: "settled"` is a read after the action's handlers; `previous` / `previousChecked` are
4320
+ * the pre-action reading, so a restore is distinguishable from a render.
4321
+ */
4322
+ FIELD_CHANGE: "field.change",
4323
+ /** browser → bridge: a human recording compiled in-page. */
4324
+ FLOW_RECORDED: "flow.recorded",
4325
+ /** synthetic: browser transport queue overflowed; events were dropped. `data: { dropped: number }`. */
4326
+ TRANSPORT_OVERFLOW: "transport.overflow",
4327
+ /**
4328
+ * synthetic: a per-channel cap truncated a batch (e.g. a DOM mutation flood). `data: { channel, dropped }`.
4329
+ * Marks downstream rollups/envelopes as built on incomplete data — a ledger that lies at scale is worse
4330
+ * than no ledger, so truncation is never silent.
4331
+ */
4332
+ TRUNCATED: "truncated",
4333
+ /**
4334
+ * synthetic: the SDK detected a region it CANNOT observe (a cross-origin iframe, a closed shadow root).
4335
+ * `data: { kind: BlindSpotKind, count }`. Surfaced on results as `coverage: partial` so a green never
4336
+ * implies it saw everything.
4337
+ */
4338
+ BLIND_SPOT: "blind-spot",
4339
+ /** synthetic: the SDK ITSELF failed (an observer threw). `data: { site, message, errorType }`.
4340
+ * Rides the existing bridge — no outbound request. See browser/observers/sdk-failure.ts. */
4341
+ SDK_FAILED: "sdk.failed",
4342
+ /**
4343
+ * synthetic (driven only): CDP/Playwright-authoritative network detail for a response the in-page
4344
+ * fetch/XHR wrapper also saw — full response headers + authoritative status/mimeType the page-side
4345
+ * wrapper can't reach. `data: { url, method?, status, headers, resourceType? }`. Merged onto the
4346
+ * matching in-page NET_REQUEST so the driven view never loses fidelity to an outside-in tool.
4347
+ */
4348
+ NET_DETAIL: "net.detail",
4349
+ /**
4350
+ * Live-control: browser → bridge. A human acted on the presenter panel.
4351
+ * `data: { kind: HumanControlKind; text?: string }`. Rides the existing EventMessage.
4352
+ */
4353
+ HUMAN_CONTROL: "human.control",
4354
+ /**
4355
+ * Human review: browser → bridge. A human pinned a mistake to an element on the running page
4356
+ * (the "annotate the bug where you see it" loop). `data` narrows to HumanMarkDataSchema — a note
4357
+ * plus a re-resolvable element anchor (and its source file:line when the framework stamped one) so
4358
+ * the agent that drains the mark knows exactly which element and which source to fix.
4359
+ */
4360
+ HUMAN_MARK: "human.mark",
4361
+ /**
4362
+ * The app produced a FILE — a Blob handed to `URL.createObjectURL`, usually saved by clicking an
4363
+ * anchor with `download`. `data: { filename?, mimeType, bytes, lines?, preview? }`. The one artifact
4364
+ * class no outside-the-browser tool can inspect: it never crosses the network, so there is no
4365
+ * request to intercept. See `observers/download.ts` for the defect that motivated it.
4366
+ */
4367
+ DOWNLOAD: "download",
4368
+ /**
4369
+ * Browser -> bridge: a person used the HUD — pressed a control, or the HUD changed how it sits.
4370
+ * `data` narrows to HudUseDataSchema: names only. Telemetry, not evidence: the session takes it
4371
+ * off the wire before the buffer, so no verdict, settle or agent read ever sees it.
4372
+ */
4373
+ HUD_USED: "hud.used",
4374
+ /**
4375
+ * A `<video>` or `<audio>` changed playback state: `data: { event, tag, currentTime, duration?,
4376
+ * readyState, paused }`. The event's own `t` orders it against the network, which is what a media
4377
+ * defect ("Ready before the response arrived") is: an ordering in time no DOM count shows.
4378
+ */
4379
+ MEDIA_STATE: "media.state",
4380
+ /**
4381
+ * A `<canvas>` drew something different: `data: { digest, width, height, mean }`. A hash of a
4382
+ * 16x16 sample of its pixels and their mean colour, emitted only when the digest changes. What a
4383
+ * canvas shows never reaches the DOM, so without this a canvas app's output is invisible.
4384
+ */
4385
+ CANVAS_DIGEST: "canvas.digest"
4386
+ };
4387
+
4388
+ // ../../../core/dist/wire/constants/constants.js
4389
+ var RETICLE_DEFAULT_PORT = 4400;
4390
+ var RETICLE_WS_PATH = "/reticle";
4391
+ var RETICLE_CLIENT_HOST = "localhost";
4392
+ function bridgeWsUrl(port = RETICLE_DEFAULT_PORT, host = RETICLE_CLIENT_HOST) {
4393
+ return `ws://${host}:${String(port)}${RETICLE_WS_PATH}`;
4394
+ }
4374
4395
  var SYNC_BATCH_LIMITS = {
4375
4396
  MAX_RUNS: 50,
4376
4397
  MAX_BYTES: 4 * 1024 * 1024
@@ -4465,6 +4486,8 @@ var ReticleDir = {
4465
4486
  VISUAL_SUBDIR: "visual",
4466
4487
  /** verification-run artifacts —.reticle/runs/<runId>.json (the OEM/CI-consumable verdict). */
4467
4488
  RUNS_SUBDIR: "runs",
4489
+ /** Dev-server logs, only when the state home cannot be written. Machine-local, like `runs/`. */
4490
+ LOGS_SUBDIR: "logs",
4468
4491
  /** fail-to-pass bug capsules —.reticle/capsules/<id>.json (a minimal failing flow + its evidence). */
4469
4492
  CAPSULES_SUBDIR: "capsules",
4470
4493
  /**
@@ -4479,6 +4502,11 @@ var ReticleDir = {
4479
4502
  PLANS_SUBDIR: "plans",
4480
4503
  /** durable causal journal —.reticle/sessions/<id>/{events,actions}.jsonl (the substrate). */
4481
4504
  SESSIONS_SUBDIR: "sessions",
4505
+ /**
4506
+ * Every tool call an agent made, in full, with what it did to the page —
4507
+ * .reticle/episodes/<connection>.jsonl. Local evidence: never committed, never sent.
4508
+ */
4509
+ EPISODES_SUBDIR: "episodes",
4482
4510
  /** append-only event ledger inside a session dir (one ReticleEvent per line). */
4483
4511
  JOURNAL_EVENTS_FILE: "events.jsonl",
4484
4512
  /** append-only action ledger inside a session dir (one JournalAction per line). */
@@ -4502,8 +4530,12 @@ var ReticleDir = {
4502
4530
  COVERAGE_FILE: "coverage.json",
4503
4531
  /** Human review marks (HUD notes), pending and resolved — see server portal/session/human. */
4504
4532
  NOTES_FILE: "notes.json",
4505
- /** The user's latest request, as the agent relayed it. Local: it is the user's own words. */
4533
+ /** The user's latest request: the newest prompt the hook captured, or the one the agent relayed. Local. */
4506
4534
  REQUEST_FILE: "request.json",
4535
+ /** Every prompt the agent hook captured, one JSON line each, oldest first. `request.json` holds the newest. */
4536
+ REQUESTS_LOG_FILE: "requests.jsonl",
4537
+ /** The prompt history moved aside when `requests.jsonl` fills. The same words, so the same rule. */
4538
+ REQUESTS_LOG_ROTATED_FILE: "requests.1.jsonl",
4507
4539
  APP_NOTES_FILE: "app-notes.json",
4508
4540
  // confirmed targeting hints for the next session; never evidence
4509
4541
  PLATFORM_MOMENTS_FILE: "platform-moments.json",
@@ -4600,6 +4632,111 @@ function projectIdFrom(pkgName, rootPath, hash) {
4600
4632
  return `${base}-${hash(rootPath)}`;
4601
4633
  }
4602
4634
 
4635
+ // ../../../core/dist/identity/checkout.js
4636
+ var MAX_UP_LEVELS = 50;
4637
+ var GITDIR_PREFIX = "gitdir:";
4638
+ var BRANCH_PREFIX = "ref: refs/heads/";
4639
+ var WORKTREES_SEGMENT = "worktrees";
4640
+ var GIT_DIR_NAME = ".git";
4641
+ function checkoutOf(dir, readFile) {
4642
+ const sep2 = dir.includes("\\") ? "\\" : "/";
4643
+ let current = trimEnd(dir, sep2);
4644
+ for (let level = 0; level <= MAX_UP_LEVELS; level += 1) {
4645
+ const head = readFile(`${current}${sep2}${GIT_DIR_NAME}${sep2}HEAD`);
4646
+ if (head !== void 0) {
4647
+ return withBranch({
4648
+ root: current,
4649
+ mainRoot: current,
4650
+ commonDir: `${current}${sep2}${GIT_DIR_NAME}`,
4651
+ linked: false
4652
+ }, head);
4653
+ }
4654
+ const pointer = readFile(`${current}${sep2}${GIT_DIR_NAME}`);
4655
+ if (pointer !== void 0 && pointer.trim().startsWith(GITDIR_PREFIX)) {
4656
+ return linkedCheckout(current, pointer.trim().slice(GITDIR_PREFIX.length).trim(), sep2, readFile);
4657
+ }
4658
+ const parent = parentOf(current, sep2);
4659
+ if (parent === void 0)
4660
+ return void 0;
4661
+ current = parent;
4662
+ }
4663
+ return void 0;
4664
+ }
4665
+ function mainCheckoutTwin(path, readFile) {
4666
+ const checkout = checkoutOf(path, readFile);
4667
+ if (checkout === void 0 || checkout.root === checkout.mainRoot)
4668
+ return void 0;
4669
+ return `${checkout.mainRoot}${path.slice(checkout.root.length)}`;
4670
+ }
4671
+ function linkedCheckout(root, gitdir, sep2, readFile) {
4672
+ const absolute = resolvePath(root, gitdir.split(/[\\/]/).join(sep2), sep2);
4673
+ const parts = absolute.split(sep2);
4674
+ const isWorktree = WORKTREES_SEGMENT === parts[parts.length - 2];
4675
+ const gitDirOfRepo = parts.slice(0, -2);
4676
+ const commonDir = isWorktree ? gitDirOfRepo.join(sep2) : absolute;
4677
+ const mainRoot = isWorktree && GIT_DIR_NAME === gitDirOfRepo[gitDirOfRepo.length - 1] ? gitDirOfRepo.slice(0, -1).join(sep2) : root;
4678
+ const head = readFile(`${absolute}${sep2}HEAD`);
4679
+ const checkout = { root, mainRoot, commonDir, linked: isWorktree };
4680
+ return head === void 0 ? checkout : withBranch(checkout, head);
4681
+ }
4682
+ function withBranch(checkout, head) {
4683
+ const ref = head.trim();
4684
+ return ref.startsWith(BRANCH_PREFIX) ? { ...checkout, branch: ref.slice(BRANCH_PREFIX.length) } : checkout;
4685
+ }
4686
+ function resolvePath(base, rel, sep2) {
4687
+ const absolute = rel.startsWith(sep2) || /^[A-Za-z]:/.test(rel);
4688
+ const parts = (absolute ? rel : `${base}${sep2}${rel}`).split(sep2);
4689
+ const out = [];
4690
+ for (const part of parts) {
4691
+ if ("." === part || "" === part && out.length > 0)
4692
+ continue;
4693
+ if (".." === part) {
4694
+ if (out.length > 1)
4695
+ out.pop();
4696
+ continue;
4697
+ }
4698
+ out.push(part);
4699
+ }
4700
+ return out.join(sep2);
4701
+ }
4702
+ function parentOf(path, sep2) {
4703
+ const at = path.lastIndexOf(sep2);
4704
+ if (at <= 0)
4705
+ return void 0;
4706
+ const parent = path.slice(0, at);
4707
+ return /^[A-Za-z]:$/.test(parent) ? void 0 : parent;
4708
+ }
4709
+ function trimEnd(path, sep2) {
4710
+ let end = path.length;
4711
+ while (end > 1 && sep2 === path[end - 1])
4712
+ end -= 1;
4713
+ return path.slice(0, end);
4714
+ }
4715
+
4716
+ // ../../../core/dist/identity/state-dir.js
4717
+ var stateDirNotAbsolute = (dir) => `The Reticle state folder "${dir}" is not an absolute path, so Reticle did not use it. A relative folder is made inside the folder this process runs in, which is often the project. Set ${ReticleEnv.PAIRING_TOKEN_DIR} and ${ReticleEnv.STATE_DIR} to absolute paths, or unset them.`;
4718
+ var stateDirResolvedFromCwd = (dir, resolved) => `The Reticle state folder "${dir}" is a relative path. Reticle uses "${resolved}", in the folder this process runs in. Set ${ReticleEnv.PAIRING_TOKEN_DIR} to an absolute path to remove this warning.`;
4719
+ var stateDirWindowsPathOnPosix = (dir, fallback) => `The Reticle state folder "${dir}" is a Windows path, and this system does not use Windows paths. Reticle did not use it, and uses "${fallback}". Set ${ReticleEnv.PAIRING_TOKEN_DIR} to a path for this system, or unset it.`;
4720
+ var WINDOWS_DRIVE = /^[A-Za-z]:/;
4721
+ var POSIX_SEPARATOR = "/";
4722
+ function resolveStateDir(override, fallback, rules, warn) {
4723
+ if (override !== void 0 && override.length > 0) {
4724
+ if (rules.isAbsolute(override)) {
4725
+ return override;
4726
+ }
4727
+ if (rules.sep !== POSIX_SEPARATOR || !WINDOWS_DRIVE.test(override)) {
4728
+ const resolved = rules.resolve(override);
4729
+ warn(stateDirResolvedFromCwd(override, resolved));
4730
+ return resolved;
4731
+ }
4732
+ warn(stateDirWindowsPathOnPosix(override, fallback));
4733
+ }
4734
+ if (!rules.isAbsolute(fallback)) {
4735
+ throw new Error(stateDirNotAbsolute(fallback));
4736
+ }
4737
+ return fallback;
4738
+ }
4739
+
4603
4740
  // ../../../core/dist/registry/daemon-registry.js
4604
4741
  var DAEMON_REGISTRY_PREFIX = "daemon-";
4605
4742
  var DAEMON_REGISTRY_SUFFIX = ".json";
@@ -4690,6 +4827,32 @@ function readNearestField(startDir, basename, field, readFile, maxDepth, accept)
4690
4827
  }
4691
4828
  return void 0;
4692
4829
  }
4830
+ function readConfigField(startDir, field, readFile, accept) {
4831
+ const own = readNearestField(
4832
+ startDir,
4833
+ RETICLE_CONFIG_BASENAME,
4834
+ field,
4835
+ readFile,
4836
+ MAX_CONFIG_SEARCH_DEPTH,
4837
+ accept
4838
+ );
4839
+ if (own !== void 0) return own;
4840
+ const twin = mainCheckoutTwin(startDir, (path) => {
4841
+ try {
4842
+ return readFile(path);
4843
+ } catch {
4844
+ return void 0;
4845
+ }
4846
+ });
4847
+ return twin === void 0 ? void 0 : readNearestField(
4848
+ twin,
4849
+ RETICLE_CONFIG_BASENAME,
4850
+ field,
4851
+ readFile,
4852
+ MAX_CONFIG_SEARCH_DEPTH,
4853
+ accept
4854
+ );
4855
+ }
4693
4856
  function readNearestPackageName(startDir, readFile = readFileOrThrow) {
4694
4857
  return readNearestField(
4695
4858
  startDir,
@@ -4701,32 +4864,16 @@ function readNearestPackageName(startDir, readFile = readFileOrThrow) {
4701
4864
  );
4702
4865
  }
4703
4866
  function readConfiguredProjectId(startDir, readFile = readFileOrThrow) {
4704
- return readNearestField(
4705
- startDir,
4706
- RETICLE_CONFIG_BASENAME,
4707
- "projectId",
4708
- readFile,
4709
- MAX_CONFIG_SEARCH_DEPTH,
4710
- nonEmptyString
4711
- );
4867
+ return readConfigField(startDir, "projectId", readFile, nonEmptyString);
4712
4868
  }
4713
4869
  function readConfiguredPort(startDir, readFile = readFileOrThrow) {
4714
- return readNearestField(
4715
- startDir,
4716
- RETICLE_CONFIG_BASENAME,
4717
- "port",
4718
- readFile,
4719
- MAX_CONFIG_SEARCH_DEPTH,
4720
- tcpPort
4721
- );
4870
+ return readConfigField(startDir, "port", readFile, tcpPort);
4722
4871
  }
4723
4872
  function readConfiguredTestIdAttribute(startDir, readFile = readFileOrThrow) {
4724
- return readNearestField(
4873
+ return readConfigField(
4725
4874
  startDir,
4726
- RETICLE_CONFIG_BASENAME,
4727
4875
  "testIdAttribute",
4728
4876
  readFile,
4729
- MAX_CONFIG_SEARCH_DEPTH,
4730
4877
  (value) => "string" === typeof value && TESTID_ATTR_PATTERN.test(value) ? value : void 0
4731
4878
  );
4732
4879
  }
@@ -4835,6 +4982,12 @@ function announceDevServer(entry, home = stateHome(), io = nodeIo) {
4835
4982
  }
4836
4983
  };
4837
4984
  }
4985
+ var DEV_PROXY_URL_ENV = "PORTLESS_URL";
4986
+ function announcedUrl(env, reported, port) {
4987
+ const proxied = env[DEV_PROXY_URL_ENV];
4988
+ if (proxied !== void 0 && proxied.length > 0) return proxied;
4989
+ return reported ?? `http://localhost:${String(port)}/`;
4990
+ }
4838
4991
 
4839
4992
  // src/svelte-source.ts
4840
4993
  var import_node_module = require("node:module");
@@ -5384,8 +5537,18 @@ function isHtmlEntry(id, specifier, root) {
5384
5537
  return candidate.endsWith(target.startsWith("/") ? target : `/${target}`);
5385
5538
  }
5386
5539
  function readPairingToken() {
5387
- const override = process.env[ReticleEnv.PAIRING_TOKEN_DIR];
5388
- const dir = override !== void 0 && override.length > 0 ? override : (0, import_node_path8.join)((0, import_node_os2.homedir)(), ReticleDir.ROOT);
5540
+ let dir;
5541
+ try {
5542
+ dir = resolveStateDir(
5543
+ process.env[ReticleEnv.PAIRING_TOKEN_DIR],
5544
+ (0, import_node_path8.join)((0, import_node_os2.homedir)(), ReticleDir.ROOT),
5545
+ { isAbsolute: import_node_path8.isAbsolute, resolve: import_node_path8.resolve, sep: import_node_path8.sep },
5546
+ (warning) => console.warn(`[reticle] ${warning}`)
5547
+ );
5548
+ } catch (error) {
5549
+ console.warn(`[reticle] ${error instanceof Error ? error.message : String(error)}`);
5550
+ return void 0;
5551
+ }
5389
5552
  return ensurePairingToken(dir);
5390
5553
  }
5391
5554
  var tokenWarned = false;
@@ -5661,7 +5824,7 @@ ${code}`;
5661
5824
  port,
5662
5825
  pid: process.pid,
5663
5826
  root: opts.root ?? process.cwd(),
5664
- url: server.resolvedUrls?.local[0] ?? `http://localhost:${String(port)}/`,
5827
+ url: announcedUrl(process.env, server.resolvedUrls?.local[0], port),
5665
5828
  ...opts.sdkVersion === void 0 || 0 === opts.sdkVersion.length ? {} : { sdkVersion: opts.sdkVersion },
5666
5829
  startedAt: Date.now(),
5667
5830
  ...opts.projectId === void 0 ? {} : { projectId: opts.projectId }
package/dist/index.js CHANGED
@@ -2,11 +2,11 @@ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { missingTokenWarning } from './token/missing-token.js';
3
3
  import { ensurePairingToken } from './token/ensure-token.js';
4
4
  import { homedir } from 'node:os';
5
- import { join } from 'node:path';
6
- import { RETICLE_RENDER_PREHOOK, ReticleDir, ReticleEnv, RETICLE_ROOT_GLOBAL, RETICLE_SDK_VERSION_GLOBAL, } from '@reticlehq/core';
5
+ import { isAbsolute, join, resolve, sep } from 'node:path';
6
+ import { RETICLE_RENDER_PREHOOK, ReticleDir, ReticleEnv, RETICLE_ROOT_GLOBAL, RETICLE_SDK_VERSION_GLOBAL, resolveStateDir, } from '@reticlehq/core';
7
7
  import { readConfiguredTestIdAttribute, resolveProjectId } from './project-id.js';
8
8
  import { resolveDaemonPort } from './discover-port.js';
9
- import { announceDevServer } from './announce.js';
9
+ import { announceDevServer, announcedUrl } from './announce.js';
10
10
  import { stampSvelte } from './svelte-source.js';
11
11
  import { NODE_MODULES, ignoredFileNotice, optsOutOfStamping, shouldStamp, shouldStampSvelte, stamp, } from './stamping.js';
12
12
  import { resolvableChain, sdkPackageVersion, sdkBuildFingerprint, viteMajor, optimizerOptionsKey, optimizerOptions, } from './installed.js';
@@ -135,8 +135,17 @@ function isHtmlEntry(id, specifier, root) {
135
135
  * undefined if the daemon hasn't started yet (the page reloads once it has). Exported for testing.
136
136
  */
137
137
  export function readPairingToken() {
138
- const override = process.env[ReticleEnv.PAIRING_TOKEN_DIR];
139
- const dir = override !== undefined && override.length > 0 ? override : join(homedir(), ReticleDir.ROOT);
138
+ let dir;
139
+ try {
140
+ // The daemon's rule: a relative folder is resolved against the working folder, and a Windows
141
+ // path on POSIX is not used. Both warn, so the two sides agree on one token file.
142
+ dir = resolveStateDir(process.env[ReticleEnv.PAIRING_TOKEN_DIR], join(homedir(), ReticleDir.ROOT), { isAbsolute, resolve, sep }, (warning) => console.warn(`[reticle] ${warning}`));
143
+ }
144
+ catch (error) {
145
+ // Only a home folder that is not absolute gets here. The dev server still starts.
146
+ console.warn(`[reticle] ${error instanceof Error ? error.message : String(error)}`);
147
+ return undefined;
148
+ }
140
149
  // Read-or-CREATE, matching the daemon. Reading alone meant a dev server started before the daemon
141
150
  // baked in an empty token and every page it served was refused — with the SDK loading and the
142
151
  // socket opening, so nothing looked broken. See ensure-token for the bisect.
@@ -568,7 +577,7 @@ export function reticle(options = {}) {
568
577
  port,
569
578
  pid: process.pid,
570
579
  root: opts.root ?? process.cwd(),
571
- url: server.resolvedUrls?.local[0] ?? `http://localhost:${String(port)}/`,
580
+ url: announcedUrl(process.env, server.resolvedUrls?.local[0], port),
572
581
  ...(opts.sdkVersion === undefined || 0 === opts.sdkVersion.length
573
582
  ? {}
574
583
  : { sdkVersion: opts.sdkVersion }),
@@ -9,7 +9,7 @@
9
9
  import { createHash } from 'node:crypto';
10
10
  import { dirname, join } from 'node:path';
11
11
  import { readFileSync } from 'node:fs';
12
- import { PROJECT_ID_HASH_LENGTH, TESTID_ATTR_PATTERN, projectIdFrom } from '@reticlehq/core';
12
+ import { PROJECT_ID_HASH_LENGTH, TESTID_ATTR_PATTERN, mainCheckoutTwin, projectIdFrom, } from '@reticlehq/core';
13
13
  /** The project config `reticle init` writes, and the id of record it carries. */
14
14
  const RETICLE_CONFIG_BASENAME = '.reticle.json';
15
15
  export { slugifyPackageName } from '@reticlehq/core';
@@ -76,6 +76,27 @@ function readNearestField(startDir, basename, field, readFile, maxDepth, accept)
76
76
  }
77
77
  return undefined;
78
78
  }
79
+ /**
80
+ * A field of the nearest `.reticle.json`, else of the main checkout's when `startDir` is in a git
81
+ * worktree that has none (it is git-ignored, or not committed yet). Every worktree of a project is
82
+ * the same project: one id, one daemon port.
83
+ */
84
+ function readConfigField(startDir, field, readFile, accept) {
85
+ const own = readNearestField(startDir, RETICLE_CONFIG_BASENAME, field, readFile, MAX_CONFIG_SEARCH_DEPTH, accept);
86
+ if (own !== undefined)
87
+ return own;
88
+ const twin = mainCheckoutTwin(startDir, (path) => {
89
+ try {
90
+ return readFile(path);
91
+ }
92
+ catch {
93
+ return undefined;
94
+ }
95
+ });
96
+ return twin === undefined
97
+ ? undefined
98
+ : readNearestField(twin, RETICLE_CONFIG_BASENAME, field, readFile, MAX_CONFIG_SEARCH_DEPTH, accept);
99
+ }
79
100
  /** Read the `name` from the nearest package.json at or above `startDir`, or undefined if none. */
80
101
  function readNearestPackageName(startDir, readFile = readFileOrThrow) {
81
102
  return readNearestField(startDir, 'package.json', 'name', readFile, MAX_PACKAGE_SEARCH_DEPTH, nonEmptyString);
@@ -87,7 +108,7 @@ function readNearestPackageName(startDir, readFile = readFileOrThrow) {
87
108
  * through `init` — it is not a second opinion, and where the two disagree the config wins.
88
109
  */
89
110
  export function readConfiguredProjectId(startDir, readFile = readFileOrThrow) {
90
- return readNearestField(startDir, RETICLE_CONFIG_BASENAME, 'projectId', readFile, MAX_CONFIG_SEARCH_DEPTH, nonEmptyString);
111
+ return readConfigField(startDir, 'projectId', readFile, nonEmptyString);
91
112
  }
92
113
  /**
93
114
  * Read the daemon port `reticle init --port` recorded in the nearest `.reticle.json`, or undefined.
@@ -97,7 +118,7 @@ export function readConfiguredProjectId(startDir, readFile = readFileOrThrow) {
97
118
  * project's daemon — and the bridge refused it.
98
119
  */
99
120
  export function readConfiguredPort(startDir, readFile = readFileOrThrow) {
100
- return readNearestField(startDir, RETICLE_CONFIG_BASENAME, 'port', readFile, MAX_CONFIG_SEARCH_DEPTH, tcpPort);
121
+ return readConfigField(startDir, 'port', readFile, tcpPort);
101
122
  }
102
123
  /**
103
124
  * Read `testIdAttribute` from the nearest `.reticle.json`, or undefined: the attribute this project
@@ -106,7 +127,7 @@ export function readConfiguredPort(startDir, readFile = readFileOrThrow) {
106
127
  * already agree on, so the plugin forwards it to `connect()` rather than asking for it twice.
107
128
  */
108
129
  export function readConfiguredTestIdAttribute(startDir, readFile = readFileOrThrow) {
109
- return readNearestField(startDir, RETICLE_CONFIG_BASENAME, 'testIdAttribute', readFile, MAX_CONFIG_SEARCH_DEPTH, (value) => ('string' === typeof value && TESTID_ATTR_PATTERN.test(value) ? value : undefined));
130
+ return readConfigField(startDir, 'testIdAttribute', readFile, (value) => 'string' === typeof value && TESTID_ATTR_PATTERN.test(value) ? value : undefined);
110
131
  }
111
132
  /**
112
133
  * Resolve the projectId for a plugin instance, most-local statement of intent first: an explicit
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reticlehq/vite-plugin",
3
- "version": "3.7.0",
3
+ "version": "3.8.0",
4
4
  "description": "Vite plugin for Reticle: dev-only source-map stamping plus auto-injected reticle.connect(). apply:'serve' guarantees it never ships to production.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -41,15 +41,15 @@
41
41
  ],
42
42
  "dependencies": {
43
43
  "@babel/core": "^7.26.0",
44
- "@reticlehq/babel-plugin": "3.7.0",
45
- "@reticlehq/core": "3.7.0"
44
+ "@reticlehq/core": "3.8.0",
45
+ "@reticlehq/babel-plugin": "3.8.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/babel__core": "^7.20.5",
49
49
  "esbuild": "^0.28.2",
50
50
  "svelte": "^5.57.1",
51
51
  "vite": "^8",
52
- "@reticlehq/react": "3.7.0"
52
+ "@reticlehq/react": "3.8.0"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "vite": ">=4"