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:
kimi-reviewer-andresmgsl 2026-07-22 17:56:14 +00:00
parent dadfca6093
commit 0a50b40798
3 changed files with 317 additions and 0 deletions

105
README.md
View file

@ -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

View file

@ -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 };

View file

@ -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);