@workflow-code/cli 0.2.4-20260929001 → 0.2.4-20261010001.codex-cli-web

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,1523 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ assertLocalCliCoreCompatible
4
+ } from "./chunk-T4FXQ7AQ.js";
5
+ import "./chunk-J2JMC4MZ.js";
6
+ import "./chunk-G7PZDTYW.js";
7
+ import {
8
+ clearCliAuthConfig,
9
+ readCliAuthConfig
10
+ } from "./chunk-PGM3IPWA.js";
11
+ import "./chunk-BP2VDZQV.js";
12
+ import {
13
+ i18n
14
+ } from "./chunk-7WEXRL5A.js";
15
+ import "./chunk-NSPRIPOP.js";
16
+
17
+ // src/web.ts
18
+ import { spawn as spawn3 } from "child_process";
19
+ import path3 from "path";
20
+
21
+ // src/web/server.ts
22
+ import { createServer } from "http";
23
+ import { createHash, randomBytes, timingSafeEqual } from "crypto";
24
+ import { mkdir, readFile, rename, writeFile, realpath } from "fs/promises";
25
+ import path2 from "path";
26
+ import os from "os";
27
+ import { pathToFileURL } from "url";
28
+ import { spawn as spawn2 } from "child_process";
29
+
30
+ // src/web/workflow-types.ts
31
+ function workflowParameters(project, entrypointId) {
32
+ return project.entrypoints.find((entry) => entry.id === entrypointId) ?? project;
33
+ }
34
+ function workflowParamValue(field, value = field.defaultValue ?? "") {
35
+ if (field.type === "file" || field.control === "audio" || !(field.multiple || field.control === "checkboxes") || Array.isArray(value)) {
36
+ return value;
37
+ }
38
+ if (!value.trim()) {
39
+ return [];
40
+ }
41
+ try {
42
+ const parsed = JSON.parse(value);
43
+ if (Array.isArray(parsed)) {
44
+ return parsed.filter((entry) => ["string", "number", "boolean"].includes(typeof entry)).map(String);
45
+ }
46
+ } catch {
47
+ }
48
+ return [value];
49
+ }
50
+ function workflowFormValue(field, value) {
51
+ if (value === void 0 || value === null) {
52
+ return workflowParamValue(field);
53
+ }
54
+ if (typeof value === "string") {
55
+ return workflowParamValue(field, value);
56
+ }
57
+ if (field.type === "file" || field.control === "audio" || field.format === "json") {
58
+ return JSON.stringify(value);
59
+ }
60
+ return workflowParamValue(field, Array.isArray(value) ? value.map(String) : String(value));
61
+ }
62
+ function workflowParamArgs(fields, values, states = {}) {
63
+ const args = [];
64
+ for (const field of fields) {
65
+ if (states[field.name]?.visible === false) {
66
+ continue;
67
+ }
68
+ const value = workflowParamValue(field, values[field.name]);
69
+ const entries = Array.isArray(value) ? value.length ? [JSON.stringify(value)] : [] : [value];
70
+ for (const entry of entries) {
71
+ if (entry === "") {
72
+ continue;
73
+ }
74
+ if (field.flag) {
75
+ args.push(field.flag, entry);
76
+ } else if (field.type === "positional") {
77
+ args.push(entry);
78
+ }
79
+ }
80
+ }
81
+ return args;
82
+ }
83
+ function applyOutputChunk(items, value) {
84
+ if (!value || typeof value !== "object") {
85
+ return;
86
+ }
87
+ const chunk = value;
88
+ if (chunk.type === "output_item_start" && chunk.item) {
89
+ const index = items.findIndex((item) => item.id && item.id === chunk.item?.id);
90
+ if (index >= 0) {
91
+ items[index] = { ...chunk.item };
92
+ } else {
93
+ items.push({ ...chunk.item });
94
+ }
95
+ } else if (chunk.type === "output_item_delta") {
96
+ const item = items.find((entry) => entry.id === chunk.itemId);
97
+ if (item) {
98
+ if (chunk.item) {
99
+ Object.assign(item, { ...chunk.item, metadata: { ...item.metadata, ...chunk.item.metadata } });
100
+ }
101
+ if (typeof chunk.delta === "string") {
102
+ item.content = `${typeof item.content === "string" ? item.content : ""}${chunk.delta}`;
103
+ } else if (chunk.delta !== void 0) {
104
+ item.content = chunk.delta;
105
+ }
106
+ }
107
+ }
108
+ }
109
+ function outputText(items) {
110
+ return items.filter((item) => !item.collapsed && item.metadata?.plan !== true && ["text", "markdown", void 0].includes(item.contentType)).map((item) => typeof item.content === "string" ? item.content : "").filter(Boolean).join("\n\n");
111
+ }
112
+ function validateWorkflowValues(fields, values, states = {}) {
113
+ for (const field of fields) {
114
+ if (states[field.name]?.visible === false || states[field.name]?.disabled) {
115
+ continue;
116
+ }
117
+ const value = workflowParamValue(field, values[field.name]);
118
+ const label = field.label || field.name;
119
+ if (field.required && (typeof value === "string" && !value.trim() || Array.isArray(value) && !value.length)) {
120
+ throw new Error(`\u8BF7\u586B\u5199 ${label}\u3002`);
121
+ }
122
+ if (value === "" || Array.isArray(value) && !value.length) {
123
+ continue;
124
+ }
125
+ if (field.type === "number" && (typeof value !== "string" || !Number.isFinite(Number(value)))) {
126
+ throw new Error(`${label} \u9700\u8981\u662F\u6709\u6548\u6570\u5B57\u3002`);
127
+ }
128
+ if (field.type === "boolean" && value !== "true" && value !== "false") {
129
+ throw new Error(`${label} \u9700\u8981\u662F\u6709\u6548\u5E03\u5C14\u503C\u3002`);
130
+ }
131
+ if (field.format === "json" || field.type === "file" || field.control === "audio") {
132
+ let parsed;
133
+ try {
134
+ parsed = JSON.parse(typeof value === "string" ? value : JSON.stringify(value));
135
+ } catch {
136
+ throw new Error(`${label} \u9700\u8981\u662F\u6709\u6548\u7684 JSON\u3002`);
137
+ }
138
+ if (field.required && (field.type === "file" || field.control === "audio") && (parsed === null || Array.isArray(parsed) && !parsed.length)) {
139
+ throw new Error(`\u8BF7\u586B\u5199 ${label}\u3002`);
140
+ }
141
+ }
142
+ const choices = states[field.name]?.resolvedOptions ?? field.options;
143
+ if (choices && choices.length > 0) {
144
+ const allowed = new Set(choices.filter((option) => states[field.name]?.options?.[option.value]?.visible !== false && !states[field.name]?.options?.[option.value]?.disabled).map((option) => option.value));
145
+ if ((Array.isArray(value) ? value : [value]).some((entry) => !allowed.has(entry))) {
146
+ throw new Error(`${label} \u7684\u9009\u9879\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u9009\u62E9\u3002`);
147
+ }
148
+ }
149
+ }
150
+ }
151
+ function resolvedWorkflowValues(fields, values) {
152
+ validateWorkflowValues(fields, values);
153
+ return Object.fromEntries(fields.map((field) => {
154
+ const value = workflowParamValue(field, values[field.name]);
155
+ if (field.type === "boolean") {
156
+ return [field.name, value === "true"];
157
+ }
158
+ if (field.type === "number" && value !== "") {
159
+ return [field.name, Number(value)];
160
+ }
161
+ if ((field.type === "file" || field.control === "audio" || field.format === "json") && typeof value === "string" && value !== "") {
162
+ return [field.name, JSON.parse(value)];
163
+ }
164
+ return [field.name, value];
165
+ }));
166
+ }
167
+ function redactWorkflowData(value, secrets = [], redactKeys = true) {
168
+ if (typeof value === "string") {
169
+ for (const secret of secrets.filter(Boolean)) {
170
+ value = value.replaceAll(secret, "[redacted]");
171
+ }
172
+ return value;
173
+ }
174
+ if (Array.isArray(value)) {
175
+ return value.map((entry) => redactWorkflowData(entry, secrets, redactKeys));
176
+ }
177
+ if (value && typeof value === "object") {
178
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [
179
+ key,
180
+ redactKeys && /password|secret|authorization|cookie|^(?:api[-_]?key|access[-_]?token|refresh[-_]?token)$/i.test(key) ? "[redacted]" : redactWorkflowData(entry, secrets, redactKeys)
181
+ ]));
182
+ }
183
+ return value;
184
+ }
185
+
186
+ // src/web/conversation-controller.ts
187
+ var ConversationController = class {
188
+ constructor(project, runtime, saved, options) {
189
+ this.project = project;
190
+ this.runtime = runtime;
191
+ this.options = options;
192
+ this.sessions = (saved.sessions ?? []).filter((entry) => typeof entry.id === "string" && typeof entry.title === "string" && Array.isArray(entry.messages));
193
+ if (!this.sessions.length) {
194
+ this.sessions.push(this.blank());
195
+ }
196
+ this.activeId = this.sessions.some((entry) => entry.id === saved.activeId) ? saved.activeId : this.sessions.at(-1).id;
197
+ for (const entry of this.sessions) {
198
+ entry.createdAt ??= entry.messages[0]?.createdAt ?? 0;
199
+ entry.updatedAt ??= entry.messages.at(-1)?.createdAt ?? entry.createdAt;
200
+ delete entry.activeRunId;
201
+ entry.queuedInputs ??= [];
202
+ for (const message of entry.messages) {
203
+ delete message.submittingInput;
204
+ if (message.status === "running") {
205
+ message.status = message.pendingInput ? "waiting_for_input" : "aborted";
206
+ message.error = "\u8FD0\u884C\u5DF2\u4E2D\u65AD";
207
+ }
208
+ }
209
+ }
210
+ }
211
+ project;
212
+ runtime;
213
+ options;
214
+ sessions;
215
+ activeId;
216
+ tasks = /* @__PURE__ */ new Map();
217
+ disposed = false;
218
+ removing = /* @__PURE__ */ new Set();
219
+ get current() {
220
+ return this.sessions.find((entry) => entry.id === this.activeId);
221
+ }
222
+ get running() {
223
+ return !!this.current.activeRunId;
224
+ }
225
+ get hasRuns() {
226
+ return this.tasks.size > 0;
227
+ }
228
+ blank() {
229
+ const now = Date.now();
230
+ return { id: globalThis.crypto.randomUUID(), title: "\u65B0\u5BF9\u8BDD", createdAt: now, updatedAt: now, messages: [], parameterValues: {}, toolPermissionPolicies: {}, queuedInputs: [], draft: { text: "", attachments: [] } };
231
+ }
232
+ async persist() {
233
+ const sessions = this.sessions.map((entry) => ({ ...entry, messages: entry.messages.map(({ submittingInput: _, ...message }) => message) }));
234
+ await this.options.save(JSON.parse(JSON.stringify(sessions)), this.activeId);
235
+ }
236
+ changed() {
237
+ if (!this.disposed) {
238
+ this.options.changed();
239
+ }
240
+ }
241
+ async create() {
242
+ const blank = this.sessions.find((entry) => !entry.messages.length && !entry.activeRunId && !entry.queuedInputs?.length && !entry.draft?.text && !entry.draft?.attachments.length);
243
+ const next = blank ?? this.blank();
244
+ next.updatedAt = Date.now();
245
+ if (blank) {
246
+ this.sessions.splice(this.sessions.indexOf(blank), 1);
247
+ }
248
+ this.sessions.push(next);
249
+ this.activeId = next.id;
250
+ this.changed();
251
+ await this.persist();
252
+ }
253
+ async select(id) {
254
+ if (!this.sessions.some((entry) => entry.id === id)) {
255
+ return;
256
+ }
257
+ this.activeId = id;
258
+ this.changed();
259
+ await this.persist();
260
+ }
261
+ async rename(id, title) {
262
+ const entry = this.sessions.find((entry2) => entry2.id === id);
263
+ if (!entry || !title.trim()) {
264
+ return;
265
+ }
266
+ entry.title = title.trim().slice(0, 160);
267
+ entry.titleLocked = true;
268
+ this.changed();
269
+ await this.persist();
270
+ }
271
+ async lockTitle(id, locked) {
272
+ const entry = this.sessions.find((entry2) => entry2.id === id);
273
+ if (entry) {
274
+ entry.titleLocked = locked;
275
+ this.changed();
276
+ await this.persist();
277
+ }
278
+ }
279
+ async remove(id) {
280
+ if (this.removing.has(id)) {
281
+ return;
282
+ }
283
+ const index = this.sessions.findIndex((entry2) => entry2.id === id);
284
+ if (index < 0) {
285
+ return;
286
+ }
287
+ const entry = this.sessions[index];
288
+ this.removing.add(id);
289
+ try {
290
+ const task = entry.activeRunId && this.tasks.get(entry.activeRunId);
291
+ if (task) {
292
+ task.cancelled = true;
293
+ this.runtime.cancel(entry.activeRunId);
294
+ await task.finished;
295
+ }
296
+ await this.runtime.deleteConversation(this.project, id);
297
+ this.sessions.splice(this.sessions.indexOf(entry), 1);
298
+ if (!this.sessions.length) {
299
+ this.sessions.push(this.blank());
300
+ }
301
+ if (this.activeId === id) {
302
+ this.activeId = this.sessions.at(-1).id;
303
+ }
304
+ this.changed();
305
+ await this.persist();
306
+ } finally {
307
+ this.removing.delete(id);
308
+ }
309
+ }
310
+ async updateDraft(id, text, attachments, editingQueueId) {
311
+ const entry = this.sessions.find((entry2) => entry2.id === id);
312
+ if (!entry) {
313
+ return;
314
+ }
315
+ entry.draft = { text: text.slice(0, 1e5), attachments, editingQueueId: editingQueueId ?? entry.draft?.editingQueueId };
316
+ await this.persist();
317
+ }
318
+ async send(input) {
319
+ const entry = this.current;
320
+ const parameters = workflowParameters(this.project, input.entrypointId);
321
+ validateWorkflowValues(parameters.fields, input.params, input.states);
322
+ if (this.removing.has(entry.id)) {
323
+ return;
324
+ }
325
+ if (this.project.projectType !== "conversation" && input.text) {
326
+ this.rawArgs(input.text);
327
+ }
328
+ if (this.project.projectType === "conversation" && this.project.conversation?.defaultInput?.enabled !== false && !input.text.trim() && !input.attachments.length) {
329
+ return;
330
+ }
331
+ const queued = { ...input, id: globalThis.crypto.randomUUID(), createdAt: Date.now(), params: { ...input.params }, attachments: [...input.attachments] };
332
+ const waiting = entry.messages.some((message) => message.pendingInput);
333
+ if (entry.activeRunId || waiting || entry.queuedInputs?.length) {
334
+ if (!this.project.conversation?.inputQueue?.enabled) {
335
+ throw new Error("\u5F53\u524D\u4F1A\u8BDD\u6B63\u5728\u8FD0\u884C\u3002");
336
+ }
337
+ if (entry.draft?.editingQueueId) {
338
+ const index = entry.queuedInputs.findIndex((item) => item.id === entry.draft?.editingQueueId);
339
+ if (index >= 0) {
340
+ queued.id = entry.queuedInputs[index].id;
341
+ entry.queuedInputs[index] = queued;
342
+ } else {
343
+ entry.queuedInputs.push(queued);
344
+ }
345
+ } else {
346
+ (entry.queuedInputs ??= []).push(queued);
347
+ }
348
+ entry.draft = { text: "", attachments: [] };
349
+ this.changed();
350
+ await this.persist();
351
+ if (!entry.activeRunId && !waiting) {
352
+ this.drain(entry);
353
+ }
354
+ return;
355
+ }
356
+ entry.draft = { text: "", attachments: [] };
357
+ this.start(entry, queued);
358
+ await this.persist();
359
+ }
360
+ start(entry, input, answer) {
361
+ const parameters = workflowParameters(this.project, input.entrypointId);
362
+ const args = workflowParamArgs(parameters.fields, input.params, input.states);
363
+ if (this.project.projectType === "conversation" && this.project.conversation?.defaultInput?.enabled !== false) {
364
+ args.push("--message", input.text);
365
+ for (const [flag, files] of [
366
+ ["--images", input.attachments.filter((file) => file.kind === "image")],
367
+ ["--videos", input.attachments.filter((file) => file.mimeType.startsWith("video/"))],
368
+ ["--files", input.attachments.filter((file) => file.kind !== "image" && !file.mimeType.startsWith("video/"))]
369
+ ]) {
370
+ if (files.length) {
371
+ args.push(flag, JSON.stringify(files.map(({ previewUrl: _, ...reference }) => reference)));
372
+ }
373
+ }
374
+ } else if (this.project.projectType !== "conversation" && input.text) {
375
+ args.push(...this.rawArgs(input.text));
376
+ }
377
+ if (!answer) {
378
+ if (!entry.messages.length && !entry.titleLocked) {
379
+ entry.title = (input.text || input.attachments[0]?.name || "\u5DE5\u4F5C\u6D41\u8FD0\u884C").slice(0, 80);
380
+ }
381
+ entry.messages.push({ id: globalThis.crypto.randomUUID(), role: "user", content: input.text || "", attachments: input.attachments, args, input, entrypointId: input.entrypointId, createdAt: Date.now() });
382
+ answer = { id: globalThis.crypto.randomUUID(), role: "assistant", content: "", items: [], status: "running", createdAt: Date.now(), model: typeof input.params.model === "string" ? input.params.model : void 0, args, entrypointId: input.entrypointId, events: [] };
383
+ answer.runId = answer.id;
384
+ entry.messages.push(answer);
385
+ }
386
+ const request = {
387
+ project: this.project,
388
+ conversationId: entry.id,
389
+ args: answer.args ?? args,
390
+ runId: answer.runId,
391
+ entrypointId: answer.entrypointId,
392
+ toolPermissionPolicies: entry.toolPermissionPolicies,
393
+ resolvedInputs: [...answer.resolvedInputs ?? [], ...answer.submittingInput ? [answer.submittingInput] : []],
394
+ resumeAfterRequestId: answer.submittingInput?.requestId
395
+ };
396
+ answer.status = "running";
397
+ answer.error = void 0;
398
+ if (!answer.submittingInput) {
399
+ answer.pendingInput = void 0;
400
+ }
401
+ entry.updatedAt = Date.now();
402
+ entry.activeRunId = answer.runId;
403
+ const task = { cancelled: false, finished: Promise.resolve() };
404
+ this.tasks.set(answer.runId, task);
405
+ this.changed();
406
+ task.finished = this.execute(entry, answer, request, task).catch((error) => this.options.log(`Workflow persistence: ${String(error)}`));
407
+ }
408
+ rawArgs(text) {
409
+ const raw = JSON.parse(text);
410
+ if (!Array.isArray(raw) || !raw.every((arg) => typeof arg === "string")) {
411
+ throw new Error("\u8FD0\u884C\u53C2\u6570\u9700\u8981\u662F\u5B57\u7B26\u4E32\u6570\u7EC4\u3002");
412
+ }
413
+ return raw;
414
+ }
415
+ async execute(entry, answer, request, task) {
416
+ const pending = answer.pendingInput;
417
+ try {
418
+ await this.persist();
419
+ if (task.cancelled || this.disposed) {
420
+ throw new Error("\u5DF2\u505C\u6B62");
421
+ }
422
+ const report = await this.runtime.run(request, (event) => {
423
+ if (this.disposed) {
424
+ return;
425
+ }
426
+ (answer.events ??= []).push(event);
427
+ if (event.type === "node_chunk") {
428
+ applyOutputChunk(answer.items ??= [], event.chunk);
429
+ answer.content = outputText(answer.items);
430
+ }
431
+ if (event.type === "conversation_title_updated" && !entry.titleLocked && typeof event.title === "string") {
432
+ entry.title = event.title.slice(0, 160);
433
+ }
434
+ if (event.type === "token_usage_updated") {
435
+ answer.tokenUsage = { enabled: true, display: true, run: event.usage };
436
+ }
437
+ if (event.type === "user_input_requested") {
438
+ answer.pendingInput = event;
439
+ }
440
+ if (event.type === "user_input_resolved") {
441
+ this.confirmInput(answer, event);
442
+ if (answer.pendingInput?.requestId === event.requestId) {
443
+ answer.pendingInput = void 0;
444
+ }
445
+ }
446
+ if (event.type === "file_created") {
447
+ (answer.files ??= []).push(event.file);
448
+ }
449
+ if (event.type === "timer_started" || event.type === "timer_finished") {
450
+ const timer = event.timer;
451
+ answer.timers = [...(answer.timers ?? []).filter((entry2) => entry2.timerId !== timer.timerId), timer];
452
+ }
453
+ this.changed();
454
+ });
455
+ answer.report = report;
456
+ answer.status = report.status;
457
+ answer.durationMs = report.durationMs;
458
+ if (report.output?.items?.length) {
459
+ answer.items = report.output.items;
460
+ answer.content = outputText(answer.items);
461
+ }
462
+ answer.pendingInput = report.status === "waiting_for_input" ? report.pendingUserInput : void 0;
463
+ for (const resolved of report.resolvedUserInputs ?? []) {
464
+ this.confirmInput(answer, resolved);
465
+ }
466
+ if (answer.submittingInput && report.status === "success") {
467
+ this.confirmInput(answer, answer.submittingInput);
468
+ }
469
+ answer.files = [...new Map([...answer.files ?? [], ...report.files ?? []].map((file) => [file.id, file])).values()];
470
+ answer.tokenUsage = report.tokenUsage ?? answer.tokenUsage;
471
+ answer.timers = report.timers ?? answer.timers;
472
+ if (report.status === "failed") {
473
+ const data = report.result?.data;
474
+ answer.error = report.result?.error?.message || (data && typeof data === "object" && "errMessage" in data && typeof data.errMessage === "string" ? data.errMessage : "\u8FD0\u884C\u5931\u8D25");
475
+ }
476
+ if (report.status === "aborted" || report.status === "timed_out") {
477
+ answer.error = report.status === "aborted" ? "\u5DF2\u505C\u6B62" : "\u8FD0\u884C\u8D85\u65F6";
478
+ }
479
+ if (answer.submittingInput && pending && report.status === "failed") {
480
+ answer.status = "waiting_for_input";
481
+ answer.pendingInput = pending;
482
+ }
483
+ if (!answer.content && !answer.items?.length && report.status === "success") {
484
+ answer.content = report.result?.data === void 0 ? "\u5DF2\u5B8C\u6210" : JSON.stringify(report.result.data, null, 2);
485
+ }
486
+ } catch (error) {
487
+ answer.error = error instanceof Error ? error.message : String(error);
488
+ answer.status = task.cancelled ? "aborted" : answer.submittingInput && pending ? "waiting_for_input" : "failed";
489
+ if (answer.status === "waiting_for_input") {
490
+ answer.pendingInput = pending;
491
+ } else if (task.cancelled) {
492
+ answer.pendingInput = void 0;
493
+ }
494
+ this.options.log(`Workflow run: ${answer.error}`);
495
+ } finally {
496
+ if (answer.status !== "waiting_for_input") {
497
+ answer.timers = answer.timers?.map((timer) => timer.status === "running" ? { ...timer, status: "cleared", endedAt: (/* @__PURE__ */ new Date()).toISOString(), durationMs: Math.max(0, Date.now() - new Date(timer.startedAt).getTime()) } : timer);
498
+ }
499
+ delete answer.submittingInput;
500
+ if (entry.activeRunId === answer.runId) {
501
+ delete entry.activeRunId;
502
+ }
503
+ this.tasks.delete(answer.runId);
504
+ this.changed();
505
+ await this.persist();
506
+ if (!this.disposed && !this.removing.has(entry.id) && !answer.pendingInput && (!task.cancelled || task.promote)) {
507
+ this.drain(entry);
508
+ }
509
+ }
510
+ }
511
+ confirmInput(answer, resolved) {
512
+ const previous = answer.resolvedInputs?.find((input) => input.requestId === resolved.requestId) ?? (answer.submittingInput?.requestId === resolved.requestId ? answer.submittingInput : void 0);
513
+ const confirmed = { ...previous, ...resolved, precedingOutputItemIds: previous?.precedingOutputItemIds ?? resolved.precedingOutputItemIds };
514
+ answer.resolvedInputs = [...(answer.resolvedInputs ?? []).filter((input) => input.requestId !== resolved.requestId), confirmed];
515
+ if (answer.submittingInput?.requestId === resolved.requestId) {
516
+ delete answer.submittingInput;
517
+ }
518
+ }
519
+ drain(entry) {
520
+ if (entry.activeRunId || entry.messages.some((message) => message.pendingInput) || entry.draft?.editingQueueId || this.disposed || this.removing.has(entry.id)) {
521
+ return;
522
+ }
523
+ const input = entry.queuedInputs?.shift();
524
+ if (input) {
525
+ try {
526
+ this.start(entry, input);
527
+ } catch (error) {
528
+ entry.queuedInputs.unshift(input);
529
+ this.options.log(String(error));
530
+ this.changed();
531
+ }
532
+ }
533
+ }
534
+ stop(entry = this.current) {
535
+ const task = entry.activeRunId && this.tasks.get(entry.activeRunId);
536
+ if (task) {
537
+ task.cancelled = true;
538
+ this.runtime.cancel(entry.activeRunId);
539
+ } else {
540
+ const pending = entry.messages.find((message) => message.pendingInput);
541
+ if (pending) {
542
+ pending.pendingInput = void 0;
543
+ pending.status = "aborted";
544
+ pending.error = "\u5DF2\u505C\u6B62";
545
+ this.changed();
546
+ void this.persist().catch((error) => this.options.log(String(error)));
547
+ }
548
+ }
549
+ }
550
+ async resume(messageId, requestId, values) {
551
+ const entry = this.current;
552
+ const answer = entry.messages.find((message) => message.id === messageId && message.pendingInput?.requestId === requestId);
553
+ if (!answer?.pendingInput || entry.activeRunId) {
554
+ return;
555
+ }
556
+ const request = answer.pendingInput;
557
+ const allowed = Object.fromEntries(request.params.map((field) => [field.name, values[field.name] ?? request.defaultValues?.[field.name] ?? field.defaultValue ?? ""]));
558
+ const form = Object.fromEntries(request.params.map((field) => [field.name, workflowFormValue(field, allowed[field.name])]));
559
+ const typedValues = resolvedWorkflowValues(request.params, form);
560
+ const resolved = {
561
+ requestId,
562
+ nodeName: request.nodeName,
563
+ values: typedValues,
564
+ submittedAt: (/* @__PURE__ */ new Date()).toISOString(),
565
+ request,
566
+ precedingOutputItemIds: (answer.items ?? []).map((item, index) => item.id ?? String(index))
567
+ };
568
+ answer.submittingInput = resolved;
569
+ if (request.metadata?.type === "agent-plan-approval" && allowed.decision === "approve") {
570
+ const mode = workflowParameters(this.project, answer.entrypointId).fields.find((field) => field.panel === "mode");
571
+ if (mode) {
572
+ (entry.parameterValues ??= {})[answer.entrypointId ?? ""] ??= {};
573
+ entry.parameterValues[answer.entrypointId ?? ""][mode.name] = "build";
574
+ }
575
+ }
576
+ this.start(entry, { id: messageId, text: "", params: {}, entrypointId: answer.entrypointId, attachments: [], createdAt: Date.now() }, answer);
577
+ await this.persist();
578
+ }
579
+ async editQueued(id) {
580
+ const input = this.current.queuedInputs?.find((input2) => input2.id === id);
581
+ if (!input) {
582
+ return;
583
+ }
584
+ this.current.draft = { text: input.text, attachments: input.attachments, editingQueueId: id };
585
+ (this.current.parameterValues ??= {})[input.entrypointId ?? ""] = input.params;
586
+ this.changed();
587
+ await this.persist();
588
+ }
589
+ async removeQueued(id) {
590
+ this.current.queuedInputs = (this.current.queuedInputs ?? []).filter((input) => input.id !== id);
591
+ if (this.current.draft?.editingQueueId === id) {
592
+ this.current.draft = { text: "", attachments: [] };
593
+ }
594
+ this.changed();
595
+ await this.persist();
596
+ this.drain(this.current);
597
+ }
598
+ async cancelQueueEdit() {
599
+ this.current.draft = { text: "", attachments: [] };
600
+ this.changed();
601
+ await this.persist();
602
+ this.drain(this.current);
603
+ }
604
+ async runQueued() {
605
+ this.drain(this.current);
606
+ await this.persist();
607
+ }
608
+ async reorderQueued(ids) {
609
+ const queue = this.current.queuedInputs ?? [];
610
+ if (ids.length !== queue.length || new Set(ids).size !== queue.length || ids.some((id) => !queue.some((input) => input.id === id))) {
611
+ return;
612
+ }
613
+ this.current.queuedInputs = ids.map((id) => queue.find((input) => input.id === id));
614
+ this.changed();
615
+ await this.persist();
616
+ }
617
+ async promoteQueued(id) {
618
+ const entry = this.current;
619
+ const queue = entry.queuedInputs ?? [];
620
+ if (!queue.some((input) => input.id === id)) {
621
+ return;
622
+ }
623
+ await this.reorderQueued([id, ...queue.filter((input) => input.id !== id).map((input) => input.id)]);
624
+ if (entry.draft?.editingQueueId) {
625
+ entry.draft = { text: "", attachments: [] };
626
+ }
627
+ const task = entry.activeRunId && this.tasks.get(entry.activeRunId);
628
+ if (task) {
629
+ task.promote = true;
630
+ task.cancelled = true;
631
+ this.runtime.cancel(entry.activeRunId);
632
+ } else {
633
+ this.stop(entry);
634
+ this.drain(entry);
635
+ }
636
+ }
637
+ async retry(id) {
638
+ const index = this.current.messages.findIndex((message) => message.id === id && message.role === "assistant");
639
+ const user = [...this.current.messages.slice(0, index)].reverse().find((message) => message.role === "user");
640
+ if (!user || this.current.activeRunId) {
641
+ return;
642
+ }
643
+ if (user.input) {
644
+ await this.send(user.input);
645
+ return;
646
+ }
647
+ const parameters = workflowParameters(this.project, user.entrypointId);
648
+ const params = {};
649
+ for (const field of parameters.fields) {
650
+ const index2 = user.args?.indexOf(field.flag ?? "") ?? -1;
651
+ if (index2 >= 0) {
652
+ params[field.name] = user.args[index2 + 1];
653
+ }
654
+ }
655
+ await this.send({ text: user.content, params, entrypointId: user.entrypointId, attachments: user.attachments ?? [] });
656
+ }
657
+ async idle() {
658
+ while (this.tasks.size) {
659
+ await Promise.all([...this.tasks.values()].map((task) => task.finished));
660
+ }
661
+ }
662
+ dispose() {
663
+ this.disposed = true;
664
+ for (const entry of this.sessions) {
665
+ this.stop(entry);
666
+ }
667
+ }
668
+ };
669
+
670
+ // src/web/runtime.ts
671
+ import { spawn } from "child_process";
672
+ import { existsSync, readFileSync } from "fs";
673
+ import path from "path";
674
+ import { fileURLToPath } from "url";
675
+ function cliRoot() {
676
+ let directory = path.dirname(fileURLToPath(import.meta.url));
677
+ for (let depth = 0; depth < 8; depth++) {
678
+ try {
679
+ if (JSON.parse(readFileSync(path.join(directory, "package.json"), "utf8")).name === "@workflow-code/cli") {
680
+ return directory;
681
+ }
682
+ } catch {
683
+ }
684
+ directory = path.dirname(directory);
685
+ }
686
+ throw new Error("Cannot locate the CLI installation.");
687
+ }
688
+ function createWebRuntime(projectDir, storageDir, connection) {
689
+ const root = cliRoot();
690
+ const builtWorker = path.join(root, "dist/web/worker.mjs");
691
+ const worker = existsSync(builtWorker) ? builtWorker : path.join(root, "src/web/worker.mjs");
692
+ const children = /* @__PURE__ */ new Set();
693
+ const active = /* @__PURE__ */ new Map();
694
+ let disposed = false;
695
+ const invoke = (request, onEvent) => new Promise((resolve, reject) => {
696
+ if (disposed) {
697
+ reject(new Error("Web runtime has stopped."));
698
+ return;
699
+ }
700
+ const auth = connection();
701
+ const env = { ...process.env, WORKFLOW_RUNTIME_PLATFORM: "cli", WORKFLOW_SERVER_URL: auth?.serverUrl ?? "", WORKFLOW_SERVER_ADMIN_KEY: auth?.accessToken ?? "" };
702
+ for (const key of ["TSX_TSCONFIG_PATH", "WORKFLOW_LOCAL_CORE_ROOT", "WORKFLOW_LOCAL_SHARED_ROOT", "WORKFLOW_LOCAL_SOURCE_MODE"]) {
703
+ delete env[key];
704
+ }
705
+ if (env.NODE_OPTIONS) {
706
+ env.NODE_OPTIONS = env.NODE_OPTIONS.replace(/(?:--conditions=development|--conditions\s+development|-C=development|-C\s+development)/g, "");
707
+ }
708
+ const child = spawn(process.execPath, [worker], { cwd: projectDir, env, shell: false, stdio: ["pipe", "pipe", "pipe"] });
709
+ children.add(child);
710
+ if (request.method === "run") {
711
+ active.set(String(request.runId), child);
712
+ }
713
+ let buffer = "", stderr = "", failure;
714
+ let result;
715
+ const timeout = setTimeout(() => {
716
+ failure = "Workflow execution timed out.";
717
+ child.kill("SIGTERM");
718
+ }, request.method === "run" ? 6e5 : 3e4);
719
+ const forcedExit = setTimeout(() => child.kill("SIGKILL"), request.method === "run" ? 605e3 : 35e3);
720
+ child.stdout.setEncoding("utf8");
721
+ child.stderr.setEncoding("utf8");
722
+ child.stderr.on("data", (chunk) => {
723
+ stderr = (stderr + chunk).slice(-12e3);
724
+ });
725
+ child.stdout.on("data", (chunk) => {
726
+ buffer += chunk;
727
+ let newline;
728
+ while ((newline = buffer.indexOf("\n")) >= 0) {
729
+ const line = buffer.slice(0, newline);
730
+ buffer = buffer.slice(newline + 1);
731
+ if (!line.startsWith("__WORKFLOW_WEB__")) {
732
+ continue;
733
+ }
734
+ try {
735
+ const message = redactWorkflowData(JSON.parse(line.slice("__WORKFLOW_WEB__".length)), [auth?.accessToken ?? ""], false);
736
+ if (message.kind === "event") {
737
+ onEvent?.(message.value);
738
+ }
739
+ if (message.kind === "result") {
740
+ result = message.value;
741
+ }
742
+ if (message.kind === "error") {
743
+ failure = message.error;
744
+ }
745
+ } catch {
746
+ failure = "Invalid workflow runtime response.";
747
+ }
748
+ }
749
+ });
750
+ const cleanup = () => {
751
+ clearTimeout(timeout);
752
+ clearTimeout(forcedExit);
753
+ children.delete(child);
754
+ if (active.get(String(request.runId)) === child) {
755
+ active.delete(String(request.runId));
756
+ }
757
+ };
758
+ child.once("error", (error) => {
759
+ cleanup();
760
+ reject(error);
761
+ });
762
+ child.once("close", (code, signal) => {
763
+ cleanup();
764
+ if (failure) {
765
+ reject(new Error(failure));
766
+ } else if (result !== void 0) {
767
+ if (request.method === "run") {
768
+ result.logs = String(redactWorkflowData(stderr, [auth?.accessToken ?? ""]));
769
+ }
770
+ resolve(result);
771
+ } else {
772
+ reject(new Error(signal || code === 130 ? "Stopped." : String(redactWorkflowData(stderr.trim(), [auth?.accessToken ?? ""])) || `Workflow runtime exited (${code}).`));
773
+ }
774
+ });
775
+ child.stdin.on("error", () => void 0);
776
+ child.stdin.end(JSON.stringify({ ...request, storageDir }));
777
+ });
778
+ return {
779
+ available: true,
780
+ inspect: (paths) => invoke({ method: "inspect", paths }),
781
+ resolveParams: (project, request, entrypointId) => invoke({ method: "resolve", project, request, entrypointId }),
782
+ run: (request, onEvent) => invoke({ method: "run", ...request }, onEvent),
783
+ uploadFiles: (project, conversationId, files) => invoke({ method: "upload", project, conversationId, files }),
784
+ readFile: (project, id) => invoke({ method: "file", project, id }),
785
+ deleteFiles: async (project, conversationId, ids) => {
786
+ await invoke({ method: "delete-files", project, conversationId, ids });
787
+ },
788
+ readResource: (project, conversationId, kind, target) => invoke({ method: "resource", project, conversationId, kind, target }),
789
+ deleteConversation: async (project, conversationId) => {
790
+ await invoke({ method: "delete-conversation", project, conversationId });
791
+ },
792
+ cancel: (runId) => {
793
+ const running = runId ? [active.get(runId)].filter((child) => !!child) : [...active.values()];
794
+ for (const child of running) {
795
+ child.kill("SIGTERM");
796
+ const timer = setTimeout(() => {
797
+ if (children.has(child)) {
798
+ child.kill("SIGKILL");
799
+ }
800
+ }, 5e3);
801
+ timer.unref();
802
+ }
803
+ },
804
+ dispose: () => {
805
+ disposed = true;
806
+ for (const child of children) {
807
+ child.kill("SIGKILL");
808
+ }
809
+ }
810
+ };
811
+ }
812
+
813
+ // src/web/resource-cards.ts
814
+ var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
815
+ var string = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
816
+ function conversationResourceCards(items, projectId, conversationId) {
817
+ const cards = /* @__PURE__ */ new Map();
818
+ for (const [index, item] of items.entries()) {
819
+ if (!["agent-part", "opencode-part"].includes(String(item.metadata?.kind)) || item.metadata?.partType !== "tool") {
820
+ continue;
821
+ }
822
+ const content = record(item.content);
823
+ if (content?.status !== "success") {
824
+ continue;
825
+ }
826
+ let output = record(content.output);
827
+ if (Array.isArray(output?.content) && output.content.length === 1) {
828
+ try {
829
+ output = record(JSON.parse(String(record(output.content[0])?.text)));
830
+ } catch {
831
+ }
832
+ }
833
+ let descriptor = record(record(output?.details)?.resourceCard);
834
+ const input = record(content.input) ?? {};
835
+ const tool = string(content.tool) ?? "";
836
+ if (tool === "workflow-ui_present_card") {
837
+ const presentation = output?.action ? output : input;
838
+ if (presentation.action === "remove") {
839
+ cards.delete(String(presentation.cardId));
840
+ continue;
841
+ }
842
+ if (presentation.action !== "upsert") {
843
+ continue;
844
+ }
845
+ descriptor = presentation;
846
+ }
847
+ if (!descriptor) {
848
+ const target2 = { source: "local", projectId, conversationId };
849
+ let kind2;
850
+ if (tool === "workflow-kv_kv_store" || /workflow-desktop_(?:read|list|write)_project_kv/.test(tool)) {
851
+ kind2 = "kv";
852
+ Object.assign(target2, { projectId: input.projectId ?? projectId, key: input.key, scope: input.scope ?? "project", conversationId: input.conversationId ?? conversationId });
853
+ } else if (tool === "workflow-knowledge_knowledge_documents" || /workflow-desktop_(?:query|mutate)_project_knowledge/.test(tool)) {
854
+ kind2 = "knowledge";
855
+ Object.assign(target2, { projectId: input.projectId ?? projectId, documentId: input.documentId ?? record(output?.document)?.id });
856
+ } else if (tool === "read" || tool === "workflow-desktop_open_dock_tool" && input.tool === "files") {
857
+ kind2 = "file";
858
+ target2.filePath = input.filePath ?? input.path;
859
+ } else if (tool === "workflow-desktop_open_project" && input.kind === "project") {
860
+ kind2 = "project";
861
+ target2.projectId = input.targetId;
862
+ } else if (tool === "workflow-desktop_open_dock_tool") {
863
+ if (input.tool === "browser") {
864
+ kind2 = "browser";
865
+ target2.url = input.url;
866
+ }
867
+ if (input.tool === "preview") {
868
+ kind2 = "kanban";
869
+ }
870
+ if (input.tool === "open-project" && input.openedProjectKind === "project") {
871
+ kind2 = "project";
872
+ target2.projectId = input.openedProjectId;
873
+ }
874
+ }
875
+ if (!kind2) {
876
+ continue;
877
+ }
878
+ descriptor = { kind: kind2, target: target2, title: record(output?.document)?.title };
879
+ }
880
+ const kind = descriptor.kind;
881
+ if (!["kv", "knowledge", "project", "browser", "file", "kanban"].includes(kind)) {
882
+ continue;
883
+ }
884
+ const target = Object.fromEntries(Object.entries(record(descriptor.target) ?? {}).filter((entry) => typeof entry[1] === "string" && entry[1].length < 8192));
885
+ if (target.source === "current") {
886
+ target.source = "local";
887
+ target.projectId = projectId;
888
+ }
889
+ if (target.source && !["local", "server"].includes(target.source)) {
890
+ continue;
891
+ }
892
+ if (kind === "browser") {
893
+ try {
894
+ const url = new URL(target.url);
895
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) {
896
+ continue;
897
+ }
898
+ } catch {
899
+ continue;
900
+ }
901
+ } else {
902
+ target.source ??= "local";
903
+ target.projectId ??= projectId;
904
+ if (target.scope === "conversation") {
905
+ target.conversationId ??= conversationId;
906
+ }
907
+ if (kind === "file" && (!target.filePath || /^(?:[\\/]|[a-z]:)|(?:^|[\\/])\.\.(?:[\\/]|$)/i.test(target.filePath))) {
908
+ continue;
909
+ }
910
+ }
911
+ const id = string(descriptor.cardId) ?? `${kind}:${JSON.stringify(target)}`;
912
+ const title = (string(descriptor.title) ?? target.key ?? target.documentId ?? target.filePath ?? target.url ?? target.projectId ?? kind).slice(0, 200);
913
+ cards.set(id, { id, kind, title, target, itemId: item.id ?? String(index) });
914
+ }
915
+ return [...cards.values()];
916
+ }
917
+
918
+ // src/web/server.ts
919
+ async function startWebServer(options) {
920
+ const projectDir = await realpath(path2.resolve(options.projectDir));
921
+ const key = createHash("sha256").update(projectDir).digest("hex").slice(0, 24);
922
+ const storageDir = path2.resolve(options.dataDir ?? path2.join(os.homedir(), ".workflow-code", "web"));
923
+ const historyPath = path2.join(storageDir, key, "conversations.json");
924
+ const clients = /* @__PURE__ */ new Set();
925
+ let connection;
926
+ let account = { status: "signed-out", serverUrl: "http://localhost:7126", revision: 0 };
927
+ let signedOut = false;
928
+ let authIdentity = "";
929
+ let login;
930
+ const runtime = options.runtime ?? createWebRuntime(projectDir, storageDir, () => connection);
931
+ let inspected;
932
+ try {
933
+ inspected = await runtime.inspect([projectDir]);
934
+ } catch (error) {
935
+ runtime.dispose();
936
+ throw error;
937
+ }
938
+ const descriptor = inspected.projects[0];
939
+ if (!descriptor) {
940
+ runtime.dispose();
941
+ throw new Error(inspected.errors.join("\n") || "No Workflow project found.");
942
+ }
943
+ if (!options.runtime) {
944
+ assertLocalCliCoreCompatible(projectDir, descriptor.runtime.coreVersion);
945
+ }
946
+ if (!["conversation", "workflow"].includes(descriptor.projectType)) {
947
+ runtime.dispose();
948
+ throw new Error("workflow-code web currently supports Conversation and Workflow projects.");
949
+ }
950
+ const project = { ...descriptor, uri: pathToFileURL(projectDir).href };
951
+ let saved = {};
952
+ try {
953
+ saved = JSON.parse(await readFile(historyPath, "utf8"));
954
+ } catch (error) {
955
+ if (error.code !== "ENOENT") {
956
+ runtime.dispose();
957
+ throw new Error(`Cannot read conversation history: ${String(error)}`);
958
+ }
959
+ }
960
+ let pendingSave = Promise.resolve();
961
+ let broadcastTimer;
962
+ let authRevision = 0;
963
+ const parameterStates = /* @__PURE__ */ new Map();
964
+ const parameterEpochs = /* @__PURE__ */ new Map();
965
+ const publicFile = (file) => ({ ...file, previewUrl: `/api/file/${encodeURIComponent(file.id)}` });
966
+ const publicValue = (value) => {
967
+ if (Array.isArray(value)) {
968
+ return value.map(publicValue);
969
+ }
970
+ if (value && typeof value === "object") {
971
+ const record2 = value;
972
+ if (typeof record2.id === "string" && /^wf_[a-f0-9]{32}$/.test(record2.id) && typeof record2.mimeType === "string") {
973
+ return publicFile(record2);
974
+ }
975
+ return Object.fromEntries(Object.entries(record2).map(([name, entry]) => [name, publicValue(entry)]));
976
+ }
977
+ return value;
978
+ };
979
+ const state = () => publicValue(redactWorkflowData({ type: "state", project, sessions: controller.sessions, activeId: controller.activeId, running: controller.running, parameterValues: controller.current.parameterValues ?? {}, account, authenticationRevision: authRevision }, [connection?.accessToken ?? ""]));
980
+ const broadcast = () => {
981
+ if (broadcastTimer) {
982
+ return;
983
+ }
984
+ broadcastTimer = setTimeout(() => {
985
+ broadcastTimer = void 0;
986
+ const data = `data: ${JSON.stringify(state())}
987
+
988
+ `;
989
+ for (const client of clients) {
990
+ client.write(data);
991
+ }
992
+ }, 30);
993
+ };
994
+ const controller = new ConversationController(project, runtime, saved, {
995
+ changed: broadcast,
996
+ log: (message) => process.stderr.write(`${message}
997
+ `),
998
+ save: (sessions, activeId) => {
999
+ const content = JSON.stringify({ sessions, activeId });
1000
+ pendingSave = pendingSave.catch(() => void 0).then(async () => {
1001
+ await mkdir(path2.dirname(historyPath), { recursive: true });
1002
+ const temp = `${historyPath}.${process.pid}.tmp`;
1003
+ await writeFile(temp, content, { mode: 384 });
1004
+ await rename(temp, historyPath);
1005
+ });
1006
+ return pendingSave;
1007
+ }
1008
+ });
1009
+ const refreshAuth = async () => {
1010
+ const config = await readCliAuthConfig();
1011
+ const serverUrl = process.env.WORKFLOW_SERVER_URL ?? config.serverUrl;
1012
+ const token = process.env.WORKFLOW_SERVER_ADMIN_KEY ?? config.apiKey;
1013
+ const identity = signedOut ? "" : `${serverUrl}
1014
+ ${token}`;
1015
+ if (identity === authIdentity && account.status !== "checking") {
1016
+ return;
1017
+ }
1018
+ authIdentity = identity;
1019
+ connection = !signedOut && serverUrl && token ? { serverUrl: serverUrl.replace(/\/+$/, ""), accessToken: token } : void 0;
1020
+ authRevision++;
1021
+ parameterStates.clear();
1022
+ for (const session of controller.sessions) {
1023
+ controller.stop(session);
1024
+ }
1025
+ account = { status: connection ? "checking" : "signed-out", serverUrl: serverUrl || "http://localhost:7126", revision: authRevision };
1026
+ if (connection) {
1027
+ try {
1028
+ const response = await fetch(`${connection.serverUrl}/api/auth/me`, { headers: { Authorization: `Bearer ${connection.accessToken}` }, signal: AbortSignal.timeout(1e4), redirect: "error" });
1029
+ const result = await response.json();
1030
+ if (!response.ok || result.errCode !== 0 || !result.data?.profile) {
1031
+ throw new Error("Workflow login is invalid or expired.");
1032
+ }
1033
+ account = { ...account, status: "authenticated", user: result.data.profile, expiresAt: result.data.principal?.expiresAt ?? config.expiresAt };
1034
+ } catch (error) {
1035
+ connection = void 0;
1036
+ account = { ...account, status: "error", error: String(error.message) };
1037
+ }
1038
+ }
1039
+ broadcast();
1040
+ };
1041
+ await refreshAuth();
1042
+ const authPoll = setInterval(() => void refreshAuth().catch((error) => process.stderr.write(`${String(error)}
1043
+ `)), 5e3);
1044
+ authPoll.unref();
1045
+ const capability = randomBytes(32).toString("hex");
1046
+ const cookie = `workflow_web_${randomBytes(8).toString("hex")}`;
1047
+ let origin = "";
1048
+ const json = (response, status, value) => {
1049
+ response.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
1050
+ response.end(JSON.stringify(value));
1051
+ };
1052
+ const authMatches = (value) => {
1053
+ const a = Buffer.from(value), b = Buffer.from(capability);
1054
+ return a.length === b.length && timingSafeEqual(a, b);
1055
+ };
1056
+ const readBody = async (request) => {
1057
+ let size = 0;
1058
+ const chunks = [];
1059
+ for await (const chunk of request) {
1060
+ size += chunk.length;
1061
+ if (size > 72 * 1024 * 1024) {
1062
+ throw new Error("Request exceeds 72 MiB.");
1063
+ }
1064
+ chunks.push(chunk);
1065
+ }
1066
+ const value = JSON.parse(Buffer.concat(chunks).toString("utf8"));
1067
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1068
+ throw new Error("Invalid request.");
1069
+ }
1070
+ return value;
1071
+ };
1072
+ const readAttachments = async (files) => {
1073
+ if (!Array.isArray(files) || files.length > 32) {
1074
+ throw new Error("Invalid attachments.");
1075
+ }
1076
+ return Promise.all(files.map(async (file) => {
1077
+ if (typeof file?.id !== "string") {
1078
+ throw new Error("Invalid attachment.");
1079
+ }
1080
+ const { reference } = await runtime.readFile(project, file.id);
1081
+ if (reference.conversationId !== controller.activeId) {
1082
+ throw new Error("Attachment belongs to another conversation.");
1083
+ }
1084
+ return reference;
1085
+ }));
1086
+ };
1087
+ const handleMessage = async (message) => {
1088
+ if (typeof message.type !== "string") {
1089
+ throw new Error("Invalid message.");
1090
+ }
1091
+ if (message.projectUri && message.projectUri !== project.uri) {
1092
+ throw new Error("Project changed.");
1093
+ }
1094
+ if (message.sessionId && message.sessionId !== controller.activeId) {
1095
+ throw new Error("Conversation changed.");
1096
+ }
1097
+ const replies = [];
1098
+ const current = controller.current;
1099
+ const entrypointId = typeof message.entrypointId === "string" && message.entrypointId ? message.entrypointId : project.defaultEntrypoint;
1100
+ if (entrypointId && !project.entrypoints.some((entry) => entry.id === entrypointId)) {
1101
+ throw new Error("Unknown entrypoint.");
1102
+ }
1103
+ switch (message.type) {
1104
+ case "ready":
1105
+ replies.push(state());
1106
+ break;
1107
+ case "resolve": {
1108
+ try {
1109
+ const parameters = workflowParameters(project, entrypointId);
1110
+ const supplied = message.params ?? {};
1111
+ if (!supplied || typeof supplied !== "object" || Array.isArray(supplied) || Object.values(supplied).some((value) => typeof value !== "string" && !(Array.isArray(value) && value.every((entry) => typeof entry === "string")))) {
1112
+ throw new Error("Invalid parameters.");
1113
+ }
1114
+ const values = Object.fromEntries(parameters.fields.filter((field) => supplied[field.name] !== void 0).map((field) => [field.name, supplied[field.name]]));
1115
+ const stateKey = `${current.id}:${entrypointId ?? ""}`;
1116
+ const epoch = (parameterEpochs.get(stateKey) ?? 0) + 1;
1117
+ parameterEpochs.set(stateKey, epoch);
1118
+ const revision = authRevision;
1119
+ const states = parameterStates.get(stateKey) ?? {};
1120
+ const trigger = message.trigger?.kind === "param" && parameters.fields.some((field) => field.name === message.trigger.paramName) ? { kind: "param", paramName: message.trigger.paramName } : { kind: message.trigger?.kind === "input" ? "input" : "init" };
1121
+ const result = parameters.paramResolver?.enabled ? await runtime.resolveParams(project, { draftText: String(message.draftText ?? "").slice(0, 2e4), args: workflowParamArgs(parameters.fields, values, states), inputMode: "form", trigger }, entrypointId) : { params: {} };
1122
+ if (parameterEpochs.get(stateKey) !== epoch || revision !== authRevision) {
1123
+ break;
1124
+ }
1125
+ for (const [name, patch] of Object.entries(result.params ?? {})) {
1126
+ states[name] = { ...states[name], ...patch, options: { ...states[name]?.options, ...patch.options } };
1127
+ if (patch.value === null) {
1128
+ delete values[name];
1129
+ } else if (patch.value !== void 0) {
1130
+ values[name] = patch.value;
1131
+ }
1132
+ }
1133
+ parameterStates.set(stateKey, states);
1134
+ (current.parameterValues ??= {})[entrypointId ?? ""] = values;
1135
+ await controller.persist();
1136
+ replies.push({ type: "params", projectUri: project.uri, sessionId: current.id, requestId: message.requestId, params: result.params });
1137
+ } catch (error) {
1138
+ replies.push({ type: "params", projectUri: project.uri, sessionId: current.id, requestId: message.requestId, error: String(error.message) });
1139
+ }
1140
+ break;
1141
+ }
1142
+ case "send": {
1143
+ if (typeof message.text !== "string" || message.text.length > 1e5) {
1144
+ throw new Error("Invalid message text.");
1145
+ }
1146
+ const params = message.params;
1147
+ if (!params || typeof params !== "object" || Array.isArray(params) || Object.values(params).some((value) => typeof value !== "string" && !(Array.isArray(value) && value.every((entry) => typeof entry === "string")))) {
1148
+ throw new Error("Invalid parameters.");
1149
+ }
1150
+ const parameters = workflowParameters(project, entrypointId);
1151
+ const stateKey = `${current.id}:${entrypointId ?? ""}`;
1152
+ parameterEpochs.set(stateKey, (parameterEpochs.get(stateKey) ?? 0) + 1);
1153
+ const revision = authRevision;
1154
+ const resolved = parameters.paramResolver?.enabled ? await runtime.resolveParams(project, { draftText: message.text, args: workflowParamArgs(parameters.fields, params), inputMode: "form", trigger: { kind: "init" } }, entrypointId) : { params: parameterStates.get(`${current.id}:${entrypointId ?? ""}`) ?? {} };
1155
+ if (revision !== authRevision) {
1156
+ throw new Error("Workflow login changed. Send the message again.");
1157
+ }
1158
+ validateWorkflowValues(parameters.fields, params, resolved.params);
1159
+ (current.parameterValues ??= {})[entrypointId ?? ""] = { ...params };
1160
+ await controller.send({ text: message.text, params, states: resolved.params, entrypointId, attachments: await readAttachments(message.attachments ?? []) });
1161
+ replies.push({ type: "sent", sessionId: current.id });
1162
+ break;
1163
+ }
1164
+ case "stop":
1165
+ controller.stop();
1166
+ break;
1167
+ case "new":
1168
+ await controller.updateDraft(current.id, String(message.text ?? ""), await readAttachments(message.attachments ?? []));
1169
+ await controller.create();
1170
+ break;
1171
+ case "select":
1172
+ await controller.select(String(message.id));
1173
+ break;
1174
+ case "draft":
1175
+ await controller.updateDraft(current.id, String(message.text ?? ""), await readAttachments(message.attachments ?? []));
1176
+ break;
1177
+ case "retry":
1178
+ await controller.retry(String(message.messageId));
1179
+ break;
1180
+ case "resume":
1181
+ await controller.resume(String(message.messageId), String(message.requestId), message.values ?? {});
1182
+ break;
1183
+ case "tool-policy": {
1184
+ const tools = workflowParameters(project, entrypointId).tools ?? [];
1185
+ current.toolPermissionPolicies = Object.fromEntries(tools.map((tool) => [tool.name, { enabled: typeof message.policies?.[tool.name]?.enabled === "boolean" ? message.policies[tool.name].enabled : tool.enabled, autoApprove: typeof message.policies?.[tool.name]?.autoApprove === "boolean" ? message.policies[tool.name].autoApprove : tool.autoApprove }]));
1186
+ await controller.persist();
1187
+ broadcast();
1188
+ break;
1189
+ }
1190
+ case "queue-edit":
1191
+ await controller.editQueued(String(message.id));
1192
+ break;
1193
+ case "queue-remove":
1194
+ await controller.removeQueued(String(message.id));
1195
+ break;
1196
+ case "queue-cancel-edit":
1197
+ await controller.cancelQueueEdit();
1198
+ break;
1199
+ case "queue-run":
1200
+ await controller.runQueued();
1201
+ break;
1202
+ case "queue-reorder":
1203
+ if (Array.isArray(message.ids) && message.ids.every((id) => typeof id === "string")) {
1204
+ await controller.reorderQueued(message.ids);
1205
+ }
1206
+ break;
1207
+ case "queue-promote":
1208
+ await controller.promoteQueued(String(message.id));
1209
+ break;
1210
+ case "session-action": {
1211
+ const session = controller.sessions.find((entry) => entry.id === message.id);
1212
+ if (!session) {
1213
+ throw new Error("Conversation no longer exists.");
1214
+ }
1215
+ if (message.action === "rename") {
1216
+ await controller.rename(session.id, String(message.title ?? ""));
1217
+ } else if (message.action === "lock") {
1218
+ await controller.lockTitle(session.id, !session.titleLocked);
1219
+ } else if (message.action === "delete") {
1220
+ await controller.remove(session.id);
1221
+ } else if (message.action === "export") {
1222
+ replies.push({ type: "download", name: `conversation-${session.id}.json`, content: JSON.stringify(redactWorkflowData(session), null, 2) });
1223
+ }
1224
+ break;
1225
+ }
1226
+ case "upload": {
1227
+ if (!Array.isArray(message.files) || message.files.length > 32 || message.files.some((file) => typeof file?.name !== "string" || typeof file.data !== "string" || file.path !== void 0)) {
1228
+ throw new Error("Invalid upload.");
1229
+ }
1230
+ const files = await runtime.uploadFiles(project, current.id, message.files);
1231
+ replies.push({ type: "attachments", projectUri: project.uri, sessionId: current.id, paramName: message.paramName, messageId: message.messageId, requestId: message.requestId, files: files.map(publicFile) });
1232
+ break;
1233
+ }
1234
+ case "trace":
1235
+ case "logs":
1236
+ case "export-message": {
1237
+ const answer = current.messages.find((entry) => entry.id === message.messageId);
1238
+ if (!answer) {
1239
+ throw new Error("Message no longer exists.");
1240
+ }
1241
+ if (message.type === "export-message") {
1242
+ replies.push({ type: "download", name: `run-${answer.id}.json`, content: JSON.stringify(redactWorkflowData(answer), null, 2) }, { type: "exported", sessionId: current.id, messageId: answer.id });
1243
+ } else {
1244
+ replies.push({ type: "preview", title: message.type === "logs" ? "\u8FD0\u884C\u65E5\u5FD7" : "\u8FD0\u884C\u8DDF\u8E2A", content: message.type === "logs" ? answer.report?.logs ?? "" : answer.report });
1245
+ }
1246
+ break;
1247
+ }
1248
+ case "resource": {
1249
+ const answer = current.messages.find((entry) => entry.id === message.messageId);
1250
+ const card = conversationResourceCards(answer?.items ?? [], project.id, current.id).find((entry) => entry.id === message.cardId);
1251
+ if (!card || card.target.projectId && card.target.projectId !== project.id || card.target.source === "server") {
1252
+ throw new Error("Resource is outside this project.");
1253
+ }
1254
+ if (card.kind === "kv" || card.kind === "knowledge") {
1255
+ replies.push({ type: "preview", title: card.title, content: await runtime.readResource(project, current.id, card.kind, card.target) });
1256
+ } else if (card.kind === "file") {
1257
+ const filename = await realpath(path2.resolve(projectDir, card.target.filePath));
1258
+ const relative = path2.relative(projectDir, filename);
1259
+ if (relative.startsWith("..") || path2.isAbsolute(relative)) {
1260
+ throw new Error("File is outside this project.");
1261
+ }
1262
+ replies.push({ type: "preview", title: card.title, content: await readFile(filename, "utf8") });
1263
+ } else {
1264
+ throw new Error("This resource requires an editor host.");
1265
+ }
1266
+ break;
1267
+ }
1268
+ case "usage": {
1269
+ if (!connection) {
1270
+ throw new Error("Workflow Server login is not configured.");
1271
+ }
1272
+ const response = await fetch(`${connection.serverUrl}/api/auth/sub2api/catalog`, { headers: { Authorization: `Bearer ${connection.accessToken}` }, signal: AbortSignal.timeout(15e3), redirect: "error" });
1273
+ const result = await response.json();
1274
+ if (!response.ok || result.errCode !== 0) {
1275
+ throw new Error(result.errMessage || "Cannot load usage.");
1276
+ }
1277
+ replies.push({ type: "usage", sessionId: current.id, usage: result.data });
1278
+ break;
1279
+ }
1280
+ case "login": {
1281
+ if (process.env.WORKFLOW_SERVER_ADMIN_KEY !== void 0) {
1282
+ throw new Error("This session is supplied by the host. Sign in through the host.");
1283
+ }
1284
+ if (login) {
1285
+ break;
1286
+ }
1287
+ const url = new URL(String(message.serverUrl));
1288
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
1289
+ throw new Error("Invalid Workflow Server URL.");
1290
+ }
1291
+ signedOut = false;
1292
+ account = { ...account, status: "checking" };
1293
+ broadcast();
1294
+ login = spawn2(process.execPath, [path2.join(cliRoot(), "dist/index.js"), "workspace", "login", "--server", url.toString().replace(/\/$/, "")], { shell: false, stdio: ["ignore", "ignore", "pipe"] });
1295
+ login.stderr?.on("data", (chunk) => process.stderr.write(chunk));
1296
+ login.once("error", (error) => {
1297
+ login = void 0;
1298
+ account = { ...account, status: "error", error: error.message };
1299
+ broadcast();
1300
+ });
1301
+ login.once("close", () => {
1302
+ login = void 0;
1303
+ authIdentity = "";
1304
+ void refreshAuth();
1305
+ });
1306
+ break;
1307
+ }
1308
+ case "logout":
1309
+ signedOut = true;
1310
+ login?.kill();
1311
+ if (process.env.WORKFLOW_SERVER_ADMIN_KEY === void 0) {
1312
+ await clearCliAuthConfig();
1313
+ }
1314
+ authIdentity = "signed-out";
1315
+ await refreshAuth();
1316
+ break;
1317
+ default:
1318
+ throw new Error(`Unsupported action: ${message.type}`);
1319
+ }
1320
+ return replies;
1321
+ };
1322
+ let mutations = Promise.resolve();
1323
+ const server = createServer((request, response) => {
1324
+ void (async () => {
1325
+ response.setHeader("X-Content-Type-Options", "nosniff");
1326
+ response.setHeader("Referrer-Policy", "no-referrer");
1327
+ if (request.headers.host !== new URL(origin).host || request.headers.origin && request.headers.origin !== origin || request.headers["sec-fetch-site"] === "cross-site") {
1328
+ json(response, 403, { message: "Invalid request origin." });
1329
+ return;
1330
+ }
1331
+ const url = new URL(request.url ?? "/", origin);
1332
+ if (url.pathname === "/api/session" && request.method === "POST") {
1333
+ if (!authMatches(String(request.headers.authorization ?? "").replace(/^Bearer /, ""))) {
1334
+ json(response, 401, { message: "Invalid session." });
1335
+ return;
1336
+ }
1337
+ response.setHeader("Set-Cookie", `${cookie}=${capability}; HttpOnly; SameSite=Strict; Path=/`);
1338
+ json(response, 200, { ok: true });
1339
+ return;
1340
+ }
1341
+ if (url.pathname.startsWith("/api/")) {
1342
+ const value = String(request.headers.cookie ?? "").split(";").map((value2) => value2.trim()).find((value2) => value2.startsWith(`${cookie}=`))?.slice(cookie.length + 1) ?? "";
1343
+ if (!authMatches(value)) {
1344
+ json(response, 401, { message: "Open the URL printed by workflow-code web." });
1345
+ return;
1346
+ }
1347
+ if (url.pathname === "/api/events" && request.method === "GET") {
1348
+ response.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
1349
+ response.write(`data: ${JSON.stringify(state())}
1350
+
1351
+ `);
1352
+ clients.add(response);
1353
+ const heartbeat = setInterval(() => response.write(": heartbeat\n\n"), 2e4);
1354
+ response.once("close", () => {
1355
+ clearInterval(heartbeat);
1356
+ clients.delete(response);
1357
+ });
1358
+ return;
1359
+ }
1360
+ if (url.pathname === "/api/message" && request.method === "POST") {
1361
+ const message = await readBody(request);
1362
+ let messages = [];
1363
+ const operation = () => handleMessage(message).then((value2) => {
1364
+ messages = value2;
1365
+ });
1366
+ const task = message.type === "resolve" || message.type === "ready" ? operation() : mutations.catch(() => void 0).then(operation);
1367
+ if (message.type !== "resolve" && message.type !== "ready") {
1368
+ mutations = task;
1369
+ }
1370
+ await task;
1371
+ json(response, 200, { messages: redactWorkflowData(messages, [connection?.accessToken ?? ""]) });
1372
+ return;
1373
+ }
1374
+ if (url.pathname.startsWith("/api/file/") && request.method === "GET") {
1375
+ const file = await runtime.readFile(project, decodeURIComponent(url.pathname.slice("/api/file/".length)));
1376
+ const bytes = await readFile(file.path);
1377
+ const safeType = /^(?:image\/(?:png|jpeg|gif|webp|avif)|audio\/[a-z0-9.+-]+|video\/[a-z0-9.+-]+)$/.test(file.reference.mimeType);
1378
+ response.writeHead(200, { "Content-Type": safeType ? file.reference.mimeType : "application/octet-stream", "Content-Disposition": `${safeType ? "inline" : "attachment"}; filename*=UTF-8''${encodeURIComponent(file.reference.name)}`, "Cache-Control": "no-store" });
1379
+ response.end(bytes);
1380
+ return;
1381
+ }
1382
+ json(response, 404, { message: "Unknown API endpoint." });
1383
+ return;
1384
+ }
1385
+ if (request.method !== "GET" && request.method !== "HEAD") {
1386
+ json(response, 405, { message: "Method not allowed." });
1387
+ return;
1388
+ }
1389
+ if (url.pathname === "/favicon.ico") {
1390
+ response.writeHead(204);
1391
+ response.end();
1392
+ return;
1393
+ }
1394
+ if (url.pathname === "/") {
1395
+ response.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store", "Content-Security-Policy": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https: http:; font-src 'self'; connect-src 'self'; media-src 'self' blob:; object-src 'none'; base-uri 'none'; frame-ancestors 'self' vscode-webview:; form-action 'self'" });
1396
+ response.end(`<!doctype html><html lang="zh-CN" data-theme="${options.theme ?? "auto"}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="workflow-project" content="${key}"><title>Workflow</title><link rel="stylesheet" href="/assets/codicons/codicon.css"><link rel="stylesheet" href="/assets/theme.css"><link rel="stylesheet" href="/assets/workflow-ui.css"></head><body data-surface="editor"><div id="app"></div><script src="/assets/workflow-ui.js" defer></script></body></html>`);
1397
+ return;
1398
+ }
1399
+ const assets = { "/assets/workflow-ui.js": "application/javascript", "/assets/workflow-ui.css": "text/css", "/assets/theme.css": "text/css", "/assets/codicons/codicon.css": "text/css", "/assets/codicons/codicon.ttf": "font/ttf" };
1400
+ if (!assets[url.pathname]) {
1401
+ json(response, 404, { message: "Not found." });
1402
+ return;
1403
+ }
1404
+ const content = await readFile(path2.join(cliRoot(), "dist/web", url.pathname.slice("/assets/".length)));
1405
+ response.writeHead(200, { "Content-Type": assets[url.pathname], "Cache-Control": "no-cache" });
1406
+ response.end(content);
1407
+ })().catch((error) => {
1408
+ if (!response.headersSent) {
1409
+ json(response, 400, { message: String(redactWorkflowData(error.message, [connection?.accessToken ?? ""])) });
1410
+ } else {
1411
+ response.end();
1412
+ }
1413
+ });
1414
+ });
1415
+ try {
1416
+ await new Promise((resolve, reject) => {
1417
+ server.once("error", reject);
1418
+ server.listen(options.port ?? 0, "127.0.0.1", () => {
1419
+ server.off("error", reject);
1420
+ resolve();
1421
+ });
1422
+ });
1423
+ } catch (error) {
1424
+ clearInterval(authPoll);
1425
+ controller.dispose();
1426
+ runtime.dispose();
1427
+ throw error;
1428
+ }
1429
+ origin = `http://127.0.0.1:${server.address().port}`;
1430
+ let closing;
1431
+ return {
1432
+ origin,
1433
+ url: `${origin}/#session=${capability}`,
1434
+ project,
1435
+ controller,
1436
+ close: () => closing ??= (async () => {
1437
+ clearInterval(authPoll);
1438
+ if (broadcastTimer) {
1439
+ clearTimeout(broadcastTimer);
1440
+ }
1441
+ login?.kill();
1442
+ controller.dispose();
1443
+ runtime.dispose();
1444
+ for (const client of clients) {
1445
+ client.end();
1446
+ }
1447
+ await new Promise((resolve) => server.close(() => resolve()));
1448
+ await controller.idle();
1449
+ await controller.persist();
1450
+ await pendingSave;
1451
+ })()
1452
+ };
1453
+ }
1454
+
1455
+ // src/web.ts
1456
+ function parseWebArgs(argv) {
1457
+ const options = { projectDir: process.cwd(), port: 0, theme: "auto", open: true, json: false, help: false };
1458
+ let projectSet = false;
1459
+ for (let index = 0; index < argv.length; index++) {
1460
+ const arg = argv[index];
1461
+ if (arg === "--no-open") {
1462
+ options.open = false;
1463
+ } else if (arg === "--json") {
1464
+ options.json = true;
1465
+ } else if (["--help", "-h"].includes(arg)) {
1466
+ options.help = true;
1467
+ } else if (["--port", "--data-dir", "--theme"].includes(arg)) {
1468
+ const value = argv[++index];
1469
+ if (!value || value.startsWith("--")) {
1470
+ throw new Error(`${arg} requires a value.`);
1471
+ }
1472
+ if (arg === "--port") {
1473
+ if (!/^\d+$/.test(value) || Number(value) > 65535) {
1474
+ throw new Error("--port must be an integer between 0 and 65535.");
1475
+ }
1476
+ options.port = Number(value);
1477
+ } else if (arg === "--theme") {
1478
+ if (!["auto", "light", "dark"].includes(value)) {
1479
+ throw new Error("--theme must be auto, light, or dark.");
1480
+ }
1481
+ options.theme = value;
1482
+ } else {
1483
+ options.dataDir = path3.resolve(value);
1484
+ }
1485
+ } else if (arg.startsWith("-") || projectSet) {
1486
+ throw new Error(`Unknown web argument: ${arg}`);
1487
+ } else {
1488
+ options.projectDir = path3.resolve(arg);
1489
+ projectSet = true;
1490
+ }
1491
+ }
1492
+ return options;
1493
+ }
1494
+ async function main(argv) {
1495
+ const options = parseWebArgs(argv);
1496
+ if (options.help) {
1497
+ console.log(i18n.language.startsWith("zh") ? "workflow-code web [\u9879\u76EE\u76EE\u5F55]\n --port <0-65535> \u76D1\u542C\u7AEF\u53E3\uFF0C0 \u81EA\u52A8\u5206\u914D\n --no-open \u4E0D\u81EA\u52A8\u6253\u5F00\u6D4F\u89C8\u5668\n --theme <auto|light|dark>\n --data-dir <\u76EE\u5F55> \u5BF9\u8BDD\u548C\u6587\u4EF6\u5B58\u50A8\u4F4D\u7F6E\n --json \u8F93\u51FA\u7ED3\u6784\u5316\u542F\u52A8\u4FE1\u606F" : "workflow-code web [project-directory]\n --port <0-65535> Listen port; 0 selects an available port\n --no-open Do not open a browser\n --theme <auto|light|dark>\n --data-dir <path> Conversation and file storage directory\n --json Print structured startup information");
1498
+ return 0;
1499
+ }
1500
+ const app = await startWebServer(options);
1501
+ const close = () => {
1502
+ process.off("SIGINT", close);
1503
+ process.off("SIGTERM", close);
1504
+ void app.close().catch((error) => {
1505
+ console.error(error);
1506
+ process.exitCode = 1;
1507
+ });
1508
+ };
1509
+ process.once("SIGINT", close);
1510
+ process.once("SIGTERM", close);
1511
+ console.log(options.json ? JSON.stringify({ type: "ready", protocolVersion: 1, url: app.url, project: { id: app.project.id, name: app.project.name, projectType: app.project.projectType }, runtime: app.project.runtime }) : `Workflow Web: ${app.url}`);
1512
+ if (options.open) {
1513
+ const invocation = process.platform === "darwin" ? { command: "open", args: [app.url] } : process.platform === "win32" ? { command: "rundll32.exe", args: ["url.dll,FileProtocolHandler", app.url] } : { command: "xdg-open", args: [app.url] };
1514
+ const browser = spawn3(invocation.command, invocation.args, { detached: true, shell: false, stdio: "ignore" });
1515
+ browser.once("error", (error) => console.error(`Cannot open browser: ${error.message}`));
1516
+ browser.unref();
1517
+ }
1518
+ return 0;
1519
+ }
1520
+ export {
1521
+ main,
1522
+ parseWebArgs
1523
+ };