@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,291 @@
1
+ 'use strict';
2
+
3
+ const SUPPORTED_PROTOCOL = 'workflow-adapter-surface/v1';
4
+
5
+ function ensureCompatibleSurface(surface) {
6
+ if (!surface || surface.type !== 'workflow_adapter_surface') {
7
+ throw new Error('expected workflow_adapter_surface JSON');
8
+ }
9
+ if (surface.protocolVersion !== SUPPORTED_PROTOCOL) {
10
+ throw new Error(`unsupported workflow adapter protocol: ${surface.protocolVersion || '<missing>'}`);
11
+ }
12
+ if (!surface.capabilities || surface.capabilities.mutationModel !== 'devflow-controlled') {
13
+ throw new Error('workflow adapter capabilities must use devflow-controlled mutations');
14
+ }
15
+ if (!surface.capabilities.supportsChatSelection) {
16
+ throw new Error('workflow adapter does not support chat selection payloads');
17
+ }
18
+ if (!surface.actions || !surface.actions.chatSelectionApi) {
19
+ throw new Error('workflow adapter missing actions.chatSelectionApi');
20
+ }
21
+ if (!surface.chatCard || !surface.chatCard.selection) {
22
+ throw new Error('workflow adapter missing chatCard.selection');
23
+ }
24
+ return surface;
25
+ }
26
+
27
+ function ensureCompatibleCatalog(catalog) {
28
+ if (!catalog || catalog.type !== 'workflow_adapter_catalog') {
29
+ throw new Error('expected workflow_adapter_catalog JSON');
30
+ }
31
+ if (catalog.protocolVersion !== SUPPORTED_PROTOCOL) {
32
+ throw new Error(`unsupported workflow adapter catalog protocol: ${catalog.protocolVersion || '<missing>'}`);
33
+ }
34
+ if (!catalog.defaults || catalog.defaults.mutationModel !== 'devflow-controlled') {
35
+ throw new Error('workflow adapter catalog defaults must use devflow-controlled mutations');
36
+ }
37
+ if (!Array.isArray(catalog.profiles) || !catalog.profiles.length) {
38
+ throw new Error('workflow adapter catalog missing profiles');
39
+ }
40
+ if (!Array.isArray(catalog.renderModes) || !catalog.renderModes.length) {
41
+ throw new Error('workflow adapter catalog missing renderModes');
42
+ }
43
+ return catalog;
44
+ }
45
+
46
+ function recommendAdapterProfile(catalog, host) {
47
+ ensureCompatibleCatalog(catalog);
48
+ const requestedHost = normalizeHostName(host || 'browser');
49
+ const aliasTarget = catalog.hostAliases && catalog.hostAliases[requestedHost];
50
+ const canonicalHost = aliasTarget || requestedHost;
51
+ const profiles = catalog.profiles || [];
52
+ const direct = profiles.find((profile) => profile.id === canonicalHost);
53
+ const recommended = direct || profiles.find((profile) => (profile.recommendedHosts || []).includes(requestedHost));
54
+ const fallbackId = catalog.defaults && catalog.defaults.customHostProfile || 'browser';
55
+ const fallback = profiles.find((profile) => profile.id === fallbackId) || profiles[0];
56
+ const profile = recommended || fallback;
57
+ const renderMode = profile.defaultRenderMode || (catalog.defaults && catalog.defaults.customHostRenderMode) || 'browser';
58
+ return {
59
+ host: requestedHost,
60
+ canonicalHost,
61
+ profile: profile.id,
62
+ renderMode,
63
+ reason: recommended
64
+ ? (direct ? 'host matches a built-in profile' : `host is recommended for ${profile.id}`)
65
+ : `custom host fallback to ${profile.id}`,
66
+ command: `devflow flow adapter --slug=<slug> --host=${requestedHost} --profile=${profile.id} --json`,
67
+ };
68
+ }
69
+
70
+ function buildSelectionPayload(surface, selectedStepIds) {
71
+ ensureCompatibleSurface(surface);
72
+ const selected = new Set(selectedStepIds || []);
73
+ const selection = surface.chatCard.selection;
74
+ return {
75
+ type: 'workflow_chat_selection',
76
+ layout: selection.layout || 'linear-checkboxes',
77
+ groups: (selection.groups || []).map((group) => ({
78
+ id: group.id,
79
+ label: group.label,
80
+ items: (group.items || []).map((item) => ({
81
+ ...item,
82
+ selected: item.locked === true || selected.has(item.step),
83
+ })),
84
+ })),
85
+ };
86
+ }
87
+
88
+ async function submitSelection(surface, selectedStepIds, options = {}) {
89
+ ensureCompatibleSurface(surface);
90
+ const payload = buildSelectionPayload(surface, selectedStepIds);
91
+ const url = surface.actions.chatSelectionApi;
92
+ if (options.dryRun) return { dryRun: true, url, payload };
93
+ const fetchImpl = options.fetch || globalThis.fetch;
94
+ if (typeof fetchImpl !== 'function') throw new Error('fetch is not available; use Node.js 20+ or pass a fetch implementation');
95
+ const response = await fetchImpl(url, {
96
+ method: 'POST',
97
+ headers: { 'content-type': 'application/json' },
98
+ body: JSON.stringify(payload),
99
+ });
100
+ const text = await response.text();
101
+ const parsed = parseJsonOrRaw(text);
102
+ if (!response.ok) {
103
+ const detail = formatWorkflowHostError(parsed);
104
+ const err = new Error(`workflow selection submit failed: HTTP ${response.status}${detail}`);
105
+ err.response = parsed;
106
+ throw err;
107
+ }
108
+ return parsed;
109
+ }
110
+
111
+ function renderCatalogExample(catalog, options = {}) {
112
+ ensureCompatibleCatalog(catalog);
113
+ const host = normalizeHostName(options.host || 'browser');
114
+ const rec = recommendAdapterProfile(catalog, host);
115
+ return [
116
+ `# ${hostLabel(host)} workflow adapter discovery`,
117
+ '',
118
+ `- protocol: \`${catalog.protocolVersion}\``,
119
+ `- host: \`${rec.host}\``,
120
+ `- canonical host: \`${rec.canonicalHost}\``,
121
+ `- profile: \`${rec.profile}\``,
122
+ `- render mode: \`${rec.renderMode}\``,
123
+ `- reason: ${rec.reason}`,
124
+ '',
125
+ '## Recommended adapter command',
126
+ '',
127
+ '```bash',
128
+ rec.command,
129
+ '```',
130
+ '',
131
+ '## Supported profiles',
132
+ '',
133
+ ...(catalog.profiles || []).map((profile) => `- \`${profile.id}\` -> \`${profile.defaultRenderMode}\`: ${profile.description}`),
134
+ '',
135
+ '## Safety model',
136
+ '',
137
+ `- mutation model: \`${catalog.safety && catalog.safety.mutationModel || '<missing>'}\``,
138
+ `- arbitrary shell: \`${catalog.safety && catalog.safety.arbitraryShell === false ? 'false' : 'true'}\``,
139
+ `- mutation actions: \`${((catalog.safety && catalog.safety.mutationActions) || []).join(', ')}\``,
140
+ ].join('\n');
141
+ }
142
+
143
+ function renderHostExample(surface, options = {}) {
144
+ ensureCompatibleSurface(surface);
145
+ const host = normalizeHostName(options.host || surface.host || 'browser');
146
+ const selectedStepIds = collectDefaultSelectedSteps(surface);
147
+ const payload = buildSelectionPayload(surface, selectedStepIds);
148
+ return [
149
+ `# ${hostLabel(host)} workflow adapter example`,
150
+ '',
151
+ `- protocol: \`${surface.protocolVersion}\``,
152
+ `- render mode: \`${surface.renderMode}\``,
153
+ `- start UI: \`${surface.actions.startUi}\``,
154
+ `- open UI: ${surface.url}`,
155
+ `- submit selection: ${surface.actions.chatSelectionApi}`,
156
+ `- error schema: ${surface.capabilities.errorSchema || '<not advertised>'}`,
157
+ `- command result schema: ${surface.capabilities.commandResultSchema || '<not advertised>'}`,
158
+ '',
159
+ '## Host rendering',
160
+ '',
161
+ hostRenderingNotes(host),
162
+ '',
163
+ '## Markdown fallback',
164
+ '',
165
+ surface.chatCard.markdown || surface.fallbackMarkdown || '',
166
+ '',
167
+ '## POST selection payload',
168
+ '',
169
+ '```json',
170
+ JSON.stringify(payload, null, 2),
171
+ '```',
172
+ '',
173
+ '## Submit example',
174
+ '',
175
+ '```bash',
176
+ `curl -sS -X POST '${surface.actions.chatSelectionApi}' \\`,
177
+ " -H 'content-type: application/json' \\",
178
+ " --data @selection.json",
179
+ '```',
180
+ ].join('\n');
181
+ }
182
+
183
+ function renderSurfaceMarkdown(surface) {
184
+ if (!surface || typeof surface !== 'object') return '## Unknown surface\n\nNo structured surface was provided.';
185
+ if (surface.type === 'workflow_ui_error') {
186
+ return [
187
+ '## Workflow UI Error',
188
+ '',
189
+ `- code: \`${surface.code || 'unknown'}\``,
190
+ `- message: ${surface.message || '<empty>'}`,
191
+ surface.command ? `- command: \`${surface.command}\`` : null,
192
+ ].filter(Boolean).join('\n');
193
+ }
194
+ if (surface.type === 'workflow_policy_confirmation_surface') {
195
+ const cp = surface.checkpoint || {};
196
+ return [
197
+ '## 风险确认',
198
+ '',
199
+ `- change: \`${surface.slug || '-'}\``,
200
+ `- checkpoint: \`${cp.id || '-'}\``,
201
+ `- summary: ${cp.summary || '-'}`,
202
+ surface.actions && surface.actions.acceptRiskApi ? `- accept risk API: ${surface.actions.acceptRiskApi}` : null,
203
+ surface.actions && surface.actions.keepCommand ? `- keep command: \`${surface.actions.keepCommand}\`` : null,
204
+ ].filter(Boolean).join('\n');
205
+ }
206
+ if (surface.type === 'workflow_ui_command_result') {
207
+ return [
208
+ '## 命令结果',
209
+ '',
210
+ `- ok: \`${surface.ok === true ? 'true' : 'false'}\``,
211
+ `- slug: \`${surface.slug || '-'}\``,
212
+ '',
213
+ '```text',
214
+ surface.output || surface.error || '',
215
+ '```',
216
+ ].join('\n');
217
+ }
218
+ if (surface.type === 'workflow_selection_result_surface') {
219
+ return [
220
+ '## Workflow Selection Applied',
221
+ '',
222
+ `- slug: \`${surface.slug || '-'}\``,
223
+ `- next: \`${surface.nextAction || '-'}\``,
224
+ ].join('\n');
225
+ }
226
+ return `## ${surface.type || 'Unknown surface'}\n\n\`\`\`json\n${JSON.stringify(surface, null, 2)}\n\`\`\``;
227
+ }
228
+
229
+ function collectDefaultSelectedSteps(surface) {
230
+ const groups = surface.chatCard && surface.chatCard.selection && surface.chatCard.selection.groups;
231
+ const steps = [];
232
+ for (const group of groups || []) {
233
+ for (const item of group.items || []) {
234
+ if (item.selected === true || item.recommended === true || item.locked === true) steps.push(item.step);
235
+ }
236
+ }
237
+ return steps;
238
+ }
239
+
240
+ function hostLabel(host) {
241
+ if (host === 'codex') return 'Codex in-app browser';
242
+ if (host === 'cursor') return 'Cursor WebView';
243
+ if (host === 'cli') return 'CLI terminal';
244
+ if (host === 'browser') return 'Browser';
245
+ return host;
246
+ }
247
+
248
+ function hostRenderingNotes(host) {
249
+ if (host === 'codex') {
250
+ return 'Codex chat can show the Markdown fallback, then open the local URL in the Codex in-app browser for the interactive card.';
251
+ }
252
+ if (host === 'cursor') {
253
+ return 'Cursor can render the Markdown fallback in chat and embed the local URL in a WebView; without an extension, use the browser fallback.';
254
+ }
255
+ if (host === 'cli') {
256
+ return 'CLI hosts can print the Markdown fallback and ask the user to edit the JSON selection payload before POSTing it.';
257
+ }
258
+ return 'Browser hosts can open the local URL directly and call the listed API endpoints from the page.';
259
+ }
260
+
261
+ function formatWorkflowHostError(parsed) {
262
+ if (!parsed || typeof parsed !== 'object') return '';
263
+ const parts = [];
264
+ if (parsed.code) parts.push(String(parsed.code));
265
+ if (parsed.message) parts.push(String(parsed.message));
266
+ return parts.length ? `: ${parts.join(': ')}` : '';
267
+ }
268
+
269
+ function parseJsonOrRaw(text) {
270
+ try {
271
+ return text ? JSON.parse(text) : {};
272
+ } catch (_) {
273
+ return { raw: text };
274
+ }
275
+ }
276
+
277
+ function normalizeHostName(host) {
278
+ return String(host || 'browser').trim().toLowerCase();
279
+ }
280
+
281
+ module.exports = {
282
+ buildSelectionPayload,
283
+ collectDefaultSelectedSteps,
284
+ ensureCompatibleCatalog,
285
+ ensureCompatibleSurface,
286
+ recommendAdapterProfile,
287
+ renderCatalogExample,
288
+ renderHostExample,
289
+ renderSurfaceMarkdown,
290
+ submitSelection,
291
+ };
@@ -0,0 +1,25 @@
1
+ 'use strict';
2
+
3
+ function workflowActions(slug) {
4
+ return {
5
+ picker: `devflow flow picker --slug=${slug} --json`,
6
+ applySelection: `devflow flow apply-selection --slug=${slug} --selection='<json>' --json`,
7
+ applySelectionFile: `devflow flow apply-selection --slug=${slug} --selection-file=<file> --json`,
8
+ card: `devflow flow card --slug=${slug} --json`,
9
+ check: `devflow flow check --slug=${slug} --json`,
10
+ confirm: `devflow flow confirm --slug=${slug}`,
11
+ };
12
+ }
13
+
14
+ function pickerActions(slug) {
15
+ return {
16
+ card: `devflow flow card --slug=${slug} --json`,
17
+ check: `devflow flow check --slug=${slug} --json`,
18
+ applySelection: `devflow flow apply-selection --slug=${slug} --selection=<json> --json`,
19
+ applySelectionFile: `devflow flow apply-selection --slug=${slug} --selection-file=<file> --json`,
20
+ confirm: `devflow flow confirm --slug=${slug}`,
21
+ suggest: `devflow flow suggest --slug=${slug} --apply-draft`,
22
+ };
23
+ }
24
+
25
+ module.exports = { workflowActions, pickerActions };
@@ -2,6 +2,7 @@
2
2
 
