mirror of
https://github.com/codeflash-ai/codeflash.git
synced 2026-05-04 18:25:17 +00:00
cleaning up
This commit is contained in:
parent
95d4863ff2
commit
a656022372
6 changed files with 7 additions and 483 deletions
|
|
@ -1,138 +0,0 @@
|
|||
---
|
||||
title: "Architecture"
|
||||
description: "Plugin components, data flow, and internal protocols"
|
||||
icon: "sitemap"
|
||||
sidebarTitle: "Architecture"
|
||||
---
|
||||
|
||||
## Data flow
|
||||
|
||||
```
|
||||
User types /optimize User commits code
|
||||
| |
|
||||
v v
|
||||
SKILL.md hooks.json
|
||||
(fork context) (Stop event, * matcher)
|
||||
| |
|
||||
v v
|
||||
optimizer agent suggest-optimize.sh
|
||||
(15 turns, inherited model) (bash, 30s timeout)
|
||||
| |
|
||||
v v
|
||||
Verify env & config Detect new commits
|
||||
| with .py/.js/.ts files
|
||||
v |
|
||||
codeflash CLI v
|
||||
(background, 10min timeout) Block Claude's stop
|
||||
| with suggestion message
|
||||
v |
|
||||
Results reported Claude acts on suggestion
|
||||
to user (install / configure / run)
|
||||
```
|
||||
|
||||
## Component inventory
|
||||
|
||||
| File | Type | Purpose |
|
||||
|------|------|---------|
|
||||
| `.claude-plugin/plugin.json` | Manifest | Plugin identity, version, metadata |
|
||||
| `.claude-plugin/marketplace.json` | Manifest | Marketplace listing, owner info |
|
||||
| `skills/optimize/SKILL.md` | Skill | `/optimize` slash command definition |
|
||||
| `commands/setup.md` | Command | `/setup` slash command for auto-permissions |
|
||||
| `agents/optimizer.md` | Agent | Background optimization agent with full workflow |
|
||||
| `hooks/hooks.json` | Hook config | Registers the Stop hook |
|
||||
| `scripts/suggest-optimize.sh` | Hook script | Commit detection, dedup, project discovery |
|
||||
| `scripts/find-venv.sh` | Helper script | Python venv auto-discovery |
|
||||
|
||||
## Skill format
|
||||
|
||||
Skills use YAML frontmatter in a Markdown file:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: optimize
|
||||
description: Optimize Python, JavaScript, or TypeScript code for performance using Codeflash
|
||||
user-invocable: true
|
||||
argument-hint: "[--file] [--function] [--subagent]"
|
||||
context: fork # Forks context so optimization doesn't pollute main conversation
|
||||
agent: codeflash:optimizer # Delegates to the optimizer agent
|
||||
allowed-tools: Task
|
||||
---
|
||||
```
|
||||
|
||||
The `context: fork` setting means the skill runs in a forked context — the optimizer agent gets its own conversation branch, keeping the main session clean.
|
||||
|
||||
## Agent format
|
||||
|
||||
Agents use YAML frontmatter followed by a system prompt:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: optimizer
|
||||
description: |
|
||||
Optimizes Python and JavaScript/TypeScript code for performance...
|
||||
model: inherit # Uses the same model as the parent conversation
|
||||
maxTurns: 15 # Maximum number of agent turns
|
||||
color: cyan # Status line color
|
||||
tools: Read, Glob, Grep, Bash, Write, Edit
|
||||
---
|
||||
```
|
||||
|
||||
The agent body contains the full workflow: project detection, environment verification, configuration setup, running codeflash, and error handling.
|
||||
|
||||
## Hook system
|
||||
|
||||
### `hooks.json` structure
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"Stop": [
|
||||
{
|
||||
"matcher": "*",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/suggest-optimize.sh",
|
||||
"timeout": 30
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **Event**: `Stop` — fires every time Claude finishes a response
|
||||
- **Matcher**: `*` — matches all stops (no filtering by tool or content)
|
||||
- **Timeout**: 30 seconds for the hook script to complete
|
||||
- **`${CLAUDE_PLUGIN_ROOT}`**: Resolved by Claude Code to the plugin's install directory
|
||||
|
||||
### Hook stdin/stdout protocol
|
||||
|
||||
**Input** (JSON on stdin):
|
||||
|
||||
```json
|
||||
{
|
||||
"stop_hook_active": false,
|
||||
"transcript_path": "/path/to/transcript.jsonl"
|
||||
}
|
||||
```
|
||||
|
||||
**Output** (JSON on stdout):
|
||||
|
||||
```json
|
||||
{"decision": "block", "reason": "message for Claude to act on"}
|
||||
```
|
||||
|
||||
Or no output / exit 0 to allow the stop (no blocking).
|
||||
|
||||
The `decision` field can be:
|
||||
- `"block"` — prevents Claude from stopping, injects `reason` as a new prompt for Claude to act on
|
||||
- Absent / script exits 0 without output — allows the stop
|
||||
|
||||
## State files
|
||||
|
||||
| File | Purpose | Lifetime |
|
||||
|------|---------|----------|
|
||||
| `/tmp/codeflash-hook-debug.log` | Debug output from the hook script (`set -x` stderr) | Persists across sessions until manually cleared |
|
||||
| `$TRANSCRIPT_DIR/codeflash-seen` | SHA-256 hashes of already-processed commit sets | Per-session (lives alongside the transcript file) |
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
---
|
||||
title: "Configuration"
|
||||
description: "Configuration reference for plugin manifests, project config, and Claude Code permissions"
|
||||
icon: "gear"
|
||||
sidebarTitle: "Configuration"
|
||||
---
|
||||
|
||||
## Plugin manifests
|
||||
|
||||
These files live in `.claude-plugin/` and define the plugin for Claude Code's plugin system. You generally don't need to modify them.
|
||||
|
||||
### `plugin.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "codeflash",
|
||||
"description": "Run codeflash as a background agent to optimize code for performance",
|
||||
"version": "0.1.10",
|
||||
"author": { "name": "Codeflash", "url": "https://codeflash.ai" },
|
||||
"repository": "https://github.com/codeflash-ai/codeflash-cc-plugin",
|
||||
"license": "MIT",
|
||||
"keywords": ["python", "javascript", "typescript", "optimization", "performance"]
|
||||
}
|
||||
```
|
||||
|
||||
### `marketplace.json`
|
||||
|
||||
Defines the plugin for the Claude Code marketplace. Contains owner info, metadata, and a `plugins` array with the same fields as `plugin.json` plus `source` (relative path) and `category`.
|
||||
|
||||
## Python project configuration
|
||||
|
||||
Codeflash reads its configuration from `[tool.codeflash]` in `pyproject.toml`.
|
||||
|
||||
### Full reference
|
||||
|
||||
```toml
|
||||
[tool.codeflash]
|
||||
# All paths are relative to this pyproject.toml's directory.
|
||||
module-root = "src" # Root of your Python module (where tests import from)
|
||||
tests-root = "tests" # Directory containing your test files
|
||||
ignore-paths = [] # Paths to exclude from optimization
|
||||
formatter-cmds = ["disabled"] # Formatter commands, or ["disabled"] to skip formatting
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
| Field | Required | Default | Description |
|
||||
|-------|----------|---------|-------------|
|
||||
| `module-root` | Yes | — | Relative path to the module root. If your tests do `from mypackage import ...`, then `mypackage/` is the module root |
|
||||
| `tests-root` | Yes | — | Relative path to the tests directory |
|
||||
| `ignore-paths` | No | `[]` | List of paths to exclude from optimization |
|
||||
| `formatter-cmds` | No | `["disabled"]` | List of formatter commands. Each can include flags, e.g. `"black --line-length 88 {file}"`. Use `["disabled"]` to skip formatting |
|
||||
|
||||
### Auto-discovery
|
||||
|
||||
When configuration is missing, the optimizer agent discovers values automatically:
|
||||
|
||||
- **module-root**: Uses Glob and Read to find the Python package directory (the one tests import from)
|
||||
- **tests-root**: Looks for directories named `tests` or `test`, or folders containing `test_*.py` files. Falls back to `tests` (creates it if needed)
|
||||
|
||||
## JS/TS project configuration
|
||||
|
||||
Codeflash reads its configuration from a `"codeflash"` key at the root level of `package.json`.
|
||||
|
||||
### Full reference
|
||||
|
||||
```json
|
||||
{
|
||||
"codeflash": {
|
||||
"moduleRoot": "src",
|
||||
"testsRoot": "tests",
|
||||
"formatterCmds": ["disabled"],
|
||||
"ignorePaths": ["dist", "**/node_modules", "**/__tests__"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
| Field | Required | Default | Description |
|
||||
|-------|----------|---------|-------------|
|
||||
| `moduleRoot` | Yes | — | Relative path to the JS/TS module root (e.g. `.` or `src`) |
|
||||
| `testsRoot` | Yes | — | Relative path to the tests directory |
|
||||
| `formatterCmds` | No | `["disabled"]` | Formatter commands. Use `npx` prefix for project-local tools, e.g. `"npx prettier --write {file}"` |
|
||||
| `ignorePaths` | No | `[]` | Glob patterns to exclude from optimization |
|
||||
|
||||
### Auto-discovery
|
||||
|
||||
When configuration is missing, the optimizer agent:
|
||||
|
||||
- **moduleRoot**: Inspects the project structure; typically `.` or `src`
|
||||
- **testsRoot**: Looks for `tests`, `test`, `__tests__`, or directories with `*.test.js`/`*.spec.ts` files. Falls back to `tests`
|
||||
|
||||
## Claude Code permissions
|
||||
|
||||
To allow codeflash to run without permission prompts, add the following to `.claude/settings.json` in your project root:
|
||||
|
||||
```json
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(*codeflash*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This can be set up automatically by running `/setup`.
|
||||
|
||||
### Scope options
|
||||
|
||||
The permission can be placed at different levels:
|
||||
|
||||
| File | Scope |
|
||||
|------|-------|
|
||||
| `.claude/settings.json` | Project-wide, shared with team (committed to git) |
|
||||
| `.claude/settings.local.json` | Project-wide, personal (gitignored) |
|
||||
| `~/.claude/settings.json` | User-wide, all projects |
|
||||
|
||||
<Note>
|
||||
The stop hook checks `.claude/settings.json` in the repo root to determine if auto-allow is already configured. If the `permissions.allow` array contains any entry matching `codeflash`, the hook skips adding auto-allow instructions to its suggestions.
|
||||
</Note>
|
||||
|
|
@ -69,10 +69,10 @@ You can continue working while codeflash optimizes in the background.
|
|||
|
||||
## Set up auto-permissions
|
||||
|
||||
Run `/setup` to allow codeflash to execute automatically without permission prompts:
|
||||
Run `/codeflash:setup` to allow codeflash to execute automatically without permission prompts:
|
||||
|
||||
```
|
||||
/setup
|
||||
/codeflash:setup
|
||||
```
|
||||
|
||||
This adds `Bash(*codeflash*)` to the `permissions.allow` array in `.claude/settings.json`. After this, the post-commit hook can trigger optimizations without asking each time.
|
||||
|
|
|
|||
|
|
@ -1,153 +0,0 @@
|
|||
---
|
||||
title: "Hook Lifecycle"
|
||||
description: "Deep dive into the Stop hook: commit detection, deduplication, and decision tree"
|
||||
icon: "arrows-rotate"
|
||||
sidebarTitle: "Hook Lifecycle"
|
||||
---
|
||||
|
||||
Deep dive into `scripts/suggest-optimize.sh` — the Stop hook that detects commits and suggests optimizations.
|
||||
|
||||
## When the hook fires
|
||||
|
||||
The hook fires on **every Claude stop** (every time Claude finishes a response). This is configured in `hooks/hooks.json` with a `*` matcher, meaning it runs unconditionally regardless of what Claude just did.
|
||||
|
||||
## Decision tree
|
||||
|
||||
The hook evaluates a series of conditions and takes the first matching exit path:
|
||||
|
||||
```
|
||||
1. stop_hook_active == true?
|
||||
YES → exit 0 (allow stop, prevent infinite loop)
|
||||
|
||||
2. Not inside a git repo?
|
||||
YES → exit 0
|
||||
|
||||
3. No transcript_path or file doesn't exist?
|
||||
YES → exit 0
|
||||
|
||||
4. Session start time unavailable?
|
||||
YES → exit 0
|
||||
|
||||
5. No commits with .py/.js/.ts/.jsx/.tsx files since session start?
|
||||
YES → exit 0
|
||||
|
||||
6. Commit hash set already seen (in codeflash-seen)?
|
||||
YES → exit 0
|
||||
|
||||
7. JS/TS project with JS changes?
|
||||
7a. Not configured → block: set up config (+ install if needed)
|
||||
7b. Configured, not installed → block: install codeflash
|
||||
7c. Configured + installed → block: run codeflash
|
||||
|
||||
8. No Python changes?
|
||||
YES → exit 0
|
||||
|
||||
9. Python project, no venv found?
|
||||
YES → block: create venv, install, configure, run
|
||||
|
||||
10. Python project with venv:
|
||||
10a. Not configured → block: set up config (+ install if needed)
|
||||
10b. Configured, not installed → block: install codeflash
|
||||
10c. Configured + installed → block: run codeflash
|
||||
```
|
||||
|
||||
<Note>
|
||||
Every `block` decision also appends auto-allow instructions if `Bash(*codeflash*)` is not yet in `.claude/settings.json`.
|
||||
</Note>
|
||||
|
||||
## Infinite loop prevention
|
||||
|
||||
When the hook blocks Claude's stop with a suggestion, Claude acts on it (e.g., runs codeflash). When Claude finishes that response, the hook fires again. To prevent an infinite loop:
|
||||
|
||||
1. Claude sets `stop_hook_active: true` in the hook input when it's responding to a previous hook block
|
||||
2. The hook checks this flag first and immediately exits if true
|
||||
|
||||
This means the hook only triggers once per "natural" Claude stop, not on stops caused by responding to hook suggestions.
|
||||
|
||||
## Session boundary detection
|
||||
|
||||
The hook needs to know when the current session started to find only commits made during this session.
|
||||
|
||||
1. It reads `transcript_path` from the hook input JSON
|
||||
2. It gets the transcript file's **birth time** (creation timestamp) using `stat`:
|
||||
- **macOS**: `stat -f %B <file>` (birth time)
|
||||
- **Linux**: `stat -c %W <file>` (birth time), falls back to `stat -c %Y` (modification time) if birth time is unavailable
|
||||
3. This timestamp becomes `SESSION_START`, used in `git log --after=@$SESSION_START`
|
||||
|
||||
## Commit detection
|
||||
|
||||
```bash
|
||||
git log --after="@$SESSION_START" --name-only --diff-filter=ACMR \
|
||||
--pretty=format: -- '*.py' '*.js' '*.ts' '*.jsx' '*.tsx'
|
||||
```
|
||||
|
||||
- `--after=@$SESSION_START` — only commits after session start (Unix timestamp)
|
||||
- `--diff-filter=ACMR` — Added, Copied, Modified, Renamed files only
|
||||
- `--pretty=format:` — suppress commit metadata, show only file names
|
||||
- File patterns filter to Python and JS/TS extensions
|
||||
|
||||
The results are sorted and deduplicated. The hook also determines which language families have changes (`HAS_PYTHON_CHANGES`, `HAS_JS_CHANGES`) by grepping the file list for extension patterns.
|
||||
|
||||
## Deduplication
|
||||
|
||||
The hook prevents suggesting optimization for the same set of commits twice:
|
||||
|
||||
1. It computes the commit hashes of all matching commits:
|
||||
```bash
|
||||
git log --after="@$SESSION_START" --pretty=format:%H \
|
||||
-- '*.py' '*.js' '*.ts' '*.jsx' '*.tsx'
|
||||
```
|
||||
2. It hashes the full list with SHA-256:
|
||||
```bash
|
||||
... | shasum -a 256 | cut -d' ' -f1
|
||||
```
|
||||
3. It checks this hash against `$TRANSCRIPT_DIR/codeflash-seen`
|
||||
4. If found, the hook exits (already processed)
|
||||
5. If not found, appends the hash and continues
|
||||
|
||||
The seen-marker file lives in the transcript directory, so it's scoped to the current session/project.
|
||||
|
||||
## Project detection (`detect_project`)
|
||||
|
||||
The `detect_project` function walks from `$PWD` upward to `$REPO_ROOT`:
|
||||
|
||||
1. At each directory level, check for `pyproject.toml` first, then `package.json`
|
||||
2. The **first** config file found determines the project type
|
||||
3. It records:
|
||||
- `PROJECT_TYPE`: `"python"` or `"js"`
|
||||
- `PROJECT_DIR`: directory containing the config file
|
||||
- `PROJECT_CONFIG_PATH`: full path to the config file
|
||||
- `PROJECT_CONFIGURED`: `"true"` if codeflash config section exists
|
||||
|
||||
For Python, it checks for `[tool.codeflash]` in `pyproject.toml`. For JS/TS, it checks for a `"codeflash"` key in `package.json` using `jq`.
|
||||
|
||||
The walk stops at `$REPO_ROOT` — it never searches above the git repository root.
|
||||
|
||||
## Auto-allow suggestion
|
||||
|
||||
Every `block` decision checks whether codeflash is already auto-allowed:
|
||||
|
||||
```bash
|
||||
SETTINGS_JSON="$REPO_ROOT/.claude/settings.json"
|
||||
jq -e '.permissions.allow // [] | any(test("codeflash"))' "$SETTINGS_JSON"
|
||||
```
|
||||
|
||||
If no matching entry exists, the block message appends instructions to add `Bash(*codeflash*)` to `permissions.allow`. This means after the first optimization, future runs won't need permission prompts.
|
||||
|
||||
## Debug logging
|
||||
|
||||
The hook writes all debug output to `/tmp/codeflash-hook-debug.log`:
|
||||
|
||||
```bash
|
||||
LOGFILE="/tmp/codeflash-hook-debug.log"
|
||||
exec 2>>"$LOGFILE"
|
||||
set -x
|
||||
```
|
||||
|
||||
All stderr (including bash trace output from `set -x`) is appended to this file. To debug hook issues:
|
||||
|
||||
```bash
|
||||
tail -f /tmp/codeflash-hook-debug.log
|
||||
```
|
||||
|
||||
The log persists across sessions and is not automatically cleaned up.
|
||||
|
|
@ -112,20 +112,3 @@ This can happen on large projects or with `--all`. Options:
|
|||
- Python: `which black` (or whichever formatter)
|
||||
- JS/TS: `npx prettier --version` (or whichever formatter)
|
||||
3. Set `formatter-cmds = ["disabled"]` or `"formatterCmds": ["disabled"]` to skip formatting entirely
|
||||
|
||||
## Debugging the hook script manually
|
||||
|
||||
Run the hook script directly to test it:
|
||||
|
||||
```bash
|
||||
echo '{"stop_hook_active": false, "transcript_path": "/path/to/transcript.jsonl"}' | \
|
||||
bash /path/to/codeflash-cc-plugin/scripts/suggest-optimize.sh
|
||||
```
|
||||
|
||||
Check the debug log for detailed trace output:
|
||||
|
||||
```bash
|
||||
tail -100 /tmp/codeflash-hook-debug.log
|
||||
```
|
||||
|
||||
The log includes every variable and branch taken (via `set -x`), making it straightforward to trace why the hook did or didn't trigger.
|
||||
|
|
|
|||
|
|
@ -23,24 +23,22 @@ sidebarTitle: "Usage Guide"
|
|||
| `/optimize src/utils.py` | Optimize all functions in `src/utils.py` |
|
||||
| `/optimize src/utils.py my_function` | Optimize only `my_function` in that file |
|
||||
| `/optimize --all` | Optimize the entire project |
|
||||
| `/optimize src/utils.py --no-pr` | Optimize without creating a PR |
|
||||
| `/optimize src/utils.py --effort high` | Set optimization effort level to high |
|
||||
|
||||
Flags can be combined: `/optimize src/utils.py my_function --no-pr --effort high`
|
||||
Flags can be combined: `/optimize src/utils.py my_function`
|
||||
|
||||
### What happens behind the scenes
|
||||
|
||||
1. The skill (defined in `skills/optimize/SKILL.md`) forks context and spawns the **optimizer agent**
|
||||
2. The agent locates your project config (`pyproject.toml` or `package.json`)
|
||||
2. The agent locates your project config (`pyproject.toml` or `package.json` or `codeflash.toml`)
|
||||
3. It verifies the codeflash CLI is installed and the project is configured
|
||||
4. It runs `codeflash --subagent` as a **background task** with a 10-minute timeout
|
||||
5. You're notified when optimization completes with results
|
||||
|
||||
The agent has up to **15 turns** to complete its work (install codeflash, configure the project, run optimization).
|
||||
|
||||
## The `/setup` command
|
||||
## The `/codeflash:setup` command
|
||||
|
||||
`/setup` configures auto-permissions so codeflash runs without prompting.
|
||||
`/codeflash:setup` configures auto-permissions so codeflash runs without prompting.
|
||||
|
||||
### What it does
|
||||
|
||||
|
|
@ -50,50 +48,6 @@ The agent has up to **15 turns** to complete its work (install codeflash, config
|
|||
4. Preserves any existing settings
|
||||
|
||||
<Info>
|
||||
Running `/setup` multiple times is safe — it's idempotent. If permissions are already configured, it reports "No changes needed."
|
||||
Running `/codeflash:setup` multiple times is safe — it's idempotent. If permissions are already configured, it reports "No changes needed."
|
||||
</Info>
|
||||
|
||||
## Automatic post-commit suggestions
|
||||
|
||||
After every Claude response (the **Stop** hook), the plugin checks whether you committed Python, JS, or TS files during the current session. If so, it suggests running `/optimize`.
|
||||
|
||||
### How commit detection works
|
||||
|
||||
1. The hook determines the session start time from the transcript file's creation timestamp
|
||||
2. It queries `git log --after=@<session_start>` for commits touching `*.py`, `*.js`, `*.ts`, `*.jsx`, `*.tsx` files
|
||||
3. It deduplicates so the same commits don't trigger suggestions twice
|
||||
4. If new commits are found, it blocks Claude's stop and injects a suggestion
|
||||
|
||||
The suggestion varies depending on the project state:
|
||||
|
||||
| State | Suggestion |
|
||||
|-------|------------|
|
||||
| Configured + installed | Run `codeflash --subagent` in the background |
|
||||
| Configured, not installed | Install codeflash first, then run |
|
||||
| Not configured | Auto-discover config, write it, then run |
|
||||
| No venv (Python) | Create venv, install codeflash, configure, then run |
|
||||
|
||||
If `Bash(*codeflash*)` is not yet in `.claude/settings.json`, the suggestion also includes adding it for auto-permissions.
|
||||
|
||||
## Python-specific workflow
|
||||
|
||||
For Python projects, the optimizer agent:
|
||||
|
||||
1. Checks for an active virtual environment (`$VIRTUAL_ENV`)
|
||||
2. If none, searches for `.venv` or `venv` directories in the project dir and repo root
|
||||
3. Verifies `codeflash` is installed in the venv (`$VIRTUAL_ENV/bin/codeflash --version`)
|
||||
4. Reads `[tool.codeflash]` from `pyproject.toml` for configuration
|
||||
5. Runs: `source $VIRTUAL_ENV/bin/activate && codeflash --subagent [flags]`
|
||||
|
||||
The agent also checks `formatter-cmds` in the config and verifies formatters are installed.
|
||||
|
||||
## JS/TS-specific workflow
|
||||
|
||||
For JavaScript/TypeScript projects, the optimizer agent:
|
||||
|
||||
1. Checks codeflash is available via `npx codeflash --version`
|
||||
2. Reads the `"codeflash"` key from `package.json` for configuration
|
||||
3. Always runs from the project root (the directory containing `package.json`)
|
||||
4. Runs: `npx codeflash --subagent [flags]`
|
||||
|
||||
No virtual environment is needed — JS/TS projects use `npx`/`npm` directly.
|
||||
|
|
|
|||
Loading…
Reference in a new issue