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.
- package/CONTRIBUTING.md +1 -1
- package/README.md +4 -3
- package/README.zh.md +4 -3
- package/lib/client.js.map +1 -1
- package/lib/index.js +303 -69
- package/lib/types/protocol/sources.d.ts +5 -0
- package/lib/types/reconcile.d.ts +24 -0
- package/lib/types/routes/route-state.d.ts +22 -2
- package/lib/types/skillfs/scan.d.ts +26 -0
- package/lib/types/skillfs.d.ts +9 -1
- package/lib/types/store/migrate.d.ts +1 -1
- package/lib/types/store/store.d.ts +10 -5
- package/package.json +1 -1
- package/src/index.ts +11 -1
- package/src/protocol/sources.ts +5 -0
- package/src/reconcile.test.ts +74 -1
- package/src/reconcile.ts +50 -1
- package/src/routes/catalog.ts +50 -22
- package/src/routes/repo-import.ts +16 -26
- package/src/routes/route-state.test.ts +4 -4
- package/src/routes/route-state.ts +45 -10
- package/src/routes/sources.ts +79 -23
- package/src/routes.test.ts +232 -0
- package/src/skillfs/scan.ts +91 -2
- package/src/skillfs.ts +14 -2
- package/src/store/migrate.ts +6 -4
- package/src/store/store.ts +21 -11
- package/src/store.test.ts +17 -1
package/src/store/store.ts
CHANGED
|
@@ -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
|
|
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.
|
|
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.
|
|
447
|
+
return [...this.trashByPath.values()].sort((a, b) => b.movedAt - a.movedAt)
|
|
448
448
|
}
|
|
449
449
|
|
|
450
|
-
/** One trash entry by skill name
|
|
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
|
-
|
|
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
|
-
/**
|
|
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.
|
|
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(
|
|
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.
|
|
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.
|
|
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)
|