@kmgeon/taskflow 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 (158) hide show
  1. package/README.md +374 -0
  2. package/bin/task-mcp.mjs +19 -0
  3. package/bin/task.mjs +19 -0
  4. package/docs/clean-code.md +29 -0
  5. package/docs/git.md +36 -0
  6. package/docs/guideline.md +25 -0
  7. package/docs/security.md +32 -0
  8. package/docs/step-by-step.md +29 -0
  9. package/docs/superpowers/specs/2026-03-21-cli-advisor-design.md +383 -0
  10. package/docs/superpowers/specs/2026-03-21-init-redesign-design.md +429 -0
  11. package/docs/superpowers/specs/2026-03-21-skill-architecture-design.md +362 -0
  12. package/docs/superpowers/specs/2026-03-23-t-create-task-run-design.md +40 -0
  13. package/docs/superpowers/specs/2026-03-23-task-run-design.md +44 -0
  14. package/docs/tdd.md +41 -0
  15. package/package.json +114 -0
  16. package/src/app/(protected)/dashboard/page.tsx +7 -0
  17. package/src/app/(protected)/layout.tsx +10 -0
  18. package/src/app/api/[[...hono]]/route.ts +13 -0
  19. package/src/app/example/page.tsx +11 -0
  20. package/src/app/favicon.ico +0 -0
  21. package/src/app/globals.css +168 -0
  22. package/src/app/layout.tsx +35 -0
  23. package/src/app/page.tsx +5 -0
  24. package/src/app/providers.tsx +57 -0
  25. package/src/backend/config/index.ts +36 -0
  26. package/src/backend/hono/app.ts +32 -0
  27. package/src/backend/hono/context.ts +38 -0
  28. package/src/backend/http/response.ts +64 -0
  29. package/src/backend/middleware/context.ts +23 -0
  30. package/src/backend/middleware/error.ts +31 -0
  31. package/src/backend/middleware/supabase.ts +23 -0
  32. package/src/backend/supabase/client.ts +17 -0
  33. package/src/cli/commands/__tests__/task-commands.test.ts +170 -0
  34. package/src/cli/commands/advisor.ts +45 -0
  35. package/src/cli/commands/ask.ts +50 -0
  36. package/src/cli/commands/board.ts +72 -0
  37. package/src/cli/commands/init.ts +184 -0
  38. package/src/cli/commands/list.ts +138 -0
  39. package/src/cli/commands/run.ts +143 -0
  40. package/src/cli/commands/set-status.ts +50 -0
  41. package/src/cli/commands/show.ts +28 -0
  42. package/src/cli/commands/tree.ts +72 -0
  43. package/src/cli/index.ts +38 -0
  44. package/src/cli/lib/__tests__/formatter.test.ts +123 -0
  45. package/src/cli/lib/error-boundary.test.ts +135 -0
  46. package/src/cli/lib/error-boundary.ts +70 -0
  47. package/src/cli/lib/formatter.ts +764 -0
  48. package/src/cli/lib/trd.ts +33 -0
  49. package/src/cli/lib/validate.test.ts +89 -0
  50. package/src/cli/lib/validate.ts +43 -0
  51. package/src/cli/prompts/task-run.md +25 -0
  52. package/src/components/layout/AppLayout.tsx +15 -0
  53. package/src/components/layout/Sidebar.tsx +124 -0
  54. package/src/components/ui/accordion.tsx +58 -0
  55. package/src/components/ui/avatar.tsx +50 -0
  56. package/src/components/ui/badge.tsx +36 -0
  57. package/src/components/ui/button.tsx +56 -0
  58. package/src/components/ui/card.tsx +79 -0
  59. package/src/components/ui/checkbox.tsx +30 -0
  60. package/src/components/ui/dialog.tsx +122 -0
  61. package/src/components/ui/dropdown-menu.tsx +200 -0
  62. package/src/components/ui/file-upload.tsx +50 -0
  63. package/src/components/ui/form.tsx +179 -0
  64. package/src/components/ui/input.tsx +25 -0
  65. package/src/components/ui/label.tsx +26 -0
  66. package/src/components/ui/scroll-area.tsx +48 -0
  67. package/src/components/ui/select.tsx +160 -0
  68. package/src/components/ui/separator.tsx +31 -0
  69. package/src/components/ui/sheet.tsx +140 -0
  70. package/src/components/ui/textarea.tsx +22 -0
  71. package/src/components/ui/toast.tsx +129 -0
  72. package/src/components/ui/toaster.tsx +35 -0
  73. package/src/core/ai/claude-client.ts +79 -0
  74. package/src/core/claude-runner/flag-builder.ts +57 -0
  75. package/src/core/claude-runner/index.ts +2 -0
  76. package/src/core/claude-runner/spawner.ts +86 -0
  77. package/src/core/prd/__tests__/auto-analyzer.test.ts +35 -0
  78. package/src/core/prd/__tests__/generator.test.ts +26 -0
  79. package/src/core/prd/__tests__/scanner.test.ts +35 -0
  80. package/src/core/prd/auto-analyzer.ts +9 -0
  81. package/src/core/prd/generator.ts +8 -0
  82. package/src/core/prd/scanner.ts +117 -0
  83. package/src/core/project/__tests__/claude-setup.test.ts +133 -0
  84. package/src/core/project/__tests__/config.test.ts +30 -0
  85. package/src/core/project/__tests__/init.test.ts +37 -0
  86. package/src/core/project/__tests__/skill-setup.test.ts +62 -0
  87. package/src/core/project/claude-setup.ts +224 -0
  88. package/src/core/project/config.ts +34 -0
  89. package/src/core/project/docs-setup.ts +26 -0
  90. package/src/core/project/docs-templates.ts +205 -0
  91. package/src/core/project/init.ts +40 -0
  92. package/src/core/project/skill-setup.ts +32 -0
  93. package/src/core/project/skill-templates.ts +277 -0
  94. package/src/core/task/index.ts +16 -0
  95. package/src/core/types.ts +58 -0
  96. package/src/features/example/backend/error.ts +9 -0
  97. package/src/features/example/backend/route.ts +52 -0
  98. package/src/features/example/backend/schema.ts +25 -0
  99. package/src/features/example/backend/service.ts +73 -0
  100. package/src/features/example/components/example-status.test.tsx +97 -0
  101. package/src/features/example/components/example-status.tsx +160 -0
  102. package/src/features/example/hooks/useExampleQuery.ts +23 -0
  103. package/src/features/example/lib/dto.test.ts +57 -0
  104. package/src/features/example/lib/dto.ts +5 -0
  105. package/src/features/kanban/backend/__tests__/sse-broadcaster.test.ts +137 -0
  106. package/src/features/kanban/backend/__tests__/sse-event-format.test.ts +55 -0
  107. package/src/features/kanban/backend/route.ts +55 -0
  108. package/src/features/kanban/backend/sse-broadcaster.ts +142 -0
  109. package/src/features/kanban/backend/sse-route.ts +43 -0
  110. package/src/features/kanban/components/KanbanBoard.tsx +105 -0
  111. package/src/features/kanban/components/KanbanColumn.tsx +51 -0
  112. package/src/features/kanban/components/KanbanError.tsx +29 -0
  113. package/src/features/kanban/components/KanbanSkeleton.tsx +46 -0
  114. package/src/features/kanban/components/ProgressCard.tsx +42 -0
  115. package/src/features/kanban/components/TaskCard.tsx +76 -0
  116. package/src/features/kanban/components/__tests__/kanban-components.test.tsx +86 -0
  117. package/src/features/kanban/hooks/useTaskSse.ts +66 -0
  118. package/src/features/kanban/hooks/useTasksQuery.ts +52 -0
  119. package/src/features/kanban/lib/__tests__/kanban-utils.test.ts +97 -0
  120. package/src/features/kanban/lib/kanban-utils.ts +37 -0
  121. package/src/features/taskflow/constants.ts +54 -0
  122. package/src/features/taskflow/index.ts +27 -0
  123. package/src/features/taskflow/lib/__tests__/filter.test.ts +89 -0
  124. package/src/features/taskflow/lib/__tests__/graph.test.ts +247 -0
  125. package/src/features/taskflow/lib/__tests__/repository.test.ts +233 -0
  126. package/src/features/taskflow/lib/__tests__/serializer.test.ts +98 -0
  127. package/src/features/taskflow/lib/advisor/__tests__/advisor-integration.test.ts +98 -0
  128. package/src/features/taskflow/lib/advisor/ai-advisor.test.ts +40 -0
  129. package/src/features/taskflow/lib/advisor/ai-advisor.ts +20 -0
  130. package/src/features/taskflow/lib/advisor/context-builder.test.ts +73 -0
  131. package/src/features/taskflow/lib/advisor/context-builder.ts +151 -0
  132. package/src/features/taskflow/lib/advisor/db.test.ts +106 -0
  133. package/src/features/taskflow/lib/advisor/db.ts +185 -0
  134. package/src/features/taskflow/lib/advisor/local-summary.test.ts +53 -0
  135. package/src/features/taskflow/lib/advisor/local-summary.ts +72 -0
  136. package/src/features/taskflow/lib/advisor/prompts.ts +86 -0
  137. package/src/features/taskflow/lib/filter.ts +54 -0
  138. package/src/features/taskflow/lib/fs-utils.ts +50 -0
  139. package/src/features/taskflow/lib/graph.ts +148 -0
  140. package/src/features/taskflow/lib/index-builder.ts +42 -0
  141. package/src/features/taskflow/lib/repository.ts +168 -0
  142. package/src/features/taskflow/lib/serializer.ts +62 -0
  143. package/src/features/taskflow/lib/watcher.ts +40 -0
  144. package/src/features/taskflow/types.ts +71 -0
  145. package/src/hooks/use-toast.ts +194 -0
  146. package/src/lib/remote/api-client.ts +40 -0
  147. package/src/lib/supabase/client.ts +8 -0
  148. package/src/lib/supabase/server.ts +46 -0
  149. package/src/lib/supabase/types.ts +3 -0
  150. package/src/lib/utils.ts +6 -0
  151. package/src/mcp/index.ts +7 -0
  152. package/src/mcp/server.ts +21 -0
  153. package/src/mcp/tools/brainstorm.ts +48 -0
  154. package/src/mcp/tools/prd.ts +71 -0
  155. package/src/mcp/tools/project.ts +39 -0
  156. package/src/mcp/tools/task-status.ts +40 -0
  157. package/src/mcp/tools/task.ts +82 -0
  158. package/src/mcp/util.ts +6 -0
