workflow-ai 1.0.0
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/README.md +76 -0
- package/agent-templates/CLAUDE.md.tpl +42 -0
- package/agent-templates/QWEN.md.tpl +34 -0
- package/bin/workflow.mjs +3 -0
- package/configs/config.yaml +117 -0
- package/configs/pipeline.yaml +422 -0
- package/package.json +24 -0
- package/src/cli.mjs +131 -0
- package/src/init.mjs +441 -0
- package/src/lib/find-root.mjs +33 -0
- package/src/lib/utils.mjs +66 -0
- package/src/runner.mjs +1466 -0
- package/src/scripts/check-anomalies.js +161 -0
- package/src/scripts/move-ticket.js +228 -0
- package/src/scripts/move-to-ready.js +110 -0
- package/src/scripts/move-to-review.js +88 -0
- package/src/scripts/pick-next-task.js +345 -0
- package/src/skills/analyze-report/SKILL.md +110 -0
- package/src/skills/check-conditions/SKILL.md +140 -0
- package/src/skills/create-plan/SKILL.md +98 -0
- package/src/skills/create-report/SKILL.md +156 -0
- package/src/skills/decompose-gaps/SKILL.md +122 -0
- package/src/skills/decompose-plan/SKILL.md +109 -0
- package/src/skills/execute-task/SKILL.md +117 -0
- package/src/skills/review-result/SKILL.md +274 -0
- package/templates/plan-template.md +116 -0
- package/templates/report-template.md +178 -0
- package/templates/ticket-template.md +103 -0
package/src/init.mjs
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, copyFileSync, readFileSync, writeFileSync, appendFileSync, symlinkSync, statSync, readdirSync, unlinkSync } from 'node:fs';
|
|
2
|
+
import { join, resolve, dirname, basename } from 'node:path';
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Возвращает абсолютный путь к корню npm-пакета через import.meta.url.
|
|
8
|
+
*
|
|
9
|
+
* @returns {string} Абсолютный путь к корню пакета
|
|
10
|
+
*/
|
|
11
|
+
function getPackageRoot() {
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = dirname(__filename);
|
|
14
|
+
// result/src → result
|
|
15
|
+
return resolve(__dirname, '../');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Создаёт директорию если она не существует.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} dirPath - Путь к директории
|
|
22
|
+
*/
|
|
23
|
+
function ensureDir(dirPath) {
|
|
24
|
+
if (!existsSync(dirPath)) {
|
|
25
|
+
mkdirSync(dirPath, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Копирует файл из источника в назначение.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} src - Исходный путь
|
|
33
|
+
* @param {string} dest - Путь назначения
|
|
34
|
+
*/
|
|
35
|
+
function copyFile(src, dest) {
|
|
36
|
+
const destDir = dirname(dest);
|
|
37
|
+
ensureDir(destDir);
|
|
38
|
+
copyFileSync(src, dest);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Рекурсивно копирует директорию.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} srcDir - Исходная директория
|
|
45
|
+
* @param {string} destDir - Директория назначения
|
|
46
|
+
*/
|
|
47
|
+
function copyDirRecursive(srcDir, destDir) {
|
|
48
|
+
ensureDir(destDir);
|
|
49
|
+
|
|
50
|
+
const entries = [];
|
|
51
|
+
try {
|
|
52
|
+
const dirEntries = readdirSync(srcDir, { withFileTypes: true });
|
|
53
|
+
for (const entry of dirEntries) {
|
|
54
|
+
entries.push(entry);
|
|
55
|
+
}
|
|
56
|
+
} catch (e) {
|
|
57
|
+
// Directory doesn't exist, skip
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
const srcPath = join(srcDir, entry.name);
|
|
63
|
+
const destPath = join(destDir, entry.name);
|
|
64
|
+
|
|
65
|
+
if (entry.isDirectory()) {
|
|
66
|
+
copyDirRecursive(srcPath, destPath);
|
|
67
|
+
} else {
|
|
68
|
+
copyFile(srcPath, destPath);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Генерирует таблицу skills из директории .workflow/src/skills/.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} workflowRoot - Путь к корню .workflow
|
|
77
|
+
* @returns {string} Markdown-таблица с навыками
|
|
78
|
+
*/
|
|
79
|
+
function generateSkillsTable(workflowRoot) {
|
|
80
|
+
const skillsDir = join(workflowRoot, 'src', 'skills');
|
|
81
|
+
|
|
82
|
+
if (!existsSync(skillsDir)) {
|
|
83
|
+
return '| Задача | Инструкция |\n|--------|------------|\n';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const skillsMap = {
|
|
87
|
+
'create-plan': 'Создание плана',
|
|
88
|
+
'analyze-report': 'Анализ отчёта',
|
|
89
|
+
'decompose-plan': 'Декомпозиция плана',
|
|
90
|
+
'check-conditions': 'Проверка готовности',
|
|
91
|
+
'create-report': 'Создание отчёта',
|
|
92
|
+
'execute-task': 'Выполнение задачи',
|
|
93
|
+
'move-ticket': 'Перемещение тикета',
|
|
94
|
+
'pick-next-task': 'Выбор следующей задачи',
|
|
95
|
+
'decompose-gaps': 'Декомпозиция пробелов',
|
|
96
|
+
'review-result': 'Ревью результата'
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
let table = '| Задача | Инструкция |\n|--------|------------|\n';
|
|
100
|
+
|
|
101
|
+
const skillDirs = readdirSync(skillsDir, { withFileTypes: true })
|
|
102
|
+
.filter(entry => entry.isDirectory())
|
|
103
|
+
.map(entry => entry.name);
|
|
104
|
+
|
|
105
|
+
for (const skillDir of skillDirs) {
|
|
106
|
+
const description = skillsMap[skillDir] || skillDir;
|
|
107
|
+
const instruction = `.workflow/src/skills/${skillDir}/SKILL.md`;
|
|
108
|
+
table += `| ${description} | \`${instruction}\` |\n`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return table;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Генерирует CLAUDE.md из шаблона.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} workflowRoot - Путь к корню .workflow
|
|
118
|
+
* @param {string} projectRoot - Путь к корню проекта
|
|
119
|
+
*/
|
|
120
|
+
function generateClaudeMd(workflowRoot, projectRoot) {
|
|
121
|
+
const templatePath = join(workflowRoot, 'templates', 'agent-templates', 'CLAUDE.md.tpl');
|
|
122
|
+
const destPath = join(projectRoot, 'CLAUDE.md');
|
|
123
|
+
|
|
124
|
+
let content;
|
|
125
|
+
if (existsSync(templatePath)) {
|
|
126
|
+
content = readFileSync(templatePath, 'utf-8');
|
|
127
|
+
} else {
|
|
128
|
+
// Default template
|
|
129
|
+
content = `# Инструкции для Claude Code
|
|
130
|
+
|
|
131
|
+
Этот проект использует систему координации AI-агентов через файловую канбан-доску.
|
|
132
|
+
|
|
133
|
+
## Структура проекта
|
|
134
|
+
|
|
135
|
+
- \`.workflow/\` — канбан-доска с тикетами
|
|
136
|
+
- \`.workflow/src/skills/\` — инструкции для выполнения задач
|
|
137
|
+
|
|
138
|
+
## Доступные Skills
|
|
139
|
+
|
|
140
|
+
{{SKILLS_TABLE}}
|
|
141
|
+
|
|
142
|
+
## Workflow
|
|
143
|
+
|
|
144
|
+
1. **Планирование**: Создай план в \`.workflow/plans/current/\`
|
|
145
|
+
2. **Декомпозиция**: Разбей план на тикеты в \`.workflow/tickets/backlog/\`
|
|
146
|
+
3. **Выполнение**: Бери задачи из \`ready/\`, выполняй, перемещай в \`done/\`
|
|
147
|
+
4. **Отчётность**: Создавай отчёты в \`.workflow/reports/\`
|
|
148
|
+
|
|
149
|
+
## Шаблоны
|
|
150
|
+
|
|
151
|
+
- \`.workflow/templates/ticket-template.md\` — шаблон тикета
|
|
152
|
+
- \`.workflow/templates/plan-template.md\` — шаблон плана
|
|
153
|
+
- \`.workflow/templates/report-template.md\` — шаблон отчёта
|
|
154
|
+
|
|
155
|
+
## Конфигурация
|
|
156
|
+
|
|
157
|
+
Настройки в \`.workflow/config/config.yaml\`
|
|
158
|
+
|
|
159
|
+
## Правила написания кода
|
|
160
|
+
При написании кода использовать методологии TDD, SOLID, DRY
|
|
161
|
+
`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const skillsTable = generateSkillsTable(workflowRoot);
|
|
165
|
+
content = content.replace('{{SKILLS_TABLE}}', skillsTable);
|
|
166
|
+
|
|
167
|
+
writeFileSync(destPath, content, 'utf-8');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Генерирует QWEN.md из шаблона.
|
|
172
|
+
*
|
|
173
|
+
* @param {string} workflowRoot - Путь к корню .workflow
|
|
174
|
+
* @param {string} projectRoot - Путь к корню проекта
|
|
175
|
+
*/
|
|
176
|
+
function generateQwenMd(workflowRoot, projectRoot) {
|
|
177
|
+
const templatePath = join(workflowRoot, 'templates', 'agent-templates', 'QWEN.md.tpl');
|
|
178
|
+
const destPath = join(projectRoot, 'QWEN.md');
|
|
179
|
+
|
|
180
|
+
let content;
|
|
181
|
+
if (existsSync(templatePath)) {
|
|
182
|
+
content = readFileSync(templatePath, 'utf-8');
|
|
183
|
+
} else {
|
|
184
|
+
// Default template
|
|
185
|
+
content = `# Инструкции для qwen Code
|
|
186
|
+
|
|
187
|
+
Этот проект использует систему координации AI-агентов через файловую канбан-доску.
|
|
188
|
+
|
|
189
|
+
## Структура проекта
|
|
190
|
+
|
|
191
|
+
- \`.workflow/\` — канбан-доска с тикетами
|
|
192
|
+
- \`.workflow/src/skills/\` — инструкции для выполнения задач
|
|
193
|
+
|
|
194
|
+
## Доступные Skills
|
|
195
|
+
|
|
196
|
+
{{SKILLS_TABLE}}
|
|
197
|
+
|
|
198
|
+
## Workflow
|
|
199
|
+
|
|
200
|
+
1. **Планирование**: Создай план в \`.workflow/plans/current/\`
|
|
201
|
+
2. **Декомпозиция**: Разбей план на тикеты в \`.workflow/tickets/backlog/\`
|
|
202
|
+
3. **Выполнение**: Бери задачи из \`ready/\`, выполняй, перемещай в \`done/\`
|
|
203
|
+
4. **Отчётность**: Создавай отчёты в \`.workflow/reports/\`
|
|
204
|
+
|
|
205
|
+
## Шаблоны
|
|
206
|
+
|
|
207
|
+
- \`.workflow/templates/ticket-template.md\` — шаблон тикета
|
|
208
|
+
- \`.workflow/templates/plan-template.md\` — шаблон плана
|
|
209
|
+
- \`.workflow/templates/report-template.md\` — шаблон отчёта
|
|
210
|
+
|
|
211
|
+
## Конфигурация
|
|
212
|
+
|
|
213
|
+
Настройки в \`.workflow/config/config.yaml\`
|
|
214
|
+
|
|
215
|
+
## Правила написания кода
|
|
216
|
+
При написании кода использовать методологии TDD, SOLID, DRY
|
|
217
|
+
`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const skillsTable = generateSkillsTable(workflowRoot);
|
|
221
|
+
content = content.replace('{{SKILLS_TABLE}}', skillsTable);
|
|
222
|
+
|
|
223
|
+
writeFileSync(destPath, content, 'utf-8');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Обновляет .gitignore, добавляя указанные строки.
|
|
228
|
+
*
|
|
229
|
+
* @param {string} projectRoot - Путь к корню проекта
|
|
230
|
+
*/
|
|
231
|
+
function updateGitignore(projectRoot) {
|
|
232
|
+
const gitignorePath = join(projectRoot, '.gitignore');
|
|
233
|
+
const linesToAdd = ['.workflow/logs/'];
|
|
234
|
+
|
|
235
|
+
let currentContent = '';
|
|
236
|
+
if (existsSync(gitignorePath)) {
|
|
237
|
+
currentContent = readFileSync(gitignorePath, 'utf-8');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const existingLines = currentContent.split('\n').map(line => line.trim());
|
|
241
|
+
|
|
242
|
+
for (const line of linesToAdd) {
|
|
243
|
+
if (!existingLines.includes(line)) {
|
|
244
|
+
appendFileSync(gitignorePath, line + '\n');
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Создаёт симлинки .kilocode.
|
|
251
|
+
*
|
|
252
|
+
* @param {string} projectRoot - Путь к корню проекта
|
|
253
|
+
* @param {boolean} force - Принудительное создание
|
|
254
|
+
* @returns {{ success: boolean, warning?: string }} Результат операции
|
|
255
|
+
*/
|
|
256
|
+
function createKilocodeSymlinks(projectRoot, force = false) {
|
|
257
|
+
const kilocodeDir = join(projectRoot, '.kilocode');
|
|
258
|
+
const skillsTarget = join(projectRoot, '.workflow', 'src', 'skills');
|
|
259
|
+
const skillsLink = join(kilocodeDir, 'skills');
|
|
260
|
+
|
|
261
|
+
ensureDir(kilocodeDir);
|
|
262
|
+
|
|
263
|
+
const isWindows = process.platform === 'win32';
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
// Remove existing link if exists
|
|
267
|
+
if (existsSync(skillsLink)) {
|
|
268
|
+
const stats = statSync(skillsLink);
|
|
269
|
+
if (stats.isSymbolicLink() || stats.isDirectory()) {
|
|
270
|
+
try {
|
|
271
|
+
if (isWindows) {
|
|
272
|
+
execSync(`rmdir "${skillsLink}"`);
|
|
273
|
+
} else {
|
|
274
|
+
unlinkSync(skillsLink);
|
|
275
|
+
}
|
|
276
|
+
} catch (e) {
|
|
277
|
+
// Ignore errors
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (isWindows) {
|
|
283
|
+
// Windows: use Junction Point
|
|
284
|
+
try {
|
|
285
|
+
execSync(`mklink /J "${skillsLink}" "${skillsTarget}"`);
|
|
286
|
+
return { success: true };
|
|
287
|
+
} catch (e) {
|
|
288
|
+
// Fallback: copy directory
|
|
289
|
+
copyDirRecursive(skillsTarget, skillsLink);
|
|
290
|
+
return {
|
|
291
|
+
success: true,
|
|
292
|
+
warning: 'Junction Point creation failed, copied files instead'
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
} else {
|
|
296
|
+
// Linux/macOS: use symlink
|
|
297
|
+
symlinkSync(skillsTarget, skillsLink);
|
|
298
|
+
return { success: true };
|
|
299
|
+
}
|
|
300
|
+
} catch (e) {
|
|
301
|
+
return {
|
|
302
|
+
success: false,
|
|
303
|
+
warning: `Failed to create symlink: ${e.message}`
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Инициализирует проект, создавая структуру .workflow/ и копируя файлы.
|
|
310
|
+
*
|
|
311
|
+
* @param {string} targetPath - Путь к целевому проекту (по умолчанию process.cwd())
|
|
312
|
+
* @param {object} options - Опции инициализации
|
|
313
|
+
* @param {boolean} options.force - Принудительная перезапись файлов
|
|
314
|
+
* @returns {object} Результат инициализации
|
|
315
|
+
*/
|
|
316
|
+
export function initProject(targetPath = process.cwd(), options = {}) {
|
|
317
|
+
const { force = false } = options;
|
|
318
|
+
const projectRoot = resolve(targetPath);
|
|
319
|
+
const workflowRoot = join(projectRoot, '.workflow');
|
|
320
|
+
const packageRoot = getPackageRoot();
|
|
321
|
+
|
|
322
|
+
const result = {
|
|
323
|
+
steps: [],
|
|
324
|
+
warnings: [],
|
|
325
|
+
errors: []
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// Step 1: Create .workflow/ structure (17 directories)
|
|
329
|
+
const directories = [
|
|
330
|
+
'tickets/backlog',
|
|
331
|
+
'tickets/ready',
|
|
332
|
+
'tickets/in-progress',
|
|
333
|
+
'tickets/blocked',
|
|
334
|
+
'tickets/review',
|
|
335
|
+
'tickets/done',
|
|
336
|
+
'plans/current',
|
|
337
|
+
'plans/archive',
|
|
338
|
+
'reports',
|
|
339
|
+
'logs',
|
|
340
|
+
'templates',
|
|
341
|
+
'config',
|
|
342
|
+
'src/skills',
|
|
343
|
+
'src/scripts',
|
|
344
|
+
'src/lib'
|
|
345
|
+
];
|
|
346
|
+
|
|
347
|
+
for (const dir of directories) {
|
|
348
|
+
ensureDir(join(workflowRoot, dir));
|
|
349
|
+
}
|
|
350
|
+
result.steps.push('Created .workflow/ directory structure (17 directories)');
|
|
351
|
+
|
|
352
|
+
// Step 2: Copy skills recursively
|
|
353
|
+
const srcSkillsSrc = join(packageRoot, 'src', 'skills');
|
|
354
|
+
const srcSkillsDest = join(workflowRoot, 'src', 'skills');
|
|
355
|
+
copyDirRecursive(srcSkillsSrc, srcSkillsDest);
|
|
356
|
+
result.steps.push('Copied skills from getPackageRoot()/src/skills/ → .workflow/src/skills/');
|
|
357
|
+
|
|
358
|
+
// Step 3: Copy scripts
|
|
359
|
+
const srcScriptsSrc = join(packageRoot, 'src', 'scripts');
|
|
360
|
+
const srcScriptsDest = join(workflowRoot, 'src', 'scripts');
|
|
361
|
+
copyDirRecursive(srcScriptsSrc, srcScriptsDest);
|
|
362
|
+
result.steps.push('Copied scripts from getPackageRoot()/src/scripts/ → .workflow/src/scripts/');
|
|
363
|
+
|
|
364
|
+
// Step 4: Copy lib (utils.mjs + find-root.mjs)
|
|
365
|
+
const libSrc = join(packageRoot, 'src', 'lib');
|
|
366
|
+
const libDest = join(workflowRoot, 'src', 'lib');
|
|
367
|
+
ensureDir(libDest);
|
|
368
|
+
|
|
369
|
+
const utilsSrc = join(libSrc, 'utils.mjs');
|
|
370
|
+
const findRootSrc = join(libSrc, 'find-root.mjs');
|
|
371
|
+
|
|
372
|
+
if (existsSync(utilsSrc)) {
|
|
373
|
+
copyFile(utilsSrc, join(libDest, 'utils.mjs'));
|
|
374
|
+
}
|
|
375
|
+
if (existsSync(findRootSrc)) {
|
|
376
|
+
copyFile(findRootSrc, join(libDest, 'find-root.mjs'));
|
|
377
|
+
}
|
|
378
|
+
result.steps.push('Copied lib files (utils.mjs, find-root.mjs) → .workflow/src/lib/');
|
|
379
|
+
|
|
380
|
+
// Step 5: Copy templates (3 templates)
|
|
381
|
+
const templatesSrc = join(packageRoot, 'templates');
|
|
382
|
+
const templatesDest = join(workflowRoot, 'templates');
|
|
383
|
+
ensureDir(templatesDest);
|
|
384
|
+
|
|
385
|
+
const templateFiles = ['ticket-template.md', 'plan-template.md', 'report-template.md'];
|
|
386
|
+
for (const template of templateFiles) {
|
|
387
|
+
const srcPath = join(templatesSrc, template);
|
|
388
|
+
const destPath = join(templatesDest, template);
|
|
389
|
+
if (existsSync(srcPath)) {
|
|
390
|
+
copyFile(srcPath, destPath);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
result.steps.push('Copied 3 templates → .workflow/templates/');
|
|
394
|
+
|
|
395
|
+
// Step 6: Generate config files
|
|
396
|
+
const configDest = join(workflowRoot, 'config');
|
|
397
|
+
ensureDir(configDest);
|
|
398
|
+
|
|
399
|
+
// config.yaml — only if not exists (fresh only)
|
|
400
|
+
const configYamlDest = join(configDest, 'config.yaml');
|
|
401
|
+
if (!existsSync(configYamlDest) || force) {
|
|
402
|
+
const configYamlSrc = join(packageRoot, 'configs', 'config.yaml');
|
|
403
|
+
if (existsSync(configYamlSrc)) {
|
|
404
|
+
copyFile(configYamlSrc, configYamlDest);
|
|
405
|
+
result.steps.push('Generated config.yaml (fresh)');
|
|
406
|
+
}
|
|
407
|
+
} else {
|
|
408
|
+
result.warnings.push('config.yaml already exists, skipped (use --force to overwrite)');
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// pipeline.yaml — always
|
|
412
|
+
const pipelineSrc = join(packageRoot, 'configs', 'pipeline.yaml');
|
|
413
|
+
if (existsSync(pipelineSrc)) {
|
|
414
|
+
copyFile(pipelineSrc, join(configDest, 'pipeline.yaml'));
|
|
415
|
+
result.steps.push('Generated pipeline.yaml (overwritten)');
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Step 7: Create .kilocode symlinks
|
|
419
|
+
const symlinkResult = createKilocodeSymlinks(projectRoot, force);
|
|
420
|
+
if (symlinkResult.success) {
|
|
421
|
+
result.steps.push('Created .kilocode symlinks (Junction Point on Windows)');
|
|
422
|
+
if (symlinkResult.warning) {
|
|
423
|
+
result.warnings.push(symlinkResult.warning);
|
|
424
|
+
}
|
|
425
|
+
} else {
|
|
426
|
+
result.errors.push(symlinkResult.warning || 'Failed to create .kilocode symlinks');
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Step 8: Generate CLAUDE.md and QWEN.md
|
|
430
|
+
generateClaudeMd(workflowRoot, projectRoot);
|
|
431
|
+
generateQwenMd(workflowRoot, projectRoot);
|
|
432
|
+
result.steps.push('Generated CLAUDE.md and QWEN.md from agent-templates');
|
|
433
|
+
|
|
434
|
+
// Step 9: Update .gitignore
|
|
435
|
+
updateGitignore(projectRoot);
|
|
436
|
+
result.steps.push('Updated .gitignore with .workflow/logs/');
|
|
437
|
+
|
|
438
|
+
return result;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export default initProject;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { resolve, dirname } from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Finds the project root by searching for `.workflow/` directory
|
|
6
|
+
* walking up from the given start directory.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} [startDir=process.cwd()] - Starting directory path
|
|
9
|
+
* @returns {string} Absolute path to project root
|
|
10
|
+
* @throws {Error} If `.workflow/` directory is not found within 20 levels
|
|
11
|
+
*/
|
|
12
|
+
export function findProjectRoot(startDir = process.cwd()) {
|
|
13
|
+
let current = resolve(startDir);
|
|
14
|
+
let iterations = 0;
|
|
15
|
+
const MAX_DEPTH = 20;
|
|
16
|
+
|
|
17
|
+
while (iterations < MAX_DEPTH) {
|
|
18
|
+
if (existsSync(resolve(current, '.workflow'))) {
|
|
19
|
+
return current;
|
|
20
|
+
}
|
|
21
|
+
const parent = dirname(current);
|
|
22
|
+
if (parent === current) {
|
|
23
|
+
// Reached filesystem root
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
current = parent;
|
|
27
|
+
iterations++;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Could not find .workflow/ directory. Run "workflow init" first.\nStarted from: ${startDir}`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import YAML from 'js-yaml';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Парсит YAML frontmatter из markdown-файла.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} content - Содержимое markdown-файла
|
|
9
|
+
* @returns {{ frontmatter: object, body: string }} Объект с frontmatter и телом документа
|
|
10
|
+
*/
|
|
11
|
+
export function parseFrontmatter(content) {
|
|
12
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)/);
|
|
13
|
+
if (!match) {
|
|
14
|
+
return { frontmatter: {}, body: content };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const frontmatterStr = match[1];
|
|
18
|
+
const body = match[2];
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const frontmatter = YAML.load(frontmatterStr);
|
|
22
|
+
return { frontmatter, body };
|
|
23
|
+
} catch (e) {
|
|
24
|
+
throw new Error(`Failed to parse frontmatter: ${e.message}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Сериализует объект frontmatter обратно в YAML-строку.
|
|
30
|
+
*
|
|
31
|
+
* @param {object} frontmatter - Объект frontmatter
|
|
32
|
+
* @returns {string} YAML-строка с обрамлением ---
|
|
33
|
+
*/
|
|
34
|
+
export function serializeFrontmatter(frontmatter) {
|
|
35
|
+
const yamlStr = YAML.dump(frontmatter, {
|
|
36
|
+
lineWidth: -1, // Не переносить длинные строки
|
|
37
|
+
quotingType: '"',
|
|
38
|
+
forceQuotes: false
|
|
39
|
+
});
|
|
40
|
+
return `---\n${yamlStr}---\n`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Форматирует и выводит объект результата в stdout.
|
|
45
|
+
*
|
|
46
|
+
* @param {object} result - Объект результата для вывода
|
|
47
|
+
*/
|
|
48
|
+
export function printResult(result) {
|
|
49
|
+
console.log('---RESULT---');
|
|
50
|
+
for (const [key, value] of Object.entries(result)) {
|
|
51
|
+
console.log(`${key}: ${value}`);
|
|
52
|
+
}
|
|
53
|
+
console.log('---RESULT---');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Возвращает абсолютный путь к корню npm-пакета через import.meta.url.
|
|
58
|
+
*
|
|
59
|
+
* @returns {string} Абсолютный путь к корню пакета
|
|
60
|
+
*/
|
|
61
|
+
export function getPackageRoot() {
|
|
62
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
63
|
+
const __dirname = path.dirname(__filename);
|
|
64
|
+
// result/src/lib → result
|
|
65
|
+
return path.resolve(__dirname, '../../');
|
|
66
|
+
}
|