@relipa/ai-flow-kit 0.1.2 → 0.1.3

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.
Files changed (41) hide show
  1. package/README.md +26 -0
  2. package/bin/aiflow.js +40 -1
  3. package/custom/harness/playwright/.env.example +10 -0
  4. package/custom/harness/playwright/playwright.config.ts +34 -0
  5. package/custom/harness/playwright/tests/e2e/auth.setup.ts +25 -0
  6. package/custom/harness/playwright/tests/e2e/fixtures/test.ts +6 -0
  7. package/custom/harness/playwright/tests/e2e/pages/BasePage.ts +9 -0
  8. package/custom/harness/playwright/tests/e2e/support/auth.ts +5 -0
  9. package/custom/mcp-presets/playwright.json +8 -0
  10. package/custom/rules/test-patterns.md +70 -0
  11. package/custom/skills/automation-testing/SKILL.md +239 -0
  12. package/custom/skills/automation-testing/templates/BasePage.ts +29 -0
  13. package/custom/skills/automation-testing/templates/PageObject.example.ts +29 -0
  14. package/custom/skills/automation-testing/templates/playwright.config.ts +39 -0
  15. package/custom/skills/automation-testing/templates/spec.example.ts +29 -0
  16. package/custom/skills/coverage-check/SKILL.md +202 -0
  17. package/custom/skills/evidence-aggregation/SKILL.md +246 -0
  18. package/custom/skills/execute-flow/SKILL.md +479 -0
  19. package/custom/skills/execute-flow/templates/playwright.config.ts +39 -0
  20. package/custom/skills/generate-test-report/SKILL.md +99 -0
  21. package/custom/skills/generate-test-report/templates/test-report.md +58 -0
  22. package/custom/skills/generate-testcase/SKILL.md +303 -0
  23. package/custom/skills/generate-testcase/templates/testcase.md +88 -0
  24. package/custom/skills/log-bug/SKILL.md +131 -0
  25. package/custom/skills/pr-impact-analysis/SKILL.md +180 -0
  26. package/custom/skills/retest-orchestration/SKILL.md +191 -0
  27. package/custom/skills/script-sync/SKILL.md +208 -0
  28. package/custom/skills/test-analysis/SKILL.md +262 -0
  29. package/custom/templates/shared/gate-workflow.md +306 -2
  30. package/docs/common/CHANGELOG.md +52 -0
  31. package/docs/common/QUICK_START.md +30 -0
  32. package/docs/common/cli-reference.md +48 -7
  33. package/package.json +1 -1
  34. package/scripts/create-score-excel.js +20 -7
  35. package/scripts/detect.js +10 -0
  36. package/scripts/guide.js +9 -0
  37. package/scripts/prompt.js +36 -0
  38. package/scripts/scaffold-playwright.js +106 -0
  39. package/scripts/task.js +21 -7
  40. package/scripts/update.js +124 -124
  41. package/scripts/use.js +30 -12
@@ -17,13 +17,13 @@ ak <alias> --help # Also works with aliases
17
17
  | Full command | Short | Full command | Short |
18
18
  |---|---|---|---|
19
19
  | `ak init` | `ak i` | `ak use` | `ak u` |
20
- | `ak prompt` | `ak p` | `ak detect` | `ak d` |
21
- | `ak task` | `ak t` | `ak context` | `ak ctx` |
22
- | `ak checkpoint` | `ak cp` | `ak validate` | `ak vl` |
23
- | `ak memory` | `ak mem` | `ak guide` | `ak g` |
24
- | `ak remove` | `ak rm` | `ak update` | `ak up` |
25
- | `ak sync-skills` | `ak sync` | `ak doctor` | `ak dr` |
26
- | `ak telemetry` | `ak tel` | | |
20
+ | `ak execute` | `ak ex` | `ak prompt` | `ak p` |
21
+ | `ak detect` | `ak d` | `ak task` | `ak t` |
22
+ | `ak context` | `ak ctx` | `ak checkpoint` | `ak cp` |
23
+ | `ak validate` | `ak vl` | `ak memory` | `ak mem` |
24
+ | `ak guide` | `ak g` | `ak remove` | `ak rm` |
25
+ | `ak update` | `ak up` | `ak sync-skills` | `ak sync` |
26
+ | `ak doctor` | `ak dr` | `ak telemetry` | `ak tel` |
27
27
 
28
28
  | `task` sub-command | Short | `memory` sub-command | Short |
29
29
  |---|---|---|---|
@@ -156,6 +156,47 @@ $ aiflow use PROJ-33 PROJ-10 docs/arch.md
156
156
  ✓ Auto-resolved 2 link(s) from description
