ci / check (push) Canceled after 0s
The first public release of LLeMbas CLI: a terminal coding agent and project manager for any LLM API, with permission modes, git snapshots, memory and skills, knowledge bases, MCP, voice, and a link to a LLeMbas instance whose web UI can work its sessions too. Signed Linux binaries for x64 and arm64. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
80 lines
3.7 KiB
TypeScript
80 lines
3.7 KiB
TypeScript
// /commit: what to commit, a drafted message, and the commit itself. Files are always staged by
|
|
// name — never `git add -A` — so nothing the session did not touch rides along.
|
|
import { readFileSync } from "node:fs"
|
|
import { join } from "node:path"
|
|
import type { Client } from "../provider/types.ts"
|
|
import { git } from "./run.ts"
|
|
|
|
export interface CommitPlan {
|
|
files: string[]
|
|
/** true: the user had already staged these; commit exactly the index. */
|
|
staged: boolean
|
|
}
|
|
|
|
/** The index if anything is staged; otherwise the session's changed files that git sees as changed. */
|
|
export function commitPlan(root: string, touched: string[], all = false): CommitPlan {
|
|
const staged = git(root, ["diff", "--cached", "--name-only", "-z"]).out.split("\0").filter(Boolean)
|
|
if (staged.length && !all) return { files: staged, staged: true }
|
|
const status = git(root, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]).out.split("\0").filter(Boolean)
|
|
const changed = status.map((l) => l.slice(3))
|
|
const files = all ? changed : changed.filter((f) => touched.includes(f))
|
|
return { files, staged: false }
|
|
}
|
|
|
|
const PROMPT = `Write a git commit message for the change below.
|
|
|
|
- First line: a summary under 72 characters, imperative mood, in the style of the recent commits shown.
|
|
- Then, only if the change needs explaining, a blank line and a short body saying what changed and why — not a list of files.
|
|
- Reply with the message only: no quotes, no code fences, no preamble.
|
|
|
|
Recent commits, for style:
|
|
{{recent}}
|
|
|
|
The change:
|
|
{{diff}}`
|
|
|
|
export async function draftMessage(client: Client, root: string, plan: CommitPlan, signal?: AbortSignal): Promise<string> {
|
|
const recent = git(root, ["log", "-10", "--format=%s"]).out || "(none yet)"
|
|
const diff = plan.staged
|
|
? git(root, ["diff", "--cached", "--no-color", "--stat", "-p"]).out
|
|
: [
|
|
git(root, ["diff", "--no-color", "--stat", "-p", "HEAD", "--", ...plan.files]).out ||
|
|
git(root, ["diff", "--no-color", "-p", "--", ...plan.files]).out,
|
|
...untrackedPreview(root, plan.files),
|
|
].join("\n")
|
|
const prompt = PROMPT.replace("{{recent}}", () => recent).replace("{{diff}}", () => diff.slice(0, 24_000))
|
|
let text = ""
|
|
for await (const ev of client.stream({ system: "", messages: [{ role: "user", parts: [{ type: "text", text: prompt }] }], tools: [], effort: null, signal }))
|
|
if (ev.type === "finish") text = ev.message.parts.map((p) => (p.type === "text" ? p.text : "")).join("")
|
|
return cleanMessage(text)
|
|
}
|
|
|
|
/** New files have no diff against HEAD; show the model their first lines instead. */
|
|
function untrackedPreview(root: string, files: string[]): string[] {
|
|
const untracked = git(root, ["ls-files", "--others", "--exclude-standard", "-z", "--", ...files]).out.split("\0").filter(Boolean)
|
|
return untracked.map((f) => {
|
|
let head = ""
|
|
try {
|
|
head = readFileSync(join(root, f), "utf8").split("\n").slice(0, 40).join("\n")
|
|
} catch {}
|
|
return `new file ${f}:\n${head}`
|
|
})
|
|
}
|
|
|
|
export function cleanMessage(text: string): string {
|
|
let t = text.trim().replace(/^```\w*\n?/, "").replace(/\n?```$/, "").trim()
|
|
t = t.replace(/^(commit message|message):\s*/i, "").replace(/^["'](.*)["']$/s, "$1")
|
|
return t
|
|
}
|
|
|
|
export function commit(root: string, plan: CommitPlan, message: string): { ok: boolean; text: string } {
|
|
if (!plan.staged && plan.files.length) {
|
|
const add = git(root, ["add", "--", ...plan.files])
|
|
if (add.code !== 0) return { ok: false, text: add.err }
|
|
}
|
|
const r = git(root, ["commit", "-q", "-F", "-"], { input: message })
|
|
if (r.code !== 0) return { ok: false, text: r.err || r.out }
|
|
const head = git(root, ["log", "-1", "--format=%h %s"]).out
|
|
return { ok: true, text: head }
|
|
}
|