/** pgh org list|show|create|delete|members|add-member|remove-member|add-resource|remove-resource|invite|join|peers|set-auth */
import { PghPeerClient } from '../peer-client.js'
export const usage = `pgh org — orgs and membership
pgh org list List orgs
pgh org show <name> Show one org
pgh org create <name> [--type group|team]
pgh org delete <name>
pgh org members <name> List members
pgh org add-member <name> <userId>
pgh org remove-member <name> <userId>
pgh org add-resource <name> <repo> Attach a repo to the org
pgh org remove-resource <name> <repo>
pgh org invite <name> [--role reader|contributor|maintainer|admin]
pgh org join <invitation> P2P join via an invite code
pgh org peers <name> Show resource repos + their writers
pgh org set-auth <name> --peer <url> --level owner|writer|reader
Admin: set a peer's write authority
across ALL of the org's repos`
export async function run (client, args) {
const sub = args[0]
if (!sub) throw new Error(usage)
if (sub === 'list' || sub === 'ls') return client.call('list-orgs', {})
// ── Per-org-autobase invite / join (issue #35) ──
if (sub === 'invite') {
const name = args[1]
if (!name) throw new Error('usage: pgh org invite <name> [--role reader|contributor|maintainer|admin]')
const ri = args.indexOf('--role')
const role = (ri !== -1 && args[ri + 1]) || 'reader'
// → { invitation } — a blind-pairing invite code (the whole UX).
return client.call('create-org-invite', { name, role })
}
if (sub === 'join') {
const invitation = args[1]
if (!invitation) throw new Error('usage: pgh org join <invitation>')
// One-step P2P join: the daemon pairs over the swarm and, for
// contributor+, enrolls us as a writer DURING pairing. No owner URL,
// no HTTP enroll call — the invite code is the whole UX.
return client.call('join-org', { invitation })
}
if (sub === 'show') {
const name = args[1]
if (!name) throw new Error('usage: pgh org show <name>')
return client.call('get-org', { name })
}
if (sub === 'create') {
const name = args[1]
if (!name) throw new Error('usage: pgh org create <name> [--type group|team]')
const typeIdx = args.indexOf('--type')
const type = (typeIdx !== -1 && args[typeIdx + 1]) || 'group'
return client.call('create-org', { name, type })
}
if (sub === 'delete' || sub === 'rm') {
const name = args[1]
if (!name) throw new Error('usage: pgh org delete <name>')
return client.call('delete-org', { name })
}
if (sub === 'members') {
const name = args[1]
if (!name) throw new Error('usage: pgh org members <name>')
const org = await client.call('get-org', { name })
return org.members || []
}
if (sub === 'add-member') {
const name = args[1]
const userId = args[2]
if (!name || !userId) throw new Error('usage: pgh org add-member <orgName> <userId>')
return client.call('add-org-member', { name, userId })
}
if (sub === 'remove-member') {
const name = args[1]
const userId = args[2]
if (!name || !userId) throw new Error('usage: pgh org remove-member <orgName> <userId>')
return client.call('remove-org-member', { name, userId })
}
if (sub === 'add-resource') {
const name = args[1]
const resource = args[2]
if (!name || !resource) throw new Error('usage: pgh org add-resource <orgName> <resource>')
return client.call('add-org-resource', { org: name, resource })
}
if (sub === 'remove-resource') {
const name = args[1]
const resource = args[2]
if (!name || !resource) throw new Error('usage: pgh org remove-resource <orgName> <resource>')
return client.call('remove-org-resource', { org: name, resource })
}
// P2P pairing: owner mints an invitation; a peer joins the org over the
// swarm and (for contributor+) is added as a writer during pairing.
if (sub === 'invite') {
const name = args[1]
if (!name) throw new Error('usage: pgh org invite <name> [--role reader|contributor|owner]')
const roleIdx = args.indexOf('--role')
const role = (roleIdx !== -1 && args[roleIdx + 1]) || 'reader'
return client.call('create-org-invite', { name, role })
}
if (sub === 'join') {
const invitation = args[1]
if (!invitation) throw new Error('usage: pgh org join <invitation>')
return client.call('join-org', { invitation })
}
// See all peers' write authority across the org's repos. For each resource
// repo, list the per-repo autobase writers (gip push authority). This is the
// admin's "who can write what" view that the org-resource path lacked.
if (sub === 'peers') {
const name = args[1]
if (!name) throw new Error('usage: pgh org peers <name>')
const org = await client.call('get-org', { name })
const resources = Array.isArray(org.resources) && org.resources.length
? org.resources
: Object.keys(org.resourceKeys || {})
const out = { org: name, members: org.members || [], resources: [] }
for (const repo of resources) {
try {
const w = await client.call('repo-list-writers', { name: repo })
out.resources.push({ repo, writers: w.writers || [] })
} catch (err) {
out.resources.push({ repo, error: err.message })
}
}
return out
}
// Admin: set a peer's write authority across EVERY repo in the org, in one
// shot. For owner/writer, pull the peer's per-repo tracker local key and
// addWriter it (the peer's read-only org-mount flips to writable). For
// reader, revoke. This is the unified "set the auth level for a peer" the
// org layer was missing — it drives the autobase writer-set, not just the
// git-push ACL.
if (sub === 'set-auth') {
const name = args[1]
if (!name) throw new Error('usage: pgh org set-auth <name> --peer <url> --level owner|writer|reader')
const pi = args.indexOf('--peer')
const peer = pi !== -1 ? args[pi + 1] : null
if (!peer) throw new Error('set-auth requires --peer <url> (the peer daemon)')
const li = args.indexOf('--level')
const level = (li !== -1 && args[li + 1]) || 'writer'
if (!['owner', 'writer', 'reader'].includes(level)) {
throw new Error('--level must be owner, writer, or reader')
}
const org = await client.call('get-org', { name })
const resources = Array.isArray(org.resources) && org.resources.length
? org.resources
: Object.keys(org.resourceKeys || {})
if (!resources.length) throw new Error('org "' + name + '" has no resource repos')
const results = []
for (const repo of resources) {
try {
// Pull the peer's tracker local key + identity for this repo.
const pc = await PghPeerClient.connect({ url: peer })
let pk
try { pk = await pc.call('repo-local-key', { name: repo }) } finally { await pc.close() }
if (!pk || !pk.localKey) {
results.push({ repo, error: 'peer has no tracker local key (repo not mounted there?)' })
continue
}
const userId = pk.identity || pk.localKey
if (level === 'reader') {
await client.call('repo-revoke-writer', { name: repo, userId })
results.push({ repo, level, userId, revoked: true })
} else {
await client.call('repo-add-writer', { name: repo, userId, writerKey: pk.localKey, role: level })
results.push({ repo, level, userId, granted: true })
}
} catch (err) {
results.push({ repo, error: err.message })
}
}
return { org: name, peer, level, resources: results }
}
throw new Error('unknown org subcommand: ' + sub + '\n' + usage)
}