speccrew 0.2.2 → 0.2.4

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,826 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * update-progress.js - 通用进度文件更新工具
5
+ *
6
+ * 为 SpecCrew 所有 Agent 提供统一的进度文件更新工具,替代手动 PowerShell/Python 内联操作。
7
+ *
8
+ * 支持的命令:
9
+ *
10
+ * 1. init - 初始化 DISPATCH-PROGRESS.json
11
+ * node update-progress.js init --file <path> --stage <stage_name> --tasks <tasks_json_or_file>
12
+ * 选项:
13
+ * --file <path> 进度文件路径(必需)
14
+ * --stage <name> 阶段名称(必需)
15
+ * --tasks <json|file> 任务列表 JSON 或 JSON 文件路径
16
+ * --tasks-file <path> 从文件读取任务列表
17
+ *
18
+ * 2. read - 读取进度文件
19
+ * node update-progress.js read --file <path> [options]
20
+ * 选项:
21
+ * --file <path> 进度文件路径(必需)
22
+ * --task-id <id> 仅输出指定任务
23
+ * --status <status> 按状态过滤任务列表(pending/in_progress/partial/completed/failed)
24
+ * --summary 输出进度摘要(总数/完成/失败/部分/待处理)
25
+ * --checkpoints 读取所有 checkpoint 状态
26
+ * --overview 读取 workflow 全景(阶段概览)
27
+ *
28
+ * 3. update-task - 更新单个任务状态
29
+ * node update-progress.js update-task --file <path> --task-id <id> --status <status> [options]
30
+ * 选项:
31
+ * --file <path> 进度文件路径(必需)
32
+ * --task-id <id> 任务 ID(必需)
33
+ * --status <status> 任务状态:pending/in_progress/partial/completed/failed(必需)
34
+ * --output <text> 任务输出(completed 时使用)
35
+ * --error <text> 错误信息(failed 时使用)
36
+ * --error-category <cat> 错误类别(failed 时使用)
37
+ * --started-at <iso> 覆盖 started_at 时间戳
38
+ * --completed-at <iso> 覆盖 completed_at 时间戳
39
+ *
40
+ * 4. update-counts - 强制重算计数
41
+ * node update-progress.js update-counts --file <path>
42
+ *
43
+ * 5. write-checkpoint - 写入/更新 checkpoint
44
+ * node update-progress.js write-checkpoint --file <path> --stage <stage> --checkpoint <name> --passed <true|false> [--description <text>]
45
+ * 选项:
46
+ * --file <path> 进度文件路径(必需)
47
+ * --stage <name> 阶段名称(必需,如文件不存在则创建)
48
+ * --checkpoint <name> 检查点名称(必需)
49
+ * --passed <true|false> 是否通过(必需)
50
+ * --description <text> 描述信息(可选)
51
+ *
52
+ * 6. update-workflow - 更新 WORKFLOW-PROGRESS 阶段状态
53
+ * node update-progress.js update-workflow --file <path> --stage <name> --status <status> [--output <text>]
54
+ * 选项:
55
+ * --file <path> 进度文件路径(必需)
56
+ * --stage <name> 阶段名称(必需)
57
+ * --status <status> 状态:pending/in_progress/completed/confirmed(必需)
58
+ * --output <text> 输出信息(可选)
59
+ * --started-at <iso> 覆盖 started_at 时间戳
60
+ * --completed-at <iso> 覆盖 completed_at 时间戳
61
+ * --confirmed-at <iso> 覆盖 confirmed_at 时间戳
62
+ *
63
+ * 输出格式:
64
+ * 成功:{"success": true, "message": "...", "data": {...}}
65
+ * 失败:{"success": false, "error": "..."}(输出到 stderr,exit code 1)
66
+ */
67
+
68
+ const fs = require('fs');
69
+ const path = require('path');
70
+
71
+ // ============================================================================
72
+ // 工具函数
73
+ // ============================================================================
74
+
75
+ /**
76
+ * 生成 ISO 8601 格式时间戳
77
+ */
78
+ function getTimestamp() {
79
+ return new Date().toISOString();
80
+ }
81
+
82
+ /**
83
+ * 输出成功结果到 stdout
84
+ */
85
+ function outputSuccess(message, data = null) {
86
+ const result = { success: true, message };
87
+ if (data !== null) {
88
+ result.data = data;
89
+ }
90
+ console.log(JSON.stringify(result, null, 2));
91
+ }
92
+
93
+ /**
94
+ * 输出错误结果到 stderr 并退出
95
+ */
96
+ function outputError(error) {
97
+ console.error(JSON.stringify({ success: false, error }, null, 2));
98
+ process.exit(1);
99
+ }
100
+
101
+ /**
102
+ * 获取文件锁(防止并发冲突)
103
+ * @param {string} filePath 目标文件路径
104
+ * @returns {string} 锁文件路径
105
+ */
106
+ function acquireLock(filePath) {
107
+ const lockPath = `${filePath}.lock`;
108
+ const maxRetries = 30;
109
+ let retryCount = 0;
110
+
111
+ while (retryCount < maxRetries) {
112
+ try {
113
+ // 尝试独占创建锁文件
114
+ const fd = fs.openSync(lockPath, 'wx');
115
+ fs.closeSync(fd);
116
+ return lockPath;
117
+ } catch (error) {
118
+ retryCount++;
119
+ if (retryCount >= maxRetries) {
120
+ throw new Error(`Failed to acquire file lock for '${filePath}' after ${maxRetries} attempts`);
121
+ }
122
+ // 等待 1 秒后重试
123
+ const start = Date.now();
124
+ while (Date.now() - start < 1000) {
125
+ // Busy wait
126
+ }
127
+ }
128
+ }
129
+ }
130
+
131
+ /**
132
+ * 释放文件锁
133
+ * @param {string} lockPath 锁文件路径
134
+ */
135
+ function releaseLock(lockPath) {
136
+ try {
137
+ if (fs.existsSync(lockPath)) {
138
+ fs.unlinkSync(lockPath);
139
+ }
140
+ } catch (e) {
141
+ // 忽略清理错误
142
+ }
143
+ }
144
+
145
+ /**
146
+ * 原子写入 JSON 文件
147
+ * @param {string} filePath 目标文件路径
148
+ * @param {object} data 要写入的数据
149
+ */
150
+ function atomicWriteJson(filePath, data) {
151
+ const tempFile = `${filePath}.tmp`;
152
+ fs.writeFileSync(tempFile, JSON.stringify(data, null, 2), 'utf8');
153
+ fs.renameSync(tempFile, filePath);
154
+ }
155
+
156
+ /**
157
+ * 读取 JSON 文件
158
+ * @param {string} filePath 文件路径
159
+ * @returns {object} 解析后的 JSON 对象
160
+ */
161
+ function readJsonFile(filePath) {
162
+ if (!fs.existsSync(filePath)) {
163
+ throw new Error(`File not found: ${filePath}`);
164
+ }
165
+ const content = fs.readFileSync(filePath, 'utf8');
166
+ try {
167
+ return JSON.parse(content);
168
+ } catch (e) {
169
+ throw new Error(`Failed to parse JSON from ${filePath}: ${e.message}`);
170
+ }
171
+ }
172
+
173
+ /**
174
+ * 计算任务计数
175
+ * @param {Array} tasks 任务列表
176
+ * @returns {object} 计数结果
177
+ */
178
+ function calculateCounts(tasks) {
179
+ const total = tasks.length;
180
+ const completed = tasks.filter(t => t.status === 'completed').length;
181
+ const failed = tasks.filter(t => t.status === 'failed').length;
182
+ const partial = tasks.filter(t => t.status === 'partial').length;
183
+ const pending = tasks.filter(t => t.status === 'pending' || !t.status).length;
184
+ const inProgress = tasks.filter(t => t.status === 'in_progress').length;
185
+ return { total, completed, failed, partial, pending, in_progress: inProgress };
186
+ }
187
+
188
+ // ============================================================================
189
+ // 参数解析
190
+ // ============================================================================
191
+
192
+ /**
193
+ * 解析命令行参数
194
+ * 支持 --flag value 和 -Flag value 两种格式
195
+ */
196
+ function parseArgs() {
197
+ const args = process.argv.slice(2);
198
+ const result = {
199
+ command: null,
200
+ file: null,
201
+ stage: null,
202
+ tasks: null,
203
+ tasksFile: null,
204
+ taskId: null,
205
+ status: null,
206
+ output: null,
207
+ error: null,
208
+ errorCategory: null,
209
+ checkpoint: null,
210
+ passed: null,
211
+ description: null,
212
+ startedAt: null,
213
+ completedAt: null,
214
+ confirmedAt: null
215
+ };
216
+
217
+ // 第一个参数是命令
218
+ if (args.length > 0 && !args[0].startsWith('-')) {
219
+ result.command = args[0];
220
+ }
221
+
222
+ for (let i = 0; i < args.length; i++) {
223
+ const arg = args[i];
224
+
225
+ // 跳过命令本身
226
+ if (i === 0 && !arg.startsWith('-')) {
227
+ continue;
228
+ }
229
+
230
+ switch (arg) {
231
+ case '--file':
232
+ case '-File':
233
+ result.file = args[++i];
234
+ break;
235
+ case '--stage':
236
+ case '-Stage':
237
+ result.stage = args[++i];
238
+ break;
239
+ case '--tasks':
240
+ case '-Tasks':
241
+ result.tasks = args[++i];
242
+ break;
243
+ case '--tasks-file':
244
+ case '-Tasks-File':
245
+ result.tasksFile = args[++i];
246
+ break;
247
+ case '--task-id':
248
+ case '-Task-Id':
249
+ result.taskId = args[++i];
250
+ break;
251
+ case '--status':
252
+ case '-Status':
253
+ result.status = args[++i];
254
+ break;
255
+ case '--output':
256
+ case '-Output':
257
+ result.output = args[++i];
258
+ break;
259
+ case '--error':
260
+ case '-Error':
261
+ result.error = args[++i];
262
+ break;
263
+ case '--error-category':
264
+ case '-Error-Category':
265
+ result.errorCategory = args[++i];
266
+ break;
267
+ case '--checkpoint':
268
+ case '-Checkpoint':
269
+ result.checkpoint = args[++i];
270
+ break;
271
+ case '--passed':
272
+ case '-Passed':
273
+ result.passed = args[++i];
274
+ break;
275
+ case '--description':
276
+ case '-Description':
277
+ result.description = args[++i];
278
+ break;
279
+ case '--started-at':
280
+ case '-Started-At':
281
+ result.startedAt = args[++i];
282
+ break;
283
+ case '--completed-at':
284
+ case '-Completed-At':
285
+ result.completedAt = args[++i];
286
+ break;
287
+ case '--confirmed-at':
288
+ case '-Confirmed-At':
289
+ result.confirmedAt = args[++i];
290
+ break;
291
+ case '--summary':
292
+ case '-Summary':
293
+ result.summary = true;
294
+ break;
295
+ case '--checkpoints':
296
+ case '-Checkpoints':
297
+ result.checkpoints = true;
298
+ break;
299
+ case '--overview':
300
+ case '-Overview':
301
+ result.overview = true;
302
+ break;
303
+ }
304
+ }
305
+
306
+ return result;
307
+ }
308
+
309
+ // ============================================================================
310
+ // 命令实现
311
+ // ============================================================================
312
+
313
+ /**
314
+ * 命令:init - 初始化进度文件
315
+ */
316
+ function cmdInit(args) {
317
+ if (!args.file || !args.stage) {
318
+ outputError('Usage: init --file <path> --stage <stage_name> [--tasks <json>] [--tasks-file <path>]');
319
+ }
320
+
321
+ const filePath = path.resolve(args.file);
322
+ let tasks = [];
323
+
324
+ // 从参数或文件读取任务列表
325
+ if (args.tasksFile) {
326
+ // 从文件读取
327
+ const tasksContent = fs.readFileSync(path.resolve(args.tasksFile), 'utf8');
328
+ try {
329
+ tasks = JSON.parse(tasksContent);
330
+ } catch (e) {
331
+ outputError(`Failed to parse tasks file: ${e.message}`);
332
+ }
333
+ } else if (args.tasks) {
334
+ // 直接解析 JSON
335
+ try {
336
+ tasks = JSON.parse(args.tasks);
337
+ } catch (e) {
338
+ outputError(`Failed to parse tasks JSON: ${e.message}`);
339
+ }
340
+ }
341
+
342
+ // 验证 tasks 是数组
343
+ if (!Array.isArray(tasks)) {
344
+ outputError('Tasks must be an array');
345
+ }
346
+
347
+ // 确保每个任务有必要的字段
348
+ tasks = tasks.map((task, index) => ({
349
+ id: task.id || `task-${index + 1}`,
350
+ name: task.name || task.id || `Task ${index + 1}`,
351
+ status: task.status || 'pending',
352
+ created_at: task.created_at || getTimestamp(),
353
+ ...task
354
+ }));
355
+
356
+ // 创建进度文件结构
357
+ const progressData = {
358
+ stage: args.stage,
359
+ created_at: getTimestamp(),
360
+ updated_at: getTimestamp(),
361
+ counts: calculateCounts(tasks),
362
+ tasks: tasks,
363
+ checkpoints: {}
364
+ };
365
+
366
+ // 确保目录存在
367
+ const dir = path.dirname(filePath);
368
+ if (!fs.existsSync(dir)) {
369
+ fs.mkdirSync(dir, { recursive: true });
370
+ }
371
+
372
+ // 获取锁并写入
373
+ let lockPath = null;
374
+ try {
375
+ lockPath = acquireLock(filePath);
376
+ atomicWriteJson(filePath, progressData);
377
+ outputSuccess(`Progress file initialized: ${filePath}`, {
378
+ file: filePath,
379
+ stage: args.stage,
380
+ counts: progressData.counts
381
+ });
382
+ } finally {
383
+ if (lockPath) releaseLock(lockPath);
384
+ }
385
+ }
386
+
387
+ /**
388
+ * 命令:read - 读取进度文件(增强版)
389
+ * 支持多种查询模式:
390
+ * - 默认:读取整个文件
391
+ * - --task-id:查询单个任务
392
+ * - --status:按状态过滤任务
393
+ * - --summary:输出进度摘要
394
+ * - --checkpoints:读取 checkpoint 状态
395
+ * - --overview:读取 workflow 全景
396
+ */
397
+ function cmdRead(args) {
398
+ if (!args.file) {
399
+ outputError('Usage: read --file <path> [--task-id <id>] [--status <status>] [--summary] [--checkpoints] [--overview]');
400
+ }
401
+
402
+ const filePath = path.resolve(args.file);
403
+ let lockPath = null;
404
+
405
+ try {
406
+ lockPath = acquireLock(filePath);
407
+ const data = readJsonFile(filePath);
408
+
409
+ // 1. --summary 模式:输出进度摘要
410
+ if (args.summary) {
411
+ const counts = data.counts || calculateCounts(data.tasks || []);
412
+ const summary = {
413
+ file: filePath,
414
+ stage: data.stage || null,
415
+ current_stage: data.current_stage || null,
416
+ updated_at: data.updated_at || null,
417
+ counts: counts,
418
+ progress_percent: counts.total > 0
419
+ ? Math.round((counts.completed / counts.total) * 100)
420
+ : 0
421
+ };
422
+ outputSuccess('Progress summary', summary);
423
+ return;
424
+ }
425
+
426
+ // 2. --checkpoints 模式:读取 checkpoint 状态
427
+ if (args.checkpoints) {
428
+ const checkpoints = data.checkpoints || {};
429
+ const checkpointList = Object.entries(checkpoints).map(([name, cp]) => ({
430
+ name,
431
+ passed: cp.passed,
432
+ checked_at: cp.checked_at,
433
+ confirmed_at: cp.confirmed_at,
434
+ description: cp.description
435
+ }));
436
+ outputSuccess('Checkpoints', {
437
+ total: checkpointList.length,
438
+ passed: checkpointList.filter(cp => cp.passed).length,
439
+ failed: checkpointList.filter(cp => !cp.passed).length,
440
+ checkpoints: checkpointList
441
+ });
442
+ return;
443
+ }
444
+
445
+ // 3. --overview 模式:读取 workflow 全景
446
+ if (args.overview) {
447
+ const stages = data.stages || {};
448
+ const stageList = Object.entries(stages).map(([name, stage]) => ({
449
+ name,
450
+ status: stage.status,
451
+ started_at: stage.started_at,
452
+ completed_at: stage.completed_at,
453
+ confirmed_at: stage.confirmed_at,
454
+ output: stage.output
455
+ }));
456
+
457
+ const overview = {
458
+ current_stage: data.current_stage || null,
459
+ created_at: data.created_at,
460
+ updated_at: data.updated_at,
461
+ stages: stageList,
462
+ stage_summary: {
463
+ total: stageList.length,
464
+ pending: stageList.filter(s => s.status === 'pending').length,
465
+ in_progress: stageList.filter(s => s.status === 'in_progress').length,
466
+ completed: stageList.filter(s => s.status === 'completed').length,
467
+ confirmed: stageList.filter(s => s.status === 'confirmed').length
468
+ }
469
+ };
470
+ outputSuccess('Workflow overview', overview);
471
+ return;
472
+ }
473
+
474
+ // 4. --task-id 模式:查询单个任务
475
+ if (args.taskId) {
476
+ const task = data.tasks?.find(t => t.id === args.taskId);
477
+ if (!task) {
478
+ outputError(`Task not found: ${args.taskId}`);
479
+ }
480
+ outputSuccess(`Task: ${args.taskId}`, task);
481
+ return;
482
+ }
483
+
484
+ // 5. --status 模式:按状态过滤任务
485
+ if (args.status) {
486
+ const validStatuses = ['pending', 'in_progress', 'partial', 'completed', 'failed'];
487
+ if (!validStatuses.includes(args.status)) {
488
+ outputError(`Invalid status filter: ${args.status}. Must be one of: ${validStatuses.join(', ')}`);
489
+ }
490
+ const filteredTasks = (data.tasks || []).filter(t => t.status === args.status);
491
+ outputSuccess(`Tasks with status: ${args.status}`, {
492
+ status: args.status,
493
+ count: filteredTasks.length,
494
+ tasks: filteredTasks
495
+ });
496
+ return;
497
+ }
498
+
499
+ // 6. 默认模式:输出整个文件
500
+ outputSuccess(`Progress file: ${filePath}`, data);
501
+ } finally {
502
+ if (lockPath) releaseLock(lockPath);
503
+ }
504
+ }
505
+
506
+ /**
507
+ * 命令:update-task - 更新单个任务状态
508
+ */
509
+ function cmdUpdateTask(args) {
510
+ if (!args.file || !args.taskId || !args.status) {
511
+ outputError('Usage: update-task --file <path> --task-id <id> --status <status> [options]');
512
+ }
513
+
514
+ const validStatuses = ['pending', 'in_progress', 'partial', 'completed', 'failed'];
515
+ if (!validStatuses.includes(args.status)) {
516
+ outputError(`Invalid status: ${args.status}. Must be one of: ${validStatuses.join(', ')}`);
517
+ }
518
+
519
+ const filePath = path.resolve(args.file);
520
+ let lockPath = null;
521
+
522
+ try {
523
+ lockPath = acquireLock(filePath);
524
+ const data = readJsonFile(filePath);
525
+
526
+ // 查找任务
527
+ const taskIndex = data.tasks?.findIndex(t => t.id === args.taskId);
528
+ if (taskIndex === -1 || taskIndex === undefined) {
529
+ outputError(`Task not found: ${args.taskId}`);
530
+ }
531
+
532
+ const task = data.tasks[taskIndex];
533
+ const now = getTimestamp();
534
+
535
+ // 更新状态
536
+ task.status = args.status;
537
+ task.updated_at = now;
538
+
539
+ // 根据状态自动设置时间戳
540
+ if (args.status === 'in_progress') {
541
+ task.started_at = args.startedAt || now;
542
+ } else if (args.status === 'partial') {
543
+ // partial 状态:部分完成,可能已有 started_at,可选设置 completed_at
544
+ if (!task.started_at) {
545
+ task.started_at = args.startedAt || now;
546
+ }
547
+ if (args.output) {
548
+ task.output = args.output;
549
+ }
550
+ } else if (args.status === 'completed') {
551
+ task.completed_at = args.completedAt || now;
552
+ if (args.output) {
553
+ task.output = args.output;
554
+ }
555
+ } else if (args.status === 'failed') {
556
+ task.completed_at = args.completedAt || now;
557
+ if (args.error) {
558
+ task.error = args.error;
559
+ }
560
+ if (args.errorCategory) {
561
+ task.error_category = args.errorCategory;
562
+ }
563
+ }
564
+
565
+ // 更新任务
566
+ data.tasks[taskIndex] = task;
567
+ data.updated_at = now;
568
+
569
+ // 重算计数
570
+ data.counts = calculateCounts(data.tasks);
571
+
572
+ // 原子写入
573
+ atomicWriteJson(filePath, data);
574
+
575
+ outputSuccess(`Task updated: ${args.taskId}`, {
576
+ task_id: args.taskId,
577
+ status: args.status,
578
+ counts: data.counts
579
+ });
580
+ } finally {
581
+ if (lockPath) releaseLock(lockPath);
582
+ }
583
+ }
584
+
585
+ /**
586
+ * 命令:update-counts - 强制重算计数
587
+ */
588
+ function cmdUpdateCounts(args) {
589
+ if (!args.file) {
590
+ outputError('Usage: update-counts --file <path>');
591
+ }
592
+
593
+ const filePath = path.resolve(args.file);
594
+ let lockPath = null;
595
+
596
+ try {
597
+ lockPath = acquireLock(filePath);
598
+ const data = readJsonFile(filePath);
599
+
600
+ if (!data.tasks || !Array.isArray(data.tasks)) {
601
+ outputError('No tasks array found in progress file');
602
+ }
603
+
604
+ // 重算计数
605
+ data.counts = calculateCounts(data.tasks);
606
+ data.updated_at = getTimestamp();
607
+
608
+ // 原子写入
609
+ atomicWriteJson(filePath, data);
610
+
611
+ outputSuccess('Counts updated', { counts: data.counts });
612
+ } finally {
613
+ if (lockPath) releaseLock(lockPath);
614
+ }
615
+ }
616
+
617
+ /**
618
+ * 命令:write-checkpoint - 写入/更新 checkpoint
619
+ */
620
+ function cmdWriteCheckpoint(args) {
621
+ if (!args.file || !args.stage || !args.checkpoint || args.passed === null) {
622
+ outputError('Usage: write-checkpoint --file <path> --stage <stage> --checkpoint <name> --passed <true|false> [--description <text>]');
623
+ }
624
+
625
+ const filePath = path.resolve(args.file);
626
+ const passed = ['true', '1', '$true', 'True', 'TRUE'].includes(args.passed);
627
+ const now = getTimestamp();
628
+ let lockPath = null;
629
+
630
+ try {
631
+ lockPath = acquireLock(filePath);
632
+
633
+ let data;
634
+ if (fs.existsSync(filePath)) {
635
+ data = readJsonFile(filePath);
636
+ } else {
637
+ // 创建新文件
638
+ data = {
639
+ stage: args.stage,
640
+ created_at: now,
641
+ counts: { total: 0, completed: 0, failed: 0, pending: 0, in_progress: 0 },
642
+ tasks: [],
643
+ checkpoints: {}
644
+ };
645
+ }
646
+
647
+ // 确保 checkpoints 对象存在
648
+ if (!data.checkpoints) {
649
+ data.checkpoints = {};
650
+ }
651
+
652
+ // 更新或创建 checkpoint
653
+ data.checkpoints[args.checkpoint] = {
654
+ passed: passed,
655
+ checked_at: now,
656
+ confirmed_at: passed ? now : null,
657
+ description: args.description || null
658
+ };
659
+
660
+ data.updated_at = now;
661
+
662
+ // 确保目录存在
663
+ const dir = path.dirname(filePath);
664
+ if (!fs.existsSync(dir)) {
665
+ fs.mkdirSync(dir, { recursive: true });
666
+ }
667
+
668
+ // 原子写入
669
+ atomicWriteJson(filePath, data);
670
+
671
+ outputSuccess(`Checkpoint updated: ${args.checkpoint}`, {
672
+ checkpoint: args.checkpoint,
673
+ passed: passed,
674
+ confirmed_at: data.checkpoints[args.checkpoint].confirmed_at
675
+ });
676
+ } finally {
677
+ if (lockPath) releaseLock(lockPath);
678
+ }
679
+ }
680
+
681
+ /**
682
+ * 命令:update-workflow - 更新 WORKFLOW-PROGRESS 阶段状态
683
+ */
684
+ function cmdUpdateWorkflow(args) {
685
+ if (!args.file || !args.stage || !args.status) {
686
+ outputError('Usage: update-workflow --file <path> --stage <name> --status <status> [--output <text>]');
687
+ }
688
+
689
+ const validStatuses = ['pending', 'in_progress', 'completed', 'confirmed'];
690
+ if (!validStatuses.includes(args.status)) {
691
+ outputError(`Invalid status: ${args.status}. Must be one of: ${validStatuses.join(', ')}`);
692
+ }
693
+
694
+ const filePath = path.resolve(args.file);
695
+ const now = getTimestamp();
696
+ let lockPath = null;
697
+
698
+ try {
699
+ lockPath = acquireLock(filePath);
700
+
701
+ let data;
702
+ if (fs.existsSync(filePath)) {
703
+ data = readJsonFile(filePath);
704
+ } else {
705
+ // 创建新文件
706
+ data = {
707
+ created_at: now,
708
+ stages: {},
709
+ current_stage: null
710
+ };
711
+ }
712
+
713
+ // 确保 stages 对象存在
714
+ if (!data.stages) {
715
+ data.stages = {};
716
+ }
717
+
718
+ // 获取或创建阶段
719
+ if (!data.stages[args.stage]) {
720
+ data.stages[args.stage] = {
721
+ status: 'pending',
722
+ started_at: null,
723
+ completed_at: null,
724
+ confirmed_at: null,
725
+ output: null
726
+ };
727
+ }
728
+
729
+ const stage = data.stages[args.stage];
730
+
731
+ // 更新状态
732
+ stage.status = args.status;
733
+
734
+ // 根据状态自动设置时间戳
735
+ if (args.status === 'in_progress') {
736
+ // 如 started_at 已有值则不覆盖
737
+ if (!stage.started_at) {
738
+ stage.started_at = args.startedAt || now;
739
+ }
740
+ } else if (args.status === 'completed') {
741
+ stage.completed_at = args.completedAt || now;
742
+ } else if (args.status === 'confirmed') {
743
+ stage.confirmed_at = args.confirmedAt || now;
744
+ }
745
+
746
+ // 更新输出
747
+ if (args.output) {
748
+ stage.output = args.output;
749
+ }
750
+
751
+ // 更新当前阶段
752
+ data.current_stage = args.stage;
753
+ data.updated_at = now;
754
+
755
+ // 确保目录存在
756
+ const dir = path.dirname(filePath);
757
+ if (!fs.existsSync(dir)) {
758
+ fs.mkdirSync(dir, { recursive: true });
759
+ }
760
+
761
+ // 原子写入
762
+ atomicWriteJson(filePath, data);
763
+
764
+ outputSuccess(`Workflow stage updated: ${args.stage}`, {
765
+ stage: args.stage,
766
+ status: args.status,
767
+ current_stage: data.current_stage
768
+ });
769
+ } finally {
770
+ if (lockPath) releaseLock(lockPath);
771
+ }
772
+ }
773
+
774
+ // ============================================================================
775
+ // 主入口
776
+ // ============================================================================
777
+
778
+ function main() {
779
+ const args = parseArgs();
780
+
781
+ // 无命令时显示帮助
782
+ if (!args.command) {
783
+ console.error('Usage: node update-progress.js <command> [options]');
784
+ console.error('');
785
+ console.error('Commands:');
786
+ console.error(' init Initialize a progress file');
787
+ console.error(' read Read a progress file');
788
+ console.error(' update-task Update a task status');
789
+ console.error(' update-counts Recalculate task counts');
790
+ console.error(' write-checkpoint Write or update a checkpoint');
791
+ console.error(' update-workflow Update a workflow stage status');
792
+ console.error('');
793
+ console.error('Run "node update-progress.js <command> --help" for more information.');
794
+ process.exit(1);
795
+ }
796
+
797
+ // 分发命令
798
+ try {
799
+ switch (args.command) {
800
+ case 'init':
801
+ cmdInit(args);
802
+ break;
803
+ case 'read':
804
+ cmdRead(args);
805
+ break;
806
+ case 'update-task':
807
+ cmdUpdateTask(args);
808
+ break;
809
+ case 'update-counts':
810
+ cmdUpdateCounts(args);
811
+ break;
812
+ case 'write-checkpoint':
813
+ cmdWriteCheckpoint(args);
814
+ break;
815
+ case 'update-workflow':
816
+ cmdUpdateWorkflow(args);
817
+ break;
818
+ default:
819
+ outputError(`Unknown command: ${args.command}`);
820
+ }
821
+ } catch (error) {
822
+ outputError(error.message);
823
+ }
824
+ }
825
+
826
+ main();