@jxsuite/studio 0.35.0 → 0.36.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.
@@ -11,6 +11,7 @@
11
11
  * @license MIT
12
12
  */
13
13
 
14
+ import type { AiModelsResponse } from "@jxsuite/protocol";
14
15
  import { getPlatform } from "../platform";
15
16
  import { getBaseUrl, getOpenAiKey } from "./ai-settings";
16
17
 
@@ -20,10 +21,35 @@ export interface AiModel {
20
21
  }
21
22
 
22
23
  let cache: AiModel[] | null = null;
24
+ let proxyConfigured = false;
25
+ let proxyManaged = false;
26
+ let proxyDefaultModel = "";
23
27
 
24
28
  /** Drop the cached model list (call after credentials/endpoint changes). */
25
29
  export function invalidateModelCache() {
26
30
  cache = null;
31
+ proxyConfigured = false;
32
+ proxyManaged = false;
33
+ proxyDefaultModel = "";
34
+ }
35
+
36
+ /**
37
+ * Whether the proxy reported itself configured on the last fetch — true when the backend holds
38
+ * credentials (managed platforms, OPENAI_API_KEY env), so the assistant works without a locally
39
+ * stored key.
40
+ */
41
+ export function isProxyConfigured(): boolean {
42
+ return proxyConfigured;
43
+ }
44
+
45
+ /** Whether the platform manages AI credentials itself (cloud Workers AI). */
46
+ export function isManagedProxy(): boolean {
47
+ return proxyManaged;
48
+ }
49
+
50
+ /** The proxy's preferred model id ("" when it does not declare one). */
51
+ export function getProxyDefaultModel(): string {
52
+ return proxyDefaultModel;
27
53
  }
28
54
 
29
55
  /**
@@ -58,7 +84,10 @@ export async function fetchAvailableModels(
58
84
  if (!resp.ok) {
59
85
  throw new Error(`HTTP ${resp.status}`);
60
86
  }
61
- const data = (await resp.json()) as { models?: { id: string; name?: string }[] };
87
+ const data = (await resp.json()) as Partial<AiModelsResponse>;
88
+ proxyConfigured = data.configured === true;
89
+ proxyManaged = data.managed === true;
90
+ proxyDefaultModel = data.defaultModel ?? "";
62
91
  cache = (data.models || []).map((m) => ({ id: m.id, name: m.name || m.id }));
63
92
  return cache;
64
93
  }
@@ -0,0 +1,58 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Cf-settings — local persistence for the Cloudflare publish connection on
4
+ * platforms without a hosted OAuth broker (dev server, desktop). The API
5
+ * token is stored like the AI key: localStorage first, mirrored through the
6
+ * platform settings backend where one exists. It only ever leaves the machine
7
+ * to the same-origin `/__studio/cf/proxy`, which forwards it as a bearer to
8
+ * api.cloudflare.com (not CORS-enabled, hence the proxy).
9
+ *
10
+ * @license MIT
11
+ */
12
+
13
+ import { persistSettings } from "./settings-store";
14
+
15
+ const TOKEN_STORAGE = "jx.cf.token";
16
+ const ACCOUNT_STORAGE = "jx.cf.accountId";
17
+
18
+ function read(key: string): string {
19
+ try {
20
+ return globalThis.localStorage?.getItem(key) ?? "";
21
+ } catch {
22
+ return "";
23
+ }
24
+ }
25
+
26
+ function write(key: string, value: string): void {
27
+ try {
28
+ const trimmed = (value || "").trim();
29
+ if (trimmed) {
30
+ globalThis.localStorage?.setItem(key, trimmed);
31
+ } else {
32
+ globalThis.localStorage?.removeItem(key);
33
+ }
34
+ } catch {
35
+ // Storage unavailable (private mode) — the connection just won't persist.
36
+ }
37
+ persistSettings();
38
+ }
39
+
40
+ /** The stored Cloudflare API token, or "" when not connected. */
41
+ export function getCfToken(): string {
42
+ return read(TOKEN_STORAGE);
43
+ }
44
+
45
+ /** Persist (or clear, with a blank value) the Cloudflare API token. */
46
+ export function setCfToken(token: string): void {
47
+ write(TOKEN_STORAGE, token);
48
+ }
49
+
50
+ /** The selected Cloudflare account id, or "" when none chosen yet. */
51
+ export function getCfAccountId(): string {
52
+ return read(ACCOUNT_STORAGE);
53
+ }
54
+
55
+ /** Persist (or clear) the selected Cloudflare account id. */
56
+ export function setCfAccountId(accountId: string): void {
57
+ write(ACCOUNT_STORAGE, accountId);
58
+ }
@@ -14,7 +14,13 @@
14
14
  import { getPlatform, hasPlatform } from "../platform";
