From 339f8af71ce58c0c671b4981967a6975ea7aded7 Mon Sep 17 00:00:00 2001 From: rafaeelricco Date: Sat, 11 Apr 2026 11:35:20 -0300 Subject: [PATCH 01/11] Centralize AI response extraction and add exhaustiveness utility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Introduce `absurd` helper in `libs/types.ts` to replace ad-hoc `_exhaustiveCheck: never` patterns across the codebase - Add `responseParser.ts` in `domain/provider` defining `RawResponse` and `extractResponse`, consolidating all per-provider text extraction logic into one place - Refactor `anthropic.ts`, `gemini.ts`, and `openai.ts` to return raw API responses and delegate to `extractResponse`, removing duplicated empty-response checks - Move `resolveProvider.ts` to `domain/provider/authResolver.ts` and replace inline exhaustiveness guards with `absurd` - Update `prompts.ts` to accept `Maybe` for the custom template instead of `string | undefined`, and fix a typo ("comic" → "commit") - Update `llm.ts` and `commit.ts` signatures to pass `Maybe` for `customTemplate`, eliminating `??` fallback logic at call sites --- src/app/commands/commit.ts | 53 ++++++------- src/app/services/anthropic.ts | 49 +++++------- src/app/services/gemini.ts | 33 +++----- src/app/services/llm.ts | 8 +- src/app/services/openai.ts | 36 +++------ src/domain/commit/prompts.ts | 30 ++++--- .../provider/authResolver.ts} | 14 ++-- src/domain/provider/responseParser.ts | 79 +++++++++++++++++++ src/libs/types.ts | 18 ++++- 9 files changed, 189 insertions(+), 131 deletions(-) rename src/{app/services/resolveProvider.ts => domain/provider/authResolver.ts} (79%) create mode 100644 src/domain/provider/responseParser.ts diff --git a/src/app/commands/commit.ts b/src/app/commands/commit.ts index 1dc31bf..da25798 100644 --- a/src/app/commands/commit.ts +++ b/src/app/commands/commit.ts @@ -1,20 +1,13 @@ export { Commit }; import * as p from "@clack/prompts"; +import * as repo from "@/lib/git/repo"; import { Future } from "@/libs/future"; import { loadConfig } from "@/lib/storage/config"; import { Setup } from "@/app/commands/setup"; -import { CommitConvention, type Config, type ProviderConfig } from "@/domain/config/config"; +import { type CommitConvention, type Config, type ProviderConfig } from "@/domain/config/config"; import { resolveProvider } from "@/app/services/resolveProvider"; -import { - checkIsGitRepo, - getStagedDiff, - performCommit, - performPush, - getCurrentBranch, - hasUpstream -} from "@/lib/git/repo"; import { generateCommitMessage, refineCommitMessage } from "@/app/services/llm"; import { Nothing, type Maybe, Just } from "@/libs/maybe"; import { loading } from "@/lib/ui/spinner"; @@ -47,9 +40,14 @@ class Commit { } run(): Future { - return checkIsGitRepo() + return repo + .checkIsGitRepo() .chain(() => this.diff()) - .chain((diff) => this.generate(diff).chain((message) => this.interact(diff, message))) + .chain((diff) => + this.generate(diff, this.config.commit_convention, this.config.custom_template).chain((message) => + this.interact(diff, message) + ) + ) .mapRej((e) => { if (e instanceof Error) { p.log.error(color.red(e.message)); @@ -59,19 +57,14 @@ class Commit { } diff(): Future { - return getStagedDiff(); + return repo.getStagedDiff(); } - generate(diff: string, convention?: CommitConvention, template?: string): Future { + generate(diff: string, convention: CommitConvention, template: Maybe = Nothing()): Future { return loading( "Generating commit message...", "Message generated!", - generateCommitMessage( - this.providerConfig, - diff, - convention ?? this.config.commit_convention, - template ?? this.config.custom_template.maybe(undefined, (t) => t) - ) + generateCommitMessage(this.providerConfig, diff, convention, template) ); } @@ -80,7 +73,7 @@ class Commit { } commit(message: string): Future { - return performCommit(message); + return repo.performCommit(message); } push(branch?: string, publish = false, forceWithLease = false): Future { @@ -94,7 +87,7 @@ class Commit { : publish ? "Published successfully!" : "Pushed successfully!"; - return loading(startMsg, endMsg, performPush(branch, publish, forceWithLease)).map(() => {}); + return loading(startMsg, endMsg, repo.performPush(branch, publish, forceWithLease)).map(() => {}); } interact(diff: string, message: string): Future { @@ -105,7 +98,9 @@ class Commit { case "commit_push": return this.handleCommitAndPush(message); case "regenerate": - return this.generate(diff, "imperative").chain((msg) => this.interact(diff, msg)); + return this.generate(diff, this.config.commit_convention, this.config.custom_template).chain((msg) => + this.interact(diff, msg) + ); case "adjust": return this.handleAdjust(diff, message); case "cancel": @@ -157,15 +152,17 @@ class Commit { } private pushAfterCommit(): Future { - return hasUpstream().chain((exists) => - exists ? - this.push().chainRej((err) => (isNonFastForwardError(err) ? this.promptForceWithLease() : Future.reject(err))) - : this.promptPublishBranch() - ); + return repo + .hasUpstream() + .chain((exists) => + exists ? + this.push().chainRej((err) => (isNonFastForwardError(err) ? this.promptForceWithLease() : Future.reject(err))) + : this.promptPublishBranch() + ); } private promptPublishBranch(): Future { - return getCurrentBranch().chain((branch) => + return repo.getCurrentBranch().chain((branch) => Future.attemptP(async () => { const publish = await p.confirm({ message: `Branch '${branch}' has no upstream. Publish to origin?` diff --git a/src/app/services/anthropic.ts b/src/app/services/anthropic.ts index 0d622bc..e4d4221 100644 --- a/src/app/services/anthropic.ts +++ b/src/app/services/anthropic.ts @@ -6,36 +6,28 @@ import { type Config } from "@/domain/config/config"; import { type GenerateContentParams } from "@/app/services/llm"; import { Future } from "@/libs/future"; import { anthropicOAuthHeaders, CLAUDE_CODE_SYSTEM_PROMPT } from "@/lib/auth/anthropic"; +import { absurd } from "@/libs/types"; +import { extractResponse } from "@/app/services/responseExtractor"; type AnthropicConfig = Extract; type TextBlock = { type: "text"; text: string }; -const MAX_TOKENS = 4096; - const toError = (error: unknown): Error => (error instanceof Error ? error : new Error(String(error))); -const extractText = (content: Array<{ type: string; text?: string }>): string => - content - .filter((block): block is TextBlock => block.type === "text" && typeof block.text === "string") - .map((block) => block.text) - .join(""); - const callAnthropicWithApiKey = (apiKey: string, model: string, params: GenerateContentParams): Future => Future.attemptP(async () => { const client = new Anthropic({ apiKey }); - const response = await client.messages.create({ + return await client.messages.create({ model, - max_tokens: MAX_TOKENS, + max_tokens: 4096, ...(params.systemInstruction !== undefined ? { system: params.systemInstruction } : {}), messages: [{ role: "user", content: params.prompt }] }); - - const text = extractText(response.content); - if (!text.trim()) throw new Error("Empty AI response"); - return text.trim(); - }).mapRej(toError); + }) + .mapRej(toError) + .chain((response) => extractResponse({ provider: "anthropic", value: response })); const callAnthropicWithSetupToken = ( authToken: string, @@ -49,22 +41,21 @@ const callAnthropicWithSetupToken = ( defaultHeaders: anthropicOAuthHeaders() }); - const systemBlocks: TextBlock[] = [ - { type: "text", text: CLAUDE_CODE_SYSTEM_PROMPT }, - ...(params.systemInstruction !== undefined ? [{ type: "text" as const, text: params.systemInstruction }] : []) - ]; + const systemBlocks: TextBlock[] = [{ type: "text", text: CLAUDE_CODE_SYSTEM_PROMPT }]; - const response = await client.messages.create({ + if (params.systemInstruction !== undefined) { + systemBlocks.push({ type: "text", text: params.systemInstruction }); + } + + return await client.messages.create({ model, - max_tokens: MAX_TOKENS, + max_tokens: 4096, system: systemBlocks, messages: [{ role: "user", content: params.prompt }] }); - - const text = extractText(response.content); - if (!text.trim()) throw new Error("Empty AI response"); - return text.trim(); - }).mapRej(toError); + }) + .mapRej(toError) + .chain((response) => extractResponse({ provider: "anthropic", value: response })); const generateContentWithAnthropic = ( config: AnthropicConfig, @@ -78,9 +69,7 @@ const generateContentWithAnthropic = ( case "google_oauth": case "openai_oauth": return Future.reject(new Error(`Unsupported auth method for Anthropic: ${config.auth_method.type}`)); - default: { - const _exhaustiveCheck: never = config.auth_method; - return Future.reject(new Error(`Unknown auth method: ${JSON.stringify(_exhaustiveCheck)}`)); - } + default: + return absurd(config.auth_method, "AuthMethod"); } }; diff --git a/src/app/services/gemini.ts b/src/app/services/gemini.ts index 992ce22..b3d547d 100644 --- a/src/app/services/gemini.ts +++ b/src/app/services/gemini.ts @@ -6,6 +6,7 @@ import { type Config, type OAuthTokens } from "@/domain/config/config"; import { getAccessToken } from "@/lib/auth/google"; import { Just, Nothing, type Maybe } from "@/libs/maybe"; import { type GenerateContentParams } from "@/app/services/llm"; +import { extractResponse } from "@/app/services/responseExtractor"; type GeminiConfig = Extract; @@ -34,19 +35,17 @@ const generateContentWithApiKey = ( params: GenerateContentParams ): Future => { const genAI = new GoogleGenerativeAI(apiKey); - const geminiModel = genAI.getGenerativeModel({ - model, - ...(params.systemInstruction !== undefined ? { systemInstruction: params.systemInstruction } : {}) - }); + const modelParams: Parameters[0] = { model }; - return Future.attemptP(async () => { - const result = await geminiModel.generateContent(params.prompt); + if (params.systemInstruction !== undefined) { + modelParams.systemInstruction = params.systemInstruction; + } - const text = result.response.text() ?? ""; + const geminiModel = genAI.getGenerativeModel(modelParams); - if (!text || !text.trim()) throw new Error("Empty AI response"); - return text.trim(); - }).mapRej(toError); + return Future.attemptP(async () => await geminiModel.generateContent(params.prompt)) + .mapRej(toError) + .chain((result) => extractResponse({ provider: "gemini", source: "sdk", value: result })); }; const generateContentWithOAuth = ( @@ -81,20 +80,14 @@ const generateContentWithOAuth = ( throw new Error(`Gemini API error (${response.status}): ${errorBody}`); } - const json = (await response.json()) as { + return (await response.json()) as { promptFeedback?: unknown; usageMetadata?: unknown; candidates?: Array<{ content?: { parts?: Array<{ text?: string }> } }>; }; - - const text = json.candidates?.[0]?.content?.parts?.[0]?.text; - - if (!text || !text.trim()) { - throw new Error("Empty AI response"); - } - - return text.trim(); - }).mapRej(toError) + }) + .mapRej(toError) + .chain((json) => extractResponse({ provider: "gemini", source: "rest", value: json })) ); const generateContentWithGemini = (config: GeminiConfig, params: GenerateContentParams): Future => { diff --git a/src/app/services/llm.ts b/src/app/services/llm.ts index a573a11..2ce587c 100644 --- a/src/app/services/llm.ts +++ b/src/app/services/llm.ts @@ -6,6 +6,7 @@ import { generateContentWithGemini } from "@/app/services/gemini"; import { generateContentWithOpenAI } from "@/app/services/openai"; import { generateContentWithAnthropic } from "@/app/services/anthropic"; import { getPrompt, getRefinePrompt } from "@/domain/commit/prompts"; +import { Maybe, Nothing } from "@/libs/maybe"; type GenerateContentParams = { readonly prompt: string; @@ -27,11 +28,8 @@ const generateCommitMessage = ( config: ProviderConfig, diff: string, convention: CommitConvention, - customTemplate?: string -): Future => - generateContent(config, { - prompt: getPrompt(diff, convention, customTemplate) - }); + customTemplate: Maybe = Nothing() +): Future => generateContent(config, { prompt: getPrompt(diff, convention, customTemplate) }); const refineCommitMessage = ( config: ProviderConfig, diff --git a/src/app/services/openai.ts b/src/app/services/openai.ts index b04be49..eb8a161 100644 --- a/src/app/services/openai.ts +++ b/src/app/services/openai.ts @@ -4,6 +4,7 @@ import { type Config, type OpenAITokens } from "@/domain/config/config"; import { type GenerateContentParams } from "@/app/services/llm"; import { Future } from "@/libs/future"; import { getOpenAIAccessToken } from "@/lib/auth/openai"; +import { extractResponse } from "@/app/services/responseExtractor"; import OpenAI from "openai"; @@ -14,17 +15,14 @@ const toError = (error: unknown): Error => (error instanceof Error ? error : new const callOpenAIWithApiKey = (authToken: string, model: string, params: GenerateContentParams): Future => Future.attemptP(async () => { const client = new OpenAI({ apiKey: authToken }); - const response = await client.responses.create({ + return await client.responses.create({ model, instructions: params.systemInstruction ?? null, input: params.prompt }); - - const text = response.output_text ?? ""; - - if (!text || !text.trim()) throw new Error("Empty AI response"); - return text.trim(); - }).mapRej(toError); + }) + .mapRej(toError) + .chain((response) => extractResponse({ provider: "openai", source: "direct", value: response })); const callOpenAIWithOAuth = (authToken: string, model: string, params: GenerateContentParams): Future => Future.attemptP(async () => { @@ -52,26 +50,10 @@ const callOpenAIWithOAuth = (authToken: string, model: string, params: GenerateC }); const response = await stream.finalResponse(); - - const extractedText = response.output - .flatMap((item) => (item.type === "message" ? item.content : [])) - .map((c) => (c.type === "output_text" ? c.text : "")) - .join(""); - - const outputText = response.output_text ?? ""; - const candidates = [ - { source: "output", value: extractedText }, - { source: "output_text", value: outputText }, - { source: "done_event", value: doneEventText }, - { source: "delta_snapshot", value: deltaSnapshotText } - ] as const; - const selected = candidates.find((candidate) => candidate.value.trim().length > 0); - const text = selected?.value.trim() ?? ""; - - if (!text.trim()) throw new Error("Empty AI response"); - - return text.trim(); - }).mapRej(toError); + return { response, doneEventText, deltaSnapshotText }; + }) + .mapRej(toError) + .chain((bundle) => extractResponse({ provider: "openai", source: "stream", value: bundle })); const generateContentWithApiKey = ( apiKey: string, diff --git a/src/domain/commit/prompts.ts b/src/domain/commit/prompts.ts index a80f967..74107f1 100644 --- a/src/domain/commit/prompts.ts +++ b/src/domain/commit/prompts.ts @@ -1,8 +1,10 @@ export { getPrompt, getRefinePrompt }; import { CommitConvention } from "@/domain/config/config"; +import { Just, Nothing, type Maybe } from "@/libs/maybe"; +import { absurd } from "@/libs/types"; -function getPrompt(diff: string, convention: CommitConvention, customTemplate?: string): string { +function getPrompt(diff: string, convention: CommitConvention, customTemplate: Maybe = Nothing()): string { switch (convention) { case "conventional": return promptConventional(diff); @@ -11,7 +13,7 @@ function getPrompt(diff: string, convention: CommitConvention, customTemplate?: case "custom": return promptCustom(diff, customTemplate); default: - return promptImperative(diff); + return absurd(convention, "CommitConvention"); } } @@ -90,14 +92,14 @@ function promptConventional(gitDiff: string): string { • Line 2: blank. • Remaining lines: each line is a bullet starting with "- ". -`; + `; } function promptImperative(gitDiff: string): string { return ` You are an expert software engineer and version control specialist. - Your job is to read git diffs and output high-quality comic messages + Your job is to read git diffs and output high-quality commit messages that follow these rules. @@ -210,14 +212,13 @@ function promptImperative(gitDiff: string): string { `; } -function promptCustom(gitDiff: string, template?: string): string { - if (!template) { - return promptImperative(gitDiff); - } - - const processedTemplate = template.replace("{diff}", gitDiff); - - return ` +function promptCustom(gitDiff: string, template: Maybe): string { + switch (true) { + case template instanceof Nothing: + return promptImperative(gitDiff); + case template instanceof Just: { + const processedTemplate = template.value.replace("{diff}", gitDiff); + return ` You are an expert software engineer and version control specialist. Your job is to read git diffs and output high-quality commit messages @@ -235,6 +236,11 @@ function promptCustom(gitDiff: string, template?: string): string { 4. Do NOT wrap the commit message in quotes or code fences. `; + } + default: + template satisfies never; + return promptImperative(gitDiff); + } } function getRefinePrompt(params: { diff: string; currentMessage: string; adjustment: string }): { diff --git a/src/app/services/resolveProvider.ts b/src/domain/provider/authResolver.ts similarity index 79% rename from src/app/services/resolveProvider.ts rename to src/domain/provider/authResolver.ts index 7d289f8..27ea03e 100644 --- a/src/app/services/resolveProvider.ts +++ b/src/domain/provider/authResolver.ts @@ -6,6 +6,7 @@ import { type Config, type ProviderConfig, type RefreshTokens } from "@/domain/c import { ensureFreshTokens } from "@/lib/auth/google"; import { ensureFreshOpenAITokens } from "@/lib/auth/openai"; import { updateGoogleTokens, updateOpenAITokens } from "@/lib/storage/config"; +import { absurd } from "@/libs/types"; type DetectTokenChange = (original: T, fresh: T) => Maybe; type RefreshProvider = (tokens: T) => Future; @@ -32,30 +33,27 @@ const refreshAndPersist: RefreshAndPersistFlow = (tokens, refresh, persist) => const resolveProvider: ResolveProvider = (config) => { const { ai } = config; - const { auth_method } = ai; - switch (auth_method.type) { + switch (ai.auth_method.type) { case "api_key": case "anthropic_setup_token": return Future.resolve(ai); case "google_oauth": - return refreshAndPersist(auth_method.content, ensureFreshTokens, updateGoogleTokens).map((tokens) => ({ + return refreshAndPersist(ai.auth_method.content, ensureFreshTokens, updateGoogleTokens).map((tokens) => ({ provider: ai.provider, model: ai.model, auth_method: { type: "google_oauth", content: tokens } })); case "openai_oauth": - return refreshAndPersist(auth_method.content, ensureFreshOpenAITokens, updateOpenAITokens).map((tokens) => ({ + return refreshAndPersist(ai.auth_method.content, ensureFreshOpenAITokens, updateOpenAITokens).map((tokens) => ({ provider: ai.provider, model: ai.model, auth_method: { type: "openai_oauth", content: tokens } })); - default: { - const _exhaustiveCheck: never = auth_method; - return Future.reject(new Error(`Unknown auth method: ${JSON.stringify(_exhaustiveCheck)}`)); - } + default: + return absurd(ai.auth_method, "AuthMethod"); } }; diff --git a/src/domain/provider/responseParser.ts b/src/domain/provider/responseParser.ts new file mode 100644 index 0000000..37b0ac3 --- /dev/null +++ b/src/domain/provider/responseParser.ts @@ -0,0 +1,79 @@ +export { type RawResponse, extractResponse, finalizeText }; + +import { Future } from "@/libs/future"; +import { absurd } from "@/libs/types"; + +const EMPTY_RESPONSE_ERROR = "Empty AI response"; + +const finalizeText = (raw: string | null | undefined): Future => { + const trimmed = (raw ?? "").trim(); + return trimmed.length === 0 ? Future.reject(new Error(EMPTY_RESPONSE_ERROR)) : Future.resolve(trimmed); +}; + +type TextBlock = { type: "text"; text: string }; +type AnthropicContent = Array<{ type: string; text?: string }>; + +type GeminiSDKLike = { response: { text: () => string | null | undefined } }; +type GeminiRESTLike = { + candidates?: Array<{ content?: { parts?: Array<{ text?: string }> } }>; +}; + +type OpenAIDirectLike = { output_text?: string | null }; +type OpenAIStreamLike = { + response: { + output: Array<{ type: string; content?: Array<{ type: string; text?: string }> }>; + output_text?: string | null; + }; + doneEventText: string; + deltaSnapshotText: string; +}; + +type RawResponse = + | { provider: "gemini"; source: "sdk"; value: GeminiSDKLike } + | { provider: "gemini"; source: "rest"; value: GeminiRESTLike } + | { provider: "anthropic"; value: { content: AnthropicContent } } + | { provider: "openai"; source: "direct"; value: OpenAIDirectLike } + | { provider: "openai"; source: "stream"; value: OpenAIStreamLike }; + +const extractAnthropicText = (content: AnthropicContent): string => + content + .filter((b): b is TextBlock => b.type === "text" && typeof b.text === "string") + .map((b) => b.text) + .join(""); + +const extractOpenAIStreamText = (raw: OpenAIStreamLike): string => { + const fromOutput = raw.response.output + .flatMap((item) => (item.type === "message" ? (item.content ?? []) : [])) + .map((c) => (c.type === "output_text" ? (c.text ?? "") : "")) + .join(""); + + const candidates = [fromOutput, raw.response.output_text ?? "", raw.doneEventText, raw.deltaSnapshotText]; + return candidates.find((v) => v.trim().length > 0) ?? ""; +}; + +const extractResponse = (raw: RawResponse): Future => { + switch (raw.provider) { + case "gemini": + switch (raw.source) { + case "sdk": + return finalizeText(raw.value.response.text()); + case "rest": + return finalizeText(raw.value.candidates?.[0]?.content?.parts?.[0]?.text); + default: + return absurd(raw, "RawResponse.gemini"); + } + case "anthropic": + return finalizeText(extractAnthropicText(raw.value.content)); + case "openai": + switch (raw.source) { + case "direct": + return finalizeText(raw.value.output_text); + case "stream": + return finalizeText(extractOpenAIStreamText(raw.value)); + default: + return absurd(raw, "RawResponse.openai"); + } + default: + return absurd(raw, "RawResponse"); + } +}; diff --git a/src/libs/types.ts b/src/libs/types.ts index fa9a969..aafa799 100644 --- a/src/libs/types.ts +++ b/src/libs/types.ts @@ -1,7 +1,23 @@ /* This module holds useful type-level functions. */ -export { type UnionPick }; +export { type UnionPick, absurd }; + +// Exhaustiveness guard for discriminated unions. +// +// Call in the `default` branch of a switch (or any unreachable position) to +// force the compiler to verify every variant was handled. Adding a new variant +// makes `value` no longer assignable to `never`, turning the missed case into +// a compile-time error. +// +// switch (x.type) { +// case "a": return ... +// case "b": return ... +// default: return absurd(x, "UserEvent"); +// } +function absurd(value: never, label: string): never { + throw new Error(`${label}: unhandled variant ${JSON.stringify(value)}`); +} type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; From e7c75f41c4a0db2737c9e8a3a2faba420161b6d7 Mon Sep 17 00:00:00 2001 From: rafaeelricco Date: Sat, 11 Apr 2026 11:35:28 -0300 Subject: [PATCH 02/11] Move `resolveProvider` and `extractResponse` to domain layer - Update imports in `commit.ts` and `model.ts` to use `resolveProvider` from `@/domain/provider/authResolver` instead of `@/app/services/resolveProvider`. - Update imports in `anthropic.ts`, `gemini.ts`, and `openai.ts` to use `extractResponse` from `@/domain/provider/responseParser` instead of `@/app/services/responseExtractor`. --- src/app/commands/commit.ts | 2 +- src/app/commands/model.ts | 2 +- src/app/services/anthropic.ts | 2 +- src/app/services/gemini.ts | 2 +- src/app/services/openai.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/commands/commit.ts b/src/app/commands/commit.ts index da25798..413557a 100644 --- a/src/app/commands/commit.ts +++ b/src/app/commands/commit.ts @@ -7,7 +7,7 @@ import { Future } from "@/libs/future"; import { loadConfig } from "@/lib/storage/config"; import { Setup } from "@/app/commands/setup"; import { type CommitConvention, type Config, type ProviderConfig } from "@/domain/config/config"; -import { resolveProvider } from "@/app/services/resolveProvider"; +import { resolveProvider } from "@/domain/provider/authResolver"; import { generateCommitMessage, refineCommitMessage } from "@/app/services/llm"; import { Nothing, type Maybe, Just } from "@/libs/maybe"; import { loading } from "@/lib/ui/spinner"; diff --git a/src/app/commands/model.ts b/src/app/commands/model.ts index fd38c5f..c19f336 100644 --- a/src/app/commands/model.ts +++ b/src/app/commands/model.ts @@ -5,7 +5,7 @@ import * as p from "@clack/prompts"; import { Future } from "@/libs/future"; import { type Config, type ProviderConfig } from "@/domain/config/config"; import { loadConfig, saveConfig } from "@/lib/storage/config"; -import { resolveProvider } from "@/app/services/resolveProvider"; +import { resolveProvider } from "@/domain/provider/authResolver"; import { fetchModels, selectModelInteractively } from "@/domain/commit/model"; import { loading } from "@/lib/ui/spinner"; diff --git a/src/app/services/anthropic.ts b/src/app/services/anthropic.ts index e4d4221..f1a14fb 100644 --- a/src/app/services/anthropic.ts +++ b/src/app/services/anthropic.ts @@ -7,7 +7,7 @@ import { type GenerateContentParams } from "@/app/services/llm"; import { Future } from "@/libs/future"; import { anthropicOAuthHeaders, CLAUDE_CODE_SYSTEM_PROMPT } from "@/lib/auth/anthropic"; import { absurd } from "@/libs/types"; -import { extractResponse } from "@/app/services/responseExtractor"; +import { extractResponse } from "@/domain/provider/responseParser"; type AnthropicConfig = Extract; diff --git a/src/app/services/gemini.ts b/src/app/services/gemini.ts index b3d547d..aa0d91d 100644 --- a/src/app/services/gemini.ts +++ b/src/app/services/gemini.ts @@ -6,7 +6,7 @@ import { type Config, type OAuthTokens } from "@/domain/config/config"; import { getAccessToken } from "@/lib/auth/google"; import { Just, Nothing, type Maybe } from "@/libs/maybe"; import { type GenerateContentParams } from "@/app/services/llm"; -import { extractResponse } from "@/app/services/responseExtractor"; +import { extractResponse } from "@/domain/provider/responseParser"; type GeminiConfig = Extract; diff --git a/src/app/services/openai.ts b/src/app/services/openai.ts index eb8a161..6c608fe 100644 --- a/src/app/services/openai.ts +++ b/src/app/services/openai.ts @@ -4,7 +4,7 @@ import { type Config, type OpenAITokens } from "@/domain/config/config"; import { type GenerateContentParams } from "@/app/services/llm"; import { Future } from "@/libs/future"; import { getOpenAIAccessToken } from "@/lib/auth/openai"; -import { extractResponse } from "@/app/services/responseExtractor"; +import { extractResponse } from "@/domain/provider/responseParser"; import OpenAI from "openai"; From 04ed31ceaf1593b7c010d5cfb8efa63c12b1912d Mon Sep 17 00:00:00 2001 From: rafaeelricco Date: Sat, 11 Apr 2026 11:56:32 -0300 Subject: [PATCH 03/11] Rename `libs` to `utils` and move LLM services under `app/llm` and `domain/llm` - Rename all files under `src/libs/` to `src/utils/`, updating all internal imports accordingly. - Move `src/app/services/{anthropic,gemini,openai,llm}.ts` to `src/app/llm/`. - Move `src/domain/provider/{authResolver,responseParser}.ts` to `src/domain/llm/`. - Update all import paths across the codebase to reflect the new directory structure. --- index.ts | 2 +- src/app/cli.ts | 4 ++-- src/app/commands/commit.ts | 8 ++++---- src/app/commands/doctor.ts | 2 +- src/app/commands/model.ts | 4 ++-- src/app/commands/setup.ts | 4 ++-- src/app/integrations.ts | 2 +- src/app/{services => llm}/anthropic.ts | 8 ++++---- src/app/{services => llm}/gemini.ts | 8 ++++---- src/app/{services => llm}/llm.ts | 10 +++++----- src/app/{services => llm}/openai.ts | 6 +++--- src/domain/commit/model.ts | 2 +- src/domain/commit/prompts.ts | 4 ++-- src/domain/config/config.ts | 2 +- src/domain/{provider => llm}/authResolver.ts | 6 +++--- .../{provider => llm}/responseParser.ts | 4 ++-- src/lib/auth/google.ts | 2 +- src/lib/auth/openai.ts | 2 +- src/lib/git/repo.ts | 2 +- src/lib/storage/config.ts | 6 +++--- src/lib/ui/spinner.ts | 2 +- src/{libs => utils}/callable.ts | 0 src/{libs => utils}/future.ts | 2 +- src/{libs => utils}/helpers/object.ts | 0 src/{libs => utils}/json/decoder.ts | 8 ++++---- src/{libs => utils}/json/encoder.ts | 4 ++-- src/{libs => utils}/json/schema.ts | 20 +++++++++---------- src/{libs => utils}/json/types.ts | 0 src/{libs => utils}/list.ts | 2 +- src/{libs => utils}/maybe.ts | 2 +- src/{libs => utils}/remote-data.ts | 4 ++-- src/{libs => utils}/result.ts | 6 +++--- src/{libs => utils}/router.ts | 2 +- src/{libs => utils}/time.ts | 6 +++--- src/{libs => utils}/trampoline.ts | 0 src/{libs => utils}/types.ts | 0 36 files changed, 73 insertions(+), 73 deletions(-) rename src/app/{services => llm}/anthropic.ts (92%) rename src/app/{services => llm}/gemini.ts (93%) rename src/app/{services => llm}/llm.ts (80%) rename src/app/{services => llm}/openai.ts (94%) rename src/domain/{provider => llm}/authResolver.ts (93%) rename src/domain/{provider => llm}/responseParser.ts (97%) rename src/{libs => utils}/callable.ts (100%) rename src/{libs => utils}/future.ts (98%) rename src/{libs => utils}/helpers/object.ts (100%) rename src/{libs => utils}/json/decoder.ts (97%) rename src/{libs => utils}/json/encoder.ts (97%) rename src/{libs => utils}/json/schema.ts (94%) rename src/{libs => utils}/json/types.ts (100%) rename src/{libs => utils}/list.ts (98%) rename src/{libs => utils}/maybe.ts (98%) rename src/{libs => utils}/remote-data.ts (97%) rename src/{libs => utils}/result.ts (95%) rename src/{libs => utils}/router.ts (99%) rename src/{libs => utils}/time.ts (98%) rename src/{libs => utils}/trampoline.ts (100%) rename src/{libs => utils}/types.ts (100%) diff --git a/index.ts b/index.ts index 62d8f91..86d1390 100755 --- a/index.ts +++ b/index.ts @@ -3,7 +3,7 @@ import { Setup } from "@/app/commands/setup"; import { Doctor } from "@/app/commands/doctor"; import { ModelCommand } from "@/app/commands/model"; import { parseArgs, showHelp, showVersion } from "@/app/cli"; -import { Future } from "@/libs/future"; +import { Future } from "@/utils/future"; import color from "picocolors"; diff --git a/src/app/cli.ts b/src/app/cli.ts index 1325a9b..6b40773 100644 --- a/src/app/cli.ts +++ b/src/app/cli.ts @@ -1,8 +1,8 @@ export { type CliCommand, parseArgs, showHelp, showVersion }; -import * as D from "@/libs/json/decoder"; +import * as D from "@/utils/json/decoder"; -import { Result } from "@/libs/result"; +import { Result } from "@/utils/result"; type CliCommand = | { type: "generate" } diff --git a/src/app/commands/commit.ts b/src/app/commands/commit.ts index 413557a..6cd21c7 100644 --- a/src/app/commands/commit.ts +++ b/src/app/commands/commit.ts @@ -3,13 +3,13 @@ export { Commit }; import * as p from "@clack/prompts"; import * as repo from "@/lib/git/repo"; -import { Future } from "@/libs/future"; +import { Future } from "@/utils/future"; import { loadConfig } from "@/lib/storage/config"; import { Setup } from "@/app/commands/setup"; import { type CommitConvention, type Config, type ProviderConfig } from "@/domain/config/config"; -import { resolveProvider } from "@/domain/provider/authResolver"; -import { generateCommitMessage, refineCommitMessage } from "@/app/services/llm"; -import { Nothing, type Maybe, Just } from "@/libs/maybe"; +import { resolveProvider } from "@/domain/llm/authResolver"; +import { generateCommitMessage, refineCommitMessage } from "@/app/llm/llm"; +import { Nothing, type Maybe, Just } from "@/utils/maybe"; import { loading } from "@/lib/ui/spinner"; import color from "picocolors"; diff --git a/src/app/commands/doctor.ts b/src/app/commands/doctor.ts index 30cdcda..d368a6a 100644 --- a/src/app/commands/doctor.ts +++ b/src/app/commands/doctor.ts @@ -1,6 +1,6 @@ export { Doctor }; -import { Future } from "@/libs/future"; +import { Future } from "@/utils/future"; import { CONFIG_FILE, loadConfig } from "@/lib/storage/config"; import { type AuthMethod, type ProviderConfig } from "@/domain/config/config"; import { access } from "node:fs/promises"; diff --git a/src/app/commands/model.ts b/src/app/commands/model.ts index c19f336..addf7fc 100644 --- a/src/app/commands/model.ts +++ b/src/app/commands/model.ts @@ -2,10 +2,10 @@ export { ModelCommand }; import * as p from "@clack/prompts"; -import { Future } from "@/libs/future"; +import { Future } from "@/utils/future"; import { type Config, type ProviderConfig } from "@/domain/config/config"; import { loadConfig, saveConfig } from "@/lib/storage/config"; -import { resolveProvider } from "@/domain/provider/authResolver"; +import { resolveProvider } from "@/domain/llm/authResolver"; import { fetchModels, selectModelInteractively } from "@/domain/commit/model"; import { loading } from "@/lib/ui/spinner"; diff --git a/src/app/commands/setup.ts b/src/app/commands/setup.ts index f1e5641..a51aaa2 100644 --- a/src/app/commands/setup.ts +++ b/src/app/commands/setup.ts @@ -3,13 +3,13 @@ export { Setup }; import * as p from "@clack/prompts"; import type { Option } from "@clack/prompts"; -import { Future } from "@/libs/future"; +import { Future } from "@/utils/future"; import { saveConfig } from "@/lib/storage/config"; import { CommitConvention, type Config, type ProviderConfig } from "@/domain/config/config"; import { performOAuthFlow, validateOAuthTokens } from "@/lib/auth/google"; import { performOpenAIOAuthFlow, validateOpenAITokens } from "@/lib/auth/openai"; import { validateAnthropicApiKey, validateAnthropicSetupToken } from "@/lib/auth/anthropic"; -import { Just, Nothing } from "@/libs/maybe"; +import { Just, Nothing } from "@/utils/maybe"; import { loading } from "@/lib/ui/spinner"; import { fetchModels, selectModelInteractively } from "@/domain/commit/model"; diff --git a/src/app/integrations.ts b/src/app/integrations.ts index 02e221d..f7b3925 100644 --- a/src/app/integrations.ts +++ b/src/app/integrations.ts @@ -1,6 +1,6 @@ export { type Environment, environment }; -import * as D from "@/libs/json/decoder"; +import * as D from "@/utils/json/decoder"; type Environment = D.Infer; diff --git a/src/app/services/anthropic.ts b/src/app/llm/anthropic.ts similarity index 92% rename from src/app/services/anthropic.ts rename to src/app/llm/anthropic.ts index f1a14fb..5c6f0d2 100644 --- a/src/app/services/anthropic.ts +++ b/src/app/llm/anthropic.ts @@ -3,11 +3,11 @@ export { generateContentWithAnthropic }; import Anthropic from "@anthropic-ai/sdk"; import { type Config } from "@/domain/config/config"; -import { type GenerateContentParams } from "@/app/services/llm"; -import { Future } from "@/libs/future"; +import { type GenerateContentParams } from "@/app/llm/llm"; +import { Future } from "@/utils/future"; import { anthropicOAuthHeaders, CLAUDE_CODE_SYSTEM_PROMPT } from "@/lib/auth/anthropic"; -import { absurd } from "@/libs/types"; -import { extractResponse } from "@/domain/provider/responseParser"; +import { absurd } from "@/utils/types"; +import { extractResponse } from "@/domain/llm/responseParser"; type AnthropicConfig = Extract; diff --git a/src/app/services/gemini.ts b/src/app/llm/gemini.ts similarity index 93% rename from src/app/services/gemini.ts rename to src/app/llm/gemini.ts index aa0d91d..5190614 100644 --- a/src/app/services/gemini.ts +++ b/src/app/llm/gemini.ts @@ -1,12 +1,12 @@ export { type GeminiAuthCredentials, generateContentWithGemini, getAuthCredentials }; import { GoogleGenerativeAI } from "@google/generative-ai"; -import { Future } from "@/libs/future"; +import { Future } from "@/utils/future"; import { type Config, type OAuthTokens } from "@/domain/config/config"; import { getAccessToken } from "@/lib/auth/google"; -import { Just, Nothing, type Maybe } from "@/libs/maybe"; -import { type GenerateContentParams } from "@/app/services/llm"; -import { extractResponse } from "@/domain/provider/responseParser"; +import { Just, Nothing, type Maybe } from "@/utils/maybe"; +import { type GenerateContentParams } from "@/app/llm/llm"; +import { extractResponse } from "@/domain/llm/responseParser"; type GeminiConfig = Extract; diff --git a/src/app/services/llm.ts b/src/app/llm/llm.ts similarity index 80% rename from src/app/services/llm.ts rename to src/app/llm/llm.ts index 2ce587c..8ab376c 100644 --- a/src/app/services/llm.ts +++ b/src/app/llm/llm.ts @@ -1,12 +1,12 @@ export { type GenerateContentParams, generateCommitMessage, refineCommitMessage }; -import { Future } from "@/libs/future"; +import { Future } from "@/utils/future"; import { type ProviderConfig, type CommitConvention } from "@/domain/config/config"; -import { generateContentWithGemini } from "@/app/services/gemini"; -import { generateContentWithOpenAI } from "@/app/services/openai"; -import { generateContentWithAnthropic } from "@/app/services/anthropic"; +import { generateContentWithGemini } from "@/app/llm/gemini"; +import { generateContentWithOpenAI } from "@/app/llm/openai"; +import { generateContentWithAnthropic } from "@/app/llm/anthropic"; import { getPrompt, getRefinePrompt } from "@/domain/commit/prompts"; -import { Maybe, Nothing } from "@/libs/maybe"; +import { Maybe, Nothing } from "@/utils/maybe"; type GenerateContentParams = { readonly prompt: string; diff --git a/src/app/services/openai.ts b/src/app/llm/openai.ts similarity index 94% rename from src/app/services/openai.ts rename to src/app/llm/openai.ts index 6c608fe..f62b9dd 100644 --- a/src/app/services/openai.ts +++ b/src/app/llm/openai.ts @@ -1,10 +1,10 @@ export { generateContentWithOpenAI }; import { type Config, type OpenAITokens } from "@/domain/config/config"; -import { type GenerateContentParams } from "@/app/services/llm"; -import { Future } from "@/libs/future"; +import { type GenerateContentParams } from "@/app/llm/llm"; +import { Future } from "@/utils/future"; import { getOpenAIAccessToken } from "@/lib/auth/openai"; -import { extractResponse } from "@/domain/provider/responseParser"; +import { extractResponse } from "@/domain/llm/responseParser"; import OpenAI from "openai"; diff --git a/src/domain/commit/model.ts b/src/domain/commit/model.ts index 45287d3..55bd5aa 100644 --- a/src/domain/commit/model.ts +++ b/src/domain/commit/model.ts @@ -1,6 +1,6 @@ export { fetchModels, selectModelInteractively }; -import { Future } from "@/libs/future"; +import { Future } from "@/utils/future"; import { Model, type ProviderConfig } from "@/domain/config/config"; import { getOpenAIAccessToken } from "@/lib/auth/openai"; import { anthropicOAuthHeaders } from "@/lib/auth/anthropic"; diff --git a/src/domain/commit/prompts.ts b/src/domain/commit/prompts.ts index 74107f1..de4a7ee 100644 --- a/src/domain/commit/prompts.ts +++ b/src/domain/commit/prompts.ts @@ -1,8 +1,8 @@ export { getPrompt, getRefinePrompt }; import { CommitConvention } from "@/domain/config/config"; -import { Just, Nothing, type Maybe } from "@/libs/maybe"; -import { absurd } from "@/libs/types"; +import { Just, Nothing, type Maybe } from "@/utils/maybe"; +import { absurd } from "@/utils/types"; function getPrompt(diff: string, convention: CommitConvention, customTemplate: Maybe = Nothing()): string { switch (convention) { diff --git a/src/domain/config/config.ts b/src/domain/config/config.ts index 4715c2c..86e9752 100644 --- a/src/domain/config/config.ts +++ b/src/domain/config/config.ts @@ -15,7 +15,7 @@ export { AI_PROVIDERS }; -import * as s from "@/libs/json/schema"; +import * as s from "@/utils/json/schema"; const COMMIT_CONVENTIONS = ["conventional", "imperative", "custom"] as const; type CommitConvention = (typeof COMMIT_CONVENTIONS)[number]; diff --git a/src/domain/provider/authResolver.ts b/src/domain/llm/authResolver.ts similarity index 93% rename from src/domain/provider/authResolver.ts rename to src/domain/llm/authResolver.ts index 27ea03e..a31587a 100644 --- a/src/domain/provider/authResolver.ts +++ b/src/domain/llm/authResolver.ts @@ -1,12 +1,12 @@ export { resolveProvider }; -import { Future } from "@/libs/future"; -import { Just, Nothing, type Maybe } from "@/libs/maybe"; +import { Future } from "@/utils/future"; +import { Just, Nothing, type Maybe } from "@/utils/maybe"; import { type Config, type ProviderConfig, type RefreshTokens } from "@/domain/config/config"; import { ensureFreshTokens } from "@/lib/auth/google"; import { ensureFreshOpenAITokens } from "@/lib/auth/openai"; import { updateGoogleTokens, updateOpenAITokens } from "@/lib/storage/config"; -import { absurd } from "@/libs/types"; +import { absurd } from "@/utils/types"; type DetectTokenChange = (original: T, fresh: T) => Maybe; type RefreshProvider = (tokens: T) => Future; diff --git a/src/domain/provider/responseParser.ts b/src/domain/llm/responseParser.ts similarity index 97% rename from src/domain/provider/responseParser.ts rename to src/domain/llm/responseParser.ts index 37b0ac3..0d3d7f5 100644 --- a/src/domain/provider/responseParser.ts +++ b/src/domain/llm/responseParser.ts @@ -1,7 +1,7 @@ export { type RawResponse, extractResponse, finalizeText }; -import { Future } from "@/libs/future"; -import { absurd } from "@/libs/types"; +import { Future } from "@/utils/future"; +import { absurd } from "@/utils/types"; const EMPTY_RESPONSE_ERROR = "Empty AI response"; diff --git a/src/lib/auth/google.ts b/src/lib/auth/google.ts index f950d28..541adde 100644 --- a/src/lib/auth/google.ts +++ b/src/lib/auth/google.ts @@ -3,7 +3,7 @@ export { performOAuthFlow, createAuthenticatedClient, ensureFreshTokens, validat import { type OAuthTokens } from "@/domain/config/config"; import { SUCCESS_HTML, ERROR_HTML } from "@/lib/auth/templates"; import { OAuth2Client, CodeChallengeMethod } from "google-auth-library"; -import { Future } from "@/libs/future"; +import { Future } from "@/utils/future"; import { environment } from "@/app/integrations"; import { randomBytes, createHash } from "node:crypto"; import { createServer, type Server } from "node:http"; diff --git a/src/lib/auth/openai.ts b/src/lib/auth/openai.ts index 890c0fe..105a100 100644 --- a/src/lib/auth/openai.ts +++ b/src/lib/auth/openai.ts @@ -2,7 +2,7 @@ export { performOpenAIOAuthFlow, ensureFreshOpenAITokens, validateOpenAITokens, import { type OpenAITokens } from "@/domain/config/config"; import { SUCCESS_HTML, ERROR_HTML } from "@/lib/auth/templates"; -import { Future } from "@/libs/future"; +import { Future } from "@/utils/future"; import { randomBytes, createHash } from "node:crypto"; import { createServer, type Server } from "node:http"; diff --git a/src/lib/git/repo.ts b/src/lib/git/repo.ts index a6adf23..8677911 100644 --- a/src/lib/git/repo.ts +++ b/src/lib/git/repo.ts @@ -1,6 +1,6 @@ export { checkIsGitRepo, getStagedDiff, performCommit, performPush, getCurrentBranch, hasUpstream }; -import { Future } from "@/libs/future"; +import { Future } from "@/utils/future"; import { spawn } from "node:child_process"; import { unlink, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; diff --git a/src/lib/storage/config.ts b/src/lib/storage/config.ts index fee888d..824094a 100644 --- a/src/lib/storage/config.ts +++ b/src/lib/storage/config.ts @@ -1,13 +1,13 @@ export { loadConfig, saveConfig, updateGoogleTokens, updateOpenAITokens, CONFIG_DIR, CONFIG_FILE }; -import * as s from "@/libs/json/schema"; +import * as s from "@/utils/json/schema"; -import { Future } from "@/libs/future"; +import { Future } from "@/utils/future"; import { resolve } from "node:path"; import { homedir } from "node:os"; import { readFile, writeFile, mkdir } from "node:fs/promises"; import { Config, type OAuthTokens, type OpenAITokens } from "@/domain/config/config"; -import { Just, Nothing, type Maybe } from "@/libs/maybe"; +import { Just, Nothing, type Maybe } from "@/utils/maybe"; const CONFIG_DIR = resolve(homedir(), ".commit-tools"); const CONFIG_FILE = resolve(CONFIG_DIR, "config.json"); diff --git a/src/lib/ui/spinner.ts b/src/lib/ui/spinner.ts index 49b0560..5df6d7d 100644 --- a/src/lib/ui/spinner.ts +++ b/src/lib/ui/spinner.ts @@ -2,7 +2,7 @@ export { loading }; import * as p from "@clack/prompts"; -import { Future } from "@/libs/future"; +import { Future } from "@/utils/future"; const loading = (label: string, stopLabel: string, f: Future): Future => { const s = p.spinner(); diff --git a/src/libs/callable.ts b/src/utils/callable.ts similarity index 100% rename from src/libs/callable.ts rename to src/utils/callable.ts diff --git a/src/libs/future.ts b/src/utils/future.ts similarity index 98% rename from src/libs/future.ts rename to src/utils/future.ts index 30c673d..91ef195 100644 --- a/src/libs/future.ts +++ b/src/utils/future.ts @@ -2,7 +2,7 @@ export { Future, type Cancel }; import * as F from "fluture"; -import { Result, Success, Failure } from "@/libs/result"; +import { Result, Success, Failure } from "@/utils/result"; import { FutureInstance } from "fluture"; type Cancel = () => void; diff --git a/src/libs/helpers/object.ts b/src/utils/helpers/object.ts similarity index 100% rename from src/libs/helpers/object.ts rename to src/utils/helpers/object.ts diff --git a/src/libs/json/decoder.ts b/src/utils/json/decoder.ts similarity index 97% rename from src/libs/json/decoder.ts rename to src/utils/json/decoder.ts index 699961a..3e9323f 100644 --- a/src/libs/json/decoder.ts +++ b/src/utils/json/decoder.ts @@ -62,10 +62,10 @@ export { recursive }; -import { Result, Success, Failure, traverse } from "@/libs/result"; -import { Maybe, Just, Nothing, Nullable } from "@/libs/maybe"; -import { List } from "@/libs/list"; -import { Json } from "@/libs/json/types"; +import { Result, Success, Failure, traverse } from "@/utils/result"; +import { Maybe, Just, Nothing, Nullable } from "@/utils/maybe"; +import { List } from "@/utils/list"; +import { Json } from "@/utils/json/types"; // Infer the type from a decoder definition type Infer> = A extends Decoder ? B : never; diff --git a/src/libs/json/encoder.ts b/src/utils/json/encoder.ts similarity index 97% rename from src/libs/json/encoder.ts rename to src/utils/json/encoder.ts index d9e9c18..0d92746 100644 --- a/src/libs/json/encoder.ts +++ b/src/utils/json/encoder.ts @@ -27,8 +27,8 @@ export { recursive }; -import { Maybe, Nothing, Just, Nullable } from "@/libs/maybe"; -import { Json, JsonObject } from "@/libs/json/types"; +import { Maybe, Nothing, Just, Nullable } from "@/utils/maybe"; +import { Json, JsonObject } from "@/utils/json/types"; // Infer the type from a encoder definition // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/libs/json/schema.ts b/src/utils/json/schema.ts similarity index 94% rename from src/libs/json/schema.ts rename to src/utils/json/schema.ts index d628b7f..968b42a 100644 --- a/src/libs/json/schema.ts +++ b/src/utils/json/schema.ts @@ -37,16 +37,16 @@ export { recursive }; -import * as decoder from "@/libs/json/decoder"; -import * as encoder from "@/libs/json/encoder"; -import { Result } from "@/libs/result"; -import { Decoder, DecoderDef } from "@/libs/json/decoder"; -import * as D from "@/libs/json/decoder"; -import { Encoder, EncoderDef } from "@/libs/json/encoder"; -import { Json } from "@/libs/json/types"; -import * as E from "@/libs/json/encoder"; -import { Maybe, Nullable } from "@/libs/maybe"; -import { filterMap, mapValues } from "@/libs/helpers/object"; +import * as decoder from "@/utils/json/decoder"; +import * as encoder from "@/utils/json/encoder"; +import { Result } from "@/utils/result"; +import { Decoder, DecoderDef } from "@/utils/json/decoder"; +import * as D from "@/utils/json/decoder"; +import { Encoder, EncoderDef } from "@/utils/json/encoder"; +import { Json } from "@/utils/json/types"; +import * as E from "@/utils/json/encoder"; +import { Maybe, Nullable } from "@/utils/maybe"; +import { filterMap, mapValues } from "@/utils/helpers/object"; // Infer the type from a schema definition // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/libs/json/types.ts b/src/utils/json/types.ts similarity index 100% rename from src/libs/json/types.ts rename to src/utils/json/types.ts diff --git a/src/libs/list.ts b/src/utils/list.ts similarity index 98% rename from src/libs/list.ts rename to src/utils/list.ts index 26facc4..3b280fe 100644 --- a/src/libs/list.ts +++ b/src/utils/list.ts @@ -1,6 +1,6 @@ export { List }; -import { Maybe, Just, Nothing } from "@/libs/maybe"; +import { Maybe, Just, Nothing } from "@/utils/maybe"; type Content = { head: T; tail: List } | { empty: null }; diff --git a/src/libs/maybe.ts b/src/utils/maybe.ts similarity index 98% rename from src/libs/maybe.ts rename to src/utils/maybe.ts index 68f6283..237ceb8 100644 --- a/src/libs/maybe.ts +++ b/src/utils/maybe.ts @@ -26,7 +26,7 @@ export { mapMaybe }; -import Callable from "@/libs/callable"; +import Callable from "@/utils/callable"; type Maybe = Just | Nothing; type Nullable = T | null; diff --git a/src/libs/remote-data.ts b/src/utils/remote-data.ts similarity index 97% rename from src/libs/remote-data.ts rename to src/utils/remote-data.ts index ff875df..8a68e7e 100644 --- a/src/libs/remote-data.ts +++ b/src/utils/remote-data.ts @@ -6,8 +6,8 @@ export { CallableLoading as Loading }; -import { Nullable, Maybe, Nothing, Just } from "@/libs/maybe"; -import Callable from "@/libs/callable"; +import { Nullable, Maybe, Nothing, Just } from "@/utils/maybe"; +import Callable from "@/utils/callable"; type RemoteData = NotAsked | Loading | Failed | Ready; diff --git a/src/libs/result.ts b/src/utils/result.ts similarity index 95% rename from src/libs/result.ts rename to src/utils/result.ts index 79a6cd0..510d670 100644 --- a/src/libs/result.ts +++ b/src/utils/result.ts @@ -1,9 +1,9 @@ export { type Result, CallableSuccess as Success, CallableFailure as Failure, traverse, traverse_ }; -import { Trampoline, end, tailRecursive } from "@/libs/trampoline"; -import { List } from "@/libs/list"; +import { Trampoline, end, tailRecursive } from "@/utils/trampoline"; +import { List } from "@/utils/list"; -import Callable from "@/libs/callable"; +import Callable from "@/utils/callable"; type Result = Success | Failure; diff --git a/src/libs/router.ts b/src/utils/router.ts similarity index 99% rename from src/libs/router.ts rename to src/utils/router.ts index 22088ec..3b05817 100644 --- a/src/libs/router.ts +++ b/src/utils/router.ts @@ -40,7 +40,7 @@ export { }; import * as express from "express"; -import { Cancel, Future } from "@/libs/future"; +import { Cancel, Future } from "@/utils/future"; type Json = null | string | number | boolean | JsonArray | JsonObject; type JsonObject = { [x: string]: Json }; diff --git a/src/libs/time.ts b/src/utils/time.ts similarity index 98% rename from src/libs/time.ts rename to src/utils/time.ts index 8356731..2e656fe 100644 --- a/src/libs/time.ts +++ b/src/utils/time.ts @@ -1,8 +1,8 @@ export { type Timezone, DateOnly, TimeOfDay, POSIX, Duration }; -import * as s from "@/libs/json/schema"; -import { fail, always } from "@/libs/json/decoder"; -import { type Maybe, Just, Nothing } from "@/libs/maybe"; +import * as s from "@/utils/json/schema"; +import { fail, always } from "@/utils/json/decoder"; +import { type Maybe, Just, Nothing } from "@/utils/maybe"; import { DateTime } from "luxon"; diff --git a/src/libs/trampoline.ts b/src/utils/trampoline.ts similarity index 100% rename from src/libs/trampoline.ts rename to src/utils/trampoline.ts diff --git a/src/libs/types.ts b/src/utils/types.ts similarity index 100% rename from src/libs/types.ts rename to src/utils/types.ts From 06da958c63eefa5d8df88a3e1710e572702e1abb Mon Sep 17 00:00:00 2001 From: rafaeelricco Date: Sat, 11 Apr 2026 12:56:22 -0300 Subject: [PATCH 04/11] Ignore `.claude` directory in gitignore --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 36d0bb4..50eaed8 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,7 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json dist # Temporary files -/tmp \ No newline at end of file +/tmp + +# Claude folder +.claude \ No newline at end of file From c5d19834c9ce840ec61e3840353bd41e6de333c2 Mon Sep 17 00:00:00 2001 From: rafaeelricco Date: Sat, 11 Apr 2026 13:38:32 -0300 Subject: [PATCH 05/11] Restructure project layout into cli, domain, infra, and libs - Move command entry points from `src/app/commands` to `src/cli` and rename `cli.ts` to `cli/parser.ts`. - Relocate infrastructure code (auth, git, llm providers, storage, ui, env) from `src/app` and `src/lib` into `src/infra`. - Rename `src/utils` to `src/libs` and update all related imports across the codebase. - Move LLM routing to `src/domain/llm/router.ts` and rename `domain/commit/model.ts` to `models.ts`. - Extract `selectModelInteractively` into new `src/infra/ui/model-picker.ts` and move `model-selector` component under `src/infra/ui`. - Update all `@/` path imports throughout `index.ts` and source files to reflect the new structure. --- index.ts | 12 +++---- src/{app/commands => cli}/commit.ts | 14 ++++----- src/{app/commands => cli}/doctor.ts | 6 ++-- src/{app/commands => cli}/model.ts | 9 +++--- src/{app/cli.ts => cli/parser.ts} | 4 +-- src/{app/commands => cli}/setup.ts | 17 +++++----- src/domain/commit/{model.ts => models.ts} | 31 +++---------------- src/domain/commit/prompts.ts | 4 +-- src/domain/config/config.ts | 2 +- src/domain/llm/authResolver.ts | 12 +++---- src/domain/llm/responseParser.ts | 4 +-- src/{app/llm/llm.ts => domain/llm/router.ts} | 10 +++--- src/{lib => infra}/auth/anthropic.ts | 0 src/{lib => infra}/auth/google.ts | 6 ++-- src/{lib => infra}/auth/openai.ts | 4 +-- src/{lib => infra}/auth/templates.ts | 0 src/{app/integrations.ts => infra/env.ts} | 2 +- src/{lib => infra}/git/repo.ts | 2 +- src/{app => infra}/llm/anthropic.ts | 8 ++--- src/{app => infra}/llm/gemini.ts | 8 ++--- src/{app => infra}/llm/openai.ts | 6 ++-- src/{lib => infra}/storage/config.ts | 6 ++-- src/infra/ui/model-picker.ts | 27 ++++++++++++++++ .../ui}/model-selector.tsx | 0 src/{lib => infra}/ui/spinner.ts | 2 +- src/{utils => libs}/callable.ts | 0 src/{utils => libs}/future.ts | 2 +- src/{utils => libs}/helpers/object.ts | 0 src/{utils => libs}/json/decoder.ts | 8 ++--- src/{utils => libs}/json/encoder.ts | 4 +-- src/{utils => libs}/json/schema.ts | 20 ++++++------ src/{utils => libs}/json/types.ts | 0 src/{utils => libs}/list.ts | 2 +- src/{utils => libs}/maybe.ts | 2 +- src/{utils => libs}/remote-data.ts | 4 +-- src/{utils => libs}/result.ts | 6 ++-- src/{utils => libs}/router.ts | 2 +- src/{utils => libs}/time.ts | 6 ++-- src/{utils => libs}/trampoline.ts | 0 src/{utils => libs}/types.ts | 0 40 files changed, 129 insertions(+), 123 deletions(-) rename src/{app/commands => cli}/commit.ts (94%) rename src/{app/commands => cli}/doctor.ts (96%) rename src/{app/commands => cli}/model.ts (83%) rename src/{app/cli.ts => cli/parser.ts} (95%) rename src/{app/commands => cli}/setup.ts (94%) rename src/domain/commit/{model.ts => models.ts} (83%) rename src/{app/llm/llm.ts => domain/llm/router.ts} (80%) rename src/{lib => infra}/auth/anthropic.ts (100%) rename src/{lib => infra}/auth/google.ts (98%) rename src/{lib => infra}/auth/openai.ts (98%) rename src/{lib => infra}/auth/templates.ts (100%) rename src/{app/integrations.ts => infra/env.ts} (91%) rename src/{lib => infra}/git/repo.ts (98%) rename src/{app => infra}/llm/anthropic.ts (93%) rename src/{app => infra}/llm/gemini.ts (94%) rename src/{app => infra}/llm/openai.ts (94%) rename src/{lib => infra}/storage/config.ts (94%) create mode 100644 src/infra/ui/model-picker.ts rename src/{lib/ui/components => infra/ui}/model-selector.tsx (100%) rename src/{lib => infra}/ui/spinner.ts (89%) rename src/{utils => libs}/callable.ts (100%) rename src/{utils => libs}/future.ts (98%) rename src/{utils => libs}/helpers/object.ts (100%) rename src/{utils => libs}/json/decoder.ts (97%) rename src/{utils => libs}/json/encoder.ts (97%) rename src/{utils => libs}/json/schema.ts (94%) rename src/{utils => libs}/json/types.ts (100%) rename src/{utils => libs}/list.ts (98%) rename src/{utils => libs}/maybe.ts (98%) rename src/{utils => libs}/remote-data.ts (97%) rename src/{utils => libs}/result.ts (95%) rename src/{utils => libs}/router.ts (99%) rename src/{utils => libs}/time.ts (98%) rename src/{utils => libs}/trampoline.ts (100%) rename src/{utils => libs}/types.ts (100%) diff --git a/index.ts b/index.ts index 86d1390..059c2b7 100755 --- a/index.ts +++ b/index.ts @@ -1,9 +1,9 @@ -import { Commit } from "@/app/commands/commit"; -import { Setup } from "@/app/commands/setup"; -import { Doctor } from "@/app/commands/doctor"; -import { ModelCommand } from "@/app/commands/model"; -import { parseArgs, showHelp, showVersion } from "@/app/cli"; -import { Future } from "@/utils/future"; +import { Commit } from "@/cli/commit"; +import { Setup } from "@/cli/setup"; +import { Doctor } from "@/cli/doctor"; +import { ModelCommand } from "@/cli/model"; +import { parseArgs, showHelp, showVersion } from "@/cli/parser"; +import { Future } from "@/libs/future"; import color from "picocolors"; diff --git a/src/app/commands/commit.ts b/src/cli/commit.ts similarity index 94% rename from src/app/commands/commit.ts rename to src/cli/commit.ts index 6cd21c7..55ebc4a 100644 --- a/src/app/commands/commit.ts +++ b/src/cli/commit.ts @@ -1,16 +1,16 @@ export { Commit }; import * as p from "@clack/prompts"; -import * as repo from "@/lib/git/repo"; +import * as repo from "@/infra/git/repo"; -import { Future } from "@/utils/future"; -import { loadConfig } from "@/lib/storage/config"; -import { Setup } from "@/app/commands/setup"; +import { Future } from "@/libs/future"; +import { loadConfig } from "@/infra/storage/config"; +import { Setup } from "@/cli/setup"; import { type CommitConvention, type Config, type ProviderConfig } from "@/domain/config/config"; import { resolveProvider } from "@/domain/llm/authResolver"; -import { generateCommitMessage, refineCommitMessage } from "@/app/llm/llm"; -import { Nothing, type Maybe, Just } from "@/utils/maybe"; -import { loading } from "@/lib/ui/spinner"; +import { generateCommitMessage, refineCommitMessage } from "@/domain/llm/router"; +import { Nothing, type Maybe, Just } from "@/libs/maybe"; +import { loading } from "@/infra/ui/spinner"; import color from "picocolors"; diff --git a/src/app/commands/doctor.ts b/src/cli/doctor.ts similarity index 96% rename from src/app/commands/doctor.ts rename to src/cli/doctor.ts index d368a6a..60b9ea3 100644 --- a/src/app/commands/doctor.ts +++ b/src/cli/doctor.ts @@ -1,10 +1,10 @@ export { Doctor }; -import { Future } from "@/utils/future"; -import { CONFIG_FILE, loadConfig } from "@/lib/storage/config"; +import { Future } from "@/libs/future"; +import { CONFIG_FILE, loadConfig } from "@/infra/storage/config"; import { type AuthMethod, type ProviderConfig } from "@/domain/config/config"; import { access } from "node:fs/promises"; -import { environment } from "@/app/integrations"; +import { environment } from "@/infra/env"; import color from "picocolors"; import Table from "cli-table3"; diff --git a/src/app/commands/model.ts b/src/cli/model.ts similarity index 83% rename from src/app/commands/model.ts rename to src/cli/model.ts index addf7fc..b61e154 100644 --- a/src/app/commands/model.ts +++ b/src/cli/model.ts @@ -2,12 +2,13 @@ export { ModelCommand }; import * as p from "@clack/prompts"; -import { Future } from "@/utils/future"; +import { Future } from "@/libs/future"; import { type Config, type ProviderConfig } from "@/domain/config/config"; -import { loadConfig, saveConfig } from "@/lib/storage/config"; +import { loadConfig, saveConfig } from "@/infra/storage/config"; import { resolveProvider } from "@/domain/llm/authResolver"; -import { fetchModels, selectModelInteractively } from "@/domain/commit/model"; -import { loading } from "@/lib/ui/spinner"; +import { fetchModels } from "@/domain/commit/models"; +import { selectModelInteractively } from "@/infra/ui/model-picker"; +import { loading } from "@/infra/ui/spinner"; import color from "picocolors"; diff --git a/src/app/cli.ts b/src/cli/parser.ts similarity index 95% rename from src/app/cli.ts rename to src/cli/parser.ts index 6b40773..1325a9b 100644 --- a/src/app/cli.ts +++ b/src/cli/parser.ts @@ -1,8 +1,8 @@ export { type CliCommand, parseArgs, showHelp, showVersion }; -import * as D from "@/utils/json/decoder"; +import * as D from "@/libs/json/decoder"; -import { Result } from "@/utils/result"; +import { Result } from "@/libs/result"; type CliCommand = | { type: "generate" } diff --git a/src/app/commands/setup.ts b/src/cli/setup.ts similarity index 94% rename from src/app/commands/setup.ts rename to src/cli/setup.ts index a51aaa2..d956470 100644 --- a/src/app/commands/setup.ts +++ b/src/cli/setup.ts @@ -3,15 +3,16 @@ export { Setup }; import * as p from "@clack/prompts"; import type { Option } from "@clack/prompts"; -import { Future } from "@/utils/future"; -import { saveConfig } from "@/lib/storage/config"; +import { Future } from "@/libs/future"; +import { saveConfig } from "@/infra/storage/config"; import { CommitConvention, type Config, type ProviderConfig } from "@/domain/config/config"; -import { performOAuthFlow, validateOAuthTokens } from "@/lib/auth/google"; -import { performOpenAIOAuthFlow, validateOpenAITokens } from "@/lib/auth/openai"; -import { validateAnthropicApiKey, validateAnthropicSetupToken } from "@/lib/auth/anthropic"; -import { Just, Nothing } from "@/utils/maybe"; -import { loading } from "@/lib/ui/spinner"; -import { fetchModels, selectModelInteractively } from "@/domain/commit/model"; +import { performOAuthFlow, validateOAuthTokens } from "@/infra/auth/google"; +import { performOpenAIOAuthFlow, validateOpenAITokens } from "@/infra/auth/openai"; +import { validateAnthropicApiKey, validateAnthropicSetupToken } from "@/infra/auth/anthropic"; +import { Just, Nothing } from "@/libs/maybe"; +import { loading } from "@/infra/ui/spinner"; +import { fetchModels } from "@/domain/commit/models"; +import { selectModelInteractively } from "@/infra/ui/model-picker"; import color from "picocolors"; diff --git a/src/domain/commit/model.ts b/src/domain/commit/models.ts similarity index 83% rename from src/domain/commit/model.ts rename to src/domain/commit/models.ts index 55bd5aa..c396680 100644 --- a/src/domain/commit/model.ts +++ b/src/domain/commit/models.ts @@ -1,9 +1,9 @@ -export { fetchModels, selectModelInteractively }; +export { fetchModels }; -import { Future } from "@/utils/future"; +import { Future } from "@/libs/future"; import { Model, type ProviderConfig } from "@/domain/config/config"; -import { getOpenAIAccessToken } from "@/lib/auth/openai"; -import { anthropicOAuthHeaders } from "@/lib/auth/anthropic"; +import { getOpenAIAccessToken } from "@/infra/auth/openai"; +import { anthropicOAuthHeaders } from "@/infra/auth/anthropic"; import OpenAI from "openai"; @@ -130,26 +130,3 @@ const fetchModels = ( return fetchAnthropicModels(authMethod); } }; - -const selectModelInteractively = (models: Model[]): Future => - Future.attemptP(async () => { - const { render } = await import("ink"); - const React = await import("react"); - const { ModelSelector } = await import("@/lib/ui/components/model-selector"); - - return new Promise((resolve, reject) => { - const { unmount } = render( - React.createElement(ModelSelector, { - models, - onSelect: (modelId: string) => { - unmount(); - resolve(modelId); - }, - onCancel: () => { - unmount(); - reject(new Error("Selection cancelled")); - } - }) - ); - }); - }); diff --git a/src/domain/commit/prompts.ts b/src/domain/commit/prompts.ts index de4a7ee..74107f1 100644 --- a/src/domain/commit/prompts.ts +++ b/src/domain/commit/prompts.ts @@ -1,8 +1,8 @@ export { getPrompt, getRefinePrompt }; import { CommitConvention } from "@/domain/config/config"; -import { Just, Nothing, type Maybe } from "@/utils/maybe"; -import { absurd } from "@/utils/types"; +import { Just, Nothing, type Maybe } from "@/libs/maybe"; +import { absurd } from "@/libs/types"; function getPrompt(diff: string, convention: CommitConvention, customTemplate: Maybe = Nothing()): string { switch (convention) { diff --git a/src/domain/config/config.ts b/src/domain/config/config.ts index 86e9752..4715c2c 100644 --- a/src/domain/config/config.ts +++ b/src/domain/config/config.ts @@ -15,7 +15,7 @@ export { AI_PROVIDERS }; -import * as s from "@/utils/json/schema"; +import * as s from "@/libs/json/schema"; const COMMIT_CONVENTIONS = ["conventional", "imperative", "custom"] as const; type CommitConvention = (typeof COMMIT_CONVENTIONS)[number]; diff --git a/src/domain/llm/authResolver.ts b/src/domain/llm/authResolver.ts index a31587a..c73328f 100644 --- a/src/domain/llm/authResolver.ts +++ b/src/domain/llm/authResolver.ts @@ -1,12 +1,12 @@ export { resolveProvider }; -import { Future } from "@/utils/future"; -import { Just, Nothing, type Maybe } from "@/utils/maybe"; +import { Future } from "@/libs/future"; +import { Just, Nothing, type Maybe } from "@/libs/maybe"; import { type Config, type ProviderConfig, type RefreshTokens } from "@/domain/config/config"; -import { ensureFreshTokens } from "@/lib/auth/google"; -import { ensureFreshOpenAITokens } from "@/lib/auth/openai"; -import { updateGoogleTokens, updateOpenAITokens } from "@/lib/storage/config"; -import { absurd } from "@/utils/types"; +import { ensureFreshTokens } from "@/infra/auth/google"; +import { ensureFreshOpenAITokens } from "@/infra/auth/openai"; +import { updateGoogleTokens, updateOpenAITokens } from "@/infra/storage/config"; +import { absurd } from "@/libs/types"; type DetectTokenChange = (original: T, fresh: T) => Maybe; type RefreshProvider = (tokens: T) => Future; diff --git a/src/domain/llm/responseParser.ts b/src/domain/llm/responseParser.ts index 0d3d7f5..37b0ac3 100644 --- a/src/domain/llm/responseParser.ts +++ b/src/domain/llm/responseParser.ts @@ -1,7 +1,7 @@ export { type RawResponse, extractResponse, finalizeText }; -import { Future } from "@/utils/future"; -import { absurd } from "@/utils/types"; +import { Future } from "@/libs/future"; +import { absurd } from "@/libs/types"; const EMPTY_RESPONSE_ERROR = "Empty AI response"; diff --git a/src/app/llm/llm.ts b/src/domain/llm/router.ts similarity index 80% rename from src/app/llm/llm.ts rename to src/domain/llm/router.ts index 8ab376c..ddaa7e8 100644 --- a/src/app/llm/llm.ts +++ b/src/domain/llm/router.ts @@ -1,12 +1,12 @@ export { type GenerateContentParams, generateCommitMessage, refineCommitMessage }; -import { Future } from "@/utils/future"; +import { Future } from "@/libs/future"; import { type ProviderConfig, type CommitConvention } from "@/domain/config/config"; -import { generateContentWithGemini } from "@/app/llm/gemini"; -import { generateContentWithOpenAI } from "@/app/llm/openai"; -import { generateContentWithAnthropic } from "@/app/llm/anthropic"; +import { generateContentWithGemini } from "@/infra/llm/gemini"; +import { generateContentWithOpenAI } from "@/infra/llm/openai"; +import { generateContentWithAnthropic } from "@/infra/llm/anthropic"; import { getPrompt, getRefinePrompt } from "@/domain/commit/prompts"; -import { Maybe, Nothing } from "@/utils/maybe"; +import { Maybe, Nothing } from "@/libs/maybe"; type GenerateContentParams = { readonly prompt: string; diff --git a/src/lib/auth/anthropic.ts b/src/infra/auth/anthropic.ts similarity index 100% rename from src/lib/auth/anthropic.ts rename to src/infra/auth/anthropic.ts diff --git a/src/lib/auth/google.ts b/src/infra/auth/google.ts similarity index 98% rename from src/lib/auth/google.ts rename to src/infra/auth/google.ts index 541adde..633718f 100644 --- a/src/lib/auth/google.ts +++ b/src/infra/auth/google.ts @@ -1,10 +1,10 @@ export { performOAuthFlow, createAuthenticatedClient, ensureFreshTokens, validateOAuthTokens, getAccessToken }; import { type OAuthTokens } from "@/domain/config/config"; -import { SUCCESS_HTML, ERROR_HTML } from "@/lib/auth/templates"; +import { SUCCESS_HTML, ERROR_HTML } from "@/infra/auth/templates"; import { OAuth2Client, CodeChallengeMethod } from "google-auth-library"; -import { Future } from "@/utils/future"; -import { environment } from "@/app/integrations"; +import { Future } from "@/libs/future"; +import { environment } from "@/infra/env"; import { randomBytes, createHash } from "node:crypto"; import { createServer, type Server } from "node:http"; diff --git a/src/lib/auth/openai.ts b/src/infra/auth/openai.ts similarity index 98% rename from src/lib/auth/openai.ts rename to src/infra/auth/openai.ts index 105a100..3e261c4 100644 --- a/src/lib/auth/openai.ts +++ b/src/infra/auth/openai.ts @@ -1,8 +1,8 @@ export { performOpenAIOAuthFlow, ensureFreshOpenAITokens, validateOpenAITokens, getOpenAIAccessToken }; import { type OpenAITokens } from "@/domain/config/config"; -import { SUCCESS_HTML, ERROR_HTML } from "@/lib/auth/templates"; -import { Future } from "@/utils/future"; +import { SUCCESS_HTML, ERROR_HTML } from "@/infra/auth/templates"; +import { Future } from "@/libs/future"; import { randomBytes, createHash } from "node:crypto"; import { createServer, type Server } from "node:http"; diff --git a/src/lib/auth/templates.ts b/src/infra/auth/templates.ts similarity index 100% rename from src/lib/auth/templates.ts rename to src/infra/auth/templates.ts diff --git a/src/app/integrations.ts b/src/infra/env.ts similarity index 91% rename from src/app/integrations.ts rename to src/infra/env.ts index f7b3925..02e221d 100644 --- a/src/app/integrations.ts +++ b/src/infra/env.ts @@ -1,6 +1,6 @@ export { type Environment, environment }; -import * as D from "@/utils/json/decoder"; +import * as D from "@/libs/json/decoder"; type Environment = D.Infer; diff --git a/src/lib/git/repo.ts b/src/infra/git/repo.ts similarity index 98% rename from src/lib/git/repo.ts rename to src/infra/git/repo.ts index 8677911..a6adf23 100644 --- a/src/lib/git/repo.ts +++ b/src/infra/git/repo.ts @@ -1,6 +1,6 @@ export { checkIsGitRepo, getStagedDiff, performCommit, performPush, getCurrentBranch, hasUpstream }; -import { Future } from "@/utils/future"; +import { Future } from "@/libs/future"; import { spawn } from "node:child_process"; import { unlink, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; diff --git a/src/app/llm/anthropic.ts b/src/infra/llm/anthropic.ts similarity index 93% rename from src/app/llm/anthropic.ts rename to src/infra/llm/anthropic.ts index 5c6f0d2..a83deec 100644 --- a/src/app/llm/anthropic.ts +++ b/src/infra/llm/anthropic.ts @@ -3,10 +3,10 @@ export { generateContentWithAnthropic }; import Anthropic from "@anthropic-ai/sdk"; import { type Config } from "@/domain/config/config"; -import { type GenerateContentParams } from "@/app/llm/llm"; -import { Future } from "@/utils/future"; -import { anthropicOAuthHeaders, CLAUDE_CODE_SYSTEM_PROMPT } from "@/lib/auth/anthropic"; -import { absurd } from "@/utils/types"; +import { type GenerateContentParams } from "@/domain/llm/router"; +import { Future } from "@/libs/future"; +import { anthropicOAuthHeaders, CLAUDE_CODE_SYSTEM_PROMPT } from "@/infra/auth/anthropic"; +import { absurd } from "@/libs/types"; import { extractResponse } from "@/domain/llm/responseParser"; type AnthropicConfig = Extract; diff --git a/src/app/llm/gemini.ts b/src/infra/llm/gemini.ts similarity index 94% rename from src/app/llm/gemini.ts rename to src/infra/llm/gemini.ts index 5190614..437e148 100644 --- a/src/app/llm/gemini.ts +++ b/src/infra/llm/gemini.ts @@ -1,11 +1,11 @@ export { type GeminiAuthCredentials, generateContentWithGemini, getAuthCredentials }; import { GoogleGenerativeAI } from "@google/generative-ai"; -import { Future } from "@/utils/future"; +import { Future } from "@/libs/future"; import { type Config, type OAuthTokens } from "@/domain/config/config"; -import { getAccessToken } from "@/lib/auth/google"; -import { Just, Nothing, type Maybe } from "@/utils/maybe"; -import { type GenerateContentParams } from "@/app/llm/llm"; +import { getAccessToken } from "@/infra/auth/google"; +import { Just, Nothing, type Maybe } from "@/libs/maybe"; +import { type GenerateContentParams } from "@/domain/llm/router"; import { extractResponse } from "@/domain/llm/responseParser"; type GeminiConfig = Extract; diff --git a/src/app/llm/openai.ts b/src/infra/llm/openai.ts similarity index 94% rename from src/app/llm/openai.ts rename to src/infra/llm/openai.ts index f62b9dd..dcb1914 100644 --- a/src/app/llm/openai.ts +++ b/src/infra/llm/openai.ts @@ -1,9 +1,9 @@ export { generateContentWithOpenAI }; import { type Config, type OpenAITokens } from "@/domain/config/config"; -import { type GenerateContentParams } from "@/app/llm/llm"; -import { Future } from "@/utils/future"; -import { getOpenAIAccessToken } from "@/lib/auth/openai"; +import { type GenerateContentParams } from "@/domain/llm/router"; +import { Future } from "@/libs/future"; +import { getOpenAIAccessToken } from "@/infra/auth/openai"; import { extractResponse } from "@/domain/llm/responseParser"; import OpenAI from "openai"; diff --git a/src/lib/storage/config.ts b/src/infra/storage/config.ts similarity index 94% rename from src/lib/storage/config.ts rename to src/infra/storage/config.ts index 824094a..fee888d 100644 --- a/src/lib/storage/config.ts +++ b/src/infra/storage/config.ts @@ -1,13 +1,13 @@ export { loadConfig, saveConfig, updateGoogleTokens, updateOpenAITokens, CONFIG_DIR, CONFIG_FILE }; -import * as s from "@/utils/json/schema"; +import * as s from "@/libs/json/schema"; -import { Future } from "@/utils/future"; +import { Future } from "@/libs/future"; import { resolve } from "node:path"; import { homedir } from "node:os"; import { readFile, writeFile, mkdir } from "node:fs/promises"; import { Config, type OAuthTokens, type OpenAITokens } from "@/domain/config/config"; -import { Just, Nothing, type Maybe } from "@/utils/maybe"; +import { Just, Nothing, type Maybe } from "@/libs/maybe"; const CONFIG_DIR = resolve(homedir(), ".commit-tools"); const CONFIG_FILE = resolve(CONFIG_DIR, "config.json"); diff --git a/src/infra/ui/model-picker.ts b/src/infra/ui/model-picker.ts new file mode 100644 index 0000000..b32e57e --- /dev/null +++ b/src/infra/ui/model-picker.ts @@ -0,0 +1,27 @@ +export { selectModelInteractively }; + +import { Future } from "@/libs/future"; +import { Model } from "@/domain/config/config"; + +const selectModelInteractively = (models: Model[]): Future => + Future.attemptP(async () => { + const { render } = await import("ink"); + const React = await import("react"); + const { ModelSelector } = await import("@/infra/ui/model-selector"); + + return new Promise((resolve, reject) => { + const { unmount } = render( + React.createElement(ModelSelector, { + models, + onSelect: (modelId: string) => { + unmount(); + resolve(modelId); + }, + onCancel: () => { + unmount(); + reject(new Error("Selection cancelled")); + } + }) + ); + }); + }); diff --git a/src/lib/ui/components/model-selector.tsx b/src/infra/ui/model-selector.tsx similarity index 100% rename from src/lib/ui/components/model-selector.tsx rename to src/infra/ui/model-selector.tsx diff --git a/src/lib/ui/spinner.ts b/src/infra/ui/spinner.ts similarity index 89% rename from src/lib/ui/spinner.ts rename to src/infra/ui/spinner.ts index 5df6d7d..49b0560 100644 --- a/src/lib/ui/spinner.ts +++ b/src/infra/ui/spinner.ts @@ -2,7 +2,7 @@ export { loading }; import * as p from "@clack/prompts"; -import { Future } from "@/utils/future"; +import { Future } from "@/libs/future"; const loading = (label: string, stopLabel: string, f: Future): Future => { const s = p.spinner(); diff --git a/src/utils/callable.ts b/src/libs/callable.ts similarity index 100% rename from src/utils/callable.ts rename to src/libs/callable.ts diff --git a/src/utils/future.ts b/src/libs/future.ts similarity index 98% rename from src/utils/future.ts rename to src/libs/future.ts index 91ef195..30c673d 100644 --- a/src/utils/future.ts +++ b/src/libs/future.ts @@ -2,7 +2,7 @@ export { Future, type Cancel }; import * as F from "fluture"; -import { Result, Success, Failure } from "@/utils/result"; +import { Result, Success, Failure } from "@/libs/result"; import { FutureInstance } from "fluture"; type Cancel = () => void; diff --git a/src/utils/helpers/object.ts b/src/libs/helpers/object.ts similarity index 100% rename from src/utils/helpers/object.ts rename to src/libs/helpers/object.ts diff --git a/src/utils/json/decoder.ts b/src/libs/json/decoder.ts similarity index 97% rename from src/utils/json/decoder.ts rename to src/libs/json/decoder.ts index 3e9323f..699961a 100644 --- a/src/utils/json/decoder.ts +++ b/src/libs/json/decoder.ts @@ -62,10 +62,10 @@ export { recursive }; -import { Result, Success, Failure, traverse } from "@/utils/result"; -import { Maybe, Just, Nothing, Nullable } from "@/utils/maybe"; -import { List } from "@/utils/list"; -import { Json } from "@/utils/json/types"; +import { Result, Success, Failure, traverse } from "@/libs/result"; +import { Maybe, Just, Nothing, Nullable } from "@/libs/maybe"; +import { List } from "@/libs/list"; +import { Json } from "@/libs/json/types"; // Infer the type from a decoder definition type Infer> = A extends Decoder ? B : never; diff --git a/src/utils/json/encoder.ts b/src/libs/json/encoder.ts similarity index 97% rename from src/utils/json/encoder.ts rename to src/libs/json/encoder.ts index 0d92746..d9e9c18 100644 --- a/src/utils/json/encoder.ts +++ b/src/libs/json/encoder.ts @@ -27,8 +27,8 @@ export { recursive }; -import { Maybe, Nothing, Just, Nullable } from "@/utils/maybe"; -import { Json, JsonObject } from "@/utils/json/types"; +import { Maybe, Nothing, Just, Nullable } from "@/libs/maybe"; +import { Json, JsonObject } from "@/libs/json/types"; // Infer the type from a encoder definition // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/utils/json/schema.ts b/src/libs/json/schema.ts similarity index 94% rename from src/utils/json/schema.ts rename to src/libs/json/schema.ts index 968b42a..d628b7f 100644 --- a/src/utils/json/schema.ts +++ b/src/libs/json/schema.ts @@ -37,16 +37,16 @@ export { recursive }; -import * as decoder from "@/utils/json/decoder"; -import * as encoder from "@/utils/json/encoder"; -import { Result } from "@/utils/result"; -import { Decoder, DecoderDef } from "@/utils/json/decoder"; -import * as D from "@/utils/json/decoder"; -import { Encoder, EncoderDef } from "@/utils/json/encoder"; -import { Json } from "@/utils/json/types"; -import * as E from "@/utils/json/encoder"; -import { Maybe, Nullable } from "@/utils/maybe"; -import { filterMap, mapValues } from "@/utils/helpers/object"; +import * as decoder from "@/libs/json/decoder"; +import * as encoder from "@/libs/json/encoder"; +import { Result } from "@/libs/result"; +import { Decoder, DecoderDef } from "@/libs/json/decoder"; +import * as D from "@/libs/json/decoder"; +import { Encoder, EncoderDef } from "@/libs/json/encoder"; +import { Json } from "@/libs/json/types"; +import * as E from "@/libs/json/encoder"; +import { Maybe, Nullable } from "@/libs/maybe"; +import { filterMap, mapValues } from "@/libs/helpers/object"; // Infer the type from a schema definition // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/utils/json/types.ts b/src/libs/json/types.ts similarity index 100% rename from src/utils/json/types.ts rename to src/libs/json/types.ts diff --git a/src/utils/list.ts b/src/libs/list.ts similarity index 98% rename from src/utils/list.ts rename to src/libs/list.ts index 3b280fe..26facc4 100644 --- a/src/utils/list.ts +++ b/src/libs/list.ts @@ -1,6 +1,6 @@ export { List }; -import { Maybe, Just, Nothing } from "@/utils/maybe"; +import { Maybe, Just, Nothing } from "@/libs/maybe"; type Content = { head: T; tail: List } | { empty: null }; diff --git a/src/utils/maybe.ts b/src/libs/maybe.ts similarity index 98% rename from src/utils/maybe.ts rename to src/libs/maybe.ts index 237ceb8..68f6283 100644 --- a/src/utils/maybe.ts +++ b/src/libs/maybe.ts @@ -26,7 +26,7 @@ export { mapMaybe }; -import Callable from "@/utils/callable"; +import Callable from "@/libs/callable"; type Maybe = Just | Nothing; type Nullable = T | null; diff --git a/src/utils/remote-data.ts b/src/libs/remote-data.ts similarity index 97% rename from src/utils/remote-data.ts rename to src/libs/remote-data.ts index 8a68e7e..ff875df 100644 --- a/src/utils/remote-data.ts +++ b/src/libs/remote-data.ts @@ -6,8 +6,8 @@ export { CallableLoading as Loading }; -import { Nullable, Maybe, Nothing, Just } from "@/utils/maybe"; -import Callable from "@/utils/callable"; +import { Nullable, Maybe, Nothing, Just } from "@/libs/maybe"; +import Callable from "@/libs/callable"; type RemoteData = NotAsked | Loading | Failed | Ready; diff --git a/src/utils/result.ts b/src/libs/result.ts similarity index 95% rename from src/utils/result.ts rename to src/libs/result.ts index 510d670..79a6cd0 100644 --- a/src/utils/result.ts +++ b/src/libs/result.ts @@ -1,9 +1,9 @@ export { type Result, CallableSuccess as Success, CallableFailure as Failure, traverse, traverse_ }; -import { Trampoline, end, tailRecursive } from "@/utils/trampoline"; -import { List } from "@/utils/list"; +import { Trampoline, end, tailRecursive } from "@/libs/trampoline"; +import { List } from "@/libs/list"; -import Callable from "@/utils/callable"; +import Callable from "@/libs/callable"; type Result = Success | Failure; diff --git a/src/utils/router.ts b/src/libs/router.ts similarity index 99% rename from src/utils/router.ts rename to src/libs/router.ts index 3b05817..22088ec 100644 --- a/src/utils/router.ts +++ b/src/libs/router.ts @@ -40,7 +40,7 @@ export { }; import * as express from "express"; -import { Cancel, Future } from "@/utils/future"; +import { Cancel, Future } from "@/libs/future"; type Json = null | string | number | boolean | JsonArray | JsonObject; type JsonObject = { [x: string]: Json }; diff --git a/src/utils/time.ts b/src/libs/time.ts similarity index 98% rename from src/utils/time.ts rename to src/libs/time.ts index 2e656fe..8356731 100644 --- a/src/utils/time.ts +++ b/src/libs/time.ts @@ -1,8 +1,8 @@ export { type Timezone, DateOnly, TimeOfDay, POSIX, Duration }; -import * as s from "@/utils/json/schema"; -import { fail, always } from "@/utils/json/decoder"; -import { type Maybe, Just, Nothing } from "@/utils/maybe"; +import * as s from "@/libs/json/schema"; +import { fail, always } from "@/libs/json/decoder"; +import { type Maybe, Just, Nothing } from "@/libs/maybe"; import { DateTime } from "luxon"; diff --git a/src/utils/trampoline.ts b/src/libs/trampoline.ts similarity index 100% rename from src/utils/trampoline.ts rename to src/libs/trampoline.ts diff --git a/src/utils/types.ts b/src/libs/types.ts similarity index 100% rename from src/utils/types.ts rename to src/libs/types.ts From 3a56bdfd974110c3b43f43ee6b06662ec8034bd8 Mon Sep 17 00:00:00 2001 From: rafaeelricco Date: Mon, 13 Apr 2026 08:57:39 -0300 Subject: [PATCH 06/11] Rename `authResolver` and `responseParser` to kebab-case filenames - Rename `authResolver.ts` to `auth-resolver.ts` and `responseParser.ts` to `response-parser.ts` for consistent file naming. - Update all import paths in `commit.ts`, `model.ts`, `anthropic.ts`, `gemini.ts`, and `openai.ts` to reflect the new filenames. --- src/cli/commit.ts | 2 +- src/cli/model.ts | 2 +- src/domain/llm/{authResolver.ts => auth-resolver.ts} | 0 src/domain/llm/{responseParser.ts => response-parser.ts} | 0 src/infra/llm/anthropic.ts | 2 +- src/infra/llm/gemini.ts | 2 +- src/infra/llm/openai.ts | 2 +- 7 files changed, 5 insertions(+), 5 deletions(-) rename src/domain/llm/{authResolver.ts => auth-resolver.ts} (100%) rename src/domain/llm/{responseParser.ts => response-parser.ts} (100%) diff --git a/src/cli/commit.ts b/src/cli/commit.ts index 55ebc4a..18b3e7b 100644 --- a/src/cli/commit.ts +++ b/src/cli/commit.ts @@ -7,7 +7,7 @@ import { Future } from "@/libs/future"; import { loadConfig } from "@/infra/storage/config"; import { Setup } from "@/cli/setup"; import { type CommitConvention, type Config, type ProviderConfig } from "@/domain/config/config"; -import { resolveProvider } from "@/domain/llm/authResolver"; +import { resolveProvider } from "@/domain/llm/auth-resolver"; import { generateCommitMessage, refineCommitMessage } from "@/domain/llm/router"; import { Nothing, type Maybe, Just } from "@/libs/maybe"; import { loading } from "@/infra/ui/spinner"; diff --git a/src/cli/model.ts b/src/cli/model.ts index b61e154..fb83e06 100644 --- a/src/cli/model.ts +++ b/src/cli/model.ts @@ -5,7 +5,7 @@ import * as p from "@clack/prompts"; import { Future } from "@/libs/future"; import { type Config, type ProviderConfig } from "@/domain/config/config"; import { loadConfig, saveConfig } from "@/infra/storage/config"; -import { resolveProvider } from "@/domain/llm/authResolver"; +import { resolveProvider } from "@/domain/llm/auth-resolver"; import { fetchModels } from "@/domain/commit/models"; import { selectModelInteractively } from "@/infra/ui/model-picker"; import { loading } from "@/infra/ui/spinner"; diff --git a/src/domain/llm/authResolver.ts b/src/domain/llm/auth-resolver.ts similarity index 100% rename from src/domain/llm/authResolver.ts rename to src/domain/llm/auth-resolver.ts diff --git a/src/domain/llm/responseParser.ts b/src/domain/llm/response-parser.ts similarity index 100% rename from src/domain/llm/responseParser.ts rename to src/domain/llm/response-parser.ts diff --git a/src/infra/llm/anthropic.ts b/src/infra/llm/anthropic.ts index a83deec..bc20a37 100644 --- a/src/infra/llm/anthropic.ts +++ b/src/infra/llm/anthropic.ts @@ -7,7 +7,7 @@ import { type GenerateContentParams } from "@/domain/llm/router"; import { Future } from "@/libs/future"; import { anthropicOAuthHeaders, CLAUDE_CODE_SYSTEM_PROMPT } from "@/infra/auth/anthropic"; import { absurd } from "@/libs/types"; -import { extractResponse } from "@/domain/llm/responseParser"; +import { extractResponse } from "@/domain/llm/response-parser"; type AnthropicConfig = Extract; diff --git a/src/infra/llm/gemini.ts b/src/infra/llm/gemini.ts index 437e148..3d28a38 100644 --- a/src/infra/llm/gemini.ts +++ b/src/infra/llm/gemini.ts @@ -6,7 +6,7 @@ import { type Config, type OAuthTokens } from "@/domain/config/config"; import { getAccessToken } from "@/infra/auth/google"; import { Just, Nothing, type Maybe } from "@/libs/maybe"; import { type GenerateContentParams } from "@/domain/llm/router"; -import { extractResponse } from "@/domain/llm/responseParser"; +import { extractResponse } from "@/domain/llm/response-parser"; type GeminiConfig = Extract; diff --git a/src/infra/llm/openai.ts b/src/infra/llm/openai.ts index dcb1914..558f224 100644 --- a/src/infra/llm/openai.ts +++ b/src/infra/llm/openai.ts @@ -4,7 +4,7 @@ import { type Config, type OpenAITokens } from "@/domain/config/config"; import { type GenerateContentParams } from "@/domain/llm/router"; import { Future } from "@/libs/future"; import { getOpenAIAccessToken } from "@/infra/auth/openai"; -import { extractResponse } from "@/domain/llm/responseParser"; +import { extractResponse } from "@/domain/llm/response-parser"; import OpenAI from "openai"; From 32aec6f805f90a4c403c16c423e236615ff5165b Mon Sep 17 00:00:00 2001 From: rafaeelricco Date: Mon, 13 Apr 2026 08:58:05 -0300 Subject: [PATCH 07/11] Bump version to 0.2.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a6034a5..0b0a4fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rafaeelricco/commit-tools", - "version": "0.2.3", + "version": "0.2.4", "type": "module", "bin": { "commit": "./dist/index.js" From 9eea943644616dab8e950babccdc89de7a797d16 Mon Sep 17 00:00:00 2001 From: rafaeelricco Date: Mon, 13 Apr 2026 09:22:36 -0300 Subject: [PATCH 08/11] Update README to document Anthropic as a supported provider - Add Anthropic (Claude) to the AI provider selection during setup. - Document `claude setup-token` auth method alongside API key option. - Move Anthropic from "coming soon" to the supported providers list. - Reorder auth method descriptions to match each provider's options. --- README.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 3010682..8ec7c36 100644 --- a/README.md +++ b/README.md @@ -90,9 +90,15 @@ commit setup You will be prompted to choose: -- **Auth method**: OAuth (sign in with your Google or ChatGPT account) or API Key (paste your own key) +- **AI provider**: Google Gemini, OpenAI, or Anthropic +- **Auth method**: + - Google Gemini: Google OAuth or API key + - OpenAI: Sign in with ChatGPT or API key + - Anthropic: Claude setup-token or API key - **Commit convention**: Conventional, Imperative, or Custom +If you want to use your claude.ai subscription with Anthropic, run `claude setup-token` in another terminal first, then paste the generated setup-token during `commit setup`. + To re-authenticate at any time: ```bash @@ -151,12 +157,9 @@ commit --help ## Providers -- **Google Gemini** — API key or Google OAuth -- **OpenAI** — API key or sign in with your ChatGPT Plus/Pro subscription - -More providers coming soon: - -- **Anthropic** (Claude) +- **Google Gemini** — Google OAuth or API key +- **OpenAI** — Sign in with your ChatGPT Plus/Pro subscription or API key +- **Anthropic** (Claude) — Claude setup-token (`claude setup-token`) or API key Contributions and feedback are welcome! From 2ccc1b3358a126cba7cd05f0a90b8334e481520d Mon Sep 17 00:00:00 2001 From: rafaeelricco Date: Sat, 18 Apr 2026 10:31:40 -0300 Subject: [PATCH 09/11] Add PR validation workflow with build and publish preview checks - Add a GitHub Actions workflow that runs on pull request open, sync, and reopen events. - Run repository checkout, Node.js setup, dependency installation, typechecking, formatting checks, and project build validation. - Add a preview job that compares `package.json` versions between the PR base and head to detect whether merging will trigger an npm publish. - Reject invalid semver values, non-forward version bumps, and versions that are already published to npm. - Write clear PR step summaries describing whether the merge will publish a new package version or why validation failed. --- .github/workflows/pr-validate.yml | 106 ++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 .github/workflows/pr-validate.yml diff --git a/.github/workflows/pr-validate.yml b/.github/workflows/pr-validate.yml new file mode 100644 index 0000000..3b3e11f --- /dev/null +++ b/.github/workflows/pr-validate.yml @@ -0,0 +1,106 @@ +name: PR Validate + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + checks: + name: Typecheck, format, and build + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: "24" + + - name: Install pnpm + run: npm install -g pnpm + + - name: Install dependencies + run: pnpm install + + - name: Typecheck + run: pnpm run typecheck + + - name: Check formatting + run: pnpm exec prettier . --check + + - name: Build project + env: + GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} + GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} + run: pnpm run build + + publish-preview: + name: Publish preview + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Install semver CLI + run: npm install -g semver + + - name: Detect publish on merge + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: | + HEAD_VERSION=$(node -p "require('./package.json').version") + PACKAGE_NAME=$(node -p "require('./package.json').name") + git show "${BASE_SHA}:package.json" > /tmp/base-package.json + BASE_VERSION=$(node -p "require('/tmp/base-package.json').version") + + echo "Package: $PACKAGE_NAME" + echo "Base: $BASE_VERSION" + echo "Head: $HEAD_VERSION" + + if ! semver "$HEAD_VERSION" >/dev/null; then + echo "::error::Head version '$HEAD_VERSION' is not valid semver." + { + echo "### Invalid version" + echo "" + echo "\`$HEAD_VERSION\` is not a valid semver string." + } >> "$GITHUB_STEP_SUMMARY" + exit 1 + fi + + if [ "$HEAD_VERSION" = "$BASE_VERSION" ]; then + { + echo "### No publish on merge" + echo "" + echo "Version pinned at \`$HEAD_VERSION\` — merging will not publish \`$PACKAGE_NAME\` to NPM." + } >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + + if ! semver "$HEAD_VERSION" -r ">$BASE_VERSION" >/dev/null; then + echo "::error::Version change $BASE_VERSION -> $HEAD_VERSION is not a valid forward bump." + { + echo "### Invalid version bump" + echo "" + echo "\`$BASE_VERSION\` -> \`$HEAD_VERSION\` is not a valid forward semver bump." + } >> "$GITHUB_STEP_SUMMARY" + exit 1 + fi + + if npm view "${PACKAGE_NAME}@${HEAD_VERSION}" version 2>/dev/null; then + echo "::error::Version $HEAD_VERSION is already published on NPM." + { + echo "### Invalid version bump" + echo "" + echo "Version \`$HEAD_VERSION\` is already published on NPM. Bump to a new unpublished version." + } >> "$GITHUB_STEP_SUMMARY" + exit 1 + fi + + { + echo "### Will publish on merge" + echo "" + echo "Merging will publish \`${PACKAGE_NAME}@${HEAD_VERSION}\` (bumped from \`$BASE_VERSION\`)." + } >> "$GITHUB_STEP_SUMMARY" From 7b853c96adf4563b90be70000b22b5fa3c3409e8 Mon Sep 17 00:00:00 2001 From: rafaeelricco Date: Sat, 18 Apr 2026 11:48:53 -0300 Subject: [PATCH 10/11] Refine PR version validation for pinned and published package cases --- .github/workflows/pr-validate.yml | 33 ++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/.github/workflows/pr-validate.yml b/.github/workflows/pr-validate.yml index 3b3e11f..b7c293e 100644 --- a/.github/workflows/pr-validate.yml +++ b/.github/workflows/pr-validate.yml @@ -70,16 +70,7 @@ jobs: exit 1 fi - if [ "$HEAD_VERSION" = "$BASE_VERSION" ]; then - { - echo "### No publish on merge" - echo "" - echo "Version pinned at \`$HEAD_VERSION\` — merging will not publish \`$PACKAGE_NAME\` to NPM." - } >> "$GITHUB_STEP_SUMMARY" - exit 0 - fi - - if ! semver "$HEAD_VERSION" -r ">$BASE_VERSION" >/dev/null; then + if [ "$HEAD_VERSION" != "$BASE_VERSION" ] && ! semver "$HEAD_VERSION" -r ">$BASE_VERSION" >/dev/null; then echo "::error::Version change $BASE_VERSION -> $HEAD_VERSION is not a valid forward bump." { echo "### Invalid version bump" @@ -89,7 +80,18 @@ jobs: exit 1 fi - if npm view "${PACKAGE_NAME}@${HEAD_VERSION}" version 2>/dev/null; then + PUBLISHED=$(npm view "${PACKAGE_NAME}@${HEAD_VERSION}" version 2>/dev/null || true) + + if [ -n "$PUBLISHED" ]; then + if [ "$HEAD_VERSION" = "$BASE_VERSION" ]; then + { + echo "### No publish on merge" + echo "" + echo "Version pinned at \`$HEAD_VERSION\` and already published on NPM — merging will not publish \`$PACKAGE_NAME\`." + } >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + echo "::error::Version $HEAD_VERSION is already published on NPM." { echo "### Invalid version bump" @@ -99,6 +101,15 @@ jobs: exit 1 fi + if [ "$HEAD_VERSION" = "$BASE_VERSION" ]; then + { + echo "### Will publish on merge" + echo "" + echo "Version pinned at \`$HEAD_VERSION\` but not yet published on NPM — merging will publish \`${PACKAGE_NAME}@${HEAD_VERSION}\`." + } >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + { echo "### Will publish on merge" echo "" From 2b369394073e17297e992d591484ce97983d4e26 Mon Sep 17 00:00:00 2001 From: rafaeelricco Date: Sat, 18 Apr 2026 17:58:32 -0300 Subject: [PATCH 11/11] Use placeholder OAuth secrets and set read-only permissions in PR validation --- .github/workflows/pr-validate.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-validate.yml b/.github/workflows/pr-validate.yml index b7c293e..a832517 100644 --- a/.github/workflows/pr-validate.yml +++ b/.github/workflows/pr-validate.yml @@ -8,6 +8,8 @@ jobs: checks: name: Typecheck, format, and build runs-on: ubuntu-latest + permissions: + contents: read steps: - name: Checkout repository uses: actions/checkout@v6 @@ -31,8 +33,8 @@ jobs: - name: Build project env: - GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} - GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} + GOOGLE_CLIENT_ID: pr-validation-not-a-real-client-id + GOOGLE_CLIENT_SECRET: pr-validation-not-a-real-secret run: pnpm run build publish-preview: