@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.
- package/client/workflow-adapter.js +3 -0
- package/docs/workflow-orchestration.md +97 -6
- package/docs/workflow-ui-prototype.html +46 -1
- package/package.json +4 -5
- package/schemas/status-surface.schema.json +33 -1
- package/schemas/workflow-adapter-catalog.schema.json +104 -0
- package/schemas/workflow-adapter-surface.schema.json +193 -0
- package/schemas/workflow-confirmation-surface.schema.json +3 -1
- package/schemas/workflow-picker.schema.json +6 -1
- package/schemas/workflow-selection-result.schema.json +69 -0
- package/schemas/workflow-selection.schema.json +84 -0
- package/schemas/workflow-ui-command-result.schema.json +29 -0
- package/schemas/workflow-ui-error.schema.json +49 -0
- package/scripts/render-workflow-ui-prototype.js +141 -4
- package/scripts/workflow-adapter-client-example.js +67 -0
- package/scripts/workflow-ui-browser-smoke.js +175 -0
- package/skills/df-orchestrator/SKILL.md +2 -1
- package/skills/df-orchestrator/references/workflow-state-machine.md +2 -2
- package/src/cli/commands/_helpers.js +1 -0
- package/src/cli/commands/flow.js +146 -16
- package/src/cli/commands/help.js +1 -1
- package/src/cli/commands/status.js +2 -5
- package/src/client/workflow-adapter-client.js +291 -0
- package/src/core/workflow-actions.js +25 -0
- package/src/core/workflow-picker.js +16 -8
- package/src/core/workflow-ui-adapter.js +467 -0
- package/src/core/workflow-ui-host.js +837 -0
- package/docs/migration-from-arb.md +0 -232
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const workflowUiHost = require('../src/core/workflow-ui-host.js');
|
|
5
|
+
|
|
6
|
+
function detectPlaywright() {
|
|
7
|
+
try {
|
|
8
|
+
return require('playwright');
|
|
9
|
+
} catch (_) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function runWorkflowUiBrowserSmoke(options = {}) {
|
|
15
|
+
const shouldRun = options.run === true || process.env.DEVFLOW_RUN_BROWSER_E2E === '1';
|
|
16
|
+
if (!shouldRun) {
|
|
17
|
+
return { status: 'skipped', reason: 'browser smoke not requested' };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const playwright = options.playwright || detectPlaywright();
|
|
21
|
+
if (!playwright) {
|
|
22
|
+
return { status: 'skipped', reason: 'Playwright is not installed' };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const calls = [];
|
|
26
|
+
const host = await workflowUiHost.createWorkflowUiHost({
|
|
27
|
+
root: options.root || process.cwd(),
|
|
28
|
+
slug: options.slug || 'browser-smoke',
|
|
29
|
+
port: 0,
|
|
30
|
+
binPath: options.binPath || path.resolve(__dirname, '..', 'bin', 'devflow.js'),
|
|
31
|
+
execFile: options.execFile || fakeExecFile(calls),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
let browser;
|
|
35
|
+
try {
|
|
36
|
+
browser = await playwright.chromium.launch({ headless: options.headless !== false });
|
|
37
|
+
const page = await browser.newPage({ viewport: { width: 1280, height: 900 } });
|
|
38
|
+
await page.goto(host.url);
|
|
39
|
+
await page.getByText('Workflow 编排').waitFor();
|
|
40
|
+
await page.getByText('编排说明').waitFor();
|
|
41
|
+
await page.getByText('frontend_change', { exact: false }).waitFor();
|
|
42
|
+
|
|
43
|
+
await page.getByRole('button', { name: '应用选择' }).click();
|
|
44
|
+
await page.getByText('选择已应用').waitFor();
|
|
45
|
+
|
|
46
|
+
await page.getByRole('button', { name: '确认 workflow' }).click();
|
|
47
|
+
await page.getByText('命令已执行').waitFor();
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
status: 'passed',
|
|
51
|
+
url: host.url,
|
|
52
|
+
calls: calls.map((call) => call.args.slice(1).join(' ')),
|
|
53
|
+
};
|
|
54
|
+
} finally {
|
|
55
|
+
if (browser) await browser.close();
|
|
56
|
+
await host.close();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function fakeExecFile(calls) {
|
|
61
|
+
return (cmd, args, execOptions, cb) => {
|
|
62
|
+
calls.push({ cmd, args, execOptions });
|
|
63
|
+
const joined = args.join(' ');
|
|
64
|
+
if (joined.includes('status')) {
|
|
65
|
+
cb(null, JSON.stringify({
|
|
66
|
+
type: 'change_status_surface',
|
|
67
|
+
slug: 'browser-smoke',
|
|
68
|
+
phase: 'workflow',
|
|
69
|
+
workflow: { status: 'draft', currentStep: null, nextStep: 'requirement' },
|
|
70
|
+
primaryPanel: {
|
|
71
|
+
type: 'workflow_confirm',
|
|
72
|
+
title: '确认本次 workflow',
|
|
73
|
+
status: 'needs_input',
|
|
74
|
+
actionIds: ['confirm-workflow'],
|
|
75
|
+
},
|
|
76
|
+
availableActions: [
|
|
77
|
+
{ id: 'confirm-workflow', label: '确认 workflow', command: 'devflow flow confirm --slug=browser-smoke', kind: 'workflow', primary: true },
|
|
78
|
+
],
|
|
79
|
+
blockingReason: { message: 'change workflow is not confirmed' },
|
|
80
|
+
nextAction: 'devflow flow confirm --slug=browser-smoke',
|
|
81
|
+
}), '');
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (joined.includes('flow picker')) {
|
|
85
|
+
cb(null, JSON.stringify({
|
|
86
|
+
type: 'workflow_picker_surface',
|
|
87
|
+
layout: 'linear-grouped',
|
|
88
|
+
baseRecipe: { id: 'standard', label: '标准研发' },
|
|
89
|
+
status: 'draft',
|
|
90
|
+
groups: [
|
|
91
|
+
{
|
|
92
|
+
id: 'quality',
|
|
93
|
+
label: '质量检查',
|
|
94
|
+
items: [
|
|
95
|
+
{ step: 'frontend-quality', skill: 'frontend-quality', selected: true, recommended: true, optional: true, reason: 'frontend_change risk signal' },
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
actions: { applySelectionFile: 'devflow flow apply-selection --slug=browser-smoke --selection-file=<file> --json' },
|
|
100
|
+
}), '');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (joined.includes('flow card')) {
|
|
104
|
+
cb(null, JSON.stringify({
|
|
105
|
+
type: 'workflow_confirmation_surface',
|
|
106
|
+
ok: true,
|
|
107
|
+
status: 'draft',
|
|
108
|
+
confirmationCard: {
|
|
109
|
+
steps: [
|
|
110
|
+
{ id: 'requirement', skill: 'requirement-analysis' },
|
|
111
|
+
{ id: 'frontend-quality', skill: 'frontend-quality' },
|
|
112
|
+
{ id: 'review', skill: 'code-review' },
|
|
113
|
+
{ id: 'verify', skill: 'verify' },
|
|
114
|
+
{ id: 'deliver', skill: 'deliver' },
|
|
115
|
+
],
|
|
116
|
+
primaryAction: { id: 'confirm', command: 'devflow flow confirm --slug=browser-smoke' },
|
|
117
|
+
secondaryActions: [],
|
|
118
|
+
},
|
|
119
|
+
}), '');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (joined.includes('flow diff')) {
|
|
123
|
+
cb(null, JSON.stringify({
|
|
124
|
+
added: ['frontend-quality'],
|
|
125
|
+
disabled: [],
|
|
126
|
+
moved: [],
|
|
127
|
+
verifyReports: ['self-test.md'],
|
|
128
|
+
}), '');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (joined.includes('flow explain')) {
|
|
132
|
+
cb(null, JSON.stringify({
|
|
133
|
+
verify: { requiredReports: ['unit-test.md', 'self-test.md'], reason: 'frontend change needs self test' },
|
|
134
|
+
openRiskSignals: [{ type: 'frontend_change', reason: 'UI touched' }],
|
|
135
|
+
overrides: [{ type: 'add-step', step: 'frontend-quality', reason: 'frontend change' }],
|
|
136
|
+
}), '');
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (joined.includes('flow apply-selection')) {
|
|
140
|
+
cb(null, JSON.stringify({
|
|
141
|
+
type: 'workflow_selection_result_surface',
|
|
142
|
+
ok: true,
|
|
143
|
+
added: ['frontend-quality'],
|
|
144
|
+
disabled: [],
|
|
145
|
+
skipped: [],
|
|
146
|
+
diff: { added: ['frontend-quality'], disabled: [], moved: [], verifyReports: ['self-test.md'] },
|
|
147
|
+
nextAction: 'devflow flow card --slug=browser-smoke --json',
|
|
148
|
+
}), '');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (joined.includes('flow confirm')) {
|
|
152
|
+
cb(null, '[ok] workflow confirmed: currentStep=requirement\n', '');
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
cb(new Error(`unexpected args: ${joined}`), '', '');
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (require.main === module) {
|
|
160
|
+
runWorkflowUiBrowserSmoke({ run: true })
|
|
161
|
+
.then((result) => {
|
|
162
|
+
const prefix = result.status === 'passed' ? '[ok]' : '[skip]';
|
|
163
|
+
console.log(`${prefix} workflow UI browser smoke: ${result.reason || result.url}`);
|
|
164
|
+
if (result.status !== 'passed' && process.env.DEVFLOW_BROWSER_E2E_REQUIRED === '1') process.exitCode = 1;
|
|
165
|
+
})
|
|
166
|
+
.catch((e) => {
|
|
167
|
+
console.error(`[error] workflow UI browser smoke: ${e.stack || e.message}`);
|
|
168
|
+
process.exitCode = 1;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = {
|
|
173
|
+
detectPlaywright,
|
|
174
|
+
runWorkflowUiBrowserSmoke,
|
|
175
|
+
};
|
|
@@ -169,7 +169,7 @@ devflow new <slug> --project-root=/path/to/primary
|
|
|
169
169
|
|
|
170
170
|
## Phase 顺序(硬约束)
|
|
171
171
|
|
|
172
|
-
> 新流程优先使用入口命令自动生成的 workflow draft,再通过 `devflow flow picker` / `devflow flow apply-selection` / `devflow flow card` / `devflow flow confirm` 完成选项卡确认并冻结 `state.workflow.steps`。`flow check` 仍保留给脚本做预检。下面的 phase 顺序是内置 recipe 的默认展开形态,不是所有需求都必须硬走的唯一链路。`level` 只影响验证强度;实际 skill 编排由 workflow recipe 决定。完整说明见 `docs/workflow-orchestration.md`。
|
|
172
|
+
> 新流程优先使用入口命令自动生成的 workflow draft,再通过 `devflow flow picker` / `devflow flow apply-selection` / `devflow flow card` / `devflow flow confirm` 完成选项卡确认并冻结 `state.workflow.steps`。IDE / Web UI 调整勾选时优先使用文件版 `devflow flow apply-selection --selection-file=<file> --json`。`flow check` 仍保留给脚本做预检。下面的 phase 顺序是内置 recipe 的默认展开形态,不是所有需求都必须硬走的唯一链路。`level` 只影响验证强度;实际 skill 编排由 workflow recipe 决定。完整说明见 `docs/workflow-orchestration.md`。
|
|
173
173
|
|
|
174
174
|
**入口后的 workflow 确认:**
|
|
175
175
|
```
|
|
@@ -182,6 +182,7 @@ new / ingest 自动生成 workflow draft
|
|
|
182
182
|
devflow flow picker --json
|
|
183
183
|
↓
|
|
184
184
|
用户如调整勾选: devflow flow apply-selection --selection='<json>' --json
|
|
185
|
+
IDE / Web UI 推荐: devflow flow apply-selection --selection-file=<file> --json
|
|
185
186
|
↓
|
|
186
187
|
devflow flow card --json
|
|
187
188
|
↓
|
|
@@ -6,10 +6,10 @@ workflow step / phase 之间怎么走、哪些转移合法、哪些必须回退
|
|
|
6
6
|
|
|
7
7
|
## 规范 workflow 模型
|
|
8
8
|
|
|
9
|
-
新 change 优先由 `devflow new` / `devflow ingest` 自动生成 `state.workflow` draft,再通过 `devflow flow picker --json` → 可选 `devflow flow apply-selection --selection='<json>' --json` → `devflow flow card --json` → `devflow flow confirm` 冻结 `state.workflow.steps`。`devflow flow check --json` 仍保留给脚本做预检。如果自动 draft 失败,再退回 `devflow flow recommend` → `devflow flow draft` 的手动补救路径。orchestrator 决定下一步时:
|
|
9
|
+
新 change 优先由 `devflow new` / `devflow ingest` 自动生成 `state.workflow` draft,再通过 `devflow flow picker --json` → 可选 `devflow flow apply-selection --selection='<json>' --json` 或 IDE / Web UI 推荐的 `devflow flow apply-selection --selection-file=<file> --json` → `devflow flow card --json` → `devflow flow confirm` 冻结 `state.workflow.steps`。`devflow flow check --json` 仍保留给脚本做预检。如果自动 draft 失败,再退回 `devflow flow recommend` → `devflow flow draft` 的手动补救路径。orchestrator 决定下一步时:
|
|
10
10
|
|
|
11
11
|
1. 若 `state.workflow.status = confirmed`,按 `state.workflow.steps` 中未完成 / 未禁用的 step 推进。
|
|
12
|
-
2. 若 `state.workflow.status = draft`,先展示 `devflow flow picker --json`
|
|
12
|
+
2. 若 `state.workflow.status = draft`,先展示 `devflow flow picker --json` 让用户看到线形选项卡;如果用户调整勾选,CLI 可执行 `devflow flow apply-selection --selection='<json>' --json`,IDE / Web UI 推荐执行 `devflow flow apply-selection --selection-file=<file> --json`;然后展示 `devflow flow explain` 和 `devflow flow card --json`,只有确认面 `ok=true` 时才展示 `devflow flow confirm` 下一步卡片,不得直接进入 requirement/design/apply。
|
|
13
13
|
3. 若旧 change 没有 `state.workflow`,才使用下面的固定 phase 列表作为兼容路径。
|
|
14
14
|
4. `review`、`verify`、`deliver` 是 protected gates,workflow 不得删除或降级。
|
|
15
15
|
|
|
@@ -79,6 +79,7 @@ function completeWorkflowStep(st, stepId) {
|
|
|
79
79
|
function printWorkflowSelectionGuidance(slug) {
|
|
80
80
|
log.dim(`workflow picker: devflow flow picker --slug=${slug} --json`);
|
|
81
81
|
log.dim(`selection: devflow flow apply-selection --slug=${slug} --selection='<json>' --json`);
|
|
82
|
+
log.dim(`selection file: devflow flow apply-selection --slug=${slug} --selection-file=<file> --json`);
|
|
82
83
|
log.dim(`card: devflow flow card --slug=${slug} --json`);
|
|
83
84
|
log.dim(`check: devflow flow check --slug=${slug} --json`);
|
|
84
85
|
log.dim(`confirm: devflow flow confirm --slug=${slug}`);
|
package/src/cli/commands/flow.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
3
6
|
const log = require('../../utils/log.js');
|
|
4
7
|
const config = require('../../core/config.js');
|
|
5
8
|
const state = require('../../core/state.js');
|
|
@@ -11,6 +14,9 @@ const workflowVerify = require('../../core/workflow-verify.js');
|
|
|
11
14
|
const workflowCheck = require('../../core/workflow-check.js');
|
|
12
15
|
const workflowInit = require('../../core/workflow-init.js');
|
|
13
16
|
const workflowPicker = require('../../core/workflow-picker.js');
|
|
17
|
+
const { workflowActions } = require('../../core/workflow-actions.js');
|
|
18
|
+
const workflowUiHost = require('../../core/workflow-ui-host.js');
|
|
19
|
+
const workflowUiAdapter = require('../../core/workflow-ui-adapter.js');
|
|
14
20
|
const helpers = require('./_helpers.js');
|
|
15
21
|
|
|
16
22
|
async function run({ sub = null, positional = [], flags = {}, cwd }) {
|
|
@@ -26,6 +32,7 @@ async function run({ sub = null, positional = [], flags = {}, cwd }) {
|
|
|
26
32
|
try { return await preview(root, cfg, args[0] || flags.recipe); }
|
|
27
33
|
catch (e) { log.error(e.message); process.exitCode = 2; return; }
|
|
28
34
|
}
|
|
35
|
+
if (action === 'adapter-catalog' || action === 'adapter-profiles') return adapterCatalog(flags);
|
|
29
36
|
const slug = await helpers.resolveSlug(root, flags, args);
|
|
30
37
|
if (!slug) { process.exitCode = 1; return; }
|
|
31
38
|
const st = await helpers.loadStateOrFail(root, slug);
|
|
@@ -44,6 +51,8 @@ async function run({ sub = null, positional = [], flags = {}, cwd }) {
|
|
|
44
51
|
if (action === 'apply-selection') return await applySelection(root, slug, st, flags);
|
|
45
52
|
if (action === 'card') return card(root, slug, st, flags);
|
|
46
53
|
if (action === 'check') return check(root, slug, st, flags);
|
|
54
|
+
if (action === 'ui') return await ui(root, slug, flags);
|
|
55
|
+
if (action === 'adapter') return adapter(root, slug, st, flags);
|
|
47
56
|
if (action === 'confirm') return await confirm(root, slug, st);
|
|
48
57
|
if (action === 'current') return current(st);
|
|
49
58
|
if (action === 'next') return next(st);
|
|
@@ -411,15 +420,6 @@ function buildWorkflowConfirmationSurface(root, slug, st) {
|
|
|
411
420
|
};
|
|
412
421
|
}
|
|
413
422
|
|
|
414
|
-
function workflowActions(slug) {
|
|
415
|
-
return {
|
|
416
|
-
picker: `devflow flow picker --slug=${slug} --json`,
|
|
417
|
-
applySelection: `devflow flow apply-selection --slug=${slug} --selection='<json>' --json`,
|
|
418
|
-
check: `devflow flow check --slug=${slug} --json`,
|
|
419
|
-
confirm: `devflow flow confirm --slug=${slug}`,
|
|
420
|
-
};
|
|
421
|
-
}
|
|
422
|
-
|
|
423
423
|
function picker(root, slug, st, flags = {}) {
|
|
424
424
|
const result = workflowPicker.buildPicker(root, slug, st);
|
|
425
425
|
if (flags.json === true) {
|
|
@@ -441,7 +441,7 @@ function picker(root, slug, st, flags = {}) {
|
|
|
441
441
|
}
|
|
442
442
|
|
|
443
443
|
async function applySelection(root, slug, st, flags = {}) {
|
|
444
|
-
const selection = parseSelection(
|
|
444
|
+
const selection = parseSelection(readSelectionInput(root, flags));
|
|
445
445
|
const applied = { added: [], disabled: [], skipped: [] };
|
|
446
446
|
|
|
447
447
|
for (const item of selection.items) {
|
|
@@ -458,13 +458,16 @@ async function applySelection(root, slug, st, flags = {}) {
|
|
|
458
458
|
stepId: item.step,
|
|
459
459
|
skill,
|
|
460
460
|
}), { json: flags.json === true })) return;
|
|
461
|
+
const placement = workflowPicker.placementForCandidate(item.step) || {};
|
|
462
|
+
const after = item.after || placement.after || null;
|
|
463
|
+
const before = item.before || placement.before || null;
|
|
461
464
|
const step = workflow.addStep(st.workflow, {
|
|
462
465
|
skill,
|
|
463
466
|
id: item.step,
|
|
464
|
-
after
|
|
465
|
-
before
|
|
467
|
+
after,
|
|
468
|
+
before,
|
|
466
469
|
});
|
|
467
|
-
state.logEvent(st, 'workflow.step.add', { step: step.id, skill, after
|
|
470
|
+
state.logEvent(st, 'workflow.step.add', { step: step.id, skill, after, before, source: 'selection' });
|
|
468
471
|
applied.added.push(step.id);
|
|
469
472
|
continue;
|
|
470
473
|
}
|
|
@@ -486,7 +489,12 @@ async function applySelection(root, slug, st, flags = {}) {
|
|
|
486
489
|
|
|
487
490
|
await state.write(root, slug, st);
|
|
488
491
|
const result = {
|
|
492
|
+
type: 'workflow_selection_result_surface',
|
|
493
|
+
slug,
|
|
494
|
+
ok: true,
|
|
489
495
|
...applied,
|
|
496
|
+
diff: workflow.diffWorkflow(st.workflow),
|
|
497
|
+
actions: workflowActions(slug),
|
|
490
498
|
nextAction: `devflow flow card --slug=${slug} --json`,
|
|
491
499
|
};
|
|
492
500
|
if (flags.json === true) {
|
|
@@ -498,14 +506,136 @@ async function applySelection(root, slug, st, flags = {}) {
|
|
|
498
506
|
return result;
|
|
499
507
|
}
|
|
500
508
|
|
|
509
|
+
async function ui(root, slug, flags = {}) {
|
|
510
|
+
const host = flags.host && flags.host !== true ? flags.host : '127.0.0.1';
|
|
511
|
+
const port = workflowUiHost.normalizePort(flags.port, 8787);
|
|
512
|
+
const app = await workflowUiHost.createWorkflowUiHost({ root, slug, host, port });
|
|
513
|
+
const surface = {
|
|
514
|
+
type: 'workflow_ui_host',
|
|
515
|
+
slug,
|
|
516
|
+
url: app.url,
|
|
517
|
+
schemas: {
|
|
518
|
+
adapter: 'https://devflow.dev/schemas/workflow-adapter-surface.schema.json',
|
|
519
|
+
adapterCatalog: 'https://devflow.dev/schemas/workflow-adapter-catalog.schema.json',
|
|
520
|
+
selection: 'https://devflow.dev/schemas/workflow-selection.schema.json',
|
|
521
|
+
error: 'https://devflow.dev/schemas/workflow-ui-error.schema.json',
|
|
522
|
+
commandResult: 'https://devflow.dev/schemas/workflow-ui-command-result.schema.json',
|
|
523
|
+
},
|
|
524
|
+
actions: {
|
|
525
|
+
status: `${app.url}api/status`,
|
|
526
|
+
picker: `${app.url}api/picker`,
|
|
527
|
+
adapter: `${app.url}api/adapter`,
|
|
528
|
+
adapterCatalog: `${app.url}api/adapter/catalog`,
|
|
529
|
+
diff: `${app.url}api/diff`,
|
|
530
|
+
explain: `${app.url}api/explain`,
|
|
531
|
+
card: `${app.url}api/card`,
|
|
532
|
+
applySelection: `${app.url}api/apply-selection`,
|
|
533
|
+
checkpointResolve: `${app.url}api/checkpoint/resolve`,
|
|
534
|
+
confirm: `${app.url}api/confirm`,
|
|
535
|
+
},
|
|
536
|
+
};
|
|
537
|
+
if (flags.json === true) {
|
|
538
|
+
log.raw(JSON.stringify(surface, null, 2));
|
|
539
|
+
} else {
|
|
540
|
+
log.ok(`workflow UI: ${app.url}`);
|
|
541
|
+
log.dim('press Ctrl+C to stop.');
|
|
542
|
+
}
|
|
543
|
+
if (flags.once === true || flags['print-only'] === true || flags.printOnly === true) {
|
|
544
|
+
await app.close();
|
|
545
|
+
return surface;
|
|
546
|
+
}
|
|
547
|
+
await waitForShutdown(app);
|
|
548
|
+
return surface;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function waitForShutdown(app) {
|
|
552
|
+
return new Promise((resolve) => {
|
|
553
|
+
let closing = false;
|
|
554
|
+
const close = async () => {
|
|
555
|
+
if (closing) return;
|
|
556
|
+
closing = true;
|
|
557
|
+
try { await app.close(); } catch (_) {}
|
|
558
|
+
resolve();
|
|
559
|
+
};
|
|
560
|
+
process.once('SIGINT', close);
|
|
561
|
+
process.once('SIGTERM', close);
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function adapterCatalog(flags = {}) {
|
|
566
|
+
const catalog = workflowUiAdapter.buildWorkflowAdapterCatalog();
|
|
567
|
+
if (flags.validate === true || flags.validateContract === true || flags['validate-contract'] === true) {
|
|
568
|
+
workflowUiAdapter.assertWorkflowAdapterCatalog(catalog);
|
|
569
|
+
}
|
|
570
|
+
if (flags.json === true) {
|
|
571
|
+
log.raw(JSON.stringify(catalog, null, 2));
|
|
572
|
+
return catalog;
|
|
573
|
+
}
|
|
574
|
+
log.raw('workflow adapter profiles');
|
|
575
|
+
for (const profile of catalog.profiles) {
|
|
576
|
+
log.raw(`- ${profile.id}: ${profile.defaultRenderMode}`);
|
|
577
|
+
log.dim(` ${profile.description}`);
|
|
578
|
+
}
|
|
579
|
+
log.raw('');
|
|
580
|
+
log.raw('render modes');
|
|
581
|
+
for (const mode of catalog.renderModes) {
|
|
582
|
+
log.raw(`- ${mode.id}: embedded=${mode.supportsEmbeddedUi}`);
|
|
583
|
+
}
|
|
584
|
+
log.raw('');
|
|
585
|
+
log.dim('custom host default: browser profile / browser render mode');
|
|
586
|
+
log.dim('next: devflow flow adapter --slug=<slug> --host=<name> --profile=<profile> --json');
|
|
587
|
+
return catalog;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function adapter(root, slug, st, flags = {}) {
|
|
591
|
+
const host = flags.host && flags.host !== true ? flags.host : (flags.target && flags.target !== true ? flags.target : 'browser');
|
|
592
|
+
const profile = flags.profile && flags.profile !== true ? flags.profile : null;
|
|
593
|
+
const renderMode = flags.renderMode || flags['render-mode'] || null;
|
|
594
|
+
const port = flags.port === undefined ? 8787 : workflowUiHost.normalizePort(flags.port, 8787);
|
|
595
|
+
const pickerSurface = st.workflow && Array.isArray(st.workflow.steps)
|
|
596
|
+
? workflowPicker.buildPicker(root, slug, st)
|
|
597
|
+
: null;
|
|
598
|
+
const surface = workflowUiAdapter.buildWorkflowAdapterSurface({ host, profile, renderMode, slug, port, pickerSurface });
|
|
599
|
+
if (flags.validate === true || flags.validateContract === true || flags['validate-contract'] === true) {
|
|
600
|
+
workflowUiAdapter.assertWorkflowAdapterSurface(surface);
|
|
601
|
+
}
|
|
602
|
+
if (flags.json === true) {
|
|
603
|
+
log.raw(JSON.stringify(surface, null, 2));
|
|
604
|
+
return surface;
|
|
605
|
+
}
|
|
606
|
+
log.raw(`${surface.host} workflow adapter`);
|
|
607
|
+
log.raw(`url: ${surface.url}`);
|
|
608
|
+
log.raw(`start: ${surface.actions.startUi}`);
|
|
609
|
+
for (const item of surface.instructions) log.dim(`- ${item}`);
|
|
610
|
+
log.raw('');
|
|
611
|
+
log.raw(surface.chatCard.markdown);
|
|
612
|
+
return surface;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function readSelectionInput(root, flags = {}) {
|
|
616
|
+
const inline = flags.selection || flags.input || flags.items;
|
|
617
|
+
if (inline && inline !== true) return inline;
|
|
618
|
+
const file = flags.selectionFile || flags['selection-file'];
|
|
619
|
+
if (!file || file === true) return inline;
|
|
620
|
+
const selectionPath = path.isAbsolute(file) ? file : path.resolve(root, file);
|
|
621
|
+
return fs.readFileSync(selectionPath, 'utf8');
|
|
622
|
+
}
|
|
623
|
+
|
|
501
624
|
function parseSelection(raw) {
|
|
502
|
-
if (!raw || raw === true) throw new Error('usage: devflow flow apply-selection --selection=<json> --slug=<slug>');
|
|
625
|
+
if (!raw || raw === true) throw new Error('usage: devflow flow apply-selection --selection=<json> | --selection-file=<file> --slug=<slug>');
|
|
503
626
|
const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
504
|
-
const items = Array.isArray(parsed)
|
|
627
|
+
const items = Array.isArray(parsed)
|
|
628
|
+
? parsed
|
|
629
|
+
: parsed.items || flattenPickerGroups(parsed.groups);
|
|
505
630
|
if (!Array.isArray(items)) throw new Error('workflow selection must contain an items array');
|
|
506
631
|
return { items };
|
|
507
632
|
}
|
|
508
633
|
|
|
634
|
+
function flattenPickerGroups(groups) {
|
|
635
|
+
if (!Array.isArray(groups)) return null;
|
|
636
|
+
return groups.flatMap((group) => Array.isArray(group.items) ? group.items : []);
|
|
637
|
+
}
|
|
638
|
+
|
|
509
639
|
async function confirm(root, slug, st) {
|
|
510
640
|
const result = workflowCheck.checkWorkflow(root, slug, st);
|
|
511
641
|
if (!result.ok) {
|
|
@@ -639,7 +769,7 @@ function validateInstalledSkills(root, steps) {
|
|
|
639
769
|
}
|
|
640
770
|
|
|
641
771
|
function usage() {
|
|
642
|
-
log.error('usage: devflow flow <recommend|draft|suggest|add-step|disable-step|move-step|set-verify|diff|picker|apply-selection|card|check|confirm|current|next|explain|validate|preview|use>');
|
|
772
|
+
log.error('usage: devflow flow <recommend|draft|suggest|add-step|disable-step|move-step|set-verify|diff|picker|apply-selection|card|check|ui|adapter|adapter-catalog|confirm|current|next|explain|validate|preview|use>');
|
|
643
773
|
}
|
|
644
774
|
|
|
645
775
|
module.exports = { run };
|
package/src/cli/commands/help.js
CHANGED
|
@@ -65,7 +65,7 @@ OPS
|
|
|
65
65
|
[--split-report] also write legacy reports/<kind>-test.md
|
|
66
66
|
devflow report compact [--remove-split]
|
|
67
67
|
merge existing split reports into reports/test-report.md
|
|
68
|
-
devflow flow <recommend|draft|suggest|add-step|disable-step|move-step|set-verify|diff|picker|apply-selection|card|check|confirm|explain>
|
|
68
|
+
devflow flow <recommend|draft|suggest|add-step|disable-step|move-step|set-verify|diff|picker|apply-selection|card|check|ui|adapter|adapter-catalog|confirm|explain>
|
|
69
69
|
draft and adjust the change workflow from builtin/project recipes
|
|
70
70
|
devflow deploy test --project=<name> trigger configured Jenkins test job
|
|
71
71
|
auto-prompts feature->test merge/push before Jenkins when run from a dev branch
|
|
@@ -9,6 +9,7 @@ const state = require('../../core/state.js');
|
|
|
9
9
|
const checkpoint = require('../../core/checkpoint.js');
|
|
10
10
|
const workflowCheck = require('../../core/workflow-check.js');
|
|
11
11
|
const workflowVerify = require('../../core/workflow-verify.js');
|
|
12
|
+
const { workflowActions } = require('../../core/workflow-actions.js');
|
|
12
13
|
const aggregate = require('../../reports/aggregate.js');
|
|
13
14
|
const helpers = require('./_helpers.js');
|
|
14
15
|
|
|
@@ -369,11 +370,7 @@ function buildStatusSurface(root, slug, st) {
|
|
|
369
370
|
nextStep: nextWorkflowStep(wf),
|
|
370
371
|
overrides: summarizeWorkflowOverrides(wf),
|
|
371
372
|
check: workflowCheckResult,
|
|
372
|
-
actions:
|
|
373
|
-
card: `devflow flow card --slug=${slug} --json`,
|
|
374
|
-
picker: `devflow flow picker --slug=${slug} --json`,
|
|
375
|
-
confirm: `devflow flow confirm --slug=${slug}`,
|
|
376
|
-
},
|
|
373
|
+
actions: workflowActions(slug),
|
|
377
374
|
} : null,
|
|
378
375
|
pendingCheckpoint: pending ? checkpoint.summarizeCheckpoint(pending) : null,
|
|
379
376
|
checkpointConfirmationCard,
|