@cliangdev/flux-plugin 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -7
- package/agents/coder.md +150 -25
- package/bin/install.cjs +171 -16
- package/commands/breakdown.md +47 -10
- package/commands/dashboard.md +29 -0
- package/commands/flux.md +92 -12
- package/commands/implement.md +166 -17
- package/commands/linear.md +6 -5
- package/commands/prd.md +996 -82
- package/manifest.json +2 -1
- package/package.json +9 -11
- package/skills/flux-orchestrator/SKILL.md +11 -3
- package/skills/prd-writer/SKILL.md +761 -0
- package/skills/ux-ui-design/SKILL.md +346 -0
- package/skills/ux-ui-design/references/design-tokens.md +359 -0
- package/src/__tests__/version.test.ts +37 -0
- package/src/adapters/local/.gitkeep +0 -0
- package/src/dashboard/__tests__/api.test.ts +211 -0
- package/src/dashboard/browser.ts +35 -0
- package/src/dashboard/public/app.js +869 -0
- package/src/dashboard/public/index.html +90 -0
- package/src/dashboard/public/styles.css +807 -0
- package/src/dashboard/public/vendor/highlight.css +10 -0
- package/src/dashboard/public/vendor/highlight.min.js +8422 -0
- package/src/dashboard/public/vendor/marked.min.js +2210 -0
- package/src/dashboard/server.ts +296 -0
- package/src/dashboard/watchers.ts +83 -0
- package/src/server/__tests__/config.test.ts +163 -0
- package/src/server/adapters/__tests__/a-client-linear.test.ts +197 -0
- package/src/server/adapters/__tests__/adapter-factory.test.ts +230 -0
- package/src/server/adapters/__tests__/dependency-ops.test.ts +429 -0
- package/src/server/adapters/__tests__/document-ops.test.ts +306 -0
- package/src/server/adapters/__tests__/linear-adapter.test.ts +91 -0
- package/src/server/adapters/__tests__/linear-config.test.ts +425 -0
- package/src/server/adapters/__tests__/linear-criteria-parser.test.ts +287 -0
- package/src/server/adapters/__tests__/linear-description-test.ts +238 -0
- package/src/server/adapters/__tests__/linear-epic-crud.test.ts +496 -0
- package/src/server/adapters/__tests__/linear-mappers-description.test.ts +276 -0
- package/src/server/adapters/__tests__/linear-mappers-epic.test.ts +294 -0
- package/src/server/adapters/__tests__/linear-mappers-prd.test.ts +300 -0
- package/src/server/adapters/__tests__/linear-mappers-task.test.ts +197 -0
- package/src/server/adapters/__tests__/linear-prd-crud.test.ts +620 -0
- package/src/server/adapters/__tests__/linear-stats.test.ts +450 -0
- package/src/server/adapters/__tests__/linear-task-crud.test.ts +534 -0
- package/src/server/adapters/__tests__/linear-types.test.ts +243 -0
- package/src/server/adapters/__tests__/status-ops.test.ts +441 -0
- package/src/server/adapters/factory.ts +90 -0
- package/src/server/adapters/index.ts +9 -0
- package/src/server/adapters/linear/adapter.ts +1141 -0
- package/src/server/adapters/linear/client.ts +169 -0
- package/src/server/adapters/linear/config.ts +152 -0
- package/src/server/adapters/linear/helpers/criteria-parser.ts +197 -0
- package/src/server/adapters/linear/helpers/index.ts +7 -0
- package/src/server/adapters/linear/index.ts +16 -0
- package/src/server/adapters/linear/mappers/description.ts +136 -0
- package/src/server/adapters/linear/mappers/epic.ts +81 -0
- package/src/server/adapters/linear/mappers/index.ts +27 -0
- package/src/server/adapters/linear/mappers/prd.ts +178 -0
- package/src/server/adapters/linear/mappers/task.ts +82 -0
- package/src/server/adapters/linear/types.ts +264 -0
- package/src/server/adapters/local-adapter.ts +1009 -0
- package/src/server/adapters/types.ts +293 -0
- package/src/server/config.ts +73 -0
- package/src/server/db/__tests__/queries.test.ts +473 -0
- package/src/server/db/ids.ts +17 -0
- package/src/server/db/index.ts +69 -0
- package/src/server/db/queries.ts +142 -0
- package/src/server/db/refs.ts +60 -0
- package/src/server/db/schema.ts +97 -0
- package/src/server/db/sqlite.ts +10 -0
- package/src/server/index.ts +81 -0
- package/src/server/tools/__tests__/crud.test.ts +411 -0
- package/src/server/tools/__tests__/get-version.test.ts +27 -0
- package/src/server/tools/__tests__/mcp-interface.test.ts +479 -0
- package/src/server/tools/__tests__/query.test.ts +405 -0
- package/src/server/tools/__tests__/z-configure-linear.test.ts +511 -0
- package/src/server/tools/__tests__/z-get-linear-url.test.ts +108 -0
- package/src/server/tools/configure-linear.ts +373 -0
- package/src/server/tools/create-epic.ts +44 -0
- package/src/server/tools/create-prd.ts +40 -0
- package/src/server/tools/create-task.ts +47 -0
- package/src/server/tools/criteria.ts +50 -0
- package/src/server/tools/delete-entity.ts +76 -0
- package/src/server/tools/dependencies.ts +55 -0
- package/src/server/tools/get-entity.ts +240 -0
- package/src/server/tools/get-linear-url.ts +28 -0
- package/src/server/tools/get-stats.ts +52 -0
- package/src/server/tools/get-version.ts +20 -0
- package/src/server/tools/index.ts +158 -0
- package/src/server/tools/init-project.ts +108 -0
- package/src/server/tools/query-entities.ts +167 -0
- package/src/server/tools/render-status.ts +219 -0
- package/src/server/tools/update-entity.ts +140 -0
- package/src/server/tools/update-status.ts +166 -0
- package/src/server/utils/__tests__/mcp-response.test.ts +331 -0
- package/src/server/utils/logger.ts +9 -0
- package/src/server/utils/mcp-response.ts +254 -0
- package/src/server/utils/status-transitions.ts +160 -0
- package/src/status-line/__tests__/status-line.test.ts +215 -0
- package/src/status-line/index.ts +147 -0
- package/src/utils/__tests__/chalk-import.test.ts +32 -0
- package/src/utils/__tests__/display.test.ts +97 -0
- package/src/utils/__tests__/status-renderer.test.ts +310 -0
- package/src/utils/display.ts +62 -0
- package/src/utils/status-renderer.ts +214 -0
- package/src/version.ts +5 -0
- package/dist/server/index.js +0 -87063
- package/skills/prd-template/SKILL.md +0 -242
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Database } from "./sqlite.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate a project prefix from project name.
|
|
5
|
+
* Uses acronym for multi-word names, or truncated uppercase for single words.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* generatePrefix("my-saas-app") // -> "MSA"
|
|
9
|
+
* generatePrefix("flux") // -> "FLUX"
|
|
10
|
+
* generatePrefix("my app") // -> "MA"
|
|
11
|
+
*/
|
|
12
|
+
export function generatePrefix(projectName: string): string {
|
|
13
|
+
const words = projectName
|
|
14
|
+
.replace(/[^a-zA-Z0-9]/g, " ")
|
|
15
|
+
.split(/\s+/)
|
|
16
|
+
.filter((w) => w.length > 0);
|
|
17
|
+
|
|
18
|
+
if (words.length >= 2) {
|
|
19
|
+
return words
|
|
20
|
+
.map((w) => w[0])
|
|
21
|
+
.join("")
|
|
22
|
+
.toUpperCase()
|
|
23
|
+
.slice(0, 10);
|
|
24
|
+
}
|
|
25
|
+
return projectName
|
|
26
|
+
.replace(/[^a-zA-Z0-9]/g, "")
|
|
27
|
+
.toUpperCase()
|
|
28
|
+
.slice(0, 10);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Generate a sequential reference for an entity.
|
|
33
|
+
* Refs follow pattern: PREFIX-TYPE{NUMBER}
|
|
34
|
+
*
|
|
35
|
+
* @param db - Database instance
|
|
36
|
+
* @param type - Entity type: "P" (PRD), "E" (Epic), "T" (Task)
|
|
37
|
+
* @param prefix - Project prefix
|
|
38
|
+
* @returns Sequential ref like "MSA-P1", "MSA-E1", "MSA-T1"
|
|
39
|
+
*/
|
|
40
|
+
export function generateRef(
|
|
41
|
+
db: Database,
|
|
42
|
+
type: "P" | "E" | "T",
|
|
43
|
+
prefix: string,
|
|
44
|
+
): string {
|
|
45
|
+
const table = type === "P" ? "prds" : type === "E" ? "epics" : "tasks";
|
|
46
|
+
const pattern = `${prefix}-${type}%`;
|
|
47
|
+
const result = db
|
|
48
|
+
.query(
|
|
49
|
+
`SELECT ref FROM ${table} WHERE ref LIKE ? ORDER BY CAST(SUBSTR(ref, LENGTH(?) + 1) AS INTEGER) DESC LIMIT 1`,
|
|
50
|
+
)
|
|
51
|
+
.get(pattern, `${prefix}-${type}`) as { ref: string } | null;
|
|
52
|
+
|
|
53
|
+
if (!result) {
|
|
54
|
+
return `${prefix}-${type}1`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const match = result.ref.match(new RegExp(`${prefix}-${type}(\\d+)$`));
|
|
58
|
+
const maxNum = match ? parseInt(match[1], 10) : 0;
|
|
59
|
+
return `${prefix}-${type}${maxNum + 1}`;
|
|
60
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export const SCHEMA = `
|
|
2
|
+
-- Projects table (for multi-project support later)
|
|
3
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
4
|
+
id TEXT PRIMARY KEY,
|
|
5
|
+
name TEXT NOT NULL,
|
|
6
|
+
ref_prefix TEXT NOT NULL,
|
|
7
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
-- PRDs
|
|
11
|
+
CREATE TABLE IF NOT EXISTS prds (
|
|
12
|
+
id TEXT PRIMARY KEY,
|
|
13
|
+
project_id TEXT NOT NULL,
|
|
14
|
+
ref TEXT UNIQUE NOT NULL,
|
|
15
|
+
title TEXT NOT NULL,
|
|
16
|
+
description TEXT,
|
|
17
|
+
status TEXT DEFAULT 'DRAFT',
|
|
18
|
+
tag TEXT,
|
|
19
|
+
folder_path TEXT,
|
|
20
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
21
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
22
|
+
FOREIGN KEY (project_id) REFERENCES projects(id)
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
-- Epics
|
|
26
|
+
CREATE TABLE IF NOT EXISTS epics (
|
|
27
|
+
id TEXT PRIMARY KEY,
|
|
28
|
+
prd_id TEXT NOT NULL,
|
|
29
|
+
ref TEXT UNIQUE NOT NULL,
|
|
30
|
+
title TEXT NOT NULL,
|
|
31
|
+
description TEXT,
|
|
32
|
+
status TEXT DEFAULT 'PENDING',
|
|
33
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
34
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
35
|
+
FOREIGN KEY (prd_id) REFERENCES prds(id)
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
-- Tasks
|
|
39
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
40
|
+
id TEXT PRIMARY KEY,
|
|
41
|
+
epic_id TEXT NOT NULL,
|
|
42
|
+
ref TEXT UNIQUE NOT NULL,
|
|
43
|
+
title TEXT NOT NULL,
|
|
44
|
+
description TEXT,
|
|
45
|
+
status TEXT DEFAULT 'PENDING',
|
|
46
|
+
priority TEXT DEFAULT 'MEDIUM',
|
|
47
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
48
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
49
|
+
FOREIGN KEY (epic_id) REFERENCES epics(id)
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
-- Acceptance Criteria (for both epics and tasks)
|
|
53
|
+
CREATE TABLE IF NOT EXISTS acceptance_criteria (
|
|
54
|
+
id TEXT PRIMARY KEY,
|
|
55
|
+
parent_type TEXT NOT NULL,
|
|
56
|
+
parent_id TEXT NOT NULL,
|
|
57
|
+
criteria TEXT NOT NULL,
|
|
58
|
+
is_met BOOLEAN DEFAULT FALSE,
|
|
59
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
-- Epic Dependencies
|
|
63
|
+
CREATE TABLE IF NOT EXISTS epic_dependencies (
|
|
64
|
+
epic_id TEXT NOT NULL,
|
|
65
|
+
depends_on_epic_id TEXT NOT NULL,
|
|
66
|
+
PRIMARY KEY (epic_id, depends_on_epic_id),
|
|
67
|
+
FOREIGN KEY (epic_id) REFERENCES epics(id),
|
|
68
|
+
FOREIGN KEY (depends_on_epic_id) REFERENCES epics(id)
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
-- Task Dependencies
|
|
72
|
+
CREATE TABLE IF NOT EXISTS task_dependencies (
|
|
73
|
+
task_id TEXT NOT NULL,
|
|
74
|
+
depends_on_task_id TEXT NOT NULL,
|
|
75
|
+
PRIMARY KEY (task_id, depends_on_task_id),
|
|
76
|
+
FOREIGN KEY (task_id) REFERENCES tasks(id),
|
|
77
|
+
FOREIGN KEY (depends_on_task_id) REFERENCES tasks(id)
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
-- PRD Dependencies
|
|
81
|
+
CREATE TABLE IF NOT EXISTS prd_dependencies (
|
|
82
|
+
prd_id TEXT NOT NULL,
|
|
83
|
+
depends_on_prd_id TEXT NOT NULL,
|
|
84
|
+
PRIMARY KEY (prd_id, depends_on_prd_id),
|
|
85
|
+
FOREIGN KEY (prd_id) REFERENCES prds(id),
|
|
86
|
+
FOREIGN KEY (depends_on_prd_id) REFERENCES prds(id)
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
-- Indexes for common queries
|
|
90
|
+
CREATE INDEX IF NOT EXISTS idx_prds_project ON prds(project_id);
|
|
91
|
+
CREATE INDEX IF NOT EXISTS idx_prds_status ON prds(status);
|
|
92
|
+
CREATE INDEX IF NOT EXISTS idx_epics_prd ON epics(prd_id);
|
|
93
|
+
CREATE INDEX IF NOT EXISTS idx_epics_status ON epics(status);
|
|
94
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_epic ON tasks(epic_id);
|
|
95
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
|
96
|
+
CREATE INDEX IF NOT EXISTS idx_criteria_parent ON acceptance_criteria(parent_type, parent_id);
|
|
97
|
+
`;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { config } from "./config.js";
|
|
4
|
+
import { fluxProjectExists, initDb } from "./db/index.js";
|
|
5
|
+
import {
|
|
6
|
+
addCriteriaTool,
|
|
7
|
+
addDependencyTool,
|
|
8
|
+
configureLinearTool,
|
|
9
|
+
createEpicTool,
|
|
10
|
+
createPrdTool,
|
|
11
|
+
createTaskTool,
|
|
12
|
+
deleteEntityTool,
|
|
13
|
+
getEntityTool,
|
|
14
|
+
getStatsTool,
|
|
15
|
+
getVersionTool,
|
|
16
|
+
initProjectTool,
|
|
17
|
+
markCriteriaMetTool,
|
|
18
|
+
queryEntitiesTool,
|
|
19
|
+
registerTools,
|
|
20
|
+
removeDependencyTool,
|
|
21
|
+
renderStatusTool,
|
|
22
|
+
type ToolDefinition,
|
|
23
|
+
updateEntityTool,
|
|
24
|
+
updateStatusTool,
|
|
25
|
+
} from "./tools/index.js";
|
|
26
|
+
import { logger } from "./utils/logger.js";
|
|
27
|
+
|
|
28
|
+
const server = new Server(
|
|
29
|
+
{ name: "flux", version: "0.1.0" },
|
|
30
|
+
{ capabilities: { tools: {} } },
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// All tools
|
|
34
|
+
const tools: ToolDefinition[] = [
|
|
35
|
+
// CRUD tools
|
|
36
|
+
createPrdTool,
|
|
37
|
+
createEpicTool,
|
|
38
|
+
createTaskTool,
|
|
39
|
+
updateEntityTool,
|
|
40
|
+
deleteEntityTool,
|
|
41
|
+
addDependencyTool,
|
|
42
|
+
removeDependencyTool,
|
|
43
|
+
addCriteriaTool,
|
|
44
|
+
markCriteriaMetTool,
|
|
45
|
+
updateStatusTool,
|
|
46
|
+
// Query tools
|
|
47
|
+
getEntityTool,
|
|
48
|
+
queryEntitiesTool,
|
|
49
|
+
initProjectTool,
|
|
50
|
+
getStatsTool,
|
|
51
|
+
getVersionTool,
|
|
52
|
+
// Configuration tools
|
|
53
|
+
configureLinearTool,
|
|
54
|
+
// Display tools
|
|
55
|
+
renderStatusTool,
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
registerTools(server, tools);
|
|
59
|
+
|
|
60
|
+
async function main() {
|
|
61
|
+
logger.info("Flux MCP server starting...");
|
|
62
|
+
logger.info(` Project root: ${config.projectRoot}`);
|
|
63
|
+
logger.info(` Flux path: ${config.fluxPath}`);
|
|
64
|
+
|
|
65
|
+
// Initialize database if flux project exists
|
|
66
|
+
if (fluxProjectExists()) {
|
|
67
|
+
logger.info("Flux project found, initializing database...");
|
|
68
|
+
initDb();
|
|
69
|
+
} else {
|
|
70
|
+
logger.info("No flux project found. Use init_project to create one.");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const transport = new StdioServerTransport();
|
|
74
|
+
await server.connect(transport);
|
|
75
|
+
logger.info("Flux MCP server running");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
main().catch((err) => {
|
|
79
|
+
logger.error("Server failed to start", err);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
});
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
// Set up test environment BEFORE any imports
|
|
6
|
+
const TEST_DIR = `/tmp/flux-test-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
7
|
+
const FLUX_DIR = join(TEST_DIR, ".flux");
|
|
8
|
+
process.env.FLUX_PROJECT_ROOT = TEST_DIR;
|
|
9
|
+
|
|
10
|
+
// Now import modules
|
|
11
|
+
import { clearAdapterCache } from "../../adapters/index.js";
|
|
12
|
+
import { config } from "../../config.js";
|
|
13
|
+
import { closeDb, initDb } from "../../db/index.js";
|
|
14
|
+
import { createEpicTool } from "../create-epic.js";
|
|
15
|
+
import { createPrdTool } from "../create-prd.js";
|
|
16
|
+
import { createTaskTool } from "../create-task.js";
|
|
17
|
+
import { addCriteriaTool, markCriteriaMetTool } from "../criteria.js";
|
|
18
|
+
import { deleteEntityTool } from "../delete-entity.js";
|
|
19
|
+
import { addDependencyTool, removeDependencyTool } from "../dependencies.js";
|
|
20
|
+
import { updateEntityTool } from "../update-entity.js";
|
|
21
|
+
import { updateStatusTool } from "../update-status.js";
|
|
22
|
+
|
|
23
|
+
describe("CRUD MCP Tools", () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
closeDb();
|
|
26
|
+
config.clearCache();
|
|
27
|
+
clearAdapterCache();
|
|
28
|
+
process.env.FLUX_PROJECT_ROOT = TEST_DIR;
|
|
29
|
+
|
|
30
|
+
mkdirSync(FLUX_DIR, { recursive: true });
|
|
31
|
+
writeFileSync(
|
|
32
|
+
join(FLUX_DIR, "project.json"),
|
|
33
|
+
JSON.stringify({ name: "test-project", ref_prefix: "TEST" }),
|
|
34
|
+
);
|
|
35
|
+
initDb();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
closeDb();
|
|
40
|
+
clearAdapterCache();
|
|
41
|
+
config.clearCache();
|
|
42
|
+
if (existsSync(TEST_DIR)) {
|
|
43
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("create_prd", () => {
|
|
48
|
+
test("creates PRD with required fields", async () => {
|
|
49
|
+
const result = (await createPrdTool.handler({
|
|
50
|
+
title: "Test PRD",
|
|
51
|
+
})) as any;
|
|
52
|
+
expect(result).toBeDefined();
|
|
53
|
+
expect(result.title).toBe("Test PRD");
|
|
54
|
+
expect(result.ref).toBeDefined();
|
|
55
|
+
expect(result.status).toBe("DRAFT");
|
|
56
|
+
expect(result.id).toBeDefined();
|
|
57
|
+
expect(result.dependencies).toEqual([]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("creates PRD with optional fields", async () => {
|
|
61
|
+
const result = (await createPrdTool.handler({
|
|
62
|
+
title: "Test PRD",
|
|
63
|
+
description: "A description",
|
|
64
|
+
tag: "mvp",
|
|
65
|
+
})) as any;
|
|
66
|
+
expect(result.description).toBe("A description");
|
|
67
|
+
expect(result.tag).toBe("mvp");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("creates PRD with depends_on", async () => {
|
|
71
|
+
const prd1 = (await createPrdTool.handler({ title: "PRD 1" })) as any;
|
|
72
|
+
const prd2 = (await createPrdTool.handler({ title: "PRD 2" })) as any;
|
|
73
|
+
const prd3 = (await createPrdTool.handler({
|
|
74
|
+
title: "PRD 3",
|
|
75
|
+
depends_on: [prd1.ref, prd2.ref],
|
|
76
|
+
})) as any;
|
|
77
|
+
|
|
78
|
+
expect(prd3.dependencies).toEqual([prd1.ref, prd2.ref]);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("create_epic", () => {
|
|
83
|
+
test("creates epic linked to PRD", async () => {
|
|
84
|
+
const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
|
|
85
|
+
const epic = (await createEpicTool.handler({
|
|
86
|
+
prd_ref: prd.ref,
|
|
87
|
+
title: "Test Epic",
|
|
88
|
+
})) as any;
|
|
89
|
+
|
|
90
|
+
expect(epic.title).toBe("Test Epic");
|
|
91
|
+
expect(epic.ref).toBeDefined();
|
|
92
|
+
expect(epic.status).toBe("PENDING");
|
|
93
|
+
expect(epic.dependencies).toEqual([]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("creates epic with acceptance criteria", async () => {
|
|
97
|
+
const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
|
|
98
|
+
const epic = (await createEpicTool.handler({
|
|
99
|
+
prd_ref: prd.ref,
|
|
100
|
+
title: "Test Epic",
|
|
101
|
+
acceptance_criteria: ["Criterion 1", "Criterion 2"],
|
|
102
|
+
})) as any;
|
|
103
|
+
expect(epic.criteria_count).toBe(2);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("fails with invalid PRD ref", async () => {
|
|
107
|
+
await expect(
|
|
108
|
+
createEpicTool.handler({ prd_ref: "INVALID-P999", title: "Test" }),
|
|
109
|
+
).rejects.toThrow("PRD not found");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("creates epic with depends_on", async () => {
|
|
113
|
+
const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
|
|
114
|
+
const epic1 = (await createEpicTool.handler({
|
|
115
|
+
prd_ref: prd.ref,
|
|
116
|
+
title: "Epic 1",
|
|
117
|
+
})) as any;
|
|
118
|
+
const epic2 = (await createEpicTool.handler({
|
|
119
|
+
prd_ref: prd.ref,
|
|
120
|
+
title: "Epic 2",
|
|
121
|
+
})) as any;
|
|
122
|
+
const epic3 = (await createEpicTool.handler({
|
|
123
|
+
prd_ref: prd.ref,
|
|
124
|
+
title: "Epic 3",
|
|
125
|
+
depends_on: [epic1.ref, epic2.ref],
|
|
126
|
+
})) as any;
|
|
127
|
+
|
|
128
|
+
expect(epic3.dependencies).toEqual([epic1.ref, epic2.ref]);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("create_task", () => {
|
|
133
|
+
test("creates task linked to epic", async () => {
|
|
134
|
+
const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
|
|
135
|
+
const epic = (await createEpicTool.handler({
|
|
136
|
+
prd_ref: prd.ref,
|
|
137
|
+
title: "Test Epic",
|
|
138
|
+
})) as any;
|
|
139
|
+
const task = (await createTaskTool.handler({
|
|
140
|
+
epic_ref: epic.ref,
|
|
141
|
+
title: "Test Task",
|
|
142
|
+
})) as any;
|
|
143
|
+
|
|
144
|
+
expect(task.title).toBe("Test Task");
|
|
145
|
+
expect(task.ref).toBeDefined();
|
|
146
|
+
expect(task.priority).toBe("MEDIUM");
|
|
147
|
+
expect(task.dependencies).toEqual([]);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("creates task with priority", async () => {
|
|
151
|
+
const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
|
|
152
|
+
const epic = (await createEpicTool.handler({
|
|
153
|
+
prd_ref: prd.ref,
|
|
154
|
+
title: "Test Epic",
|
|
155
|
+
})) as any;
|
|
156
|
+
const task = (await createTaskTool.handler({
|
|
157
|
+
epic_ref: epic.ref,
|
|
158
|
+
title: "Test Task",
|
|
159
|
+
priority: "HIGH",
|
|
160
|
+
})) as any;
|
|
161
|
+
expect(task.priority).toBe("HIGH");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("creates task with depends_on", async () => {
|
|
165
|
+
const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
|
|
166
|
+
const epic = (await createEpicTool.handler({
|
|
167
|
+
prd_ref: prd.ref,
|
|
168
|
+
title: "Test Epic",
|
|
169
|
+
})) as any;
|
|
170
|
+
const task1 = (await createTaskTool.handler({
|
|
171
|
+
epic_ref: epic.ref,
|
|
172
|
+
title: "Task 1",
|
|
173
|
+
})) as any;
|
|
174
|
+
const task2 = (await createTaskTool.handler({
|
|
175
|
+
epic_ref: epic.ref,
|
|
176
|
+
title: "Task 2",
|
|
177
|
+
})) as any;
|
|
178
|
+
const task3 = (await createTaskTool.handler({
|
|
179
|
+
epic_ref: epic.ref,
|
|
180
|
+
title: "Task 3",
|
|
181
|
+
depends_on: [task1.ref, task2.ref],
|
|
182
|
+
})) as any;
|
|
183
|
+
|
|
184
|
+
expect(task3.dependencies).toEqual([task1.ref, task2.ref]);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe("update_entity", () => {
|
|
189
|
+
test("updates PRD fields", async () => {
|
|
190
|
+
const prd = (await createPrdTool.handler({ title: "Original" })) as any;
|
|
191
|
+
const updated = (await updateEntityTool.handler({
|
|
192
|
+
ref: prd.ref,
|
|
193
|
+
fields: { title: "Updated", status: "APPROVED" },
|
|
194
|
+
})) as any;
|
|
195
|
+
|
|
196
|
+
expect(updated.title).toBe("Updated");
|
|
197
|
+
expect(updated.status).toBe("APPROVED");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("updates epic status", async () => {
|
|
201
|
+
const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
|
|
202
|
+
const epic = (await createEpicTool.handler({
|
|
203
|
+
prd_ref: prd.ref,
|
|
204
|
+
title: "Test Epic",
|
|
205
|
+
})) as any;
|
|
206
|
+
const updated = (await updateEntityTool.handler({
|
|
207
|
+
ref: epic.ref,
|
|
208
|
+
fields: { status: "IN_PROGRESS" },
|
|
209
|
+
})) as any;
|
|
210
|
+
|
|
211
|
+
expect(updated.status).toBe("IN_PROGRESS");
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe("delete_entity", () => {
|
|
216
|
+
test("deletes task", async () => {
|
|
217
|
+
const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
|
|
218
|
+
const epic = (await createEpicTool.handler({
|
|
219
|
+
prd_ref: prd.ref,
|
|
220
|
+
title: "Test Epic",
|
|
221
|
+
})) as any;
|
|
222
|
+
const task = (await createTaskTool.handler({
|
|
223
|
+
epic_ref: epic.ref,
|
|
224
|
+
title: "Test Task",
|
|
225
|
+
})) as any;
|
|
226
|
+
|
|
227
|
+
const result = (await deleteEntityTool.handler({ ref: task.ref })) as any;
|
|
228
|
+
expect(result.deleted).toBe(task.ref);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("cascade deletes epic with tasks", async () => {
|
|
232
|
+
const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
|
|
233
|
+
const epic = (await createEpicTool.handler({
|
|
234
|
+
prd_ref: prd.ref,
|
|
235
|
+
title: "Test Epic",
|
|
236
|
+
})) as any;
|
|
237
|
+
await createTaskTool.handler({ epic_ref: epic.ref, title: "Task 1" });
|
|
238
|
+
await createTaskTool.handler({ epic_ref: epic.ref, title: "Task 2" });
|
|
239
|
+
|
|
240
|
+
const result = (await deleteEntityTool.handler({ ref: epic.ref })) as any;
|
|
241
|
+
expect(result.cascade.tasks).toBe(2);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("cascade deletes PRD with dependencies", async () => {
|
|
245
|
+
const prd1 = (await createPrdTool.handler({ title: "PRD 1" })) as any;
|
|
246
|
+
const prd2 = (await createPrdTool.handler({ title: "PRD 2" })) as any;
|
|
247
|
+
await addDependencyTool.handler({
|
|
248
|
+
ref: prd2.ref,
|
|
249
|
+
depends_on_ref: prd1.ref,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const result = (await deleteEntityTool.handler({ ref: prd1.ref })) as any;
|
|
253
|
+
expect(result.deleted).toBe(prd1.ref);
|
|
254
|
+
expect(result.cascade.dependencies).toBe(1);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe("dependencies", () => {
|
|
259
|
+
test("adds and removes epic dependency", async () => {
|
|
260
|
+
const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
|
|
261
|
+
const epic1 = (await createEpicTool.handler({
|
|
262
|
+
prd_ref: prd.ref,
|
|
263
|
+
title: "Epic 1",
|
|
264
|
+
})) as any;
|
|
265
|
+
const epic2 = (await createEpicTool.handler({
|
|
266
|
+
prd_ref: prd.ref,
|
|
267
|
+
title: "Epic 2",
|
|
268
|
+
})) as any;
|
|
269
|
+
|
|
270
|
+
const addResult = (await addDependencyTool.handler({
|
|
271
|
+
ref: epic2.ref,
|
|
272
|
+
depends_on_ref: epic1.ref,
|
|
273
|
+
})) as any;
|
|
274
|
+
expect(addResult.success).toBe(true);
|
|
275
|
+
|
|
276
|
+
const removeResult = (await removeDependencyTool.handler({
|
|
277
|
+
ref: epic2.ref,
|
|
278
|
+
depends_on_ref: epic1.ref,
|
|
279
|
+
})) as any;
|
|
280
|
+
expect(removeResult.success).toBe(true);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("adds and removes PRD dependency", async () => {
|
|
284
|
+
const prd1 = (await createPrdTool.handler({ title: "PRD 1" })) as any;
|
|
285
|
+
const prd2 = (await createPrdTool.handler({ title: "PRD 2" })) as any;
|
|
286
|
+
|
|
287
|
+
const addResult = (await addDependencyTool.handler({
|
|
288
|
+
ref: prd2.ref,
|
|
289
|
+
depends_on_ref: prd1.ref,
|
|
290
|
+
})) as any;
|
|
291
|
+
expect(addResult.success).toBe(true);
|
|
292
|
+
expect(addResult.ref).toBe(prd2.ref);
|
|
293
|
+
expect(addResult.depends_on).toBe(prd1.ref);
|
|
294
|
+
|
|
295
|
+
const removeResult = (await removeDependencyTool.handler({
|
|
296
|
+
ref: prd2.ref,
|
|
297
|
+
depends_on_ref: prd1.ref,
|
|
298
|
+
})) as any;
|
|
299
|
+
expect(removeResult.success).toBe(true);
|
|
300
|
+
expect(removeResult.removed_dependency).toBe(prd1.ref);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("prevents self-dependency", async () => {
|
|
304
|
+
const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
|
|
305
|
+
const epic = (await createEpicTool.handler({
|
|
306
|
+
prd_ref: prd.ref,
|
|
307
|
+
title: "Epic",
|
|
308
|
+
})) as any;
|
|
309
|
+
|
|
310
|
+
await expect(
|
|
311
|
+
addDependencyTool.handler({ ref: epic.ref, depends_on_ref: epic.ref }),
|
|
312
|
+
).rejects.toThrow("depend on itself");
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("prevents PRD self-dependency", async () => {
|
|
316
|
+
const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
|
|
317
|
+
|
|
318
|
+
await expect(
|
|
319
|
+
addDependencyTool.handler({ ref: prd.ref, depends_on_ref: prd.ref }),
|
|
320
|
+
).rejects.toThrow("depend on itself");
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("prevents cross-type dependencies between PRD and Epic", async () => {
|
|
324
|
+
const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
|
|
325
|
+
const epic = (await createEpicTool.handler({
|
|
326
|
+
prd_ref: prd.ref,
|
|
327
|
+
title: "Epic",
|
|
328
|
+
})) as any;
|
|
329
|
+
|
|
330
|
+
await expect(
|
|
331
|
+
addDependencyTool.handler({ ref: prd.ref, depends_on_ref: epic.ref }),
|
|
332
|
+
).rejects.toThrow("same type");
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe("criteria", () => {
|
|
337
|
+
test("adds and marks criterion", async () => {
|
|
338
|
+
const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
|
|
339
|
+
const epic = (await createEpicTool.handler({
|
|
340
|
+
prd_ref: prd.ref,
|
|
341
|
+
title: "Test Epic",
|
|
342
|
+
})) as any;
|
|
343
|
+
|
|
344
|
+
const criterion = (await addCriteriaTool.handler({
|
|
345
|
+
parent_ref: epic.ref,
|
|
346
|
+
criteria: "Test criterion",
|
|
347
|
+
})) as any;
|
|
348
|
+
expect(criterion.criteria).toBe("Test criterion");
|
|
349
|
+
expect(criterion.is_met).toBe(false);
|
|
350
|
+
|
|
351
|
+
const updated = (await markCriteriaMetTool.handler({
|
|
352
|
+
criteria_id: criterion.id,
|
|
353
|
+
})) as any;
|
|
354
|
+
expect(updated.is_met).toBe(true);
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
describe("update_status", () => {
|
|
359
|
+
test("updates PRD status with valid transition", async () => {
|
|
360
|
+
const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
|
|
361
|
+
// DRAFT → PENDING_REVIEW is a valid transition
|
|
362
|
+
const updated = (await updateStatusTool.handler({
|
|
363
|
+
ref: prd.ref,
|
|
364
|
+
status: "PENDING_REVIEW",
|
|
365
|
+
})) as any;
|
|
366
|
+
expect(updated.status).toBe("PENDING_REVIEW");
|
|
367
|
+
expect(updated.available_transitions).toContain("REVIEWED");
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test("rejects invalid status for entity type", async () => {
|
|
371
|
+
const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
|
|
372
|
+
await expect(
|
|
373
|
+
updateStatusTool.handler({ ref: prd.ref, status: "IN_PROGRESS" }),
|
|
374
|
+
).rejects.toThrow("Invalid status");
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test("rejects invalid status transition", async () => {
|
|
378
|
+
const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
|
|
379
|
+
// DRAFT → APPROVED is not a valid transition (must go through PENDING_REVIEW first)
|
|
380
|
+
await expect(
|
|
381
|
+
updateStatusTool.handler({ ref: prd.ref, status: "APPROVED" }),
|
|
382
|
+
).rejects.toThrow("Invalid transition");
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("updates task status with valid transitions", async () => {
|
|
386
|
+
const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
|
|
387
|
+
const epic = (await createEpicTool.handler({
|
|
388
|
+
prd_ref: prd.ref,
|
|
389
|
+
title: "Test Epic",
|
|
390
|
+
})) as any;
|
|
391
|
+
const task = (await createTaskTool.handler({
|
|
392
|
+
epic_ref: epic.ref,
|
|
393
|
+
title: "Test Task",
|
|
394
|
+
})) as any;
|
|
395
|
+
|
|
396
|
+
// PENDING → IN_PROGRESS is valid
|
|
397
|
+
const inProgress = (await updateStatusTool.handler({
|
|
398
|
+
ref: task.ref,
|
|
399
|
+
status: "IN_PROGRESS",
|
|
400
|
+
})) as any;
|
|
401
|
+
expect(inProgress.status).toBe("IN_PROGRESS");
|
|
402
|
+
|
|
403
|
+
// IN_PROGRESS → COMPLETED is valid
|
|
404
|
+
const completed = (await updateStatusTool.handler({
|
|
405
|
+
ref: task.ref,
|
|
406
|
+
status: "COMPLETED",
|
|
407
|
+
})) as any;
|
|
408
|
+
expect(completed.status).toBe("COMPLETED");
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { VERSION } from "../../../version.js";
|
|
3
|
+
import { getVersionTool } from "../get-version.js";
|
|
4
|
+
|
|
5
|
+
describe("get_version", () => {
|
|
6
|
+
test("returns version and name", async () => {
|
|
7
|
+
const result = (await getVersionTool.handler({})) as any;
|
|
8
|
+
|
|
9
|
+
expect(result.version).toBe(VERSION);
|
|
10
|
+
expect(result.name).toBe("flux-plugin");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("requires no input parameters", async () => {
|
|
14
|
+
// Should work with empty object
|
|
15
|
+
const result = (await getVersionTool.handler({})) as any;
|
|
16
|
+
expect(result).toBeDefined();
|
|
17
|
+
expect(result.version).toBeDefined();
|
|
18
|
+
expect(result.name).toBeDefined();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("has correct tool definition", () => {
|
|
22
|
+
expect(getVersionTool.name).toBe("get_version");
|
|
23
|
+
expect(getVersionTool.description).toContain("version");
|
|
24
|
+
expect(getVersionTool.inputSchema).toBeDefined();
|
|
25
|
+
expect(getVersionTool.handler).toBeDefined();
|
|
26
|
+
});
|
|
27
|
+
});
|