@jxsuite/protocol 0.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 ADDED
@@ -0,0 +1,65 @@
1
+ # @jxsuite/protocol
2
+
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
5
+ talks to whatever registered a `StudioPlatform` adapter (see
6
+ `@jxsuite/studio/platform`), and every adapter ultimately speaks the contract
7
+ defined here. This package is the single source of truth for that contract:
8
+
9
+ - `@jxsuite/protocol/types` — the request/response shapes (`DirEntry`,
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
14
+ with methods, one-line contract summaries, and per-route optionality.
15
+
16
+ ## Who implements this
17
+
18
+ | Backend | Transport |
19
+ | ------------------------------- | --------------------------------------------------------------------------------------------------------- |
20
+ | `@jxsuite/server` (dev server) | The literal `/__studio/*` HTTP routes — the reference implementation |
21
+ | Desktop (Electrobun / Chromium) | Typed RPC / WebSocket JSON-RPC dispatching into the same handlers |
22
+ | Cloud platforms | HTTP behind a session gateway (e.g. `/api/v1/p/:owner/:repo/:branch/studio/*`), same sub-paths and shapes |
23
+
24
+ A backend is free to change the transport and path prefix; the **sub-path,
25
+ method, and body shapes are the contract**. `STUDIO_PROTOCOL_VERSION` bumps
26
+ when any shape changes incompatibly.
27
+
28
+ ## Core vs optional
29
+
30
+ Optional routes back optional `StudioPlatform` members. Omitting one is not an
31
+ error — Studio degrades exactly as each entry's `degradation` describes (the
32
+ starter picker empties, the Projects catalogue hides, the Publish panel
33
+ explains git-push publishing, and so on). `coreRouteNames()` /
34
+ `optionalRouteNames()` split the table programmatically; a conformance test
35
+ for a new backend can iterate `STUDIO_ROUTES` and assert every core route is
36
+ served.
37
+
38
+ Highlights an implementer should know:
39
+
40
+ - **`git/commit`** commits the staged files if any are staged, otherwise all
41
+ dirty files. Cloud backends may make commit+push atomic and treat
42
+ `git/push` as a sync check (`ahead` stays 0).
43
+ - **`git/pull`** returns `409 { code: "pull_conflict", conflicts: [paths] }`
44
+ when local dirty files overlap remote changes.
45
+ - **`format`** dispatches `{ format, action: "parse" | "serialize", source? |
46
+ doc?, options? }` through the project's format registry; without it only
47
+ `.json` documents open.
48
+ - **`ai/chat`** accepts `{ messages, tools, systemPrompt, model }` and streams
49
+ the normalized `StreamEvent` SSE defined by `@jxsuite/ai/streaming-client`.
50
+ `ai/models` returns `AiModelsResponse`; report `configured: true` when the
51
+ backend holds credentials (Studio then unlocks the assistant without a
52
+ locally stored key) and `managed: true` when the platform brokers them.
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.
57
+ - **Errors** are `{ error, code?, detail? }` (`ErrorBody`) with a meaningful
58
+ HTTP status; `code` is the machine-readable discriminator Studio switches
59
+ on (e.g. `remote_moved`, `cf_not_connected`, `needs_installation_access`).
60
+
61
+ ## Versioning
62
+
63
+ The types are published as TypeScript source (like every `@jxsuite` package)
64
+ and follow the monorepo's release train. Within the monorepo, packages depend
65
+ on it via `workspace:^`.
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@jxsuite/protocol",
3
+ "version": "0.1.0",
4
+ "description": "The Studio Backend Protocol — wire types and the canonical route table every Jx Studio backend implements",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/jxsuite/jx.git",
9
+ "directory": "packages/protocol"
10
+ },
11
+ "files": [
12
+ "src/"
13
+ ],
14
+ "type": "module",
15
+ "exports": {
16
+ ".": "./src/index.ts",
17
+ "./types": "./src/types.ts",
18
+ "./routes": "./src/routes.ts"
19
+ },
20
+ "publishConfig": {
21
+ "provenance": false
22
+ },
23
+ "scripts": {
24
+ "test": "bun test --isolate",
25
+ "test:coverage": "bun test --isolate --coverage"
26
+ },
27
+ "dependencies": {
28
+ "@jxsuite/schema": "workspace:^"
29
+ }
30
+ }
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The Studio Backend Protocol package: wire types plus the canonical route
3
+ * table every Jx Studio backend implements. See README.md for the implementer
4
+ * guide.
5
+ *
6
+ * @license MIT
7
+ */
8
+
9
+ export * from "./routes";
10
+ export type * from "./types";
package/src/routes.ts ADDED
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Studio Backend Protocol — the canonical route table. The dev server
3
+ * (@jxsuite/server) is the reference implementation; every other backend
4
+ * (desktop RPC bridge, cloud platforms) serves the same shapes, either at
5
+ * these literal paths or through an equivalent transport. Optional routes
6
+ * back optional StudioPlatform members — Studio degrades without them, as
7
+ * described by each entry's `degradation`.
8
+ *
9
+ * @license MIT
10
+ */
11
+
12
+ /** Bumped when a route's request/response shape changes incompatibly. */
13
+ export const STUDIO_PROTOCOL_VERSION = 1;
14
+
15
+ export type StudioRouteMethod = "GET" | "POST" | "PUT" | "DELETE";
16
+
17
+ export interface StudioRoute {
18
+ path: string;
19
+ method: StudioRouteMethod;
20
+ /** True when a backend may omit the route (its PAL member is optional). */
21
+ optional: boolean;
22
+ /** One-line contract summary. */
23
+ summary: string;
24
+ /** What Studio does when an optional route is absent. */
25
+ degradation?: string;
26
+ }
27
+
28
+ const route = (
29
+ method: StudioRouteMethod,
30
+ path: string,
31
+ summary: string,
32
+ degradation?: string,
33
+ ): StudioRoute => ({
34
+ method,
35
+ path,
36
+ optional: degradation !== undefined,
37
+ summary,
38
+ ...(degradation === undefined ? {} : { degradation }),
39
+ });
40
+
41
+ /**
42
+ * Every protocol route, keyed by a stable name. Paths are the dev server's literal `/__studio/*`
43
+ * endpoints; transport-mapped backends (RPC, gateway prefixes) preserve the sub-path and shapes.
44
+ */
45
+ export const STUDIO_ROUTES = {
46
+ // ─── Session / project ────────────────────────────────────────────────────
47
+ activate: route("POST", "/__studio/activate", "Bind the backend to a project root"),
48
+ project: route("GET", "/__studio/project", "Root project metadata {name, root}"),
49
+ projectInfo: route(
50
+ "GET",
51
+ "/__studio/project-info",
52
+ "Probe a directory: {isSiteProject, projectConfig?, directories?}",
53
+ ),
54
+ resolveSite: route(
55
+ "GET",
56
+ "/__studio/resolve-site",
57
+ "Resolve the owning site of a file path {sitePath, projectConfig?, fileRelPath?}",
58
+ ),
59
+ sites: route(
60
+ "GET",
61
+ "/__studio/sites",
62
+ "Enumerate site projects [{config, path}] (backs listProjects)",
63
+ "The welcome screen's Projects catalogue stays hidden.",
64
+ ),
65
+ findProject: route(
66
+ "GET",
67
+ "/__studio/find-project",
68
+ "Locate a project directory by name outside the root",
69
+ "openProject falls back to config-matching only.",
70
+ ),
71
+ createProject: route("POST", "/__studio/create-project", "Scaffold a project → {root, config}"),
72
+ starters: route(
73
+ "GET",
74
+ "/__studio/starters",
75
+ "Starter templates (StarterInfo[])",
76
+ "The New Project picker offers only blank/templates.",
77
+ ),
78
+
79
+ // ─── Filesystem ───────────────────────────────────────────────────────────
80
+ files: route("GET", "/__studio/files", "List a directory (DirEntry[])"),
81
+ fileRead: route("GET", "/__studio/file", "Read a file's text content"),
82
+ fileWrite: route("PUT", "/__studio/file", "Write a file's text content"),
83
+ fileDelete: route("DELETE", "/__studio/file", "Delete a file"),
84
+ fileUpload: route("POST", "/__studio/file/upload", "Upload binary content to a path"),
85
+ fileRename: route("POST", "/__studio/file/rename", "Rename/move (+ refactor report)"),
86
+ locate: route("POST", "/__studio/locate", "Find a file by name → {path | null}"),
87
+
88
+ // ─── Documents / components / formats ─────────────────────────────────────
89
+ components: route("GET", "/__studio/components", "Discover components (ComponentMeta[])"),
90
+ cem: route(
91
+ "GET",
92
+ "/__studio/cem",
93
+ "Custom-elements manifest of an npm dependency",
94
+ "Dependency components lose prop/slot metadata.",
95
+ ),
96
+ formats: route(
97
+ "GET",
98
+ "/__studio/formats",
99
+ "The project's format registry entries",
100
+ "Only .json documents open (backs listFormats).",
101
+ ),
102
+ format: route(
103
+ "POST",
104
+ "/__studio/format",
105
+ "Dispatch a format capability {format, action: parse|serialize, source?|doc?}",
106
+ "Non-JSON documents cannot be parsed/serialized (backs formatAction).",
107
+ ),
108
+ pluginSchema: route(
109
+ "GET",
110
+ "/__studio/plugin-schema",
111
+ "Extract a $studio schema from a class source",
112
+ "Plugin property panels fall back to generic JSON editing.",
113
+ ),
114
+
115
+ // ─── Packages ─────────────────────────────────────────────────────────────
116
+ packages: route("GET", "/__studio/packages", "List dependencies (PackageInfo[])"),
117
+ packagesAdd: route("POST", "/__studio/packages/add", "Add a dependency"),
118
+ packagesRemove: route("POST", "/__studio/packages/remove", "Remove a dependency"),
119
+ packagesInstall: route(
120
+ "POST",
121
+ "/__studio/packages/install",
122
+ "Run the package manager install",
123
+ "Install/reinstall affordances are hidden; manifest-only edits still work.",
124
+ ),
125
+ packagesNeedsInstall: route(
126
+ "GET",
127
+ "/__studio/packages/needs-install",
128
+ "Whether node_modules is stale",
129
+ "The install-on-open prompt never shows.",
130
+ ),
131
+ packagesOutdated: route(
132
+ "GET",
133
+ "/__studio/packages/outdated",
134
+ "Dependencies with newer versions (OutdatedInfo[])",
135
+ "The update affordances are hidden.",
136
+ ),
137
+ packagesSetVersions: route(
138
+ "POST",
139
+ "/__studio/packages/set-versions",
140
+ "Rewrite dependency ranges and install",
141
+ "Bulk version updates are hidden.",
142
+ ),
143
+
144
+ // ─── Git ──────────────────────────────────────────────────────────────────
145
+ gitStatus: route("GET", "/__studio/git/status", "Working-tree status (GitStatusResult)"),
146
+ gitBranches: route("GET", "/__studio/git/branches", "Branch list (GitBranchesResult)"),
147
+ gitLog: route("GET", "/__studio/git/log", "Recent commits (GitLogEntry[])"),
148
+ gitStage: route("POST", "/__studio/git/stage", "Stage files"),
149
+ gitUnstage: route("POST", "/__studio/git/unstage", "Unstage files"),
150
+ gitCommit: route("POST", "/__studio/git/commit", "Commit staged (else all dirty) files"),
151
+ gitPush: route("POST", "/__studio/git/push", "Push (cloud: sync check — commits land on push)"),
152
+ gitPull: route("POST", "/__studio/git/pull", "Pull/fast-forward; 409 {conflicts} on overlap"),
153
+ gitFetch: route("POST", "/__studio/git/fetch", "Refresh remote tracking state"),
154
+ gitCheckout: route("POST", "/__studio/git/checkout", "Switch branches"),
155
+ gitCreateBranch: route("POST", "/__studio/git/create-branch", "Create a branch"),
156
+ gitDiff: route("GET", "/__studio/git/diff", "Unified diff of dirty files (or one path)"),
157
+ gitShow: route("GET", "/__studio/git/show", "File content at a ref"),
158
+ gitDiscard: route("POST", "/__studio/git/discard", "Discard working changes"),
159
+ gitInit: route("POST", "/__studio/git/init", "Initialize a repository"),
160
+ gitAddRemote: route("POST", "/__studio/git/add-remote", "Add a remote"),
161
+ gitClone: route(
162
+ "POST",
163
+ "/__studio/git/clone",
164
+ "Clone a repository",
165
+ "The welcome screen hides Clone Git Repository.",
166
+ ),
167
+ gitPr: route(
168
+ "POST",
169
+ "/__studio/git/pr",
170
+ "Open a pull request → PullRequestInfo",
171
+ "Studio falls back to a direct GitHub API call with the user's token.",
172
+ ),
173
+
174
+ // ─── AI proxy ─────────────────────────────────────────────────────────────
175
+ aiChat: route(
176
+ "POST",
177
+ "/__studio/ai/chat",
178
+ "StreamEvent SSE chat proxy {messages, tools, systemPrompt, model}",
179
+ ),
180
+ aiModels: route("GET", "/__studio/ai/models", "Model catalogue (AiModelsResponse)"),
181
+
182
+ // ─── Cloudflare publish surface ───────────────────────────────────────────
183
+ cfProxy: route(
184
+ "POST",
185
+ "/__studio/cf/proxy",
186
+ "Allowlisted Cloudflare API passthrough {path, method?, body?} (backs cfApi)",
187
+ "The Publish panel explains the git-push publishing path instead.",
188
+ ),
189
+ } as const satisfies Record<string, StudioRoute>;
190
+
191
+ /** A stable route name in {@link STUDIO_ROUTES}. */
192
+ export type StudioRouteName = keyof typeof STUDIO_ROUTES;
193
+
194
+ /** Names of every route a minimal (core) backend must implement. */
195
+ export function coreRouteNames(): StudioRouteName[] {
196
+ return (Object.keys(STUDIO_ROUTES) as StudioRouteName[]).filter(
197
+ (name) => !STUDIO_ROUTES[name].optional,
198
+ );
199
+ }
200
+
201
+ /** Names of the optional routes (each backs an optional StudioPlatform member). */
202
+ export function optionalRouteNames(): StudioRouteName[] {
203
+ return (Object.keys(STUDIO_ROUTES) as StudioRouteName[]).filter(
204
+ (name) => STUDIO_ROUTES[name].optional,
205
+ );
206
+ }
package/src/types.ts ADDED
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Studio Backend Protocol — wire types. Every shape a Studio backend serves
3
+ * (or a `StudioPlatform` adapter consumes) lives here so client adapters and
4
+ * server implementations (the dev server, the desktop RPC bridge, cloud
5
+ * platforms) share one contract. Environment-agnostic: no DOM, no node —
6
+ * importable in browsers, Bun, and Cloudflare Workers alike.
7
+ *
8
+ * @license MIT
9
+ */
10
+
11
+ import type { JsonValue as SchemaJsonValue, JxMutableNode } from "@jxsuite/schema/types";
12
+
13
+ /**
14
+ * A JSON document value, or `undefined` to signal property removal in the mutators. Re-uses the
15
+ * schema's precise recursive JSON model.
16
+ */
17
+ export type JsonValue = SchemaJsonValue | undefined;
18
+
19
+ // ─── Errors ──────────────────────────────────────────────────────────────────
20
+
21
+ /** Message-level failure body every protocol route may return. */
22
+ export interface ErrorBody {
23
+ error: string;
24
+ /** Machine-readable discriminator (e.g. "remote_moved", "cf_not_connected"). */
25
+ code?: string;
26
+ detail?: unknown;
27
+ }
28
+
29
+ // ─── Filesystem ──────────────────────────────────────────────────────────────
30
+
31
+ export interface DirEntry {
32
+ name: string;
33
+ path: string;
34
+ type: "file" | "directory";
35
+ size?: number;
36
+ modified?: string;
37
+ }
38
+
39
+ /** A filesystem change pushed from the backend (project-relative, forward-slashed path). */
40
+ export interface FsEvent {
41
+ type: "add" | "change" | "unlink" | "addDir" | "unlinkDir";
42
+ path: string;
43
+ isDir: boolean;
44
+ }
45
+
46
+ /** Result of a rename, including the references rewritten across the project (refactor report). */
47
+ export interface RenameResult {
48
+ ok: boolean;
49
+ from: string;
50
+ to: string;
51
+ isDir?: boolean;
52
+ references?: {
53
+ filesChanged: number;
54
+ refsUpdated: number;
55
+ files: { path: string; count: number }[];
56
+ };
57
+ errors?: { path: string; error: string }[];
58
+ tag?: { from: string; to: string; filesChanged: number; refsUpdated: number };
59
+ tagSkipped?: string;
60
+ error?: string;
61
+ }
62
+
63
+ // ─── Git ─────────────────────────────────────────────────────────────────────
64
+
65
+ export interface GitFileStatus {
66
+ status: string;
67
+ path: string;
68
+ staged?: boolean;
69
+ }
70
+
71
+ export interface GitStatusResult {
72
+ branch: string;
73
+ files: GitFileStatus[];
74
+ ahead: number;
75
+ behind: number;
76
+ isRepo: boolean;
77
+ remotes: string[];
78
+ }
79
+
80
+ export interface GitBranchesResult {
81
+ current: string;
82
+ branches: string[];
83
+ }
84
+
85
+ export interface GitLogEntry {
86
+ hash: string;
87
+ message: string;
88
+ author: string;
89
+ date: string;
90
+ }
91
+
92
+ /** Response of the pull-request route (see StudioPlatform.createPullRequest). */
93
+ export interface PullRequestInfo {
94
+ url: string;
95
+ number: number;
96
+ }
97
+
98
+ // ─── Components ──────────────────────────────────────────────────────────────
99
+
100
+ export interface ComponentSlotMeta {
101
+ name: string;
102
+ description?: string;
103
+ fallback?: (JxMutableNode | string)[];
104
+ }
105
+
106
+ export interface ComponentMeta {
107
+ tagName: string;
108
+ $id?: string | null;
109
+ path: string;
110
+ props?: { name: string; type?: string; default?: JsonValue; [k: string]: unknown }[];
111
+ slots?: ComponentSlotMeta[];
112
+ hasElements?: boolean;
113
+ }
114
+
115
+ // ─── Packages ────────────────────────────────────────────────────────────────
116
+
117
+ export interface PackageInfo {
118
+ name: string;
119
+ version: string;
120
+ /** True when the dependency lives in `devDependencies` rather than `dependencies`. */
121
+ dev?: boolean;
122
+ }
123
+
124
+ /** A dependency with a newer version available, as reported by `bun outdated` / the npm registry. */
125
+ export interface OutdatedInfo {
126
+ name: string;
127
+ /** The version range pinned in package.json (e.g. "^0.19.0"). */
128
+ current: string;
129
+ /** The newest published version. */
130
+ latest: string;
131
+ /** The newest version satisfying the current range, if known. */
132
+ wanted?: string;
133
+ dev?: boolean;
134
+ }
135
+
136
+ /** Result of a package mutation that runs `bun install` (install / set-versions). */
137
+ export interface PackageOpResult {
138
+ ok: boolean;
139
+ /** Combined stdout/stderr from the bun invocation, surfaced to the user on failure. */
140
+ log?: string;
141
+ }
142
+
143
+ // ─── App / code services ─────────────────────────────────────────────────────
144
+
145
+ /** Desktop app/build info surfaced in the About screen. */
146
+ export interface AppInfo {
147
+ version: string;
148
+ channel: string;
149
+ hash: string;
150
+ /** Human-readable update status (e.g. "Up to date", "Update available"), if known. */
151
+ updateStatus?: string;
152
+ }
153
+
154
+ export interface CodeServiceResult {
155
+ code?: string;
156
+ diagnostics?: unknown[];
157
+ [key: string]: unknown;
158
+ }
159
+
160
+ // ─── Projects & starters ─────────────────────────────────────────────────────
161
+
162
+ /** A starter template surfaced in the New Project picker (mirrors @jxsuite/starters StarterMeta). */
163
+ export interface StarterInfo {
164
+ id: string;
165
+ name: string;
166
+ industry: string;
167
+ tagline: string;
168
+ description: string;
169
+ features: string[];
170
+ accent: string;
171
+ /** Preview image as a self-contained `data:` URI. */
172
+ thumbnail: string;
173
+ }
174
+
175
+ /** A recently-opened project, keyed by its re-openable `root` (platform-specific). */
176
+ export interface RecentProjectEntry {
177
+ name: string;
178
+ root: string;
179
+ timestamp: number;
180
+ }
181
+
182
+ /** One entry in the platform's project catalogue (see StudioPlatform.listProjects). */
183
+ export interface ProjectListEntry {
184
+ /** Display name (project.json name, repository name, ...). */
185
+ name: string;
186
+ /** Re-openable root key (server-relative path, owner/repo, absolute path). */
187
+ root: string;
188
+ /** Optional one-line descriptor shown under the name (path, permission, ...). */
189
+ description?: string | undefined;
190
+ }
191
+
192
+ // ─── Site import ─────────────────────────────────────────────────────────────
193
+
194
+ /** A progress line from the AI-guided site import (mirrors @jxsuite/import ImportProgressEvent). */
195
+ export interface ImportProgressEvent {
196
+ phase: string;
197
+ message: string;
198
+ current?: number;
199
+ total?: number;
200
+ }
201
+
202
+ /** Options for the import-site pipeline (StudioPlatform.importSite). */
203
+ export interface ImportSiteOptions {
204
+ /** The live site to clone; must be http(s). */
205
+ url: string;
206
+ /** Display name for the new project. */
207
+ name: string;
208
+ /** Destination directory (platform-interpreted: project-relative on the dev server). */
209
+ directory: string;
210
+ /** Max crawl depth; 0 = single page. */
211
+ depth: number;
212
+ /** Max pages to capture. */
213
+ maxPages: number;
214
+ /** Refine component/prop names with the LLM (requires a key). */
215
+ aiComponents: boolean;
216
+ /** OpenAI-compatible credentials, from the user's AI settings. */
217
+ apiKey?: string;
218
+ baseUrl?: string;
219
+ model?: string;
220
+ }
221
+
222
+ // ─── AI proxy ────────────────────────────────────────────────────────────────
223
+
224
+ /** One entry of the AI proxy's model catalogue. */
225
+ export interface AiModelInfo {
226
+ id: string;
227
+ name?: string;
228
+ contextWindow?: number;
229
+ /** Whether the model supports tool/function calling (agentic editing). */
230
+ toolSupport?: boolean;
231
+ }
232
+
233
+ /** Response of the AI proxy's models route (the chat route's sibling). */
234
+ export interface AiModelsResponse {
235
+ models: AiModelInfo[];
236
+ /** True when the backend holds working credentials (env key, managed platform). */
237
+ configured: boolean;
238
+ /** True when the platform manages AI credentials itself (e.g. cloud Workers AI). */
239
+ managed?: boolean;
240
+ /** The backend's preferred model id, when it declares one. */
241
+ defaultModel?: string;
242
+ /** Set when the upstream provider was unreachable and defaults were returned. */
243
+ upstreamError?: number | string;
244
+ }
245
+
246
+ // ─── Cloudflare publish surface ──────────────────────────────────────────────
247
+
248
+ /** Cloudflare connection state (see StudioPlatform.cfConnection). */
249
+ export interface CfConnection {
250
+ connected: boolean;
251
+ accountId?: string | undefined;
252
+ accountName?: string | undefined;
253
+ }