workflow-ai 1.0.59 → 1.0.61

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.
@@ -1,295 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * check-plan-templates.js — Проверяет шаблоны планов и создаёт планы по триггерам.
5
- *
6
- * Логика:
7
- * 1. Читает все .md из plans/templates/
8
- * 2. Для каждого с enabled: true — проверяет триггер
9
- * 3. Если триггер сработал — создаёт план в plans/current/ со статусом approved
10
- * 4. Обновляет last_triggered в шаблоне
11
- *
12
- * Типы триггеров:
13
- * - daily — раз в день
14
- * - weekly — в указанные дни недели (params.days_of_week: [0-6])
15
- * - date_after — однократно после указанной даты (params.date)
16
- * - interval_days — каждые N дней (params.days)
17
- *
18
- * Результат:
19
- * - plan_created + plan_ids — созданы планы
20
- * - no_triggers — ни один триггер не сработал
21
- * - error — ошибка
22
- *
23
- * Использование:
24
- * node check-plan-templates.js
25
- */
26
-
27
- import fs from 'fs';
28
- import path from 'path';
29
- import { findProjectRoot } from 'workflow-ai/lib/find-root.mjs';
30
- import { parseFrontmatter, serializeFrontmatter, printResult } from 'workflow-ai/lib/utils.mjs';
31
-
32
- const PROJECT_DIR = findProjectRoot();
33
- const WORKFLOW_DIR = path.join(PROJECT_DIR, '.workflow');
34
- const TEMPLATES_DIR = path.join(WORKFLOW_DIR, 'plans', 'templates');
35
- const PLANS_DIR = path.join(WORKFLOW_DIR, 'plans', 'current');
36
- const TICKETS_DIR = path.join(WORKFLOW_DIR, 'tickets');
37
- const DONE_DIR = 'done';
38
-
39
- /**
40
- * Проверяет, сработал ли триггер шаблона.
41
- *
42
- * @param {{ type: string, params?: object }} trigger — конфигурация триггера
43
- * @param {string} lastTriggered — ISO-дата последнего срабатывания (или пустая строка)
44
- * @param {Date} [now] — текущая дата (для тестов)
45
- * @returns {boolean}
46
- */
47
- export function evaluateTrigger(trigger, lastTriggered, now = new Date()) {
48
- if (!trigger || !trigger.type) return false;
49
-
50
- const todayStr = now.toISOString().slice(0, 10);
51
- const lastDate = lastTriggered ? String(lastTriggered).slice(0, 10) : null;
52
-
53
- switch (trigger.type) {
54
- case 'daily':
55
- return lastDate !== todayStr;
56
-
57
- case 'weekly': {
58
- const dayOfWeek = now.getDay();
59
- const targetDays = trigger.params?.days_of_week || [1];
60
- if (!targetDays.includes(dayOfWeek)) return false;
61
- return lastDate !== todayStr;
62
- }
63
-
64
- case 'date_after': {
65
- const targetDate = trigger.params?.date;
66
- if (!targetDate) return false;
67
- if (todayStr < targetDate) return false;
68
- return !lastDate || lastDate < targetDate;
69
- }
70
-
71
- case 'interval_days': {
72
- const intervalDays = trigger.params?.days || 1;
73
- if (!lastDate) return true;
74
- const lastTime = new Date(lastDate).getTime();
75
- const elapsed = (now.getTime() - lastTime) / (1000 * 60 * 60 * 24);
76
- return elapsed >= intervalDays;
77
- }
78
-
79
- default:
80
- console.error(`[WARN] Unknown trigger type: ${trigger.type}`);
81
- return false;
82
- }
83
- }
84
-
85
- /**
86
- * Генерирует следующий ID плана (PLAN-NNN), сканируя plans/current/.
87
- *
88
- * @param {string} plansDir — путь к директории plans/current/
89
- * @returns {string}
90
- */
91
- export function generateNextPlanId(plansDir) {
92
- if (!fs.existsSync(plansDir)) return 'PLAN-001';
93
-
94
- const files = fs.readdirSync(plansDir).filter(f => f.endsWith('.md'));
95
- let maxNum = 0;
96
-
97
- for (const file of files) {
98
- const match = file.match(/^PLAN-(\d+)\.md$/i);
99
- if (match) {
100
- const num = parseInt(match[1], 10);
101
- if (num > maxNum) maxNum = num;
102
- }
103
- }
104
-
105
- return `PLAN-${String(maxNum + 1).padStart(3, '0')}`;
106
- }
107
-
108
- /**
109
- * Создаёт план из шаблона.
110
- *
111
- * @param {string} templatePath — полный путь к файлу шаблона
112
- * @param {object} templateFm — frontmatter шаблона
113
- * @param {string} templateBody — тело шаблона
114
- * @param {string} planId — ID нового плана
115
- * @param {string} todayStr — сегодняшняя дата ISO
116
- * @returns {string} путь к созданному плану
117
- */
118
- function createPlanFromTemplate(templatePath, templateFm, templateBody, planId, todayStr) {
119
- const planFm = {
120
- id: planId,
121
- title: `${templateFm.title} (${todayStr})`,
122
- status: 'approved',
123
- source_template: templateFm.id,
124
- author: templateFm.author || 'system',
125
- created_at: todayStr,
126
- updated_at: todayStr,
127
- completed_at: '',
128
- previous_plan: '',
129
- related_reports: []
130
- };
131
-
132
- const planContent = serializeFrontmatter(planFm) + '\n' + templateBody;
133
- const planFileName = `${planId}.md`;
134
- const planPath = path.join(PLANS_DIR, planFileName);
135
-
136
- if (!fs.existsSync(PLANS_DIR)) {
137
- fs.mkdirSync(PLANS_DIR, { recursive: true });
138
- }
139
-
140
- fs.writeFileSync(planPath, planContent, 'utf8');
141
- console.log(`[INFO] Created plan ${planId} from template ${templateFm.id}`);
142
-
143
- return planPath;
144
- }
145
-
146
- /**
147
- * Обновляет last_triggered в шаблоне.
148
- *
149
- * @param {string} templatePath — полный путь к файлу шаблона
150
- * @param {object} frontmatter — frontmatter шаблона
151
- * @param {string} body — тело шаблона
152
- * @param {string} todayStr — сегодняшняя дата ISO
153
- */
154
- function updateTemplateLastTriggered(templatePath, frontmatter, body, todayStr) {
155
- frontmatter.last_triggered = todayStr;
156
- const content = serializeFrontmatter(frontmatter) + '\n' + body;
157
- fs.writeFileSync(templatePath, content, 'utf8');
158
- console.log(`[INFO] Updated last_triggered for ${frontmatter.id} to ${todayStr}`);
159
- }
160
-
161
- /**
162
- * Проверяет, есть ли незавершённые тикеты по планам, созданным из данного шаблона.
163
- * Тикет считается незавершённым, если он находится не в done/.
164
- *
165
- * @param {string} templateId — ID шаблона (например, "TMPL-001")
166
- * @returns {boolean} true если есть активные тикеты
167
- */
168
- export function hasActiveTicketsForTemplate(templateId) {
169
- // 1. Найти все планы с source_template === templateId
170
- const planPaths = [];
171
- if (fs.existsSync(PLANS_DIR)) {
172
- for (const file of fs.readdirSync(PLANS_DIR).filter(f => f.endsWith('.md'))) {
173
- const content = fs.readFileSync(path.join(PLANS_DIR, file), 'utf8');
174
- const { frontmatter } = parseFrontmatter(content);
175
- if (frontmatter.source_template === templateId) {
176
- planPaths.push(`plans/current/${file}`);
177
- }
178
- }
179
- }
180
-
181
- if (planPaths.length === 0) return false;
182
-
183
- // 2. Проверить тикеты во всех папках кроме done/
184
- if (!fs.existsSync(TICKETS_DIR)) return false;
185
-
186
- const ticketDirs = fs.readdirSync(TICKETS_DIR, { withFileTypes: true })
187
- .filter(d => d.isDirectory() && d.name !== DONE_DIR)
188
- .map(d => d.name);
189
-
190
- for (const dir of ticketDirs) {
191
- const dirPath = path.join(TICKETS_DIR, dir);
192
- const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.md'));
193
-
194
- for (const file of files) {
195
- const content = fs.readFileSync(path.join(dirPath, file), 'utf8');
196
- const { frontmatter } = parseFrontmatter(content);
197
- if (frontmatter.parent_plan && planPaths.includes(frontmatter.parent_plan)) {
198
- return true;
199
- }
200
- }
201
- }
202
-
203
- return false;
204
- }
205
-
206
- async function main() {
207
- if (!fs.existsSync(TEMPLATES_DIR)) {
208
- console.log('[INFO] Templates directory does not exist, nothing to check');
209
- printResult({ status: 'no_triggers' });
210
- return;
211
- }
212
-
213
- const templateFiles = fs.readdirSync(TEMPLATES_DIR)
214
- .filter(f => f.endsWith('.md'));
215
-
216
- if (templateFiles.length === 0) {
217
- console.log('[INFO] No template files found');
218
- printResult({ status: 'no_triggers' });
219
- return;
220
- }
221
-
222
- console.log(`[INFO] Found ${templateFiles.length} template(s) in plans/templates/`);
223
-
224
- const now = new Date();
225
- const todayStr = now.toISOString().slice(0, 10);
226
- const createdPlanIds = [];
227
-
228
- for (const file of templateFiles) {
229
- const templatePath = path.join(TEMPLATES_DIR, file);
230
-
231
- try {
232
- const content = fs.readFileSync(templatePath, 'utf8');
233
- const { frontmatter, body } = parseFrontmatter(content);
234
-
235
- if (frontmatter.type !== 'template') {
236
- console.log(`[INFO] Skipping ${file} — type is not "template"`);
237
- continue;
238
- }
239
-
240
- if (!frontmatter.enabled) {
241
- console.log(`[INFO] Skipping ${file} — disabled`);
242
- continue;
243
- }
244
-
245
- if (!frontmatter.trigger) {
246
- console.log(`[WARN] Skipping ${file} — no trigger defined`);
247
- continue;
248
- }
249
-
250
- const shouldTrigger = evaluateTrigger(frontmatter.trigger, frontmatter.last_triggered, now);
251
-
252
- if (!shouldTrigger) {
253
- console.log(`[INFO] Template ${frontmatter.id}: trigger not fired`);
254
- continue;
255
- }
256
-
257
- console.log(`[INFO] Template ${frontmatter.id}: trigger fired!`);
258
-
259
- if (hasActiveTicketsForTemplate(frontmatter.id)) {
260
- console.log(`[INFO] Template ${frontmatter.id}: skipped — active tickets exist from previous plan`);
261
- continue;
262
- }
263
-
264
- const planId = generateNextPlanId(PLANS_DIR);
265
- createPlanFromTemplate(templatePath, frontmatter, body, planId, todayStr);
266
- updateTemplateLastTriggered(templatePath, frontmatter, body, todayStr);
267
- createdPlanIds.push(planId);
268
-
269
- } catch (e) {
270
- console.error(`[WARN] Failed to process template ${file}: ${e.message}`);
271
- }
272
- }
273
-
274
- if (createdPlanIds.length > 0) {
275
- console.log(`[INFO] Created ${createdPlanIds.length} plan(s): ${createdPlanIds.join(', ')}`);
276
- printResult({ status: 'plan_created', plan_ids: createdPlanIds.join(', ') });
277
- } else {
278
- console.log('[INFO] No triggers fired');
279
- printResult({ status: 'no_triggers' });
280
- }
281
- }
282
-
283
- // Запуск main() только при прямом вызове (не при импорте)
284
- const isDirectRun = process.argv[1] && (
285
- process.argv[1].endsWith('check-plan-templates.js') ||
286
- process.argv[1].endsWith('check-plan-templates')
287
- );
288
-
289
- if (isDirectRun) {
290
- main().catch(e => {
291
- console.error(`[ERROR] ${e.message}`);
292
- printResult({ status: 'error', error: e.message });
293
- process.exit(1);
294
- });
295
- }
@@ -1,308 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * check-relevance.js - Скрипт проверки актуальности тикета
5
- *
6
- * Использование:
7
- * node check-relevance.js <path-to-ticket>
8
- *
9
- * Вывод:
10
- * ---RESULT---
11
- * verdict: relevant|irrelevant
12
- * reason: ...
13
- * ---RESULT---
14
- */
15
-
16
- import fs from "fs";
17
- import path from "path";
18
- import YAML from "../lib/js-yaml.mjs";
19
- import { findProjectRoot } from "../lib/find-root.mjs";
20
- import {
21
- parseFrontmatter,
22
- serializeFrontmatter,
23
- getLastReviewStatus,
24
- } from "../lib/utils.mjs";
25
-
26
- const PROJECT_DIR = findProjectRoot();
27
- const WORKFLOW_DIR = path.join(PROJECT_DIR, ".workflow");
28
- const TICKETS_DIR = path.join(WORKFLOW_DIR, "tickets");
29
-
30
- const VALID_STATUSES = [
31
- "backlog",
32
- "ready",
33
- "in-progress",
34
- "blocked",
35
- "review",
36
- "done",
37
- "archive",
38
- ];
39
-
40
- function getCurrentStatus(ticketPath) {
41
- const fileName = path.basename(ticketPath);
42
- for (const status of VALID_STATUSES) {
43
- const statusDir = path.join(TICKETS_DIR, status);
44
- const expectedPath = path.join(statusDir, fileName);
45
- if (ticketPath === expectedPath) {
46
- return status;
47
- }
48
- }
49
- return null;
50
- }
51
-
52
- function extractPlanId(parentPlan) {
53
- if (!parentPlan) return null;
54
- const basename = path.basename(parentPlan, ".md");
55
- const match = basename.match(/^PLAN-(\d+)$/i);
56
- if (match) {
57
- return `PLAN-${String(parseInt(match[1], 10)).padStart(3, "0")}`;
58
- }
59
- return basename;
60
- }
61
-
62
- function getDodCompletion(content) {
63
- const dodSectionMatch = content.match(/## Критерии готовности.*?\n([\s\S]*?)(?=\n## |\n# |\z)/i);
64
- if (!dodSectionMatch) return { completed: false, total: 0, checked: 0 };
65
-
66
- const section = dodSectionMatch[1];
67
- const checkedMatches = section.match(/\[x\]/gi) || [];
68
- const uncheckedMatches = section.match(/\[ \]/gi) || [];
69
-
70
- const total = checkedMatches.length + uncheckedMatches.length;
71
- const completed = total > 0 && uncheckedMatches.length === 0;
72
-
73
- return { completed, total, checked: checkedMatches.length };
74
- }
75
-
76
- function hasResultSection(content) {
77
- const resultPatterns = [
78
- /##\s*Result/gi,
79
- /##\s*Результат/gi,
80
- /##\s*Результат выполнения/gi,
81
- ];
82
- return resultPatterns.some((pattern) => pattern.test(content));
83
- }
84
-
85
- function getBlockedSection(content) {
86
- const blockedMatch = content.match(/##\s*Блокировки\s*\n([\s\S]*?)(?=\n## |\n# |\z)/i);
87
- return blockedMatch ? blockedMatch[1].trim() : "";
88
- }
89
-
90
- function findTicketInColumns(ticketId) {
91
- for (const status of VALID_STATUSES) {
92
- const statusDir = path.join(TICKETS_DIR, status);
93
- const ticketPath = path.join(statusDir, `${ticketId}.md`);
94
- if (fs.existsSync(ticketPath)) {
95
- return status;
96
- }
97
- }
98
- return null;
99
- }
100
-
101
- async function checkRelevance(ticketPath) {
102
- if (!fs.existsSync(ticketPath)) {
103
- return {
104
- verdict: "relevant",
105
- reason: "file_not_found",
106
- error: `Ticket file not found: ${ticketPath}`,
107
- };
108
- }
109
-
110
- let content;
111
- try {
112
- content = fs.readFileSync(ticketPath, "utf8");
113
- } catch (e) {
114
- return {
115
- verdict: "relevant",
116
- reason: "read_error",
117
- error: `Failed to read ticket: ${e.message}`,
118
- };
119
- }
120
-
121
- let frontmatter, body;
122
- try {
123
- ({ frontmatter, body } = parseFrontmatter(content));
124
- } catch (e) {
125
- return {
126
- verdict: "relevant",
127
- reason: "invalid_frontmatter",
128
- warning: `Failed to parse frontmatter: ${e.message}. Treating as relevant (fail-safe).`,
129
- };
130
- }
131
-
132
- const currentStatus = getCurrentStatus(ticketPath);
133
- const fullContent = body;
134
-
135
- const lastReview = getLastReviewStatus(fullContent);
136
- if (lastReview === "skipped") {
137
- return { verdict: "irrelevant", reason: "already_skipped" };
138
- }
139
- if (lastReview === "failed") {
140
- return { verdict: "relevant", reason: "review_failed_needs_rework" };
141
- }
142
-
143
- if (frontmatter.blocked === true || frontmatter.blocked === "true") {
144
- return { verdict: "relevant", reason: "blocked" };
145
- }
146
-
147
- const blockedSection = getBlockedSection(fullContent);
148
- const hasActiveBlockers = blockedSection.length > 0 && !blockedSection.includes("нет");
149
- if (hasActiveBlockers) {
150
- return { verdict: "relevant", reason: "blocked" };
151
- }
152
-
153
- const parentPlan = frontmatter.parent_plan;
154
- if (parentPlan) {
155
- const planId = extractPlanId(parentPlan);
156
- if (planId) {
157
- const planPath = path.join(WORKFLOW_DIR, "plans", "current", `${planId}.md`);
158
- if (fs.existsSync(planPath)) {
159
- try {
160
- const planContent = fs.readFileSync(planPath, "utf8");
161
- const { frontmatter: planFm } = parseFrontmatter(planContent);
162
- const planStatus = planFm.status;
163
- if (["completed", "archived", "cancelled"].includes(planStatus)) {
164
- return { verdict: "irrelevant", reason: "plan_inactive" };
165
- }
166
- } catch (e) {
167
- // fail-safe: treat as relevant
168
- }
169
- } else {
170
- // fail-safe: treat as relevant
171
- }
172
- }
173
- } else {
174
- // fail-safe: parent_plan is empty
175
- }
176
-
177
- const dod = getDodCompletion(fullContent);
178
- const hasResult = hasResultSection(fullContent);
179
-
180
- if (dod.completed && hasResult) {
181
- if (lastReview === "passed") {
182
- return { verdict: "irrelevant", reason: "dod_completed" };
183
- } else if (lastReview === null) {
184
- return { verdict: "relevant", reason: "needs_review" };
185
- }
186
- }
187
-
188
- const dependencies = frontmatter.dependencies || [];
189
- if (dependencies.length > 0) {
190
- for (const dep of dependencies) {
191
- const depStatus = findTicketInColumns(dep);
192
- if (depStatus === null) {
193
- return { verdict: "irrelevant", reason: "dependencies_inactive" };
194
- }
195
- if (depStatus === "blocked") {
196
- try {
197
- const blockedDir = path.join(TICKETS_DIR, "blocked", `${dep}.md`);
198
- const blockedContent = fs.readFileSync(blockedDir, "utf8");
199
- const { body: blockedBody } = parseFrontmatter(blockedContent);
200
- if (blockedBody.toLowerCase().includes("неактуально")) {
201
- return { verdict: "irrelevant", reason: "dependencies_inactive" };
202
- }
203
- } catch (e) {
204
- // ignore
205
- }
206
- }
207
- }
208
- }
209
-
210
- return { verdict: "relevant", reason: "all_checks_passed" };
211
- }
212
-
213
- function addSkippedReview(ticketPath, reason) {
214
- const now = new Date();
215
- const date = now.toISOString().slice(0, 10);
216
-
217
- let content;
218
- try {
219
- content = fs.readFileSync(ticketPath, "utf8");
220
- } catch (e) {
221
- throw new Error(`Failed to read ticket: ${e.message}`);
222
- }
223
-
224
- let { frontmatter, body } = parseFrontmatter(content);
225
-
226
- const reviewSectionMatch = body.match(/##\s*Ревью\s*\n([\s\S]*)/i);
227
- let newBody;
228
-
229
- if (reviewSectionMatch) {
230
- const reviewContent = reviewSectionMatch[1];
231
- const lines = reviewContent.split("\n");
232
- let insertIndex = 0;
233
- for (let i = 0; i < lines.length; i++) {
234
- if (lines[i].trim().startsWith("|") && lines[i].includes("---")) {
235
- insertIndex = i + 1;
236
- break;
237
- }
238
- }
239
-
240
- const newRow = `| ${date} | ⏭️ skipped | ${reason} |`;
241
- lines.splice(insertIndex, 0, newRow);
242
- newBody = body.slice(0, reviewSectionMatch.index) + lines.join("\n");
243
- } else {
244
- const reviewTable = `\n## Ревью\n\n| Дата | Статус | Самари |\n|------|--------|--------|\n| ${date} | ⏭️ skipped | ${reason} |\n`;
245
- newBody = body.trimEnd() + reviewTable;
246
- }
247
-
248
- const newContent = serializeFrontmatter(frontmatter) + newBody;
249
- fs.writeFileSync(ticketPath, newContent, "utf8");
250
- }
251
-
252
- async function main() {
253
- const args = process.argv.slice(2);
254
- let ticketPath;
255
-
256
- if (args.length === 0) {
257
- console.error("Usage: node check-relevance.js <path-to-ticket>");
258
- console.error("Example: node check-relevance.js .workflow/tickets/in-progress/IMPL-001.md");
259
- process.exit(1);
260
- } else if (args.length === 1) {
261
- const prompt = args[0];
262
- const ticketMatch = prompt.match(/ticket_id:\s*(\S+)/);
263
- if (ticketMatch) {
264
- const ticketId = ticketMatch[1];
265
- ticketPath = path.join(TICKETS_DIR, "in-progress", `${ticketId}.md`);
266
- } else {
267
- ticketPath = args[0];
268
- }
269
- } else {
270
- ticketPath = args[0];
271
- }
272
-
273
- if (!path.isAbsolute(ticketPath)) {
274
- ticketPath = path.resolve(process.cwd(), ticketPath);
275
- }
276
-
277
- const result = await checkRelevance(ticketPath);
278
-
279
- if (result.warning && !result.error) {
280
- console.error(`[WARNING] ${result.warning}`);
281
- }
282
-
283
- if (result.error) {
284
- console.error(`[ERROR] ${result.error}`);
285
- }
286
-
287
- if (result.verdict === "irrelevant") {
288
- try {
289
- addSkippedReview(ticketPath, result.reason);
290
- } catch (e) {
291
- console.error(`[ERROR] Failed to add review entry: ${e.message}`);
292
- }
293
- }
294
-
295
- console.log("---RESULT---");
296
- console.log(`verdict: ${result.verdict}`);
297
- console.log(`reason: ${result.reason}`);
298
- console.log("---RESULT---");
299
-
300
- if (result.error) {
301
- process.exit(1);
302
- }
303
- }
304
-
305
- main().catch((e) => {
306
- console.error("[FATAL]", e.message);
307
- process.exit(1);
308
- });
@@ -1,110 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * get-next-id.js - Универсальный генератор ID для тикетов, планов и других артефактов
5
- *
6
- * Использование:
7
- * node get-next-id.js --prefix TASK --dir tickets
8
- * node get-next-id.js --prefix PLAN --dir plans
9
- *
10
- * Вывод: следующий ID через ---RESULT---, например: TASK-007
11
- */
12
-
13
- import fs from "fs";
14
- import path from "path";
15
- import { findProjectRoot } from "workflow-ai/lib/find-root.mjs";
16
- import { printResult } from "workflow-ai/lib/utils.mjs";
17
-
18
- const PROJECT_DIR = findProjectRoot();
19
-
20
- function parseArgs() {
21
- const args = process.argv.slice(2);
22
- let prefix = null;
23
- let dir = null;
24
-
25
- for (let i = 0; i < args.length; i++) {
26
- if (args[i] === "--prefix" && i + 1 < args.length) {
27
- prefix = args[i + 1];
28
- i++;
29
- } else if (args[i] === "--dir" && i + 1 < args.length) {
30
- dir = args[i + 1];
31
- i++;
32
- }
33
- }
34
-
35
- return { prefix, dir };
36
- }
37
-
38
- function findMaxNumber(targetDir, prefix) {
39
- let maxNum = 0;
40
- const regex = new RegExp(`^${prefix}-(\\d{3})\\.md$`, "i");
41
-
42
- function scanDirectory(dir) {
43
- if (!fs.existsSync(dir)) {
44
- return;
45
- }
46
-
47
- const entries = fs.readdirSync(dir, { withFileTypes: true });
48
-
49
- for (const entry of entries) {
50
- const fullPath = path.join(dir, entry.name);
51
-
52
- if (entry.isDirectory()) {
53
- scanDirectory(fullPath);
54
- } else if (entry.isFile()) {
55
- const match = entry.name.match(regex);
56
- if (match) {
57
- const num = parseInt(match[1], 10);
58
- if (num > maxNum) {
59
- maxNum = num;
60
- }
61
- }
62
- }
63
- }
64
- }
65
-
66
- scanDirectory(targetDir);
67
- return maxNum;
68
- }
69
-
70
- function formatNumber(num) {
71
- return num.toString().padStart(3, "0");
72
- }
73
-
74
- async function main() {
75
- const { prefix, dir } = parseArgs();
76
-
77
- if (!prefix || !dir) {
78
- console.error("Usage: node get-next-id.js --prefix <PREFIX> --dir <DIRECTORY>");
79
- console.error("Example: node get-next-id.js --prefix TASK --dir tickets");
80
- printResult({
81
- status: "error",
82
- error: "Missing required arguments: --prefix and --dir",
83
- });
84
- process.exit(1);
85
- }
86
-
87
- let targetDir;
88
-
89
- if (dir === "tickets") {
90
- targetDir = path.join(PROJECT_DIR, ".workflow", "tickets");
91
- } else if (dir === "plans") {
92
- targetDir = path.join(PROJECT_DIR, ".workflow", "plans");
93
- } else {
94
- targetDir = path.join(PROJECT_DIR, dir);
95
- }
96
-
97
- if (!fs.existsSync(targetDir)) {
98
- const nextId = `${prefix}-001`;
99
- printResult({ status: "success", id: nextId });
100
- return;
101
- }
102
-
103
- const maxNum = findMaxNumber(targetDir, prefix);
104
- const nextNum = maxNum + 1;
105
- const nextId = `${prefix}-${formatNumber(nextNum)}`;
106
-
107
- printResult({ status: "success", id: nextId });
108
- }
109
-
110
- main();