2026-06-01 00:59:22 +00:00
|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
|
|
|
|
import { Command } from 'commander';
|
|
|
|
|
import { AppState, Config } from './types.js';
|
|
|
|
|
import { TuiEngine } from './tui.js';
|
|
|
|
|
import { validateConnection, normalizeUrl } from './api.js';
|
2026-06-01 02:27:57 +00:00
|
|
|
import { loadSavedConfig, saveGlobalConfig } from './config.js';
|
2026-06-01 00:59:22 +00:00
|
|
|
import chalk from 'chalk';
|
|
|
|
|
|
|
|
|
|
const program = new Command();
|
|
|
|
|
|
|
|
|
|
program
|
|
|
|
|
.name('gitea-tui')
|
|
|
|
|
.description('A premium CLI TUI Dashboard for exploring Gitea and Forgejo issues')
|
|
|
|
|
.version('1.0.0')
|
|
|
|
|
.option('-u, --url <url>', 'Forgejo/Gitea instance base URL (e.g. https://forgejo.freshbrewed.science)')
|
2026-06-01 02:02:23 +00:00
|
|
|
.option('-i, --userid <userid>', 'Forgejo/Gitea user ID / username')
|
2026-06-01 00:59:22 +00:00
|
|
|
.option('-r, --repo <owner/repo>', 'Repository path (e.g. owner/repo)')
|
2026-06-01 02:27:57 +00:00
|
|
|
.option('-t, --token <token>', 'Personal Access Token (optional for public repositories)')
|
|
|
|
|
.option('-s, --save', 'Save the provided settings to the global config file (~/.config/fjtui/fjtui.json)');
|
2026-06-01 00:59:22 +00:00
|
|
|
|
|
|
|
|
program.parse(process.argv);
|
|
|
|
|
|
|
|
|
|
const options = program.opts();
|
|
|
|
|
|
|
|
|
|
async function bootstrap() {
|
2026-06-01 02:27:57 +00:00
|
|
|
const savedConfig = loadSavedConfig();
|
|
|
|
|
|
|
|
|
|
const url = options.url || savedConfig.url || '';
|
|
|
|
|
const userid = options.userid || savedConfig.userid || '';
|
|
|
|
|
const token = options.token || savedConfig.token || '';
|
|
|
|
|
const repo = options.repo || savedConfig.repo || '';
|
|
|
|
|
|
2026-06-01 00:59:22 +00:00
|
|
|
// Initialize default state
|
|
|
|
|
const state: AppState = {
|
|
|
|
|
screen: 'setup',
|
2026-06-03 13:34:31 +00:00
|
|
|
previousScreen: 'list',
|
|
|
|
|
focusedPane: 'list',
|
|
|
|
|
selectedSettingIndex: 0,
|
2026-06-03 18:38:41 +00:00
|
|
|
autoRefreshInterval: savedConfig.autoRefreshInterval || 0,
|
2026-06-01 00:59:22 +00:00
|
|
|
config: {
|
|
|
|
|
url: '',
|
|
|
|
|
token: null,
|
2026-06-01 02:02:23 +00:00
|
|
|
userid: '',
|
2026-06-01 00:59:22 +00:00
|
|
|
owner: '',
|
|
|
|
|
repo: '',
|
2026-06-03 18:38:41 +00:00
|
|
|
autoRefreshInterval: savedConfig.autoRefreshInterval || 0,
|
2026-06-01 00:59:22 +00:00
|
|
|
},
|
|
|
|
|
issues: [],
|
|
|
|
|
currentPage: 1,
|
|
|
|
|
issuesPerPage: 15, // matches standard screen sizes perfectly
|
|
|
|
|
totalIssuesCount: 0,
|
|
|
|
|
loading: false,
|
|
|
|
|
error: null,
|
|
|
|
|
selectedIssueIndex: 0,
|
|
|
|
|
searchQuery: '',
|
|
|
|
|
stateFilter: 'open',
|
|
|
|
|
typeFilter: 'all',
|
|
|
|
|
sortField: 'created',
|
|
|
|
|
sortOrder: 'desc',
|
|
|
|
|
selectedIssue: null,
|
|
|
|
|
selectedIssueComments: [],
|
|
|
|
|
commentsLoading: false,
|
|
|
|
|
detailScrollOffset: 0,
|
2026-06-02 18:41:33 +00:00
|
|
|
assigneesInput: '',
|
2026-06-01 02:02:23 +00:00
|
|
|
repos: [],
|
|
|
|
|
selectedRepoIndex: 0,
|
|
|
|
|
repoSearchQuery: '',
|
|
|
|
|
repoPickerActiveSearch: false,
|
2026-06-01 00:59:22 +00:00
|
|
|
setupForm: {
|
2026-06-01 02:27:57 +00:00
|
|
|
url: url || 'https://forgejo.freshbrewed.science',
|
|
|
|
|
userid: userid || '',
|
|
|
|
|
token: token || '',
|
|
|
|
|
saveConfig: true,
|
|
|
|
|
activeField: url ? (userid ? 'token' : 'userid') : 'url',
|
2026-06-01 00:59:22 +00:00
|
|
|
},
|
2026-06-01 03:06:53 +00:00
|
|
|
createIssueForm: {
|
|
|
|
|
title: '',
|
|
|
|
|
body: '',
|
|
|
|
|
activeField: 'title',
|
|
|
|
|
},
|
2026-06-01 03:18:12 +00:00
|
|
|
addCommentForm: {
|
|
|
|
|
body: '',
|
|
|
|
|
},
|
2026-06-01 23:15:01 +00:00
|
|
|
editIssueForm: {
|
|
|
|
|
title: '',
|
|
|
|
|
body: '',
|
|
|
|
|
activeField: 'title',
|
|
|
|
|
},
|
2026-06-01 23:34:20 +00:00
|
|
|
addTimeForm: {
|
|
|
|
|
timeInput: '',
|
|
|
|
|
},
|
2026-06-02 23:42:07 +00:00
|
|
|
labels: [],
|
|
|
|
|
selectedLabelIndex: 0,
|
|
|
|
|
labelsLoading: false,
|
|
|
|
|
labelForm: {
|
|
|
|
|
name: '',
|
|
|
|
|
color: '',
|
|
|
|
|
description: '',
|
|
|
|
|
exclusive: false,
|
|
|
|
|
activeField: 'name',
|
|
|
|
|
},
|
2026-06-01 00:59:22 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// If parameters are provided, try direct connection first
|
2026-06-01 02:27:57 +00:00
|
|
|
if (url && repo) {
|
|
|
|
|
const repoParts = repo.split('/');
|
2026-06-01 00:59:22 +00:00
|
|
|
if (repoParts.length === 2 && repoParts[0].trim() && repoParts[1].trim()) {
|
2026-06-01 02:27:57 +00:00
|
|
|
const normalized = normalizeUrl(url);
|
2026-06-01 00:59:22 +00:00
|
|
|
const config: Config = {
|
|
|
|
|
url: normalized,
|
2026-06-01 02:27:57 +00:00
|
|
|
token: token ? token.trim() : null,
|
|
|
|
|
userid: userid || repoParts[0].trim(),
|
2026-06-01 00:59:22 +00:00
|
|
|
owner: repoParts[0].trim(),
|
|
|
|
|
repo: repoParts[1].trim(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
console.log(chalk.cyan(`Connecting to Gitea/Forgejo instance at ${normalized}...`));
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await validateConnection(config);
|
|
|
|
|
// Valid connection! Go straight to list screen
|
|
|
|
|
state.config = config;
|
|
|
|
|
state.screen = 'list';
|
2026-06-01 02:27:57 +00:00
|
|
|
|
|
|
|
|
// Optionally save to global config if --save option is provided
|
|
|
|
|
if (options.save) {
|
|
|
|
|
saveGlobalConfig({
|
|
|
|
|
url: normalized,
|
|
|
|
|
userid: config.userid,
|
|
|
|
|
token: config.token,
|
|
|
|
|
repo: repo,
|
2026-06-03 18:38:41 +00:00
|
|
|
autoRefreshInterval: state.autoRefreshInterval,
|
2026-06-01 02:27:57 +00:00
|
|
|
});
|
|
|
|
|
console.log(chalk.green('Settings saved successfully to global config file!'));
|
|
|
|
|
}
|
2026-06-01 00:59:22 +00:00
|
|
|
} catch (err: any) {
|
|
|
|
|
// Validation failed, let's load setup form with the entered values and show error!
|
|
|
|
|
state.error = `Connection failed: ${err.message}`;
|
|
|
|
|
state.screen = 'setup';
|
|
|
|
|
state.setupForm = {
|
2026-06-01 02:27:57 +00:00
|
|
|
url: url,
|
|
|
|
|
userid: userid || '',
|
|
|
|
|
token: token || '',
|
|
|
|
|
saveConfig: true,
|
2026-06-01 00:59:22 +00:00
|
|
|
activeField: 'url',
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2026-06-01 02:27:57 +00:00
|
|
|
state.error = 'Invalid repository path. Must be in owner/repo format.';
|
2026-06-01 00:59:22 +00:00
|
|
|
state.screen = 'setup';
|
2026-06-01 02:27:57 +00:00
|
|
|
state.setupForm = {
|
|
|
|
|
url: url,
|
|
|
|
|
userid: userid || '',
|
|
|
|
|
token: token || '',
|
|
|
|
|
saveConfig: true,
|
|
|
|
|
activeField: 'url',
|
|
|
|
|
};
|
2026-06-01 00:59:22 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create and start the TUI engine
|
|
|
|
|
const engine = new TuiEngine(state);
|
|
|
|
|
|
|
|
|
|
engine.start(() => {
|
|
|
|
|
// Perform cleanup and exit cleanly
|
|
|
|
|
console.log(chalk.bold.green('\nThank you for using Forgejo TUI Issue Explorer! Goodbye.'));
|
|
|
|
|
process.exit(0);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bootstrap().catch((err) => {
|
|
|
|
|
console.error(chalk.bold.red('Fatal Error on Bootstrap:'), err);
|
|
|
|
|
process.exit(1);
|
|
|
|
|
});
|