prd-workflow-cli 1.1.31 → 1.2.6

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,460 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>PRD A2UI 预览器</title>
8
+ <style>
9
+ :root {
10
+ --bg-color: #f3f4f6;
11
+ --panel-bg: #ffffff;
12
+ --text-primary: #1f2937;
13
+ --border-color: #e5e7eb;
14
+ --primary-color: #2563eb;
15
+ }
16
+
17
+ body {
18
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
19
+ background-color: var(--bg-color);
20
+ color: var(--text-primary);
21
+ margin: 0;
22
+ padding: 20px;
23
+ display: flex;
24
+ justify-content: center;
25
+ }
26
+
27
+ #app {
28
+ width: 100%;
29
+ max-width: 1200px;
30
+ }
31
+
32
+ /* 基础组件样式 */
33
+ .component-page {
34
+ display: flex;
35
+ flex-direction: column;
36
+ gap: 16px;
37
+ }
38
+
39
+ .component-panel {
40
+ background: var(--panel-bg);
41
+ border-radius: 8px;
42
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
43
+ padding: 20px;
44
+ border: 1px solid var(--border-color);
45
+ }
46
+
47
+ .component-header {
48
+ font-size: 1.5rem;
49
+ font-weight: 600;
50
+ margin-bottom: 16px;
51
+ border-bottom: 1px solid var(--border-color);
52
+ padding-bottom: 12px;
53
+ }
54
+
55
+ .component-button {
56
+ background-color: var(--primary-color);
57
+ color: white;
58
+ border: none;
59
+ padding: 8px 16px;
60
+ border-radius: 6px;
61
+ cursor: pointer;
62
+ font-size: 0.9rem;
63
+ transition: background 0.2s;
64
+ }
65
+
66
+ .component-button:hover {
67
+ background-color: #1d4ed8;
68
+ }
69
+
70
+ .component-input {
71
+ width: 100%;
72
+ padding: 8px 12px;
73
+ border: 1px solid var(--border-color);
74
+ border-radius: 6px;
75
+ margin-top: 4px;
76
+ }
77
+
78
+ .component-label {
79
+ display: block;
80
+ font-weight: 500;
81
+ margin-bottom: 4px;
82
+ font-size: 0.9rem;
83
+ }
84
+
85
+ .component-group {
86
+ margin-bottom: 16px;
87
+ }
88
+
89
+ /* 布局组件 */
90
+ .layout-row {
91
+ display: flex;
92
+ gap: 16px;
93
+ flex-wrap: wrap;
94
+ }
95
+
96
+ .layout-col {
97
+ flex: 1;
98
+ min-width: 300px;
99
+ }
100
+
101
+ /* 状态提示 */
102
+ #status {
103
+ position: fixed;
104
+ bottom: 20px;
105
+ right: 20px;
106
+ background: #333;
107
+ color: white;
108
+ padding: 8px 16px;
109
+ border-radius: 20px;
110
+ font-size: 0.8rem;
111
+ opacity: 0.8;
112
+ }
113
+
114
+ /* ========== 架构图组件样式 ========== */
115
+
116
+ /* 图表容器 */
117
+ .component-diagram {
118
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
119
+ border-radius: 12px;
120
+ padding: 24px;
121
+ min-height: 300px;
122
+ position: relative;
123
+ }
124
+
125
+ .diagram-title {
126
+ color: white;
127
+ font-size: 1.5rem;
128
+ font-weight: 600;
129
+ margin-bottom: 20px;
130
+ text-align: center;
131
+ }
132
+
133
+ .diagram-content {
134
+ display: flex;
135
+ flex-direction: column;
136
+ gap: 20px;
137
+ align-items: center;
138
+ }
139
+
140
+ /* 模块方框 */
141
+ .component-box {
142
+ background: white;
143
+ border-radius: 8px;
144
+ padding: 16px 24px;
145
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
146
+ text-align: center;
147
+ min-width: 120px;
148
+ transition: transform 0.2s, box-shadow 0.2s;
149
+ }
150
+
151
+ .component-box:hover {
152
+ transform: translateY(-2px);
153
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
154
+ }
155
+
156
+ .box-title {
157
+ font-weight: 600;
158
+ font-size: 1rem;
159
+ color: #1f2937;
160
+ margin-bottom: 4px;
161
+ }
162
+
163
+ .box-desc {
164
+ font-size: 0.8rem;
165
+ color: #6b7280;
166
+ }
167
+
168
+ /* 分组容器 */
169
+ .component-group-diagram {
170
+ background: rgba(255, 255, 255, 0.1);
171
+ border: 2px dashed rgba(255, 255, 255, 0.3);
172
+ border-radius: 12px;
173
+ padding: 16px;
174
+ width: 100%;
175
+ }
176
+
177
+ .group-title {
178
+ color: rgba(255, 255, 255, 0.9);
179
+ font-size: 0.9rem;
180
+ font-weight: 500;
181
+ margin-bottom: 12px;
182
+ }
183
+
184
+ .group-content {
185
+ display: flex;
186
+ flex-wrap: wrap;
187
+ gap: 12px;
188
+ justify-content: center;
189
+ }
190
+
191
+ /* 连接线/箭头 */
192
+ .component-arrow {
193
+ display: flex;
194
+ align-items: center;
195
+ justify-content: center;
196
+ color: rgba(255, 255, 255, 0.8);
197
+ font-size: 1.5rem;
198
+ padding: 8px 0;
199
+ }
200
+
201
+ .arrow-label {
202
+ font-size: 0.75rem;
203
+ margin-left: 8px;
204
+ color: rgba(255, 255, 255, 0.7);
205
+ }
206
+
207
+ /* 层级容器 */
208
+ .component-layer {
209
+ width: 100%;
210
+ display: flex;
211
+ flex-wrap: wrap;
212
+ gap: 12px;
213
+ justify-content: center;
214
+ padding: 12px 0;
215
+ }
216
+
217
+ .layer-title {
218
+ width: 100%;
219
+ text-align: center;
220
+ color: rgba(255, 255, 255, 0.7);
221
+ font-size: 0.8rem;
222
+ margin-bottom: 8px;
223
+ }
224
+ </style>
225
+ </head>
226
+
227
+ <body>
228
+ <div id="app">
229
+ <div style="text-align: center; padding: 50px;">
230
+ <h2>等待 A2UI 数据...</h2>
231
+ <p>请在 CLI 中生成界面数据</p>
232
+ </div>
233
+ </div>
234
+ <div id="status">连接中...</div>
235
+
236
+ <script>
237
+ // A2UI 渲染引擎
238
+ const Renderer = {
239
+ create(type, props) {
240
+ console.log('Creating component:', type, props);
241
+ const el = document.createElement('div');
242
+ el.className = `component-${type.toLowerCase()}`;
243
+
244
+ // 处理通用属性
245
+ if (props.id) el.id = props.id;
246
+ if (props.style) Object.assign(el.style, props.style);
247
+
248
+ // 特定组件渲染逻辑
249
+ switch (type) {
250
+ case 'Page':
251
+ if (props.title) {
252
+ const title = document.createElement('div');
253
+ title.className = 'component-header'; // 复用 header 样式
254
+ title.style.borderBottom = 'none';
255
+ title.style.fontSize = '2rem';
256
+ title.textContent = props.title;
257
+ el.appendChild(title);
258
+ }
259
+ return el;
260
+
261
+ case 'Panel':
262
+ if (props.title) {
263
+ const title = document.createElement('div');
264
+ title.className = 'component-header';
265
+ title.textContent = props.title;
266
+ el.appendChild(title);
267
+ }
268
+ return el;
269
+
270
+ case 'Button':
271
+ const btn = document.createElement('button');
272
+ btn.className = 'component-button';
273
+ btn.textContent = props.text || 'Button';
274
+ return btn;
275
+
276
+ case 'Input':
277
+ const group = document.createElement('div');
278
+ group.className = 'component-group';
279
+ if (props.label) {
280
+ const label = document.createElement('label');
281
+ label.className = 'component-label';
282
+ label.textContent = props.label;
283
+ group.appendChild(label);
284
+ }
285
+ const input = document.createElement('input');
286
+ input.className = 'component-input';
287
+ input.placeholder = props.placeholder || '';
288
+ group.appendChild(input);
289
+ return group;
290
+
291
+ case 'Text':
292
+ const p = document.createElement('p');
293
+ p.style.margin = '8px 0';
294
+ p.textContent = props.content || '';
295
+ return p;
296
+
297
+ case 'Row':
298
+ el.className = 'layout-row';
299
+ return el;
300
+
301
+ case 'Col':
302
+ el.className = 'layout-col';
303
+ return el;
304
+
305
+ // ========== 架构图组件 ==========
306
+
307
+ case 'Diagram':
308
+ el.className = 'component-diagram';
309
+ if (props.title) {
310
+ const title = document.createElement('div');
311
+ title.className = 'diagram-title';
312
+ title.textContent = props.title;
313
+ el.appendChild(title);
314
+ }
315
+ const content = document.createElement('div');
316
+ content.className = 'diagram-content';
317
+ el.appendChild(content);
318
+ el._content = content; // 用于子元素渲染
319
+ return el;
320
+
321
+ case 'Box':
322
+ el.className = 'component-box';
323
+ if (props.color) {
324
+ el.style.borderLeft = `4px solid ${props.color}`;
325
+ }
326
+ const boxTitle = document.createElement('div');
327
+ boxTitle.className = 'box-title';
328
+ boxTitle.textContent = props.title || props.text || '';
329
+ el.appendChild(boxTitle);
330
+ if (props.desc) {
331
+ const boxDesc = document.createElement('div');
332
+ boxDesc.className = 'box-desc';
333
+ boxDesc.textContent = props.desc;
334
+ el.appendChild(boxDesc);
335
+ }
336
+ return el;
337
+
338
+ case 'Arrow':
339
+ el.className = 'component-arrow';
340
+ const arrowSymbol = props.direction === 'up' ? '↑' :
341
+ props.direction === 'left' ? '←' :
342
+ props.direction === 'right' ? '→' : '↓';
343
+ el.textContent = arrowSymbol;
344
+ if (props.label) {
345
+ const arrowLabel = document.createElement('span');
346
+ arrowLabel.className = 'arrow-label';
347
+ arrowLabel.textContent = props.label;
348
+ el.appendChild(arrowLabel);
349
+ }
350
+ return el;
351
+
352
+ case 'Layer':
353
+ el.className = 'component-layer';
354
+ if (props.title) {
355
+ const layerTitle = document.createElement('div');
356
+ layerTitle.className = 'layer-title';
357
+ layerTitle.textContent = props.title;
358
+ el.appendChild(layerTitle);
359
+ }
360
+ return el;
361
+
362
+ case 'DiagramGroup':
363
+ el.className = 'component-group-diagram';
364
+ if (props.title) {
365
+ const groupTitle = document.createElement('div');
366
+ groupTitle.className = 'group-title';
367
+ groupTitle.textContent = props.title;
368
+ el.appendChild(groupTitle);
369
+ }
370
+ const groupContent = document.createElement('div');
371
+ groupContent.className = 'group-content';
372
+ el.appendChild(groupContent);
373
+ el._content = groupContent;
374
+ return el;
375
+
376
+ default:
377
+ console.warn('Unknown component type:', type);
378
+ el.textContent = `Unknown Component: ${type}`;
379
+ el.style.border = '1px dashed red';
380
+ el.style.padding = '10px';
381
+ el.style.color = 'red';
382
+ return el;
383
+ }
384
+ },
385
+
386
+ // 递归渲染树
387
+ renderNode(node, container) {
388
+ if (!node) return;
389
+
390
+ try {
391
+ const el = this.create(node.type || 'Unknown', node);
392
+
393
+ // 容器只有在非叶子节点时才去递归渲染 children
394
+ // Input, Button, Text, Box, Arrow 等通常不包含 children
395
+ const containerTypes = ['Page', 'Panel', 'Row', 'Col', 'Diagram', 'Layer', 'DiagramGroup'];
396
+ if (containerTypes.includes(node.type)) {
397
+ if (node.children && Array.isArray(node.children)) {
398
+ // Diagram 和 DiagramGroup 有特殊的内容容器
399
+ const targetContainer = el._content || el;
400
+ node.children.forEach(child => this.renderNode(child, targetContainer));
401
+ }
402
+ }
403
+
404
+ container.appendChild(el);
405
+ } catch (err) {
406
+ console.error('Render Error:', err, node);
407
+ const errEl = document.createElement('div');
408
+ errEl.style.color = 'red';
409
+ errEl.textContent = `渲染错误: ${err.message}`;
410
+ container.appendChild(errEl);
411
+ }
412
+ },
413
+
414
+ render(data) {
415
+ console.log('Rendering data:', data);
416
+ const app = document.getElementById('app');
417
+ app.innerHTML = ''; // 清空当前视图
418
+
419
+ if (!data) {
420
+ app.innerHTML = '<div style="text-align: center; padding: 50px;">无数据</div>';
421
+ return;
422
+ }
423
+
424
+ this.renderNode(data, app);
425
+ }
426
+ };
427
+
428
+ // 模拟数据轮询 (真实环境应该用 WebSocket 或 SSE)
429
+ let lastDataStr = '';
430
+
431
+ async function checkUpdate() {
432
+ try {
433
+ const res = await fetch('/ui.json');
434
+ if (res.ok) {
435
+ const data = await res.json();
436
+ const currentDataStr = JSON.stringify(data);
437
+
438
+ // 只有数据变化时才重新渲染
439
+ if (currentDataStr !== lastDataStr) {
440
+ lastDataStr = currentDataStr;
441
+ Renderer.render(data);
442
+ }
443
+
444
+ document.getElementById('status').textContent = '已连接: 实时预览中';
445
+ document.getElementById('status').style.background = '#059669';
446
+ }
447
+ } catch (e) {
448
+ console.log('Waiting for data...', e);
449
+ document.getElementById('status').textContent = `连接失败: ${e.message}`;
450
+ document.getElementById('status').style.background = '#dc2626';
451
+ }
452
+ }
453
+
454
+ // 初始化
455
+ setInterval(checkUpdate, 1000); // 每秒轮询一次
456
+ checkUpdate();
457
+ </script>
458
+ </body>
459
+
460
+ </html>
package/bin/prd-cli.js CHANGED
@@ -100,6 +100,17 @@ program
100
100
  require('../commands/upgrade')(options);
