1+ import { stat , rm , writeFile , mkdir } from 'node:fs/promises' ;
2+ import * as path from 'node:path' ;
3+ import { homedir } from 'node:os' ;
4+ import { execa } from 'execa' ;
5+
6+ import { expandHomeDir } from '../../../../shared/utils/index.js' ;
7+ import { metadata } from './metadata.js' ;
8+
9+ /**
10+ * Check if CLI is installed
11+ */
12+ async function isCliInstalled ( command : string ) : Promise < boolean > {
13+ try {
14+ const result = await execa ( command , [ '--version' ] , { timeout : 3000 , reject : false } ) ;
15+ if ( typeof result . exitCode === 'number' && result . exitCode === 0 ) return true ;
16+ const out = `${ result . stdout ?? '' } \n${ result . stderr ?? '' } ` ;
17+ if ( / n o t r e c o g n i z e d a s a n i n t e r n a l o r e x t e r n a l c o m m a n d / i. test ( out ) ) return false ;
18+ if ( / c o m m a n d n o t f o u n d / i. test ( out ) ) return false ;
19+ if ( / N o s u c h f i l e o r d i r e c t o r y / i. test ( out ) ) return false ;
20+ return false ;
21+ } catch {
22+ return false ;
23+ }
24+ }
25+
26+ export interface CcrAuthOptions {
27+ ccrConfigDir ?: string ;
28+ }
29+
30+ /**
31+ * Resolves the CCR config directory (shared for authentication)
32+ */
33+ export function resolveCcrConfigDir ( options ?: CcrAuthOptions ) : string {
34+ if ( options ?. ccrConfigDir ) {
35+ return expandHomeDir ( options . ccrConfigDir ) ;
36+ }
37+
38+ if ( process . env . CCR_CONFIG_DIR ) {
39+ return expandHomeDir ( process . env . CCR_CONFIG_DIR ) ;
40+ }
41+
42+ // Authentication is shared globally
43+ return path . join ( homedir ( ) , '.codemachine' , 'ccr' ) ;
44+ }
45+
46+ /**
47+ * Gets the path to the credentials file
48+ * CCR stores it directly in CCR_CONFIG_DIR
49+ */
50+ export function getCredentialsPath ( configDir : string ) : string {
51+ return path . join ( configDir , '.credentials.json' ) ;
52+ }
53+
54+ /**
55+ * Gets paths to all CCR-related files that need to be cleaned up
56+ */
57+ export function getCcrAuthPaths ( configDir : string ) : string [ ] {
58+ return [
59+ getCredentialsPath ( configDir ) , // .credentials.json
60+ path . join ( configDir , '.ccr.json' ) ,
61+ path . join ( configDir , '.ccr.json.backup' ) ,
62+ ] ;
63+ }
64+
65+ /**
66+ * Checks if CCR is authenticated
67+ */
68+ export async function isAuthenticated ( options ?: CcrAuthOptions ) : Promise < boolean > {
69+ // Check if token is set via environment variable
70+ if ( process . env . CCR_CODE_TOKEN ) {
71+ return true ;
72+ }
73+
74+ const configDir = resolveCcrConfigDir ( options ) ;
75+ const credPath = getCredentialsPath ( configDir ) ;
76+
77+ try {
78+ await stat ( credPath ) ;
79+ return true ;
80+ } catch ( _error ) {
81+ return false ;
82+ }
83+ }
84+
85+ /**
86+ * Ensures CCR is authenticated
87+ * Unlike Claude, CCR doesn't require interactive setup - authentication is done via environment variable
88+ */
89+ export async function ensureAuth ( options ?: CcrAuthOptions ) : Promise < boolean > {
90+ // Check if token is already set via environment variable
91+ if ( process . env . CCR_CODE_TOKEN ) {
92+ return true ;
93+ }
94+
95+ const configDir = resolveCcrConfigDir ( options ) ;
96+ const credPath = getCredentialsPath ( configDir ) ;
97+
98+ // If already authenticated, nothing to do
99+ try {
100+ await stat ( credPath ) ;
101+ return true ;
102+ } catch {
103+ // Credentials file doesn't exist
104+ }
105+
106+ if ( process . env . CODEMACHINE_SKIP_AUTH === '1' ) {
107+ // Create a placeholder for testing/dry-run mode
108+ const ccrDir = path . dirname ( credPath ) ;
109+ await mkdir ( ccrDir , { recursive : true } ) ;
110+ await writeFile ( credPath , '{}' , { encoding : 'utf8' } ) ;
111+ return true ;
112+ }
113+
114+ // Check if CLI is installed
115+ const cliInstalled = await isCliInstalled ( metadata . cliBinary ) ;
116+ if ( ! cliInstalled ) {
117+ console . error ( `\n────────────────────────────────────────────────────────────` ) ;
118+ console . error ( ` ⚠️ ${ metadata . name } CLI Not Installed` ) ;
119+ console . error ( `────────────────────────────────────────────────────────────` ) ;
120+ console . error ( `\nThe '${ metadata . cliBinary } ' command is not available.` ) ;
121+ console . error ( `Please install ${ metadata . name } CLI first:\n` ) ;
122+ console . error ( ` ${ metadata . installCommand } \n` ) ;
123+ console . error ( `────────────────────────────────────────────────────────────\n` ) ;
124+ throw new Error ( `${ metadata . name } CLI is not installed.` ) ;
125+ }
126+
127+ // For CCR, authentication is token-based via environment variable
128+ console . error ( `\n────────────────────────────────────────────────────────────` ) ;
129+ console . error ( ` ℹ️ CCR Authentication Notice` ) ;
130+ console . error ( `────────────────────────────────────────────────────────────` ) ;
131+ console . error ( `\nCCR uses token-based authentication.` ) ;
132+ console . error ( `Please set your CCR token as an environment variable:\n` ) ;
133+ console . error ( ` export CCR_CODE_TOKEN=<your-token>\n` ) ;
134+ console . error ( `For persistence, add this line to your shell configuration:` ) ;
135+ console . error ( ` ~/.bashrc (Bash) or ~/.zshrc (Zsh)\n` ) ;
136+ console . error ( `────────────────────────────────────────────────────────────\n` ) ;
137+
138+ throw new Error ( 'Authentication incomplete. Please set CCR_CODE_TOKEN environment variable.' ) ;
139+ }
140+
141+ /**
142+ * Clears all CCR authentication data
143+ */
144+ export async function clearAuth ( options ?: CcrAuthOptions ) : Promise < void > {
145+ const configDir = resolveCcrConfigDir ( options ) ;
146+ const authPaths = getCcrAuthPaths ( configDir ) ;
147+
148+ // Remove all auth-related files
149+ await Promise . all (
150+ authPaths . map ( async ( authPath ) => {
151+ try {
152+ await rm ( authPath , { force : true } ) ;
153+ } catch ( _error ) {
154+ // Ignore removal errors; treat as cleared
155+ }
156+ } ) ,
157+ ) ;
158+ }
159+
160+ /**
161+ * Returns the next auth menu action based on current auth state
162+ */
163+ export async function nextAuthMenuAction ( options ?: CcrAuthOptions ) : Promise < 'login' | 'logout' > {
164+ return ( await isAuthenticated ( options ) ) ? 'logout' : 'login' ;
165+ }
0 commit comments