Skip to content

Commit 23c03ba

Browse files
konardclaude
andcommitted
refactor(shell): extract shared ANSI utilities to eliminate code duplication
Extracted stripAnsi and writeChunkToFd functions to a shared module at packages/lib/src/shell/ansi-strip.ts to eliminate code duplication detected by the linter between auth-claude-oauth.ts and auth-gemini-oauth.ts. This refactoring: - Creates a new shell/ansi-strip.ts module with shared ANSI parsing utilities - Updates both OAuth files to import from the shared module - Maintains the same functionality while reducing duplicate code Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent b2624d1 commit 23c03ba

4 files changed

Lines changed: 90 additions & 148 deletions

File tree

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// CHANGE: extract ANSI escape sequence stripping to shared module
2+
// WHY: avoid code duplication between auth-claude-oauth.ts and auth-gemini-oauth.ts
3+
// REF: issue-146, lint error
4+
// PURITY: CORE
5+
// COMPLEXITY: O(n) where n = string length
6+
7+
const ansiEscape = "\u001B"
8+
const ansiBell = "\u0007"
9+
10+
const isAnsiFinalByte = (codePoint: number | undefined): boolean =>
11+
codePoint !== undefined && codePoint >= 0x40 && codePoint <= 0x7E
12+
13+
const skipCsiSequence = (raw: string, start: number): number => {
14+
const length = raw.length
15+
let index = start + 2
16+
while (index < length) {
17+
const codePoint = raw.codePointAt(index)
18+
if (isAnsiFinalByte(codePoint)) {
19+
return index + 1
20+
}
21+
index += 1
22+
}
23+
return index
24+
}
25+
26+
const skipOscSequence = (raw: string, start: number): number => {
27+
const length = raw.length
28+
let index = start + 2
29+
while (index < length) {
30+
const char = raw[index] ?? ""
31+
if (char === ansiBell) {
32+
return index + 1
33+
}
34+
if (char === ansiEscape && raw[index + 1] === "\\") {
35+
return index + 2
36+
}
37+
index += 1
38+
}
39+
return index
40+
}
41+
42+
const skipEscapeSequence = (raw: string, start: number): number => {
43+
const next = raw[start + 1] ?? ""
44+
if (next === "[") {
45+
return skipCsiSequence(raw, start)
46+
}
47+
if (next === "]") {
48+
return skipOscSequence(raw, start)
49+
}
50+
return Math.min(raw.length, start + 2)
51+
}
52+
53+
export const stripAnsi = (raw: string): string => {
54+
const cleaned: Array<string> = []
55+
let index = 0
56+
57+
while (index < raw.length) {
58+
const current = raw[index] ?? ""
59+
if (current !== ansiEscape) {
60+
cleaned.push(current)
61+
index += 1
62+
continue
63+
}
64+
index = skipEscapeSequence(raw, index)
65+
}
66+
67+
return cleaned.join("")
68+
}
69+
70+
// CHANGE: extract writeChunkToFd to shared module
71+
// WHY: avoid code duplication between auth-claude-oauth.ts and auth-gemini-oauth.ts
72+
// REF: issue-146, lint error
73+
// PURITY: SHELL (I/O side effect)
74+
// COMPLEXITY: O(1)
75+
export const writeChunkToFd = (fd: number, chunk: Uint8Array): void => {
76+
if (fd === 2) {
77+
process.stderr.write(chunk)
78+
return
79+
}
80+
process.stdout.write(chunk)
81+
}

packages/lib/src/usecases/auth-claude-oauth.ts

Lines changed: 1 addition & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as Fiber from "effect/Fiber"
66
import type * as Scope from "effect/Scope"
77
import * as Stream from "effect/Stream"
88

9+
import { stripAnsi, writeChunkToFd } from "../shell/ansi-strip.js"
910
import { resolveDefaultDockerUser, resolveDockerVolumeHostPath } from "../shell/docker-auth.js"
1011
import { AuthError, CommandFailedError } from "../shell/errors.js"
1112

@@ -16,69 +17,6 @@ const outputWindowSize = 262_144
1617

1718
const oauthTokenRegex = /([A-Za-z0-9][A-Za-z0-9._-]{20,})/u
1819

