@joycostudio/studio 0.1.6 → 0.2.1
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 +28 -4
- package/dist/{chunk-6KZJEKOL.js → chunk-BBS4RC6N.js} +22 -48
- package/dist/chunk-BBS4RC6N.js.map +1 -0
- package/dist/{chunk-DEJYGBPJ.js → chunk-C6BT4KJA.js} +3 -3
- package/dist/chunk-C6BT4KJA.js.map +1 -0
- package/dist/chunk-SN5V6C26.js +28 -0
- package/dist/chunk-SN5V6C26.js.map +1 -0
- package/dist/cli.js +97 -42
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +15 -5
- package/dist/client.js +3 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/next.d.ts +3 -1
- package/dist/next.js +3 -2
- package/dist/next.js.map +1 -1
- package/dist/{protocol-DjrMADPm.d.ts → protocol-BviGBdSG.d.ts} +7 -5
- package/dist/react.d.ts +28 -5
- package/dist/react.js +37 -52
- package/dist/react.js.map +1 -1
- package/dist/runtime.d.ts +5 -2
- package/dist/runtime.js +42 -18
- package/dist/runtime.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/ui.d.ts +43 -0
- package/dist/ui.js +80 -0
- package/dist/ui.js.map +1 -0
- package/package.json +13 -4
- package/dist/chunk-6KZJEKOL.js.map +0 -1
- package/dist/chunk-DEJYGBPJ.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,29 @@ var DEFAULT_PORT = 5179;
|
|
|
14
14
|
import { watch } from "fs";
|
|
15
15
|
import path2 from "path";
|
|
16
16
|
|
|
17
|
+
// src/core/diff.ts
|
|
18
|
+
function diff(before, after, path9 = []) {
|
|
19
|
+
const nested = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20
|
+
if (nested(before) && nested(after)) {
|
|
21
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
22
|
+
return [...keys].flatMap((key) => diff(before[key], after[key], [...path9, key]));
|
|
23
|
+
}
|
|
24
|
+
return JSON.stringify(before) === JSON.stringify(after) ? [] : [{ path: path9, before, after }];
|
|
25
|
+
}
|
|
26
|
+
var define = (target, key, value) => Object.defineProperty(target, key, { value, writable: true, enumerable: true, configurable: true });
|
|
27
|
+
function writePath(target, path9, value) {
|
|
28
|
+
const key = path9.at(-1);
|
|
29
|
+
let parent = target;
|
|
30
|
+
for (const segment of path9.slice(0, -1)) {
|
|
31
|
+
const next = Object.hasOwn(parent, segment) ? parent[segment] : void 0;
|
|
32
|
+
if (typeof next !== "object" || next === null) define(parent, segment, {});
|
|
33
|
+
parent = parent[segment];
|
|
34
|
+
}
|
|
35
|
+
if (value === void 0) {
|
|
36
|
+
if (Object.hasOwn(parent, key)) delete parent[key];
|
|
37
|
+
} else define(parent, key, structuredClone(value));
|
|
38
|
+
}
|
|
39
|
+
|
|
17
40
|
// src/core/result.ts
|
|
18
41
|
var success = (data) => ({ ok: true, data });
|
|
19
42
|
var error = (code, message) => ({
|
|
@@ -50,9 +73,9 @@ function shapeMismatch(reference, value, path9) {
|
|
|
50
73
|
if (expected === "object") {
|
|
51
74
|
const ref = reference;
|
|
52
75
|
const obj = value;
|
|
53
|
-
for (const key of Object.keys(obj)) if (!(key
|
|
76
|
+
for (const key of Object.keys(obj)) if (!Object.hasOwn(ref, key)) return `${path9}.${key}: unknown key`;
|
|
54
77
|
for (const key of Object.keys(ref)) {
|
|
55
|
-
if (!(key
|
|
78
|
+
if (!Object.hasOwn(obj, key)) return `${path9}.${key}: missing`;
|
|
56
79
|
const nested = shapeMismatch(ref[key], obj[key], `${path9}.${key}`);
|
|
57
80
|
if (nested) return nested;
|
|
58
81
|
}
|
|
@@ -143,6 +166,28 @@ var equal = (a, b) => {
|
|
|
143
166
|
const keys = Object.keys(a);
|
|
144
167
|
return keys.length === Object.keys(b).length && keys.every((key) => equal(a[key], b[key]));
|
|
145
168
|
};
|
|
169
|
+
function rebase(tool2, old, saved) {
|
|
170
|
+
const changes = diff(old.saved.value, old.draft.value);
|
|
171
|
+
const apply = (base, edits) => {
|
|
172
|
+
const candidate = structuredClone(base);
|
|
173
|
+
try {
|
|
174
|
+
for (const { path: path9, after } of edits) writePath(candidate, path9, after);
|
|
175
|
+
} catch (cause) {
|
|
176
|
+
return error("invalid_config", cause instanceof Error ? cause.message : String(cause));
|
|
177
|
+
}
|
|
178
|
+
return validateConfig(tool2, candidate);
|
|
179
|
+
};
|
|
180
|
+
const all = apply(saved, changes);
|
|
181
|
+
if (all.ok) return { value: all.data, dropped: [] };
|
|
182
|
+
let value = saved;
|
|
183
|
+
const dropped = [];
|
|
184
|
+
for (const change of changes) {
|
|
185
|
+
const parsed = apply(value, [change]);
|
|
186
|
+
if (parsed.ok) value = parsed.data;
|
|
187
|
+
else dropped.push(change.path.join("."));
|
|
188
|
+
}
|
|
189
|
+
return { value, dropped };
|
|
190
|
+
}
|
|
146
191
|
async function load(tool2, file) {
|
|
147
192
|
const disk = await readDisk(file);
|
|
148
193
|
if (!disk) return { value: structuredClone(tool2.initial), hash: null };
|
|
@@ -302,24 +347,22 @@ async function createRuntime({ root, tools }) {
|
|
|
302
347
|
const prepared = await prepare(root, nextTools);
|
|
303
348
|
for (const [id, old] of entries) {
|
|
304
349
|
const next = prepared.entries.get(id);
|
|
305
|
-
if (
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
if (!parsed.ok && old.state.dirty)
|
|
310
|
-
throw new Error(
|
|
311
|
-
`Draft for "${id}" is incompatible with the new controls; save/discard it first: ${parsed.message}`
|
|
312
|
-
);
|
|
313
|
-
if (parsed.ok && old.state.dirty) {
|
|
314
|
-
next.state = {
|
|
315
|
-
...old.state,
|
|
316
|
-
saved: next.state.saved,
|
|
317
|
-
conflict: old.state.conflict || next.state.saved.hash !== old.state.saved.hash,
|
|
318
|
-
draft: { value: parsed.data, revision: old.state.draft.revision + 1 }
|
|
319
|
-
};
|
|
320
|
-
} else {
|
|
321
|
-
next.state.draft.revision = old.state.draft.revision + 1;
|
|
350
|
+
if (!next || next.file !== old.file) {
|
|
351
|
+
if (old.state.dirty)
|
|
352
|
+
console.warn(`[studio] dropped unsaved "${id}" edits: the tool was removed or its config moved`);
|
|
353
|
+
continue;
|
|
322
354
|
}
|
|
355
|
+
next.state.draft.revision = old.state.draft.revision + 1;
|
|
356
|
+
if (!old.state.dirty) continue;
|
|
357
|
+
const { value, dropped } = rebase(next.tool, old.state, next.state.saved.value);
|
|
358
|
+
if (dropped.length)
|
|
359
|
+
console.warn(`[studio] dropped unsaved "${id}" edits that no longer fit its controls: ${dropped.join(", ")}`);
|
|
360
|
+
next.state = {
|
|
361
|
+
...old.state,
|
|
362
|
+
saved: next.state.saved,
|
|
363
|
+
conflict: old.state.conflict || next.state.saved.hash !== old.state.saved.hash,
|
|
364
|
+
draft: { value, revision: next.state.draft.revision }
|
|
365
|
+
};
|
|
323
366
|
}
|
|
324
367
|
entries = prepared.entries;
|
|
325
368
|
ids = prepared.ids;
|
|
@@ -970,7 +1013,7 @@ import { parseArgs } from "util";
|
|
|
970
1013
|
// package.json
|
|
971
1014
|
var package_default = {
|
|
972
1015
|
name: "@joycostudio/studio",
|
|
973
|
-
version: "0.1
|
|
1016
|
+
version: "0.2.1",
|
|
974
1017
|
description: "Plug project-specific tools into a /studio route, a floating debug pane, and MCP agents.",
|
|
975
1018
|
type: "module",
|
|
976
1019
|
license: "ISC",
|
|
@@ -1017,6 +1060,10 @@ var package_default = {
|
|
|
1017
1060
|
types: "./dist/controls.d.ts",
|
|
1018
1061
|
default: "./dist/controls.js"
|
|
1019
1062
|
},
|
|
1063
|
+
"./ui": {
|
|
1064
|
+
types: "./dist/ui.d.ts",
|
|
1065
|
+
default: "./dist/ui.js"
|
|
1066
|
+
},
|
|
1020
1067
|
"./next/config": {
|
|
1021
1068
|
types: "./dist/next-config.d.ts",
|
|
1022
1069
|
default: "./dist/next-config.js"
|
|
@@ -1026,7 +1073,7 @@ var package_default = {
|
|
|
1026
1073
|
build: "pnpm build:shadow-css && tsup && tailwindcss -i src/react/styles.css -o dist/styles.css --minify && cp src/react/command-menu-export.css dist/command-menu.css",
|
|
1027
1074
|
dev: 'pnpm build:shadow-css && pnpm run --parallel "/^dev:/"',
|
|
1028
1075
|
test: "vitest run",
|
|
1029
|
-
"test:browser": "STUDIO_TEST_BROWSER=1 vitest run test/browser-sync.test.ts",
|
|
1076
|
+
"test:browser": "STUDIO_TEST_BROWSER=1 vitest run test/browser-sync.test.ts test/ui.test.ts",
|
|
1030
1077
|
"test:capture": "STUDIO_TEST_CAPTURE=1 vitest run test/capture.test.ts",
|
|
1031
1078
|
typecheck: "tsc --noEmit",
|
|
1032
1079
|
"dev:js": "tsup --watch",
|
|
@@ -1053,7 +1100,8 @@ var package_default = {
|
|
|
1053
1100
|
typescript: "^5.9.3"
|
|
1054
1101
|
},
|
|
1055
1102
|
peerDependencies: {
|
|
1056
|
-
"@joycostudio/xyz": "^1.
|
|
1103
|
+
"@joycostudio/xyz": "^1.5.0",
|
|
1104
|
+
dialkit: "^2.2.0",
|
|
1057
1105
|
react: ">=18",
|
|
1058
1106
|
"react-dom": ">=18",
|
|
1059
1107
|
tweakpane: "^4.0.5"
|
|
@@ -1067,10 +1115,13 @@ var package_default = {
|
|
|
1067
1115
|
},
|
|
1068
1116
|
"react-dom": {
|
|
1069
1117
|
optional: true
|
|
1118
|
+
},
|
|
1119
|
+
dialkit: {
|
|
1120
|
+
optional: true
|
|
1070
1121
|
}
|
|
1071
1122
|
},
|
|
1072
1123
|
devDependencies: {
|
|
1073
|
-
"@joycostudio/xyz": "^1.
|
|
1124
|
+
"@joycostudio/xyz": "^1.5.0",
|
|
1074
1125
|
"@modelcontextprotocol/client": "^2.3.1",
|
|
1075
1126
|
"@tailwindcss/cli": "^4.3.3",
|
|
1076
1127
|
"@tweakpane/core": "^2.0.5",
|
|
@@ -1078,6 +1129,7 @@ var package_default = {
|
|
|
1078
1129
|
"@types/react": "^19.3.0",
|
|
1079
1130
|
"@types/react-dom": "^19.3.0",
|
|
1080
1131
|
"@types/ws": "^8.18.2",
|
|
1132
|
+
dialkit: "^2.2.0",
|
|
1081
1133
|
react: "^19.3.0",
|
|
1082
1134
|
"react-dom": "^19.3.0",
|
|
1083
1135
|
tailwindcss: "^4.3.3",
|
|
@@ -1138,9 +1190,11 @@ async function init(argv) {
|
|
|
1138
1190
|
const { positionals, values } = parseArgs({
|
|
1139
1191
|
args: argv,
|
|
1140
1192
|
allowPositionals: true,
|
|
1141
|
-
options: { "no-install": { type: "boolean" } }
|
|
1193
|
+
options: { "no-install": { type: "boolean" }, ui: { type: "string", default: "tweakpane" } }
|
|
1142
1194
|
});
|
|
1143
|
-
if (positionals.length > 1) throw new Error("Usage: studio init [directory] [--no-install]");
|
|
1195
|
+
if (positionals.length > 1) throw new Error("Usage: studio init [directory] [--ui tweakpane|dialkit] [--no-install]");
|
|
1196
|
+
const ui = values.ui;
|
|
1197
|
+
if (ui !== "tweakpane" && ui !== "dialkit") throw new Error(`[studio] --ui must be tweakpane or dialkit, got ${ui}.`);
|
|
1144
1198
|
const root = path6.resolve(positionals[0] ?? ".");
|
|
1145
1199
|
const manifest = path6.join(root, "package.json");
|
|
1146
1200
|
const pkg = JSON.parse(readFileSync3(manifest, "utf8"));
|
|
@@ -1161,10 +1215,17 @@ async function init(argv) {
|
|
|
1161
1215
|
const page = `${app}/studio/page.tsx`;
|
|
1162
1216
|
const preview = `${app}/studio-preview/page.tsx`;
|
|
1163
1217
|
const files = {
|
|
1164
|
-
[client]:
|
|
1218
|
+
[client]: ui === "dialkit" ? `import 'dialkit/vanilla/styles.css'
|
|
1219
|
+
import { createStudio } from '@joycostudio/studio/next'
|
|
1220
|
+
import { dialkit } from '@joycostudio/studio/ui'
|
|
1221
|
+
import { tools } from '${relative(client, ".studio/registry.generated")}'
|
|
1222
|
+
|
|
1223
|
+
export const studio = createStudio({ tools, ui: dialkit(() => import('dialkit/vanilla')) })
|
|
1224
|
+
` : `import { createStudio } from '@joycostudio/studio/next'
|
|
1225
|
+
import { tweakpane } from '@joycostudio/studio/ui'
|
|
1165
1226
|
import { tools } from '${relative(client, ".studio/registry.generated")}'
|
|
1166
1227
|
|
|
1167
|
-
export const studio = createStudio({ tools })
|
|
1228
|
+
export const studio = createStudio({ tools, ui: tweakpane(() => import('tweakpane')) })
|
|
1168
1229
|
`,
|
|
1169
1230
|
[data]: '{ "scale": 1 }\n',
|
|
1170
1231
|
[tool2]: `import { defineTool } from '@joycostudio/studio'
|
|
@@ -1251,7 +1312,8 @@ export default function StudioPreview() {
|
|
|
1251
1312
|
if (pkg.dependencies?.["@joycostudio/studio"] || pkg.devDependencies?.["@joycostudio/studio"]) {
|
|
1252
1313
|
try {
|
|
1253
1314
|
const require2 = createRequire(manifest);
|
|
1254
|
-
for (const entry of ["next", "next/config", "controls", "react"])
|
|
1315
|
+
for (const entry of ["next", "next/config", "controls", "react", "ui"])
|
|
1316
|
+
require2.resolve(`@joycostudio/studio/${entry}`);
|
|
1255
1317
|
} catch {
|
|
1256
1318
|
throw new Error(
|
|
1257
1319
|
"[studio] The declared Studio dependency is missing or lacks the required exports. Install the same version (or preview) as this initializer, then rerun init. No files were changed."
|
|
@@ -1261,7 +1323,7 @@ export default function StudioPreview() {
|
|
|
1261
1323
|
const dependencies = {
|
|
1262
1324
|
"@joycostudio/studio": `^${package_default.version}`,
|
|
1263
1325
|
"@joycostudio/xyz": package_default.peerDependencies["@joycostudio/xyz"],
|
|
1264
|
-
|
|
1326
|
+
[ui]: package_default.peerDependencies[ui]
|
|
1265
1327
|
};
|
|
1266
1328
|
pkg.dependencies ??= {};
|
|
1267
1329
|
for (const [name, version] of Object.entries(dependencies)) {
|
|
@@ -1436,7 +1498,7 @@ function watchDiscovery(initial, refresh) {
|
|
|
1436
1498
|
var HELP = `Usage: studio dev [--config studio.config.ts] [--port ${DEFAULT_PORT}] [--app-url http://localhost:3000] [-- <command...>]
|
|
1437
1499
|
studio generate [--config studio.config.ts]
|
|
1438
1500
|
studio install-browser
|
|
1439
|
-
studio init [directory] [--no-install]
|
|
1501
|
+
studio init [directory] [--ui tweakpane|dialkit] [--no-install]
|
|
1440
1502
|
|
|
1441
1503
|
Starts the Studio runtime with a WebSocket bridge and an HTTP MCP endpoint at /mcp,
|
|
1442
1504
|
moving to the next free port when another project's Studio already uses it.
|
|
@@ -1499,27 +1561,20 @@ async function main(argv) {
|
|
|
1499
1561
|
`[studio] runtime ready on ws://127.0.0.1:${bridge.port} \u2014 ${config.tools.length} tool(s) in ${path8.relative(process.cwd(), root) || "."}`
|
|
1500
1562
|
);
|
|
1501
1563
|
console.log(`[studio] MCP ready on http://127.0.0.1:${bridge.port}/mcp`);
|
|
1502
|
-
let rejected = false;
|
|
1503
1564
|
let accepted = discovery;
|
|
1504
1565
|
const watcher = discovery && watchDiscovery(discovery, async () => {
|
|
1566
|
+
const next = await loadConfig(process.cwd(), values.config, false, accepted);
|
|
1505
1567
|
try {
|
|
1506
|
-
const next = await loadConfig(process.cwd(), values.config, false, accepted);
|
|
1507
1568
|
await runtime.replaceTools(next.config.tools);
|
|
1508
|
-
await next.discovery?.write();
|
|
1509
|
-
accepted = next.discovery;
|
|
1510
|
-
rejected = false;
|
|
1511
|
-
return next.discovery;
|
|
1512
1569
|
} catch (cause) {
|
|
1513
|
-
|
|
1514
|
-
throw cause;
|
|
1570
|
+
throw new DiscoveryInputError(cause, next.discovery?.dependencies ?? /* @__PURE__ */ new Set());
|
|
1515
1571
|
}
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1572
|
+
await next.discovery?.write();
|
|
1573
|
+
accepted = next.discovery;
|
|
1574
|
+
return next.discovery;
|
|
1519
1575
|
});
|
|
1520
1576
|
const shutdown = async (code = 0) => {
|
|
1521
1577
|
watcher?.close();
|
|
1522
|
-
unsubscribe();
|
|
1523
1578
|
runtime.close();
|
|
1524
1579
|
await bridge.close();
|
|
1525
1580
|
process.exit(code);
|