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
@@ -1,4 +1,4 @@
1
- import { lstat, readFile, realpath } from 'node:fs/promises';
1
+ import { lstat, readFile, readdir, realpath } from 'node:fs/promises';
2
2
  import { isAbsolute, join, relative, resolve, sep } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  const MAX_SOURCE_BYTES = 1024 * 1024;
@@ -16,6 +16,16 @@ function relativeSource(path) {
16
16
  }
17
17
  return normalized;
18
18
  }
19
+ function clientExport(manifest) {
20
+ if (typeof manifest.exports !== 'object' || manifest.exports === null)
21
+ return undefined;
22
+ const entry = manifest.exports['./client'];
23
+ const target = typeof entry === 'string' ? entry
24
+ : typeof entry === 'object' && entry !== null && typeof entry.default === 'string'
25
+ ? entry.default
26
+ : undefined;
27
+ return target === undefined ? undefined : relativeSource(target);
28
+ }
19
29
  function sourceReference(file) {
20
30
  const trimmed = file.trim();
21
31
  if (trimmed.startsWith('file:')) {
@@ -54,33 +64,44 @@ async function packageRoot(path, expectedName) {
54
64
  try {
55
65
  const root = await realpath(path);
56
66
  const manifest = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
57
- return manifest.name === expectedName ? { name: expectedName, root, kind: 'dependency' } : undefined;
67
+ const client = clientExport(manifest);
68
+ return manifest.name === expectedName
69
+ ? { name: expectedName, root, kind: 'dependency', ...(client === undefined ? {} : { client }) }
70
+ : undefined;
58
71
  }
59
72
  catch {
60
73
  return undefined;
61
74
  }
62
75
  }
63
- async function packageRoots(draftRoot, profileDir) {
64
- const draft = await realpath(draftRoot);
65
- const manifest = JSON.parse(await readFile(join(profileDir, 'package.json'), 'utf8'));
66
- const dependencies = Object.keys(manifest.dependencies ?? {});
67
- const bundles = manifest.dsh?.profile?.bundles ?? [];
68
- const roots = [{
69
- name: JSON.parse(await readFile(join(draft, 'package.json'), 'utf8')).name,
76
+ async function installedPackageNames(nodeModules) {
77
+ const entries = (await readdir(nodeModules)).filter(name => !name.startsWith('.'));
78
+ const names = await Promise.all(entries.map(async (name) => name.startsWith('@')
79
+ ? readdir(join(nodeModules, name)).then(packages => packages.filter(item => !item.startsWith('.')).map(item => `${name}/${item}`))
80
+ : [name]));
81
+ return names.flat();
82
+ }
83
+ async function packageRoots(draftRoot, nodeModulesDirs) {
84
+ const roots = [];
85
+ if (draftRoot !== undefined) {
86
+ const draft = await realpath(draftRoot);
87
+ const draftManifest = JSON.parse(await readFile(join(draft, 'package.json'), 'utf8'));
88
+ const draftClient = clientExport(draftManifest);
89
+ roots.push({
90
+ name: draftManifest.name,
70
91
  root: draft,
71
92
  kind: 'draft',
72
- }];
73
- for (const name of dependencies) {
74
- const installed = await packageRoot(join(profileDir, 'node_modules', ...name.split('/')), name);
75
- if (installed !== undefined)
76
- roots.push(installed);
93
+ ...(draftClient === undefined ? {} : { client: draftClient }),
94
+ });
77
95
  }
78
- for (const name of bundles.filter(name => !dependencies.includes(name))) {
79
- const installed = await packageRoot(join(profileDir, 'node_modules', ...name.split('/')), name);
80
- if (installed !== undefined)
81
- roots.push(installed);
96
+ for (const nodeModules of nodeModulesDirs) {
97
+ for (const name of await installedPackageNames(nodeModules)) {
98
+ const installed = await packageRoot(join(nodeModules, ...name.split('/')), name);
99
+ if (installed !== undefined)
100
+ roots.push(installed);
101
+ }
82
102
  }
83
- return roots.filter((item, index, all) => all.findIndex(candidate => candidate.root === item.root) === index);
103
+ return roots.filter((item, index, all) => all.findIndex(candidate => candidate.root === item.root
104
+ || item.kind === 'dependency' && candidate.kind === 'dependency' && candidate.name === item.name) === index);
84
105
  }
85
106
  function result(source, file, kind, confidence, packageName) {
86
107
  return {
@@ -104,13 +125,30 @@ async function exactMatch(path, roots) {
104
125
  const root = matches[0];
105
126
  return root === undefined ? undefined : { root, file: posix(relative(root.root, target)) };
106
127
  }
128
+ async function pluginClientMatch(path, roots) {
129
+ const route = path.trim().replace(/[?#].*$/, '');
130
+ const root = roots.find(item => route === `/plugins/${item.name}/client.js` && item.client !== undefined);
131
+ return root?.client === undefined ? undefined : exactMatch(resolve(root.root, root.client), [root]);
132
+ }
107
133
  export class StudioSourceResolver {
134
+ draftRoot;
135
+ profileDir;
136
+ packageDirs;
108
137
  #roots;
109
- constructor(draftRoot, profileDir) {
110
- this.#roots = packageRoots(draftRoot, profileDir);
138
+ constructor(draftRoot, profileDir, packageDirs = []) {
139
+ this.draftRoot = draftRoot;
140
+ this.profileDir = profileDir;
141
+ this.packageDirs = packageDirs;
142
+ }
143
+ roots() {
144
+ return this.#roots ??= packageRoots(this.draftRoot, [join(this.profileDir, 'node_modules'), ...this.packageDirs]);
111
145
  }
112
146
  async resolve(source) {
113
- const roots = await this.#roots;
147
+ const roots = await this.roots();
148
+ const pluginClient = await pluginClientMatch(source.file, roots);
149
+ if (pluginClient !== undefined) {
150
+ return result(source, pluginClient.file, pluginClient.root.kind, 'exact', pluginClient.root.name);
151
+ }
114
152
  const reference = sourceReference(source.file);
115
153
  if (reference.absolute !== undefined) {
116
154
  const match = await exactMatch(reference.absolute, roots);
@@ -130,10 +168,13 @@ export class StudioSourceResolver {
130
168
  return result(source, source.file, reference.generated ? 'generated' : 'unknown', 'candidate');
131
169
  }
132
170
  async readDependency(packageName, file) {
171
+ return (await this.readDependencyTarget(packageName, file)).source;
172
+ }
173
+ async readDependencyTarget(packageName, file) {
133
174
  const relativeFile = relativeSource(file);
134
175
  if (packageName === '' || relativeFile === undefined)
135
176
  throw new Error('dependency source reference is invalid');
136
- const roots = (await this.#roots).filter(root => root.kind === 'dependency' && root.name === packageName);
177
+ const roots = (await this.roots()).filter(root => root.kind === 'dependency' && root.name === packageName);
137
178
  if (roots.length !== 1)
138
179
  throw new Error(`dependency package ${JSON.stringify(packageName)} is not uniquely installed in Preview`);
139
180
  const match = await exactMatch(resolve(roots[0].root, relativeFile), roots);
@@ -149,6 +190,10 @@ export class StudioSourceResolver {
149
190
  const content = await readFile(target);
150
191
  if (content.includes(0))
151
192
  throw new Error('binary dependency sources cannot be read');
152
- return content.toString('utf8');
193
+ const manifest = JSON.parse(await readFile(join(roots[0].root, 'package.json'), 'utf8'));
194
+ if (typeof manifest.version !== 'string' || manifest.version === '') {
195
+ throw new Error(`dependency package ${JSON.stringify(packageName)} does not declare a version`);
196
+ }
197
+ return { package: packageName, file: relativeFile, version: manifest.version, source: content.toString('utf8') };
153
198
  }
154
199
  }
@@ -0,0 +1,129 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
3
+ import type { TypertContribution } from '@deepseek-ai/dsh-typert-registry/types';
4
+ import type { StudioAutomaticPatchRequest, StudioCreateDraftInput, StudioElementStyleSource, StudioPreviewUpdate, StudioSourceLocation, StudioWorkspaceState } from '../contracts.js';
5
+ import { StudioBackend } from './backend.js';
6
+ export declare const STUDIO_LOCAL: TypertContribution;
7
+ export declare class StudioService extends TypertRemoteService {
8
+ private readonly backend;
9
+ constructor(ctx: Context, backend: StudioBackend);
10
+ currentGet(signal: AbortSignal): import("../contracts.js").StudioCurrentInstanceView;
11
+ currentPreviewStatus(signal: AbortSignal): import("../contracts.js").StudioPreviewStatus;
12
+ currentPreviewUpdate(input: StudioPreviewUpdate, signal: AbortSignal): import("../contracts.js").StudioPreviewStatus;
13
+ currentResolveSource(input: {
14
+ source: StudioSourceLocation;
15
+ }, signal: AbortSignal): Promise<import("../contracts.js").StudioSourceCandidate>;
16
+ currentAgentCreate(input: {
17
+ agentPreset?: string;
18
+ }, signal: AbortSignal): Promise<import("../contracts.js").StudioAgentBinding>;
19
+ currentAgentAttach(input: {
20
+ sessionId: string;
21
+ }, signal: AbortSignal): Promise<import("../contracts.js").StudioAgentBinding>;
22
+ currentAgentLeave(signal: AbortSignal): Promise<import("../contracts.js").StudioCurrentInstanceView>;
23
+ draftsList(signal: AbortSignal): Promise<import("../contracts.js").StudioDraftView[]>;
24
+ draftsCreate(input: StudioCreateDraftInput, signal: AbortSignal): Promise<import("../contracts.js").StudioDraftView>;
25
+ draftsRename(input: {
26
+ draftId: string;
27
+ label: string;
28
+ }, signal: AbortSignal): Promise<import("../contracts.js").StudioDraftView>;
29
+ draftsExport(input: {
30
+ draftId: string;
31
+ }, signal: AbortSignal): Promise<import("../contracts.js").StudioDraftView>;
32
+ draftsStart(input: {
33
+ draftId: string;
34
+ }, signal: AbortSignal): Promise<import("../contracts.js").StudioDraftView>;
35
+ draftsStop(input: {
36
+ draftId: string;
37
+ }, signal: AbortSignal): Promise<import("../contracts.js").StudioDraftView>;
38
+ workspaceGet(signal: AbortSignal): Promise<StudioWorkspaceState>;
39
+ workspaceUpdate(input: StudioWorkspaceState, signal: AbortSignal): Promise<StudioWorkspaceState>;
40
+ harmonyProfile(input: {
41
+ draftId: string;
42
+ }, signal: AbortSignal): Promise<import("../contracts.js").StudioHarmonyProfile>;
43
+ harmonyInspect(input: {
44
+ draftId: string;
45
+ package?: string;
46
+ file?: string;
47
+ }, signal: AbortSignal): Promise<import("dsh-harmony").HarmonyInspection>;
48
+ harmonyUpdateProfile(input: {
49
+ draftId: string;
50
+ order?: string[];
51
+ patchOrder?: string[];
52
+ disabled?: string[];
53
+ }, signal: AbortSignal): Promise<import("../contracts.js").StudioHarmonyProfileUpdateResult>;
54
+ projectState(input: {
55
+ draftId: string;
56
+ }, signal: AbortSignal): Promise<import("../contracts.js").StudioProjectState>;
57
+ projectActivate(input: {
58
+ draftId: string;
59
+ graphRev: string;
60
+ }, signal: AbortSignal): Promise<import("../contracts.js").StudioProjectState>;
61
+ projectFiles(input: {
62
+ draftId: string;
63
+ }, signal: AbortSignal): Promise<import("../contracts.js").StudioProjectFile[]>;
64
+ projectReadFile(input: {
65
+ draftId: string;
66
+ path: string;
67
+ }, signal: AbortSignal): Promise<{
68
+ path: string;
69
+ content: string;
70
+ }>;
71
+ projectWriteFile(input: {
72
+ draftId: string;
73
+ path: string;
74
+ content: string;
75
+ }, signal: AbortSignal): Promise<{
76
+ path: string;
77
+ saved: true;
78
+ }>;
79
+ projectBuild(input: {
80
+ draftId: string;
81
+ }, signal: AbortSignal): Promise<import("../contracts.js").StudioBuildResult>;
82
+ projectCancelBuild(input: {
83
+ draftId: string;
84
+ }, signal: AbortSignal): Promise<{
85
+ canceled: boolean;
86
+ }>;
87
+ elementsStyles(input: {
88
+ draftId: string;
89
+ }, signal: AbortSignal): Promise<StudioElementStyleSource[]>;
90
+ elementsSaveSource(input: {
91
+ draftId: string;
92
+ styles: StudioElementStyleSource[];
93
+ }, signal: AbortSignal): Promise<{
94
+ files: string[];
95
+ }>;
96
+ patchesAnalyzeAutomatic(input: StudioAutomaticPatchRequest & {
97
+ draftId: string;
98
+ }, signal: AbortSignal): Promise<import("../contracts.js").StudioAutomaticPatchPlan>;
99
+ patchesCreateAutomatic(input: StudioAutomaticPatchRequest & {
100
+ draftId: string;
101
+ }, signal: AbortSignal): Promise<import("../contracts.js").StudioAutomaticPatchWriteResult>;
102
+ readinessInspect(input: {
103
+ draftId: string;
104
+ }, signal: AbortSignal): Promise<import("../contracts.js").StudioReadinessReport>;
105
+ readinessPack(input: {
106
+ draftId: string;
107
+ }, signal: AbortSignal): Promise<import("../contracts.js").StudioReadinessReport>;
108
+ previewStatus(input: {
109
+ draftId: string;
110
+ }, signal: AbortSignal): Promise<import("../contracts.js").StudioPreviewStatus>;
111
+ previewUpdate(input: StudioPreviewUpdate & {
112
+ draftId: string;
113
+ }, signal: AbortSignal): Promise<import("../contracts.js").StudioPreviewStatus>;
114
+ previewResolveSource(input: {
115
+ draftId: string;
116
+ source: StudioSourceLocation;
117
+ }, signal: AbortSignal): Promise<import("../contracts.js").StudioSourceCandidate>;
118
+ agentCreate(input: {
119
+ draftId: string;
120
+ agentPreset?: string;
121
+ }, signal: AbortSignal): Promise<import("../contracts.js").StudioAgentBinding>;
122
+ agentAttach(input: {
123
+ draftId: string;
124
+ sessionId: string;
125
+ }, signal: AbortSignal): Promise<import("../contracts.js").StudioAgentBinding>;
126
+ agentLeave(input: {
127
+ draftId: string;
128
+ }, signal: AbortSignal): Promise<import("../contracts.js").StudioDraftView>;
129
+ }
@@ -0,0 +1,53 @@
1
+ import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
2
+ import { STUDIO_INVOCATIONS } from '../studio-remote.js';
3
+ export const STUDIO_LOCAL = {
4
+ package: 'dsh-webui-studio/studio',
5
+ face: 'host',
6
+ schemas: [],
7
+ model: { services: [], events: [], objects: [] },
8
+ invocations: STUDIO_INVOCATIONS,
9
+ };
10
+ export class StudioService extends TypertRemoteService {
11
+ backend;
12
+ constructor(ctx, backend) {
13
+ super(ctx, 'studio');
14
+ this.backend = backend;
15
+ }
16
+ currentGet(signal) { signal.throwIfAborted(); return this.backend.currentGet(); }
17
+ currentPreviewStatus(signal) { signal.throwIfAborted(); return this.backend.currentPreviewStatus(); }
18
+ currentPreviewUpdate(input, signal) { signal.throwIfAborted(); return this.backend.currentPreviewUpdate(input); }
19
+ currentResolveSource(input, signal) { signal.throwIfAborted(); return this.backend.currentResolveSource(input); }
20
+ currentAgentCreate(input, signal) { signal.throwIfAborted(); return this.backend.currentAgentCreate(input); }
21
+ currentAgentAttach(input, signal) { signal.throwIfAborted(); return this.backend.currentAgentAttach(input); }
22
+ currentAgentLeave(signal) { signal.throwIfAborted(); return this.backend.currentAgentLeave(); }
23
+ draftsList(signal) { signal.throwIfAborted(); return this.backend.draftsList(); }
24
+ draftsCreate(input, signal) { signal.throwIfAborted(); return this.backend.draftsCreate(input); }
25
+ draftsRename(input, signal) { signal.throwIfAborted(); return this.backend.draftsRename(input); }
26
+ draftsExport(input, signal) { signal.throwIfAborted(); return this.backend.draftsExport(input); }
27
+ draftsStart(input, signal) { signal.throwIfAborted(); return this.backend.draftsStart(input); }
28
+ draftsStop(input, signal) { signal.throwIfAborted(); return this.backend.draftsStop(input); }
29
+ workspaceGet(signal) { signal.throwIfAborted(); return this.backend.workspaceGet(); }
30
+ workspaceUpdate(input, signal) { signal.throwIfAborted(); return this.backend.workspaceUpdate(input); }
31
+ harmonyProfile(input, signal) { signal.throwIfAborted(); return this.backend.harmonyProfile(input); }
32
+ harmonyInspect(input, signal) { signal.throwIfAborted(); return this.backend.harmonyInspect(input); }
33
+ harmonyUpdateProfile(input, signal) { signal.throwIfAborted(); return this.backend.harmonyUpdateProfile(input); }
34
+ projectState(input, signal) { signal.throwIfAborted(); return this.backend.projectState(input); }
35
+ projectActivate(input, signal) { signal.throwIfAborted(); return this.backend.projectActivate(input); }
36
+ projectFiles(input, signal) { signal.throwIfAborted(); return this.backend.projectFiles(input); }
37
+ projectReadFile(input, signal) { signal.throwIfAborted(); return this.backend.projectReadFile(input); }
38
+ projectWriteFile(input, signal) { signal.throwIfAborted(); return this.backend.projectWriteFile(input); }
39
+ projectBuild(input, signal) { return this.backend.projectBuild(input, signal); }
40
+ projectCancelBuild(input, signal) { signal.throwIfAborted(); return this.backend.projectCancelBuild(input); }
41
+ elementsStyles(input, signal) { signal.throwIfAborted(); return this.backend.elementsStyles(input); }
42
+ elementsSaveSource(input, signal) { signal.throwIfAborted(); return this.backend.elementsSaveSource(input); }
43
+ patchesAnalyzeAutomatic(input, signal) { signal.throwIfAborted(); return this.backend.patchesAnalyzeAutomatic(input); }
44
+ patchesCreateAutomatic(input, signal) { signal.throwIfAborted(); return this.backend.patchesCreateAutomatic(input); }
45
+ readinessInspect(input, signal) { signal.throwIfAborted(); return this.backend.readinessInspect(input); }
46
+ readinessPack(input, signal) { signal.throwIfAborted(); return this.backend.readinessPack(input); }
47
+ previewStatus(input, signal) { signal.throwIfAborted(); return this.backend.previewStatus(input); }
48
+ previewUpdate(input, signal) { signal.throwIfAborted(); return this.backend.previewUpdate(input); }
49
+ previewResolveSource(input, signal) { signal.throwIfAborted(); return this.backend.previewResolveSource(input); }
50
+ agentCreate(input, signal) { signal.throwIfAborted(); return this.backend.agentCreate(input); }
51
+ agentAttach(input, signal) { signal.throwIfAborted(); return this.backend.agentAttach(input); }
52
+ agentLeave(input, signal) { signal.throwIfAborted(); return this.backend.agentLeave(input); }
53
+ }
package/lib/index.d.ts CHANGED
@@ -1,10 +1,14 @@
1
1
  import type { Context } from '@deepseek-ai/cordis';
2
+ import '@deepseek-ai/cordis-plugin-loader';
3
+ import '@deepseek-ai/dsh-client-modules';
2
4
  import '@deepseek-ai/dsh-agent';
3
5
  import '@deepseek-ai/dsh-host-apiproxy';
4
6
  import '@deepseek-ai/dsh-host-webserver';
7
+ import '@deepseek-ai/dsh-skill';
5
8
  import '@deepseek-ai/dsh-system-prompt';
6
9
  import '@deepseek-ai/dsh-subprocess';
7
10
  import '@deepseek-ai/dsh-tools';
11
+ import 'dsh-harmony';
8
12
  export declare const name = "harmony-studio";
9
13
  export declare const inject: string[];
10
14
  export declare function apply(ctx: Context): void;
@@ -13,6 +17,7 @@ export { StudioBuildError, StudioBuildRunner, resolveBuildArgv } from './host/bu
13
17
  export type { StudioBuildOutput } from './host/build.js';
14
18
  export { StudioDraftRegistry, dshHomeFromProfile } from './host/drafts.js';
15
19
  export { inspectReadiness, StudioPackRunner } from './host/readiness.js';
20
+ export { createStudioMcpRoute, STUDIO_MCP_PATH } from './host/mcp.js';
16
21
  export { StudioPreviewSupervisor } from './host/preview.js';
17
22
  export { createStudioRoutes, isTrustedStudioRequest } from './host/routes.js';
18
23
  export { StudioWorkspaceStore } from './host/workspace.js';
package/lib/index.js CHANGED
@@ -1,67 +1,101 @@
1
- import { randomBytes } from 'node:crypto';
2
1
  import { readFileSync } from 'node:fs';
2
+ import { randomBytes } from 'node:crypto';
3
+ import '@deepseek-ai/cordis-plugin-loader';
4
+ import '@deepseek-ai/dsh-client-modules';
3
5
  import '@deepseek-ai/dsh-agent';
4
6
  import '@deepseek-ai/dsh-host-apiproxy';
5
7
  import '@deepseek-ai/dsh-host-webserver';
8
+ import '@deepseek-ai/dsh-skill';
6
9
  import '@deepseek-ai/dsh-system-prompt';
7
10
  import '@deepseek-ai/dsh-subprocess';
8
11
  import '@deepseek-ai/dsh-tools';
12
+ import 'dsh-harmony';
9
13
  import { STUDIO_PATH } from './contracts.js';
10
14
  import { StudioBackend } from './host/backend.js';
11
15
  import { dshHomeFromProfile, StudioDraftRegistry, studioCommands } from './host/drafts.js';
16
+ import { createStudioMcpRoute } from './host/mcp.js';
12
17
  import { applyPreviewWorker } from './host/preview-worker.js';
13
18
  import { createStudioRoutes } from './host/routes.js';
19
+ import { STUDIO_LOCAL, StudioService } from './host/studio-service.js';
14
20
  import { StudioWorkspaceStore } from './host/workspace.js';
15
21
  export const name = 'harmony-studio';
16
- export const inject = ['harmony', 'agents', 'tools', 'systemPrompt', 'webServer', 'subprocess'];
22
+ export const inject = ['webServer'];
23
+ const runtimeInject = ['harmony', 'agents', 'tools', 'skills', 'systemPrompt', 'webServer', 'subprocess', 'loader', 'clientModules', 'typert'];
17
24
  export function apply(ctx) {
18
- ctx.effect(() => {
19
- if (ctx.webServer.host !== '127.0.0.1') {
20
- ctx.logger.warn('harmony-studio: Studio is disabled because dsh web is not bound to 127.0.0.1');
21
- return () => { };
22
- }
23
- const harmony = ctx.harmony;
24
- const assets = {
25
- script: readFileSync(new URL('../dist/studio.js', import.meta.url)),
26
- style: readFileSync(new URL('../dist/studio.css', import.meta.url)),
27
- bridge: readFileSync(new URL('../dist/bridge.js', import.meta.url)),
28
- icon: readFileSync(new URL('../assets/harmony-icon.png', import.meta.url)),
29
- iconMono: readFileSync(new URL('../assets/harmony-icon-mono.png', import.meta.url)),
30
- };
31
- const previewRoot = process.env.DSH_STUDIO_PREVIEW_DRAFT_ROOT;
32
- if (previewRoot !== undefined) {
33
- const controlToken = process.env.DSH_STUDIO_PREVIEW_CONTROL_TOKEN;
25
+ if (ctx.webServer.host !== '127.0.0.1') {
26
+ ctx.logger.warn('harmony-studio: Studio is disabled because dsh web is not bound to 127.0.0.1');
27
+ return;
28
+ }
29
+ const assets = {
30
+ script: readFileSync(new URL('../dist/studio.js', import.meta.url)),
31
+ style: readFileSync(new URL('../dist/studio.css', import.meta.url)),
32
+ bridge: readFileSync(new URL('../dist/bridge.js', import.meta.url)),
33
+ icon: readFileSync(new URL('../assets/harmony-icon.png', import.meta.url)),
34
+ iconMono: readFileSync(new URL('../assets/harmony-icon-mono.png', import.meta.url)),
35
+ };
36
+ const previewRoot = process.env.DSH_STUDIO_PREVIEW_DRAFT_ROOT;
37
+ if (previewRoot !== undefined) {
38
+ ctx.inject(runtimeInject, previewCtx => {
34
39
  const parentOrigin = process.env.DSH_STUDIO_PREVIEW_PARENT_ORIGIN;
35
40
  const bridgeCapability = process.env.DSH_STUDIO_PREVIEW_BRIDGE_CAPABILITY;
36
- if (controlToken === undefined || parentOrigin === undefined || bridgeCapability === undefined) {
41
+ const packageDirsSource = process.env.DSH_STUDIO_PREVIEW_PACKAGE_DIRS;
42
+ if (parentOrigin === undefined || bridgeCapability === undefined || packageDirsSource === undefined) {
37
43
  throw new Error('harmony-studio: Preview worker environment is incomplete');
38
44
  }
39
- applyPreviewWorker(ctx, harmony, { root: previewRoot, controlToken, parentOrigin, bridgeCapability, bridge: assets.bridge });
40
- return () => { };
41
- }
42
- const token = randomBytes(32).toString('hex');
43
- const host = `127.0.0.1:${ctx.webServer.port}`;
44
- const dshHome = dshHomeFromProfile(harmony.profileDir);
45
- const backend = new StudioBackend(harmony, ctx.agents, ctx.subprocess, new StudioDraftRegistry(dshHome), new StudioWorkspaceStore(dshHome), studioCommands, `http://${host}`);
45
+ const packageDirs = JSON.parse(packageDirsSource);
46
+ if (!Array.isArray(packageDirs) || !packageDirs.every(item => typeof item === 'string')) {
47
+ throw new Error('harmony-studio: Preview package directories are invalid');
48
+ }
49
+ applyPreviewWorker(previewCtx, previewCtx.harmony, {
50
+ root: previewRoot,
51
+ packageDirs,
52
+ parentOrigin,
53
+ bridgeCapability,
54
+ bridge: assets.bridge,
55
+ });
56
+ });
57
+ return;
58
+ }
59
+ let runtimeReady = false;
60
+ ctx.effect(() => {
61
+ const dispose = createStudioRoutes(assets, () => runtimeReady).map(route => ctx.webServer.register(route));
62
+ return () => {
63
+ for (const stop of dispose.reverse())
64
+ stop();
65
+ };
66
+ }, 'harmony-studio: static routes');
67
+ ctx.inject(runtimeInject, runtimeCtx => runtimeCtx.effect(() => {
68
+ const host = `127.0.0.1:${runtimeCtx.webServer.port}`;
69
+ const dshHome = dshHomeFromProfile(runtimeCtx.harmony.profile().dir);
70
+ const currentBridgeCapability = randomBytes(24).toString('base64url');
71
+ const backend = new StudioBackend(runtimeCtx.harmony, runtimeCtx.agents, runtimeCtx.subprocess, new StudioDraftRegistry(dshHome), new StudioWorkspaceStore(dshHome), studioCommands, `http://${host}`, currentBridgeCapability);
72
+ new StudioService(runtimeCtx, backend);
46
73
  const dispose = [
47
- ...createStudioRoutes(backend, assets, { token, host, origin: `http://${host}` }).map(route => ctx.webServer.register(route)),
48
- ctx.webServer.tapIndex(html => {
49
- const script = `<script src="${STUDIO_PATH}/bridge.js"></script>`;
74
+ runtimeCtx.typert.register(STUDIO_LOCAL),
75
+ runtimeCtx.webServer.register(createStudioMcpRoute(backend)),
76
+ runtimeCtx.webServer.tapIndex(html => {
77
+ const script = `<script>window.__DSH_STUDIO_PREVIEW__=${JSON.stringify({
78
+ parentOrigin: `http://${host}`,
79
+ capability: currentBridgeCapability,
80
+ })}</script><script src="${STUDIO_PATH}/bridge.js"></script>`;
50
81
  const head = html.indexOf('<head>');
51
82
  return head === -1 ? `${script}${html}` : `${html.slice(0, head + 6)}${script}${html.slice(head + 6)}`;
52
83
  }),
53
84
  ];
85
+ runtimeReady = true;
54
86
  return async () => {
87
+ runtimeReady = false;
55
88
  for (const stop of dispose.reverse())
56
89
  stop();
57
90
  await backend.dispose();
58
91
  };
59
- }, 'harmony-studio: routes');
92
+ }, 'harmony-studio: runtime routes'));
60
93
  }
61
94
  export { StudioBackend } from './host/backend.js';
62
95
  export { StudioBuildError, StudioBuildRunner, resolveBuildArgv } from './host/build.js';
63
96
  export { StudioDraftRegistry, dshHomeFromProfile } from './host/drafts.js';
64
97
  export { inspectReadiness, StudioPackRunner } from './host/readiness.js';
98
+ export { createStudioMcpRoute, STUDIO_MCP_PATH } from './host/mcp.js';
65
99
  export { StudioPreviewSupervisor } from './host/preview.js';
66
100
  export { createStudioRoutes, isTrustedStudioRequest } from './host/routes.js';
67
101
  export { StudioWorkspaceStore } from './host/workspace.js';
@@ -0,0 +1,126 @@
1
+ import type { InvocationDescriptor, RemoteResult, TypertRemoteContribution } from '@deepseek-ai/dsh-typert-protocol';
2
+ import type { StudioAgentBinding, StudioAutomaticPatchPlan, StudioAutomaticPatchRequest, StudioAutomaticPatchWriteResult, StudioBuildResult, StudioCreateDraftInput, StudioCurrentInstanceView, StudioDraftView, StudioElementStyleSource, StudioHarmonyInspection, StudioHarmonyProfile, StudioHarmonyProfileUpdateResult, StudioPreviewStatus, StudioPreviewUpdate, StudioProjectFile, StudioProjectState, StudioReadinessReport, StudioSourceCandidate, StudioSourceLocation, StudioWorkspaceState } from './contracts.js';
3
+ export interface StudioRemote {
4
+ currentGet(signal?: AbortSignal): Promise<RemoteResult<StudioCurrentInstanceView>>;
5
+ currentPreviewStatus(signal?: AbortSignal): Promise<RemoteResult<StudioPreviewStatus>>;
6
+ currentPreviewUpdate(input: StudioPreviewUpdate, signal?: AbortSignal): Promise<RemoteResult<StudioPreviewStatus>>;
7
+ currentResolveSource(input: {
8
+ source: StudioSourceLocation;
9
+ }, signal?: AbortSignal): Promise<RemoteResult<StudioSourceCandidate>>;
10
+ currentAgentCreate(input: {
11
+ agentPreset?: string;
12
+ }, signal?: AbortSignal): Promise<RemoteResult<StudioAgentBinding>>;
13
+ currentAgentAttach(input: {
14
+ sessionId: string;
15
+ }, signal?: AbortSignal): Promise<RemoteResult<StudioAgentBinding>>;
16
+ currentAgentLeave(signal?: AbortSignal): Promise<RemoteResult<StudioCurrentInstanceView>>;
17
+ draftsList(signal?: AbortSignal): Promise<RemoteResult<StudioDraftView[]>>;
18
+ draftsCreate(input: StudioCreateDraftInput, signal?: AbortSignal): Promise<RemoteResult<StudioDraftView>>;
19
+ draftsRename(input: {
20
+ draftId: string;
21
+ label: string;
22
+ }, signal?: AbortSignal): Promise<RemoteResult<StudioDraftView>>;
23
+ draftsExport(input: {
24
+ draftId: string;
25
+ }, signal?: AbortSignal): Promise<RemoteResult<StudioDraftView>>;
26
+ draftsStart(input: {
27
+ draftId: string;
28
+ }, signal?: AbortSignal): Promise<RemoteResult<StudioDraftView>>;
29
+ draftsStop(input: {
30
+ draftId: string;
31
+ }, signal?: AbortSignal): Promise<RemoteResult<StudioDraftView>>;
32
+ workspaceGet(signal?: AbortSignal): Promise<RemoteResult<StudioWorkspaceState>>;
33
+ workspaceUpdate(input: StudioWorkspaceState, signal?: AbortSignal): Promise<RemoteResult<StudioWorkspaceState>>;
34
+ harmonyProfile(input: {
35
+ draftId: string;
36
+ }, signal?: AbortSignal): Promise<RemoteResult<StudioHarmonyProfile>>;
37
+ harmonyInspect(input: {
38
+ draftId: string;
39
+ package?: string;
40
+ file?: string;
41
+ }, signal?: AbortSignal): Promise<RemoteResult<StudioHarmonyInspection>>;
42
+ harmonyUpdateProfile(input: {
43
+ draftId: string;
44
+ order?: string[];
45
+ patchOrder?: string[];
46
+ disabled?: string[];
47
+ }, signal?: AbortSignal): Promise<RemoteResult<StudioHarmonyProfileUpdateResult>>;
48
+ projectState(input: {
49
+ draftId: string;
50
+ }, signal?: AbortSignal): Promise<RemoteResult<StudioProjectState>>;
51
+ projectActivate(input: {
52
+ draftId: string;
53
+ graphRev: string;
54
+ }, signal?: AbortSignal): Promise<RemoteResult<StudioProjectState>>;
55
+ projectFiles(input: {
56
+ draftId: string;
57
+ }, signal?: AbortSignal): Promise<RemoteResult<StudioProjectFile[]>>;
58
+ projectReadFile(input: {
59
+ draftId: string;
60
+ path: string;
61
+ }, signal?: AbortSignal): Promise<RemoteResult<{
62
+ path: string;
63
+ content: string;
64
+ }>>;
65
+ projectWriteFile(input: {
66
+ draftId: string;
67
+ path: string;
68
+ content: string;
69
+ }, signal?: AbortSignal): Promise<RemoteResult<{
70
+ path: string;
71
+ saved: true;
72
+ }>>;
73
+ projectBuild(input: {
74
+ draftId: string;
75
+ }, signal?: AbortSignal): Promise<RemoteResult<StudioBuildResult>>;
76
+ projectCancelBuild(input: {
77
+ draftId: string;
78
+ }, signal?: AbortSignal): Promise<RemoteResult<{
79
+ canceled: boolean;
80
+ }>>;
81
+ elementsStyles(input: {
82
+ draftId: string;
83
+ }, signal?: AbortSignal): Promise<RemoteResult<StudioElementStyleSource[]>>;
84
+ elementsSaveSource(input: {
85
+ draftId: string;
86
+ styles: StudioElementStyleSource[];
87
+ }, signal?: AbortSignal): Promise<RemoteResult<{
88
+ files: string[];
89
+ }>>;
90
+ patchesAnalyzeAutomatic(input: StudioAutomaticPatchRequest & {
91
+ draftId: string;
92
+ }, signal?: AbortSignal): Promise<RemoteResult<StudioAutomaticPatchPlan>>;
93
+ patchesCreateAutomatic(input: StudioAutomaticPatchRequest & {
94
+ draftId: string;
95
+ }, signal?: AbortSignal): Promise<RemoteResult<StudioAutomaticPatchWriteResult>>;
96
+ readinessInspect(input: {
97
+ draftId: string;
98
+ }, signal?: AbortSignal): Promise<RemoteResult<StudioReadinessReport>>;
99
+ readinessPack(input: {
100
+ draftId: string;
101
+ }, signal?: AbortSignal): Promise<RemoteResult<StudioReadinessReport>>;
102
+ previewStatus(input: {
103
+ draftId: string;
104
+ }, signal?: AbortSignal): Promise<RemoteResult<StudioPreviewStatus>>;
105
+ previewUpdate(input: StudioPreviewUpdate & {
106
+ draftId: string;
107
+ }, signal?: AbortSignal): Promise<RemoteResult<StudioPreviewStatus>>;
108
+ previewResolveSource(input: {
109
+ draftId: string;
110
+ source: StudioSourceLocation;
111
+ }, signal?: AbortSignal): Promise<RemoteResult<StudioSourceCandidate>>;
112
+ agentCreate(input: {
113
+ draftId: string;
114
+ agentPreset?: string;
115
+ }, signal?: AbortSignal): Promise<RemoteResult<StudioAgentBinding>>;
116
+ agentAttach(input: {
117
+ draftId: string;
118
+ sessionId: string;
119
+ }, signal?: AbortSignal): Promise<RemoteResult<StudioAgentBinding>>;
120
+ agentLeave(input: {
121
+ draftId: string;
122
+ }, signal?: AbortSignal): Promise<RemoteResult<StudioDraftView>>;
123
+ }
124
+ export declare function invokeStudioRemote(remote: StudioRemote, method: string, payload: any, signal?: AbortSignal): Promise<RemoteResult<unknown>> | undefined;
125
+ export declare const STUDIO_INVOCATIONS: readonly InvocationDescriptor[];
126
+ export declare const STUDIO_REMOTE: TypertRemoteContribution;