Skip to content

Commit be55fac

Browse files
committed
Add fine_system_claude_code extension; fix lint bridge file-target dispatch; remove pyrefly from fine_python_lint
- Add extensions/fine_system_claude_code: InstallClaudeCodeHandler downloads and runs the Claude Code native installer (shell on Unix, PowerShell on Windows), skips if already installed, and reports installed/skipped/failed via SetupSystemRunResult. - Fix LintInspectCodeBridgeHandler to correctly handle InspectCodeTarget.FILES: group requested files by known project before dispatching per-project LintAction runs (previously all projects were linted regardless of target). Warn via user_messenger when no files matched a known project. Add debug logging for project dispatch and partial result forwarding. - Remove pyrefly handler from fine_python_lint preset (lint_python_files and its extension dependency). Add fine_lint as an explicit preset dependency of fine_python_lint. Add PLC0415 (import-outside-top-level) to ruff extend_select.
1 parent ecd69b3 commit be55fac

7 files changed

Lines changed: 237 additions & 17 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.venvs
2+
build/
3+
*.egg-info/
4+
__pycache__
5+
finecode_config_dump/
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from fine_system_claude_code.install_claude_code_handler import (
2+
InstallClaudeCodeHandler,
3+
InstallClaudeCodeHandlerConfig,
4+
)
5+
6+
__all__ = [
7+
"InstallClaudeCodeHandler",
8+
"InstallClaudeCodeHandlerConfig",
9+
]
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import dataclasses
2+
import shlex
3+
import shutil
4+
import sys
5+
import tempfile
6+
7+
from finecode_extension_api import code_action
8+
from finecode_extension_api.interfaces import icommandrunner, ihttpclient, ilogger
9+
from fine_system_setup.setup_system_action import (
10+
SetupSystemAction,
11+
SetupSystemRunContext,
12+
SetupSystemRunPayload,
13+
SetupSystemRunResult,
14+
)
15+
16+
_TOOL_NAME = "claude-code"
17+
_INSTALL_SH_URL = "https://claude.ai/install.sh"
18+
_INSTALL_PS1_URL = "https://claude.ai/install.ps1"
19+
20+
21+
@dataclasses.dataclass
22+
class InstallClaudeCodeHandlerConfig(code_action.ActionHandlerConfig): ...
23+
24+
25+
class InstallClaudeCodeHandler(
26+
code_action.ActionHandler[
27+
SetupSystemAction,
28+
InstallClaudeCodeHandlerConfig,
29+
]
30+
):
31+
"""Install the Claude Code CLI via the native installer if not already present."""
32+
33+
def __init__(
34+
self,
35+
logger: ilogger.ILogger,
36+
command_runner: icommandrunner.ICommandRunner,
37+
http_client: ihttpclient.IHttpClient,
38+
) -> None:
39+
self.logger = logger
40+
self.command_runner = command_runner
41+
self.http_client = http_client
42+
43+
async def run(
44+
self,
45+
payload: SetupSystemRunPayload,
46+
run_context: SetupSystemRunContext,
47+
) -> SetupSystemRunResult:
48+
if shutil.which("claude") is not None:
49+
self.logger.info("claude already installed, skipping")
50+
return SetupSystemRunResult(skipped=[_TOOL_NAME])
51+
52+
if sys.platform == "win32":
53+
url = _INSTALL_PS1_URL
54+
suffix = ".ps1"
55+
else:
56+
url = _INSTALL_SH_URL
57+
suffix = ".sh"
58+
59+
self.logger.info(f"Downloading installer from {url}")
60+
async with run_context.progress("Installing claude-code", total=2) as progress:
61+
await progress.report("Downloading installer")
62+
async with self.http_client.session() as session:
63+
response = await session.get(url)
64+
response.raise_for_status()
65+
script_content = response.text
66+
67+
with tempfile.NamedTemporaryFile(mode="w", suffix=suffix, delete_on_close=False) as f:
68+
f.write(script_content)
69+
script_path = f.name
70+
f.close() # flush and release the file before the subprocess opens it
71+
72+
if sys.platform == "win32":
73+
cmd = shlex.join(
74+
["powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", script_path]
75+
)
76+
else:
77+
cmd = shlex.join(["bash", script_path])
78+
79+
self.logger.info(f"Running installer: {cmd}")
80+
await progress.advance(1, "Running installer")
81+
process = await self.command_runner.run(cmd)
82+
await process.wait_for_end()
83+
await progress.advance(1)
84+
85+
exit_code = process.get_exit_code()
86+
if exit_code != 0:
87+
error = process.get_error_output().strip() or process.get_output().strip()
88+
self.logger.error(f"Install failed: {error}")
89+
return SetupSystemRunResult(failed=[f"{_TOOL_NAME}: {error}"])
90+
91+
self.logger.info("claude-code installed successfully")
92+
return SetupSystemRunResult(installed=[_TOOL_NAME])
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[project]
2+
name = "fine_system_claude_code"
3+
version = "0.1.0a0"
4+
description = "FineCode handler to install the Claude Code CLI"
5+
authors = [{ name = "Vladyslav Hnatiuk", email = "aders1234@gmail.com" }]
6+
requires-python = ">=3.11, < 3.15"
7+
dependencies = ["finecode_extension_api~=0.4.0a0", "fine_system_setup~=0.1.0a0"]
8+
9+
[dependency-groups]
10+
dev_workspace = ["finecode~=0.4.0a0", "finecode_dev_common_preset~=0.3.0a0"]
11+
12+
[tool.finecode]
13+
presets = [{ source = "finecode_dev_common_preset" }]
14+
15+
[tool.setuptools]
16+
packages = ["fine_system_claude_code"]
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import atexit
2+
import shutil
3+
import sys
4+
import tempfile
5+
6+
from setuptools import setup
7+
from setuptools.command.build import build
8+
from setuptools.command.build_ext import build_ext
9+
from setuptools.command.build_py import build_py
10+
from setuptools.command.egg_info import egg_info
11+
12+
13+
_TEMP_BUILD_DIR = None
14+
15+
16+
def get_temp_build_dir(pkg_name):
17+
global _TEMP_BUILD_DIR
18+
if _TEMP_BUILD_DIR is None:
19+
_TEMP_BUILD_DIR = tempfile.mkdtemp(prefix=f"{pkg_name}_build_")
20+
atexit.register(lambda: shutil.rmtree(_TEMP_BUILD_DIR, ignore_errors=True))
21+
return _TEMP_BUILD_DIR
22+
23+
24+
class TempDirBuildMixin:
25+
def initialize_options(self):
26+
super().initialize_options()
27+
temp_dir = get_temp_build_dir(self.distribution.get_name())
28+
self.build_base = temp_dir
29+
30+
31+
class TempDirEggInfoMixin:
32+
def initialize_options(self):
33+
super().initialize_options()
34+
temp_dir = get_temp_build_dir(self.distribution.get_name())
35+
self.egg_base = temp_dir
36+
37+
38+
class CustomBuild(TempDirBuildMixin, build):
39+
pass
40+
41+
42+
class CustomBuildPy(TempDirBuildMixin, build_py):
43+
pass
44+
45+
46+
class CustomBuildExt(TempDirBuildMixin, build_ext):
47+
pass
48+
49+
50+
class CustomEggInfo(TempDirEggInfoMixin, egg_info):
51+
def initialize_options(self):
52+
if "--editable" in sys.argv or "-e" in sys.argv:
53+
egg_info.initialize_options(self)
54+
else:
55+
super().initialize_options()
56+
57+
58+
setup(
59+
name="fine_system_claude_code",
60+
cmdclass={
61+
"build": CustomBuild,
62+
"build_py": CustomBuildPy,
63+
"build_ext": CustomBuildExt,
64+
"egg_info": CustomEggInfo,
65+
},
66+
)

