@jxsuite/protocol 1.2.1 → 2.1.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/README.md CHANGED
@@ -1,23 +1,23 @@
1
1
  # @jxsuite/protocol
2
2
 
3
3
  The **Studio Backend Protocol**: the wire types and canonical route table that
4
- every Jx Studio backend implements. Jx Studio is backend-agnostic — the editor
4
+ every Jx Studio backend implements. Jx Studio is backend-agnostic. The editor
5
5
  talks to whatever registered a `StudioPlatform` adapter (see
6
6
  `@jxsuite/studio/platform`), and every adapter ultimately speaks the contract
7
7
  defined here. This package is the single source of truth for that contract:
8
8
 
9
- - `@jxsuite/protocol/types` — the request/response shapes (`DirEntry`,
9
+ - `@jxsuite/protocol/types`: the request/response shapes (`DirEntry`,
10
10
  `GitStatusResult`, `StarterInfo`, `AiModelsResponse`, `CfConnection`, …).
11
- Environment-agnostic: no DOM, no node — importable in browsers, Bun, and
12
- Cloudflare Workers alike.
13
- - `@jxsuite/protocol/routes` — `STUDIO_ROUTES`, the canonical endpoint table
11
+ Environment-agnostic, with no DOM and no node, so the types are importable
12
+ in browsers, Bun, and Cloudflare Workers alike.
13
+ - `@jxsuite/protocol/routes`: `STUDIO_ROUTES`, the canonical endpoint table
14
14
  with methods, one-line contract summaries, and per-route optionality.
15
15
 
16
16
  ## Who implements this
17
17
 
18
18
  | Backend | Transport |
19
19
  | ------------------------------- | --------------------------------------------------------------------------------------------------------- |
20
- | `@jxsuite/server` (dev server) | The literal `/__studio/*` HTTP routes — the reference implementation |
20
+ | `@jxsuite/server` (dev server) | The literal `/__studio/*` HTTP routes (the reference implementation) |
21
21
  | Desktop (Electrobun / Chromium) | Typed RPC / WebSocket JSON-RPC dispatching into the same handlers |
22
22
  | Cloud platforms | HTTP behind a session gateway (e.g. `/api/v1/p/:owner/:repo/:branch/studio/*`), same sub-paths and shapes |
23
23
 
@@ -28,7 +28,7 @@ when any shape changes incompatibly.
28
28
  ## Core vs optional
29
29
 
30
30
  Optional routes back optional `StudioPlatform` members. Omitting one is not an
31
- error — Studio degrades exactly as each entry's `degradation` describes (the
31
+ error. Studio degrades exactly as each entry's `degradation` describes (the
32
32
  starter picker empties, the Projects catalogue hides, the Publish panel
33
33
  explains git-push publishing, and so on). `coreRouteNames()` /
34
34
  `optionalRouteNames()` split the table programmatically; a conformance test
