@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,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flux Dashboard Server
|
|
3
|
+
*
|
|
4
|
+
* HTTP + WebSocket server for the Flux Dashboard.
|
|
5
|
+
* Serves static files and API endpoints for viewing PRD/Epic/Task data.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getAdapter } from "../server/adapters/index.js";
|
|
9
|
+
import type { Epic, Prd, Task } from "../server/adapters/types.js";
|
|
10
|
+
import { initDb } from "../server/db/index.js";
|
|
11
|
+
import { openBrowser } from "./browser.js";
|
|
12
|
+
import { startWatchers, stopWatchers } from "./watchers.js";
|
|
13
|
+
|
|
14
|
+
const DEFAULT_PORT = 3333;
|
|
15
|
+
const MAX_PORT_ATTEMPTS = 10;
|
|
16
|
+
|
|
17
|
+
interface DashboardServer {
|
|
18
|
+
server: ReturnType<typeof Bun.serve>;
|
|
19
|
+
port: number;
|
|
20
|
+
stop: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getPort(): number {
|
|
24
|
+
const envPort = process.env.FLUX_DASHBOARD_PORT;
|
|
25
|
+
if (envPort) {
|
|
26
|
+
const parsed = Number.parseInt(envPort, 10);
|
|
27
|
+
if (!Number.isNaN(parsed) && parsed > 0 && parsed < 65536) {
|
|
28
|
+
return parsed;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return DEFAULT_PORT;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function findAvailablePort(startPort: number): Promise<number> {
|
|
35
|
+
for (let port = startPort; port < startPort + MAX_PORT_ATTEMPTS; port++) {
|
|
36
|
+
try {
|
|
37
|
+
const testServer = Bun.serve({
|
|
38
|
+
port,
|
|
39
|
+
fetch: () => new Response("test"),
|
|
40
|
+
});
|
|
41
|
+
testServer.stop();
|
|
42
|
+
return port;
|
|
43
|
+
} catch {}
|
|
44
|
+
}
|
|
45
|
+
throw new Error(
|
|
46
|
+
`No available port found between ${startPort} and ${startPort + MAX_PORT_ATTEMPTS - 1}`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getContentType(path: string): string {
|
|
51
|
+
if (path.endsWith(".html")) return "text/html";
|
|
52
|
+
if (path.endsWith(".css")) return "text/css";
|
|
53
|
+
if (path.endsWith(".js")) return "application/javascript";
|
|
54
|
+
if (path.endsWith(".json")) return "application/json";
|
|
55
|
+
if (path.endsWith(".svg")) return "image/svg+xml";
|
|
56
|
+
return "application/octet-stream";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function startDashboard(): Promise<DashboardServer> {
|
|
60
|
+
const preferredPort = getPort();
|
|
61
|
+
const port = await findAvailablePort(preferredPort);
|
|
62
|
+
|
|
63
|
+
const publicDir = new URL("./public/", import.meta.url).pathname;
|
|
64
|
+
|
|
65
|
+
const server = Bun.serve({
|
|
66
|
+
port,
|
|
67
|
+
|
|
68
|
+
async fetch(req, server) {
|
|
69
|
+
const url = new URL(req.url);
|
|
70
|
+
const path = url.pathname;
|
|
71
|
+
|
|
72
|
+
// WebSocket upgrade
|
|
73
|
+
if (path === "/ws") {
|
|
74
|
+
const upgraded = server.upgrade(req);
|
|
75
|
+
return upgraded
|
|
76
|
+
? undefined
|
|
77
|
+
: new Response("WebSocket upgrade failed", { status: 400 });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// API endpoints
|
|
81
|
+
if (path.startsWith("/api/")) {
|
|
82
|
+
return handleApiRequest(path, url);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Static files
|
|
86
|
+
const filePath = path === "/" ? "/index.html" : path;
|
|
87
|
+
const fullPath = `${publicDir}${filePath}`;
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const file = Bun.file(fullPath);
|
|
91
|
+
if (await file.exists()) {
|
|
92
|
+
return new Response(file, {
|
|
93
|
+
headers: { "Content-Type": getContentType(fullPath) },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
} catch {}
|
|
97
|
+
|
|
98
|
+
return new Response("Not found", { status: 404 });
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
websocket: {
|
|
102
|
+
open(ws) {
|
|
103
|
+
ws.subscribe("updates");
|
|
104
|
+
},
|
|
105
|
+
message(_ws, _message) {
|
|
106
|
+
// Client messages not needed for read-only dashboard
|
|
107
|
+
},
|
|
108
|
+
close(ws) {
|
|
109
|
+
ws.unsubscribe("updates");
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Start file watchers
|
|
115
|
+
startWatchers(server);
|
|
116
|
+
|
|
117
|
+
// Graceful shutdown
|
|
118
|
+
const cleanup = () => {
|
|
119
|
+
console.log("\nShutting down dashboard...");
|
|
120
|
+
stopWatchers();
|
|
121
|
+
server.stop();
|
|
122
|
+
process.exit(0);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
process.on("SIGINT", cleanup);
|
|
126
|
+
process.on("SIGTERM", cleanup);
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
server,
|
|
130
|
+
port,
|
|
131
|
+
stop: () => {
|
|
132
|
+
stopWatchers();
|
|
133
|
+
server.stop();
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
interface TreePrd extends Prd {
|
|
139
|
+
epics: TreeEpic[];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
interface TreeEpic extends Epic {
|
|
143
|
+
tasks: Task[];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function handleApiRequest(path: string, _url: URL): Promise<Response> {
|
|
147
|
+
try {
|
|
148
|
+
const adapter = getAdapter();
|
|
149
|
+
|
|
150
|
+
// GET /api/tree
|
|
151
|
+
if (path === "/api/tree") {
|
|
152
|
+
const prds = await adapter.listPrds({}, { limit: 100, offset: 0 });
|
|
153
|
+
const tree: TreePrd[] = await Promise.all(
|
|
154
|
+
prds.items.map(async (prd) => {
|
|
155
|
+
const epics = await adapter.listEpics(
|
|
156
|
+
{ prdRef: prd.ref },
|
|
157
|
+
{ limit: 100, offset: 0 },
|
|
158
|
+
);
|
|
159
|
+
const epicsWithTasks: TreeEpic[] = await Promise.all(
|
|
160
|
+
epics.items.map(async (epic) => {
|
|
161
|
+
const tasks = await adapter.listTasks(
|
|
162
|
+
{ epicRef: epic.ref },
|
|
163
|
+
{ limit: 100, offset: 0 },
|
|
164
|
+
);
|
|
165
|
+
return { ...epic, tasks: tasks.items };
|
|
166
|
+
}),
|
|
167
|
+
);
|
|
168
|
+
return { ...prd, epics: epicsWithTasks };
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
171
|
+
return Response.json(tree);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// GET /api/prd/:ref
|
|
175
|
+
if (path.startsWith("/api/prd/")) {
|
|
176
|
+
const ref = path.slice("/api/prd/".length);
|
|
177
|
+
const prd = await adapter.getPrd(ref);
|
|
178
|
+
if (!prd) {
|
|
179
|
+
return Response.json({ error: "PRD not found" }, { status: 404 });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let markdown = prd.description || "";
|
|
183
|
+
if (prd.folderPath) {
|
|
184
|
+
try {
|
|
185
|
+
const prdPath = `${prd.folderPath}/prd.md`;
|
|
186
|
+
const file = Bun.file(prdPath);
|
|
187
|
+
if (await file.exists()) {
|
|
188
|
+
markdown = await file.text();
|
|
189
|
+
}
|
|
190
|
+
} catch {}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return Response.json({ ...prd, markdown });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// GET /api/epic/:ref
|
|
197
|
+
if (path.startsWith("/api/epic/")) {
|
|
198
|
+
const ref = path.slice("/api/epic/".length);
|
|
199
|
+
const epic = await adapter.getEpic(ref);
|
|
200
|
+
if (!epic) {
|
|
201
|
+
return Response.json({ error: "Epic not found" }, { status: 404 });
|
|
202
|
+
}
|
|
203
|
+
const criteria = await adapter.getCriteria(ref);
|
|
204
|
+
return Response.json({ ...epic, criteria });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// GET /api/task/:ref
|
|
208
|
+
if (path.startsWith("/api/task/")) {
|
|
209
|
+
const ref = path.slice("/api/task/".length);
|
|
210
|
+
const task = await adapter.getTask(ref);
|
|
211
|
+
if (!task) {
|
|
212
|
+
return Response.json({ error: "Task not found" }, { status: 404 });
|
|
213
|
+
}
|
|
214
|
+
const criteria = await adapter.getCriteria(ref);
|
|
215
|
+
const deps = await adapter.getDependencies(ref);
|
|
216
|
+
return Response.json({ ...task, criteria, dependencies: deps });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// GET /api/tags
|
|
220
|
+
if (path === "/api/tags") {
|
|
221
|
+
const prds = await adapter.listPrds({}, { limit: 1000, offset: 0 });
|
|
222
|
+
const tagCounts = new Map<string, number>();
|
|
223
|
+
let total = 0;
|
|
224
|
+
|
|
225
|
+
for (const prd of prds.items) {
|
|
226
|
+
total++;
|
|
227
|
+
if (prd.tag) {
|
|
228
|
+
tagCounts.set(prd.tag, (tagCounts.get(prd.tag) || 0) + 1);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const tags = [
|
|
233
|
+
{ tag: "All", count: total },
|
|
234
|
+
...Array.from(tagCounts.entries())
|
|
235
|
+
.map(([tag, count]) => ({ tag, count }))
|
|
236
|
+
.sort((a, b) => a.tag.localeCompare(b.tag)),
|
|
237
|
+
];
|
|
238
|
+
|
|
239
|
+
return Response.json(tags);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// GET /api/dependencies
|
|
243
|
+
if (path === "/api/dependencies") {
|
|
244
|
+
const edges: Array<{ from: string; to: string; type: string }> = [];
|
|
245
|
+
|
|
246
|
+
const prds = await adapter.listPrds({}, { limit: 1000, offset: 0 });
|
|
247
|
+
for (const prd of prds.items) {
|
|
248
|
+
const deps = await adapter.getDependencies(prd.ref);
|
|
249
|
+
for (const depRef of deps) {
|
|
250
|
+
edges.push({ from: prd.ref, to: depRef, type: "prd" });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const epics = await adapter.listEpics({}, { limit: 1000, offset: 0 });
|
|
255
|
+
for (const epic of epics.items) {
|
|
256
|
+
const deps = await adapter.getDependencies(epic.ref);
|
|
257
|
+
for (const depRef of deps) {
|
|
258
|
+
edges.push({ from: epic.ref, to: depRef, type: "epic" });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const tasks = await adapter.listTasks({}, { limit: 1000, offset: 0 });
|
|
263
|
+
for (const task of tasks.items) {
|
|
264
|
+
const deps = await adapter.getDependencies(task.ref);
|
|
265
|
+
for (const depRef of deps) {
|
|
266
|
+
edges.push({ from: task.ref, to: depRef, type: "task" });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return Response.json({ edges });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return Response.json({ error: "Unknown endpoint" }, { status: 404 });
|
|
274
|
+
} catch (error) {
|
|
275
|
+
console.error("API error:", error);
|
|
276
|
+
return Response.json(
|
|
277
|
+
{ error: error instanceof Error ? error.message : "Internal error" },
|
|
278
|
+
{ status: 500 },
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Main entry point
|
|
284
|
+
if (import.meta.main) {
|
|
285
|
+
try {
|
|
286
|
+
// Initialize local database (no-op if using external adapter like Linear)
|
|
287
|
+
initDb();
|
|
288
|
+
const { port } = await startDashboard();
|
|
289
|
+
const url = `http://localhost:${port}`;
|
|
290
|
+
console.log(`\n Flux Dashboard running at ${url}\n`);
|
|
291
|
+
await openBrowser(url);
|
|
292
|
+
} catch (error) {
|
|
293
|
+
console.error("Failed to start dashboard:", error);
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Watchers for Real-time Updates
|
|
3
|
+
*
|
|
4
|
+
* Watches .flux/flux.db and .flux/prds/ for changes and broadcasts
|
|
5
|
+
* updates to connected WebSocket clients.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { type FSWatcher, watch } from "node:fs";
|
|
9
|
+
import { resolve } from "node:path";
|
|
10
|
+
|
|
11
|
+
let dbWatcher: FSWatcher | null = null;
|
|
12
|
+
let prdsWatcher: FSWatcher | null = null;
|
|
13
|
+
let debounceTimer: Timer | null = null;
|
|
14
|
+
|
|
15
|
+
const DEBOUNCE_MS = 500;
|
|
16
|
+
|
|
17
|
+
export function startWatchers(server: ReturnType<typeof Bun.serve>): void {
|
|
18
|
+
const projectRoot = process.env.FLUX_PROJECT_ROOT || process.cwd();
|
|
19
|
+
const fluxDir = resolve(projectRoot, ".flux");
|
|
20
|
+
const dbPath = resolve(fluxDir, "flux.db");
|
|
21
|
+
const prdsDir = resolve(fluxDir, "prds");
|
|
22
|
+
|
|
23
|
+
const broadcast = (type: "db" | "file", path?: string) => {
|
|
24
|
+
if (debounceTimer) {
|
|
25
|
+
clearTimeout(debounceTimer);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
debounceTimer = setTimeout(() => {
|
|
29
|
+
server.publish(
|
|
30
|
+
"updates",
|
|
31
|
+
JSON.stringify({
|
|
32
|
+
type: "update",
|
|
33
|
+
source: type,
|
|
34
|
+
path,
|
|
35
|
+
timestamp: Date.now(),
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
}, DEBOUNCE_MS);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Watch database file
|
|
42
|
+
try {
|
|
43
|
+
dbWatcher = watch(dbPath, (eventType) => {
|
|
44
|
+
if (eventType === "change") {
|
|
45
|
+
broadcast("db");
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.warn("Could not watch database file:", error);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Watch PRDs directory recursively
|
|
53
|
+
try {
|
|
54
|
+
prdsWatcher = watch(
|
|
55
|
+
prdsDir,
|
|
56
|
+
{ recursive: true },
|
|
57
|
+
(_eventType, filename) => {
|
|
58
|
+
if (filename?.endsWith(".md")) {
|
|
59
|
+
broadcast("file", filename);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.warn("Could not watch PRDs directory:", error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function stopWatchers(): void {
|
|
69
|
+
if (debounceTimer) {
|
|
70
|
+
clearTimeout(debounceTimer);
|
|
71
|
+
debounceTimer = null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (dbWatcher) {
|
|
75
|
+
dbWatcher.close();
|
|
76
|
+
dbWatcher = null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (prdsWatcher) {
|
|
80
|
+
prdsWatcher.close();
|
|
81
|
+
prdsWatcher = null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
realpathSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
|
|
11
|
+
describe("config", () => {
|
|
12
|
+
const originalEnv = process.env.FLUX_PROJECT_ROOT;
|
|
13
|
+
const originalCwd = process.cwd();
|
|
14
|
+
// Use os.tmpdir() to get the real temp path (handles /tmp -> /private/tmp on macOS)
|
|
15
|
+
const TEST_DIR = `${realpathSync(tmpdir())}/flux-config-test-${Date.now()}`;
|
|
16
|
+
const NESTED_DIR = `${TEST_DIR}/subdir/nested`;
|
|
17
|
+
|
|
18
|
+
beforeEach(async () => {
|
|
19
|
+
// Clean up any previous test directory
|
|
20
|
+
if (existsSync(TEST_DIR)) {
|
|
21
|
+
rmSync(TEST_DIR, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Create test directory structure
|
|
25
|
+
mkdirSync(NESTED_DIR, { recursive: true });
|
|
26
|
+
|
|
27
|
+
// Clear the config cache before each test
|
|
28
|
+
const { config } = await import("../config.js");
|
|
29
|
+
config.clearCache();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(async () => {
|
|
33
|
+
// Restore original env
|
|
34
|
+
if (originalEnv !== undefined) {
|
|
35
|
+
process.env.FLUX_PROJECT_ROOT = originalEnv;
|
|
36
|
+
} else {
|
|
37
|
+
delete process.env.FLUX_PROJECT_ROOT;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Restore original cwd
|
|
41
|
+
process.chdir(originalCwd);
|
|
42
|
+
|
|
43
|
+
// Clear the config cache
|
|
44
|
+
const { config } = await import("../config.js");
|
|
45
|
+
config.clearCache();
|
|
46
|
+
|
|
47
|
+
// Clean up test directory
|
|
48
|
+
if (existsSync(TEST_DIR)) {
|
|
49
|
+
rmSync(TEST_DIR, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("uses env var when properly set", async () => {
|
|
54
|
+
process.env.FLUX_PROJECT_ROOT = "/some/valid/path";
|
|
55
|
+
|
|
56
|
+
const { config } = await import("../config.js");
|
|
57
|
+
config.clearCache();
|
|
58
|
+
|
|
59
|
+
expect(config.projectRoot).toBe("/some/valid/path");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("ignores unresolved template variable and walks up directories", async () => {
|
|
63
|
+
// Create a .flux folder at TEST_DIR
|
|
64
|
+
mkdirSync(`${TEST_DIR}/.flux`, { recursive: true });
|
|
65
|
+
writeFileSync(
|
|
66
|
+
`${TEST_DIR}/.flux/project.json`,
|
|
67
|
+
JSON.stringify({ name: "test" }),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Simulate Claude Code passing unresolved template variable
|
|
71
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: intentionally testing literal template string
|
|
72
|
+
process.env.FLUX_PROJECT_ROOT = "${CLAUDE_PROJECT_DIR}";
|
|
73
|
+
|
|
74
|
+
// Change to nested directory
|
|
75
|
+
process.chdir(NESTED_DIR);
|
|
76
|
+
|
|
77
|
+
const { config } = await import("../config.js");
|
|
78
|
+
config.clearCache();
|
|
79
|
+
|
|
80
|
+
// Should walk up and find TEST_DIR, not use the literal "${CLAUDE_PROJECT_DIR}"
|
|
81
|
+
expect(config.projectRoot).not.toContain("${");
|
|
82
|
+
expect(config.projectRoot).toBe(TEST_DIR);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("walks up directories to find .flux folder", async () => {
|
|
86
|
+
// Create a .flux folder at TEST_DIR (parent of NESTED_DIR)
|
|
87
|
+
mkdirSync(`${TEST_DIR}/.flux`, { recursive: true });
|
|
88
|
+
writeFileSync(
|
|
89
|
+
`${TEST_DIR}/.flux/project.json`,
|
|
90
|
+
JSON.stringify({ name: "test" }),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// No env var set
|
|
94
|
+
delete process.env.FLUX_PROJECT_ROOT;
|
|
95
|
+
|
|
96
|
+
// Change to nested directory
|
|
97
|
+
process.chdir(NESTED_DIR);
|
|
98
|
+
|
|
99
|
+
const { config } = await import("../config.js");
|
|
100
|
+
config.clearCache();
|
|
101
|
+
|
|
102
|
+
// Should walk up and find TEST_DIR
|
|
103
|
+
expect(config.projectRoot).toBe(TEST_DIR);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("falls back to cwd when no .flux folder found", async () => {
|
|
107
|
+
// No .flux folder anywhere in TEST_DIR hierarchy
|
|
108
|
+
// No env var set
|
|
109
|
+
delete process.env.FLUX_PROJECT_ROOT;
|
|
110
|
+
|
|
111
|
+
// Change to test directory (which has no .flux)
|
|
112
|
+
process.chdir(TEST_DIR);
|
|
113
|
+
|
|
114
|
+
const { config } = await import("../config.js");
|
|
115
|
+
config.clearCache();
|
|
116
|
+
|
|
117
|
+
// Should fall back to cwd
|
|
118
|
+
expect(config.projectRoot).toBe(TEST_DIR);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("projectExists returns true when project.json exists", async () => {
|
|
122
|
+
mkdirSync(`${TEST_DIR}/.flux`, { recursive: true });
|
|
123
|
+
writeFileSync(
|
|
124
|
+
`${TEST_DIR}/.flux/project.json`,
|
|
125
|
+
JSON.stringify({ name: "test" }),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
process.env.FLUX_PROJECT_ROOT = TEST_DIR;
|
|
129
|
+
|
|
130
|
+
const { config } = await import("../config.js");
|
|
131
|
+
config.clearCache();
|
|
132
|
+
|
|
133
|
+
expect(config.projectExists).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("projectExists returns false when project.json does not exist", async () => {
|
|
137
|
+
// No .flux folder
|
|
138
|
+
process.env.FLUX_PROJECT_ROOT = TEST_DIR;
|
|
139
|
+
|
|
140
|
+
const { config } = await import("../config.js");
|
|
141
|
+
config.clearCache();
|
|
142
|
+
|
|
143
|
+
expect(config.projectExists).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("caches project root for performance", async () => {
|
|
147
|
+
process.env.FLUX_PROJECT_ROOT = "/first/path";
|
|
148
|
+
|
|
149
|
+
const { config } = await import("../config.js");
|
|
150
|
+
config.clearCache();
|
|
151
|
+
|
|
152
|
+
// First call caches the value
|
|
153
|
+
expect(config.projectRoot).toBe("/first/path");
|
|
154
|
+
|
|
155
|
+
// Changing env var should not affect cached value
|
|
156
|
+
process.env.FLUX_PROJECT_ROOT = "/second/path";
|
|
157
|
+
expect(config.projectRoot).toBe("/first/path");
|
|
158
|
+
|
|
159
|
+
// After clearing cache, should use new env var
|
|
160
|
+
config.clearCache();
|
|
161
|
+
expect(config.projectRoot).toBe("/second/path");
|
|
162
|
+
});
|
|
163
|
+
});
|