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.
Files changed (51) hide show
  1. package/PRODUCT.md +11 -7
  2. package/README.md +69 -21
  3. package/README.zh-CN.md +64 -20
  4. package/dist/bridge.js +10 -10
  5. package/dist/studio.css +1 -1
  6. package/dist/studio.js +16691 -10202
  7. package/docs/bidirectional-connection-handoff.md +729 -0
  8. package/docs/harmony-api-requirements.md +17 -13
  9. package/docs/remote-development.md +80 -0
  10. package/lib/bridge/element-style-selector.d.ts +1 -0
  11. package/lib/bridge/element-style-selector.js +53 -0
  12. package/lib/contracts.d.ts +152 -83
  13. package/lib/contracts.js +0 -2
  14. package/lib/host/agent.d.ts +15 -13
  15. package/lib/host/agent.js +213 -56
  16. package/lib/host/automatic-patch.d.ts +9 -0
  17. package/lib/host/automatic-patch.js +433 -0
  18. package/lib/host/backend.d.ts +134 -4
  19. package/lib/host/backend.js +465 -114
  20. package/lib/host/drafts.d.ts +1 -1
  21. package/lib/host/drafts.js +65 -15
  22. package/lib/host/element-source.d.ts +9 -0
  23. package/lib/host/element-source.js +295 -0
  24. package/lib/host/mcp.d.ts +4 -0
  25. package/lib/host/mcp.js +97 -0
  26. package/lib/host/preview-draft.d.ts +22 -0
  27. package/lib/host/preview-draft.js +162 -0
  28. package/lib/host/preview-port.d.ts +8 -0
  29. package/lib/host/preview-port.js +32 -0
  30. package/lib/host/preview-worker.d.ts +65 -2
  31. package/lib/host/preview-worker.js +203 -76
  32. package/lib/host/preview.d.ts +26 -5
  33. package/lib/host/preview.js +130 -49
  34. package/lib/host/readiness.d.ts +2 -2
  35. package/lib/host/readiness.js +14 -17
  36. package/lib/host/routes.d.ts +1 -7
  37. package/lib/host/routes.js +41 -50
  38. package/lib/host/runtime-profile.d.ts +2 -1
  39. package/lib/host/runtime-profile.js +30 -8
  40. package/lib/host/source-resolution.d.ts +11 -1
  41. package/lib/host/source-resolution.js +69 -24
  42. package/lib/host/studio-service.d.ts +129 -0
  43. package/lib/host/studio-service.js +53 -0
  44. package/lib/index.d.ts +5 -0
  45. package/lib/index.js +64 -30
  46. package/lib/studio-remote.d.ts +126 -0
  47. package/lib/studio-remote.js +188 -0
  48. package/lib/variable-tree.d.ts +2 -0
  49. package/lib/variable-tree.js +13 -0
  50. package/package.json +62 -25
  51. package/studio.patch.yml +12 -0
