| … | … | @@ -50,6 +50,9 @@ export async function repoCommand (client, sub, args, flags) { |
| 50 | 50 | return repoCollabUnlink(client, args[0], flags) |
| 51 | 51 | case 'set': |
| 52 | 52 | return repoSet(client, args[0], flags) |
| 53 | case 'delete': | |
| 54 | case 'rm': | |
| 55 | return repoDelete(client, args[0], flags) | |
| 53 | 56 | default: |
| 54 | 57 | repoHelp() |
| 55 | 58 | } |
| … | … | @@ -460,8 +463,6 @@ async function repoCollabJoin (client, invite, flags) { |
| 460 | 463 | log(`Joining via blind-pairing invite...`) |
| 461 | 464 | log(`(Note: full P2P join requires server mode. Using API for local join.)`) |
| 462 | 465 | |
| 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 | |
| 465 | 466 | const data = await client.post('/api/repos/_join', { invite, role: 'writer' }).catch(() => null) |
| 466 | 467 | |
| 467 | 468 | if (data && data.error) { |
| … | … | @@ -557,6 +558,19 @@ async function repoCollabUnlink (client, name, flags) { |
| 557 | 558 | log(`\u2713 Unlinked: ${name}`) |
| 558 | 559 | } |
| 559 | 560 | |
| 561 | async 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 | ||
| 560 | 574 | export function repoHelp () { |
| 561 | 575 | log(`Usage: pgh repo <subcommand> [args] [flags] |
| 562 | 576 | |
| … | … | @@ -572,6 +586,7 @@ Subcommands: |
| 572 | 586 | import-all Import all git repos |
| 573 | 587 | create <name> Create repo via P2P (requires --server-key) |
| 574 | 588 | migrate <path> Migrate repos to new directory |
| 589 | delete <name> Remove repo from registry (admin only) | |
| 575 | 590 | link <name> <path> Add 'pear' remote to working repo |
| 576 | 591 | link <name> Create Autobase overlay + display invite |
| 577 | 592 | join <invite> Join linked repo via blind-pairing invite |
| … | … | @@ -553,6 +553,42 @@ echo "[post-receive] Auto-deploy triggered for $REPO_NAME" |
| 553 | 553 | } |
| 554 | 554 | } |
| 555 | 555 | |
| 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 | ||
| 556 | 592 | /** |
| 557 | 593 | * Register all repositories |
| 558 | 594 | */ |
| … | … | @@ -14,6 +14,7 @@ |
| 14 | 14 | * - POST /api/admin/repos/:repoName/collaborators - Add collaborator |
| 15 | 15 | * - DELETE /api/admin/repos/:repoName/collaborators/:userId - Remove collaborator |
| 16 | 16 | * - POST /api/admin/repos/:repoName/archived - Set archived status |
| 17 | * - DELETE /api/admin/repos/:repoName - Delete repo from registry | |
| 17 | 18 | */ |
| 18 | 19 | |
| 19 | 20 | import { html, json, unauthorized, forbidden, badRequest, readJson } from '../response.js' |
| … | … | @@ -45,6 +46,7 @@ export function registerAdminRoutes(router) { |
| 45 | 46 | router.post('/api/admin/repos/:repoName/collaborators', requireAdmin, handleAddCollaborator) |
| 46 | 47 | router.delete('/api/admin/repos/:repoName/collaborators/:userId', requireAdmin, handleRemoveCollaborator) |
| 47 | 48 | router.post('/api/admin/repos/:repoName/archived', requireAdmin, handleSetRepoArchived) |
| 49 | router.delete('/api/admin/repos/:repoName', requireAdmin, handleDeleteRepo) | |
| 48 | 50 | } |
| 49 | 51 | |
| 50 | 52 | /** |
| … | … | @@ -321,6 +323,24 @@ async function handleSetRepoArchived(req, res, server) { |
| 321 | 323 | } |
| 322 | 324 | } |
| 323 | 325 | |
| 326 | /** | |
| 327 | * Delete repository from registry | |
| 328 | */ | |
| 329 | async 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 | ||
| 324 | 344 | // Helper functions |
| 325 | 345 | async function countUsers(server) { |
| 326 | 346 | const { authManager } = server.auth |
| … | … | @@ -361,5 +381,6 @@ export { |
| 361 | 381 | handleSetRepoOwner, |
| 362 | 382 | handleAddCollaborator, |
| 363 | 383 | handleRemoveCollaborator, |
| 364 | handleSetRepoArchived | |
| 384 | handleSetRepoArchived, | |
| 385 | handleDeleteRepo | |
| 365 | 386 | } |