157
157
  ```
158
158
 
159
+ ## execute (alias: `ex`)
160
+
161
+ Execute test cases via the 4-gate Executing Flow. Syncs Playwright scripts, runs them, collects evidence, and generates a test report.
162
+
163
+ ```bash
164
+ ak execute PROJ-44 # from ticket (taskType must be "execute")
165
+ ak execute ./testcases/AD10.md # from TC file
166
+ ak execute # manual — AI asks for TC file
167
+ ```
168
+
169
+ **Arguments:**
170
+ - `[target]` — Optional. A ticket ID (`PROJ-44`), a path to a TC file (`./testcases/AD10.md`), or omitted for manual entry.
171
+
172
+ **What it does (4 gates):**
173
+ 1. **Gate 1 — Pre-flight:** Parse TC file, verify `ak-test/{repo}/` and `playwright.config.ts` exist, check `BASE_URL`
174
+ 2. **Gate 2 — Script Sync:** Hash-based sync — gen new TCs, update changed, skip unchanged. Uses Playwright MCP for real selectors (never fabricates).
175
+ 3. **Gate 3 — Execute & Evidence:** Run Playwright, organize evidence into `run-{N}/`, update TC file R1/R2 columns (`✅ Pass` · `❌ Fail` · `⏭️ Untest` · `⏳ Pending`)
176
+ 4. **Gate 4 — Report & Bug Logging:** Generate `testreport.md`, confirm each bug with TESTER before logging to Jira
177
+
178
+ **Output structure:**
179
+ ```
180
+ ak-test/{repo}/
181
+ ├── scripts/{screenId}/{ScreenID}.spec.ts
182
+ └── results/{screenId}/run-{N}/
183
+ ├── {TC_ID}-{scenario}/ (screenshots, trace.zip, result.md)
184
+ ├── bugs/BUG-NNN-{slug}.md
185
+ └── testreport.md
186
+ ```
187
+
188
+ **Example:**
189
+ ```bash
190
+ $ ak execute ./testcases/AD10.md
191
+ ⏸️ GATE 1: EXECUTE FLOW READY
192
+ TC file: testcases/AD10.md
193
+ Repo: repo-fe | Screen: AD10 — Create Product
194
+ BASE_URL: http://localhost:3000
195
+ → Type APPROVED to sync scripts (Gate 2)
196
+ ```
197
+
198
+ ---
199
+
159
200
  ## fetch-links
160
201
 
161
202
  Fetch a single Backlog/Jira URL and print a `SupplementaryContext` JSON object to stdout. Intended for AI runtime use inside the `read-study-requirement` skill — when AI encounters a link in the ticket description that wasn't already resolved by `ak use`, it can call this command and integrate the JSON output.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@relipa/ai-flow-kit",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "All-in-one AI Flow Kit for team development with Claude AI - skills, templates, and MCP adapters",
5
5
  "author": "Example Team",
6
6
  "publishConfig": {
@@ -193,9 +193,9 @@ function buildRow(email, u) {
193
193
  }
194
194
 
195
195
  // ── Excel writer ───────────────────────────────────────────────────────────
196
- async function writeExcel(rows, outputPath) {
196
+ async function writeExcel(rows, outputPath, sheetLabel, fromLabel, toLabel) {
197
197
  const workbook = new ExcelJS.Workbook();
198
- const sheet = workbook.addWorksheet('Score 18-24.05.2026');
198
+ const sheet = workbook.addWorksheet(`Score ${sheetLabel}`);
199
199
 
200
200
  sheet.columns = [
201
201
  { key: 'no', width: 5 },
@@ -220,7 +220,7 @@ async function writeExcel(rows, outputPath) {
220
220
  };
221
221
 
222
222
  // Title row
223
- sheet.addRow(['AI Flow Kit — Weekly Score Report: 18/05/202624/05/2026']);
223
+ sheet.addRow([`AI Flow Kit — Weekly Score Report: ${fromLabel}${toLabel}`]);
224
224
  sheet.mergeCells('A1:N1');
225
225
  const title = sheet.getCell('A1');
226
226
  title.font = { bold: true, size: 13, color: { argb: 'FF1F3864' } };
@@ -296,7 +296,7 @@ const CANONICAL_USERS = [
296
296
  'nguyenlt@relipasoft.com',
297
297
  'thuongvv@relipasoft.com',
298
298
  'yenvtb@relipasoft.com',
299
- 'tructth@relipasoft.com',
299
+ 'tructh@relipasoft.com',
300
300
  'thainq@relipasoft.com',
301
301
  'tuoittx@relipasoft.com',
302
302
  'hungdv@relipasoft.com',
@@ -323,8 +323,21 @@ const EMPTY_USER = (email) => ({
323
323
 
324
324
  // ── Main ───────────────────────────────────────────────────────────────────
325
325
  (async () => {
326
- const logFile = path.join(__dirname, '..', 'tellog', 'log_18-05-2026_24-05-2026');
327
- const outputFile = path.join(__dirname, '..', 'tellog', 'log_18-05-2026_24-05-2026_new.xlsx');
326
+ // Usage: node create-score-excel.js [log-file-stem]
327
+ // e.g.: node create-score-excel.js log_08-06-2026_14-06-2026
328
+ const stem = process.argv[2] || 'log_18-05-2026_24-05-2026';
329
+
330
+ // Derive display dates from stem: log_DD-MM-YYYY_DD-MM-YYYY → DD/MM/YYYY – DD/MM/YYYY
331
+ const dateMatch = stem.match(/log_(\d{2}-\d{2}-\d{4})_(\d{2}-\d{2}-\d{4})/);
332
+ const [fromLabel, toLabel] = dateMatch
333
+ ? [dateMatch[1].replace(/-/g, '/'), dateMatch[2].replace(/-/g, '/')]
334
+ : ['?', '?'];
335
+ const sheetLabel = dateMatch
336
+ ? `${dateMatch[1].slice(0,5).replace(/-/g,'.')}-${dateMatch[2].slice(0,5).replace(/-/g,'.')}.${dateMatch[1].slice(6,10)}`
337
+ : stem;
338
+
339
+ const logFile = path.join(__dirname, '..', 'tellog', stem);
340
+ const outputFile = path.join(__dirname, '..', 'tellog', `${stem}.xlsx`);
328
341
 
329
342
  const rawUsers = parseLog(logFile);
330
343
 
@@ -350,5 +363,5 @@ const EMPTY_USER = (email) => ({
350
363
  return buildRow(email, u);
351
364
  });
352
365
 
353
- await writeExcel(rows, outputFile);
366
+ await writeExcel(rows, outputFile, sheetLabel, fromLabel, toLabel);
354
367
  })();
package/scripts/detect.js CHANGED
@@ -64,6 +64,16 @@ class TaskDetector {
64
64
  antiKeywords: ['code'],
65
65
  icon: '📖',
66
66
  description: 'Writing documentation'
67
+ },
68
+ 'testing': {
69
+ keywords: [
70
+ 'test', 'testing', 'unit test', 'integration test', 'e2e',
71
+ 'qa', 'quality assurance', 'coverage', 'spec', 'assert',
72
+ 'mock', 'stub', 'test case', 'test suite', 'automated test'
73
+ ],
74
+ antiKeywords: [],
75
+ icon: '🧪',
76
+ description: 'Writing tests and QA'
67
77
  }
68
78
  };
69
79
  }
package/scripts/guide.js CHANGED
@@ -181,6 +181,14 @@ function showCommands() {
181
181
  ['aiflow detect "<description>"', '', 'Auto-detect task type'],
182
182
  ]
183
183
  },
184
+ {
185
+ group: 'QA Execute Flow',
186
+ items: [
187
+ ['aiflow execute <TICKET-ID>', '', 'Execute tests from ticket (taskType: execute)'],
188
+ ['aiflow execute <TC-FILE>', '', 'Execute tests from TC file path'],
189
+ ['aiflow execute', '', 'Execute tests — manual (AI asks for TC file)'],
190
+ ]
191
+ },
184
192
  {
185
193
  group: 'Task management',
186
194
  items: [
@@ -279,6 +287,7 @@ function showPromptGuide() {
279
287
  ['refactor', 'Improving code without changing behavior'],
280
288
  ['impact-analysis', 'Analyzing the impact of changes'],
281
289
  ['documentation', 'Writing docs, README, API reference'],
290
+ ['testing', 'Writing tests, test coverage, QA'],
282
291
  ];
283
292
 
284
293
  for (const [type, when] of types) {
package/scripts/prompt.js CHANGED
@@ -215,6 +215,42 @@ AI must:
215
215
  5. Display "GATE 1: Impact report complete" → wait for **APPROVED**
216
216
 
217
217
  **Do NOT make any code changes.** Implementation needs a separate ticket.
218
+ `,
219
+ },
220
+ 'testing': {
221
+ header: 'Testing',
222
+ instruction: 'Write tests for the feature or scenario described below.',
223
+ skillWorkflow: `
224
+ ## STRICT GATE WORKFLOW — Testing
225
+
226
+ ### GATE 1 — AI Analyze Requirement
227
+ **INVOKE:** \`read-study-requirement\` skill
228
+ AI must:
229
+ 1. Load ticket context + read source code thoroughly
230
+ 2. Understand what needs to be tested (unit, integration, e2e)
231
+ 3. If unclear → ask ONE question at a time
232
+ 4. Output \`plan/[ticket-id]/requirement.md\` with:
233
+ - Test scope and strategy
234
+ - Test cases to cover (happy path + edge cases + failure cases)
235
+ - Source code references
236
+ - Effort estimate
237
+ 5. Display "GATE 1: Test plan ready" → wait for **APPROVED**
238
+
239
+ ### GATE 2 — Implementation Plan
240
+ **INVOKE:** \`generate-spec\` skill, then \`superpowers:writing-plans\`
241
+ - Create detailed TDD test implementation plan.
242
+ - Output \`plan/[ticket-id]/plan.md\`.
243
+ - Display: "GATE 2 PAUSED: type APPROVED to start coding".
244
+
245
+ ### GATE 3 — Write Tests (TDD)
246
+ Only runs after Gate 2 APPROVED.
247
+ **INVOKE:** \`superpowers:test-driven-development\`
248
+ - Write failing tests first → confirm FAIL → implement → confirm PASS.
249
+
250
+ ### GATE 4 — AI Self-Review (wait for APPROVED)
251
+ **INVOKE:** \`superpowers:verification-before-completion\`
252
+ All tests must PASS. Check coverage meets requirements.
253
+ Display: "GATE 4 PAUSED: type APPROVED or BUG: [description]"
218
254
  `,
219
255
  },