@@ -0,0 +1,433 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { unlink } from 'node:fs/promises';
3
+ import { basename, dirname, extname, join, posix } from 'node:path';
4
+ import ts from 'typescript';
5
+ import { readProjectFile, writeProjectFile } from './project-files.js';
6
+ import { compileElementStyleSelector } from '../bridge/element-style-selector.js';
7
+ function digest(value, length = 12) {
8
+ return createHash('sha256').update(JSON.stringify(value)).digest('hex').slice(0, length);
9
+ }
10
+ function identifier(value) {
11
+ const result = value.replace(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, '').toLowerCase();
12
+ return result === '' ? 'target' : result.slice(0, 32);
13
+ }
14
+ function excerpt(source, start, end) {
15
+ const lineStart = source.lastIndexOf('\n', start - 1) + 1;
16
+ const nextLine = source.indexOf('\n', end);
17
+ const lineEnd = nextLine === -1 ? source.length : nextLine;
18
+ const value = source.slice(lineStart, lineEnd).trim();
19
+ return value.length <= 240 ? value : `${value.slice(0, 237)}...`;
20
+ }
21
+ function match(sourceFile, source, node, applicable, reason) {
22
+ const start = node.getStart(sourceFile);
23
+ const location = sourceFile.getLineAndCharacterOfPosition(start);
24
+ return {
25
+ line: location.line + 1,
26
+ column: location.character + 1,
27
+ excerpt: excerpt(source, start, node.getEnd()),
28
+ applicable,
29
+ ...(reason === undefined ? {} : { reason }),
30
+ };
31
+ }
32
+ function cssVariable(value) {
33
+ if (typeof value !== 'object' || value === null)
34
+ return false;
35
+ const variable = value;
36
+ return typeof variable.id === 'string' && /^[A-Za-z_][A-Za-z0-9_-]*$/.test(variable.id)
37
+ && typeof variable.label === 'string' && variable.label !== ''
38
+ && typeof variable.property === 'string' && /^(?:--)?[a-zA-Z][a-zA-Z0-9-]*$/.test(variable.property)
39
+ && ['color', 'length', 'number', 'enum', 'string'].includes(variable.control ?? '')
40
+ && (typeof variable.value === 'string' || (typeof variable.value === 'number' && Number.isFinite(variable.value)))
41
+ && (variable.options === undefined || (Array.isArray(variable.options) && variable.options.length > 0 && variable.options.every(item => typeof item === 'string')))
42
+ && (variable.constraints === undefined || Object.values(variable.constraints).every(item => typeof item === 'number' && Number.isFinite(item)));
43
+ }
44
+ function validateCssRequest(request) {
45
+ if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(request.component))
46
+ throw new Error('automatic CSS Patch component name is invalid');
47
+ if (!/\.(?:[cm]?[jt]sx?)$/.test(request.clientFile))
48
+ throw new Error('automatic CSS Patch client source must be a JavaScript or TypeScript file');
49
+ compileElementStyleSelector(request.selector, '[data-dsh-studio-root]');
50
+ if (request.boundary.surfaceId === '' || request.boundary.path.length === 0 || request.boundary.path.some(item => item === '')) {
51
+ throw new Error('automatic CSS Patch boundary is invalid');
52
+ }
53
+ if (request.targetSelector !== undefined
54
+ && (request.targetSelector.trim() === '' || request.targetSelector.length > 2_000 || /[{};]/.test(request.targetSelector))) {
55
+ throw new Error('automatic CSS Patch target selector is invalid');
56
+ }
57
+ if (!/^[A-Za-z_][A-Za-z0-9_-]*$/.test(request.elementId))
58
+ throw new Error('automatic CSS Patch element id is invalid');
59
+ if (request.elementLabel.trim() === '')
60
+ throw new Error('automatic CSS Patch element label must not be empty');
61
+ if (request.variables.length === 0)
62
+ throw new Error('automatic CSS Patch requires at least one variable');
63
+ const ids = new Set();
64
+ const properties = new Set();
65
+ for (const variable of request.variables) {
66
+ if (!cssVariable(variable))
67
+ throw new Error(`automatic CSS Patch variable ${JSON.stringify(variable.id)} is invalid`);
68
+ if (ids.has(variable.id))
69
+ throw new Error(`automatic CSS Patch variable ${JSON.stringify(variable.id)} is duplicated`);
70
+ if (properties.has(variable.property))
71
+ throw new Error(`automatic CSS Patch property ${JSON.stringify(variable.property)} is duplicated`);
72
+ ids.add(variable.id);
73
+ properties.add(variable.property);
74
+ }
75
+ }
76
+ function validateContentRequest(request) {
77
+ if (!/\.(?:[cm]?[jt]sx?)$/.test(request.clientFile))
78
+ throw new Error('automatic content Patch client source must be a JavaScript or TypeScript file');
79
+ compileElementStyleSelector(request.selector, '[data-dsh-studio-root]');
80
+ if (request.boundary.surfaceId === '' || request.boundary.path.length === 0 || request.boundary.path.some(item => item === '')) {
81
+ throw new Error('automatic content Patch boundary is invalid');
82
+ }
83
+ if (request.targetSelector !== undefined
84
+ && (request.targetSelector.trim() === '' || request.targetSelector.length > 2_000 || /[{};]/.test(request.targetSelector))) {
85
+ throw new Error('automatic content Patch target selector is invalid');
86
+ }
87
+ if (!/^[A-Za-z_][A-Za-z0-9_-]*$/.test(request.elementId))
88
+ throw new Error('automatic content Patch element id is invalid');
89
+ if (request.elementLabel.trim() === '')
90
+ throw new Error('automatic content Patch element label must not be empty');
91
+ }
92
+ function analyzeTarget(request, target) {
93
+ const sourceFile = ts.createSourceFile(target.file, target.source, ts.ScriptTarget.Latest, true);
94
+ let matches;
95
+ if (request.kind === 'replace-string') {
96
+ matches = [];
97
+ const visit = (node) => {
98
+ if (ts.isStringLiteral(node) && node.text === request.text)
99
+ matches.push(match(sourceFile, target.source, node, true));
100
+ ts.forEachChild(node, visit);
101
+ };
102
+ visit(sourceFile);
103
+ }
104
+ else {
105
+ matches = [];
106
+ const visit = (node) => {
107
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === request.component) {
108
+ matches.push(match(sourceFile, target.source, node, node.initializer !== undefined, node.initializer === undefined ? 'component variable declaration has no initializer' : undefined));
109
+ }
110
+ else if (ts.isFunctionDeclaration(node) && node.name?.text === request.component) {
111
+ matches.push(match(sourceFile, target.source, node, node.body !== undefined, node.body === undefined ? 'component function declaration has no body' : undefined));
112
+ }
113
+ ts.forEachChild(node, visit);
114
+ };
115
+ visit(sourceFile);
116
+ }
117
+ return { package: target.package, file: target.file, version: target.version, matches };
118
+ }
119
+ function patchId(request, target) {
120
+ return `auto-${request.kind === 'css-style' ? 'css' : 'replace'}-${identifier(target.package)}-${digest({ request, target: { package: target.package, file: target.file, version: target.version } })}`;
121
+ }
122
+ function stringPatch(request, target, id) {
123
+ return ` {
124
+ id: ${JSON.stringify(id)},
125
+ target: { package: ${JSON.stringify(target.package)}, version: ${JSON.stringify(target.version)}, file: ${JSON.stringify(target.file)} },
126
+ select: ${JSON.stringify(`StringLiteral[text=${JSON.stringify(request.text)}]`)},
127
+ expect: ${target.matches.length},
128
+ apply({ node, sourceFile, edit }) {
129
+ edit.overwrite(node.getStart(sourceFile), node.getEnd(), JSON.stringify(AUTO_CONTENT))
130
+ },
131
+ }`;
132
+ }
133
+ function cssPatch(request, target, id, owner) {
134
+ return ` component({
135
+ id: ${JSON.stringify(id)},
136
+ target: { package: ${JSON.stringify(target.package)}, version: ${JSON.stringify(target.version)}, file: ${JSON.stringify(target.file)} },
137
+ select: { name: ${JSON.stringify(request.component)} },
138
+ expect: ${target.matches.length},
139
+ operation: { kind: 'decorate', with: { module: ${JSON.stringify(owner)}, export: AUTO_EXPORT } },
140
+ })`;
141
+ }
142
+ function cssClientSource(request, owner, id, file, exportName) {
143
+ const definitions = request.variables.map(variable => `{
144
+ kind: 'variable', id: ${JSON.stringify(variable.id)}, label: ${JSON.stringify(variable.label)}, control: ${JSON.stringify(variable.control)},
145
+ ${variable.options === undefined ? '' : `options: ${JSON.stringify(variable.options)},`}
146
+ ${variable.constraints === undefined ? '' : `constraints: ${JSON.stringify(variable.constraints)},`}
147
+ defaultSource: { file: ${JSON.stringify(file)}, before: ${JSON.stringify(` ${JSON.stringify(variable.id)}: /* dsh-studio-default:${id}:${variable.id} */ `)}, after: ${JSON.stringify(',\n')} },
148
+ }`).join(',\n');
149
+ const defaults = request.variables.map(variable => ` ${JSON.stringify(variable.id)}: /* dsh-studio-default:${id}:${variable.id} */ ${JSON.stringify(variable.value)},`).join('\n');
150
+ const properties = JSON.stringify(request.variables.map(item => ({ id: item.id, property: item.property })));
151
+ const root = `[data-ui-surface=${JSON.stringify(request.boundary.surfaceId)}][data-ui-surface-path=${JSON.stringify(JSON.stringify(request.boundary.path))}]`;
152
+ const selector = compileElementStyleSelector(request.selector, root);
153
+ return `import { registerStudioElement } from 'dsh-harmony-react/studio'
154
+
155
+ const targetSelector = ${request.targetSelector === undefined ? 'undefined' : JSON.stringify(request.targetSelector)}
156
+ const boundarySurface = ${JSON.stringify(request.boundary.surfaceId)}
157
+ const boundaryPath = ${JSON.stringify(JSON.stringify(request.boundary.path))}
158
+ const values = {
159
+ ${defaults}
160
+ }
161
+ const declarations = ${properties}
162
+ let styleElement
163
+ let targetObserver
164
+
165
+ function markTargets() {
166
+ if (targetSelector === undefined) return
167
+ for (const element of document.querySelectorAll(targetSelector)) {
168
+ if (element.hasAttribute('data-ui-surface') || element.hasAttribute('data-ui-surface-path')) continue
169
+ element.setAttribute('data-ui-surface', boundarySurface)
170
+ element.setAttribute('data-ui-surface-path', boundaryPath)
171
+ }
172
+ }
173
+
174
+ function applyStyles() {
175
+ if (typeof document === 'undefined') return
176
+ markTargets()
177
+ if (styleElement === undefined) {
178
+ styleElement = document.createElement('style')
179
+ styleElement.dataset.plugin = ${JSON.stringify(owner)}
180
+ document.head.append(styleElement)
181
+ }
182
+ styleElement.textContent = ''
183
+ const sheet = styleElement.sheet
184
+ if (sheet === null) return
185
+ while (sheet.cssRules.length > 0) sheet.deleteRule(0)
186
+ const index = sheet.insertRule(${JSON.stringify(`${selector} {}`)}, 0)
187
+ const rule = sheet.cssRules[index]
188
+ if (!(rule instanceof CSSStyleRule)) return
189
+ for (const declaration of declarations) rule.style.setProperty(declaration.property, String(values[declaration.id]))
190
+ }
191
+
192
+ const bindings = Object.fromEntries(declarations.map(declaration => [declaration.id, {
193
+ get: () => values[declaration.id],
194
+ set: value => { values[declaration.id] = value; applyStyles() },
195
+ }]))
196
+
197
+ registerStudioElement({
198
+ owner: ${JSON.stringify(owner)},
199
+ element: {
200
+ id: ${JSON.stringify(request.elementId)}, label: ${JSON.stringify(request.elementLabel)},
201
+ boundary: ${JSON.stringify(request.boundary)}, source: { file: ${JSON.stringify(file)} },
202
+ variables: [{ kind: 'group', id: 'css', label: 'CSS', children: [
203
+ ${definitions}
204
+ ] }],
205
+ },
206
+ bindings,
207
+ })
208
+ applyStyles()
209
+ if (targetSelector !== undefined) {
210
+ targetObserver = new MutationObserver(markTargets)
211
+ targetObserver.observe(document.documentElement, { childList: true, subtree: true })
212
+ }
213
+
214
+ export function ${exportName}(Original) {
215
+ return Original
216
+ }
217
+ `;
218
+ }
219
+ function contentClientSource(request, owner, id, providerFile, file, exportName) {
220
+ const root = `[data-ui-surface=${JSON.stringify(request.boundary.surfaceId)}][data-ui-surface-path=${JSON.stringify(JSON.stringify(request.boundary.path))}]`;
221
+ const selector = compileElementStyleSelector(request.selector, root);
222
+ const variableId = `text_${id.slice('auto-content-'.length)}`;
223
+ const groupId = `content_${id.slice('auto-content-'.length)}`;
224
+ const sourceFile = request.elementSourceFile ?? file;
225
+ return `import { registerStudioElement } from 'dsh-harmony-react/studio'
226
+
227
+ const targetSelector = ${request.targetSelector === undefined ? 'undefined' : JSON.stringify(request.targetSelector)}
228
+ const boundarySurface = ${JSON.stringify(request.boundary.surfaceId)}
229
+ const boundaryPath = ${JSON.stringify(JSON.stringify(request.boundary.path))}
230
+ let value = ${JSON.stringify(request.replacement)}
231
+ let initialized = false
232
+ const listeners = new Set()
233
+
234
+ function notify() {
235
+ for (const listener of listeners) listener()
236
+ }
237
+
238
+ function markTargets() {
239
+ if (targetSelector === undefined) return
240
+ for (const element of document.querySelectorAll(targetSelector)) {
241
+ if (element.hasAttribute('data-ui-surface') || element.hasAttribute('data-ui-surface-path')) continue
242
+ element.setAttribute('data-ui-surface', boundarySurface)
243
+ element.setAttribute('data-ui-surface-path', boundaryPath)
244
+ }
245
+ }
246
+
247
+ function syncText() {
248
+ if (typeof document === 'undefined') return
249
+ markTargets()
250
+ const elements = [...document.querySelectorAll(${JSON.stringify(selector)})]
251
+ if (!initialized && elements[0] !== undefined) {
252
+ value = elements[0].textContent ?? value
253
+ initialized = true
254
+ notify()
255
+ }
256
+ for (const element of elements) if (element.textContent !== value) element.textContent = value
257
+ }
258
+
259
+ registerStudioElement({
260
+ owner: ${JSON.stringify(owner)},
261
+ element: {
262
+ id: ${JSON.stringify(request.elementId)}, label: ${JSON.stringify(request.elementLabel)},
263
+ boundary: ${JSON.stringify(request.boundary)}, source: { file: ${JSON.stringify(sourceFile)} },
264
+ variables: [{ kind: 'group', id: ${JSON.stringify(groupId)}, label: 'Content', children: [{
265
+ kind: 'variable', id: ${JSON.stringify(variableId)}, label: 'Text', control: 'string',
266
+ defaultSource: { file: ${JSON.stringify(providerFile)}, before: ${JSON.stringify(`const AUTO_CONTENT = /* dsh-studio-default:${id}:text */ `)}, after: ${JSON.stringify(';\n')} },
267
+ }] }],
268
+ },
269
+ bindings: { ${JSON.stringify(variableId)}: {
270
+ get: () => value,
271
+ set: next => { value = String(next); initialized = true; syncText(); notify() },
272
+ subscribe: listener => { listeners.add(listener); return () => listeners.delete(listener) },
273
+ } },
274
+ })
275
+ syncText()
276
+ const observer = new MutationObserver(syncText)
277
+ observer.observe(document.documentElement, { childList: true, subtree: true, characterData: true })
278
+
279
+ export const ${exportName} = true
280
+ `;
281
+ }
282
+ function providerSource(request, targets, owner, defaultId) {
283
+ const applicable = targets.filter(target => target.matches.length > 0 && target.matches.every(item => item.applicable));
284
+ const patchIds = applicable.map(target => patchId(request, target));
285
+ const declarations = applicable.map((target, index) => request.kind === 'replace-string'
286
+ ? stringPatch(request, target, patchIds[index])
287
+ : cssPatch(request, target, patchIds[index], owner)).join(',\n');
288
+ const prefix = request.kind === 'css-style'
289
+ ? `'use strict'\n\nconst { component } = require('dsh-harmony-react')\nconst AUTO_EXPORT = ${JSON.stringify(`DshStudioAuto${digest(request, 10)}`)}\n\n`
290
+ : `'use strict'\n\nconst AUTO_CONTENT = /* dsh-studio-default:${defaultId}:text */ ${JSON.stringify(request.replacement)};\n\n`;
291
+ return { patchIds, source: `${prefix}module.exports = [\n${declarations}\n]\n` };
292
+ }
293
+ export function analyzeAutomaticPatch(request, sources, owner) {
294
+ if (request.kind === 'replace-string' && request.text === '')
295
+ throw new Error('automatic string Patch text must not be empty');
296
+ if (request.targets.length === 0)
297
+ throw new Error('automatic Patch requires at least one target');
298
+ if (request.targets.length !== sources.length)
299
+ throw new Error('automatic Patch target sources are incomplete');
300
+ if (request.kind === 'css-style')
301
+ validateCssRequest(request);
302
+ else
303
+ validateContentRequest(request);
304
+ const identities = request.targets.map(target => `${target.package}\0${target.file}`);
305
+ if (new Set(identities).size !== identities.length)
306
+ throw new Error('automatic Patch targets must be unique');
307
+ for (let index = 0; index < sources.length; index += 1) {
308
+ const target = request.targets[index];
309
+ const source = sources[index];
310
+ if (source.package !== target.package || source.file !== target.file)
311
+ throw new Error('automatic Patch source does not match its requested target');
312
+ }
313
+ const targets = sources.map(source => analyzeTarget(request, source));
314
+ const file = `patch.auto-${digest({ request, owner, versions: targets.map(target => target.version) })}.cjs`;
315
+ const suffix = digest({ request, owner }, 10);
316
+ const defaultId = `auto-${request.kind === 'css-style' ? 'css' : 'content'}-${suffix}`;
317
+ const generated = providerSource(request, targets, owner, defaultId);
318
+ const extension = extname(request.clientFile);
319
+ const stem = basename(request.clientFile, extension);
320
+ const generatedFile = posix.join(dirname(request.clientFile).split('\\').join('/'), `${stem}.dsh-studio-auto-${suffix}.js`);
321
+ const elementId = request.kind === 'replace-string' && request.elementSourceFile !== undefined
322
+ ? request.elementId : `${request.elementId}-${suffix}`;
323
+ const client = request.kind === 'css-style'
324
+ ? {
325
+ file: generatedFile,
326
+ source: cssClientSource({ ...request, elementId }, owner, defaultId, generatedFile, `DshStudioAuto${digest(request, 10)}`),
327
+ export: `DshStudioAuto${digest(request, 10)}`,
328
+ entryFile: request.clientFile,
329
+ }
330
+ : {
331
+ file: generatedFile,
332
+ source: contentClientSource({ ...request, elementId }, owner, defaultId, file, generatedFile, `DshStudioContent${digest(request, 10)}`),
333
+ export: `DshStudioContent${digest(request, 10)}`,
334
+ entryFile: request.clientFile,
335
+ };
336
+ return {
337
+ request,
338
+ targets,
339
+ canApply: generated.patchIds.length > 0,
340
+ provider: { file, source: generated.source, patchIds: generated.patchIds },
341
+ client,
342
+ };
343
+ }
344
+ export async function writeAutomaticPatch(root, plan) {
345
+ if (!plan.canApply || plan.provider.patchIds.length === 0)
346
+ throw new Error('automatic Patch has no matches to apply');
347
+ try {
348
+ await readProjectFile(root, plan.provider.file);
349
+ throw new Error(`automatic Patch provider ${JSON.stringify(plan.provider.file)} already exists`);
350
+ }
351
+ catch (error) {
352
+ if (error.code !== 'ENOENT')
353
+ throw error;
354
+ }
355
+ const manifestSource = await readProjectFile(root, 'package.json');
356
+ const manifest = JSON.parse(manifestSource);
357
+ const current = manifest.dsh?.harmony?.patches;
358
+ if (!Array.isArray(current) || current.some(value => typeof value !== 'string'))
359
+ throw new Error('Draft package.json must declare dsh.harmony.patches as an array of file paths');
360
+ const declaration = `./${plan.provider.file}`;
361
+ if (current.includes(declaration))
362
+ throw new Error('automatic Patch provider is already declared');
363
+ let clientEntrySource;
364
+ let clientEntryNext;
365
+ if (plan.client !== undefined) {
366
+ clientEntrySource = await readProjectFile(root, plan.client.entryFile);
367
+ if (clientEntrySource.includes('__ModuleLoader__.load')) {
368
+ throw new Error('automatic Component Patch requires the Draft client source before it is bundled');
369
+ }
370
+ const relative = posix.relative(posix.dirname(plan.client.entryFile), plan.client.file);
371
+ const specifier = relative.startsWith('.') ? relative : `./${relative}`;
372
+ const reexport = `export { ${plan.client.export} } from ${JSON.stringify(specifier)}`;
373
+ if (clientEntrySource.includes(reexport))
374
+ throw new Error('automatic Patch client export is already declared');
375
+ clientEntryNext = `${clientEntrySource.trimEnd()}\n\n${reexport}\n`;
376
+ }
377
+ const nextManifest = {
378
+ ...manifest,
379
+ dependencies: plan.client === undefined ? manifest.dependencies : { ...manifest.dependencies, 'dsh-harmony-react': '^0.3.0' },
380
+ devDependencies: plan.request.kind !== 'css-style' || manifest.devDependencies?.tsdown !== '0.22.14'
381
+ || manifest.dependencies?.['@tsdown/css'] !== undefined || manifest.devDependencies['@tsdown/css'] !== undefined
382
+ ? manifest.devDependencies
383
+ : { ...manifest.devDependencies, '@tsdown/css': manifest.devDependencies.tsdown },
384
+ dsh: {
385
+ ...manifest.dsh,
386
+ ...(plan.client === undefined ? {} : { client: { ...manifest.dsh?.client, immediately: true } }),
387
+ harmony: { ...manifest.dsh?.harmony, patches: [...current, declaration] },
388
+ },
389
+ };
390
+ const writes = [
391
+ { file: plan.provider.file, content: plan.provider.source, created: true },
392
+ ...(plan.client === undefined ? [] : [
393
+ { file: plan.client.file, content: plan.client.source, created: true },
394
+ { file: plan.client.entryFile, content: clientEntryNext, original: clientEntrySource, created: false },
395
+ ]),
396
+ { file: 'package.json', content: `${JSON.stringify(nextManifest, null, 2)}\n`, original: manifestSource, created: false },
397
+ ];
398
+ const written = [];
399
+ try {
400
+ for (const write of writes) {
401
+ if (write.created) {
402
+ try {
403
+ await readProjectFile(root, write.file);
404
+ throw new Error(`automatic Patch file ${JSON.stringify(write.file)} already exists`);
405
+ }
406
+ catch (error) {
407
+ if (error.code !== 'ENOENT')
408
+ throw error;
409
+ }
410
+ }
411
+ await writeProjectFile(root, write.file, write.content);
412
+ written.push(write);
413
+ }
414
+ }
415
+ catch (error) {
416
+ const rollbackErrors = [];
417
+ for (const write of written.reverse()) {
418
+ try {
419
+ if (write.created)
420
+ await unlink(join(root, write.file));
421
+ else
422
+ await writeProjectFile(root, write.file, write.original);
423
+ }
424
+ catch (rollbackError) {
425
+ rollbackErrors.push(rollbackError);
426
+ }
427
+ }
428
+ if (rollbackErrors.length > 0)
429
+ throw new AggregateError([error, ...rollbackErrors], 'automatic Patch write rollback failed');
430
+ throw error;
431
+ }
432
+ return { ...plan, files: writes.map(item => item.file) };
433
+ }
@@ -1,6 +1,6 @@
1
1
  import type { AgentRegistry } from '@deepseek-ai/dsh-agent';
