workflow-ai 1.0.10 → 1.0.12
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/configs/pipeline.yaml +480 -440
- package/package.json +1 -1
- package/src/cli.mjs +130 -130
- package/src/lib/utils.mjs +74 -0
- package/src/runner.mjs +1518 -1492
- package/src/scripts/check-conditions.js +69 -4
- package/src/scripts/move-ticket.js +3 -3
- package/src/scripts/pick-next-task.js +177 -2
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -1,131 +1,131 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { initProject } from './init.mjs';
|
|
4
|
-
import { runPipeline } from './runner.mjs';
|
|
5
|
-
import { readFileSync } from 'node:fs';
|
|
6
|
-
import { join, dirname } from 'node:path';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
-
|
|
9
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
-
const pkgPath = join(__dirname, '..', 'package.json');
|
|
11
|
-
|
|
12
|
-
const HELP_TEXT = `workflow-ai v1.0.0
|
|
13
|
-
|
|
14
|
-
Usage:
|
|
15
|
-
workflow init [path] [--force] Initialize .workflow/ in target directory
|
|
16
|
-
workflow run [options] Run the AI pipeline
|
|
17
|
-
workflow help Show this help
|
|
18
|
-
workflow version Show version
|
|
19
|
-
|
|
20
|
-
Run options:
|
|
21
|
-
--plan <plan> Plan ID to execute
|
|
22
|
-
--config <path> Config file path
|
|
23
|
-
--project <path> Project root (default: auto-detect)
|
|
24
|
-
`;
|
|
25
|
-
|
|
26
|
-
function showHelp() {
|
|
27
|
-
console.log(HELP_TEXT);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function showVersion() {
|
|
31
|
-
try {
|
|
32
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
33
|
-
console.log(`workflow-ai v${pkg.version}`);
|
|
34
|
-
} catch (err) {
|
|
35
|
-
console.error('Error reading package.json:', err.message);
|
|
36
|
-
process.exit(1);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function parseArgs(argv) {
|
|
41
|
-
const args = { _: [] };
|
|
42
|
-
let i = 0;
|
|
43
|
-
while (i < argv.length) {
|
|
44
|
-
const arg = argv[i];
|
|
45
|
-
if (arg.startsWith('--')) {
|
|
46
|
-
const key = arg.slice(2);
|
|
47
|
-
if (i + 1 < argv.length && !argv[i + 1].startsWith('--')) {
|
|
48
|
-
args[key] = argv[i + 1];
|
|
49
|
-
i += 2;
|
|
50
|
-
} else {
|
|
51
|
-
args[key] = true;
|
|
52
|
-
i += 1;
|
|
53
|
-
}
|
|
54
|
-
} else {
|
|
55
|
-
args._.push(arg);
|
|
56
|
-
i += 1;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return args;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async function runInit(args) {
|
|
63
|
-
const targetPath = args._[0] || process.cwd();
|
|
64
|
-
const force = args.force === true;
|
|
65
|
-
const result = initProject(targetPath, { force });
|
|
66
|
-
|
|
67
|
-
if (result.errors && result.errors.length > 0) {
|
|
68
|
-
console.error('Errors:', result.errors.join(', '));
|
|
69
|
-
process.exit(1);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
console.log('✅ Initialization completed:');
|
|
73
|
-
result.steps.forEach(step => console.log(` • ${step}`));
|
|
74
|
-
if (result.warnings && result.warnings.length > 0) {
|
|
75
|
-
console.warn('Warnings:', result.warnings.join(', '));
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async function runRun(args) {
|
|
80
|
-
// Expose wf's node_modules to child ESM scripts via a custom loader
|
|
81
|
-
const loaderPath = join(__dirname, 'wf-loader.mjs');
|
|
82
|
-
const loaderUrl = `file:///${loaderPath.replace(/\\/g, '/')}`;
|
|
83
|
-
process.env.NODE_OPTIONS = process.env.NODE_OPTIONS
|
|
84
|
-
? `${process.env.NODE_OPTIONS} --import ${loaderUrl}`
|
|
85
|
-
: `--import ${loaderUrl}`;
|
|
86
|
-
|
|
87
|
-
const argv = [];
|
|
88
|
-
if (args.plan) {
|
|
89
|
-
argv.push('--plan', args.plan);
|
|
90
|
-
}
|
|
91
|
-
if (args.config) {
|
|
92
|
-
argv.push('--config', args.config);
|
|
93
|
-
}
|
|
94
|
-
if (args.project) {
|
|
95
|
-
argv.push('--project', args.project);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
await runPipeline(argv);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function run(argv) {
|
|
102
|
-
const args = parseArgs(argv);
|
|
103
|
-
const command = args._.shift();
|
|
104
|
-
|
|
105
|
-
switch (command) {
|
|
106
|
-
case 'init':
|
|
107
|
-
runInit(args);
|
|
108
|
-
break;
|
|
109
|
-
case 'run':
|
|
110
|
-
runRun(args);
|
|
111
|
-
break;
|
|
112
|
-
case 'help':
|
|
113
|
-
showHelp();
|
|
114
|
-
break;
|
|
115
|
-
case 'version':
|
|
116
|
-
showVersion();
|
|
117
|
-
break;
|
|
118
|
-
default:
|
|
119
|
-
if (!command) {
|
|
120
|
-
showHelp();
|
|
121
|
-
} else {
|
|
122
|
-
console.error(`Unknown command: ${command}`);
|
|
123
|
-
console.error('Run "workflow help" for usage.');
|
|
124
|
-
process.exit(1);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
130
|
-
run(process.argv.slice(2));
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { initProject } from './init.mjs';
|
|
4
|
+
import { runPipeline } from './runner.mjs';
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { join, dirname } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const pkgPath = join(__dirname, '..', 'package.json');
|
|
11
|
+
|
|
12
|
+
const HELP_TEXT = `workflow-ai v1.0.0
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
workflow init [path] [--force] Initialize .workflow/ in target directory
|
|
16
|
+
workflow run [options] Run the AI pipeline
|
|
17
|
+
workflow help Show this help
|
|
18
|
+
workflow version Show version
|
|
19
|
+
|
|
20
|
+
Run options:
|
|
21
|
+
--plan <plan> Plan ID to execute
|
|
22
|
+
--config <path> Config file path
|
|
23
|
+
--project <path> Project root (default: auto-detect)
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
function showHelp() {
|
|
27
|
+
console.log(HELP_TEXT);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function showVersion() {
|
|
31
|
+
try {
|
|
32
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
33
|
+
console.log(`workflow-ai v${pkg.version}`);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error('Error reading package.json:', err.message);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseArgs(argv) {
|
|
41
|
+
const args = { _: [] };
|
|
42
|
+
let i = 0;
|
|
43
|
+
while (i < argv.length) {
|
|
44
|
+
const arg = argv[i];
|
|
45
|
+
if (arg.startsWith('--')) {
|
|
46
|
+
const key = arg.slice(2);
|
|
47
|
+
if (i + 1 < argv.length && !argv[i + 1].startsWith('--')) {
|
|
48
|
+
args[key] = argv[i + 1];
|
|
49
|
+
i += 2;
|
|
50
|
+
} else {
|
|
51
|
+
args[key] = true;
|
|
52
|
+
i += 1;
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
args._.push(arg);
|
|
56
|
+
i += 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return args;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function runInit(args) {
|
|
63
|
+
const targetPath = args._[0] || process.cwd();
|
|
64
|
+
const force = args.force === true;
|
|
65
|
+
const result = initProject(targetPath, { force });
|
|
66
|
+
|
|
67
|
+
if (result.errors && result.errors.length > 0) {
|
|
68
|
+
console.error('Errors:', result.errors.join(', '));
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log('✅ Initialization completed:');
|
|
73
|
+
result.steps.forEach(step => console.log(` • ${step}`));
|
|
74
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
75
|
+
console.warn('Warnings:', result.warnings.join(', '));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function runRun(args) {
|
|
80
|
+
// Expose wf's node_modules to child ESM scripts via a custom loader
|
|
81
|
+
const loaderPath = join(__dirname, 'wf-loader.mjs');
|
|
82
|
+
const loaderUrl = `file:///${loaderPath.replace(/\\/g, '/')}`;
|
|
83
|
+
process.env.NODE_OPTIONS = process.env.NODE_OPTIONS
|
|
84
|
+
? `${process.env.NODE_OPTIONS} --import ${loaderUrl}`
|
|
85
|
+
: `--import ${loaderUrl}`;
|
|
86
|
+
|
|
87
|
+
const argv = [];
|
|
88
|
+
if (args.plan) {
|
|
89
|
+
argv.push('--plan', args.plan);
|
|
90
|
+
}
|
|
91
|
+
if (args.config) {
|
|
92
|
+
argv.push('--config', args.config);
|
|
93
|
+
}
|
|
94
|
+
if (args.project) {
|
|
95
|
+
argv.push('--project', args.project);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
await runPipeline(argv);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function run(argv) {
|
|
102
|
+
const args = parseArgs(argv);
|
|
103
|
+
const command = args._.shift();
|
|
104
|
+
|
|
105
|
+
switch (command) {
|
|
106
|
+
case 'init':
|
|
107
|
+
runInit(args);
|
|
108
|
+
break;
|
|
109
|
+
case 'run':
|
|
110
|
+
runRun(args);
|
|
111
|
+
break;
|
|
112
|
+
case 'help':
|
|
113
|
+
showHelp();
|
|
114
|
+
break;
|
|
115
|
+
case 'version':
|
|
116
|
+
showVersion();
|
|
117
|
+
break;
|
|
118
|
+
default:
|
|
119
|
+
if (!command) {
|
|
120
|
+
showHelp();
|
|
121
|
+
} else {
|
|
122
|
+
console.error(`Unknown command: ${command}`);
|
|
123
|
+
console.error('Run "workflow help" for usage.');
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
130
|
+
run(process.argv.slice(2));
|
|
131
131
|
}
|
package/src/lib/utils.mjs
CHANGED
|
@@ -96,3 +96,77 @@ export function getPackageRoot() {
|
|
|
96
96
|
// result/src/lib → result
|
|
97
97
|
return path.resolve(__dirname, '../../');
|
|
98
98
|
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Парсит секцию "## Ревью" тикета и возвращает статус последней записи.
|
|
102
|
+
* Поддерживает табличный и текстовый форматы.
|
|
103
|
+
*
|
|
104
|
+
* Табличный формат:
|
|
105
|
+
* | Дата | Статус | Комментарий |
|
|
106
|
+
* |------|--------|-------------|
|
|
107
|
+
* | 2026-03-08 | passed | Всё ок |
|
|
108
|
+
*
|
|
109
|
+
* Текстовый формат:
|
|
110
|
+
* - 2026-03-08: passed - Всё ок
|
|
111
|
+
* - 2026-03-08: failed - Есть замечания
|
|
112
|
+
*
|
|
113
|
+
* @param {string} content - Содержимое тикета (markdown)
|
|
114
|
+
* @returns {string|null} "passed", "failed" или null (если нет ревью)
|
|
115
|
+
*/
|
|
116
|
+
export function getLastReviewStatus(content) {
|
|
117
|
+
if (!content) return null;
|
|
118
|
+
|
|
119
|
+
// Находим секцию "## Ревью" — захватываем всё до следующего заголовка ## или конца файла
|
|
120
|
+
const reviewSectionMatch = content.match(/^##\s*Ревью\s*\n([\s\S]*)(?=\n^##\s|$)/m);
|
|
121
|
+
if (!reviewSectionMatch) return null;
|
|
122
|
+
|
|
123
|
+
const reviewSection = reviewSectionMatch[1].trim();
|
|
124
|
+
if (!reviewSection) return null;
|
|
125
|
+
|
|
126
|
+
// Пробуем распарсить табличный формат
|
|
127
|
+
const tableRows = reviewSection.split('\n').filter(line => line.trim().startsWith('|'));
|
|
128
|
+
if (tableRows.length >= 2) {
|
|
129
|
+
// Есть заголовок и разделитель, ищем строки с данными
|
|
130
|
+
const dataRows = tableRows.slice(2).filter(row => {
|
|
131
|
+
const cells = row.split('|').map(c => c.trim()).filter(c => c);
|
|
132
|
+
return cells.length >= 2;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
if (dataRows.length > 0) {
|
|
136
|
+
// Ищем строку с самой поздней датой (при равных — последняя по позиции)
|
|
137
|
+
let latestRow = dataRows[0];
|
|
138
|
+
let latestDate = '';
|
|
139
|
+
for (const row of dataRows) {
|
|
140
|
+
const cells = row.split('|').map(c => c.trim()).filter(c => c);
|
|
141
|
+
const dateStr = cells[0] || '';
|
|
142
|
+
if (dateStr >= latestDate) {
|
|
143
|
+
latestDate = dateStr;
|
|
144
|
+
latestRow = row;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const cells = latestRow.split('|').map(c => c.trim()).filter(c => c);
|
|
148
|
+
const statusRaw = cells[1]?.toLowerCase() || '';
|
|
149
|
+
if (statusRaw.includes('passed')) return 'passed';
|
|
150
|
+
if (statusRaw.includes('failed')) return 'failed';
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Пробуем распарсить текстовый формат (список)
|
|
155
|
+
const listItems = reviewSection.split('\n').filter(line => line.trim().match(/^[-*]\s/));
|
|
156
|
+
if (listItems.length > 0) {
|
|
157
|
+
// Ищем элемент с самой поздней датой (при равных — последний по позиции)
|
|
158
|
+
let latestItem = listItems[0].trim();
|
|
159
|
+
let latestDate = '';
|
|
160
|
+
for (const item of listItems) {
|
|
161
|
+
const dateMatch = item.match(/(\d{4}-\d{2}-\d{2})/);
|
|
162
|
+
if (dateMatch && dateMatch[1] >= latestDate) {
|
|
163
|
+
latestDate = dateMatch[1];
|
|
164
|
+
latestItem = item.trim();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const statusMatch = latestItem.match(/:\s*(passed|failed)\b/i);
|
|
168
|
+
if (statusMatch) return statusMatch[1].toLowerCase();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return null;
|
|
172
|
+
}
|