220
256
  'documentation': {
@@ -0,0 +1,106 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs-extra');
4
+ const path = require('path');
5
+ const chalk = require('chalk');
6
+
7
+ const PKG_DIR = path.join(__dirname, '..');
8
+ const HARNESS_SRC = path.join(PKG_DIR, 'custom', 'harness', 'playwright');
9
+
10
+ const HARNESS_FILES = [
11
+ 'playwright.config.ts',
12
+ '.env.example',
13
+ 'tests/e2e/pages/BasePage.ts',
14
+ 'tests/e2e/fixtures/test.ts',
15
+ 'tests/e2e/auth.setup.ts',
16
+ 'tests/e2e/support/auth.ts',
17
+ ];
18
+
19
+ const GITIGNORE_LINES = [
20
+ '# Playwright',
21
+ 'playwright-report/',
22
+ 'test-results/',
23
+ 'tests/e2e/.auth/',
24
+ '.env',
25
+ ];
26
+
27
+ const MCP_CONFIG = {
28
+ playwright: {
29
+ command: 'npx',
30
+ args: ['-y', '@playwright/mcp@latest', '--isolated', '--browser', 'chromium', '--headless'],
31
+ },
32
+ };
33
+
34
+ /**
35
+ * Scaffold Playwright E2E harness into a target project directory.
36
+ * @param {string} [targetDir] Destination directory (defaults to process.cwd())
37
+ * @param {{ force?: boolean, silent?: boolean }} [opts]
38
+ * @returns {Promise<{ copied: string[], skipped: string[], mcpUpdated: boolean }>}
39
+ */
40
+ async function scaffoldPlaywright(targetDir, opts) {
41
+ if (targetDir === undefined) targetDir = process.cwd();
42
+ var force = opts && opts.force ? true : false;
43
+ var silent = opts && opts.silent ? true : false;
44
+ var log = silent ? function () {} : console.log;
45
+
46
+ var result = { copied: [], skipped: [], mcpUpdated: false };
47
+
48
+ // ── Harness files ─────────────────────────────────────────────
49
+ for (var i = 0; i < HARNESS_FILES.length; i++) {
50
+ var relPath = HARNESS_FILES[i];
51
+ var src = path.join(HARNESS_SRC, relPath);
52
+ var dest = path.join(targetDir, relPath);
53
+ var exists = await fs.pathExists(dest);
54
+
55
+ if (!force && exists) {
56
+ log(chalk.gray(' ↷ Skipped (already exists): ' + relPath));
57
+ result.skipped.push(relPath);
58
+ continue;
59
+ }
60
+
61
+ await fs.ensureDir(path.dirname(dest));
62
+ await fs.copy(src, dest, { overwrite: true });
63
+ log(chalk.green(' ✓ ' + (exists ? 'Overwrote' : 'Created') + ': ' + relPath));
64
+ result.copied.push(relPath);
65
+ }
66
+
67
+ // ── .mcp.json ─────────────────────────────────────────────────
68
+ var mcpFile = path.join(targetDir, '.mcp.json');
69
+ var mcpConfig = { mcpServers: {} };
70
+ if (await fs.pathExists(mcpFile)) {
71
+ mcpConfig = await fs.readJson(mcpFile).catch(function () { return { mcpServers: {} }; });
72
+ }
73
+ if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
74
+
75
+ if (!force && mcpConfig.mcpServers.playwright) {
76
+ log(chalk.gray(' ↷ Skipped .mcp.json playwright entry (already exists)'));
77
+ } else {
78
+ mcpConfig.mcpServers.playwright = MCP_CONFIG.playwright;
79
+ await fs.writeJson(mcpFile, mcpConfig, { spaces: 2 });
80
+ log(chalk.green(' ✓ Updated .mcp.json with playwright MCP server'));
81
+ result.mcpUpdated = true;
82
+ }
83
+
84
+ // ── .gitignore ────────────────────────────────────────────────
85
+ var gitignoreFile = path.join(targetDir, '.gitignore');
86
+ var gitignoreContent = '';
87
+ if (await fs.pathExists(gitignoreFile)) {
88
+ gitignoreContent = await fs.readFile(gitignoreFile, 'utf-8');
89
+ }
90
+ var missingLines = GITIGNORE_LINES.filter(function (line) {
91
+ return !gitignoreContent.includes(line);
92
+ });
93
+ if (missingLines.length > 0) {
94
+ var addition = '\n' + missingLines.join('\n') + '\n';
95
+ await fs.appendFile(gitignoreFile, addition);
96
+ log(chalk.green(' ✓ Updated .gitignore'));
97
+ }
98
+
99
+ return result;
100
+ }
101
+
102
+ module.exports = {
103
+ scaffoldPlaywright: scaffoldPlaywright,
104
+ HARNESS_FILES: HARNESS_FILES,
105
+ MCP_CONFIG: MCP_CONFIG,
106
+ };
package/scripts/task.js CHANGED
@@ -189,7 +189,7 @@ async function resumeTask(taskId) {
189
189
  await fs.writeJson(statePath, taskState, { spaces: 2 });
190
190
 
191
191
  console.log(chalk.green(`✓ Resumed task: ${taskId}`));
192
- console.log(` ${chalk.white('Gate:')} ${taskState.currentGate} (${gateLabel(taskState.currentGate)})`);
192
+ console.log(` ${chalk.white('Gate:')} ${taskState.currentGate} (${gateLabel(taskState.currentGate, taskState.taskType)})`);
193
193
  console.log(` ${chalk.white('Title:')} ${ctx.title.substring(0, 70)}`);
194
194
  if (taskState.notes) console.log(` ${chalk.white('Note:')} ${taskState.notes}`);
195
195
  console.log(chalk.gray('\n Open Claude to resume: claude'));
@@ -355,10 +355,11 @@ async function nextGate(taskId) {
355
355
  const summaryContent = await generateMarkdownSummary(taskState);
356
356
  await fs.writeFile(summaryPath, summaryContent, 'utf-8');
357
357
 
358
- const gateLabels = { 1: 'Analyze', 2: 'Plan', 3: 'Code', 4: 'Review', 5: 'PR' };
358
+ const taskType = existing.taskType || 'feature';
359
359
  console.log(chalk.green(`✓ Gate ${currentGate} approved for ${resolvedId}.`));
360
360
  console.log(chalk.gray(` Summary saved to: plan/${resolvedId}/task-summary.md`));
361
- console.log(chalk.white(`\n Next: Gate ${nextGateNum} — ${gateLabels[nextGateNum] || 'Done'}`));
361
+ const nextLabel = gateLabel(nextGateNum, taskType);
362
+ console.log(chalk.white(`\n Next: Gate ${nextGateNum} — ${nextLabel}`));
362
363
  console.log(chalk.cyan(`\n To continue in a fresh session (Recommended to avoid context pollution):`));
363
364
  console.log(chalk.gray(` 1. Open a NEW chatbox or terminal session.`));
364
365
  console.log(chalk.gray(` 2. Run: aiflow task resume ${resolvedId} (to load context).`));
@@ -418,21 +419,24 @@ async function detectCurrentGate(taskId) {
418
419
  }
419
420
 
420
421
  async function generateMarkdownSummary(taskState) {
422
+ const taskType = taskState.taskType || 'feature';
423
+ const maxGate = taskType === 'testing' ? 4 : 5;
421
424
  const lines = [];
422
425
  lines.push(`# Task Summary: ${taskState.taskId}`);
423
426
  lines.push(`**Title:** ${taskState.title}`);
427
+ lines.push(`**Type:** ${taskType}`);
424
428
  lines.push(`**Status:** ${taskState.status}`);
425
- lines.push(`**Current Gate:** ${taskState.currentGate} (${gateLabel(taskState.currentGate)})`);
429
+ lines.push(`**Current Gate:** ${taskState.currentGate} (${gateLabel(taskState.currentGate, taskType)})`);
426
430
  lines.push(`**Updated At:** ${new Date().toLocaleString()}`);
427
431
  lines.push(``);
428
432
  lines.push(`## Gate History`);
429
433
  lines.push(`| Gate | Status | Approved At |`);
430
434
  lines.push(`|------|--------|-------------|`);
431
- for (let i = 1; i <= 5; i++) {
435
+ for (let i = 1; i <= maxGate; i++) {
432
436
  const approvedAt = taskState.gateApprovals && taskState.gateApprovals[String(i)];
433
437
  const status = approvedAt ? '✅ Approved' : (i === taskState.currentGate ? '⏳ In Progress' : '⚪ Pending');
434
438
  const timeStr = approvedAt ? new Date(approvedAt).toLocaleString() : '-';
435
- lines.push(`| Gate ${i} (${gateLabel(i)}) | ${status} | ${timeStr} |`);
439
+ lines.push(`| Gate ${i} (${gateLabel(i, taskType)}) | ${status} | ${timeStr} |`);
436
440
  }
437
441
  lines.push(``);
438
442
  lines.push(`---`);
@@ -440,7 +444,16 @@ async function generateMarkdownSummary(taskState) {
440
444
  return lines.join('\n');
441
445
  }
442
446
 
443
- function gateLabel(n) {
447
+ function gateLabel(n, taskType) {
448
+ if (taskType === 'testing') {
449
+ const labels = {
450
+ 1: 'Phân tích & Confirm',
451
+ 2: 'Lập kế hoạch Test',
452
+ 3: 'Thực thi & Evidence',
453
+ 4: 'Báo cáo & Sign-off',
454
+ };
455
+ return labels[n] || `Gate ${n}`;
456
+ }
444
457
  const labels = {
445
458
  1: 'AI Analyze Requirement',
446
459
  2: 'Implementation Plan',
@@ -494,6 +507,7 @@ module.exports.createOrActivateTaskState = async function createOrActivateTaskSt
494
507
  ...existing,
495
508
  taskId,
496
509
  title: ctx.title || '',
510
+ taskType: ctx.taskType || existing.taskType || 'feature',
497
511
  status: 'active',
498
512
  createdAt: existing.createdAt || now,
499
513
  updatedAt: now,
package/scripts/update.js CHANGED
@@ -1,124 +1,124 @@
1
- const fs = require('fs-extra');
2
- const path = require('path');
3
- const chalk = require('chalk');
4
- const { confirm, select } = require('@inquirer/prompts');
5
-
6
- const PKG_DIR = path.join(__dirname, '..');
7
- const PKG_VERSION = require('../package.json').version;
8
-
9
- // Same language map as init.js
10
- const FRAMEWORK_LANGUAGE = {
11
- 'laravel': 'php',
12
- 'spring-boot': 'java',
13
- 'reactjs': 'javascript',
14
- 'nextjs': 'javascript',
15
- 'vue-nuxt': 'javascript',
16
- };
17
-
18
- module.exports = async function update(options = {}) {
19
- const projectDir = process.cwd();
20
- const aiflowDir = path.join(projectDir, '.aiflow');
21
- const stateFile = path.join(aiflowDir, 'state.json');
22
-
23
- if (!(await fs.pathExists(stateFile))) {
24
- console.log(chalk.red('Project is not initialized. Please run `aiflow init` first.'));
25
- return;
26
- }
27
-
28
- const state = await fs.readJson(stateFile);
29
- const currentVersion = state.current_version;
30
-
31
- if (currentVersion === PKG_VERSION && !options.force) {
32
- console.log(chalk.cyan(`You are already on v${PKG_VERSION}. Syncing latest assets and instruction files...`));
33
- } else {
34
- console.log(chalk.blue(`Updating from v${currentVersion} to v${PKG_VERSION}...`));
35
- }
36
-
37
- const newVersionDir = path.join(aiflowDir, 'versions', PKG_VERSION);
38
- await fs.ensureDir(newVersionDir);
39
-
40
- // Merge skills
41
- const upstreamSkills = path.join(PKG_DIR, 'upstream', 'skills');
42
- const customSkills = path.join(PKG_DIR, 'custom', 'skills');
43
-
44
- await fs.ensureDir(path.join(newVersionDir, 'skills'));
45
- if (await fs.pathExists(upstreamSkills)) {
46
- await fs.copy(upstreamSkills, path.join(newVersionDir, 'skills'), { overwrite: true });
47
- }
48
- if (await fs.pathExists(customSkills)) {
49
- await fs.copy(customSkills, path.join(newVersionDir, 'skills'), { overwrite: true });
50
- }
51
-
52
- // Copy rules: common first, then language-specific overlay (mirrors init.js logic)
53
- const rulesTarget = path.join(newVersionDir, 'rules');
54
- const commonRules = path.join(PKG_DIR, 'custom', 'rules', 'common');
55
- const allRules = path.join(PKG_DIR, 'custom', 'rules');
56
- const primaryFramework = (state.frameworks || (state.framework ? [state.framework] : []))[0] || null;
57
-
58
- await fs.ensureDir(rulesTarget);
59
- if (await fs.pathExists(commonRules)) {
60
- await fs.copy(commonRules, rulesTarget, { overwrite: true });
61
- } else if (await fs.pathExists(allRules)) {
62
- const entries = await fs.readdir(allRules, { withFileTypes: true });
63
- for (const entry of entries) {
64
- if (entry.isFile() && entry.name.endsWith('.md')) {
65
- await fs.copy(path.join(allRules, entry.name), path.join(rulesTarget, entry.name), { overwrite: true });
66
- }
67
- }
68
- }
69
-
70
- if (primaryFramework) {
71
- const lang = FRAMEWORK_LANGUAGE[primaryFramework];
72
- if (lang) {
73
- const langRules = path.join(PKG_DIR, 'custom', 'rules', lang);
74
- if (await fs.pathExists(langRules)) {
75
- await fs.copy(langRules, rulesTarget, { overwrite: true });
76
- console.log(chalk.gray(` ✓ Applied ${lang} rules`));
77
- }
78
- }
79
- }
80
-
81
- console.log(chalk.green(`✓ Downloaded assets for v${PKG_VERSION} into the project.`));
82
-
83
- // Only offer to delete old version when it's a different version.
84
- // When --force re-copies the same version, old === new === newVersionDir,
85
- // so deleting "old" would immediately break the copy step below.
86
- const isSameVersion = currentVersion === PKG_VERSION;
87
- if (!isSameVersion) {
88
- const deleteOld = await confirm({ message: `Do you want to delete the old version (v${currentVersion})?` });
89
- if (deleteOld) {
90
- await fs.remove(path.join(aiflowDir, 'versions', currentVersion));
91
- console.log(chalk.gray(`Deleted v${currentVersion}.`));
92
- } else {
93
- console.log(chalk.gray(`Kept v${currentVersion}. You can switch back using 'aiflow use ${currentVersion}'.`));
94
- }
95
- }
96
-
97
- // Switch to new version — copy skills without wiping the whole .claude dir
98
- // (wiping would destroy settings.json which holds the hooks configuration)
99
- const claudeDir = path.join(projectDir, '.claude');
100
- const claudeSkillsDir = path.join(claudeDir, 'skills');
101
- await fs.emptyDir(claudeSkillsDir);
102
- await fs.copy(path.join(newVersionDir, 'skills'), claudeSkillsDir, { overwrite: true });
103
-
104
- const rulesDir = path.join(projectDir, '.rules');
105
- await fs.emptyDir(rulesDir);
106
- await fs.copy(path.join(newVersionDir, 'rules'), rulesDir, { overwrite: true });
107
-
108
- state.current_version = PKG_VERSION;
109
- await fs.writeJson(stateFile, state);
110
-
111
- // Sync instruction files with the updated skills.
112
- // Support both old 'framework' (singular string) and new 'frameworks' (array).
113
- const { setupFramework, AI_TOOL_FILES, ensureAiflowGitignored } = require('./init');
114
- const frameworks = state.frameworks || (state.framework ? [state.framework] : []);
115
- const selectedTools = state.aiTools || Object.keys(AI_TOOL_FILES);
116
- for (const fw of frameworks) {
117
- await setupFramework(projectDir, fw, frameworks.length > 1, selectedTools, { force: true });
118
- }
119
-
120
- // Ensure all aiflow files are gitignored (especially for projects upgrading from older versions)
121
- await ensureAiflowGitignored(projectDir);
122
-
123
- console.log(chalk.green(`\n✨ Update completed! Your project is now on v${PKG_VERSION}.`));
124
- };
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+ const { confirm, select } = require('@inquirer/prompts');
5
+
6
+ const PKG_DIR = path.join(__dirname, '..');
7
+ const PKG_VERSION = require('../package.json').version;
8
+
9
+ // Same language map as init.js
10
+ const FRAMEWORK_LANGUAGE = {
11
+ 'laravel': 'php',
12
+ 'spring-boot': 'java',
13
+ 'reactjs': 'javascript',
14
+ 'nextjs': 'javascript',
15
+ 'vue-nuxt': 'javascript',
16
+ };
17
+
18
+ module.exports = async function update(options = {}) {
19
+ const projectDir = process.cwd();
20
+ const aiflowDir = path.join(projectDir, '.aiflow');
21
+ const stateFile = path.join(aiflowDir, 'state.json');
22
+
23
+ if (!(await fs.pathExists(stateFile))) {
24
+ console.log(chalk.red('Project is not initialized. Please run `aiflow init` first.'));
25
+ return;
26
+ }
27
+
28
+ const state = await fs.readJson(stateFile);
29
+ const currentVersion = state.current_version;
30
+
31
+ if (currentVersion === PKG_VERSION && !options.force) {
32
+ console.log(chalk.cyan(`You are already on v${PKG_VERSION}. Syncing latest assets and instruction files...`));
33
+ } else {
34
+ console.log(chalk.blue(`Updating from v${currentVersion} to v${PKG_VERSION}...`));
35
+ }
36
+
37
+ const newVersionDir = path.join(aiflowDir, 'versions', PKG_VERSION);
38
+ await fs.ensureDir(newVersionDir);
39
+
40
+ // Merge skills
41
+ const upstreamSkills = path.join(PKG_DIR, 'upstream', 'skills');
42
+ const customSkills = path.join(PKG_DIR, 'custom', 'skills');
43
+
44
+ await fs.ensureDir(path.join(newVersionDir, 'skills'));
45
+ if (await fs.pathExists(upstreamSkills)) {
46
+ await fs.copy(upstreamSkills, path.join(newVersionDir, 'skills'), { overwrite: true });
47
+ }
48
+ if (await fs.pathExists(customSkills)) {
49
+ await fs.copy(customSkills, path.join(newVersionDir, 'skills'), { overwrite: true });
50
+ }
51
+
52
+ // Copy rules: common first, then language-specific overlay (mirrors init.js logic)
53
+ const rulesTarget = path.join(newVersionDir, 'rules');
54
+ const commonRules = path.join(PKG_DIR, 'custom', 'rules', 'common');
55
+ const allRules = path.join(PKG_DIR, 'custom', 'rules');
56
+ const primaryFramework = (state.frameworks || (state.framework ? [state.framework] : []))[0] || null;
57
+
58
+ await fs.ensureDir(rulesTarget);
59
+ if (await fs.pathExists(commonRules)) {
60
+ await fs.copy(commonRules, rulesTarget, { overwrite: true });
61
+ } else if (await fs.pathExists(allRules)) {
62
+ const entries = await fs.readdir(allRules, { withFileTypes: true });
63
+ for (const entry of entries) {
64
+ if (entry.isFile() && entry.name.endsWith('.md')) {
65
+ await fs.copy(path.join(allRules, entry.name), path.join(rulesTarget, entry.name), { overwrite: true });
66
+ }
67
+ }
68
+ }
69
+
70
+ if (primaryFramework) {
71
+ const lang = FRAMEWORK_LANGUAGE[primaryFramework];
72
+ if (lang) {
73
+ const langRules = path.join(PKG_DIR, 'custom', 'rules', lang);
74
+ if (await fs.pathExists(langRules)) {
75
+ await fs.copy(langRules, rulesTarget, { overwrite: true });
76
+ console.log(chalk.gray(` ✓ Applied ${lang} rules`));
77
+ }
78
+ }
79
+ }
80
+
81
+ console.log(chalk.green(`✓ Downloaded assets for v${PKG_VERSION} into the project.`));
82
+
83
+ // Only offer to delete old version when it's a different version.
84
+ // When --force re-copies the same version, old === new === newVersionDir,
85
+ // so deleting "old" would immediately break the copy step below.
86
+ const isSameVersion = currentVersion === PKG_VERSION;
87
+ if (!isSameVersion) {
88
+ const deleteOld = await confirm({ message: `Do you want to delete the old version (v${currentVersion})?` });
89
+ if (deleteOld) {
90
+ await fs.remove(path.join(aiflowDir, 'versions', currentVersion));
91
+ console.log(chalk.gray(`Deleted v${currentVersion}.`));
92
+ } else {
93
+ console.log(chalk.gray(`Kept v${currentVersion}. You can switch back using 'aiflow use ${currentVersion}'.`));
94
+ }
95
+ }
96
+
97
+ // Switch to new version — copy skills without wiping the whole .claude dir
98
+ // (wiping would destroy settings.json which holds the hooks configuration)
99
+ const claudeDir = path.join(projectDir, '.claude');
100
+ const claudeSkillsDir = path.join(claudeDir, 'skills');
101
+ await fs.emptyDir(claudeSkillsDir);
102
+ await fs.copy(path.join(newVersionDir, 'skills'), claudeSkillsDir, { overwrite: true });
103
+
104
+ const rulesDir = path.join(projectDir, '.rules');
105
+ await fs.emptyDir(rulesDir);
106
+ await fs.copy(path.join(newVersionDir, 'rules'), rulesDir, { overwrite: true });
107
+
108
+ state.current_version = PKG_VERSION;
109
+ await fs.writeJson(stateFile, state);
110
+
111
+ // Sync instruction files with the updated skills.
112
+ // Support both old 'framework' (singular string) and new 'frameworks' (array).
113
+ const { setupFramework, AI_TOOL_FILES, ensureAiflowGitignored } = require('./init');
114
+ const frameworks = state.frameworks || (state.framework ? [state.framework] : []);
115
+ const selectedTools = state.aiTools || Object.keys(AI_TOOL_FILES);
116
+ for (const fw of frameworks) {
117
+ await setupFramework(projectDir, fw, frameworks.length > 1, selectedTools, { force: true });
118
+ }
119
+
120
+ // Ensure all aiflow files are gitignored (especially for projects upgrading from older versions)
121
+ await ensureAiflowGitignored(projectDir);
122
+
123
+ console.log(chalk.green(`\n✨ Update completed! Your project is now on v${PKG_VERSION}.`));
124
+ };