@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,429 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+ import type { HydratedIssue, LinearAdapter } from "../linear/adapter.js";
3
+ import type { LinearConfig } from "../linear/types.js";
4
+
5
+ /**
6
+ * Helper to create a mock HydratedIssue
7
+ */
8
+ function createMockIssue(
9
+ overrides: Partial<HydratedIssue> = {},
10
+ ): HydratedIssue {
11
+ return {
12
+ id: "issue_abc123",
13
+ identifier: "ENG-42",
14
+ title: "Test Issue",
15
+ description: "Test description",
16
+ stateName: "Backlog",
17
+ stateType: "backlog",
18
+ labels: ["epic"],
19
+ parentIdentifier: undefined,
20
+ priority: 3,
21
+ createdAt: new Date("2024-01-01T00:00:00Z"),
22
+ updatedAt: new Date("2024-01-01T00:00:00Z"),
23
+ _raw: {},
24
+ ...overrides,
25
+ };
26
+ }
27
+
28
+ describe("LinearAdapter - Dependency Operations", () => {
29
+ const mockConfig: LinearConfig = {
30
+ apiKey: "lin_api_test123",
31
+ teamId: "team_abc",
32
+ projectId: "proj_container",
33
+ defaultLabels: {
34
+ prd: "prd",
35
+ epic: "epic",
36
+ task: "task",
37
+ },
38
+ };
39
+
40
+ let adapter: LinearAdapter;
41
+
42
+ beforeEach(async () => {
43
+ const { LinearAdapter: LA } = await import("../linear/adapter.js");
44
+ adapter = new LA(mockConfig);
45
+ });
46
+
47
+ describe("addDependency", () => {
48
+ test("creates blocked relation between two epics", async () => {
49
+ const blockerIssue = createMockIssue({
50
+ id: "issue_epic_1",
51
+ identifier: "ENG-42",
52
+ title: "Epic 1",
53
+ labels: ["epic"],
54
+ parentIdentifier: undefined,
55
+ });
56
+
57
+ const blockedIssue = createMockIssue({
58
+ id: "issue_epic_2",
59
+ identifier: "ENG-43",
60
+ title: "Epic 2",
61
+ labels: ["epic"],
62
+ parentIdentifier: undefined,
63
+ });
64
+
65
+ let fetchCallCount = 0;
66
+ (adapter as any).fetchIssue = mock(async () => {
67
+ fetchCallCount++;
68
+ if (fetchCallCount === 1) return blockedIssue;
69
+ return blockerIssue;
70
+ });
71
+
72
+ const mockCreateRelation = mock(async () => ({
73
+ id: "relation_1",
74
+ type: "blocks",
75
+ }));
76
+
77
+ (adapter as any).client = {
78
+ execute: mock(async (fn: () => Promise<any>) => fn()),
79
+ client: {
80
+ createIssueRelation: mockCreateRelation,
81
+ },
82
+ };
83
+
84
+ await adapter.addDependency("ENG-43", "ENG-42");
85
+
86
+ expect(mockCreateRelation).toHaveBeenCalledWith({
87
+ issueId: "issue_epic_1", // blocker
88
+ relatedIssueId: "issue_epic_2", // blocked
89
+ type: "blocks",
90
+ });
91
+ });
92
+
93
+ test("creates blocked relation between two tasks", async () => {
94
+ const blockerTask = createMockIssue({
95
+ id: "issue_task_1",
96
+ identifier: "ENG-44",
97
+ title: "Task 1",
98
+ labels: ["task"],
99
+ parentIdentifier: "ENG-42",
100
+ });
101
+
102
+ const blockedTask = createMockIssue({
103
+ id: "issue_task_2",
104
+ identifier: "ENG-45",
105
+ title: "Task 2",
106
+ labels: ["task"],
107
+ parentIdentifier: "ENG-42",
108
+ });
109
+
110
+ let fetchCallCount = 0;
111
+ (adapter as any).fetchIssue = mock(async () => {
112
+ fetchCallCount++;
113
+ if (fetchCallCount === 1) return blockedTask;
114
+ return blockerTask;
115
+ });
116
+
117
+ const mockCreateRelation = mock(async () => ({
118
+ id: "relation_2",
119
+ type: "blocks",
120
+ }));
121
+
122
+ (adapter as any).client = {
123
+ execute: mock(async (fn: () => Promise<any>) => fn()),
124
+ client: {
125
+ createIssueRelation: mockCreateRelation,
126
+ },
127
+ };
128
+
129
+ await adapter.addDependency("ENG-45", "ENG-44");
130
+
131
+ expect(mockCreateRelation).toHaveBeenCalledWith({
132
+ issueId: "issue_task_1",
133
+ relatedIssueId: "issue_task_2",
134
+ type: "blocks",
135
+ });
136
+ });
137
+
138
+ test("throws error if dependsOnRef issue not found", async () => {
139
+ const blockedIssue = createMockIssue({
140
+ id: "issue_epic_1",
141
+ identifier: "ENG-42",
142
+ labels: ["epic"],
143
+ });
144
+
145
+ let fetchCallCount = 0;
146
+ (adapter as any).fetchIssue = mock(async () => {
147
+ fetchCallCount++;
148
+ if (fetchCallCount === 1) return blockedIssue;
149
+ return null;
150
+ });
151
+
152
+ await expect(
153
+ adapter.addDependency("ENG-42", "INVALID-1"),
154
+ ).rejects.toThrow("Issue not found: INVALID-1");
155
+ });
156
+
157
+ test("throws error if entity types don't match (epic and task)", async () => {
158
+ const epicIssue = createMockIssue({
159
+ id: "issue_epic_1",
160
+ identifier: "ENG-42",
161
+ labels: ["epic"],
162
+ parentIdentifier: undefined,
163
+ });
164
+
165
+ const taskIssue = createMockIssue({
166
+ id: "issue_task_1",
167
+ identifier: "ENG-43",
168
+ labels: ["task"],
169
+ parentIdentifier: "ENG-42",
170
+ });
171
+
172
+ let fetchCallCount = 0;
173
+ (adapter as any).fetchIssue = mock(async () => {
174
+ fetchCallCount++;
175
+ if (fetchCallCount === 1) return epicIssue;
176
+ return taskIssue;
177
+ });
178
+
179
+ await expect(adapter.addDependency("ENG-42", "ENG-43")).rejects.toThrow(
180
+ "Cannot create dependency between different entity types (epic and task)",
181
+ );
182
+ });
183
+
184
+ test("throws error for self-dependency", async () => {
185
+ await expect(adapter.addDependency("ENG-42", "ENG-42")).rejects.toThrow(
186
+ "Entity cannot depend on itself",
187
+ );
188
+ });
189
+ });
190
+
191
+ describe("removeDependency", () => {
192
+ test("removes blocked relation between issues", async () => {
193
+ const mockDelete = mock(async () => ({ success: true }));
194
+ const mockRelation = {
195
+ id: "relation_1",
196
+ type: "blocks",
197
+ // Linear SDK returns promises for lazy-loaded related objects
198
+ issue: Promise.resolve({ id: "issue_epic_1", identifier: "ENG-42" }),
199
+ relatedIssue: Promise.resolve({
200
+ id: "issue_epic_2",
201
+ identifier: "ENG-43",
202
+ }),
203
+ delete: mockDelete,
204
+ };
205
+
206
+ const blockerIssue = createMockIssue({
207
+ id: "issue_epic_1",
208
+ identifier: "ENG-42",
209
+ });
210
+
211
+ const blockedIssue = createMockIssue({
212
+ id: "issue_epic_2",
213
+ identifier: "ENG-43",
214
+ _raw: {
215
+ inverseRelations: mock(async () => ({
216
+ nodes: [mockRelation],
217
+ })),
218
+ },
219
+ });
220
+
221
+ let fetchCallCount = 0;
222
+ (adapter as any).fetchIssue = mock(async () => {
223
+ fetchCallCount++;
224
+ if (fetchCallCount === 1) return blockedIssue;
225
+ return blockerIssue;
226
+ });
227
+
228
+ (adapter as any).client = {
229
+ execute: mock(async (fn: () => Promise<any>) => fn()),
230
+ };
231
+
232
+ await adapter.removeDependency("ENG-43", "ENG-42");
233
+
234
+ expect(mockDelete).toHaveBeenCalled();
235
+ });
236
+
237
+ test("throws error if dependsOnRef issue not found", async () => {
238
+ const blockedIssue = createMockIssue({
239
+ id: "issue_epic_1",
240
+ identifier: "ENG-42",
241
+ _raw: {
242
+ inverseRelations: mock(async () => ({ nodes: [] })),
243
+ },
244
+ });
245
+
246
+ let fetchCallCount = 0;
247
+ (adapter as any).fetchIssue = mock(async () => {
248
+ fetchCallCount++;
249
+ if (fetchCallCount === 1) return blockedIssue;
250
+ return null;
251
+ });
252
+
253
+ (adapter as any).client = {
254
+ execute: mock(async (fn: () => Promise<any>) => fn()),
255
+ };
256
+
257
+ await expect(
258
+ adapter.removeDependency("ENG-42", "INVALID-1"),
259
+ ).rejects.toThrow("Issue not found: INVALID-1");
260
+ });
261
+
262
+ test("throws error if relation not found", async () => {
263
+ const blockerIssue = createMockIssue({
264
+ id: "issue_epic_1",
265
+ identifier: "ENG-42",
266
+ });
267
+
268
+ const blockedIssue = createMockIssue({
269
+ id: "issue_epic_2",
270
+ identifier: "ENG-43",
271
+ _raw: {
272
+ inverseRelations: mock(async () => ({
273
+ nodes: [], // No relations
274
+ })),
275
+ },
276
+ });
277
+
278
+ let fetchCallCount = 0;
279
+ (adapter as any).fetchIssue = mock(async () => {
280
+ fetchCallCount++;
281
+ if (fetchCallCount === 1) return blockedIssue;
282
+ return blockerIssue;
283
+ });
284
+
285
+ (adapter as any).client = {
286
+ execute: mock(async (fn: () => Promise<any>) => fn()),
287
+ };
288
+
289
+ await expect(
290
+ adapter.removeDependency("ENG-43", "ENG-42"),
291
+ ).rejects.toThrow("Dependency not found between ENG-43 and ENG-42");
292
+ });
293
+ });
294
+
295
+ describe("getDependencies", () => {
296
+ test("returns list of blocking issue refs", async () => {
297
+ const mockIssue = createMockIssue({
298
+ id: "issue_epic_1",
299
+ identifier: "ENG-43",
300
+ _raw: {
301
+ inverseRelations: mock(async () => ({
302
+ nodes: [
303
+ {
304
+ id: "relation_1",
305
+ type: "blocks",
306
+ issue: Promise.resolve({
307
+ id: "issue_epic_2",
308
+ identifier: "ENG-42",
309
+ }),
310
+ relatedIssue: Promise.resolve({
311
+ id: "issue_epic_1",
312
+ identifier: "ENG-43",
313
+ }),
314
+ },
315
+ {
316
+ id: "relation_2",
317
+ type: "blocks",
318
+ issue: Promise.resolve({
319
+ id: "issue_epic_3",
320
+ identifier: "ENG-44",
321
+ }),
322
+ relatedIssue: Promise.resolve({
323
+ id: "issue_epic_1",
324
+ identifier: "ENG-43",
325
+ }),
326
+ },
327
+ ],
328
+ })),
329
+ },
330
+ });
331
+
332
+ (adapter as any).fetchIssue = mock(async () => mockIssue);
333
+ (adapter as any).client = {
334
+ execute: mock(async (fn: () => Promise<any>) => fn()),
335
+ };
336
+
337
+ const dependencies = await adapter.getDependencies("ENG-43");
338
+
339
+ expect(dependencies).toEqual(["ENG-42", "ENG-44"]);
340
+ });
341
+
342
+ test("returns empty array if no dependencies", async () => {
343
+ const mockIssue = createMockIssue({
344
+ id: "issue_epic_1",
345
+ identifier: "ENG-42",
346
+ _raw: {
347
+ inverseRelations: mock(async () => ({
348
+ nodes: [],
349
+ })),
350
+ },
351
+ });
352
+
353
+ (adapter as any).fetchIssue = mock(async () => mockIssue);
354
+ (adapter as any).client = {
355
+ execute: mock(async (fn: () => Promise<any>) => fn()),
356
+ };
357
+
358
+ const dependencies = await adapter.getDependencies("ENG-42");
359
+
360
+ expect(dependencies).toEqual([]);
361
+ });
362
+
363
+ test("filters out non-blocking relations", async () => {
364
+ const mockIssue = createMockIssue({
365
+ id: "issue_epic_1",
366
+ identifier: "ENG-43",
367
+ _raw: {
368
+ inverseRelations: mock(async () => ({
369
+ nodes: [
370
+ {
371
+ id: "relation_1",
372
+ type: "blocks",
373
+ issue: Promise.resolve({
374
+ id: "issue_epic_2",
375
+ identifier: "ENG-42",
376
+ }),
377
+ relatedIssue: Promise.resolve({
378
+ id: "issue_epic_1",
379
+ identifier: "ENG-43",
380
+ }),
381
+ },
382
+ {
383
+ id: "relation_2",
384
+ type: "duplicate",
385
+ issue: Promise.resolve({
386
+ id: "issue_epic_3",
387
+ identifier: "ENG-44",
388
+ }),
389
+ relatedIssue: Promise.resolve({
390
+ id: "issue_epic_1",
391
+ identifier: "ENG-43",
392
+ }),
393
+ },
394
+ {
395
+ id: "relation_3",
396
+ type: "related",
397
+ issue: Promise.resolve({
398
+ id: "issue_epic_4",
399
+ identifier: "ENG-45",
400
+ }),
401
+ relatedIssue: Promise.resolve({
402
+ id: "issue_epic_1",
403
+ identifier: "ENG-43",
404
+ }),
405
+ },
406
+ ],
407
+ })),
408
+ },
409
+ });
410
+
411
+ (adapter as any).fetchIssue = mock(async () => mockIssue);
412
+ (adapter as any).client = {
413
+ execute: mock(async (fn: () => Promise<any>) => fn()),
414
+ };
415
+
416
+ const dependencies = await adapter.getDependencies("ENG-43");
417
+
418
+ expect(dependencies).toEqual(["ENG-42"]);
419
+ });
420
+
421
+ test("throws error if issue not found", async () => {
422
+ (adapter as any).fetchIssue = mock(async () => null);
423
+
424
+ await expect(adapter.getDependencies("INVALID-1")).rejects.toThrow(
425
+ "Issue not found: INVALID-1",
426
+ );
427
+ });
428
+ });
429
+ });