@@ -0,0 +1,86 @@
1
+ import { spawnSync, spawn } from "child_process";
2
+
3
+ export interface SpawnResult {
4
+ exitCode: number;
5
+ stdout: string;
6
+ stderr: string;
7
+ }
8
+
9
+ /**
10
+ * Claude CLI를 실행한다.
11
+ *
12
+ * - spawnSync + stdio: 'inherit'로 실시간 터미널 I/O
13
+ * - dry-run 모드: CC_DRY_RUN=1이면 실제 spawn 없이 명령어 출력
14
+ * - env 파라미터: process.env에 merge되어 subprocess에 전달
15
+ */
16
+ export function spawnClaude(
17
+ flags: string[],
18
+ env?: Record<string, string>,
19
+ ): SpawnResult {
20
+ if (process.env.CC_DRY_RUN === "1") {
21
+ const command = ["claude", ...flags].join(" ");
22
+ process.stdout.write(`[DRY-RUN] ${command}\n`);
23
+ if (env && Object.keys(env).length > 0) {
24
+ process.stdout.write(`[DRY-RUN] env: ${Object.keys(env).join(", ")}\n`);
25
+ }
26
+ return { exitCode: 0, stdout: "", stderr: "" };
27
+ }
28
+
29
+ const result = spawnSync("claude", flags, {
30
+ stdio: "inherit",
31
+ env: { ...process.env, ...env },
32
+ });
33
+
34
+ return {
35
+ exitCode: result.status ?? 1,
36
+ stdout: "",
37
+ stderr: "",
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Claude CLI를 실행하되, stdout을 캡처하면서 터미널에도 출력한다.
43
+ *
44
+ * - stdin/stderr는 터미널에 직접 연결 (대화형 유지)
45
+ * - stdout은 pipe → 터미널 출력 + 버퍼 저장
46
+ */
47
+ export interface SpawnCaptureOptions {
48
+ env?: Record<string, string>;
49
+ /** stdout 청크를 터미널에 쓰기 전에 변환하는 함수 */
50
+ transformOutput?: (text: string) => string;
51
+ }
52
+
53
+ export function spawnClaudeWithCapture(
54
+ flags: string[],
55
+ options?: SpawnCaptureOptions,
56
+ ): Promise<SpawnResult> {
57
+ const { env, transformOutput } = options ?? {};
58
+ if (process.env.CC_DRY_RUN === "1") {
59
+ const command = ["claude", ...flags].join(" ");
60
+ process.stdout.write(`[DRY-RUN] ${command}\n`);
61
+ return Promise.resolve({ exitCode: 0, stdout: "", stderr: "" });
62
+ }
63
+
64
+ return new Promise((resolve) => {
65
+ const child = spawn("claude", flags, {
66
+ stdio: ["inherit", "pipe", "inherit"],
67
+ env: { ...process.env, ...env },
68
+ });
69
+
70
+ const chunks: Buffer[] = [];
71
+
72
+ child.stdout?.on("data", (chunk: Buffer) => {
73
+ const text = chunk.toString("utf-8");
74
+ process.stdout.write(transformOutput ? transformOutput(text) : text);
75
+ chunks.push(chunk);
76
+ });
77
+
78
+ child.on("close", (code) => {
79
+ resolve({
80
+ exitCode: code ?? 1,
81
+ stdout: Buffer.concat(chunks).toString("utf-8"),
82
+ stderr: "",
83
+ });
84
+ });
85
+ });
86
+ }
@@ -0,0 +1,35 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { maskSensitive, extractSignature, inferProjectName } from "../auto-analyzer.js";
3
+ import type { FileSample } from "../auto-analyzer.js";
4
+
5
+ describe("maskSensitive", () => {
6
+ it("should mask API keys", () => {
7
+ const input = 'const apiKey = "sk-abc123def456ghi789jkl012"';
8
+ const result = maskSensitive(input);
9
+ expect(result).not.toContain("sk-abc123def456ghi789jkl012");
10
+ expect(result).toContain("[REDACTED]");
11
+ });
12
+ });
13
+
14
+ describe("extractSignature", () => {
15
+ it("should keep import/export lines within byte limit", () => {
16
+ const content = 'import foo from "bar";\nexport function test() {}\nconst x = 1;\nconst y = 2;\n';
17
+ const result = extractSignature(content, 100);
18
+ expect(result).toContain("import foo");
19
+ expect(result).toContain("export function");
20
+ });
21
+ });
22
+
23
+ describe("inferProjectName", () => {
24
+ it("should extract name from package.json sample", () => {
25
+ const samples: FileSample[] = [
26
+ { path: "package.json", content: '{"name": "my-project"}', truncated: false },
27
+ ];
28
+ expect(inferProjectName(samples, "/tmp/test")).toBe("my-project");
29
+ });
30
+
31
+ it("should fallback to projectRoot basename", () => {
32
+ const samples: FileSample[] = [];
33
+ expect(inferProjectName(samples, "/tmp/my-awesome-project")).toBe("my-awesome-project");
34
+ });
35
+ });
@@ -0,0 +1,26 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { savePrd } from "../generator.js";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import os from "node:os";
6
+
7
+ describe("savePrd", () => {
8
+ let tmpDir: string;
9
+
10
+ beforeEach(async () => {
11
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "gen-test-"));
12
+ await fs.mkdir(path.join(tmpDir, ".taskflow"), { recursive: true });
13
+ });
14
+
15
+ afterEach(async () => {
16
+ await fs.rm(tmpDir, { recursive: true, force: true });
17
+ });
18
+
19
+ it("should save PRD markdown to .taskflow/prd.md", async () => {
20
+ const markdown = "# Test PRD\n\nContent here";
21
+ const result = await savePrd(tmpDir, markdown);
22
+ expect(result).toContain("prd.md");
23
+ const content = await fs.readFile(result, "utf-8");
24
+ expect(content).toBe(markdown);
25
+ });
26
+ });
@@ -0,0 +1,35 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { maskSensitive, extractSignature, inferProjectName } from "../scanner.js";
3
+ import type { FileSample } from "../scanner.js";
4
+
5
+ describe("maskSensitive", () => {
6
+ it("should mask API keys", () => {
7
+ const input = 'const apiKey = "sk-abc123def456ghi789jkl012"';
8
+ const result = maskSensitive(input);
9
+ expect(result).not.toContain("sk-abc123def456ghi789jkl012");
10
+ expect(result).toContain("[REDACTED]");
11
+ });
12
+ });
13
+
14
+ describe("extractSignature", () => {
15
+ it("should keep import/export lines within byte limit", () => {
16
+ const content = 'import foo from "bar";\nexport function test() {}\nconst x = 1;\nconst y = 2;\n';
17
+ const result = extractSignature(content, 100);
18
+ expect(result).toContain("import foo");
19
+ expect(result).toContain("export function");
20
+ });
21
+ });
22
+
23
+ describe("inferProjectName", () => {
24
+ it("should extract name from package.json sample", () => {
25
+ const samples: FileSample[] = [
26
+ { path: "package.json", content: '{"name": "my-project"}', truncated: false },
27
+ ];
28
+ expect(inferProjectName(samples, "/tmp/test")).toBe("my-project");
29
+ });
30
+
31
+ it("should fallback to projectRoot basename", () => {
32
+ const samples: FileSample[] = [];
33
+ expect(inferProjectName(samples, "/tmp/my-awesome-project")).toBe("my-awesome-project");
34
+ });
35
+ });
@@ -0,0 +1,9 @@
1
+ // 하위 호환성을 위한 re-export
2
+ export {
3
+ type FileSample,
4
+ maskSensitive,
5
+ extractSignature,
6
+ scanFiles,
7
+ sampleFiles,
8
+ inferProjectName,
9
+ } from "./scanner.js";
@@ -0,0 +1,8 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ export async function savePrd(projectRoot: string, markdown: string): Promise<string> {
5
+ const filePath = path.join(projectRoot, ".taskflow", "prd.md");
6
+ await fs.writeFile(filePath, markdown, "utf-8");
7
+ return filePath;
8
+ }
@@ -0,0 +1,117 @@
1
+ import fg from "fast-glob";
2
+ import { readFile } from "node:fs/promises";
3
+ import { basename } from "node:path";
4
+
5
+ const SCAN_PATTERNS = [
6
+ "package.json", "tsconfig*.json", "next.config.*", "vite.config.*",
7
+ "nuxt.config.*", "nest-cli.json", "angular.json",
8
+ "docker-compose*.{yml,yaml}", "Dockerfile", ".env.example",
9
+ "src/**/*.{ts,tsx,js,jsx}", "app/**/*.{ts,tsx,js,jsx}",
10
+ "server/**/*.{ts,tsx,js,jsx}", "api/**/*.{ts,tsx,js,jsx}",
11
+ "lib/**/*.{ts,tsx,js,jsx}", "pages/**/*.{ts,tsx,js,jsx}",
12
+ ];
13
+
14
+ const IGNORE_PATTERNS = [
15
+ "**/node_modules/**", "**/.git/**", "**/dist/**", "**/build/**",
16
+ "**/.next/**", "**/coverage/**", "**/*.test.*", "**/*.spec.*",
17
+ "**/__tests__/**", "**/*.d.ts",
18
+ ];
19
+
20
+ export const MAX_BYTES_PER_FILE = 16_000;
21
+ export const MAX_FILES = 200;
22
+
23
+ const SENSITIVE_PATTERNS = [
24
+ /(?:api[_-]?key|secret|token|password|credential|auth)\s*[:=]\s*["']?[^\s"',]+/gi,
25
+ /(?:sk|pk|key)-[a-zA-Z0-9]{20,}/g,
26
+ /\/Users\/[^\s/]+/g,
27
+ /\/home\/[^\s/]+/g,
28
+ /C:\\Users\\[^\s\\]+/g,
29
+ ];
30
+
31
+ export function maskSensitive(content: string): string {
32
+ let masked = content;
33
+ for (const pattern of SENSITIVE_PATTERNS) {
34
+ masked = masked.replace(pattern, "[REDACTED]");
35
+ }
36
+ return masked;
37
+ }
38
+
39
+ export interface FileSample {
40
+ path: string;
41
+ content: string;
42
+ truncated: boolean;
43
+ }
44
+
45
+ export async function scanFiles(cwd: string): Promise<string[]> {
46
+ const files = await fg(SCAN_PATTERNS, {
47
+ cwd,
48
+ ignore: IGNORE_PATTERNS,
49
+ dot: true,
50
+ absolute: false,
51
+ onlyFiles: true,
52
+ });
53
+ return files.slice(0, MAX_FILES).sort();
54
+ }
55
+
56
+ export function extractSignature(content: string, maxBytes: number): string {
57
+ if (Buffer.byteLength(content, "utf-8") <= maxBytes) {
58
+ return content;
59
+ }
60
+
61
+ const lines = content.split("\n");
62
+ const significant: string[] = [];
63
+ let bytes = 0;
64
+
65
+ for (const line of lines) {
66
+ const trimmed = line.trim();
67
+ const isSignificant =
68
+ trimmed.startsWith("import ") || trimmed.startsWith("export ") ||
69
+ trimmed.startsWith("//") || trimmed.startsWith("/*") ||
70
+ trimmed.startsWith("* ") || trimmed.startsWith("*/") ||
71
+ trimmed.startsWith("interface ") || trimmed.startsWith("type ") ||
72
+ trimmed.startsWith("class ") || trimmed.startsWith("function ") ||
73
+ trimmed.startsWith("const ") || trimmed.startsWith("async function") ||
74
+ trimmed.startsWith("@") ||
75
+ /^\s*(app|router|server)\.(get|post|put|delete|patch|use)\(/.test(trimmed) ||
76
+ trimmed === "" || trimmed === "{" || trimmed === "}";
77
+
78
+ if (isSignificant) {
79
+ const lineBytes = Buffer.byteLength(line + "\n", "utf-8");
80
+ if (bytes + lineBytes > maxBytes) break;
81
+ significant.push(line);
82
+ bytes += lineBytes;
83
+ }
84
+ }
85
+
86
+ return significant.join("\n");
87
+ }
88
+
89
+ export async function sampleFiles(filePaths: string[], cwd: string): Promise<FileSample[]> {
90
+ const samples: FileSample[] = [];
91
+
92
+ for (const filePath of filePaths) {
93
+ try {
94
+ const fullPath = `${cwd}/${filePath}`;
95
+ const raw = await readFile(fullPath, "utf-8");
96
+ const originalBytes = Buffer.byteLength(raw, "utf-8");
97
+ const content = extractSignature(raw, MAX_BYTES_PER_FILE);
98
+ const masked = maskSensitive(content);
99
+ samples.push({ path: filePath, content: masked, truncated: originalBytes > MAX_BYTES_PER_FILE });
100
+ } catch {
101
+ // skip unreadable files
102
+ }
103
+ }
104
+
105
+ return samples;
106
+ }
107
+
108
+ export function inferProjectName(samples: FileSample[], projectRoot: string): string {
109
+ const pkgSample = samples.find((s) => s.path === "package.json");
110
+ if (pkgSample) {
111
+ try {
112
+ const pkg = JSON.parse(pkgSample.content);
113
+ if (pkg.name && typeof pkg.name === "string") return pkg.name;
114
+ } catch { /* fallback */ }
115
+ }
116
+ return basename(projectRoot);
117
+ }
@@ -0,0 +1,133 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import {
6
+ generateClaudeMd,
7
+ generateMcpJson,
8
+ appendClaudeImport,
9
+ } from "../claude-setup.js";
10
+
11
+ describe("generateClaudeMd", () => {
12
+ let tmpDir: string;
13
+
14
+ beforeEach(async () => {
15
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "claude-setup-"));
16
+ await fs.mkdir(path.join(tmpDir, ".taskflow"), { recursive: true });
17
+ });
18
+
19
+ afterEach(async () => {
20
+ await fs.rm(tmpDir, { recursive: true, force: true });
21
+ });
22
+
23
+ it("should generate .taskflow/CLAUDE.md with project info", async () => {
24
+ await generateClaudeMd(tmpDir, {
25
+ projectName: "TestProject",
26
+ summary: "A test project",
27
+ stack: ["TypeScript", "Next.js"],
28
+ });
29
+
30
+ const content = await fs.readFile(path.join(tmpDir, ".taskflow/CLAUDE.md"), "utf-8");
31
+ expect(content).toContain("TestProject");
32
+ expect(content).toContain("A test project");
33
+ expect(content).toContain("TypeScript");
34
+ expect(content).toContain("list_tasks");
35
+ // new MCP tools
36
+ expect(content).toContain("scan_codebase");
37
+ expect(content).toContain("save_prd");
38
+ expect(content).toContain("read_prd");
39
+ // old tools should not be present
40
+ expect(content).not.toContain("brainstorm_prd");
41
+ expect(content).not.toContain("auto_analyze_prd");
42
+ expect(content).not.toContain("parse_prd");
43
+ expect(content).not.toContain("brainstorm_task");
44
+ expect(content).not.toContain("refine_tasks");
45
+ expect(content).not.toContain("generate_feature_prd");
46
+ // skill commands
47
+ expect(content).toContain("/prd");
48
+ expect(content).toContain("/trd");
49
+ expect(content).toContain("/brainstorm");
50
+ expect(content).toContain("/refine");
51
+ expect(content).toContain("/parse-prd");
52
+ expect(content).toContain("/next");
53
+ expect(content).toContain("/task-status");
54
+ });
55
+ });
56
+
57
+ describe("generateMcpJson", () => {
58
+ let tmpDir: string;
59
+
60
+ beforeEach(async () => {
61
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "mcp-json-"));
62
+ });
63
+
64
+ afterEach(async () => {
65
+ await fs.rm(tmpDir, { recursive: true, force: true });
66
+ });
67
+
68
+ it("should create .mcp.json with taskflow server config", async () => {
69
+ await generateMcpJson(tmpDir);
70
+
71
+ const content = await fs.readFile(path.join(tmpDir, ".mcp.json"), "utf-8");
72
+ const parsed = JSON.parse(content);
73
+ expect(parsed.mcpServers.taskflow).toBeDefined();
74
+ expect(parsed.mcpServers.taskflow.type).toBe("stdio");
75
+ });
76
+
77
+ it("should merge with existing .mcp.json without overwriting other servers", async () => {
78
+ await fs.writeFile(
79
+ path.join(tmpDir, ".mcp.json"),
80
+ JSON.stringify({ mcpServers: { other: { type: "stdio", command: "other" } } }),
81
+ );
82
+
83
+ await generateMcpJson(tmpDir);
84
+
85
+ const content = await fs.readFile(path.join(tmpDir, ".mcp.json"), "utf-8");
86
+ const parsed = JSON.parse(content);
87
+ expect(parsed.mcpServers.other).toBeDefined();
88
+ expect(parsed.mcpServers.taskflow).toBeDefined();
89
+ });
90
+ });
91
+
92
+ describe("appendClaudeImport", () => {
93
+ let tmpDir: string;
94
+
95
+ beforeEach(async () => {
96
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "claude-import-"));
97
+ });
98
+
99
+ afterEach(async () => {
100
+ await fs.rm(tmpDir, { recursive: true, force: true });
101
+ });
102
+
103
+ it("should create CLAUDE.md with import if it does not exist", async () => {
104
+ await appendClaudeImport(tmpDir);
105
+
106
+ const content = await fs.readFile(path.join(tmpDir, "CLAUDE.md"), "utf-8");
107
+ expect(content).toContain("@./.taskflow/CLAUDE.md");
108
+ });
109
+
110
+ it("should append import to existing CLAUDE.md", async () => {
111
+ await fs.writeFile(path.join(tmpDir, "CLAUDE.md"), "# My Project\n\nExisting content\n");
112
+
113
+ await appendClaudeImport(tmpDir);
114
+
115
+ const content = await fs.readFile(path.join(tmpDir, "CLAUDE.md"), "utf-8");
116
+ expect(content).toContain("# My Project");
117
+ expect(content).toContain("Existing content");
118
+ expect(content).toContain("@./.taskflow/CLAUDE.md");
119
+ });
120
+
121
+ it("should not duplicate import if already present", async () => {
122
+ await fs.writeFile(
123
+ path.join(tmpDir, "CLAUDE.md"),
124
+ "# My Project\n\n## TaskFlow\n@./.taskflow/CLAUDE.md\n",
125
+ );
126
+
127
+ await appendClaudeImport(tmpDir);
128
+
129
+ const content = await fs.readFile(path.join(tmpDir, "CLAUDE.md"), "utf-8");
130
+ const matches = content.match(/@\.\/\.taskflow\/CLAUDE\.md/g);
131
+ expect(matches).toHaveLength(1);
132
+ });
133
+ });
@@ -0,0 +1,30 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { readConfig, writeConfig, DEFAULT_CONFIG } from "../config.js";
6
+
7
+ describe("config", () => {
8
+ let tmpDir: string;
9
+
10
+ beforeEach(async () => {
11
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "taskflow-test-"));
12
+ await fs.mkdir(path.join(tmpDir, ".taskflow"), { recursive: true });
13
+ });
14
+
15
+ afterEach(async () => {
16
+ await fs.rm(tmpDir, { recursive: true, force: true });
17
+ });
18
+
19
+ it("should return default config when file does not exist", async () => {
20
+ const config = await readConfig(tmpDir);
21
+ expect(config).toEqual(DEFAULT_CONFIG);
22
+ });
23
+
24
+ it("should write and read config", async () => {
25
+ const config = { ...DEFAULT_CONFIG, project: { ...DEFAULT_CONFIG.project, name: "test" } };
26
+ await writeConfig(tmpDir, config);
27
+ const result = await readConfig(tmpDir);
28
+ expect(result.project.name).toBe("test");
29
+ });
30
+ });
@@ -0,0 +1,37 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { initProject } from "../init.js";
6
+
7
+ describe("initProject", () => {
8
+ let tmpDir: string;
9
+
10
+ beforeEach(async () => {
11
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "taskflow-init-"));
12
+ });
13
+
14
+ afterEach(async () => {
15
+ await fs.rm(tmpDir, { recursive: true, force: true });
16
+ });
17
+
18
+ it("should create .taskflow directory and config.json", async () => {
19
+ const result = await initProject(tmpDir);
20
+ expect(result.created).toBe(true);
21
+
22
+ const stat = await fs.stat(path.join(tmpDir, ".taskflow"));
23
+ expect(stat.isDirectory()).toBe(true);
24
+
25
+ const config = await fs.readFile(path.join(tmpDir, ".taskflow/config.json"), "utf-8");
26
+ expect(JSON.parse(config).version).toBe("1.0");
27
+ });
28
+
29
+ it("should return created=false when .taskflow already exists", async () => {
30
+ await fs.mkdir(path.join(tmpDir, ".taskflow"), { recursive: true });
31
+ await fs.writeFile(path.join(tmpDir, ".taskflow/config.json"), "{}");
32
+
33
+ const result = await initProject(tmpDir);
34
+ expect(result.created).toBe(false);
35
+ expect(result.alreadyExists).toBe(true);
36
+ });
37
+ });
@@ -0,0 +1,62 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { installSkills, SKILL_NAMES } from "../skill-setup.js";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import os from "node:os";
6
+
7
+ describe("installSkills", () => {
8
+ let tmpDir: string;
9
+
10
+ beforeEach(async () => {
11
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "skill-test-"));
12
+ await fs.mkdir(path.join(tmpDir, ".taskflow"), { recursive: true });
13
+ });
14
+
15
+ afterEach(async () => {
16
+ await fs.rm(tmpDir, { recursive: true, force: true });
17
+ });
18
+
19
+ it("should create skill source files and symlinks", async () => {
20
+ await installSkills(tmpDir);
21
+
22
+ const srcDir = path.join(tmpDir, ".taskflow", ".claude", "commands");
23
+ const destDir = path.join(tmpDir, ".claude", "commands");
24
+
25
+ for (const name of SKILL_NAMES) {
26
+ const srcFile = path.join(srcDir, `${name}.md`);
27
+ const srcContent = await fs.readFile(srcFile, "utf-8");
28
+ expect(srcContent.length).toBeGreaterThan(0);
29
+
30
+ const linkPath = path.join(destDir, `${name}.md`);
31
+ const stat = await fs.lstat(linkPath);
32
+ expect(stat.isSymbolicLink()).toBe(true);
33
+
34
+ const linkContent = await fs.readFile(linkPath, "utf-8");
35
+ expect(linkContent).toBe(srcContent);
36
+ }
37
+ });
38
+
39
+ it("should skip symlink if destination file already exists", async () => {
40
+ const destDir = path.join(tmpDir, ".claude", "commands");
41
+ await fs.mkdir(destDir, { recursive: true });
42
+ await fs.writeFile(path.join(destDir, "prd.md"), "# user custom prd");
43
+
44
+ await installSkills(tmpDir);
45
+
46
+ const content = await fs.readFile(path.join(destDir, "prd.md"), "utf-8");
47
+ expect(content).toBe("# user custom prd");
48
+ });
49
+
50
+ it("should overwrite stale skill source files on re-init", async () => {
51
+ await installSkills(tmpDir);
52
+
53
+ const srcFile = path.join(tmpDir, ".taskflow", ".claude", "commands", "prd.md");
54
+ await fs.writeFile(srcFile, "# old content");
55
+
56
+ await installSkills(tmpDir);
57
+
58
+ const content = await fs.readFile(srcFile, "utf-8");
59
+ expect(content).not.toBe("# old content");
60
+ expect(content).toContain("PRD");
61
+ });
62
+ });