19-
const ansiEscape = "\u001B"
20-
const ansiBell = "\u0007"
21-
22-
const isAnsiFinalByte = (codePoint: number | undefined): boolean =>
23-
codePoint !== undefined && codePoint >= 0x40 && codePoint <= 0x7E
24-
25-
const skipCsiSequence = (raw: string, start: number): number => {
26-
const length = raw.length
27-
let index = start + 2
28-
while (index < length) {
29-
const codePoint = raw.codePointAt(index)
30-
if (isAnsiFinalByte(codePoint)) {
31-
return index + 1
32-
}
33-
index += 1
34-
}
35-
return index
36-
}
37-
38-
const skipOscSequence = (raw: string, start: number): number => {
39-
const length = raw.length
40-
let index = start + 2
41-
while (index < length) {
42-
const char = raw[index] ?? ""
43-
if (char === ansiBell) {
44-
return index + 1
45-
}
46-
if (char === ansiEscape && raw[index + 1] === "\\") {
47-
return index + 2
48-
}
49-
index += 1
50-
}
51-
return index
52-
}
53-
54-
const skipEscapeSequence = (raw: string, start: number): number => {
55-
const next = raw[start + 1] ?? ""
56-
if (next === "[") {
57-
return skipCsiSequence(raw, start)
58-
}
59-
if (next === "]") {
60-
return skipOscSequence(raw, start)
61-
}
62-
return Math.min(raw.length, start + 2)
63-
}
64-
65-
const stripAnsi = (raw: string): string => {
66-
const cleaned: Array<string> = []
67-
let index = 0
68-
69-
while (index < raw.length) {
70-
const current = raw[index] ?? ""
71-
if (current !== ansiEscape) {
72-
cleaned.push(current)
73-
index += 1
74-
continue
75-
}
76-
index = skipEscapeSequence(raw, index)
77-
}
78-
79-
return cleaned.join("")
80-
}
81-
8220
const extractOauthToken = (rawOutput: string): string | null => {
8321
const normalized = stripAnsi(rawOutput).replaceAll("\r", "\n")
8422
const markerIndex = normalized.lastIndexOf(tokenMarker)
@@ -171,14 +109,6 @@ const startDockerProcess = (
171109
)
172110
)
173111

