@cvr/repo 1.0.0
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/README.md +94 -0
- package/package.json +51 -0
- package/src/commands/clean.ts +40 -0
- package/src/commands/fetch.ts +152 -0
- package/src/commands/info.ts +83 -0
- package/src/commands/list.ts +107 -0
- package/src/commands/open.ts +89 -0
- package/src/commands/path.ts +38 -0
- package/src/commands/prune.ts +112 -0
- package/src/commands/remove.ts +41 -0
- package/src/commands/search.ts +102 -0
- package/src/commands/stats.ts +100 -0
- package/src/main.ts +77 -0
- package/src/services/cache.ts +109 -0
- package/src/services/git.ts +226 -0
- package/src/services/metadata.ts +141 -0
- package/src/services/registry.ts +713 -0
- package/src/test-utils/index.ts +109 -0
- package/src/test-utils/layers/cache.ts +137 -0
- package/src/test-utils/layers/git.ts +104 -0
- package/src/test-utils/layers/index.ts +33 -0
- package/src/test-utils/layers/metadata.ts +155 -0
- package/src/test-utils/layers/registry.ts +201 -0
- package/src/test-utils/run-cli.ts +157 -0
- package/src/test-utils/sequence.ts +57 -0
- package/src/types.ts +101 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Layer, Ref } from "effect"
|
|
2
|
+
|
|
3
|
+
import type { CacheService } from "../services/cache.js"
|
|
4
|
+
import type { GitService } from "../services/git.js"
|
|
5
|
+
import type { MetadataService } from "../services/metadata.js"
|
|
6
|
+
import type { RegistryService } from "../services/registry.js"
|
|
7
|
+
import {
|
|
8
|
+
createMockCacheService,
|
|
9
|
+
createMockGitService,
|
|
10
|
+
createMockMetadataService,
|
|
11
|
+
createMockRegistryService,
|
|
12
|
+
type MockCacheState,
|
|
13
|
+
type MockGitState,
|
|
14
|
+
type MockMetadataState,
|
|
15
|
+
type MockRegistryState,
|
|
16
|
+
} from "./layers/index.js"
|
|
17
|
+
import { createSequenceRef, type RecordedCall } from "./sequence.js"
|
|
18
|
+
|
|
19
|
+
// ─── Re-exports ────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
export * from "./layers/index.js"
|
|
22
|
+
export * from "./sequence.js"
|
|
23
|
+
export * from "./run-cli.js"
|
|
24
|
+
|
|
25
|
+
// ─── Test Layer Types ──────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
/** All services provided by the test layer */
|
|
28
|
+
export type TestServices = GitService | CacheService | MetadataService | RegistryService
|
|
29
|
+
|
|
30
|
+
// ─── Test Layer Composition ────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
export interface CreateTestLayerOptions {
|
|
33
|
+
/** Initial git state */
|
|
34
|
+
git?: Partial<MockGitState>
|
|
35
|
+
/** Initial cache state */
|
|
36
|
+
cache?: Partial<MockCacheState>
|
|
37
|
+
/** Initial metadata state */
|
|
38
|
+
metadata?: Partial<MockMetadataState>
|
|
39
|
+
/** Initial registry state */
|
|
40
|
+
registry?: Partial<MockRegistryState>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface TestLayerResult {
|
|
44
|
+
/** Combined layer providing all test services */
|
|
45
|
+
layer: Layer.Layer<TestServices>
|
|
46
|
+
/** Git mock utilities for state inspection */
|
|
47
|
+
git: ReturnType<typeof createMockGitService>
|
|
48
|
+
/** Cache mock utilities for state inspection */
|
|
49
|
+
cache: ReturnType<typeof createMockCacheService>
|
|
50
|
+
/** Metadata mock utilities for state inspection */
|
|
51
|
+
metadata: ReturnType<typeof createMockMetadataService>
|
|
52
|
+
/** Registry mock utilities for state inspection */
|
|
53
|
+
registry: ReturnType<typeof createMockRegistryService>
|
|
54
|
+
/** Sequence ref for recording all service calls in order */
|
|
55
|
+
sequenceRef: Ref.Ref<RecordedCall[]>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Creates a complete test layer with all mock services.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* const { layer, git, cache, sequenceRef } = createTestLayer({
|
|
64
|
+
* git: { defaultBranch: 'main' },
|
|
65
|
+
* cache: { cacheDir: '/custom/cache' },
|
|
66
|
+
* });
|
|
67
|
+
*
|
|
68
|
+
* const result = yield* someCommand.pipe(Effect.provide(layer));
|
|
69
|
+
*
|
|
70
|
+
* // Verify git state
|
|
71
|
+
* const gitState = yield* git.getState();
|
|
72
|
+
* expect(gitState.clonedRepos.size).toBe(1);
|
|
73
|
+
*
|
|
74
|
+
* // Verify call sequence
|
|
75
|
+
* const calls = yield* Ref.get(sequenceRef);
|
|
76
|
+
* expect(calls[0].service).toBe('registry');
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export function createTestLayer(options: CreateTestLayerOptions = {}): TestLayerResult {
|
|
80
|
+
const sequenceRef = createSequenceRef()
|
|
81
|
+
|
|
82
|
+
const git = createMockGitService({
|
|
83
|
+
...(options.git && { initialState: options.git }),
|
|
84
|
+
sequenceRef,
|
|
85
|
+
})
|
|
86
|
+
const cache = createMockCacheService({
|
|
87
|
+
...(options.cache && { initialState: options.cache }),
|
|
88
|
+
sequenceRef,
|
|
89
|
+
})
|
|
90
|
+
const metadata = createMockMetadataService({
|
|
91
|
+
...(options.metadata && { initialState: options.metadata }),
|
|
92
|
+
sequenceRef,
|
|
93
|
+
})
|
|
94
|
+
const registry = createMockRegistryService({
|
|
95
|
+
...(options.registry && { initialState: options.registry }),
|
|
96
|
+
sequenceRef,
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const layer = Layer.mergeAll(git.layer, cache.layer, metadata.layer, registry.layer)
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
layer,
|
|
103
|
+
git,
|
|
104
|
+
cache,
|
|
105
|
+
metadata,
|
|
106
|
+
registry,
|
|
107
|
+
sequenceRef,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Effect, Layer, Option, Ref } from "effect"
|
|
2
|
+
import { CacheService } from "../../services/cache.js"
|
|
3
|
+
import type { PackageSpec } from "../../types.js"
|
|
4
|
+
import { recordCall, type SequenceRef } from "../sequence.js"
|
|
5
|
+
|
|
6
|
+
// ─── Mock State ───────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
export interface MockCacheState {
|
|
9
|
+
store: Map<string, { content: string; size: number }>
|
|
10
|
+
cacheDir: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const defaultMockCacheState: MockCacheState = {
|
|
14
|
+
store: new Map(),
|
|
15
|
+
cacheDir: "/tmp/test-repo-cache",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ─── Mock Implementation ──────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export interface CreateMockCacheServiceOptions {
|
|
21
|
+
initialState?: Partial<MockCacheState>
|
|
22
|
+
sequenceRef?: SequenceRef
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createMockCacheService(options: CreateMockCacheServiceOptions = {}): {
|
|
26
|
+
layer: Layer.Layer<CacheService>
|
|
27
|
+
stateRef: Ref.Ref<MockCacheState>
|
|
28
|
+
getState: () => Effect.Effect<MockCacheState>
|
|
29
|
+
/** Add a file to the mock cache (for test setup) */
|
|
30
|
+
addFile: (path: string, size: number) => Effect.Effect<void>
|
|
31
|
+
} {
|
|
32
|
+
const initialState = options.initialState ?? {}
|
|
33
|
+
const sequenceRef = options.sequenceRef
|
|
34
|
+
|
|
35
|
+
const state: MockCacheState = {
|
|
36
|
+
...defaultMockCacheState,
|
|
37
|
+
...initialState,
|
|
38
|
+
store: new Map(initialState.store ?? []),
|
|
39
|
+
}
|
|
40
|
+
const stateRef = Ref.unsafeMake(state)
|
|
41
|
+
|
|
42
|
+
const record = (method: string, args: unknown, result?: unknown): Effect.Effect<void> =>
|
|
43
|
+
sequenceRef ? recordCall(sequenceRef, { service: "cache", method, args, result }) : Effect.void
|
|
44
|
+
|
|
45
|
+
const getPath = (spec: PackageSpec) =>
|
|
46
|
+
Effect.gen(function* () {
|
|
47
|
+
const s = yield* Ref.get(stateRef)
|
|
48
|
+
const version = Option.getOrElse(spec.version, () => "default")
|
|
49
|
+
let result: string
|
|
50
|
+
switch (spec.registry) {
|
|
51
|
+
case "github":
|
|
52
|
+
result = `${s.cacheDir}/${spec.name}`
|
|
53
|
+
break
|
|
54
|
+
case "npm":
|
|
55
|
+
case "pypi":
|
|
56
|
+
case "crates":
|
|
57
|
+
result = `${s.cacheDir}/${spec.name}/${version}`
|
|
58
|
+
break
|
|
59
|
+
}
|
|
60
|
+
yield* record("getPath", { spec }, result)
|
|
61
|
+
return result
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const layer = Layer.succeed(
|
|
65
|
+
CacheService,
|
|
66
|
+
CacheService.of({
|
|
67
|
+
cacheDir: state.cacheDir,
|
|
68
|
+
|
|
69
|
+
getPath,
|
|
70
|
+
|
|
71
|
+
exists: (spec) =>
|
|
72
|
+
Effect.gen(function* () {
|
|
73
|
+
const path = yield* getPath(spec)
|
|
74
|
+
const s = yield* Ref.get(stateRef)
|
|
75
|
+
const result = s.store.has(path)
|
|
76
|
+
yield* record("exists", { spec }, result)
|
|
77
|
+
return result
|
|
78
|
+
}),
|
|
79
|
+
|
|
80
|
+
remove: (path) =>
|
|
81
|
+
Effect.gen(function* () {
|
|
82
|
+
yield* record("remove", { path })
|
|
83
|
+
yield* Ref.update(stateRef, (s) => {
|
|
84
|
+
const newStore = new Map(s.store)
|
|
85
|
+
for (const key of newStore.keys()) {
|
|
86
|
+
if (key.startsWith(path)) {
|
|
87
|
+
newStore.delete(key)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { ...s, store: newStore }
|
|
91
|
+
})
|
|
92
|
+
}),
|
|
93
|
+
|
|
94
|
+
removeAll: () =>
|
|
95
|
+
Effect.gen(function* () {
|
|
96
|
+
yield* record("removeAll", {})
|
|
97
|
+
yield* Ref.update(stateRef, (s) => ({ ...s, store: new Map() }))
|
|
98
|
+
}),
|
|
99
|
+
|
|
100
|
+
getSize: (path) =>
|
|
101
|
+
Effect.gen(function* () {
|
|
102
|
+
const s = yield* Ref.get(stateRef)
|
|
103
|
+
let total = 0
|
|
104
|
+
for (const [key, value] of s.store.entries()) {
|
|
105
|
+
if (key.startsWith(path)) {
|
|
106
|
+
total += value.size
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
yield* record("getSize", { path }, total)
|
|
110
|
+
return total
|
|
111
|
+
}),
|
|
112
|
+
|
|
113
|
+
ensureDir: (path) =>
|
|
114
|
+
Effect.gen(function* () {
|
|
115
|
+
yield* record("ensureDir", { path })
|
|
116
|
+
}),
|
|
117
|
+
})
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
const addFile = (path: string, size: number) =>
|
|
121
|
+
Ref.update(stateRef, (s) => {
|
|
122
|
+
const newStore = new Map(s.store)
|
|
123
|
+
newStore.set(path, { content: "", size })
|
|
124
|
+
return { ...s, store: newStore }
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
layer,
|
|
129
|
+
stateRef,
|
|
130
|
+
getState: () => Ref.get(stateRef),
|
|
131
|
+
addFile,
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─── Preset Configurations ────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
export const MockCacheServiceDefault = createMockCacheService()
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Effect, Layer, Ref } from "effect"
|
|
2
|
+
import { GitService } from "../../services/git.js"
|
|
3
|
+
import { recordCall, type SequenceRef } from "../sequence.js"
|
|
4
|
+
|
|
5
|
+
// ─── Mock State ───────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
export interface MockGitState {
|
|
8
|
+
clonedRepos: Map<string, { url: string; ref?: string; depth?: number }>
|
|
9
|
+
updatedPaths: string[]
|
|
10
|
+
defaultBranch: string
|
|
11
|
+
currentRef: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const defaultMockGitState: MockGitState = {
|
|
15
|
+
clonedRepos: new Map(),
|
|
16
|
+
updatedPaths: [],
|
|
17
|
+
defaultBranch: "main",
|
|
18
|
+
currentRef: "v1.0.0",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ─── Mock Implementation ──────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
export interface CreateMockGitServiceOptions {
|
|
24
|
+
initialState?: Partial<MockGitState>
|
|
25
|
+
sequenceRef?: SequenceRef
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createMockGitService(options: CreateMockGitServiceOptions = {}): {
|
|
29
|
+
layer: Layer.Layer<GitService>
|
|
30
|
+
stateRef: Ref.Ref<MockGitState>
|
|
31
|
+
getState: () => Effect.Effect<MockGitState>
|
|
32
|
+
} {
|
|
33
|
+
const initialState = options.initialState ?? {}
|
|
34
|
+
const sequenceRef = options.sequenceRef
|
|
35
|
+
|
|
36
|
+
const state: MockGitState = {
|
|
37
|
+
...defaultMockGitState,
|
|
38
|
+
...initialState,
|
|
39
|
+
clonedRepos: new Map(initialState.clonedRepos ?? []),
|
|
40
|
+
updatedPaths: [...(initialState.updatedPaths ?? [])],
|
|
41
|
+
}
|
|
42
|
+
const stateRef = Ref.unsafeMake(state)
|
|
43
|
+
|
|
44
|
+
const record = (method: string, args: unknown, result?: unknown): Effect.Effect<void> =>
|
|
45
|
+
sequenceRef ? recordCall(sequenceRef, { service: "git", method, args, result }) : Effect.void
|
|
46
|
+
|
|
47
|
+
const layer = Layer.succeed(
|
|
48
|
+
GitService,
|
|
49
|
+
GitService.of({
|
|
50
|
+
clone: (url, dest, options) =>
|
|
51
|
+
Effect.gen(function* () {
|
|
52
|
+
yield* record("clone", { url, dest, options })
|
|
53
|
+
const entry: { url: string; ref?: string; depth?: number } = { url }
|
|
54
|
+
if (options?.ref) entry.ref = options.ref
|
|
55
|
+
if (options?.depth) entry.depth = options.depth
|
|
56
|
+
yield* Ref.update(stateRef, (s) => ({
|
|
57
|
+
...s,
|
|
58
|
+
clonedRepos: new Map(s.clonedRepos).set(dest, entry),
|
|
59
|
+
}))
|
|
60
|
+
}),
|
|
61
|
+
|
|
62
|
+
update: (path) =>
|
|
63
|
+
Effect.gen(function* () {
|
|
64
|
+
yield* record("update", { path })
|
|
65
|
+
yield* Ref.update(stateRef, (s) => ({
|
|
66
|
+
...s,
|
|
67
|
+
updatedPaths: [...s.updatedPaths, path],
|
|
68
|
+
}))
|
|
69
|
+
}),
|
|
70
|
+
|
|
71
|
+
isGitRepo: (path) =>
|
|
72
|
+
Effect.gen(function* () {
|
|
73
|
+
const s = yield* Ref.get(stateRef)
|
|
74
|
+
const result = s.clonedRepos.has(path)
|
|
75
|
+
yield* record("isGitRepo", { path }, result)
|
|
76
|
+
return result
|
|
77
|
+
}),
|
|
78
|
+
|
|
79
|
+
getDefaultBranch: (url) =>
|
|
80
|
+
Effect.gen(function* () {
|
|
81
|
+
const s = yield* Ref.get(stateRef)
|
|
82
|
+
yield* record("getDefaultBranch", { url }, s.defaultBranch)
|
|
83
|
+
return s.defaultBranch
|
|
84
|
+
}),
|
|
85
|
+
|
|
86
|
+
getCurrentRef: (path) =>
|
|
87
|
+
Effect.gen(function* () {
|
|
88
|
+
const s = yield* Ref.get(stateRef)
|
|
89
|
+
yield* record("getCurrentRef", { path }, s.currentRef)
|
|
90
|
+
return s.currentRef
|
|
91
|
+
}),
|
|
92
|
+
})
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
layer,
|
|
97
|
+
stateRef,
|
|
98
|
+
getState: () => Ref.get(stateRef),
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ─── Preset Configurations ────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
export const MockGitServiceDefault = createMockGitService()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Re-export all mock service factories and types
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
createMockGitService,
|
|
5
|
+
defaultMockGitState,
|
|
6
|
+
MockGitServiceDefault,
|
|
7
|
+
type CreateMockGitServiceOptions,
|
|
8
|
+
type MockGitState,
|
|
9
|
+
} from "./git.js"
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
createMockCacheService,
|
|
13
|
+
defaultMockCacheState,
|
|
14
|
+
MockCacheServiceDefault,
|
|
15
|
+
type CreateMockCacheServiceOptions,
|
|
16
|
+
type MockCacheState,
|
|
17
|
+
} from "./cache.js"
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
createMockMetadataService,
|
|
21
|
+
defaultMockMetadataState,
|
|
22
|
+
MockMetadataServiceDefault,
|
|
23
|
+
type CreateMockMetadataServiceOptions,
|
|
24
|
+
type MockMetadataState,
|
|
25
|
+
} from "./metadata.js"
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
createMockRegistryService,
|
|
29
|
+
defaultMockRegistryState,
|
|
30
|
+
MockRegistryServiceDefault,
|
|
31
|
+
type CreateMockRegistryServiceOptions,
|
|
32
|
+
type MockRegistryState,
|
|
33
|
+
} from "./registry.js"
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { Effect, Layer, Option, Ref } from "effect"
|
|
2
|
+
import { MetadataService } from "../../services/metadata.js"
|
|
3
|
+
import type { MetadataIndex, PackageSpec } from "../../types.js"
|
|
4
|
+
import { recordCall, type SequenceRef } from "../sequence.js"
|
|
5
|
+
|
|
6
|
+
// ─── Mock State ───────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
export interface MockMetadataState {
|
|
9
|
+
index: MetadataIndex
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const defaultMockMetadataState: MockMetadataState = {
|
|
13
|
+
index: { version: 1, repos: [] },
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ─── Mock Implementation ──────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
export interface CreateMockMetadataServiceOptions {
|
|
19
|
+
initialState?: Partial<MockMetadataState>
|
|
20
|
+
sequenceRef?: SequenceRef
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createMockMetadataService(options: CreateMockMetadataServiceOptions = {}): {
|
|
24
|
+
layer: Layer.Layer<MetadataService>
|
|
25
|
+
stateRef: Ref.Ref<MockMetadataState>
|
|
26
|
+
getState: () => Effect.Effect<MockMetadataState>
|
|
27
|
+
} {
|
|
28
|
+
const initialState = options.initialState ?? {}
|
|
29
|
+
const sequenceRef = options.sequenceRef
|
|
30
|
+
|
|
31
|
+
const state: MockMetadataState = {
|
|
32
|
+
...defaultMockMetadataState,
|
|
33
|
+
...initialState,
|
|
34
|
+
index: {
|
|
35
|
+
version: initialState.index?.version ?? 1,
|
|
36
|
+
repos: [...(initialState.index?.repos ?? [])],
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
const stateRef = Ref.unsafeMake(state)
|
|
40
|
+
|
|
41
|
+
const record = (method: string, args: unknown, result?: unknown): Effect.Effect<void> =>
|
|
42
|
+
sequenceRef ? recordCall(sequenceRef, { service: "metadata", method, args, result }) : Effect.void
|
|
43
|
+
|
|
44
|
+
const specMatches = (a: PackageSpec, b: PackageSpec): boolean => {
|
|
45
|
+
if (a.registry !== b.registry || a.name !== b.name) return false
|
|
46
|
+
const aVersion = Option.getOrElse(a.version, () => "")
|
|
47
|
+
const bVersion = Option.getOrElse(b.version, () => "")
|
|
48
|
+
return aVersion === bVersion
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const layer = Layer.succeed(
|
|
52
|
+
MetadataService,
|
|
53
|
+
MetadataService.of({
|
|
54
|
+
load: () =>
|
|
55
|
+
Effect.gen(function* () {
|
|
56
|
+
const s = yield* Ref.get(stateRef)
|
|
57
|
+
yield* record("load", {}, s.index)
|
|
58
|
+
return s.index
|
|
59
|
+
}),
|
|
60
|
+
|
|
61
|
+
save: (newIndex) =>
|
|
62
|
+
Effect.gen(function* () {
|
|
63
|
+
yield* record("save", { index: newIndex })
|
|
64
|
+
yield* Ref.update(stateRef, (s) => ({ ...s, index: newIndex }))
|
|
65
|
+
}),
|
|
66
|
+
|
|
67
|
+
add: (metadata) =>
|
|
68
|
+
Effect.gen(function* () {
|
|
69
|
+
yield* record("add", { metadata })
|
|
70
|
+
yield* Ref.update(stateRef, (s) => {
|
|
71
|
+
const filtered = s.index.repos.filter((r) => !specMatches(r.spec, metadata.spec))
|
|
72
|
+
return {
|
|
73
|
+
...s,
|
|
74
|
+
index: { ...s.index, repos: [...filtered, metadata] },
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
}),
|
|
78
|
+
|
|
79
|
+
remove: (spec) =>
|
|
80
|
+
Effect.gen(function* () {
|
|
81
|
+
const s = yield* Ref.get(stateRef)
|
|
82
|
+
const originalLength = s.index.repos.length
|
|
83
|
+
const filtered = s.index.repos.filter((r) => !specMatches(r.spec, spec))
|
|
84
|
+
const result = filtered.length < originalLength
|
|
85
|
+
yield* record("remove", { spec }, result)
|
|
86
|
+
yield* Ref.set(stateRef, {
|
|
87
|
+
...s,
|
|
88
|
+
index: { ...s.index, repos: filtered },
|
|
89
|
+
})
|
|
90
|
+
return result
|
|
91
|
+
}),
|
|
92
|
+
|
|
93
|
+
find: (spec) =>
|
|
94
|
+
Effect.gen(function* () {
|
|
95
|
+
const s = yield* Ref.get(stateRef)
|
|
96
|
+
const result = s.index.repos.find((r) => specMatches(r.spec, spec)) ?? null
|
|
97
|
+
yield* record("find", { spec }, result)
|
|
98
|
+
return result
|
|
99
|
+
}),
|
|
100
|
+
|
|
101
|
+
updateAccessTime: (spec) =>
|
|
102
|
+
Effect.gen(function* () {
|
|
103
|
+
yield* record("updateAccessTime", { spec })
|
|
104
|
+
yield* Ref.update(stateRef, (s) => ({
|
|
105
|
+
...s,
|
|
106
|
+
index: {
|
|
107
|
+
...s.index,
|
|
108
|
+
repos: s.index.repos.map((r) => {
|
|
109
|
+
if (specMatches(r.spec, spec)) {
|
|
110
|
+
return { ...r, lastAccessedAt: new Date().toISOString() }
|
|
111
|
+
}
|
|
112
|
+
return r
|
|
113
|
+
}),
|
|
114
|
+
},
|
|
115
|
+
}))
|
|
116
|
+
}),
|
|
117
|
+
|
|
118
|
+
findOlderThan: (days) =>
|
|
119
|
+
Effect.gen(function* () {
|
|
120
|
+
const s = yield* Ref.get(stateRef)
|
|
121
|
+
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000
|
|
122
|
+
const result = s.index.repos.filter(
|
|
123
|
+
(r) => new Date(r.lastAccessedAt).getTime() < cutoff
|
|
124
|
+
)
|
|
125
|
+
yield* record("findOlderThan", { days }, result)
|
|
126
|
+
return result
|
|
127
|
+
}),
|
|
128
|
+
|
|
129
|
+
findLargerThan: (bytes) =>
|
|
130
|
+
Effect.gen(function* () {
|
|
131
|
+
const s = yield* Ref.get(stateRef)
|
|
132
|
+
const result = s.index.repos.filter((r) => r.sizeBytes > bytes)
|
|
133
|
+
yield* record("findLargerThan", { bytes }, result)
|
|
134
|
+
return result
|
|
135
|
+
}),
|
|
136
|
+
|
|
137
|
+
all: () =>
|
|
138
|
+
Effect.gen(function* () {
|
|
139
|
+
const s = yield* Ref.get(stateRef)
|
|
140
|
+
yield* record("all", {}, s.index.repos)
|
|
141
|
+
return s.index.repos
|
|
142
|
+
}),
|
|
143
|
+
})
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
layer,
|
|
148
|
+
stateRef,
|
|
149
|
+
getState: () => Ref.get(stateRef),
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ─── Preset Configurations ────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
export const MockMetadataServiceDefault = createMockMetadataService()
|