@chenguangyao/devflow-kit 0.1.43 → 0.1.44

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,467 @@
1
+ 'use strict';
2
+
3
+ const adapterSurfaceSchema = require('../../schemas/workflow-adapter-surface.schema.json');
4
+ const adapterCatalogSchema = require('../../schemas/workflow-adapter-catalog.schema.json');
5
+
6
+ const SUPPORTED_PROFILES = new Set(['codex', 'cursor', 'browser', 'cli']);
7
+ const SUPPORTED_RENDER_MODES = new Set(['in_app_browser', 'webview_or_browser', 'browser', 'terminal_markdown']);
8
+ const PROTOCOL_VERSION = 'workflow-adapter-surface/v1';
9
+ const ADAPTER_SURFACE_SCHEMA = 'https://devflow.dev/schemas/workflow-adapter-surface.schema.json';
10
+ const ADAPTER_CATALOG_SCHEMA = 'https://devflow.dev/schemas/workflow-adapter-catalog.schema.json';
11
+ const WORKFLOW_SELECTION_SCHEMA = 'https://devflow.dev/schemas/workflow-selection.schema.json';
12
+ const WORKFLOW_UI_ERROR_SCHEMA = 'https://devflow.dev/schemas/workflow-ui-error.schema.json';
13
+ const WORKFLOW_UI_COMMAND_RESULT_SCHEMA = 'https://devflow.dev/schemas/workflow-ui-command-result.schema.json';
14
+ const ADAPTER_FEATURES = [
15
+ 'catalog-discovery',
16
+ 'chat-card',
17
+ 'chat-selection',
18
+ 'embedded-ui',
19
+ 'read-only-diff',
20
+ 'read-only-explain',
21
+ 'checkpoint-resolve',
22
+ 'controlled-confirm',
23
+ ];
24
+
25
+ const HOST_ALIASES = {
26
+ 'codex-app': 'codex',
27
+ vscode: 'browser',
28
+ web: 'browser',
29
+ terminal: 'cli',
30
+ shell: 'cli',
31
+ };
32
+
33
+ const PROFILE_CATALOG = [
34
+ {
35
+ id: 'codex',
36
+ label: 'Codex App',
37
+ defaultRenderMode: 'in_app_browser',
38
+ description: 'Use the Codex in-app browser for the interactive workflow UI and Markdown for chat fallback.',
39
+ recommendedHosts: ['codex', 'codex-app'],
40
+ },
41
+ {
42
+ id: 'cursor',
43
+ label: 'WebView host',
44
+ defaultRenderMode: 'webview_or_browser',
45
+ description: 'Use an IDE WebView when available, with the same local browser UI as fallback.',
46
+ recommendedHosts: ['cursor', 'windsurf'],
47
+ },
48
+ {
49
+ id: 'browser',
50
+ label: 'Browser host',
51
+ defaultRenderMode: 'browser',
52
+ description: 'Use the system browser or any generic tool that can open the local workflow UI URL.',
53
+ recommendedHosts: ['browser', 'jetbrains', 'zed'],
54
+ },
55
+ {
56
+ id: 'cli',
57
+ label: 'Terminal or Markdown host',
58
+ defaultRenderMode: 'terminal_markdown',
59
+ description: 'Print the Markdown chat card and submit edited workflow_chat_selection JSON through the local API.',
60
+ recommendedHosts: ['cli', 'terminal', 'shell'],
61
+ },
62
+ ];
63
+
64
+ const RENDER_MODE_CATALOG = [
65
+ {
66
+ id: 'in_app_browser',
67
+ label: 'In-app browser',
68
+ supportsEmbeddedUi: true,
69
+ description: 'A host-owned browser surface can open the local workflow UI directly.',
70
+ },
71
+ {
72
+ id: 'webview_or_browser',
73
+ label: 'WebView or browser',
74
+ supportsEmbeddedUi: true,
75
+ description: 'An IDE extension may embed the local UI in a WebView and fall back to the system browser.',
76
+ },
77
+ {
78
+ id: 'browser',
79
+ label: 'Browser',
80
+ supportsEmbeddedUi: false,
81
+ description: 'Open the local workflow UI in an external browser.',
82
+ },
83
+ {
84
+ id: 'terminal_markdown',
85
+ label: 'Terminal Markdown',
86
+ supportsEmbeddedUi: false,
87
+ description: 'Render the Markdown fallback and call the listed local API endpoints for explicit user actions.',
88
+ },
89
+ ];
90
+
91
+ function buildWorkflowAdapterSurface(options = {}) {
92
+ const slug = options.slug;
93
+ if (!slug) throw new Error('buildWorkflowAdapterSurface requires slug');
94
+ const host = normalizeAdapterHost(options.host || 'browser');
95
+ const profile = normalizeAdapterProfile(options.profile || profileForHost(host));
96
+ const renderMode = normalizeRenderMode(options.renderMode || options['render-mode'] || renderModeFor(profile));
97
+ const uiHost = options.uiHost || options.hostname || '127.0.0.1';
98
+ const port = options.port === undefined || options.port === null ? 8787 : Number(options.port);
99
+ if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error(`invalid adapter port: ${options.port}`);
100
+ const url = `http://${uiHost}:${port}/`;
101
+ const actions = {
102
+ startUi: `devflow flow ui --slug=${slug} --port=${port}`,
103
+ startUiJson: `devflow flow ui --slug=${slug} --port=${port} --json`,
104
+ statusApi: `${url}api/status`,
105
+ pickerApi: `${url}api/picker`,
106
+ diffApi: `${url}api/diff`,
107
+ explainApi: `${url}api/explain`,
108
+ applySelectionApi: `${url}api/apply-selection`,
109
+ chatSelectionApi: `${url}api/adapter/apply-selection`,
110
+ cardApi: `${url}api/card`,
111
+ checkpointResolveApi: `${url}api/checkpoint/resolve`,
112
+ confirmApi: `${url}api/confirm`,
113
+ };
114
+ const chatCard = buildChatCard({ host, profile, renderMode, slug, url, actions, pickerSurface: options.pickerSurface || null });
115
+ return {
116
+ type: 'workflow_adapter_surface',
117
+ protocolVersion: PROTOCOL_VERSION,
118
+ host,
119
+ profile,
120
+ slug,
121
+ renderMode,
122
+ url,
123
+ features: [...ADAPTER_FEATURES],
124
+ capabilities: capabilitiesFor(host, profile, renderMode),
125
+ actions,
126
+ chatCard,
127
+ instructions: instructionsFor(host, profile, renderMode, url, actions),
128
+ fallbackMarkdown: chatCard.markdown,
129
+ };
130
+ }
131
+
132
+ function buildWorkflowAdapterCatalog() {
133
+ return {
134
+ type: 'workflow_adapter_catalog',
135
+ protocolVersion: PROTOCOL_VERSION,
136
+ schema: ADAPTER_CATALOG_SCHEMA,
137
+ features: [...ADAPTER_FEATURES],
138
+ defaults: {
139
+ customHostProfile: 'browser',
140
+ customHostRenderMode: 'browser',
141
+ mutationModel: 'devflow-controlled',
142
+ },
143
+ profiles: PROFILE_CATALOG.map((profile) => ({ ...profile })),
144
+ renderModes: RENDER_MODE_CATALOG.map((mode) => ({ ...mode })),
145
+ hostAliases: { ...HOST_ALIASES },
146
+ examples: [
147
+ {
148
+ host: 'codex',
149
+ profile: 'codex',
150
+ renderMode: 'in_app_browser',
151
+ command: 'devflow flow adapter --slug=<slug> --host=codex --json',
152
+ },
153
+ {
154
+ host: 'windsurf',
155
+ profile: 'cursor',
156
+ renderMode: 'webview_or_browser',
157
+ command: 'devflow flow adapter --slug=<slug> --host=windsurf --profile=cursor --json',
158
+ },
159
+ {
160
+ host: 'jetbrains',
161
+ profile: 'browser',
162
+ renderMode: 'browser',
163
+ command: 'devflow flow adapter --slug=<slug> --host=jetbrains --render-mode=browser --json',
164
+ },
165
+ {
166
+ host: 'zed',
167
+ profile: 'cli',
168
+ renderMode: 'terminal_markdown',
169
+ command: 'devflow flow adapter --slug=<slug> --host=zed --profile=cli --json',
170
+ },
171
+ ],
172
+ safety: {
173
+ arbitraryShell: false,
174
+ mutationModel: 'devflow-controlled',
175
+ readOnlyActions: ['statusApi', 'pickerApi', 'diffApi', 'explainApi', 'cardApi'],
176
+ mutationActions: ['applySelectionApi', 'chatSelectionApi', 'checkpointResolveApi', 'confirmApi'],
177
+ },
178
+ };
179
+ }
180
+
181
+ function normalizeAdapterHost(host) {
182
+ const normalized = String(host || 'browser').trim().toLowerCase();
183
+ if (HOST_ALIASES[normalized]) return HOST_ALIASES[normalized];
184
+ if (!/^[a-z0-9][a-z0-9._-]*$/.test(normalized)) throw new Error(`invalid workflow adapter host: ${host}`);
185
+ return normalized;
186
+ }
187
+
188
+ function normalizeAdapterProfile(profile) {
189
+ const normalized = String(profile || 'browser').trim().toLowerCase();
190
+ if (normalized === 'codex-app') return 'codex';
191
+ if (normalized === 'vscode' || normalized === 'web') return 'browser';
192
+ if (normalized === 'webview') return 'cursor';
193
+ if (normalized === 'terminal' || normalized === 'shell' || normalized === 'markdown') return 'cli';
194
+ if (!SUPPORTED_PROFILES.has(normalized)) {
195
+ throw new Error(`unsupported workflow adapter profile: ${profile}. supported: codex, cursor, browser, cli`);
196
+ }
197
+ return normalized;
198
+ }
199
+
200
+ function profileForHost(host) {
201
+ if (SUPPORTED_PROFILES.has(host)) return host;
202
+ return 'browser';
203
+ }
204
+
205
+ function normalizeRenderMode(renderMode) {
206
+ const normalized = String(renderMode || 'browser').trim().toLowerCase().replace(/-/g, '_');
207
+ if (!SUPPORTED_RENDER_MODES.has(normalized)) {
208
+ throw new Error(`unsupported workflow adapter render mode: ${renderMode}. supported: in_app_browser, webview_or_browser, browser, terminal_markdown`);
209
+ }
210
+ return normalized;
211
+ }
212
+
213
+ function renderModeFor(profile) {
214
+ if (profile === 'codex') return 'in_app_browser';
215
+ if (profile === 'cursor') return 'webview_or_browser';
216
+ if (profile === 'cli') return 'terminal_markdown';
217
+ return 'browser';
218
+ }
219
+
220
+ function capabilitiesFor(host, profile, renderMode) {
221
+ return {
222
+ schema: ADAPTER_SURFACE_SCHEMA,
223
+ selectionSchema: WORKFLOW_SELECTION_SCHEMA,
224
+ errorSchema: WORKFLOW_UI_ERROR_SCHEMA,
225
+ commandResultSchema: WORKFLOW_UI_COMMAND_RESULT_SCHEMA,
226
+ host,
227
+ profile,
228
+ renderMode,
229
+ features: [...ADAPTER_FEATURES],
230
+ supportsChatCard: true,
231
+ supportsChatSelection: true,
232
+ supportsNativeCheckboxes: false,
233
+ supportsEmbeddedUi: renderMode === 'in_app_browser' || renderMode === 'webview_or_browser',
234
+ supportsReadOnlyDiff: true,
235
+ supportsReadOnlyExplain: true,
236
+ supportsApplySelection: true,
237
+ supportsCheckpointResolve: true,
238
+ supportsConfirm: true,
239
+ mutationModel: 'devflow-controlled',
240
+ };
241
+ }
242
+
243
+ function validateWorkflowAdapterSurface(surface) {
244
+ return validateAgainstSchema(surface, adapterSurfaceSchema, adapterSurfaceSchema, '');
245
+ }
246
+
247
+ function assertWorkflowAdapterSurface(surface) {
248
+ const errors = validateWorkflowAdapterSurface(surface);
249
+ if (errors.length) {
250
+ throw new Error(`workflow adapter surface contract violation:\n- ${errors.join('\n- ')}`);
251
+ }
252
+ }
253
+
254
+ function validateWorkflowAdapterCatalog(catalog) {
255
+ return validateAgainstSchema(catalog, adapterCatalogSchema, adapterCatalogSchema, '');
256
+ }
257
+
258
+ function assertWorkflowAdapterCatalog(catalog) {
259
+ const errors = validateWorkflowAdapterCatalog(catalog);
260
+ if (errors.length) {
261
+ throw new Error(`workflow adapter catalog contract violation:\n- ${errors.join('\n- ')}`);
262
+ }
263
+ }
264
+
265
+ function validateAgainstSchema(value, schema, rootSchema, path) {
266
+ const errors = [];
267
+ if (!schema) return errors;
268
+ if (schema.$ref) {
269
+ return validateAgainstSchema(value, resolveRef(rootSchema, schema.$ref), rootSchema, path);
270
+ }
271
+ if (Object.prototype.hasOwnProperty.call(schema, 'const') && value !== schema.const) {
272
+ errors.push(`${formatPath(path)} must equal ${JSON.stringify(schema.const)}`);
273
+ }
274
+ if (schema.enum && !schema.enum.includes(value)) {
275
+ errors.push(`${formatPath(path)} must be one of ${schema.enum.map((item) => JSON.stringify(item)).join(', ')}`);
276
+ }
277
+ if (schema.type && !matchesType(value, schema.type)) {
278
+ errors.push(`${formatPath(path)} must be ${Array.isArray(schema.type) ? schema.type.join(' or ') : schema.type}`);
279
+ return errors;
280
+ }
281
+ if (schema.required && isPlainObject(value)) {
282
+ for (const key of schema.required) {
283
+ if (!Object.prototype.hasOwnProperty.call(value, key) || value[key] === undefined) {
284
+ errors.push(`${formatPath(joinPath(path, key))} is required`);
285
+ }
286
+ }
287
+ }
288
+ if (schema.properties && isPlainObject(value)) {
289
+ for (const [key, childSchema] of Object.entries(schema.properties)) {
290
+ if (Object.prototype.hasOwnProperty.call(value, key) && value[key] !== undefined) {
291
+ errors.push(...validateAgainstSchema(value[key], childSchema, rootSchema, joinPath(path, key)));
292
+ }
293
+ }
294
+ }
295
+ if (schema.items && Array.isArray(value)) {
296
+ value.forEach((item, index) => {
297
+ errors.push(...validateAgainstSchema(item, schema.items, rootSchema, `${path}[${index}]`));
298
+ });
299
+ }
300
+ return errors;
301
+ }
302
+
303
+ function resolveRef(rootSchema, ref) {
304
+ if (!ref.startsWith('#/')) throw new Error(`unsupported schema ref: ${ref}`);
305
+ return ref.slice(2).split('/').reduce((node, part) => node && node[part], rootSchema);
306
+ }
307
+
308
+ function matchesType(value, type) {
309
+ if (Array.isArray(type)) return type.some((entry) => matchesType(value, entry));
310
+ if (type === 'array') return Array.isArray(value);
311
+ if (type === 'object') return isPlainObject(value);
312
+ if (type === 'null') return value === null;
313
+ return typeof value === type;
314
+ }
315
+
316
+ function isPlainObject(value) {
317
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
318
+ }
319
+
320
+ function joinPath(base, key) {
321
+ return base ? `${base}.${key}` : key;
322
+ }
323
+
324
+ function formatPath(path) {
325
+ return path || '<root>';
326
+ }
327
+
328
+ function buildChatCard({ host, profile, renderMode, slug, url, actions, pickerSurface }) {
329
+ const hostLine = chatHostLine(host, profile, renderMode);
330
+ const selection = buildChatSelection(pickerSurface);
331
+ const skillLines = renderSkillSelection(selection);
332
+ return {
333
+ type: 'workflow_chat_card',
334
+ format: 'markdown',
335
+ title: 'Workflow 编排',
336
+ selection,
337
+ markdown: [
338
+ '## Workflow 编排',
339
+ '',
340
+ `- change: \`${slug}\``,
341
+ `- host: \`${host}\``,
342
+ `- profile: \`${profile}\``,
343
+ `- render mode: \`${renderMode}\``,
344
+ `- surface: ${hostLine}`,
345
+ '',
346
+ `[打开可交互编排卡片](${url})`,
347
+ '',
348
+ '**可用操作**',
349
+ `- 启动 UI:\`${actions.startUi}\``,
350
+ `- 状态接口:\`${actions.statusApi}\``,
351
+ `- diff/explain:\`${actions.diffApi}\` / \`${actions.explainApi}\``,
352
+ `- 应用选择:\`${actions.chatSelectionApi}\``,
353
+ `- 确认 workflow:\`${actions.confirmApi}\``,
354
+ ...skillLines,
355
+ '',
356
+ '提示:聊天卡片只负责展示;状态变更仍由 devflow CLI / local UI API 执行。',
357
+ ].join('\n'),
358
+ };
359
+ }
360
+
361
+ function buildChatSelection(pickerSurface) {
362
+ const groups = Array.isArray(pickerSurface && pickerSurface.groups) ? pickerSurface.groups : [];
363
+ return {
364
+ type: 'workflow_chat_selection',
365
+ layout: 'linear-checkboxes',
366
+ groups: groups.map((group) => ({
367
+ id: group.id,
368
+ label: group.label,
369
+ items: (group.items || []).map((item) => ({
370
+ step: item.step,
371
+ skill: item.skill,
372
+ label: item.label || item.step,
373
+ selected: item.selected === true,
374
+ recommended: item.recommended === true,
375
+ locked: item.locked === true,
376
+ required: item.required === true,
377
+ installed: item.installed === true,
378
+ reason: item.reason || null,
379
+ command: item.command || null,
380
+ })),
381
+ })),
382
+ };
383
+ }
384
+
385
+ function renderSkillSelection(selection) {
386
+ if (!selection.groups.length) return [];
387
+ const lines = ['', '**Skill 选择**'];
388
+ for (const group of selection.groups) {
389
+ lines.push('', `### ${group.label || group.id}`);
390
+ for (const item of group.items) {
391
+ const checked = item.selected ? 'x' : ' ';
392
+ const badges = [];
393
+ if (item.locked) badges.push('锁定');
394
+ if (item.required) badges.push('required');
395
+ if (item.recommended) badges.push('推荐');
396
+ if (item.installed === false) badges.push('未安装');
397
+ const suffix = badges.length ? ` (${badges.join(', ')})` : '';
398
+ lines.push(`- [${checked}] \`${item.step}\` -> \`${item.skill}\`${suffix}`);
399
+ if (item.reason) lines.push(` - 原因:${item.reason}`);
400
+ if (item.command) lines.push(` - 命令:\`${item.command}\``);
401
+ }
402
+ }
403
+ return lines;
404
+ }
405
+
406
+ function chatHostLine(host, profile, renderMode) {
407
+ if (profile === 'codex') return 'Codex in-app browser';
408
+ if (profile === 'cursor') return host === 'cursor' ? 'Cursor WebView 或浏览器' : `${host} WebView 或浏览器`;
409
+ if (profile === 'cli') return 'CLI terminal';
410
+ if (renderMode === 'terminal_markdown') return 'CLI terminal';
411
+ if (renderMode === 'webview_or_browser') return `${host} WebView 或浏览器`;
412
+ if (renderMode === 'in_app_browser') return `${host} in-app browser`;
413
+ return 'browser';
414
+ }
415
+
416
+ function instructionsFor(host, profile, renderMode, url, actions) {
417
+ if (profile === 'codex') {
418
+ return [
419
+ `Codex App adapter: start the local UI with \`${actions.startUi}\`, then open ${url} in the Codex in-app browser.`,
420
+ 'Codex chat can show the fallback Markdown link, but interactive cards should run in the in-app browser surface.',
421
+ 'Use diff/explain endpoints for read-only workflow insight, and checkpoint resolve only for explicit confirmation buttons.',
422
+ 'All mutations still go through devflow CLI JSON APIs; the adapter does not edit state directly.',
423
+ ];
424
+ }
425
+ if (profile === 'cursor') {
426
+ const label = adapterLabel(host);
427
+ return [
428
+ `${label} adapter: start the local UI with \`${actions.startUi}\`, then open ${url} in a WebView or the system browser.`,
429
+ `${label} extensions can embed this URL in a WebView and call the listed local API endpoints from that WebView.`,
430
+ 'Use diff/explain endpoints for read-only cards; apply selection, confirm, and checkpoint resolve are the only mutation APIs.',
431
+ 'Without an extension, keep the terminal process running and use the browser fallback; workflow state remains CLI-owned.',
432
+ ];
433
+ }
434
+ if (profile === 'cli' || renderMode === 'terminal_markdown') {
435
+ const label = adapterLabel(host);
436
+ return [
437
+ `${label} adapter: start the local UI with \`${actions.startUi}\`, or use this Markdown card directly in a terminal/chat surface.`,
438
+ 'CLI hosts should print the fallback Markdown and submit edited workflow_chat_selection JSON to the listed chatSelectionApi.',
439
+ 'Use diff/explain endpoints for read-only inspection; apply selection, confirm, and checkpoint resolve remain devflow-controlled.',
440
+ 'Do not execute arbitrary shell from the card; present commands and API endpoints for explicit user action.',
441
+ ];
442
+ }
443
+ return [
444
+ `${adapterLabel(host)} adapter: start the local UI with \`${actions.startUi}\`, then open ${url}.`,
445
+ 'Use the listed local API endpoints if another host wants to render its own UI.',
446
+ ];
447
+ }
448
+
449
+ function adapterLabel(host) {
450
+ if (host === 'codex') return 'Codex App';
451
+ if (host === 'cursor') return 'Cursor';
452
+ if (host === 'browser') return 'Browser';
453
+ if (host === 'cli') return 'CLI';
454
+ return host;
455
+ }
456
+
457
+ module.exports = {
458
+ assertWorkflowAdapterCatalog,
459
+ assertWorkflowAdapterSurface,
460
+ buildWorkflowAdapterCatalog,
461
+ buildWorkflowAdapterSurface,
462
+ normalizeAdapterHost,
463
+ normalizeAdapterProfile,
464
+ normalizeRenderMode,
465
+ validateWorkflowAdapterCatalog,
466
+ validateWorkflowAdapterSurface,
467
+ };