OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git commits / 4ab5746

Add repo delete endpoint — unregister repos from registry
- RepoRegistry.unregister() removes repo: key and cleans category index - DELETE /api/admin/repos/:repoName admin endpoint - pgh repo delete <name> CLI command (alias: rm) - Only removes registry entry, does not touch bare git repo Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
4ab5746f19a6b658ddfeb8284e8dc24314f44567
by plankatron <fleeky@gmail.com> · 2026-04-09T02:48:56.000Z

Changed files (3) +75 -3

lib/cli/commands/repo.js +17 -2
……@@ -50,6 +50,9 @@ export async function repoCommand (client, sub, args, flags) {
5050 return repoCollabUnlink(client, args[0], flags)
5151 case 'set':
5252 return repoSet(client, args[0], flags)
53 case 'delete':
54 case 'rm':
55 return repoDelete(client, args[0], flags)
5356 default:
5457 repoHelp()
5558 }
……@@ -460,8 +463,6 @@ async function repoCollabJoin (client, invite, flags) {
460463 log(`Joining via blind-pairing invite...`)
461464 log(`(Note: full P2P join requires server mode. Using API for local join.)`)
462465
463 // For API-based join, we need the server to process the invite
464 // This is a placeholder — full implementation needs the server to run RepoLinker
465466 const data = await client.post('/api/repos/_join', { invite, role: 'writer' }).catch(() => null)
466467
467468 if (data && data.error) {
……@@ -557,6 +558,19 @@ async function repoCollabUnlink (client, name, flags) {
557558 log(`\u2713 Unlinked: ${name}`)
558559}
559560
561async function repoDelete (client, name, flags) {
562 if (!name) error('Usage: pgh repo delete <name>')
563
564 const data = await client.delete(`/api/admin/repos/${encodeURIComponent(name)}`)
565
566 if (flags.json) {
567 jsonOut(data)
568 return
569 }
570
571 log(`Deleted ${name} from registry`)
572}
573
560574export function repoHelp () {
561575 log(`Usage: pgh repo <subcommand> [args] [flags]
562576
……@@ -572,6 +586,7 @@ Subcommands:
572586 import-all Import all git repos
573587 create <name> Create repo via P2P (requires --server-key)
574588 migrate <path> Migrate repos to new directory
589 delete <name> Remove repo from registry (admin only)
575590 link <name> <path> Add 'pear' remote to working repo
576591 link <name> Create Autobase overlay + display invite
577592 join <invite> Join linked repo via blind-pairing invite
lib/repo-registry.js +36 -0
……@@ -553,6 +553,42 @@ echo "[post-receive] Auto-deploy triggered for $REPO_NAME"
553553 }
554554 }
555555
556 /**
557 * Unregister a repository (remove from registry)
558 */
559 async unregister(repoName) {
560 const existing = await this.registry.get(`repo:${repoName}`).catch(() => null)
561 if (!existing || !existing.value) {
562 throw new Error(`Repository "${repoName}" not found in registry`)
563 }
564
565 const repoInfo = existing.value
566
567 // Remove from category index
568 if (repoInfo.category) {
569 const categoryKey = `category:${repoInfo.category}`
570 const existingCategory = await this.registry.get(categoryKey).catch(() => null)
571 if (existingCategory && existingCategory.value) {
572 const repos = existingCategory.value.repos.filter(r => r !== repoName)
573 if (repos.length > 0) {
574 await this.registry.put(categoryKey, {
575 category: repoInfo.category,
576 repos,
577 updatedAt: Date.now()
578 })
579 } else {
580 await this.registry.del(categoryKey)
581 }
582 }
583 }
584
585 // Remove main repo entry
586 await this.registry.del(`repo:${repoName}`)
587
588 console.log(` ✗ Unregistered ${repoName}`)
589 return repoInfo
590 }
591
556592 /**
557593 * Register all repositories
558594 */
lib/routes/admin.js +22 -1
……@@ -14,6 +14,7 @@
1414 * - POST /api/admin/repos/:repoName/collaborators - Add collaborator
1515 * - DELETE /api/admin/repos/:repoName/collaborators/:userId - Remove collaborator
1616 * - POST /api/admin/repos/:repoName/archived - Set archived status
17 * - DELETE /api/admin/repos/:repoName - Delete repo from registry
1718 */
1819
1920import { html, json, unauthorized, forbidden, badRequest, readJson } from '../response.js'
……@@ -45,6 +46,7 @@ export function registerAdminRoutes(router) {
4546 router.post('/api/admin/repos/:repoName/collaborators', requireAdmin, handleAddCollaborator)
4647 router.delete('/api/admin/repos/:repoName/collaborators/:userId', requireAdmin, handleRemoveCollaborator)
4748 router.post('/api/admin/repos/:repoName/archived', requireAdmin, handleSetRepoArchived)
49 router.delete('/api/admin/repos/:repoName', requireAdmin, handleDeleteRepo)
4850}
4951
5052/**
……@@ -321,6 +323,24 @@ async function handleSetRepoArchived(req, res, server) {
321323 }
322324}
323325
326/**
327 * Delete repository from registry
328 */
329async function handleDeleteRepo(req, res, server) {
330 try {
331 const { repoName } = req.params
332
333 const removed = await server.repoRegistry.unregister(repoName)
334
335 console.log(`[Admin] ${req.auth.userId || req.auth.identityId} deleted repo ${repoName} from registry`)
336
337 json(res, { success: true, repo: removed.name })
338 } catch (err) {
339 console.error('[API] Delete repo error:', err)
340 json(res, { error: err.message }, err.message.includes('not found') ? 404 : 500)
341 }
342}
343
324344// Helper functions
325345async function countUsers(server) {
326346 const { authManager } = server.auth
……@@ -361,5 +381,6 @@ export {
361381 handleSetRepoOwner,
362382 handleAddCollaborator,
363383 handleRemoveCollaborator,
364 handleSetRepoArchived
384 handleSetRepoArchived,
385 handleDeleteRepo
365386}