@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,226 @@
|
|
|
1
|
+
import { Context, Effect, Layer } from "effect"
|
|
2
|
+
import { GitError } from "../types.js"
|
|
3
|
+
|
|
4
|
+
// Service interface
|
|
5
|
+
export class GitService extends Context.Tag("@repo/GitService")<
|
|
6
|
+
GitService,
|
|
7
|
+
{
|
|
8
|
+
readonly clone: (
|
|
9
|
+
url: string,
|
|
10
|
+
dest: string,
|
|
11
|
+
options?: { depth?: number; ref?: string }
|
|
12
|
+
) => Effect.Effect<void, GitError>
|
|
13
|
+
readonly update: (path: string) => Effect.Effect<void, GitError>
|
|
14
|
+
readonly isGitRepo: (path: string) => Effect.Effect<boolean>
|
|
15
|
+
readonly getDefaultBranch: (url: string) => Effect.Effect<string, GitError>
|
|
16
|
+
readonly getCurrentRef: (path: string) => Effect.Effect<string, GitError>
|
|
17
|
+
}
|
|
18
|
+
>() {
|
|
19
|
+
// Live layer using real git
|
|
20
|
+
static readonly layer = Layer.succeed(
|
|
21
|
+
GitService,
|
|
22
|
+
GitService.of({
|
|
23
|
+
clone: (url, dest, options) =>
|
|
24
|
+
Effect.gen(function* () {
|
|
25
|
+
const args = ["clone"]
|
|
26
|
+
|
|
27
|
+
if (options?.depth) {
|
|
28
|
+
args.push("--depth", String(options.depth))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (options?.ref) {
|
|
32
|
+
args.push("--branch", options.ref)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
args.push(url, dest)
|
|
36
|
+
|
|
37
|
+
const proc = Bun.spawn(["git", ...args], {
|
|
38
|
+
stdout: "pipe",
|
|
39
|
+
stderr: "pipe",
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const exitCode = yield* Effect.tryPromise({
|
|
43
|
+
try: () => proc.exited,
|
|
44
|
+
catch: (cause) =>
|
|
45
|
+
new GitError({ operation: "clone", repo: url, cause }),
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
if (exitCode !== 0) {
|
|
49
|
+
// If ref failed, try without it (fallback to default branch)
|
|
50
|
+
if (options?.ref) {
|
|
51
|
+
const fallbackArgs = ["clone"]
|
|
52
|
+
if (options.depth) {
|
|
53
|
+
fallbackArgs.push("--depth", String(options.depth))
|
|
54
|
+
}
|
|
55
|
+
fallbackArgs.push(url, dest)
|
|
56
|
+
|
|
57
|
+
const fallbackProc = Bun.spawn(["git", ...fallbackArgs], {
|
|
58
|
+
stdout: "pipe",
|
|
59
|
+
stderr: "pipe",
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const fallbackResult = yield* Effect.tryPromise({
|
|
63
|
+
try: () => fallbackProc.exited,
|
|
64
|
+
catch: (cause) =>
|
|
65
|
+
new GitError({
|
|
66
|
+
operation: "clone-fallback",
|
|
67
|
+
repo: url,
|
|
68
|
+
cause,
|
|
69
|
+
}),
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
if (fallbackResult !== 0) {
|
|
73
|
+
return yield* Effect.fail(
|
|
74
|
+
new GitError({
|
|
75
|
+
operation: "clone",
|
|
76
|
+
repo: url,
|
|
77
|
+
cause: new Error(
|
|
78
|
+
`git clone failed with exit code ${fallbackResult}`
|
|
79
|
+
),
|
|
80
|
+
})
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
return yield* Effect.fail(
|
|
85
|
+
new GitError({
|
|
86
|
+
operation: "clone",
|
|
87
|
+
repo: url,
|
|
88
|
+
cause: new Error(`git clone failed with exit code ${exitCode}`),
|
|
89
|
+
})
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}),
|
|
94
|
+
|
|
95
|
+
update: (path) =>
|
|
96
|
+
Effect.gen(function* () {
|
|
97
|
+
// Fetch all updates
|
|
98
|
+
const fetchProc = Bun.spawn(["git", "-C", path, "fetch", "--all", "--prune"], {
|
|
99
|
+
stdout: "pipe",
|
|
100
|
+
stderr: "pipe",
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const fetchExit = yield* Effect.tryPromise({
|
|
104
|
+
try: () => fetchProc.exited,
|
|
105
|
+
catch: (cause) =>
|
|
106
|
+
new GitError({ operation: "fetch", repo: path, cause }),
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
if (fetchExit !== 0) {
|
|
110
|
+
return yield* Effect.fail(
|
|
111
|
+
new GitError({
|
|
112
|
+
operation: "fetch",
|
|
113
|
+
repo: path,
|
|
114
|
+
cause: new Error(`git fetch failed with exit code ${fetchExit}`),
|
|
115
|
+
})
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Reset to origin/HEAD (or current branch's upstream)
|
|
120
|
+
const resetProc = Bun.spawn(
|
|
121
|
+
["git", "-C", path, "reset", "--hard", "origin/HEAD"],
|
|
122
|
+
{
|
|
123
|
+
stdout: "pipe",
|
|
124
|
+
stderr: "pipe",
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
const resetExit = yield* Effect.tryPromise({
|
|
129
|
+
try: () => resetProc.exited,
|
|
130
|
+
catch: (cause) =>
|
|
131
|
+
new GitError({ operation: "reset", repo: path, cause }),
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
if (resetExit !== 0) {
|
|
135
|
+
// Try resetting to @{upstream} instead
|
|
136
|
+
const upstreamProc = Bun.spawn(
|
|
137
|
+
["git", "-C", path, "reset", "--hard", "@{upstream}"],
|
|
138
|
+
{
|
|
139
|
+
stdout: "pipe",
|
|
140
|
+
stderr: "pipe",
|
|
141
|
+
}
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
const upstreamExit = yield* Effect.tryPromise({
|
|
145
|
+
try: () => upstreamProc.exited,
|
|
146
|
+
catch: (cause) =>
|
|
147
|
+
new GitError({ operation: "reset-upstream", repo: path, cause }),
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
if (upstreamExit !== 0) {
|
|
151
|
+
return yield* Effect.fail(
|
|
152
|
+
new GitError({
|
|
153
|
+
operation: "reset",
|
|
154
|
+
repo: path,
|
|
155
|
+
cause: new Error(`git reset failed with exit code ${upstreamExit}`),
|
|
156
|
+
})
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}),
|
|
161
|
+
|
|
162
|
+
isGitRepo: (path) =>
|
|
163
|
+
Effect.gen(function* () {
|
|
164
|
+
const proc = Bun.spawn(["git", "-C", path, "rev-parse", "--git-dir"], {
|
|
165
|
+
stdout: "pipe",
|
|
166
|
+
stderr: "pipe",
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const exitCode = yield* Effect.promise(() => proc.exited).pipe(
|
|
170
|
+
Effect.orElseSucceed(() => 1)
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
return exitCode === 0
|
|
174
|
+
}),
|
|
175
|
+
|
|
176
|
+
getDefaultBranch: (url) =>
|
|
177
|
+
Effect.gen(function* () {
|
|
178
|
+
const proc = Bun.spawn(["git", "ls-remote", "--symref", url, "HEAD"], {
|
|
179
|
+
stdout: "pipe",
|
|
180
|
+
stderr: "pipe",
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
const output = yield* Effect.tryPromise({
|
|
184
|
+
try: async () => {
|
|
185
|
+
const stdout = await new Response(proc.stdout).text()
|
|
186
|
+
await proc.exited
|
|
187
|
+
return stdout
|
|
188
|
+
},
|
|
189
|
+
catch: (cause) =>
|
|
190
|
+
new GitError({ operation: "getDefaultBranch", repo: url, cause }),
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
// Parse output like: ref: refs/heads/main\tHEAD
|
|
194
|
+
const match = output.match(/ref: refs\/heads\/(\S+)/)
|
|
195
|
+
if (match?.[1]) {
|
|
196
|
+
return match[1]
|
|
197
|
+
}
|
|
198
|
+
return "main" // fallback
|
|
199
|
+
}),
|
|
200
|
+
|
|
201
|
+
getCurrentRef: (path) =>
|
|
202
|
+
Effect.gen(function* () {
|
|
203
|
+
const proc = Bun.spawn(
|
|
204
|
+
["git", "-C", path, "describe", "--tags", "--always"],
|
|
205
|
+
{
|
|
206
|
+
stdout: "pipe",
|
|
207
|
+
stderr: "pipe",
|
|
208
|
+
}
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
const output = yield* Effect.tryPromise({
|
|
212
|
+
try: async () => {
|
|
213
|
+
const stdout = await new Response(proc.stdout).text()
|
|
214
|
+
await proc.exited
|
|
215
|
+
return stdout.trim()
|
|
216
|
+
},
|
|
217
|
+
catch: (cause) =>
|
|
218
|
+
new GitError({ operation: "getCurrentRef", repo: path, cause }),
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
return output || "unknown"
|
|
222
|
+
}),
|
|
223
|
+
})
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { FileSystem, Path } from "@effect/platform"
|
|
2
|
+
import { Context, Effect, Layer, Option, Schema } from "effect"
|
|
3
|
+
import type { PackageSpec, RepoMetadata } from "../types.js"
|
|
4
|
+
import { MetadataIndex } from "../types.js"
|
|
5
|
+
|
|
6
|
+
// Create encode/decode functions for proper JSON serialization
|
|
7
|
+
const encodeMetadata = Schema.encodeSync(MetadataIndex)
|
|
8
|
+
const decodeMetadata = Schema.decodeUnknownSync(MetadataIndex)
|
|
9
|
+
|
|
10
|
+
// Service interface
|
|
11
|
+
export class MetadataService extends Context.Tag("@repo/MetadataService")<
|
|
12
|
+
MetadataService,
|
|
13
|
+
{
|
|
14
|
+
readonly load: () => Effect.Effect<MetadataIndex>
|
|
15
|
+
readonly save: (index: MetadataIndex) => Effect.Effect<void>
|
|
16
|
+
readonly add: (metadata: RepoMetadata) => Effect.Effect<void>
|
|
17
|
+
readonly remove: (spec: PackageSpec) => Effect.Effect<boolean>
|
|
18
|
+
readonly find: (spec: PackageSpec) => Effect.Effect<RepoMetadata | null>
|
|
19
|
+
readonly updateAccessTime: (spec: PackageSpec) => Effect.Effect<void>
|
|
20
|
+
readonly findOlderThan: (days: number) => Effect.Effect<readonly RepoMetadata[]>
|
|
21
|
+
readonly findLargerThan: (bytes: number) => Effect.Effect<readonly RepoMetadata[]>
|
|
22
|
+
readonly all: () => Effect.Effect<readonly RepoMetadata[]>
|
|
23
|
+
}
|
|
24
|
+
>() {
|
|
25
|
+
// Live layer using real filesystem
|
|
26
|
+
static readonly layer = Layer.effect(
|
|
27
|
+
MetadataService,
|
|
28
|
+
Effect.gen(function* () {
|
|
29
|
+
const fs = yield* FileSystem.FileSystem
|
|
30
|
+
const pathService = yield* Path.Path
|
|
31
|
+
const home = process.env.HOME ?? "~"
|
|
32
|
+
const cacheDir = pathService.join(home, ".cache", "repo")
|
|
33
|
+
const metadataPath = pathService.join(cacheDir, "metadata.json")
|
|
34
|
+
|
|
35
|
+
const specMatches = (a: PackageSpec, b: PackageSpec): boolean => {
|
|
36
|
+
if (a.registry !== b.registry || a.name !== b.name) return false
|
|
37
|
+
const aVersion = Option.getOrElse(a.version, () => "")
|
|
38
|
+
const bVersion = Option.getOrElse(b.version, () => "")
|
|
39
|
+
return aVersion === bVersion
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const load = (): Effect.Effect<MetadataIndex> =>
|
|
43
|
+
Effect.gen(function* () {
|
|
44
|
+
const exists = yield* fs.exists(metadataPath)
|
|
45
|
+
if (!exists) {
|
|
46
|
+
return { version: 1, repos: [] }
|
|
47
|
+
}
|
|
48
|
+
const content = yield* fs.readFileString(metadataPath)
|
|
49
|
+
return decodeMetadata(JSON.parse(content))
|
|
50
|
+
}).pipe(Effect.orElse(() => Effect.succeed({ version: 1, repos: [] })))
|
|
51
|
+
|
|
52
|
+
const save = (index: MetadataIndex): Effect.Effect<void> =>
|
|
53
|
+
Effect.gen(function* () {
|
|
54
|
+
yield* fs.makeDirectory(cacheDir, { recursive: true })
|
|
55
|
+
const encoded = encodeMetadata(index)
|
|
56
|
+
yield* fs.writeFileString(metadataPath, JSON.stringify(encoded, null, 2))
|
|
57
|
+
}).pipe(Effect.ignore)
|
|
58
|
+
|
|
59
|
+
const add = (metadata: RepoMetadata): Effect.Effect<void> =>
|
|
60
|
+
Effect.gen(function* () {
|
|
61
|
+
const index = yield* load()
|
|
62
|
+
const filtered = index.repos.filter(
|
|
63
|
+
(r) => !specMatches(r.spec, metadata.spec)
|
|
64
|
+
)
|
|
65
|
+
const newIndex: MetadataIndex = {
|
|
66
|
+
...index,
|
|
67
|
+
repos: [...filtered, metadata],
|
|
68
|
+
}
|
|
69
|
+
yield* save(newIndex)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const remove = (spec: PackageSpec): Effect.Effect<boolean> =>
|
|
73
|
+
Effect.gen(function* () {
|
|
74
|
+
const index = yield* load()
|
|
75
|
+
const originalLength = index.repos.length
|
|
76
|
+
const filtered = index.repos.filter((r) => !specMatches(r.spec, spec))
|
|
77
|
+
if (filtered.length === originalLength) {
|
|
78
|
+
return false
|
|
79
|
+
}
|
|
80
|
+
yield* save({ ...index, repos: filtered })
|
|
81
|
+
return true
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const find = (spec: PackageSpec): Effect.Effect<RepoMetadata | null> =>
|
|
85
|
+
Effect.gen(function* () {
|
|
86
|
+
const index = yield* load()
|
|
87
|
+
return index.repos.find((r) => specMatches(r.spec, spec)) ?? null
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const updateAccessTime = (spec: PackageSpec): Effect.Effect<void> =>
|
|
91
|
+
Effect.gen(function* () {
|
|
92
|
+
const index = yield* load()
|
|
93
|
+
const updated = index.repos.map((r) => {
|
|
94
|
+
if (specMatches(r.spec, spec)) {
|
|
95
|
+
return { ...r, lastAccessedAt: new Date().toISOString() }
|
|
96
|
+
}
|
|
97
|
+
return r
|
|
98
|
+
})
|
|
99
|
+
yield* save({ ...index, repos: updated })
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const findOlderThan = (
|
|
103
|
+
days: number
|
|
104
|
+
): Effect.Effect<readonly RepoMetadata[]> =>
|
|
105
|
+
Effect.gen(function* () {
|
|
106
|
+
const index = yield* load()
|
|
107
|
+
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000
|
|
108
|
+
return index.repos.filter(
|
|
109
|
+
(r) => new Date(r.lastAccessedAt).getTime() < cutoff
|
|
110
|
+
)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const findLargerThan = (
|
|
114
|
+
bytes: number
|
|
115
|
+
): Effect.Effect<readonly RepoMetadata[]> =>
|
|
116
|
+
Effect.gen(function* () {
|
|
117
|
+
const index = yield* load()
|
|
118
|
+
return index.repos.filter((r) => r.sizeBytes > bytes)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
const all = (): Effect.Effect<readonly RepoMetadata[]> =>
|
|
122
|
+
Effect.gen(function* () {
|
|
123
|
+
const index = yield* load()
|
|
124
|
+
return index.repos
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
return MetadataService.of({
|
|
128
|
+
load,
|
|
129
|
+
save,
|
|
130
|
+
add,
|
|
131
|
+
remove,
|
|
132
|
+
find,
|
|
133
|
+
updateAccessTime,
|
|
134
|
+
findOlderThan,
|
|
135
|
+
findLargerThan,
|
|
136
|
+
all,
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
}
|