@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 ADDED
@@ -0,0 +1,94 @@
1
+ # repo
2
+
3
+ Multi-registry source code cache manager. Fetch and cache source code from GitHub, npm, PyPI, and Crates.io.
4
+
5
+ Inspired by [vercel-labs/opensrc](https://github.com/vercel-labs/opensrc).
6
+
7
+ ## Features
8
+
9
+ - **Source-first**: Package registries (npm, PyPI, Crates) resolve to source git repos when possible, with tarball fallback
10
+ - **Multi-registry**: GitHub, npm, PyPI, Crates.io support
11
+ - **Shallow clones**: Default depth of 100 commits for fast fetching
12
+ - **Auto-update**: Re-fetching updates existing git repos
13
+ - **Global cache**: All repos cached at `~/.cache/repo/`
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ # Clone and build
19
+ git clone https://github.com/cevr/repo.git
20
+ cd repo
21
+ bun install
22
+ bun run build
23
+
24
+ # Link globally
25
+ bun link
26
+ ```
27
+
28
+ ## Quick Start
29
+
30
+ ```bash
31
+ # Fetch a GitHub repo
32
+ repo fetch vercel/next.js
33
+
34
+ # Fetch an npm package (resolves to source repo)
35
+ repo fetch npm:effect@3.0.0
36
+
37
+ # Get the cached path
38
+ repo path vercel/next.js
39
+
40
+ # List all cached repos
41
+ repo list
42
+
43
+ # Search across all cached repos
44
+ repo search "createServer"
45
+ ```
46
+
47
+ ## Spec Formats
48
+
49
+ | Format | Example |
50
+ |--------|---------|
51
+ | GitHub | `owner/repo`, `owner/repo@v1.0.0` |
52
+ | npm | `npm:lodash`, `npm:@effect/cli@0.73.0` |
53
+ | PyPI | `pypi:requests@2.31.0` |
54
+ | Crates | `crates:serde@1.0.0` |
55
+
56
+ ## Commands
57
+
58
+ | Command | Description |
59
+ |---------|-------------|
60
+ | `repo fetch <spec>` | Fetch/update repository |
61
+ | `repo path <spec>` | Get cached path |
62
+ | `repo info <spec>` | Show repository metadata |
63
+ | `repo list` | List cached repos |
64
+ | `repo search <query>` | Search across cached repos |
65
+ | `repo stats` | Cache statistics |
66
+ | `repo remove <spec>` | Remove from cache |
67
+ | `repo prune` | Remove old/large repos |
68
+ | `repo clean` | Clear entire cache |
69
+ | `repo open <spec>` | Open in editor |
70
+
71
+ See [SKILL.md](./SKILL.md) for detailed usage.
72
+
73
+ ## Storage
74
+
75
+ ```
76
+ ~/.cache/repo/
77
+ ├── metadata.json # Index
78
+ ├── {owner}/{repo}/ # GitHub repos
79
+ └── {package}/{version}/ # Package registries
80
+ ```
81
+
82
+ ## Tech Stack
83
+
84
+ - [Bun](https://bun.sh) - Runtime
85
+ - [Effect](https://effect.website) - Functional TypeScript
86
+ - [@effect/cli](https://github.com/Effect-TS/effect/tree/main/packages/cli) - CLI framework
87
+
88
+ ## Credits
89
+
90
+ Inspired by [vercel-labs/opensrc](https://github.com/vercel-labs/opensrc) - a tool for exploring open source projects.
91
+
92
+ ## License
93
+
94
+ MIT
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@cvr/repo",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "repo": "./bin/repo"
7
+ },
8
+ "files": [
9
+ "src",
10
+ "bin",
11
+ "tsconfig.json"
12
+ ],
13
+ "scripts": {
14
+ "build": "bun build src/main.ts --outfile bin/repo --compile",
15
+ "postinstall": "bun run build",
16
+ "prepublishOnly": "rm -rf bin",
17
+ "dev": "bun run src/main.ts",
18
+ "typecheck": "tsc --noEmit",
19
+ "test": "vitest run",
20
+ "test:watch": "vitest",
21
+ "prepare": "effect-language-service patch",
22
+ "link": "ln -sf $(pwd)/bin/repo ~/.bun/bin/repo",
23
+ "version": "changeset version",
24
+ "release": "changeset publish"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/cevr/repo.git"
32
+ },
33
+ "engines": {
34
+ "bun": ">=1.0.0"
35
+ },
36
+ "dependencies": {
37
+ "@effect/cli": "^0.73.0",
38
+ "@effect/platform": "^0.94.1",
39
+ "@effect/platform-bun": "^0.87.0",
40
+ "effect": "^3.19.14"
41
+ },
42
+ "devDependencies": {
43
+ "@changesets/changelog-github": "^0.5.0",
44
+ "@changesets/cli": "^2.27.0",
45
+ "@effect/language-service": "^0.64.0",
46
+ "@effect/vitest": "^0.27.0",
47
+ "@types/bun": "^1.3.5",
48
+ "typescript": "^5.9.3",
49
+ "vitest": "^4.0.16"
50
+ }
51
+ }
@@ -0,0 +1,40 @@
1
+ import { Command, Options } from "@effect/cli"
2
+ import { Console, Effect } from "effect"
3
+ import { formatBytes } from "../types.js"
4
+ import { CacheService } from "../services/cache.js"
5
+ import { MetadataService } from "../services/metadata.js"
6
+
7
+ const confirmOption = Options.boolean("yes").pipe(
8
+ Options.withAlias("y"),
9
+ Options.withDefault(false),
10
+ Options.withDescription("Skip confirmation prompt")
11
+ )
12
+
13
+ export const clean = Command.make("clean", { yes: confirmOption }, ({ yes }) =>
14
+ Effect.gen(function* () {
15
+ const cache = yield* CacheService
16
+ const metadata = yield* MetadataService
17
+
18
+ const repos = yield* metadata.all()
19
+ if (repos.length === 0) {
20
+ yield* Console.log("Cache is already empty.")
21
+ return
22
+ }
23
+
24
+ const totalSize = repos.reduce((sum, r) => sum + r.sizeBytes, 0)
25
+
26
+ if (!yes) {
27
+ yield* Console.log(
28
+ `This will remove ${repos.length} cached repositories (${formatBytes(totalSize)}).`
29
+ )
30
+ yield* Console.log('Use --yes to confirm.')
31
+ return
32
+ }
33
+
34
+ yield* cache.removeAll()
35
+ yield* metadata.save({ version: 1, repos: [] })
36
+
37
+ yield* Console.log(`Removed ${repos.length} repositories.`)
38
+ yield* Console.log(`Freed: ${formatBytes(totalSize)}`)
39
+ })
40
+ )
@@ -0,0 +1,152 @@
1
+ import { Args, Command, Options } from "@effect/cli"
2
+ import { Console, Effect } from "effect"
3
+ import { formatBytes, specToString } from "../types.js"
4
+ import { CacheService } from "../services/cache.js"
5
+ import { MetadataService } from "../services/metadata.js"
6
+ import { RegistryService } from "../services/registry.js"
7
+ import { GitService } from "../services/git.js"
8
+
9
+ const specArg = Args.text({ name: "spec" }).pipe(
10
+ Args.withDescription(
11
+ "Package spec: owner/repo, npm:package[@version], pypi:package, crates:crate"
12
+ )
13
+ )
14
+
15
+ const forceOption = Options.boolean("force").pipe(
16
+ Options.withAlias("f"),
17
+ Options.withDefault(false),
18
+ Options.withDescription("Force re-clone (removes existing and clones fresh)")
19
+ )
20
+
21
+ const updateOption = Options.boolean("update").pipe(
22
+ Options.withAlias("u"),
23
+ Options.withDefault(false),
24
+ Options.withDescription("Update existing repo (git pull)")
25
+ )
26
+
27
+ const fullHistoryOption = Options.boolean("full").pipe(
28
+ Options.withDefault(false),
29
+ Options.withDescription("Clone full git history (default: shallow clone with depth 100)")
30
+ )
31
+
32
+ export const fetch = Command.make(
33
+ "fetch",
34
+ { spec: specArg, force: forceOption, update: updateOption, full: fullHistoryOption },
35
+ ({ spec, force, update, full }) =>
36
+ Effect.gen(function* () {
37
+ const registry = yield* RegistryService
38
+ const cache = yield* CacheService
39
+ const metadata = yield* MetadataService
40
+ const git = yield* GitService
41
+
42
+ // Parse the spec
43
+ const parsedSpec = yield* registry.parseSpec(spec)
44
+ const specStr = specToString(parsedSpec)
45
+
46
+ // Check if already cached
47
+ const existing = yield* metadata.find(parsedSpec)
48
+ const destPath = yield* cache.getPath(parsedSpec)
49
+
50
+ if (existing) {
51
+ const isGit = yield* git.isGitRepo(existing.path)
52
+
53
+ if (force) {
54
+ // Force: remove and re-clone
55
+ yield* Console.log(`Force re-fetching ${specStr}...`)
56
+ yield* cache.remove(existing.path)
57
+ yield* metadata.remove(parsedSpec)
58
+ } else if (update || isGit) {
59
+ // Update existing git repo
60
+ if (isGit) {
61
+ yield* Console.log(`Updating ${specStr}...`)
62
+ yield* git.update(existing.path).pipe(
63
+ Effect.catchAll((e) =>
64
+ Console.log(`Update failed, repo may be up to date: ${e._tag}`)
65
+ )
66
+ )
67
+
68
+ // Recalculate size after update
69
+ const sizeBytes = yield* cache.getSize(existing.path)
70
+ const currentRef = yield* git.getCurrentRef(existing.path).pipe(
71
+ Effect.orElseSucceed(() => "unknown")
72
+ )
73
+
74
+ yield* metadata.add({
75
+ spec: parsedSpec,
76
+ fetchedAt: existing.fetchedAt,
77
+ lastAccessedAt: new Date().toISOString(),
78
+ sizeBytes,
79
+ path: existing.path,
80
+ })
81
+
82
+ yield* Console.log(`Updated: ${existing.path}`)
83
+ yield* Console.log(`Current ref: ${currentRef}`)
84
+ yield* Console.log(`Size: ${formatBytes(sizeBytes)}`)
85
+ return
86
+ } else {
87
+ // Not a git repo, just report it exists
88
+ yield* metadata.updateAccessTime(parsedSpec)
89
+ yield* Console.log(`Already cached at: ${existing.path}`)
90
+ yield* Console.log(`Size: ${formatBytes(existing.sizeBytes)}`)
91
+ yield* Console.log(`(Not a git repo - use --force to re-fetch)`)
92
+ return
93
+ }
94
+ } else {
95
+ // Already cached, just update access time
96
+ yield* metadata.updateAccessTime(parsedSpec)
97
+ yield* Console.log(`Already cached at: ${existing.path}`)
98
+ yield* Console.log(`Size: ${formatBytes(existing.sizeBytes)}`)
99
+ if (isGit) {
100
+ yield* Console.log(`Use --update to pull latest changes`)
101
+ }
102
+ yield* Console.log(`Use --force to re-fetch from scratch`)
103
+ return
104
+ }
105
+ }
106
+
107
+ // Fresh fetch
108
+ yield* Console.log(`Fetching ${specStr}...`)
109
+
110
+ // Ensure parent directory exists
111
+ const parentPath = destPath.split("/").slice(0, -1).join("/")
112
+ yield* cache.ensureDir(parentPath)
113
+
114
+ // Fetch from registry
115
+ yield* registry.fetch(parsedSpec, destPath, { fullHistory: full })
116
+
117
+ // Calculate size
118
+ const sizeBytes = yield* cache.getSize(destPath)
119
+
120
+ // Get current ref if it's a git repo
121
+ const isGit = yield* git.isGitRepo(destPath)
122
+ if (isGit) {
123
+ const currentRef = yield* git.getCurrentRef(destPath).pipe(
124
+ Effect.orElseSucceed(() => "unknown")
125
+ )
126
+ yield* Console.log(`Ref: ${currentRef}`)
127
+ }
128
+
129
+ // Update metadata
130
+ const now = new Date().toISOString()
131
+ yield* metadata.add({
132
+ spec: parsedSpec,
133
+ fetchedAt: now,
134
+ lastAccessedAt: now,
135
+ sizeBytes,
136
+ path: destPath,
137
+ })
138
+
139
+ yield* Console.log(`Fetched to: ${destPath}`)
140
+ yield* Console.log(`Size: ${formatBytes(sizeBytes)}`)
141
+ }).pipe(
142
+ Effect.catchAll((error) =>
143
+ Effect.gen(function* () {
144
+ if (typeof error === "object" && error !== null && "_tag" in error) {
145
+ yield* Console.error(`Error: ${error._tag}: ${JSON.stringify(error)}`)
146
+ } else {
147
+ yield* Console.error(`Error: ${String(error)}`)
148
+ }
149
+ })
150
+ )
151
+ )
152
+ )
@@ -0,0 +1,83 @@
1
+ import { Args, Command, Options } from "@effect/cli"
2
+ import { Console, Effect, Option } from "effect"
3
+ import { specToString, formatBytes, formatRelativeTime } from "../types.js"
4
+ import { MetadataService } from "../services/metadata.js"
5
+ import { RegistryService } from "../services/registry.js"
6
+ import { GitService } from "../services/git.js"
7
+
8
+ const specArg = Args.text({ name: "spec" }).pipe(
9
+ Args.withDescription("Package spec to get info for")
10
+ )
11
+
12
+ const jsonOption = Options.boolean("json").pipe(
13
+ Options.withDefault(false),
14
+ Options.withDescription("Output as JSON")
15
+ )
16
+
17
+ export const info = Command.make(
18
+ "info",
19
+ { spec: specArg, json: jsonOption },
20
+ ({ spec, json }) =>
21
+ Effect.gen(function* () {
22
+ const registry = yield* RegistryService
23
+ const metadata = yield* MetadataService
24
+ const git = yield* GitService
25
+
26
+ const parsedSpec = yield* registry.parseSpec(spec)
27
+ const existing = yield* metadata.find(parsedSpec)
28
+
29
+ if (!existing) {
30
+ yield* Console.error(`Not cached: ${specToString(parsedSpec)}`)
31
+ return
32
+ }
33
+
34
+ const isGit = yield* git.isGitRepo(existing.path)
35
+ const currentRef = isGit
36
+ ? yield* git
37
+ .getCurrentRef(existing.path)
38
+ .pipe(Effect.orElseSucceed(() => "unknown"))
39
+ : null
40
+
41
+ if (json) {
42
+ yield* Console.log(
43
+ JSON.stringify(
44
+ {
45
+ spec: {
46
+ registry: existing.spec.registry,
47
+ name: existing.spec.name,
48
+ version: Option.getOrNull(existing.spec.version),
49
+ },
50
+ path: existing.path,
51
+ sizeBytes: existing.sizeBytes,
52
+ sizeHuman: formatBytes(existing.sizeBytes),
53
+ fetchedAt: existing.fetchedAt,
54
+ lastAccessedAt: existing.lastAccessedAt,
55
+ isGitRepo: isGit,
56
+ currentRef,
57
+ },
58
+ null,
59
+ 2
60
+ )
61
+ )
62
+ } else {
63
+ yield* Console.log(``)
64
+ yield* Console.log(`Repository Info`)
65
+ yield* Console.log("═".repeat(50))
66
+ yield* Console.log(`Spec: ${specToString(existing.spec)}`)
67
+ yield* Console.log(`Registry: ${existing.spec.registry}`)
68
+ yield* Console.log(`Path: ${existing.path}`)
69
+ yield* Console.log(`Size: ${formatBytes(existing.sizeBytes)}`)
70
+ yield* Console.log(
71
+ `Fetched: ${formatRelativeTime(new Date(existing.fetchedAt))}`
72
+ )
73
+ yield* Console.log(
74
+ `Accessed: ${formatRelativeTime(new Date(existing.lastAccessedAt))}`
75
+ )
76
+ if (isGit && currentRef) {
77
+ yield* Console.log(`Git ref: ${currentRef}`)
78
+ }
79
+ }
80
+
81
+ yield* metadata.updateAccessTime(parsedSpec)
82
+ })
83
+ )
@@ -0,0 +1,107 @@
1
+ import { Command, Options } from "@effect/cli"
2
+ import { Console, Effect, Option } from "effect"
3
+ import { formatBytes, formatRelativeTime, specToString } from "../types.js"
4
+ import { MetadataService } from "../services/metadata.js"
5
+
6
+ const registryOption = Options.choice("registry", [
7
+ "github",
8
+ "npm",
9
+ "pypi",
10
+ "crates",
11
+ ] as const).pipe(
12
+ Options.withAlias("r"),
13
+ Options.optional,
14
+ Options.withDescription("Filter by registry")
15
+ )
16
+
17
+ const jsonOption = Options.boolean("json").pipe(
18
+ Options.withDefault(false),
19
+ Options.withDescription("Output as JSON")
20
+ )
21
+
22
+ const sortOption = Options.choice("sort", [
23
+ "date",
24
+ "size",
25
+ "name",
26
+ ] as const).pipe(
27
+ Options.withAlias("s"),
28
+ Options.withDefault("date" as const),
29
+ Options.withDescription("Sort by: date, size, name")
30
+ )
31
+
32
+ export const list = Command.make(
33
+ "list",
34
+ { registry: registryOption, json: jsonOption, sort: sortOption },
35
+ ({ registry, json, sort }) =>
36
+ Effect.gen(function* () {
37
+ const metadata = yield* MetadataService
38
+ let repos = yield* metadata.all()
39
+
40
+ // Filter by registry if specified
41
+ if (Option.isSome(registry)) {
42
+ repos = repos.filter((r) => r.spec.registry === registry.value)
43
+ }
44
+
45
+ // Sort
46
+ const sorted = [...repos].sort((a, b) => {
47
+ switch (sort) {
48
+ case "date":
49
+ return (
50
+ new Date(b.lastAccessedAt).getTime() -
51
+ new Date(a.lastAccessedAt).getTime()
52
+ )
53
+ case "size":
54
+ return b.sizeBytes - a.sizeBytes
55
+ case "name":
56
+ return a.spec.name.localeCompare(b.spec.name)
57
+ }
58
+ })
59
+
60
+ if (json) {
61
+ yield* Console.log(
62
+ JSON.stringify(
63
+ {
64
+ repos: sorted.map((r) => ({
65
+ ...r,
66
+ spec: {
67
+ registry: r.spec.registry,
68
+ name: r.spec.name,
69
+ version: Option.getOrNull(r.spec.version),
70
+ },
71
+ })),
72
+ total: sorted.length,
73
+ totalSize: sorted.reduce((sum, r) => sum + r.sizeBytes, 0),
74
+ },
75
+ null,
76
+ 2
77
+ )
78
+ )
79
+ return
80
+ }
81
+
82
+ if (sorted.length === 0) {
83
+ yield* Console.log("No repositories cached.")
84
+ yield* Console.log('Use "repo fetch <spec>" to cache a repository.')
85
+ return
86
+ }
87
+
88
+ yield* Console.log("")
89
+ yield* Console.log(`Cached Repositories (${sorted.length})`)
90
+ yield* Console.log("═".repeat(80))
91
+
92
+ for (const repo of sorted) {
93
+ const spec = specToString(repo.spec)
94
+ const size = formatBytes(repo.sizeBytes)
95
+ const date = formatRelativeTime(new Date(repo.lastAccessedAt))
96
+ const registryStr = repo.spec.registry.padEnd(6)
97
+
98
+ yield* Console.log(
99
+ `${registryStr} ${spec.padEnd(40)} ${size.padStart(10)} ${date}`
100
+ )
101
+ }
102
+
103
+ yield* Console.log("═".repeat(80))
104
+ const totalSize = sorted.reduce((sum, r) => sum + r.sizeBytes, 0)
105
+ yield* Console.log(`Total: ${formatBytes(totalSize)}`)
106
+ })
107
+ )
@@ -0,0 +1,89 @@
1
+ import { Args, Command, Options } from "@effect/cli"
2
+ import { Console, Effect, Option } from "effect"
3
+ import { OpenError, specToString } from "../types.js"
4
+ import { MetadataService } from "../services/metadata.js"
5
+ import { RegistryService } from "../services/registry.js"
6
+
7
+ const specArg = Args.text({ name: "spec" }).pipe(
8
+ Args.withDescription("Package spec to open")
9
+ )
10
+
11
+ const finderOption = Options.boolean("finder").pipe(
12
+ Options.withAlias("f"),
13
+ Options.withDefault(false),
14
+ Options.withDescription("Open in Finder instead of editor")
15
+ )
16
+
17
+ const editorOption = Options.text("editor").pipe(
18
+ Options.withAlias("e"),
19
+ Options.optional,
20
+ Options.withDescription("Editor to use (defaults to $EDITOR or code)")
21
+ )
22
+
23
+ export const open = Command.make(
24
+ "open",
25
+ { spec: specArg, finder: finderOption, editor: editorOption },
26
+ ({ spec, finder, editor }) =>
27
+ Effect.gen(function* () {
28
+ const registry = yield* RegistryService
29
+ const metadata = yield* MetadataService
30
+
31
+ // Parse the spec
32
+ const parsedSpec = yield* registry.parseSpec(spec)
33
+
34
+ // Find in metadata
35
+ const existing = yield* metadata.find(parsedSpec)
36
+ if (!existing) {
37
+ yield* Console.log(`Not found: ${specToString(parsedSpec)}`)
38
+ yield* Console.log('Use "repo fetch" first to cache it.')
39
+ return
40
+ }
41
+
42
+ // Update access time
43
+ yield* metadata.updateAccessTime(parsedSpec)
44
+
45
+ if (finder) {
46
+ // Open in Finder (macOS)
47
+ const proc = Bun.spawn(["open", existing.path], {
48
+ stdout: "pipe",
49
+ stderr: "pipe",
50
+ })
51
+ yield* Effect.tryPromise({
52
+ try: () => proc.exited.then((code) => {
53
+ if (code !== 0) throw new Error(`exit code ${code}`)
54
+ }),
55
+ catch: (e) => new OpenError({ command: "open", cause: e }),
56
+ })
57
+ yield* Console.log(`Opened in Finder: ${existing.path}`)
58
+ } else {
59
+ // Open in editor
60
+ const editorCmd = Option.isSome(editor)
61
+ ? editor.value
62
+ : process.env.EDITOR ?? "code"
63
+
64
+ const proc = Bun.spawn([editorCmd, existing.path], {
65
+ stdout: "pipe",
66
+ stderr: "pipe",
67
+ })
68
+ yield* Effect.tryPromise({
69
+ try: () => proc.exited.then((code) => {
70
+ if (code !== 0) throw new Error(`exit code ${code}`)
71
+ }),
72
+ catch: (e) => new OpenError({ command: editorCmd, cause: e }),
73
+ })
74
+ yield* Console.log(`Opened in ${editorCmd}: ${existing.path}`)
75
+ }
76
+ }).pipe(
77
+ Effect.catchAll((error) =>
78
+ Effect.gen(function* () {
79
+ if (typeof error === "object" && error !== null && "_tag" in error) {
80
+ yield* Console.error(
81
+ `Error: ${(error as { _tag: string })._tag}: ${JSON.stringify(error)}`
82
+ )
83
+ } else {
84
+ yield* Console.error(`Error: ${String(error)}`)
85
+ }
86
+ })
87
+ )
88
+ )
89
+ )
@@ -0,0 +1,38 @@
1
+ import { Args, Command, Options } from "@effect/cli"
2
+ import { Console, Effect } from "effect"
3
+ import { NotFoundError, specToString } from "../types.js"
4
+ import { MetadataService } from "../services/metadata.js"
5
+ import { RegistryService } from "../services/registry.js"
6
+
7
+ const specArg = Args.text({ name: "spec" }).pipe(
8
+ Args.withDescription("Package spec to get path for")
9
+ )
10
+
11
+ const quietOption = Options.boolean("quiet").pipe(
12
+ Options.withAlias("q"),
13
+ Options.withDefault(false),
14
+ Options.withDescription("Output only the path, exit 1 if not cached")
15
+ )
16
+
17
+ export const path = Command.make(
18
+ "path",
19
+ { spec: specArg, quiet: quietOption },
20
+ ({ spec, quiet }) =>
21
+ Effect.gen(function* () {
22
+ const registry = yield* RegistryService
23
+ const metadata = yield* MetadataService
24
+
25
+ const parsedSpec = yield* registry.parseSpec(spec)
26
+ const existing = yield* metadata.find(parsedSpec)
27
+
28
+ if (!existing) {
29
+ if (!quiet) {
30
+ yield* Console.error(`Not cached: ${specToString(parsedSpec)}`)
31
+ yield* Console.error(`Run: repo fetch ${spec}`)
32
+ }
33
+ return yield* Effect.fail(new NotFoundError({ spec: parsedSpec }))
34
+ }
35
+
36
+ yield* Console.log(existing.path)
37
+ }).pipe(Effect.catchAll(() => Effect.void))
38
+ )