3
3
  const workflow = require('./workflow.js');
4
4
  const workflowPolicy = require('./workflow-policy.js');
5
+ const { pickerActions } = require('./workflow-actions.js');
5
6
 
6
7
  const GROUPS = [
7
8
  { id: 'core', label: '核心流程' },
@@ -48,6 +49,8 @@ function buildPicker(root, slug, st) {
48
49
  protected: false,
49
50
  source: 'builtin',
50
51
  status: 'available',
52
+ after: candidate.after || null,
53
+ before: candidate.before || null,
51
54
  }, {
52
55
  selected: false,
53
56
  recommended: riskReasons.has(candidate.id),
@@ -67,13 +70,7 @@ function buildPicker(root, slug, st) {
67
70
  status: wf.status || null,
68
71
  currentStep: wf.currentStep || null,
69
72
  groups,
70
- actions: {
71
- card: `devflow flow card --slug=${slug} --json`,
72
- check: `devflow flow check --slug=${slug} --json`,
73
- applySelection: `devflow flow apply-selection --slug=${slug} --selection=<json> --json`,
74
- confirm: `devflow flow confirm --slug=${slug}`,
75
- suggest: `devflow flow suggest --slug=${slug} --apply-draft`,
76
- },
73
+ actions: pickerActions(slug),
77
74
  };
78
75
  }
79
76
 
@@ -94,6 +91,8 @@ function buildItem(root, slug, step, opts = {}) {
94
91
  locked,
95
92
  source: step.source || null,
96
93
  status: step.status || null,
94
+ after: step.after || null,
95
+ before: step.before || null,
97
96
  installed: workflow.installedSkillExists(root, step.skill),
98
97
  command: opts.command || commandForStep(slug, step, selected, locked),
99
98
  };
@@ -151,4 +150,13 @@ function itemOrder(item) {
151
150
  return idx === -1 ? order.length : idx;
152
151
  }
153
152
 
154
- module.exports = { buildPicker };
153
+ function placementForCandidate(stepId) {
154
+ const candidate = QUALITY_CANDIDATES.find((item) => item.id === stepId || item.skill === stepId);
155
+ if (!candidate) return null;
156
+ return {
157
+ after: candidate.after || null,
158
+ before: candidate.before || null,
159
+ };
160
+ }
161
+
162
+ module.exports = { buildPicker, placementForCandidate };