@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.
- package/dist/studio.js +848 -208
- package/dist/studio.js.map +15 -10
- package/package.json +8 -5
- package/src/panels/ai-panel.ts +25 -3
- package/src/panels/toolbar.ts +2 -0
- package/src/panels/welcome-screen.ts +26 -0
- package/src/platforms/cloud.ts +774 -0
- package/src/platforms/devserver.ts +70 -0
- package/src/project-list.ts +38 -0
- package/src/publish/pages-service.ts +186 -0
- package/src/publish/publish-panel.ts +360 -0
- package/src/services/ai-models.ts +30 -1
- package/src/services/cf-settings.ts +58 -0
- package/src/services/settings-store.ts +7 -1
- package/src/studio.ts +8 -0
- package/src/types.ts +87 -174
|
@@ -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
|
|
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 = [
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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;
|