project-mgr 0.1.5 → 0.1.7
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 +12 -1
- package/README.md +53 -8
- package/README_CN.md +53 -8
- package/dist/cli.js +86 -6
- package/dist/db.d.ts +5 -1
- package/dist/db.js +103 -4
- package/dist/domain.d.ts +17 -0
- package/dist/domain.js +46 -5
- package/docs/architecture.md +3 -3
- package/package.json +1 -1
- package/skills/project-mgr/SKILL.md +13 -3
- package/src/cli.ts +106 -5
- package/src/db.ts +137 -6
- package/src/domain.ts +69 -5
- package/test/core.test.ts +240 -4
package/test/core.test.ts
CHANGED
|
@@ -13,9 +13,12 @@ import {
|
|
|
13
13
|
databasePath,
|
|
14
14
|
history,
|
|
15
15
|
importStatuses,
|
|
16
|
+
listProjects,
|
|
16
17
|
openLedger,
|
|
17
18
|
parseTime,
|
|
19
|
+
setProjectMetadata,
|
|
18
20
|
toPortableStatusEntry,
|
|
21
|
+
validateProjectMetadataInput,
|
|
19
22
|
validateStatusEntry,
|
|
20
23
|
ValidationError,
|
|
21
24
|
} from "../src/index.js";
|
|
@@ -55,6 +58,14 @@ test("domain normalizes valid values and rejects invalid input", () => {
|
|
|
55
58
|
externalRef: "ref",
|
|
56
59
|
},
|
|
57
60
|
);
|
|
61
|
+
assert.ok(
|
|
62
|
+
validateStatusEntry({
|
|
63
|
+
project: "default-time",
|
|
64
|
+
status: "active",
|
|
65
|
+
summary: "sum",
|
|
66
|
+
}).occurredAt,
|
|
67
|
+
);
|
|
68
|
+
|
|
58
69
|
for (const invalid of [
|
|
59
70
|
null,
|
|
60
71
|
[],
|
|
@@ -79,12 +90,59 @@ test("domain normalizes valid values and rejects invalid input", () => {
|
|
|
79
90
|
});
|
|
80
91
|
assert.equal("id" in portable, false);
|
|
81
92
|
assert.equal("recordedAt" in portable, false);
|
|
93
|
+
|
|
94
|
+
assert.deepEqual(
|
|
95
|
+
validateProjectMetadataInput({
|
|
96
|
+
project: " demo ",
|
|
97
|
+
displayName: " Demo Project ",
|
|
98
|
+
description: " Desc ",
|
|
99
|
+
notes: " Notes ",
|
|
100
|
+
}),
|
|
101
|
+
{
|
|
102
|
+
project: "demo",
|
|
103
|
+
displayName: "Demo Project",
|
|
104
|
+
description: "Desc",
|
|
105
|
+
notes: "Notes",
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
assert.deepEqual(
|
|
110
|
+
validateProjectMetadataInput({
|
|
111
|
+
project: "demo",
|
|
112
|
+
description: null,
|
|
113
|
+
notes: "",
|
|
114
|
+
}),
|
|
115
|
+
{
|
|
116
|
+
project: "demo",
|
|
117
|
+
description: null,
|
|
118
|
+
notes: null,
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
for (const invalid of [
|
|
123
|
+
null,
|
|
124
|
+
[],
|
|
125
|
+
{ unknownKey: true },
|
|
126
|
+
{ project: "bad space" },
|
|
127
|
+
{ project: "demo", description: 123 },
|
|
128
|
+
{ project: "demo", notes: "x".repeat(2_001) },
|
|
129
|
+
]) {
|
|
130
|
+
assert.throws(() => validateProjectMetadataInput(invalid), ValidationError);
|
|
131
|
+
}
|
|
82
132
|
});
|
|
83
133
|
|
|
84
134
|
test("SQLite ledger migrates, appends, is idempotent, and queries by timeline", () => {
|
|
85
135
|
const db = openLedger(":memory:");
|
|
86
|
-
const first = addStatus(
|
|
136
|
+
const first = addStatus(
|
|
137
|
+
db,
|
|
138
|
+
entry({
|
|
139
|
+
externalRef: "one",
|
|
140
|
+
progress: "done",
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
87
143
|
assert.equal(first.inserted, true);
|
|
144
|
+
assert.equal(first.entry.progress, "done");
|
|
145
|
+
|
|
88
146
|
assert.equal(
|
|
89
147
|
addStatus(db, entry({ summary: "ignored", externalRef: "one" })).inserted,
|
|
90
148
|
false,
|
|
@@ -137,8 +195,64 @@ test("SQLite ledger migrates, appends, is idempotent, and queries by timeline",
|
|
|
137
195
|
count: number;
|
|
138
196
|
}
|
|
139
197
|
).count,
|
|
140
|
-
|
|
198
|
+
2,
|
|
141
199
|
);
|
|
200
|
+
|
|
201
|
+
// Test setProjectMetadata & listProjects
|
|
202
|
+
const meta = setProjectMetadata(db, {
|
|
203
|
+
project: "project-mgr",
|
|
204
|
+
displayName: "Project Manager",
|
|
205
|
+
description: "Updated Description",
|
|
206
|
+
notes: "Updated Notes",
|
|
207
|
+
});
|
|
208
|
+
assert.equal(meta.displayName, "Project Manager");
|
|
209
|
+
assert.equal(meta.description, "Updated Description");
|
|
210
|
+
assert.equal(meta.notes, "Updated Notes");
|
|
211
|
+
|
|
212
|
+
// Create project with notes only (no description)
|
|
213
|
+
|
|
214
|
+
const notesOnlyMeta = setProjectMetadata(db, {
|
|
215
|
+
project: "notes-only-meta",
|
|
216
|
+
notes: "Only Note",
|
|
217
|
+
});
|
|
218
|
+
assert.equal(notesOnlyMeta.description, undefined);
|
|
219
|
+
assert.equal(notesOnlyMeta.notes, "Only Note");
|
|
220
|
+
|
|
221
|
+
// Create project without status entries
|
|
222
|
+
setProjectMetadata(db, {
|
|
223
|
+
project: "standalone",
|
|
224
|
+
description: "Standalone App",
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Create project with tag for search
|
|
228
|
+
addStatus(db, entry({ project: "tag-proj", tags: ["my-special-tag"] }));
|
|
229
|
+
const matchedTag = listProjects(db, { query: "my-special-tag" });
|
|
230
|
+
assert.equal(matchedTag.length, 1);
|
|
231
|
+
assert.equal(matchedTag[0].project, "tag-proj");
|
|
232
|
+
|
|
233
|
+
const allProjects = listProjects(db);
|
|
234
|
+
assert.equal(allProjects.length, 5);
|
|
235
|
+
|
|
236
|
+
const matchedNotes = listProjects(db, { query: "Updated Notes" });
|
|
237
|
+
assert.equal(matchedNotes.length, 1);
|
|
238
|
+
assert.equal(matchedNotes[0].project, "project-mgr");
|
|
239
|
+
|
|
240
|
+
const matchedStandalone = listProjects(db, { query: "Standalone App" });
|
|
241
|
+
assert.equal(matchedStandalone.length, 1);
|
|
242
|
+
assert.equal(matchedStandalone[0].project, "standalone");
|
|
243
|
+
|
|
244
|
+
const matchedNone = listProjects(db, { query: "nonexistent" });
|
|
245
|
+
assert.equal(matchedNone.length, 0);
|
|
246
|
+
|
|
247
|
+
// Test clearing description and notes with null or empty string
|
|
248
|
+
const clearedMeta = setProjectMetadata(db, {
|
|
249
|
+
project: "project-mgr",
|
|
250
|
+
description: null,
|
|
251
|
+
notes: "",
|
|
252
|
+
});
|
|
253
|
+
assert.equal(clearedMeta.description, undefined);
|
|
254
|
+
assert.equal(clearedMeta.notes, undefined);
|
|
255
|
+
|
|
142
256
|
db.close();
|
|
143
257
|
});
|
|
144
258
|
|
|
@@ -209,7 +323,7 @@ test("CLI runs through a package-style symlink", () => {
|
|
|
209
323
|
rmSync(directory, { recursive: true, force: true });
|
|
210
324
|
});
|
|
211
325
|
|
|
212
|
-
test("CLI supports add, import, reads, errors, and export formats", () => {
|
|
326
|
+
test("CLI supports add, import, reads, errors, project metadata, and export formats", () => {
|
|
213
327
|
const directory = mkdtempSync(join(tmpdir(), "pmgr-cli-"));
|
|
214
328
|
const file = join(directory, "ledger.sqlite");
|
|
215
329
|
const importFile = join(directory, "entries.jsonl");
|
|
@@ -253,6 +367,10 @@ test("CLI supports add, import, reads, errors, and export formats", () => {
|
|
|
253
367
|
"active",
|
|
254
368
|
"--summary",
|
|
255
369
|
"added",
|
|
370
|
+
"--description",
|
|
371
|
+
"CLI project description",
|
|
372
|
+
"--notes",
|
|
373
|
+
"CLI project notes",
|
|
256
374
|
"--progress",
|
|
257
375
|
"covered",
|
|
258
376
|
"--next",
|
|
@@ -263,10 +381,118 @@ test("CLI supports add, import, reads, errors, and export formats", () => {
|
|
|
263
381
|
"cli",
|
|
264
382
|
"--external-ref",
|
|
265
383
|
"cli-add",
|
|
384
|
+
"--at",
|
|
385
|
+
timestamp,
|
|
266
386
|
"--json",
|
|
267
387
|
]),
|
|
268
388
|
);
|
|
269
389
|
assert.equal(added.project, "cli");
|
|
390
|
+
|
|
391
|
+
// CLI set-project, projects, and list commands
|
|
392
|
+
assert.match(
|
|
393
|
+
execute([
|
|
394
|
+
"set-project",
|
|
395
|
+
"--project",
|
|
396
|
+
"custom-proj",
|
|
397
|
+
"--display-name",
|
|
398
|
+
"Custom Proj",
|
|
399
|
+
"--description",
|
|
400
|
+
"Custom description",
|
|
401
|
+
"--notes",
|
|
402
|
+
"Custom note",
|
|
403
|
+
"--json",
|
|
404
|
+
]),
|
|
405
|
+
/Custom Proj/,
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
// Test set-project updates existing project with partial fields
|
|
409
|
+
assert.match(
|
|
410
|
+
execute([
|
|
411
|
+
"set-project",
|
|
412
|
+
"--project",
|
|
413
|
+
"custom-proj",
|
|
414
|
+
"--display-name",
|
|
415
|
+
"Custom Proj Renamed",
|
|
416
|
+
]),
|
|
417
|
+
/Custom Proj Renamed/,
|
|
418
|
+
);
|
|
419
|
+
execute([
|
|
420
|
+
"set-project",
|
|
421
|
+
"--project",
|
|
422
|
+
"custom-proj",
|
|
423
|
+
"--description",
|
|
424
|
+
"Updated desc only",
|
|
425
|
+
]);
|
|
426
|
+
execute([
|
|
427
|
+
"set-project",
|
|
428
|
+
"--project",
|
|
429
|
+
"custom-proj",
|
|
430
|
+
"--notes",
|
|
431
|
+
"Updated note only",
|
|
432
|
+
]);
|
|
433
|
+
execute(["set-project", "--project", "custom-proj"]);
|
|
434
|
+
|
|
435
|
+
// Test list / projects fuzzy search across all branches and human formatting
|
|
436
|
+
assert.match(execute(["projects"]), /cli\tcli/);
|
|
437
|
+
assert.match(execute(["projects"]), /Description: CLI project description/);
|
|
438
|
+
assert.match(execute(["projects"]), /Notes: CLI project notes/);
|
|
439
|
+
assert.match(execute(["projects"]), /Latest Status: \(none\)/);
|
|
440
|
+
|
|
441
|
+
assert.match(
|
|
442
|
+
execute(["list", "--query", "Custom Proj Renamed"]),
|
|
443
|
+
/Custom Proj Renamed/,
|
|
444
|
+
);
|
|
445
|
+
assert.match(
|
|
446
|
+
execute(["list", "--query", "Updated desc only"]),
|
|
447
|
+
/Updated desc only/,
|
|
448
|
+
);
|
|
449
|
+
assert.match(
|
|
450
|
+
execute(["list", "--query", "Updated note only"]),
|
|
451
|
+
/Updated note only/,
|
|
452
|
+
);
|
|
453
|
+
assert.match(execute(["projects", "--query", "active"]), /cli/);
|
|
454
|
+
assert.match(execute(["projects", "--query", "added"]), /cli/);
|
|
455
|
+
assert.match(execute(["projects", "--query", "covered"]), /cli/);
|
|
456
|
+
assert.match(execute(["projects", "--query", "cli"]), /cli/);
|
|
457
|
+
assert.equal(
|
|
458
|
+
execute(["projects", "--query", "no-match"]),
|
|
459
|
+
"No projects found.\n",
|
|
460
|
+
);
|
|
461
|
+
assert.equal(execute(["projects", "--query", "no-match", "--json"]), "[]\n");
|
|
462
|
+
|
|
463
|
+
// Test clearing description and notes via CLI flags
|
|
464
|
+
assert.throws(
|
|
465
|
+
() =>
|
|
466
|
+
execute([
|
|
467
|
+
"set-project",
|
|
468
|
+
"--project",
|
|
469
|
+
"custom-proj",
|
|
470
|
+
"--description",
|
|
471
|
+
"desc",
|
|
472
|
+
"--clear-description",
|
|
473
|
+
]),
|
|
474
|
+
/cannot specify both --description and --clear-description/,
|
|
475
|
+
);
|
|
476
|
+
assert.throws(
|
|
477
|
+
() =>
|
|
478
|
+
execute([
|
|
479
|
+
"set-project",
|
|
480
|
+
"--project",
|
|
481
|
+
"custom-proj",
|
|
482
|
+
"--notes",
|
|
483
|
+
"note",
|
|
484
|
+
"--clear-notes",
|
|
485
|
+
]),
|
|
486
|
+
/cannot specify both --notes and --clear-notes/,
|
|
487
|
+
);
|
|
488
|
+
execute([
|
|
489
|
+
"set-project",
|
|
490
|
+
"--project",
|
|
491
|
+
"custom-proj",
|
|
492
|
+
"--clear-description",
|
|
493
|
+
"--clear-notes",
|
|
494
|
+
]);
|
|
495
|
+
|
|
270
496
|
assert.match(
|
|
271
497
|
execute([
|
|
272
498
|
"status",
|
|
@@ -277,6 +503,8 @@ test("CLI supports add, import, reads, errors, and export formats", () => {
|
|
|
277
503
|
"active",
|
|
278
504
|
"--summary",
|
|
279
505
|
"human output",
|
|
506
|
+
"--at",
|
|
507
|
+
timestamp,
|
|
280
508
|
]),
|
|
281
509
|
/human output/,
|
|
282
510
|
);
|
|
@@ -314,6 +542,13 @@ test("CLI supports add, import, reads, errors, and export formats", () => {
|
|
|
314
542
|
execute(["status", "history", "--project", "cli", "--json"]),
|
|
315
543
|
/added/,
|
|
316
544
|
);
|
|
545
|
+
assert.deepEqual(
|
|
546
|
+
JSON.parse(execute(["history", "--limit", "2", "--json"])).map(
|
|
547
|
+
(item: { project: string }) => item.project,
|
|
548
|
+
),
|
|
549
|
+
["array", "json"],
|
|
550
|
+
);
|
|
551
|
+
|
|
317
552
|
assert.equal(
|
|
318
553
|
JSON.parse(
|
|
319
554
|
execute([
|
|
@@ -366,8 +601,9 @@ test("CLI supports add, import, reads, errors, and export formats", () => {
|
|
|
366
601
|
() => execute(["status", "history", "--project", "cli", "--limit", "1.5"]),
|
|
367
602
|
ValidationError,
|
|
368
603
|
);
|
|
369
|
-
assert.throws(() => execute(["
|
|
604
|
+
assert.throws(() => execute(["set-project"]), ValidationError);
|
|
370
605
|
assert.throws(() => execute(["status", "add", "--project"]), ValidationError);
|
|
606
|
+
|
|
371
607
|
assert.throws(
|
|
372
608
|
() =>
|
|
373
609
|
execute(["status", "current", "--project", "cli", "--project", "again"]),
|