ci: move SDK publishing into cline/cline and retire legacy CLI infra (#10648)

* ci: migrate SDK publish workflows to repo root

Move publish-cli.yaml, publish-sdk.yaml, and test.yml (renamed to
sdk-test.yml) from sdk/.github/workflows/ to the repo root so GitHub
Actions actually picks them up. Adapt them to run with cwd sdk/ via
workflow-level defaults.run.working-directory, repoint repo guards
from cline/sdk to cline/cline, switch publish-sdk's nested test call
to sdk-test.yml, add path filters on sdk-test.yml so it doesn't fire
on extension-only PRs, and re-enable NPM_CONFIG_PROVENANCE now that
cline/cline is public.

Update the publish-cli skill with a cwd note so the documented
release commands keep working from sdk/.

* ci: remove legacy CLI publish workflows

The legacy publish chain (publish-cli-trusted.yaml dispatching into
npm-main.yaml + npm-nightly.yaml) publishes the old cli/ folder to
the same cline npm package the new SDK CLI is taking over. Leave
both wired up and a maintainer could accidentally publish an old
build over the handoff. Remove the dispatcher, the two callees, the
PR-preview tarball workflow (pack-cli.yml + build-cli-artifact.sh),
the TUI test workflow that only fed into them, and the npm packaging
script those workflows shared.

The cli/ source itself is left in place for a separate removal PR.

* chore: remove legacy CLI dev and eval helpers

With the legacy CLI publish workflows gone, the surrounding dev and
eval glue that only existed to feed those workflows is also dead.
Delete tests/e2e/cli/ (TUI tests), evals/smoke-tests/ (CLI smoke
evals), and the cline-evals-regression.yml workflow that drove them.

Trim the root package.json scripts that pointed at this infra:
- cli:link, cli:build, cli:run, cli:build:production, cli:watch,
  cli:test, cli:dev, cli:unlink (all delegated into cli/)
- compile-standalone-npm and postcompile-standalone-npm (only the
  removed npm-main/npm-nightly workflows called them)
- test:e2e:cli:tui (only the removed cli-tui-tests.yml called it)
- eval:smoke:*, which chained through cli:build + cli:link

Drop the trailing `cd ../cli && npx tsc --noEmit` segment from
check-types so the root typecheck stops walking into cli/.

The cli/ source itself stays in place for a separate removal PR.

* docs(evals): note removed smoke-tests layer

The evals/README.md and evals/ARCHITECTURE.md were structured around
smoke-tests as Layer 2 of the pyramid. With evals/smoke-tests/ and
the eval:smoke scripts gone, those references are stale. Add a top-
of-doc banner pointing at the removal rather than gutting both files
in this PR; a follow-up can scrub the structure when the framework
is updated for the new SDK CLI.

* chore(evals): restore smoke-tests, disable workflow pending rewire

The smoke-test scenarios in evals/smoke-tests/ are CLI-agnostic — each
scenario is just a config.json prompt plus optional template files —
so they're worth preserving across the legacy CLI sunset. Restore the
directory and the cline-evals-regression.yml workflow, but reduce the
workflow's triggers to workflow_dispatch only so it doesn't auto-run
in its current legacy-CLI-coupled form. Add a header comment pointing
at the rewire work.

Update the evals/README.md and evals/ARCHITECTURE.md banners from
"removed" to "temporarily disabled" to match reality.

Wiring the workflow at the new SDK CLI (and restoring the eval:smoke
root scripts) is left for whoever picks up the eval framework refresh.

* chore(evals): restore eval:smoke:run for ad-hoc smoke checks

The runner (`evals/smoke-tests/run-smoke-tests.ts`) shells out to
whichever `cline` is on $PATH, so it already works against the new
SDK CLI once `npm i -g cline` installs it. Add back just the single
`eval:smoke:run` script so docs and manual validation have a working
entrypoint. The build-and-link chain (`eval:smoke:build`, `eval:smoke`,
`eval:smoke:ci`) stays out — those need rewiring before they function.

Update the README/ARCHITECTURE banners accordingly.

* fix(ci): update SDK npm repository metadata

* fix(ci): tighten SDK publish workflows
This commit is contained in:
Saoud Rizwan
2026-05-11 19:13:25 -07:00
committed by GitHub
parent 6363b4ff6e
commit 9b6fba6214
50 changed files with 91 additions and 2764 deletions
-83
View File
@@ -1,83 +0,0 @@
name: CLI TUI Tests
on:
pull_request:
branches:
- main
workflow_dispatch:
workflow_call:
permissions:
contents: read
jobs:
cli-tui-tests:
name: CLI TUI Tests
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install dependencies
run: npm ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build CLI
run: npm run cli:build
- name: Run TUI Tests
id: tui_tests
run: |
npm run test:e2e:cli:tui 2>&1 | tee tui-test-output.log
exit_code=${PIPESTATUS[0]}
echo "tui_exit_code=$exit_code" >> $GITHUB_OUTPUT
exit $exit_code
- name: Write failure summary
if: always() && steps.tui_tests.outcome != 'success' && steps.tui_tests.outcome != 'skipped'
run: |
echo "## ❌ CLI TUI Tests Failed" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Step outcome:** \`${{ steps.tui_tests.outcome }}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Test Output" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
if [ -f tui-test-output.log ]; then
cat tui-test-output.log >> $GITHUB_STEP_SUMMARY
else
echo "(no test output captured — process may have been killed before output was flushed)" >> $GITHUB_STEP_SUMMARY
fi
echo '```' >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Debugging" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **TUI traces** are attached as artifacts below — download and inspect them to see terminal state at the point of failure." >> $GITHUB_STEP_SUMMARY
echo "- **To view a trace replay/Run a TUI Trace: ** run \`npx tui-test show-trace path/to/trace/file\` in your terminal" >> $GITHUB_STEP_SUMMARY
echo "- **Full test log** is also attached as an artifact." >> $GITHUB_STEP_SUMMARY
echo "- Tests run with \`retries: 2\` so any failure shown is a consistent failure, not a flake." >> $GITHUB_STEP_SUMMARY
- name: Upload TUI traces
if: always() && steps.tui_tests.outcome != 'success' && steps.tui_tests.outcome != 'skipped'
uses: actions/upload-artifact@v4
with:
name: tui-test-traces
path: tests/e2e/cli/tui-traces/
retention-days: 14
if-no-files-found: warn
- name: Upload test log
if: always() && steps.tui_tests.outcome != 'success' && steps.tui_tests.outcome != 'skipped'
uses: actions/upload-artifact@v4
with:
name: tui-test-log
path: tui-test-output.log
retention-days: 14
if-no-files-found: warn
+8 -15
View File
@@ -1,21 +1,14 @@
name: Smoke Tests
# Temporarily disabled: this workflow built and linked the legacy CLI
# (`cd cli && npm install && npm run build && npm link`) before running the
# smoke-test scenarios. The legacy CLI publish chain has been retired in
# favor of the SDK CLI at `sdk/apps/cli/`. The scenarios under
# `evals/smoke-tests/scenarios/` are CLI-agnostic and should be re-enabled
# once the build step is repointed at the new SDK CLI. Until then, only
# manual `workflow_dispatch` runs are accepted (and will fail in their
# current form).
on:
push:
branches: [main]
paths:
- 'src/core/**'
- 'src/shared/**'
- 'proto/**'
- 'evals/**'
- '.github/workflows/cline-evals-regression.yml'
pull_request:
paths:
- 'src/core/**'
- 'src/shared/**'
- 'proto/**'
- 'evals/**'
- '.github/workflows/cline-evals-regression.yml'
workflow_dispatch:
permissions:
-111
View File
@@ -1,111 +0,0 @@
name: Publish NPM Release
on:
workflow_call:
inputs:
confirm_publish:
description: 'Type "publish" to confirm you want to publish to NPM'
required: true
type: string
permissions:
contents: write # Required for pushing tags
id-token: write # Required for npm trusted publishing (OIDC)
checks: write # Required by test workflow
pull-requests: write # Required by test workflow
jobs:
test:
uses: ./.github/workflows/test.yml
publish-npm-release:
needs: test
name: Publish Cline CLI to NPM
if: github.repository == 'cline/cline' && github.ref == 'refs/heads/main' && inputs.confirm_publish == 'publish'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "24.x"
registry-url: "https://registry.npmjs.org"
- name: Install root dependencies and CLI dependencies
run: npm ci --include=optional # this will also install cli deps because "cli" in included in root package.json workspaces field
- name: Generate Protos
run: npm run protos
- name: Read release version
id: version
run: |
# Read version from cli/package.json
VERSION=$(node -p "require('./cli/package.json').version")
echo "Release version: $VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Build standalone NPM package
env:
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
CLINE_ENVIRONMENT: production
OTEL_TELEMETRY_ENABLED: "1"
OTEL_METRICS_EXPORTER: otlp
OTEL_LOGS_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: node scripts/package-npm.mjs
- name: Verify build output
run: |
echo "Checking dist-standalone directory..."
ls -la dist-standalone/
echo "Verifying CLI binaries..."
ls -lh cli/bin/cline-* || echo "Warning: CLI binaries not found"
echo "Checking package.json in dist-standalone..."
cat dist-standalone/package.json | grep version
- name: Publish to NPM with latest tag
run: |
echo "Publishing version ${{ steps.version.outputs.version }} to NPM with tag 'latest'..."
cd dist-standalone
npm publish --tag latest --access public
- name: Tag release
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag "v${{ steps.version.outputs.version }}-cli"
git push origin "v${{ steps.version.outputs.version }}-cli"
- name: Summary
run: |
echo "✅ Successfully published cline@${{ steps.version.outputs.version }} to NPM with tag 'latest'"
echo ""
echo "📦 Install with: npm install -g cline"
echo "🔗 NPM: https://www.npmjs.com/package/cline/v/${{ steps.version.outputs.version }}"
- name: Post release to Slack
uses: slackapi/slack-github-action@v3.0.1
with:
method: chat.postMessage
token: ${{ secrets.SLACK_RELEASE_BOT_TOKEN }}
payload: |
channel: "C0APVKGGZFC"
text: "Cline CLI v${{ steps.version.outputs.version }}"
blocks:
- type: "section"
text:
type: "mrkdwn"
text: "*Cline CLI v${{ steps.version.outputs.version }}*"
- type: "context"
elements:
- type: "mrkdwn"
text: "<https://www.npmjs.com/package/cline/v/${{ steps.version.outputs.version }}|View on npm>"
-136
View File
@@ -1,136 +0,0 @@
name: Publish NPM Nightly
on:
workflow_call:
inputs:
force_publish:
description: "Force publish even if there are no commits in the last 24 hours"
required: false
type: boolean
default: false
permissions:
contents: read
id-token: write # Required for npm trusted publishing (OIDC)
checks: write # Required by test workflow
pull-requests: write # Required by test workflow
jobs:
test:
uses: ./.github/workflows/test.yml
publish-npm-nightly:
needs: test
name: Publish Cline CLI (Nightly) to NPM
if: github.repository == 'cline/cline' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Check for recent commits
id: check_commits
env:
FORCE_PUBLISH: ${{ inputs.force_publish }}
run: |
if [ "$FORCE_PUBLISH" = "true" ]; then
echo "force_publish enabled, proceeding with publish"
echo "skip=false" >> $GITHUB_OUTPUT
exit 0
fi
if [ $(git rev-list --count HEAD --since="24 hours ago") -eq 0 ]; then
echo "No commits in last 24 hours, skipping publish"
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "Found recent commits, proceeding with publish"
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Setup Node.js
if: steps.check_commits.outputs.skip != 'true'
uses: actions/setup-node@v4
with:
node-version: "24.x"
registry-url: "https://registry.npmjs.org"
- name: Install root dependencies and CLI dependencies
if: steps.check_commits.outputs.skip != 'true'
run: npm ci --include=optional # this will also install cli deps because "cli" in included in root package.json workspaces field
- name: Generate Protos
if: steps.check_commits.outputs.skip != 'true'
run: npm run protos
- name: Generate nightly version with timestamp
if: steps.check_commits.outputs.skip != 'true'
id: version
run: |
# Read base version from cli/package.json (e.g., "2.0.0")
BASE_VERSION=$(node -p "require('./cli/package.json').version")
# Generate timestamp (Unix epoch seconds)
TIMESTAMP=$(date +%s)
# Create unique nightly version: 1.0.9-nightly.1736365200
VERSION="${BASE_VERSION}-nightly.${TIMESTAMP}"
echo "Base version: $BASE_VERSION"
echo "Generated nightly version: $VERSION"
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Update cli/package.json with nightly version
if: steps.check_commits.outputs.skip != 'true'
run: |
# Update version with timestamp-based nightly version
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('cli/package.json', 'utf8'));
pkg.version = '${{ steps.version.outputs.version }}';
fs.writeFileSync('cli/package.json', JSON.stringify(pkg, null, '\t'));
"
echo "Using version ${{ steps.version.outputs.version }} for build"
cat cli/package.json | grep '"version"'
- name: Build and package CLI
if: steps.check_commits.outputs.skip != 'true'
env:
TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }}
ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }}
CLINE_ENVIRONMENT: production
OTEL_TELEMETRY_ENABLED: "1"
OTEL_METRICS_EXPORTER: otlp
OTEL_LOGS_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: node scripts/package-npm.mjs
- name: Verify build output
if: steps.check_commits.outputs.skip != 'true'
run: |
echo "Checking dist-standalone directory..."
ls -la dist-standalone/
echo "Verifying CLI binaries..."
ls -lh cli/bin/cline-* || echo "Warning: CLI binaries not found"
echo "Checking package.json in dist-standalone..."
cat dist-standalone/package.json | grep version
- name: Publish to NPM with nightly tag
if: steps.check_commits.outputs.skip != 'true'
run: |
echo "Publishing version ${{ steps.version.outputs.version }} to NPM with tag 'nightly'..."
cd dist-standalone
npm publish --tag nightly --access public
- name: Summary
if: steps.check_commits.outputs.skip != 'true'
run: |
echo "✅ Successfully published cline@${{ steps.version.outputs.version }} to NPM with tag 'nightly'"
echo ""
echo "📦 Install with: npm install -g cline@nightly"
echo "🔗 NPM: https://www.npmjs.com/package/cline/v/${{ steps.version.outputs.version }}"
-215
View File
@@ -1,215 +0,0 @@
# Build and Pack CLI
#
# Builds a CLI tarball from any branch/commit and publishes it as a GitHub Release.
# Requires write access to the repository (maintainers/collaborators only).
#
# Security: Split into two jobs to isolate untrusted build code from write tokens.
# The build job runs arbitrary ref code with zero permissions. The release job
# only runs trusted GitHub Actions with write scope.
#
# Usage (helper script, auto-detects current branch):
# ./scripts/build-cli-artifact.sh
# ./scripts/build-cli-artifact.sh feature/my-changes
# ./scripts/build-cli-artifact.sh feature/my-changes 1234 # comments on PR
#
# Usage (gh CLI directly):
# gh workflow run pack-cli.yml -f ref=main
# gh workflow run pack-cli.yml -f ref=abc123 -f pr_number=1234
#
# Install the built CLI (no auth required):
# npm install -g https://github.com/cline/cline/releases/download/cli-build-<sha>/cline-<ver>.tgz
#
# Find releases:
# gh release list --limit 10
name: Build and Pack CLI
permissions:
contents: read
on:
workflow_dispatch:
inputs:
ref:
description: 'Branch, tag, or commit SHA to build (leave empty for default branch)'
required: false
type: string
pr_number:
description: 'PR number to comment on with install instructions (optional)'
required: false
type: number
jobs:
# ── Build job: runs untrusted ref code with ZERO permissions ──
build:
name: Build CLI
runs-on: ubuntu-latest
permissions: {}
outputs:
commit_sha: ${{ steps.commit.outputs.sha }}
tarball: ${{ steps.pack.outputs.tarball }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
persist-credentials: false
- name: Get commit SHA
id: commit
run: |
COMMIT_SHA=$(git rev-parse --short HEAD)
echo "sha=$COMMIT_SHA" >> $GITHUB_OUTPUT
echo "Building from commit: $COMMIT_SHA"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20.x"
- name: Install dependencies
run: npm ci --include=optional
- name: Generate Protos
run: npm run protos
- name: Build standalone package
run: node scripts/package-npm.mjs
- name: Create Tarball
id: pack
run: |
cd dist-standalone
TARBALL=$(npm pack)
echo "tarball=$TARBALL" >> $GITHUB_OUTPUT
echo "Created tarball: $TARBALL"
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: cli-tarball
path: dist-standalone/*.tgz
# ── Release job: only trusted Actions code, with write permissions ──
release:
name: Release CLI
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: cli-tarball
path: dist-standalone
- name: Create GitHub Release
id: create_release
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const path = require('path');
const commit = '${{ needs.build.outputs.commit_sha }}';
const tarball = '${{ needs.build.outputs.tarball }}';
// Delete existing release/tag if re-running for the same commit
const tagName = `cli-build-${commit}`;
try {
const existing = await github.rest.repos.getReleaseByTag({
owner: context.repo.owner,
repo: context.repo.repo,
tag: tagName
});
await github.rest.repos.deleteRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: existing.data.id
});
await github.rest.git.deleteRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `tags/${tagName}`
});
core.info(`Deleted existing release for ${tagName}`);
} catch (e) {
// Release doesn't exist yet, that's fine
}
// Create a release
const release = await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: tagName,
name: `CLI Build (${commit})`,
body: `Automated CLI build from commit ${commit}\n\nInstall with:\n\`\`\`bash\nnpm install -g https://github.com/${context.repo.owner}/${context.repo.repo}/releases/download/${tagName}/${tarball}\n\`\`\``,
draft: false,
prerelease: true
});
// Upload the tarball as a release asset
const tarballPath = path.join('dist-standalone', tarball);
const tarballData = fs.readFileSync(tarballPath);
await github.rest.repos.uploadReleaseAsset({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: release.data.id,
name: tarball,
data: tarballData
});
const downloadUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/releases/download/${tagName}/${tarball}`;
core.setOutput('release_url', release.data.html_url);
core.setOutput('download_url', downloadUrl);
- name: Comment on PR with download instructions
if: inputs.pr_number != ''
uses: actions/github-script@v7
with:
script: |
const commit = '${{ needs.build.outputs.commit_sha }}';
const releaseUrl = '${{ steps.create_release.outputs.release_url }}';
const downloadUrl = '${{ steps.create_release.outputs.download_url }}';
const prNumber = ${{ inputs.pr_number || 0 }};
if (!prNumber) return;
const comment = `## 📦 CLI Build Ready
A CLI build has been created for commit \`${commit}\`.
### Install Directly from URL (No Authentication Required!)
\`\`\`bash
npm install -g ${downloadUrl}
\`\`\`
### Alternative: Download and Install
\`\`\`bash
curl -L ${downloadUrl} -o cline.tgz
npm install -g ./cline.tgz
\`\`\`
📦 [View Release](${releaseUrl})
`;
await github.rest.issues.createComment({
issue_number: prNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});
- name: Summary
run: |
echo "✅ CLI build complete!"
echo ""
echo "📦 Release: ${{ steps.create_release.outputs.release_url }}"
echo "🔗 Download URL: ${{ steps.create_release.outputs.download_url }}"
echo ""
echo "Install from anywhere (no authentication required):"
echo " npm install -g ${{ steps.create_release.outputs.download_url }}"
@@ -1,60 +0,0 @@
name: Publish CLI (Trusted)
on:
schedule:
- cron: "0 12 * * *" # 4 AM PST (UTC-8) = 12 UTC
workflow_dispatch:
inputs:
publish_target:
description: "Which publish flow to run"
required: true
default: "main"
type: choice
options:
- main
- nightly
confirm_publish:
description: 'Required when publish_target=main. Type "publish" to confirm release publish.'
required: false
type: string
force_nightly_publish:
description: "Force nightly publish even with no commits in last 24h"
required: false
type: boolean
default: false
permissions:
id-token: write # Required for npm trusted publishing (OIDC)
contents: write # Required because npm-main creates/pushes git tags
checks: write # Required by nested reusable test workflow
pull-requests: write # Required by nested reusable test workflow
jobs:
cli-tui-tests:
uses: ./.github/workflows/cli-tui-tests.yml
publish-main:
needs: cli-tui-tests
if: |
github.repository == 'cline/cline' && (
github.event_name == 'workflow_dispatch' &&
github.event.inputs.publish_target == 'main' &&
github.event.inputs.confirm_publish == 'publish' &&
!endsWith(github.actor, '[bot]')
)
uses: ./.github/workflows/npm-main.yaml
secrets: inherit
with:
confirm_publish: ${{ github.event.inputs.confirm_publish }}
publish-nightly:
needs: cli-tui-tests
if: |
github.repository == 'cline/cline' && (
github.event_name == 'schedule' ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.publish_target == 'nightly')
)
uses: ./.github/workflows/npm-nightly.yaml
secrets: inherit
with:
force_publish: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.force_nightly_publish == 'true' }}
@@ -31,6 +31,10 @@ permissions:
contents: read
id-token: write
defaults:
run:
working-directory: sdk
jobs:
publish-main:
name: Publish cline
@@ -38,7 +42,7 @@ jobs:
contents: write
id-token: write
if: |
github.repository == 'cline/sdk' &&
github.repository == 'cline/cline' &&
github.ref == 'refs/heads/main' &&
github.event_name == 'workflow_dispatch' &&
github.event.inputs.publish_target == 'main' &&
@@ -101,12 +105,12 @@ jobs:
PACKAGE_VERSION=$(node -p "require('./apps/cli/package.json').version")
if [ "$PACKAGE_VERSION" != "$VERSION" ]; then
echo "apps/cli/package.json version ${PACKAGE_VERSION} does not match ${TAG}"
echo "sdk/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 "apps/cli/package.json has invalid version: ${VERSION}"
echo "sdk/apps/cli/package.json has invalid version: ${VERSION}"
exit 1
fi
@@ -134,7 +138,7 @@ jobs:
- name: Build platform binaries
run: bun script/build.ts --install-native-variants --skip-sdk-build
working-directory: apps/cli
working-directory: sdk/apps/cli
- name: Verify build output
run: |
@@ -167,10 +171,11 @@ jobs:
ls -lh "$dir/bin/"
done
# TODO: re-enable NPM_CONFIG_PROVENANCE: "true" when repo is public
- name: Publish to NPM with latest tag
env:
NPM_CONFIG_PROVENANCE: "true"
run: bun script/publish-npm.ts --tag latest
working-directory: apps/cli
working-directory: sdk/apps/cli
- name: Create GitHub release
env:
@@ -214,7 +219,7 @@ jobs:
contents: read
id-token: write
if: |
github.repository == 'cline/sdk' &&
github.repository == 'cline/cline' &&
github.ref == 'refs/heads/main' &&
(
github.event_name == 'schedule' ||
@@ -321,7 +326,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: apps/cli
working-directory: sdk/apps/cli
- name: Verify build output
if: steps.check_commits.outputs.skip != 'true'
@@ -355,11 +360,12 @@ jobs:
ls -lh "$dir/bin/"
done
# TODO: re-enable NPM_CONFIG_PROVENANCE: "true" when repo is public
- name: Publish to NPM with nightly tag
if: steps.check_commits.outputs.skip != 'true'
env:
NPM_CONFIG_PROVENANCE: "true"
run: bun script/publish-npm.ts --tag nightly
working-directory: apps/cli
working-directory: sdk/apps/cli
- name: Summary
if: steps.check_commits.outputs.skip != 'true'
@@ -1,4 +1,4 @@
name: "Publish SDK Nightly Release"
name: "Publish New SDK Extension Nightly"
on:
schedule:
@@ -17,7 +17,7 @@ env:
jobs:
publish:
name: Publish Cline (Nightly SDK) Extension
name: Publish Cline New SDK Extension Nightly
if: github.repository == 'cline/cline' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: PublishNightly
@@ -16,15 +16,23 @@ on:
required: false
type: boolean
default: false
confirm_publish:
description: 'Required when channel=latest. Type "publish" to confirm release publish.'
required: false
type: string
schedule:
# Run nightly at 2:00 AM UTC
- cron: "0 2 * * *"
defaults:
run:
working-directory: sdk
jobs:
test:
permissions:
contents: read
uses: ./.github/workflows/test.yml
uses: ./.github/workflows/sdk-test.yml
publish-sdk:
needs: test
@@ -32,7 +40,17 @@ jobs:
permissions:
contents: write
id-token: write
if: github.repository == 'cline/sdk' && github.ref == 'refs/heads/main'
if: |
github.repository == 'cline/cline' &&
github.ref == 'refs/heads/main' &&
(
github.event_name != 'workflow_dispatch' ||
inputs.channel != 'latest' ||
(
inputs.confirm_publish == 'publish' &&
!endsWith(github.actor, '[bot]')
)
)
runs-on: ubuntu-latest
steps:
@@ -67,7 +85,7 @@ jobs:
exit 0
fi
if [ $(git rev-list --count HEAD --since="24 hours ago") -eq 0 ]; then
if [ "$(git rev-list --count HEAD --since="24 hours ago")" -eq 0 ]; then
echo "No commits in last 24 hours, skipping publish"
echo "skip=true" >> $GITHUB_OUTPUT
else
@@ -156,6 +174,8 @@ jobs:
# Publish sequentially in dependency order: shared → llms → agents → core → sdk
- name: Publish @cline/shared
if: steps.check_commits.outputs.skip != 'true'
env:
NPM_CONFIG_PROVENANCE: "true"
run: |
CHANNEL="${{ steps.channel.outputs.channel }}"
echo "Publishing @cline/shared@${{ steps.version.outputs.version }} with tag '${CHANNEL}'..."
@@ -165,6 +185,8 @@ jobs:
- name: Publish @cline/llms
if: steps.check_commits.outputs.skip != 'true'
env:
NPM_CONFIG_PROVENANCE: "true"
run: |
CHANNEL="${{ steps.channel.outputs.channel }}"
echo "Publishing @cline/llms@${{ steps.version.outputs.version }} with tag '${CHANNEL}'..."
@@ -174,6 +196,8 @@ jobs:
- name: Publish @cline/agents
if: steps.check_commits.outputs.skip != 'true'
env:
NPM_CONFIG_PROVENANCE: "true"
run: |
CHANNEL="${{ steps.channel.outputs.channel }}"
echo "Publishing @cline/agents@${{ steps.version.outputs.version }} with tag '${CHANNEL}'..."
@@ -183,6 +207,8 @@ jobs:
- name: Publish @cline/core
if: steps.check_commits.outputs.skip != 'true'
env:
NPM_CONFIG_PROVENANCE: "true"
run: |
CHANNEL="${{ steps.channel.outputs.channel }}"
echo "Publishing @cline/core@${{ steps.version.outputs.version }} with tag '${CHANNEL}'..."
@@ -192,6 +218,8 @@ jobs:
- name: Publish @cline/sdk
if: steps.check_commits.outputs.skip != 'true'
env:
NPM_CONFIG_PROVENANCE: "true"
run: |
CHANNEL="${{ steps.channel.outputs.channel }}"
echo "Publishing @cline/sdk@${{ steps.version.outputs.version }} with tag '${CHANNEL}'..."
@@ -225,14 +253,14 @@ jobs:
run: |
VERSION="${{ steps.version.outputs.version }}"
CHANNEL="${{ steps.channel.outputs.channel }}"
echo "✅ Published SDK packages with tag '${CHANNEL}':"
echo "Published SDK packages with tag '${CHANNEL}':"
echo " - @cline/shared@${VERSION}"
echo " - @cline/llms@${VERSION}"
echo " - @cline/agents@${VERSION}"
echo " - @cline/core@${VERSION}"
echo " - @cline/sdk@${VERSION}"
if [ "$CHANNEL" = "latest" ]; then
echo "✅ Created git tags:"
echo "Created git tags:"
echo " - sdk/shared/v${VERSION}"
echo " - sdk/llms/v${VERSION}"
echo " - sdk/agents/v${VERSION}"
@@ -1,18 +1,28 @@
name: Tests
name: SDK Tests
on:
push:
branches:
- main
paths:
- "sdk/**"
- ".github/workflows/sdk-test.yml"
workflow_dispatch:
pull_request:
branches:
- main
paths:
- "sdk/**"
- ".github/workflows/sdk-test.yml"
workflow_call:
permissions:
contents: read
defaults:
run:
working-directory: sdk
jobs:
quality-checks:
runs-on: ubuntu-latest
+3 -1
View File
@@ -1,5 +1,7 @@
# Cline Evals Architecture
> Note: Smoke tests (Layer 2) are partially disabled while the eval framework is repointed at the new SDK CLI. The scenarios under `evals/smoke-tests/` are preserved and `npm run eval:smoke:run` still works against whatever `cline` is on `$PATH` (install with `npm i -g cline`). The build-and-link helpers (`eval:smoke:build`, `eval:smoke`, `eval:smoke:ci`) and the auto-running `cline-evals-regression.yml` workflow are off until someone wires the build step at the new SDK CLI.
## Overview
The evals system provides multi-layered testing for Cline's AI capabilities.
@@ -220,7 +222,7 @@ ls evals/smoke-tests/results/latest/<scenario>/<model>/workspace-trial-1/
## CI Integration
Smoke tests run automatically on merge to `main` via `.github/workflows/cline-evals-regression.yml`.
Smoke tests are temporarily disabled. `.github/workflows/cline-evals-regression.yml` accepts manual `workflow_dispatch` only until the build step is repointed at the new SDK CLI.
**Triggers:**
- Push to `main` branch (paths: `src/core/**`, `src/shared/**`, `proto/**`)
+2
View File
@@ -2,6 +2,8 @@
A layered testing system for measuring Cline's performance at different levels.
> Note: Smoke tests (Layer 2) are partially disabled while the eval framework is repointed at the new SDK CLI. The scenarios under `evals/smoke-tests/` are preserved and `npm run eval:smoke:run` still works against whatever `cline` is on `$PATH` (install with `npm i -g cline`). The build-and-link helpers (`eval:smoke:build`, `eval:smoke`, `eval:smoke:ci`) and the auto-running `cline-evals-regression.yml` workflow are off until someone wires the build step at the new SDK CLI.
## Directory Structure
```
+2 -16
View File
@@ -384,17 +384,8 @@
"vscode:prepublish": "npm run package",
"compile": "npm run check-types && npm run lint && node esbuild.mjs",
"compile-standalone": "npm run check-types && npm run lint && node esbuild.mjs --standalone",
"compile-standalone-npm": "npm run protos && npm run check-types && npm run lint && node esbuild.mjs --standalone",
"cli:link": "cd cli && npm run link",
"cli:build": "npm run protos && cd cli && npm run build",
"cli:run": "node cli/dist/cli.mjs",
"cli:build:production": "cd cli && npm run build:production",
"cli:watch": "cd cli && npm run watch",
"cli:test": "cd cli && npm run test",
"test:install": "bash scripts/test-install.sh",
"cli:dev": "cd cli && npm run dev",
"postcompile-standalone": "node scripts/package-standalone.mjs",
"postcompile-standalone-npm": "node scripts/package-npm.mjs",
"dev": "npm run protos && npm run watch",
"watch": "npx npm-run-all -p watch:*",
"watch:esbuild": "node esbuild.mjs --watch",
@@ -409,7 +400,7 @@
"clean:all": "npm run clean:build && npm run clean:deps",
"compile-tests": "node ./scripts/build-tests.js",
"watch-tests": "tsc -p . -w --outDir out",
"check-types": "npm run protos && npx tsc --noEmit && cd webview-ui && npx tsc --noEmit && cd ../cli && npx tsc --noEmit",
"check-types": "npm run protos && npx tsc --noEmit && cd webview-ui && npx tsc --noEmit",
"lint": "biome lint --no-errors-on-unmatched --files-ignore-unknown=true --diagnostic-level=error && npm run lint:proto",
"lint:proto": "bash ./scripts/proto-lint.sh",
"format": "biome format --changed --no-errors-on-unmatched --files-ignore-unknown=true --diagnostic-level=error",
@@ -428,7 +419,6 @@
"test:e2e:build": "vsce package --allow-package-secrets sendgrid --out dist/e2e.vsix",
"test:e2e": "playwright install && npm run test:e2e:build && node src/test/e2e/utils/build.mjs && playwright test",
"test:e2e:optimal": "npm run test:e2e:build && node src/test/e2e/utils/build.mjs && playwright test",
"test:e2e:cli:tui": "cd tests/e2e/cli && tui-test",
"test:e2e:ui": "npx tsx scripts/interactive-playwright.ts",
"install:all": "npm install && cd webview-ui && npm install",
"dev:webview": "cd webview-ui && npm run dev",
@@ -443,11 +433,7 @@
"docs:rename-file": "cd docs && npm run rename",
"report-issue": "node scripts/report-issue.js",
"storybook": "cd webview-ui && npm run storybook",
"cli:unlink": "cd cli && npm run unlink",
"eval:smoke:build": "npm run cli:build && npm run cli:link",
"eval:smoke:run": "npx tsx evals/smoke-tests/run-smoke-tests.ts",
"eval:smoke": "npm run eval:smoke:build && npm run eval:smoke:run",
"eval:smoke:ci": "npm run eval:smoke:build && npm run eval:smoke:run -- --trials 1 --parallel"
"eval:smoke:run": "npx tsx evals/smoke-tests/run-smoke-tests.ts"
},
"lint-staged": {
"src/shared/storage/state-keys.ts": [
-44
View File
@@ -1,44 +0,0 @@
#!/bin/bash
# Build CLI release for a specific ref/commit using GitHub Actions
#
# Usage:
# ./scripts/build-cli-artifact.sh [ref] [pr_number]
#
# Examples:
# ./scripts/build-cli-artifact.sh # Build from current branch
# ./scripts/build-cli-artifact.sh main # Build from main branch
# ./scripts/build-cli-artifact.sh abc123 # Build from commit abc123
# ./scripts/build-cli-artifact.sh feature/new 1234 # Build from branch and comment on PR #1234
set -e
REF="${1:-$(git rev-parse --abbrev-ref HEAD)}"
PR_NUMBER="${2:-}"
echo "🚀 Triggering CLI build workflow..."
echo " Branch/commit: $REF"
# Build args array
ARGS=(-f "ref=$REF")
if [ -n "$PR_NUMBER" ]; then
ARGS+=(-f "pr_number=$PR_NUMBER")
echo " Will comment on PR #$PR_NUMBER"
fi
# Trigger the workflow
gh workflow run pack-cli.yml "${ARGS[@]}"
echo ""
echo "✅ Workflow triggered!"
echo ""
echo "The workflow will create a GitHub Release with a public download URL."
echo ""
echo "To monitor the workflow:"
echo " gh run list --workflow=pack-cli.yml --limit 5"
echo ""
echo "Once complete, find the release:"
echo " gh release list --limit 10"
echo ""
echo "Install from the release URL (no authentication required):"
echo " npm install -g https://github.com/cline/cline/releases/download/cli-build-<commit>/cline-<version>.tgz"
-192
View File
@@ -1,192 +0,0 @@
#!/usr/bin/env node
/**
* NPM Package Builder for Cline CLI
*
* This script builds the Cline CLI NPM package (dist-standalone/).
* It packages the CLI from cli/.
*
* Usage: node scripts/package-npm.mjs
*
* Prerequisites:
* - cd cli && npm run build:production
*/
import { fileURLToPath } from "node:url"
import { execSync } from "child_process"
import dotenv from "dotenv"
import fs from "fs"
import { cp } from "fs/promises"
import path from "path"
const BUILD_DIR = "dist-standalone"
const CLI_DIR = "cli"
const IS_VERBOSE = process.argv.includes("-v") || process.argv.includes("--verbose")
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const rootDir = path.resolve(__dirname, "..")
// Load .env from repo root
dotenv.config({ path: path.join(rootDir, ".env") })
async function main() {
console.log("🚀 Building Cline CLI NPM Package (TypeScript)\n")
await cleanBuildDir()
setupEnvironmentVariables()
await buildTypeScriptCli()
await copyCliDist()
await copyPackageJson()
await copyReadme()
await createNpmIgnoreFile()
console.log("\n✅ Build complete!")
console.log(`\n📦 NPM package ready in ${BUILD_DIR}/`)
console.log(`To publish: cd ${BUILD_DIR} && npm publish`)
}
function setupEnvironmentVariables() {
// Use a different API key for CLI error capturing, to redirect CLI errors to a different project
const cliErrorTrackingKey = process.env.CLI_ERROR_SERVICE_API_KEY
if (cliErrorTrackingKey) {
process.env.ERROR_SERVICE_API_KEY = cliErrorTrackingKey
// If we're sending to a different project, enable exception autocapture
process.env.ENABLE_ERROR_AUTOCAPTURE = "true"
log_verbose("Set ERROR_SERVICE_API_KEY for build")
}
}
/**
* Clean the build directory
*/
async function cleanBuildDir() {
console.log("Cleaning build directory...")
await rmrf(BUILD_DIR)
fs.mkdirSync(BUILD_DIR, { recursive: true })
console.log(`✓ ${BUILD_DIR}/ cleaned`)
}
/**
* Build the TypeScript CLI
*/
async function buildTypeScriptCli() {
console.log("Building TypeScript CLI...")
// Install dependencies if needed
if (!fs.existsSync(path.join(CLI_DIR, "node_modules"))) {
console.log("Installing cli dependencies...")
execSync("npm install", { stdio: "inherit", cwd: CLI_DIR })
}
// Build production bundle
execSync("npm run build:production", { stdio: "inherit", cwd: CLI_DIR, env: process.env })
console.log("✓ TypeScript CLI built")
}
/**
* Copy the CLI dist folder to build directory
*/
async function copyCliDist() {
console.log("Copying CLI distribution files...")
const distSource = path.join(CLI_DIR, "dist")
const distDest = path.join(BUILD_DIR, "dist")
if (!fs.existsSync(distSource)) {
console.error(`Error: CLI dist not found at ${distSource}`)
console.error(`Please run: cd cli && npm run build:production`)
process.exit(1)
}
await cpr(distSource, distDest)
// Make the CLI executable
const cliPath = path.join(distDest, "cli.mjs")
if (fs.existsSync(cliPath)) {
fs.chmodSync(cliPath, 0o755)
}
console.log(`✓ CLI dist copied to ${distDest}`)
}
/**
* Copy package.json from cli/ directory
*/
async function copyPackageJson() {
console.log("Copying package.json...")
const source = path.join(CLI_DIR, "package.json")
const dest = path.join(BUILD_DIR, "package.json")
await cpr(source, dest)
console.log(`✓ package.json copied`)
}
/**
* Copy README.md from cli/ directory
*/
async function copyReadme() {
console.log("Copying README...")
// Try cli README first, fall back to cli/ README
let readmeSource = path.join(CLI_DIR, "README.md")
if (!fs.existsSync(readmeSource)) {
readmeSource = path.join("cli", "README.md")
}
if (!fs.existsSync(readmeSource)) {
console.warn("Warning: No README.md found, skipping")
return
}
const readmeDest = path.join(BUILD_DIR, "README.md")
await cpr(readmeSource, readmeDest)
console.log(`✓ README.md copied from ${readmeSource}`)
}
/**
* Create .npmignore file to exclude unnecessary files
*/
async function createNpmIgnoreFile() {
console.log("Creating .npmignore file...")
const npmignoreContent = `# Exclude build artifacts and unnecessary files
*.map
*.ts
!*.d.ts
tsconfig.json
.eslintrc*
.prettierrc*
`
const npmignorePath = path.join(BUILD_DIR, ".npmignore")
fs.writeFileSync(npmignorePath, npmignoreContent)
console.log(`✓ .npmignore created`)
}
/* cp -r */
async function cpr(source, dest) {
log_verbose(`Copying ${source} -> ${dest}`)
await cp(source, dest, {
recursive: true,
preserveTimestamps: true,
dereference: false,
})
}
/* rm -rf */
async function rmrf(dir) {
if (fs.existsSync(dir)) {
log_verbose(`Removing ${dir}`)
fs.rmSync(dir, { recursive: true, force: true })
}
}
function log_verbose(...args) {
if (IS_VERBOSE) {
console.log(...args)
}
}
await main()
@@ -9,6 +9,8 @@ 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/`.
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
+2 -2
View File
@@ -9,8 +9,8 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/cline/sdk.git",
"directory": "apps/cli"
"url": "git+https://github.com/cline/cline.git",
"directory": "sdk/apps/cli"
},
"bin": {
"cline": "src/index.ts"
+2 -2
View File
@@ -3,8 +3,8 @@
"version": "0.0.39",
"repository": {
"type": "git",
"url": "https://github.com/cline/sdk",
"directory": "packages/agents"
"url": "https://github.com/cline/cline",
"directory": "sdk/packages/agents"
},
"main": "dist/index.js",
"dependencies": {
+2 -2
View File
@@ -4,8 +4,8 @@
"version": "0.0.39",
"repository": {
"type": "git",
"url": "https://github.com/cline/sdk",
"directory": "packages/core"
"url": "https://github.com/cline/cline",
"directory": "sdk/packages/core"
},
"type": "module",
"types": "./dist/index.d.ts",
+2 -2
View File
@@ -4,8 +4,8 @@
"description": "Config-driven SDK for selecting, extending, and instantiating LLM providers and models",
"repository": {
"type": "git",
"url": "https://github.com/cline/sdk",
"directory": "packages/llms"
"url": "https://github.com/cline/cline",
"directory": "sdk/packages/llms"
},
"main": "./dist/index.js",
"module": "./dist/index.js",
+2 -2
View File
@@ -4,8 +4,8 @@
"version": "0.0.39",
"repository": {
"type": "git",
"url": "https://github.com/cline/sdk",
"directory": "packages/sdk"
"url": "https://github.com/cline/cline",
"directory": "sdk/packages/sdk"
},
"type": "module",
"types": "./dist/index.d.ts",
+2 -2
View File
@@ -4,8 +4,8 @@
"description": "Shared utilities, types, and schemas for Cline packages",
"repository": {
"type": "git",
"url": "https://github.com/cline/sdk",
"directory": "packages/shared"
"url": "https://github.com/cline/cline",
"directory": "sdk/packages/shared"
},
"type": "module",
"main": "dist/index.js",
-191
View File
@@ -1,191 +0,0 @@
// ---------------------------------------------------------------------------
// cline auth — CLI flag and contract tests
//
// These tests cover the `cline auth` subcommand behavior:
// - Interactive auth screen navigation
// - `cline auth -p <provider> -k <apiKey> -m <modelId>` golden path
// - Invalid provider / key / model error handling
// - Partial-flag fallback to interactive screen
// - `cline auth --help`
// ---------------------------------------------------------------------------
import { test } from "@microsoft/tui-test"
import { CLINE_BIN, TERMINAL_WIDE } from "../helpers/constants.js"
import { clineEnv } from "../helpers/env.js"
import { waitForAuthScreen } from "../helpers/page-objects/auth.js"
import { expectVisible } from "../helpers/terminal.js"
// ---------------------------------------------------------------------------
// cline auth (interactive screen — no flags)
// ---------------------------------------------------------------------------
test.describe("cline auth (interactive screen)", () => {
test.use({
program: { file: CLINE_BIN, args: ["auth"] },
...TERMINAL_WIDE,
env: clineEnv("unauthenticated"),
})
test("shows all auth options", async ({ terminal }) => {
await waitForAuthScreen(terminal)
})
test("can navigate options with keyUp / keyDown", async ({ terminal }) => {
await waitForAuthScreen(terminal)
terminal.keyDown()
terminal.keyUp()
// Still on the auth screen after navigation
await expectVisible(terminal, "Sign in with Cline")
})
})
// ---------------------------------------------------------------------------
// cline auth --help
// ---------------------------------------------------------------------------
test.describe("cline auth --help", () => {
test.use({
program: { file: CLINE_BIN, args: ["auth", "--help"] },
...TERMINAL_WIDE,
env: clineEnv("unauthenticated"),
})
test("shows auth help page", async ({ terminal }) => {
await expectVisible(terminal, "Usage:")
await expectVisible(terminal, "--provider")
await expectVisible(terminal, "--apikey")
await expectVisible(terminal, "--modelid")
await expectVisible(terminal, "--baseurl")
})
})
// ---------------------------------------------------------------------------
// cline auth with only partial flags → falls back to interactive screen
// ---------------------------------------------------------------------------
test.describe("cline auth --provider only (partial flags)", () => {
test.use({
program: { file: CLINE_BIN, args: ["auth", "--provider", "openai"] },
...TERMINAL_WIDE,
env: clineEnv("unauthenticated"),
})
test("ignores partial flags and shows interactive auth screen", async ({ terminal }) => {
await waitForAuthScreen(terminal)
})
})
test.describe("cline auth --apikey only (partial flags)", () => {
test.use({
program: {
file: CLINE_BIN,
args: ["auth", "--apikey", "sk-test-key"],
},
...TERMINAL_WIDE,
env: clineEnv("unauthenticated"),
})
test("ignores partial flags and shows interactive auth screen", async ({ terminal }) => {
await waitForAuthScreen(terminal)
})
})
test.describe("cline auth --modelid only (partial flags)", () => {
test.use({
program: {
file: CLINE_BIN,
args: ["auth", "--modelid", "gpt-4o"],
},
...TERMINAL_WIDE,
env: clineEnv("unauthenticated"),
})
test("ignores partial flags and shows interactive auth screen", async ({ terminal }) => {
await waitForAuthScreen(terminal)
})
})
test.describe("cline auth --baseurl only (partial flags)", () => {
test.use({
program: {
file: CLINE_BIN,
args: ["auth", "--baseurl", "https://api.example.com"],
},
...TERMINAL_WIDE,
env: clineEnv("unauthenticated"),
})
test("ignores partial flags and shows interactive auth screen", async ({ terminal }) => {
await waitForAuthScreen(terminal)
})
})
test.describe("cline auth --verbose only (partial flags)", () => {
test.use({
program: {
file: CLINE_BIN,
args: ["auth", "--verbose"],
},
...TERMINAL_WIDE,
env: clineEnv("unauthenticated"),
})
test("ignores --verbose and shows interactive auth screen", async ({ terminal }) => {
await waitForAuthScreen(terminal)
})
})
// ---------------------------------------------------------------------------
// cline auth --cwd
// User sees interactive auth screen; after authing, footer shows workspace dir
// ---------------------------------------------------------------------------
test.describe("cline auth --cwd", () => {
test.use({
program: {
file: CLINE_BIN,
args: ["auth", "--cwd", "/tmp"],
},
...TERMINAL_WIDE,
env: clineEnv("unauthenticated"),
})
test("shows interactive auth screen with --cwd flag", async ({ terminal }) => {
await waitForAuthScreen(terminal)
})
})
// ---------------------------------------------------------------------------
// cline auth --config <dir>
// User sees interactive auth screen; after authing, custom config dir exists
// with globalState.json and secrets.json; default ~/.cline does NOT exist
// ---------------------------------------------------------------------------
test.describe("cline auth --config", () => {
test.use({
program: {
file: CLINE_BIN,
args: ["auth", "--config", "configs/unauthenticated"],
},
...TERMINAL_WIDE,
env: clineEnv("unauthenticated"),
})
test("shows interactive auth screen with --config flag", async ({ terminal }) => {
await waitForAuthScreen(terminal)
})
})
// ---------------------------------------------------------------------------
// cline auth -p <invalid-provider> -k <key> -m <model>
// → should show "invalid provider" and exit 1
// ---------------------------------------------------------------------------
test.describe("cline auth with invalid provider", () => {
test.use({
program: {
file: CLINE_BIN,
args: ["auth", "--provider", "not-a-real-provider", "--apikey", "sk-test", "--modelid", "gpt-4o"],
},
...TERMINAL_WIDE,
env: clineEnv("unauthenticated"),
})
test("shows invalid provider error", async ({ terminal }) => {
await expectVisible(terminal, /invalid provider/i, { timeout: 5000 })
})
})
-51
View File
@@ -1,51 +0,0 @@
// ---------------------------------------------------------------------------
// cline config — CLI tests
//
// Covers:
// - `cline config --config <dir>` — shows config for specific directory
// - `cline config --help` — help page
// ---------------------------------------------------------------------------
import { test } from "@microsoft/tui-test"
import { CLINE_BIN, TERMINAL_WIDE } from "../helpers/constants.js"
import { clineEnv } from "../helpers/env.js"
import { expectVisible } from "../helpers/terminal.js"
// ---------------------------------------------------------------------------
// cline config --help
// ---------------------------------------------------------------------------
test.describe("cline config --help", () => {
test.use({
program: { file: CLINE_BIN, args: ["config", "--help"] },
...TERMINAL_WIDE,
env: clineEnv("default"),
})
test("shows config help page", async ({ terminal }) => {
await expectVisible(terminal, "Usage:")
await expectVisible(terminal, "--config")
})
})
// ---------------------------------------------------------------------------
// cline config --config <dir>
// Shows interactive config view for the specified directory
// ---------------------------------------------------------------------------
test.describe("cline config (default config)", () => {
test.use({
program: { file: CLINE_BIN, args: ["config"] },
...TERMINAL_WIDE,
env: clineEnv("default"),
})
})
test.describe("cline config --config (claude-sonnet-4.6)", () => {
test.use({
program: {
file: CLINE_BIN,
args: ["config", "--config", "configs/claude-sonnet-4.6"],
},
...TERMINAL_WIDE,
env: clineEnv("claude-sonnet-4.6"),
})
})
-18
View File
@@ -1,18 +0,0 @@
// ---------------------------------------------------------------------------
// cline dev — CLI tests
//
// Covers:
// - `cline dev log`
// ---------------------------------------------------------------------------
import { test } from "@microsoft/tui-test"
import { CLINE_BIN, TERMINAL_WIDE } from "../helpers/constants.js"
import { clineEnv } from "../helpers/env.js"
test.describe("cline dev log", () => {
test.use({
program: { file: CLINE_BIN, args: ["dev", "log"] },
...TERMINAL_WIDE,
env: clineEnv("default"),
})
})
-213
View File
@@ -1,213 +0,0 @@
// ---------------------------------------------------------------------------
// CLI flag behavioral tests
//
// These tests verify the runtime behavior of each CLI flag — not just that
// the flag appears in --help output (that's covered in tests/flags.test.ts),
// but that the flag actually changes what cline does.
//
// Tests marked ⚠️ in the spec reflect known gaps where the flag is accepted
// but currently has no observable effect. They are still written so the
// behavior can be asserted once the implementation catches up.
// ---------------------------------------------------------------------------
import { test } from "@microsoft/tui-test"
import { CLINE_BIN, TERMINAL_WIDE } from "../helpers/constants.js"
import { clineEnv } from "../helpers/env.js"
import { expectVisible } from "../utils.js"
// ---------------------------------------------------------------------------
// cline --act
// Starts cline in Act mode regardless of globalState
// ---------------------------------------------------------------------------
test.describe("cline --act", () => {
test.use({
program: { file: CLINE_BIN, args: ["--tui", "--act"] },
...TERMINAL_WIDE,
env: clineEnv("default"),
})
test("starts in Act mode", async ({ terminal }) => {
await expectVisible(terminal, "Act")
})
})
// ---------------------------------------------------------------------------
// cline --plan
// Starts cline in Plan mode regardless of globalState
// ---------------------------------------------------------------------------
test.describe("cline --plan", () => {
test.use({
program: { file: CLINE_BIN, args: ["--tui", "--plan"] },
...TERMINAL_WIDE,
env: clineEnv("default"),
})
test("starts in Plan mode", async ({ terminal }) => {
await expectVisible(terminal, "Plan")
})
})
// ---------------------------------------------------------------------------
// cline --timeout <n> ⚠️
// Current behavior: starts interactive mode and ignores timeout value
// ---------------------------------------------------------------------------
test.describe("cline --timeout (interactive mode, flag ignored) ⚠️", () => {
test.use({
program: { file: CLINE_BIN, args: ["--tui", "--timeout", "30"] },
...TERMINAL_WIDE,
env: clineEnv("default"),
})
test("starts interactive mode (timeout value currently ignored)", async ({ terminal }) => {
await expectVisible(terminal, /what can i do|plan|act/i)
})
})
// ---------------------------------------------------------------------------
// cline --model <model-id> ⚠️
// Current behavior: starts interactive mode and ignores model value
// ---------------------------------------------------------------------------
test.describe("cline --model (interactive mode, flag ignored) ⚠️", () => {
test.use({
program: {
file: CLINE_BIN,
args: ["--tui", "--model", "claude-3-5-haiku-20241022"],
},
...TERMINAL_WIDE,
env: clineEnv("default"),
})
test("starts interactive mode (model value currently ignored)", async ({ terminal }) => {
await expectVisible(terminal, /what can i do|plan|act/i)
})
})
// ---------------------------------------------------------------------------
// cline --verbose ⚠️
// Current behavior: starts interactive mode and ignores verbose value
// ---------------------------------------------------------------------------
test.describe("cline --verbose (interactive mode, flag ignored) ⚠️", () => {
test.use({
program: { file: CLINE_BIN, args: ["--tui", "--verbose"] },
...TERMINAL_WIDE,
env: clineEnv("default"),
})
test("starts interactive mode (verbose value currently ignored)", async ({ terminal }) => {
await expectVisible(terminal, /what can i do|plan|act/i)
})
})
// ---------------------------------------------------------------------------
// cline -c / cline --cwd <dir> ⚠️
// Starts cline in interactive mode with the cwd present in the client footer
// ---------------------------------------------------------------------------
test.describe("cline --cwd <dir>", () => {
test.use({
program: { file: CLINE_BIN, args: ["--tui", "--cwd", "/tmp"] },
...TERMINAL_WIDE,
env: clineEnv("default"),
})
test("starts interactive mode with --cwd flag", async ({ terminal }) => {
await expectVisible(terminal, /what can i do|plan|act/i)
})
})
test.describe("cline -c <dir> (short alias)", () => {
test.use({
program: { file: CLINE_BIN, args: ["--tui", "-c", "/tmp"] },
...TERMINAL_WIDE,
env: clineEnv("default"),
})
test("starts interactive mode with -c flag", async ({ terminal }) => {
await expectVisible(terminal, /what can i do|plan|act/i)
})
})
// ---------------------------------------------------------------------------
// cline --config <dir>
// Starts cline in interactive mode using settings from the custom config dir
// ---------------------------------------------------------------------------
test.describe("cline --config (claude-sonnet-4.6)", () => {
test.use({
program: {
file: CLINE_BIN,
args: ["--tui", "--config", "configs/claude-sonnet-4.6"],
},
...TERMINAL_WIDE,
env: clineEnv("claude-sonnet-4.6"),
})
test("starts interactive mode with custom config directory", async ({ terminal }) => {
await expectVisible(terminal, /what can i do|plan|act/i)
})
})
// ---------------------------------------------------------------------------
// cline --thinking ⚠️
// Starts cline in interactive mode with thinking turned on regardless of globalState
// (if thinking not supported, this flag is a no-op)
// ---------------------------------------------------------------------------
test.describe("cline --thinking ⚠️", () => {
test.use({
program: { file: CLINE_BIN, args: ["--tui", "--thinking"] },
...TERMINAL_WIDE,
env: clineEnv("default"),
})
test("starts interactive mode with --thinking flag", async ({ terminal }) => {
await expectVisible(terminal, /what can i do|plan|act/i)
})
})
// ---------------------------------------------------------------------------
// cline --reasoning-effort <level> ⚠️
// Starts cline in interactive mode with reasoning turned on regardless of globalState
// (if reasoning not supported, this flag is a no-op)
// ---------------------------------------------------------------------------
test.describe("cline --reasoning-effort ⚠️", () => {
test.use({
program: { file: CLINE_BIN, args: ["--tui", "--reasoning-effort", "high"] },
...TERMINAL_WIDE,
env: clineEnv("default"),
})
test("starts interactive mode with --reasoning-effort flag", async ({ terminal }) => {
await expectVisible(terminal, /what can i do|plan|act/i)
})
})
// ---------------------------------------------------------------------------
// cline --max-consecutive-mistakes <n>
// ---------------------------------------------------------------------------
test.describe("cline --max-consecutive-mistakes", () => {
test.use({
program: {
file: CLINE_BIN,
args: ["--tui", "--max-consecutive-mistakes", "5"],
},
...TERMINAL_WIDE,
env: clineEnv("default"),
})
test("starts interactive mode with --max-consecutive-mistakes flag", async ({ terminal }) => {
await expectVisible(terminal, /what can i do|plan|act/i)
})
})
// ---------------------------------------------------------------------------
// cline --double-check-completion
// ---------------------------------------------------------------------------
test.describe("cline --double-check-completion", () => {
test.use({
program: { file: CLINE_BIN, args: ["--tui", "--double-check-completion"] },
...TERMINAL_WIDE,
env: clineEnv("default"),
})
test("starts interactive mode with --double-check-completion flag", async ({ terminal }) => {
await expectVisible(terminal, /what can i do|plan|act/i)
})
})
-32
View File
@@ -1,32 +0,0 @@
// ---------------------------------------------------------------------------
// cline history — CLI tests
//
// Covers:
// - `cline history --limit X` — pagination limit
// - `cline history --page N` — page selection
// - `cline history --config` — custom config directory
// - `cline history --help` — help page
// ---------------------------------------------------------------------------
import { test } from "@microsoft/tui-test"
import { CLINE_BIN, TERMINAL_WIDE } from "../helpers/constants.js"
import { clineEnv } from "../helpers/env.js"
import { expectVisible } from "../helpers/terminal.js"
// ---------------------------------------------------------------------------
// cline history --help
// ---------------------------------------------------------------------------
test.describe("cline history --help", () => {
test.use({
program: { file: CLINE_BIN, args: ["history", "--help"] },
...TERMINAL_WIDE,
env: clineEnv("default"),
})
test("shows history help page with all flags", async ({ terminal }) => {
await expectVisible(terminal, "Usage:")
await expectVisible(terminal, "--limit")
await expectVisible(terminal, "--page")
await expectVisible(terminal, "--config")
})
})
-22
View File
@@ -1,22 +0,0 @@
# Test Fixture Configs
Each subdirectory here is a **Cline config directory** used by one or more test suites.
Tests point to a config via `clineEnv("<name>")` in `tests/e2e/cli/helpers/env.ts`.
## Available fixtures
| Directory | Purpose |
|-----------|---------|
| `default/` | Minimal config used for local/non-live CLI TUI tests. |
| `unauthenticated/` | Explicitly empty config (no provider or API keys). |
## Adding a new fixture
1. Create `configs/<name>/data/globalState.json` with the desired state.
2. Create `configs/<name>/data/settings/cline_mcp_settings.json` (can be `{ "mcpServers": {} }`).
3. Reference it in tests with `clineEnv("<name>")`.
## Secrets
API keys and secrets should never be committed.
If you create authenticated fixtures locally, keep `data/secrets.json` untracked.
@@ -1,42 +0,0 @@
{
"remoteRulesToggles": {},
"remoteWorkflowToggles": {},
"welcomeViewCompleted": true,
"actModeOpenRouterModelInfo": {
"name": "Anthropic: Claude Sonnet 4.6",
"maxTokens": 128000,
"contextWindow": 200000,
"supportsImages": true,
"supportsPromptCache": true,
"inputPrice": 3,
"outputPrice": 15,
"cacheWritesPrice": 3.75,
"cacheReadsPrice": 0.3,
"description": "Sonnet 4.6 is Anthropic's most capable Sonnet-class model yet, with frontier performance across coding, agents, and professional work. It excels at iterative development, complex codebase navigation, end-to-end project management with memory, polished document creation, and confident computer use for web QA and workflow automation.",
"thinkingConfig": {
"maxBudget": 6000
}
},
"planModeOpenRouterModelInfo": {
"name": "Anthropic: Claude Sonnet 4.6",
"maxTokens": 128000,
"contextWindow": 200000,
"supportsImages": true,
"supportsPromptCache": true,
"inputPrice": 3,
"outputPrice": 15,
"cacheWritesPrice": 3.75,
"cacheReadsPrice": 0.3,
"description": "Sonnet 4.6 is Anthropic's most capable Sonnet-class model yet, with frontier performance across coding, agents, and professional work. It excels at iterative development, complex codebase navigation, end-to-end project management with memory, polished document creation, and confident computer use for web QA and workflow automation.",
"thinkingConfig": {
"maxBudget": 6000
}
},
"actModeApiProvider": "cline",
"planModeApiProvider": "cline",
"actModeClineModelId": "anthropic/claude-sonnet-4.6",
"planModeClineModelId": "anthropic/claude-sonnet-4.6",
"openAiHeaders": {},
"sapAiCoreUseOrchestrationMode": true,
"ocaMode": "internal"
}
@@ -1,3 +0,0 @@
{
"mcpServers": {}
}
@@ -1,62 +0,0 @@
{
"remoteRulesToggles": {},
"remoteWorkflowToggles": {},
"actModeOpenRouterModelInfo": {
"name": "Anthropic: Claude Sonnet 4.6",
"maxTokens": 128000,
"contextWindow": 200000,
"supportsImages": true,
"supportsPromptCache": true,
"inputPrice": 3,
"outputPrice": 15,
"cacheWritesPrice": 3.75,
"cacheReadsPrice": 0.3,
"description": "Sonnet 4.6 is Anthropic's most capable Sonnet-class model yet, with frontier performance across coding, agents, and professional work. It excels at iterative development, complex codebase navigation, end-to-end project management with memory, polished document creation, and confident computer use for web QA and workflow automation.",
"thinkingConfig": {
"maxBudget": 6000
}
},
"planModeOpenRouterModelInfo": {
"name": "Anthropic: Claude Sonnet 4.6",
"maxTokens": 128000,
"contextWindow": 200000,
"supportsImages": true,
"supportsPromptCache": true,
"inputPrice": 3,
"outputPrice": 15,
"cacheWritesPrice": 3.75,
"cacheReadsPrice": 0.3,
"description": "Sonnet 4.6 is Anthropic's most capable Sonnet-class model yet, with frontier performance across coding, agents, and professional work. It excels at iterative development, complex codebase navigation, end-to-end project management with memory, polished document creation, and confident computer use for web QA and workflow automation.",
"thinkingConfig": {
"maxBudget": 6000
}
},
"actModeApiProvider": "cline",
"planModeApiProvider": "cline",
"actModeClineModelId": "anthropic/claude-sonnet-4.6",
"planModeClineModelId": "anthropic/claude-sonnet-4.6",
"welcomeViewCompleted": true,
"actModeThinkingBudgetTokens": 0,
"planModeThinkingBudgetTokens": 0,
"autoApprovalSettings": {
"version": 19,
"enabled": true,
"favorites": [],
"maxRequests": 20,
"actions": {
"readFiles": true,
"readFilesExternally": false,
"editFiles": false,
"editFilesExternally": false,
"executeSafeCommands": true,
"executeAllCommands": false,
"useBrowser": false,
"useMcp": true
},
"enableNotifications": false
},
"primaryRootIndex": 0,
"globalWorkflowToggles": {},
"globalClineRulesToggles": {},
"isNewUser": false
}
@@ -1,3 +0,0 @@
{
"mcpServers": {}
}
@@ -1,38 +0,0 @@
{
"apiProvider": null,
"apiKey": null,
"openRouterApiKey": null,
"awsAccessKey": null,
"awsSecretKey": null,
"awsSessionToken": null,
"awsRegion": null,
"vertexProjectId": null,
"vertexRegion": null,
"openAiBaseUrl": null,
"openAiApiKey": null,
"openAiModelId": null,
"ollamaModelId": null,
"ollamaBaseUrl": null,
"lmStudioModelId": null,
"lmStudioBaseUrl": null,
"anthropicBaseUrl": null,
"geminiApiKey": null,
"openAiNativeApiKey": null,
"deepSeekApiKey": null,
"mistralApiKey": null,
"unboundApiKey": null,
"requestyApiKey": null,
"togetherApiKey": null,
"qwenApiKey": null,
"doubaoApiKey": null,
"liteLlmBaseUrl": null,
"liteLlmApiKey": null,
"liteLlmModelId": null,
"clineApiKey": null,
"taskHistory": [],
"remoteRulesToggles": {},
"remoteWorkflowToggles": {},
"actModeThinkingBudgetTokens": 0,
"planModeThinkingBudgetTokens": 0,
"welcomeViewCompleted": false
}
@@ -1,3 +0,0 @@
{
"mcpServers": {}
}
-141
View File
@@ -1,141 +0,0 @@
import { test } from "@microsoft/tui-test"
import { CLINE_BIN } from "./helpers/constants.js"
import { expectVisible, testEnv } from "./utils.js"
const HELP_TERMINAL = { columns: 120, rows: 50 }
// ===========================================================================
// Root-level flag descriptions
// ===========================================================================
test.describe("root flag descriptions", () => {
test.use({
program: { file: CLINE_BIN, args: ["--help"] },
env: testEnv("default"),
...HELP_TERMINAL,
})
test("all root flags have correct descriptions", async ({ terminal }) => {
await expectVisible(terminal, [
"Run in act mode",
"Run in plan mode",
"yolo mode",
"timeout in seconds",
"Model to use",
"verbose output",
"Working directory",
"Configuration directory",
"extended thinking",
"Reasoning effort",
"consecutive mistakes",
"Output messages as JSON",
"first completion attempt",
"ACP",
"legacy terminal UI",
"Resume an existing task",
])
})
test("all short flag aliases are shown", async ({ terminal }) => {
await expectVisible(terminal, ["-a", "-p", "-y", "-t", "-m", "-v", "-c", "-T"])
})
})
// ===========================================================================
// Task subcommand flag descriptions
// ===========================================================================
test.describe("task flag descriptions", () => {
test.use({
program: { file: CLINE_BIN, args: ["task", "--help"] },
env: testEnv("default"),
...HELP_TERMINAL,
})
test("all task flags have correct descriptions", async ({ terminal }) => {
await expectVisible(terminal, [
"Run in act mode",
"Run in plan mode",
"yolo mode",
"timeout in seconds",
"Model to use",
"verbose output",
"Working directory",
"configuration directory",
"extended thinking",
"Reasoning effort",
"consecutive mistakes",
"Output messages as JSON",
"first completion attempt",
"Resume an existing task",
])
})
})
// ===========================================================================
// History subcommand flag descriptions
// ===========================================================================
test.describe("history flag descriptions", () => {
test.use({
program: { file: CLINE_BIN, args: ["history", "--help"] },
env: testEnv("default"),
...HELP_TERMINAL,
})
test("all history flags have correct descriptions", async ({ terminal }) => {
await expectVisible(terminal, "Number of tasks to show")
await expectVisible(terminal, "Page number")
await expectVisible(terminal, "configuration directory")
})
})
// ===========================================================================
// Auth subcommand flag descriptions
// ===========================================================================
test.describe("auth flag descriptions", () => {
test.use({
program: { file: CLINE_BIN, args: ["auth", "--help"] },
env: testEnv("default"),
...HELP_TERMINAL,
})
test("all auth flags have correct descriptions", async ({ terminal }) => {
await expectVisible(terminal, [
"Provider ID",
"API key",
"Model ID",
"Base URL",
"verbose output",
"Working directory",
"configuration directory",
])
})
})
// ===========================================================================
// Config subcommand flag descriptions
// ===========================================================================
test.describe("config flag descriptions", () => {
test.use({
program: { file: CLINE_BIN, args: ["config", "--help"] },
env: testEnv("default"),
...HELP_TERMINAL,
})
test("config --config flag description", async ({ terminal }) => {
await expectVisible(terminal, "configuration directory")
})
})
// ===========================================================================
// Update subcommand flag descriptions
// ===========================================================================
test.describe("update flag descriptions", () => {
test.use({
program: { file: CLINE_BIN, args: ["update", "--help"] },
env: testEnv("default"),
...HELP_TERMINAL,
})
test("update --verbose flag description", async ({ terminal }) => {
await expectVisible(terminal, "verbose output")
})
})
-86
View File
@@ -1,86 +0,0 @@
// ---------------------------------------------------------------------------
// CLI headless use cases (cline -y / cline --json / piped stdin)
//
// These tests run cline as a child process (no TUI harness) and assert on
// stdout, stderr, and exit codes.
//
// Tests tagged @live require a configured provider and are skipped by default.
// Run them with: CLINE_BIN=... npm test -- headless @live
// ---------------------------------------------------------------------------
import { test } from "@microsoft/tui-test"
import { CLINE_BIN, TERMINAL_WIDE } from "../helpers/constants.js"
import { clineEnv } from "../helpers/env.js"
import { expectVisible } from "../helpers/terminal.js"
// ---------------------------------------------------------------------------
// cline -y "tell me a joke"
// Golden path: prints "Task started" then LLM output, then exits 0.
// Unauthenticated: prints "Not authenticated" and exits 1.
// ---------------------------------------------------------------------------
test.describe("cline -y (headless yolo mode) — unauthenticated", () => {
test.use({
program: { file: CLINE_BIN, args: ["-y", "tell me a joke"] },
...TERMINAL_WIDE,
env: clineEnv("unauthenticated"),
})
test("prints Not authenticated and exits 1", async ({ terminal }) => {
await expectVisible(terminal, /not authenticated/i)
})
})
// ---------------------------------------------------------------------------
// echo "max paulus" | cline -y "print only the second word I gave you"
// Piped stdin test — uses TUI harness with stdin pre-written
// ---------------------------------------------------------------------------
test.describe("piped stdin | cline -y — unauthenticated", () => {
test.use({
program: {
file: "sh",
args: ["-c", `echo "max paulus" | ${CLINE_BIN} -y "print only the second word I gave you"`],
},
...TERMINAL_WIDE,
env: clineEnv("unauthenticated"),
})
test("prints Not Authenticated for piped stdin", async ({ terminal }) => {
await expectVisible(terminal, /not authenticated/i)
})
})
// ---------------------------------------------------------------------------
// cline -y --verbose "tell me a joke" 2>&1
// Golden path: prints task started, prompt, api request, reasoning, task_completion lines
// ---------------------------------------------------------------------------
test.describe("cline -y --verbose — unauthenticated", () => {
test.use({
program: {
file: "sh",
args: ["-c", `${CLINE_BIN} -y --verbose "tell me a joke" 2>&1`],
},
...TERMINAL_WIDE,
env: clineEnv("unauthenticated"),
})
test("shows verbose output or not-authenticated", async ({ terminal }) => {
await expectVisible(terminal, /not authenticated|verbose|task/i)
})
})
// ---------------------------------------------------------------------------
// cline --json "tell me a joke"
// All output must conform to JSON (one JSON object per line)
// ---------------------------------------------------------------------------
test.describe("cline --json — unauthenticated", () => {
test.use({
program: { file: CLINE_BIN, args: ["--json", "tell me a joke"] },
...TERMINAL_WIDE,
env: clineEnv("unauthenticated"),
})
test("outputs JSON error for unauthenticated", async ({ terminal }) => {
// cline --json when unauthenticated outputs a plain "Not authenticated" message
await expectVisible(terminal, /not authenticated/i)
})
})
-226
View File
@@ -1,226 +0,0 @@
import { test } from "@microsoft/tui-test"
import { CLINE_BIN } from "./helpers/constants.js"
import { expectVisible, testEnv } from "./utils.js"
const HELP_TERMINAL = { columns: 120, rows: 50 }
// ===========================================================================
// cline --help (root help)
// ===========================================================================
test.describe("cline --help", () => {
test.use({
program: { file: CLINE_BIN, args: ["--help"] },
env: testEnv("claude-sonnet-4.6"),
...HELP_TERMINAL,
})
test("shows Usage line and lists all subcommands", async ({ terminal }) => {
await expectVisible(terminal, [
"Usage:",
"task|t",
"history|h",
"config [options]",
"auth [options]",
"version",
"update [options]",
"dev ",
])
})
test("shows all root-level option flags", async ({ terminal }) => {
await expectVisible(terminal, [
"--act",
"--plan",
"--yolo",
"--timeout",
"--model",
"--verbose",
"--cwd",
"--config",
"--thinking",
"--reasoning-effort",
"--max-consecutive-mistakes",
"--json",
"--double-check-completion",
"--acp",
"--tui",
"--taskId",
])
})
})
// ===========================================================================
// cline -h (short help flag)
// ===========================================================================
test.describe("cline -h", () => {
test.use({
program: { file: CLINE_BIN, args: ["-h"] },
env: testEnv("claude-sonnet-4.6"),
...HELP_TERMINAL,
})
test("shows Usage line with short flag", async ({ terminal }) => {
await expectVisible(terminal, "Usage:")
})
})
// ===========================================================================
// cline task --help
// ===========================================================================
test.describe("cline task --help", () => {
test.use({
program: { file: CLINE_BIN, args: ["task", "--help"] },
env: testEnv("claude-sonnet-4.6"),
...HELP_TERMINAL,
})
test("shows task usage, prompt argument, and all flags", async ({ terminal }) => {
await expectVisible(terminal, [
"Usage:",
"prompt",
"--act",
"--plan",
"--yolo",
"--timeout",
"--model",
"--verbose",
"--cwd",
"--config",
"--thinking",
"--reasoning-effort",
"--max-consecutive-mistakes",
"--json",
"--double-check-completion",
"--taskId",
])
})
})
// ===========================================================================
// cline t --help (task alias)
// ===========================================================================
test.describe("cline t --help (task alias)", () => {
test.use({
program: { file: CLINE_BIN, args: ["t", "--help"] },
env: testEnv("claude-sonnet-4.6"),
...HELP_TERMINAL,
})
test("shows task usage and flags via alias", async ({ terminal }) => {
await expectVisible(terminal, ["Usage:", "--yolo"])
})
})
// ===========================================================================
// cline history --help
// ===========================================================================
test.describe("cline history --help", () => {
test.use({
program: { file: CLINE_BIN, args: ["history", "--help"] },
env: testEnv("claude-sonnet-4.6"),
...HELP_TERMINAL,
})
test("shows history usage and all flags", async ({ terminal }) => {
await expectVisible(terminal, ["Usage:", "--limit", "--page", "--config"])
})
})
// ===========================================================================
// cline h --help (history alias)
// ===========================================================================
test.describe("cline h --help (history alias)", () => {
test.use({
program: { file: CLINE_BIN, args: ["h", "--help"] },
env: testEnv("claude-sonnet-4.6"),
...HELP_TERMINAL,
})
test("shows history usage and flags via alias", async ({ terminal }) => {
await expectVisible(terminal, ["Usage:", "--limit"])
})
})
// ===========================================================================
// cline config --help
// ===========================================================================
test.describe("cline config --help", () => {
test.use({
program: { file: CLINE_BIN, args: ["config", "--help"] },
env: testEnv("claude-sonnet-4.6"),
...HELP_TERMINAL,
})
test("shows config usage and --config flag", async ({ terminal }) => {
await expectVisible(terminal, ["Usage:", "--config"])
})
})
// ===========================================================================
// cline auth --help
// ===========================================================================
test.describe("cline auth --help", () => {
test.use({
program: { file: CLINE_BIN, args: ["auth", "--help"] },
env: testEnv("claude-sonnet-4.6"),
...HELP_TERMINAL,
})
test("shows auth usage and all flags", async ({ terminal }) => {
await expectVisible(terminal, [
"Usage:",
"--provider",
"--apikey",
"--modelid",
"--baseurl",
"--verbose",
"--cwd",
"--config",
])
})
})
// ===========================================================================
// cline version --help
// ===========================================================================
test.describe("cline version --help", () => {
test.use({
program: { file: CLINE_BIN, args: ["version", "--help"] },
env: testEnv("claude-sonnet-4.6"),
...HELP_TERMINAL,
})
test("shows version command usage", async ({ terminal }) => {
await expectVisible(terminal, "Usage:")
})
})
// ===========================================================================
// cline update --help
// ===========================================================================
test.describe("cline update --help", () => {
test.use({
program: { file: CLINE_BIN, args: ["update", "--help"] },
env: testEnv("claude-sonnet-4.6"),
...HELP_TERMINAL,
})
test("shows update usage and --verbose flag", async ({ terminal }) => {
await expectVisible(terminal, ["Usage:", "--verbose"])
})
})
// ===========================================================================
// cline dev --help
// ===========================================================================
test.describe("cline dev --help", () => {
test.use({
program: { file: CLINE_BIN, args: ["dev", "--help"] },
env: testEnv("claude-sonnet-4.6"),
...HELP_TERMINAL,
})
test("shows dev usage and lists log subcommand", async ({ terminal }) => {
await expectVisible(terminal, ["Usage:", "log"])
})
})
-24
View File
@@ -1,24 +0,0 @@
// ---------------------------------------------------------------------------
// Shared constants for all test files.
//
// tui-test workers run with a minimal PATH, so we resolve the binary
// explicitly rather than relying on PATH lookup at test runtime.
// ---------------------------------------------------------------------------
import fs from "fs"
import path from "path"
function resolveClineBin(): string {
const localBin = path.resolve(process.cwd(), "..", "..", "..", "cli", "dist", "cli.mjs")
if (fs.existsSync(localBin)) {
return localBin
}
throw new Error("Unable to resolve cline binary. build cli/dist/cli.mjs.")
}
export const CLINE_BIN = resolveClineBin()
// Standard terminal dimensions used across test suites
export const TERMINAL_WIDE = { columns: 120, rows: 50 } as const
export const TERMINAL_NARROW = { columns: 80, rows: 30 } as const
-44
View File
@@ -1,44 +0,0 @@
// ---------------------------------------------------------------------------
// Environment helpers for test setup.
//
// Usage:
// test.use({ env: clineEnv("default") });
// test.use({ env: clineEnv("claude-sonnet-4.6") });
// test.use({ env: clineEnv("/absolute/path/to/config") });
// ---------------------------------------------------------------------------
import path from "path"
const TEST_SUITE_ROOT = new URL("../", import.meta.url).pathname
/**
* Build the process environment for a cline test.
*
* @param configDir - Named config under `configs/`, or an absolute path.
* @param extra - Additional env vars to merge in (override defaults).
*/
export function clineEnv(configDir: string, extra: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv {
const clinePath = path.isAbsolute(configDir) ? configDir : path.join(TEST_SUITE_ROOT, "configs", configDir)
// Remove CI env var so Ink's `is-in-ci` check doesn't disable interactive
// rendering. When CI=true (set by GitHub Actions / act), Ink treats the
// environment as non-interactive and skips rendering to stdout — even
// inside a real PTY — which causes tui-test traces to be empty.
const { CI: _ci, ...cleanEnv } = process.env
return {
...cleanEnv,
CLINE_TELEMETRY_DISABLED: "1",
CLINE_DIR: clinePath,
NO_UPDATE_NOTIFIER: "1",
CLINE_NO_AUTO_UPDATE: "1",
...extra,
}
}
/**
* @deprecated Use `clineEnv` instead. Kept for backward compatibility.
*/
export function testEnv(configDir: string): NodeJS.ProcessEnv {
return clineEnv(configDir)
}
@@ -1,6 +0,0 @@
import { Terminal } from "@microsoft/tui-test/lib/terminal/term"
import { expectVisible } from "../terminal.js"
export async function waitForAuthScreen(terminal: Terminal): Promise<void> {
await expectVisible(terminal, ["Sign in with Cline", "Sign in with ChatGPT", "Use your own API key"])
}
@@ -1,84 +0,0 @@
// ---------------------------------------------------------------------------
// Page-object helpers for the main Cline chat view.
// ---------------------------------------------------------------------------
import type { Terminal } from "@microsoft/tui-test/lib/terminal/term"
import { expectVisible, typeAndSubmit } from "../terminal.js"
/** Wait for the main chat view to be ready */
export async function waitForChatReady(terminal: Terminal): Promise<void> {
await expectVisible(terminal, "What can I do for you?")
}
/** Submit a prompt in the chat input */
export async function submitPrompt(terminal: Terminal, prompt: string, delay = 500): Promise<void> {
await waitForChatReady(terminal)
await typeAndSubmit(terminal, prompt, delay)
}
/** Toggle between Plan and Act mode by pressing Tab */
export async function togglePlanAct(terminal: Terminal): Promise<void> {
terminal.write("\t")
// Wait for the mode indicator to update rather than sleeping a fixed amount
await expectVisible(terminal, /● Plan|● Act/)
}
/** Toggle auto-approve-all with Shift+Tab */
export async function toggleAutoApproveAll(terminal: Terminal): Promise<void> {
terminal.write("\x1b[Z") // shift tab
}
/** Wait for "Task completed" to appear */
export async function waitForTaskCompleted(terminal: Terminal, timeout = 60_000): Promise<void> {
await expectVisible(terminal, "Task completed", { timeout })
}
/** Wait for the "Start New Task (1)" and "Exit (2)" buttons */
export async function waitForTaskButtons(terminal: Terminal, timeout = 60_000): Promise<void> {
await expectVisible(terminal, ["Start New Task", "Exit"], { timeout })
}
/** Press "1" to start a new task after task completion */
export async function startNewTask(terminal: Terminal): Promise<void> {
terminal.write("1")
}
/** Press "2" to exit after task completion */
export async function exitAfterTask(terminal: Terminal): Promise<void> {
terminal.write("2")
}
/** Wait for a permission prompt and approve it (press "1" / Save) */
export async function approvePermission(terminal: Terminal): Promise<void> {
terminal.write("1")
}
/** Wait for a permission prompt and reject it (press "2" / Reject) */
export async function rejectPermission(terminal: Terminal): Promise<void> {
terminal.write("2")
}
/** Navigate to /settings */
export async function openSettings(terminal: Terminal): Promise<void> {
await waitForChatReady(terminal)
await typeAndSubmit(terminal, "/settings")
await expectVisible(terminal, "Settings (Esc to close)")
}
/** Navigate to /history */
export async function openHistory(terminal: Terminal): Promise<void> {
await waitForChatReady(terminal)
await typeAndSubmit(terminal, "/history")
}
/** Navigate to /models */
export async function openModels(terminal: Terminal): Promise<void> {
await waitForChatReady(terminal)
await typeAndSubmit(terminal, "/models")
}
/** Navigate to /skills */
export async function openSkills(terminal: Terminal): Promise<void> {
await waitForChatReady(terminal)
await typeAndSubmit(terminal, "/skills")
}
@@ -1,69 +0,0 @@
// ---------------------------------------------------------------------------
// Page-object helpers for the /settings view.
// ---------------------------------------------------------------------------
import type { Terminal } from "@microsoft/tui-test/lib/terminal/term"
import { expectVisible } from "../terminal.js"
const TAB_ORDER = ["API", "Auto-approve", "Features", "Account", "Other"] as const
export type SettingsTab = (typeof TAB_ORDER)[number]
/**
* Navigate to a specific settings tab by pressing Right from the API tab (index 0).
* Waits for each tab's content to appear before pressing the next key, making
* navigation deterministic regardless of machine speed.
*/
export async function goToSettingsTab(terminal: Terminal, tab: SettingsTab): Promise<void> {
const targetIndex = TAB_ORDER.indexOf(tab)
for (let i = 0; i < targetIndex; i++) {
terminal.keyRight()
// Wait for the next tab's content to appear before pressing again
await assertTabContent(terminal, TAB_ORDER[i + 1])
}
}
/** Assert the API tab content is visible */
export async function assertApiTab(terminal: Terminal): Promise<void> {
await expectVisible(terminal, ["Provider:", "Model ID:"])
}
/** Assert the Auto-approve tab content is visible */
export async function assertAutoApproveTab(terminal: Terminal): Promise<void> {
await expectVisible(terminal, ["Read project files", "Execute safe commands", "Edit project files"])
}
/** Assert the Features tab content is visible */
export async function assertFeaturesTab(terminal: Terminal): Promise<void> {
await expectVisible(terminal, ["Subagents", "Web tools", "Double-check completion"])
}
/** Assert the Account tab content is visible */
export async function assertAccountTab(terminal: Terminal): Promise<void> {
// The account tab shows sign-in options when not authenticated to Cline
await expectVisible(terminal, /sign in|sign out/i)
}
/** Assert the Other tab content is visible */
export async function assertOtherTab(terminal: Terminal): Promise<void> {
await expectVisible(terminal, ["Preferred language:", "Cline v"])
}
/**
* Assert the content for a given tab is visible.
* Used internally by goToSettingsTab to confirm navigation landed correctly.
*/
export async function assertTabContent(terminal: Terminal, tab: SettingsTab): Promise<void> {
switch (tab) {
case "API":
return assertApiTab(terminal)
case "Auto-approve":
return assertAutoApproveTab(terminal)
case "Features":
return assertFeaturesTab(terminal)
case "Account":
return assertAccountTab(terminal)
case "Other":
return assertOtherTab(terminal)
}
}
-100
View File
@@ -1,100 +0,0 @@
// ---------------------------------------------------------------------------
// Process-level helpers for headless / contract-style CLI tests.
//
// These helpers spawn cline as a child process and return stdout, stderr,
// and exit code — without going through the TUI harness. Use these for:
// - Exit code assertions
// - Pure stdout/stderr contract tests
// - Piped stdin tests (echo "..." | cline -y ...)
// - JSON output validation
// - Timeout behavior
// ---------------------------------------------------------------------------
import { SpawnSyncOptions, spawnSync } from "child_process"
import { CLINE_BIN } from "./constants.js"
import { clineEnv } from "./env.js"
export interface RunResult {
stdout: string
stderr: string
exitCode: number | null
combined: string
}
export interface RunOptions {
/** Named config dir under `configs/`, or absolute path */
config?: string
/** Additional env vars */
env?: NodeJS.ProcessEnv
/** Stdin to pipe into the process */
stdin?: string
/** Timeout in ms (default: 30_000) */
timeout?: number
/** Working directory for the spawned process */
cwd?: string
}
/**
* Run `cline [args]` synchronously and return stdout/stderr/exitCode.
*
* Suitable for deterministic, fast-exiting commands like:
* cline --help, cline --version, cline auth -p ... -k ..., etc.
*/
export function runCline(args: string[], opts: RunOptions = {}): RunResult {
const { config = "default", env: extraEnv = {}, stdin, timeout = 30_000, cwd = process.cwd() } = opts
const spawnOpts: SpawnSyncOptions = {
encoding: "utf8",
timeout,
cwd,
env: { ...clineEnv(config), ...extraEnv },
input: stdin,
}
const result = spawnSync(CLINE_BIN, args, spawnOpts)
const stdout = (result.stdout as string) ?? ""
const stderr = (result.stderr as string) ?? ""
return {
stdout,
stderr,
exitCode: result.status,
combined: stdout + stderr,
}
}
/**
* Assert that a runCline result exited with the expected code.
* Throws a descriptive error if the code doesn't match.
*/
export function assertExitCode(result: RunResult, expected: number, label = ""): void {
if (result.exitCode !== expected) {
const prefix = label ? `[${label}] ` : ""
throw new Error(
`${prefix}Expected exit code ${expected}, got ${result.exitCode}.\n` +
`stdout: ${result.stdout}\n` +
`stderr: ${result.stderr}`,
)
}
}
/**
* Assert that stdout/stderr contains a given string or matches a regex.
*/
export function assertOutput(
result: RunResult,
pattern: string | RegExp,
stream: "stdout" | "stderr" | "combined" = "combined",
label = "",
): void {
const text = result[stream]
const matches = typeof pattern === "string" ? text.includes(pattern) : pattern.test(text)
if (!matches) {
const prefix = label ? `[${label}] ` : ""
throw new Error(
`${prefix}Expected ${stream} to match ${pattern}.\n` + `stdout: ${result.stdout}\n` + `stderr: ${result.stderr}`,
)
}
}
-53
View File
@@ -1,53 +0,0 @@
// ---------------------------------------------------------------------------
// Terminal interaction helpers for TUI tests.
//
// These wrap common tui-test patterns to keep test bodies readable.
// ---------------------------------------------------------------------------
import { expect } from "@microsoft/tui-test"
import type { Terminal } from "@microsoft/tui-test/lib/terminal/term"
// ---------------------------------------------------------------------------
// Core wait / assertion helpers
// ---------------------------------------------------------------------------
const maxTimeoutMs = 10_000
/**
* Wait for one or more text strings/regexes to appear on screen.
*
* @example
* await expectVisible(terminal, "What can I do for you?");
* await expectVisible(terminal, ["/help", "/settings"], { timeout: 5000 });
*/
export async function expectVisible(
terminal: Terminal,
text: string | RegExp | (string | RegExp)[],
options: { timeout?: number } = { timeout: maxTimeoutMs },
): Promise<void> {
const items = Array.isArray(text) ? text : [text]
await Promise.all(
items.map((t) => {
// tui-test uses String.prototype.matchAll internally, which requires
// the global flag on RegExp arguments. Ensure it is set.
if (t instanceof RegExp && !t.flags.includes("g")) {
t = new RegExp(t.source, `${t.flags}g`)
}
const locator = terminal.getByText(t, {
full: true,
strict: false,
})
return expect(locator).toBeVisible(options.timeout !== undefined ? { timeout: options.timeout } : undefined)
}),
)
}
/**
* Type text into the terminal and press Enter.
* Waits `delay` ms between writing and submitting to let the UI settle.
*/
export async function typeAndSubmit(terminal: Terminal, text: string, delay = 500): Promise<void> {
terminal.write(text)
await new Promise((resolve) => setTimeout(resolve, delay))
terminal.submit()
}
-64
View File
@@ -1,64 +0,0 @@
import { test } from "@microsoft/tui-test"
import { CLINE_BIN } from "./helpers/constants.js"
import { assertApiTab, assertAutoApproveTab, assertFeaturesTab, assertOtherTab } from "./helpers/page-objects/settings.js"
import { expectVisible, testEnv, typeAndSubmit } from "./utils.js"
test.describe("cline interactive basics", () => {
test.use({
program: { file: CLINE_BIN, args: ["--tui"] },
rows: 50,
columns: 120,
env: testEnv("default"),
})
test("shows logo, prompt, mode toggles, and hints", async ({ terminal }) => {
await expectVisible(terminal, ["What can I do for you?", "@@@@@@", "Plan", "Act", "@ for files", "Tab"])
})
test("shows slash commands after / input", async ({ terminal }) => {
await expectVisible(terminal, "What can I do for you?")
await typeAndSubmit(terminal, "/")
await expectVisible(terminal, ["/help", "/settings", "/models"], {
timeout: 5000,
})
})
test("opens /settings and navigates tabs with left/right arrows", async ({ terminal }) => {
await expectVisible(terminal, "What can I do for you?")
await typeAndSubmit(terminal, "/settings")
await expectVisible(terminal, "Settings (Esc to close)")
// API tab (default)
await assertApiTab(terminal)
await expectVisible(terminal, "Use separate models for Plan and Act")
// Auto-approve tab
terminal.keyRight()
await assertAutoApproveTab(terminal)
// Features tab
terminal.keyRight()
await assertFeaturesTab(terminal)
await expectVisible(terminal, ["Strict plan mode", "Native tool call", "Parallel tool calling"])
// Account tab
terminal.keyRight()
await expectVisible(terminal, ["Sign in to access Cline features", "Sign in with Cline"])
// Other tab
terminal.keyRight()
await assertOtherTab(terminal)
// Left once from Other should move back to Account
terminal.keyLeft()
await expectVisible(terminal, "Sign in to access Cline features")
// Two rights from Account should wrap back to API
terminal.keyRight()
await assertOtherTab(terminal)
terminal.keyRight()
await assertApiTab(terminal)
})
})
-219
View File
@@ -1,219 +0,0 @@
// ---------------------------------------------------------------------------
// CLI Interactive use cases — main chat view
//
// Covers:
// - `cline` launches interactive view (authed / unauthed)
// - /settings navigation and tab verification
// - /models view
// - /history view
// - /skills view
// - Plan/Act mode toggle (Tab)
// - Plan task → toggle to Act → task executes
// - Act task → file edit permission prompt → Save / Reject
// - Task completed → Start New Task / Exit buttons
// - Auto-approve settings
// - Subagents
// - Web tools
// - Auto-approve all (Shift+Tab)
// ---------------------------------------------------------------------------
import { test } from "@microsoft/tui-test"
import { CLINE_BIN, TERMINAL_WIDE } from "../helpers/constants.js"
import { clineEnv } from "../helpers/env.js"
import {
openHistory,
openModels,
openSettings,
openSkills,
toggleAutoApproveAll,
togglePlanAct,
waitForChatReady,
} from "../helpers/page-objects/chat.js"
import {
assertApiTab,
assertAutoApproveTab,
assertFeaturesTab,
assertOtherTab,
goToSettingsTab,
} from "../helpers/page-objects/settings.js"
import { expectVisible } from "../helpers/terminal.js"
// ---------------------------------------------------------------------------
// cline (unauthenticated) → shows interactive auth view
// ---------------------------------------------------------------------------
test.describe("cline (unauthenticated) — shows auth view", () => {
test.use({
program: { file: CLINE_BIN, args: ["--tui"] },
...TERMINAL_WIDE,
env: clineEnv("unauthenticated"),
})
test("shows interactive auth view when not authenticated", async ({ terminal }) => {
await expectVisible(terminal, /sign in|authenticate|api key/i, {
timeout: 10_000,
})
})
})
// ---------------------------------------------------------------------------
// cline (authenticated) → shows main chat view
// ---------------------------------------------------------------------------
test.describe("cline (authenticated) — shows chat view", () => {
test.use({
program: { file: CLINE_BIN, args: ["--tui"] },
...TERMINAL_WIDE,
env: clineEnv("claude-sonnet-4.6"),
})
test("shows interactive chat view", async ({ terminal }) => {
await waitForChatReady(terminal)
})
})
// ---------------------------------------------------------------------------
// /settings — tab navigation
// ---------------------------------------------------------------------------
test.describe("/settings — tab navigation", () => {
test.use({
program: { file: CLINE_BIN, args: ["--tui"] },
...TERMINAL_WIDE,
env: clineEnv("claude-sonnet-4.6"),
})
test("opens settings and shows API tab by default", async ({ terminal }) => {
await openSettings(terminal)
await assertApiTab(terminal)
})
test("can navigate to Auto-approve tab with keyRight", async ({ terminal }) => {
await openSettings(terminal)
terminal.keyRight()
await assertAutoApproveTab(terminal)
})
test("can navigate to Features tab", async ({ terminal }) => {
await openSettings(terminal)
await goToSettingsTab(terminal, "Features")
await assertFeaturesTab(terminal)
})
test("can navigate to Other tab", async ({ terminal }) => {
await openSettings(terminal)
await goToSettingsTab(terminal, "Other")
await assertOtherTab(terminal)
})
test("pressing Escape closes settings", async ({ terminal }) => {
await openSettings(terminal)
terminal.keyEscape()
await waitForChatReady(terminal)
})
})
// ---------------------------------------------------------------------------
// /models — browse models
// ---------------------------------------------------------------------------
test.describe("/models — model browser", () => {
test.use({
program: { file: CLINE_BIN, args: ["--tui"] },
...TERMINAL_WIDE,
env: clineEnv("claude-sonnet-4.6"),
})
test("opens models view with featured models", async ({ terminal }) => {
await openModels(terminal)
await expectVisible(terminal, "Browse all models...")
})
test("pressing Escape returns to chat view", async ({ terminal }) => {
await openModels(terminal)
await expectVisible(terminal, /model|browse/i, { timeout: 5000 })
terminal.keyEscape()
await waitForChatReady(terminal)
})
})
// ---------------------------------------------------------------------------
// /history — task history
// ---------------------------------------------------------------------------
test.describe("/history — task history", () => {
test.use({
program: { file: CLINE_BIN, args: ["--tui"] },
...TERMINAL_WIDE,
env: clineEnv("claude-sonnet-4.6"),
})
test("opens history view", async ({ terminal }) => {
await openHistory(terminal)
await expectVisible(terminal, /history|task/i)
})
test("pressing Escape closes history", async ({ terminal }) => {
await openHistory(terminal)
await expectVisible(terminal, /history|task/i)
terminal.keyEscape()
await waitForChatReady(terminal)
})
})
// ---------------------------------------------------------------------------
// /skills — skills view
// ---------------------------------------------------------------------------
test.describe("/skills — skills view", () => {
test.use({
program: { file: CLINE_BIN, args: ["--tui"] },
...TERMINAL_WIDE,
env: clineEnv("default"),
})
test("opens skills view", async ({ terminal }) => {
await openSkills(terminal)
await expectVisible(terminal, "Skills (Esc to close)")
})
test("pressing Escape closes skills", async ({ terminal }) => {
await openSkills(terminal)
await expectVisible(terminal, "Skills (Esc to close)")
terminal.keyEscape()
await waitForChatReady(terminal)
})
})
// ---------------------------------------------------------------------------
// Plan/Act mode toggle
// ---------------------------------------------------------------------------
test.describe("Plan/Act mode toggle", () => {
test.use({
program: { file: CLINE_BIN, args: ["--tui"] },
...TERMINAL_WIDE,
env: clineEnv("default"),
})
test("pressing Tab toggles between Plan and Act mode", async ({ terminal }) => {
await waitForChatReady(terminal)
// Default should show Act
await expectVisible(terminal, "○ Plan ● Act")
await togglePlanAct(terminal)
// After toggle, the other mode should be active
await expectVisible(terminal, "● Plan ○ Act")
})
})
// ---------------------------------------------------------------------------
// Auto-approve all (Shift+Tab)
// ---------------------------------------------------------------------------
test.describe("Auto-approve all — Shift+Tab toggle", () => {
test.use({
program: { file: CLINE_BIN, args: ["--tui"] },
...TERMINAL_WIDE,
env: clineEnv("default"),
})
test("Shift+Tab toggles auto-approve-all setting", async ({ terminal }) => {
await waitForChatReady(terminal)
await expectVisible(terminal, "Auto-approve all disabled")
await toggleAutoApproveAll(terminal)
// TODO: verify config store is updated
await expectVisible(terminal, "Auto-approve all enabled")
})
})
-3
View File
@@ -1,3 +0,0 @@
{
"type": "module"
}
-10
View File
@@ -1,10 +0,0 @@
import { defineConfig } from "@microsoft/tui-test"
export default defineConfig({
retries: 2,
workers: 4,
trace: true,
traceFolder: "./tui-traces",
testMatch: "./**/*.test.ts",
timeout: 30_000,
})
-9
View File
@@ -1,9 +0,0 @@
// ---------------------------------------------------------------------------
// Backward-compatible re-exports.
//
// Existing test files import from "./utils.js" — this file keeps those
// imports working while the canonical implementations live in helpers/.
// ---------------------------------------------------------------------------
export { clineEnv, testEnv } from "./helpers/env.js"
export { expectVisible, typeAndSubmit } from "./helpers/terminal.js"
-45
View File
@@ -1,45 +0,0 @@
import { test } from "@microsoft/tui-test"
import { CLINE_BIN } from "./helpers/constants.js"
import { expectVisible, testEnv } from "./utils.js"
// ---------------------------------------------------------------------------
// cline --version (root flag)
// ---------------------------------------------------------------------------
test.describe("cline --version", () => {
test.use({
program: { file: CLINE_BIN, args: ["--version"] },
env: testEnv("claude-sonnet-4.6"),
})
test("prints the version string", async ({ terminal }) => {
await expectVisible(terminal, /\d+\.\d+\.\d+/g)
})
})
// ---------------------------------------------------------------------------
// cline -V (short flag)
// ---------------------------------------------------------------------------
test.describe("cline -V", () => {
test.use({
program: { file: CLINE_BIN, args: ["-V"] },
env: testEnv("claude-sonnet-4.6"),
})
test("prints the version string with short flag", async ({ terminal }) => {
await expectVisible(terminal, /\d+\.\d+\.\d+/g)
})
})
// ---------------------------------------------------------------------------
// cline version (subcommand)
// ---------------------------------------------------------------------------
test.describe("cline version subcommand", () => {
test.use({
program: { file: CLINE_BIN, args: ["version"] },
env: testEnv("claude-sonnet-4.6"),
})
test("prints 'Cline CLI version:' message", async ({ terminal }) => {
await expectVisible(terminal, ["Cline CLI version:", /\d+\.\d+\.\d+/g])
})
})