Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 10 additions & 47 deletions .github/ISSUE_TEMPLATE/bug-report.yml
Original file line number Diff line number Diff line change
@@ -1,57 +1,20 @@
name: Bug report
description: Report something that is not working in Free Claude Code.
name: Problem after installation
description: Report a problem using Free Claude Code after installing it.
labels:
- bug
body:
- type: input
id: fcc-version
- type: textarea
id: description
attributes:
label: FCC version
description: Run `fcc-server --version` and include one version exactly as printed, including any development suffix. If FCC did not install, enter `None`.
placeholder: "The version is 1.22.333, or None"
label: Describe the issue
description: Describe what happened, what you expected, and how to reproduce it. Include the provider, model, and harness you were using when relevant. Reports missing relevant details may be closed. You can attach screenshots here.
validations:
required: true

- type: dropdown
id: cli
attributes:
label: CLI
description: If the problem occurred while using a coding CLI, select it.
options:
- Claude Code (fcc-claude)
- Codex (fcc-codex)
- Pi (fcc-pi)

- type: input
id: selected-models
attributes:
label: Selected model(s)
description: If relevant, enter each model exactly as shown in the selector or configuration.
placeholder: nvidia_nim/nvidia/nemotron-3-super-120b-a12b

- type: textarea
id: model-mappings
attributes:
label: Model mappings
description: If relevant, enter the model mappings used when the problem occurred.
placeholder: |
MODEL_FABLE=
MODEL_OPUS=
MODEL_SONNET=
MODEL_HAIKU=

- type: input
id: providers
attributes:
label: Provider(s)
description: If relevant, enter the provider or providers involved.
placeholder: NVIDIA NIM, OpenRouter

- type: textarea
id: issue
id: doctor
attributes:
label: Issue
description: Describe what happened and include any useful reproduction details. Remove API keys and tokens from logs or screenshots.
placeholder: Tell us what went wrong.
label: FCC doctor output
description: Run `fcc-doctor` and paste the JSON it copies to your clipboard. If copying is unavailable, copy the printed JSON from your terminal.
render: json
validations:
required: true
12 changes: 12 additions & 0 deletions .github/ISSUE_TEMPLATE/installation-problem.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: Installation problem
description: Report a problem before FCC has been installed successfully.
labels:
- bug
body:
- type: textarea
id: description
attributes:
label: Installation issue
description: Describe what happened and include the error you received. You can attach screenshots here if helpful.
validations:
required: true
37 changes: 30 additions & 7 deletions .github/workflows/validate-bug-report-version.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,34 @@ jobs:
const fieldPattern = "^### FCC version\\r?\\n+\\s*([^\\r\\n]+)";
const releaseVersionPattern = "^v((?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*))$";
const versionPattern = "(?:^|[^0-9A-Za-z_.])v?([0-9]+\\.[0-9]+\\.[0-9]+(?:\\.dev[0-9]+(?:\\+g[0-9a-f]+(?:\\.d[0-9]{8})?)?|\\+d[0-9]{8})?)(?![0-9A-Za-z_+-]|\\.[0-9A-Za-z_])";
const match = (issue.body || "").match(new RegExp(fieldPattern, "m"));
const fieldValue = match?.[1]?.trim() || "";
const versionMatches = [...fieldValue.matchAll(new RegExp(versionPattern, "g"))];
const reportedVersion = versionMatches.length === 1 ? versionMatches[0][1] : undefined;
const valid = reportedVersion !== undefined || fieldValue === "None";
const parseReport = (body) => {
const sections = body.split(/^### /m);
const doctor = sections.filter((section) => /^FCC doctor output\r?\n/.test(section));
if (doctor.length) {
if (doctor.length !== 1) return { valid: false };
let text = doctor[0].replace(/^FCC doctor output\r?\n/, "").trim();
const fence = text.match(/^```(?:json)?\r?\n([\s\S]*)\r?\n```$/);
if (fence) text = fence[1];
let report;
try { report = JSON.parse(text); }
catch { return { valid: false }; }
const version = report?.version;
if (version === "0+unknown") return { valid: true };
if (typeof version !== "string") return { valid: false };
const matches = [...version.matchAll(new RegExp(versionPattern, "g"))];
if (matches.length !== 1 || matches[0][0] !== version) return { valid: false };
return { valid: true, reportedVersion: matches[0][1] };
}
const match = body.match(new RegExp(fieldPattern, "m"));
if (!match && sections.some((section) => /^Installation issue\r?\n/.test(section))) {
return { valid: true };
}
const fieldValue = match?.[1]?.trim() || "";
const versionMatches = [...fieldValue.matchAll(new RegExp(versionPattern, "g"))];
const reportedVersion = versionMatches.length === 1 ? versionMatches[0][1] : undefined;
return { valid: reportedVersion !== undefined || fieldValue === "None", reportedVersion };
};
const { reportedVersion, valid } = parseReport(issue.body || "");
const labelName = "needs-fcc-version";
const invalidMarker = "<!-- fcc-version-validator -->";
const outdatedMarker = "<!-- fcc-version-outdated -->";
Expand Down Expand Up @@ -82,7 +105,7 @@ jobs:
...repo,
name: labelName,
color: "d4c5f9",
description: "FCC version must contain one installed version or be None",
description: "Bug report needs valid FCC version information",
});
} catch (createError) {
if (createError.status !== 422) throw createError;
Expand All @@ -98,7 +121,7 @@ jobs:
await github.rest.issues.createComment({
...repo,
issue_number,
body: `${invalidMarker}\nPlease edit **FCC version** so it contains one version copied from \`fcc-server --version\`, including any development suffix, or is exactly \`None\`.`,
body: `${invalidMarker}\nPlease run \`fcc-doctor\` and paste its complete JSON into **FCC doctor output**. For an older issue with an **FCC version** field, enter one version from \`fcc-server --version\` or exactly \`None\`. If FCC has not installed successfully, use the Installation problem form.`,
});
}
return;
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,12 @@ Windows PowerShell:

