@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,160 @@
1
+ /**
2
+ * Status Transition Validation
3
+ *
4
+ * Defines valid status transitions for PRDs, Epics, and Tasks.
5
+ * Prevents skipping workflow stages.
6
+ */
7
+
8
+ import type { EpicStatus, PrdStatus, TaskStatus } from "../adapters/types.js";
9
+
10
+ // =============================================================================
11
+ // PRD Status Transitions
12
+ // =============================================================================
13
+
14
+ /**
15
+ * Valid PRD status transitions.
16
+ * Users cannot skip steps in the workflow.
17
+ */
18
+ const PRD_TRANSITIONS: Record<PrdStatus, PrdStatus[]> = {
19
+ DRAFT: ["PENDING_REVIEW"],
20
+ PENDING_REVIEW: ["REVIEWED", "DRAFT"], // Can go back to DRAFT for revisions
21
+ REVIEWED: ["APPROVED", "DRAFT"], // Can reject back to DRAFT
22
+ APPROVED: ["BREAKDOWN_READY"],
23
+ BREAKDOWN_READY: ["COMPLETED"],
24
+ COMPLETED: ["ARCHIVED"], // Can archive completed PRDs
25
+ ARCHIVED: [], // Terminal state
26
+ };
27
+
28
+ /**
29
+ * Check if a PRD status transition is valid.
30
+ */
31
+ export function isValidPrdTransition(from: PrdStatus, to: PrdStatus): boolean {
32
+ return PRD_TRANSITIONS[from]?.includes(to) ?? false;
33
+ }
34
+
35
+ /**
36
+ * Get valid next statuses for a PRD.
37
+ */
38
+ export function getValidPrdTransitions(status: PrdStatus): PrdStatus[] {
39
+ return PRD_TRANSITIONS[status] ?? [];
40
+ }
41
+
42
+ // =============================================================================
43
+ // Epic Status Transitions
44
+ // =============================================================================
45
+
46
+ /**
47
+ * Valid Epic status transitions.
48
+ */
49
+ const EPIC_TRANSITIONS: Record<EpicStatus, EpicStatus[]> = {
50
+ PENDING: ["IN_PROGRESS"],
51
+ IN_PROGRESS: ["COMPLETED", "PENDING"], // Can pause back to PENDING
52
+ COMPLETED: [], // Terminal state
53
+ };
54
+
55
+ /**
56
+ * Check if an Epic status transition is valid.
57
+ */
58
+ export function isValidEpicTransition(
59
+ from: EpicStatus,
60
+ to: EpicStatus,
61
+ ): boolean {
62
+ return EPIC_TRANSITIONS[from]?.includes(to) ?? false;
63
+ }
64
+
65
+ /**
66
+ * Get valid next statuses for an Epic.
67
+ */
68
+ export function getValidEpicTransitions(status: EpicStatus): EpicStatus[] {
69
+ return EPIC_TRANSITIONS[status] ?? [];
70
+ }
71
+
72
+ // =============================================================================
73
+ // Task Status Transitions
74
+ // =============================================================================
75
+
76
+ /**
77
+ * Valid Task status transitions.
78
+ */
79
+ const TASK_TRANSITIONS: Record<TaskStatus, TaskStatus[]> = {
80
+ PENDING: ["IN_PROGRESS"],
81
+ IN_PROGRESS: ["COMPLETED", "PENDING"], // Can pause back to PENDING
82
+ COMPLETED: [], // Terminal state
83
+ };
84
+
85
+ /**
86
+ * Check if a Task status transition is valid.
87
+ */
88
+ export function isValidTaskTransition(
89
+ from: TaskStatus,
90
+ to: TaskStatus,
91
+ ): boolean {
92
+ return TASK_TRANSITIONS[from]?.includes(to) ?? false;
93
+ }
94
+
95
+ /**
96
+ * Get valid next statuses for a Task.
97
+ */
98
+ export function getValidTaskTransitions(status: TaskStatus): TaskStatus[] {
99
+ return TASK_TRANSITIONS[status] ?? [];
100
+ }
101
+
102
+ // =============================================================================
103
+ // Generic Helpers
104
+ // =============================================================================
105
+
106
+ export type EntityType = "prd" | "epic" | "task";
107
+
108
+ /**
109
+ * Get valid transitions for any entity type.
110
+ */
111
+ export function getValidTransitions(
112
+ entityType: EntityType,
113
+ status: string,
114
+ ): string[] {
115
+ switch (entityType) {
116
+ case "prd":
117
+ return getValidPrdTransitions(status as PrdStatus);
118
+ case "epic":
119
+ return getValidEpicTransitions(status as EpicStatus);
120
+ case "task":
121
+ return getValidTaskTransitions(status as TaskStatus);
122
+ default:
123
+ return [];
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Check if a status transition is valid for any entity type.
129
+ */
130
+ export function isValidTransition(
131
+ entityType: EntityType,
132
+ from: string,
133
+ to: string,
134
+ ): boolean {
135
+ switch (entityType) {
136
+ case "prd":
137
+ return isValidPrdTransition(from as PrdStatus, to as PrdStatus);
138
+ case "epic":
139
+ return isValidEpicTransition(from as EpicStatus, to as EpicStatus);
140
+ case "task":
141
+ return isValidTaskTransition(from as TaskStatus, to as TaskStatus);
142
+ default:
143
+ return false;
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Get a human-readable error message for invalid transitions.
149
+ */
150
+ export function getTransitionErrorMessage(
151
+ entityType: EntityType,
152
+ from: string,
153
+ to: string,
154
+ ): string {
155
+ const validNext = getValidTransitions(entityType, from);
156
+ if (validNext.length === 0) {
157
+ return `Cannot transition from ${from}: it is a terminal state`;
158
+ }
159
+ return `Invalid transition from ${from} to ${to}. Valid next statuses: ${validNext.join(", ")}`;
160
+ }
@@ -0,0 +1,215 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
3
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
6
+
7
+ const BUN_PATH = join(homedir(), ".bun", "bin", "bun");
8
+
9
+ function setupTestProject(testDir: string, projectName = "test-project") {
10
+ const fluxDir = join(testDir, ".flux");
11
+ mkdirSync(fluxDir, { recursive: true });
12
+
13
+ writeFileSync(
14
+ join(fluxDir, "project.json"),
15
+ JSON.stringify({
16
+ name: projectName,
17
+ ref_prefix: "TEST",
18
+ }),
19
+ );
20
+
21
+ const db = new Database(join(fluxDir, "flux.db"));
22
+ db.run(`
23
+ CREATE TABLE IF NOT EXISTS tasks (
24
+ id TEXT PRIMARY KEY,
25
+ ref TEXT UNIQUE NOT NULL,
26
+ title TEXT NOT NULL,
27
+ status TEXT DEFAULT 'PENDING'
28
+ )
29
+ `);
30
+ return db;
31
+ }
32
+
33
+ describe("status-line script", () => {
34
+ const TEST_DIR = `/tmp/flux-status-line-test-${Date.now()}-${Math.random().toString(36).slice(2)}`;
35
+
36
+ beforeEach(() => {
37
+ if (existsSync(TEST_DIR)) {
38
+ rmSync(TEST_DIR, { recursive: true, force: true });
39
+ }
40
+ mkdirSync(TEST_DIR, { recursive: true });
41
+ });
42
+
43
+ afterEach(() => {
44
+ if (existsSync(TEST_DIR)) {
45
+ rmSync(TEST_DIR, { recursive: true, force: true });
46
+ }
47
+ });
48
+
49
+ test("script is executable", async () => {
50
+ const scriptPath = join(import.meta.dir, "..", "index.ts");
51
+ expect(existsSync(scriptPath)).toBe(true);
52
+
53
+ const proc = Bun.spawn([BUN_PATH, scriptPath], {
54
+ stdin: "pipe",
55
+ stdout: "pipe",
56
+ stderr: "pipe",
57
+ cwd: TEST_DIR,
58
+ });
59
+
60
+ proc.stdin.write("{}");
61
+ proc.stdin.end();
62
+
63
+ await proc.exited;
64
+ expect(proc.exitCode).toBe(0);
65
+ });
66
+
67
+ test("script reads JSON from stdin", async () => {
68
+ const db = setupTestProject(TEST_DIR);
69
+ db.run(
70
+ "INSERT INTO tasks (id, ref, title, status) VALUES ('t1', 'TEST-T1', 'Active task', 'IN_PROGRESS')",
71
+ );
72
+ db.close();
73
+
74
+ const scriptPath = join(import.meta.dir, "..", "index.ts");
75
+ const proc = Bun.spawn([BUN_PATH, scriptPath], {
76
+ stdin: "pipe",
77
+ stdout: "pipe",
78
+ stderr: "pipe",
79
+ });
80
+
81
+ proc.stdin.write(JSON.stringify({ cwd: TEST_DIR }));
82
+ proc.stdin.end();
83
+
84
+ await proc.exited;
85
+ const output = await new Response(proc.stdout).text();
86
+
87
+ expect(output).toContain("TEST-T1");
88
+ expect(output).toContain("Active task");
89
+ });
90
+
91
+ test("script outputs single line to stdout", async () => {
92
+ const db = setupTestProject(TEST_DIR);
93
+ db.run(
94
+ "INSERT INTO tasks (id, ref, title, status) VALUES ('t1', 'TEST-T1', 'Active task', 'IN_PROGRESS')",
95
+ );
96
+ db.run(
97
+ "INSERT INTO tasks (id, ref, title, status) VALUES ('t2', 'TEST-T2', 'Done task', 'COMPLETED')",
98
+ );
99
+ db.close();
100
+
101
+ const scriptPath = join(import.meta.dir, "..", "index.ts");
102
+ const proc = Bun.spawn([BUN_PATH, scriptPath], {
103
+ stdin: "pipe",
104
+ stdout: "pipe",
105
+ stderr: "pipe",
106
+ });
107
+
108
+ proc.stdin.write(JSON.stringify({ cwd: TEST_DIR }));
109
+ proc.stdin.end();
110
+
111
+ await proc.exited;
112
+ const output = await new Response(proc.stdout).text();
113
+
114
+ const lines = output.trim().split("\n");
115
+ expect(lines.length).toBe(1);
116
+
117
+ expect(output).toContain("TEST-T1");
118
+ expect(output).toContain("Active task");
119
+ expect(output).toContain("⟳");
120
+ });
121
+
122
+ test("shows only active task without task counts", async () => {
123
+ const db = setupTestProject(TEST_DIR);
124
+ db.run(
125
+ "INSERT INTO tasks (id, ref, title, status) VALUES ('t1', 'TEST-T1', 'Done 1', 'COMPLETED')",
126
+ );
127
+ db.run(
128
+ "INSERT INTO tasks (id, ref, title, status) VALUES ('t2', 'TEST-T2', 'Done 2', 'COMPLETED')",
129
+ );
130
+ db.run(
131
+ "INSERT INTO tasks (id, ref, title, status) VALUES ('t3', 'TEST-T3', 'Active', 'IN_PROGRESS')",
132
+ );
133
+ db.close();
134
+
135
+ const scriptPath = join(import.meta.dir, "..", "index.ts");
136
+ const proc = Bun.spawn([BUN_PATH, scriptPath], {
137
+ stdin: "pipe",
138
+ stdout: "pipe",
139
+ stderr: "pipe",
140
+ });
141
+
142
+ proc.stdin.write(JSON.stringify({ cwd: TEST_DIR }));
143
+ proc.stdin.end();
144
+
145
+ await proc.exited;
146
+ const output = await new Response(proc.stdout).text();
147
+
148
+ expect(output).toContain("TEST-T3");
149
+ expect(output).toContain("Active");
150
+ expect(output).not.toContain("tasks");
151
+ });
152
+
153
+ test("outputs nothing when no flux project found", async () => {
154
+ const scriptPath = join(import.meta.dir, "..", "index.ts");
155
+ const proc = Bun.spawn([BUN_PATH, scriptPath], {
156
+ stdin: "pipe",
157
+ stdout: "pipe",
158
+ stderr: "pipe",
159
+ });
160
+
161
+ proc.stdin.write(JSON.stringify({ cwd: TEST_DIR }));
162
+ proc.stdin.end();
163
+
164
+ await proc.exited;
165
+ const output = await new Response(proc.stdout).text();
166
+
167
+ expect(output.trim()).toBe("");
168
+ });
169
+
170
+ test("includes model name from JSON input", async () => {
171
+ const db = setupTestProject(TEST_DIR);
172
+ db.run(
173
+ "INSERT INTO tasks (id, ref, title, status) VALUES ('t1', 'TEST-T1', 'Active task', 'IN_PROGRESS')",
174
+ );
175
+ db.close();
176
+
177
+ const scriptPath = join(import.meta.dir, "..", "index.ts");
178
+ const proc = Bun.spawn([BUN_PATH, scriptPath], {
179
+ stdin: "pipe",
180
+ stdout: "pipe",
181
+ stderr: "pipe",
182
+ });
183
+
184
+ proc.stdin.write(JSON.stringify({ cwd: TEST_DIR, model: "Opus" }));
185
+ proc.stdin.end();
186
+
187
+ await proc.exited;
188
+ const output = await new Response(proc.stdout).text();
189
+
190
+ expect(output).toContain("[Opus]");
191
+ });
192
+
193
+ test("outputs nothing when no active task", async () => {
194
+ const db = setupTestProject(TEST_DIR);
195
+ db.run(
196
+ "INSERT INTO tasks (id, ref, title, status) VALUES ('t1', 'TEST-T1', 'Pending task', 'PENDING')",
197
+ );
198
+ db.close();
199
+
200
+ const scriptPath = join(import.meta.dir, "..", "index.ts");
201
+ const proc = Bun.spawn([BUN_PATH, scriptPath], {
202
+ stdin: "pipe",
203
+ stdout: "pipe",
204
+ stderr: "pipe",
205
+ });
206
+
207
+ proc.stdin.write(JSON.stringify({ cwd: TEST_DIR }));
208
+ proc.stdin.end();
209
+
210
+ await proc.exited;
211
+ const output = await new Response(proc.stdout).text();
212
+
213
+ expect(output.trim()).toBe("");
214
+ });
215
+ });
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * Flux Status Line Script
5
+ *
6
+ * This script is designed to be used with Claude Code's status line feature.
7
+ * It reads JSON context from stdin and outputs a single status line.
8
+ *
9
+ * Expected input format (JSON):
10
+ * { "cwd": "/path/to/project", ... }
11
+ *
12
+ * Output format:
13
+ * [Opus] | ⟳ FP-T7: Implement tree view
14
+ */
15
+
16
+ import { Database } from "bun:sqlite";
17
+ import { existsSync, readFileSync } from "node:fs";
18
+ import { dirname, join } from "node:path";
19
+
20
+ interface StdinContext {
21
+ cwd?: string;
22
+ workspace?: {
23
+ current_dir?: string;
24
+ project_dir?: string;
25
+ };
26
+ model?:
27
+ | {
28
+ display_name?: string;
29
+ }
30
+ | string;
31
+ }
32
+
33
+ interface TaskRow {
34
+ ref: string;
35
+ title: string;
36
+ status: string;
37
+ }
38
+
39
+ const STATUS_ICONS: Record<string, string> = {
40
+ COMPLETED: "✓",
41
+ IN_PROGRESS: "⟳",
42
+ PENDING: "○",
43
+ DRAFT: "◇",
44
+ APPROVED: "●",
45
+ };
46
+
47
+ function findFluxRoot(startDir: string): string | null {
48
+ let dir = startDir;
49
+ const root = "/";
50
+
51
+ while (dir !== root) {
52
+ if (existsSync(join(dir, ".flux", "project.json"))) {
53
+ return dir;
54
+ }
55
+ const parent = dirname(dir);
56
+ if (parent === dir) break;
57
+ dir = parent;
58
+ }
59
+
60
+ return null;
61
+ }
62
+
63
+ function getStatusLine(cwd: string, model?: string): string {
64
+ const projectRoot = findFluxRoot(cwd);
65
+ if (!projectRoot) {
66
+ return "";
67
+ }
68
+
69
+ const fluxDir = join(projectRoot, ".flux");
70
+ const projectJsonPath = join(fluxDir, "project.json");
71
+ const dbPath = join(fluxDir, "flux.db");
72
+
73
+ if (!existsSync(projectJsonPath) || !existsSync(dbPath)) {
74
+ return "";
75
+ }
76
+
77
+ try {
78
+ readFileSync(projectJsonPath, "utf-8");
79
+ } catch {
80
+ return "";
81
+ }
82
+
83
+ let db: Database;
84
+ try {
85
+ db = new Database(dbPath, { readonly: true });
86
+ } catch {
87
+ return "";
88
+ }
89
+
90
+ try {
91
+ const activeTask = db
92
+ .query<TaskRow, []>(
93
+ "SELECT ref, title, status FROM tasks WHERE status = 'IN_PROGRESS' LIMIT 1",
94
+ )
95
+ .get();
96
+
97
+ if (!activeTask) {
98
+ return "";
99
+ }
100
+
101
+ const parts: string[] = [];
102
+
103
+ if (model) {
104
+ parts.push(`[${model}]`);
105
+ }
106
+
107
+ const icon = STATUS_ICONS[activeTask.status] || "?";
108
+ const maxTitleLen = 30;
109
+ const title =
110
+ activeTask.title.length > maxTitleLen
111
+ ? `${activeTask.title.slice(0, maxTitleLen - 1)}…`
112
+ : activeTask.title;
113
+ parts.push(`${icon} ${activeTask.ref}: ${title}`);
114
+
115
+ return parts.join(" | ");
116
+ } finally {
117
+ db.close();
118
+ }
119
+ }
120
+
121
+ async function main() {
122
+ let context: StdinContext = {};
123
+
124
+ try {
125
+ const stdin = await Bun.stdin.text();
126
+ if (stdin.trim()) {
127
+ context = JSON.parse(stdin);
128
+ }
129
+ } catch {}
130
+
131
+ const cwd =
132
+ context.workspace?.current_dir ||
133
+ context.workspace?.project_dir ||
134
+ context.cwd ||
135
+ process.cwd();
136
+ const model =
137
+ typeof context.model === "object"
138
+ ? context.model?.display_name
139
+ : context.model;
140
+ const statusLine = getStatusLine(cwd, model);
141
+
142
+ if (statusLine) {
143
+ console.log(statusLine);
144
+ }
145
+ }
146
+
147
+ main().catch(() => {});
@@ -0,0 +1,32 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ describe("chalk dependency", () => {
6
+ test("chalk is listed in package.json dependencies", () => {
7
+ const packageJsonPath = join(process.cwd(), "package.json");
8
+ expect(existsSync(packageJsonPath)).toBe(true);
9
+
10
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
11
+ expect(packageJson.dependencies).toBeDefined();
12
+ expect(packageJson.dependencies.chalk).toBeDefined();
13
+ expect(typeof packageJson.dependencies.chalk).toBe("string");
14
+ });
15
+
16
+ test("chalk imports work in TypeScript", async () => {
17
+ // Dynamic import to verify chalk is available
18
+ const chalk = await import("chalk");
19
+
20
+ // Verify chalk has expected API
21
+ expect(chalk.default).toBeDefined();
22
+ expect(typeof chalk.default.red).toBe("function");
23
+ expect(typeof chalk.default.green).toBe("function");
24
+ expect(typeof chalk.default.blue).toBe("function");
25
+ expect(typeof chalk.default.yellow).toBe("function");
26
+
27
+ // Verify chalk can actually format text
28
+ const redText = chalk.default.red("test");
29
+ expect(typeof redText).toBe("string");
30
+ expect(redText).toContain("test");
31
+ });
32
+ });
@@ -0,0 +1,97 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ getStatusBadge,
4
+ getStatusIcon,
5
+ renderProgressBar,
6
+ } from "../display.js";
7
+
8
+ describe("renderProgressBar", () => {
9
+ test("returns empty bar for 0% progress", () => {
10
+ const result = renderProgressBar(0, 10);
11
+ expect(result).toBe("··········");
12
+ });
13
+
14
+ test("returns half-filled bar for 50% progress", () => {
15
+ const result = renderProgressBar(50, 10);
16
+ expect(result).toBe("━━━━━·····");
17
+ });
18
+
19
+ test("returns full bar for 100% progress", () => {
20
+ const result = renderProgressBar(100, 10);
21
+ expect(result).toBe("━━━━━━━━━━");
22
+ });
23
+
24
+ test("handles 33% progress correctly (rounds down)", () => {
25
+ const result = renderProgressBar(33, 10);
26
+ expect(result).toBe("━━━·······");
27
+ });
28
+
29
+ test("handles different widths", () => {
30
+ expect(renderProgressBar(0, 5)).toBe("·····");
31
+ expect(renderProgressBar(50, 5)).toBe("━━···");
32
+ expect(renderProgressBar(100, 5)).toBe("━━━━━");
33
+ });
34
+
35
+ test("handles edge case percentages", () => {
36
+ expect(renderProgressBar(1, 10)).toBe("··········");
37
+ expect(renderProgressBar(99, 10)).toBe("━━━━━━━━━·");
38
+ });
39
+ });
40
+
41
+ describe("getStatusIcon", () => {
42
+ test("returns checkmark for COMPLETED status", () => {
43
+ expect(getStatusIcon("COMPLETED")).toBe("✓");
44
+ });
45
+
46
+ test("returns spinner for IN_PROGRESS status", () => {
47
+ expect(getStatusIcon("IN_PROGRESS")).toBe("⟳");
48
+ });
49
+
50
+ test("returns circle for PENDING status", () => {
51
+ expect(getStatusIcon("PENDING")).toBe("○");
52
+ });
53
+
54
+ test("returns diamond for DRAFT status", () => {
55
+ expect(getStatusIcon("DRAFT")).toBe("◇");
56
+ });
57
+
58
+ test("returns diamond for APPROVED status (PRD-specific)", () => {
59
+ expect(getStatusIcon("APPROVED")).toBe("●");
60
+ });
61
+
62
+ test("returns question mark for unknown status", () => {
63
+ expect(getStatusIcon("UNKNOWN")).toBe("?");
64
+ });
65
+ });
66
+
67
+ describe("getStatusBadge", () => {
68
+ test("returns green badge for COMPLETED status", () => {
69
+ const badge = getStatusBadge("COMPLETED");
70
+ expect(badge).toContain("COMPLETED");
71
+ });
72
+
73
+ test("returns yellow badge for IN_PROGRESS status", () => {
74
+ const badge = getStatusBadge("IN_PROGRESS");
75
+ expect(badge).toContain("IN_PROGRESS");
76
+ });
77
+
78
+ test("returns gray badge for PENDING status", () => {
79
+ const badge = getStatusBadge("PENDING");
80
+ expect(badge).toContain("PENDING");
81
+ });
82
+
83
+ test("returns blue badge for DRAFT status", () => {
84
+ const badge = getStatusBadge("DRAFT");
85
+ expect(badge).toContain("DRAFT");
86
+ });
87
+
88
+ test("returns cyan badge for APPROVED status", () => {
89
+ const badge = getStatusBadge("APPROVED");
90
+ expect(badge).toContain("APPROVED");
91
+ });
92
+
93
+ test("returns plain badge for unknown status", () => {
94
+ const badge = getStatusBadge("UNKNOWN");
95
+ expect(badge).toContain("UNKNOWN");
96
+ });
97
+ });