dsh-webui-studio 0.1.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.
@@ -0,0 +1,394 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { spawn } from 'node:child_process';
3
+ import { chmod, copyFile, lstat, mkdir, readFile, readdir, realpath, rename, rm, writeFile } from 'node:fs/promises';
4
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
5
+ const PACKAGE_NAME = /^(?:@[a-z0-9._~-]+\/)?[a-z0-9._~-]+$/;
6
+ const COMMAND_OUTPUT_LIMIT = 2 * 1024 * 1024;
7
+ export const studioCommands = {
8
+ run(command, args, cwd, onOutput, signal) {
9
+ return new Promise((resolve, reject) => {
10
+ const child = spawn(command, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'], signal });
11
+ let output = '';
12
+ const read = (chunk) => {
13
+ const text = chunk.toString();
14
+ output = `${output}${text}`.slice(-COMMAND_OUTPUT_LIMIT);
15
+ onOutput?.(text);
16
+ };
17
+ child.stdout.on('data', read);
18
+ child.stderr.on('data', read);
19
+ child.once('error', reject);
20
+ child.once('close', (code, signal) => {
21
+ if (code === 0)
22
+ resolve();
23
+ else
24
+ reject(new Error(`Command exited with ${code === null ? signal ?? 'a signal' : `code ${code}`}\n${output.trimEnd()}`));
25
+ });
26
+ });
27
+ },
28
+ };
29
+ function templateManifest(name) {
30
+ return `${JSON.stringify({
31
+ name,
32
+ version: '0.1.0',
33
+ private: true,
34
+ type: 'module',
35
+ packageManager: 'npm@10.0.0',
36
+ exports: { '.': './lib/index.js', './client': './lib/client.js', './package.json': './package.json' },
37
+ scripts: { build: 'node --check lib/index.js && node --check lib/client.js' },
38
+ dsh: { client: { platform: 'web' }, harmony: { patches: [] } },
39
+ }, null, 2)}\n`;
40
+ }
41
+ function templateClient(name) {
42
+ return `window.__ModuleLoader__.load({
43
+ id: ${JSON.stringify(name)},
44
+ factory: () => ({
45
+ apply() {},
46
+ }),
47
+ })
48
+ `;
49
+ }
50
+ async function pluginManifest(root) {
51
+ let manifest;
52
+ try {
53
+ manifest = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
54
+ }
55
+ catch (error) {
56
+ throw new Error(`Plugin folder must contain a readable package.json: ${error instanceof Error ? error.message : String(error)}`);
57
+ }
58
+ if (typeof manifest.name !== 'string' || !PACKAGE_NAME.test(manifest.name)) {
59
+ throw new Error('Plugin package.json must declare a valid npm package name');
60
+ }
61
+ if (manifest.dsh?.client?.platform !== 'web') {
62
+ throw new Error('Plugin package.json must declare dsh.client.platform as "web"');
63
+ }
64
+ if (typeof manifest.exports !== 'object' || manifest.exports === null
65
+ || !Object.hasOwn(manifest.exports, '.') || !Object.hasOwn(manifest.exports, './client')) {
66
+ throw new Error('Plugin package.json exports must include "." and "./client"');
67
+ }
68
+ if (typeof manifest.scripts?.build !== 'string' || manifest.scripts.build.trim() === '') {
69
+ throw new Error('Plugin package.json must declare a non-empty scripts.build');
70
+ }
71
+ return manifest;
72
+ }
73
+ async function copyPluginDirectory(source, target) {
74
+ const info = await lstat(source);
75
+ if (info.isSymbolicLink())
76
+ throw new Error(`Plugin snapshot does not include symbolic links: ${source}`);
77
+ if (info.isDirectory()) {
78
+ try {
79
+ const targetInfo = await lstat(target);
80
+ if (targetInfo.isSymbolicLink() || !targetInfo.isDirectory()) {
81
+ throw new Error(`Plugin destination must contain only regular files and directories: ${target}`);
82
+ }
83
+ }
84
+ catch (error) {
85
+ if (error.code !== 'ENOENT')
86
+ throw error;
87
+ await mkdir(target, { mode: info.mode, recursive: true });
88
+ }
89
+ const entries = await readdir(source, { withFileTypes: true });
90
+ for (const entry of entries) {
91
+ if (entry.name === '.git' || entry.name === 'node_modules')
92
+ continue;
93
+ await copyPluginDirectory(join(source, entry.name), join(target, entry.name));
94
+ }
95
+ return;
96
+ }
97
+ if (!info.isFile())
98
+ throw new Error(`Plugin snapshot only supports regular files and directories: ${source}`);
99
+ try {
100
+ const targetInfo = await lstat(target);
101
+ if (targetInfo.isSymbolicLink() || !targetInfo.isFile()) {
102
+ throw new Error(`Plugin destination must contain only regular files and directories: ${target}`);
103
+ }
104
+ }
105
+ catch (error) {
106
+ if (error.code !== 'ENOENT')
107
+ throw error;
108
+ }
109
+ await copyFile(source, target);
110
+ await chmod(target, info.mode);
111
+ }
112
+ const PRESERVED_EXPORT_ENTRIES = ['.git', 'node_modules'];
113
+ async function replacePluginDirectory(source, target, targetExists) {
114
+ const parent = dirname(target);
115
+ const suffix = randomUUID();
116
+ const staging = join(parent, `.${basename(target)}.${suffix}.dsh-studio.tmp`);
117
+ const backup = join(parent, `.${basename(target)}.${suffix}.dsh-studio.backup`);
118
+ let movedTarget = false;
119
+ let committed = false;
120
+ try {
121
+ await copyPluginDirectory(source, staging);
122
+ if (targetExists) {
123
+ for (const entry of PRESERVED_EXPORT_ENTRIES) {
124
+ const info = await pathInfo(join(target, entry));
125
+ if (info?.isSymbolicLink())
126
+ throw new Error(`Local plugin folder contains an unsafe symbolic link: ${entry}`);
127
+ }
128
+ await rename(target, backup);
129
+ movedTarget = true;
130
+ for (const entry of PRESERVED_EXPORT_ENTRIES) {
131
+ if (await pathInfo(join(backup, entry)) !== undefined) {
132
+ await rename(join(backup, entry), join(staging, entry));
133
+ }
134
+ }
135
+ }
136
+ await rename(staging, target);
137
+ committed = true;
138
+ if (movedTarget)
139
+ await rm(backup, { recursive: true, force: true });
140
+ }
141
+ catch (error) {
142
+ if (movedTarget && !committed) {
143
+ for (const entry of PRESERVED_EXPORT_ENTRIES) {
144
+ if (await pathInfo(join(staging, entry)) !== undefined) {
145
+ await rename(join(staging, entry), join(backup, entry));
146
+ }
147
+ }
148
+ if (await pathInfo(target) === undefined)
149
+ await rename(backup, target);
150
+ }
151
+ await rm(staging, { recursive: true, force: true });
152
+ throw error;
153
+ }
154
+ }
155
+ async function pathInfo(path) {
156
+ try {
157
+ return await lstat(path);
158
+ }
159
+ catch (error) {
160
+ if (error.code === 'ENOENT')
161
+ return undefined;
162
+ throw error;
163
+ }
164
+ }
165
+ function inside(root, target) {
166
+ const path = relative(root, target);
167
+ return path === '' || (path !== '..' && !path.startsWith(`..${sep}`) && !isAbsolute(path));
168
+ }
169
+ async function validateDestinationDirectory(input, studioRoot) {
170
+ if (input === undefined)
171
+ return undefined;
172
+ if (typeof input !== 'string' || input.trim() === '')
173
+ throw new Error('Local plugin folder is required');
174
+ const requested = input.trim();
175
+ if (!isAbsolute(requested))
176
+ throw new Error('Local plugin folder must be an absolute path');
177
+ const requestedPath = resolve(requested);
178
+ let canonicalParent;
179
+ try {
180
+ canonicalParent = await realpath(dirname(requestedPath));
181
+ }
182
+ catch (error) {
183
+ if (error.code === 'ENOENT')
184
+ throw new Error('The parent of the local plugin folder must already exist');
185
+ throw error;
186
+ }
187
+ const target = join(canonicalParent, basename(requestedPath));
188
+ if (inside(await realpath(studioRoot), target))
189
+ throw new Error('Local plugin folder must be outside the Studio data directory');
190
+ const info = await pathInfo(target);
191
+ if (info?.isSymbolicLink() || (info !== undefined && !info.isDirectory())) {
192
+ throw new Error('Local plugin folder must be a directory and cannot be a symbolic link');
193
+ }
194
+ if (info !== undefined && (await readdir(target)).length > 0) {
195
+ throw new Error('Local plugin folder must be new or empty');
196
+ }
197
+ return target;
198
+ }
199
+ async function initializeRepository(root, name, commands) {
200
+ await mkdir(join(root, 'lib'), { recursive: true });
201
+ await writeFile(join(root, 'package.json'), templateManifest(name));
202
+ await writeFile(join(root, 'lib/index.js'), 'export const name = "draft-host"\nexport function apply() {}\n');
203
+ await writeFile(join(root, 'lib/client.js'), templateClient(name));
204
+ await writeFile(join(root, 'README.md'), `# ${name}\n\nCreated by dsh-webui-studio.\n`);
205
+ await commands.run('git', ['init', '--initial-branch=main'], root);
206
+ await commands.run('git', ['add', '.'], root);
207
+ await commands.run('git', ['-c', 'user.name=dsh-webui-studio', '-c', 'user.email=studio@localhost', 'commit', '-m', 'Initial Draft'], root);
208
+ }
209
+ function nextNewPluginLabel(records) {
210
+ const labels = new Set(records.map(record => record.label));
211
+ for (let index = records.filter(record => record.source.kind === 'new').length + 1;; index += 1) {
212
+ const label = `新插件_${index}`;
213
+ if (!labels.has(label))
214
+ return label;
215
+ }
216
+ }
217
+ export class StudioDraftRegistry {
218
+ commands;
219
+ root;
220
+ recordsDir;
221
+ repositoriesDir;
222
+ worktreesDir;
223
+ runtimesDir;
224
+ recordMutations = new Map();
225
+ constructor(dshHome, commands = studioCommands) {
226
+ this.commands = commands;
227
+ this.root = join(dshHome, 'studio');
228
+ this.recordsDir = join(this.root, 'drafts');
229
+ this.repositoriesDir = join(this.root, 'repositories');
230
+ this.worktreesDir = join(this.root, 'worktrees');
231
+ this.runtimesDir = join(this.root, 'runtimes');
232
+ }
233
+ async list() {
234
+ await mkdir(this.recordsDir, { recursive: true });
235
+ const files = (await readdir(this.recordsDir)).filter(file => file.endsWith('.json')).sort();
236
+ return Promise.all(files.map(async (file) => JSON.parse(await readFile(join(this.recordsDir, file), 'utf8'))));
237
+ }
238
+ async get(id) {
239
+ if (!/^[a-f0-9-]{36}$/.test(id))
240
+ throw new Error('invalid Draft id');
241
+ return JSON.parse(await readFile(join(this.recordsDir, `${id}.json`), 'utf8'));
242
+ }
243
+ async create(input) {
244
+ if (input.profileMode === 'custom')
245
+ throw new Error('Custom Draft profiles are not implemented yet');
246
+ const id = randomUUID();
247
+ const repositoryDir = join(this.repositoriesDir, id);
248
+ const worktreeDir = join(this.worktreesDir, id);
249
+ const runtimeHome = join(this.runtimesDir, id, 'dsh-home');
250
+ await Promise.all([
251
+ mkdir(this.recordsDir, { recursive: true }),
252
+ mkdir(this.repositoriesDir, { recursive: true }),
253
+ mkdir(this.worktreesDir, { recursive: true }),
254
+ mkdir(dirname(runtimeHome), { recursive: true }),
255
+ ]);
256
+ try {
257
+ let name;
258
+ let label;
259
+ let source;
260
+ let destinationDirectory;
261
+ if (input.source.kind === 'new') {
262
+ const packageName = input.source.packageName;
263
+ if (typeof packageName !== 'string' || !PACKAGE_NAME.test(packageName))
264
+ throw new Error('New Draft package name is invalid');
265
+ name = packageName;
266
+ label = nextNewPluginLabel(await this.list());
267
+ source = { kind: 'new', packageName };
268
+ destinationDirectory = await validateDestinationDirectory(input.destinationDirectory, this.root);
269
+ await mkdir(repositoryDir);
270
+ await initializeRepository(repositoryDir, packageName, this.commands);
271
+ await this.commands.run('git', ['worktree', 'add', '-b', `dsh-studio/${id}`, worktreeDir, 'HEAD'], repositoryDir);
272
+ }
273
+ else {
274
+ if (input.destinationDirectory !== undefined)
275
+ throw new Error('Only new plugins can have a local destination folder');
276
+ if (typeof input.source.directory !== 'string' || input.source.directory.trim() === '') {
277
+ throw new Error('Existing plugin folder is required');
278
+ }
279
+ const directory = input.source.directory.trim();
280
+ if (!isAbsolute(directory))
281
+ throw new Error('Existing plugin folder must be an absolute path');
282
+ const canonicalSource = await realpath(directory);
283
+ if (!(await lstat(canonicalSource)).isDirectory())
284
+ throw new Error('Existing plugin path must be a directory');
285
+ const manifest = await pluginManifest(canonicalSource);
286
+ name = manifest.name;
287
+ label = basename(canonicalSource);
288
+ source = { kind: 'existing', directory: canonicalSource };
289
+ await mkdir(repositoryDir);
290
+ await copyPluginDirectory(canonicalSource, repositoryDir);
291
+ await this.commands.run('git', ['init', '--initial-branch=main'], repositoryDir);
292
+ await this.commands.run('git', ['add', '.'], repositoryDir);
293
+ await this.commands.run('git', [
294
+ '-c', 'user.name=dsh-webui-studio', '-c', 'user.email=studio@localhost',
295
+ 'commit', '-m', 'Import plugin snapshot',
296
+ ], repositoryDir);
297
+ await this.commands.run('git', ['worktree', 'add', '-b', `dsh-studio/${id}`, worktreeDir, 'HEAD'], repositoryDir);
298
+ }
299
+ const canonicalWorktree = await realpath(worktreeDir);
300
+ const root = canonicalWorktree;
301
+ const record = {
302
+ id,
303
+ name,
304
+ label,
305
+ source,
306
+ ...(destinationDirectory === undefined ? {} : { destinationDirectory }),
307
+ repositoryDir: await realpath(repositoryDir),
308
+ worktreeDir: canonicalWorktree,
309
+ root,
310
+ runtimeHome,
311
+ profileMode: input.profileMode,
312
+ createdAt: new Date().toISOString(),
313
+ };
314
+ await writeFile(join(this.recordsDir, `${id}.json`), `${JSON.stringify(record, null, 2)}\n`, { flag: 'wx' });
315
+ return record;
316
+ }
317
+ catch (error) {
318
+ const cleanup = await Promise.allSettled([
319
+ rm(worktreeDir, { recursive: true, force: true }),
320
+ rm(repositoryDir, { recursive: true, force: true }),
321
+ rm(dirname(runtimeHome), { recursive: true, force: true }),
322
+ ]);
323
+ const cleanupErrors = cleanup.flatMap(result => result.status === 'rejected' ? [result.reason] : []);
324
+ if (cleanupErrors.length > 0)
325
+ throw new AggregateError([error, ...cleanupErrors], 'Draft creation failed and cleanup was incomplete');
326
+ throw error;
327
+ }
328
+ }
329
+ async rename(id, label) {
330
+ const nextLabel = label.trim();
331
+ if (nextLabel === '' || nextLabel.length > 120)
332
+ throw new Error('Draft name must contain 1 to 120 characters');
333
+ return this.mutate(id, record => ({ ...record, label: nextLabel }));
334
+ }
335
+ async export(id) {
336
+ return this.mutate(id, async (record) => {
337
+ const sourceManifest = JSON.parse(await readFile(join(record.root, 'package.json'), 'utf8'));
338
+ if (sourceManifest.name !== record.name) {
339
+ throw new Error(`Draft package.json name must remain ${JSON.stringify(record.name)}`);
340
+ }
341
+ const target = record.destinationDirectory;
342
+ if (target === undefined)
343
+ throw new Error('This Draft does not have a local plugin folder');
344
+ const info = await pathInfo(target);
345
+ if (record.exportedAt === undefined) {
346
+ if (info?.isSymbolicLink() || (info !== undefined && !info.isDirectory())) {
347
+ throw new Error('Local plugin folder must be a directory and cannot be a symbolic link');
348
+ }
349
+ if (info !== undefined && (await readdir(target)).length > 0) {
350
+ throw new Error('Local plugin folder is no longer empty; choose another folder');
351
+ }
352
+ }
353
+ else {
354
+ if (info?.isSymbolicLink() || info === undefined || !info.isDirectory()) {
355
+ throw new Error('The saved local plugin folder is missing or is no longer a regular directory');
356
+ }
357
+ const manifest = await pluginManifest(target);
358
+ if (manifest.name !== record.name)
359
+ throw new Error('The local plugin folder now belongs to a different package');
360
+ }
361
+ await replacePluginDirectory(record.root, target, info !== undefined);
362
+ return { ...record, exportedAt: new Date().toISOString() };
363
+ });
364
+ }
365
+ async mutate(id, update) {
366
+ const previous = this.recordMutations.get(id)?.catch(() => undefined) ?? Promise.resolve();
367
+ let release;
368
+ const turn = new Promise(resolve => { release = resolve; });
369
+ const queued = previous.then(() => turn);
370
+ this.recordMutations.set(id, queued);
371
+ await previous;
372
+ try {
373
+ const next = await update(await this.get(id));
374
+ await this.replace(next);
375
+ return next;
376
+ }
377
+ finally {
378
+ release();
379
+ if (this.recordMutations.get(id) === queued)
380
+ this.recordMutations.delete(id);
381
+ }
382
+ }
383
+ async replace(record) {
384
+ const file = join(this.recordsDir, `${record.id}.json`);
385
+ const temporary = join(this.recordsDir, `.${record.id}.${randomUUID()}.tmp`);
386
+ await writeFile(temporary, `${JSON.stringify(record, null, 2)}\n`, { flag: 'wx' });
387
+ await rename(temporary, file);
388
+ }
389
+ }
390
+ export function dshHomeFromProfile(profileDir) {
391
+ if (basename(dirname(profileDir)) !== 'profiles')
392
+ throw new Error('Harmony profile is not under a DSH_HOME/profiles directory');
393
+ return dirname(dirname(profileDir));
394
+ }
@@ -0,0 +1,11 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import { type StudioHarmonyService } from '../contracts.js';
3
+ interface PreviewWorkerOptions {
4
+ root: string;
5
+ controlToken: string;
6
+ parentOrigin: string;
7
+ bridgeCapability: string;
8
+ bridge: Buffer;
9
+ }
10
+ export declare function applyPreviewWorker(ctx: Context, harmony: StudioHarmonyService, options: PreviewWorkerOptions): void;
11
+ export {};
@@ -0,0 +1,110 @@
1
+ import { STUDIO_PATH, STUDIO_PREVIEW_API_PATH, } from '../contracts.js';
2
+ import { StudioSourceResolver } from './source-resolution.js';
3
+ function loopback(request) {
4
+ const address = request.socket.remoteAddress;
5
+ return address === '::1' || address === '127.0.0.1' || address?.startsWith('::ffff:127.') === true;
6
+ }
7
+ function json(response, status, body) {
8
+ response.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
9
+ response.end(JSON.stringify(body));
10
+ }
11
+ async function readJson(request) {
12
+ const chunks = [];
13
+ for await (const chunk of request)
14
+ chunks.push(Buffer.from(chunk));
15
+ if (Buffer.concat(chunks).byteLength > 1024 * 1024)
16
+ throw new Error('request body is too large');
17
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
18
+ }
19
+ export function applyPreviewWorker(ctx, harmony, options) {
20
+ ctx.effect(() => {
21
+ let handle;
22
+ const ready = harmony.prepareDraft({ root: options.root }).then(value => { handle = value; return value; });
23
+ const sources = new StudioSourceResolver(options.root, harmony.profileDir);
24
+ const worker = {
25
+ kind: 'prefix',
26
+ path: STUDIO_PREVIEW_API_PATH,
27
+ async handler(request, response) {
28
+ if (!loopback(request) || request.headers.authorization !== `Bearer ${options.controlToken}`) {
29
+ return json(response, 403, { ok: false, error: 'invalid Preview worker capability' });
30
+ }
31
+ if (request.method !== 'POST')
32
+ return json(response, 405, { ok: false, error: 'method not allowed' });
33
+ try {
34
+ const payload = await readJson(request);
35
+ const method = new URL(request.url ?? '/', 'http://localhost').pathname.slice(`${STUDIO_PREVIEW_API_PATH}/`.length);
36
+ if (method === 'health')
37
+ return json(response, 200, { ok: true, value: { ready: true } });
38
+ const draft = await ready;
39
+ if (method === 'state')
40
+ return json(response, 200, { ok: true, value: { project: draft.snapshot() } });
41
+ if (method === 'activate') {
42
+ if (typeof payload.graphRev !== 'string')
43
+ throw new Error('graphRev is required');
44
+ return json(response, 200, { ok: true, value: { project: await draft.activateAfterPreviewReady(payload.graphRev) } });
45
+ }
46
+ if (method === 'apply-build') {
47
+ return json(response, 200, { ok: true, value: { project: await draft.applyBuild() } });
48
+ }
49
+ if (method === 'inspect') {
50
+ const packageName = typeof payload.package === 'string' ? payload.package : undefined;
51
+ const file = typeof payload.file === 'string' ? payload.file : undefined;
52
+ return json(response, 200, {
53
+ ok: true,
54
+ value: {
55
+ harmony: harmony.inspect({ ...(packageName === undefined ? {} : { package: packageName }), ...(file === undefined ? {} : { file }) }),
56
+ dependencies: harmony.inspectDependencies(draft.snapshot().name),
57
+ },
58
+ });
59
+ }
60
+ if (method === 'resolve-source') {
61
+ const source = payload.source;
62
+ if (typeof source?.file !== 'string' || source.file === ''
63
+ || (source.line !== undefined && (!Number.isInteger(source.line) || source.line < 1))
64
+ || (source.column !== undefined && (!Number.isInteger(source.column) || source.column < 1))) {
65
+ throw new Error('source location is invalid');
66
+ }
67
+ return json(response, 200, { ok: true, value: await sources.resolve(source) });
68
+ }
69
+ if (method === 'read-source') {
70
+ if (typeof payload.package !== 'string' || typeof payload.file !== 'string') {
71
+ throw new Error('dependency package and file are required');
72
+ }
73
+ return json(response, 200, {
74
+ ok: true,
75
+ value: await sources.readDependency(payload.package, payload.file),
76
+ });
77
+ }
78
+ return json(response, 404, { ok: false, error: `unknown Preview worker method ${method}` });
79
+ }
80
+ catch (error) {
81
+ return json(response, 400, { ok: false, error: error instanceof Error ? error.message : String(error) });
82
+ }
83
+ },
84
+ };
85
+ const bridge = {
86
+ kind: 'exact',
87
+ path: `${STUDIO_PATH}/bridge.js`,
88
+ handler(request, response) {
89
+ if (!loopback(request))
90
+ return json(response, 403, { error: 'Preview is local only' });
91
+ response.writeHead(200, { 'cache-control': 'no-cache', 'content-type': 'text/javascript; charset=utf-8' });
92
+ response.end(request.method === 'HEAD' ? undefined : options.bridge);
93
+ },
94
+ };
95
+ const dispose = [ctx.webServer.register(worker), ctx.webServer.register(bridge), ctx.webServer.tapIndex(html => {
96
+ const config = `<script>window.__DSH_STUDIO_PREVIEW__=${JSON.stringify({
97
+ parentOrigin: options.parentOrigin,
98
+ capability: options.bridgeCapability,
99
+ })}</script><script src="${STUDIO_PATH}/bridge.js"></script>`;
100
+ const head = html.indexOf('<head>');
101
+ return head === -1 ? `${config}${html}` : `${html.slice(0, head + 6)}${config}${html.slice(head + 6)}`;
102
+ })];
103
+ return async () => {
104
+ for (const stop of dispose.reverse())
105
+ stop();
106
+ await ready.catch(() => undefined);
107
+ await handle?.deactivate();
108
+ };
109
+ }, 'harmony-studio: Preview worker');
110
+ }
@@ -0,0 +1,42 @@
1
+ import type { StudioDraftRecord, StudioPreviewInspection, StudioProjectState, StudioSourceCandidate, StudioSourceLocation } from '../contracts.js';
2
+ import type { StudioCommandRunner } from './drafts.js';
3
+ export interface StudioPreviewRuntime {
4
+ state: 'stopped' | 'starting' | 'running' | 'failed';
5
+ previewUrl?: string;
6
+ bridgeCapability?: string;
7
+ error?: string;
8
+ log: string;
9
+ }
10
+ export declare class StudioPreviewSupervisor {
11
+ readonly draft: StudioDraftRecord;
12
+ private readonly mainProfileDir;
13
+ private readonly parentOrigin;
14
+ private readonly commands;
15
+ private readonly harmonyBinEntry;
16
+ private readonly stopTimeoutMs;
17
+ private child?;
18
+ private controlToken?;
19
+ private runtime;
20
+ private startAbort?;
21
+ private startPromise?;
22
+ constructor(draft: StudioDraftRecord, mainProfileDir: string, parentOrigin: string, commands: StudioCommandRunner, harmonyBinEntry: string, stopTimeoutMs?: number);
23
+ snapshot(): StudioPreviewRuntime;
24
+ start(): Promise<StudioPreviewRuntime>;
25
+ private startRuntime;
26
+ stop(): Promise<StudioPreviewRuntime>;
27
+ private terminateChild;
28
+ state(): Promise<StudioProjectState>;
29
+ activate(graphRev: string): Promise<StudioProjectState>;
30
+ applyBuild(): Promise<StudioProjectState>;
31
+ inspect(input?: {
32
+ package?: string;
33
+ file?: string;
34
+ }): Promise<StudioPreviewInspection>;
35
+ resolveSource(source: StudioSourceLocation): Promise<StudioSourceCandidate>;
36
+ readDependencySource(packageName: string, file: string): Promise<string>;
37
+ dispose(): Promise<void>;
38
+ private waitUntilRunning;
39
+ private waitForWorker;
40
+ private worker;
41
+ private delay;
42
+ }