Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
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
ai-commit.ts4.5 KB · 152 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
143
144
145
146
147
148
149
150
151
152
/**
 * Generate an AI commit message for the currently-staged diff and drop
 * it into the Source Control input box.
 *
 * Strategy:
 *   1. Read the staged diff via `git diff --cached` in the workspace.
 *   2. POST it to `${host}/api/v2/ai/commit-message` with the user's PAT.
 *   3. Format `{ subject, body }` into a single string and assign it to
 *      the active git SCM repository's `inputBox.value`.
 *
 * We talk to the API directly rather than shelling out to the `gluecron`
 * CLI — the CLI may not be installed in the user's $PATH, and the API
 * surface is the same.
 */

import * as vscode from "vscode";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { getToken, signInFlow } from "../auth";
import { getHost, pickWorkspaceFolder } from "../repo";

const execFileP = promisify(execFile);

/**
 * VS Code's built-in git extension exposes a typed API; we declare just
 * the bits we touch so the extension compiles without bundling
 * `@types/vscode.git`.
 */
interface GitInputBox {
  value: string;
}
interface GitRepository {
  rootUri: vscode.Uri;
  inputBox: GitInputBox;
}
interface GitApi {
  repositories: GitRepository[];
  getRepository?(uri: vscode.Uri): GitRepository | null;
}
interface GitExtensionExports {
  getAPI(version: 1): GitApi;
}

async function getStagedDiff(cwd: string): Promise<string> {
  const { stdout } = await execFileP("git", ["diff", "--cached"], {
    cwd,
    maxBuffer: 16 * 1024 * 1024, // 16 MiB — generous; API caps server-side too.
  });
  return stdout;
}

function pickRepository(cwd: string): GitRepository | null {
  const git = vscode.extensions.getExtension<GitExtensionExports>("vscode.git");
  if (!git) return null;
  const api = git.isActive ? git.exports.getAPI(1) : null;
  if (!api) return null;
  if (api.getRepository) {
    const found = api.getRepository(vscode.Uri.file(cwd));
    if (found) return found;
  }
  return api.repositories[0] || null;
}

export async function runAiCommitMessage(
  context: vscode.ExtensionContext
): Promise<void> {
  const folder = pickWorkspaceFolder();
  if (!folder) {
    vscode.window.showWarningMessage("Gluecron: no workspace open.");
    return;
  }

  let token = await getToken(context);
  if (!token) {
    const ok = await signInFlow(context, getHost());
    if (!ok) return;
    token = await getToken(context);
  }
  if (!token) return;

  let diff: string;
  try {
    diff = await getStagedDiff(folder.uri.fsPath);
  } catch (err) {
    vscode.window.showErrorMessage(
      `Gluecron: failed to read staged diff (${(err as Error).message}).`
    );
    return;
  }
  if (!diff.trim()) {
    vscode.window.showInformationMessage(
      "Gluecron: nothing staged. Stage changes first (git add)."
    );
    return;
  }

  const message = await vscode.window.withProgress(
    {
      location: vscode.ProgressLocation.SourceControl,
      title: "Gluecron: drafting commit message...",
      cancellable: false,
    },
    async () => {
      const url = `${getHost().replace(/\/+$/, "")}/api/v2/ai/commit-message`;
      const res = await fetch(url, {
        method: "POST",
        headers: {
          "content-type": "application/json",
          accept: "application/json",
          authorization: `Bearer ${token}`,
        },
        body: JSON.stringify({ diff, style: "conventional" }),
      });
      const text = await res.text();
      if (!res.ok) {
        throw new Error(`server returned ${res.status}: ${text.slice(0, 200)}`);
      }
      let json: { subject?: string; body?: string };
      try {
        json = JSON.parse(text);
      } catch {
        throw new Error("server returned non-JSON");
      }
      const subject = (json.subject || "").trim();
      const body = (json.body || "").trim();
      if (!subject) throw new Error("server returned an empty subject");
      return body ? `${subject}\n\n${body}` : subject;
    }
  ).then(
    (v) => v,
    (err: Error) => {
      vscode.window.showErrorMessage(`Gluecron: ${err.message}`);
      return null;
    }
  );

  if (!message) return;

  const repo = pickRepository(folder.uri.fsPath);
  if (repo) {
    repo.inputBox.value = message;
    vscode.window.showInformationMessage(
      "Gluecron: commit message dropped into Source Control."
    );
  } else {
    // Git extension isn't ready — fall back to copying the message.
    await vscode.env.clipboard.writeText(message);
    vscode.window.showInformationMessage(
      "Gluecron: copied commit message to clipboard (git SCM not ready)."
    );
  }
}