backlog-mcp 0.27.1 → 0.27.2
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/dist/cli/bridge.mjs +43 -8
- package/dist/cli/bridge.mjs.map +1 -1
- package/dist/cli/supervisor.d.mts +38 -0
- package/dist/cli/supervisor.mjs +54 -0
- package/dist/cli/supervisor.mjs.map +1 -0
- package/dist/server/fastify-server.mjs +7 -0
- package/dist/server/fastify-server.mjs.map +1 -1
- package/dist/storage/backlog.mjs +9 -1
- package/dist/storage/backlog.mjs.map +1 -1
- package/dist/utils/logger.d.mts +10 -0
- package/dist/utils/logger.mjs +49 -0
- package/dist/utils/logger.mjs.map +1 -0
- package/package.json +1 -1
package/dist/cli/bridge.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { paths } from "../utils/paths.mjs";
|
|
3
|
+
import { logger } from "../utils/logger.mjs";
|
|
3
4
|
import { ensureServer } from "./server-manager.mjs";
|
|
5
|
+
import { DEFAULT_CONFIG, Supervisor } from "./supervisor.mjs";
|
|
4
6
|
import { existsSync } from "node:fs";
|
|
5
7
|
import { spawn } from "node:child_process";
|
|
6
8
|
|
|
@@ -10,18 +12,51 @@ async function runBridge(port) {
|
|
|
10
12
|
const serverUrl = `http://localhost:${port}/mcp`;
|
|
11
13
|
const mcpRemotePath = paths.getBinPath("mcp-remote");
|
|
12
14
|
if (!existsSync(mcpRemotePath)) {
|
|
13
|
-
|
|
15
|
+
logger.error("mcp-remote not found", { path: mcpRemotePath });
|
|
14
16
|
process.exit(1);
|
|
15
17
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
const supervisor = new Supervisor(DEFAULT_CONFIG);
|
|
19
|
+
const spawnBridge = () => {
|
|
20
|
+
supervisor.onStart();
|
|
21
|
+
const bridge = spawn(mcpRemotePath, [
|
|
22
|
+
serverUrl,
|
|
23
|
+
"--allow-http",
|
|
24
|
+
"--transport",
|
|
25
|
+
"http-only"
|
|
26
|
+
], { stdio: [
|
|
27
|
+
"inherit",
|
|
28
|
+
"inherit",
|
|
29
|
+
"pipe"
|
|
30
|
+
] });
|
|
31
|
+
let connectionLost = false;
|
|
32
|
+
bridge.stderr?.on("data", (data) => {
|
|
33
|
+
const msg = data.toString();
|
|
34
|
+
process.stderr.write(msg);
|
|
35
|
+
if (!connectionLost && (msg.includes("ECONNREFUSED") || msg.includes("fetch failed"))) {
|
|
36
|
+
connectionLost = true;
|
|
37
|
+
logger.warn("mcp-remote lost connection, restarting");
|
|
38
|
+
bridge.kill();
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
bridge.on("exit", async (code) => {
|
|
42
|
+
const result = supervisor.onExit(connectionLost ? 1 : code);
|
|
43
|
+
if (result.action === "stop") process.exit(0);
|
|
44
|
+
if (result.action === "give-up") {
|
|
45
|
+
logger.error("mcp-remote crashed too many times", { restarts: result.restartCount });
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
logger.warn("mcp-remote restarting", {
|
|
49
|
+
delay: result.delay,
|
|
50
|
+
attempt: result.restartCount
|
|
51
|
+
});
|
|
52
|
+
await ensureServer(port).catch(() => {});
|
|
53
|
+
setTimeout(spawnBridge, result.delay);
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
spawnBridge();
|
|
22
57
|
}
|
|
23
58
|
runBridge(parseInt(process.env.BACKLOG_VIEWER_PORT || "3030")).catch((error) => {
|
|
24
|
-
|
|
59
|
+
logger.error("Bridge error", { error: String(error) });
|
|
25
60
|
process.exit(1);
|
|
26
61
|
});
|
|
27
62
|
|
package/dist/cli/bridge.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge.mjs","names":[],"sources":["../../src/cli/bridge.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { spawn } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { ensureServer } from './server-manager.js';\nimport { paths } from '@/utils/paths.js';\n\nasync function runBridge(port: number): Promise<void> {\n await ensureServer(port);\n \n const serverUrl = `http://localhost:${port}/mcp`;\n const mcpRemotePath = paths.getBinPath('mcp-remote');\n \n if (!existsSync(mcpRemotePath)) {\n
|
|
1
|
+
{"version":3,"file":"bridge.mjs","names":[],"sources":["../../src/cli/bridge.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { spawn, ChildProcess } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { ensureServer } from './server-manager.js';\nimport { Supervisor, DEFAULT_CONFIG } from './supervisor.js';\nimport { paths } from '@/utils/paths.js';\nimport { logger } from '@/utils/logger.js';\n\nasync function runBridge(port: number): Promise<void> {\n await ensureServer(port);\n \n const serverUrl = `http://localhost:${port}/mcp`;\n const mcpRemotePath = paths.getBinPath('mcp-remote');\n \n if (!existsSync(mcpRemotePath)) {\n logger.error('mcp-remote not found', { path: mcpRemotePath });\n process.exit(1);\n }\n \n const supervisor = new Supervisor(DEFAULT_CONFIG);\n \n const spawnBridge = () => {\n supervisor.onStart();\n \n const bridge = spawn(mcpRemotePath, [serverUrl, '--allow-http', '--transport', 'http-only'], {\n stdio: ['inherit', 'inherit', 'pipe']\n });\n \n let connectionLost = false;\n \n bridge.stderr?.on('data', (data: Buffer) => {\n const msg = data.toString();\n process.stderr.write(msg);\n \n // mcp-remote hangs on connection errors instead of exiting - detect and kill\n if (!connectionLost && (msg.includes('ECONNREFUSED') || msg.includes('fetch failed'))) {\n connectionLost = true;\n logger.warn('mcp-remote lost connection, restarting');\n bridge.kill();\n }\n });\n \n bridge.on('exit', async (code) => {\n const result = supervisor.onExit(connectionLost ? 1 : code);\n \n if (result.action === 'stop') {\n process.exit(0);\n }\n \n if (result.action === 'give-up') {\n logger.error('mcp-remote crashed too many times', { restarts: result.restartCount });\n process.exit(1);\n }\n \n logger.warn('mcp-remote restarting', { delay: result.delay, attempt: result.restartCount });\n \n await ensureServer(port).catch(() => {});\n setTimeout(spawnBridge, result.delay);\n });\n };\n \n spawnBridge();\n}\n\nconst port = parseInt(process.env.BACKLOG_VIEWER_PORT || '3030');\nrunBridge(port).catch((error) => {\n logger.error('Bridge error', { error: String(error) });\n process.exit(1);\n});\n"],"mappings":";;;;;;;;;AASA,eAAe,UAAU,MAA6B;AACpD,OAAM,aAAa,KAAK;CAExB,MAAM,YAAY,oBAAoB,KAAK;CAC3C,MAAM,gBAAgB,MAAM,WAAW,aAAa;AAEpD,KAAI,CAAC,WAAW,cAAc,EAAE;AAC9B,SAAO,MAAM,wBAAwB,EAAE,MAAM,eAAe,CAAC;AAC7D,UAAQ,KAAK,EAAE;;CAGjB,MAAM,aAAa,IAAI,WAAW,eAAe;CAEjD,MAAM,oBAAoB;AACxB,aAAW,SAAS;EAEpB,MAAM,SAAS,MAAM,eAAe;GAAC;GAAW;GAAgB;GAAe;GAAY,EAAE,EAC3F,OAAO;GAAC;GAAW;GAAW;GAAO,EACtC,CAAC;EAEF,IAAI,iBAAiB;AAErB,SAAO,QAAQ,GAAG,SAAS,SAAiB;GAC1C,MAAM,MAAM,KAAK,UAAU;AAC3B,WAAQ,OAAO,MAAM,IAAI;AAGzB,OAAI,CAAC,mBAAmB,IAAI,SAAS,eAAe,IAAI,IAAI,SAAS,eAAe,GAAG;AACrF,qBAAiB;AACjB,WAAO,KAAK,yCAAyC;AACrD,WAAO,MAAM;;IAEf;AAEF,SAAO,GAAG,QAAQ,OAAO,SAAS;GAChC,MAAM,SAAS,WAAW,OAAO,iBAAiB,IAAI,KAAK;AAE3D,OAAI,OAAO,WAAW,OACpB,SAAQ,KAAK,EAAE;AAGjB,OAAI,OAAO,WAAW,WAAW;AAC/B,WAAO,MAAM,qCAAqC,EAAE,UAAU,OAAO,cAAc,CAAC;AACpF,YAAQ,KAAK,EAAE;;AAGjB,UAAO,KAAK,yBAAyB;IAAE,OAAO,OAAO;IAAO,SAAS,OAAO;IAAc,CAAC;AAE3F,SAAM,aAAa,KAAK,CAAC,YAAY,GAAG;AACxC,cAAW,aAAa,OAAO,MAAM;IACrC;;AAGJ,cAAa;;AAIf,UADa,SAAS,QAAQ,IAAI,uBAAuB,OAAO,CACjD,CAAC,OAAO,UAAU;AAC/B,QAAO,MAAM,gBAAgB,EAAE,OAAO,OAAO,MAAM,EAAE,CAAC;AACtD,SAAQ,KAAK,EAAE;EACf"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//#region src/cli/supervisor.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Supervisor manages restart logic with exponential backoff.
|
|
4
|
+
* Extracted for testability - no child process or I/O dependencies.
|
|
5
|
+
*/
|
|
6
|
+
interface SupervisorConfig {
|
|
7
|
+
maxRestarts: number;
|
|
8
|
+
initialDelayMs: number;
|
|
9
|
+
maxDelayMs: number;
|
|
10
|
+
successThresholdMs: number;
|
|
11
|
+
}
|
|
12
|
+
declare const DEFAULT_CONFIG: SupervisorConfig;
|
|
13
|
+
declare class Supervisor {
|
|
14
|
+
private config;
|
|
15
|
+
private restartCount;
|
|
16
|
+
private delay;
|
|
17
|
+
private startTime;
|
|
18
|
+
constructor(config?: SupervisorConfig);
|
|
19
|
+
/** Call when process starts */
|
|
20
|
+
onStart(): void;
|
|
21
|
+
/**
|
|
22
|
+
* Call when process exits. Returns action to take.
|
|
23
|
+
* @param code - exit code (0 = normal, null = signal, other = crash)
|
|
24
|
+
*/
|
|
25
|
+
onExit(code: number | null): {
|
|
26
|
+
action: 'stop' | 'restart' | 'give-up';
|
|
27
|
+
delay?: number;
|
|
28
|
+
restartCount?: number;
|
|
29
|
+
};
|
|
30
|
+
/** Get current state for testing/logging */
|
|
31
|
+
getState(): {
|
|
32
|
+
restartCount: number;
|
|
33
|
+
delay: number;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
export { DEFAULT_CONFIG, Supervisor, SupervisorConfig };
|
|
38
|
+
//# sourceMappingURL=supervisor.d.mts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
//#region src/cli/supervisor.ts
|
|
2
|
+
const DEFAULT_CONFIG = {
|
|
3
|
+
maxRestarts: 10,
|
|
4
|
+
initialDelayMs: 1e3,
|
|
5
|
+
maxDelayMs: 3e4,
|
|
6
|
+
successThresholdMs: 3e4
|
|
7
|
+
};
|
|
8
|
+
var Supervisor = class {
|
|
9
|
+
restartCount = 0;
|
|
10
|
+
delay;
|
|
11
|
+
startTime = 0;
|
|
12
|
+
constructor(config = DEFAULT_CONFIG) {
|
|
13
|
+
this.config = config;
|
|
14
|
+
this.delay = config.initialDelayMs;
|
|
15
|
+
}
|
|
16
|
+
/** Call when process starts */
|
|
17
|
+
onStart() {
|
|
18
|
+
this.startTime = Date.now();
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Call when process exits. Returns action to take.
|
|
22
|
+
* @param code - exit code (0 = normal, null = signal, other = crash)
|
|
23
|
+
*/
|
|
24
|
+
onExit(code) {
|
|
25
|
+
if (code === 0 || code === null) return { action: "stop" };
|
|
26
|
+
if (Date.now() - this.startTime > this.config.successThresholdMs) {
|
|
27
|
+
this.restartCount = 0;
|
|
28
|
+
this.delay = this.config.initialDelayMs;
|
|
29
|
+
}
|
|
30
|
+
this.restartCount++;
|
|
31
|
+
if (this.restartCount > this.config.maxRestarts) return {
|
|
32
|
+
action: "give-up",
|
|
33
|
+
restartCount: this.restartCount
|
|
34
|
+
};
|
|
35
|
+
const currentDelay = this.delay;
|
|
36
|
+
this.delay = Math.min(this.delay * 2, this.config.maxDelayMs);
|
|
37
|
+
return {
|
|
38
|
+
action: "restart",
|
|
39
|
+
delay: currentDelay,
|
|
40
|
+
restartCount: this.restartCount
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** Get current state for testing/logging */
|
|
44
|
+
getState() {
|
|
45
|
+
return {
|
|
46
|
+
restartCount: this.restartCount,
|
|
47
|
+
delay: this.delay
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
//#endregion
|
|
53
|
+
export { DEFAULT_CONFIG, Supervisor };
|
|
54
|
+
//# sourceMappingURL=supervisor.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"supervisor.mjs","names":[],"sources":["../../src/cli/supervisor.ts"],"sourcesContent":["/**\n * Supervisor manages restart logic with exponential backoff.\n * Extracted for testability - no child process or I/O dependencies.\n */\nexport interface SupervisorConfig {\n maxRestarts: number;\n initialDelayMs: number;\n maxDelayMs: number;\n successThresholdMs: number;\n}\n\nexport const DEFAULT_CONFIG: SupervisorConfig = {\n maxRestarts: 10,\n initialDelayMs: 1000,\n maxDelayMs: 30000,\n successThresholdMs: 30000,\n};\n\nexport class Supervisor {\n private restartCount = 0;\n private delay: number;\n private startTime = 0;\n \n constructor(private config: SupervisorConfig = DEFAULT_CONFIG) {\n this.delay = config.initialDelayMs;\n }\n \n /** Call when process starts */\n onStart(): void {\n this.startTime = Date.now();\n }\n \n /** \n * Call when process exits. Returns action to take.\n * @param code - exit code (0 = normal, null = signal, other = crash)\n */\n onExit(code: number | null): { action: 'stop' | 'restart' | 'give-up'; delay?: number; restartCount?: number } {\n // Normal exit or signal - stop\n if (code === 0 || code === null) {\n return { action: 'stop' };\n }\n \n // Reset if ran successfully for a while\n const runDuration = Date.now() - this.startTime;\n if (runDuration > this.config.successThresholdMs) {\n this.restartCount = 0;\n this.delay = this.config.initialDelayMs;\n }\n \n this.restartCount++;\n \n if (this.restartCount > this.config.maxRestarts) {\n return { action: 'give-up', restartCount: this.restartCount };\n }\n \n const currentDelay = this.delay;\n this.delay = Math.min(this.delay * 2, this.config.maxDelayMs);\n \n return { action: 'restart', delay: currentDelay, restartCount: this.restartCount };\n }\n \n /** Get current state for testing/logging */\n getState(): { restartCount: number; delay: number } {\n return { restartCount: this.restartCount, delay: this.delay };\n }\n}\n"],"mappings":";AAWA,MAAa,iBAAmC;CAC9C,aAAa;CACb,gBAAgB;CAChB,YAAY;CACZ,oBAAoB;CACrB;AAED,IAAa,aAAb,MAAwB;CACtB,AAAQ,eAAe;CACvB,AAAQ;CACR,AAAQ,YAAY;CAEpB,YAAY,AAAQ,SAA2B,gBAAgB;EAA3C;AAClB,OAAK,QAAQ,OAAO;;;CAItB,UAAgB;AACd,OAAK,YAAY,KAAK,KAAK;;;;;;CAO7B,OAAO,MAAwG;AAE7G,MAAI,SAAS,KAAK,SAAS,KACzB,QAAO,EAAE,QAAQ,QAAQ;AAK3B,MADoB,KAAK,KAAK,GAAG,KAAK,YACpB,KAAK,OAAO,oBAAoB;AAChD,QAAK,eAAe;AACpB,QAAK,QAAQ,KAAK,OAAO;;AAG3B,OAAK;AAEL,MAAI,KAAK,eAAe,KAAK,OAAO,YAClC,QAAO;GAAE,QAAQ;GAAW,cAAc,KAAK;GAAc;EAG/D,MAAM,eAAe,KAAK;AAC1B,OAAK,QAAQ,KAAK,IAAI,KAAK,QAAQ,GAAG,KAAK,OAAO,WAAW;AAE7D,SAAO;GAAE,QAAQ;GAAW,OAAO;GAAc,cAAc,KAAK;GAAc;;;CAIpF,WAAoD;AAClD,SAAO;GAAE,cAAc,KAAK;GAAc,OAAO,KAAK;GAAO"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { paths } from "../utils/paths.mjs";
|
|
3
|
+
import { logger } from "../utils/logger.mjs";
|
|
3
4
|
import { registerViewerRoutes } from "./viewer-routes.mjs";
|
|
4
5
|
import { registerMcpHandler } from "./mcp-handler.mjs";
|
|
5
6
|
import { authMiddleware } from "../middleware/auth.mjs";
|
|
@@ -26,12 +27,18 @@ async function startHttpServer(port = 3030) {
|
|
|
26
27
|
port,
|
|
27
28
|
host: "0.0.0.0"
|
|
28
29
|
});
|
|
30
|
+
logger.info("Server started", {
|
|
31
|
+
port,
|
|
32
|
+
dataDir: paths.backlogDataDir,
|
|
33
|
+
version: paths.getVersion()
|
|
34
|
+
});
|
|
29
35
|
console.log(`Backlog MCP server running on http://localhost:${port}`);
|
|
30
36
|
console.log(`- Viewer: http://localhost:${port}/`);
|
|
31
37
|
console.log(`- MCP endpoint: http://localhost:${port}/mcp`);
|
|
32
38
|
console.log(`- Data directory: ${paths.backlogDataDir}`);
|
|
33
39
|
}
|
|
34
40
|
const shutdown = async () => {
|
|
41
|
+
logger.info("Server shutting down");
|
|
35
42
|
console.log("Shutting down gracefully...");
|
|
36
43
|
await app.close();
|
|
37
44
|
setTimeout(() => process.exit(0), 500);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fastify-server.mjs","names":[],"sources":["../../src/server/fastify-server.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport Fastify from 'fastify';\nimport cors from '@fastify/cors';\nimport { registerViewerRoutes } from './viewer-routes.js';\nimport { registerMcpHandler } from './mcp-handler.js';\nimport { authMiddleware } from '@/middleware/auth.js';\nimport { paths } from '@/utils/paths.js';\n\nconst app = Fastify({ logger: false, bodyLimit: 10 * 1024 * 1024 });\n\n// CORS\nawait app.register(cors, { origin: '*' });\n\n// Auth middleware\napp.addHook('preHandler', authMiddleware);\n\n// Register routes\nregisterViewerRoutes(app);\nregisterMcpHandler(app);\n\n// Health check\napp.get('/health', async () => ({ status: 'ok' }));\n\n// Version endpoint\napp.get('/version', async () => paths.getVersion());\n\n// Shutdown endpoint\napp.post('/shutdown', async (request, reply) => {\n reply.send('Shutting down...');\n setTimeout(() => process.exit(0), 500);\n});\n\nexport async function startHttpServer(port: number = 3030): Promise<void> {\n await app.listen({ port, host: '0.0.0.0' });\n console.log(`Backlog MCP server running on http://localhost:${port}`);\n console.log(`- Viewer: http://localhost:${port}/`);\n console.log(`- MCP endpoint: http://localhost:${port}/mcp`);\n console.log(`- Data directory: ${paths.backlogDataDir}`);\n}\n\n// Graceful shutdown\nconst shutdown = async () => {\n console.log('Shutting down gracefully...');\n await app.close();\n setTimeout(() => process.exit(0), 500);\n};\n\nprocess.on('SIGTERM', shutdown);\nprocess.on('SIGINT', shutdown);\n\n// Run if executed directly\nif (import.meta.url === `file://${process.argv[1]}`) {\n const port = parseInt(process.env.BACKLOG_VIEWER_PORT || '3030');\n startHttpServer(port).catch((error) => {\n console.error('Failed to start server:', error);\n process.exit(1);\n });\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"fastify-server.mjs","names":[],"sources":["../../src/server/fastify-server.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport Fastify from 'fastify';\nimport cors from '@fastify/cors';\nimport { registerViewerRoutes } from './viewer-routes.js';\nimport { registerMcpHandler } from './mcp-handler.js';\nimport { authMiddleware } from '@/middleware/auth.js';\nimport { paths } from '@/utils/paths.js';\nimport { logger } from '@/utils/logger.js';\n\nconst app = Fastify({ logger: false, bodyLimit: 10 * 1024 * 1024 });\n\n// CORS\nawait app.register(cors, { origin: '*' });\n\n// Auth middleware\napp.addHook('preHandler', authMiddleware);\n\n// Register routes\nregisterViewerRoutes(app);\nregisterMcpHandler(app);\n\n// Health check\napp.get('/health', async () => ({ status: 'ok' }));\n\n// Version endpoint\napp.get('/version', async () => paths.getVersion());\n\n// Shutdown endpoint\napp.post('/shutdown', async (request, reply) => {\n reply.send('Shutting down...');\n setTimeout(() => process.exit(0), 500);\n});\n\nexport async function startHttpServer(port: number = 3030): Promise<void> {\n await app.listen({ port, host: '0.0.0.0' });\n logger.info('Server started', { port, dataDir: paths.backlogDataDir, version: paths.getVersion() });\n console.log(`Backlog MCP server running on http://localhost:${port}`);\n console.log(`- Viewer: http://localhost:${port}/`);\n console.log(`- MCP endpoint: http://localhost:${port}/mcp`);\n console.log(`- Data directory: ${paths.backlogDataDir}`);\n}\n\n// Graceful shutdown\nconst shutdown = async () => {\n logger.info('Server shutting down');\n console.log('Shutting down gracefully...');\n await app.close();\n setTimeout(() => process.exit(0), 500);\n};\n\nprocess.on('SIGTERM', shutdown);\nprocess.on('SIGINT', shutdown);\n\n// Run if executed directly\nif (import.meta.url === `file://${process.argv[1]}`) {\n const port = parseInt(process.env.BACKLOG_VIEWER_PORT || '3030');\n startHttpServer(port).catch((error) => {\n console.error('Failed to start server:', error);\n process.exit(1);\n });\n}\n"],"mappings":";;;;;;;;;;AAUA,MAAM,MAAM,QAAQ;CAAE,QAAQ;CAAO,WAAW,KAAK,OAAO;CAAM,CAAC;AAGnE,MAAM,IAAI,SAAS,MAAM,EAAE,QAAQ,KAAK,CAAC;AAGzC,IAAI,QAAQ,cAAc,eAAe;AAGzC,qBAAqB,IAAI;AACzB,mBAAmB,IAAI;AAGvB,IAAI,IAAI,WAAW,aAAa,EAAE,QAAQ,MAAM,EAAE;AAGlD,IAAI,IAAI,YAAY,YAAY,MAAM,YAAY,CAAC;AAGnD,IAAI,KAAK,aAAa,OAAO,SAAS,UAAU;AAC9C,OAAM,KAAK,mBAAmB;AAC9B,kBAAiB,QAAQ,KAAK,EAAE,EAAE,IAAI;EACtC;AAEF,eAAsB,gBAAgB,OAAe,MAAqB;AACxE,OAAM,IAAI,OAAO;EAAE;EAAM,MAAM;EAAW,CAAC;AAC3C,QAAO,KAAK,kBAAkB;EAAE;EAAM,SAAS,MAAM;EAAgB,SAAS,MAAM,YAAY;EAAE,CAAC;AACnG,SAAQ,IAAI,kDAAkD,OAAO;AACrE,SAAQ,IAAI,8BAA8B,KAAK,GAAG;AAClD,SAAQ,IAAI,oCAAoC,KAAK,MAAM;AAC3D,SAAQ,IAAI,qBAAqB,MAAM,iBAAiB;;AAI1D,MAAM,WAAW,YAAY;AAC3B,QAAO,KAAK,uBAAuB;AACnC,SAAQ,IAAI,8BAA8B;AAC1C,OAAM,IAAI,OAAO;AACjB,kBAAiB,QAAQ,KAAK,EAAE,EAAE,IAAI;;AAGxC,QAAQ,GAAG,WAAW,SAAS;AAC/B,QAAQ,GAAG,UAAU,SAAS;AAG9B,IAAI,OAAO,KAAK,QAAQ,UAAU,QAAQ,KAAK,KAE7C,iBADa,SAAS,QAAQ,IAAI,uBAAuB,OAAO,CAC3C,CAAC,OAAO,UAAU;AACrC,SAAQ,MAAM,2BAA2B,MAAM;AAC/C,SAAQ,KAAK,EAAE;EACf"}
|
package/dist/storage/backlog.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { paths } from "../utils/paths.mjs";
|
|
2
|
+
import { logger } from "../utils/logger.mjs";
|
|
2
3
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import matter from "gray-matter";
|
|
@@ -43,7 +44,14 @@ var BacklogStorage = class BacklogStorage {
|
|
|
43
44
|
if (!task.id) continue;
|
|
44
45
|
yield task;
|
|
45
46
|
} catch (error) {
|
|
46
|
-
if (error.code !== "ENOENT")
|
|
47
|
+
if (error.code !== "ENOENT") {
|
|
48
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
49
|
+
logger.warn("Malformed task file", {
|
|
50
|
+
file,
|
|
51
|
+
error: errorMessage
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
continue;
|
|
47
55
|
}
|
|
48
56
|
}
|
|
49
57
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backlog.mjs","names":[],"sources":["../../src/storage/backlog.ts"],"sourcesContent":["import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, unlinkSync, rmSync } from 'node:fs';\nimport { join } from 'node:path';\nimport matter from 'gray-matter';\nimport type { Task, Status, TaskType } from './schema.js';\nimport { paths } from '@/utils/paths.js';\n\nconst TASKS_DIR = 'tasks';\n\nclass BacklogStorage {\n private static instance: BacklogStorage;\n\n static getInstance(): BacklogStorage {\n if (!BacklogStorage.instance) {\n BacklogStorage.instance = new BacklogStorage();\n }\n return BacklogStorage.instance;\n }\n\n private get tasksPath(): string {\n return join(paths.backlogDataDir, TASKS_DIR);\n }\n\n private ensureDir(dir: string): void {\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n }\n\n private taskFilePath(id: string): string {\n return join(this.tasksPath, `${id}.md`);\n }\n\n private taskToMarkdown(task: Task): string {\n const { description, ...frontmatter } = task;\n return matter.stringify(description || '', frontmatter);\n }\n\n private markdownToTask(content: string): Task {\n const { data, content: description } = matter(content);\n return { ...data, description: description.trim() } as Task;\n }\n\n getFilePath(id: string): string | null {\n const path = this.taskFilePath(id);\n return existsSync(path) ? path : null;\n }\n\n private *iterateTasks(): Generator<Task> {\n if (existsSync(this.tasksPath)) {\n for (const file of readdirSync(this.tasksPath).filter(f => f.endsWith('.md'))) {\n const filePath = join(this.tasksPath, file);\n try {\n const task = this.markdownToTask(readFileSync(filePath, 'utf-8'));\n // Skip malformed tasks without valid ID\n if (!task.id) continue;\n yield task;\n } catch (error) {\n // Skip files that
|
|
1
|
+
{"version":3,"file":"backlog.mjs","names":[],"sources":["../../src/storage/backlog.ts"],"sourcesContent":["import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, unlinkSync, rmSync } from 'node:fs';\nimport { join } from 'node:path';\nimport matter from 'gray-matter';\nimport type { Task, Status, TaskType } from './schema.js';\nimport { paths } from '@/utils/paths.js';\nimport { logger } from '@/utils/logger.js';\n\nconst TASKS_DIR = 'tasks';\n\nclass BacklogStorage {\n private static instance: BacklogStorage;\n\n static getInstance(): BacklogStorage {\n if (!BacklogStorage.instance) {\n BacklogStorage.instance = new BacklogStorage();\n }\n return BacklogStorage.instance;\n }\n\n private get tasksPath(): string {\n return join(paths.backlogDataDir, TASKS_DIR);\n }\n\n private ensureDir(dir: string): void {\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n }\n\n private taskFilePath(id: string): string {\n return join(this.tasksPath, `${id}.md`);\n }\n\n private taskToMarkdown(task: Task): string {\n const { description, ...frontmatter } = task;\n return matter.stringify(description || '', frontmatter);\n }\n\n private markdownToTask(content: string): Task {\n const { data, content: description } = matter(content);\n return { ...data, description: description.trim() } as Task;\n }\n\n getFilePath(id: string): string | null {\n const path = this.taskFilePath(id);\n return existsSync(path) ? path : null;\n }\n\n private *iterateTasks(): Generator<Task> {\n if (existsSync(this.tasksPath)) {\n for (const file of readdirSync(this.tasksPath).filter(f => f.endsWith('.md'))) {\n const filePath = join(this.tasksPath, file);\n try {\n const task = this.markdownToTask(readFileSync(filePath, 'utf-8'));\n // Skip malformed tasks without valid ID\n if (!task.id) continue;\n yield task;\n } catch (error) {\n // Skip files that fail to parse (deleted, malformed YAML, etc.)\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {\n const errorMessage = error instanceof Error ? error.message : String(error);\n logger.warn('Malformed task file', { file, error: errorMessage });\n }\n continue;\n }\n }\n }\n }\n\n get(id: string): Task | undefined {\n const path = this.taskFilePath(id);\n if (existsSync(path)) {\n return this.markdownToTask(readFileSync(path, 'utf-8'));\n }\n return undefined;\n }\n\n getMarkdown(id: string): string | null {\n const path = this.taskFilePath(id);\n if (existsSync(path)) {\n return readFileSync(path, 'utf-8');\n }\n return null;\n }\n\n list(filter?: { status?: Status[]; type?: TaskType; epic_id?: string; limit?: number }): Task[] {\n const { status, type, epic_id, limit = 20 } = filter ?? {};\n\n let tasks = Array.from(this.iterateTasks());\n \n if (status) tasks = tasks.filter(t => status.includes(t.status));\n if (type) tasks = tasks.filter(t => (t.type ?? 'task') === type);\n if (epic_id) tasks = tasks.filter(t => t.epic_id === epic_id);\n\n return tasks\n .sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime())\n .slice(0, limit);\n }\n\n add(task: Task): void {\n this.ensureDir(this.tasksPath);\n const filePath = this.taskFilePath(task.id);\n writeFileSync(filePath, this.taskToMarkdown(task));\n }\n\n save(task: Task): void {\n this.ensureDir(this.tasksPath);\n const filePath = this.taskFilePath(task.id);\n writeFileSync(filePath, this.taskToMarkdown(task));\n }\n\n delete(id: string): boolean {\n const path = this.taskFilePath(id);\n if (existsSync(path)) {\n unlinkSync(path);\n \n // Delete associated resources if they exist\n const resourcesPath = join(paths.backlogDataDir, 'resources', id);\n if (existsSync(resourcesPath)) {\n rmSync(resourcesPath, { recursive: true, force: true });\n }\n \n return true;\n }\n return false;\n }\n\n counts(): { total_tasks: number; total_epics: number; by_status: Record<Status, number> } {\n const by_status: Record<Status, number> = {\n open: 0,\n in_progress: 0,\n blocked: 0,\n done: 0,\n cancelled: 0,\n };\n\n let total_tasks = 0;\n let total_epics = 0;\n\n for (const task of this.iterateTasks()) {\n by_status[task.status]++;\n if ((task.type ?? 'task') === 'epic') {\n total_epics++;\n } else {\n total_tasks++;\n }\n }\n\n return { total_tasks, total_epics, by_status };\n }\n\n getMaxId(type?: 'task' | 'epic'): number {\n const pattern = type === 'epic' ? /^EPIC-(\\d{4,})\\.md$/ : /^TASK-(\\d{4,})\\.md$/;\n let maxNum = 0;\n\n if (existsSync(this.tasksPath)) {\n for (const file of readdirSync(this.tasksPath)) {\n const match = pattern.exec(file);\n if (match?.[1]) {\n const num = parseInt(match[1], 10);\n if (num > maxNum) maxNum = num;\n }\n }\n }\n\n return maxNum;\n }\n}\n\nexport const storage = BacklogStorage.getInstance();\n"],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/utils/logger.d.ts
|
|
2
|
+
declare const logger: {
|
|
3
|
+
debug: (message: string, data?: Record<string, unknown>) => void;
|
|
4
|
+
info: (message: string, data?: Record<string, unknown>) => void;
|
|
5
|
+
warn: (message: string, data?: Record<string, unknown>) => void;
|
|
6
|
+
error: (message: string, data?: Record<string, unknown>) => void;
|
|
7
|
+
};
|
|
8
|
+
//#endregion
|
|
9
|
+
export { logger };
|
|
10
|
+
//# sourceMappingURL=logger.d.mts.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { paths } from "./paths.mjs";
|
|
2
|
+
import { appendFile, existsSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
//#region src/utils/logger.ts
|
|
6
|
+
const LEVELS = [
|
|
7
|
+
"debug",
|
|
8
|
+
"info",
|
|
9
|
+
"warn",
|
|
10
|
+
"error"
|
|
11
|
+
];
|
|
12
|
+
const levelPriority = {
|
|
13
|
+
debug: 0,
|
|
14
|
+
info: 1,
|
|
15
|
+
warn: 2,
|
|
16
|
+
error: 3
|
|
17
|
+
};
|
|
18
|
+
function getLogLevel() {
|
|
19
|
+
const env = process.env.LOG_LEVEL?.toLowerCase();
|
|
20
|
+
return LEVELS.includes(env) ? env : "info";
|
|
21
|
+
}
|
|
22
|
+
function getLogFile() {
|
|
23
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
24
|
+
return join(paths.backlogDataDir, "logs", `backlog-${date}.log`);
|
|
25
|
+
}
|
|
26
|
+
function write(level, message, data) {
|
|
27
|
+
if (levelPriority[level] < levelPriority[getLogLevel()]) return;
|
|
28
|
+
const logDir = join(paths.backlogDataDir, "logs");
|
|
29
|
+
if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
|
|
30
|
+
const entry = JSON.stringify({
|
|
31
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
32
|
+
level,
|
|
33
|
+
message,
|
|
34
|
+
...data
|
|
35
|
+
});
|
|
36
|
+
appendFile(getLogFile(), entry + "\n", (err) => {
|
|
37
|
+
if (err) process.stderr.write(`Logger error: ${err.message}\n`);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const logger = {
|
|
41
|
+
debug: (message, data) => write("debug", message, data),
|
|
42
|
+
info: (message, data) => write("info", message, data),
|
|
43
|
+
warn: (message, data) => write("warn", message, data),
|
|
44
|
+
error: (message, data) => write("error", message, data)
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
//#endregion
|
|
48
|
+
export { logger };
|
|
49
|
+
//# sourceMappingURL=logger.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.mjs","names":[],"sources":["../../src/utils/logger.ts"],"sourcesContent":["import { appendFile, mkdirSync, existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { paths } from './paths.js';\n\nconst LEVELS = ['debug', 'info', 'warn', 'error'] as const;\ntype Level = (typeof LEVELS)[number];\n\nconst levelPriority: Record<Level, number> = { debug: 0, info: 1, warn: 2, error: 3 };\n\nfunction getLogLevel(): Level {\n const env = process.env.LOG_LEVEL?.toLowerCase();\n return LEVELS.includes(env as Level) ? (env as Level) : 'info';\n}\n\nfunction getLogFile(): string {\n const date = new Date().toISOString().split('T')[0];\n return join(paths.backlogDataDir, 'logs', `backlog-${date}.log`);\n}\n\nfunction write(level: Level, message: string, data?: Record<string, unknown>): void {\n if (levelPriority[level] < levelPriority[getLogLevel()]) return;\n\n const logDir = join(paths.backlogDataDir, 'logs');\n if (!existsSync(logDir)) {\n mkdirSync(logDir, { recursive: true });\n }\n\n const entry = JSON.stringify({\n timestamp: new Date().toISOString(),\n level,\n message,\n ...data,\n });\n\n appendFile(getLogFile(), entry + '\\n', (err) => {\n if (err) {\n process.stderr.write(`Logger error: ${err.message}\\n`);\n }\n });\n}\n\nexport const logger = {\n debug: (message: string, data?: Record<string, unknown>) => write('debug', message, data),\n info: (message: string, data?: Record<string, unknown>) => write('info', message, data),\n warn: (message: string, data?: Record<string, unknown>) => write('warn', message, data),\n error: (message: string, data?: Record<string, unknown>) => write('error', message, data),\n};\n"],"mappings":";;;;;AAIA,MAAM,SAAS;CAAC;CAAS;CAAQ;CAAQ;CAAQ;AAGjD,MAAM,gBAAuC;CAAE,OAAO;CAAG,MAAM;CAAG,MAAM;CAAG,OAAO;CAAG;AAErF,SAAS,cAAqB;CAC5B,MAAM,MAAM,QAAQ,IAAI,WAAW,aAAa;AAChD,QAAO,OAAO,SAAS,IAAa,GAAI,MAAgB;;AAG1D,SAAS,aAAqB;CAC5B,MAAM,wBAAO,IAAI,MAAM,EAAC,aAAa,CAAC,MAAM,IAAI,CAAC;AACjD,QAAO,KAAK,MAAM,gBAAgB,QAAQ,WAAW,KAAK,MAAM;;AAGlE,SAAS,MAAM,OAAc,SAAiB,MAAsC;AAClF,KAAI,cAAc,SAAS,cAAc,aAAa,EAAG;CAEzD,MAAM,SAAS,KAAK,MAAM,gBAAgB,OAAO;AACjD,KAAI,CAAC,WAAW,OAAO,CACrB,WAAU,QAAQ,EAAE,WAAW,MAAM,CAAC;CAGxC,MAAM,QAAQ,KAAK,UAAU;EAC3B,4BAAW,IAAI,MAAM,EAAC,aAAa;EACnC;EACA;EACA,GAAG;EACJ,CAAC;AAEF,YAAW,YAAY,EAAE,QAAQ,OAAO,QAAQ;AAC9C,MAAI,IACF,SAAQ,OAAO,MAAM,iBAAiB,IAAI,QAAQ,IAAI;GAExD;;AAGJ,MAAa,SAAS;CACpB,QAAQ,SAAiB,SAAmC,MAAM,SAAS,SAAS,KAAK;CACzF,OAAO,SAAiB,SAAmC,MAAM,QAAQ,SAAS,KAAK;CACvF,OAAO,SAAiB,SAAmC,MAAM,QAAQ,SAAS,KAAK;CACvF,QAAQ,SAAiB,SAAmC,MAAM,SAAS,SAAS,KAAK;CAC1F"}
|