presets/fine_lint/fine_lint/lint_inspect_code_bridge_handler.py

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,20 @@
22
import dataclasses
33
import pathlib
44

5+
from loguru import logger as loguru_logger
56
from finecode_extension_api import code_action
67
from fine_inspect_code.inspect_code_action import (
78
InspectCodeAction,
89
InspectCodeRunPayload,
910
InspectCodeRunContext,
1011
InspectCodeRunResult,
12+
InspectCodeTarget,
1113
)
1214
from fine_lint.lint_action import LintAction, LintRunPayload, LintTarget
13-
from finecode_extension_api.interfaces import iworkspaceactionrunner, iworkspaceinfoprovider, ilogger
15+
from finecode_extension_api.interfaces import iworkspaceactionrunner, iworkspaceinfoprovider, ilogger, iuser_messenger
1416
from finecode_extension_api.interfaces.iworkspaceinfoprovider import actionable_project_paths
15-
from finecode_extension_api.resource_uri import resource_uri_to_path
17+
from finecode_extension_api.resource_uri import resource_uri_to_path, path_to_resource_uri
18+
from finecode_extension_api.workspace_utils import group_files_by_project
1619

1720

1821
@dataclasses.dataclass
@@ -29,10 +32,12 @@ def __init__(
2932
workspace_action_runner: iworkspaceactionrunner.IWorkspaceActionRunner,
3033
workspace_info_provider: iworkspaceinfoprovider.IWorkspaceInfoProvider,
3134
logger: ilogger.ILogger,
35+
user_messenger: iuser_messenger.IUserMessenger,
3236
) -> None:
3337
self.workspace_action_runner = workspace_action_runner
3438
self.workspace_info_provider = workspace_info_provider
3539
self.logger = logger
40+
self.user_messenger = user_messenger
3641