## Project Links

For a problem after installation, run the command below and paste its output into your bug report. It also copies the JSON to your clipboard when available.

```bash
fcc-doctor
```

- [Report bugs or request features](https://github.com/Alishahryar1/free-claude-code/issues)
- [Contributing guide](CONTRIBUTING.md)

Expand Down
54 changes: 29 additions & 25 deletions e2e/test_admin_apply.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,32 @@ def record_config_mutation(method: str, url: str) -> None:
def test_key_rejection_retains_edits_and_restores_focus(
page: Page, admin_base_url: str
):
pending: list[Route] = []
page.route("**/admin/api/config/apply", lambda route: pending.append(route))
request_count = 0

def reject_key(route: Route) -> None:
nonlocal request_count
request_count += 1
expect(page.locator("#messageArea")).to_have_text("Checking API keys…")
expect(page.locator("#view-providers")).to_have_attribute("inert", "")
expect(page.locator("#applyButton")).to_be_disabled()
submitted = route.request.post_data_json
assert isinstance(submitted, dict)
assert submitted["values"] == {"MISTRAL_API_KEY": "bad-key"}
route.fulfill(
json={
"applied": False,
"errors": ["Rejected key"],
"credential_checks": [
{
"key": "MISTRAL_API_KEY",
"status": "rejected",
"message": "Check this API key.",
}
],
}
)

page.route("**/admin/api/config/apply", reject_key)
page.goto(f"{admin_base_url}/admin")
expect(page.locator("#messageArea")).to_have_text("")
page.get_by_role("button", name="Model Config", exact=True).click()
Expand All @@ -58,34 +82,14 @@ def test_key_rejection_retains_edits_and_restores_focus(
open_provider(page, "mistral")
key = page.locator("#field-MISTRAL_API_KEY")
key.fill("bad-key")
page.get_by_role("button", name="Save", exact=True).click()
expect(page.locator("#messageArea")).to_have_text("Checking API keys…")
expect(page.locator("#view-providers")).to_have_attribute("inert", "")
expect(page.locator("#applyButton")).to_be_disabled()
assert len(pending) == 1
submitted = pending[0].request.post_data_json
assert isinstance(submitted, dict)
assert submitted["values"] == {
"MISTRAL_API_KEY": "bad-key",
}
pending.pop().fulfill(
json={
"applied": False,
"errors": ["Rejected key"],
"credential_checks": [
{
"key": "MISTRAL_API_KEY",
"status": "rejected",
"message": "Check this API key.",
}
],
}
)
with page.expect_response("**/admin/api/config/apply"):
page.get_by_role("button", name="Save", exact=True).click()
expect(key).to_be_focused()
expect(key).to_have_attribute("aria-invalid", "true")
expect(page.locator("#field-MISTRAL_API_KEY-error")).to_have_text(
"Check this API key."
)
assert request_count == 1
expect(key).to_have_value("bad-key")
expect(other).to_have_value("open_router/other-edit")
expect(page.locator("#dirtyState")).to_have_text("1 unsaved change")
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,12 @@ dependencies = [
"grpcio>=1.84.0",
"grpcio-tools>=1.81.1",
"nvidia-riva-client>=2.26.0",
"pyperclip>=1.11.0",
]

[project.scripts]
fcc-server = "free_claude_code.cli.entrypoints:serve"
fcc-doctor = "free_claude_code.cli.entrypoints:doctor"
fcc-claude = "free_claude_code.cli.launchers.claude:launch"
fcc-codex = "free_claude_code.cli.launchers.codex:launch"
fcc-pi = "free_claude_code.cli.launchers.pi:launch"
Expand Down
3 changes: 2 additions & 1 deletion scripts/install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ $FccCommands = @(
"fcc-grok",
"fcc-muse",
"fcc-aider",
"fcc-doctor",
"fcc-update",
"fcc-init",
"free-claude-code"
Expand Down Expand Up @@ -1488,7 +1489,7 @@ function Configure-AndConfirmFreeClaudeCode {
[IO.Path]::AltDirectorySeparatorChar
)
$installedCommands = @{}
foreach ($commandName in @("fcc-desktop", "fcc-server", "fcc-claude", "fcc-codex", "fcc-pi", "fcc-opencode", "fcc-cline", "fcc-hermes", "fcc-dsh", "fcc-grok", "fcc-muse", "fcc-aider", "fcc-update.cmd")) {
foreach ($commandName in @("fcc-desktop", "fcc-server", "fcc-claude", "fcc-codex", "fcc-pi", "fcc-opencode", "fcc-cline", "fcc-hermes", "fcc-dsh", "fcc-grok", "fcc-muse", "fcc-aider", "fcc-doctor", "fcc-update.cmd")) {
$command = Get-ApplicationCommand $commandName
if (-not $command) {
throw "Free Claude Code installation did not create '$commandName'."
Expand Down
4 changes: 2 additions & 2 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ UV_INSTALL_URL="https://astral.sh/uv/install.sh"
FCC_MACOS_BUNDLE_ID="io.github.alishahryar1.free-claude-code"
FCC_MACOS_OWNER_FILE=".free-claude-code-owner"
# Include retired entry points so updates reject older FCC processes before replacement.
FCC_COMMANDS="fcc-desktop fcc-server fcc-claude fcc-codex fcc-pi fcc-opencode fcc-cline fcc-hermes fcc-dsh fcc-grok fcc-muse fcc-aider fcc-update fcc-init free-claude-code"
FCC_COMMANDS="fcc-desktop fcc-server fcc-claude fcc-codex fcc-pi fcc-opencode fcc-cline fcc-hermes fcc-dsh fcc-grok fcc-muse fcc-aider fcc-doctor fcc-update fcc-init free-claude-code"

dry_run=0
voice_local=0
Expand Down Expand Up @@ -1325,7 +1325,7 @@ configure_and_verify_free_claude_code() {

add_uv_tool_bin_directory

for command_name in fcc-desktop fcc-server fcc-claude fcc-codex fcc-pi fcc-opencode fcc-cline fcc-hermes fcc-dsh fcc-grok fcc-muse fcc-aider fcc-update; do
for command_name in fcc-desktop fcc-server fcc-claude fcc-codex fcc-pi fcc-opencode fcc-cline fcc-hermes fcc-dsh fcc-grok fcc-muse fcc-aider fcc-doctor fcc-update; do
[ -x "$tool_bin/$command_name" ] || fail "Free Claude Code installation did not create $tool_bin/$command_name."
done

Expand Down
1 change: 1 addition & 0 deletions scripts/uninstall.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ $FccCommands = @(
"fcc-grok",
"fcc-muse",
"fcc-aider",
"fcc-doctor",
"fcc-update",
"fcc-init",
"free-claude-code"
Expand Down
2 changes: 1 addition & 1 deletion scripts/uninstall.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ FCC_HOME_DIRNAME=".fcc"
FCC_MACOS_BUNDLE_ID="io.github.alishahryar1.free-claude-code"
FCC_MACOS_OWNER_FILE=".free-claude-code-owner"
# Include retired entry points so older installations are fully stopped and removed.
FCC_COMMANDS="fcc-desktop fcc-server fcc-claude fcc-codex fcc-pi fcc-opencode fcc-cline fcc-hermes fcc-dsh fcc-grok fcc-muse fcc-aider fcc-update fcc-init free-claude-code"
FCC_COMMANDS="fcc-desktop fcc-server fcc-claude fcc-codex fcc-pi fcc-opencode fcc-cline fcc-hermes fcc-dsh fcc-grok fcc-muse fcc-aider fcc-doctor fcc-update fcc-init free-claude-code"

dry_run=0
uv_tool_bin=""
Expand Down
13 changes: 11 additions & 2 deletions smoke/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# Product E2E Smoke Tests

`smoke/` is local-only. It can launch subprocesses, call real providers, touch
local model servers, and optionally send/delete bot messages. Hermetic contracts
belong under `tests/` and must stay green with plain `uv run pytest`.
local model servers, and optionally send/delete bot messages. Installer checks
also live here as an opt-in suite. Regular CI collects only `tests/` and `e2e/`.

## Taxonomy

- `smoke/installers/`: installer, uninstaller, and harness lifecycle checks with
isolated files and simulated downloads. These do not install real tools.
- `smoke/prereq/`: liveness checks that prove the server, routes, auth, CLI
scripts, provider pings, local `/models`, and bot permissions are reachable.
These are prerequisites only.
Expand All @@ -24,6 +26,13 @@ uv run pytest smoke -n 0 -s --tb=short
The second command skips everything unless `FCC_LIVE_SMOKE=1` is set, but still
writes skip entries to `.smoke-results/`.

Run only the installer checks:

```powershell
$env:FCC_LIVE_SMOKE = "1"
uv run pytest smoke/installers
```

## Product Smoke Run

```powershell
Expand Down
6 changes: 3 additions & 3 deletions tests/installers/AGENTS.md → smoke/installers/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ These rules apply to installer, uninstaller, and harness lifecycle tests here.
and abort ordering between installation phases. Preserve those assertions when
making tests faster. Prepare unrelated prerequisites as already installed.
- Keep coverage for both Windows PowerShell and PowerShell 7, plus the applicable
POSIX paths. Do not skip coverage or move it out of CI to hide slow tests.
POSIX paths. This suite is opt-in and does not run in regular CI.
- Stub downloads and external installers. Never install real harnesses, use live
providers, or modify the user's installed tools and configuration in these tests.
- Give every scenario private files, configuration, PATH, and cache storage. Use
Expand All @@ -22,7 +22,7 @@ These rules apply to installer, uninstaller, and harness lifecycle tests here.
or user configuration to save startup time. Clean up processes you launch.
- Diagnose slowness with per-test durations and timings inside subprocesses. Do not
increase timeouts or add sleeps without identifying the operation being delayed.
- Use `scripts/ci.ps1` for local verification. `PYTEST_ADDOPTS` can select focused
cases and enable duration reporting. Run the full relevant suite before pushing.
- Run these checks explicitly with `FCC_LIVE_SMOKE=1` and
`uv run pytest smoke/installers`. Select focused cases when checking a component.
- Reuse pytest's capture handler for logs. Do not create a logging cycle between
Python logging and Loguru while adding diagnostics.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"fcc-grok",
"fcc-muse",
"fcc-aider",
"fcc-doctor",
"fcc-update",
"fcc-init",
"free-claude-code",
Expand Down Expand Up @@ -191,6 +192,7 @@ def _posix_uv_command(version: str) -> str:
cp "$FAKE_FIXTURES/fcc-command.sh" "$tool_bin/fcc-grok"
cp "$FAKE_FIXTURES/fcc-command.sh" "$tool_bin/fcc-muse"
cp "$FAKE_FIXTURES/fcc-command.sh" "$tool_bin/fcc-aider"
cp "$FAKE_FIXTURES/fcc-command.sh" "$tool_bin/fcc-doctor"
cp "$FAKE_FIXTURES/fcc-command.sh" "$tool_bin/fcc-update"
if [ "$FAIL_STEP" != "fcc-missing" ]; then
cp "$FAKE_FIXTURES/fcc-command.sh" "$tool_bin/fcc-codex"
Expand Down Expand Up @@ -2107,6 +2109,7 @@ def _batch_uv(version: str) -> str:
copy /y "%FAKE_FIXTURES%\fcc-command.cmd" "%UV_BIN_DIR%\fcc-grok.cmd" >nul
copy /y "%FAKE_FIXTURES%\fcc-command.cmd" "%UV_BIN_DIR%\fcc-muse.cmd" >nul
copy /y "%FAKE_FIXTURES%\fcc-command.cmd" "%UV_BIN_DIR%\fcc-aider.cmd" >nul
copy /y "%FAKE_FIXTURES%\fcc-command.cmd" "%UV_BIN_DIR%\fcc-doctor.cmd" >nul
copy /y "%FAKE_FIXTURES%\fcc-command.cmd" "%UV_BIN_DIR%\fcc-update.cmd" >nul
if not "%FAIL_STEP%"=="fcc-missing" copy /y "%FAKE_FIXTURES%\fcc-command.cmd" "%UV_BIN_DIR%\fcc-codex.cmd" >nul
exit /b 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"fcc-grok",
"fcc-muse",
"fcc-aider",
"fcc-doctor",
"fcc-update",
"fcc-init",
"free-claude-code",
Expand Down Expand Up @@ -192,7 +193,7 @@ def posix_uninstall_harness(tmp_path: Path) -> PosixUninstallHarness:
echo 'Tool `free-claude-code` is not installed' >&2
exit 2
fi
for name in fcc-desktop fcc-server fcc-claude fcc-codex fcc-pi fcc-opencode fcc-cline fcc-hermes fcc-dsh fcc-grok fcc-muse fcc-aider fcc-update fcc-init free-claude-code; do
for name in fcc-desktop fcc-server fcc-claude fcc-codex fcc-pi fcc-opencode fcc-cline fcc-hermes fcc-dsh fcc-grok fcc-muse fcc-aider fcc-doctor fcc-update fcc-init free-claude-code; do
/bin/rm -f "$FAKE_TOOL_BIN/$name"
done
echo "Uninstalled free-claude-code"
Expand Down
14 changes: 14 additions & 0 deletions src/free_claude_code/cli/clipboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Best-effort text copy for the diagnostic command."""

import pyperclip


class ClipboardUnavailable(Exception):
"""The current desktop cannot accept clipboard text."""


def copy_text(text: str) -> None:
try:
pyperclip.copy(text)
except (pyperclip.PyperclipException, OSError) as error:
raise ClipboardUnavailable from error
Loading
Loading