@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.
Files changed (108) hide show
  1. package/README.md +11 -7
  2. package/agents/coder.md +150 -25
  3. package/bin/install.cjs +171 -16
  4. package/commands/breakdown.md +47 -10
  5. package/commands/dashboard.md +29 -0
  6. package/commands/flux.md +92 -12
  7. package/commands/implement.md +166 -17
  8. package/commands/linear.md +6 -5
  9. package/commands/prd.md +996 -82
  10. package/manifest.json +2 -1
  11. package/package.json +9 -11
  12. package/skills/flux-orchestrator/SKILL.md +11 -3
  13. package/skills/prd-writer/SKILL.md +761 -0
  14. package/skills/ux-ui-design/SKILL.md +346 -0
  15. package/skills/ux-ui-design/references/design-tokens.md +359 -0
  16. package/src/__tests__/version.test.ts +37 -0
  17. package/src/adapters/local/.gitkeep +0 -0
  18. package/src/dashboard/__tests__/api.test.ts +211 -0
  19. package/src/dashboard/browser.ts +35 -0
  20. package/src/dashboard/public/app.js +869 -0
  21. package/src/dashboard/public/index.html +90 -0
  22. package/src/dashboard/public/styles.css +807 -0
  23. package/src/dashboard/public/vendor/highlight.css +10 -0
  24. package/src/dashboard/public/vendor/highlight.min.js +8422 -0
  25. package/src/dashboard/public/vendor/marked.min.js +2210 -0
  26. package/src/dashboard/server.ts +296 -0
  27. package/src/dashboard/watchers.ts +83 -0
  28. package/src/server/__tests__/config.test.ts +163 -0
  29. package/src/server/adapters/__tests__/a-client-linear.test.ts +197 -0
  30. package/src/server/adapters/__tests__/adapter-factory.test.ts +230 -0
  31. package/src/server/adapters/__tests__/dependency-ops.test.ts +429 -0
  32. package/src/server/adapters/__tests__/document-ops.test.ts +306 -0
  33. package/src/server/adapters/__tests__/linear-adapter.test.ts +91 -0
  34. package/src/server/adapters/__tests__/linear-config.test.ts +425 -0
  35. package/src/server/adapters/__tests__/linear-criteria-parser.test.ts +287 -0
  36. package/src/server/adapters/__tests__/linear-description-test.ts +238 -0
  37. package/src/server/adapters/__tests__/linear-epic-crud.test.ts +496 -0
  38. package/src/server/adapters/__tests__/linear-mappers-description.test.ts +276 -0
  39. package/src/server/adapters/__tests__/linear-mappers-epic.test.ts +294 -0
  40. package/src/server/adapters/__tests__/linear-mappers-prd.test.ts +300 -0
  41. package/src/server/adapters/__tests__/linear-mappers-task.test.ts +197 -0
  42. package/src/server/adapters/__tests__/linear-prd-crud.test.ts +620 -0
  43. package/src/server/adapters/__tests__/linear-stats.test.ts +450 -0
  44. package/src/server/adapters/__tests__/linear-task-crud.test.ts +534 -0
  45. package/src/server/adapters/__tests__/linear-types.test.ts +243 -0
  46. package/src/server/adapters/__tests__/status-ops.test.ts +441 -0
  47. package/src/server/adapters/factory.ts +90 -0
  48. package/src/server/adapters/index.ts +9 -0
  49. package/src/server/adapters/linear/adapter.ts +1141 -0
  50. package/src/server/adapters/linear/client.ts +169 -0
  51. package/src/server/adapters/linear/config.ts +152 -0
  52. package/src/server/adapters/linear/helpers/criteria-parser.ts +197 -0
  53. package/src/server/adapters/linear/helpers/index.ts +7 -0
  54. package/src/server/adapters/linear/index.ts +16 -0
  55. package/src/server/adapters/linear/mappers/description.ts +136 -0
  56. package/src/server/adapters/linear/mappers/epic.ts +81 -0
  57. package/src/server/adapters/linear/mappers/index.ts +27 -0
  58. package/src/server/adapters/linear/mappers/prd.ts +178 -0
  59. package/src/server/adapters/linear/mappers/task.ts +82 -0
  60. package/src/server/adapters/linear/types.ts +264 -0
  61. package/src/server/adapters/local-adapter.ts +1009 -0
  62. package/src/server/adapters/types.ts +293 -0
  63. package/src/server/config.ts +73 -0
  64. package/src/server/db/__tests__/queries.test.ts +473 -0
  65. package/src/server/db/ids.ts +17 -0
  66. package/src/server/db/index.ts +69 -0
  67. package/src/server/db/queries.ts +142 -0
  68. package/src/server/db/refs.ts +60 -0
  69. package/src/server/db/schema.ts +97 -0
  70. package/src/server/db/sqlite.ts +10 -0
  71. package/src/server/index.ts +81 -0
  72. package/src/server/tools/__tests__/crud.test.ts +411 -0
  73. package/src/server/tools/__tests__/get-version.test.ts +27 -0
  74. package/src/server/tools/__tests__/mcp-interface.test.ts +479 -0
  75. package/src/server/tools/__tests__/query.test.ts +405 -0
  76. package/src/server/tools/__tests__/z-configure-linear.test.ts +511 -0
  77. package/src/server/tools/__tests__/z-get-linear-url.test.ts +108 -0
  78. package/src/server/tools/configure-linear.ts +373 -0
  79. package/src/server/tools/create-epic.ts +44 -0
  80. package/src/server/tools/create-prd.ts +40 -0
  81. package/src/server/tools/create-task.ts +47 -0
  82. package/src/server/tools/criteria.ts +50 -0
  83. package/src/server/tools/delete-entity.ts +76 -0
  84. package/src/server/tools/dependencies.ts +55 -0
  85. package/src/server/tools/get-entity.ts +240 -0
  86. package/src/server/tools/get-linear-url.ts +28 -0
  87. package/src/server/tools/get-stats.ts +52 -0
  88. package/src/server/tools/get-version.ts +20 -0
  89. package/src/server/tools/index.ts +158 -0
  90. package/src/server/tools/init-project.ts +108 -0
  91. package/src/server/tools/query-entities.ts +167 -0
  92. package/src/server/tools/render-status.ts +219 -0
  93. package/src/server/tools/update-entity.ts +140 -0
  94. package/src/server/tools/update-status.ts +166 -0
  95. package/src/server/utils/__tests__/mcp-response.test.ts +331 -0
  96. package/src/server/utils/logger.ts +9 -0
  97. package/src/server/utils/mcp-response.ts +254 -0
  98. package/src/server/utils/status-transitions.ts +160 -0
  99. package/src/status-line/__tests__/status-line.test.ts +215 -0
  100. package/src/status-line/index.ts +147 -0
  101. package/src/utils/__tests__/chalk-import.test.ts +32 -0
  102. package/src/utils/__tests__/display.test.ts +97 -0
  103. package/src/utils/__tests__/status-renderer.test.ts +310 -0
  104. package/src/utils/display.ts +62 -0
  105. package/src/utils/status-renderer.ts +214 -0
  106. package/src/version.ts +5 -0
  107. package/dist/server/index.js +0 -87063
  108. package/skills/prd-template/SKILL.md +0 -242
