mirror of
https://github.com/cline/cline.git
synced 2026-09-04 11:44:01 +08:00
Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8660e6a2ce | |||
| 091cbb9a58 | |||
| 65f683d5e6 | |||
| 5bc4455862 | |||
| ecd51562c2 | |||
| df4cb1fac9 | |||
| a5532236e3 | |||
| c3844fa076 | |||
| 16c3011d96 | |||
| 9e3c50412c | |||
| 87d287b127 | |||
| 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 |
+1
-1
@@ -1,2 +1,2 @@
|
||||
/.github/ @saoudrizwan @arafatkatze @maxpaulus43 @candieduniverse
|
||||
/.github/ @saoudrizwan @arafatkatze @maxpaulus43 @dominiccooney
|
||||
/README.md @saoudrizwan @juanpflores
|
||||
|
||||
@@ -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,69 +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
|
||||
defaults:
|
||||
run:
|
||||
working-directory: apps/vscode
|
||||
|
||||
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:
|
||||
|
||||
@@ -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
|
||||
|
||||
+11
@@ -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
+22
-21
@@ -5,8 +5,8 @@
|
||||
"tasks": [
|
||||
{
|
||||
"label": "compile-standalone",
|
||||
"type": "npm",
|
||||
"script": "compile-standalone",
|
||||
"type": "shell",
|
||||
"command": "npm run compile-standalone",
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
@@ -18,8 +18,8 @@
|
||||
},
|
||||
{
|
||||
"label": "npm: protos",
|
||||
"type": "npm",
|
||||
"script": "protos",
|
||||
"type": "shell",
|
||||
"command": "npm run protos",
|
||||
"problemMatcher": [],
|
||||
"isBackground": false,
|
||||
"presentation": {
|
||||
@@ -64,8 +64,8 @@
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "build:webview",
|
||||
"type": "shell",
|
||||
"command": "npm run build:webview",
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"isBackground": true,
|
||||
@@ -85,8 +85,8 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "build:webview:test",
|
||||
"type": "shell",
|
||||
"command": "npm run build:webview:test",
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"isBackground": true,
|
||||
@@ -107,8 +107,8 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "dev:webview",
|
||||
"type": "shell",
|
||||
"command": "npm run dev:webview",
|
||||
"group": "build",
|
||||
"problemMatcher": [
|
||||
{
|
||||
@@ -144,8 +144,8 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch:esbuild",
|
||||
"type": "shell",
|
||||
"command": "npm run watch:esbuild",
|
||||
"group": "build",
|
||||
"problemMatcher": {
|
||||
"pattern": [
|
||||
@@ -183,8 +183,8 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch:esbuild:test",
|
||||
"type": "shell",
|
||||
"command": "npm run watch:esbuild:test",
|
||||
"group": "build",
|
||||
"problemMatcher": {
|
||||
"pattern": [
|
||||
@@ -223,8 +223,8 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch:tsc",
|
||||
"type": "shell",
|
||||
"command": "npm run watch:tsc",
|
||||
"group": "build",
|
||||
"problemMatcher": "$tsc-watch",
|
||||
"isBackground": true,
|
||||
@@ -241,8 +241,9 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch-tests",
|
||||
"type": "shell",
|
||||
"command": "npm run watch-tests",
|
||||
"label": "npm: watch-tests",
|
||||
"problemMatcher": "$tsc-watch",
|
||||
"isBackground": true,
|
||||
"dependsOn": [
|
||||
@@ -280,8 +281,8 @@
|
||||
"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,
|
||||
@@ -308,7 +309,7 @@
|
||||
"$tsc"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/sdk"
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,5 +1,43 @@
|
||||
# Changelog
|
||||
|
||||
## [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
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.4.5/schema.json",
|
||||
"root": false,
|
||||
"extends": ["../sdk/biome.json"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"a11y": {
|
||||
"noStaticElementInteractions": "warn"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,25 @@
|
||||
# Cline CLI Changelog
|
||||
|
||||
## 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.
|
||||
@@ -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 (`apps/cli/.cline/skills/publish-cli/SKILL.md`).
|
||||
|
||||
From the `apps/cli` workspace:
|
||||
|
||||
@@ -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"],
|
||||
@@ -63,10 +116,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 +126,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.13",
|
||||
"version": "3.0.15",
|
||||
"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",
|
||||
@@ -90,12 +91,15 @@
|
||||
"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;
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
saveOAuthProviderSettings,
|
||||
toProviderApiKey,
|
||||
} from "../commands/auth";
|
||||
import { writeErr } from "../utils/output";
|
||||
import { writeDiagnostic } from "../utils/output";
|
||||
|
||||
/**
|
||||
* Supported ACP OAuth provider IDs.
|
||||
@@ -73,10 +73,10 @@ async function performOAuthLogin(
|
||||
),
|
||||
);
|
||||
},
|
||||
onOutput: (message) => writeErr(`[acp/auth] ${message}`),
|
||||
onOutput: (message) => writeDiagnostic(`[acp/auth] ${message}`),
|
||||
openUrl: (url) => open(url, { wait: false }).then(() => undefined),
|
||||
onOpenUrlError: ({ url }) => {
|
||||
writeErr(
|
||||
writeDiagnostic(
|
||||
`[acp/auth] Could not open browser automatically. Open this URL manually:\n${url}`,
|
||||
);
|
||||
},
|
||||
@@ -116,12 +116,12 @@ export async function authenticateAcpProvider(
|
||||
// Check for already-stored credentials.
|
||||
const existingKey = getPersistedProviderApiKey(methodId, existing);
|
||||
if (existingKey) {
|
||||
writeErr(`[acp/auth] Using existing credentials for ${methodId}`);
|
||||
writeDiagnostic(`[acp/auth] Using existing credentials for ${methodId}`);
|
||||
return { providerId: methodId, apiKey: existingKey };
|
||||
}
|
||||
|
||||
// Perform a fresh OAuth login.
|
||||
writeErr(`[acp/auth] Starting OAuth login for ${methodId}…`);
|
||||
writeDiagnostic(`[acp/auth] Starting OAuth login for ${methodId}…`);
|
||||
const credentials = await performOAuthLogin(methodId, existing);
|
||||
|
||||
saveOAuthProviderSettings(
|
||||
@@ -132,6 +132,6 @@ export async function authenticateAcpProvider(
|
||||
);
|
||||
|
||||
const apiKey = toProviderApiKey(methodId, credentials);
|
||||
writeErr(`[acp/auth] Successfully authenticated with ${methodId}`);
|
||||
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>,
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -115,8 +115,8 @@ describe("runDoctorCommand", () => {
|
||||
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"),
|
||||
};
|
||||
}
|
||||
@@ -266,7 +266,7 @@ describe("runDoctorCommand", () => {
|
||||
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,
|
||||
@@ -14,6 +14,10 @@ import { formatUptime } 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 +38,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;
|
||||
@@ -287,142 +278,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 {
|
||||
@@ -1,3 +1,4 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import {
|
||||
existsSync,
|
||||
mkdtempSync,
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
installPlugin,
|
||||
isOfficialPluginSlug,
|
||||
parsePluginSource,
|
||||
runPluginInstallCommand,
|
||||
} from "./plugin";
|
||||
@@ -47,6 +49,29 @@ describe("plugin install command", () => {
|
||||
setClineDir(process.env.CLINE_DIR);
|
||||
});
|
||||
|
||||
function runGitCommand(cwd: string, args: string[]): void {
|
||||
execFileSync("git", args, { cwd, stdio: "ignore" });
|
||||
}
|
||||
|
||||
async function createOfficialPluginsRepo(
|
||||
plugins: Record<string, Record<string, string>>,
|
||||
): Promise<string> {
|
||||
const repo = mkdtempSync(join(root, "official-plugins-"));
|
||||
for (const [slug, files] of Object.entries(plugins)) {
|
||||
const pluginRoot = join(repo, "plugins", slug);
|
||||
await mkdir(pluginRoot, { recursive: true });
|
||||
for (const [filename, content] of Object.entries(files)) {
|
||||
await writeFile(join(pluginRoot, filename), content, "utf8");
|
||||
}
|
||||
}
|
||||
runGitCommand(repo, ["init"]);
|
||||
runGitCommand(repo, ["config", "user.email", "test@example.com"]);
|
||||
runGitCommand(repo, ["config", "user.name", "Cline Test"]);
|
||||
runGitCommand(repo, ["add", "."]);
|
||||
runGitCommand(repo, ["commit", "-m", "seed plugins"]);
|
||||
return repo;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.unstubAllGlobals();
|
||||
@@ -85,6 +110,25 @@ describe("plugin install command", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("parses bare official keywords as official plugin slugs", () => {
|
||||
expect(isOfficialPluginSlug("clickhouse")).toBe(true);
|
||||
expect(isOfficialPluginSlug("web-search")).toBe(true);
|
||||
expect(isOfficialPluginSlug("WebSearch")).toBe(false);
|
||||
expect(parsePluginSource("clickhouse")).toEqual({
|
||||
type: "official",
|
||||
slug: "clickhouse",
|
||||
});
|
||||
expect(parsePluginSource("web-search")).toEqual({
|
||||
type: "official",
|
||||
slug: "web-search",
|
||||
});
|
||||
expect(parsePluginSource("web-search", "npm")).toEqual({
|
||||
type: "npm",
|
||||
spec: "web-search",
|
||||
name: "web-search",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects hostname-style sources without --git guidance", () => {
|
||||
expect(() => parsePluginSource("github.com/acme/plugin")).toThrow(
|
||||
/Use --git/,
|
||||
@@ -169,6 +213,125 @@ describe("plugin install command", () => {
|
||||
).toEqual(result.entryPaths);
|
||||
});
|
||||
|
||||
it("installs an official plugin slug from the configured collection repo", async () => {
|
||||
const officialPluginsRepo = await createOfficialPluginsRepo({
|
||||
"web-search": {
|
||||
"index.ts":
|
||||
"export default { name: 'official-web-search', manifest: { capabilities: ['tools'] } };",
|
||||
},
|
||||
"other-plugin": {
|
||||
"index.ts":
|
||||
"export default { name: 'other-plugin', manifest: { capabilities: ['tools'] } };",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await installPlugin({
|
||||
source: "web-search",
|
||||
cwd: workspace,
|
||||
officialPluginsRepo,
|
||||
});
|
||||
|
||||
expect(result.installPath).toContain(
|
||||
join(workspace, ".cline", "plugins", "_installed", "official"),
|
||||
);
|
||||
expect(result.entryPaths).toHaveLength(1);
|
||||
expect(readFileSync(result.entryPaths[0] ?? "", "utf8")).toContain(
|
||||
"official-web-search",
|
||||
);
|
||||
expect(existsSync(join(result.installPath, "repo"))).toBe(false);
|
||||
expect(
|
||||
existsSync(join(result.installPath, "package", "other-plugin")),
|
||||
).toBe(false);
|
||||
expect(
|
||||
discoverPluginModulePaths(join(workspace, ".cline", "plugins")),
|
||||
).toEqual(result.entryPaths);
|
||||
});
|
||||
|
||||
it("installs an official package plugin and runs package dependency install", async () => {
|
||||
const officialPluginsRepo = await createOfficialPluginsRepo({
|
||||
"package-plugin": {
|
||||
"package.json": JSON.stringify(
|
||||
{
|
||||
name: "package-plugin",
|
||||
type: "module",
|
||||
cline: {
|
||||
plugins: [{ paths: ["./index.ts"] }],
|
||||
},
|
||||
dependencies: {
|
||||
yaml: "^2.8.1",
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"index.ts":
|
||||
"export default { name: 'package-plugin', manifest: { capabilities: ['tools'] } };",
|
||||
},
|
||||
});
|
||||
const npmLogPath = join(root, "official-npm-install.log");
|
||||
const npmCommandPath = join(root, "official-fake-npm.sh");
|
||||
writeFileSync(
|
||||
npmCommandPath,
|
||||
`#!/bin/sh\nprintf '%s\\n' "$PWD $*" >> "${npmLogPath}"\nexit 0\n`,
|
||||
{ encoding: "utf8", mode: 0o755 },
|
||||
);
|
||||
|
||||
const result = await installPlugin({
|
||||
source: "package-plugin",
|
||||
cwd: workspace,
|
||||
officialPluginsRepo,
|
||||
npmCommand: npmCommandPath,
|
||||
});
|
||||
|
||||
const npmLog = readFileSync(npmLogPath, "utf8");
|
||||
expect(npmLog).toContain("package install --omit=dev --omit=peer");
|
||||
expect(result.entryPaths).toHaveLength(1);
|
||||
expect(readFileSync(result.entryPaths[0] ?? "", "utf8")).toContain(
|
||||
"package-plugin",
|
||||
);
|
||||
});
|
||||
|
||||
it("reports a clear error when an official plugin slug is missing", async () => {
|
||||
const officialPluginsRepo = await createOfficialPluginsRepo({
|
||||
"known-plugin": {
|
||||
"index.ts":
|
||||
"export default { name: 'known-plugin', manifest: { capabilities: ['tools'] } };",
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
installPlugin({
|
||||
source: "missing-plugin",
|
||||
cwd: workspace,
|
||||
officialPluginsRepo,
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
/Official Cline plugin "missing-plugin" was not found at plugins\/missing-plugin/,
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps explicit relative paths as local plugin installs", async () => {
|
||||
const localPluginRoot = join(workspace, "web-search");
|
||||
await mkdir(localPluginRoot, { recursive: true });
|
||||
await writeFile(
|
||||
join(localPluginRoot, "index.ts"),
|
||||
"export default { name: 'local-web-search', manifest: { capabilities: ['tools'] } };",
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const result = await installPlugin({
|
||||
source: "./web-search",
|
||||
cwd: workspace,
|
||||
});
|
||||
|
||||
expect(result.installPath).toContain(
|
||||
join(workspace, ".cline", "plugins", "_installed", "local"),
|
||||
);
|
||||
expect(readFileSync(result.entryPaths[0] ?? "", "utf8")).toContain(
|
||||
"local-web-search",
|
||||
);
|
||||
});
|
||||
|
||||
it("times out stalled remote plugin downloads", async () => {
|
||||
vi.useFakeTimers();
|
||||
const source =
|
||||
@@ -258,6 +421,7 @@ describe("plugin install command", () => {
|
||||
},
|
||||
peerDependencies: {
|
||||
"@cline/shared": "*",
|
||||
bun: ">=1.0.0",
|
||||
},
|
||||
peerDependenciesMeta: {
|
||||
"@cline/shared": {
|
||||
@@ -304,12 +468,12 @@ describe("plugin install command", () => {
|
||||
peerDependenciesMeta?: Record<string, unknown>;
|
||||
};
|
||||
expect(packageManifest.dependencies).toEqual({ yaml: "^2.8.1" });
|
||||
expect(packageManifest.peerDependencies).toBeUndefined();
|
||||
expect(packageManifest.peerDependencies).toEqual({ bun: ">=1.0.0" });
|
||||
expect(packageManifest.peerDependenciesMeta).toBeUndefined();
|
||||
const npmLog = readFileSync(npmLogPath, "utf8");
|
||||
expect(npmLog).toContain(`${join(".tmp")}/`);
|
||||
expect(npmLog).toContain(
|
||||
"package install --omit=dev --no-audit --no-fund --package-lock=false",
|
||||
"package install --omit=dev --omit=peer --legacy-peer-deps --no-audit --no-fund --package-lock=false",
|
||||
);
|
||||
expect(existsSync(join(result.installPath, "package", ".git"))).toBe(false);
|
||||
expect(
|
||||
@@ -356,6 +520,7 @@ describe("plugin install command", () => {
|
||||
const npmLog = readFileSync(npmLogPath, "utf8");
|
||||
expect(npmLog).toContain("install published-plugin@1.0.0");
|
||||
expect(npmLog).toContain("--omit=peer");
|
||||
expect(npmLog).toContain("--legacy-peer-deps");
|
||||
expect(
|
||||
existsSync(
|
||||
join(result.installPath, "package", "node_modules", "@cline", "core"),
|
||||
@@ -458,6 +623,40 @@ describe("plugin install command", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("prints JSON output for official plugin installs", async () => {
|
||||
const officialPluginsRepo = await createOfficialPluginsRepo({
|
||||
"json-plugin": {
|
||||
"index.ts":
|
||||
"export default { name: 'json-plugin', manifest: { capabilities: ['tools'] } };",
|
||||
},
|
||||
});
|
||||
const stdout: string[] = [];
|
||||
const originalWrite = process.stdout.write;
|
||||
process.stdout.write = ((chunk: string | Uint8Array) => {
|
||||
stdout.push(String(chunk));
|
||||
return true;
|
||||
}) as typeof process.stdout.write;
|
||||
try {
|
||||
const code = await runPluginInstallCommand({
|
||||
source: "json-plugin",
|
||||
cwd: workspace,
|
||||
officialPluginsRepo,
|
||||
json: true,
|
||||
io: {
|
||||
writeln: () => {},
|
||||
writeErr: () => {},
|
||||
},
|
||||
});
|
||||
expect(code).toBe(0);
|
||||
const parsed = JSON.parse(stdout.join("")) as { installPath: string };
|
||||
expect(parsed.installPath).toContain(
|
||||
join(workspace, ".cline", "plugins", "_installed", "official"),
|
||||
);
|
||||
} finally {
|
||||
process.stdout.write = originalWrite;
|
||||
}
|
||||
});
|
||||
|
||||
it("uses shared search paths for cwd installs", async () => {
|
||||
const source = join(root, "workspace.ts");
|
||||
writeFileSync(
|
||||
@@ -25,6 +25,7 @@ export interface PluginInstallOptions {
|
||||
cwd?: string;
|
||||
force?: boolean;
|
||||
npmCommand?: string;
|
||||
officialPluginsRepo?: string;
|
||||
io?: PluginInstallIo;
|
||||
}
|
||||
|
||||
@@ -60,6 +61,10 @@ type ParsedPluginSource =
|
||||
| {
|
||||
type: "local";
|
||||
path: string;
|
||||
}
|
||||
| {
|
||||
type: "official";
|
||||
slug: string;
|
||||
};
|
||||
|
||||
type PluginInstallSourceType = "npm" | "git" | "local" | "remote";
|
||||
@@ -77,6 +82,7 @@ interface PluginPackageManifest {
|
||||
|
||||
const INSTALLS_DIRECTORY_NAME = "_installed";
|
||||
const PACKAGE_DIRECTORY_NAME = "package";
|
||||
const OFFICIAL_PLUGINS_REPO = "https://github.com/cline/plugins.git";
|
||||
const REMOTE_PLUGIN_FETCH_TIMEOUT_MS = 30_000;
|
||||
const REMOTE_PLUGIN_MAX_BYTES = 10 * 1024 * 1024;
|
||||
const HOST_PROVIDED_SDK_PREFIX = "@cline/";
|
||||
@@ -121,6 +127,14 @@ function sanitizeSegment(value: string): string {
|
||||
return sanitized || "plugin";
|
||||
}
|
||||
|
||||
export function isOfficialPluginSlug(source: string): boolean {
|
||||
return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(source.trim());
|
||||
}
|
||||
|
||||
function resolveOfficialPluginsRepo(override: string | undefined): string {
|
||||
return override?.trim() || OFFICIAL_PLUGINS_REPO;
|
||||
}
|
||||
|
||||
function parseNpmSpec(spec: string): { name: string } {
|
||||
const trimmed = spec.trim();
|
||||
const match = trimmed.match(/^(@?[^@/]+(?:\/[^@/]+)?)(?:@.+)?$/);
|
||||
@@ -371,6 +385,9 @@ export function parsePluginSource(
|
||||
if (git) {
|
||||
return git;
|
||||
}
|
||||
if (isOfficialPluginSlug(trimmed)) {
|
||||
return { type: "official", slug: trimmed };
|
||||
}
|
||||
if (looksLikeHostnamePath(trimmed)) {
|
||||
throw new Error(
|
||||
`Unrecognized plugin source "${source}". Use --git for hostname-style repositories or pass an explicit local path such as ./github.com/owner/repo.`,
|
||||
@@ -415,6 +432,14 @@ function getInstallPath(
|
||||
`${sanitizeSegment(parsed.filename)}-${hashSource(sourceKey)}`,
|
||||
);
|
||||
}
|
||||
if (parsed.type === "official") {
|
||||
return join(
|
||||
pluginRoot,
|
||||
INSTALLS_DIRECTORY_NAME,
|
||||
"official",
|
||||
`${sanitizeSegment(parsed.slug)}-${hashSource(sourceKey)}`,
|
||||
);
|
||||
}
|
||||
return join(
|
||||
pluginRoot,
|
||||
INSTALLS_DIRECTORY_NAME,
|
||||
@@ -423,7 +448,11 @@ function getInstallPath(
|
||||
);
|
||||
}
|
||||
|
||||
function getInstallSourceKey(parsed: ParsedPluginSource, cwd: string): string {
|
||||
function getInstallSourceKey(
|
||||
parsed: ParsedPluginSource,
|
||||
cwd: string,
|
||||
officialPluginsRepo: string,
|
||||
): string {
|
||||
if (parsed.type === "npm") {
|
||||
return `npm:${parsed.spec}`;
|
||||
}
|
||||
@@ -433,6 +462,9 @@ function getInstallSourceKey(parsed: ParsedPluginSource, cwd: string): string {
|
||||
if (parsed.type === "remote") {
|
||||
return `remote:${parsed.url}`;
|
||||
}
|
||||
if (parsed.type === "official") {
|
||||
return `official:${officialPluginsRepo}#plugins/${parsed.slug}`;
|
||||
}
|
||||
return `local:${resolve(cwd, resolveHomePath(parsed.path))}`;
|
||||
}
|
||||
|
||||
@@ -671,6 +703,7 @@ async function installNpmPackage(
|
||||
packageRoot,
|
||||
"--omit=dev",
|
||||
"--omit=peer",
|
||||
"--legacy-peer-deps",
|
||||
"--no-audit",
|
||||
"--no-fund",
|
||||
"--package-lock=false",
|
||||
@@ -692,6 +725,8 @@ async function installPackageDependencies(
|
||||
[
|
||||
"install",
|
||||
"--omit=dev",
|
||||
"--omit=peer",
|
||||
"--legacy-peer-deps",
|
||||
"--no-audit",
|
||||
"--no-fund",
|
||||
"--package-lock=false",
|
||||
@@ -729,6 +764,43 @@ async function installGitPackage(
|
||||
return packageRoot;
|
||||
}
|
||||
|
||||
async function installOfficialPlugin(
|
||||
parsed: Extract<ParsedPluginSource, { type: "official" }>,
|
||||
stagingRoot: string,
|
||||
npmCommand: string,
|
||||
officialPluginsRepo: string,
|
||||
): Promise<string> {
|
||||
const repoRoot = join(stagingRoot, "repo");
|
||||
await runCommand("git", [
|
||||
"clone",
|
||||
"--filter=blob:none",
|
||||
"--depth",
|
||||
"1",
|
||||
"--",
|
||||
officialPluginsRepo,
|
||||
repoRoot,
|
||||
]);
|
||||
|
||||
const sourceRoot = join(repoRoot, "plugins", parsed.slug);
|
||||
if (!existsSync(sourceRoot) || !statSync(sourceRoot).isDirectory()) {
|
||||
throw new Error(
|
||||
`Official Cline plugin "${parsed.slug}" was not found at plugins/${parsed.slug} in ${officialPluginsRepo}`,
|
||||
);
|
||||
}
|
||||
|
||||
const packageRoot = join(stagingRoot, PACKAGE_DIRECTORY_NAME);
|
||||
await cp(sourceRoot, packageRoot, {
|
||||
recursive: true,
|
||||
filter: (sourcePath) => {
|
||||
const name = basename(sourcePath);
|
||||
return name !== ".git" && name !== "node_modules";
|
||||
},
|
||||
});
|
||||
rmSync(repoRoot, { recursive: true, force: true });
|
||||
await installPackageDependencies(packageRoot, npmCommand);
|
||||
return packageRoot;
|
||||
}
|
||||
|
||||
function remotePluginSizeLimitError(url: string): Error {
|
||||
return new Error(
|
||||
`Remote plugin file from ${url} exceeds the ${REMOTE_PLUGIN_MAX_BYTES} byte limit`,
|
||||
@@ -910,7 +982,10 @@ export async function installPlugin(
|
||||
const explicitCwd = options.cwd?.trim();
|
||||
const cwd = explicitCwd ? resolve(explicitCwd) : process.cwd();
|
||||
const pluginRoot = getPluginRoot(explicitCwd ? cwd : undefined);
|
||||
const sourceKey = getInstallSourceKey(parsed, cwd);
|
||||
const officialPluginsRepo = resolveOfficialPluginsRepo(
|
||||
options.officialPluginsRepo,
|
||||
);
|
||||
const sourceKey = getInstallSourceKey(parsed, cwd, officialPluginsRepo);
|
||||
const installPath = getInstallPath(pluginRoot, parsed, sourceKey);
|
||||
const stagingParent = join(pluginRoot, INSTALLS_DIRECTORY_NAME, ".tmp");
|
||||
const stagingRoot = join(
|
||||
@@ -930,6 +1005,13 @@ export async function installPlugin(
|
||||
packageRoot = await installNpmPackage(parsed, stagingRoot, npmCommand);
|
||||
} else if (parsed.type === "git") {
|
||||
packageRoot = await installGitPackage(parsed, stagingRoot, npmCommand);
|
||||
} else if (parsed.type === "official") {
|
||||
packageRoot = await installOfficialPlugin(
|
||||
parsed,
|
||||
stagingRoot,
|
||||
npmCommand,
|
||||
officialPluginsRepo,
|
||||
);
|
||||
} else if (parsed.type === "remote") {
|
||||
packageRoot = await installRemoteFile(parsed, stagingRoot);
|
||||
} else {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user