2
2
  import type { SubprocessRuntime } from '@deepseek-ai/dsh-subprocess';
3
- import type { StudioClientRequest, StudioHarmonyService, StudioServerResponse } from '../contracts.js';
3
+ import type { StudioBuildResult, StudioAgentBinding, StudioAgentContext, StudioAutomaticPatchPlan, StudioAutomaticPatchRequest, StudioAutomaticPatchWriteResult, StudioCreateDraftInput, StudioCurrentInstanceView, StudioDraftView, StudioHarmonyInspection, StudioHarmonyProfile, StudioHarmonyProfileUpdateResult, StudioHarmonyService, StudioElementStyleSource, StudioPreviewStatus, StudioPreviewUpdate, StudioProjectFile, StudioProjectState, StudioReadinessReport, StudioSourceLocation, StudioWorkspaceState } from '../contracts.js';
4
4
  import type { StudioCommandRunner, StudioDraftRegistry } from './drafts.js';
5
5
  import type { StudioWorkspaceStore } from './workspace.js';
6
6
  /** Stable-Host control plane for persistent, isolated Draft Preview runtimes. */
@@ -14,12 +14,142 @@ export declare class StudioBackend {
14
14
  private readonly parentOrigin;
15
15
  private readonly controllers;
16
16
  private readonly controllerCreations;
17
- constructor(harmony: StudioHarmonyService, agents: AgentRegistry, subprocess: SubprocessRuntime, registry: StudioDraftRegistry, workspace: StudioWorkspaceStore, commands: StudioCommandRunner, parentOrigin: string);
18
- call(message: StudioClientRequest): Promise<StudioServerResponse>;
17
+ private readonly current;
18
+ constructor(harmony: StudioHarmonyService, agents: AgentRegistry, subprocess: SubprocessRuntime, registry: StudioDraftRegistry, workspace: StudioWorkspaceStore, commands: StudioCommandRunner, parentOrigin: string, currentBridgeCapability?: string);
19
+ currentGet(): StudioCurrentInstanceView;
20
+ currentPreviewStatus(): StudioPreviewStatus;
21
+ currentProjectState(): StudioProjectState;
22
+ currentContext(): Promise<StudioAgentContext>;
23
+ currentHarmonyProfile(): Promise<import("dsh-harmony").HarmonyProfileView>;
24
+ currentHarmonyInspect(input: {
25
+ package?: string;
26
+ file?: string;
27
+ }): Promise<StudioHarmonyInspection>;
28
+ currentReadDependencySource(input: {
29
+ package: string;
30
+ file: string;
31
+ }): Promise<string>;
32
+ currentPreviewUpdate(input: StudioPreviewUpdate): StudioPreviewStatus;
33
+ currentResolveSource(input: {
34
+ source: StudioSourceLocation;
35
+ }): Promise<import("../contracts.js").StudioSourceCandidate>;
36
+ currentAgentCreate(input: {
37
+ agentPreset?: string;
38
+ }): Promise<StudioAgentBinding>;
39
+ currentAgentAttach(input: {
40
+ sessionId: string;
41
+ }): Promise<StudioAgentBinding>;
42
+ currentAgentLeave(): Promise<StudioCurrentInstanceView>;
43
+ draftsList(): Promise<StudioDraftView[]>;
44
+ draftsCreate(input: StudioCreateDraftInput): Promise<StudioDraftView>;
45
+ workspaceGet(): Promise<StudioWorkspaceState>;
46
+ workspaceUpdate(input: StudioWorkspaceState): Promise<StudioWorkspaceState>;
47
+ harmonyProfile(input: {
48
+ draftId: string;
49
+ }): Promise<StudioHarmonyProfile>;
50
+ harmonyInspect(input: {
51
+ draftId: string;
52
+ package?: string;
53
+ file?: string;
54
+ }): Promise<StudioHarmonyInspection>;
55
+ harmonyUpdateProfile(input: {
56
+ draftId: string;
57
+ order?: string[];
58
+ patchOrder?: string[];
59
+ disabled?: string[];
60
+ }): Promise<StudioHarmonyProfileUpdateResult>;
61
+ draftsRename(input: {
62
+ draftId: string;
63
+ label: string;
64
+ }): Promise<StudioDraftView>;
65
+ draftsExport(input: {
66
+ draftId: string;
67
+ }): Promise<StudioDraftView>;
68
+ draftsStart(input: {
69
+ draftId: string;
70
+ }): Promise<StudioDraftView>;
71
+ draftsStop(input: {
72
+ draftId: string;
73
+ }): Promise<StudioDraftView>;
74
+ projectState(input: {
75
+ draftId: string;
76
+ }): Promise<StudioProjectState>;
77
+ projectActivate(input: {
78
+ draftId: string;
79
+ graphRev: string;
80
+ }): Promise<StudioProjectState>;
81
+ projectFiles(input: {
82
+ draftId: string;
83
+ }): Promise<StudioProjectFile[]>;
84
+ projectReadFile(input: {
85
+ draftId: string;
86
+ path: string;
87
+ }): Promise<{
88
+ path: string;
89
+ content: string;
90
+ }>;
91
+ projectWriteFile(input: {
92
+ draftId: string;
93
+ path: string;
94
+ content: string;
95
+ }): Promise<{
96
+ path: string;
97
+ saved: true;
98
+ }>;
99
+ elementsStyles(input: {
100
+ draftId: string;
101
+ }): Promise<StudioElementStyleSource[]>;
102
+ elementsSaveSource(input: {
103
+ draftId: string;
104
+ styles: StudioElementStyleSource[];
105
+ }): Promise<{
106
+ files: string[];
107
+ }>;
108
+ patchesAnalyzeAutomatic(input: StudioAutomaticPatchRequest & {
109
+ draftId: string;
110
+ }): Promise<StudioAutomaticPatchPlan>;
111
+ patchesCreateAutomatic(input: StudioAutomaticPatchRequest & {
112
+ draftId: string;
113
+ }): Promise<StudioAutomaticPatchWriteResult>;
114
+ projectBuild(input: {
115
+ draftId: string;
116
+ }, signal: AbortSignal): Promise<StudioBuildResult>;
117
+ projectCancelBuild(input: {
118
+ draftId: string;
119
+ }): Promise<{
120
+ canceled: boolean;
121
+ }>;
122
+ readinessInspect(input: {
123
+ draftId: string;
124
+ }): Promise<StudioReadinessReport>;
125
+ readinessPack(input: {
126
+ draftId: string;
127
+ }): Promise<StudioReadinessReport>;
128
+ previewStatus(input: {
129
+ draftId: string;
130
+ }): Promise<StudioPreviewStatus>;
131
+ previewUpdate(input: StudioPreviewUpdate & {
132
+ draftId: string;
133
+ }): Promise<StudioPreviewStatus>;
134
+ previewResolveSource(input: {
135
+ draftId: string;
136
+ source: StudioSourceLocation;
137
+ }): Promise<import("../contracts.js").StudioSourceCandidate>;
138
+ agentCreate(input: {
139
+ draftId: string;
140
+ agentPreset?: string;
141
+ }): Promise<StudioAgentBinding>;
142
+ agentAttach(input: {
143
+ draftId: string;
144
+ sessionId: string;
145
+ }): Promise<StudioAgentBinding>;
146
+ agentLeave(input: {
147
+ draftId: string;
148
+ }): Promise<StudioDraftView>;
19
149
  dispose(): Promise<void>;
20
150
  private list;
21
151
  private create;
22
152
  private controller;
23
153
  private makeController;
24
- private previewStatus;
154
+ private parsePreviewStatus;
25
155
  }