@openspecui/server 2.1.3 → 2.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/dist/index.mjs +64 -25
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -139,6 +139,19 @@ var DashboardOverviewService = class {
|
|
|
139
139
|
}
|
|
140
140
|
};
|
|
141
141
|
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region ../core/src/dashboard-display.ts
|
|
144
|
+
const DASHBOARD_RECENT_LIST_LIMIT = 10;
|
|
145
|
+
function compareDashboardItemsByUpdatedAt(left, right) {
|
|
146
|
+
return right.updatedAt - left.updatedAt || left.id.localeCompare(right.id);
|
|
147
|
+
}
|
|
148
|
+
function sortDashboardItemsByUpdatedAt(items) {
|
|
149
|
+
return [...items].sort(compareDashboardItemsByUpdatedAt);
|
|
150
|
+
}
|
|
151
|
+
function selectRecentDashboardItems(items, limit = DASHBOARD_RECENT_LIST_LIMIT) {
|
|
152
|
+
return sortDashboardItemsByUpdatedAt(items).slice(0, Math.max(0, Math.trunc(limit)));
|
|
153
|
+
}
|
|
154
|
+
|
|
142
155
|
//#endregion
|
|
143
156
|
//#region src/dashboard-git-snapshot.ts
|
|
144
157
|
const execFileAsync$1 = promisify(execFile);
|
|
@@ -165,6 +178,14 @@ async function defaultRunGit(cwd, args) {
|
|
|
165
178
|
};
|
|
166
179
|
}
|
|
167
180
|
}
|
|
181
|
+
async function defaultReadPathTimestampMs(absolutePath) {
|
|
182
|
+
try {
|
|
183
|
+
const stats = await stat(absolutePath);
|
|
184
|
+
return Number.isFinite(stats.mtimeMs) && stats.mtimeMs > 0 ? stats.mtimeMs : null;
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
168
189
|
function parseShortStat(output) {
|
|
169
190
|
const files = Number(/(\d+)\s+files? changed/.exec(output)?.[1] ?? 0);
|
|
170
191
|
const insertions = Number(/(\d+)\s+insertions?\(\+\)/.exec(output)?.[1] ?? 0);
|
|
@@ -274,17 +295,17 @@ async function resolveDefaultBranch(projectDir, runGit) {
|
|
|
274
295
|
return "main";
|
|
275
296
|
}
|
|
276
297
|
async function collectCommitEntries(options) {
|
|
277
|
-
const { worktreePath, defaultBranch, maxCommitEntries, runGit } = options;
|
|
278
|
-
const
|
|
298
|
+
const { worktreePath, defaultBranch, maxCommitEntries, runGit, readPathTimestampMs } = options;
|
|
299
|
+
const commitEntries = [];
|
|
279
300
|
const commits = await runGit(worktreePath, [
|
|
280
301
|
"log",
|
|
281
|
-
"--format=%H%x1f%s",
|
|
302
|
+
"--format=%H%x1f%ct%x1f%s",
|
|
282
303
|
`-n${maxCommitEntries}`,
|
|
283
304
|
`${defaultBranch}..HEAD`
|
|
284
305
|
]);
|
|
285
306
|
if (commits.ok) for (const line of commits.stdout.split("\n")) {
|
|
286
307
|
if (!line.trim()) continue;
|
|
287
|
-
const [hash, title = ""] = line.split("");
|
|
308
|
+
const [hash, committedAtRaw = "0", title = ""] = line.split("");
|
|
288
309
|
if (!hash) continue;
|
|
289
310
|
const diffResult = await runGit(worktreePath, [
|
|
290
311
|
"show",
|
|
@@ -298,10 +319,12 @@ async function collectCommitEntries(options) {
|
|
|
298
319
|
"--format=",
|
|
299
320
|
hash
|
|
300
321
|
])).stdout.split("\n").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
301
|
-
|
|
322
|
+
const committedAt = Number(committedAtRaw) * 1e3;
|
|
323
|
+
commitEntries.push({
|
|
302
324
|
type: "commit",
|
|
303
325
|
hash,
|
|
304
326
|
title: title.trim() || hash.slice(0, 7),
|
|
327
|
+
committedAt: Number.isFinite(committedAt) && committedAt > 0 ? committedAt : 0,
|
|
305
328
|
relatedChanges: parseRelatedChanges(changedFiles),
|
|
306
329
|
diff: diffResult.ok ? parseNumStat(diffResult.stdout) : EMPTY_DIFF
|
|
307
330
|
});
|
|
@@ -325,20 +348,28 @@ async function collectCommitEntries(options) {
|
|
|
325
348
|
const untrackedFiles = untrackedResult.stdout.split("\n").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
326
349
|
const allUncommittedFiles = new Set([...trackedFiles, ...untrackedFiles]);
|
|
327
350
|
const trackedDiff = trackedResult.ok ? parseNumStat(trackedResult.stdout) : EMPTY_DIFF;
|
|
328
|
-
|
|
351
|
+
const updatedAt = (await Promise.all([...allUncommittedFiles].map((path) => readPathTimestampMs(resolve(worktreePath, path))))).reduce((latest, current) => {
|
|
352
|
+
if (!current || !Number.isFinite(current) || current <= 0) return latest;
|
|
353
|
+
return latest === null || current > latest ? current : latest;
|
|
354
|
+
}, null) ?? null;
|
|
355
|
+
commitEntries.sort((left, right) => {
|
|
356
|
+
if (left.type !== "commit" || right.type !== "commit") return 0;
|
|
357
|
+
return right.committedAt - left.committedAt;
|
|
358
|
+
});
|
|
359
|
+
return [{
|
|
329
360
|
type: "uncommitted",
|
|
330
361
|
title: "Uncommitted",
|
|
362
|
+
updatedAt,
|
|
331
363
|
relatedChanges: parseRelatedChanges([...allUncommittedFiles]),
|
|
332
364
|
diff: {
|
|
333
365
|
files: allUncommittedFiles.size,
|
|
334
366
|
insertions: trackedDiff.insertions,
|
|
335
367
|
deletions: trackedDiff.deletions
|
|
336
368
|
}
|
|
337
|
-
}
|
|
338
|
-
return entries;
|
|
369
|
+
}, ...commitEntries];
|
|
339
370
|
}
|
|
340
371
|
async function collectWorktree(options) {
|
|
341
|
-
const { projectDir, worktree, defaultBranch, runGit, maxCommitEntries } = options;
|
|
372
|
+
const { projectDir, worktree, defaultBranch, runGit, maxCommitEntries, readPathTimestampMs } = options;
|
|
342
373
|
const worktreePath = resolve(worktree.path);
|
|
343
374
|
const resolvedProjectDir = resolve(projectDir);
|
|
344
375
|
const aheadBehindResult = await runGit(worktreePath, [
|
|
@@ -364,7 +395,8 @@ async function collectWorktree(options) {
|
|
|
364
395
|
worktreePath,
|
|
365
396
|
defaultBranch,
|
|
366
397
|
maxCommitEntries,
|
|
367
|
-
runGit
|
|
398
|
+
runGit,
|
|
399
|
+
readPathTimestampMs
|
|
368
400
|
});
|
|
369
401
|
return {
|
|
370
402
|
path: worktreePath,
|
|
@@ -402,6 +434,7 @@ async function removeDetachedDashboardGitWorktree(options) {
|
|
|
402
434
|
async function buildDashboardGitSnapshot(options) {
|
|
403
435
|
const runGit = options.runGit ?? defaultRunGit;
|
|
404
436
|
const maxCommitEntries = options.maxCommitEntries ?? 8;
|
|
437
|
+
const readPathTimestampMs = options.readPathTimestampMs ?? defaultReadPathTimestampMs;
|
|
405
438
|
const resolvedProjectDir = resolve(options.projectDir);
|
|
406
439
|
const defaultBranch = await resolveDefaultBranch(resolvedProjectDir, runGit);
|
|
407
440
|
const worktreeResult = await runGit(resolvedProjectDir, [
|
|
@@ -420,7 +453,8 @@ async function buildDashboardGitSnapshot(options) {
|
|
|
420
453
|
worktree,
|
|
421
454
|
defaultBranch,
|
|
422
455
|
runGit,
|
|
423
|
-
maxCommitEntries
|
|
456
|
+
maxCommitEntries,
|
|
457
|
+
readPathTimestampMs
|
|
424
458
|
})));
|
|
425
459
|
worktrees.sort((a, b) => {
|
|
426
460
|
if (a.isCurrent !== b.isCurrent) return a.isCurrent ? -1 : 1;
|
|
@@ -635,12 +669,13 @@ async function loadDashboardOverview(ctx, reason = "dashboard-refresh") {
|
|
|
635
669
|
ctx.adapter.listChangesWithMeta(),
|
|
636
670
|
ctx.adapter.listArchivedChangesWithMeta()
|
|
637
671
|
]);
|
|
638
|
-
const
|
|
672
|
+
const allActiveChanges = changeMetas.map((changeMeta) => ({
|
|
639
673
|
id: changeMeta.id,
|
|
640
674
|
name: changeMeta.name ?? changeMeta.id,
|
|
641
675
|
progress: changeMeta.progress,
|
|
642
676
|
updatedAt: changeMeta.updatedAt
|
|
643
|
-
}))
|
|
677
|
+
}));
|
|
678
|
+
const activeChanges = selectRecentDashboardItems(allActiveChanges);
|
|
644
679
|
const archivedChanges = (await Promise.all(archiveMetas.map(async (meta) => {
|
|
645
680
|
const change = await ctx.adapter.readArchivedChange(meta.id);
|
|
646
681
|
if (!change) return null;
|
|
@@ -651,7 +686,7 @@ async function loadDashboardOverview(ctx, reason = "dashboard-refresh") {
|
|
|
651
686
|
tasksCompleted: change.tasks.filter((task) => task.completed).length
|
|
652
687
|
};
|
|
653
688
|
}))).filter((item) => item !== null);
|
|
654
|
-
const
|
|
689
|
+
const allSpecifications = (await Promise.all(specMetas.map(async (meta) => {
|
|
655
690
|
const spec = await ctx.adapter.readSpec(meta.id);
|
|
656
691
|
if (!spec) return null;
|
|
657
692
|
return {
|
|
@@ -660,13 +695,14 @@ async function loadDashboardOverview(ctx, reason = "dashboard-refresh") {
|
|
|
660
695
|
requirements: spec.requirements.length,
|
|
661
696
|
updatedAt: meta.updatedAt
|
|
662
697
|
};
|
|
663
|
-
}))).filter((item) => item !== null)
|
|
664
|
-
const
|
|
665
|
-
const
|
|
666
|
-
const
|
|
698
|
+
}))).filter((item) => item !== null);
|
|
699
|
+
const specifications = selectRecentDashboardItems(allSpecifications);
|
|
700
|
+
const requirements = allSpecifications.reduce((sum, spec) => sum + spec.requirements, 0);
|
|
701
|
+
const tasksTotal = allActiveChanges.reduce((sum, change) => sum + change.progress.total, 0);
|
|
702
|
+
const tasksCompleted = allActiveChanges.reduce((sum, change) => sum + change.progress.completed, 0);
|
|
667
703
|
const archivedTasksCompleted = archivedChanges.reduce((sum, change) => sum + change.tasksCompleted, 0);
|
|
668
704
|
const taskCompletionPercent = tasksTotal > 0 ? Math.round(tasksCompleted / tasksTotal * 100) : null;
|
|
669
|
-
const inProgressChanges =
|
|
705
|
+
const inProgressChanges = allActiveChanges.filter((change) => change.progress.total > 0 && change.progress.completed < change.progress.total).length;
|
|
670
706
|
const specificationTrendEvents = specMetas.flatMap((spec) => {
|
|
671
707
|
const ts = resolveTrendTimestamp(spec.createdAt, spec.updatedAt);
|
|
672
708
|
return ts === null ? [] : [{
|
|
@@ -682,7 +718,7 @@ async function loadDashboardOverview(ctx, reason = "dashboard-refresh") {
|
|
|
682
718
|
}];
|
|
683
719
|
});
|
|
684
720
|
const specMetaById = new Map(specMetas.map((meta) => [meta.id, meta]));
|
|
685
|
-
const requirementTrendEvents =
|
|
721
|
+
const requirementTrendEvents = allSpecifications.flatMap((spec) => {
|
|
686
722
|
const meta = specMetaById.get(spec.id);
|
|
687
723
|
const ts = resolveTrendTimestamp(meta?.updatedAt, meta?.createdAt);
|
|
688
724
|
return ts === null ? [] : [{
|
|
@@ -690,7 +726,7 @@ async function loadDashboardOverview(ctx, reason = "dashboard-refresh") {
|
|
|
690
726
|
value: spec.requirements
|
|
691
727
|
}];
|
|
692
728
|
});
|
|
693
|
-
const hasObjectiveSpecificationTrend = specificationTrendEvents.length > 0 ||
|
|
729
|
+
const hasObjectiveSpecificationTrend = specificationTrendEvents.length > 0 || allSpecifications.length === 0;
|
|
694
730
|
const hasObjectiveRequirementTrend = requirementTrendEvents.length > 0 || requirements === 0;
|
|
695
731
|
const hasObjectiveCompletedTrend = completedTrendEvents.length > 0 || archiveMetas.length === 0;
|
|
696
732
|
const config = await ctx.configManager.readConfig();
|
|
@@ -764,9 +800,9 @@ async function loadDashboardOverview(ctx, reason = "dashboard-refresh") {
|
|
|
764
800
|
});
|
|
765
801
|
return {
|
|
766
802
|
summary: {
|
|
767
|
-
specifications:
|
|
803
|
+
specifications: allSpecifications.length,
|
|
768
804
|
requirements,
|
|
769
|
-
activeChanges:
|
|
805
|
+
activeChanges: allActiveChanges.length,
|
|
770
806
|
inProgressChanges,
|
|
771
807
|
completedChanges: archiveMetas.length,
|
|
772
808
|
archivedTasksCompleted,
|
|
@@ -1583,6 +1619,9 @@ const changeRouter = router({
|
|
|
1583
1619
|
if (!await ctx.adapter.toggleTask(input.changeId, input.taskIndex, input.completed)) throw new Error(`Failed to toggle task ${input.taskIndex} in change ${input.changeId}`);
|
|
1584
1620
|
return { success: true };
|
|
1585
1621
|
}),
|
|
1622
|
+
subscribe: publicProcedure.subscription(({ ctx }) => {
|
|
1623
|
+
return createReactiveSubscription(() => ctx.adapter.listChangesWithMeta());
|
|
1624
|
+
}),
|
|
1586
1625
|
subscribeFiles: publicProcedure.input(z.object({ id: z.string() })).subscription(({ ctx, input }) => {
|
|
1587
1626
|
return createReactiveSubscriptionWithInput((id) => ctx.adapter.readChangeFiles(id))(input.id);
|
|
1588
1627
|
})
|
|
@@ -2247,8 +2286,8 @@ const dashboardRouter = router({
|
|
|
2247
2286
|
}),
|
|
2248
2287
|
refreshGitSnapshot: publicProcedure.input(z.object({ reason: z.string().optional() }).optional()).mutation(async ({ ctx, input }) => {
|
|
2249
2288
|
const reason = input?.reason?.trim() || "manual-refresh";
|
|
2250
|
-
await touchDashboardGitRefreshStamp(ctx.projectDir, reason);
|
|
2251
2289
|
await ctx.dashboardOverviewService.refresh(reason);
|
|
2290
|
+
await touchDashboardGitRefreshStamp(ctx.projectDir, reason);
|
|
2252
2291
|
return { success: true };
|
|
2253
2292
|
}),
|
|
2254
2293
|
removeDetachedWorktree: publicProcedure.input(z.object({ path: z.string().min(1) })).mutation(async ({ ctx, input }) => {
|
|
@@ -2257,8 +2296,8 @@ const dashboardRouter = router({
|
|
|
2257
2296
|
projectDir: ctx.projectDir,
|
|
2258
2297
|
targetPath: input.path
|
|
2259
2298
|
});
|
|
2260
|
-
await touchDashboardGitRefreshStamp(ctx.projectDir, "remove-detached-worktree");
|
|
2261
2299
|
await ctx.dashboardOverviewService.refresh("remove-detached-worktree");
|
|
2300
|
+
await touchDashboardGitRefreshStamp(ctx.projectDir, "remove-detached-worktree");
|
|
2262
2301
|
return { success: true };
|
|
2263
2302
|
}),
|
|
2264
2303
|
gitTaskStatus: publicProcedure.query(async ({ ctx }) => {
|