@@ -0,0 +1,405 @@
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-query-${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 { addDependencyTool } from "../dependencies.js";
18
+ import { getEntityTool } from "../get-entity.js";
19
+ import { getStatsTool } from "../get-stats.js";
20
+ import { initProjectTool } from "../init-project.js";
21
+ import { queryEntitiesTool } from "../query-entities.js";
22
+
23
+ describe("Query 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("get_entity", () => {
48
+ test("gets PRD by ref", async () => {
49
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
50
+ const result = (await getEntityTool.handler({ ref: prd.ref })) as any;
51
+
52
+ expect(result.id).toBe(prd.id);
53
+ expect(result.title).toBe("Test PRD");
54
+ expect(result.ref).toBe(prd.ref);
55
+ });
56
+
57
+ test("gets epic with tasks include", async () => {
58
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
59
+ const epic = (await createEpicTool.handler({
60
+ prd_ref: prd.ref,
61
+ title: "Test Epic",
62
+ })) as any;
63
+ await createTaskTool.handler({ epic_ref: epic.ref, title: "Task 1" });
64
+ await createTaskTool.handler({ epic_ref: epic.ref, title: "Task 2" });
65
+
66
+ const result = (await getEntityTool.handler({
67
+ ref: epic.ref,
68
+ include: ["tasks"],
69
+ })) as any;
70
+
71
+ expect(result.tasks).toBeDefined();
72
+ expect(result.tasks.length).toBe(2);
73
+ });
74
+
75
+ test("gets entity with criteria include", async () => {
76
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
77
+ const epic = (await createEpicTool.handler({
78
+ prd_ref: prd.ref,
79
+ title: "Test Epic",
80
+ acceptance_criteria: ["Criterion 1", "Criterion 2"],
81
+ })) as any;
82
+
83
+ const result = (await getEntityTool.handler({
84
+ ref: epic.ref,
85
+ include: ["criteria"],
86
+ })) as any;
87
+
88
+ expect(result.criteria).toBeDefined();
89
+ expect(result.criteria.length).toBe(2);
90
+ });
91
+
92
+ test("gets entity with dependencies include", async () => {
93
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
94
+ const epic1 = (await createEpicTool.handler({
95
+ prd_ref: prd.ref,
96
+ title: "Epic 1",
97
+ })) as any;
98
+ const epic2 = (await createEpicTool.handler({
99
+ prd_ref: prd.ref,
100
+ title: "Epic 2",
101
+ })) as any;
102
+ await addDependencyTool.handler({
103
+ ref: epic2.ref,
104
+ depends_on_ref: epic1.ref,
105
+ });
106
+
107
+ const result = (await getEntityTool.handler({
108
+ ref: epic2.ref,
109
+ include: ["dependencies"],
110
+ })) as any;
111
+
112
+ expect(result.dependencies).toBeDefined();
113
+ expect(result.dependencies.length).toBe(1);
114
+ // Dependencies now return refs directly, not objects
115
+ expect(result.dependencies[0]).toBe(epic1.ref);
116
+ });
117
+
118
+ test("returns dependencies by default for PRD without include", async () => {
119
+ const prd1 = (await createPrdTool.handler({ title: "PRD 1" })) as any;
120
+ const prd2 = (await createPrdTool.handler({ title: "PRD 2" })) as any;
121
+ await addDependencyTool.handler({
122
+ ref: prd2.ref,
123
+ depends_on_ref: prd1.ref,
124
+ });
125
+
126
+ const result = (await getEntityTool.handler({
127
+ ref: prd2.ref,
128
+ })) as any;
129
+
130
+ expect(result.dependencies).toBeDefined();
131
+ expect(result.dependencies.length).toBe(1);
132
+ expect(result.dependencies[0]).toBe(prd1.ref);
133
+ });
134
+
135
+ test("returns dependencies by default for Epic without include", async () => {
136
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
137
+ const epic1 = (await createEpicTool.handler({
138
+ prd_ref: prd.ref,
139
+ title: "Epic 1",
140
+ })) as any;
141
+ const epic2 = (await createEpicTool.handler({
142
+ prd_ref: prd.ref,
143
+ title: "Epic 2",
144
+ })) as any;
145
+ await addDependencyTool.handler({
146
+ ref: epic2.ref,
147
+ depends_on_ref: epic1.ref,
148
+ });
149
+
150
+ const result = (await getEntityTool.handler({
151
+ ref: epic2.ref,
152
+ })) as any;
153
+
154
+ expect(result.dependencies).toBeDefined();
155
+ expect(result.dependencies.length).toBe(1);
156
+ expect(result.dependencies[0]).toBe(epic1.ref);
157
+ });
158
+
159
+ test("returns dependencies by default for Task without include", async () => {
160
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
161
+ const epic = (await createEpicTool.handler({
162
+ prd_ref: prd.ref,
163
+ title: "Test Epic",
164
+ })) as any;
165
+ const task1 = (await createTaskTool.handler({
166
+ epic_ref: epic.ref,
167
+ title: "Task 1",
168
+ })) as any;
169
+ const task2 = (await createTaskTool.handler({
170
+ epic_ref: epic.ref,
171
+ title: "Task 2",
172
+ })) as any;
173
+ await addDependencyTool.handler({
174
+ ref: task2.ref,
175
+ depends_on_ref: task1.ref,
176
+ });
177
+
178
+ const result = (await getEntityTool.handler({
179
+ ref: task2.ref,
180
+ })) as any;
181
+
182
+ expect(result.dependencies).toBeDefined();
183
+ expect(result.dependencies.length).toBe(1);
184
+ expect(result.dependencies[0]).toBe(task1.ref);
185
+ });
186
+
187
+ test("throws error for invalid ref", async () => {
188
+ await expect(
189
+ getEntityTool.handler({ ref: "INVALID-P999" }),
190
+ ).rejects.toThrow("Entity not found");
191
+ });
192
+ });
193
+
194
+ describe("query_entities", () => {
195
+ test("queries all PRDs", async () => {
196
+ await createPrdTool.handler({ title: "PRD 1" });
197
+ await createPrdTool.handler({ title: "PRD 2" });
198
+
199
+ const result = (await queryEntitiesTool.handler({
200
+ type: "prd",
201
+ })) as any;
202
+ expect(result.items.length).toBe(2);
203
+ expect(result.total).toBe(2);
204
+ expect(result.has_more).toBe(false);
205
+ });
206
+
207
+ test("filters PRDs by status", async () => {
208
+ const prd1 = (await createPrdTool.handler({ title: "PRD 1" })) as any;
209
+ await createPrdTool.handler({ title: "PRD 2" });
210
+
211
+ // Update first PRD to APPROVED
212
+ const { updateEntityTool } = await import("../update-entity.js");
213
+ await updateEntityTool.handler({
214
+ ref: prd1.ref,
215
+ fields: { status: "APPROVED" },
216
+ });
217
+
218
+ const result = (await queryEntitiesTool.handler({
219
+ type: "prd",
220
+ status: "DRAFT",
221
+ })) as any;
222
+ expect(result.items.length).toBe(1);
223
+ expect(result.total).toBe(1);
224
+ });
225
+
226
+ test("queries epics by PRD ref", async () => {
227
+ const prd1 = (await createPrdTool.handler({ title: "PRD 1" })) as any;
228
+ const prd2 = (await createPrdTool.handler({ title: "PRD 2" })) as any;
229
+ await createEpicTool.handler({ prd_ref: prd1.ref, title: "Epic 1" });
230
+ await createEpicTool.handler({ prd_ref: prd1.ref, title: "Epic 2" });
231
+ await createEpicTool.handler({ prd_ref: prd2.ref, title: "Epic 3" });
232
+
233
+ const result = (await queryEntitiesTool.handler({
234
+ type: "epic",
235
+ prd_ref: prd1.ref,
236
+ })) as any;
237
+ expect(result.items.length).toBe(2);
238
+ expect(result.total).toBe(2);
239
+ });
240
+
241
+ test("queries tasks by epic ref", async () => {
242
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
243
+ const epic1 = (await createEpicTool.handler({
244
+ prd_ref: prd.ref,
245
+ title: "Epic 1",
246
+ })) as any;
247
+ const epic2 = (await createEpicTool.handler({
248
+ prd_ref: prd.ref,
249
+ title: "Epic 2",
250
+ })) as any;
251
+ await createTaskTool.handler({ epic_ref: epic1.ref, title: "Task 1" });
252
+ await createTaskTool.handler({ epic_ref: epic1.ref, title: "Task 2" });
253
+ await createTaskTool.handler({ epic_ref: epic2.ref, title: "Task 3" });
254
+
255
+ const result = (await queryEntitiesTool.handler({
256
+ type: "task",
257
+ epic_ref: epic1.ref,
258
+ })) as any;
259
+ expect(result.items.length).toBe(2);
260
+ expect(result.total).toBe(2);
261
+ });
262
+
263
+ test("includes criteria counts with entities", async () => {
264
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
265
+ const _epic = (await createEpicTool.handler({
266
+ prd_ref: prd.ref,
267
+ title: "Test Epic",
268
+ acceptance_criteria: ["Criterion 1"],
269
+ })) as any;
270
+
271
+ const result = (await queryEntitiesTool.handler({
272
+ type: "epic",
273
+ include: ["criteria"],
274
+ })) as any;
275
+
276
+ // Now returns counts instead of full criteria array
277
+ expect(result.items[0].criteria_count).toBe(1);
278
+ expect(result.items[0].criteria_met).toBe(0);
279
+ });
280
+
281
+ test("supports pagination with limit and offset", async () => {
282
+ const prd = (await createPrdTool.handler({ title: "Test PRD" })) as any;
283
+ for (let i = 1; i <= 5; i++) {
284
+ await createEpicTool.handler({ prd_ref: prd.ref, title: `Epic ${i}` });
285
+ }
286
+
287
+ const page1 = (await queryEntitiesTool.handler({
288
+ type: "epic",
289
+ limit: 2,
290
+ offset: 0,
291
+ })) as any;
292
+ expect(page1.items.length).toBe(2);
293
+ expect(page1.total).toBe(5);
294
+ expect(page1.has_more).toBe(true);
295
+
296
+ const page2 = (await queryEntitiesTool.handler({
297
+ type: "epic",
298
+ limit: 2,
299
+ offset: 2,
300
+ })) as any;
301
+ expect(page2.items.length).toBe(2);
302
+ expect(page2.has_more).toBe(true);
303
+
304
+ const page3 = (await queryEntitiesTool.handler({
305
+ type: "epic",
306
+ limit: 2,
307
+ offset: 4,
308
+ })) as any;
309
+ expect(page3.items.length).toBe(1);
310
+ expect(page3.has_more).toBe(false);
311
+ });
312
+ });
313
+
314
+ describe("get_stats", () => {
315
+ test("returns zeroes for empty project", async () => {
316
+ const result = (await getStatsTool.handler({})) as any;
317
+
318
+ expect(result.prds.total).toBe(0);
319
+ expect(result.epics.total).toBe(0);
320
+ expect(result.tasks.total).toBe(0);
321
+ });
322
+
323
+ test("counts entities by status", async () => {
324
+ const prd = (await createPrdTool.handler({ title: "PRD 1" })) as any;
325
+ const epic = (await createEpicTool.handler({
326
+ prd_ref: prd.ref,
327
+ title: "Epic 1",
328
+ })) as any;
329
+ await createTaskTool.handler({ epic_ref: epic.ref, title: "Task 1" });
330
+ await createTaskTool.handler({ epic_ref: epic.ref, title: "Task 2" });
331
+
332
+ const result = (await getStatsTool.handler({})) as any;
333
+
334
+ expect(result.prds.total).toBe(1);
335
+ expect(result.prds.draft).toBe(1);
336
+ expect(result.epics.total).toBe(1);
337
+ expect(result.epics.pending).toBe(1);
338
+ expect(result.tasks.total).toBe(2);
339
+ expect(result.tasks.pending).toBe(2);
340
+ });
341
+ });
342
+ });
343
+
344
+ describe("init_project", () => {
345
+ const INIT_TEST_DIR = `/tmp/flux-init-test-${Date.now()}-${Math.random().toString(36).slice(2)}`;
346
+
347
+ beforeEach(() => {
348
+ closeDb();
349
+ config.clearCache();
350
+ clearAdapterCache();
351
+ process.env.FLUX_PROJECT_ROOT = INIT_TEST_DIR;
352
+ mkdirSync(INIT_TEST_DIR, { recursive: true });
353
+ });
354
+
355
+ afterEach(() => {
356
+ closeDb();
357
+ clearAdapterCache();
358
+ config.clearCache();
359
+ if (existsSync(INIT_TEST_DIR)) {
360
+ rmSync(INIT_TEST_DIR, { recursive: true, force: true });
361
+ }
362
+ });
363
+
364
+ test("initializes new project", async () => {
365
+ const result = (await initProjectTool.handler({
366
+ name: "My Project",
367
+ vision: "Build something great",
368
+ })) as any;
369
+
370
+ expect(result.success).toBe(true);
371
+ expect(result.project.name).toBe("My Project");
372
+ expect(result.project.vision).toBe("Build something great");
373
+ expect(result.project.ref_prefix).toBeDefined();
374
+ expect(existsSync(join(INIT_TEST_DIR, ".flux"))).toBe(true);
375
+ expect(existsSync(join(INIT_TEST_DIR, ".flux", "project.json"))).toBe(true);
376
+ expect(existsSync(join(INIT_TEST_DIR, ".flux", "flux.db"))).toBe(true);
377
+ });
378
+
379
+ test("generates ref prefix from project name", async () => {
380
+ const result = (await initProjectTool.handler({
381
+ name: "My Awesome App",
382
+ vision: "Test vision",
383
+ })) as any;
384
+
385
+ // Should generate prefix like "MAA" or "MYAWE"
386
+ expect(result.project.ref_prefix).toBeDefined();
387
+ expect(result.project.ref_prefix.length).toBeGreaterThan(0);
388
+ });
389
+
390
+ test("fails if already initialized", async () => {
391
+ // First init
392
+ await initProjectTool.handler({
393
+ name: "First Project",
394
+ vision: "First vision",
395
+ });
396
+
397
+ // Second init should fail
398
+ await expect(
399
+ initProjectTool.handler({
400
+ name: "Second Project",
401
+ vision: "Second vision",
402
+ }),
403
+ ).rejects.toThrow("already initialized");
404
+ });
405
+ });