Add user and org team commands
New commands, one per Forgejo API call used to set up the heavy-duty
organization membership:
- stoke user list (GET /api/v1/users/search)
- stoke user show (GET /api/v1/users/{username})
- stoke org team list (GET /api/v1/orgs/{org}/teams)
- stoke org team create (POST /api/v1/orgs/{org}/teams)
- stoke org team member-list (GET /api/v1/teams/{id}/members)
- stoke org team member-add (PUT /api/v1/teams/{id}/members/{username})
- stoke org team member-remove (DELETE /api/v1/teams/{id}/members/{username})
All commands documented in the README.
This commit is contained in:
parent
dadfca6093
commit
0a50b40798
3 changed files with 317 additions and 0 deletions
105
README.md
105
README.md
|
|
@ -410,6 +410,111 @@ stoke org avatar -o heavy-duty -f heavy-duty-logo.png
|
|||
|
||||
Calls `POST /api/v1/orgs/{org}/avatar`.
|
||||
|
||||
### `stoke org team list`
|
||||
|
||||
List teams in an organization.
|
||||
|
||||
```bash
|
||||
stoke org team list -o heavy-duty
|
||||
```
|
||||
|
||||
Calls `GET /api/v1/orgs/{org}/teams` and auto-paginates.
|
||||
|
||||
### `stoke org team create`
|
||||
|
||||
Create a team in an organization.
|
||||
|
||||
```text
|
||||
Options:
|
||||
-o, --org <org> organization name (required)
|
||||
--name <name> team name (required)
|
||||
-d, --description <description> team description
|
||||
--permission <permission> read, write, admin (default: read)
|
||||
--all-repos grant access to all current and future org repositories
|
||||
--can-create-repo allow members to create repositories in the organization
|
||||
--units <units> comma-separated team units
|
||||
(default: repo.code,repo.issues,repo.pulls,repo.releases,repo.wiki,repo.projects)
|
||||
```
|
||||
|
||||
Example used to create the regular members team:
|
||||
|
||||
```bash
|
||||
stoke org team create -o heavy-duty --name members --permission write --all-repos
|
||||
```
|
||||
|
||||
Calls `POST /api/v1/orgs/{org}/teams`.
|
||||
|
||||
### `stoke org team member-list`
|
||||
|
||||
List members of a team. Requires being a member of that team (or an instance admin).
|
||||
|
||||
```bash
|
||||
stoke org team member-list --team-id 2
|
||||
```
|
||||
|
||||
Calls `GET /api/v1/teams/{id}/members` and auto-paginates.
|
||||
|
||||
### `stoke org team member-add`
|
||||
|
||||
Add a user to a team. Adding a user to any team also makes them a member of the organization; membership in the `Owners` team is what makes a user an organization admin.
|
||||
|
||||
```text
|
||||
Options:
|
||||
--team-id <id> team id, see `stoke org team list` (required)
|
||||
-u, --user <username> username to add (required)
|
||||
```
|
||||
|
||||
Example used to make Dan and Andres organization admins:
|
||||
|
||||
```bash
|
||||
stoke org team member-add --team-id 1 -u andres
|
||||
stoke org team member-add --team-id 1 -u dan
|
||||
```
|
||||
|
||||
Calls `PUT /api/v1/teams/{id}/members/{username}`.
|
||||
|
||||
### `stoke org team member-remove`
|
||||
|
||||
Remove a user from a team.
|
||||
|
||||
```text
|
||||
Options:
|
||||
--team-id <id> team id, see `stoke org team list` (required)
|
||||
-u, --user <username> username to remove (required)
|
||||
```
|
||||
|
||||
```bash
|
||||
stoke org team member-remove --team-id 1 -u kimi-reviewer-andresmgsl
|
||||
```
|
||||
|
||||
Calls `DELETE /api/v1/teams/{id}/members/{username}`.
|
||||
|
||||
### `stoke user list`
|
||||
|
||||
Search/list users on the Forgejo instance. Non-admin searches are visibility-limited: an empty query typically returns only the authenticated user.
|
||||
|
||||
```text
|
||||
Options:
|
||||
-q, --query <query> search query (default: empty)
|
||||
-l, --limit <number> maximum users to display (default: 50; use 0 for all)
|
||||
```
|
||||
|
||||
```bash
|
||||
stoke user list -q andres
|
||||
```
|
||||
|
||||
Calls `GET /api/v1/users/search` and auto-paginates.
|
||||
|
||||
### `stoke user show`
|
||||
|
||||
Show a single user profile. Useful to check whether a username exists.
|
||||
|
||||
```bash
|
||||
stoke user show -u andres
|
||||
```
|
||||
|
||||
Calls `GET /api/v1/users/{username}`.
|
||||
|
||||
## Architecture
|
||||
|
||||
```text
|
||||
|
|
|
|||
43
src/api.js
43
src/api.js
|
|
@ -175,6 +175,49 @@ class ForgejoClient {
|
|||
image: base64Image,
|
||||
});
|
||||
}
|
||||
|
||||
async searchUsers(query = '', opts = {}) {
|
||||
const pageSize = 50;
|
||||
const all = [];
|
||||
for (let page = 1; page <= 1000; page += 1) {
|
||||
const queryString = new URLSearchParams({
|
||||
q: query,
|
||||
...opts,
|
||||
limit: String(pageSize),
|
||||
page: String(page),
|
||||
}).toString();
|
||||
const res = await this.get(`/users/search?${queryString}`);
|
||||
const items = Array.isArray(res?.data) ? res.data : [];
|
||||
if (items.length === 0) break;
|
||||
all.push(...items);
|
||||
if (items.length < pageSize) break;
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
async listOrgTeams(org, opts = {}) {
|
||||
return this.getAll(`/orgs/${encodeURIComponent(org)}/teams`, opts);
|
||||
}
|
||||
|
||||
async createTeam(org, payload) {
|
||||
return this.post(`/orgs/${encodeURIComponent(org)}/teams`, payload);
|
||||
}
|
||||
|
||||
async addTeamMember(teamId, username) {
|
||||
return this.request('PUT', `/teams/${teamId}/members/${encodeURIComponent(username)}`);
|
||||
}
|
||||
|
||||
async listTeamMembers(teamId, opts = {}) {
|
||||
return this.getAll(`/teams/${teamId}/members`, opts);
|
||||
}
|
||||
|
||||
async removeTeamMember(teamId, username) {
|
||||
return this.del(`/teams/${teamId}/members/${encodeURIComponent(username)}`);
|
||||
}
|
||||
|
||||
async getUser(username) {
|
||||
return this.get(`/users/${encodeURIComponent(username)}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ForgejoClient };
|
||||
|
|
|
|||
169
src/cli.js
169
src/cli.js
|
|
@ -682,6 +682,175 @@ org
|
|||
}
|
||||
});
|
||||
|
||||
const team = org
|
||||
.command('team')
|
||||
.description('Manage organization teams');
|
||||
|
||||
team
|
||||
.command('list')
|
||||
.description('List teams in an organization')
|
||||
.requiredOption('-o, --org <org>', 'organization name')
|
||||
.action(async (options) => {
|
||||
try {
|
||||
const config = loadConfig();
|
||||
const client = ForgejoClient.fromConfig(config);
|
||||
const teams = await client.listOrgTeams(options.org);
|
||||
if (!teams.length) {
|
||||
console.log('No teams found.');
|
||||
return;
|
||||
}
|
||||
for (const t of teams) {
|
||||
console.log(`#${t.id} ${t.name} [${t.permission}]`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to list teams: ${err.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
team
|
||||
.command('create')
|
||||
.description('Create a team in an organization')
|
||||
.requiredOption('-o, --org <org>', 'organization name')
|
||||
.requiredOption('--name <name>', 'team name')
|
||||
.option('-d, --description <description>', 'team description', '')
|
||||
.option('--permission <permission>', 'permission level: read, write, admin', 'read')
|
||||
.option('--all-repos', 'grant access to all current and future organization repositories', false)
|
||||
.option('--can-create-repo', 'allow members to create repositories in the organization', false)
|
||||
.option('--units <units>', 'comma-separated team units', 'repo.code,repo.issues,repo.pulls,repo.releases,repo.wiki,repo.projects')
|
||||
.action(async (options) => {
|
||||
try {
|
||||
const config = loadConfig();
|
||||
const client = ForgejoClient.fromConfig(config);
|
||||
const payload = {
|
||||
name: options.name,
|
||||
description: options.description,
|
||||
permission: options.permission,
|
||||
includes_all_repositories: options.allRepos,
|
||||
can_create_org_repo: options.canCreateRepo,
|
||||
units: options.units.split(',').map((u) => u.trim()).filter(Boolean),
|
||||
};
|
||||
const result = await client.createTeam(options.org, payload);
|
||||
console.log(`Team created: #${result.id} ${result.name} [${result.permission}]`);
|
||||
} catch (err) {
|
||||
console.error(`Team creation failed: ${err.message}`);
|
||||
if (err.status) console.error(`HTTP status: ${err.status}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
team
|
||||
.command('member-list')
|
||||
.description('List members of a team')
|
||||
.requiredOption('--team-id <id>', 'team id (see `stoke org team list`)')
|
||||
.action(async (options) => {
|
||||
try {
|
||||
const config = loadConfig();
|
||||
const client = ForgejoClient.fromConfig(config);
|
||||
const members = await client.listTeamMembers(Number(options.teamId));
|
||||
if (!members.length) {
|
||||
console.log('No members found.');
|
||||
return;
|
||||
}
|
||||
for (const m of members) {
|
||||
const name = m.full_name ? ` (${m.full_name})` : '';
|
||||
console.log(`${m.login}${name}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to list team members: ${err.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
team
|
||||
.command('member-add')
|
||||
.description('Add a user to a team')
|
||||
.requiredOption('--team-id <id>', 'team id (see `stoke org team list`)')
|
||||
.requiredOption('-u, --user <username>', 'username to add')
|
||||
.action(async (options) => {
|
||||
try {
|
||||
const config = loadConfig();
|
||||
const client = ForgejoClient.fromConfig(config);
|
||||
await client.addTeamMember(Number(options.teamId), options.user);
|
||||
console.log(`Added ${options.user} to team #${options.teamId}.`);
|
||||
} catch (err) {
|
||||
console.error(`Failed to add team member: ${err.message}`);
|
||||
if (err.status) console.error(`HTTP status: ${err.status}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
team
|
||||
.command('member-remove')
|
||||
.description('Remove a user from a team')
|
||||
.requiredOption('--team-id <id>', 'team id (see `stoke org team list`)')
|
||||
.requiredOption('-u, --user <username>', 'username to remove')
|
||||
.action(async (options) => {
|
||||
try {
|
||||
const config = loadConfig();
|
||||
const client = ForgejoClient.fromConfig(config);
|
||||
await client.removeTeamMember(Number(options.teamId), options.user);
|
||||
console.log(`Removed ${options.user} from team #${options.teamId}.`);
|
||||
} catch (err) {
|
||||
console.error(`Failed to remove team member: ${err.message}`);
|
||||
if (err.status) console.error(`HTTP status: ${err.status}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
const user = program
|
||||
.command('user')
|
||||
.description('Manage users');
|
||||
|
||||
user
|
||||
.command('list')
|
||||
.description('Search/list users on the Forgejo instance')
|
||||
.option('-q, --query <query>', 'search query (empty lists all visible users)', '')
|
||||
.option('-l, --limit <number>', 'maximum users to return', '50')
|
||||
.action(async (options) => {
|
||||
try {
|
||||
const config = loadConfig();
|
||||
const client = ForgejoClient.fromConfig(config);
|
||||
const users = await client.searchUsers(options.query);
|
||||
const limit = Number(options.limit);
|
||||
const display = limit > 0 ? users.slice(0, limit) : users;
|
||||
if (!display.length) {
|
||||
console.log('No users found.');
|
||||
return;
|
||||
}
|
||||
for (const u of display) {
|
||||
const name = u.full_name ? ` (${u.full_name})` : '';
|
||||
console.log(`${u.login}${name}`);
|
||||
}
|
||||
if (users.length > display.length) {
|
||||
console.log(`...and ${users.length - display.length} more (use -l 0 for all).`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to list users: ${err.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
user
|
||||
.command('show')
|
||||
.description('Show a single user profile')
|
||||
.requiredOption('-u, --user <username>', 'username to look up')
|
||||
.action(async (options) => {
|
||||
try {
|
||||
const config = loadConfig();
|
||||
const client = ForgejoClient.fromConfig(config);
|
||||
const u = await client.getUser(options.user);
|
||||
console.log(`Login: ${u.login}`);
|
||||
console.log(`Full name: ${u.full_name || '-'}`);
|
||||
console.log(`Email: ${u.email || '-'}`);
|
||||
console.log(`URL: ${u.html_url}`);
|
||||
} catch (err) {
|
||||
console.error(`Failed to show user: ${err.message}`);
|
||||
if (err.status) console.error(`HTTP status: ${err.status}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
program.parseAsync(process.argv).catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
|
|
|
|||
Loading…
Reference in a new issue