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/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
+ }