project-mgr 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/CHANGELOG.md +17 -0
- package/README.md +22 -1
- package/README_CN.md +1 -1
- package/dist/cli.js +37 -32
- package/dist/db.d.ts +33 -1
- package/dist/db.js +259 -10
- package/dist/domain.d.ts +12 -1
- package/dist/domain.js +9 -0
- package/dist/server.js +19 -2
- package/dist/web/i18n.d.ts +8 -0
- package/dist/web/i18n.js +77 -0
- package/dist/web/locales/en.d.ts +2 -0
- package/dist/web/locales/en.js +52 -0
- package/dist/web/locales/zh.d.ts +54 -0
- package/dist/web/locales/zh.js +52 -0
- package/dist/web/script.d.ts +2 -0
- package/dist/web/script.js +356 -0
- package/dist/web/styles.d.ts +1 -0
- package/dist/web/styles.js +636 -0
- package/dist/web/ui.d.ts +3 -1
- package/dist/web/ui.js +33 -924
- package/package.json +8 -2
- package/skills/project-mgr/SKILL.md +39 -16
- package/.claude/settings.local.json +0 -5
- package/.env_sample +0 -7
- package/.prettierignore +0 -4
- package/AGENTS.md +0 -5
- package/docs/architecture.md +0 -95
- package/eslint.config.js +0 -12
- package/pnpm-workspace.yaml +0 -3
- package/src/cli.ts +0 -399
- package/src/db.ts +0 -373
- package/src/domain.ts +0 -226
- package/src/index.ts +0 -5
- package/src/server.ts +0 -202
- package/src/web/ui.ts +0 -977
- package/test/core.test.ts +0 -739
- package/test/server.test.ts +0 -354
- package/tsconfig.json +0 -14
package/test/server.test.ts
DELETED
|
@@ -1,354 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { EventEmitter } from "node:events";
|
|
3
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
4
|
-
import { type IncomingMessage, type ServerResponse } from "node:http";
|
|
5
|
-
import { tmpdir } from "node:os";
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
-
import test from "node:test";
|
|
8
|
-
import {
|
|
9
|
-
addStatus,
|
|
10
|
-
getWebUIHTML,
|
|
11
|
-
handleRequest,
|
|
12
|
-
openBrowser,
|
|
13
|
-
openLedger,
|
|
14
|
-
run,
|
|
15
|
-
setProjectMetadata,
|
|
16
|
-
startServer,
|
|
17
|
-
} from "../src/index.js";
|
|
18
|
-
|
|
19
|
-
type MockResponse = {
|
|
20
|
-
statusCode: number;
|
|
21
|
-
headers: Record<string, string>;
|
|
22
|
-
body: string;
|
|
23
|
-
json: () => unknown;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
function simulateRequest(
|
|
27
|
-
db: ReturnType<typeof openLedger>,
|
|
28
|
-
method: string,
|
|
29
|
-
url?: string,
|
|
30
|
-
hostHeader?: string,
|
|
31
|
-
): MockResponse {
|
|
32
|
-
const req = new EventEmitter() as unknown as IncomingMessage;
|
|
33
|
-
req.method = method;
|
|
34
|
-
req.url = url as string;
|
|
35
|
-
req.headers = hostHeader ? { host: hostHeader } : {};
|
|
36
|
-
|
|
37
|
-
let statusCode = 200;
|
|
38
|
-
let headers: Record<string, string> = {};
|
|
39
|
-
let body = "";
|
|
40
|
-
|
|
41
|
-
const res = {
|
|
42
|
-
writeHead(code: number, hdrs: Record<string, string>) {
|
|
43
|
-
statusCode = code;
|
|
44
|
-
headers = hdrs;
|
|
45
|
-
},
|
|
46
|
-
end(data?: string) {
|
|
47
|
-
if (data) body += data;
|
|
48
|
-
},
|
|
49
|
-
} as unknown as ServerResponse;
|
|
50
|
-
|
|
51
|
-
handleRequest(req, res, db);
|
|
52
|
-
|
|
53
|
-
return {
|
|
54
|
-
statusCode,
|
|
55
|
-
headers,
|
|
56
|
-
body,
|
|
57
|
-
json: () => (body ? JSON.parse(body) : null),
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
test("server handles Web UI and REST API request routing", () => {
|
|
62
|
-
const db = openLedger(":memory:");
|
|
63
|
-
setProjectMetadata(db, {
|
|
64
|
-
project: "alpha",
|
|
65
|
-
displayName: "Alpha Project",
|
|
66
|
-
description: "Alpha description",
|
|
67
|
-
notes: "Alpha note",
|
|
68
|
-
});
|
|
69
|
-
addStatus(db, {
|
|
70
|
-
project: "alpha",
|
|
71
|
-
status: "active",
|
|
72
|
-
summary: "Alpha started",
|
|
73
|
-
progress: "50%",
|
|
74
|
-
blockers: ["B1"],
|
|
75
|
-
nextSteps: ["N1"],
|
|
76
|
-
decisions: ["D1"],
|
|
77
|
-
tags: ["t1"],
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
setProjectMetadata(db, {
|
|
81
|
-
project: "beta",
|
|
82
|
-
displayName: "Beta Project",
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// Test GET / with empty/undefined url and host header
|
|
86
|
-
const rootRes = simulateRequest(db, "GET", "", "customhost:8080");
|
|
87
|
-
assert.equal(rootRes.statusCode, 200);
|
|
88
|
-
assert.equal(rootRes.headers["Content-Type"], "text/html; charset=utf-8");
|
|
89
|
-
assert.ok(rootRes.body.includes("Project Manager"));
|
|
90
|
-
|
|
91
|
-
// Test GET /index.html
|
|
92
|
-
const indexRes = simulateRequest(db, "GET", "/index.html");
|
|
93
|
-
assert.equal(indexRes.statusCode, 200);
|
|
94
|
-
|
|
95
|
-
// Test GET /api/projects
|
|
96
|
-
const projectsRes = simulateRequest(db, "GET", "/api/projects");
|
|
97
|
-
assert.equal(projectsRes.statusCode, 200);
|
|
98
|
-
assert.equal((projectsRes.json() as unknown[]).length, 2);
|
|
99
|
-
|
|
100
|
-
// Test GET /api/projects?query=alpha
|
|
101
|
-
const queryRes = simulateRequest(db, "GET", "/api/projects?query=alpha");
|
|
102
|
-
assert.equal(queryRes.statusCode, 200);
|
|
103
|
-
assert.equal((queryRes.json() as unknown[]).length, 1);
|
|
104
|
-
|
|
105
|
-
// Test GET /api/projects/:slug (found)
|
|
106
|
-
const projAlphaRes = simulateRequest(db, "GET", "/api/projects/alpha");
|
|
107
|
-
assert.equal(projAlphaRes.statusCode, 200);
|
|
108
|
-
assert.equal((projAlphaRes.json() as { project: string }).project, "alpha");
|
|
109
|
-
|
|
110
|
-
// Test GET /api/projects/:slug (not found)
|
|
111
|
-
const proj404Res = simulateRequest(db, "GET", "/api/projects/nonexistent");
|
|
112
|
-
assert.equal(proj404Res.statusCode, 404);
|
|
113
|
-
assert.equal(
|
|
114
|
-
(proj404Res.json() as { error: string }).error,
|
|
115
|
-
"Project not found",
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
// Test GET /api/history without query params
|
|
119
|
-
const defaultHistoryRes = simulateRequest(db, "GET", "/api/history");
|
|
120
|
-
assert.equal(defaultHistoryRes.statusCode, 200);
|
|
121
|
-
|
|
122
|
-
// Test GET /api/history with valid numeric limit and offset
|
|
123
|
-
const historyRes = simulateRequest(
|
|
124
|
-
db,
|
|
125
|
-
"GET",
|
|
126
|
-
"/api/history?project=alpha&limit=10&offset=5",
|
|
127
|
-
);
|
|
128
|
-
assert.equal(historyRes.statusCode, 200);
|
|
129
|
-
|
|
130
|
-
// Test GET /api/history with fallback limit/offset NaN
|
|
131
|
-
const historyFallbackRes = simulateRequest(
|
|
132
|
-
db,
|
|
133
|
-
"GET",
|
|
134
|
-
"/api/history?limit=invalid&offset=invalid",
|
|
135
|
-
);
|
|
136
|
-
assert.equal(historyFallbackRes.statusCode, 200);
|
|
137
|
-
|
|
138
|
-
// Test GET /api/stats
|
|
139
|
-
const statsRes = simulateRequest(db, "GET", "/api/stats");
|
|
140
|
-
assert.equal(statsRes.statusCode, 200);
|
|
141
|
-
const stats = statsRes.json() as {
|
|
142
|
-
totalProjects: number;
|
|
143
|
-
totalEntries: number;
|
|
144
|
-
statusCounts: Record<string, number>;
|
|
145
|
-
};
|
|
146
|
-
assert.equal(stats.totalProjects, 2);
|
|
147
|
-
assert.equal(stats.totalEntries, 1);
|
|
148
|
-
assert.equal(stats.statusCounts.active, 1);
|
|
149
|
-
assert.equal(stats.statusCounts.none, 1);
|
|
150
|
-
|
|
151
|
-
// Test 404 for unknown route
|
|
152
|
-
const notFoundRes = simulateRequest(db, "GET", "/api/unknown");
|
|
153
|
-
assert.equal(notFoundRes.statusCode, 404);
|
|
154
|
-
|
|
155
|
-
// Test 405 Method Not Allowed
|
|
156
|
-
const postRes = simulateRequest(db, "POST", "/api/projects");
|
|
157
|
-
assert.equal(postRes.statusCode, 405);
|
|
158
|
-
|
|
159
|
-
db.close();
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
test("startServer initializes HTTP server, closes clean, and handles 500 error boundary", async () => {
|
|
163
|
-
const directory = mkdtempSync(join(tmpdir(), "pmgr-server-"));
|
|
164
|
-
const dbFile = join(directory, "test-server.sqlite");
|
|
165
|
-
const previousDb = process.env.PMGR_DB_PATH;
|
|
166
|
-
const previousPort = process.env.PORT;
|
|
167
|
-
process.env.PMGR_DB_PATH = dbFile;
|
|
168
|
-
process.env.PORT = "0";
|
|
169
|
-
|
|
170
|
-
// Test startServer with custom host and PORT env
|
|
171
|
-
const serverWithHost = await startServer({ host: "localhost", open: false });
|
|
172
|
-
assert.equal(serverWithHost.host, "localhost");
|
|
173
|
-
await serverWithHost.close();
|
|
174
|
-
|
|
175
|
-
delete process.env.PORT;
|
|
176
|
-
const userDb = openLedger(":memory:");
|
|
177
|
-
const serverWithDb = await startServer({ port: 0, open: false, db: userDb });
|
|
178
|
-
assert.ok(serverWithDb.url.length > 0);
|
|
179
|
-
await serverWithDb.close();
|
|
180
|
-
userDb.close();
|
|
181
|
-
|
|
182
|
-
// Test startServer with default options (options = undefined / empty)
|
|
183
|
-
const defaultServerInst = await startServer({ port: 0 });
|
|
184
|
-
assert.ok(defaultServerInst.url.length > 0);
|
|
185
|
-
await defaultServerInst.close();
|
|
186
|
-
|
|
187
|
-
const serverInst = await startServer({ port: 0, open: false });
|
|
188
|
-
assert.ok(serverInst.url.length > 0);
|
|
189
|
-
assert.equal(serverInst.host, "127.0.0.1");
|
|
190
|
-
|
|
191
|
-
// Test createServer callback event firing
|
|
192
|
-
const req = new EventEmitter() as unknown as IncomingMessage;
|
|
193
|
-
req.method = "GET";
|
|
194
|
-
req.url = "/api/projects";
|
|
195
|
-
req.headers = { host: "localhost" };
|
|
196
|
-
|
|
197
|
-
let statusCode = 0;
|
|
198
|
-
let responseData = "";
|
|
199
|
-
const res = {
|
|
200
|
-
writeHead(code: number) {
|
|
201
|
-
statusCode = code;
|
|
202
|
-
},
|
|
203
|
-
end(data?: string) {
|
|
204
|
-
if (data) responseData += data;
|
|
205
|
-
},
|
|
206
|
-
} as unknown as ServerResponse;
|
|
207
|
-
|
|
208
|
-
serverInst.server.emit("request", req, res);
|
|
209
|
-
assert.equal(statusCode, 200);
|
|
210
|
-
assert.ok(responseData.includes("[]"));
|
|
211
|
-
|
|
212
|
-
// Test 500 error catch block in createServer handler for Error and non-Error
|
|
213
|
-
const brokenDbError = {
|
|
214
|
-
prepare() {
|
|
215
|
-
throw new Error("Simulated DB Failure");
|
|
216
|
-
},
|
|
217
|
-
} as unknown as ReturnType<typeof openLedger>;
|
|
218
|
-
|
|
219
|
-
const brokenDbString = {
|
|
220
|
-
prepare() {
|
|
221
|
-
throw "String Exception";
|
|
222
|
-
},
|
|
223
|
-
} as unknown as ReturnType<typeof openLedger>;
|
|
224
|
-
|
|
225
|
-
let errCode = 0;
|
|
226
|
-
let errData = "";
|
|
227
|
-
const errRes = {
|
|
228
|
-
writeHead(code: number) {
|
|
229
|
-
errCode = code;
|
|
230
|
-
},
|
|
231
|
-
end(data?: string) {
|
|
232
|
-
if (data) errData += data;
|
|
233
|
-
},
|
|
234
|
-
} as unknown as ServerResponse;
|
|
235
|
-
|
|
236
|
-
const brokenServerInst = await startServer({
|
|
237
|
-
port: 0,
|
|
238
|
-
open: false,
|
|
239
|
-
db: brokenDbError,
|
|
240
|
-
});
|
|
241
|
-
brokenServerInst.server.emit("request", req, errRes);
|
|
242
|
-
assert.equal(errCode, 500);
|
|
243
|
-
assert.ok(errData.includes("Simulated DB Failure"));
|
|
244
|
-
await brokenServerInst.close();
|
|
245
|
-
|
|
246
|
-
let stringErrCode = 0;
|
|
247
|
-
let stringErrData = "";
|
|
248
|
-
const stringErrRes = {
|
|
249
|
-
writeHead(code: number) {
|
|
250
|
-
stringErrCode = code;
|
|
251
|
-
},
|
|
252
|
-
end(data?: string) {
|
|
253
|
-
if (data) stringErrData += data;
|
|
254
|
-
},
|
|
255
|
-
} as unknown as ServerResponse;
|
|
256
|
-
|
|
257
|
-
const stringBrokenServerInst = await startServer({
|
|
258
|
-
port: 0,
|
|
259
|
-
open: false,
|
|
260
|
-
db: brokenDbString,
|
|
261
|
-
});
|
|
262
|
-
stringBrokenServerInst.server.emit("request", req, stringErrRes);
|
|
263
|
-
assert.equal(stringErrCode, 500);
|
|
264
|
-
assert.ok(stringErrData.includes("String Exception"));
|
|
265
|
-
await stringBrokenServerInst.close();
|
|
266
|
-
|
|
267
|
-
// Test address non-object null and string fallback in server listen callback
|
|
268
|
-
const mockServerInst = await startServer({ port: 0, open: false });
|
|
269
|
-
mockServerInst.server.address = () => null;
|
|
270
|
-
await mockServerInst.close();
|
|
271
|
-
|
|
272
|
-
const mockServerInst2 = await startServer({ port: 0, open: false });
|
|
273
|
-
mockServerInst2.server.address = () => "pipe-address";
|
|
274
|
-
await mockServerInst2.close();
|
|
275
|
-
|
|
276
|
-
// Test close error callback rejection (double close)
|
|
277
|
-
await serverInst.close();
|
|
278
|
-
await assert.rejects(async () => {
|
|
279
|
-
await serverInst.close();
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
if (previousDb) process.env.PMGR_DB_PATH = previousDb;
|
|
283
|
-
else delete process.env.PMGR_DB_PATH;
|
|
284
|
-
if (previousPort) process.env.PORT = previousPort;
|
|
285
|
-
else delete process.env.PORT;
|
|
286
|
-
|
|
287
|
-
rmSync(directory, { recursive: true, force: true });
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
test("openBrowser executes without throwing on all platforms", () => {
|
|
291
|
-
const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform");
|
|
292
|
-
try {
|
|
293
|
-
Object.defineProperty(process, "platform", { value: "darwin" });
|
|
294
|
-
openBrowser("http://localhost:3000");
|
|
295
|
-
|
|
296
|
-
Object.defineProperty(process, "platform", { value: "win32" });
|
|
297
|
-
openBrowser("http://localhost:3000");
|
|
298
|
-
|
|
299
|
-
Object.defineProperty(process, "platform", { value: "linux" });
|
|
300
|
-
openBrowser("http://localhost:3000");
|
|
301
|
-
} finally {
|
|
302
|
-
if (originalPlatform) {
|
|
303
|
-
Object.defineProperty(process, "platform", originalPlatform);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
test("CLI serve command supports port, host, no-open, and json output", async () => {
|
|
309
|
-
const directory = mkdtempSync(join(tmpdir(), "pmgr-cli-serve-"));
|
|
310
|
-
const dbFile = join(directory, "serve.sqlite");
|
|
311
|
-
const previousDb = process.env.PMGR_DB_PATH;
|
|
312
|
-
process.env.PMGR_DB_PATH = dbFile;
|
|
313
|
-
|
|
314
|
-
let outputText = "";
|
|
315
|
-
const write = (str: string) => {
|
|
316
|
-
outputText += str;
|
|
317
|
-
};
|
|
318
|
-
|
|
319
|
-
const instance = (await run(
|
|
320
|
-
["serve", "--port", "0", "--host", "127.0.0.1", "--no-open", "--json"],
|
|
321
|
-
write,
|
|
322
|
-
)) as Awaited<ReturnType<typeof startServer>>;
|
|
323
|
-
|
|
324
|
-
assert.ok(outputText.includes('"url"'));
|
|
325
|
-
assert.ok(instance && typeof instance.close === "function");
|
|
326
|
-
await instance.close();
|
|
327
|
-
|
|
328
|
-
outputText = "";
|
|
329
|
-
const instance2 = (await run(
|
|
330
|
-
["status", "serve", "--port", "0", "--open"],
|
|
331
|
-
write,
|
|
332
|
-
)) as Awaited<ReturnType<typeof startServer>>;
|
|
333
|
-
assert.ok(outputText.includes("Server running at"));
|
|
334
|
-
await instance2.close();
|
|
335
|
-
|
|
336
|
-
outputText = "";
|
|
337
|
-
process.env.PORT = "0";
|
|
338
|
-
const instance3 = (await run(["serve"], write)) as Awaited<
|
|
339
|
-
ReturnType<typeof startServer>
|
|
340
|
-
>;
|
|
341
|
-
assert.ok(outputText.includes("Server running at"));
|
|
342
|
-
await instance3.close();
|
|
343
|
-
delete process.env.PORT;
|
|
344
|
-
|
|
345
|
-
if (previousDb) process.env.PMGR_DB_PATH = previousDb;
|
|
346
|
-
else delete process.env.PMGR_DB_PATH;
|
|
347
|
-
rmSync(directory, { recursive: true, force: true });
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
test("getWebUIHTML returns HTML string", () => {
|
|
351
|
-
const html = getWebUIHTML();
|
|
352
|
-
assert.ok(typeof html === "string");
|
|
353
|
-
assert.ok(html.includes("<!DOCTYPE html>"));
|
|
354
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "NodeNext",
|
|
5
|
-
"moduleResolution": "NodeNext",
|
|
6
|
-
"outDir": "dist",
|
|
7
|
-
"rootDir": "src",
|
|
8
|
-
"strict": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"skipLibCheck": true,
|
|
11
|
-
"declaration": true
|
|
12
|
-
},
|
|
13
|
-
"include": ["src/**/*.ts"]
|
|
14
|
-
}
|