Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
claude/adoring-hopper-5x74bqclaude/affectionate-feynman-ykrf1hclaude/architecture-audit-design-wxprenclaude/build-status-update-3MXsfclaude/charming-meitner-mllb5rclaude/compare-gate-gluecron-s4mFQclaude/confident-faraday-tikcwbclaude/continue-work-XMTlIclaude/crontech-gluecron-deploy-7MIECclaude/crontech-platform-setup-SeKfwclaude/design-2026claude/ecstatic-ptolemy-jMdigclaude/enhance-github-integration-QNHdGclaude/fix-aa-loop-issue-PonMQclaude/fix-actions-and-processclaude/fix-desktop-errors-XqoW8claude/fix-red-workflowsclaude/fix-website-access-6FKJNclaude/gatetest-integration-hardeningclaude/github-audit-improvements-bDFr9claude/gluecron-launch-status-FoMRlclaude/hopeful-lamport-olfCTclaude/issue-to-pr-and-protectionsclaude/jolly-heisenberg-2sg1Qclaude/launch-preparation-QmTb6claude/new-session-xk1l7claude/plan-platform-architecture-kkN4yclaude/platform-analysis-roadmap-1nUGLclaude/platform-launch-assessment-8dWV8claude/polish-platform-release-AeDrUclaude/resume-previous-work-KzyLwclaude/review-crontech-handoff-qYEVqclaude/review-project-completeness-lHhS2claude/review-readme-docs-ulqPKclaude/serene-edison-rj87weclaude/setup-multi-repo-dev-BCwNQclaude/ship-fixes-and-tests-Jvz1cclaude/site-audit-competitive-pctlwgclaude/site-migration-vercel-XstpKclaude/standalone-product-repos-XHFTDcopilot/feat-smart-empty-states-keyboard-first-enhancementcopilot/feat-smart-morning-digest-review-context-restorecopilot/fix-and-process-workflowscopilot/update-ai-powered-code-reviewfeat/debt-mapfeat/push-policy-codeowners-hardeningfeat/smart-digest-contextfeat/stage-impactfeat/t1-secret-migrationfeat/u-polishfeat/w-self-hostfeat/w2-claude-configfix/agent-journey-orphan-sweepgatetest/auto-fix-1776586424172gatetest/auto-fix-1776586534814gatetest/auto-fix-1776590685143gatetest/auto-fix-1776590808199mainops/redeploy-retriggerstyle/dxt-cta-themeworktree-agent-a3377aad30d55da26worktree-agent-a7ef607b7ee1d6c74
GluecronUtil.kt5.2 KB · 142 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
package com.gluecron

import com.gluecron.settings.GluecronSettingsState
import com.intellij.notification.NotificationGroupManager
import com.intellij.notification.NotificationType
import com.intellij.openapi.ide.CopyPasteManager
import com.intellij.openapi.project.Project
import git4idea.repo.GitRepositoryManager
import java.awt.Desktop
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.time.Duration

/**
 * Shared utilities for Gluecron actions.
 *
 * - [detectOwnerRepo] — infers `owner/repo` from the project's git remote URL
 * - [openBrowser]     — opens a URL in the system's default browser
 * - [apiPost]         — synchronous POST helper used by MergePrAction
 * - [notify]          — show a balloon notification
 */
object GluecronUtil {

    /**
     * Detects the owner and repository name from the first git remote URL
     * in the project that contains the configured Gluecron host, or falls
     * back to any remote named "origin".
     *
     * Handles both HTTPS and SSH remote formats:
     *   https://gluecron.com/owner/repo.git
     *   git@gluecron.com:owner/repo.git
     *
     * Returns null if no usable git remote is found.
     */
    fun detectOwnerRepo(project: Project): Pair<String, String>? {
        val settings = GluecronSettingsState.getInstance()
        val manager = GitRepositoryManager.getInstance(project)
        val repos = manager.repositories
        if (repos.isEmpty()) return null

        // Prefer remotes whose URL contains the configured host
        val hostDomain = settings.host
            .removePrefix("https://")
            .removePrefix("http://")
            .trimEnd('/')

        fun parseRemoteUrl(url: String): Pair<String, String>? {
            // Normalise: strip protocol + host prefix or git@ prefix
            val cleaned = url
                .replace(Regex("^https?://[^/]+/"), "")
                .replace(Regex("^git@[^:]+:"), "")
                .removeSuffix(".git")
            val parts = cleaned.split("/").filter { it.isNotBlank() }
            if (parts.size < 2) return null
            return Pair(parts[0], parts[1])
        }

        for (repo in repos) {
            val remotes = repo.remotes
            // Try host-matching remote first
            for (remote in remotes) {
                val url = remote.firstUrl ?: continue
                if (url.contains(hostDomain)) {
                    return parseRemoteUrl(url)
                }
            }
            // Fall back to any remote named "origin"
            val origin = remotes.firstOrNull { it.name == "origin" }
            if (origin != null) {
                val url = origin.firstUrl ?: continue
                return parseRemoteUrl(url)
            }
        }
        return null
    }

    /**
     * Returns the name of the currently checked-out branch in the first
     * git repository found in the project, or null if none.
     */
    fun currentBranch(project: Project): String? {
        val manager = GitRepositoryManager.getInstance(project)
        return manager.repositories.firstOrNull()
            ?.currentBranchName
    }

    /**
     * Opens [url] in the system's default browser.
     * Falls back gracefully if Desktop is not supported.
     */
    fun openBrowser(url: String) {
        if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
            Desktop.getDesktop().browse(URI(url))
        }
    }

    /**
     * Performs a synchronous HTTP POST to [path] (relative to the configured host)
     * with a JSON [body]. Returns the response body as a string, or throws on error.
     *
     * This is intentionally minimal — used only by MergePrAction which needs a
     * blocking call so we can surface the result in the same action invocation.
     */
    fun apiPost(path: String, body: String): String {
        val settings = GluecronSettingsState.getInstance()
        val url = "${settings.host}$path"
        val token = settings.token

        val client = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(10))
            .build()

        val requestBuilder = HttpRequest.newBuilder()
            .uri(URI(url))
            .timeout(Duration.ofSeconds(30))
            .header("Content-Type", "application/json")
            .header("Accept", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(body))

        if (token.isNotBlank()) {
            requestBuilder.header("Authorization", "Bearer $token")
        }

        val response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString())
        if (response.statusCode() !in 200..299) {
            throw RuntimeException("HTTP ${response.statusCode()}: ${response.body().take(200)}")
        }
        return response.body()
    }

    /**
     * Shows a balloon notification attached to [project].
     */
    fun notify(project: Project, message: String, type: NotificationType = NotificationType.INFORMATION) {
        NotificationGroupManager.getInstance()
            .getNotificationGroup("Gluecron Notifications")
            .createNotification("Gluecron", message, type)
            .notify(project)
    }
}