101
101
  });
102
102
 
103
+ // A2UI 预览服务
104
+ program
105
+ .command('ui')
106
+ .description('启动 A2UI 界面预览服务')
107
+ .option('-p, --port <number>', '指定端口号', '3333')
108
+ .action((options) => {
109
+ const A2UIServer = require('../commands/a2ui-server');
110
+ const server = new A2UIServer(options.port);
111
+ server.start();
112
+ });
113
+
103
114
  // 帮助信息增强
104
115
  program.on('--help', () => {
105
116
  console.log('');
@@ -0,0 +1,69 @@
1
+ const http = require('http');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const chalk = require('chalk');
5
+ const { exec } = require('child_process');
6
+
7
+ class A2UIServer {
8
+ constructor(port = 3333) {
9
+ this.port = port;
10
+ this.viewerPath = path.join(__dirname, '../a2ui-viewer');
11
+ this.projectPath = process.cwd();
12
+ }
13
+
14
+ start() {
15
+ const server = http.createServer((req, res) => {
16
+ // 处理 CORS
17
+ res.setHeader('Access-Control-Allow-Origin', '*');
18
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
19
+
20
+ // 路由处理
21
+ if (req.url === '/') {
22
+ this.serveFile(res, path.join(this.viewerPath, 'index.html'), 'text/html');
23
+ } else if (req.url === '/ui.json') {
24
+ // 读取项目根目录下的 a2ui-data.json
25
+ this.serveFile(res, path.join(this.projectPath, '.a2ui/current.json'), 'application/json');
26
+ } else {
27
+ res.writeHead(404);
28
+ res.end('Not found');
29
+ }
30
+ });
31
+
32
+ server.listen(this.port, () => {
33
+ console.log(chalk.green(`\n🚀 A2UI 预览服务已启动!`));
34
+ console.log(chalk.cyan(`👉 打开浏览器访问: http://localhost:${this.port}\n`));
35
+
36
+ // 自动打开浏览器
37
+ const startCommand = process.platform === 'darwin' ? 'open' :
38
+ process.platform === 'win32' ? 'start' : 'xdg-open';
39
+ exec(`${startCommand} http://localhost:${this.port}`);
40
+ });
41
+
42
+ return server;
43
+ }
44
+
45
+ serveFile(res, filePath, contentType) {
46
+ fs.readFile(filePath, (err, content) => {
47
+ if (err) {
48
+ if (err.code === 'ENOENT') {
49
+ // 如果数据文件不存在,返回空对象
50
+ if (filePath.endsWith('.json')) {
51
+ res.writeHead(200, { 'Content-Type': 'application/json' });
52
+ res.end(JSON.stringify({ type: 'Page', title: '等待数据...', children: [] }));
53
+ } else {
54
+ res.writeHead(404);
55
+ res.end('File not found');
56
+ }
57
+ } else {
58
+ res.writeHead(500);
59
+ res.end(`Server Error: ${err.code}`);
60
+ }
61
+ } else {
62
+ res.writeHead(200, { 'Content-Type': contentType });
63
+ res.end(content);
64
+ }
65
+ });
66
+ }
67
+ }
68
+
69
+ module.exports = A2UIServer;
package/commands/init.js CHANGED
@@ -18,6 +18,21 @@ module.exports = async function (projectName) {
18
18
  // 检查当前目录是否已经是 PRD 项目
19
19
  if (isCurrentDir && await fs.pathExists(path.join(projectPath, '.prd-config.json'))) {
20
20
  console.log(chalk.red('✗ 当前目录已经是 PRD 项目'));
21
+ console.log(chalk.yellow(' 如需更新规则文件,请运行: prd upgrade'));
22
+ return;
23
+ }
24
+
25
+ // ⚠️ 检查是否在已有 PRD 项目中创建子项目(常见错误)
26
+ if (!isCurrentDir && await fs.pathExists(path.join(process.cwd(), '.prd-config.json'))) {
27
+ console.log(chalk.yellow('⚠️ 警告:当前目录已经是一个 PRD 项目!'));
28
+ console.log(chalk.yellow(` 你正在尝试在 PRD 项目中创建子项目 "${projectName}"。`));
29
+ console.log('');
30
+ console.log(chalk.cyan(' 建议操作:'));
31
+ console.log(chalk.gray(' 1. 如果要在当前项目工作,直接使用 prd baseline create A0 等命令'));
32
+ console.log(chalk.gray(' 2. 如果确实要创建独立新项目,请先 cd 到其他目录'));
33
+ console.log(chalk.gray(' 3. 如果要更新规则文件,请运行: prd upgrade'));
34
+ console.log('');
35
+ console.log(chalk.red(' 已取消操作。'));
21
36
  return;
22
37
  }
23
38
 
@@ -267,6 +282,18 @@ module.exports = async function (projectName) {
267
282
  );
268
283
  }
269
284
 
285
+ // 复制 A2UI 预览器
286
+ const a2uiViewerDir = path.join(__dirname, '../a2ui-viewer');
287
+ if (await fs.pathExists(a2uiViewerDir)) {
288
+ await fs.copy(
289
+ a2uiViewerDir,
290
+ path.join(projectPath, 'a2ui-viewer')
291
+ );
292
+ }
293
+
294
+ // 创建 .a2ui 目录(用于临时预览数据)
295
+ await fs.ensureDir(path.join(projectPath, '.a2ui'));
296
+
270
297
  // 创建 README
271
298
  const readme = `# ${displayName}
272
299
 
@@ -352,9 +379,12 @@ prd plan freeze
352
379
  console.log(chalk.gray(' ✓ .agent/workflows/ - PRD 工作流指引(包含所有阶段的详细步骤)'));
353
380
  console.log(chalk.gray(' ✓ .cursorrules - Cursor AI 规则'));
354
381
  console.log(chalk.gray(' ✓ .antigravity/ - Antigravity AI 规则'));
382
+ console.log(chalk.gray(' ✓ a2ui-viewer/ - A2UI 界面预览器'));
383
+ console.log(chalk.gray(' ✓ .a2ui/ - A2UI 临时数据目录'));
355
384
  console.log('');
356
385
  console.log(chalk.yellow(' 💡 现在你可以直接与 AI 助手对话,AI 已经知道如何协助你完成 PRD 流程!'));
357
386
  console.log(chalk.gray(' 例如:告诉 AI "我要创建一个新项目的需求文档"'));
387
+ console.log(chalk.gray(' 启动界面预览:运行 prd ui'));
358
388
  console.log('');
359
389
 
360
390
  console.log(chalk.bold('📋 下一步操作(请按顺序执行):'));
@@ -375,6 +405,11 @@ prd plan freeze
375
405
  console.log(' prd baseline create A0 # P0 填写完成后执行');
376
406
  console.log('');
377
407
 
408
+ console.log(chalk.bold('🔄 后续更新:'));
409
+ console.log(chalk.gray(' 当 CLI 包有新版本时,运行以下命令同步更新项目规则:'));
410
+ console.log(chalk.cyan(' npm update -g prd-workflow-cli && prd upgrade'));
411
+ console.log('');
412
+
378
413
  } catch (error) {
379
414
  console.log(chalk.red('✗ 创建项目失败:'), error.message);
380
415
  }
@@ -105,6 +105,12 @@ module.exports = async function (options = {}) {
105
105
  source: '.antigravity',
106
106
  target: '.antigravity',
107
107
  isDir: true
108
+ },
109
+ {
110
+ name: 'A2UI Viewer (预览器)',
111
+ source: 'a2ui-viewer',
112
+ target: 'a2ui-viewer',
113
+ isDir: true
108
114
  }
109
115
  ];
110
116
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prd-workflow-cli",
3
- "version": "1.1.31",
3
+ "version": "1.2.6",
4
4
  "description": "产品需求管理规范 CLI 工具 - 基于 A→R→B→C 流程,集成 PM 确认机制和对话归档的需求管理命令行工具",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -48,6 +48,7 @@
48
48
  "bin/",
49
49
  "commands/",
50
50
  "scripts/",
51
+ "a2ui-viewer/",
51
52
  ".agent/",
52
53
  ".antigravity/",
53
54
  "templates/",
@@ -70,4 +71,4 @@
70
71
  "/tests/"
71
72
  ]
72
73
  }
73
- }
74
+ }