forked from heavy-duty/stoke
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`.
|
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
|
## Architecture
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|
|
||||||
43
src/api.js
43
src/api.js
|
|
@ -175,6 +175,49 @@ class ForgejoClient {
|
||||||
image: base64Image,
|
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 };
|
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) => {
|
program.parseAsync(process.argv).catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue