Add GitHub connection type — repo viewer with org grouping and auto-refresh
New protocol type 'github' for browsing repositories via the GitHub REST API. Creates a non-terminal panel showing repos grouped by owner/org, sorted by last commit date (newest first), with search filtering and auto-refresh. Backend: - ProtocolGitHub constant and GitHubConfig/GitHubRepo model structs - New internal/githubclient package (REST API, pagination, token validation) - Session manager connectGitHub() with periodic refresh goroutine - API surface: RefreshGitHubRepos() for on-demand refresh - Token stored via existing credential infrastructure (keychain/vault) Frontend: - GitHub tab in profile editor with API URL and token storage fields - New github.js viewer component (grouped repos, badges, language dots) - github.css with charcoal-themed repo cards and org headers - Protocol colour tokens, tab badge, sidebar indicator for GitHub - Event wiring for session:github:repos and session:github:error - Cleanup on tab close Supports GitHub Enterprise via configurable API base URL.
This commit is contained in:
+69
-10
@@ -52,6 +52,16 @@ const DEFAULTS = {
|
||||
payloadPresets: [],
|
||||
},
|
||||
},
|
||||
github: {
|
||||
port: 443,
|
||||
protocol: 'github',
|
||||
host: 'github.com',
|
||||
github: {
|
||||
baseURL: 'https://api.github.com',
|
||||
secretMode: 'saved',
|
||||
refreshIntervalSecs: 300,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
class ProfileEditor {
|
||||
@@ -119,11 +129,12 @@ class ProfileEditor {
|
||||
drawer.appendChild(header);
|
||||
|
||||
// ── Protocol selector tabs ──────────────────────────────
|
||||
const protocols = ['ssh', 'telnet', 'raw'];
|
||||
const protocols = ['ssh', 'telnet', 'raw', 'github'];
|
||||
const tabBar = createElement('div', 'drawer-protocol-tabs');
|
||||
for (const proto of protocols) {
|
||||
const label = proto === 'github' ? 'GitHub' : proto.toUpperCase();
|
||||
const btn = createElement('button', `drawer-proto-tab ${profile.protocol === proto ? 'active' : ''}`, {
|
||||
textContent: proto.toUpperCase(),
|
||||
textContent: label,
|
||||
dataset: { proto },
|
||||
});
|
||||
btn.addEventListener('click', () => this.handleProtocolChange(proto));
|
||||
@@ -148,10 +159,13 @@ class ProfileEditor {
|
||||
case 'raw':
|
||||
this._renderRAWSections(profile).forEach((s) => body.appendChild(s));
|
||||
break;
|
||||
case 'github':
|
||||
this._renderGitHubSections(profile).forEach((s) => body.appendChild(s));
|
||||
break;
|
||||
}
|
||||
|
||||
// Terminal section (SSH and Telnet only)
|
||||
if (profile.protocol !== 'raw') {
|
||||
if (profile.protocol !== 'raw' && profile.protocol !== 'github') {
|
||||
body.appendChild(this._renderTerminalSection(profile));
|
||||
}
|
||||
|
||||
@@ -237,14 +251,24 @@ class ProfileEditor {
|
||||
|
||||
/** @private */
|
||||
_renderGeneralSection(profile) {
|
||||
return this._renderSection('General', [
|
||||
const fields = [
|
||||
this._field('text', 'displayName', 'Display Name', profile.displayName, { placeholder: 'My Server' }),
|
||||
this._field('text', 'host', 'Host', profile.host, { placeholder: '192.168.1.1 or example.com', required: true }),
|
||||
this._field('number', 'port', 'Port', profile.port, { min: 1, max: 65535, required: true }),
|
||||
this._field('text', 'username', 'Username', profile.username, { placeholder: 'root' }),
|
||||
this._renderGroupField(profile),
|
||||
this._field('text', 'description', 'Description', profile.description, { placeholder: 'Optional description' }),
|
||||
]);
|
||||
this._field('text', 'host', 'Host', profile.host, {
|
||||
placeholder: profile.protocol === 'github' ? 'github.com' : '192.168.1.1 or example.com',
|
||||
required: true,
|
||||
}),
|
||||
];
|
||||
|
||||
// Port and username not relevant for GitHub connections
|
||||
if (profile.protocol !== 'github') {
|
||||
fields.push(this._field('number', 'port', 'Port', profile.port, { min: 1, max: 65535, required: true }));
|
||||
fields.push(this._field('text', 'username', 'Username', profile.username, { placeholder: 'root' }));
|
||||
}
|
||||
|
||||
fields.push(this._renderGroupField(profile));
|
||||
fields.push(this._field('text', 'description', 'Description', profile.description, { placeholder: 'Optional description' }));
|
||||
|
||||
return this._renderSection('General', fields);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -451,6 +475,33 @@ class ProfileEditor {
|
||||
return sections;
|
||||
}
|
||||
|
||||
/** @private */
|
||||
_renderGitHubSections(profile) {
|
||||
const sections = [];
|
||||
const gh = profile.github || DEFAULTS.github.github;
|
||||
|
||||
sections.push(this._renderSection('API', [
|
||||
this._field('text', 'github.baseURL', 'API Base URL', gh.baseURL, {
|
||||
placeholder: 'https://api.github.com',
|
||||
}),
|
||||
this._field('select', 'github.secretMode', 'Token Storage', gh.secretMode, {
|
||||
options: [
|
||||
{ value: 'saved', label: 'Save to keychain' },
|
||||
{ value: 'session-only', label: 'Remember for session' },
|
||||
{ value: 'ask-every-time', label: 'Ask every time' },
|
||||
],
|
||||
}),
|
||||
]));
|
||||
|
||||
sections.push(this._renderSection('Refresh', [
|
||||
this._field('number', 'github.refreshIntervalSecs', 'Refresh Interval (seconds)', gh.refreshIntervalSecs, {
|
||||
min: 60, max: 3600,
|
||||
}),
|
||||
]));
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
||||
/** @private */
|
||||
_renderTerminalSection(profile) {
|
||||
return this._renderSection('Terminal', [
|
||||
@@ -843,6 +894,14 @@ class ProfileEditor {
|
||||
}
|
||||
p.raw.payloadPresets = presets;
|
||||
}
|
||||
|
||||
// GitHub-specific
|
||||
if (p.protocol === 'github') {
|
||||
if (!p.github) p.github = {};
|
||||
p.github.baseURL = val('github.baseURL') || 'https://api.github.com';
|
||||
p.github.secretMode = val('github.secretMode') || 'saved';
|
||||
p.github.refreshIntervalSecs = val('github.refreshIntervalSecs') || 300;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Validation ───────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user