@@ -827,7 +827,7 @@ def _wcswidth(s: str) -> int:
827827class  Tool :
828828    name : str 
829829    candidates : tuple [str , ...]
830-     source_kind : str   # "gh" | "pypi" | "crates" | "npm" | "gnu" | "skip" 
830+     source_kind : str   # "gh" | "gitlab" | " pypi" | "crates" | "npm" | "gnu" | "skip" 
831831    source_args : tuple [str , ...]  # e.g., (owner, repo) or (package,) or (crate,) or (npm_pkg,) or (gnu_project,) 
832832
833833
@@ -902,7 +902,7 @@ class Tool:
902902    # 5) VCS & platforms 
903903    Tool ("git" , ("git" ,), "gh" , ("git" , "git" )),
904904    Tool ("gh" , ("gh" ,), "gh" , ("cli" , "cli" )),
905-     Tool ("glab" , ("glab" ,), "gh " , ("profclems " , "glab " )),
905+     Tool ("glab" , ("glab" ,), "gitlab " , ("gitlab-org " , "cli " )),
906906    Tool ("gam" , ("gam" ,), "gh" , ("GAM-team" , "GAM" )),
907907    # 6) Task runners & build systems 
908908    Tool ("just" , ("just" ,), "gh" , ("casey" , "just" )),
@@ -1892,6 +1892,8 @@ def upstream_method_for(tool: Tool) -> str:
18921892        return  "npm (nvm)" 
18931893    if  kind  ==  "gh" :
18941894        return  "github" 
1895+     if  kind  ==  "gitlab" :
1896+         return  "gitlab" 
18951897    if  kind  ==  "gnu" :
18961898        return  "gnu-ftp" 
18971899    return  "" 
@@ -1904,6 +1906,9 @@ def tool_homepage_url(tool: Tool) -> str:
19041906        if  kind  ==  "gh" :
19051907            owner , repo  =  args   # type: ignore[misc] 
19061908            return  f"https://github.com/{ owner } { repo }  
1909+         if  kind  ==  "gitlab" :
1910+             group , project  =  args   # type: ignore[misc] 
1911+             return  f"https://gitlab.com/{ group } { project }  
19071912        if  kind  ==  "pypi" :
19081913            (pkg ,) =  args   # type: ignore[misc] 
19091914            return  f"https://pypi.org/project/{ pkg }  
@@ -1930,6 +1935,11 @@ def latest_target_url(tool: Tool, latest_tag: str, latest_num: str) -> str:
19301935            if  latest_tag :
19311936                return  f"https://github.com/{ owner } { repo } { latest_tag }  
19321937            return  f"https://github.com/{ owner } { repo }  
1938+         if  kind  ==  "gitlab" :
1939+             group , project  =  args   # type: ignore[misc] 
1940+             if  latest_tag :
1941+                 return  f"https://gitlab.com/{ group } { project } { latest_tag }  
1942+             return  f"https://gitlab.com/{ group } { project }  
19331943        if  kind  ==  "pypi" :
19341944            (pkg ,) =  args   # type: ignore[misc] 
19351945            return  f"https://pypi.org/project/{ pkg }  
@@ -2162,6 +2172,92 @@ def latest_github(owner: str, repo: str) -> tuple[str, str]:
21622172    return  "" , "" 
21632173
21642174
2175+ def  latest_gitlab (group : str , project : str ) ->  tuple [str , str ]:
2176+     """ 
2177+     Fetch the latest release from GitLab using the GitLab API. 
2178+     Args: 
2179+         group: GitLab group/namespace (e.g., "gitlab-org") 
2180+         project: Project name (e.g., "cli") 
2181+     Returns: 
2182+         (tag_name, version_number) tuple or ("", "") if not found 
2183+     """ 
2184+     if  OFFLINE_MODE :
2185+         return  "" , "" 
2186+ 
2187+     # GitLab API requires URL-encoded project path 
2188+     project_path  =  f"{ group } { project }  
2189+ 
2190+     # Try releases API first (excludes pre-releases by default) 
2191+     try :
2192+         url  =  f"https://gitlab.com/api/v4/projects/{ project_path }  
2193+         if  AUDIT_DEBUG :
2194+             print (f"# DEBUG: GitLab API { url } { TIMEOUT_SECONDS }  , file = sys .stderr , flush = True )
2195+ 
2196+         data  =  json .loads (http_get (url ))
2197+ 
2198+         if  isinstance (data , list ) and  data :
2199+             # GitLab releases API returns releases in descending order by default 
2200+             # First release is the latest 
2201+             release  =  data [0 ]
2202+             tag  =  normalize_version_tag ((release .get ("tag_name" ) or  "" ).strip ())
2203+ 
2204+             if  tag :
2205+                 result  =  (tag , extract_version_number (tag ))
2206+                 set_manual_latest (project , tag )
2207+                 set_hint (f"gitlab:{ group } { project }  , "releases_api" )
2208+                 if  AUDIT_DEBUG :
2209+                     print (f"# DEBUG: GitLab found release: { tag }  , file = sys .stderr , flush = True )
2210+                 return  result 
2211+     except  Exception  as  e :
2212+         if  AUDIT_DEBUG :
2213+             print (f"# DEBUG: GitLab releases API failed: { e }  , file = sys .stderr , flush = True )
2214+         pass 
2215+ 
2216+     # Fallback to tags API 
2217+     try :
2218+         url  =  f"https://gitlab.com/api/v4/projects/{ project_path }  
2219+         if  AUDIT_DEBUG :
2220+             print (f"# DEBUG: GitLab tags API { url }  , file = sys .stderr , flush = True )
2221+ 
2222+         data  =  json .loads (http_get (url ))
2223+ 
2224+         if  isinstance (data , list ):
2225+             # Filter stable releases and find highest version 
2226+             best : tuple [tuple [int , ...], str , str ] |  None  =  None 
2227+ 
2228+             for  item  in  data :
2229+                 tag_name  =  (item .get ("name" ) or  "" ).strip ()
2230+                 tag  =  normalize_version_tag (tag_name )
2231+ 
2232+                 # Accept only stable final release tags (v1.2.3, 1.2.3) 
2233+                 # Exclude rc, alpha, beta, pre, dev suffixes 
2234+                 if  tag  and  re .match (r"^v?\d+\.\d+(\.\d+)?$" , tag ):
2235+                     ver  =  extract_version_number (tag )
2236+                     if  ver :
2237+                         try :
2238+                             nums  =  tuple (int (x ) for  x  in  ver .split ("." ))
2239+                             tup  =  (nums , tag , ver )
2240+                             if  best  is  None  or  tup [0 ] >  best [0 ]:
2241+                                 best  =  tup 
2242+                         except  Exception :
2243+                             continue 
2244+ 
2245+             if  best  is  not None :
2246+                 _ , tag , ver  =  best 
2247+                 result  =  (tag , ver )
2248+                 set_manual_latest (project , tag )
2249+                 set_hint (f"gitlab:{ group } { project }  , "tags_api" )
2250+                 if  AUDIT_DEBUG :
2251+                     print (f"# DEBUG: GitLab found tag: { tag }  , file = sys .stderr , flush = True )
2252+                 return  result 
2253+     except  Exception  as  e :
2254+         if  AUDIT_DEBUG :
2255+             print (f"# DEBUG: GitLab tags API failed: { e }  , file = sys .stderr , flush = True )
2256+         pass 
2257+ 
2258+     return  "" , "" 
2259+ 
2260+ 
21652261def  latest_pypi (package : str ) ->  tuple [str , str ]:
21662262    if  OFFLINE_MODE :
21672263        return  "" , "" 
@@ -2365,6 +2461,18 @@ def get_latest(tool: Tool) -> tuple[str, str]:
23652461            return  man_tag , man_num 
23662462        MANUAL_USED [tool .name ] =  False 
23672463        return  tag , num 
2464+     if  kind  ==  "gitlab" :
2465+         group , project  =  args   # type: ignore[misc] 
2466+         tag , num  =  latest_gitlab (group , project )
2467+         if  tag  or  num :
2468+             MANUAL_USED [tool .name ] =  False 
2469+             set_manual_method (tool .name , "gitlab" )
2470+             return  tag , num 
2471+         if  manual_available :
2472+             MANUAL_USED [tool .name ] =  True 
2473+             return  man_tag , man_num 
2474+         MANUAL_USED [tool .name ] =  False 
2475+         return  tag , num 
23682476    if  kind  ==  "pypi" :
23692477        (pkg ,) =  args   # type: ignore[misc] 
23702478        tag , num  =  latest_pypi (pkg )
@@ -2530,20 +2638,9 @@ def audit_tool(tool: Tool) -> tuple[str, str, str, str, str, str, str, str]:
25302638                except  Exception :
25312639                    pass   # Catalog read failed, continue with original status 
25322640
2533-     # Check if tool is marked as "never install" 
2534-     if  status  ==  "NOT INSTALLED" :
2535-         script_dir  =  os .path .dirname (os .path .abspath (__file__ ))
2536-         catalog_file  =  os .path .join (script_dir , "catalog" , f"{ tool .name }  )
2537-         if  os .path .exists (catalog_file ):
2538-             try :
2539-                 with  open (catalog_file , "r" , encoding = "utf-8" ) as  f :
2540-                     catalog_data  =  json .load (f )
2541-                     pinned_version  =  catalog_data .get ("pinned_version" , "" )
2542-                     if  pinned_version  ==  "never" :
2543-                         # Tool is marked as never install - treat as up-to-date to suppress prompts 
2544-                         status  =  "UP-TO-DATE" 
2545-             except  Exception :
2546-                 pass   # Catalog read failed, continue with original status 
2641+     # Note: Tools with pinned_version="never" are filtered out in guide.sh, 
2642+     # so we don't need to change their status here. Keep them as NOT INSTALLED 
2643+     # to avoid confusion (showing ✅ icon when tool isn't actually installed). 
25472644
25482645    # Sanitize latest display to numeric (like installed) 
25492646    if  latest_num :
0 commit comments