From 46c487cf1bf0c064ddd8bd154902cd19e2b6ed2c Mon Sep 17 00:00:00 2001 From: rob Date: Fri, 9 Jan 2026 22:26:00 -0400 Subject: [PATCH] Add setup wizard and configurable git hosting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - First-run setup wizard prompts for projects dir and git hosting - Git Hosting section in Settings dialog (provider, URL, owner, token) - Supports GitHub, GitLab, and Gitea - new-project script reads from ~/.config/development-hub/settings.json - Environment variables still work as fallback (GITEA_URL, GITEA_TOKEN, etc.) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- bin/new-project | 169 +++++++++++++++++-------- src/development_hub/app.py | 9 ++ src/development_hub/dialogs.py | 217 ++++++++++++++++++++++++++++++++ src/development_hub/settings.py | 53 ++++++++ 4 files changed, 396 insertions(+), 52 deletions(-) diff --git a/bin/new-project b/bin/new-project index fa08c95..b79df75 100755 --- a/bin/new-project +++ b/bin/new-project @@ -18,15 +18,38 @@ set -e # Configuration SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" HUB_ROOT="$(dirname "$SCRIPT_DIR")" -PROJECTS_ROOT="$HOME/PycharmProjects" -PROJECT_DOCS_ROOT="$PROJECTS_ROOT/project-docs" -TEMPLATES_DIR="$HUB_ROOT/templates" CONFIG_DIR="$HOME/.config/development-hub" +SETTINGS_FILE="$CONFIG_DIR/settings.json" TOKEN_FILE="$CONFIG_DIR/gitea-token" +TEMPLATES_DIR="$HUB_ROOT/templates" -GITEA_URL="https://gitea.brrd.tech" -GITEA_CLONE_URL="https://gitea.brrd.tech" # Use HTTPS for cloning (SSH port 222 often unavailable) -GITEA_OWNER="rob" +# Load settings from JSON file if it exists +load_settings() { + if [[ -f "$SETTINGS_FILE" ]]; then + # Use python to parse JSON (more portable than jq) + PROJECTS_ROOT=$(python3 -c "import json; d=json.load(open('$SETTINGS_FILE')); print(d.get('default_project_path', '$HOME/PycharmProjects'))" 2>/dev/null || echo "$HOME/PycharmProjects") + GIT_HOST_TYPE=$(python3 -c "import json; d=json.load(open('$SETTINGS_FILE')); print(d.get('git_host_type', ''))" 2>/dev/null || echo "") + GIT_HOST_URL=$(python3 -c "import json; d=json.load(open('$SETTINGS_FILE')); print(d.get('git_host_url', ''))" 2>/dev/null || echo "") + GIT_HOST_OWNER=$(python3 -c "import json; d=json.load(open('$SETTINGS_FILE')); print(d.get('git_host_owner', ''))" 2>/dev/null || echo "") + GIT_HOST_TOKEN=$(python3 -c "import json; d=json.load(open('$SETTINGS_FILE')); print(d.get('git_host_token', ''))" 2>/dev/null || echo "") + else + PROJECTS_ROOT="${PROJECTS_ROOT:-$HOME/PycharmProjects}" + GIT_HOST_TYPE="" + GIT_HOST_URL="" + GIT_HOST_OWNER="" + GIT_HOST_TOKEN="" + fi + + # Allow environment variables to override + PROJECTS_ROOT="${PROJECTS_ROOT_OVERRIDE:-$PROJECTS_ROOT}" + GIT_HOST_URL="${GITEA_URL:-$GIT_HOST_URL}" + GIT_HOST_OWNER="${GITEA_OWNER:-$GIT_HOST_OWNER}" + GIT_HOST_TOKEN="${GITEA_TOKEN:-$GIT_HOST_TOKEN}" + + PROJECT_DOCS_ROOT="$PROJECTS_ROOT/project-docs" +} + +load_settings # Colors RED='\033[0;31m' @@ -176,75 +199,121 @@ prompt_for_missing() { fi } -load_gitea_token() { - # Check environment variable first - if [[ -n "$GITEA_TOKEN" ]]; then - log_info "Using GITEA_TOKEN from environment" +load_git_token() { + # Already loaded from settings? + if [[ -n "$GIT_HOST_TOKEN" ]]; then + log_info "Using token from settings" return 0 fi - # Check token file + # Check legacy token file if [[ -f "$TOKEN_FILE" ]]; then - GITEA_TOKEN=$(cat "$TOKEN_FILE") - log_info "Loaded Gitea token from $TOKEN_FILE" + GIT_HOST_TOKEN=$(cat "$TOKEN_FILE") + log_info "Loaded token from $TOKEN_FILE" return 0 fi + # Check if git hosting is configured + if [[ -z "$GIT_HOST_URL" ]] || [[ -z "$GIT_HOST_OWNER" ]]; then + log_error "Git hosting not configured" + echo "" + echo "Please configure git hosting in Development Hub:" + echo " 1. Run 'development-hub'" + echo " 2. Go to Settings" + echo " 3. Configure Git Hosting section" + echo "" + log_info "Or use --skip-gitea to skip repository creation" + exit 1 + fi + # Prompt user to create token - log_warn "No Gitea API token found" + log_warn "No API token found" echo "" echo "To create a token:" - echo " 1. Go to $GITEA_URL/user/settings/applications" - echo " 2. Generate a new token with 'repo' scope" + if [[ "$GIT_HOST_TYPE" == "github" ]]; then + echo " 1. Go to https://github.com/settings/tokens" + echo " 2. Generate a new token with 'repo' scope" + elif [[ "$GIT_HOST_TYPE" == "gitlab" ]]; then + echo " 1. Go to $GIT_HOST_URL/-/profile/personal_access_tokens" + echo " 2. Generate a new token with 'api' scope" + else + echo " 1. Go to $GIT_HOST_URL/user/settings/applications" + echo " 2. Generate a new token with 'repo' scope" + fi echo " 3. Copy the token" echo "" - read -rsp "Paste your Gitea API token: " GITEA_TOKEN + read -rsp "Paste your API token: " GIT_HOST_TOKEN echo "" - if [[ -z "$GITEA_TOKEN" ]]; then - log_error "Token is required for Gitea API access" + if [[ -z "$GIT_HOST_TOKEN" ]]; then + log_error "Token is required for API access" log_info "Use --skip-gitea to skip repository creation" exit 1 fi # Save token for future use mkdir -p "$CONFIG_DIR" - echo "$GITEA_TOKEN" > "$TOKEN_FILE" + echo "$GIT_HOST_TOKEN" > "$TOKEN_FILE" chmod 600 "$TOKEN_FILE" log_success "Token saved to $TOKEN_FILE" } -create_gitea_repo() { +create_git_repo() { local name="$1" local description="$2" - log_step "Creating Gitea repository: $name" + log_step "Creating repository: $name on $GIT_HOST_TYPE" if [[ "$DRY_RUN" == true ]]; then - log_info "[DRY RUN] Would create repo: $GITEA_URL/$GITEA_OWNER/$name" + log_info "[DRY RUN] Would create repo: $GIT_HOST_URL/$GIT_HOST_OWNER/$name" return 0 fi - local response - response=$(curl -s -w "\n%{http_code}" -X POST "$GITEA_URL/api/v1/user/repos" \ - -H "Authorization: token $GITEA_TOKEN" \ - -H "Content-Type: application/json" \ - -d "{ - \"name\": \"$name\", - \"description\": \"$description\", - \"private\": false, - \"auto_init\": false - }") + local response http_code body + + if [[ "$GIT_HOST_TYPE" == "github" ]]; then + # GitHub API + response=$(curl -s -w "\n%{http_code}" -X POST "https://api.github.com/user/repos" \ + -H "Authorization: token $GIT_HOST_TOKEN" \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Content-Type: application/json" \ + -d "{ + \"name\": \"$name\", + \"description\": \"$description\", + \"private\": false, + \"auto_init\": false + }") + elif [[ "$GIT_HOST_TYPE" == "gitlab" ]]; then + # GitLab API + response=$(curl -s -w "\n%{http_code}" -X POST "$GIT_HOST_URL/api/v4/projects" \ + -H "PRIVATE-TOKEN: $GIT_HOST_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"name\": \"$name\", + \"description\": \"$description\", + \"visibility\": \"public\", + \"initialize_with_readme\": false + }") + else + # Gitea API (default) + response=$(curl -s -w "\n%{http_code}" -X POST "$GIT_HOST_URL/api/v1/user/repos" \ + -H "Authorization: token $GIT_HOST_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"name\": \"$name\", + \"description\": \"$description\", + \"private\": false, + \"auto_init\": false + }") + fi - local http_code http_code=$(echo "$response" | tail -n1) - local body body=$(echo "$response" | sed '$d') if [[ "$http_code" == "201" ]]; then - log_success "Created repository: $GITEA_URL/$GITEA_OWNER/$name" - elif [[ "$http_code" == "409" ]]; then - log_warn "Repository already exists on Gitea" + log_success "Created repository: $GIT_HOST_URL/$GIT_HOST_OWNER/$name" + elif [[ "$http_code" == "409" ]] || [[ "$http_code" == "422" ]]; then + log_warn "Repository already exists" else log_error "Failed to create repository (HTTP $http_code)" echo "$body" | head -5 @@ -267,7 +336,7 @@ create_local_project() { cd "$project_dir" git init --quiet - git remote add origin "$GITEA_CLONE_URL/$GITEA_OWNER/$name.git" + git remote add origin "$GIT_HOST_URL/$GIT_HOST_OWNER/$name.git" log_success "Created local project with git" } @@ -289,8 +358,8 @@ apply_template() { -e "s|{{PROJECT_TAGLINE}}|$tagline|g" \ -e "s|{{YEAR}}|$year|g" \ -e "s|{{DATE}}|$date|g" \ - -e "s|{{GITEA_URL}}|$GITEA_URL|g" \ - -e "s|{{GITEA_OWNER}}|$GITEA_OWNER|g" \ + -e "s|{{GITEA_URL}}|$GIT_HOST_URL|g" \ + -e "s|{{GITEA_OWNER}}|$GIT_HOST_OWNER|g" \ "$template" > "$output" } @@ -390,12 +459,12 @@ update_build_script() { if [[ "$DRY_RUN" == true ]]; then log_info "[DRY RUN] Would add to PROJECT_CONFIG:" - log_info " PROJECT_CONFIG[\"$name\"]=\"$title|$tagline|$GITEA_OWNER|$name|$name\"" + log_info " PROJECT_CONFIG[\"$name\"]=\"$title|$tagline|$GIT_HOST_OWNER|$name|$name\"" return 0 fi # Find the last PROJECT_CONFIG line and add after it - local config_line="PROJECT_CONFIG[\"$name\"]=\"$title|$tagline|$GITEA_OWNER|$name|$name\"" + local config_line="PROJECT_CONFIG[\"$name\"]=\"$title|$tagline|$GIT_HOST_OWNER|$name|$name\"" # Check if already exists if grep -q "PROJECT_CONFIG\[\"$name\"\]" "$build_script"; then @@ -468,7 +537,7 @@ print_summary() { echo "" echo "Locations:" echo " Local: $PROJECTS_ROOT/$name/" - echo " Gitea: $GITEA_URL/$GITEA_OWNER/$name" + echo " Remote: $GIT_HOST_URL/$GIT_HOST_OWNER/$name" echo " Docs: $PROJECT_DOCS_ROOT/docs/projects/$name/" echo "" echo "Next steps:" @@ -476,14 +545,10 @@ print_summary() { echo " 2. Start developing!" echo "" if [[ "$deployed" == true ]]; then - echo "Public docs are live at:" - echo " https://pages.brrd.tech/$GITEA_OWNER/$name/" + echo "Public docs deployed." else echo "To publish documentation:" echo " $PROJECT_DOCS_ROOT/scripts/build-public-docs.sh $name --deploy" - echo "" - echo "Public docs will be available at:" - echo " https://pages.brrd.tech/$GITEA_OWNER/$name/" fi echo "" } @@ -504,10 +569,10 @@ main() { echo "" fi - # Load Gitea token (unless skipping) + # Load token and create repo (unless skipping) if [[ "$SKIP_GITEA" != true ]]; then - load_gitea_token - create_gitea_repo "$PROJECT_NAME" "$PROJECT_TAGLINE" + load_git_token + create_git_repo "$PROJECT_NAME" "$PROJECT_TAGLINE" fi create_local_project "$PROJECT_NAME" diff --git a/src/development_hub/app.py b/src/development_hub/app.py index 8972b2a..79747a5 100644 --- a/src/development_hub/app.py +++ b/src/development_hub/app.py @@ -7,6 +7,8 @@ from PySide6.QtCore import QTimer from PySide6.QtWidgets import QApplication from development_hub.main_window import MainWindow +from development_hub.settings import Settings +from development_hub.dialogs import SetupWizardDialog from development_hub.styles import DARK_THEME @@ -28,6 +30,13 @@ def main() -> int: Exit code (0 for success). """ app = DevelopmentHubApp(sys.argv) + + # Check for first-run setup + settings = Settings() + if not settings.get("setup_completed", False): + wizard = SetupWizardDialog() + wizard.exec() + window = MainWindow() # Handle Ctrl+C gracefully diff --git a/src/development_hub/dialogs.py b/src/development_hub/dialogs.py index b7ef797..1fd37de 100644 --- a/src/development_hub/dialogs.py +++ b/src/development_hub/dialogs.py @@ -576,6 +576,45 @@ class SettingsDialog(QDialog): layout.addWidget(docs_group) + # Git hosting settings + git_group = QGroupBox("Git Hosting") + git_layout = QFormLayout(git_group) + + self.git_type_combo = QComboBox() + self.git_type_combo.addItem("Not configured", "") + current_type = self.settings.git_host_type + current_type_index = 0 + for i, (value, label) in enumerate(Settings.GIT_HOST_CHOICES): + self.git_type_combo.addItem(label, value) + if value == current_type: + current_type_index = i + 1 + self.git_type_combo.setCurrentIndex(current_type_index) + self.git_type_combo.currentIndexChanged.connect(self._on_git_type_changed) + git_layout.addRow("Provider:", self.git_type_combo) + + self.git_url_edit = QLineEdit() + self.git_url_edit.setText(self.settings.git_host_url) + self.git_url_edit.setPlaceholderText("https://github.com or https://gitea.example.com") + git_layout.addRow("URL:", self.git_url_edit) + + self.git_owner_edit = QLineEdit() + self.git_owner_edit.setText(self.settings.git_host_owner) + self.git_owner_edit.setPlaceholderText("username or organization") + git_layout.addRow("Owner:", self.git_owner_edit) + + self.git_token_edit = QLineEdit() + self.git_token_edit.setText(self.settings.git_host_token) + self.git_token_edit.setEchoMode(QLineEdit.EchoMode.Password) + self.git_token_edit.setPlaceholderText("API token with repo scope") + git_layout.addRow("Token:", self.git_token_edit) + + token_help = QLabel('How to create a token') + token_help.setOpenExternalLinks(False) + token_help.linkActivated.connect(self._show_token_help) + git_layout.addRow("", token_help) + + layout.addWidget(git_group) + layout.addStretch() # Buttons @@ -614,6 +653,39 @@ class SettingsDialog(QDialog): row = self.paths_list.row(current) self.paths_list.takeItem(row) + def _on_git_type_changed(self, index: int): + """Update URL placeholder based on git type.""" + git_type = self.git_type_combo.currentData() + if git_type == "github": + self.git_url_edit.setPlaceholderText("https://github.com") + if not self.git_url_edit.text(): + self.git_url_edit.setText("https://github.com") + elif git_type == "gitlab": + self.git_url_edit.setPlaceholderText("https://gitlab.com or self-hosted URL") + if not self.git_url_edit.text(): + self.git_url_edit.setText("https://gitlab.com") + elif git_type == "gitea": + self.git_url_edit.setPlaceholderText("https://gitea.example.com") + + def _show_token_help(self): + """Show help dialog for creating tokens.""" + git_type = self.git_type_combo.currentData() + if git_type == "github": + url = "https://github.com/settings/tokens" + msg = "Go to GitHub Settings → Developer settings → Personal access tokens\n\nCreate a token with 'repo' scope." + elif git_type == "gitlab": + url = "https://gitlab.com/-/profile/personal_access_tokens" + msg = "Go to GitLab Settings → Access Tokens\n\nCreate a token with 'api' scope." + elif git_type == "gitea": + base_url = self.git_url_edit.text() or "https://your-gitea-instance" + url = f"{base_url}/user/settings/applications" + msg = f"Go to {url}\n\nGenerate a new token with 'repo' scope." + else: + msg = "Select a git provider first." + url = None + + QMessageBox.information(self, "Creating an API Token", msg) + def _save(self): """Save settings and close.""" # Save search paths @@ -626,6 +698,151 @@ class SettingsDialog(QDialog): self.settings.deploy_docs_after_creation = self.deploy_checkbox.isChecked() self.settings.preferred_editor = self.editor_combo.currentData() self.settings.auto_start_docs_server = self.auto_docs_server_checkbox.isChecked() + + # Save git hosting settings + self.settings.git_host_type = self.git_type_combo.currentData() + self.settings.git_host_url = self.git_url_edit.text().rstrip("/") + self.settings.git_host_owner = self.git_owner_edit.text() + self.settings.git_host_token = self.git_token_edit.text() + + self.accept() + + +class SetupWizardDialog(QDialog): + """First-run setup wizard for configuring Development Hub.""" + + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Welcome to Development Hub") + self.setMinimumWidth(500) + self.settings = Settings() + self._setup_ui() + + def _setup_ui(self): + """Set up the wizard UI.""" + layout = QVBoxLayout(self) + + # Welcome message + welcome = QLabel( + "