3742
async def _run_lint_for_project(
3843
self,
@@ -41,6 +46,10 @@ async def _run_lint_for_project(
4146
run_meta: code_action.RunActionMeta,
4247
partial_result_sender: code_action.PartialResultSender,
4348
) -> None:
49+
self.logger.debug(
50+
f"LintInspectCodeBridgeHandler: running LintAction for project={project_path}"
51+
f" file_paths={payload.file_paths}"
52+
)
4453
results = await self.workspace_action_runner.run_action_in_projects(
4554
action_type=LintAction,
4655
payload=LintRunPayload(
@@ -51,26 +60,49 @@ async def _run_lint_for_project(
5160
meta=run_meta,
5261
project_paths=[project_path],
5362
)
54-
for result in results.values():
63+
loguru_logger.debug(
64+
f"LintInspectCodeBridgeHandler: LintAction returned {len(results)} project results"
65+
f" projects={list(results.keys())}"
66+
)
67+
for proj_path, result in results.items():
68+
loguru_logger.debug(
69+
f"LintInspectCodeBridgeHandler: sending partial result for proj_path={proj_path}"
70+
f" messages keys={list(result.messages.keys())}"
71+
)
5572
await partial_result_sender.send(InspectCodeRunResult(messages=result.messages))
5673

5774
async def run(
5875
self,
5976
payload: InspectCodeRunPayload,
6077
run_context: InspectCodeRunContext,
6178
) -> None:
62-
project_paths = (
63-
[resource_uri_to_path(uri) for uri in payload.project_paths]
64-
if payload.project_paths is not None
65-
else actionable_project_paths(await self.workspace_info_provider.get_workspace_projects())
66-
)
79+
if payload.project_paths is not None:
80+
project_paths = [resource_uri_to_path(uri) for uri in payload.project_paths]
81+
else:
82+
project_paths = actionable_project_paths(await self.workspace_info_provider.get_workspace_projects())
6783

84+
if payload.target == InspectCodeTarget.FILES:
85+
if not payload.file_paths:
86+
return
87+
file_abs_paths = [resource_uri_to_path(uri) for uri in payload.file_paths]
88+
project_to_files = group_files_by_project(file_abs_paths, project_paths)
89+
tasks = [
90+
(project_path, dataclasses.replace(payload, file_paths=[path_to_resource_uri(f) for f in files]))
91+
for project_path, files in project_to_files.items()
92+
]
93+
if not tasks:
94+
self.user_messenger.warning(
95+
f"LintInspectCodeBridgeHandler: none of the requested files matched a known "
96+
f"project — no lint will run. file_paths={payload.file_paths}"
97+
)
98+
else:
99+
tasks = [(project_path, payload) for project_path in project_paths]
68100
async with asyncio.TaskGroup() as tg:
69-
for project_path in project_paths:
101+
for project_path, project_payload in tasks:
70102
tg.create_task(
71103
self._run_lint_for_project(
72104
project_path,
73-
payload,
105+
project_payload,
74106
run_context.meta,
75107
run_context.partial_result_sender,
76108
)

presets/fine_python_lint/fine_python_lint/preset.toml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
[tool.finecode]
2+
presets = [{ source = "fine_lint" }]
3+
14
[tool.finecode.action.lint_python_files]
25
source = "fine_python_lang.LintPythonFilesAction"
36
handlers = [
@@ -7,9 +10,6 @@ handlers = [
710
{ name = "flake8", source = "fine_python_flake8.Flake8LintFilesHandler", env = "dev_no_runtime", dependencies = [
811
"fine_python_flake8~=0.3.0a0",
912
] },
10-
{ name = "pyrefly", source = "fine_python_pyrefly.PyreflyLintFilesHandler", env = "dev_no_runtime", dependencies = [
11-
"fine_python_pyrefly[jsonrpc]~=0.2.0a0",
12-
] },
1313
]
1414

1515
[tool.finecode.action.get_lint_fixes_python_files]
@@ -26,12 +26,12 @@ dependencies_override = ["ruff==0.15.*"]
2626
[tool.finecode.extension.fine_python_flake8]
2727
dependencies_override = ["flake8==7.3.*"]
2828

29-
[tool.finecode.extension.fine_python_pyrefly]
30-
dependencies_override = ["pyrefly==0.64.*"]
31-
3229
[[tool.finecode.action_handler]]
3330
source = "fine_python_ruff.RuffLintFilesHandler"
34-
config.extend_select = ["B", "I"]
31+
# PLC0415 (import-outside-top-level): Checks for import statements outside of a module's
32+
# top-level scope, such as within a function or
33+
# class definition.
34+
config.extend_select = ["B", "I", "PLC0415"]
3535

3636
# flake8 is used only for custom rules, all standard rules are checked by ruff, but
3737
# keep flake8 configuration if someone activates some rules or uses flake8 config

0 commit comments

Comments
 (0)