Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions apps/cli/src/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ test('run prints usage with no command', async () => {
assert.match(output, /ghcrawl <command>/);
assert.match(output, /\n version\n/);
assert.match(output, /refresh <owner\/repo>/);
assert.match(output, /refresh-user <owner\/repo> --login <user>/);
assert.match(output, /refresh-users <owner\/repo>/);
assert.match(output, /threads <owner\/repo>/);
assert.match(output, /author <owner\/repo> --login <user>/);
assert.match(output, /close-thread <owner\/repo> --number <thread>/);
Expand All @@ -42,6 +44,8 @@ test('run prints usage for help flag', async () => {
assert.match(output, /ghcrawl <command>/);
assert.match(output, /\n version\n/);
assert.match(output, /refresh <owner\/repo>/);
assert.match(output, /refresh-user <owner\/repo> --login <user>/);
assert.match(output, /refresh-users <owner\/repo>/);
assert.match(output, /threads <owner\/repo>/);
assert.match(output, /author <owner\/repo> --login <user>/);
assert.match(output, /close-thread <owner\/repo> --number <thread>/);
Expand Down Expand Up @@ -162,6 +166,13 @@ test('parseRepoFlags accepts include-closed boolean flag', () => {
assert.equal(parsed.values['include-closed'], true);
});

test('parseRepoFlags accepts force boolean flag', () => {
const parsed = parseRepoFlags(['openclaw/openclaw', '--force']);
assert.equal(parsed.owner, 'openclaw');
assert.equal(parsed.repo, 'openclaw');
assert.equal(parsed.values.force, true);
});

test('resolveSinceValue keeps ISO timestamps', () => {
assert.equal(resolveSinceValue('2026-03-01T00:00:00Z'), '2026-03-01T00:00:00.000Z');
});
Expand Down
33 changes: 33 additions & 0 deletions apps/cli/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ type CommandName =
| 'version'
| 'sync'
| 'refresh'
| 'refresh-user'
| 'refresh-users'
| 'threads'
| 'author'
| 'close-thread'
Expand Down Expand Up @@ -44,6 +46,8 @@ function usage(devMode = false): string {
' version',
' sync <owner/repo> [--since <iso|duration>] [--limit <count>] [--include-comments] [--full-reconcile]',
' refresh <owner/repo> [--no-sync] [--no-embed] [--no-cluster]',
' refresh-user <owner/repo> --login <user> [--force]',
' refresh-users <owner/repo> [--mode flagged|trusted_prs] [--limit <count>] [--force]',
' threads <owner/repo> [--numbers <n,n,...>] [--kind issue|pull_request] [--include-closed]',
' author <owner/repo> --login <user> [--include-closed]',
' close-thread <owner/repo> --number <thread>',
Expand Down Expand Up @@ -119,6 +123,7 @@ export function parseRepoFlags(args: string[]): { owner: string; repo: string; v
'no-sync': { type: 'boolean' },
'no-embed': { type: 'boolean' },
'no-cluster': { type: 'boolean' },
force: { type: 'boolean' },
},
});

Expand Down Expand Up @@ -335,6 +340,34 @@ export async function run(argv: string[], stdout: NodeJS.WritableStream = proces
stdout.write(`${JSON.stringify(result, null, 2)}\n`);
return;
}
case 'refresh-user': {
const { owner, repo, values } = parseRepoFlags(rest);
if (typeof values.login !== 'string' || values.login.trim().length === 0) {
throw new Error('Missing --login');
}
const result = await getService().refreshRepoUser({
owner,
repo,
login: values.login,
force: values.force === true,
});
stdout.write(`${JSON.stringify(result, null, 2)}\n`);
return;
}
case 'refresh-users': {
const { owner, repo, values } = parseRepoFlags(rest);
const mode = values.mode === 'trusted_prs' ? 'trusted_prs' : 'flagged';
const result = await getService().refreshRepoUsers({
owner,
repo,
mode,
limit: typeof values.limit === 'string' ? parsePositiveInteger('limit', values.limit) : undefined,
force: values.force === true,
onProgress: writeProgress,
});
stdout.write(`${JSON.stringify(result, null, 2)}\n`);
return;
}
case 'threads': {
const { owner, repo, values } = parseRepoFlags(rest);
const kind = values.kind === 'issue' || values.kind === 'pull_request' ? values.kind : undefined;
Expand Down
39 changes: 39 additions & 0 deletions apps/cli/src/tui/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import assert from 'node:assert/strict';
import type { TuiClusterDetail, TuiRepoStats, TuiThreadDetail } from '@ghcrawl/api-core';

import {
buildFooterCommandHints,
buildRefreshCliArgs,
buildRefreshUserCliArgs,
buildRefreshUsersCliArgs,
buildHelpContent,
buildUpdatePipelineLabels,
describeUpdateTask,
Expand Down Expand Up @@ -184,6 +187,15 @@ test('buildUpdatePipelineLabels marks the selected tasks and includes task guida
test('buildHelpContent includes the full key command list', () => {
const content = buildHelpContent();

assert.match(content, /Slash Commands/);
assert.match(content, /\/clusters\s+switch to the current issue and PR cluster explorer/);
assert.match(content, /\/users\s+switch to the flagged contributor explorer/);
assert.match(content, /\/users flagged\s+show low-reputation, stale, or hidden-activity contributors/);
assert.match(content, /\/users trusted\s+show high-reputation contributors with open PRs/);
assert.match(content, /\/refresh\s+reload the current view, or open the user refresh menu on \/users/);
assert.match(content, /\/user-refresh\s+refresh the selected user profile and reputation signals/);
assert.match(content, /\/user-refresh-bulk\s+open the bulk user refresh menu for the current user mode/);
assert.match(content, /\/filter\s+open the cluster filter prompt/);
assert.match(content, /Tab \/ Shift-Tab/);
assert.match(content, /Left \/ Right\s+cycle focus backward or forward across panes/);
assert.match(content, /Up \/ Down\s+move selection, or scroll detail when detail is focused/);
Expand All @@ -195,10 +207,17 @@ test('buildHelpContent includes the full key command list', () => {
assert.match(content, /x\s+show or hide locally closed clusters and members/);
assert.match(content, /h or \?\s+open this help popup/);
assert.match(content, /q\s+quit the TUI/);
assert.doesNotMatch(content, /\/\s+filter clusters by title\/member text/);
assert.doesNotMatch(content, /j \/ k/);
assert.match(content, /This popup scrolls\./);
});

test('buildFooterCommandHints leads with slash commands for each screen', () => {
assert.match(buildFooterCommandHints('clusters')[0], /\/clusters \/users \/filter \/repos \/update \/help \/quit/);
assert.match(buildFooterCommandHints('users')[0], /\/users flagged \/users trusted \/refresh \/user-refresh-bulk \/user-open \/repos \/help \/quit/);
assert.match(buildFooterCommandHints('users')[1], /r refresh menu/);
});

test('buildRefreshCliArgs maps the staged selection to refresh skip flags', () => {
assert.deepEqual(buildRefreshCliArgs({ owner: 'openclaw', repo: 'openclaw' }, { sync: true, embed: true, cluster: true }), [
'refresh',
Expand All @@ -211,3 +230,23 @@ test('buildRefreshCliArgs maps the staged selection to refresh skip flags', () =
'--no-cluster',
]);
});

test('buildRefreshUsersCliArgs maps user refresh mode and limit', () => {
assert.deepEqual(buildRefreshUsersCliArgs({ owner: 'openclaw', repo: 'openclaw' }, { mode: 'flagged', limit: 25 }), [
'refresh-users',
'openclaw/openclaw',
'--mode',
'flagged',
'--limit',
'25',
]);
});

test('buildRefreshUserCliArgs maps a selected user refresh', () => {
assert.deepEqual(buildRefreshUserCliArgs({ owner: 'openclaw', repo: 'openclaw' }, { login: 'alice' }), [
'refresh-user',
'openclaw/openclaw',
'--login',
'alice',
]);
});
Loading