@@ -51,9 +51,10 @@ doc?, options? }` through the project's format registry; without it only
51
51
  backend holds credentials (Studio then unlocks the assistant without a
52
52
  locally stored key) and `managed: true` when the platform brokers them.
53
53
  - **`cf/proxy`** is an allowlisted Cloudflare API passthrough (accounts and
54
- Pages projects/deployments only). Credentials are injected by the backend —
55
- a header-borne user token on the dev server, a vaulted OAuth token on cloud
56
- platforms — and must never be persisted by stateless implementations.
54
+ Pages projects/deployments only). Credentials are injected by the backend
55
+ and must never be persisted by stateless implementations. On the dev server
56
+ the credential is a header-borne user token, and on cloud platforms a
57
+ vaulted OAuth token.
57
58
  - **Errors** are `{ error, code?, detail? }` (`ErrorBody`) with a meaningful
58
59
  HTTP status; `code` is the machine-readable discriminator Studio switches
59
60
  on (e.g. `remote_moved`, `cf_not_connected`, `needs_installation_access`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/protocol",
3
- "version": "1.2.1",
3
+ "version": "2.1.0",
4
4
  "description": "The Studio Backend Protocol — wire types and the canonical route table every Jx Studio backend implements",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -27,6 +27,6 @@
27
27
  "test:coverage": "bun test --isolate --coverage"
28
28
  },
29
29
  "dependencies": {
30
- "@jxsuite/schema": "^1.8.1"
30
+ "@jxsuite/schema": "^2.0.0"
31
31
  }
32
32
  }
package/src/routes.ts CHANGED
@@ -107,19 +107,54 @@ export const STUDIO_ROUTES = {
107
107
  files: route(
108
108
  "GET",
109
109
  "/__studio/files",
110
- "List a directory (DirEntry[]); with ?glob=<pattern>, search matching files project-wide",
110
+ "List a directory (DirEntry[]) in stable path order; with ?glob=<pattern>, search matching " +
111
+ "files project-wide",
111
112
  ),
112
113
  fileRead: route("GET", "/__studio/file", "Read a file's text content"),
113
114
  fileWrite: route("PUT", "/__studio/file", "Write a file's text content"),
114
115
  fileDelete: route("DELETE", "/__studio/file", "Delete a file"),
115
- fileUpload: route("POST", "/__studio/file/upload", "Upload binary content to a path"),
116
- fileRename: route("POST", "/__studio/file/rename", "Rename/move (+ refactor report)"),
116
+ fileUpload: route(
117
+ "POST",
118
+ "/__studio/file/upload",
119
+ "Upload binary content to a path → UploadResult {path, size?}. `path` is the answer, not an " +
120
+ "echo: a store that de-duplicates, suffixes, or normalizes names reports what it really wrote",
121
+ ),
122
+ /**
123
+ * A project file as its own BYTES, at its own URL.
124
+ *
125
+ * `fileRead` answers `{ path, content }`, which is right for the editor and useless to the
126
+ * renderer: `@jxsuite/runtime` resolves a component `$ref` with `fetch(url).then(r => r.json())`
127
+ * and expects the DOCUMENT, and an `<img src>` cannot be a JSON envelope at all. So a backend
128
+ * whose origin does not serve the site's own URL space serves this instead, declares `assetSpace:
129
+ * "repo"`, and Studio addresses every project file under it.
130
+ *
131
+ * The path is appended to the prefix, so this is the one route whose `path` is a PREFIX rather
132
+ * than a whole path. It was a textual assertion on both sides of the cloud seam before it was a
133
+ * route; naming it here is what makes that assertion a contract.
134
+ */
135
+ documentRaw: route(
136
+ "GET",
137
+ "/__studio/raw/",
138
+ "A project file as its own bytes, at its own URL — the mount a canvas renders against when " +
139
+ "the editor origin does not serve the site's URL space. The project-relative path is " +
140
+ "appended to this prefix",
141
+ "The canvas cannot address project files by URL, so a host whose origin does not serve the " +
142
+ "site URL space renders no component $refs and no images.",
143
+ ),
144
+ fileRename: route(
145
+ "POST",
146
+ "/__studio/file/rename",
147
+ "Rename/move (+ refactor report). `from`/`to` are server-root-relative; every path in the " +
148
+ "report is relative to the active project (specs/server.md §4.1)",
149
+ ),
117
150
  references: route(
118
151
  "GET",
119
152
  "/__studio/references",
120
153
  "Where a file or a component tag is used (?path=&tag=, at least one) → ReferencesResult " +
121
154
  "{files, filesReferencing, refsTotal} — the read side of the rename refactor's own walker, " +
122
- "cached until the backend's watcher sees the tree move (backs findReferences)",
155
+ "cached until the backend's watcher sees the tree move (backs findReferences). `?path=` is " +
156
+ "server-root-relative and every path in the answer is relative to the active project; a " +
157
+ "target outside that project is a 400, never a zero-result 200 (specs/server.md §4.1)",
123
158
  'Usage counts are hidden: no "Used on N pages" in the inspector, no Selection: Find Usages, ' +
124
159
  "and delete/rename confirmations state no reference count.",
125
160
  ),
@@ -208,12 +243,49 @@ export const STUDIO_ROUTES = {
208
243
  * these paths mean the project's SOURCES, and a built page addresses its own OUTPUT by the same
209
244
  * paths (`/components/x.js` is the formula module here and the custom element there), so a
210
245
  * backend serves the built site somewhere of its own and says where.
246
+ *
247
+ * A backend that cannot run the compiler at all — a hosted one executes no project JS and has no
248
+ * bundler, image pipeline or filesystem — may instead serve the working tree as a site and let
249
+ * the runtime assemble each page in the reader's browser. `mode` reports which happened, and an
250
+ * absent `mode` means `built`, so a backend predating the field keeps its meaning.
211
251
  */
212
252
  buildSite: route(
213
253
  "POST",
214
254
  "/__studio/build",
215
- "Build the site to its output directory → {routes, files, errors, url}",
216
- "Open in Browser reports that this target cannot build a preview.",
255
+ "Build or render the site → {routes, files, errors, mode?, url}",
256
+ "Build Site reports that this target cannot build.",
257
+ ),
258
+
259
+ /**
260
+ * Preview the site LIVE at a route, with no build on the path.
261
+ *
262
+ * The sibling of `buildSite` and what `Open in Browser` reaches first. Nothing is compiled: the
263
+ * working tree is composed on demand and the runtime assembles each page in the reader's browser,
264
+ * so what opens carries edits nobody has saved (see `previewOverlay`).
265
+ *
266
+ * `reused` is the answer a caller must honour: `true` means a client already holding this
267
+ * project's preview took the route, and opening a tab anyway would leave the author with two on
268
+ * one project.
269
+ */
270
+ previewSite: route(
271
+ "POST",
272
+ "/__studio/preview",
273
+ "Preview the working tree at a route → {routes, files, errors, mode, url, reused}",
274
+ "Open in Browser falls back to buildSite.",
275
+ ),
276
+
277
+ /**
278
+ * The unsaved bytes a live preview reads before the disk.
279
+ *
280
+ * POST publishes one document's bytes, DELETE retracts one (or all, with no path). The editor
281
+ * owns the lifecycle — it knows which documents are dirty and when a save, a close or a discard
282
+ * ends that — so a backend stores what it is told and decides nothing.
283
+ */
284
+ previewOverlay: route(
285
+ "POST",
286
+ "/__studio/preview/overlay",
287
+ "Publish (POST) or retract (DELETE) a document's unsaved bytes → 204",
288
+ "A live preview shows the last saved state instead of the editor's.",
217
289
  ),
218
290
 
219
291
  // ─── Packages ─────────────────────────────────────────────────────────────
@@ -232,11 +304,11 @@ export const STUDIO_ROUTES = {
232
304
  "Whether node_modules is stale",
233
305
  "The install-on-open prompt never shows.",
234
306
  ),
235
- packagesOutdated: route(
307
+ packagesVersions: route(
236
308
  "GET",
237
- "/__studio/packages/outdated",
238
- "Dependencies with newer versions (OutdatedInfo[])",
239
- "The update affordances are hidden.",
309
+ "/__studio/packages/versions",
310
+ "Newest published version of each dependency (PackageVersionInfo[])",
311
+ "The Latest column stays empty and the update affordances are hidden.",
240
312
  ),
241
313
  packagesSetVersions: route(
242
314
  "POST",
package/src/types.ts CHANGED
@@ -68,6 +68,43 @@ export interface RenameResult {
68
68
  error?: string;
69
69
  }
70
70
 
71
+ /**
72
+ * What an upload actually stored.
73
+ *
74
+ * `path` is the ANSWER, not an echo. A backend may store the bytes somewhere other than the path
75
+ * asked for — a store that de-duplicates by content hash, one that appends a collision suffix, one
76
+ * that normalizes a name — and the client has to write a reference to what was really written. The
77
+ * upload response used to be untyped (`Promise<unknown>`), and every caller ignored it and used the
78
+ * requested path, so any such backend produced a document pointing at a file that is not there.
79
+ *
80
+ * `size` is what the backend recorded, when it says: the client already knows the bytes it sent, so
81
+ * a differing value is the backend telling you it transformed them.
82
+ */
83
+ export interface UploadResult {
84
+ /** Project-relative path the bytes were stored at. */
85
+ path: string;
86
+ /** Stored size in bytes, when the backend reports one. */
87
+ size?: number;
88
+ }
89
+
90
+ /**
91
+ * What a backend will accept as an upload, when it says.
92
+ *
93
+ * Every field is optional and absence means "no declared limit" — a client must not invent one,
94
+ * because a limit it made up is a file the user cannot upload for no reason it can name. A DECLARED
95
+ * limit is different: it lets the client refuse before spending the round trip, and name the number
96
+ * in the refusal.
97
+ */
98
+ export interface AssetCapabilities {
99
+ /** Largest single upload the backend accepts, in bytes. */
100
+ maxUploadBytes?: number;
101
+ /**
102
+ * Media types and extensions the backend accepts, in `<input accept>` syntax (`"image/*,.pdf"`).
103
+ * Narrows the client's own default; it never widens it.
104
+ */
105
+ accept?: string;
106
+ }
107
+
71
108
  /** One kind of reference in one file, with how many times it occurs there. */
72
109
  export interface ReferenceHit {
73
110
  /**
@@ -389,6 +426,24 @@ export interface SecretsSetResponse {
389
426
  names: string[];
390
427
  }
391
428
 
429
+ // ─── User settings (desktop launchers; specs/desktop.md §3.1) ───────────────
430
+
431
+ /**
432
+ * A set of changes to the user-level settings store.
433
+ *
434
+ * A patch rather than the whole map, and for the same reason {@link SecretsSetRequest} is one: a key
435
+ * named by neither `set` nor `remove` must be left exactly as it was found. Writing the whole map
436
+ * let any window overwrite the shared store with its own view of it — and on the chromium launcher
437
+ * every window is a separate process with a separate browser profile, so a window holding no
438
+ * settings could clear the credentials another had just stored.
439
+ */
440
+ export interface SettingsPatch {
441
+ /** Keys to store, with their new values. */
442
+ set?: Record<string, string>;
443
+ /** Keys to forget. */
444
+ remove?: string[];
445
+ }
446
+
392
447
  // ─── Packages ────────────────────────────────────────────────────────────────
393
448
 
394
449
  export interface PackageInfo {
@@ -398,15 +453,20 @@ export interface PackageInfo {
398
453
  dev?: boolean;
399
454
  }
400
455
 
401
- /** A dependency with a newer version available, as reported by `bun outdated` / the npm registry. */
402
- export interface OutdatedInfo {
456
+ /**
457
+ * A dependency's pinned range beside the newest version published for it on the npm registry.
458
+ *
459
+ * Reported for EVERY dependency whose spec is a registry range, whether or not it is behind — "is
460
+ * this outdated?" is a comparison the caller makes, not a filter the backend applies. That matters
461
+ * for the Packages table, which shows a Latest column for every row: a backend that only answered
462
+ * about outdated packages could describe the up-to-date ones only as a blank.
463
+ */
464
+ export interface PackageVersionInfo {
403
465
  name: string;
404
466
  /** The version range pinned in package.json (e.g. "^0.19.0"). */
405
467
  current: string;
406
- /** The newest published version. */
468
+ /** The newest version published for this package on the registry. */
407
469
  latest: string;
408
- /** The newest version satisfying the current range, if known. */
409
- wanted?: string;
410
470
  dev?: boolean;
411
471
  }
412
472
 
@@ -476,6 +536,40 @@ export interface ImportProgressEvent {
476
536
  total?: number;
477
537
  }
478
538
 
539
+ /**
540
+ * How many of a site's declared breakpoints the imported project keeps.
541
+ *
542
+ * A real site declares as many as it has accumulated frameworks — nine is ordinary — and each one
543
+ * becomes a canvas size in Studio and a column in every style editor. `limit` is the default
544
+ * because that is the complaint it answers; `explicit` is for an author who already knows the
545
+ * widths their project should have. `rounding` decides which DECLARED width backs a kept one, since
546
+ * the styles flip where the site says they do, not where the author wishes they did.
547
+ */
548
+ export type BreakpointRounding = "nearest" | "down" | "up";
549
+
550
+ export interface ImportBreakpointPolicy {
551
+ mode: "all" | "limit" | "explicit";
552
+ /** `limit` only — how many to keep, evenly spaced across the declared range. */
553
+ count?: number;
554
+ /** `explicit` only — the widths the project should have, in CSS pixels. */
555
+ widths?: number[];
556
+ /** `limit` and `explicit` — how a kept width matches a declared one. */
557
+ rounding?: BreakpointRounding;
558
+ }
559
+
560
+ /**
561
+ * The line the import stream sends the moment the destination exists and holds a `project.json`.
562
+ *
563
+ * It is what makes an import watchable. The emit phase rewrites that file completely, so nothing
564
+ * here is a guess about the result — it says the directory is now a PROJECT, so the caller can open
565
+ * it and see the pages, components and assets arrive in its own file tree over the several minutes
566
+ * a crawl takes, rather than facing a welcome screen until the run ends.
567
+ */
568
+ export interface ImportReadyEvent {
569
+ /** The project root, in the platform's own form. */
570
+ root: string;
571
+ }
572
+
479
573
  /** Options for the import-site pipeline (StudioPlatform.importSite). */
480
574
  export interface ImportSiteOptions {
481
575
  /** The live site to clone; must be http(s). */
@@ -488,14 +582,89 @@ export interface ImportSiteOptions {
488
582
  depth: number;
489
583
  /** Max pages to capture. */
490
584
  maxPages: number;
585
+ /** Which of the site's declared breakpoints the project keeps. Omitted means the default limit. */
586
+ breakpoints?: ImportBreakpointPolicy;
491
587
  /** Refine component/prop names with the LLM (requires a key). */
492
588
  aiComponents: boolean;
589
+ /**
590
+ * Build the emitted project and screenshot-diff every page against the original.
591
+ *
592
+ * Off by default: it runs a full compile and a second browser pass, so it roughly doubles the
593
+ * run. What it buys is the one number the assistant can act on — a page at 61% fidelity is a
594
+ * question worth asking, and every other finding is a count of things that were skipped.
595
+ */
596
+ verify?: boolean;
597
+ /**
598
+ * Pixelmatch's per-pixel COLOUR tolerance, 0..1, for `verify` (default 0.15).
599
+ *
600
+ * It decides when two pixels count as the same colour, so it moves the score and never the
601
+ * outcome. `verifyMinFidelity` is the bar.
602
+ */
603
+ verifyThreshold?: number;
604
+ /**
605
+ * The average fidelity, 0..100, this run has to reach for `verify` to report `passed`.
606
+ *
607
+ * A finding rather than a failure on this transport: the project is written and opened either
608
+ * way, and the summary says the bar was missed. `jx-import` uses the same number to decide an
609
+ * exit code, because a script in a pipeline has no reader to tell.
610
+ */
611
+ verifyMinFidelity?: number;
493
612
  /** OpenAI-compatible credentials, from the user's AI settings. */
494
613
  apiKey?: string;
495
614
  baseUrl?: string;
496
615
  model?: string;
497
616
  }
498
617
 
618
+ /** Per-page fidelity from the `verify` pass. */
619
+ export interface ImportVerifyPage {
620
+ route: string;
621
+ /** 0..100 — the percentage of pixels that matched the original. */
622
+ fidelity: number;
623
+ /**
624
+ * Console errors and uncaught exceptions the rendered page produced.
625
+ *
626
+ * What a percentage cannot say. A page that 404s on fifteen images scores badly, and only this
627
+ * and `failedRequests` name the reason (issue #232).
628
+ */
629
+ consoleErrors?: number;
630
+ /** Requests the rendered page made that failed or answered 4xx/5xx. */
631
+ failedRequests?: number;
632
+ /** Why this page has no score at all — it could not be rendered. */
633
+ error?: string;
634
+ }
635
+
636
+ /**
637
+ * What the run produced, carried on the import stream's terminal `done` line.
638
+ *
639
+ * It exists because the pipeline computed all of this and the HTTP layer threw it away: the `done`
640
+ * line carried `{root, config}` and nothing else, so the caller could report that an import had
641
+ * happened and nothing whatever about what it found. Every field is optional — a backend that does
642
+ * not send one is not broken, and an older client ignores them.
643
+ */
644
+ export interface ImportSiteSummary {
645
+ pages?: { route: string; title: string; nodeCount: number }[];
646
+ fileCount?: number;
647
+ /** Soft failures the pipeline recorded: assets that would not download, pages it skipped. */
648
+ warnings?: string[];
649
+ /** Present only when `verify` was requested and reference screenshots were captured. */
650
+ verify?: {
651
+ averageFidelity: number;
652
+ reportDir: string;
653
+ pages?: ImportVerifyPage[];
654
+ /**
655
+ * Whether the run met its bar: the project built cleanly, every page rendered, and the average
656
+ * fidelity reached `minFidelity`. Over HTTP that bar is 0 by default, so `passed: false` from
657
+ * this backend means the project did not build or a page did not render — not that the clone
658
+ * merely scored low. The `jx-import` CLI sets a real floor and exits non-zero.
659
+ */
660
+ passed?: boolean;
661
+ /** The bar `passed` was measured against, 0..100. */
662
+ minFidelity?: number;
663
+ /** Errors the compiler reported building the emitted project. */
664
+ buildErrors?: string[];
665
+ };
666
+ }
667
+
499
668
  // ─── AI proxy ────────────────────────────────────────────────────────────────
500
669
 
501
670
  /** One entry of the AI proxy's model catalogue. */