15
15
 
16
16
  /** The localStorage keys mirrored into the platform's user-settings store. */
17
- export const PERSISTED_SETTINGS_KEYS = ["jx.ai.openaiKey", "jx.ai.baseUrl", "jx.ai.model"] as const;
17
+ export const PERSISTED_SETTINGS_KEYS = [
18
+ "jx.ai.openaiKey",
19
+ "jx.ai.baseUrl",
20
+ "jx.ai.model",
21
+ "jx.cf.token",
22
+ "jx.cf.accountId",
23
+ ] as const;
18
24
 
19
25
  /** Read a localStorage value defensively, treating unavailable/throwing storage as empty. */
20
26
  function readLocal(key: string): string {
package/src/studio.ts CHANGED
@@ -126,6 +126,7 @@ import {
126
126
  } from "./panels/block-action-bar";
127
127
  import { initCssData } from "./panels/style-utils";
128
128
  import { initQuickSearch } from "./panels/quick-search";
129
+ import { hydrateProjectList } from "./project-list";
129
130
  import { addRecentProject, hydrateRecentProjects, removeRecentProject } from "./recent-projects";
130
131
  import { hydrateSettings } from "./services/settings-store";
131
132
  import { initWelcome } from "./panels/welcome-screen";
@@ -744,6 +745,13 @@ void hydrateRecentProjects().then(() => {
744
745
  render();
745
746
  });
746
747
 
748
+ // Hydrate the platform's project catalogue (dev server sites, cloud projects), then refresh the
749
+ // Welcome screen, which reads it synchronously. No-op on platforms without listProjects.
750
+ // oxlint-disable-next-line unicorn/prefer-top-level-await -- deliberate fire-and-forget: hydration must not block initial render
751
+ void hydrateProjectList().then(() => {
752
+ render();
753
+ });
754
+
747
755
  // Hydrate user settings (AI connection parameters) from the backend store, then re-render so
748
756
  // Key-gated surfaces (assistant gate, New Project Import/Agent tabs) see the stored key.
749
757
  // oxlint-disable-next-line unicorn/prefer-top-level-await -- deliberate fire-and-forget: hydration must not block initial render
package/src/types.ts CHANGED
@@ -1,168 +1,58 @@
1
1
  /// <reference lib="dom" />
2
- import type {
3
- JsonValue as SchemaJsonValue,
4
- JxMutableNode,
5
- JxPath,
6
- ProjectConfig,
7
- } from "@jxsuite/schema/types";
8
-
9
- // ─── Git & Platform Types ───────────────────────────────────────────────────
10
-
11
- export interface GitFileStatus {
12
- status: string;
13
- path: string;
14
- staged?: boolean;
15
- }
16
-
17
- export interface GitStatusResult {
18
- branch: string;
19
- files: GitFileStatus[];
20
- ahead: number;
21
- behind: number;
22
- isRepo: boolean;
23
- remotes: string[];
24
- }
25
-
26
- export interface GitBranchesResult {
27
- current: string;
28
- branches: string[];
29
- }
30
-
31
- export interface GitLogEntry {
32
- hash: string;
33
- message: string;
34
- author: string;
35
- date: string;
36
- }
37
-
38
- export interface ComponentSlotMeta {
39
- name: string;
40
- description?: string;
41
- fallback?: (JxMutableNode | string)[];
42
- }
43
-
44
- export interface ComponentMeta {
45
- tagName: string;
46
- $id?: string | null;
47
- path: string;
48
- props?: { name: string; type?: string; default?: JsonValue; [k: string]: unknown }[];
49
- slots?: ComponentSlotMeta[];
50
- hasElements?: boolean;
51
- }
52
-
53
- export interface PackageInfo {
54
- name: string;
55
- version: string;
56
- /** True when the dependency lives in `devDependencies` rather than `dependencies`. */
57
- dev?: boolean;
58
- }
59
-
60
- /** A dependency with a newer version available, as reported by `bun outdated` / the npm registry. */
61
- export interface OutdatedInfo {
62
- name: string;
63
- /** The version range pinned in package.json (e.g. "^0.19.0"). */
64
- current: string;
65
- /** The newest published version. */
66
- latest: string;
67
- /** The newest version satisfying the current range, if known. */
68
- wanted?: string;
69
- dev?: boolean;
70
- }
71
-
72
- /** Result of a package mutation that runs `bun install` (install / set-versions). */
73
- export interface PackageOpResult {
74
- ok: boolean;
75
- /** Combined stdout/stderr from the bun invocation, surfaced to the user on failure. */
76
- log?: string;
77
- }
78
-
79
- /** Desktop app/build info surfaced in the About screen. */
80
- export interface AppInfo {
81
- version: string;
82
- channel: string;
83
- hash: string;
84
- /** Human-readable update status (e.g. "Up to date", "Update available"), if known. */
85
- updateStatus?: string;
86
- }
87
-
88
- export interface CodeServiceResult {
89
- code?: string;
90
- diagnostics?: unknown[];
91
- [key: string]: unknown;
92
- }
93
-
94
- export interface DirEntry {
95
- name: string;
96
- path: string;
97
- type: "file" | "directory";
98
- size?: number;
99
- modified?: string;
100
- }
2
+ import type { JxMutableNode, JxPath, ProjectConfig } from "@jxsuite/schema/types";
101
3
 
102
- /** A filesystem change pushed from the backend (project-relative, forward-slashed path). */
103
- export interface FsEvent {
104
- type: "add" | "change" | "unlink" | "addDir" | "unlinkDir";
105
- path: string;
106
- isDir: boolean;
107
- }
108
-
109
- /** Result of a rename, including the references rewritten across the project (refactor report). */
110
- export interface RenameResult {
111
- ok: boolean;
112
- from: string;
113
- to: string;
114
- isDir?: boolean;
115
- references?: {
116
- filesChanged: number;
117
- refsUpdated: number;
118
- files: { path: string; count: number }[];
119
- };
120
- errors?: { path: string; error: string }[];
121
- tag?: { from: string; to: string; filesChanged: number; refsUpdated: number };
122
- tagSkipped?: string;
123
- error?: string;
124
- }
4
+ // ─── Wire types (the Studio Backend Protocol) ───────────────────────────────
5
+ /* The request/response shapes every backend serves live in @jxsuite/protocol;
6
+ re-exported here so existing `../types` imports keep working. */
125
7
 
126
- /** A starter template surfaced in the New Project picker (mirrors @jxsuite/starters StarterMeta). */
127
- export interface StarterInfo {
128
- id: string;
129
- name: string;
130
- industry: string;
131
- tagline: string;
132
- description: string;
133
- features: string[];
134
- accent: string;
135
- /** Preview image as a self-contained `data:` URI. */
136
- thumbnail: string;
137
- }
8
+ import type {
9
+ AppInfo,
10
+ CfConnection,
11
+ CodeServiceResult,
12
+ ComponentMeta,
13
+ DirEntry,
14
+ FsEvent,
15
+ GitBranchesResult,
16
+ GitLogEntry,
17
+ GitStatusResult,
18
+ ImportProgressEvent,
19
+ ImportSiteOptions,
20
+ OutdatedInfo,
21
+ PackageInfo,
22
+ PackageOpResult,
23
+ ProjectListEntry,
24
+ RecentProjectEntry,
25
+ RenameResult,
26
+ StarterInfo,
27
+ } from "@jxsuite/protocol";
138
28
 
139
- /** A progress line from the AI-guided site import (mirrors @jxsuite/import ImportProgressEvent). */
140
- export interface ImportProgressEvent {
141
- phase: string;
142
- message: string;
143
- current?: number;
144
- total?: number;
145
- }
146
-
147
- /** Options for {@link StudioPlatform.importSite}. */
148
- export interface ImportSiteOptions {
149
- /** The live site to clone; must be http(s). */
150
- url: string;
151
- /** Display name for the new project. */
152
- name: string;
153
- /** Destination directory (platform-interpreted: project-relative on the dev server). */
154
- directory: string;
155
- /** Max crawl depth; 0 = single page. */
156
- depth: number;
157
- /** Max pages to capture. */
158
- maxPages: number;
159
- /** Refine component/prop names with the LLM (requires a key). */
160
- aiComponents: boolean;
161
- /** OpenAI-compatible credentials, from the user's AI settings. */
162
- apiKey?: string;
163
- baseUrl?: string;
164
- model?: string;
165
- }
29
+ export type {
30
+ AiModelInfo,
31
+ AiModelsResponse,
32
+ AppInfo,
33
+ CfConnection,
34
+ CodeServiceResult,
35
+ ComponentMeta,
36
+ ComponentSlotMeta,
37
+ DirEntry,
38
+ ErrorBody,
39
+ FsEvent,
40
+ GitBranchesResult,
41
+ GitFileStatus,
42
+ GitLogEntry,
43
+ GitStatusResult,
44
+ ImportProgressEvent,
45
+ ImportSiteOptions,
46
+ JsonValue,
47
+ OutdatedInfo,
48
+ PackageInfo,
49
+ PackageOpResult,
50
+ ProjectListEntry,
51
+ PullRequestInfo,
52
+ RecentProjectEntry,
53
+ RenameResult,
54
+ StarterInfo,
55
+ } from "@jxsuite/protocol";
166
56
 
167
57
  export interface StudioPlatform {
168
58
  id: string;
@@ -326,6 +216,42 @@ export interface StudioPlatform {
326
216
  getRecentProjects?: () => Promise<RecentProjectEntry[]>;
327
217
  /** Persist the full recent-projects list to the backend store. */
328
218
  saveRecentProjects?: (projects: RecentProjectEntry[]) => Promise<void>;
219
+ // ─── Project catalogue (platforms that can enumerate openable projects) ─────
220
+ /**
221
+ * Enumerate every project this platform can open — the dev server's sites under its root, a cloud
222
+ * platform's remote projects. Entry `root` values re-open through the same paths as recent
223
+ * projects (openRecentProject). Absent on desktop, where the OS file system is the catalogue and
224
+ * projects are found via the native picker instead.
225
+ */
226
+ listProjects?: () => Promise<ProjectListEntry[]>;
227
+ // ─── Identity & hosting connections (publish surface) ───────────────────────
228
+ /** The signed-in user's identity, when the platform has one (cloud). */
229
+ getUser?: () => Promise<{ login: string; name?: string; avatarUrl?: string } | null>;
230
+ /**
231
+ * Open a pull request for the current branch. Cloud platforms implement it against their session;
232
+ * local platforms omit it and Studio falls back to a direct GitHub API call with the user's
233
+ * stored token.
234
+ */
235
+ createPullRequest?: (opts: {
236
+ title: string;
237
+ body?: string;
238
+ head?: string;
239
+ base?: string;
240
+ }) => Promise<{ url: string; number: number }>;
241
+ /** Current Cloudflare connection state, when the platform can broker one. */
242
+ cfConnection?: () => Promise<CfConnection | null>;
243
+ /**
244
+ * Interactively connect a Cloudflare account (hosted OAuth on the cloud platform). Local
245
+ * platforms omit it — the publish UI collects an API token instead and verifies via
246
+ * cfConnection.
247
+ */
248
+ cfConnect?: () => Promise<CfConnection | null>;
249
+ /**
250
+ * Allowlisted Cloudflare API passthrough (accounts, Pages projects and deployments). The backend
251
+ * injects credentials — an OAuth token on the cloud platform, the user's pasted API token locally
252
+ * — so no secret ever rides in the request body.
253
+ */
254
+ cfApi?: (path: string, init?: { method?: string; body?: unknown }) => Promise<unknown>;
329
255
  // ─── User settings (backend-persisted; undefined on dev-server) ─────────────
330
256
  /**
331
257
  * Read the user-level settings map (e.g. the AI connection parameters) from a backend store
@@ -337,13 +263,6 @@ export interface StudioPlatform {
337
263
  saveSettings?: (settings: Record<string, string>) => Promise<void>;
338
264
  }
339
265
 
340
- /** A recently-opened project, keyed by its re-openable `root` (platform-specific). */
341
- export interface RecentProjectEntry {
342
- name: string;
343
- root: string;
344
- timestamp: number;
345
- }
346
-
347
266
  // ─── Studio Types ───────────────────────────────────────────────────────────
348
267
 
349
268
  export interface CanvasPanel {
@@ -409,9 +328,3 @@ export interface ProjectState {
409
328
  projectDirs?: string[];
410
329
  [key: string]: unknown;
411
330
  }
412
-
413
- /**
414
- * A JSON document value, or `undefined` to signal property removal in the mutators. Re-uses the
415
- * schema's precise recursive JSON model.
416
- */
417
- export type JsonValue = SchemaJsonValue | undefined;