@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,310 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { renderFullTreeView, renderSummaryView } from "../status-renderer.js";
|
|
3
|
+
|
|
4
|
+
describe("renderSummaryView", () => {
|
|
5
|
+
const mockProjectContext = {
|
|
6
|
+
name: "flux plugin",
|
|
7
|
+
vision: "AI-first workflow orchestration",
|
|
8
|
+
ref_prefix: "FP",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const mockStats = {
|
|
12
|
+
prds: { total: 2, draft: 1, approved: 1, completed: 0, archived: 0 },
|
|
13
|
+
epics: { total: 4, pending: 1, in_progress: 2, completed: 1 },
|
|
14
|
+
tasks: { total: 10, pending: 3, in_progress: 2, completed: 5 },
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const mockPrds = [
|
|
18
|
+
{
|
|
19
|
+
ref: "FP-P1",
|
|
20
|
+
title: "MCP Tool Implementation",
|
|
21
|
+
status: "COMPLETED",
|
|
22
|
+
epics: [
|
|
23
|
+
{
|
|
24
|
+
ref: "FP-E1",
|
|
25
|
+
title: "Core CRUD Tools",
|
|
26
|
+
status: "COMPLETED",
|
|
27
|
+
task_count: 3,
|
|
28
|
+
tasks_completed: 3,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
ref: "FP-E2",
|
|
32
|
+
title: "Query Tools",
|
|
33
|
+
status: "COMPLETED",
|
|
34
|
+
task_count: 2,
|
|
35
|
+
tasks_completed: 2,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
ref: "FP-P5",
|
|
41
|
+
title: "Status Display Feature",
|
|
42
|
+
status: "DRAFT",
|
|
43
|
+
epics: [
|
|
44
|
+
{
|
|
45
|
+
ref: "FP-E4",
|
|
46
|
+
title: "Terminal UI",
|
|
47
|
+
status: "IN_PROGRESS",
|
|
48
|
+
task_count: 4,
|
|
49
|
+
tasks_completed: 1,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
test("shows version and project name in header", () => {
|
|
56
|
+
const result = renderSummaryView(
|
|
57
|
+
mockProjectContext,
|
|
58
|
+
mockStats,
|
|
59
|
+
mockPrds,
|
|
60
|
+
"0.1.0",
|
|
61
|
+
);
|
|
62
|
+
expect(result).toContain("Flux v0.1.0 • flux plugin");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("shows only project name when version is not provided", () => {
|
|
66
|
+
const result = renderSummaryView(mockProjectContext, mockStats, mockPrds);
|
|
67
|
+
expect(result).toContain("flux plugin");
|
|
68
|
+
expect(result).not.toContain("Flux v");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("shows project name header", () => {
|
|
72
|
+
const result = renderSummaryView(mockProjectContext, mockStats, mockPrds);
|
|
73
|
+
expect(result).toContain("flux plugin");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("shows overall progress bar with percentage", () => {
|
|
77
|
+
const result = renderSummaryView(mockProjectContext, mockStats, mockPrds);
|
|
78
|
+
// 5 completed out of 10 = 50%
|
|
79
|
+
expect(result).toContain("50%");
|
|
80
|
+
expect(result).toMatch(/[━·]+/);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("shows task summary counts", () => {
|
|
84
|
+
const result = renderSummaryView(mockProjectContext, mockStats, mockPrds);
|
|
85
|
+
expect(result).toContain("5 done");
|
|
86
|
+
expect(result).toContain("2 active");
|
|
87
|
+
expect(result).toContain("3 pending");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("lists PRDs with status badges", () => {
|
|
91
|
+
const result = renderSummaryView(mockProjectContext, mockStats, mockPrds);
|
|
92
|
+
expect(result).toContain("FP-P1");
|
|
93
|
+
expect(result).toContain("MCP Tool Implementation");
|
|
94
|
+
expect(result).toContain("FP-P5");
|
|
95
|
+
expect(result).toContain("Status Display Feature");
|
|
96
|
+
// Status badges
|
|
97
|
+
expect(result).toContain("COMPLETED");
|
|
98
|
+
expect(result).toContain("DRAFT");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("shows epic progress bars under each PRD", () => {
|
|
102
|
+
const result = renderSummaryView(mockProjectContext, mockStats, mockPrds);
|
|
103
|
+
expect(result).toContain("FP-E1");
|
|
104
|
+
expect(result).toContain("Core CRUD Tools");
|
|
105
|
+
expect(result).toContain("FP-E4");
|
|
106
|
+
expect(result).toContain("Terminal UI");
|
|
107
|
+
// Progress indicators
|
|
108
|
+
expect(result).toContain("3/3");
|
|
109
|
+
expect(result).toContain("1/4");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("handles empty project", () => {
|
|
113
|
+
const emptyStats = {
|
|
114
|
+
prds: { total: 0, draft: 0, approved: 0, completed: 0, archived: 0 },
|
|
115
|
+
epics: { total: 0, pending: 0, in_progress: 0, completed: 0 },
|
|
116
|
+
tasks: { total: 0, pending: 0, in_progress: 0, completed: 0 },
|
|
117
|
+
};
|
|
118
|
+
const result = renderSummaryView(mockProjectContext, emptyStats, []);
|
|
119
|
+
expect(result).toContain("flux plugin");
|
|
120
|
+
expect(result).toContain("0%");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("sorts PRDs by status with APPROVED first, DRAFT last", () => {
|
|
124
|
+
const unsortedPrds = [
|
|
125
|
+
{
|
|
126
|
+
ref: "FP-P1",
|
|
127
|
+
title: "Draft PRD",
|
|
128
|
+
status: "DRAFT",
|
|
129
|
+
epics: [],
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
ref: "FP-P2",
|
|
133
|
+
title: "Approved PRD",
|
|
134
|
+
status: "APPROVED",
|
|
135
|
+
epics: [],
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
ref: "FP-P3",
|
|
139
|
+
title: "Pending Review PRD",
|
|
140
|
+
status: "PENDING_REVIEW",
|
|
141
|
+
epics: [],
|
|
142
|
+
},
|
|
143
|
+
];
|
|
144
|
+
const result = renderSummaryView(
|
|
145
|
+
mockProjectContext,
|
|
146
|
+
mockStats,
|
|
147
|
+
unsortedPrds,
|
|
148
|
+
);
|
|
149
|
+
const lines = result.split("\n");
|
|
150
|
+
const prdLines = lines.filter((line) => line.includes("FP-P"));
|
|
151
|
+
// APPROVED should come first, then PENDING_REVIEW, then DRAFT
|
|
152
|
+
expect(prdLines[0]).toContain("FP-P2"); // APPROVED
|
|
153
|
+
expect(prdLines[1]).toContain("FP-P3"); // PENDING_REVIEW
|
|
154
|
+
expect(prdLines[2]).toContain("FP-P1"); // DRAFT
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("shows '(no tasks)' indicator for epics with zero tasks", () => {
|
|
158
|
+
const prdsWithEmptyEpic = [
|
|
159
|
+
{
|
|
160
|
+
ref: "FP-P1",
|
|
161
|
+
title: "PRD with empty epic",
|
|
162
|
+
status: "DRAFT",
|
|
163
|
+
epics: [
|
|
164
|
+
{
|
|
165
|
+
ref: "FP-E1",
|
|
166
|
+
title: "Empty Epic",
|
|
167
|
+
status: "PENDING",
|
|
168
|
+
task_count: 0,
|
|
169
|
+
tasks_completed: 0,
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
];
|
|
174
|
+
const result = renderSummaryView(
|
|
175
|
+
mockProjectContext,
|
|
176
|
+
mockStats,
|
|
177
|
+
prdsWithEmptyEpic,
|
|
178
|
+
);
|
|
179
|
+
expect(result).toContain("(no tasks)");
|
|
180
|
+
expect(result).toContain("··········");
|
|
181
|
+
// Should NOT show "0/0"
|
|
182
|
+
expect(result).not.toMatch(/FP-E1.*0\/0/);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe("renderFullTreeView", () => {
|
|
187
|
+
const mockPrdsWithTasks = [
|
|
188
|
+
{
|
|
189
|
+
ref: "FP-P5",
|
|
190
|
+
title: "Status Display Feature",
|
|
191
|
+
status: "DRAFT",
|
|
192
|
+
epics: [
|
|
193
|
+
{
|
|
194
|
+
ref: "FP-E4",
|
|
195
|
+
title: "Terminal UI",
|
|
196
|
+
status: "IN_PROGRESS",
|
|
197
|
+
tasks: [
|
|
198
|
+
{ ref: "FP-T6", title: "Design mockups", status: "COMPLETED" },
|
|
199
|
+
{
|
|
200
|
+
ref: "FP-T7",
|
|
201
|
+
title: "Implement tree view",
|
|
202
|
+
status: "IN_PROGRESS",
|
|
203
|
+
},
|
|
204
|
+
{ ref: "FP-T8", title: "Add color support", status: "PENDING" },
|
|
205
|
+
{ ref: "FP-T9", title: "Write tests", status: "PENDING" },
|
|
206
|
+
],
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
},
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
test("shows full PRD→Epic→Task hierarchy", () => {
|
|
213
|
+
const result = renderFullTreeView(mockPrdsWithTasks);
|
|
214
|
+
// PRD level
|
|
215
|
+
expect(result).toContain("FP-P5");
|
|
216
|
+
expect(result).toContain("Status Display Feature");
|
|
217
|
+
// Epic level
|
|
218
|
+
expect(result).toContain("FP-E4");
|
|
219
|
+
expect(result).toContain("Terminal UI");
|
|
220
|
+
// Task level
|
|
221
|
+
expect(result).toContain("FP-T6");
|
|
222
|
+
expect(result).toContain("Design mockups");
|
|
223
|
+
expect(result).toContain("FP-T7");
|
|
224
|
+
expect(result).toContain("FP-T8");
|
|
225
|
+
expect(result).toContain("FP-T9");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("each item has status icon", () => {
|
|
229
|
+
const result = renderFullTreeView(mockPrdsWithTasks);
|
|
230
|
+
// Status icons: ✓ for completed, ⟳ for in progress, ○ for pending
|
|
231
|
+
expect(result).toContain("✓");
|
|
232
|
+
expect(result).toContain("⟳");
|
|
233
|
+
expect(result).toContain("○");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("current task is highlighted with ← CURRENT", () => {
|
|
237
|
+
const result = renderFullTreeView(mockPrdsWithTasks);
|
|
238
|
+
// IN_PROGRESS task should be marked as current
|
|
239
|
+
expect(result).toContain("CURRENT");
|
|
240
|
+
// The line with FP-T7 should have the CURRENT marker
|
|
241
|
+
const lines = result.split("\n");
|
|
242
|
+
const currentLine = lines.find((line) => line.includes("FP-T7"));
|
|
243
|
+
expect(currentLine).toContain("CURRENT");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("handles PRD with no epics", () => {
|
|
247
|
+
const prdsNoEpics = [
|
|
248
|
+
{
|
|
249
|
+
ref: "FP-P1",
|
|
250
|
+
title: "Empty PRD",
|
|
251
|
+
status: "DRAFT",
|
|
252
|
+
epics: [],
|
|
253
|
+
},
|
|
254
|
+
];
|
|
255
|
+
const result = renderFullTreeView(prdsNoEpics);
|
|
256
|
+
expect(result).toContain("FP-P1");
|
|
257
|
+
expect(result).toContain("Empty PRD");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("handles epic with no tasks", () => {
|
|
261
|
+
const epicNoTasks = [
|
|
262
|
+
{
|
|
263
|
+
ref: "FP-P1",
|
|
264
|
+
title: "PRD",
|
|
265
|
+
status: "APPROVED",
|
|
266
|
+
epics: [
|
|
267
|
+
{
|
|
268
|
+
ref: "FP-E1",
|
|
269
|
+
title: "Empty Epic",
|
|
270
|
+
status: "PENDING",
|
|
271
|
+
tasks: [],
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
},
|
|
275
|
+
];
|
|
276
|
+
const result = renderFullTreeView(epicNoTasks);
|
|
277
|
+
expect(result).toContain("FP-E1");
|
|
278
|
+
expect(result).toContain("Empty Epic");
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("sorts PRDs by status with APPROVED first, DRAFT last", () => {
|
|
282
|
+
const unsortedPrds = [
|
|
283
|
+
{
|
|
284
|
+
ref: "FP-P1",
|
|
285
|
+
title: "Draft PRD",
|
|
286
|
+
status: "DRAFT",
|
|
287
|
+
epics: [],
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
ref: "FP-P2",
|
|
291
|
+
title: "Approved PRD",
|
|
292
|
+
status: "APPROVED",
|
|
293
|
+
epics: [],
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
ref: "FP-P3",
|
|
297
|
+
title: "Pending Review PRD",
|
|
298
|
+
status: "PENDING_REVIEW",
|
|
299
|
+
epics: [],
|
|
300
|
+
},
|
|
301
|
+
];
|
|
302
|
+
const result = renderFullTreeView(unsortedPrds);
|
|
303
|
+
const lines = result.split("\n");
|
|
304
|
+
const prdLines = lines.filter((line) => line.includes("FP-P"));
|
|
305
|
+
// APPROVED should come first, then PENDING_REVIEW, then DRAFT
|
|
306
|
+
expect(prdLines[0]).toContain("FP-P2"); // APPROVED
|
|
307
|
+
expect(prdLines[1]).toContain("FP-P3"); // PENDING_REVIEW
|
|
308
|
+
expect(prdLines[2]).toContain("FP-P1"); // DRAFT
|
|
309
|
+
});
|
|
310
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders a progress bar with filled and empty portions.
|
|
3
|
+
*
|
|
4
|
+
* @param percentage - Progress percentage (0-100)
|
|
5
|
+
* @param width - Total width of the progress bar in characters
|
|
6
|
+
* @returns String representation of the progress bar
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* renderProgressBar(0, 10) // "··········"
|
|
10
|
+
* renderProgressBar(50, 10) // "━━━━━·····"
|
|
11
|
+
* renderProgressBar(100, 10) // "━━━━━━━━━━"
|
|
12
|
+
*/
|
|
13
|
+
export function renderProgressBar(percentage: number, width: number): string {
|
|
14
|
+
const filledCount = Math.floor((percentage / 100) * width);
|
|
15
|
+
const emptyCount = width - filledCount;
|
|
16
|
+
|
|
17
|
+
const filledChar = "━";
|
|
18
|
+
const emptyChar = "·";
|
|
19
|
+
|
|
20
|
+
return filledChar.repeat(filledCount) + emptyChar.repeat(emptyCount);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Status icons for different entity states.
|
|
25
|
+
*/
|
|
26
|
+
const STATUS_ICONS: Record<string, string> = {
|
|
27
|
+
COMPLETED: "✓",
|
|
28
|
+
IN_PROGRESS: "⟳",
|
|
29
|
+
PENDING: "○",
|
|
30
|
+
DRAFT: "◇",
|
|
31
|
+
APPROVED: "●",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Returns the appropriate icon for a given status.
|
|
36
|
+
*
|
|
37
|
+
* @param status - Entity status (COMPLETED, IN_PROGRESS, PENDING, DRAFT, APPROVED)
|
|
38
|
+
* @returns Unicode icon character representing the status
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* getStatusIcon("COMPLETED") // "✓"
|
|
42
|
+
* getStatusIcon("IN_PROGRESS") // "⟳"
|
|
43
|
+
* getStatusIcon("PENDING") // "○"
|
|
44
|
+
*/
|
|
45
|
+
export function getStatusIcon(status: string): string {
|
|
46
|
+
return STATUS_ICONS[status] ?? "?";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Returns a badge for the given status.
|
|
51
|
+
*
|
|
52
|
+
* @param status - Entity status
|
|
53
|
+
* @returns String with the status in brackets
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* getStatusBadge("COMPLETED") // "[COMPLETED]"
|
|
57
|
+
* getStatusBadge("IN_PROGRESS") // "[IN_PROGRESS]"
|
|
58
|
+
* getStatusBadge("DRAFT") // "[DRAFT]"
|
|
59
|
+
*/
|
|
60
|
+
export function getStatusBadge(status: string): string {
|
|
61
|
+
return `[${status}]`;
|
|
62
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { getStatusBadge, getStatusIcon, renderProgressBar } from "./display.js";
|
|
2
|
+
|
|
3
|
+
interface ProjectContext {
|
|
4
|
+
name: string;
|
|
5
|
+
vision?: string;
|
|
6
|
+
ref_prefix: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface Stats {
|
|
10
|
+
prds: {
|
|
11
|
+
total: number;
|
|
12
|
+
draft: number;
|
|
13
|
+
approved: number;
|
|
14
|
+
completed: number;
|
|
15
|
+
archived: number;
|
|
16
|
+
};
|
|
17
|
+
epics: {
|
|
18
|
+
total: number;
|
|
19
|
+
pending: number;
|
|
20
|
+
in_progress: number;
|
|
21
|
+
completed: number;
|
|
22
|
+
};
|
|
23
|
+
tasks: {
|
|
24
|
+
total: number;
|
|
25
|
+
pending: number;
|
|
26
|
+
in_progress: number;
|
|
27
|
+
completed: number;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface Epic {
|
|
32
|
+
ref: string;
|
|
33
|
+
title: string;
|
|
34
|
+
status: string;
|
|
35
|
+
task_count: number;
|
|
36
|
+
tasks_completed: number;
|
|
37
|
+
dependencies?: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface Task {
|
|
41
|
+
ref: string;
|
|
42
|
+
title: string;
|
|
43
|
+
status: string;
|
|
44
|
+
dependencies?: string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface EpicWithTasks {
|
|
48
|
+
ref: string;
|
|
49
|
+
title: string;
|
|
50
|
+
status: string;
|
|
51
|
+
tasks: Task[];
|
|
52
|
+
dependencies?: string[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface Prd {
|
|
56
|
+
ref: string;
|
|
57
|
+
title: string;
|
|
58
|
+
status: string;
|
|
59
|
+
epics: Epic[];
|
|
60
|
+
dependencies?: string[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface PrdWithTasks {
|
|
64
|
+
ref: string;
|
|
65
|
+
title: string;
|
|
66
|
+
status: string;
|
|
67
|
+
epics: EpicWithTasks[];
|
|
68
|
+
dependencies?: string[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const STATUS_ORDER: Record<string, number> = {
|
|
72
|
+
APPROVED: 1,
|
|
73
|
+
BREAKDOWN_READY: 2,
|
|
74
|
+
PENDING_REVIEW: 3,
|
|
75
|
+
REVIEWED: 4,
|
|
76
|
+
DRAFT: 5,
|
|
77
|
+
COMPLETED: 6,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
function sortPrdsByStatus<T extends { status: string; ref: string }>(
|
|
81
|
+
prds: T[],
|
|
82
|
+
): T[] {
|
|
83
|
+
return [...prds].sort((a, b) => {
|
|
84
|
+
const orderA = STATUS_ORDER[a.status] ?? 99;
|
|
85
|
+
const orderB = STATUS_ORDER[b.status] ?? 99;
|
|
86
|
+
if (orderA !== orderB) return orderA - orderB;
|
|
87
|
+
return a.ref.localeCompare(b.ref);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Renders the summary view for /flux:status command.
|
|
93
|
+
*/
|
|
94
|
+
export function renderSummaryView(
|
|
95
|
+
project: ProjectContext,
|
|
96
|
+
stats: Stats,
|
|
97
|
+
prds: Prd[],
|
|
98
|
+
version?: string,
|
|
99
|
+
): string {
|
|
100
|
+
const lines: string[] = [];
|
|
101
|
+
|
|
102
|
+
if (version) {
|
|
103
|
+
lines.push(`Flux v${version} • ${project.name}`);
|
|
104
|
+
} else {
|
|
105
|
+
lines.push(project.name);
|
|
106
|
+
}
|
|
107
|
+
lines.push("");
|
|
108
|
+
|
|
109
|
+
const totalTasks = stats.tasks.total;
|
|
110
|
+
const completedTasks = stats.tasks.completed;
|
|
111
|
+
const percentage =
|
|
112
|
+
totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0;
|
|
113
|
+
const progressBar = renderProgressBar(percentage, 40);
|
|
114
|
+
|
|
115
|
+
lines.push(`Progress ${progressBar} ${percentage}%`);
|
|
116
|
+
lines.push(
|
|
117
|
+
`Tasks ${completedTasks} done · ${stats.tasks.in_progress} active · ${stats.tasks.pending} pending`,
|
|
118
|
+
);
|
|
119
|
+
lines.push("");
|
|
120
|
+
|
|
121
|
+
const sortedPrds = sortPrdsByStatus(prds);
|
|
122
|
+
|
|
123
|
+
for (const prd of sortedPrds) {
|
|
124
|
+
const badge = getStatusBadge(prd.status);
|
|
125
|
+
const prdBlocked = prd.dependencies && prd.dependencies.length > 0;
|
|
126
|
+
const prdBlockedBy = prdBlocked
|
|
127
|
+
? ` ⚠️ blocked by: ${prd.dependencies?.join(", ")}`
|
|
128
|
+
: "";
|
|
129
|
+
lines.push(`${prd.ref} ${prd.title} ${badge}${prdBlockedBy}`);
|
|
130
|
+
|
|
131
|
+
for (const epic of prd.epics) {
|
|
132
|
+
const icon = getStatusIcon(epic.status);
|
|
133
|
+
const epicBlocked = epic.dependencies && epic.dependencies.length > 0;
|
|
134
|
+
const epicBlockedBy = epicBlocked
|
|
135
|
+
? ` ⚠️ blocked by: ${epic.dependencies?.join(", ")}`
|
|
136
|
+
: "";
|
|
137
|
+
|
|
138
|
+
if (epic.task_count === 0) {
|
|
139
|
+
lines.push(
|
|
140
|
+
` ${icon} ${epic.ref} ${epic.title} ·········· (no tasks)${epicBlockedBy}`,
|
|
141
|
+
);
|
|
142
|
+
} else {
|
|
143
|
+
const epicProgress = renderProgressBar(
|
|
144
|
+
(epic.tasks_completed / epic.task_count) * 100,
|
|
145
|
+
10,
|
|
146
|
+
);
|
|
147
|
+
lines.push(
|
|
148
|
+
` ${icon} ${epic.ref} ${epic.title} ${epicProgress} ${epic.tasks_completed}/${epic.task_count}${epicBlockedBy}`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
lines.push("");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
lines.push("─".repeat(50));
|
|
156
|
+
lines.push(
|
|
157
|
+
`✓ ${completedTasks} done ⟳ ${stats.tasks.in_progress} active ○ ${stats.tasks.pending} pending`,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
return lines.join("\n");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Renders the full tree view for /flux:status --full command.
|
|
165
|
+
* Shows complete PRD → Epic → Task hierarchy with status icons.
|
|
166
|
+
*/
|
|
167
|
+
export function renderFullTreeView(prds: PrdWithTasks[]): string {
|
|
168
|
+
const lines: string[] = [];
|
|
169
|
+
|
|
170
|
+
const sortedPrds = sortPrdsByStatus(prds);
|
|
171
|
+
|
|
172
|
+
for (const prd of sortedPrds) {
|
|
173
|
+
const prdBadge = getStatusBadge(prd.status);
|
|
174
|
+
const prdBlocked = prd.dependencies && prd.dependencies.length > 0;
|
|
175
|
+
const prdBlockedBy = prdBlocked
|
|
176
|
+
? ` ⚠️ blocked by: ${prd.dependencies?.join(", ")}`
|
|
177
|
+
: "";
|
|
178
|
+
lines.push(`${prd.ref} ${prd.title}`);
|
|
179
|
+
lines.push(`${prdBadge}${prdBlockedBy}`);
|
|
180
|
+
lines.push("");
|
|
181
|
+
|
|
182
|
+
for (const epic of prd.epics) {
|
|
183
|
+
const epicIcon = getStatusIcon(epic.status);
|
|
184
|
+
const epicBlocked = epic.dependencies && epic.dependencies.length > 0;
|
|
185
|
+
const epicBlockedBy = epicBlocked
|
|
186
|
+
? ` ⚠️ blocked by: ${epic.dependencies?.join(", ")}`
|
|
187
|
+
: "";
|
|
188
|
+
lines.push(` ${epicIcon} ${epic.ref} ${epic.title}${epicBlockedBy}`);
|
|
189
|
+
|
|
190
|
+
if (epic.tasks.length > 0) {
|
|
191
|
+
lines.push(` ┌${"─".repeat(50)}┐`);
|
|
192
|
+
|
|
193
|
+
for (const task of epic.tasks) {
|
|
194
|
+
const taskIcon = getStatusIcon(task.status);
|
|
195
|
+
const taskBlocked = task.dependencies && task.dependencies.length > 0;
|
|
196
|
+
const taskBlockedBy = taskBlocked
|
|
197
|
+
? ` ⚠️ blocked by: ${task.dependencies?.join(", ")}`
|
|
198
|
+
: "";
|
|
199
|
+
const currentMarker =
|
|
200
|
+
task.status === "IN_PROGRESS" ? " ← CURRENT" : "";
|
|
201
|
+
lines.push(
|
|
202
|
+
` │ ${taskIcon} ${task.ref} ${task.title}${currentMarker}${taskBlockedBy}`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
lines.push(` └${"─".repeat(50)}┘`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
lines.push("");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return lines.join("\n");
|
|
214
|
+
}
|