Welcome to Development Hub!

" + "

Let's set up a few things to get you started.

" + ) + welcome.setWordWrap(True) + layout.addWidget(welcome) + + # Projects directory + projects_group = QGroupBox("Projects Directory") + projects_layout = QHBoxLayout(projects_group) + + self.projects_dir_edit = QLineEdit() + self.projects_dir_edit.setText(str(self.settings.default_project_path)) + projects_layout.addWidget(self.projects_dir_edit) + + browse_btn = QPushButton("Browse...") + browse_btn.clicked.connect(self._browse_projects_dir) + projects_layout.addWidget(browse_btn) + + layout.addWidget(projects_group) + + # Git hosting settings + git_group = QGroupBox("Git Hosting (for creating new projects)") + git_layout = QFormLayout(git_group) + + self.git_type_combo = QComboBox() + for value, label in Settings.GIT_HOST_CHOICES: + self.git_type_combo.addItem(label, value) + self.git_type_combo.currentIndexChanged.connect(self._on_git_type_changed) + git_layout.addRow("Provider:", self.git_type_combo) + + self.git_url_edit = QLineEdit() + self.git_url_edit.setPlaceholderText("https://github.com") + git_layout.addRow("URL:", self.git_url_edit) + + self.git_owner_edit = QLineEdit() + self.git_owner_edit.setPlaceholderText("your username or organization") + git_layout.addRow("Owner:", self.git_owner_edit) + + self.git_token_edit = QLineEdit() + self.git_token_edit.setEchoMode(QLineEdit.EchoMode.Password) + self.git_token_edit.setPlaceholderText("API token (optional - needed for creating repos)") + git_layout.addRow("Token:", self.git_token_edit) + + skip_note = QLabel( + "You can skip git setup and configure it later in Settings." + ) + skip_note.setStyleSheet("color: #888;") + git_layout.addRow("", skip_note) + + layout.addWidget(git_group) + + layout.addStretch() + + # Buttons + button_layout = QHBoxLayout() + + skip_btn = QPushButton("Skip Setup") + skip_btn.clicked.connect(self._skip) + button_layout.addWidget(skip_btn) + + button_layout.addStretch() + + finish_btn = QPushButton("Finish Setup") + finish_btn.setDefault(True) + finish_btn.clicked.connect(self._finish) + button_layout.addWidget(finish_btn) + + layout.addLayout(button_layout) + + # Set initial URL based on default selection + self._on_git_type_changed(0) + + def _browse_projects_dir(self): + """Browse for projects directory.""" + path = QFileDialog.getExistingDirectory( + self, + "Select Projects Directory", + self.projects_dir_edit.text() or str(Path.home()), + ) + if path: + self.projects_dir_edit.setText(path) + + def _on_git_type_changed(self, index: int): + """Update URL based on git type.""" + git_type = self.git_type_combo.currentData() + if git_type == "github": + self.git_url_edit.setText("https://github.com") + elif git_type == "gitlab": + self.git_url_edit.setText("https://gitlab.com") + elif git_type == "gitea": + self.git_url_edit.setText("") + self.git_url_edit.setPlaceholderText("https://gitea.example.com") + + def _skip(self): + """Skip setup and just set first_run flag.""" + self.settings.set("setup_completed", True) + self.reject() + + def _finish(self): + """Save settings and finish setup.""" + # Save projects directory + projects_dir = self.projects_dir_edit.text() + if projects_dir: + self.settings.default_project_path = Path(projects_dir) + self.settings.project_search_paths = [projects_dir] + + # Save git settings if provided + git_type = self.git_type_combo.currentData() + git_url = self.git_url_edit.text().rstrip("/") + git_owner = self.git_owner_edit.text() + git_token = self.git_token_edit.text() + + if git_type and git_url and git_owner: + self.settings.git_host_type = git_type + self.settings.git_host_url = git_url + self.settings.git_host_owner = git_owner + self.settings.git_host_token = git_token + + self.settings.set("setup_completed", True) self.accept() diff --git a/src/development_hub/settings.py b/src/development_hub/settings.py index 056b8f2..ffb5efa 100644 --- a/src/development_hub/settings.py +++ b/src/development_hub/settings.py @@ -19,6 +19,11 @@ class Settings: "project_search_paths": [str(Path.home() / "PycharmProjects")], "project_ignore_folders": ["trash", "project-docs", ".cache", "__pycache__", "node_modules"], "auto_start_docs_server": True, + # Git hosting settings + "git_host_type": "", # "gitea", "github", "gitlab", "" + "git_host_url": "", # e.g., "https://gitea.example.com" or "https://github.com" + "git_host_owner": "", # username or organization + "git_host_token": "", # API token (stored in settings, not ideal but simple) } # Available editor choices with display names @@ -31,6 +36,13 @@ class Settings: ("subl", "Sublime Text"), ] + # Git hosting provider choices + GIT_HOST_CHOICES = [ + ("gitea", "Gitea"), + ("github", "GitHub"), + ("gitlab", "GitLab"), + ] + def __new__(cls): """Singleton pattern.""" if cls._instance is None: @@ -120,6 +132,47 @@ class Settings: def auto_start_docs_server(self, value: bool): self.set("auto_start_docs_server", value) + @property + def git_host_type(self) -> str: + """Git hosting type (gitea, github, gitlab).""" + return self.get("git_host_type", "") + + @git_host_type.setter + def git_host_type(self, value: str): + self.set("git_host_type", value) + + @property + def git_host_url(self) -> str: + """Git host URL.""" + return self.get("git_host_url", "") + + @git_host_url.setter + def git_host_url(self, value: str): + self.set("git_host_url", value) + + @property + def git_host_owner(self) -> str: + """Git host username or organization.""" + return self.get("git_host_owner", "") + + @git_host_owner.setter + def git_host_owner(self, value: str): + self.set("git_host_owner", value) + + @property + def git_host_token(self) -> str: + """Git host API token.""" + return self.get("git_host_token", "") + + @git_host_token.setter + def git_host_token(self, value: str): + self.set("git_host_token", value) + + @property + def is_git_configured(self) -> bool: + """Check if git hosting is configured.""" + return bool(self.git_host_type and self.git_host_url and self.git_host_owner) + def save_session(self, state: dict): """Save session state to file.""" self._session_file.parent.mkdir(parents=True, exist_ok=True)