mirror of
https://github.com/cline/cline.git
synced 2026-09-01 23:19:18 +08:00
Compare commits
160 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 55d78f4bfa | |||
| 4fc366df5f | |||
| d8eb06318b | |||
| fe4eb44c6b | |||
| 6c52bdc177 | |||
| 5260595472 | |||
| a279388451 | |||
| 7810a81efe | |||
| 2a54e2a76e | |||
| b7c38f76c9 | |||
| d20e517831 | |||
| fa3630da47 | |||
| 8229d0c9be | |||
| efa14b6cab | |||
| c10b417b78 | |||
| 9958e3f354 | |||
| ec75291d5b | |||
| a3a31da37d | |||
| a69d650838 | |||
| 7934d367a9 | |||
| 7f9d5461f1 | |||
| e1bdeeff68 | |||
| 49897830bb | |||
| 1f316a2734 | |||
| 9c1f9133c7 | |||
| 2faef2b40d | |||
| 64829bca8c | |||
| 4c9ba6b091 | |||
| 6138bdfe40 | |||
| 7d119351b1 | |||
| de987a5246 | |||
| 90050426df | |||
| 205c5676ff | |||
| 1c13edd395 | |||
| a2a1936709 | |||
| 35ce6a3f26 | |||
| 2c4aeae4f3 | |||
| 0c027d2731 | |||
| 6cc93c124e | |||
| 7e5b8be28c | |||
| 764e901693 | |||
| 2cabb2ddf6 | |||
| c32789f697 | |||
| 349a8da750 | |||
| f09dab7a0b | |||
| 3a3ea6ee96 | |||
| 1c1ea0bd53 | |||
| 70303d8541 | |||
| 8ba15dfca6 | |||
| 6ab6a1eabc | |||
| 2ad4146de1 | |||
| cfc2250717 | |||
| 730bac7f59 | |||
| 797ea1f607 | |||
| 9d59de4a4c | |||
| ae67ca7a13 | |||
| 7e2583f40c | |||
| ecca88bb98 | |||
| 4bb93ee5b9 | |||
| 4f2d7398ed | |||
| bc184f346d | |||
| 96aea0d34b | |||
| 676b446d47 | |||
| a8835425bf | |||
| e152741e1d | |||
| 6fec41ea70 | |||
| 5de1a45d1d | |||
| 717a2e643a | |||
| e50184d316 | |||
| 08c6f7ecbe | |||
| 15e6a685d6 | |||
| bc0eed950b | |||
| 48316027a7 | |||
| 4d15d16109 | |||
| d2339f57f1 | |||
| a209825116 | |||
| acc1a25e51 | |||
| 5f17a6963f | |||
| c7ddb96ddb | |||
| 107bce75b2 | |||
| 81792d20c6 | |||
| e8e2af705d | |||
| 8f00fcf3ed | |||
| a64d17734d | |||
| 8e621817c5 | |||
| 423fde4828 | |||
| 76af785fa6 | |||
| dd7042fc7b | |||
| 38e5b7c26b | |||
| 8ae99cd69f | |||
| 845970ba7d | |||
| 444a9be6ec | |||
| ade7775337 | |||
| e147682945 | |||
| ae78fb422c | |||
| 220a21bdcf | |||
| 6aef7f5280 | |||
| 4e56ed6922 | |||
| 3a0f182408 | |||
| 1f7adbd87e | |||
| b0590554da | |||
| af2454f8d9 | |||
| 1a4bf98e31 | |||
| 4139db4127 | |||
| d55916e3ab | |||
| 386ded5126 | |||
| 44e15319e4 | |||
| e424b28702 | |||
| db9971890e | |||
| d7cc9b6155 | |||
| 05042d3ff7 | |||
| dc2c662de6 | |||
| 4b68826cb5 | |||
| 85be70bb25 | |||
| c824d8380a | |||
| fb80840086 | |||
| 42e4ea60db | |||
| 31a118fc0c | |||
| c33c3176ef | |||
| f5a3c591c8 | |||
| be930a69a8 | |||
| 3651fe9a55 | |||
| 526d8e9c93 | |||
| fad8271f41 | |||
| d6837edda8 | |||
| d0f20ca135 | |||
| b2a113a2a5 | |||
| 5efa8cfd3f | |||
| 9f42aea85d | |||
| b87f61f9e4 | |||
| 38f1c7eb14 | |||
| 81121663a4 | |||
| 854ac75fe0 | |||
| 107f0f8337 | |||
| e330695cb5 | |||
| c2879dba43 | |||
| f2d692cfc2 | |||
| 0c90bd9bcf | |||
| 33e521e551 | |||
| 9e942fbb6b | |||
| 6f609e8945 | |||
| 762e3c42ab | |||
| 49e8c1b324 | |||
| 71b8f43a7a | |||
| 3068fcfedf | |||
| 7530900166 | |||
| 2b45b7b7aa | |||
| 791d238996 | |||
| 9ee618111d | |||
| 4bec5931f6 | |||
| 3115679031 | |||
| 8a6441fddd | |||
| 7ef5b1e7af | |||
| 3e58e7b024 | |||
| d16667f0b8 | |||
| a740c49524 | |||
| 3bc1ee8382 | |||
| ecf354c753 | |||
| a66c5ee973 | |||
| 46659eebeb |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
fix: use correct base URL for Vertex AI global endpoint with Claude models
|
||||
+89
-3
@@ -9,12 +9,13 @@ Use this skill when the user asks to release the CLI, publish `cline`, bump the
|
||||
|
||||
The CLI is npm-only. Do not add alternate distribution or signing steps.
|
||||
|
||||
> Working directory: this skill lives in the SDK sub-monorepo. Run `cd sdk` (from the repo root) before any of the shell commands below. Paths in commands and instructions (e.g. `apps/cli/package.json`, `bun release cli`) are written relative to `sdk/`.
|
||||
> Working directory: run every command below from the repository root. Paths and scripts (e.g. `apps/cli/package.json`, `sdk/packages/`, `bun release cli`, `bun run version`) are written relative to the repo root.
|
||||
|
||||
The skill should guide the user through one release preparation flow, then offer the publish path options. The two normal publish paths are GitHub Actions and local publishing from an authenticated machine.
|
||||
|
||||
## Release contract
|
||||
|
||||
- SDK prerequisite: the CLI depends on the SDK via `workspace:*` (`@cline/core`, `@cline/shared`, and friends). If the SDK changed since its last release, release the SDK first and wait for it to finish publishing before releasing the CLI. See "Step 0: Release the SDK first if it changed" below.
|
||||
- Version source: `apps/cli/package.json`.
|
||||
- Main release tag: `cli-vX.Y.Z`, where `X.Y.Z` matches `apps/cli/package.json`.
|
||||
- Nightly release version: `X.Y.Z-nightly.TIMESTAMP`.
|
||||
@@ -30,8 +31,93 @@ The skill should guide the user through one release preparation flow, then offer
|
||||
- Always ask before pushing commits or tags.
|
||||
- Do not amend commits unless explicitly requested.
|
||||
|
||||
## Step 0: Release the SDK first if it changed
|
||||
|
||||
Do this before anything else in the Workflow below.
|
||||
|
||||
The CLI builds and ships against the SDK source in the monorepo (`workspace:*` for `@cline/core`, `@cline/shared`, and the rest), so a CLI release always contains the latest SDK code whether or not the SDK was released. The build and tests use that source too, not anything from npm. Releasing the SDK alongside the CLI is still worth doing for two reasons:
|
||||
|
||||
- Hub freshness. The hub daemon lives in `@cline/core` and stamps a `buildId` that defaults to the `@cline/core` package version (`resolveHubBuildId` in `sdk/packages/core/src/hub/discovery/index.ts`). A running hub is only retired and respawned when that `buildId` changes (`isCompatibleHubRecord` / `retireIncompatibleHub` in `sdk/packages/core/src/hub/daemon/index.ts`). So if the SDK code changed but the version did not, a user who upgrades the CLI keeps talking to their already-running hub, which is still executing the old SDK code. Bumping the SDK version makes the new CLI's `buildId` differ, so the stale hub is detected as incompatible and respawned with the fresh code.
|
||||
- Release hygiene. We want regular SDK releases; cutting one whenever we cut a CLI release keeps the published SDK in step with what the CLI ships.
|
||||
|
||||
So when the SDK has changed, release it first (which bumps the `@cline/core` version), then cut the CLI release on top of that bump. Leave the CLI's SDK dependency as `workspace:*` — the fix is to release the SDK, not to pin the CLI.
|
||||
|
||||
1. Check for unreleased SDK changes.
|
||||
|
||||
```sh
|
||||
git fetch origin --tags
|
||||
git tag --list 'sdk/sdk/v*' 'sdk-v*' --sort=-v:refname | head -1
|
||||
git log <last-sdk-tag>..origin/main --oneline --no-merges -- sdk/packages
|
||||
```
|
||||
|
||||
`sdk/<pkg>/v*` tags are created by the `sdk-publish.yml` workflow; `sdk-v*` tags are created by the local `bun release sdk` helper. Use whichever is newest as the baseline.
|
||||
|
||||
If `git log` prints no commits, the SDK is already up to date. Skip the rest of Step 0 and continue with the Workflow below.
|
||||
|
||||
If it prints commits, sanity-check the diff (ignore entries that are only the previous version-bump commit's lockfile or generated files), then release the SDK.
|
||||
|
||||
2. Decide the SDK version bump.
|
||||
|
||||
All SDK packages share one version, read from `sdk/packages/llms/package.json`. Ask whether this is patch, minor, major, or an explicit version. Patch is the default. Do not guess if the user has not made it clear.
|
||||
|
||||
3. Draft the SDK release notes and update the changelog.
|
||||
|
||||
Draft user-facing notes from the SDK commits found in step 1, translating commit messages into user-facing language (same approach as the CLI release notes below). Prepend a new `## <version>` section with those notes to the top of `sdk/CHANGELOG.md`, using the header format `## <version>` with no date — the same flat, newest-on-top format as `apps/cli/CHANGELOG.md`. This is the SDK changelog (all SDK packages share one version) and it is maintained by hand; the `sdk-publish.yml` workflow does not read it.
|
||||
|
||||
4. Bump versions and regenerate.
|
||||
|
||||
```sh
|
||||
bun run version <version>
|
||||
```
|
||||
|
||||
This bumps every SDK `package.json` to the new version, regenerates the lockfile and the generated model catalog, formats, and builds. Review the result.
|
||||
|
||||
5. Commit and push the bump to `main`.
|
||||
|
||||
The `sdk-publish.yml` workflow publishes the version that is committed on `main` and tags that commit, so the bump must land on `main` before the workflow runs.
|
||||
|
||||
```sh
|
||||
git add -A
|
||||
git commit -m "chore(sdk): release v<version>"
|
||||
```
|
||||
|
||||
Ask before pushing:
|
||||
|
||||
```sh
|
||||
git push origin HEAD
|
||||
```
|
||||
|
||||
6. Trigger the SDK publish workflow on the `latest` channel.
|
||||
|
||||
```sh
|
||||
gh workflow run sdk-publish.yml -f channel=latest -f confirm_publish=publish
|
||||
gh run list --workflow=sdk-publish.yml --limit=1 --json databaseId,url,status,createdAt --jq '.[0]'
|
||||
```
|
||||
|
||||
The workflow runs the SDK tests, publishes `@cline/shared`, `@cline/llms`, `@cline/agents`, `@cline/core`, and `@cline/sdk` to npm with the `latest` dist-tag in dependency order, and pushes `sdk/<pkg>/v<version>` git tags.
|
||||
|
||||
7. Wait for the SDK workflow to succeed before starting the CLI release.
|
||||
|
||||
```sh
|
||||
gh run watch <run-id> --exit-status
|
||||
```
|
||||
|
||||
Do not start the CLI release until this run has finished successfully. The CLI does not install the SDK from npm, but cutting the CLI release on top of a clean, completed SDK release keeps the two in step: the CLI release commit then sits on top of the `@cline/core` version bump, so the shipped CLI carries the new version that forces a running hub to respawn with the new code, and you are not building a CLI release on top of an SDK release that failed midway.
|
||||
|
||||
After the SDK release succeeds, pull `main` so the CLI release is prepared on top of the SDK version bump:
|
||||
|
||||
```sh
|
||||
git checkout main && git pull --ff-only
|
||||
```
|
||||
|
||||
Then continue with the Workflow below.
|
||||
|
||||
For a local SDK publish from an authenticated machine instead of the workflow, `bun release sdk <version>` exists, but prefer the `sdk-publish.yml` workflow for normal releases so the CLI release can gate on a single GitHub Actions run.
|
||||
|
||||
## Workflow
|
||||
|
||||
Complete Step 0 first. Only proceed once the SDK is released (or you confirmed no SDK release was needed).
|
||||
|
||||
1. Gather context.
|
||||
|
||||
```sh
|
||||
@@ -46,10 +132,10 @@ Find the latest CLI tag. If there is no `cli-v*` tag, use the first relevant CLI
|
||||
2. Collect release commits.
|
||||
|
||||
```sh
|
||||
git log <last-cli-tag>..HEAD --oneline --no-merges -- apps/cli packages scripts .github/workflows/cli-publish.yml
|
||||
git log <last-cli-tag>..HEAD --oneline --no-merges -- apps/cli sdk/packages sdk/scripts .github/workflows/cli-publish.yml
|
||||
```
|
||||
|
||||
If the release includes broader SDK changes that affect the CLI, also inspect commits outside `apps/cli`.
|
||||
The `sdk/packages` commits matter here even though the SDK was released separately in Step 0: the CLI bundles the SDK, so SDK changes ship in this CLI release too. Read those commits and fold anything user-relevant to the CLI into the release notes (provider/model updates, behavior changes, fixes the CLI inherits). Skip SDK changes that are purely internal or have no CLI-visible effect.
|
||||
|
||||
3. Draft user-facing release notes.
|
||||
|
||||
+9
-13
@@ -140,12 +140,10 @@ Adding a new key to global state requires updates in multiple places. Missing an
|
||||
|
||||
Required steps:
|
||||
1. Type definition in `src/shared/storage/state-keys.ts` - Add to `GlobalState` or `Settings` interface
|
||||
2. Read from globalState in `src/core/storage/utils/state-helpers.ts`:
|
||||
- Add `const myKey = context.globalState.get<GlobalStateAndSettings["myKey"]>("myKey")` in `readGlobalStateFromDisk()`
|
||||
- Add to the return object: `myKey: myKey ?? defaultValue,`
|
||||
3. StateManager handles read/write via `setGlobalState()`/`getGlobalStateKey()` after initialization
|
||||
2. Add any default value or transform in `src/shared/storage/state-keys.ts` if the key needs one
|
||||
3. Read and write the value through `StateManager` (`setGlobalState()` / `getGlobalStateKey()`) after initialization
|
||||
|
||||
Common mistake: Adding only the return value without the `context.globalState.get()` call. This compiles but the value is always `undefined` on load.
|
||||
Persistent state is file-backed through `StateManager`; do not add new runtime reads or writes against VS Code `ExtensionContext` storage. That storage is only a legacy migration source.
|
||||
|
||||
Settings plumbing gotcha: if a key is user-toggleable from settings, wire both controller update paths:
|
||||
- `src/core/controller/state/updateSettings.ts` for webview `updateSetting(...)`
|
||||
@@ -159,22 +157,20 @@ Webview toggle gotcha: settings changes must also round-trip back in state paylo
|
||||
If this round-trip wiring is missing, the backend value can update but the toggle in webview appears stuck or reverts.
|
||||
|
||||
## StateManager Cache vs Direct globalState Access
|
||||
StateManager uses an in-memory cache populated during `StateManager.initialize(context)` in `common.ts`. For most state, use `controller.stateManager.setGlobalState()`/`getGlobalStateKey()`.
|
||||
StateManager uses an in-memory cache populated during `StateManager.initialize()` from file-backed storage. For most state, use `controller.stateManager.setGlobalState()`/`getGlobalStateKey()`.
|
||||
|
||||
Exception: State needed immediately at extension startup (before cache is ready)
|
||||
Exception: host migration code may read legacy VS Code storage before file-backed storage is initialized.
|
||||
|
||||
When Window A sets state and immediately opens Window B, the new window's StateManager cache is populated from `context.globalState` during initialization. If you need to read state in Window B right at startup (e.g., in `common.ts` during `initialize()`), read directly from `context.globalState.get()` instead of StateManager's cache.
|
||||
|
||||
Example pattern (see `lastShownAnnouncementId` and `worktreeAutoOpenPath`):
|
||||
Example pattern:
|
||||
```typescript
|
||||
// Writing (normal pattern)
|
||||
controller.stateManager.setGlobalState("myKey", value)
|
||||
|
||||
// Reading at startup in common.ts (bypass cache)
|
||||
const value = context.globalState.get<string>("myKey")
|
||||
// Reading after initialization
|
||||
const value = controller.stateManager.getGlobalStateKey("myKey")
|
||||
```
|
||||
|
||||
This is only needed for cross-window state read during the brief startup window before StateManager cache is fully usable. Normal state access after initialization should use StateManager.
|
||||
Use `context.globalState` only in VS Code migration code that copies legacy ExtensionContext values into the shared file-backed stores.
|
||||
|
||||
## ChatRow Cancelled/Interrupted States
|
||||
When a ChatRow displays a loading/in-progress state (spinner), you must handle what happens when the task is cancelled. This is non-obvious because cancellation doesn't update the message content—you have to infer it from context.
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
/.github/ @saoudrizwan @arafatkatze @maxpaulus43 @candieduniverse
|
||||
/.github/ @saoudrizwan @arafatkatze @maxpaulus43 @dominiccooney
|
||||
/README.md @saoudrizwan @juanpflores
|
||||
|
||||
@@ -7,10 +7,10 @@ body:
|
||||
value: |
|
||||
**Important:** All bug reports must be reproducible using Claude Sonnet 4.5. Cline uses complex prompts so less capable models may not work as expected.
|
||||
- type: dropdown
|
||||
id: plugin-type
|
||||
id: cline-surface
|
||||
attributes:
|
||||
label: Plugin Type
|
||||
description: Which plugin are you reporting a bug for?
|
||||
label: Cline Surface
|
||||
description: Which Cline surface are you reporting a bug for?
|
||||
options:
|
||||
- VSCode Extension
|
||||
- JetBrains Plugin
|
||||
@@ -59,6 +59,18 @@ body:
|
||||
placeholder: 'e.g., cline:anthropic/claude-sonnet-4.5, gemini:gemini-2.5-pro-exp-03-25'
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: ide-diagnostics
|
||||
attributes:
|
||||
label: IDE / CLI Diagnostics
|
||||
description: |
|
||||
Paste the "About" diagnostics for your Cline surface. This captures the IDE build, runtime, and host details we need.
|
||||
- VSCode Extension: open `Help → About` (Windows/Linux) or `Code → About Visual Studio Code` (macOS), then copy the info.
|
||||
- JetBrains Plugin: open `Help → About` (Windows/Linux) or `<IDE name> → About` (macOS), then click `Copy` to grab build, runtime, OS, memory, and cores.
|
||||
- CLI: there is no About dialog. Run `cline --version` and paste the output.
|
||||
placeholder: Paste the copied About info or `cline --version` output here.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: system-info
|
||||
attributes:
|
||||
|
||||
@@ -44,7 +44,7 @@ For Responses API providers: add to `isNextGenModelProvider()` in `src/utils/mod
|
||||
Modular: `components/` (shared) + `variants/` (model-specific) + `templates/` (`{{PLACEHOLDER}}`). Variants override components via `componentOverrides` in `config.ts` or custom `template.ts`. XS variant is heavily condensed inline. Always regenerate snapshots after changes.
|
||||
|
||||
## Global State Keys (silent failure risk)
|
||||
Adding a key requires: type in `src/shared/storage/state-keys.ts`, read via `context.globalState.get()` in `src/core/storage/utils/state-helpers.ts` `readGlobalStateFromDisk()`, and add to return object. Missing the `.get()` call compiles fine but value is always `undefined`.
|
||||
Adding a key requires updating the typed storage definitions in `src/shared/storage/state-keys.ts`; runtime reads and writes should go through `StateManager`, not VS Code `ExtensionContext` storage. Persistent state is file-backed so it works across VS Code, CLI, and JetBrains hosts.
|
||||
|
||||
## Slash Commands (3 places)
|
||||
- `src/core/slash-commands/index.ts` — definitions.
|
||||
|
||||
@@ -2,7 +2,7 @@ version: 2
|
||||
updates:
|
||||
# Main extension dependencies
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
directory: "/apps/vscode"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
# Group all updates into a single PR
|
||||
@@ -20,7 +20,7 @@ updates:
|
||||
|
||||
# Webview UI dependencies
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/webview-ui"
|
||||
directory: "/apps/vscode/webview-ui"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
groups:
|
||||
|
||||
@@ -33,7 +33,7 @@ permissions:
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: sdk
|
||||
working-directory: .
|
||||
|
||||
jobs:
|
||||
publish-main:
|
||||
@@ -105,12 +105,12 @@ jobs:
|
||||
PACKAGE_VERSION=$(node -p "require('./apps/cli/package.json').version")
|
||||
|
||||
if [ "$PACKAGE_VERSION" != "$VERSION" ]; then
|
||||
echo "sdk/apps/cli/package.json version ${PACKAGE_VERSION} does not match ${TAG}"
|
||||
echo "apps/cli/package.json version ${PACKAGE_VERSION} does not match ${TAG}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! printf "%s\n" "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then
|
||||
echo "sdk/apps/cli/package.json has invalid version: ${VERSION}"
|
||||
echo "apps/cli/package.json has invalid version: ${VERSION}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -147,7 +147,7 @@ jobs:
|
||||
|
||||
- name: Build platform binaries
|
||||
run: bun script/build.ts --install-native-variants --skip-sdk-build
|
||||
working-directory: sdk/apps/cli
|
||||
working-directory: apps/cli
|
||||
env:
|
||||
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
|
||||
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
|
||||
@@ -194,7 +194,7 @@ jobs:
|
||||
env:
|
||||
NPM_CONFIG_PROVENANCE: "true"
|
||||
run: bun script/publish-npm.ts --tag latest
|
||||
working-directory: sdk/apps/cli
|
||||
working-directory: apps/cli
|
||||
|
||||
- name: Get Previous CLI Tag
|
||||
id: prev_tag
|
||||
@@ -375,7 +375,7 @@ jobs:
|
||||
- name: Build platform binaries
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: bun script/build.ts --install-native-variants --skip-sdk-build
|
||||
working-directory: sdk/apps/cli
|
||||
working-directory: apps/cli
|
||||
env:
|
||||
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
|
||||
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
|
||||
@@ -424,7 +424,7 @@ jobs:
|
||||
env:
|
||||
NPM_CONFIG_PROVENANCE: "true"
|
||||
run: bun script/publish-npm.ts --tag nightly
|
||||
working-directory: sdk/apps/cli
|
||||
working-directory: apps/cli
|
||||
|
||||
- name: Summary
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
|
||||
@@ -15,9 +15,11 @@ jobs:
|
||||
trigger-integration-test:
|
||||
name: Run Tests
|
||||
runs-on: ubuntu-latest
|
||||
# Run on PR open/reopen, or when someone comments /test-jetbrains on a PR
|
||||
# Auto-run only for trusted PR authors. Anyone else needs a maintainer
|
||||
# to opt their PR in by commenting /test-jetbrains.
|
||||
if: |
|
||||
github.event_name == 'pull_request_target' ||
|
||||
(github.event_name == 'pull_request_target' &&
|
||||
contains(fromJSON('["MEMBER","OWNER","COLLABORATOR"]'), github.event.pull_request.author_association)) ||
|
||||
(github.event_name == 'issue_comment' &&
|
||||
github.event.issue.pull_request &&
|
||||
contains(github.event.comment.body, '/test-jetbrains') &&
|
||||
@@ -27,8 +29,8 @@ jobs:
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: 1998650
|
||||
private-key: ${{ secrets.CLINE_JETBRAINS_WORKFLOW_KEY }}
|
||||
app-id: ${{ vars.CLINE_JETBRAINS_APP_ID }}
|
||||
private-key: ${{ secrets.CLINE_JETBRAINS_APP_KEY }}
|
||||
owner: cline
|
||||
repositories: intellij-plugin
|
||||
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
# TODO: Fold this workflow's SDK login changes into ext-vscode-publish-nightly.yml
|
||||
# and delete this file. Pinned to dpc/sdk-migration-simpler-login while Max is iterating.
|
||||
# Owner: Max Paulus
|
||||
name: ext-vscode-publish-nightly-sdk
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 12 * * *' # 4 AM PST (UTC-8) = 12 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
checks: write
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
# Keep the publish source pinned to one reviewed branch instead of accepting arbitrary refs.
|
||||
SDK_NIGHTLY_REF: dpc/sdk-migration-simpler-login
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish Cline New SDK Extension Nightly
|
||||
if: github.repository == 'cline/cline' && github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
environment: PublishNightly
|
||||
|
||||
steps:
|
||||
- name: Checkout trusted SDK nightly branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ env.SDK_NIGHTLY_REF }}
|
||||
lfs: true
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
# Keep publish environment aligned with test workflow/tooling lockfile expectations.
|
||||
# Newer LTS (Node 24 / npm 11) can make `npm list` fail with ELSPROBLEMS during vsce packaging.
|
||||
node-version: 22
|
||||
|
||||
- name: Install root dependencies
|
||||
run: npm ci --include=optional
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
run: cd webview-ui && npm ci --include=optional
|
||||
|
||||
- name: Install Publishing Tools
|
||||
run: npm install -g @vscode/vsce ovsx
|
||||
|
||||
- name: Publish SDK nightly extension
|
||||
env:
|
||||
VSCE_PAT: ${{ secrets.VSCE_PAT }}
|
||||
OVSX_PAT: ${{ secrets.OVSX_PAT }}
|
||||
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
|
||||
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
|
||||
CLINE_ENVIRONMENT: production
|
||||
# OpenTelemetry production defaults (can be overridden at runtime)
|
||||
OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }}
|
||||
OTEL_LOGS_EXPORTER: otlp
|
||||
OTEL_METRICS_EXPORTER: otlp
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
run: npm run publish:marketplace:nightly
|
||||
@@ -20,6 +20,7 @@ jobs:
|
||||
if: github.repository == 'cline/cline' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dpc/sdk-migration-simpler-login')
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
uses: ./.github/workflows/ext-vscode-test.yml
|
||||
|
||||
publish:
|
||||
@@ -30,6 +31,9 @@ jobs:
|
||||
if: github.repository == 'cline/cline' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dpc/sdk-migration-simpler-login')
|
||||
runs-on: ubuntu-latest
|
||||
environment: PublishNightly
|
||||
defaults:
|
||||
run:
|
||||
working-directory: apps/vscode
|
||||
|
||||
steps:
|
||||
- name: Checkout selected branch
|
||||
@@ -40,6 +44,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Show build source
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
echo "Building ref: $GITHUB_REF"
|
||||
echo "Building sha: $GITHUB_SHA"
|
||||
@@ -52,11 +57,13 @@ jobs:
|
||||
# Newer LTS (Node 24 / npm 11) can make `npm list` fail with ELSPROBLEMS during vsce packaging.
|
||||
node-version: 22
|
||||
|
||||
- name: Install root dependencies
|
||||
run: npm ci --include=optional
|
||||
- name: Install extension dependencies
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: npm --prefix apps/vscode ci --include=optional
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
run: cd webview-ui && npm ci --include=optional
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: npm --prefix apps/vscode/webview-ui ci --include=optional
|
||||
|
||||
- name: Install Publishing Tools
|
||||
run: npm install -g @vscode/vsce ovsx
|
||||
@@ -78,6 +85,7 @@ jobs:
|
||||
run: npm run publish:marketplace:nightly
|
||||
|
||||
- name: Tag published commit
|
||||
working-directory: ${{ github.workspace }}
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
|
||||
@@ -36,6 +36,9 @@ jobs:
|
||||
name: Publish Extension
|
||||
runs-on: ubuntu-latest
|
||||
environment: publish
|
||||
defaults:
|
||||
run:
|
||||
working-directory: apps/vscode
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -47,6 +50,7 @@ jobs:
|
||||
|
||||
- name: Resolve Release Tag
|
||||
id: resolve_tag
|
||||
working-directory: ${{ github.workspace }}
|
||||
env:
|
||||
TAG: ${{ github.event.inputs.tag }}
|
||||
AUTO_CREATE: ${{ github.event.inputs.auto_create_tag_from_main }}
|
||||
@@ -110,11 +114,13 @@ jobs:
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install root dependencies
|
||||
run: npm install --include=optional
|
||||
- name: Install extension dependencies
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: npm --prefix apps/vscode install --include=optional
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
run: cd webview-ui && npm install --include=optional
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: npm --prefix apps/vscode/webview-ui install --include=optional
|
||||
|
||||
- name: Install Publishing Tools
|
||||
run: npm install -g @vscode/vsce ovsx
|
||||
@@ -171,6 +177,7 @@ jobs:
|
||||
|
||||
- name: Get Previous Tag
|
||||
id: prev_tag
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
CURRENT_TAG="${{ steps.resolve_tag.outputs.tag }}"
|
||||
PREV_TAG=$(git describe --tags --abbrev=0 "$CURRENT_TAG^" 2>/dev/null || echo "")
|
||||
@@ -178,6 +185,7 @@ jobs:
|
||||
|
||||
- name: Get Changelog Entry
|
||||
id: changelog
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
# Get content between first ## [ and second ## [
|
||||
CONTENT=$(awk '/^## \[/{if(found) exit; found=1; next} found{print}' CHANGELOG.md)
|
||||
@@ -189,7 +197,7 @@ jobs:
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ steps.resolve_tag.outputs.tag }}
|
||||
files: "*.vsix"
|
||||
files: "apps/vscode/*.vsix"
|
||||
body: |
|
||||
${{ steps.changelog.outputs.content }}
|
||||
|
||||
|
||||
@@ -36,24 +36,24 @@ jobs:
|
||||
with:
|
||||
filters: |
|
||||
e2e:
|
||||
- 'src/**'
|
||||
- 'webview-ui/**'
|
||||
- 'proto/**'
|
||||
- 'tests/**'
|
||||
- 'scripts/**'
|
||||
- 'standalone/**'
|
||||
- 'assets/**'
|
||||
- 'walkthrough/**'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- 'buf.yaml'
|
||||
- 'tsconfig*.json'
|
||||
- 'biome.jsonc'
|
||||
- 'esbuild.mjs'
|
||||
- '.mocharc.json'
|
||||
- '.vscode-test.mjs'
|
||||
- '.vscodeignore'
|
||||
- 'playwright*.ts'
|
||||
- 'apps/vscode/src/**'
|
||||
- 'apps/vscode/webview-ui/**'
|
||||
- 'apps/vscode/proto/**'
|
||||
- 'apps/vscode/tests/**'
|
||||
- 'apps/vscode/scripts/**'
|
||||
- 'apps/vscode/standalone/**'
|
||||
- 'apps/vscode/assets/**'
|
||||
- 'apps/vscode/walkthrough/**'
|
||||
- 'apps/vscode/package.json'
|
||||
- 'apps/vscode/package-lock.json'
|
||||
- 'apps/vscode/buf.yaml'
|
||||
- 'apps/vscode/tsconfig*.json'
|
||||
- 'apps/vscode/biome.jsonc'
|
||||
- 'apps/vscode/esbuild.mjs'
|
||||
- 'apps/vscode/.mocharc.json'
|
||||
- 'apps/vscode/.vscode-test.mjs'
|
||||
- 'apps/vscode/.vscodeignore'
|
||||
- 'apps/vscode/playwright*.ts'
|
||||
- '.github/workflows/ext-vscode-test-e2e.yml'
|
||||
|
||||
matrix_prep:
|
||||
@@ -79,6 +79,9 @@ jobs:
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
defaults:
|
||||
run:
|
||||
working-directory: apps/vscode
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js environment
|
||||
@@ -91,24 +94,24 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
id: root-cache
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}
|
||||
path: apps/vscode/node_modules
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('apps/vscode/package-lock.json') }}
|
||||
|
||||
# Cache webview-ui dependencies - only reuse if package-lock.json exactly matches
|
||||
- name: Cache webview-ui dependencies
|
||||
uses: actions/cache@v4
|
||||
id: webview-cache
|
||||
with:
|
||||
path: webview-ui/node_modules
|
||||
key: ${{ runner.os }}-npm-webview-${{ hashFiles('webview-ui/package-lock.json') }}
|
||||
path: apps/vscode/webview-ui/node_modules
|
||||
key: ${{ runner.os }}-npm-webview-${{ hashFiles('apps/vscode/webview-ui/package-lock.json') }}
|
||||
|
||||
# Cache VS Code installation
|
||||
- name: Cache VS Code
|
||||
uses: actions/cache@v4
|
||||
id: vscode-cache
|
||||
with:
|
||||
path: .vscode-test
|
||||
key: vscode-${{ runner.os }}-stable-${{ hashFiles('.vscode-test.mjs', 'package.json') }}
|
||||
path: apps/vscode/.vscode-test
|
||||
key: vscode-${{ runner.os }}-stable-${{ hashFiles('apps/vscode/.vscode-test.mjs', 'apps/vscode/package.json') }}
|
||||
restore-keys: |
|
||||
vscode-${{ runner.os }}-stable-
|
||||
|
||||
@@ -121,17 +124,19 @@ jobs:
|
||||
~/.cache/ms-playwright
|
||||
~/Library/Caches/ms-playwright
|
||||
~/AppData/Local/ms-playwright
|
||||
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
|
||||
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('apps/vscode/package-lock.json') }}
|
||||
restore-keys: |
|
||||
playwright-browsers-${{ runner.os }}-
|
||||
|
||||
- name: Install root dependencies
|
||||
run: npm ci
|
||||
- name: Install extension dependencies
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: npm --prefix apps/vscode ci
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
run: cd webview-ui && npm ci
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: npm --prefix apps/vscode/webview-ui ci
|
||||
|
||||
- name: Install vsce
|
||||
run: npm install -g @vscode/vsce
|
||||
|
||||
@@ -36,38 +36,38 @@ jobs:
|
||||
with:
|
||||
filters: |
|
||||
vscode:
|
||||
- 'src/**'
|
||||
- 'webview-ui/**'
|
||||
- 'proto/**'
|
||||
- 'tests/**'
|
||||
- 'scripts/**'
|
||||
- 'standalone/**'
|
||||
- 'assets/**'
|
||||
- 'walkthrough/**'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- 'buf.yaml'
|
||||
- 'tsconfig*.json'
|
||||
- 'biome.jsonc'
|
||||
- 'esbuild.mjs'
|
||||
- '.mocharc.json'
|
||||
- '.nycrc*.json'
|
||||
- '.vscode-test.mjs'
|
||||
- 'test-setup.js'
|
||||
- 'apps/vscode/src/**'
|
||||
- 'apps/vscode/webview-ui/**'
|
||||
- 'apps/vscode/proto/**'
|
||||
- 'apps/vscode/tests/**'
|
||||
- 'apps/vscode/scripts/**'
|
||||
- 'apps/vscode/standalone/**'
|
||||
- 'apps/vscode/assets/**'
|
||||
- 'apps/vscode/walkthrough/**'
|
||||
- 'apps/vscode/package.json'
|
||||
- 'apps/vscode/package-lock.json'
|
||||
- 'apps/vscode/buf.yaml'
|
||||
- 'apps/vscode/tsconfig*.json'
|
||||
- 'apps/vscode/biome.jsonc'
|
||||
- 'apps/vscode/esbuild.mjs'
|
||||
- 'apps/vscode/.mocharc.json'
|
||||
- 'apps/vscode/.nycrc*.json'
|
||||
- 'apps/vscode/.vscode-test.mjs'
|
||||
- 'apps/vscode/test-setup.js'
|
||||
- '.github/workflows/ext-vscode-test.yml'
|
||||
testing_platform:
|
||||
- 'src/**'
|
||||
- 'proto/**'
|
||||
- 'standalone/**'
|
||||
- 'testing-platform/**'
|
||||
- 'tests/specs/**'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- 'buf.yaml'
|
||||
- 'tsconfig*.json'
|
||||
- 'esbuild.mjs'
|
||||
- '.vscodeignore'
|
||||
- 'scripts/**'
|
||||
- 'apps/vscode/src/**'
|
||||
- 'apps/vscode/proto/**'
|
||||
- 'apps/vscode/standalone/**'
|
||||
- 'apps/vscode/testing-platform/**'
|
||||
- 'apps/vscode/tests/specs/**'
|
||||
- 'apps/vscode/package.json'
|
||||
- 'apps/vscode/package-lock.json'
|
||||
- 'apps/vscode/buf.yaml'
|
||||
- 'apps/vscode/tsconfig*.json'
|
||||
- 'apps/vscode/esbuild.mjs'
|
||||
- 'apps/vscode/.vscodeignore'
|
||||
- 'apps/vscode/scripts/**'
|
||||
- '.github/workflows/ext-vscode-test.yml'
|
||||
|
||||
quality-checks:
|
||||
@@ -75,6 +75,9 @@ jobs:
|
||||
if: needs.detect-changes.outputs.vscode == 'true' || needs.detect-changes.outputs.testing_platform == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
name: Quality Checks
|
||||
defaults:
|
||||
run:
|
||||
working-directory: apps/vscode
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -85,16 +88,18 @@ jobs:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
cache-dependency-path: |
|
||||
package-lock.json
|
||||
webview-ui/package-lock.json
|
||||
apps/vscode/package-lock.json
|
||||
apps/vscode/webview-ui/package-lock.json
|
||||
|
||||
- name: Install root dependencies
|
||||
run: npm ci
|
||||
- name: Install extension dependencies
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: npm --prefix apps/vscode ci
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
run: cd webview-ui && npm ci
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: npm --prefix apps/vscode/webview-ui ci
|
||||
|
||||
- name: Run Quality Checks (Parallel)
|
||||
run: npm run ci:check-all
|
||||
@@ -113,6 +118,7 @@ jobs:
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
working-directory: apps/vscode
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -123,19 +129,22 @@ jobs:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
cache-dependency-path: |
|
||||
package-lock.json
|
||||
webview-ui/package-lock.json
|
||||
apps/vscode/package-lock.json
|
||||
apps/vscode/webview-ui/package-lock.json
|
||||
|
||||
- name: Install root dependencies
|
||||
run: npm ci
|
||||
- name: Install extension dependencies
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: npm --prefix apps/vscode ci
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
run: cd webview-ui && npm ci
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: npm --prefix apps/vscode/webview-ui ci
|
||||
|
||||
- name: Set up NPM on Windows
|
||||
if: runner.os == 'Windows'
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: |
|
||||
npm config set script-shell "C:\\Program Files\\Git\\bin\\bash.exe"
|
||||
|
||||
@@ -201,13 +210,16 @@ jobs:
|
||||
with:
|
||||
name: pr-coverage-reports
|
||||
path: |
|
||||
coverage-unit/lcov.info
|
||||
webview-ui/coverage/lcov.info
|
||||
apps/vscode/coverage-unit/lcov.info
|
||||
apps/vscode/webview-ui/coverage/lcov.info
|
||||
|
||||
test-platform-integration:
|
||||
needs: [detect-changes, quality-checks]
|
||||
if: needs.detect-changes.outputs.testing_platform == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: apps/vscode
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -218,17 +230,19 @@ jobs:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
cache-dependency-path: |
|
||||
package-lock.json
|
||||
webview-ui/package-lock.json
|
||||
testing-platform/package-lock.json
|
||||
apps/vscode/package-lock.json
|
||||
apps/vscode/webview-ui/package-lock.json
|
||||
apps/vscode/testing-platform/package-lock.json
|
||||
|
||||
- name: Install root dependencies
|
||||
run: npm ci
|
||||
- name: Install extension dependencies
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: npm --prefix apps/vscode ci
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install webview-ui dependencies
|
||||
run: cd webview-ui && npm ci
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: npm --prefix apps/vscode/webview-ui ci
|
||||
|
||||
- name: Download ripgrep binaries
|
||||
run: npm run download-ripgrep
|
||||
@@ -237,7 +251,8 @@ jobs:
|
||||
run: npm run compile-standalone
|
||||
|
||||
- name: Install testing platform dependencies
|
||||
run: cd testing-platform && npm ci
|
||||
working-directory: ${{ github.workspace }}
|
||||
run: npm --prefix apps/vscode/testing-platform ci
|
||||
|
||||
- name: Running testing platform integration spec tests
|
||||
timeout-minutes: 7
|
||||
@@ -247,7 +262,7 @@ jobs:
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test-platform-integration-core-coverage
|
||||
path: coverage/**/lcov.info
|
||||
path: apps/vscode/coverage/**/lcov.info
|
||||
|
||||
# Keep the required "test" check as a tiny aggregate gate instead of the conditional
|
||||
# VS Code matrix. GitHub treats conditionally skipped jobs as successful required
|
||||
@@ -309,7 +324,7 @@ jobs:
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: pr-coverage-reports
|
||||
path: .
|
||||
path: apps/vscode
|
||||
|
||||
- name: Upload core unit tests coverage to Qlty
|
||||
if: needs.detect-changes.outputs.vscode == 'true'
|
||||
@@ -318,7 +333,7 @@ jobs:
|
||||
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
|
||||
# we can merge multiple files if necessary
|
||||
files: |
|
||||
coverage-unit/lcov.info
|
||||
apps/vscode/coverage-unit/lcov.info
|
||||
tag: unit:core
|
||||
|
||||
- name: Upload webview-ui unit tests coverage to Qlty
|
||||
@@ -328,7 +343,7 @@ jobs:
|
||||
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
|
||||
# we can merge multiple files if necessary
|
||||
files: |
|
||||
webview-ui/coverage/lcov.info
|
||||
apps/vscode/webview-ui/coverage/lcov.info
|
||||
tag: unit:webview-ui
|
||||
add-prefix: webview-ui/
|
||||
|
||||
@@ -339,12 +354,12 @@ jobs:
|
||||
id: download-integration-coverage
|
||||
with:
|
||||
name: test-platform-integration-core-coverage
|
||||
path: integration-core-coverage-reports
|
||||
path: apps/vscode/integration-core-coverage-reports
|
||||
|
||||
- name: Upload core integration tests coverage to Qlty
|
||||
if: needs.detect-changes.outputs.testing_platform == 'true' && steps.download-integration-coverage.outcome == 'success'
|
||||
uses: qltysh/qlty-action/coverage@v2
|
||||
with:
|
||||
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
|
||||
files: integration-core-coverage-reports/**/lcov.info
|
||||
files: apps/vscode/integration-core-coverage-reports/**/lcov.info
|
||||
tag: integration:core
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
const labels = context.payload.issue.labels.map(l => l.name);
|
||||
|
||||
// Check if JetBrains Plugin is selected
|
||||
if (body.match(/###\s*Plugin Type\s*\n+JetBrains Plugin/i)) {
|
||||
if (body.match(/###\s*Cline Surface\s*\n+JetBrains Plugin/i)) {
|
||||
if (!labels.includes('JetBrains')) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
}
|
||||
|
||||
// Check if VSCode Extension is selected
|
||||
if (body.match(/###\s*Plugin Type\s*\n+VSCode Extension/i)) {
|
||||
if (body.match(/###\s*Cline Surface\s*\n+VSCode Extension/i)) {
|
||||
if (!labels.includes('VS Code')) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
}
|
||||
|
||||
// Check if CLI is selected
|
||||
if (body.match(/###\s*Plugin Type\s*\n+CLI/i)) {
|
||||
if (body.match(/###\s*Cline Surface\s*\n+CLI/i)) {
|
||||
if (!labels.includes('CLI')) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
|
||||
@@ -26,7 +26,7 @@ on:
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: sdk
|
||||
working-directory: .
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -148,7 +148,7 @@ jobs:
|
||||
env:
|
||||
CHANNEL: ${{ steps.channel.outputs.channel }}
|
||||
run: |
|
||||
BASE_VERSION=$(node -p "require('./packages/llms/package.json').version")
|
||||
BASE_VERSION=$(node -p "require('./sdk/packages/llms/package.json').version")
|
||||
|
||||
if [ "$CHANNEL" = "nightly" ]; then
|
||||
TIMESTAMP=$(date +%s)
|
||||
@@ -166,11 +166,11 @@ jobs:
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
env:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: bun scripts/version.ts "$VERSION"
|
||||
run: bun sdk/scripts/version.ts "$VERSION"
|
||||
|
||||
- name: Verify publishability
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
run: bun scripts/check-publish.ts
|
||||
run: bun sdk/scripts/check-publish.ts
|
||||
|
||||
- name: Prepare package tarball directory
|
||||
if: steps.check_commits.outputs.skip != 'true'
|
||||
@@ -187,7 +187,7 @@ jobs:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
echo "Publishing @cline/shared@${VERSION} with tag '${CHANNEL}'..."
|
||||
cd packages/shared
|
||||
cd sdk/packages/shared
|
||||
TARBALL=$(bun pm pack --destination "$RUNNER_TEMP/sdk-npm-packs" --quiet)
|
||||
npm publish "$RUNNER_TEMP/sdk-npm-packs/$(basename "$TARBALL")" --tag "$CHANNEL" --access public
|
||||
|
||||
@@ -199,7 +199,7 @@ jobs:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
echo "Publishing @cline/llms@${VERSION} with tag '${CHANNEL}'..."
|
||||
cd packages/llms
|
||||
cd sdk/packages/llms
|
||||
TARBALL=$(bun pm pack --destination "$RUNNER_TEMP/sdk-npm-packs" --quiet)
|
||||
npm publish "$RUNNER_TEMP/sdk-npm-packs/$(basename "$TARBALL")" --tag "$CHANNEL" --access public
|
||||
|
||||
@@ -211,7 +211,7 @@ jobs:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
echo "Publishing @cline/agents@${VERSION} with tag '${CHANNEL}'..."
|
||||
cd packages/agents
|
||||
cd sdk/packages/agents
|
||||
TARBALL=$(bun pm pack --destination "$RUNNER_TEMP/sdk-npm-packs" --quiet)
|
||||
npm publish "$RUNNER_TEMP/sdk-npm-packs/$(basename "$TARBALL")" --tag "$CHANNEL" --access public
|
||||
|
||||
@@ -223,7 +223,7 @@ jobs:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
echo "Publishing @cline/core@${VERSION} with tag '${CHANNEL}'..."
|
||||
cd packages/core
|
||||
cd sdk/packages/core
|
||||
TARBALL=$(bun pm pack --destination "$RUNNER_TEMP/sdk-npm-packs" --quiet)
|
||||
npm publish "$RUNNER_TEMP/sdk-npm-packs/$(basename "$TARBALL")" --tag "$CHANNEL" --access public
|
||||
|
||||
@@ -235,7 +235,7 @@ jobs:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
echo "Publishing @cline/sdk@${VERSION} with tag '${CHANNEL}'..."
|
||||
cd packages/sdk
|
||||
cd sdk/packages/sdk
|
||||
TARBALL=$(bun pm pack --destination "$RUNNER_TEMP/sdk-npm-packs" --quiet)
|
||||
npm publish "$RUNNER_TEMP/sdk-npm-packs/$(basename "$TARBALL")" --tag "$CHANNEL" --access public
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ permissions:
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: sdk
|
||||
working-directory: .
|
||||
|
||||
jobs:
|
||||
quality-checks:
|
||||
@@ -96,12 +96,12 @@ jobs:
|
||||
|
||||
- name: Run SDK Tests (Windows)
|
||||
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' && steps.build_cli_step.outcome == 'success' && matrix.os == 'windows-latest' }}
|
||||
run: bun -F './packages/**' test
|
||||
run: bun -F './sdk/packages/**' test
|
||||
|
||||
- name: Smoke test SQLite under Node
|
||||
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' && matrix.os != 'windows-latest' }}
|
||||
timeout-minutes: 10
|
||||
run: bun scripts/ci-node-smoke.ts
|
||||
run: bun sdk/scripts/ci-node-smoke.ts
|
||||
|
||||
- name: Run TUI e2e tests
|
||||
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' && steps.build_cli_step.outcome == 'success' && matrix.os == 'ubuntu-latest' && matrix.node-version == '24.x' }}
|
||||
@@ -109,4 +109,4 @@ jobs:
|
||||
|
||||
- name: Verify packages are publishable
|
||||
if: ${{ !cancelled() && steps.build_sdk_step.outcome == 'success' && steps.build_cli_step.outcome == 'success' && matrix.os == 'ubuntu-latest' && matrix.node-version == '24.x' }}
|
||||
run: bun scripts/check-publish.ts
|
||||
run: bun sdk/scripts/check-publish.ts
|
||||
|
||||
+16
-5
@@ -17,8 +17,8 @@ pnpm-lock.yaml
|
||||
.actrc
|
||||
CLAUDE.local.md
|
||||
|
||||
webview-ui/src/**/*.js
|
||||
webview-ui/src/**/*.js.map
|
||||
apps/vscode/webview-ui/src/**/*.js
|
||||
apps/vscode/webview-ui/src/**/*.js.map
|
||||
|
||||
# Ignore coverage directories and files
|
||||
coverage
|
||||
@@ -35,9 +35,9 @@ coverage-unit
|
||||
.worktrees
|
||||
|
||||
## Generated files ##
|
||||
src/generated/
|
||||
src/shared/proto/
|
||||
webview-ui/src/services/grpc-client.ts
|
||||
apps/vscode/src/generated/
|
||||
apps/vscode/src/shared/proto/
|
||||
apps/vscode/webview-ui/src/services/grpc-client.ts
|
||||
*.tsbuildinfo
|
||||
|
||||
# E2E Tests
|
||||
@@ -61,6 +61,17 @@ tests/**/cache
|
||||
# Should never be committed: only exists if a publish aborts mid-swap.
|
||||
.README.github.bak
|
||||
|
||||
# Tauri generated code
|
||||
apps/*/src-tauri/gen
|
||||
apps/*/src-tauri/bin
|
||||
apps/examples/*/src-tauri/gen
|
||||
apps/examples/*/src-tauri/bin
|
||||
# Tauri UI test snapshots
|
||||
apps/*/src/tests/.tui-test
|
||||
apps/*/src/tests/tui-traces
|
||||
apps/vscode/webview-ui/src/**/*.js
|
||||
apps/vscode/webview-ui/src/**/*.js.map
|
||||
|
||||
|
||||
# SDK Session files / User data
|
||||
.cline/data
|
||||
|
||||
@@ -6,15 +6,18 @@
|
||||
{
|
||||
"id": "sdk-tool-handler-telemetry",
|
||||
"rule": "Any new tool handler added to packages/agents/src or packages/core/src that performs a user-visible action (writes files, executes commands, modifies state, calls external APIs) must include a call to captureToolUsage() from packages/core/src/services/telemetry/core-events.ts, or emit a task.tool_used event via telemetry.capture(). Pure read-only helpers and getters are exempt. When in doubt, prefer instrumentation.",
|
||||
"scope": ["packages/agents/src/**", "packages/core/src/**"],
|
||||
"scope": [
|
||||
"sdk/packages/agents/src/**",
|
||||
"sdk/packages/core/src/**"
|
||||
],
|
||||
"severity": "high"
|
||||
},
|
||||
{
|
||||
"id": "sdk-session-lifecycle-telemetry",
|
||||
"rule": "New session start, end, or state-transition code paths in packages/core/src must call the appropriate typed helper from packages/core/src/services/telemetry/core-events.ts (captureTaskCreated, captureTaskCompleted, captureConversationTurnEvent, captureTokenUsage, etc.). Do not inline raw telemetry.capture() calls for session lifecycle events — always use the typed helper, which guarantees a consistent payload shape.",
|
||||
"scope": [
|
||||
"packages/core/src/cline-core/**",
|
||||
"packages/core/src/runtime/**"
|
||||
"sdk/packages/core/src/cline-core/**",
|
||||
"sdk/packages/core/src/runtime/**"
|
||||
],
|
||||
"severity": "high"
|
||||
},
|
||||
@@ -22,8 +25,8 @@
|
||||
"id": "sdk-no-raw-event-strings",
|
||||
"rule": "All telemetry event name strings must be sourced from CORE_TELEMETRY_EVENTS in packages/core/src/services/telemetry/core-events.ts. If a PR introduces a string literal in a telemetry.capture(), telemetry.captureRequired(), or recordCounter()/recordHistogram()/recordGauge() call that does not reference CORE_TELEMETRY_EVENTS, flag it. New events must be added to CORE_TELEMETRY_EVENTS first, with a typed capture helper created alongside them.",
|
||||
"scope": [
|
||||
"packages/core/src/**",
|
||||
"packages/agents/src/**",
|
||||
"sdk/packages/core/src/**",
|
||||
"sdk/packages/agents/src/**",
|
||||
"apps/cli/src/**",
|
||||
"apps/vscode/src/**"
|
||||
],
|
||||
@@ -32,13 +35,17 @@
|
||||
{
|
||||
"id": "sdk-auth-telemetry-completeness",
|
||||
"rule": "Any new OAuth or authentication provider added under packages/core/src/auth must emit all four lifecycle events using the typed helpers from core-events.ts: captureAuthStarted (at flow entry), captureAuthSucceeded + identifyAccount (on token success), captureAuthFailed (on error), and captureAuthLoggedOut (on token invalidation or explicit logout). Flag PRs that introduce a new auth flow file without all four. Cross-reference packages/core/src/auth/cline.ts and packages/core/src/auth/codex.ts as canonical examples.",
|
||||
"scope": ["packages/core/src/auth/**"],
|
||||
"scope": [
|
||||
"sdk/packages/core/src/auth/**"
|
||||
],
|
||||
"severity": "high"
|
||||
},
|
||||
{
|
||||
"id": "sdk-telemetry-doc-update",
|
||||
"rule": "Any PR that adds new event constants to CORE_TELEMETRY_EVENTS in packages/core/src/services/telemetry/core-events.ts, adds new capture* helper functions, or changes the payload shape of an existing event must update the Event Catalog section in DOC.md. Flag PRs that modify core-events.ts without a corresponding change to DOC.md.",
|
||||
"scope": ["packages/core/src/services/telemetry/core-events.ts"],
|
||||
"scope": [
|
||||
"sdk/packages/core/src/services/telemetry/core-events.ts"
|
||||
],
|
||||
"severity": "medium"
|
||||
}
|
||||
]
|
||||
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"files": [
|
||||
{
|
||||
"path": "packages/core/src/services/telemetry/core-events.ts",
|
||||
"path": "sdk/packages/core/src/services/telemetry/core-events.ts",
|
||||
"description": "Single source of truth for all telemetry event names (CORE_TELEMETRY_EVENTS) and their typed capture helper functions. Every PR touching telemetry must be evaluated against this catalog. New events must be defined here first."
|
||||
},
|
||||
{
|
||||
"path": "packages/shared/src/services/telemetry.ts",
|
||||
"path": "sdk/packages/shared/src/services/telemetry.ts",
|
||||
"description": "ITelemetryService interface definition. Defines the contract all telemetry implementations must satisfy (capture, captureRequired, recordCounter, recordHistogram, recordGauge, flush, dispose)."
|
||||
},
|
||||
{
|
||||
"path": "packages/core/src/services/telemetry/TelemetryService.ts",
|
||||
"path": "sdk/packages/core/src/services/telemetry/TelemetryService.ts",
|
||||
"description": "Reference implementation of ITelemetryService used by all hosts. Multi-adapter fan-out service that forwards events to OpenTelemetry."
|
||||
},
|
||||
{
|
||||
"path": "packages/core/src/services/telemetry/OpenTelemetryProvider.ts",
|
||||
"path": "sdk/packages/core/src/services/telemetry/OpenTelemetryProvider.ts",
|
||||
"description": "OpenTelemetry-backed provider that wires logs/metrics/traces exporters. Contains createConfiguredTelemetryService and createConfiguredTelemetryHandle, the canonical factories every host should use."
|
||||
},
|
||||
{
|
||||
@@ -21,11 +21,11 @@
|
||||
"description": "Public API and event documentation. The Event Catalog and 'Activation funnel' sections must be kept in sync with core-events.ts. Host integration rules (CLI dir ordering, hub daemon metadata forwarding) are documented here."
|
||||
},
|
||||
{
|
||||
"path": "ARCHITECTURE.md",
|
||||
"path": "sdk/ARCHITECTURE.md",
|
||||
"description": "Architecture reference. Telemetry design decisions, completion semantics (submit_and_exit anchoring), and hub-daemon telemetry forwarding are documented here. Use as ground truth for design intent."
|
||||
},
|
||||
{
|
||||
"path": "AGENTS.md",
|
||||
"path": "sdk/AGENTS.md",
|
||||
"description": "Package boundary rules. Telemetry runtime services live in @cline/core; @cline/agents must not own stateful telemetry. Use to evaluate whether a telemetry change is being made in the correct package."
|
||||
}
|
||||
]
|
||||
@@ -10,9 +10,9 @@ The SDK uses OpenTelemetry (OTEL) as its sole telemetry transport. Events flow t
|
||||
```
|
||||
core-events.ts (event catalog + typed helpers)
|
||||
↓
|
||||
ITelemetryService (packages/shared) ← interface contract
|
||||
ITelemetryService (sdk/packages/shared) ← interface contract
|
||||
↓
|
||||
TelemetryService (packages/core) ← multi-adapter fan-out
|
||||
TelemetryService (sdk/packages/core) ← multi-adapter fan-out
|
||||
↓
|
||||
OpenTelemetryAdapter → OpenTelemetryProvider ← OTLP transport
|
||||
↓
|
||||
@@ -24,7 +24,7 @@ parallel-but-independent stacks; this `.greptile/` config covers only the SDK.
|
||||
|
||||
## The Single Source of Truth
|
||||
|
||||
`packages/core/src/services/telemetry/core-events.ts` is the single source of truth for all
|
||||
`sdk/packages/core/src/services/telemetry/core-events.ts` is the single source of truth for all
|
||||
event names. It exports:
|
||||
|
||||
- `CORE_TELEMETRY_EVENTS` — a frozen const object grouped by family
|
||||
@@ -60,8 +60,8 @@ Emission ownership:
|
||||
emitter in `prepareLocalRuntimeBootstrap`. Hosts must NOT re-emit these.
|
||||
- `workspace.path_resolved`: emitted from default tool executors **only when**
|
||||
`WorkspaceManager` exposes more than one root.
|
||||
- `task.*`: emitted by core session lifecycle code in `packages/core/src/cline-core/` and
|
||||
`packages/core/src/runtime/`. Hosts must not duplicate this emission.
|
||||
- `task.*`: emitted by core session lifecycle code in `sdk/packages/core/src/cline-core/` and
|
||||
`sdk/packages/core/src/runtime/`. Hosts must not duplicate this emission.
|
||||
|
||||
## `task.completed` Semantics
|
||||
|
||||
@@ -105,7 +105,7 @@ forwarding, hub-backed sessions silently drop their lifecycle telemetry.
|
||||
|
||||
## Auth Lifecycle Completeness
|
||||
|
||||
Every authentication provider in `packages/core/src/auth/` must emit all four auth lifecycle
|
||||
Every authentication provider in `sdk/packages/core/src/auth/` must emit all four auth lifecycle
|
||||
events using the typed helpers:
|
||||
|
||||
| Phase | Helper | Where it fires |
|
||||
@@ -115,7 +115,7 @@ events using the typed helpers:
|
||||
| Token error | `captureAuthFailed(provider, errorMessage)` | In the catch block |
|
||||
| Token invalidation | `captureAuthLoggedOut(provider, reason)` | On invalid_grant or explicit logout |
|
||||
|
||||
Cross-reference `packages/core/src/auth/cline.ts` and `packages/core/src/auth/codex.ts` as
|
||||
Cross-reference `sdk/packages/core/src/auth/cline.ts` and `sdk/packages/core/src/auth/codex.ts` as
|
||||
canonical examples of all four phases.
|
||||
|
||||
## Single Telemetry Service Per Host
|
||||
@@ -1 +1,10 @@
|
||||
if ! command -v gitleaks >/dev/null 2>&1; then
|
||||
echo "gitleaks is required for the pre-commit secret scan."
|
||||
echo "Install it with: brew install gitleaks"
|
||||
echo "Other install options: https://github.com/gitleaks/gitleaks#installing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
gitleaks git --pre-commit --redact --staged --verbose || exit 1
|
||||
|
||||
lint-staged
|
||||
Vendored
+31
-31
@@ -10,23 +10,23 @@
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||
"--extensionDevelopmentPath=${workspaceFolder}/apps/vscode",
|
||||
"--disable-workspace-trust",
|
||||
"--disable-extension",
|
||||
"saoudrizwan.claude-dev", // Avoid conflicts with installed Cline
|
||||
"--disable-extension",
|
||||
"saoudrizwan.cline-nightly", // Avoid conflicts with installed Cline Nightly
|
||||
"${workspaceFolder}",
|
||||
"${workspaceFolder}/apps/vscode",
|
||||
"--disable-extensions"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/dist/**/*.js"
|
||||
"${workspaceFolder}/apps/vscode/dist/**/*.js"
|
||||
],
|
||||
"preLaunchTask": "${defaultBuildTask}",
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"envFile": "${workspaceFolder}/apps/vscode/.env",
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}/apps/vscode",
|
||||
"CLINE_ENVIRONMENT": "production"
|
||||
}
|
||||
},
|
||||
@@ -35,22 +35,22 @@
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||
"--extensionDevelopmentPath=${workspaceFolder}/apps/vscode",
|
||||
"--disable-workspace-trust",
|
||||
"--disable-extension",
|
||||
"saoudrizwan.claude-dev", // Avoid conflicts with installed Cline
|
||||
"--disable-extension",
|
||||
"saoudrizwan.cline-nightly", // Avoid conflicts with installed Cline Nightly
|
||||
"${workspaceFolder}"
|
||||
"${workspaceFolder}/apps/vscode"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/dist/**/*.js"
|
||||
"${workspaceFolder}/apps/vscode/dist/**/*.js"
|
||||
],
|
||||
"preLaunchTask": "${defaultBuildTask}",
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"envFile": "${workspaceFolder}/apps/vscode/.env",
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}/apps/vscode",
|
||||
"CLINE_ENVIRONMENT": "staging"
|
||||
}
|
||||
},
|
||||
@@ -59,22 +59,22 @@
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||
"--extensionDevelopmentPath=${workspaceFolder}/apps/vscode",
|
||||
"--disable-workspace-trust",
|
||||
"--disable-extension",
|
||||
"saoudrizwan.claude-dev", // Avoid conflicts with installed Cline
|
||||
"--disable-extension",
|
||||
"saoudrizwan.cline-nightly", // Avoid conflicts with installed Cline Nightly
|
||||
"${workspaceFolder}"
|
||||
"${workspaceFolder}/apps/vscode"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/dist/**/*.js"
|
||||
"${workspaceFolder}/apps/vscode/dist/**/*.js"
|
||||
],
|
||||
"preLaunchTask": "${defaultBuildTask}",
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"envFile": "${workspaceFolder}/apps/vscode/.env",
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}/apps/vscode",
|
||||
"CLINE_ENVIRONMENT": "local"
|
||||
}
|
||||
},
|
||||
@@ -84,27 +84,27 @@
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "${execPath}",
|
||||
"args": [
|
||||
"--user-data-dir=${workspaceFolder}/dist/tmp/user",
|
||||
"--user-data-dir=${workspaceFolder}/apps/vscode/dist/tmp/user",
|
||||
"--profile-temp",
|
||||
"--sync=off",
|
||||
"--disable-extension",
|
||||
"saoudrizwan.claude-dev", // Avoid conflicts with installed Cline
|
||||
"--disable-extension",
|
||||
"saoudrizwan.cline-nightly", // Avoid conflicts with installed Cline Nightly
|
||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||
"${workspaceFolder}"
|
||||
"--extensionDevelopmentPath=${workspaceFolder}/apps/vscode",
|
||||
"${workspaceFolder}/apps/vscode"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/dist/**/*.js"
|
||||
"${workspaceFolder}/apps/vscode/dist/**/*.js"
|
||||
],
|
||||
"preLaunchTask": "clean-tmp-user",
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"postDebugTask": "stop",
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"envFile": "${workspaceFolder}/apps/vscode/.env",
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"TEMP_PROFILE": "true",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}/apps/vscode",
|
||||
"CLINE_ENVIRONMENT": "production"
|
||||
}
|
||||
},
|
||||
@@ -117,13 +117,13 @@
|
||||
],
|
||||
"sourceMaps": true,
|
||||
"resolveSourceMapLocations": [
|
||||
"${workspaceFolder}/**",
|
||||
"${workspaceFolder}/apps/vscode/**",
|
||||
"!**/node_modules/**"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"cwd": "${workspaceFolder}/apps/vscode",
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/dist/**/*.js",
|
||||
"${workspaceFolder}/dist-standalone/**/*.js"
|
||||
"${workspaceFolder}/apps/vscode/dist/**/*.js",
|
||||
"${workspaceFolder}/apps/vscode/dist-standalone/**/*.js"
|
||||
],
|
||||
"preLaunchTask": "compile-standalone",
|
||||
"runtimeExecutable": "npx",
|
||||
@@ -131,11 +131,11 @@
|
||||
"tsx"
|
||||
],
|
||||
"program": "scripts/test-standalone-core-api-server.ts",
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"envFile": "${workspaceFolder}/apps/vscode/.env",
|
||||
"env": {
|
||||
"PROTOBUS_PORT": "26040",
|
||||
"HOSTBRIDGE_PORT": "26041",
|
||||
"WORKSPACE_DIR": "${workspaceFolder}",
|
||||
"WORKSPACE_DIR": "${workspaceFolder}/apps/vscode",
|
||||
"E2E_TEST": "true",
|
||||
"CLINE_ENVIRONMENT": "local"
|
||||
},
|
||||
@@ -151,10 +151,10 @@
|
||||
],
|
||||
"sourceMaps": true,
|
||||
"resolveSourceMapLocations": [
|
||||
"${workspaceFolder}/**",
|
||||
"${workspaceFolder}/apps/vscode/**",
|
||||
"!**/node_modules/**"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"cwd": "${workspaceFolder}/apps/vscode",
|
||||
"runtimeExecutable": "npx",
|
||||
"runtimeArgs": [
|
||||
"mocha"
|
||||
@@ -169,7 +169,7 @@
|
||||
"--exit",
|
||||
"${file}"
|
||||
],
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"envFile": "${workspaceFolder}/apps/vscode/.env",
|
||||
"env": {
|
||||
"TS_NODE_PROJECT": "./tsconfig.unit-test.json",
|
||||
"NODE_ENV": "test",
|
||||
@@ -188,7 +188,7 @@
|
||||
"run",
|
||||
"storybook"
|
||||
],
|
||||
"cwd": "${workspaceFolder}/webview-ui",
|
||||
"cwd": "${workspaceFolder}/apps/vscode/webview-ui",
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"serverReadyAction": {
|
||||
|
||||
Vendored
+1
-1
@@ -17,7 +17,7 @@
|
||||
// Protobuf settings
|
||||
"protoc": {
|
||||
"options": [
|
||||
"--proto_path=proto"
|
||||
"--proto_path=apps/vscode/proto"
|
||||
]
|
||||
},
|
||||
// Enable Lint and format using Biome
|
||||
|
||||
Vendored
+40
-23
@@ -5,24 +5,28 @@
|
||||
"tasks": [
|
||||
{
|
||||
"label": "compile-standalone",
|
||||
"type": "npm",
|
||||
"script": "compile-standalone",
|
||||
"type": "shell",
|
||||
"command": "npm run compile-standalone",
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"reveal": "always"
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/apps/vscode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "npm: protos",
|
||||
"type": "npm",
|
||||
"script": "protos",
|
||||
"type": "shell",
|
||||
"command": "npm run protos",
|
||||
"problemMatcher": [],
|
||||
"isBackground": false,
|
||||
"presentation": {
|
||||
"reveal": "always"
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/apps/vscode",
|
||||
"env": {
|
||||
"IS_DEV": "true"
|
||||
}
|
||||
@@ -60,8 +64,8 @@
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "build:webview",
|
||||
"type": "shell",
|
||||
"command": "npm run build:webview",
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"isBackground": true,
|
||||
@@ -74,14 +78,15 @@
|
||||
"reveal": "always"
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/apps/vscode",
|
||||
"env": {
|
||||
"IS_DEV": "true"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "build:webview:test",
|
||||
"type": "shell",
|
||||
"command": "npm run build:webview:test",
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"isBackground": true,
|
||||
@@ -94,6 +99,7 @@
|
||||
"reveal": "always"
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/apps/vscode",
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"IS_TEST": "true"
|
||||
@@ -101,8 +107,8 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "dev:webview",
|
||||
"type": "shell",
|
||||
"command": "npm run dev:webview",
|
||||
"group": "build",
|
||||
"problemMatcher": [
|
||||
{
|
||||
@@ -131,14 +137,15 @@
|
||||
"reveal": "always"
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/apps/vscode",
|
||||
"env": {
|
||||
"IS_DEV": "true"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch:esbuild",
|
||||
"type": "shell",
|
||||
"command": "npm run watch:esbuild",
|
||||
"group": "build",
|
||||
"problemMatcher": {
|
||||
"pattern": [
|
||||
@@ -169,14 +176,15 @@
|
||||
"reveal": "always"
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/apps/vscode",
|
||||
"env": {
|
||||
"IS_DEV": "true"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch:esbuild:test",
|
||||
"type": "shell",
|
||||
"command": "npm run watch:esbuild:test",
|
||||
"group": "build",
|
||||
"problemMatcher": {
|
||||
"pattern": [
|
||||
@@ -207,6 +215,7 @@
|
||||
"reveal": "always"
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/apps/vscode",
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"IS_TEST": "true"
|
||||
@@ -214,8 +223,8 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch:tsc",
|
||||
"type": "shell",
|
||||
"command": "npm run watch:tsc",
|
||||
"group": "build",
|
||||
"problemMatcher": "$tsc-watch",
|
||||
"isBackground": true,
|
||||
@@ -226,11 +235,15 @@
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
"reveal": "always"
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/apps/vscode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch-tests",
|
||||
"type": "shell",
|
||||
"command": "npm run watch-tests",
|
||||
"label": "npm: watch-tests",
|
||||
"problemMatcher": "$tsc-watch",
|
||||
"isBackground": true,
|
||||
"dependsOn": [
|
||||
@@ -240,7 +253,10 @@
|
||||
"reveal": "always",
|
||||
"group": "watchers"
|
||||
},
|
||||
"group": "build"
|
||||
"group": "build",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/apps/vscode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "tasks: watch-tests",
|
||||
@@ -262,11 +278,11 @@
|
||||
"dependsOn": [
|
||||
"watch"
|
||||
],
|
||||
"command": "rm -rf ${workspaceFolder}/dist/tmp/user && mkdir -p ${workspaceFolder}/dist/tmp/user"
|
||||
"command": "rm -rf ${workspaceFolder}/apps/vscode/dist/tmp/user && mkdir -p ${workspaceFolder}/apps/vscode/dist/tmp/user"
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "storybook",
|
||||
"type": "shell",
|
||||
"command": "npm run storybook",
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"isBackground": false,
|
||||
@@ -279,6 +295,7 @@
|
||||
"reveal": "always"
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/apps/vscode",
|
||||
"env": {
|
||||
"IS_DEV": "true"
|
||||
}
|
||||
@@ -292,7 +309,7 @@
|
||||
"$tsc"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/sdk"
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
+109
@@ -1,5 +1,114 @@
|
||||
# Changelog
|
||||
|
||||
## [3.89.2]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Complete the fix for the Anthropic provider on VS Code 1.123 and later by upgrading the bundled Anthropic SDK to a release compatible with the Node 24 runtime.
|
||||
- Update the Vertex AI provider to a compatible Anthropic Vertex SDK release so it works with the upgraded Anthropic SDK.
|
||||
|
||||
## [3.89.1]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Restore the Anthropic provider on VS Code 1.123 and later, where the updated Node 24 runtime broke the bundled Anthropic SDK.
|
||||
- Handle the DeepSeek V4 reasoning format.
|
||||
|
||||
## [3.89.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Add Claude Fable 5 model support.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix MiniMax M3 thinking controls across gateways.
|
||||
|
||||
### Changed
|
||||
|
||||
- Clean up the Codex model list.
|
||||
|
||||
## [3.88.1]
|
||||
|
||||
### Added
|
||||
|
||||
- Add a debug section in settings for Cline testers.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Include the walkthrough markdown files in the VS Code extension package so the first-run walkthrough steps load correctly.
|
||||
|
||||
## [3.88.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Add the latest Fireworks AI serverless models and update the default Fireworks model to Kimi K2.6.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix MCP server delete/add flows so settings writes do not cause the MCP server list to be emptied by the file watcher.
|
||||
- Remove stale Fireworks AI models and correct Fireworks model metadata and cache pricing.
|
||||
|
||||
### Changed
|
||||
|
||||
- Always use the upstream Cline recommended models endpoint instead of gating it behind a feature flag.
|
||||
|
||||
## [3.87.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Add MiniMax M3 model support.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Update VS Code extension dependencies to resolve security issues in `@xmldom/xmldom`, `basic-ftp`, `axios`, `undici`, and other direct/transitive packages.
|
||||
|
||||
## [3.86.2]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix `@` file mentions and workspace file search on VS Code 1.122+ by resolving the new bundled `@vscode/ripgrep-universal` per-platform binary layout before falling back to legacy ripgrep paths.
|
||||
|
||||
## [3.86.1]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix `@` file mentions failing to find files in some environments (notably VS Code Remote SSH, and after certain VS Code updates) by keeping the file-search fallback alive when the workspace index or bundled ripgrep binary is unavailable.
|
||||
|
||||
## [3.86.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Add Claude Opus 4.8 provider support, including 1M-context variants where available.
|
||||
- Add Moonshot Kimi K2.6 model support.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Show prompt-cache support for Qwen 3.7 Max in the Cline provider.
|
||||
- Fix the VS Code nightly publish workflow startup permissions.
|
||||
|
||||
### Changed
|
||||
|
||||
- Move the VS Code extension project into `apps/vscode`.
|
||||
|
||||
## [3.85.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Add GPT-5.5 support to SAP AI Core.
|
||||
- Add DeepSeek V4 Flash and Pro models.
|
||||
- Add Gemini 3.5 Flash to Gemini and Vertex providers.
|
||||
- Add `/lg-task` URI webhook integration for LG dashboard flows.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix Vertex AI global endpoint handling for Claude models.
|
||||
- Route Poolside Laguna models through next-gen prompts and native tool calling.
|
||||
|
||||
### Changed
|
||||
|
||||
- Update `diff` and `protobufjs` dependencies.
|
||||
|
||||
## [3.84.0]
|
||||
|
||||
### Added
|
||||
|
||||
+3
-2
@@ -45,7 +45,7 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
|
||||
3. Install [bun](https://bun.com)
|
||||
4. Install the necessary dependencies for the extension and webview-gui:
|
||||
```bash
|
||||
npm run install:all
|
||||
cd apps/vscode && npm run install:all && cd ../..
|
||||
cd sdk && bun run build && cd ..
|
||||
```
|
||||
5. Generate Protocol Buffer files (required before first build):
|
||||
@@ -61,7 +61,7 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
|
||||
2. Push your branch and create a PR on GitHub. Our CI will:
|
||||
- Run tests and checks
|
||||
3. Testing
|
||||
- Run `npm run test` to run tests locally.
|
||||
- Run `cd apps/vscode && npm run test` to run tests locally.
|
||||
- Before submitting PR, run `npm run format:fix` to format your code
|
||||
|
||||
### Extension
|
||||
@@ -73,6 +73,7 @@ We also welcome contributions to our [documentation](https://github.com/cline/cl
|
||||
- If you dismissed the prompts, you can install them manually from the Extensions panel
|
||||
|
||||
2. **Local Development**
|
||||
- cd into the vscode extension, `cd apps/vscode`
|
||||
- Run `npm run install:all` to install dependencies
|
||||
- Run `npm run protos` to generate Protocol Buffer files (required before first build)
|
||||
- Run `npm run test` to run tests locally
|
||||
|
||||
@@ -51,7 +51,7 @@ for CI/CD and scripting.
|
||||
npm i -g cline
|
||||
```
|
||||
|
||||
<a href="./sdk/apps/cli/README.md">Learn more</a>
|
||||
<a href="./apps/cli/README.md">Learn more</a>
|
||||
<br><br>
|
||||
|
||||
</td>
|
||||
@@ -129,7 +129,7 @@ npm install @cline/sdk
|
||||
| Product | Description | Location | CHANGELOG |
|
||||
|---------|------------|--------------|--------------|
|
||||
| **SDK** | Node.js programmatic agent API and extension exports. | [`sdk/`](https://github.com/cline/cline/tree/main/sdk) | [CHANGELOG.md](https://github.com/cline/cline/blob/main/sdk/CHANGELOG.md) |
|
||||
| **CLI** | Terminal UI, headless mode, shell commands, and CLI-specific flows. | [`sdk/apps/cli/`](https://github.com/cline/cline/tree/main/sdk/apps/cli) | [CHANGELOG.md](https://github.com/cline/cline/blob/main/sdk/apps/cli/CHANGELOG.md) |
|
||||
| **CLI** | Terminal UI, headless mode, shell commands, and CLI-specific flows. | [`apps/cli/`](https://github.com/cline/cline/tree/main/apps/cli) | [CHANGELOG.md](https://github.com/cline/cline/blob/main/apps/cli/CHANGELOG.md) |
|
||||
| **VS Code Extension** | The Marketplace extension and extension host integration. | [`/`](https://github.com/cline/cline/tree/main) (WIP migrating) | [CHANGELOG.md](https://github.com/cline/cline/blob/main/CHANGELOG.md) |
|
||||
| **JetBrains Plugin** | JetBrains-hosted client that talks to the shared agent core. | Currently we are not open-sourcing JetBrains plugins | - |
|
||||
| **Kanban** | Web-based multi-agent task board. | [`cline/kanban`](https://github.com/cline/kanban) | [CHANGELOG.md](https://github.com/cline/kanban/blob/main/CHANGELOG.md) |
|
||||
@@ -212,8 +212,12 @@ cline schedule create "PR summary" \
|
||||
Chat with your agent from any messaging platform: Telegram, Slack, Discord, Google Chat, WhatsApp, and Linear. Each conversation thread maps to an agent session with full context. Set up access control to restrict who can interact with your agent.
|
||||
|
||||
```bash
|
||||
# Connect to Telegram
|
||||
cline connect telegram -k $BOT_TOKEN
|
||||
cline connect slack --token $SLACK_TOKEN --signing-secret $SECRET --base-url $URL
|
||||
# Connect to Slack through webhook
|
||||
cline connect slack --bot-token $SLACK_TOKEN --signing-secret $SECRET --base-url $URL
|
||||
# Connect to Slack using socket mode
|
||||
cline connect slack --bot-token $SLACK_TOKEN --app-token $SLACK_APP_TOKEN
|
||||
```
|
||||
|
||||
## Headless CLI for CI/CD
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.4.5/schema.json",
|
||||
"root": false,
|
||||
"extends": [
|
||||
"../sdk/biome.json"
|
||||
],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"a11y": {
|
||||
"noStaticElementInteractions": "warn"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,103 @@
|
||||
# Cline CLI Changelog
|
||||
|
||||
## 3.0.24
|
||||
|
||||
- Plugin commands can now submit prompts to the agent
|
||||
- Added support for overriding the API base URL
|
||||
- Open the verification URL automatically when starting device authentication
|
||||
- Enforced a single shared Cline Hub, so a stale hub is respawned after an upgrade
|
||||
- Suppressed flickering console windows on Windows
|
||||
- Fixed truncation of structured tool operation result strings so oversized tool output stays within limits
|
||||
- Stopped echoing the full command text in run_commands tool results
|
||||
|
||||
## 3.0.23
|
||||
|
||||
- Fixed Vertex AI GCP settings configuration
|
||||
- Fixed the Azure Foundry API version
|
||||
- Added support for configured agents as subagent tools
|
||||
- Centralized OAuth management into the SDK
|
||||
- Fixed an error caused by disabled reasoning on Fable 5
|
||||
|
||||
## 3.0.22
|
||||
|
||||
- Added support for the Claude Fable 5 model
|
||||
- Fixed MiniMax M3 thinking controls so they route correctly across gateways
|
||||
|
||||
## 3.0.21
|
||||
|
||||
- Added a global auto-update setting that controls automatic updates on CLI startup
|
||||
- Added a Cline credits refill link
|
||||
- Fixed scrolling for inline ask-question responses
|
||||
- Fixed connector thread session routing and stale hub session handling
|
||||
- Added support for Vertex AI Application Default Credentials (ADC) with tool use
|
||||
- Fixed empty message content replay for Bedrock
|
||||
- Cleaned up the OpenAI Codex model list
|
||||
|
||||
## 3.0.20
|
||||
|
||||
- Installed plugin wrappers are now named from their source (npm package name, git repo, remote filename, official slug, or local directory) instead of an opaque hash, making installed plugins easier to identify.
|
||||
|
||||
## 3.0.19
|
||||
|
||||
- Fixed CLI auto-update to use `npm update` so updates apply reliably, while preserving the installed release channel (e.g. nightly).
|
||||
|
||||
## 3.0.18
|
||||
|
||||
- Fix Slack channel mentions so replies post in the original message's thread.
|
||||
- Fix the abort indicator to clear immediately when a task is cancelled.
|
||||
- Sync the Fireworks AI model registry and refresh the bundled model catalog with current platform offerings.
|
||||
- Bump the bundled SDK to v0.0.43, which forces a running Cline Hub to restart so it picks up the latest SDK code.
|
||||
|
||||
## 3.0.17
|
||||
|
||||
- Fix a regression introduced in 3.0.15 where the interactive CLI could get stuck after stopping and restarting Cline Hub and then pressing Escape to cancel a request. The CLI now detects stale or missing sessions, recovers any pending messages, and starts a fresh session instead of failing with "session not found".
|
||||
- Fix Ctrl+C and Hub shutdown races that surfaced as "hook dispatch failed" and WebSocket connection errors from late hook events racing against Hub shutdown.
|
||||
- Fix the Hub daemon being shut down prematurely when a runtime request was aborted, so the daemon now stays alive.
|
||||
- Improve the Telegram connector with a new `--allowed-user-id` flag to restrict which Telegram users are authorized to interact with the agent.
|
||||
|
||||
## 3.0.16
|
||||
|
||||
- Install official Cline plugins by slug off the new github.com/cline/plugins collection.
|
||||
- Uninstall plugins using `cline plugin uninstall <plugin>` or in the TUI.
|
||||
- Plugins can now bundle skills, and plugin skills are grouped together in settings.
|
||||
- Add Slack socket mode support.
|
||||
- Allow a custom base URL for Anthropic vendor-type providers.
|
||||
- Fix OAuth token migration for users signed in through the old extension.
|
||||
- Use a union schema for read-files tool input validation.
|
||||
- Add a `CLINE_PLUGIN_IMPORT_TIMEOUT_MS` env override to control the plugin import timeout.
|
||||
|
||||
## 3.0.15
|
||||
|
||||
- Add Cline Hub, a web app for monitoring connected clients, viewing and driving sessions, streaming assistant output, and restarting the local hub, with local, LAN, and tunnel usage gated by a room secret.
|
||||
- Support global AGENTS rules so agent rules can be applied across all sessions, not just per-project.
|
||||
- Let plugins contribute static or dynamic rule content when installed in the sandbox.
|
||||
- Bind Discord sessions to individual message authors so different Discord users no longer share chat state in a thread.
|
||||
- Support participant mute targets in Discord: resolve `/mute` and `/unmute` from user mentions or raw user IDs to mute a specific participant in a thread.
|
||||
- Make OAuth URLs clickable in the TUI.
|
||||
- Refresh the bundled model catalog, adding Claude Opus 4.8, Moonshot Kimi K2.6, and Qwen3.7 Max (with cache support).
|
||||
- Discover SDK skill directories that are symlinked, including handling circular symlinks.
|
||||
- Steer active connector sessions across turn keys by matching on session ID, so replies continue the existing session instead of starting a duplicate.
|
||||
- Stop the Discord connector after repeated identical errors (per thread, within a time window) to prevent error messages from flooding a channel.
|
||||
- Fix Discord connector registration and reply fallback handling.
|
||||
- Fix SAP AI Core to use the AI SDK community provider.
|
||||
- Log ACP output as diagnostics instead of errors so normal output no longer appears as errors.
|
||||
|
||||
## 3.0.14
|
||||
|
||||
- Fix OTEL telemetry variable bundling so telemetry is correctly enabled in compiled CLI builds: guard against environments where `process.env` is undefined and remove optional chaining so bundlers can inline the values at build time.
|
||||
|
||||
## 3.0.13
|
||||
|
||||
- Show a loading dialog while resuming a session from history so the TUI no longer appears frozen during the load.
|
||||
- Speed up the `/clear` command by deferring new session creation until you send the next prompt, so clearing no longer blocks on spinning up an empty session.
|
||||
|
||||
## 3.0.12
|
||||
|
||||
- Show a loading dialog while the config screen switches provider or model so the transition no longer looks frozen.
|
||||
- Render the ask question tool prompt inline with the conversation so the question and suggested answers stay attached to the assistant turn that asked them, instead of appearing in a separate modal.
|
||||
- Allow manual `cline update` runs to install the latest published version immediately, bypassing the release age gate that delays automatic updates.
|
||||
- Refresh the bundled SDK to 0.0.42, updating the model catalog.
|
||||
|
||||
## 3.0.11
|
||||
|
||||
- Fix a regression in the ChatGPT OAuth provider where requests failed with `max_output_tokens not supported`, by restoring the full output token budget instead of applying an implicit cap.
|
||||
@@ -416,7 +416,7 @@ Then attach VS Code or Chrome DevTools to `ws://127.0.0.1:6499`.
|
||||
|
||||
## Publishing
|
||||
|
||||
The CLI is published as the `cline` wrapper package on npm with platform-specific binaries under `@cline/cli-*`. The release flow lives in the `publish-cli` skill (`sdk/apps/cli/.cline/skills/publish-cli/SKILL.md`).
|
||||
The CLI is published as the `cline` wrapper package on npm with platform-specific binaries under `@cline/cli-*`. The release flow lives in the `publish-cli` skill (`.cline/skills/publish-cli/SKILL.md` at the repo root).
|
||||
|
||||
From the `apps/cli` workspace:
|
||||
|
||||
@@ -174,6 +174,9 @@ cline connect telegram -k 123456:ABCDEF...
|
||||
# Slack (webhook mode)
|
||||
cline connect slack --bot-token $SLACK_BOT_TOKEN --signing-secret $SLACK_SIGNING_SECRET --base-url https://your-domain.com
|
||||
|
||||
# Slack (socket mode)
|
||||
cline connect slack --bot-token $SLACK_BOT_TOKEN --app-token $SLACK_APP_TOKEN
|
||||
|
||||
# Google Chat (webhook mode)
|
||||
cline connect gchat --base-url https://your-domain.com
|
||||
|
||||
@@ -1,12 +1,65 @@
|
||||
import { copyFileSync, mkdirSync } from "node:fs";
|
||||
import {
|
||||
copyFileSync,
|
||||
cpSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readdirSync,
|
||||
statSync,
|
||||
} from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { $ } from "bun";
|
||||
|
||||
function defineProcessEnv(name: string): string {
|
||||
return JSON.stringify(process.env[name] ?? "");
|
||||
}
|
||||
|
||||
const sourcemap = Bun.env.CLINE_SOURCEMAPS === "1" ? "linked" : "none";
|
||||
const rootDir = dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = join(rootDir, "../../");
|
||||
const hubWebviewSourcePath = join(repoRoot, "apps/cline-hub/src/webview");
|
||||
const hubWebviewDistPath = join(repoRoot, "apps/cline-hub/dist/webview");
|
||||
const hubWebviewIndexPath = join(hubWebviewDistPath, "index.html");
|
||||
const cliHubWebviewDistPath = join(rootDir, "dist/cline-hub/webview");
|
||||
|
||||
function newestFileMtimeMs(dir: string): number {
|
||||
let newest = 0;
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
if (
|
||||
entry.name === "node_modules" ||
|
||||
entry.name === "dist" ||
|
||||
entry.name === ".turbo"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const path = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
newest = Math.max(newest, newestFileMtimeMs(path));
|
||||
} else if (entry.isFile()) {
|
||||
newest = Math.max(newest, statSync(path).mtimeMs);
|
||||
}
|
||||
}
|
||||
return newest;
|
||||
}
|
||||
|
||||
function shouldBuildHubWebview(): boolean {
|
||||
if (!existsSync(hubWebviewIndexPath)) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
return (
|
||||
newestFileMtimeMs(hubWebviewSourcePath) >
|
||||
statSync(hubWebviewIndexPath).mtimeMs
|
||||
);
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldBuildHubWebview()) {
|
||||
console.log("Building Cline Hub webview...");
|
||||
await $`bun -F @cline/cline-hub build:webview`.cwd(repoRoot);
|
||||
}
|
||||
|
||||
const result = await Bun.build({
|
||||
entrypoints: ["./src/index.ts"],
|
||||
@@ -32,6 +85,20 @@ const result = await Bun.build({
|
||||
],
|
||||
define: {
|
||||
"process.env.NODE_ENV": '"production"',
|
||||
...(process.env.TELEMETRY_SERVICE_API_KEY
|
||||
? {
|
||||
"process.env.TELEMETRY_SERVICE_API_KEY": defineProcessEnv(
|
||||
"TELEMETRY_SERVICE_API_KEY",
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
...(process.env.ERROR_SERVICE_API_KEY
|
||||
? {
|
||||
"process.env.ERROR_SERVICE_API_KEY": defineProcessEnv(
|
||||
"ERROR_SERVICE_API_KEY",
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
"process.env.OTEL_TELEMETRY_ENABLED": defineProcessEnv(
|
||||
"OTEL_TELEMETRY_ENABLED",
|
||||
),
|
||||
@@ -63,10 +130,9 @@ if (result.logs.length > 0) {
|
||||
}
|
||||
}
|
||||
|
||||
const rootDir = dirname(fileURLToPath(import.meta.url));
|
||||
const coreBootstrapPath = join(
|
||||
rootDir,
|
||||
"../../packages/core/dist/extensions/plugin-sandbox-bootstrap.js",
|
||||
"../../sdk/packages/core/dist/extensions/plugin-sandbox-bootstrap.js",
|
||||
);
|
||||
const cliBootstrapPath = join(
|
||||
rootDir,
|
||||
@@ -74,3 +140,8 @@ const cliBootstrapPath = join(
|
||||
);
|
||||
mkdirSync(dirname(cliBootstrapPath), { recursive: true });
|
||||
copyFileSync(coreBootstrapPath, cliBootstrapPath);
|
||||
|
||||
if (existsSync(hubWebviewDistPath)) {
|
||||
mkdirSync(dirname(cliHubWebviewDistPath), { recursive: true });
|
||||
cpSync(hubWebviewDistPath, cliHubWebviewDistPath, { recursive: true });
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@cline/cli",
|
||||
"displayName": "cline",
|
||||
"version": "3.0.11",
|
||||
"version": "3.0.24",
|
||||
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
@@ -10,7 +10,7 @@
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/cline/cline.git",
|
||||
"directory": "sdk/apps/cli"
|
||||
"directory": "apps/cli"
|
||||
},
|
||||
"keywords": [
|
||||
"cline",
|
||||
@@ -75,6 +75,7 @@
|
||||
"@chat-adapter/telegram": "^4.23.0",
|
||||
"@chat-adapter/whatsapp": "^4.23.0",
|
||||
"@clack/prompts": "^1.2.0",
|
||||
"@cline/cline-hub": "workspace:*",
|
||||
"@gramio/format": "^0.7.0",
|
||||
"@opentui-ui/dialog": "^0.1.2",
|
||||
"@opentui/core": "0.1.102",
|
||||
@@ -86,16 +87,20 @@
|
||||
"open": "^10.2.0",
|
||||
"opentui-spinner": "^0.0.6",
|
||||
"pino": "^10.3.1",
|
||||
"posthog-node": "^5.8.0",
|
||||
"react": "19.2.4",
|
||||
"react-devtools-core": "^7.0.1",
|
||||
"react-reconciler": "0.32.0",
|
||||
"yaml": "^2.8.2",
|
||||
"nanoid": "^5.1.7",
|
||||
"zod": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cline/core": "workspace:*",
|
||||
"@cline/shared": "workspace:*",
|
||||
"@microsoft/tui-test": "^0.0.2",
|
||||
"@types/react": "19.2.14"
|
||||
"@types/react": "19.2.14",
|
||||
"vitest": "^4.0.18",
|
||||
"@types/bun": "^1.3.10"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,14 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
|
||||
import {
|
||||
cpSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
realpathSync,
|
||||
statSync,
|
||||
} from "node:fs";
|
||||
import { join, relative, resolve } from "node:path";
|
||||
import { $ } from "bun";
|
||||
import {
|
||||
@@ -95,6 +103,48 @@ if (!buildOptions.skipSdkBuild) {
|
||||
await $`bun -F @cline/cli build`.cwd(rootDir);
|
||||
}
|
||||
|
||||
const hubWebviewSource = join(cliDir, "../cline-hub/src/webview");
|
||||
const hubWebviewDist = join(cliDir, "../cline-hub/dist/webview");
|
||||
const hubWebviewIndex = join(hubWebviewDist, "index.html");
|
||||
|
||||
function newestFileMtimeMs(dir: string): number {
|
||||
let newest = 0;
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
if (
|
||||
entry.name === "node_modules" ||
|
||||
entry.name === "dist" ||
|
||||
entry.name === ".turbo"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const path = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
newest = Math.max(newest, newestFileMtimeMs(path));
|
||||
} else if (entry.isFile()) {
|
||||
newest = Math.max(newest, statSync(path).mtimeMs);
|
||||
}
|
||||
}
|
||||
return newest;
|
||||
}
|
||||
|
||||
function shouldBuildHubWebview(): boolean {
|
||||
if (!existsSync(hubWebviewIndex)) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
return (
|
||||
newestFileMtimeMs(hubWebviewSource) > statSync(hubWebviewIndex).mtimeMs
|
||||
);
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldBuildHubWebview()) {
|
||||
console.log("Building Cline Hub webview...");
|
||||
await $`bun -F @cline/cline-hub build:webview`.cwd(rootDir);
|
||||
}
|
||||
|
||||
const binaries: Record<string, string> = {};
|
||||
|
||||
function findOpenTuiParserWorker(): string {
|
||||
@@ -209,7 +259,7 @@ for (const item of targets) {
|
||||
// Copy plugin sandbox bootstrap if it exists
|
||||
const bootstrapSrc = join(
|
||||
rootDir,
|
||||
"packages/core/dist/extensions/plugin-sandbox-bootstrap.js",
|
||||
"sdk/packages/core/dist/extensions/plugin-sandbox-bootstrap.js",
|
||||
);
|
||||
if (existsSync(bootstrapSrc)) {
|
||||
const bootstrapDir = join(cliDir, `dist/${dirName}/extensions`);
|
||||
@@ -218,6 +268,14 @@ for (const item of targets) {
|
||||
await Bun.write(join(bootstrapDir, "plugin-sandbox-bootstrap.js"), content);
|
||||
}
|
||||
|
||||
if (existsSync(hubWebviewDist)) {
|
||||
const hubWebviewDest = join(cliDir, `dist/${dirName}/cline-hub/webview`);
|
||||
mkdirSync(join(cliDir, `dist/${dirName}/cline-hub`), {
|
||||
recursive: true,
|
||||
});
|
||||
cpSync(hubWebviewDist, hubWebviewDest, { recursive: true });
|
||||
}
|
||||
|
||||
// Generate platform package.json
|
||||
await Bun.write(
|
||||
join(cliDir, `dist/${dirName}/package.json`),
|
||||
@@ -90,7 +90,7 @@ function buildHostSdkDependencies(): Record<string, string> {
|
||||
for (const pkg of hostSdkPackages) {
|
||||
dependencies[pkg.name] = readPackageVersion(
|
||||
pkg.name,
|
||||
join(cliDir, "../../packages", pkg.directory, "package.json"),
|
||||
join(cliDir, "../../sdk/packages", pkg.directory, "package.json"),
|
||||
);
|
||||
}
|
||||
return dependencies;
|
||||
@@ -0,0 +1,102 @@
|
||||
import type { ProviderSettingsManager } from "@cline/core";
|
||||
import { loginAndSaveProviderOAuthCredentials } from "@cline/core";
|
||||
import { getPersistedProviderApiKey } from "../commands/auth";
|
||||
import { writeDiagnostic } from "../utils/output";
|
||||
|
||||
/**
|
||||
* Supported ACP OAuth provider IDs.
|
||||
*/
|
||||
export const ACP_AUTH_METHODS = [
|
||||
{ id: "cline", name: "Sign in with Cline" },
|
||||
{ id: "openai-codex", name: "Sign in with ChatGPT Subscription" },
|
||||
] as const;
|
||||
|
||||
export type AcpAuthMethodId = (typeof ACP_AUTH_METHODS)[number]["id"];
|
||||
|
||||
export function isAcpAuthMethodId(id: string): id is AcpAuthMethodId {
|
||||
return ACP_AUTH_METHODS.some((m) => m.id === id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform an OAuth login for the given provider in ACP mode.
|
||||
*
|
||||
* Since stdin/stdout are used for the JSON-RPC transport, all user-facing
|
||||
* output is written to stderr and URLs are opened via the `open` package.
|
||||
* If the OAuth flow requires interactive prompts (rare), defaults are used
|
||||
* when available; otherwise an error is thrown.
|
||||
*/
|
||||
async function performOAuthLogin(input: {
|
||||
providerId: AcpAuthMethodId;
|
||||
providerSettingsManager: ProviderSettingsManager;
|
||||
}): Promise<string> {
|
||||
const [{ createOAuthClientCallbacks }, { default: open }] = await Promise.all(
|
||||
[import("@cline/core"), import("open")],
|
||||
);
|
||||
|
||||
const callbacks = createOAuthClientCallbacks({
|
||||
onPrompt: ({ defaultValue }) => {
|
||||
if (defaultValue) {
|
||||
return Promise.resolve(defaultValue);
|
||||
}
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
"OAuth flow requires interactive input which is unavailable in ACP mode",
|
||||
),
|
||||
);
|
||||
},
|
||||
onOutput: (message) => writeDiagnostic(`[acp/auth] ${message}`),
|
||||
openUrl: (url) => open(url, { wait: false }).then(() => undefined),
|
||||
onOpenUrlError: ({ url }) => {
|
||||
writeDiagnostic(
|
||||
`[acp/auth] Could not open browser automatically. Open this URL manually:\n${url}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const settings = await loginAndSaveProviderOAuthCredentials(
|
||||
input.providerSettingsManager,
|
||||
input.providerId,
|
||||
{ callbacks },
|
||||
);
|
||||
const apiKey = getPersistedProviderApiKey(input.providerId, settings);
|
||||
if (!apiKey) {
|
||||
throw new Error(
|
||||
`OAuth login did not persist credentials for ${input.providerId}`,
|
||||
);
|
||||
}
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
export interface AcpAuthResult {
|
||||
providerId: AcpAuthMethodId;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate via OAuth for the given ACP auth method.
|
||||
*
|
||||
* Uses `ProviderSettingsManager` to check for existing credentials first,
|
||||
* falling back to a fresh OAuth login if needed.
|
||||
*/
|
||||
export async function authenticateAcpProvider(
|
||||
methodId: AcpAuthMethodId,
|
||||
providerSettingsManager: ProviderSettingsManager,
|
||||
): Promise<AcpAuthResult> {
|
||||
const existing = providerSettingsManager.getProviderSettings(methodId);
|
||||
|
||||
// Check for already-stored credentials.
|
||||
const existingKey = getPersistedProviderApiKey(methodId, existing);
|
||||
if (existingKey) {
|
||||
writeDiagnostic(`[acp/auth] Using existing credentials for ${methodId}`);
|
||||
return { providerId: methodId, apiKey: existingKey };
|
||||
}
|
||||
|
||||
// Perform a fresh OAuth login.
|
||||
writeDiagnostic(`[acp/auth] Starting OAuth login for ${methodId}…`);
|
||||
const apiKey = await performOAuthLogin({
|
||||
providerId: methodId,
|
||||
providerSettingsManager,
|
||||
});
|
||||
writeDiagnostic(`[acp/auth] Successfully authenticated with ${methodId}`);
|
||||
return { providerId: methodId, apiKey };
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("runAcpMode", () => {
|
||||
afterEach(() => {
|
||||
vi.doUnmock("@agentclientprotocol/sdk");
|
||||
vi.doUnmock("./acpAgent");
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("writes the startup diagnostic without labeling it as an error", async () => {
|
||||
const stderrWrite = vi
|
||||
.spyOn(process.stderr, "write")
|
||||
.mockImplementation(() => true);
|
||||
|
||||
vi.doMock("@agentclientprotocol/sdk", () => ({
|
||||
ndJsonStream: vi.fn(() => ({})),
|
||||
AgentSideConnection: class {
|
||||
closed = Promise.resolve();
|
||||
},
|
||||
}));
|
||||
vi.doMock("./acpAgent", () => ({
|
||||
AcpAgent: class {},
|
||||
}));
|
||||
|
||||
const { runAcpMode } = await import("./index");
|
||||
|
||||
await runAcpMode();
|
||||
|
||||
expect(stderrWrite).toHaveBeenCalledWith(
|
||||
"[acp] starting ACP mode over stdio…\n",
|
||||
);
|
||||
expect(stderrWrite).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining("error:"),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Readable, Writable } from "node:stream";
|
||||
import { writeErr } from "../utils/output";
|
||||
import { writeDiagnostic } from "../utils/output";
|
||||
|
||||
export async function runAcpMode(): Promise<void> {
|
||||
const { AgentSideConnection, ndJsonStream } = await import(
|
||||
@@ -7,7 +7,7 @@ export async function runAcpMode(): Promise<void> {
|
||||
);
|
||||
const { AcpAgent } = await import("./acpAgent");
|
||||
|
||||
writeErr("[acp] starting ACP mode over stdio…");
|
||||
writeDiagnostic("[acp] starting ACP mode over stdio…");
|
||||
|
||||
const stream = ndJsonStream(
|
||||
Writable.toWeb(process.stdout) as WritableStream<Uint8Array>,
|
||||
@@ -2,7 +2,37 @@ import { spawnSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { ProviderSettingsManager } from "@cline/core";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { getPersistedProviderApiKey, saveOAuthProviderSettings } from "./auth";
|
||||
import {
|
||||
getPersistedProviderApiKey,
|
||||
normalizeAuthProviderId,
|
||||
parseAuthCommandArgs,
|
||||
saveOAuthProviderSettings,
|
||||
} from "./auth";
|
||||
|
||||
describe("parseAuthCommandArgs", () => {
|
||||
it("parses Azure API version quick setup option", () => {
|
||||
expect(
|
||||
parseAuthCommandArgs([
|
||||
"--provider",
|
||||
"openai-compatible",
|
||||
"--apikey",
|
||||
"key",
|
||||
"--modelid",
|
||||
"gpt-4.1",
|
||||
"--baseurl",
|
||||
"https://example.openai.azure.com/openai/deployments/gpt-4.1",
|
||||
"--azure-api-version",
|
||||
"2025-01-01-preview",
|
||||
]),
|
||||
).toMatchObject({
|
||||
explicitProvider: "openai-compatible",
|
||||
apikey: "key",
|
||||
modelid: "gpt-4.1",
|
||||
baseurl: "https://example.openai.azure.com/openai/deployments/gpt-4.1",
|
||||
azureApiVersion: "2025-01-01-preview",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("saveOAuthProviderSettings", () => {
|
||||
it("preserves existing manual apiKey while updating OAuth tokens", () => {
|
||||
@@ -67,6 +97,12 @@ describe("getPersistedProviderApiKey", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeAuthProviderId", () => {
|
||||
it("keeps CLI-only codex shorthand in CLI parsing", () => {
|
||||
expect(normalizeAuthProviderId("codex")).toBe("openai-codex");
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadAuthTuiRuntime", () => {
|
||||
it("loads OpenTUI React after provider catalog initialization", async () => {
|
||||
const cliRoot = fileURLToPath(new URL("../..", import.meta.url));
|
||||
@@ -3,11 +3,12 @@ import {
|
||||
BUILT_IN_PROVIDER,
|
||||
createOAuthClientCallbacks,
|
||||
ensureCustomProvidersLoaded,
|
||||
listLocalProviders,
|
||||
getProviderAuthHandler,
|
||||
loginAndSaveProviderOAuthCredentials,
|
||||
type ProviderSettings,
|
||||
type ProviderSettingsManager,
|
||||
saveProviderOAuthCredentials,
|
||||
} from "@cline/core";
|
||||
import { getClineEnvironmentConfig } from "@cline/shared";
|
||||
import { Command } from "commander";
|
||||
import open from "open";
|
||||
import React from "react";
|
||||
@@ -20,6 +21,8 @@ import {
|
||||
type OAuthCredentials,
|
||||
toProviderApiKey,
|
||||
} from "../utils/provider-auth";
|
||||
import { listLocalProviders } from "../utils/provider-catalog";
|
||||
import { identifyTelemetryAccount } from "../utils/telemetry";
|
||||
|
||||
export {
|
||||
getPersistedProviderApiKey,
|
||||
@@ -37,40 +40,6 @@ const c = {
|
||||
green: "\x1b[32m",
|
||||
};
|
||||
|
||||
type CoreOAuthApi = {
|
||||
loginClineOAuth: (input: {
|
||||
apiBaseUrl: string;
|
||||
useWorkOSDeviceAuth?: boolean;
|
||||
callbacks: {
|
||||
onAuth: (info: { url: string; instructions?: string }) => void;
|
||||
onPrompt: (prompt: {
|
||||
message: string;
|
||||
defaultValue?: string;
|
||||
}) => Promise<string>;
|
||||
onManualCodeInput?: () => Promise<string>;
|
||||
};
|
||||
}) => Promise<OAuthCredentials>;
|
||||
loginOcaOAuth: (input: {
|
||||
mode?: "internal" | "external";
|
||||
callbacks: {
|
||||
onAuth: (info: { url: string; instructions?: string }) => void;
|
||||
onPrompt: (prompt: {
|
||||
message: string;
|
||||
defaultValue?: string;
|
||||
}) => Promise<string>;
|
||||
onManualCodeInput?: () => Promise<string>;
|
||||
};
|
||||
}) => Promise<OAuthCredentials>;
|
||||
loginOpenAICodex: (input: {
|
||||
onAuth: (info: { url: string; instructions?: string }) => void;
|
||||
onPrompt: (prompt: {
|
||||
message: string;
|
||||
defaultValue?: string;
|
||||
}) => Promise<string>;
|
||||
onManualCodeInput?: () => Promise<string>;
|
||||
}) => Promise<OAuthCredentials>;
|
||||
};
|
||||
|
||||
type AuthIo = {
|
||||
writeln: (text?: string) => void;
|
||||
writeErr: (text: string) => void;
|
||||
@@ -81,6 +50,7 @@ type AuthQuickSetupInput = {
|
||||
apikey: string;
|
||||
modelid: string;
|
||||
baseurl?: string;
|
||||
azureApiVersion?: string;
|
||||
};
|
||||
|
||||
type AuthCommandInput = {
|
||||
@@ -90,6 +60,7 @@ type AuthCommandInput = {
|
||||
apikey?: string;
|
||||
modelid?: string;
|
||||
baseurl?: string;
|
||||
azureApiVersion?: string;
|
||||
};
|
||||
|
||||
type ParsedAuthCommandArgs = {
|
||||
@@ -97,30 +68,10 @@ type ParsedAuthCommandArgs = {
|
||||
apikey?: string;
|
||||
modelid?: string;
|
||||
baseurl?: string;
|
||||
azureApiVersion?: string;
|
||||
parseError?: string;
|
||||
};
|
||||
|
||||
let cachedCoreOAuthApi: Promise<CoreOAuthApi> | undefined;
|
||||
|
||||
async function getCoreOAuthApi(): Promise<CoreOAuthApi> {
|
||||
if (!cachedCoreOAuthApi) {
|
||||
cachedCoreOAuthApi = import("@cline/core").then((module) => {
|
||||
const runtimeApi = module as Partial<CoreOAuthApi>;
|
||||
if (
|
||||
typeof runtimeApi.loginClineOAuth !== "function" ||
|
||||
typeof runtimeApi.loginOcaOAuth !== "function" ||
|
||||
typeof runtimeApi.loginOpenAICodex !== "function"
|
||||
) {
|
||||
throw new Error(
|
||||
"Installed @cline/core does not expose OAuth login helpers required by the CLI",
|
||||
);
|
||||
}
|
||||
return runtimeApi as CoreOAuthApi;
|
||||
});
|
||||
}
|
||||
return cachedCoreOAuthApi;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the `auth` subcommand for Commander.
|
||||
*
|
||||
@@ -137,7 +88,8 @@ export function createAuthCommand(): Command {
|
||||
.option("-p, --provider <id>", "provider id")
|
||||
.option("-k, --apikey <key>", "API key")
|
||||
.option("-m, --modelid <id>", "model id")
|
||||
.option("-b, --baseurl <url>", "base URL");
|
||||
.option("-b, --baseurl <url>", "base URL")
|
||||
.option("--azure-api-version <version>", "Azure API version");
|
||||
return cmd;
|
||||
}
|
||||
|
||||
@@ -154,6 +106,7 @@ export function parseAuthCommandArgs(args: string[]): ParsedAuthCommandArgs {
|
||||
apikey?: string;
|
||||
modelid?: string;
|
||||
baseurl?: string;
|
||||
azureApiVersion?: string;
|
||||
}>();
|
||||
const positionalProvider = cmd.args[0];
|
||||
return {
|
||||
@@ -161,6 +114,7 @@ export function parseAuthCommandArgs(args: string[]): ParsedAuthCommandArgs {
|
||||
apikey: opts.apikey,
|
||||
modelid: opts.modelid,
|
||||
baseurl: opts.baseurl,
|
||||
azureApiVersion: opts.azureApiVersion,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -200,6 +154,12 @@ async function ensureQuickSetupInputValid(
|
||||
) {
|
||||
return "base URL is only supported for OpenAI and OpenAI-compatible providers";
|
||||
}
|
||||
if (
|
||||
input.azureApiVersion?.trim() &&
|
||||
normalizedProvider !== BUILT_IN_PROVIDER.OPENAI_COMPATIBLE
|
||||
) {
|
||||
return "Azure API version is only supported for OpenAI-compatible providers";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -209,6 +169,7 @@ function saveQuickAuthProviderSettings(input: {
|
||||
apikey: string;
|
||||
modelid: string;
|
||||
baseurl?: string;
|
||||
azureApiVersion?: string;
|
||||
}): void {
|
||||
const existing = input.providerSettingsManager.getProviderSettings(
|
||||
input.providerId,
|
||||
@@ -224,6 +185,12 @@ function saveQuickAuthProviderSettings(input: {
|
||||
if (input.baseurl?.trim()) {
|
||||
nextSettings.baseUrl = input.baseurl.trim();
|
||||
}
|
||||
if (input.azureApiVersion?.trim()) {
|
||||
nextSettings.azure = {
|
||||
...(nextSettings.azure ?? {}),
|
||||
apiVersion: input.azureApiVersion.trim(),
|
||||
};
|
||||
}
|
||||
input.providerSettingsManager.saveProviderSettings(nextSettings);
|
||||
}
|
||||
|
||||
@@ -272,64 +239,18 @@ function createOAuthCallbacks(io: AuthIo): {
|
||||
});
|
||||
}
|
||||
|
||||
async function loginWithOAuthProvider(
|
||||
providerId: string,
|
||||
existing: ProviderSettings | undefined,
|
||||
io: AuthIo,
|
||||
): Promise<OAuthCredentials> {
|
||||
const oauthApi = await getCoreOAuthApi();
|
||||
const callbacks = createOAuthCallbacks(io);
|
||||
|
||||
if (providerId === "cline") {
|
||||
return oauthApi.loginClineOAuth({
|
||||
apiBaseUrl:
|
||||
existing?.baseUrl?.trim() || getClineEnvironmentConfig().apiBaseUrl,
|
||||
useWorkOSDeviceAuth: true,
|
||||
callbacks,
|
||||
});
|
||||
}
|
||||
|
||||
if (providerId === "oca") {
|
||||
const mode = existing?.oca?.mode;
|
||||
return oauthApi.loginOcaOAuth({
|
||||
mode,
|
||||
callbacks,
|
||||
});
|
||||
}
|
||||
|
||||
if (providerId === "openai-codex") {
|
||||
return oauthApi.loginOpenAICodex(callbacks);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Provider "${providerId}" does not support CLI OAuth flow (supported: cline, openai-codex, oca)`,
|
||||
);
|
||||
}
|
||||
|
||||
export function saveOAuthProviderSettings(
|
||||
providerSettingsManager: ProviderSettingsManager,
|
||||
providerId: string,
|
||||
existing: ProviderSettings | undefined,
|
||||
credentials: OAuthCredentials,
|
||||
): ProviderSettings {
|
||||
const auth = {
|
||||
...(existing?.auth ?? {}),
|
||||
accessToken: toProviderApiKey(providerId, credentials),
|
||||
refreshToken: credentials.refresh,
|
||||
accountId: credentials.accountId,
|
||||
} as ProviderSettings["auth"] & { expiresAt?: number };
|
||||
auth.expiresAt = credentials.expires;
|
||||
const merged: ProviderSettings = {
|
||||
...(existing ?? {
|
||||
provider: providerId as ProviderSettings["provider"],
|
||||
}),
|
||||
provider: providerId as ProviderSettings["provider"],
|
||||
auth,
|
||||
};
|
||||
providerSettingsManager.saveProviderSettings(merged, {
|
||||
tokenSource: "oauth",
|
||||
return saveProviderOAuthCredentials({
|
||||
manager: providerSettingsManager,
|
||||
providerId,
|
||||
settings: existing,
|
||||
credentials,
|
||||
});
|
||||
return merged;
|
||||
}
|
||||
|
||||
export async function ensureOAuthProviderApiKey(input: {
|
||||
@@ -348,19 +269,14 @@ export async function ensureOAuthProviderApiKey(input: {
|
||||
selectedProviderSettings: input.existingSettings,
|
||||
};
|
||||
}
|
||||
const credentials = await loginWithOAuthProvider(
|
||||
input.providerId,
|
||||
input.existingSettings,
|
||||
input.io,
|
||||
);
|
||||
const selectedProviderSettings = saveOAuthProviderSettings(
|
||||
const selectedProviderSettings = await loginAndSaveProviderOAuthCredentials(
|
||||
input.providerSettingsManager,
|
||||
input.providerId,
|
||||
input.existingSettings,
|
||||
credentials,
|
||||
{ callbacks: createOAuthCallbacks(input.io) },
|
||||
);
|
||||
const handler = getProviderAuthHandler(input.providerId);
|
||||
return {
|
||||
apiKey: toProviderApiKey(input.providerId, credentials),
|
||||
apiKey: handler?.getApiKey(selectedProviderSettings),
|
||||
selectedProviderSettings,
|
||||
};
|
||||
}
|
||||
@@ -370,12 +286,14 @@ async function runQuickAuthSetup(input: AuthCommandInput): Promise<number> {
|
||||
const apikey = input.apikey?.trim() ?? "";
|
||||
const modelid = input.modelid?.trim() ?? "";
|
||||
const baseurl = input.baseurl?.trim();
|
||||
const azureApiVersion = input.azureApiVersion?.trim();
|
||||
const validationError = await ensureQuickSetupInputValid(
|
||||
{
|
||||
provider: providerId,
|
||||
apikey,
|
||||
modelid,
|
||||
baseurl,
|
||||
azureApiVersion,
|
||||
},
|
||||
input.providerSettingsManager,
|
||||
);
|
||||
@@ -389,6 +307,7 @@ async function runQuickAuthSetup(input: AuthCommandInput): Promise<number> {
|
||||
apikey,
|
||||
modelid,
|
||||
baseurl,
|
||||
azureApiVersion,
|
||||
});
|
||||
input.io.writeln(
|
||||
`${c.green}Provider configured:${c.reset} ${c.cyan}${providerId}${c.reset} (${modelid})`,
|
||||
@@ -473,12 +392,13 @@ export async function runAuthCommand(input: AuthCommandInput): Promise<number> {
|
||||
const hasQuickSetupFlags =
|
||||
typeof input.apikey === "string" ||
|
||||
typeof input.modelid === "string" ||
|
||||
typeof input.baseurl === "string";
|
||||
typeof input.baseurl === "string" ||
|
||||
typeof input.azureApiVersion === "string";
|
||||
|
||||
if (hasQuickSetupFlags) {
|
||||
if (!input.explicitProvider?.trim()) {
|
||||
input.io.writeErr(
|
||||
"auth quick setup requires --provider <id> when using --apikey/--modelid/--baseurl",
|
||||
"auth quick setup requires --provider <id> when using --apikey/--modelid/--baseurl/--azure-api-version",
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
@@ -515,14 +435,15 @@ export async function runAuthProviderCommand(
|
||||
return 1;
|
||||
}
|
||||
try {
|
||||
const existing = providerSettingsManager.getProviderSettings(providerId);
|
||||
const credentials = await loginWithOAuthProvider(providerId, existing, io);
|
||||
saveOAuthProviderSettings(
|
||||
const settings = await loginAndSaveProviderOAuthCredentials(
|
||||
providerSettingsManager,
|
||||
providerId,
|
||||
existing,
|
||||
credentials,
|
||||
{ callbacks: createOAuthCallbacks(io) },
|
||||
);
|
||||
identifyTelemetryAccount({
|
||||
id: settings.auth?.accountId,
|
||||
provider: providerId,
|
||||
});
|
||||
io.writeln(
|
||||
`${c.green}You are now logged in to ${c.cyan}${providerId}${c.reset}`,
|
||||
);
|
||||
@@ -30,6 +30,18 @@ function resolveCliAgentConfigSearchPaths(cwd: string): string[] {
|
||||
return [join(cwd, ".cline", "agents"), join(clineDir, "agents")];
|
||||
}
|
||||
|
||||
function createConfigUserInstructionService(cwd: string) {
|
||||
return createUserInstructionConfigService({
|
||||
skills: {
|
||||
workspacePath: cwd,
|
||||
includePluginSkills: true,
|
||||
cwd,
|
||||
},
|
||||
rules: { workspacePath: cwd },
|
||||
workflows: { workspacePath: cwd },
|
||||
});
|
||||
}
|
||||
|
||||
async function runWorkflowsConfigCommand(
|
||||
cwd: string,
|
||||
outputMode: CliOutputMode,
|
||||
@@ -39,11 +51,7 @@ async function runWorkflowsConfigCommand(
|
||||
string,
|
||||
{ id: string; name: string; instructions: string; path: string }
|
||||
>();
|
||||
const service = createUserInstructionConfigService({
|
||||
skills: { workspacePath: cwd },
|
||||
rules: { workspacePath: cwd },
|
||||
workflows: { workspacePath: cwd },
|
||||
});
|
||||
const service = createConfigUserInstructionService(cwd);
|
||||
try {
|
||||
await service.start();
|
||||
for (const record of service.listRecords<WorkflowConfig>("workflow")) {
|
||||
@@ -90,11 +98,7 @@ async function runRulesConfigCommand(
|
||||
string,
|
||||
{ name: string; instructions: string; path: string }
|
||||
>();
|
||||
const service = createUserInstructionConfigService({
|
||||
skills: { workspacePath: cwd },
|
||||
rules: { workspacePath: cwd },
|
||||
workflows: { workspacePath: cwd },
|
||||
});
|
||||
const service = createConfigUserInstructionService(cwd);
|
||||
try {
|
||||
await service.start();
|
||||
for (const record of service.listRecords<RuleConfig>("rule")) {
|
||||
@@ -142,11 +146,7 @@ async function runSkillsConfigCommand(
|
||||
path: string;
|
||||
}
|
||||
>();
|
||||
const service = createUserInstructionConfigService({
|
||||
skills: { workspacePath: cwd },
|
||||
rules: { workspacePath: cwd },
|
||||
workflows: { workspacePath: cwd },
|
||||
});
|
||||
const service = createConfigUserInstructionService(cwd);
|
||||
try {
|
||||
await service.start();
|
||||
for (const record of service.listRecords<SkillConfig>("skill")) {
|
||||
@@ -420,11 +420,7 @@ async function runToolsConfigCommand(
|
||||
async function loadInteractiveConfigDataForCommand(
|
||||
cwd: string,
|
||||
): Promise<Awaited<ReturnType<typeof loadInteractiveConfigData>>> {
|
||||
const userInstructionService = createUserInstructionConfigService({
|
||||
skills: { workspacePath: cwd },
|
||||
rules: { workspacePath: cwd },
|
||||
workflows: { workspacePath: cwd },
|
||||
});
|
||||
const userInstructionService = createConfigUserInstructionService(cwd);
|
||||
try {
|
||||
await userInstructionService.start();
|
||||
return await loadInteractiveConfigData({
|
||||
@@ -0,0 +1,184 @@
|
||||
import { mkdirSync, mkdtempSync } from "node:fs";
|
||||
import { arch, platform, tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { runDashboardCommand, waitForProcessShutdown } from "./dashboard";
|
||||
|
||||
const ENV_KEYS = [
|
||||
"WORKSPACE_ROOT",
|
||||
"HOST",
|
||||
"CLINE_HUB_DASHBOARD_PORT",
|
||||
"PUBLIC_URL",
|
||||
"ROOM_SECRET",
|
||||
"CLINE_HUB_WEBVIEW_DIST_DIR",
|
||||
"CLINE_WRAPPER_PATH",
|
||||
] as const;
|
||||
|
||||
const originalEnv = Object.fromEntries(
|
||||
ENV_KEYS.map((key) => [key, process.env[key]]),
|
||||
);
|
||||
|
||||
afterEach(() => {
|
||||
for (const key of ENV_KEYS) {
|
||||
const value = originalEnv[key];
|
||||
if (value === undefined) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe("runDashboardCommand", () => {
|
||||
it("starts the dashboard server, opens the invite URL, and waits for shutdown", async () => {
|
||||
const output: string[] = [];
|
||||
const errors: string[] = [];
|
||||
const opened: string[] = [];
|
||||
const stop = vi.fn();
|
||||
let observedEnv:
|
||||
| {
|
||||
workspaceRoot: string | undefined;
|
||||
host: string | undefined;
|
||||
port: string | undefined;
|
||||
publicUrl: string | undefined;
|
||||
roomSecret: string | undefined;
|
||||
webviewDistDir: string | undefined;
|
||||
}
|
||||
| undefined;
|
||||
const webviewDistDir = mkdtempSync(join(tmpdir(), "cline-webview-dist-"));
|
||||
mkdirSync(webviewDistDir, { recursive: true });
|
||||
process.env.CLINE_HUB_WEBVIEW_DIST_DIR = webviewDistDir;
|
||||
|
||||
const exitCode = await runDashboardCommand({
|
||||
cwd: "sdk",
|
||||
host: "127.0.0.1",
|
||||
port: "9090",
|
||||
publicUrl: "http://127.0.0.1:9090",
|
||||
roomSecret: "secret",
|
||||
io: {
|
||||
writeln: (text) => output.push(text ?? ""),
|
||||
writeErr: (text) => errors.push(text),
|
||||
},
|
||||
startServer: async () => {
|
||||
observedEnv = {
|
||||
workspaceRoot: process.env.WORKSPACE_ROOT,
|
||||
host: process.env.HOST,
|
||||
port: process.env.CLINE_HUB_DASHBOARD_PORT,
|
||||
publicUrl: process.env.PUBLIC_URL,
|
||||
roomSecret: process.env.ROOM_SECRET,
|
||||
webviewDistDir: process.env.CLINE_HUB_WEBVIEW_DIST_DIR,
|
||||
};
|
||||
return {
|
||||
listenUrl: "http://127.0.0.1:9090/",
|
||||
publicUrl: "http://127.0.0.1:9090",
|
||||
inviteUrl: "http://127.0.0.1:9090/?roomSecret=secret",
|
||||
hubUrl: "ws://127.0.0.1:25463/hub",
|
||||
stop,
|
||||
};
|
||||
},
|
||||
openUrl: async (url) => {
|
||||
opened.push(url);
|
||||
},
|
||||
waitForShutdown: async (server) => {
|
||||
await server.stop();
|
||||
},
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(observedEnv).toEqual({
|
||||
workspaceRoot: resolve("sdk"),
|
||||
host: "127.0.0.1",
|
||||
port: "9090",
|
||||
publicUrl: "http://127.0.0.1:9090",
|
||||
roomSecret: "secret",
|
||||
webviewDistDir,
|
||||
});
|
||||
expect(opened).toEqual(["http://127.0.0.1:9090/?roomSecret=secret"]);
|
||||
expect(stop).toHaveBeenCalledTimes(1);
|
||||
expect(output.join("\n")).toContain("Cline dashboard listening at");
|
||||
expect(output.join("\n")).toContain("ws://127.0.0.1:25463/hub");
|
||||
expect(errors).toEqual([]);
|
||||
expect(process.env.WORKSPACE_ROOT).toBe(originalEnv.WORKSPACE_ROOT);
|
||||
expect(process.env.CLINE_HUB_WEBVIEW_DIST_DIR).toBe(webviewDistDir);
|
||||
});
|
||||
|
||||
it("honors --no-open behavior", async () => {
|
||||
const openUrl = vi.fn();
|
||||
|
||||
const exitCode = await runDashboardCommand({
|
||||
openBrowser: false,
|
||||
io: {
|
||||
writeln: () => {},
|
||||
writeErr: () => {},
|
||||
},
|
||||
startServer: async () => ({
|
||||
listenUrl: "http://127.0.0.1:8787/",
|
||||
publicUrl: "http://127.0.0.1:8787",
|
||||
inviteUrl: "http://127.0.0.1:8787",
|
||||
stop: vi.fn(),
|
||||
}),
|
||||
openUrl,
|
||||
waitForShutdown: async () => {},
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(openUrl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("finds webview assets from the published wrapper package layout", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "cline-wrapper-layout-"));
|
||||
const wrapperPath = join(root, "node_modules", "cline", "bin", "cline");
|
||||
const platformName = platform() === "win32" ? "windows" : platform();
|
||||
const webviewDistDir = join(
|
||||
root,
|
||||
"node_modules",
|
||||
"cline",
|
||||
"node_modules",
|
||||
"@cline",
|
||||
`cli-${platformName}-${arch()}`,
|
||||
"cline-hub",
|
||||
"webview",
|
||||
);
|
||||
mkdirSync(join(wrapperPath, ".."), { recursive: true });
|
||||
mkdirSync(webviewDistDir, { recursive: true });
|
||||
process.env.CLINE_WRAPPER_PATH = wrapperPath;
|
||||
delete process.env.CLINE_HUB_WEBVIEW_DIST_DIR;
|
||||
let observedWebviewDistDir: string | undefined;
|
||||
|
||||
const exitCode = await runDashboardCommand({
|
||||
openBrowser: false,
|
||||
io: {
|
||||
writeln: () => {},
|
||||
writeErr: () => {},
|
||||
},
|
||||
startServer: async () => {
|
||||
observedWebviewDistDir = process.env.CLINE_HUB_WEBVIEW_DIST_DIR;
|
||||
return {
|
||||
listenUrl: "http://127.0.0.1:8787/",
|
||||
publicUrl: "http://127.0.0.1:8787",
|
||||
inviteUrl: "http://127.0.0.1:8787",
|
||||
stop: vi.fn(),
|
||||
};
|
||||
},
|
||||
waitForShutdown: async () => {},
|
||||
});
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(observedWebviewDistDir).toBe(webviewDistDir);
|
||||
});
|
||||
|
||||
it("settles shutdown when server stop rejects", async () => {
|
||||
const shutdown = waitForProcessShutdown({
|
||||
listenUrl: "http://127.0.0.1:8787/",
|
||||
publicUrl: "http://127.0.0.1:8787",
|
||||
inviteUrl: "http://127.0.0.1:8787",
|
||||
stop: vi.fn(async () => {
|
||||
throw new Error("stop failed");
|
||||
}),
|
||||
});
|
||||
|
||||
process.emit("SIGINT", "SIGINT");
|
||||
|
||||
await expect(shutdown).rejects.toThrow("stop failed");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,195 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { arch, platform } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import open from "open";
|
||||
import { c } from "../utils/output";
|
||||
|
||||
export interface DashboardServerHandle {
|
||||
listenUrl: string;
|
||||
publicUrl: string;
|
||||
inviteUrl: string;
|
||||
hubUrl?: string;
|
||||
stop: () => void | Promise<void>;
|
||||
}
|
||||
|
||||
interface DashboardCommandIo {
|
||||
writeln: (text?: string) => void;
|
||||
writeErr: (text: string) => void;
|
||||
}
|
||||
|
||||
export interface RunDashboardCommandOptions {
|
||||
cwd?: string;
|
||||
host?: string;
|
||||
port?: string;
|
||||
publicUrl?: string;
|
||||
roomSecret?: string;
|
||||
openBrowser?: boolean;
|
||||
io: DashboardCommandIo;
|
||||
startServer?: () => Promise<DashboardServerHandle>;
|
||||
openUrl?: (url: string) => Promise<void>;
|
||||
waitForShutdown?: (server: DashboardServerHandle) => Promise<void>;
|
||||
}
|
||||
|
||||
const DASHBOARD_PORT_ENV = "CLINE_HUB_DASHBOARD_PORT";
|
||||
const WEBVIEW_DIST_ENV = "CLINE_HUB_WEBVIEW_DIST_DIR";
|
||||
|
||||
function setEnvValue(name: string, value: string | undefined): () => void {
|
||||
const previous = process.env[name];
|
||||
if (value === undefined) {
|
||||
return () => {};
|
||||
}
|
||||
process.env[name] = value;
|
||||
return () => {
|
||||
if (previous === undefined) {
|
||||
delete process.env[name];
|
||||
} else {
|
||||
process.env[name] = previous;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function withDashboardEnvironment<T>(
|
||||
options: RunDashboardCommandOptions,
|
||||
fn: () => Promise<T>,
|
||||
): Promise<T> {
|
||||
const restore = [
|
||||
setEnvValue(
|
||||
"WORKSPACE_ROOT",
|
||||
options.cwd ? resolve(options.cwd) : undefined,
|
||||
),
|
||||
setEnvValue("HOST", options.host),
|
||||
setEnvValue(DASHBOARD_PORT_ENV, options.port),
|
||||
setEnvValue("PUBLIC_URL", options.publicUrl),
|
||||
setEnvValue("ROOM_SECRET", options.roomSecret),
|
||||
setEnvValue(WEBVIEW_DIST_ENV, resolveDefaultWebviewDistDir()),
|
||||
];
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
for (let i = restore.length - 1; i >= 0; i--) {
|
||||
restore[i]?.();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resolveDefaultWebviewDistDir(): string | undefined {
|
||||
if (process.env[WEBVIEW_DIST_ENV]?.trim()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
||||
const candidates = [
|
||||
...resolveInstalledPlatformPackageWebviewCandidates(),
|
||||
// Source checkout: apps/cli/src/commands/dashboard.ts
|
||||
join(moduleDir, "../../../cline-hub/dist/webview"),
|
||||
// Node bundle: apps/cli/dist/index.js
|
||||
join(moduleDir, "cline-hub/webview"),
|
||||
// Compiled platform package: apps/cli/dist/<platform>/bin/cline
|
||||
join(dirname(process.execPath), "../cline-hub/webview"),
|
||||
];
|
||||
|
||||
return candidates.find((candidate) => existsSync(candidate));
|
||||
}
|
||||
|
||||
function resolveInstalledPlatformPackageWebviewCandidates(): string[] {
|
||||
const packageName = resolvePlatformPackageName();
|
||||
const starts = [
|
||||
process.env.CLINE_WRAPPER_PATH
|
||||
? dirname(process.env.CLINE_WRAPPER_PATH)
|
||||
: undefined,
|
||||
dirname(process.execPath),
|
||||
].filter((value): value is string => !!value?.trim());
|
||||
const candidates: string[] = [];
|
||||
for (const start of starts) {
|
||||
let current = start;
|
||||
for (;;) {
|
||||
candidates.push(
|
||||
join(current, "node_modules", packageName, "cline-hub/webview"),
|
||||
);
|
||||
const parent = dirname(current);
|
||||
if (parent === current) break;
|
||||
current = parent;
|
||||
}
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
function resolvePlatformPackageName(): string {
|
||||
const platformName = platform() === "win32" ? "windows" : platform();
|
||||
return `@cline/cli-${platformName}-${arch()}`;
|
||||
}
|
||||
|
||||
async function startDefaultDashboardServer(): Promise<DashboardServerHandle> {
|
||||
const { startClineHubDashboardServer } = await import("@cline/cline-hub");
|
||||
return await startClineHubDashboardServer();
|
||||
}
|
||||
|
||||
async function openDefaultUrl(url: string): Promise<void> {
|
||||
await open(url, { wait: false });
|
||||
}
|
||||
|
||||
export function waitForProcessShutdown(
|
||||
server: DashboardServerHandle,
|
||||
): Promise<void> {
|
||||
return new Promise<void>((resolveShutdown, rejectShutdown) => {
|
||||
let settled = false;
|
||||
|
||||
const cleanup = () => {
|
||||
process.off("SIGINT", handleSignal);
|
||||
process.off("SIGTERM", handleSignal);
|
||||
};
|
||||
|
||||
const stop = async () => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
cleanup();
|
||||
try {
|
||||
await server.stop();
|
||||
resolveShutdown();
|
||||
} catch (error) {
|
||||
rejectShutdown(error);
|
||||
}
|
||||
};
|
||||
|
||||
function handleSignal() {
|
||||
void stop();
|
||||
}
|
||||
|
||||
process.on("SIGINT", handleSignal);
|
||||
process.on("SIGTERM", handleSignal);
|
||||
});
|
||||
}
|
||||
|
||||
export async function runDashboardCommand(
|
||||
options: RunDashboardCommandOptions,
|
||||
): Promise<number> {
|
||||
try {
|
||||
const server = await withDashboardEnvironment(options, () =>
|
||||
(options.startServer ?? startDefaultDashboardServer)(),
|
||||
);
|
||||
const dashboardUrl =
|
||||
server.inviteUrl || server.publicUrl || server.listenUrl;
|
||||
options.io.writeln(
|
||||
`${c.green}Cline dashboard listening at${c.reset} ${dashboardUrl}`,
|
||||
);
|
||||
if (server.hubUrl) {
|
||||
options.io.writeln(`${c.dim}Hub endpoint: ${server.hubUrl}${c.reset}`);
|
||||
}
|
||||
|
||||
if (options.openBrowser !== false) {
|
||||
try {
|
||||
await (options.openUrl ?? openDefaultUrl)(dashboardUrl);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
options.io.writeErr(`Failed to open browser: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
await (options.waitForShutdown ?? waitForProcessShutdown)(server);
|
||||
return 0;
|
||||
} catch (error) {
|
||||
options.io.writeErr(error instanceof Error ? error.message : String(error));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import { getCliBuildInfo } from "../utils/common";
|
||||
const {
|
||||
mockSpawnSync,
|
||||
mockResolveClineDataDir,
|
||||
mockResolveProductionHubOwnerContext,
|
||||
mockResolveSharedHubOwnerContext,
|
||||
mockReadHubDiscovery,
|
||||
mockProbeHubServer,
|
||||
@@ -24,6 +25,15 @@ const {
|
||||
} = vi.hoisted(() => ({
|
||||
mockSpawnSync: vi.fn(),
|
||||
mockResolveClineDataDir: vi.fn(() => "/tmp/cline-data"),
|
||||
mockResolveProductionHubOwnerContext: vi.fn(() => ({
|
||||
ownerId: "hub-production",
|
||||
discoveryPath: path.join(
|
||||
"/tmp/cline-data",
|
||||
"locks",
|
||||
"hub",
|
||||
"production.json",
|
||||
),
|
||||
})),
|
||||
mockResolveSharedHubOwnerContext: vi.fn(() => ({
|
||||
ownerId: "hub-owner",
|
||||
discoveryPath: path.join(
|
||||
@@ -52,6 +62,7 @@ vi.mock("node:child_process", () => ({
|
||||
|
||||
vi.mock("@cline/core", () => ({
|
||||
resolveClineDataDir: mockResolveClineDataDir,
|
||||
resolveProductionHubOwnerContext: mockResolveProductionHubOwnerContext,
|
||||
resolveSharedHubOwnerContext: mockResolveSharedHubOwnerContext,
|
||||
clearHubDiscovery: mockClearHubDiscovery,
|
||||
probeHubServer: mockProbeHubServer,
|
||||
@@ -76,6 +87,15 @@ describe("runDoctorCommand", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockResolveClineDataDir.mockReturnValue("/tmp/cline-data");
|
||||
mockResolveProductionHubOwnerContext.mockReturnValue({
|
||||
ownerId: "hub-production",
|
||||
discoveryPath: path.join(
|
||||
"/tmp/cline-data",
|
||||
"locks",
|
||||
"hub",
|
||||
"production.json",
|
||||
),
|
||||
});
|
||||
mockStopLocalHubServerGracefully.mockResolvedValue(false);
|
||||
mockStopAllConnectors.mockResolvedValue({
|
||||
stoppedProcesses: 0,
|
||||
@@ -110,13 +130,14 @@ describe("runDoctorCommand", () => {
|
||||
command === "pgrep" &&
|
||||
Array.isArray(args) &&
|
||||
args[0] === "-fal" &&
|
||||
args[1] === "/apps/cli/src/index.ts"
|
||||
args[1] === "--" &&
|
||||
args[2] === "/apps/cli/src/index.ts"
|
||||
) {
|
||||
return {
|
||||
status: 0,
|
||||
stdout: [
|
||||
"50174 /Users/example/.bun/bin/bun /Users/example/dev/sdk/apps/cli/src/index.ts hub start --cwd /workspace",
|
||||
"50190 /Users/example/.bun/bin/bun /Users/example/dev/sdk/apps/cli/src/index.ts hey",
|
||||
"50174 /Users/example/.bun/bin/bun /Users/example/dev/apps/cli/src/index.ts hub start --cwd /workspace",
|
||||
"50190 /Users/example/.bun/bin/bun /Users/example/dev/apps/cli/src/index.ts hey",
|
||||
].join("\n"),
|
||||
};
|
||||
}
|
||||
@@ -261,12 +282,13 @@ describe("runDoctorCommand", () => {
|
||||
command === "pgrep" &&
|
||||
Array.isArray(args) &&
|
||||
args[0] === "-fal" &&
|
||||
args[1] === "/src-tauri/bin/code-sidecar"
|
||||
args[1] === "--" &&
|
||||
args[2] === "/src-tauri/bin/code-sidecar"
|
||||
) {
|
||||
return {
|
||||
status: 0,
|
||||
stdout:
|
||||
"60123 /Users/example/dev/sdk/apps/examples/desktop-app/src-tauri/bin/code-sidecar\n",
|
||||
"60123 /Users/example/dev/apps/examples/desktop-app/src-tauri/bin/code-sidecar\n",
|
||||
};
|
||||
}
|
||||
return { status: 1, stdout: "" };
|
||||
@@ -1,5 +1,5 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
||||
import { existsSync, readFileSync, rmSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import {
|
||||
clearHubDiscovery,
|
||||
@@ -7,13 +7,18 @@ import {
|
||||
probeHubServer,
|
||||
readHubDiscovery,
|
||||
resolveClineDataDir,
|
||||
resolveProductionHubOwnerContext,
|
||||
resolveSharedHubOwnerContext,
|
||||
stopLocalHubServerGracefully,
|
||||
} from "@cline/core";
|
||||
import { formatUptime } from "@cline/shared";
|
||||
import { formatUptime, resolveClineBuildEnv } from "@cline/shared";
|
||||
import { Command } from "commander";
|
||||
import open from "open";
|
||||
import { isProcessRunning } from "../connectors/common";
|
||||
import {
|
||||
type ActiveConnectorRecord,
|
||||
listActiveConnectors,
|
||||
} from "../connectors/status";
|
||||
import { getCliBuildInfo } from "../utils/common";
|
||||
import { c, writeln } from "../utils/output";
|
||||
import { stopAllConnectors } from "./connect";
|
||||
@@ -34,19 +39,6 @@ type StartupArtifact = {
|
||||
stale: boolean;
|
||||
};
|
||||
|
||||
type ActiveConnectorRecord = {
|
||||
type: string;
|
||||
pid: number;
|
||||
hubUrl: string;
|
||||
startedAt?: string;
|
||||
applicationId?: string;
|
||||
botUsername?: string;
|
||||
userName?: string;
|
||||
phoneNumberId?: string;
|
||||
port?: number;
|
||||
baseUrl?: string;
|
||||
};
|
||||
|
||||
type SpawnedProcessRecord = {
|
||||
timestamp?: string;
|
||||
pid?: number;
|
||||
@@ -63,6 +55,7 @@ type DoctorStatus = {
|
||||
hubStartedAt?: string;
|
||||
hubUptime?: string;
|
||||
listeningPids: number[];
|
||||
staleHubPids: number[];
|
||||
hubStartupLocks: StartupArtifact[];
|
||||
staleCliPids: number[];
|
||||
staleSidecarPids: number[];
|
||||
@@ -86,7 +79,11 @@ function listMatchingProcesses(pattern: string): ProcessRecord[] {
|
||||
if (process.platform === "win32") {
|
||||
return [];
|
||||
}
|
||||
const result = spawnSync("pgrep", ["-fal", pattern], { encoding: "utf8" });
|
||||
// "--" stops pgrep's option parsing so patterns that start with dashes
|
||||
// (e.g. the "--cline-hub-daemon" marker) are treated as patterns.
|
||||
const result = spawnSync("pgrep", ["-fal", "--", pattern], {
|
||||
encoding: "utf8",
|
||||
});
|
||||
if (result.status !== 0 && result.status !== 1) {
|
||||
return [];
|
||||
}
|
||||
@@ -157,6 +154,25 @@ function listStaleCliPids(): number[] {
|
||||
.map((record) => record.pid);
|
||||
}
|
||||
|
||||
function listStaleHubPids(currentHubPids: number[]): number[] {
|
||||
const current = new Set(currentHubPids.filter((pid) => pid > 0));
|
||||
const patterns = [
|
||||
"/sdk/packages/core/src/hub/daemon/entry.ts",
|
||||
"/sdk/packages/core/dist/hub/daemon/entry.js",
|
||||
"--cline-hub-daemon",
|
||||
];
|
||||
const records = new Map<number, ProcessRecord>();
|
||||
for (const pattern of patterns) {
|
||||
for (const record of listMatchingProcesses(pattern)) {
|
||||
if (current.has(record.pid) || /\bpgrep\s+-fal\b/.test(record.command)) {
|
||||
continue;
|
||||
}
|
||||
records.set(record.pid, record);
|
||||
}
|
||||
}
|
||||
return [...records.values()].map((record) => record.pid);
|
||||
}
|
||||
|
||||
function listStaleSidecarPids(): number[] {
|
||||
const patterns = [
|
||||
"/apps/examples/desktop-app/sidecar/index.ts",
|
||||
@@ -244,7 +260,7 @@ function readStartupArtifact(path: string): StartupArtifact | undefined {
|
||||
}
|
||||
|
||||
function listHubStartupLocks(_cwd: string): StartupArtifact[] {
|
||||
const owner = resolveSharedHubOwnerContext();
|
||||
const owner = resolveCliHubOwnerContext();
|
||||
const ownerPath = join(`${owner.discoveryPath}.lock`, "owner.json");
|
||||
if (!existsSync(ownerPath)) {
|
||||
return [];
|
||||
@@ -268,7 +284,7 @@ async function clearHubStartupArtifacts(
|
||||
_cwd: string,
|
||||
options?: { clearDiscovery?: boolean },
|
||||
): Promise<{ startupLocks: number; discovery: number }> {
|
||||
const owner = resolveSharedHubOwnerContext();
|
||||
const owner = resolveCliHubOwnerContext();
|
||||
const startupLocks = listHubStartupLocks(_cwd);
|
||||
let clearedStartupLocks = 0;
|
||||
for (const artifact of startupLocks) {
|
||||
@@ -287,142 +303,6 @@ async function clearHubStartupArtifacts(
|
||||
};
|
||||
}
|
||||
|
||||
function listConnectorStatePaths(
|
||||
type: ActiveConnectorRecord["type"],
|
||||
): string[] {
|
||||
const dir = join(resolveClineDataDir(), "connectors", type);
|
||||
if (!existsSync(dir)) {
|
||||
return [];
|
||||
}
|
||||
return readdirSync(dir)
|
||||
.filter((name) => name.endsWith(".json") && !name.endsWith(".threads.json"))
|
||||
.map((name) => join(dir, name));
|
||||
}
|
||||
|
||||
function readJsonRecord(path: string): Record<string, unknown> | undefined {
|
||||
if (!existsSync(path)) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const raw = readFileSync(path, "utf8");
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
||||
return parsed as Record<string, unknown>;
|
||||
}
|
||||
} catch {
|
||||
// Ignore malformed connector state.
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
type ConnectorFieldKey = keyof Omit<
|
||||
ActiveConnectorRecord,
|
||||
"type" | "pid" | "hubUrl"
|
||||
>;
|
||||
|
||||
const connectorFieldExtractors: Record<
|
||||
ConnectorFieldKey,
|
||||
(p: Record<string, unknown>) => string | number | undefined
|
||||
> = {
|
||||
startedAt: (p) => (typeof p.startedAt === "string" ? p.startedAt : undefined),
|
||||
port: (p) => (typeof p.port === "number" ? p.port : undefined),
|
||||
baseUrl: (p) => (typeof p.baseUrl === "string" ? p.baseUrl : undefined),
|
||||
userName: (p) => (typeof p.userName === "string" ? p.userName : undefined),
|
||||
botUsername: (p) =>
|
||||
typeof p.botUsername === "string" ? p.botUsername : undefined,
|
||||
applicationId: (p) =>
|
||||
typeof p.applicationId === "string" ? p.applicationId : undefined,
|
||||
phoneNumberId: (p) =>
|
||||
typeof p.phoneNumberId === "string" ? p.phoneNumberId : undefined,
|
||||
};
|
||||
|
||||
const connectorConfigs: Record<
|
||||
string,
|
||||
{ required: ConnectorFieldKey[]; optional: ConnectorFieldKey[] }
|
||||
> = {
|
||||
discord: {
|
||||
required: ["userName", "applicationId"],
|
||||
optional: ["startedAt", "port", "baseUrl"],
|
||||
},
|
||||
telegram: { required: ["botUsername"], optional: ["startedAt"] },
|
||||
gchat: { required: ["userName"], optional: ["startedAt", "port", "baseUrl"] },
|
||||
linear: {
|
||||
required: ["userName"],
|
||||
optional: ["startedAt", "port", "baseUrl"],
|
||||
},
|
||||
whatsapp: {
|
||||
required: ["userName"],
|
||||
optional: ["startedAt", "phoneNumberId", "port", "baseUrl"],
|
||||
},
|
||||
};
|
||||
|
||||
function readActiveConnectorRecord(
|
||||
type: ActiveConnectorRecord["type"],
|
||||
statePath: string,
|
||||
): ActiveConnectorRecord | undefined {
|
||||
const parsed = readJsonRecord(statePath);
|
||||
if (!parsed) {
|
||||
return undefined;
|
||||
}
|
||||
const pid = typeof parsed.pid === "number" ? parsed.pid : undefined;
|
||||
const hubUrl =
|
||||
typeof parsed.hubUrl === "string"
|
||||
? parsed.hubUrl
|
||||
: typeof parsed.rpcAddress === "string"
|
||||
? parsed.rpcAddress
|
||||
: undefined;
|
||||
if (!pid || !hubUrl || !isProcessRunning(pid)) {
|
||||
return undefined;
|
||||
}
|
||||
const config = connectorConfigs[type];
|
||||
if (!config) {
|
||||
return undefined;
|
||||
}
|
||||
const fields: Partial<
|
||||
Omit<ActiveConnectorRecord, "type" | "pid" | "hubUrl">
|
||||
> = {};
|
||||
for (const key of config.required) {
|
||||
const value = connectorFieldExtractors[key](parsed);
|
||||
if (!value || (typeof value === "string" && !value.trim())) {
|
||||
return undefined;
|
||||
}
|
||||
(fields as Record<string, unknown>)[key] = value;
|
||||
}
|
||||
for (const key of config.optional) {
|
||||
const value = connectorFieldExtractors[key](parsed);
|
||||
if (value !== undefined) {
|
||||
(fields as Record<string, unknown>)[key] = value;
|
||||
}
|
||||
}
|
||||
return { type, pid, hubUrl, ...fields } as ActiveConnectorRecord;
|
||||
}
|
||||
|
||||
function listActiveConnectors(): ActiveConnectorRecord[] {
|
||||
const connectorTypes: ActiveConnectorRecord["type"][] = [
|
||||
"telegram",
|
||||
"gchat",
|
||||
"linear",
|
||||
"whatsapp",
|
||||
];
|
||||
const records: ActiveConnectorRecord[] = [];
|
||||
for (const type of connectorTypes) {
|
||||
for (const statePath of listConnectorStatePaths(type)) {
|
||||
const record = readActiveConnectorRecord(type, statePath);
|
||||
if (record) {
|
||||
records.push(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
return records.sort((left, right) => {
|
||||
if (left.type !== right.type) {
|
||||
return left.type.localeCompare(right.type);
|
||||
}
|
||||
const leftName = left.botUsername ?? left.userName ?? "";
|
||||
const rightName = right.botUsername ?? right.userName ?? "";
|
||||
return leftName.localeCompare(rightName);
|
||||
});
|
||||
}
|
||||
|
||||
function formatHubUptimeFromStartedAt(
|
||||
startedAt: string | undefined,
|
||||
): string | undefined {
|
||||
@@ -436,14 +316,25 @@ function formatHubUptimeFromStartedAt(
|
||||
return formatUptime(Date.now() - timestamp);
|
||||
}
|
||||
|
||||
function resolveCliHubOwnerContext() {
|
||||
return resolveClineBuildEnv() === "production"
|
||||
? resolveProductionHubOwnerContext()
|
||||
: resolveSharedHubOwnerContext();
|
||||
}
|
||||
|
||||
async function collectDoctorStatus(cwd: string): Promise<DoctorStatus> {
|
||||
const owner = resolveSharedHubOwnerContext();
|
||||
const owner = resolveCliHubOwnerContext();
|
||||
const discovery = await readHubDiscovery(owner.discoveryPath);
|
||||
const health = discovery?.url
|
||||
? await probeHubServer(discovery.url)
|
||||
? await probeHubServer(discovery.url, { authToken: discovery.authToken })
|
||||
: undefined;
|
||||
const current = health ?? discovery;
|
||||
const hubUptime = formatHubUptimeFromStartedAt(health?.startedAt);
|
||||
const listeningPids = listListeningPids(current?.port);
|
||||
const currentHubPids = [
|
||||
...(current?.pid ? [current.pid] : []),
|
||||
...listeningPids,
|
||||
];
|
||||
return {
|
||||
cwd,
|
||||
hubUrl: current?.url,
|
||||
@@ -451,7 +342,8 @@ async function collectDoctorStatus(cwd: string): Promise<DoctorStatus> {
|
||||
hubPid: current?.pid,
|
||||
hubStartedAt: health?.startedAt,
|
||||
hubUptime,
|
||||
listeningPids: listListeningPids(current?.port),
|
||||
listeningPids,
|
||||
staleHubPids: listStaleHubPids(currentHubPids),
|
||||
hubStartupLocks: listHubStartupLocks(cwd),
|
||||
staleCliPids: listStaleCliPids(),
|
||||
staleSidecarPids: listStaleSidecarPids(),
|
||||
@@ -533,6 +425,7 @@ export async function runDoctorCommand(
|
||||
);
|
||||
writeln(`hub uptime ${c.dim}${before.hubUptime ?? "n/a"}${c.reset}`);
|
||||
writeln(formatPidList("hub listeners", before.listeningPids));
|
||||
writeln(formatPidList("stale hub daemons", before.staleHubPids));
|
||||
writeln(
|
||||
formatPidList(
|
||||
"hub startup locks",
|
||||
@@ -557,6 +450,7 @@ export async function runDoctorCommand(
|
||||
}
|
||||
if (
|
||||
before.listeningPids.length > 0 ||
|
||||
before.staleHubPids.length > 0 ||
|
||||
before.staleCliPids.length > 0 ||
|
||||
before.staleSidecarPids.length > 0
|
||||
) {
|
||||
@@ -568,7 +462,9 @@ export async function runDoctorCommand(
|
||||
}
|
||||
|
||||
const gracefullyStoppedHub = before.hubHealthy
|
||||
? await stopLocalHubServerGracefully().catch(() => false)
|
||||
? await stopLocalHubServerGracefully(resolveCliHubOwnerContext()).catch(
|
||||
() => false,
|
||||
)
|
||||
: false;
|
||||
const refreshedAfterGracefulStop = gracefullyStoppedHub
|
||||
? await collectDoctorStatus(opts.cwd)
|
||||
@@ -576,13 +472,20 @@ export async function runDoctorCommand(
|
||||
const killedHub = gracefullyStoppedHub
|
||||
? 0
|
||||
: killPids(refreshedAfterGracefulStop.listeningPids);
|
||||
const staleCliTargets = before.staleCliPids.filter(
|
||||
const staleHubTargets = before.staleHubPids.filter(
|
||||
(pid) => !refreshedAfterGracefulStop.listeningPids.includes(pid),
|
||||
);
|
||||
const killedStaleHubs = killPids(staleHubTargets);
|
||||
const staleCliTargets = before.staleCliPids.filter(
|
||||
(pid) =>
|
||||
!refreshedAfterGracefulStop.listeningPids.includes(pid) &&
|
||||
!staleHubTargets.includes(pid),
|
||||
);
|
||||
const killedCli = killPids(staleCliTargets);
|
||||
const staleSidecarTargets = before.staleSidecarPids.filter(
|
||||
(pid) =>
|
||||
!refreshedAfterGracefulStop.listeningPids.includes(pid) &&
|
||||
!staleHubTargets.includes(pid) &&
|
||||
!staleCliTargets.includes(pid),
|
||||
);
|
||||
const killedSidecars = killPids(staleSidecarTargets);
|
||||
@@ -604,6 +507,7 @@ export async function runDoctorCommand(
|
||||
after,
|
||||
killed: {
|
||||
hubListeners: killedHub,
|
||||
staleHubDaemons: killedStaleHubs,
|
||||
cliProcesses: killedCli,
|
||||
sidecarProcesses: killedSidecars,
|
||||
connectorProcesses: stoppedConnectors.stoppedProcesses,
|
||||
@@ -616,6 +520,7 @@ export async function runDoctorCommand(
|
||||
return 0;
|
||||
}
|
||||
writeln(`killed hub listeners ${c.dim}${killedHub}${c.reset}`);
|
||||
writeln(`killed stale hub daemons ${c.dim}${killedStaleHubs}${c.reset}`);
|
||||
writeln(`killed cli processes ${c.dim}${killedCli}${c.reset}`);
|
||||
writeln(`killed sidecar processes ${c.dim}${killedSidecars}${c.reset}`);
|
||||
writeln(
|
||||
@@ -632,6 +537,7 @@ export async function runDoctorCommand(
|
||||
);
|
||||
writeln(`hub healthy after fix: ${after.hubHealthy ? "yes" : "no"}`);
|
||||
writeln(formatPidList("remaining hub listeners", after.listeningPids));
|
||||
writeln(formatPidList("remaining stale hub daemons", after.staleHubPids));
|
||||
writeln(
|
||||
formatPidList(
|
||||
"remaining hub startup locks",
|
||||
@@ -1,10 +1,11 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const {
|
||||
mockClearHubDiscovery,
|
||||
mockEnsureDetachedHubServer,
|
||||
mockProbeHubServer,
|
||||
mockReadHubDiscovery,
|
||||
mockResolveProductionHubOwnerContext,
|
||||
mockResolveSharedHubOwnerContext,
|
||||
mockStopLocalHubServerGracefully,
|
||||
} = vi.hoisted(() => ({
|
||||
@@ -12,6 +13,10 @@ const {
|
||||
mockEnsureDetachedHubServer: vi.fn(),
|
||||
mockProbeHubServer: vi.fn(),
|
||||
mockReadHubDiscovery: vi.fn(),
|
||||
mockResolveProductionHubOwnerContext: vi.fn(() => ({
|
||||
ownerId: "hub-production",
|
||||
discoveryPath: "/tmp/cline-data/locks/hub/production.json",
|
||||
})),
|
||||
mockResolveSharedHubOwnerContext: vi.fn(() => ({
|
||||
ownerId: "hub-owner",
|
||||
discoveryPath: "/tmp/cline-data/locks/hub/owners/hub-owner.json",
|
||||
@@ -24,13 +29,25 @@ vi.mock("@cline/core", () => ({
|
||||
ensureDetachedHubServer: mockEnsureDetachedHubServer,
|
||||
probeHubServer: mockProbeHubServer,
|
||||
readHubDiscovery: mockReadHubDiscovery,
|
||||
resolveProductionHubOwnerContext: mockResolveProductionHubOwnerContext,
|
||||
resolveSharedHubOwnerContext: mockResolveSharedHubOwnerContext,
|
||||
stopLocalHubServerGracefully: mockStopLocalHubServerGracefully,
|
||||
}));
|
||||
|
||||
import { createHubCommand } from "./hub";
|
||||
|
||||
const originalBuildEnv = process.env.CLINE_BUILD_ENV;
|
||||
|
||||
describe("createHubCommand", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
if (originalBuildEnv === undefined) {
|
||||
delete process.env.CLINE_BUILD_ENV;
|
||||
} else {
|
||||
process.env.CLINE_BUILD_ENV = originalBuildEnv;
|
||||
}
|
||||
});
|
||||
|
||||
it("includes uptime in hub status output", async () => {
|
||||
vi.spyOn(Date, "now").mockReturnValue(
|
||||
new Date("2026-01-01T00:01:05.000Z").getTime(),
|
||||
@@ -73,4 +90,37 @@ describe("createHubCommand", () => {
|
||||
uptime: "1m 5s",
|
||||
});
|
||||
});
|
||||
|
||||
it("passes the selected owner to graceful stop", async () => {
|
||||
process.env.CLINE_BUILD_ENV = "development";
|
||||
mockReadHubDiscovery.mockResolvedValue({
|
||||
url: "ws://127.0.0.1:25466/hub",
|
||||
port: 25466,
|
||||
pid: 50174,
|
||||
});
|
||||
mockStopLocalHubServerGracefully.mockResolvedValue(true);
|
||||
|
||||
const output: string[] = [];
|
||||
let exitCode = 0;
|
||||
const cmd = createHubCommand(
|
||||
{
|
||||
writeln: (text) => {
|
||||
output.push(text ?? "");
|
||||
},
|
||||
writeErr: () => {},
|
||||
},
|
||||
(code) => {
|
||||
exitCode = code;
|
||||
},
|
||||
);
|
||||
|
||||
await cmd.parseAsync(["stop"], { from: "user" });
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(mockStopLocalHubServerGracefully).toHaveBeenCalledWith({
|
||||
ownerId: "hub-owner",
|
||||
discoveryPath: "/tmp/cline-data/locks/hub/owners/hub-owner.json",
|
||||
});
|
||||
expect(JSON.parse(output[0] || "")).toEqual({ stopped: true });
|
||||
});
|
||||
});
|
||||
@@ -3,10 +3,11 @@ import {
|
||||
ensureDetachedHubServer,
|
||||
probeHubServer,
|
||||
readHubDiscovery,
|
||||
resolveProductionHubOwnerContext,
|
||||
resolveSharedHubOwnerContext,
|
||||
stopLocalHubServerGracefully,
|
||||
} from "@cline/core";
|
||||
import { formatUptime } from "@cline/shared";
|
||||
import { formatUptime, resolveClineBuildEnv } from "@cline/shared";
|
||||
import { Command } from "commander";
|
||||
|
||||
interface HubCommandIo {
|
||||
@@ -15,9 +16,9 @@ interface HubCommandIo {
|
||||
}
|
||||
|
||||
async function stopHubServer(_workspaceRoot: string): Promise<boolean> {
|
||||
const owner = resolveSharedHubOwnerContext();
|
||||
const owner = resolveCliHubOwnerContext();
|
||||
const discovery = await readHubDiscovery(owner.discoveryPath);
|
||||
if (await stopLocalHubServerGracefully()) {
|
||||
if (await stopLocalHubServerGracefully(owner)) {
|
||||
await clearHubDiscovery(owner.discoveryPath);
|
||||
return true;
|
||||
}
|
||||
@@ -46,6 +47,12 @@ function formatHubUptimeFromStartedAt(
|
||||
return formatUptime(Date.now() - timestamp);
|
||||
}
|
||||
|
||||
function resolveCliHubOwnerContext() {
|
||||
return resolveClineBuildEnv() === "production"
|
||||
? resolveProductionHubOwnerContext()
|
||||
: resolveSharedHubOwnerContext();
|
||||
}
|
||||
|
||||
export function createHubCommand(
|
||||
io: HubCommandIo,
|
||||
setExitCode: (code: number) => void,
|
||||
@@ -112,10 +119,12 @@ export function createHubCommand(
|
||||
|
||||
hub.command("status").action(
|
||||
action(async () => {
|
||||
const owner = resolveSharedHubOwnerContext();
|
||||
const owner = resolveCliHubOwnerContext();
|
||||
const discovery = await readHubDiscovery(owner.discoveryPath);
|
||||
const health = discovery?.url
|
||||
? await probeHubServer(discovery.url)
|
||||
? await probeHubServer(discovery.url, {
|
||||
authToken: discovery.authToken,
|
||||
})
|
||||
: undefined;
|
||||
const uptime = formatHubUptimeFromStartedAt(health?.startedAt);
|
||||
io.writeln(
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user