dsh-skill-hub 0.3.20 → 0.3.21

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.
@@ -13,7 +13,7 @@ export class SkillHubStore {
13
13
  private tagsById = new Map<string, SkillTag>()
14
14
  private sourcesByRepo = new Map<string, SourceRecord>()
15
15
  private marketSources: MarketSourceRecord[] = []
16
- private trashByName = new Map<string, TrashEntry>()
16
+ private trashByPath = new Map<string, TrashEntry>()
17
17
  private skillStats: SkillStatsCheckpoint | undefined = undefined
18
18
  private marketStats: MarketStatsSnapshot | undefined = undefined
19
19
  private collectionOrder: string[] = []
@@ -41,7 +41,7 @@ export class SkillHubStore {
41
41
  this.tagsById = state.tagsById
42
42
  this.sourcesByRepo = state.sourcesByRepo
43
43
  this.marketSources = state.marketSources
44
- this.trashByName = state.trashByName
44
+ this.trashByPath = state.trashByPath
45
45
  this.skillStats = state.skillStats
46
46
  this.marketStats = state.marketStats
47
47
  this.collectionOrder = state.collectionOrder
@@ -444,26 +444,36 @@ export class SkillHubStore {
444
444
  /** All trashed skills, newest first. */
445
445
  async listTrash(): Promise<TrashEntry[]> {
446
446
  await this.ensureLoaded()
447
- return [...this.trashByName.values()].sort((a, b) => b.movedAt - a.movedAt)
447
+ return [...this.trashByPath.values()].sort((a, b) => b.movedAt - a.movedAt)
448
448
  }
449
449
 
450
- /** One trash entry by skill name (undefined when absent). */
450
+ /** One trash entry by skill name — the newest when a name has several. */
451
451
  async getTrash(name: string): Promise<TrashEntry | undefined> {
452
452
  await this.ensureLoaded()
453
- return this.trashByName.get(name)
453
+ let newest: TrashEntry | undefined
454
+ for (const entry of this.trashByPath.values()) {
455
+ if (entry.name !== name) continue
456
+ if (newest === undefined || entry.movedAt > newest.movedAt) newest = entry
457
+ }
458
+ return newest
454
459
  }
455
460
 
456
- /** Record a trashed skill. */
461
+ /**
462
+ * Record a trashed skill, keyed by its trashed path. Keying by name used to
463
+ * silently drop the previous record when the same name landed in the trash
464
+ * twice (a re-delete, or the two on-disk discovery forms), orphaning its
465
+ * directory with nothing left pointing at it.
466
+ */
457
467
  async addTrash(entry: TrashEntry): Promise<void> {
458
468
  await this.ensureLoaded()
459
- this.trashByName.set(entry.name, entry)
469
+ this.trashByPath.set(entry.path, entry)
460
470
  await this.persist()
461
471
  }
462
472
 
463
- /** Remove a trash record (after restore). */
464
- async removeTrash(name: string): Promise<void> {
473
+ /** Remove a trash record by its trashed path (after restore or permanent clear). */
474
+ async removeTrash(path: string): Promise<void> {
465
475
  await this.ensureLoaded()
466
- if (!this.trashByName.delete(name)) return
476
+ if (!this.trashByPath.delete(path)) return
467
477
  await this.persist()
468
478
  }
469
479
 
@@ -521,7 +531,7 @@ export class SkillHubStore {
521
531
  ...(this.tagsById.size > 0 ? { tags: [...this.tagsById.values()] } : {}),
522
532
  ...(this.sourcesByRepo.size > 0 ? { sources: [...this.sourcesByRepo.values()] } : {}),
523
533
  ...(this.marketSources.length > 0 ? { marketSources: [...this.marketSources] } : {}),
524
- ...(this.trashByName.size > 0 ? { trash: [...this.trashByName.values()] } : {}),
534
+ ...(this.trashByPath.size > 0 ? { trash: [...this.trashByPath.values()] } : {}),
525
535
  ...(this.skillStats !== undefined ? { skillStats: this.skillStats } : {}),
526
536
  ...(this.marketStats !== undefined ? { marketStats: this.marketStats } : {}),
527
537
  ...(this.collectionOrder.length > 0 ? { collectionOrder: [...this.collectionOrder] } : {}),
package/src/store.test.ts CHANGED
@@ -206,12 +206,28 @@ describe('SkillHubStore', () => {
206
206
  await store.addTrash({ name: 'older', path: '/tmp/.trash/older-1', movedAt: 10 })
207
207
  const trash = await store.listTrash()
208
208
  expect(trash.map((entry) => entry.name)).toEqual(['gone-skill', 'older']) // newest first
209
- await store.removeTrash('gone-skill')
209
+ await store.removeTrash('/tmp/.trash/gone-skill-123')
210
210
  const reloaded = new SkillHubStore(file)
211
211
  expect(await reloaded.listTrash()).toHaveLength(1)
212
212
  expect(await reloaded.getTrash('older')).toMatchObject({ name: 'older' })
213
213
  })
214
214
 
215
+ it('keeps several trash entries sharing one skill name (no silent overwrite)', async () => {
216
+ // 同一个名字先后入站两次(重复删除,或磁盘上两种发现形态各进一次):
217
+ // 按名字索引会让后一条覆盖前一条,前一份目录就成了谁都指不到的孤儿。
218
+ await store.addTrash({ name: 'dup', path: '/tmp/.trash/dup-1', movedAt: 1, sourcePath: '/tmp/skills/dup' })
219
+ await store.addTrash({ name: 'dup', path: '/tmp/.trash/dup.md.disabled-2', movedAt: 2, sourcePath: '/tmp/skills/dup.md.disabled' })
220
+ const reloaded = new SkillHubStore(file)
221
+ expect((await reloaded.listTrash()).map((entry) => entry.path).sort()).toEqual([
222
+ '/tmp/.trash/dup-1',
223
+ '/tmp/.trash/dup.md.disabled-2',
224
+ ])
225
+ // getTrash 按名字取最新的一条(恢复入口用)。
226
+ expect(await reloaded.getTrash('dup')).toMatchObject({ path: '/tmp/.trash/dup.md.disabled-2' })
227
+ await reloaded.removeTrash('/tmp/.trash/dup.md.disabled-2')
228
+ expect(await reloaded.getTrash('dup')).toMatchObject({ path: '/tmp/.trash/dup-1' })
229
+ })
230
+
215
231
  it('persists the trash source path', async () => {
216
232
  await store.addTrash({ name: 'flat-gone', path: '/tmp/.trash/flat-gone.md-1', movedAt: 7, sourcePath: '/tmp/skills/flat-gone.md' })
217
233
  const reloaded = new SkillHubStore(file)