@openspecui/server 2.1.5 → 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.
Files changed (2) hide show
  1. package/dist/index.mjs +37 -13
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -178,6 +178,14 @@ async function defaultRunGit(cwd, args) {
178
178
  };
179
179
  }
180
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
+ }
181
189
  function parseShortStat(output) {
182
190
  const files = Number(/(\d+)\s+files? changed/.exec(output)?.[1] ?? 0);
183
191
  const insertions = Number(/(\d+)\s+insertions?\(\+\)/.exec(output)?.[1] ?? 0);
@@ -287,17 +295,17 @@ async function resolveDefaultBranch(projectDir, runGit) {
287
295
  return "main";
288
296
  }
289
297
  async function collectCommitEntries(options) {
290
- const { worktreePath, defaultBranch, maxCommitEntries, runGit } = options;
291
- const entries = [];
298
+ const { worktreePath, defaultBranch, maxCommitEntries, runGit, readPathTimestampMs } = options;
299
+ const commitEntries = [];
292
300
  const commits = await runGit(worktreePath, [
293
301
  "log",
294
- "--format=%H%x1f%s",
302
+ "--format=%H%x1f%ct%x1f%s",
295
303
  `-n${maxCommitEntries}`,
296
304
  `${defaultBranch}..HEAD`
297
305
  ]);
298
306
  if (commits.ok) for (const line of commits.stdout.split("\n")) {
299
307
  if (!line.trim()) continue;
300
- const [hash, title = ""] = line.split("");
308
+ const [hash, committedAtRaw = "0", title = ""] = line.split("");
301
309
  if (!hash) continue;
302
310
  const diffResult = await runGit(worktreePath, [
303
311
  "show",
@@ -311,10 +319,12 @@ async function collectCommitEntries(options) {
311
319
  "--format=",
312
320
  hash
313
321
  ])).stdout.split("\n").map((item) => item.trim()).filter((item) => item.length > 0);
314
- entries.push({
322
+ const committedAt = Number(committedAtRaw) * 1e3;
323
+ commitEntries.push({
315
324
  type: "commit",
316
325
  hash,
317
326
  title: title.trim() || hash.slice(0, 7),
327
+ committedAt: Number.isFinite(committedAt) && committedAt > 0 ? committedAt : 0,
318
328
  relatedChanges: parseRelatedChanges(changedFiles),
319
329
  diff: diffResult.ok ? parseNumStat(diffResult.stdout) : EMPTY_DIFF
320
330
  });
@@ -338,20 +348,28 @@ async function collectCommitEntries(options) {
338
348
  const untrackedFiles = untrackedResult.stdout.split("\n").map((item) => item.trim()).filter((item) => item.length > 0);
339
349
  const allUncommittedFiles = new Set([...trackedFiles, ...untrackedFiles]);
340
350
  const trackedDiff = trackedResult.ok ? parseNumStat(trackedResult.stdout) : EMPTY_DIFF;
341
- entries.push({
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 [{
342
360
  type: "uncommitted",
343
361
  title: "Uncommitted",
362
+ updatedAt,
344
363
  relatedChanges: parseRelatedChanges([...allUncommittedFiles]),
345
364
  diff: {
346
365
  files: allUncommittedFiles.size,
347
366
  insertions: trackedDiff.insertions,
348
367
  deletions: trackedDiff.deletions
349
368
  }
350
- });
351
- return entries;
369
+ }, ...commitEntries];
352
370
  }
353
371
  async function collectWorktree(options) {
354
- const { projectDir, worktree, defaultBranch, runGit, maxCommitEntries } = options;
372
+ const { projectDir, worktree, defaultBranch, runGit, maxCommitEntries, readPathTimestampMs } = options;
355
373
  const worktreePath = resolve(worktree.path);
356
374
  const resolvedProjectDir = resolve(projectDir);
357
375
  const aheadBehindResult = await runGit(worktreePath, [
@@ -377,7 +395,8 @@ async function collectWorktree(options) {
377
395
  worktreePath,
378
396
  defaultBranch,
379
397
  maxCommitEntries,
380
- runGit
398
+ runGit,
399
+ readPathTimestampMs
381
400
  });
382
401
  return {
383
402
  path: worktreePath,
@@ -415,6 +434,7 @@ async function removeDetachedDashboardGitWorktree(options) {
415
434
  async function buildDashboardGitSnapshot(options) {
416
435
  const runGit = options.runGit ?? defaultRunGit;
417
436
  const maxCommitEntries = options.maxCommitEntries ?? 8;
437
+ const readPathTimestampMs = options.readPathTimestampMs ?? defaultReadPathTimestampMs;
418
438
  const resolvedProjectDir = resolve(options.projectDir);
419
439
  const defaultBranch = await resolveDefaultBranch(resolvedProjectDir, runGit);
420
440
  const worktreeResult = await runGit(resolvedProjectDir, [
@@ -433,7 +453,8 @@ async function buildDashboardGitSnapshot(options) {
433
453
  worktree,
434
454
  defaultBranch,
435
455
  runGit,
436
- maxCommitEntries
456
+ maxCommitEntries,
457
+ readPathTimestampMs
437
458
  })));
438
459
  worktrees.sort((a, b) => {
439
460
  if (a.isCurrent !== b.isCurrent) return a.isCurrent ? -1 : 1;
@@ -1598,6 +1619,9 @@ const changeRouter = router({
1598
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}`);
1599
1620
  return { success: true };
1600
1621
  }),
1622
+ subscribe: publicProcedure.subscription(({ ctx }) => {
1623
+ return createReactiveSubscription(() => ctx.adapter.listChangesWithMeta());
1624
+ }),
1601
1625
  subscribeFiles: publicProcedure.input(z.object({ id: z.string() })).subscription(({ ctx, input }) => {
1602
1626
  return createReactiveSubscriptionWithInput((id) => ctx.adapter.readChangeFiles(id))(input.id);
1603
1627
  })
@@ -2262,8 +2286,8 @@ const dashboardRouter = router({
2262
2286
  }),
2263
2287
  refreshGitSnapshot: publicProcedure.input(z.object({ reason: z.string().optional() }).optional()).mutation(async ({ ctx, input }) => {
2264
2288
  const reason = input?.reason?.trim() || "manual-refresh";
2265
- await touchDashboardGitRefreshStamp(ctx.projectDir, reason);
2266
2289
  await ctx.dashboardOverviewService.refresh(reason);
2290
+ await touchDashboardGitRefreshStamp(ctx.projectDir, reason);
2267
2291
  return { success: true };
2268
2292
  }),
2269
2293
  removeDetachedWorktree: publicProcedure.input(z.object({ path: z.string().min(1) })).mutation(async ({ ctx, input }) => {
@@ -2272,8 +2296,8 @@ const dashboardRouter = router({
2272
2296
  projectDir: ctx.projectDir,
2273
2297
  targetPath: input.path
2274
2298
  });
2275
- await touchDashboardGitRefreshStamp(ctx.projectDir, "remove-detached-worktree");
2276
2299
  await ctx.dashboardOverviewService.refresh("remove-detached-worktree");
2300
+ await touchDashboardGitRefreshStamp(ctx.projectDir, "remove-detached-worktree");
2277
2301
  return { success: true };
2278
2302
  }),
2279
2303
  gitTaskStatus: publicProcedure.query(async ({ ctx }) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openspecui/server",
3
- "version": "2.1.5",
3
+ "version": "2.1.7",
4
4
  "type": "module",
5
5
  "main": "dist/index.mjs",
6
6
  "exports": {