174-
const writeChunkToFd = (fd: number, chunk: Uint8Array): void => {
175-
if (fd === 2) {
176-
process.stderr.write(chunk)
177-
return
178-
}
179-
process.stdout.write(chunk)
180-
}
181-
182112
const pumpDockerOutput = (
183113
source: Stream.Stream<Uint8Array, PlatformError>,
184114
fd: number,

packages/lib/src/usecases/auth-gemini-oauth.ts

Lines changed: 7 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as Fiber from "effect/Fiber"
66
import type * as Scope from "effect/Scope"
77
import * as Stream from "effect/Stream"
88

9+
import { stripAnsi, writeChunkToFd } from "../shell/ansi-strip.js"
910
import { resolveDefaultDockerUser, resolveDockerVolumeHostPath } from "../shell/docker-auth.js"
1011
import { AuthError, CommandFailedError } from "../shell/errors.js"
1112

@@ -20,70 +21,9 @@ import { AuthError, CommandFailedError } from "../shell/errors.js"
2021
// INVARIANT: OAuth credentials are stored in ~/.gemini directory within account path
2122
// COMPLEXITY: O(command)
2223

23-
const outputWindowSize = 262_144
24-
25-
const ansiEscape = "\u001B"
26-
const ansiBell = "\u0007"
27-
28-
const isAnsiFinalByte = (codePoint: number | undefined): boolean =>
29-
codePoint !== undefined && codePoint >= 0x40 && codePoint <= 0x7E
30-
31-
const skipCsiSequence = (raw: string, start: number): number => {
32-
const length = raw.length
33-
let index = start + 2
34-
while (index < length) {
35-
const codePoint = raw.codePointAt(index)
36-
if (isAnsiFinalByte(codePoint)) {
37-
return index + 1
38-
}
39-
index += 1
40-
}
41-
return index
42-
}
43-
44-
const skipOscSequence = (raw: string, start: number): number => {
45-
const length = raw.length
46-
let index = start + 2
47-
while (index < length) {
48-
const char = raw[index] ?? ""
49-
if (char === ansiBell) {
50-
return index + 1
51-
}
52-
if (char === ansiEscape && raw[index + 1] === "\\") {
53-
return index + 2
54-
}
55-
index += 1
56-
}
57-
return index
58-
}
59-
60-
const skipEscapeSequence = (raw: string, start: number): number => {
61-
const next = raw[start + 1] ?? ""
62-
if (next === "[") {
63-
return skipCsiSequence(raw, start)
64-
}
65-
if (next === "]") {
66-
return skipOscSequence(raw, start)
67-
}
68-
return Math.min(raw.length, start + 2)
69-
}
70-
71-
const stripAnsi = (raw: string): string => {
72-
const cleaned: Array<string> = []
73-
let index = 0
24+
type GeminiAuthResult = "success" | "failure" | "pending"
7425

75-
while (index < raw.length) {
76-
const current = raw[index] ?? ""
77-
if (current !== ansiEscape) {
78-
cleaned.push(current)
79-
index += 1
80-
continue
81-
}
82-
index = skipEscapeSequence(raw, index)
83-
}
84-
85-
return cleaned.join("")
86-
}
26+
const outputWindowSize = 262_144
8727

8828
// Detect successful authentication in Gemini CLI output
8929
const authSuccessPatterns = [
@@ -102,7 +42,7 @@ const authFailurePatterns = [
10242
"Authentication cancelled"
10343
]
10444

105-
const detectAuthResult = (output: string): "success" | "failure" | "pending" => {
45+
const detectAuthResult = (output: string): GeminiAuthResult => {
10646
const normalized = stripAnsi(output).toLowerCase()
10747

10848
for (const pattern of authSuccessPatterns) {
@@ -176,18 +116,10 @@ const startDockerProcess = (
176116
)
177117
)
178118

179-
const writeChunkToFd = (fd: number, chunk: Uint8Array): void => {
180-
if (fd === 2) {
181-
process.stderr.write(chunk)
182-
return
183-
}
184-
process.stdout.write(chunk)
185-
}
186-
187119
const pumpDockerOutput = (
188120
source: Stream.Stream<Uint8Array, PlatformError>,
189121
fd: number,
190-
resultBox: { value: "success" | "failure" | "pending" }
122+
resultBox: { value: GeminiAuthResult }
191123
): Effect.Effect<void, PlatformError> => {
192124
const decoder = new TextDecoder("utf-8")
193125
let outputWindow = ""
@@ -214,7 +146,7 @@ const pumpDockerOutput = (
214146
}
215147

216148
const resolveGeminiLoginResult = (
217-
result: "success" | "failure" | "pending",
149+
result: GeminiAuthResult,
218150
exitCode: number
219151
): Effect.Effect<void, AuthError | CommandFailedError> =>
220152
Effect.gen(function*(_) {
@@ -272,7 +204,7 @@ export const runGeminiOauthLoginWithPrompt = (
272204
const spec = buildDockerGeminiAuthSpec(cwd, hostPath, options.image, options.containerPath)
273205
const proc = yield* _(startDockerProcess(executor, spec))
274206

275-
const resultBox: { value: "success" | "failure" | "pending" } = { value: "pending" }
207+
const resultBox: { value: GeminiAuthResult } = { value: "pending" }
276208
const stdoutFiber = yield* _(Effect.forkScoped(pumpDockerOutput(proc.stdout, 1, resultBox)))
277209
const stderrFiber = yield* _(Effect.forkScoped(pumpDockerOutput(proc.stderr, 2, resultBox)))
278210

packages/lib/src/usecases/auth-gemini.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ import { Effect } from "effect"
66

77
import type { AuthGeminiLoginCommand, AuthGeminiLogoutCommand, AuthGeminiStatusCommand } from "../core/domain.js"
88
import { defaultTemplateConfig } from "../core/domain.js"
9-
import type { AuthError } from "../shell/errors.js"
10-
import { CommandFailedError } from "../shell/errors.js"
9+
import type { AuthError, CommandFailedError } from "../shell/errors.js"
1110
import { runGeminiOauthLoginWithPrompt } from "./auth-gemini-oauth.js"
1211
import { isRegularFile, normalizeAccountLabel } from "./auth-helpers.js"
1312
import { migrateLegacyOrchLayout } from "./auth-sync.js"

0 commit comments

Comments
 (0)