dsh-webui-studio 0.1.0 → 0.2.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/PRODUCT.md +11 -7
- package/README.md +69 -21
- package/README.zh-CN.md +64 -20
- package/dist/bridge.js +10 -10
- package/dist/studio.css +1 -1
- package/dist/studio.js +16691 -10202
- package/docs/bidirectional-connection-handoff.md +729 -0
- package/docs/harmony-api-requirements.md +17 -13
- package/docs/remote-development.md +80 -0
- package/lib/bridge/element-style-selector.d.ts +1 -0
- package/lib/bridge/element-style-selector.js +53 -0
- package/lib/contracts.d.ts +152 -83
- package/lib/contracts.js +0 -2
- package/lib/host/agent.d.ts +15 -13
- package/lib/host/agent.js +213 -56
- package/lib/host/automatic-patch.d.ts +9 -0
- package/lib/host/automatic-patch.js +433 -0
- package/lib/host/backend.d.ts +134 -4
- package/lib/host/backend.js +465 -114
- package/lib/host/drafts.d.ts +1 -1
- package/lib/host/drafts.js +65 -15
- package/lib/host/element-source.d.ts +9 -0
- package/lib/host/element-source.js +295 -0
- package/lib/host/mcp.d.ts +4 -0
- package/lib/host/mcp.js +97 -0
- package/lib/host/preview-draft.d.ts +22 -0
- package/lib/host/preview-draft.js +162 -0
- package/lib/host/preview-port.d.ts +8 -0
- package/lib/host/preview-port.js +32 -0
- package/lib/host/preview-worker.d.ts +65 -2
- package/lib/host/preview-worker.js +203 -76
- package/lib/host/preview.d.ts +26 -5
- package/lib/host/preview.js +130 -49
- package/lib/host/readiness.d.ts +2 -2
- package/lib/host/readiness.js +14 -17
- package/lib/host/routes.d.ts +1 -7
- package/lib/host/routes.js +41 -50
- package/lib/host/runtime-profile.d.ts +2 -1
- package/lib/host/runtime-profile.js +30 -8
- package/lib/host/source-resolution.d.ts +11 -1
- package/lib/host/source-resolution.js +69 -24
- package/lib/host/studio-service.d.ts +129 -0
- package/lib/host/studio-service.js +53 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.js +64 -30
- package/lib/studio-remote.d.ts +126 -0
- package/lib/studio-remote.js +188 -0
- package/lib/variable-tree.d.ts +2 -0
- package/lib/variable-tree.js +13 -0
- package/package.json +62 -25
- package/studio.patch.yml +12 -0
package/lib/host/backend.js
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
1
2
|
import { StudioAgentController } from './agent.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
3
|
+
import { analyzeAutomaticPatch, writeAutomaticPatch } from './automatic-patch.js';
|
|
4
|
+
import { StudioBuildRunner } from './build.js';
|
|
5
|
+
import { readElementsStyles, saveElementsSource } from './element-source.js';
|
|
6
|
+
import { dshPackageModules, StudioPreviewSupervisor } from './preview.js';
|
|
4
7
|
import { applyProjectPatch, listProjectFiles, readProjectFile, writeProjectFile } from './project-files.js';
|
|
5
8
|
import { inspectReadiness, StudioPackRunner } from './readiness.js';
|
|
6
|
-
import { assertDraftPackageIdentity } from './runtime-profile.js';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
function success(rpcId, value) {
|
|
11
|
-
return { type: 'server-response', rpcId, result: { ok: true, value } };
|
|
12
|
-
}
|
|
9
|
+
import { assertDraftPackageIdentity, installDraftDependencies } from './runtime-profile.js';
|
|
10
|
+
import { StudioSourceResolver } from './source-resolution.js';
|
|
11
|
+
const HARMONY_BIN_ENTRY = fileURLToPath(import.meta.resolve('dsh-harmony/bin'));
|
|
13
12
|
function objectPayload(payload) {
|
|
14
13
|
if (typeof payload !== 'object' || payload === null)
|
|
15
14
|
throw new Error('request payload must be an object');
|
|
@@ -21,16 +20,104 @@ function draftId(payload) {
|
|
|
21
20
|
throw new Error('draftId is required');
|
|
22
21
|
return id;
|
|
23
22
|
}
|
|
23
|
+
function optionalStringList(value, field) {
|
|
24
|
+
if (value === undefined)
|
|
25
|
+
return undefined;
|
|
26
|
+
if (!Array.isArray(value) || value.some(item => typeof item !== 'string' || item.length === 0)) {
|
|
27
|
+
throw new Error(`${field} must be an array of non-empty strings`);
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
function automaticPatchRequest(payload) {
|
|
32
|
+
const input = objectPayload(payload);
|
|
33
|
+
if (!Array.isArray(input.targets) || input.targets.length === 0) {
|
|
34
|
+
throw new Error('automatic Patch targets must be a non-empty array');
|
|
35
|
+
}
|
|
36
|
+
const targets = input.targets.map((value, index) => {
|
|
37
|
+
if (typeof value !== 'object' || value === null)
|
|
38
|
+
throw new Error(`automatic Patch target ${index} must be an object`);
|
|
39
|
+
const target = value;
|
|
40
|
+
if (typeof target.package !== 'string' || target.package === '' || typeof target.file !== 'string' || target.file === '') {
|
|
41
|
+
throw new Error(`automatic Patch target ${index} requires package and file`);
|
|
42
|
+
}
|
|
43
|
+
return { package: target.package, file: target.file };
|
|
44
|
+
});
|
|
45
|
+
if (input.kind === 'replace-string') {
|
|
46
|
+
if (typeof input.text !== 'string' || typeof input.replacement !== 'string' || typeof input.clientFile !== 'string'
|
|
47
|
+
|| typeof input.selector !== 'string' || typeof input.elementId !== 'string' || typeof input.elementLabel !== 'string'
|
|
48
|
+
|| typeof input.boundary !== 'object' || input.boundary === null || !Array.isArray(input.boundary.path)) {
|
|
49
|
+
throw new Error('automatic content Patch requires text, replacement, client source, boundary, selector, and element identity');
|
|
50
|
+
}
|
|
51
|
+
const boundary = input.boundary;
|
|
52
|
+
if (typeof boundary.surfaceId !== 'string' || !boundary.path.every(item => typeof item === 'string')) {
|
|
53
|
+
throw new Error('automatic content Patch boundary is invalid');
|
|
54
|
+
}
|
|
55
|
+
if (input.targetSelector !== undefined && (typeof input.targetSelector !== 'string' || input.targetSelector === '')) {
|
|
56
|
+
throw new Error('automatic content Patch target selector is invalid');
|
|
57
|
+
}
|
|
58
|
+
if (input.elementSourceFile !== undefined && (typeof input.elementSourceFile !== 'string' || input.elementSourceFile === '')) {
|
|
59
|
+
throw new Error('automatic content Patch Element source is invalid');
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
kind: input.kind, targets, text: input.text, replacement: input.replacement, clientFile: input.clientFile,
|
|
63
|
+
boundary: { surfaceId: boundary.surfaceId, path: boundary.path },
|
|
64
|
+
...(input.targetSelector === undefined ? {} : { targetSelector: input.targetSelector }),
|
|
65
|
+
selector: input.selector, elementId: input.elementId, elementLabel: input.elementLabel,
|
|
66
|
+
...(input.elementSourceFile === undefined ? {} : { elementSourceFile: input.elementSourceFile }),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (input.kind !== 'css-style' || typeof input.component !== 'string' || typeof input.clientFile !== 'string'
|
|
70
|
+
|| typeof input.selector !== 'string' || typeof input.elementId !== 'string' || typeof input.elementLabel !== 'string'
|
|
71
|
+
|| typeof input.boundary !== 'object' || input.boundary === null || !Array.isArray(input.boundary.path)
|
|
72
|
+
|| !Array.isArray(input.variables))
|
|
73
|
+
throw new Error('automatic CSS Patch requires component, client source, boundary, selector, element identity, and variables');
|
|
74
|
+
const boundary = input.boundary;
|
|
75
|
+
if (typeof boundary.surfaceId !== 'string' || !boundary.path.every(item => typeof item === 'string')) {
|
|
76
|
+
throw new Error('automatic CSS Patch boundary is invalid');
|
|
77
|
+
}
|
|
78
|
+
if (input.targetSelector !== undefined && (typeof input.targetSelector !== 'string' || input.targetSelector === '')) {
|
|
79
|
+
throw new Error('automatic CSS Patch target selector is invalid');
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
kind: input.kind,
|
|
83
|
+
targets,
|
|
84
|
+
component: input.component,
|
|
85
|
+
clientFile: input.clientFile,
|
|
86
|
+
boundary: { surfaceId: boundary.surfaceId, path: boundary.path },
|
|
87
|
+
...(input.targetSelector === undefined ? {} : { targetSelector: input.targetSelector }),
|
|
88
|
+
selector: input.selector,
|
|
89
|
+
elementId: input.elementId,
|
|
90
|
+
elementLabel: input.elementLabel,
|
|
91
|
+
variables: input.variables,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function elementStyleSources(payload) {
|
|
95
|
+
const value = objectPayload(payload).styles;
|
|
96
|
+
if (!Array.isArray(value))
|
|
97
|
+
throw new Error('styles must be an array');
|
|
98
|
+
return value.map((entry, index) => {
|
|
99
|
+
if (typeof entry !== 'object' || entry === null)
|
|
100
|
+
throw new Error(`styles[${index}] must be an object`);
|
|
101
|
+
const input = entry;
|
|
102
|
+
if (typeof input.elementId !== 'string' || !Array.isArray(input.rules))
|
|
103
|
+
throw new Error(`styles[${index}] requires elementId and rules`);
|
|
104
|
+
return { elementId: input.elementId, rules: input.rules };
|
|
105
|
+
});
|
|
106
|
+
}
|
|
24
107
|
class StudioDraftController {
|
|
25
108
|
record;
|
|
109
|
+
commands;
|
|
110
|
+
kind = 'draft';
|
|
26
111
|
projectState;
|
|
27
112
|
previewState = { connected: false, mode: 'browse' };
|
|
28
113
|
builds;
|
|
29
114
|
packs;
|
|
30
115
|
agent;
|
|
116
|
+
automaticPatchWrites = Promise.resolve();
|
|
31
117
|
preview;
|
|
32
118
|
constructor(record, profileDir, parentOrigin, commands, harmonyBinEntry, agents, subprocess) {
|
|
33
119
|
this.record = record;
|
|
120
|
+
this.commands = commands;
|
|
34
121
|
this.preview = new StudioPreviewSupervisor(record, profileDir, parentOrigin, commands, harmonyBinEntry);
|
|
35
122
|
this.builds = new StudioBuildRunner(subprocess);
|
|
36
123
|
this.packs = new StudioPackRunner(subprocess);
|
|
@@ -51,7 +138,7 @@ class StudioDraftController {
|
|
|
51
138
|
return this.view();
|
|
52
139
|
}
|
|
53
140
|
async stop() {
|
|
54
|
-
await this.agent.
|
|
141
|
+
await this.agent.leave();
|
|
55
142
|
await this.builds.cancel();
|
|
56
143
|
await this.preview.stop();
|
|
57
144
|
this.projectState = undefined;
|
|
@@ -59,7 +146,7 @@ class StudioDraftController {
|
|
|
59
146
|
return this.view();
|
|
60
147
|
}
|
|
61
148
|
async dispose() {
|
|
62
|
-
await this.agent.
|
|
149
|
+
await this.agent.leave();
|
|
63
150
|
await this.builds.dispose();
|
|
64
151
|
await this.packs.dispose();
|
|
65
152
|
await this.preview.dispose();
|
|
@@ -80,6 +167,42 @@ class StudioDraftController {
|
|
|
80
167
|
selection() {
|
|
81
168
|
return this.previewState.selection;
|
|
82
169
|
}
|
|
170
|
+
async context() {
|
|
171
|
+
const selection = this.selection();
|
|
172
|
+
const refs = new Map();
|
|
173
|
+
for (const patch of selection?.react?.patches ?? []) {
|
|
174
|
+
const key = `${patch.target.package}\0${patch.target.file}`;
|
|
175
|
+
refs.set(key, patch.target);
|
|
176
|
+
}
|
|
177
|
+
const source = selection?.react?.source?.resolved;
|
|
178
|
+
if (source?.package !== undefined) {
|
|
179
|
+
const key = `${source.package}\0${source.file}`;
|
|
180
|
+
refs.set(key, { package: source.package, file: source.file });
|
|
181
|
+
}
|
|
182
|
+
const allTargetRefs = [...refs.values()];
|
|
183
|
+
const targetRefs = allTargetRefs.slice(0, 8);
|
|
184
|
+
const inspections = await Promise.all(targetRefs.map(ref => this.inspectHarmony(ref)));
|
|
185
|
+
const inspectedHarmony = inspections.length === 0 ? null : {
|
|
186
|
+
patches: [...new Map(inspections.flatMap(item => item.patches).map(patch => [patch.key, patch])).values()],
|
|
187
|
+
targets: [...new Map(inspections.flatMap(item => item.targets).map(target => [`${target.package}\0${target.file}`, target])).values()],
|
|
188
|
+
};
|
|
189
|
+
const harmony = inspectedHarmony !== null && Buffer.byteLength(JSON.stringify(inspectedHarmony)) <= 256 * 1024
|
|
190
|
+
? inspectedHarmony : null;
|
|
191
|
+
const readiness = await this.readiness();
|
|
192
|
+
return {
|
|
193
|
+
target: 'draft',
|
|
194
|
+
readOnly: false,
|
|
195
|
+
selection: selection ?? null,
|
|
196
|
+
project: this.project(),
|
|
197
|
+
preview: this.previewStatus(),
|
|
198
|
+
projectFiles: await listProjectFiles(this.record.root),
|
|
199
|
+
profile: await this.harmonyProfile(),
|
|
200
|
+
harmony,
|
|
201
|
+
targetRefs,
|
|
202
|
+
targetRefsTruncated: targetRefs.length < allTargetRefs.length,
|
|
203
|
+
readiness: { findings: readiness.findings },
|
|
204
|
+
};
|
|
205
|
+
}
|
|
83
206
|
updatePreview(update) {
|
|
84
207
|
const next = { ...this.previewState, ...update };
|
|
85
208
|
if (next.selection === null)
|
|
@@ -101,9 +224,18 @@ class StudioDraftController {
|
|
|
101
224
|
async inspectHarmony(input) {
|
|
102
225
|
return (await this.preview.inspect(input)).harmony;
|
|
103
226
|
}
|
|
227
|
+
harmonyProfile() {
|
|
228
|
+
return this.preview.profile();
|
|
229
|
+
}
|
|
230
|
+
profile() {
|
|
231
|
+
return this.preview.profile();
|
|
232
|
+
}
|
|
233
|
+
updateProfile(input) {
|
|
234
|
+
return this.preview.updateProfile(input);
|
|
235
|
+
}
|
|
104
236
|
async readiness() {
|
|
105
237
|
const inspection = await this.preview.inspect();
|
|
106
|
-
return inspectReadiness(this.record.root, this.record.name, inspection.harmony, `${this.record.runtimeHome}/profiles/web
|
|
238
|
+
return inspectReadiness(this.record.root, this.record.name, inspection.harmony, `${this.record.runtimeHome}/profiles/web`);
|
|
107
239
|
}
|
|
108
240
|
async pack() {
|
|
109
241
|
const report = await this.readiness();
|
|
@@ -116,6 +248,33 @@ class StudioDraftController {
|
|
|
116
248
|
async applyPatch(path, before, after) {
|
|
117
249
|
return applyProjectPatch(this.record.root, path, before, after);
|
|
118
250
|
}
|
|
251
|
+
draftElements() {
|
|
252
|
+
const elements = this.previewStatus().registry?.elements.filter(item => item.owner === this.record.name) ?? [];
|
|
253
|
+
if (elements.length === 0)
|
|
254
|
+
throw new Error('No Elements are registered by the active Draft');
|
|
255
|
+
return elements;
|
|
256
|
+
}
|
|
257
|
+
async readElementStyles() {
|
|
258
|
+
return readElementsStyles(this.record.root, this.draftElements());
|
|
259
|
+
}
|
|
260
|
+
async saveElementSource(styles) {
|
|
261
|
+
return saveElementsSource(this.record.root, this.draftElements(), styles);
|
|
262
|
+
}
|
|
263
|
+
async analyzeAutomaticPatch(request) {
|
|
264
|
+
const sources = await Promise.all(request.targets.map(target => this.preview.readPatchTarget(target.package, target.file)));
|
|
265
|
+
return analyzeAutomaticPatch(request, sources, this.record.name);
|
|
266
|
+
}
|
|
267
|
+
async createAutomaticPatch(request) {
|
|
268
|
+
const run = this.automaticPatchWrites.then(async () => {
|
|
269
|
+
const plan = await this.analyzeAutomaticPatch(request);
|
|
270
|
+
const result = await writeAutomaticPatch(this.record.root, plan);
|
|
271
|
+
if (plan.client !== undefined)
|
|
272
|
+
await installDraftDependencies(this.record, this.commands);
|
|
273
|
+
return result;
|
|
274
|
+
});
|
|
275
|
+
this.automaticPatchWrites = run.then(() => undefined, () => undefined);
|
|
276
|
+
return run;
|
|
277
|
+
}
|
|
119
278
|
async build(signal) {
|
|
120
279
|
const current = this.project();
|
|
121
280
|
if (current.state !== 'active')
|
|
@@ -131,8 +290,118 @@ class StudioDraftController {
|
|
|
131
290
|
createAgent(agentPreset) {
|
|
132
291
|
return this.agent.create(agentPreset);
|
|
133
292
|
}
|
|
134
|
-
|
|
135
|
-
|
|
293
|
+
attachAgent(sessionId) {
|
|
294
|
+
return this.agent.attach(sessionId);
|
|
295
|
+
}
|
|
296
|
+
async leaveAgent() {
|
|
297
|
+
await this.agent.leave();
|
|
298
|
+
return this.view();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
class StudioCurrentInstanceController {
|
|
302
|
+
harmony;
|
|
303
|
+
previewUrl;
|
|
304
|
+
bridgeCapability;
|
|
305
|
+
kind = 'current-instance';
|
|
306
|
+
previewState = { connected: false, mode: 'browse' };
|
|
307
|
+
agent;
|
|
308
|
+
sources;
|
|
309
|
+
constructor(harmony, agents, previewUrl, bridgeCapability) {
|
|
310
|
+
this.harmony = harmony;
|
|
311
|
+
this.previewUrl = previewUrl;
|
|
312
|
+
this.bridgeCapability = bridgeCapability;
|
|
313
|
+
this.agent = new StudioAgentController(agents, this);
|
|
314
|
+
const profileDir = harmony.profile().dir;
|
|
315
|
+
this.sources = new StudioSourceResolver(undefined, profileDir, [dshPackageModules(HARMONY_BIN_ENTRY)]);
|
|
316
|
+
}
|
|
317
|
+
view() {
|
|
318
|
+
const agent = this.agent.snapshot();
|
|
319
|
+
return {
|
|
320
|
+
previewUrl: this.previewUrl,
|
|
321
|
+
bridgeCapability: this.bridgeCapability,
|
|
322
|
+
...(agent === undefined ? {} : { agent }),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
project() {
|
|
326
|
+
const profile = this.harmony.profile();
|
|
327
|
+
return {
|
|
328
|
+
name: 'current-webui',
|
|
329
|
+
root: profile.dir,
|
|
330
|
+
state: 'active',
|
|
331
|
+
graphRev: this.previewState.graphRev ?? String(profile.revision),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
selection() {
|
|
335
|
+
return this.previewState.selection;
|
|
336
|
+
}
|
|
337
|
+
previewStatus() {
|
|
338
|
+
return this.previewState;
|
|
339
|
+
}
|
|
340
|
+
updatePreview(update) {
|
|
341
|
+
const next = { ...this.previewState, ...update };
|
|
342
|
+
if (next.selection === null)
|
|
343
|
+
delete next.selection;
|
|
344
|
+
if (next.registry === null)
|
|
345
|
+
delete next.registry;
|
|
346
|
+
this.previewState = next;
|
|
347
|
+
return this.previewState;
|
|
348
|
+
}
|
|
349
|
+
resolveSource(source) {
|
|
350
|
+
return this.sources.resolve(source);
|
|
351
|
+
}
|
|
352
|
+
harmonyProfile() {
|
|
353
|
+
return Promise.resolve(this.harmony.profile());
|
|
354
|
+
}
|
|
355
|
+
inspectHarmony(input) {
|
|
356
|
+
return Promise.resolve(this.harmony.inspect(input));
|
|
357
|
+
}
|
|
358
|
+
readDependencySource(packageName, file) {
|
|
359
|
+
return this.sources.readDependency(packageName, file);
|
|
360
|
+
}
|
|
361
|
+
async context() {
|
|
362
|
+
const selection = this.selection();
|
|
363
|
+
const refs = new Map();
|
|
364
|
+
for (const patch of selection?.react?.patches ?? []) {
|
|
365
|
+
refs.set(`${patch.target.package}\0${patch.target.file}`, patch.target);
|
|
366
|
+
}
|
|
367
|
+
const source = selection?.react?.source?.resolved;
|
|
368
|
+
if (source?.package !== undefined)
|
|
369
|
+
refs.set(`${source.package}\0${source.file}`, { package: source.package, file: source.file });
|
|
370
|
+
const allTargetRefs = [...refs.values()];
|
|
371
|
+
const targetRefs = allTargetRefs.slice(0, 8);
|
|
372
|
+
const inspections = await Promise.all(targetRefs.map(ref => this.inspectHarmony(ref)));
|
|
373
|
+
const inspectedHarmony = inspections.length === 0 ? null : {
|
|
374
|
+
patches: [...new Map(inspections.flatMap(item => item.patches).map(patch => [patch.key, patch])).values()],
|
|
375
|
+
targets: [...new Map(inspections.flatMap(item => item.targets).map(target => [`${target.package}\0${target.file}`, target])).values()],
|
|
376
|
+
};
|
|
377
|
+
const harmony = inspectedHarmony !== null && Buffer.byteLength(JSON.stringify(inspectedHarmony)) <= 256 * 1024
|
|
378
|
+
? inspectedHarmony : null;
|
|
379
|
+
return {
|
|
380
|
+
target: 'current-instance',
|
|
381
|
+
readOnly: true,
|
|
382
|
+
selection: selection ?? null,
|
|
383
|
+
project: this.project(),
|
|
384
|
+
preview: this.previewStatus(),
|
|
385
|
+
projectFiles: [],
|
|
386
|
+
profile: this.harmony.profile(),
|
|
387
|
+
harmony,
|
|
388
|
+
targetRefs,
|
|
389
|
+
targetRefsTruncated: targetRefs.length < allTargetRefs.length,
|
|
390
|
+
readiness: { findings: [] },
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
createAgent(agentPreset) {
|
|
394
|
+
return this.agent.create(agentPreset);
|
|
395
|
+
}
|
|
396
|
+
attachAgent(sessionId) {
|
|
397
|
+
return this.agent.attach(sessionId);
|
|
398
|
+
}
|
|
399
|
+
async leaveAgent() {
|
|
400
|
+
await this.agent.leave();
|
|
401
|
+
return this.view();
|
|
402
|
+
}
|
|
403
|
+
dispose() {
|
|
404
|
+
return this.agent.leave();
|
|
136
405
|
}
|
|
137
406
|
}
|
|
138
407
|
/** Stable-Host control plane for persistent, isolated Draft Preview runtimes. */
|
|
@@ -146,7 +415,8 @@ export class StudioBackend {
|
|
|
146
415
|
parentOrigin;
|
|
147
416
|
controllers = new Map();
|
|
148
417
|
controllerCreations = new Map();
|
|
149
|
-
|
|
418
|
+
current;
|
|
419
|
+
constructor(harmony, agents, subprocess, registry, workspace, commands, parentOrigin, currentBridgeCapability = 'current-instance') {
|
|
150
420
|
this.harmony = harmony;
|
|
151
421
|
this.agents = agents;
|
|
152
422
|
this.subprocess = subprocess;
|
|
@@ -154,109 +424,189 @@ export class StudioBackend {
|
|
|
154
424
|
this.workspace = workspace;
|
|
155
425
|
this.commands = commands;
|
|
156
426
|
this.parentOrigin = parentOrigin;
|
|
427
|
+
this.current = new StudioCurrentInstanceController(harmony, agents, `${parentOrigin}/#dsh-studio-preview=${encodeURIComponent(currentBridgeCapability)}`, currentBridgeCapability);
|
|
157
428
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
return success(rpcId, await controller.activate(graphRev));
|
|
198
|
-
}
|
|
199
|
-
if (method === 'studio.project.files')
|
|
200
|
-
return success(rpcId, await listProjectFiles(controller.record.root));
|
|
201
|
-
if (method === 'studio.project.readFile') {
|
|
202
|
-
const path = objectPayload(payload).path;
|
|
203
|
-
if (typeof path !== 'string')
|
|
204
|
-
throw new Error('path is required');
|
|
205
|
-
return success(rpcId, { path, content: await controller.readFile(path) });
|
|
206
|
-
}
|
|
207
|
-
if (method === 'studio.project.writeFile') {
|
|
208
|
-
const { path, content } = objectPayload(payload);
|
|
209
|
-
if (typeof path !== 'string' || typeof content !== 'string')
|
|
210
|
-
throw new Error('path and content are required');
|
|
211
|
-
await writeProjectFile(controller.record.root, path, content);
|
|
212
|
-
return success(rpcId, { path, saved: true });
|
|
213
|
-
}
|
|
214
|
-
if (method === 'studio.project.build')
|
|
215
|
-
return success(rpcId, await controller.build(new AbortController().signal));
|
|
216
|
-
if (method === 'studio.project.cancelBuild')
|
|
217
|
-
return success(rpcId, { canceled: await controller.cancelBuild() });
|
|
218
|
-
if (method === 'studio.readiness.inspect')
|
|
219
|
-
return success(rpcId, await controller.readiness());
|
|
220
|
-
if (method === 'studio.readiness.pack')
|
|
221
|
-
return success(rpcId, await controller.pack());
|
|
222
|
-
if (method === 'studio.harmony.inspect') {
|
|
223
|
-
const input = objectPayload(payload);
|
|
224
|
-
return success(rpcId, await controller.inspectHarmony({
|
|
225
|
-
...(typeof input.package === 'string' ? { package: input.package } : {}),
|
|
226
|
-
...(typeof input.file === 'string' ? { file: input.file } : {}),
|
|
227
|
-
}));
|
|
228
|
-
}
|
|
229
|
-
if (method === 'studio.preview.status')
|
|
230
|
-
return success(rpcId, controller.previewStatus());
|
|
231
|
-
if (method === 'studio.preview.update')
|
|
232
|
-
return success(rpcId, controller.updatePreview(this.previewStatus(payload)));
|
|
233
|
-
if (method === 'studio.preview.resolveSource') {
|
|
234
|
-
const source = objectPayload(payload).source;
|
|
235
|
-
if (typeof source?.file !== 'string')
|
|
236
|
-
throw new Error('source is required');
|
|
237
|
-
return success(rpcId, await controller.resolveSource(source));
|
|
238
|
-
}
|
|
239
|
-
if (method === 'studio.agent.create') {
|
|
240
|
-
const preset = objectPayload(payload).agentPreset;
|
|
241
|
-
if (preset !== undefined && typeof preset !== 'string')
|
|
242
|
-
throw new Error('agentPreset must be a string');
|
|
243
|
-
return success(rpcId, await controller.createAgent(preset));
|
|
244
|
-
}
|
|
245
|
-
if (method === 'studio.agent.dispose') {
|
|
246
|
-
await controller.disposeAgent();
|
|
247
|
-
return success(rpcId, { disposed: true });
|
|
248
|
-
}
|
|
249
|
-
return failure(rpcId, 'studio-method-forbidden', `method ${method} is not exposed by Studio`);
|
|
429
|
+
currentGet() {
|
|
430
|
+
return this.current.view();
|
|
431
|
+
}
|
|
432
|
+
currentPreviewStatus() {
|
|
433
|
+
return this.current.previewStatus();
|
|
434
|
+
}
|
|
435
|
+
currentProjectState() {
|
|
436
|
+
return this.current.project();
|
|
437
|
+
}
|
|
438
|
+
currentContext() {
|
|
439
|
+
return this.current.context();
|
|
440
|
+
}
|
|
441
|
+
currentHarmonyProfile() {
|
|
442
|
+
return this.current.harmonyProfile();
|
|
443
|
+
}
|
|
444
|
+
currentHarmonyInspect(input) {
|
|
445
|
+
return this.current.inspectHarmony(input);
|
|
446
|
+
}
|
|
447
|
+
currentReadDependencySource(input) {
|
|
448
|
+
return this.current.readDependencySource(input.package, input.file);
|
|
449
|
+
}
|
|
450
|
+
currentPreviewUpdate(input) {
|
|
451
|
+
return this.current.updatePreview(this.parsePreviewStatus(input));
|
|
452
|
+
}
|
|
453
|
+
currentResolveSource(input) {
|
|
454
|
+
if (typeof input.source?.file !== 'string')
|
|
455
|
+
throw new Error('source is required');
|
|
456
|
+
return this.current.resolveSource(input.source);
|
|
457
|
+
}
|
|
458
|
+
currentAgentCreate(input) {
|
|
459
|
+
if (input.agentPreset !== undefined && typeof input.agentPreset !== 'string')
|
|
460
|
+
throw new Error('agentPreset must be a string');
|
|
461
|
+
return this.current.createAgent(input.agentPreset);
|
|
462
|
+
}
|
|
463
|
+
currentAgentAttach(input) {
|
|
464
|
+
if (typeof input.sessionId !== 'string' || input.sessionId.trim() === '')
|
|
465
|
+
throw new Error('sessionId is required');
|
|
466
|
+
if ([...this.controllers.values()].some(controller => controller.view().agent?.sessionId === input.sessionId)) {
|
|
467
|
+
throw new Error('the selected session is already attached to a Draft');
|
|
250
468
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
469
|
+
return this.current.attachAgent(input.sessionId);
|
|
470
|
+
}
|
|
471
|
+
currentAgentLeave() {
|
|
472
|
+
return this.current.leaveAgent();
|
|
473
|
+
}
|
|
474
|
+
draftsList() {
|
|
475
|
+
return this.list();
|
|
476
|
+
}
|
|
477
|
+
draftsCreate(input) {
|
|
478
|
+
return this.create(input);
|
|
479
|
+
}
|
|
480
|
+
async workspaceGet() {
|
|
481
|
+
const records = await this.registry.list();
|
|
482
|
+
return this.workspace.read(records.map(record => record.id));
|
|
483
|
+
}
|
|
484
|
+
async workspaceUpdate(input) {
|
|
485
|
+
const records = await this.registry.list();
|
|
486
|
+
return this.workspace.write(input, records.map(record => record.id));
|
|
487
|
+
}
|
|
488
|
+
async harmonyProfile(input) {
|
|
489
|
+
return (await this.controller(draftId(input))).profile();
|
|
490
|
+
}
|
|
491
|
+
async harmonyInspect(input) {
|
|
492
|
+
const controller = await this.controller(draftId(input));
|
|
493
|
+
return controller.inspectHarmony({
|
|
494
|
+
...(input.package === undefined ? {} : { package: input.package }),
|
|
495
|
+
...(input.file === undefined ? {} : { file: input.file }),
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
async harmonyUpdateProfile(input) {
|
|
499
|
+
const controller = await this.controller(draftId(input));
|
|
500
|
+
const order = optionalStringList(input.order, 'order');
|
|
501
|
+
const patchOrder = optionalStringList(input.patchOrder, 'patchOrder');
|
|
502
|
+
const disabled = optionalStringList(input.disabled, 'disabled');
|
|
503
|
+
return controller.updateProfile({
|
|
504
|
+
...(order === undefined ? {} : { order }),
|
|
505
|
+
...(patchOrder === undefined ? {} : { patchOrder }),
|
|
506
|
+
...(disabled === undefined ? {} : { disabled }),
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
async draftsRename(input) {
|
|
510
|
+
const controller = await this.controller(draftId(input));
|
|
511
|
+
if (typeof input.label !== 'string')
|
|
512
|
+
throw new Error('Draft name is required');
|
|
513
|
+
controller.record = await this.registry.rename(controller.record.id, input.label);
|
|
514
|
+
return controller.view();
|
|
515
|
+
}
|
|
516
|
+
async draftsExport(input) {
|
|
517
|
+
const controller = await this.controller(draftId(input));
|
|
518
|
+
controller.record = await this.registry.export(controller.record.id);
|
|
519
|
+
return controller.view();
|
|
520
|
+
}
|
|
521
|
+
async draftsStart(input) {
|
|
522
|
+
return (await this.controller(draftId(input))).start();
|
|
523
|
+
}
|
|
524
|
+
async draftsStop(input) {
|
|
525
|
+
return (await this.controller(draftId(input))).stop();
|
|
526
|
+
}
|
|
527
|
+
async projectState(input) {
|
|
528
|
+
return (await this.controller(draftId(input))).refreshProject();
|
|
529
|
+
}
|
|
530
|
+
async projectActivate(input) {
|
|
531
|
+
if (typeof input.graphRev !== 'string')
|
|
532
|
+
throw new Error('graphRev is required');
|
|
533
|
+
return (await this.controller(draftId(input))).activate(input.graphRev);
|
|
534
|
+
}
|
|
535
|
+
async projectFiles(input) {
|
|
536
|
+
const controller = await this.controller(draftId(input));
|
|
537
|
+
return listProjectFiles(controller.record.root);
|
|
538
|
+
}
|
|
539
|
+
async projectReadFile(input) {
|
|
540
|
+
if (typeof input.path !== 'string')
|
|
541
|
+
throw new Error('path is required');
|
|
542
|
+
const controller = await this.controller(draftId(input));
|
|
543
|
+
return { path: input.path, content: await controller.readFile(input.path) };
|
|
544
|
+
}
|
|
545
|
+
async projectWriteFile(input) {
|
|
546
|
+
if (typeof input.path !== 'string' || typeof input.content !== 'string')
|
|
547
|
+
throw new Error('path and content are required');
|
|
548
|
+
const controller = await this.controller(draftId(input));
|
|
549
|
+
await writeProjectFile(controller.record.root, input.path, input.content);
|
|
550
|
+
return { path: input.path, saved: true };
|
|
551
|
+
}
|
|
552
|
+
async elementsStyles(input) {
|
|
553
|
+
return (await this.controller(draftId(input))).readElementStyles();
|
|
554
|
+
}
|
|
555
|
+
async elementsSaveSource(input) {
|
|
556
|
+
return (await this.controller(draftId(input))).saveElementSource(elementStyleSources(input));
|
|
557
|
+
}
|
|
558
|
+
async patchesAnalyzeAutomatic(input) {
|
|
559
|
+
return (await this.controller(draftId(input))).analyzeAutomaticPatch(automaticPatchRequest(input));
|
|
560
|
+
}
|
|
561
|
+
async patchesCreateAutomatic(input) {
|
|
562
|
+
return (await this.controller(draftId(input))).createAutomaticPatch(automaticPatchRequest(input));
|
|
563
|
+
}
|
|
564
|
+
async projectBuild(input, signal) {
|
|
565
|
+
return (await this.controller(draftId(input))).build(signal);
|
|
566
|
+
}
|
|
567
|
+
async projectCancelBuild(input) {
|
|
568
|
+
return { canceled: await (await this.controller(draftId(input))).cancelBuild() };
|
|
569
|
+
}
|
|
570
|
+
async readinessInspect(input) {
|
|
571
|
+
return (await this.controller(draftId(input))).readiness();
|
|
572
|
+
}
|
|
573
|
+
async readinessPack(input) {
|
|
574
|
+
return (await this.controller(draftId(input))).pack();
|
|
575
|
+
}
|
|
576
|
+
async previewStatus(input) {
|
|
577
|
+
return (await this.controller(draftId(input))).previewStatus();
|
|
578
|
+
}
|
|
579
|
+
async previewUpdate(input) {
|
|
580
|
+
return (await this.controller(draftId(input))).updatePreview(this.parsePreviewStatus(input));
|
|
581
|
+
}
|
|
582
|
+
async previewResolveSource(input) {
|
|
583
|
+
if (typeof input.source?.file !== 'string')
|
|
584
|
+
throw new Error('source is required');
|
|
585
|
+
return (await this.controller(draftId(input))).resolveSource(input.source);
|
|
586
|
+
}
|
|
587
|
+
async agentCreate(input) {
|
|
588
|
+
if (input.agentPreset !== undefined && typeof input.agentPreset !== 'string')
|
|
589
|
+
throw new Error('agentPreset must be a string');
|
|
590
|
+
return (await this.controller(draftId(input))).createAgent(input.agentPreset);
|
|
591
|
+
}
|
|
592
|
+
async agentAttach(input) {
|
|
593
|
+
const controller = await this.controller(draftId(input));
|
|
594
|
+
if (typeof input.sessionId !== 'string' || input.sessionId.trim() === '')
|
|
595
|
+
throw new Error('sessionId is required');
|
|
596
|
+
if (this.current.view().agent?.sessionId === input.sessionId) {
|
|
597
|
+
throw new Error('the selected session is already attached to the current instance');
|
|
255
598
|
}
|
|
599
|
+
const other = [...this.controllers.entries()].find(([id, candidate]) => (id !== controller.record.id && candidate.view().agent?.sessionId === input.sessionId));
|
|
600
|
+
if (other !== undefined)
|
|
601
|
+
throw new Error('the selected session is already attached to another Draft');
|
|
602
|
+
return controller.attachAgent(input.sessionId);
|
|
603
|
+
}
|
|
604
|
+
async agentLeave(input) {
|
|
605
|
+
return (await this.controller(draftId(input))).leaveAgent();
|
|
256
606
|
}
|
|
257
607
|
async dispose() {
|
|
258
608
|
await Promise.all([...this.controllerCreations.values()].map(creation => creation.catch(() => undefined)));
|
|
259
|
-
await Promise.all([...this.controllers.values()].map(controller => controller.dispose()));
|
|
609
|
+
await Promise.all([this.current.dispose(), ...[...this.controllers.values()].map(controller => controller.dispose())]);
|
|
260
610
|
this.controllers.clear();
|
|
261
611
|
this.controllerCreations.clear();
|
|
262
612
|
}
|
|
@@ -272,6 +622,7 @@ export class StudioBackend {
|
|
|
272
622
|
if ((candidate.profileMode !== 'main-home' && candidate.profileMode !== 'custom')
|
|
273
623
|
|| typeof candidate.source !== 'object' || candidate.source === null
|
|
274
624
|
|| (candidate.source.kind !== 'new' && candidate.source.kind !== 'existing')
|
|
625
|
+
|| (candidate.profileDirectory !== undefined && typeof candidate.profileDirectory !== 'string')
|
|
275
626
|
|| (candidate.destinationDirectory !== undefined && typeof candidate.destinationDirectory !== 'string')) {
|
|
276
627
|
throw new Error('Draft source and profileMode are invalid');
|
|
277
628
|
}
|
|
@@ -296,11 +647,11 @@ export class StudioBackend {
|
|
|
296
647
|
}
|
|
297
648
|
}
|
|
298
649
|
makeController(record) {
|
|
299
|
-
const controller = new StudioDraftController(record, this.harmony.
|
|
650
|
+
const controller = new StudioDraftController(record, this.harmony.profile().dir, this.parentOrigin, this.commands, HARMONY_BIN_ENTRY, this.agents, this.subprocess);
|
|
300
651
|
this.controllers.set(record.id, controller);
|
|
301
652
|
return controller;
|
|
302
653
|
}
|
|
303
|
-
|
|
654
|
+
parsePreviewStatus(payload) {
|
|
304
655
|
const candidate = objectPayload(payload);
|
|
305
656
|
if (typeof candidate.connected !== 'boolean' || (candidate.mode !== 'browse' && candidate.mode !== 'inspect')
|
|
306
657
|
|| (candidate.graphRev !== undefined && typeof candidate.graphRev !== 'string')) {
|